wip: trpc

This commit is contained in:
Andras Bacsai
2023-01-13 15:50:20 +01:00
parent 91c36dc810
commit 5cb9216add
14 changed files with 1208 additions and 2 deletions

View File

@@ -0,0 +1,6 @@
<script lang="ts">
export let text: string;
export let customClass = 'max-w-[24rem]';
</script>
<div class="p-2 text-xs text-stone-400 {customClass}">{@html text}</div>

View File

@@ -0,0 +1,69 @@
<script lang="ts">
import type { LayoutData } from './$types';
export let data: LayoutData;
let destination = data.destination.destination;
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { errorNotification } from '$lib/common';
import { appSession, trpc } from '$lib/store';
import * as Icons from '$lib/components/icons';
import Tooltip from '$lib/components/Tooltip.svelte';
const isDestinationDeletable =
(destination?.application.length === 0 &&
destination?.database.length === 0 &&
destination?.service.length === 0) ||
true;
async function deleteDestination(destination: any) {
if (!isDestinationDeletable) return;
const sure = confirm("Are you sure you want to delete this destination? This can't be undone.");
if (sure) {
try {
await trpc.destinations.delete.mutate({ id: destination.id });
return await goto('/', { replaceState: true });
} catch (error) {
return errorNotification(error);
}
}
}
function deletable() {
if (!isDestinationDeletable) {
return 'Please delete all resources before deleting this.';
}
if ($appSession.isAdmin) {
return "Delete this destination. This can't be undone.";
} else {
return "You don't have permission to delete this destination.";
}
}
</script>
{#if $page.params.id !== 'new'}
<nav class="header lg:flex-row flex-col-reverse">
<div class="flex flex-row space-x-2 font-bold pt-10 lg:pt-0">
<div class="flex flex-col items-center justify-center title">
{#if $page.url.pathname === `/destinations/${$page.params.id}`}
Configurations
{:else if $page.url.pathname.startsWith(`/destinations/${$page.params.id}/configuration/sshkey`)}
Select a SSH Key
{/if}
</div>
</div>
<div class="lg:block hidden flex-1" />
<div class="flex flex-row flex-wrap space-x-3 justify-center lg:justify-start lg:py-0">
<button
id="delete"
on:click={() => deleteDestination(destination)}
type="submit"
disabled={!$appSession.isAdmin && isDestinationDeletable}
class:hover:text-red-500={$appSession.isAdmin && isDestinationDeletable}
class="icons bg-transparent text-sm"
class:text-stone-600={!isDestinationDeletable}><Icons.Delete /></button
>
<Tooltip triggeredBy="#delete">{deletable()}</Tooltip>
</div>
</nav>
{/if}
<slot />

View File

@@ -0,0 +1,45 @@
import { error } from '@sveltejs/kit';
import { trpc } from '$lib/store';
import type { LayoutLoad } from './$types';
import { redirect } from '@sveltejs/kit';
function checkConfiguration(destination: any): string | null {
let configurationPhase = null;
if (!destination?.remoteEngine) return configurationPhase;
if (!destination?.sshKey) {
configurationPhase = 'sshkey';
}
return configurationPhase;
}
export const load: LayoutLoad = async ({ params, url }) => {
const { pathname } = new URL(url);
const { id } = params;
try {
const destination = await trpc.destinations.getDestinationById.query({ id });
if (!destination) {
throw redirect(307, '/destinations');
}
const configurationPhase = checkConfiguration(destination);
console.log({ configurationPhase });
// if (
// configurationPhase &&
// pathname !== `/applications/${params.id}/configuration/${configurationPhase}`
// ) {
// throw redirect(302, `/applications/${params.id}/configuration/${configurationPhase}`);
// }
return {
destination
};
} catch (err) {
if (err instanceof Error) {
throw error(500, {
message: 'An unexpected error occurred, please try again later.' + '<br><br>' + err.message
});
}
throw error(500, {
message: 'An unexpected error occurred, please try again later.'
});
}
};

View File

@@ -0,0 +1,18 @@
<script lang="ts">
import type { LayoutData } from './$types';
export let data: LayoutData;
let destination = data.destination.destination;
let settings = data.destination.settings;
import { page } from '$app/stores';
import New from './components/New.svelte';
import Destination from './components/Destination.svelte';
const { id } = $page.params;
</script>
{#if id === 'new'}
<New />
{:else}
<Destination bind:destination bind:settings />
{/if}

View File

@@ -0,0 +1,14 @@
<script lang="ts">
export let destination: any;
export let settings: any;
import LocalDocker from './LocalDocker.svelte';
import RemoteDocker from './RemoteDocker.svelte';
</script>
<div class="mx-auto max-w-6xl px-6">
{#if destination.remoteEngine}
<RemoteDocker bind:destination {settings} />
{:else}
<LocalDocker bind:destination {settings} />
{/if}
</div>

View File

@@ -0,0 +1,212 @@
<script lang="ts">
export let destination: any;
export let settings: any;
import { page } from '$app/stores';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { onMount } from 'svelte';
import { errorNotification } from '$lib/common';
import { addToast, appSession, trpc } 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 = {
restart: false,
proxy: false,
save: false
};
async function handleSubmit() {
loading.save = true;
try {
await trpc.destinations.save.mutate({ ...destination });
addToast({
message: 'Configuration saved.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading.save = false;
}
}
onMount(async () => {
loading.proxy = true;
const { isRunning } = await trpc.destinations.status.query({ id });
let proxyUsed = !destination.isCoolifyProxyUsed;
if (isRunning === false && destination.isCoolifyProxyUsed === true) {
try {
await trpc.destinations.saveSettings.mutate({
id,
isCoolifyProxyUsed: proxyUsed,
engine: destination.engine
});
await stopProxy();
} catch (error) {
return errorNotification(error);
}
} else if (isRunning === true && destination.isCoolifyProxyUsed === false) {
try {
await trpc.destinations.saveSettings.mutate({
id,
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) {
const isProxyActivated = destination.isCoolifyProxyUsed;
if (isProxyActivated) {
const sure = confirm(
`Are you sure you want to ${
destination.isCoolifyProxyUsed ? 'disable' : 'enable'
} Coolify proxy? It will remove the proxy for all configured networks and all deployments on '${
destination.engine
}'! Nothing will be reachable if you do it!`
);
if (!sure) return;
}
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
try {
loading.proxy = true;
await trpc.destinations.saveSettings.mutate({
id,
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
engine: destination.engine
});
if (isProxyActivated) {
await stopProxy();
} else {
await startProxy();
}
} catch (error) {
return errorNotification(error);
} finally {
loading.proxy = false;
}
}
}
async function stopProxy() {
try {
await trpc.destinations.stopProxy.mutate({ id });
return addToast({
message: 'Coolify proxy stopped.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
}
}
async function startProxy() {
try {
await trpc.destinations.startProxy.mutate({ id });
return addToast({
message: ' Coolify proxy started.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
}
}
async function forceRestartProxy() {
const sure = confirm(
"Are you sure you want to restart the proxy? It will remove the proxy for all configured networks and all deployments on '" +
destination.engine +
"'! Nothing will be reachable if you do it!"
);
if (sure) {
try {
loading.restart = true;
addToast({
message: 'Restarting proxy...',
type: 'success'
});
await trpc.destinations.restartProxy.mutate({
id
});
} catch (error) {
setTimeout(() => {
window.location.reload();
}, 5000);
} finally {
loading.restart = false;
}
}
}
</script>
<form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-2">
<button
type="submit"
class="btn btn-sm"
class:bg-destinations={!loading.save}
class:loading={loading.save}
disabled={loading.save}
>Save
</button>
<button
class="btn btn-sm"
class:loading={loading.restart}
class:bg-error={!loading.restart}
disabled={loading.restart}
on:click|preventDefault={forceRestartProxy}>Force restart proxy</button
>
</div>
<div class="grid gap-2 grid-cols-2 auto-rows-max mt-10 items-center">
<label for="name">Name</label>
<input
class="w-full"
name="name"
placeholder="Name"
disabled={!$appSession.isAdmin}
readonly={!$appSession.isAdmin}
bind:value={destination.name}
/>
<label for="engine">Engine</label>
<CopyPasswordField
id="engine"
readonly
disabled
name="engine"
placeholder="Example: /var/run/docker.sock"
value={destination.engine}
/>
<label for="network">Netwokr</label>
<CopyPasswordField
id="network"
readonly
disabled
name="network"
placeholder="Default: coolify"
value={destination.network}
/>
{#if $appSession.teamId === '0'}
<Setting
id="changeProxySetting"
loading={loading.proxy}
disabled={cannotDisable}
bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting}
title="Use Coolify Proxy?"
description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration.${
cannotDisable
? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>'
: ''
}`}
/>
{/if}
</div>
</form>

View File

@@ -0,0 +1,53 @@
<script lang="ts">
import cuid from 'cuid';
import NewLocalDocker from './NewLocalDocker.svelte';
import NewRemoteDocker from './NewRemoteDocker.svelte';
let payload = {};
let selected = 'localDocker';
function setPredefined(type: any) {
selected = type;
switch (type) {
case 'localDocker':
payload = {
name: 'Local Docker',
engine: '/var/run/docker.sock',
remoteEngine: false,
network: cuid(),
isCoolifyProxyUsed: true
};
break;
case 'remoteDocker':
payload = {
name: 'Remote Docker',
remoteEngine: true,
remoteIpAddress: null,
remoteUser: 'root',
remotePort: 22,
network: cuid(),
isCoolifyProxyUsed: true
};
break;
default:
break;
}
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Add New Destination</div>
</div>
<div class="flex-col space-y-2 pb-10 text-center">
<div class="text-xl font-bold text-white">Predefined destinations</div>
<div class="flex justify-center space-x-2">
<button class="btn btn-sm" on:click={() => setPredefined('localDocker')}>Local Docker</button>
<button class="btn btn-sm" on:click={() => setPredefined('remoteDocker')}>Remote Docker</button>
<!-- <button class="w-32" on:click={() => setPredefined('kubernetes')}>Kubernetes</button> -->
</div>
</div>
{#if selected === 'localDocker'}
<NewLocalDocker {payload} />
{:else if selected === 'remoteDocker'}
<NewRemoteDocker {payload} />
{:else}
<div class="text-center font-bold text-4xl py-10">Not implemented yet</div>
{/if}

View File

@@ -0,0 +1,72 @@
<script lang="ts">
export let payload: any;
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { errorNotification } from '$lib/common';
import Setting from '$lib/components/Setting.svelte';
import { appSession, trpc } from '$lib/store';
const from = $page.url.searchParams.get('from');
let loading = false;
async function handleSubmit() {
if (loading) return;
try {
loading = true;
await trpc.destinations.check.query({ network: payload.network });
const { id } = await trpc.destinations.save.mutate({ id: 'new', ...payload });
return await goto(from || `/destinations/${id}`);
} catch (error) {
return errorNotification(error);
} finally {
loading = false;
}
}
</script>
<div class="flex justify-center px-6 pb-8">
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div
class="flex items-start lg:items-center space-x-0 lg:space-x-4 pb-5 flex-col lg:flex-row space-y-4 lg:space-y-0"
>
<div class="title font-bold">Configuration</div>
<button
type="submit"
class="btn btn-sm bg-destinations w-full lg:w-fit"
class:loading
disabled={loading}
>{loading ? (payload.isCoolifyProxyUsed ? 'Saving...' : 'Saving...') : 'Save'}</button
>
</div>
<div class="mt-2 grid grid-cols-2 items-center lg:pl-10">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
<input required name="name" placeholder="Name" bind:value={payload.name} />
</div>
<div class="grid grid-cols-2 items-center lg:pl-10">
<label for="engine" class="text-base font-bold text-stone-100">Engine</label>
<input
required
name="engine"
placeholder="Example: /var/run/docker.sock"
bind:value={payload.engine}
/>
</div>
<div class="grid grid-cols-2 items-center lg:pl-10">
<label for="network" class="text-base font-bold text-stone-100">Network</label>
<input required name="network" placeholder="Default: coolify" bind:value={payload.network} />
</div>
{#if $appSession.teamId === '0'}
<div class="grid grid-cols-2 items-center lg:pl-10">
<Setting
id="changeProxySetting"
bind:setting={payload.isCoolifyProxyUsed}
on:click={() => (payload.isCoolifyProxyUsed = !payload.isCoolifyProxyUsed)}
title="Use Coolify Proxy?"
description={'This will install a proxy on the destination to allow you to access your applications and services without any manual configuration.'}
/>
</div>
{/if}
</form>
</div>

View File

@@ -0,0 +1,104 @@
<script lang="ts">
export let payload: any;
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { errorNotification } from '$lib/common';
import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
import Setting from '$lib/components/Setting.svelte';
import { trpc } from '$lib/store';
const from = $page.url.searchParams.get('from');
let loading = false;
async function handleSubmit() {
if (loading) return;
try {
loading = true;
await trpc.destinations.check.query({ network: payload.network });
const { id } = await trpc.destinations.save.mutate({ id: 'new', ...payload });
return await goto(from || `/destinations/${id}`);
} catch (error) {
return errorNotification(error);
} finally {
loading = false;
}
}
</script>
<div class="text-center flex justify-center">
<SimpleExplainer
customClass="max-w-[32rem]"
text="Remote Docker Engines are using <span class='text-white font-bold'>SSH</span> to communicate with the remote docker engine.
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/coolify/destinations#remote-docker-engine' target='blank'>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-start lg:items-center space-x-0 lg:space-x-4 pb-5 flex-col lg:flex-row space-y-4 lg:space-y-0">
<div class="title font-bold">Configuration</div>
<button type="submit" class="btn btn-sm bg-destinations w-full lg:w-fit" class:loading disabled={loading}
>{loading
? payload.isCoolifyProxyUsed
? 'Saving...'
: 'Saving...'
: "Save"}</button
>
</div>
<div class="mt-2 grid grid-cols-2 items-center lg:pl-10">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
<input required name="name" placeholder="Name" bind:value={payload.name} />
</div>
<div class="grid grid-cols-2 items-center lg:pl-10">
<label for="remoteIpAddress" class="text-base font-bold text-stone-100"
>IP Address</label
>
<input
required
name="remoteIpAddress"
placeholder="Example: 192.168..."
bind:value={payload.remoteIpAddress}
/>
</div>
<div class="grid grid-cols-2 items-center lg:pl-10">
<label for="remoteUser" class="text-base font-bold text-stone-100">User</label>
<input
required
name="remoteUser"
placeholder="Example: root"
bind:value={payload.remoteUser}
/>
</div>
<div class="grid grid-cols-2 items-center lg:pl-10">
<label for="remotePort" class="text-base font-bold text-stone-100">Port</label>
<input
required
name="remotePort"
placeholder="Example: 22"
bind:value={payload.remotePort}
/>
</div>
<div class="grid grid-cols-2 items-center lg:pl-10">
<label for="network" class="text-base font-bold text-stone-100">Network</label>
<input
required
name="network"
placeholder="Default: coolify"
bind:value={payload.network}
/>
</div>
<div class="grid grid-cols-2 items-center lg:pl-10">
<Setting
id="isCoolifyProxyUsed"
bind:setting={payload.isCoolifyProxyUsed}
on:click={() => (payload.isCoolifyProxyUsed = !payload.isCoolifyProxyUsed)}
title="Use Coolify Proxy?"
description={'This will install a proxy on the destination to allow you to access your applications and services without any manual configuration.'}
/>
</div>
</form>
</div>

View File

@@ -0,0 +1,279 @@
<script lang="ts">
export let destination: any;
export let settings: any;
import { page } from '$app/stores';
import Setting from '$lib/components/Setting.svelte';
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 { addToast, appSession } from '$lib/store';
const { id } = $page.params;
let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock';
let loading = {
restart: false,
proxy: true,
save: false,
verify: false
};
$: isDisabled = !$appSession.isAdmin;
async function handleSubmit() {
loading.save = true;
try {
await post(`/destinations/${id}`, { ...destination });
addToast({
message: 'Configuration saved.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading.save = false;
}
}
onMount(async () => {
if (destination.remoteEngine && destination.remoteVerified) {
loading.proxy = true;
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() {
if (!destination.remoteVerified) return;
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! Nothing will be reachable if you do it!`
);
if (!sure) {
loading.proxy = false;
return;
}
}
let proxyUsed = !destination.isCoolifyProxyUsed;
try {
await post(`/destinations/${id}/settings`, {
isCoolifyProxyUsed: proxyUsed,
engine: destination.engine
});
if (isProxyActivated) {
await stopProxy();
} else {
await startProxy();
}
destination.isCoolifyProxyUsed = proxyUsed;
} catch (error) {
return errorNotification(error);
} finally {
loading.proxy = false;
}
}
}
async function stopProxy() {
try {
await post(`/destinations/${id}/stop`, { engine: destination.engine });
return addToast({
message: $t('destination.coolify_proxy_stopped'),
type: 'success'
});
} catch (error) {
return errorNotification(error);
}
}
async function startProxy() {
try {
await post(`/destinations/${id}/start`, { engine: destination.engine });
return addToast({
message: $t('destination.coolify_proxy_started'),
type: 'success'
});
} catch (error) {
return errorNotification(error);
}
}
async function forceRestartProxy() {
const sure = confirm($t('destination.confirm_restart_proxy'));
if (sure) {
try {
loading.restart = true;
addToast({
message: $t('destination.coolify_proxy_restarting'),
type: 'success'
});
await post(`/destinations/${id}/restart`, {
engine: destination.engine,
fqdn: settings.fqdn
});
} catch (error) {
setTimeout(() => {
window.location.reload();
}, 5000);
} finally {
loading.restart = false;
}
}
}
async function verifyRemoteDocker() {
try {
loading.verify = true;
await post(`/destinations/${id}/verify`, {});
destination.remoteVerified = true;
return addToast({
message: 'Remote Docker Engine verified!',
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading.verify = false;
}
}
</script>
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex space-x-1 pb-5">
{#if $appSession.isAdmin}
<button
type="submit"
class="btn btn-sm"
class:loading={loading.save}
class:bg-destinations={!loading.save}
disabled={loading.save}
>{$t('forms.save')}
</button>
<button
disabled={loading.verify}
class="btn btn-sm"
class:loading={loading.verify}
on:click|preventDefault|stopPropagation={verifyRemoteDocker}
>{!destination.remoteVerified
? 'Verify Remote Docker Engine'
: 'Check Remote Docker Engine'}</button
>
{#if destination.remoteVerified}
<button
class="btn btn-sm"
class:loading={loading.restart}
class:bg-error={!loading.restart}
disabled={loading.restart}
on:click|preventDefault={forceRestartProxy}
>{$t('destination.force_restart_proxy')}</button
>
{/if}
{/if}
</div>
<div class="grid grid-cols-2 items-center px-10 ">
<label for="name">{$t('forms.name')}</label>
<input
name="name"
class="w-full"
placeholder={$t('forms.name')}
disabled={!$appSession.isAdmin}
readonly={!$appSession.isAdmin}
bind:value={destination.name}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="network">{$t('forms.network')}</label>
<CopyPasswordField
id="network"
readonly
disabled
name="network"
placeholder="{$t('forms.default')}: coolify"
value={destination.network}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="remoteIpAddress">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">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">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">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 w-full"
/></a
>
</div>
<div class="grid grid-cols-2 items-center px-10">
<Setting
id="changeProxySetting"
disabled={cannotDisable || !destination.remoteVerified}
loading={loading.proxy}
bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting}
title={$t('destination.use_coolify_proxy')}
description={`Install & configure a proxy (based on Traefik) on the destination to allow you to access your applications and services without any manual configuration.${
cannotDisable
? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>'
: ''
}`}
/>
</div>
</form>