31
apps/ui/src/routes/__error.svelte
Normal file
31
apps/ui/src/routes/__error.svelte
Normal file
@@ -0,0 +1,31 @@
|
||||
<script context="module" lang="ts">
|
||||
export function load({ error, status }: { error: any; status: any }) {
|
||||
console.log(error);
|
||||
return {
|
||||
props: {
|
||||
error,
|
||||
status
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/translations';
|
||||
|
||||
export let status: string;
|
||||
export let error: any;
|
||||
</script>
|
||||
|
||||
<div class="mx-auto flex h-screen flex-col items-center justify-center px-4">
|
||||
<div class="pb-10 text-7xl font-bold">{status}</div>
|
||||
<div class="text-3xl font-bold">{$t('error.you_are_lost')}</div>
|
||||
<div class="text-xl">
|
||||
{$t('error.you_can_find_your_way_back')}
|
||||
<a href="/" class="font-bold uppercase text-sky-400">{$t('error.here')}</a>
|
||||
</div>
|
||||
<div class="py-10 text-xs font-bold">
|
||||
<pre
|
||||
class="w-full whitespace-pre-wrap break-words text-left text-xs tracking-tighter">{error.message} {error.stack}</pre>
|
||||
</div>
|
||||
</div>
|
||||
373
apps/ui/src/routes/__layout.svelte
Normal file
373
apps/ui/src/routes/__layout.svelte
Normal file
@@ -0,0 +1,373 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ url }) => {
|
||||
try {
|
||||
if (Cookies.get('token')) {
|
||||
const response = await get(`/user`);
|
||||
return {
|
||||
props: {
|
||||
...response
|
||||
},
|
||||
stuff: {
|
||||
...response
|
||||
}
|
||||
};
|
||||
} else {
|
||||
if (url.pathname !== '/login' && url.pathname !== '/register') {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/login'
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error?.code?.startsWith('FAST_JWT') || error.status === 401) {
|
||||
Cookies.remove('token');
|
||||
if (url.pathname !== '/login') {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/login'
|
||||
};
|
||||
}
|
||||
}
|
||||
if (url.pathname !== '/login') {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/login'
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(error)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let settings: any;
|
||||
export let userId: string;
|
||||
export let teamId: string;
|
||||
export let permission: string;
|
||||
export let isAdmin: boolean;
|
||||
import '../tailwind.css';
|
||||
import Cookies from 'js-cookie';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import { SvelteToast } from '@zerodevx/svelte-toast';
|
||||
import { navigating, page } from '$app/stores';
|
||||
|
||||
import { get } from '$lib/api';
|
||||
import UpdateAvailable from '$lib/components/UpdateAvailable.svelte';
|
||||
import PageLoader from '$lib/components/PageLoader.svelte';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { appSession, isTraefikUsed } from '$lib/store';
|
||||
|
||||
if (userId) $appSession.userId = userId;
|
||||
if (teamId) $appSession.teamId = teamId;
|
||||
if (permission) $appSession.permission = permission;
|
||||
if (isAdmin) $appSession.isAdmin = isAdmin;
|
||||
|
||||
$isTraefikUsed = settings?.isTraefikUsed || false;
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
Cookies.remove('token');
|
||||
return window.location.replace('/login');
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Coolify</title>
|
||||
{#if !$appSession.whiteLabeled}
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
{:else if $appSession.whiteLabeledDetails.icon}
|
||||
<link rel="icon" href={$appSession.whiteLabeledDetails.icon} />
|
||||
{/if}
|
||||
</svelte:head>
|
||||
<SvelteToast options={{ intro: { y: -64 }, duration: 3000, pausable: true }} />
|
||||
{#if $navigating}
|
||||
<div out:fade={{ delay: 100 }}>
|
||||
<PageLoader />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $appSession.userId}
|
||||
<nav class="nav-main">
|
||||
<div class="flex h-screen w-full flex-col items-center transition-all duration-100">
|
||||
{#if !$appSession.whiteLabeled}
|
||||
<div class="my-4 h-10 w-10"><img src="/favicon.png" alt="coolLabs logo" /></div>
|
||||
{/if}
|
||||
<div class="flex flex-col space-y-2 py-2" class:mt-2={$appSession.whiteLabeled}>
|
||||
<a
|
||||
sveltekit:prefetch
|
||||
href="/"
|
||||
class="icons tooltip-right bg-coolgray-200 hover:text-white"
|
||||
class:text-white={$page.url.pathname === '/'}
|
||||
class:bg-coolgray-500={$page.url.pathname === '/'}
|
||||
data-tooltip="Dashboard"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8"
|
||||
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="M19 8.71l-5.333 -4.148a2.666 2.666 0 0 0 -3.274 0l-5.334 4.148a2.665 2.665 0 0 0 -1.029 2.105v7.2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-7.2c0 -.823 -.38 -1.6 -1.03 -2.105"
|
||||
/>
|
||||
<path d="M16 15c-2.21 1.333 -5.792 1.333 -8 0" />
|
||||
</svg>
|
||||
</a>
|
||||
<div class="border-t border-stone-700" />
|
||||
|
||||
<a
|
||||
sveltekit:prefetch
|
||||
href="/applications"
|
||||
class="icons tooltip-green-500 tooltip-right bg-coolgray-200 hover:text-green-500"
|
||||
class:text-green-500={$page.url.pathname.startsWith('/applications') ||
|
||||
$page.url.pathname.startsWith('/new/application')}
|
||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/applications') ||
|
||||
$page.url.pathname.startsWith('/new/application')}
|
||||
data-tooltip="Applications"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8"
|
||||
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="4" width="6" height="6" rx="1" />
|
||||
<rect x="4" y="14" width="6" height="6" rx="1" />
|
||||
<rect x="14" y="14" width="6" height="6" rx="1" />
|
||||
<line x1="14" y1="7" x2="20" y2="7" />
|
||||
<line x1="17" y1="4" x2="17" y2="10" />
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
sveltekit:prefetch
|
||||
href="/sources"
|
||||
class="icons tooltip-orange-500 tooltip-right bg-coolgray-200 hover:text-orange-500"
|
||||
class:text-orange-500={$page.url.pathname.startsWith('/sources') ||
|
||||
$page.url.pathname.startsWith('/new/source')}
|
||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/sources') ||
|
||||
$page.url.pathname.startsWith('/new/source')}
|
||||
data-tooltip="Git Sources"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8"
|
||||
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="6" cy="6" r="2" />
|
||||
<circle cx="18" cy="18" r="2" />
|
||||
<path d="M11 6h5a2 2 0 0 1 2 2v8" />
|
||||
<polyline points="14 9 11 6 14 3" />
|
||||
<path d="M13 18h-5a2 2 0 0 1 -2 -2v-8" />
|
||||
<polyline points="10 15 13 18 10 21" />
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
sveltekit:prefetch
|
||||
href="/destinations"
|
||||
class="icons tooltip-sky-500 tooltip-right bg-coolgray-200 hover:text-sky-500"
|
||||
class:text-sky-500={$page.url.pathname.startsWith('/destinations') ||
|
||||
$page.url.pathname.startsWith('/new/destination')}
|
||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/destinations') ||
|
||||
$page.url.pathname.startsWith('/new/destination')}
|
||||
data-tooltip="Destinations"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8"
|
||||
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="M22 12.54c-1.804 -.345 -2.701 -1.08 -3.523 -2.94c-.487 .696 -1.102 1.568 -.92 2.4c.028 .238 -.32 1.002 -.557 1h-14c0 5.208 3.164 7 6.196 7c4.124 .022 7.828 -1.376 9.854 -5c1.146 -.101 2.296 -1.505 2.95 -2.46z"
|
||||
/>
|
||||
<path d="M5 10h3v3h-3z" />
|
||||
<path d="M8 10h3v3h-3z" />
|
||||
<path d="M11 10h3v3h-3z" />
|
||||
<path d="M8 7h3v3h-3z" />
|
||||
<path d="M11 7h3v3h-3z" />
|
||||
<path d="M11 4h3v3h-3z" />
|
||||
<path d="M4.571 18c1.5 0 2.047 -.074 2.958 -.78" />
|
||||
<line x1="10" y1="16" x2="10" y2="16.01" />
|
||||
</svg>
|
||||
</a>
|
||||
<div class="border-t border-stone-700" />
|
||||
<a
|
||||
sveltekit:prefetch
|
||||
href="/databases"
|
||||
class="icons tooltip-purple-500 tooltip-right bg-coolgray-200 hover:text-purple-500"
|
||||
class:text-purple-500={$page.url.pathname.startsWith('/databases') ||
|
||||
$page.url.pathname.startsWith('/new/database')}
|
||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/databases') ||
|
||||
$page.url.pathname.startsWith('/new/database')}
|
||||
data-tooltip="Databases"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8"
|
||||
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" />
|
||||
<ellipse cx="12" cy="6" rx="8" ry="3" />
|
||||
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
|
||||
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
sveltekit:prefetch
|
||||
href="/services"
|
||||
class="icons tooltip-pink-500 tooltip-right bg-coolgray-200 hover:text-pink-500"
|
||||
class:text-pink-500={$page.url.pathname.startsWith('/services') ||
|
||||
$page.url.pathname.startsWith('/new/service')}
|
||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/services') ||
|
||||
$page.url.pathname.startsWith('/new/service')}
|
||||
data-tooltip="Services"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8"
|
||||
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 18a4.6 4.4 0 0 1 0 -9a5 4.5 0 0 1 11 2h1a3.5 3.5 0 0 1 0 7h-12" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex-1" />
|
||||
|
||||
<UpdateAvailable />
|
||||
<div class="flex flex-col space-y-2 py-2">
|
||||
<a
|
||||
sveltekit:prefetch
|
||||
href="/iam"
|
||||
class="icons tooltip-fuchsia-500 tooltip-right bg-coolgray-200 hover:text-fuchsia-500"
|
||||
class:text-fuchsia-500={$page.url.pathname.startsWith('/iam')}
|
||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/iam')}
|
||||
data-tooltip="IAM"
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8"
|
||||
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="9" cy="7" r="4" />
|
||||
<path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" />
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
|
||||
</svg>
|
||||
</a>
|
||||
{#if $appSession.teamId === '0'}
|
||||
<a
|
||||
sveltekit:prefetch
|
||||
href="/settings"
|
||||
class="icons tooltip-yellow-500 tooltip-right bg-coolgray-200 hover:text-yellow-500"
|
||||
class:text-yellow-500={$page.url.pathname.startsWith('/settings')}
|
||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/settings')}
|
||||
data-tooltip="Settings"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8"
|
||||
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="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"
|
||||
/>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="icons tooltip-red-500 tooltip-right bg-coolgray-200 hover:text-red-500"
|
||||
data-tooltip="Logout"
|
||||
on:click={logout}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="ml-1 h-7 w-7"
|
||||
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="M14 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2"
|
||||
/>
|
||||
<path d="M7 12h14l-3 -3m0 6l3 -3" />
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="w-full text-center font-bold text-stone-400 hover:bg-coolgray-200 hover:text-white"
|
||||
>
|
||||
<a
|
||||
class="text-[10px] no-underline"
|
||||
href={`https://github.com/coollabsio/coolify/releases/tag/v${$appSession.version}`}
|
||||
target="_blank">v{$appSession.version}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{#if $appSession.whiteLabeled}
|
||||
<span class="fixed bottom-0 left-[50px] z-50 m-2 px-4 text-xs text-stone-700"
|
||||
>Powered by <a href="https://coolify.io" target="_blank">Coolify</a></span
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
172
apps/ui/src/routes/applications/[id]/_Secret.svelte
Normal file
172
apps/ui/src/routes/applications/[id]/_Secret.svelte
Normal file
@@ -0,0 +1,172 @@
|
||||
<script lang="ts">
|
||||
export let name = '';
|
||||
export let value = '';
|
||||
export let isBuildSecret = false;
|
||||
export let isNewSecret = false;
|
||||
export let isPRMRSecret = false;
|
||||
export let PRMRSecret: any = {};
|
||||
|
||||
if (isPRMRSecret) value = PRMRSecret.value;
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { del } from '$lib/api';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { saveSecret } from './utils';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const { id } = $page.params;
|
||||
async function removeSecret() {
|
||||
try {
|
||||
await del(`/applications/${id}/secrets`, { name });
|
||||
dispatch('refresh');
|
||||
if (isNewSecret) {
|
||||
name = '';
|
||||
value = '';
|
||||
isBuildSecret = false;
|
||||
}
|
||||
toast.push('Secret removed.');
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function createSecret(isNew: any) {
|
||||
try {
|
||||
await saveSecret({
|
||||
isNew,
|
||||
name,
|
||||
value,
|
||||
isBuildSecret,
|
||||
isPRMRSecret,
|
||||
isNewSecret,
|
||||
applicationId: id
|
||||
});
|
||||
if (isNewSecret) {
|
||||
name = '';
|
||||
value = '';
|
||||
isBuildSecret = false;
|
||||
}
|
||||
dispatch('refresh');
|
||||
toast.push('Secret saved.');
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function setSecretValue() {
|
||||
if (!isPRMRSecret) {
|
||||
isBuildSecret = !isBuildSecret;
|
||||
if (!isNewSecret) {
|
||||
await saveSecret({
|
||||
isNew: isNewSecret,
|
||||
name,
|
||||
value,
|
||||
isBuildSecret,
|
||||
isPRMRSecret,
|
||||
isNewSecret,
|
||||
applicationId: id
|
||||
});
|
||||
toast.push('Secret saved.');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<td>
|
||||
<input
|
||||
id={isNewSecret ? 'secretName' : 'secretNameNew'}
|
||||
bind:value={name}
|
||||
required
|
||||
placeholder="EXAMPLE_VARIABLE"
|
||||
class=" border border-dashed border-coolgray-300"
|
||||
readonly={!isNewSecret}
|
||||
class:bg-transparent={!isNewSecret}
|
||||
class:cursor-not-allowed={!isNewSecret}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<CopyPasswordField
|
||||
id={isNewSecret ? 'secretValue' : 'secretValueNew'}
|
||||
name={isNewSecret ? 'secretValue' : 'secretValueNew'}
|
||||
isPasswordField={true}
|
||||
bind:value
|
||||
required
|
||||
placeholder="J$#@UIO%HO#$U%H"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<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:opacity-50={isPRMRSecret}
|
||||
class:cursor-not-allowed={isPRMRSecret}
|
||||
class:cursor-pointer={!isPRMRSecret}
|
||||
>
|
||||
<span class="sr-only">{$t('application.secrets.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>
|
||||
</td>
|
||||
<td>
|
||||
{#if isNewSecret}
|
||||
<div class="flex items-center justify-center">
|
||||
<button class="bg-green-600 hover:bg-green-500" on:click={() => createSecret(true)}
|
||||
>{$t('forms.add')}</button
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row justify-center space-x-2">
|
||||
<div class="flex items-center justify-center">
|
||||
<button class="" on:click={() => createSecret(false)}>{$t('forms.set')}</button>
|
||||
</div>
|
||||
{#if !isPRMRSecret}
|
||||
<div class="flex justify-center items-end">
|
||||
<button class="bg-red-600 hover:bg-red-500" on:click={removeSecret}
|
||||
>{$t('forms.remove')}</button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
73
apps/ui/src/routes/applications/[id]/_Setting.svelte
Normal file
73
apps/ui/src/routes/applications/[id]/_Setting.svelte
Normal file
@@ -0,0 +1,73 @@
|
||||
<script lang="ts">
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
|
||||
export let setting: any;
|
||||
export let title: any;
|
||||
export let description: any;
|
||||
export let isCenter = true;
|
||||
export let disabled = false;
|
||||
export let dataTooltip: any = null;
|
||||
export let loading = false;
|
||||
</script>
|
||||
|
||||
<div class="flex items-center py-4 pr-8">
|
||||
<div class="flex w-96 flex-col">
|
||||
<div class="text-xs font-bold text-stone-100 md:text-base">{title}</div>
|
||||
<Explainer text={description} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class:tooltip={dataTooltip}
|
||||
class:text-center={isCenter}
|
||||
data-tooltip={dataTooltip}
|
||||
class="flex justify-center"
|
||||
>
|
||||
<div
|
||||
type="button"
|
||||
on:click
|
||||
aria-pressed="false"
|
||||
class="relative mx-20 inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out"
|
||||
class:opacity-50={disabled || loading}
|
||||
class:bg-green-600={!loading && setting}
|
||||
class:bg-stone-700={!loading && !setting}
|
||||
class:bg-yellow-500={loading}
|
||||
>
|
||||
<span class="sr-only">Use setting</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={setting}
|
||||
class:translate-x-0={!setting}
|
||||
>
|
||||
<span
|
||||
class=" absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
|
||||
class:opacity-0={setting}
|
||||
class:opacity-100={!setting}
|
||||
class:animate-spin={loading}
|
||||
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={setting}
|
||||
class:opacity-0={!setting}
|
||||
class:animate-spin={loading}
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
77
apps/ui/src/routes/applications/[id]/_Storage.svelte
Normal file
77
apps/ui/src/routes/applications/[id]/_Storage.svelte
Normal file
@@ -0,0 +1,77 @@
|
||||
<script lang="ts">
|
||||
export let isNew = false;
|
||||
export let storage: any = {
|
||||
id: null,
|
||||
path: null
|
||||
};
|
||||
import { del, post } from '$lib/api';
|
||||
import { page } from '$app/stores';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
const { id } = $page.params;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
async function saveStorage(newStorage = false) {
|
||||
try {
|
||||
if (!storage.path) return errorNotification($t('application.storage.path_is_required'));
|
||||
storage.path = storage.path.startsWith('/') ? storage.path : `/${storage.path}`;
|
||||
storage.path = storage.path.endsWith('/') ? storage.path.slice(0, -1) : storage.path;
|
||||
storage.path.replace(/\/\//g, '/');
|
||||
await post(`/applications/${id}/storages`, {
|
||||
path: storage.path,
|
||||
storageId: storage.id,
|
||||
newStorage
|
||||
});
|
||||
dispatch('refresh');
|
||||
if (isNew) {
|
||||
storage.path = null;
|
||||
storage.id = null;
|
||||
}
|
||||
if (newStorage) toast.push($t('application.storage.storage_saved'));
|
||||
else toast.push($t('application.storage.storage_updated'));
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function removeStorage() {
|
||||
try {
|
||||
await del(`/applications/${id}/storages`, { path: storage.path });
|
||||
dispatch('refresh');
|
||||
toast.push($t('application.storage.storage_deleted'));
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<td>
|
||||
<input
|
||||
bind:value={storage.path}
|
||||
required
|
||||
placeholder="eg: /sqlite.db"
|
||||
class=" border border-dashed border-coolgray-300"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
{#if isNew}
|
||||
<div class="flex items-center justify-center">
|
||||
<button class="bg-green-600 hover:bg-green-500" on:click={() => saveStorage(true)}
|
||||
>{$t('forms.add')}</button
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row justify-center space-x-2">
|
||||
<div class="flex items-center justify-center">
|
||||
<button class="" on:click={() => saveStorage(false)}>{$t('forms.set')}</button>
|
||||
</div>
|
||||
<div class="flex justify-center items-end">
|
||||
<button class="bg-red-600 hover:bg-red-500" on:click={removeStorage}
|
||||
>{$t('forms.remove')}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
497
apps/ui/src/routes/applications/[id]/__layout.svelte
Normal file
497
apps/ui/src/routes/applications/[id]/__layout.svelte
Normal file
@@ -0,0 +1,497 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
function checkConfiguration(application: any): string | null {
|
||||
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 }) => {
|
||||
try {
|
||||
const response = await get(`/applications/${params.id}`);
|
||||
let { application, appId, settings, isQueueActive } = response;
|
||||
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: {
|
||||
isQueueActive,
|
||||
application
|
||||
},
|
||||
stuff: {
|
||||
application,
|
||||
appId,
|
||||
settings
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return handlerNotFoundLoad(error, url);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let application: any;
|
||||
export let isQueueActive: any;
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
|
||||
import { del, get, post } from '$lib/api';
|
||||
import { goto } from '$app/navigation';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { t } from '$lib/translations';
|
||||
import { appSession, disabledButton, status } from '$lib/store';
|
||||
import { errorNotification, handlerNotFoundLoad } from '$lib/common';
|
||||
import Loading from '$lib/components/Loading.svelte';
|
||||
|
||||
let loading = false;
|
||||
let statusInterval: any;
|
||||
$disabledButton =
|
||||
!$appSession.isAdmin ||
|
||||
!application.fqdn ||
|
||||
!application.gitSource ||
|
||||
!application.repository ||
|
||||
!application.destinationDocker ||
|
||||
!application.buildPack;
|
||||
const { id } = $page.params;
|
||||
|
||||
async function handleDeploySubmit() {
|
||||
try {
|
||||
const { buildId } = await post(`/applications/${id}/deploy`, { ...application });
|
||||
toast.push($t('application.deployment_queued'));
|
||||
if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) {
|
||||
return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`);
|
||||
} else {
|
||||
return await goto(`/applications/${id}/logs/build?buildId=${buildId}`, {
|
||||
replaceState: true
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteApplication(name: string) {
|
||||
const sure = confirm($t('application.confirm_to_delete', { name }));
|
||||
if (sure) {
|
||||
loading = true;
|
||||
try {
|
||||
await del(`/applications/${id}`, { id });
|
||||
return await goto(`/applications`);
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
async function stopApplication() {
|
||||
try {
|
||||
loading = true;
|
||||
await post(`/applications/${id}/stop`, {});
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function getStatus() {
|
||||
if ($status.application.loading) return;
|
||||
$status.application.loading = true;
|
||||
const data = await get(`/applications/${id}`);
|
||||
$status.application.isRunning = data.isRunning;
|
||||
$status.application.isExited = data.isExited;
|
||||
$status.application.loading = false;
|
||||
$status.application.initialLoading = false;
|
||||
}
|
||||
onDestroy(() => {
|
||||
$status.application.initialLoading = true;
|
||||
clearInterval(statusInterval);
|
||||
});
|
||||
onMount(async () => {
|
||||
$status.application.isRunning = false;
|
||||
$status.application.isExited = false;
|
||||
$status.application.loading = false;
|
||||
if (application.gitSourceId && application.destinationDockerId && application.fqdn) {
|
||||
await getStatus();
|
||||
statusInterval = setInterval(async () => {
|
||||
await getStatus();
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<nav class="nav-side">
|
||||
{#if loading}
|
||||
<Loading fullscreen cover />
|
||||
{:else}
|
||||
{#if $status.application.isExited}
|
||||
<a
|
||||
href={!$disabledButton ? `/applications/${id}/logs` : null}
|
||||
class=" icons bg-transparent tooltip-bottom text-sm flex items-center text-red-500 tooltip-red-500"
|
||||
data-tooltip="Application exited with an error!"
|
||||
sveltekit:prefetch
|
||||
>
|
||||
<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="M8.7 3h6.6c.3 0 .5 .1 .7 .3l4.7 4.7c.2 .2 .3 .4 .3 .7v6.6c0 .3 -.1 .5 -.3 .7l-4.7 4.7c-.2 .2 -.4 .3 -.7 .3h-6.6c-.3 0 -.5 -.1 -.7 -.3l-4.7 -4.7c-.2 -.2 -.3 -.4 -.3 -.7v-6.6c0 -.3 .1 -.5 .3 -.7l4.7 -4.7c.2 -.2 .4 -.3 .7 -.3z"
|
||||
/>
|
||||
<line x1="12" y1="8" x2="12" y2="12" />
|
||||
<line x1="12" y1="16" x2="12.01" y2="16" />
|
||||
</svg>
|
||||
</a>
|
||||
{/if}
|
||||
{#if $status.application.initialLoading}
|
||||
<button
|
||||
class="icons tooltip-bottom flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out"
|
||||
>
|
||||
<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="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
|
||||
<line x1="5.63" y1="7.16" x2="5.63" y2="7.17" />
|
||||
<line x1="4.06" y1="11" x2="4.06" y2="11.01" />
|
||||
<line x1="4.63" y1="15.1" x2="4.63" y2="15.11" />
|
||||
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
|
||||
<line x1="11" y1="19.94" x2="11" y2="19.95" />
|
||||
</svg>
|
||||
</button>
|
||||
{:else if $status.application.isRunning}
|
||||
<button
|
||||
on:click={stopApplication}
|
||||
title="Stop application"
|
||||
type="submit"
|
||||
disabled={$disabledButton}
|
||||
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-red-500"
|
||||
data-tooltip={$appSession.isAdmin
|
||||
? $t('application.stop_application')
|
||||
: $t('application.permission_denied_stop_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={$disabledButton || !isQueueActive}
|
||||
class:hover:text-green-500={isQueueActive}
|
||||
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2"
|
||||
data-tooltip={$appSession.isAdmin
|
||||
? isQueueActive
|
||||
? 'Rebuild application'
|
||||
: 'Autoupdate inprogress. Cannot 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={$disabledButton}
|
||||
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-green-500"
|
||||
data-tooltip={$appSession.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-coolgray-500 h-8" />
|
||||
<a
|
||||
href={!$disabledButton ? `/applications/${id}` : null}
|
||||
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"
|
||||
disabled={$disabledButton}
|
||||
class="icons bg-transparent tooltip-bottom text-sm"
|
||||
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={!$disabledButton ? `/applications/${id}/secrets` : null}
|
||||
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="Secret"
|
||||
disabled={$disabledButton}
|
||||
class="icons bg-transparent tooltip-bottom text-sm"
|
||||
data-tooltip="Secret"
|
||||
>
|
||||
<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={!$disabledButton ? `/applications/${id}/storages` : null}
|
||||
sveltekit:prefetch
|
||||
class="hover:text-pink-500 rounded"
|
||||
class:text-pink-500={$page.url.pathname === `/applications/${id}/storages`}
|
||||
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/storages`}
|
||||
>
|
||||
<button
|
||||
title="Persistent Storages"
|
||||
disabled={$disabledButton}
|
||||
class="icons bg-transparent tooltip-bottom text-sm"
|
||||
data-tooltip="Persistent Storages"
|
||||
>
|
||||
<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" />
|
||||
<ellipse cx="12" cy="6" rx="8" ry="3" />
|
||||
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
|
||||
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
|
||||
</svg>
|
||||
</button></a
|
||||
>
|
||||
<a
|
||||
href={!$disabledButton ? `/applications/${id}/previews` : null}
|
||||
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"
|
||||
disabled={$disabledButton}
|
||||
class="icons bg-transparent tooltip-bottom text-sm"
|
||||
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-coolgray-500 h-8" />
|
||||
<a
|
||||
href={!$disabledButton && $status.application.isRunning ? `/applications/${id}/logs` : null}
|
||||
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={$t('application.logs')}
|
||||
disabled={$disabledButton || !$status.application.isRunning}
|
||||
class="icons bg-transparent tooltip-bottom text-sm"
|
||||
data-tooltip={$t('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={!$disabledButton ? `/applications/${id}/logs/build` : null}
|
||||
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"
|
||||
disabled={$disabledButton}
|
||||
class="icons bg-transparent tooltip-bottom text-sm"
|
||||
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-coolgray-500 h-8" />
|
||||
|
||||
<button
|
||||
on:click={() => deleteApplication(application.name)}
|
||||
title={$t('application.delete_application')}
|
||||
type="submit"
|
||||
disabled={!$appSession.isAdmin}
|
||||
class:hover:text-red-500={$appSession.isAdmin}
|
||||
class="icons bg-transparent tooltip-bottom text-sm"
|
||||
data-tooltip={$appSession.isAdmin
|
||||
? $t('application.delete_application')
|
||||
: $t('application.permission_denied_delete_application')}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</button>
|
||||
{/if}
|
||||
</nav>
|
||||
<slot />
|
||||
@@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { post } from '$lib/api';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { findBuildPack } from '$lib/templates';
|
||||
import { t } from '$lib/translations';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
|
||||
export let buildPack: any;
|
||||
export let foundConfig: any;
|
||||
export let scanning: any;
|
||||
export let packageManager: any;
|
||||
|
||||
async function handleSubmit(name: string) {
|
||||
try {
|
||||
const tempBuildPack = JSON.parse(
|
||||
JSON.stringify(findBuildPack(buildPack.name, packageManager))
|
||||
);
|
||||
|
||||
delete tempBuildPack.name;
|
||||
delete tempBuildPack.fancyName;
|
||||
delete tempBuildPack.color;
|
||||
delete tempBuildPack.hoverColor;
|
||||
|
||||
if (foundConfig.buildPack !== name) {
|
||||
await post(`/applications/${id}`, { ...tempBuildPack, buildPack: name });
|
||||
}
|
||||
await post(`/applications/${id}/configuration/buildpack`, { 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"
|
||||
>{$t('application.configuration.buildpack.choose_this_one')}</span
|
||||
>
|
||||
{/if}
|
||||
</button>
|
||||
</form>
|
||||
@@ -0,0 +1,201 @@
|
||||
<script lang="ts">
|
||||
export let application: any;
|
||||
//@ts-ignore
|
||||
import Select from 'svelte-select';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { get, post } from '$lib/api';
|
||||
import { onMount } from 'svelte';
|
||||
import { appSession } from '$lib/store';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
|
||||
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: any = [];
|
||||
let branches: any = [];
|
||||
|
||||
let selected = {
|
||||
projectId: undefined,
|
||||
repository: undefined,
|
||||
branch: undefined,
|
||||
autodeploy: application.settings.autodeploy || true
|
||||
};
|
||||
let showSave = false;
|
||||
|
||||
async function loadRepositoriesByPage(page = 0) {
|
||||
return await get(`${apiUrl}/installation/repositories?per_page=100&page=${page}`, {
|
||||
Authorization: `token ${$appSession.tokens.github}`
|
||||
});
|
||||
}
|
||||
|
||||
async function loadBranchesByPage(page = 0) {
|
||||
return await get(`${apiUrl}/repos/${selected.repository}/branches?per_page=100&page=${page}`, {
|
||||
Authorization: `token ${$appSession.tokens.github}`
|
||||
});
|
||||
}
|
||||
|
||||
let reposSelectOptions: any;
|
||||
let branchSelectOptions: any;
|
||||
|
||||
async function loadRepositories() {
|
||||
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;
|
||||
reposSelectOptions = repositories.map((repo: any) => ({
|
||||
value: repo.full_name,
|
||||
label: repo.name
|
||||
}));
|
||||
}
|
||||
async function loadBranches(event: any) {
|
||||
branches = [];
|
||||
selected.repository = event.detail.value;
|
||||
selected.projectId = repositories.find(
|
||||
(repo: any) => repo.full_name === selected.repository
|
||||
).id;
|
||||
let page = 1;
|
||||
let branchCount = 0;
|
||||
loading.branches = true;
|
||||
const loadedBranches = await loadBranchesByPage();
|
||||
branches = branches.concat(loadedBranches);
|
||||
branchCount = branches.length;
|
||||
if (branchCount === 100) {
|
||||
while (branchCount === 100) {
|
||||
page = page + 1;
|
||||
const nextBranches = await loadBranchesByPage(page);
|
||||
branches = branches.concat(nextBranches);
|
||||
branchCount = nextBranches.length;
|
||||
}
|
||||
}
|
||||
loading.branches = false;
|
||||
branchSelectOptions = branches.map((branch: any) => ({
|
||||
value: branch.name,
|
||||
label: branch.name
|
||||
}));
|
||||
}
|
||||
async function isBranchAlreadyUsed(event: any) {
|
||||
selected.branch = event.detail.value;
|
||||
try {
|
||||
const data = await get(
|
||||
`/applications/${id}/configuration/repository?repository=${selected.repository}&branch=${selected.branch}`
|
||||
);
|
||||
if (data.used) {
|
||||
const sure = confirm($t('application.configuration.branch_already_in_use'));
|
||||
if (sure) {
|
||||
selected.autodeploy = false;
|
||||
showSave = true;
|
||||
return true;
|
||||
}
|
||||
showSave = false;
|
||||
return true;
|
||||
}
|
||||
showSave = true;
|
||||
} catch ({ error }) {
|
||||
showSave = false;
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
if (!$appSession.tokens.github) {
|
||||
const { token } = await get(`/applications/${id}/configuration/githubToken`);
|
||||
$appSession.tokens.github = token;
|
||||
}
|
||||
await loadRepositories();
|
||||
} catch (error: any) {
|
||||
if (error.message === 'Bad credentials') {
|
||||
const { token } = await get(`/applications/${id}/configuration/githubToken`);
|
||||
$appSession.tokens.github = token;
|
||||
return await loadRepositories();
|
||||
}
|
||||
return errorNotification(error);
|
||||
}
|
||||
});
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await post(`/applications/${id}/configuration/repository`, { ...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">{$t('application.configuration.no_repositories_configured')}</div>
|
||||
<a href={`/sources/${application.gitSource.id}`}
|
||||
><button>{$t('application.configuration.configure_it_now')}</button></a
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<form on:submit|preventDefault={handleSubmit} class="flex flex-col justify-center text-center">
|
||||
<div class="flex-col space-y-3 md:space-y-0 space-x-1">
|
||||
<div class="flex-row md:flex gap-4">
|
||||
<div class="custom-select-wrapper">
|
||||
<Select
|
||||
placeholder={loading.repositories
|
||||
? $t('application.configuration.loading_repositories')
|
||||
: $t('application.configuration.select_a_repository')}
|
||||
id="repository"
|
||||
showIndicator={!loading.repositories}
|
||||
isWaiting={loading.repositories}
|
||||
on:select={loadBranches}
|
||||
items={reposSelectOptions}
|
||||
isDisabled={loading.repositories}
|
||||
isClearable={false}
|
||||
/>
|
||||
</div>
|
||||
<input class="hidden" bind:value={selected.projectId} name="projectId" />
|
||||
<div class="custom-select-wrapper">
|
||||
<Select
|
||||
placeholder={loading.branches
|
||||
? $t('application.configuration.loading_branches')
|
||||
: !selected.repository
|
||||
? $t('application.configuration.select_a_repository_first')
|
||||
: $t('application.configuration.select_a_branch')}
|
||||
isWaiting={loading.branches}
|
||||
showIndicator={selected.repository && !loading.branches}
|
||||
id="branches"
|
||||
on:select={isBranchAlreadyUsed}
|
||||
items={branchSelectOptions}
|
||||
isDisabled={loading.branches || !selected.repository}
|
||||
isClearable={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</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}>{$t('forms.save')}</button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
@@ -0,0 +1,412 @@
|
||||
<script lang="ts">
|
||||
export let application: any;
|
||||
export let appId: any;
|
||||
export let settings: any;
|
||||
//@ts-ignore
|
||||
import cuid from 'cuid';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { dev } from '$app/env';
|
||||
import { goto } from '$app/navigation';
|
||||
import { del, get, post } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { appSession } from '$lib/store';
|
||||
import Select from 'svelte-select';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
|
||||
let url = settings?.fqdn ? settings.fqdn : window.location.origin;
|
||||
if (dev) url = `http://localhost:3001`;
|
||||
|
||||
const updateDeployKeyIdUrl = `/applications/${id}/configuration/deploykey`;
|
||||
|
||||
let loading = {
|
||||
base: true,
|
||||
projects: false,
|
||||
branches: false,
|
||||
save: false
|
||||
};
|
||||
|
||||
let htmlUrl = application.gitSource.htmlUrl;
|
||||
let apiUrl = application.gitSource.apiUrl;
|
||||
|
||||
let username: any = null;
|
||||
let groups: any = [];
|
||||
let projects: any = [];
|
||||
let branches: any = [];
|
||||
let showSave = false;
|
||||
let autodeploy = application.settings.autodeploy || true;
|
||||
|
||||
let selected: any = {
|
||||
group: undefined,
|
||||
project: undefined,
|
||||
branch: undefined
|
||||
};
|
||||
let search = {
|
||||
project: '',
|
||||
branch: ''
|
||||
};
|
||||
onMount(async () => {
|
||||
if (!$appSession.tokens.gitlab) {
|
||||
await getGitlabToken();
|
||||
}
|
||||
loading.base = true;
|
||||
try {
|
||||
const user = await get(`${apiUrl}/v4/user`, {
|
||||
Authorization: `Bearer ${$appSession.tokens.gitlab}`
|
||||
});
|
||||
username = user.username;
|
||||
} catch (error) {
|
||||
return await getGitlabToken();
|
||||
}
|
||||
try {
|
||||
groups = await get(`${apiUrl}/v4/groups?per_page=5000`, {
|
||||
Authorization: `Bearer ${$appSession.tokens.gitlab}`
|
||||
});
|
||||
} catch (error: any) {
|
||||
errorNotification(error);
|
||||
throw new Error(error);
|
||||
} finally {
|
||||
loading.base = false;
|
||||
}
|
||||
});
|
||||
function selectGroup(event: any) {
|
||||
selected.group = event.detail;
|
||||
selected.project = null;
|
||||
selected.branch = null;
|
||||
showSave = false;
|
||||
loadProjects();
|
||||
}
|
||||
async function searchProjects(searchText: any) {
|
||||
if (!selected.group) {
|
||||
return;
|
||||
}
|
||||
search.project = searchText;
|
||||
await loadProjects();
|
||||
return projects;
|
||||
}
|
||||
function selectProject(event: any) {
|
||||
selected.project = event.detail;
|
||||
selected.branch = null;
|
||||
showSave = false;
|
||||
loadBranches();
|
||||
}
|
||||
async function getGitlabToken() {
|
||||
return await new Promise<void>((resolve, reject) => {
|
||||
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=${url}/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);
|
||||
$appSession.tokens.gitlab = localStorage.getItem('gitLabToken');
|
||||
localStorage.removeItem('gitLabToken');
|
||||
resolve();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadProjects() {
|
||||
//@ts-ignore
|
||||
const params: any = new URLSearchParams({
|
||||
page: 1,
|
||||
per_page: 25,
|
||||
archived: false
|
||||
});
|
||||
if (search.project) {
|
||||
params.append('search', search.project);
|
||||
}
|
||||
loading.projects = true;
|
||||
if (username === selected.group.name) {
|
||||
try {
|
||||
params.append('min_access_level', 40);
|
||||
projects = await get(`${apiUrl}/v4/users/${selected.group.name}/projects?${params}`, {
|
||||
Authorization: `Bearer ${$appSession.tokens.gitlab}`
|
||||
});
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.projects = false;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
projects = await get(`${apiUrl}/v4/groups/${selected.group.id}/projects?${params}`, {
|
||||
Authorization: `Bearer ${$appSession.tokens.gitlab}`
|
||||
});
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.projects = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
async function searchBranches(searchText: any) {
|
||||
if (!selected.project) {
|
||||
return;
|
||||
}
|
||||
search.branch = searchText;
|
||||
await loadBranches();
|
||||
return branches;
|
||||
}
|
||||
function selectBranch(event: any) {
|
||||
selected.branch = event.detail;
|
||||
isBranchAlreadyUsed();
|
||||
}
|
||||
async function loadBranches() {
|
||||
//@ts-ignore
|
||||
const params = new URLSearchParams({
|
||||
page: 1,
|
||||
per_page: 100
|
||||
});
|
||||
if (search.branch) {
|
||||
params.append('search', search.branch);
|
||||
}
|
||||
loading.branches = true;
|
||||
try {
|
||||
branches = await get(
|
||||
`${apiUrl}/v4/projects/${selected.project.id}/repository/branches?${params}`,
|
||||
{
|
||||
Authorization: `Bearer ${$appSession.tokens.gitlab}`
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.branches = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function isBranchAlreadyUsed() {
|
||||
try {
|
||||
const data = await get(
|
||||
`/applications/${id}/configuration/repository?repository=${selected.project.path_with_namespace}&branch=${selected.branch.name}`
|
||||
);
|
||||
if (data.used) {
|
||||
const sure = confirm($t('application.configuration.branch_already_in_use'));
|
||||
if (sure) {
|
||||
autodeploy = false;
|
||||
showSave = true;
|
||||
return true;
|
||||
}
|
||||
showSave = false;
|
||||
return true;
|
||||
}
|
||||
showSave = true;
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function checkSSHKey(sshkeyUrl: any) {
|
||||
try {
|
||||
return await post(sshkeyUrl, {});
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function setWebhook(url: any, webhookToken: any) {
|
||||
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 ${$appSession.tokens.gitlab}`
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
return errorNotification(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`;
|
||||
const webhookUrl = `${apiUrl}/v4/projects/${selected.project.id}/hooks`;
|
||||
const webhookToken = cuid();
|
||||
|
||||
try {
|
||||
if (!privateSshKey || !publicSshKey) {
|
||||
const data: any = await checkSSHKey(sshkeyUrl);
|
||||
publicSshKey = data.publicKey;
|
||||
}
|
||||
const deployKeys = await get(deployKeyUrl, {
|
||||
Authorization: `Bearer ${$appSession.tokens.gitlab}`
|
||||
});
|
||||
const deployKeyFound = deployKeys.filter(
|
||||
(dk: any) => dk.title === `${appId}-coolify-deploy-key`
|
||||
);
|
||||
if (deployKeyFound.length > 0) {
|
||||
for (const deployKey of deployKeyFound) {
|
||||
await del(
|
||||
`${deployKeyUrl}/${deployKey.id}`,
|
||||
{},
|
||||
{
|
||||
Authorization: `Bearer ${$appSession.tokens.gitlab}`
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
const { id } = await post(
|
||||
deployKeyUrl,
|
||||
{
|
||||
title: `${appId}-coolify-deploy-key`,
|
||||
key: publicSshKey,
|
||||
can_push: false
|
||||
},
|
||||
{
|
||||
Authorization: `Bearer ${$appSession.tokens.gitlab}`
|
||||
}
|
||||
);
|
||||
await post(updateDeployKeyIdUrl, { deployKeyId: id });
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.save = false;
|
||||
}
|
||||
|
||||
try {
|
||||
await setWebhook(webhookUrl, webhookToken);
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.save = false;
|
||||
}
|
||||
|
||||
const url = `/applications/${id}/configuration/repository`;
|
||||
try {
|
||||
const repository = selected.project.path_with_namespace;
|
||||
await post(url, {
|
||||
repository,
|
||||
branch: selected.branch.name,
|
||||
projectId: selected.project.id,
|
||||
autodeploy,
|
||||
webhookToken
|
||||
});
|
||||
return await goto(from || `/applications/${id}/configuration/buildpack`);
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.save = false;
|
||||
}
|
||||
}
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await post(`/applications/${id}/configuration/repository`, { ...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 ">
|
||||
<div class="custom-select-wrapper">
|
||||
<Select
|
||||
placeholder={loading.base
|
||||
? $t('application.configuration.loading_groups')
|
||||
: $t('application.configuration.select_a_group')}
|
||||
id="group"
|
||||
showIndicator={!loading.base}
|
||||
isWaiting={loading.base}
|
||||
on:select={selectGroup}
|
||||
on:clear={() => {
|
||||
showSave = false;
|
||||
projects = [];
|
||||
branches = [];
|
||||
selected.group = null;
|
||||
selected.project = null;
|
||||
selected.branch = null;
|
||||
}}
|
||||
value={selected.group}
|
||||
isDisabled={loading.base}
|
||||
isClearable={false}
|
||||
items={groups}
|
||||
labelIdentifier="full_name"
|
||||
optionIdentifier="id"
|
||||
/>
|
||||
</div>
|
||||
<div class="custom-select-wrapper">
|
||||
<Select
|
||||
placeholder={loading.projects
|
||||
? $t('application.configuration.loading_projects')
|
||||
: $t('application.configuration.select_a_project')}
|
||||
noOptionsMessage={$t('application.configuration.no_projects_found')}
|
||||
id="project"
|
||||
showIndicator={!loading.projects}
|
||||
isWaiting={loading.projects}
|
||||
isDisabled={loading.projects || !selected.group}
|
||||
on:select={selectProject}
|
||||
on:clear={() => {
|
||||
showSave = false;
|
||||
branches = [];
|
||||
selected.project = null;
|
||||
selected.branch = null;
|
||||
}}
|
||||
value={selected.project}
|
||||
isClearable={false}
|
||||
items={projects}
|
||||
loadOptions={searchProjects}
|
||||
labelIdentifier="name"
|
||||
optionIdentifier="id"
|
||||
/>
|
||||
</div>
|
||||
<div class="custom-select-wrapper">
|
||||
<Select
|
||||
placeholder={loading.branches
|
||||
? $t('application.configuration.loading_branches')
|
||||
: $t('application.configuration.select_a_branch')}
|
||||
noOptionsMessage={$t('application.configuration.no_branches_found')}
|
||||
id="branch"
|
||||
showIndicator={!loading.branches}
|
||||
isWaiting={loading.branches}
|
||||
isDisabled={loading.branches || !selected.project}
|
||||
on:select={selectBranch}
|
||||
on:clear={() => {
|
||||
showSave = false;
|
||||
selected.branch = null;
|
||||
}}
|
||||
value={selected.branch}
|
||||
isClearable={false}
|
||||
items={branches}
|
||||
loadOptions={searchBranches}
|
||||
labelIdentifier="name"
|
||||
optionIdentifier="web_url"
|
||||
/>
|
||||
</div>
|
||||
</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 ? $t('forms.saving') : $t('forms.save')}</button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,273 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
try {
|
||||
const { application } = stuff;
|
||||
if (application?.buildPack && !url.searchParams.get('from')) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/applications/${params.id}`
|
||||
};
|
||||
}
|
||||
const response = await get(`/applications/${params.id}/configuration/buildpack`);
|
||||
return {
|
||||
props: {
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { get } from '$lib/api';
|
||||
import { appSession } from '$lib/store';
|
||||
import { browser } from '$app/env';
|
||||
import { t } from '$lib/translations';
|
||||
import { buildPacks, findBuildPack, scanningTemplates } from '$lib/templates';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import BuildPack from './_BuildPack.svelte';
|
||||
|
||||
const { id } = $page.params;
|
||||
|
||||
let scanning = true;
|
||||
let foundConfig: any = null;
|
||||
let packageManager = 'npm';
|
||||
|
||||
export let apiUrl: any;
|
||||
export let projectId: any;
|
||||
export let repository: any;
|
||||
export let branch: any;
|
||||
export let type: any;
|
||||
export let application: any;
|
||||
|
||||
function checkPackageJSONContents({ key, json }: { key: any; json: any }) {
|
||||
return json?.dependencies?.hasOwnProperty(key) || json?.devDependencies?.hasOwnProperty(key);
|
||||
}
|
||||
function checkTemplates({ json, packageManager }: { json: any; packageManager: any }) {
|
||||
for (const [key, value] of Object.entries(scanningTemplates)) {
|
||||
if (checkPackageJSONContents({ key, json })) {
|
||||
foundConfig = findBuildPack(value.buildPack, packageManager);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
async function scanRepository() {
|
||||
try {
|
||||
if (type === 'gitlab') {
|
||||
const files = await get(`${apiUrl}/v4/projects/${projectId}/repository/tree`, {
|
||||
Authorization: `Bearer ${$appSession.tokens.gitlab}`
|
||||
});
|
||||
const packageJson = files.find(
|
||||
(file: { name: string; type: string }) =>
|
||||
file.name === 'package.json' && file.type === 'blob'
|
||||
);
|
||||
const yarnLock = files.find(
|
||||
(file: { name: string; type: string }) =>
|
||||
file.name === 'yarn.lock' && file.type === 'blob'
|
||||
);
|
||||
const pnpmLock = files.find(
|
||||
(file: { name: string; type: string }) =>
|
||||
file.name === 'pnpm-lock.yaml' && file.type === 'blob'
|
||||
);
|
||||
const dockerfile = files.find(
|
||||
(file: { name: string; type: string }) =>
|
||||
file.name === 'Dockerfile' && file.type === 'blob'
|
||||
);
|
||||
const cargoToml = files.find(
|
||||
(file: { name: string; type: string }) =>
|
||||
file.name === 'Cargo.toml' && file.type === 'blob'
|
||||
);
|
||||
const requirementsTxt = files.find(
|
||||
(file: { name: string; type: string }) =>
|
||||
file.name === 'requirements.txt' && file.type === 'blob'
|
||||
);
|
||||
const indexHtml = files.find(
|
||||
(file: { name: string; type: string }) =>
|
||||
file.name === 'index.html' && file.type === 'blob'
|
||||
);
|
||||
const indexPHP = files.find(
|
||||
(file: { name: string; type: string }) =>
|
||||
file.name === 'index.php' && file.type === 'blob'
|
||||
);
|
||||
const composerPHP = files.find(
|
||||
(file: { name: string; type: string }) =>
|
||||
file.name === 'composer.json' && file.type === 'blob'
|
||||
);
|
||||
const laravel = files.find(
|
||||
(file: { name: string; type: string }) => file.name === 'artisan' && file.type === 'blob'
|
||||
);
|
||||
|
||||
if (yarnLock) packageManager = 'yarn';
|
||||
if (pnpmLock) packageManager = 'pnpm';
|
||||
|
||||
if (dockerfile) {
|
||||
foundConfig = findBuildPack('docker', packageManager);
|
||||
} else if (packageJson && !laravel) {
|
||||
const path = packageJson.path;
|
||||
const data: any = await get(
|
||||
`${apiUrl}/v4/projects/${projectId}/repository/files/${path}/raw?ref=${branch}`,
|
||||
{
|
||||
Authorization: `Bearer ${$appSession.tokens.gitlab}`
|
||||
}
|
||||
);
|
||||
const json = JSON.parse(data) || {};
|
||||
checkTemplates({ json, packageManager });
|
||||
} else if (cargoToml) {
|
||||
foundConfig = findBuildPack('rust');
|
||||
} else if (requirementsTxt) {
|
||||
foundConfig = findBuildPack('python');
|
||||
} else if (indexHtml) {
|
||||
foundConfig = findBuildPack('static', packageManager);
|
||||
} else if ((indexPHP || composerPHP) && !laravel) {
|
||||
foundConfig = findBuildPack('php');
|
||||
} else if (laravel) {
|
||||
foundConfig = findBuildPack('laravel');
|
||||
} else {
|
||||
foundConfig = findBuildPack('node', packageManager);
|
||||
}
|
||||
} else if (type === 'github') {
|
||||
const files = await get(`${apiUrl}/repos/${repository}/contents?ref=${branch}`, {
|
||||
Authorization: `Bearer ${$appSession.tokens.github}`,
|
||||
Accept: 'application/vnd.github.v2.json'
|
||||
});
|
||||
const packageJson = files.find(
|
||||
(file: { name: string; type: string }) =>
|
||||
file.name === 'package.json' && file.type === 'file'
|
||||
);
|
||||
const yarnLock = files.find(
|
||||
(file: { name: string; type: string }) =>
|
||||
file.name === 'yarn.lock' && file.type === 'file'
|
||||
);
|
||||
const pnpmLock = files.find(
|
||||
(file: { name: string; type: string }) =>
|
||||
file.name === 'pnpm-lock.yaml' && file.type === 'file'
|
||||
);
|
||||
const dockerfile = files.find(
|
||||
(file: { name: string; type: string }) =>
|
||||
file.name === 'Dockerfile' && file.type === 'file'
|
||||
);
|
||||
const cargoToml = files.find(
|
||||
(file: { name: string; type: string }) =>
|
||||
file.name === 'Cargo.toml' && file.type === 'file'
|
||||
);
|
||||
const requirementsTxt = files.find(
|
||||
(file: { name: string; type: string }) =>
|
||||
file.name === 'requirements.txt' && file.type === 'file'
|
||||
);
|
||||
const indexHtml = files.find(
|
||||
(file: { name: string; type: string }) =>
|
||||
file.name === 'index.html' && file.type === 'file'
|
||||
);
|
||||
const indexPHP = files.find(
|
||||
(file: { name: string; type: string }) =>
|
||||
file.name === 'index.php' && file.type === 'file'
|
||||
);
|
||||
const composerPHP = files.find(
|
||||
(file: { name: string; type: string }) =>
|
||||
file.name === 'composer.json' && file.type === 'file'
|
||||
);
|
||||
const laravel = files.find(
|
||||
(file: { name: string; type: string }) => file.name === 'artisan' && file.type === 'file'
|
||||
);
|
||||
|
||||
if (yarnLock) packageManager = 'yarn';
|
||||
if (pnpmLock) packageManager = 'pnpm';
|
||||
|
||||
if (dockerfile) {
|
||||
foundConfig = findBuildPack('docker', packageManager);
|
||||
} else if (packageJson && !laravel) {
|
||||
const data: any = await get(`${packageJson.git_url}`, {
|
||||
Authorization: `Bearer ${$appSession.tokens.github}`,
|
||||
Accept: 'application/vnd.github.v2.raw'
|
||||
});
|
||||
const json = JSON.parse(data) || {};
|
||||
checkTemplates({ json, packageManager });
|
||||
} else if (cargoToml) {
|
||||
foundConfig = findBuildPack('rust');
|
||||
} else if (requirementsTxt) {
|
||||
foundConfig = findBuildPack('python');
|
||||
} else if (indexHtml) {
|
||||
foundConfig = findBuildPack('static', packageManager);
|
||||
} else if ((indexPHP || composerPHP) && !laravel) {
|
||||
foundConfig = findBuildPack('php');
|
||||
} else if (laravel) {
|
||||
foundConfig = findBuildPack('laravel');
|
||||
} else {
|
||||
foundConfig = findBuildPack('node', packageManager);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
scanning = true;
|
||||
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);
|
||||
}
|
||||
}
|
||||
if (error.message === 'Bad credentials') {
|
||||
const { token } = await get(`/applications/${id}/configuration/githubToken`);
|
||||
$appSession.tokens.github = token;
|
||||
return await scanRepository()
|
||||
}
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
if (!foundConfig) foundConfig = findBuildPack('node', packageManager);
|
||||
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">
|
||||
{$t('application.configuration.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">
|
||||
{$t('application.configuration.scanning_repository_suggest_build_pack')}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="max-w-7xl mx-auto flex flex-wrap justify-center">
|
||||
{#each buildPacks as buildPack}
|
||||
<div class="p-2">
|
||||
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,116 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
try {
|
||||
const { application } = stuff;
|
||||
if (application?.destinationDockerId && !url.searchParams.get('from')) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/applications/${params.id}`
|
||||
};
|
||||
}
|
||||
const response = await get(`/destinations`);
|
||||
return {
|
||||
props: {
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { get, post } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { appSession } from '$lib/store';
|
||||
import { errorNotification } from '$lib/common';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
|
||||
export let destinations: any;
|
||||
|
||||
const ownDestinations = destinations.filter((destination: any) => {
|
||||
if (destination.teams[0].id === $appSession.teamId) {
|
||||
return destination;
|
||||
}
|
||||
});
|
||||
const otherDestinations = destinations.filter((destination: any) => {
|
||||
if (destination.teams[0].id !== $appSession.teamId) {
|
||||
return destination;
|
||||
}
|
||||
});
|
||||
async function handleSubmit(destinationId: any) {
|
||||
try {
|
||||
await post(`/applications/${id}/configuration/destination`, { 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">
|
||||
{$t('application.configuration.configure_destination')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center">
|
||||
{#if !destinations || ownDestinations.length === 0}
|
||||
<div class="flex-col">
|
||||
<div class="pb-2">{$t('application.configuration.no_configurable_destination')}</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-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each ownDestinations 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 otherDestinations.length > 0 && $appSession.teamId === '0'}
|
||||
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Destinations</div>
|
||||
{/if}
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each otherDestinations 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>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ params, url, stuff }) => {
|
||||
try {
|
||||
const { application, appId, settings } = stuff;
|
||||
if (application?.branch && application?.repository && !url.searchParams.get('from')) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/applications/${params.id}`
|
||||
};
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
application,
|
||||
appId,
|
||||
settings
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/translations';
|
||||
|
||||
export let application: any;
|
||||
export let appId: string;
|
||||
export let settings: any;
|
||||
|
||||
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">
|
||||
{$t('application.configuration.select_a_repository_project')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#if application.gitSource.type === 'github'}
|
||||
<GithubRepositories {application} />
|
||||
{:else if application.gitSource.type === 'gitlab'}
|
||||
<GitlabRepositories {application} {appId} {settings} />
|
||||
{/if}
|
||||
</div>
|
||||
148
apps/ui/src/routes/applications/[id]/configuration/source.svelte
Normal file
148
apps/ui/src/routes/applications/[id]/configuration/source.svelte
Normal file
@@ -0,0 +1,148 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
try {
|
||||
const { application } = stuff;
|
||||
if (application?.gitSourceId && !url.searchParams.get('from')) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/applications/${params.id}`
|
||||
};
|
||||
}
|
||||
const response = await get(`/sources`);
|
||||
return {
|
||||
props: {
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { page, session } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { get, post } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { appSession } from '$lib/store';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from')
|
||||
|
||||
export let sources: any;
|
||||
const filteredSources = sources.filter(
|
||||
(source: any) =>
|
||||
(source.type === 'github' && source.githubAppId && source.githubApp.installationId) ||
|
||||
(source.type === 'gitlab' && source.gitlabAppId)
|
||||
);
|
||||
const ownSources = filteredSources.filter((source: any) => {
|
||||
if (source.teams[0].id === $appSession.teamId) {
|
||||
return source;
|
||||
}
|
||||
});
|
||||
const otherSources = filteredSources.filter((source: any) => {
|
||||
if (source.teams[0].id !== $appSession.teamId) {
|
||||
return source;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit(gitSourceId: string) {
|
||||
try {
|
||||
await post(`/applications/${id}/configuration/source`, { gitSourceId });
|
||||
return await goto(from || `/applications/${id}/configuration/repository`);
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function newSource() {
|
||||
const { id } = await post('/sources/new', {});
|
||||
return await goto(`/sources/${id}`, { replaceState: true });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">
|
||||
{$t('application.configuration.select_a_git_source')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center">
|
||||
{#if !filteredSources || ownSources.length === 0}
|
||||
<div class="flex-col">
|
||||
<div class="pb-2 text-center font-bold">{$t('application.configuration.no_configurable_git')}</div>
|
||||
<div class="flex justify-center">
|
||||
<a href="/sources/new?from={$page.url.pathname}" 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-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each ownSources 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 otherSources.length > 0 && $appSession.teamId === '0'}
|
||||
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Sources</div>
|
||||
{/if}
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each otherSources 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">
|
||||
{$t('application.configuration.configuration_missing')}
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
750
apps/ui/src/routes/applications/[id]/index.svelte
Normal file
750
apps/ui/src/routes/applications/[id]/index.svelte
Normal file
@@ -0,0 +1,750 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, stuff, url }) => {
|
||||
try {
|
||||
if (stuff?.application?.id) {
|
||||
return {
|
||||
props: {
|
||||
application: stuff.application
|
||||
}
|
||||
};
|
||||
}
|
||||
const response = await get(`/applications/${params.id}`);
|
||||
return {
|
||||
props: {
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let application: any;
|
||||
import { page } from '$app/stores';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import Select from 'svelte-select';
|
||||
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { get, post } from '$lib/api';
|
||||
import cuid from 'cuid';
|
||||
import { browser } from '$app/env';
|
||||
import { appSession, disabledButton, status } from '$lib/store';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common';
|
||||
import Setting from './_Setting.svelte';
|
||||
const { id } = $page.params;
|
||||
|
||||
let domainEl: HTMLInputElement;
|
||||
|
||||
let loading = false;
|
||||
|
||||
let usageLoading = false;
|
||||
let usage = {
|
||||
MemUsage: 0,
|
||||
CPUPerc: 0,
|
||||
NetIO: 0
|
||||
};
|
||||
let usageInterval: any;
|
||||
|
||||
let forceSave = false;
|
||||
let debug = application.settings.debug;
|
||||
let previews = application.settings.previews;
|
||||
let dualCerts = application.settings.dualCerts;
|
||||
let autodeploy = application.settings.autodeploy;
|
||||
|
||||
let nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
|
||||
let isNonWWWDomainOK = false;
|
||||
let isWWWDomainOK = false;
|
||||
|
||||
$: isDisabled = !$appSession.isAdmin || $status.application.isRunning;
|
||||
let wsgis = [
|
||||
{
|
||||
value: 'None',
|
||||
label: 'None'
|
||||
},
|
||||
{
|
||||
value: 'Gunicorn',
|
||||
label: 'Gunicorn'
|
||||
},
|
||||
{
|
||||
value: 'Uvicorn',
|
||||
label: 'Uvicorn'
|
||||
}
|
||||
];
|
||||
function containerClass() {
|
||||
return 'text-white border border-dashed border-coolgray-300 bg-transparent font-thin px-0';
|
||||
}
|
||||
|
||||
async function getUsage() {
|
||||
if (usageLoading) return;
|
||||
if (!$status.application.isRunning) return;
|
||||
usageLoading = true;
|
||||
const data = await get(`/applications/${id}/usage`);
|
||||
usage = data.usage;
|
||||
usageLoading = false;
|
||||
}
|
||||
onDestroy(() => {
|
||||
clearInterval(usageInterval);
|
||||
});
|
||||
onMount(async () => {
|
||||
if (window.location.hostname === 'demo.coolify.io' && !application.fqdn) {
|
||||
application.fqdn = `http://${cuid()}.demo.coolify.io`;
|
||||
await handleSubmit();
|
||||
}
|
||||
domainEl.focus();
|
||||
await getUsage();
|
||||
usageInterval = setInterval(async () => {
|
||||
await getUsage();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
async function changeSettings(name: any) {
|
||||
if (name === 'debug') {
|
||||
debug = !debug;
|
||||
}
|
||||
if (name === 'previews') {
|
||||
previews = !previews;
|
||||
}
|
||||
if (name === 'dualCerts') {
|
||||
dualCerts = !dualCerts;
|
||||
}
|
||||
if (name === 'autodeploy') {
|
||||
autodeploy = !autodeploy;
|
||||
}
|
||||
try {
|
||||
await post(`/applications/${id}/settings`, {
|
||||
previews,
|
||||
debug,
|
||||
dualCerts,
|
||||
autodeploy,
|
||||
branch: application.branch,
|
||||
projectId: application.projectId
|
||||
});
|
||||
return toast.push($t('application.settings_saved'));
|
||||
} catch (error) {
|
||||
if (name === 'debug') {
|
||||
debug = !debug;
|
||||
}
|
||||
if (name === 'previews') {
|
||||
previews = !previews;
|
||||
}
|
||||
if (name === 'dualCerts') {
|
||||
dualCerts = !dualCerts;
|
||||
}
|
||||
if (name === 'autodeploy') {
|
||||
autodeploy = !autodeploy;
|
||||
}
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function handleSubmit() {
|
||||
if (loading) return;
|
||||
loading = true;
|
||||
try {
|
||||
nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
|
||||
await post(`/applications/${id}/check`, {
|
||||
fqdn: application.fqdn,
|
||||
forceSave,
|
||||
dualCerts,
|
||||
exposePort: application.exposePort
|
||||
});
|
||||
await post(`/applications/${id}`, { ...application });
|
||||
$disabledButton = false;
|
||||
forceSave = false;
|
||||
return toast.push('Configurations saved.');
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
//@ts-ignore
|
||||
if (error?.message.startsWith($t('application.dns_not_set_partial_error'))) {
|
||||
forceSave = true;
|
||||
if (dualCerts) {
|
||||
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
|
||||
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
|
||||
} else {
|
||||
const isWWW = getDomain(application.fqdn).includes('www.');
|
||||
if (isWWW) {
|
||||
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
|
||||
} else {
|
||||
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
async function selectWSGI(event: any) {
|
||||
application.pythonWSGI = event.detail.value;
|
||||
}
|
||||
async function selectBaseImage(event: any) {
|
||||
application.baseImage = event.detail.value;
|
||||
await handleSubmit();
|
||||
}
|
||||
async function selectBaseBuildImage(event: any) {
|
||||
application.baseBuildImage = event.detail.value;
|
||||
await handleSubmit();
|
||||
}
|
||||
|
||||
async function isDNSValid(domain: any, isWWW: any) {
|
||||
try {
|
||||
await get(`/applications/${id}/check?domain=${domain}`);
|
||||
toast.push('DNS configuration is valid.');
|
||||
isWWW ? (isWWWDomainOK = true) : (isNonWWWDomainOK = true);
|
||||
return true;
|
||||
} catch ({ error }) {
|
||||
errorNotification(error);
|
||||
isWWW ? (isWWWDomainOK = false) : (isNonWWWDomainOK = false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
|
||||
<div class="-mb-5 flex-col">
|
||||
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
|
||||
Configuration
|
||||
</div>
|
||||
<span class="text-xs">{application.name} </span>
|
||||
</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 py-4">
|
||||
<div class="text-2xl font-bold">Application Usage</div>
|
||||
<div class="mx-auto">
|
||||
<dl class="relative mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3">
|
||||
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left">
|
||||
<dt class=" text-sm font-medium text-white">Used Memory / Memory Limit</dt>
|
||||
<dd class="mt-1 text-xl font-semibold text-white">
|
||||
{usage?.MemUsage}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left">
|
||||
<dt class="truncate text-sm font-medium text-white">Used CPU</dt>
|
||||
<dd class="mt-1 text-xl font-semibold text-white ">
|
||||
{usage?.CPUPerc}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left">
|
||||
<dt class="truncate text-sm font-medium text-white">Network IO</dt>
|
||||
<dd class="mt-1 text-xl font-semibold text-white ">
|
||||
{usage?.NetIO}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</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">{$t('general')}</div>
|
||||
{#if $appSession.isAdmin}
|
||||
<button
|
||||
type="submit"
|
||||
class:bg-green-600={!loading}
|
||||
class:bg-orange-600={forceSave}
|
||||
class:hover:bg-green-500={!loading}
|
||||
class:hover:bg-orange-400={forceSave}
|
||||
disabled={loading}
|
||||
>{loading
|
||||
? $t('forms.saving')
|
||||
: forceSave
|
||||
? $t('forms.confirm_continue')
|
||||
: $t('forms.save')}</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grid grid-flow-row gap-2 px-10">
|
||||
<div class="mt-2 grid grid-cols-2 items-center">
|
||||
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
|
||||
<input
|
||||
readonly={!isDisabled}
|
||||
name="name"
|
||||
id="name"
|
||||
bind:value={application.name}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="gitSource" class="text-base font-bold text-stone-100"
|
||||
>{$t('application.git_source')}</label
|
||||
>
|
||||
<a
|
||||
href={!isDisabled
|
||||
? `/applications/${id}/configuration/source?from=/applications/${id}`
|
||||
: ''}
|
||||
class="no-underline"
|
||||
><input
|
||||
value={application.gitSource.name}
|
||||
id="gitSource"
|
||||
disabled
|
||||
class="cursor-pointer hover:bg-coolgray-500"
|
||||
/></a
|
||||
>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="repository" class="text-base font-bold text-stone-100"
|
||||
>{$t('application.git_repository')}</label
|
||||
>
|
||||
<a
|
||||
href={!isDisabled
|
||||
? `/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 hover:bg-coolgray-500"
|
||||
/></a
|
||||
>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="buildPack" class="text-base font-bold text-stone-100"
|
||||
>{$t('application.build_pack')}</label
|
||||
>
|
||||
<a
|
||||
href={!isDisabled
|
||||
? `/applications/${id}/configuration/buildpack?from=/applications/${id}`
|
||||
: ''}
|
||||
class="no-underline "
|
||||
>
|
||||
<input
|
||||
value={application.buildPack}
|
||||
id="buildPack"
|
||||
disabled
|
||||
class="cursor-pointer hover:bg-coolgray-500"
|
||||
/></a
|
||||
>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center pb-8">
|
||||
<label for="destination" class="text-base font-bold text-stone-100"
|
||||
>{$t('application.destination')}</label
|
||||
>
|
||||
<div class="no-underline">
|
||||
<input
|
||||
value={application.destinationDocker.name}
|
||||
id="destination"
|
||||
disabled
|
||||
class="bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if application.buildPack !== 'docker'}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="baseImage" class="text-base font-bold text-stone-100"
|
||||
>{$t('application.base_image')}</label
|
||||
>
|
||||
<div class="custom-select-wrapper">
|
||||
<Select
|
||||
{isDisabled}
|
||||
containerClasses={isDisabled && containerClass()}
|
||||
id="baseImages"
|
||||
showIndicator={!$status.application.isRunning}
|
||||
items={application.baseImages}
|
||||
on:select={selectBaseImage}
|
||||
value={application.baseImage}
|
||||
isClearable={false}
|
||||
/>
|
||||
</div>
|
||||
<Explainer text={$t('application.base_image_explainer')} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'}
|
||||
<div class="grid grid-cols-2 items-center pb-8">
|
||||
<label for="baseBuildImage" class="text-base font-bold text-stone-100"
|
||||
>{$t('application.base_build_image')}</label
|
||||
>
|
||||
|
||||
<div class="custom-select-wrapper">
|
||||
<Select
|
||||
{isDisabled}
|
||||
containerClasses={isDisabled && containerClass()}
|
||||
id="baseBuildImages"
|
||||
showIndicator={!$status.application.isRunning}
|
||||
items={application.baseBuildImages}
|
||||
on:select={selectBaseBuildImage}
|
||||
value={application.baseBuildImage}
|
||||
isClearable={false}
|
||||
/>
|
||||
</div>
|
||||
{#if application.buildPack === 'laravel'}
|
||||
<Explainer text="For building frontend assets with webpack." />
|
||||
{:else}
|
||||
<Explainer text={$t('application.base_build_image_explainer')} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">{$t('application.application')}</div>
|
||||
</div>
|
||||
<div class="grid grid-flow-row gap-2 px-10">
|
||||
<div class="grid grid-cols-2">
|
||||
<div class="flex-col">
|
||||
<label for="fqdn" class="pt-2 text-base font-bold text-stone-100"
|
||||
>{$t('application.url_fqdn')}</label
|
||||
>
|
||||
{#if browser && window.location.hostname === 'demo.coolify.io'}
|
||||
<Explainer
|
||||
text="<span class='text-white font-bold'>You can use the predefined random url name or enter your own domain name.</span>"
|
||||
/>
|
||||
{/if}
|
||||
<Explainer text={$t('application.https_explainer')} />
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
readonly={isDisabled}
|
||||
disabled={isDisabled}
|
||||
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"
|
||||
/>
|
||||
{#if forceSave}
|
||||
<div class="flex-col space-y-2 pt-4 text-center">
|
||||
{#if isNonWWWDomainOK}
|
||||
<button
|
||||
class="bg-green-600 hover:bg-green-500"
|
||||
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
|
||||
>DNS settings for {nonWWWDomain} is OK, click to recheck.</button
|
||||
>
|
||||
{:else}
|
||||
<button
|
||||
class="bg-red-600 hover:bg-red-500"
|
||||
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
|
||||
>DNS settings for {nonWWWDomain} is invalid, click to recheck.</button
|
||||
>
|
||||
{/if}
|
||||
{#if dualCerts}
|
||||
{#if isWWWDomainOK}
|
||||
<button
|
||||
class="bg-green-600 hover:bg-green-500"
|
||||
on:click|preventDefault={() =>
|
||||
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
|
||||
>DNS settings for www.{nonWWWDomain} is OK, click to recheck.</button
|
||||
>
|
||||
{:else}
|
||||
<button
|
||||
class="bg-red-600 hover:bg-red-500"
|
||||
on:click|preventDefault={() =>
|
||||
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
|
||||
>DNS settings for www.{nonWWWDomain} is invalid, click to recheck.</button
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center pb-8">
|
||||
<Setting
|
||||
dataTooltip={$t('forms.must_be_stopped_to_modify')}
|
||||
disabled={$status.application.isRunning}
|
||||
isCenter={false}
|
||||
bind:setting={dualCerts}
|
||||
title={$t('application.ssl_www_and_non_www')}
|
||||
description={$t('application.ssl_explainer')}
|
||||
on:click={() => !$status.application.isRunning && changeSettings('dualCerts')}
|
||||
/>
|
||||
</div>
|
||||
{#if application.buildPack === 'python'}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="pythonModule" class="text-base font-bold text-stone-100">WSGI / ASGI</label>
|
||||
<div class="custom-select-wrapper">
|
||||
<Select id="wsgi" items={wsgis} on:select={selectWSGI} value={application.pythonWSGI} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="pythonModule" class="text-base font-bold text-stone-100">Module</label>
|
||||
<input
|
||||
readonly={!$appSession.isAdmin}
|
||||
name="pythonModule"
|
||||
id="pythonModule"
|
||||
required
|
||||
bind:value={application.pythonModule}
|
||||
placeholder={application.pythonWSGI?.toLowerCase() !== 'none' ? 'main' : 'main.py'}
|
||||
/>
|
||||
</div>
|
||||
{#if application.pythonWSGI?.toLowerCase() === 'gunicorn'}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="pythonVariable" class="text-base font-bold text-stone-100">Variable</label>
|
||||
<input
|
||||
readonly={!$appSession.isAdmin}
|
||||
name="pythonVariable"
|
||||
id="pythonVariable"
|
||||
required
|
||||
bind:value={application.pythonVariable}
|
||||
placeholder="default: app"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if application.pythonWSGI?.toLowerCase() === 'uvicorn'}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="pythonVariable" class="text-base font-bold text-stone-100">Variable</label>
|
||||
<input
|
||||
readonly={!$appSession.isAdmin}
|
||||
name="pythonVariable"
|
||||
id="pythonVariable"
|
||||
required
|
||||
bind:value={application.pythonVariable}
|
||||
placeholder="default: app"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if !staticDeployments.includes(application.buildPack)}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="port" class="text-base font-bold text-stone-100">{$t('forms.port')}</label>
|
||||
<input
|
||||
readonly={!$appSession.isAdmin}
|
||||
name="port"
|
||||
id="port"
|
||||
bind:value={application.port}
|
||||
placeholder="{$t('forms.default')}: 'python' ? '8000' : '3000'"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if application.buildPack !== 'docker'}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="exposePort" class="text-base font-bold text-stone-100">Exposed Port</label>
|
||||
<input
|
||||
readonly={!$appSession.isAdmin && !$status.application.isRunning}
|
||||
disabled={isDisabled}
|
||||
name="exposePort"
|
||||
id="exposePort"
|
||||
bind:value={application.exposePort}
|
||||
placeholder="12345"
|
||||
/>
|
||||
<Explainer
|
||||
text={'You can expose your application to a port on the host system.<br><br>Useful if you would like to use your own reverse proxy or tunnel and also in development mode. Otherwise leave empty.'}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if !notNodeDeployments.includes(application.buildPack)}
|
||||
<div class="grid grid-cols-2 items-center pt-4">
|
||||
<label for="installCommand" class="text-base font-bold text-stone-100"
|
||||
>{$t('application.install_command')}</label
|
||||
>
|
||||
<input
|
||||
readonly={!$appSession.isAdmin}
|
||||
name="installCommand"
|
||||
id="installCommand"
|
||||
bind:value={application.installCommand}
|
||||
placeholder="{$t('forms.default')}: yarn install"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="buildCommand" class="text-base font-bold text-stone-100"
|
||||
>{$t('application.build_command')}</label
|
||||
>
|
||||
<input
|
||||
readonly={!$appSession.isAdmin}
|
||||
name="buildCommand"
|
||||
id="buildCommand"
|
||||
bind:value={application.buildCommand}
|
||||
placeholder="{$t('forms.default')}: yarn build"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="startCommand" class="text-base font-bold text-stone-100"
|
||||
>{$t('application.start_command')}</label
|
||||
>
|
||||
<input
|
||||
readonly={!$appSession.isAdmin}
|
||||
name="startCommand"
|
||||
id="startCommand"
|
||||
bind:value={application.startCommand}
|
||||
placeholder="{$t('forms.default')}: yarn start"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if application.buildPack === 'docker'}
|
||||
<div class="grid grid-cols-2 items-center pt-4">
|
||||
<label for="dockerFileLocation" class="text-base font-bold text-stone-100"
|
||||
>Dockerfile Location</label
|
||||
>
|
||||
<input
|
||||
readonly={!$appSession.isAdmin}
|
||||
name="dockerFileLocation"
|
||||
id="dockerFileLocation"
|
||||
bind:value={application.dockerFileLocation}
|
||||
placeholder="default: /Dockerfile"
|
||||
/>
|
||||
<Explainer
|
||||
text="Does not rely on Base Directory. <br>Should be absolute path, like <span class='text-green-500 font-bold'>/data/Dockerfile</span> or <span class='text-green-500 font-bold'>/Dockerfile.</span>"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if application.buildPack === 'deno'}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="denoMainFile" class="text-base font-bold text-stone-100">Main File</label>
|
||||
<input
|
||||
readonly={!$appSession.isAdmin}
|
||||
name="denoMainFile"
|
||||
id="denoMainFile"
|
||||
bind:value={application.denoMainFile}
|
||||
placeholder="default: main.ts"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="denoOptions" class="text-base font-bold text-stone-100">Arguments</label>
|
||||
<input
|
||||
readonly={!$appSession.isAdmin}
|
||||
name="denoOptions"
|
||||
id="denoOptions"
|
||||
bind:value={application.denoOptions}
|
||||
placeholder="eg: --allow-net --allow-hrtime --config path/to/file.json"
|
||||
/>
|
||||
<Explainer
|
||||
text="List of arguments to pass to <span class='text-green-500 font-bold'>deno run</span> command. Could include permissions, configurations files, etc."
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if application.buildPack !== 'laravel'}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<div class="flex-col">
|
||||
<label for="baseDirectory" class="pt-2 text-base font-bold text-stone-100"
|
||||
>{$t('forms.base_directory')}</label
|
||||
>
|
||||
<Explainer text={$t('application.directory_to_use_explainer')} />
|
||||
</div>
|
||||
<input
|
||||
readonly={!$appSession.isAdmin}
|
||||
name="baseDirectory"
|
||||
id="baseDirectory"
|
||||
bind:value={application.baseDirectory}
|
||||
placeholder="{$t('forms.default')}: /"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if !notNodeDeployments.includes(application.buildPack)}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<div class="flex-col">
|
||||
<label for="publishDirectory" class="pt-2 text-base font-bold text-stone-100"
|
||||
>{$t('forms.publish_directory')}</label
|
||||
>
|
||||
<Explainer text={$t('application.publish_directory_explainer')} />
|
||||
</div>
|
||||
|
||||
<input
|
||||
readonly={!$appSession.isAdmin}
|
||||
name="publishDirectory"
|
||||
id="publishDirectory"
|
||||
bind:value={application.publishDirectory}
|
||||
placeholder=" {$t('forms.default')}: /"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
<div class="flex space-x-1 pb-5 font-bold">
|
||||
<div class="title">{$t('application.features')}</div>
|
||||
</div>
|
||||
<div class="px-10 pb-10">
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
isCenter={false}
|
||||
bind:setting={autodeploy}
|
||||
on:click={() => changeSettings('autodeploy')}
|
||||
title={$t('application.enable_automatic_deployment')}
|
||||
description={$t('application.enable_auto_deploy_webhooks')}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
isCenter={false}
|
||||
bind:setting={previews}
|
||||
on:click={() => changeSettings('previews')}
|
||||
title={$t('application.enable_mr_pr_previews')}
|
||||
description={$t('application.enable_preview_deploy_mr_pr_requests')}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
isCenter={false}
|
||||
bind:setting={debug}
|
||||
on:click={() => changeSettings('debug')}
|
||||
title={$t('application.debug_logs')}
|
||||
description={$t('application.enable_debug_log_during_build')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
179
apps/ui/src/routes/applications/[id]/logs/_BuildLog.svelte
Normal file
179
apps/ui/src/routes/applications/[id]/logs/_BuildLog.svelte
Normal file
@@ -0,0 +1,179 @@
|
||||
<script lang="ts">
|
||||
export let buildId: any;
|
||||
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import { page } from '$app/stores';
|
||||
|
||||
import Loading from '$lib/components/Loading.svelte';
|
||||
import { get, post } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import LoadingLogs from '$lib/components/LoadingLogs.svelte';
|
||||
import { errorNotification } from '$lib/common';
|
||||
|
||||
let logs: any = [];
|
||||
let loading = true;
|
||||
let currentStatus: any;
|
||||
let streamInterval: any;
|
||||
let followingBuild: any;
|
||||
let followingInterval: any;
|
||||
let logsEl: any;
|
||||
|
||||
let cancelInprogress = false;
|
||||
|
||||
const { id } = $page.params;
|
||||
|
||||
const cleanAnsiCodes = (str: string) => str.replace(/\x1B\[(\d+)m/g, '');
|
||||
|
||||
function followBuild() {
|
||||
followingBuild = !followingBuild;
|
||||
if (followingBuild) {
|
||||
followingInterval = setInterval(() => {
|
||||
logsEl.scrollTop = logsEl.scrollHeight;
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
}, 100);
|
||||
} else {
|
||||
window.clearInterval(followingInterval);
|
||||
}
|
||||
}
|
||||
async function streamLogs(sequence = 0) {
|
||||
try {
|
||||
let { logs: responseLogs, status } = await get(
|
||||
`/applications/${id}/logs/build/${buildId}?sequence=${sequence}`
|
||||
);
|
||||
currentStatus = status;
|
||||
logs = logs.concat(
|
||||
responseLogs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) }))
|
||||
);
|
||||
loading = false;
|
||||
streamInterval = setInterval(async () => {
|
||||
if (status !== 'running' && status !== 'queued') {
|
||||
clearInterval(streamInterval);
|
||||
return;
|
||||
}
|
||||
const nextSequence = logs[logs.length - 1]?.time || 0;
|
||||
try {
|
||||
const data = await get(
|
||||
`/applications/${id}/logs/build/${buildId}?sequence=${nextSequence}`
|
||||
);
|
||||
status = data.status;
|
||||
currentStatus = status;
|
||||
|
||||
logs = logs.concat(
|
||||
data.logs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) }))
|
||||
);
|
||||
dispatch('updateBuildStatus', { status });
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function cancelBuild() {
|
||||
if (cancelInprogress) return;
|
||||
try {
|
||||
cancelInprogress = true;
|
||||
await post(`/applications/${id}/cancel`, {
|
||||
buildId,
|
||||
applicationId: id
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
onDestroy(() => {
|
||||
clearInterval(streamInterval);
|
||||
clearInterval(followingInterval);
|
||||
});
|
||||
onMount(async () => {
|
||||
window.scrollTo(0, 0);
|
||||
await streamLogs();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if loading}
|
||||
<Loading />
|
||||
{:else}
|
||||
<div class="relative ">
|
||||
{#if currentStatus === 'running'}
|
||||
<LoadingLogs />
|
||||
{/if}
|
||||
{#if currentStatus === 'queued'}
|
||||
<div class="text-center font-bold text-xl">{$t('application.build.queued_waiting_exec')}</div>
|
||||
{:else}
|
||||
<div class="flex justify-end sticky top-0 p-2">
|
||||
<button
|
||||
on:click={followBuild}
|
||||
class="bg-transparent hover:text-green-500 hover:bg-coolgray-500"
|
||||
data-tooltip="Follow logs"
|
||||
class:text-green-500={followingBuild}
|
||||
>
|
||||
<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="12" cy="12" r="9" />
|
||||
<line x1="8" y1="12" x2="12" y2="16" />
|
||||
<line x1="12" y1="8" x2="12" y2="16" />
|
||||
<line x1="16" y1="12" x2="12" y2="16" />
|
||||
</svg>
|
||||
</button>
|
||||
{#if currentStatus === 'running'}
|
||||
<button
|
||||
on:click={cancelBuild}
|
||||
class:animation-spin={cancelInprogress}
|
||||
class="bg-transparent hover:text-red-500 hover:bg-coolgray-500"
|
||||
data-tooltip="Cancel build"
|
||||
>
|
||||
{#if cancelInprogress}
|
||||
Cancelling...
|
||||
{:else}
|
||||
<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="12" cy="12" r="9" />
|
||||
<path d="M10 10l4 4m0 -4l-4 4" />
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if logs.length > 0}
|
||||
<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 overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
|
||||
bind:this={logsEl}
|
||||
>
|
||||
{#each logs as log}
|
||||
<div>{log.line + '\n'}</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<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 overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
|
||||
>
|
||||
No logs found.
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
218
apps/ui/src/routes/applications/[id]/logs/build.svelte
Normal file
218
apps/ui/src/routes/applications/[id]/logs/build.svelte
Normal file
@@ -0,0 +1,218 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
try {
|
||||
const response = await get(`/applications/${params.id}/logs/build?skip=0`);
|
||||
return {
|
||||
props: {
|
||||
application: stuff.application,
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let builds: any;
|
||||
export let application: any;
|
||||
export let buildCount: any;
|
||||
import { page } from '$app/stores';
|
||||
|
||||
import BuildLog from './_BuildLog.svelte';
|
||||
import { get } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { changeQueryParams, dateOptions, errorNotification } from '$lib/common';
|
||||
|
||||
let buildId: any;
|
||||
|
||||
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 }: { detail: any }) {
|
||||
const { status } = detail;
|
||||
if (status !== 'running') {
|
||||
try {
|
||||
const data = await get(`/applications/${id}/logs/build?buildId=${buildId}`);
|
||||
builds = builds.filter((build: any) => {
|
||||
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;
|
||||
}
|
||||
return build;
|
||||
});
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
} else {
|
||||
builds = builds.filter((build: any) => {
|
||||
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?skip=${skip}`);
|
||||
builds = builds.concat(data.builds);
|
||||
return;
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
} else {
|
||||
noMoreBuilds = true;
|
||||
}
|
||||
}
|
||||
function loadBuild(build: any) {
|
||||
buildId = build;
|
||||
return changeQueryParams(buildId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
|
||||
<div class="-mb-5 flex-col">
|
||||
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
|
||||
{$t('application.build_logs')}
|
||||
</div>
|
||||
<span class="text-xs">{application.name} </span>
|
||||
</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="block flex-row justify-start space-x-2 px-5 pt-6 sm:px-10 md:flex">
|
||||
<div class="mb-4 min-w-[16rem] space-y-2 md:mb-0 ">
|
||||
<div class="top-4 md:sticky">
|
||||
{#each builds as build, index (build.id)}
|
||||
<div
|
||||
data-tooltip={new Intl.DateTimeFormat('default', dateOptions).format(
|
||||
new Date(build.createdAt)
|
||||
) + `\n${build.status}`}
|
||||
on:click={() => loadBuild(build.id)}
|
||||
class:rounded-tr={index === 0}
|
||||
class:rounded-br={index === builds.length - 1}
|
||||
class="tooltip-top flex cursor-pointer items-center justify-center border-l-2 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 === 'running'}
|
||||
>
|
||||
<div class="flex-col px-2">
|
||||
<div class="text-sm font-bold">
|
||||
{build.branch || 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">{$t('application.build.running')}</div>
|
||||
{:else if build.status === 'queued'}
|
||||
<div class="font-bold">{$t('application.build.queued')}</div>
|
||||
{:else}
|
||||
<div>{build.since}</div>
|
||||
<div>
|
||||
{$t('application.build.finished_in')} <span class="font-bold">{build.took}s</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if !noMoreBuilds}
|
||||
{#if buildCount > 5}
|
||||
<div class="flex space-x-2">
|
||||
<button disabled={noMoreBuilds} class="w-full" on:click={loadMoreBuilds}
|
||||
>{$t('application.build.load_more')}</button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-1 md:w-96">
|
||||
{#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">{$t('application.build.no_logs')}</div>
|
||||
{/if}
|
||||
219
apps/ui/src/routes/applications/[id]/logs/index.svelte
Normal file
219
apps/ui/src/routes/applications/[id]/logs/index.svelte
Normal file
@@ -0,0 +1,219 @@
|
||||
<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 }) => {
|
||||
try {
|
||||
const response = await get(`/applications/${params.id}/logs`);
|
||||
return {
|
||||
props: {
|
||||
application: stuff.application,
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let application: any;
|
||||
import { page } from '$app/stores';
|
||||
import { get } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import LoadingLogs from '$lib/components/LoadingLogs.svelte';
|
||||
|
||||
let loadLogsInterval: any = null;
|
||||
let logs: any = [];
|
||||
let lastLog: any = null;
|
||||
let followingInterval: any;
|
||||
let followingLogs: any;
|
||||
let logsEl: any;
|
||||
let position = 0;
|
||||
|
||||
const { id } = $page.params;
|
||||
onMount(async () => {
|
||||
loadAllLogs();
|
||||
loadLogsInterval = setInterval(() => {
|
||||
loadLogs();
|
||||
}, 1000);
|
||||
});
|
||||
onDestroy(() => {
|
||||
clearInterval(loadLogsInterval);
|
||||
clearInterval(followingInterval);
|
||||
});
|
||||
async function loadAllLogs() {
|
||||
try {
|
||||
const data: any = await get(`/applications/${id}/logs`);
|
||||
if (data?.logs) {
|
||||
lastLog = data.logs[data.logs.length - 1];
|
||||
logs = data.logs;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function loadLogs() {
|
||||
try {
|
||||
const newLogs: any = await get(
|
||||
`/applications/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`
|
||||
);
|
||||
|
||||
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
|
||||
logs = logs.concat(newLogs.logs);
|
||||
lastLog = newLogs.logs[newLogs.logs.length - 1];
|
||||
}
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
function detect() {
|
||||
if (position < logsEl.scrollTop) {
|
||||
position = logsEl.scrollTop;
|
||||
} else {
|
||||
if (followingLogs) {
|
||||
clearInterval(followingInterval);
|
||||
followingLogs = false;
|
||||
}
|
||||
position = logsEl.scrollTop;
|
||||
}
|
||||
}
|
||||
|
||||
function followBuild() {
|
||||
followingLogs = !followingLogs;
|
||||
if (followingLogs) {
|
||||
followingInterval = setInterval(() => {
|
||||
logsEl.scrollTop = logsEl.scrollHeight;
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
}, 1000);
|
||||
} else {
|
||||
clearInterval(followingInterval);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold">
|
||||
<div class="-mb-5 flex-col">
|
||||
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
|
||||
Application Logs
|
||||
</div>
|
||||
<span class="text-xs">{application.name}</span>
|
||||
</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="flex flex-row justify-center space-x-2 px-10 pt-6">
|
||||
{#if logs.length === 0}
|
||||
<div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div>
|
||||
{:else}
|
||||
<div class="relative w-full">
|
||||
<div class="text-right " />
|
||||
{#if loadLogsInterval}
|
||||
<LoadingLogs />
|
||||
{/if}
|
||||
<div class="flex justify-end sticky top-0 p-2 mx-1">
|
||||
<button
|
||||
on:click={followBuild}
|
||||
class="bg-transparent"
|
||||
data-tooltip="Follow logs"
|
||||
class:text-green-500={followingLogs}
|
||||
>
|
||||
<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="12" cy="12" r="9" />
|
||||
<line x1="8" y1="12" x2="12" y2="16" />
|
||||
<line x1="12" y1="8" x2="12" y2="16" />
|
||||
<line x1="16" y1="12" x2="12" y2="16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="font-mono w-full leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
|
||||
bind:this={logsEl}
|
||||
on:scroll={detect}
|
||||
>
|
||||
<div class="px-2 pr-14">
|
||||
{#each logs as log}
|
||||
{log + '\n'}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
196
apps/ui/src/routes/applications/[id]/previews.svelte
Normal file
196
apps/ui/src/routes/applications/[id]/previews.svelte
Normal file
@@ -0,0 +1,196 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, stuff, url }) => {
|
||||
try {
|
||||
const response = await get(`/applications/${params.id}/previews`);
|
||||
return {
|
||||
props: {
|
||||
application: stuff.application,
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let containers: any;
|
||||
export let application: any;
|
||||
export let PRMRSecrets: any;
|
||||
export let applicationSecrets: any;
|
||||
import Secret from './_Secret.svelte';
|
||||
import { get, post } from '$lib/api';
|
||||
import { page } from '$app/stores';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { t } from '$lib/translations';
|
||||
import { goto } from '$app/navigation';
|
||||
import { errorNotification, getDomain } from '$lib/common';
|
||||
|
||||
const { id } = $page.params;
|
||||
async function refreshSecrets() {
|
||||
const data = await get(`/applications/${id}/secrets`);
|
||||
PRMRSecrets = [...data.secrets];
|
||||
}
|
||||
async function redeploy(container: any) {
|
||||
try {
|
||||
const { buildId } = await post(`/applications/${id}/deploy`, {
|
||||
pullmergeRequestId: container.pullmergeRequestId,
|
||||
branch: container.branch
|
||||
});
|
||||
toast.push('Deployment queued');
|
||||
if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) {
|
||||
return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`);
|
||||
} else {
|
||||
return await goto(`/applications/${id}/logs/build?buildId=${buildId}`, {
|
||||
replaceState: true
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
|
||||
<div class="-mb-5 flex-col">
|
||||
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
|
||||
Preview Deployments
|
||||
</div>
|
||||
<span class="text-xs">{application.name} </span>
|
||||
</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-6xl px-6 pt-4">
|
||||
<div class="flex justify-center py-4 text-center">
|
||||
<Explainer
|
||||
customClass="w-full"
|
||||
text={applicationSecrets.length === 0
|
||||
? "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."
|
||||
: "These values overwrite application secrets in PR/MR deployments. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."}
|
||||
/>
|
||||
</div>
|
||||
{#if applicationSecrets.length !== 0}
|
||||
<table class="mx-auto border-separate text-left">
|
||||
<thead>
|
||||
<tr class="h-12">
|
||||
<th scope="col">{$t('forms.name')}</th>
|
||||
<th scope="col">{$t('forms.value')}</th>
|
||||
<th scope="col" class="w-64 text-center"
|
||||
>{$t('application.preview.need_during_buildtime')}</th
|
||||
>
|
||||
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each applicationSecrets as secret}
|
||||
{#key secret.id}
|
||||
<tr>
|
||||
<Secret
|
||||
PRMRSecret={PRMRSecrets.find((s) => s.name === secret.name)}
|
||||
isPRMRSecret
|
||||
name={secret.name}
|
||||
value={secret.value}
|
||||
isBuildSecret={secret.isBuildSecret}
|
||||
on:refresh={refreshSecrets}
|
||||
/>
|
||||
</tr>
|
||||
{/key}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-4xl py-10">
|
||||
<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>
|
||||
<div class="flex items-center justify-center">
|
||||
<button class="bg-coollabs hover:bg-coollabs-100" on:click={() => redeploy(container)}
|
||||
>{$t('application.preview.redeploy')}</button
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="flex-col">
|
||||
<div class="text-center font-bold text-xl">
|
||||
{$t('application.preview.no_previews_available')}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
176
apps/ui/src/routes/applications/[id]/secrets.svelte
Normal file
176
apps/ui/src/routes/applications/[id]/secrets.svelte
Normal file
@@ -0,0 +1,176 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, stuff, url }) => {
|
||||
try {
|
||||
const response = await get(`/applications/${params.id}/secrets`);
|
||||
return {
|
||||
props: {
|
||||
application: stuff.application,
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let secrets: any;
|
||||
export let application: any;
|
||||
import pLimit from 'p-limit';
|
||||
import Secret from './_Secret.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { t } from '$lib/translations';
|
||||
import { get } from '$lib/api';
|
||||
import { saveSecret } from './utils';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
|
||||
const limit = pLimit(1);
|
||||
const { id } = $page.params;
|
||||
|
||||
let batchSecrets = '';
|
||||
async function refreshSecrets() {
|
||||
const data = await get(`/applications/${id}/secrets`);
|
||||
secrets = [...data.secrets];
|
||||
}
|
||||
async function getValues(e: any) {
|
||||
e.preventDefault();
|
||||
const eachValuePair = batchSecrets.split('\n');
|
||||
const batchSecretsPairs = eachValuePair
|
||||
.filter((secret) => !secret.startsWith('#') && secret)
|
||||
.map((secret) => {
|
||||
const [name, value] = secret.split('=');
|
||||
const cleanValue = value?.replaceAll('"', '') || '';
|
||||
return {
|
||||
name,
|
||||
value: cleanValue,
|
||||
isNew: !secrets.find((secret: any) => name === secret.name)
|
||||
};
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
batchSecretsPairs.map(({ name, value, isNew }) =>
|
||||
limit(() => saveSecret({ name, value, applicationId: id, isNew }))
|
||||
)
|
||||
);
|
||||
batchSecrets = '';
|
||||
await refreshSecrets();
|
||||
toast.push('Secrets saved.');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
|
||||
<div class="-mb-5 flex-col">
|
||||
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
|
||||
{$t('application.secret')}
|
||||
</div>
|
||||
<span class="text-xs">{application.name} </span>
|
||||
</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-6xl px-6 pt-4">
|
||||
<table class="mx-auto border-separate text-left">
|
||||
<thead>
|
||||
<tr class="h-12">
|
||||
<th scope="col">{$t('forms.name')}</th>
|
||||
<th scope="col">{$t('forms.value')}</th>
|
||||
<th scope="col" class="w-64 text-center"
|
||||
>{$t('application.preview.need_during_buildtime')}</th
|
||||
>
|
||||
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each secrets as secret}
|
||||
{#key secret.id}
|
||||
<tr>
|
||||
<Secret
|
||||
name={secret.name}
|
||||
value={secret.value}
|
||||
isBuildSecret={secret.isBuildSecret}
|
||||
on:refresh={refreshSecrets}
|
||||
/>
|
||||
</tr>
|
||||
{/key}
|
||||
{/each}
|
||||
<tr>
|
||||
<Secret isNewSecret on:refresh={refreshSecrets} />
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 class="title my-6 font-bold">Paste .env file</h2>
|
||||
<form on:submit|preventDefault={getValues} class="mb-12 w-full">
|
||||
<textarea bind:value={batchSecrets} class="mb-2 min-h-[200px] w-full" />
|
||||
<button
|
||||
class="bg-green-600 hover:bg-green-500 disabled:text-white disabled:opacity-40"
|
||||
type="submit">Batch add secrets</button
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
133
apps/ui/src/routes/applications/[id]/storages.svelte
Normal file
133
apps/ui/src/routes/applications/[id]/storages.svelte
Normal file
@@ -0,0 +1,133 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ params, stuff, url }) => {
|
||||
try {
|
||||
const response = await get(`/applications/${params.id}/storages`);
|
||||
return {
|
||||
props: {
|
||||
application: stuff.application,
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let application: any;
|
||||
export let persistentStorages: any;
|
||||
import { page } from '$app/stores';
|
||||
import Storage from './_Storage.svelte';
|
||||
import { get } from '$lib/api';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
|
||||
const { id } = $page.params;
|
||||
async function refreshStorage() {
|
||||
const data = await get(`/applications/${id}/storages`);
|
||||
persistentStorages = [...data.persistentStorages];
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
|
||||
<div class="-mb-5 flex-col">
|
||||
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
|
||||
Persistent Storage
|
||||
</div>
|
||||
<span class="text-xs">{application.name} </span>
|
||||
</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-6xl rounded-xl px-6 pt-4">
|
||||
<div class="flex justify-center py-4 text-center">
|
||||
<Explainer customClass="w-full" text={$t('application.storage.persistent_storage_explainer')} />
|
||||
</div>
|
||||
<table class="mx-auto border-separate text-left">
|
||||
<thead>
|
||||
<tr class="h-12">
|
||||
<th scope="col">{$t('forms.path')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each persistentStorages as storage}
|
||||
{#key storage.id}
|
||||
<tr>
|
||||
<Storage on:refresh={refreshStorage} {storage} />
|
||||
</tr>
|
||||
{/key}
|
||||
{/each}
|
||||
<tr>
|
||||
<Storage on:refresh={refreshStorage} isNew />
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
43
apps/ui/src/routes/applications/[id]/utils.ts
Normal file
43
apps/ui/src/routes/applications/[id]/utils.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { post } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
|
||||
type Props = {
|
||||
isNew: boolean;
|
||||
name: string;
|
||||
value: string;
|
||||
isBuildSecret?: boolean;
|
||||
isPRMRSecret?: boolean;
|
||||
isNewSecret?: boolean;
|
||||
applicationId: string;
|
||||
};
|
||||
|
||||
export async function saveSecret({
|
||||
isNew,
|
||||
name,
|
||||
value,
|
||||
isBuildSecret,
|
||||
isPRMRSecret,
|
||||
isNewSecret,
|
||||
applicationId
|
||||
}: Props): Promise<void> {
|
||||
if (!name) return errorNotification(`${t.get('forms.name')} ${t.get('forms.is_required')}`);
|
||||
if (!value) return errorNotification(`${t.get('forms.value')} ${t.get('forms.is_required')}`);
|
||||
try {
|
||||
await post(`/applications/${applicationId}/secrets`, {
|
||||
name,
|
||||
value,
|
||||
isBuildSecret,
|
||||
isPRMRSecret,
|
||||
isNew: isNew || false
|
||||
});
|
||||
if (isNewSecret) {
|
||||
name = '';
|
||||
value = '';
|
||||
isBuildSecret = false;
|
||||
}
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
223
apps/ui/src/routes/applications/index.svelte
Normal file
223
apps/ui/src/routes/applications/index.svelte
Normal file
@@ -0,0 +1,223 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async () => {
|
||||
try {
|
||||
const response = await get(`/applications`);
|
||||
return {
|
||||
props: {
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(error)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let applications: Array<any> = [];
|
||||
let ownApplications: Array<any> = [];
|
||||
let otherApplications: Array<any> = [];
|
||||
import { get, post } from '$lib/api';
|
||||
import { goto } from '$app/navigation';
|
||||
import { t } from '$lib/translations';
|
||||
import { getDomain } from '$lib/common';
|
||||
import { appSession } from '$lib/store';
|
||||
|
||||
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';
|
||||
import Astro from '$lib/components/svg/applications/Astro.svelte';
|
||||
import Eleventy from '$lib/components/svg/applications/Eleventy.svelte';
|
||||
import Deno from '$lib/components/svg/applications/Deno.svelte';
|
||||
import Laravel from '$lib/components/svg/applications/Laravel.svelte';
|
||||
ownApplications = applications.filter((application) => {
|
||||
if (application.teams[0].id === $appSession.teamId) {
|
||||
return application;
|
||||
}
|
||||
});
|
||||
otherApplications = applications.filter((application) => {
|
||||
if (application.teams[0].id !== $appSession.teamId) {
|
||||
return application;
|
||||
}
|
||||
});
|
||||
async function newApplication() {
|
||||
const { id } = await post('/applications/new', {});
|
||||
return await goto(`/applications/${id}`, { replaceState: true });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl ">{$t('index.applications')}</div>
|
||||
{#if $appSession.isAdmin}
|
||||
<button
|
||||
on:click={newApplication}
|
||||
class="add-icon cursor-pointer 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
|
||||
>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-col justify-center">
|
||||
{#if !applications || ownApplications.length === 0}
|
||||
<div class="flex-col">
|
||||
<div class="text-center text-xl font-bold">{$t('application.no_applications_found')}</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if ownApplications.length > 0 || otherApplications.length > 0}
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each ownApplications as application}
|
||||
<a href="/applications/{application.id}" class="w-96 p-2 no-underline">
|
||||
<div class="box-selection group relative hover:bg-green-600">
|
||||
{#if application.buildPack}
|
||||
{#if application.buildPack.toLowerCase() === 'rust'}
|
||||
<Rust />
|
||||
{:else if application.buildPack.toLowerCase() === 'node'}
|
||||
<Nodejs />
|
||||
{:else if application.buildPack.toLowerCase() === 'react'}
|
||||
<React />
|
||||
{:else if application.buildPack.toLowerCase() === 'svelte'}
|
||||
<Svelte />
|
||||
{:else if application.buildPack.toLowerCase() === 'vuejs'}
|
||||
<Vuejs />
|
||||
{:else if application.buildPack.toLowerCase() === 'php'}
|
||||
<PHP />
|
||||
{:else if application.buildPack.toLowerCase() === 'python'}
|
||||
<Python />
|
||||
{:else if application.buildPack.toLowerCase() === 'static'}
|
||||
<Static />
|
||||
{:else if application.buildPack.toLowerCase() === 'nestjs'}
|
||||
<Nestjs />
|
||||
{:else if application.buildPack.toLowerCase() === 'nuxtjs'}
|
||||
<Nuxtjs />
|
||||
{:else if application.buildPack.toLowerCase() === 'nextjs'}
|
||||
<Nextjs />
|
||||
{:else if application.buildPack.toLowerCase() === 'gatsby'}
|
||||
<Gatsby />
|
||||
{:else if application.buildPack.toLowerCase() === 'docker'}
|
||||
<Docker />
|
||||
{:else if application.buildPack.toLowerCase() === 'astro'}
|
||||
<Astro />
|
||||
{:else if application.buildPack.toLowerCase() === 'eleventy'}
|
||||
<Eleventy />
|
||||
{:else if application.buildPack.toLowerCase() === 'deno'}
|
||||
<Deno />
|
||||
{:else if application.buildPack.toLowerCase() === 'laravel'}
|
||||
<Laravel />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="truncate text-center text-xl font-bold">{application.name}</div>
|
||||
{#if $appSession.teamId === '0' && otherApplications.length > 0}
|
||||
<div class="truncate text-center">Team {application.teams[0].name}</div>
|
||||
{/if}
|
||||
{#if application.fqdn}
|
||||
<div class="truncate text-center">{getDomain(application.fqdn) || ''}</div>
|
||||
{/if}
|
||||
{#if !application.gitSourceId}
|
||||
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
|
||||
Git Source Missing
|
||||
</div>
|
||||
{:else if !application.destinationDockerId}
|
||||
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
|
||||
Destination Missing
|
||||
</div>
|
||||
{:else if !application.fqdn}
|
||||
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
|
||||
URL Missing
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{#if otherApplications.length > 0 && $appSession.teamId === '0'}
|
||||
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Applications</div>
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each otherApplications as application}
|
||||
<a href="/applications/{application.id}" class="w-96 p-2 no-underline">
|
||||
<div class="box-selection group relative hover:bg-green-600">
|
||||
{#if application.buildPack}
|
||||
{#if application.buildPack.toLowerCase() === 'rust'}
|
||||
<Rust />
|
||||
{:else if application.buildPack.toLowerCase() === 'node'}
|
||||
<Nodejs />
|
||||
{:else if application.buildPack.toLowerCase() === 'react'}
|
||||
<React />
|
||||
{:else if application.buildPack.toLowerCase() === 'svelte'}
|
||||
<Svelte />
|
||||
{:else if application.buildPack.toLowerCase() === 'vuejs'}
|
||||
<Vuejs />
|
||||
{:else if application.buildPack.toLowerCase() === 'php'}
|
||||
<PHP />
|
||||
{:else if application.buildPack.toLowerCase() === 'python'}
|
||||
<Python />
|
||||
{:else if application.buildPack.toLowerCase() === 'static'}
|
||||
<Static />
|
||||
{:else if application.buildPack.toLowerCase() === 'nestjs'}
|
||||
<Nestjs />
|
||||
{:else if application.buildPack.toLowerCase() === 'nuxtjs'}
|
||||
<Nuxtjs />
|
||||
{:else if application.buildPack.toLowerCase() === 'nextjs'}
|
||||
<Nextjs />
|
||||
{:else if application.buildPack.toLowerCase() === 'gatsby'}
|
||||
<Gatsby />
|
||||
{:else if application.buildPack.toLowerCase() === 'docker'}
|
||||
<Docker />
|
||||
{:else if application.buildPack.toLowerCase() === 'astro'}
|
||||
<Astro />
|
||||
{:else if application.buildPack.toLowerCase() === 'eleventy'}
|
||||
<Eleventy />
|
||||
{:else if application.buildPack.toLowerCase() === 'deno'}
|
||||
<Deno />
|
||||
{:else if application.buildPack.toLowerCase() === 'laravel'}
|
||||
<Laravel />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="truncate text-center text-xl font-bold">{application.name}</div>
|
||||
{#if $appSession.teamId === '0'}
|
||||
<div class="truncate text-center">Team {application.teams[0].name}</div>
|
||||
{/if}
|
||||
{#if application.fqdn}
|
||||
<div class="truncate text-center">{getDomain(application.fqdn) || ''}</div>
|
||||
{/if}
|
||||
{#if !application.gitSourceId || !application.destinationDockerId || !application.fqdn}
|
||||
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
|
||||
Configuration missing
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
29
apps/ui/src/routes/databases/[id]/_DatabaseLinks.svelte
Normal file
29
apps/ui/src/routes/databases/[id]/_DatabaseLinks.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
export let database: any;
|
||||
|
||||
import Clickhouse from '$lib/components/svg/databases/Clickhouse.svelte';
|
||||
import CouchDb from '$lib/components/svg/databases/CouchDB.svelte';
|
||||
import MariaDb from '$lib/components/svg/databases/MariaDB.svelte';
|
||||
import MongoDb from '$lib/components/svg/databases/MongoDB.svelte';
|
||||
import MySql from '$lib/components/svg/databases/MySQL.svelte';
|
||||
import PostgreSql from '$lib/components/svg/databases/PostgreSQL.svelte';
|
||||
import Redis from '$lib/components/svg/databases/Redis.svelte';
|
||||
</script>
|
||||
|
||||
<span class="relative">
|
||||
{#if database.type === 'clickhouse'}
|
||||
<Clickhouse />
|
||||
{:else if database.type === 'couchdb'}
|
||||
<CouchDb />
|
||||
{:else if database.type === 'mongodb'}
|
||||
<MongoDb />
|
||||
{:else if database.type === 'mysql'}
|
||||
<MySql />
|
||||
{:else if database.type === 'mariadb'}
|
||||
<MariaDb />
|
||||
{:else if database.type === 'postgresql'}
|
||||
<PostgreSql />
|
||||
{:else if database.type === 'redis'}
|
||||
<Redis />
|
||||
{/if}
|
||||
</span>
|
||||
75
apps/ui/src/routes/databases/[id]/_Databases/_CouchDb.svelte
Normal file
75
apps/ui/src/routes/databases/[id]/_Databases/_CouchDb.svelte
Normal file
@@ -0,0 +1,75 @@
|
||||
<script lang="ts">
|
||||
export let database: any;
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">CouchDB</div>
|
||||
</div>
|
||||
<div class="space-y-2 px-10">
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="defaultDatabase" class="text-base font-bold text-stone-100"
|
||||
>{$t('database.default_database')}</label
|
||||
>
|
||||
<CopyPasswordField
|
||||
required
|
||||
readonly={database.defaultDatabase}
|
||||
disabled={database.defaultDatabase}
|
||||
placeholder="{$t('forms.eg')}: mydb"
|
||||
id="defaultDatabase"
|
||||
name="defaultDatabase"
|
||||
bind:value={database.defaultDatabase}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="dbUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label>
|
||||
<CopyPasswordField
|
||||
readonly
|
||||
disabled
|
||||
placeholder={$t('forms.generated_automatically_after_start')}
|
||||
id="dbUser"
|
||||
name="dbUser"
|
||||
value={database.dbUser}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="dbUserPassword" class="text-base font-bold text-stone-100"
|
||||
>{$t('forms.password')}</label
|
||||
>
|
||||
<CopyPasswordField
|
||||
readonly
|
||||
disabled
|
||||
placeholder={$t('forms.generated_automatically_after_start')}
|
||||
isPasswordField
|
||||
id="dbUserPassword"
|
||||
name="dbUserPassword"
|
||||
value={database.dbUserPassword}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label>
|
||||
<CopyPasswordField
|
||||
readonly
|
||||
disabled
|
||||
placeholder={$t('forms.generated_automatically_after_start')}
|
||||
id="rootUser"
|
||||
name="rootUser"
|
||||
value={database.rootUser}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="rootUserPassword" class="text-base font-bold text-stone-100"
|
||||
>{$t('forms.roots_password')}</label
|
||||
>
|
||||
<CopyPasswordField
|
||||
readonly
|
||||
disabled
|
||||
placeholder={$t('forms.generated_automatically_after_start')}
|
||||
isPasswordField
|
||||
id="rootUserPassword"
|
||||
name="rootUserPassword"
|
||||
value={database.rootUserPassword}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
242
apps/ui/src/routes/databases/[id]/_Databases/_Databases.svelte
Normal file
242
apps/ui/src/routes/databases/[id]/_Databases/_Databases.svelte
Normal file
@@ -0,0 +1,242 @@
|
||||
<script lang="ts">
|
||||
export let database: any;
|
||||
export let privatePort: any;
|
||||
export let settings: any;
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Setting from '$lib/components/Setting.svelte';
|
||||
|
||||
import MySql from './_MySQL.svelte';
|
||||
import MongoDb from './_MongoDB.svelte';
|
||||
import MariaDb from './_MariaDB.svelte';
|
||||
import PostgreSql from './_PostgreSQL.svelte';
|
||||
import Redis from './_Redis.svelte';
|
||||
import CouchDb from './_CouchDb.svelte';
|
||||
import { post } from '$lib/api';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification, getDomain } from '$lib/common';
|
||||
import { appSession, status } from '$lib/store';
|
||||
|
||||
const { id } = $page.params;
|
||||
|
||||
let loading = false;
|
||||
let publicLoading = false;
|
||||
|
||||
let isPublic = database.settings.isPublic || false;
|
||||
let appendOnly = database.settings.appendOnly;
|
||||
|
||||
let databaseDefault: any;
|
||||
let databaseDbUser: any;
|
||||
let databaseDbUserPassword: any;
|
||||
|
||||
generateDbDetails();
|
||||
|
||||
function generateDbDetails() {
|
||||
databaseDefault = database.defaultDatabase;
|
||||
databaseDbUser = database.dbUser;
|
||||
databaseDbUserPassword = database.dbUserPassword;
|
||||
if (database.type === 'mongodb') {
|
||||
databaseDefault = '?readPreference=primary&ssl=false';
|
||||
databaseDbUser = database.rootUser;
|
||||
databaseDbUserPassword = database.rootUserPassword;
|
||||
} else if (database.type === 'redis') {
|
||||
databaseDefault = '';
|
||||
databaseDbUser = '';
|
||||
}
|
||||
}
|
||||
|
||||
function generateUrl(): string {
|
||||
return `${database.type}://${
|
||||
databaseDbUser ? databaseDbUser + ':' : ''
|
||||
}${databaseDbUserPassword}@${
|
||||
isPublic ? (settings.fqdn ? getDomain(settings.fqdn) : window.location.hostname) : database.id
|
||||
}:${isPublic ? database.publicPort : privatePort}/${databaseDefault}`;
|
||||
}
|
||||
|
||||
async function changeSettings(name: any) {
|
||||
if (publicLoading || !$status.database.isRunning) return;
|
||||
publicLoading = true;
|
||||
let data = {
|
||||
isPublic,
|
||||
appendOnly
|
||||
};
|
||||
if (name === 'isPublic') {
|
||||
data.isPublic = !isPublic;
|
||||
}
|
||||
if (name === 'appendOnly') {
|
||||
data.appendOnly = !appendOnly;
|
||||
}
|
||||
try {
|
||||
const { publicPort } = await post(`/databases/${id}/settings`, {
|
||||
isPublic: data.isPublic,
|
||||
appendOnly: data.appendOnly
|
||||
});
|
||||
isPublic = data.isPublic;
|
||||
appendOnly = data.appendOnly;
|
||||
if (isPublic) {
|
||||
database.publicPort = publicPort;
|
||||
}
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
publicLoading = false;
|
||||
}
|
||||
}
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
loading = true;
|
||||
await post(`/databases/${id}`, { ...database, isRunning: $status.database.isRunning });
|
||||
generateDbDetails();
|
||||
toast.push('Settings saved.');
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mx-auto max-w-4xl px-6">
|
||||
<form on:submit|preventDefault={handleSubmit} class="py-4">
|
||||
<div class="flex space-x-1 pb-5 font-bold">
|
||||
<div class="title">{$t('general')}</div>
|
||||
{#if $appSession.isAdmin}
|
||||
<button
|
||||
type="submit"
|
||||
class:bg-purple-600={!loading}
|
||||
class:hover:bg-purple-500={!loading}
|
||||
disabled={loading}>{loading ? $t('forms.saving') : $t('forms.save')}</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-flow-row gap-2 px-10">
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
|
||||
<input
|
||||
readonly={!$appSession.isAdmin}
|
||||
name="name"
|
||||
id="name"
|
||||
bind:value={database.name}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="destination" class="text-base font-bold text-stone-100"
|
||||
>{$t('application.destination')}</label
|
||||
>
|
||||
{#if database.destinationDockerId}
|
||||
<div class="no-underline">
|
||||
<input
|
||||
value={database.destinationDocker.name}
|
||||
id="destination"
|
||||
disabled
|
||||
readonly
|
||||
class="bg-transparent "
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="version" class="text-base font-bold text-stone-100">Version / Tag</label>
|
||||
<a
|
||||
href={$appSession.isAdmin && !$status.database.isRunning
|
||||
? `/databases/${id}/configuration/version?from=/databases/${id}`
|
||||
: ''}
|
||||
class="no-underline"
|
||||
>
|
||||
<input
|
||||
value={database.version}
|
||||
disabled={$status.database.isRunning}
|
||||
class:cursor-pointer={!$status.database.isRunning}
|
||||
/></a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-flow-row gap-2 px-10 pt-2">
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="host" class="text-base font-bold text-stone-100">{$t('forms.host')}</label>
|
||||
<CopyPasswordField
|
||||
placeholder={$t('forms.generated_automatically_after_start')}
|
||||
isPasswordField={false}
|
||||
readonly
|
||||
disabled
|
||||
id="host"
|
||||
name="host"
|
||||
value={database.id}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="publicPort" class="text-base font-bold text-stone-100">{$t('forms.port')}</label
|
||||
>
|
||||
<CopyPasswordField
|
||||
placeholder={$t('database.generated_automatically_after_set_to_public')}
|
||||
id="publicPort"
|
||||
readonly
|
||||
disabled
|
||||
name="publicPort"
|
||||
value={publicLoading ? 'Loading...' : isPublic ? database.publicPort : privatePort}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-flow-row gap-2">
|
||||
{#if database.type === 'mysql'}
|
||||
<MySql bind:database />
|
||||
{:else if database.type === 'postgresql'}
|
||||
<PostgreSql bind:database />
|
||||
{:else if database.type === 'mongodb'}
|
||||
<MongoDb bind:database />
|
||||
{:else if database.type === 'mariadb'}
|
||||
<MariaDb bind:database />
|
||||
{:else if database.type === 'redis'}
|
||||
<Redis bind:database />
|
||||
{:else if database.type === 'couchdb'}
|
||||
<CouchDb {database} />
|
||||
{/if}
|
||||
<div class="grid grid-cols-2 items-center px-10 pb-8">
|
||||
<label for="url" class="text-base font-bold text-stone-100"
|
||||
>{$t('database.connection_string')}</label
|
||||
>
|
||||
<CopyPasswordField
|
||||
textarea={true}
|
||||
placeholder={$t('forms.generated_automatically_after_start')}
|
||||
isPasswordField={false}
|
||||
id="url"
|
||||
name="url"
|
||||
readonly
|
||||
disabled
|
||||
value={publicLoading || loading ? 'Loading...' : generateUrl()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="flex space-x-1 pb-5 font-bold">
|
||||
<div class="title">{$t('application.features')}</div>
|
||||
</div>
|
||||
<div class="px-10 pb-10">
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
loading={publicLoading}
|
||||
bind:setting={isPublic}
|
||||
on:click={() => changeSettings('isPublic')}
|
||||
title={$t('database.set_public')}
|
||||
description={$t('database.warning_database_public')}
|
||||
disabled={!$status.database.isRunning}
|
||||
/>
|
||||
</div>
|
||||
{#if database.type === 'redis'}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
bind:setting={appendOnly}
|
||||
on:click={() => changeSettings('appendOnly')}
|
||||
title={$t('database.change_append_only_mode')}
|
||||
description={$t('database.warning_append_only')}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
79
apps/ui/src/routes/databases/[id]/_Databases/_MariaDB.svelte
Normal file
79
apps/ui/src/routes/databases/[id]/_Databases/_MariaDB.svelte
Normal file
@@ -0,0 +1,79 @@
|
||||
<script lang="ts">
|
||||
export let database: any;
|
||||
import { status } from '$lib/store';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">MariaDB</div>
|
||||
</div>
|
||||
<div class="space-y-2 px-10">
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="defaultDatabase" class="text-base font-bold text-stone-100"
|
||||
>{$t('database.default_database')}</label
|
||||
>
|
||||
<CopyPasswordField
|
||||
required
|
||||
readonly={database.defaultDatabase}
|
||||
disabled={database.defaultDatabase}
|
||||
placeholder="{$t('forms.eg')}: mydb"
|
||||
id="defaultDatabase"
|
||||
name="defaultDatabase"
|
||||
bind:value={database.defaultDatabase}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="dbUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label>
|
||||
<CopyPasswordField
|
||||
readonly
|
||||
disabled
|
||||
placeholder={$t('forms.generated_automatically_after_start')}
|
||||
id="dbUser"
|
||||
name="dbUser"
|
||||
value={database.dbUser}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="dbUserPassword" class="text-base font-bold text-stone-100"
|
||||
>{$t('forms.password')}</label
|
||||
>
|
||||
<CopyPasswordField
|
||||
disabled={!$status.database.isRunning}
|
||||
readonly={!$status.database.isRunning}
|
||||
placeholder={$t('forms.generated_automatically_after_start')}
|
||||
isPasswordField
|
||||
id="dbUserPassword"
|
||||
name="dbUserPassword"
|
||||
bind:value={database.dbUserPassword}
|
||||
/>
|
||||
<Explainer text="Could be changed while the database is running." />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label>
|
||||
<CopyPasswordField
|
||||
readonly
|
||||
disabled
|
||||
placeholder={$t('forms.generated_automatically_after_start')}
|
||||
id="rootUser"
|
||||
name="rootUser"
|
||||
value={database.rootUser}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="rootUserPassword" class="text-base font-bold text-stone-100"
|
||||
>{$t('forms.roots_password')}</label
|
||||
>
|
||||
<CopyPasswordField
|
||||
disabled={!$status.database.isRunning}
|
||||
readonly={!$status.database.isRunning}
|
||||
placeholder={$t('forms.generated_automatically_after_start')}
|
||||
isPasswordField
|
||||
id="rootUserPassword"
|
||||
name="rootUserPassword"
|
||||
bind:value={database.rootUserPassword}
|
||||
/>
|
||||
<Explainer text="Could be changed while the database is running." />
|
||||
</div>
|
||||
</div>
|
||||
39
apps/ui/src/routes/databases/[id]/_Databases/_MongoDB.svelte
Normal file
39
apps/ui/src/routes/databases/[id]/_Databases/_MongoDB.svelte
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
export let database: any;
|
||||
import { status } from '$lib/store';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">MongoDB</div>
|
||||
</div>
|
||||
<div class="space-y-2 px-10">
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label>
|
||||
<CopyPasswordField
|
||||
placeholder={$t('forms.generated_automatically_after_start')}
|
||||
id="rootUser"
|
||||
readonly
|
||||
disabled
|
||||
name="rootUser"
|
||||
value={database.rootUser}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="rootUserPassword" class="text-base font-bold text-stone-100"
|
||||
>{$t('forms.roots_password')}</label
|
||||
>
|
||||
<CopyPasswordField
|
||||
disabled={!$status.database.isRunning}
|
||||
readonly={!$status.database.isRunning}
|
||||
placeholder={$t('forms.generated_automatically_after_start')}
|
||||
isPasswordField={true}
|
||||
id="rootUserPassword"
|
||||
name="rootUserPassword"
|
||||
bind:value={database.rootUserPassword}
|
||||
/>
|
||||
<Explainer text="Could be changed while the database is running." />
|
||||
</div>
|
||||
</div>
|
||||
79
apps/ui/src/routes/databases/[id]/_Databases/_MySQL.svelte
Normal file
79
apps/ui/src/routes/databases/[id]/_Databases/_MySQL.svelte
Normal file
@@ -0,0 +1,79 @@
|
||||
<script lang="ts">
|
||||
export let database: any;
|
||||
import { status } from '$lib/store';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">MySQL</div>
|
||||
</div>
|
||||
<div class="space-y-2 px-10">
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="defaultDatabase" class="text-base font-bold text-stone-100"
|
||||
>{$t('database.default_database')}</label
|
||||
>
|
||||
<CopyPasswordField
|
||||
required
|
||||
readonly={database.defaultDatabase}
|
||||
disabled={database.defaultDatabase}
|
||||
placeholder="{$t('forms.eg')}: mydb"
|
||||
id="defaultDatabase"
|
||||
name="defaultDatabase"
|
||||
bind:value={database.defaultDatabase}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="dbUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label>
|
||||
<CopyPasswordField
|
||||
readonly
|
||||
disabled
|
||||
placeholder={$t('forms.generated_automatically_after_start')}
|
||||
id="dbUser"
|
||||
name="dbUser"
|
||||
value={database.dbUser}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="dbUserPassword" class="text-base font-bold text-stone-100"
|
||||
>{$t('forms.password')}</label
|
||||
>
|
||||
<CopyPasswordField
|
||||
disabled={!$status.database.isRunning}
|
||||
readonly={!$status.database.isRunning}
|
||||
placeholder={$t('forms.generated_automatically_after_start')}
|
||||
isPasswordField
|
||||
id="dbUserPassword"
|
||||
name="dbUserPassword"
|
||||
bind:value={database.dbUserPassword}
|
||||
/>
|
||||
<Explainer text="Could be changed while the database is running." />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label>
|
||||
<CopyPasswordField
|
||||
readonly
|
||||
disabled
|
||||
placeholder={$t('forms.generated_automatically_after_start')}
|
||||
id="rootUser"
|
||||
name="rootUser"
|
||||
value={database.rootUser}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="rootUserPassword" class="text-base font-bold text-stone-100"
|
||||
>{$t('forms.roots_password')}</label
|
||||
>
|
||||
<CopyPasswordField
|
||||
disabled={!$status.database.isRunning}
|
||||
readonly={!$status.database.isRunning}
|
||||
placeholder={$t('forms.generated_automatically_after_start')}
|
||||
isPasswordField
|
||||
id="rootUserPassword"
|
||||
name="rootUserPassword"
|
||||
bind:value={database.rootUserPassword}
|
||||
/>
|
||||
<Explainer text="Could be changed while the database is running." />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,68 @@
|
||||
<script lang="ts">
|
||||
export let database: any;
|
||||
import { status } from '$lib/store';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">PostgreSQL</div>
|
||||
</div>
|
||||
<div class="space-y-2 px-10">
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="defaultDatabase" class="text-base font-bold text-stone-100"
|
||||
>{$t('database.default_database')}</label
|
||||
>
|
||||
<CopyPasswordField
|
||||
required
|
||||
readonly={database.defaultDatabase}
|
||||
disabled={database.defaultDatabase}
|
||||
placeholder="{$t('forms.eg')}: mydb"
|
||||
id="defaultDatabase"
|
||||
name="defaultDatabase"
|
||||
bind:value={database.defaultDatabase}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="rootUser" class="text-base font-bold text-stone-100"
|
||||
>Root (postgres) User Password</label
|
||||
>
|
||||
<CopyPasswordField
|
||||
disabled={!$status.database.isRunning}
|
||||
readonly={!$status.database.isRunning}
|
||||
placeholder="Generated automatically after start"
|
||||
isPasswordField
|
||||
id="rootUserPassword"
|
||||
name="rootUserPassword"
|
||||
bind:value={database.rootUserPassword}
|
||||
/>
|
||||
<Explainer text="Could be changed while the database is running." />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="dbUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label>
|
||||
<CopyPasswordField
|
||||
readonly
|
||||
disabled
|
||||
placeholder={$t('forms.generated_automatically_after_start')}
|
||||
id="dbUser"
|
||||
name="dbUser"
|
||||
value={database.dbUser}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="dbUserPassword" class="text-base font-bold text-stone-100"
|
||||
>{$t('forms.password')}</label
|
||||
>
|
||||
<CopyPasswordField
|
||||
disabled={!$status.database.isRunning}
|
||||
readonly={!$status.database.isRunning}
|
||||
placeholder={$t('forms.generated_automatically_after_start')}
|
||||
isPasswordField
|
||||
id="dbUserPassword"
|
||||
name="dbUserPassword"
|
||||
bind:value={database.dbUserPassword}
|
||||
/>
|
||||
<Explainer text="Could be changed while the database is running." />
|
||||
</div>
|
||||
</div>
|
||||
28
apps/ui/src/routes/databases/[id]/_Databases/_Redis.svelte
Normal file
28
apps/ui/src/routes/databases/[id]/_Databases/_Redis.svelte
Normal file
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
export let database: any;
|
||||
import { status } from '$lib/store';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Redis</div>
|
||||
</div>
|
||||
<div class="space-y-2 px-10">
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="dbUserPassword" class="text-base font-bold text-stone-100"
|
||||
>{$t('forms.password')}</label
|
||||
>
|
||||
<CopyPasswordField
|
||||
disabled={!$status.database.isRunning}
|
||||
readonly={!$status.database.isRunning}
|
||||
placeholder={$t('forms.generated_automatically_after_start')}
|
||||
isPasswordField
|
||||
id="dbUserPassword"
|
||||
name="dbUserPassword"
|
||||
bind:value={database.dbUserPassword}
|
||||
/>
|
||||
<Explainer text="Could be changed while the database is running." />
|
||||
</div>
|
||||
</div>
|
||||
297
apps/ui/src/routes/databases/[id]/__layout.svelte
Normal file
297
apps/ui/src/routes/databases/[id]/__layout.svelte
Normal file
@@ -0,0 +1,297 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
function checkConfiguration(database: any): any {
|
||||
let configurationPhase = null;
|
||||
if (!database.type) {
|
||||
configurationPhase = 'type';
|
||||
} else if (!database.version) {
|
||||
configurationPhase = 'version';
|
||||
} else if (!database.destinationDockerId) {
|
||||
configurationPhase = 'destination';
|
||||
}
|
||||
return configurationPhase;
|
||||
}
|
||||
export const load: Load = async ({ fetch, url, params }) => {
|
||||
try {
|
||||
const { id } = params;
|
||||
const response = await get(`/databases/${id}`);
|
||||
const { database, versions, privatePort, settings } = response;
|
||||
if (id !== 'new' && (!database || Object.entries(database).length === 0)) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/databases'
|
||||
};
|
||||
}
|
||||
const configurationPhase = checkConfiguration(database);
|
||||
if (
|
||||
configurationPhase &&
|
||||
url.pathname !== `/databases/${params.id}/configuration/${configurationPhase}`
|
||||
) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/databases/${params.id}/configuration/${configurationPhase}`
|
||||
};
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
database,
|
||||
versions,
|
||||
privatePort
|
||||
},
|
||||
stuff: {
|
||||
database,
|
||||
versions,
|
||||
privatePort,
|
||||
settings
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return handlerNotFoundLoad(error, url);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let database: any;
|
||||
import { del, get, post } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { errorNotification, handlerNotFoundLoad } from '$lib/common';
|
||||
import { appSession, status } from '$lib/store';
|
||||
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
|
||||
import Loading from '$lib/components/Loading.svelte';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
const { id } = $page.params;
|
||||
|
||||
let loading = false;
|
||||
let statusInterval: any = false;
|
||||
async function deleteDatabase() {
|
||||
const sure = confirm(`Are you sure you would like to delete '${database.name}'?`);
|
||||
if (sure) {
|
||||
loading = true;
|
||||
try {
|
||||
await del(`/databases/${database.id}`, { id: database.id });
|
||||
return await goto('/databases');
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
async function stopDatabase() {
|
||||
const sure = confirm($t('database.confirm_stop', { name: database.name }));
|
||||
if (sure) {
|
||||
loading = true;
|
||||
try {
|
||||
await post(`/databases/${database.id}/stop`, {});
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
async function startDatabase() {
|
||||
loading = true;
|
||||
try {
|
||||
await post(`/databases/${database.id}/start`, {});
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function getStatus() {
|
||||
if ($status.database.loading) return;
|
||||
$status.database.loading = true;
|
||||
const data = await get(`/databases/${id}`);
|
||||
$status.database.isRunning = data.isRunning;
|
||||
$status.database.initialLoading = false;
|
||||
$status.database.loading = false;
|
||||
}
|
||||
onDestroy(() => {
|
||||
$status.database.initialLoading = true;
|
||||
clearInterval(statusInterval);
|
||||
});
|
||||
onMount(async () => {
|
||||
$status.database.isRunning = false;
|
||||
$status.database.loading = false;
|
||||
if (
|
||||
database.type &&
|
||||
database.destinationDockerId &&
|
||||
database.version &&
|
||||
database.defaultDatabase
|
||||
) {
|
||||
await getStatus();
|
||||
statusInterval = setInterval(async () => {
|
||||
await getStatus();
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if id !== 'new'}
|
||||
<nav class="nav-side">
|
||||
{#if loading}
|
||||
<Loading fullscreen cover />
|
||||
{:else}
|
||||
{#if database.type && database.destinationDockerId && database.version && database.defaultDatabase}
|
||||
{#if $status.database.initialLoading}
|
||||
<button
|
||||
class="icons tooltip-bottom flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out"
|
||||
>
|
||||
<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="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
|
||||
<line x1="5.63" y1="7.16" x2="5.63" y2="7.17" />
|
||||
<line x1="4.06" y1="11" x2="4.06" y2="11.01" />
|
||||
<line x1="4.63" y1="15.1" x2="4.63" y2="15.11" />
|
||||
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
|
||||
<line x1="11" y1="19.94" x2="11" y2="19.95" />
|
||||
</svg>
|
||||
</button>
|
||||
{:else if $status.database.isRunning}
|
||||
<button
|
||||
on:click={stopDatabase}
|
||||
title={$t('database.stop_database')}
|
||||
type="submit"
|
||||
disabled={!$appSession.isAdmin}
|
||||
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-red-500"
|
||||
data-tooltip={$appSession.isAdmin
|
||||
? $t('database.stop_database')
|
||||
: $t('database.permission_denied_stop_database')}
|
||||
>
|
||||
<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>
|
||||
{:else}
|
||||
<button
|
||||
on:click={startDatabase}
|
||||
title={$t('database.start_database')}
|
||||
type="submit"
|
||||
disabled={!$appSession.isAdmin}
|
||||
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-green-500"
|
||||
data-tooltip={$appSession.isAdmin
|
||||
? $t('database.start_database')
|
||||
: $t('database.permission_denied_start_database')}
|
||||
><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>
|
||||
{/if}
|
||||
{/if}
|
||||
<div class="border border-stone-700 h-8" />
|
||||
<a
|
||||
href="/databases/{id}"
|
||||
sveltekit:prefetch
|
||||
class="hover:text-yellow-500 rounded"
|
||||
class:text-yellow-500={$page.url.pathname === `/databases/${id}`}
|
||||
class:bg-coolgray-500={$page.url.pathname === `/databases/${id}`}
|
||||
>
|
||||
<button
|
||||
title={$t('application.configurations')}
|
||||
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
|
||||
data-tooltip={$t('application.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
|
||||
>
|
||||
<div class="border border-stone-700 h-8" />
|
||||
<a
|
||||
href={$status.database.isRunning ? `/databases/${id}/logs` : null}
|
||||
sveltekit:prefetch
|
||||
class="hover:text-pink-500 rounded"
|
||||
class:text-pink-500={$page.url.pathname === `/databases/${id}/logs`}
|
||||
class:bg-coolgray-500={$page.url.pathname === `/databases/${id}/logs`}
|
||||
>
|
||||
<button
|
||||
title={$t('database.logs')}
|
||||
disabled={!$status.database.isRunning}
|
||||
class="icons bg-transparent tooltip-bottom text-sm"
|
||||
data-tooltip={$t('database.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
|
||||
>
|
||||
<button
|
||||
on:click={deleteDatabase}
|
||||
title={$t('database.delete_database')}
|
||||
type="submit"
|
||||
disabled={!$appSession.isAdmin}
|
||||
class:hover:text-red-500={$appSession.isAdmin}
|
||||
class="icons bg-transparent tooltip-bottom text-sm"
|
||||
data-tooltip={$appSession.isAdmin
|
||||
? $t('database.delete_database')
|
||||
: $t('database.permission_denied_delete_database')}><DeleteIcon /></button
|
||||
>
|
||||
{/if}
|
||||
</nav>
|
||||
{/if}
|
||||
<slot />
|
||||
@@ -0,0 +1,91 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
try {
|
||||
const { database } = stuff;
|
||||
if (database?.destinationDockerId && !url.searchParams.get('from')) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/database/${params.id}`
|
||||
};
|
||||
}
|
||||
const response = await get(`/destinations`);
|
||||
return {
|
||||
props: {
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { get, post } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
|
||||
export let destinations: any;
|
||||
async function handleSubmit(destinationId: any) {
|
||||
try {
|
||||
await post(`/databases/${id}/configuration/destination`, {
|
||||
destinationId
|
||||
});
|
||||
return await goto(from || `/databases/${id}`);
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">
|
||||
{$t('application.configuration.configure_destination')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
{#if !destinations || destinations.length === 0}
|
||||
<div class="flex-col">
|
||||
<div class="pb-2">{$t('application.configuration.no_configurable_destination')}</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>
|
||||
84
apps/ui/src/routes/databases/[id]/configuration/type.svelte
Normal file
84
apps/ui/src/routes/databases/[id]/configuration/type.svelte
Normal file
@@ -0,0 +1,84 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
try {
|
||||
const { database } = stuff;
|
||||
if (database?.type && !url.searchParams.get('from')) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/database/${params.id}`
|
||||
};
|
||||
}
|
||||
const response = await get(`/databases/${params.id}/configuration/type`);
|
||||
return {
|
||||
props: {
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let types: any;
|
||||
|
||||
import { page } from '$app/stores';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
|
||||
import Clickhouse from '$lib/components/svg/databases/Clickhouse.svelte';
|
||||
import CouchDB from '$lib/components/svg/databases/CouchDB.svelte';
|
||||
import MongoDB from '$lib/components/svg/databases/MongoDB.svelte';
|
||||
import MariaDB from '$lib/components/svg/databases/MariaDB.svelte';
|
||||
import MySQL from '$lib/components/svg/databases/MySQL.svelte';
|
||||
import PostgreSQL from '$lib/components/svg/databases/PostgreSQL.svelte';
|
||||
import Redis from '$lib/components/svg/databases/Redis.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { get, post } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
async function handleSubmit(type: any) {
|
||||
try {
|
||||
await post(`/databases/${id}/configuration/type`, { type });
|
||||
return await goto(from || `/databases/${id}/configuration/version`);
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">{$t('database.select_database_type')}</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#each types as type}
|
||||
<div class="p-2">
|
||||
<form on:submit|preventDefault={() => handleSubmit(type.name)}>
|
||||
<button type="submit" class="box-selection relative text-xl font-bold hover:bg-purple-700">
|
||||
{#if type.name === 'clickhouse'}
|
||||
<Clickhouse isAbsolute />
|
||||
{:else if type.name === 'couchdb'}
|
||||
<CouchDB isAbsolute />
|
||||
{:else if type.name === 'mongodb'}
|
||||
<MongoDB isAbsolute />
|
||||
{:else if type.name === 'mariadb'}
|
||||
<MariaDB isAbsolute />
|
||||
{:else if type.name === 'mysql'}
|
||||
<MySQL isAbsolute />
|
||||
{:else if type.name === 'postgresql'}
|
||||
<PostgreSQL isAbsolute />
|
||||
{:else if type.name === 'redis'}
|
||||
<Redis isAbsolute />
|
||||
{/if}{type.fancyName}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -0,0 +1,70 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
try {
|
||||
const { database } = stuff;
|
||||
if (database?.version && !url.searchParams.get('from')) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/database/${params.id}`
|
||||
};
|
||||
}
|
||||
const response = await get(`/databases/${params.id}/configuration/version`);
|
||||
return {
|
||||
props: {
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let versions: any;
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { get, post } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
|
||||
async function handleSubmit(version: any) {
|
||||
try {
|
||||
await post(`/databases/${id}/configuration/version`, { version });
|
||||
return await goto(from || `/databases/${id}/configuration/destination`);
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">{$t('database.select_database_version')}</div>
|
||||
</div>
|
||||
{#if from}
|
||||
<div class="pb-10 text-center">
|
||||
Warning: you are about to change the version of this database.<br />This could cause problem
|
||||
after you restart the database,
|
||||
<span class="font-bold text-pink-600">like losing your data, incompatibility issues, etc</span
|
||||
>.<br />Only do if you know what you are doing!
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#each versions as version}
|
||||
<div class="p-2">
|
||||
<form on:submit|preventDefault={() => handleSubmit(version)}>
|
||||
<button type="submit" class="box-selection text-xl font-bold hover:bg-purple-700"
|
||||
>{version}</button
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
89
apps/ui/src/routes/databases/[id]/index.svelte
Normal file
89
apps/ui/src/routes/databases/[id]/index.svelte
Normal file
@@ -0,0 +1,89 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ stuff }) => {
|
||||
return {
|
||||
props: { ...stuff }
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { get } from '$lib/api';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import DatabaseLinks from './_DatabaseLinks.svelte';
|
||||
import Databases from './_Databases/_Databases.svelte';
|
||||
import { status } from '$lib/store';
|
||||
export let database: any;
|
||||
export let settings: any;
|
||||
export let privatePort: any;
|
||||
|
||||
const { id } = $page.params;
|
||||
let loading = {
|
||||
usage: false
|
||||
};
|
||||
let usage = {
|
||||
MemUsage: 0,
|
||||
CPUPerc: 0,
|
||||
NetIO: 0
|
||||
};
|
||||
let usageInterval: any;
|
||||
|
||||
async function getUsage() {
|
||||
if (loading.usage) return;
|
||||
if (!$status.database.isRunning) return;
|
||||
loading.usage = true;
|
||||
const data = await get(`/databases/${id}/usage`);
|
||||
usage = data.usage;
|
||||
loading.usage = false;
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
clearInterval(usageInterval);
|
||||
});
|
||||
onMount(async () => {
|
||||
await getUsage();
|
||||
usageInterval = setInterval(async () => {
|
||||
await getUsage();
|
||||
}, 1500);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex items-center space-x-2 p-6 text-2xl font-bold">
|
||||
<div class="-mb-5 flex-col">
|
||||
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
|
||||
Configuration
|
||||
</div>
|
||||
<span class="text-xs">{database.name}</span>
|
||||
</div>
|
||||
<DatabaseLinks {database} />
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-4xl px-6 py-4">
|
||||
<div class="text-2xl font-bold">Database Usage</div>
|
||||
<div class="mx-auto">
|
||||
<dl class="relative mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3">
|
||||
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left">
|
||||
<dt class=" text-sm font-medium text-white">Used Memory / Memory Limit</dt>
|
||||
<dd class="mt-1 text-xl font-semibold text-white">
|
||||
{usage?.MemUsage}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left">
|
||||
<dt class="truncate text-sm font-medium text-white">Used CPU</dt>
|
||||
<dd class="mt-1 text-xl font-semibold text-white ">
|
||||
{usage?.CPUPerc}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left">
|
||||
<dt class="truncate text-sm font-medium text-white">Network IO</dt>
|
||||
<dd class="mt-1 text-xl font-semibold text-white ">
|
||||
{usage?.NetIO}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<Databases bind:database {privatePort} {settings} />
|
||||
41
apps/ui/src/routes/databases/[id]/logs/_Loading.svelte
Normal file
41
apps/ui/src/routes/databases/[id]/logs/_Loading.svelte
Normal 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>
|
||||
178
apps/ui/src/routes/databases/[id]/logs/index.svelte
Normal file
178
apps/ui/src/routes/databases/[id]/logs/index.svelte
Normal file
@@ -0,0 +1,178 @@
|
||||
<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 }) => {
|
||||
try {
|
||||
const response = await get(`/databases/${params.id}/logs`);
|
||||
return {
|
||||
props: {
|
||||
database: stuff.database,
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let database: any;
|
||||
import { page } from '$app/stores';
|
||||
import LoadingLogs from './_Loading.svelte';
|
||||
import { get } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
|
||||
let loadLogsInterval: any = null;
|
||||
let logs: any = [];
|
||||
let lastLog: any = null;
|
||||
let followingInterval: any;
|
||||
let followingLogs: any;
|
||||
let logsEl: any;
|
||||
let position = 0;
|
||||
|
||||
const { id } = $page.params;
|
||||
onMount(async () => {
|
||||
loadAllLogs();
|
||||
loadLogsInterval = setInterval(() => {
|
||||
loadLogs();
|
||||
}, 1000);
|
||||
});
|
||||
onDestroy(() => {
|
||||
clearInterval(loadLogsInterval);
|
||||
clearInterval(followingInterval);
|
||||
});
|
||||
async function loadAllLogs() {
|
||||
try {
|
||||
const data: any = await get(`/databases/${id}/logs`);
|
||||
if (data?.logs) {
|
||||
lastLog = data.logs[data.logs.length - 1];
|
||||
logs = data.logs;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function loadLogs() {
|
||||
try {
|
||||
const newLogs: any = await get(
|
||||
`/databases/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`
|
||||
);
|
||||
|
||||
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
|
||||
logs = logs.concat(newLogs.logs);
|
||||
lastLog = newLogs.logs[newLogs.logs.length - 1];
|
||||
}
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
function detect() {
|
||||
if (position < logsEl.scrollTop) {
|
||||
position = logsEl.scrollTop;
|
||||
} else {
|
||||
if (followingLogs) {
|
||||
clearInterval(followingInterval);
|
||||
followingLogs = false;
|
||||
}
|
||||
position = logsEl.scrollTop;
|
||||
}
|
||||
}
|
||||
|
||||
function followBuild() {
|
||||
followingLogs = !followingLogs;
|
||||
if (followingLogs) {
|
||||
followingInterval = setInterval(() => {
|
||||
logsEl.scrollTop = logsEl.scrollHeight;
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
}, 1000);
|
||||
} else {
|
||||
clearInterval(followingInterval);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold">
|
||||
<div class="-mb-5 flex-col">
|
||||
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
|
||||
Database Logs
|
||||
</div>
|
||||
<span class="text-xs">{database.name}</span>
|
||||
</div>
|
||||
|
||||
{#if database.fqdn}
|
||||
<a
|
||||
href={database.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}
|
||||
</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">{$t('application.build.waiting_logs')}</div>
|
||||
{:else}
|
||||
<div class="relative w-full">
|
||||
<div class="text-right " />
|
||||
{#if loadLogsInterval}
|
||||
<LoadingLogs />
|
||||
{/if}
|
||||
<div class="flex justify-end sticky top-0 p-2 mx-1">
|
||||
<button
|
||||
on:click={followBuild}
|
||||
class="bg-transparent"
|
||||
data-tooltip="Follow logs"
|
||||
class:text-green-500={followingLogs}
|
||||
>
|
||||
<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="12" cy="12" r="9" />
|
||||
<line x1="8" y1="12" x2="12" y2="16" />
|
||||
<line x1="12" y1="8" x2="12" y2="16" />
|
||||
<line x1="16" y1="12" x2="12" y2="16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="font-mono w-full leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
|
||||
bind:this={logsEl}
|
||||
on:scroll={detect}
|
||||
>
|
||||
<div class="px-2 pr-14">
|
||||
{#each logs as log}
|
||||
{log + '\n'}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
153
apps/ui/src/routes/databases/index.svelte
Normal file
153
apps/ui/src/routes/databases/index.svelte
Normal file
@@ -0,0 +1,153 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async () => {
|
||||
try {
|
||||
const response = await get(`/databases`);
|
||||
return {
|
||||
props: {
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(error)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let databases: any = [];
|
||||
import Clickhouse from '$lib/components/svg/databases/Clickhouse.svelte';
|
||||
import CouchDB from '$lib/components/svg/databases/CouchDB.svelte';
|
||||
import MongoDB from '$lib/components/svg/databases/MongoDB.svelte';
|
||||
import MariaDB from '$lib/components/svg/databases/MariaDB.svelte';
|
||||
import MySQL from '$lib/components/svg/databases/MySQL.svelte';
|
||||
import PostgreSQL from '$lib/components/svg/databases/PostgreSQL.svelte';
|
||||
import Redis from '$lib/components/svg/databases/Redis.svelte';
|
||||
import { get, post } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { appSession } from '$lib/store';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
async function newDatabase() {
|
||||
const { id } = await post('/databases/new', {});
|
||||
return await goto(`/databases/${id}`, { replaceState: true });
|
||||
}
|
||||
|
||||
const ownDatabases = databases.filter((database: any) => {
|
||||
if (database.teams[0].id === $appSession.teamId) {
|
||||
return database;
|
||||
}
|
||||
});
|
||||
const otherDatabases = databases.filter((database: any) => {
|
||||
if (database.teams[0].id !== $appSession.teamId) {
|
||||
return database;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">{$t('index.databases')}</div>
|
||||
<div on:click={newDatabase} class="add-icon cursor-pointer bg-purple-600 hover:bg-purple-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
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-col justify-center">
|
||||
{#if !databases || ownDatabases.length === 0}
|
||||
<div class="flex-col">
|
||||
<div class="text-center text-xl font-bold">{$t('database.no_databases_found')}</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if ownDatabases.length > 0 || otherDatabases.length > 0}
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each ownDatabases as database}
|
||||
<a href="/databases/{database.id}" class="w-96 p-2 no-underline">
|
||||
<div class="box-selection group relative hover:bg-purple-600">
|
||||
{#if database.type === 'clickhouse'}
|
||||
<Clickhouse isAbsolute />
|
||||
{:else if database.type === 'couchdb'}
|
||||
<CouchDB isAbsolute />
|
||||
{:else if database.type === 'mongodb'}
|
||||
<MongoDB isAbsolute />
|
||||
{:else if database.type === 'mysql'}
|
||||
<MySQL isAbsolute />
|
||||
{:else if database.type === 'mariadb'}
|
||||
<MariaDB isAbsolute />
|
||||
{:else if database.type === 'postgresql'}
|
||||
<PostgreSQL isAbsolute />
|
||||
{:else if database.type === 'redis'}
|
||||
<Redis isAbsolute />
|
||||
{/if}
|
||||
<div class="truncate text-center text-xl font-bold">
|
||||
{database.name}
|
||||
</div>
|
||||
{#if $appSession.teamId === '0' && otherDatabases.length > 0}
|
||||
<div class="truncate text-center">{database.teams[0].name}</div>
|
||||
{/if}
|
||||
{#if !database.type}
|
||||
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
|
||||
{$t('application.configuration.configuration_missing')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{#if otherDatabases.length > 0 && $appSession.teamId === '0'}
|
||||
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Databases</div>
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each otherDatabases as database}
|
||||
<a href="/databases/{database.id}" class="w-96 p-2 no-underline">
|
||||
<div class="box-selection group relative hover:bg-purple-600">
|
||||
{#if database.type === 'clickhouse'}
|
||||
<Clickhouse isAbsolute />
|
||||
{:else if database.type === 'couchdb'}
|
||||
<CouchDB isAbsolute />
|
||||
{:else if database.type === 'mongodb'}
|
||||
<MongoDB isAbsolute />
|
||||
{:else if database.type === 'mariadb'}
|
||||
<MariaDB isAbsolute />
|
||||
{:else if database.type === 'mysql'}
|
||||
<MySQL isAbsolute />
|
||||
{:else if database.type === 'postgresql'}
|
||||
<PostgreSQL isAbsolute />
|
||||
{:else if database.type === 'redis'}
|
||||
<Redis isAbsolute />
|
||||
{/if}
|
||||
<div class="truncate text-center text-xl font-bold">
|
||||
{database.name}
|
||||
</div>
|
||||
{#if $appSession.teamId === '0'}
|
||||
<div class="truncate text-center">{database.teams[0].name}</div>
|
||||
{/if}
|
||||
{#if !database.type}
|
||||
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
|
||||
Configuration missing
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-center truncate">{database.type}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
25
apps/ui/src/routes/destinations/[id]/_Destination.svelte
Normal file
25
apps/ui/src/routes/destinations/[id]/_Destination.svelte
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
<script lang="ts">
|
||||
export let destination: any;
|
||||
export let settings: any;
|
||||
export let state: any;
|
||||
import LocalDocker from './_LocalDocker.svelte';
|
||||
import RemoteDocker from './_RemoteDocker.svelte';
|
||||
</script>
|
||||
|
||||
<div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold">
|
||||
<div class="-mb-5 flex-col">
|
||||
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
|
||||
Configuration
|
||||
</div>
|
||||
<span class="text-xs">{destination.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-4xl px-6">
|
||||
{#if destination.remoteEngine}
|
||||
<RemoteDocker bind:destination {settings} {state} />
|
||||
{:else}
|
||||
<LocalDocker bind:destination {settings} {state} />
|
||||
{/if}
|
||||
</div>
|
||||
202
apps/ui/src/routes/destinations/[id]/_LocalDocker.svelte
Normal file
202
apps/ui/src/routes/destinations/[id]/_LocalDocker.svelte
Normal file
@@ -0,0 +1,202 @@
|
||||
<script lang="ts">
|
||||
export let destination: any;
|
||||
export let settings: any;
|
||||
export let state: any;
|
||||
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { page } from '$app/stores';
|
||||
import { post } from '$lib/api';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { appSession } from '$lib/store';
|
||||
import Setting from '$lib/components/Setting.svelte';
|
||||
const { id } = $page.params;
|
||||
let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock';
|
||||
let loading = false;
|
||||
let loadingProxy = false;
|
||||
let restarting = false;
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
try {
|
||||
return await post(`/destinations/${id}`, { ...destination });
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
onMount(async () => {
|
||||
if (state === false && destination.isCoolifyProxyUsed === true) {
|
||||
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
|
||||
try {
|
||||
await post(`/destinations/${id}/settings`, {
|
||||
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
|
||||
engine: destination.engine
|
||||
});
|
||||
await stopProxy();
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
} else if (state === true && destination.isCoolifyProxyUsed === false) {
|
||||
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
|
||||
try {
|
||||
await post(`/destinations/${id}/settings`, {
|
||||
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
|
||||
engine: destination.engine
|
||||
});
|
||||
await startProxy();
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
async function changeProxySetting() {
|
||||
if (!cannotDisable) {
|
||||
const isProxyActivated = destination.isCoolifyProxyUsed;
|
||||
if (isProxyActivated) {
|
||||
const sure = confirm(
|
||||
`Are you sure you want to ${
|
||||
destination.isCoolifyProxyUsed ? 'disable' : 'enable'
|
||||
} Coolify proxy? It will remove the proxy for all configured networks and all deployments on '${
|
||||
destination.engine
|
||||
}'! Nothing will be reachable if you do it!`
|
||||
);
|
||||
if (!sure) return;
|
||||
}
|
||||
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
|
||||
try {
|
||||
loadingProxy = true;
|
||||
await post(`/destinations/${id}/settings`, {
|
||||
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
|
||||
engine: destination.engine
|
||||
});
|
||||
if (isProxyActivated) {
|
||||
await stopProxy();
|
||||
} else {
|
||||
await startProxy();
|
||||
}
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loadingProxy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
async function stopProxy() {
|
||||
try {
|
||||
await post(`/destinations/${id}/stop`, { engine: destination.engine });
|
||||
return toast.push($t('destination.coolify_proxy_stopped'));
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function startProxy() {
|
||||
try {
|
||||
await post(`/destinations/${id}/start`, { engine: destination.engine });
|
||||
return toast.push($t('destination.coolify_proxy_started'));
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function forceRestartProxy() {
|
||||
const sure = confirm($t('destination.confirm_restart_proxy'));
|
||||
if (sure) {
|
||||
try {
|
||||
restarting = true;
|
||||
toast.push($t('destination.coolify_proxy_restarting'));
|
||||
await post(`/destinations/${id}/restart`, {
|
||||
engine: destination.engine,
|
||||
fqdn: settings.fqdn
|
||||
});
|
||||
} catch ({ error }) {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 5000);
|
||||
} finally {
|
||||
restarting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
|
||||
<div class="flex space-x-1 pb-5">
|
||||
<div class="title font-bold">{$t('forms.configuration')}</div>
|
||||
{#if $appSession.isAdmin}
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-sky-600 hover:bg-sky-500"
|
||||
class:bg-sky-600={!loading}
|
||||
class:hover:bg-sky-500={!loading}
|
||||
disabled={loading}
|
||||
>{loading ? $t('forms.saving') : $t('forms.save')}
|
||||
</button>
|
||||
<button
|
||||
class={restarting ? '' : 'bg-red-600 hover:bg-red-500'}
|
||||
disabled={restarting}
|
||||
on:click|preventDefault={forceRestartProxy}
|
||||
>{restarting
|
||||
? $t('destination.restarting_please_wait')
|
||||
: $t('destination.force_restart_proxy')}</button
|
||||
>
|
||||
{/if}
|
||||
<!-- <button type="button" class="bg-coollabs hover:bg-coollabs-100" on:click={scanApps}
|
||||
>Scan for applications</button
|
||||
> -->
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10 ">
|
||||
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
|
||||
<input
|
||||
name="name"
|
||||
placeholder={$t('forms.name')}
|
||||
disabled={!$appSession.isAdmin}
|
||||
readonly={!$appSession.isAdmin}
|
||||
bind:value={destination.name}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="engine" class="text-base font-bold text-stone-100">{$t('forms.engine')}</label>
|
||||
<CopyPasswordField
|
||||
id="engine"
|
||||
readonly
|
||||
disabled
|
||||
name="engine"
|
||||
placeholder="{$t('forms.eg')}: /var/run/docker.sock"
|
||||
value={destination.engine}
|
||||
/>
|
||||
</div>
|
||||
<!-- <div class="flex items-center">
|
||||
<label for="remoteEngine">Remote Engine?</label>
|
||||
<input name="remoteEngine" type="checkbox" bind:checked={payload.remoteEngine} />
|
||||
</div> -->
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
|
||||
<CopyPasswordField
|
||||
id="network"
|
||||
readonly
|
||||
disabled
|
||||
name="network"
|
||||
placeholder="{$t('forms.default')}: coolify"
|
||||
value={destination.network}
|
||||
/>
|
||||
</div>
|
||||
{#if $appSession.teamId === '0'}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
loading={loadingProxy}
|
||||
disabled={cannotDisable}
|
||||
bind:setting={destination.isCoolifyProxyUsed}
|
||||
on:click={changeProxySetting}
|
||||
title={$t('destination.use_coolify_proxy')}
|
||||
description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration. Databases will have their own proxy. <br><br>${
|
||||
cannotDisable
|
||||
? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>'
|
||||
: ''
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
57
apps/ui/src/routes/destinations/[id]/_New.svelte
Normal file
57
apps/ui/src/routes/destinations/[id]/_New.svelte
Normal file
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
import cuid from 'cuid';
|
||||
import { t } from '$lib/translations';
|
||||
import NewLocalDocker from './_NewLocalDocker.svelte';
|
||||
import NewRemoteDocker from './_NewRemoteDocker.svelte';
|
||||
let payload = {};
|
||||
let selected = 'localDocker';
|
||||
function setPredefined(type: any) {
|
||||
selected = type;
|
||||
switch (type) {
|
||||
case 'localDocker':
|
||||
payload = {
|
||||
name: t.get('sources.local_docker'),
|
||||
engine: '/var/run/docker.sock',
|
||||
remoteEngine: false,
|
||||
network: cuid(),
|
||||
isCoolifyProxyUsed: true
|
||||
};
|
||||
break;
|
||||
case 'remoteDocker':
|
||||
payload = {
|
||||
name: $t('sources.remote_docker'),
|
||||
remoteEngine: true,
|
||||
ipAddress: null,
|
||||
user: 'root',
|
||||
port: 22,
|
||||
sshPrivateKey: null,
|
||||
network: cuid(),
|
||||
isCoolifyProxyUsed: true
|
||||
};
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">{$t('destination.new.add_new_destination')}</div>
|
||||
</div>
|
||||
<div class="flex-col space-y-2 pb-10 text-center">
|
||||
<div class="text-xl font-bold text-white">{$t('destination.new.predefined_destinations')}</div>
|
||||
<div class="flex justify-center space-x-2">
|
||||
<button class="w-32" on:click={() => setPredefined('localDocker')}
|
||||
>{$t('sources.local_docker')}</button
|
||||
>
|
||||
<!-- <button class="w-32" on:click={() => setPredefined('remoteDocker')}>Remote Docker</button> -->
|
||||
<!-- <button class="w-32" on:click={() => setPredefined('kubernetes')}>Kubernetes</button> -->
|
||||
</div>
|
||||
</div>
|
||||
{#if selected === 'localDocker'}
|
||||
<NewLocalDocker {payload} />
|
||||
{:else if selected === 'remoteDocker'}
|
||||
<NewRemoteDocker {payload} />
|
||||
{:else}
|
||||
<div class="text-center font-bold text-4xl py-10">{$t('index.not_implemented_yet')}</div>
|
||||
{/if}
|
||||
82
apps/ui/src/routes/destinations/[id]/_NewLocalDocker.svelte
Normal file
82
apps/ui/src/routes/destinations/[id]/_NewLocalDocker.svelte
Normal file
@@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
export let payload: any;
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { post } from '$lib/api';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import Setting from '$lib/components/Setting.svelte';
|
||||
import { appSession } from '$lib/store';
|
||||
import { t } from '$lib/translations';
|
||||
|
||||
let loading = false;
|
||||
|
||||
async function handleSubmit() {
|
||||
if (loading) return;
|
||||
try {
|
||||
loading = true;
|
||||
await post(`/destinations/check`, { network: payload.network });
|
||||
const { id } = await post(`/destinations/new`, {
|
||||
...payload
|
||||
});
|
||||
await goto(`/destinations/${id}`);
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex justify-center px-6 pb-8">
|
||||
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
|
||||
<div class="flex items-center space-x-2 pb-5">
|
||||
<div class="title font-bold">{$t('forms.configuration')}</div>
|
||||
<button
|
||||
type="submit"
|
||||
class:bg-sky-600={!loading}
|
||||
class:hover:bg-sky-500={!loading}
|
||||
disabled={loading}
|
||||
>{loading
|
||||
? payload.isCoolifyProxyUsed
|
||||
? $t('destination.new.saving_and_configuring_proxy')
|
||||
: $t('forms.saving')
|
||||
: $t('forms.save')}</button
|
||||
>
|
||||
</div>
|
||||
<div class="mt-2 grid grid-cols-2 items-center px-10">
|
||||
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
|
||||
<input required name="name" placeholder={$t('forms.name')} bind:value={payload.name} />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="engine" class="text-base font-bold text-stone-100">{$t('forms.engine')}</label>
|
||||
<input
|
||||
required
|
||||
name="engine"
|
||||
placeholder="{$t('forms.eg')}: /var/run/docker.sock"
|
||||
bind:value={payload.engine}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
|
||||
<input
|
||||
required
|
||||
name="network"
|
||||
placeholder="{$t('forms.default')}: coolify"
|
||||
bind:value={payload.network}
|
||||
/>
|
||||
</div>
|
||||
{#if $appSession.teamId === '0'}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
bind:setting={payload.isCoolifyProxyUsed}
|
||||
on:click={() => (payload.isCoolifyProxyUsed = !payload.isCoolifyProxyUsed)}
|
||||
title={$t('destination.use_coolify_proxy')}
|
||||
description={$t('destination.new.install_proxy')}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
</div>
|
||||
99
apps/ui/src/routes/destinations/[id]/_NewRemoteDocker.svelte
Normal file
99
apps/ui/src/routes/destinations/[id]/_NewRemoteDocker.svelte
Normal file
@@ -0,0 +1,99 @@
|
||||
<script lang="ts">
|
||||
export let payload: any;
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { post } from '$lib/api';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import Setting from '$lib/components/Setting.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
|
||||
let loading = false;
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const { id } = await post('/new/destination/docker', {
|
||||
...payload
|
||||
});
|
||||
return await goto(`/destinations/${id}`);
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex justify-center px-6 pb-8">
|
||||
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
|
||||
<div class="flex items-center space-x-2 pb-5">
|
||||
<div class="title font-bold">{$t('forms.configuration')}</div>
|
||||
<button
|
||||
type="submit"
|
||||
class:bg-sky-600={!loading}
|
||||
class:hover:bg-sky-500={!loading}
|
||||
disabled={loading}
|
||||
>{loading
|
||||
? payload.isCoolifyProxyUsed
|
||||
? $t('destination.new.saving_and_configuring_proxy')
|
||||
: $t('forms.saving')
|
||||
: $t('forms.save')}</button
|
||||
>
|
||||
</div>
|
||||
<div class="mt-2 grid grid-cols-2 items-center px-10">
|
||||
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
|
||||
<input required name="name" placeholder={$t('forms.name')} bind:value={payload.name} />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="ipAddress" class="text-base font-bold text-stone-100"
|
||||
>{$t('forms.ip_address')}</label
|
||||
>
|
||||
<input
|
||||
required
|
||||
name="ipAddress"
|
||||
placeholder="{$t('forms.eg')}: 192.168..."
|
||||
bind:value={payload.ipAddress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="user" class="text-base font-bold text-stone-100">{$t('forms.user')}</label>
|
||||
<input required name="user" placeholder="{$t('forms.eg')}: root" bind:value={payload.user} />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="port" class="text-base font-bold text-stone-100">{$t('forms.port')}</label>
|
||||
<input required name="port" placeholder="{$t('forms.eg')}: 22" bind:value={payload.port} />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="sshPrivateKey" class="text-base font-bold text-stone-100"
|
||||
>{$t('forms.ssh_private_key')}</label
|
||||
>
|
||||
<textarea
|
||||
rows="10"
|
||||
class="resize-none"
|
||||
required
|
||||
name="sshPrivateKey"
|
||||
placeholder="{$t('forms.eg')}: -----BEGIN...."
|
||||
bind:value={payload.sshPrivateKey}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
|
||||
<input
|
||||
required
|
||||
name="network"
|
||||
placeholder="{$t('forms.default')}: coolify"
|
||||
bind:value={payload.network}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
bind:setting={payload.isCoolifyProxyUsed}
|
||||
on:click={() => (payload.isCoolifyProxyUsed = !payload.isCoolifyProxyUsed)}
|
||||
title={$t('destination.use_coolify_proxy')}
|
||||
description={$t('destination.new.install_proxy')}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
226
apps/ui/src/routes/destinations/[id]/_RemoteDocker.svelte
Normal file
226
apps/ui/src/routes/destinations/[id]/_RemoteDocker.svelte
Normal file
@@ -0,0 +1,226 @@
|
||||
<script lang="ts">
|
||||
export let destination: any;
|
||||
export let settings: any
|
||||
export let state: any
|
||||
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { page, session } from '$app/stores';
|
||||
import Setting from '$lib/components/Setting.svelte';
|
||||
import { post } from '$lib/api';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification, generateRemoteEngine } from '$lib/common';
|
||||
import { appSession } from '$lib/store';
|
||||
const { id } = $page.params;
|
||||
let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock';
|
||||
// let scannedApps = [];
|
||||
let loading = false;
|
||||
let restarting = false;
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
try {
|
||||
return await post(`/destinations/${id}.json`, { ...destination });
|
||||
} catch (error ) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
// async function scanApps() {
|
||||
// scannedApps = [];
|
||||
// const data = await fetch(`/destinations/${id}/scan.json`);
|
||||
// const { containers } = await data.json();
|
||||
// scannedApps = containers;
|
||||
// }
|
||||
onMount(async () => {
|
||||
if (state === false && destination.isCoolifyProxyUsed === true) {
|
||||
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
|
||||
try {
|
||||
await post(`/destinations/${id}/settings.json`, {
|
||||
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
|
||||
engine: destination.engine
|
||||
});
|
||||
await stopProxy();
|
||||
} catch (error ) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
} else if (state === true && destination.isCoolifyProxyUsed === false) {
|
||||
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
|
||||
try {
|
||||
await post(`/destinations/${id}/settings.json`, {
|
||||
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
|
||||
engine: destination.engine
|
||||
});
|
||||
await startProxy();
|
||||
} catch ( error ) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
async function changeProxySetting() {
|
||||
if (!cannotDisable) {
|
||||
const isProxyActivated = destination.isCoolifyProxyUsed;
|
||||
if (isProxyActivated) {
|
||||
const sure = confirm(
|
||||
`Are you sure you want to ${
|
||||
destination.isCoolifyProxyUsed ? 'disable' : 'enable'
|
||||
} Coolify proxy? It will remove the proxy for all configured networks and all deployments on '${
|
||||
destination.engine
|
||||
}'! Nothing will be reachable if you do it!`
|
||||
);
|
||||
if (!sure) return;
|
||||
}
|
||||
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
|
||||
try {
|
||||
await post(`/destinations/${id}/settings.json`, {
|
||||
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
|
||||
engine: destination.engine
|
||||
});
|
||||
if (isProxyActivated) {
|
||||
await stopProxy();
|
||||
} else {
|
||||
await startProxy();
|
||||
}
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
async function stopProxy() {
|
||||
try {
|
||||
const engine = generateRemoteEngine(destination);
|
||||
await post(`/destinations/${id}/stop.json`, { engine });
|
||||
return toast.push($t('destination.coolify_proxy_stopped'));
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function startProxy() {
|
||||
try {
|
||||
const engine = generateRemoteEngine(destination);
|
||||
await post(`/destinations/${id}/start.json`, { engine });
|
||||
return toast.push($t('destination.coolify_proxy_started'));
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function forceRestartProxy() {
|
||||
const sure = confirm($t('destination.confirm_restart_proxy'));
|
||||
if (sure) {
|
||||
try {
|
||||
restarting = true;
|
||||
toast.push($t('destination.coolify_proxy_restarting'));
|
||||
await post(`/destinations/${id}/restart.json`, {
|
||||
engine: destination.engine,
|
||||
fqdn: settings.fqdn
|
||||
});
|
||||
} catch ({ error }) {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
|
||||
<div class="flex space-x-1 pb-5">
|
||||
<div class="title font-bold">{$t('forms.configuration')}</div>
|
||||
{#if $appSession.isAdmin}
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-sky-600 hover:bg-sky-500"
|
||||
class:bg-sky-600={!loading}
|
||||
class:hover:bg-sky-500={!loading}
|
||||
disabled={loading}
|
||||
>{loading ? $t('forms.saving') : $t('forms.save')}
|
||||
</button>
|
||||
<button
|
||||
class={restarting ? '' : 'bg-red-600 hover:bg-red-500'}
|
||||
disabled={restarting}
|
||||
on:click|preventDefault={forceRestartProxy}
|
||||
>{restarting
|
||||
? $t('destination.restarting_please_wait')
|
||||
: $t('destination.force_restart_proxy')}</button
|
||||
>
|
||||
{/if}
|
||||
<!-- <button type="button" class="bg-coollabs hover:bg-coollabs-100" on:click={scanApps}
|
||||
>Scan for applications</button
|
||||
> -->
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10 ">
|
||||
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
|
||||
<input
|
||||
name="name"
|
||||
placeholder={$t('forms.name')}
|
||||
disabled={!$appSession.isAdmin}
|
||||
readonly={!$appSession.isAdmin}
|
||||
bind:value={destination.name}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="engine" class="text-base font-bold text-stone-100">{$t('forms.engine')}</label>
|
||||
<CopyPasswordField
|
||||
id="engine"
|
||||
readonly
|
||||
disabled
|
||||
name="engine"
|
||||
placeholder="{$t('forms.eg')}: /var/run/docker.sock"
|
||||
value={destination.engine}
|
||||
/>
|
||||
</div>
|
||||
<!-- <div class="flex items-center">
|
||||
<label for="remoteEngine">Remote Engine?</label>
|
||||
<input name="remoteEngine" type="checkbox" bind:checked={payload.remoteEngine} />
|
||||
</div> -->
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
|
||||
<CopyPasswordField
|
||||
id="network"
|
||||
readonly
|
||||
disabled
|
||||
name="network"
|
||||
placeholder="{$t('forms.default')}: coolify"
|
||||
value={destination.network}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
disabled={cannotDisable}
|
||||
bind:setting={destination.isCoolifyProxyUsed}
|
||||
on:click={changeProxySetting}
|
||||
title={$t('destination.use_coolify_proxy')}
|
||||
description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration. Databases will have their own proxy. <br><br>${
|
||||
cannotDisable
|
||||
? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>'
|
||||
: ''
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<!-- <div class="flex justify-center">
|
||||
{#if payload.isCoolifyProxyUsed}
|
||||
{#if state}
|
||||
<button on:click={stopProxy}>Stop proxy</button>
|
||||
{:else}
|
||||
<button on:click={startProxy}>Start proxy</button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div> -->
|
||||
|
||||
<!-- {#if scannedApps.length > 0}
|
||||
<div class="flex justify-center px-6 pb-10">
|
||||
<div class="flex space-x-2 h-8 items-center">
|
||||
<div class="font-bold text-xl text-white">Found applications</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="max-w-4xl mx-auto px-6">
|
||||
<div class="flex space-x-2 justify-center">
|
||||
{#each scannedApps as app}
|
||||
<FoundApp {app} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if} -->
|
||||
71
apps/ui/src/routes/destinations/[id]/__layout.svelte
Normal file
71
apps/ui/src/routes/destinations/[id]/__layout.svelte
Normal file
@@ -0,0 +1,71 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, url, params }) => {
|
||||
try {
|
||||
const { id } = params;
|
||||
const response = await get(`/destinations/${id}`);
|
||||
const { destination, settings, state } = response;
|
||||
if (id !== 'new' && (!destination || Object.entries(destination).length === 0)) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/destinations'
|
||||
};
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
destination
|
||||
},
|
||||
stuff: {
|
||||
destination,
|
||||
settings,
|
||||
state
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return handlerNotFoundLoad(error, url);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let destination: any;
|
||||
|
||||
import { del, get } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { errorNotification, handlerNotFoundLoad } from '$lib/common';
|
||||
import { appSession } from '$lib/store';
|
||||
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
|
||||
|
||||
const { id } = $page.params;
|
||||
|
||||
async function deleteDestination(destination: any) {
|
||||
const sure = confirm($t('application.confirm_to_delete', { name: destination.name }));
|
||||
if (sure) {
|
||||
try {
|
||||
await del(`/destinations/${destination.id}`, { id: destination.id });
|
||||
return await goto('/destinations');
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if id !== 'new'}
|
||||
<nav class="nav-side">
|
||||
<button
|
||||
on:click={() => deleteDestination(destination)}
|
||||
title={$t('source.delete_git_source')}
|
||||
type="submit"
|
||||
disabled={!$appSession.isAdmin}
|
||||
class:hover:text-red-500={$appSession.isAdmin}
|
||||
class="icons tooltip-bottom bg-transparent text-sm"
|
||||
data-tooltip={$appSession.isAdmin
|
||||
? $t('destination.delete_destination')
|
||||
: $t('destination.permission_denied_delete_destination')}><DeleteIcon /></button
|
||||
>
|
||||
</nav>
|
||||
{/if}
|
||||
<slot />
|
||||
24
apps/ui/src/routes/destinations/[id]/index.svelte
Normal file
24
apps/ui/src/routes/destinations/[id]/index.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, stuff }) => {
|
||||
return {
|
||||
props: { ...stuff }
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let destination: any;
|
||||
export let settings: any;
|
||||
export let state: any;
|
||||
import { page } from '$app/stores';
|
||||
import New from './_New.svelte';
|
||||
import Destination from './_Destination.svelte';
|
||||
const { id } = $page.params;
|
||||
</script>
|
||||
|
||||
{#if id === 'new'}
|
||||
<New />
|
||||
{:else}
|
||||
<Destination bind:destination bind:state bind:settings />
|
||||
{/if}
|
||||
99
apps/ui/src/routes/destinations/index.svelte
Normal file
99
apps/ui/src/routes/destinations/index.svelte
Normal file
@@ -0,0 +1,99 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async () => {
|
||||
try {
|
||||
const response = await get(`/destinations`);
|
||||
return {
|
||||
props: {
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(error)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let destinations: any[];
|
||||
|
||||
import { t } from '$lib/translations';
|
||||
import { appSession } from '$lib/store';
|
||||
import { get } from '$lib/api';
|
||||
|
||||
const ownDestinations = destinations.filter((destination) => {
|
||||
if (destination.teams[0].id === $appSession.teamId) {
|
||||
return destination;
|
||||
}
|
||||
});
|
||||
const otherDestinations = destinations.filter((destination) => {
|
||||
if (destination.teams[0].id !== $appSession.teamId) {
|
||||
return destination;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">{$t('index.destinations')}</div>
|
||||
{#if $appSession.isAdmin}
|
||||
<a href="/destinations/new" 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>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-col justify-center">
|
||||
{#if !destinations || ownDestinations.length === 0}
|
||||
<div class="flex-col">
|
||||
<div class="text-center text-xl font-bold">{$t('destination.no_destination_found')}</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if ownDestinations.length > 0 || otherDestinations.length > 0}
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each ownDestinations as destination}
|
||||
<a href="/destinations/{destination.id}" class="w-96 p-2 no-underline">
|
||||
<div class="box-selection hover:bg-sky-600">
|
||||
<div class="truncate text-center text-xl font-bold">{destination.name}</div>
|
||||
{#if $appSession.teamId === '0' && otherDestinations.length > 0}
|
||||
<div class="truncate text-center">{destination.teams[0].name}</div>
|
||||
{/if}
|
||||
<div class="truncate text-center">{destination.network}</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if otherDestinations.length > 0 && $appSession.teamId === '0'}
|
||||
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Destinations</div>
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each otherDestinations as destination}
|
||||
<a href="/destinations/{destination.id}" class="w-96 p-2 no-underline">
|
||||
<div class="box-selection hover:bg-sky-600">
|
||||
<div class="truncate text-center text-xl font-bold">{destination.name}</div>
|
||||
{#if $appSession.teamId === '0'}
|
||||
<div class="truncate text-center">{destination.teams[0].name}</div>
|
||||
{/if}
|
||||
<div class="truncate text-center">{destination.network}</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
242
apps/ui/src/routes/iam/index.svelte
Normal file
242
apps/ui/src/routes/iam/index.svelte
Normal file
@@ -0,0 +1,242 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async () => {
|
||||
try {
|
||||
const response = await get(`/iam`);
|
||||
return {
|
||||
props: {
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(error)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let account: any;
|
||||
export let accounts: any;
|
||||
export let invitations: any;
|
||||
export let ownTeams: any;
|
||||
export let allTeams: any;
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { del, get, post } from '$lib/api';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { appSession } from '$lib/store';
|
||||
import { goto } from '$app/navigation';
|
||||
import Cookies from 'js-cookie';
|
||||
if (accounts.length === 0) {
|
||||
accounts.push(account);
|
||||
}
|
||||
|
||||
async function resetPassword(id: any) {
|
||||
const sure = window.confirm('Are you sure you want to reset the password?');
|
||||
if (!sure) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await post(`/iam/user/password`, { id });
|
||||
toast.push('Password reset successfully. Please relogin to reset it.');
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function deleteUser(id: any) {
|
||||
const sure = window.confirm('Are you sure you want to delete this user?');
|
||||
if (!sure) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await del(`/iam/user/remove`, { uid: id });
|
||||
toast.push('Account deleted.');
|
||||
const data = await get('/iam');
|
||||
accounts = data.accounts;
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function acceptInvitation(id: any, teamId: any) {
|
||||
try {
|
||||
await post(`/iam/team/${teamId}/invitation/accept`, { id });
|
||||
return window.location.reload();
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function revokeInvitation(id: any, teamId: any) {
|
||||
try {
|
||||
await post(`/iam/team/${teamId}/invitation/revoke`, { id });
|
||||
return window.location.reload();
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function switchTeam(selectedTeamId: any) {
|
||||
try {
|
||||
const payload = await get(`/user?teamId=${selectedTeamId}`);
|
||||
if (payload.token) {
|
||||
Cookies.set('token', payload.token, {
|
||||
path: '/'
|
||||
});
|
||||
$appSession.teamId = payload.teamId;
|
||||
$appSession.userId = payload.userId;
|
||||
$appSession.permission = payload.permission;
|
||||
$appSession.isAdmin = payload.isAdmin;
|
||||
return window.location.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function newTeam() {
|
||||
const { id } = await post('/iam/new', {});
|
||||
return await goto(`/iam/team/${id}`, { replaceState: true });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">Identity and Access Management</div>
|
||||
<div on:click={newTeam} class="add-icon cursor-pointer bg-fuchsia-600 hover:bg-fuchsia-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
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if invitations.length > 0}
|
||||
<div class="mx-auto max-w-4xl px-6 py-4">
|
||||
<div class="title font-bold">Pending invitations</div>
|
||||
<div class="pt-10 text-center">
|
||||
{#each invitations as invitation}
|
||||
<div class="flex justify-center space-x-2">
|
||||
<div>
|
||||
Invited to <span class="font-bold text-pink-600">{invitation.teamName}</span> with
|
||||
<span class="font-bold text-rose-600">{invitation.permission}</span> permission.
|
||||
</div>
|
||||
<button
|
||||
class="hover:bg-green-500"
|
||||
on:click={() => acceptInvitation(invitation.id, invitation.teamId)}>Accept</button
|
||||
>
|
||||
<button
|
||||
class="hover:bg-red-600"
|
||||
on:click={() => revokeInvitation(invitation.id, invitation.teamId)}>Delete</button
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mx-auto max-w-4xl px-6 py-4">
|
||||
{#if $appSession.teamId === '0' && accounts.length > 0}
|
||||
<div class="title font-bold">Accounts</div>
|
||||
{:else}
|
||||
<div class="title font-bold">Account</div>
|
||||
{/if}
|
||||
<div class="flex items-center justify-center pt-10">
|
||||
<table class="mx-2 text-left">
|
||||
<thead class="mb-2">
|
||||
<tr>
|
||||
{#if accounts.length > 1}
|
||||
<th class="px-2">Email</th>
|
||||
<th>Actions</th>
|
||||
{/if}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{#each accounts as account}
|
||||
<tr>
|
||||
<td class="px-2">{account.email}</td>
|
||||
<td class="flex space-x-2">
|
||||
<form on:submit|preventDefault={() => resetPassword(account.id)}>
|
||||
<button
|
||||
class="mx-auto my-4 w-32 bg-fuchsia-600 hover:bg-fuchsia-500 disabled:bg-coolgray-200"
|
||||
>Reset Password</button
|
||||
>
|
||||
</form>
|
||||
<form on:submit|preventDefault={() => deleteUser(account.id)}>
|
||||
<button
|
||||
disabled={account.id === $appSession.userId}
|
||||
class="mx-auto my-4 w-32 bg-red-600 hover:bg-red-500 disabled:bg-coolgray-200"
|
||||
type="submit">Delete User</button
|
||||
>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-4xl px-6">
|
||||
<div class="title font-bold">Teams</div>
|
||||
<div class="flex items-center justify-center pt-10">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-row flex-wrap justify-center px-2 pb-10 md:flex-row">
|
||||
{#each ownTeams as team}
|
||||
<a href="/iam/team/{team.id}" class="w-96 p-2 no-underline">
|
||||
<div class="box-selection relative">
|
||||
<div>
|
||||
<div class="truncate text-center text-xl font-bold">
|
||||
{team.name}
|
||||
</div>
|
||||
<div class="mt-1 text-center text-xs">
|
||||
{team.permissions?.length} member(s)
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-center pt-3">
|
||||
<button
|
||||
on:click|preventDefault={() => switchTeam(team.id)}
|
||||
class:bg-fuchsia-600={$appSession.teamId !== team.id}
|
||||
class:hover:bg-fuchsia-500={$appSession.teamId !== team.id}
|
||||
class:bg-transparent={$appSession.teamId === team.id}
|
||||
disabled={$appSession.teamId === team.id}
|
||||
>{$appSession.teamId === team.id ? 'Current Team' : 'Switch Team'}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{#if $appSession.teamId === '0' && allTeams.length > 0}
|
||||
<div class="pb-5 pt-10 text-xl font-bold">Other Teams</div>
|
||||
<div class="flex flex-row flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each allTeams as team}
|
||||
<a href="/iam/team/{team.id}" class="w-96 p-2 no-underline">
|
||||
<div
|
||||
class="box-selection relative"
|
||||
class:hover:bg-fuchsia-600={team.id !== '0'}
|
||||
class:hover:bg-red-500={team.id === '0'}
|
||||
>
|
||||
<div class="truncate text-center text-xl font-bold">
|
||||
{team.name}
|
||||
</div>
|
||||
|
||||
<div class="mt-1 text-center">{team.permissions?.length} member(s)</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
63
apps/ui/src/routes/iam/team/[id]/__layout.svelte
Normal file
63
apps/ui/src/routes/iam/team/[id]/__layout.svelte
Normal file
@@ -0,0 +1,63 @@
|
||||
<script context="module" lang="ts">
|
||||
import { del, get } from '$lib/api';
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ params, url }) => {
|
||||
try {
|
||||
const response = await get(`/iam/team/${params.id}`);
|
||||
if (!response.permissions || Object.entries(response.permissions).length === 0) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/iam'
|
||||
};
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
...response
|
||||
},
|
||||
stuff: {
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return handlerNotFoundLoad(error, url);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { errorNotification, handlerNotFoundLoad } from '$lib/common';
|
||||
import { appSession } from '$lib/store';
|
||||
import { t } from '$lib/translations';
|
||||
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
const { id } = $page.params;
|
||||
async function deleteTeam() {
|
||||
const sure = confirm('Are you sure you want to delete this team?');
|
||||
if (sure) {
|
||||
try {
|
||||
await del(`/iam/team/${id}`, { id });
|
||||
return await goto('/iam', { replaceState: true });
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if id !== 'new'}
|
||||
<nav class="nav-side">
|
||||
<button
|
||||
on:click={deleteTeam}
|
||||
title={$t('source.delete_git_source')}
|
||||
type="submit"
|
||||
disabled={!$appSession.isAdmin}
|
||||
class:hover:text-red-500={$appSession.isAdmin}
|
||||
class="icons tooltip-bottom bg-transparent text-sm"
|
||||
data-tooltip={$appSession.isAdmin
|
||||
? 'Delete Team'
|
||||
: $t('destination.permission_denied_delete_destination')}><DeleteIcon /></button
|
||||
>
|
||||
</nav>
|
||||
{/if}
|
||||
<slot />
|
||||
210
apps/ui/src/routes/iam/team/[id]/index.svelte
Normal file
210
apps/ui/src/routes/iam/team/[id]/index.svelte
Normal file
@@ -0,0 +1,210 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, stuff }) => {
|
||||
return {
|
||||
props: { ...stuff }
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let permissions: any;
|
||||
export let team: any;
|
||||
export let invitations: any[];
|
||||
import { page } from '$app/stores';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { post } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { appSession } from '$lib/store';
|
||||
const { id } = $page.params;
|
||||
let invitation: any = {
|
||||
teamName: team.name,
|
||||
email: null,
|
||||
permission: 'read'
|
||||
};
|
||||
function isAdmin(permission: string) {
|
||||
if (permission === 'admin' || permission === 'owner') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function sendInvitation() {
|
||||
try {
|
||||
await post(`/iam/team/${id}/invitation/invite`, {
|
||||
teamId: team.id,
|
||||
teamName: invitation.teamName,
|
||||
email: invitation.email.toLowerCase(),
|
||||
permission: invitation.permission
|
||||
});
|
||||
return window.location.reload();
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function revokeInvitation(id: string) {
|
||||
try {
|
||||
await post(`/iam/team/${id}/invitation/revoke`, { id });
|
||||
return window.location.reload();
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function removeFromTeam(uid: string) {
|
||||
try {
|
||||
await post(`/iam/team/${id}/user/remove`, { teamId: team.id, uid });
|
||||
return window.location.reload();
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function changePermission(userId: string, permissionId: string, currentPermission: string) {
|
||||
let newPermission = 'read';
|
||||
if (currentPermission === 'read') {
|
||||
newPermission = 'admin';
|
||||
}
|
||||
try {
|
||||
await post(`/iam/team/${id}/permission`, { userId, newPermission, permissionId });
|
||||
return window.location.reload();
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await post(`/iam/team/${id}`, { ...team });
|
||||
return window.location.reload();
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 px-6 text-2xl font-bold">
|
||||
<div class="tracking-tight">{$t('index.team')}</div>
|
||||
<span class="arrow-right-applications px-1 text-fuchsia-500">></span>
|
||||
<span class="pr-2">{team.name}</span>
|
||||
</div>
|
||||
<div class="mx-auto max-w-4xl px-6">
|
||||
<form on:submit|preventDefault={handleSubmit} class=" py-4">
|
||||
<div class="flex space-x-1 pb-5">
|
||||
<div class="title font-bold">{$t('index.settings')}</div>
|
||||
<button class="bg-fuchsia-600 hover:bg-fuchsia-500" type="submit">{$t('forms.save')}</button>
|
||||
</div>
|
||||
<div class="grid grid-flow-row gap-2 px-10">
|
||||
<div class="mt-2 grid grid-cols-2">
|
||||
<div class="flex-col">
|
||||
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
|
||||
{#if team.id === '0'}
|
||||
<Explainer customClass="w-full" text={$t('team.root_team_explainer')} />
|
||||
{/if}
|
||||
</div>
|
||||
<input id="name" name="name" placeholder="name" bind:value={team.name} />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="flex space-x-1 py-5 pt-10 font-bold">
|
||||
<div class="title">{$t('team.members')}</div>
|
||||
</div>
|
||||
<div class="px-4 sm:px-6">
|
||||
<table class="w-full border-separate text-left">
|
||||
<thead>
|
||||
<tr class="h-8 border-b border-coolgray-400">
|
||||
<th scope="col">{$t('forms.email')}</th>
|
||||
<th scope="col">{$t('team.permission')}</th>
|
||||
<th scope="col" class="text-center">{$t('forms.action')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{#each permissions as permission}
|
||||
<tr class="text-xs">
|
||||
<td class="py-4"
|
||||
>{permission.user.email}
|
||||
<span class="font-bold"
|
||||
>{permission.user.id === $appSession.userId ? $t('team.you') : ''}</span
|
||||
></td
|
||||
>
|
||||
<td class="py-4">{permission.permission}</td>
|
||||
{#if $appSession.isAdmin && permission.user.id !== $appSession.userId && permission.permission !== 'owner'}
|
||||
<td class="flex flex-col items-center justify-center space-y-2 py-4 text-center">
|
||||
<button
|
||||
class="w-52 bg-red-600 hover:bg-red-500"
|
||||
on:click={() => removeFromTeam(permission.user.id)}>{$t('forms.remove')}</button
|
||||
>
|
||||
<button
|
||||
class="w-52"
|
||||
on:click={() =>
|
||||
changePermission(permission.user.id, permission.id, permission.permission)}
|
||||
>{$t('team.promote_to', {
|
||||
grade: permission.permission === 'admin' ? 'read' : 'admin'
|
||||
})}</button
|
||||
>
|
||||
</td>
|
||||
{:else}
|
||||
<td class="text-center py-4 flex-col space-y-2">
|
||||
{$t('forms.no_actions_available')}
|
||||
</td>
|
||||
{/if}
|
||||
</tr>
|
||||
{/each}
|
||||
|
||||
{#each invitations as invitation}
|
||||
<tr class="text-xs">
|
||||
<td class="py-4 font-bold text-yellow-500">{invitation.email} </td>
|
||||
<td class="py-4 font-bold text-yellow-500">{invitation.permission}</td>
|
||||
{#if isAdmin(team.permissions[0].permission)}
|
||||
<td class="flex-col space-y-2 py-4 text-center">
|
||||
<button
|
||||
class="w-52 bg-red-600 hover:bg-red-500"
|
||||
on:click={() => revokeInvitation(invitation.id)}
|
||||
>{$t('team.revoke_invitation')}</button
|
||||
>
|
||||
</td>
|
||||
{:else}
|
||||
<td class="text-center py-4 flex-col space-y-2">{$t('team.pending_invitation')}</td>
|
||||
{/if}
|
||||
</tr>
|
||||
{/each}
|
||||
</table>
|
||||
</div>
|
||||
{#if $appSession.isAdmin}
|
||||
<form on:submit|preventDefault={sendInvitation} class="py-5 pt-10">
|
||||
<div class="flex space-x-1">
|
||||
<div class="flex space-x-1">
|
||||
<div class="title font-bold">{$t('team.invite_new_member')}</div>
|
||||
<button class="bg-fuchsia-600 hover:bg-fuchsia-500" type="submit"
|
||||
>{$t('team.send_invitation')}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<Explainer text={$t('team.invite_only_register_explainer')} />
|
||||
<div class="flex-col space-y-2 px-4 pt-5 sm:px-6">
|
||||
<div class="flex space-x-0">
|
||||
<input
|
||||
bind:value={invitation.email}
|
||||
placeholder={$t('forms.email')}
|
||||
class="mr-2 w-full"
|
||||
required
|
||||
/>
|
||||
<div class="flex-1" />
|
||||
<button
|
||||
on:click={() => (invitation.permission = 'read')}
|
||||
class="rounded-none rounded-l border border-dashed border-transparent"
|
||||
type="button"
|
||||
class:border-coolgray-300={invitation.permission !== 'read'}
|
||||
class:bg-fuchsia-500={invitation.permission === 'read'}>{$t('team.read')}</button
|
||||
>
|
||||
<button
|
||||
on:click={() => (invitation.permission = 'admin')}
|
||||
class="rounded-none rounded-r border border-dashed border-transparent"
|
||||
type="button"
|
||||
class:border-coolgray-300={invitation.permission !== 'admin'}
|
||||
class:bg-red-500={invitation.permission === 'admin'}>{$t('team.admin')}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
110
apps/ui/src/routes/index.svelte
Normal file
110
apps/ui/src/routes/index.svelte
Normal file
@@ -0,0 +1,110 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
|
||||
export const load: Load = async ({}) => {
|
||||
try {
|
||||
const data = await get('/resources');
|
||||
return {
|
||||
props: {
|
||||
...data
|
||||
},
|
||||
stuff: {
|
||||
...data
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { get } from '$lib/api';
|
||||
import Usage from '$lib/components/Usage.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
|
||||
export let applicationsCount: number = 0;
|
||||
export let sourcesCount: number = 0;
|
||||
export let destinationsCount: number = 0;
|
||||
export let teamsCount: number = 0;
|
||||
export let databasesCount: number = 0;
|
||||
export let servicesCount: number = 0;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">{$t('index.dashboard')}</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-10 pb-12 tracking-tight sm:pb-16">
|
||||
<div class="mx-auto max-w-4xl">
|
||||
<Usage />
|
||||
<dl class="mt-5 grid grid-cols-1 gap-5 px-2 sm:grid-cols-3">
|
||||
<a
|
||||
href="/applications"
|
||||
sveltekit:prefetch
|
||||
class="overflow-hidden rounded px-4 py-5 text-center text-green-500 no-underline transition-all duration-100 hover:bg-green-500 hover:text-white sm:p-6 sm:text-left"
|
||||
>
|
||||
<dt class="truncate text-sm font-medium text-white">{$t('index.applications')}</dt>
|
||||
<dd class="mt-1 text-3xl font-semibold ">
|
||||
{applicationsCount}
|
||||
</dd>
|
||||
</a>
|
||||
<a
|
||||
href="/destinations"
|
||||
sveltekit:prefetch
|
||||
class="overflow-hidden rounded px-4 py-5 text-center text-sky-500 no-underline transition-all duration-100 hover:bg-sky-500 hover:text-white sm:p-6 sm:text-left"
|
||||
>
|
||||
<dt class="truncate text-sm font-medium text-white">{$t('index.destinations')}</dt>
|
||||
<dd class="mt-1 text-3xl font-semibold ">
|
||||
{destinationsCount}
|
||||
</dd>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/sources"
|
||||
sveltekit:prefetch
|
||||
class="overflow-hidden rounded px-4 py-5 text-center text-orange-500 no-underline transition-all duration-100 hover:bg-orange-500 hover:text-white sm:p-6 sm:text-left"
|
||||
>
|
||||
<dt class="truncate text-sm font-medium text-white">{$t('index.git_sources')}</dt>
|
||||
<dd class="mt-1 text-3xl font-semibold">
|
||||
{sourcesCount}
|
||||
</dd>
|
||||
</a>
|
||||
</dl>
|
||||
<dl class="mt-5 grid grid-cols-1 gap-5 px-2 sm:grid-cols-3">
|
||||
<a
|
||||
href="/databases"
|
||||
sveltekit:prefetch
|
||||
class="overflow-hidden rounded px-4 py-5 text-center text-purple-500 no-underline transition-all duration-100 hover:bg-purple-500 hover:text-white sm:p-6 sm:text-left"
|
||||
>
|
||||
<dt class="truncate text-sm font-medium text-white">{$t('index.databases')}</dt>
|
||||
<dd class="mt-1 text-3xl font-semibold ">
|
||||
{databasesCount}
|
||||
</dd>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/services"
|
||||
sveltekit:prefetch
|
||||
class="overflow-hidden rounded px-4 py-5 text-center text-pink-500 no-underline transition-all duration-100 hover:bg-pink-500 hover:text-white sm:p-6 sm:text-left"
|
||||
>
|
||||
<dt class="truncate text-sm font-medium text-white">{$t('index.services')}</dt>
|
||||
<dd class="mt-1 text-3xl font-semibold ">
|
||||
{servicesCount}
|
||||
</dd>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/iam"
|
||||
sveltekit:prefetch
|
||||
class="overflow-hidden rounded px-4 py-5 text-center text-cyan-500 no-underline transition-all duration-100 hover:bg-cyan-500 hover:text-white sm:p-6 sm:text-left"
|
||||
>
|
||||
<dt class="truncate text-sm font-medium text-white">{$t('index.teams')}</dt>
|
||||
<dd class="mt-1 text-3xl font-semibold ">
|
||||
{teamsCount}
|
||||
</dd>
|
||||
</a>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
109
apps/ui/src/routes/login.svelte
Normal file
109
apps/ui/src/routes/login.svelte
Normal file
@@ -0,0 +1,109 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/env';
|
||||
import Cookies from 'js-cookie';
|
||||
import { goto } from '$app/navigation';
|
||||
import { post } from '$lib/api';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { appSession, loginEmail } from '$lib/store';
|
||||
import { t } from '$lib/translations';
|
||||
import { onMount } from 'svelte';
|
||||
let loading = false;
|
||||
let emailEl: HTMLInputElement;
|
||||
let email: string, password: string;
|
||||
|
||||
onMount(async () => {
|
||||
if ($appSession.userId) {
|
||||
return await goto('/');
|
||||
}
|
||||
emailEl.focus();
|
||||
});
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
try {
|
||||
const { token, payload } = await post(`/login`, {
|
||||
email: email.toLowerCase(),
|
||||
password,
|
||||
isLogin: true
|
||||
});
|
||||
Cookies.set('token', token, {
|
||||
path: '/'
|
||||
});
|
||||
$appSession.teamId = payload.teamId;
|
||||
$appSession.userId = payload.userId;
|
||||
$appSession.permission = payload.permission;
|
||||
$appSession.isAdmin = payload.isAdmin;
|
||||
return await goto('/');
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
async function gotoRegister() {
|
||||
$loginEmail = email?.toLowerCase();
|
||||
return await goto('/register');
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelt:head>
|
||||
<title>{$t('login.login')}</title>
|
||||
</svelt:head>
|
||||
|
||||
<div class="flex h-screen flex-col items-center justify-center">
|
||||
<div class="flex justify-center px-4">
|
||||
<form on:submit|preventDefault={handleSubmit} class="flex flex-col py-4 space-y-2">
|
||||
{#if $appSession.whiteLabeledDetails.icon}
|
||||
<img
|
||||
class="w-32 mx-auto pb-8"
|
||||
src={$appSession.whiteLabeledDetails.icon}
|
||||
alt="Icon for white labeled version of Coolify"
|
||||
/>
|
||||
{:else}
|
||||
<div class="text-6xl font-bold border-gradient w-48 mx-auto border-b-4 mb-8">Coolify</div>
|
||||
{/if}
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder={$t('forms.email')}
|
||||
autocomplete="off"
|
||||
required
|
||||
bind:this={emailEl}
|
||||
bind:value={email}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder={$t('forms.password')}
|
||||
bind:value={password}
|
||||
required
|
||||
/>
|
||||
|
||||
<div class="flex space-x-2 h-8 items-center justify-center pt-8">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
class="hover:opacity-90 text-white"
|
||||
class:bg-transparent={loading}
|
||||
class:text-stone-600={loading}
|
||||
class:bg-coollabs={!loading}
|
||||
>{loading ? $t('login.authenticating') : $t('login.login')}</button
|
||||
>
|
||||
|
||||
<button
|
||||
on:click|preventDefault={gotoRegister}
|
||||
class="bg-transparent hover:bg-coolgray-300 text-white ">{$t('register.register')}</button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{#if browser && window.location.host === 'demo.coolify.io'}
|
||||
<div class="pt-5 font-bold">
|
||||
Registration is <span class="text-pink-500">open</span>, just fill in an email (does not need
|
||||
to be live email address for the demo instance) and a password.
|
||||
</div>
|
||||
<div class="pt-5 font-bold">
|
||||
All users gets an <span class="text-pink-500">own namespace</span>, so you won't be able to
|
||||
access other users data.
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
140
apps/ui/src/routes/register.svelte
Normal file
140
apps/ui/src/routes/register.svelte
Normal file
@@ -0,0 +1,140 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, stuff }) => {
|
||||
return {
|
||||
props: { ...stuff }
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let userCount: number;
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import { post } from '$lib/api';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { appSession, loginEmail } from '$lib/store';
|
||||
import { t } from '$lib/translations';
|
||||
import { onMount } from 'svelte';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
let loading = false;
|
||||
let emailEl: HTMLInputElement;
|
||||
let passwordEl: HTMLInputElement;
|
||||
let email: string | undefined, password: string, passwordCheck: string;
|
||||
|
||||
onMount(() => {
|
||||
email = $loginEmail;
|
||||
if (email) {
|
||||
passwordEl.focus();
|
||||
} else {
|
||||
emailEl.focus();
|
||||
}
|
||||
});
|
||||
async function handleSubmit() {
|
||||
// Prevent double submission
|
||||
if (loading) return;
|
||||
|
||||
if (password !== passwordCheck) {
|
||||
return errorNotification($t('forms.passwords_not_match'));
|
||||
}
|
||||
loading = true;
|
||||
try {
|
||||
const { token, payload } = await post(`/login`, {
|
||||
email: email?.toLowerCase(),
|
||||
password,
|
||||
isLogin: false
|
||||
});
|
||||
Cookies.set('token', token, {
|
||||
path: '/'
|
||||
});
|
||||
$appSession.teamId = payload.teamId;
|
||||
$appSession.userId = payload.userId;
|
||||
$appSession.permission = payload.permission;
|
||||
$appSession.isAdmin = payload.isAdmin;
|
||||
return await goto('/');
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="icons fixed top-0 left-0 m-3 cursor-pointer" on:click={() => goto('/')}>
|
||||
<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" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
<line x1="5" y1="12" x2="11" y2="18" />
|
||||
<line x1="5" y1="12" x2="11" y2="6" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex h-screen flex-col items-center justify-center">
|
||||
{#if $appSession.userId}
|
||||
<div class="flex justify-center px-4 text-xl font-bold">{$t('login.already_logged_in')}</div>
|
||||
{:else}
|
||||
<div class="flex justify-center px-4">
|
||||
<form on:submit|preventDefault={handleSubmit} class="flex flex-col py-4 space-y-2">
|
||||
{#if $appSession.whiteLabeledDetails.icon}
|
||||
<img
|
||||
class="w-32 mx-auto pb-8"
|
||||
src={$appSession.whiteLabeledDetails.icon}
|
||||
alt="Icon for white labeled version of Coolify"
|
||||
/>
|
||||
{:else}
|
||||
<div class="text-6xl font-bold border-gradient w-48 mx-auto border-b-4 mb-8">Coolify</div>
|
||||
{/if}
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder={$t('forms.email')}
|
||||
autocomplete="off"
|
||||
required
|
||||
bind:this={emailEl}
|
||||
bind:value={email}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder={$t('forms.password')}
|
||||
bind:this={passwordEl}
|
||||
bind:value={password}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
name="passwordCheck"
|
||||
placeholder={$t('forms.password_again')}
|
||||
bind:value={passwordCheck}
|
||||
required
|
||||
/>
|
||||
|
||||
<div class="flex space-x-2 h-8 items-center justify-center pt-8">
|
||||
<button
|
||||
type="submit"
|
||||
class="hover:bg-coollabs-100 text-white"
|
||||
disabled={loading}
|
||||
class:bg-transparent={loading}
|
||||
class:text-stone-600={loading}
|
||||
class:bg-coollabs={!loading}
|
||||
>{loading ? $t('register.registering') : $t('register.register')}</button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{#if userCount === 0}
|
||||
<div class="pt-5">
|
||||
{$t('register.first_user')}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
87
apps/ui/src/routes/services/[id]/_Secret.svelte
Normal file
87
apps/ui/src/routes/services/[id]/_Secret.svelte
Normal file
@@ -0,0 +1,87 @@
|
||||
<script>
|
||||
export let name = '';
|
||||
export let value = '';
|
||||
export let isNewSecret = false;
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { del, post } from '$lib/api';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const { id } = $page.params;
|
||||
async function removeSecret() {
|
||||
try {
|
||||
await del(`/services/${id}/secrets`, { name });
|
||||
dispatch('refresh');
|
||||
if (isNewSecret) {
|
||||
name = '';
|
||||
value = '';
|
||||
}
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function saveSecret(isNew = false) {
|
||||
if (!name) return errorNotification('Name is required.');
|
||||
if (!value) return errorNotification('Value is required.');
|
||||
try {
|
||||
await post(`/services/${id}/secrets`, {
|
||||
name,
|
||||
value,
|
||||
|
||||
isNew
|
||||
});
|
||||
dispatch('refresh');
|
||||
if (isNewSecret) {
|
||||
name = '';
|
||||
value = '';
|
||||
}
|
||||
toast.push('Secret saved.');
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<td>
|
||||
<input
|
||||
id={isNewSecret ? 'secretName' : 'secretNameNew'}
|
||||
bind:value={name}
|
||||
required
|
||||
placeholder="EXAMPLE_VARIABLE"
|
||||
class=" border border-dashed border-coolgray-300"
|
||||
readonly={!isNewSecret}
|
||||
class:bg-transparent={!isNewSecret}
|
||||
class:cursor-not-allowed={!isNewSecret}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<CopyPasswordField
|
||||
id={isNewSecret ? 'secretValue' : 'secretValueNew'}
|
||||
name={isNewSecret ? 'secretValue' : 'secretValueNew'}
|
||||
isPasswordField={true}
|
||||
bind:value
|
||||
required
|
||||
placeholder="J$#@UIO%HO#$U%H"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{#if isNewSecret}
|
||||
<div class="flex items-center justify-center">
|
||||
<button class="bg-green-600 hover:bg-green-500" on:click={() => saveSecret(true)}>Add</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row justify-center space-x-2">
|
||||
<div class="flex items-center justify-center">
|
||||
<button class="" on:click={() => saveSecret(false)}>Set</button>
|
||||
</div>
|
||||
<div class="flex justify-center items-end">
|
||||
<button class="bg-red-600 hover:bg-red-500" on:click={removeSecret}>Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
76
apps/ui/src/routes/services/[id]/_ServiceLinks.svelte
Normal file
76
apps/ui/src/routes/services/[id]/_ServiceLinks.svelte
Normal file
@@ -0,0 +1,76 @@
|
||||
<script lang="ts">
|
||||
export let service: any;
|
||||
|
||||
import Fider from '$lib/components/svg/services/Fider.svelte';
|
||||
|
||||
import Ghost from '$lib/components/svg/services/Ghost.svelte';
|
||||
import Hasura from '$lib/components/svg/services/Hasura.svelte';
|
||||
|
||||
import LanguageTool from '$lib/components/svg/services/LanguageTool.svelte';
|
||||
|
||||
import MinIo from '$lib/components/svg/services/MinIO.svelte';
|
||||
import N8n from '$lib/components/svg/services/N8n.svelte';
|
||||
|
||||
import NocoDb from '$lib/components/svg/services/NocoDB.svelte';
|
||||
|
||||
import PlausibleAnalytics from '$lib/components/svg/services/PlausibleAnalytics.svelte';
|
||||
import Umami from '$lib/components/svg/services/Umami.svelte';
|
||||
import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte';
|
||||
import VaultWarden from '$lib/components/svg/services/VaultWarden.svelte';
|
||||
import VsCodeServer from '$lib/components/svg/services/VSCodeServer.svelte';
|
||||
import Wordpress from '$lib/components/svg/services/Wordpress.svelte';
|
||||
</script>
|
||||
|
||||
{#if service.type === 'plausibleanalytics'}
|
||||
<a href="https://plausible.io" target="_blank">
|
||||
<PlausibleAnalytics />
|
||||
</a>
|
||||
{:else if service.type === 'nocodb'}
|
||||
<a href="https://nocodb.com" target="_blank">
|
||||
<NocoDb />
|
||||
</a>
|
||||
{:else if service.type === 'minio'}
|
||||
<a href="https://min.io" target="_blank">
|
||||
<MinIo />
|
||||
</a>
|
||||
{:else if service.type === 'vscodeserver'}
|
||||
<a href="https://coder.com" target="_blank">
|
||||
<VsCodeServer />
|
||||
</a>
|
||||
{:else if service.type === 'wordpress'}
|
||||
<a href="https://wordpress.org" target="_blank">
|
||||
<Wordpress />
|
||||
</a>
|
||||
{:else if service.type === 'vaultwarden'}
|
||||
<a href="https://github.com/dani-garcia/vaultwarden" target="_blank">
|
||||
<VaultWarden />
|
||||
</a>
|
||||
{:else if service.type === 'languagetool'}
|
||||
<a href="https://languagetool.org/dev" target="_blank">
|
||||
<LanguageTool />
|
||||
</a>
|
||||
{:else if service.type === 'n8n'}
|
||||
<a href="https://n8n.io" target="_blank">
|
||||
<N8n />
|
||||
</a>
|
||||
{:else if service.type === 'uptimekuma'}
|
||||
<a href="https://github.com/louislam/uptime-kuma" target="_blank">
|
||||
<UptimeKuma />
|
||||
</a>
|
||||
{:else if service.type === 'ghost'}
|
||||
<a href="https://ghost.org" target="_blank">
|
||||
<Ghost />
|
||||
</a>
|
||||
{:else if service.type === 'umami'}
|
||||
<a href="https://umami.is" target="_blank">
|
||||
<Umami />
|
||||
</a>
|
||||
{:else if service.type === 'hasura'}
|
||||
<a href="https://hasura.io" target="_blank">
|
||||
<Hasura />
|
||||
</a>
|
||||
{:else if service.type === 'fider'}
|
||||
<a href="https://fider.io" target="_blank">
|
||||
<Fider />
|
||||
</a>
|
||||
{/if}
|
||||
183
apps/ui/src/routes/services/[id]/_Services/_Fider.svelte
Normal file
183
apps/ui/src/routes/services/[id]/_Services/_Fider.svelte
Normal file
@@ -0,0 +1,183 @@
|
||||
<script lang="ts">
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
import Select from 'svelte-select';
|
||||
export let service: any;
|
||||
export let readOnly: any;
|
||||
|
||||
let mailgunRegions = [
|
||||
{
|
||||
value: 'EU',
|
||||
label: 'EU'
|
||||
},
|
||||
{
|
||||
value: 'US',
|
||||
label: 'US'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Fider</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="jwtSecret">JWT Secret</label>
|
||||
<CopyPasswordField
|
||||
name="jwtSecret"
|
||||
id="jwtSecret"
|
||||
isPasswordField
|
||||
value={service.fider.jwtSecret}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailNoreply">Noreply Email</label>
|
||||
<input
|
||||
name="emailNoreply"
|
||||
id="emailNoreply"
|
||||
type="email"
|
||||
required
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
bind:value={service.fider.emailNoreply}
|
||||
placeholder="{$t('forms.eg')}: noreply@yourdomain.com"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Email</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailMailgunApiKey">Mailgun API Key</label>
|
||||
<CopyPasswordField
|
||||
name="emailMailgunApiKey"
|
||||
id="emailMailgunApiKey"
|
||||
isPasswordField
|
||||
bind:value={service.fider.emailMailgunApiKey}
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
placeholder="{$t('forms.eg')}: key-yourkeygoeshere"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailMailgunDomain">Mailgun Domain</label>
|
||||
<input
|
||||
name="emailMailgunDomain"
|
||||
id="emailMailgunDomain"
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
bind:value={service.fider.emailMailgunDomain}
|
||||
placeholder="{$t('forms.eg')}: yourdomain.com"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailMailgunRegion">Mailgun Region</label>
|
||||
<div class="custom-select-wrapper">
|
||||
<Select
|
||||
id="baseBuildImages"
|
||||
items={mailgunRegions}
|
||||
showIndicator
|
||||
on:select={(event) => (service.fider.emailMailgunRegion = event.detail.value)}
|
||||
value={service.fider.emailMailgunRegion || 'EU'}
|
||||
isClearable={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1 py-5 px-10 font-bold">
|
||||
<div class="text-lg">Or</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailSmtpHost">SMTP Host</label>
|
||||
<input
|
||||
name="emailSmtpHost"
|
||||
id="emailSmtpHost"
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
bind:value={service.fider.emailSmtpHost}
|
||||
placeholder="{$t('forms.eg')}: smtp.yourdomain.com"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailSmtpPort">SMTP Port</label>
|
||||
<input
|
||||
name="emailSmtpPort"
|
||||
id="emailSmtpPort"
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
bind:value={service.fider.emailSmtpPort}
|
||||
placeholder="{$t('forms.eg')}: 587"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailSmtpUser">SMTP User</label>
|
||||
<input
|
||||
name="emailSmtpUser"
|
||||
id="emailSmtpUser"
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
bind:value={service.fider.emailSmtpUser}
|
||||
placeholder="{$t('forms.eg')}: user@yourdomain.com"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailSmtpPassword">SMTP Password</label>
|
||||
<CopyPasswordField
|
||||
name="emailSmtpPassword"
|
||||
id="emailSmtpPassword"
|
||||
isPasswordField
|
||||
bind:value={service.fider.emailSmtpPassword}
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
placeholder="{$t('forms.eg')}: s0m3p4ssw0rd"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailSmtpEnableStartTls">SMTP Start TLS</label>
|
||||
<input
|
||||
name="emailSmtpEnableStartTls"
|
||||
id="emailSmtpEnableStartTls"
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
bind:value={service.fider.emailSmtpEnableStartTls}
|
||||
placeholder="{$t('forms.eg')}: true"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">PostgreSQL</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlUser">{$t('forms.username')}</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlUser"
|
||||
id="postgresqlUser"
|
||||
value={service.fider.postgresqlUser}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlPassword">{$t('forms.password')}</label>
|
||||
<CopyPasswordField
|
||||
id="postgresqlPassword"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="postgresqlPassword"
|
||||
value={service.fider.postgresqlPassword}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlDatabase">{$t('index.database')}</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlDatabase"
|
||||
id="postgresqlDatabase"
|
||||
value={service.fider.postgresqlDatabase}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
90
apps/ui/src/routes/services/[id]/_Services/_Ghost.svelte
Normal file
90
apps/ui/src/routes/services/[id]/_Services/_Ghost.svelte
Normal file
@@ -0,0 +1,90 @@
|
||||
<script lang="ts">
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
export let readOnly: any;
|
||||
export let service: any;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Ghost</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="email">{$t('forms.default_email_address')}</label>
|
||||
<input
|
||||
name="email"
|
||||
id="email"
|
||||
disabled
|
||||
readonly
|
||||
placeholder={$t('forms.email')}
|
||||
value={service.ghost.defaultEmail}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="defaultPassword">{$t('forms.default_password')}</label>
|
||||
<CopyPasswordField
|
||||
id="defaultPassword"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="defaultPassword"
|
||||
value={service.ghost.defaultPassword}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">MariaDB</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="mariadbUser">{$t('forms.username')}</label>
|
||||
<CopyPasswordField
|
||||
name="mariadbUser"
|
||||
id="mariadbUser"
|
||||
value={service.ghost.mariadbUser}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="mariadbPassword">{$t('forms.password')}</label>
|
||||
<CopyPasswordField
|
||||
id="mariadbPassword"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="mariadbPassword"
|
||||
value={service.ghost.mariadbPassword}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="mariadbDatabase">{$t('index.database')}</label>
|
||||
<input
|
||||
name="mariadbDatabase"
|
||||
id="mariadbDatabase"
|
||||
required
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
bind:value={service.ghost.mariadbDatabase}
|
||||
placeholder="{$t('forms.eg')}: ghost_db"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="mariadbRootUser">{$t('forms.root_db_user')}</label>
|
||||
<CopyPasswordField
|
||||
id="mariadbRootUser"
|
||||
readonly
|
||||
disabled
|
||||
name="mariadbRootUser"
|
||||
value={service.ghost.mariadbRootUser}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="mariadbRootUserPassword">{$t('forms.root_db_password')}</label>
|
||||
<CopyPasswordField
|
||||
id="mariadbRootUserPassword"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="mariadbRootUserPassword"
|
||||
value={service.ghost.mariadbRootUserPassword}
|
||||
/>
|
||||
</div>
|
||||
58
apps/ui/src/routes/services/[id]/_Services/_Hasura.svelte
Normal file
58
apps/ui/src/routes/services/[id]/_Services/_Hasura.svelte
Normal file
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
export let service: any;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Hasura</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="graphQLAdminPassword">GraphQL Admin Password</label>
|
||||
<CopyPasswordField
|
||||
name="graphQLAdminPassword"
|
||||
id="graphQLAdminPassword"
|
||||
isPasswordField
|
||||
value={service.hasura.graphQLAdminPassword}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">PostgreSQL</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlUser">{$t('forms.username')}</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlUser"
|
||||
id="postgresqlUser"
|
||||
value={service.hasura.postgresqlUser}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlPassword">{$t('forms.password')}</label>
|
||||
<CopyPasswordField
|
||||
id="postgresqlPassword"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="postgresqlPassword"
|
||||
value={service.hasura.postgresqlPassword}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlDatabase">{$t('index.database')}</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlDatabase"
|
||||
id="postgresqlDatabase"
|
||||
value={service.hasura.postgresqlDatabase}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
export let service: any;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">MeiliSearch</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="masterKey">{$t('forms.admin_api_key')}</label>
|
||||
<CopyPasswordField
|
||||
id="masterKey"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="masterKey"
|
||||
value={service.meiliSearch.masterKey}
|
||||
/>
|
||||
</div>
|
||||
45
apps/ui/src/routes/services/[id]/_Services/_MinIO.svelte
Normal file
45
apps/ui/src/routes/services/[id]/_Services/_MinIO.svelte
Normal file
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
|
||||
export let service: any;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">MinIO</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="rootUser">{$t('forms.root_user')}</label>
|
||||
<input
|
||||
name="rootUser"
|
||||
id="rootUser"
|
||||
placeholder={$t('forms.username')}
|
||||
value={service.minio.rootUser}
|
||||
disabled
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="rootUserPassword">{$t('forms.roots_password')}</label>
|
||||
<CopyPasswordField
|
||||
id="rootUserPassword"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="rootUserPassword"
|
||||
value={service.minio.rootUserPassword}
|
||||
/>
|
||||
</div>
|
||||
{#if !service.minio.apiFqdn}
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="publicPort">{$t('forms.api_port')}</label>
|
||||
<input
|
||||
name="publicPort"
|
||||
id="publicPort"
|
||||
value={service.minio.publicPort}
|
||||
disabled
|
||||
readonly
|
||||
placeholder={$t('forms.generated_automatically_after_start')}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,96 @@
|
||||
<script lang="ts">
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { appSession, status } from '$lib/store';
|
||||
import { t } from '$lib/translations';
|
||||
export let service: any;
|
||||
export let readOnly: any;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Plausible Analytics</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="scriptName">Script Name</label>
|
||||
<input
|
||||
name="scriptName"
|
||||
id="scriptName"
|
||||
readonly={!$appSession.isAdmin && !$status.service.isRunning}
|
||||
disabled={!$appSession.isAdmin || $status.service.isRunning}
|
||||
placeholder="plausible.js"
|
||||
bind:value={service.plausibleAnalytics.scriptName}
|
||||
required
|
||||
/>
|
||||
<Explainer
|
||||
text="Useful if you would like to rename the collector script to prevent it blocked by AdBlockers."
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="email">{$t('forms.email')}</label>
|
||||
<input
|
||||
name="email"
|
||||
id="email"
|
||||
disabled={readOnly}
|
||||
readonly={readOnly}
|
||||
placeholder={$t('forms.email')}
|
||||
bind:value={service.plausibleAnalytics.email}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="username">{$t('forms.username')}</label>
|
||||
<CopyPasswordField
|
||||
name="username"
|
||||
id="username"
|
||||
disabled={readOnly}
|
||||
readonly={readOnly}
|
||||
placeholder={$t('forms.username')}
|
||||
bind:value={service.plausibleAnalytics.username}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="password">{$t('forms.password')}</label>
|
||||
<CopyPasswordField
|
||||
id="password"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="password"
|
||||
value={service.plausibleAnalytics.password}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">PostgreSQL</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlUser">{$t('forms.username')}</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlUser"
|
||||
id="postgresqlUser"
|
||||
value={service.plausibleAnalytics.postgresqlUser}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlPassword">{$t('forms.password')}</label>
|
||||
<CopyPasswordField
|
||||
id="postgresqlPassword"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="postgresqlPassword"
|
||||
value={service.plausibleAnalytics.postgresqlPassword}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlDatabase">{$t('index.database')}</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlDatabase"
|
||||
id="postgresqlDatabase"
|
||||
value={service.plausibleAnalytics.postgresqlDatabase}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
277
apps/ui/src/routes/services/[id]/_Services/_Services.svelte
Normal file
277
apps/ui/src/routes/services/[id]/_Services/_Services.svelte
Normal file
@@ -0,0 +1,277 @@
|
||||
<script lang="ts">
|
||||
export let service: any;
|
||||
export let readOnly: any;
|
||||
export let settings: any;
|
||||
|
||||
import cuid from 'cuid';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import { browser } from '$app/env';
|
||||
import { page } from '$app/stores';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
|
||||
import { get, post } from '$lib/api';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { t } from '$lib/translations';
|
||||
import { appSession, disabledButton, status } from '$lib/store';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import Setting from '$lib/components/Setting.svelte'
|
||||
|
||||
import Fider from './_Fider.svelte';
|
||||
import Ghost from './_Ghost.svelte';
|
||||
import Hasura from './_Hasura.svelte';
|
||||
import MeiliSearch from './_MeiliSearch.svelte';
|
||||
import MinIo from './_MinIO.svelte';
|
||||
import PlausibleAnalytics from './_PlausibleAnalytics.svelte';
|
||||
import Umami from './_Umami.svelte';
|
||||
import VsCodeServer from './_VSCodeServer.svelte';
|
||||
import Wordpress from './_Wordpress.svelte';
|
||||
|
||||
const { id } = $page.params;
|
||||
|
||||
let loading = false;
|
||||
let loadingVerification = false;
|
||||
let dualCerts = service.dualCerts;
|
||||
|
||||
async function handleSubmit() {
|
||||
if (loading) return;
|
||||
loading = true;
|
||||
try {
|
||||
await post(`/services/${id}/check`, {
|
||||
fqdn: service.fqdn,
|
||||
otherFqdns: service.minio?.apiFqdn ? [service.minio?.apiFqdn] : [],
|
||||
exposePort: service.exposePort
|
||||
});
|
||||
await post(`/services/${id}`, { ...service });
|
||||
$disabledButton = false;
|
||||
toast.push('Settings saved.');
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
async function setEmailsToVerified() {
|
||||
loadingVerification = true;
|
||||
try {
|
||||
await post(`/services/${id}/${service.type}/activate`, { id: service.id });
|
||||
toast.push(t.get('services.all_email_verified'));
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loadingVerification = false;
|
||||
}
|
||||
}
|
||||
async function changeSettings(name: any) {
|
||||
try {
|
||||
if (name === 'dualCerts') {
|
||||
dualCerts = !dualCerts;
|
||||
}
|
||||
await post(`/services/${id}/settings`, { dualCerts });
|
||||
return toast.push(t.get('application.settings_saved'));
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
onMount(async () => {
|
||||
if (browser && window.location.hostname === 'demo.coolify.io' && !service.fqdn) {
|
||||
service.fqdn = `http://${cuid()}.demo.coolify.io`;
|
||||
if (service.type === 'wordpress') {
|
||||
service.wordpress.mysqlDatabase = 'db';
|
||||
}
|
||||
if (service.type === 'plausibleanalytics') {
|
||||
service.plausibleAnalytics.email = 'noreply@demo.com';
|
||||
service.plausibleAnalytics.username = 'admin';
|
||||
}
|
||||
if (service.type === 'minio') {
|
||||
service.minio.apiFqdn = `http://${cuid()}.demo.coolify.io`;
|
||||
}
|
||||
if (service.type === 'ghost') {
|
||||
service.ghost.mariadbDatabase = 'db';
|
||||
}
|
||||
if (service.type === 'fider') {
|
||||
service.fider.emailNoreply = 'noreply@demo.com';
|
||||
}
|
||||
await handleSubmit();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<div class="mx-auto max-w-4xl px-6 pb-12">
|
||||
<form on:submit|preventDefault={handleSubmit} class="py-4">
|
||||
<div class="flex space-x-1 pb-5 font-bold">
|
||||
<div class="title">{$t('general')}</div>
|
||||
{#if $appSession.isAdmin}
|
||||
<button
|
||||
type="submit"
|
||||
class:bg-pink-600={!loading}
|
||||
class:hover:bg-pink-500={!loading}
|
||||
disabled={loading}>{loading ? $t('forms.saving') : $t('forms.save')}</button
|
||||
>
|
||||
{/if}
|
||||
{#if service.type === 'plausibleanalytics' && $status.service.isRunning}
|
||||
<button on:click|preventDefault={setEmailsToVerified} disabled={loadingVerification}
|
||||
>{loadingVerification
|
||||
? $t('forms.verifying')
|
||||
: $t('forms.verify_emails_without_smtp')}</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-flow-row gap-2">
|
||||
{#if service.type === 'minio' && !service.minio.apiFqdn && $status.service.isRunning}
|
||||
<div class="text-center">
|
||||
<span class="font-bold text-red-500">IMPORTANT!</span> There was a small modification with
|
||||
Minio in the latest version of Coolify. Now you can separate the Console URL from the API URL,
|
||||
so you could use both through SSL. But this proccess cannot be done automatically, so you have
|
||||
to stop your Minio instance, configure the new domain and start it back. Sorry for any inconvenience.
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mt-2 grid grid-cols-2 items-center px-10">
|
||||
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
|
||||
<div>
|
||||
<input
|
||||
readonly={!$appSession.isAdmin}
|
||||
name="name"
|
||||
id="name"
|
||||
bind:value={service.name}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="version" class="text-base font-bold text-stone-100">Version / Tag</label>
|
||||
<a
|
||||
href={$appSession.isAdmin && !$status.service.isRunning
|
||||
? `/services/${id}/configuration/version?from=/services/${id}`
|
||||
: ''}
|
||||
class="no-underline"
|
||||
>
|
||||
<input
|
||||
value={service.version}
|
||||
id="service"
|
||||
disabled={$status.service.isRunning}
|
||||
class:cursor-pointer={!$status.service.isRunning}
|
||||
/></a
|
||||
>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="destination" class="text-base font-bold text-stone-100"
|
||||
>{$t('application.destination')}</label
|
||||
>
|
||||
<div>
|
||||
{#if service.destinationDockerId}
|
||||
<div class="no-underline">
|
||||
<input
|
||||
value={service.destinationDocker.name}
|
||||
id="destination"
|
||||
disabled
|
||||
class="bg-transparent "
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if service.type === 'minio'}
|
||||
<div class="grid grid-cols-2 px-10">
|
||||
<div class="flex-col ">
|
||||
<label for="fqdn" class="pt-2 text-base font-bold text-stone-100">Console URL</label>
|
||||
</div>
|
||||
|
||||
<CopyPasswordField
|
||||
placeholder="eg: https://console.min.io"
|
||||
readonly={!$appSession.isAdmin && !$status.service.isRunning}
|
||||
disabled={!$appSession.isAdmin || $status.service.isRunning}
|
||||
name="fqdn"
|
||||
id="fqdn"
|
||||
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
|
||||
bind:value={service.fqdn}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 px-10">
|
||||
<div class="flex-col ">
|
||||
<label for="apiFqdn" class="pt-2 text-base font-bold text-stone-100">API URL</label>
|
||||
<Explainer text={$t('application.https_explainer')} />
|
||||
</div>
|
||||
|
||||
<CopyPasswordField
|
||||
placeholder="eg: https://min.io"
|
||||
readonly={!$appSession.isAdmin && !$status.service.isRunning}
|
||||
disabled={!$appSession.isAdmin || $status.service.isRunning}
|
||||
name="apiFqdn"
|
||||
id="apiFqdn"
|
||||
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
|
||||
bind:value={service.minio.apiFqdn}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid grid-cols-2 px-10">
|
||||
<div class="flex-col ">
|
||||
<label for="fqdn" class="pt-2 text-base font-bold text-stone-100"
|
||||
>{$t('application.url_fqdn')}</label
|
||||
>
|
||||
<Explainer text={$t('application.https_explainer')} />
|
||||
</div>
|
||||
|
||||
<CopyPasswordField
|
||||
placeholder="eg: https://analytics.coollabs.io"
|
||||
readonly={!$appSession.isAdmin && !$status.service.isRunning}
|
||||
disabled={!$appSession.isAdmin || $status.service.isRunning}
|
||||
name="fqdn"
|
||||
id="fqdn"
|
||||
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
|
||||
bind:value={service.fqdn}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<Setting
|
||||
disabled={$status.service.isRunning}
|
||||
dataTooltip={$t('forms.must_be_stopped_to_modify')}
|
||||
bind:setting={dualCerts}
|
||||
title={$t('application.ssl_www_and_non_www')}
|
||||
description={$t('services.generate_www_non_www_ssl')}
|
||||
on:click={() => !$status.service.isRunning && changeSettings('dualCerts')}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="exposePort" class="text-base font-bold text-stone-100">Exposed Port</label>
|
||||
<input
|
||||
readonly={!$appSession.isAdmin && !$status.service.isRunning}
|
||||
disabled={!$appSession.isAdmin || $status.service.isRunning}
|
||||
name="exposePort"
|
||||
id="exposePort"
|
||||
bind:value={service.exposePort}
|
||||
placeholder="12345"
|
||||
/>
|
||||
<Explainer
|
||||
text={'You can expose your application to a port on the host system.<br><br>Useful if you would like to use your own reverse proxy or tunnel and also in development mode. Otherwise leave empty.'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if service.type === 'plausibleanalytics'}
|
||||
<PlausibleAnalytics bind:service {readOnly} />
|
||||
{:else if service.type === 'minio'}
|
||||
<MinIo {service} />
|
||||
{:else if service.type === 'vscodeserver'}
|
||||
<VsCodeServer {service} />
|
||||
{:else if service.type === 'wordpress'}
|
||||
<Wordpress bind:service {readOnly} {settings} />
|
||||
{:else if service.type === 'ghost'}
|
||||
<Ghost bind:service {readOnly} />
|
||||
{:else if service.type === 'meilisearch'}
|
||||
<MeiliSearch bind:service />
|
||||
{:else if service.type === 'umami'}
|
||||
<Umami bind:service />
|
||||
{:else if service.type === 'hasura'}
|
||||
<Hasura bind:service />
|
||||
{:else if service.type === 'fider'}
|
||||
<Fider bind:service {readOnly} />
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
29
apps/ui/src/routes/services/[id]/_Services/_Umami.svelte
Normal file
29
apps/ui/src/routes/services/[id]/_Services/_Umami.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
|
||||
export let service: any;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Umami</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="adminUser">Admin User</label>
|
||||
<input name="adminUser" id="adminUser" placeholder="admin" value="admin" disabled readonly />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="umamiAdminPassword">Initial Admin Password</label>
|
||||
<CopyPasswordField
|
||||
isPasswordField
|
||||
name="umamiAdminPassword"
|
||||
id="umamiAdminPassword"
|
||||
placeholder="admin"
|
||||
value={service.umami.umamiAdminPassword}
|
||||
disabled
|
||||
readonly
|
||||
/>
|
||||
<Explainer
|
||||
text="It could be changed in Umami. <br>This is just the password set initially after the first start."
|
||||
/>
|
||||
</div>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
|
||||
export let service: any;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">VSCode Server</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="password">{$t('forms.password')}</label>
|
||||
<CopyPasswordField
|
||||
id="password"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="password"
|
||||
value={service.vscodeserver.password}
|
||||
/>
|
||||
</div>
|
||||
213
apps/ui/src/routes/services/[id]/_Services/_Wordpress.svelte
Normal file
213
apps/ui/src/routes/services/[id]/_Services/_Wordpress.svelte
Normal file
@@ -0,0 +1,213 @@
|
||||
<script lang="ts">
|
||||
import { post } from '$lib/api';
|
||||
import { page } from '$app/stores';
|
||||
import { status } from '$lib/store';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Setting from '$lib/components/Setting.svelte';
|
||||
import { browser } from '$app/env';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification, getDomain } from '$lib/common';
|
||||
|
||||
export let service: any;
|
||||
export let readOnly: any;
|
||||
export let settings: any;
|
||||
const { id } = $page.params;
|
||||
|
||||
let ftpUrl = generateUrl(service.wordpress.ftpPublicPort);
|
||||
let ftpUser = service.wordpress.ftpUser;
|
||||
let ftpPassword = service.wordpress.ftpPassword;
|
||||
let ftpLoading = false;
|
||||
let ownMysql = service.wordpress.ownMysql;
|
||||
|
||||
function generateUrl(publicPort: any) {
|
||||
return browser
|
||||
? `sftp://${
|
||||
settings.fqdn ? getDomain(settings.fqdn) : window.location.hostname
|
||||
}:${publicPort}`
|
||||
: 'Loading...';
|
||||
}
|
||||
async function changeSettings(name: any) {
|
||||
if (ftpLoading) return;
|
||||
if ($status.service.isRunning) {
|
||||
ftpLoading = true;
|
||||
let ftpEnabled = service.wordpress.ftpEnabled;
|
||||
|
||||
if (name === 'ftpEnabled') {
|
||||
ftpEnabled = !ftpEnabled;
|
||||
}
|
||||
try {
|
||||
const {
|
||||
publicPort,
|
||||
ftpUser: user,
|
||||
ftpPassword: password
|
||||
} = await post(`/services/${id}/wordpress/ftp`, {
|
||||
ftpEnabled
|
||||
});
|
||||
ftpUrl = generateUrl(publicPort);
|
||||
ftpUser = user;
|
||||
ftpPassword = password;
|
||||
service.wordpress.ftpEnabled = ftpEnabled;
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
ftpLoading = false;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
if (name === 'ownMysql') {
|
||||
ownMysql = !ownMysql;
|
||||
}
|
||||
await post(`/services/${id}/wordpress/settings`, {
|
||||
ownMysql
|
||||
});
|
||||
service.wordpress.ownMysql = ownMysql;
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Wordpress</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="extraConfig">{$t('forms.extra_config')}</label>
|
||||
<textarea
|
||||
bind:value={service.wordpress.extraConfig}
|
||||
disabled={$status.service.isRunning}
|
||||
readonly={$status.service.isRunning}
|
||||
class:resize-none={$status.service.isRunning}
|
||||
rows="5"
|
||||
name="extraConfig"
|
||||
id="extraConfig"
|
||||
placeholder={!$status.service.isRunning
|
||||
? `${$t('forms.eg')}:
|
||||
|
||||
define('WP_ALLOW_MULTISITE', true);
|
||||
define('MULTISITE', true);
|
||||
define('SUBDOMAIN_INSTALL', false);`
|
||||
: 'N/A'}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<Setting
|
||||
bind:setting={service.wordpress.ftpEnabled}
|
||||
loading={ftpLoading}
|
||||
disabled={!$status.service.isRunning}
|
||||
on:click={() => changeSettings('ftpEnabled')}
|
||||
title="Enable sFTP connection to WordPress data"
|
||||
description="Enables an on-demand sFTP connection to the WordPress data directory. This is useful if you want to use sFTP to upload files."
|
||||
/>
|
||||
</div>
|
||||
{#if service.wordpress.ftpEnabled}
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="ftpUrl">sFTP Connection URI</label>
|
||||
<CopyPasswordField id="ftpUrl" readonly disabled name="ftpUrl" value={ftpUrl} />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="ftpUser">User</label>
|
||||
<CopyPasswordField id="ftpUser" readonly disabled name="ftpUser" value={ftpUser} />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="ftpPassword">Password</label>
|
||||
<CopyPasswordField id="ftpPassword" readonly disabled name="ftpPassword" value={ftpPassword} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">MySQL</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<Setting
|
||||
dataTooltip={$t('forms.must_be_stopped_to_modify')}
|
||||
bind:setting={service.wordpress.ownMysql}
|
||||
disabled={$status.service.isRunning}
|
||||
on:click={() => !$status.service.isRunning && changeSettings('ownMysql')}
|
||||
title="Use your own MySQL server"
|
||||
description="Enables the use of your own MySQL server. If you don't have one, you can use the one provided by Coolify."
|
||||
/>
|
||||
</div>
|
||||
{#if service.wordpress.ownMysql}
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="mysqlHost">Host</label>
|
||||
<input
|
||||
name="mysqlHost"
|
||||
id="mysqlHost"
|
||||
required
|
||||
readonly={$status.service.isRunning}
|
||||
disabled={$status.service.isRunning}
|
||||
bind:value={service.wordpress.mysqlHost}
|
||||
placeholder="{$t('forms.eg')}: db.coolify.io"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="mysqlPort">Port</label>
|
||||
<input
|
||||
name="mysqlPort"
|
||||
id="mysqlPort"
|
||||
required
|
||||
readonly={$status.service.isRunning}
|
||||
disabled={$status.service.isRunning}
|
||||
bind:value={service.wordpress.mysqlPort}
|
||||
placeholder="{$t('forms.eg')}: 3306"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="mysqlDatabase">{$t('index.database')}</label>
|
||||
<input
|
||||
name="mysqlDatabase"
|
||||
id="mysqlDatabase"
|
||||
required
|
||||
readonly={readOnly && !service.wordpress.ownMysql}
|
||||
disabled={readOnly && !service.wordpress.ownMysql}
|
||||
bind:value={service.wordpress.mysqlDatabase}
|
||||
placeholder="{$t('forms.eg')}: wordpress_db"
|
||||
/>
|
||||
</div>
|
||||
{#if !service.wordpress.ownMysql}
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="mysqlRootUser">{$t('forms.root_user')}</label>
|
||||
<input
|
||||
name="mysqlRootUser"
|
||||
id="mysqlRootUser"
|
||||
placeholder="MySQL {$t('forms.root_user')}"
|
||||
value={service.wordpress.mysqlRootUser}
|
||||
readonly={$status.service.isRunning || !service.wordpress.ownMysql}
|
||||
disabled={$status.service.isRunning || !service.wordpress.ownMysql}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="mysqlRootUserPassword">{$t('forms.roots_password')}</label>
|
||||
<CopyPasswordField
|
||||
id="mysqlRootUserPassword"
|
||||
isPasswordField
|
||||
readonly={$status.service.isRunning || !service.wordpress.ownMysql}
|
||||
disabled={$status.service.isRunning || !service.wordpress.ownMysql}
|
||||
name="mysqlRootUserPassword"
|
||||
value={service.wordpress.mysqlRootUserPassword}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="mysqlUser">{$t('forms.user')}</label>
|
||||
<input
|
||||
name="mysqlUser"
|
||||
id="mysqlUser"
|
||||
bind:value={service.wordpress.mysqlUser}
|
||||
readonly={$status.service.isRunning || !service.wordpress.ownMysql}
|
||||
disabled={$status.service.isRunning || !service.wordpress.ownMysql}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="mysqlPassword">{$t('forms.password')}</label>
|
||||
<CopyPasswordField
|
||||
id="mysqlPassword"
|
||||
isPasswordField
|
||||
readonly={$status.service.isRunning || !service.wordpress.ownMysql}
|
||||
disabled={$status.service.isRunning || !service.wordpress.ownMysql}
|
||||
name="mysqlPassword"
|
||||
bind:value={service.wordpress.mysqlPassword}
|
||||
/>
|
||||
</div>
|
||||
82
apps/ui/src/routes/services/[id]/_Storage.svelte
Normal file
82
apps/ui/src/routes/services/[id]/_Storage.svelte
Normal file
@@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
export let isNew = false;
|
||||
export let storage: any = {
|
||||
id: null,
|
||||
path: null
|
||||
};
|
||||
import { del, post } from '$lib/api';
|
||||
import { page } from '$app/stores';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { errorNotification } from '$lib/common';
|
||||
const { id } = $page.params;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
async function saveStorage(newStorage = false) {
|
||||
try {
|
||||
if (!storage.path) return errorNotification('Path is required.');
|
||||
storage.path = storage.path.startsWith('/') ? storage.path : `/${storage.path}`;
|
||||
storage.path = storage.path.endsWith('/') ? storage.path.slice(0, -1) : storage.path;
|
||||
storage.path.replace(/\/\//g, '/');
|
||||
await post(`/services/${id}/storages`, {
|
||||
path: storage.path,
|
||||
storageId: storage.id,
|
||||
newStorage
|
||||
});
|
||||
dispatch('refresh');
|
||||
if (isNew) {
|
||||
storage.path = null;
|
||||
storage.id = null;
|
||||
}
|
||||
if (newStorage) toast.push('Storage saved.');
|
||||
else toast.push('Storage updated.');
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function removeStorage() {
|
||||
try {
|
||||
await del(`/services/${id}/storages`, { path: storage.path });
|
||||
dispatch('refresh');
|
||||
toast.push('Storage deleted.');
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function handleSubmit() {
|
||||
if (isNew) {
|
||||
await saveStorage(true)
|
||||
} else {
|
||||
await saveStorage(false)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<td>
|
||||
<form on:submit|preventDefault={handleSubmit}>
|
||||
<input
|
||||
bind:value={storage.path}
|
||||
required
|
||||
placeholder="eg: /data"
|
||||
class=" border border-dashed border-coolgray-300"
|
||||
/>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
{#if isNew}
|
||||
<div class="flex items-center justify-center">
|
||||
<button class="bg-green-600 hover:bg-green-500" on:click={() => saveStorage(true)}>Add</button
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row justify-center space-x-2">
|
||||
<div class="flex items-center justify-center">
|
||||
<button class="" on:click={() => saveStorage(false)}>Set</button>
|
||||
</div>
|
||||
<div class="flex justify-center items-end">
|
||||
<button class="bg-red-600 hover:bg-red-500" on:click={removeStorage}>Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
366
apps/ui/src/routes/services/[id]/__layout.svelte
Normal file
366
apps/ui/src/routes/services/[id]/__layout.svelte
Normal file
@@ -0,0 +1,366 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
function checkConfiguration(service: any) {
|
||||
let configurationPhase = null;
|
||||
if (!service.type) {
|
||||
configurationPhase = 'type';
|
||||
} else if (!service.version) {
|
||||
configurationPhase = 'version';
|
||||
} else if (!service.destinationDockerId) {
|
||||
configurationPhase = 'destination';
|
||||
}
|
||||
return configurationPhase;
|
||||
}
|
||||
export const load: Load = async ({ params, url }) => {
|
||||
try {
|
||||
let readOnly = false;
|
||||
const response = await get(`/services/${params.id}`);
|
||||
const { service, settings } = await response;
|
||||
if (!service || Object.entries(service).length === 0) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/databases'
|
||||
};
|
||||
}
|
||||
const configurationPhase = checkConfiguration(service);
|
||||
if (
|
||||
configurationPhase &&
|
||||
url.pathname !== `/services/${params.id}/configuration/${configurationPhase}`
|
||||
) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/services/${params.id}/configuration/${configurationPhase}`
|
||||
};
|
||||
}
|
||||
if (service.plausibleAnalytics?.email && service.plausibleAnalytics.username) readOnly = true;
|
||||
if (service.wordpress?.mysqlDatabase) readOnly = true;
|
||||
if (service.ghost?.mariadbDatabase && service.ghost.mariadbDatabase) readOnly = true;
|
||||
|
||||
return {
|
||||
props: {
|
||||
service
|
||||
},
|
||||
stuff: {
|
||||
service,
|
||||
readOnly,
|
||||
settings
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return handlerNotFoundLoad(error, url);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
|
||||
import Loading from '$lib/components/Loading.svelte';
|
||||
import { del, get, post } from '$lib/api';
|
||||
import { goto } from '$app/navigation';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification, handlerNotFoundLoad } from '$lib/common';
|
||||
import { appSession, disabledButton, status } from '$lib/store';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
const { id } = $page.params;
|
||||
|
||||
export let service: any;
|
||||
|
||||
$disabledButton =
|
||||
!$appSession.isAdmin ||
|
||||
!service.fqdn ||
|
||||
!service.destinationDocker ||
|
||||
!service.version ||
|
||||
!service.type;
|
||||
|
||||
let loading = false;
|
||||
let statusInterval: any;
|
||||
|
||||
async function deleteService() {
|
||||
const sure = confirm($t('application.confirm_to_delete', { name: service.name }));
|
||||
if (sure) {
|
||||
loading = true;
|
||||
try {
|
||||
if (service.type && $status.service.isRunning)
|
||||
await post(`/services/${service.id}/${service.type}/stop`, {});
|
||||
await del(`/services/${service.id}`, { id: service.id });
|
||||
return await goto(`/services`);
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
async function stopService() {
|
||||
const sure = confirm($t('database.confirm_stop', { name: service.name }));
|
||||
if (sure) {
|
||||
loading = true;
|
||||
try {
|
||||
await post(`/services/${service.id}/${service.type}/stop`, {});
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
async function startService() {
|
||||
loading = true;
|
||||
try {
|
||||
await post(`/services/${service.id}/${service.type}/start`, {});
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
async function getStatus() {
|
||||
if ($status.service.loading) return;
|
||||
$status.service.loading = true;
|
||||
const data = await get(`/services/${id}`);
|
||||
$status.service.isRunning = data.isRunning;
|
||||
$status.service.initialLoading = false;
|
||||
$status.service.loading = false;
|
||||
}
|
||||
onDestroy(() => {
|
||||
$status.service.initialLoading = true;
|
||||
clearInterval(statusInterval);
|
||||
});
|
||||
onMount(async () => {
|
||||
$status.service.isRunning = false;
|
||||
$status.service.loading = false;
|
||||
if (service.type && service.destinationDockerId && service.version && service.fqdn) {
|
||||
await getStatus();
|
||||
statusInterval = setInterval(async () => {
|
||||
await getStatus();
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<nav class="nav-side">
|
||||
{#if loading}
|
||||
<Loading fullscreen cover />
|
||||
{:else}
|
||||
{#if service.type && service.destinationDockerId && service.version}
|
||||
{#if $status.service.initialLoading}
|
||||
<button
|
||||
class="icons tooltip-bottom flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out"
|
||||
>
|
||||
<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="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
|
||||
<line x1="5.63" y1="7.16" x2="5.63" y2="7.17" />
|
||||
<line x1="4.06" y1="11" x2="4.06" y2="11.01" />
|
||||
<line x1="4.63" y1="15.1" x2="4.63" y2="15.11" />
|
||||
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
|
||||
<line x1="11" y1="19.94" x2="11" y2="19.95" />
|
||||
</svg>
|
||||
</button>
|
||||
{:else if $status.service.isRunning}
|
||||
<button
|
||||
on:click={stopService}
|
||||
title={$t('service.stop_service')}
|
||||
type="submit"
|
||||
disabled={$disabledButton}
|
||||
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-red-500"
|
||||
data-tooltip={$appSession.isAdmin
|
||||
? $t('service.stop_service')
|
||||
: $t('service.permission_denied_stop_service')}
|
||||
>
|
||||
<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>
|
||||
{:else}
|
||||
<button
|
||||
on:click={startService}
|
||||
title={$t('service.start_service')}
|
||||
type="submit"
|
||||
disabled={$disabledButton}
|
||||
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-green-500"
|
||||
data-tooltip={$appSession.isAdmin
|
||||
? $t('service.start_service')
|
||||
: $t('service.permission_denied_start_service')}
|
||||
><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>
|
||||
{/if}
|
||||
<div class="border border-stone-700 h-8" />
|
||||
{/if}
|
||||
{#if service.type && service.destinationDockerId && service.version}
|
||||
<a
|
||||
href="/services/{id}"
|
||||
sveltekit:prefetch
|
||||
class="hover:text-yellow-500 rounded"
|
||||
class:text-yellow-500={$page.url.pathname === `/services/${id}`}
|
||||
class:bg-coolgray-500={$page.url.pathname === `/services/${id}`}
|
||||
>
|
||||
<button
|
||||
title={$t('application.configurations')}
|
||||
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
|
||||
data-tooltip={$t('application.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="/services/{id}/secrets"
|
||||
sveltekit:prefetch
|
||||
class="hover:text-pink-500 rounded"
|
||||
class:text-pink-500={$page.url.pathname === `/services/${id}/secrets`}
|
||||
class:bg-coolgray-500={$page.url.pathname === `/services/${id}/secrets`}
|
||||
>
|
||||
<button
|
||||
title={$t('application.secret')}
|
||||
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
|
||||
data-tooltip={$t('application.secret')}
|
||||
>
|
||||
<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="/services/{id}/storages"
|
||||
sveltekit:prefetch
|
||||
class="hover:text-pink-500 rounded"
|
||||
class:text-pink-500={$page.url.pathname === `/services/${id}/storages`}
|
||||
class:bg-coolgray-500={$page.url.pathname === `/services/${id}/storages`}
|
||||
>
|
||||
<button
|
||||
title="Persistent Storage"
|
||||
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
|
||||
data-tooltip="Persistent Storage"
|
||||
>
|
||||
<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" />
|
||||
<ellipse cx="12" cy="6" rx="8" ry="3" />
|
||||
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
|
||||
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
|
||||
</svg>
|
||||
</button></a
|
||||
>
|
||||
<div class="border border-stone-700 h-8" />
|
||||
<a
|
||||
href={$status.service.isRunning ? `/services/${id}/logs` : null}
|
||||
sveltekit:prefetch
|
||||
class="hover:text-pink-500 rounded"
|
||||
class:text-pink-500={$page.url.pathname === `/services/${id}/logs`}
|
||||
class:bg-coolgray-500={$page.url.pathname === `/services/${id}/logs`}
|
||||
>
|
||||
<button
|
||||
title={$t('service.logs')}
|
||||
disabled={!$status.service.isRunning}
|
||||
class="icons bg-transparent tooltip-bottom text-sm"
|
||||
data-tooltip={$t('service.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
|
||||
>
|
||||
{/if}
|
||||
<button
|
||||
on:click={deleteService}
|
||||
title={$t('service.delete_service')}
|
||||
type="submit"
|
||||
disabled={!$appSession.isAdmin}
|
||||
class:hover:text-red-500={$appSession.isAdmin}
|
||||
class="icons bg-transparent tooltip-bottom text-sm"
|
||||
data-tooltip={$appSession.isAdmin
|
||||
? $t('service.delete_service')
|
||||
: $t('service.permission_denied_delete_service')}><DeleteIcon /></button
|
||||
>
|
||||
{/if}
|
||||
</nav>
|
||||
<slot />
|
||||
@@ -0,0 +1,90 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
try {
|
||||
const { service } = stuff;
|
||||
if (service?.destinationDockerId && !url.searchParams.get('from')) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/services/${params.id}`
|
||||
};
|
||||
}
|
||||
const response = await get(`/destinations`);
|
||||
return {
|
||||
props: {
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let destinations: any;
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { get, post } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
|
||||
async function handleSubmit(destinationId: any) {
|
||||
try {
|
||||
await post(`/services/${id}/configuration/destination`, { destinationId });
|
||||
return await goto(from || `/services/${id}`);
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">
|
||||
{$t('application.configuration.configure_destination')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
{#if !destinations || destinations.length === 0}
|
||||
<div class="flex-col">
|
||||
<div class="pb-2">{$t('application.configuration.no_configurable_destination')}</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>
|
||||
106
apps/ui/src/routes/services/[id]/configuration/type.svelte
Normal file
106
apps/ui/src/routes/services/[id]/configuration/type.svelte
Normal file
@@ -0,0 +1,106 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
try {
|
||||
const { service } = stuff;
|
||||
if (service?.type && !url.searchParams.get('from')) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/services/${params.id}`
|
||||
};
|
||||
}
|
||||
const response = await get(`/services/${params.id}/configuration/type`);
|
||||
return {
|
||||
props: {
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let types: any;
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { get, post } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
|
||||
import PlausibleAnalytics from '$lib/components/svg/services/PlausibleAnalytics.svelte';
|
||||
import NocoDb from '$lib/components/svg/services/NocoDB.svelte';
|
||||
import MinIo from '$lib/components/svg/services/MinIO.svelte';
|
||||
import VsCodeServer from '$lib/components/svg/services/VSCodeServer.svelte';
|
||||
import Wordpress from '$lib/components/svg/services/Wordpress.svelte';
|
||||
import VaultWarden from '$lib/components/svg/services/VaultWarden.svelte';
|
||||
import LanguageTool from '$lib/components/svg/services/LanguageTool.svelte';
|
||||
import N8n from '$lib/components/svg/services/N8n.svelte';
|
||||
import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte';
|
||||
import Ghost from '$lib/components/svg/services/Ghost.svelte';
|
||||
import MeiliSearch from '$lib/components/svg/services/MeiliSearch.svelte';
|
||||
import Umami from '$lib/components/svg/services/Umami.svelte';
|
||||
import Hasura from '$lib/components/svg/services/Hasura.svelte';
|
||||
import Fider from '$lib/components/svg/services/Fider.svelte';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
|
||||
async function handleSubmit(type: any) {
|
||||
try {
|
||||
await post(`/services/${id}/configuration/type`, { type });
|
||||
return await goto(from || `/services/${id}`);
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">{$t('forms.select_a_service')}</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#each types as type}
|
||||
<div class="p-2">
|
||||
<form on:submit|preventDefault={() => handleSubmit(type.name)}>
|
||||
<button type="submit" class="box-selection relative text-xl font-bold hover:bg-pink-600">
|
||||
{#if type.name === 'plausibleanalytics'}
|
||||
<PlausibleAnalytics isAbsolute />
|
||||
{:else if type.name === 'nocodb'}
|
||||
<NocoDb isAbsolute />
|
||||
{:else if type.name === 'minio'}
|
||||
<MinIo isAbsolute />
|
||||
{:else if type.name === 'vscodeserver'}
|
||||
<VsCodeServer isAbsolute />
|
||||
{:else if type.name === 'wordpress'}
|
||||
<Wordpress isAbsolute />
|
||||
{:else if type.name === 'vaultwarden'}
|
||||
<VaultWarden isAbsolute />
|
||||
{:else if type.name === 'languagetool'}
|
||||
<LanguageTool isAbsolute />
|
||||
{:else if type.name === 'n8n'}
|
||||
<N8n isAbsolute />
|
||||
{:else if type.name === 'uptimekuma'}
|
||||
<UptimeKuma isAbsolute />
|
||||
{:else if type.name === 'ghost'}
|
||||
<Ghost isAbsolute />
|
||||
{:else if type.name === 'meilisearch'}
|
||||
<MeiliSearch isAbsolute />
|
||||
{:else if type.name === 'umami'}
|
||||
<Umami isAbsolute />
|
||||
{:else if type.name === 'hasura'}
|
||||
<Hasura isAbsolute />
|
||||
{:else if type.name === 'fider'}
|
||||
<Fider isAbsolute />
|
||||
{/if}{type.fancyName}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -0,0 +1,79 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
try {
|
||||
const { service } = stuff;
|
||||
if (service?.version && !url.searchParams.get('from')) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/services/${params.id}`
|
||||
};
|
||||
}
|
||||
const response = await get(`/services/${params.id}/configuration/version`);
|
||||
return {
|
||||
props: {
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let versions: any;
|
||||
export let type: any;
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { get, post } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification, supportedServiceTypesAndVersions } from '$lib/common';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
|
||||
let recommendedVersion = supportedServiceTypesAndVersions.find(
|
||||
({ name }) => name === type
|
||||
)?.recommendedVersion;
|
||||
async function handleSubmit(version: any) {
|
||||
try {
|
||||
await post(`/services/${id}/configuration/version`, { version });
|
||||
return await goto(from || `/services/${id}`);
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">{$t('forms.select_a_service_version')}</div>
|
||||
</div>
|
||||
{#if from}
|
||||
<div class="pb-10 text-center">
|
||||
Warning: you are about to change the version of this service.<br />This could cause problem
|
||||
after you restart the service,
|
||||
<span class="font-bold text-pink-600">like losing your data, incompatibility issues, etc</span
|
||||
>.<br />Only do if you know what you are doing.
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#each versions as version}
|
||||
<div class="p-2">
|
||||
<form on:submit|preventDefault={() => handleSubmit(version)}>
|
||||
<button
|
||||
type="submit"
|
||||
class:bg-pink-500={recommendedVersion === version}
|
||||
class="box-selection relative flex text-xl font-bold hover:bg-pink-600"
|
||||
>{version}
|
||||
{#if recommendedVersion === version}
|
||||
<span class="absolute bottom-0 pb-2 text-xs">recommended</span>
|
||||
{/if}</button
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
113
apps/ui/src/routes/services/[id]/index.svelte
Normal file
113
apps/ui/src/routes/services/[id]/index.svelte
Normal file
@@ -0,0 +1,113 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ stuff }) => {
|
||||
return {
|
||||
props: { ...stuff }
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import Services from './_Services/_Services.svelte';
|
||||
import { get } from '$lib/api';
|
||||
import { page } from '$app/stores';
|
||||
import { status } from '$lib/store';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import ServiceLinks from './_ServiceLinks.svelte';
|
||||
|
||||
export let service: any;
|
||||
export let readOnly: any;
|
||||
export let settings: any;
|
||||
|
||||
const { id } = $page.params;
|
||||
let loading = {
|
||||
usage: false
|
||||
};
|
||||
let usage = {
|
||||
MemUsage: 0,
|
||||
CPUPerc: 0,
|
||||
NetIO: 0
|
||||
};
|
||||
let usageInterval: any;
|
||||
|
||||
async function getUsage() {
|
||||
if (loading.usage) return;
|
||||
if (!$status.service.isRunning) return;
|
||||
loading.usage = true;
|
||||
const data = await get(`/services/${id}/usage`);
|
||||
usage = data.usage;
|
||||
loading.usage = false;
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
clearInterval(usageInterval);
|
||||
});
|
||||
onMount(async () => {
|
||||
await getUsage();
|
||||
usageInterval = setInterval(async () => {
|
||||
await getUsage();
|
||||
}, 1000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold">
|
||||
<div class="-mb-5 flex-col">
|
||||
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
|
||||
Configuration
|
||||
</div>
|
||||
<span class="text-xs">{service.name}</span>
|
||||
</div>
|
||||
|
||||
{#if service.fqdn}
|
||||
<a
|
||||
href={service.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}
|
||||
|
||||
<ServiceLinks {service} />
|
||||
</div>
|
||||
<div class="mx-auto max-w-4xl px-6 py-4">
|
||||
<div class="text-2xl font-bold">Service Usage</div>
|
||||
<div class="mx-auto">
|
||||
<dl class="relative mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3">
|
||||
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left">
|
||||
<dt class=" text-sm font-medium text-white">Used Memory / Memory Limit</dt>
|
||||
<dd class="mt-1 text-xl font-semibold text-white">
|
||||
{usage?.MemUsage}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left">
|
||||
<dt class="truncate text-sm font-medium text-white">Used CPU</dt>
|
||||
<dd class="mt-1 text-xl font-semibold text-white ">
|
||||
{usage?.CPUPerc}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left">
|
||||
<dt class="truncate text-sm font-medium text-white">Network IO</dt>
|
||||
<dd class="mt-1 text-xl font-semibold text-white ">
|
||||
{usage?.NetIO}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<Services bind:service bind:readOnly bind:settings />
|
||||
41
apps/ui/src/routes/services/[id]/logs/_Loading.svelte
Normal file
41
apps/ui/src/routes/services/[id]/logs/_Loading.svelte
Normal 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>
|
||||
178
apps/ui/src/routes/services/[id]/logs/index.svelte
Normal file
178
apps/ui/src/routes/services/[id]/logs/index.svelte
Normal file
@@ -0,0 +1,178 @@
|
||||
<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 }) => {
|
||||
try {
|
||||
const response = await get(`/services/${params.id}/logs`);
|
||||
return {
|
||||
props: {
|
||||
service: stuff.service,
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let service: any;
|
||||
import { page } from '$app/stores';
|
||||
import LoadingLogs from './_Loading.svelte';
|
||||
import { get } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
|
||||
let loadLogsInterval: any = null;
|
||||
let logs: any = [];
|
||||
let lastLog: any = null;
|
||||
let followingInterval: any;
|
||||
let followingLogs: any;
|
||||
let logsEl: any;
|
||||
let position = 0;
|
||||
|
||||
const { id } = $page.params;
|
||||
onMount(async () => {
|
||||
loadAllLogs();
|
||||
loadLogsInterval = setInterval(() => {
|
||||
loadLogs();
|
||||
}, 1000);
|
||||
});
|
||||
onDestroy(() => {
|
||||
clearInterval(loadLogsInterval);
|
||||
clearInterval(followingInterval);
|
||||
});
|
||||
async function loadAllLogs() {
|
||||
try {
|
||||
const data: any = await get(`/services/${id}/logs`);
|
||||
if (data?.logs) {
|
||||
lastLog = data.logs[data.logs.length - 1];
|
||||
logs = data.logs;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function loadLogs() {
|
||||
try {
|
||||
const newLogs: any = await get(
|
||||
`/services/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`
|
||||
);
|
||||
|
||||
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
|
||||
logs = logs.concat(newLogs.logs);
|
||||
lastLog = newLogs.logs[newLogs.logs.length - 1];
|
||||
}
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
function detect() {
|
||||
if (position < logsEl.scrollTop) {
|
||||
position = logsEl.scrollTop;
|
||||
} else {
|
||||
if (followingLogs) {
|
||||
clearInterval(followingInterval);
|
||||
followingLogs = false;
|
||||
}
|
||||
position = logsEl.scrollTop;
|
||||
}
|
||||
}
|
||||
|
||||
function followBuild() {
|
||||
followingLogs = !followingLogs;
|
||||
if (followingLogs) {
|
||||
followingInterval = setInterval(() => {
|
||||
logsEl.scrollTop = logsEl.scrollHeight;
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
}, 1000);
|
||||
} else {
|
||||
clearInterval(followingInterval);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold">
|
||||
<div class="-mb-5 flex-col">
|
||||
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
|
||||
Service Logs
|
||||
</div>
|
||||
<span class="text-xs">{service.name}</span>
|
||||
</div>
|
||||
|
||||
{#if service.fqdn}
|
||||
<a
|
||||
href={service.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}
|
||||
</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">{$t('application.build.waiting_logs')}</div>
|
||||
{:else}
|
||||
<div class="relative w-full">
|
||||
<div class="text-right " />
|
||||
{#if loadLogsInterval}
|
||||
<LoadingLogs />
|
||||
{/if}
|
||||
<div class="flex justify-end sticky top-0 p-2 mx-1">
|
||||
<button
|
||||
on:click={followBuild}
|
||||
class="bg-transparent"
|
||||
data-tooltip="Follow logs"
|
||||
class:text-green-500={followingLogs}
|
||||
>
|
||||
<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="12" cy="12" r="9" />
|
||||
<line x1="8" y1="12" x2="12" y2="16" />
|
||||
<line x1="12" y1="8" x2="12" y2="16" />
|
||||
<line x1="16" y1="12" x2="12" y2="16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="font-mono w-full leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
|
||||
bind:this={logsEl}
|
||||
on:scroll={detect}
|
||||
>
|
||||
<div class="px-2 pr-14">
|
||||
{#each logs as log}
|
||||
{log + '\n'}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
96
apps/ui/src/routes/services/[id]/secrets.svelte
Normal file
96
apps/ui/src/routes/services/[id]/secrets.svelte
Normal file
@@ -0,0 +1,96 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, stuff, url }) => {
|
||||
try {
|
||||
const response = await get(`/services/${params.id}/secrets`);
|
||||
return {
|
||||
props: {
|
||||
service: stuff.service,
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let secrets: any;
|
||||
export let service: any;
|
||||
import Secret from './_Secret.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { get } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import ServiceLinks from './_ServiceLinks.svelte';
|
||||
|
||||
const { id } = $page.params;
|
||||
|
||||
async function refreshSecrets() {
|
||||
const data = await get(`/services/${id}/secrets`);
|
||||
secrets = [...data.secrets];
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex items-center space-x-2 p-5 px-6 font-bold"
|
||||
class:p-5={service.fqdn}
|
||||
class:p-6={!service.fqdn}
|
||||
>
|
||||
<div class="-mb-5 flex-col">
|
||||
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
|
||||
{$t('application.secret')}
|
||||
</div>
|
||||
<span class="text-xs">{service.name}</span>
|
||||
</div>
|
||||
{#if service.fqdn}
|
||||
<a
|
||||
href={service.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}
|
||||
|
||||
<ServiceLinks {service} />
|
||||
</div>
|
||||
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">
|
||||
<table class="mx-auto border-separate text-left">
|
||||
<thead>
|
||||
<tr class="h-12">
|
||||
<th scope="col">{$t('forms.name')}</th>
|
||||
<th scope="col">{$t('forms.value')}</th>
|
||||
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each secrets as secret}
|
||||
{#key secret.id}
|
||||
<tr>
|
||||
<Secret name={secret.name} value={secret.value} on:refresh={refreshSecrets} />
|
||||
</tr>
|
||||
{/key}
|
||||
{/each}
|
||||
<tr>
|
||||
<Secret isNewSecret on:refresh={refreshSecrets} />
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
101
apps/ui/src/routes/services/[id]/storages.svelte
Normal file
101
apps/ui/src/routes/services/[id]/storages.svelte
Normal file
@@ -0,0 +1,101 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ params, stuff, url }) => {
|
||||
try {
|
||||
const response = await get(`/services/${params.id}/storages`);
|
||||
return {
|
||||
props: {
|
||||
service: stuff.service,
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let service: any;
|
||||
export let persistentStorages: any;
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import Storage from './_Storage.svelte';
|
||||
import { get } from '$lib/api';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import ServiceLinks from './_ServiceLinks.svelte';
|
||||
|
||||
const { id } = $page.params;
|
||||
async function refreshStorage() {
|
||||
const data = await get(`/services/${id}/storages`);
|
||||
persistentStorages = [...data.persistentStorages];
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex items-center space-x-2 p-5 px-6 font-bold"
|
||||
class:p-5={service.fqdn}
|
||||
class:p-6={!service.fqdn}
|
||||
>
|
||||
<div class="-mb-5 flex-col">
|
||||
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
|
||||
Persistent Storage
|
||||
</div>
|
||||
<span class="text-xs">{service.name}</span>
|
||||
</div>
|
||||
{#if service.fqdn}
|
||||
<a
|
||||
href={service.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}
|
||||
|
||||
<ServiceLinks {service} />
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">
|
||||
<div class="flex justify-center py-4 text-center">
|
||||
<Explainer
|
||||
customClass="w-full"
|
||||
text={'You can specify any folder that you want to be persistent across restarts. <br>This is useful for storing data for VSCode server or WordPress.'}
|
||||
/>
|
||||
</div>
|
||||
<table class="mx-auto border-separate text-left">
|
||||
<thead>
|
||||
<tr class="h-12">
|
||||
<th scope="col">Path</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each persistentStorages as storage}
|
||||
{#key storage.id}
|
||||
<tr>
|
||||
<Storage on:refresh={refreshStorage} {storage} />
|
||||
</tr>
|
||||
{/key}
|
||||
{/each}
|
||||
<tr>
|
||||
<Storage on:refresh={refreshStorage} isNew />
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
196
apps/ui/src/routes/services/index.svelte
Normal file
196
apps/ui/src/routes/services/index.svelte
Normal file
@@ -0,0 +1,196 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async () => {
|
||||
try {
|
||||
const response = await get(`/services`);
|
||||
return {
|
||||
props: {
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(error)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let services: any = [];
|
||||
import { post, get } from '$lib/api';
|
||||
import { goto } from '$app/navigation';
|
||||
import { t } from '$lib/translations';
|
||||
import { appSession } from '$lib/store';
|
||||
|
||||
import PlausibleAnalytics from '$lib/components/svg/services/PlausibleAnalytics.svelte';
|
||||
import NocoDb from '$lib/components/svg/services/NocoDB.svelte';
|
||||
import MinIo from '$lib/components/svg/services/MinIO.svelte';
|
||||
import VsCodeServer from '$lib/components/svg/services/VSCodeServer.svelte';
|
||||
import Wordpress from '$lib/components/svg/services/Wordpress.svelte';
|
||||
import VaultWarden from '$lib/components/svg/services/VaultWarden.svelte';
|
||||
import LanguageTool from '$lib/components/svg/services/LanguageTool.svelte';
|
||||
|
||||
import N8n from '$lib/components/svg/services/N8n.svelte';
|
||||
import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte';
|
||||
import Ghost from '$lib/components/svg/services/Ghost.svelte';
|
||||
import MeiliSearch from '$lib/components/svg/services/MeiliSearch.svelte';
|
||||
import Umami from '$lib/components/svg/services/Umami.svelte';
|
||||
import Hasura from '$lib/components/svg/services/Hasura.svelte';
|
||||
import Fider from '$lib/components/svg/services/Fider.svelte';
|
||||
import { getDomain } from '$lib/common';
|
||||
|
||||
async function newService() {
|
||||
const { id } = await post('/services/new', {});
|
||||
return await goto(`/services/${id}`, { replaceState: true });
|
||||
}
|
||||
const ownServices = services.filter((service: any) => {
|
||||
if (service.teams[0].id === $appSession.teamId) {
|
||||
return service;
|
||||
}
|
||||
});
|
||||
const otherServices = services.filter((service: any) => {
|
||||
if (service.teams[0].id !== $appSession.teamId) {
|
||||
return service;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">{$t('index.services')}</div>
|
||||
<div on:click={newService} class="add-icon cursor-pointer bg-pink-600 hover:bg-pink-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
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-col justify-center">
|
||||
{#if !services || ownServices.length === 0}
|
||||
<div class="flex-col">
|
||||
<div class="text-center text-xl font-bold">{$t('service.no_service')}</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if ownServices.length > 0 || otherServices.length > 0}
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each ownServices as service}
|
||||
<a href="/services/{service.id}" class="w-96 p-2 no-underline">
|
||||
<div class="box-selection group relative hover:bg-pink-600">
|
||||
{#if service.type === 'plausibleanalytics'}
|
||||
<PlausibleAnalytics isAbsolute />
|
||||
{:else if service.type === 'nocodb'}
|
||||
<NocoDb isAbsolute />
|
||||
{:else if service.type === 'minio'}
|
||||
<MinIo isAbsolute />
|
||||
{:else if service.type === 'vscodeserver'}
|
||||
<VsCodeServer isAbsolute />
|
||||
{:else if service.type === 'wordpress'}
|
||||
<Wordpress isAbsolute />
|
||||
{:else if service.type === 'vaultwarden'}
|
||||
<VaultWarden isAbsolute />
|
||||
{:else if service.type === 'languagetool'}
|
||||
<LanguageTool isAbsolute />
|
||||
{:else if service.type === 'n8n'}
|
||||
<N8n isAbsolute />
|
||||
{:else if service.type === 'uptimekuma'}
|
||||
<UptimeKuma isAbsolute />
|
||||
{:else if service.type === 'ghost'}
|
||||
<Ghost isAbsolute />
|
||||
{:else if service.type === 'meilisearch'}
|
||||
<MeiliSearch isAbsolute />
|
||||
{:else if service.type === 'umami'}
|
||||
<Umami isAbsolute />
|
||||
{:else if service.type === 'hasura'}
|
||||
<Hasura isAbsolute />
|
||||
{:else if service.type === 'fider'}
|
||||
<Fider isAbsolute />
|
||||
{/if}
|
||||
<div class="truncate text-center text-xl font-bold">
|
||||
{service.name}
|
||||
</div>
|
||||
{#if $appSession.teamId === '0' && otherServices.length > 0}
|
||||
<div class="truncate text-center">{service.teams[0].name}</div>
|
||||
{/if}
|
||||
{#if service.fqdn}
|
||||
<div class="truncate text-center">{getDomain(service.fqdn) || ''}</div>
|
||||
{/if}
|
||||
{#if !service.type || !service.fqdn}
|
||||
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
|
||||
{$t('application.configuration.configuration_missing')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{#if otherServices.length > 0 && $appSession.teamId === '0'}
|
||||
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Services</div>
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each otherServices as service}
|
||||
<a href="/services/{service.id}" class="w-96 p-2 no-underline">
|
||||
<div class="box-selection group relative hover:bg-pink-600">
|
||||
{#if service.type === 'plausibleanalytics'}
|
||||
<PlausibleAnalytics isAbsolute />
|
||||
{:else if service.type === 'nocodb'}
|
||||
<NocoDb isAbsolute />
|
||||
{:else if service.type === 'minio'}
|
||||
<MinIo isAbsolute />
|
||||
{:else if service.type === 'vscodeserver'}
|
||||
<VsCodeServer isAbsolute />
|
||||
{:else if service.type === 'wordpress'}
|
||||
<Wordpress isAbsolute />
|
||||
{:else if service.type === 'vaultwarden'}
|
||||
<VaultWarden isAbsolute />
|
||||
{:else if service.type === 'languagetool'}
|
||||
<LanguageTool isAbsolute />
|
||||
{:else if service.type === 'n8n'}
|
||||
<N8n isAbsolute />
|
||||
{:else if service.type === 'uptimekuma'}
|
||||
<UptimeKuma isAbsolute />
|
||||
{:else if service.type === 'ghost'}
|
||||
<Ghost isAbsolute />
|
||||
{:else if service.type === 'meilisearch'}
|
||||
<MeiliSearch isAbsolute />
|
||||
{:else if service.type === 'umami'}
|
||||
<Umami isAbsolute />
|
||||
{:else if service.type === 'hasura'}
|
||||
<Hasura isAbsolute />
|
||||
{:else if service.type === 'fider'}
|
||||
<Fider isAbsolute />
|
||||
{/if}
|
||||
<div class="truncate text-center text-xl font-bold">
|
||||
{service.name}
|
||||
</div>
|
||||
{#if $appSession.teamId === '0'}
|
||||
<div class="truncate text-center">{service.teams[0].name}</div>
|
||||
{/if}
|
||||
{#if service.fqdn}
|
||||
<div class="truncate text-center">{getDomain(service.fqdn) || ''}</div>
|
||||
{/if}
|
||||
{#if !service.type || !service.fqdn}
|
||||
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
|
||||
Configuration missing
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-center truncate">{service.type}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
357
apps/ui/src/routes/settings/index.svelte
Normal file
357
apps/ui/src/routes/settings/index.svelte
Normal file
@@ -0,0 +1,357 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async () => {
|
||||
try {
|
||||
const response = await get(`/settings`);
|
||||
return {
|
||||
props: {
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(error)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let settings: any;
|
||||
|
||||
import Setting from '$lib/components/Setting.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { del, get, post } from '$lib/api';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import { browser } from '$app/env';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { t } from '$lib/translations';
|
||||
import { appSession, features, isTraefikUsed } from '$lib/store';
|
||||
import { errorNotification, getDomain } from '$lib/common';
|
||||
|
||||
let isRegistrationEnabled = settings.isRegistrationEnabled;
|
||||
let dualCerts = settings.dualCerts;
|
||||
let isAutoUpdateEnabled = settings.isAutoUpdateEnabled;
|
||||
let isDNSCheckEnabled = settings.isDNSCheckEnabled;
|
||||
$isTraefikUsed = settings.isTraefikUsed;
|
||||
|
||||
let minPort = settings.minPort;
|
||||
let maxPort = settings.maxPort;
|
||||
|
||||
let forceSave = false;
|
||||
let fqdn = settings.fqdn;
|
||||
let nonWWWDomain = fqdn && getDomain(fqdn).replace(/^www\./, '');
|
||||
let isNonWWWDomainOK = false;
|
||||
let isWWWDomainOK = false;
|
||||
let isFqdnSet = !!settings.fqdn;
|
||||
let loading = {
|
||||
save: false,
|
||||
remove: false,
|
||||
proxyMigration: false
|
||||
};
|
||||
|
||||
async function removeFqdn() {
|
||||
if (fqdn) {
|
||||
loading.remove = true;
|
||||
try {
|
||||
const { redirect } = await del(`/settings`, { fqdn });
|
||||
return redirect ? window.location.replace(redirect) : window.location.reload();
|
||||
} catch (error ) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.remove = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
async function changeSettings(name: any) {
|
||||
try {
|
||||
resetView();
|
||||
if (name === 'isRegistrationEnabled') {
|
||||
isRegistrationEnabled = !isRegistrationEnabled;
|
||||
}
|
||||
if (name === 'dualCerts') {
|
||||
dualCerts = !dualCerts;
|
||||
}
|
||||
if (name === 'isAutoUpdateEnabled') {
|
||||
isAutoUpdateEnabled = !isAutoUpdateEnabled;
|
||||
}
|
||||
if (name === 'isDNSCheckEnabled') {
|
||||
isDNSCheckEnabled = !isDNSCheckEnabled;
|
||||
}
|
||||
|
||||
await post(`/settings`, {
|
||||
isRegistrationEnabled,
|
||||
dualCerts,
|
||||
isAutoUpdateEnabled,
|
||||
isDNSCheckEnabled
|
||||
});
|
||||
return toast.push(t.get('application.settings_saved'));
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
loading.save = true;
|
||||
nonWWWDomain = fqdn && getDomain(fqdn).replace(/^www\./, '');
|
||||
|
||||
if (fqdn !== settings.fqdn) {
|
||||
await post(`/settings/check`, { fqdn, forceSave, dualCerts, isDNSCheckEnabled });
|
||||
await post(`/settings`, { fqdn });
|
||||
return window.location.reload();
|
||||
}
|
||||
if (minPort !== settings.minPort || maxPort !== settings.maxPort) {
|
||||
await post(`/settings`, { minPort, maxPort });
|
||||
settings.minPort = minPort;
|
||||
settings.maxPort = maxPort;
|
||||
}
|
||||
forceSave = false;
|
||||
} catch (error ) {
|
||||
if (error.message?.startsWith($t('application.dns_not_set_partial_error'))) {
|
||||
forceSave = true;
|
||||
if (dualCerts) {
|
||||
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
|
||||
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
|
||||
} else {
|
||||
const isWWW = getDomain(settings.fqdn).includes('www.');
|
||||
if (isWWW) {
|
||||
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
|
||||
} else {
|
||||
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(error)
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.save = false;
|
||||
}
|
||||
}
|
||||
async function isDNSValid(domain: any, isWWW: any) {
|
||||
try {
|
||||
await get(`/settings/check?domain=${domain}`);
|
||||
toast.push('DNS configuration is valid.');
|
||||
isWWW ? (isWWWDomainOK = true) : (isNonWWWDomainOK = true);
|
||||
return true;
|
||||
} catch ({ error }) {
|
||||
errorNotification(error);
|
||||
isWWW ? (isWWWDomainOK = false) : (isNonWWWDomainOK = false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function resetView() {
|
||||
forceSave = false;
|
||||
}
|
||||
async function migrateProxy(to: any) {
|
||||
if (loading.proxyMigration) return;
|
||||
try {
|
||||
loading.proxyMigration = true;
|
||||
await post(`/update`, { type: to });
|
||||
const data = await get(`/settings`);
|
||||
$isTraefikUsed = data.settings.isTraefikUsed;
|
||||
return toast.push('Proxy migration started, it takes a few seconds.');
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.proxyMigration = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">{$t('index.settings')}</div>
|
||||
</div>
|
||||
{#if $appSession.teamId === '0'}
|
||||
<div class="mx-auto max-w-4xl px-6">
|
||||
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
|
||||
<div class="flex space-x-1 pb-6">
|
||||
<div class="title font-bold">{$t('index.global_settings')}</div>
|
||||
<button
|
||||
type="submit"
|
||||
class:bg-green-600={!loading.save}
|
||||
class:bg-orange-600={forceSave}
|
||||
class:hover:bg-green-500={!loading.save}
|
||||
class:hover:bg-orange-400={forceSave}
|
||||
disabled={loading.save}
|
||||
>{loading.save
|
||||
? $t('forms.saving')
|
||||
: forceSave
|
||||
? $t('forms.confirm_continue')
|
||||
: $t('forms.save')}</button
|
||||
>
|
||||
|
||||
{#if isFqdnSet}
|
||||
<button
|
||||
on:click|preventDefault={removeFqdn}
|
||||
disabled={loading.remove}
|
||||
class:bg-red-600={!loading.remove}
|
||||
class:hover:bg-red-500={!loading.remove}
|
||||
>{loading.remove ? $t('forms.removing') : $t('forms.remove_domain')}</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grid grid-flow-row gap-2 px-10">
|
||||
<!-- <Language /> -->
|
||||
<div class="grid grid-cols-2 items-start">
|
||||
<div class="flex-col">
|
||||
<div class="pt-2 text-base font-bold text-stone-100">
|
||||
{$t('application.url_fqdn')}
|
||||
</div>
|
||||
<Explainer text={$t('setting.ssl_explainer')} />
|
||||
</div>
|
||||
<div class="justify-start text-left">
|
||||
<input
|
||||
bind:value={fqdn}
|
||||
readonly={!$appSession.isAdmin || isFqdnSet}
|
||||
disabled={!$appSession.isAdmin || isFqdnSet}
|
||||
on:input={resetView}
|
||||
name="fqdn"
|
||||
id="fqdn"
|
||||
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
|
||||
placeholder="{$t('forms.eg')}: https://coolify.io"
|
||||
/>
|
||||
|
||||
{#if forceSave}
|
||||
<div class="flex-col space-y-2 pt-4 text-center">
|
||||
{#if isNonWWWDomainOK}
|
||||
<button
|
||||
class="bg-green-600 hover:bg-green-500"
|
||||
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
|
||||
>DNS settings for {nonWWWDomain} is OK, click to recheck.</button
|
||||
>
|
||||
{:else}
|
||||
<button
|
||||
class="bg-red-600 hover:bg-red-500"
|
||||
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
|
||||
>DNS settings for {nonWWWDomain} is invalid, click to recheck.</button
|
||||
>
|
||||
{/if}
|
||||
{#if dualCerts}
|
||||
{#if isWWWDomainOK}
|
||||
<button
|
||||
class="bg-green-600 hover:bg-green-500"
|
||||
on:click|preventDefault={() =>
|
||||
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
|
||||
>DNS settings for www.{nonWWWDomain} is OK, click to recheck.</button
|
||||
>
|
||||
{:else}
|
||||
<button
|
||||
class="bg-red-600 hover:bg-red-500"
|
||||
on:click|preventDefault={() =>
|
||||
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
|
||||
>DNS settings for www.{nonWWWDomain} is invalid, click to recheck.</button
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-start py-6">
|
||||
<div class="flex-col">
|
||||
<div class="pt-2 text-base font-bold text-stone-100">
|
||||
{$t('forms.public_port_range')}
|
||||
</div>
|
||||
<Explainer text={$t('forms.public_port_range_explainer')} />
|
||||
</div>
|
||||
<div class="mx-auto flex-row items-center justify-center space-y-2">
|
||||
<input
|
||||
class="h-8 w-20 px-2"
|
||||
type="number"
|
||||
bind:value={minPort}
|
||||
min="1024"
|
||||
max={maxPort}
|
||||
/>
|
||||
-
|
||||
<input
|
||||
class="h-8 w-20 px-2"
|
||||
type="number"
|
||||
bind:value={maxPort}
|
||||
min={minPort}
|
||||
max="65543"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
bind:setting={isDNSCheckEnabled}
|
||||
title={$t('setting.is_dns_check_enabled')}
|
||||
description={$t('setting.is_dns_check_enabled_explainer')}
|
||||
on:click={() => changeSettings('isDNSCheckEnabled')}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
dataTooltip={$t('setting.must_remove_domain_before_changing')}
|
||||
disabled={isFqdnSet}
|
||||
bind:setting={dualCerts}
|
||||
title={$t('application.ssl_www_and_non_www')}
|
||||
description={$t('setting.generate_www_non_www_ssl')}
|
||||
on:click={() => !isFqdnSet && changeSettings('dualCerts')}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
bind:setting={isRegistrationEnabled}
|
||||
title={$t('setting.registration_allowed')}
|
||||
description={$t('setting.registration_allowed_explainer')}
|
||||
on:click={() => changeSettings('isRegistrationEnabled')}
|
||||
/>
|
||||
</div>
|
||||
{#if browser && $features.beta}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
bind:setting={isAutoUpdateEnabled}
|
||||
title={$t('setting.auto_update_enabled')}
|
||||
description={$t('setting.auto_update_enabled_explainer')}
|
||||
on:click={() => changeSettings('isAutoUpdateEnabled')}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
{#if !settings.isTraefikUsed}
|
||||
<div class="flex space-x-1 pt-6 font-bold">
|
||||
<div class="title">{$t('setting.coolify_proxy_settings')}</div>
|
||||
</div>
|
||||
<Explainer
|
||||
text={$t('setting.credential_stat_explainer', {
|
||||
link: fqdn
|
||||
? `http://${settings.proxyUser}:${settings.proxyPassword}@` + getDomain(fqdn) + ':8404'
|
||||
: browser &&
|
||||
`http://${settings.proxyUser}:${settings.proxyPassword}@` +
|
||||
window.location.hostname +
|
||||
':8404'
|
||||
})}
|
||||
/>
|
||||
<div class="space-y-2 px-10 py-5">
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="proxyUser">{$t('forms.user')}</label>
|
||||
<CopyPasswordField
|
||||
readonly
|
||||
disabled
|
||||
id="proxyUser"
|
||||
name="proxyUser"
|
||||
value={settings.proxyUser}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="proxyPassword">{$t('forms.password')}</label>
|
||||
<CopyPasswordField
|
||||
readonly
|
||||
disabled
|
||||
id="proxyPassword"
|
||||
name="proxyPassword"
|
||||
isPasswordField
|
||||
value={settings.proxyPassword}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mx-auto max-w-4xl px-6">
|
||||
<!-- <Language /> -->
|
||||
</div>
|
||||
{/if}
|
||||
217
apps/ui/src/routes/sources/[id]/_Github.svelte
Normal file
217
apps/ui/src/routes/sources/[id]/_Github.svelte
Normal file
@@ -0,0 +1,217 @@
|
||||
<script lang="ts">
|
||||
export let source: any;
|
||||
export let settings: any;
|
||||
import { page } from '$app/stores';
|
||||
import { post } from '$lib/api';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { t } from '$lib/translations';
|
||||
import { dashify, errorNotification, getDomain } from '$lib/common';
|
||||
import { appSession } from '$lib/store';
|
||||
import { dev } from '$app/env';
|
||||
|
||||
const { id } = $page.params;
|
||||
let loading = false;
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
try {
|
||||
await post(`/sources/${id}`, {
|
||||
name: source.name,
|
||||
htmlUrl: source.htmlUrl.replace(/\/$/, ''),
|
||||
apiUrl: source.apiUrl.replace(/\/$/, '')
|
||||
});
|
||||
toast.push('Settings saved.');
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function installRepositories(source: any) {
|
||||
const { htmlUrl } = source;
|
||||
const left = screen.width / 2 - 1020 / 2;
|
||||
const top = screen.height / 2 - 1000 / 2;
|
||||
const newWindow = open(
|
||||
`${htmlUrl}/apps/${source.githubApp.name}/installations/new`,
|
||||
'GitHub',
|
||||
'resizable=1, scrollbars=1, fullscreen=0, height=1000, 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 newGithubApp() {
|
||||
loading = true;
|
||||
try {
|
||||
const { id } = await post(`/sources/new/github`, {
|
||||
type: 'github',
|
||||
name: source.name,
|
||||
htmlUrl: source.htmlUrl.replace(/\/$/, ''),
|
||||
apiUrl: source.apiUrl.replace(/\/$/, ''),
|
||||
organization: source.organization
|
||||
});
|
||||
const { organization, htmlUrl } = source;
|
||||
const { fqdn } = settings;
|
||||
const host = dev
|
||||
? 'http://localhost:3001'
|
||||
: fqdn
|
||||
? fqdn
|
||||
: `http://${window.location.host}` || '';
|
||||
const domain = getDomain(fqdn);
|
||||
|
||||
let url = 'settings/apps/new';
|
||||
if (organization) url = `organizations/${organization}/settings/apps/new`;
|
||||
const name = dashify(domain) || 'app';
|
||||
const data = JSON.stringify({
|
||||
name: `coolify-${name}`,
|
||||
url: host,
|
||||
hook_attributes: {
|
||||
url: dev
|
||||
? 'https://webhook.site/0e5beb2c-4e9b-40e2-a89e-32295e570c21/events'
|
||||
: `${host}/webhooks/github/events`
|
||||
},
|
||||
redirect_url: `${host}/webhooks/github`,
|
||||
callback_urls: [`${host}/login/github/app`],
|
||||
public: false,
|
||||
request_oauth_on_install: false,
|
||||
setup_url: `${host}/webhooks/github/install?gitSourceId=${id}`,
|
||||
setup_on_update: true,
|
||||
default_permissions: {
|
||||
contents: 'read',
|
||||
metadata: 'read',
|
||||
pull_requests: 'read',
|
||||
emails: 'read'
|
||||
},
|
||||
default_events: ['pull_request', 'push']
|
||||
});
|
||||
const form = document.createElement('form');
|
||||
form.setAttribute('method', 'post');
|
||||
form.setAttribute('action', `${htmlUrl}/${url}?state=${id}`);
|
||||
const input = document.createElement('input');
|
||||
input.setAttribute('id', 'manifest');
|
||||
input.setAttribute('name', 'manifest');
|
||||
input.setAttribute('type', 'hidden');
|
||||
input.setAttribute('value', data);
|
||||
form.appendChild(input);
|
||||
document.getElementsByTagName('body')[0].appendChild(form);
|
||||
form.submit();
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mx-auto max-w-4xl px-6">
|
||||
{#if !source.githubAppId}
|
||||
<form on:submit|preventDefault={newGithubApp} class="py-4">
|
||||
<div class="flex space-x-1 pb-5 font-bold">
|
||||
<div class="title">General</div>
|
||||
</div>
|
||||
<div class="grid grid-flow-row gap-2 px-10">
|
||||
<div class="grid grid-flow-row gap-2">
|
||||
<div class="mt-2 grid grid-cols-2 items-center">
|
||||
<label for="name" class="text-base font-bold text-stone-100">Name</label>
|
||||
<input name="name" id="name" required bind:value={source.name} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="htmlUrl" class="text-base font-bold text-stone-100">HTML URL</label>
|
||||
<input name="htmlUrl" id="htmlUrl" required bind:value={source.htmlUrl} />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="apiUrl" class="text-base font-bold text-stone-100">API URL</label>
|
||||
<input name="apiUrl" id="apiUrl" required bind:value={source.apiUrl} />
|
||||
</div>
|
||||
<div class="grid grid-cols-2">
|
||||
<div class="flex flex-col">
|
||||
<label for="organization" class="pt-2 text-base font-bold text-stone-100"
|
||||
>Organization</label
|
||||
>
|
||||
<Explainer
|
||||
text="Fill it if you would like to use an organization's as your Git Source. Otherwise your user will be used."
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
name="organization"
|
||||
id="organization"
|
||||
placeholder="eg: coollabsio"
|
||||
bind:value={source.organization}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if source.apiUrl && source.htmlUrl && source.name}
|
||||
<div class="text-center">
|
||||
<button class=" mt-8 bg-orange-600" type="submit">Create new GitHub App</button>
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
{:else if source.githubApp?.installationId}
|
||||
<form on:submit|preventDefault={handleSubmit} class="py-4">
|
||||
<div class="flex space-x-1 pb-5 font-bold">
|
||||
<div class="title">{$t('general')}</div>
|
||||
{#if $appSession.isAdmin}
|
||||
<button
|
||||
type="submit"
|
||||
class:bg-orange-600={!loading}
|
||||
class:hover:bg-orange-500={!loading}
|
||||
disabled={loading}>{loading ? 'Saving...' : 'Save'}</button
|
||||
>
|
||||
|
||||
<button
|
||||
><a
|
||||
class="no-underline"
|
||||
href={`${source.htmlUrl}/apps/${source.githubApp.name}/installations/new`}
|
||||
>{$t('source.change_app_settings', { name: 'GitHub' })}</a
|
||||
></button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grid grid-flow-row gap-2 px-10">
|
||||
<div class="grid grid-flow-row gap-2">
|
||||
<div class="mt-2 grid grid-cols-2 items-center">
|
||||
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
|
||||
<input name="name" id="name" required bind:value={source.name} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="htmlUrl" class="text-base font-bold text-stone-100">HTML URL</label>
|
||||
<input name="htmlUrl" id="htmlUrl" required bind:value={source.htmlUrl} />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="apiUrl" class="text-base font-bold text-stone-100">API URL</label>
|
||||
<input name="apiUrl" id="apiUrl" required bind:value={source.apiUrl} />
|
||||
</div>
|
||||
<div class="grid grid-cols-2">
|
||||
<div class="flex flex-col">
|
||||
<label for="organization" class="pt-2 text-base font-bold text-stone-100"
|
||||
>Organization</label
|
||||
>
|
||||
</div>
|
||||
<input
|
||||
readonly
|
||||
disabled
|
||||
name="organization"
|
||||
id="organization"
|
||||
placeholder="eg: coollabsio"
|
||||
bind:value={source.organization}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{:else}
|
||||
<div class="text-center">
|
||||
<a href={`${source.htmlUrl}/apps/${source.githubApp.name}/installations/new`}>
|
||||
<button class=" bg-orange-600 hover:bg-orange-500 mt-8">Install Repositories</button></a
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
284
apps/ui/src/routes/sources/[id]/_Gitlab.svelte
Normal file
284
apps/ui/src/routes/sources/[id]/_Gitlab.svelte
Normal file
@@ -0,0 +1,284 @@
|
||||
<script lang="ts">
|
||||
export let source: any;
|
||||
export let settings: any;
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { post } from '$lib/api';
|
||||
import { dev } from '$app/env';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { appSession } from '$lib/store';
|
||||
import { goto } from '$app/navigation';
|
||||
const { id } = $page.params;
|
||||
let url = settings.fqdn ? settings.fqdn : window.location.origin;
|
||||
|
||||
if (dev) {
|
||||
url = `http://localhost:3001`;
|
||||
}
|
||||
let loading = false;
|
||||
|
||||
let oauthIdEl: HTMLInputElement;
|
||||
let applicationType: string;
|
||||
if (!source.gitlabAppId) {
|
||||
source.gitlabApp = {
|
||||
oauthId: null,
|
||||
groupName: null,
|
||||
appId: null,
|
||||
appSecret: null
|
||||
};
|
||||
}
|
||||
onMount(() => {
|
||||
oauthIdEl && oauthIdEl.focus();
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
if (loading) return;
|
||||
loading = true;
|
||||
if (!source.gitlabAppId) {
|
||||
// New GitLab App
|
||||
try {
|
||||
const { id } = await post(`/sources/new/gitlab`, {
|
||||
type: 'gitlab',
|
||||
name: source.name,
|
||||
htmlUrl: source.htmlUrl.replace(/\/$/, ''),
|
||||
apiUrl: source.apiUrl.replace(/\/$/, ''),
|
||||
oauthId: source.gitlabApp.oauthId,
|
||||
appId: source.gitlabApp.appId,
|
||||
appSecret: source.gitlabApp.appSecret,
|
||||
groupName: source.gitlabApp.groupName
|
||||
});
|
||||
const from = $page.url.searchParams.get('from');
|
||||
if (from) {
|
||||
return window.location.assign(from);
|
||||
}
|
||||
return window.location.assign(`/sources/${id}`);
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
} else {
|
||||
// Update GitLab App
|
||||
try {
|
||||
await post(`/sources/${id}`, {
|
||||
name: source.name,
|
||||
htmlUrl: source.htmlUrl.replace(/\/$/, ''),
|
||||
apiUrl: source.apiUrl.replace(/\/$/, '')
|
||||
});
|
||||
toast.push('Settings saved.');
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function changeSettings() {
|
||||
const {
|
||||
htmlUrl,
|
||||
gitlabApp: { oauthId }
|
||||
} = source;
|
||||
const left = screen.width / 2 - 1020 / 2;
|
||||
const top = screen.height / 2 - 1000 / 2;
|
||||
const newWindow = open(
|
||||
`${htmlUrl}/oauth/applications/${oauthId}`,
|
||||
'GitLab',
|
||||
'resizable=1, scrollbars=1, fullscreen=0, height=1000, width=1020,top=' +
|
||||
top +
|
||||
', left=' +
|
||||
left +
|
||||
', toolbar=0, menubar=0, status=0'
|
||||
);
|
||||
const timer = setInterval(() => {
|
||||
if (newWindow?.closed) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
async function checkOauthId() {
|
||||
if (source.gitlabApp?.oauthId) {
|
||||
try {
|
||||
await post(`/sources/${id}/check`, {
|
||||
oauthId: source.gitlabApp?.oauthId
|
||||
});
|
||||
} catch ({ error }) {
|
||||
source.gitlabApp.oauthId = null;
|
||||
oauthIdEl.focus();
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
function newApp() {
|
||||
switch (applicationType) {
|
||||
case 'user':
|
||||
window.open(`${source.htmlUrl}/-/profile/applications`);
|
||||
break;
|
||||
case 'group':
|
||||
window.open(
|
||||
`${source.htmlUrl}/groups/${source.gitlabApp.groupName}/-/settings/applications`
|
||||
);
|
||||
break;
|
||||
case 'instance':
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mx-auto max-w-4xl px-6">
|
||||
<form on:submit|preventDefault={handleSubmit} class="py-4">
|
||||
<div class="flex space-x-1 pb-7 font-bold">
|
||||
<div class="title">General</div>
|
||||
{#if $appSession.isAdmin}
|
||||
<button
|
||||
type="submit"
|
||||
class:bg-orange-600={!loading}
|
||||
class:hover:bg-orange-500={!loading}
|
||||
disabled={loading}>{loading ? $t('forms.saving') : $t('forms.save')}</button
|
||||
>
|
||||
{#if source.gitlabAppId}
|
||||
<button on:click|preventDefault={changeSettings}
|
||||
>{$t('source.change_app_settings', { name: 'GitLab' })}</button
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grid grid-flow-row gap-2 px-10">
|
||||
{#if !source.gitlabAppId}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="type" class="text-base font-bold text-stone-100">Application Type</label>
|
||||
<select name="type" id="type" class="w-96" bind:value={applicationType}>
|
||||
<option value="user">{$t('source.gitlab.user_owned')}</option>
|
||||
<option value="group">{$t('source.gitlab.group_owned')}</option>
|
||||
{#if source.htmlUrl !== 'https://gitlab.com'}
|
||||
<option value="instance">{$t('source.gitlab.self_hosted')}</option>
|
||||
{/if}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{#if applicationType === 'group'}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="groupName" class="text-base font-bold text-stone-100">Group Name</label>
|
||||
<input
|
||||
name="groupName"
|
||||
id="groupName"
|
||||
required
|
||||
bind:value={source.gitlabApp.groupName}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="grid grid-flow-row gap-2">
|
||||
<div class="mt-2 grid grid-cols-2 items-center">
|
||||
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
|
||||
<input name="name" id="name" required bind:value={source.name} />
|
||||
</div>
|
||||
</div>
|
||||
{#if source.gitlabApp.groupName}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="groupName" class="text-base font-bold text-stone-100"
|
||||
>{$t('source.group_name')}</label
|
||||
>
|
||||
<input
|
||||
name="groupName"
|
||||
id="groupName"
|
||||
disabled={source.gitlabAppId}
|
||||
readonly={source.gitlabAppId}
|
||||
required
|
||||
bind:value={source.gitlabApp.groupName}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="htmlUrl" class="text-base font-bold text-stone-100">HTML URL</label>
|
||||
<input
|
||||
name="htmlUrl"
|
||||
id="htmlUrl"
|
||||
required
|
||||
disabled={source.gitlabAppId}
|
||||
readonly={source.gitlabAppId}
|
||||
value={source.htmlUrl}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="apiUrl" class="text-base font-bold text-stone-100">API URL</label>
|
||||
<input
|
||||
name="apiUrl"
|
||||
id="apiUrl"
|
||||
disabled={source.gitlabAppId}
|
||||
readonly={source.gitlabAppId}
|
||||
required
|
||||
value={source.apiUrl}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-start">
|
||||
<div class="flex-col">
|
||||
<label for="oauthId" class="pt-2 text-base font-bold text-stone-100"
|
||||
>{$t('source.oauth_id')}</label
|
||||
>
|
||||
{#if !source.gitlabAppId}
|
||||
<Explainer text={$t('source.oauth_id_explainer')} />
|
||||
{/if}
|
||||
</div>
|
||||
<input
|
||||
disabled={source.gitlabAppId}
|
||||
readonly={source.gitlabAppId}
|
||||
on:change={checkOauthId}
|
||||
bind:this={oauthIdEl}
|
||||
name="oauthId"
|
||||
id="oauthId"
|
||||
type="number"
|
||||
required
|
||||
bind:value={source.gitlabApp.oauthId}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="appId" class="text-base font-bold text-stone-100"
|
||||
>{$t('source.application_id')}</label
|
||||
>
|
||||
<input
|
||||
name="appId"
|
||||
id="appId"
|
||||
disabled={source.gitlabAppId}
|
||||
readonly={source.gitlabAppId}
|
||||
required
|
||||
bind:value={source.gitlabApp.appId}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="appSecret" class="text-base font-bold text-stone-100"
|
||||
>{$t('index.secret')}</label
|
||||
>
|
||||
<CopyPasswordField
|
||||
disabled={source.gitlabAppId}
|
||||
readonly={source.gitlabAppId}
|
||||
isPasswordField={true}
|
||||
name="appSecret"
|
||||
id="appSecret"
|
||||
required
|
||||
bind:value={source.gitlabApp.appSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{#if !source.gitlabAppId}
|
||||
<Explainer
|
||||
customClass="w-full"
|
||||
text="<span class='font-bold text-base text-white'>Scopes required:</span>
|
||||
<br>- <span class='text-orange-500 font-bold'>api</span> (Access the authenticated user's API)
|
||||
<br>- <span class='text-orange-500 font-bold'>read_repository</span> (Allows read-only access to the repository)
|
||||
<br>- <span class='text-orange-500 font-bold'>email</span> (Allows read-only access to the user's primary email address using OpenID Connect)
|
||||
<br>
|
||||
<br>For extra security, you can set <span class='text-orange-500 font-bold'>Expire Access Tokens</span>
|
||||
<br><br>Webhook URL: <span class='text-orange-500 font-bold'>{url}/webhooks/gitlab</span>"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
62
apps/ui/src/routes/sources/[id]/_New.svelte
Normal file
62
apps/ui/src/routes/sources/[id]/_New.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<script lang="ts">
|
||||
export let source: any;
|
||||
export let settings: any;
|
||||
import Github from './_Github.svelte';
|
||||
import Gitlab from './_Gitlab.svelte';
|
||||
function setPredefined(type: string) {
|
||||
switch (type) {
|
||||
case 'github':
|
||||
source.name = 'Github.com';
|
||||
source.type = 'github';
|
||||
source.htmlUrl = 'https://github.com';
|
||||
source.apiUrl = 'https://api.github.com';
|
||||
source.organization = undefined;
|
||||
|
||||
break;
|
||||
case 'gitlab':
|
||||
source.name = 'Gitlab.com';
|
||||
source.type = 'gitlab';
|
||||
source.htmlUrl = 'https://gitlab.com';
|
||||
source.apiUrl = 'https://gitlab.com/api';
|
||||
source.organization = undefined;
|
||||
|
||||
break;
|
||||
case 'bitbucket':
|
||||
source.name = 'Bitbucket.com';
|
||||
source.type = 'bitbucket';
|
||||
source.htmlUrl = 'https://bitbucket.com';
|
||||
source.apiUrl = 'https://api.bitbucket.org';
|
||||
source.organization = undefined;
|
||||
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold">
|
||||
<div class="-mb-1 flex-col">
|
||||
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
|
||||
New Git Source
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center">
|
||||
<div class="flex-col space-y-2 pb-10 text-center">
|
||||
<div class="text-xl font-bold text-white">Select a provider</div>
|
||||
<div class="flex justify-center space-x-2">
|
||||
<button class="w-32" on:click={() => setPredefined('github')}>GitHub.com</button>
|
||||
<button class="w-32" on:click={() => setPredefined('gitlab')}>GitLab.com</button>
|
||||
</div>
|
||||
</div>
|
||||
{#if source?.type}
|
||||
<div>
|
||||
{#if source.type === 'github'}
|
||||
<Github bind:source {settings} />
|
||||
{:else if source.type === 'gitlab'}
|
||||
<Gitlab bind:source {settings} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
58
apps/ui/src/routes/sources/[id]/_Source.svelte
Normal file
58
apps/ui/src/routes/sources/[id]/_Source.svelte
Normal file
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import Github from './_Github.svelte';
|
||||
import Gitlab from './_Gitlab.svelte';
|
||||
|
||||
export let source: any;
|
||||
export let settings: any;
|
||||
</script>
|
||||
|
||||
<div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold">
|
||||
<div class="-mb-5 flex-col">
|
||||
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
|
||||
Configuration
|
||||
</div>
|
||||
<span class="text-xs">{source.name}</span>
|
||||
</div>
|
||||
{#if source?.type === 'gitlab'}
|
||||
<svg viewBox="0 0 128 128" class="w-8">
|
||||
<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 source?.type === 'github'}
|
||||
<svg viewBox="0 0 128 128" class="w-8">
|
||||
<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}
|
||||
</div>
|
||||
<div>
|
||||
{#if source.type === 'github'}
|
||||
<Github bind:source {settings} />
|
||||
{:else if source.type === 'gitlab'}
|
||||
<Gitlab bind:source {settings} />
|
||||
{/if}
|
||||
</div>
|
||||
70
apps/ui/src/routes/sources/[id]/__layout.svelte
Normal file
70
apps/ui/src/routes/sources/[id]/__layout.svelte
Normal file
@@ -0,0 +1,70 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
|
||||
export const load: Load = async ({ fetch, url, params }) => {
|
||||
try {
|
||||
const { id } = params;
|
||||
const response = await get(`/sources/${id}`);
|
||||
const { source, settings } = response;
|
||||
if (id !== 'new' && (!source || Object.entries(source).length === 0)) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/sources'
|
||||
};
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
source
|
||||
},
|
||||
stuff: {
|
||||
source,
|
||||
settings
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return handlerNotFoundLoad(error, url);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let source: any;
|
||||
import { del, get } from '$lib/api';
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { errorNotification, handlerNotFoundLoad } from '$lib/common';
|
||||
import { t } from '$lib/translations';
|
||||
import { appSession } from '$lib/store';
|
||||
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
const { id } = $page.params;
|
||||
|
||||
async function deleteSource(name: string) {
|
||||
const sure = confirm($t('application.confirm_to_delete', { name }));
|
||||
if (sure) {
|
||||
try {
|
||||
await del(`/sources/${id}`, {});
|
||||
await goto('/sources', { replaceState: true });
|
||||
} catch (error) {
|
||||
errorNotification(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if id !== 'new'}
|
||||
<nav class="nav-side">
|
||||
<button
|
||||
on:click={() => deleteSource(source.name)}
|
||||
title={$t('source.delete_git_source')}
|
||||
type="submit"
|
||||
disabled={!$appSession.isAdmin}
|
||||
class:hover:text-red-500={$appSession.isAdmin}
|
||||
class="icons tooltip-bottom bg-transparent text-sm"
|
||||
data-tooltip={$appSession.isAdmin
|
||||
? $t('source.delete_git_source')
|
||||
: $t('source.permission_denied')}><DeleteIcon /></button
|
||||
>
|
||||
</nav>
|
||||
{/if}
|
||||
<slot />
|
||||
24
apps/ui/src/routes/sources/[id]/index.svelte
Normal file
24
apps/ui/src/routes/sources/[id]/index.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, stuff }) => {
|
||||
return {
|
||||
props: { ...stuff }
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let source: any;
|
||||
export let settings: any;
|
||||
import { page } from '$app/stores';
|
||||
import Source from './_Source.svelte';
|
||||
import New from './_New.svelte';
|
||||
const { id } = $page.params;
|
||||
</script>
|
||||
|
||||
|
||||
{#if id === 'new'}
|
||||
<New bind:source bind:settings />
|
||||
{:else}
|
||||
<Source bind:source bind:settings />
|
||||
{/if}
|
||||
159
apps/ui/src/routes/sources/index.svelte
Normal file
159
apps/ui/src/routes/sources/index.svelte
Normal file
@@ -0,0 +1,159 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async () => {
|
||||
try {
|
||||
const response = await get(`/sources`);
|
||||
return {
|
||||
props: {
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(error)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let sources: any = [];
|
||||
import { get, post } from '$lib/api';
|
||||
import { goto } from '$app/navigation';
|
||||
import { getDomain } from '$lib/common';
|
||||
import { t } from '$lib/translations';
|
||||
import { appSession } from '$lib/store';
|
||||
const ownSources = sources.filter((source: { teams: { id: any }[] }) => {
|
||||
if (source.teams[0].id === $appSession.teamId) {
|
||||
return source;
|
||||
}
|
||||
});
|
||||
const otherSources = sources.filter((source: any) => {
|
||||
if (source.teams[0].id !== $appSession.teamId) {
|
||||
return source;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">{$t('index.git_sources')}</div>
|
||||
{#if $appSession.isAdmin}
|
||||
<a href="/sources/new" 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>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-col justify-center">
|
||||
{#if !sources || ownSources.length === 0}
|
||||
<div class="flex-col">
|
||||
<div class="text-center text-xl font-bold">{$t('source.no_git_sources_found')}</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if ownSources.length > 0 || otherSources.length > 0}
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each ownSources as source}
|
||||
<a href="/sources/{source.id}" class="w-96 p-2 no-underline">
|
||||
<div
|
||||
class="box-selection group relative hover:bg-orange-600"
|
||||
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="absolute top-0 left-0 -m-5 h-10 w-10">
|
||||
{#if source?.type === 'gitlab'}
|
||||
<svg viewBox="0 0 128 128">
|
||||
<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 source?.type === 'github'}
|
||||
<svg viewBox="0 0 128 128">
|
||||
<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}
|
||||
</div>
|
||||
<div class="truncate text-center text-xl font-bold">{source.name}</div>
|
||||
{#if $appSession.teamId === '0' && otherSources.length > 0}
|
||||
<div class="truncate text-center">{source.teams[0].name}</div>
|
||||
{/if}
|
||||
|
||||
{#if (source.type === 'gitlab' && !source.gitlabAppId) || (source.type === 'github' && source.githubApp?.installationId === null)}
|
||||
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
|
||||
{$t('application.configuration.configuration_missing')}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="truncate text-center">{getDomain(source.htmlUrl) || ''}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if otherSources.length > 0 && $appSession.teamId === '0'}
|
||||
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Sources</div>
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each otherSources as source}
|
||||
<a href="/sources/{source.id}" class="w-96 p-2 no-underline">
|
||||
<div
|
||||
class="box-selection group hover:bg-orange-600"
|
||||
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="truncate text-center text-xl font-bold">{source.name}</div>
|
||||
{#if $appSession.teamId === '0'}
|
||||
<div class="truncate text-center">{source.teams[0].name}</div>
|
||||
{/if}
|
||||
{#if (source.type === 'gitlab' && !source.gitlabAppId) || (source.type === 'github' && source.githubApp?.installationId === null)}
|
||||
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
|
||||
{$t('application.configuration.configuration_missing')}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="truncate text-center">{source.htmlUrl}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
6
apps/ui/src/routes/webhooks/success.svelte
Normal file
6
apps/ui/src/routes/webhooks/success.svelte
Normal file
@@ -0,0 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
const token = $page.url.searchParams.get('token');
|
||||
localStorage.setItem('gitLabToken', token);
|
||||
window.close();
|
||||
</script>
|
||||
Reference in New Issue
Block a user