initial production release 🎉

This commit is contained in:
Andras
2021-03-24 22:11:14 +01:00
commit dbe82b3e7c
101 changed files with 12479 additions and 0 deletions

63
src/App.svelte Normal file
View File

@@ -0,0 +1,63 @@
<script>
import { SvelteToast } from "@zerodevx/svelte-toast";
import { Router } from "@roxi/routify";
import { routes } from "../.routify/routes";
const options = {
duration: 2000,
dismissable: false
};
</script>
<style lang="postcss">
: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 !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;
}
</style>
<SvelteToast options="{options}" />
<Router routes="{routes}" />

View File

@@ -0,0 +1,22 @@
<script>
import { application} from "@store";
</script>
<div class="grid grid-cols-1 space-y-2 max-w-2xl md:mx-auto mx-6 text-center">
<label for="buildCommand">Build Command</label>
<input
id="buildCommand"
bind:value="{$application.build.command.build}"
placeholder="eg: yarn build"
/>
<label for="installCommand">Install Command</label>
<input
id="installCommand"
bind:value="{$application.build.command.installation}"
placeholder="eg: yarn install"
/>
<label for="baseDir">Base Directory</label>
<input id="baseDir" bind:value="{$application.build.directory}" placeholder="/" />
</div>

View File

@@ -0,0 +1,106 @@
<script>
import { application } from "@store";
</script>
<div>
<div
class="grid grid-cols-1 text-sm space-y-2 max-w-2xl md:mx-auto mx-6 pb-6 auto-cols-max"
>
<label for="buildPack">Build Pack</label>
<select id="buildPack" bind:value="{$application.build.pack}">
<option selected class="font-medium">static</option>
<option class="font-medium">nodejs</option>
</select>
</div>
<div
class="grid grid-cols-2 space-y-2 max-w-2xl md:mx-auto mx-6 justify-center items-center"
>
<label for="Domain">Domain</label>
<input
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)"
/>
<label for="Path">Path Prefix</label>
<input
id="Path"
bind:value="{$application.publish.path}"
placeholder="/"
/>
<label for="publishDir">Publish Directory</label>
<input
id="publishDir"
bind:value="{$application.publish.directory}"
placeholder="/"
/>
{#if $application.build.pack !== "static"}
<label for="Port">Port</label>
<input
id="Port"
bind:value="{$application.publish.port}"
placeholder="{$application.build.pack === 'static'
? '80'
: '3000'}"
/>
{/if}
<!-- {#if config.buildPack === "static"}
<div class="text-base font-bold text-white pt-2">
Preview Deploys
</div>
<button
type="button"
on:click="{() =>
(config.previewDeploy = !config.previewDeploy)}"
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 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black"
class:bg-green-600="{config.previewDeploy}"
class:bg-coolgray-300="{!config.previewDeploy}"
>
<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 ring-0 transition ease-in-out duration-200"
class:translate-x-5="{config.previewDeploy}"
class:translate-x-0="{!config.previewDeploy}"
>
<span
class="ease-in duration-200 absolute inset-0 h-full w-full flex items-center justify-center transition-opacity"
class:opacity-0="{config.previewDeploy}"
class:opacity-100="{!config.previewDeploy}"
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="{config.previewDeploy}"
class:opacity-0="{!config.previewDeploy}"
>
<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>
{/if} -->
</div>
</div>

View File

@@ -0,0 +1,72 @@
<script>
import { application } from "@store";
let secret = {
name: null,
value: null,
};
let foundSecret = null;
async function saveSecret() {
if (secret.name && secret.value) {
const found = $application.publish.secrets.find(
s => s.name === secret.name,
);
if (!found) {
$application.publish.secrets = [
...$application.publish.secrets,
{
name: secret.name,
value: secret.value,
},
];
secret = {
name: null,
value: null
}
} else {
foundSecret = found;
}
}
}
async function removeSecret(name) {
$application.publish.secrets = [...$application.publish.secrets.filter(s => s.name !== name)]
}
</script>
<div class="space-y-2 max-w-2xl md:mx-auto mx-6 text-center">
<div class="text-left text-base font-bold tracking-tight text-warmGray-400">New Secret</div>
<div class="grid md:grid-flow-col grid-flow-row gap-2">
<input id="secretName" bind:value="{secret.name}" placeholder="Name" />
<input id="secretValue" bind:value="{secret.value}" placeholder="Value" />
<button
class="button p-1 w-20 bg-green-600 hover:bg-green-500 text-white"
on:click="{saveSecret}">Save</button
>
</div>
{#if $application.publish.secrets.length > 0}
{#each $application.publish.secrets as s}
<div class="grid md:grid-flow-col grid-flow-row gap-2">
<input
id="{s.name}"
value="{s.name}"
disabled
class="bg-transparent border-transparent"
class:border-red-600="{foundSecret && foundSecret.name === s.name}"
/>
<input
id="{s.createdAt}"
value="ENCRYPTED"
disabled
class="bg-transparent border-transparent"
/>
<button
class="button w-20 bg-red-600 hover:bg-red-500 text-white"
on:click="{() => removeSecret(s.name)}">Delete</button
>
</div>
{/each}
{/if}
</div>

View File

@@ -0,0 +1,24 @@
<script>
export let loading, branches;
import { application } from "@store";
</script>
{#if loading}
<div class="grid grid-cols-1">
<label for="branch">Branch</label>
<select disabled>
<option selected>Loading branches</option>
</select>
</div>
{:else}
<div class="grid grid-cols-1">
<label for="branch">Branch</label>
<!-- svelte-ignore a11y-no-onchange -->
<select id="branch" bind:value="{$application.repository.branch}">
<option disabled selected>Select a branch</option>
{#each branches as branch}
<option value="{branch.name}" class="font-medium">{branch.name}</option>
{/each}
</select>
</div>
{/if}

View File

@@ -0,0 +1,133 @@
<script>
import { redirect, isActive } from "@roxi/routify";
import { fade } from "svelte/transition";
import { session, application, fetch, initialApplication } 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,
};
let branches = [];
let repositories = [];
async function loadBranches() {
loading.branches = true;
const selectedRepository = repositories.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 loadGithub() {
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;
const data = await $fetch(
`https://api.github.com/user/installations/${$application.github.installation.id}/repositories?per_page=10000`,
);
repositories = data.repositories;
const foundRepositoryOnGithub = data.repositories.find(
r =>
r.full_name ===
`${$application.repository.organization}/${$application.repository.name}`,
);
if (foundRepositoryOnGithub) {
$application.repository.id = foundRepositoryOnGithub.id;
await loadBranches();
}
} catch (error) {
return false;
}
}
function modifyGithubAppConfig() {
const left = screen.width / 2 - 1020 / 2;
const top = screen.height / 2 - 618 / 2;
const newWindow = open(
`https://github.com/apps/${
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);
if (!$isActive("/application/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 = [];
repositories = [];
await loadGithub();
}
}, 100);
}
</script>
<div in:fade="{{ duration: 100 }}">
{#if !$session.githubAppToken}
<Login />
{:else}
{#await loadGithub()}
<Loading />
{:then}
<div
class="text-center space-y-2 max-w-4xl mx-auto px-6"
in:fade="{{ duration: 100 }}"
>
<Repositories
bind:repositories
on:loadBranches="{loadBranches}"
on:modifyGithubAppConfig="{modifyGithubAppConfig}"
/>
{#if $application.repository.organization !== "new"}
<Branches loading="{loading.branches}" branches="{branches}" />
{/if}
{#if $application.repository.branch}
<Tabs />
{/if}
</div>
{/await}
{/if}
</div>

View File

@@ -0,0 +1,50 @@
<script>
import { session } from "@store";
function login() {
const left = screen.width / 2 - 1020 / 2;
const top = screen.height / 2 - 618 / 2;
const newWindow = open(
`https://github.com/login/oauth/authorize?client_id=${
import.meta.env.VITE_GITHUB_APP_CLIENTID
}`,
"Authenticate",
"resizable=1, scrollbars=1, fullscreen=0, height=618, width=1020,top=" +
top +
", left=" +
left +
", toolbar=0, menubar=0, status=0",
);
const timer = setInterval(() => {
if (newWindow.closed) {
clearInterval(timer);
const ghToken = new URL(newWindow.document.URL).searchParams.get(
"ghToken",
);
if (ghToken) {
$session.githubAppToken = ghToken;
}
}
}, 100);
}
</script>
<div class="text-center text-white">
<div class="text-2xl font-bold text-center pb-4">
Choose your Git provider
</div>
<button on:click="{login}" class="hover:scale-110 transform duration-100 transition">
<svg
class="w-16"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><path
d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"
></path></svg
>
</button>
</div>

View File

@@ -0,0 +1,45 @@
<script>
import { createEventDispatcher } from "svelte";
import { isActive } from "@roxi/routify";
import { application } from "@store";
export let repositories;
const dispatch = createEventDispatcher();
const loadBranches = () => dispatch("loadBranches");
const modifyGithubAppConfig = () => dispatch("modifyGithubAppConfig");
</script>
<div class="grid grid-cols-1">
{#if repositories.length !== 0}
<label for="repository">Organization / Repository</label>
<div class="grid grid-cols-3">
<!-- svelte-ignore a11y-no-onchange -->
<select
id="repository"
class:cursor-not-allowed="{!$isActive('/application/new')}"
class="col-span-2"
bind:value="{$application.repository.id}"
on:change="{loadBranches}"
disabled="{!$isActive('/application/new')}"
>
<option selected disabled>Select a repository</option>
{#each repositories as repo}
<option value="{repo.id}" class="font-medium">
{repo.owner.login}
/
{repo.name}
</option>
{/each}
</select>
<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"
on:click="{modifyGithubAppConfig}">Add repositories on Github</button
>
{/if}
</div>

View File

@@ -0,0 +1,97 @@
<script>
import { redirect, isActive } from "@roxi/routify";
import { application, fetch, deployments } from "@store";
import General from "./ActiveTab/General.svelte";
import BuildStep from "./ActiveTab/BuildStep.svelte";
import Secrets from "./ActiveTab/Secrets.svelte";
import { onMount } from "svelte";
onMount(async () => {
if (!$isActive("/application/new")) {
const config = await $fetch(`/api/v1/config`, {
body: {
name: $application.repository.name,
organization: $application.repository.organization,
branch: $application.repository.branch,
},
});
$application = { ...config };
$redirect(`/application/:organization/:name/:branch/configuration`, {
name: $application.repository.name,
organization: $application.repository.organization,
branch: $application.repository.branch,
});
} else {
$deployments.applications.deployed.filter(d => {
const conf = d?.Spec?.Labels.application;
if (
conf.repository.organization ===
$application.repository.organization &&
conf.repository.name === $application.repository.name &&
conf.repository.branch === $application.repository.branch
) {
$redirect(`/application/:organization/:name/:branch/configuration`, {
name: $application.repository.name,
organization: $application.repository.organization,
branch: $application.repository.branch,
});
}
});
}
});
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;
}
}
</script>
<div class="block text-center py-4">
<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:text-green-500"
>
General
</div>
<div
on:click="{() => activateTab('buildStep')}"
class:text-green-500="{activeTab.buildStep}"
class="px-3 py-2 cursor-pointer hover:text-green-500"
>
Build Step
</div>
<div
on:click="{() => activateTab('secrets')}"
class:text-green-500="{activeTab.secrets}"
class="px-3 py-2 cursor-pointer hover:text-green-500"
>
Secrets
</div>
</nav>
</div>
<div class="max-w-4xl mx-auto">
<div class="h-full">
{#if activeTab.general}
<General />
{:else if activeTab.buildStep}
<BuildStep />
{:else if activeTab.secrets}
<Secrets />
{/if}
</div>
</div>

View File

@@ -0,0 +1,90 @@
<script>
import { fetch, dbInprogress } from "@store";
import { isActive, redirect } from "@roxi/routify/runtime";
import { fade } from "svelte/transition";
import { toast } from "@zerodevx/svelte-toast";
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">
<button
class="button bg-gray-500 p-2 text-white hover:bg-green-600 cursor-pointer w-32"
on:click="{() => (type = 'mongodb')}"
class:bg-green-600="{type === 'mongodb'}"
>
MongoDB
</button>
<button
class="button bg-gray-500 p-2 text-white hover:bg-blue-600 cursor-pointer w-32"
on:click="{() => (type = 'postgresql')}"
class:bg-blue-600="{type === 'postgresql'}"
>
PostgreSQL
</button>
<button
class="button bg-gray-500 p-2 text-white hover:bg-orange-600 cursor-pointer w-32"
on:click="{() => (type = 'mysql')}"
class:bg-orange-600="{type === 'mysql'}"
>
MySQL
</button>
<button
class="button bg-gray-500 p-2 text-white hover:bg-red-600 cursor-pointer w-32"
on:click="{() => (type = 'couchdb')}"
class:bg-red-600="{type === 'couchdb'}"
>
Couchdb
</button>
</div>
{#if type}
<div>
<div
class="grid grid-rows-1 justify-center items-center text-center pb-5"
>
<label for="defaultDB">Default database</label>
<input
id="defaultDB"
class="w-64"
placeholder="random"
bind:value="{defaultDatabaseName}"
/>
</div>
<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="button p-2 w-32 text-white"
on:click="{deploy}">Deploy</button
>
</div>
{/if}
{/if}
</div>

View File

@@ -0,0 +1,16 @@
<script>
export let customClass;
</script>
<svg
class={customClass}
id="CouchDB"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 128 128"
><g id="original"
><path
class=""
d="M101.4,77.2c0,5-2.7,7.5-7.6,7.7H33.9c-4.9,0-7.6-2.5-7.6-7.7,0-5,2.7-7.5,7.6-7.7H94.1C99,69.7,101.4,72.2,101.4,77.2ZM94.1,88.7H33.9c-4.9,0-7.6,2.4-7.6,7.7,0,5,2.7,7.4,7.6,7.7H94.1c4.9,0,7.6-2.5,7.6-7.7C101.4,91.1,99,88.7,94.1,88.7Zm18.6-42.1h0c-4.9,0-7.6,2.5-7.6,7.4V96.1c0,5,2.7,7.5,7.6,7.7h0c7.4-.2,11.3-7.7,11.3-22.9V62C124,51.8,120.1,46.8,112.7,46.6Zm-97.4,0h0C7.9,46.8,4,51.8,4,62V80.9c0,15.2,3.9,22.7,11.3,22.9h0c4.9,0,7.6-2.4,7.6-7.7V54.3C22.7,49.3,20.2,46.8,15.3,46.6Zm97.4-3.8c0-12.7-6.6-18.7-18.6-18.9H33.9c-12.2.2-18.6,6.5-18.6,18.9h0c7.4,0,11.3,4,11.3,11.5s3.9,11.4,11.3,11.4H90.4c7.3,0,11.3-3.9,11.3-11.4-.3-7.7,3.9-11.2,11-11.5Z"
></path></g
></svg
>

View File

@@ -0,0 +1,32 @@
<script>
export let customClass;
</script>
<svg
class={customClass}
xmlns="http://www.w3.org/2000/svg"
id="Layer_1"
data-name="Layer 1"
viewBox="0 0 216.56 448.5"
><defs
><style>
.cls-1 {
fill: #10aa50;
}
.cls-2 {
fill: #b8c4c2;
}
.cls-3 {
fill: #12924f;
}
</style></defs
><title>MongoDB_Leaf_FullColor_RGB</title><path
class="cls-1"
d="M202.8,179.68c-23-101.47-71-128.49-83.18-147.59C113,21.7,106.25,5.91,106.25,5.91c-.66,9-1.83,14.7-9.51,21.54C81.36,41.16,16,94.42,10.51,209.72c-5.12,107.5,79,173.8,90.18,180.65,8.54,4.2,19,.08,24-3.77,40.54-27.84,96-102.07,78.06-206.92"
></path><path
class="cls-2"
d="M109.73,333.11c-2.11,26.62-3.63,42.11-9,57.29,0,0,3.54,25.33,6,52.17l8.77,0a488.62,488.62,0,0,1,9.57-56.2C113.71,380.8,110.16,356.46,109.73,333.11Z"
></path><path
class="cls-3"
d="M125.06,386.39h0c-11.48-5.3-14.8-30.13-15.31-53.28A1090.8,1090.8,0,0,0,112.2,218.4c-.6-20.07.3-185.92-4.94-210.2,2.12,4.75,7.24,15.91,12.36,23.88,12.23,19.11,60.19,46.13,83.17,147.61C220.7,284.27,165.57,358.37,125.06,386.39Z"
></path>
</svg>

View File

@@ -0,0 +1,16 @@
<script>
export let customClass;
</script>
<svg
class={customClass}
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
viewBox="0 0 25.6 25.6"
><path
d="M179.076 94.886c-3.568-.1-6.336.268-8.656 1.25-.668.27-1.74.27-1.828 1.116.357.355.4.936.713 1.428.535.893 1.473 2.096 2.32 2.72l2.855 2.053c1.74 1.07 3.703 1.695 5.398 2.766.982.625 1.963 1.428 2.945 2.098.5.357.803.938 1.428 1.16v-.135c-.312-.4-.402-.98-.713-1.428l-1.34-1.293c-1.293-1.74-2.9-3.258-4.64-4.506-1.428-.982-4.55-2.32-5.13-3.97l-.088-.1c.98-.1 2.14-.447 3.078-.715 1.518-.4 2.9-.312 4.46-.713l2.143-.625v-.4c-.803-.803-1.383-1.874-2.23-2.632-2.275-1.963-4.775-3.882-7.363-5.488-1.383-.892-3.168-1.473-4.64-2.23-.537-.268-1.428-.402-1.74-.848-.805-.98-1.25-2.275-1.83-3.436l-3.658-7.763c-.803-1.74-1.295-3.48-2.275-5.086-4.596-7.585-9.594-12.18-17.268-16.687-1.65-.937-3.613-1.34-5.7-1.83l-3.346-.18c-.715-.312-1.428-1.16-2.053-1.562-2.543-1.606-9.102-5.086-10.977-.5-1.205 2.9 1.785 5.755 2.8 7.228.76 1.026 1.74 2.186 2.277 3.346.3.758.4 1.562.713 2.365.713 1.963 1.383 4.15 2.32 5.98.5.937 1.025 1.92 1.65 2.767.357.5.982.714 1.115 1.517-.625.893-.668 2.23-1.025 3.347-1.607 5.042-.982 11.288 1.293 15 .715 1.115 2.4 3.57 4.686 2.632 2.008-.803 1.56-3.346 2.14-5.577.135-.535.045-.892.312-1.25v.1l1.83 3.703c1.383 2.186 3.793 4.462 5.8 5.98 1.07.803 1.918 2.187 3.256 2.677v-.135h-.088c-.268-.4-.67-.58-1.027-.892-.803-.803-1.695-1.785-2.32-2.677-1.873-2.498-3.523-5.265-4.996-8.12-.715-1.383-1.34-2.9-1.918-4.283-.27-.536-.27-1.34-.715-1.606-.67.98-1.65 1.83-2.143 3.034-.848 1.918-.936 4.283-1.248 6.737-.18.045-.1 0-.18.1-1.426-.356-1.918-1.83-2.453-3.078-1.338-3.168-1.562-8.254-.402-11.913.312-.937 1.652-3.882 1.117-4.774-.27-.848-1.16-1.338-1.652-2.008-.58-.848-1.203-1.918-1.605-2.855-1.07-2.5-1.605-5.265-2.766-7.764-.537-1.16-1.473-2.365-2.232-3.435-.848-1.205-1.783-2.053-2.453-3.48-.223-.5-.535-1.294-.178-1.83.088-.357.268-.5.623-.58.58-.5 2.232.134 2.812.4 1.65.67 3.033 1.294 4.416 2.23.625.446 1.295 1.294 2.098 1.518h.938c1.428.312 3.033.1 4.37.5 2.365.76 4.506 1.874 6.426 3.08 5.844 3.703 10.664 8.968 13.92 15.26.535 1.026.758 1.963 1.25 3.034.938 2.187 2.098 4.417 3.033 6.56.938 2.097 1.83 4.24 3.168 5.98.67.937 3.346 1.427 4.55 1.918.893.4 2.275.76 3.08 1.25 1.516.937 3.033 2.008 4.46 3.034.713.534 2.945 1.65 3.078 2.54zm-45.5-38.772a7.09 7.09 0 0 0-1.828.223v.1h.088c.357.714.982 1.205 1.428 1.83l1.027 2.142.088-.1c.625-.446.938-1.16.938-2.23-.268-.312-.312-.625-.535-.937-.268-.446-.848-.67-1.206-1.026z"
transform="matrix(.390229 0 0 .38781 -46.300037 -16.856717)"
fill-rule="evenodd"
fill="#00678c"></path></svg
>

View File

@@ -0,0 +1,60 @@
<script>
export let customClass;
</script>
<svg
class={customClass}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 432.071 445.383"
xml:space="preserve"
>
<g
id="orginal"
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;"
>
</g>
<g
id="Layer_x0020_3"
style="fill-rule:nonzero;clip-rule:nonzero;fill:none;stroke:#FFFFFF;stroke-width:12.4651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;"
>
<path
style="fill:#000000;stroke:#000000;stroke-width:37.3953;stroke-linecap:butt;stroke-linejoin:miter;"
d="M323.205,324.227c2.833-23.601,1.984-27.062,19.563-23.239l4.463,0.392c13.517,0.615,31.199-2.174,41.587-7c22.362-10.376,35.622-27.7,13.572-23.148c-50.297,10.376-53.755-6.655-53.755-6.655c53.111-78.803,75.313-178.836,56.149-203.322 C352.514-5.534,262.036,26.049,260.522,26.869l-0.482,0.089c-9.938-2.062-21.06-3.294-33.554-3.496c-22.761-0.374-40.032,5.967-53.133,15.904c0,0-161.408-66.498-153.899,83.628c1.597,31.936,45.777,241.655,98.47,178.31 c19.259-23.163,37.871-42.748,37.871-42.748c9.242,6.14,20.307,9.272,31.912,8.147l0.897-0.765c-0.281,2.876-0.157,5.689,0.359,9.019c-13.572,15.167-9.584,17.83-36.723,23.416c-27.457,5.659-11.326,15.734-0.797,18.367c12.768,3.193,42.305,7.716,62.268-20.224 l-0.795,3.188c5.325,4.26,4.965,30.619,5.72,49.452c0.756,18.834,2.017,36.409,5.856,46.771c3.839,10.36,8.369,37.05,44.036,29.406c29.809-6.388,52.6-15.582,54.677-101.107"
></path>
<path
style="fill:#336791;stroke:none;"
d="M402.395,271.23c-50.302,10.376-53.76-6.655-53.76-6.655c53.111-78.808,75.313-178.843,56.153-203.326c-52.27-66.785-142.752-35.2-144.262-34.38l-0.486,0.087c-9.938-2.063-21.06-3.292-33.56-3.496c-22.761-0.373-40.026,5.967-53.127,15.902 c0,0-161.411-66.495-153.904,83.63c1.597,31.938,45.776,241.657,98.471,178.312c19.26-23.163,37.869-42.748,37.869-42.748c9.243,6.14,20.308,9.272,31.908,8.147l0.901-0.765c-0.28,2.876-0.152,5.689,0.361,9.019c-13.575,15.167-9.586,17.83-36.723,23.416 c-27.459,5.659-11.328,15.734-0.796,18.367c12.768,3.193,42.307,7.716,62.266-20.224l-0.796,3.188c5.319,4.26,9.054,27.711,8.428,48.969c-0.626,21.259-1.044,35.854,3.147,47.254c4.191,11.4,8.368,37.05,44.042,29.406c29.809-6.388,45.256-22.942,47.405-50.555 c1.525-19.631,4.976-16.729,5.194-34.28l2.768-8.309c3.192-26.611,0.507-35.196,18.872-31.203l4.463,0.392c13.517,0.615,31.208-2.174,41.591-7c22.358-10.376,35.618-27.7,13.573-23.148z"
></path>
<path
d="M215.866,286.484c-1.385,49.516,0.348,99.377,5.193,111.495c4.848,12.118,15.223,35.688,50.9,28.045c29.806-6.39,40.651-18.756,45.357-46.051c3.466-20.082,10.148-75.854,11.005-87.281"
></path>
<path
d="M173.104,38.256c0,0-161.521-66.016-154.012,84.109c1.597,31.938,45.779,241.664,98.473,178.316c19.256-23.166,36.671-41.335,36.671-41.335"
></path>
<path
d="M260.349,26.207c-5.591,1.753,89.848-34.889,144.087,34.417c19.159,24.484-3.043,124.519-56.153,203.329"
></path>
<path
style="stroke-linejoin:bevel;"
d="M348.282,263.953c0,0,3.461,17.036,53.764,6.653c22.04-4.552,8.776,12.774-13.577,23.155c-18.345,8.514-59.474,10.696-60.146-1.069c-1.729-30.355,21.647-21.133,19.96-28.739c-1.525-6.85-11.979-13.573-18.894-30.338 c-6.037-14.633-82.796-126.849,21.287-110.183c3.813-0.789-27.146-99.002-124.553-100.599c-97.385-1.597-94.19,119.762-94.19,119.762"
></path>
<path
d="M188.604,274.334c-13.577,15.166-9.584,17.829-36.723,23.417c-27.459,5.66-11.326,15.733-0.797,18.365c12.768,3.195,42.307,7.718,62.266-20.229c6.078-8.509-0.036-22.086-8.385-25.547c-4.034-1.671-9.428-3.765-16.361,3.994z"
></path>
<path
d="M187.715,274.069c-1.368-8.917,2.93-19.528,7.536-31.942c6.922-18.626,22.893-37.255,10.117-96.339c-9.523-44.029-73.396-9.163-73.436-3.193c-0.039,5.968,2.889,30.26-1.067,58.548c-5.162,36.913,23.488,68.132,56.479,64.938"
></path>
<path
style="fill:#FFFFFF;stroke-width:4.155;stroke-linecap:butt;stroke-linejoin:miter;"
d="M172.517,141.7c-0.288,2.039,3.733,7.48,8.976,8.207c5.234,0.73,9.714-3.522,9.998-5.559c0.284-2.039-3.732-4.285-8.977-5.015c-5.237-0.731-9.719,0.333-9.996,2.367z"
></path>
<path
style="fill:#FFFFFF;stroke-width:2.0775;stroke-linecap:butt;stroke-linejoin:miter;"
d="M331.941,137.543c0.284,2.039-3.732,7.48-8.976,8.207c-5.238,0.73-9.718-3.522-10.005-5.559c-0.277-2.039,3.74-4.285,8.979-5.015c5.239-0.73,9.718,0.333,10.002,2.368z"
></path>
<path
d="M350.676,123.432c0.863,15.994-3.445,26.888-3.988,43.914c-0.804,24.748,11.799,53.074-7.191,81.435"
></path>
<path style="stroke-width:3;" d="M0,60.232"></path>
</g>
</svg>

View File

@@ -0,0 +1,52 @@
<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;
}
100% {
height: 4px;
}
}
</style>
<script>
export let fullscreen = true;
</script>
{#if fullscreen}
<div class="fixed top-0 flex flex-wrap content-center h-full w-full">
<span class="loader"></span>
</div>
{/if}

18
src/index.css Normal file
View File

@@ -0,0 +1,18 @@
@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';
}

8
src/index.js Normal file
View File

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

View File

@@ -0,0 +1,11 @@
<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>

258
src/pages/_layout.svelte Normal file
View File

@@ -0,0 +1,258 @@
<style lang="postcss">
.min-w-4rem {
min-width: 4rem;
}
.main {
width: calc(100% - 4rem);
margin-left: 4rem;
}
</style>
<script>
import { url, goto, route, isActive, redirect } from "@roxi/routify/runtime";
import {
loggedIn,
session,
fetch,
deployments,
application,
initConf,
} from "@store";
import { toast } from "@zerodevx/svelte-toast";
import packageJson from "../../package.json";
import { onMount } from "svelte";
let upgradeAvailable = false;
let upgradeDisabled = false;
let upgradeDone = false;
let latest = {};
onMount(async () => {
upgradeAvailable = await checkUpgrade();
});
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://raw.githubusercontent.com/coollabsio/coolify/main/package.json",
{ cache: "no-cache" },
)
.then(r => r.json());
if (
latest.version.split(".").join("") >
packageJson.version.split(".").join("")
) {
return true;
}
}
</script>
{#await verifyToken() then notUsed}
{#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 space-y-4 transition-all duration-100"
class:border-green-500="{$isActive('/dashboard/applications')}"
class:border-purple-500="{$isActive('/dashboard/databases')}"
>
<img class="w-10 pt-4 pb-4" src="/favicon.png" alt="coolLabs logo" />
<div
class="p-2 hover:bg-warmGray-700 rounded hover:text-green-500 my-4 transition-all duration-100 cursor-pointer"
on:click="{() => $goto('/dashboard/applications')}"
class:text-green-500="{$isActive('/dashboard/applications') ||
$isActive('/application')}"
class:bg-warmGray-700="{$isActive('/dashboard/applications') ||
$isActive('/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><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>
<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="{$isActive('/dashboard/databases') ||
$isActive('/database')}"
class:bg-warmGray-700="{$isActive('/dashboard/databases') ||
$isActive('/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"
></path>
</svg>
</div>
<div class="flex-1"></div>
<button
title="Settings"
class="p-2 hover:bg-warmGray-700 rounded hover:text-yellow-500 my-4 transition-all duration-100 cursor-pointer"
class:text-yellow-500="{$isActive('/settings')}"
class:bg-warmGray-700="{$isActive('/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>
<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>
<div
class="cursor-pointer text-xs font-bold text-warmGray-400 py-2 hover:bg-warmGray-700 w-full text-center"
>
v{packageJson.version}
</div>
</div>
</nav>
{/if}
{#if upgradeAvailable}
<footer
class="absolute top-0 right-0 p-2 w-auto rounded-tl text-white "
>
<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 font-bold text-xs rounded px-2 py-2"
disabled="{upgradeDisabled}"
on:click="{upgrade}"
>New version available. <br>Click here to upgrade!</button
>
{:else if upgradeDone}
<button
use:reloadInAMin
class="font-bold text-xs rounded px-2 cursor-not-allowed"
disabled="{upgradeDisabled}"
>Upgrade done. 🎉 Automatically reloading in 30s.</button
>
{:else}
<button
class="opacity-50 tracking-tight font-bold text-xs rounded px-2 cursor-not-allowed"
disabled="{upgradeDisabled}">Upgrading. It could take a while, please wait...</button
>
{/if}
</div>
</footer>
{/if}
<main class:main={$route.path !== "/index"}>
<slot />
</main>
{:catch test}
{$goto("/index")}
{/await}

View File

@@ -0,0 +1,62 @@
<script>
import { redirect, isActive } from "@roxi/routify";
import { application, fetch, initialApplication, initConf } from "@store";
import { toast } from "@zerodevx/svelte-toast";
import Configuration from "../../../../../components/Application/Configuration/Configuration.svelte";
import Loading from "../../../../../components/Loading.svelte";
async function loadConfiguration() {
if (!$isActive("/application/new")) {
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");
}
} else {
$application = JSON.parse(JSON.stringify(initialApplication));
}
}
</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"
>
<a
target="_blank"
class="text-green-500 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"
></path></svg
></a
>
</div>
</div>
<Configuration />

View File

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

View File

@@ -0,0 +1,57 @@
<script>
import { params } 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() {
const { events, progress } = await $fetch(
`/api/v1/application/deploy/logs/${$params.deployId}`,
);
logs = [...events];
if (progress === "done" || progress === "failed") {
clearInterval(loadLogsInterval);
}
}
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 space-y-2 max-w-7xl mx-auto px-6"
in:fade="{{ duration: 100 }}"
>
<div class="max-w-4xl mx-auto" in:fade="{{ duration: 100 }}">
<pre
class="text-left font-mono text-xs font-medium tracking-tighter rounded-lg bg-warmGray-800 p-4 whitespace-pre-wrap">
{#if logs.length > 0}
{#each logs as log}
{log + '\n'}
{/each}
{:else}
It's starting soon.
{/if}
</pre>
</div>
</div>
{/await}

View File

@@ -0,0 +1,140 @@
<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 space-y-2 max-w-7xl mx-auto 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">Waiting for the logs...</div>
{:else}
<pre
class="text-left font-mono text-xs font-medium rounded bg-warmGray-800 text-white p-4 whitespace-pre-wrap w-full">
{#each logs as log}
{log + '\n'}
{/each}
</pre>
{/if}
</div>
<div>
<div class="font-bold text-left pb-2 text-xl w-300">
Deployment logs
</div>
{#if deployments.length > 0}
{#each deployments as deployment}
<div
in:fade="{{ duration: 100 }}"
class="flex space-x-4 text-md py-4 hover:shadow mx-auto cursor-pointer transition-all duration-100 border-l-4 border-transparent rounded hover:bg-warmGray-700"
class:hover:border-green-500="{deployment.progress === 'done'}"
class:border-yellow-300="{deployment.progress !== 'done' &&
deployment.progress !== 'failed'}"
class:bg-warmGray-800="{deployment.progress !== 'done' &&
deployment.progress !== 'failed'}"
class:hover:bg-red-200="{deployment.progress === 'failed'}"
class:hover:border-red-500="{deployment.progress === 'failed'}"
on:click="{() => $goto(`./${deployment.deployId}`)}"
>
<div
class="font-bold text-sm px-3 flex justify-center items-center"
>
{deployment.branch}
</div>
<div class="flex-1"></div>
<div class="px-3 w-48">
<div
class="text-xs"
title="{new Intl.DateTimeFormat(
'default',
$dateOptions,
).format(new Date(deployment.createdAt))}"
>
{deployment.since}
</div>
{#if deployment.progress === "done"}
<div class="text-xs">
Deployed in <span class="font-bold">{deployment.took}s</span
>
</div>
{:else if deployment.progress === "failed"}
<div class="text-xs text-red-500">Failed</div>
{:else}
<div class="text-xs">Deploying...</div>
{/if}
</div>
</div>
{/each}
<button
class="text-xs bg-green-600 hover:bg-green-500 p-1 rounded text-white px-2 font-medium my-6"
on:click="{loadMoreDeploymentLogs}">Show more</button
>
{:else}
<div class="text-left text-sm tracking-tight">
No deployments found
</div>
{/if}
</div>
</div>
</div>
{:catch}
<div class="text-center font-bold tracking-tight text-xl">No logs found</div>
{/await}

View File

@@ -0,0 +1,31 @@
<script>
import { fade } from "svelte/transition";
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="text-green-500 hover:underline cursor-pointer px-2"
href="{'https://' +
$application.publish.domain +
$application.publish.path}">{$application.publish.domain}</a
>
<a
target="_blank"
class="icon"
href="{`https://github.com/${$application.repository.organization}/${$application.repository.name}`}"
>
<svg class="w-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg></a
>
</div>
</div>
<Configuration />

View File

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

View File

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

View File

@@ -0,0 +1,215 @@
<script>
import { params, goto, redirect, isActive } from "@roxi/routify";
import { application, fetch, initialApplication, initConf } from "@store";
import { onDestroy } from "svelte";
import { fade } from "svelte/transition";
import Loading from "../../components/Loading.svelte";
import { toast } from "@zerodevx/svelte-toast";
$application.repository.organization = $params.organization;
$application.repository.name = $params.name;
$application.repository.branch = $params.branch;
async function loadConfiguration() {
if (!$isActive("/application/new")) {
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");
}
} else {
$application = JSON.parse(JSON.stringify(initialApplication));
}
}
async function removeApplication() {
await $fetch(`/api/v1/application/remove`, {
body: {
organization: $params.organization,
name: $params.name,
branch: $params.branch,
},
});
toast.push("Application removed.");
$redirect(`/dashboard/applications`);
}
onDestroy(() => {
$application = JSON.parse(JSON.stringify(initialApplication));
});
async function deploy() {
try {
toast.push("Checking inputs.");
await $fetch(`/api/v1/application/check`, {
body: $application,
});
const { nickname, name } = await $fetch(`/api/v1/application/deploy`, {
body: $application,
});
$application.general.nickname = nickname;
$application.build.container.name = name;
$initConf = JSON.parse(JSON.stringify($application));
toast.push("Application deployment queued.");
$redirect(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/logs`,
);
} catch (error) {
console.log(error);
toast.push(error.error ? error.error : "Ooops something went wrong.");
}
}
</script>
{#await loadConfiguration()}
<Loading />
{:then}
<nav
class="flex text-white justify-end items-center m-4 fixed right-0 top-0 space-x-4"
>
<button
title="Deploy"
disabled="{$application.publish.domain === '' ||
$application.publish.domain === null}"
class:cursor-not-allowed="{$application.publish.domain === '' ||
$application.publish.domain === null}"
class:hover:text-green-500="{$application.publish.domain}"
class:hover:bg-warmGray-700="{$application.publish.domain}"
class:hover:bg-transparent="{$isActive('/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"></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>
<button
title="Delete"
disabled="{$application.publish.domain === '' ||
$application.publish.domain === null ||
$isActive('/application/new')}"
class:cursor-not-allowed="{$application.publish.domain === '' ||
$application.publish.domain === null ||
$isActive('/application/new')}"
class:hover:text-red-500="{$application.publish.domain &&
!$isActive('/application/new')}"
class:hover:bg-warmGray-700="{$application.publish.domain &&
!$isActive('/application/new')}"
class:hover:bg-transparent="{$isActive('/application/new')}"
class:text-warmGray-700="{$application.publish.domain === '' ||
$application.publish.domain === null ||
$isActive('/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"
></path>
</svg>
</button>
<div class="border border-warmGray-700 h-8"></div>
<button
title="Logs"
class="icon"
class:text-warmGray-700="{$isActive('/application/new')}"
disabled="{$isActive('/application/new')}"
class:hover:text-blue-400="{!$isActive('/application/new')}"
class:hover:bg-transparent="{$isActive('/application/new')}"
class:cursor-not-allowed="{$isActive('/application/new')}"
class:text-blue-400="{$isActive(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/logs`,
)}"
class:bg-warmGray-700="{$isActive(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/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>
<button
title="Configuration"
class="icon hover:text-yellow-400"
disabled="{$isActive(`/application/new`)}"
class:text-yellow-400="{$isActive(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/configuration`,
) || $isActive(`/application/new`)}"
class:bg-warmGray-700="{$isActive(
`/application/${$application.repository.organization}/${$application.repository.name}/${$application.repository.branch}/configuration`,
) || $isActive(`/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"
></path>
</svg>
</button>
</nav>
<div class="text-white">
<slot />
</div>
{/await}

View File

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

View File

@@ -0,0 +1,13 @@
<script>
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"
>
New Application
</div>
</div>
<Configuration />

View File

@@ -0,0 +1,32 @@
<script>
import { fetch, deployments } from "@store";
import { onDestroy, onMount } from "svelte";
import { fade } from "svelte/transition";
import { goto, isActive } from "@roxi/routify/runtime";
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>

View File

@@ -0,0 +1,120 @@
<script>
import { deployments } 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>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">
<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 py-6 w-52 h-32 bg-warmGray-800 hover:bg-green-500 text-white shadow-md cursor-pointer ease-in-out transform hover:scale-105 duration-200 hover:rotate-1 group"
on:click="{() =>
switchTo({
branch:
application.Spec.Labels.configuration.repository.branch,
name: application.Spec.Labels.configuration.repository.name,
organization:
application.Spec.Labels.configuration.repository
.organization,
})}"
>
<div class="flex items-center ">
{#if application.Spec.Labels.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.Spec.Labels.configuration.build.pack === "nodejs"}
<svg
class="text-white 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>
{/if}
<div
class="text-xs font-bold text-center w-full text-warmGray-300 group-hover:text-white"
>
{application.Spec.Labels.configuration.publish
.domain}{application.Spec.Labels.configuration.publish
.path !== "/"
? application.Spec.Labels.configuration.publish.path
: ""}
</div>
</div>
</div>
</div>
{/each}
</div>
</div>
{:else}
<div class="text-2xl font-bold text-center">No applications found</div>
{/if}
</div>

View File

@@ -0,0 +1,147 @@
<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";
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.Spec.Labels.configuration.general.deployId}/overview`,
)}"
>
<div
class="relative rounded-xl p-6 w-52 h-32 bg-warmGray-800 hover:bg-purple-500 text-white shadow-md cursor-pointer ease-in-out transform hover:scale-105 duration-200 hover:rotate-1 group"
>
<div class="flex items-center">
{#if database.Spec.Labels.configuration.general.type == "mongodb"}
<MongoDb customClass="w-10 h-10 absolute top-0 left-0 -m-4" />
{:else if database.Spec.Labels.configuration.general.type == "postgresql"}
<Postgresql
customClass="w-10 h-10 absolute top-0 left-0 -m-4"
/>
{:else if database.Spec.Labels.configuration.general.type == "mysql"}
<Mysql customClass="w-10 h-10 absolute top-0 left-0 -m-4" />
{:else if database.Spec.Labels.configuration.general.type == "couchdb"}
<CouchDb
customClass="w-10 h-10 fill-current text-red-600 absolute top-0 left-0 -m-4"
/>
{/if}
<div
class="text-xs font-bold text-center w-full text-warmGray-300 group-hover:text-white"
>
{database.Spec.Labels.configuration.general.nickname}
</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}
{/if}
</div>

View File

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

View File

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

View File

@@ -0,0 +1,91 @@
<script>
import { fetch, database } from "@store";
import { redirect, params } from "@roxi/routify/runtime";
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";
$: 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-5xl mx-auto px-6"
in:fade="{{ duration: 100 }}"
>
<div class="pb-2 pt-5">
<div class="flex items-center">
<div class="font-bold w-48 text-warmGray-400">Connection string</div>
{#if $database.config.general.type === "mongodb"}
<textarea
disabled
class="w-full"
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"}
<textarea
disabled
class="w-full"
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"}
<textarea
disabled
class="w-full"
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"}
<textarea
disabled
class="w-full"
value="{`http://${$database.envs.COUCHDB_USER}:${$database.envs.COUCHDB_PASSWORD}@${$database.config.general.deployId}:5984`}"
/>
{/if}
</div>
</div>
{#if $database.config.general.type === "mongodb"}
<div class="flex items-center">
<div class="font-bold w-48 text-warmGray-400">Root password</div>
<textarea
disabled
class="w-full"
value="{$database.envs.MONGODB_ROOT_PASSWORD}"
></textarea>
</div>
{/if}
</div>
{/await}

View File

@@ -0,0 +1,71 @@
<script>
import { params, goto, isActive, redirect, url } from "@roxi/routify";
import { fetch, database, initialDatabase } from "@store";
import { toast } from "@zerodevx/svelte-toast";
import { onDestroy } from "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"
>
<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>
<div class="border border-warmGray-700 h-8"></div>
<button
title="Configuration"
disabled
class="icon text-warmGray-700 hover:bg-transparent cursor-not-allowed"
>
<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>
</nav>
{/if}
<div class="text-white">
<slot />
</div>

View File

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

View File

@@ -0,0 +1,13 @@
<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 />

65
src/pages/index.svelte Normal file
View File

@@ -0,0 +1,65 @@
<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"
>
Coolify
</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-700 hover:bg-warmGray-600 rounded p-2 px-10 font-bold" on:click="{login}">Login with Github</button
>
{:else}
<button class="text-white bg-warmGray-700 hover:bg-warmGray-600 rounded p-2 px-10 font-bold" on:click="{() => $goto('/dashboard/applications')}"
>Get Started</button
>
{/if}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,110 @@
<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,
};
async function loadSettings() {
const response = await $fetch(`/api/v1/settings`);
settings.allowRegistration = response.settings.allowRegistration;
}
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 class="">
<div class="divide-y divide-gray-200">
<div class="px-4 sm:px-6">
<ul class="mt-2 divide-y divide-gray-200">
<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>
</ul>
</div>
</div>
</div>
</div>
</div>
{/await}

220
src/store.js Normal file
View File

@@ -0,0 +1,220 @@
import { writable, derived, readable } from 'svelte/store'
const sessionStore = {
token: window.localStorage.getItem('token') || null,
githubAppToken: null
}
function waitAtLeast (time, promise) {
const timeoutPromise = new Promise((resolve) => {
setTimeout(resolve, time)
})
return Promise.all([promise, timeoutPromise]).then((values) => values[0])
};
export const fetch = writable(
async (
url,
{ method, body, ...customConfig } = { body: null, method: null }
) => {
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 ${sessionStore.githubAppToken}`
})
} else {
headers = Object.assign(headers, {
Authorization: `Bearer ${sessionStore.token}`
})
}
const config = {
cache: 'no-cache',
method: method || (body ? 'POST' : 'GET'),
...customConfig,
headers: {
...headers,
...customConfig.headers
}
}
if (body) {
config.body = JSON.stringify(body)
}
const response = await waitAtLeast(350, window.fetch(url, config))
if (response.status >= 200 && response.status <= 299) {
if (response.headers.get('content-type').match(/application\/json/)) {
return await response.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 {
return await response.blob()
}
} else {
/* eslint-disable */
if (response.status === 401) {
return Promise.reject({
code: response.status,
error: 'Unauthorized'
})
} else if (response.status >= 500) {
const error = (await response.json()).message
return Promise.reject({
code: response.status,
error: error || 'Oops, something is not okay. Are you okay?'
})
} else {
return Promise.reject({
code: response.status,
error: response.statusText
})
}
/* eslint-enable */
}
}
)
export const session = writable(sessionStore)
export const loggedIn = derived(session, ($session) => {
return $session.token
})
export const savedBranch = writable()
export const dateOptions = readable({
year: 'numeric',
month: 'short',
day: '2-digit',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
hour12: false
})
export const deployments = writable({})
export const initConf = writable({})
export const application = writable({
github: {
installation: {
id: null
},
app: {
id: null
}
},
repository: {
id: null,
organization: 'new',
name: 'start',
branch: null
},
general: {
deployId: null,
nickname: null,
workdir: null
},
build: {
pack: 'static',
directory: null,
command: {
build: null,
installation: null
},
container: {
name: null,
tag: null
}
},
publish: {
directory: null,
domain: null,
path: '/',
port: null,
secrets: []
}
})
export const initialApplication = {
github: {
installation: {
id: null
},
app: {
id: null
}
},
repository: {
id: null,
organization: 'new',
name: 'start',
branch: null
},
general: {
deployId: null,
nickname: null,
workdir: null
},
build: {
pack: 'static',
directory: null,
command: {
build: null,
installation: null
},
container: {
name: null,
tag: null
}
},
publish: {
directory: null,
domain: null,
path: '/',
port: null,
secrets: []
}
}
export const initialDatabase = {
config: {
general: {
workdir: null,
deployId: null,
nickname: null,
type: null
},
database: {
username: null,
passwords: [],
defaultDatabaseName: null
},
deploy: {
name: null
}
},
envs: {}
}
export const database = writable({
config: {
general: {
workdir: null,
deployId: null,
nickname: null,
type: null
},
database: {
username: null,
passwords: [],
defaultDatabaseName: null
},
deploy: {
name: null
}
},
envs: {}
})
export const dbInprogress = writable(false)