v1.0.12 - Sveltekit migration (#44)
Changed the whole tech stack to SvelteKit which means: - Typescript - SSR - No fastify :( - Beta, but it's fine! Other changes: - Tailwind -> Tailwind JIT - A lot more
This commit is contained in:
15
src/routes/__error.svelte
Normal file
15
src/routes/__error.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<script context="module">
|
||||
export function load({ error, status }) {
|
||||
return {
|
||||
props: {
|
||||
error: `${status}: ${error.message}`
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export let error;
|
||||
</script>
|
||||
|
||||
<h1 class="text-xl font-bold">{error}</h1>
|
||||
315
src/routes/__layout.svelte
Normal file
315
src/routes/__layout.svelte
Normal file
@@ -0,0 +1,315 @@
|
||||
<script context="module" lang="ts">
|
||||
import { publicPages } from '$lib/consts';
|
||||
import { request } from '$lib/api/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: 301,
|
||||
redirect: '/'
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
if (!publicPages.includes(path)) {
|
||||
return {
|
||||
status: 301,
|
||||
redirect: '/'
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import '../app.postcss';
|
||||
export let initDashboard;
|
||||
import { onMount } from 'svelte';
|
||||
import { SvelteToast } from '@zerodevx/svelte-toast';
|
||||
import { goto } from '$app/navigation';
|
||||
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 } from '$store';
|
||||
import { browser } from '$app/env';
|
||||
$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;
|
||||
const options = {
|
||||
duration: 2000
|
||||
};
|
||||
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 } });
|
||||
}
|
||||
}
|
||||
});
|
||||
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() {
|
||||
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.'
|
||||
);
|
||||
}
|
||||
}
|
||||
async function logout() {
|
||||
await request('/api/v1/logout', $session, { body: {}, method: 'DELETE' });
|
||||
location.reload();
|
||||
}
|
||||
function reloadInAMin() {
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 30000);
|
||||
}
|
||||
function ackError() {
|
||||
localStorage.setItem('automaticErrorReportsAck', 'true');
|
||||
showAck = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<SvelteToast {options} />
|
||||
|
||||
{#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
|
||||
>.
|
||||
</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>
|
||||
|
||||
<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"
|
||||
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">
|
||||
<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')}
|
||||
>
|
||||
<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="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>
|
||||
<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')}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-8"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div class="flex-1" />
|
||||
<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"
|
||||
>
|
||||
{packageJson.version}
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
{/if}
|
||||
<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 transform 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}
|
||||
42
src/routes/api/_index.ts
Normal file
42
src/routes/api/_index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
|
||||
// export async function api(request: Request, resource: string, data?: {}) {
|
||||
// const base = 'https://github.com/';
|
||||
// if (!request.context.isLoggedIn) {
|
||||
// return { status: 401, body: 'Unauthorized' };
|
||||
// }
|
||||
|
||||
// const res = await fetch(`${base}${resource}`, {
|
||||
// method: request.method,
|
||||
// headers: {
|
||||
// 'content-type': 'application/json'
|
||||
// },
|
||||
// body: data && JSON.stringify(data)
|
||||
// });
|
||||
// return {
|
||||
// status: res.status,
|
||||
// body: await res.json()
|
||||
// };
|
||||
// }
|
||||
|
||||
export async function githubAPI(
|
||||
request: Request,
|
||||
resource: string,
|
||||
token?: string,
|
||||
data?: Record<string, unknown>
|
||||
) {
|
||||
const base = 'https://api.github.com';
|
||||
const res = await fetch(`${base}${resource}`, {
|
||||
method: request.method,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
accept: 'application/json',
|
||||
authorization: token ? `token ${token}` : ''
|
||||
},
|
||||
body: data && JSON.stringify(data)
|
||||
});
|
||||
return {
|
||||
status: res.status,
|
||||
body: await res.json()
|
||||
};
|
||||
}
|
||||
51
src/routes/api/v1/application/check.ts
Normal file
51
src/routes/api/v1/application/check.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { setDefaultConfiguration } from '$lib/api/applications/configuration';
|
||||
import { saveServerLog } from '$lib/api/applications/logging';
|
||||
import { docker } from '$lib/api/docker';
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
|
||||
export async function post(request: Request) {
|
||||
try {
|
||||
const { DOMAIN } = process.env;
|
||||
const configuration = setDefaultConfiguration(request.body);
|
||||
|
||||
const services = (await docker.engine.listServices()).filter(
|
||||
(r) => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application'
|
||||
);
|
||||
let foundDomain = false;
|
||||
|
||||
for (const service of services) {
|
||||
const running = JSON.parse(service.Spec.Labels.configuration);
|
||||
if (running) {
|
||||
if (
|
||||
running.publish.domain === configuration.publish.domain &&
|
||||
running.repository.id !== configuration.repository.id &&
|
||||
running.publish.path === configuration.publish.path
|
||||
) {
|
||||
foundDomain = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (DOMAIN === configuration.publish.domain) foundDomain = true;
|
||||
if (foundDomain) {
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
success: false,
|
||||
message: 'Domain already in use.'
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: 200,
|
||||
body: { success: true, message: 'OK' }
|
||||
};
|
||||
} catch (error) {
|
||||
await saveServerLog(error);
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
50
src/routes/api/v1/application/config.ts
Normal file
50
src/routes/api/v1/application/config.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { docker } from '$lib/api/docker';
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
|
||||
export async function post(request: Request) {
|
||||
const { name, organization, branch }: any = request.body || {};
|
||||
if (name && organization && branch) {
|
||||
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 (branch) {
|
||||
if (
|
||||
configuration.repository.name === name &&
|
||||
configuration.repository.organization === organization &&
|
||||
configuration.repository.branch === branch
|
||||
) {
|
||||
return r;
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
configuration.repository.name === name &&
|
||||
configuration.repository.organization === organization
|
||||
) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
if (found) {
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
success: true,
|
||||
...JSON.parse(found.Spec.Labels.configuration)
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'No configuration found.'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
90
src/routes/api/v1/application/deploy/index.ts
Normal file
90
src/routes/api/v1/application/deploy/index.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
import Deployment from '$models/Logs/Deployment';
|
||||
import { docker } from '$lib/api/docker';
|
||||
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';
|
||||
export async function post(request: Request) {
|
||||
let configuration;
|
||||
try {
|
||||
const services = (await docker.engine.listServices()).filter(
|
||||
(r) => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application'
|
||||
);
|
||||
configuration = setDefaultConfiguration(request.body);
|
||||
|
||||
if (!configuration) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'Whaaat?'
|
||||
}
|
||||
};
|
||||
}
|
||||
await cloneRepository(configuration);
|
||||
const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment({
|
||||
services,
|
||||
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: configuration.repository.id,
|
||||
branch: configuration.repository.branch,
|
||||
organization: configuration.repository.organization,
|
||||
name: configuration.repository.name,
|
||||
domain: configuration.publish.domain,
|
||||
progress: { $in: ['queued', 'inprogress'] }
|
||||
});
|
||||
if (alreadyQueued.length > 0) {
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
success: false,
|
||||
message: 'Already in the queue.'
|
||||
}
|
||||
};
|
||||
}
|
||||
queueAndBuild(configuration, imageChanged);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
message: 'Deployment queued.',
|
||||
nickname: configuration.general.nickname,
|
||||
name: configuration.build.container.name,
|
||||
deployId: configuration.general.deployId
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
35
src/routes/api/v1/application/deploy/logs/[deployId].ts
Normal file
35
src/routes/api/v1/application/deploy/logs/[deployId].ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
import ApplicationLog from '$models/Logs/Application';
|
||||
import Deployment from '$models/Logs/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')
|
||||
.sort({ createdAt: 'desc' });
|
||||
|
||||
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 (e) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: e
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
47
src/routes/api/v1/application/deploy/logs/index.ts
Normal file
47
src/routes/api/v1/application/deploy/logs/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
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/Logs/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();
|
||||
|
||||
return finalLogs;
|
||||
});
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
success: true,
|
||||
logs: finalLogs
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
27
src/routes/api/v1/application/logs.ts
Normal file
27
src/routes/api/v1/application/logs.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
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) {
|
||||
await saveServerLog(error);
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
60
src/routes/api/v1/application/remove.ts
Normal file
60
src/routes/api/v1/application/remove.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { purgeImagesContainers } from '$lib/api/applications/cleanup';
|
||||
import { docker } from '$lib/api/docker';
|
||||
import Deployment from '$models/Logs/Deployment';
|
||||
import ApplicationLog from '$models/Logs/Application';
|
||||
import { delay, execShellAsync } from '$lib/api/common';
|
||||
|
||||
async function call(found) {
|
||||
await delay(10000);
|
||||
await purgeImagesContainers(found, true);
|
||||
}
|
||||
export async function post(request: Request) {
|
||||
const { organization, name, branch } = request.body;
|
||||
let found = false;
|
||||
try {
|
||||
(await docker.engine.listServices())
|
||||
.filter((r) => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
|
||||
.map((s) => {
|
||||
const running = JSON.parse(s.Spec.Labels.configuration);
|
||||
if (
|
||||
running.repository.organization === organization &&
|
||||
running.repository.name === name &&
|
||||
running.repository.branch === branch
|
||||
) {
|
||||
found = running;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
if (found) {
|
||||
const deploys = await Deployment.find({ organization, branch, name });
|
||||
for (const deploy of deploys) {
|
||||
await ApplicationLog.deleteMany({ deployId: deploy.deployId });
|
||||
await Deployment.deleteMany({ deployId: deploy.deployId });
|
||||
}
|
||||
await execShellAsync(`docker stack rm ${found.build.container.name}`);
|
||||
call(found);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
organization,
|
||||
name,
|
||||
branch
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 500,
|
||||
error: {
|
||||
message: 'Nothing to do.'
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: {
|
||||
message: 'Nothing to do.'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
76
src/routes/api/v1/dashboard.ts
Normal file
76
src/routes/api/v1/dashboard.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { docker } from '$lib/api/docker';
|
||||
import LogsServer from '$models/Logs/Server';
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
|
||||
export async function get(request: Request) {
|
||||
const serverLogs = await LogsServer.find();
|
||||
const dockerServices = await docker.engine.listServices();
|
||||
let applications: any = dockerServices.filter(
|
||||
(r) =>
|
||||
r.Spec.Labels.managedBy === 'coolify' &&
|
||||
r.Spec.Labels.type === 'application' &&
|
||||
r.Spec.Labels.configuration
|
||||
);
|
||||
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
|
||||
);
|
||||
applications = applications.map((r) => {
|
||||
if (JSON.parse(r.Spec.Labels.configuration)) {
|
||||
return {
|
||||
configuration: JSON.parse(r.Spec.Labels.configuration),
|
||||
UpdatedAt: r.UpdatedAt
|
||||
};
|
||||
}
|
||||
return {};
|
||||
});
|
||||
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 {};
|
||||
});
|
||||
applications = [
|
||||
...new Map(
|
||||
applications.map((item) => [
|
||||
item.configuration.publish.domain + item.configuration.publish.path,
|
||||
item
|
||||
])
|
||||
).values()
|
||||
];
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
success: true,
|
||||
serverLogs,
|
||||
applications: {
|
||||
deployed: applications
|
||||
},
|
||||
databases: {
|
||||
deployed: databases
|
||||
},
|
||||
services: {
|
||||
deployed: services
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
122
src/routes/api/v1/databases/[deployId]/backup.ts
Normal file
122
src/routes/api/v1/databases/[deployId]/backup.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
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}`)
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: 501,
|
||||
body: {
|
||||
error: `Backup method not implemented yet for ${type}.`
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await saveServerLog(error);
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error
|
||||
}
|
||||
};
|
||||
} finally {
|
||||
await execShellAsync(`rm -fr ${tmpdir}`);
|
||||
}
|
||||
}
|
||||
59
src/routes/api/v1/databases/[deployId]/index.ts
Normal file
59
src/routes/api/v1/databases/[deployId]/index.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
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.'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
161
src/routes/api/v1/databases/deploy.ts
Normal file
161
src/routes/api/v1/databases/deploy.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
110
src/routes/api/v1/login/github/app.ts
Normal file
110
src/routes/api/v1/login/github/app.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { githubAPI } from '$api';
|
||||
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';
|
||||
|
||||
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, id } = 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
|
||||
});
|
||||
const defaultSettings = new Settings({
|
||||
_id: new mongoose.Types.ObjectId()
|
||||
});
|
||||
try {
|
||||
await newUser.save();
|
||||
await defaultSettings.save();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return {
|
||||
status: 500,
|
||||
body: e
|
||||
};
|
||||
}
|
||||
} 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
|
||||
});
|
||||
try {
|
||||
await newUser.save();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: e
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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) {
|
||||
console.log('error happened');
|
||||
console.log(error);
|
||||
return { status: 500, body: { ...error } };
|
||||
}
|
||||
}
|
||||
10
src/routes/api/v1/logout.ts
Normal file
10
src/routes/api/v1/logout.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
export async function del(request: Request) {
|
||||
request.locals.session.destroy = true;
|
||||
|
||||
return {
|
||||
body: {
|
||||
ok: true
|
||||
}
|
||||
};
|
||||
}
|
||||
52
src/routes/api/v1/services/[serviceName].ts
Normal file
52
src/routes/api/v1/services/[serviceName].ts
Normal file
@@ -0,0 +1,52 @@
|
||||
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) {
|
||||
console.log(error);
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
success: false,
|
||||
error
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function del(request: Request) {
|
||||
const { serviceName } = request.params;
|
||||
await execShellAsync(`docker stack rm ${serviceName}`);
|
||||
return { status: 200, body: {} };
|
||||
}
|
||||
24
src/routes/api/v1/services/deploy/plausible/activate.ts
Normal file
24
src/routes/api/v1/services/deploy/plausible/activate.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
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' }
|
||||
};
|
||||
}
|
||||
187
src/routes/api/v1/services/deploy/plausible/index.ts
Normal file
187
src/routes/api/v1/services/deploy/plausible/index.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
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' }
|
||||
};
|
||||
}
|
||||
52
src/routes/api/v1/settings.ts
Normal file
52
src/routes/api/v1/settings.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
20
src/routes/api/v1/upgrade.ts
Normal file
20
src/routes/api/v1/upgrade.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
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)"'
|
||||
);
|
||||
// saveServerLog({ message: upgradeP2, type: 'UPGRADE-P-2' })
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
message: "I'm trying, okay?"
|
||||
}
|
||||
};
|
||||
}
|
||||
24
src/routes/api/v1/verify.ts
Normal file
24
src/routes/api/v1/verify.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// import { deleteCookies } from '$lib/api/common';
|
||||
// import { verifyUserId } from '$lib/api/common';
|
||||
// import type { Request } from '@sveltejs/kit';
|
||||
// import * as cookie from 'cookie';
|
||||
|
||||
// export async function post(request: Request) {
|
||||
// const { coolToken } = cookie.parse(request.headers.cookie || '');
|
||||
// try {
|
||||
// await verifyUserId(coolToken);
|
||||
// return {
|
||||
// status: 200,
|
||||
// body: { success: true }
|
||||
// };
|
||||
// } catch (error) {
|
||||
// return {
|
||||
// status: 301,
|
||||
// headers: {
|
||||
// location: '/',
|
||||
// 'set-cookie': [...deleteCookies]
|
||||
// },
|
||||
// body: { error: 'Unauthorized' }
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
113
src/routes/api/v1/webhooks/deploy.ts
Normal file
113
src/routes/api/v1/webhooks/deploy.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
import crypto from 'crypto';
|
||||
import Deployment from '$models/Logs/Deployment';
|
||||
import { docker } from '$lib/api/docker';
|
||||
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';
|
||||
export async function post(request: Request) {
|
||||
let configuration;
|
||||
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 (request.headers['x-github-event'] !== 'push') {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'Not a push event.'
|
||||
}
|
||||
};
|
||||
}
|
||||
try {
|
||||
const services = (await docker.engine.listServices()).filter(
|
||||
(r) => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application'
|
||||
);
|
||||
|
||||
configuration = services.find((r) => {
|
||||
if (request.body.ref.startsWith('refs')) {
|
||||
const branch = request.body.ref.split('/')[2];
|
||||
if (
|
||||
JSON.parse(r.Spec.Labels.configuration).repository.id === request.body.repository.id &&
|
||||
JSON.parse(r.Spec.Labels.configuration).repository.branch === branch
|
||||
) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
configuration = setDefaultConfiguration(JSON.parse(configuration.Spec.Labels.configuration));
|
||||
|
||||
if (!configuration) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'Whaaat?'
|
||||
}
|
||||
};
|
||||
}
|
||||
await cloneRepository(configuration);
|
||||
const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment({
|
||||
services,
|
||||
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: configuration.repository.id,
|
||||
branch: configuration.repository.branch,
|
||||
organization: configuration.repository.organization,
|
||||
name: configuration.repository.name,
|
||||
domain: configuration.publish.domain,
|
||||
progress: { $in: ['queued', 'inprogress'] }
|
||||
});
|
||||
if (alreadyQueued.length > 0) {
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
success: false,
|
||||
message: 'Already in the queue.'
|
||||
}
|
||||
};
|
||||
}
|
||||
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) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<script>
|
||||
import Configuration from '$components/Application/Configuration.svelte';
|
||||
|
||||
</script>
|
||||
|
||||
<Configuration />
|
||||
@@ -0,0 +1,83 @@
|
||||
<script>
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import Loading from '$components/Loading.svelte';
|
||||
import { request } from '$lib/api/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}
|
||||
@@ -0,0 +1,135 @@
|
||||
<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/api/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:bg-red-200={deployment.progress === 'failed'}
|
||||
class:hover:border-red-500={deployment.progress === 'failed'}
|
||||
on:click={() => goto(`./logs/${deployment.deployId}`)}
|
||||
>
|
||||
<div class="font-bold text-sm px-3 flex justify-center items-center">
|
||||
{deployment.branch}
|
||||
</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>
|
||||
127
src/routes/application/__layout.svelte
Normal file
127
src/routes/application/__layout.svelte
Normal file
@@ -0,0 +1,127 @@
|
||||
<script>
|
||||
import { application, initialApplication, initConf, dashboard } 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/api/request';
|
||||
|
||||
$application.repository.organization = $page.params.organization;
|
||||
$application.repository.name = $page.params.name;
|
||||
$application.repository.branch = $page.params.branch;
|
||||
|
||||
async function setConfiguration() {
|
||||
try {
|
||||
const config = await request(`/api/v1/application/config`, $session, {
|
||||
body: {
|
||||
name: $application.repository.name,
|
||||
organization: $application.repository.organization,
|
||||
branch: $application.repository.branch
|
||||
}
|
||||
});
|
||||
$application = { ...config };
|
||||
$initConf = JSON.parse(JSON.stringify($application));
|
||||
} 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 { organization, name, branch } = app.configuration.repository;
|
||||
if (
|
||||
organization === $application.repository.organization &&
|
||||
name === $application.repository.name &&
|
||||
branch === $application.repository.branch
|
||||
) {
|
||||
return app;
|
||||
}
|
||||
});
|
||||
if (found) {
|
||||
$application = { ...found.configuration };
|
||||
$initConf = JSON.parse(JSON.stringify($application));
|
||||
} else {
|
||||
await setConfiguration();
|
||||
}
|
||||
}
|
||||
} 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}
|
||||
5
src/routes/application/new.svelte
Normal file
5
src/routes/application/new.svelte
Normal file
@@ -0,0 +1,5 @@
|
||||
<script>
|
||||
import Configuration from '$components/Application/Configuration.svelte';
|
||||
</script>
|
||||
|
||||
<Configuration />
|
||||
44
src/routes/dashboard/__layout.svelte
Normal file
44
src/routes/dashboard/__layout.svelte
Normal file
@@ -0,0 +1,44 @@
|
||||
<script context="module" lang="ts">
|
||||
import { request } from '$lib/api/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>
|
||||
299
src/routes/dashboard/applications.svelte
Normal file
299
src/routes/dashboard/applications.svelte
Normal file
File diff suppressed because one or more lines are too long
82
src/routes/dashboard/databases.svelte
Normal file
82
src/routes/dashboard/databases.svelte
Normal file
@@ -0,0 +1,82 @@
|
||||
<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';
|
||||
</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 transform 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 == '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>
|
||||
|
||||
62
src/routes/dashboard/services.svelte
Normal file
62
src/routes/dashboard/services.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
import { dashboard } from '$store';
|
||||
import { fade } from 'svelte/transition';
|
||||
</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={() => goto(`/service/${service.serviceName}/configuration`)}
|
||||
>
|
||||
<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 transform 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>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-2xl font-bold text-center">No services found</div>
|
||||
{/if}
|
||||
</div>
|
||||
110
src/routes/database/[name]/configuration.svelte
Normal file
110
src/routes/database/[name]/configuration.svelte
Normal file
@@ -0,0 +1,110 @@
|
||||
<script>
|
||||
import { database } from '$store';
|
||||
import { page, session } from '$app/stores';
|
||||
import { request } from '$lib/api/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';
|
||||
|
||||
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" />
|
||||
{/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 === '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}
|
||||
<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}
|
||||
78
src/routes/database/__layout.svelte
Normal file
78
src/routes/database/__layout.svelte
Normal file
@@ -0,0 +1,78 @@
|
||||
<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/api/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>
|
||||
11
src/routes/database/new.svelte
Normal file
11
src/routes/database/new.svelte
Normal file
@@ -0,0 +1,11 @@
|
||||
<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 />
|
||||
60
src/routes/index.svelte
Normal file
60
src/routes/index.svelte
Normal file
@@ -0,0 +1,60 @@
|
||||
<script>
|
||||
import { browser } from '$app/env';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import { session } from '$app/stores';
|
||||
import { request } from '$lib/api/request';
|
||||
|
||||
async function login() {
|
||||
const left = screen.width / 2 - 1020 / 2;
|
||||
const top = screen.height / 2 - 618 / 2;
|
||||
const newWindow = open(
|
||||
`https://github.com/login/oauth/authorize?client_id=${
|
||||
import.meta.env.VITE_GITHUB_APP_CLIENTID
|
||||
}`,
|
||||
'Authenticate',
|
||||
'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);
|
||||
browser && location.reload()
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="flex justify-center items-center h-screen w-full bg-warmGray-900">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:py-24 sm:px-6 lg:px-8">
|
||||
<div class="text-center">
|
||||
<p
|
||||
class="mt-1 pb-8 font-extrabold text-white text-5xl sm:tracking-tight lg:text-6xl text-center"
|
||||
>
|
||||
<span class="border-gradient">Coolify</span>
|
||||
</p>
|
||||
<h2 class="text-2xl md:text-3xl font-extrabold text-white">
|
||||
An open-source, hassle-free, self-hostable<br />
|
||||
<span class="text-indigo-400">Heroku</span>
|
||||
& <span class="text-green-400">Netlify</span> alternative
|
||||
</h2>
|
||||
<div class="text-center py-10">
|
||||
{#if !$session.isLoggedIn}
|
||||
<button
|
||||
class="text-white bg-warmGray-800 hover:bg-warmGray-700 rounded p-2 px-10 font-bold"
|
||||
on:click={login}>Login with Github</button
|
||||
>
|
||||
{:else}
|
||||
<button
|
||||
class="text-white bg-warmGray-800 hover:bg-warmGray-700 rounded p-2 px-10 font-bold"
|
||||
on:click={() => goto('/dashboard/applications')}>Get Started</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
71
src/routes/service/[name]/__layout.svelte
Normal file
71
src/routes/service/[name]/__layout.svelte
Normal file
@@ -0,0 +1,71 @@
|
||||
<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/api/request';
|
||||
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
|
||||
async function removeService() {
|
||||
await request(`/api/v1/services/${$page.params.name}`, $session, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (browser) {
|
||||
toast.push('Service removed.');
|
||||
goto(`/dashboard/services`, { replaceState: true });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<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={removeService}>
|
||||
<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 === '/service/new'}
|
||||
class:text-yellow-400={$page.path.startsWith('/service')}
|
||||
class:bg-warmGray-700={$page.path.startsWith('/service')}
|
||||
on:click={() => goto(`/service/${$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>
|
||||
|
||||
<div class="text-white">
|
||||
<slot />
|
||||
</div>
|
||||
83
src/routes/service/[name]/configuration.svelte
Normal file
83
src/routes/service/[name]/configuration.svelte
Normal file
@@ -0,0 +1,83 @@
|
||||
<script>
|
||||
import { fade } from 'svelte/transition';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
|
||||
import { page, session } from '$app/stores';
|
||||
import { request } from '$lib/api/request';
|
||||
import { goto } from '$app/navigation';
|
||||
import Loading from '$components/Loading.svelte';
|
||||
import Plausible from '$components/Service/Plausible.svelte';
|
||||
import { browser } from '$app/env';
|
||||
let service = {};
|
||||
async function loadServiceConfig() {
|
||||
if ($page.params.name) {
|
||||
try {
|
||||
service = await request(`/api/v1/services/${$page.params.name}`, $session);
|
||||
} catch (error) {
|
||||
browser && toast.push(`Cannot find service ${$page.params.name}?!`);
|
||||
goto(`/dashboard/services`, { replaceState: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
async function activate() {
|
||||
try {
|
||||
await request(`/api/v1/services/deploy/${$page.params.name}/activate`, $session, {
|
||||
method: 'PATCH',
|
||||
body: {}
|
||||
});
|
||||
browser && toast.push(`All users are activated for Plausible.`);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
browser && toast.push(`Ooops, there was an error activating users for Plausible?!`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#await loadServiceConfig()}
|
||||
<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>{$page.params.name === 'plausible' ? 'Plausible Analytics' : $page.params.name}</div>
|
||||
<div class="px-4">
|
||||
{#if $page.params.name === 'plausible'}
|
||||
<img
|
||||
alt="plausible logo"
|
||||
class="w-6 mx-auto"
|
||||
src="https://cdn.coollabs.io/assets/coolify/services/plausible/logo_sm.png"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<a
|
||||
target="_blank"
|
||||
class="icon mx-2"
|
||||
href={service.config.baseURL}
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
<div class="space-y-2 max-w-4xl mx-auto px-6" in:fade={{ duration: 100 }}>
|
||||
<div class="block text-center py-4">
|
||||
{#if $page.params.name === 'plausible'}
|
||||
<Plausible {service} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/await}
|
||||
42
src/routes/service/new/[type]/__layout.svelte
Normal file
42
src/routes/service/new/[type]/__layout.svelte
Normal file
@@ -0,0 +1,42 @@
|
||||
<script>
|
||||
import { browser } from '$app/env';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { page, session } from '$app/stores';
|
||||
import Loading from '$components/Loading.svelte';
|
||||
import { request } from '$lib/api/request';
|
||||
import { initialNewService, newService } from '$store';
|
||||
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
async function checkService() {
|
||||
try {
|
||||
const data = await request(`/api/v1/services/${$page.params.type}`, $session);
|
||||
if (!data?.success) {
|
||||
if (browser) {
|
||||
goto(`/dashboard/services`, { replaceState: true });
|
||||
toast.push(
|
||||
`${
|
||||
$page.params.type === 'plausible' ? 'Plausible Analytics' : $page.params.type
|
||||
} already deployed.`
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
}
|
||||
onDestroy(() => {
|
||||
$newService = JSON.parse(JSON.stringify(initialNewService));
|
||||
});
|
||||
</script>
|
||||
|
||||
{#await checkService()}
|
||||
<Loading />
|
||||
{:then}
|
||||
<div class="text-white">
|
||||
<slot />
|
||||
</div>
|
||||
{/await}
|
||||
130
src/routes/service/new/[type]/index.svelte
Normal file
130
src/routes/service/new/[type]/index.svelte
Normal file
@@ -0,0 +1,130 @@
|
||||
<script>
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { newService } from '$store';
|
||||
import { page, session } from '$app/stores';
|
||||
import { request } from '$lib/api/request';
|
||||
import { goto } from '$app/navigation';
|
||||
import Loading from '$components/Loading.svelte';
|
||||
import TooltipInfo from '$components/TooltipInfo.svelte';
|
||||
import { browser } from '$app/env';
|
||||
|
||||
$: deployable =
|
||||
$newService.baseURL === '' ||
|
||||
$newService.baseURL === null ||
|
||||
$newService.email === '' ||
|
||||
$newService.email === null ||
|
||||
$newService.userName === '' ||
|
||||
$newService.userName === null ||
|
||||
$newService.userPassword === '' ||
|
||||
$newService.userPassword === null ||
|
||||
$newService.userPassword.length <= 6 ||
|
||||
$newService.userPassword !== $newService.userPasswordAgain;
|
||||
let loading = false;
|
||||
async function deploy() {
|
||||
try {
|
||||
loading = true;
|
||||
const payload = $newService;
|
||||
delete payload.userPasswordAgain;
|
||||
await request(`/api/v1/services/deploy/${$page.params.type}`, $session, {
|
||||
body: payload
|
||||
});
|
||||
if (browser) {
|
||||
toast.push(
|
||||
'Service deployment queued.<br><br><br>It could take 2-5 minutes to be ready, be patient and grab a coffee/tea!',
|
||||
{ duration: 4000 }
|
||||
);
|
||||
goto(`/dashboard/services`, { replaceState: true });
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
browser && toast.push('Oops something went wrong. See console.log.');
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="min-h-full text-white">
|
||||
<div class="py-5 text-left px-6 text-3xl tracking-tight font-bold">
|
||||
Deploy new
|
||||
{#if $page.params.type === 'plausible'}
|
||||
<span class="text-blue-500 px-2 capitalize">Plausible Analytics</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if loading}
|
||||
<Loading />
|
||||
{:else}
|
||||
<div class="space-y-2 max-w-4xl mx-auto px-6 flex-col text-center" in:fade={{ duration: 100 }}>
|
||||
<div class="grid grid-flow-row">
|
||||
<label for="Domain"
|
||||
>Domain <TooltipInfo
|
||||
position="right"
|
||||
label={`You will have your Plausible instance at here.`}
|
||||
/></label
|
||||
>
|
||||
<input
|
||||
id="Domain"
|
||||
class:border-red-500={$newService.baseURL == null || $newService.baseURL == ''}
|
||||
bind:value={$newService.baseURL}
|
||||
placeholder="analytics.coollabs.io"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-flow-row">
|
||||
<label for="Email">Email</label>
|
||||
<input
|
||||
id="Email"
|
||||
class:border-red-500={$newService.email == null || $newService.email == ''}
|
||||
bind:value={$newService.email}
|
||||
placeholder="hi@coollabs.io"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-flow-row">
|
||||
<label for="Username">Username </label>
|
||||
<input
|
||||
id="Username"
|
||||
class:border-red-500={$newService.userName == null || $newService.userName == ''}
|
||||
bind:value={$newService.userName}
|
||||
placeholder="admin"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-flow-row">
|
||||
<label for="Password"
|
||||
>Password <TooltipInfo position="right" label={`Must be at least 7 characters.`} /></label
|
||||
>
|
||||
<input
|
||||
id="Password"
|
||||
type="password"
|
||||
class:border-red-500={$newService.userPassword == null ||
|
||||
$newService.userPassword == '' ||
|
||||
$newService.userPassword.length <= 6}
|
||||
bind:value={$newService.userPassword}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-flow-row pb-5">
|
||||
<label for="PasswordAgain">Password again </label>
|
||||
<input
|
||||
id="PasswordAgain"
|
||||
type="password"
|
||||
class:placeholder-red-500={$newService.userPassword !== $newService.userPasswordAgain}
|
||||
class:border-red-500={$newService.userPassword !== $newService.userPasswordAgain}
|
||||
bind:value={$newService.userPasswordAgain}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
disabled={deployable}
|
||||
class:cursor-not-allowed={deployable}
|
||||
class:bg-blue-500={!deployable}
|
||||
class:hover:bg-blue-400={!deployable}
|
||||
class:hover:bg-transparent={deployable}
|
||||
class:text-warmGray-700={deployable}
|
||||
class:text-white={!deployable}
|
||||
class="button p-2"
|
||||
on:click={deploy}
|
||||
>
|
||||
Deploy
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
29
src/routes/service/new/index.svelte
Normal file
29
src/routes/service/new/index.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
import { fade } from 'svelte/transition';
|
||||
</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 service
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center space-y-2 max-w-4xl mx-auto px-6" in:fade={{ duration: 100 }}>
|
||||
{#if $page.path === '/service/new'}
|
||||
<div class="flex justify-center space-x-4 font-bold pb-6">
|
||||
<div
|
||||
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-blue-500 p-2 rounded bg-warmGray-800"
|
||||
on:click={() => goto('/service/new/plausible')}
|
||||
>
|
||||
<img
|
||||
alt="plausible logo"
|
||||
class="w-12 mx-auto"
|
||||
src="https://cdn.coollabs.io/assets/coolify/services/plausible/logo_sm.png"
|
||||
/>
|
||||
<div class="text-white">Plausible Analytics</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
190
src/routes/settings.svelte
Normal file
190
src/routes/settings.svelte
Normal file
@@ -0,0 +1,190 @@
|
||||
<script context="module">
|
||||
/**
|
||||
* @type {import('@sveltejs/kit').Load}
|
||||
*/
|
||||
export async function load({ fetch }) {
|
||||
try {
|
||||
const { allowRegistration, sendErrors } = await (await fetch(`/api/v1/settings`)).json();
|
||||
return {
|
||||
props: {
|
||||
allowRegistration,
|
||||
sendErrors
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
props: {
|
||||
allowRegistration: null,
|
||||
sendErrors: null
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export let allowRegistration;
|
||||
export let sendErrors;
|
||||
import { browser } from '$app/env';
|
||||
import { session } from '$app/stores';
|
||||
|
||||
import { request } from '$lib/api/request';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { fade } from 'svelte/transition';
|
||||
let settings = {
|
||||
allowRegistration,
|
||||
sendErrors
|
||||
};
|
||||
async function changeSettings(value) {
|
||||
try {
|
||||
settings[value] = !settings[value];
|
||||
await request(`/api/v1/settings`, $session, {
|
||||
body: {
|
||||
...settings
|
||||
}
|
||||
});
|
||||
browser && toast.push('Configuration saved.');
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="min-h-full text-white" in:fade={{ duration: 100 }}>
|
||||
<div class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center">
|
||||
<div>Settings</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div in:fade={{ duration: 100 }}>
|
||||
<div class="max-w-4xl mx-auto px-6 pb-4">
|
||||
<div>
|
||||
<div class="text-2xl font-bold border-gradient w-32 pt-4 text-white">General</div>
|
||||
<div class=" pt-4">
|
||||
<div class="px-4 sm:px-6">
|
||||
<ul class="mt-2 divide-y divide-warmGray-800">
|
||||
<li class="py-4 flex items-center justify-between">
|
||||
<div class="flex flex-col">
|
||||
<p class="text-base font-bold text-warmGray-100">Registration allowed?</p>
|
||||
<p class="text-sm font-medium text-warmGray-400">
|
||||
Allow further registrations to the application. It's turned off after the first
|
||||
registration.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => changeSettings('allowRegistration')}
|
||||
aria-pressed="false"
|
||||
class="relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200"
|
||||
class:bg-green-600={settings?.allowRegistration}
|
||||
class:bg-warmGray-700={!settings?.allowRegistration}
|
||||
>
|
||||
<span class="sr-only">Use setting</span>
|
||||
<span
|
||||
class="pointer-events-none relative inline-block h-5 w-5 rounded-full bg-white shadow transform transition ease-in-out duration-200"
|
||||
class:translate-x-5={settings?.allowRegistration}
|
||||
class:translate-x-0={!settings?.allowRegistration}
|
||||
>
|
||||
<span
|
||||
class=" ease-in duration-200 absolute inset-0 h-full w-full flex items-center justify-center transition-opacity"
|
||||
class:opacity-0={settings?.allowRegistration}
|
||||
class:opacity-100={!settings?.allowRegistration}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg class="bg-white h-3 w-3 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="ease-out duration-100 absolute inset-0 h-full w-full flex items-center justify-center transition-opacity"
|
||||
aria-hidden="true"
|
||||
class:opacity-100={settings?.allowRegistration}
|
||||
class:opacity-0={!settings?.allowRegistration}
|
||||
>
|
||||
<svg
|
||||
class="bg-white h-3 w-3 text-green-600"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 12 12"
|
||||
>
|
||||
<path
|
||||
d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="py-4 flex items-center justify-between">
|
||||
<div class="flex flex-col">
|
||||
<p class="text-base font-bold text-warmGray-100">Send errors automatically?</p>
|
||||
<p class="text-sm font-medium text-warmGray-400">
|
||||
Allow to send errors automatically to developer(s) at coolLabs (<a
|
||||
href="https://twitter.com/andrasbacsai"
|
||||
target="_blank"
|
||||
class="underline text-white font-bold hover:text-blue-400">Andras Bacsai</a
|
||||
>). This will help to fix bugs quicker. 🙏
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => changeSettings('sendErrors')}
|
||||
aria-pressed="true"
|
||||
class="relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200"
|
||||
class:bg-green-600={settings?.sendErrors}
|
||||
class:bg-warmGray-700={!settings?.sendErrors}
|
||||
>
|
||||
<span class="sr-only">Use setting</span>
|
||||
<span
|
||||
class="pointer-events-none relative inline-block h-5 w-5 rounded-full bg-white shadow transform transition ease-in-out duration-200"
|
||||
class:translate-x-5={settings?.sendErrors}
|
||||
class:translate-x-0={!settings?.sendErrors}
|
||||
>
|
||||
<span
|
||||
class=" ease-in duration-200 absolute inset-0 h-full w-full flex items-center justify-center transition-opacity"
|
||||
class:opacity-0={settings?.sendErrors}
|
||||
class:opacity-100={!settings?.sendErrors}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg class="bg-white h-3 w-3 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="ease-out duration-100 absolute inset-0 h-full w-full flex items-center justify-center transition-opacity"
|
||||
aria-hidden="true"
|
||||
class:opacity-100={settings?.sendErrors}
|
||||
class:opacity-0={!settings?.sendErrors}
|
||||
>
|
||||
<svg
|
||||
class="bg-white h-3 w-3 text-green-600"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 12 12"
|
||||
>
|
||||
<path
|
||||
d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
13
src/routes/success.svelte
Normal file
13
src/routes/success.svelte
Normal file
@@ -0,0 +1,13 @@
|
||||
<script>
|
||||
import { browser } from '$app/env';
|
||||
if (browser) {
|
||||
setTimeout(() => {
|
||||
window.close();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="flex w-full h-screen justify-center items-center">
|
||||
<div class="text-3xl font-bold">Succesfully logged in! 🎉</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user