mirror of
https://github.com/ershisan99/coolify.git
synced 2026-01-28 05:02:13 +00:00
87
apps/ui/src/routes/services/[id]/_Secret.svelte
Normal file
87
apps/ui/src/routes/services/[id]/_Secret.svelte
Normal file
@@ -0,0 +1,87 @@
|
||||
<script>
|
||||
export let name = '';
|
||||
export let value = '';
|
||||
export let isNewSecret = false;
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { del, post } from '$lib/api';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const { id } = $page.params;
|
||||
async function removeSecret() {
|
||||
try {
|
||||
await del(`/services/${id}/secrets`, { name });
|
||||
dispatch('refresh');
|
||||
if (isNewSecret) {
|
||||
name = '';
|
||||
value = '';
|
||||
}
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function saveSecret(isNew = false) {
|
||||
if (!name) return errorNotification('Name is required.');
|
||||
if (!value) return errorNotification('Value is required.');
|
||||
try {
|
||||
await post(`/services/${id}/secrets`, {
|
||||
name,
|
||||
value,
|
||||
|
||||
isNew
|
||||
});
|
||||
dispatch('refresh');
|
||||
if (isNewSecret) {
|
||||
name = '';
|
||||
value = '';
|
||||
}
|
||||
toast.push('Secret saved.');
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<td>
|
||||
<input
|
||||
id={isNewSecret ? 'secretName' : 'secretNameNew'}
|
||||
bind:value={name}
|
||||
required
|
||||
placeholder="EXAMPLE_VARIABLE"
|
||||
class=" border border-dashed border-coolgray-300"
|
||||
readonly={!isNewSecret}
|
||||
class:bg-transparent={!isNewSecret}
|
||||
class:cursor-not-allowed={!isNewSecret}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<CopyPasswordField
|
||||
id={isNewSecret ? 'secretValue' : 'secretValueNew'}
|
||||
name={isNewSecret ? 'secretValue' : 'secretValueNew'}
|
||||
isPasswordField={true}
|
||||
bind:value
|
||||
required
|
||||
placeholder="J$#@UIO%HO#$U%H"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{#if isNewSecret}
|
||||
<div class="flex items-center justify-center">
|
||||
<button class="bg-green-600 hover:bg-green-500" on:click={() => saveSecret(true)}>Add</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row justify-center space-x-2">
|
||||
<div class="flex items-center justify-center">
|
||||
<button class="" on:click={() => saveSecret(false)}>Set</button>
|
||||
</div>
|
||||
<div class="flex justify-center items-end">
|
||||
<button class="bg-red-600 hover:bg-red-500" on:click={removeSecret}>Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
76
apps/ui/src/routes/services/[id]/_ServiceLinks.svelte
Normal file
76
apps/ui/src/routes/services/[id]/_ServiceLinks.svelte
Normal file
@@ -0,0 +1,76 @@
|
||||
<script lang="ts">
|
||||
export let service: any;
|
||||
|
||||
import Fider from '$lib/components/svg/services/Fider.svelte';
|
||||
|
||||
import Ghost from '$lib/components/svg/services/Ghost.svelte';
|
||||
import Hasura from '$lib/components/svg/services/Hasura.svelte';
|
||||
|
||||
import LanguageTool from '$lib/components/svg/services/LanguageTool.svelte';
|
||||
|
||||
import MinIo from '$lib/components/svg/services/MinIO.svelte';
|
||||
import N8n from '$lib/components/svg/services/N8n.svelte';
|
||||
|
||||
import NocoDb from '$lib/components/svg/services/NocoDB.svelte';
|
||||
|
||||
import PlausibleAnalytics from '$lib/components/svg/services/PlausibleAnalytics.svelte';
|
||||
import Umami from '$lib/components/svg/services/Umami.svelte';
|
||||
import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte';
|
||||
import VaultWarden from '$lib/components/svg/services/VaultWarden.svelte';
|
||||
import VsCodeServer from '$lib/components/svg/services/VSCodeServer.svelte';
|
||||
import Wordpress from '$lib/components/svg/services/Wordpress.svelte';
|
||||
</script>
|
||||
|
||||
{#if service.type === 'plausibleanalytics'}
|
||||
<a href="https://plausible.io" target="_blank">
|
||||
<PlausibleAnalytics />
|
||||
</a>
|
||||
{:else if service.type === 'nocodb'}
|
||||
<a href="https://nocodb.com" target="_blank">
|
||||
<NocoDb />
|
||||
</a>
|
||||
{:else if service.type === 'minio'}
|
||||
<a href="https://min.io" target="_blank">
|
||||
<MinIo />
|
||||
</a>
|
||||
{:else if service.type === 'vscodeserver'}
|
||||
<a href="https://coder.com" target="_blank">
|
||||
<VsCodeServer />
|
||||
</a>
|
||||
{:else if service.type === 'wordpress'}
|
||||
<a href="https://wordpress.org" target="_blank">
|
||||
<Wordpress />
|
||||
</a>
|
||||
{:else if service.type === 'vaultwarden'}
|
||||
<a href="https://github.com/dani-garcia/vaultwarden" target="_blank">
|
||||
<VaultWarden />
|
||||
</a>
|
||||
{:else if service.type === 'languagetool'}
|
||||
<a href="https://languagetool.org/dev" target="_blank">
|
||||
<LanguageTool />
|
||||
</a>
|
||||
{:else if service.type === 'n8n'}
|
||||
<a href="https://n8n.io" target="_blank">
|
||||
<N8n />
|
||||
</a>
|
||||
{:else if service.type === 'uptimekuma'}
|
||||
<a href="https://github.com/louislam/uptime-kuma" target="_blank">
|
||||
<UptimeKuma />
|
||||
</a>
|
||||
{:else if service.type === 'ghost'}
|
||||
<a href="https://ghost.org" target="_blank">
|
||||
<Ghost />
|
||||
</a>
|
||||
{:else if service.type === 'umami'}
|
||||
<a href="https://umami.is" target="_blank">
|
||||
<Umami />
|
||||
</a>
|
||||
{:else if service.type === 'hasura'}
|
||||
<a href="https://hasura.io" target="_blank">
|
||||
<Hasura />
|
||||
</a>
|
||||
{:else if service.type === 'fider'}
|
||||
<a href="https://fider.io" target="_blank">
|
||||
<Fider />
|
||||
</a>
|
||||
{/if}
|
||||
183
apps/ui/src/routes/services/[id]/_Services/_Fider.svelte
Normal file
183
apps/ui/src/routes/services/[id]/_Services/_Fider.svelte
Normal file
@@ -0,0 +1,183 @@
|
||||
<script lang="ts">
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
import Select from 'svelte-select';
|
||||
export let service: any;
|
||||
export let readOnly: any;
|
||||
|
||||
let mailgunRegions = [
|
||||
{
|
||||
value: 'EU',
|
||||
label: 'EU'
|
||||
},
|
||||
{
|
||||
value: 'US',
|
||||
label: 'US'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Fider</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="jwtSecret">JWT Secret</label>
|
||||
<CopyPasswordField
|
||||
name="jwtSecret"
|
||||
id="jwtSecret"
|
||||
isPasswordField
|
||||
value={service.fider.jwtSecret}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailNoreply">Noreply Email</label>
|
||||
<input
|
||||
name="emailNoreply"
|
||||
id="emailNoreply"
|
||||
type="email"
|
||||
required
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
bind:value={service.fider.emailNoreply}
|
||||
placeholder="{$t('forms.eg')}: noreply@yourdomain.com"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Email</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailMailgunApiKey">Mailgun API Key</label>
|
||||
<CopyPasswordField
|
||||
name="emailMailgunApiKey"
|
||||
id="emailMailgunApiKey"
|
||||
isPasswordField
|
||||
bind:value={service.fider.emailMailgunApiKey}
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
placeholder="{$t('forms.eg')}: key-yourkeygoeshere"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailMailgunDomain">Mailgun Domain</label>
|
||||
<input
|
||||
name="emailMailgunDomain"
|
||||
id="emailMailgunDomain"
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
bind:value={service.fider.emailMailgunDomain}
|
||||
placeholder="{$t('forms.eg')}: yourdomain.com"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailMailgunRegion">Mailgun Region</label>
|
||||
<div class="custom-select-wrapper">
|
||||
<Select
|
||||
id="baseBuildImages"
|
||||
items={mailgunRegions}
|
||||
showIndicator
|
||||
on:select={(event) => (service.fider.emailMailgunRegion = event.detail.value)}
|
||||
value={service.fider.emailMailgunRegion || 'EU'}
|
||||
isClearable={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1 py-5 px-10 font-bold">
|
||||
<div class="text-lg">Or</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailSmtpHost">SMTP Host</label>
|
||||
<input
|
||||
name="emailSmtpHost"
|
||||
id="emailSmtpHost"
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
bind:value={service.fider.emailSmtpHost}
|
||||
placeholder="{$t('forms.eg')}: smtp.yourdomain.com"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailSmtpPort">SMTP Port</label>
|
||||
<input
|
||||
name="emailSmtpPort"
|
||||
id="emailSmtpPort"
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
bind:value={service.fider.emailSmtpPort}
|
||||
placeholder="{$t('forms.eg')}: 587"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailSmtpUser">SMTP User</label>
|
||||
<input
|
||||
name="emailSmtpUser"
|
||||
id="emailSmtpUser"
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
bind:value={service.fider.emailSmtpUser}
|
||||
placeholder="{$t('forms.eg')}: user@yourdomain.com"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailSmtpPassword">SMTP Password</label>
|
||||
<CopyPasswordField
|
||||
name="emailSmtpPassword"
|
||||
id="emailSmtpPassword"
|
||||
isPasswordField
|
||||
bind:value={service.fider.emailSmtpPassword}
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
placeholder="{$t('forms.eg')}: s0m3p4ssw0rd"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="emailSmtpEnableStartTls">SMTP Start TLS</label>
|
||||
<input
|
||||
name="emailSmtpEnableStartTls"
|
||||
id="emailSmtpEnableStartTls"
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
bind:value={service.fider.emailSmtpEnableStartTls}
|
||||
placeholder="{$t('forms.eg')}: true"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">PostgreSQL</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlUser">{$t('forms.username')}</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlUser"
|
||||
id="postgresqlUser"
|
||||
value={service.fider.postgresqlUser}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlPassword">{$t('forms.password')}</label>
|
||||
<CopyPasswordField
|
||||
id="postgresqlPassword"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="postgresqlPassword"
|
||||
value={service.fider.postgresqlPassword}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlDatabase">{$t('index.database')}</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlDatabase"
|
||||
id="postgresqlDatabase"
|
||||
value={service.fider.postgresqlDatabase}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
90
apps/ui/src/routes/services/[id]/_Services/_Ghost.svelte
Normal file
90
apps/ui/src/routes/services/[id]/_Services/_Ghost.svelte
Normal file
@@ -0,0 +1,90 @@
|
||||
<script lang="ts">
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
export let readOnly: any;
|
||||
export let service: any;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Ghost</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="email">{$t('forms.default_email_address')}</label>
|
||||
<input
|
||||
name="email"
|
||||
id="email"
|
||||
disabled
|
||||
readonly
|
||||
placeholder={$t('forms.email')}
|
||||
value={service.ghost.defaultEmail}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="defaultPassword">{$t('forms.default_password')}</label>
|
||||
<CopyPasswordField
|
||||
id="defaultPassword"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="defaultPassword"
|
||||
value={service.ghost.defaultPassword}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">MariaDB</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="mariadbUser">{$t('forms.username')}</label>
|
||||
<CopyPasswordField
|
||||
name="mariadbUser"
|
||||
id="mariadbUser"
|
||||
value={service.ghost.mariadbUser}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="mariadbPassword">{$t('forms.password')}</label>
|
||||
<CopyPasswordField
|
||||
id="mariadbPassword"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="mariadbPassword"
|
||||
value={service.ghost.mariadbPassword}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="mariadbDatabase">{$t('index.database')}</label>
|
||||
<input
|
||||
name="mariadbDatabase"
|
||||
id="mariadbDatabase"
|
||||
required
|
||||
readonly={readOnly}
|
||||
disabled={readOnly}
|
||||
bind:value={service.ghost.mariadbDatabase}
|
||||
placeholder="{$t('forms.eg')}: ghost_db"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="mariadbRootUser">{$t('forms.root_db_user')}</label>
|
||||
<CopyPasswordField
|
||||
id="mariadbRootUser"
|
||||
readonly
|
||||
disabled
|
||||
name="mariadbRootUser"
|
||||
value={service.ghost.mariadbRootUser}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="mariadbRootUserPassword">{$t('forms.root_db_password')}</label>
|
||||
<CopyPasswordField
|
||||
id="mariadbRootUserPassword"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="mariadbRootUserPassword"
|
||||
value={service.ghost.mariadbRootUserPassword}
|
||||
/>
|
||||
</div>
|
||||
58
apps/ui/src/routes/services/[id]/_Services/_Hasura.svelte
Normal file
58
apps/ui/src/routes/services/[id]/_Services/_Hasura.svelte
Normal file
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
export let service: any;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Hasura</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="graphQLAdminPassword">GraphQL Admin Password</label>
|
||||
<CopyPasswordField
|
||||
name="graphQLAdminPassword"
|
||||
id="graphQLAdminPassword"
|
||||
isPasswordField
|
||||
value={service.hasura.graphQLAdminPassword}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">PostgreSQL</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlUser">{$t('forms.username')}</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlUser"
|
||||
id="postgresqlUser"
|
||||
value={service.hasura.postgresqlUser}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlPassword">{$t('forms.password')}</label>
|
||||
<CopyPasswordField
|
||||
id="postgresqlPassword"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="postgresqlPassword"
|
||||
value={service.hasura.postgresqlPassword}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlDatabase">{$t('index.database')}</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlDatabase"
|
||||
id="postgresqlDatabase"
|
||||
value={service.hasura.postgresqlDatabase}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
export let service: any;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">MeiliSearch</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="masterKey">{$t('forms.admin_api_key')}</label>
|
||||
<CopyPasswordField
|
||||
id="masterKey"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="masterKey"
|
||||
value={service.meiliSearch.masterKey}
|
||||
/>
|
||||
</div>
|
||||
45
apps/ui/src/routes/services/[id]/_Services/_MinIO.svelte
Normal file
45
apps/ui/src/routes/services/[id]/_Services/_MinIO.svelte
Normal file
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
|
||||
export let service: any;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">MinIO</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="rootUser">{$t('forms.root_user')}</label>
|
||||
<input
|
||||
name="rootUser"
|
||||
id="rootUser"
|
||||
placeholder={$t('forms.username')}
|
||||
value={service.minio.rootUser}
|
||||
disabled
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="rootUserPassword">{$t('forms.roots_password')}</label>
|
||||
<CopyPasswordField
|
||||
id="rootUserPassword"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="rootUserPassword"
|
||||
value={service.minio.rootUserPassword}
|
||||
/>
|
||||
</div>
|
||||
{#if !service.minio.apiFqdn}
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="publicPort">{$t('forms.api_port')}</label>
|
||||
<input
|
||||
name="publicPort"
|
||||
id="publicPort"
|
||||
value={service.minio.publicPort}
|
||||
disabled
|
||||
readonly
|
||||
placeholder={$t('forms.generated_automatically_after_start')}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,96 @@
|
||||
<script lang="ts">
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { appSession, status } from '$lib/store';
|
||||
import { t } from '$lib/translations';
|
||||
export let service: any;
|
||||
export let readOnly: any;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Plausible Analytics</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="scriptName">Script Name</label>
|
||||
<input
|
||||
name="scriptName"
|
||||
id="scriptName"
|
||||
readonly={!$appSession.isAdmin && !$status.service.isRunning}
|
||||
disabled={!$appSession.isAdmin || $status.service.isRunning}
|
||||
placeholder="plausible.js"
|
||||
bind:value={service.plausibleAnalytics.scriptName}
|
||||
required
|
||||
/>
|
||||
<Explainer
|
||||
text="Useful if you would like to rename the collector script to prevent it blocked by AdBlockers."
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="email">{$t('forms.email')}</label>
|
||||
<input
|
||||
name="email"
|
||||
id="email"
|
||||
disabled={readOnly}
|
||||
readonly={readOnly}
|
||||
placeholder={$t('forms.email')}
|
||||
bind:value={service.plausibleAnalytics.email}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="username">{$t('forms.username')}</label>
|
||||
<CopyPasswordField
|
||||
name="username"
|
||||
id="username"
|
||||
disabled={readOnly}
|
||||
readonly={readOnly}
|
||||
placeholder={$t('forms.username')}
|
||||
bind:value={service.plausibleAnalytics.username}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="password">{$t('forms.password')}</label>
|
||||
<CopyPasswordField
|
||||
id="password"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="password"
|
||||
value={service.plausibleAnalytics.password}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">PostgreSQL</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlUser">{$t('forms.username')}</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlUser"
|
||||
id="postgresqlUser"
|
||||
value={service.plausibleAnalytics.postgresqlUser}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlPassword">{$t('forms.password')}</label>
|
||||
<CopyPasswordField
|
||||
id="postgresqlPassword"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="postgresqlPassword"
|
||||
value={service.plausibleAnalytics.postgresqlPassword}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="postgresqlDatabase">{$t('index.database')}</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlDatabase"
|
||||
id="postgresqlDatabase"
|
||||
value={service.plausibleAnalytics.postgresqlDatabase}
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
277
apps/ui/src/routes/services/[id]/_Services/_Services.svelte
Normal file
277
apps/ui/src/routes/services/[id]/_Services/_Services.svelte
Normal file
@@ -0,0 +1,277 @@
|
||||
<script lang="ts">
|
||||
export let service: any;
|
||||
export let readOnly: any;
|
||||
export let settings: any;
|
||||
|
||||
import cuid from 'cuid';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import { browser } from '$app/env';
|
||||
import { page } from '$app/stores';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
|
||||
import { get, post } from '$lib/api';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { t } from '$lib/translations';
|
||||
import { appSession, disabledButton, status } from '$lib/store';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import Setting from '$lib/components/Setting.svelte'
|
||||
|
||||
import Fider from './_Fider.svelte';
|
||||
import Ghost from './_Ghost.svelte';
|
||||
import Hasura from './_Hasura.svelte';
|
||||
import MeiliSearch from './_MeiliSearch.svelte';
|
||||
import MinIo from './_MinIO.svelte';
|
||||
import PlausibleAnalytics from './_PlausibleAnalytics.svelte';
|
||||
import Umami from './_Umami.svelte';
|
||||
import VsCodeServer from './_VSCodeServer.svelte';
|
||||
import Wordpress from './_Wordpress.svelte';
|
||||
|
||||
const { id } = $page.params;
|
||||
|
||||
let loading = false;
|
||||
let loadingVerification = false;
|
||||
let dualCerts = service.dualCerts;
|
||||
|
||||
async function handleSubmit() {
|
||||
if (loading) return;
|
||||
loading = true;
|
||||
try {
|
||||
await post(`/services/${id}/check`, {
|
||||
fqdn: service.fqdn,
|
||||
otherFqdns: service.minio?.apiFqdn ? [service.minio?.apiFqdn] : [],
|
||||
exposePort: service.exposePort
|
||||
});
|
||||
await post(`/services/${id}`, { ...service });
|
||||
$disabledButton = false;
|
||||
toast.push('Settings saved.');
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
async function setEmailsToVerified() {
|
||||
loadingVerification = true;
|
||||
try {
|
||||
await post(`/services/${id}/${service.type}/activate`, { id: service.id });
|
||||
toast.push(t.get('services.all_email_verified'));
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loadingVerification = false;
|
||||
}
|
||||
}
|
||||
async function changeSettings(name: any) {
|
||||
try {
|
||||
if (name === 'dualCerts') {
|
||||
dualCerts = !dualCerts;
|
||||
}
|
||||
await post(`/services/${id}/settings`, { dualCerts });
|
||||
return toast.push(t.get('application.settings_saved'));
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
onMount(async () => {
|
||||
if (browser && window.location.hostname === 'demo.coolify.io' && !service.fqdn) {
|
||||
service.fqdn = `http://${cuid()}.demo.coolify.io`;
|
||||
if (service.type === 'wordpress') {
|
||||
service.wordpress.mysqlDatabase = 'db';
|
||||
}
|
||||
if (service.type === 'plausibleanalytics') {
|
||||
service.plausibleAnalytics.email = 'noreply@demo.com';
|
||||
service.plausibleAnalytics.username = 'admin';
|
||||
}
|
||||
if (service.type === 'minio') {
|
||||
service.minio.apiFqdn = `http://${cuid()}.demo.coolify.io`;
|
||||
}
|
||||
if (service.type === 'ghost') {
|
||||
service.ghost.mariadbDatabase = 'db';
|
||||
}
|
||||
if (service.type === 'fider') {
|
||||
service.fider.emailNoreply = 'noreply@demo.com';
|
||||
}
|
||||
await handleSubmit();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<div class="mx-auto max-w-4xl px-6 pb-12">
|
||||
<form on:submit|preventDefault={handleSubmit} class="py-4">
|
||||
<div class="flex space-x-1 pb-5 font-bold">
|
||||
<div class="title">{$t('general')}</div>
|
||||
{#if $appSession.isAdmin}
|
||||
<button
|
||||
type="submit"
|
||||
class:bg-pink-600={!loading}
|
||||
class:hover:bg-pink-500={!loading}
|
||||
disabled={loading}>{loading ? $t('forms.saving') : $t('forms.save')}</button
|
||||
>
|
||||
{/if}
|
||||
{#if service.type === 'plausibleanalytics' && $status.service.isRunning}
|
||||
<button on:click|preventDefault={setEmailsToVerified} disabled={loadingVerification}
|
||||
>{loadingVerification
|
||||
? $t('forms.verifying')
|
||||
: $t('forms.verify_emails_without_smtp')}</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-flow-row gap-2">
|
||||
{#if service.type === 'minio' && !service.minio.apiFqdn && $status.service.isRunning}
|
||||
<div class="text-center">
|
||||
<span class="font-bold text-red-500">IMPORTANT!</span> There was a small modification with
|
||||
Minio in the latest version of Coolify. Now you can separate the Console URL from the API URL,
|
||||
so you could use both through SSL. But this proccess cannot be done automatically, so you have
|
||||
to stop your Minio instance, configure the new domain and start it back. Sorry for any inconvenience.
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mt-2 grid grid-cols-2 items-center px-10">
|
||||
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
|
||||
<div>
|
||||
<input
|
||||
readonly={!$appSession.isAdmin}
|
||||
name="name"
|
||||
id="name"
|
||||
bind:value={service.name}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="version" class="text-base font-bold text-stone-100">Version / Tag</label>
|
||||
<a
|
||||
href={$appSession.isAdmin && !$status.service.isRunning
|
||||
? `/services/${id}/configuration/version?from=/services/${id}`
|
||||
: ''}
|
||||
class="no-underline"
|
||||
>
|
||||
<input
|
||||
value={service.version}
|
||||
id="service"
|
||||
disabled={$status.service.isRunning}
|
||||
class:cursor-pointer={!$status.service.isRunning}
|
||||
/></a
|
||||
>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="destination" class="text-base font-bold text-stone-100"
|
||||
>{$t('application.destination')}</label
|
||||
>
|
||||
<div>
|
||||
{#if service.destinationDockerId}
|
||||
<div class="no-underline">
|
||||
<input
|
||||
value={service.destinationDocker.name}
|
||||
id="destination"
|
||||
disabled
|
||||
class="bg-transparent "
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if service.type === 'minio'}
|
||||
<div class="grid grid-cols-2 px-10">
|
||||
<div class="flex-col ">
|
||||
<label for="fqdn" class="pt-2 text-base font-bold text-stone-100">Console URL</label>
|
||||
</div>
|
||||
|
||||
<CopyPasswordField
|
||||
placeholder="eg: https://console.min.io"
|
||||
readonly={!$appSession.isAdmin && !$status.service.isRunning}
|
||||
disabled={!$appSession.isAdmin || $status.service.isRunning}
|
||||
name="fqdn"
|
||||
id="fqdn"
|
||||
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
|
||||
bind:value={service.fqdn}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 px-10">
|
||||
<div class="flex-col ">
|
||||
<label for="apiFqdn" class="pt-2 text-base font-bold text-stone-100">API URL</label>
|
||||
<Explainer text={$t('application.https_explainer')} />
|
||||
</div>
|
||||
|
||||
<CopyPasswordField
|
||||
placeholder="eg: https://min.io"
|
||||
readonly={!$appSession.isAdmin && !$status.service.isRunning}
|
||||
disabled={!$appSession.isAdmin || $status.service.isRunning}
|
||||
name="apiFqdn"
|
||||
id="apiFqdn"
|
||||
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
|
||||
bind:value={service.minio.apiFqdn}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid grid-cols-2 px-10">
|
||||
<div class="flex-col ">
|
||||
<label for="fqdn" class="pt-2 text-base font-bold text-stone-100"
|
||||
>{$t('application.url_fqdn')}</label
|
||||
>
|
||||
<Explainer text={$t('application.https_explainer')} />
|
||||
</div>
|
||||
|
||||
<CopyPasswordField
|
||||
placeholder="eg: https://analytics.coollabs.io"
|
||||
readonly={!$appSession.isAdmin && !$status.service.isRunning}
|
||||
disabled={!$appSession.isAdmin || $status.service.isRunning}
|
||||
name="fqdn"
|
||||
id="fqdn"
|
||||
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
|
||||
bind:value={service.fqdn}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<Setting
|
||||
disabled={$status.service.isRunning}
|
||||
dataTooltip={$t('forms.must_be_stopped_to_modify')}
|
||||
bind:setting={dualCerts}
|
||||
title={$t('application.ssl_www_and_non_www')}
|
||||
description={$t('services.generate_www_non_www_ssl')}
|
||||
on:click={() => !$status.service.isRunning && changeSettings('dualCerts')}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="exposePort" class="text-base font-bold text-stone-100">Exposed Port</label>
|
||||
<input
|
||||
readonly={!$appSession.isAdmin && !$status.service.isRunning}
|
||||
disabled={!$appSession.isAdmin || $status.service.isRunning}
|
||||
name="exposePort"
|
||||
id="exposePort"
|
||||
bind:value={service.exposePort}
|
||||
placeholder="12345"
|
||||
/>
|
||||
<Explainer
|
||||
text={'You can expose your application to a port on the host system.<br><br>Useful if you would like to use your own reverse proxy or tunnel and also in development mode. Otherwise leave empty.'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if service.type === 'plausibleanalytics'}
|
||||
<PlausibleAnalytics bind:service {readOnly} />
|
||||
{:else if service.type === 'minio'}
|
||||
<MinIo {service} />
|
||||
{:else if service.type === 'vscodeserver'}
|
||||
<VsCodeServer {service} />
|
||||
{:else if service.type === 'wordpress'}
|
||||
<Wordpress bind:service {readOnly} {settings} />
|
||||
{:else if service.type === 'ghost'}
|
||||
<Ghost bind:service {readOnly} />
|
||||
{:else if service.type === 'meilisearch'}
|
||||
<MeiliSearch bind:service />
|
||||
{:else if service.type === 'umami'}
|
||||
<Umami bind:service />
|
||||
{:else if service.type === 'hasura'}
|
||||
<Hasura bind:service />
|
||||
{:else if service.type === 'fider'}
|
||||
<Fider bind:service {readOnly} />
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
29
apps/ui/src/routes/services/[id]/_Services/_Umami.svelte
Normal file
29
apps/ui/src/routes/services/[id]/_Services/_Umami.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
|
||||
export let service: any;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Umami</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="adminUser">Admin User</label>
|
||||
<input name="adminUser" id="adminUser" placeholder="admin" value="admin" disabled readonly />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="umamiAdminPassword">Initial Admin Password</label>
|
||||
<CopyPasswordField
|
||||
isPasswordField
|
||||
name="umamiAdminPassword"
|
||||
id="umamiAdminPassword"
|
||||
placeholder="admin"
|
||||
value={service.umami.umamiAdminPassword}
|
||||
disabled
|
||||
readonly
|
||||
/>
|
||||
<Explainer
|
||||
text="It could be changed in Umami. <br>This is just the password set initially after the first start."
|
||||
/>
|
||||
</div>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
|
||||
export let service: any;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">VSCode Server</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="password">{$t('forms.password')}</label>
|
||||
<CopyPasswordField
|
||||
id="password"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="password"
|
||||
value={service.vscodeserver.password}
|
||||
/>
|
||||
</div>
|
||||
213
apps/ui/src/routes/services/[id]/_Services/_Wordpress.svelte
Normal file
213
apps/ui/src/routes/services/[id]/_Services/_Wordpress.svelte
Normal file
@@ -0,0 +1,213 @@
|
||||
<script lang="ts">
|
||||
import { post } from '$lib/api';
|
||||
import { page } from '$app/stores';
|
||||
import { status } from '$lib/store';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Setting from '$lib/components/Setting.svelte';
|
||||
import { browser } from '$app/env';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification, getDomain } from '$lib/common';
|
||||
|
||||
export let service: any;
|
||||
export let readOnly: any;
|
||||
export let settings: any;
|
||||
const { id } = $page.params;
|
||||
|
||||
let ftpUrl = generateUrl(service.wordpress.ftpPublicPort);
|
||||
let ftpUser = service.wordpress.ftpUser;
|
||||
let ftpPassword = service.wordpress.ftpPassword;
|
||||
let ftpLoading = false;
|
||||
let ownMysql = service.wordpress.ownMysql;
|
||||
|
||||
function generateUrl(publicPort: any) {
|
||||
return browser
|
||||
? `sftp://${
|
||||
settings.fqdn ? getDomain(settings.fqdn) : window.location.hostname
|
||||
}:${publicPort}`
|
||||
: 'Loading...';
|
||||
}
|
||||
async function changeSettings(name: any) {
|
||||
if (ftpLoading) return;
|
||||
if ($status.service.isRunning) {
|
||||
ftpLoading = true;
|
||||
let ftpEnabled = service.wordpress.ftpEnabled;
|
||||
|
||||
if (name === 'ftpEnabled') {
|
||||
ftpEnabled = !ftpEnabled;
|
||||
}
|
||||
try {
|
||||
const {
|
||||
publicPort,
|
||||
ftpUser: user,
|
||||
ftpPassword: password
|
||||
} = await post(`/services/${id}/wordpress/ftp`, {
|
||||
ftpEnabled
|
||||
});
|
||||
ftpUrl = generateUrl(publicPort);
|
||||
ftpUser = user;
|
||||
ftpPassword = password;
|
||||
service.wordpress.ftpEnabled = ftpEnabled;
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
ftpLoading = false;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
if (name === 'ownMysql') {
|
||||
ownMysql = !ownMysql;
|
||||
}
|
||||
await post(`/services/${id}/wordpress/settings`, {
|
||||
ownMysql
|
||||
});
|
||||
service.wordpress.ownMysql = ownMysql;
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Wordpress</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="extraConfig">{$t('forms.extra_config')}</label>
|
||||
<textarea
|
||||
bind:value={service.wordpress.extraConfig}
|
||||
disabled={$status.service.isRunning}
|
||||
readonly={$status.service.isRunning}
|
||||
class:resize-none={$status.service.isRunning}
|
||||
rows="5"
|
||||
name="extraConfig"
|
||||
id="extraConfig"
|
||||
placeholder={!$status.service.isRunning
|
||||
? `${$t('forms.eg')}:
|
||||
|
||||
define('WP_ALLOW_MULTISITE', true);
|
||||
define('MULTISITE', true);
|
||||
define('SUBDOMAIN_INSTALL', false);`
|
||||
: 'N/A'}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<Setting
|
||||
bind:setting={service.wordpress.ftpEnabled}
|
||||
loading={ftpLoading}
|
||||
disabled={!$status.service.isRunning}
|
||||
on:click={() => changeSettings('ftpEnabled')}
|
||||
title="Enable sFTP connection to WordPress data"
|
||||
description="Enables an on-demand sFTP connection to the WordPress data directory. This is useful if you want to use sFTP to upload files."
|
||||
/>
|
||||
</div>
|
||||
{#if service.wordpress.ftpEnabled}
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="ftpUrl">sFTP Connection URI</label>
|
||||
<CopyPasswordField id="ftpUrl" readonly disabled name="ftpUrl" value={ftpUrl} />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="ftpUser">User</label>
|
||||
<CopyPasswordField id="ftpUser" readonly disabled name="ftpUser" value={ftpUser} />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="ftpPassword">Password</label>
|
||||
<CopyPasswordField id="ftpPassword" readonly disabled name="ftpPassword" value={ftpPassword} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">MySQL</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<Setting
|
||||
dataTooltip={$t('forms.must_be_stopped_to_modify')}
|
||||
bind:setting={service.wordpress.ownMysql}
|
||||
disabled={$status.service.isRunning}
|
||||
on:click={() => !$status.service.isRunning && changeSettings('ownMysql')}
|
||||
title="Use your own MySQL server"
|
||||
description="Enables the use of your own MySQL server. If you don't have one, you can use the one provided by Coolify."
|
||||
/>
|
||||
</div>
|
||||
{#if service.wordpress.ownMysql}
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="mysqlHost">Host</label>
|
||||
<input
|
||||
name="mysqlHost"
|
||||
id="mysqlHost"
|
||||
required
|
||||
readonly={$status.service.isRunning}
|
||||
disabled={$status.service.isRunning}
|
||||
bind:value={service.wordpress.mysqlHost}
|
||||
placeholder="{$t('forms.eg')}: db.coolify.io"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="mysqlPort">Port</label>
|
||||
<input
|
||||
name="mysqlPort"
|
||||
id="mysqlPort"
|
||||
required
|
||||
readonly={$status.service.isRunning}
|
||||
disabled={$status.service.isRunning}
|
||||
bind:value={service.wordpress.mysqlPort}
|
||||
placeholder="{$t('forms.eg')}: 3306"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="mysqlDatabase">{$t('index.database')}</label>
|
||||
<input
|
||||
name="mysqlDatabase"
|
||||
id="mysqlDatabase"
|
||||
required
|
||||
readonly={readOnly && !service.wordpress.ownMysql}
|
||||
disabled={readOnly && !service.wordpress.ownMysql}
|
||||
bind:value={service.wordpress.mysqlDatabase}
|
||||
placeholder="{$t('forms.eg')}: wordpress_db"
|
||||
/>
|
||||
</div>
|
||||
{#if !service.wordpress.ownMysql}
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="mysqlRootUser">{$t('forms.root_user')}</label>
|
||||
<input
|
||||
name="mysqlRootUser"
|
||||
id="mysqlRootUser"
|
||||
placeholder="MySQL {$t('forms.root_user')}"
|
||||
value={service.wordpress.mysqlRootUser}
|
||||
readonly={$status.service.isRunning || !service.wordpress.ownMysql}
|
||||
disabled={$status.service.isRunning || !service.wordpress.ownMysql}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="mysqlRootUserPassword">{$t('forms.roots_password')}</label>
|
||||
<CopyPasswordField
|
||||
id="mysqlRootUserPassword"
|
||||
isPasswordField
|
||||
readonly={$status.service.isRunning || !service.wordpress.ownMysql}
|
||||
disabled={$status.service.isRunning || !service.wordpress.ownMysql}
|
||||
name="mysqlRootUserPassword"
|
||||
value={service.wordpress.mysqlRootUserPassword}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="mysqlUser">{$t('forms.user')}</label>
|
||||
<input
|
||||
name="mysqlUser"
|
||||
id="mysqlUser"
|
||||
bind:value={service.wordpress.mysqlUser}
|
||||
readonly={$status.service.isRunning || !service.wordpress.ownMysql}
|
||||
disabled={$status.service.isRunning || !service.wordpress.ownMysql}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="mysqlPassword">{$t('forms.password')}</label>
|
||||
<CopyPasswordField
|
||||
id="mysqlPassword"
|
||||
isPasswordField
|
||||
readonly={$status.service.isRunning || !service.wordpress.ownMysql}
|
||||
disabled={$status.service.isRunning || !service.wordpress.ownMysql}
|
||||
name="mysqlPassword"
|
||||
bind:value={service.wordpress.mysqlPassword}
|
||||
/>
|
||||
</div>
|
||||
82
apps/ui/src/routes/services/[id]/_Storage.svelte
Normal file
82
apps/ui/src/routes/services/[id]/_Storage.svelte
Normal file
@@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
export let isNew = false;
|
||||
export let storage: any = {
|
||||
id: null,
|
||||
path: null
|
||||
};
|
||||
import { del, post } from '$lib/api';
|
||||
import { page } from '$app/stores';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { errorNotification } from '$lib/common';
|
||||
const { id } = $page.params;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
async function saveStorage(newStorage = false) {
|
||||
try {
|
||||
if (!storage.path) return errorNotification('Path is required.');
|
||||
storage.path = storage.path.startsWith('/') ? storage.path : `/${storage.path}`;
|
||||
storage.path = storage.path.endsWith('/') ? storage.path.slice(0, -1) : storage.path;
|
||||
storage.path.replace(/\/\//g, '/');
|
||||
await post(`/services/${id}/storages`, {
|
||||
path: storage.path,
|
||||
storageId: storage.id,
|
||||
newStorage
|
||||
});
|
||||
dispatch('refresh');
|
||||
if (isNew) {
|
||||
storage.path = null;
|
||||
storage.id = null;
|
||||
}
|
||||
if (newStorage) toast.push('Storage saved.');
|
||||
else toast.push('Storage updated.');
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function removeStorage() {
|
||||
try {
|
||||
await del(`/services/${id}/storages`, { path: storage.path });
|
||||
dispatch('refresh');
|
||||
toast.push('Storage deleted.');
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function handleSubmit() {
|
||||
if (isNew) {
|
||||
await saveStorage(true)
|
||||
} else {
|
||||
await saveStorage(false)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<td>
|
||||
<form on:submit|preventDefault={handleSubmit}>
|
||||
<input
|
||||
bind:value={storage.path}
|
||||
required
|
||||
placeholder="eg: /data"
|
||||
class=" border border-dashed border-coolgray-300"
|
||||
/>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
{#if isNew}
|
||||
<div class="flex items-center justify-center">
|
||||
<button class="bg-green-600 hover:bg-green-500" on:click={() => saveStorage(true)}>Add</button
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row justify-center space-x-2">
|
||||
<div class="flex items-center justify-center">
|
||||
<button class="" on:click={() => saveStorage(false)}>Set</button>
|
||||
</div>
|
||||
<div class="flex justify-center items-end">
|
||||
<button class="bg-red-600 hover:bg-red-500" on:click={removeStorage}>Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
366
apps/ui/src/routes/services/[id]/__layout.svelte
Normal file
366
apps/ui/src/routes/services/[id]/__layout.svelte
Normal file
@@ -0,0 +1,366 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
function checkConfiguration(service: any) {
|
||||
let configurationPhase = null;
|
||||
if (!service.type) {
|
||||
configurationPhase = 'type';
|
||||
} else if (!service.version) {
|
||||
configurationPhase = 'version';
|
||||
} else if (!service.destinationDockerId) {
|
||||
configurationPhase = 'destination';
|
||||
}
|
||||
return configurationPhase;
|
||||
}
|
||||
export const load: Load = async ({ params, url }) => {
|
||||
try {
|
||||
let readOnly = false;
|
||||
const response = await get(`/services/${params.id}`);
|
||||
const { service, settings } = await response;
|
||||
if (!service || Object.entries(service).length === 0) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/databases'
|
||||
};
|
||||
}
|
||||
const configurationPhase = checkConfiguration(service);
|
||||
if (
|
||||
configurationPhase &&
|
||||
url.pathname !== `/services/${params.id}/configuration/${configurationPhase}`
|
||||
) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/services/${params.id}/configuration/${configurationPhase}`
|
||||
};
|
||||
}
|
||||
if (service.plausibleAnalytics?.email && service.plausibleAnalytics.username) readOnly = true;
|
||||
if (service.wordpress?.mysqlDatabase) readOnly = true;
|
||||
if (service.ghost?.mariadbDatabase && service.ghost.mariadbDatabase) readOnly = true;
|
||||
|
||||
return {
|
||||
props: {
|
||||
service
|
||||
},
|
||||
stuff: {
|
||||
service,
|
||||
readOnly,
|
||||
settings
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return handlerNotFoundLoad(error, url);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
|
||||
import Loading from '$lib/components/Loading.svelte';
|
||||
import { del, get, post } from '$lib/api';
|
||||
import { goto } from '$app/navigation';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification, handlerNotFoundLoad } from '$lib/common';
|
||||
import { appSession, disabledButton, status } from '$lib/store';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
const { id } = $page.params;
|
||||
|
||||
export let service: any;
|
||||
|
||||
$disabledButton =
|
||||
!$appSession.isAdmin ||
|
||||
!service.fqdn ||
|
||||
!service.destinationDocker ||
|
||||
!service.version ||
|
||||
!service.type;
|
||||
|
||||
let loading = false;
|
||||
let statusInterval: any;
|
||||
|
||||
async function deleteService() {
|
||||
const sure = confirm($t('application.confirm_to_delete', { name: service.name }));
|
||||
if (sure) {
|
||||
loading = true;
|
||||
try {
|
||||
if (service.type && $status.service.isRunning)
|
||||
await post(`/services/${service.id}/${service.type}/stop`, {});
|
||||
await del(`/services/${service.id}`, { id: service.id });
|
||||
return await goto(`/services`);
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
async function stopService() {
|
||||
const sure = confirm($t('database.confirm_stop', { name: service.name }));
|
||||
if (sure) {
|
||||
loading = true;
|
||||
try {
|
||||
await post(`/services/${service.id}/${service.type}/stop`, {});
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
async function startService() {
|
||||
loading = true;
|
||||
try {
|
||||
await post(`/services/${service.id}/${service.type}/start`, {});
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
async function getStatus() {
|
||||
if ($status.service.loading) return;
|
||||
$status.service.loading = true;
|
||||
const data = await get(`/services/${id}`);
|
||||
$status.service.isRunning = data.isRunning;
|
||||
$status.service.initialLoading = false;
|
||||
$status.service.loading = false;
|
||||
}
|
||||
onDestroy(() => {
|
||||
$status.service.initialLoading = true;
|
||||
clearInterval(statusInterval);
|
||||
});
|
||||
onMount(async () => {
|
||||
$status.service.isRunning = false;
|
||||
$status.service.loading = false;
|
||||
if (service.type && service.destinationDockerId && service.version && service.fqdn) {
|
||||
await getStatus();
|
||||
statusInterval = setInterval(async () => {
|
||||
await getStatus();
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<nav class="nav-side">
|
||||
{#if loading}
|
||||
<Loading fullscreen cover />
|
||||
{:else}
|
||||
{#if service.type && service.destinationDockerId && service.version}
|
||||
{#if $status.service.initialLoading}
|
||||
<button
|
||||
class="icons tooltip-bottom flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
|
||||
<line x1="5.63" y1="7.16" x2="5.63" y2="7.17" />
|
||||
<line x1="4.06" y1="11" x2="4.06" y2="11.01" />
|
||||
<line x1="4.63" y1="15.1" x2="4.63" y2="15.11" />
|
||||
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
|
||||
<line x1="11" y1="19.94" x2="11" y2="19.95" />
|
||||
</svg>
|
||||
</button>
|
||||
{:else if $status.service.isRunning}
|
||||
<button
|
||||
on:click={stopService}
|
||||
title={$t('service.stop_service')}
|
||||
type="submit"
|
||||
disabled={$disabledButton}
|
||||
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-red-500"
|
||||
data-tooltip={$appSession.isAdmin
|
||||
? $t('service.stop_service')
|
||||
: $t('service.permission_denied_stop_service')}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<rect x="6" y="5" width="4" height="14" rx="1" />
|
||||
<rect x="14" y="5" width="4" height="14" rx="1" />
|
||||
</svg>
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
on:click={startService}
|
||||
title={$t('service.start_service')}
|
||||
type="submit"
|
||||
disabled={$disabledButton}
|
||||
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-green-500"
|
||||
data-tooltip={$appSession.isAdmin
|
||||
? $t('service.start_service')
|
||||
: $t('service.permission_denied_start_service')}
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
<div class="border border-stone-700 h-8" />
|
||||
{/if}
|
||||
{#if service.type && service.destinationDockerId && service.version}
|
||||
<a
|
||||
href="/services/{id}"
|
||||
sveltekit:prefetch
|
||||
class="hover:text-yellow-500 rounded"
|
||||
class:text-yellow-500={$page.url.pathname === `/services/${id}`}
|
||||
class:bg-coolgray-500={$page.url.pathname === `/services/${id}`}
|
||||
>
|
||||
<button
|
||||
title={$t('application.configurations')}
|
||||
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
|
||||
data-tooltip={$t('application.configurations')}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<rect x="4" y="8" width="4" height="4" />
|
||||
<line x1="6" y1="4" x2="6" y2="8" />
|
||||
<line x1="6" y1="12" x2="6" y2="20" />
|
||||
<rect x="10" y="14" width="4" height="4" />
|
||||
<line x1="12" y1="4" x2="12" y2="14" />
|
||||
<line x1="12" y1="18" x2="12" y2="20" />
|
||||
<rect x="16" y="5" width="4" height="4" />
|
||||
<line x1="18" y1="4" x2="18" y2="5" />
|
||||
<line x1="18" y1="9" x2="18" y2="20" />
|
||||
</svg></button
|
||||
></a
|
||||
>
|
||||
<a
|
||||
href="/services/{id}/secrets"
|
||||
sveltekit:prefetch
|
||||
class="hover:text-pink-500 rounded"
|
||||
class:text-pink-500={$page.url.pathname === `/services/${id}/secrets`}
|
||||
class:bg-coolgray-500={$page.url.pathname === `/services/${id}/secrets`}
|
||||
>
|
||||
<button
|
||||
title={$t('application.secret')}
|
||||
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
|
||||
data-tooltip={$t('application.secret')}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3"
|
||||
/>
|
||||
<circle cx="12" cy="11" r="1" />
|
||||
<line x1="12" y1="12" x2="12" y2="14.5" />
|
||||
</svg></button
|
||||
></a
|
||||
>
|
||||
<a
|
||||
href="/services/{id}/storages"
|
||||
sveltekit:prefetch
|
||||
class="hover:text-pink-500 rounded"
|
||||
class:text-pink-500={$page.url.pathname === `/services/${id}/storages`}
|
||||
class:bg-coolgray-500={$page.url.pathname === `/services/${id}/storages`}
|
||||
>
|
||||
<button
|
||||
title="Persistent Storage"
|
||||
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
|
||||
data-tooltip="Persistent Storage"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<ellipse cx="12" cy="6" rx="8" ry="3" />
|
||||
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
|
||||
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
|
||||
</svg>
|
||||
</button></a
|
||||
>
|
||||
<div class="border border-stone-700 h-8" />
|
||||
<a
|
||||
href={$status.service.isRunning ? `/services/${id}/logs` : null}
|
||||
sveltekit:prefetch
|
||||
class="hover:text-pink-500 rounded"
|
||||
class:text-pink-500={$page.url.pathname === `/services/${id}/logs`}
|
||||
class:bg-coolgray-500={$page.url.pathname === `/services/${id}/logs`}
|
||||
>
|
||||
<button
|
||||
title={$t('service.logs')}
|
||||
disabled={!$status.service.isRunning}
|
||||
class="icons bg-transparent tooltip-bottom text-sm"
|
||||
data-tooltip={$t('service.logs')}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
|
||||
<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
|
||||
<line x1="3" y1="6" x2="3" y2="19" />
|
||||
<line x1="12" y1="6" x2="12" y2="19" />
|
||||
<line x1="21" y1="6" x2="21" y2="19" />
|
||||
</svg></button
|
||||
></a
|
||||
>
|
||||
{/if}
|
||||
<button
|
||||
on:click={deleteService}
|
||||
title={$t('service.delete_service')}
|
||||
type="submit"
|
||||
disabled={!$appSession.isAdmin}
|
||||
class:hover:text-red-500={$appSession.isAdmin}
|
||||
class="icons bg-transparent tooltip-bottom text-sm"
|
||||
data-tooltip={$appSession.isAdmin
|
||||
? $t('service.delete_service')
|
||||
: $t('service.permission_denied_delete_service')}><DeleteIcon /></button
|
||||
>
|
||||
{/if}
|
||||
</nav>
|
||||
<slot />
|
||||
@@ -0,0 +1,90 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
try {
|
||||
const { service } = stuff;
|
||||
if (service?.destinationDockerId && !url.searchParams.get('from')) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/services/${params.id}`
|
||||
};
|
||||
}
|
||||
const response = await get(`/destinations`);
|
||||
return {
|
||||
props: {
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let destinations: any;
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { get, post } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
|
||||
async function handleSubmit(destinationId: any) {
|
||||
try {
|
||||
await post(`/services/${id}/configuration/destination`, { destinationId });
|
||||
return await goto(from || `/services/${id}`);
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">
|
||||
{$t('application.configuration.configure_destination')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
{#if !destinations || destinations.length === 0}
|
||||
<div class="flex-col">
|
||||
<div class="pb-2">{$t('application.configuration.no_configurable_destination')}</div>
|
||||
<div class="flex justify-center">
|
||||
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
|
||||
<svg
|
||||
class="w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||
/></svg
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#each destinations as destination}
|
||||
<div class="p-2">
|
||||
<form on:submit|preventDefault={() => handleSubmit(destination.id)}>
|
||||
<button type="submit" class="box-selection hover:bg-sky-700 font-bold">
|
||||
<div class="font-bold text-xl text-center truncate">{destination.name}</div>
|
||||
<div class="text-center truncate">{destination.network}</div>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
106
apps/ui/src/routes/services/[id]/configuration/type.svelte
Normal file
106
apps/ui/src/routes/services/[id]/configuration/type.svelte
Normal file
@@ -0,0 +1,106 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
try {
|
||||
const { service } = stuff;
|
||||
if (service?.type && !url.searchParams.get('from')) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/services/${params.id}`
|
||||
};
|
||||
}
|
||||
const response = await get(`/services/${params.id}/configuration/type`);
|
||||
return {
|
||||
props: {
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let types: any;
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { get, post } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
|
||||
import PlausibleAnalytics from '$lib/components/svg/services/PlausibleAnalytics.svelte';
|
||||
import NocoDb from '$lib/components/svg/services/NocoDB.svelte';
|
||||
import MinIo from '$lib/components/svg/services/MinIO.svelte';
|
||||
import VsCodeServer from '$lib/components/svg/services/VSCodeServer.svelte';
|
||||
import Wordpress from '$lib/components/svg/services/Wordpress.svelte';
|
||||
import VaultWarden from '$lib/components/svg/services/VaultWarden.svelte';
|
||||
import LanguageTool from '$lib/components/svg/services/LanguageTool.svelte';
|
||||
import N8n from '$lib/components/svg/services/N8n.svelte';
|
||||
import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte';
|
||||
import Ghost from '$lib/components/svg/services/Ghost.svelte';
|
||||
import MeiliSearch from '$lib/components/svg/services/MeiliSearch.svelte';
|
||||
import Umami from '$lib/components/svg/services/Umami.svelte';
|
||||
import Hasura from '$lib/components/svg/services/Hasura.svelte';
|
||||
import Fider from '$lib/components/svg/services/Fider.svelte';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
|
||||
async function handleSubmit(type: any) {
|
||||
try {
|
||||
await post(`/services/${id}/configuration/type`, { type });
|
||||
return await goto(from || `/services/${id}`);
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">{$t('forms.select_a_service')}</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#each types as type}
|
||||
<div class="p-2">
|
||||
<form on:submit|preventDefault={() => handleSubmit(type.name)}>
|
||||
<button type="submit" class="box-selection relative text-xl font-bold hover:bg-pink-600">
|
||||
{#if type.name === 'plausibleanalytics'}
|
||||
<PlausibleAnalytics isAbsolute />
|
||||
{:else if type.name === 'nocodb'}
|
||||
<NocoDb isAbsolute />
|
||||
{:else if type.name === 'minio'}
|
||||
<MinIo isAbsolute />
|
||||
{:else if type.name === 'vscodeserver'}
|
||||
<VsCodeServer isAbsolute />
|
||||
{:else if type.name === 'wordpress'}
|
||||
<Wordpress isAbsolute />
|
||||
{:else if type.name === 'vaultwarden'}
|
||||
<VaultWarden isAbsolute />
|
||||
{:else if type.name === 'languagetool'}
|
||||
<LanguageTool isAbsolute />
|
||||
{:else if type.name === 'n8n'}
|
||||
<N8n isAbsolute />
|
||||
{:else if type.name === 'uptimekuma'}
|
||||
<UptimeKuma isAbsolute />
|
||||
{:else if type.name === 'ghost'}
|
||||
<Ghost isAbsolute />
|
||||
{:else if type.name === 'meilisearch'}
|
||||
<MeiliSearch isAbsolute />
|
||||
{:else if type.name === 'umami'}
|
||||
<Umami isAbsolute />
|
||||
{:else if type.name === 'hasura'}
|
||||
<Hasura isAbsolute />
|
||||
{:else if type.name === 'fider'}
|
||||
<Fider isAbsolute />
|
||||
{/if}{type.fancyName}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -0,0 +1,79 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
try {
|
||||
const { service } = stuff;
|
||||
if (service?.version && !url.searchParams.get('from')) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/services/${params.id}`
|
||||
};
|
||||
}
|
||||
const response = await get(`/services/${params.id}/configuration/version`);
|
||||
return {
|
||||
props: {
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let versions: any;
|
||||
export let type: any;
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { get, post } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification, supportedServiceTypesAndVersions } from '$lib/common';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
|
||||
let recommendedVersion = supportedServiceTypesAndVersions.find(
|
||||
({ name }) => name === type
|
||||
)?.recommendedVersion;
|
||||
async function handleSubmit(version: any) {
|
||||
try {
|
||||
await post(`/services/${id}/configuration/version`, { version });
|
||||
return await goto(from || `/services/${id}`);
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">{$t('forms.select_a_service_version')}</div>
|
||||
</div>
|
||||
{#if from}
|
||||
<div class="pb-10 text-center">
|
||||
Warning: you are about to change the version of this service.<br />This could cause problem
|
||||
after you restart the service,
|
||||
<span class="font-bold text-pink-600">like losing your data, incompatibility issues, etc</span
|
||||
>.<br />Only do if you know what you are doing.
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#each versions as version}
|
||||
<div class="p-2">
|
||||
<form on:submit|preventDefault={() => handleSubmit(version)}>
|
||||
<button
|
||||
type="submit"
|
||||
class:bg-pink-500={recommendedVersion === version}
|
||||
class="box-selection relative flex text-xl font-bold hover:bg-pink-600"
|
||||
>{version}
|
||||
{#if recommendedVersion === version}
|
||||
<span class="absolute bottom-0 pb-2 text-xs">recommended</span>
|
||||
{/if}</button
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
113
apps/ui/src/routes/services/[id]/index.svelte
Normal file
113
apps/ui/src/routes/services/[id]/index.svelte
Normal file
@@ -0,0 +1,113 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ stuff }) => {
|
||||
return {
|
||||
props: { ...stuff }
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import Services from './_Services/_Services.svelte';
|
||||
import { get } from '$lib/api';
|
||||
import { page } from '$app/stores';
|
||||
import { status } from '$lib/store';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import ServiceLinks from './_ServiceLinks.svelte';
|
||||
|
||||
export let service: any;
|
||||
export let readOnly: any;
|
||||
export let settings: any;
|
||||
|
||||
const { id } = $page.params;
|
||||
let loading = {
|
||||
usage: false
|
||||
};
|
||||
let usage = {
|
||||
MemUsage: 0,
|
||||
CPUPerc: 0,
|
||||
NetIO: 0
|
||||
};
|
||||
let usageInterval: any;
|
||||
|
||||
async function getUsage() {
|
||||
if (loading.usage) return;
|
||||
if (!$status.service.isRunning) return;
|
||||
loading.usage = true;
|
||||
const data = await get(`/services/${id}/usage`);
|
||||
usage = data.usage;
|
||||
loading.usage = false;
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
clearInterval(usageInterval);
|
||||
});
|
||||
onMount(async () => {
|
||||
await getUsage();
|
||||
usageInterval = setInterval(async () => {
|
||||
await getUsage();
|
||||
}, 1000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold">
|
||||
<div class="-mb-5 flex-col">
|
||||
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
|
||||
Configuration
|
||||
</div>
|
||||
<span class="text-xs">{service.name}</span>
|
||||
</div>
|
||||
|
||||
{#if service.fqdn}
|
||||
<a
|
||||
href={service.fqdn}
|
||||
target="_blank"
|
||||
class="icons tooltip-bottom flex items-center bg-transparent text-sm"
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
|
||||
<line x1="10" y1="14" x2="20" y2="4" />
|
||||
<polyline points="15 4 20 4 20 9" />
|
||||
</svg></a
|
||||
>
|
||||
{/if}
|
||||
|
||||
<ServiceLinks {service} />
|
||||
</div>
|
||||
<div class="mx-auto max-w-4xl px-6 py-4">
|
||||
<div class="text-2xl font-bold">Service Usage</div>
|
||||
<div class="mx-auto">
|
||||
<dl class="relative mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3">
|
||||
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left">
|
||||
<dt class=" text-sm font-medium text-white">Used Memory / Memory Limit</dt>
|
||||
<dd class="mt-1 text-xl font-semibold text-white">
|
||||
{usage?.MemUsage}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left">
|
||||
<dt class="truncate text-sm font-medium text-white">Used CPU</dt>
|
||||
<dd class="mt-1 text-xl font-semibold text-white ">
|
||||
{usage?.CPUPerc}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left">
|
||||
<dt class="truncate text-sm font-medium text-white">Network IO</dt>
|
||||
<dd class="mt-1 text-xl font-semibold text-white ">
|
||||
{usage?.NetIO}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<Services bind:service bind:readOnly bind:settings />
|
||||
41
apps/ui/src/routes/services/[id]/logs/_Loading.svelte
Normal file
41
apps/ui/src/routes/services/[id]/logs/_Loading.svelte
Normal file
@@ -0,0 +1,41 @@
|
||||
<div class="lds-ripple absolute left-0">
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.lds-ripple {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
left: -19px;
|
||||
top: -8px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.lds-ripple div {
|
||||
position: absolute;
|
||||
border: 4px solid #fff;
|
||||
opacity: 1;
|
||||
border-radius: 50%;
|
||||
animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
||||
}
|
||||
.lds-ripple div:nth-child(2) {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
@keyframes lds-ripple {
|
||||
0% {
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
178
apps/ui/src/routes/services/[id]/logs/index.svelte
Normal file
178
apps/ui/src/routes/services/[id]/logs/index.svelte
Normal file
@@ -0,0 +1,178 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
try {
|
||||
const response = await get(`/services/${params.id}/logs`);
|
||||
return {
|
||||
props: {
|
||||
service: stuff.service,
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let service: any;
|
||||
import { page } from '$app/stores';
|
||||
import LoadingLogs from './_Loading.svelte';
|
||||
import { get } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
|
||||
let loadLogsInterval: any = null;
|
||||
let logs: any = [];
|
||||
let lastLog: any = null;
|
||||
let followingInterval: any;
|
||||
let followingLogs: any;
|
||||
let logsEl: any;
|
||||
let position = 0;
|
||||
|
||||
const { id } = $page.params;
|
||||
onMount(async () => {
|
||||
loadAllLogs();
|
||||
loadLogsInterval = setInterval(() => {
|
||||
loadLogs();
|
||||
}, 1000);
|
||||
});
|
||||
onDestroy(() => {
|
||||
clearInterval(loadLogsInterval);
|
||||
clearInterval(followingInterval);
|
||||
});
|
||||
async function loadAllLogs() {
|
||||
try {
|
||||
const data: any = await get(`/services/${id}/logs`);
|
||||
if (data?.logs) {
|
||||
lastLog = data.logs[data.logs.length - 1];
|
||||
logs = data.logs;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function loadLogs() {
|
||||
try {
|
||||
const newLogs: any = await get(
|
||||
`/services/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`
|
||||
);
|
||||
|
||||
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
|
||||
logs = logs.concat(newLogs.logs);
|
||||
lastLog = newLogs.logs[newLogs.logs.length - 1];
|
||||
}
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
function detect() {
|
||||
if (position < logsEl.scrollTop) {
|
||||
position = logsEl.scrollTop;
|
||||
} else {
|
||||
if (followingLogs) {
|
||||
clearInterval(followingInterval);
|
||||
followingLogs = false;
|
||||
}
|
||||
position = logsEl.scrollTop;
|
||||
}
|
||||
}
|
||||
|
||||
function followBuild() {
|
||||
followingLogs = !followingLogs;
|
||||
if (followingLogs) {
|
||||
followingInterval = setInterval(() => {
|
||||
logsEl.scrollTop = logsEl.scrollHeight;
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
}, 1000);
|
||||
} else {
|
||||
clearInterval(followingInterval);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold">
|
||||
<div class="-mb-5 flex-col">
|
||||
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
|
||||
Service Logs
|
||||
</div>
|
||||
<span class="text-xs">{service.name}</span>
|
||||
</div>
|
||||
|
||||
{#if service.fqdn}
|
||||
<a
|
||||
href={service.fqdn}
|
||||
target="_blank"
|
||||
class="icons tooltip-bottom flex items-center bg-transparent text-sm"
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
|
||||
<line x1="10" y1="14" x2="20" y2="4" />
|
||||
<polyline points="15 4 20 4 20 9" />
|
||||
</svg></a
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-row justify-center space-x-2 px-10 pt-6">
|
||||
{#if logs.length === 0}
|
||||
<div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div>
|
||||
{:else}
|
||||
<div class="relative w-full">
|
||||
<div class="text-right " />
|
||||
{#if loadLogsInterval}
|
||||
<LoadingLogs />
|
||||
{/if}
|
||||
<div class="flex justify-end sticky top-0 p-2 mx-1">
|
||||
<button
|
||||
on:click={followBuild}
|
||||
class="bg-transparent"
|
||||
data-tooltip="Follow logs"
|
||||
class:text-green-500={followingLogs}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<line x1="8" y1="12" x2="12" y2="16" />
|
||||
<line x1="12" y1="8" x2="12" y2="16" />
|
||||
<line x1="16" y1="12" x2="12" y2="16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="font-mono w-full leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
|
||||
bind:this={logsEl}
|
||||
on:scroll={detect}
|
||||
>
|
||||
<div class="px-2 pr-14">
|
||||
{#each logs as log}
|
||||
{log + '\n'}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
96
apps/ui/src/routes/services/[id]/secrets.svelte
Normal file
96
apps/ui/src/routes/services/[id]/secrets.svelte
Normal file
@@ -0,0 +1,96 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, stuff, url }) => {
|
||||
try {
|
||||
const response = await get(`/services/${params.id}/secrets`);
|
||||
return {
|
||||
props: {
|
||||
service: stuff.service,
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let secrets: any;
|
||||
export let service: any;
|
||||
import Secret from './_Secret.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { get } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import ServiceLinks from './_ServiceLinks.svelte';
|
||||
|
||||
const { id } = $page.params;
|
||||
|
||||
async function refreshSecrets() {
|
||||
const data = await get(`/services/${id}/secrets`);
|
||||
secrets = [...data.secrets];
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex items-center space-x-2 p-5 px-6 font-bold"
|
||||
class:p-5={service.fqdn}
|
||||
class:p-6={!service.fqdn}
|
||||
>
|
||||
<div class="-mb-5 flex-col">
|
||||
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
|
||||
{$t('application.secret')}
|
||||
</div>
|
||||
<span class="text-xs">{service.name}</span>
|
||||
</div>
|
||||
{#if service.fqdn}
|
||||
<a
|
||||
href={service.fqdn}
|
||||
target="_blank"
|
||||
class="icons tooltip-bottom flex items-center bg-transparent text-sm"
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
|
||||
<line x1="10" y1="14" x2="20" y2="4" />
|
||||
<polyline points="15 4 20 4 20 9" />
|
||||
</svg></a
|
||||
>
|
||||
{/if}
|
||||
|
||||
<ServiceLinks {service} />
|
||||
</div>
|
||||
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">
|
||||
<table class="mx-auto border-separate text-left">
|
||||
<thead>
|
||||
<tr class="h-12">
|
||||
<th scope="col">{$t('forms.name')}</th>
|
||||
<th scope="col">{$t('forms.value')}</th>
|
||||
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each secrets as secret}
|
||||
{#key secret.id}
|
||||
<tr>
|
||||
<Secret name={secret.name} value={secret.value} on:refresh={refreshSecrets} />
|
||||
</tr>
|
||||
{/key}
|
||||
{/each}
|
||||
<tr>
|
||||
<Secret isNewSecret on:refresh={refreshSecrets} />
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
101
apps/ui/src/routes/services/[id]/storages.svelte
Normal file
101
apps/ui/src/routes/services/[id]/storages.svelte
Normal file
@@ -0,0 +1,101 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ params, stuff, url }) => {
|
||||
try {
|
||||
const response = await get(`/services/${params.id}/storages`);
|
||||
return {
|
||||
props: {
|
||||
service: stuff.service,
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let service: any;
|
||||
export let persistentStorages: any;
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import Storage from './_Storage.svelte';
|
||||
import { get } from '$lib/api';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import ServiceLinks from './_ServiceLinks.svelte';
|
||||
|
||||
const { id } = $page.params;
|
||||
async function refreshStorage() {
|
||||
const data = await get(`/services/${id}/storages`);
|
||||
persistentStorages = [...data.persistentStorages];
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex items-center space-x-2 p-5 px-6 font-bold"
|
||||
class:p-5={service.fqdn}
|
||||
class:p-6={!service.fqdn}
|
||||
>
|
||||
<div class="-mb-5 flex-col">
|
||||
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
|
||||
Persistent Storage
|
||||
</div>
|
||||
<span class="text-xs">{service.name}</span>
|
||||
</div>
|
||||
{#if service.fqdn}
|
||||
<a
|
||||
href={service.fqdn}
|
||||
target="_blank"
|
||||
class="icons tooltip-bottom flex items-center bg-transparent text-sm"
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
|
||||
<line x1="10" y1="14" x2="20" y2="4" />
|
||||
<polyline points="15 4 20 4 20 9" />
|
||||
</svg></a
|
||||
>
|
||||
{/if}
|
||||
|
||||
<ServiceLinks {service} />
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">
|
||||
<div class="flex justify-center py-4 text-center">
|
||||
<Explainer
|
||||
customClass="w-full"
|
||||
text={'You can specify any folder that you want to be persistent across restarts. <br>This is useful for storing data for VSCode server or WordPress.'}
|
||||
/>
|
||||
</div>
|
||||
<table class="mx-auto border-separate text-left">
|
||||
<thead>
|
||||
<tr class="h-12">
|
||||
<th scope="col">Path</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each persistentStorages as storage}
|
||||
{#key storage.id}
|
||||
<tr>
|
||||
<Storage on:refresh={refreshStorage} {storage} />
|
||||
</tr>
|
||||
{/key}
|
||||
{/each}
|
||||
<tr>
|
||||
<Storage on:refresh={refreshStorage} isNew />
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
Reference in New Issue
Block a user