initial production release 🎉
This commit is contained in:
63
src/App.svelte
Normal file
63
src/App.svelte
Normal 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}" />
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
24
src/components/Application/Configuration/Branches.svelte
Normal file
24
src/components/Application/Configuration/Branches.svelte
Normal 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}
|
133
src/components/Application/Configuration/Configuration.svelte
Normal file
133
src/components/Application/Configuration/Configuration.svelte
Normal 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>
|
50
src/components/Application/Configuration/Login.svelte
Normal file
50
src/components/Application/Configuration/Login.svelte
Normal 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>
|
45
src/components/Application/Configuration/Repositories.svelte
Normal file
45
src/components/Application/Configuration/Repositories.svelte
Normal 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>
|
97
src/components/Application/Configuration/Tabs.svelte
Normal file
97
src/components/Application/Configuration/Tabs.svelte
Normal 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>
|
90
src/components/Databases/Configuration/Configuration.svelte
Normal file
90
src/components/Databases/Configuration/Configuration.svelte
Normal 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>
|
16
src/components/Databases/SVGs/CouchDb.svelte
Normal file
16
src/components/Databases/SVGs/CouchDb.svelte
Normal 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
|
||||
>
|
32
src/components/Databases/SVGs/MongoDb.svelte
Normal file
32
src/components/Databases/SVGs/MongoDb.svelte
Normal 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>
|
16
src/components/Databases/SVGs/Mysql.svelte
Normal file
16
src/components/Databases/SVGs/Mysql.svelte
Normal 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
|
||||
>
|
60
src/components/Databases/SVGs/Postgresql.svelte
Normal file
60
src/components/Databases/SVGs/Postgresql.svelte
Normal 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>
|
52
src/components/Loading.svelte
Normal file
52
src/components/Loading.svelte
Normal 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
18
src/index.css
Normal 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
8
src/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import App from './App.svelte'
|
||||
import './index.css'
|
||||
|
||||
const app = new App({
|
||||
target: document.body
|
||||
})
|
||||
|
||||
export default app
|
11
src/pages/_fallback.svelte
Normal file
11
src/pages/_fallback.svelte
Normal 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
258
src/pages/_layout.svelte
Normal 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}
|
@@ -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 />
|
@@ -0,0 +1,4 @@
|
||||
<script>
|
||||
import { params, redirect } from "@roxi/routify";
|
||||
$redirect(`/application/${$params.organization}/${$params.name}/${$params.branch}/configuration`);
|
||||
</script>
|
@@ -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}
|
@@ -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}
|
@@ -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 />
|
4
src/pages/application/[organization]/[name]/index.svelte
Normal file
4
src/pages/application/[organization]/[name]/index.svelte
Normal file
@@ -0,0 +1,4 @@
|
||||
<script>
|
||||
import { redirect } from "@roxi/routify";
|
||||
$redirect("/dashboard/applications");
|
||||
</script>
|
4
src/pages/application/[organization]/index.svelte
Normal file
4
src/pages/application/[organization]/index.svelte
Normal file
@@ -0,0 +1,4 @@
|
||||
<script>
|
||||
import { redirect } from "@roxi/routify";
|
||||
$redirect("/dashboard/applications");
|
||||
</script>
|
215
src/pages/application/_layout.svelte
Normal file
215
src/pages/application/_layout.svelte
Normal 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}
|
4
src/pages/application/index.svelte
Normal file
4
src/pages/application/index.svelte
Normal file
@@ -0,0 +1,4 @@
|
||||
<script>
|
||||
import { redirect } from "@roxi/routify";
|
||||
$redirect("/dashboard/applications");
|
||||
</script>
|
13
src/pages/application/new.svelte
Normal file
13
src/pages/application/new.svelte
Normal 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 />
|
32
src/pages/dashboard/_layout.svelte
Normal file
32
src/pages/dashboard/_layout.svelte
Normal 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>
|
120
src/pages/dashboard/applications.svelte
Normal file
120
src/pages/dashboard/applications.svelte
Normal 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>
|
147
src/pages/dashboard/databases.svelte
Normal file
147
src/pages/dashboard/databases.svelte
Normal 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>
|
4
src/pages/dashboard/index.svelte
Normal file
4
src/pages/dashboard/index.svelte
Normal file
@@ -0,0 +1,4 @@
|
||||
<script>
|
||||
import { redirect } from "@roxi/routify";
|
||||
$redirect("/dashboard/applications");
|
||||
</script>
|
4
src/pages/database/[name]/index.svelte
Normal file
4
src/pages/database/[name]/index.svelte
Normal file
@@ -0,0 +1,4 @@
|
||||
<script>
|
||||
import { redirect } from "@roxi/routify";
|
||||
$redirect("/dashboard/databases");
|
||||
</script>
|
91
src/pages/database/[name]/overview.svelte
Normal file
91
src/pages/database/[name]/overview.svelte
Normal 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}
|
71
src/pages/database/_layout.svelte
Normal file
71
src/pages/database/_layout.svelte
Normal 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>
|
5
src/pages/database/index.svelte
Normal file
5
src/pages/database/index.svelte
Normal file
@@ -0,0 +1,5 @@
|
||||
<script>
|
||||
import { redirect } from "@roxi/routify";
|
||||
$redirect("/dashboard/databases");
|
||||
</script>
|
||||
|
13
src/pages/database/new.svelte
Normal file
13
src/pages/database/new.svelte
Normal 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
65
src/pages/index.svelte
Normal 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>
|
110
src/pages/settings/index.svelte
Normal file
110
src/pages/settings/index.svelte
Normal 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
220
src/store.js
Normal 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)
|
Reference in New Issue
Block a user