Merge branch 'next' into main

This commit is contained in:
Andras Bacsai
2022-11-29 10:50:00 +01:00
committed by GitHub
220 changed files with 11969 additions and 9333 deletions

View File

@@ -16,6 +16,7 @@
}
</script>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div class="dropdown dropdown-bottom">
<slot>
<label for="new" tabindex="0" class="btn btn-sm text-sm bg-coollabs hover:bg-coollabs-100 w-64">

View File

@@ -65,7 +65,6 @@
<script lang="ts">
export let baseSettings: any;
export let supportedServiceTypesAndVersions: any;
export let pendingInvitations: any = 0;
$appSession.isRegistrationEnabled = baseSettings.isRegistrationEnabled;
@@ -74,7 +73,6 @@
$appSession.version = baseSettings.version;
$appSession.whiteLabeled = baseSettings.whiteLabeled;
$appSession.whiteLabeledDetails.icon = baseSettings.whiteLabeledIcon;
$appSession.supportedServiceTypesAndVersions = supportedServiceTypesAndVersions;
$appSession.pendingInvitations = pendingInvitations;
@@ -83,6 +81,7 @@
export let permission: string;
export let isAdmin: boolean;
import { status, io } from '$lib/store';
import '../tailwind.css';
import Cookies from 'js-cookie';
import { fade } from 'svelte/transition';
@@ -95,6 +94,8 @@
import { appSession } from '$lib/store';
import Toasts from '$lib/components/Toasts.svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
import { onMount } from 'svelte';
import LocalePicker from '$lib/components/LocalePicker.svelte';
if (userId) $appSession.userId = userId;
if (teamId) $appSession.teamId = teamId;
@@ -109,6 +110,16 @@
return errorNotification(error);
}
}
onMount(async () => {
io.connect();
io.on('start-service', (message) => {
const { serviceId, state } = message;
$status.service.startup[serviceId] = state;
if (state === 0 || state === 1) {
delete $status.service.startup[serviceId];
}
});
});
</script>
<svelte:head>
@@ -259,6 +270,7 @@
</svg>
</a>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
id="logout"
class="icons bg-coolgray-200 hover:text-error cursor-pointer"
@@ -281,13 +293,16 @@
<path d="M7 12h14l-3 -3m0 6l3 -3" />
</svg>
</div>
<!-- <div class="lg:block">
<LocalePicker/>
</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
target="_blank noreferrer">v{$appSession.version}</a
>
</div>
</div>
@@ -295,24 +310,27 @@
</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
>Powered by <a href="https://coolify.io" target="_blank noreferrer">Coolify</a></span
>
{/if}
{/if}
<div
class="navbar lg:hidden space-x-2 flex flex-row items-center bg-coollabs"
class="navbar lg:hidden space-x-2 flex flex-row justify-between bg-coollabs"
class:hidden={!$appSession.userId}
>
<label for="main-drawer" class="drawer-button btn btn-square btn-ghost flex-col">
<span class="burger bg-white" />
<span class="burger bg-white" />
<span class="burger bg-white" />
</label>
<div class="prose flex flex-row justify-between space-x-1 w-full items-center pr-3">
{#if !$appSession.whiteLabeled}
<h3 class="mb-0 text-white">Coolify</h3>
{/if}
<div>
<label for="main-drawer" class="drawer-button btn btn-square btn-ghost flex-col">
<span class="burger bg-white" />
<span class="burger bg-white" />
<span class="burger bg-white" />
</label>
<div class="prose flex flex-row justify-between space-x-1 w-full items-center pr-3">
{#if !$appSession.whiteLabeled}
<h3 class="mb-0 text-white">Coolify</h3>
{/if}
</div>
</div>
<!-- <LocalePicker /> -->
</div>
<main>
<div class={$appSession.userId ? 'lg:pl-16' : null}>
@@ -436,6 +454,7 @@
<UpdateAvailable />
</div>
<li>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="no-underline icons hover:bg-error" on:click={logout}>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -460,7 +479,7 @@
<a
class="text-xs hover:bg-coolgray-200 no-underline hover:text-white text-right"
href={`https://github.com/coollabsio/coolify/releases/tag/v${$appSession.version}`}
target="_blank">v{$appSession.version}</a
target="_blank noreferrer">v{$appSession.version}</a
>
</li>
</ul>

View File

@@ -6,7 +6,7 @@
<ul class="menu border bg-coolgray-100 border-coolgray-200 rounded p-2 space-y-2 sticky top-4">
<li class="menu-title">
<span>Configuration</span>
<span>General</span>
</li>
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
<li>
@@ -86,7 +86,7 @@
<path
d="M7 10h3v-3l-3.5 -3.5a6 6 0 0 1 8 8l6 6a2 2 0 0 1 -3 3l-6 -6a6 6 0 0 1 -8 -8l3.5 3.5"
/>
</svg>Build & Deploy</a
</svg>Configuration</a
>
</li>
<li

View File

@@ -32,10 +32,10 @@
}
</script>
<div class="w-full font-bold grid grid-cols-1 lg:grid-cols-4 gap-2 pb-2">
<div class="w-full grid grid-cols-1 lg:grid-cols-4 gap-2 pb-2">
<div class="flex flex-col">
{#if index === 0 || length === 0}
<label for="name" class="pb-2 uppercase">name</label>
<label for="name" class="pb-2 uppercase font-bold">name</label>
{/if}
<input
@@ -50,7 +50,7 @@
</div>
<div class="flex flex-col">
{#if index === 0 || length === 0}
<label for="value" class="pb-2 uppercase">value</label>
<label for="value" class="pb-2 uppercase font-bold">value</label>
{/if}
<CopyPasswordField
@@ -63,9 +63,12 @@
</div>
<div class="flex lg:flex-col flex-row justify-start items-center pt-3 lg:pt-0">
{#if index === 0 || length === 0}
<label for="name" class="pb-2 uppercase lg:block hidden">Need during buildtime?</label>
<label for="name" class="pb-2 uppercase lg:block hidden font-bold"
>Need during buildtime?</label
>
{/if}
<label for="name" class="pb-2 uppercase lg:hidden block">Need during buildtime?</label>
<label for="name" class="pb-2 uppercase lg:hidden block font-bold">Need during buildtime?</label
>
<div class="flex justify-center h-full items-center pt-0 lg:pt-0 pl-4 lg:pl-0">
<button
@@ -114,7 +117,7 @@
</div>
<div class="flex flex-row lg:flex-col lg:items-center items-start">
{#if index === 0 || length === 0}
<label for="name" class="pb-2 uppercase lg:block hidden">Actions</label>
<label for="name" class="pb-5 uppercase lg:block hidden font-bold" />
{/if}
<div class="flex justify-center h-full items-center pt-3">

View File

@@ -39,11 +39,11 @@
async function addNewSecret() {
try {
if (!name) return errorNotification({ message: 'Name is required.' });
if (!value) return errorNotification({ message: 'Value is required.' });
if (!name.trim()) return errorNotification({ message: 'Name is required.' });
if (!value.trim()) return errorNotification({ message: 'Value is required.' });
await post(`/applications/${id}/secrets`, {
name,
value,
name: name.trim(),
value: value.trim(),
isBuildSecret
});
cleanupState();
@@ -64,8 +64,8 @@
if (isNewSecret) return;
try {
await put(`/applications/${id}/secrets`, {
name,
value,
name: name.trim(),
value: value.trim(),
isBuildSecret: changeIsBuildSecret ? isBuildSecret : undefined
});
addToast({
@@ -79,10 +79,10 @@
}
</script>
<div class="w-full font-bold grid grid-cols-1 lg:grid-cols-4 gap-2 pb-2">
<div class="w-full grid grid-cols-1 lg:grid-cols-4 gap-2 pb-2">
<div class="flex flex-col">
{#if (index === 0 && !isNewSecret) || length === 0}
<label for="name" class="pb-2 uppercase">name</label>
<label for="name" class="pb-2 uppercase font-bold">name</label>
{/if}
<input
@@ -91,7 +91,7 @@
required
placeholder="EXAMPLE_VARIABLE"
readonly={!isNewSecret}
class=" w-full"
class="w-full"
class:bg-coolblack={!isNewSecret}
class:border={!isNewSecret}
class:border-dashed={!isNewSecret}
@@ -101,7 +101,7 @@
</div>
<div class="flex flex-col">
{#if (index === 0 && !isNewSecret) || length === 0}
<label for="value" class="pb-2 uppercase">value</label>
<label for="value" class="pb-2 uppercase font-bold">value</label>
{/if}
<CopyPasswordField
@@ -114,9 +114,12 @@
</div>
<div class="flex lg:flex-col flex-row justify-start items-center pt-3 lg:pt-0">
{#if (index === 0 && !isNewSecret) || length === 0}
<label for="name" class="pb-2 uppercase lg:block hidden">Need during buildtime?</label>
<label for="name" class="pb-2 uppercase lg:block hidden font-bold"
>Need during buildtime?</label
>
{/if}
<label for="name" class="pb-2 uppercase lg:hidden block">Need during buildtime?</label>
<label for="name" class="pb-2 uppercase lg:hidden block font-bold">Need during buildtime?</label
>
<div class="flex justify-center h-full items-center pt-0 lg:pt-0 pl-4 lg:pl-0">
<button
@@ -166,7 +169,7 @@
</div>
<div class="flex flex-row lg:flex-col lg:items-center items-start">
{#if (index === 0 && !isNewSecret) || length === 0}
<label for="name" class="pb-2 uppercase lg:block hidden">Actions</label>
<label for="name" class="pb-5 uppercase lg:block hidden font-bold" />
{/if}
<div class="flex justify-center h-full items-center pt-3">

View File

@@ -59,36 +59,55 @@
}
</script>
<div class="w-full font-bold grid gap-2">
<div class="flex flex-col pb-2">
<div class="flex flex-col lg:flex-row lg:space-y-0 space-y-2">
<div class="w-full lg:px-0 px-4">
{#if storage.predefined}
<div class="flex flex-col lg:flex-row gap-4 pb-2">
<input disabled readonly class="w-full" value={storage.id} />
<input disabled readonly class="w-full" bind:value={storage.path} />
</div>
{:else}
<div class="flex gap-4 pb-2" class:pt-8={isNew}>
{#if storage.applicationId}
{#if storage.oldPath}
<input
disabled
readonly
class="w-full"
value="{storage.applicationId}{storage.path.replace(/\//gi, '-').replace('-app', '')}"
/>
{:else}
<input
disabled
readonly
class="w-full"
value="{storage.applicationId}{storage.path.replace(/\//gi, '-')}"
/>
{/if}
{/if}
<input
class="w-full lg:w-64"
disabled={!isNew}
readonly={!isNew}
class="w-full"
bind:value={storage.path}
required
placeholder="eg: /sqlite.db"
placeholder="eg: /data"
/>
{#if isNew}
<div class="flex items-center justify-center w-full lg:w-64">
<button class="btn btn-sm btn-primary" on:click={() => saveStorage(true)}
>{$t('forms.add')}</button
>
</div>
{:else}
<div class="flex flex-row items-center justify-center space-x-2 w-full lg:w-64">
<div class="flex items-center justify-center">
<button class="btn btn-sm btn-primary" on:click={() => saveStorage(false)}
>{$t('forms.set')}</button
<div class="flex items-center justify-center">
{#if isNew}
<div class="w-full lg:w-64">
<button class="btn btn-sm btn-primary w-full" on:click={() => saveStorage(true)}
>{$t('forms.add')}</button
>
</div>
{:else}
<div class="flex justify-center">
<button class="btn btn-sm btn-error" on:click={removeStorage}
>{$t('forms.remove')}</button
>
</div>
</div>
{/if}
{/if}
</div>
</div>
</div>
{/if}
</div>

View File

@@ -75,7 +75,7 @@
let statusInterval: any;
let forceDelete = false;
let stopping = false;
const { id } = $page.params;
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
@@ -138,17 +138,17 @@
}
async function stopApplication() {
try {
$status.application.initialLoading = true;
stopping = true;
await post(`/applications/${id}/stop`, {});
} catch (error) {
return errorNotification(error);
} finally {
$status.application.initialLoading = false;
stopping = false;
await getStatus();
}
}
async function getStatus() {
if ($status.application.loading) return;
if ($status.application.loading && stopping) return;
$status.application.loading = true;
const data = await get(`/applications/${id}/status`);
@@ -194,6 +194,8 @@
onDestroy(() => {
$status.application.initialLoading = true;
$status.application.loading = false;
$status.application.statuses = [];
$status.application.overallStatus = 'stopped';
$location = null;
$isDeploymentEnabled = false;
clearInterval(statusInterval);
@@ -233,7 +235,7 @@
class:text-red-500={$status.application.overallStatus === 'stopped'}
>
{$status.application.overallStatus === 'healthy'
? 'Running'
? 'Healthy'
: $status.application.overallStatus === 'degraded'
? 'Degraded'
: 'Stopped'}
@@ -242,14 +244,14 @@
{/if}
</div>
{#if $page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
<div class="px-2">
<div class="px-4">
{#if forceDelete}
<button
on:click={() => deleteApplication(application.name, true)}
disabled={!$appSession.isAdmin}
class:bg-red-600={$appSession.isAdmin}
class:hover:bg-red-500={$appSession.isAdmin}
class="btn btn-sm btn-error text-sm"
class="btn btn-sm btn-error hover:bg-red-700 text-sm w-64"
>
Force Delete Application
</button>
@@ -259,7 +261,7 @@
disabled={!$appSession.isAdmin}
class:bg-red-600={$appSession.isAdmin}
class:hover:bg-red-500={$appSession.isAdmin}
class="btn btn-sm btn-error text-sm"
class="btn btn-sm btn-error hover:bg-red-700 text-sm w-64"
>
Delete Application
</button>
@@ -296,7 +298,29 @@
Application Error
</a>
{/if}
{#if $status.application.initialLoading}
{#if stopping}
<button class="btn btn-ghost btn-sm gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 animate-spin duration-500 ease-in-out"
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>
Stopping...
</button>
{:else if $status.application.initialLoading}
<button class="btn btn-ghost btn-sm gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -319,27 +343,6 @@
Loading...
</button>
{:else if $status.application.overallStatus === 'healthy'}
<button
on:click={stopApplication}
type="submit"
disabled={!$isDeploymentEnabled}
class="btn btn-sm btn-error gap-2"
>
<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> Stop
</button>
{#if application.buildPack !== 'compose'}
<button
on:click={restartApplication}
@@ -387,17 +390,38 @@
Force Redeploy
</button>
<button
on:click={stopApplication}
type="submit"
disabled={!$isDeploymentEnabled}
class="btn btn-sm gap-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-error"
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> Stop
</button>
{:else if $isDeploymentEnabled && !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
{#if $status.application.overallStatus === 'degraded'}
<button
on:click={stopApplication}
type="submit"
disabled={!$isDeploymentEnabled}
class="btn btn-sm btn-error gap-2"
class="btn btn-sm gap-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 "
class="w-6 h-6 text-error"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
@@ -413,14 +437,13 @@
{/if}
<button
class="btn btn-sm gap-2"
class:btn-primary={$status.application.overallStatus !== 'degraded'}
disabled={!$isDeploymentEnabled}
on:click={() => handleDeploySubmit(false)}
>
{#if $status.application.overallStatus !== 'degraded'}
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
class="w-6 h-6 text-pink-500"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
@@ -457,7 +480,7 @@
</button>
{/if}
{#if $location && $status.application.overallStatus === 'healthy'}
<a href={$location} target="_blank" class="btn btn-sm gap-2 text-sm bg-primary"
<a href={$location} target="_blank noreferrer" class="btn btn-sm gap-2 text-sm bg-primary"
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
@@ -478,7 +501,7 @@
</div>
</div>
<div
class="mx-auto max-w-screen-2xl px-0 lg:px-2 grid grid-cols-1"
class="mx-auto max-w-screen-2xl px-0 lg:px-10 grid grid-cols-1"
class:lg:grid-cols-4={!$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
>
{#if !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}

View File

@@ -92,27 +92,9 @@
label: branch.name
}));
}
async function isBranchAlreadyUsed(event: any) {
async function selectBranch(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);
}
showSave = true;
}
onMount(async () => {
@@ -178,7 +160,7 @@
isWaiting={loading.branches}
showIndicator={selected.repository && !loading.branches}
id="branches"
on:select={isBranchAlreadyUsed}
on:select={selectBranch}
items={branchSelectOptions}
isDisabled={loading.branches || !selected.repository}
isClearable={false}
@@ -186,10 +168,9 @@
</div></div>
<div class="pt-5 flex-col flex justify-center items-center space-y-4">
<button
class="btn btn-wide"
class="btn btn-wide btn-primary"
type="submit"
disabled={!showSave}
class:bg-applications={showSave}
>{$t('forms.save')}</button
>
</div>

View File

@@ -195,27 +195,11 @@
}
}
async function isBranchAlreadyUsed(event) {
async function selectBranch(event: any) {
selected.branch = event.detail;
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);
}
showSave = true;
}
async function checkSSHKey(sshkeyUrl: any) {
try {
return await post(sshkeyUrl, {});
@@ -394,7 +378,7 @@
showIndicator={!loading.branches}
isWaiting={loading.branches}
isDisabled={loading.branches || !selected.project}
on:select={isBranchAlreadyUsed}
on:select={selectBranch}
on:clear={() => {
showSave = false;
selected.branch = null;
@@ -414,7 +398,6 @@
class="btn btn-wide"
type="submit"
disabled={!showSave || loading.save}
class:bg-applications={showSave && !loading.save}
>{loading.save ? $t('forms.saving') : $t('forms.save')}</button
>
{#if tryAgain}
@@ -423,7 +406,7 @@
configuration <a href={`/sources/${application.gitSource.id}`}>here.</a>
</div>
<button
class="btn btn-sm w-40 bg-green-600"
class="btn btn-sm w-40 btn-primary"
on:click|stopPropagation|preventDefault={() => window.location.reload()}
>
Try again

View File

@@ -39,6 +39,9 @@
if (branch[0] === 'tree' && branch[1]) {
branchName = branch[1];
}
if (branch.length === 1) {
branchName = branch[0]
}
}
if (host === 'gitlab.com') {
host = 'gitlab.com/api/v4';
@@ -46,6 +49,9 @@
if (branch[1] === 'tree' && branch[2]) {
branchName = branch[2];
}
if (branch.length === 1) {
branchName = branch[0]
}
}
const apiUrl = `${protocol}://${host}`;
if (type === 'github') {
@@ -165,7 +171,7 @@
placeholder="eg: https://github.com/coollabsio/nodejs-example/tree/main"
bind:value={publicRepositoryLink}
/>
<button class="btn bg-orange-600" type="submit">
<button class="btn btn-primary" disabled={loading.branches} type="submit" class:loading={loading.branches}>
Load Repository
</button>
</div>

View File

@@ -70,7 +70,11 @@
{$t('application.configuration.no_configurable_destination')}
</div>
<div class="flex justify-center">
<a href="/destinations/new" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
<a
href={`/destinations/new?from=/applications/${id}/configuration/destination`}
sveltekit:prefetch
class="add-icon bg-sky-600 hover:bg-sky-500"
>
<svg
class="w-6"
xmlns="http://www.w3.org/2000/svg"
@@ -88,31 +92,111 @@
</div>
</div>
{:else}
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row mx-auto">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row mx-auto gap-4">
{#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>
<button
on:click={() => handleSubmit(destination.id)}
class="box-selection hover:bg-primary font-bold relative"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="absolute top-0 left-0 -m-4 h-12 w-12 text-sky-500"
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>
{#if destination.remoteEngine}
<svg
xmlns="http://www.w3.org/2000/svg"
class="absolute top-0 left-9 -m-2 h-6 w-6 text-sky-500 rotate-45"
viewBox="0 0 24 24"
stroke-width="3"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<line x1="12" y1="18" x2="12.01" y2="18" />
<path d="M9.172 15.172a4 4 0 0 1 5.656 0" />
<path d="M6.343 12.343a8 8 0 0 1 11.314 0" />
<path d="M3.515 9.515c4.686 -4.687 12.284 -4.687 17 0" />
</svg>
{/if}
<div class="font-bold text-xl text-center truncate">{destination.name}</div>
<div class="text-center truncate">{destination.network}</div>
</button>
{/each}
</div>
{#if otherDestinations.length > 0 && $appSession.teamId === '0'}
<div class="px-6 pb-5 pt-10 title">Other Destinations</div>
{/if}
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row mx-auto">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row mx-auto gap-4">
{#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>
<button
class="box-selection hover:bg-sky-700 font-bold relative"
on:click={() => handleSubmit(destination.id)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="absolute top-0 left-0 -m-4 h-12 w-12 text-sky-500"
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>
{#if destination.remoteEngine}
<svg
xmlns="http://www.w3.org/2000/svg"
class="absolute top-0 left-9 -m-2 h-6 w-6 text-sky-500 rotate-45"
viewBox="0 0 24 24"
stroke-width="3"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<line x1="12" y1="18" x2="12.01" y2="18" />
<path d="M9.172 15.172a4 4 0 0 1 5.656 0" />
<path d="M6.343 12.343a8 8 0 0 1 11.314 0" />
<path d="M3.515 9.515c4.686 -4.687 12.284 -4.687 17 0" />
</svg>
{/if}
<div class="font-bold text-xl text-center truncate">{destination.name}</div>
<div class="text-center truncate">{destination.network}</div>
</button>
{/each}
</div>
{/if}

View File

@@ -0,0 +1,118 @@
<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(`/settings`);
return {
props: {
...response
}
};
} catch (error) {
return {
status: 500,
error: new Error(`Could not load ${url}`)
};
}
};
</script>
<script lang="ts">
export let registries: any;
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { get, post } from '$lib/api';
import { errorNotification } from '$lib/common';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
async function handleSubmit(registryId: any) {
try {
await post(`/applications/${id}/configuration/registry`, { registryId });
return await goto(from || `/applications/${id}`);
} catch (error) {
return errorNotification(error);
}
}
</script>
<div class="flex flex-col justify-center w-full">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row mx-auto gap-4">
{#each registries.public as registry}
<button
on:click={() => handleSubmit(registry.id)}
class="box-selection hover:bg-primary relative"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="absolute top-0 left-0 -m-4 h-12 w-12 text-sky-500"
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>
<div class="font-bold text-xl text-center truncate">{registry.name}</div>
<div class="text-center truncate">{registry.url}</div>
<div>public</div>
</button>
{/each}
{#each registries.private as registry}
<button
on:click={() => handleSubmit(registry.id)}
class="box-selection hover:bg-primary relative"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="absolute top-0 left-0 -m-4 h-12 w-12 text-sky-500"
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>
<div class="font-bold text-xl text-center truncate">{registry.name}</div>
<div class="text-center truncate">{registry.url}</div>
<div>private</div>
</button>
{/each}
</div>
</div>

View File

@@ -68,7 +68,9 @@
</script>
<div class="max-w-screen-2xl mx-auto px-9">
<div class="title pb-8">Git App</div>
{#if !filteredSources}
<div class="title pb-8">Git App</div>
{/if}
<div class="flex flex-wrap justify-center">
{#if !filteredSources}
<div class="flex-col">
@@ -76,10 +78,7 @@
{$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"
>
<a href="/sources/new?from={$page.url.pathname}" class="add-icon">
<svg
class="w-6"
xmlns="http://www.w3.org/2000/svg"
@@ -141,7 +140,7 @@
<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 w-full lg:w-96"
class="disabled:opacity-95 disabled:text-white box-selection hover:btn-primary group w-full lg:w-96"
class:border-red-500={source.gitlabApp && !source.gitlabAppId}
class:border-0={source.gitlabApp && !source.gitlabAppId}
class:border-l-4={source.gitlabApp && !source.gitlabAppId}
@@ -244,7 +243,7 @@
{/if}
</div>
<div class="flex flex-row items-center">
<div class="title py-4">Public Repository</div>
<div class="title py-4 pr-4">Public Repository</div>
<DocLink url="https://docs.coollabs.io/coolify/applications/#public-repository" />
</div>
<PublicRepository />

View File

@@ -61,7 +61,7 @@
disabled={!$appSession.isAdmin}
class:bg-red-600={$appSession.isAdmin}
class:hover:bg-red-500={$appSession.isAdmin}
class="btn btn-sm btn-error text-sm"
class="btn btn-lg btn-error hover:bg-red-700 text-sm w-64"
>
Force Delete Application
</button>
@@ -71,7 +71,7 @@
on:click={() => deleteApplication(application.name, false)}
type="submit"
disabled={!$appSession.isAdmin}
class="btn btn-lg btn-error hover:bg-red-700 text-sm"
class="btn btn-lg btn-error hover:bg-red-700 text-sm w-64"
>
Delete Application
</button>

View File

@@ -44,7 +44,6 @@
const { id } = $page.params;
let debug = application.settings.debug;
let previews = application.settings.previews;
let dualCerts = application.settings.dualCerts;
let autodeploy = application.settings.autodeploy;
@@ -52,9 +51,6 @@
let isDBBranching = application.settings.isDBBranching;
async function changeSettings(name: any) {
if (name === 'debug') {
debug = !debug;
}
if (name === 'previews') {
previews = !previews;
}
@@ -77,7 +73,6 @@
try {
await post(`/applications/${id}/settings`, {
previews,
debug,
dualCerts,
isBot,
autodeploy,
@@ -90,9 +85,6 @@
type: 'success'
});
} catch (error) {
if (name === 'debug') {
debug = !debug;
}
if (name === 'previews') {
previews = !previews;
}
@@ -132,29 +124,21 @@
description={$t('application.enable_auto_deploy_webhooks')}
/>
</div>
{#if !application.settings.isBot}
<div class="grid grid-cols-2 items-center">
<Setting
id="previews"
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>
{/if}
{:else}
No features available for this application
{/if}
{#if !application.settings.isBot && !application.settings.isPublicRepository}
<div class="grid grid-cols-2 items-center">
<Setting
id="previews"
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>
{/if}
<div class="grid grid-cols-2 items-center w-full">
<Setting
id="debug"
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>
</div>

View File

@@ -61,26 +61,29 @@
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
let statues: any = {};
let loading = false;
let loading = {
save: false,
reloadCompose: false
};
let fqdnEl: any = null;
let forceSave = false;
let isPublicRepository = application.settings.isPublicRepository;
let apiUrl = application.gitSource.apiUrl;
let isPublicRepository = application.settings?.isPublicRepository;
let apiUrl = application.gitSource?.apiUrl;
let branch = application.branch;
let repository = application.repository;
let debug = application.settings.debug;
let previews = application.settings.previews;
let dualCerts = application.settings.dualCerts;
let isCustomSSL = application.settings.isCustomSSL;
let autodeploy = application.settings.autodeploy;
let isBot = application.settings.isBot;
let isDBBranching = application.settings.isDBBranching;
let htmlUrl = application.gitSource.htmlUrl;
let debug = application.settings?.debug;
let previews = application.settings?.previews;
let dualCerts = application.settings?.dualCerts;
let isCustomSSL = application.settings?.isCustomSSL;
let autodeploy = application.settings?.autodeploy;
let isBot = application.settings?.isBot;
let isDBBranching = application.settings?.isDBBranching;
let htmlUrl = application.gitSource?.htmlUrl;
let dockerComposeFile = JSON.parse(application.dockerComposeFile) || null;
let dockerComposeServices: any[] = [];
let dockerComposeFileLocation = application.dockerComposeFileLocation;
let dockerComposeConfiguration = JSON.parse(application.dockerComposeConfiguration) || {};
let originalDockerComposeFileLocation = application.dockerComposeFileLocation;
let baseDatabaseBranch: any = application?.connectedDatabase?.hostedDatabaseDBName || null;
let nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
@@ -102,7 +105,6 @@
label: 'Uvicorn'
}
];
function normalizeDockerServices(services: any[]) {
const tempdockerComposeServices = [];
for (const [name, data] of Object.entries(services)) {
@@ -237,12 +239,16 @@
}
}
async function handleSubmit(toast: boolean = true) {
if (loading) return;
if (toast) loading = true;
if (loading.save) return;
if (toast) loading.save = true;
try {
nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
if (application.deploymentType)
if (application.deploymentType) {
application.deploymentType = application.deploymentType.toLowerCase();
}
if (originalDockerComposeFileLocation !== application.dockerComposeFileLocation) {
await reloadCompose();
}
if (!isBot) {
await post(`/applications/${id}/check`, {
fqdn: application.fqdn,
@@ -299,7 +305,7 @@
}
return errorNotification(error);
} finally {
loading = false;
loading.save = false;
}
}
async function selectWSGI(event: any) {
@@ -361,6 +367,11 @@
});
}
async function reloadCompose() {
if (loading.reloadCompose) return;
loading.reloadCompose = true;
const composeLocation = application.dockerComposeFileLocation.startsWith('/')
? application.dockerComposeFileLocation
: `/${application.dockerComposeFileLocation}`;
try {
if (application.gitSource.type === 'github') {
const headers = isPublicRepository
@@ -369,9 +380,10 @@
Authorization: `token ${$appSession.tokens.github}`
};
const data = await get(
`${apiUrl}/repos/${repository}/contents/${dockerComposeFileLocation}?ref=${branch}`,
`${apiUrl}/repos/${repository}/contents/${composeLocation}?ref=${branch}`,
{
...headers,
'If-None-Match': '',
Accept: 'application/vnd.github.v2.json'
}
);
@@ -401,7 +413,7 @@
});
const dockerComposeFileYml = files.find(
(file: { name: string; type: string }) =>
file.name === dockerComposeFileLocation && file.type === 'blob'
file.name === composeLocation && file.type === 'blob'
);
const id = dockerComposeFileYml.id;
@@ -420,13 +432,20 @@
await handleSubmit(false);
}
}
originalDockerComposeFileLocation = application.dockerComposeFileLocation;
addToast({
message: 'Compose file reloaded.',
type: 'success'
});
} catch (error) {
} catch (error: any) {
if (error.message === 'Not Found') {
error.message = `Can't find ${application.dockerComposeFileLocation} file.`;
errorNotification(error);
throw error;
}
errorNotification(error);
} finally {
loading.reloadCompose = false;
}
}
$: if ($status.application.statuses) {
@@ -459,15 +478,15 @@
<form on:submit|preventDefault={() => handleSubmit()}>
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3 ">General</div>
<div class="title font-bold pb-3">General</div>
{#if $appSession.isAdmin}
<button
class="btn btn-sm btn-primary"
type="submit"
class:loading
class:loading={loading.save}
class:bg-orange-600={forceSave}
class:hover:bg-orange-400={forceSave}
disabled={loading}>{$t('forms.save')}</button
disabled={loading.save}>{$t('forms.save')}</button
>
{/if}
</div>
@@ -482,14 +501,14 @@
<input
disabled={isDisabled || application.settings.isPublicRepository}
class="w-full"
value={application.gitSource.name}
value={application.gitSource?.name}
/>
{:else}
<a
href={`/applications/${id}/configuration/source?from=/applications/${id}`}
class="no-underline"
><input
value={application.gitSource.name}
value={application.gitSource?.name}
id="gitSource"
class="cursor-pointer hover:bg-coolgray-500 w-full"
/></a
@@ -533,6 +552,27 @@
>
{/if}
</div>
<div class="grid grid-cols-2 items-center">
<label for="registry">Docker Registry</label>
{#if isDisabled}
<input
class="capitalize w-full"
disabled={isDisabled}
value={application.dockerRegistry.name}
/>
{:else}
<a
href={`/applications/${id}/configuration/registry?from=/applications/${id}`}
class="no-underline"
>
<input
value={application.dockerRegistry.name}
id="registry"
class="cursor-pointer hover:bg-coolgray-500 capitalize w-full"
/></a
>
{/if}
</div>
<div class="grid grid-cols-2 items-center">
<label for="buildPack">{$t('application.build_pack')} </label>
{#if isDisabled}
@@ -586,13 +626,13 @@
<input
bind:this={fqdnEl}
class="w-full"
required={!application.settings.isBot}
required={!application.settings?.isBot}
readonly={isDisabled}
disabled={isDisabled}
name="fqdn"
id="fqdn"
class:border={!application.settings.isBot && !application.fqdn}
class:border-red-500={!application.settings.isBot && !application.fqdn}
class:border={!application.settings?.isBot && !application.fqdn}
class:border-red-500={!application.settings?.isBot && !application.fqdn}
bind:value={application.fqdn}
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="eg: https://coollabs.io"
@@ -661,7 +701,7 @@
</div>
{#if application.buildPack !== 'compose'}
<div class="title font-bold pb-3 pt-10 border-b border-coolgray-500 mb-6">
Build & Deploy
Configuration
</div>
<div class="grid grid-flow-row gap-2 px-4 pr-5">
{#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'}
@@ -731,7 +771,7 @@
</div>
{/if}
{#if $features.beta}
{#if !application.settings.isBot && !application.settings.isPublicRepository}
{#if !application.settings?.isBot && !application.settings?.isPublicRepository}
<div class="grid grid-cols-2 items-center">
<Setting
id="isDBBranching"
@@ -855,8 +895,8 @@
>
<input
class="w-full"
readonly={!isDisabled}
disabled={isDisabled}
readonly={!$appSession.isAdmin}
name="exposePort"
id="exposePort"
bind:value={application.exposePort}
@@ -1010,12 +1050,34 @@
<div class="title font-bold pb-3 pt-10 border-b border-coolgray-500 mb-6">
Stack <Beta />
{#if $appSession.isAdmin}
<button class="btn btn-sm btn-primary" on:click|preventDefault={reloadCompose}
>Reload Docker Compose File</button
<button
class="btn btn-sm btn-primary"
class:loading={loading.reloadCompose}
disabled={loading.reloadCompose}
on:click|preventDefault={reloadCompose}>Reload Docker Compose File</button
>
{/if}
</div>
<div class="grid grid-flow-row gap-2">
<div class="grid grid-cols-2 items-center px-8 pb-4">
<label for="dockerComposeFileLocation"
>Docker Compose File Location
<Explainer
explanation="You can specify a custom docker compose file location. <br> Should be absolute path, like <span class='text-settings font-bold'>/data/docker-compose.yml</span> or <span class='text-settings font-bold'>/docker-compose.yml.</span>"
/>
</label>
<div>
<input
class="w-full"
disabled={isDisabled}
readonly={!$appSession.isAdmin}
name="dockerComposeFileLocation"
id="dockerComposeFileLocation"
bind:value={application.dockerComposeFileLocation}
placeholder="eg: /docker-compose.yml"
/>
</div>
</div>
{#each dockerComposeServices as service}
<div
class="grid items-center bg-coolgray-100 rounded border border-coolgray-300 p-2 px-4"

View File

@@ -61,12 +61,27 @@
fromDb = from;
streamInterval = setInterval(async () => {
const nextSequence = logs[logs.length - 1]?.time || 0;
if (status !== 'running' && status !== 'queued') {
loading = false;
try {
const data = await get(
`/applications/${id}/logs/build/${$selectedBuildId}?sequence=${nextSequence}`
);
status = data.status;
currentStatus = status;
fromDb = data.fromDb;
logs = logs.concat(
data.logs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) }))
);
loading = false;
} catch (error) {
return errorNotification(error);
}
clearInterval(streamInterval);
return;
}
const nextSequence = logs[logs.length - 1]?.time || 0;
try {
const data = await get(
`/applications/${id}/logs/build/${$selectedBuildId}?sequence=${nextSequence}`

View File

@@ -32,7 +32,7 @@
import { day } from '$lib/dayjs';
import { onDestroy, onMount } from 'svelte';
const { id } = $page.params;
let debug = application.settings.debug;
let loadBuildLogsInterval: any = null;
let skip = 0;
@@ -104,42 +104,74 @@
return 'text-white';
}
}
async function changeSettings(name: any) {
if (name === 'debug') {
debug = !debug;
}
try {
await post(`/applications/${id}/settings`, {
debug,
branch: application.branch,
projectId: application.projectId
});
return addToast({
message: $t('application.settings_saved'),
type: 'success'
});
} catch (error) {
if (name === 'debug') {
debug = !debug;
}
return errorNotification(error);
}
}
</script>
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="mx-auto w-full lg:px-0 px-1">
<div class="flex lg:flex-row flex-col border-b border-coolgray-500 mb-6 space-x-2">
<div class="flex flex-row">
<div class="title font-bold pb-3 pr-3">Build Logs</div>
<button class="btn btn-sm bg-error" on:click={resetQueue}>Reset Build Queue</button>
</div>
<div class=" flex-1" />
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text text-white pr-4 font-bold">Enable Debug Logs</span>
<input
type="checkbox"
checked={debug}
class="checkbox checkbox-success"
on:click={() => changeSettings('debug')}
/>
</label>
</div>
</div>
</div>
<div class="block flex-col justify-start space-x-5 flex flex-col-reverse lg:flex-row">
<div class="justify-start space-x-5 flex flex-col-reverse lg:flex-row">
<div class="flex-1 md:w-96">
{#if $selectedBuildId}
{#key $selectedBuildId}
<svelte:component this={BuildLog} />
{/key}
{:else if buildCount === 0}
Not build logs found.
{:else}
{#if buildCount === 0}
Not build logs found.
{:else}
Select a build to see the logs.
{/if}
{/if}
</div>
<div class="mb-4 min-w-[16rem] space-y-2 md:mb-0 ">
<div class="top-4 md:sticky">
<div class="flex space-x-2 pb-2">
<button
disabled={noMoreBuilds}
class:btn-primary={!noMoreBuilds}
class=" btn btn-sm w-full"
on:click={loadMoreBuilds}>{$t('application.build.load_more')}</button
>
</div>
<div class="flex space-x-2 pb-2">
<button
disabled={noMoreBuilds}
class:btn-primary={!noMoreBuilds}
class=" btn btn-sm w-full"
on:click={loadMoreBuilds}>{$t('application.build.load_more')}</button
>
</div>
{#each builds as build, index (build.id)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
id={`building-${build.id}`}
on:click={() => loadBuild(build.id)}
@@ -187,4 +219,4 @@
{/each}
</div>
</div>
</div>
</div>

View File

@@ -1,10 +1,8 @@
<script lang="ts">
import { page } from '$app/stores';
import { get } from '$lib/api';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
import { onMount, onDestroy } from 'svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
let application: any = {};
let logsLoading = false;
@@ -137,12 +135,7 @@
{:else}
<div class="relative w-full">
<div class="flex justify-start sticky space-x-2 pb-2">
<button
on:click={followBuild}
class="btn btn-sm bg-coollabs"
class:bg-coolgray-300={followingLogs}
class:text-applications={followingLogs}
>
<button on:click={followBuild} class="btn btn-sm " class:bg-coollabs={followingLogs}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 mr-2"
@@ -162,8 +155,9 @@
{followingLogs ? 'Following Logs...' : 'Follow Logs'}
</button>
{#if loadLogsInterval}
<button id="streaming" class="btn btn-sm bg-transparent border-none loading" />
<Tooltip triggeredBy="#streaming">Streaming logs</Tooltip>
<button id="streaming" class="btn btn-sm bg-transparent border-none loading"
>Streaming logs</button
>
{/if}
</div>
<div

View File

@@ -216,7 +216,7 @@
<div class="flex justify-end items-end space-x-2 h-10">
{#if preview.customDomain}
<a id="openpreview" href={preview.customDomain} target="_blank" class="icons">
<a id="openpreview" href={preview.customDomain} target="_blank noreferrer" class="icons">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"

View File

@@ -48,10 +48,10 @@
.map((secret) => {
const [name, ...rest] = secret.split('=');
const value = rest.join('=');
const cleanValue = value?.replaceAll('"', '') || '';
const cleanValue = (value?.replaceAll('"', '') || '').trim();
return {
name,
value: cleanValue,
name: name.trim(),
value: cleanValue.trim(),
createSecret: !secrets.find((secret: any) => name === secret.name)
};
});
@@ -60,6 +60,7 @@
batchSecretsPairs.map(({ name, value, createSecret }) =>
limit(async () => {
try {
if (!name || !value) return;
if (createSecret) {
await post(`/applications/${id}/secrets`, {
name,
@@ -87,10 +88,6 @@
);
batchSecrets = '';
await refreshSecrets();
addToast({
message: 'Secrets saved.',
type: 'success'
});
}
</script>

View File

@@ -2,9 +2,11 @@
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ params, stuff, url }) => {
try {
const { application } = stuff;
const response = await get(`/applications/${params.id}/storages`);
return {
props: {
application,
...response
}
};
@@ -19,12 +21,31 @@
<script lang="ts">
export let persistentStorages: any;
export let application: any;
import { page } from '$app/stores';
import Storage from './_Storage.svelte';
import { get } from '$lib/api';
import { t } from '$lib/translations';
import Explainer from '$lib/components/Explainer.svelte';
let composeJson = JSON.parse(application?.dockerComposeFile || '{}');
let predefinedVolumes: any[] = [];
if (composeJson?.services) {
for (const [_, service] of Object.entries(composeJson.services)) {
if (service?.volumes) {
for (const [_, volumeName] of Object.entries(service.volumes)) {
let [volume, target] = volumeName.split(':');
if (!target) {
target = volume;
volume = `${application.id}${volume.replace(/\//gi, '-').replace(/\./gi, '')}`;
} else {
volume = `${application.id}${volume.replace(/\//gi, '-').replace(/\./gi, '')}`;
}
predefinedVolumes.push({ id: volume, path: target, predefined: true });
}
}
}
}
const { id } = $page.params;
async function refreshStorage() {
const data = await get(`/applications/${id}/storages`);
@@ -34,20 +55,40 @@
<div class="w-full">
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3">
Persistent Volumes <Explainer
position="dropdown-bottom"
explanation={$t('application.storage.persistent_storage_explainer')}
/>
</div>
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3">Persistent Volumes</div>
</div>
<label for="name" class="pb-2 uppercase font-bold">name</label>
{#if predefinedVolumes.length > 0}
<div class="title">Predefined Volumes</div>
<div class="w-full lg:px-0 px-4">
<div class="grid grid-col-1 lg:grid-cols-2 py-2 gap-2">
<div class="font-bold uppercase">Volume Id</div>
<div class="font-bold uppercase">Mount Dir</div>
</div>
</div>
<div class="gap-4">
{#each predefinedVolumes as storage}
{#key storage.id}
<Storage on:refresh={refreshStorage} {storage} />
{/key}
{/each}
</div>
{/if}
{#if persistentStorages.length > 0}
<div class="title" class:pt-10={predefinedVolumes.length > 0}>Custom Volumes</div>
{/if}
{#each persistentStorages as storage}
{#key storage.id}
<Storage on:refresh={refreshStorage} {storage} />
{/key}
{/each}
<div class="title pt-10">
Add New Volume <Explainer
position="dropdown-bottom"
explanation={$t('application.storage.persistent_storage_explainer')}
/>
</div>
<Storage on:refresh={refreshStorage} isNew />
</div>
</div>

View File

@@ -2,13 +2,14 @@
export let payload: any;
import { goto } from '$app/navigation';
import { page } from '$app/stores';
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';
const from = $page.url.searchParams.get('from');
let loading = false;
async function handleSubmit() {
@@ -19,8 +20,7 @@
const { id } = await post(`/destinations/new`, {
...payload
});
await goto(`/destinations/${id}`);
window.location.reload();
return await goto(from || `/destinations/${id}`);
} catch (error) {
return errorNotification(error);
} finally {
@@ -31,9 +31,15 @@
<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-start lg:items-center space-x-0 lg:space-x-4 pb-5 flex-col space-y-4 lg:space-y-0">
<div
class="flex items-start lg:items-center space-x-0 lg:space-x-4 pb-5 flex-col lg:flex-row space-y-4 lg:space-y-0"
>
<div class="title font-bold">{$t('forms.configuration')}</div>
<button type="submit" class="btn btn-sm bg-destinations w-full lg:w-fit" class:loading disabled={loading}
<button
type="submit"
class="btn btn-sm bg-destinations w-full lg:w-fit"
class:loading
disabled={loading}
>{loading
? payload.isCoolifyProxyUsed
? $t('destination.new.saving_and_configuring_proxy')

View File

@@ -2,13 +2,14 @@
export let payload: any;
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { post } from '$lib/api';
import { errorNotification } from '$lib/common';
import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
import Setting from '$lib/components/Setting.svelte';
import { t } from '$lib/translations';
const from = $page.url.searchParams.get('from');
let loading = false;
async function handleSubmit() {
@@ -19,7 +20,7 @@
const { id } = await post(`/destinations/new`, {
...payload
});
return await goto(`/destinations/${id}`);
return await goto(from || `/destinations/${id}`);
} catch (error) {
return errorNotification(error);
} finally {

View File

@@ -184,14 +184,16 @@
? 'Verify Remote Docker Engine'
: 'Check Remote Docker Engine'}</button
>
<button
class="btn btn-sm"
class:loading={loading.restart}
class:bg-error={!loading.restart}
disabled={loading.restart}
on:click|preventDefault={forceRestartProxy}>{$t('destination.force_restart_proxy')}</button
>
{#if destination.remoteVerified}
<button
class="btn btn-sm"
class:loading={loading.restart}
class:bg-error={!loading.restart}
disabled={loading.restart}
on:click|preventDefault={forceRestartProxy}
>{$t('destination.force_restart_proxy')}</button
>
{/if}
{/if}
</div>
<div class="grid grid-cols-2 items-center px-10 ">

View File

@@ -29,7 +29,7 @@
export let settings: any;
export let gitSources: any;
export let destinations: any;
let filtered: any = setInitials();
import { get, post } from '$lib/api';
import { t } from '$lib/translations';
@@ -41,6 +41,7 @@
import ServiceIcons from '$lib/components/svg/services/ServiceIcons.svelte';
import { dev } from '$app/env';
import NewResource from './_NewResource.svelte';
import { onMount } from 'svelte';
let numberOfGetStatus = 0;
let status: any = {};
@@ -54,8 +55,13 @@
services: false,
databases: false
};
let searchInput: HTMLInputElement;
doSearch();
onMount(() => {
setTimeout(() => {
searchInput.focus();
}, 100);
});
async function refreshStatusApplications() {
noInitialStatus.applications = false;
numberOfGetStatus = 0;
@@ -84,31 +90,50 @@
return {
applications:
!onlyOthers &&
applications.filter((application: any) => application.teams[0].id === $appSession.teamId),
applications.filter(
(application: any) =>
application?.teams.length > 0 && application.teams[0].id === $appSession.teamId
),
otherApplications: applications.filter(
(application: any) => application.teams[0].id !== $appSession.teamId
(application: any) =>
application?.teams.length > 0 && application.teams[0].id !== $appSession.teamId
),
databases:
!onlyOthers &&
databases.filter((database: any) => database.teams[0].id === $appSession.teamId),
databases.filter(
(database: any) =>
database?.teams.length > 0 && database.teams[0].id === $appSession.teamId
),
otherDatabases: databases.filter(
(database: any) => database.teams[0].id !== $appSession.teamId
(database: any) => database?.teams.length > 0 && database.teams[0].id !== $appSession.teamId
),
services:
!onlyOthers &&
services.filter((service: any) => service.teams[0].id === $appSession.teamId),
otherServices: services.filter((service: any) => service.teams[0].id !== $appSession.teamId),
services.filter(
(service: any) => service?.teams.length > 0 && service.teams[0].id === $appSession.teamId
),
otherServices: services.filter(
(service: any) => service?.teams.length > 0 && service.teams[0].id !== $appSession.teamId
),
gitSources:
!onlyOthers &&
gitSources.filter((gitSource: any) => gitSource.teams[0].id === $appSession.teamId),
gitSources.filter(
(gitSource: any) =>
gitSource?.teams.length > 0 && gitSource.teams[0].id === $appSession.teamId
),
otherGitSources: gitSources.filter(
(gitSource: any) => gitSource.teams[0].id !== $appSession.teamId
(gitSource: any) =>
gitSource?.teams.length > 0 && gitSource.teams[0].id !== $appSession.teamId
),
destinations:
!onlyOthers &&
destinations.filter((destination: any) => destination.teams[0].id === $appSession.teamId),
destinations.filter(
(destination: any) =>
destination?.teams.length > 0 && destination.teams[0].id === $appSession.teamId
),
otherDestinations: destinations.filter(
(destination: any) => destination.teams[0].id !== $appSession.teamId
(destination: any) =>
destination?.teams.length > 0 && destination.teams[0].id !== $appSession.teamId
)
};
}
@@ -171,7 +196,24 @@
}
} else if (typeof dualCerts !== 'undefined') {
const response = await get(`/services/${id}/status`);
isRunning = response.isRunning;
if (Object.keys(response).length === 0) {
isRunning = false;
} else {
let overallStatus = false;
for (const oneStatus of Object.keys(response)) {
if (response[oneStatus].status.isRunning) {
overallStatus = true;
} else {
isDegraded = true;
break;
}
}
if (overallStatus) {
isRunning = true;
} else {
isRunning = false;
}
}
} else {
const response = await get(`/databases/${id}/status`);
isRunning = response.isRunning;
@@ -237,7 +279,8 @@
(application.id && application.id.toLowerCase().includes($search.toLowerCase())) ||
(application.name && application.name.toLowerCase().includes($search.toLowerCase())) ||
(application.fqdn && application.fqdn.toLowerCase().includes($search.toLowerCase())) ||
(application.dockerComposeConfiguration && application.dockerComposeConfiguration.toLowerCase().includes($search.toLowerCase())) ||
(application.dockerComposeConfiguration &&
application.dockerComposeConfiguration.toLowerCase().includes($search.toLowerCase())) ||
(application.repository &&
application.repository.toLowerCase().includes($search.toLowerCase())) ||
(application.buildpack &&
@@ -523,6 +566,7 @@
</div>
<div class="form-control">
<div class="input-group flex w-full">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="btn btn-square cursor-default no-animation hover:bg-error"
on:click={() => doSearch('')}
@@ -544,6 +588,7 @@
</div>
<input
bind:this={searchInput}
id="search"
type="text"
placeholder="Search: You can search for names, domains, types, database types, version, servers etc..."
@@ -643,7 +688,7 @@
<div class="h-10 text-xs">
{#if application?.fqdn}
<h2>{application?.fqdn.replace('https://', '').replace('http://', '')}</h2>
{:else if (!application.settings?.isBot && !application?.fqdn) && application.buildPack !== 'compose'}
{:else if !application.settings?.isBot && !application?.fqdn && application.buildPack !== 'compose'}
<h2 class="text-red-500">Not configured</h2>
{/if}
{#if application.destinationDocker?.name}
@@ -656,7 +701,11 @@
<div class="flex justify-end items-end space-x-2 h-10">
{#if application?.fqdn}
<a href={application?.fqdn} target="_blank" class="icons hover:bg-green-500">
<a
href={application?.fqdn}
target="_blank noreferrer"
class="icons hover:bg-green-500"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
@@ -680,7 +729,7 @@
href={`http://${dev ? 'localhost' : settings.ipv4}:${
application.exposePort
}`}
target="_blank"
target="_blank noreferrer"
class="icons hover:bg-green-500"
>
<svg
@@ -762,7 +811,11 @@
<div class="flex justify-end items-end space-x-2 h-10">
{#if application?.fqdn}
<a href={application?.fqdn} target="_blank" class="icons hover:bg-green-500">
<a
href={application?.fqdn}
target="_blank noreferrer"
class="icons hover:bg-green-500"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
@@ -784,7 +837,7 @@
{#if application.settings?.isBot && application.exposePort}
<a
href={`http://${dev ? 'localhost' : settings.ipv4}:${application.exposePort}`}
target="_blank"
target="_blank noreferrer"
class="icons hover:bg-green-500"
>
<svg
@@ -835,6 +888,88 @@
>
{#if filtered.services.length > 0}
{#each filtered.services as service}
{#key service.id}
<a class="no-underline mb-5" href={`/services/${service.id}`}>
<div
class="w-full rounded p-5 bg-coolgray-200 hover:bg-pink-600 indicator duration-150"
>
{#await getStatus(service)}
<span class="indicator-item badge bg-yellow-300 badge-sm" />
{:then}
{#if !noInitialStatus.services}
{#if status[service.id] === 'loading'}
<span class="indicator-item badge bg-yellow-300 badge-sm" />
{:else if status[service.id] === 'running'}
<span class="indicator-item badge bg-success badge-sm" />
{:else}
<span class="indicator-item badge bg-error badge-sm" />
{/if}
{/if}
{/await}
<div class="w-full flex flex-row">
<ServiceIcons type={service.type} isAbsolute={true} />
<div class="w-full flex flex-col">
<h1 class="font-bold text-base truncate">{service.name}</h1>
<div class="h-10 text-xs">
{#if service?.fqdn}
<h2>{service?.fqdn.replace('https://', '').replace('http://', '')}</h2>
{:else}
<h2 class="text-red-500">URL not configured</h2>
{/if}
{#if service.destinationDocker?.name}
<div class="truncate">{service.destinationDocker?.name}</div>
{/if}
{#if service.teams.length > 0 && service.teams[0]?.name}
<div class="truncate">{service.teams[0]?.name}</div>
{/if}
</div>
<div class="flex justify-end items-end space-x-2 h-10">
{#if service?.fqdn}
<a
href={service?.fqdn}
target="_blank noreferrer"
class="icons hover:bg-pink-500"
>
<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>
</div>
</div>
</a>
{/key}
{/each}
{:else}
<h1 class="">Nothing here.</h1>
{/if}
</div>
{/if}
{#if filtered.otherServices.length > 0}
{#if filtered.services.length > 0}
<div class="divider w-32 mx-auto" />
{/if}
{/if}
{#if filtered.otherServices.length > 0}
<div
class="grid grid-col gap-8 auto-cols-max grid-cols-1 md:grid-cols-2 lg:md:grid-cols-3 xl:grid-cols-4 p-4"
>
{#each filtered.otherServices as service}
{#key service.id}
<a class="no-underline mb-5" href={`/services/${service.id}`}>
<div
class="w-full rounded p-5 bg-coolgray-200 hover:bg-pink-600 indicator duration-150"
@@ -871,7 +1006,11 @@
</div>
<div class="flex justify-end items-end space-x-2 h-10">
{#if service?.fqdn}
<a href={service?.fqdn} target="_blank" class="icons hover:bg-pink-500">
<a
href={service?.fqdn}
target="_blank noreferrer"
class="icons hover:bg-pink-500"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
@@ -894,79 +1033,7 @@
</div>
</div>
</a>
{/each}
{:else}
<h1 class="">Nothing here.</h1>
{/if}
</div>
{/if}
{#if filtered.otherServices.length > 0}
{#if filtered.services.length > 0}
<div class="divider w-32 mx-auto" />
{/if}
{/if}
{#if filtered.otherServices.length > 0}
<div
class="grid grid-col gap-8 auto-cols-max grid-cols-1 md:grid-cols-2 lg:md:grid-cols-3 xl:grid-cols-4 p-4"
>
{#each filtered.otherServices as service}
<a class="no-underline mb-5" href={`/services/${service.id}`}>
<div class="w-full rounded p-5 bg-coolgray-200 hover:bg-pink-600 indicator duration-150">
{#await getStatus(service)}
<span class="indicator-item badge bg-yellow-300 badge-sm" />
{:then}
{#if !noInitialStatus.services}
{#if status[service.id] === 'loading'}
<span class="indicator-item badge bg-yellow-300 badge-sm" />
{:else if status[service.id] === 'running'}
<span class="indicator-item badge bg-success badge-sm" />
{:else}
<span class="indicator-item badge bg-error badge-sm" />
{/if}
{/if}
{/await}
<div class="w-full flex flex-row">
<ServiceIcons type={service.type} isAbsolute={true} />
<div class="w-full flex flex-col">
<h1 class="font-bold text-base truncate">{service.name}</h1>
<div class="h-10 text-xs">
{#if service?.fqdn}
<h2>{service?.fqdn.replace('https://', '').replace('http://', '')}</h2>
{:else}
<h2 class="text-red-500">URL not configured</h2>
{/if}
{#if service.destinationDocker?.name}
<div class="truncate">{service.destinationDocker?.name}</div>
{/if}
{#if service.teams.length > 0 && service.teams[0]?.name}
<div class="truncate">{service.teams[0]?.name}</div>
{/if}
</div>
<div class="flex justify-end items-end space-x-2 h-10">
{#if service?.fqdn}
<a href={service?.fqdn} target="_blank" class="icons hover:bg-pink-500">
<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>
</div>
</div>
</a>
{/key}
{/each}
</div>
{/if}
@@ -993,6 +1060,88 @@
>
{#if filtered.databases.length > 0}
{#each filtered.databases as database}
{#key database.id}
<a class="no-underline mb-5" href={`/databases/${database.id}`}>
<div
class="w-full rounded p-5 bg-coolgray-200 hover:bg-databases indicator duration-150"
>
{#await getStatus(database)}
<span class="indicator-item badge bg-yellow-300 badge-sm" />
{:then}
{#if !noInitialStatus.databases}
{#if status[database.id] === 'loading'}
<span class="indicator-item badge bg-yellow-300 badge-sm" />
{:else if status[database.id] === 'running'}
<span class="indicator-item badge bg-success badge-sm" />
{:else}
<span class="indicator-item badge bg-error badge-sm" />
{/if}
{/if}
{/await}
<div class="w-full flex flex-row">
<DatabaseIcons type={database.type} isAbsolute={true} />
<div class="w-full flex flex-col">
<div class="h-10">
<h1 class="font-bold text-base truncate">{database.name}</h1>
<div class="h-10 text-xs">
{#if database?.version}
<h2 class="">{database?.version}</h2>
{:else}
<h2 class="text-red-500">Not version not configured</h2>
{/if}
{#if database.destinationDocker?.name}
<div class="truncate">{database.destinationDocker?.name}</div>
{/if}
{#if database.teams.length > 0 && database.teams[0]?.name}
<div class="truncate">{database.teams[0]?.name}</div>
{/if}
</div>
</div>
<div class="flex justify-end items-end space-x-2 h-10">
{#if database.settings?.isPublic}
<div title="Public">
<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="12" cy="12" r="9" />
<line x1="3.6" y1="9" x2="20.4" y2="9" />
<line x1="3.6" y1="15" x2="20.4" y2="15" />
<path d="M11.5 3a17 17 0 0 0 0 18" />
<path d="M12.5 3a17 17 0 0 1 0 18" />
</svg>
</div>
{/if}
</div>
</div>
</div>
</div>
</a>
{/key}
{/each}
{:else}
<h1 class="">Nothing here.</h1>
{/if}
</div>
{/if}
{#if filtered.otherDatabases.length > 0}
{#if filtered.databases.length > 0}
<div class="divider w-32 mx-auto" />
{/if}
{/if}
{#if filtered.otherDatabases.length > 0}
<div
class="grid grid-col gap-8 auto-cols-max grid-cols-1 md:grid-cols-2 lg:md:grid-cols-3 xl:grid-cols-4 p-4"
>
{#each filtered.otherDatabases as database}
{#key database.id}
<a class="no-underline mb-5" href={`/databases/${database.id}`}>
<div
class="w-full rounded p-5 bg-coolgray-200 hover:bg-databases indicator duration-150"
@@ -1056,83 +1205,7 @@
</div>
</div>
</a>
{/each}
{:else}
<h1 class="">Nothing here.</h1>
{/if}
</div>
{/if}
{#if filtered.otherDatabases.length > 0}
{#if filtered.databases.length > 0}
<div class="divider w-32 mx-auto" />
{/if}
{/if}
{#if filtered.otherDatabases.length > 0}
<div
class="grid grid-col gap-8 auto-cols-max grid-cols-1 md:grid-cols-2 lg:md:grid-cols-3 xl:grid-cols-4 p-4"
>
{#each filtered.otherDatabases as database}
<a class="no-underline mb-5" href={`/databases/${database.id}`}>
<div class="w-full rounded p-5 bg-coolgray-200 hover:bg-databases indicator duration-150">
{#await getStatus(database)}
<span class="indicator-item badge bg-yellow-300 badge-sm" />
{:then}
{#if !noInitialStatus.databases}
{#if status[database.id] === 'loading'}
<span class="indicator-item badge bg-yellow-300 badge-sm" />
{:else if status[database.id] === 'running'}
<span class="indicator-item badge bg-success badge-sm" />
{:else}
<span class="indicator-item badge bg-error badge-sm" />
{/if}
{/if}
{/await}
<div class="w-full flex flex-row">
<DatabaseIcons type={database.type} isAbsolute={true} />
<div class="w-full flex flex-col">
<div class="h-10">
<h1 class="font-bold text-base truncate">{database.name}</h1>
<div class="h-10 text-xs">
{#if database?.version}
<h2 class="">{database?.version}</h2>
{:else}
<h2 class="text-red-500">Not version not configured</h2>
{/if}
{#if database.destinationDocker?.name}
<div class="truncate">{database.destinationDocker?.name}</div>
{/if}
{#if database.teams.length > 0 && database.teams[0]?.name}
<div class="truncate">{database.teams[0]?.name}</div>
{/if}
</div>
</div>
<div class="flex justify-end items-end space-x-2 h-10">
{#if database.settings?.isPublic}
<div title="Public">
<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="12" cy="12" r="9" />
<line x1="3.6" y1="9" x2="20.4" y2="9" />
<line x1="3.6" y1="15" x2="20.4" y2="15" />
<path d="M11.5 3a17 17 0 0 0 0 18" />
<path d="M12.5 3a17 17 0 0 1 0 18" />
</svg>
</div>
{/if}
</div>
</div>
</div>
</div>
</a>
{/key}
{/each}
</div>
{/if}
@@ -1148,6 +1221,106 @@
>
{#if filtered.gitSources.length > 0}
{#each filtered.gitSources as source}
{#key source.id}
<a class="no-underline mb-5" href={`/sources/${source.id}`}>
<div
class="w-full rounded p-5 bg-coolgray-200 hover:bg-sources indicator duration-150"
>
<div class="w-full flex flex-row">
<div class="absolute top-0 left-0 -m-5 flex">
{#if source?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="h-10 w-10">
<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="h-10 w-10">
<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}
{#if source.isSystemWide}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-10 w-10"
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="3.6" y1="9" x2="20.4" y2="9" />
<line x1="3.6" y1="15" x2="20.4" y2="15" />
<path d="M11.5 3a17 17 0 0 0 0 18" />
<path d="M12.5 3a17 17 0 0 1 0 18" />
</svg>
{/if}
</div>
<div class="w-full flex flex-col">
<div class="h-10">
<h1 class="font-bold text-base truncate">{source.name}</h1>
{#if source.teams.length > 0 && source.teams[0]?.name}
<div class="truncate text-xs">{source.teams[0]?.name}</div>
{/if}
</div>
<div class="flex justify-end items-end space-x-2 h-10" />
</div>
</div>
</div>
</a>
{/key}
{/each}
{:else}
<h1 class="">Nothing here.</h1>
{/if}
</div>
{/if}
{#if filtered.otherGitSources.length > 0}
{#if filtered.gitSources.length > 0}
<div class="divider w-32 mx-auto" />
{/if}
{/if}
{#if filtered.otherGitSources.length > 0}
<div
class="grid grid-col gap-8 auto-cols-max grid-cols-1 md:grid-cols-2 lg:md:grid-cols-3 xl:grid-cols-4 p-4"
>
{#each filtered.otherGitSources as source}
{#key source.id}
<a class="no-underline mb-5" href={`/sources/${source.id}`}>
<div class="w-full rounded p-5 bg-coolgray-200 hover:bg-sources indicator duration-150">
<div class="w-full flex flex-row">
@@ -1215,100 +1388,12 @@
<div class="truncate text-xs">{source.teams[0]?.name}</div>
{/if}
</div>
<div class="flex justify-end items-end space-x-2 h-10" />
</div>
</div>
</div>
</a>
{/each}
{:else}
<h1 class="">Nothing here.</h1>
{/if}
</div>
{/if}
{#if filtered.otherGitSources.length > 0}
{#if filtered.gitSources.length > 0}
<div class="divider w-32 mx-auto" />
{/if}
{/if}
{#if filtered.otherGitSources.length > 0}
<div
class="grid grid-col gap-8 auto-cols-max grid-cols-1 md:grid-cols-2 lg:md:grid-cols-3 xl:grid-cols-4 p-4"
>
{#each filtered.otherGitSources as source}
<a class="no-underline mb-5" href={`/sources/${source.id}`}>
<div class="w-full rounded p-5 bg-coolgray-200 hover:bg-sources indicator duration-150">
<div class="w-full flex flex-row">
<div class="absolute top-0 left-0 -m-5 flex">
{#if source?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="h-10 w-10">
<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="h-10 w-10">
<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}
{#if source.isSystemWide}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-10 w-10"
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="3.6" y1="9" x2="20.4" y2="9" />
<line x1="3.6" y1="15" x2="20.4" y2="15" />
<path d="M11.5 3a17 17 0 0 0 0 18" />
<path d="M12.5 3a17 17 0 0 1 0 18" />
</svg>
{/if}
</div>
<div class="w-full flex flex-col">
<div class="h-10">
<h1 class="font-bold text-base truncate">{source.name}</h1>
{#if source.teams.length > 0 && source.teams[0]?.name}
<div class="truncate text-xs">{source.teams[0]?.name}</div>
{/if}
</div>
<div class="flex justify-end items-end space-x-2 h-10" />
</div>
</div>
</div>
</a>
{/key}
{/each}
</div>
{/if}
@@ -1324,6 +1409,90 @@
>
{#if filtered.destinations.length > 0}
{#each filtered.destinations as destination}
{#key destination.id}
<a class="no-underline mb-5" href={`/destinations/${destination.id}`}>
<div
class="w-full rounded p-5 bg-coolgray-200 hover:bg-destinations indicator duration-150"
>
<div class="w-full flex flex-row">
<div class="absolute top-0 left-0 -m-5 h-10 w-10">
<svg
xmlns="http://www.w3.org/2000/svg"
class="absolute top-0 left-0 -m-2 h-12 w-12 text-sky-500"
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>
{#if destination.remoteEngine}
<svg
xmlns="http://www.w3.org/2000/svg"
class="absolute top-0 left-9 -m-2 h-6 w-6 text-sky-500 rotate-45"
viewBox="0 0 24 24"
stroke-width="3"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<line x1="12" y1="18" x2="12.01" y2="18" />
<path d="M9.172 15.172a4 4 0 0 1 5.656 0" />
<path d="M6.343 12.343a8 8 0 0 1 11.314 0" />
<path d="M3.515 9.515c4.686 -4.687 12.284 -4.687 17 0" />
</svg>
{/if}
</div>
<div class="w-full flex flex-col">
<h1 class="font-bold text-base truncate">{destination.name}</h1>
<div class="h-10 text-xs">
{#if $appSession.teamId === '0' && destination.remoteVerified === false && destination.remoteEngine}
<h2 class="text-red-500">Not verified yet</h2>
{/if}
{#if destination.remoteEngine && !destination.sshKeyId}
<h2 class="text-red-500">SSH key missing</h2>
{/if}
{#if destination.teams.length > 0 && destination.teams[0]?.name}
<div class="truncate">{destination.teams[0]?.name}</div>
{/if}
</div>
</div>
</div>
</div>
</a>
{/key}
{/each}
{:else}
<h1 class="">Nothing here.</h1>
{/if}
</div>
{/if}
{#if filtered.otherDestinations.length > 0}
{#if filtered.destinations.length > 0}
<div class="divider w-32 mx-auto" />
{/if}
{/if}
{#if filtered.otherDestinations.length > 0}
<div
class="grid grid-col gap-8 auto-cols-max grid-cols-1 md:grid-cols-2 lg:md:grid-cols-3 xl:grid-cols-4 p-4"
>
{#each filtered.otherDestinations as destination}
{#key destination.id}
<a class="no-underline mb-5" href={`/destinations/${destination.id}`}>
<div
class="w-full rounded p-5 bg-coolgray-200 hover:bg-destinations indicator duration-150"
@@ -1389,87 +1558,7 @@
</div>
</div>
</a>
{/each}
{:else}
<h1 class="">Nothing here.</h1>
{/if}
</div>
{/if}
{#if filtered.otherDestinations.length > 0}
{#if filtered.destinations.length > 0}
<div class="divider w-32 mx-auto" />
{/if}
{/if}
{#if filtered.otherDestinations.length > 0}
<div
class="grid grid-col gap-8 auto-cols-max grid-cols-1 md:grid-cols-2 lg:md:grid-cols-3 xl:grid-cols-4 p-4"
>
{#each filtered.otherDestinations as destination}
<a class="no-underline mb-5" href={`/destinations/${destination.id}`}>
<div
class="w-full rounded p-5 bg-coolgray-200 hover:bg-destinations indicator duration-150"
>
<div class="w-full flex flex-row">
<div class="absolute top-0 left-0 -m-5 h-10 w-10">
<svg
xmlns="http://www.w3.org/2000/svg"
class="absolute top-0 left-0 -m-2 h-12 w-12 text-sky-500"
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>
{#if destination.remoteEngine}
<svg
xmlns="http://www.w3.org/2000/svg"
class="absolute top-0 left-9 -m-2 h-6 w-6 text-sky-500 rotate-45"
viewBox="0 0 24 24"
stroke-width="3"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<line x1="12" y1="18" x2="12.01" y2="18" />
<path d="M9.172 15.172a4 4 0 0 1 5.656 0" />
<path d="M6.343 12.343a8 8 0 0 1 11.314 0" />
<path d="M3.515 9.515c4.686 -4.687 12.284 -4.687 17 0" />
</svg>
{/if}
</div>
<div class="w-full flex flex-col">
<h1 class="font-bold text-base truncate">{destination.name}</h1>
<div class="h-10 text-xs">
{#if $appSession.teamId === '0' && destination.remoteVerified === false && destination.remoteEngine}
<h2 class="text-red-500">Not verified yet</h2>
{/if}
{#if destination.remoteEngine && !destination.sshKeyId}
<h2 class="text-red-500">SSH key missing</h2>
{/if}
{#if destination.teams.length > 0 && destination.teams[0]?.name}
<div class="truncate">{destination.teams[0]?.name}</div>
{/if}
</div>
</div>
</div>
</div>
</a>
{/key}
{/each}
</div>
{/if}

View File

@@ -5,11 +5,10 @@
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({}) => {
try {
const { servers } = await get('/servers');
const {servers} = await get('/servers');
const {destinations} = await get('/resources');
return {
props: {
servers
}
props: { servers, destinations }
};
} catch (error: any) {
return {
@@ -22,16 +21,25 @@
<script lang="ts">
export let servers: any;
export let destinations:any;
import { appSession } from '$lib/store';
import { goto } from '$app/navigation';
import ContextMenu from '$lib/components/ContextMenu.svelte';
import LocalDockerIcon from '$lib/components/svg/servers/LocalDockerIcon.svelte';
import RemoteDockerIcon from '$lib/components/svg/servers/RemoteDockerIcon.svelte';
import PublicBadge from '$lib/components/badges/PublicBadge.svelte';
import TeamsBadge from '$lib/components/badges/TeamsBadge.svelte';
import Grid3 from '$lib/components/grids/Grid3.svelte';
if ($appSession.teamId !== '0') {
goto('/');
}
</script>
<div class="header">
<h1 class="text-2xl font-bold">Servers</h1>
</div>
<ContextMenu>
<h1 class="title">Servers</h1>
</ContextMenu>
<div class="container lg:mx-auto lg:p-0 px-8 p-5">
{#if servers.length > 0}
<div class="grid grid-col gap-8 auto-cols-max grid-cols-1 p-4">
@@ -49,3 +57,45 @@
<h1 class="text-center text-xs">Nothing here.</h1>
{/if}
</div>
<div class="flex items-center mt-10">
<h1 class="title lg:text-3xl">Destinations</h1>
</div>
{#if destinations.length > 0}
<div class="divider" />
<Grid3>
{#if destinations.length > 0}
{#each destinations as destination}
<a class="no-underline mb-5" href={`/destinations/${destination.id}`}>
<div
class="w-full rounded p-5 bg-coolgray-200 hover:bg-destinations indicator duration-150"
>
<div class="w-full flex flex-row">
<div class="absolute top-0 left-0 -m-5 h-10 w-10">
<LocalDockerIcon/>
{#if destination.remoteEngine}
<RemoteDockerIcon/>
{/if}
</div>
<div class="w-full flex flex-col">
<h1 class="font-bold text-base truncate">{destination.name}</h1>
<div class="h-10 text-xs">
{#if $appSession.teamId === '0' && destination.remoteVerified === false && destination.remoteEngine}
<h2 class="text-red-500">Not verified yet</h2>
{/if}
{#if destination.remoteEngine && !destination.sshKeyId}
<h2 class="text-red-500">SSH key missing</h2>
{/if}
<TeamsBadge teams={destination.teams} thing={destination}/>
</div>
</div>
</div>
</div>
</a>
{/each}
{:else}
<h1 class="">Nothing here.</h1>
{/if}
</Grid3>
{/if}

View File

@@ -0,0 +1,135 @@
<script lang="ts">
export let service: any;
export let template: any;
import { page } from '$app/stores';
import ServiceLinks from './_ServiceLinks.svelte';
</script>
<ul class="menu border bg-coolgray-100 border-coolgray-200 rounded p-2 space-y-2 sticky top-4">
<li class="menu-title">
<span>General</span>
</li>
<li class="rounded">
<ServiceLinks {template} {service} linkToDocs={true} />
</li>
<li class="rounded" class:bg-coollabs={$page.url.pathname === `/services/${$page.params.id}`}>
<a href={`/services/${$page.params.id}`} class="no-underline w-full"
><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 10h3v-3l-3.5 -3.5a6 6 0 0 1 8 8l6 6a2 2 0 0 1 -3 3l-6 -6a6 6 0 0 1 -8 -8l3.5 3.5"
/>
</svg>Configurations</a
>
</li>
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/services/${$page.params.id}/secrets`}
>
<a href={`/services/${$page.params.id}/secrets`} class="no-underline w-full"
><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>Secrets</a
>
</li>
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/services/${$page.params.id}/storages`}
>
<a href={`/services/${$page.params.id}/storages`} class="no-underline w-full"
><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>Persistent Volumes</a
>
</li>
<li class="menu-title">
<span>Logs</span>
</li>
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/services/${$page.params.id}/logs`}
>
<a
href={`/services/${$page.params.id}/logs`}
class="no-underline w-full"
><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>Service</a
>
</li>
<li class="menu-title">
<span>Advanced</span>
</li>
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/services/${$page.params.id}/danger`}
>
<a href={`/services/${$page.params.id}/danger`} class="no-underline w-full"
><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 9v2m0 4v.01" />
<path
d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"
/>
</svg>Danger Zone</a
>
</li>
</ul>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
export let name = '';
export let value = '';
export let readonly = false;
export let isNewSecret = false;
import { page } from '$app/stores';
@@ -50,24 +51,27 @@
<td>
<input
style="min-width: 350px !important;"
style="min-width: 350px !important;"
id={isNewSecret ? 'secretName' : 'secretNameNew'}
bind:value={name}
required
placeholder="EXAMPLE_VARIABLE"
readonly={!isNewSecret}
class:bg-transparent={!isNewSecret}
class:cursor-not-allowed={!isNewSecret}
readonly={!isNewSecret || readonly}
class="w-full"
class:bg-coolblack={!isNewSecret}
class:border={!isNewSecret}
class:border-dashed={!isNewSecret}
class:border-coolgray-300={!isNewSecret}
/>
</td>
<td>
<CopyPasswordField
id={isNewSecret ? 'secretValue' : 'secretValueNew'}
name={isNewSecret ? 'secretValue' : 'secretValueNew'}
disabled={readonly}
{readonly}
isPasswordField={true}
bind:value
required
placeholder="J$#@UIO%HO#$U%H"
inputStyle="min-width: 350px; !important"
/>
@@ -76,12 +80,12 @@
<td>
{#if isNewSecret}
<div class="flex items-center justify-center">
<button class="btn btn-sm bg-services" on:click={() => saveSecret(true)}>Add</button>
<button class="btn btn-sm btn-primary" on:click={() => saveSecret(true)}>Add</button>
</div>
{:else}
{:else if !readonly}
<div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center">
<button class="btn btn-sm bg-services" on:click={() => saveSecret(false)}>Set</button>
<button class="btn btn-sm btn-primary" on:click={() => saveSecret(false)}>Set</button>
</div>
<div class="flex justify-center items-end">
<button class="btn btn-sm bg-error" on:click={removeSecret}>Remove</button>

View File

@@ -1,86 +1,14 @@
<script lang="ts">
import DocLink from '$lib/components/DocLink.svelte';
import ServiceIcons from '$lib/components/svg/services/ServiceIcons.svelte';
export let service: any;
import * as Icons from '$lib/components/svg/services';
export let template: any;
export let linkToDocs: boolean = false;
const name: any = service.type && service.type[0].toUpperCase() + service.type.substring(1);
</script>
{#if service.type === 'plausibleanalytics'}
<a href="https://plausible.io" target="_blank">
<Icons.PlausibleAnalytics />
</a>
{:else if service.type === 'nocodb'}
<a href="https://nocodb.com" target="_blank">
<Icons.NocoDb />
</a>
{:else if service.type === 'minio'}
<a href="https://min.io" target="_blank">
<Icons.MinIo />
</a>
{:else if service.type === 'vscodeserver'}
<a href="https://coder.com" target="_blank">
<Icons.VsCodeServer />
</a>
{:else if service.type === 'wordpress'}
<a href="https://wordpress.org" target="_blank">
<Icons.Wordpress />
</a>
{:else if service.type === 'vaultwarden'}
<a href="https://github.com/dani-garcia/vaultwarden" target="_blank">
<Icons.VaultWarden />
</a>
{:else if service.type === 'languagetool'}
<a href="https://languagetool.org/dev" target="_blank">
<Icons.LanguageTool />
</a>
{:else if service.type === 'n8n'}
<a href="https://n8n.io" target="_blank">
<Icons.N8n />
</a>
{:else if service.type === 'uptimekuma'}
<a href="https://github.com/louislam/uptime-kuma" target="_blank">
<Icons.UptimeKuma />
</a>
{:else if service.type === 'ghost'}
<a href="https://ghost.org" target="_blank">
<Icons.Ghost />
</a>
{:else if service.type === 'umami'}
<a href="https://umami.is" target="_blank">
<Icons.Umami />
</a>
{:else if service.type === 'hasura'}
<a href="https://hasura.io" target="_blank">
<Icons.Hasura />
</a>
{:else if service.type === 'fider'}
<a href="https://fider.io" target="_blank">
<Icons.Fider />
</a>
{:else if service.type === 'appwrite'}
<a href="https://appwrite.io" target="_blank">
<Icons.Appwrite />
</a>
{:else if service.type === 'moodle'}
<a href="https://moodle.org" target="_blank">
<Icons.Moodle />
</a>
{:else if service.type === 'glitchTip'}
<a href="https://glitchtip.com" target="_blank">
<Icons.GlitchTip />
</a>
{:else if service.type === 'searxng'}
<a href="https://searxng.org" target="_blank">
<Icons.Searxng />
</a>
{:else if service.type === 'weblate'}
<a href="https://weblate.org" target="_blank">
<Icons.Weblate />
</a>
{:else if service.type === 'grafana'}
<a href="https://github.com/grafana/grafana" target="_blank">
<Icons.Grafana />
</a>
{:else if service.type === 'trilium'}
<a href="https://github.com/zadam/trilium" target="_blank">
<Icons.Trilium />
</a>
{#if linkToDocs}
<DocLink url={template[service.id]?.documentation || 'https://docs.coollabs.io'} text={`Documentation`} isExternal={true} />
{:else}
<ServiceIcons type={service.type} />
{/if}

View File

@@ -1,93 +0,0 @@
<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;
</script>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Appwrite</div>
</div>
<div class="space-y-2">
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="opensslKeyV1">Encryption Key</label>
<CopyPasswordField
name="opensslKeyV1"
id="opensslKeyV1"
isPasswordField
value={service.appwrite.opensslKeyV1}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="executorSecret">Executor Secret</label>
<CopyPasswordField
name="executorSecret"
id="executorSecret"
isPasswordField
value={service.appwrite.executorSecret}
readonly
disabled
/>
</div>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">MariaDB</div>
</div>
<div class="space-y-2">
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="mariadbUser">{$t('forms.username')}</label>
<CopyPasswordField
name="mariadbUser"
id="mariadbUser"
value={service.appwrite.mariadbUser}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2 ">
<label for="mariadbPassword">{$t('forms.password')}</label>
<CopyPasswordField
id="mariadbPassword"
isPasswordField
readonly
disabled
name="mariadbPassword"
value={service.appwrite.mariadbPassword}
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="mariadbRootUser">Root User</label>
<CopyPasswordField
name="mariadbRootUser"
id="mariadbRootUser"
value={service.appwrite.mariadbRootUser}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2 ">
<label for="mariadbRootUserPassword">Root Password</label>
<CopyPasswordField
id="mariadbRootUserPassword"
isPasswordField
readonly
disabled
name="mariadbRootUserPassword"
value={service.appwrite.mariadbRootUserPassword}
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="mariadbDatabase">{$t('index.database')}</label>
<CopyPasswordField
name="mariadbDatabase"
id="mariadbDatabase"
value={service.appwrite.mariadbDatabase}
readonly
disabled
/>
</div>
</div>

View File

@@ -1,194 +0,0 @@
<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="space-y-2">
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<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 lg:px-10 px-2">
<label for="emailNoreply">Noreply Email</label>
<input
class="w-full"
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>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Email</div>
</div>
<div class="space-y-2">
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<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 lg:px-10 px-2">
<label for="emailMailgunDomain">Mailgun Domain</label>
<input
class="w-full"
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 lg:px-10 px-2">
<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 lg:px-10 px-2 font-bold">
<div class="text-lg">Or</div>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="emailSmtpHost">SMTP Host</label>
<input
class="w-full"
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 lg:px-10 px-2">
<label for="emailSmtpPort">SMTP Port</label>
<input
class="w-full"
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 lg:px-10 px-2">
<label for="emailSmtpUser">SMTP User</label>
<input
class="w-full"
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 lg:px-10 px-2">
<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 lg:px-10 px-2">
<label for="emailSmtpEnableStartTls">SMTP Start TLS</label>
<input
class="w-full"
name="emailSmtpEnableStartTls"
id="emailSmtpEnableStartTls"
readonly={readOnly}
disabled={readOnly}
bind:value={service.fider.emailSmtpEnableStartTls}
placeholder="{$t('forms.eg')}: true"
/>
</div>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">PostgreSQL</div>
</div>
<div class="space-y-2">
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<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 lg:px-10 px-2">
<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 lg:px-10 px-2">
<label for="postgresqlDatabase">{$t('index.database')}</label>
<CopyPasswordField
name="postgresqlDatabase"
id="postgresqlDatabase"
value={service.fider.postgresqlDatabase}
readonly
disabled
/>
</div>
</div>

View File

@@ -1,99 +0,0 @@
<script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations';
export let readOnly: any;
export let service: any;
</script>
<div class="flex space-x-1 py-5">
<div class="title">
Ghost <Explainer explanation="You can change these values in the <span class='text-settings'>Ghost admin panel<span>." />
</div>
</div>
<div class="space-y-2">
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="email">{$t('forms.default_email_address')}</label>
<input
class="w-full"
name="email"
id="email"
disabled
readonly
placeholder={$t('forms.email')}
value={service.ghost.defaultEmail}
required
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="defaultPassword">{$t('forms.default_password')}</label>
<CopyPasswordField
id="defaultPassword"
isPasswordField
readonly
disabled
name="defaultPassword"
value={service.ghost.defaultPassword}
/>
</div>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">MariaDB</div>
</div>
<div class="space-y-2">
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<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 lg:px-10 px-2">
<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 lg:px-10 px-2">
<label for="mariadbDatabase">{$t('index.database')}</label>
<input
class="w-full"
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 lg:px-10 px-2">
<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 lg:px-10 px-2">
<label for="mariadbRootUserPassword">{$t('forms.root_db_password')}</label>
<CopyPasswordField
id="mariadbRootUserPassword"
isPasswordField
readonly
disabled
name="mariadbRootUserPassword"
value={service.ghost.mariadbRootUserPassword}
/>
</div>
</div>

View File

@@ -1,246 +0,0 @@
<script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { addToast, status } from '$lib/store';
import Setting from '$lib/components/Setting.svelte';
import { t } from '$lib/translations';
import { post } from '$lib/api';
import { page } from '$app/stores';
import { errorNotification } from '$lib/common';
export let service: any;
const { id } = $page.params;
let loading = false;
async function changeSettings(name: any) {
if (loading || $status.service.isRunning) return;
let enableOpenUserRegistration = service.glitchTip.enableOpenUserRegistration;
let emailSmtpUseSsl = service.glitchTip.emailSmtpUseSsl;
let emailSmtpUseTls = service.glitchTip.emailSmtpUseTls;
loading = true;
if (name === 'enableOpenUserRegistration') {
enableOpenUserRegistration = !enableOpenUserRegistration;
}
if (name === 'emailSmtpUseSsl') {
emailSmtpUseSsl = !emailSmtpUseSsl;
}
if (name === 'emailSmtpUseTls') {
emailSmtpUseTls = !emailSmtpUseTls;
}
try {
await post(`/services/${id}/glitchtip/settings`, {
enableOpenUserRegistration,
emailSmtpUseSsl,
emailSmtpUseTls
});
service.glitchTip.emailSmtpUseTls = emailSmtpUseTls;
service.glitchTip.emailSmtpUseSsl = emailSmtpUseSsl;
service.glitchTip.enableOpenUserRegistration = enableOpenUserRegistration;
return addToast({
message: 'Settings updated.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading = false;
}
}
</script>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">GlitchTip</div>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<Setting
id="enableOpenUserRegistration"
bind:setting={service.glitchTip.enableOpenUserRegistration}
{loading}
disabled={$status.service.isRunning}
on:click={() => changeSettings('enableOpenUserRegistration')}
title="Enable Open User Registration"
description={''}
/>
<!-- <Setting
bind:setting={service.glitchTip.enableOpenUserRegistration}
on:click={toggleEnableOpenUserRegistration}
title={'Enable Open User Registration'}
description={''}
/> -->
</div>
<div class="flex space-x-1 py-2 font-bold">
<div class="subtitle">Email settings</div>
</div>
<div class="space-y-2">
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<Setting
id="emailSmtpUseTls"
bind:setting={service.glitchTip.emailSmtpUseTls}
{loading}
disabled={$status.service.isRunning}
on:click={() => changeSettings('emailSmtpUseTls')}
title="Use TLS for SMTP"
description={''}
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<Setting
id="emailSmtpUseSsl"
bind:setting={service.glitchTip.emailSmtpUseSsl}
{loading}
disabled={$status.service.isRunning}
on:click={() => changeSettings('emailSmtpUseSsl')}
title="Use SSL for SMTP"
description={''}
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="defaultEmailFrom">Default Email From</label>
<CopyPasswordField
required
name="defaultEmailFrom"
id="defaultEmailFrom"
bind:value={service.glitchTip.defaultEmailFrom}
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="emailSmtpHost">SMTP Host</label>
<CopyPasswordField
name="emailSmtpHost"
id="emailSmtpHost"
bind:value={service.glitchTip.emailSmtpHost}
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="emailSmtpPort">SMTP Port</label>
<CopyPasswordField
name="emailSmtpPort"
id="emailSmtpPort"
bind:value={service.glitchTip.emailSmtpPort}
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="emailSmtpUser">SMTP User</label>
<CopyPasswordField
name="emailSmtpUser"
id="emailSmtpUser"
bind:value={service.glitchTip.emailSmtpUser}
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="emailSmtpPassword">SMTP Password</label>
<CopyPasswordField
name="emailSmtpPassword"
id="emailSmtpPassword"
bind:value={service.glitchTip.emailSmtpPassword}
isPasswordField
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="emailBackend">Email Backend</label>
<CopyPasswordField
name="emailBackend"
id="emailBackend"
bind:value={service.glitchTip.emailBackend}
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="mailgunApiKey">Mailgun API Key</label>
<CopyPasswordField
name="mailgunApiKey"
id="mailgunApiKey"
bind:value={service.glitchTip.mailgunApiKey}
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="sendgridApiKey">SendGrid API Key</label>
<CopyPasswordField
name="sendgridApiKey"
id="sendgridApiKey"
bind:value={service.glitchTip.sendgridApiKey}
/>
</div>
<div class="flex space-x-1 py-2 font-bold">
<div class="subtitle">Default User & Superuser</div>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="defaultEmail">{$t('forms.email')}</label>
<CopyPasswordField
name="defaultEmail"
id="defaultEmail"
bind:value={service.glitchTip.defaultEmail}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="defaultUsername">{$t('forms.username')}</label>
<CopyPasswordField
name="defaultUsername"
id="defaultUsername"
bind:value={service.glitchTip.defaultUsername}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="defaultPassword">{$t('forms.password')}</label>
<CopyPasswordField
name="defaultPassword"
id="defaultPassword"
bind:value={service.glitchTip.defaultPassword}
readonly
disabled
isPasswordField
/>
</div>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">PostgreSQL</div>
</div>
<div class="space-y-2">
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="postgresqlUser">{$t('forms.username')}</label>
<CopyPasswordField
name="postgresqlUser"
id="postgresqlUser"
bind:value={service.glitchTip.postgresqlUser}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="postgresqlPassword">{$t('forms.password')}</label>
<CopyPasswordField
id="postgresqlPassword"
isPasswordField
readonly
disabled
name="postgresqlPassword"
bind:value={service.glitchTip.postgresqlPassword}
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="postgresqlDatabase">{$t('index.database')}</label>
<CopyPasswordField
name="postgresqlDatabase"
id="postgresqlDatabase"
bind:value={service.glitchTip.postgresqlDatabase}
readonly
disabled
/>
</div>
</div>

View File

@@ -1,60 +0,0 @@
<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">Hasura</div>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="graphQLAdminPassword">GraphQL Admin Password</label>
<CopyPasswordField
name="graphQLAdminPassword"
id="graphQLAdminPassword"
isPasswordField
value={service.hasura.graphQLAdminPassword}
readonly
disabled
/>
</div>
<div class="lg:px-10 px-2 py-4">Hasura Console is <span class="text-warning">not enabled by default</span> for security reasons. <br>To enable it, add the following secret:<br><br> <code>HASURA_GRAPHQL_ENABLE_CONSOLE=true</code></div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">PostgreSQL</div>
</div>
<div class="space-y-2">
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<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 lg:px-10 px-2">
<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 lg:px-10 px-2">
<label for="postgresqlDatabase">{$t('index.database')}</label>
<CopyPasswordField
name="postgresqlDatabase"
id="postgresqlDatabase"
value={service.hasura.postgresqlDatabase}
readonly
disabled
/>
</div>
</div>

View File

@@ -1,20 +0,0 @@
<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 lg:px-10 px-2">
<label for="masterKey">{$t('forms.admin_api_key')}</label>
<CopyPasswordField
id="masterKey"
isPasswordField
readonly
disabled
name="masterKey"
value={service.meiliSearch.masterKey}
/>
</div>

View File

@@ -1,49 +0,0 @@
<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="space-y-2">
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="rootUser">{$t('forms.root_user')}</label>
<input
class="w-full"
name="rootUser"
id="rootUser"
placeholder={$t('forms.username')}
value={service.minio.rootUser}
disabled
readonly
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<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 lg:px-10 px-2">
<label for="publicPort">{$t('forms.api_port')}</label>
<input
class="w-full"
name="publicPort"
id="publicPort"
value={service.minio.publicPort}
disabled
readonly
placeholder={$t('forms.generated_automatically_after_start')}
/>
</div>
{/if}
</div>

View File

@@ -1,104 +0,0 @@
<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">Moodle</div>
</div>
<div class="grid grid-cols-2 items-center lg:px-10">
<label class="text-base font-bold text-stone-100" for="email">{$t('forms.default_email_address')}</label>
<input
class="w-full"
name="email"
id="email"
required
readonly={readOnly}
disabled={readOnly}
placeholder={$t('forms.email')}
value={service.moodle.defaultEmail}
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10">
<label class="text-base font-bold text-stone-100" for="defaultUsername">Default Username</label>
<CopyPasswordField
id="defaultUsername"
required
readonly={readOnly}
disabled={readOnly}
name="defaultUsername"
value={service.moodle.defaultUsername}
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10">
<label class="text-base font-bold text-stone-100" for="defaultPassword">{$t('forms.default_password')}</label>
<CopyPasswordField
id="defaultPassword"
isPasswordField
required
readonly={readOnly}
disabled={readOnly}
name="defaultPassword"
value={service.moodle.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 lg:px-10">
<label class="text-base font-bold text-stone-100" for="mariadbUser">{$t('forms.username')}</label>
<CopyPasswordField
name="mariadbUser"
id="mariadbUser"
value={service.moodle.mariadbUser}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10">
<label class="text-base font-bold text-stone-100" for="mariadbPassword">{$t('forms.password')}</label>
<CopyPasswordField
id="mariadbPassword"
isPasswordField
readonly
disabled
name="mariadbPassword"
value={service.moodle.mariadbPassword}
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10">
<label class="text-base font-bold text-stone-100" for="mariadbDatabase">{$t('index.database')}</label>
<input
class="w-full"
name="mariadbDatabase"
id="mariadbDatabase"
required
readonly={readOnly}
disabled={readOnly}
bind:value={service.moodle.mariadbDatabase}
placeholder="{$t('forms.eg')}: moodle_db"
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10">
<label class="text-base font-bold text-stone-100" for="mariadbRootUser">{$t('forms.root_db_user')}</label>
<CopyPasswordField
id="mariadbRootUser"
readonly
disabled
name="mariadbRootUser"
value={service.moodle.mariadbRootUser}
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10">
<label class="text-base font-bold text-stone-100" for="mariadbRootUserPassword">{$t('forms.root_db_password')}</label>
<CopyPasswordField
id="mariadbRootUserPassword"
isPasswordField
readonly
disabled
name="mariadbRootUserPassword"
value={service.moodle.mariadbRootUserPassword}
/>
</div>

View File

@@ -1,103 +0,0 @@
<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="space-y-2">
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="scriptName"
>Script Name <Explainer
explanation="Useful if you would like to rename the collector script to prevent it blocked by AdBlockers."
/></label
>
<input
class="w-full"
name="scriptName"
id="scriptName"
readonly={!$appSession.isAdmin && !$status.service.isRunning}
disabled={!$appSession.isAdmin || $status.service.isRunning || $status.service.initialLoading}
placeholder="plausible.js"
bind:value={service.plausibleAnalytics.scriptName}
required
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="email">{$t('forms.email')}</label>
<input
class="w-full"
name="email"
id="email"
disabled={!$appSession.isAdmin || $status.service.isRunning || $status.service.initialLoading}
readonly={readOnly}
placeholder={$t('forms.email')}
bind:value={service.plausibleAnalytics.email}
required
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="username">{$t('forms.username')}</label>
<CopyPasswordField
name="username"
id="username"
disabled={!$appSession.isAdmin || $status.service.isRunning || $status.service.initialLoading}
readonly={readOnly}
placeholder={$t('forms.username')}
bind:value={service.plausibleAnalytics.username}
required
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="password">{$t('forms.password')}</label>
<CopyPasswordField
id="password"
isPasswordField
readonly
disabled
name="password"
value={service.plausibleAnalytics.password}
/>
</div>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">PostgreSQL</div>
</div>
<div class="space-y-2">
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<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 lg:px-10 px-2">
<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 lg:px-10 px-2">
<label for="postgresqlDatabase">{$t('index.database')}</label>
<CopyPasswordField
name="postgresqlDatabase"
id="postgresqlDatabase"
value={service.plausibleAnalytics.postgresqlDatabase}
readonly
disabled
/>
</div>
</div>

View File

@@ -1,36 +0,0 @@
<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">SearXNG</div>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="secretKey">Secret Key</label>
<CopyPasswordField
name="secretKey"
id="secretKey"
isPasswordField
value={service.searxng.secretKey}
readonly
disabled
/>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Redis</div>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="redisPassword">{$t('forms.password')}</label>
<CopyPasswordField
name="redisPassword"
id="redisPassword"
isPasswordField
value={service.searxng.redisPassword}
readonly
disabled
/>
</div>

View File

@@ -1,450 +0,0 @@
<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 { get, post } from '$lib/api';
import { errorNotification, getDomain } from '$lib/common';
import { t } from '$lib/translations';
import {
appSession,
status,
setLocation,
addToast,
checkIfDeploymentEnabledServices,
isDeploymentEnabled
} from '$lib/store';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Setting from '$lib/components/Setting.svelte';
import Fider from './_Fider.svelte';
import Ghost from './_Ghost.svelte';
import GlitchTip from './_GlitchTip.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';
import Appwrite from './_Appwrite.svelte';
import Moodle from './_Moodle.svelte';
import Searxng from './_Searxng.svelte';
import Weblate from './_Weblate.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import Taiga from './_Taiga.svelte';
import DocLink from '$lib/components/DocLink.svelte';
const { id } = $page.params;
$: isDisabled =
!$appSession.isAdmin || $status.service.isRunning || $status.service.initialLoading;
let forceSave = false;
let loading = {
save: false,
verification: false,
cleanup: false
};
let dualCerts = service.dualCerts;
let nonWWWDomain = service.fqdn && getDomain(service.fqdn).replace(/^www\./, '');
let isNonWWWDomainOK = false;
let isWWWDomainOK = false;
async function isDNSValid(domain: any, isWWW: any) {
try {
await get(`/services/${id}/check?domain=${domain}`);
addToast({
message: 'DNS configuration is valid.',
type: 'success'
});
isWWW ? (isWWWDomainOK = true) : (isNonWWWDomainOK = true);
return true;
} catch (error) {
errorNotification(error);
isWWW ? (isWWWDomainOK = false) : (isNonWWWDomainOK = false);
return false;
}
}
async function handleSubmit() {
if (loading.save) return;
loading.save = true;
try {
await post(`/services/${id}/check`, {
fqdn: service.fqdn,
forceSave,
dualCerts,
otherFqdns: service.minio?.apiFqdn ? [service.minio?.apiFqdn] : [],
exposePort: service.exposePort
});
await post(`/services/${id}`, { ...service });
setLocation(service);
forceSave = false;
$isDeploymentEnabled = checkIfDeploymentEnabledServices($appSession.isAdmin, service);
return addToast({
message: 'Configuration saved.',
type: 'success'
});
} catch (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(service.fqdn).includes('www.');
if (isWWW) {
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
} else {
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
}
}
}
return errorNotification(error);
} finally {
loading.save = false;
}
}
async function setEmailsToVerified() {
loading.verification = true;
try {
await post(`/services/${id}/${service.type}/activate`, { id: service.id });
return addToast({
message: t.get('services.all_email_verified'),
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading.verification = false;
}
}
async function migrateAppwriteDB() {
loading.verification = true;
try {
await post(`/services/${id}/${service.type}/migrate`, { id: service.id });
return addToast({
message: "Appwrite's database has been migrated.",
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading.verification = false;
}
}
async function changeSettings(name: any) {
try {
if (name === 'dualCerts') {
dualCerts = !dualCerts;
}
await post(`/services/${id}/settings`, { dualCerts });
return addToast({
message: t.get('application.settings_saved'),
type: 'success'
});
} catch (error) {
return errorNotification(error);
}
}
async function cleanupLogs() {
loading.cleanup = true;
try {
await post(`/services/${id}/${service.type}/cleanup`, { id: service.id });
return addToast({
message: 'Cleared DB Logs',
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading.cleanup = false;
}
}
function doNothing() {
return;
}
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-6xl px-6 pb-12">
<form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-5 items-center">
<h1 class="title">{$t('general')}</h1>
<div class="flex flex-row space-x-2 items-center">
{#if $appSession.isAdmin}
<button
type="submit"
class="btn btn-sm"
class:bg-orange-600={forceSave}
class:hover:bg-orange-400={forceSave}
class:loading={loading.save}
class:bg-services={!loading.save}
disabled={loading.save}
>{loading.save
? $t('forms.save')
: forceSave
? $t('forms.confirm_continue')
: $t('forms.save')}</button
>
{/if}
{#if service.type === 'plausibleanalytics' && $status.service.isRunning}
<div class="btn-group">
<button
class="btn btn-sm"
on:click|preventDefault={setEmailsToVerified}
disabled={loading.verification}
class:loading={loading.verification}
>{loading.verification
? $t('forms.verifying')
: $t('forms.verify_emails_without_smtp')}</button
>
<button
class="btn btn-sm"
on:click|preventDefault={cleanupLogs}
disabled={loading.cleanup}
class:loading={loading.cleanup}>Cleanup Unnecessary Database Logs</button
>
</div>
{/if}
{#if service.type === 'appwrite' && $status.service.isRunning}
<button
class="btn btn-sm"
on:click|preventDefault={migrateAppwriteDB}
disabled={loading.verification}
class:loading={loading.verification}
>{loading.verification
? 'Migrating... it may take a while...'
: "Migrate Appwrite's Database"}</button
>
<DocLink url="https://appwrite.io/docs/upgrade#run-the-migration" />
{/if}
</div>
</div>
{#if service.type === 'minio' && !service.minio.apiFqdn && $status.service.isRunning}
<div class="py-5">
<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="grid gap-2 grid-cols-1 grid-rows-1 lg:px-10 px-2">
<div class="mt-2 grid grid-cols-2 items-center">
<label for="name">{$t('forms.name')}</label>
<input name="name" id="name" class="w-full" bind:value={service.name} required />
</div>
<div class="grid grid-cols-2 items-center">
<label for="version">Version / Tag</label>
<a
href={$appSession.isAdmin && !$status.service.isRunning && !$status.service.initialLoading
? `/services/${id}/configuration/version?from=/services/${id}`
: ''}
class="no-underline"
>
<input
class="w-full"
value={service.version}
id="service"
readonly
disabled={$status.service.isRunning || $status.service.initialLoading}
class:cursor-pointer={!$status.service.isRunning}
/></a
>
</div>
<div class="grid grid-cols-2 items-center">
<label for="destination">{$t('application.destination')}</label>
<div>
{#if service.destinationDockerId}
<div class="no-underline">
<input
value={service.destinationDocker.name}
id="destination"
disabled
class="bg-transparent w-full"
/>
</div>
{/if}
</div>
</div>
{#if service.type === 'minio'}
<div class="grid grid-cols-2 items-center">
<label for="fqdn">Console URL</label>
<CopyPasswordField
placeholder="eg: https://console.min.io"
readonly={isDisabled}
disabled={isDisabled}
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 items-center">
<label for="apiFqdn"
>API URL <Explainer explanation={$t('application.https_explainer')} /></label
>
<CopyPasswordField
placeholder="eg: https://min.io"
readonly={!$appSession.isAdmin && !$status.service.isRunning}
disabled={isDisabled}
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 items-center">
<label for="fqdn"
>{$t('application.url_fqdn')}
<Explainer explanation={$t('application.https_explainer')} />
</label>
<CopyPasswordField
placeholder="eg: https://analytics.coollabs.io"
readonly={!$appSession.isAdmin && !$status.service.isRunning}
disabled={!$appSession.isAdmin ||
$status.service.isRunning ||
$status.service.initialLoading}
name="fqdn"
id="fqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
bind:value={service.fqdn}
required
/>
</div>
{/if}
</div>
{#if forceSave}
<div class="flex-col space-y-2 pt-4 text-center">
{#if isNonWWWDomainOK}
<button
class="btn btn-sm 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="btn btn-sm 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="btn btn-sm 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="btn btn-sm 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 class="grid grid-flow-row gap-2 lg:px-10 px-2 pt-2">
<div class="grid grid-cols-2 items-center">
<Setting
id="dualCerts"
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">
<label for="exposePort"
>Exposed Port <Explainer
explanation={'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.'}
/></label
>
<input
class="w-full"
readonly={!$appSession.isAdmin && !$status.service.isRunning}
disabled={!$appSession.isAdmin ||
$status.service.isRunning ||
$status.service.initialLoading}
name="exposePort"
id="exposePort"
bind:value={service.exposePort}
placeholder="12345"
/>
</div>
</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} />
{:else if service.type === 'appwrite'}
<Appwrite bind:service {readOnly} />
{:else if service.type === 'moodle'}
<Moodle bind:service {readOnly} />
{:else if service.type === 'glitchTip'}
<GlitchTip bind:service />
{:else if service.type === 'searxng'}
<Searxng bind:service />
{:else if service.type === 'weblate'}
<Weblate bind:service />
{:else if service.type === 'taiga'}
<Taiga bind:service />
{/if}
</form>
</div>

View File

@@ -1,118 +0,0 @@
<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">Taiga</div>
</div>
<div class="grid grid-cols-2 items-center lg:px-10">
<label class="text-base font-bold text-stone-100" for="secretKey">Secret Key</label>
<CopyPasswordField
name="secretKey"
id="secretKey"
isPasswordField
value={service.taiga.secretKey}
readonly
disabled
/>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Django</div>
</div>
<div class="grid grid-cols-2 items-center lg:px-10">
<label class="text-base font-bold text-stone-100" for="djangoAdminUser">Admin User</label>
<CopyPasswordField
name="djangoAdminUser"
id="djangoAdminUser"
value={service.taiga.djangoAdminUser}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10">
<label class="text-base font-bold text-stone-100" for="djangoAdminPassword">Admin Password</label>
<CopyPasswordField
name="djangoAdminPassword"
id="djangoAdminPassword"
isPasswordField
value={service.taiga.djangoAdminPassword}
readonly
disabled
/>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">RabbitMQ</div>
</div>
<div class="grid grid-cols-2 items-center lg:px-10">
<label class="text-base font-bold text-stone-100" for="rabbitMQUser">User</label>
<CopyPasswordField
name="rabbitMQUser"
id="rabbitMQUser"
value={service.taiga.rabbitMQUser}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10">
<label class="text-base font-bold text-stone-100" for="rabbitMQPassword">Password</label>
<CopyPasswordField
name="rabbitMQPassword"
id="rabbitMQPassword"
isPasswordField
value={service.taiga.rabbitMQPassword}
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 lg:px-10">
<label class="text-base font-bold text-stone-100" for="postgresqlHost">PostgreSQL Host</label>
<CopyPasswordField
name="postgresqlHost"
id="postgresqlHost"
value={service.taiga.postgresqlHost}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10">
<label class="text-base font-bold text-stone-100" for="postgresqlPort">PostgreSQL Port</label>
<CopyPasswordField
name="postgresqlPort"
id="postgresqlPort"
value={service.taiga.postgresqlPort}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10">
<label class="text-base font-bold text-stone-100" for="postgresqlUser">PostgreSQL User</label>
<CopyPasswordField
name="postgresqlUser"
id="postgresqlUser"
value={service.taiga.postgresqlUser}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10">
<label class="text-base font-bold text-stone-100" for="postgresqlPassword">PostgreSQL Password</label>
<CopyPasswordField
name="postgresqlPassword"
id="postgresqlPassword"
isPasswordField
value={service.taiga.postgresqlPassword}
readonly
disabled
/>
</div>

View File

@@ -1,39 +0,0 @@
<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="space-y-2">
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="adminUser">Admin User</label>
<input
class="w-full"
name="adminUser"
id="adminUser"
placeholder="admin"
value="admin"
disabled
readonly
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="umamiAdminPassword"
>Initial Admin Password <Explainer
explanation="It could be changed in Umami. <br>This is just the password set initially after the first start."
/></label
>
<CopyPasswordField
isPasswordField
name="umamiAdminPassword"
id="umamiAdminPassword"
placeholder="admin"
value={service.umami.umamiAdminPassword}
disabled
readonly
/>
</div>
</div>

View File

@@ -1,21 +0,0 @@
<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 lg:px-10 px-2">
<label for="password">{$t('forms.password')}</label>
<CopyPasswordField
id="password"
isPasswordField
readonly
disabled
name="password"
value={service.vscodeserver.password}
/>
</div>

View File

@@ -1,67 +0,0 @@
<script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
export let service: any;
</script>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Weblate</div>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="adminPassword">Admin password</label>
<CopyPasswordField
name="adminPassword"
id="adminPassword"
isPasswordField
value={service.weblate.adminPassword}
readonly
disabled
/>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">PostgreSQL</div>
</div>
<div class="space-y-2">
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="postgresqlHost">PostgreSQL Host</label>
<CopyPasswordField
name="postgresqlHost"
id="postgresqlHost"
value={service.weblate.postgresqlHost}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="postgresqlPort">PostgreSQL Port</label>
<CopyPasswordField
name="postgresqlPort"
id="postgresqlPort"
value={service.weblate.postgresqlPort}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="postgresqlUser">PostgreSQL User</label>
<CopyPasswordField
name="postgresqlUser"
id="postgresqlUser"
value={service.weblate.postgresqlUser}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="postgresqlPassword">PostgreSQL Password</label>
<CopyPasswordField
name="postgresqlPassword"
id="postgresqlPassword"
isPasswordField
value={service.weblate.postgresqlPassword}
readonly
disabled
/>
</div>
</div>

View File

@@ -1,228 +0,0 @@
<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;
const { ipv4, ipv6 } = settings;
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) : ipv4 || ipv6}:${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 lg:px-10 px-2">
<label for="extraConfig">{$t('forms.extra_config')}</label>
<textarea
class="w-full"
bind:value={service.wordpress.extraConfig}
disabled={$status.service.isRunning || $status.service.initialLoading}
readonly={$status.service.isRunning}
class:resize-none={$status.service.isRunning}
rows="5"
name="extraConfig"
id="extraConfig"
placeholder={!$status.service.isRunning && !$status.service.initialLoading
? `${$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 lg:px-10 px-2">
<Setting
id="ftpEnabled"
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 lg:px-10 px-2">
<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 lg:px-10 px-2">
<label for="ftpUser">User</label>
<CopyPasswordField id="ftpUser" readonly disabled name="ftpUser" value={ftpUser} />
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="ftpPassword">Password</label>
<CopyPasswordField
id="ftpPassword"
isPasswordField
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="space-y-2">
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<Setting
id="ownMysql"
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 lg:px-10 px-2">
<label for="mysqlHost">Host</label>
<input
class="w-full"
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 lg:px-10 px-2">
<label for="mysqlPort">Port</label>
<input
class="w-full"
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 lg:px-10 px-2">
<label for="mysqlDatabase">{$t('index.database')}</label>
<input
class="w-full"
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 lg:px-10 px-2">
<label for="mysqlRootUser">{$t('forms.root_user')}</label>
<input
class="w-full"
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 lg:px-10 px-2">
<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 lg:px-10 px-2">
<label for="mysqlUser">{$t('forms.user')}</label>
<input
class="w-full"
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 lg:px-10 px-2">
<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>
</div>

View File

@@ -0,0 +1,86 @@
<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 { errorNotification, getDomain } from '$lib/common';
export let service: any;
const { id } = $page.params;
const settings = service.settings;
const { ipv4, ipv6 } = settings;
let ftpUrl = generateUrl(service.wordpress?.ftpPublicPort) || '';
let ftpUser = service.wordpress?.ftpUser;
let ftpPassword = service.wordpress?.ftpPassword;
let ftpLoading = false;
let ftpEnabled = service.wordpress?.ftpEnabled || false;
function generateUrl(publicPort: any) {
return browser
? `sftp://${settings?.fqdn ? getDomain(settings.fqdn) : ipv4 || ipv6}:${publicPort}`
: 'Loading...';
}
async function changeSettings(name: any) {
if (ftpLoading) return;
if ($status.service.overallStatus === 'healthy') {
ftpLoading = true;
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;
}
}
}
</script>
<div class="grid grid-cols-2 items-center">
<Setting
id="ftpEnabled"
bind:setting={ftpEnabled}
loading={ftpLoading}
disabled={$status.service.overallStatus !== 'healthy'}
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">
<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">
<label for="ftpUser">User</label>
<CopyPasswordField id="ftpUser" readonly disabled name="ftpUser" value={ftpUser} />
</div>
<div class="grid grid-cols-2 items-center">
<label for="ftpPassword">Password</label>
<CopyPasswordField
id="ftpPassword"
isPasswordField
readonly
disabled
name="ftpPassword"
value={ftpPassword}
/>
</div>
{/if}

View File

@@ -1,86 +1,170 @@
<script lang="ts">
export let isNew = false;
export let storage: any = {
id: null,
path: null
};
export let storage: any = {};
export let services: any = [];
import { del, post } from '$lib/api';
import { page } from '$app/stores';
import { createEventDispatcher } from 'svelte';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
import { addToast } from '$lib/store';
const { id } = $page.params;
const dispatch = createEventDispatcher();
async function saveStorage(newStorage = false) {
async function saveStorage(e: any) {
try {
if (!storage.path) return errorNotification({message: "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, '/');
const formData = new FormData(e.target);
let isNewStorage = true;
let newStorage: any = {
id: null,
containerId: null,
path: null
};
for (let field of formData) {
const [key, value] = field;
newStorage[key] = value;
}
newStorage.path = newStorage.path.startsWith('/') ? newStorage.path : `/${newStorage.path}`;
newStorage.path = newStorage.path.endsWith('/')
? newStorage.path.slice(0, -1)
: newStorage.path;
newStorage.path.replace(/\/\//g, '/');
await post(`/services/${id}/storages`, {
path: storage.path,
storageId: storage.id,
newStorage
path: newStorage.path,
storageId: newStorage.id,
containerId: newStorage.containerId,
isNewStorage
});
dispatch('refresh');
if (isNew) {
storage.path = null;
storage.id = null;
}
addToast({
message: 'Storage saved.',
type: 'success'
});
if (isNewStorage) {
addToast({
message: $t('application.storage.storage_saved'),
type: 'success'
});
} else {
addToast({
message: $t('application.storage.storage_updated'),
type: 'success'
});
}
} catch (error) {
return errorNotification(error);
}
}
async function removeStorage() {
async function removeStorage(removableStorage: any) {
try {
await del(`/services/${id}/storages`, { path: storage.path });
dispatch('refresh');
return addToast({
message: 'Storage deleted.',
type: 'success'
});
const { id: storageId, volumeName, path } = removableStorage;
const sure = confirm(
`Are you sure you want to delete this storage ${volumeName + ':' + path}?`
);
if (sure) {
await del(`/services/${id}/storages`, { storageId });
dispatch('refresh');
addToast({
message: $t('application.storage.storage_deleted'),
type: 'success'
});
}
} 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"
/>
</form>
</td>
<td>
{#if isNew}
<div class="flex items-center justify-center">
<button class="btn btn-sm btn-success" on:click={() => saveStorage(true)}>Add</button
<div class="w-full lg:px-0 px-4">
{#if storage.predefined}
<div class="grid grid-col-1 lg:grid-cols-2 pt-2 gap-2">
<div>
<input
id={storage.containerId}
disabled
readonly
class="w-full"
value={`${
services.find((s) => s.id === storage.containerId).name || storage.containerId
}`}
/>
</div>
<div>
<input
id={storage.volumeName}
disabled
readonly
class="w-full"
value={`${storage.volumeName}:${storage.path}`}
/>
</div>
</div>
{:else if isNew}
<form id="saveVolumesForm" on:submit|preventDefault={saveStorage}>
<div class="grid grid-col-1 lg:grid-cols-2 lg:space-x-4 pt-8">
<div class="flex flex-row">
<div class="flex flex-col w-full">
<label for="name" class="pb-2 uppercase font-bold">Container</label>
<select
form="saveVolumesForm"
name="containerId"
class="w-full lg:w-64"
disabled={storage.predefined}
readonly={storage.predefined}
bind:value={storage.containerId}
>
{#if services.length === 1}
{#if services[0].name}
<option selected value={services[0].id}>{services[0].name}</option>
{:else}
<option selected value={services[0]}>{services[0]}</option>
{/if}
{:else}
{#each services as service}
{#if service.name}
<option value={service.id}>{service.name}</option>
{:else}
<option value={service}>{service}</option>
{/if}
{/each}
{/if}
</select>
</div>
<div class="flex flex-col w-full">
<label for="name" class="pb-2 uppercase font-bold">Path</label>
<input
name="path"
disabled={storage.predefined}
readonly={storage.predefined}
class="w-full lg:w-64"
bind:value={storage.path}
required
placeholder="eg: /sqlite.db"
/>
</div>
</div>
<div class="pt-8">
<button type="submit" class="btn btn-sm btn-primary w-full lg:w-64"
>{$t('forms.add')}</button
>
</div>
</div>
</form>
{:else}
<div class="flex lg:flex-row flex-col items-center gap-2 py-1">
<input
disabled
readonly
class="w-full"
value={`${services.find((s) => s.id === storage.containerId).name || storage.containerId}`}
/>
<input disabled readonly class="w-full" value={`${storage.volumeName}:${storage.path}`} />
<button
class="btn btn-sm btn-error"
on:click|stopPropagation|preventDefault={() => removeStorage(storage)}
>{$t('forms.remove')}</button
>
</div>
{:else}
<div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center">
<button class="btn btn-sm btn-success" on:click={() => saveStorage(false)}>Set</button>
</div>
<div class="flex justify-center items-end">
<button class="btn btn-sm btn-error" on:click={removeStorage}>Remove</button>
</div>
</div>
{/if}
</td>
</div>

View File

@@ -4,8 +4,6 @@
let configurationPhase = null;
if (!service.type) {
configurationPhase = 'type';
} else if (!service.version) {
configurationPhase = 'version';
} else if (!service.destinationDockerId) {
configurationPhase = 'destination';
}
@@ -15,7 +13,7 @@
try {
let readOnly = false;
const response = await get(`/services/${params.id}`);
const { service, settings } = await response;
const { service, settings, template, tags } = await response;
if (!service || Object.entries(service).length === 0) {
return {
status: 302,
@@ -38,12 +36,15 @@
return {
props: {
service
service,
template
},
stuff: {
service,
template,
readOnly,
settings
settings,
tags
}
};
} catch (error) {
@@ -53,8 +54,9 @@
</script>
<script lang="ts">
export let service: any;
export let template: any;
import { page } from '$app/stores';
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import { del, get, post } from '$lib/api';
import { t } from '$lib/translations';
import { errorNotification, handlerNotFoundLoad } from '$lib/common';
@@ -64,19 +66,20 @@
status,
location,
setLocation,
checkIfDeploymentEnabledServices
checkIfDeploymentEnabledServices,
addToast
} from '$lib/store';
import { onDestroy, onMount } from 'svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
import ServiceLinks from './_ServiceLinks.svelte';
import { goto } from '$app/navigation';
import Menu from './_Menu.svelte';
import { saveForm } from './utils';
import { dev } from '$app/env';
const { id } = $page.params;
export let service: any;
$isDeploymentEnabled = checkIfDeploymentEnabledServices($appSession.isAdmin, service);
let statusInterval: any;
async function deleteService() {
const sure = confirm($t('application.confirm_to_delete', { name: service.name }));
@@ -84,7 +87,7 @@
$status.service.initialLoading = true;
try {
if (service.type && $status.service.isRunning)
await post(`/services/${service.id}/${service.type}/stop`, {});
await post(`/services/${service.id}/stop`, {});
await del(`/services/${service.id}`, { id: service.id });
return await goto('/');
} catch (error) {
@@ -94,13 +97,46 @@
}
}
}
async function stopService() {
async function restartService() {
const sure = confirm('Are you sure you want to restart this service?');
if (sure) {
await stopService(true);
await startService();
}
}
async function stopService(skip = false) {
if (skip) {
$status.service.initialLoading = true;
$status.service.loading = true;
try {
await post(`/services/${service.id}/stop`, {});
if (service.type.startsWith('wordpress')) {
await post(`/services/${id}/wordpress/ftp`, {
ftpEnabled: false
});
service.wordpress?.ftpEnabled && window.location.reload();
}
} catch (error) {
return errorNotification(error);
} finally {
$status.service.initialLoading = false;
$status.service.loading = false;
await getStatus();
}
return;
}
const sure = confirm($t('database.confirm_stop', { name: service.name }));
if (sure) {
$status.service.initialLoading = true;
$status.service.loading = true;
try {
await post(`/services/${service.id}/${service.type}/stop`, {});
await post(`/services/${service.id}/stop`, {});
if (service.type.startsWith('wordpress')) {
await post(`/services/${id}/wordpress/ftp`, {
ftpEnabled: false
});
service.wordpress?.ftpEnabled && window.location.reload();
}
} catch (error) {
return errorNotification(error);
} finally {
@@ -114,7 +150,12 @@
$status.service.initialLoading = true;
$status.service.loading = true;
try {
await post(`/services/${service.id}/${service.type}/start`, {});
const form: any = document.getElementById('saveForm');
if (form) {
const formData = new FormData(form);
service = await saveForm(formData, service);
}
await post(`/services/${service.id}/start`, {});
} catch (error) {
return errorNotification(error);
} finally {
@@ -127,25 +168,47 @@
if ($status.service.loading) return;
$status.service.loading = true;
const data = await get(`/services/${id}/status`);
$status.service.isRunning = data.isRunning;
$status.service.isExited = data.isExited;
$status.service.initialLoading = false;
$status.service.statuses = data;
let numberOfServices = Object.keys(data).length;
if (Object.keys($status.service.statuses).length === 0) {
$status.service.overallStatus = 'stopped';
} else {
if (Object.keys($status.service.statuses).length !== numberOfServices) {
$status.service.overallStatus = 'degraded';
} else {
for (const oneService in $status.service.statuses) {
const { isExited, isRestarting, isRunning } = $status.service.statuses[oneService].status;
if (isExited || isRestarting) {
$status.service.overallStatus = 'degraded';
break;
}
if (isRunning) {
$status.service.overallStatus = 'healthy';
}
if (!isExited && !isRestarting && !isRunning) {
$status.service.overallStatus = 'stopped';
}
}
}
}
$status.service.loading = false;
$status.service.initialLoading = false;
}
onDestroy(() => {
$status.service.initialLoading = true;
$status.service.isRunning = false;
$status.service.isExited = false;
$status.service.loading = false;
$status.service.statuses = [];
$status.service.overallStatus = 'stopped';
$location = null;
$isDeploymentEnabled = false;
clearInterval(statusInterval);
});
onMount(async () => {
setLocation(service);
$status.service.isRunning = false;
$status.service.loading = false;
if (service.type && service.destinationDockerId && service.version && service.fqdn) {
if ($isDeploymentEnabled) {
await getStatus();
statusInterval = setInterval(async () => {
await getStatus();
@@ -156,90 +219,59 @@
});
</script>
<nav class="header lg:flex-row flex-col-reverse">
<div class="flex flex-row space-x-2 font-bold pt-10 lg:pt-0">
<div class="flex flex-col items-center justify-center">
<div class="title">
{#if $page.url.pathname === `/services/${id}`}
Configurations
{:else if $page.url.pathname === `/services/${id}/secrets`}
Secrets
{:else if $page.url.pathname === `/services/${id}/storages`}
Persistent Storages
{:else if $page.url.pathname === `/services/${id}/logs`}
Service Logs
{:else if $page.url.pathname === `/services/${id}/configuration/type`}
Select a Service Type
{:else if $page.url.pathname === `/services/${id}/configuration/version`}
Select a Service Version
{:else if $page.url.pathname === `/services/${id}/configuration/destination`}
Select a Destination
{/if}
<div class="mx-auto max-w-screen-2xl px-6 grid grid-cols-1 lg:grid-cols-2">
<nav class="header flex flex-col lg:flex-row order-2 lg:order-1 px-0 lg:px-4 items-start">
<div class="title lg:pb-10">
<div class="flex justify-center items-center space-x-2">
<div>
{#if $page.url.pathname === `/services/${id}/configuration/type`}
Select a Service Type
{:else if $page.url.pathname === `/services/${id}/configuration/version`}
Select a Service Version
{:else if $page.url.pathname === `/services/${id}/configuration/destination`}
Select a Destination
{:else}
<div class="flex justify-center items-center space-x-2">
<div>Configurations</div>
<div
class="badge badge-lg rounded uppercase"
class:text-green-500={$status.service.overallStatus === 'healthy'}
class:text-yellow-400={$status.service.overallStatus === 'degraded'}
class:text-red-500={$status.service.overallStatus === 'stopped'}
>
{$status.service.overallStatus === 'healthy'
? 'Healthy'
: $status.service.overallStatus === 'degraded'
? 'Degraded'
: 'Stopped'}
</div>
</div>
{/if}
</div>
</div>
</div>
<ServiceLinks {service} />
</div>
<div class="lg:block hidden flex-1" />
<div class="flex flex-row flex-wrap space-x-3 justify-center lg:justify-start lg:py-0">
{#if $location}
<a
id="open"
href={$location}
target="_blank"
class="icons 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"
<div class="flex flex-row space-x-2 lg:px-2">
{#if $page.url.pathname.startsWith(`/services/${id}/configuration/`)}
<button
on:click={() => deleteService()}
disabled={!$appSession.isAdmin}
class:bg-red-600={$appSession.isAdmin}
class:hover:bg-red-500={$appSession.isAdmin}
class="btn btn-sm btn-error text-sm"
>
<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
>
<Tooltip triggeredBy="#open">Open</Tooltip>
<div class="hidden lg:block border border-coolgray-500 h-8" />
{/if}
{#if $status.service.isExited}
<a
id="error"
href={$isDeploymentEnabled ? `/services/${id}/logs` : null}
class="icons bg-transparent text-sm flex items-center text-red-500 tooltip-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>
<Tooltip triggeredBy="#error">Service exited with an error!</Tooltip>
{/if}
Delete Service
</button>
{/if}
</div>
</nav>
<div
class="pt-4 flex flex-row items-start justify-center lg:justify-end space-x-2 order-1 lg:order-2"
>
{#if $status.service.initialLoading}
<button
class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out"
>
<button class="btn btn-ghost btn-sm gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
class="h-6 w-6 animate-spin duration-500 ease-in-out"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
@@ -255,14 +287,13 @@
<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>
{$status.service.startup[id] || 'Loading...'}
</button>
{:else if $status.service.isRunning}
{:else if $status.service.overallStatus === 'healthy'}
<button
id="stop"
on:click={stopService}
type="submit"
disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2 text-red-500"
class="btn btn-sm gap-2"
on:click={() => restartService()}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -275,21 +306,23 @@
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" />
<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>
Force Redeploy
</button>
<Tooltip triggeredBy="#stop">Stop</Tooltip>
{:else}
<button
id="start"
on:click={startService}
on:click={() => stopService(false)}
type="submit"
disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2 text-green-500"
><svg
class="btn btn-sm gap-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
class="w-6 h-6 text-error "
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
@@ -298,58 +331,40 @@
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg>
Stop
</button>
<Tooltip triggeredBy="#start">Start</Tooltip>
{/if}
{#if service.type && service.destinationDockerId && service.version}
<div class="hidden lg:block border border-coolgray-500 h-8" />
<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}`}
{:else if $status.service.overallStatus === 'degraded'}
<button
on:click={stopService}
type="submit"
disabled={!$isDeploymentEnabled}
class="btn btn-sm gap-2"
>
<button
id="configuration"
disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm"
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-error"
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> Stop
</button>
{:else if $status.service.overallStatus === 'stopped'}
{#if $status.service.overallStatus === 'degraded'}
<button
class="btn btn-sm gap-2"
disabled={!$isDeploymentEnabled}
on:click={() => restartService()}
>
<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
>
<Tooltip triggeredBy="#configuration">Configuration</Tooltip>
<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 id="secrets" disabled={!$isDeploymentEnabled} class="icons bg-transparent text-sm ">
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
@@ -362,60 +377,21 @@
>
<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"
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)"
/>
<circle cx="12" cy="11" r="1" />
<line x1="12" y1="12" x2="12" y2="14.5" />
</svg></button
></a
>
<Tooltip triggeredBy="#secrets">Secrets</Tooltip>
<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
id="persistentstorage"
disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm"
>
<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
>
<Tooltip triggeredBy="#persistentstorage">Persistent Storages</Tooltip>
<div class="hidden lg:block border border-coolgray-500 h-8" />
<a
href={$isDeploymentEnabled && $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`}
>
{$status.application.statuses.length === 1 ? 'Force Redeploy' : 'Redeploy Stack'}
</button>
{:else if $status.service.overallStatus === 'stopped'}
<button
id="logs"
disabled={!$status.service.isRunning}
class="icons bg-transparent text-sm"
class="btn btn-sm gap-2"
disabled={!$isDeploymentEnabled}
on:click={() => startService()}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
class="w-6 h-6 text-pink-500"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
@@ -424,26 +400,25 @@
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
>
<Tooltip triggeredBy="#logs">Logs</Tooltip>
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
</button>
{/if}
{/if}
<div class="hidden lg:block border border-coolgray-500 h-8" />
<button
id="delete"
on:click={deleteService}
type="submit"
disabled={!$appSession.isAdmin}
class:hover:text-red-500={$appSession.isAdmin}
class="icons bg-transparent text-sm"><DeleteIcon /></button
>
<Tooltip triggeredBy="#delete" placement="left">Delete</Tooltip>
</div>
</nav>
<slot />
</div>
<div
class="mx-auto max-w-screen-2xl px-0 lg:px-10 grid grid-cols-1"
class:lg:grid-cols-4={!$page.url.pathname.startsWith(`/services/${id}/configuration/`)}
>
{#if !$page.url.pathname.startsWith(`/services/${id}/configuration/`)}
<nav class="header flex flex-col lg:pt-0 ">
<Menu {service} {template} />
</nav>
{/if}
<div class="pt-0 col-span-0 lg:col-span-3 pb-24 px-4 lg:px-0">
<slot />
</div>
</div>

View File

@@ -77,16 +77,57 @@
</div>
</div>
{:else}
<div class="flex flex-wrap justify-center">
<div class="flex flex-wrap justify-center gap-4">
{#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>
<button
type="submit"
class="box-selection hover:bg-primary font-bold relative"
on:click={() => handleSubmit(destination.id)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="absolute top-0 left-0 -m-4 h-12 w-12 text-sky-500"
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>
{#if destination.remoteEngine}
<svg
xmlns="http://www.w3.org/2000/svg"
class="absolute top-0 left-9 -m-2 h-6 w-6 text-sky-500 rotate-45"
viewBox="0 0 24 24"
stroke-width="3"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<line x1="12" y1="18" x2="12.01" y2="18" />
<path d="M9.172 15.172a4 4 0 0 1 5.656 0" />
<path d="M6.343 12.343a8 8 0 0 1 11.314 0" />
<path d="M3.515 9.515c4.686 -4.687 12.284 -4.687 17 0" />
</svg>
{/if}
<div class="font-bold text-xl text-center truncate">{destination.name}</div>
<div class="text-center truncate">{destination.network}</div>
</button>
{/each}
</div>
{/if}

View File

@@ -25,43 +25,68 @@
</script>
<script lang="ts">
export let types: any;
export let services: any;
let search = '';
let filteredTypes = types;
let filteredServices = services;
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { get, post } from '$lib/api';
import { errorNotification } from '$lib/common';
import ServiceIcons from '$lib/components/svg/services/ServiceIcons.svelte';
import { onMount } from 'svelte';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
let searchInput: HTMLInputElement;
async function handleSubmit(type: any) {
onMount(() => {
setTimeout(() => {
searchInput.focus();
}, 100);
});
async function handleSubmit(service: any) {
try {
await post(`/services/${id}/configuration/type`, { type });
await post(`/services/${id}/configuration/type`, { type: service.type });
return await goto(from || `/services/${id}`);
} catch (error) {
return errorNotification(error);
}
}
function doSearch() {
filteredTypes = types.filter(
(type: any) =>
type.name.toLowerCase().includes(search.toLowerCase()) ||
type.labels.some((label: string) => label.toLowerCase().includes(search.toLowerCase()))
filteredServices = services.filter(
(service: any) =>
service.name.toLowerCase().includes(search.toLowerCase()) ||
service.labels?.some((label: string) =>
label.toLowerCase().includes(search.toLowerCase())
) ||
service.description.toLowerCase().includes(search.toLowerCase())
);
}
function cleanupSearch() {
search = '';
filteredTypes = types;
filteredServices = services;
}
function sortMe(data: any) {
return data.sort((a, b) => {
let fa = a.name.toLowerCase(),
fb = b.name.toLowerCase();
if (fa < fb) {
return -1;
}
if (fa > fb) {
return 1;
}
return 0;
});
}
</script>
<div class="container lg:mx-auto lg:p-0 px-8 pt-5">
<div class="input-group flex w-full">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="btn btn-square cursor-default no-animation hover:bg-error" on:click={cleanupSearch}>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -79,8 +104,9 @@
</svg>
</div>
<input
bind:this={searchInput}
id="search"
class="input w-full"
class="input w-full input-primary"
type="text"
placeholder="Search for services"
bind:value={search}
@@ -88,17 +114,29 @@
/>
</div>
</div>
<div class="container lg:mx-auto lg:pt-20 lg:p-0 px-8 pt-20">
<div class="flex flex-wrap justify-center gap-8">
{#each filteredTypes 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">
<ServiceIcons type={type.name} />
{type.fancyName}
<div class=" lg:pt-20 lg:p-0 px-8 pt-20">
<div class="grid grid-flow-rows grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8">
{#each sortMe(filteredServices).filter(s=> !s.ignore) as service}
{#key service.name}
<button
on:click={() => handleSubmit(service)}
class="box-selection relative text-xl font-bold hover:bg-primary"
>
<div class="flex flex-col">
<div class="flex justify-center items-center gap-2 pb-4">
<ServiceIcons type={service.type} />
<div>{service.name}</div>
{#if service.subname}
<div class="text-sm">{service.subname}</div>
{/if}
</div>
{#if service.description}
<div class="text-sm font-mono">{service.description}</div>
{/if}
</div>
</button>
</form>
</div>
{/each}
</div>
{/key}
{/each}
</div>
</div>

View File

@@ -37,9 +37,6 @@
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
let recommendedVersion = $appSession.supportedServiceTypesAndVersions.find(
({ name }) => name === type
)?.recommendedVersion;
onMount(async () => {
if (versions.length === 1) {

View File

@@ -0,0 +1,66 @@
<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}`);
return {
props: {
application: stuff.application,
...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 { del, get, post } from '$lib/api';
import { t } from '$lib/translations';
import { appSession, status } from '$lib/store';
import { errorNotification } from '$lib/common';
import { goto } from '$app/navigation';
const { id } = $page.params;
let forceDelete = false;
async function deleteService() {
const sure = confirm($t('application.confirm_to_delete', { name: service.name }));
if (sure) {
$status.service.initialLoading = true;
try {
if (service.type && $status.service.overallStatus !== 'stopped') {
await post(`/services/${service.id}/stop`, {});
}
await del(`/services/${service.id}`, { id: service.id });
return await goto('/');
} catch (error) {
return errorNotification(error);
} finally {
$status.service.initialLoading = false;
}
}
}
</script>
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3">Danger Zone</div>
</div>
<button
id="forcedelete"
on:click={() => deleteService()}
type="submit"
disabled={!$appSession.isAdmin}
class:bg-red-600={$appSession.isAdmin}
class:hover:bg-red-500={$appSession.isAdmin}
class="btn btn-lg btn-error text-sm"
>
Delete Service
</button>
</div>

View File

@@ -8,64 +8,556 @@
</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;
export let template: any;
export let tags: any;
import cuid from 'cuid';
import { onMount } from 'svelte';
import { browser } from '$app/env';
import { page } from '$app/stores';
import { get, post } from '$lib/api';
import { errorNotification, getDomain } from '$lib/common';
import { t } from '$lib/translations';
import {
appSession,
status,
setLocation,
addToast,
checkIfDeploymentEnabledServices,
isDeploymentEnabled
} from '$lib/store';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Setting from '$lib/components/Setting.svelte';
import DocLink from '$lib/components/DocLink.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import ServiceStatus from '$lib/components/ServiceStatus.svelte';
import { saveForm } from './utils';
import Select from 'svelte-select';
import Wordpress from './_Services/wordpress.svelte';
const { id } = $page.params;
let loading = {
usage: false
};
let usage = {
MemUsage: 0,
CPUPerc: 0,
NetIO: 0
};
let usageInterval: any;
let hostPorts = Object.keys(template).filter((key) => {
if (template[key]?.hostPorts?.length > 0) {
return true;
}
});
$: isDisabled =
!$appSession.isAdmin ||
$status.service.overallStatus === 'degraded' ||
$status.service.overallStatus === 'healthy' ||
$status.service.initialLoading;
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;
let forceSave = false;
let loading = {
save: false,
verification: false,
cleanup: false
};
let dualCerts = service.dualCerts;
let nonWWWDomain = service.fqdn && getDomain(service.fqdn).replace(/^www\./, '');
let isNonWWWDomainOK = false;
let isWWWDomainOK = false;
function containerClass() {
return 'text-white bg-transparent font-thin px-0 w-full border border-dashed border-coolgray-200';
}
async function isDNSValid(domain: any, isWWW: any) {
try {
await get(`/services/${id}/check?domain=${domain}`);
addToast({
message: 'DNS configuration is valid.',
type: 'success'
});
isWWW ? (isWWWDomainOK = true) : (isNonWWWDomainOK = true);
return true;
} catch (error) {
errorNotification(error);
isWWW ? (isWWWDomainOK = false) : (isNonWWWDomainOK = false);
return false;
}
}
onDestroy(() => {
clearInterval(usageInterval);
});
async function handleSubmit(e: any) {
if (loading.save) return;
loading.save = true;
try {
const formData = new FormData(e.target);
await post(`/services/${id}/check`, {
fqdn: service.fqdn,
forceSave,
dualCerts,
exposePort: service.exposePort
});
for (const setting of service.serviceSetting) {
if (setting.variableName?.startsWith('$$config_coolify_fqdn') && setting.value) {
for (let field of formData) {
const [key, value] = field;
if (setting.name === key) {
if (setting.value !== value) {
await post(`/services/${id}/check`, {
fqdn: value,
otherFqdn: true
});
}
}
}
}
}
if (formData) service = await saveForm(formData, service);
setLocation(service);
forceSave = false;
$isDeploymentEnabled = checkIfDeploymentEnabledServices($appSession.isAdmin, service);
return addToast({
message: 'Configuration saved.',
type: 'success'
});
} catch (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(service.fqdn).includes('www.');
if (isWWW) {
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
} else {
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
}
}
}
return errorNotification(error);
} finally {
loading.save = false;
}
}
async function setEmailsToVerified() {
loading.verification = true;
try {
await post(`/services/${id}/${service.type}/activate`, { id: service.id });
return addToast({
message: t.get('services.all_email_verified'),
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading.verification = false;
}
}
async function migrateAppwriteDB() {
loading.verification = true;
try {
await post(`/services/${id}/${service.type}/migrate`, { id: service.id });
return addToast({
message: "Appwrite's database has been migrated.",
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading.verification = false;
}
}
async function changeSettings(name: any) {
try {
if (name === 'dualCerts') {
dualCerts = !dualCerts;
}
await post(`/services/${id}/settings`, { dualCerts });
return addToast({
message: t.get('application.settings_saved'),
type: 'success'
});
} catch (error) {
return errorNotification(error);
}
}
async function cleanupLogs() {
loading.cleanup = true;
try {
await post(`/services/${id}/${service.type}/cleanup`, { id: service.id });
return addToast({
message: 'Cleared unnecessary database logs.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading.cleanup = false;
}
}
async function selectTag(event: any) {
service.version = event.detail.value;
}
onMount(async () => {
await getUsage();
usageInterval = setInterval(async () => {
await getUsage();
}, 1000);
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-6xl px-6 lg:my-0 my-4 lg:pt-0 pt-4 rounded">
<div class="text-center">
<div class="stat w-64">
<div class="stat-title">Used Memory / Memory Limit</div>
<div class="stat-value text-xl">{usage?.MemUsage}</div>
<div class="w-full">
<form id="saveForm" on:submit|preventDefault={handleSubmit}>
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3 ">General</div>
{#if $appSession.isAdmin}
<button
type="submit"
class="btn btn-sm"
class:bg-orange-600={forceSave}
class:hover:bg-orange-400={forceSave}
class:loading={loading.save}
class:btn-primary={!loading.save}
disabled={loading.save}
>{loading.save
? $t('forms.save')
: forceSave
? $t('forms.confirm_continue')
: $t('forms.save')}</button
>
{/if}
{#if service.type === 'plausibleanalytics' && $status.service.overallStatus === 'healthy'}
<button
class="btn btn-sm"
on:click|preventDefault={setEmailsToVerified}
disabled={loading.verification}
class:loading={loading.verification}
>{loading.verification
? $t('forms.verifying')
: $t('forms.verify_emails_without_smtp')}</button
>
<button
class="btn btn-sm"
on:click|preventDefault={cleanupLogs}
disabled={loading.cleanup}
class:loading={loading.cleanup}>Cleanup Unnecessary Database Logs</button
>
{/if}
{#if service.type === 'appwrite' && $status.service.overallStatus === 'healthy'}
<button
class="btn btn-sm"
on:click|preventDefault={migrateAppwriteDB}
disabled={loading.verification}
class:loading={loading.verification}
>{loading.verification
? 'Migrating... it may take a while...'
: "Migrate Appwrite's Database"}</button
>
<div>
<DocLink url="https://appwrite.io/docs/upgrade#run-the-migration" />
</div>
{/if}
</div>
</div>
<div class="stat w-64">
<div class="stat-title">Used CPU</div>
<div class="stat-value text-xl">{usage?.CPUPerc}</div>
</div>
<div class="grid grid-flow-row gap-2 px-4">
<div class="mt-2 grid grid-cols-2 items-center">
<label for="name">{$t('forms.name')}</label>
<input name="name" id="name" class="w-full" bind:value={service.name} required />
</div>
<div class="grid grid-cols-2 items-center">
<label for="version">Version / Tag</label>
{#if tags.tags?.length > 0}
<div class="custom-select-wrapper w-full">
<Select
form="saveForm"
containerClasses={isDisabled && containerClass()}
{isDisabled}
id="version"
showIndicator={!isDisabled}
items={[...tags.tags]}
on:select={selectTag}
value={service.version}
isClearable={false}
/>
</div>
{:else}
<input class="w-full border-red-500" disabled placeholder="Error getting tags..." />
{/if}
</div>
<div class="stat w-64">
<div class="stat-title">Network IO</div>
<div class="stat-value text-xl">{usage?.NetIO}</div>
<div class="grid grid-cols-2 items-center">
<label for="destination">{$t('application.destination')}</label>
<div>
{#if service.destinationDockerId}
<div class="no-underline">
<input
value={service.destinationDocker.name}
id="destination"
disabled
class="bg-transparent w-full"
/>
</div>
{/if}
</div>
</div>
<div class="grid grid-cols-2 items-center">
<label for="fqdn"
>{$t('application.url_fqdn')}
<Explainer explanation={$t('application.https_explainer')} />
</label>
<CopyPasswordField
placeholder="eg: https://coollabs.io"
readonly={isDisabled}
disabled={isDisabled}
name="fqdn"
id="fqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
bind:value={service.fqdn}
required
/>
</div>
{#each Object.keys(template) as oneService}
{#each template[oneService].fqdns as fqdn}
<div class="grid grid-cols-2 items-center py-1">
<label for={fqdn.name}>{fqdn.label || fqdn.name}</label>
<CopyPasswordField
placeholder="eg: https://coolify.io"
readonly={isDisabled}
disabled={isDisabled}
required={fqdn.required}
name={fqdn.name}
id={fqdn.name}
bind:value={fqdn.value}
/>
</div>
{/each}
{/each}
</div>
</div>
{#if forceSave}
<div class="flex-col space-y-2 pt-4 text-center">
{#if isNonWWWDomainOK}
<button
class="btn btn-sm 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="btn btn-sm 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="btn btn-sm 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="btn btn-sm 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 class="grid grid-flow-row gap-2 px-4">
<div class="grid grid-cols-2 items-center">
<Setting
id="dualCerts"
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>
{#if hostPorts.length === 0}
<div class="grid grid-cols-2 items-center">
<label for="exposePort"
>Exposed Port <Explainer
explanation={'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.'}
/></label
>
<input
class="w-full"
readonly={isDisabled}
disabled={isDisabled}
name="exposePort"
id="exposePort"
bind:value={service.exposePort}
placeholder="12345"
/>
</div>
{/if}
</div>
<div class="pt-6">
{#each Object.keys(template) as oneService}
<div
class="flex flex-row my-2 space-x-2 mb-6"
class:my-6={template[oneService].environment.length > 0 &&
template[oneService].environment.find((env) => env.main === oneService)}
class:border-b={template[oneService].environment.length > 0 &&
template[oneService].environment.find((env) => env.main === oneService)}
class:border-coolgray-500={template[oneService].environment.length > 0 &&
template[oneService].environment.find((env) => env.main === oneService)}
>
<div class="title font-bold pb-3 capitalize">
{template[oneService].name ||
oneService.replace(`${id}-`, '').replace(id, service.type)}
</div>
<ServiceStatus id={oneService} />
</div>
<div class="grid grid-flow-row gap-2 px-4">
{#if template[oneService].environment.length > 0}
{#each template[oneService].environment as variable}
{#if variable.main === oneService}
<div class="grid grid-cols-2 items-center gap-2">
<label class="h-10" for={variable.name}
>{variable.label || variable.name}
{#if variable.description}
<Explainer explanation={variable.description} />
{/if}</label
>
{#if variable.defaultValue === '$$generate_fqdn'}
<CopyPasswordField
disabled
readonly
name={variable.name}
id={variable.name}
value={service.fqdn}
placeholder={variable.placeholder}
required={variable?.required}
/>
{:else if variable.defaultValue === '$$generate_fqdn_slash'}
<CopyPasswordField
disabled
readonly
name={variable.name}
id={variable.name}
value={service.fqdn + '/' || ''}
placeholder={variable.placeholder}
required={variable?.required}
/>
{:else if variable.defaultValue === '$$generate_domain'}
<CopyPasswordField
disabled
readonly
name={variable.name}
id={variable.name}
value={getDomain(service.fqdn) || ''}
placeholder={variable.placeholder}
required={variable?.required}
/>
{:else if variable.defaultValue === '$$generate_network'}
<CopyPasswordField
disabled
readonly
name={variable.name}
id={variable.name}
value={service.destinationDocker.network}
placeholder={variable.placeholder}
required={variable?.required}
/>
{:else if variable.defaultValue === 'true' || variable.defaultValue === 'false'}
{#if variable.value === 'true' || variable.value === 'false'}
<select
class="w-full font-normal"
readonly={isDisabled}
disabled={isDisabled}
id={variable.name}
name={variable.name}
bind:value={variable.value}
form="saveForm"
placeholder={variable.placeholder}
required={variable?.required}
>
<option value="true">enabled</option>
<option value="false">disabled</option>
</select>
{:else}
<select
class="w-full font-normal"
readonly={isDisabled}
disabled={isDisabled}
id={variable.name}
name={variable.name}
bind:value={variable.defaultValue}
form="saveForm"
placeholder={variable.placeholder}
required={variable?.required}
>
<option value="true">true</option>
<option value="false"> false</option>
</select>
{/if}
{:else if variable.defaultValue === '$$generate_password'}
<CopyPasswordField
isPasswordField
readonly
disabled
name={variable.name}
id={variable.name}
value={variable.value}
placeholder={variable.placeholder}
required={variable?.required}
/>
{:else if variable.type === 'textarea'}
<textarea
class="w-full"
value={variable.value}
readonly={isDisabled}
disabled={isDisabled}
class:resize-none={$status.service.overallStatus === 'healthy'}
rows="5"
name={variable.name}
id={variable.name}
placeholder={variable.placeholder}
required={variable?.required}
/>
{:else}
<CopyPasswordField
isPasswordField={variable.id.startsWith('secret')}
required={variable?.required}
readonly={variable.readOnly || isDisabled}
disabled={variable.readOnly || isDisabled}
name={variable.name}
id={variable.name}
value={variable.value}
placeholder={variable.placeholder}
/>
{/if}
</div>
{/if}
{/each}
{#if template[oneService].name.toLowerCase() === 'wordpress' && service.type.startsWith('wordpress')}
<Wordpress {service} />
{/if}
{/if}
</div>
{/each}
</div>
</form>
</div>
<Services bind:service bind:readOnly bind:settings />

View File

@@ -1,13 +1,11 @@
<script lang="ts">
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';
import { onDestroy, onMount } from 'svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
let service: any = {};
let template: any = null;
let logsLoading = false;
let loadLogsInterval: any = null;
let logs: any = [];
@@ -16,41 +14,39 @@
let followingLogs: any;
let logsEl: any;
let position = 0;
let selectedService: any = null;
let noContainer = false;
const { id } = $page.params;
onMount(async () => {
const response = await get(`/services/${id}`);
template = response.template;
service = response.service;
loadAllLogs();
loadLogsInterval = setInterval(() => {
loadLogs();
}, 1000);
});
onDestroy(() => {
clearInterval(loadLogsInterval);
clearInterval(followingInterval);
});
async function loadAllLogs() {
try {
logsLoading = true;
const data: any = await get(`/services/${id}/logs`);
if (data?.logs) {
lastLog = data.logs[data.logs.length - 1];
logs = data.logs;
}
} catch (error) {
return errorNotification(error);
} finally {
logsLoading = false;
}
}
async function loadLogs() {
if (logsLoading) return;
try {
const newLogs: any = await get(`/services/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`);
const newLogs: any = await get(
`/services/${id}/logs/${selectedService}?since=${lastLog?.split(' ')[0] || 0}`
);
if (newLogs.noContainer) {
noContainer = true;
logs = [];
if (logs.length > 0) {
clearInterval(loadLogsInterval);
selectedService = null;
}
return;
} else {
noContainer = false;
}
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];
@@ -82,48 +78,93 @@
clearInterval(followingInterval);
}
}
async function selectService(service: any, init: boolean = false) {
if (loadLogsInterval) clearInterval(loadLogsInterval);
if (followingInterval) clearInterval(followingInterval);
logs = [];
lastLog = null;
followingLogs = false;
selectedService = service;
loadLogs();
loadLogsInterval = setInterval(() => {
loadLogs();
}, 1000);
}
</script>
<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-1 mx-1">
<button
id="follow"
on:click={followBuild}
class="bg-transparent btn btn-sm btn-link"
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>
<Tooltip triggeredBy="#follow">Follow Logs</Tooltip>
</div>
<div class="font-mono w-full bg-coolgray-200 p-5 overflow-x-auto overflox-y-auto rounded mb-20 flex flex-col -mt-12 scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1">
{#each logs as log}
<p>{log + '\n'}</p>
{/each}
</div>
</div>
{/if}
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3">Service Logs</div>
</div>
</div>
{#if template}
<div class="grid grid-cols-3 gap-2 lg:gap-8 pb-4">
{#each Object.keys(template) as service}
<button
on:click={() => selectService(service, true)}
class:bg-primary={selectedService === service}
class:bg-coolgray-200={selectedService !== service}
class="w-full rounded p-5 hover:bg-primary font-bold"
>
{#if template[service].name}
{template[service].name || ''} <br /><span class="text-xs">({service})</span>
{:else}
<span>{service}</span>
{/if}
</button>
{/each}
</div>
{:else}
<div class="w-full flex justify-center font-bold text-xl">Loading components...</div>
{/if}
{#if selectedService}
<div class="flex flex-row justify-center space-x-2">
{#if logs.length === 0}
{#if noContainer}
<div class="text-xl font-bold tracking-tighter">Container not found / exited.</div>
{/if}
{:else}
<div class="relative w-full">
<div class="flex justify-start sticky space-x-2 pb-2">
<button on:click={followBuild} class="btn btn-sm " class:bg-coollabs={followingLogs}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 mr-2"
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>
{followingLogs ? 'Following Logs...' : 'Follow Logs'}
</button>
{#if loadLogsInterval}
<button id="streaming" class="btn btn-sm bg-transparent border-none loading"
>Streaming logs</button
>
{/if}
</div>
<div
bind:this={logsEl}
on:scroll={detect}
class="font-mono w-full bg-coolgray-100 border border-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded mb-20 flex flex-col scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1"
>
{#each logs as log}
<p>{log + '\n'}</p>
{/each}
</div>
</div>
{/if}
</div>
{/if}

View File

@@ -20,13 +20,11 @@
<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 pLimit from 'p-limit';
import ServiceLinks from './_ServiceLinks.svelte';
import { addToast } from '$lib/store';
import { saveSecret } from './utils';
const limit = pLimit(1);
@@ -38,8 +36,8 @@
const data = await get(`/services/${id}/secrets`);
secrets = [...data.secrets];
}
async function getValues(e: any) {
e.preventDefault();
async function getValues() {
if (!batchSecrets) return;
const eachValuePair = batchSecrets.split('\n');
const batchSecretsPairs = eachValuePair
.filter((secret) => !secret.startsWith('#') && secret)
@@ -48,8 +46,8 @@
const value = rest.join('=');
const cleanValue = value?.replaceAll('"', '') || '';
return {
name,
value: cleanValue,
name: name.trim(),
value: cleanValue.trim(),
isNew: !secrets.find((secret: any) => name === secret.name)
};
});
@@ -68,21 +66,24 @@
}
</script>
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3">Secrets</div>
</div>
<div class="overflow-x-auto">
<table class="w-full border-separate text-left">
<thead>
<tr class="h-12">
<tr class="uppercase">
<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>
<th scope="col uppercase">{$t('forms.value')}</th>
<th scope="col uppercase" class="w-96 text-center">{$t('forms.action')}</th>
</tr>
</thead>
<tbody>
<tbody class="space-y-2">
{#each secrets as secret}
{#key secret.id}
<tr>
<Secret name={secret.name} value={secret.value} on:refresh={refreshSecrets} />
<Secret name={secret.name} value={secret.value} readonly={secret.readOnly} on:refresh={refreshSecrets} />
</tr>
{/key}
{/each}
@@ -92,9 +93,18 @@
</tbody>
</table>
</div>
<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="btn btn-sm bg-services" type="submit">Batch add secrets</button>
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2 pt-10">
<div class="flex flex-row space-x-2">
<div class="title font-bold pb-3 ">Paste <code>.env</code> file</div>
<button type="submit" class="btn btn-sm bg-primary">Add Secrets in Batch</button>
</div>
</div>
<textarea
placeholder={`PORT=1337\nPASSWORD=supersecret`}
bind:value={batchSecrets}
class="mb-2 min-h-[200px] w-full"
/>
</form>
</div>

View File

@@ -5,7 +5,7 @@
const response = await get(`/services/${params.id}/storages`);
return {
props: {
service: stuff.service,
template: stuff.template,
...response
}
};
@@ -19,46 +19,72 @@
</script>
<script lang="ts">
export let service: any;
export let persistentStorages: any;
export let template: any;
import { page } from '$app/stores';
import Storage from './_Storage.svelte';
import { get } from '$lib/api';
import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
import ServiceLinks from './_ServiceLinks.svelte';
import { t } from '$lib/translations';
import Explainer from '$lib/components/Explainer.svelte';
const { id } = $page.params;
async function refreshStorage() {
const data = await get(`/services/${id}/storages`);
persistentStorages = [...data.persistentStorages];
}
let services = Object.keys(template).map((service) => {
if (template[service]?.name) {
return {
name: template[service].name,
id: service
};
} else {
return service;
}
});
</script>
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">
<div class="flex justify-center py-4 text-center">
<SimpleExplainer
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}
<div class="w-full">
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3">
Persistent Volumes <Explainer
position="dropdown-bottom"
explanation={$t('application.storage.persistent_storage_explainer')}
/>
</div>
</div>
{#if persistentStorages.filter((s) => s.predefined).length > 0}
<div class="title">Predefined Volumes</div>
<div class="w-full lg:px-0 px-4">
<div class="grid grid-col-1 lg:grid-cols-2 pt-2 gap-2">
<div class="font-bold uppercase">Container</div>
<div class="font-bold uppercase">Volume ID : Mount Dir</div>
</div>
</div>
{#each persistentStorages.filter((s) => s.predefined) as storage}
{#key storage.id}
<tr>
<Storage on:refresh={refreshStorage} {storage} />
</tr>
<Storage on:refresh={refreshStorage} {storage} {services} />
{/key}
{/each}
<tr>
<Storage on:refresh={refreshStorage} isNew />
</tr>
</tbody>
</table>
{/if}
{#if persistentStorages.filter((s) => !s.predefined).length > 0}
<div class="title" class:pt-10={persistentStorages.filter((s) => s.predefined).length > 0}>
Custom Volumes
</div>
{#each persistentStorages.filter((s) => !s.predefined) as storage}
{#key storage.id}
<Storage on:refresh={refreshStorage} {storage} {services} />
{/key}
{/each}
{/if}
<div class="title" class:pt-10={persistentStorages.filter((s) => s.predefined).length > 0}>
Add New Volume
</div>
<Storage on:refresh={refreshStorage} isNew {services} />
</div>
</div>

View File

@@ -1,4 +1,4 @@
import { post } from '$lib/api';
import { get, post } from '$lib/api';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
@@ -40,3 +40,42 @@ export async function saveSecret({
throw error
}
}
export async function saveForm(formData: any, service: any) {
const settings = service.serviceSetting.map((setting: { name: string }) => setting.name);
const secrets = service.serviceSecret.map((secret: { name: string }) => secret.name);
const baseCoolifySetting = ['name', 'fqdn', 'exposePort', 'version'];
for (let field of formData) {
const [key, value] = field;
if (secrets.includes(key) && value) {
await post(`/services/${service.id}/secrets`, {
name: key,
value,
});
} else {
service.serviceSetting = service.serviceSetting.map((setting: any) => {
if (setting.name === key) {
setting.changed = true;
setting.value = value;
}
return setting;
});
if (!settings.includes(key) && !baseCoolifySetting.includes(key)) {
service.serviceSetting.push({
id: service.id,
name: key,
value: value,
isNew: true
});
}
if (baseCoolifySetting.includes(key)) {
service[key] = value;
}
}
}
await post(`/services/${service.id}`, { ...service });
const { service: reloadedService } = await get(`/services/${service.id}`);
return reloadedService;
}

View File

@@ -3,7 +3,7 @@
import { appSession } from '$lib/store';
</script>
<ul class="menu border bg-coolgray-100 border-coolgray-200 rounded-box p-2 space-y-2">
<ul class="menu border bg-coolgray-100 border-coolgray-200 rounded p-2 space-y-2">
{#if $appSession.teamId === '0'}
<li class="menu-title">
<span>General</span>
@@ -27,6 +27,34 @@
</svg>Coolify Settings</a
>
</li>
<li class="rounded" class:bg-coollabs={$page.url.pathname === `/settings/docker`}>
<a href={`/settings/docker`} class="no-underline w-full">
<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="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>Docker Registries</a
>
</li>
{/if}
<li class="menu-title">
<span>Keys & Certificates</span>

View File

@@ -18,16 +18,13 @@
<script lang="ts">
export let settings: any;
export let certificates: any;
import Setting from '$lib/components/Setting.svelte';
import { del, get, post } from '$lib/api';
import { browser } from '$app/env';
import { t } from '$lib/translations';
import { addToast, appSession, features } from '$lib/store';
import { asyncSleep, errorNotification, getDomain } from '$lib/common';
import Menu from './_Menu.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import Upload from '$lib/components/Upload.svelte';
import { dev } from '$app/env';
let isAPIDebuggingEnabled = settings.isAPIDebuggingEnabled;
let isRegistrationEnabled = settings.isRegistrationEnabled;
@@ -37,6 +34,8 @@
let DNSServers = settings.DNSServers;
let minPort = settings.minPort;
let maxPort = settings.maxPort;
let proxyDefaultRedirect = settings.proxyDefaultRedirect;
let doNotTrack = settings.doNotTrack;
let forceSave = false;
let fqdn = settings.fqdn;
@@ -48,9 +47,61 @@
save: false,
remove: false,
proxyMigration: false,
restart: false
restart: false,
rollback: false
};
let rollbackVersion = localStorage.getItem('lastVersion');
async function rollback() {
if (rollbackVersion) {
const sure = confirm(`Are you sure you want rollback Coolify to ${rollbackVersion}?`);
if (sure) {
try {
loading.rollback = true;
console.log('loading.rollback', loading.rollback);
if (dev) {
console.log('rolling back to', rollbackVersion);
await asyncSleep(4000);
return window.location.reload();
} else {
addToast({
message: 'Rollback started...',
type: 'success'
});
await post(`/update`, { type: 'update', latestVersion: rollbackVersion });
addToast({
message: 'Rollback completed.<br><br>Waiting for the new version to start...',
type: 'success'
});
let reachable = false;
let tries = 0;
do {
await asyncSleep(4000);
try {
await get(`/undead`);
reachable = true;
} catch (error) {
reachable = false;
}
if (reachable) break;
tries++;
} while (!reachable || tries < 120);
addToast({
message: 'New version reachable. Reloading...',
type: 'success'
});
await asyncSleep(3000);
return window.location.reload();
}
} catch (error) {
return errorNotification(error);
} finally {
loading.rollback = false;
}
}
}
}
async function removeFqdn() {
if (fqdn) {
loading.remove = true;
@@ -67,6 +118,9 @@
async function changeSettings(name: any) {
try {
resetView();
if (name === 'doNotTrack') {
doNotTrack = !doNotTrack;
}
if (name === 'isRegistrationEnabled') {
isRegistrationEnabled = !isRegistrationEnabled;
}
@@ -83,6 +137,7 @@
isAPIDebuggingEnabled = !isAPIDebuggingEnabled;
}
await post(`/settings`, {
doNotTrack,
isAPIDebuggingEnabled,
isRegistrationEnabled,
dualCerts,
@@ -107,6 +162,9 @@
await post(`/settings`, { fqdn });
return window.location.reload();
}
if (proxyDefaultRedirect !== settings.proxyDefaultRedirect) {
await post(`/settings`, { proxyDefaultRedirect });
}
if (minPort !== settings.minPort || maxPort !== settings.maxPort) {
await post(`/settings`, { minPort, maxPort });
settings.minPort = minPort;
@@ -228,7 +286,7 @@
</div>
</div>
<div class="flex lg:flex-row flex-col">
<div class="grid grid-flow-row gap-2 px-4 pr-5">
<div class="grid grid-flow-row gap-2 px-4 pr-5">
<div class="grid grid-cols-2 items-center">
<div>
{$t('application.url_fqdn')}
@@ -292,6 +350,49 @@
on:click={() => !isFqdnSet && changeSettings('dualCerts')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<div>
Default Redirect URL
<Explainer
position="dropdown-bottom"
explanation="You can specify where to redirect all requests that does not have a running resource."
/>
</div>
<input
class="w-full"
bind:value={proxyDefaultRedirect}
readonly={!$appSession.isAdmin}
disabled={!$appSession.isAdmin}
name="proxyDefaultRedirect"
id="proxyDefaultRedirect"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="{$t('forms.eg')}: https://coolify.io"
/>
</div>
<div class="grid grid-cols-4 items-center">
<div class="col-span-2">
Rollback to a specific version
<Explainer
position="dropdown-bottom"
explanation="You can rollback to a specific version of Coolify. This will not affect your current running resources.<br><br><a href='https://github.com/coollabsio/coolify/releases' target='_blank'>See available versions</a>"
/>
</div>
<input
class="w-full"
bind:value={rollbackVersion}
readonly={!$appSession.isAdmin}
disabled={!$appSession.isAdmin}
name="rollbackVersion"
id="rollbackVersion"
/>
<button
class:loading={loading.rollback}
class="btn btn-primary ml-2"
disabled={!rollbackVersion || loading.rollback}
on:click|preventDefault|stopPropagation={rollback}>Rollback</button
>
</div>
<div class="grid grid-cols-2 items-center">
<div>
{$t('forms.public_port_range')}
@@ -362,6 +463,15 @@
on:click={() => changeSettings('isAutoUpdateEnabled')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
id="doNotTrack"
bind:setting={doNotTrack}
title="Do Not Track"
description="Do not send error reports to Coolify developers or any telemetry."
on:click={() => changeSettings('doNotTrack')}
/>
</div>
</div>
</div>
</form>

View File

@@ -0,0 +1,240 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ stuff }) => {
try {
return {
props: {
...stuff
}
};
} catch (error: any) {
return {
status: 500,
error: new Error(error)
};
}
};
</script>
<script lang="ts">
export let registries: any;
import { del, post } from '$lib/api';
import { errorNotification } from '$lib/common';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { addToast } from '$lib/store';
const publicRegistries = registries.public;
const privateRegistries = registries.private;
let isModalActive = false;
let newRegistry = {
name: '',
username: '',
password: '',
url: '',
isSystemWide: false
};
async function handleSubmit() {
try {
await post(`/settings/registry/new`, newRegistry);
return window.location.reload();
} catch (error) {
errorNotification(error);
return false;
}
}
async function setRegistry(registry: any) {
try {
await post(`/settings/registry`, registry);
return addToast({
message: 'Registry updated successfully.',
type: 'success'
});
} catch (error) {
errorNotification(error);
return false;
}
}
async function deleteDockerRegistry(id: string) {
const sure = confirm(
'Are you sure you would like to delete this Docker Registry? All dependent resources will be affected and fails to redeploy.'
);
if (sure) {
try {
if (!id) return;
await del(`/settings/registry`, { id });
return window.location.reload();
} catch (error) {
errorNotification(error);
return false;
}
}
}
</script>
<div class="w-full">
<div class="flex border-b border-coolgray-500 mb-6">
<div class="title font-bold pb-3 pr-4">Docker Registries</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<label for="my-modal" class="btn btn-sm btn-primary" on:click={() => (isModalActive = true)}
>Add Docker Registry</label
>
</div>
<div class="mx-auto w-full">
<table class="table w-full">
<thead>
<tr>
<th>Name</th>
<th>SystemWide</th>
<th>Username</th>
<th>Password</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{#each publicRegistries as registry}
<tr>
<td>{registry.name}<div class="text-xs">{registry.url}</div></td>
<td>{(registry.isSystemWide && 'Yes') || 'No'}</td>
<td>
<CopyPasswordField
name="username"
id="Username"
bind:value={registry.username}
placeholder="Username"
/></td
>
<td
><CopyPasswordField
isPasswordField={true}
name="Password"
id="Password"
bind:value={registry.password}
placeholder="Password"
/></td
>
<td>
<button on:click={() => setRegistry(registry)} class="btn btn-sm btn-primary"
>Set</button
>
{#if registry.id !== '0'}
<button
on:click={() => deleteDockerRegistry(registry.id)}
class="btn btn-sm btn-error">Delete</button
>
{/if}
</td>
</tr>
{/each}
{#each privateRegistries as registry}
<tr>
<td>{registry.name} <div class="text-xs">{registry.url}</div></td>
<td>{(registry.isSystemWide && 'Yes') || 'No'}</td>
<td>
<CopyPasswordField
name="username"
id="Username"
bind:value={registry.username}
placeholder="Username"
/></td
>
<td
><CopyPasswordField
isPasswordField={true}
name="Password"
id="Password"
bind:value={registry.password}
placeholder="Password"
/></td
>
<td>
<button on:click={() => setRegistry(registry)} class="btn btn-sm btn-primary"
>Set</button
>
{#if registry.id !== '0'}
<button
on:click={() => deleteDockerRegistry(registry.id)}
class="btn btn-sm btn-error">Delete</button
>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
{#if isModalActive}
<input type="checkbox" id="my-modal" class="modal-toggle" />
<div class="modal modal-bottom sm:modal-middle">
<div class="modal-box rounded bg-coolgray-300">
<h3 class="font-bold text-lg">Add a Docker Registry to Coolify</h3>
<div>
<form on:submit|preventDefault={handleSubmit}>
<label for="name" class="label">
<span class="label-text">Name</span>
</label>
<CopyPasswordField
id="name"
name="name"
bind:value={newRegistry.name}
placeholder="Docker Registry Name"
required
/>
<label for="url" class="label">
<span class="label-text">URL</span>
</label>
<CopyPasswordField
id="url"
name="url"
bind:value={newRegistry.url}
placeholder="Docker Registry URL"
required
/>
<label for="Username" class="label">
<span class="label-text">Username</span>
</label>
<CopyPasswordField
id="Username"
name="username"
bind:value={newRegistry.username}
placeholder="Username"
/>
<label for="Password" class="label">
<span class="label-text">Password</span>
</label>
<CopyPasswordField
isPasswordField={true}
id="Password"
name="password"
bind:value={newRegistry.password}
placeholder="Password"
/>
<div class="flex items-center">
<label for="systemwide" class="label">
<span class="label-text">System Wide</span>
</label>
<input
id="systemwide"
type="checkbox"
bind:checked={newRegistry.isSystemWide}
class="checkbox checkbox-primary"
/>
</div>
<label for="my-modal">
<button type="submit" class="btn btn-sm btn-primary mt-4">Save</button></label
>
<button
on:click={() => (isModalActive = false)}
type="button"
class="btn btn-sm btn-error">Cancel</button
>
</form>
</div>
</div>
</div>
{/if}

View File

@@ -169,7 +169,7 @@
<a
href="https://docs.coollabs.io/coolify/sources#how-to-integrate-with-gitlab"
class="font-bold "
target="_blank"
target="_blank noreferrer"
rel="noopener noreferrer">Documentation and detailed instructions.</a
>
<div class="grid grid-cols-2 items-center">

View File

@@ -19,8 +19,7 @@
<script lang="ts">
export let sources: any = [];
import { get, post } from '$lib/api';
import { goto } from '$app/navigation';
import { get } from '$lib/api';
import { getDomain } from '$lib/common';
import { t } from '$lib/translations';
import { appSession } from '$lib/store';