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:
Andras Bacsai
2021-05-14 21:51:14 +02:00
committed by GitHub
parent cccb9a5fec
commit 23a4ebb74a
229 changed files with 7781 additions and 11333 deletions

View File

@@ -1,82 +0,0 @@
<script>
import { SvelteToast } from "@zerodevx/svelte-toast";
import { Router } from "@roxi/routify";
import { routes } from "../.routify/routes";
const options = {
duration: 2000
};
</script>
<style lang="postcss">
:global(.main) {
width: calc(100% - 4rem);
margin-left: 4rem;
}
:global(._toastMsg) {
@apply text-sm font-bold !important;
}
:global(._toastItem) {
@apply w-full border-l-2 border-green-600 !important;
}
:global(._toastBtn) {
@apply text-xs !important;
}
:global(._toastBtn:hover) {
@apply bg-gray-500 !important;
}
:global(.icon) {
@apply text-white rounded p-2 transition duration-100 !important;
}
:global(.icon:hover) {
@apply bg-warmGray-700 !important;
}
:global(input) {
@apply text-sm rounded py-2 px-6 font-bold bg-warmGray-800 text-white transition duration-150 outline-none border border-transparent !important;
}
:global(input:hover) {
@apply bg-warmGray-700 !important;
}
:global(textarea) {
@apply text-sm rounded py-2 px-6 font-bold bg-warmGray-800 text-white transition duration-150 outline-none resize-none !important;
}
:global(textarea:hover) {
@apply bg-warmGray-700 !important;
}
:global(select) {
@apply text-sm rounded py-2 px-6 font-bold bg-warmGray-800 text-white transition duration-150 outline-none !important;
}
:global(select:hover) {
@apply bg-warmGray-700 !important;
}
:global(label) {
@apply text-left text-base font-bold text-warmGray-400 !important;
}
:global(button) {
@apply outline-none !important;
}
:global(.button) {
@apply rounded text-sm font-bold transition-all duration-100 !important;
}
:global(.h-271) {
min-height: 271px !important;
}
:global(.repository-select-search .listItem .item),
:global(.repository-select-search .empty) {
@apply text-sm py-3 font-bold bg-warmGray-800 text-white cursor-pointer border-none hover:bg-warmGray-700 !important;
}
:global(.repository-select-search .listContainer) {
@apply bg-transparent !important;
}
:global(.repository-select-search .clearSelect) {
@apply text-white cursor-pointer !important;
}
:global(.repository-select-search .selectedItem) {
@apply text-white relative cursor-pointer font-bold text-sm flex items-center !important;
}
</style>
<SvelteToast options="{options}" />
<Router routes="{routes}" />

17
src/app.html Normal file
View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Coolify</title>
<link rel="dns-prefetch" href="https://cdn.coollabs.io/" />
<link rel="preconnect" href="https://cdn.coollabs.io/" crossorigin="" />
<link rel="stylesheet" href="https://cdn.coollabs.io/fonts/montserrat/montserrat.css" />
<link rel="stylesheet" href="https://cdn.coollabs.io/css/microtip-0.2.2.min.css" />
%svelte.head%
</head>
<body>
<div id="svelte">%svelte.body%</div>
</body>
</html>

141
src/app.postcss Normal file
View File

@@ -0,0 +1,141 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html {
height: 100%;
}
body {
background-color: rgb(22, 22, 22);
min-height: 100vh;
overflow-x: hidden;
@apply text-white;
}
:root {
--toastBackground: rgba(41, 37, 36, 0.8);
--toastProgressBackground: transparent;
--toastFont: 'Inter';
}
.border-gradient {
border-bottom: 2px solid transparent;
border-image: linear-gradient(
0.25turn,
rgba(255, 249, 34),
rgba(255, 0, 128),
rgba(56, 2, 155, 0)
);
border-image-slice: 1;
}
.border-gradient-full {
border: 4px solid transparent;
border-image: linear-gradient(
0.25turn,
rgba(255, 249, 34),
rgba(255, 0, 128),
rgba(56, 2, 155, 0)
);
border-image-slice: 1;
}
[aria-label][role~='tooltip']::after {
background: rgba(41, 37, 36, 0.9);
color: white;
font-family: 'Inter';
font-size: 16px;
font-weight: 600;
white-space: normal;
}
[role~='tooltip'][data-microtip-position|='bottom']::before {
background: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba%2841,%2037,%2036,%200.9%29%22%20transform%3D%22rotate%28180%2018%206%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E')
no-repeat;
}
[role~='tooltip'][data-microtip-position|='top']::before {
background: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba%2841,%2037,%2036,%200.9%29%22%20transform%3D%22rotate%280%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E')
no-repeat;
}
[role~='tooltip'][data-microtip-position='right']::before {
background: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2212px%22%20height%3D%2236px%22%3E%3Cpath%20fill%3D%22rgba%2841,%2037,%2036,%200.9%29%22%20transform%3D%22rotate%2890%206%206%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E')
no-repeat;
}
[role~='tooltip'][data-microtip-position='left']::before {
background: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2212px%22%20height%3D%2236px%22%3E%3Cpath%20fill%3D%22rgba%2841,%2037,%2036,%200.9%29%22%20transform%3D%22rotate%28-90%2018%2018%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E')
no-repeat;
}
.main {
width: calc(100% - 4rem);
margin-left: 4rem;
}
._toastMsg {
@apply text-sm font-bold !important;
}
._toastItem {
@apply w-full border-l-2 border-green-600 !important;
}
._toastBtn {
@apply text-xs !important;
}
._toastBtn:hover {
@apply bg-gray-500 !important;
}
.icon {
@apply text-white rounded p-2 transition duration-100;
}
.icon:hover {
@apply bg-warmGray-700;
}
input {
@apply text-sm rounded py-2 px-6 font-bold bg-warmGray-800 text-white transition duration-150 outline-none border border-transparent !important;
}
input:hover {
@apply bg-warmGray-700 !important;
}
textarea {
@apply text-sm rounded py-2 px-6 font-bold bg-warmGray-800 text-white transition duration-150 outline-none resize-none !important;
}
textarea:hover {
@apply bg-warmGray-700 !important;
}
select {
@apply text-sm rounded py-2 px-6 font-bold bg-warmGray-800 text-white transition duration-150 outline-none !important;
}
select:hover {
@apply bg-warmGray-700 !important;
}
label {
@apply text-left text-base font-bold text-warmGray-400 !important;
}
button {
@apply outline-none !important;
}
.button {
@apply rounded text-sm font-bold transition-all duration-100 !important;
}
.h-271 {
min-height: 271px !important;
}
.repository-select-search .listItem .item,
.repository-select-search .empty {
@apply text-sm py-3 font-bold bg-warmGray-800 text-white cursor-pointer border-none hover:bg-warmGray-700 !important;
}
.repository-select-search .listContainer {
@apply bg-transparent !important;
}
.repository-select-search .clearSelect {
@apply text-white cursor-pointer !important;
}
.repository-select-search .selectedItem {
@apply text-white relative cursor-pointer font-bold text-sm flex items-center !important;
}
.selectContainer {
background: transparent !important;
@apply border-0 !important;
}

View File

@@ -0,0 +1,312 @@
<script>
import { application } from '$store';
import { onMount } from 'svelte';
import TooltipInfo from '$components/TooltipInfo.svelte';
let domainInput;
const buildpacks = {
static: {
port: {
active: false,
number: 80
},
build: true
},
nodejs: {
port: {
active: true,
number: 3000
},
build: true
},
vuejs: {
port: {
active: false,
number: 80
},
build: true
},
nuxtjs: {
port: {
active: true,
number: 3000
},
build: true
},
react: {
port: {
active: false,
number: 80
},
build: true
},
nextjs: {
port: {
active: true,
number: 3000
},
build: true
},
gatsby: {
port: {
active: true,
number: 3000
},
build: true
},
svelte: {
port: {
active: false,
number: 80
},
build: true
},
php: {
port: {
active: false,
number: 80
},
build: false
},
rust: {
port: {
active: true,
number: 3000
},
build: false
},
docker: {
port: {
active: true,
number: 3000
},
build: false
}
};
function selectBuildPack(event) {
if (event.target.innerText === 'React/Preact') {
$application.build.pack = 'react';
} else {
$application.build.pack = event.target.innerText.replace(/\./g, '').toLowerCase();
}
}
onMount(() => {
if(!$application.publish.domain) domainInput.focus();
});
</script>
<div>
<div class="grid grid-cols-1 text-sm max-w-4xl md:mx-auto mx-6 pb-16 auto-cols-max ">
<div class="text-2xl font-bold border-gradient w-40">Build Packs</div>
<div class="flex font-bold flex-wrap justify-center pt-10">
<div
class={$application.build.pack === 'static'
? 'buildpack bg-red-500'
: 'buildpack hover:border-red-500'}
on:click={selectBuildPack}
>
Static
</div>
<div
class={$application.build.pack === 'nodejs'
? 'buildpack bg-emerald-600'
: 'buildpack hover:border-emerald-600'}
on:click={selectBuildPack}
>
NodeJS
</div>
<div
class={$application.build.pack === 'vuejs'
? 'buildpack bg-green-500'
: 'buildpack hover:border-green-500'}
on:click={selectBuildPack}
>
VueJS
</div>
<div
class={$application.build.pack === 'nuxtjs'
? 'buildpack bg-green-500'
: 'buildpack hover:border-green-500'}
on:click={selectBuildPack}
>
NuxtJS
</div>
<div
class={$application.build.pack === 'react'
? 'buildpack bg-gradient-to-r from-blue-500 to-purple-500'
: 'buildpack hover:border-blue-500'}
on:click={selectBuildPack}
>
React/Preact
</div>
<div
class={$application.build.pack === 'nextjs'
? 'buildpack bg-blue-500'
: 'buildpack hover:border-blue-500'}
on:click={selectBuildPack}
>
NextJS
</div>
<div
class={$application.build.pack === 'gatsby'
? 'buildpack bg-blue-500'
: 'buildpack hover:border-blue-500'}
on:click={selectBuildPack}
>
Gatsby
</div>
<div
class={$application.build.pack === 'svelte'
? 'buildpack bg-orange-600'
: 'buildpack hover:border-orange-600'}
on:click={selectBuildPack}
>
Svelte
</div>
<div
class={$application.build.pack === 'php'
? 'buildpack bg-indigo-500'
: 'buildpack hover:border-indigo-500'}
on:click={selectBuildPack}
>
PHP
</div>
<div
class={$application.build.pack === 'rust'
? 'buildpack bg-pink-500'
: 'buildpack hover:border-pink-500'}
on:click={selectBuildPack}
>
Rust
</div>
<div
class={$application.build.pack === 'docker'
? 'buildpack bg-purple-500'
: 'buildpack hover:border-purple-500'}
on:click={selectBuildPack}
>
Docker
</div>
</div>
</div>
<div class="text-2xl font-bold border-gradient w-52">General settings</div>
<div class="grid grid-cols-1 max-w-2xl md:mx-auto mx-6 justify-center items-center pt-10">
<div class="grid grid-flow-col gap-2 items-center pb-6">
<div class="grid grid-flow-row">
<label for="Domain" class="">Domain</label>
<input
bind:this={domainInput}
class="border-2"
class:placeholder-red-500={$application.publish.domain == null ||
$application.publish.domain == ''}
class:border-red-500={$application.publish.domain == null ||
$application.publish.domain == ''}
id="Domain"
bind:value={$application.publish.domain}
placeholder="eg: coollabs.io (without www)"
/>
</div>
<div class="grid grid-flow-row">
<label for="Path"
>Path <TooltipInfo
label={`Path to deploy your application on your domain. eg: /api means it will be deployed to -> https://${
$application.publish.domain || '<yourdomain>'
}/api`}
/></label
>
<input id="Path" bind:value={$application.publish.path} placeholder="/" />
</div>
</div>
<label for="Port" class:text-warmGray-800={!buildpacks[$application.build.pack].port.active}
>Port</label
>
<input
disabled={!buildpacks[$application.build.pack].port.active}
id="Port"
class:bg-warmGray-900={!buildpacks[$application.build.pack].port.active}
class:text-warmGray-900={!buildpacks[$application.build.pack].port.active}
class:placeholder-warmGray-800={!buildpacks[$application.build.pack].port.active}
class:hover:bg-warmGray-900={!buildpacks[$application.build.pack].port.active}
class:cursor-not-allowed={!buildpacks[$application.build.pack].port.active}
bind:value={$application.publish.port}
placeholder={buildpacks[$application.build.pack].port.number}
/>
<div class="grid grid-flow-col gap-2 items-center pt-6 pb-12">
<div class="grid grid-flow-row">
<label for="baseDir"
>Base Directory <TooltipInfo
label="The directory to use as base for every command (could be useful if you have a monorepo)."
/></label
>
<input id="baseDir" bind:value={$application.build.directory} placeholder="eg: sourcedir" />
</div>
<div class="grid grid-flow-row">
<label for="publishDir"
>Publish Directory <TooltipInfo
label="The directory to deploy after running the build command. eg: dist, _site, public."
/></label
>
<input
id="publishDir"
bind:value={$application.publish.directory}
placeholder="eg: dist, _site, public"
/>
</div>
</div>
</div>
<div
class="text-2xl font-bold w-40"
class:border-gradient={buildpacks[$application.build.pack].build}
class:text-warmGray-800={!buildpacks[$application.build.pack].build}
>
Commands
</div>
<div class=" max-w-2xl md:mx-auto mx-6 justify-center items-center pt-10 pb-32">
<div class="grid grid-flow-col gap-2 items-center">
<div class="grid grid-flow-row">
<label
for="installCommand"
class:text-warmGray-800={!buildpacks[$application.build.pack].build}
>Install Command <TooltipInfo
label="Command to run for installing dependencies. eg: yarn install."
/>
</label>
<input
class="mb-6"
class:bg-warmGray-900={!buildpacks[$application.build.pack].build}
class:text-warmGray-900={!buildpacks[$application.build.pack].build}
class:placeholder-warmGray-800={!buildpacks[$application.build.pack].build}
class:hover:bg-warmGray-900={!buildpacks[$application.build.pack].build}
class:cursor-not-allowed={!buildpacks[$application.build.pack].build}
id="installCommand"
bind:value={$application.build.command.installation}
placeholder="eg: yarn install"
/>
<label
for="buildCommand"
class:text-warmGray-800={!buildpacks[$application.build.pack].build}
>Build Command <TooltipInfo
label="Command to run for building your application. If empty, no build phase initiated in the deploy process."
/></label
>
<input
class="mb-6"
class:bg-warmGray-900={!buildpacks[$application.build.pack].build}
class:text-warmGray-900={!buildpacks[$application.build.pack].build}
class:placeholder-warmGray-800={!buildpacks[$application.build.pack].build}
class:hover:bg-warmGray-900={!buildpacks[$application.build.pack].build}
class:cursor-not-allowed={!buildpacks[$application.build.pack].build}
id="buildCommand"
bind:value={$application.build.command.build}
placeholder="eg: yarn build"
/>
</div>
</div>
</div>
</div>
<style lang="postcss">
.buildpack {
@apply px-6 py-2 mx-2 my-2 bg-warmGray-800 w-48 ease-in-out transform hover:scale-105 text-center rounded border-2 border-transparent border-dashed cursor-pointer transition duration-100;
}
</style>

View File

@@ -1,5 +1,5 @@
<script>
import { application } from "@store";
import { application } from "$store";
let secret = {
name: null,

View File

@@ -0,0 +1,46 @@
<script>
import { page } from '$app/stores';
export let loading, branches;
import { application } from '$store';
import Select from 'svelte-select';
const selectedValue = $page.path !== '/application/new' && $application.repository.branch;
function handleSelect(event) {
$application.repository.branch = null;
setTimeout(() => {
$application.repository.branch = event.detail.value;
}, 1);
}
</script>
{#if loading}
<div class="grid grid-cols-1">
<label for="branch">Branch</label>
<div class="repository-select-search col-span-2">
<Select
containerClasses="w-full border-none bg-transparent"
placeholder="Loading branches..."
isDisabled
/>
</div>
</div>
{:else}
<div class="grid grid-cols-1">
<label for="branch">Branch</label>
<div class="repository-select-search col-span-2">
<Select
containerClasses="w-full border-none bg-transparent"
on:select={handleSelect}
{selectedValue}
isClearable={false}
items={branches.map((b) => ({ label: b.name, value: b.name }))}
showIndicator={$page.path === '/application/new'}
noOptionsMessage="No branches found"
placeholder="Select a branch"
isDisabled={$page.path !== '/application/new'}
/>
</div>
</div>
{/if}

View File

@@ -0,0 +1,186 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { request } from '$lib/api/request';
import { session } from '$app/stores';
import { githubRepositories, application, githubInstallations } from '$store';
import { fade } from 'svelte/transition';
import Loading from '$components/Loading.svelte';
import { browser } from '$app/env';
import Branches from '$components/Application/Branches.svelte';
import Tabs from '$components/Application/Tabs.svelte';
import Repositories from '$components/Application/Repositories.svelte';
import Login from '$components/Application/Login.svelte';
let loading = {
github: false,
branches: false
};
let branches = [];
let relogin = false;
function dashify(str: string, options?: any) {
if (typeof str !== 'string') return str;
return str
.trim()
.replace(/\W/g, (m) => (/[À-ž]/.test(m) ? m : '-'))
.replace(/^-+|-+$/g, '')
.replace(/-{2,}/g, (m) => (options && options.condense ? '-' : m))
.toLowerCase();
}
async function getGithubRepos(id, page) {
return await request(
`https://api.github.com/user/installations/${id}/repositories?per_page=100&page=${page}`,
$session
);
}
async function loadGithubRepositories(force) {
if ($githubRepositories.length > 0 && !force) {
$application.github.installation.id = $githubInstallations.id;
$application.github.app.id = $githubInstallations.app_id;
const foundRepositoryOnGithub = $githubRepositories.find(
(r) =>
r.full_name === `${$application.repository.organization}/${$application.repository.name}`
);
if (foundRepositoryOnGithub) {
$application.repository.id = foundRepositoryOnGithub.id;
$application.repository.organization = foundRepositoryOnGithub.owner.login;
$application.repository.name = foundRepositoryOnGithub.name;
}
return;
} else {
loading.github = true;
let installations = [];
try {
const data = await request('https://api.github.com/user/installations', $session);
installations = data.installations;
} catch (error) {
relogin = true;
console.log(error);
return false;
}
if (installations.length === 0) {
relogin = true;
return false;
}
$application.github.installation.id = installations[0].id;
$application.github.app.id = installations[0].app_id;
$githubInstallations = installations[0];
try {
let page = 1;
let userRepos = 0;
const data = await getGithubRepos($application.github.installation.id, page);
$githubRepositories = $githubRepositories.concat(data.repositories);
userRepos = data.total_count;
if (userRepos > $githubRepositories.length) {
while (userRepos > $githubRepositories.length) {
page = page + 1;
const repos = await getGithubRepos($application.github.installation.id, page);
$githubRepositories = $githubRepositories.concat(repos.repositories);
}
}
const foundRepositoryOnGithub = $githubRepositories.find(
(r) =>
r.full_name ===
`${$application.repository.organization}/${$application.repository.name}`
);
if (foundRepositoryOnGithub) {
$application.repository.id = foundRepositoryOnGithub.id;
await loadBranches();
}
} catch (error) {
return false;
} finally {
loading.github = false;
}
}
}
async function loadBranches() {
loading.branches = true;
const selectedRepository = $githubRepositories.find((r) => r.id === $application.repository.id);
if (selectedRepository) {
$application.repository.organization = selectedRepository.owner.login;
$application.repository.name = selectedRepository.name;
}
branches = await request(
`https://api.github.com/repos/${$application.repository.organization}/${$application.repository.name}/branches`,
$session
);
loading.branches = false;
}
async function modifyGithubAppConfig() {
if (browser) {
const left = screen.width / 2 - 1020 / 2;
const top = screen.height / 2 - 618 / 2;
const newWindow = open(
`https://github.com/apps/${dashify(
import.meta.env.VITE_GITHUB_APP_NAME
)}/installations/new`,
'Install App',
'resizable=1, scrollbars=1, fullscreen=0, height=1000, width=1020,top=' +
top +
', left=' +
left +
', toolbar=0, menubar=0, status=0'
);
const timer = setInterval(async () => {
if (newWindow.closed) {
clearInterval(timer);
loading.github = true;
if ($application.repository.name) {
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 };
} catch (error) {
browser && goto('/dashboard/applications', { replaceState: true });
}
}
branches = [];
$githubRepositories = [];
await loadGithubRepositories(true);
}
}, 100);
}
}
</script>
<div in:fade={{ duration: 100 }}>
{#if relogin}
<Login />
{:else}
{#await loadGithubRepositories(false)}
<Loading github githubLoadingText="Loading repositories..." />
{:then}
{#if loading.github}
<Loading github githubLoadingText="Loading repositories..." />
{:else}
<div class="space-y-2 max-w-4xl mx-auto px-6" in:fade={{ duration: 100 }}>
<Repositories
on:loadBranches={loadBranches}
on:modifyGithubAppConfig={modifyGithubAppConfig}
/>
{#if $application.repository.organization}
<Branches loading={loading.branches} {branches} />
{/if}
{#if $application.repository.branch}
<Tabs />
{/if}
</div>
{/if}
{/await}
{/if}
</div>

View File

@@ -1,340 +0,0 @@
<style lang="postcss">
.buildpack {
@apply px-6 py-2 mx-2 my-2 bg-warmGray-800 w-48 ease-in-out transform hover:scale-105 text-center rounded border-2 border-transparent border-dashed cursor-pointer transition duration-100;
}
</style>
<script>
import { application } from "@store";
import { onMount } from "svelte";
import TooltipInfo from "../../../Tooltip/TooltipInfo.svelte";
let domainInput;
const buildpacks = {
static: {
port: {
active: false,
number: 80,
},
build: true,
},
nodejs: {
port: {
active: true,
number: 3000,
},
build: true,
},
vuejs: {
port: {
active: false,
number: 80,
},
build: true,
},
nuxtjs: {
port: {
active: true,
number: 3000,
},
build: true,
},
react: {
port: {
active: false,
number: 80,
},
build: true,
},
nextjs: {
port: {
active: true,
number: 3000,
},
build: true,
},
gatsby: {
port: {
active: true,
number: 3000,
},
build: true,
},
svelte: {
port: {
active: false,
number: 80,
},
build: true,
},
php: {
port: {
active: false,
number: 80,
},
build: false,
},
rust: {
port: {
active: true,
number: 3000,
},
build: false,
},
docker: {
port: {
active: true,
number: 3000,
},
build: false,
},
};
function selectBuildPack(event) {
if (event.target.innerText === "React/Preact") {
$application.build.pack = "react";
} else {
$application.build.pack = event.target.innerText
.replace(/\./g, "")
.toLowerCase();
}
}
onMount(()=> {
domainInput.focus();
})
</script>
<div>
<div
class="grid grid-cols-1 text-sm max-w-4xl md:mx-auto mx-6 pb-16 auto-cols-max "
>
<div class="text-2xl font-bold border-gradient w-40">Build Packs</div>
<div class="flex font-bold flex-wrap justify-center pt-10">
<div
class="{$application.build.pack === 'static'
? 'buildpack bg-red-500'
: 'buildpack hover:border-red-500'}"
on:click="{selectBuildPack}"
>
Static
</div>
<div
class="{$application.build.pack === 'nodejs'
? 'buildpack bg-emerald-600'
: 'buildpack hover:border-emerald-600'}"
on:click="{selectBuildPack}"
>
NodeJS
</div>
<div
class="{$application.build.pack === 'vuejs'
? 'buildpack bg-green-500'
: 'buildpack hover:border-green-500'}"
on:click="{selectBuildPack}"
>
VueJS
</div>
<div
class="{$application.build.pack === 'nuxtjs'
? 'buildpack bg-green-500'
: 'buildpack hover:border-green-500'}"
on:click="{selectBuildPack}"
>
NuxtJS
</div>
<div
class="{$application.build.pack === 'react'
? 'buildpack bg-gradient-to-r from-blue-500 to-purple-500'
: 'buildpack hover:border-blue-500'}"
on:click="{selectBuildPack}"
>
React/Preact
</div>
<div
class="{$application.build.pack === 'nextjs'
? 'buildpack bg-blue-500'
: 'buildpack hover:border-blue-500'}"
on:click="{selectBuildPack}"
>
NextJS
</div>
<div
class="{$application.build.pack === 'gatsby'
? 'buildpack bg-blue-500'
: 'buildpack hover:border-blue-500'}"
on:click="{selectBuildPack}"
>
Gatsby
</div>
<div
class="{$application.build.pack === 'svelte'
? 'buildpack bg-orange-600'
: 'buildpack hover:border-orange-600'}"
on:click="{selectBuildPack}"
>
Svelte
</div>
<div
class="{$application.build.pack === 'php'
? 'buildpack bg-indigo-500'
: 'buildpack hover:border-indigo-500'}"
on:click="{selectBuildPack}"
>
PHP
</div>
<div
class="{$application.build.pack === 'rust'
? 'buildpack bg-pink-500'
: 'buildpack hover:border-pink-500'}"
on:click="{selectBuildPack}"
>
Rust
</div>
<div
class="{$application.build.pack === 'docker'
? 'buildpack bg-purple-500'
: 'buildpack hover:border-purple-500'}"
on:click="{selectBuildPack}"
>
Docker
</div>
</div>
</div>
<div class="text-2xl font-bold border-gradient w-52">General settings</div>
<div
class="grid grid-cols-1 max-w-2xl md:mx-auto mx-6 justify-center items-center pt-10"
>
<div class="grid grid-flow-col gap-2 items-center pb-6">
<div class="grid grid-flow-row">
<label for="Domain" class="">Domain</label>
<input
bind:this={domainInput}
class="border-2"
class:placeholder-red-500="{$application.publish.domain == null ||
$application.publish.domain == ''}"
class:border-red-500="{$application.publish.domain == null ||
$application.publish.domain == ''}"
id="Domain"
bind:value="{$application.publish.domain}"
placeholder="eg: coollabs.io (without www)"
/>
</div>
<div class="grid grid-flow-row">
<label for="Path"
>Path <TooltipInfo
label="{`Path to deploy your application on your domain. eg: /api means it will be deployed to -> https://${
$application.publish.domain || '<yourdomain>'
}/api`}"
/></label
>
<input
id="Path"
bind:value="{$application.publish.path}"
placeholder="/"
/>
</div>
</div>
<label
for="Port"
class:text-warmGray-800="{!buildpacks[$application.build.pack].port
.active}">Port</label
>
<input
disabled="{!buildpacks[$application.build.pack].port.active}"
id="Port"
class:bg-warmGray-900="{!buildpacks[$application.build.pack].port.active}"
class:text-warmGray-900="{!buildpacks[$application.build.pack].port
.active}"
class:placeholder-warmGray-800="{!buildpacks[$application.build.pack].port
.active}"
class:hover:bg-warmGray-900="{!buildpacks[$application.build.pack].port
.active}"
class:cursor-not-allowed="{!buildpacks[$application.build.pack].port
.active}"
bind:value="{$application.publish.port}"
placeholder="{buildpacks[$application.build.pack].port.number}"
/>
<div class="grid grid-flow-col gap-2 items-center pt-6 pb-12">
<div class="grid grid-flow-row">
<label for="baseDir"
>Base Directory <TooltipInfo
label="The directory to use as base for every command (could be useful if you have a monorepo)."
/></label
>
<input
id="baseDir"
bind:value="{$application.build.directory}"
placeholder="eg: sourcedir"
/>
</div>
<div class="grid grid-flow-row">
<label for="publishDir"
>Publish Directory <TooltipInfo
label="The directory to deploy after running the build command. eg: dist, _site, public."
/></label
>
<input
id="publishDir"
bind:value="{$application.publish.directory}"
placeholder="eg: dist, _site, public"
/>
</div>
</div>
</div>
<div
class="text-2xl font-bold w-40"
class:border-gradient="{buildpacks[$application.build.pack].build}"
class:text-warmGray-800="{!buildpacks[$application.build.pack].build}"
>
Commands
</div>
<div
class=" max-w-2xl md:mx-auto mx-6 justify-center items-center pt-10 pb-32"
>
<div class="grid grid-flow-col gap-2 items-center">
<div class="grid grid-flow-row">
<label
for="installCommand"
class:text-warmGray-800="{!buildpacks[$application.build.pack].build}"
>Install Command <TooltipInfo
label="Command to run for installing dependencies. eg: yarn install."
/>
</label>
<input
class="mb-6"
class:bg-warmGray-900="{!buildpacks[$application.build.pack].build}"
class:text-warmGray-900="{!buildpacks[$application.build.pack].build}"
class:placeholder-warmGray-800="{!buildpacks[$application.build.pack]
.build}"
class:hover:bg-warmGray-900="{!buildpacks[$application.build.pack]
.build}"
class:cursor-not-allowed="{!buildpacks[$application.build.pack]
.build}"
id="installCommand"
bind:value="{$application.build.command.installation}"
placeholder="eg: yarn install"
/>
<label
for="buildCommand"
class:text-warmGray-800="{!buildpacks[$application.build.pack].build}"
>Build Command <TooltipInfo
label="Command to run for building your application. If empty, no build phase initiated in the deploy process."
/></label
>
<input
class="mb-6"
class:bg-warmGray-900="{!buildpacks[$application.build.pack].build}"
class:text-warmGray-900="{!buildpacks[$application.build.pack].build}"
class:placeholder-warmGray-800="{!buildpacks[$application.build.pack]
.build}"
class:hover:bg-warmGray-900="{!buildpacks[$application.build.pack]
.build}"
class:cursor-not-allowed="{!buildpacks[$application.build.pack]
.build}"
id="buildCommand"
bind:value="{$application.build.command.build}"
placeholder="eg: yarn build"
/>
</div>
</div>
</div>
</div>

View File

@@ -1,45 +0,0 @@
<script>
export let loading, branches;
import { application, activePage } from "@store";
import Select from "svelte-select";
const selectedValue =
$activePage.application !== "new" && $application.repository.branch;
function handleSelect(event) {
$application.repository.branch = null;
setTimeout(() => {
$application.repository.branch = event.detail.value;
}, 1);
}
</script>
{#if loading}
<div class="grid grid-cols-1">
<label for="branch">Branch</label>
<div class="repository-select-search col-span-2">
<Select
containerClasses="w-full border-none bg-transparent"
placeholder="Loading branches..."
isDisabled
/>
</div>
</div>
{:else}
<div class="grid grid-cols-1">
<label for="branch">Branch</label>
<div class="repository-select-search col-span-2">
<Select
containerClasses="w-full border-none bg-transparent"
on:select="{handleSelect}"
selectedValue="{selectedValue}"
isClearable="{false}"
items="{branches.map(b => ({ label: b.name, value: b.name }))}"
showIndicator="{$activePage.new}"
noOptionsMessage="No branches found"
placeholder="Select a branch"
isDisabled="{!$activePage.new}"
/>
</div>
</div>
{/if}

View File

@@ -1,263 +0,0 @@
<script>
import { redirect, isActive } from "@roxi/routify";
import { fade } from "svelte/transition";
import {
session,
application,
fetch,
initialApplication,
githubRepositories,
githubInstallations,
activePage,
} from "@store";
import Login from "./Login.svelte";
import Loading from "../../Loading.svelte";
import Repositories from "./Repositories.svelte";
import Branches from "./Branches.svelte";
import Tabs from "./Tabs.svelte";
let loading = {
branches: false,
github: false,
};
let branches = [];
function dashify(str, options) {
if (typeof str !== "string") return str;
return str
.trim()
.replace(/\W/g, m => (/[À-ž]/.test(m) ? m : "-"))
.replace(/^-+|-+$/g, "")
.replace(/-{2,}/g, m => (options && options.condense ? "-" : m))
.toLowerCase();
}
async function loadBranches() {
loading.branches = true;
if ($activePage.new) $application.repository.branch = null;
const selectedRepository = $githubRepositories.find(
r => r.id === $application.repository.id,
);
if (selectedRepository) {
$application.repository.organization = selectedRepository.owner.login;
$application.repository.name = selectedRepository.name;
}
branches = await $fetch(
`https://api.github.com/repos/${$application.repository.organization}/${$application.repository.name}/branches`,
);
loading.branches = false;
}
async function getGithubRepos(id, page) {
const data = await $fetch(
`https://api.github.com/user/installations/${id}/repositories?per_page=100&page=${page}`,
);
return data;
}
async function loadGithub() {
if ($githubRepositories.length > 0) {
$application.github.installation.id = $githubInstallations.id;
$application.github.app.id = $githubInstallations.app_id;
const foundRepositoryOnGithub = $githubRepositories.find(
r =>
r.full_name ===
`${$application.repository.organization}/${$application.repository.name}`,
);
if (foundRepositoryOnGithub) {
$application.repository.id = foundRepositoryOnGithub.id;
$application.repository.organization = foundRepositoryOnGithub.owner.login;
$application.repository.name = foundRepositoryOnGithub.name;
// await loadBranches();
}
return;
}
loading.github = true;
try {
const { installations } = await $fetch(
"https://api.github.com/user/installations",
);
if (installations.length === 0) {
return false;
}
$application.github.installation.id = installations[0].id;
$application.github.app.id = installations[0].app_id;
$githubInstallations = installations[0];
let page = 1;
let userRepos = 0;
const data = await getGithubRepos(
$application.github.installation.id,
page,
);
$githubRepositories = $githubRepositories.concat(data.repositories);
userRepos = data.total_count;
if (userRepos > $githubRepositories.length) {
while (userRepos > $githubRepositories.length) {
page = page + 1;
const repos = await getGithubRepos(
$application.github.installation.id,
page,
);
$githubRepositories = $githubRepositories.concat(repos.repositories);
}
}
const foundRepositoryOnGithub = $githubRepositories.find(
r =>
r.full_name ===
`${$application.repository.organization}/${$application.repository.name}`,
);
if (foundRepositoryOnGithub) {
$application.repository.id = foundRepositoryOnGithub.id;
await loadBranches();
}
} catch (error) {
return false;
} finally {
loading.github = false;
}
}
function modifyGithubAppConfig() {
const left = screen.width / 2 - 1020 / 2;
const top = screen.height / 2 - 618 / 2;
const newWindow = open(
`https://github.com/apps/${dashify(
import.meta.env.VITE_GITHUB_APP_NAME,
)}/installations/new`,
"Install App",
"resizable=1, scrollbars=1, fullscreen=0, height=1000, width=1020,top=" +
top +
", left=" +
left +
", toolbar=0, menubar=0, status=0",
);
const timer = setInterval(async () => {
if (newWindow.closed) {
clearInterval(timer);
loading.github = true;
if (!$activePage.new) {
try {
const config = await $fetch(`/api/v1/config`, {
body: {
name: $application.repository.name,
organization: $application.repository.organization,
branch: $application.repository.branch,
},
});
$application = { ...config };
} catch (error) {
$redirect("/dashboard/applications");
}
} else {
$application = JSON.parse(JSON.stringify(initialApplication));
}
branches = [];
$githubRepositories = [];
await loadGithub();
}
}, 100);
}
</script>
{#if !$activePage.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"
>
{$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"
></path>
</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"
></path></svg
></a
>
</div>
</div>
{:else if $activePage.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}
<div in:fade="{{ duration: 100 }}">
{#if !$session.githubAppToken}
<Login />
{:else}
{#await loadGithub()}
<Loading github githubLoadingText="Loading repositories..." />
{:then}
{#if loading.github}
<Loading github githubLoadingText="Loading repositories..." />
{:else}
<div
class="space-y-2 max-w-4xl mx-auto px-6"
in:fade="{{ duration: 100 }}"
>
<Repositories
on:loadBranches="{loadBranches}"
on:modifyGithubAppConfig="{modifyGithubAppConfig}"
/>
{#if $application.repository.organization}
<Branches loading="{loading.branches}" branches="{branches}" />
{/if}
{#if $application.repository.branch}
<Tabs />
{/if}
</div>
{/if}
{/await}
{/if}
</div>

View File

@@ -1,50 +0,0 @@
<script>
import { session } from "@store";
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);
const ghToken = new URL(newWindow.document.URL).searchParams.get(
"ghToken",
);
if (ghToken) {
$session.githubAppToken = ghToken;
}
}
}, 100);
}
</script>
<div class="text-center text-white">
<div class="text-2xl font-bold text-center pb-4">
Choose your Git provider
</div>
<button on:click="{login}" class="hover:scale-110 transform duration-100 transition">
<svg
class="w-16"
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"
></path></svg
>
</button>
</div>

View File

@@ -1,53 +0,0 @@
<script>
import { createEventDispatcher } from "svelte";
import { application, githubRepositories, activePage } from "@store";
import Select from "svelte-select";
function handleSelect(event) {
$application.build.pack = 'static'
$application.repository.id = parseInt(event.detail.value, 10);
dispatch("loadBranches");
}
let items = $githubRepositories.map(repo => ({
label: `${repo.owner.login}/${repo.name}`,
value: repo.id.toString(),
}));
const selectedValue =
!$activePage.new &&
`${$application.repository.organization}/${$application.repository.name}`;
const dispatch = createEventDispatcher();
const modifyGithubAppConfig = () => dispatch("modifyGithubAppConfig");
</script>
<div class="grid grid-cols-1 pt-4">
{#if $githubRepositories.length !== 0}
<label for="repository">Organization / Repository</label>
<div class="grid grid-cols-3 ">
<div class="repository-select-search col-span-2">
<Select
isFocused="true"
containerClasses="w-full border-none bg-transparent"
on:select="{handleSelect}"
selectedValue="{selectedValue}"
isClearable="{false}"
items="{items}"
showIndicator="{$activePage.new}"
noOptionsMessage="No Repositories found"
placeholder="Select a Repository"
isDisabled="{!$activePage.new}"
/>
</div>
<button
class="button col-span-1 ml-2 bg-warmGray-800 hover:bg-warmGray-700 text-white"
on:click="{modifyGithubAppConfig}">Configure on Github</button
>
</div>
{:else}
<button
class="button col-span-1 ml-2 bg-warmGray-800 hover:bg-warmGray-700 text-white py-2"
on:click="{modifyGithubAppConfig}">Add repositories on Github</button
>
{/if}
</div>

View File

@@ -1,153 +0,0 @@
<script>
import { redirect } from "@roxi/routify";
import { onMount } from "svelte";
import { toast } from "@zerodevx/svelte-toast";
import templates from "../../../utils/templates";
import { application, fetch, deployments, activePage } from "@store";
import General from "./ActiveTab/General.svelte";
import Secrets from "./ActiveTab/Secrets.svelte";
import Loading from "../../Loading.svelte";
let activeTab = {
general: true,
buildStep: false,
secrets: false,
};
function activateTab(tab) {
if (activeTab.hasOwnProperty(tab)) {
activeTab = {
general: false,
buildStep: false,
secrets: false,
};
activeTab[tab] = true;
}
}
async function load() {
const found = $deployments?.applications?.deployed.find(deployment => {
if (
deployment.configuration.repository.organization ===
$application.repository.organization &&
deployment.configuration.repository.name ===
$application.repository.name &&
deployment.configuration.repository.branch ===
$application.repository.branch
) {
return deployment;
}
});
if (found) {
$application = { ...found.configuration };
if ($activePage.new) {
$activePage.new = false;
toast.push(
"This repository & branch is already defined. Redirecting...",
);
$redirect(`/application/:organization/:name/:branch/configuration`, {
name: $application.repository.name,
organization: $application.repository.organization,
branch: $application.repository.branch,
});
}
return;
}
if (!$activePage.new) {
const config = await $fetch(`/api/v1/config`, {
body: {
name: $application.repository.name,
organization: $application.repository.organization,
branch: $application.repository.branch,
},
});
$application = { ...config };
} else {
try {
const dir = await $fetch(
`https://api.github.com/repos/${$application.repository.organization}/${$application.repository.name}/contents/?ref=${$application.repository.branch}`,
);
const packageJson = dir.find(
f => f.type === "file" && f.name === "package.json",
);
const Dockerfile = dir.find(
f => f.type === "file" && f.name === "Dockerfile",
);
const CargoToml = dir.find(
f => f.type === "file" && f.name === "Cargo.toml",
);
if (packageJson) {
const { content } = await $fetch(packageJson.git_url);
const packageJsonContent = JSON.parse(atob(content));
const checkPackageJSONContents = dep => {
return (
packageJsonContent?.dependencies?.hasOwnProperty(dep) ||
packageJsonContent?.devDependencies?.hasOwnProperty(dep)
);
};
Object.keys(templates).map(dep => {
if (checkPackageJSONContents(dep)) {
const config = templates[dep];
$application.build.pack = config.pack;
if (config.installation)
$application.build.command.installation = config.installation;
if (config.port) $application.publish.port = config.port;
if (config.directory)
$application.publish.directory = config.directory;
if (
packageJsonContent.scripts.hasOwnProperty("build") &&
config.build
) {
$application.build.command.build = config.build;
}
toast.push(`${config.name} detected. Default values set.`);
}
});
} else if (CargoToml) {
$application.build.pack = "rust";
toast.push(`Rust language detected. Default values set.`);
} else if (Dockerfile) {
$application.build.pack = "docker";
toast.push("Custom Dockerfile found. Build pack set to docker.");
}
} catch (error) {
// Nothing detected
}
}
}
</script>
{#await load()}
<Loading github githubLoadingText="Scanning repository..." />
{:then}
<div class="block text-center py-8">
<nav
class="flex space-x-4 justify-center font-bold text-md text-white"
aria-label="Tabs"
>
<div
on:click="{() => activateTab('general')}"
class:text-green-500="{activeTab.general}"
class="px-3 py-2 cursor-pointer hover:bg-warmGray-700 rounded-lg transition duration-100"
>
General
</div>
<div
on:click="{() => activateTab('secrets')}"
class:text-green-500="{activeTab.secrets}"
class="px-3 py-2 cursor-pointer hover:bg-warmGray-700 rounded-lg transition duration-100"
>
Secrets
</div>
</nav>
</div>
<div class="max-w-4xl mx-auto">
<div class="h-full">
{#if activeTab.general}
<General />
{:else if activeTab.secrets}
<Secrets />
{/if}
</div>
</div>
{/await}

View File

@@ -0,0 +1,42 @@
<script>
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);
location.reload()
}
}, 100);
}
</script>
<div class="text-center text-white">
<div class="text-2xl font-bold text-center pb-4">Choose your Git provider</div>
<button on:click={login} class="hover:scale-110 transform duration-100 transition">
<svg
class="w-16"
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
>
</button>
</div>

View File

@@ -1,195 +1,178 @@
<script>
import { params, goto, redirect } from "@roxi/routify";
import {
application,
fetch,
initialApplication,
initConf,
activePage,
} from "@store";
import { onDestroy } from "svelte";
import { toast } from "@zerodevx/svelte-toast";
import Tooltip from "../../components/Tooltip/Tooltip.svelte";
import { application, initialApplication, initConf } from '$store';
import { onDestroy } from 'svelte';
import { toast } from '@zerodevx/svelte-toast';
import Tooltip from '$components/Tooltip.svelte';
import { request } from '$lib/api/request';
import { page, session } from '$app/stores';
import { goto } from '$app/navigation';
import { browser } from '$app/env';
async function removeApplication() {
await request(`/api/v1/application/remove`, $session, {
body: {
organization: $application.repository.organization,
name: $application.repository.name,
branch: $application.repository.branch
}
});
$application.repository.organization = $params.organization;
$application.repository.name = $params.name;
$application.repository.branch = $params.branch;
browser && toast.push('Application removed.');
$application = JSON.parse(JSON.stringify(initialApplication));
browser && goto(`/dashboard/applications`, { replaceState: true });
}
async function removeApplication() {
await $fetch(`/api/v1/application/remove`, {
body: {
organization: $params.organization,
name: $params.name,
branch: $params.branch,
},
});
onDestroy(() => {
$application = JSON.parse(JSON.stringify(initialApplication));
});
toast.push("Application removed.");
$application = JSON.parse(JSON.stringify(initialApplication));
$redirect(`/dashboard/applications`);
}
onDestroy(() => {
$application = JSON.parse(JSON.stringify(initialApplication));
});
async function deploy() {
try {
toast.push("Checking configuration.");
await $fetch(`/api/v1/application/check`, {
body: $application,
});
const { nickname, name, deployId } = await $fetch(
`/api/v1/application/deploy`,
{
body: $application,
},
);
$application.general.nickname = nickname;
$application.build.container.name = name;
$application.general.deployId = deployId;
$initConf = JSON.parse(JSON.stringify($application));
toast.push("Application deployment queued.");
$redirect(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/logs/${$application.general.deployId}`,
);
} catch (error) {
console.log(error);
toast.push(error.error || error || "Ooops something went wrong.");
}
}
async function deploy() {
try {
browser && toast.push('Checking configuration.');
await request(`/api/v1/application/check`, $session, {
body: $application
});
const { nickname, name, deployId } = await request(`/api/v1/application/deploy`, $session, {
body: $application
});
$application.general.nickname = nickname;
$application.build.container.name = name;
$application.general.deployId = deployId;
$initConf = JSON.parse(JSON.stringify($application));
if (browser) {
toast.push('Application deployment queued.');
goto(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/logs/${$application.general.deployId}`,
{ replaceState: true }
);
}
} catch (error) {
// console.log(error);
// toast.push(error.error || error || 'Ooops something went wrong.');
}
}
</script>
<nav
class="flex text-white justify-end items-center m-4 fixed right-0 top-0 space-x-4 z-50"
>
<Tooltip position="bottom" label="Deploy">
<button
disabled="{$application.publish.domain === '' ||
$application.publish.domain === null}"
class:cursor-not-allowed="{$application.publish.domain === '' ||
$application.publish.domain === null}"
class:hover:bg-green-500="{$application.publish.domain}"
class:bg-green-600="{$application.publish.domain}"
class:hover:bg-transparent="{$activePage.new}"
class:text-warmGray-700="{$application.publish.domain === '' ||
$application.publish.domain === null}"
class="icon"
on:click="{deploy}"
>
<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"
><polyline points="16 16 12 12 8 16"></polyline><line
x1="12"
y1="12"
x2="12"
y2="21"></line><path
d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"
></path><polyline points="16 16 12 12 8 16"></polyline></svg
>
</button>
</Tooltip>
<Tooltip position="bottom" label="Delete">
<button
disabled="{$application.publish.domain === '' ||
$application.publish.domain === null ||
$activePage.new}"
class:cursor-not-allowed="{$application.publish.domain === '' ||
$application.publish.domain === null ||
$activePage.new}"
class:hover:text-red-500="{$application.publish.domain &&
!$activePage.new}"
class:hover:bg-warmGray-700="{$application.publish.domain &&
!$activePage.new}"
class:hover:bg-transparent="{$activePage.new}"
class:text-warmGray-700="{$application.publish.domain === '' ||
$application.publish.domain === null ||
$activePage.new}"
class="icon"
on:click="{removeApplication}"
>
<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"
></path>
</svg>
</button>
</Tooltip>
<div class="border border-warmGray-700 h-8"></div>
<Tooltip position="bottom" label="Logs">
<button
class="icon"
class:text-warmGray-700="{$activePage.new}"
disabled="{$activePage.new}"
class:hover:text-blue-400="{!$activePage.new}"
class:hover:bg-transparent="{$activePage.new}"
class:cursor-not-allowed="{$activePage.new}"
class:text-blue-400="{$activePage.application === 'logs'}"
class:bg-warmGray-700="{$activePage.application === 'logs'}"
on:click="{() =>
$goto(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/logs`,
)}"
>
<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 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
></path>
</svg>
</button>
</Tooltip>
<Tooltip position="bottom-left" label="Configuration">
<button
class="icon hover:text-yellow-400"
disabled="{$activePage.new}"
class:text-yellow-400="{$activePage.application === 'configuration' ||
$activePage.new}"
class:bg-warmGray-700="{$activePage.application === 'configuration' ||
$activePage.new}"
on:click="{() =>
$goto(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/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"
></path>
</svg>
</button>
</Tooltip>
<nav class="flex text-white justify-end items-center m-4 fixed right-0 top-0 space-x-4 z-50">
<Tooltip position="bottom" label="Deploy">
<button
disabled={$application.publish.domain === '' || $application.publish.domain === null}
class:cursor-not-allowed={$application.publish.domain === '' ||
$application.publish.domain === null}
class:hover:bg-green-500={$application.publish.domain}
class:bg-green-600={$application.publish.domain}
class:hover:bg-transparent={!$application.publish.domain && $page.path === '/application/new'}
class:text-warmGray-700={$application.publish.domain === '' ||
$application.publish.domain === null}
class="icon"
on:click={deploy}
>
<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"
><polyline points="16 16 12 12 8 16" /><line x1="12" y1="12" x2="12" y2="21" /><path
d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"
/><polyline points="16 16 12 12 8 16" /></svg
>
</button>
</Tooltip>
<Tooltip position="bottom" label="Delete">
<button
disabled={$application.publish.domain === '' ||
$application.publish.domain === null ||
$page.path === '/application/new'}
class:cursor-not-allowed={$application.publish.domain === '' ||
$application.publish.domain === null ||
$page.path === '/application/new'}
class:hover:text-red-500={$application.publish.domain && $page.path !== '/application/new'}
class:hover:bg-warmGray-700={$application.publish.domain && $page.path !== '/application/new'}
class:hover:bg-transparent={$page.path === '/application/new'}
class:text-warmGray-700={$application.publish.domain === '' ||
$application.publish.domain === null ||
$page.path === '/application/new'}
class="icon"
on:click={removeApplication}
>
<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" label="Logs">
<button
class="icon"
class:text-warmGray-700={$page.path === '/application/new'}
disabled={$page.path === '/application/new'}
class:hover:text-blue-400={$page.path !== '/application/new'}
class:hover:bg-transparent={$page.path === '/application/new'}
class:cursor-not-allowed={$page.path === '/application/new'}
class:text-blue-400={/logs\/*/.test($page.path)}
class:bg-warmGray-700={/logs\/*/.test($page.path)}
on:click={() =>
goto(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/logs`
)}
>
<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 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
/>
</svg>
</button>
</Tooltip>
<Tooltip position="bottom-left" label="Configuration">
<button
class="icon hover:text-yellow-400"
disabled={$page.path === '/application/new'}
class:text-yellow-400={$page.path.endsWith('configuration') ||
$page.path === '/application/new'}
class:bg-warmGray-700={$page.path.endsWith('configuration') ||
$page.path === '/application/new'}
on:click={() =>
goto(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/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>

View File

@@ -0,0 +1,54 @@
<script>
import { createEventDispatcher } from 'svelte';
import { application, githubRepositories } from '$store';
import Select from 'svelte-select';
import { page } from '$app/stores';
function handleSelect(event) {
$application.build.pack = 'static';
$application.repository.id = parseInt(event.detail.value, 10);
$application.repository.branch = null
dispatch('loadBranches');
}
const path = $page.path === '/application/new';
let items = $githubRepositories.map((repo) => ({
label: `${repo.owner.login}/${repo.name}`,
value: repo.id.toString()
}));
const selectedValue =
!path && `${$application.repository.organization}/${$application.repository.name}`;
const dispatch = createEventDispatcher();
const modifyGithubAppConfig = () => dispatch('modifyGithubAppConfig');
</script>
<div class="grid grid-cols-1 pt-4">
{#if $githubRepositories.length !== 0}
<label for="repository">Organization / Repository</label>
<div class="grid grid-cols-3 ">
<div class="repository-select-search col-span-2">
<Select
isFocused="{true}"
containerClasses="w-full border-none bg-transparent"
on:select={handleSelect}
{selectedValue}
isClearable={false}
{items}
showIndicator={path}
noOptionsMessage="No Repositories found"
placeholder="Select a Repository"
isDisabled={!path}
/>
</div>
<button
class="button col-span-1 ml-2 bg-warmGray-800 hover:bg-warmGray-700 text-white"
on:click={modifyGithubAppConfig}>Configure on Github</button
>
</div>
{:else}
<button
class="button col-span-1 ml-2 bg-warmGray-800 hover:bg-warmGray-700 text-white py-2"
on:click={modifyGithubAppConfig}>Add repositories on Github</button
>
{/if}
</div>

View File

@@ -0,0 +1,138 @@
<script>
import { toast } from '@zerodevx/svelte-toast';
import templates from '$lib/api/applications/templates';
import { application, dashboard } from '$store';
import General from '$components/Application/ActiveTab/General.svelte';
import Secrets from '$components/Application/ActiveTab/Secrets.svelte';
import Loading from '$components/Loading.svelte';
import { goto } from '$app/navigation';
import { page, session } from '$app/stores';
import { request } from '$lib/api/request';
import { browser } from '$app/env';
let activeTab = {
general: true,
buildStep: false,
secrets: false
};
function activateTab(tab) {
if (activeTab.hasOwnProperty(tab)) {
activeTab = {
general: false,
buildStep: false,
secrets: false
};
activeTab[tab] = true;
}
}
async function load() {
const found = $dashboard?.applications?.deployed.find((deployment) => {
if (
deployment.configuration.repository.organization === $application.repository.organization &&
deployment.configuration.repository.name === $application.repository.name &&
deployment.configuration.repository.branch === $application.repository.branch
) {
return deployment;
}
});
if (found) {
$application = { ...found.configuration };
if ($page.path === '/application/new') {
if (browser) {
toast.push('This repository & branch is already defined. Redirecting...');
goto(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/configuration`,
{ replaceState: true }
);
}
}
return;
}
if ($page.path !== '/application/new') {
const config = await request(`/api/v1/application/config`, $session, {
body: {
name: $application.repository.name,
organization: $application.repository.organization,
branch: $application.repository.branch
}
});
$application = { ...config };
} else {
try {
const dir = await request(
`https://api.github.com/repos/${$application.repository.organization}/${$application.repository.name}/contents/?ref=${$application.repository.branch}`,
$session
);
const packageJson = dir.find((f) => f.type === 'file' && f.name === 'package.json');
const Dockerfile = dir.find((f) => f.type === 'file' && f.name === 'Dockerfile');
const CargoToml = dir.find((f) => f.type === 'file' && f.name === 'Cargo.toml');
if (packageJson) {
const { content } = await request(packageJson.git_url, $session);
const packageJsonContent = JSON.parse(atob(content));
const checkPackageJSONContents = (dep) => {
return (
packageJsonContent?.dependencies?.hasOwnProperty(dep) ||
packageJsonContent?.devDependencies?.hasOwnProperty(dep)
);
};
Object.keys(templates).map((dep) => {
if (checkPackageJSONContents(dep)) {
const config = templates[dep];
$application.build.pack = config.pack;
if (config.installation)
$application.build.command.installation = config.installation;
if (config.port) $application.publish.port = config.port;
if (config.directory) $application.publish.directory = config.directory;
if (packageJsonContent.scripts.hasOwnProperty('build') && config.build) {
$application.build.command.build = config.build;
}
browser && toast.push(`${config.name} detected. Default values set.`);
}
});
} else if (CargoToml) {
$application.build.pack = 'rust';
browser && toast.push(`Rust language detected. Default values set.`);
} else if (Dockerfile) {
$application.build.pack = 'docker';
browser && toast.push('Custom Dockerfile found. Build pack set to docker.');
}
} catch (error) {
// Nothing detected
}
}
}
</script>
{#await load()}
<Loading github githubLoadingText="Scanning repository..." />
{:then}
<div class="block text-center py-8">
<nav class="flex space-x-4 justify-center font-bold text-md text-white" aria-label="Tabs">
<div
on:click={() => activateTab('general')}
class:text-green-500={activeTab.general}
class="px-3 py-2 cursor-pointer hover:bg-warmGray-700 rounded-lg transition duration-100"
>
General
</div>
<div
on:click={() => activateTab('secrets')}
class:text-green-500={activeTab.secrets}
class="px-3 py-2 cursor-pointer hover:bg-warmGray-700 rounded-lg transition duration-100"
>
Secrets
</div>
</nav>
</div>
<div class="max-w-4xl mx-auto">
<div class="h-full">
{#if activeTab.general}
<General />
{:else if activeTab.secrets}
<Secrets />
{/if}
</div>
</div>
{/await}

View File

@@ -0,0 +1,109 @@
<script>
import { fade } from 'svelte/transition';
import { toast } from '@zerodevx/svelte-toast';
import MongoDb from './SVGs/MongoDb.svelte';
import Postgresql from './SVGs/Postgresql.svelte';
import Mysql from './SVGs/Mysql.svelte';
import CouchDb from './SVGs/CouchDb.svelte';
import { page, session } from '$app/stores';
import { goto } from '$app/navigation';
import { request } from '$lib/api/request';
import { browser } from '$app/env';
let type;
let defaultDatabaseName;
async function deploy() {
try {
await request(`/api/v1/databases/deploy`, $session, {
body: {
type,
defaultDatabaseName
}
});
if (browser) {
toast.push('Database deployment queued.');
goto(`/dashboard/databases`, { replaceState: true });
}
} catch (error) {
console.log(error);
}
}
</script>
<div class="text-center space-y-2 max-w-4xl mx-auto px-6" in:fade={{ duration: 100 }}>
{#if $page.path === '/database/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-green-600 p-2 rounded bg-warmGray-800 w-32"
class:border-green-600={type === 'mongodb'}
on:click={() => (type = 'mongodb')}
>
<div class="flex items-center justify-center my-2">
<MongoDb customClass="w-6" />
</div>
<div class="text-white">MongoDB</div>
</div>
<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-red-600 p-2 rounded bg-warmGray-800 w-32"
class:border-red-600={type === 'couchdb'}
on:click={() => (type = 'couchdb')}
>
<div class="flex items-center justify-center my-2">
<CouchDb customClass="w-12 text-red-600 fill-current" />
</div>
<div class="text-white">Couchdb</div>
</div>
<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-600 p-2 rounded bg-warmGray-800 w-32"
class:border-blue-600={type === 'postgresql'}
on:click={() => (type = 'postgresql')}
>
<div class="flex items-center justify-center my-2">
<Postgresql customClass="w-12" />
</div>
<div class="text-white">PostgreSQL</div>
</div>
<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-orange-600 p-2 rounded bg-warmGray-800 w-32"
class:border-orange-600={type === 'mysql'}
on:click={() => (type = 'mysql')}
>
<div class="flex items-center justify-center">
<Mysql customClass="w-10" />
</div>
<div class="text-white">MySQL</div>
</div>
<!-- <button
class="button bg-gray-500 p-2 text-white hover:bg-yellow-500 cursor-pointer w-32"
on:click="{() => (type = 'clickhouse')}"
class:bg-yellow-500="{type === 'clickhouse'}"
>
Clickhouse
</button> -->
</div>
{#if type}
<div class="flex justify-center space-x-4 items-center">
<label for="defaultDB">Default database</label>
<input id="defaultDB" class="w-64" placeholder="random" bind:value={defaultDatabaseName} />
<button
class:bg-green-600={type === 'mongodb'}
class:hover:bg-green-500={type === 'mongodb'}
class:bg-blue-600={type === 'postgresql'}
class:hover:bg-blue-500={type === 'postgresql'}
class:bg-orange-600={type === 'mysql'}
class:hover:bg-orange-500={type === 'mysql'}
class:bg-red-600={type === 'couchdb'}
class:hover:bg-red-500={type === 'couchdb'}
class:bg-yellow-500={type === 'clickhouse'}
class:hover:bg-yellow-400={type === 'clickhouse'}
class="button p-2 w-32 text-white"
on:click={deploy}>Deploy</button
>
</div>
{/if}
{/if}
</div>

View File

@@ -1,113 +0,0 @@
<script>
import { fetch, dbInprogress } from "@store";
import { isActive, redirect } from "@roxi/routify/runtime";
import { fade } from "svelte/transition";
import { toast } from "@zerodevx/svelte-toast";
import MongoDb from "../SVGs/MongoDb.svelte";
import Postgresql from "../SVGs/Postgresql.svelte";
import Mysql from "../SVGs/Mysql.svelte";
import CouchDb from "../SVGs/CouchDb.svelte";
let type;
let defaultDatabaseName;
async function deploy() {
try {
await $fetch(`/api/v1/databases/deploy`, {
body: {
type,
defaultDatabaseName,
},
});
$dbInprogress = true;
toast.push("Database deployment queued.");
$redirect(`/dashboard/databases`);
} catch (error) {
console.log(error);
}
}
</script>
<div
class="text-center space-y-2 max-w-4xl mx-auto px-6"
in:fade="{{ duration: 100 }}"
>
{#if $isActive("/database/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-green-600 p-2 rounded bg-warmGray-800 w-32"
class:border-green-600="{type === 'mongodb'}"
on:click="{() => (type = 'mongodb')}"
>
<div class="flex items-center justify-center my-2">
<MongoDb customClass="w-6" />
</div>
<div class="text-white">MongoDB</div>
</div>
<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-red-600 p-2 rounded bg-warmGray-800 w-32"
class:border-red-600="{type === 'couchdb'}"
on:click="{() => (type = 'couchdb')}"
>
<div class="flex items-center justify-center my-2">
<CouchDb customClass="w-12 text-red-600 fill-current" />
</div>
<div class="text-white">Couchdb</div>
</div>
<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-600 p-2 rounded bg-warmGray-800 w-32"
class:border-blue-600="{type === 'postgresql'}"
on:click="{() => (type = 'postgresql')}"
>
<div class="flex items-center justify-center my-2">
<Postgresql customClass="w-12" />
</div>
<div class="text-white">PostgreSQL</div>
</div>
<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-orange-600 p-2 rounded bg-warmGray-800 w-32"
class:border-orange-600="{type === 'mysql'}"
on:click="{() => (type = 'mysql')}"
>
<div class="flex items-center justify-center">
<Mysql customClass="w-10" />
</div>
<div class="text-white">MySQL</div>
</div>
<!-- <button
class="button bg-gray-500 p-2 text-white hover:bg-yellow-500 cursor-pointer w-32"
on:click="{() => (type = 'clickhouse')}"
class:bg-yellow-500="{type === 'clickhouse'}"
>
Clickhouse
</button> -->
</div>
{#if type}
<div class="flex justify-center space-x-4 items-center">
<label for="defaultDB">Default database</label>
<input
id="defaultDB"
class="w-64"
placeholder="random"
bind:value="{defaultDatabaseName}"
/>
<button
class:bg-green-600="{type === 'mongodb'}"
class:hover:bg-green-500="{type === 'mongodb'}"
class:bg-blue-600="{type === 'postgresql'}"
class:hover:bg-blue-500="{type === 'postgresql'}"
class:bg-orange-600="{type === 'mysql'}"
class:hover:bg-orange-500="{type === 'mysql'}"
class:bg-red-600="{type === 'couchdb'}"
class:hover:bg-red-500="{type === 'couchdb'}"
class:bg-yellow-500="{type === 'clickhouse'}"
class:hover:bg-yellow-400="{type === 'clickhouse'}"
class="button p-2 w-32 text-white"
on:click="{deploy}">Deploy</button
>
</div>
{/if}
{/if}
</div>

View File

@@ -1,79 +1,80 @@
<style lang="postcss">
.loader {
width: 8px;
height: 40px;
border-radius: 4px;
display: block;
margin: 20px auto;
position: relative;
background: currentColor;
color: #fff;
box-sizing: border-box;
animation: animloader 0.3s 0.3s linear infinite alternate;
}
.loader::after,
.loader::before {
content: "";
width: 8px;
height: 40px;
border-radius: 4px;
background: currentColor;
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 20px;
box-sizing: border-box;
animation: animloader 0.3s 0.45s linear infinite alternate;
}
.loader::before {
left: -20px;
animation-delay: 0s;
}
@keyframes animloader {
0% {
height: 48px;
.loader {
width: 8px;
height: 40px;
border-radius: 4px;
display: block;
margin: 20px auto;
position: relative;
background: currentColor;
color: #fff;
box-sizing: border-box;
animation: animloader 0.3s 0.3s linear infinite alternate;
}
100% {
height: 4px;
.loader::after,
.loader::before {
content: "";
width: 8px;
height: 40px;
border-radius: 4px;
background: currentColor;
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 20px;
box-sizing: border-box;
animation: animloader 0.3s 0.45s linear infinite alternate;
}
}
</style>
<script>
export let github = false;
export let githubLoadingText = "Loading GitHub...";
export let fullscreen = true;
</script>
{#if fullscreen}
{#if github}
<div class="fixed left-0 top-0 flex flex-wrap content-center h-full w-full">
<div class="main flex justify-center items-center">
<div class="w-64">
<svg
class=" w-28 animate-bounce mx-auto"
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"
></path></svg
>
<div class="text-xl font-bold text-center">
{githubLoadingText}
.loader::before {
left: -20px;
animation-delay: 0s;
}
@keyframes animloader {
0% {
height: 48px;
}
100% {
height: 4px;
}
}
</style>
<script>
export let github = false;
export let githubLoadingText = "Loading GitHub...";
export let fullscreen = true;
</script>
{#if fullscreen}
{#if github}
<div class="fixed left-0 top-0 flex flex-wrap content-center h-full w-full">
<div class="main flex justify-center items-center">
<div class="w-64">
<svg
class=" w-28 animate-bounce mx-auto"
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"
></path></svg
>
<div class="text-xl font-bold text-center">
{githubLoadingText}
</div>
</div>
</div>
</div>
</div>
{:else}
<div class="main fixed left-0 top-0 flex flex-wrap content-center h-full">
<span class=" loader"></span>
</div>
{:else}
<div class="main fixed left-0 top-0 flex flex-wrap content-center h-full">
<span class=" loader"></span>
</div>
{/if}
{/if}
{/if}

View File

@@ -0,0 +1,87 @@
<script>
import { fade } from 'svelte/transition';
import { toast } from '@zerodevx/svelte-toast';
import Loading from '../Loading.svelte';
import Tooltip from '$components/Tooltip.svelte';
import { request } from '$lib/api/request';
import { page, session } from '$app/stores';
import PasswordField from '$components/PasswordField.svelte';
import { browser } from '$app/env';
export let service;
let loading = false;
async function activate() {
try {
loading = true;
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?!`);
} finally {
loading = false;
}
}
</script>
{#if loading}
<Loading />
{:else}
<div class="text-left max-w-5xl mx-auto px-6" in:fade={{ duration: 100 }}>
<div class="pb-2 pt-5 space-y-4">
<div class="flex space-x-5 items-center">
<div class="text-2xl font-bold border-gradient">General</div>
<div class="flex-1" />
<Tooltip
position="bottom"
size="large"
label="Activate all users in Plausible database, so you can login without the email verification."
>
<button class="button bg-blue-500 hover:bg-blue-400 px-2" on:click={activate}
>Activate All Users</button
>
</Tooltip>
</div>
<div class="flex items-center pt-4">
<div class="font-bold w-64 text-warmGray-400">Domain</div>
<input class="w-full" value={service.config.baseURL} disabled />
</div>
<div class="flex items-center">
<div class="font-bold w-64 text-warmGray-400">Email address</div>
<input class="w-full" value={service.config.email} disabled />
</div>
<div class="flex items-center">
<div class="font-bold w-64 text-warmGray-400">Username</div>
<input class="w-full" value={service.config.userName} disabled />
</div>
<div class="flex items-center">
<div class="font-bold w-64 text-warmGray-400">Password</div>
<PasswordField value={service.config.userPassword} />
</div>
<div class="text-2xl font-bold pt-4 border-gradient w-32">PostgreSQL</div>
<div class="flex items-center pt-4">
<div class="font-bold w-64 text-warmGray-400">Username</div>
<input
class="w-full"
value={service.config.generateEnvsPostgres.POSTGRESQL_USERNAME}
disabled
/>
</div>
<div class="flex items-center">
<div class="font-bold w-64 text-warmGray-400">Password</div>
<PasswordField value={service.config.generateEnvsPostgres.POSTGRESQL_PASSWORD} />
</div>
<div class="flex items-center">
<div class="font-bold w-64 text-warmGray-400">Database</div>
<input
class="w-full"
value={service.config.generateEnvsPostgres.POSTGRESQL_DATABASE}
disabled
/>
</div>
</div>
</div>
{/if}

View File

@@ -1,82 +0,0 @@
<script>
import { fetch } from "@store";
import { params } from "@roxi/routify/runtime";
import { fade } from "svelte/transition";
import { toast } from "@zerodevx/svelte-toast";
import Loading from "../Loading.svelte";
import TooltipInfo from "../Tooltip/TooltipInfo.svelte";
import PasswordField from "../PasswordField.svelte";
import Tooltip from "../Tooltip/Tooltip.svelte";
export let service;
$: name = $params.name;
let loading = false;
async function activate() {
try {
loading = true;
await $fetch(`/api/v1/services/deploy/${name}/activate`, {
method: "PATCH",
body: {},
});
toast.push(`All users are activated for Plausible.`);
} catch (error) {
console.log(error);
toast.push(`Ooops, there was an error activating users for Plausible?!`);
} finally {
loading = false;
}
}
</script>
{#if loading}
<Loading />
{:else}
<div class="text-left max-w-5xl mx-auto px-6" in:fade="{{ duration: 100 }}">
<div class="pb-2 pt-5 space-y-4">
<div class="flex space-x-5 items-center">
<div class="text-2xl font-bold border-gradient">General</div>
<div class="flex-1"></div>
<Tooltip
position="bottom"
size="large"
label="Activate all users in Plausible database, so you can login without the email verification."
>
<button
class="button bg-blue-500 hover:bg-blue-400 px-2"
on:click="{activate}">Activate All Users</button
>
</Tooltip>
</div>
<div class="flex items-center pt-4">
<div class="font-bold w-64 text-warmGray-400">Domain</div>
<input class="w-full" value="{service.config.baseURL}" disabled />
</div>
<div class="flex items-center">
<div class="font-bold w-64 text-warmGray-400">Email address</div>
<input class="w-full" value="{service.config.email}" disabled />
</div>
<div class="flex items-center">
<div class="font-bold w-64 text-warmGray-400">Username</div>
<input class="w-full" value="{service.config.userName}" disabled />
</div>
<div class="flex items-center">
<div class="font-bold w-64 text-warmGray-400">Password</div>
<PasswordField value="{service.config.userPassword}" />
</div>
<div class="text-2xl font-bold pt-4 border-gradient w-32">PostgreSQL</div>
<div class="flex items-center pt-4">
<div class="font-bold w-64 text-warmGray-400">Username</div>
<input class="w-full" value="{service.config.generateEnvsPostgres.POSTGRESQL_USERNAME}" disabled />
</div>
<div class="flex items-center">
<div class="font-bold w-64 text-warmGray-400">Password</div>
<PasswordField value="{service.config.generateEnvsPostgres.POSTGRESQL_PASSWORD}" />
</div>
<div class="flex items-center">
<div class="font-bold w-64 text-warmGray-400">Database</div>
<input class="w-full" value="{service.config.generateEnvsPostgres.POSTGRESQL_DATABASE}" disabled />
</div>
</div>
</div>
{/if}

View File

@@ -0,0 +1,15 @@
<script>
export let position = "bottom";
export let label;
export let size = "fit";
</script>
<span
aria-label="{label}"
data-microtip-position="{position}"
data-microtip-size="{size}"
role="tooltip"
>
<slot></slot>
</span>

View File

@@ -1,14 +0,0 @@
<script>
export let position = "bottom";
export let label;
export let size = "fit";
</script>
<span
aria-label="{label}"
data-microtip-position="{position}"
data-microtip-size="{size}"
role="tooltip"
>
<slot></slot>
</span>

115
src/global.d.ts vendored Normal file
View File

@@ -0,0 +1,115 @@
/// <reference types="@sveltejs/kit" />
/// <reference types="svelte" />
/// <reference types="vite/client" />
export type DateTimeFormatOptions = {
localeMatcher?: 'lookup' | 'best fit';
weekday?: 'long' | 'short' | 'narrow';
era?: 'long' | 'short' | 'narrow';
year?: 'numeric' | '2-digit';
month?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow';
day?: 'numeric' | '2-digit';
hour?: 'numeric' | '2-digit';
minute?: 'numeric' | '2-digit';
second?: 'numeric' | '2-digit';
timeZoneName?: 'long' | 'short';
formatMatcher?: 'basic' | 'best fit';
hour12?: boolean;
timeZone?: string;
};
export type Application = {
github: {
installation: {
id: number;
};
app: {
id: number;
};
};
repository: {
id: number;
organization: string;
name: string;
branch: string;
};
general: {
deployId: string;
nickname: string;
workdir: string;
};
build: {
pack: string;
directory: string;
command: {
build: string | null;
installation: string;
};
container: {
name: string;
tag: string;
baseSHA: string;
};
};
publish: {
directory: string;
domain: string;
path: string;
port: number;
secrets: Array<Record<string, unknown>>;
};
};
export type Database = {
config:
| {
general: {
deployId: string;
nickname: string;
workdir: string;
type: string;
};
database: {
usernames: Array;
passwords: Array;
defaultDatabaseName: string;
};
deploy: {
name: string;
};
}
| Record<string, unknown>;
envs: Array;
};
export type Dashboard = {
databases: {
deployed:
| [
{
configuration: Database;
}
]
| [];
};
services: {
deployed:
| [
{
configuration: any;
}
]
| [];
};
applications: {
deployed:
| [
{
configuration: Application;
UpdatedAt: any;
}
]
| [];
};
};
export type GithubInstallations = {
id: number;
app_id: number;
};

80
src/hooks/index.ts Normal file
View File

@@ -0,0 +1,80 @@
import dotEnvExtended from 'dotenv-extended';
dotEnvExtended.load();
import { publicPages } from '$lib/consts';
import mongoose from 'mongoose';
import { verifyUserId } from '$lib/api/common';
import { initializeSession } from 'svelte-kit-cookie-session';
process.on('SIGINT', function () {
mongoose.connection.close(function () {
console.log('Mongoose default connection disconnected through app termination');
process.exit(0);
});
});
async function connectMongoDB() {
const { MONGODB_USER, MONGODB_PASSWORD, MONGODB_HOST, MONGODB_PORT, MONGODB_DB } = process.env;
try {
if (process.env.NODE_ENV === 'production') {
await mongoose.connect(
`mongodb://${MONGODB_USER}:${MONGODB_PASSWORD}@${MONGODB_HOST}:${MONGODB_PORT}/${MONGODB_DB}?authSource=${MONGODB_DB}&readPreference=primary&ssl=false`,
{ useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false }
);
} else {
await mongoose.connect(
'mongodb://supercooldbuser:developmentPassword4db@localhost:27017/coolify?&readPreference=primary&ssl=false',
{ useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false }
);
}
console.log('Connected to mongodb.');
} catch (error) {
console.log(error);
}
}
if (mongoose.connection.readyState !== 1) connectMongoDB();
export async function handle({ request, render }) {
const { SECRETS_ENCRYPTION_KEY } = process.env;
const session = initializeSession(request.headers, {
secret: SECRETS_ENCRYPTION_KEY,
cookie: { path: '/' }
});
request.locals.session = session;
if (session?.data?.coolToken) {
try {
await verifyUserId(session.data.coolToken);
request.locals.session = session;
} catch (error) {
request.locals.session.destroy = true;
}
}
const response = await render(request);
if (!session['set-cookie']) {
if (!session?.data?.coolToken && !publicPages.includes(request.path)) {
return {
status: 301,
headers: {
location: '/'
}
};
}
return response;
}
return {
...response,
headers: {
...response.headers,
...session
}
};
}
export function getSession(request) {
const { data } = request.locals.session;
return {
isLoggedIn: data && Object.keys(data).length !== 0 ? true : false,
expires: data.expires,
coolToken: data.coolToken,
ghToken: data.ghToken
};
}

View File

@@ -1,53 +0,0 @@
@import "tailwindcss/base.css";
@import "tailwindcss/components.css";
@import "tailwindcss/utilities.css";
html {
height: 100%;
}
body {
background-color: rgb(22, 22, 22);
min-height: 100vh;
overflow-x: hidden;
}
:root {
--toastBackground: rgba(41, 37, 36, 0.8);
--toastProgressBackground: transparent;
--toastFont: 'Inter';
}
.border-gradient {
border-bottom: 2px solid transparent;
border-image: linear-gradient(0.25turn, rgba(255, 249, 34), rgba(255, 0, 128), rgba(56, 2, 155, 0));
border-image-slice: 1;
}
.border-gradient-full {
border: 4px solid transparent;
border-image: linear-gradient(0.25turn, rgba(255, 249, 34), rgba(255, 0, 128), rgba(56, 2, 155, 0));
border-image-slice: 1;
}
[aria-label][role~="tooltip"]::after {
background: rgba(41, 37, 36, 0.9);
color: white;
font-family: 'Inter';
font-size: 16px;
font-weight: 600;
white-space: normal;
}
[role~="tooltip"][data-microtip-position|="bottom"]::before {
background: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba%2841,%2037,%2036,%200.9%29%22%20transform%3D%22rotate%28180%2018%206%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E") no-repeat;
}
[role~="tooltip"][data-microtip-position|="top"]::before {
background: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba%2841,%2037,%2036,%200.9%29%22%20transform%3D%22rotate%280%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E") no-repeat;
}
[role~="tooltip"][data-microtip-position="right"]::before {
background: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2212px%22%20height%3D%2236px%22%3E%3Cpath%20fill%3D%22rgba%2841,%2037,%2036,%200.9%29%22%20transform%3D%22rotate%2890%206%206%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E") no-repeat;
}
[role~="tooltip"][data-microtip-position="left"]::before {
background: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2212px%22%20height%3D%2236px%22%3E%3Cpath%20fill%3D%22rgba%2841,%2037,%2036,%200.9%29%22%20transform%3D%22rotate%28-90%2018%2018%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E") no-repeat;
}

View File

@@ -1,8 +0,0 @@
import App from './App.svelte'
import './index.css'
const app = new App({
target: document.body
})
export default app

View File

@@ -0,0 +1,29 @@
import Deployment from '$models/Logs/Deployment';
import { saveAppLog } from './logging';
import * as packs from './packs';
export default async function (configuration) {
const { id, organization, name, branch } = configuration.repository;
const { domain } = configuration.publish;
const deployId = configuration.general.deployId;
const execute = packs[configuration.build.pack];
if (execute) {
await Deployment.findOneAndUpdate(
{ repoId: id, branch, deployId, organization, name, domain },
{ repoId: id, branch, deployId, organization, name, domain, progress: 'inprogress' }
);
await saveAppLog('### Building application.', configuration);
await execute(configuration);
await saveAppLog('### Building done.', configuration);
} else {
try {
await Deployment.findOneAndUpdate(
{ repoId: id, branch, deployId, organization, name, domain },
{ repoId: id, branch, deployId, organization, name, domain, progress: 'failed' }
);
} catch (error) {
// Hmm.
}
throw new Error('No buildpack found.');
}
}

View File

@@ -0,0 +1,45 @@
import { docker } from '$lib/api/docker';
import { execShellAsync } from '../common';
export async function deleteSameDeployments(configuration) {
await (
await docker.engine.listServices()
)
.filter((r) => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
.map(async (s) => {
const running = JSON.parse(s.Spec.Labels.configuration);
if (
running.repository.id === configuration.repository.id &&
running.repository.branch === configuration.repository.branch
) {
await execShellAsync(`docker stack rm ${s.Spec.Labels['com.docker.stack.namespace']}`);
}
});
}
export async function purgeImagesContainers(configuration, deleteAll = false) {
const { name, tag } = configuration.build.container;
await execShellAsync('docker container prune -f');
if (deleteAll) {
const IDsToDelete = (
await execShellAsync(`docker images ls --filter=reference='${name}' --format '{{json .ID }}'`)
)
.trim()
.replace(/"/g, '')
.split('\n');
if (IDsToDelete.length > 0)
await execShellAsync(`docker rmi -f ${IDsToDelete.toString().replace(',', ' ')}`);
} else {
const IDsToDelete = (
await execShellAsync(
`docker images ls --filter=reference='${name}' --filter=before='${name}:${tag}' --format '{{json .ID }}'`
)
)
.trim()
.replace(/"/g, '')
.split('\n');
if (IDsToDelete.length > 1)
await execShellAsync(`docker rmi -f ${IDsToDelete.toString().replace(',', ' ')}`);
}
await execShellAsync('docker image prune -f');
}

View File

@@ -0,0 +1,48 @@
import jsonwebtoken from 'jsonwebtoken';
import { execShellAsync } from '../common';
export default async function (configuration) {
try {
const { GITHUB_APP_PRIVATE_KEY } = process.env;
const { workdir } = configuration.general;
const { organization, name, branch } = configuration.repository;
const github = configuration.github;
if (!github.installation.id || !github.app.id) {
throw new Error('Github installation ID is invalid.');
}
const githubPrivateKey = GITHUB_APP_PRIVATE_KEY.replace(/\\n/g, '\n').replace(/"/g, '');
const payload = {
iat: Math.round(new Date().getTime() / 1000),
exp: Math.round(new Date().getTime() / 1000 + 60),
iss: parseInt(github.app.id)
};
const jwtToken = jsonwebtoken.sign(payload, githubPrivateKey, {
algorithm: 'RS256'
});
const { token } = await (
await fetch(
`https://api.github.com/app/installations/${github.installation.id}/access_tokens`,
{
method: 'POST',
headers: {
Authorization: 'Bearer ' + jwtToken,
Accept: 'application/vnd.github.machine-man-preview+json'
}
}
)
).json();
await execShellAsync(
`mkdir -p ${workdir} && git clone -q -b ${branch} https://x-access-token:${token}@github.com/${organization}/${name}.git ${workdir}/`
);
configuration.build.container.tag = (
await execShellAsync(`cd ${configuration.general.workdir}/ && git rev-parse HEAD`)
)
.replace('\n', '')
.slice(0, 7);
} catch (error) {
console.log(error);
}
}

View File

@@ -0,0 +1,18 @@
export const baseServiceConfiguration = {
replicas: 1,
restart_policy: {
condition: 'any',
max_attempts: 6
},
update_config: {
parallelism: 1,
delay: '10s',
order: 'start-first'
},
rollback_config: {
parallelism: 1,
delay: '10s',
order: 'start-first',
failure_action: 'rollback'
}
};

View File

@@ -0,0 +1,156 @@
import cuid from 'cuid';
import crypto from 'crypto';
import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator';
import { docker } from '$lib/api/docker';
import { baseServiceConfiguration } from './common';
import { execShellAsync } from '../common';
function getUniq() {
return uniqueNamesGenerator({ dictionaries: [adjectives, animals, colors], length: 2 });
}
export function setDefaultConfiguration(configuration) {
const nickname = getUniq();
const deployId = cuid();
const shaBase = JSON.stringify({ repository: configuration.repository });
const sha256 = crypto.createHash('sha256').update(shaBase).digest('hex');
configuration.build.container.name = sha256.slice(0, 15);
configuration.general.nickname = nickname;
configuration.general.deployId = deployId;
configuration.general.workdir = `/tmp/${deployId}`;
if (!configuration.publish.path) configuration.publish.path = '/';
if (!configuration.publish.port) {
if (
configuration.build.pack === 'nodejs' ||
configuration.build.pack === 'vuejs' ||
configuration.build.pack === 'nuxtjs' ||
configuration.build.pack === 'rust' ||
configuration.build.pack === 'nextjs'
) {
configuration.publish.port = 3000;
} else {
configuration.publish.port = 80;
}
}
if (!configuration.build.directory) configuration.build.directory = '';
if (configuration.build.directory.startsWith('/'))
configuration.build.directory = configuration.build.directory.replace('/', '');
if (!configuration.publish.directory) configuration.publish.directory = '';
if (configuration.publish.directory.startsWith('/'))
configuration.publish.directory = configuration.publish.directory.replace('/', '');
if (configuration.build.pack === 'static' || configuration.build.pack === 'nodejs') {
if (!configuration.build.command.installation)
configuration.build.command.installation = 'yarn install';
}
configuration.build.container.baseSHA = crypto
.createHash('sha256')
.update(JSON.stringify(baseServiceConfiguration))
.digest('hex');
configuration.baseServiceConfiguration = baseServiceConfiguration;
return configuration;
}
export async function precheckDeployment({ services, configuration }) {
let foundService = false;
let configChanged = false;
let imageChanged = false;
let forceUpdate = false;
for (const service of services) {
const running = JSON.parse(service.Spec.Labels.configuration);
if (running) {
if (
running.repository.id === configuration.repository.id &&
running.repository.branch === configuration.repository.branch
) {
// Base service configuration changed
if (
!running.build.container.baseSHA ||
running.build.container.baseSHA !== configuration.build.container.baseSHA
) {
forceUpdate = true;
}
// If the deployment is in error state, forceUpdate
const state = await execShellAsync(
`docker stack ps ${running.build.container.name} --format '{{ json . }}'`
);
const isError = state
.split('\n')
.filter((n) => n)
.map((s) => JSON.parse(s))
.filter(
(n) =>
n.DesiredState !== 'Running' && n.Image.split(':')[1] === running.build.container.tag
);
if (isError.length > 0) forceUpdate = true;
foundService = true;
const runningWithoutContainer = JSON.parse(JSON.stringify(running));
delete runningWithoutContainer.build.container;
const configurationWithoutContainer = JSON.parse(JSON.stringify(configuration));
delete configurationWithoutContainer.build.container;
// If only the configuration changed
if (
JSON.stringify(runningWithoutContainer.build) !==
JSON.stringify(configurationWithoutContainer.build) ||
JSON.stringify(runningWithoutContainer.publish) !==
JSON.stringify(configurationWithoutContainer.publish)
)
configChanged = true;
// If only the image changed
if (running.build.container.tag !== configuration.build.container.tag) imageChanged = true;
// If build pack changed, forceUpdate the service
if (running.build.pack !== configuration.build.pack) forceUpdate = true;
}
}
}
if (forceUpdate) {
imageChanged = false;
configChanged = false;
}
return {
foundService,
imageChanged,
configChanged,
forceUpdate
};
}
export async function updateServiceLabels(configuration) {
// In case of any failure during deployment, still update the current configuration.
const services = (await docker.engine.listServices()).filter(
(r) => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application'
);
const found = services.find((s) => {
const config = JSON.parse(s.Spec.Labels.configuration);
if (
config.repository.id === configuration.repository.id &&
config.repository.branch === configuration.repository.branch
) {
return config;
}
return null;
});
if (found) {
const { ID } = found;
const Labels = { ...JSON.parse(found.Spec.Labels.configuration), ...configuration };
await execShellAsync(
`docker service update --label-add configuration='${JSON.stringify(
Labels
)}' --label-add com.docker.stack.image='${configuration.build.container.name}:${
configuration.build.container.tag
}' ${ID}`
);
}
}

View File

@@ -0,0 +1,69 @@
import { promises as fs } from 'fs';
export default async function (configuration) {
const staticDeployments = ['react', 'vuejs', 'static', 'svelte', 'gatsby'];
try {
// TODO: Write full .dockerignore for all deployments!!
if (configuration.build.pack === 'php') {
await fs.writeFile(
`${configuration.general.workdir}/.htaccess`,
`
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.+)$ index.php [QSA,L]
`
);
}
// await fs.writeFile(`${configuration.general.workdir}/.dockerignore`, 'node_modules')
if (staticDeployments.includes(configuration.build.pack)) {
await fs.writeFile(
`${configuration.general.workdir}/nginx.conf`,
`user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
access_log off;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/index.html $uri/ /index.html =404;
}
error_page 404 /50x.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
}
`
);
}
} catch (error) {
console.log(error);
throw new Error(error);
}
}

View File

@@ -0,0 +1,76 @@
import { docker } from '$lib/api/docker';
import { saveAppLog } from './logging';
import { promises as fs } from 'fs';
import { deleteSameDeployments } from './cleanup';
import yaml from 'js-yaml';
import { execShellAsync } from '../common';
export default async function (configuration, imageChanged) {
const generateEnvs = {};
for (const secret of configuration.publish.secrets) {
generateEnvs[secret.name] = secret.value;
}
const containerName = configuration.build.container.name;
// Only save SHA256 of it in the configuration label
const baseServiceConfiguration = configuration.baseServiceConfiguration;
delete configuration.baseServiceConfiguration;
const stack = {
version: '3.8',
services: {
[containerName]: {
image: `${configuration.build.container.name}:${configuration.build.container.tag}`,
networks: [`${docker.network}`],
environment: generateEnvs,
deploy: {
...baseServiceConfiguration,
labels: [
'managedBy=coolify',
'type=application',
'configuration=' + JSON.stringify(configuration),
'traefik.enable=true',
'traefik.http.services.' +
configuration.build.container.name +
`.loadbalancer.server.port=${configuration.publish.port}`,
'traefik.http.routers.' + configuration.build.container.name + '.entrypoints=websecure',
'traefik.http.routers.' +
configuration.build.container.name +
'.rule=Host(`' +
configuration.publish.domain +
'`) && PathPrefix(`' +
configuration.publish.path +
'`)',
'traefik.http.routers.' +
configuration.build.container.name +
'.tls.certresolver=letsencrypt',
'traefik.http.routers.' +
configuration.build.container.name +
'.middlewares=global-compress'
]
}
}
},
networks: {
[`${docker.network}`]: {
external: true
}
}
};
await saveAppLog('### Publishing.', configuration);
await fs.writeFile(`${configuration.general.workdir}/stack.yml`, yaml.dump(stack));
if (imageChanged) {
// console.log('image changed')
await execShellAsync(
`docker service update --image ${configuration.build.container.name}:${configuration.build.container.tag} ${configuration.build.container.name}_${configuration.build.container.name}`
);
} else {
// console.log('new deployment or force deployment or config changed')
await deleteSameDeployments(configuration);
await execShellAsync(
`cat ${configuration.general.workdir}/stack.yml | docker stack deploy --prune -c - ${containerName}`
);
}
await saveAppLog('### Published done!', configuration);
}

View File

@@ -0,0 +1,58 @@
import Settings from '$models/Settings';
import ServerLog from '$models/Logs/Server';
import ApplicationLog from '$models/Logs/Application';
import dayjs from 'dayjs';
import { version } from '../../../../package.json';
function generateTimestamp() {
return `${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')} `;
}
const patterns = [
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))'
].join('|');
export async function saveAppLog(event, configuration, isError?: boolean) {
try {
const deployId = configuration.general.deployId;
const repoId = configuration.repository.id;
const branch = configuration.repository.branch;
if (isError) {
const clearedEvent =
'[ERROR 😱] ' +
generateTimestamp() +
event.replace(new RegExp(patterns, 'g'), '').replace(/(\r\n|\n|\r)/gm, '');
await new ApplicationLog({ repoId, branch, deployId, event: clearedEvent }).save();
} else {
if (event && event !== '\n') {
const clearedEvent =
'[INFO] ' +
generateTimestamp() +
event.replace(new RegExp(patterns, 'g'), '').replace(/(\r\n|\n|\r)/gm, '');
await new ApplicationLog({ repoId, branch, deployId, event: clearedEvent }).save();
}
}
} catch (error) {
console.log(error);
return error;
}
}
export async function saveServerLog(error) {
const settings = await Settings.findOne({ applicationName: 'coolify' });
const payload = {
message: error.message,
stack: error.stack,
type: error.type || 'spaghetticode',
version
};
const found = await ServerLog.find(payload);
if (found.length === 0 && error.message) await new ServerLog(payload).save();
if (settings && settings.sendErrors && process.env.NODE_ENV === 'production') {
await fetch('https://errors.coollabs.io/api/error', {
method: 'POST',
body: JSON.stringify({ ...payload })
});
}
}

View File

@@ -0,0 +1,17 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
export default async function (configuration) {
const path = `${configuration.general.workdir}/${
configuration.build.directory ? configuration.build.directory : ''
}`;
if (fs.stat(`${path}/Dockerfile`)) {
const stream = await docker.engine.buildImage(
{ src: ['.'], context: path },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
);
await streamEvents(stream, configuration);
} else {
throw new Error('No custom dockerfile found.');
}
}

View File

@@ -0,0 +1,28 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
import { buildImage } from '../helpers';
// 'HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost/ || exit 1',
const publishStaticDocker = (configuration) => {
return [
'FROM nginx:stable-alpine',
'COPY nginx.conf /etc/nginx/nginx.conf',
'WORKDIR /usr/share/nginx/html',
`COPY --from=${configuration.build.container.name}:${configuration.build.container.tag}-cache /usr/src/app/${configuration.publish.directory} ./`,
'EXPOSE 80',
'CMD ["nginx", "-g", "daemon off;"]'
].join('\n');
};
export default async function (configuration) {
await buildImage(configuration, true);
await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`,
publishStaticDocker(configuration)
);
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
);
await streamEvents(stream, configuration);
}

View File

@@ -0,0 +1,30 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
const buildImageNodeDocker = (configuration) => {
return [
'FROM node:lts',
'WORKDIR /usr/src/app',
`COPY ${configuration.build.directory}/package*.json ./`,
configuration.build.command.installation && `RUN ${configuration.build.command.installation}`,
`COPY ./${configuration.build.directory} ./`,
`RUN ${configuration.build.command.build}`
].join('\n');
};
export async function buildImage(configuration, cacheBuild?: boolean) {
await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`,
buildImageNodeDocker(configuration)
);
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{
t: `${configuration.build.container.name}:${
cacheBuild
? `${configuration.build.container.tag}-cache`
: configuration.build.container.tag
}`
}
);
await streamEvents(stream, configuration);
}

View File

@@ -0,0 +1,25 @@
import vuejs from './vuejs';
import svelte from './svelte';
import Static from './static';
import rust from './rust';
import react from './react';
import php from './php';
import nuxtjs from './nuxtjs';
import nodejs from './nodejs';
import nextjs from './nextjs';
import gatsby from './gatsby';
import docker from './docker';
export {
vuejs,
svelte,
Static as static,
rust,
react,
php,
nuxtjs,
nodejs,
nextjs,
gatsby,
docker
};

View File

@@ -0,0 +1,30 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
import { buildImage } from '../helpers';
// `HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost:${configuration.publish.port}${configuration.publish.path} || exit 1`,
const publishNodejsDocker = (configuration) => {
return [
'FROM node:lts',
'WORKDIR /usr/src/app',
configuration.build.command.build
? `COPY --from=${configuration.build.container.name}:${configuration.build.container.tag} /usr/src/app/${configuration.publish.directory} ./`
: `
COPY ${configuration.build.directory}/package*.json ./
RUN ${configuration.build.command.installation}
COPY ./${configuration.build.directory} ./`,
`EXPOSE ${configuration.publish.port}`,
'CMD [ "yarn", "start" ]'
].join('\n');
};
export default async function (configuration) {
await buildImage(configuration);
await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`,
publishNodejsDocker(configuration)
);
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
);
await streamEvents(stream, configuration);
}

View File

@@ -0,0 +1,31 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
import { buildImage } from '../helpers';
// `HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost:${configuration.publish.port}${configuration.publish.path} || exit 1`,
const publishNodejsDocker = (configuration) => {
return [
'FROM node:lts',
'WORKDIR /usr/src/app',
configuration.build.command.build
? `COPY --from=${configuration.build.container.name}:${configuration.build.container.tag} /usr/src/app/${configuration.publish.directory} ./`
: `
COPY ${configuration.build.directory}/package*.json ./
RUN ${configuration.build.command.installation}
COPY ./${configuration.build.directory} ./`,
`EXPOSE ${configuration.publish.port}`,
'CMD [ "yarn", "start" ]'
].join('\n');
};
export default async function (configuration) {
if (configuration.build.command.build) await buildImage(configuration);
await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`,
publishNodejsDocker(configuration)
);
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
);
await streamEvents(stream, configuration);
}

View File

@@ -0,0 +1,31 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
import { buildImage } from '../helpers';
// `HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost:${configuration.publish.port}${configuration.publish.path} || exit 1`,
const publishNodejsDocker = (configuration) => {
return [
'FROM node:lts',
'WORKDIR /usr/src/app',
configuration.build.command.build
? `COPY --from=${configuration.build.container.name}:${configuration.build.container.tag} /usr/src/app/${configuration.publish.directory} ./`
: `
COPY ${configuration.build.directory}/package*.json ./
RUN ${configuration.build.command.installation}
COPY ./${configuration.build.directory} ./`,
`EXPOSE ${configuration.publish.port}`,
'CMD [ "yarn", "start" ]'
].join('\n');
};
export default async function (configuration) {
await buildImage(configuration);
await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`,
publishNodejsDocker(configuration)
);
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
);
await streamEvents(stream, configuration);
}

View File

@@ -0,0 +1,25 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
// 'HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost/ || exit 1',
const publishPHPDocker = (configuration) => {
return [
'FROM php:apache',
'RUN a2enmod rewrite',
'WORKDIR /usr/src/app',
`COPY ./${configuration.build.directory} /var/www/html`,
'EXPOSE 80',
' CMD ["apache2-foreground"]'
].join('\n');
};
export default async function (configuration) {
await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`,
publishPHPDocker(configuration)
);
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
);
await streamEvents(stream, configuration);
}

View File

@@ -0,0 +1,28 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
import { buildImage } from '../helpers';
// 'HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost/ || exit 1',
const publishStaticDocker = (configuration) => {
return [
'FROM nginx:stable-alpine',
'COPY nginx.conf /etc/nginx/nginx.conf',
'WORKDIR /usr/share/nginx/html',
`COPY --from=${configuration.build.container.name}:${configuration.build.container.tag}-cache /usr/src/app/${configuration.publish.directory} ./`,
'EXPOSE 80',
'CMD ["nginx", "-g", "daemon off;"]'
].join('\n');
};
export default async function (configuration) {
await buildImage(configuration, true);
await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`,
publishStaticDocker(configuration)
);
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
);
await streamEvents(stream, configuration);
}

View File

@@ -0,0 +1,66 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
import TOML from '@iarna/toml';
import { execShellAsync } from '$lib/api/common';
const publishRustDocker = (configuration, custom) => {
return [
'FROM rust:latest',
'WORKDIR /app',
`COPY --from=${configuration.build.container.name}:cache /app/target target`,
`COPY --from=${configuration.build.container.name}:cache /usr/local/cargo /usr/local/cargo`,
'COPY . .',
`RUN cargo build --release --bin ${custom.name}`,
'FROM debian:buster-slim',
'WORKDIR /app',
'RUN apt-get update -y && apt-get install -y --no-install-recommends openssl libcurl4 ca-certificates && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/*',
'RUN update-ca-certificates',
`COPY --from=${configuration.build.container.name}:cache /app/target/release/${custom.name} ${custom.name}`,
`EXPOSE ${configuration.publish.port}`,
`CMD ["/app/${custom.name}"]`
].join('\n');
};
const cacheRustDocker = (configuration, custom) => {
return [
`FROM rust:latest AS planner-${configuration.build.container.name}`,
'WORKDIR /app',
'RUN cargo install cargo-chef',
'COPY . .',
'RUN cargo chef prepare --recipe-path recipe.json',
'FROM rust:latest',
'WORKDIR /app',
'RUN cargo install cargo-chef',
`COPY --from=planner-${configuration.build.container.name} /app/recipe.json recipe.json`,
'RUN cargo chef cook --release --recipe-path recipe.json'
].join('\n');
};
export default async function (configuration) {
const cargoToml = await execShellAsync(`cat ${configuration.general.workdir}/Cargo.toml`);
const parsedToml = TOML.parse(cargoToml);
const custom = {
name: parsedToml.package.name
};
await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`,
cacheRustDocker(configuration, custom)
);
let stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:cache` }
);
await streamEvents(stream, configuration);
await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`,
publishRustDocker(configuration, custom)
);
stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
);
await streamEvents(stream, configuration);
}

View File

@@ -0,0 +1,30 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
import { buildImage } from '../helpers';
// 'HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost/ || exit 1',
const publishStaticDocker = (configuration) => {
return [
'FROM nginx:stable-alpine',
'COPY nginx.conf /etc/nginx/nginx.conf',
'WORKDIR /usr/share/nginx/html',
configuration.build.command.build
? `COPY --from=${configuration.build.container.name}:${configuration.build.container.tag}-cache /usr/src/app/${configuration.publish.directory} ./`
: `COPY ./${configuration.build.directory} ./`,
'EXPOSE 80',
'CMD ["nginx", "-g", "daemon off;"]'
].join('\n');
};
export default async function (configuration) {
if (configuration.build.command.build) await buildImage(configuration, true);
await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`,
publishStaticDocker(configuration)
);
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
);
await streamEvents(stream, configuration);
}

View File

@@ -0,0 +1,28 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
import { buildImage } from '../helpers';
// 'HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost/ || exit 1',
const publishStaticDocker = (configuration) => {
return [
'FROM nginx:stable-alpine',
'COPY nginx.conf /etc/nginx/nginx.conf',
'WORKDIR /usr/share/nginx/html',
`COPY --from=${configuration.build.container.name}:${configuration.build.container.tag}-cache /usr/src/app/${configuration.publish.directory} ./`,
'EXPOSE 80',
'CMD ["nginx", "-g", "daemon off;"]'
].join('\n');
};
export default async function (configuration) {
await buildImage(configuration, true);
await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`,
publishStaticDocker(configuration)
);
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
);
await streamEvents(stream, configuration);
}

View File

@@ -0,0 +1,28 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
import { buildImage } from '../helpers';
// 'HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost/ || exit 1',
const publishStaticDocker = (configuration) => {
return [
'FROM nginx:stable-alpine',
'COPY nginx.conf /etc/nginx/nginx.conf',
'WORKDIR /usr/share/nginx/html',
`COPY --from=${configuration.build.container.name}:${configuration.build.container.tag}-cache /usr/src/app/${configuration.publish.directory} ./`,
'EXPOSE 80',
'CMD ["nginx", "-g", "daemon off;"]'
].join('\n');
};
export default async function (configuration) {
await buildImage(configuration, true);
await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`,
publishStaticDocker(configuration)
);
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
);
await streamEvents(stream, configuration);
}

View File

@@ -0,0 +1,31 @@
import Deployment from '$models/Logs/Deployment';
import dayjs from 'dayjs';
import buildContainer from './buildContainer';
import { updateServiceLabels } from './configuration';
import copyFiles from './copyFiles';
import deploy from './deploy';
import { saveAppLog } from './logging';
export default async function (configuration, imageChanged) {
const { id, organization, name, branch } = configuration.repository;
const { domain } = configuration.publish;
const { deployId, nickname } = configuration.general;
await new Deployment({
repoId: id,
branch,
deployId,
domain,
organization,
name,
nickname
}).save();
await saveAppLog(`${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')} Queued.`, configuration);
await copyFiles(configuration);
await buildContainer(configuration);
await deploy(configuration, imageChanged);
await Deployment.findOneAndUpdate(
{ repoId: id, branch, deployId, organization, name, domain },
{ repoId: id, branch, deployId, organization, name, domain, progress: 'done' }
);
await updateServiceLabels(configuration);
}

View File

@@ -0,0 +1,57 @@
const defaultBuildAndDeploy = {
installation: 'yarn install',
build: 'yarn build'
};
const templates = {
svelte: {
pack: 'svelte',
...defaultBuildAndDeploy,
directory: 'public',
name: 'Svelte'
},
next: {
pack: 'nextjs',
...defaultBuildAndDeploy,
port: 3000,
name: 'NextJS'
},
nuxt: {
pack: 'nuxtjs',
...defaultBuildAndDeploy,
port: 3000,
name: 'NuxtJS'
},
'react-scripts': {
pack: 'react',
...defaultBuildAndDeploy,
directory: 'build',
name: 'React'
},
'parcel-bundler': {
pack: 'static',
...defaultBuildAndDeploy,
directory: 'dist',
name: 'Parcel'
},
'@vue/cli-service': {
pack: 'vuejs',
...defaultBuildAndDeploy,
directory: 'dist',
name: 'Vue'
},
gatsby: {
pack: 'gatsby',
...defaultBuildAndDeploy,
directory: 'public',
name: 'Gatsby'
},
'preact-cli': {
pack: 'react',
...defaultBuildAndDeploy,
directory: 'build',
name: 'Preact'
}
};
export default templates;

44
src/lib/api/common.ts Normal file
View File

@@ -0,0 +1,44 @@
import shell from 'shelljs';
import User from '$models/User';
import jsonwebtoken from 'jsonwebtoken';
export function execShellAsync(cmd, opts = {}) {
try {
return new Promise(function (resolve, reject) {
shell.config.silent = true;
shell.exec(cmd, opts, function (code, stdout, stderr) {
if (code !== 0) return reject(new Error(stderr));
return resolve(stdout);
});
});
} catch (error) {
return new Error('Oops');
}
}
export function cleanupTmp(dir) {
if (dir !== '/') shell.rm('-fr', dir);
}
export async function verifyUserId(token) {
const { JWT_SIGN_KEY } = process.env;
try {
const verify = jsonwebtoken.verify(token, JWT_SIGN_KEY);
const found = await User.findOne({ uid: verify.jti });
if (found) {
return Promise.resolve(true);
} else {
return Promise.reject(false);
}
} catch (error) {
console.log(error);
return Promise.reject(false);
}
}
export function delay(t) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve('OK');
}, t);
});
}

27
src/lib/api/docker.ts Normal file
View File

@@ -0,0 +1,27 @@
import Dockerode from 'dockerode';
import { saveAppLog } from './applications/logging';
const { DOCKER_ENGINE, DOCKER_NETWORK } = process.env;
export const docker = {
engine: new Dockerode({
socketPath: DOCKER_ENGINE
}),
network: DOCKER_NETWORK
};
export async function streamEvents(stream, configuration) {
await new Promise((resolve, reject) => {
docker.engine.modem.followProgress(stream, onFinished, onProgress);
function onFinished(err, res) {
if (err) reject(err);
resolve(res);
}
function onProgress(event) {
if (event.error) {
saveAppLog(event.error, configuration, true);
reject(event.error);
} else if (event.stream) {
saveAppLog(event.stream, configuration);
}
}
});
}

102
src/lib/api/request.ts Normal file
View File

@@ -0,0 +1,102 @@
import { toast } from '@zerodevx/svelte-toast';
import { browser } from '$app/env';
export async function request(
url,
session,
{
method,
body,
customHeaders
}: {
url?: string;
session?: any;
fetch?: any;
method?: string;
body?: any;
customHeaders?: Record<string, unknown>;
} = {}
) {
let fetch;
if (browser) {
fetch = window.fetch;
} else {
fetch = session.fetch;
}
let headers = { 'content-type': 'application/json; charset=UTF-8' };
if (method === 'DELETE') {
delete headers['content-type'];
}
const isGithub = url.match(/api.github.com/);
if (isGithub) {
headers = Object.assign(headers, {
Authorization: `token ${session.ghToken}`
});
}
const config: any = {
method: method || (body ? 'POST' : 'GET'),
cache: isGithub ? 'no-cache' : 'default',
headers: {
...headers,
...customHeaders
}
};
if (body) {
config.body = JSON.stringify(body);
}
const response = await fetch(url, config);
if (response.status >= 200 && response.status <= 299) {
if (response.headers.get('content-type').match(/application\/json/)) {
const json = await response.json();
if (json?.success === false) {
browser && json.showToast !== false && toast.push(json.message);
return Promise.reject({
status: response.status,
error: json.message
});
}
return json;
} else if (response.headers.get('content-type').match(/text\/plain/)) {
return await response.text();
} else if (response.headers.get('content-type').match(/multipart\/form-data/)) {
return await response.formData();
} else {
console.log(response);
if (response.headers.get('content-disposition')) {
const blob = await response.blob();
console.log(blob);
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = response.headers.get('content-disposition').split('=')[1] || 'backup.gz';
link.target = '_blank';
link.setAttribute('type', 'hidden');
document.body.appendChild(link);
link.click();
link.remove();
return;
}
return await response.blob();
}
} else {
if (response.status === 401) {
browser && toast.push('Unauthorized');
return Promise.reject({
status: response.status,
error: 'Unauthorized'
});
} else if (response.status >= 500) {
const error = (await response.json()).error;
browser && toast.push(error);
return Promise.reject({
status: response.status,
error: error || 'Oops, something is not okay. Are you okay?'
});
} else {
browser && toast.push(response.statusText);
return Promise.reject({
status: response.status,
error: response.statusText
});
}
}
}

1
src/lib/consts.ts Normal file
View File

@@ -0,0 +1 @@
export const publicPages = ['/', '/api/v1/login/github/app', '/api/v1/webhooks/deploy', '/success'];

View File

@@ -0,0 +1,11 @@
import mongoose from 'mongoose';
const { Schema } = mongoose;
const ApplicationLogsSchema = new Schema({
deployId: { type: String, required: true },
event: { type: String, required: true }
});
ApplicationLogsSchema.set('timestamps', true);
export default mongoose.model('logs-application', ApplicationLogsSchema);

View File

@@ -0,0 +1,17 @@
import mongoose from 'mongoose';
const { Schema } = mongoose;
const DeploymentSchema = new Schema({
deployId: { type: String, required: true },
nickname: { type: String, required: true },
repoId: { type: Number, required: true },
organization: { type: String, required: true },
name: { type: String, required: true },
branch: { type: String, required: true },
domain: { type: String, required: true },
progress: { type: String, require: true, default: 'queued' }
});
DeploymentSchema.set('timestamps', true);
export default mongoose.model('deployment', DeploymentSchema);

23
src/models/Logs/Server.ts Normal file
View File

@@ -0,0 +1,23 @@
import mongoose from 'mongoose';
import { version } from '../../../package.json';
const { Schema, Document } = mongoose;
// export interface ILogsServer extends Document {
// version: string;
// type: string;
// message: string;
// stack: string;
// seen: Boolean;
// }
const LogsServerSchema = new Schema({
version: { type: String, default: version },
type: { type: String, required: true },
message: { type: String, required: true },
stack: { type: String },
seen: { type: Boolean, default: false }
});
LogsServerSchema.set('timestamps', { createdAt: 'createdAt', updatedAt: false });
export default mongoose.model('logs-server', LogsServerSchema);

17
src/models/Settings.ts Normal file
View File

@@ -0,0 +1,17 @@
import mongoose from 'mongoose';
const { Schema } = mongoose;
export interface ISettings extends Document {
applicationName: string;
allowRegistration: string;
sendErrors: boolean;
}
const SettingsSchema = new Schema({
applicationName: { type: String, required: true, default: 'coolify' },
allowRegistration: { type: Boolean, required: true, default: false },
sendErrors: { type: Boolean, required: true, default: true }
});
SettingsSchema.set('timestamps', true);
export default mongoose.model('settings', SettingsSchema);

17
src/models/User.ts Normal file
View File

@@ -0,0 +1,17 @@
import mongoose from 'mongoose';
const { Schema } = mongoose;
export interface IUser extends Document {
email: string;
avatar?: string;
uid: string;
}
const UserSchema = new Schema({
email: { type: String, required: true, unique: true },
avatar: { type: String },
uid: { type: String, required: true }
});
UserSchema.set('timestamps', true);
export default mongoose.model('user', UserSchema);

View File

@@ -1,11 +0,0 @@
<script>
import { url } from "@roxi/routify/runtime";
</script>
<div class="text-center text-white pt-10">
<div class="text-6xl font-bold tracking-tight py-3 text-center">
404
</div>
<div class="pt-2 py-3 font-bold text-xl">Ah you lost. Don't worry. I'm here for you!</div>
<a class="font-bold hover:underline text-2xl" href="{$url('/dashboard')}">Go back</a>
</div>

View File

@@ -1,354 +0,0 @@
<style lang="postcss">
.min-w-4rem {
min-width: 4rem;
}
</style>
<script>
import { goto, route, isChangingPage } from "@roxi/routify/runtime";
import { loggedIn, session, fetch, deployments, activePage } from "@store";
import { toast } from "@zerodevx/svelte-toast";
import { onMount } from "svelte";
import compareVersions from "compare-versions";
import packageJson from "../../package.json";
import Tooltip from "../components/Tooltip/Tooltip.svelte";
let upgradeAvailable = false;
let upgradeDisabled = false;
let upgradeDone = false;
let latest = {};
let showAck = false;
const branch =
process.env.NODE_ENV === "production" &&
window.location.hostname !== "test.andrasbacsai.dev"
? "main"
: "next";
$: if ($isChangingPage) {
const path = window.location.pathname;
if (path === "/dashboard/applications" || path.match(/\/application/)) {
$activePage.mainmenu = "applications";
} else if (path === "/dashboard/databases" || path.match(/\/database/)) {
$activePage.mainmenu = "databases";
} else if (path === "/dashboard/services" || path.match(/\/service/)) {
$activePage.mainmenu = "services";
} else if (path === "/settings") {
$activePage.mainmenu = "settings";
} else {
$activePage.mainmenu = null;
}
if (path.match(/\/application\/.*\/logs/)) {
$activePage.application = "logs";
$activePage.new = false;
} else if (path === "/application/new") {
$activePage.application = "configuration";
$activePage.new = true;
} else if (path.match(/\/application\/.*\/configuration/)) {
$activePage.application = "configuration";
$activePage.new = false;
} else {
$activePage.application = null;
}
}
onMount(async () => {
if ($session.token) {
upgradeAvailable = await checkUpgrade();
if (!localStorage.getItem("automaticErrorReportsAck")) {
showAck = true;
if (latest?.coolify[branch]?.settings?.sendErrors) {
const settings = {
sendErrors: true,
};
await $fetch("/api/v1/settings", {
body: {
...settings,
},
headers: {
Authorization: `Bearer ${$session.token}`,
},
});
}
}
}
});
function ackError() {
localStorage.setItem("automaticErrorReportsAck", "true");
showAck = false;
}
async function verifyToken() {
if ($session.token) {
try {
await $fetch("/api/v1/verify", {
headers: {
Authorization: `Bearer ${$session.token}`,
},
});
$deployments = await $fetch(`/api/v1/dashboard`);
} catch (e) {
toast.push("Unauthorized.");
logout();
}
}
}
if (!$loggedIn) {
logout();
$goto("/index");
}
function logout() {
localStorage.removeItem("token");
$session.token = null;
$session.githubAppToken = null;
$goto("/");
}
function reloadInAMin() {
setTimeout(() => {
location.reload();
}, 30000);
}
async function upgrade() {
try {
upgradeDisabled = true;
await $fetch(`/api/v1/upgrade`);
upgradeDone = true;
} catch (error) {
toast.push(
"Something happened during update. Ooops. Automatic error reporting will happen soon.",
);
}
}
async function checkUpgrade() {
latest = await window
.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;
}
</script>
{#await verifyToken() then notUsed}
{#if showAck}
<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}
{#if $route.path !== "/index"}
<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="{$activePage.mainmenu === 'applications'}"
class:border-purple-500="{$activePage.mainmenu === 'databases'}"
>
<img class="w-10 pt-4 pb-4" src="/favicon.png" alt="coolLabs logo" />
<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="{$activePage.mainmenu === 'applications'}"
class:bg-warmGray-700="{$activePage.mainmenu === 'applications'}"
>
<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><rect x="9" y="9" width="6" height="6"></rect><line
x1="9"
y1="1"
x2="9"
y2="4"></line><line x1="15" y1="1" x2="15" y2="4"></line><line
x1="9"
y1="20"
x2="9"
y2="23"></line><line x1="15" y1="20" x2="15" y2="23"
></line><line x1="20" y1="9" x2="23" y2="9"></line><line
x1="20"
y1="14"
x2="23"
y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line
x1="1"
y1="14"
x2="4"
y2="14"></line></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="{$activePage.mainmenu === 'databases'}"
class:bg-warmGray-700="{$activePage.mainmenu === 'databases'}"
>
<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"
></path>
</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"
on:click="{() => $goto('/dashboard/services')}"
class:text-blue-500="{$activePage.mainmenu === 'services'}"
class:bg-warmGray-700="{$activePage.mainmenu === '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"
></path>
</svg>
</div>
</Tooltip>
<div class="flex-1"></div>
<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="{$activePage.mainmenu === 'settings'}"
class:bg-warmGray-700="{$activePage.mainmenu === '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>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
</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"
></path><polyline points="16 17 21 12 16 7"></polyline><line
x1="21"
y1="12"
x2="9"
y2="12"></line></svg
>
</button>
</Tooltip>
<div
class="cursor-pointer text-xs font-bold text-warmGray-400 py-2 hover:bg-warmGray-700 w-full text-center"
>
{packageJson.version}
</div>
</div>
</nav>
{/if}
{#if upgradeAvailable}
<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>
<div class="flex-1"></div>
{#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}
<main class:main="{$route.path !== '/index'}">
<slot />
</main>
{:catch test}
{$goto("/index")}
{/await}

View File

@@ -1,5 +0,0 @@
<script>
import Configuration from "../../../../../components/Application/Configuration/Configuration.svelte";
</script>
<Configuration />

View File

@@ -1,4 +0,0 @@
<script>
import { params, redirect } from "@roxi/routify";
$redirect(`/application/${$params.organization}/${$params.name}/${$params.branch}/configuration`);
</script>

View File

@@ -1,62 +0,0 @@
<script>
import { params, redirect } from "@roxi/routify";
import { onDestroy, onMount } from "svelte";
import { fade } from "svelte/transition";
import { fetch } from "@store";
import Loading from "../../../../../../components/Loading.svelte";
let loadLogsInterval;
let logs = [];
onMount(() => {
loadLogsInterval = setInterval(() => {
loadLogs();
}, 500);
});
async function loadLogs() {
try {
const { events, progress } = await $fetch(
`/api/v1/application/deploy/logs/${$params.deployId}`,
);
logs = [...events];
if (progress === "done" || progress === "failed") {
clearInterval(loadLogsInterval);
}
} catch(error) {
$redirect('/dashboard')
}
}
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>
</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}

View File

@@ -1,140 +0,0 @@
<style lang="postcss">
.w-300 {
width: 300px !important;
}
</style>
<script>
import { fetch, application, dateOptions } from "@store";
import { fade } from "svelte/transition";
import { goto } from "@roxi/routify";
import { onDestroy, onMount } from "svelte";
import Loading from "../../../../../../components/Loading.svelte";
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 $fetch(
`/api/v1/application/deploy/logs?repoId=${$application.repository.id}&branch=${$application.repository.branch}&page=${page}`,
);
}
async function loadApplicationLogs() {
logs = (
await $fetch(
`/api/v1/application/logs?name=${$application.build.container.name}`,
)
).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}
{#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(`./${deployment.deployId}`)}"
>
<div
class="font-bold text-sm px-3 flex justify-center items-center"
>
{deployment.branch}
</div>
<div class="flex-1"></div>
<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}
<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}

View File

@@ -1,30 +0,0 @@
<script>
import { application } from "@store";
import Configuration from "../../../../../components/Application/Configuration/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"
>
Overview of
<a
target="_blank"
class="hover:underline cursor-pointer px-2"
href="{'https://' +
$application.publish.domain +
$application.publish.path}">{$application.publish.domain}</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>
<Configuration />

View File

@@ -1,4 +0,0 @@
<script>
import { redirect } from "@roxi/routify";
$redirect("/dashboard/applications");
</script>

View File

@@ -1,4 +0,0 @@
<script>
import { redirect } from "@roxi/routify";
$redirect("/dashboard/applications");
</script>

View File

@@ -1,75 +0,0 @@
<script>
import { params, redirect } from "@roxi/routify";
import {
application,
fetch,
initialApplication,
initConf,
deployments,
activePage,
} from "@store";
import { onDestroy } from "svelte";
import Loading from "../../components/Loading.svelte";
import { toast } from "@zerodevx/svelte-toast";
import Navbar from "../../components/Application/Navbar.svelte";
$application.repository.organization = $params.organization;
$application.repository.name = $params.name;
$application.repository.branch = $params.branch;
async function setConfiguration() {
try {
const config = await $fetch(`/api/v1/config`, {
body: {
name: $application.repository.name,
organization: $application.repository.organization,
branch: $application.repository.branch,
},
});
$application = { ...config };
$initConf = JSON.parse(JSON.stringify($application));
} catch (error) {
toast.push("Configuration not found.");
$redirect("/dashboard/applications");
}
}
async function loadConfiguration() {
if (!$activePage.new) {
if ($deployments.length === 0) {
await setConfiguration();
} else {
const found = $deployments.applications.deployed.find(app => {
const { organization, name, branch } = app.configuration;
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">
<slot />
</div>
{/await}

View File

@@ -1,4 +0,0 @@
<script>
import { redirect } from "@roxi/routify";
$redirect("/dashboard/applications");
</script>

View File

@@ -1,5 +0,0 @@
<script>
import Configuration from "../../components/Application/Configuration/Configuration.svelte";
</script>
<Configuration />

View File

@@ -1,30 +0,0 @@
<script>
import { fetch, deployments } from "@store";
import { onDestroy, onMount } from "svelte";
import { toast } from "@zerodevx/svelte-toast";
let loadDashboardInterval = null;
async function loadDashboard() {
try {
$deployments = await $fetch(`/api/v1/dashboard`);
} catch (error) {
toast.push(error?.error || error);
}
}
onMount(() => {
loadDashboard();
loadDashboardInterval = setInterval(() => {
loadDashboard();
}, 2000);
});
onDestroy(() => {
clearInterval(loadDashboardInterval);
});
</script>
<div class="min-h-full text-white">
<slot />
</div>

File diff suppressed because one or more lines are too long

View File

@@ -1,156 +0,0 @@
<style lang="postcss">
.gradient-border {
--border-width: 2px;
position: relative;
display: flex;
justify-content: center;
width: 208px;
height: 126px;
background: #222;
border-radius: 0.75rem;
}
.gradient-border::after {
position: absolute;
content: "";
top: calc(-1 * var(--border-width));
left: calc(-1 * var(--border-width));
z-index: -1;
width: calc(100% + var(--border-width) * 2);
height: calc(100% + var(--border-width) * 2);
background: linear-gradient(
60deg,
hsl(224, 85%, 66%),
hsl(269, 85%, 66%),
hsl(314, 85%, 66%),
hsl(359, 85%, 66%),
hsl(44, 85%, 66%),
hsl(89, 85%, 66%),
hsl(134, 85%, 66%),
hsl(179, 85%, 66%)
);
background-size: 300% 300%;
background-position: 0 50%;
border-radius: calc(2 * var(--border-width));
animation: moveGradient 1s alternate infinite;
}
@keyframes moveGradient {
50% {
background-position: 100% 50%;
}
}
</style>
<script>
import { deployments, dbInprogress } from "@store";
import { fade } from "svelte/transition";
import { goto } from "@roxi/routify/runtime";
import MongoDb from "../../components/Databases/SVGs/MongoDb.svelte";
import Postgresql from "../../components/Databases/SVGs/Postgresql.svelte";
import Mysql from "../../components/Databases/SVGs/Mysql.svelte";
import CouchDb from "../../components/Databases/SVGs/CouchDb.svelte";
import Clickhouse from "../../components/Databases/SVGs/Clickhouse.svelte";
const initialNumberOfDBs = $deployments.databases?.deployed.length;
$: if ($deployments.databases?.deployed.length) {
if (initialNumberOfDBs !== $deployments.databases?.deployed.length) {
$dbInprogress = false;
}
}
</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"></path>
</svg>
</button>
</div>
<div in:fade="{{ duration: 100 }}">
{#if $deployments.databases?.deployed.length > 0}
<div class="px-4 mx-auto py-5">
<div class="flex items-center justify-center flex-wrap">
{#each $deployments.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}
{#if $dbInprogress}
<div class=" px-4 pb-4">
<div
class="gradient-border text-xs font-bold text-warmGray-300 pt-6"
>
Working...
</div>
</div>
{/if}
</div>
</div>
{:else if $dbInprogress}
<div class="px-4 mx-auto py-5">
<div class="flex items-center justify-center flex-wrap">
<div class=" px-4 pb-4">
<div class="gradient-border text-xs font-bold text-warmGray-300 pt-6">
Working...
</div>
</div>
</div>
</div>
{:else}
<div class="text-2xl font-bold text-center">No databases found</div>
{/if}
</div>

View File

@@ -1,4 +0,0 @@
<script>
import { redirect } from "@roxi/routify";
$redirect("/dashboard/applications");
</script>

View File

@@ -1,74 +0,0 @@
<script>
import { deployments, dateOptions } from "@store";
import { fade } from "svelte/transition";
import { goto } from "@roxi/routify/runtime";
function switchTo(application) {
const { branch, name, organization } = application;
$goto(`/application/:organization/:name/:branch`, {
name,
organization,
branch,
});
}
</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"></path>
</svg>
</button>
</div>
<div in:fade="{{ duration: 100 }}">
{#if $deployments?.services?.deployed.length > 0}
<div class="px-4 mx-auto py-5">
<div class="flex items-center justify-center flex-wrap">
{#each $deployments?.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>

View File

@@ -1,87 +0,0 @@
<script>
import { fetch, database } from "@store";
import { redirect, params } from "@roxi/routify/runtime";
import { toast } from "@zerodevx/svelte-toast";
import { fade } from "svelte/transition";
import CouchDb from "../../../components/Databases/SVGs/CouchDb.svelte";
import MongoDb from "../../../components/Databases/SVGs/MongoDb.svelte";
import Mysql from "../../../components/Databases/SVGs/Mysql.svelte";
import Postgresql from "../../../components/Databases/SVGs/Postgresql.svelte";
import Loading from "../../../components/Loading.svelte";
import PasswordField from "../../../components/PasswordField.svelte";
$: name = $params.name;
async function loadDatabaseConfig() {
if (name) {
try {
$database = await $fetch(`/api/v1/databases/${name}`);
} catch (error) {
toast.push(`Cannot find database ${name}`);
$redirect(`/dashboard/databases`);
}
}
}
</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>
{/await}

View File

@@ -1,4 +0,0 @@
<script>
import { redirect } from "@roxi/routify";
$redirect("/dashboard/databases");
</script>

View File

@@ -1,80 +0,0 @@
<script>
import { params, goto, isActive, redirect } from "@roxi/routify";
import { fetch, database, initialDatabase } from "@store";
import { toast } from "@zerodevx/svelte-toast";
import { onDestroy } from "svelte";
import Tooltip from "../../components/Tooltip/Tooltip.svelte";
$: name = $params.name;
onDestroy(() => {
$database = JSON.parse(JSON.stringify(initialDatabase));
});
async function removeDB() {
await $fetch(`/api/v1/databases/${name}`, {
method: "DELETE",
});
toast.push("Database removed.");
$redirect(`/dashboard/databases`);
}
</script>
{#if !$isActive("/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"
></path>
</svg>
</button>
</Tooltip>
<div class="border border-warmGray-700 h-8"></div>
<Tooltip position="bottom-left" label="Configuration">
<button
class="icon hover:text-yellow-400"
disabled="{$isActive(`/database/new`)}"
class:text-yellow-400="{$isActive(`/database/${name}/configuration`) ||
$isActive(`/application/new`)}"
class:bg-warmGray-700="{$isActive(`/database/${name}/configuration`) ||
$isActive(`/database/new`)}"
on:click="{() => $goto(`/database/${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"
></path>
</svg>
</button>
</Tooltip>
</nav>
{/if}
<div class="text-white">
<slot />
</div>

View File

@@ -1,5 +0,0 @@
<script>
import { redirect } from "@roxi/routify";
$redirect("/dashboard/databases");
</script>

View File

@@ -1,13 +0,0 @@
<script>
import Configuration from "../../components/Databases/Configuration/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 />

View File

@@ -1,65 +0,0 @@
<script>
import { goto } from "@roxi/routify";
import { session, loggedIn } from "@store";
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);
const jwtToken = new URL(newWindow.document.URL).searchParams.get(
"jwtToken",
);
const ghToken = new URL(newWindow.document.URL).searchParams.get(
"ghToken",
);
if (ghToken) {
$session.githubAppToken = ghToken;
}
if (jwtToken) {
$session.token = jwtToken;
localStorage.setItem("token", jwtToken);
$goto("/dashboard/applications");
}
}
}, 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 !$loggedIn}
<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>

View File

@@ -1,74 +0,0 @@
<script>
import { params, goto, isActive, redirect } from "@roxi/routify";
import { fetch } from "@store";
import { toast } from "@zerodevx/svelte-toast";
import Tooltip from "../../../components/Tooltip/Tooltip.svelte";
$: name = $params.name;
async function removeService() {
await $fetch(`/api/v1/services/${name}`, {
method: "DELETE",
});
toast.push("Service removed.");
$redirect(`/dashboard/services`);
}
</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"
></path>
</svg>
</button>
</Tooltip>
<div class="border border-warmGray-700 h-8"></div>
<Tooltip position="bottom-left" label="Configuration">
<button
class="icon hover:text-yellow-400"
disabled="{$isActive(`/application/new`)}"
class:text-yellow-400="{$isActive(`/service/${name}/configuration`) ||
$isActive(`/application/new`)}"
class:bg-warmGray-700="{$isActive(`/service/${name}/configuration`) ||
$isActive(`/application/new`)}"
on:click="{() => $goto(`/service/${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"
></path>
</svg>
</button>
</Tooltip>
</nav>
<div class="text-white">
<slot />
</div>

View File

@@ -1,68 +0,0 @@
<script>
import { fetch } from "@store";
import { redirect, params } from "@roxi/routify/runtime";
import { fade } from "svelte/transition";
import { toast } from "@zerodevx/svelte-toast";
import Loading from "../../../components/Loading.svelte";
import Plausible from "../../../components/Services/Plausible.svelte";
$: name = $params.name;
let service = {};
async function loadServiceConfig() {
if (name) {
try {
service = await $fetch(`/api/v1/services/${name}`);
} catch (error) {
toast.push(`Cannot find service ${name}?!`);
$redirect(`/dashboard/services`);
}
}
}
async function activate() {
try {
await $fetch(`/api/v1/services/deploy/${name}/activate`, {
method: "PATCH",
body: {},
});
toast.push(`All users are activated for Plausible.`);
} catch (error) {
console.log(error);
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"
>
<a
href="{service.config.baseURL}"
target="_blank"
class="inline-flex hover:underline cursor-pointer px-2"
>
<div>{name === "plausible" ? "Plausible Analytics" : name}</div>
<div class="px-4">
{#if 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>
</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 name === "plausible"}
<Plausible service="{service}" />
{/if}
</div>
</div>
{/await}

View File

@@ -1,5 +0,0 @@
<script>
import { redirect } from "@roxi/routify";
$redirect("/dashboard/services");
</script>

View File

@@ -1,32 +0,0 @@
<script>
import { params, redirect } from "@roxi/routify";
import { fetch, newService, initialNewService } from "@store";
import { toast } from "@zerodevx/svelte-toast";
import { onDestroy } from "svelte";
import Loading from "../../../../components/Loading.svelte";
$: type = $params.type;
async function checkService() {
try {
await $fetch(`/api/v1/services/${type}`);
$redirect(`/dashboard/services`);
toast.push(
`${
type === "plausible" ? "Plausible Analytics" : type
} already deployed.`,
);
} catch (error) {
//
}
}
onDestroy(() => {
$newService = JSON.parse(JSON.stringify(initialNewService));
});
</script>
{#await checkService()}
<Loading />
{:then}
<div class="text-white">
<slot />
</div>
{/await}

View File

@@ -1,136 +0,0 @@
<script>
import { redirect, params, isActive } from "@roxi/routify/runtime";
import { newService, fetch } from "@store";
import { fade } from "svelte/transition";
import Loading from "../../../../components/Loading.svelte";
import TooltipInfo from "../../../../components/Tooltip/TooltipInfo.svelte";
import { toast } from "@zerodevx/svelte-toast";
$: type = $params.type;
$: 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 $fetch(`/api/v1/services/deploy/${type}`, {
body: payload,
});
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 },
);
$redirect(`/dashboard/services`);
} catch (error) {
console.log(error);
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 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}

View File

@@ -1,32 +0,0 @@
<script>
import { isActive, goto } from "@roxi/routify/runtime";
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 $isActive("/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>

View File

@@ -1,174 +0,0 @@
<script>
import { toast } from "@zerodevx/svelte-toast";
import { fade } from "svelte/transition";
import { fetch } from "@store";
import Loading from "../../components/Loading.svelte";
let settings = {
allowRegistration: false,
sendErrors: true
};
async function loadSettings() {
const response = await $fetch(`/api/v1/settings`);
settings.allowRegistration = response.settings.allowRegistration;
settings.sendErrors = response.settings.sendErrors;
}
async function changeSettings(value) {
settings[value] = !settings[value];
await $fetch(`/api/v1/settings`, {
body: {
...settings,
},
});
toast.push("Configuration saved.");
}
</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>
{#await loadSettings()}
<Loading />
{:then}
<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"></path>
</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"
></path>
</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"></path>
</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"
></path>
</svg>
</span>
</span>
</button>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
{/await}

15
src/routes/__error.svelte Normal file
View 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
View 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
View 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()
};
}

View 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
}
};
}
}

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