mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-28 04:18:20 +00:00
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:
1 parent
cccb9a5fec
commit
23a4ebb74a
229 files changed
+7781
-11333
No files matched your search
@@ -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}" />
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { application } from "@store";
|
||||
import { application } from "$store";
|
||||
|
||||
let secret = {
|
||||
name: null,
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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>
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
File renamed without changes.
Vendored
+115
@@ -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;
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import App from './App.svelte'
|
||||
import './index.css'
|
||||
|
||||
const app = new App({
|
||||
target: document.body
|
||||
})
|
||||
|
||||
export default app
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
};
|
||||
@@ -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}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 })
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const publicPages = ['/', '/api/v1/login/github/app', '/api/v1/webhooks/deploy', '/success'];
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
@@ -1,5 +0,0 @@
|
||||
<script>
|
||||
import Configuration from "../../../../../components/Application/Configuration/Configuration.svelte";
|
||||
</script>
|
||||
|
||||
<Configuration />
|
||||
@@ -1,4 +0,0 @@
|
||||
<script>
|
||||
import { params, redirect } from "@roxi/routify";
|
||||
$redirect(`/application/${$params.organization}/${$params.name}/${$params.branch}/configuration`);
|
||||
</script>
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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 />
|
||||
@@ -1,4 +0,0 @@
|
||||
<script>
|
||||
import { redirect } from "@roxi/routify";
|
||||
$redirect("/dashboard/applications");
|
||||
</script>
|
||||
@@ -1,4 +0,0 @@
|
||||
<script>
|
||||
import { redirect } from "@roxi/routify";
|
||||
$redirect("/dashboard/applications");
|
||||
</script>
|
||||
@@ -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}
|
||||
@@ -1,4 +0,0 @@
|
||||
<script>
|
||||
import { redirect } from "@roxi/routify";
|
||||
$redirect("/dashboard/applications");
|
||||
</script>
|
||||
@@ -1,5 +0,0 @@
|
||||
<script>
|
||||
import Configuration from "../../components/Application/Configuration/Configuration.svelte";
|
||||
</script>
|
||||
|
||||
<Configuration />
|
||||
@@ -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>
|
||||
@@ -1,295 +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/configuration`, {
|
||||
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>Applications</div>
|
||||
<button
|
||||
class="icon p-1 ml-4 bg-green-500 hover:bg-green-400"
|
||||
on:click="{() => $goto('/application/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.applications?.deployed.length > 0}
|
||||
<div class="px-4 mx-auto py-5 z-auto">
|
||||
<div class="flex items-center justify-center flex-wrap">
|
||||
{#each $deployments.applications.deployed as application}
|
||||
<div class="px-4 pb-4">
|
||||
<div
|
||||
class="relative rounded-xl p-6 bg-warmGray-800 border-2 border-dashed border-transparent hover:border-green-500 text-white shadow-md cursor-pointer ease-in-out transform hover:scale-105 duration-100 group"
|
||||
on:click="{() =>
|
||||
switchTo({
|
||||
branch:
|
||||
application.configuration.repository.branch,
|
||||
name: application.configuration.repository.name,
|
||||
organization:
|
||||
application.configuration.repository
|
||||
.organization,
|
||||
})}"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
{#if application.configuration.build.pack === "static"}
|
||||
<svg
|
||||
class="text-white w-10 h-10 absolute top-0 left-0 -m-4"
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><g clip-path="url(#HTML5_Clip0_4)"
|
||||
><path
|
||||
d="M30.216 0L27.6454 28.7967L16.0907 32L4.56783 28.8012L2 0H30.216Z"
|
||||
fill="#E44D26"></path><path
|
||||
d="M16.108 29.5515L25.4447 26.963L27.6415 2.35497H16.108V29.5515Z"
|
||||
fill="#F16529"></path><path
|
||||
d="M11.1109 9.4197H16.108V5.88731H7.25053L7.33509 6.83499L8.20327 16.5692H16.108V13.0369H11.4338L11.1109 9.4197Z"
|
||||
fill="#EBEBEB"></path><path
|
||||
d="M11.907 18.3354H8.36111L8.856 23.8818L16.0917 25.8904L16.108 25.8859V22.2108L16.0925 22.2149L12.1585 21.1527L11.907 18.3354Z"
|
||||
fill="#EBEBEB"></path><path
|
||||
d="M16.0958 16.5692H20.4455L20.0354 21.1504L16.0958 22.2138V25.8887L23.3373 23.8817L23.3904 23.285L24.2205 13.9855L24.3067 13.0369H16.0958V16.5692Z"
|
||||
fill="white"></path><path
|
||||
d="M16.0958 9.41105V9.41969H24.6281L24.6989 8.62572L24.8599 6.83499L24.9444 5.88731H16.0958V9.41105Z"
|
||||
fill="white"></path></g
|
||||
><defs
|
||||
><clipPath id="HTML5_Clip0_4"
|
||||
><rect width="32" height="32" fill="white"
|
||||
></rect></clipPath
|
||||
></defs
|
||||
></svg
|
||||
>
|
||||
{:else if application.configuration.build.pack === "react"}
|
||||
<svg
|
||||
class="text-blue-500 w-10 h-10 absolute top-0 left-0 -m-4"
|
||||
viewBox="0 0 128 128"
|
||||
>
|
||||
<g fill="#61DAFB"
|
||||
><circle cx="64" cy="64" r="11.4"></circle><path
|
||||
d="M107.3 45.2c-2.2-.8-4.5-1.6-6.9-2.3.6-2.4 1.1-4.8 1.5-7.1 2.1-13.2-.2-22.5-6.6-26.1-1.9-1.1-4-1.6-6.4-1.6-7 0-15.9 5.2-24.9 13.9-9-8.7-17.9-13.9-24.9-13.9-2.4 0-4.5.5-6.4 1.6-6.4 3.7-8.7 13-6.6 26.1.4 2.3.9 4.7 1.5 7.1-2.4.7-4.7 1.4-6.9 2.3-12.5 4.8-19.3 11.4-19.3 18.8s6.9 14 19.3 18.8c2.2.8 4.5 1.6 6.9 2.3-.6 2.4-1.1 4.8-1.5 7.1-2.1 13.2.2 22.5 6.6 26.1 1.9 1.1 4 1.6 6.4 1.6 7.1 0 16-5.2 24.9-13.9 9 8.7 17.9 13.9 24.9 13.9 2.4 0 4.5-.5 6.4-1.6 6.4-3.7 8.7-13 6.6-26.1-.4-2.3-.9-4.7-1.5-7.1 2.4-.7 4.7-1.4 6.9-2.3 12.5-4.8 19.3-11.4 19.3-18.8s-6.8-14-19.3-18.8zm-14.8-30.5c4.1 2.4 5.5 9.8 3.8 20.3-.3 2.1-.8 4.3-1.4 6.6-5.2-1.2-10.7-2-16.5-2.5-3.4-4.8-6.9-9.1-10.4-13 7.4-7.3 14.9-12.3 21-12.3 1.3 0 2.5.3 3.5.9zm-11.2 59.3c-1.8 3.2-3.9 6.4-6.1 9.6-3.7.3-7.4.4-11.2.4-3.9 0-7.6-.1-11.2-.4-2.2-3.2-4.2-6.4-6-9.6-1.9-3.3-3.7-6.7-5.3-10 1.6-3.3 3.4-6.7 5.3-10 1.8-3.2 3.9-6.4 6.1-9.6 3.7-.3 7.4-.4 11.2-.4 3.9 0 7.6.1 11.2.4 2.2 3.2 4.2 6.4 6 9.6 1.9 3.3 3.7 6.7 5.3 10-1.7 3.3-3.4 6.6-5.3 10zm8.3-3.3c1.5 3.5 2.7 6.9 3.8 10.3-3.4.8-7 1.4-10.8 1.9 1.2-1.9 2.5-3.9 3.6-6 1.2-2.1 2.3-4.2 3.4-6.2zm-25.6 27.1c-2.4-2.6-4.7-5.4-6.9-8.3 2.3.1 4.6.2 6.9.2 2.3 0 4.6-.1 6.9-.2-2.2 2.9-4.5 5.7-6.9 8.3zm-18.6-15c-3.8-.5-7.4-1.1-10.8-1.9 1.1-3.3 2.3-6.8 3.8-10.3 1.1 2 2.2 4.1 3.4 6.1 1.2 2.2 2.4 4.1 3.6 6.1zm-7-25.5c-1.5-3.5-2.7-6.9-3.8-10.3 3.4-.8 7-1.4 10.8-1.9-1.2 1.9-2.5 3.9-3.6 6-1.2 2.1-2.3 4.2-3.4 6.2zm25.6-27.1c2.4 2.6 4.7 5.4 6.9 8.3-2.3-.1-4.6-.2-6.9-.2-2.3 0-4.6.1-6.9.2 2.2-2.9 4.5-5.7 6.9-8.3zm22.2 21l-3.6-6c3.8.5 7.4 1.1 10.8 1.9-1.1 3.3-2.3 6.8-3.8 10.3-1.1-2.1-2.2-4.2-3.4-6.2zm-54.5-16.2c-1.7-10.5-.3-17.9 3.8-20.3 1-.6 2.2-.9 3.5-.9 6 0 13.5 4.9 21 12.3-3.5 3.8-7 8.2-10.4 13-5.8.5-11.3 1.4-16.5 2.5-.6-2.3-1-4.5-1.4-6.6zm-24.7 29c0-4.7 5.7-9.7 15.7-13.4 2-.8 4.2-1.5 6.4-2.1 1.6 5 3.6 10.3 6 15.6-2.4 5.3-4.5 10.5-6 15.5-13.8-4-22.1-10-22.1-15.6zm28.5 49.3c-4.1-2.4-5.5-9.8-3.8-20.3.3-2.1.8-4.3 1.4-6.6 5.2 1.2 10.7 2 16.5 2.5 3.4 4.8 6.9 9.1 10.4 13-7.4 7.3-14.9 12.3-21 12.3-1.3 0-2.5-.3-3.5-.9zm60.8-20.3c1.7 10.5.3 17.9-3.8 20.3-1 .6-2.2.9-3.5.9-6 0-13.5-4.9-21-12.3 3.5-3.8 7-8.2 10.4-13 5.8-.5 11.3-1.4 16.5-2.5.6 2.3 1 4.5 1.4 6.6zm9-15.6c-2 .8-4.2 1.5-6.4 2.1-1.6-5-3.6-10.3-6-15.6 2.4-5.3 4.5-10.5 6-15.5 13.8 4 22.1 10 22.1 15.6 0 4.7-5.8 9.7-15.7 13.4z"
|
||||
></path></g
|
||||
>
|
||||
</svg>
|
||||
{:else if application.configuration.build.pack === "gatsby"}
|
||||
<svg class="w-10 h-10 absolute top-0 left-0 -m-4" viewBox="0 0 128 128">
|
||||
<path fill="#64328B" d="M64,0C28.7,0,0,28.7,0,64v0c0,35.3,28.7,64,64,64s64-28.7,64-64v0C128,28.7,99.3,0,64,0z M13.2,64L64,114.8 C35.9,114.8,13.2,92.1,13.2,64z M75.4,113.5l-60.9-61C19.7,30,39.9,13.2,64,13.2c16.6,0,31.3,7.9,40.5,20.2l-7.5,7.2 C89.7,30.2,77.7,23.5,64,23.5c-17.6,0-32.5,11.2-38.1,26.8C33.1,57,75.4,98.8,78.1,102c12.7-4.7,22.3-15.5,25.4-28.9H81.9v-9.4 l33,0.2C114.8,88.2,98,108.4,75.4,113.5z"></path>
|
||||
</svg>
|
||||
{:else if application.configuration.build.pack === "nuxtjs"}
|
||||
<svg class="w-10 h-10 absolute top-0 left-0 -m-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400">
|
||||
<g fill-rule="nonzero" transform="translate(0 50)" fill="none">
|
||||
<path d="M227.92099 83.45116l-13.6889 24.10141-46.8148-82.44693L23.7037 278.17052h97.3037c0 13.31084 10.61252 24.10142 23.70371 24.10142H23.70371c-8.46771 0-16.29145-4.59601-20.5246-12.05272-4.23315-7.4567-4.23272-16.64312.00114-24.0994L146.89383 13.05492c4.23415-7.45738 12.0596-12.05138 20.5284-12.05138 8.46878 0 16.29423 4.594 20.52839 12.05138l39.97037 70.39623z" fill="#00C58E"/>
|
||||
<path d="M331.6642 266.11981l-90.05432-158.56724-13.6889-24.10141-13.68888 24.10141-90.04445 158.56724c-4.23385 7.45629-4.23428 16.64271-.00113 24.09941 4.23314 7.4567 12.05689 12.05272 20.5246 12.05272h166.4c8.46946 0 16.29644-4.591 20.532-12.04837 4.23555-7.45736 4.23606-16.64592.00132-24.10376h.01976zM144.7111 278.17052L227.921 131.65399l83.19012 146.51653h-166.4z" fill="#FFF"/>
|
||||
<path d="M396.04938 290.22123c-4.23344 7.45557-12.05656 12.0507-20.52345 12.0507H311.1111c13.0912 0 23.7037-10.79057 23.7037-24.10141h40.66173L260.09877 74.98553l-18.4889 32.56704L227.921 83.45116l11.65432-20.51634c4.23416-7.45738 12.0596-12.05138 20.5284-12.05138 8.46879 0 16.29423 4.594 20.52839 12.05138l115.41728 203.185c4.23426 7.457 4.23426 16.6444 0 24.1014z" fill="#108775"/>
|
||||
</g>
|
||||
</svg>
|
||||
{:else if application.configuration.build.pack === "svelte"}
|
||||
<svg
|
||||
class="w-10 h-10 absolute top-0 left-0 -m-4"
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 98.1 118"
|
||||
style="enable-background:new 0 0 98.1 118;"
|
||||
xml:space="preserve"
|
||||
>
|
||||
<path
|
||||
fill="#FF3E00"
|
||||
d="M91.8,15.6C80.9-0.1,59.2-4.7,43.6,5.2L16.1,22.8C8.6,27.5,3.4,35.2,1.9,43.9c-1.3,7.3-0.2,14.8,3.3,21.3 c-2.4,3.6-4,7.6-4.7,11.8c-1.6,8.9,0.5,18.1,5.7,25.4c11,15.7,32.6,20.3,48.2,10.4l27.5-17.5c7.5-4.7,12.7-12.4,14.2-21.1 c1.3-7.3,0.2-14.8-3.3-21.3c2.4-3.6,4-7.6,4.7-11.8C99.2,32.1,97.1,22.9,91.8,15.6"
|
||||
></path>
|
||||
<path
|
||||
fill="#FFFFFF"
|
||||
d="M40.9,103.9c-8.9,2.3-18.2-1.2-23.4-8.7c-3.2-4.4-4.4-9.9-3.5-15.3c0.2-0.9,0.4-1.7,0.6-2.6l0.5-1.6l1.4,1 c3.3,2.4,6.9,4.2,10.8,5.4l1,0.3l-0.1,1c-0.1,1.4,0.3,2.9,1.1,4.1c1.6,2.3,4.4,3.4,7.1,2.7c0.6-0.2,1.2-0.4,1.7-0.7L65.5,72 c1.4-0.9,2.3-2.2,2.6-3.8c0.3-1.6-0.1-3.3-1-4.6c-1.6-2.3-4.4-3.3-7.1-2.6c-0.6,0.2-1.2,0.4-1.7,0.7l-10.5,6.7 c-1.7,1.1-3.6,1.9-5.6,2.4c-8.9,2.3-18.2-1.2-23.4-8.7c-3.1-4.4-4.4-9.9-3.4-15.3c0.9-5.2,4.1-9.9,8.6-12.7l27.5-17.5 c1.7-1.1,3.6-1.9,5.6-2.5c8.9-2.3,18.2,1.2,23.4,8.7c3.2,4.4,4.4,9.9,3.5,15.3c-0.2,0.9-0.4,1.7-0.7,2.6l-0.5,1.6l-1.4-1 c-3.3-2.4-6.9-4.2-10.8-5.4l-1-0.3l0.1-1c0.1-1.4-0.3-2.9-1.1-4.1c-1.6-2.3-4.4-3.3-7.1-2.6c-0.6,0.2-1.2,0.4-1.7,0.7L32.4,46.1 c-1.4,0.9-2.3,2.2-2.6,3.8s0.1,3.3,1,4.6c1.6,2.3,4.4,3.3,7.1,2.6c0.6-0.2,1.2-0.4,1.7-0.7l10.5-6.7c1.7-1.1,3.6-1.9,5.6-2.5 c8.9-2.3,18.2,1.2,23.4,8.7c3.2,4.4,4.4,9.9,3.5,15.3c-0.9,5.2-4.1,9.9-8.6,12.7l-27.5,17.5C44.8,102.5,42.9,103.3,40.9,103.9"
|
||||
></path>
|
||||
</svg>
|
||||
{:else if application.configuration.build.pack === "vuejs"}
|
||||
<svg
|
||||
class="text-green-500 w-10 h-10 absolute top-0 left-0 -m-4"
|
||||
viewBox="0 0 128 128"
|
||||
>
|
||||
<path
|
||||
d="m-2.3125e-8 8.9337 49.854 0.1586 14.167 24.47 14.432-24.47 49.547-0.1577-63.834 110.14zm126.98 0.6374-24.36 0.0207-38.476 66.052-38.453-66.052-24.749-0.0194 63.211 107.89zm-25.149-0.008-22.745 0.16758l-15.053 24.647-14.817-24.647-22.794-0.1679 37.731 64.476zM25.997 9.3929l23.002 0.0087M25.997 9.3929l23.002 0.0087"
|
||||
fill="none"></path><path
|
||||
d="m25.997 9.3929 23.002 0.0087l15.036 24.958 14.983-24.956 22.982-0.0057-37.85 65.655z"
|
||||
fill="#35495e"></path><path
|
||||
d="m0.91068 9.5686 25.066-0.1711 38.151 65.658 37.852-65.654 25.11 0.0263-62.966 108.06z"
|
||||
fill="#41b883"></path>
|
||||
</svg>
|
||||
{:else if application.configuration.build.pack === "nextjs"}
|
||||
<svg
|
||||
class="text-blue-500 w-10 h-10 absolute top-0 left-0 -m-4 fill-current"
|
||||
viewBox="0 0 128 128"
|
||||
>
|
||||
<path
|
||||
d="M64 0C28.7 0 0 28.7 0 64s28.7 64 64 64c11.2 0 21.7-2.9 30.8-7.9L48.4 55.3v36.6h-6.8V41.8h6.8l50.5 75.8C116.4 106.2 128 86.5 128 64c0-35.3-28.7-64-64-64zm22.1 84.6l-7.5-11.3V41.8h7.5v42.8z"
|
||||
></path>
|
||||
</svg>
|
||||
{:else if application.configuration.build.pack === "nodejs"}
|
||||
<svg
|
||||
class="text-green-400 w-10 h-10 absolute top-0 left-0 -m-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
data-prefix="fab"
|
||||
data-icon="node-js"
|
||||
role="img"
|
||||
viewBox="0 0 448 512"
|
||||
><path
|
||||
fill="currentColor"
|
||||
d="M224 508c-6.7 0-13.5-1.8-19.4-5.2l-61.7-36.5c-9.2-5.2-4.7-7-1.7-8 12.3-4.3 14.8-5.2 27.9-12.7 1.4-.8 3.2-.5 4.6.4l47.4 28.1c1.7 1 4.1 1 5.7 0l184.7-106.6c1.7-1 2.8-3 2.8-5V149.3c0-2.1-1.1-4-2.9-5.1L226.8 37.7c-1.7-1-4-1-5.7 0L36.6 144.3c-1.8 1-2.9 3-2.9 5.1v213.1c0 2 1.1 4 2.9 4.9l50.6 29.2c27.5 13.7 44.3-2.4 44.3-18.7V167.5c0-3 2.4-5.3 5.4-5.3h23.4c2.9 0 5.4 2.3 5.4 5.3V378c0 36.6-20 57.6-54.7 57.6-10.7 0-19.1 0-42.5-11.6l-48.4-27.9C8.1 389.2.7 376.3.7 362.4V149.3c0-13.8 7.4-26.8 19.4-33.7L204.6 9c11.7-6.6 27.2-6.6 38.8 0l184.7 106.7c12 6.9 19.4 19.8 19.4 33.7v213.1c0 13.8-7.4 26.7-19.4 33.7L243.4 502.8c-5.9 3.4-12.6 5.2-19.4 5.2zm149.1-210.1c0-39.9-27-50.5-83.7-58-57.4-7.6-63.2-11.5-63.2-24.9 0-11.1 4.9-25.9 47.4-25.9 37.9 0 51.9 8.2 57.7 33.8.5 2.4 2.7 4.2 5.2 4.2h24c1.5 0 2.9-.6 3.9-1.7s1.5-2.6 1.4-4.1c-3.7-44.1-33-64.6-92.2-64.6-52.7 0-84.1 22.2-84.1 59.5 0 40.4 31.3 51.6 81.8 56.6 60.5 5.9 65.2 14.8 65.2 26.7 0 20.6-16.6 29.4-55.5 29.4-48.9 0-59.6-12.3-63.2-36.6-.4-2.6-2.6-4.5-5.3-4.5h-23.9c-3 0-5.3 2.4-5.3 5.3 0 31.1 16.9 68.2 97.8 68.2 58.4-.1 92-23.2 92-63.4z"
|
||||
></path>
|
||||
</svg>
|
||||
{:else if application.configuration.build.pack === "php"}
|
||||
<svg
|
||||
viewBox="0 0 128 128"
|
||||
class="text-white w-14 h-14 absolute top-0 left-0 -m-6"
|
||||
>
|
||||
<path
|
||||
fill="#6181B6"
|
||||
d="M64 33.039c-33.74 0-61.094 13.862-61.094 30.961s27.354 30.961 61.094 30.961 61.094-13.862 61.094-30.961-27.354-30.961-61.094-30.961zm-15.897 36.993c-1.458 1.364-3.077 1.927-4.86 2.507-1.783.581-4.052.461-6.811.461h-6.253l-1.733 10h-7.301l6.515-34h14.04c4.224 0 7.305 1.215 9.242 3.432 1.937 2.217 2.519 5.364 1.747 9.337-.319 1.637-.856 3.159-1.614 4.515-.759 1.357-1.75 2.624-2.972 3.748zm21.311 2.968l2.881-14.42c.328-1.688.208-2.942-.361-3.555-.57-.614-1.782-1.025-3.635-1.025h-5.79l-3.731 19h-7.244l6.515-33h7.244l-1.732 9h6.453c4.061 0 6.861.815 8.402 2.231s2.003 3.356 1.387 6.528l-3.031 15.241h-7.358zm40.259-11.178c-.318 1.637-.856 3.133-1.613 4.488-.758 1.357-1.748 2.598-2.971 3.722-1.458 1.364-3.078 1.927-4.86 2.507-1.782.581-4.053.461-6.812.461h-6.253l-1.732 10h-7.301l6.514-34h14.041c4.224 0 7.305 1.215 9.241 3.432 1.935 2.217 2.518 5.418 1.746 9.39zM95.919 54h-5.001l-2.727 14h4.442c2.942 0 5.136-.29 6.576-1.4 1.442-1.108 2.413-2.828 2.918-5.421.484-2.491.264-4.434-.66-5.458-.925-1.024-2.774-1.721-5.548-1.721zM38.934 54h-5.002l-2.727 14h4.441c2.943 0 5.136-.29 6.577-1.4 1.441-1.108 2.413-2.828 2.917-5.421.484-2.491.264-4.434-.66-5.458s-2.772-1.721-5.546-1.721z"
|
||||
></path>
|
||||
</svg>
|
||||
{:else if application.configuration.build.pack === "docker"}
|
||||
<svg
|
||||
viewBox="0 0 128 128"
|
||||
class="w-16 h-16 absolute top-0 left-0 -m-8"
|
||||
>
|
||||
<g
|
||||
><path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fill="#3A4D54"
|
||||
d="M73.8 50.8h11.3v11.5h5.7c2.6 0 5.3-.5 7.8-1.3 1.2-.4 2.6-1 3.8-1.7-1.6-2.1-2.4-4.7-2.6-7.3-.3-3.5.4-8.1 2.8-10.8l1.2-1.4 1.4 1.1c3.6 2.9 6.5 6.8 7.1 11.4 4.3-1.3 9.3-1 13.1 1.2l1.5.9-.8 1.6c-3.2 6.2-9.9 8.2-16.4 7.8-9.8 24.3-31 35.8-56.8 35.8-13.3 0-25.5-5-32.5-16.8l-.1-.2-1-2.1c-2.4-5.2-3.1-10.9-2.6-16.6l.2-1.7h9.6v-11.4h11.3v-11.2h22.5v-11.3h13.5v22.5z"
|
||||
></path><path
|
||||
fill="#00AADA"
|
||||
d="M110.4 55.1c.8-5.9-3.6-10.5-6.4-12.7-3.1 3.6-3.6 13.2 1.3 17.2-2.8 2.4-8.5 4.7-14.5 4.7h-72.2c-.6 6.2.5 11.9 3 16.8l.8 1.5c.5.9 1.1 1.7 1.7 2.6 3 .2 5.7.3 8.2.2 4.9-.1 8.9-.7 12-1.7.5-.2.9.1 1.1.5.2.5-.1.9-.5 1.1-.4.1-.8.3-1.3.4-2.4.7-5 1.1-8.3 1.3h-.6000000000000001c-1.3.1-2.7.1-4.2.1-1.6 0-3.1 0-4.9-.1 6 6.8 15.4 10.8 27.2 10.8 25 0 46.2-11.1 55.5-35.9 6.7.7 13.1-1 16-6.7-4.5-2.7-10.5-1.8-13.9-.1z"
|
||||
></path><path
|
||||
fill="#28B8EB"
|
||||
d="M110.4 55.1c.8-5.9-3.6-10.5-6.4-12.7-3.1 3.6-3.6 13.2 1.3 17.2-2.8 2.4-8.5 4.7-14.5 4.7h-68c-.3 9.5 3.2 16.7 9.5 21 4.9-.1 8.9-.7 12-1.7.5-.2.9.1 1.1.5.2.5-.1.9-.5 1.1-.4.1-.8.3-1.3.4-2.4.7-5.2 1.2-8.5 1.4l-.1-.1c8.5 4.4 20.8 4.3 35-1.1 15.8-6.1 30.6-17.7 40.9-30.9-.2.1-.4.1-.5.2z"
|
||||
></path><path
|
||||
fill="#028BB8"
|
||||
d="M18.7 71.8c.4 3.3 1.4 6.4 2.9 9.3l.8 1.5c.5.9 1.1 1.7 1.7 2.6 3 .2 5.7.3 8.2.2 4.9-.1 8.9-.7 12-1.7.5-.2.9.1 1.1.5.2.5-.1.9-.5 1.1-.4.1-.8.3-1.3.4-2.4.7-5.2 1.2-8.5 1.4h-.4c-1.3.1-2.7.1-4.1.1-1.6 0-3.2 0-4.9-.1 6 6.8 15.5 10.8 27.3 10.8 21.4 0 40-8.1 50.8-26h-85.1v-.1z"
|
||||
></path><path
|
||||
fill="#019BC6"
|
||||
d="M23.5 71.8c1.3 5.8 4.3 10.4 8.8 13.5 4.9-.1 8.9-.7 12-1.7.5-.2.9.1 1.1.5.2.5-.1.9-.5 1.1-.4.1-.8.3-1.3.4-2.4.7-5.2 1.2-8.6 1.4 8.5 4.4 20.8 4.3 34.9-1.1 8.5-3.3 16.8-8.2 24.2-14.1h-70.6z"
|
||||
></path><path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fill="#00ACD3"
|
||||
d="M28.4 52.7h9.8v9.8h-9.8v-9.8zm.8.8h.8v8.1h-.8v-8.1zm1.4 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zM39.6 41.5h9.8v9.8h-9.8v-9.8zm.9.8h.8v8.1h-.8v-8.1zm1.4 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.4 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1z"
|
||||
></path><path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fill="#23C2EE"
|
||||
d="M39.6 52.7h9.8v9.8h-9.8v-9.8zm.9.8h.8v8.1h-.8v-8.1zm1.4 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.4 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1z"
|
||||
></path><path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fill="#00ACD3"
|
||||
d="M50.9 52.7h9.8v9.8h-9.8v-9.8zm.8.8h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.4 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1z"
|
||||
></path><path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fill="#23C2EE"
|
||||
d="M50.9 41.5h9.8v9.8h-9.8v-9.8zm.8.8h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.4 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zM62.2 52.7h9.8v9.8h-9.8v-9.8zm.8.8h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.4 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1z"
|
||||
></path><path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fill="#00ACD3"
|
||||
d="M62.2 41.5h9.8v9.8h-9.8v-9.8zm.8.8h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.4 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1z"
|
||||
></path><path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fill="#23C2EE"
|
||||
d="M62.2 30.2h9.8v9.8h-9.8v-9.8zm.8.8h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.4 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1z"
|
||||
></path><path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fill="#00ACD3"
|
||||
d="M73.5 52.7h9.8v9.8h-9.8v-9.8zm.8.8h.8v8.1h-.8v-8.1zm1.4 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1zm1.5 0h.8v8.1h-.8v-8.1z"
|
||||
></path><path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fill="#D4EEF1"
|
||||
d="M48.8 78.3c1.5 0 2.7 1.2 2.7 2.7 0 1.5-1.2 2.7-2.7 2.7-1.5 0-2.7-1.2-2.7-2.7 0-1.5 1.2-2.7 2.7-2.7"
|
||||
></path><path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fill="#3A4D54"
|
||||
d="M48.8 79.1c.2 0 .5 0 .7.1-.2.1-.4.4-.4.7 0 .4.4.8.8.8.3 0 .6-.2.7-.4.1.2.1.5.1.7 0 1.1-.9 1.9-1.9 1.9-1.1 0-1.9-.9-1.9-1.9 0-1 .8-1.9 1.9-1.9M1.1 72.8h125.4c-2.7-.7-8.6-1.6-7.7-5.2-5 5.7-16.9 4-20 1.2-3.4 4.9-23 3-24.3-.8-4.2 5-17.3 5-21.5 0-1.4 3.8-21 5.7-24.3.8-3 2.8-15 4.5-20-1.2 1.1 3.5-4.9 4.5-7.6 5.2"
|
||||
></path><path
|
||||
fill="#BFDBE0"
|
||||
d="M56 97.8c-6.7-3.2-10.3-7.5-12.4-12.2-2.5.7-5.5 1.2-8.9 1.4-1.3.1-2.7.1-4.1.1-1.7 0-3.4 0-5.2-.1 6 6 13.6 10.7 27.5 10.8h3.1z"
|
||||
></path><path
|
||||
fill="#D4EEF1"
|
||||
d="M46.1 89.9c-.9-1.3-1.8-2.8-2.5-4.3-2.5.7-5.5 1.2-8.9 1.4 2.3 1.2 5.7 2.4 11.4 2.9z"
|
||||
></path></g
|
||||
>
|
||||
</svg>
|
||||
{:else if application.configuration.build.pack === "rust"}
|
||||
<svg
|
||||
class="w-14 h-14 absolute top-0 left-0 -m-6"
|
||||
viewBox="0 0 128 128"
|
||||
>
|
||||
<path
|
||||
d="M62.271 10.880 C 62.082 10.990,61.289 12.128,60.508 13.409 C 58.548 16.626,58.526 16.628,55.893 13.857 C 54.180 12.055,53.766 11.725,53.214 11.729 C 52.855 11.731,52.402 11.853,52.206 12.000 C 52.011 12.147,51.458 13.317,50.978 14.600 C 49.879 17.539,49.826 17.634,49.217 17.751 C 48.842 17.822,48.120 17.420,46.389 16.177 C 45.111 15.258,43.857 14.507,43.603 14.507 C 42.549 14.507,42.252 15.083,41.750 18.100 C 41.112 21.936,41.134 21.923,37.676 20.352 C 36.280 19.719,34.956 19.200,34.733 19.200 C 34.510 19.200,34.087 19.440,33.794 19.733 C 33.262 20.266,33.261 20.268,33.406 23.201 L 33.552 26.137 32.997 26.434 C 32.505 26.697,32.166 26.665,29.988 26.150 C 27.145 25.479,26.545 25.497,25.969 26.272 L 25.548 26.838 26.113 29.259 C 26.423 30.590,26.722 31.872,26.778 32.107 C 26.833 32.341,26.738 32.716,26.566 32.939 C 26.282 33.306,25.980 33.339,23.349 33.299 C 18.896 33.229,18.643 33.611,20.483 37.627 C 21.068 38.902,21.547 40.060,21.547 40.199 C 21.547 40.933,20.962 41.200,18.449 41.610 C 17.043 41.839,15.821 42.027,15.733 42.027 C 15.645 42.027,15.381 42.219,15.147 42.453 C 14.382 43.218,14.599 43.936,16.334 46.385 C 18.495 49.435,18.491 49.446,14.921 50.812 C 10.861 52.365,10.779 52.748,13.870 55.680 C 16.749 58.410,16.752 58.370,13.493 60.419 C 11.024 61.970,10.986 62.007,10.923 62.848 C 10.847 63.871,10.865 63.889,13.813 65.690 C 16.728 67.470,16.728 67.524,13.867 70.231 C 10.790 73.141,10.885 73.566,14.948 75.099 C 18.498 76.438,18.503 76.454,16.338 79.504 C 15.111 81.233,14.720 81.953,14.720 82.487 C 14.720 83.486,15.240 83.741,18.347 84.263 C 20.964 84.704,21.547 84.963,21.547 85.685 C 21.547 85.833,21.067 87.001,20.480 88.279 C 18.654 92.256,18.862 92.587,23.184 92.587 C 27.209 92.587,27.102 92.464,26.235 96.094 C 25.581 98.830,25.571 99.354,26.163 99.945 C 26.616 100.399,27.470 100.348,30.141 99.709 C 32.181 99.222,32.539 99.188,33.012 99.441 L 33.552 99.730 33.406 102.665 C 33.261 105.599,33.262 105.601,33.794 106.134 C 34.087 106.427,34.516 106.667,34.746 106.667 C 34.976 106.667,36.300 106.151,37.689 105.520 C 41.136 103.955,41.114 103.942,41.750 107.766 C 42.254 110.797,42.548 111.360,43.624 111.360 C 43.891 111.360,45.118 110.640,46.352 109.760 C 48.519 108.214,49.081 107.972,49.658 108.339 C 49.807 108.433,50.385 109.703,50.942 111.161 C 51.761 113.305,52.061 113.863,52.517 114.081 C 53.385 114.497,53.922 114.163,55.962 111.941 C 58.425 109.258,58.526 109.271,60.537 112.530 C 62.758 116.128,63.333 116.120,65.610 112.457 C 67.572 109.301,67.549 109.303,70.201 112.073 C 71.962 113.911,72.337 114.204,72.931 114.204 C 73.310 114.204,73.763 114.062,73.936 113.888 C 74.110 113.714,74.686 112.429,75.216 111.033 C 75.746 109.636,76.295 108.420,76.437 108.330 C 76.998 107.973,77.579 108.224,79.743 109.760 C 81.017 110.665,82.216 111.360,82.501 111.360 C 83.559 111.360,83.941 110.609,84.381 107.657 C 84.757 105.140,84.833 104.899,85.328 104.648 C 85.815 104.401,86.107 104.484,88.391 105.521 C 89.780 106.151,91.104 106.667,91.334 106.667 C 91.564 106.667,92.000 106.420,92.301 106.118 L 92.850 105.570 92.699 102.755 C 92.555 100.067,92.568 99.923,92.997 99.535 C 93.438 99.136,93.483 99.138,95.949 99.701 C 98.935 100.383,99.492 100.401,100.053 99.840 C 100.601 99.292,100.595 99.172,99.845 96.009 C 99.004 92.461,98.891 92.587,102.933 92.587 C 105.688 92.587,105.995 92.548,106.346 92.161 C 106.932 91.513,106.793 90.771,105.614 88.258 C 105.019 86.992,104.536 85.840,104.540 85.698 C 104.560 84.951,105.147 84.696,107.860 84.255 C 109.520 83.986,110.762 83.674,110.987 83.471 C 111.741 82.790,111.464 81.904,109.743 79.491 C 107.586 76.467,107.595 76.438,111.049 75.165 C 115.185 73.641,115.303 73.133,112.208 70.192 C 109.341 67.468,109.340 67.483,112.480 65.555 C 116.276 63.225,116.282 62.700,112.547 60.382 C 109.335 58.389,109.337 58.417,112.216 55.683 C 115.304 52.749,115.220 52.365,111.159 50.812 C 107.575 49.441,107.564 49.407,109.742 46.418 C 111.039 44.638,111.360 44.047,111.360 43.437 C 111.360 42.371,110.882 42.132,107.738 41.624 C 105.111 41.200,104.533 40.942,104.533 40.195 C 104.533 40.053,105.013 38.910,105.600 37.653 C 106.749 35.192,106.910 34.207,106.260 33.618 C 105.911 33.302,105.443 33.257,102.939 33.298 C 100.319 33.342,99.984 33.305,99.621 32.940 C 99.224 32.541,99.228 32.485,99.848 29.898 C 100.608 26.728,100.611 26.651,99.986 26.064 C 99.352 25.468,98.956 25.478,96.045 26.163 C 93.924 26.663,93.573 26.696,93.091 26.438 L 92.544 26.145 92.695 23.219 L 92.847 20.294 92.300 19.747 C 91.999 19.446,91.572 19.200,91.350 19.200 C 91.129 19.200,89.812 19.723,88.424 20.361 C 86.106 21.428,85.857 21.499,85.356 21.237 C 84.856 20.975,84.773 20.717,84.346 18.110 C 83.853 15.094,83.548 14.507,82.477 14.507 C 82.223 14.507,80.964 15.262,79.679 16.185 C 77.569 17.701,77.286 17.844,76.760 17.661 C 76.325 17.509,76.072 17.178,75.763 16.355 C 75.534 15.749,75.096 14.581,74.788 13.760 C 74.166 12.104,73.819 11.733,72.887 11.733 C 72.367 11.733,71.896 12.107,70.208 13.860 C 67.555 16.616Line truncated
|
||||
stroke="none"
|
||||
fill="rgb(239, 68, 68)"
|
||||
fill-rule="evenodd"></path>
|
||||
</svg>
|
||||
{/if}
|
||||
<div class="flex flex-col justify-center items-center w-full">
|
||||
<div
|
||||
class="text-base font-bold text-center w-full text-white pb-6"
|
||||
>
|
||||
{application.configuration.publish
|
||||
.domain}{application.configuration.publish
|
||||
.path !== "/"
|
||||
? application.configuration.publish.path
|
||||
: ""}
|
||||
</div>
|
||||
<div
|
||||
class="text-xs font-bold text-center w-full text-warmGray-300 group-hover:text-white"
|
||||
>
|
||||
Last deployment<br />
|
||||
{new Intl.DateTimeFormat("default", $dateOptions).format(
|
||||
new Date(application.UpdatedAt),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-2xl font-bold text-center">No applications found</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -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>
|
||||
@@ -1,4 +0,0 @@
|
||||
<script>
|
||||
import { redirect } from "@roxi/routify";
|
||||
$redirect("/dashboard/applications");
|
||||
</script>
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
@@ -1,4 +0,0 @@
|
||||
<script>
|
||||
import { redirect } from "@roxi/routify";
|
||||
$redirect("/dashboard/databases");
|
||||
</script>
|
||||
@@ -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>
|
||||
@@ -1,5 +0,0 @@
|
||||
<script>
|
||||
import { redirect } from "@roxi/routify";
|
||||
$redirect("/dashboard/databases");
|
||||
</script>
|
||||
|
||||
@@ -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 />
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
@@ -1,5 +0,0 @@
|
||||
<script>
|
||||
import { redirect } from "@roxi/routify";
|
||||
$redirect("/dashboard/services");
|
||||
</script>
|
||||
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
@@ -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()
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Loaded 100 of 229 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user