work-work

This commit is contained in:
Andras Bacsai 2022-10-17 15:43:57 +02:00
parent a7e86d9afd
commit 8f660c0276
12 changed files with 322 additions and 105 deletions

View File

@ -0,0 +1,13 @@
-- CreateTable
CREATE TABLE "ServiceSetting" (
"id" TEXT NOT NULL PRIMARY KEY,
"serviceId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"value" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ServiceSetting_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "ServiceSetting_serviceId_name_key" ON "ServiceSetting"("serviceId", "name");

View File

@ -398,6 +398,7 @@ model Service {
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
persistentStorage ServicePersistentStorage[]
serviceSecret ServiceSecret[]
serviceSetting ServiceSetting[]
teams Team[]
fider Fider?
@ -417,6 +418,18 @@ model Service {
taiga Taiga?
}
model ServiceSetting {
id String @id @default(cuid())
serviceId String
name String
value String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
@@unique([serviceId, name])
}
model PlausibleAnalytics {
id String @id @default(cuid())
email String?

View File

@ -1454,7 +1454,6 @@ export async function getServiceFromDB({
}
let { type } = body;
type = fixType(type);
if (body?.serviceSecret.length > 0) {
body.serviceSecret = body.serviceSecret.map((s) => {
s.value = decrypt(s.value);
@ -1462,7 +1461,7 @@ export async function getServiceFromDB({
});
}
body[type] = { ...body[type], ...getUpdateableFields(type, body[type]) };
// body[type] = { ...body[type], ...getUpdateableFields(type, body[type]) };
return { ...body, settings };
}

View File

@ -6,6 +6,7 @@ export const includeServices: any = {
destinationDocker: true,
persistentStorage: true,
serviceSecret: true,
serviceSetting: true,
minio: true,
plausibleAnalytics: true,
vscodeserver: true,
@ -362,6 +363,7 @@ export async function configureServiceType({
export async function removeService({ id }: { id: string }): Promise<void> {
await prisma.serviceSecret.deleteMany({ where: { serviceId: id } });
await prisma.serviceSetting.deleteMany({ where: { serviceId: id } });
await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } });
await prisma.meiliSearch.deleteMany({ where: { serviceId: id } });
await prisma.fider.deleteMany({ where: { serviceId: id } });

View File

@ -7,6 +7,7 @@ import { asyncSleep, ComposeFile, createDirectories, defaultComposeConfiguration
import { defaultServiceConfigurations } from '../services';
import { OnlyId } from '../../types';
import templates from '../templates'
import { parseAndFindServiceTemplates } from '../../routes/api/v1/services/handlers';
// export async function startService(request: FastifyRequest<ServiceStartStop>) {
// try {
@ -691,36 +692,36 @@ export async function startService(request: FastifyRequest<ServiceStartStop>) {
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } =
const { type, version, destinationDockerId, destinationDocker, serviceSecret,serviceSetting, exposePort, persistentStorage } =
service;
let template = templates.find((template) => template.name === type);
template = JSON.parse(JSON.stringify(template).replaceAll('$$id', id).replaceAll('$$fqdn', service.fqdn))
const { workdir } = await createDirectories({ repository: type, buildId: id });
const template: any = await parseAndFindServiceTemplates(service, workdir, true)
const network = destinationDockerId && destinationDocker.network;
const config = {};
for (const service in template.services) {
console.log(template.services[service])
config[service] = {
container_name: id,
image: template.services[service].image.replace('$$core_version', version),
container_name: service,
image: template.services[service].image,
expose: template.services[service].ports,
// ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
volumes: template.services[service].volumes,
environment: {},
environment: template.services[service].environment,
depends_on: template.services[service].depends_on,
ulimits: template.services[service].ulimits,
labels: makeLabelForServices(type),
...defaultComposeConfiguration(network),
}
if (serviceSecret.length > 0) {
serviceSecret.forEach((secret) => {
config[service].environment[secret.name] = secret.value;
});
// Generate files for builds
if (template.services[service].build) {
if (template.services[service]?.extras?.files?.length > 0) {
console.log(template.services[service]?.extras?.files)
}
}
}
const { workdir } = await createDirectories({ repository: type, buildId: id });
const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = {

View File

@ -4,10 +4,10 @@ export default [
"serviceDefaultVersion": "0.198.1",
"name": "n8n",
"displayName": "n8n.io",
"isOfficial": true,
"description": "n8n is a free and open node based Workflow Automation Tool.",
"services": {
"$$id": {
"name": "N8n",
"documentation": "Taken from https://hub.docker.com/r/n8nio/n8n",
"depends_on": [],
"image": "n8nio/n8n:$$core_version",
@ -17,24 +17,31 @@ export default [
"/var/run/docker.sock:/var/run/docker.sock"
],
"environment": [
"WEBHOOK_URL=$$fqdn"
"WEBHOOK_URL=$$config_webhook_url"
],
"ports": [
"5678"
]
}
},
"variables": []
"variables": [
{
"id": "$$config_webhook_url",
"name": "WEBHOOK_URL",
"label": "Webhook URL",
"defaultValue": "$$generate_fqdn",
"description": "",
}]
},
{
"templateVersion": "1.0.0",
"serviceDefaultVersion": "stable",
"name": "plausibleanalytics",
"displayName": "PlausibleAnalytics",
"isOfficial": true,
"description": "Plausible is a lightweight and open-source website analytics tool.",
"services": {
"$$id": {
"name": "Plausible Analytics",
"documentation": "Taken from https://plausible.io/",
"command": ['sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh db init-admin && /entrypoint.sh run"'],
"depends_on": [
@ -43,31 +50,33 @@ export default [
],
"image": "plausible/analytics:$$core_version",
"environment": [
"ADMIN_USER_EMAIL=$$secret_email",
"ADMIN_USER_NAME=$$secret_name",
"ADMIN_USER_PASSWORD=$$secret_password",
"BASE_URL=$$fqdn",
"SECRET_KEY_BASE=$$secret_key_base",
"DISABLE_AUTH=$$secret_disable_auth",
"DISABLE_REGISTRATION=$$secret_disable_registration",
"DATABASE_URL=postgresql://$$secret_postgresql_username:$$secret_postgresql_password@$$id-postgresql:5432/$$secret_postgresql_database",
"CLICKHOUSE_DATABASE_URL=http://$$id-clickhouse:8123/plausible",
"ADMIN_USER_EMAIL=$$config_admin_user_email",
"ADMIN_USER_NAME=$$config_admin_user_name",
"ADMIN_USER_PASSWORD=$$secret_admin_user_password",
"BASE_URL=$$config_base_url",
"SECRET_KEY_BASE=$$secret_secret_key_base",
"DISABLE_AUTH=$$config_disable_auth",
"DISABLE_REGISTRATION=$$config_disable_registration",
"DATABASE_URL=$$secret_database_url",
"CLICKHOUSE_DATABASE_URL=$$secret_clickhouse_database_url",
],
"ports": [
"8000"
],
},
"$$id-postgresql": {
"name": "PostgreSQL",
"documentation": "Taken from https://plausible.io/",
"image": "bitnami/postgresql:13.2.0",
"environment": [
"POSTGRESQL_PASSWORD=$$secret_postgresql_password",
"POSTGRESQL_USERNAME=$$secret_postgresql_username",
"POSTGRESQL_DATABASE=$$secret_postgresql_database",
"POSTGRESQL_USERNAME=$$config_postgresql_username",
"POSTGRESQL_DATABASE=$$config_postgresql_database",
],
},
"$$id-clickhouse": {
"name": "Clickhouse",
"documentation": "Taken from https://plausible.io/",
"build": "$$workdir",
"image": "yandex/clickhouse-server:21.3.2.5",
@ -102,69 +111,98 @@ export default [
},
"variables": [
{
"id": "$$secret_email",
"label": "Admin Email",
"id": "$$config_base_url",
"name": "BASE_URL",
"label": "Base URL",
"defaultValue": "$$generate_fqdn",
"description": "You must set this to the FQDN of the Plausible Analytics instance. This is used to generate the links to the Plausible Analytics instance.",
},
{
"id": "$$secret_database_url",
"name": "DATABASE_URL",
"label": "Database URL for PostgreSQL",
"defaultValue": "postgresql://$$config_postgresql_username:$$secret_postgresql_password@$$id-postgresql:5432/$$config_postgresql_database",
"description": "",
},
{
"id": "$$secret_clickhouse_database_url",
"name": "CLICKHOUSE_DATABASE_URL",
"label": "Database URL for Clickhouse",
"defaultValue": "http://$$id-clickhouse:8123/plausible",
"description": "",
},
{
"id": "$$config_admin_user_email",
"name": "ADMIN_USER_EMAIL",
"label": "Admin Email Address",
"defaultValue": "admin@example.com",
"description": "This is the admin email. Please change it.",
"validRegex": /^([^\s^\/])+$/
},
{
"id": "$$secret_name",
"label": "Admin Name",
"id": "$$config_admin_user_name",
"name": "ADMIN_USER_NAME",
"label": "Admin User Name",
"defaultValue": "$$generate_username",
"description": "This is the admin username. Please change it.",
"validRegex": /^([^\s^\/])+$/
},
{
"id": "$$secret_password",
"label": "Admin Password",
"defaultValue":"$$generate_password",
"id": "$$secret_admin_user_password",
"name": "ADMIN_USER_PASSWORD",
"showAsConfiguration": true,
"label": "Admin User Password",
"defaultValue": "$$generate_password",
"description": "This is the admin password. Please change it.",
"validRegex": /^([^\s^\/])+$/
},
{
"id": "$$secret_secret_key_base",
"name": "SECRET_KEY_BASE",
"label": "Secret Key Base",
"defaultValue":"$$generate_passphrase",
"defaultValue": "$$generate_passphrase",
"description": "",
"validRegex": /^([^\s^\/])+$/
},
{
"id": "$$secret_disable_auth",
"label": "Disable Auth",
"id": "$$config_disable_auth",
"name": "DISABLE_AUTH",
"label": "Disable Authentication",
"defaultValue": "false",
"description": "",
"validRegex": /^([^\s^\/])+$/
},
{
"id": "$$secret_disable_registration",
"id": "$$config_disable_registration",
"name": "DISABLE_REGISTRATION",
"label": "Disable Registration",
"defaultValue": "true",
"description": "",
"validRegex": /^([^\s^\/])+$/
},
{
"id": "$$secret_postgresql_username",
"id": "$$config_postgresql_username",
"name": "POSTGRESQL_USERNAME",
"label": "PostgreSQL Username",
"defaultValue": "postgresql",
"description": "",
"validRegex": /^([^\s^\/])+$/
},
{
"id": "$$secret_postgresql_password",
"name": "POSTGRESQL_PASSWORD",
"label": "PostgreSQL Password",
"defaultValue": "$$generate_password",
"description": "",
"validRegex": /^([^\s^\/])+$/
}
,
{
"id": "$$secret_postgresql_database",
"id": "$$config_postgresql_database",
"name": "POSTGRESQL_DATABASE",
"label": "PostgreSQL Database",
"defaultValue": "plausible",
"description": "",
"validRegex": /^([^\s^\/])+$/
}
},
{
"id": "$$config_scriptName",
"name": "SCRIPT_NAME",
"label": "Custom Script Name",
"defaultValue": "plausible.js",
"description": "This is the default script name.",
},
]
}
]

View File

@ -114,13 +114,70 @@ export async function getServiceStatus(request: FastifyRequest<OnlyId>) {
return errorHandler({ status, message })
}
}
function parseAndFindServiceTemplates(service: any) {
export async function parseAndFindServiceTemplates(service: any, workdir?: string, isDeploy: boolean = false) {
const foundTemplate = templates.find(t => t.name === service.type)
let parsedTemplate = {}
if (foundTemplate) {
return JSON.parse(JSON.stringify(foundTemplate).replaceAll('$$id', service.id).replaceAll('$$fqdn', service.fqdn))
if (!isDeploy) {
for (const [key, value] of Object.entries(foundTemplate.services)) {
const realKey = key.replace('$$id', service.id)
parsedTemplate[realKey] = {
name: value.name,
image: value.image,
environment: []
}
if (value.environment?.length > 0) {
for (const env of value.environment) {
const [envKey, envValue] = env.split('=')
const label = foundTemplate.variables.find(v => v.name === envKey)?.label
const description = foundTemplate.variables.find(v => v.name === envKey)?.description
const defaultValue = foundTemplate.variables.find(v => v.name === envKey)?.defaultValue
const showAsConfiguration = foundTemplate.variables.find(v => v.name === envKey)?.showAsConfiguration
if (envValue.startsWith('$$config') || showAsConfiguration) {
parsedTemplate[realKey].environment.push(
{ name: envKey, value: envValue, label, description, defaultValue }
)
}
}
}
}
} else {
parsedTemplate = foundTemplate
}
// replace $$id and $$workdir
parsedTemplate = JSON.parse(JSON.stringify(parsedTemplate).replaceAll('$$id', service.id).replaceAll('$$core_version', foundTemplate.serviceDefaultVersion))
// replace $$fqdn
if (workdir) {
parsedTemplate = JSON.parse(JSON.stringify(parsedTemplate).replaceAll('$$workdir', workdir))
}
// replace $$config
if (service.serviceSetting.length > 0) {
for (const setting of service.serviceSetting) {
const { name, value } = setting
if (service.fqdn && value === '$$generate_fqdn') {
parsedTemplate = JSON.parse(JSON.stringify(parsedTemplate).replaceAll(`$$config_${name.toLowerCase()}`, service.fqdn))
} else {
parsedTemplate = JSON.parse(JSON.stringify(parsedTemplate).replaceAll(`$$config_${name.toLowerCase()}`, value))
}
}
}
// replace $$secret
if (service.serviceSecret.length > 0) {
for (const secret of service.serviceSecret) {
const { name, value } = secret
parsedTemplate = JSON.parse(JSON.stringify(parsedTemplate).replaceAll(`$$secret_${name.toLowerCase()}`, value))
}
}
}
return parsedTemplate
}
export async function getService(request: FastifyRequest<OnlyId>) {
try {
const teamId = request.user.teamId;
@ -129,10 +186,11 @@ export async function getService(request: FastifyRequest<OnlyId>) {
if (!service) {
throw { status: 404, message: 'Service not found.' }
}
const template = await parseAndFindServiceTemplates(service)
return {
settings: await listSettings(),
service,
template: parseAndFindServiceTemplates(service)
template,
}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -150,30 +208,69 @@ export async function getServiceType(request: FastifyRequest) {
export async function saveServiceType(request: FastifyRequest<SaveServiceType>, reply: FastifyReply) {
try {
const { id } = request.params;
const { name, variables = [], serviceDefaultVersion = 'latest' } = request.body;
if (variables.length > 0) {
for (const variable of variables) {
const { id: variableId, defaultValue, value = null } = variable;
if (variableId.startsWith('$$secret_')) {
const secretName = variableId.replace('$$secret_', '');
let secretValue = defaultValue || value || null;
if (defaultValue === '$$generate_password') {
secretValue = generatePassword({});
const { type } = request.body;
let foundTemplate = templates.find(t => t.name === type)
if (foundTemplate) {
let generatedVariables = new Set()
let missingVariables = new Set()
foundTemplate = JSON.parse(JSON.stringify(foundTemplate).replaceAll('$$id', id))
if (foundTemplate.variables.length > 0) {
foundTemplate.variables = foundTemplate.variables.map(variable => {
let { id: variableId } = variable;
if (variableId.startsWith('$$secret_')) {
if (variable.defaultValue === '$$generate_password') {
variable.value = generatePassword({});
} else if (variable.defaultValue === '$$generate_passphrase') {
variable.value = cuid();
}
}
if (defaultValue === '$$generate_username') {
secretValue = cuid();
if (variableId.startsWith('$$config_')) {
if (variable.defaultValue === '$$generate_username') {
variable.value = cuid();
} else {
variable.value = variable.defaultValue
}
}
if (defaultValue === '$$generate_passphrase') {
secretValue = cuid();
if (variable.value) {
generatedVariables.add(`${variableId}=${variable.value}`)
} else {
missingVariables.add(variableId)
}
await prisma.serviceSecret.create({
data: { name: secretName, value: encrypt(secretValue), service: { connect: { id } } }
return variable
})
if (missingVariables.size > 0) {
foundTemplate.variables = foundTemplate.variables.map(variable => {
if (missingVariables.has(variable.id)) {
variable.value = variable.defaultValue
for (const generatedVariable of generatedVariables) {
let [id, value] = generatedVariable.split('=')
variable.value = variable.value.replaceAll(id, value)
}
}
return variable
})
}
for (const variable of foundTemplate.variables) {
if (variable.id.startsWith('$$secret_')) {
await prisma.serviceSecret.create({
data: { name: variable.name, value: encrypt(variable.value), service: { connect: { id } } }
})
}
if (variable.id.startsWith('$$config_')) {
await prisma.serviceSetting.create({
data: { name: variable.name, value: variable.value, service: { connect: { id } } }
})
}
}
}
await prisma.service.update({ where: { id }, data: { type, version: foundTemplate.serviceDefaultVersion } })
return reply.code(201).send()
} else {
throw { status: 404, message: 'Service type not found.' }
}
await prisma.service.update({ where: { id }, data: { type: name, version: serviceDefaultVersion } })
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
@ -344,23 +441,30 @@ export async function checkService(request: FastifyRequest<CheckService>) {
export async function saveService(request: FastifyRequest<SaveService>, reply: FastifyReply) {
try {
const { id } = request.params;
let { name, fqdn, exposePort, type } = request.body;
let { name, fqdn, exposePort, type, serviceSetting } = request.body;
if (fqdn) fqdn = fqdn.toLowerCase();
if (exposePort) exposePort = Number(exposePort);
type = fixType(type)
const update = saveUpdateableFields(type, request.body[type])
// const update = saveUpdateableFields(type, request.body[type])
const data = {
fqdn,
name,
exposePort,
}
if (Object.keys(update).length > 0) {
data[type] = { update: update }
// if (Object.keys(update).length > 0) {
// data[type] = { update: update }
// }
for (const setting of serviceSetting) {
const { id: settingId, value, changed = false } = setting
if (setting.changed) {
await prisma.serviceSetting.update({ where: { id: settingId }, data: { value } })
}
}
await prisma.service.update({
where: { id }, data
});
return reply.code(201).send()
} catch ({ status, message }) {

View File

@ -72,6 +72,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get<OnlyId>('/:id/usage', async (request) => await getServiceUsage(request));
fastify.get<GetServiceLogs>('/:id/logs', async (request) => await getServiceLogs(request));
fastify.post<ServiceStartStop>('/:id/start', async (request) => await startService(request));
fastify.post<ServiceStartStop>('/:id/:type/start', async (request) => await startService(request));
fastify.post<ServiceStartStop>('/:id/:type/stop', async (request) => await stopService(request));
fastify.post<ServiceStartStop & SetWordpressSettings & SetGlitchTipSettings>('/:id/:type/settings', async (request, reply) => await setSettingsService(request, reply));

View File

@ -23,7 +23,7 @@
}
</script>
<div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<!-- <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title font-bold pb-3">Plausible Analytics</div>
<ServiceStatus id={service.id} />
</div>
@ -123,4 +123,4 @@
<div class="flex flex-row my-6 space-x-2">
<div class="title font-bold pb-3">ClickHouse</div>
<ServiceStatus id={`${service.id}-clickhouse`} />
</div>
</div> -->

View File

@ -41,6 +41,7 @@
},
stuff: {
service,
template,
readOnly,
settings
}
@ -111,7 +112,7 @@
$status.service.initialLoading = true;
$status.service.loading = true;
try {
await post(`/services/${service.id}/${service.type}/start`, {});
await post(`/services/${service.id}/start`, {});
} catch (error) {
return errorNotification(error);
} finally {

View File

@ -26,7 +26,7 @@
<script lang="ts">
export let services: any;
let search = '';
let filteredServices = services;
@ -40,7 +40,7 @@
async function handleSubmit(service: any) {
try {
await post(`/services/${id}/configuration/type`, { ...service });
await post(`/services/${id}/configuration/type`, { type: service.name });
return await goto(from || `/services/${id}`);
} catch (error) {
return errorNotification(error);

View File

@ -10,11 +10,9 @@
<script lang="ts">
export let service: any;
export let readOnly: any;
export let settings: any;
export let template: any;
import cuid from 'cuid';
import { onMount } from 'svelte';
import { browser } from '$app/env';
import { page } from '$app/stores';
@ -31,16 +29,18 @@
} from '$lib/store';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Setting from '$lib/components/Setting.svelte';
import * as Services from '$lib/components/Services';
// import * as Services from '$lib/components/Services';
import DocLink from '$lib/components/DocLink.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import ServiceStatus from '$lib/components/ServiceStatus.svelte';
const { id } = $page.params;
let serviceName: any = service.type && service.type[0].toUpperCase() + service.type.substring(1);
// let serviceName: any = service.type && service.type[0].toUpperCase() + service.type.substring(1);
$: isDisabled =
!$appSession.isAdmin || $status.service.isRunning || $status.service.initialLoading;
let newConfiguration = null;
let forceSave = false;
let loading = {
save: false,
@ -69,17 +69,27 @@
}
}
async function handleSubmit() {
async function handleSubmit(e: any) {
const formData = new FormData(e.target);
for (let field of formData) {
const [key, value] = field;
for (const setting of service.serviceSetting) {
if (setting.name === key && setting.value !== value) {
setting.changed = true;
setting.value = value;
}
}
}
if (loading.save) return;
loading.save = true;
try {
await post(`/services/${id}/check`, {
fqdn: service.fqdn,
forceSave,
dualCerts,
otherFqdns: service.minio?.apiFqdn ? [service.minio?.apiFqdn] : [],
exposePort: service.exposePort
});
// await post(`/services/${id}/check`, {
// fqdn: service.fqdn,
// forceSave,
// dualCerts,
// otherFqdns: service.minio?.apiFqdn ? [service.minio?.apiFqdn] : [],
// exposePort: service.exposePort
// });
await post(`/services/${id}`, { ...service });
setLocation(service);
forceSave = false;
@ -174,10 +184,10 @@
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 === 'plausibleanalytics') {
// service.plausibleAnalytics.email = 'noreply@demo.com';
// service.plausibleAnalytics.username = 'admin';
// }
if (service.type === 'minio') {
service.minio.apiFqdn = `http://${cuid()}.demo.coolify.io`;
}
@ -187,13 +197,13 @@
if (service.type === 'fider') {
service.fider.emailNoreply = 'noreply@demo.com';
}
await handleSubmit();
// await handleSubmit();
}
});
</script>
<div class="w-full">
<form on:submit|preventDefault={() => handleSubmit()}>
<form on:submit|preventDefault={handleSubmit}>
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3 ">General</div>
@ -213,7 +223,7 @@
: $t('forms.save')}</button
>
{/if}
{#if service.type === 'plausibleanalytics' && $status.service.isRunning}
<!-- {#if service.type === 'plausibleanalytics' && $status.service.isRunning}
<div class="btn-group">
<button
class="btn btn-sm"
@ -231,7 +241,7 @@
class:loading={loading.cleanup}>Cleanup Unnecessary Database Logs</button
>
</div>
{/if}
{/if} -->
{#if service.type === 'appwrite' && $status.service.isRunning}
<button
class="btn btn-sm"
@ -412,6 +422,41 @@
/>
</div>
</div>
<svelte:component this={Services[serviceName]} bind:service {readOnly} {settings} />
<div />
<div>
{#each Object.keys(template) as oneService}
<div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title font-bold pb-3">{template[oneService].name}</div>
<ServiceStatus id={template[oneService]} />
</div>
<div class="grid grid-flow-row gap-2 px-4">
{#if template[oneService].environment.length > 0}
{#each template[oneService].environment as variable}
<div class="grid grid-cols-2 items-center">
<label for={variable.name}>{variable.label}</label>
{#if variable.defaultValue === '$$generate_fqdn'}
<input
class="w-full"
disabled
readonly
name={variable.name}
id={variable.name}
value={service.fqdn}
/>
{:else}
<input
class="w-full"
name={variable.name}
id={variable.name}
value={variable.value}
/>
{/if}
</div>
{/each}
{/if}
</div>
{/each}
</div>
</form>
</div>