diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md index 9f4bf9321..52c6d91d4 100644 --- a/CONTRIBUTION.md +++ b/CONTRIBUTION.md @@ -111,13 +111,11 @@ ### Custom logo > SVG is recommended, but you can use PNG as well. It should have the `isAbsolute` variable with the suitable CSS classes, primarily for sizing and positioning. -- At [here](apps/ui/src/lib/components/svg/services/ServiceIcons.svelte) include the new logo with `isAbsolute` property. - - At [here](apps/ui/src/routes/services/[id]/_ServiceLinks.svelte) add links to the documentation of the service. ### Custom fields on the UI -By default the URL and name are shown on the UI. Everything else needs to be added [here](apps/ui/src/routes/services/[id]/_Services/_Services.svelte) +By default the URL and name are shown on the UI. Everything else needs to be added [here](apps/ui/src/lib/components/Services) -> If you need to show more details on the frontend, such as users/passwords, you need to add Svelte component [here](apps/ui/src/routes/services/[id]/_Services) with an underscore. For example, see other [here](apps/ui/src/routes/services/[id]/_Services/_Umami.svelte). +> If you need to show more details on the frontend, such as users/passwords, you need to add Svelte component [here](apps/ui/src/lib/components/Services). For example, see other [here](apps/ui/src/lib/components/Services/Umami.svelte). Good job! 👏 \ No newline at end of file diff --git a/apps/api/src/lib/common.ts b/apps/api/src/lib/common.ts index 75e75947f..32ed080dc 100644 --- a/apps/api/src/lib/common.ts +++ b/apps/api/src/lib/common.ts @@ -198,7 +198,7 @@ export const encrypt = (text: string) => { if (text) { const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv(algorithm, process.env['COOLIFY_SECRET_KEY'], iv); - const encrypted = Buffer.concat([cipher.update(text), cipher.final()]); + const encrypted = Buffer.concat([cipher.update(text.trim()), cipher.final()]); return JSON.stringify({ iv: iv.toString('hex'), content: encrypted.toString('hex') @@ -1681,7 +1681,9 @@ export function persistentVolumes(id, persistentStorage, config) { for (const [key, value] of Object.entries(config)) { if (value.volumes) { for (const volume of value.volumes) { - volumeSet.add(volume); + if (!volume.startsWith('/var/run/docker.sock')) { + volumeSet.add(volume); + } } } } diff --git a/apps/api/src/lib/services/handlers.ts b/apps/api/src/lib/services/handlers.ts index bcaf07d36..408da0000 100644 --- a/apps/api/src/lib/services/handlers.ts +++ b/apps/api/src/lib/services/handlers.ts @@ -6,82 +6,83 @@ import { ServiceStartStop } from '../../routes/api/v1/services/types'; import { asyncSleep, ComposeFile, createDirectories, defaultComposeConfiguration, errorHandler, executeDockerCmd, getDomain, getFreePublicPort, getServiceFromDB, getServiceImage, getServiceMainPort, isARM, isDev, makeLabelForServices, persistentVolumes, prisma } from '../common'; import { defaultServiceConfigurations } from '../services'; import { OnlyId } from '../../types'; +import templates from '../templates' -export async function startService(request: FastifyRequest) { - try { - const { type } = request.params - if (type === 'plausibleanalytics') { - return await startPlausibleAnalyticsService(request) - } - if (type === 'nocodb') { - return await startNocodbService(request) - } - if (type === 'minio') { - return await startMinioService(request) - } - if (type === 'vscodeserver') { - return await startVscodeService(request) - } - if (type === 'wordpress') { - return await startWordpressService(request) - } - if (type === 'vaultwarden') { - return await startVaultwardenService(request) - } - if (type === 'languagetool') { - return await startLanguageToolService(request) - } - if (type === 'n8n') { - return await startN8nService(request) - } - if (type === 'uptimekuma') { - return await startUptimekumaService(request) - } - if (type === 'ghost') { - return await startGhostService(request) - } - if (type === 'meilisearch') { - return await startMeilisearchService(request) - } - if (type === 'umami') { - return await startUmamiService(request) - } - if (type === 'hasura') { - return await startHasuraService(request) - } - if (type === 'fider') { - return await startFiderService(request) - } - if (type === 'moodle') { - return await startMoodleService(request) - } - if (type === 'appwrite') { - return await startAppWriteService(request) - } - if (type === 'glitchTip') { - return await startGlitchTipService(request) - } - if (type === 'searxng') { - return await startSearXNGService(request) - } - if (type === 'weblate') { - return await startWeblateService(request) - } - if (type === 'taiga') { - return await startTaigaService(request) - } - if (type === 'grafana') { - return await startGrafanaService(request) - } - if (type === 'trilium') { - return await startTriliumService(request) - } +// export async function startService(request: FastifyRequest) { +// try { +// const { type } = request.params +// if (type === 'plausibleanalytics') { +// return await startPlausibleAnalyticsService(request) +// } +// if (type === 'nocodb') { +// return await startNocodbService(request) +// } +// if (type === 'minio') { +// return await startMinioService(request) +// } +// if (type === 'vscodeserver') { +// return await startVscodeService(request) +// } +// if (type === 'wordpress') { +// return await startWordpressService(request) +// } +// if (type === 'vaultwarden') { +// return await startVaultwardenService(request) +// } +// if (type === 'languagetool') { +// return await startLanguageToolService(request) +// } +// if (type === 'n8n') { +// return await startN8nService(request) +// } +// if (type === 'uptimekuma') { +// return await startUptimekumaService(request) +// } +// if (type === 'ghost') { +// return await startGhostService(request) +// } +// if (type === 'meilisearch') { +// return await startMeilisearchService(request) +// } +// if (type === 'umami') { +// return await startUmamiService(request) +// } +// if (type === 'hasura') { +// return await startHasuraService(request) +// } +// if (type === 'fider') { +// return await startFiderService(request) +// } +// if (type === 'moodle') { +// return await startMoodleService(request) +// } +// if (type === 'appwrite') { +// return await startAppWriteService(request) +// } +// if (type === 'glitchTip') { +// return await startGlitchTipService(request) +// } +// if (type === 'searxng') { +// return await startSearXNGService(request) +// } +// if (type === 'weblate') { +// return await startWeblateService(request) +// } +// if (type === 'taiga') { +// return await startTaigaService(request) +// } +// if (type === 'grafana') { +// return await startGrafanaService(request) +// } +// if (type === 'trilium') { +// return await startTriliumService(request) +// } - throw `Service type ${type} not supported.` - } catch (error) { - throw { status: 500, message: error?.message || error } - } -} +// throw `Service type ${type} not supported.` +// } catch (error) { +// throw { status: 500, message: error?.message || error } +// } +// } export async function stopService(request: FastifyRequest) { try { return await stopServiceContainers(request) @@ -684,54 +685,54 @@ async function startLanguageToolService(request: FastifyRequest) { +export async function startService(request: FastifyRequest) { try { const { id } = request.params; const teamId = request.user.teamId; + const service = await getServiceFromDB({ id, teamId }); const { type, version, destinationDockerId, destinationDocker, serviceSecret, 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 network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('n8n'); - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - n8n: { - image: `${image}:${version}`, - volumes: [`${id}-n8n:/root/.n8n`], - environmentVariables: { - WEBHOOK_URL: `${service.fqdn}` - } + const config = {}; + for (const service in template.services) { + config[service] = { + container_name: id, + image: template.services[service].image.replace('$$core_version', version), + expose: template.services[service].ports, + // ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + volumes: template.services[service].volumes, + 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; + }); } - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.n8n.environmentVariables[secret.name] = secret.value; - }); } + const { workdir } = await createDirectories({ repository: type, buildId: id }); const { volumeMounts } = persistentVolumes(id, persistentStorage, config) + const composeFile: ComposeFile = { version: '3.8', - services: { - [id]: { - container_name: id, - image: config.n8n.image, - volumes: config.n8n.volumes, - environment: config.n8n.environmentVariables, - labels: makeLabelForServices('n8n'), - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - ...defaultComposeConfiguration(network), - } - }, + services: config, networks: { [network]: { external: true } }, volumes: volumeMounts - }; + } const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await startServiceContainers(destinationDocker.id, composeFileDestination) diff --git a/apps/api/src/lib/templates.ts b/apps/api/src/lib/templates.ts new file mode 100644 index 000000000..2155fcb24 --- /dev/null +++ b/apps/api/src/lib/templates.ts @@ -0,0 +1,175 @@ +export default [ + { + "templateVersion": "1.0.0", + "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": { + "documentation": "Taken from https://hub.docker.com/r/n8nio/n8n", + "depends_on": [], + "image": "n8nio/n8n:$$core_version", + "volumes": [ + "$$id-data:/root/.n8n", + "$$id-data-write:/files", + "/var/run/docker.sock:/var/run/docker.sock" + ], + "environment": [ + "WEBHOOK_URL=$$fqdn" + ], + "ports": [ + "5678" + ] + } + }, + "variables": [] + }, + { + "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": { + "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": [ + "$$id-postgresql", + "$$id-clickhouse" + ], + "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", + ], + "ports": [ + "8000" + ], + }, + "$$id-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", + ], + + }, + "$$id-clickhouse": { + "documentation": "Taken from https://plausible.io/", + "build": "$$workdir", + "image": "yandex/clickhouse-server:21.3.2.5", + "ulimits": { + "nofile": { + "soft": 262144, + "hard": 262144 + } + }, + "extras": { + "files:": [ + { + location: '$$workdir/clickhouse-config.xml', + content: 'warningtrue' + }, + { + location: '$$workdir/clickhouse-user-config.xml', + content: '00' + }, + { + location: '$$workdir/init.query', + content: 'CREATE DATABASE IF NOT EXISTS plausible;' + }, + { + location: '$$workdir/init-db.sh', + content: 'clickhouse client --queries-file /docker-entrypoint-initdb.d/init.query' + } + ] + } + }, + + }, + "variables": [ + { + "id": "$$secret_email", + "label": "Admin Email", + "defaultValue": "admin@example.com", + "description": "This is the admin email. Please change it.", + "validRegex": /^([^\s^\/])+$/ + }, + { + "id": "$$secret_name", + "label": "Admin Name", + "defaultValue": "admin", + "description": "This is the admin username. Please change it.", + "validRegex": /^([^\s^\/])+$/ + }, + { + "id": "$$secret_password", + "label": "Admin Password", + "description": "This is the admin password. Please change it.", + "validRegex": /^([^\s^\/])+$/ + }, + { + "id": "$$secret_secret_key_base", + "label": "Secret Key Base", + "description": "", + "validRegex": /^([^\s^\/])+$/ + }, + { + "id": "$$secret_disable_auth", + "label": "Disable Auth", + "defaultValue": "false", + "description": "", + "validRegex": /^([^\s^\/])+$/ + }, + { + "id": "$$secret_disable_registration", + "label": "Disable Registration", + "defaultValue": "true", + "description": "", + "validRegex": /^([^\s^\/])+$/ + }, + { + "id": "$$secret_disable_registration", + "label": "Disable Registration", + "defaultValue": "true", + "description": "", + "validRegex": /^([^\s^\/])+$/ + }, + { + "id": "$$secret_postgresql_username", + "label": "PostgreSQL Username", + "defaultValue": "postgresql", + "description": "", + "validRegex": /^([^\s^\/])+$/ + }, + { + "id": "$$secret_postgresql_password", + "label": "PostgreSQL Password", + "defaultValue": "postgresql", + "description": "", + "validRegex": /^([^\s^\/])+$/ + } + , + { + "id": "$$secret_postgresql_database", + "label": "PostgreSQL Database", + "defaultValue": "plausible", + "description": "", + "validRegex": /^([^\s^\/])+$/ + } + ] + } +] diff --git a/apps/api/src/routes/api/v1/services/handlers.ts b/apps/api/src/routes/api/v1/services/handlers.ts index 2dcc434b6..cb7e2796f 100644 --- a/apps/api/src/routes/api/v1/services/handlers.ts +++ b/apps/api/src/routes/api/v1/services/handlers.ts @@ -5,6 +5,7 @@ import { prisma, uniqueName, asyncExecShell, getServiceFromDB, getContainerUsage import { day } from '../../../../lib/dayjs'; import { checkContainer, isContainerExited } from '../../../../lib/docker'; import cuid from 'cuid'; +import templates from '../../../../lib/templates'; import type { OnlyId } from '../../../../types'; import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetGlitchTipSettings, SetWordpressSettings } from './types'; @@ -73,25 +74,53 @@ export async function getServiceStatus(request: FastifyRequest) { let isRestarting = false; const service = await getServiceFromDB({ id, teamId }); const { destinationDockerId, settings } = service; - + let payload = {} if (destinationDockerId) { - const status = await checkContainer({ dockerId: service.destinationDocker.id, container: id }); - if (status?.found) { - isRunning = status.status.isRunning; - isExited = status.status.isExited; - isRestarting = status.status.isRestarting + const { stdout: containers } = await executeDockerCmd({ + dockerId: service.destinationDocker.id, + command: + `docker ps -a --filter "label=com.docker.compose.project=${id}" --format '{{json .}}'` + }); + const containersArray = containers.trim().split('\n'); + if (containersArray.length > 0 && containersArray[0] !== '') { + for (const container of containersArray) { + let isRunning = false; + let isExited = false; + let isRestarting = false; + const containerObj = JSON.parse(container); + const status = containerObj.State + if (status === 'running') { + isRunning = true; + } + if (status === 'exited') { + isExited = true; + } + if (status === 'restarting') { + isRestarting = true; + } + payload[containerObj.Names] = { + status: { + isRunning, + isExited, + isRestarting + } + + } + } } } - return { - isRunning, - isExited, - settings - } + return payload } catch ({ status, message }) { return errorHandler({ status, message }) } } +function parseAndFindServiceTemplates(service: any) { + const foundTemplate = templates.find(t => t.name === service.type) + if (foundTemplate) { + return JSON.parse(JSON.stringify(foundTemplate).replaceAll('$$id', service.id).replaceAll('$$fqdn', service.fqdn)) + } +} export async function getService(request: FastifyRequest) { try { const teamId = request.user.teamId; @@ -102,7 +131,8 @@ export async function getService(request: FastifyRequest) { } return { settings: await listSettings(), - service + service, + template: parseAndFindServiceTemplates(service) } } catch ({ status, message }) { return errorHandler({ status, message }) @@ -111,7 +141,7 @@ export async function getService(request: FastifyRequest) { export async function getServiceType(request: FastifyRequest) { try { return { - types: supportedServiceTypesAndVersions + services: templates } } catch ({ status, message }) { return errorHandler({ status, message }) @@ -120,8 +150,21 @@ export async function getServiceType(request: FastifyRequest) { export async function saveServiceType(request: FastifyRequest, reply: FastifyReply) { try { const { id } = request.params; - const { type } = request.body; - await configureServiceType({ id, type }); + 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 (secretValue) secretValue = encrypt(secretValue); + await prisma.serviceSecret.create({ + data: { name: secretName, value: secretValue, service: { connect: { id } } } + }) + } + } + } + await prisma.service.update({ where: { id }, data: { type: name, version: serviceDefaultVersion } }) return reply.code(201).send() } catch ({ status, message }) { return errorHandler({ status, message }) diff --git a/apps/ui/src/lib/components/CopyPasswordField.svelte b/apps/ui/src/lib/components/CopyPasswordField.svelte index 9083fa47c..6aedb6ff5 100644 --- a/apps/ui/src/lib/components/CopyPasswordField.svelte +++ b/apps/ui/src/lib/components/CopyPasswordField.svelte @@ -38,6 +38,8 @@ class={disabledClass} class:pr-10={true} class:pr-20={value && isHttps} + class:border={required && !value} + class:border-red-500={required && !value} {placeholder} type="text" {id} @@ -54,6 +56,8 @@ type="text" class:pr-10={true} class:pr-20={value && isHttps} + class:border={required && !value} + class:border-red-500={required && !value} {id} {name} {required} @@ -70,6 +74,8 @@ class={disabledClass} class:pr-10={true} class:pr-20={value && isHttps} + class:border={required && !value} + class:border-red-500={required && !value} type="password" {id} {name} diff --git a/apps/ui/src/lib/components/DocLink.svelte b/apps/ui/src/lib/components/DocLink.svelte index d561566f0..1abb893f5 100644 --- a/apps/ui/src/lib/components/DocLink.svelte +++ b/apps/ui/src/lib/components/DocLink.svelte @@ -1,6 +1,9 @@ - - - - - + + + + + {text} + {#if isExternal} + + {/if} -See details in the documentation +{#if !text} + See details in the documentation +{/if} diff --git a/apps/ui/src/lib/components/ExternalLink.svelte b/apps/ui/src/lib/components/ExternalLink.svelte new file mode 100644 index 000000000..62f2e312a --- /dev/null +++ b/apps/ui/src/lib/components/ExternalLink.svelte @@ -0,0 +1,10 @@ + + + diff --git a/apps/ui/src/lib/components/ServiceStatus.svelte b/apps/ui/src/lib/components/ServiceStatus.svelte new file mode 100644 index 000000000..b14e6cdc3 --- /dev/null +++ b/apps/ui/src/lib/components/ServiceStatus.svelte @@ -0,0 +1,31 @@ + + +{#if serviceStatus.isRunning} + Running +{:else if serviceStatus.isStopped || serviceStatus.isExited} + Stopped +{:else if serviceStatus.isRestarting} + Restarting +{/if} diff --git a/apps/ui/src/routes/services/[id]/_Services/_Appwrite.svelte b/apps/ui/src/lib/components/Services/Appwrite.svelte similarity index 74% rename from apps/ui/src/routes/services/[id]/_Services/_Appwrite.svelte rename to apps/ui/src/lib/components/Services/Appwrite.svelte index 5af58ae7f..9b5f8bf6d 100644 --- a/apps/ui/src/routes/services/[id]/_Services/_Appwrite.svelte +++ b/apps/ui/src/lib/components/Services/Appwrite.svelte @@ -6,12 +6,12 @@ export let readOnly: any; -
-
Appwrite
+
+
Appwrite
-
-
+
+
-
+
-
-
MariaDB
+
+
MariaDB
-
-
+
+
-
+
-
+
-
+
-
+
-
-
Fider
+
+
Fider
-
-
+
+
-
+
-
-
Email
+
+
Email
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
-
PostgreSQL
+
+
PostgreSQL
-
-
+
+
-
+
-
+
+ import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; + import Explainer from '$lib/components/Explainer.svelte'; + import { t } from '$lib/translations'; + export let readOnly: any; + export let service: any; + + +
+
+ Ghost +
+
+
+
+ + +
+
+ + +
+
+
+
MariaDB
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
diff --git a/apps/ui/src/routes/services/[id]/_Services/_GlitchTip.svelte b/apps/ui/src/lib/components/Services/GlitchTip.svelte similarity index 78% rename from apps/ui/src/routes/services/[id]/_Services/_GlitchTip.svelte rename to apps/ui/src/lib/components/Services/GlitchTip.svelte index 2d824b484..c0591018b 100644 --- a/apps/ui/src/routes/services/[id]/_Services/_GlitchTip.svelte +++ b/apps/ui/src/lib/components/Services/GlitchTip.svelte @@ -49,11 +49,11 @@ } -
-
GlitchTip
+
+
GlitchTip
-
+
-
-
-
Email settings
+
+
Email Settings
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
Default User & Superuser
-
+
-
+
-
+
-
-
PostgreSQL
+
+
PostgreSQL
-
-
+
+
-
+
-
+
-
-
Hasura
+
+
Hasura
-
+
-
Hasura Console is not enabled by default for security reasons.
To enable it, add the following secret:

HASURA_GRAPHQL_ENABLE_CONSOLE=true
- -
-
PostgreSQL
+
+ Hasura Console is not enabled by default for security reasons. +
To enable it, add the following secret:

+ HASURA_GRAPHQL_ENABLE_CONSOLE=true +
+
+
PostgreSQL
-
-
+
+
-
+
-
+
-
-
MeiliSearch
+
+
MeiliSearch
-
+
-
-
MinIO
+
+
MinIO
-
-
+
+
-
+
{#if !service.minio.apiFqdn} -
+
-
-
Moodle
+
+
Moodle
-
+
-
+
-
+
-
-
MariaDB
+
+
MariaDB
-
+
-
+
-
+
-
+
-
+
+ export let service: any; + export let readOnly: any; + 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; + import ServiceStatus from '../ServiceStatus.svelte'; + let serviceStatus = { + isExited: false, + isRunning: false, + isRestarting: false, + isStopped: false + }; + + $: if (Object.keys($status.service.statuses).length > 0) { + let { isExited, isRunning, isRestarting } = $status.service.statuses[service.id].status; + serviceStatus.isExited = isExited; + serviceStatus.isRunning = isRunning; + serviceStatus.isRestarting = isRestarting; + serviceStatus.isStopped = !isExited && !isRunning && !isRestarting; + } -
-
Plausible Analytics
+
+
Plausible Analytics
+
-
-
+
+
-
+
-
+
-
+
-
-
PostgreSQL
+ +
+
PostgreSQL
+
-
-
+
+
-
+
-
+
+
+
ClickHouse
+ +
\ No newline at end of file diff --git a/apps/ui/src/routes/services/[id]/_Services/_Searxng.svelte b/apps/ui/src/lib/components/Services/Searxng.svelte similarity index 63% rename from apps/ui/src/routes/services/[id]/_Services/_Searxng.svelte rename to apps/ui/src/lib/components/Services/Searxng.svelte index d726deb82..5e7d045ed 100644 --- a/apps/ui/src/routes/services/[id]/_Services/_Searxng.svelte +++ b/apps/ui/src/lib/components/Services/Searxng.svelte @@ -4,11 +4,11 @@ export let service: any; -
-
SearXNG
+
+
SearXNG
-
+
-
-
Redis
+
+
Redis
-
+
-
-
Taiga
+
+
Taiga
-
+
-
-
Django
+
+
Django
-
+
-
+
-
-
RabbitMQ
+
+
RabbitMQ
-
+
-
+
-
-
PostgreSQL
+
+
PostgreSQL
-
+
-
+
-
+
-
+
-
-
Umami
+
+
Umami
-
-
+
+
-
+