Merge branch 'next' into edge-db

This commit is contained in:
Ikechukwu Eze
2022-07-31 19:54:30 +02:00
committed by GitHub
91 changed files with 3998 additions and 2151 deletions

View File

@@ -250,7 +250,7 @@
"no_destination_found": "No destination found",
"new_error_network_already_exists": "Network {{network}} already configured for another team!",
"new": {
"saving_and_configuring_proxy": "Saving and configuring proxy...",
"saving_and_configuring_proxy": "Saving...",
"install_proxy": "This will install a proxy on the destination to allow you to access your applications and services without any manual configuration (recommended for Docker).<br><br>Databases will have their own proxy.",
"add_new_destination": "Add New Destination",
"predefined_destinations": "Predefined destinations"

View File

@@ -1,6 +1,8 @@
import { writable, readable, type Writable, type Readable } from 'svelte/store';
interface AppSession {
ipv4: string | null,
ipv6: string | null,
version: string | null,
userId: string | null,
teamId: string | null,
@@ -17,6 +19,8 @@ interface AppSession {
}
export const loginEmail: Writable<string | undefined> = writable()
export const appSession: Writable<AppSession> = writable({
ipv4: null,
ipv6: null,
version: null,
userId: null,
teamId: null,
@@ -31,7 +35,6 @@ export const appSession: Writable<AppSession> = writable({
gitlab: null
}
});
export const isTraefikUsed: Writable<boolean> = writable(false);
export const disabledButton: Writable<boolean> = writable(false);
export const status: Writable<any> = writable({
application: {
@@ -41,14 +44,16 @@ export const status: Writable<any> = writable({
initialLoading: true
},
service: {
initialLoading: true,
isRunning: false,
isExited: false,
loading: false,
isRunning: false
initialLoading: true
},
database: {
initialLoading: true,
isRunning: false,
isExited: false,
loading: false,
isRunning: false
initialLoading: true
}
});
@@ -60,7 +65,6 @@ export const features = readable({
export const location: Writable<null | string> = writable(null)
export const setLocation = (resource: any) => {
console.log(GITPOD_WORKSPACE_URL)
if (GITPOD_WORKSPACE_URL && resource.exposePort) {
const { href } = new URL(GITPOD_WORKSPACE_URL);
const newURL = href

View File

@@ -65,11 +65,12 @@
<script lang="ts">
export let baseSettings: any;
$appSession.ipv4 = baseSettings.ipv4;
$appSession.ipv6 = baseSettings.ipv6;
$appSession.version = baseSettings.version;
$appSession.whiteLabeled = baseSettings.whiteLabeled;
$appSession.whiteLabeledDetails.icon = baseSettings.whiteLabeledIcon;
export let settings: any;
export let userId: string;
export let teamId: string;
export let permission: string;
@@ -84,15 +85,13 @@
import UpdateAvailable from '$lib/components/UpdateAvailable.svelte';
import PageLoader from '$lib/components/PageLoader.svelte';
import { errorNotification } from '$lib/common';
import { appSession, isTraefikUsed } from '$lib/store';
import { appSession } from '$lib/store';
if (userId) $appSession.userId = userId;
if (teamId) $appSession.teamId = teamId;
if (permission) $appSession.permission = permission;
if (isAdmin) $appSession.isAdmin = isAdmin;
$isTraefikUsed = settings?.isTraefikUsed || false;
async function logout() {
try {
Cookies.remove('token');
@@ -322,33 +321,31 @@
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
</svg>
</a>
{#if $appSession.teamId === '0'}
<a
sveltekit:prefetch
href="/settings"
class="icons tooltip-yellow-500 tooltip-right bg-coolgray-200 hover:text-yellow-500"
class:text-yellow-500={$page.url.pathname.startsWith('/settings')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/settings')}
data-tooltip="Settings"
<a
sveltekit:prefetch
href={$appSession.teamId === '0' ? '/settings/global' : '/settings/ssh-keys'}
class="icons tooltip-yellow-500 tooltip-right bg-coolgray-200 hover:text-yellow-500"
class:text-yellow-500={$page.url.pathname.startsWith('/settings')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/settings')}
data-tooltip="Settings"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"
/>
<circle cx="12" cy="12" r="3" />
</svg>
</a>
{/if}
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"
/>
<circle cx="12" cy="12" r="3" />
</svg>
</a>
<div
class="icons tooltip-red-500 tooltip-right bg-coolgray-200 hover:text-red-500"

View File

@@ -36,7 +36,6 @@
return {
props: {
isQueueActive,
application
},
stuff: {
@@ -53,7 +52,6 @@
<script lang="ts">
export let application: any;
export let isQueueActive: any;
import { page } from '$app/stores';
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
@@ -68,7 +66,7 @@
let loading = false;
let statusInterval: any;
let isQueueActive= false;
$disabledButton =
!$appSession.isAdmin ||
!application.fqdn ||
@@ -114,12 +112,12 @@
return window.location.reload();
} catch (error) {
return errorNotification(error);
}
}
}
async function getStatus() {
if ($status.application.loading) return;
$status.application.loading = true;
const data = await get(`/applications/${id}`);
const data = await get(`/applications/${id}/status`);
isQueueActive = data.isQueueActive;
$status.application.isRunning = data.isRunning;
$status.application.isExited = data.isExited;

View File

@@ -9,7 +9,7 @@
redirect: `/applications/${params.id}`
};
}
const response = await get(`/destinations`);
const response = await get(`/destinations?onlyVerified=true`);
return {
props: {
...response
@@ -65,7 +65,7 @@
<div class="flex flex-col justify-center">
{#if !destinations || ownDestinations.length === 0}
<div class="flex-col">
<div class="pb-2">{$t('application.configuration.no_configurable_destination')}</div>
<div class="pb-2 text-center font-bold">{$t('application.configuration.no_configurable_destination')}</div>
<div class="flex justify-center">
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
<svg

View File

@@ -41,7 +41,7 @@
import Setting from './_Setting.svelte';
const { id } = $page.params;
$: isDisabled = !$appSession.isAdmin || $status.application.isRunning;
$: isDisabled = !$appSession.isAdmin || $status.application.isRunning || $status.application.initialLoading;
let domainEl: HTMLInputElement;
@@ -308,7 +308,7 @@
<div class="mx-auto max-w-4xl px-6">
<!-- svelte-ignore missing-declaration -->
<form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-5 font-bold">
<div class="flex space-x-1 pb-5">
<div class="title">{$t('general')}</div>
{#if $appSession.isAdmin}
<button
@@ -536,7 +536,7 @@
<div class="grid grid-cols-2 items-center pb-8">
<Setting
dataTooltip={$t('forms.must_be_stopped_to_modify')}
disabled={isDisabled}
disabled={$status.application.isRunning}
isCenter={false}
bind:setting={dualCerts}
title={$t('application.ssl_www_and_non_www')}

View File

@@ -106,7 +106,7 @@
{#if currentStatus === 'queued'}
<div class="text-center font-bold text-xl">{$t('application.build.queued_waiting_exec')}</div>
{:else}
<div class="flex justify-end sticky top-0 p-2">
<div class="flex justify-end sticky top-0 p-1 mx-1">
<button
on:click={followBuild}
class="bg-transparent hover:text-green-500 hover:bg-coolgray-500"

View File

@@ -1,32 +1,13 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
import { onDestroy, onMount } from 'svelte';
export const load: Load = async ({ fetch, params, url, stuff }) => {
try {
const response = await get(`/applications/${params.id}/logs`);
return {
props: {
application: stuff.application,
...response
}
};
} catch (error) {
return {
status: 500,
error: new Error(`Could not load ${url}`)
};
}
};
</script>
<script lang="ts">
export let application: any;
import { page } from '$app/stores';
import { get } from '$lib/api';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
import LoadingLogs from '$lib/components/LoadingLogs.svelte';
import { onMount, onDestroy } from 'svelte';
let application: any = {};
let logsLoading = false;
let loadLogsInterval: any = null;
let logs: any = [];
let lastLog: any = null;
@@ -37,6 +18,8 @@
const { id } = $page.params;
onMount(async () => {
const response = await get(`/applications/${id}`);
application = response.application;
loadAllLogs();
loadLogsInterval = setInterval(() => {
loadLogs();
@@ -48,6 +31,7 @@
});
async function loadAllLogs() {
try {
logsLoading = true;
const data: any = await get(`/applications/${id}/logs`);
if (data?.logs) {
lastLog = data.logs[data.logs.length - 1];
@@ -56,9 +40,12 @@
} catch (error) {
console.log(error);
return errorNotification(error);
} finally {
logsLoading = false;
}
}
async function loadLogs() {
if (logsLoading) return;
try {
const newLogs: any = await get(
`/applications/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`
@@ -157,7 +144,7 @@
{#if loadLogsInterval}
<LoadingLogs />
{/if}
<div class="flex justify-end sticky top-0 p-2 mx-1">
<div class="flex justify-end sticky top-0 p-1 mx-1">
<button
on:click={followBuild}
class="bg-transparent"

View File

@@ -1,12 +1,10 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, stuff, url }) => {
export const load: Load = async ({ stuff, url }) => {
try {
const response = await get(`/applications/${params.id}/previews`);
return {
props: {
application: stuff.application,
...response
application: stuff.application
}
};
} catch (error) {
@@ -19,10 +17,7 @@
</script>
<script lang="ts">
export let containers: any;
export let application: any;
export let PRMRSecrets: any;
export let applicationSecrets: any;
import Secret from './_Secret.svelte';
import { get, post } from '$lib/api';
import { page } from '$app/stores';
@@ -31,12 +26,33 @@
import { t } from '$lib/translations';
import { goto } from '$app/navigation';
import { errorNotification, getDomain } from '$lib/common';
import { onMount } from 'svelte';
import Loading from '$lib/components/Loading.svelte';
const { id } = $page.params;
let containers: any;
let PRMRSecrets: any;
let applicationSecrets: any;
let loading = {
init: true,
removing: false
};
async function refreshSecrets() {
const data = await get(`/applications/${id}/secrets`);
PRMRSecrets = [...data.secrets];
}
async function removeApplication(container: any) {
try {
loading.removing = true;
await post(`/applications/${id}/stop/preview`, {
pullmergeRequestId: container.pullmergeRequestId
});
return window.location.reload();
} catch (error) {
return errorNotification(error);
}
}
async function redeploy(container: any) {
try {
const { buildId } = await post(`/applications/${id}/deploy`, {
@@ -55,6 +71,19 @@
return errorNotification(error);
}
}
onMount(async () => {
try {
loading.init = true;
const response = await get(`/applications/${id}/previews`);
containers = response.containers;
PRMRSecrets = response.PRMRSecrets;
applicationSecrets = response.applicationSecrets;
} catch (error) {
return errorNotification(error);
} finally {
loading.init = false;
}
});
</script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
@@ -62,7 +91,7 @@
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Preview Deployments
</div>
<span class="text-xs">{application.name} </span>
<span class="text-xs">{application?.name}</span>
</div>
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
<a
@@ -108,68 +137,81 @@
</a>
{/if}
</div>
<div class="mx-auto max-w-6xl px-6 pt-4">
<div class="flex justify-center py-4 text-center">
<Explainer
customClass="w-full"
text={applicationSecrets.length === 0
? "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."
: "These values overwrite application secrets in PR/MR deployments. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."}
/>
</div>
{#if applicationSecrets.length !== 0}
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
<th scope="col">{$t('forms.name')}</th>
<th scope="col">{$t('forms.value')}</th>
<th scope="col" class="w-64 text-center"
>{$t('application.preview.need_during_buildtime')}</th
>
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
</tr>
</thead>
<tbody>
{#each applicationSecrets as secret}
{#key secret.id}
<tr>
<Secret
PRMRSecret={PRMRSecrets.find((s) => s.name === secret.name)}
isPRMRSecret
name={secret.name}
value={secret.value}
isBuildSecret={secret.isBuildSecret}
on:refresh={refreshSecrets}
/>
</tr>
{/key}
{/each}
</tbody>
</table>
{/if}
</div>
<div class="mx-auto max-w-4xl py-10">
<div class="flex flex-wrap justify-center space-x-2">
{#if containers.length > 0}
{#each containers as container}
<a href={container.fqdn} class="p-2 no-underline" target="_blank">
<div class="box-selection text-center hover:border-transparent hover:bg-coolgray-200">
<div class="truncate text-center text-xl font-bold">{getDomain(container.fqdn)}</div>
</div>
</a>
<div class="flex items-center justify-center">
<button class="bg-coollabs hover:bg-coollabs-100" on:click={() => redeploy(container)}
>{$t('application.preview.redeploy')}</button
>
</div>
{/each}
{:else}
<div class="flex-col">
<div class="text-center font-bold text-xl">
{$t('application.preview.no_previews_available')}
</div>
</div>
{#if loading.init}
<Loading />
{:else}
<div class="mx-auto max-w-6xl px-6 pt-4">
<div class="flex justify-center py-4 text-center">
<Explainer
customClass="w-full"
text={applicationSecrets.length === 0
? "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."
: "These values overwrite application secrets in PR/MR deployments. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."}
/>
</div>
{#if applicationSecrets.length !== 0}
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
<th scope="col">{$t('forms.name')}</th>
<th scope="col">{$t('forms.value')}</th>
<th scope="col" class="w-64 text-center"
>{$t('application.preview.need_during_buildtime')}</th
>
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
</tr>
</thead>
<tbody>
{#each applicationSecrets as secret}
{#key secret.id}
<tr>
<Secret
PRMRSecret={PRMRSecrets.find((s) => s.name === secret.name)}
isPRMRSecret
name={secret.name}
value={secret.value}
isBuildSecret={secret.isBuildSecret}
on:refresh={refreshSecrets}
/>
</tr>
{/key}
{/each}
</tbody>
</table>
{/if}
</div>
</div>
<div class="mx-auto max-w-4xl py-10">
<div class="flex flex-wrap justify-center space-x-2">
{#if containers.length > 0}
{#each containers as container}
<a href={container.fqdn} class="p-2 no-underline" target="_blank">
<div class="box-selection text-center hover:border-transparent hover:bg-green-600">
<div class="truncate text-center text-xl font-bold">{getDomain(container.fqdn)}</div>
</div>
</a>
<div class="flex items-center justify-center">
<button class="bg-coollabs hover:bg-coollabs-100" on:click={() => redeploy(container)}
>{$t('application.preview.redeploy')}</button
>
</div>
<div class="flex items-center justify-center">
<button
class:bg-red-600={!loading.removing}
class:hover:bg-red-500={!loading.removing}
disabled={loading.removing}
on:click={() => removeApplication(container)}
>{loading.removing ? 'Removing...' : 'Remove Application'}
</button>
</div>
{/each}
{:else}
<div class="flex-col">
<div class="text-center font-bold text-xl">
{$t('application.preview.no_previews_available')}
</div>
</div>
{/if}
</div>
</div>
{/if}

View File

@@ -93,7 +93,7 @@
<div class="flex flex-col">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each ownApplications as application}
<a href="/applications/{application.id}" class="w-96 p-2 no-underline">
<a href="/applications/{application.id}" class="p-2 no-underline">
<div class="box-selection group relative hover:bg-green-600">
{#if application.buildPack}
{#if application.buildPack.toLowerCase() === 'rust'}
@@ -140,15 +140,18 @@
{#if application.fqdn}
<div class="truncate text-center">{getDomain(application.fqdn) || ''}</div>
{/if}
{#if application.destinationDocker?.name}
<div class="truncate text-center">{application.destinationDocker.name}</div>
{/if}
{#if !application.gitSourceId || !application.repository || !application.branch}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Git Source Missing
</div>
{:else if !application.destinationDockerId}
{:else if !application.destinationDockerId}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Destination Missing
</div>
{:else if !application.fqdn}
{:else if !application.fqdn}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
URL Missing
</div>
@@ -158,10 +161,10 @@
{/each}
</div>
{#if otherApplications.length > 0 && $appSession.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Applications</div>
<div class="px-6 pb-5 pt-10 text-2xl font-bold text-center">Other Applications</div>
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherApplications as application}
<a href="/applications/{application.id}" class="w-96 p-2 no-underline">
<a href="/applications/{application.id}" class="p-2 no-underline">
<div class="box-selection group relative hover:bg-green-600">
{#if application.buildPack}
{#if application.buildPack.toLowerCase() === 'rust'}

View File

@@ -1,7 +1,6 @@
<script lang="ts">
export let database: any;
export let privatePort: any;
export let settings: any;
import { page } from '$app/stores';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
@@ -17,8 +16,9 @@
import { post } from '$lib/api';
import { toast } from '@zerodevx/svelte-toast';
import { t } from '$lib/translations';
import { errorNotification, getDomain } from '$lib/common';
import { errorNotification } from '$lib/common';
import { appSession, status } from '$lib/store';
import Explainer from '$lib/components/Explainer.svelte';
const { id } = $page.params;
@@ -49,12 +49,15 @@
databaseDbUser = '';
}
}
function generateUrl(): string {
return `${database.type}://${
databaseDbUser ? databaseDbUser + ':' : ''
}${databaseDbUserPassword}@${
isPublic ? (settings.fqdn ? getDomain(settings.fqdn) : window.location.hostname) : database.id
isPublic
? database.destinationDocker.remoteEngine
? database.destinationDocker.remoteIpAddress
: $appSession.ipv4
: database.id
}:${isPublic ? database.publicPort : privatePort}/${databaseDefault}`;
}
@@ -103,7 +106,7 @@
<div class="mx-auto max-w-4xl px-6">
<form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-5 font-bold">
<div class="flex space-x-1 pb-5">
<div class="title">{$t('general')}</div>
{#if $appSession.isAdmin}
<button
@@ -153,7 +156,8 @@
>
<input
value={database.version}
disabled={$status.database.isRunning}
readonly
disabled={$status.database.isRunning || $status.database.initialLoading}
class:cursor-pointer={!$status.database.isRunning}
/></a
>
@@ -203,9 +207,16 @@
<EdgeDB {database} />
{/if}
<div class="grid grid-cols-2 items-center px-10 pb-8">
<label for="url" class="text-base font-bold text-stone-100"
>{$t('database.connection_string')}</label
>
<div>
<label for="url" class="text-base font-bold text-stone-100"
>{$t('database.connection_string')}</label
>
{#if !isPublic && database.destinationDocker.remoteEngine}
<Explainer
text="You can only access the database with this URL if your application is deployed to the same Destination."
/>
{/if}
</div>
<CopyPasswordField
textarea={true}
placeholder={$t('forms.generated_automatically_after_start')}

View File

@@ -58,7 +58,7 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { errorNotification, handlerNotFoundLoad } from '$lib/common';
import { appSession, status } from '$lib/store';
import { appSession, status, disabledButton } from '$lib/store';
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import Loading from '$lib/components/Loading.svelte';
import { onDestroy, onMount } from 'svelte';
@@ -66,6 +66,9 @@
let loading = false;
let statusInterval: any = false;
$disabledButton = !$appSession.isAdmin;
async function deleteDatabase() {
const sure = confirm(`Are you sure you would like to delete '${database.name}'?`);
if (sure) {
@@ -104,7 +107,7 @@
async function getStatus() {
if ($status.database.loading) return;
$status.database.loading = true;
const data = await get(`/databases/${id}`);
const data = await get(`/databases/${id}/status`);
$status.database.isRunning = data.isRunning;
$status.database.initialLoading = false;
$status.database.loading = false;
@@ -138,6 +141,32 @@
<Loading fullscreen cover />
{:else}
{#if database.type && database.destinationDockerId && database.version && database.defaultDatabase}
{#if $status.database.isExited}
<a
href={!$disabledButton ? `/databases/${id}/logs` : null}
class=" icons bg-transparent tooltip-bottom text-sm flex items-center text-red-500 tooltip-red-500"
data-tooltip="Service exited with an error!"
sveltekit:prefetch
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentcolor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M8.7 3h6.6c.3 0 .5 .1 .7 .3l4.7 4.7c.2 .2 .3 .4 .3 .7v6.6c0 .3 -.1 .5 -.3 .7l-4.7 4.7c-.2 .2 -.4 .3 -.7 .3h-6.6c-.3 0 -.5 -.1 -.7 -.3l-4.7 -4.7c-.2 -.2 -.3 -.4 -.3 -.7v-6.6c0 -.3 .1 -.5 .3 -.7l4.7 -4.7c.2 -.2 .4 -.3 .7 -.3z"
/>
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
</a>
{/if}
{#if $status.database.initialLoading}
<button
class="icons tooltip-bottom flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out"

View File

@@ -9,7 +9,7 @@
redirect: `/database/${params.id}`
};
}
const response = await get(`/destinations`);
const response = await get(`/destinations?onlyVerified=true`);
return {
props: {
...response
@@ -55,7 +55,7 @@
<div class="flex justify-center">
{#if !destinations || destinations.length === 0}
<div class="flex-col">
<div class="pb-2">{$t('application.configuration.no_configurable_destination')}</div>
<div class="pb-2 text-center font-bold">{$t('application.configuration.no_configurable_destination')}</div>
<div class="flex justify-center">
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
<svg

View File

@@ -15,7 +15,6 @@
import Databases from './_Databases/_Databases.svelte';
import { status } from '$lib/store';
export let database: any;
export let settings: any;
export let privatePort: any;
const { id } = $page.params;
@@ -86,4 +85,4 @@
</dl>
</div>
</div>
<Databases bind:database {privatePort} {settings} />
<Databases bind:database {privatePort}/>

View File

@@ -1,31 +1,13 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
import { onDestroy, onMount } from 'svelte';
export const load: Load = async ({ fetch, params, url, stuff }) => {
try {
const response = await get(`/databases/${params.id}/logs`);
return {
props: {
database: stuff.database,
...response
}
};
} catch (error) {
return {
status: 500,
error: new Error(`Could not load ${url}`)
};
}
};
</script>
<script lang="ts">
export let database: any;
import { onDestroy, onMount } from 'svelte';
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 { errorNotification } from '$lib/common';
const { id } = $page.params;
let loadLogsInterval: any = null;
let logs: any = [];
@@ -34,9 +16,12 @@ import { errorNotification } from '$lib/common';
let followingLogs: any;
let logsEl: any;
let position = 0;
let loadingLogs = false;
let database: any = {};
const { id } = $page.params;
onMount(async () => {
const { logs: firstLogs } = await get(`/databases/${id}/logs`);
logs = firstLogs;
loadAllLogs();
loadLogsInterval = setInterval(() => {
loadLogs();
@@ -48,6 +33,7 @@ import { errorNotification } from '$lib/common';
});
async function loadAllLogs() {
try {
loadingLogs = true;
const data: any = await get(`/databases/${id}/logs`);
if (data?.logs) {
lastLog = data.logs[data.logs.length - 1];
@@ -56,13 +42,15 @@ import { errorNotification } from '$lib/common';
} catch (error) {
console.log(error);
return errorNotification(error);
} finally {
loadingLogs = false;
}
}
async function loadLogs() {
if (loadingLogs) return;
try {
const newLogs: any = await get(
`/databases/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`
);
loadingLogs = true;
const newLogs: any = await get(`/databases/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`);
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
logs = logs.concat(newLogs.logs);
@@ -70,6 +58,8 @@ import { errorNotification } from '$lib/common';
}
} catch (error) {
return errorNotification(error);
} finally {
loadingLogs = false;
}
}
function detect() {
@@ -137,7 +127,7 @@ import { errorNotification } from '$lib/common';
{#if loadLogsInterval}
<LoadingLogs />
{/if}
<div class="flex justify-end sticky top-0 p-2 mx-1">
<div class="flex justify-end sticky top-0 p-1 mx-1">
<button
on:click={followBuild}
class="bg-transparent"

View File

@@ -78,7 +78,7 @@
<div class="flex flex-col">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each ownDatabases as database}
<a href="/databases/{database.id}" class="w-96 p-2 no-underline">
<a href="/databases/{database.id}" class="p-2 no-underline">
<div class="box-selection group relative hover:bg-purple-600">
{#if database.type === 'clickhouse'}
<Clickhouse isAbsolute />
@@ -103,6 +103,9 @@
{#if $appSession.teamId === '0' && otherDatabases.length > 0}
<div class="truncate text-center">{database.teams[0].name}</div>
{/if}
{#if database.destinationDocker?.name}
<div class="truncate text-center">{database.destinationDocker.name}</div>
{/if}
{#if !database.type}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
{$t('application.configuration.configuration_missing')}
@@ -113,10 +116,10 @@
{/each}
</div>
{#if otherDatabases.length > 0 && $appSession.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Databases</div>
<div class="px-6 pb-5 pt-10 text-2xl font-bold text-center">Other Databases</div>
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherDatabases as database}
<a href="/databases/{database.id}" class="w-96 p-2 no-underline">
<a href="/databases/{database.id}" class="p-2 no-underline">
<div class="box-selection group relative hover:bg-purple-600">
{#if database.type === 'clickhouse'}
<Clickhouse isAbsolute />

View File

@@ -1,56 +1,65 @@
<script lang="ts">
export let destination: any;
export let settings: any;
export let state: any;
import { toast } from '@zerodevx/svelte-toast';
import { page } from '$app/stores';
import { post } from '$lib/api';
import { get, post } from '$lib/api';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { onMount } from 'svelte';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
import { appSession } from '$lib/store';
import Setting from '$lib/components/Setting.svelte';
const { id } = $page.params;
let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock';
let loading = false;
let loadingProxy = false;
let restarting = false;
let loading = {
restart: false,
proxy: false,
save: false
};
async function handleSubmit() {
loading = true;
loading.save = true;
try {
return await post(`/destinations/${id}`, { ...destination });
await post(`/destinations/${id}`, { ...destination });
toast.push('Configuration saved.');
} catch (error) {
return errorNotification(error);
} finally {
loading = false;
loading.save = false;
}
}
onMount(async () => {
if (state === false && destination.isCoolifyProxyUsed === true) {
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
loading.proxy = true;
const { isRunning } = await get(`/destinations/${id}/status`);
let proxyUsed = !destination.isCoolifyProxyUsed;
if (isRunning === false && destination.isCoolifyProxyUsed === true) {
try {
await post(`/destinations/${id}/settings`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
isCoolifyProxyUsed: proxyUsed,
engine: destination.engine
});
await stopProxy();
} catch (error) {
return errorNotification(error);
}
} else if (state === true && destination.isCoolifyProxyUsed === false) {
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
} else if (isRunning === true && destination.isCoolifyProxyUsed === false) {
try {
await post(`/destinations/${id}/settings`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
isCoolifyProxyUsed: proxyUsed,
engine: destination.engine
});
await startProxy();
destination.isCoolifyProxyUsed = proxyUsed;
} catch (error) {
return errorNotification(error);
} finally {
loading.proxy = false;
}
}
loading.proxy = false;
});
async function changeProxySetting() {
if (!cannotDisable) {
@@ -67,7 +76,7 @@
}
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
try {
loadingProxy = true;
loading.proxy = true;
await post(`/destinations/${id}/settings`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
engine: destination.engine
@@ -80,7 +89,7 @@
} catch (error) {
return errorNotification(error);
} finally {
loadingProxy = false;
loading.proxy = false;
}
}
}
@@ -104,7 +113,7 @@
const sure = confirm($t('destination.confirm_restart_proxy'));
if (sure) {
try {
restarting = true;
loading.restart = true;
toast.push($t('destination.coolify_proxy_restarting'));
await post(`/destinations/${id}/restart`, {
engine: destination.engine,
@@ -115,7 +124,7 @@
window.location.reload();
}, 5000);
} finally {
restarting = false;
loading.restart = false;
}
}
}
@@ -128,23 +137,20 @@
<button
type="submit"
class="bg-sky-600 hover:bg-sky-500"
class:bg-sky-600={!loading}
class:hover:bg-sky-500={!loading}
disabled={loading}
>{loading ? $t('forms.saving') : $t('forms.save')}
class:bg-sky-600={!loading.save}
class:hover:bg-sky-500={!loading.save}
disabled={loading.save}
>{loading.save ? $t('forms.saving') : $t('forms.save')}
</button>
<button
class={restarting ? '' : 'bg-red-600 hover:bg-red-500'}
disabled={restarting}
class={loading.restart ? '' : 'bg-red-600 hover:bg-red-500'}
disabled={loading.restart}
on:click|preventDefault={forceRestartProxy}
>{restarting
>{loading.restart
? $t('destination.restarting_please_wait')
: $t('destination.force_restart_proxy')}</button
>
{/if}
<!-- <button type="button" class="bg-coollabs hover:bg-coollabs-100" on:click={scanApps}
>Scan for applications</button
> -->
</div>
<div class="grid grid-cols-2 items-center px-10 ">
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
@@ -168,10 +174,6 @@
value={destination.engine}
/>
</div>
<!-- <div class="flex items-center">
<label for="remoteEngine">Remote Engine?</label>
<input name="remoteEngine" type="checkbox" bind:checked={payload.remoteEngine} />
</div> -->
<div class="grid grid-cols-2 items-center px-10">
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
<CopyPasswordField
@@ -186,7 +188,7 @@
{#if $appSession.teamId === '0'}
<div class="grid grid-cols-2 items-center">
<Setting
loading={loadingProxy}
loading={loading.proxy}
disabled={cannotDisable}
bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting}

View File

@@ -21,10 +21,9 @@
payload = {
name: $t('sources.remote_docker'),
remoteEngine: true,
ipAddress: null,
user: 'root',
port: 22,
sshPrivateKey: null,
remoteIpAddress: null,
remoteUser: 'root',
remotePort: 22,
network: cuid(),
isCoolifyProxyUsed: true
};
@@ -44,7 +43,7 @@
<button class="w-32" on:click={() => setPredefined('localDocker')}
>{$t('sources.local_docker')}</button
>
<!-- <button class="w-32" on:click={() => setPredefined('remoteDocker')}>Remote Docker</button> -->
<button class="w-32" on:click={() => setPredefined('remoteDocker')}>Remote Docker</button>
<!-- <button class="w-32" on:click={() => setPredefined('kubernetes')}>Kubernetes</button> -->
</div>
</div>

View File

@@ -5,23 +5,35 @@
import { post } from '$lib/api';
import { errorNotification } from '$lib/common';
import Explainer from '$lib/components/Explainer.svelte';
import Setting from '$lib/components/Setting.svelte';
import { t } from '$lib/translations';
let loading = false;
async function handleSubmit() {
if (loading) return;
try {
const { id } = await post('/new/destination/docker', {
loading = true;
await post(`/destinations/check`, { network: payload.network });
const { id } = await post(`/destinations/new`, {
...payload
});
return await goto(`/destinations/${id}`);
} catch (error) {
return errorNotification(error);
} finally {
loading = false;
}
}
</script>
<div class="text-center flex justify-center">
<Explainer
customClass="max-w-[32rem]"
text="Remote Docker Engines are using <span class='text-white font-bold'>SSH connection</span> to initiate connection. You need to setup an <span class='text-white font-bold'>SSH key</span> in advance on the server and install Docker. <br>See <a class='text-white' href='https://docs.coollabs.io'>docs</a> for more details."
/>
</div>
<div class="flex justify-center px-6 pb-8">
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex items-center space-x-2 pb-5">
@@ -44,40 +56,36 @@
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="ipAddress" class="text-base font-bold text-stone-100"
<label for="remoteIpAddress" class="text-base font-bold text-stone-100"
>{$t('forms.ip_address')}</label
>
<input
required
name="ipAddress"
name="remoteIpAddress"
placeholder="{$t('forms.eg')}: 192.168..."
bind:value={payload.ipAddress}
bind:value={payload.remoteIpAddress}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="user" class="text-base font-bold text-stone-100">{$t('forms.user')}</label>
<input required name="user" placeholder="{$t('forms.eg')}: root" bind:value={payload.user} />
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="port" class="text-base font-bold text-stone-100">{$t('forms.port')}</label>
<input required name="port" placeholder="{$t('forms.eg')}: 22" bind:value={payload.port} />
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="sshPrivateKey" class="text-base font-bold text-stone-100"
>{$t('forms.ssh_private_key')}</label
>
<textarea
rows="10"
class="resize-none"
<label for="remoteUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label>
<input
required
name="sshPrivateKey"
placeholder="{$t('forms.eg')}: -----BEGIN...."
bind:value={payload.sshPrivateKey}
name="remoteUser"
placeholder="{$t('forms.eg')}: root"
bind:value={payload.remoteUser}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="remotePort" class="text-base font-bold text-stone-100">{$t('forms.port')}</label>
<input
required
name="remotePort"
placeholder="{$t('forms.eg')}: 22"
bind:value={payload.remotePort}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
<input

View File

@@ -1,80 +1,90 @@
<script lang="ts">
export let destination: any;
export let settings: any
export let state: any
export let settings: any;
import { toast } from '@zerodevx/svelte-toast';
import { page, session } from '$app/stores';
import Setting from '$lib/components/Setting.svelte';
import { post } from '$lib/api';
import { get, post } from '$lib/api';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { onMount } from 'svelte';
import { t } from '$lib/translations';
import { errorNotification, generateRemoteEngine } from '$lib/common';
import { appSession } from '$lib/store';
import { errorNotification } from '$lib/common';
import { appSession } from '$lib/store';
const { id } = $page.params;
let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock';
// let scannedApps = [];
let loading = false;
let restarting = false;
let loading = {
restart: false,
proxy: true,
save: false,
verify: false
};
$: isDisabled = !$appSession.isAdmin;
async function handleSubmit() {
loading = true;
loading.save = true;
try {
return await post(`/destinations/${id}.json`, { ...destination });
} catch (error ) {
await post(`/destinations/${id}`, { ...destination });
toast.push('Configuration saved.');
} catch (error) {
return errorNotification(error);
} finally {
loading = false;
loading.save = false;
}
}
// async function scanApps() {
// scannedApps = [];
// const data = await fetch(`/destinations/${id}/scan.json`);
// const { containers } = await data.json();
// scannedApps = containers;
// }
onMount(async () => {
if (state === false && destination.isCoolifyProxyUsed === true) {
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
try {
await post(`/destinations/${id}/settings.json`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
engine: destination.engine
});
await stopProxy();
} catch (error ) {
return errorNotification(error);
}
} else if (state === true && destination.isCoolifyProxyUsed === false) {
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
try {
await post(`/destinations/${id}/settings.json`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
engine: destination.engine
});
await startProxy();
} catch ( error ) {
return errorNotification(error);
loading.proxy = true;
if (destination.remoteEngine && destination.remoteVerified) {
const { isRunning } = await get(`/destinations/${id}/status`);
if (isRunning === false && destination.isCoolifyProxyUsed === true) {
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
try {
await post(`/destinations/${id}/settings`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
engine: destination.engine
});
await stopProxy();
} catch (error) {
return errorNotification(error);
}
} else if (isRunning === true && destination.isCoolifyProxyUsed === false) {
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
try {
await post(`/destinations/${id}/settings`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
engine: destination.engine
});
await startProxy();
} catch (error) {
return errorNotification(error);
}
}
}
loading.proxy = false;
});
async function changeProxySetting() {
loading.proxy = true;
if (!cannotDisable) {
const isProxyActivated = destination.isCoolifyProxyUsed;
if (isProxyActivated) {
const sure = confirm(
`Are you sure you want to ${
destination.isCoolifyProxyUsed ? 'disable' : 'enable'
} Coolify proxy? It will remove the proxy for all configured networks and all deployments on '${
destination.engine
}'! Nothing will be reachable if you do it!`
} Coolify proxy? It will remove the proxy for all configured networks and all deployments! Nothing will be reachable if you do it!`
);
if (!sure) return;
if (!sure) {
loading.proxy = false;
return;
}
}
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
let proxyUsed = !destination.isCoolifyProxyUsed;
try {
await post(`/destinations/${id}/settings.json`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
await post(`/destinations/${id}/settings`, {
isCoolifyProxyUsed: proxyUsed,
engine: destination.engine
});
if (isProxyActivated) {
@@ -82,15 +92,17 @@ import { appSession } from '$lib/store';
} else {
await startProxy();
}
destination.isCoolifyProxyUsed = proxyUsed;
} catch (error) {
return errorNotification(error);
} finally {
loading.proxy = false;
}
}
}
async function stopProxy() {
try {
const engine = generateRemoteEngine(destination);
await post(`/destinations/${id}/stop.json`, { engine });
await post(`/destinations/${id}/stop`, { engine: destination.engine });
return toast.push($t('destination.coolify_proxy_stopped'));
} catch (error) {
return errorNotification(error);
@@ -98,8 +110,7 @@ import { appSession } from '$lib/store';
}
async function startProxy() {
try {
const engine = generateRemoteEngine(destination);
await post(`/destinations/${id}/start.json`, { engine });
await post(`/destinations/${id}/start`, { engine: destination.engine });
return toast.push($t('destination.coolify_proxy_started'));
} catch (error) {
return errorNotification(error);
@@ -109,9 +120,9 @@ import { appSession } from '$lib/store';
const sure = confirm($t('destination.confirm_restart_proxy'));
if (sure) {
try {
restarting = true;
loading.restart = true;
toast.push($t('destination.coolify_proxy_restarting'));
await post(`/destinations/${id}/restart.json`, {
await post(`/destinations/${id}/restart`, {
engine: destination.engine,
fqdn: settings.fqdn
});
@@ -119,9 +130,24 @@ import { appSession } from '$lib/store';
setTimeout(() => {
window.location.reload();
}, 5000);
} finally {
loading.restart = false;
}
}
}
async function verifyRemoteDocker() {
try {
loading.verify = true;
await post(`/destinations/${id}/verify`, {});
destination.remoteVerified = true;
toast.push('Remote Docker Engine verified!');
return;
} catch (error) {
return errorNotification(error);
} finally {
loading.verify = false;
}
}
</script>
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
@@ -131,23 +157,28 @@ import { appSession } from '$lib/store';
<button
type="submit"
class="bg-sky-600 hover:bg-sky-500"
class:bg-sky-600={!loading}
class:hover:bg-sky-500={!loading}
disabled={loading}
>{loading ? $t('forms.saving') : $t('forms.save')}
class:bg-sky-600={!loading.save}
class:hover:bg-sky-500={!loading.save}
disabled={loading.save}
>{loading.save ? $t('forms.saving') : $t('forms.save')}
</button>
<button
class={restarting ? '' : 'bg-red-600 hover:bg-red-500'}
disabled={restarting}
on:click|preventDefault={forceRestartProxy}
>{restarting
? $t('destination.restarting_please_wait')
: $t('destination.force_restart_proxy')}</button
>
{#if !destination.remoteVerified}
<button
disabled={loading.verify}
on:click|preventDefault|stopPropagation={verifyRemoteDocker}
>{loading.verify ? 'Verifying...' : 'Verify Remote Docker Engine'}</button
>
{:else}
<button
class={loading.restart ? '' : 'bg-red-600 hover:bg-red-500'}
disabled={loading.restart}
on:click|preventDefault={forceRestartProxy}
>{loading.restart
? $t('destination.restarting_please_wait')
: $t('destination.force_restart_proxy')}</button
>
{/if}
{/if}
<!-- <button type="button" class="bg-coollabs hover:bg-coollabs-100" on:click={scanApps}
>Scan for applications</button
> -->
</div>
<div class="grid grid-cols-2 items-center px-10 ">
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
@@ -159,22 +190,6 @@ import { appSession } from '$lib/store';
bind:value={destination.name}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="engine" class="text-base font-bold text-stone-100">{$t('forms.engine')}</label>
<CopyPasswordField
id="engine"
readonly
disabled
name="engine"
placeholder="{$t('forms.eg')}: /var/run/docker.sock"
value={destination.engine}
/>
</div>
<!-- <div class="flex items-center">
<label for="remoteEngine">Remote Engine?</label>
<input name="remoteEngine" type="checkbox" bind:checked={payload.remoteEngine} />
</div> -->
<div class="grid grid-cols-2 items-center px-10">
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
<CopyPasswordField
@@ -186,9 +201,53 @@ import { appSession } from '$lib/store';
value={destination.network}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="remoteIpAddress" class="text-base font-bold text-stone-100">IP Address</label>
<CopyPasswordField
id="remoteIpAddress"
readonly
disabled
name="remoteIpAddress"
value={destination.remoteIpAddress}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="remoteUser" class="text-base font-bold text-stone-100">User</label>
<CopyPasswordField
id="remoteUser"
readonly
disabled
name="remoteUser"
value={destination.remoteUser}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="remotePort" class="text-base font-bold text-stone-100">Port</label>
<CopyPasswordField
id="remotePort"
readonly
disabled
name="remotePort"
value={destination.remotePort}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="sshKey" class="text-base font-bold text-stone-100">SSH Key</label>
<a
href={!isDisabled ? `/destinations/${id}/configuration/sshkey?from=/destinations/${id}` : ''}
class="no-underline"
><input
value={destination.sshKey.name}
readonly
id="sshKey"
class="cursor-pointer hover:bg-coolgray-500"
/></a
>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
disabled={cannotDisable}
loading={loading.proxy}
bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting}
title={$t('destination.use_coolify_proxy')}
@@ -200,27 +259,3 @@ import { appSession } from '$lib/store';
/>
</div>
</form>
<!-- <div class="flex justify-center">
{#if payload.isCoolifyProxyUsed}
{#if state}
<button on:click={stopProxy}>Stop proxy</button>
{:else}
<button on:click={startProxy}>Start proxy</button>
{/if}
{/if}
</div> -->
<!-- {#if scannedApps.length > 0}
<div class="flex justify-center px-6 pb-10">
<div class="flex space-x-2 h-8 items-center">
<div class="font-bold text-xl text-white">Found applications</div>
</div>
</div>
<div class="max-w-4xl mx-auto px-6">
<div class="flex space-x-2 justify-center">
{#each scannedApps as app}
<FoundApp {app} />
{/each}
</div>
</div>
{/if} -->

View File

@@ -1,27 +1,46 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, url, params }) => {
function checkConfiguration(destination: any): string | null {
let configurationPhase = null;
if (!destination?.remoteEngine) return configurationPhase;
if (!destination?.sshKey) {
configurationPhase = 'sshkey';
}
return configurationPhase;
}
export const load: Load = async ({ url, params }) => {
try {
const { id } = params;
const response = await get(`/destinations/${id}`);
const { destination, settings, state } = response;
const { destination, settings } = response;
if (id !== 'new' && (!destination || Object.entries(destination).length === 0)) {
return {
status: 302,
redirect: '/destinations'
};
}
const configurationPhase = checkConfiguration(destination);
if (
configurationPhase &&
url.pathname !== `/destinations/${params.id}/configuration/${configurationPhase}`
) {
return {
status: 302,
redirect: `/destinations/${params.id}/configuration/${configurationPhase}`
};
}
return {
props: {
destination
},
stuff: {
destination,
settings,
state
settings
}
};
} catch (error) {
console.log(error)
return handlerNotFoundLoad(error, url);
}
};

View File

@@ -0,0 +1,86 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, url, stuff }) => {
try {
const response = await get(`/settings`);
return {
props: {
...response
}
};
} catch (error) {
return {
status: 500,
error: new Error(`Could not load ${url}`)
};
}
};
</script>
<script lang="ts">
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { get, post } from '$lib/api';
import { errorNotification } from '$lib/common';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
export let sshKeys: any;
async function handleSubmit(sshKeyId: string) {
try {
await post(`/destinations/${id}/configuration/sshKey`, { id: sshKeyId });
return await goto(from || `/destinations/${id}`);
} catch (error) {
return errorNotification(error);
}
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a SSH Keys</div>
</div>
<div class="flex flex-col justify-center">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row ">
{#if sshKeys.length > 0}
{#each sshKeys as sshKey}
<div class="p-2 relative">
<form on:submit|preventDefault={() => handleSubmit(sshKey.id)}>
<button
type="submit"
class="disabled:opacity-95 bg-coolgray-200 disabled:text-white box-selection hover:bg-orange-700 group"
>
<div class="font-bold text-xl text-center truncate">{sshKey.name}</div>
</button>
</form>
</div>
{/each}
{:else}
<div class="flex-col">
<div class="pb-2 text-center font-bold">No SSH key found</div>
<div class="flex justify-center">
<a
href="/settings/ssh-keys"
sveltekit:prefetch
class="add-icon bg-sky-600 hover:bg-sky-500"
>
<svg
class="w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
>
</a>
</div>
</div>
{/if}
</div>
</div>

View File

@@ -39,7 +39,7 @@
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">{$t('index.destinations')}</div>
{#if $appSession.isAdmin}
<a href="/destinations/new" class="add-icon bg-sky-600 hover:bg-sky-500">
<a href="/destinations/new" class="add-icon bg-sky-600 hover:bg-sky-500">
<svg
class="w-6"
xmlns="http://www.w3.org/2000/svg"
@@ -66,24 +66,112 @@
<div class="flex flex-col">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each ownDestinations as destination}
<a href="/destinations/{destination.id}" class="w-96 p-2 no-underline">
<div class="box-selection hover:bg-sky-600">
<a href="/destinations/{destination.id}" class="p-2 no-underline relative">
<div class="box-selection hover:bg-sky-600 ">
<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-4 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="truncate text-center text-xl font-bold">{destination.name}</div>
{#if $appSession.teamId === '0' && otherDestinations.length > 0}
<div class="truncate text-center">{destination.teams[0].name}</div>
{/if}
<div class="truncate text-center">{destination.network}</div>
{#if $appSession.teamId === '0' && destination.remoteVerified === false && destination.remoteEngine}
<div class="truncate text-center text-sm text-red-500">Not verified yet</div>
{/if}
{#if destination.remoteEngine && !destination.sshKeyId}
<div class="truncate text-center text-sm text-red-500">SSH Key missing!</div>
{/if}
</div>
</a>
{/each}
</div>
{#if otherDestinations.length > 0 && $appSession.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Destinations</div>
<div class="px-6 pb-5 pt-10 text-2xl font-bold text-center">Other Destinations</div>
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherDestinations as destination}
<a href="/destinations/{destination.id}" class="w-96 p-2 no-underline">
<a href="/destinations/{destination.id}" class="p-2 no-underline relative">
<div class="box-selection hover:bg-sky-600">
<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-4 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="truncate text-center text-xl font-bold">{destination.name}</div>
{#if $appSession.teamId === '0'}
<div class="truncate text-center">{destination.teams[0].name}</div>

View File

@@ -189,54 +189,52 @@
<div class="mx-auto max-w-4xl px-6">
<div class="title font-bold">Teams</div>
<div class="flex items-center justify-center pt-10">
<div class="flex flex-col">
<div class="flex flex-row flex-wrap justify-center px-2 pb-10 md:flex-row">
{#each ownTeams as team}
<a href="/iam/team/{team.id}" class="w-96 p-2 no-underline">
<div class="box-selection relative">
<div>
<div class="truncate text-center text-xl font-bold">
{team.name}
</div>
<div class="mt-1 text-center text-xs">
{team.permissions?.length} member(s)
</div>
<div class="flex-col items-center justify-center pt-10">
<div class="flex flex-row flex-wrap justify-center px-2 pb-10 md:flex-row">
{#each ownTeams as team}
<a href="/iam/team/{team.id}" class="p-2 no-underline">
<div class="box-selection relative">
<div>
<div class="truncate text-center text-xl font-bold">
{team.name}
</div>
<div class="flex items-center justify-center pt-3">
<button
on:click|preventDefault={() => switchTeam(team.id)}
class:bg-fuchsia-600={$appSession.teamId !== team.id}
class:hover:bg-fuchsia-500={$appSession.teamId !== team.id}
class:bg-transparent={$appSession.teamId === team.id}
disabled={$appSession.teamId === team.id}
>{$appSession.teamId === team.id ? 'Current Team' : 'Switch Team'}</button
>
<div class="mt-1 text-center text-xs">
{team.permissions?.length} member(s)
</div>
</div>
<div class="flex items-center justify-center pt-3">
<button
on:click|preventDefault={() => switchTeam(team.id)}
class:bg-fuchsia-600={$appSession.teamId !== team.id}
class:hover:bg-fuchsia-500={$appSession.teamId !== team.id}
class:bg-transparent={$appSession.teamId === team.id}
disabled={$appSession.teamId === team.id}
>{$appSession.teamId === team.id ? 'Current Team' : 'Switch Team'}</button
>
</div>
</div>
</a>
{/each}
</div>
{#if $appSession.teamId === '0' && allTeams.length > 0}
<div class="pb-5 pt-10 text-xl font-bold">Other Teams</div>
<div class="flex flex-row flex-wrap justify-center px-2 md:flex-row">
{#each allTeams as team}
<a href="/iam/team/{team.id}" class="p-2 no-underline">
<div
class="box-selection relative"
class:hover:bg-fuchsia-600={team.id !== '0'}
class:hover:bg-red-500={team.id === '0'}
>
<div class="truncate text-center text-xl font-bold">
{team.name}
</div>
<div class="mt-1 text-center">{team.permissions?.length} member(s)</div>
</div>
</a>
{/each}
</div>
{#if $appSession.teamId === '0' && allTeams.length > 0}
<div class="pb-5 pt-10 text-xl font-bold">Other Teams</div>
<div class="flex flex-row flex-wrap justify-center px-2 md:flex-row">
{#each allTeams as team}
<a href="/iam/team/{team.id}" class="w-96 p-2 no-underline">
<div
class="box-selection relative"
class:hover:bg-fuchsia-600={team.id !== '0'}
class:hover:bg-red-500={team.id === '0'}
>
<div class="truncate text-center text-xl font-bold">
{team.name}
</div>
<div class="mt-1 text-center">{team.permissions?.length} member(s)</div>
</div>
</a>
{/each}
</div>
{/if}
</div>
{/if}
</div>
</div>

View File

@@ -121,10 +121,10 @@
<div class="flex space-x-2 h-8 items-center justify-center pt-8">
<button
type="submit"
class="hover:bg-coollabs-100 text-white"
class="text-white"
disabled={loading}
class:hover:bg-coollabs-100={!loading}
class:bg-transparent={loading}
class:text-stone-600={loading}
class:bg-coollabs={!loading}
>{loading ? $t('register.registering') : $t('register.register')}</button
>

View File

@@ -11,7 +11,7 @@
import { toast } from '@zerodevx/svelte-toast';
import { get, post } from '$lib/api';
import { errorNotification } from '$lib/common';
import { errorNotification, getDomain } from '$lib/common';
import { t } from '$lib/translations';
import { appSession, disabledButton, status, location, setLocation } from '$lib/store';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
@@ -30,25 +30,63 @@
import Moodle from './_Moodle.svelte';
const { id } = $page.params;
$: isDisabled =
!$appSession.isAdmin || $status.service.isRunning || $status.service.initialLoading;
let forceSave = false;
let loading = false;
let loadingVerification = 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}`);
toast.push('DNS configuration is valid.');
isWWW ? (isWWWDomainOK = true) : (isNonWWWDomainOK = true);
return true;
} catch (error) {
errorNotification(error);
isWWW ? (isWWWDomainOK = false) : (isNonWWWDomainOK = false);
return false;
}
}
async function handleSubmit() {
if (loading) return;
loading = 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)
setLocation(service);
$disabledButton = false;
forceSave = false;
toast.push('Configuration saved.');
} 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 = false;
@@ -102,14 +140,21 @@
<div class="mx-auto max-w-4xl px-6 pb-12">
<form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-5 font-bold">
<div class="flex space-x-1 pb-5">
<div class="title">{$t('general')}</div>
{#if $appSession.isAdmin}
<button
type="submit"
class:bg-pink-600={!loading}
class:bg-orange-600={forceSave}
class:hover:bg-pink-500={!loading}
disabled={loading}>{loading ? $t('forms.saving') : $t('forms.save')}</button
class:hover:bg-orange-400={forceSave}
disabled={loading}
>{loading
? $t('forms.saving')
: forceSave
? $t('forms.confirm_continue')
: $t('forms.save')}</button
>
{/if}
{#if service.type === 'plausibleanalytics' && $status.service.isRunning}
@@ -145,7 +190,7 @@
<div class="grid grid-cols-2 items-center px-10">
<label for="version" class="text-base font-bold text-stone-100">Version / Tag</label>
<a
href={$appSession.isAdmin && !$status.service.isRunning
href={$appSession.isAdmin && !$status.service.isRunning && !$status.service.initialLoading
? `/services/${id}/configuration/version?from=/services/${id}`
: ''}
class="no-underline"
@@ -153,7 +198,8 @@
<input
value={service.version}
id="service"
disabled={$status.service.isRunning}
readonly
disabled={$status.service.isRunning || $status.service.initialLoading}
class:cursor-pointer={!$status.service.isRunning}
/></a
>
@@ -183,8 +229,8 @@
<CopyPasswordField
placeholder="eg: https://console.min.io"
readonly={!$appSession.isAdmin && !$status.service.isRunning}
disabled={!$appSession.isAdmin || $status.service.isRunning}
readonly={isDisabled}
disabled={isDisabled}
name="fqdn"
id="fqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
@@ -201,7 +247,7 @@
<CopyPasswordField
placeholder="eg: https://min.io"
readonly={!$appSession.isAdmin && !$status.service.isRunning}
disabled={!$appSession.isAdmin || $status.service.isRunning}
disabled={isDisabled}
name="apiFqdn"
id="apiFqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
@@ -221,7 +267,9 @@
<CopyPasswordField
placeholder="eg: https://analytics.coollabs.io"
readonly={!$appSession.isAdmin && !$status.service.isRunning}
disabled={!$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,{'}'}$"
@@ -230,7 +278,38 @@
/>
</div>
{/if}
{#if forceSave}
<div class="flex-col space-y-2 pt-4 text-center">
{#if isNonWWWDomainOK}
<button
class="bg-green-600 hover:bg-green-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="bg-red-600 hover:bg-red-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{#if dualCerts}
{#if isWWWDomainOK}
<button
class="bg-green-600 hover:bg-green-500"
on:click|preventDefault={() => isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="bg-red-600 hover:bg-red-500"
on:click|preventDefault={() => isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{/if}
</div>
{/if}
<div class="grid grid-cols-2 items-center px-10">
<Setting
disabled={$status.service.isRunning}
@@ -245,7 +324,9 @@
<label for="exposePort" class="text-base font-bold text-stone-100">Exposed Port</label>
<input
readonly={!$appSession.isAdmin && !$status.service.isRunning}
disabled={!$appSession.isAdmin || $status.service.isRunning}
disabled={!$appSession.isAdmin ||
$status.service.isRunning ||
$status.service.initialLoading}
name="exposePort"
id="exposePort"
bind:value={service.exposePort}

View File

@@ -76,13 +76,13 @@
<label for="extraConfig">{$t('forms.extra_config')}</label>
<textarea
bind:value={service.wordpress.extraConfig}
disabled={$status.service.isRunning}
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
placeholder={!$status.service.isRunning && !$status.service.initialLoading
? `${$t('forms.eg')}:
define('WP_ALLOW_MULTISITE', true);
@@ -112,7 +112,14 @@ define('SUBDOMAIN_INSTALL', false);`
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="ftpPassword">Password</label>
<CopyPasswordField id="ftpPassword" isPasswordField readonly disabled name="ftpPassword" value={ftpPassword} />
<CopyPasswordField
id="ftpPassword"
isPasswordField
readonly
disabled
name="ftpPassword"
value={ftpPassword}
/>
</div>
{/if}
<div class="flex space-x-1 py-5 font-bold">

View File

@@ -120,8 +120,9 @@
async function getStatus() {
if ($status.service.loading) return;
$status.service.loading = true;
const data = await get(`/services/${id}`);
const data = await get(`/services/${id}/status`);
$status.service.isRunning = data.isRunning;
$status.service.isExited = data.isExited;
$status.service.initialLoading = false;
$status.service.loading = false;
}
@@ -173,6 +174,32 @@
>
<div class="border border-stone-700 h-8" />
{/if}
{#if $status.service.isExited}
<a
href={!$disabledButton ? `/services/${id}/logs` : null}
class=" icons bg-transparent tooltip-bottom text-sm flex items-center text-red-500 tooltip-red-500"
data-tooltip="Service exited with an error!"
sveltekit:prefetch
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentcolor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M8.7 3h6.6c.3 0 .5 .1 .7 .3l4.7 4.7c.2 .2 .3 .4 .3 .7v6.6c0 .3 -.1 .5 -.3 .7l-4.7 4.7c-.2 .2 -.4 .3 -.7 .3h-6.6c-.3 0 -.5 -.1 -.7 -.3l-4.7 -4.7c-.2 -.2 -.3 -.4 -.3 -.7v-6.6c0 -.3 .1 -.5 .3 -.7l4.7 -4.7c.2 -.2 .4 -.3 .7 -.3z"
/>
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
</a>
{/if}
{#if $status.service.initialLoading}
<button
class="icons tooltip-bottom flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out"
@@ -347,7 +374,7 @@
>
<div class="border border-stone-700 h-8" />
<a
href={$status.service.isRunning ? `/services/${id}/logs` : null}
href={!$disabledButton && $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`}

View File

@@ -9,7 +9,7 @@
redirect: `/services/${params.id}`
};
}
const response = await get(`/destinations`);
const response = await get(`/destinations?onlyVerified=true`);
return {
props: {
...response
@@ -54,7 +54,7 @@
<div class="flex justify-center">
{#if !destinations || destinations.length === 0}
<div class="flex-col">
<div class="pb-2">{$t('application.configuration.no_configurable_destination')}</div>
<div class="pb-2 text-center font-bold">{$t('application.configuration.no_configurable_destination')}</div>
<div class="flex justify-center">
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
<svg

View File

@@ -1,32 +1,13 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
import { onDestroy, onMount } from 'svelte';
export const load: Load = async ({ fetch, params, url, stuff }) => {
try {
const response = await get(`/services/${params.id}/logs`);
return {
props: {
service: stuff.service,
...response
}
};
} catch (error) {
return {
status: 500,
error: new Error(`Could not load ${url}`)
};
}
};
</script>
<script lang="ts">
export let service: any;
import { page } from '$app/stores';
import LoadingLogs from './_Loading.svelte';
import { get } from '$lib/api';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
import { errorNotification } from '$lib/common';
import { onDestroy, onMount } from 'svelte';
let service: any = {};
let logsLoading = false;
let loadLogsInterval: any = null;
let logs: any = [];
let lastLog: any = null;
@@ -36,18 +17,23 @@ import { errorNotification } from '$lib/common';
let position = 0;
const { id } = $page.params;
onMount(async () => {
const response = await get(`/services/${id}`);
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];
@@ -56,13 +42,14 @@ import { errorNotification } from '$lib/common';
} catch (error) {
console.log(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?since=${lastLog?.split(' ')[0] || 0}`);
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
logs = logs.concat(newLogs.logs);
@@ -137,7 +124,7 @@ import { errorNotification } from '$lib/common';
{#if loadLogsInterval}
<LoadingLogs />
{/if}
<div class="flex justify-end sticky top-0 p-2 mx-1">
<div class="flex justify-end sticky top-0 p-1 mx-1">
<button
on:click={followBuild}
class="bg-transparent"

View File

@@ -73,7 +73,7 @@
<div class="flex flex-col">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each ownServices as service}
<a href="/services/{service.id}" class="w-96 p-2 no-underline">
<a href="/services/{service.id}" class=" p-2 no-underline">
<div class="box-selection group relative hover:bg-pink-600">
<Services type={service.type} />
<div class="truncate text-center text-xl font-bold">
@@ -85,6 +85,9 @@
{#if service.fqdn}
<div class="truncate text-center">{getDomain(service.fqdn) || ''}</div>
{/if}
{#if service.destinationDocker?.name}
<div class="truncate text-center">{service.destinationDocker.name}</div>
{/if}
{#if !service.type || !service.fqdn}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
{$t('application.configuration.configuration_missing')}
@@ -95,10 +98,10 @@
{/each}
</div>
{#if otherServices.length > 0 && $appSession.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Services</div>
<div class="px-6 pb-5 pt-10 text-2xl font-bold text-center">Other Services</div>
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherServices as service}
<a href="/services/{service.id}" class="w-96 p-2 no-underline">
<a href="/services/{service.id}" class="p-2 no-underline">
<div class="box-selection group relative hover:bg-pink-600">
<Services type={service.type} />
<div class="truncate text-center text-xl font-bold">

View File

@@ -0,0 +1,21 @@
<script lang="ts">
import { page } from '$app/stores';
import { appSession } from '$lib/store';
</script>
<div class="flex flex-col pt-4 space-y-6 w-96 px-20">
{#if $appSession.teamId === '0'}
<a
href="/settings/global"
class="sub-menu no-underline"
class:sub-menu-active={$page.routeId === 'settings/global'}
>
Global Settings
</a>
{/if}
<a
href="/settings/ssh-keys"
class="sub-menu no-underline"
class:sub-menu-active={$page.routeId === 'settings/ssh-keys'}>SSH Keys</a
>
</div>

View File

@@ -0,0 +1,23 @@
<script context="module" lang="ts">
import { get } from '$lib/api';
import type { Load } from '@sveltejs/kit';
export const load: Load = async () => {
try {
const response = await get(`/settings`);
return {
stuff: {
...response
}
};
} catch (error: any) {
return {
status: 500,
error: new Error(error)
};
}
};
</script>
<slot />

View File

@@ -0,0 +1,302 @@
<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 settings: any;
import Setting from '$lib/components/Setting.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { del, get, post } from '$lib/api';
import { browser } from '$app/env';
import { toast } from '@zerodevx/svelte-toast';
import { t } from '$lib/translations';
import { appSession, features } from '$lib/store';
import { errorNotification, getDomain } from '$lib/common';
import Menu from './_Menu.svelte';
let isRegistrationEnabled = settings.isRegistrationEnabled;
let dualCerts = settings.dualCerts;
let isAutoUpdateEnabled = settings.isAutoUpdateEnabled;
let isDNSCheckEnabled = settings.isDNSCheckEnabled;
let minPort = settings.minPort;
let maxPort = settings.maxPort;
let forceSave = false;
let fqdn = settings.fqdn;
let nonWWWDomain = fqdn && getDomain(fqdn).replace(/^www\./, '');
let isNonWWWDomainOK = false;
let isWWWDomainOK = false;
let isFqdnSet = !!settings.fqdn;
let loading = {
save: false,
remove: false,
proxyMigration: false
};
async function removeFqdn() {
if (fqdn) {
loading.remove = true;
try {
const { redirect } = await del(`/settings`, { fqdn });
return redirect ? window.location.replace(redirect) : window.location.reload();
} catch (error) {
return errorNotification(error);
} finally {
loading.remove = false;
}
}
}
async function changeSettings(name: any) {
try {
resetView();
if (name === 'isRegistrationEnabled') {
isRegistrationEnabled = !isRegistrationEnabled;
}
if (name === 'dualCerts') {
dualCerts = !dualCerts;
}
if (name === 'isAutoUpdateEnabled') {
isAutoUpdateEnabled = !isAutoUpdateEnabled;
}
if (name === 'isDNSCheckEnabled') {
isDNSCheckEnabled = !isDNSCheckEnabled;
}
await post(`/settings`, {
isRegistrationEnabled,
dualCerts,
isAutoUpdateEnabled,
isDNSCheckEnabled
});
return toast.push(t.get('application.settings_saved'));
} catch (error) {
return errorNotification(error);
}
}
async function handleSubmit() {
try {
loading.save = true;
nonWWWDomain = fqdn && getDomain(fqdn).replace(/^www\./, '');
if (fqdn !== settings.fqdn) {
await post(`/settings/check`, { fqdn, forceSave, dualCerts, isDNSCheckEnabled });
await post(`/settings`, { fqdn });
return window.location.reload();
}
if (minPort !== settings.minPort || maxPort !== settings.maxPort) {
await post(`/settings`, { minPort, maxPort });
settings.minPort = minPort;
settings.maxPort = maxPort;
}
forceSave = false;
toast.push('Configuration saved.');
} catch (error) {
if (error.message?.startsWith($t('application.dns_not_set_partial_error'))) {
forceSave = true;
if (dualCerts) {
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
} else {
const isWWW = getDomain(settings.fqdn).includes('www.');
if (isWWW) {
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
} else {
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
}
}
}
console.log(error);
return errorNotification(error);
} finally {
loading.save = false;
}
}
async function isDNSValid(domain: any, isWWW: any) {
try {
await get(`/settings/check?domain=${domain}`);
toast.push('DNS configuration is valid.');
isWWW ? (isWWWDomainOK = true) : (isNonWWWDomainOK = true);
return true;
} catch (error) {
errorNotification(error);
isWWW ? (isWWWDomainOK = false) : (isNonWWWDomainOK = false);
return false;
}
}
function resetView() {
forceSave = false;
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">{$t('index.settings')}</div>
</div>
<div class="mx-auto w-full">
<div class="flex flex-row">
<Menu />
<div>
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex space-x-1 pb-6">
<div class="title font-bold">{$t('index.global_settings')}</div>
<button
type="submit"
class:bg-yellow-500={!loading.save}
class:bg-orange-600={forceSave}
class:hover:bg-yellow-500={!loading.save}
class:hover:bg-orange-400={forceSave}
disabled={loading.save}
>{loading.save
? $t('forms.saving')
: forceSave
? $t('forms.confirm_continue')
: $t('forms.save')}</button
>
{#if isFqdnSet}
<button
on:click|preventDefault={removeFqdn}
disabled={loading.remove}
class:bg-red-600={!loading.remove}
class:hover:bg-red-500={!loading.remove}
>{loading.remove ? $t('forms.removing') : $t('forms.remove_domain')}</button
>
{/if}
</div>
<div class="grid grid-flow-row gap-2 px-10">
<!-- <Language /> -->
<div class="grid grid-cols-2 items-start">
<div class="flex-col">
<div class="pt-2 text-base font-bold text-stone-100">
{$t('application.url_fqdn')}
</div>
<Explainer text={$t('setting.ssl_explainer')} />
</div>
<div class="justify-start text-left">
<input
bind:value={fqdn}
readonly={!$appSession.isAdmin || isFqdnSet}
disabled={!$appSession.isAdmin || isFqdnSet}
on:input={resetView}
name="fqdn"
id="fqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="{$t('forms.eg')}: https://coolify.io"
/>
{#if forceSave}
<div class="flex-col space-y-2 pt-4 text-center">
{#if isNonWWWDomainOK}
<button
class="bg-green-600 hover:bg-green-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="bg-red-600 hover:bg-red-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{#if dualCerts}
{#if isWWWDomainOK}
<button
class="bg-green-600 hover:bg-green-500"
on:click|preventDefault={() =>
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="bg-red-600 hover:bg-red-500"
on:click|preventDefault={() =>
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{/if}
</div>
{/if}
</div>
</div>
<div class="grid grid-cols-2 items-start py-6">
<div class="flex-col">
<div class="pt-2 text-base font-bold text-stone-100">
{$t('forms.public_port_range')}
</div>
<Explainer text={$t('forms.public_port_range_explainer')} />
</div>
<div class="mx-auto flex-row items-center justify-center space-y-2">
<input
class="h-8 w-20 px-2"
type="number"
bind:value={minPort}
min="1024"
max={maxPort}
/>
-
<input
class="h-8 w-20 px-2"
type="number"
bind:value={maxPort}
min={minPort}
max="65543"
/>
</div>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={isDNSCheckEnabled}
title={$t('setting.is_dns_check_enabled')}
description={$t('setting.is_dns_check_enabled_explainer')}
on:click={() => changeSettings('isDNSCheckEnabled')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
dataTooltip={$t('setting.must_remove_domain_before_changing')}
disabled={isFqdnSet}
bind:setting={dualCerts}
title={$t('application.ssl_www_and_non_www')}
description={$t('setting.generate_www_non_www_ssl')}
on:click={() => !isFqdnSet && changeSettings('dualCerts')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={isRegistrationEnabled}
title={$t('setting.registration_allowed')}
description={$t('setting.registration_allowed_explainer')}
on:click={() => changeSettings('isRegistrationEnabled')}
/>
</div>
{#if browser && $features.beta}
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={isAutoUpdateEnabled}
title={$t('setting.auto_update_enabled')}
description={$t('setting.auto_update_enabled_explainer')}
on:click={() => changeSettings('isAutoUpdateEnabled')}
/>
</div>
{/if}
</div>
</form>
</div>
</div>
</div>

View File

@@ -1,357 +1,8 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async () => {
try {
const response = await get(`/settings`);
return {
props: {
...response
}
};
} catch (error: any) {
return {
status: 500,
error: new Error(error)
};
}
};
</script>
<script lang="ts">
export let settings: any;
import Setting from '$lib/components/Setting.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { del, get, post } from '$lib/api';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { browser } from '$app/env';
import { toast } from '@zerodevx/svelte-toast';
import { t } from '$lib/translations';
import { appSession, features, isTraefikUsed } from '$lib/store';
import { errorNotification, getDomain } from '$lib/common';
let isRegistrationEnabled = settings.isRegistrationEnabled;
let dualCerts = settings.dualCerts;
let isAutoUpdateEnabled = settings.isAutoUpdateEnabled;
let isDNSCheckEnabled = settings.isDNSCheckEnabled;
$isTraefikUsed = settings.isTraefikUsed;
let minPort = settings.minPort;
let maxPort = settings.maxPort;
let forceSave = false;
let fqdn = settings.fqdn;
let nonWWWDomain = fqdn && getDomain(fqdn).replace(/^www\./, '');
let isNonWWWDomainOK = false;
let isWWWDomainOK = false;
let isFqdnSet = !!settings.fqdn;
let loading = {
save: false,
remove: false,
proxyMigration: false
};
async function removeFqdn() {
if (fqdn) {
loading.remove = true;
try {
const { redirect } = await del(`/settings`, { fqdn });
return redirect ? window.location.replace(redirect) : window.location.reload();
} catch (error ) {
return errorNotification(error);
} finally {
loading.remove = false;
}
}
}
async function changeSettings(name: any) {
try {
resetView();
if (name === 'isRegistrationEnabled') {
isRegistrationEnabled = !isRegistrationEnabled;
}
if (name === 'dualCerts') {
dualCerts = !dualCerts;
}
if (name === 'isAutoUpdateEnabled') {
isAutoUpdateEnabled = !isAutoUpdateEnabled;
}
if (name === 'isDNSCheckEnabled') {
isDNSCheckEnabled = !isDNSCheckEnabled;
}
await post(`/settings`, {
isRegistrationEnabled,
dualCerts,
isAutoUpdateEnabled,
isDNSCheckEnabled
});
return toast.push(t.get('application.settings_saved'));
} catch (error) {
return errorNotification(error);
}
}
async function handleSubmit() {
try {
loading.save = true;
nonWWWDomain = fqdn && getDomain(fqdn).replace(/^www\./, '');
if (fqdn !== settings.fqdn) {
await post(`/settings/check`, { fqdn, forceSave, dualCerts, isDNSCheckEnabled });
await post(`/settings`, { fqdn });
return window.location.reload();
}
if (minPort !== settings.minPort || maxPort !== settings.maxPort) {
await post(`/settings`, { minPort, maxPort });
settings.minPort = minPort;
settings.maxPort = maxPort;
}
forceSave = false;
} catch (error ) {
if (error.message?.startsWith($t('application.dns_not_set_partial_error'))) {
forceSave = true;
if (dualCerts) {
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
} else {
const isWWW = getDomain(settings.fqdn).includes('www.');
if (isWWW) {
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
} else {
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
}
}
}
console.log(error)
return errorNotification(error);
} finally {
loading.save = false;
}
}
async function isDNSValid(domain: any, isWWW: any) {
try {
await get(`/settings/check?domain=${domain}`);
toast.push('DNS configuration is valid.');
isWWW ? (isWWWDomainOK = true) : (isNonWWWDomainOK = true);
return true;
} catch (error) {
errorNotification(error);
isWWW ? (isWWWDomainOK = false) : (isNonWWWDomainOK = false);
return false;
}
}
function resetView() {
forceSave = false;
}
async function migrateProxy(to: any) {
if (loading.proxyMigration) return;
try {
loading.proxyMigration = true;
await post(`/update`, { type: to });
const data = await get(`/settings`);
$isTraefikUsed = data.settings.isTraefikUsed;
return toast.push('Proxy migration started, it takes a few seconds.');
} catch (error) {
return errorNotification(error);
} finally {
loading.proxyMigration = false;
}
import { goto } from '$app/navigation';
import { appSession } from '$lib/store';
if ($appSession.teamId !== '0') {
goto('/settings/ssh-keys');
}
goto('/settings/global');
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">{$t('index.settings')}</div>
</div>
{#if $appSession.teamId === '0'}
<div class="mx-auto max-w-4xl px-6">
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex space-x-1 pb-6">
<div class="title font-bold">{$t('index.global_settings')}</div>
<button
type="submit"
class:bg-green-600={!loading.save}
class:bg-orange-600={forceSave}
class:hover:bg-green-500={!loading.save}
class:hover:bg-orange-400={forceSave}
disabled={loading.save}
>{loading.save
? $t('forms.saving')
: forceSave
? $t('forms.confirm_continue')
: $t('forms.save')}</button
>
{#if isFqdnSet}
<button
on:click|preventDefault={removeFqdn}
disabled={loading.remove}
class:bg-red-600={!loading.remove}
class:hover:bg-red-500={!loading.remove}
>{loading.remove ? $t('forms.removing') : $t('forms.remove_domain')}</button
>
{/if}
</div>
<div class="grid grid-flow-row gap-2 px-10">
<!-- <Language /> -->
<div class="grid grid-cols-2 items-start">
<div class="flex-col">
<div class="pt-2 text-base font-bold text-stone-100">
{$t('application.url_fqdn')}
</div>
<Explainer text={$t('setting.ssl_explainer')} />
</div>
<div class="justify-start text-left">
<input
bind:value={fqdn}
readonly={!$appSession.isAdmin || isFqdnSet}
disabled={!$appSession.isAdmin || isFqdnSet}
on:input={resetView}
name="fqdn"
id="fqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="{$t('forms.eg')}: https://coolify.io"
/>
{#if forceSave}
<div class="flex-col space-y-2 pt-4 text-center">
{#if isNonWWWDomainOK}
<button
class="bg-green-600 hover:bg-green-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="bg-red-600 hover:bg-red-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{#if dualCerts}
{#if isWWWDomainOK}
<button
class="bg-green-600 hover:bg-green-500"
on:click|preventDefault={() =>
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="bg-red-600 hover:bg-red-500"
on:click|preventDefault={() =>
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{/if}
</div>
{/if}
</div>
</div>
<div class="grid grid-cols-2 items-start py-6">
<div class="flex-col">
<div class="pt-2 text-base font-bold text-stone-100">
{$t('forms.public_port_range')}
</div>
<Explainer text={$t('forms.public_port_range_explainer')} />
</div>
<div class="mx-auto flex-row items-center justify-center space-y-2">
<input
class="h-8 w-20 px-2"
type="number"
bind:value={minPort}
min="1024"
max={maxPort}
/>
-
<input
class="h-8 w-20 px-2"
type="number"
bind:value={maxPort}
min={minPort}
max="65543"
/>
</div>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={isDNSCheckEnabled}
title={$t('setting.is_dns_check_enabled')}
description={$t('setting.is_dns_check_enabled_explainer')}
on:click={() => changeSettings('isDNSCheckEnabled')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
dataTooltip={$t('setting.must_remove_domain_before_changing')}
disabled={isFqdnSet}
bind:setting={dualCerts}
title={$t('application.ssl_www_and_non_www')}
description={$t('setting.generate_www_non_www_ssl')}
on:click={() => !isFqdnSet && changeSettings('dualCerts')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={isRegistrationEnabled}
title={$t('setting.registration_allowed')}
description={$t('setting.registration_allowed_explainer')}
on:click={() => changeSettings('isRegistrationEnabled')}
/>
</div>
{#if browser && $features.beta}
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={isAutoUpdateEnabled}
title={$t('setting.auto_update_enabled')}
description={$t('setting.auto_update_enabled_explainer')}
on:click={() => changeSettings('isAutoUpdateEnabled')}
/>
</div>
{/if}
</div>
</form>
{#if !settings.isTraefikUsed}
<div class="flex space-x-1 pt-6 font-bold">
<div class="title">{$t('setting.coolify_proxy_settings')}</div>
</div>
<Explainer
text={$t('setting.credential_stat_explainer', {
link: fqdn
? `http://${settings.proxyUser}:${settings.proxyPassword}@` + getDomain(fqdn) + ':8404'
: browser &&
`http://${settings.proxyUser}:${settings.proxyPassword}@` +
window.location.hostname +
':8404'
})}
/>
<div class="space-y-2 px-10 py-5">
<div class="grid grid-cols-2 items-center">
<label for="proxyUser">{$t('forms.user')}</label>
<CopyPasswordField
readonly
disabled
id="proxyUser"
name="proxyUser"
value={settings.proxyUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="proxyPassword">{$t('forms.password')}</label>
<CopyPasswordField
readonly
disabled
id="proxyPassword"
name="proxyPassword"
isPasswordField
value={settings.proxyPassword}
/>
</div>
</div>
{/if}
</div>
{:else}
<div class="mx-auto max-w-4xl px-6">
<!-- <Language /> -->
</div>
{/if}

View File

@@ -0,0 +1,150 @@
<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 sshKeys: any;
import { del, post } from '$lib/api';
import { toast } from '@zerodevx/svelte-toast';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
import Menu from './_Menu.svelte';
let loading = {
save: false
};
let isModalActive = false;
let newSSHKey = {
name: null,
privateKey: null
};
async function handleSubmit() {
try {
await post(`/settings/sshKey`, { ...newSSHKey });
return window.location.reload();
} catch (error) {
errorNotification(error);
return false;
}
}
async function deleteSSHKey(id: string) {
const sure = confirm('Are you sure you would like to delete this SSH key?');
if (sure) {
try {
if (!id) return;
await del(`/settings/sshKey`, { id });
return window.location.reload();
} catch (error) {
errorNotification(error);
return false;
}
}
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">{$t('index.settings')}</div>
</div>
<div class="mx-auto w-full">
<div class="flex flex-row">
<Menu />
<div class="grid grid-flow-row gap-2 py-4">
<div class="flex space-x-1 pb-6">
<div class="title font-bold">SSH Keys</div>
<button
on:click={() => (isModalActive = true)}
class:bg-yellow-500={!loading.save}
class:hover:bg-yellow-400={!loading.save}
disabled={loading.save}>New SSH Key</button
>
</div>
<div class="grid grid-flow-col gap-2 px-10">
{#if sshKeys.length === 0}
<div class="text-sm ">No SSH keys found</div>
{:else}
{#each sshKeys as key}
<div class="box-selection group relative">
<div class="text-xl font-bold">{key.name}</div>
<div class="py-3 text-stone-600">Added on {key.createdAt}</div>
<button on:click={() => deleteSSHKey(key.id)} class="bg-red-500">Delete</button>
</div>
{/each}
{/if}
</div>
</div>
</div>
</div>
{#if isModalActive}
<div class="relative z-10" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="fixed inset-0 bg-coolgray-500 bg-opacity-75 transition-opacity" />
<div class="fixed z-10 inset-0 overflow-y-auto text-white">
<div class="flex items-end sm:items-center justify-center min-h-full p-4 text-center sm:p-0">
<form
on:submit|preventDefault={handleSubmit}
class="relative bg-coolblack rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:max-w-lg sm:w-full sm:p-6 border border-coolgray-500"
>
<div class="hidden sm:block absolute top-0 right-0 pt-4 pr-4">
<button
on:click={() => (isModalActive = false)}
type="button"
class=" rounded-md text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<span class="sr-only">Close</span>
<svg
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium pb-4" id="modal-title">New SSH Key</h3>
<div class="text-xs text-stone-400">Add an SSH key to your Coolify instance.</div>
<div class="mt-2">
<label for="privateKey" class="pb-2">Key</label>
<textarea
id="privateKey"
required
bind:value={newSSHKey.privateKey}
class="w-full"
rows={15}
/>
</div>
<div class="mt-2">
<label for="name" class="pb-2">Name</label>
<input id="name" required bind:value={newSSHKey.name} class="w-full" />
</div>
</div>
</div>
<div class="mt-5 flex space-x-4 justify-end">
<button type="submit" class="bg-green-600 hover:bg-green-500">Save</button>
<button on:click={() => (isModalActive = false)} type="button" class="">Cancel</button>
</div>
</form>
</div>
</div>
</div>
{/if}

View File

@@ -66,11 +66,7 @@
});
const { organization, htmlUrl } = source;
const { fqdn } = settings;
const host = dev
? getAPIUrl()
: fqdn
? fqdn
: `http://${window.location.host}` || '';
const host = dev ? getAPIUrl() : fqdn ? fqdn : `http://${window.location.host}` || '';
const domain = getDomain(fqdn);
let url = 'settings/apps/new';
@@ -80,9 +76,7 @@
name: `coolify-${name}`,
url: host,
hook_attributes: {
url: dev
? getWebhookUrl('github')
: `${host}/webhooks/github/events`
url: dev ? getWebhookUrl('github') : `${host}/webhooks/github/events`
},
redirect_url: `${host}/webhooks/github`,
callback_urls: [`${host}/login/github/app`],
@@ -118,10 +112,10 @@
<div class="mx-auto max-w-4xl px-6">
{#if !source.githubAppId}
<form on:submit|preventDefault={newGithubApp} class="py-4">
<div class="flex space-x-1 pb-7 font-bold">
<div class="flex space-x-1 pb-7">
<div class="title">General</div>
{#if !source.githubAppId}
<button class="bg-orange-600" type="submit">Save</button>
<button class="bg-orange-600 font-normal" type="submit">Save</button>
{/if}
</div>
<div class="grid grid-flow-row gap-2 px-10">
@@ -173,7 +167,7 @@
</form>
{:else if source.githubApp?.installationId}
<form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-5 font-bold">
<div class="flex space-x-1 pb-5 ">
<div class="title">{$t('general')}</div>
{#if $appSession.isAdmin}
<button
@@ -218,20 +212,24 @@
bind:value={source.apiUrl}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="customPort" class="text-base font-bold text-stone-100">Custom SSH Port</label>
<input
name="customPort"
id="customPort"
disabled={!selfHosted}
readonly={!selfHosted}
required
value={source.customPort}
/>
<Explainer
text="If you use a self-hosted version of Git, you can provide custom port for all the Git related actions."
/>
</div>
{#if selfHosted}
<div class="grid grid-cols-2 items-center">
<label for="customPort" class="text-base font-bold text-stone-100"
>Custom SSH Port</label
>
<input
name="customPort"
id="customPort"
disabled={!selfHosted}
readonly={!selfHosted}
required
value={source.customPort}
/>
<Explainer
text="If you use a self-hosted version of Git, you can provide custom port for all the Git related actions."
/>
</div>
{/if}
<div class="grid grid-cols-2">
<div class="flex flex-col">
<label for="organization" class="pt-2 text-base font-bold text-stone-100"

View File

@@ -31,7 +31,7 @@
appSecret: null
};
}
$: selfHosted = source.htmlUrl !== 'https://gitlab.com' ;
$: selfHosted = source.htmlUrl !== 'https://gitlab.com';
onMount(() => {
oauthIdEl && oauthIdEl.focus();
@@ -141,7 +141,7 @@
<div class="mx-auto max-w-4xl px-6">
<form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-7 font-bold">
<div class="flex space-x-1 pb-7">
<div class="title">General</div>
{#if $appSession.isAdmin}
<button
@@ -228,20 +228,22 @@
bind:value={source.apiUrl}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="customPort" class="text-base font-bold text-stone-100">Custom SSH Port</label>
<input
name="customPort"
id="customPort"
disabled={!selfHosted}
readonly={!selfHosted}
required
bind:value={source.customPort}
/>
<Explainer
text="If you use a self-hosted version of Git, you can provide custom port for all the Git related actions."
/>
</div>
{#if selfHosted}
<div class="grid grid-cols-2 items-center">
<label for="customPort" class="text-base font-bold text-stone-100">Custom SSH Port</label>
<input
name="customPort"
id="customPort"
disabled={!selfHosted}
readonly={!selfHosted}
required
bind:value={source.customPort}
/>
<Explainer
text="If you use a self-hosted version of Git, you can provide custom port for all the Git related actions."
/>
</div>
{/if}
<div class="grid grid-cols-2 items-start">
<div class="flex-col">
<label for="oauthId" class="pt-2 text-base font-bold text-stone-100"

View File

@@ -66,7 +66,7 @@
<div class="flex flex-col">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each ownSources as source}
<a href="/sources/{source.id}" class="w-96 p-2 no-underline">
<a href="/sources/{source.id}" class="p-2 no-underline">
<div
class="box-selection group relative hover:bg-orange-600"
class:border-red-500={source.gitlabApp && !source.gitlabAppId}
@@ -128,10 +128,10 @@
</div>
{#if otherSources.length > 0 && $appSession.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Sources</div>
<div class="px-6 pb-5 pt-10 text-2xl font-bold text-center">Other Sources</div>
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherSources as source}
<a href="/sources/{source.id}" class="w-96 p-2 no-underline">
<a href="/sources/{source.id}" class="p-2 no-underline">
<div
class="box-selection group hover:bg-orange-600"
class:border-red-500={source.gitlabApp && !source.gitlabAppId}

View File

@@ -92,7 +92,7 @@ label {
}
button, .button {
@apply rounded bg-coolgray-200 p-1 px-2 py-1 text-xs font-bold outline-none transition-all duration-100 hover:bg-coolgray-500 disabled:cursor-not-allowed disabled:bg-coolblack disabled:text-stone-600;
@apply rounded bg-coolgray-200 p-2 px-3 text-sm outline-none transition-all duration-100 hover:bg-coolgray-500 disabled:cursor-not-allowed disabled:bg-coolblack disabled:text-stone-600;
}
a {
@@ -104,7 +104,7 @@ a {
}
.title {
@apply mr-4 text-base tracking-tight md:text-2xl;
@apply mr-4 text-base tracking-tight md:text-2xl font-bold;
}
.nav-main {
@apply fixed top-0 left-0 min-h-screen w-16 min-w-[4rem] overflow-auto border-r border-stone-800 bg-coolgray-200 scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200 xl:overflow-visible;
@@ -395,3 +395,11 @@ a {
transform: scale(0.9);
}
}
.sub-menu {
@apply text-xl font-bold hover:bg-coolgray-500 rounded p-2 hover:text-white text-stone-200 cursor-pointer;
}
.sub-menu-active {
@apply bg-coolgray-500 text-white;
}