Just v2
This commit is contained in:
Andras Bacsai
2022-02-10 15:47:44 +01:00
committed by GitHub
parent a64b095c13
commit 460ae85226
403 changed files with 22039 additions and 12465 deletions

View File

@@ -0,0 +1,376 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
function checkConfiguration(application): string {
let configurationPhase = null;
if (!application.gitSourceId) {
configurationPhase = 'source';
} else if (!application.repository && !application.branch) {
configurationPhase = 'repository';
} else if (!application.destinationDockerId) {
configurationPhase = 'destination';
} else if (!application.buildPack) {
configurationPhase = 'buildpack';
}
return configurationPhase;
}
export const load: Load = async ({ fetch, url, params }) => {
const endpoint = `/applications/${params.id}.json`;
const res = await fetch(endpoint);
if (res.ok) {
const { application, githubToken, ghToken, isRunning, appId } = await res.json();
if (!application || Object.entries(application).length === 0) {
return {
status: 302,
redirect: '/applications'
};
}
const configurationPhase = checkConfiguration(application);
if (
configurationPhase &&
url.pathname !== `/applications/${params.id}/configuration/${configurationPhase}`
) {
return {
status: 302,
redirect: `/applications/${params.id}/configuration/${configurationPhase}`
};
}
return {
props: {
application,
isRunning
},
stuff: {
isRunning,
ghToken,
githubToken,
application,
appId
}
};
}
return {
status: 302,
redirect: '/applications'
};
};
</script>
<script lang="ts">
export let application;
export let isRunning;
import { page, session } from '$app/stores';
import { errorNotification } from '$lib/form';
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import Loading from '$lib/components/Loading.svelte';
import { del, post } from '$lib/api';
import { goto } from '$app/navigation';
let loading = false;
const { id } = $page.params;
async function handleDeploySubmit() {
try {
const { buildId } = await post(`/applications/${id}/deploy.json`, { ...application });
return await goto(`/applications/${id}/logs/build?buildId=${buildId}`);
} catch ({ error }) {
return errorNotification(error);
}
}
async function deleteApplication(name) {
const sure = confirm(`Are you sure you would like to delete '${name}'?`);
if (sure) {
loading = true;
try {
await del(`/applications/${id}/delete.json`, { id });
return await goto(`/applications`);
} catch ({ error }) {
return errorNotification(error);
}
}
}
async function stopApplication() {
try {
loading = true;
await post(`/applications/${id}/stop.json`, {});
return window.location.reload();
} catch ({ error }) {
return errorNotification(error);
} finally {
loading = false;
}
}
</script>
<nav class="nav-side">
{#if loading}
<Loading fullscreen cover />
{:else}
{#if application.fqdn && application.gitSource && application.repository && application.destinationDocker && application.buildPack}
{#if isRunning}
<button
on:click={stopApplication}
title="Stop application"
type="submit"
disabled={!$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 hover:bg-green-600 hover:text-white"
data-tooltip={$session.isAdmin
? 'Stop application'
: 'You do not have permission to stop the application.'}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg>
</button>
<form on:submit|preventDefault={handleDeploySubmit}>
<button
title="Rebuild application"
type="submit"
disabled={!$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 hover:bg-green-600 hover:text-white"
data-tooltip={$session.isAdmin
? 'Rebuild application'
: 'You do not have permission to rebuild application.'}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M16.3 5h.7a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h5l-2.82 -2.82m0 5.64l2.82 -2.82"
transform="rotate(-45 12 12)"
/>
</svg>
</button>
</form>
{:else}
<form on:submit|preventDefault={handleDeploySubmit}>
<button
title="Build and start application"
type="submit"
disabled={!$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 hover:bg-green-600 hover:text-white"
data-tooltip={$session.isAdmin
? 'Build and start application'
: 'You do not have permission to Build and start application.'}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
</button>
</form>
{/if}
<div class="border border-stone-700 h-8" />
<a
href="/applications/{id}"
sveltekit:prefetch
class="hover:text-yellow-500 rounded"
class:text-yellow-500={$page.url.pathname === `/applications/${id}`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}`}
>
<button
title="Configurations"
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
data-tooltip="Configurations"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="4" y="8" width="4" height="4" />
<line x1="6" y1="4" x2="6" y2="8" />
<line x1="6" y1="12" x2="6" y2="20" />
<rect x="10" y="14" width="4" height="4" />
<line x1="12" y1="4" x2="12" y2="14" />
<line x1="12" y1="18" x2="12" y2="20" />
<rect x="16" y="5" width="4" height="4" />
<line x1="18" y1="4" x2="18" y2="5" />
<line x1="18" y1="9" x2="18" y2="20" />
</svg></button
></a
>
<a
href="/applications/{id}/secrets"
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/applications/${id}/secrets`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/secrets`}
>
<button
title="Secrets"
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
data-tooltip="Secrets"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3"
/>
<circle cx="12" cy="11" r="1" />
<line x1="12" y1="12" x2="12" y2="14.5" />
</svg></button
></a
>
<a
href="/applications/{id}/previews"
sveltekit:prefetch
class="hover:text-orange-500 rounded"
class:text-orange-500={$page.url.pathname === `/applications/${id}/previews`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/previews`}
>
<button
title="Previews"
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
data-tooltip="Previews"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="7" cy="18" r="2" />
<circle cx="7" cy="6" r="2" />
<circle cx="17" cy="12" r="2" />
<line x1="7" y1="8" x2="7" y2="16" />
<path d="M7 8a4 4 0 0 0 4 4h4" />
</svg></button
></a
>
<div class="border border-stone-700 h-8" />
<a
href="/applications/{id}/logs"
sveltekit:prefetch
class="hover:text-sky-500 rounded"
class:text-sky-500={$page.url.pathname === `/applications/${id}/logs`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs`}
>
<button
title="Application Logs"
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500 "
data-tooltip="Application Logs"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<line x1="3" y1="6" x2="3" y2="19" />
<line x1="12" y1="6" x2="12" y2="19" />
<line x1="21" y1="6" x2="21" y2="19" />
</svg>
</button></a
>
<a
href="/applications/{id}/logs/build"
sveltekit:prefetch
class="hover:text-red-500 rounded"
class:text-red-500={$page.url.pathname === `/applications/${id}/logs/build`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs/build`}
>
<button
title="Build Logs"
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500 "
data-tooltip="Build Logs"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="19" cy="13" r="2" />
<circle cx="4" cy="17" r="2" />
<circle cx="13" cy="17" r="2" />
<line x1="13" y1="19" x2="4" y2="19" />
<line x1="4" y1="15" x2="13" y2="15" />
<path d="M8 12v-5h2a3 3 0 0 1 3 3v5" />
<path d="M5 15v-2a1 1 0 0 1 1 -1h7" />
<path d="M19 11v-7l-6 7" />
</svg>
</button></a
>
<div class="border border-stone-700 h-8" />
{/if}
<button
on:click={() => deleteApplication(application.name)}
title="Delete application"
type="submit"
disabled={!$session.isAdmin}
class:hover:text-red-500={$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm"
data-tooltip={$session.isAdmin
? 'Delete application'
: 'You do not have permission to delete this application'}
>
<DeleteIcon />
</button>
{/if}
</nav>
<slot />

View File

@@ -0,0 +1,28 @@
import { asyncExecShell, getDomain, getEngine, getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { PrismaErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
let { fqdn } = await event.request.json();
fqdn = fqdn.toLowerCase();
try {
const found = await db.isDomainConfigured({ id, fqdn });
if (found) {
throw {
message: `Domain ${getDomain(fqdn)} is already configured.`
};
}
return {
status: 200
};
} catch (error) {
return PrismaErrorHandler(error);
}
};

View File

@@ -0,0 +1,44 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { post } from '$lib/api';
import { errorNotification } from '$lib/form';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
export let buildPack;
export let foundConfig;
export let scanning;
async function handleSubmit(name) {
try {
const tempBuildPack = JSON.parse(JSON.stringify(buildPack));
delete tempBuildPack.name;
delete tempBuildPack.fancyName;
delete tempBuildPack.color;
delete tempBuildPack.hoverColor;
if (foundConfig.buildPack !== name) {
await post(`/applications/${id}.json`, { ...tempBuildPack });
}
await post(`/applications/${id}/configuration/buildpack.json`, { buildPack: name });
return await goto(from || `/applications/${id}`);
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<form on:submit|preventDefault={() => handleSubmit(buildPack.name)}>
<button
type="submit"
class="box-selection relative flex text-xl font-bold {buildPack.hoverColor} {foundConfig?.name ===
buildPack.name && buildPack.color}"
><span>{buildPack.fancyName}</span>
{#if !scanning && foundConfig?.name === buildPack.name}
<span class="absolute bottom-0 pb-2 text-xs">Choose this one...</span>
{/if}
</button>
</form>

View File

@@ -0,0 +1,170 @@
<script lang="ts">
import { goto } from '$app/navigation';
export let githubToken;
export let application;
import { page } from '$app/stores';
import { get, post } from '$lib/api';
import { getGithubToken } from '$lib/components/common';
import { enhance, errorNotification } from '$lib/form';
import { onMount } from 'svelte';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
const to = $page.url.searchParams.get('to');
let htmlUrl = application.gitSource.htmlUrl;
let apiUrl = application.gitSource.apiUrl;
let loading = {
repositories: true,
branches: false
};
let repositories = [];
let branches = [];
let selected = {
projectId: undefined,
repository: undefined,
branch: undefined
};
let showSave = false;
let token = null;
async function loadRepositoriesByPage(page = 0) {
try {
return await get(`${apiUrl}/installation/repositories?per_page=100&page=${page}`, {
Authorization: `token ${token}`
});
} catch ({ error }) {
return errorNotification(error);
}
}
async function loadRepositories() {
token = await getGithubToken({ apiUrl, githubToken, application });
let page = 1;
let reposCount = 0;
const loadedRepos = await loadRepositoriesByPage();
repositories = repositories.concat(loadedRepos.repositories);
reposCount = loadedRepos.total_count;
if (reposCount > repositories.length) {
while (reposCount > repositories.length) {
page = page + 1;
const repos = await loadRepositoriesByPage(page);
repositories = repositories.concat(repos.repositories);
}
}
loading.repositories = false;
}
async function loadBranches() {
loading.branches = true;
selected.branch = undefined;
selected.projectId = repositories.find((repo) => repo.full_name === selected.repository).id;
try {
branches = await get(`${apiUrl}/repos/${selected.repository}/branches`, {
Authorization: `token ${token}`
});
return;
} catch ({ error }) {
return errorNotification(error);
} finally {
loading.branches = false;
}
}
async function isBranchAlreadyUsed() {
try {
return await get(
`/applications/${id}/configuration/repository.json?repository=${selected.repository}&branch=${selected.branch}`
);
} catch ({ error }) {
return errorNotification(error);
} finally {
showSave = true;
}
}
onMount(async () => {
await loadRepositories();
});
async function handleSubmit() {
try {
await post(`/applications/${id}/configuration/repository.json`, { ...selected });
if (to) {
return await goto(`${to}?from=${from}`);
}
return await goto(from || `/applications/${id}/configuration/destination`);
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
{#if repositories.length === 0 && loading.repositories === false}
<div class="flex-col text-center">
<div class="pb-4">No repositories configured for your Git Application.</div>
<a href={`/sources/${application.gitSource.id}`}><button>Configure it now</button></a>
</div>
{:else}
<form on:submit|preventDefault={handleSubmit}>
<div>
{#if loading.repositories}
<select name="repository" disabled class="w-96">
<option selected value="">Loading repositories...</option>
</select>
{:else}
<select
name="repository"
class="w-96"
bind:value={selected.repository}
on:change={loadBranches}
>
<option value="" disabled selected>Please select a repository</option>
{#each repositories as repository}
<option value={repository.full_name}>{repository.name}</option>
{/each}
</select>
{/if}
<input class="hidden" bind:value={selected.projectId} name="projectId" />
{#if loading.branches}
<select name="branch" disabled class="w-96">
<option selected value="">Loading branches...</option>
</select>
{:else}
<select
name="branch"
class="w-96"
disabled={!selected.repository}
bind:value={selected.branch}
on:change={isBranchAlreadyUsed}
>
{#if !selected.repository}
<option value="" disabled selected>Select a repository first</option>
{:else}
<option value="" disabled selected>Please select a branch</option>
{/if}
{#each branches as branch}
<option value={branch.name}>{branch.name}</option>
{/each}
</select>
{/if}
</div>
<div class="pt-5 flex-col flex justify-center items-center space-y-4">
<button
class="w-40"
type="submit"
disabled={!showSave}
class:bg-orange-600={showSave}
class:hover:bg-orange-500={showSave}>Save</button
>
<!-- <button class="w-40"
><a
class="no-underline"
href="{apiUrl}/apps/{application.gitSource.githubApp.name}/installations/new"
>Modify Repositories</a
></button
> -->
</div>
</form>
{/if}

View File

@@ -0,0 +1,334 @@
<script lang="ts">
export let application;
export let appId;
import { page, session } from '$app/stores';
import { onMount } from 'svelte';
import { errorNotification } from '$lib/form';
import { dev } from '$app/env';
import cuid from 'cuid';
import { goto } from '$app/navigation';
import { del, get, post, put } from '$lib/api';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
const updateDeployKeyIdUrl = `/applications/${id}/configuration/deploykey.json`;
let loading = {
base: true,
projects: false,
branches: false,
save: false
};
let htmlUrl = application.gitSource.htmlUrl;
let apiUrl = application.gitSource.apiUrl;
let username = null;
let groups = [];
let projects = [];
let branches = [];
let showSave = false;
let selected = {
group: undefined,
project: undefined,
branch: undefined
};
onMount(async () => {
if (!$session.gitlabToken) {
getGitlabToken();
} else {
loading.base = true;
try {
const user = await get(`${apiUrl}/v4/user`, {
Authorization: `Bearer ${$session.gitlabToken}`
});
username = user.username;
} catch (error) {
return getGitlabToken();
}
try {
groups = await get(`${apiUrl}/v4/groups?per_page=5000`, {
Authorization: `Bearer ${$session.gitlabToken}`
});
} catch (error) {
errorNotification(error);
throw new Error(error);
} finally {
loading.base = false;
}
}
});
function getGitlabToken() {
const left = screen.width / 2 - 1020 / 2;
const top = screen.height / 2 - 618 / 2;
const newWindow = open(
`${htmlUrl}/oauth/authorize?client_id=${application.gitSource.gitlabApp.appId}&redirect_uri=${window.location.origin}/webhooks/gitlab&response_type=code&scope=api+email+read_repository&state=${$page.params.id}`,
'GitLab',
'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);
window.location.reload();
}
}, 100);
}
async function loadProjects() {
loading.projects = true;
if (username === selected.group.name) {
try {
projects = await get(
`${apiUrl}/v4/users/${selected.group.name}/projects?min_access_level=40&page=1&per_page=25&archived=false`,
{
Authorization: `Bearer ${$session.gitlabToken}`
}
);
} catch (error) {
errorNotification(error);
throw new Error(error);
} finally {
loading.projects = false;
}
} else {
try {
projects = await get(
`${apiUrl}/v4/groups/${selected.group.id}/projects?page=1&per_page=25&archived=false`,
{
Authorization: `Bearer ${$session.gitlabToken}`
}
);
} catch (error) {
errorNotification(error);
throw new Error(error);
} finally {
loading.projects = false;
}
}
}
async function loadBranches() {
loading.branches = true;
try {
branches = await get(
`${apiUrl}/v4/projects/${selected.project.id}/repository/branches?per_page=100&page=1`,
{
Authorization: `Bearer ${$session.gitlabToken}`
}
);
} catch (error) {
errorNotification(error);
throw new Error(error);
} finally {
loading.branches = false;
}
}
async function isBranchAlreadyUsed() {
const url = `/applications/${id}/configuration/repository.json?repository=${selected.project.path_with_namespace}&branch=${selected.branch.name}`;
try {
await get(url);
showSave = true;
} catch (error) {
showSave = false;
return errorNotification('Branch already configured');
}
}
// async function saveDeployKey(deployKeyId: number) {
// try {
// await post(updateDeployKeyIdUrl, { deployKeyId });
// } catch (error) {
// errorNotification(error);
// throw new Error(error);
// }
// }
async function checkSSHKey(sshkeyUrl) {
try {
return await post(sshkeyUrl, {});
} catch (error) {
errorNotification(error);
throw new Error(error);
}
}
async function setWebhook(url, webhookToken) {
const host = dev
? 'https://webhook.site/0e5beb2c-4e9b-40e2-a89e-32295e570c21'
: `${window.location.origin}/webhooks/gitlab/events`;
try {
await post(
url,
{
id: selected.project.id,
url: host,
token: webhookToken,
push_events: true,
enable_ssl_verification: true,
merge_requests_events: true
},
{
Authorization: `Bearer ${$session.gitlabToken}`
}
);
} catch (error) {
errorNotification(error);
throw error;
}
}
async function save() {
loading.save = true;
let privateSshKey = application.gitSource.gitlabApp.privateSshKey;
let publicSshKey = application.gitSource.gitlabApp.publicSshKey;
const deployKeyUrl = `${apiUrl}/v4/projects/${selected.project.id}/deploy_keys`;
const sshkeyUrl = `/applications/${id}/configuration/sshkey.json`;
const webhookUrl = `${apiUrl}/v4/projects/${selected.project.id}/hooks`;
const webhookToken = cuid();
try {
if (!privateSshKey || !publicSshKey) {
const { publicKey } = await checkSSHKey(sshkeyUrl);
publicSshKey = publicKey;
}
const deployKeys = await get(deployKeyUrl, {
Authorization: `Bearer ${$session.gitlabToken}`
});
const deployKeyFound = deployKeys.filter((dk) => dk.title === `${appId}-coolify-deploy-key`);
if (deployKeyFound.length > 0) {
for (const deployKey of deployKeyFound) {
console.log(`${deployKeyUrl}/${deployKey.id}`);
await del(
`${deployKeyUrl}/${deployKey.id}`,
{},
{
Authorization: `Bearer ${$session.gitlabToken}`
}
);
}
}
const { id } = await post(
deployKeyUrl,
{
title: `${appId}-coolify-deploy-key`,
key: publicSshKey,
can_push: false
},
{
Authorization: `Bearer ${$session.gitlabToken}`
}
);
await post(updateDeployKeyIdUrl, { deployKeyId: id });
} catch (error) {
console.log(error);
throw new Error(error);
}
try {
await setWebhook(webhookUrl, webhookToken);
} catch (err) {
console.log(err);
if (!dev) throw new Error(err);
}
const url = `/applications/${id}/configuration/repository.json`;
try {
await post(url, {
repository: `${selected.group.full_path}/${selected.project.name}`,
branch: selected.branch.name,
projectId: selected.project.id,
webhookToken
});
return await goto(from || `/applications/${id}/configuration/buildpack`);
} catch ({ error }) {
return errorNotification(error);
}
}
async function handleSubmit() {
try {
await post(`/applications/{id}/configuration/repository.json`, { ...selected });
return await goto(from || `/applications/${id}/configuration/destination`);
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<form on:submit={handleSubmit}>
<div class="flex flex-col space-y-2 px-4 xl:flex-row xl:space-y-0 xl:space-x-2 ">
{#if loading.base}
<select name="group" disabled class="w-96">
<option selected value="">Loading groups...</option>
</select>
{:else}
<select name="group" class="w-96" bind:value={selected.group} on:change={loadProjects}>
<option value="" disabled selected>Please select a group</option>
{#each groups as group}
<option value={group}>{group.full_name}</option>
{/each}
</select>
{/if}
{#if loading.projects}
<select name="project" disabled class="w-96">
<option selected value="">Loading projects...</option>
</select>
{:else if !loading.projects && projects.length > 0}
<select
name="project"
class="w-96"
bind:value={selected.project}
on:change={loadBranches}
disabled={!selected.group}
>
<option value="" disabled selected>Please select a project</option>
{#each projects as project}
<option value={project}>{project.name}</option>
{/each}
</select>
{:else}
<select name="project" disabled class="w-96">
<option disabled selected value="">No projects found</option>
</select>
{/if}
{#if loading.branches}
<select name="branch" disabled class="w-96">
<option selected value="">Loading branches...</option>
</select>
{:else if !loading.branches && branches.length > 0}
<select
name="branch"
class="w-96"
bind:value={selected.branch}
on:change={isBranchAlreadyUsed}
disabled={!selected.project}
>
<option value="" disabled selected>Please select a branch</option>
{#each branches as branch}
<option value={branch}>{branch.name}</option>
{/each}
</select>
{:else}
<select name="project" disabled class="w-96">
<option disabled selected value="">No branches found</option>
</select>
{/if}
</div>
<div class="flex flex-col items-center justify-center space-y-4 pt-5">
<button
on:click|preventDefault={save}
class="w-40"
type="submit"
disabled={!showSave || loading.save}
class:bg-orange-600={showSave && !loading.save}
class:hover:bg-orange-500={showSave && !loading.save}
>{loading.save ? 'Saving...' : 'Save'}</button
>
</div>
</form>

View File

@@ -0,0 +1,41 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
import { PrismaErrorHandler } from '$lib/database';
export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const application = await db.getApplication({ id, teamId });
return {
status: 200,
body: {
type: application.gitSource.type,
projectId: application.projectId,
repository: application.repository,
branch: application.branch,
apiUrl: application.gitSource.apiUrl
}
};
} catch (error) {
return PrismaErrorHandler(error);
}
};
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
const { buildPack } = await event.request.json();
try {
await db.configureBuildPack({ id, buildPack });
return { status: 201 };
} catch (error) {
return PrismaErrorHandler(error);
}
};

View File

@@ -0,0 +1,186 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, url, stuff }) => {
const { application, ghToken } = stuff;
if (application?.buildPack && !url.searchParams.get('from')) {
return {
status: 302,
redirect: `/applications/${params.id}`
};
}
const endpoint = `/applications/${params.id}/configuration/buildpack.json`;
const res = await fetch(endpoint);
if (res.ok) {
return {
props: {
...(await res.json()),
application,
ghToken
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${url}`)
};
};
</script>
<script lang="ts">
import { onMount } from 'svelte';
import { buildPacks, scanningTemplates } from '$lib/components/templates';
import BuildPack from './_BuildPack.svelte';
import { page, session } from '$app/stores';
import { get } from '$lib/api';
import { errorNotification } from '$lib/form';
let scanning = true;
let foundConfig = null;
export let apiUrl;
export let projectId;
export let repository;
export let branch;
export let ghToken;
export let type;
export let application;
function checkPackageJSONContents({ key, json }) {
return json?.dependencies?.hasOwnProperty(key) || json?.devDependencies?.hasOwnProperty(key);
}
function checkTemplates({ json }) {
for (const [key, value] of Object.entries(scanningTemplates)) {
if (checkPackageJSONContents({ key, json })) {
return buildPacks.find((bp) => bp.name === value.buildPack);
}
}
}
async function scanRepository() {
try {
if (type === 'gitlab') {
const files = await get(`${apiUrl}/v4/projects/${projectId}/repository/tree`, {
Authorization: `Bearer ${$session.gitlabToken}`
});
const packageJson = files.find(
(file) => file.name === 'package.json' && file.type === 'blob'
);
const dockerfile = files.find((file) => file.name === 'Dockerfile' && file.type === 'blob');
const cargoToml = files.find((file) => file.name === 'Cargo.toml' && file.type === 'blob');
const requirementsTxt = files.find(
(file) => file.name === 'requirements.txt' && file.type === 'blob'
);
const indexHtml = files.find((file) => file.name === 'index.html' && file.type === 'blob');
const indexPHP = files.find((file) => file.name === 'index.php' && file.type === 'blob');
if (dockerfile) {
foundConfig.buildPack = 'docker';
} else if (packageJson) {
const path = packageJson.path;
const data = await get(
`${apiUrl}/v4/projects/${projectId}/repository/files/${path}/raw?ref=${branch}`,
{
Authorization: `Bearer ${$session.gitlabToken}`
}
);
const json = JSON.parse(data) || {};
foundConfig = checkTemplates({ json });
} else if (cargoToml) {
foundConfig = buildPacks.find((bp) => bp.name === 'rust');
} else if (requirementsTxt) {
foundConfig = buildPacks.find((bp) => bp.name === 'python');
} else if (indexHtml) {
foundConfig = buildPacks.find((bp) => bp.name === 'static');
} else if (indexPHP) {
foundConfig = buildPacks.find((bp) => bp.name === 'php');
}
} else if (type === 'github') {
const files = await get(`${apiUrl}/repos/${repository}/contents?ref=${branch}`, {
Authorization: `Bearer ${ghToken}`,
Accept: 'application/vnd.github.v2.json'
});
const packageJson = files.find(
(file) => file.name === 'package.json' && file.type === 'file'
);
const dockerfile = files.find((file) => file.name === 'Dockerfile' && file.type === 'file');
const cargoToml = files.find((file) => file.name === 'Cargo.toml' && file.type === 'file');
const requirementsTxt = files.find(
(file) => file.name === 'requirements.txt' && file.type === 'file'
);
const indexHtml = files.find((file) => file.name === 'index.html' && file.type === 'file');
const indexPHP = files.find((file) => file.name === 'index.php' && file.type === 'file');
if (dockerfile) {
foundConfig.buildPack = 'docker';
} else if (packageJson) {
const data = await get(`${packageJson.git_url}`, {
Authorization: `Bearer ${ghToken}`,
Accept: 'application/vnd.github.v2.raw'
});
const json = JSON.parse(data) || {};
foundConfig = checkTemplates({ json });
} else if (cargoToml) {
foundConfig = buildPacks.find((bp) => bp.name === 'rust');
} else if (requirementsTxt) {
foundConfig = buildPacks.find((bp) => bp.name === 'python');
} else if (indexHtml) {
foundConfig = buildPacks.find((bp) => bp.name === 'static');
} else if (indexPHP) {
foundConfig = buildPacks.find((bp) => bp.name === 'php');
}
}
} catch (error) {
if (
error.error === 'invalid_token' ||
error.error_description ===
'Token is expired. You can either do re-authorization or token refresh.' ||
error.message === '401 Unauthorized'
) {
if (application.gitSource.gitlabAppId) {
let htmlUrl = application.gitSource.htmlUrl;
const left = screen.width / 2 - 1020 / 2;
const top = screen.height / 2 - 618 / 2;
const newWindow = open(
`${htmlUrl}/oauth/authorize?client_id=${application.gitSource.gitlabApp.appId}&redirect_uri=${window.location.origin}/webhooks/gitlab&response_type=code&scope=api+email+read_repository&state=${$page.params.id}`,
'GitLab',
'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);
window.location.reload();
}
}, 100);
}
}
return errorNotification(error);
} finally {
if (!foundConfig) foundConfig = buildPacks.find((bp) => bp.name === 'node');
scanning = false;
}
}
onMount(async () => {
await scanRepository();
});
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Configure Build Pack</div>
</div>
{#if scanning}
<div class="flex justify-center space-x-1 p-6 font-bold">
<div class="text-xl tracking-tight">Scanning repository to suggest a build pack for you...</div>
</div>
{:else}
<div class="max-w-7xl mx-auto flex flex-wrap justify-center">
{#each buildPacks as buildPack}
<div class="p-2">
<BuildPack {buildPack} {scanning} bind:foundConfig />
</div>
{/each}
</div>
{/if}

View File

@@ -0,0 +1,17 @@
import * as db from '$lib/database';
import { PrismaErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { id } = event.params;
let { deployKeyId } = await event.request.json();
deployKeyId = Number(deployKeyId);
try {
await db.updateDeployKey({ id, deployKeyId });
return { status: 201 };
} catch (error) {
return PrismaErrorHandler(error);
}
};

View File

@@ -0,0 +1,19 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { PrismaErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
const { destinationId } = await event.request.json();
try {
await db.configureDestinationForApplication({ id, destinationId });
return { status: 201 };
} catch (error) {
return PrismaErrorHandler(error);
}
};

View File

@@ -0,0 +1,91 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, url, stuff }) => {
const { application } = stuff;
if (application?.destinationDockerId && !url.searchParams.get('from')) {
return {
status: 302,
redirect: `/applications/${params.id}`
};
}
const endpoint = `/destinations.json`;
const res = await fetch(endpoint);
if (res.ok) {
return {
props: {
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${url}`)
};
};
</script>
<script lang="ts">
import type Prisma from '@prisma/client';
import { page } from '$app/stores';
import { errorNotification } from '$lib/form';
import { goto } from '$app/navigation';
import { post } from '$lib/api';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
export let destinations: Prisma.DestinationDocker[];
async function handleSubmit(destinationId) {
try {
await post(`/applications/${id}/configuration/destination.json`, { destinationId });
return await goto(from || `/applications/${id}/configuration/buildpack`);
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Configure Destination</div>
</div>
<div class="flex justify-center">
{#if !destinations || destinations.length === 0}
<div class="flex-col">
<div class="pb-2">No configurable Destination found</div>
<div class="flex justify-center">
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
<svg
class="w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
>
</a>
</div>
</div>
{:else}
<div class="flex flex-wrap justify-center">
{#each destinations as destination}
<div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(destination.id)}>
<button type="submit" class="box-selection hover:bg-sky-700 font-bold">
<div class="font-bold text-xl text-center truncate">{destination.name}</div>
<div class="text-center truncate">{destination.network}</div>
</button>
</form>
</div>
{/each}
</div>
{/if}
</div>

View File

@@ -0,0 +1,47 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { PrismaErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
const repository = event.url.searchParams.get('repository')?.toLocaleLowerCase() || undefined;
const branch = event.url.searchParams.get('branch')?.toLocaleLowerCase() || undefined;
try {
const found = await db.isBranchAlreadyUsed({ repository, branch, id });
if (found) {
throw {
error: `Branch ${branch} is already used by another application`
};
}
return {
status: 200
};
} catch (error) {
return PrismaErrorHandler(error);
}
};
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
let { repository, branch, projectId, webhookToken } = await event.request.json();
repository = repository.toLowerCase();
branch = branch.toLowerCase();
projectId = Number(projectId);
try {
await db.configureGitRepository({ id, repository, branch, projectId, webhookToken });
return { status: 201 };
} catch (error) {
return PrismaErrorHandler(error);
}
};

View File

@@ -0,0 +1,38 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ params, url, stuff }) => {
const { application, githubToken, appId } = stuff;
if (application?.branch && application?.repository && !url.searchParams.get('from')) {
return {
status: 302,
redirect: `/applications/${params.id}`
};
}
return {
props: {
githubToken,
application,
appId
}
};
};
</script>
<script lang="ts">
export let application;
export let githubToken;
export let appId;
import GithubRepositories from './_GithubRepositories.svelte';
import GitlabRepositories from './_GitlabRepositories.svelte';
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a Repository / Project</div>
</div>
<div class="flex flex-wrap justify-center">
{#if application.gitSource.type === 'github'}
<GithubRepositories {application} {githubToken} />
{:else if application.gitSource.type === 'gitlab'}
<GitlabRepositories {application} {appId} />
{/if}
</div>

View File

@@ -0,0 +1,18 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { PrismaErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
const { gitSourceId } = await event.request.json();
try {
await db.configureGitsource({ id, gitSourceId });
return { status: 201 };
} catch (error) {
return PrismaErrorHandler(error);
}
};

View File

@@ -0,0 +1,109 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, url, stuff }) => {
const { application } = stuff;
if (application?.gitSourceId && !url.searchParams.get('from')) {
return {
status: 302,
redirect: `/applications/${params.id}`
};
}
const endpoint = `/sources.json`;
const res = await fetch(endpoint);
if (res.ok) {
return {
props: {
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${url}`)
};
};
</script>
<script lang="ts">
import type Prisma from '@prisma/client';
import { page } from '$app/stores';
import { enhance, errorNotification } from '$lib/form';
import { goto } from '$app/navigation';
import { post } from '$lib/api';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
export let sources: Prisma.GitSource[] & {
gitlabApp: Prisma.GitlabApp;
githubApp: Prisma.GithubApp;
};
sources = sources.filter(
(source) =>
(source.type === 'github' && source.githubAppId && source.githubApp.installationId) ||
(source.type === 'gitlab' && source.gitlabAppId)
);
async function handleSubmit(gitSourceId) {
try {
await post(`/applications/${id}/configuration/source.json`, { gitSourceId });
return await goto(from || `/applications/${id}/configuration/repository`);
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a Git Source</div>
</div>
<div class="flex justify-center">
{#if !sources || sources.length === 0}
<div class="flex-col">
<div class="pb-2">No configurable Git Source found</div>
<div class="flex justify-center">
<a href="/new/source" sveltekit:prefetch class="add-icon bg-orange-600 hover:bg-orange-500">
<svg
class="w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
>
</a>
</div>
</div>
{:else}
<div class="flex flex-wrap justify-center">
{#each sources as source}
<div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(source.id)}>
<button
disabled={source.gitlabApp && !source.gitlabAppId}
type="submit"
class="disabled:opacity-95 bg-coolgray-200 disabled:text-white box-selection hover:bg-orange-700 group"
class:border-red-500={source.gitlabApp && !source.gitlabAppId}
class:border-0={source.gitlabApp && !source.gitlabAppId}
class:border-l-4={source.gitlabApp && !source.gitlabAppId}
>
<div class="font-bold text-xl text-center truncate">{source.name}</div>
{#if source.gitlabApp && !source.gitlabAppId}
<div class="font-bold text-center truncate text-red-500 group-hover:text-white">
Configuration missing
</div>
{/if}
</button>
</form>
</div>
{/each}
</div>
{/if}
</div>

View File

@@ -0,0 +1,20 @@
import * as db from '$lib/database';
import { PrismaErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => {
const { id } = event.params;
try {
return await db.getSshKey({ id });
} catch (error) {
return PrismaErrorHandler(error);
}
};
export const post: RequestHandler = async (event) => {
const { id } = event.params;
try {
return await db.generateSshKey({ id });
} catch (error) {
return PrismaErrorHandler(error);
}
};

View File

@@ -0,0 +1,19 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { PrismaErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const del: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
try {
await db.removeApplication({ id, teamId });
return {
status: 200
};
} catch (error) {
return PrismaErrorHandler(error);
}
};

View File

@@ -0,0 +1,42 @@
import * as db from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
import cuid from 'cuid';
import crypto from 'crypto';
import { buildQueue } from '$lib/queues';
import { getUserDetails } from '$lib/common';
import { PrismaErrorHandler } from '$lib/database';
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const buildId = cuid();
const applicationFound = await db.getApplication({ id, teamId });
if (!applicationFound.configHash) {
const configHash = crypto
.createHash('sha256')
.update(
JSON.stringify({
buildPack: applicationFound.buildPack,
port: applicationFound.port,
installCommand: applicationFound.installCommand,
buildCommand: applicationFound.buildCommand,
startCommand: applicationFound.startCommand
})
)
.digest('hex');
await db.prisma.application.update({ where: { id }, data: { configHash } });
}
await buildQueue.add(buildId, { build_id: buildId, type: 'manual', ...applicationFound });
return {
status: 200,
body: {
buildId
}
};
} catch (error) {
return PrismaErrorHandler(error);
}
};

View File

@@ -0,0 +1,87 @@
import { getTeam, getUserDetails } from '$lib/common';
import { getGithubToken } from '$lib/components/common';
import * as db from '$lib/database';
import { PrismaErrorHandler } from '$lib/database';
import { checkContainer } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit';
import jsonwebtoken from 'jsonwebtoken';
export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const appId = process.env['COOLIFY_APP_ID'];
let githubToken = null;
let ghToken = null;
let isRunning = false;
const { id } = event.params;
try {
const application = await db.getApplication({ id, teamId });
const { gitSource } = application;
if (gitSource?.type === 'github' && gitSource?.githubApp) {
const payload = {
iat: Math.round(new Date().getTime() / 1000),
exp: Math.round(new Date().getTime() / 1000 + 60),
iss: gitSource.githubApp.appId
};
githubToken = jsonwebtoken.sign(payload, gitSource.githubApp.privateKey, {
algorithm: 'RS256'
});
ghToken = await getGithubToken({ apiUrl: gitSource.apiUrl, application, githubToken });
}
if (application.destinationDockerId) {
isRunning = await checkContainer(application.destinationDocker.engine, id);
}
return {
body: {
isRunning,
ghToken,
githubToken,
application,
appId
}
};
} catch (error) {
console.log(error);
return PrismaErrorHandler(error);
}
};
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
let {
name,
buildPack,
fqdn,
port,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory
} = await event.request.json();
if (port) port = Number(port);
try {
await db.configureApplication({
id,
buildPack,
name,
fqdn,
port,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory
});
return { status: 201 };
} catch (error) {
return PrismaErrorHandler(error);
}
};

View File

@@ -0,0 +1,390 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, stuff }) => {
if (stuff?.application?.id) {
return {
props: {
application: stuff.application,
isRunning: stuff.isRunning
}
};
}
const endpoint = `/applications/${params.id}.json`;
const res = await fetch(endpoint);
if (res.ok) {
return {
props: {
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${endpoint}`)
};
};
</script>
<script lang="ts">
export let application: Prisma.Application & {
settings: Prisma.ApplicationSettings;
gitlabApp: Prisma.GitlabApp;
gitSource: Prisma.GitSource;
destinationDocker: Prisma.DestinationDocker;
};
export let isRunning;
import { page, session } from '$app/stores';
import { errorNotification } from '$lib/form';
import { onMount } from 'svelte';
import Explainer from '$lib/components/Explainer.svelte';
import Setting from '$lib/components/Setting.svelte';
import type Prisma from '@prisma/client';
import { getDomain, notNodeDeployments, staticDeployments } from '$lib/components/common';
import { toast } from '@zerodevx/svelte-toast';
import { post } from '$lib/api';
const { id } = $page.params;
let domainEl: HTMLInputElement;
let loading = false;
let debug = application.settings.debug;
let previews = application.settings.previews;
onMount(() => {
domainEl.focus();
});
async function changeSettings(name) {
if (name === 'debug') {
debug = !debug;
}
if (name === 'previews') {
previews = !previews;
}
try {
await post(`/applications/${id}/settings.json`, { previews, debug });
return toast.push('Settings saved.');
} catch ({ error }) {
return errorNotification(error);
}
}
async function handleSubmit() {
loading = true;
try {
await post(`/applications/${id}/check.json`, { fqdn: application.fqdn });
await post(`/applications/${id}.json`, { ...application });
return window.location.reload();
} catch ({ error }) {
return errorNotification(error);
} finally {
loading = false;
}
}
</script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
{application.name}
</div>
{#if application.fqdn}
<a
href={application.fqdn}
target="_blank"
class="icons tooltip-bottom flex items-center bg-transparent text-sm"
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
>
{/if}
<a
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
</div>
<div class="mx-auto max-w-4xl px-6">
<!-- svelte-ignore missing-declaration -->
<form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-5 font-bold">
<div class="title">General</div>
{#if $session.isAdmin}
<button
type="submit"
class:bg-green-600={!loading}
class:hover:bg-green-500={!loading}
disabled={loading}>{loading ? 'Saving...' : 'Save'}</button
>
{/if}
</div>
<div class="grid grid-flow-row gap-2 px-10">
<div class="mt-2 grid grid-cols-3 items-center">
<label for="name">Name</label>
<div class="col-span-2 ">
<input
readonly={!$session.isAdmin}
name="name"
id="name"
bind:value={application.name}
required
/>
</div>
</div>
<div class="grid grid-cols-3 items-center">
<label for="gitSource">Git Source</label>
<div class="col-span-2">
<a
href={$session.isAdmin
? `/applications/${id}/configuration/source?from=/applications/${id}`
: ''}
class="no-underline"
><input
value={application.gitSource.name}
id="gitSource"
disabled
class="cursor-pointer bg-coolgray-200 hover:bg-coolgray-500"
/></a
>
</div>
</div>
<div class="grid grid-cols-3 items-center">
<label for="repository">Git Repository</label>
<div class="col-span-2">
<a
href={$session.isAdmin
? `/applications/${id}/configuration/repository?from=/applications/${id}&to=/applications/${id}/configuration/buildpack`
: ''}
class="no-underline"
><input
value="{application.repository}/{application.branch}"
id="repository"
disabled
class="cursor-pointer bg-coolgray-200 hover:bg-coolgray-500"
/></a
>
</div>
</div>
<div class="grid grid-cols-3 items-center">
<label for="buildPack">Build Pack</label>
<div class="col-span-2">
<a
href={$session.isAdmin
? `/applications/${id}/configuration/buildpack?from=/applications/${id}`
: ''}
class="no-underline "
>
<input
value={application.buildPack}
id="buildPack"
disabled
class="cursor-pointer bg-coolgray-200 hover:bg-coolgray-500"
/></a
>
</div>
</div>
<div class="grid grid-cols-3 items-center pb-8">
<label for="destination">Destination</label>
<div class="col-span-2">
<div class="no-underline">
<input
value={application.destinationDocker.name}
id="destination"
disabled
class="bg-transparent "
/>
</div>
</div>
</div>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Application</div>
</div>
<div class="grid grid-flow-row gap-2 px-10">
<div class="grid grid-cols-3">
<label for="fqdn" class="pt-2">Domain (FQDN)</label>
<div class="col-span-2">
<input
readonly={!$session.isAdmin || isRunning}
disabled={!$session.isAdmin || isRunning}
bind:this={domainEl}
name="fqdn"
id="fqdn"
bind:value={application.fqdn}
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="eg: https://coollabs.io"
required
/>
<Explainer
text="If you specify <span class='text-green-600 font-bold'>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>To modify the domain, you must first stop the application."
/>
</div>
</div>
{#if !staticDeployments.includes(application.buildPack)}
<div class="grid grid-cols-3 items-center">
<label for="port">Port</label>
<div class="col-span-2">
<input
readonly={!$session.isAdmin}
name="port"
id="port"
bind:value={application.port}
placeholder="default: 3000"
/>
</div>
</div>
{/if}
{#if !notNodeDeployments.includes(application.buildPack)}
<div class="grid grid-cols-3 items-center">
<label for="installCommand">Install Command</label>
<div class="col-span-2">
<input
readonly={!$session.isAdmin}
name="installCommand"
id="installCommand"
bind:value={application.installCommand}
placeholder="default: yarn install"
/>
</div>
</div>
<div class="grid grid-cols-3 items-center">
<label for="buildCommand">Build Command</label>
<div class="col-span-2">
<input
readonly={!$session.isAdmin}
name="buildCommand"
id="buildCommand"
bind:value={application.buildCommand}
placeholder="default: yarn build"
/>
</div>
</div>
<div class="grid grid-cols-3 items-center">
<label for="startCommand" class="">Start Command</label>
<div class="col-span-2">
<input
readonly={!$session.isAdmin}
name="startCommand"
id="startCommand"
bind:value={application.startCommand}
placeholder="default: yarn start"
/>
</div>
</div>
{/if}
<div class="grid grid-cols-3">
<label for="baseDirectory" class="pt-2">Base Directory</label>
<div class="col-span-2">
<input
readonly={!$session.isAdmin}
name="baseDirectory"
id="baseDirectory"
bind:value={application.baseDirectory}
placeholder="default: /"
/>
<Explainer
text="Directory to use as the base of all commands. <br> Could be useful with monorepos."
/>
</div>
</div>
{#if !notNodeDeployments.includes(application.buildPack)}
<div class="grid grid-cols-3">
<label for="publishDirectory" class="pt-2">Publish Directory</label>
<div class="col-span-2">
<input
readonly={!$session.isAdmin}
name="publishDirectory"
id="publishDirectory"
bind:value={application.publishDirectory}
placeholder=" default: /"
/>
<Explainer
text="Directory containing all the assets for deployment. <br> For example: <span class='text-green-600 font-bold'>dist</span>,<span class='text-green-600 font-bold'>_site</span> or <span class='text-green-600 font-bold'>public</span>."
/>
</div>
</div>
{/if}
</div>
</form>
<div class="flex space-x-1 pb-5 font-bold">
<div class="title">Features</div>
</div>
<div class="px-4 pb-10 sm:px-6">
<!-- <ul class="mt-2 divide-y divide-stone-800">
<Setting
bind:setting={forceSSL}
on:click={() => changeSettings('forceSSL')}
title="Force https"
description="Creates a https redirect for all requests from http and also generates a https certificate for the domain through Let's Encrypt."
/>
</ul> -->
<ul class="mt-2 divide-y divide-stone-800">
<Setting
bind:setting={previews}
on:click={() => changeSettings('previews')}
title="Enable MR/PR Previews"
description="Creates previews from pull and merge requests."
/>
</ul>
<ul class="mt-2 divide-y divide-stone-800">
<Setting
bind:setting={debug}
on:click={() => changeSettings('debug')}
title="Debug Logs"
description="Enable debug logs during build phase. <br>(<span class='text-red-500'>sensitive information</span> could be visible in logs)"
/>
</ul>
</div>
</div>

View File

@@ -0,0 +1,41 @@
<div class="lds-ripple absolute left-0">
<div />
<div />
</div>
<style>
.lds-ripple {
display: inline-block;
position: relative;
left: -19px;
top: -8px;
width: 40px;
height: 40px;
}
.lds-ripple div {
position: absolute;
border: 4px solid #fff;
opacity: 1;
border-radius: 50%;
animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.lds-ripple div:nth-child(2) {
animation-delay: -0.5s;
}
@keyframes lds-ripple {
0% {
top: 1px;
left: 1px;
width: 0;
height: 0;
opacity: 1;
}
100% {
top: 0px;
left: 0px;
width: 36px;
height: 36px;
opacity: 0;
}
}
</style>

View File

@@ -0,0 +1,75 @@
<script lang="ts">
export let buildId;
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
const dispatch = createEventDispatcher();
import { page } from '$app/stores';
import Loading from '$lib/components/Loading.svelte';
import LoadingLogs from '../_Loading.svelte';
import { get } from '$lib/api';
import { errorNotification } from '$lib/form';
let logs = [];
let loading = true;
let currentStatus;
let streamInterval;
const { id } = $page.params;
async function streamLogs(sequence = 0) {
try {
let { logs: responseLogs, status } = await get(
`/applications/${id}/logs/build/build.json?buildId=${buildId}&sequence=${sequence}`
);
currentStatus = status;
logs = logs.concat(responseLogs);
loading = false;
streamInterval = setInterval(async () => {
if (status !== 'running') {
clearInterval(streamInterval);
return;
}
const nextSequence = logs[logs.length - 1].time;
try {
const data = await get(
`/applications/${id}/logs/build/build.json?buildId=${buildId}&sequence=${nextSequence}`
);
status = data.status;
currentStatus = status;
logs = logs.concat(data.logs);
dispatch('updateBuildStatus', { status });
} catch ({ error }) {
return errorNotification(error);
}
}, 1000);
} catch ({ error }) {
return errorNotification(error);
}
}
onDestroy(() => {
clearInterval(streamInterval);
});
onMount(async () => {
window.scrollTo(0, 0);
await streamLogs();
});
</script>
{#if loading}
<Loading />
{:else}
<div class="relative">
{#if currentStatus === 'running'}
<LoadingLogs />
{/if}
<div
class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words"
>
{#each logs as log}
<div>{log.line + '\n'}</div>
{/each}
</div>
</div>
{/if}

View File

@@ -0,0 +1,28 @@
import { getTeam, getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { PrismaErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const buildId = event.url.searchParams.get('buildId');
const sequence = Number(event.url.searchParams.get('sequence'));
try {
let logs = await db.prisma.buildLog.findMany({
where: { buildId, time: { gt: sequence } },
orderBy: { time: 'asc' }
});
const { status } = await db.prisma.build.findFirst({ where: { id: buildId } });
return {
body: {
logs,
status
}
};
} catch (error) {
return PrismaErrorHandler(error);
}
};

View File

@@ -0,0 +1,40 @@
import * as db from '$lib/database';
import { PrismaErrorHandler } from '$lib/database';
import { dayjs } from '$lib/dayjs';
import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => {
const { id } = event.params;
const buildId = event.url.searchParams.get('buildId');
const skip = Number(event.url.searchParams.get('skip')) || 0;
let builds = [];
try {
const buildCount = await db.prisma.build.count({ where: { applicationId: id } });
if (buildId) {
builds = await db.prisma.build.findMany({ where: { applicationId: id, id: buildId } });
} else {
builds = await db.prisma.build.findMany({
where: { applicationId: id },
orderBy: { createdAt: 'desc' },
take: 5,
skip
});
}
builds = builds.map((build) => {
const updatedAt = dayjs(build.updatedAt).utc();
build.took = updatedAt.diff(dayjs(build.createdAt)) / 1000;
build.since = updatedAt.fromNow();
return build;
});
return {
status: 200,
body: {
builds,
buildCount
}
};
} catch (error) {
return PrismaErrorHandler(error);
}
};

View File

@@ -0,0 +1,143 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, url, stuff }) => {
let endpoint = `/applications/${params.id}/logs/build.json?skip=0`;
const res = await fetch(endpoint);
if (res.ok) {
return {
props: {
application: stuff.application,
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${url}`)
};
};
</script>
<script lang="ts">
import { page } from '$app/stores';
import { dateOptions, getDomain } from '$lib/components/common';
import BuildLog from './_BuildLog.svelte';
import { get } from '$lib/api';
import { errorNotification } from '$lib/form';
import { goto } from '$app/navigation';
export let builds;
export let application;
export let buildCount;
let buildId;
$: buildId;
let skip = 0;
let noMoreBuilds = buildCount < 5 || buildCount <= skip;
const { id } = $page.params;
let preselectedBuildId = $page.url.searchParams.get('buildId');
if (preselectedBuildId) buildId = preselectedBuildId;
async function updateBuildStatus({ detail }) {
const { status } = detail;
if (status !== 'running') {
try {
const data = await get(`/applications/${id}/logs/build.json?buildId=${buildId}`);
builds = builds.filter((build) => {
if (build.id === data.builds[0].id) {
build.status = data.builds[0].status;
build.took = data.builds[0].took;
build.since = data.builds[0].since;
}
window.location.reload();
return build;
});
return;
} catch ({ error }) {
return errorNotification(error);
}
} else {
builds = builds.filter((build) => {
if (build.id === buildId) build.status = status;
return build;
});
}
}
async function loadMoreBuilds() {
if (buildCount >= skip) {
skip = skip + 5;
noMoreBuilds = buildCount >= skip;
try {
const data = await get(`/applications/${id}/logs/build.json?skip=${skip}`);
builds = builds.concat(data.builds);
return;
} catch ({ error }) {
return errorNotification(error);
}
} else {
noMoreBuilds = true;
}
}
async function loadBuild(build) {
buildId = build;
goto(`/applications/${id}/logs/build?buildId=${buildId}`);
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">
Build logs of <a href={application.fqdn} target="_blank">{getDomain(application.fqdn)}</a>
</div>
</div>
<div class="flex flex-row justify-start space-x-2 px-10 pt-6 ">
<div class="min-w-[16rem] space-y-2">
{#each builds as build (build.id)}
<div
data-tooltip={new Intl.DateTimeFormat('default', dateOptions).format(
new Date(build.createdAt)
) + `\n${build.status}`}
on:click={() => loadBuild(build.id)}
class="tooltip-top flex cursor-pointer items-center justify-center rounded-r border-l-2 border-transparent py-4 no-underline transition-all duration-100 hover:bg-coolgray-400 hover:shadow-xl"
class:bg-coolgray-400={buildId === build.id}
class:border-red-500={build.status === 'failed'}
class:border-green-500={build.status === 'success'}
class:border-yellow-500={build.status === 'inprogress'}
>
<div class="flex-col px-2">
<div class="text-sm font-bold">
{application.branch}
</div>
<div class="text-xs">
{build.type}
</div>
</div>
<div class="flex-1" />
<div class="w-48 text-center text-xs">
{#if build.status === 'running'}
<div class="font-bold">Running</div>
{:else}
<div>{build.since}</div>
<div>Finished in <span class="font-bold">{build.took}s</span></div>
{/if}
</div>
</div>
{/each}
{#if buildCount > 0 && !noMoreBuilds}
<button class="w-full" on:click={loadMoreBuilds}>Load More</button>
{/if}
</div>
<div class="w-96 flex-1">
{#if buildId}
{#key buildId}
<svelte:component this={BuildLog} {buildId} on:updateBuildStatus={updateBuildStatus} />
{/key}
{/if}
</div>
</div>
{#if buildCount === 0}
<div class="text-center text-xl font-bold">No logs found</div>
{/if}

View File

@@ -0,0 +1,53 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { PrismaErrorHandler } from '$lib/database';
import { dayjs } from '$lib/dayjs';
import { dockerInstance } from '$lib/docker';
import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const { destinationDockerId, destinationDocker } = await db.prisma.application.findUnique({
where: { id },
include: { destinationDocker: true }
});
if (destinationDockerId) {
const docker = dockerInstance({ destinationDocker });
try {
const container = await docker.engine.getContainer(id);
if (container) {
return {
body: {
logs: (await container.logs({ stdout: true, stderr: true, timestamps: true }))
.toString()
.split('\n')
.map((l) => l.slice(8))
.filter((a) => a)
}
};
}
} catch (error) {
const { statusCode } = error;
if (statusCode === 404) {
return {
body: {
logs: []
}
};
}
}
}
return {
status: 200,
body: {
message: 'No logs found.'
}
};
} catch (error) {
return PrismaErrorHandler(error);
}
};

View File

@@ -0,0 +1,75 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
import { onDestroy, onMount } from 'svelte';
export const load: Load = async ({ fetch, params, url, stuff }) => {
let endpoint = `/applications/${params.id}/logs.json`;
const res = await fetch(endpoint);
if (res.ok) {
return {
props: {
application: stuff.application,
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${url}`)
};
};
</script>
<script lang="ts">
export let application;
import { page } from '$app/stores';
import LoadingLogs from './_Loading.svelte';
import { getDomain } from '$lib/components/common';
import { get } from '$lib/api';
import { errorNotification } from '$lib/form';
let loadLogsInterval = null;
let logs = [];
const { id } = $page.params;
onMount(async () => {
loadLogs();
loadLogsInterval = setInterval(() => {
loadLogs();
}, 3000);
});
onDestroy(() => {
clearInterval(loadLogsInterval);
});
async function loadLogs() {
try {
const newLogs = await get(`/applications/${id}/logs.json`);
logs = newLogs.logs;
return;
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">
Application logs of <a href={application.fqdn} target="_blank">{getDomain(application.fqdn)}</a>
</div>
</div>
<div class="flex flex-row justify-center space-x-2 px-10 pt-6">
{#if logs.length === 0}
<div class="text-xl font-bold tracking-tighter">Waiting for the logs...</div>
{:else}
<div class="relative w-full">
<LoadingLogs />
<div
class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 p-6 whitespace-pre-wrap break-words w-full"
>
{#each logs as log}
{log + '\n'}
{/each}
</div>
</div>
{/if}
</div>

View File

@@ -0,0 +1,44 @@
import { getTeam, getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { PrismaErrorHandler } from '$lib/database';
import { dockerInstance } from '$lib/docker';
import type { RequestHandler } from '@sveltejs/kit';
import jsonwebtoken from 'jsonwebtoken';
export const get: RequestHandler = async (event) => {
const { status, body, teamId } = await getUserDetails(event, false);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const destinationDocker = await db.getDestinationByApplicationId({ id, teamId });
const docker = dockerInstance({ destinationDocker });
const listContainers = await docker.engine.listContainers({
filters: { network: [destinationDocker.network] }
});
const containers = listContainers.filter((container) => {
return (
container.Labels['coolify.configuration'] &&
container.Labels['coolify.type'] === 'standalone-application'
);
});
const jsonContainers = containers
.map((container) =>
JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString())
)
.filter((container) => {
return (
container.type !== 'manual' &&
container.type !== 'webhook_commit' &&
container.applicationId === id
);
});
return {
body: {
containers: jsonContainers
}
};
} catch (error) {
return PrismaErrorHandler(error);
}
};

View File

@@ -0,0 +1,51 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, stuff }) => {
let endpoint = `/applications/${params.id}/previews.json`;
const res = await fetch(endpoint);
if (res.ok) {
return {
props: {
application: stuff.application,
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${endpoint}`)
};
};
</script>
<script lang="ts">
export let containers;
export let application;
import { getDomain } from '$lib/components/common';
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">
Previews for <a href={application.fqdn} target="_blank">{getDomain(application.fqdn)}</a>
</div>
</div>
<div class="mx-auto max-w-4xl px-6">
<div class="flex flex-wrap justify-center space-x-2">
{#if containers.length > 0}
{#each containers as container}
<a href={container.fqdn} class="p-2 no-underline" target="_blank">
<div class="box-selection text-center hover:border-transparent hover:bg-coolgray-200">
<div class="truncate text-center text-xl font-bold">{getDomain(container.fqdn)}</div>
</div>
</a>
{/each}
{:else}
<div class="flex-col">
<div class="text-center font-bold text-xl">No previews available</div>
</div>
{/if}
</div>
</div>

View File

@@ -0,0 +1,133 @@
<script>
export let name = '';
export let value = '';
export let isBuildSecret = false;
export let isNewSecret = false;
import { page } from '$app/stores';
import { del, post } from '$lib/api';
import { errorNotification } from '$lib/form';
if (name) value = 'ENCRYPTED';
const { id } = $page.params;
async function removeSecret() {
try {
await del(`/applications/${id}/secrets.json`, { name });
return window.location.reload();
} catch ({ error }) {
return errorNotification(error);
}
}
async function saveSecret() {
try {
await post(`/applications/${id}/secrets.json`, { name, value, isBuildSecret });
return window.location.reload();
} catch ({ error }) {
return errorNotification(error);
}
}
function setSecretValue() {
if (isNewSecret) {
isBuildSecret = !isBuildSecret;
}
}
</script>
<div class="mx-auto max-w-3xl pt-4">
<div class="flex space-x-2">
<div class="grid grid-flow-row">
<label for="secretName">Name</label>
<input
id="secretName"
bind:value={name}
placeholder="EXAMPLE_VARIABLE"
class="w-64 border-2 border-transparent"
readonly={!isNewSecret}
class:hover:bg-coolgray-200={!isNewSecret}
class:cursor-not-allowed={!isNewSecret}
/>
</div>
<div class="grid grid-flow-row">
<label for="secretValue">Value (will be encrypted)</label>
<input
id="secretValue"
bind:value
placeholder="J$#@UIO%HO#$U%H"
class="w-64 border-2 border-transparent"
class:hover:bg-coolgray-200={!isNewSecret}
class:cursor-not-allowed={!isNewSecret}
readonly={!isNewSecret}
/>
</div>
<div class="w-32 px-2 text-center">
<div class="text-xs">Is build variable?</div>
<div class="mt-2">
<ul class="divide-y divide-stone-800">
<li>
<div
type="button"
on:click={setSecretValue}
aria-pressed="false"
class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out"
class:bg-green-600={isBuildSecret}
class:bg-stone-700={!isBuildSecret}
class:cursor-not-allowed={!isNewSecret}
class:cursor-pointer={isNewSecret}
>
<span class="sr-only">Use isBuildSecret</span>
<span
class="pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow transition duration-200 ease-in-out"
class:translate-x-5={isBuildSecret}
class:translate-x-0={!isBuildSecret}
>
<span
class=" absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
class:opacity-0={isBuildSecret}
class:opacity-100={!isBuildSecret}
aria-hidden="true"
>
<svg class="h-3 w-3 bg-white text-red-600" fill="none" viewBox="0 0 12 12">
<path
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</span>
<span
class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-100 ease-out"
aria-hidden="true"
class:opacity-100={isBuildSecret}
class:opacity-0={!isBuildSecret}
>
<svg
class="h-3 w-3 bg-white text-green-600"
fill="currentColor"
viewBox="0 0 12 12"
>
<path
d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"
/>
</svg>
</span>
</span>
</div>
</li>
</ul>
</div>
</div>
{#if isNewSecret}
<div class="mt-6">
<button class="w-20 bg-green-600 hover:bg-green-500" on:click={saveSecret}>Add</button>
</div>
{:else}
<div class="mt-6">
<button class="w-20 bg-red-600 hover:bg-red-500" on:click={removeSecret}>Remove</button>
</div>
{/if}
</div>
</div>

View File

@@ -0,0 +1,63 @@
import { getTeam, getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { PrismaErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
try {
const secrets = await db.listSecrets({ applicationId: event.params.id });
return {
status: 200,
body: {
secrets: secrets.sort((a, b) => {
return ('' + a.name).localeCompare(b.name);
})
}
};
} catch (error) {
return PrismaErrorHandler(error);
}
};
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
const { name, value, isBuildSecret } = await event.request.json();
try {
const found = await db.isSecretExists({ id, name });
if (found) {
throw {
error: `Secret ${name} already exists`
};
} else {
await db.createSecret({ id, name, value, isBuildSecret });
return {
status: 201
};
}
} catch (error) {
return PrismaErrorHandler(error);
}
};
export const del: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
const { name } = await event.request.json();
try {
await db.removeSecret({ id, name });
return {
status: 200
};
} catch (error) {
return PrismaErrorHandler(error);
}
};

View File

@@ -0,0 +1,41 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, stuff }) => {
let endpoint = `/applications/${params.id}/secrets.json`;
const res = await fetch(endpoint);
if (res.ok) {
return {
props: {
application: stuff.application,
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${endpoint}`)
};
};
</script>
<script lang="ts">
export let secrets;
export let application;
import Secret from './_Secret.svelte';
import { getDomain } from '$lib/components/common';
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">
Secrets for <a href={application.fqdn} target="_blank">{getDomain(application.fqdn)}</a>
</div>
</div>
<div class="mx-auto max-w-4xl px-6">
<div class="flex-col justify-start space-y-1">
{#each secrets as secret}
<Secret name={secret.name} value={secret.value} isBuildSecret={secret.isBuildSecret} />
{/each}
<Secret isNewSecret />
</div>
</div>

View File

@@ -0,0 +1,19 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { PrismaErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
const { debug, previews } = await event.request.json();
try {
await db.setApplicationSettings({ id, debug, previews });
return { status: 201 };
} catch (error) {
return PrismaErrorHandler(error);
}
};

View File

@@ -0,0 +1,31 @@
import { getDomain, getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { PrismaErrorHandler } from '$lib/database';
import { dockerInstance } from '$lib/docker';
import { removeProxyConfiguration } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const { destinationDocker, destinationDockerId, fqdn } = await db.getApplication({
id,
teamId
});
const domain = getDomain(fqdn);
if (destinationDockerId) {
const docker = dockerInstance({ destinationDocker });
await docker.engine.getContainer(id).stop();
}
await removeProxyConfiguration({ domain });
return {
status: 200
};
} catch (error) {
return PrismaErrorHandler(error);
}
};

View File

@@ -0,0 +1,60 @@
<script lang="ts">
export let application;
import Rust from '$lib/components/svg/applications/Rust.svelte';
import Nodejs from '$lib/components/svg/applications/Nodejs.svelte';
import React from '$lib/components/svg/applications/React.svelte';
import Svelte from '$lib/components/svg/applications/Svelte.svelte';
import Vuejs from '$lib/components/svg/applications/Vuejs.svelte';
import PHP from '$lib/components/svg/applications/PHP.svelte';
import Python from '$lib/components/svg/applications/Python.svelte';
import Static from '$lib/components/svg/applications/Static.svelte';
import Nestjs from '$lib/components/svg/applications/Nestjs.svelte';
import Nuxtjs from '$lib/components/svg/applications/Nuxtjs.svelte';
import Nextjs from '$lib/components/svg/applications/Nextjs.svelte';
import Gatsby from '$lib/components/svg/applications/Gatsby.svelte';
import Docker from '$lib/components/svg/applications/Docker.svelte';
const buildPack = application?.buildPack?.toLowerCase();
</script>
<a href="/applications/{application.id}" class="w-96 p-2 no-underline">
<div class="box-selection group relative hover:bg-green-600">
{#if buildPack === 'rust'}
<Rust />
{:else if buildPack === 'node'}
<Nodejs />
{:else if buildPack === 'react'}
<React />
{:else if buildPack === 'svelte'}
<Svelte />
{:else if buildPack === 'vuejs'}
<Vuejs />
{:else if buildPack === 'php'}
<PHP />
{:else if buildPack === 'python'}
<Python />
{:else if buildPack === 'static'}
<Static />
{:else if buildPack === 'nestjs'}
<Nestjs />
{:else if buildPack === 'nuxtjs'}
<Nuxtjs />
{:else if buildPack === 'nextjs'}
<Nextjs />
{:else if buildPack === 'gatsby'}
<Gatsby />
{:else if buildPack === 'docker'}
<Docker />
{/if}
<div class="truncate text-center text-xl font-bold">{application.name}</div>
{#if application.fqdn}
<div class="truncate text-center">{application.fqdn}</div>
{/if}
{#if !application.gitSourceId || !application.destinationDockerId}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Configuration missing
</div>
{/if}
</div>
</a>

View File

@@ -0,0 +1,21 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { PrismaErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
try {
const applications = await db.listApplications(teamId);
return {
status: 200,
body: {
applications
}
};
} catch (error) {
return PrismaErrorHandler(error);
}
};

View File

@@ -0,0 +1,58 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch }) => {
const endpoint = '/applications.json';
const res = await fetch(endpoint);
if (res.ok) {
return {
props: {
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${endpoint}`)
};
};
</script>
<script lang="ts">
export let applications: Array<Applications>;
import { session } from '$app/stores';
import Application from './_Application.svelte';
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl ">Applications</div>
{#if $session.isAdmin}
<a href="/new/application" class="add-icon bg-green-600 hover:bg-green-500">
<svg
class="w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
>
</a>
{/if}
</div>
<div class="flex flex-wrap justify-center">
{#if !applications || applications.length === 0}
<div class="flex-col">
<div class="text-center text-xl font-bold">No applications found</div>
</div>
{:else}
{#each applications as application}
<Application {application} />
{/each}
{/if}
</div>