@@ -1,15 +1,29 @@
|
||||
<script context="module">
|
||||
/** @type {import('@sveltejs/kit').ErrorLoad} */
|
||||
export function load({ error, status }) {
|
||||
console.log(error);
|
||||
return {
|
||||
props: {
|
||||
error: `${status}: ${error.message}`
|
||||
error,
|
||||
status
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export let status;
|
||||
export let error;
|
||||
</script>
|
||||
|
||||
<h1 class="text-xl font-bold">{error}</h1>
|
||||
<div class="mx-auto flex h-screen flex-col items-center justify-center px-4">
|
||||
<div class="pb-10 text-7xl font-bold">{status}</div>
|
||||
<div class="text-3xl font-bold">Ooops you are lost! But don't be afraid!</div>
|
||||
<div class="text-xl">
|
||||
You can find your way back <a href="/" class="font-bold uppercase text-sky-400">here</a>
|
||||
</div>
|
||||
<div class="py-10 text-xs font-bold">
|
||||
<pre
|
||||
class="w-full whitespace-pre-wrap break-words text-left text-xs tracking-tighter">{error.message} {error.stack}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,408 +1,568 @@
|
||||
<script context="module" lang="ts">
|
||||
import { publicPages } from '$lib/consts';
|
||||
import { request } from '$lib/request';
|
||||
/**
|
||||
* @type {import('@sveltejs/kit').Load}
|
||||
*/
|
||||
export async function load(session) {
|
||||
const { path } = session.page;
|
||||
if (!publicPages.includes(path)) {
|
||||
if (!session.session.isLoggedIn) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/'
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
if (!publicPages.includes(path)) {
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
import { publicPaths } from '$lib/settings';
|
||||
|
||||
export const load: Load = async ({ fetch, url, params, session }) => {
|
||||
if (!session.uid && !publicPaths.includes(url.pathname)) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/'
|
||||
redirect: '/login'
|
||||
};
|
||||
}
|
||||
if (!session.uid) {
|
||||
return {};
|
||||
}
|
||||
const endpoint = `/teams.json`;
|
||||
const res = await fetch(endpoint);
|
||||
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
selectedTeamId: session.teamId,
|
||||
...(await res.json())
|
||||
}
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import 'microtip/microtip.css';
|
||||
import '../app.postcss';
|
||||
export let initDashboard;
|
||||
import { onMount } from 'svelte';
|
||||
import { SvelteToast } from '@zerodevx/svelte-toast';
|
||||
import { goto } from '$app/navigation';
|
||||
<script>
|
||||
export let teams;
|
||||
export let selectedTeamId;
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import '../tailwind.css';
|
||||
import { SvelteToast, toast } from '@zerodevx/svelte-toast';
|
||||
import { page, session } from '$app/stores';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import Tooltip from '$components/Tooltip.svelte';
|
||||
import compareVersions from 'compare-versions';
|
||||
import packageJson from '../../package.json';
|
||||
import { dashboard, settings } from '$store';
|
||||
import { browser } from '$app/env';
|
||||
$settings.clientId = import.meta.env.VITE_GITHUB_APP_CLIENTID !== 'null' ? import.meta.env.VITE_GITHUB_APP_CLIENTID : null
|
||||
$dashboard = initDashboard;
|
||||
const branch =
|
||||
process.env.NODE_ENV === 'production' &&
|
||||
browser &&
|
||||
window.location.hostname !== 'test.andrasbacsai.dev'
|
||||
? 'main'
|
||||
: 'next';
|
||||
let latest = {
|
||||
coolify: {}
|
||||
};
|
||||
let upgradeAvailable = false;
|
||||
let upgradeDisabled = false;
|
||||
let upgradeDone = false;
|
||||
let showAck = false;
|
||||
let globalFeatureFlag = browser && localStorage.getItem('globalFeatureFlag');
|
||||
const options = {
|
||||
duration: 2000
|
||||
import { onMount } from 'svelte';
|
||||
import { errorNotification } from '$lib/form';
|
||||
import { asyncSleep } from '$lib/components/common';
|
||||
import { del, get, post } from '$lib/api';
|
||||
import { dev } from '$app/env';
|
||||
import Loading from '$lib/components/Loading.svelte';
|
||||
|
||||
let isUpdateAvailable = false;
|
||||
let updateStatus = {
|
||||
loading: false,
|
||||
checking: false,
|
||||
success: null
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
upgradeAvailable = await checkUpgrade();
|
||||
browser && localStorage.removeItem('token');
|
||||
if (!localStorage.getItem('automaticErrorReportsAck')) {
|
||||
showAck = true;
|
||||
if (latest?.coolify[branch]?.settings?.sendErrors) {
|
||||
const settings = {
|
||||
sendErrors: true
|
||||
};
|
||||
await request('/api/v1/settings', $session, { body: { ...settings } });
|
||||
if ($session.uid) {
|
||||
try {
|
||||
await get(`/login.json`);
|
||||
} catch ({ error }) {
|
||||
await del(`/logout.json`, {});
|
||||
window.location.reload();
|
||||
return errorNotification(error);
|
||||
}
|
||||
if ($session.teamId === '0') {
|
||||
updateStatus.checking = true;
|
||||
try {
|
||||
const data = await get(`/update.json`);
|
||||
if (data?.isUpdateAvailable) {
|
||||
await post(`/update.json`, { type: 'pull' });
|
||||
}
|
||||
isUpdateAvailable = data?.isUpdateAvailable;
|
||||
} catch (error) {
|
||||
} finally {
|
||||
updateStatus.checking = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
async function checkUpgrade() {
|
||||
latest = await fetch(`https://get.coollabs.io/version.json`, {
|
||||
cache: 'no-cache'
|
||||
}).then((r) => r.json());
|
||||
|
||||
return compareVersions(latest.coolify[branch].version, packageJson.version) === 1
|
||||
? true
|
||||
: false;
|
||||
}
|
||||
async function upgrade() {
|
||||
async function logout() {
|
||||
try {
|
||||
upgradeDisabled = true;
|
||||
await request('/api/v1/upgrade', $session);
|
||||
upgradeDone = true;
|
||||
} catch (error) {
|
||||
browser &&
|
||||
toast.push(
|
||||
'Something happened during update. Ooops. Automatic error reporting will happen soon.'
|
||||
);
|
||||
await del(`/logout.json`, {});
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function logout() {
|
||||
await request('/api/v1/logout', $session, { body: {}, method: 'DELETE' });
|
||||
location.reload();
|
||||
async function switchTeam() {
|
||||
try {
|
||||
await post(`/index.json?from=${$page.url.pathname}`, {
|
||||
cookie: 'teamId',
|
||||
value: selectedTeamId
|
||||
});
|
||||
return window.location.reload();
|
||||
} catch (error) {
|
||||
return window.location.reload();
|
||||
}
|
||||
}
|
||||
function reloadInAMin() {
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 30000);
|
||||
}
|
||||
function ackError() {
|
||||
localStorage.setItem('automaticErrorReportsAck', 'true');
|
||||
showAck = false;
|
||||
|
||||
async function update() {
|
||||
updateStatus.loading = true;
|
||||
if (!dev) {
|
||||
try {
|
||||
await post(`/update.json`, { type: 'update' });
|
||||
toast.push('Update completed. Waiting for the new version to start...');
|
||||
let reachable = false;
|
||||
let tries = 0;
|
||||
do {
|
||||
await asyncSleep(4000);
|
||||
try {
|
||||
await get(`/undead.json`);
|
||||
reachable = true;
|
||||
} catch (error) {
|
||||
reachable = false;
|
||||
}
|
||||
if (reachable) break;
|
||||
tries++;
|
||||
} while (!reachable || tries < 120);
|
||||
toast.push('New version reachable. Reloading...');
|
||||
updateStatus.loading = false;
|
||||
updateStatus.success = true;
|
||||
await asyncSleep(3000);
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
updateStatus.success = false;
|
||||
updateStatus.loading = false;
|
||||
}
|
||||
} else {
|
||||
let reachable = false;
|
||||
let tries = 0;
|
||||
do {
|
||||
await asyncSleep(1000);
|
||||
try {
|
||||
await get(`/undead.json`);
|
||||
reachable = true;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
reachable = false;
|
||||
}
|
||||
console.log(reachable);
|
||||
if (reachable) break;
|
||||
tries++;
|
||||
} while (!reachable || tries < 120);
|
||||
toast.push('New version reachable. Reloading...');
|
||||
await asyncSleep(2000);
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<SvelteToast {options} />
|
||||
<svelte:head>
|
||||
<title>Coolify</title>
|
||||
</svelte:head>
|
||||
<SvelteToast options={{ intro: { y: -64 }, duration: 3000, pausable: true }} />
|
||||
{#if $session.uid}
|
||||
<nav class="nav-main">
|
||||
<div class="flex h-screen w-full flex-col items-center transition-all duration-100">
|
||||
<div class="my-4 h-10 w-10"><img src="/favicon.png" alt="coolLabs logo" /></div>
|
||||
<div class="flex flex-col space-y-4 py-2">
|
||||
<a
|
||||
sveltekit:prefetch
|
||||
href="/"
|
||||
class="icons tooltip-right bg-coolgray-200 hover:text-white"
|
||||
class:text-white={$page.url.pathname === '/'}
|
||||
class:bg-coolgray-500={$page.url.pathname === '/'}
|
||||
data-tooltip="Dashboard"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M19 8.71l-5.333 -4.148a2.666 2.666 0 0 0 -3.274 0l-5.334 4.148a2.665 2.665 0 0 0 -1.029 2.105v7.2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-7.2c0 -.823 -.38 -1.6 -1.03 -2.105"
|
||||
/>
|
||||
<path d="M16 15c-2.21 1.333 -5.792 1.333 -8 0" />
|
||||
</svg>
|
||||
</a>
|
||||
<div class="border-t border-stone-700" />
|
||||
|
||||
{#if showAck && $page.path !== '/success' && $page.path !== '/'}
|
||||
<div class="p-2 fixed top-0 right-0 z-50 w-64 m-2 rounded border-gradient-full bg-black">
|
||||
<div class="text-white text-xs space-y-2 text-justify font-medium">
|
||||
<div>We implemented an automatic error reporting feature, which is enabled by default.</div>
|
||||
<div>Why? Because we would like to hunt down bugs faster and easier.</div>
|
||||
<div class="py-5">
|
||||
If you do not like it, you can turn it off in the <button
|
||||
class="underline font-bold"
|
||||
on:click={() => goto('/settings')}>Settings menu</button
|
||||
>.
|
||||
<a
|
||||
sveltekit:prefetch
|
||||
href="/applications"
|
||||
class="icons tooltip-right bg-coolgray-200 hover:text-green-500"
|
||||
class:text-green-500={$page.url.pathname.startsWith('/applications') ||
|
||||
$page.url.pathname.startsWith('/new/application')}
|
||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/applications') ||
|
||||
$page.url.pathname.startsWith('/new/application')}
|
||||
data-tooltip="Applications"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentcolor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<rect x="4" y="4" width="6" height="6" rx="1" />
|
||||
<rect x="4" y="14" width="6" height="6" rx="1" />
|
||||
<rect x="14" y="14" width="6" height="6" rx="1" />
|
||||
<line x1="14" y1="7" x2="20" y2="7" />
|
||||
<line x1="17" y1="4" x2="17" y2="10" />
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
sveltekit:prefetch
|
||||
href="/sources"
|
||||
class="icons tooltip-right bg-coolgray-200 hover:text-orange-500"
|
||||
class:text-orange-500={$page.url.pathname.startsWith('/sources') ||
|
||||
$page.url.pathname.startsWith('/new/source')}
|
||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/sources') ||
|
||||
$page.url.pathname.startsWith('/new/source')}
|
||||
data-tooltip="Git Sources"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<circle cx="6" cy="6" r="2" />
|
||||
<circle cx="18" cy="18" r="2" />
|
||||
<path d="M11 6h5a2 2 0 0 1 2 2v8" />
|
||||
<polyline points="14 9 11 6 14 3" />
|
||||
<path d="M13 18h-5a2 2 0 0 1 -2 -2v-8" />
|
||||
<polyline points="10 15 13 18 10 21" />
|
||||
</svg>
|
||||
</a>
|
||||
<div class="border-t border-stone-700" />
|
||||
<a
|
||||
sveltekit:prefetch
|
||||
href="/destinations"
|
||||
class="icons tooltip-right bg-coolgray-200 hover:text-sky-500"
|
||||
class:text-sky-500={$page.url.pathname.startsWith('/destinations') ||
|
||||
$page.url.pathname.startsWith('/new/destination')}
|
||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/destinations') ||
|
||||
$page.url.pathname.startsWith('/new/destination')}
|
||||
data-tooltip="Destinations"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M22 12.54c-1.804 -.345 -2.701 -1.08 -3.523 -2.94c-.487 .696 -1.102 1.568 -.92 2.4c.028 .238 -.32 1.002 -.557 1h-14c0 5.208 3.164 7 6.196 7c4.124 .022 7.828 -1.376 9.854 -5c1.146 -.101 2.296 -1.505 2.95 -2.46z"
|
||||
/>
|
||||
<path d="M5 10h3v3h-3z" />
|
||||
<path d="M8 10h3v3h-3z" />
|
||||
<path d="M11 10h3v3h-3z" />
|
||||
<path d="M8 7h3v3h-3z" />
|
||||
<path d="M11 7h3v3h-3z" />
|
||||
<path d="M11 4h3v3h-3z" />
|
||||
<path d="M4.571 18c1.5 0 2.047 -.074 2.958 -.78" />
|
||||
<line x1="10" y1="16" x2="10" y2="16.01" />
|
||||
</svg>
|
||||
</a>
|
||||
<div class="border-t border-stone-700" />
|
||||
<a
|
||||
sveltekit:prefetch
|
||||
href="/databases"
|
||||
class="icons tooltip-right bg-coolgray-200 hover:text-purple-500"
|
||||
class:text-purple-500={$page.url.pathname.startsWith('/databases') ||
|
||||
$page.url.pathname.startsWith('/new/database')}
|
||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/databases') ||
|
||||
$page.url.pathname.startsWith('/new/database')}
|
||||
data-tooltip="Databases"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<ellipse cx="12" cy="6" rx="8" ry="3" />
|
||||
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
|
||||
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
|
||||
</svg>
|
||||
</a>
|
||||
<div class="border-t border-stone-700" />
|
||||
<a
|
||||
sveltekit:prefetch
|
||||
href="/services"
|
||||
class="icons tooltip-right bg-coolgray-200 hover:text-pink-500"
|
||||
class:text-pink-500={$page.url.pathname.startsWith('/services') ||
|
||||
$page.url.pathname.startsWith('/new/service')}
|
||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/services') ||
|
||||
$page.url.pathname.startsWith('/new/service')}
|
||||
data-tooltip="Services"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 18a4.6 4.4 0 0 1 0 -9a5 4.5 0 0 1 11 2h1a3.5 3.5 0 0 1 0 7h-12" />
|
||||
</svg>
|
||||
</a>
|
||||
<div class="border-t border-stone-700" />
|
||||
</div>
|
||||
<button
|
||||
class="button p-2 bg-warmGray-800 w-full text-center hover:bg-warmGray-700"
|
||||
on:click={ackError}>OK</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<main class:main={$page.path !== '/success' && $page.path !== '/'}>
|
||||
{#if $page.path !== '/' && $page.path !== '/success'}
|
||||
<nav class="w-16 bg-warmGray-800 text-white top-0 left-0 fixed min-w-4rem min-h-screen">
|
||||
<div
|
||||
class="flex flex-col w-full h-screen items-center transition-all duration-100"
|
||||
class:border-green-500={$page.path === '/dashboard/applications'}
|
||||
class:border-purple-500={$page.path === '/dashboard/databases'}
|
||||
>
|
||||
<div class="w-10 pt-4 pb-4"><img src="/favicon.png" alt="coolLabs logo" /></div>
|
||||
{#if $settings.clientId}
|
||||
<Tooltip position="right" label="Applications">
|
||||
<div
|
||||
class="p-2 hover:bg-warmGray-700 rounded hover:text-green-500 mt-4 transition-all duration-100 cursor-pointer"
|
||||
on:click={() => goto('/dashboard/applications')}
|
||||
class:text-green-500={$page.path === '/dashboard/applications' ||
|
||||
$page.path.startsWith('/application')}
|
||||
class:bg-warmGray-700={$page.path === '/dashboard/applications' ||
|
||||
$page.path.startsWith('/application')}
|
||||
>
|
||||
<svg
|
||||
class="w-8"
|
||||
<div class="flex-1" />
|
||||
|
||||
<div class="flex flex-col space-y-4 py-2">
|
||||
{#if $session.teamId === '0'}
|
||||
{#if updateStatus.checking}
|
||||
<button
|
||||
disabled
|
||||
in:fade={{ duration: 150 }}
|
||||
class="icons tooltip-right bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 text-white duration-75 hover:scale-105"
|
||||
data-tooltip="Checking for updates..."
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-9 w-8 animate-spin"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><rect x="4" y="4" width="16" height="16" rx="2" ry="2" /><rect
|
||||
x="9"
|
||||
y="9"
|
||||
width="6"
|
||||
height="6"
|
||||
/><line x1="9" y1="1" x2="9" y2="4" /><line x1="15" y1="1" x2="15" y2="4" /><line
|
||||
x1="9"
|
||||
y1="20"
|
||||
x2="9"
|
||||
y2="23"
|
||||
/><line x1="15" y1="20" x2="15" y2="23" /><line
|
||||
x1="20"
|
||||
y1="9"
|
||||
x2="23"
|
||||
y2="9"
|
||||
/><line x1="20" y1="14" x2="23" y2="14" /><line x1="1" y1="9" x2="4" y2="9" /><line
|
||||
x1="1"
|
||||
y1="14"
|
||||
x2="4"
|
||||
y2="14"
|
||||
/></svg
|
||||
>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip position="right" label="Databases">
|
||||
<div
|
||||
class="p-2 hover:bg-warmGray-700 rounded hover:text-purple-500 my-4 transition-all duration-100 cursor-pointer"
|
||||
on:click={() => goto('/dashboard/databases')}
|
||||
class:text-purple-500={$page.path === '/dashboard/databases' ||
|
||||
$page.path.startsWith('/database')}
|
||||
class:bg-warmGray-700={$page.path === '/dashboard/databases' ||
|
||||
$page.path.startsWith('/database')}
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
|
||||
<line x1="5.63" y1="7.16" x2="5.63" y2="7.17" />
|
||||
<line x1="4.06" y1="11" x2="4.06" y2="11.01" />
|
||||
<line x1="4.63" y1="15.1" x2="4.63" y2="15.11" />
|
||||
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
|
||||
<line x1="11" y1="19.94" x2="11" y2="19.95" />
|
||||
</svg></button
|
||||
>
|
||||
<svg
|
||||
class="w-8"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
{:else if isUpdateAvailable}
|
||||
<button
|
||||
disabled={updateStatus.success === false}
|
||||
data-tooltip="Update available"
|
||||
on:click={update}
|
||||
class="icons tooltip-right bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 text-white duration-75 hover:scale-105"
|
||||
>
|
||||
{#if updateStatus.loading}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-8 h-9 lds-heart"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<Tooltip
|
||||
position="right"
|
||||
label="Applications disabled, no GitHub Integration detected"
|
||||
size="large"
|
||||
>
|
||||
<div class="p-2 text-warmGray-700 mt-4 transition-all duration-100 cursor-pointer">
|
||||
<svg
|
||||
class="w-8"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><rect x="4" y="4" width="16" height="16" rx="2" ry="2" /><rect
|
||||
x="9"
|
||||
y="9"
|
||||
width="6"
|
||||
height="6"
|
||||
/><line x1="9" y1="1" x2="9" y2="4" /><line x1="15" y1="1" x2="15" y2="4" /><line
|
||||
x1="9"
|
||||
y1="20"
|
||||
x2="9"
|
||||
y2="23"
|
||||
/><line x1="15" y1="20" x2="15" y2="23" /><line
|
||||
x1="20"
|
||||
y1="9"
|
||||
x2="23"
|
||||
y2="9"
|
||||
/><line x1="20" y1="14" x2="23" y2="14" /><line x1="1" y1="9" x2="4" y2="9" /><line
|
||||
x1="1"
|
||||
y1="14"
|
||||
x2="4"
|
||||
y2="14"
|
||||
/></svg
|
||||
>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip position="right" label="Databases disabled, no GitHub Integration detected" size="large">
|
||||
<div
|
||||
class="p-2 text-warmGray-700 my-4 transition-all duration-100 cursor-pointer"
|
||||
>
|
||||
<svg
|
||||
class="w-8"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M19.5 13.572l-7.5 7.428l-7.5 -7.428m0 0a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572"
|
||||
/>
|
||||
</svg>
|
||||
{:else if updateStatus.success === null}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-8 h-9"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</Tooltip>
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<line x1="12" y1="8" x2="8" y2="12" />
|
||||
<line x1="12" y1="8" x2="12" y2="16" />
|
||||
<line x1="16" y1="12" x2="12" y2="8" />
|
||||
</svg>
|
||||
{:else if updateStatus.success}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" class="w-8 h-9"
|
||||
><path
|
||||
fill="#DD2E44"
|
||||
d="M11.626 7.488c-.112.112-.197.247-.268.395l-.008-.008L.134 33.141l.011.011c-.208.403.14 1.223.853 1.937.713.713 1.533 1.061 1.936.853l.01.01L28.21 24.735l-.008-.009c.147-.07.282-.155.395-.269 1.562-1.562-.971-6.627-5.656-11.313-4.687-4.686-9.752-7.218-11.315-5.656z"
|
||||
/><path
|
||||
fill="#EA596E"
|
||||
d="M13 12L.416 32.506l-.282.635.011.011c-.208.403.14 1.223.853 1.937.232.232.473.408.709.557L17 17l-4-5z"
|
||||
/><path
|
||||
fill="#A0041E"
|
||||
d="M23.012 13.066c4.67 4.672 7.263 9.652 5.789 11.124-1.473 1.474-6.453-1.118-11.126-5.788-4.671-4.672-7.263-9.654-5.79-11.127 1.474-1.473 6.454 1.119 11.127 5.791z"
|
||||
/><path
|
||||
fill="#AA8DD8"
|
||||
d="M18.59 13.609c-.199.161-.459.245-.734.215-.868-.094-1.598-.396-2.109-.873-.541-.505-.808-1.183-.735-1.862.128-1.192 1.324-2.286 3.363-2.066.793.085 1.147-.17 1.159-.292.014-.121-.277-.446-1.07-.532-.868-.094-1.598-.396-2.11-.873-.541-.505-.809-1.183-.735-1.862.13-1.192 1.325-2.286 3.362-2.065.578.062.883-.057 1.012-.134.103-.063.144-.123.148-.158.012-.121-.275-.446-1.07-.532-.549-.06-.947-.552-.886-1.102.059-.549.55-.946 1.101-.886 2.037.219 2.973 1.542 2.844 2.735-.13 1.194-1.325 2.286-3.364 2.067-.578-.063-.88.057-1.01.134-.103.062-.145.123-.149.157-.013.122.276.446 1.071.532 2.037.22 2.973 1.542 2.844 2.735-.129 1.192-1.324 2.286-3.362 2.065-.578-.062-.882.058-1.012.134-.104.064-.144.124-.148.158-.013.121.276.446 1.07.532.548.06.947.553.886 1.102-.028.274-.167.511-.366.671z"
|
||||
/><path
|
||||
fill="#77B255"
|
||||
d="M30.661 22.857c1.973-.557 3.334.323 3.658 1.478.324 1.154-.378 2.615-2.35 3.17-.77.216-1.001.584-.97.701.034.118.425.312 1.193.095 1.972-.555 3.333.325 3.657 1.479.326 1.155-.378 2.614-2.351 3.17-.769.216-1.001.585-.967.702.033.117.423.311 1.192.095.53-.149 1.084.16 1.233.691.148.532-.161 1.084-.693 1.234-1.971.555-3.333-.323-3.659-1.479-.324-1.154.379-2.613 2.353-3.169.77-.217 1.001-.584.967-.702-.032-.117-.422-.312-1.19-.096-1.974.556-3.334-.322-3.659-1.479-.325-1.154.378-2.613 2.351-3.17.768-.215.999-.585.967-.701-.034-.118-.423-.312-1.192-.096-.532.15-1.083-.16-1.233-.691-.149-.53.161-1.082.693-1.232z"
|
||||
/><path
|
||||
fill="#AA8DD8"
|
||||
d="M23.001 20.16c-.294 0-.584-.129-.782-.375-.345-.432-.274-1.061.156-1.406.218-.175 5.418-4.259 12.767-3.208.547.078.927.584.849 1.131-.078.546-.58.93-1.132.848-6.493-.922-11.187 2.754-11.233 2.791-.186.148-.406.219-.625.219z"
|
||||
/><path
|
||||
fill="#77B255"
|
||||
d="M5.754 16c-.095 0-.192-.014-.288-.042-.529-.159-.829-.716-.67-1.245 1.133-3.773 2.16-9.794.898-11.364-.141-.178-.354-.353-.842-.316-.938.072-.849 2.051-.848 2.071.042.551-.372 1.031-.922 1.072-.559.034-1.031-.372-1.072-.923-.103-1.379.326-4.035 2.692-4.214 1.056-.08 1.933.287 2.552 1.057 2.371 2.951-.036 11.506-.542 13.192-.13.433-.528.712-.958.712z"
|
||||
/><circle fill="#5C913B" cx="25.5" cy="9.5" r="1.5" /><circle
|
||||
fill="#9266CC"
|
||||
cx="2"
|
||||
cy="18"
|
||||
r="2"
|
||||
/><circle fill="#5C913B" cx="32.5" cy="19.5" r="1.5" /><circle
|
||||
fill="#5C913B"
|
||||
cx="23.5"
|
||||
cy="31.5"
|
||||
r="1.5"
|
||||
/><circle fill="#FFCC4D" cx="28" cy="4" r="2" /><circle
|
||||
fill="#FFCC4D"
|
||||
cx="32.5"
|
||||
cy="8.5"
|
||||
r="1.5"
|
||||
/><circle fill="#FFCC4D" cx="29.5" cy="12.5" r="1.5" /><circle
|
||||
fill="#FFCC4D"
|
||||
cx="7.5"
|
||||
cy="23.5"
|
||||
r="1.5"
|
||||
/></svg
|
||||
>
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" class="w-8 h-9"
|
||||
><path
|
||||
fill="#FFCC4D"
|
||||
d="M36 18c0 9.941-8.059 18-18 18S0 27.941 0 18 8.059 0 18 0s18 8.059 18 18"
|
||||
/><path
|
||||
fill="#664500"
|
||||
d="M22 27c0 2.763-1.791 3-4 3-2.21 0-4-.237-4-3 0-2.761 1.79-6 4-6 2.209 0 4 3.239 4 6zm8-12c-.124 0-.25-.023-.371-.072-5.229-2.091-7.372-5.241-7.461-5.374-.307-.46-.183-1.081.277-1.387.459-.306 1.077-.184 1.385.274.019.027 1.93 2.785 6.541 4.629.513.206.763.787.558 1.3-.157.392-.533.63-.929.63zM6 15c-.397 0-.772-.238-.929-.629-.205-.513.044-1.095.557-1.3 4.612-1.844 6.523-4.602 6.542-4.629.308-.456.929-.577 1.387-.27.457.308.581.925.275 1.383-.089.133-2.232 3.283-7.46 5.374C6.25 14.977 6.124 15 6 15z"
|
||||
/><path fill="#5DADEC" d="M24 16h4v19l-4-.046V16zM8 35l4-.046V16H8v19z" /><path
|
||||
fill="#664500"
|
||||
d="M14.999 18c-.15 0-.303-.034-.446-.105-3.512-1.756-7.07-.018-7.105 0-.495.249-1.095.046-1.342-.447-.247-.494-.047-1.095.447-1.342.182-.09 4.498-2.197 8.895 0 .494.247.694.848.447 1.342-.176.35-.529.552-.896.552zm14 0c-.15 0-.303-.034-.446-.105-3.513-1.756-7.07-.018-7.105 0-.494.248-1.094.047-1.342-.447-.247-.494-.047-1.095.447-1.342.182-.09 4.501-2.196 8.895 0 .494.247.694.848.447 1.342-.176.35-.529.552-.896.552z"
|
||||
/><ellipse fill="#5DADEC" cx="18" cy="34" rx="18" ry="2" /><ellipse
|
||||
fill="#E75A70"
|
||||
cx="18"
|
||||
cy="27"
|
||||
rx="3"
|
||||
ry="2"
|
||||
/></svg
|
||||
>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<Tooltip position="right" label="Services">
|
||||
<div
|
||||
class="p-2 hover:bg-warmGray-700 rounded hover:text-blue-500 transition-all duration-100 cursor-pointer"
|
||||
class:text-blue-500={$page.path === '/dashboard/services' ||
|
||||
$page.path.startsWith('/service')}
|
||||
class:bg-warmGray-700={$page.path === '/dashboard/services' ||
|
||||
$page.path.startsWith('/service')}
|
||||
on:click={() => goto('/dashboard/services')}
|
||||
<a
|
||||
sveltekit:prefetch
|
||||
href="/teams"
|
||||
class="icons tooltip-right bg-coolgray-200 hover:text-cyan-500"
|
||||
class:text-cyan-500={$page.url.pathname.startsWith('/teams')}
|
||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/teams')}
|
||||
data-tooltip="Teams"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<circle cx="9" cy="7" r="4" />
|
||||
<path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" />
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
|
||||
</svg>
|
||||
</a>
|
||||
{#if $session.teamId === '0'}
|
||||
<a
|
||||
sveltekit:prefetch
|
||||
href="/settings"
|
||||
class="icons tooltip-right bg-coolgray-200 hover:text-yellow-500"
|
||||
class:text-yellow-500={$page.url.pathname.startsWith('/settings')}
|
||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/settings')}
|
||||
data-tooltip="Settings"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-8"
|
||||
fill="none"
|
||||
class="h-8 w-8"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div class="flex-1" />
|
||||
{#if globalFeatureFlag}
|
||||
<Tooltip position="right" label="Servers">
|
||||
<div
|
||||
class="p-2 hover:bg-warmGray-700 rounded hover:text-red-500 mb-4 transition-all duration-100 cursor-pointer"
|
||||
on:click={() => goto('/servers')}
|
||||
class:text-red-500={$page.path === '/servers' || $page.path.startsWith('/servers')}
|
||||
class:bg-warmGray-700={$page.path === '/servers' || $page.path.startsWith('/servers')}
|
||||
>
|
||||
<svg
|
||||
class="w-8"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
<Tooltip position="right" label="Settings">
|
||||
<button
|
||||
class="p-2 hover:bg-warmGray-700 rounded hover:text-yellow-500 transition-all duration-100 cursor-pointer"
|
||||
class:text-yellow-500={$page.path === '/settings'}
|
||||
class:bg-warmGray-700={$page.path === '/settings'}
|
||||
on:click={() => goto('/settings')}
|
||||
>
|
||||
<svg
|
||||
class="w-8"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip position="right" label="Logout">
|
||||
<button
|
||||
class="p-2 hover:bg-warmGray-700 rounded hover:text-red-500 my-4 transition-all duration-100 cursor-pointer"
|
||||
on:click={logout}
|
||||
>
|
||||
<svg
|
||||
class="w-7"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /><polyline
|
||||
points="16 17 21 12 16 7"
|
||||
/><line x1="21" y1="12" x2="9" y2="12" /></svg
|
||||
>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<a
|
||||
href={`https://github.com/coollabsio/coolify/releases/tag/v${packageJson.version}`}
|
||||
target="_blank"
|
||||
class="cursor-pointer text-xs font-bold text-warmGray-400 py-2 hover:bg-warmGray-700 w-full text-center"
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"
|
||||
/>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
</a>
|
||||
{/if}
|
||||
<div
|
||||
class="icons tooltip-right bg-coolgray-200 hover:text-red-500"
|
||||
data-tooltip="Logout"
|
||||
on:click={logout}
|
||||
>
|
||||
{packageJson.version}
|
||||
</a>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="ml-1 h-7 w-7"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M14 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2"
|
||||
/>
|
||||
<path d="M7 12h14l-3 -3m0 6l3 -3" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{/if}
|
||||
<div
|
||||
class="w-full text-center font-bold text-stone-400 hover:bg-coolgray-200 hover:text-white"
|
||||
>
|
||||
<a
|
||||
class="text-[10px] no-underline"
|
||||
href={`https://github.com/coollabsio/coolify/releases/tag/v${$session.version}`}
|
||||
target="_blank">v{$session.version}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<select
|
||||
class="fixed right-0 bottom-0 z-50 m-2 p-2 px-4"
|
||||
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 />
|
||||
</main>
|
||||
{#if upgradeAvailable && $page.path !== '/success' && $page.path !== '/'}
|
||||
<footer
|
||||
class="fixed bottom-0 right-0 p-4 px-6 w-auto rounded-tl text-white hover:scale-110 transition duration-100"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div />
|
||||
<div class="flex-1" />
|
||||
{#if !upgradeDisabled}
|
||||
<button
|
||||
class="bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 text-xs font-bold rounded px-2 py-2"
|
||||
disabled={upgradeDisabled}
|
||||
on:click={upgrade}>New version available, <br />click here to upgrade!</button
|
||||
>
|
||||
{:else if upgradeDone}
|
||||
<button
|
||||
use:reloadInAMin
|
||||
class="font-bold text-xs rounded px-2 cursor-not-allowed"
|
||||
disabled={upgradeDisabled}>Upgrade done. 🎉 Automatically reloading in 30s.</button
|
||||
>
|
||||
{:else}
|
||||
<button
|
||||
class="opacity-50 tracking-tight font-bold text-xs rounded px-2 cursor-not-allowed"
|
||||
disabled={upgradeDisabled}>Upgrading. It could take a while, please wait...</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</footer>
|
||||
{/if}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { setDefaultConfiguration } from '$lib/api/applications/configuration';
|
||||
import { saveServerLog } from '$lib/api/applications/logging';
|
||||
import Configuration from '$models/Configuration';
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
|
||||
export async function post(request: Request) {
|
||||
try {
|
||||
const { DOMAIN } = process.env;
|
||||
const configuration = setDefaultConfiguration(request.body);
|
||||
const sameDomainAndPath = await Configuration.find({
|
||||
'publish.path': configuration.publish.path,
|
||||
'publish.domain': configuration.publish.domain
|
||||
}).select('-_id -__v -createdAt -updatedAt');
|
||||
if (sameDomainAndPath.length > 1 || configuration.publish.domain === DOMAIN) {
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
success: false,
|
||||
message: 'Domain/path are already in use.'
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: 200,
|
||||
body: { success: true, message: 'OK' }
|
||||
};
|
||||
} catch (error) {
|
||||
await saveServerLog(error);
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: error.message || error
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import { docker } from '$lib/api/docker';
|
||||
import Configuration from '$models/Configuration';
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
|
||||
export async function post(request: Request) {
|
||||
const { nickname }: any = request.body || {};
|
||||
if (nickname) {
|
||||
const configurationFound = await Configuration.find({
|
||||
'general.nickname': nickname
|
||||
}).select('-_id -__v -createdAt -updatedAt');
|
||||
if (configurationFound) {
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
configuration: [...configurationFound]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const services = await docker.engine.listServices();
|
||||
const applications = services.filter(
|
||||
(r) => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application'
|
||||
);
|
||||
const found = applications.find((r) => {
|
||||
const configuration = r.Spec.Labels.configuration
|
||||
? JSON.parse(r.Spec.Labels.configuration)
|
||||
: null;
|
||||
|
||||
if (configuration.general.nickname === nickname) return r;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (found) {
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
success: true,
|
||||
...JSON.parse(found.Spec.Labels.configuration)
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'No configuration found.'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import { updateServiceLabels } from '$lib/api/applications/configuration';
|
||||
import { execShellAsync } from '$lib/api/common';
|
||||
import { docker } from '$lib/api/docker';
|
||||
import ApplicationLog from '$models/ApplicationLog';
|
||||
import Configuration from '$models/Configuration';
|
||||
import Deployment from '$models/Deployment';
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
|
||||
export async function post(request: Request) {
|
||||
const { name, organization, branch, isPreviewDeploymentEnabled }: any = request.body || {};
|
||||
if (name && organization && branch) {
|
||||
const configuration = await Configuration.findOneAndUpdate(
|
||||
{
|
||||
'repository.name': name,
|
||||
'repository.organization': organization,
|
||||
'repository.branch': branch
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
'general.isPreviewDeploymentEnabled': isPreviewDeploymentEnabled,
|
||||
'general.pullRequest': 0
|
||||
}
|
||||
},
|
||||
{ new: true }
|
||||
).select('-_id -__v -createdAt -updatedAt');
|
||||
if (!isPreviewDeploymentEnabled) {
|
||||
const found = await Configuration.find({
|
||||
'repository.name': name,
|
||||
'repository.organization': organization,
|
||||
'repository.branch': branch,
|
||||
'general.pullRequest': { $ne: 0 }
|
||||
});
|
||||
for (const prDeployment of found) {
|
||||
await Configuration.findOneAndRemove({
|
||||
'repository.name': name,
|
||||
'repository.organization': organization,
|
||||
'repository.branch': branch,
|
||||
'publish.domain': prDeployment.publish.domain
|
||||
});
|
||||
const deploys = await Deployment.find({
|
||||
organization,
|
||||
branch,
|
||||
name,
|
||||
domain: prDeployment.publish.domain
|
||||
});
|
||||
for (const deploy of deploys) {
|
||||
await ApplicationLog.deleteMany({ deployId: deploy.deployId });
|
||||
await Deployment.deleteMany({ deployId: deploy.deployId });
|
||||
}
|
||||
await execShellAsync(`docker stack rm ${prDeployment.build.container.name}`);
|
||||
}
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
organization,
|
||||
name,
|
||||
branch
|
||||
}
|
||||
};
|
||||
}
|
||||
updateServiceLabels(configuration);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
success: true
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'Cannot save.'
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
import Deployment from '$models/Deployment';
|
||||
import { precheckDeployment, setDefaultConfiguration } from '$lib/api/applications/configuration';
|
||||
import cloneRepository from '$lib/api/applications/cloneRepository';
|
||||
import { cleanupTmp } from '$lib/api/common';
|
||||
import queueAndBuild from '$lib/api/applications/queueAndBuild';
|
||||
import Configuration from '$models/Configuration';
|
||||
import preChecks from '$lib/api/applications/preChecks';
|
||||
import preTasks from '$lib/api/applications/preTasks';
|
||||
|
||||
export async function post(request: Request) {
|
||||
const configuration = setDefaultConfiguration(request.body);
|
||||
if (!configuration) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'Whaaat?'
|
||||
}
|
||||
};
|
||||
}
|
||||
try {
|
||||
await cloneRepository(configuration);
|
||||
const nextStep = await preChecks(configuration);
|
||||
if (nextStep === 0) {
|
||||
cleanupTmp(configuration.general.workdir);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
success: false,
|
||||
message: 'Nothing changed, no need to redeploy.'
|
||||
}
|
||||
};
|
||||
}
|
||||
await preTasks(configuration)
|
||||
|
||||
queueAndBuild(configuration, nextStep);
|
||||
return {
|
||||
status: 201,
|
||||
body: {
|
||||
message: 'Deployment queued.',
|
||||
nickname: configuration.general.nickname,
|
||||
name: configuration.build.container.name,
|
||||
deployId: configuration.general.deployId
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await Deployment.findOneAndUpdate({ nickname: configuration.general.nickname }, { $set: { progress: 'failed' } });
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: error.message || error
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
import ApplicationLog from '$models/ApplicationLog';
|
||||
import Deployment from '$models/Deployment';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export async function get(request: Request) {
|
||||
const { deployId } = request.params;
|
||||
try {
|
||||
const logs: any = await ApplicationLog.find({ deployId })
|
||||
.select('-_id -__v')
|
||||
.sort({ createdAt: 'asc' });
|
||||
|
||||
const deploy: any = await Deployment.findOne({ deployId }).select('-_id -__v');
|
||||
const finalLogs: any = {};
|
||||
finalLogs.progress = deploy.progress;
|
||||
finalLogs.events = logs.map((log) => log.event);
|
||||
finalLogs.human = dayjs(deploy.updatedAt).from(dayjs(deploy.updatedAt));
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
...finalLogs
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: error.message || error
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc.js';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime.js';
|
||||
import Deployment from '$models/Deployment';
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(relativeTime);
|
||||
export async function get(request: Request) {
|
||||
try {
|
||||
const repoId = request.query.get('repoId');
|
||||
const branch = request.query.get('branch');
|
||||
const page = request.query.get('page');
|
||||
const onePage = 5;
|
||||
const show = Number(page) * onePage || 5;
|
||||
const deploy: any = await Deployment.find({ repoId, branch })
|
||||
.select('-_id -__v -repoId')
|
||||
.sort({ createdAt: 'desc' })
|
||||
.limit(show);
|
||||
const finalLogs = deploy.map((d) => {
|
||||
const finalLogs = { ...d._doc };
|
||||
const updatedAt = dayjs(d.updatedAt).utc();
|
||||
finalLogs.took = updatedAt.diff(dayjs(d.createdAt)) / 1000;
|
||||
finalLogs.since = updatedAt.fromNow();
|
||||
finalLogs.isPr = d.domain.startsWith('pr');
|
||||
return finalLogs;
|
||||
});
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
success: true,
|
||||
logs: finalLogs
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: error.message || error
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { saveServerLog } from '$lib/api/applications/logging';
|
||||
import { docker } from '$lib/api/docker';
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
|
||||
export async function get(request: Request) {
|
||||
try {
|
||||
const name = request.query.get('name');
|
||||
const service = await docker.engine.getService(`${name}_${name}`);
|
||||
const logs = (await service.logs({ stdout: true, stderr: true, timestamps: true }))
|
||||
.toString()
|
||||
.split('\n')
|
||||
.map((l) => l.slice(8))
|
||||
.filter((a) => a);
|
||||
return {
|
||||
status: 200,
|
||||
body: { success: true, logs }
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await saveServerLog(error);
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'No such service. Is it under deployment?'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import { purgeImagesContainers } from '$lib/api/applications/cleanup';
|
||||
import Deployment from '$models/Deployment';
|
||||
import ApplicationLog from '$models/ApplicationLog';
|
||||
import { delay, execShellAsync } from '$lib/api/common';
|
||||
import Configuration from '$models/Configuration';
|
||||
|
||||
export async function post(request: Request) {
|
||||
const { nickname } = request.body;
|
||||
try {
|
||||
const configurationFound = await Configuration.findOne({
|
||||
'general.nickname': nickname
|
||||
});
|
||||
if (configurationFound) {
|
||||
const id = configurationFound._id;
|
||||
if (configurationFound?.general?.pullRequest === 0) {
|
||||
// Main deployment deletion request; deleting main + PRs
|
||||
const allConfiguration = await Configuration.find({
|
||||
'publish.domain': { $regex: `.*${configurationFound.publish.domain}`, $options: 'i' },
|
||||
'publish.path': configurationFound.publish.path
|
||||
});
|
||||
for (const config of allConfiguration) {
|
||||
await execShellAsync(`docker stack rm ${config.build.container.name}`);
|
||||
}
|
||||
await Configuration.deleteMany({
|
||||
'publish.domain': { $regex: `.*${configurationFound.publish.domain}`, $options: 'i' },
|
||||
'publish.path': configurationFound.publish.path
|
||||
});
|
||||
const deploys = await Deployment.find({ nickname });
|
||||
for (const deploy of deploys) {
|
||||
await ApplicationLog.deleteMany({ deployId: deploy.deployId });
|
||||
await Deployment.deleteMany({ deployId: deploy.deployId });
|
||||
}
|
||||
} else {
|
||||
// Delete only PRs
|
||||
await Configuration.findByIdAndRemove(id);
|
||||
await execShellAsync(`docker stack rm ${configurationFound.build.container.name}`);
|
||||
const deploys = await Deployment.find({ nickname });
|
||||
for (const deploy of deploys) {
|
||||
await ApplicationLog.deleteMany({ deployId: deploy.deployId });
|
||||
await Deployment.deleteMany({ deployId: deploy.deployId });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {}
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {
|
||||
status: 500,
|
||||
error: {
|
||||
message: 'Nothing to do.'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { docker } from '$lib/api/docker';
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
import Configuration from '$models/Configuration';
|
||||
export async function get(request: Request) {
|
||||
// Should update this to get data from mongodb and update db with the currently running services on start!
|
||||
const dockerServices = await docker.engine.listServices();
|
||||
let databases: any = dockerServices.filter(
|
||||
(r) =>
|
||||
r.Spec.Labels.managedBy === 'coolify' &&
|
||||
r.Spec.Labels.type === 'database' &&
|
||||
r.Spec.Labels.configuration
|
||||
);
|
||||
let services: any = dockerServices.filter(
|
||||
(r) =>
|
||||
r.Spec.Labels.managedBy === 'coolify' &&
|
||||
r.Spec.Labels.type === 'service' &&
|
||||
r.Spec.Labels.configuration
|
||||
);
|
||||
databases = databases.map((r) => {
|
||||
if (JSON.parse(r.Spec.Labels.configuration)) {
|
||||
return {
|
||||
configuration: JSON.parse(r.Spec.Labels.configuration)
|
||||
};
|
||||
}
|
||||
return {};
|
||||
});
|
||||
services = services.map((r) => {
|
||||
if (JSON.parse(r.Spec.Labels.configuration)) {
|
||||
return {
|
||||
serviceName: r.Spec.Labels.serviceName,
|
||||
configuration: JSON.parse(r.Spec.Labels.configuration)
|
||||
};
|
||||
}
|
||||
return {};
|
||||
});
|
||||
const configurations = await Configuration.find({
|
||||
'general.pullRequest': { $in: [null, 0] }
|
||||
}).select('-_id -__v -createdAt');
|
||||
const applications = [];
|
||||
for (const configuration of configurations) {
|
||||
const foundPRDeployments = await Configuration.find({
|
||||
'repository.id': configuration.repository.id,
|
||||
'repository.branch': configuration.repository.branch,
|
||||
'general.pullRequest': { $ne: 0 }
|
||||
}).select('-_id -__v -createdAt');
|
||||
const payload = {
|
||||
configuration,
|
||||
UpdatedAt: configuration.updatedAt,
|
||||
prBuilds: foundPRDeployments.length > 0 ? true : false
|
||||
};
|
||||
applications.push(payload);
|
||||
}
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
success: true,
|
||||
applications: {
|
||||
deployed: applications
|
||||
},
|
||||
databases: {
|
||||
deployed: databases
|
||||
},
|
||||
services: {
|
||||
deployed: services
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
import { saveServerLog } from '$lib/api/applications/logging';
|
||||
import { execShellAsync } from '$lib/api/common';
|
||||
import { docker } from '$lib/api/docker';
|
||||
import fs from 'fs';
|
||||
|
||||
export async function post(request: Request) {
|
||||
const tmpdir = '/tmp/backups';
|
||||
const { deployId } = request.params;
|
||||
try {
|
||||
const now = new Date();
|
||||
const configuration = JSON.parse(
|
||||
JSON.parse(await execShellAsync(`docker inspect ${deployId}_${deployId}`))[0].Spec.Labels
|
||||
.configuration
|
||||
);
|
||||
const type = configuration.general.type;
|
||||
const serviceId = configuration.general.deployId;
|
||||
const databaseService = (await docker.engine.listContainers()).find(
|
||||
(r) => r.Labels['com.docker.stack.namespace'] === serviceId && r.State === 'running'
|
||||
);
|
||||
const containerID = databaseService.Labels['com.docker.swarm.task.name'];
|
||||
await execShellAsync(`mkdir -p ${tmpdir}`);
|
||||
if (type === 'mongodb') {
|
||||
if (databaseService) {
|
||||
const username = configuration.database.usernames[0];
|
||||
const password = configuration.database.passwords[1];
|
||||
const databaseName = configuration.database.defaultDatabaseName;
|
||||
const filename = `${databaseName}_${now.getTime()}.gz`;
|
||||
const fullfilename = `${tmpdir}/${filename}`;
|
||||
await execShellAsync(
|
||||
`docker exec -i ${containerID} /bin/bash -c "mkdir -p ${tmpdir};mongodump --uri='mongodb://${username}:${password}@${deployId}:27017' -d ${databaseName} --gzip --archive=${fullfilename}"`
|
||||
);
|
||||
await execShellAsync(`docker cp ${containerID}:${fullfilename} ${fullfilename}`);
|
||||
await execShellAsync(`docker exec -i ${containerID} /bin/bash -c "rm -f ${fullfilename}"`);
|
||||
return {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Transfer-Encoding': 'binary',
|
||||
'Content-Disposition': `attachment; filename=${filename}`
|
||||
},
|
||||
body: fs.readFileSync(`${fullfilename}`)
|
||||
};
|
||||
}
|
||||
} else if (type === 'postgresql') {
|
||||
if (databaseService) {
|
||||
const username = configuration.database.usernames[0];
|
||||
const password = configuration.database.passwords[0];
|
||||
const databaseName = configuration.database.defaultDatabaseName;
|
||||
const filename = `${databaseName}_${now.getTime()}.sql.gz`;
|
||||
const fullfilename = `${tmpdir}/${filename}`;
|
||||
await execShellAsync(
|
||||
`docker exec -i ${containerID} /bin/bash -c "PGPASSWORD=${password} pg_dump --username ${username} -Z 9 ${databaseName}" > ${fullfilename}`
|
||||
);
|
||||
return {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Transfer-Encoding': 'binary',
|
||||
'Content-Disposition': `attachment; filename=${filename}`
|
||||
},
|
||||
body: fs.readFileSync(`${fullfilename}`)
|
||||
};
|
||||
}
|
||||
} else if (type === 'couchdb') {
|
||||
if (databaseService) {
|
||||
const databaseName = configuration.database.defaultDatabaseName;
|
||||
const filename = `${databaseName}_${now.getTime()}.tar.gz`;
|
||||
const fullfilename = `${tmpdir}/${filename}`;
|
||||
await execShellAsync(
|
||||
`docker exec -i ${containerID} /bin/bash -c "cd /bitnami/couchdb/data/ && tar -czvf - ." > ${fullfilename}`
|
||||
);
|
||||
return {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Transfer-Encoding': 'binary',
|
||||
'Content-Disposition': `attachment; filename=${filename}`
|
||||
},
|
||||
body: fs.readFileSync(`${fullfilename}`)
|
||||
};
|
||||
}
|
||||
} else if (type === 'mysql') {
|
||||
if (databaseService) {
|
||||
const username = configuration.database.usernames[0];
|
||||
const password = configuration.database.passwords[0];
|
||||
const databaseName = configuration.database.defaultDatabaseName;
|
||||
const filename = `${databaseName}_${now.getTime()}.sql.gz`;
|
||||
const fullfilename = `${tmpdir}/${filename}`;
|
||||
await execShellAsync(
|
||||
`docker exec -i ${containerID} /bin/bash -c "mysqldump -u ${username} -p${password} ${databaseName} | gzip -9 -" > ${fullfilename}`
|
||||
);
|
||||
return {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Transfer-Encoding': 'binary',
|
||||
'Content-Disposition': `attachment; filename=${filename}`
|
||||
},
|
||||
body: fs.readFileSync(`${fullfilename}`)
|
||||
};
|
||||
}
|
||||
} else if (type === 'redis') {
|
||||
if (databaseService) {
|
||||
const password = configuration.database.passwords[0];
|
||||
const databaseName = configuration.database.defaultDatabaseName;
|
||||
const filename = `${databaseName}_${now.getTime()}.rdb`;
|
||||
const fullfilename = `${tmpdir}/${filename}`;
|
||||
await execShellAsync(
|
||||
`docker exec -i ${containerID} /bin/bash -c "redis-cli --pass ${password} save"`
|
||||
);
|
||||
await execShellAsync(
|
||||
`docker cp ${containerID}:/bitnami/redis/data/dump.rdb ${fullfilename}`
|
||||
);
|
||||
await execShellAsync(
|
||||
`docker exec -i ${containerID} /bin/bash -c "rm -f /bitnami/redis/data/dump.rdb"`
|
||||
);
|
||||
return {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Transfer-Encoding': 'binary',
|
||||
'Content-Disposition': `attachment; filename=${filename}`
|
||||
},
|
||||
body: fs.readFileSync(`${fullfilename}`)
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: 501,
|
||||
body: {
|
||||
error: `Backup method not implemented yet for ${type}.`
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
await saveServerLog(error);
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: error.message || error
|
||||
}
|
||||
};
|
||||
} finally {
|
||||
await execShellAsync(`rm -fr ${tmpdir}`);
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { execShellAsync } from '$lib/api/common';
|
||||
import { docker } from '$lib/api/docker';
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
|
||||
export async function del(request: Request) {
|
||||
const { deployId } = request.params;
|
||||
await execShellAsync(`docker stack rm ${deployId}`);
|
||||
return {
|
||||
status: 200,
|
||||
body: {}
|
||||
};
|
||||
}
|
||||
export async function get(request: Request) {
|
||||
const { deployId } = request.params;
|
||||
|
||||
try {
|
||||
const database = (await docker.engine.listServices()).find(
|
||||
(r) =>
|
||||
r.Spec.Labels.managedBy === 'coolify' &&
|
||||
r.Spec.Labels.type === 'database' &&
|
||||
JSON.parse(r.Spec.Labels.configuration).general.deployId === deployId
|
||||
);
|
||||
|
||||
if (database) {
|
||||
const jsonEnvs = {};
|
||||
if (database.Spec.TaskTemplate.ContainerSpec.Env) {
|
||||
for (const d of database.Spec.TaskTemplate.ContainerSpec.Env) {
|
||||
const s = d.split('=');
|
||||
jsonEnvs[s[0]] = s[1];
|
||||
}
|
||||
}
|
||||
const payload = {
|
||||
config: JSON.parse(database.Spec.Labels.configuration),
|
||||
envs: jsonEnvs || null
|
||||
};
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
...payload
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'No database found.'
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'No database found.'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
import { saveServerLog } from '$lib/api/applications/logging';
|
||||
import { docker } from '$lib/api/docker';
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
import yaml from 'js-yaml';
|
||||
import { promises as fs } from 'fs';
|
||||
import cuid from 'cuid';
|
||||
import generator from 'generate-password';
|
||||
import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator';
|
||||
import { execShellAsync } from '$lib/api/common';
|
||||
|
||||
function getUniq() {
|
||||
return uniqueNamesGenerator({ dictionaries: [adjectives, animals, colors], length: 2 });
|
||||
}
|
||||
|
||||
export async function post(request: Request) {
|
||||
try {
|
||||
const { type } = request.body;
|
||||
let { defaultDatabaseName } = request.body;
|
||||
const passwords = generator.generateMultiple(2, {
|
||||
length: 24,
|
||||
numbers: true,
|
||||
strict: true
|
||||
});
|
||||
const usernames = generator.generateMultiple(2, {
|
||||
length: 10,
|
||||
numbers: true,
|
||||
strict: true
|
||||
});
|
||||
// TODO: Query for existing db with the same name
|
||||
const nickname = getUniq();
|
||||
|
||||
if (!defaultDatabaseName) defaultDatabaseName = nickname;
|
||||
|
||||
const deployId = cuid();
|
||||
const configuration = {
|
||||
general: {
|
||||
workdir: `/tmp/${deployId}`,
|
||||
deployId,
|
||||
nickname,
|
||||
type
|
||||
},
|
||||
database: {
|
||||
usernames,
|
||||
passwords,
|
||||
defaultDatabaseName
|
||||
},
|
||||
deploy: {
|
||||
name: nickname
|
||||
}
|
||||
};
|
||||
await execShellAsync(`mkdir -p ${configuration.general.workdir}`);
|
||||
let generateEnvs = {};
|
||||
let image = null;
|
||||
let volume = null;
|
||||
let ulimits = {};
|
||||
if (type === 'mongodb') {
|
||||
generateEnvs = {
|
||||
MONGODB_ROOT_PASSWORD: passwords[0],
|
||||
MONGODB_USERNAME: usernames[0],
|
||||
MONGODB_PASSWORD: passwords[1],
|
||||
MONGODB_DATABASE: defaultDatabaseName
|
||||
};
|
||||
image = 'bitnami/mongodb:4.4';
|
||||
volume = `${configuration.general.deployId}-${type}-data:/bitnami/mongodb`;
|
||||
} else if (type === 'postgresql') {
|
||||
generateEnvs = {
|
||||
POSTGRESQL_PASSWORD: passwords[0],
|
||||
POSTGRESQL_USERNAME: usernames[0],
|
||||
POSTGRESQL_DATABASE: defaultDatabaseName
|
||||
};
|
||||
image = 'bitnami/postgresql:13.2.0';
|
||||
volume = `${configuration.general.deployId}-${type}-data:/bitnami/postgresql`;
|
||||
} else if (type === 'couchdb') {
|
||||
generateEnvs = {
|
||||
COUCHDB_PASSWORD: passwords[0],
|
||||
COUCHDB_USER: usernames[0]
|
||||
};
|
||||
image = 'bitnami/couchdb:3';
|
||||
volume = `${configuration.general.deployId}-${type}-data:/bitnami/couchdb`;
|
||||
} else if (type === 'mysql') {
|
||||
generateEnvs = {
|
||||
MYSQL_ROOT_PASSWORD: passwords[0],
|
||||
MYSQL_ROOT_USER: usernames[0],
|
||||
MYSQL_USER: usernames[1],
|
||||
MYSQL_PASSWORD: passwords[1],
|
||||
MYSQL_DATABASE: defaultDatabaseName
|
||||
};
|
||||
image = 'bitnami/mysql:8.0';
|
||||
volume = `${configuration.general.deployId}-${type}-data:/bitnami/mysql/data`;
|
||||
} else if (type === 'clickhouse') {
|
||||
image = 'yandex/clickhouse-server';
|
||||
volume = `${configuration.general.deployId}-${type}-data:/var/lib/clickhouse`;
|
||||
ulimits = {
|
||||
nofile: {
|
||||
soft: 262144,
|
||||
hard: 262144
|
||||
}
|
||||
};
|
||||
} else if (type === 'redis') {
|
||||
image = 'bitnami/redis';
|
||||
volume = `${configuration.general.deployId}-${type}-data:/bitnami/redis/data`;
|
||||
generateEnvs = {
|
||||
REDIS_PASSWORD: passwords[0]
|
||||
};
|
||||
}
|
||||
|
||||
const stack = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[configuration.general.deployId]: {
|
||||
image,
|
||||
networks: [`${docker.network}`],
|
||||
environment: generateEnvs,
|
||||
volumes: [volume],
|
||||
ulimits,
|
||||
deploy: {
|
||||
replicas: 1,
|
||||
update_config: {
|
||||
parallelism: 0,
|
||||
delay: '10s',
|
||||
order: 'start-first'
|
||||
},
|
||||
rollback_config: {
|
||||
parallelism: 0,
|
||||
delay: '10s',
|
||||
order: 'start-first'
|
||||
},
|
||||
labels: [
|
||||
'managedBy=coolify',
|
||||
'type=database',
|
||||
'configuration=' + JSON.stringify(configuration)
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[`${docker.network}`]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
volumes: {
|
||||
[`${configuration.general.deployId}-${type}-data`]: {
|
||||
external: true
|
||||
}
|
||||
}
|
||||
};
|
||||
await fs.writeFile(`${configuration.general.workdir}/stack.yml`, yaml.dump(stack));
|
||||
await execShellAsync(
|
||||
`cat ${configuration.general.workdir}/stack.yml | docker stack deploy -c - ${configuration.general.deployId}`
|
||||
);
|
||||
return {
|
||||
status: 201,
|
||||
body: {
|
||||
message: 'Deployed.'
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await saveServerLog(error);
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
import mongoose from 'mongoose';
|
||||
import Settings from '$models/Settings';
|
||||
import User from '$models/User';
|
||||
import bcrypt from 'bcrypt';
|
||||
import cuid from 'cuid';
|
||||
import jsonwebtoken from 'jsonwebtoken';
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
|
||||
const saltRounds = 15;
|
||||
|
||||
export async function post(request: Request) {
|
||||
const { email, password } = request.body;
|
||||
const { JWT_SIGN_KEY } = process.env;
|
||||
const settings = await Settings.findOne({ applicationName: 'coolify' });
|
||||
const registeredUsers = await User.find().countDocuments();
|
||||
const foundUser = await User.findOne({ email });
|
||||
try {
|
||||
let uid = cuid();
|
||||
if (foundUser) {
|
||||
if (foundUser.type === 'github') {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'Wrong password or email address.'
|
||||
}
|
||||
};
|
||||
}
|
||||
uid = foundUser.uid;
|
||||
if (!(await bcrypt.compare(password, foundUser.password))) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'Wrong password or email address.'
|
||||
}
|
||||
};
|
||||
}
|
||||
} else {
|
||||
if (registeredUsers === 0) {
|
||||
const newUser = new User({
|
||||
_id: new mongoose.Types.ObjectId(),
|
||||
email,
|
||||
uid,
|
||||
type: 'email',
|
||||
password: await bcrypt.hash(password, saltRounds)
|
||||
});
|
||||
const defaultSettings = new Settings({
|
||||
_id: new mongoose.Types.ObjectId()
|
||||
});
|
||||
try {
|
||||
await newUser.save();
|
||||
await defaultSettings.save();
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: error.message || error
|
||||
};
|
||||
}
|
||||
} else {
|
||||
if (!settings?.allowRegistration) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'Registration disabled, enable it in settings.'
|
||||
}
|
||||
};
|
||||
} else {
|
||||
const newUser = new User({
|
||||
_id: new mongoose.Types.ObjectId(),
|
||||
email,
|
||||
uid,
|
||||
type: 'email',
|
||||
password: await bcrypt.hash(password, saltRounds)
|
||||
});
|
||||
try {
|
||||
await newUser.save();
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: error.message || error
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const coolToken = jsonwebtoken.sign({}, JWT_SIGN_KEY, {
|
||||
expiresIn: 15778800,
|
||||
algorithm: 'HS256',
|
||||
audience: 'coolLabs',
|
||||
issuer: 'coolLabs',
|
||||
jwtid: uid,
|
||||
subject: `User:${uid}`,
|
||||
notBefore: -1000
|
||||
});
|
||||
request.locals.session.data = { coolToken, ghToken: null };
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
message: 'Successfully logged in.'
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return { status: 500, body: { error: error.message || error } };
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
import mongoose from 'mongoose';
|
||||
import User from '$models/User';
|
||||
import Settings from '$models/Settings';
|
||||
import cuid from 'cuid';
|
||||
import jsonwebtoken from 'jsonwebtoken';
|
||||
import { githubAPI } from '$lib/api/github';
|
||||
|
||||
export async function get(request: Request) {
|
||||
const code = request.query.get('code');
|
||||
const { GITHUB_APP_CLIENT_SECRET, JWT_SIGN_KEY, VITE_GITHUB_APP_CLIENTID } = process.env;
|
||||
try {
|
||||
let uid = cuid();
|
||||
const { access_token } = await (
|
||||
await fetch(
|
||||
`https://github.com/login/oauth/access_token?client_id=${VITE_GITHUB_APP_CLIENTID}&client_secret=${GITHUB_APP_CLIENT_SECRET}&code=${code}`,
|
||||
{ headers: { accept: 'application/json' } }
|
||||
)
|
||||
).json();
|
||||
const { avatar_url } = await (await githubAPI(request, '/user', access_token)).body;
|
||||
const email = (await githubAPI(request, '/user/emails', access_token)).body.filter(
|
||||
(e) => e.primary
|
||||
)[0].email;
|
||||
const settings = await Settings.findOne({ applicationName: 'coolify' });
|
||||
const registeredUsers = await User.find().countDocuments();
|
||||
const foundUser = await User.findOne({ email });
|
||||
if (foundUser) {
|
||||
await User.findOneAndUpdate({ email }, { avatar: avatar_url }, { upsert: true, new: true });
|
||||
uid = foundUser.uid;
|
||||
} else {
|
||||
if (registeredUsers === 0) {
|
||||
const newUser = new User({
|
||||
_id: new mongoose.Types.ObjectId(),
|
||||
email,
|
||||
avatar: avatar_url,
|
||||
uid,
|
||||
type: 'github'
|
||||
});
|
||||
const defaultSettings = new Settings({
|
||||
_id: new mongoose.Types.ObjectId()
|
||||
});
|
||||
try {
|
||||
await newUser.save();
|
||||
await defaultSettings.save();
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: error.message || error
|
||||
};
|
||||
}
|
||||
} else {
|
||||
if (!settings && registeredUsers > 0) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'Registration disabled, enable it in settings.'
|
||||
}
|
||||
};
|
||||
} else {
|
||||
if (!settings.allowRegistration) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'You are not allowed here!'
|
||||
}
|
||||
};
|
||||
} else {
|
||||
const newUser = new User({
|
||||
_id: new mongoose.Types.ObjectId(),
|
||||
email,
|
||||
avatar: avatar_url,
|
||||
uid,
|
||||
type: 'github'
|
||||
});
|
||||
try {
|
||||
await newUser.save();
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: error.message || error
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const coolToken = jsonwebtoken.sign({}, JWT_SIGN_KEY, {
|
||||
expiresIn: 15778800,
|
||||
algorithm: 'HS256',
|
||||
audience: 'coolLabs',
|
||||
issuer: 'coolLabs',
|
||||
jwtid: uid,
|
||||
subject: `User:${uid}`,
|
||||
notBefore: -1000
|
||||
});
|
||||
request.locals.session.data = { coolToken, ghToken: access_token };
|
||||
return {
|
||||
status: 302,
|
||||
headers: {
|
||||
location: `/success`
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return { status: 500, body: { error: error.message || error } };
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
export async function del(request: Request) {
|
||||
request.locals.session.destroy()
|
||||
return {
|
||||
body: {
|
||||
ok: true
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { saveServerLog } from '$lib/api/applications/logging';
|
||||
import { execShellAsync } from '$lib/api/common';
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
|
||||
export async function post(request: Request) {
|
||||
try {
|
||||
const output = await execShellAsync('docker builder prune -af');
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
message: 'OK',
|
||||
output: output
|
||||
.replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm, '')
|
||||
.split('\n')
|
||||
.pop()
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
await saveServerLog(error);
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: error.message || error
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { saveServerLog } from '$lib/api/applications/logging';
|
||||
import { execShellAsync } from '$lib/api/common';
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
|
||||
export async function post(request: Request) {
|
||||
try {
|
||||
const output = await execShellAsync('docker container prune -f');
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
message: 'OK',
|
||||
output: output
|
||||
.replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm, '')
|
||||
.split('\n')
|
||||
.pop()
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
await saveServerLog(error);
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: error.message || error
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { saveServerLog } from '$lib/api/applications/logging';
|
||||
import { execShellAsync } from '$lib/api/common';
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
|
||||
export async function post(request: Request) {
|
||||
try {
|
||||
const output = await execShellAsync('docker image prune -af');
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
message: 'OK',
|
||||
output: output
|
||||
.replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm, '')
|
||||
.split('\n')
|
||||
.pop()
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
await saveServerLog(error);
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: error.message || error
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { saveServerLog } from '$lib/api/applications/logging';
|
||||
import { execShellAsync } from '$lib/api/common';
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
|
||||
export async function post(request: Request) {
|
||||
try {
|
||||
const output = await execShellAsync('docker volume prune -f');
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
message: 'OK',
|
||||
output: output
|
||||
.replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm, '')
|
||||
.split('\n')
|
||||
.pop()
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
await saveServerLog(error);
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: error.message || error
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { saveServerLog } from '$lib/api/applications/logging';
|
||||
import { execShellAsync } from '$lib/api/common';
|
||||
import { docker } from '$lib/api/docker';
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
import systeminformation from 'systeminformation';
|
||||
|
||||
export async function get(request: Request) {
|
||||
try {
|
||||
const df = await execShellAsync(`docker system df --format '{{ json . }}'`);
|
||||
const dockerReclaimable = df
|
||||
.split('\n')
|
||||
.filter((n) => n)
|
||||
.map((s) => JSON.parse(s));
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
hostname: await (await systeminformation.osInfo()).hostname,
|
||||
filesystems: await (
|
||||
await systeminformation.fsSize()
|
||||
).filter((fs) => !fs.fs.match('/dev/loop') || !fs.fs.match('/var/lib/docker/')),
|
||||
dockerReclaimable
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
await saveServerLog(error);
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: error.message || error
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { execShellAsync } from '$lib/api/common';
|
||||
import { docker } from '$lib/api/docker';
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
|
||||
export async function get(request: Request) {
|
||||
const { serviceName } = request.params;
|
||||
|
||||
try {
|
||||
const service = (await docker.engine.listServices()).find(
|
||||
(r) =>
|
||||
r.Spec.Labels.managedBy === 'coolify' &&
|
||||
r.Spec.Labels.type === 'service' &&
|
||||
r.Spec.Labels.serviceName === serviceName &&
|
||||
r.Spec.Name === `${serviceName}_${serviceName}`
|
||||
);
|
||||
if (service) {
|
||||
const payload = {
|
||||
config: JSON.parse(service.Spec.Labels.configuration)
|
||||
};
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
success: true,
|
||||
...payload
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
success: false,
|
||||
showToast: false,
|
||||
message: 'Not found'
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
success: false,
|
||||
error: error.message || error
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function del(request: Request) {
|
||||
const { serviceName } = request.params;
|
||||
await execShellAsync(`docker stack rm ${serviceName}`);
|
||||
return { status: 200, body: {} };
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
import yaml from 'js-yaml';
|
||||
import { promises as fs } from 'fs';
|
||||
import { docker } from '$lib/api/docker';
|
||||
import { baseServiceConfiguration } from '$lib/api/applications/common';
|
||||
import { cleanupTmp, execShellAsync } from '$lib/api/common';
|
||||
|
||||
export async function post(request: Request) {
|
||||
let { baseURL } = request.body;
|
||||
const traefikURL = baseURL;
|
||||
baseURL = `https://${baseURL}`;
|
||||
const workdir = '/tmp/code-server';
|
||||
const deployId = 'code-server';
|
||||
// const environment = [
|
||||
// { name: 'DOCKER_USER', value: 'root' }
|
||||
|
||||
// ];
|
||||
// const generateEnvsCodeServer = {};
|
||||
// for (const env of environment) generateEnvsCodeServer[env.name] = env.value;
|
||||
|
||||
const stack = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[deployId]: {
|
||||
image: 'codercom/code-server',
|
||||
command: 'code-server --disable-telemetry',
|
||||
networks: [`${docker.network}`],
|
||||
volumes: [`${deployId}-code-server-data:/home/coder`],
|
||||
// environment: generateEnvsCodeServer,
|
||||
deploy: {
|
||||
...baseServiceConfiguration,
|
||||
labels: [
|
||||
'managedBy=coolify',
|
||||
'type=service',
|
||||
'serviceName=code-server',
|
||||
'configuration=' +
|
||||
JSON.stringify({
|
||||
baseURL
|
||||
}),
|
||||
'traefik.enable=true',
|
||||
'traefik.http.services.' + deployId + '.loadbalancer.server.port=8080',
|
||||
'traefik.http.routers.' + deployId + '.entrypoints=websecure',
|
||||
'traefik.http.routers.' +
|
||||
deployId +
|
||||
'.rule=Host(`' +
|
||||
traefikURL +
|
||||
'`) && PathPrefix(`/`)',
|
||||
'traefik.http.routers.' + deployId + '.tls.certresolver=letsencrypt',
|
||||
'traefik.http.routers.' + deployId + '.middlewares=global-compress'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[`${docker.network}`]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
volumes: {
|
||||
[`${deployId}-code-server-data`]: {
|
||||
external: true
|
||||
}
|
||||
}
|
||||
};
|
||||
await execShellAsync(`mkdir -p ${workdir}`);
|
||||
await fs.writeFile(`${workdir}/stack.yml`, yaml.dump(stack));
|
||||
await execShellAsync('docker stack rm code-server');
|
||||
await execShellAsync(`cat ${workdir}/stack.yml | docker stack deploy --prune -c - ${deployId}`);
|
||||
cleanupTmp(workdir);
|
||||
return {
|
||||
status: 200,
|
||||
body: { message: 'OK' }
|
||||
};
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { execShellAsync } from '$lib/api/common';
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
export async function get(request: Request) {
|
||||
// const { POSTGRESQL_USERNAME, POSTGRESQL_PASSWORD, POSTGRESQL_DATABASE } = JSON.parse(
|
||||
// JSON.parse(
|
||||
// await execShellAsync(
|
||||
// "docker service inspect code-server_code-server --format='{{json .Spec.Labels.configuration}}'"
|
||||
// )
|
||||
// )
|
||||
// ).generateEnvsPostgres;
|
||||
const containers = (await execShellAsync("docker ps -a --format='{{json .Names}}'"))
|
||||
.replace(/"/g, '')
|
||||
.trim()
|
||||
.split('\n');
|
||||
const codeServer = containers.find((container) => container.startsWith('code-server'));
|
||||
const configYaml = yaml.load(
|
||||
await execShellAsync(
|
||||
`docker exec ${codeServer} cat /home/coder/.config/code-server/config.yaml`
|
||||
)
|
||||
);
|
||||
return {
|
||||
status: 200,
|
||||
body: { message: 'OK', password: configYaml.password }
|
||||
};
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
import yaml from 'js-yaml';
|
||||
import generator from 'generate-password';
|
||||
import { promises as fs } from 'fs';
|
||||
import { docker } from '$lib/api/docker';
|
||||
import { baseServiceConfiguration } from '$lib/api/applications/common';
|
||||
import { cleanupTmp, execShellAsync } from '$lib/api/common';
|
||||
|
||||
export async function post(request: Request) {
|
||||
let { baseURL } = request.body;
|
||||
const traefikURL = baseURL;
|
||||
baseURL = `https://${baseURL}`;
|
||||
const workdir = '/tmp/minio';
|
||||
const deployId = 'minio';
|
||||
const secrets = [
|
||||
{
|
||||
name: 'MINIO_ROOT_USER',
|
||||
value: generator.generate({ length: 12, numbers: true, strict: true })
|
||||
},
|
||||
{
|
||||
name: 'MINIO_ROOT_PASSWORD',
|
||||
value: generator.generate({ length: 24, numbers: true, strict: true })
|
||||
}
|
||||
];
|
||||
const generateEnvsMinIO = {};
|
||||
for (const secret of secrets) generateEnvsMinIO[secret.name] = secret.value;
|
||||
|
||||
const stack = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[deployId]: {
|
||||
image: 'minio/minio',
|
||||
command: 'server /data',
|
||||
networks: [`${docker.network}`],
|
||||
environment: generateEnvsMinIO,
|
||||
volumes: [`${deployId}-minio-data:/data`],
|
||||
deploy: {
|
||||
...baseServiceConfiguration,
|
||||
labels: [
|
||||
'managedBy=coolify',
|
||||
'type=service',
|
||||
'serviceName=minio',
|
||||
'configuration=' +
|
||||
JSON.stringify({
|
||||
baseURL,
|
||||
generateEnvsMinIO
|
||||
}),
|
||||
'traefik.enable=true',
|
||||
'traefik.http.services.' + deployId + '.loadbalancer.server.port=9000',
|
||||
'traefik.http.routers.' + deployId + '.entrypoints=websecure',
|
||||
'traefik.http.routers.' +
|
||||
deployId +
|
||||
'.rule=Host(`' +
|
||||
traefikURL +
|
||||
'`) && PathPrefix(`/`)',
|
||||
'traefik.http.routers.' + deployId + '.tls.certresolver=letsencrypt',
|
||||
'traefik.http.routers.' + deployId + '.middlewares=global-compress'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[`${docker.network}`]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
volumes: {
|
||||
[`${deployId}-minio-data`]: {
|
||||
external: true
|
||||
}
|
||||
}
|
||||
};
|
||||
await execShellAsync(`mkdir -p ${workdir}`);
|
||||
await fs.writeFile(`${workdir}/stack.yml`, yaml.dump(stack));
|
||||
await execShellAsync('docker stack rm minio');
|
||||
await execShellAsync(`cat ${workdir}/stack.yml | docker stack deploy --prune -c - ${deployId}`);
|
||||
cleanupTmp(workdir);
|
||||
return {
|
||||
status: 200,
|
||||
body: { message: 'OK' }
|
||||
};
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
import yaml from 'js-yaml';
|
||||
import { promises as fs } from 'fs';
|
||||
import { docker } from '$lib/api/docker';
|
||||
import { baseServiceConfiguration } from '$lib/api/applications/common';
|
||||
import { cleanupTmp, execShellAsync } from '$lib/api/common';
|
||||
|
||||
export async function post(request: Request) {
|
||||
let { baseURL } = request.body;
|
||||
const traefikURL = baseURL;
|
||||
baseURL = `https://${baseURL}`;
|
||||
const workdir = '/tmp/nocodb';
|
||||
const deployId = 'nocodb';
|
||||
const stack = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[deployId]: {
|
||||
image: 'nocodb/nocodb',
|
||||
networks: [`${docker.network}`],
|
||||
deploy: {
|
||||
...baseServiceConfiguration,
|
||||
labels: [
|
||||
'managedBy=coolify',
|
||||
'type=service',
|
||||
'serviceName=nocodb',
|
||||
'configuration=' +
|
||||
JSON.stringify({
|
||||
baseURL
|
||||
}),
|
||||
'traefik.enable=true',
|
||||
'traefik.http.services.' + deployId + '.loadbalancer.server.port=8080',
|
||||
'traefik.http.routers.' + deployId + '.entrypoints=websecure',
|
||||
'traefik.http.routers.' +
|
||||
deployId +
|
||||
'.rule=Host(`' +
|
||||
traefikURL +
|
||||
'`) && PathPrefix(`/`)',
|
||||
'traefik.http.routers.' + deployId + '.tls.certresolver=letsencrypt',
|
||||
'traefik.http.routers.' + deployId + '.middlewares=global-compress'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[`${docker.network}`]: {
|
||||
external: true
|
||||
}
|
||||
}
|
||||
};
|
||||
await execShellAsync(`mkdir -p ${workdir}`);
|
||||
await fs.writeFile(`${workdir}/stack.yml`, yaml.dump(stack));
|
||||
await execShellAsync('docker stack rm nocodb');
|
||||
await execShellAsync(`cat ${workdir}/stack.yml | docker stack deploy --prune -c - ${deployId}`);
|
||||
cleanupTmp(workdir);
|
||||
return {
|
||||
status: 200,
|
||||
body: { message: 'OK' }
|
||||
};
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { execShellAsync } from '$lib/api/common';
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
|
||||
export async function patch(request: Request) {
|
||||
const { POSTGRESQL_USERNAME, POSTGRESQL_PASSWORD, POSTGRESQL_DATABASE } = JSON.parse(
|
||||
JSON.parse(
|
||||
await execShellAsync(
|
||||
"docker service inspect plausible_plausible --format='{{json .Spec.Labels.configuration}}'"
|
||||
)
|
||||
)
|
||||
).generateEnvsPostgres;
|
||||
const containers = (await execShellAsync("docker ps -a --format='{{json .Names}}'"))
|
||||
.replace(/"/g, '')
|
||||
.trim()
|
||||
.split('\n');
|
||||
const postgresDB = containers.find((container) => container.startsWith('plausible_plausible_db'));
|
||||
await execShellAsync(
|
||||
`docker exec ${postgresDB} psql -H postgresql://${POSTGRESQL_USERNAME}:${POSTGRESQL_PASSWORD}@localhost:5432/${POSTGRESQL_DATABASE} -c "UPDATE users SET email_verified = true;"`
|
||||
);
|
||||
return {
|
||||
status: 200,
|
||||
body: { message: 'OK' }
|
||||
};
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
import generator from 'generate-password';
|
||||
import { promises as fs } from 'fs';
|
||||
import yaml from 'js-yaml';
|
||||
import { docker } from '$lib/api/docker';
|
||||
import { baseServiceConfiguration } from '$lib/api/applications/common';
|
||||
import { cleanupTmp, execShellAsync } from '$lib/api/common';
|
||||
|
||||
export async function post(request: Request) {
|
||||
const { email, userName, userPassword } = request.body;
|
||||
let { baseURL } = request.body;
|
||||
const traefikURL = baseURL;
|
||||
baseURL = `https://${baseURL}`;
|
||||
const deployId = 'plausible';
|
||||
const workdir = '/tmp/plausible';
|
||||
const secretKey = generator.generate({ length: 64, numbers: true, strict: true });
|
||||
const generateEnvsPostgres = {
|
||||
POSTGRESQL_PASSWORD: generator.generate({ length: 24, numbers: true, strict: true }),
|
||||
POSTGRESQL_USERNAME: generator.generate({ length: 10, numbers: true, strict: true }),
|
||||
POSTGRESQL_DATABASE: 'plausible'
|
||||
};
|
||||
|
||||
const secrets = [
|
||||
{ name: 'ADMIN_USER_EMAIL', value: email },
|
||||
{ name: 'ADMIN_USER_NAME', value: userName },
|
||||
{ name: 'ADMIN_USER_PWD', value: userPassword },
|
||||
{ name: 'BASE_URL', value: baseURL },
|
||||
{ name: 'SECRET_KEY_BASE', value: secretKey },
|
||||
{ name: 'DISABLE_AUTH', value: 'false' },
|
||||
{ name: 'DISABLE_REGISTRATION', value: 'true' },
|
||||
{
|
||||
name: 'DATABASE_URL',
|
||||
value: `postgresql://${generateEnvsPostgres.POSTGRESQL_USERNAME}:${generateEnvsPostgres.POSTGRESQL_PASSWORD}@plausible_db:5432/${generateEnvsPostgres.POSTGRESQL_DATABASE}`
|
||||
},
|
||||
{ name: 'CLICKHOUSE_DATABASE_URL', value: 'http://plausible_events_db:8123/plausible' }
|
||||
];
|
||||
|
||||
const generateEnvsClickhouse = {};
|
||||
for (const secret of secrets) generateEnvsClickhouse[secret.name] = secret.value;
|
||||
|
||||
const clickhouseConfigXml = `
|
||||
<yandex>
|
||||
<logger>
|
||||
<level>warning</level>
|
||||
<console>true</console>
|
||||
</logger>
|
||||
|
||||
<!-- Stop all the unnecessary logging -->
|
||||
<query_thread_log remove="remove"/>
|
||||
<query_log remove="remove"/>
|
||||
<text_log remove="remove"/>
|
||||
<trace_log remove="remove"/>
|
||||
<metric_log remove="remove"/>
|
||||
<asynchronous_metric_log remove="remove"/>
|
||||
</yandex>`;
|
||||
const clickhouseUserConfigXml = `
|
||||
<yandex>
|
||||
<profiles>
|
||||
<default>
|
||||
<log_queries>0</log_queries>
|
||||
<log_query_threads>0</log_query_threads>
|
||||
</default>
|
||||
</profiles>
|
||||
</yandex>`;
|
||||
|
||||
const clickhouseConfigs = [
|
||||
{
|
||||
source: 'plausible-clickhouse-user-config.xml',
|
||||
target: '/etc/clickhouse-server/users.d/logging.xml'
|
||||
},
|
||||
{
|
||||
source: 'plausible-clickhouse-config.xml',
|
||||
target: '/etc/clickhouse-server/config.d/logging.xml'
|
||||
},
|
||||
{ source: 'plausible-init.query', target: '/docker-entrypoint-initdb.d/init.query' },
|
||||
{ source: 'plausible-init-db.sh', target: '/docker-entrypoint-initdb.d/init-db.sh' }
|
||||
];
|
||||
|
||||
const initQuery = 'CREATE DATABASE IF NOT EXISTS plausible;';
|
||||
const initScript = 'clickhouse client --queries-file /docker-entrypoint-initdb.d/init.query';
|
||||
await execShellAsync(`mkdir -p ${workdir}`);
|
||||
await fs.writeFile(`${workdir}/clickhouse-config.xml`, clickhouseConfigXml);
|
||||
await fs.writeFile(`${workdir}/clickhouse-user-config.xml`, clickhouseUserConfigXml);
|
||||
await fs.writeFile(`${workdir}/init.query`, initQuery);
|
||||
await fs.writeFile(`${workdir}/init-db.sh`, initScript);
|
||||
const stack = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[deployId]: {
|
||||
image: 'plausible/analytics:latest',
|
||||
command:
|
||||
'sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh db init-admin && /entrypoint.sh run"',
|
||||
networks: [`${docker.network}`],
|
||||
volumes: [`${deployId}-postgres-data:/var/lib/postgresql/data`],
|
||||
environment: generateEnvsClickhouse,
|
||||
deploy: {
|
||||
...baseServiceConfiguration,
|
||||
labels: [
|
||||
'managedBy=coolify',
|
||||
'type=service',
|
||||
'serviceName=plausible',
|
||||
'configuration=' +
|
||||
JSON.stringify({
|
||||
email,
|
||||
userName,
|
||||
userPassword,
|
||||
baseURL,
|
||||
secretKey,
|
||||
generateEnvsPostgres,
|
||||
generateEnvsClickhouse
|
||||
}),
|
||||
'traefik.enable=true',
|
||||
'traefik.http.services.' + deployId + '.loadbalancer.server.port=8000',
|
||||
'traefik.http.routers.' + deployId + '.entrypoints=websecure',
|
||||
'traefik.http.routers.' +
|
||||
deployId +
|
||||
'.rule=Host(`' +
|
||||
traefikURL +
|
||||
'`) && PathPrefix(`/`)',
|
||||
'traefik.http.routers.' + deployId + '.tls.certresolver=letsencrypt',
|
||||
'traefik.http.routers.' + deployId + '.middlewares=global-compress'
|
||||
]
|
||||
}
|
||||
},
|
||||
plausible_db: {
|
||||
image: 'bitnami/postgresql:13.2.0',
|
||||
networks: [`${docker.network}`],
|
||||
environment: generateEnvsPostgres,
|
||||
deploy: {
|
||||
...baseServiceConfiguration,
|
||||
labels: ['managedBy=coolify', 'type=service', 'serviceName=plausible']
|
||||
}
|
||||
},
|
||||
plausible_events_db: {
|
||||
image: 'yandex/clickhouse-server:21.3.2.5',
|
||||
networks: [`${docker.network}`],
|
||||
volumes: [`${deployId}-clickhouse-data:/var/lib/clickhouse`],
|
||||
ulimits: {
|
||||
nofile: {
|
||||
soft: 262144,
|
||||
hard: 262144
|
||||
}
|
||||
},
|
||||
configs: [...clickhouseConfigs],
|
||||
deploy: {
|
||||
...baseServiceConfiguration,
|
||||
labels: ['managedBy=coolify', 'type=service', 'serviceName=plausible']
|
||||
}
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[`${docker.network}`]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
volumes: {
|
||||
[`${deployId}-clickhouse-data`]: {
|
||||
external: true
|
||||
},
|
||||
[`${deployId}-postgres-data`]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
configs: {
|
||||
'plausible-clickhouse-user-config.xml': {
|
||||
file: `${workdir}/clickhouse-user-config.xml`
|
||||
},
|
||||
'plausible-clickhouse-config.xml': {
|
||||
file: `${workdir}/clickhouse-config.xml`
|
||||
},
|
||||
'plausible-init.query': {
|
||||
file: `${workdir}/init.query`
|
||||
},
|
||||
'plausible-init-db.sh': {
|
||||
file: `${workdir}/init-db.sh`
|
||||
}
|
||||
}
|
||||
};
|
||||
await fs.writeFile(`${workdir}/stack.yml`, yaml.dump(stack));
|
||||
await execShellAsync('docker stack rm plausible');
|
||||
await execShellAsync(`cat ${workdir}/stack.yml | docker stack deploy --prune -c - ${deployId}`);
|
||||
cleanupTmp(workdir);
|
||||
return {
|
||||
status: 200,
|
||||
body: { message: 'OK' }
|
||||
};
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
import yaml from 'js-yaml';
|
||||
import generator from 'generate-password';
|
||||
import { promises as fs } from 'fs';
|
||||
import { docker } from '$lib/api/docker';
|
||||
import { baseServiceConfiguration } from '$lib/api/applications/common';
|
||||
import { cleanupTmp, execShellAsync } from '$lib/api/common';
|
||||
|
||||
export async function post(request: Request) {
|
||||
let { baseURL, remoteDB, database, wordpressExtraConfiguration } = request.body;
|
||||
const traefikURL = baseURL;
|
||||
baseURL = `https://${baseURL}`;
|
||||
const workdir = '/tmp/wordpress';
|
||||
const deployId = `wp-${generator.generate({ length: 5, numbers: true, strict: true })}`;
|
||||
const defaultDatabaseName = generator.generate({ length: 12, numbers: true, strict: true });
|
||||
const defaultDatabaseHost = `${deployId}-mysql`;
|
||||
const defaultDatabaseUser = generator.generate({ length: 12, numbers: true, strict: true });
|
||||
const defaultDatabasePassword = generator.generate({ length: 24, numbers: true, strict: true });
|
||||
const defaultDatabaseRootPassword = generator.generate({
|
||||
length: 24,
|
||||
numbers: true,
|
||||
strict: true
|
||||
});
|
||||
const defaultDatabaseRootUser = generator.generate({ length: 12, numbers: true, strict: true });
|
||||
let secrets = [
|
||||
{ name: 'WORDPRESS_DB_HOST', value: defaultDatabaseHost },
|
||||
{ name: 'WORDPRESS_DB_USER', value: defaultDatabaseUser },
|
||||
{ name: 'WORDPRESS_DB_PASSWORD', value: defaultDatabasePassword },
|
||||
{ name: 'WORDPRESS_DB_NAME', value: defaultDatabaseName },
|
||||
{ name: 'WORDPRESS_CONFIG_EXTRA', value: wordpressExtraConfiguration }
|
||||
];
|
||||
|
||||
const generateEnvsMySQL = {
|
||||
MYSQL_ROOT_PASSWORD: defaultDatabaseRootPassword,
|
||||
MYSQL_ROOT_USER: defaultDatabaseRootUser,
|
||||
MYSQL_USER: defaultDatabaseUser,
|
||||
MYSQL_PASSWORD: defaultDatabasePassword,
|
||||
MYSQL_DATABASE: defaultDatabaseName
|
||||
};
|
||||
const image = 'bitnami/mysql:8.0';
|
||||
const volume = `${deployId}-mysql-data:/bitnami/mysql/data`;
|
||||
|
||||
if (remoteDB) {
|
||||
secrets = [
|
||||
{ name: 'WORDPRESS_DB_HOST', value: database.host },
|
||||
{ name: 'WORDPRESS_DB_USER', value: database.user },
|
||||
{ name: 'WORDPRESS_DB_PASSWORD', value: database.password },
|
||||
{ name: 'WORDPRESS_DB_NAME', value: database.name },
|
||||
{ name: 'WORDPRESS_TABLE_PREFIX', value: database.tablePrefix },
|
||||
{ name: 'WORDPRESS_CONFIG_EXTRA', value: wordpressExtraConfiguration }
|
||||
];
|
||||
}
|
||||
|
||||
const generateEnvsWordpress = {};
|
||||
for (const secret of secrets) generateEnvsWordpress[secret.name] = secret.value;
|
||||
let stack = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[deployId]: {
|
||||
image: 'wordpress',
|
||||
networks: [`${docker.network}`],
|
||||
environment: generateEnvsWordpress,
|
||||
volumes: [`${deployId}-wordpress-data:/var/www/html`],
|
||||
deploy: {
|
||||
...baseServiceConfiguration,
|
||||
labels: [
|
||||
'managedBy=coolify',
|
||||
'type=service',
|
||||
'serviceName=' + deployId,
|
||||
'configuration=' +
|
||||
JSON.stringify({
|
||||
deployId,
|
||||
baseURL,
|
||||
generateEnvsWordpress
|
||||
}),
|
||||
'traefik.enable=true',
|
||||
'traefik.http.services.' + deployId + '.loadbalancer.server.port=80',
|
||||
'traefik.http.routers.' + deployId + '.entrypoints=websecure',
|
||||
'traefik.http.routers.' +
|
||||
deployId +
|
||||
'.rule=Host(`' +
|
||||
traefikURL +
|
||||
'`) && PathPrefix(`/`)',
|
||||
'traefik.http.routers.' + deployId + '.tls.certresolver=letsencrypt',
|
||||
'traefik.http.routers.' + deployId + '.middlewares=global-compress'
|
||||
]
|
||||
}
|
||||
},
|
||||
[`${deployId}-mysql`]: {
|
||||
image,
|
||||
networks: [`${docker.network}`],
|
||||
environment: generateEnvsMySQL,
|
||||
volumes: [volume],
|
||||
deploy: {
|
||||
...baseServiceConfiguration,
|
||||
labels: ['managedBy=coolify', 'type=service', 'serviceName=' + deployId]
|
||||
}
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[`${docker.network}`]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
volumes: {
|
||||
[`${deployId}-wordpress-data`]: {
|
||||
external: true
|
||||
},
|
||||
[`${deployId}-mysql-data`]: {
|
||||
external: true
|
||||
}
|
||||
}
|
||||
};
|
||||
if (remoteDB) {
|
||||
stack = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[deployId]: {
|
||||
image: 'wordpress',
|
||||
networks: [`${docker.network}`],
|
||||
environment: generateEnvsWordpress,
|
||||
volumes: [`${deployId}-wordpress-data:/var/www/html`],
|
||||
deploy: {
|
||||
...baseServiceConfiguration,
|
||||
labels: [
|
||||
'managedBy=coolify',
|
||||
'type=service',
|
||||
'serviceName=' + deployId,
|
||||
'configuration=' +
|
||||
JSON.stringify({
|
||||
deployId,
|
||||
baseURL,
|
||||
generateEnvsWordpress
|
||||
}),
|
||||
'traefik.enable=true',
|
||||
'traefik.http.services.' + deployId + '.loadbalancer.server.port=80',
|
||||
'traefik.http.routers.' + deployId + '.entrypoints=websecure',
|
||||
'traefik.http.routers.' +
|
||||
deployId +
|
||||
'.rule=Host(`' +
|
||||
traefikURL +
|
||||
'`) && PathPrefix(`/`)',
|
||||
'traefik.http.routers.' + deployId + '.tls.certresolver=letsencrypt',
|
||||
'traefik.http.routers.' + deployId + '.middlewares=global-compress'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[`${docker.network}`]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
volumes: {
|
||||
[`${deployId}-wordpress-data`]: {
|
||||
external: true
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
await execShellAsync(`mkdir -p ${workdir}`);
|
||||
await fs.writeFile(`${workdir}/stack.yml`, yaml.dump(stack));
|
||||
await execShellAsync(`docker stack rm ${deployId}`);
|
||||
await execShellAsync(`cat ${workdir}/stack.yml | docker stack deploy --prune -c - ${deployId}`);
|
||||
cleanupTmp(workdir);
|
||||
return {
|
||||
status: 200,
|
||||
body: { message: 'OK' }
|
||||
};
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { saveServerLog } from '$lib/api/applications/logging';
|
||||
import Settings from '$models/Settings';
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
const applicationName = 'coolify';
|
||||
|
||||
export async function get(request: Request) {
|
||||
try {
|
||||
const settings = await Settings.findOne({ applicationName }).select('-_id -__v');
|
||||
const payload = {
|
||||
applicationName,
|
||||
allowRegistration: false,
|
||||
...settings._doc
|
||||
};
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
...payload
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
await saveServerLog(error);
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: error.message || error
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
export async function post(request: Request) {
|
||||
try {
|
||||
const settings = await Settings.findOneAndUpdate(
|
||||
{ applicationName },
|
||||
{ applicationName, ...request.body },
|
||||
{ upsert: true, new: true }
|
||||
).select('-_id -__v');
|
||||
return {
|
||||
status: 201,
|
||||
body: {
|
||||
...settings._doc
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
await saveServerLog(error);
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: error.message || error
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { saveServerLog } from '$lib/api/applications/logging';
|
||||
import { execShellAsync } from '$lib/api/common';
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
|
||||
export async function get(request: Request) {
|
||||
const upgradeP1 = await execShellAsync(
|
||||
'bash -c "$(curl -fsSL https://get.coollabs.io/coolify/upgrade-p1.sh)"'
|
||||
);
|
||||
await saveServerLog({ message: upgradeP1, type: 'UPGRADE-P-1' });
|
||||
execShellAsync(
|
||||
'docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -u root coolify bash -c "$(curl -fsSL https://get.coollabs.io/coolify/upgrade-p2.sh)"'
|
||||
);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
message: "I'm trying, okay?"
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
import crypto from 'crypto';
|
||||
import Deployment from '$models/Deployment';
|
||||
import { docker } from '$lib/api/docker';
|
||||
import { precheckDeployment, setDefaultConfiguration } from '$lib/api/applications/configuration';
|
||||
import cloneRepository from '$lib/api/applications/cloneRepository';
|
||||
import { cleanupTmp, execShellAsync } from '$lib/api/common';
|
||||
import queueAndBuild from '$lib/api/applications/queueAndBuild';
|
||||
import Configuration from '$models/Configuration';
|
||||
import ApplicationLog from '$models/ApplicationLog';
|
||||
import { cleanupStuckedDeploymentsInDB } from '$lib/api/applications/cleanup';
|
||||
export async function post(request: Request) {
|
||||
let configuration;
|
||||
const allowedGithubEvents = ['push', 'pull_request'];
|
||||
const allowedPRActions = ['opened', 'reopened', 'synchronize', 'closed'];
|
||||
const githubEvent = request.headers['x-github-event'];
|
||||
const { GITHUP_APP_WEBHOOK_SECRET } = process.env;
|
||||
const hmac = crypto.createHmac('sha256', GITHUP_APP_WEBHOOK_SECRET);
|
||||
const digest = Buffer.from(
|
||||
'sha256=' + hmac.update(JSON.stringify(request.body)).digest('hex'),
|
||||
'utf8'
|
||||
);
|
||||
const checksum = Buffer.from(request.headers['x-hub-signature-256'], 'utf8');
|
||||
if (checksum.length !== digest.length || !crypto.timingSafeEqual(digest, checksum)) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'Invalid request.'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!allowedGithubEvents.includes(githubEvent)) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'Event not allowed.'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Monorepo support here. Find all configurations by id and update all deployments! Tough!
|
||||
try {
|
||||
const applications = await Configuration.find({
|
||||
'repository.id': request.body.repository.id
|
||||
}).select('-_id -__v -createdAt -updatedAt');
|
||||
if (githubEvent === 'push') {
|
||||
configuration = applications.find((r) => {
|
||||
if (request.body.ref.startsWith('refs')) {
|
||||
if (r.repository.branch === request.body.ref.split('/')[2]) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
} else if (githubEvent === 'pull_request') {
|
||||
if (!allowedPRActions.includes(request.body.action)) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'PR action is not allowed.'
|
||||
}
|
||||
};
|
||||
}
|
||||
configuration = applications.find(
|
||||
(r) => r.repository.branch === request.body['pull_request'].base.ref
|
||||
);
|
||||
if (configuration) {
|
||||
if (!configuration.general.isPreviewDeploymentEnabled) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'PR deployments are not enabled.'
|
||||
}
|
||||
};
|
||||
}
|
||||
configuration.general.pullRequest = request.body.number;
|
||||
}
|
||||
}
|
||||
if (!configuration) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'No configuration found.'
|
||||
}
|
||||
};
|
||||
}
|
||||
configuration = setDefaultConfiguration(configuration);
|
||||
const { id, organization, name, branch } = configuration.repository;
|
||||
const { domain } = configuration.publish;
|
||||
const { deployId, nickname, pullRequest } = configuration.general;
|
||||
|
||||
if (request.body.action === 'closed') {
|
||||
const deploys = await Deployment.find({ organization, branch, name, domain });
|
||||
for (const deploy of deploys) {
|
||||
await ApplicationLog.deleteMany({ deployId: deploy.deployId });
|
||||
await Deployment.deleteMany({ deployId: deploy.deployId });
|
||||
}
|
||||
await Configuration.findOneAndRemove({
|
||||
'repository.id': id,
|
||||
'repository.organization': organization,
|
||||
'repository.name': name,
|
||||
'repository.branch': branch,
|
||||
'general.pullRequest': pullRequest
|
||||
});
|
||||
await execShellAsync(`docker stack rm ${configuration.build.container.name}`);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
success: true,
|
||||
message: 'Removed'
|
||||
}
|
||||
};
|
||||
}
|
||||
await cloneRepository(configuration);
|
||||
const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment(
|
||||
configuration
|
||||
);
|
||||
if (foundService && !forceUpdate && !imageChanged && !configChanged) {
|
||||
cleanupTmp(configuration.general.workdir);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
success: false,
|
||||
message: 'Nothing changed, no need to redeploy.'
|
||||
}
|
||||
};
|
||||
}
|
||||
const alreadyQueued = await Deployment.find({
|
||||
repoId: id,
|
||||
branch: branch,
|
||||
organization: organization,
|
||||
name: name,
|
||||
domain: domain,
|
||||
progress: { $in: ['queued', 'inprogress'] }
|
||||
});
|
||||
if (alreadyQueued.length > 0) {
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
success: false,
|
||||
message: 'Already in the queue.'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
await new Deployment({
|
||||
repoId: id,
|
||||
branch,
|
||||
deployId,
|
||||
domain,
|
||||
organization,
|
||||
name,
|
||||
nickname
|
||||
}).save();
|
||||
|
||||
if (githubEvent === 'pull_request') {
|
||||
await Configuration.findOneAndUpdate(
|
||||
{
|
||||
'repository.id': id,
|
||||
'repository.organization': organization,
|
||||
'repository.name': name,
|
||||
'repository.branch': branch,
|
||||
'general.pullRequest': pullRequest
|
||||
},
|
||||
{ ...configuration },
|
||||
{ upsert: true, new: true }
|
||||
);
|
||||
} else {
|
||||
await Configuration.findOneAndUpdate(
|
||||
{
|
||||
'repository.id': id,
|
||||
'repository.organization': organization,
|
||||
'repository.name': name,
|
||||
'repository.branch': branch,
|
||||
'general.pullRequest': { $in: [null, 0] }
|
||||
},
|
||||
{ ...configuration },
|
||||
{ upsert: true, new: true }
|
||||
);
|
||||
}
|
||||
|
||||
queueAndBuild(configuration, imageChanged);
|
||||
return {
|
||||
status: 201,
|
||||
body: {
|
||||
message: 'Deployment queued.',
|
||||
nickname: configuration.general.nickname,
|
||||
name: configuration.build.container.name,
|
||||
deployId: configuration.general.deployId
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
// console.log(configuration)
|
||||
if (configuration) {
|
||||
cleanupTmp(configuration.general.workdir);
|
||||
await Deployment.findOneAndUpdate(
|
||||
{
|
||||
repoId: configuration.repository.id,
|
||||
branch: configuration.repository.branch,
|
||||
organization: configuration.repository.organization,
|
||||
name: configuration.repository.name,
|
||||
domain: configuration.publish.domain
|
||||
},
|
||||
{
|
||||
repoId: configuration.repository.id,
|
||||
branch: configuration.repository.branch,
|
||||
organization: configuration.repository.organization,
|
||||
name: configuration.repository.name,
|
||||
domain: configuration.publish.domain,
|
||||
progress: 'failed'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: error.message || error
|
||||
}
|
||||
};
|
||||
} finally {
|
||||
try {
|
||||
await cleanupStuckedDeploymentsInDB();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
<script>
|
||||
import Configuration from '$components/Application/Configuration.svelte';
|
||||
|
||||
</script>
|
||||
|
||||
<Configuration />
|
||||
@@ -1,83 +0,0 @@
|
||||
<script>
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import Loading from '$components/Loading.svelte';
|
||||
import { request } from '$lib/request';
|
||||
import { page, session } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/env';
|
||||
import { application } from '$store';
|
||||
|
||||
let loadLogsInterval;
|
||||
let logs = [];
|
||||
|
||||
onMount(() => {
|
||||
loadLogsInterval = setInterval(() => {
|
||||
loadLogs();
|
||||
}, 500);
|
||||
});
|
||||
|
||||
async function loadLogs() {
|
||||
try {
|
||||
const { events, progress } = await request(
|
||||
`/api/v1/application/deploy/logs/${$page.params.deployId}`,
|
||||
$session
|
||||
);
|
||||
logs = [...events];
|
||||
if (progress === 'done' || progress === 'failed') {
|
||||
clearInterval(loadLogsInterval);
|
||||
}
|
||||
} catch (error) {
|
||||
browser && goto('/dashboard/applications', { replaceState: true });
|
||||
}
|
||||
}
|
||||
onDestroy(() => {
|
||||
clearInterval(loadLogsInterval);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center"
|
||||
in:fade={{ duration: 100 }}
|
||||
>
|
||||
<div>Deployment log</div>
|
||||
<a
|
||||
target="_blank"
|
||||
class="icon mx-2"
|
||||
href={'https://' + $application.publish.domain + $application.publish.path}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
||||
/>
|
||||
</svg></a
|
||||
>
|
||||
</div>
|
||||
{#await loadLogs()}
|
||||
<Loading />
|
||||
{:then}
|
||||
<div class="text-center px-6" in:fade={{ duration: 100 }}>
|
||||
<div in:fade={{ duration: 100 }}>
|
||||
<pre
|
||||
class="leading-4 text-left text-sm font-semibold tracking-tighter rounded-lg bg-black p-6 whitespace-pre-wrap">
|
||||
{#if logs.length > 0}
|
||||
{#each logs as log}
|
||||
{log + '\n'}
|
||||
{/each}
|
||||
{:else}
|
||||
It's starting soon.
|
||||
{/if}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
{/await}
|
||||
@@ -1,137 +0,0 @@
|
||||
<script>
|
||||
import { application, dateOptions } from '$store';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
|
||||
import Loading from '$components/Loading.svelte';
|
||||
import { request } from '$lib/request';
|
||||
import { session } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let loadDeploymentsInterval = null;
|
||||
let loadLogsInterval = null;
|
||||
let deployments = [];
|
||||
let logs = [];
|
||||
let page = 1;
|
||||
|
||||
onMount(async () => {
|
||||
loadApplicationLogs();
|
||||
loadLogsInterval = setInterval(() => {
|
||||
loadApplicationLogs();
|
||||
}, 3000);
|
||||
loadDeploymentsInterval = setInterval(() => {
|
||||
loadDeploymentLogs();
|
||||
}, 1000);
|
||||
});
|
||||
onDestroy(() => {
|
||||
clearInterval(loadDeploymentsInterval);
|
||||
clearInterval(loadLogsInterval);
|
||||
});
|
||||
async function loadMoreDeploymentLogs() {
|
||||
page = page + 1;
|
||||
await loadDeploymentLogs();
|
||||
}
|
||||
async function loadDeploymentLogs() {
|
||||
deployments = (
|
||||
await request(
|
||||
`/api/v1/application/deploy/logs?repoId=${$application.repository.id}&branch=${$application.repository.branch}&page=${page}`,
|
||||
$session
|
||||
)
|
||||
).logs;
|
||||
}
|
||||
async function loadApplicationLogs() {
|
||||
logs = (
|
||||
await request(`/api/v1/application/logs?name=${$application.build.container.name}`, $session)
|
||||
).logs;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center"
|
||||
in:fade={{ duration: 100 }}
|
||||
>
|
||||
<div>Logs</div>
|
||||
</div>
|
||||
{#await loadDeploymentLogs()}
|
||||
<Loading />
|
||||
{:then}
|
||||
<div class="text-center px-6" in:fade={{ duration: 100 }}>
|
||||
<div class="flex pt-2 space-x-4 w-full">
|
||||
<div class="w-full">
|
||||
<div class="font-bold text-left pb-2 text-xl">Application logs</div>
|
||||
{#if logs.length === 0}
|
||||
<div class="text-xs font-semibold tracking-tighter">Waiting for the logs...</div>
|
||||
{:else}
|
||||
<pre
|
||||
class="leading-4 text-left text-sm font-semibold tracking-tighter rounded-lg bg-black p-6 whitespace-pre-wrap w-full">
|
||||
{#each logs as log}
|
||||
{log + '\n'}
|
||||
{/each}
|
||||
</pre>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold text-left pb-2 text-xl w-300">Deployment logs</div>
|
||||
{#if deployments.length > 0}
|
||||
<div class="space-y-2">
|
||||
{#each deployments as deployment}
|
||||
<div
|
||||
in:fade={{ duration: 100 }}
|
||||
class="flex space-x-4 text-md py-4 hover:shadow mx-auto cursor-pointer transition-all duration-100 border-l-4 border-transparent rounded hover:bg-warmGray-700"
|
||||
class:hover:border-green-500={deployment.progress === 'done'}
|
||||
class:border-yellow-300={deployment.progress !== 'done' &&
|
||||
deployment.progress !== 'failed'}
|
||||
class:bg-warmGray-800={deployment.progress !== 'done' &&
|
||||
deployment.progress !== 'failed'}
|
||||
class:hover:border-red-500={deployment.progress === 'failed'}
|
||||
on:click={() => goto(`./logs/${deployment.deployId}`)}
|
||||
>
|
||||
<div class="flex space-x-2 px-2">
|
||||
<div class="font-bold text-sm flex justify-center items-center">
|
||||
{deployment.branch}
|
||||
</div>
|
||||
<div class="font-bold text-xs flex justify-center items-center text-warmGray-500">{deployment.isPr ? 'PR' : ''}</div>
|
||||
</div>
|
||||
<div class="flex-1" />
|
||||
<div class="px-3 w-48">
|
||||
<div
|
||||
class="text-xs"
|
||||
title={new Intl.DateTimeFormat('default', dateOptions).format(
|
||||
new Date(deployment.createdAt)
|
||||
)}
|
||||
>
|
||||
{deployment.since}
|
||||
</div>
|
||||
{#if deployment.progress === 'done'}
|
||||
<div class="text-xs">
|
||||
Deployed in <span class="font-bold">{deployment.took}s</span>
|
||||
</div>
|
||||
{:else if deployment.progress === 'failed'}
|
||||
<div class="text-xs text-red-500">Failed</div>
|
||||
{:else}
|
||||
<div class="text-xs">Deploying...</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<button
|
||||
class="text-xs bg-green-600 hover:bg-green-500 p-1 rounded text-white px-2 font-medium my-6"
|
||||
on:click={loadMoreDeploymentLogs}>Show more</button
|
||||
>
|
||||
{:else}
|
||||
<div class="text-left text-sm tracking-tight">No deployments found</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:catch}
|
||||
<div class="text-center font-bold tracking-tight text-xl">No logs found</div>
|
||||
{/await}
|
||||
|
||||
<style lang="postcss">
|
||||
.w-300 {
|
||||
width: 300px !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,137 +0,0 @@
|
||||
<script context="module" lang="ts">
|
||||
/**
|
||||
* @type {import('@sveltejs/kit').Load}
|
||||
*/
|
||||
export async function load(session) {
|
||||
if (!browser) {
|
||||
if (!import.meta.env.VITE_GITHUB_APP_CLIENTID) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/dashboard/services'
|
||||
};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { application, initialApplication, initConf, dashboard, prApplication, originalDomain } from '$store';
|
||||
import { onDestroy } from 'svelte';
|
||||
import Loading from '$components/Loading.svelte';
|
||||
import Navbar from '$components/Application/Navbar.svelte';
|
||||
import { page, session } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/env';
|
||||
import { request } from '$lib/request';
|
||||
|
||||
$application.general.nickname = $page.params.nickname;
|
||||
async function setConfiguration() {
|
||||
try {
|
||||
const { configuration } = await request(`/api/v1/application/config`, $session, {
|
||||
body: {
|
||||
nickname: $application.general.nickname
|
||||
}
|
||||
});
|
||||
$prApplication = configuration.filter((c) => c.general.pullRequest !== 0);
|
||||
$application = configuration.find((c) => c.general.pullRequest === 0);
|
||||
if (!$application) browser && goto('/dashboard/applications');
|
||||
} catch (error) {
|
||||
browser && goto('/dashboard/applications');
|
||||
}
|
||||
}
|
||||
async function loadConfiguration() {
|
||||
if ($page.path !== '/application/new') {
|
||||
if (!$dashboard) {
|
||||
await setConfiguration();
|
||||
} else {
|
||||
const found = $dashboard.applications.deployed.find((app) => {
|
||||
const { domain } = app.configuration.publish;
|
||||
if (domain === $application.publish.domain) {
|
||||
return app;
|
||||
}
|
||||
});
|
||||
if (found) {
|
||||
$application = { ...found.configuration };
|
||||
$initConf = JSON.parse(JSON.stringify($application));
|
||||
} else {
|
||||
await setConfiguration();
|
||||
}
|
||||
}
|
||||
$originalDomain = $application.publish.domain
|
||||
|
||||
} else {
|
||||
$application = JSON.parse(JSON.stringify(initialApplication));
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
$application = JSON.parse(JSON.stringify(initialApplication));
|
||||
});
|
||||
</script>
|
||||
|
||||
{#await loadConfiguration()}
|
||||
<Loading />
|
||||
{:then}
|
||||
<Navbar />
|
||||
<div class="text-white">
|
||||
{#if $page.path.endsWith('configuration')}
|
||||
<div class="min-h-full text-white">
|
||||
<div class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center">
|
||||
{$application.publish.domain
|
||||
? `${$application.publish.domain}${
|
||||
$application.publish.path !== '/' ? $application.publish.path : ''
|
||||
}`
|
||||
: 'example.com'}
|
||||
<a
|
||||
target="_blank"
|
||||
class="icon mx-2"
|
||||
href={'https://' + $application.publish.domain + $application.publish.path}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
||||
/>
|
||||
</svg></a
|
||||
>
|
||||
|
||||
<a
|
||||
target="_blank"
|
||||
class="icon"
|
||||
href={`https://github.com/${$application.repository.organization}/${$application.repository.name}`}
|
||||
>
|
||||
<svg
|
||||
class="w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><path
|
||||
d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"
|
||||
/></svg
|
||||
></a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{:else if $page.path === '/application/new'}
|
||||
<div class="min-h-full text-white">
|
||||
<div class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center">
|
||||
New Application
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<slot />
|
||||
</div>
|
||||
{/await}
|
||||
@@ -1,5 +0,0 @@
|
||||
<script>
|
||||
import Configuration from '$components/Application/Configuration.svelte';
|
||||
</script>
|
||||
|
||||
<Configuration />
|
||||
376
src/routes/applications/[id]/__layout.svelte
Normal file
376
src/routes/applications/[id]/__layout.svelte
Normal file
@@ -0,0 +1,376 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
function checkConfiguration(application): string {
|
||||
let configurationPhase = null;
|
||||
if (!application.gitSourceId) {
|
||||
configurationPhase = 'source';
|
||||
} else if (!application.repository && !application.branch) {
|
||||
configurationPhase = 'repository';
|
||||
} else if (!application.destinationDockerId) {
|
||||
configurationPhase = 'destination';
|
||||
} else if (!application.buildPack) {
|
||||
configurationPhase = 'buildpack';
|
||||
}
|
||||
return configurationPhase;
|
||||
}
|
||||
export const load: Load = async ({ fetch, url, params }) => {
|
||||
const endpoint = `/applications/${params.id}.json`;
|
||||
const res = await fetch(endpoint);
|
||||
if (res.ok) {
|
||||
const { application, githubToken, ghToken, isRunning, appId } = await res.json();
|
||||
if (!application || Object.entries(application).length === 0) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/applications'
|
||||
};
|
||||
}
|
||||
const configurationPhase = checkConfiguration(application);
|
||||
if (
|
||||
configurationPhase &&
|
||||
url.pathname !== `/applications/${params.id}/configuration/${configurationPhase}`
|
||||
) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/applications/${params.id}/configuration/${configurationPhase}`
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
application,
|
||||
isRunning
|
||||
},
|
||||
stuff: {
|
||||
isRunning,
|
||||
ghToken,
|
||||
githubToken,
|
||||
application,
|
||||
appId
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/applications'
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let application;
|
||||
export let isRunning;
|
||||
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';
|
||||
import { del, post } from '$lib/api';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let loading = false;
|
||||
const { id } = $page.params;
|
||||
|
||||
async function handleDeploySubmit() {
|
||||
try {
|
||||
const { buildId } = await post(`/applications/${id}/deploy.json`, { ...application });
|
||||
return await goto(`/applications/${id}/logs/build?buildId=${buildId}`);
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteApplication(name) {
|
||||
const sure = confirm(`Are you sure you would like to delete '${name}'?`);
|
||||
if (sure) {
|
||||
loading = true;
|
||||
try {
|
||||
await del(`/applications/${id}/delete.json`, { id });
|
||||
return await goto(`/applications`);
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
async function stopApplication() {
|
||||
try {
|
||||
loading = true;
|
||||
await post(`/applications/${id}/stop.json`, {});
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav class="nav-side">
|
||||
{#if loading}
|
||||
<Loading fullscreen cover />
|
||||
{:else}
|
||||
{#if application.fqdn && application.gitSource && application.repository && application.destinationDocker && application.buildPack}
|
||||
{#if isRunning}
|
||||
<button
|
||||
on:click={stopApplication}
|
||||
title="Stop application"
|
||||
type="submit"
|
||||
disabled={!$session.isAdmin}
|
||||
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 hover:bg-green-600 hover:text-white"
|
||||
data-tooltip={$session.isAdmin
|
||||
? 'Stop application'
|
||||
: 'You do not have permission to stop the application.'}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<rect x="6" y="5" width="4" height="14" rx="1" />
|
||||
<rect x="14" y="5" width="4" height="14" rx="1" />
|
||||
</svg>
|
||||
</button>
|
||||
<form on:submit|preventDefault={handleDeploySubmit}>
|
||||
<button
|
||||
title="Rebuild application"
|
||||
type="submit"
|
||||
disabled={!$session.isAdmin}
|
||||
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 hover:bg-green-600 hover:text-white"
|
||||
data-tooltip={$session.isAdmin
|
||||
? 'Rebuild application'
|
||||
: 'You do not have permission to rebuild application.'}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M16.3 5h.7a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h5l-2.82 -2.82m0 5.64l2.82 -2.82"
|
||||
transform="rotate(-45 12 12)"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
{:else}
|
||||
<form on:submit|preventDefault={handleDeploySubmit}>
|
||||
<button
|
||||
title="Build and start application"
|
||||
type="submit"
|
||||
disabled={!$session.isAdmin}
|
||||
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 hover:bg-green-600 hover:text-white"
|
||||
data-tooltip={$session.isAdmin
|
||||
? 'Build and start application'
|
||||
: 'You do not have permission to Build and start application.'}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
<div class="border border-stone-700 h-8" />
|
||||
<a
|
||||
href="/applications/{id}"
|
||||
sveltekit:prefetch
|
||||
class="hover:text-yellow-500 rounded"
|
||||
class:text-yellow-500={$page.url.pathname === `/applications/${id}`}
|
||||
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}`}
|
||||
>
|
||||
<button
|
||||
title="Configurations"
|
||||
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
|
||||
data-tooltip="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
|
||||
>
|
||||
<a
|
||||
href="/applications/{id}/secrets"
|
||||
sveltekit:prefetch
|
||||
class="hover:text-pink-500 rounded"
|
||||
class:text-pink-500={$page.url.pathname === `/applications/${id}/secrets`}
|
||||
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/secrets`}
|
||||
>
|
||||
<button
|
||||
title="Secrets"
|
||||
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
|
||||
data-tooltip="Secrets"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3"
|
||||
/>
|
||||
<circle cx="12" cy="11" r="1" />
|
||||
<line x1="12" y1="12" x2="12" y2="14.5" />
|
||||
</svg></button
|
||||
></a
|
||||
>
|
||||
<a
|
||||
href="/applications/{id}/previews"
|
||||
sveltekit:prefetch
|
||||
class="hover:text-orange-500 rounded"
|
||||
class:text-orange-500={$page.url.pathname === `/applications/${id}/previews`}
|
||||
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/previews`}
|
||||
>
|
||||
<button
|
||||
title="Previews"
|
||||
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
|
||||
data-tooltip="Previews"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<circle cx="7" cy="18" r="2" />
|
||||
<circle cx="7" cy="6" r="2" />
|
||||
<circle cx="17" cy="12" r="2" />
|
||||
<line x1="7" y1="8" x2="7" y2="16" />
|
||||
<path d="M7 8a4 4 0 0 0 4 4h4" />
|
||||
</svg></button
|
||||
></a
|
||||
>
|
||||
<div class="border border-stone-700 h-8" />
|
||||
<a
|
||||
href="/applications/{id}/logs"
|
||||
sveltekit:prefetch
|
||||
class="hover:text-sky-500 rounded"
|
||||
class:text-sky-500={$page.url.pathname === `/applications/${id}/logs`}
|
||||
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs`}
|
||||
>
|
||||
<button
|
||||
title="Application Logs"
|
||||
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500 "
|
||||
data-tooltip="Application 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
|
||||
>
|
||||
<a
|
||||
href="/applications/{id}/logs/build"
|
||||
sveltekit:prefetch
|
||||
class="hover:text-red-500 rounded"
|
||||
class:text-red-500={$page.url.pathname === `/applications/${id}/logs/build`}
|
||||
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs/build`}
|
||||
>
|
||||
<button
|
||||
title="Build Logs"
|
||||
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500 "
|
||||
data-tooltip="Build 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" />
|
||||
<circle cx="19" cy="13" r="2" />
|
||||
<circle cx="4" cy="17" r="2" />
|
||||
<circle cx="13" cy="17" r="2" />
|
||||
<line x1="13" y1="19" x2="4" y2="19" />
|
||||
<line x1="4" y1="15" x2="13" y2="15" />
|
||||
<path d="M8 12v-5h2a3 3 0 0 1 3 3v5" />
|
||||
<path d="M5 15v-2a1 1 0 0 1 1 -1h7" />
|
||||
<path d="M19 11v-7l-6 7" />
|
||||
</svg>
|
||||
</button></a
|
||||
>
|
||||
<div class="border border-stone-700 h-8" />
|
||||
{/if}
|
||||
|
||||
<button
|
||||
on:click={() => deleteApplication(application.name)}
|
||||
title="Delete application"
|
||||
type="submit"
|
||||
disabled={!$session.isAdmin}
|
||||
class:hover:text-red-500={$session.isAdmin}
|
||||
class="icons bg-transparent tooltip-bottom text-sm"
|
||||
data-tooltip={$session.isAdmin
|
||||
? 'Delete application'
|
||||
: 'You do not have permission to delete this application'}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</button>
|
||||
{/if}
|
||||
</nav>
|
||||
<slot />
|
||||
28
src/routes/applications/[id]/check.json.ts
Normal file
28
src/routes/applications/[id]/check.json.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { asyncExecShell, getDomain, getEngine, getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } 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 { fqdn } = await event.request.json();
|
||||
fqdn = fqdn.toLowerCase();
|
||||
|
||||
try {
|
||||
const found = await db.isDomainConfigured({ id, fqdn });
|
||||
if (found) {
|
||||
throw {
|
||||
message: `Domain ${getDomain(fqdn)} is already configured.`
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: 200
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
44
src/routes/applications/[id]/configuration/_BuildPack.svelte
Normal file
44
src/routes/applications/[id]/configuration/_BuildPack.svelte
Normal file
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { post } from '$lib/api';
|
||||
import { errorNotification } from '$lib/form';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
|
||||
export let buildPack;
|
||||
export let foundConfig;
|
||||
export let scanning;
|
||||
|
||||
async function handleSubmit(name) {
|
||||
try {
|
||||
const tempBuildPack = JSON.parse(JSON.stringify(buildPack));
|
||||
delete tempBuildPack.name;
|
||||
delete tempBuildPack.fancyName;
|
||||
delete tempBuildPack.color;
|
||||
delete tempBuildPack.hoverColor;
|
||||
|
||||
if (foundConfig.buildPack !== name) {
|
||||
await post(`/applications/${id}.json`, { ...tempBuildPack });
|
||||
}
|
||||
await post(`/applications/${id}/configuration/buildpack.json`, { buildPack: name });
|
||||
return await goto(from || `/applications/${id}`);
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<form on:submit|preventDefault={() => handleSubmit(buildPack.name)}>
|
||||
<button
|
||||
type="submit"
|
||||
class="box-selection relative flex text-xl font-bold {buildPack.hoverColor} {foundConfig?.name ===
|
||||
buildPack.name && buildPack.color}"
|
||||
><span>{buildPack.fancyName}</span>
|
||||
{#if !scanning && foundConfig?.name === buildPack.name}
|
||||
<span class="absolute bottom-0 pb-2 text-xs">Choose this one...</span>
|
||||
{/if}
|
||||
</button>
|
||||
</form>
|
||||
@@ -0,0 +1,170 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let githubToken;
|
||||
export let application;
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { get, post } from '$lib/api';
|
||||
import { getGithubToken } from '$lib/components/common';
|
||||
import { enhance, errorNotification } from '$lib/form';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
const to = $page.url.searchParams.get('to');
|
||||
|
||||
let htmlUrl = application.gitSource.htmlUrl;
|
||||
let apiUrl = application.gitSource.apiUrl;
|
||||
|
||||
let loading = {
|
||||
repositories: true,
|
||||
branches: false
|
||||
};
|
||||
let repositories = [];
|
||||
let branches = [];
|
||||
|
||||
let selected = {
|
||||
projectId: undefined,
|
||||
repository: undefined,
|
||||
branch: undefined
|
||||
};
|
||||
let showSave = false;
|
||||
let token = null;
|
||||
|
||||
async function loadRepositoriesByPage(page = 0) {
|
||||
try {
|
||||
return await get(`${apiUrl}/installation/repositories?per_page=100&page=${page}`, {
|
||||
Authorization: `token ${token}`
|
||||
});
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function loadRepositories() {
|
||||
token = await getGithubToken({ apiUrl, githubToken, application });
|
||||
let page = 1;
|
||||
let reposCount = 0;
|
||||
const loadedRepos = await loadRepositoriesByPage();
|
||||
repositories = repositories.concat(loadedRepos.repositories);
|
||||
reposCount = loadedRepos.total_count;
|
||||
if (reposCount > repositories.length) {
|
||||
while (reposCount > repositories.length) {
|
||||
page = page + 1;
|
||||
const repos = await loadRepositoriesByPage(page);
|
||||
repositories = repositories.concat(repos.repositories);
|
||||
}
|
||||
}
|
||||
loading.repositories = false;
|
||||
}
|
||||
async function loadBranches() {
|
||||
loading.branches = true;
|
||||
selected.branch = undefined;
|
||||
selected.projectId = repositories.find((repo) => repo.full_name === selected.repository).id;
|
||||
try {
|
||||
branches = await get(`${apiUrl}/repos/${selected.repository}/branches`, {
|
||||
Authorization: `token ${token}`
|
||||
});
|
||||
return;
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.branches = false;
|
||||
}
|
||||
}
|
||||
async function isBranchAlreadyUsed() {
|
||||
try {
|
||||
return await get(
|
||||
`/applications/${id}/configuration/repository.json?repository=${selected.repository}&branch=${selected.branch}`
|
||||
);
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
showSave = true;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await loadRepositories();
|
||||
});
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await post(`/applications/${id}/configuration/repository.json`, { ...selected });
|
||||
if (to) {
|
||||
return await goto(`${to}?from=${from}`);
|
||||
}
|
||||
return await goto(from || `/applications/${id}/configuration/destination`);
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if repositories.length === 0 && loading.repositories === false}
|
||||
<div class="flex-col text-center">
|
||||
<div class="pb-4">No repositories configured for your Git Application.</div>
|
||||
<a href={`/sources/${application.gitSource.id}`}><button>Configure it now</button></a>
|
||||
</div>
|
||||
{:else}
|
||||
<form on:submit|preventDefault={handleSubmit}>
|
||||
<div>
|
||||
{#if loading.repositories}
|
||||
<select name="repository" disabled class="w-96">
|
||||
<option selected value="">Loading repositories...</option>
|
||||
</select>
|
||||
{:else}
|
||||
<select
|
||||
name="repository"
|
||||
class="w-96"
|
||||
bind:value={selected.repository}
|
||||
on:change={loadBranches}
|
||||
>
|
||||
<option value="" disabled selected>Please select a repository</option>
|
||||
{#each repositories as repository}
|
||||
<option value={repository.full_name}>{repository.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
<input class="hidden" bind:value={selected.projectId} name="projectId" />
|
||||
{#if loading.branches}
|
||||
<select name="branch" disabled class="w-96">
|
||||
<option selected value="">Loading branches...</option>
|
||||
</select>
|
||||
{:else}
|
||||
<select
|
||||
name="branch"
|
||||
class="w-96"
|
||||
disabled={!selected.repository}
|
||||
bind:value={selected.branch}
|
||||
on:change={isBranchAlreadyUsed}
|
||||
>
|
||||
{#if !selected.repository}
|
||||
<option value="" disabled selected>Select a repository first</option>
|
||||
{:else}
|
||||
<option value="" disabled selected>Please select a branch</option>
|
||||
{/if}
|
||||
|
||||
{#each branches as branch}
|
||||
<option value={branch.name}>{branch.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="pt-5 flex-col flex justify-center items-center space-y-4">
|
||||
<button
|
||||
class="w-40"
|
||||
type="submit"
|
||||
disabled={!showSave}
|
||||
class:bg-orange-600={showSave}
|
||||
class:hover:bg-orange-500={showSave}>Save</button
|
||||
>
|
||||
<!-- <button class="w-40"
|
||||
><a
|
||||
class="no-underline"
|
||||
href="{apiUrl}/apps/{application.gitSource.githubApp.name}/installations/new"
|
||||
>Modify Repositories</a
|
||||
></button
|
||||
> -->
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
@@ -0,0 +1,334 @@
|
||||
<script lang="ts">
|
||||
export let application;
|
||||
export let appId;
|
||||
import { page, session } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { errorNotification } from '$lib/form';
|
||||
import { dev } from '$app/env';
|
||||
import cuid from 'cuid';
|
||||
import { goto } from '$app/navigation';
|
||||
import { del, get, post, put } from '$lib/api';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
|
||||
const updateDeployKeyIdUrl = `/applications/${id}/configuration/deploykey.json`;
|
||||
|
||||
let loading = {
|
||||
base: true,
|
||||
projects: false,
|
||||
branches: false,
|
||||
save: false
|
||||
};
|
||||
|
||||
let htmlUrl = application.gitSource.htmlUrl;
|
||||
let apiUrl = application.gitSource.apiUrl;
|
||||
|
||||
let username = null;
|
||||
let groups = [];
|
||||
let projects = [];
|
||||
let branches = [];
|
||||
let showSave = false;
|
||||
|
||||
let selected = {
|
||||
group: undefined,
|
||||
project: undefined,
|
||||
branch: undefined
|
||||
};
|
||||
onMount(async () => {
|
||||
if (!$session.gitlabToken) {
|
||||
getGitlabToken();
|
||||
} else {
|
||||
loading.base = true;
|
||||
try {
|
||||
const user = await get(`${apiUrl}/v4/user`, {
|
||||
Authorization: `Bearer ${$session.gitlabToken}`
|
||||
});
|
||||
username = user.username;
|
||||
} catch (error) {
|
||||
return getGitlabToken();
|
||||
}
|
||||
try {
|
||||
groups = await get(`${apiUrl}/v4/groups?per_page=5000`, {
|
||||
Authorization: `Bearer ${$session.gitlabToken}`
|
||||
});
|
||||
} catch (error) {
|
||||
errorNotification(error);
|
||||
throw new Error(error);
|
||||
} finally {
|
||||
loading.base = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function getGitlabToken() {
|
||||
const left = screen.width / 2 - 1020 / 2;
|
||||
const top = screen.height / 2 - 618 / 2;
|
||||
const newWindow = open(
|
||||
`${htmlUrl}/oauth/authorize?client_id=${application.gitSource.gitlabApp.appId}&redirect_uri=${window.location.origin}/webhooks/gitlab&response_type=code&scope=api+email+read_repository&state=${$page.params.id}`,
|
||||
'GitLab',
|
||||
'resizable=1, scrollbars=1, fullscreen=0, height=618, width=1020,top=' +
|
||||
top +
|
||||
', left=' +
|
||||
left +
|
||||
', toolbar=0, menubar=0, status=0'
|
||||
);
|
||||
const timer = setInterval(() => {
|
||||
if (newWindow?.closed) {
|
||||
clearInterval(timer);
|
||||
window.location.reload();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
async function loadProjects() {
|
||||
loading.projects = true;
|
||||
if (username === selected.group.name) {
|
||||
try {
|
||||
projects = await get(
|
||||
`${apiUrl}/v4/users/${selected.group.name}/projects?min_access_level=40&page=1&per_page=25&archived=false`,
|
||||
{
|
||||
Authorization: `Bearer ${$session.gitlabToken}`
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
errorNotification(error);
|
||||
throw new Error(error);
|
||||
} finally {
|
||||
loading.projects = false;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
projects = await get(
|
||||
`${apiUrl}/v4/groups/${selected.group.id}/projects?page=1&per_page=25&archived=false`,
|
||||
{
|
||||
Authorization: `Bearer ${$session.gitlabToken}`
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
errorNotification(error);
|
||||
throw new Error(error);
|
||||
} finally {
|
||||
loading.projects = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBranches() {
|
||||
loading.branches = true;
|
||||
try {
|
||||
branches = await get(
|
||||
`${apiUrl}/v4/projects/${selected.project.id}/repository/branches?per_page=100&page=1`,
|
||||
{
|
||||
Authorization: `Bearer ${$session.gitlabToken}`
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
errorNotification(error);
|
||||
throw new Error(error);
|
||||
} finally {
|
||||
loading.branches = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function isBranchAlreadyUsed() {
|
||||
const url = `/applications/${id}/configuration/repository.json?repository=${selected.project.path_with_namespace}&branch=${selected.branch.name}`;
|
||||
|
||||
try {
|
||||
await get(url);
|
||||
showSave = true;
|
||||
} catch (error) {
|
||||
showSave = false;
|
||||
return errorNotification('Branch already configured');
|
||||
}
|
||||
}
|
||||
// async function saveDeployKey(deployKeyId: number) {
|
||||
// try {
|
||||
// await post(updateDeployKeyIdUrl, { deployKeyId });
|
||||
// } catch (error) {
|
||||
// errorNotification(error);
|
||||
// throw new Error(error);
|
||||
// }
|
||||
// }
|
||||
async function checkSSHKey(sshkeyUrl) {
|
||||
try {
|
||||
return await post(sshkeyUrl, {});
|
||||
} catch (error) {
|
||||
errorNotification(error);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
async function setWebhook(url, webhookToken) {
|
||||
const host = dev
|
||||
? 'https://webhook.site/0e5beb2c-4e9b-40e2-a89e-32295e570c21'
|
||||
: `${window.location.origin}/webhooks/gitlab/events`;
|
||||
try {
|
||||
await post(
|
||||
url,
|
||||
{
|
||||
id: selected.project.id,
|
||||
url: host,
|
||||
token: webhookToken,
|
||||
push_events: true,
|
||||
enable_ssl_verification: true,
|
||||
merge_requests_events: true
|
||||
},
|
||||
{
|
||||
Authorization: `Bearer ${$session.gitlabToken}`
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
errorNotification(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async function save() {
|
||||
loading.save = true;
|
||||
let privateSshKey = application.gitSource.gitlabApp.privateSshKey;
|
||||
let publicSshKey = application.gitSource.gitlabApp.publicSshKey;
|
||||
|
||||
const deployKeyUrl = `${apiUrl}/v4/projects/${selected.project.id}/deploy_keys`;
|
||||
const sshkeyUrl = `/applications/${id}/configuration/sshkey.json`;
|
||||
const webhookUrl = `${apiUrl}/v4/projects/${selected.project.id}/hooks`;
|
||||
const webhookToken = cuid();
|
||||
|
||||
try {
|
||||
if (!privateSshKey || !publicSshKey) {
|
||||
const { publicKey } = await checkSSHKey(sshkeyUrl);
|
||||
publicSshKey = publicKey;
|
||||
}
|
||||
const deployKeys = await get(deployKeyUrl, {
|
||||
Authorization: `Bearer ${$session.gitlabToken}`
|
||||
});
|
||||
const deployKeyFound = deployKeys.filter((dk) => dk.title === `${appId}-coolify-deploy-key`);
|
||||
if (deployKeyFound.length > 0) {
|
||||
for (const deployKey of deployKeyFound) {
|
||||
console.log(`${deployKeyUrl}/${deployKey.id}`);
|
||||
await del(
|
||||
`${deployKeyUrl}/${deployKey.id}`,
|
||||
{},
|
||||
{
|
||||
Authorization: `Bearer ${$session.gitlabToken}`
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
const { id } = await post(
|
||||
deployKeyUrl,
|
||||
{
|
||||
title: `${appId}-coolify-deploy-key`,
|
||||
key: publicSshKey,
|
||||
can_push: false
|
||||
},
|
||||
{
|
||||
Authorization: `Bearer ${$session.gitlabToken}`
|
||||
}
|
||||
);
|
||||
await post(updateDeployKeyIdUrl, { deployKeyId: id });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
try {
|
||||
await setWebhook(webhookUrl, webhookToken);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
if (!dev) throw new Error(err);
|
||||
}
|
||||
|
||||
const url = `/applications/${id}/configuration/repository.json`;
|
||||
try {
|
||||
await post(url, {
|
||||
repository: `${selected.group.full_path}/${selected.project.name}`,
|
||||
branch: selected.branch.name,
|
||||
projectId: selected.project.id,
|
||||
webhookToken
|
||||
});
|
||||
return await goto(from || `/applications/${id}/configuration/buildpack`);
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await post(`/applications/{id}/configuration/repository.json`, { ...selected });
|
||||
return await goto(from || `/applications/${id}/configuration/destination`);
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<form on:submit={handleSubmit}>
|
||||
<div class="flex flex-col space-y-2 px-4 xl:flex-row xl:space-y-0 xl:space-x-2 ">
|
||||
{#if loading.base}
|
||||
<select name="group" disabled class="w-96">
|
||||
<option selected value="">Loading groups...</option>
|
||||
</select>
|
||||
{:else}
|
||||
<select name="group" class="w-96" bind:value={selected.group} on:change={loadProjects}>
|
||||
<option value="" disabled selected>Please select a group</option>
|
||||
{#each groups as group}
|
||||
<option value={group}>{group.full_name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
{#if loading.projects}
|
||||
<select name="project" disabled class="w-96">
|
||||
<option selected value="">Loading projects...</option>
|
||||
</select>
|
||||
{:else if !loading.projects && projects.length > 0}
|
||||
<select
|
||||
name="project"
|
||||
class="w-96"
|
||||
bind:value={selected.project}
|
||||
on:change={loadBranches}
|
||||
disabled={!selected.group}
|
||||
>
|
||||
<option value="" disabled selected>Please select a project</option>
|
||||
{#each projects as project}
|
||||
<option value={project}>{project.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else}
|
||||
<select name="project" disabled class="w-96">
|
||||
<option disabled selected value="">No projects found</option>
|
||||
</select>
|
||||
{/if}
|
||||
|
||||
{#if loading.branches}
|
||||
<select name="branch" disabled class="w-96">
|
||||
<option selected value="">Loading branches...</option>
|
||||
</select>
|
||||
{:else if !loading.branches && branches.length > 0}
|
||||
<select
|
||||
name="branch"
|
||||
class="w-96"
|
||||
bind:value={selected.branch}
|
||||
on:change={isBranchAlreadyUsed}
|
||||
disabled={!selected.project}
|
||||
>
|
||||
<option value="" disabled selected>Please select a branch</option>
|
||||
{#each branches as branch}
|
||||
<option value={branch}>{branch.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else}
|
||||
<select name="project" disabled class="w-96">
|
||||
<option disabled selected value="">No branches found</option>
|
||||
</select>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-center space-y-4 pt-5">
|
||||
<button
|
||||
on:click|preventDefault={save}
|
||||
class="w-40"
|
||||
type="submit"
|
||||
disabled={!showSave || loading.save}
|
||||
class:bg-orange-600={showSave && !loading.save}
|
||||
class:hover:bg-orange-500={showSave && !loading.save}
|
||||
>{loading.save ? 'Saving...' : 'Save'}</button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
41
src/routes/applications/[id]/configuration/buildpack.json.ts
Normal file
41
src/routes/applications/[id]/configuration/buildpack.json.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
try {
|
||||
const application = await db.getApplication({ id, teamId });
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
type: application.gitSource.type,
|
||||
projectId: application.projectId,
|
||||
repository: application.repository,
|
||||
branch: application.branch,
|
||||
apiUrl: application.gitSource.apiUrl
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
const { buildPack } = await event.request.json();
|
||||
|
||||
try {
|
||||
await db.configureBuildPack({ id, buildPack });
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
186
src/routes/applications/[id]/configuration/buildpack.svelte
Normal file
186
src/routes/applications/[id]/configuration/buildpack.svelte
Normal file
@@ -0,0 +1,186 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
const { application, ghToken } = stuff;
|
||||
if (application?.buildPack && !url.searchParams.get('from')) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/applications/${params.id}`
|
||||
};
|
||||
}
|
||||
const endpoint = `/applications/${params.id}/configuration/buildpack.json`;
|
||||
const res = await fetch(endpoint);
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
...(await res.json()),
|
||||
application,
|
||||
ghToken
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import { buildPacks, scanningTemplates } from '$lib/components/templates';
|
||||
import BuildPack from './_BuildPack.svelte';
|
||||
import { page, session } from '$app/stores';
|
||||
import { get } from '$lib/api';
|
||||
import { errorNotification } from '$lib/form';
|
||||
|
||||
let scanning = true;
|
||||
let foundConfig = null;
|
||||
|
||||
export let apiUrl;
|
||||
export let projectId;
|
||||
export let repository;
|
||||
export let branch;
|
||||
export let ghToken;
|
||||
export let type;
|
||||
export let application;
|
||||
|
||||
function checkPackageJSONContents({ key, json }) {
|
||||
return json?.dependencies?.hasOwnProperty(key) || json?.devDependencies?.hasOwnProperty(key);
|
||||
}
|
||||
function checkTemplates({ json }) {
|
||||
for (const [key, value] of Object.entries(scanningTemplates)) {
|
||||
if (checkPackageJSONContents({ key, json })) {
|
||||
return buildPacks.find((bp) => bp.name === value.buildPack);
|
||||
}
|
||||
}
|
||||
}
|
||||
async function scanRepository() {
|
||||
try {
|
||||
if (type === 'gitlab') {
|
||||
const files = await get(`${apiUrl}/v4/projects/${projectId}/repository/tree`, {
|
||||
Authorization: `Bearer ${$session.gitlabToken}`
|
||||
});
|
||||
const packageJson = files.find(
|
||||
(file) => file.name === 'package.json' && file.type === 'blob'
|
||||
);
|
||||
const dockerfile = files.find((file) => file.name === 'Dockerfile' && file.type === 'blob');
|
||||
const cargoToml = files.find((file) => file.name === 'Cargo.toml' && file.type === 'blob');
|
||||
const requirementsTxt = files.find(
|
||||
(file) => file.name === 'requirements.txt' && file.type === 'blob'
|
||||
);
|
||||
const indexHtml = files.find((file) => file.name === 'index.html' && file.type === 'blob');
|
||||
const indexPHP = files.find((file) => file.name === 'index.php' && file.type === 'blob');
|
||||
if (dockerfile) {
|
||||
foundConfig.buildPack = 'docker';
|
||||
} else if (packageJson) {
|
||||
const path = packageJson.path;
|
||||
const data = await get(
|
||||
`${apiUrl}/v4/projects/${projectId}/repository/files/${path}/raw?ref=${branch}`,
|
||||
{
|
||||
Authorization: `Bearer ${$session.gitlabToken}`
|
||||
}
|
||||
);
|
||||
const json = JSON.parse(data) || {};
|
||||
foundConfig = checkTemplates({ json });
|
||||
} else if (cargoToml) {
|
||||
foundConfig = buildPacks.find((bp) => bp.name === 'rust');
|
||||
} else if (requirementsTxt) {
|
||||
foundConfig = buildPacks.find((bp) => bp.name === 'python');
|
||||
} else if (indexHtml) {
|
||||
foundConfig = buildPacks.find((bp) => bp.name === 'static');
|
||||
} else if (indexPHP) {
|
||||
foundConfig = buildPacks.find((bp) => bp.name === 'php');
|
||||
}
|
||||
} else if (type === 'github') {
|
||||
const files = await get(`${apiUrl}/repos/${repository}/contents?ref=${branch}`, {
|
||||
Authorization: `Bearer ${ghToken}`,
|
||||
Accept: 'application/vnd.github.v2.json'
|
||||
});
|
||||
const packageJson = files.find(
|
||||
(file) => file.name === 'package.json' && file.type === 'file'
|
||||
);
|
||||
const dockerfile = files.find((file) => file.name === 'Dockerfile' && file.type === 'file');
|
||||
const cargoToml = files.find((file) => file.name === 'Cargo.toml' && file.type === 'file');
|
||||
const requirementsTxt = files.find(
|
||||
(file) => file.name === 'requirements.txt' && file.type === 'file'
|
||||
);
|
||||
const indexHtml = files.find((file) => file.name === 'index.html' && file.type === 'file');
|
||||
const indexPHP = files.find((file) => file.name === 'index.php' && file.type === 'file');
|
||||
if (dockerfile) {
|
||||
foundConfig.buildPack = 'docker';
|
||||
} else if (packageJson) {
|
||||
const data = await get(`${packageJson.git_url}`, {
|
||||
Authorization: `Bearer ${ghToken}`,
|
||||
Accept: 'application/vnd.github.v2.raw'
|
||||
});
|
||||
const json = JSON.parse(data) || {};
|
||||
foundConfig = checkTemplates({ json });
|
||||
} else if (cargoToml) {
|
||||
foundConfig = buildPacks.find((bp) => bp.name === 'rust');
|
||||
} else if (requirementsTxt) {
|
||||
foundConfig = buildPacks.find((bp) => bp.name === 'python');
|
||||
} else if (indexHtml) {
|
||||
foundConfig = buildPacks.find((bp) => bp.name === 'static');
|
||||
} else if (indexPHP) {
|
||||
foundConfig = buildPacks.find((bp) => bp.name === 'php');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
error.error === 'invalid_token' ||
|
||||
error.error_description ===
|
||||
'Token is expired. You can either do re-authorization or token refresh.' ||
|
||||
error.message === '401 Unauthorized'
|
||||
) {
|
||||
if (application.gitSource.gitlabAppId) {
|
||||
let htmlUrl = application.gitSource.htmlUrl;
|
||||
const left = screen.width / 2 - 1020 / 2;
|
||||
const top = screen.height / 2 - 618 / 2;
|
||||
const newWindow = open(
|
||||
`${htmlUrl}/oauth/authorize?client_id=${application.gitSource.gitlabApp.appId}&redirect_uri=${window.location.origin}/webhooks/gitlab&response_type=code&scope=api+email+read_repository&state=${$page.params.id}`,
|
||||
'GitLab',
|
||||
'resizable=1, scrollbars=1, fullscreen=0, height=618, width=1020,top=' +
|
||||
top +
|
||||
', left=' +
|
||||
left +
|
||||
', toolbar=0, menubar=0, status=0'
|
||||
);
|
||||
const timer = setInterval(() => {
|
||||
if (newWindow?.closed) {
|
||||
clearInterval(timer);
|
||||
window.location.reload();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
if (!foundConfig) foundConfig = buildPacks.find((bp) => bp.name === 'node');
|
||||
scanning = false;
|
||||
}
|
||||
}
|
||||
onMount(async () => {
|
||||
await scanRepository();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">Configure Build Pack</div>
|
||||
</div>
|
||||
|
||||
{#if scanning}
|
||||
<div class="flex justify-center space-x-1 p-6 font-bold">
|
||||
<div class="text-xl tracking-tight">Scanning repository to suggest a build pack for you...</div>
|
||||
</div>
|
||||
{:else}
|
||||
<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 />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
17
src/routes/applications/[id]/configuration/deploykey.json.ts
Normal file
17
src/routes/applications/[id]/configuration/deploykey.json.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { id } = event.params;
|
||||
let { deployKeyId } = await event.request.json();
|
||||
|
||||
deployKeyId = Number(deployKeyId);
|
||||
|
||||
try {
|
||||
await db.updateDeployKey({ id, deployKeyId });
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
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;
|
||||
const { destinationId } = await event.request.json();
|
||||
|
||||
try {
|
||||
await db.configureDestinationForApplication({ id, destinationId });
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
const { application } = stuff;
|
||||
if (application?.destinationDockerId && !url.searchParams.get('from')) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/applications/${params.id}`
|
||||
};
|
||||
}
|
||||
const endpoint = `/destinations.json`;
|
||||
const res = await fetch(endpoint);
|
||||
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
...(await res.json())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type Prisma from '@prisma/client';
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { errorNotification } from '$lib/form';
|
||||
import { goto } from '$app/navigation';
|
||||
import { post } from '$lib/api';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
|
||||
export let destinations: Prisma.DestinationDocker[];
|
||||
|
||||
async function handleSubmit(destinationId) {
|
||||
try {
|
||||
await post(`/applications/${id}/configuration/destination.json`, { destinationId });
|
||||
return await goto(from || `/applications/${id}/configuration/buildpack`);
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">Configure Destination</div>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
{#if !destinations || destinations.length === 0}
|
||||
<div class="flex-col">
|
||||
<div class="pb-2">No configurable Destination found</div>
|
||||
<div class="flex justify-center">
|
||||
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
|
||||
<svg
|
||||
class="w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||
/></svg
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#each destinations as destination}
|
||||
<div class="p-2">
|
||||
<form on:submit|preventDefault={() => handleSubmit(destination.id)}>
|
||||
<button type="submit" class="box-selection hover:bg-sky-700 font-bold">
|
||||
<div class="font-bold text-xl text-center truncate">{destination.name}</div>
|
||||
<div class="text-center truncate">{destination.network}</div>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,47 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
|
||||
const repository = event.url.searchParams.get('repository')?.toLocaleLowerCase() || undefined;
|
||||
const branch = event.url.searchParams.get('branch')?.toLocaleLowerCase() || undefined;
|
||||
|
||||
try {
|
||||
const found = await db.isBranchAlreadyUsed({ repository, branch, id });
|
||||
if (found) {
|
||||
throw {
|
||||
error: `Branch ${branch} is already used by another application`
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: 200
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
let { repository, branch, projectId, webhookToken } = await event.request.json();
|
||||
|
||||
repository = repository.toLowerCase();
|
||||
branch = branch.toLowerCase();
|
||||
projectId = Number(projectId);
|
||||
|
||||
try {
|
||||
await db.configureGitRepository({ id, repository, branch, projectId, webhookToken });
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
38
src/routes/applications/[id]/configuration/repository.svelte
Normal file
38
src/routes/applications/[id]/configuration/repository.svelte
Normal file
@@ -0,0 +1,38 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ params, url, stuff }) => {
|
||||
const { application, githubToken, appId } = stuff;
|
||||
if (application?.branch && application?.repository && !url.searchParams.get('from')) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/applications/${params.id}`
|
||||
};
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
githubToken,
|
||||
application,
|
||||
appId
|
||||
}
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let application;
|
||||
export let githubToken;
|
||||
export let appId;
|
||||
import GithubRepositories from './_GithubRepositories.svelte';
|
||||
import GitlabRepositories from './_GitlabRepositories.svelte';
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">Select a Repository / Project</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#if application.gitSource.type === 'github'}
|
||||
<GithubRepositories {application} {githubToken} />
|
||||
{:else if application.gitSource.type === 'gitlab'}
|
||||
<GitlabRepositories {application} {appId} />
|
||||
{/if}
|
||||
</div>
|
||||
18
src/routes/applications/[id]/configuration/source.json.ts
Normal file
18
src/routes/applications/[id]/configuration/source.json.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
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;
|
||||
const { gitSourceId } = await event.request.json();
|
||||
try {
|
||||
await db.configureGitsource({ id, gitSourceId });
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
109
src/routes/applications/[id]/configuration/source.svelte
Normal file
109
src/routes/applications/[id]/configuration/source.svelte
Normal file
@@ -0,0 +1,109 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
const { application } = stuff;
|
||||
if (application?.gitSourceId && !url.searchParams.get('from')) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/applications/${params.id}`
|
||||
};
|
||||
}
|
||||
const endpoint = `/sources.json`;
|
||||
const res = await fetch(endpoint);
|
||||
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
...(await res.json())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type Prisma from '@prisma/client';
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { enhance, errorNotification } from '$lib/form';
|
||||
import { goto } from '$app/navigation';
|
||||
import { post } from '$lib/api';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
|
||||
export let sources: Prisma.GitSource[] & {
|
||||
gitlabApp: Prisma.GitlabApp;
|
||||
githubApp: Prisma.GithubApp;
|
||||
};
|
||||
sources = sources.filter(
|
||||
(source) =>
|
||||
(source.type === 'github' && source.githubAppId && source.githubApp.installationId) ||
|
||||
(source.type === 'gitlab' && source.gitlabAppId)
|
||||
);
|
||||
async function handleSubmit(gitSourceId) {
|
||||
try {
|
||||
await post(`/applications/${id}/configuration/source.json`, { gitSourceId });
|
||||
return await goto(from || `/applications/${id}/configuration/repository`);
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">Select a Git Source</div>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
{#if !sources || sources.length === 0}
|
||||
<div class="flex-col">
|
||||
<div class="pb-2">No configurable Git Source found</div>
|
||||
<div class="flex justify-center">
|
||||
<a href="/new/source" sveltekit:prefetch class="add-icon bg-orange-600 hover:bg-orange-500">
|
||||
<svg
|
||||
class="w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||
/></svg
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#each sources as source}
|
||||
<div class="p-2">
|
||||
<form on:submit|preventDefault={() => handleSubmit(source.id)}>
|
||||
<button
|
||||
disabled={source.gitlabApp && !source.gitlabAppId}
|
||||
type="submit"
|
||||
class="disabled:opacity-95 bg-coolgray-200 disabled:text-white box-selection hover:bg-orange-700 group"
|
||||
class:border-red-500={source.gitlabApp && !source.gitlabAppId}
|
||||
class:border-0={source.gitlabApp && !source.gitlabAppId}
|
||||
class:border-l-4={source.gitlabApp && !source.gitlabAppId}
|
||||
>
|
||||
<div class="font-bold text-xl text-center truncate">{source.name}</div>
|
||||
{#if source.gitlabApp && !source.gitlabAppId}
|
||||
<div class="font-bold text-center truncate text-red-500 group-hover:text-white">
|
||||
Configuration missing
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
20
src/routes/applications/[id]/configuration/sshkey.json.ts
Normal file
20
src/routes/applications/[id]/configuration/sshkey.json.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
const { id } = event.params;
|
||||
try {
|
||||
return await db.getSshKey({ id });
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { id } = event.params;
|
||||
try {
|
||||
return await db.generateSshKey({ id });
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
19
src/routes/applications/[id]/delete.json.ts
Normal file
19
src/routes/applications/[id]/delete.json.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const del: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
try {
|
||||
await db.removeApplication({ id, teamId });
|
||||
return {
|
||||
status: 200
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
42
src/routes/applications/[id]/deploy.json.ts
Normal file
42
src/routes/applications/[id]/deploy.json.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import * as db from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import cuid from 'cuid';
|
||||
import crypto from 'crypto';
|
||||
import { buildQueue } from '$lib/queues';
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
|
||||
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 buildId = cuid();
|
||||
const applicationFound = await db.getApplication({ id, teamId });
|
||||
if (!applicationFound.configHash) {
|
||||
const configHash = crypto
|
||||
.createHash('sha256')
|
||||
.update(
|
||||
JSON.stringify({
|
||||
buildPack: applicationFound.buildPack,
|
||||
port: applicationFound.port,
|
||||
installCommand: applicationFound.installCommand,
|
||||
buildCommand: applicationFound.buildCommand,
|
||||
startCommand: applicationFound.startCommand
|
||||
})
|
||||
)
|
||||
.digest('hex');
|
||||
await db.prisma.application.update({ where: { id }, data: { configHash } });
|
||||
}
|
||||
await buildQueue.add(buildId, { build_id: buildId, type: 'manual', ...applicationFound });
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
buildId
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
87
src/routes/applications/[id]/index.json.ts
Normal file
87
src/routes/applications/[id]/index.json.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { getTeam, getUserDetails } from '$lib/common';
|
||||
import { getGithubToken } from '$lib/components/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import { checkContainer } from '$lib/haproxy';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import jsonwebtoken from 'jsonwebtoken';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const appId = process.env['COOLIFY_APP_ID'];
|
||||
let githubToken = null;
|
||||
let ghToken = null;
|
||||
let isRunning = false;
|
||||
|
||||
const { id } = event.params;
|
||||
try {
|
||||
const application = await db.getApplication({ id, teamId });
|
||||
const { gitSource } = application;
|
||||
if (gitSource?.type === 'github' && gitSource?.githubApp) {
|
||||
const payload = {
|
||||
iat: Math.round(new Date().getTime() / 1000),
|
||||
exp: Math.round(new Date().getTime() / 1000 + 60),
|
||||
iss: gitSource.githubApp.appId
|
||||
};
|
||||
githubToken = jsonwebtoken.sign(payload, gitSource.githubApp.privateKey, {
|
||||
algorithm: 'RS256'
|
||||
});
|
||||
ghToken = await getGithubToken({ apiUrl: gitSource.apiUrl, application, githubToken });
|
||||
}
|
||||
if (application.destinationDockerId) {
|
||||
isRunning = await checkContainer(application.destinationDocker.engine, id);
|
||||
}
|
||||
return {
|
||||
body: {
|
||||
isRunning,
|
||||
ghToken,
|
||||
githubToken,
|
||||
application,
|
||||
appId
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
let {
|
||||
name,
|
||||
buildPack,
|
||||
fqdn,
|
||||
port,
|
||||
installCommand,
|
||||
buildCommand,
|
||||
startCommand,
|
||||
baseDirectory,
|
||||
publishDirectory
|
||||
} = await event.request.json();
|
||||
|
||||
if (port) port = Number(port);
|
||||
|
||||
try {
|
||||
await db.configureApplication({
|
||||
id,
|
||||
buildPack,
|
||||
name,
|
||||
fqdn,
|
||||
port,
|
||||
installCommand,
|
||||
buildCommand,
|
||||
startCommand,
|
||||
baseDirectory,
|
||||
publishDirectory
|
||||
});
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
390
src/routes/applications/[id]/index.svelte
Normal file
390
src/routes/applications/[id]/index.svelte
Normal file
@@ -0,0 +1,390 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, stuff }) => {
|
||||
if (stuff?.application?.id) {
|
||||
return {
|
||||
props: {
|
||||
application: stuff.application,
|
||||
isRunning: stuff.isRunning
|
||||
}
|
||||
};
|
||||
}
|
||||
const endpoint = `/applications/${params.id}.json`;
|
||||
const res = await fetch(endpoint);
|
||||
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
...(await res.json())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
error: new Error(`Could not load ${endpoint}`)
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let application: Prisma.Application & {
|
||||
settings: Prisma.ApplicationSettings;
|
||||
gitlabApp: Prisma.GitlabApp;
|
||||
gitSource: Prisma.GitSource;
|
||||
destinationDocker: Prisma.DestinationDocker;
|
||||
};
|
||||
export let isRunning;
|
||||
import { page, session } from '$app/stores';
|
||||
import { errorNotification } from '$lib/form';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import Setting from '$lib/components/Setting.svelte';
|
||||
import type Prisma from '@prisma/client';
|
||||
import { getDomain, notNodeDeployments, staticDeployments } from '$lib/components/common';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { post } from '$lib/api';
|
||||
const { id } = $page.params;
|
||||
|
||||
let domainEl: HTMLInputElement;
|
||||
|
||||
let loading = false;
|
||||
let debug = application.settings.debug;
|
||||
let previews = application.settings.previews;
|
||||
|
||||
onMount(() => {
|
||||
domainEl.focus();
|
||||
});
|
||||
|
||||
async function changeSettings(name) {
|
||||
if (name === 'debug') {
|
||||
debug = !debug;
|
||||
}
|
||||
if (name === 'previews') {
|
||||
previews = !previews;
|
||||
}
|
||||
try {
|
||||
await post(`/applications/${id}/settings.json`, { previews, debug });
|
||||
return toast.push('Settings saved.');
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
try {
|
||||
await post(`/applications/${id}/check.json`, { fqdn: application.fqdn });
|
||||
await post(`/applications/${id}.json`, { ...application });
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<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">
|
||||
{application.name}
|
||||
</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 class="mx-auto max-w-4xl px-6">
|
||||
<!-- svelte-ignore missing-declaration -->
|
||||
<form on:submit|preventDefault={handleSubmit} class="py-4">
|
||||
<div class="flex space-x-1 pb-5 font-bold">
|
||||
<div class="title">General</div>
|
||||
{#if $session.isAdmin}
|
||||
<button
|
||||
type="submit"
|
||||
class:bg-green-600={!loading}
|
||||
class:hover:bg-green-500={!loading}
|
||||
disabled={loading}>{loading ? 'Saving...' : 'Save'}</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grid grid-flow-row gap-2 px-10">
|
||||
<div class="mt-2 grid grid-cols-3 items-center">
|
||||
<label for="name">Name</label>
|
||||
<div class="col-span-2 ">
|
||||
<input
|
||||
readonly={!$session.isAdmin}
|
||||
name="name"
|
||||
id="name"
|
||||
bind:value={application.name}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="gitSource">Git Source</label>
|
||||
<div class="col-span-2">
|
||||
<a
|
||||
href={$session.isAdmin
|
||||
? `/applications/${id}/configuration/source?from=/applications/${id}`
|
||||
: ''}
|
||||
class="no-underline"
|
||||
><input
|
||||
value={application.gitSource.name}
|
||||
id="gitSource"
|
||||
disabled
|
||||
class="cursor-pointer bg-coolgray-200 hover:bg-coolgray-500"
|
||||
/></a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="repository">Git Repository</label>
|
||||
<div class="col-span-2">
|
||||
<a
|
||||
href={$session.isAdmin
|
||||
? `/applications/${id}/configuration/repository?from=/applications/${id}&to=/applications/${id}/configuration/buildpack`
|
||||
: ''}
|
||||
class="no-underline"
|
||||
><input
|
||||
value="{application.repository}/{application.branch}"
|
||||
id="repository"
|
||||
disabled
|
||||
class="cursor-pointer bg-coolgray-200 hover:bg-coolgray-500"
|
||||
/></a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="buildPack">Build Pack</label>
|
||||
<div class="col-span-2">
|
||||
<a
|
||||
href={$session.isAdmin
|
||||
? `/applications/${id}/configuration/buildpack?from=/applications/${id}`
|
||||
: ''}
|
||||
class="no-underline "
|
||||
>
|
||||
<input
|
||||
value={application.buildPack}
|
||||
id="buildPack"
|
||||
disabled
|
||||
class="cursor-pointer bg-coolgray-200 hover:bg-coolgray-500"
|
||||
/></a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center pb-8">
|
||||
<label for="destination">Destination</label>
|
||||
<div class="col-span-2">
|
||||
<div class="no-underline">
|
||||
<input
|
||||
value={application.destinationDocker.name}
|
||||
id="destination"
|
||||
disabled
|
||||
class="bg-transparent "
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Application</div>
|
||||
</div>
|
||||
<div class="grid grid-flow-row gap-2 px-10">
|
||||
<div class="grid grid-cols-3">
|
||||
<label for="fqdn" class="pt-2">Domain (FQDN)</label>
|
||||
<div class="col-span-2">
|
||||
<input
|
||||
readonly={!$session.isAdmin || isRunning}
|
||||
disabled={!$session.isAdmin || isRunning}
|
||||
bind:this={domainEl}
|
||||
name="fqdn"
|
||||
id="fqdn"
|
||||
bind:value={application.fqdn}
|
||||
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
|
||||
placeholder="eg: https://coollabs.io"
|
||||
required
|
||||
/>
|
||||
<Explainer
|
||||
text="If you specify <span class='text-green-600 font-bold'>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>To modify the domain, you must first stop the application."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !staticDeployments.includes(application.buildPack)}
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="port">Port</label>
|
||||
<div class="col-span-2">
|
||||
<input
|
||||
readonly={!$session.isAdmin}
|
||||
name="port"
|
||||
id="port"
|
||||
bind:value={application.port}
|
||||
placeholder="default: 3000"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if !notNodeDeployments.includes(application.buildPack)}
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="installCommand">Install Command</label>
|
||||
<div class="col-span-2">
|
||||
<input
|
||||
readonly={!$session.isAdmin}
|
||||
name="installCommand"
|
||||
id="installCommand"
|
||||
bind:value={application.installCommand}
|
||||
placeholder="default: yarn install"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="buildCommand">Build Command</label>
|
||||
<div class="col-span-2">
|
||||
<input
|
||||
readonly={!$session.isAdmin}
|
||||
name="buildCommand"
|
||||
id="buildCommand"
|
||||
bind:value={application.buildCommand}
|
||||
placeholder="default: yarn build"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="startCommand" class="">Start Command</label>
|
||||
<div class="col-span-2">
|
||||
<input
|
||||
readonly={!$session.isAdmin}
|
||||
name="startCommand"
|
||||
id="startCommand"
|
||||
bind:value={application.startCommand}
|
||||
placeholder="default: yarn start"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid grid-cols-3">
|
||||
<label for="baseDirectory" class="pt-2">Base Directory</label>
|
||||
<div class="col-span-2">
|
||||
<input
|
||||
readonly={!$session.isAdmin}
|
||||
name="baseDirectory"
|
||||
id="baseDirectory"
|
||||
bind:value={application.baseDirectory}
|
||||
placeholder="default: /"
|
||||
/>
|
||||
<Explainer
|
||||
text="Directory to use as the base of all commands. <br> Could be useful with monorepos."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if !notNodeDeployments.includes(application.buildPack)}
|
||||
<div class="grid grid-cols-3">
|
||||
<label for="publishDirectory" class="pt-2">Publish Directory</label>
|
||||
<div class="col-span-2">
|
||||
<input
|
||||
readonly={!$session.isAdmin}
|
||||
name="publishDirectory"
|
||||
id="publishDirectory"
|
||||
bind:value={application.publishDirectory}
|
||||
placeholder=" default: /"
|
||||
/>
|
||||
<Explainer
|
||||
text="Directory containing all the assets for deployment. <br> For example: <span class='text-green-600 font-bold'>dist</span>,<span class='text-green-600 font-bold'>_site</span> or <span class='text-green-600 font-bold'>public</span>."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
<div class="flex space-x-1 pb-5 font-bold">
|
||||
<div class="title">Features</div>
|
||||
</div>
|
||||
<div class="px-4 pb-10 sm:px-6">
|
||||
<!-- <ul class="mt-2 divide-y divide-stone-800">
|
||||
<Setting
|
||||
bind:setting={forceSSL}
|
||||
on:click={() => changeSettings('forceSSL')}
|
||||
title="Force https"
|
||||
description="Creates a https redirect for all requests from http and also generates a https certificate for the domain through Let's Encrypt."
|
||||
/>
|
||||
</ul> -->
|
||||
<ul class="mt-2 divide-y divide-stone-800">
|
||||
<Setting
|
||||
bind:setting={previews}
|
||||
on:click={() => changeSettings('previews')}
|
||||
title="Enable MR/PR Previews"
|
||||
description="Creates previews from pull and merge requests."
|
||||
/>
|
||||
</ul>
|
||||
<ul class="mt-2 divide-y divide-stone-800">
|
||||
<Setting
|
||||
bind:setting={debug}
|
||||
on:click={() => changeSettings('debug')}
|
||||
title="Debug Logs"
|
||||
description="Enable debug logs during build phase. <br>(<span class='text-red-500'>sensitive information</span> could be visible in logs)"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
41
src/routes/applications/[id]/logs/_Loading.svelte
Normal file
41
src/routes/applications/[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>
|
||||
75
src/routes/applications/[id]/logs/build/_BuildLog.svelte
Normal file
75
src/routes/applications/[id]/logs/build/_BuildLog.svelte
Normal file
@@ -0,0 +1,75 @@
|
||||
<script lang="ts">
|
||||
export let buildId;
|
||||
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import { page } from '$app/stores';
|
||||
|
||||
import Loading from '$lib/components/Loading.svelte';
|
||||
import LoadingLogs from '../_Loading.svelte';
|
||||
import { get } from '$lib/api';
|
||||
import { errorNotification } from '$lib/form';
|
||||
|
||||
let logs = [];
|
||||
let loading = true;
|
||||
let currentStatus;
|
||||
let streamInterval;
|
||||
|
||||
const { id } = $page.params;
|
||||
|
||||
async function streamLogs(sequence = 0) {
|
||||
try {
|
||||
let { logs: responseLogs, status } = await get(
|
||||
`/applications/${id}/logs/build/build.json?buildId=${buildId}&sequence=${sequence}`
|
||||
);
|
||||
currentStatus = status;
|
||||
logs = logs.concat(responseLogs);
|
||||
loading = false;
|
||||
streamInterval = setInterval(async () => {
|
||||
if (status !== 'running') {
|
||||
clearInterval(streamInterval);
|
||||
return;
|
||||
}
|
||||
const nextSequence = logs[logs.length - 1].time;
|
||||
try {
|
||||
const data = await get(
|
||||
`/applications/${id}/logs/build/build.json?buildId=${buildId}&sequence=${nextSequence}`
|
||||
);
|
||||
status = data.status;
|
||||
currentStatus = status;
|
||||
logs = logs.concat(data.logs);
|
||||
dispatch('updateBuildStatus', { status });
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}, 1000);
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
onDestroy(() => {
|
||||
clearInterval(streamInterval);
|
||||
});
|
||||
onMount(async () => {
|
||||
window.scrollTo(0, 0);
|
||||
await streamLogs();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if loading}
|
||||
<Loading />
|
||||
{:else}
|
||||
<div class="relative">
|
||||
{#if currentStatus === 'running'}
|
||||
<LoadingLogs />
|
||||
{/if}
|
||||
<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"
|
||||
>
|
||||
{#each logs as log}
|
||||
<div>{log.line + '\n'}</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
28
src/routes/applications/[id]/logs/build/build.json.ts
Normal file
28
src/routes/applications/[id]/logs/build/build.json.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { getTeam, getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const buildId = event.url.searchParams.get('buildId');
|
||||
const sequence = Number(event.url.searchParams.get('sequence'));
|
||||
try {
|
||||
let logs = await db.prisma.buildLog.findMany({
|
||||
where: { buildId, time: { gt: sequence } },
|
||||
orderBy: { time: 'asc' }
|
||||
});
|
||||
const { status } = await db.prisma.build.findFirst({ where: { id: buildId } });
|
||||
|
||||
return {
|
||||
body: {
|
||||
logs,
|
||||
status
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
40
src/routes/applications/[id]/logs/build/index.json.ts
Normal file
40
src/routes/applications/[id]/logs/build/index.json.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import { dayjs } from '$lib/dayjs';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
const { id } = event.params;
|
||||
const buildId = event.url.searchParams.get('buildId');
|
||||
const skip = Number(event.url.searchParams.get('skip')) || 0;
|
||||
|
||||
let builds = [];
|
||||
try {
|
||||
const buildCount = await db.prisma.build.count({ where: { applicationId: id } });
|
||||
if (buildId) {
|
||||
builds = await db.prisma.build.findMany({ where: { applicationId: id, id: buildId } });
|
||||
} else {
|
||||
builds = await db.prisma.build.findMany({
|
||||
where: { applicationId: id },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 5,
|
||||
skip
|
||||
});
|
||||
}
|
||||
builds = builds.map((build) => {
|
||||
const updatedAt = dayjs(build.updatedAt).utc();
|
||||
build.took = updatedAt.diff(dayjs(build.createdAt)) / 1000;
|
||||
build.since = updatedAt.fromNow();
|
||||
return build;
|
||||
});
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
builds,
|
||||
buildCount
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
143
src/routes/applications/[id]/logs/build/index.svelte
Normal file
143
src/routes/applications/[id]/logs/build/index.svelte
Normal file
@@ -0,0 +1,143 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
let endpoint = `/applications/${params.id}/logs/build.json?skip=0`;
|
||||
const res = await fetch(endpoint);
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
application: stuff.application,
|
||||
...(await res.json())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { dateOptions, getDomain } from '$lib/components/common';
|
||||
|
||||
import BuildLog from './_BuildLog.svelte';
|
||||
import { get } from '$lib/api';
|
||||
import { errorNotification } from '$lib/form';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let builds;
|
||||
export let application;
|
||||
export let buildCount;
|
||||
|
||||
let buildId;
|
||||
$: buildId;
|
||||
|
||||
let skip = 0;
|
||||
let noMoreBuilds = buildCount < 5 || buildCount <= skip;
|
||||
const { id } = $page.params;
|
||||
let preselectedBuildId = $page.url.searchParams.get('buildId');
|
||||
if (preselectedBuildId) buildId = preselectedBuildId;
|
||||
|
||||
async function updateBuildStatus({ detail }) {
|
||||
const { status } = detail;
|
||||
if (status !== 'running') {
|
||||
try {
|
||||
const data = await get(`/applications/${id}/logs/build.json?buildId=${buildId}`);
|
||||
builds = builds.filter((build) => {
|
||||
if (build.id === data.builds[0].id) {
|
||||
build.status = data.builds[0].status;
|
||||
build.took = data.builds[0].took;
|
||||
build.since = data.builds[0].since;
|
||||
}
|
||||
window.location.reload();
|
||||
return build;
|
||||
});
|
||||
return;
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
} else {
|
||||
builds = builds.filter((build) => {
|
||||
if (build.id === buildId) build.status = status;
|
||||
return build;
|
||||
});
|
||||
}
|
||||
}
|
||||
async function loadMoreBuilds() {
|
||||
if (buildCount >= skip) {
|
||||
skip = skip + 5;
|
||||
noMoreBuilds = buildCount >= skip;
|
||||
try {
|
||||
const data = await get(`/applications/${id}/logs/build.json?skip=${skip}`);
|
||||
builds = builds.concat(data.builds);
|
||||
return;
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
} else {
|
||||
noMoreBuilds = true;
|
||||
}
|
||||
}
|
||||
async function loadBuild(build) {
|
||||
buildId = build;
|
||||
goto(`/applications/${id}/logs/build?buildId=${buildId}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">
|
||||
Build logs of <a href={application.fqdn} target="_blank">{getDomain(application.fqdn)}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row justify-start space-x-2 px-10 pt-6 ">
|
||||
<div class="min-w-[16rem] space-y-2">
|
||||
{#each builds as build (build.id)}
|
||||
<div
|
||||
data-tooltip={new Intl.DateTimeFormat('default', dateOptions).format(
|
||||
new Date(build.createdAt)
|
||||
) + `\n${build.status}`}
|
||||
on:click={() => loadBuild(build.id)}
|
||||
class="tooltip-top flex cursor-pointer items-center justify-center rounded-r border-l-2 border-transparent py-4 no-underline transition-all duration-100 hover:bg-coolgray-400 hover:shadow-xl"
|
||||
class:bg-coolgray-400={buildId === build.id}
|
||||
class:border-red-500={build.status === 'failed'}
|
||||
class:border-green-500={build.status === 'success'}
|
||||
class:border-yellow-500={build.status === 'inprogress'}
|
||||
>
|
||||
<div class="flex-col px-2">
|
||||
<div class="text-sm font-bold">
|
||||
{application.branch}
|
||||
</div>
|
||||
<div class="text-xs">
|
||||
{build.type}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1" />
|
||||
|
||||
<div class="w-48 text-center text-xs">
|
||||
{#if build.status === 'running'}
|
||||
<div class="font-bold">Running</div>
|
||||
{:else}
|
||||
<div>{build.since}</div>
|
||||
<div>Finished in <span class="font-bold">{build.took}s</span></div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{#if buildCount > 0 && !noMoreBuilds}
|
||||
<button class="w-full" on:click={loadMoreBuilds}>Load More</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="w-96 flex-1">
|
||||
{#if buildId}
|
||||
{#key buildId}
|
||||
<svelte:component this={BuildLog} {buildId} on:updateBuildStatus={updateBuildStatus} />
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if buildCount === 0}
|
||||
<div class="text-center text-xl font-bold">No logs found</div>
|
||||
{/if}
|
||||
53
src/routes/applications/[id]/logs/index.json.ts
Normal file
53
src/routes/applications/[id]/logs/index.json.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } 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;
|
||||
try {
|
||||
const { destinationDockerId, destinationDocker } = await db.prisma.application.findUnique({
|
||||
where: { id },
|
||||
include: { destinationDocker: true }
|
||||
});
|
||||
if (destinationDockerId) {
|
||||
const docker = dockerInstance({ destinationDocker });
|
||||
try {
|
||||
const container = await docker.engine.getContainer(id);
|
||||
if (container) {
|
||||
return {
|
||||
body: {
|
||||
logs: (await container.logs({ stdout: true, stderr: true, timestamps: true }))
|
||||
.toString()
|
||||
.split('\n')
|
||||
.map((l) => l.slice(8))
|
||||
.filter((a) => a)
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
const { statusCode } = error;
|
||||
if (statusCode === 404) {
|
||||
return {
|
||||
body: {
|
||||
logs: []
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
message: 'No logs found.'
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
75
src/routes/applications/[id]/logs/index.svelte
Normal file
75
src/routes/applications/[id]/logs/index.svelte
Normal file
@@ -0,0 +1,75 @@
|
||||
<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 = `/applications/${params.id}/logs.json`;
|
||||
const res = await fetch(endpoint);
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
application: stuff.application,
|
||||
...(await res.json())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let application;
|
||||
import { page } from '$app/stores';
|
||||
import LoadingLogs from './_Loading.svelte';
|
||||
import { getDomain } from '$lib/components/common';
|
||||
import { get } from '$lib/api';
|
||||
import { errorNotification } from '$lib/form';
|
||||
let loadLogsInterval = null;
|
||||
let logs = [];
|
||||
|
||||
const { id } = $page.params;
|
||||
|
||||
onMount(async () => {
|
||||
loadLogs();
|
||||
loadLogsInterval = setInterval(() => {
|
||||
loadLogs();
|
||||
}, 3000);
|
||||
});
|
||||
onDestroy(() => {
|
||||
clearInterval(loadLogsInterval);
|
||||
});
|
||||
async function loadLogs() {
|
||||
try {
|
||||
const newLogs = await get(`/applications/${id}/logs.json`);
|
||||
logs = newLogs.logs;
|
||||
return;
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">
|
||||
Application logs of <a href={application.fqdn} target="_blank">{getDomain(application.fqdn)}</a>
|
||||
</div>
|
||||
</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">Waiting for the logs...</div>
|
||||
{:else}
|
||||
<div class="relative w-full">
|
||||
<LoadingLogs />
|
||||
<div
|
||||
class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 p-6 whitespace-pre-wrap break-words w-full"
|
||||
>
|
||||
{#each logs as log}
|
||||
{log + '\n'}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
44
src/routes/applications/[id]/previews/index.json.ts
Normal file
44
src/routes/applications/[id]/previews/index.json.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { getTeam, getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import { dockerInstance } from '$lib/docker';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import jsonwebtoken from 'jsonwebtoken';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
const { status, body, teamId } = await getUserDetails(event, false);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
try {
|
||||
const destinationDocker = await db.getDestinationByApplicationId({ id, teamId });
|
||||
const docker = dockerInstance({ destinationDocker });
|
||||
const listContainers = await docker.engine.listContainers({
|
||||
filters: { network: [destinationDocker.network] }
|
||||
});
|
||||
const containers = listContainers.filter((container) => {
|
||||
return (
|
||||
container.Labels['coolify.configuration'] &&
|
||||
container.Labels['coolify.type'] === 'standalone-application'
|
||||
);
|
||||
});
|
||||
const jsonContainers = containers
|
||||
.map((container) =>
|
||||
JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString())
|
||||
)
|
||||
.filter((container) => {
|
||||
return (
|
||||
container.type !== 'manual' &&
|
||||
container.type !== 'webhook_commit' &&
|
||||
container.applicationId === id
|
||||
);
|
||||
});
|
||||
return {
|
||||
body: {
|
||||
containers: jsonContainers
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
51
src/routes/applications/[id]/previews/index.svelte
Normal file
51
src/routes/applications/[id]/previews/index.svelte
Normal file
@@ -0,0 +1,51 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, stuff }) => {
|
||||
let endpoint = `/applications/${params.id}/previews.json`;
|
||||
const res = await fetch(endpoint);
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
application: stuff.application,
|
||||
...(await res.json())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
error: new Error(`Could not load ${endpoint}`)
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let containers;
|
||||
export let application;
|
||||
|
||||
import { getDomain } from '$lib/components/common';
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">
|
||||
Previews for <a href={application.fqdn} target="_blank">{getDomain(application.fqdn)}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-4xl px-6">
|
||||
<div class="flex flex-wrap justify-center space-x-2">
|
||||
{#if containers.length > 0}
|
||||
{#each containers as container}
|
||||
<a href={container.fqdn} class="p-2 no-underline" target="_blank">
|
||||
<div class="box-selection text-center hover:border-transparent hover:bg-coolgray-200">
|
||||
<div class="truncate text-center text-xl font-bold">{getDomain(container.fqdn)}</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="flex-col">
|
||||
<div class="text-center font-bold text-xl">No previews available</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
133
src/routes/applications/[id]/secrets/_Secret.svelte
Normal file
133
src/routes/applications/[id]/secrets/_Secret.svelte
Normal file
@@ -0,0 +1,133 @@
|
||||
<script>
|
||||
export let name = '';
|
||||
export let value = '';
|
||||
export let isBuildSecret = false;
|
||||
export let isNewSecret = false;
|
||||
import { page } from '$app/stores';
|
||||
import { del, post } from '$lib/api';
|
||||
import { errorNotification } from '$lib/form';
|
||||
|
||||
if (name) value = 'ENCRYPTED';
|
||||
const { id } = $page.params;
|
||||
|
||||
async function removeSecret() {
|
||||
try {
|
||||
await del(`/applications/${id}/secrets.json`, { name });
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function saveSecret() {
|
||||
try {
|
||||
await post(`/applications/${id}/secrets.json`, { name, value, isBuildSecret });
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
function setSecretValue() {
|
||||
if (isNewSecret) {
|
||||
isBuildSecret = !isBuildSecret;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mx-auto max-w-3xl pt-4">
|
||||
<div class="flex space-x-2">
|
||||
<div class="grid grid-flow-row">
|
||||
<label for="secretName">Name</label>
|
||||
<input
|
||||
id="secretName"
|
||||
bind:value={name}
|
||||
placeholder="EXAMPLE_VARIABLE"
|
||||
class="w-64 border-2 border-transparent"
|
||||
readonly={!isNewSecret}
|
||||
class:hover:bg-coolgray-200={!isNewSecret}
|
||||
class:cursor-not-allowed={!isNewSecret}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-flow-row">
|
||||
<label for="secretValue">Value (will be encrypted)</label>
|
||||
<input
|
||||
id="secretValue"
|
||||
bind:value
|
||||
placeholder="J$#@UIO%HO#$U%H"
|
||||
class="w-64 border-2 border-transparent"
|
||||
class:hover:bg-coolgray-200={!isNewSecret}
|
||||
class:cursor-not-allowed={!isNewSecret}
|
||||
readonly={!isNewSecret}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-32 px-2 text-center">
|
||||
<div class="text-xs">Is build variable?</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<ul class="divide-y divide-stone-800">
|
||||
<li>
|
||||
<div
|
||||
type="button"
|
||||
on:click={setSecretValue}
|
||||
aria-pressed="false"
|
||||
class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out"
|
||||
class:bg-green-600={isBuildSecret}
|
||||
class:bg-stone-700={!isBuildSecret}
|
||||
class:cursor-not-allowed={!isNewSecret}
|
||||
class:cursor-pointer={isNewSecret}
|
||||
>
|
||||
<span class="sr-only">Use isBuildSecret</span>
|
||||
<span
|
||||
class="pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow transition duration-200 ease-in-out"
|
||||
class:translate-x-5={isBuildSecret}
|
||||
class:translate-x-0={!isBuildSecret}
|
||||
>
|
||||
<span
|
||||
class=" absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
|
||||
class:opacity-0={isBuildSecret}
|
||||
class:opacity-100={!isBuildSecret}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg class="h-3 w-3 bg-white text-red-600" fill="none" viewBox="0 0 12 12">
|
||||
<path
|
||||
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-100 ease-out"
|
||||
aria-hidden="true"
|
||||
class:opacity-100={isBuildSecret}
|
||||
class:opacity-0={!isBuildSecret}
|
||||
>
|
||||
<svg
|
||||
class="h-3 w-3 bg-white text-green-600"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 12 12"
|
||||
>
|
||||
<path
|
||||
d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{#if isNewSecret}
|
||||
<div class="mt-6">
|
||||
<button class="w-20 bg-green-600 hover:bg-green-500" on:click={saveSecret}>Add</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mt-6">
|
||||
<button class="w-20 bg-red-600 hover:bg-red-500" on:click={removeSecret}>Remove</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
63
src/routes/applications/[id]/secrets/index.json.ts
Normal file
63
src/routes/applications/[id]/secrets/index.json.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { getTeam, getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
try {
|
||||
const secrets = await db.listSecrets({ applicationId: event.params.id });
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
secrets: secrets.sort((a, b) => {
|
||||
return ('' + a.name).localeCompare(b.name);
|
||||
})
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
const { name, value, isBuildSecret } = await event.request.json();
|
||||
|
||||
try {
|
||||
const found = await db.isSecretExists({ id, name });
|
||||
if (found) {
|
||||
throw {
|
||||
error: `Secret ${name} already exists`
|
||||
};
|
||||
} else {
|
||||
await db.createSecret({ id, name, value, isBuildSecret });
|
||||
return {
|
||||
status: 201
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
export const del: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
const { name } = await event.request.json();
|
||||
|
||||
try {
|
||||
await db.removeSecret({ id, name });
|
||||
return {
|
||||
status: 200
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
41
src/routes/applications/[id]/secrets/index.svelte
Normal file
41
src/routes/applications/[id]/secrets/index.svelte
Normal file
@@ -0,0 +1,41 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, stuff }) => {
|
||||
let endpoint = `/applications/${params.id}/secrets.json`;
|
||||
const res = await fetch(endpoint);
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
application: stuff.application,
|
||||
...(await res.json())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
error: new Error(`Could not load ${endpoint}`)
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let secrets;
|
||||
export let application;
|
||||
import Secret from './_Secret.svelte';
|
||||
import { getDomain } from '$lib/components/common';
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">
|
||||
Secrets for <a href={application.fqdn} target="_blank">{getDomain(application.fqdn)}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-auto max-w-4xl px-6">
|
||||
<div class="flex-col justify-start space-y-1">
|
||||
{#each secrets as secret}
|
||||
<Secret name={secret.name} value={secret.value} isBuildSecret={secret.isBuildSecret} />
|
||||
{/each}
|
||||
<Secret isNewSecret />
|
||||
</div>
|
||||
</div>
|
||||
19
src/routes/applications/[id]/settings.json.ts
Normal file
19
src/routes/applications/[id]/settings.json.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
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;
|
||||
const { debug, previews } = await event.request.json();
|
||||
|
||||
try {
|
||||
await db.setApplicationSettings({ id, debug, previews });
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
31
src/routes/applications/[id]/stop.json.ts
Normal file
31
src/routes/applications/[id]/stop.json.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { getDomain, getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import { dockerInstance } from '$lib/docker';
|
||||
import { removeProxyConfiguration } 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 { destinationDocker, destinationDockerId, fqdn } = await db.getApplication({
|
||||
id,
|
||||
teamId
|
||||
});
|
||||
const domain = getDomain(fqdn);
|
||||
if (destinationDockerId) {
|
||||
const docker = dockerInstance({ destinationDocker });
|
||||
await docker.engine.getContainer(id).stop();
|
||||
}
|
||||
await removeProxyConfiguration({ domain });
|
||||
return {
|
||||
status: 200
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
60
src/routes/applications/_Application.svelte
Normal file
60
src/routes/applications/_Application.svelte
Normal file
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
export let application;
|
||||
import Rust from '$lib/components/svg/applications/Rust.svelte';
|
||||
import Nodejs from '$lib/components/svg/applications/Nodejs.svelte';
|
||||
import React from '$lib/components/svg/applications/React.svelte';
|
||||
import Svelte from '$lib/components/svg/applications/Svelte.svelte';
|
||||
import Vuejs from '$lib/components/svg/applications/Vuejs.svelte';
|
||||
import PHP from '$lib/components/svg/applications/PHP.svelte';
|
||||
import Python from '$lib/components/svg/applications/Python.svelte';
|
||||
import Static from '$lib/components/svg/applications/Static.svelte';
|
||||
import Nestjs from '$lib/components/svg/applications/Nestjs.svelte';
|
||||
import Nuxtjs from '$lib/components/svg/applications/Nuxtjs.svelte';
|
||||
import Nextjs from '$lib/components/svg/applications/Nextjs.svelte';
|
||||
import Gatsby from '$lib/components/svg/applications/Gatsby.svelte';
|
||||
import Docker from '$lib/components/svg/applications/Docker.svelte';
|
||||
|
||||
const buildPack = application?.buildPack?.toLowerCase();
|
||||
</script>
|
||||
|
||||
<a href="/applications/{application.id}" class="w-96 p-2 no-underline">
|
||||
<div class="box-selection group relative hover:bg-green-600">
|
||||
{#if buildPack === 'rust'}
|
||||
<Rust />
|
||||
{:else if buildPack === 'node'}
|
||||
<Nodejs />
|
||||
{:else if buildPack === 'react'}
|
||||
<React />
|
||||
{:else if buildPack === 'svelte'}
|
||||
<Svelte />
|
||||
{:else if buildPack === 'vuejs'}
|
||||
<Vuejs />
|
||||
{:else if buildPack === 'php'}
|
||||
<PHP />
|
||||
{:else if buildPack === 'python'}
|
||||
<Python />
|
||||
{:else if buildPack === 'static'}
|
||||
<Static />
|
||||
{:else if buildPack === 'nestjs'}
|
||||
<Nestjs />
|
||||
{:else if buildPack === 'nuxtjs'}
|
||||
<Nuxtjs />
|
||||
{:else if buildPack === 'nextjs'}
|
||||
<Nextjs />
|
||||
{:else if buildPack === 'gatsby'}
|
||||
<Gatsby />
|
||||
{:else if buildPack === 'docker'}
|
||||
<Docker />
|
||||
{/if}
|
||||
|
||||
<div class="truncate text-center text-xl font-bold">{application.name}</div>
|
||||
{#if application.fqdn}
|
||||
<div class="truncate text-center">{application.fqdn}</div>
|
||||
{/if}
|
||||
{#if !application.gitSourceId || !application.destinationDockerId}
|
||||
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
|
||||
Configuration missing
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
21
src/routes/applications/index.json.ts
Normal file
21
src/routes/applications/index.json.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
try {
|
||||
const applications = await db.listApplications(teamId);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
applications
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
58
src/routes/applications/index.svelte
Normal file
58
src/routes/applications/index.svelte
Normal file
@@ -0,0 +1,58 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch }) => {
|
||||
const endpoint = '/applications.json';
|
||||
const res = await fetch(endpoint);
|
||||
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
...(await res.json())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
error: new Error(`Could not load ${endpoint}`)
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let applications: Array<Applications>;
|
||||
import { session } from '$app/stores';
|
||||
import Application from './_Application.svelte';
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl ">Applications</div>
|
||||
{#if $session.isAdmin}
|
||||
<a href="/new/application" class="add-icon bg-green-600 hover:bg-green-500">
|
||||
<svg
|
||||
class="w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||
/></svg
|
||||
>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#if !applications || applications.length === 0}
|
||||
<div class="flex-col">
|
||||
<div class="text-center text-xl font-bold">No applications found</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#each applications as application}
|
||||
<Application {application} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
10
src/routes/common/getUniqueName.json.ts
Normal file
10
src/routes/common/getUniqueName.json.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { uniqueName } from '$lib/common';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async () => {
|
||||
return {
|
||||
body: {
|
||||
name: uniqueName()
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
<script context="module" lang="ts">
|
||||
import { request } from '$lib/request';
|
||||
/**
|
||||
* @type {import('@sveltejs/kit').Load}
|
||||
*/
|
||||
export async function load(session) {
|
||||
return {
|
||||
props: {
|
||||
initDashboard: await request('/api/v1/dashboard', session)
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let initDashboard;
|
||||
import { dashboard } from '$store';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { session } from '$app/stores';
|
||||
$dashboard = initDashboard;
|
||||
let loadDashboardInterval = null;
|
||||
|
||||
async function loadDashboard() {
|
||||
try {
|
||||
$dashboard = await request('/api/v1/dashboard', $session);
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await loadDashboard();
|
||||
loadDashboardInterval = setInterval(async () => {
|
||||
await loadDashboard();
|
||||
}, 2000);
|
||||
});
|
||||
onDestroy(() => {
|
||||
clearInterval(loadDashboardInterval);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="min-h-full">
|
||||
<slot />
|
||||
</div>
|
||||
File diff suppressed because one or more lines are too long
@@ -1,100 +0,0 @@
|
||||
<script context="module" lang="ts">
|
||||
/**
|
||||
* @type {import('@sveltejs/kit').Load}
|
||||
*/
|
||||
export async function load(session) {
|
||||
if (!browser && !process.env.VITE_GITHUB_APP_CLIENTID) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/dashboard/services'
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
import MongoDb from '$components/Database/SVGs/MongoDb.svelte';
|
||||
import Postgresql from '$components/Database/SVGs/Postgresql.svelte';
|
||||
import Clickhouse from '$components/Database/SVGs/Clickhouse.svelte';
|
||||
import CouchDb from '$components/Database/SVGs/CouchDb.svelte';
|
||||
import Mysql from '$components/Database/SVGs/Mysql.svelte';
|
||||
import { dashboard } from '$store';
|
||||
import { fade } from 'svelte/transition';
|
||||
import Redis from '$components/Database/SVGs/Redis.svelte';
|
||||
import { browser } from '$app/env';
|
||||
</script>
|
||||
|
||||
<div class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center">
|
||||
<div in:fade={{ duration: 100 }}>Databases</div>
|
||||
<button
|
||||
class="icon p-1 ml-4 bg-purple-500 hover:bg-purple-400"
|
||||
on:click={() => goto('/database/new')}
|
||||
>
|
||||
<svg
|
||||
class="w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div in:fade={{ duration: 100 }}>
|
||||
{#if $dashboard.databases?.deployed.length > 0}
|
||||
<div class="px-4 mx-auto py-5">
|
||||
<div class="flex items-center justify-center flex-wrap">
|
||||
{#each $dashboard.databases.deployed as database}
|
||||
<div
|
||||
in:fade={{ duration: 200 }}
|
||||
class="px-4 pb-4"
|
||||
on:click={() =>
|
||||
goto(`/database/${database.configuration.general.deployId}/configuration`)}
|
||||
>
|
||||
<div
|
||||
class="relative rounded-xl p-6 bg-warmGray-800 border-2 border-dashed border-transparent hover:border-purple-500 text-white shadow-md cursor-pointer ease-in-out hover:scale-105 duration-100 group"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
{#if database.configuration.general.type == 'mongodb'}
|
||||
<MongoDb customClass="w-10 h-10 absolute top-0 left-0 -m-4" />
|
||||
{:else if database.configuration.general.type == 'postgresql'}
|
||||
<Postgresql customClass="w-10 h-10 absolute top-0 left-0 -m-4" />
|
||||
{:else if database.configuration.general.type == 'mysql'}
|
||||
<Mysql customClass="w-10 h-10 absolute top-0 left-0 -m-4" />
|
||||
{:else if database.configuration.general.type == 'couchdb'}
|
||||
<CouchDb
|
||||
customClass="w-10 h-10 fill-current text-red-600 absolute top-0 left-0 -m-4"
|
||||
/>
|
||||
{:else if database.configuration.general.type == 'redis'}
|
||||
<Redis customClass="w-10 h-10 absolute top-0 left-0 -m-4" />
|
||||
{:else if database.configuration.general.type == 'clickhouse'}
|
||||
<Clickhouse
|
||||
customClass="w-10 h-10 fill-current text-red-600 absolute top-0 left-0 -m-4"
|
||||
/>
|
||||
{/if}
|
||||
<div class="text-center w-full">
|
||||
<div class="text-base font-bold text-white group-hover:text-white">
|
||||
{database.configuration.general.nickname}
|
||||
</div>
|
||||
<div class="text-xs font-bold text-warmGray-300 ">
|
||||
({database.configuration.general.type})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-2xl font-bold text-center">No databases found</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,124 +0,0 @@
|
||||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
import { dashboard } from '$store';
|
||||
import { fade } from 'svelte/transition';
|
||||
async function openConfiguration(service) {
|
||||
if (service.serviceName === 'wordpress') {
|
||||
goto(`/service/${service.configuration.deployId}/configuration`);
|
||||
} else {
|
||||
goto(`/service/${service.serviceName}/configuration`);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div
|
||||
in:fade={{ duration: 100 }}
|
||||
class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center"
|
||||
>
|
||||
<div>Services</div>
|
||||
<button class="icon p-1 ml-4 bg-blue-500 hover:bg-blue-400" on:click={() => goto('/service/new')}>
|
||||
<svg
|
||||
class="w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div in:fade={{ duration: 100 }}>
|
||||
{#if $dashboard?.services?.deployed.length > 0}
|
||||
<div class="px-4 mx-auto py-5">
|
||||
<div class="flex items-center justify-center flex-wrap">
|
||||
{#each $dashboard?.services?.deployed as service}
|
||||
<div
|
||||
in:fade={{ duration: 200 }}
|
||||
class="px-4 pb-4"
|
||||
on:click={() => openConfiguration(service)}
|
||||
>
|
||||
<div
|
||||
class="relative rounded-xl p-6 bg-warmGray-800 border-2 border-dashed border-transparent hover:border-blue-500 text-white shadow-md cursor-pointer ease-in-out hover:scale-105 duration-100 group"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
{#if service.serviceName == 'plausible'}
|
||||
<div>
|
||||
<img
|
||||
alt="plausible logo"
|
||||
class="w-10 absolute top-0 left-0 -m-6"
|
||||
src="https://cdn.coollabs.io/assets/coolify/services/plausible/logo_sm.png"
|
||||
/>
|
||||
<div class="text-white font-bold">Plausible Analytics</div>
|
||||
</div>
|
||||
{:else if service.serviceName == 'nocodb'}
|
||||
<div>
|
||||
<img
|
||||
alt="nocodedb"
|
||||
class="w-10 absolute top-0 left-0 -m-6"
|
||||
src="https://cdn.coollabs.io/assets/coolify/services/nocodb/nocodb.png"
|
||||
/>
|
||||
<div class="text-white font-bold">NocoDB</div>
|
||||
</div>
|
||||
{:else if service.serviceName == 'code-server'}
|
||||
<div>
|
||||
<svg class="w-10 absolute top-0 left-0 -m-6" viewBox="0 0 128 128">
|
||||
<path
|
||||
d="M3.656 45.043s-3.027-2.191.61-5.113l8.468-7.594s2.426-2.559 4.989-.328l78.175 59.328v28.45s-.039 4.468-5.757 3.976zm0 0"
|
||||
fill="#2489ca"
|
||||
/><path
|
||||
d="M23.809 63.379L3.656 81.742s-2.07 1.543 0 4.305l9.356 8.527s2.222 2.395 5.508-.328l21.359-16.238zm0 0"
|
||||
fill="#1070b3"
|
||||
/><path
|
||||
d="M59.184 63.531l36.953-28.285-.239-28.297S94.32.773 89.055 3.99L39.879 48.851zm0 0"
|
||||
fill="#0877b9"
|
||||
/><path
|
||||
d="M90.14 123.797c2.145 2.203 4.747 1.48 4.747 1.48l28.797-14.222c3.687-2.52 3.171-5.645 3.171-5.645V20.465c0-3.735-3.812-5.024-3.812-5.024L98.082 3.38c-5.453-3.379-9.027.61-9.027.61s4.593-3.317 6.843 2.96v112.317c0 .773-.164 1.53-.492 2.214-.656 1.332-2.086 2.57-5.504 2.051zm0 0"
|
||||
fill="#3c99d4"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div class="text-white font-bold">VSCode Server</div>
|
||||
</div>
|
||||
{:else if service.serviceName == 'minio'}
|
||||
<div>
|
||||
<img
|
||||
alt="minio"
|
||||
class="w-7 absolute top-0 left-0 -my-7 -mx-3"
|
||||
src="https://cdn.coollabs.io/assets/coolify/services/minio/MINIO_Bird.png"
|
||||
/>
|
||||
|
||||
<div class="text-white font-bold">MinIO</div>
|
||||
</div>
|
||||
{:else if service.serviceName.match(/wp-/)}
|
||||
<svg class="w-10 absolute top-0 left-0 -m-6" viewBox="0 0 128 128">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fill="white"
|
||||
d="M64.094 126.224c34.275-.052 62.021-27.933 62.021-62.325 0-33.833-27.618-61.697-60.613-62.286C30.85.995 1.894 29.113 1.885 63.21c-.01 35.079 27.612 63.064 62.209 63.014zM63.993 4.63c32.907-.011 59.126 26.725 59.116 60.28-.011 31.679-26.925 58.18-59.092 58.187-32.771.007-59.125-26.563-59.124-59.608.002-32.193 26.766-58.848 59.1-58.859zM39.157 35.896c.538 1.793-.968 2.417-2.569 2.542-1.685.13-3.369.257-5.325.406 6.456 19.234 12.815 38.183 19.325 57.573.464-.759.655-.973.739-1.223 3.574-10.682 7.168-21.357 10.651-32.069.318-.977.16-2.271-.188-3.275-1.843-5.32-4.051-10.524-5.667-15.908-1.105-3.686-2.571-6.071-6.928-5.644-.742.073-1.648-1.524-2.479-2.349 1.005-.6 2.003-1.704 3.017-1.719a849.593 849.593 0 0126.618.008c1.018.017 2.016 1.15 3.021 1.765-.88.804-1.639 2.01-2.668 2.321-1.651.498-3.482.404-5.458.58l19.349 57.56c2.931-9.736 5.658-18.676 8.31-27.639 2.366-8.001.956-15.473-3.322-22.52-1.286-2.119-2.866-4.175-3.595-6.486-.828-2.629-1.516-5.622-1.077-8.259.745-4.469 4.174-6.688 8.814-7.113C74.333.881 34.431 9.317 19.728 34.922c5.66-.261 11.064-.604 16.472-.678 1.022-.013 2.717.851 2.957 1.652zm10.117 77.971c-.118.345-.125.729-.218 1.302 10.943 3.034 21.675 2.815 32.659-.886l-16.78-45.96c-5.37 15.611-10.52 30.575-15.661 45.544zm-8.456-2.078l-25.281-69.35c-11.405 22.278-2.729 56.268 25.281 69.35zm76.428-44.562c.802-10.534-2.832-25.119-5.97-27.125-.35 3.875-.106 8.186-1.218 12.114-2.617 9.255-5.817 18.349-8.899 27.468-3.35 9.912-6.832 19.779-10.257 29.666 16.092-9.539 24.935-23.618 26.344-42.123z"
|
||||
/>
|
||||
</svg>
|
||||
<div class="text-white font-bold text-center">
|
||||
Wordpress<span
|
||||
class="flex text-xs items-center justify-center text-warmGray-300 group-hover:text-white"
|
||||
>({service.configuration.baseURL.replace('https://', '')})</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-2xl font-bold text-center">No services found</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,123 +0,0 @@
|
||||
<script>
|
||||
import { database } from '$store';
|
||||
import { page, session } from '$app/stores';
|
||||
import { request } from '$lib/request';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { goto } from '$app/navigation';
|
||||
import MongoDb from '$components/Database/SVGs/MongoDb.svelte';
|
||||
import Postgresql from '$components/Database/SVGs/Postgresql.svelte';
|
||||
import Mysql from '$components/Database/SVGs/Mysql.svelte';
|
||||
import CouchDb from '$components/Database/SVGs/CouchDb.svelte';
|
||||
import Loading from '$components/Loading.svelte';
|
||||
import PasswordField from '$components/PasswordField.svelte';
|
||||
import { browser } from '$app/env';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import Redis from '$components/Database/SVGs/Redis.svelte';
|
||||
|
||||
async function backup() {
|
||||
try {
|
||||
await request(`/api/v1/databases/${$page.params.name}/backup`, $session, { body: {} });
|
||||
|
||||
browser && toast.push(`Successfully created backup.`);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
if (error.code === 501) {
|
||||
browser && toast.push(error.error);
|
||||
} else {
|
||||
browser && toast.push(`Error occured during database backup!`);
|
||||
}
|
||||
}
|
||||
}
|
||||
async function loadDatabaseConfig() {
|
||||
if ($page.params.name) {
|
||||
try {
|
||||
$database = await request(`/api/v1/databases/${$page.params.name}`, $session);
|
||||
} catch (error) {
|
||||
browser && goto(`/dashboard/databases`, { replaceState: true });
|
||||
}
|
||||
} else {
|
||||
browser && goto(`/dashboard/databases`, { replaceState: true });
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
{#await loadDatabaseConfig()}
|
||||
<Loading />
|
||||
{:then}
|
||||
<div class="min-h-full text-white">
|
||||
<div class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center">
|
||||
<div>{$database.config.general.nickname}</div>
|
||||
<div class="px-4">
|
||||
{#if $database.config.general.type === 'mongodb'}
|
||||
<MongoDb customClass="w-8 h-8" />
|
||||
{:else if $database.config.general.type === 'postgresql'}
|
||||
<Postgresql customClass="w-8 h-8" />
|
||||
{:else if $database.config.general.type === 'mysql'}
|
||||
<Mysql customClass="w-8 h-8" />
|
||||
{:else if $database.config.general.type === 'couchdb'}
|
||||
<CouchDb customClass="w-8 h-8 fill-current text-red-600" />
|
||||
{:else if $database.config.general.type === 'redis'}
|
||||
<Redis customClass="w-8 h-8" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-left max-w-6xl mx-auto px-6" in:fade={{ duration: 100 }}>
|
||||
<div class="pb-2 pt-5 space-y-4">
|
||||
<div class="text-2xl font-bold border-gradient w-32">Database</div>
|
||||
<div class="flex items-center pt-4">
|
||||
<div class="font-bold w-64 text-warmGray-400">Connection string</div>
|
||||
{#if $database.config.general.type === 'mongodb'}
|
||||
<PasswordField
|
||||
value={`mongodb://${$database.envs.MONGODB_USERNAME}:${$database.envs.MONGODB_PASSWORD}@${$database.config.general.deployId}:27017/${$database.envs.MONGODB_DATABASE}`}
|
||||
/>
|
||||
{:else if $database.config.general.type === 'postgresql'}
|
||||
<PasswordField
|
||||
value={`postgresql://${$database.envs.POSTGRESQL_USERNAME}:${$database.envs.POSTGRESQL_PASSWORD}@${$database.config.general.deployId}:5432/${$database.envs.POSTGRESQL_DATABASE}`}
|
||||
/>
|
||||
{:else if $database.config.general.type === 'mysql'}
|
||||
<PasswordField
|
||||
value={`mysql://${$database.envs.MYSQL_USER}:${$database.envs.MYSQL_PASSWORD}@${$database.config.general.deployId}:3306/${$database.envs.MYSQL_DATABASE}`}
|
||||
/>
|
||||
{:else if $database.config.general.type === 'couchdb'}
|
||||
<PasswordField
|
||||
value={`http://${$database.envs.COUCHDB_USER}:${$database.envs.COUCHDB_PASSWORD}@${$database.config.general.deployId}:5984`}
|
||||
/>
|
||||
{:else if $database.config.general.type === 'redis'}
|
||||
<PasswordField
|
||||
value={`redis://${$database.envs.REDIS_PASSWORD}@${$database.config.general.deployId}:6379`}
|
||||
/>
|
||||
{:else if $database.config.general.type === 'clickhouse'}
|
||||
<!-- {JSON.stringify($database)} -->
|
||||
<!-- <textarea
|
||||
disabled
|
||||
class="w-full"
|
||||
value="{`postgresql://${$database.envs.POSTGRESQL_USERNAME}:${$database.envs.POSTGRESQL_PASSWORD}@${$database.config.general.deployId}:5432/${$database.envs.POSTGRESQL_DATABASE}`}"
|
||||
></textarea> -->
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if $database.config.general.type === 'mongodb'}
|
||||
<div class="flex items-center">
|
||||
<div class="font-bold w-64 text-warmGray-400">Root password</div>
|
||||
<PasswordField value={$database.envs.MONGODB_ROOT_PASSWORD} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $database.config.general.type === 'redis'}
|
||||
<div class="flex items-center">
|
||||
<div class="font-bold w-64 text-warmGray-400">Redis password</div>
|
||||
<PasswordField value={$database.envs.REDIS_PASSWORD} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="pb-2 pt-5 space-y-4">
|
||||
<div class="text-2xl font-bold border-gradient w-32">Backup</div>
|
||||
<div class="pt-4">
|
||||
<button
|
||||
class="button hover:bg-warmGray-700 bg-warmGray-800 rounded p-2 font-bold "
|
||||
on:click={backup}>Download database backup</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/await}
|
||||
@@ -1,78 +0,0 @@
|
||||
<script>
|
||||
import { browser } from '$app/env';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import { page, session } from '$app/stores';
|
||||
import Tooltip from '$components/Tooltip.svelte';
|
||||
import { request } from '$lib/request';
|
||||
import { database, initialDatabase } from '$store';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
onDestroy(() => {
|
||||
$database = JSON.parse(JSON.stringify(initialDatabase));
|
||||
});
|
||||
|
||||
async function removeDB() {
|
||||
await request(`/api/v1/databases/${$page.params.name}`, $session, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (browser) {
|
||||
toast.push('Database removed.');
|
||||
goto(`/dashboard/databases`, { replaceState: true });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $page.path !== '/database/new'}
|
||||
<nav class="flex text-white justify-end items-center m-4 fixed right-0 top-0 space-x-4">
|
||||
<Tooltip position="bottom" label="Delete">
|
||||
<button title="Delete" class="icon hover:text-red-500" on:click={removeDB}>
|
||||
<svg
|
||||
class="w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<div class="border border-warmGray-700 h-8" />
|
||||
<Tooltip position="bottom-left" label="Configuration">
|
||||
<button
|
||||
class="icon hover:text-yellow-400"
|
||||
disabled={$page.path === '/database/new'}
|
||||
class:text-yellow-400={$page.path.endsWith('configuration') ||
|
||||
$page.path === '/database/new'}
|
||||
class:bg-warmGray-700={$page.path.endsWith('configuration') ||
|
||||
$page.path === '/database/new'}
|
||||
on:click={() => goto(`/database/${$page.params.name}/configuration`)}
|
||||
>
|
||||
<svg
|
||||
class="w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</nav>
|
||||
{/if}
|
||||
<div class="text-white">
|
||||
<slot />
|
||||
</div>
|
||||
@@ -1,11 +0,0 @@
|
||||
<script>
|
||||
import Configuration from '$components/Database/Configuration.svelte';
|
||||
</script>
|
||||
|
||||
<div class="min-h-full text-white">
|
||||
<div class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center">
|
||||
Select a database
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Configuration />
|
||||
78
src/routes/databases/[id]/_Databases/_CouchDb.svelte
Normal file
78
src/routes/databases/[id]/_Databases/_CouchDb.svelte
Normal file
@@ -0,0 +1,78 @@
|
||||
<script>
|
||||
export let database;
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">CouchDB</div>
|
||||
</div>
|
||||
<div class="px-10">
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="defaultDatabase">Default Database</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
required
|
||||
readonly={database.defaultDatabase}
|
||||
disabled={database.defaultDatabase}
|
||||
placeholder="eg: mydb"
|
||||
id="defaultDatabase"
|
||||
name="defaultDatabase"
|
||||
bind:value={database.defaultDatabase}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="dbUser">User</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
readonly
|
||||
disabled
|
||||
placeholder="Generated automatically after start"
|
||||
id="dbUser"
|
||||
name="dbUser"
|
||||
value={database.dbUser}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="dbUserPassword">Password</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
readonly
|
||||
disabled
|
||||
placeholder="Generated automatically after start"
|
||||
isPasswordField
|
||||
id="dbUserPassword"
|
||||
name="dbUserPassword"
|
||||
value={database.dbUserPassword}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="rootUser">Root User</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
readonly
|
||||
disabled
|
||||
placeholder="Generated automatically after start"
|
||||
id="rootUser"
|
||||
name="rootUser"
|
||||
value={database.rootUser}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="rootUserPassword">Root's Password</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
readonly
|
||||
disabled
|
||||
placeholder="Generated automatically after start"
|
||||
isPasswordField
|
||||
id="rootUserPassword"
|
||||
name="rootUserPassword"
|
||||
value={database.rootUserPassword}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
209
src/routes/databases/[id]/_Databases/_Databases.svelte
Normal file
209
src/routes/databases/[id]/_Databases/_Databases.svelte
Normal file
@@ -0,0 +1,209 @@
|
||||
<script lang="ts">
|
||||
export let database;
|
||||
export let privatePort;
|
||||
export let settings;
|
||||
import { page, session } from '$app/stores';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Setting from '$lib/components/Setting.svelte';
|
||||
import { errorNotification } from '$lib/form';
|
||||
|
||||
import MySql from './_MySQL.svelte';
|
||||
import MongoDb from './_MongoDB.svelte';
|
||||
import PostgreSql from './_PostgreSQL.svelte';
|
||||
import Redis from './_Redis.svelte';
|
||||
import CouchDb from './_CouchDb.svelte';
|
||||
import { browser } from '$app/env';
|
||||
import { post } from '$lib/api';
|
||||
import { getDomain } from '$lib/components/common';
|
||||
|
||||
const { id } = $page.params;
|
||||
let loading = false;
|
||||
let isPublic = database.settings.isPublic || false;
|
||||
let appendOnly = database.settings.appendOnly;
|
||||
|
||||
let databaseDefault = database.defaultDatabase;
|
||||
let databaseDbUser = database.dbUser;
|
||||
let databaseDbUserPassword = database.dbUserPassword;
|
||||
if (database.type === 'mongodb') {
|
||||
databaseDefault = '?readPreference=primary&ssl=false';
|
||||
databaseDbUser = database.rootUser;
|
||||
databaseDbUserPassword = database.rootUserPassword;
|
||||
} else if (database.type === 'redis') {
|
||||
databaseDefault = '';
|
||||
databaseDbUser = '';
|
||||
}
|
||||
let databaseUrl = generateUrl();
|
||||
|
||||
function generateUrl() {
|
||||
return browser
|
||||
? `${database.type}://${
|
||||
databaseDbUser ? databaseDbUser + ':' : ''
|
||||
}${databaseDbUserPassword}@${
|
||||
isPublic
|
||||
? settings.fqdn
|
||||
? getDomain(settings.fqdn)
|
||||
: window.location.hostname
|
||||
: database.id
|
||||
}:${isPublic ? database.publicPort : privatePort}/${databaseDefault}`
|
||||
: 'Loading...';
|
||||
}
|
||||
|
||||
async function changeSettings(name) {
|
||||
if (name === 'isPublic') {
|
||||
isPublic = !isPublic;
|
||||
}
|
||||
if (name === 'appendOnly') {
|
||||
appendOnly = !appendOnly;
|
||||
}
|
||||
try {
|
||||
await post(`/databases/${id}/settings.json`, { isPublic, appendOnly });
|
||||
databaseUrl = generateUrl();
|
||||
return;
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await post(`/databases/${id}.json`, { ...database });
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mx-auto max-w-4xl px-6">
|
||||
<form on:submit|preventDefault={handleSubmit} class="py-4">
|
||||
<div class="flex space-x-1 pb-5 font-bold">
|
||||
<div class="title">General</div>
|
||||
{#if $session.isAdmin}
|
||||
<button
|
||||
type="submit"
|
||||
class:bg-purple-600={!loading}
|
||||
class:hover:bg-purple-500={!loading}
|
||||
disabled={loading}>{loading ? 'Saving...' : 'Save'}</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-flow-row gap-2 px-10">
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="name">Name</label>
|
||||
<div class="col-span-2 ">
|
||||
<input
|
||||
readonly={!$session.isAdmin}
|
||||
name="name"
|
||||
id="name"
|
||||
bind:value={database.name}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="destination">Destination</label>
|
||||
<div class="col-span-2">
|
||||
{#if database.destinationDockerId}
|
||||
<div class="no-underline">
|
||||
<input
|
||||
value={database.destinationDocker.name}
|
||||
id="destination"
|
||||
disabled
|
||||
readonly
|
||||
class="bg-transparent "
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="version">Version</label>
|
||||
<div class="col-span-2 ">
|
||||
<input value={database.version} readonly disabled class="bg-transparent " />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-flow-row gap-2 px-10">
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="host">Host</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
placeholder="Generated automatically after start"
|
||||
isPasswordField={false}
|
||||
readonly
|
||||
disabled
|
||||
id="host"
|
||||
name="host"
|
||||
value={database.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="publicPort">Port</label>
|
||||
<div class="col-span-2">
|
||||
<CopyPasswordField
|
||||
placeholder="Generated automatically after start"
|
||||
id="publicPort"
|
||||
readonly
|
||||
disabled
|
||||
name="publicPort"
|
||||
value={isPublic ? database.publicPort : privatePort}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-flow-row gap-2">
|
||||
{#if database.type === 'mysql'}
|
||||
<MySql bind:database />
|
||||
{:else if database.type === 'postgresql'}
|
||||
<PostgreSql bind:database />
|
||||
{:else if database.type === 'mongodb'}
|
||||
<MongoDb {database} />
|
||||
{:else if database.type === 'redis'}
|
||||
<Redis {database} />
|
||||
{:else if database.type === 'couchdb'}
|
||||
<CouchDb bind:database />
|
||||
{/if}
|
||||
<div class="grid grid-cols-3 items-center px-10 pb-8">
|
||||
<label for="url">Connection String</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
textarea={true}
|
||||
placeholder="Generated automatically after start"
|
||||
isPasswordField={false}
|
||||
id="url"
|
||||
name="url"
|
||||
readonly
|
||||
disabled
|
||||
value={databaseUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="flex space-x-1 pb-5 font-bold">
|
||||
<div class="title">Features</div>
|
||||
</div>
|
||||
<div class="px-4 pb-10 sm:px-6">
|
||||
<ul class="mt-2 divide-y divide-stone-800">
|
||||
<Setting
|
||||
bind:setting={isPublic}
|
||||
on:click={() => changeSettings('isPublic')}
|
||||
title="Set it public"
|
||||
description="Your database will be reachable over the internet. <br>Take security seriously in this case!"
|
||||
/>
|
||||
</ul>
|
||||
{#if database.type === 'redis'}
|
||||
<ul class="mt-2 divide-y divide-stone-800">
|
||||
<Setting
|
||||
bind:setting={appendOnly}
|
||||
on:click={() => changeSettings('appendOnly')}
|
||||
title="Change append only mode"
|
||||
description="Useful if you would like to restore redis data from a backup.<br><span class='font-bold text-white'>Database restart is required.</span>"
|
||||
/>
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
37
src/routes/databases/[id]/_Databases/_MongoDB.svelte
Normal file
37
src/routes/databases/[id]/_Databases/_MongoDB.svelte
Normal file
@@ -0,0 +1,37 @@
|
||||
<script>
|
||||
export let database;
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">MongoDB</div>
|
||||
</div>
|
||||
<div class="px-10">
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="rootUser">Root User</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
placeholder="Generated automatically after start"
|
||||
id="rootUser"
|
||||
readonly
|
||||
disabled
|
||||
name="rootUser"
|
||||
value={database.rootUser}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="rootUserPassword">Root's Password</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
placeholder="Generated automatically after start"
|
||||
isPasswordField={true}
|
||||
readonly
|
||||
disabled
|
||||
id="rootUserPassword"
|
||||
name="rootUserPassword"
|
||||
value={database.rootUserPassword}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
78
src/routes/databases/[id]/_Databases/_MySQL.svelte
Normal file
78
src/routes/databases/[id]/_Databases/_MySQL.svelte
Normal file
@@ -0,0 +1,78 @@
|
||||
<script>
|
||||
export let database;
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">MySQL</div>
|
||||
</div>
|
||||
<div class=" px-10">
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="defaultDatabase">Default Database</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
required
|
||||
readonly={database.defaultDatabase}
|
||||
disabled={database.defaultDatabase}
|
||||
placeholder="eg: mydb"
|
||||
id="defaultDatabase"
|
||||
name="defaultDatabase"
|
||||
bind:value={database.defaultDatabase}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="dbUser">User</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
readonly
|
||||
disabled
|
||||
placeholder="Generated automatically after start"
|
||||
id="dbUser"
|
||||
name="dbUser"
|
||||
value={database.dbUser}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="dbUserPassword">Password</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
readonly
|
||||
disabled
|
||||
placeholder="Generated automatically after start"
|
||||
isPasswordField
|
||||
id="dbUserPassword"
|
||||
name="dbUserPassword"
|
||||
value={database.dbUserPassword}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="rootUser">Root User</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
readonly
|
||||
disabled
|
||||
placeholder="Generated automatically after start"
|
||||
id="rootUser"
|
||||
name="rootUser"
|
||||
value={database.rootUser}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="rootUserPassword">Root's Password</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
readonly
|
||||
disabled
|
||||
placeholder="Generated automatically after start"
|
||||
isPasswordField
|
||||
id="rootUserPassword"
|
||||
name="rootUserPassword"
|
||||
value={database.rootUserPassword}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
51
src/routes/databases/[id]/_Databases/_PostgreSQL.svelte
Normal file
51
src/routes/databases/[id]/_Databases/_PostgreSQL.svelte
Normal file
@@ -0,0 +1,51 @@
|
||||
<script>
|
||||
export let database;
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">PostgreSQL</div>
|
||||
</div>
|
||||
<div class="px-10">
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="defaultDatabase">Default Database</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
required
|
||||
readonly={database.defaultDatabase}
|
||||
disabled={database.defaultDatabase}
|
||||
placeholder="eg: mydb"
|
||||
id="defaultDatabase"
|
||||
name="defaultDatabase"
|
||||
bind:value={database.defaultDatabase}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="dbUser">User</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
readonly
|
||||
disabled
|
||||
placeholder="Generated automatically after start"
|
||||
id="dbUser"
|
||||
name="dbUser"
|
||||
value={database.dbUser}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="dbUserPassword">Password</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
readonly
|
||||
disabled
|
||||
placeholder="Generated automatically after start"
|
||||
isPasswordField
|
||||
id="dbUserPassword"
|
||||
name="dbUserPassword"
|
||||
value={database.dbUserPassword}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
64
src/routes/databases/[id]/_Databases/_Redis.svelte
Normal file
64
src/routes/databases/[id]/_Databases/_Redis.svelte
Normal file
@@ -0,0 +1,64 @@
|
||||
<script>
|
||||
export let database;
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Redis</div>
|
||||
</div>
|
||||
<div class="px-10">
|
||||
<!-- <div class="grid grid-cols-3 items-center">
|
||||
<label for="dbUser">User</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
readonly
|
||||
disabled
|
||||
placeholder="Generated automatically after start"
|
||||
id="dbUser"
|
||||
name="dbUser"
|
||||
bind:value={database.dbUser}
|
||||
/>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="dbUserPassword">Password</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
disabled
|
||||
readonly
|
||||
placeholder="Generated automatically after start"
|
||||
isPasswordField
|
||||
id="dbUserPassword"
|
||||
name="dbUserPassword"
|
||||
value={database.dbUserPassword}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="grid grid-cols-3 items-center">
|
||||
<label for="rootUser">Root User</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
disabled
|
||||
readonly
|
||||
placeholder="Generated automatically after start"
|
||||
id="rootUser"
|
||||
name="rootUser"
|
||||
value={database.rootUser}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="rootUserPassword">Root's Password</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
disabled
|
||||
readonly
|
||||
placeholder="Generated automatically after start"
|
||||
isPasswordField
|
||||
id="rootUserPassword"
|
||||
name="rootUserPassword"
|
||||
value={database.rootUserPassword}
|
||||
/>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
182
src/routes/databases/[id]/__layout.svelte
Normal file
182
src/routes/databases/[id]/__layout.svelte
Normal file
@@ -0,0 +1,182 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
function checkConfiguration(database): string {
|
||||
let configurationPhase = null;
|
||||
if (!database.type) {
|
||||
configurationPhase = 'type';
|
||||
} else if (!database.version) {
|
||||
configurationPhase = 'version';
|
||||
} else if (!database.destinationDockerId) {
|
||||
configurationPhase = 'destination';
|
||||
}
|
||||
return configurationPhase;
|
||||
}
|
||||
export const load: Load = async ({ fetch, params, url }) => {
|
||||
const endpoint = `/databases/${params.id}.json`;
|
||||
const res = await fetch(endpoint);
|
||||
if (res.ok) {
|
||||
const { database, state, versions, privatePort, settings } = await res.json();
|
||||
if (!database || Object.entries(database).length === 0) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/databases'
|
||||
};
|
||||
}
|
||||
const configurationPhase = checkConfiguration(database);
|
||||
if (
|
||||
configurationPhase &&
|
||||
url.pathname !== `/databases/${params.id}/configuration/${configurationPhase}`
|
||||
) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/databases/${params.id}/configuration/${configurationPhase}`
|
||||
};
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
database,
|
||||
state,
|
||||
versions,
|
||||
privatePort
|
||||
},
|
||||
stuff: {
|
||||
database,
|
||||
state,
|
||||
versions,
|
||||
privatePort,
|
||||
settings
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/databases'
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { session } from '$app/stores';
|
||||
import { errorNotification } from '$lib/form';
|
||||
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
|
||||
import Loading from '$lib/components/Loading.svelte';
|
||||
import { del, post } from '$lib/api';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let database;
|
||||
export let state;
|
||||
let loading = false;
|
||||
|
||||
async function deleteDatabase() {
|
||||
const sure = confirm(`Are you sure you would like to delete '${database.name}'?`);
|
||||
if (sure) {
|
||||
loading = true;
|
||||
try {
|
||||
await del(`/databases/${database.id}/delete.json`, { id: database.id });
|
||||
return await goto('/databases');
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
async function stopDatabase() {
|
||||
const sure = confirm(`Are you sure you would like to stop '${database.name}'?`);
|
||||
if (sure) {
|
||||
loading = true;
|
||||
try {
|
||||
await post(`/databases/${database.id}/stop.json`, {});
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
async function startDatabase() {
|
||||
loading = true;
|
||||
try {
|
||||
await post(`/databases/${database.id}/start.json`, {});
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav class="nav-side">
|
||||
{#if loading}
|
||||
<Loading fullscreen cover />
|
||||
{:else}
|
||||
{#if database.type && database.destinationDockerId && database.version && database.defaultDatabase}
|
||||
{#if state === 'running'}
|
||||
<button
|
||||
on:click={stopDatabase}
|
||||
title="Stop database"
|
||||
type="submit"
|
||||
disabled={!$session.isAdmin}
|
||||
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 hover:bg-purple-600 hover:text-white"
|
||||
data-tooltip={$session.isAdmin
|
||||
? 'Stop database'
|
||||
: 'You do not have permission to stop the database.'}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<rect x="6" y="5" width="4" height="14" rx="1" />
|
||||
<rect x="14" y="5" width="4" height="14" rx="1" />
|
||||
</svg>
|
||||
</button>
|
||||
{:else if state === 'not started'}
|
||||
<button
|
||||
on:click={startDatabase}
|
||||
title="Start database"
|
||||
type="submit"
|
||||
disabled={!$session.isAdmin}
|
||||
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 hover:bg-purple-600 hover:text-white"
|
||||
data-tooltip={$session.isAdmin
|
||||
? 'Start database'
|
||||
: 'You do not have permission to start the database.'}
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
<button
|
||||
on:click={deleteDatabase}
|
||||
title="Delete Database"
|
||||
type="submit"
|
||||
disabled={!$session.isAdmin}
|
||||
class:hover:text-red-500={$session.isAdmin}
|
||||
class="icons bg-transparent tooltip-bottom text-sm"
|
||||
data-tooltip={$session.isAdmin
|
||||
? 'Delete Database'
|
||||
: 'You do not have permission to delete a Database'}><DeleteIcon /></button
|
||||
>
|
||||
{/if}
|
||||
</nav>
|
||||
<slot />
|
||||
19
src/routes/databases/[id]/configuration/destination.json.ts
Normal file
19
src/routes/databases/[id]/configuration/destination.json.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
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;
|
||||
const { destinationId } = await event.request.json();
|
||||
|
||||
try {
|
||||
await db.configureDestinationForDatabase({ id, destinationId });
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
93
src/routes/databases/[id]/configuration/destination.svelte
Normal file
93
src/routes/databases/[id]/configuration/destination.svelte
Normal file
@@ -0,0 +1,93 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
const { database } = stuff;
|
||||
if (database?.destinationDockerId && !url.searchParams.get('from')) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/database/${params.id}`
|
||||
};
|
||||
}
|
||||
|
||||
const endpoint = `/destinations.json`;
|
||||
const res = await fetch(endpoint);
|
||||
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
...(await res.json())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type Prisma from '@prisma/client';
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { errorNotification } from '$lib/form';
|
||||
import { goto } from '$app/navigation';
|
||||
import { post } from '$lib/api';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
|
||||
export let destinations: Prisma.DestinationDocker[];
|
||||
async function handleSubmit(destinationId) {
|
||||
try {
|
||||
await post(`/databases/${id}/configuration/destination.json`, {
|
||||
destinationId
|
||||
});
|
||||
return await goto(from || `/databases/${id}`);
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">Configure Destination</div>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
{#if !destinations || destinations.length === 0}
|
||||
<div class="flex-col">
|
||||
<div class="pb-2">No configurable Destination found</div>
|
||||
<div class="flex justify-center">
|
||||
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
|
||||
<svg
|
||||
class="w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||
/></svg
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#each destinations as destination}
|
||||
<div class="p-2">
|
||||
<form on:submit|preventDefault={() => handleSubmit(destination.id)}>
|
||||
<button type="submit" class="box-selection hover:bg-sky-700 font-bold">
|
||||
<div class="font-bold text-xl text-center truncate">{destination.name}</div>
|
||||
<div class="text-center truncate">{destination.network}</div>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
32
src/routes/databases/[id]/configuration/type.json.ts
Normal file
32
src/routes/databases/[id]/configuration/type.json.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler, supportedDatabaseTypesAndVersions } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
types: supportedDatabaseTypesAndVersions
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
const { type } = await event.request.json();
|
||||
|
||||
try {
|
||||
await db.configureDatabaseType({ id, type });
|
||||
return {
|
||||
status: 201
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
81
src/routes/databases/[id]/configuration/type.svelte
Normal file
81
src/routes/databases/[id]/configuration/type.svelte
Normal file
@@ -0,0 +1,81 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
const { database } = stuff;
|
||||
if (database?.type && !url.searchParams.get('from')) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/databases/${params.id}`
|
||||
};
|
||||
}
|
||||
const endpoint = `/databases/${params.id}/configuration/type.json`;
|
||||
const res = await fetch(endpoint);
|
||||
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
...(await res.json())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { errorNotification } from '$lib/form';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
|
||||
export let types;
|
||||
import Clickhouse from '$lib/components/svg/databases/Clickhouse.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';
|
||||
import { goto } from '$app/navigation';
|
||||
import { post } from '$lib/api';
|
||||
async function handleSubmit(type) {
|
||||
try {
|
||||
await post(`/databases/${id}/configuration/type.json`, { type });
|
||||
return await goto(from || `/databases/${id}/configuration/version`);
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">Select a Database type</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#each types as type}
|
||||
<div class="p-2">
|
||||
<form on:submit|preventDefault={() => handleSubmit(type.name)}>
|
||||
<button type="submit" class="box-selection relative text-xl font-bold hover:bg-purple-700">
|
||||
{#if type.name === 'clickhouse'}
|
||||
<Clickhouse isAbsolute />
|
||||
{:else if type.name === 'couchdb'}
|
||||
<CouchDB isAbsolute />
|
||||
{:else if type.name === 'mongodb'}
|
||||
<MongoDB isAbsolute />
|
||||
{:else if type.name === 'mysql'}
|
||||
<MySQL isAbsolute />
|
||||
{:else if type.name === 'postgresql'}
|
||||
<PostgreSQL isAbsolute />
|
||||
{:else if type.name === 'redis'}
|
||||
<Redis isAbsolute />
|
||||
{/if}{type.fancyName}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
36
src/routes/databases/[id]/configuration/version.json.ts
Normal file
36
src/routes/databases/[id]/configuration/version.json.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler, supportedDatabaseTypesAndVersions } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
const { type } = await db.getDatabase({ id, teamId });
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
versions: supportedDatabaseTypesAndVersions.find((name) => name.name === type).versions
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
const { version } = await event.request.json();
|
||||
|
||||
try {
|
||||
await db.setDatabase({ id, version });
|
||||
return {
|
||||
status: 201
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
63
src/routes/databases/[id]/configuration/version.svelte
Normal file
63
src/routes/databases/[id]/configuration/version.svelte
Normal file
@@ -0,0 +1,63 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
const { database } = stuff;
|
||||
if (database?.version && !url.searchParams.get('from')) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/databases/${params.id}`
|
||||
};
|
||||
}
|
||||
const endpoint = `/databases/${params.id}/configuration/version.json`;
|
||||
const res = await fetch(endpoint);
|
||||
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
...(await res.json())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { enhance, errorNotification } from '$lib/form';
|
||||
import { goto } from '$app/navigation';
|
||||
import { post } from '$lib/api';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
|
||||
export let versions;
|
||||
async function handleSubmit(version) {
|
||||
try {
|
||||
await post(`/databases/${id}/configuration/version.json`, { version });
|
||||
return await goto(from || `/databases/${id}/configuration/destination`);
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">Select a Database version</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#each versions as version}
|
||||
<div class="p-2">
|
||||
<form on:submit|preventDefault={() => handleSubmit(version)}>
|
||||
<button type="submit" class="box-selection text-xl font-bold hover:bg-purple-700"
|
||||
>{version}</button
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
22
src/routes/databases/[id]/delete.json.ts
Normal file
22
src/routes/databases/[id]/delete.json.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler, stopDatabase } from '$lib/database';
|
||||
import { deleteProxy } from '$lib/haproxy';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const del: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
const { id } = event.params;
|
||||
try {
|
||||
const database = await db.getDatabase({ id, teamId });
|
||||
if (database.destinationDockerId) {
|
||||
const everStarted = await stopDatabase(database);
|
||||
if (everStarted) await deleteProxy({ id });
|
||||
}
|
||||
await db.removeDatabase({ id });
|
||||
return { status: 200 };
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
71
src/routes/databases/[id]/index.json.ts
Normal file
71
src/routes/databases/[id]/index.json.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { asyncExecShell, getEngine, getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { generateDatabaseConfiguration, getVersions, PrismaErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
try {
|
||||
const database = await db.getDatabase({ id, teamId });
|
||||
const { destinationDockerId, destinationDocker } = database;
|
||||
|
||||
let state = 'not started';
|
||||
if (destinationDockerId) {
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
|
||||
try {
|
||||
const { stdout } = await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker inspect --format '{{json .State}}' ${id}`
|
||||
);
|
||||
|
||||
if (JSON.parse(stdout).Running) {
|
||||
state = 'running';
|
||||
}
|
||||
} catch (error) {
|
||||
// if (!error.stderr.includes('No such object')) {
|
||||
// console.log(error)
|
||||
// }
|
||||
}
|
||||
}
|
||||
const configuration = generateDatabaseConfiguration(database);
|
||||
const settings = await db.listSettings();
|
||||
return {
|
||||
body: {
|
||||
privatePort: configuration?.privatePort,
|
||||
database,
|
||||
state,
|
||||
versions: getVersions(database.type),
|
||||
settings
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
const { id } = event.params;
|
||||
const { name, defaultDatabase, dbUser, dbUserPassword, rootUser, rootUserPassword, version } =
|
||||
await event.request.json();
|
||||
|
||||
try {
|
||||
await db.updateDatabase({
|
||||
id,
|
||||
name,
|
||||
defaultDatabase,
|
||||
dbUser,
|
||||
dbUserPassword,
|
||||
rootUser,
|
||||
rootUserPassword,
|
||||
version
|
||||
});
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
67
src/routes/databases/[id]/index.svelte
Normal file
67
src/routes/databases/[id]/index.svelte
Normal file
@@ -0,0 +1,67 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
import Databases from './_Databases/_Databases.svelte';
|
||||
export const load: Load = async ({ fetch, params, stuff }) => {
|
||||
if (stuff?.database?.id) {
|
||||
return {
|
||||
props: {
|
||||
database: stuff.database,
|
||||
versions: stuff.versions,
|
||||
privatePort: stuff.privatePort,
|
||||
settings: stuff.settings
|
||||
}
|
||||
};
|
||||
}
|
||||
const endpoint = `/databases/${params.id}.json`;
|
||||
const res = await fetch(endpoint);
|
||||
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
...(await res.json())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
error: new Error(`Could not load ${endpoint}`)
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import Clickhouse from '$lib/components/svg/databases/Clickhouse.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 settings;
|
||||
export let privatePort;
|
||||
</script>
|
||||
|
||||
<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">
|
||||
{database.name}
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<Databases bind:database {privatePort} {settings} />
|
||||
34
src/routes/databases/[id]/settings.json.ts
Normal file
34
src/routes/databases/[id]/settings.json.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { generateDatabaseConfiguration, PrismaErrorHandler } from '$lib/database';
|
||||
import { startTcpProxy, stopTcpHttpProxy } from '$lib/haproxy';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { status, body, teamId } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
const { isPublic, appendOnly = true } = await event.request.json();
|
||||
|
||||
try {
|
||||
await db.setDatabase({ id, isPublic, appendOnly });
|
||||
const database = await db.getDatabase({ id, teamId });
|
||||
const { destinationDockerId, destinationDocker, publicPort } = database;
|
||||
const { privatePort } = generateDatabaseConfiguration(database);
|
||||
|
||||
if (destinationDockerId) {
|
||||
if (isPublic) {
|
||||
await startTcpProxy(destinationDocker, id, publicPort, privatePort);
|
||||
} else {
|
||||
await stopTcpHttpProxy(destinationDocker, publicPort);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: 201
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
82
src/routes/databases/[id]/start.json.ts
Normal file
82
src/routes/databases/[id]/start.json.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { asyncExecShell, createDirectories, getEngine, getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { generateDatabaseConfiguration, PrismaErrorHandler } from '$lib/database';
|
||||
import { promises as fs } from 'fs';
|
||||
import yaml from 'js-yaml';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { makeLabelForStandaloneDatabase } from '$lib/buildPacks/common';
|
||||
import { startTcpProxy } from '$lib/haproxy';
|
||||
|
||||
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 database = await db.getDatabase({ id, teamId });
|
||||
const {
|
||||
type,
|
||||
destinationDockerId,
|
||||
destinationDocker,
|
||||
publicPort,
|
||||
settings: { isPublic }
|
||||
} = database;
|
||||
const { privatePort, environmentVariables, image, volume, ulimits } =
|
||||
generateDatabaseConfiguration(database);
|
||||
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
const engine = destinationDocker.engine;
|
||||
const volumeName = volume.split(':')[0];
|
||||
const labels = await makeLabelForStandaloneDatabase({ id, image, volume });
|
||||
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
|
||||
const composeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[id]: {
|
||||
container_name: id,
|
||||
image,
|
||||
networks: [network],
|
||||
environment: environmentVariables,
|
||||
volumes: [volume],
|
||||
ulimits,
|
||||
labels,
|
||||
restart: 'always'
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[network]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
volumes: {
|
||||
[volumeName]: {
|
||||
external: true
|
||||
}
|
||||
}
|
||||
};
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
try {
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker volume create ${volumeName}`);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
try {
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
|
||||
if (isPublic) await startTcpProxy(destinationDocker, id, publicPort, privatePort);
|
||||
return {
|
||||
status: 200
|
||||
};
|
||||
} catch (error) {
|
||||
throw {
|
||||
error
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user