tons of updates

This commit is contained in:
Andras Bacsai 2022-10-14 15:48:37 +02:00
parent 79c30dfc91
commit 462eea90c0
54 changed files with 1760 additions and 1427 deletions

View File

@ -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. > 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. - At [here](apps/ui/src/routes/services/[id]/_ServiceLinks.svelte) add links to the documentation of the service.
### Custom fields on the UI ### 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! 👏 Good job! 👏

View File

@ -198,7 +198,7 @@ export const encrypt = (text: string) => {
if (text) { if (text) {
const iv = crypto.randomBytes(16); const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, process.env['COOLIFY_SECRET_KEY'], iv); 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({ return JSON.stringify({
iv: iv.toString('hex'), iv: iv.toString('hex'),
content: encrypted.toString('hex') content: encrypted.toString('hex')
@ -1681,7 +1681,9 @@ export function persistentVolumes(id, persistentStorage, config) {
for (const [key, value] of Object.entries(config)) { for (const [key, value] of Object.entries(config)) {
if (value.volumes) { if (value.volumes) {
for (const volume of value.volumes) { for (const volume of value.volumes) {
volumeSet.add(volume); if (!volume.startsWith('/var/run/docker.sock')) {
volumeSet.add(volume);
}
} }
} }
} }

View File

@ -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 { asyncSleep, ComposeFile, createDirectories, defaultComposeConfiguration, errorHandler, executeDockerCmd, getDomain, getFreePublicPort, getServiceFromDB, getServiceImage, getServiceMainPort, isARM, isDev, makeLabelForServices, persistentVolumes, prisma } from '../common';
import { defaultServiceConfigurations } from '../services'; import { defaultServiceConfigurations } from '../services';
import { OnlyId } from '../../types'; import { OnlyId } from '../../types';
import templates from '../templates'
export async function startService(request: FastifyRequest<ServiceStartStop>) { // export async function startService(request: FastifyRequest<ServiceStartStop>) {
try { // try {
const { type } = request.params // const { type } = request.params
if (type === 'plausibleanalytics') { // if (type === 'plausibleanalytics') {
return await startPlausibleAnalyticsService(request) // return await startPlausibleAnalyticsService(request)
} // }
if (type === 'nocodb') { // if (type === 'nocodb') {
return await startNocodbService(request) // return await startNocodbService(request)
} // }
if (type === 'minio') { // if (type === 'minio') {
return await startMinioService(request) // return await startMinioService(request)
} // }
if (type === 'vscodeserver') { // if (type === 'vscodeserver') {
return await startVscodeService(request) // return await startVscodeService(request)
} // }
if (type === 'wordpress') { // if (type === 'wordpress') {
return await startWordpressService(request) // return await startWordpressService(request)
} // }
if (type === 'vaultwarden') { // if (type === 'vaultwarden') {
return await startVaultwardenService(request) // return await startVaultwardenService(request)
} // }
if (type === 'languagetool') { // if (type === 'languagetool') {
return await startLanguageToolService(request) // return await startLanguageToolService(request)
} // }
if (type === 'n8n') { // if (type === 'n8n') {
return await startN8nService(request) // return await startN8nService(request)
} // }
if (type === 'uptimekuma') { // if (type === 'uptimekuma') {
return await startUptimekumaService(request) // return await startUptimekumaService(request)
} // }
if (type === 'ghost') { // if (type === 'ghost') {
return await startGhostService(request) // return await startGhostService(request)
} // }
if (type === 'meilisearch') { // if (type === 'meilisearch') {
return await startMeilisearchService(request) // return await startMeilisearchService(request)
} // }
if (type === 'umami') { // if (type === 'umami') {
return await startUmamiService(request) // return await startUmamiService(request)
} // }
if (type === 'hasura') { // if (type === 'hasura') {
return await startHasuraService(request) // return await startHasuraService(request)
} // }
if (type === 'fider') { // if (type === 'fider') {
return await startFiderService(request) // return await startFiderService(request)
} // }
if (type === 'moodle') { // if (type === 'moodle') {
return await startMoodleService(request) // return await startMoodleService(request)
} // }
if (type === 'appwrite') { // if (type === 'appwrite') {
return await startAppWriteService(request) // return await startAppWriteService(request)
} // }
if (type === 'glitchTip') { // if (type === 'glitchTip') {
return await startGlitchTipService(request) // return await startGlitchTipService(request)
} // }
if (type === 'searxng') { // if (type === 'searxng') {
return await startSearXNGService(request) // return await startSearXNGService(request)
} // }
if (type === 'weblate') { // if (type === 'weblate') {
return await startWeblateService(request) // return await startWeblateService(request)
} // }
if (type === 'taiga') { // if (type === 'taiga') {
return await startTaigaService(request) // return await startTaigaService(request)
} // }
if (type === 'grafana') { // if (type === 'grafana') {
return await startGrafanaService(request) // return await startGrafanaService(request)
} // }
if (type === 'trilium') { // if (type === 'trilium') {
return await startTriliumService(request) // return await startTriliumService(request)
} // }
throw `Service type ${type} not supported.` // throw `Service type ${type} not supported.`
} catch (error) { // } catch (error) {
throw { status: 500, message: error?.message || error } // throw { status: 500, message: error?.message || error }
} // }
} // }
export async function stopService(request: FastifyRequest<ServiceStartStop>) { export async function stopService(request: FastifyRequest<ServiceStartStop>) {
try { try {
return await stopServiceContainers(request) return await stopServiceContainers(request)
@ -684,54 +685,54 @@ async function startLanguageToolService(request: FastifyRequest<ServiceStartStop
} }
} }
async function startN8nService(request: FastifyRequest<ServiceStartStop>) { export async function startService(request: FastifyRequest<ServiceStartStop>) {
try { try {
const { id } = request.params; const { id } = request.params;
const teamId = request.user.teamId; const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId }); const service = await getServiceFromDB({ id, teamId });
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } =
service; 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 network = destinationDockerId && destinationDocker.network;
const port = getServiceMainPort('n8n');
const { workdir } = await createDirectories({ repository: type, buildId: id }); const config = {};
const image = getServiceImage(type); for (const service in template.services) {
config[service] = {
const config = { container_name: id,
n8n: { image: template.services[service].image.replace('$$core_version', version),
image: `${image}:${version}`, expose: template.services[service].ports,
volumes: [`${id}-n8n:/root/.n8n`], // ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
environmentVariables: { volumes: template.services[service].volumes,
WEBHOOK_URL: `${service.fqdn}` 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 { volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: config,
[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),
}
},
networks: { networks: {
[network]: { [network]: {
external: true external: true
} }
}, },
volumes: volumeMounts volumes: volumeMounts
}; }
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await startServiceContainers(destinationDocker.id, composeFileDestination) await startServiceContainers(destinationDocker.id, composeFileDestination)

View File

@ -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: '<yandex><logger><level>warning</level><console>true</console></logger><query_thread_log remove="remove"/><query_log remove="remove"/><text_log remove="remove"/><trace_log remove="remove"/><metric_log remove="remove"/><asynchronous_metric_log remove="remove"/><session_log remove="remove"/><part_log remove="remove"/></yandex>'
},
{
location: '$$workdir/clickhouse-user-config.xml',
content: '<yandex><profiles><default><log_queries>0</log_queries><log_query_threads>0</log_query_threads></default></profiles></yandex>'
},
{
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^\/])+$/
}
]
}
]

View File

@ -5,6 +5,7 @@ import { prisma, uniqueName, asyncExecShell, getServiceFromDB, getContainerUsage
import { day } from '../../../../lib/dayjs'; import { day } from '../../../../lib/dayjs';
import { checkContainer, isContainerExited } from '../../../../lib/docker'; import { checkContainer, isContainerExited } from '../../../../lib/docker';
import cuid from 'cuid'; import cuid from 'cuid';
import templates from '../../../../lib/templates';
import type { OnlyId } from '../../../../types'; 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'; 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<OnlyId>) {
let isRestarting = false; let isRestarting = false;
const service = await getServiceFromDB({ id, teamId }); const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, settings } = service; const { destinationDockerId, settings } = service;
let payload = {}
if (destinationDockerId) { if (destinationDockerId) {
const status = await checkContainer({ dockerId: service.destinationDocker.id, container: id }); const { stdout: containers } = await executeDockerCmd({
if (status?.found) { dockerId: service.destinationDocker.id,
isRunning = status.status.isRunning; command:
isExited = status.status.isExited; `docker ps -a --filter "label=com.docker.compose.project=${id}" --format '{{json .}}'`
isRestarting = status.status.isRestarting });
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 { return payload
isRunning,
isExited,
settings
}
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ 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<OnlyId>) { export async function getService(request: FastifyRequest<OnlyId>) {
try { try {
const teamId = request.user.teamId; const teamId = request.user.teamId;
@ -102,7 +131,8 @@ export async function getService(request: FastifyRequest<OnlyId>) {
} }
return { return {
settings: await listSettings(), settings: await listSettings(),
service service,
template: parseAndFindServiceTemplates(service)
} }
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
@ -111,7 +141,7 @@ export async function getService(request: FastifyRequest<OnlyId>) {
export async function getServiceType(request: FastifyRequest) { export async function getServiceType(request: FastifyRequest) {
try { try {
return { return {
types: supportedServiceTypesAndVersions services: templates
} }
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
@ -120,8 +150,21 @@ export async function getServiceType(request: FastifyRequest) {
export async function saveServiceType(request: FastifyRequest<SaveServiceType>, reply: FastifyReply) { export async function saveServiceType(request: FastifyRequest<SaveServiceType>, reply: FastifyReply) {
try { try {
const { id } = request.params; const { id } = request.params;
const { type } = request.body; const { name, variables = [], serviceDefaultVersion = 'latest' } = request.body;
await configureServiceType({ id, type }); 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() return reply.code(201).send()
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })

View File

@ -38,6 +38,8 @@
class={disabledClass} class={disabledClass}
class:pr-10={true} class:pr-10={true}
class:pr-20={value && isHttps} class:pr-20={value && isHttps}
class:border={required && !value}
class:border-red-500={required && !value}
{placeholder} {placeholder}
type="text" type="text"
{id} {id}
@ -54,6 +56,8 @@
type="text" type="text"
class:pr-10={true} class:pr-10={true}
class:pr-20={value && isHttps} class:pr-20={value && isHttps}
class:border={required && !value}
class:border-red-500={required && !value}
{id} {id}
{name} {name}
{required} {required}
@ -70,6 +74,8 @@
class={disabledClass} class={disabledClass}
class:pr-10={true} class:pr-10={true}
class:pr-20={value && isHttps} class:pr-20={value && isHttps}
class:border={required && !value}
class:border-red-500={required && !value}
type="password" type="password"
{id} {id}
{name} {name}

View File

@ -1,6 +1,9 @@
<script lang="ts"> <script lang="ts">
import ExternalLink from './ExternalLink.svelte';
import Tooltip from './Tooltip.svelte'; import Tooltip from './Tooltip.svelte';
export let url = 'https://docs.coollabs.io'; export let url = 'https://docs.coollabs.io';
export let text: any = '';
export let isExternal = false;
let id = let id =
'cool-' + 'cool-' +
url url
@ -10,10 +13,32 @@
.slice(-16); .slice(-16);
</script> </script>
<a {id} href={url} target="_blank" class="icons inline-block cursor-pointer text-xs mx-2"> <a
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> {id}
<path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" /> href={url}
</svg> target="_blank"
class="flex no-underline inline-block cursor-pointer"
class:icons={!text}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z"
/>
</svg>
{text}
{#if isExternal}
<ExternalLink />
{/if}
</a> </a>
<Tooltip triggeredBy={`#${id}`}>See details in the documentation</Tooltip> {#if !text}
<Tooltip triggeredBy={`#${id}`}>See details in the documentation</Tooltip>
{/if}

View File

@ -0,0 +1,10 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 24 24"
stroke-width="3"
stroke="currentColor"
class="w-3 h-3 text-white"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 19.5l15-15m0 0H8.25m11.25 0v11.25" />
</svg>

After

Width:  |  Height:  |  Size: 261 B

View File

@ -0,0 +1,31 @@
<script lang="ts">
export let id: any;
import { status } from '$lib/store';
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[id].status;
serviceStatus.isExited = isExited;
serviceStatus.isRunning = isRunning;
serviceStatus.isRestarting = isRestarting;
serviceStatus.isStopped = !isExited && !isRunning && !isRestarting;
} else {
serviceStatus.isExited = false;
serviceStatus.isRunning = false;
serviceStatus.isRestarting = false;
serviceStatus.isStopped = true;
}
</script>
{#if serviceStatus.isRunning}
<span class="badge font-bold uppercase rounded text-green-500 mt-2">Running</span>
{:else if serviceStatus.isStopped || serviceStatus.isExited}
<span class="badge font-bold uppercase rounded text-red-500 mt-2">Stopped</span>
{:else if serviceStatus.isRestarting}
<span class="badge font-bold uppercase rounded text-yellow-500 mt-2">Restarting</span>
{/if}

View File

@ -6,12 +6,12 @@
export let readOnly: any; export let readOnly: any;
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title">Appwrite</div> <div class="title font-bold pb-3">Appwrite</div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2 px-4">
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="opensslKeyV1">Encryption Key</label> <label for="opensslKeyV1">Encryption Key</label>
<CopyPasswordField <CopyPasswordField
name="opensslKeyV1" name="opensslKeyV1"
@ -22,7 +22,7 @@
disabled disabled
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="executorSecret">Executor Secret</label> <label for="executorSecret">Executor Secret</label>
<CopyPasswordField <CopyPasswordField
name="executorSecret" name="executorSecret"
@ -34,11 +34,11 @@
/> />
</div> </div>
</div> </div>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title">MariaDB</div> <div class="title font-bold pb-3">MariaDB</div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2 px-4">
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="mariadbUser">{$t('forms.username')}</label> <label for="mariadbUser">{$t('forms.username')}</label>
<CopyPasswordField <CopyPasswordField
name="mariadbUser" name="mariadbUser"
@ -48,7 +48,7 @@
disabled disabled
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2 "> <div class="grid grid-cols-2 items-center ">
<label for="mariadbPassword">{$t('forms.password')}</label> <label for="mariadbPassword">{$t('forms.password')}</label>
<CopyPasswordField <CopyPasswordField
id="mariadbPassword" id="mariadbPassword"
@ -59,7 +59,7 @@
value={service.appwrite.mariadbPassword} value={service.appwrite.mariadbPassword}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="mariadbRootUser">Root User</label> <label for="mariadbRootUser">Root User</label>
<CopyPasswordField <CopyPasswordField
name="mariadbRootUser" name="mariadbRootUser"
@ -69,7 +69,7 @@
disabled disabled
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2 "> <div class="grid grid-cols-2 items-center ">
<label for="mariadbRootUserPassword">Root Password</label> <label for="mariadbRootUserPassword">Root Password</label>
<CopyPasswordField <CopyPasswordField
id="mariadbRootUserPassword" id="mariadbRootUserPassword"
@ -80,7 +80,7 @@
value={service.appwrite.mariadbRootUserPassword} value={service.appwrite.mariadbRootUserPassword}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="mariadbDatabase">{$t('index.database')}</label> <label for="mariadbDatabase">{$t('index.database')}</label>
<CopyPasswordField <CopyPasswordField
name="mariadbDatabase" name="mariadbDatabase"

View File

@ -17,12 +17,12 @@
]; ];
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title">Fider</div> <div class="title font-bold pb-3">Fider</div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2 px-4">
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="jwtSecret">JWT Secret</label> <label for="jwtSecret">JWT Secret</label>
<CopyPasswordField <CopyPasswordField
name="jwtSecret" name="jwtSecret"
@ -34,7 +34,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="emailNoreply">Noreply Email</label> <label for="emailNoreply">Noreply Email</label>
<input <input
class="w-full" class="w-full"
@ -49,11 +49,11 @@
/> />
</div> </div>
</div> </div>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title">Email</div> <div class="title font-bold pb-3">Email</div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2 px-4">
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="emailMailgunApiKey">Mailgun API Key</label> <label for="emailMailgunApiKey">Mailgun API Key</label>
<CopyPasswordField <CopyPasswordField
name="emailMailgunApiKey" name="emailMailgunApiKey"
@ -66,7 +66,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="emailMailgunDomain">Mailgun Domain</label> <label for="emailMailgunDomain">Mailgun Domain</label>
<input <input
class="w-full" class="w-full"
@ -78,7 +78,7 @@
placeholder="{$t('forms.eg')}: yourdomain.com" placeholder="{$t('forms.eg')}: yourdomain.com"
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="emailMailgunRegion">Mailgun Region</label> <label for="emailMailgunRegion">Mailgun Region</label>
<div class="custom-select-wrapper"> <div class="custom-select-wrapper">
<Select <Select
@ -92,10 +92,10 @@
</div> </div>
</div> </div>
<div class="flex space-x-1 py-5 lg:px-10 px-2 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="text-lg">Or</div> <div class="text-lg">Or</div>
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="emailSmtpHost">SMTP Host</label> <label for="emailSmtpHost">SMTP Host</label>
<input <input
class="w-full" class="w-full"
@ -107,7 +107,7 @@
placeholder="{$t('forms.eg')}: smtp.yourdomain.com" placeholder="{$t('forms.eg')}: smtp.yourdomain.com"
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="emailSmtpPort">SMTP Port</label> <label for="emailSmtpPort">SMTP Port</label>
<input <input
class="w-full" class="w-full"
@ -119,7 +119,7 @@
placeholder="{$t('forms.eg')}: 587" placeholder="{$t('forms.eg')}: 587"
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="emailSmtpUser">SMTP User</label> <label for="emailSmtpUser">SMTP User</label>
<input <input
class="w-full" class="w-full"
@ -131,7 +131,7 @@
placeholder="{$t('forms.eg')}: user@yourdomain.com" placeholder="{$t('forms.eg')}: user@yourdomain.com"
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="emailSmtpPassword">SMTP Password</label> <label for="emailSmtpPassword">SMTP Password</label>
<CopyPasswordField <CopyPasswordField
name="emailSmtpPassword" name="emailSmtpPassword"
@ -143,7 +143,7 @@
placeholder="{$t('forms.eg')}: s0m3p4ssw0rd" placeholder="{$t('forms.eg')}: s0m3p4ssw0rd"
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="emailSmtpEnableStartTls">SMTP Start TLS</label> <label for="emailSmtpEnableStartTls">SMTP Start TLS</label>
<input <input
class="w-full" class="w-full"
@ -156,11 +156,11 @@
/> />
</div> </div>
</div> </div>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title">PostgreSQL</div> <div class="title font-bold pb-3">PostgreSQL</div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2 px-4">
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="postgresqlUser">{$t('forms.username')}</label> <label for="postgresqlUser">{$t('forms.username')}</label>
<CopyPasswordField <CopyPasswordField
name="postgresqlUser" name="postgresqlUser"
@ -170,7 +170,7 @@
disabled disabled
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="postgresqlPassword">{$t('forms.password')}</label> <label for="postgresqlPassword">{$t('forms.password')}</label>
<CopyPasswordField <CopyPasswordField
id="postgresqlPassword" id="postgresqlPassword"
@ -181,7 +181,7 @@
value={service.fider.postgresqlPassword} value={service.fider.postgresqlPassword}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="postgresqlDatabase">{$t('index.database')}</label> <label for="postgresqlDatabase">{$t('index.database')}</label>
<CopyPasswordField <CopyPasswordField
name="postgresqlDatabase" name="postgresqlDatabase"

View File

@ -0,0 +1,101 @@
<script lang="ts">
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;
</script>
<div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title font-bold pb-3">
Ghost <Explainer
explanation="You can change these values in the <span class='text-settings'>Ghost admin panel<span>."
/>
</div>
</div>
<div class="space-y-2">
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="email">{$t('forms.default_email_address')}</label>
<input
class="w-full"
name="email"
id="email"
disabled
readonly
placeholder={$t('forms.email')}
value={service.ghost.defaultEmail}
required
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="defaultPassword">{$t('forms.default_password')}</label>
<CopyPasswordField
id="defaultPassword"
isPasswordField
readonly
disabled
name="defaultPassword"
value={service.ghost.defaultPassword}
/>
</div>
</div>
<div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title font-bold pb-3">MariaDB</div>
</div>
<div class="space-y-2">
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<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 lg:px-10 px-2">
<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 lg:px-10 px-2">
<label for="mariadbDatabase">{$t('index.database')}</label>
<input
class="w-full"
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 lg:px-10 px-2">
<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 lg:px-10 px-2">
<label for="mariadbRootUserPassword">{$t('forms.root_db_password')}</label>
<CopyPasswordField
id="mariadbRootUserPassword"
isPasswordField
readonly
disabled
name="mariadbRootUserPassword"
value={service.ghost.mariadbRootUserPassword}
/>
</div>
</div>

View File

@ -49,11 +49,11 @@
} }
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title">GlitchTip</div> <div class="title font-bold pb-3">GlitchTip</div>
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
id="enableOpenUserRegistration" id="enableOpenUserRegistration"
bind:setting={service.glitchTip.enableOpenUserRegistration} bind:setting={service.glitchTip.enableOpenUserRegistration}
@ -63,19 +63,13 @@
title="Enable Open User Registration" title="Enable Open User Registration"
description={''} description={''}
/> />
<!-- <Setting
bind:setting={service.glitchTip.enableOpenUserRegistration}
on:click={toggleEnableOpenUserRegistration}
title={'Enable Open User Registration'}
description={''}
/> -->
</div> </div>
<div class="flex space-x-1 py-2 font-bold"> <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="subtitle">Email settings</div> <div class="title font-bold pb-3">Email Settings</div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2 px-4">
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
id="emailSmtpUseTls" id="emailSmtpUseTls"
bind:setting={service.glitchTip.emailSmtpUseTls} bind:setting={service.glitchTip.emailSmtpUseTls}
@ -87,7 +81,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
id="emailSmtpUseSsl" id="emailSmtpUseSsl"
bind:setting={service.glitchTip.emailSmtpUseSsl} bind:setting={service.glitchTip.emailSmtpUseSsl}
@ -98,7 +92,7 @@
description={''} description={''}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="defaultEmailFrom">Default Email From</label> <label for="defaultEmailFrom">Default Email From</label>
<CopyPasswordField <CopyPasswordField
required required
@ -108,7 +102,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="emailSmtpHost">SMTP Host</label> <label for="emailSmtpHost">SMTP Host</label>
<CopyPasswordField <CopyPasswordField
name="emailSmtpHost" name="emailSmtpHost"
@ -117,7 +111,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="emailSmtpPort">SMTP Port</label> <label for="emailSmtpPort">SMTP Port</label>
<CopyPasswordField <CopyPasswordField
name="emailSmtpPort" name="emailSmtpPort"
@ -126,7 +120,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="emailSmtpUser">SMTP User</label> <label for="emailSmtpUser">SMTP User</label>
<CopyPasswordField <CopyPasswordField
name="emailSmtpUser" name="emailSmtpUser"
@ -135,7 +129,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="emailSmtpPassword">SMTP Password</label> <label for="emailSmtpPassword">SMTP Password</label>
<CopyPasswordField <CopyPasswordField
name="emailSmtpPassword" name="emailSmtpPassword"
@ -145,7 +139,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="emailBackend">Email Backend</label> <label for="emailBackend">Email Backend</label>
<CopyPasswordField <CopyPasswordField
name="emailBackend" name="emailBackend"
@ -154,7 +148,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="mailgunApiKey">Mailgun API Key</label> <label for="mailgunApiKey">Mailgun API Key</label>
<CopyPasswordField <CopyPasswordField
name="mailgunApiKey" name="mailgunApiKey"
@ -163,7 +157,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="sendgridApiKey">SendGrid API Key</label> <label for="sendgridApiKey">SendGrid API Key</label>
<CopyPasswordField <CopyPasswordField
name="sendgridApiKey" name="sendgridApiKey"
@ -176,7 +170,7 @@
<div class="subtitle">Default User & Superuser</div> <div class="subtitle">Default User & Superuser</div>
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="defaultEmail">{$t('forms.email')}</label> <label for="defaultEmail">{$t('forms.email')}</label>
<CopyPasswordField <CopyPasswordField
name="defaultEmail" name="defaultEmail"
@ -186,7 +180,7 @@
disabled disabled
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="defaultUsername">{$t('forms.username')}</label> <label for="defaultUsername">{$t('forms.username')}</label>
<CopyPasswordField <CopyPasswordField
name="defaultUsername" name="defaultUsername"
@ -196,7 +190,7 @@
disabled disabled
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="defaultPassword">{$t('forms.password')}</label> <label for="defaultPassword">{$t('forms.password')}</label>
<CopyPasswordField <CopyPasswordField
name="defaultPassword" name="defaultPassword"
@ -208,11 +202,11 @@
/> />
</div> </div>
</div> </div>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title">PostgreSQL</div> <div class="title font-bold pb-3">PostgreSQL</div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2 px-4">
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="postgresqlUser">{$t('forms.username')}</label> <label for="postgresqlUser">{$t('forms.username')}</label>
<CopyPasswordField <CopyPasswordField
name="postgresqlUser" name="postgresqlUser"
@ -222,7 +216,7 @@
disabled disabled
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="postgresqlPassword">{$t('forms.password')}</label> <label for="postgresqlPassword">{$t('forms.password')}</label>
<CopyPasswordField <CopyPasswordField
id="postgresqlPassword" id="postgresqlPassword"
@ -233,7 +227,7 @@
bind:value={service.glitchTip.postgresqlPassword} bind:value={service.glitchTip.postgresqlPassword}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="postgresqlDatabase">{$t('index.database')}</label> <label for="postgresqlDatabase">{$t('index.database')}</label>
<CopyPasswordField <CopyPasswordField
name="postgresqlDatabase" name="postgresqlDatabase"

View File

@ -4,11 +4,11 @@
export let service: any; export let service: any;
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title">Hasura</div> <div class="title font-bold pb-3">Hasura</div>
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="graphQLAdminPassword">GraphQL Admin Password</label> <label for="graphQLAdminPassword">GraphQL Admin Password</label>
<CopyPasswordField <CopyPasswordField
name="graphQLAdminPassword" name="graphQLAdminPassword"
@ -19,14 +19,17 @@
disabled disabled
/> />
</div> </div>
<div class="lg:px-10 px-2 py-4">Hasura Console is <span class="text-warning">not enabled by default</span> for security reasons. <br>To enable it, add the following secret:<br><br> <code>HASURA_GRAPHQL_ENABLE_CONSOLE=true</code></div> <div class="px-4">
Hasura Console is <span class="text-warning">not enabled by default</span> for security reasons.
<div class="flex space-x-1 py-5 font-bold"> <br />To enable it, add the following secret:<br /><br />
<div class="title">PostgreSQL</div> <code>HASURA_GRAPHQL_ENABLE_CONSOLE=true</code>
</div>
<div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title font-bold pb-3">PostgreSQL</div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2 px-4">
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="postgresqlUser">{$t('forms.username')}</label> <label for="postgresqlUser">{$t('forms.username')}</label>
<CopyPasswordField <CopyPasswordField
name="postgresqlUser" name="postgresqlUser"
@ -36,7 +39,7 @@
disabled disabled
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="postgresqlPassword">{$t('forms.password')}</label> <label for="postgresqlPassword">{$t('forms.password')}</label>
<CopyPasswordField <CopyPasswordField
id="postgresqlPassword" id="postgresqlPassword"
@ -47,7 +50,7 @@
value={service.hasura.postgresqlPassword} value={service.hasura.postgresqlPassword}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="postgresqlDatabase">{$t('index.database')}</label> <label for="postgresqlDatabase">{$t('index.database')}</label>
<CopyPasswordField <CopyPasswordField
name="postgresqlDatabase" name="postgresqlDatabase"

View File

@ -4,10 +4,10 @@
export let service: any; export let service: any;
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title">MeiliSearch</div> <div class="title font-bold pb-3">MeiliSearch</div>
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center px-4">
<label for="masterKey">{$t('forms.admin_api_key')}</label> <label for="masterKey">{$t('forms.admin_api_key')}</label>
<CopyPasswordField <CopyPasswordField
id="masterKey" id="masterKey"

View File

@ -5,11 +5,11 @@
export let service: any; export let service: any;
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title">MinIO</div> <div class="title font-bold pb-3">MinIO</div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2 px-4">
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="rootUser">{$t('forms.root_user')}</label> <label for="rootUser">{$t('forms.root_user')}</label>
<input <input
class="w-full" class="w-full"
@ -21,7 +21,7 @@
readonly readonly
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="rootUserPassword">{$t('forms.roots_password')}</label> <label for="rootUserPassword">{$t('forms.roots_password')}</label>
<CopyPasswordField <CopyPasswordField
id="rootUserPassword" id="rootUserPassword"
@ -33,7 +33,7 @@
/> />
</div> </div>
{#if !service.minio.apiFqdn} {#if !service.minio.apiFqdn}
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center">
<label for="publicPort">{$t('forms.api_port')}</label> <label for="publicPort">{$t('forms.api_port')}</label>
<input <input
class="w-full" class="w-full"

View File

@ -5,10 +5,10 @@
export let service: any; export let service: any;
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title">Moodle</div> <div class="title font-bold pb-3">Moodle</div>
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10"> <div class="grid grid-cols-2 items-center">
<label class="text-base font-bold text-stone-100" for="email">{$t('forms.default_email_address')}</label> <label class="text-base font-bold text-stone-100" for="email">{$t('forms.default_email_address')}</label>
<input <input
class="w-full" class="w-full"
@ -21,7 +21,7 @@
value={service.moodle.defaultEmail} value={service.moodle.defaultEmail}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10"> <div class="grid grid-cols-2 items-center">
<label class="text-base font-bold text-stone-100" for="defaultUsername">Default Username</label> <label class="text-base font-bold text-stone-100" for="defaultUsername">Default Username</label>
<CopyPasswordField <CopyPasswordField
id="defaultUsername" id="defaultUsername"
@ -32,7 +32,7 @@
value={service.moodle.defaultUsername} value={service.moodle.defaultUsername}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10"> <div class="grid grid-cols-2 items-center">
<label class="text-base font-bold text-stone-100" for="defaultPassword">{$t('forms.default_password')}</label> <label class="text-base font-bold text-stone-100" for="defaultPassword">{$t('forms.default_password')}</label>
<CopyPasswordField <CopyPasswordField
id="defaultPassword" id="defaultPassword"
@ -44,10 +44,10 @@
value={service.moodle.defaultPassword} value={service.moodle.defaultPassword}
/> />
</div> </div>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title">MariaDB</div> <div class="title font-bold pb-3">MariaDB</div>
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10"> <div class="grid grid-cols-2 items-center">
<label class="text-base font-bold text-stone-100" for="mariadbUser">{$t('forms.username')}</label> <label class="text-base font-bold text-stone-100" for="mariadbUser">{$t('forms.username')}</label>
<CopyPasswordField <CopyPasswordField
name="mariadbUser" name="mariadbUser"
@ -57,7 +57,7 @@
disabled disabled
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10"> <div class="grid grid-cols-2 items-center">
<label class="text-base font-bold text-stone-100" for="mariadbPassword">{$t('forms.password')}</label> <label class="text-base font-bold text-stone-100" for="mariadbPassword">{$t('forms.password')}</label>
<CopyPasswordField <CopyPasswordField
id="mariadbPassword" id="mariadbPassword"
@ -68,7 +68,7 @@
value={service.moodle.mariadbPassword} value={service.moodle.mariadbPassword}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10"> <div class="grid grid-cols-2 items-center">
<label class="text-base font-bold text-stone-100" for="mariadbDatabase">{$t('index.database')}</label> <label class="text-base font-bold text-stone-100" for="mariadbDatabase">{$t('index.database')}</label>
<input <input
class="w-full" class="w-full"
@ -81,7 +81,7 @@
placeholder="{$t('forms.eg')}: moodle_db" placeholder="{$t('forms.eg')}: moodle_db"
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10"> <div class="grid grid-cols-2 items-center">
<label class="text-base font-bold text-stone-100" for="mariadbRootUser">{$t('forms.root_db_user')}</label> <label class="text-base font-bold text-stone-100" for="mariadbRootUser">{$t('forms.root_db_user')}</label>
<CopyPasswordField <CopyPasswordField
id="mariadbRootUser" id="mariadbRootUser"
@ -91,7 +91,7 @@
value={service.moodle.mariadbRootUser} value={service.moodle.mariadbRootUser}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10"> <div class="grid grid-cols-2 items-center">
<label class="text-base font-bold text-stone-100" for="mariadbRootUserPassword">{$t('forms.root_db_password')}</label> <label class="text-base font-bold text-stone-100" for="mariadbRootUserPassword">{$t('forms.root_db_password')}</label>
<CopyPasswordField <CopyPasswordField
id="mariadbRootUserPassword" id="mariadbRootUserPassword"

View File

@ -1,17 +1,34 @@
<script lang="ts"> <script lang="ts">
export let service: any;
export let readOnly: any;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte'; import Explainer from '$lib/components/Explainer.svelte';
import { appSession, status } from '$lib/store'; import { appSession, status } from '$lib/store';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
export let service: any; import ServiceStatus from '../ServiceStatus.svelte';
export let readOnly: any; 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;
}
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title">Plausible Analytics</div> <div class="title font-bold pb-3">Plausible Analytics</div>
<ServiceStatus id={service.id} />
</div> </div>
<div class="space-y-2"> <div class="space-y-2 px-4">
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center ">
<label for="scriptName" <label for="scriptName"
>Script Name <Explainer >Script Name <Explainer
explanation="Useful if you would like to rename the collector script to prevent it blocked by AdBlockers." explanation="Useful if you would like to rename the collector script to prevent it blocked by AdBlockers."
@ -28,7 +45,7 @@
required required
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center ">
<label for="email">{$t('forms.email')}</label> <label for="email">{$t('forms.email')}</label>
<input <input
class="w-full" class="w-full"
@ -41,7 +58,7 @@
required required
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center ">
<label for="username">{$t('forms.username')}</label> <label for="username">{$t('forms.username')}</label>
<CopyPasswordField <CopyPasswordField
name="username" name="username"
@ -53,7 +70,7 @@
required required
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center ">
<label for="password">{$t('forms.password')}</label> <label for="password">{$t('forms.password')}</label>
<CopyPasswordField <CopyPasswordField
id="password" id="password"
@ -65,11 +82,13 @@
/> />
</div> </div>
</div> </div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">PostgreSQL</div> <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title font-bold pb-3">PostgreSQL</div>
<ServiceStatus id={`${service.id}-postgresql`} />
</div> </div>
<div class="space-y-2"> <div class="space-y-2 px-4">
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center ">
<label for="postgresqlUser">{$t('forms.username')}</label> <label for="postgresqlUser">{$t('forms.username')}</label>
<CopyPasswordField <CopyPasswordField
name="postgresqlUser" name="postgresqlUser"
@ -79,7 +98,7 @@
disabled disabled
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center ">
<label for="postgresqlPassword">{$t('forms.password')}</label> <label for="postgresqlPassword">{$t('forms.password')}</label>
<CopyPasswordField <CopyPasswordField
id="postgresqlPassword" id="postgresqlPassword"
@ -90,7 +109,7 @@
value={service.plausibleAnalytics.postgresqlPassword} value={service.plausibleAnalytics.postgresqlPassword}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center ">
<label for="postgresqlDatabase">{$t('index.database')}</label> <label for="postgresqlDatabase">{$t('index.database')}</label>
<CopyPasswordField <CopyPasswordField
name="postgresqlDatabase" name="postgresqlDatabase"
@ -101,3 +120,7 @@
/> />
</div> </div>
</div> </div>
<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>

View File

@ -4,11 +4,11 @@
export let service: any; export let service: any;
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title">SearXNG</div> <div class="title font-bold pb-3">SearXNG</div>
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center px-4">
<label for="secretKey">Secret Key</label> <label for="secretKey">Secret Key</label>
<CopyPasswordField <CopyPasswordField
name="secretKey" name="secretKey"
@ -19,11 +19,11 @@
disabled disabled
/> />
</div> </div>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title">Redis</div> <div class="title font-bold pb-3">Redis</div>
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center px-4">
<label for="redisPassword">{$t('forms.password')}</label> <label for="redisPassword">{$t('forms.password')}</label>
<CopyPasswordField <CopyPasswordField
name="redisPassword" name="redisPassword"

View File

@ -4,11 +4,11 @@
export let service: any; export let service: any;
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title">Taiga</div> <div class="title font-bold pb-3">Taiga</div>
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10"> <div class="grid grid-cols-2 items-center px-4">
<label class="text-base font-bold text-stone-100" for="secretKey">Secret Key</label> <label class="text-base font-bold text-stone-100" for="secretKey">Secret Key</label>
<CopyPasswordField <CopyPasswordField
name="secretKey" name="secretKey"
@ -20,11 +20,11 @@
/> />
</div> </div>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title">Django</div> <div class="title font-bold pb-3">Django</div>
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10"> <div class="grid grid-cols-2 items-center px-4">
<label class="text-base font-bold text-stone-100" for="djangoAdminUser">Admin User</label> <label class="text-base font-bold text-stone-100" for="djangoAdminUser">Admin User</label>
<CopyPasswordField <CopyPasswordField
name="djangoAdminUser" name="djangoAdminUser"
@ -34,7 +34,7 @@
disabled disabled
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10"> <div class="grid grid-cols-2 items-center px-4">
<label class="text-base font-bold text-stone-100" for="djangoAdminPassword">Admin Password</label> <label class="text-base font-bold text-stone-100" for="djangoAdminPassword">Admin Password</label>
<CopyPasswordField <CopyPasswordField
name="djangoAdminPassword" name="djangoAdminPassword"
@ -45,11 +45,11 @@
disabled disabled
/> />
</div> </div>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title">RabbitMQ</div> <div class="title font-bold pb-3">RabbitMQ</div>
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10"> <div class="grid grid-cols-2 items-center px-4">
<label class="text-base font-bold text-stone-100" for="rabbitMQUser">User</label> <label class="text-base font-bold text-stone-100" for="rabbitMQUser">User</label>
<CopyPasswordField <CopyPasswordField
name="rabbitMQUser" name="rabbitMQUser"
@ -59,7 +59,7 @@
disabled disabled
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10"> <div class="grid grid-cols-2 items-center px-4">
<label class="text-base font-bold text-stone-100" for="rabbitMQPassword">Password</label> <label class="text-base font-bold text-stone-100" for="rabbitMQPassword">Password</label>
<CopyPasswordField <CopyPasswordField
name="rabbitMQPassword" name="rabbitMQPassword"
@ -71,11 +71,11 @@
/> />
</div> </div>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title">PostgreSQL</div> <div class="title font-bold pb-3">PostgreSQL</div>
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10"> <div class="grid grid-cols-2 items-center px-4">
<label class="text-base font-bold text-stone-100" for="postgresqlHost">PostgreSQL Host</label> <label class="text-base font-bold text-stone-100" for="postgresqlHost">PostgreSQL Host</label>
<CopyPasswordField <CopyPasswordField
name="postgresqlHost" name="postgresqlHost"
@ -85,7 +85,7 @@
disabled disabled
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10"> <div class="grid grid-cols-2 items-center px-4">
<label class="text-base font-bold text-stone-100" for="postgresqlPort">PostgreSQL Port</label> <label class="text-base font-bold text-stone-100" for="postgresqlPort">PostgreSQL Port</label>
<CopyPasswordField <CopyPasswordField
name="postgresqlPort" name="postgresqlPort"
@ -95,7 +95,7 @@
disabled disabled
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10"> <div class="grid grid-cols-2 items-center px-4">
<label class="text-base font-bold text-stone-100" for="postgresqlUser">PostgreSQL User</label> <label class="text-base font-bold text-stone-100" for="postgresqlUser">PostgreSQL User</label>
<CopyPasswordField <CopyPasswordField
name="postgresqlUser" name="postgresqlUser"
@ -105,7 +105,7 @@
disabled disabled
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10"> <div class="grid grid-cols-2 items-center px-4">
<label class="text-base font-bold text-stone-100" for="postgresqlPassword">PostgreSQL Password</label> <label class="text-base font-bold text-stone-100" for="postgresqlPassword">PostgreSQL Password</label>
<CopyPasswordField <CopyPasswordField
name="postgresqlPassword" name="postgresqlPassword"

View File

@ -4,11 +4,11 @@
export let service: any; export let service: any;
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title">Umami</div> <div class="title font-bold pb-3">Umami</div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2 px-4">
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center ">
<label for="adminUser">Admin User</label> <label for="adminUser">Admin User</label>
<input <input
class="w-full" class="w-full"
@ -20,7 +20,7 @@
readonly readonly
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center ">
<label for="umamiAdminPassword" <label for="umamiAdminPassword"
>Initial Admin Password <Explainer >Initial Admin Password <Explainer
explanation="It could be changed in Umami. <br>This is just the password set initially after the first start." explanation="It could be changed in Umami. <br>This is just the password set initially after the first start."

View File

@ -5,10 +5,10 @@
export let service: any; export let service: any;
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title">VSCode Server</div> <div class="title font-bold pb-3">VScode Server</div>
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center px-4">
<label for="password">{$t('forms.password')}</label> <label for="password">{$t('forms.password')}</label>
<CopyPasswordField <CopyPasswordField
id="password" id="password"

View File

@ -3,11 +3,11 @@
export let service: any; export let service: any;
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title">Weblate</div> <div class="title font-bold pb-3">Weblate</div>
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center px-4">
<label for="adminPassword">Admin password</label> <label for="adminPassword">Admin password</label>
<CopyPasswordField <CopyPasswordField
name="adminPassword" name="adminPassword"
@ -19,12 +19,12 @@
/> />
</div> </div>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title">PostgreSQL</div> <div class="title font-bold pb-3">PostgreSQL</div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center px-4">
<label for="postgresqlHost">PostgreSQL Host</label> <label for="postgresqlHost">Host</label>
<CopyPasswordField <CopyPasswordField
name="postgresqlHost" name="postgresqlHost"
id="postgresqlHost" id="postgresqlHost"
@ -33,8 +33,8 @@
disabled disabled
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center px-4">
<label for="postgresqlPort">PostgreSQL Port</label> <label for="postgresqlPort">Port</label>
<CopyPasswordField <CopyPasswordField
name="postgresqlPort" name="postgresqlPort"
id="postgresqlPort" id="postgresqlPort"
@ -43,8 +43,8 @@
disabled disabled
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center px-4">
<label for="postgresqlUser">PostgreSQL User</label> <label for="postgresqlUser">User</label>
<CopyPasswordField <CopyPasswordField
name="postgresqlUser" name="postgresqlUser"
id="postgresqlUser" id="postgresqlUser"
@ -53,8 +53,8 @@
disabled disabled
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center px-4">
<label for="postgresqlPassword">PostgreSQL Password</label> <label for="postgresqlPassword">Password</label>
<CopyPasswordField <CopyPasswordField
name="postgresqlPassword" name="postgresqlPassword"
id="postgresqlPassword" id="postgresqlPassword"

View File

@ -66,11 +66,11 @@
} }
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title">Wordpress</div> <div class="title font-bold pb-3">WordPress</div>
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center px-4">
<label for="extraConfig">{$t('forms.extra_config')}</label> <label for="extraConfig">{$t('forms.extra_config')}</label>
<textarea <textarea
class="w-full" class="w-full"
@ -90,7 +90,7 @@ define('SUBDOMAIN_INSTALL', false);`
: 'N/A'} : 'N/A'}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center px-4">
<Setting <Setting
id="ftpEnabled" id="ftpEnabled"
bind:setting={service.wordpress.ftpEnabled} bind:setting={service.wordpress.ftpEnabled}
@ -102,15 +102,15 @@ define('SUBDOMAIN_INSTALL', false);`
/> />
</div> </div>
{#if service.wordpress.ftpEnabled} {#if service.wordpress.ftpEnabled}
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center px-4">
<label for="ftpUrl">sFTP Connection URI</label> <label for="ftpUrl">sFTP Connection URI</label>
<CopyPasswordField id="ftpUrl" readonly disabled name="ftpUrl" value={ftpUrl} /> <CopyPasswordField id="ftpUrl" readonly disabled name="ftpUrl" value={ftpUrl} />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center px-4">
<label for="ftpUser">User</label> <label for="ftpUser">User</label>
<CopyPasswordField id="ftpUser" readonly disabled name="ftpUser" value={ftpUser} /> <CopyPasswordField id="ftpUser" readonly disabled name="ftpUser" value={ftpUser} />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center px-4">
<label for="ftpPassword">Password</label> <label for="ftpPassword">Password</label>
<CopyPasswordField <CopyPasswordField
id="ftpPassword" id="ftpPassword"
@ -122,11 +122,11 @@ define('SUBDOMAIN_INSTALL', false);`
/> />
</div> </div>
{/if} {/if}
<div class="flex space-x-1 py-5 font-bold"> <div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
<div class="title">MySQL</div> <div class="title font-bold pb-3">MySQL</div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center px-4">
<Setting <Setting
id="ownMysql" id="ownMysql"
dataTooltip={$t('forms.must_be_stopped_to_modify')} dataTooltip={$t('forms.must_be_stopped_to_modify')}
@ -138,7 +138,7 @@ define('SUBDOMAIN_INSTALL', false);`
/> />
</div> </div>
{#if service.wordpress.ownMysql} {#if service.wordpress.ownMysql}
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center px-4">
<label for="mysqlHost">Host</label> <label for="mysqlHost">Host</label>
<input <input
class="w-full" class="w-full"
@ -151,7 +151,7 @@ define('SUBDOMAIN_INSTALL', false);`
placeholder="{$t('forms.eg')}: db.coolify.io" placeholder="{$t('forms.eg')}: db.coolify.io"
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center px-4">
<label for="mysqlPort">Port</label> <label for="mysqlPort">Port</label>
<input <input
class="w-full" class="w-full"
@ -165,7 +165,7 @@ define('SUBDOMAIN_INSTALL', false);`
/> />
</div> </div>
{/if} {/if}
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center px-4">
<label for="mysqlDatabase">{$t('index.database')}</label> <label for="mysqlDatabase">{$t('index.database')}</label>
<input <input
class="w-full" class="w-full"
@ -179,7 +179,7 @@ define('SUBDOMAIN_INSTALL', false);`
/> />
</div> </div>
{#if !service.wordpress.ownMysql} {#if !service.wordpress.ownMysql}
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center px-4">
<label for="mysqlRootUser">{$t('forms.root_user')}</label> <label for="mysqlRootUser">{$t('forms.root_user')}</label>
<input <input
class="w-full" class="w-full"
@ -191,7 +191,7 @@ define('SUBDOMAIN_INSTALL', false);`
disabled={$status.service.isRunning || !service.wordpress.ownMysql} disabled={$status.service.isRunning || !service.wordpress.ownMysql}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center px-4">
<label for="mysqlRootUserPassword">{$t('forms.roots_password')}</label> <label for="mysqlRootUserPassword">{$t('forms.roots_password')}</label>
<CopyPasswordField <CopyPasswordField
id="mysqlRootUserPassword" id="mysqlRootUserPassword"
@ -203,7 +203,7 @@ define('SUBDOMAIN_INSTALL', false);`
/> />
</div> </div>
{/if} {/if}
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center px-4">
<label for="mysqlUser">{$t('forms.user')}</label> <label for="mysqlUser">{$t('forms.user')}</label>
<input <input
class="w-full" class="w-full"
@ -214,7 +214,7 @@ define('SUBDOMAIN_INSTALL', false);`
disabled={$status.service.isRunning || !service.wordpress.ownMysql} disabled={$status.service.isRunning || !service.wordpress.ownMysql}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2"> <div class="grid grid-cols-2 items-center px-4">
<label for="mysqlPassword">{$t('forms.password')}</label> <label for="mysqlPassword">{$t('forms.password')}</label>
<CopyPasswordField <CopyPasswordField
id="mysqlPassword" id="mysqlPassword"

View File

@ -0,0 +1,15 @@
//@ts-nocheck
export { default as Plausibleanalytics } from './PlausibleAnalytics.svelte';
export { default as Minio } from './MinIO.svelte';
export { default as Vscodeserver } from './VSCodeServer.svelte';
export { default as Wordpress } from './Wordpress.svelte';
export { default as Ghost } from './Ghost.svelte';
export { default as Meilisearch } from './MeiliSearch.svelte';
export { default as Umami } from './Umami.svelte';
export { default as Hasura } from './Hasura.svelte';
export { default as Fider } from './Fider.svelte';
export { default as Appwrite } from './Appwrite.svelte';
export { default as Moodle } from './Moodle.svelte';
export { default as Glitchtip } from './GlitchTip.svelte';
export { default as Searxng } from './Searxng.svelte';
export { default as Weblate } from './Weblate.svelte';

View File

@ -0,0 +1,15 @@
export function getStatusOfService(service: any) {
if (service) {
if (service.status.isRunning === 'running') {
return 'running';
}
if (service.status.isExited === 'exited') {
return 'stopped';
}
if (service.status.isRestarting === 'degraded') {
return 'degraded';
}
}
return 'stopped';
}

View File

@ -4,7 +4,7 @@
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class={isAbsolute ? 'w-14 h-14 absolute top-0 left-0 -m-5' : 'w-9 h-9 mx-auto'} class={isAbsolute ? 'w-14 h-14 absolute top-0 left-0 -m-5' : 'w-7 h-7 mx-auto'}
viewBox="0 0 384 384" viewBox="0 0 384 384"
fill="none" fill="none"
> >

View File

@ -5,7 +5,7 @@
<svg <svg
viewBox="0 0 700 240" viewBox="0 0 700 240"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class={isAbsolute ? 'w-36 absolute top-0 left-0 -m-3 -mt-5' : 'w-full h-10 mx-auto'} class={isAbsolute ? 'w-36 absolute top-0 left-0 -m-3 -mt-5' : 'w-16 h-10'}
><path fill="#FDBC3D" d="m90.694 107.498-.981.39-20.608 8.23 6.332 6.547z" /><path ><path fill="#FDBC3D" d="m90.694 107.498-.981.39-20.608 8.23 6.332 6.547z" /><path
fill="#8EC63F" fill="#8EC63F"
d="M61.139 77.914 46.632 93 56.9 103.547c8.649-7.169 17.832-10.502 18.653-10.789L61.139 77.914z" d="M61.139 77.914 46.632 93 56.9 103.547c8.649-7.169 17.832-10.502 18.653-10.789L61.139 77.914z"

View File

@ -2,48 +2,7 @@
export let type: string; export let type: string;
export let isAbsolute = true; export let isAbsolute = true;
import * as Icons from '$lib/components/svg/services'; import * as Icons from '$lib/components/svg/services';
const name: any = type && type[0].toUpperCase() + type.substring(1).toLowerCase();
</script> </script>
{#if type === 'plausibleanalytics'} <svelte:component this={Icons[name]} {isAbsolute} />
<Icons.PlausibleAnalytics {isAbsolute} />
{:else if type === 'nocodb'}
<Icons.NocoDb {isAbsolute} />
{:else if type === 'minio'}
<Icons.MinIo {isAbsolute} />
{:else if type === 'vscodeserver'}
<Icons.VsCodeServer {isAbsolute} />
{:else if type === 'wordpress'}
<Icons.Wordpress {isAbsolute} />
{:else if type === 'vaultwarden'}
<Icons.VaultWarden {isAbsolute} />
{:else if type === 'languagetool'}
<Icons.LanguageTool {isAbsolute} />
{:else if type === 'n8n'}
<Icons.N8n {isAbsolute} />
{:else if type === 'uptimekuma'}
<Icons.UptimeKuma {isAbsolute} />
{:else if type === 'ghost'}
<Icons.Ghost {isAbsolute} />
{:else if type === 'meilisearch'}
<Icons.MeiliSearch {isAbsolute} />
{:else if type === 'umami'}
<Icons.Umami {isAbsolute} />
{:else if type === 'hasura'}
<Icons.Hasura {isAbsolute} />
{:else if type === 'fider'}
<Icons.Fider {isAbsolute} />
{:else if type === 'appwrite'}
<Icons.Appwrite {isAbsolute} />
{:else if type === 'moodle'}
<Icons.Moodle {isAbsolute} />
{:else if type === 'glitchTip'}
<Icons.GlitchTip {isAbsolute} />
{:else if type === 'searxng'}
<Icons.Searxng {isAbsolute} />
{:else if type === 'weblate'}
<Icons.Weblate {isAbsolute} />
{:else if type === 'grafana'}
<Icons.Grafana {isAbsolute} />
{:else if type === 'trilium'}
<Icons.Trilium {isAbsolute} />
{/if}

View File

@ -1,21 +1,21 @@
//@ts-nocheck //@ts-nocheck
export { default as PlausibleAnalytics } from './PlausibleAnalytics.svelte'; export { default as Plausibleanalytics } from './PlausibleAnalytics.svelte';
export { default as NocoDb } from './NocoDB.svelte'; export { default as Nocodb } from './NocoDB.svelte';
export { default as MinIo } from './MinIO.svelte'; export { default as Minio } from './MinIO.svelte';
export { default as VsCodeServer } from './VSCodeServer.svelte'; export { default as Vscodeserver } from './VSCodeServer.svelte';
export { default as Wordpress } from './Wordpress.svelte'; export { default as Wordpress } from './Wordpress.svelte';
export { default as VaultWarden } from './VaultWarden.svelte'; export { default as Vaultwarden } from './VaultWarden.svelte';
export { default as LanguageTool } from './LanguageTool.svelte'; export { default as Languagetool } from './LanguageTool.svelte';
export { default as N8n } from './N8n.svelte'; export { default as N8n } from './N8n.svelte';
export { default as UptimeKuma } from './UptimeKuma.svelte'; export { default as Uptimekuma } from './UptimeKuma.svelte';
export { default as Ghost } from './Ghost.svelte'; export { default as Ghost } from './Ghost.svelte';
export { default as MeiliSearch } from './MeiliSearch.svelte'; export { default as Meilisearch } from './MeiliSearch.svelte';
export { default as Umami } from './Umami.svelte'; export { default as Umami } from './Umami.svelte';
export { default as Hasura } from './Hasura.svelte'; export { default as Hasura } from './Hasura.svelte';
export { default as Fider } from './Fider.svelte'; export { default as Fider } from './Fider.svelte';
export { default as Appwrite } from './Appwrite.svelte'; export { default as Appwrite } from './Appwrite.svelte';
export { default as Moodle } from './Moodle.svelte'; export { default as Moodle } from './Moodle.svelte';
export { default as GlitchTip } from './GlitchTip.svelte'; export { default as Glitchtip } from './GlitchTip.svelte';
export { default as Searxng } from './Searxng.svelte'; export { default as Searxng } from './Searxng.svelte';
export { default as Weblate } from './Weblate.svelte'; export { default as Weblate } from './Weblate.svelte';
export { default as Grafana } from './Grafana.svelte'; export { default as Grafana } from './Grafana.svelte';

View File

@ -81,8 +81,8 @@ export const status: Writable<any> = writable({
initialLoading: true initialLoading: true
}, },
service: { service: {
isRunning: false, statuses: [],
isExited: false, overallStatus: 'stopped',
loading: false, loading: false,
initialLoading: true initialLoading: true
}, },

View File

@ -6,7 +6,7 @@
<ul class="menu border bg-coolgray-100 border-coolgray-200 rounded p-2 space-y-2 sticky top-4"> <ul class="menu border bg-coolgray-100 border-coolgray-200 rounded p-2 space-y-2 sticky top-4">
<li class="menu-title"> <li class="menu-title">
<span>Configuration</span> <span>General</span>
</li> </li>
{#if application.gitSource?.htmlUrl && application.repository && application.branch} {#if application.gitSource?.htmlUrl && application.repository && application.branch}
<li> <li>

View File

@ -91,7 +91,7 @@
required required
placeholder="EXAMPLE_VARIABLE" placeholder="EXAMPLE_VARIABLE"
readonly={!isNewSecret} readonly={!isNewSecret}
class=" w-full" class="w-full"
class:bg-coolblack={!isNewSecret} class:bg-coolblack={!isNewSecret}
class:border={!isNewSecret} class:border={!isNewSecret}
class:border-dashed={!isNewSecret} class:border-dashed={!isNewSecret}
@ -166,7 +166,7 @@
</div> </div>
<div class="flex flex-row lg:flex-col lg:items-center items-start"> <div class="flex flex-row lg:flex-col lg:items-center items-start">
{#if (index === 0 && !isNewSecret) || length === 0} {#if (index === 0 && !isNewSecret) || length === 0}
<label for="name" class="pb-2 uppercase lg:block hidden">Actions</label> <label for="name" class="pb-2 uppercase lg:block hidden">Action</label>
{/if} {/if}
<div class="flex justify-center h-full items-center pt-3"> <div class="flex justify-center h-full items-center pt-3">

View File

@ -59,9 +59,8 @@
} }
</script> </script>
<div class="w-full font-bold grid gap-2"> <div class="w-full grid gap-2">
<div class="flex flex-col pb-2"> <div class="flex flex-col pb-2">
<div class="flex flex-col lg:flex-row lg:space-y-0 space-y-2"> <div class="flex flex-col lg:flex-row lg:space-y-0 space-y-2">
<input <input
class="w-full lg:w-64" class="w-full lg:w-64"

View File

@ -233,7 +233,7 @@
class:text-red-500={$status.application.overallStatus === 'stopped'} class:text-red-500={$status.application.overallStatus === 'stopped'}
> >
{$status.application.overallStatus === 'healthy' {$status.application.overallStatus === 'healthy'
? 'Running' ? 'Healthy'
: $status.application.overallStatus === 'degraded' : $status.application.overallStatus === 'degraded'
? 'Degraded' ? 'Degraded'
: 'Stopped'} : 'Stopped'}

View File

@ -244,7 +244,7 @@
{/if} {/if}
</div> </div>
<div class="flex flex-row items-center"> <div class="flex flex-row items-center">
<div class="title py-4">Public Repository</div> <div class="title py-4 pr-4">Public Repository</div>
<DocLink url="https://docs.coollabs.io/coolify/applications/#public-repository" /> <DocLink url="https://docs.coollabs.io/coolify/applications/#public-repository" />
</div> </div>
<PublicRepository /> <PublicRepository />

View File

@ -61,7 +61,7 @@
disabled={!$appSession.isAdmin} disabled={!$appSession.isAdmin}
class:bg-red-600={$appSession.isAdmin} class:bg-red-600={$appSession.isAdmin}
class:hover:bg-red-500={$appSession.isAdmin} class:hover:bg-red-500={$appSession.isAdmin}
class="btn btn-sm btn-error text-sm" class="btn btn-lg btn-error text-sm"
> >
Force Delete Application Force Delete Application
</button> </button>

View File

@ -459,7 +459,7 @@
<form on:submit|preventDefault={() => handleSubmit()}> <form on:submit|preventDefault={() => handleSubmit()}>
<div class="mx-auto w-full"> <div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2"> <div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3 ">General</div> <div class="title font-bold pb-3">General</div>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<button <button
class="btn btn-sm btn-primary" class="btn btn-sm btn-primary"

View File

@ -135,7 +135,7 @@
<div class="text-xl font-bold tracking-tighter">Container not found / exited.</div> <div class="text-xl font-bold tracking-tighter">Container not found / exited.</div>
{/if} {/if}
{:else} {:else}
<div class="relative w-full"> <div class="relative w-full"></div>
<div class="flex justify-start sticky space-x-2 pb-2"> <div class="flex justify-start sticky space-x-2 pb-2">
<button <button
on:click={followBuild} on:click={followBuild}
@ -162,8 +162,9 @@
{followingLogs ? 'Following Logs...' : 'Follow Logs'} {followingLogs ? 'Following Logs...' : 'Follow Logs'}
</button> </button>
{#if loadLogsInterval} {#if loadLogsInterval}
<button id="streaming" class="btn btn-sm bg-transparent border-none loading" /> <button id="streaming" class="btn btn-sm bg-transparent border-none loading"
<Tooltip triggeredBy="#streaming">Streaming logs</Tooltip> >Streaming logs</button
>
{/if} {/if}
</div> </div>
<div <div

View File

@ -88,10 +88,6 @@
); );
batchSecrets = ''; batchSecrets = '';
await refreshSecrets(); await refreshSecrets();
// addToast({
// message: 'Secrets saved.',
// type: 'success'
// });
} }
</script> </script>

View File

@ -0,0 +1,136 @@
<script lang="ts">
export let service: any;
import { status } from '$lib/store';
import { page } from '$app/stores';
import ServiceLinks from './_ServiceLinks.svelte';
</script>
<ul class="menu border bg-coolgray-100 border-coolgray-200 rounded p-2 space-y-2 sticky top-4">
<li class="menu-title">
<span>General</span>
</li>
<li class="rounded">
<ServiceLinks {service} linkToDocs={true} />
</li>
<li class="rounded" class:bg-coollabs={$page.url.pathname === `/services/${$page.params.id}`}>
<a href={`/services/${$page.params.id}`} class="no-underline w-full"
><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 10h3v-3l-3.5 -3.5a6 6 0 0 1 8 8l6 6a2 2 0 0 1 -3 3l-6 -6a6 6 0 0 1 -8 -8l3.5 3.5"
/>
</svg>Configurations</a
>
</li>
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/services/${$page.params.id}/secrets`}
>
<a href={`/services/${$page.params.id}/secrets`} class="no-underline w-full"
><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>Secrets</a
>
</li>
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/services/${$page.params.id}/storages`}
>
<a href={`/services/${$page.params.id}/storages`} class="no-underline w-full"
><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>Persistent Volumes</a
>
</li>
<li class="menu-title">
<span>Logs</span>
</li>
<li
class:text-stone-600={$status.service.overallStatus === 'stopped'}
class="rounded"
class:bg-coollabs={$page.url.pathname === `/services/${$page.params.id}/logs`}
>
<a
href={$status.service.overallStatus !== 'stopped' ? `/services/${$page.params.id}/logs` : ''}
class="no-underline w-full"
><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>Service</a
>
</li>
<li class="menu-title">
<span>Advanced</span>
</li>
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/services/${$page.params.id}/danger`}
>
<a href={`/services/${$page.params.id}/danger`} class="no-underline w-full"
><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 9v2m0 4v.01" />
<path
d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"
/>
</svg>Danger Zone</a
>
</li>
</ul>

View File

@ -50,14 +50,17 @@
<td> <td>
<input <input
style="min-width: 350px !important;"
style="min-width: 350px !important;"
id={isNewSecret ? 'secretName' : 'secretNameNew'} id={isNewSecret ? 'secretName' : 'secretNameNew'}
bind:value={name} bind:value={name}
required required
placeholder="EXAMPLE_VARIABLE" placeholder="EXAMPLE_VARIABLE"
readonly={!isNewSecret} readonly={!isNewSecret}
class:bg-transparent={!isNewSecret} class="w-full"
class:bg-coolblack={!isNewSecret}
class:border={!isNewSecret}
class:border-dashed={!isNewSecret}
class:border-coolgray-300={!isNewSecret}
class:cursor-not-allowed={!isNewSecret} class:cursor-not-allowed={!isNewSecret}
/> />
</td> </td>
@ -67,7 +70,6 @@
name={isNewSecret ? 'secretValue' : 'secretValueNew'} name={isNewSecret ? 'secretValue' : 'secretValueNew'}
isPasswordField={true} isPasswordField={true}
bind:value bind:value
required
placeholder="J$#@UIO%HO#$U%H" placeholder="J$#@UIO%HO#$U%H"
inputStyle="min-width: 350px; !important" inputStyle="min-width: 350px; !important"
/> />
@ -76,12 +78,12 @@
<td> <td>
{#if isNewSecret} {#if isNewSecret}
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<button class="btn btn-sm bg-services" on:click={() => saveSecret(true)}>Add</button> <button class="btn btn-sm btn-primary" on:click={() => saveSecret(true)}>Add</button>
</div> </div>
{:else} {:else}
<div class="flex flex-row justify-center space-x-2"> <div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<button class="btn btn-sm bg-services" on:click={() => saveSecret(false)}>Set</button> <button class="btn btn-sm btn-primary" on:click={() => saveSecret(false)}>Set</button>
</div> </div>
<div class="flex justify-center items-end"> <div class="flex justify-center items-end">
<button class="btn btn-sm bg-error" on:click={removeSecret}>Remove</button> <button class="btn btn-sm bg-error" on:click={removeSecret}>Remove</button>

View File

@ -1,86 +1,45 @@
<script lang="ts"> <script lang="ts">
import DocLink from '$lib/components/DocLink.svelte';
export let service: any; export let service: any;
export let linkToDocs: boolean = false;
import ExternalLink from '$lib/components/ExternalLink.svelte';
import * as Icons from '$lib/components/svg/services'; import * as Icons from '$lib/components/svg/services';
const name: any = service.type && service.type[0].toUpperCase() + service.type.substring(1);
const links: any = {
plausible: 'https://plausible.io/doc/',
nocodb: 'https://docs.nocodb.com',
minio: 'https://min.io/docs/minio',
vscodeserver: 'https://coder.com/docs/coder-oss/latest',
wordpress: 'https://wordpress.org/',
vaultwarden: 'https://github.com/dani-garcia/vaultwarden',
languagetool: 'https://languagetool.org/dev',
n8n: 'https://docs.n8n.io',
uptimekuma: 'https://github.com/louislam/uptime-kuma',
ghost: 'https://ghost.org/resources/',
umami: 'https://umami.is/docs/getting-started',
hasura: 'https://hasura.io/docs/latest/index/',
fider: 'https://fider.io/docs',
appwrite: 'https://appwrite.io/docs',
moodle: 'https://docs.moodle.org/400/en/Main_page',
glitchtip: 'https://glitchtip.com/documentation',
searxng: 'https://searxng.org',
weblate: 'https://docs.weblate.org/en/latest/',
grafana: 'https://github.com/grafana/grafana',
trilium: 'https://github.com/zadam/trilium'
};
</script> </script>
{#if service.type === 'plausibleanalytics'} {#if linkToDocs}
<a href="https://plausible.io" target="_blank"> <DocLink url={links[service.type]} text={`Open documentation`} isExternal={true} />
<Icons.PlausibleAnalytics /> {:else}
</a> <svelte:component this={Icons[name]} />
{:else if service.type === 'nocodb'}
<a href="https://nocodb.com" target="_blank">
<Icons.NocoDb />
</a>
{:else if service.type === 'minio'}
<a href="https://min.io" target="_blank">
<Icons.MinIo />
</a>
{:else if service.type === 'vscodeserver'}
<a href="https://coder.com" target="_blank">
<Icons.VsCodeServer />
</a>
{:else if service.type === 'wordpress'}
<a href="https://wordpress.org" target="_blank">
<Icons.Wordpress />
</a>
{:else if service.type === 'vaultwarden'}
<a href="https://github.com/dani-garcia/vaultwarden" target="_blank">
<Icons.VaultWarden />
</a>
{:else if service.type === 'languagetool'}
<a href="https://languagetool.org/dev" target="_blank">
<Icons.LanguageTool />
</a>
{:else if service.type === 'n8n'}
<a href="https://n8n.io" target="_blank">
<Icons.N8n />
</a>
{:else if service.type === 'uptimekuma'}
<a href="https://github.com/louislam/uptime-kuma" target="_blank">
<Icons.UptimeKuma />
</a>
{:else if service.type === 'ghost'}
<a href="https://ghost.org" target="_blank">
<Icons.Ghost />
</a>
{:else if service.type === 'umami'}
<a href="https://umami.is" target="_blank">
<Icons.Umami />
</a>
{:else if service.type === 'hasura'}
<a href="https://hasura.io" target="_blank">
<Icons.Hasura />
</a>
{:else if service.type === 'fider'}
<a href="https://fider.io" target="_blank">
<Icons.Fider />
</a>
{:else if service.type === 'appwrite'}
<a href="https://appwrite.io" target="_blank">
<Icons.Appwrite />
</a>
{:else if service.type === 'moodle'}
<a href="https://moodle.org" target="_blank">
<Icons.Moodle />
</a>
{:else if service.type === 'glitchTip'}
<a href="https://glitchtip.com" target="_blank">
<Icons.GlitchTip />
</a>
{:else if service.type === 'searxng'}
<a href="https://searxng.org" target="_blank">
<Icons.Searxng />
</a>
{:else if service.type === 'weblate'}
<a href="https://weblate.org" target="_blank">
<Icons.Weblate />
</a>
{:else if service.type === 'grafana'}
<a href="https://github.com/grafana/grafana" target="_blank">
<Icons.Grafana />
</a>
{:else if service.type === 'trilium'}
<a href="https://github.com/zadam/trilium" target="_blank">
<Icons.Trilium />
</a>
{/if} {/if}
<!-- <a href={links[service.type]} target="_blank" class="no-underline">
{#if linkToDocs}
Open Documentation
<ExternalLink />
{:else}
{/if}
</a> -->

View File

@ -1,99 +0,0 @@
<script lang="ts">
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;
</script>
<div class="flex space-x-1 py-5">
<div class="title">
Ghost <Explainer explanation="You can change these values in the <span class='text-settings'>Ghost admin panel<span>." />
</div>
</div>
<div class="space-y-2">
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="email">{$t('forms.default_email_address')}</label>
<input
class="w-full"
name="email"
id="email"
disabled
readonly
placeholder={$t('forms.email')}
value={service.ghost.defaultEmail}
required
/>
</div>
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="defaultPassword">{$t('forms.default_password')}</label>
<CopyPasswordField
id="defaultPassword"
isPasswordField
readonly
disabled
name="defaultPassword"
value={service.ghost.defaultPassword}
/>
</div>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">MariaDB</div>
</div>
<div class="space-y-2">
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<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 lg:px-10 px-2">
<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 lg:px-10 px-2">
<label for="mariadbDatabase">{$t('index.database')}</label>
<input
class="w-full"
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 lg:px-10 px-2">
<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 lg:px-10 px-2">
<label for="mariadbRootUserPassword">{$t('forms.root_db_password')}</label>
<CopyPasswordField
id="mariadbRootUserPassword"
isPasswordField
readonly
disabled
name="mariadbRootUserPassword"
value={service.ghost.mariadbRootUserPassword}
/>
</div>
</div>

View File

@ -1,450 +0,0 @@
<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 { get, post } from '$lib/api';
import { errorNotification, getDomain } from '$lib/common';
import { t } from '$lib/translations';
import {
appSession,
status,
setLocation,
addToast,
checkIfDeploymentEnabledServices,
isDeploymentEnabled
} from '$lib/store';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Setting from '$lib/components/Setting.svelte';
import Fider from './_Fider.svelte';
import Ghost from './_Ghost.svelte';
import GlitchTip from './_GlitchTip.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';
import Appwrite from './_Appwrite.svelte';
import Moodle from './_Moodle.svelte';
import Searxng from './_Searxng.svelte';
import Weblate from './_Weblate.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import Taiga from './_Taiga.svelte';
import DocLink from '$lib/components/DocLink.svelte';
const { id } = $page.params;
$: isDisabled =
!$appSession.isAdmin || $status.service.isRunning || $status.service.initialLoading;
let forceSave = false;
let loading = {
save: false,
verification: false,
cleanup: false
};
let dualCerts = service.dualCerts;
let nonWWWDomain = service.fqdn && getDomain(service.fqdn).replace(/^www\./, '');
let isNonWWWDomainOK = false;
let isWWWDomainOK = false;
async function isDNSValid(domain: any, isWWW: any) {
try {
await get(`/services/${id}/check?domain=${domain}`);
addToast({
message: 'DNS configuration is valid.',
type: 'success'
});
isWWW ? (isWWWDomainOK = true) : (isNonWWWDomainOK = true);
return true;
} catch (error) {
errorNotification(error);
isWWW ? (isWWWDomainOK = false) : (isNonWWWDomainOK = false);
return false;
}
}
async function handleSubmit() {
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}`, { ...service });
setLocation(service);
forceSave = false;
$isDeploymentEnabled = checkIfDeploymentEnabledServices($appSession.isAdmin, service);
return addToast({
message: 'Configuration saved.',
type: 'success'
});
} catch (error) {
//@ts-ignore
if (error?.message.startsWith($t('application.dns_not_set_partial_error'))) {
forceSave = true;
if (dualCerts) {
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
} else {
const isWWW = getDomain(service.fqdn).includes('www.');
if (isWWW) {
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
} else {
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
}
}
}
return errorNotification(error);
} finally {
loading.save = false;
}
}
async function setEmailsToVerified() {
loading.verification = true;
try {
await post(`/services/${id}/${service.type}/activate`, { id: service.id });
return addToast({
message: t.get('services.all_email_verified'),
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading.verification = false;
}
}
async function migrateAppwriteDB() {
loading.verification = true;
try {
await post(`/services/${id}/${service.type}/migrate`, { id: service.id });
return addToast({
message: "Appwrite's database has been migrated.",
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading.verification = false;
}
}
async function changeSettings(name: any) {
try {
if (name === 'dualCerts') {
dualCerts = !dualCerts;
}
await post(`/services/${id}/settings`, { dualCerts });
return addToast({
message: t.get('application.settings_saved'),
type: 'success'
});
} catch (error) {
return errorNotification(error);
}
}
async function cleanupLogs() {
loading.cleanup = true;
try {
await post(`/services/${id}/${service.type}/cleanup`, { id: service.id });
return addToast({
message: 'Cleared DB Logs',
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading.cleanup = false;
}
}
function doNothing() {
return;
}
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-6xl px-6 pb-12">
<form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-5 items-center">
<h1 class="title">{$t('general')}</h1>
<div class="flex flex-row space-x-2 items-center">
{#if $appSession.isAdmin}
<button
type="submit"
class="btn btn-sm"
class:bg-orange-600={forceSave}
class:hover:bg-orange-400={forceSave}
class:loading={loading.save}
class:bg-services={!loading.save}
disabled={loading.save}
>{loading.save
? $t('forms.save')
: forceSave
? $t('forms.confirm_continue')
: $t('forms.save')}</button
>
{/if}
{#if service.type === 'plausibleanalytics' && $status.service.isRunning}
<div class="btn-group">
<button
class="btn btn-sm"
on:click|preventDefault={setEmailsToVerified}
disabled={loading.verification}
class:loading={loading.verification}
>{loading.verification
? $t('forms.verifying')
: $t('forms.verify_emails_without_smtp')}</button
>
<button
class="btn btn-sm"
on:click|preventDefault={cleanupLogs}
disabled={loading.cleanup}
class:loading={loading.cleanup}>Cleanup Unnecessary Database Logs</button
>
</div>
{/if}
{#if service.type === 'appwrite' && $status.service.isRunning}
<button
class="btn btn-sm"
on:click|preventDefault={migrateAppwriteDB}
disabled={loading.verification}
class:loading={loading.verification}
>{loading.verification
? 'Migrating... it may take a while...'
: "Migrate Appwrite's Database"}</button
>
<DocLink url="https://appwrite.io/docs/upgrade#run-the-migration" />
{/if}
</div>
</div>
{#if service.type === 'minio' && !service.minio.apiFqdn && $status.service.isRunning}
<div class="py-5">
<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="grid gap-2 grid-cols-1 grid-rows-1 lg:px-10 px-2">
<div class="mt-2 grid grid-cols-2 items-center">
<label for="name">{$t('forms.name')}</label>
<input name="name" id="name" class="w-full" bind:value={service.name} required />
</div>
<div class="grid grid-cols-2 items-center">
<label for="version">Version / Tag</label>
<a
href={$appSession.isAdmin && !$status.service.isRunning && !$status.service.initialLoading
? `/services/${id}/configuration/version?from=/services/${id}`
: ''}
class="no-underline"
>
<input
class="w-full"
value={service.version}
id="service"
readonly
disabled={$status.service.isRunning || $status.service.initialLoading}
class:cursor-pointer={!$status.service.isRunning}
/></a
>
</div>
<div class="grid grid-cols-2 items-center">
<label for="destination">{$t('application.destination')}</label>
<div>
{#if service.destinationDockerId}
<div class="no-underline">
<input
value={service.destinationDocker.name}
id="destination"
disabled
class="bg-transparent w-full"
/>
</div>
{/if}
</div>
</div>
{#if service.type === 'minio'}
<div class="grid grid-cols-2 items-center">
<label for="fqdn">Console URL</label>
<CopyPasswordField
placeholder="eg: https://console.min.io"
readonly={isDisabled}
disabled={isDisabled}
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 items-center">
<label for="apiFqdn"
>API URL <Explainer explanation={$t('application.https_explainer')} /></label
>
<CopyPasswordField
placeholder="eg: https://min.io"
readonly={!$appSession.isAdmin && !$status.service.isRunning}
disabled={isDisabled}
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 items-center">
<label for="fqdn"
>{$t('application.url_fqdn')}
<Explainer explanation={$t('application.https_explainer')} />
</label>
<CopyPasswordField
placeholder="eg: https://analytics.coollabs.io"
readonly={!$appSession.isAdmin && !$status.service.isRunning}
disabled={!$appSession.isAdmin ||
$status.service.isRunning ||
$status.service.initialLoading}
name="fqdn"
id="fqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
bind:value={service.fqdn}
required
/>
</div>
{/if}
</div>
{#if forceSave}
<div class="flex-col space-y-2 pt-4 text-center">
{#if isNonWWWDomainOK}
<button
class="btn btn-sm bg-green-600 hover:bg-green-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="btn btn-sm bg-red-600 hover:bg-red-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{#if dualCerts}
{#if isWWWDomainOK}
<button
class="btn btn-sm bg-green-600 hover:bg-green-500"
on:click|preventDefault={() => isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="btn btn-sm bg-red-600 hover:bg-red-500"
on:click|preventDefault={() => isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{/if}
</div>
{/if}
<div class="grid grid-flow-row gap-2 lg:px-10 px-2 pt-2">
<div class="grid grid-cols-2 items-center">
<Setting
id="dualCerts"
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">
<label for="exposePort"
>Exposed Port <Explainer
explanation={'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.'}
/></label
>
<input
class="w-full"
readonly={!$appSession.isAdmin && !$status.service.isRunning}
disabled={!$appSession.isAdmin ||
$status.service.isRunning ||
$status.service.initialLoading}
name="exposePort"
id="exposePort"
bind:value={service.exposePort}
placeholder="12345"
/>
</div>
</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} />
{:else if service.type === 'appwrite'}
<Appwrite bind:service {readOnly} />
{:else if service.type === 'moodle'}
<Moodle bind:service {readOnly} />
{:else if service.type === 'glitchTip'}
<GlitchTip bind:service />
{:else if service.type === 'searxng'}
<Searxng bind:service />
{:else if service.type === 'weblate'}
<Weblate bind:service />
{:else if service.type === 'taiga'}
<Taiga bind:service />
{/if}
</form>
</div>

View File

@ -8,6 +8,7 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import { addToast } from '$lib/store'; import { addToast } from '$lib/store';
const { id } = $page.params; const { id } = $page.params;
@ -15,7 +16,7 @@
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
async function saveStorage(newStorage = false) { async function saveStorage(newStorage = false) {
try { try {
if (!storage.path) return errorNotification({message: "Path is required!"}); if (!storage.path) return errorNotification($t('application.storage.path_is_required'));
storage.path = storage.path.startsWith('/') ? storage.path : `/${storage.path}`; storage.path = storage.path.startsWith('/') ? storage.path : `/${storage.path}`;
storage.path = storage.path.endsWith('/') ? storage.path.slice(0, -1) : storage.path; storage.path = storage.path.endsWith('/') ? storage.path.slice(0, -1) : storage.path;
storage.path.replace(/\/\//g, '/'); storage.path.replace(/\/\//g, '/');
@ -29,10 +30,17 @@
storage.path = null; storage.path = null;
storage.id = null; storage.id = null;
} }
addToast({ if (newStorage) {
message: 'Storage saved.', addToast({
type: 'success' message: $t('application.storage.storage_saved'),
}); type: 'success'
});
} else {
addToast({
message: $t('application.storage.storage_updated'),
type: 'success'
});
}
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} }
@ -41,46 +49,45 @@
try { try {
await del(`/services/${id}/storages`, { path: storage.path }); await del(`/services/${id}/storages`, { path: storage.path });
dispatch('refresh'); dispatch('refresh');
return addToast({ addToast({
message: 'Storage deleted.', message: $t('application.storage.storage_deleted'),
type: 'success' type: 'success'
}); });
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} }
} }
async function handleSubmit() {
if (isNew) {
await saveStorage(true);
} else {
await saveStorage(false);
}
}
</script> </script>
<td> <div class="w-fullgrid gap-2">
<form on:submit|preventDefault={handleSubmit}> <div class="flex flex-col pb-2">
<input <div class="flex flex-col lg:flex-row lg:space-y-0 space-y-2">
bind:value={storage.path} <input
required class="w-full lg:w-64"
placeholder="eg: /data" bind:value={storage.path}
/> required
</form> placeholder="eg: /sqlite.db"
</td> />
<td> {#if isNew}
{#if isNew} <div class="flex items-center justify-center w-full lg:w-64">
<div class="flex items-center justify-center"> <button class="btn btn-sm btn-primary" on:click={() => saveStorage(true)}
<button class="btn btn-sm btn-success" on:click={() => saveStorage(true)}>Add</button >{$t('forms.add')}</button
> >
</div>
{:else}
<div class="flex flex-row items-center justify-center space-x-2 w-full lg:w-64">
<div class="flex items-center justify-center">
<button class="btn btn-sm btn-primary" on:click={() => saveStorage(false)}
>{$t('forms.set')}</button
>
</div>
<div class="flex justify-center">
<button class="btn btn-sm btn-error" on:click={removeStorage}
>{$t('forms.remove')}</button
>
</div>
</div>
{/if}
</div> </div>
{:else} </div>
<div class="flex flex-row justify-center space-x-2"> </div>
<div class="flex items-center justify-center">
<button class="btn btn-sm btn-success" on:click={() => saveStorage(false)}>Set</button>
</div>
<div class="flex justify-center items-end">
<button class="btn btn-sm btn-error" on:click={removeStorage}>Remove</button>
</div>
</div>
{/if}
</td>

View File

@ -4,8 +4,6 @@
let configurationPhase = null; let configurationPhase = null;
if (!service.type) { if (!service.type) {
configurationPhase = 'type'; configurationPhase = 'type';
} else if (!service.version) {
configurationPhase = 'version';
} else if (!service.destinationDockerId) { } else if (!service.destinationDockerId) {
configurationPhase = 'destination'; configurationPhase = 'destination';
} }
@ -15,7 +13,7 @@
try { try {
let readOnly = false; let readOnly = false;
const response = await get(`/services/${params.id}`); const response = await get(`/services/${params.id}`);
const { service, settings } = await response; const { service, settings, template } = await response;
if (!service || Object.entries(service).length === 0) { if (!service || Object.entries(service).length === 0) {
return { return {
status: 302, status: 302,
@ -38,7 +36,8 @@
return { return {
props: { props: {
service service,
template
}, },
stuff: { stuff: {
service, service,
@ -53,8 +52,9 @@
</script> </script>
<script lang="ts"> <script lang="ts">
export let service: any;
import { page } from '$app/stores'; import { page } from '$app/stores';
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import { del, get, post } from '$lib/api'; import { del, get, post } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification, handlerNotFoundLoad } from '$lib/common'; import { errorNotification, handlerNotFoundLoad } from '$lib/common';
@ -67,13 +67,10 @@
checkIfDeploymentEnabledServices checkIfDeploymentEnabledServices
} from '$lib/store'; } from '$lib/store';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
import ServiceLinks from './_ServiceLinks.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import Menu from './_Menu.svelte';
const { id } = $page.params; const { id } = $page.params;
export let service: any;
$isDeploymentEnabled = checkIfDeploymentEnabledServices($appSession.isAdmin, service); $isDeploymentEnabled = checkIfDeploymentEnabledServices($appSession.isAdmin, service);
let statusInterval: any; let statusInterval: any;
@ -127,25 +124,46 @@
if ($status.service.loading) return; if ($status.service.loading) return;
$status.service.loading = true; $status.service.loading = true;
const data = await get(`/services/${id}/status`); const data = await get(`/services/${id}/status`);
$status.service.isRunning = data.isRunning;
$status.service.isExited = data.isExited; $status.service.statuses = data;
$status.service.initialLoading = false; let numberOfServices = Object.keys(data).length;
if (Object.keys($status.service.statuses).length === 0) {
$status.service.overallStatus = 'stopped';
} else {
if (Object.keys($status.service.statuses).length !== numberOfServices) {
$status.service.overallStatus = 'degraded';
} else {
for (const oneService in $status.service.statuses) {
const { isExited, isRestarting, isRunning } = $status.service.statuses[oneService].status;
if (isExited || isRestarting) {
$status.service.overallStatus = 'degraded';
break;
}
if (isRunning) {
$status.service.overallStatus = 'healthy';
}
if (!isExited && !isRestarting && !isRunning) {
$status.service.overallStatus = 'stopped';
}
}
}
}
$status.service.loading = false; $status.service.loading = false;
$status.service.initialLoading = false;
} }
onDestroy(() => { onDestroy(() => {
$status.service.initialLoading = true; $status.service.initialLoading = true;
$status.service.isRunning = false;
$status.service.isExited = false;
$status.service.loading = false; $status.service.loading = false;
$status.service.statuses = [];
$location = null; $location = null;
$isDeploymentEnabled = false; $isDeploymentEnabled = false;
clearInterval(statusInterval); clearInterval(statusInterval);
}); });
onMount(async () => { onMount(async () => {
setLocation(service); setLocation(service);
$status.service.isRunning = false;
$status.service.loading = false; $status.service.loading = false;
if (service.type && service.destinationDockerId && service.version && service.fqdn) { if ($isDeploymentEnabled) {
await getStatus(); await getStatus();
statusInterval = setInterval(async () => { statusInterval = setInterval(async () => {
await getStatus(); await getStatus();
@ -156,90 +174,65 @@
}); });
</script> </script>
<nav class="header lg:flex-row flex-col-reverse"> <div class="mx-auto max-w-screen-2xl px-6 grid grid-cols-1 lg:grid-cols-2">
<div class="flex flex-row space-x-2 font-bold pt-10 lg:pt-0"> <nav class="header flex flex-row order-2 lg:order-1 px-0 lg:px-4 items-start">
<div class="flex flex-col items-center justify-center"> <div class="title lg:pb-10">
<div class="title"> <div class="flex justify-center items-center space-x-2">
{#if $page.url.pathname === `/services/${id}`} <div>
Configurations {#if $page.url.pathname === `/services/${id}/secrets`}
{:else if $page.url.pathname === `/services/${id}/secrets`} Secrets
Secrets {:else if $page.url.pathname === `/services/${id}/storages`}
{:else if $page.url.pathname === `/services/${id}/storages`} Persistent Storages
Persistent Storages {:else if $page.url.pathname === `/services/${id}/logs`}
{:else if $page.url.pathname === `/services/${id}/logs`} Service Logs
Service Logs {:else if $page.url.pathname === `/services/${id}/configuration/type`}
{:else if $page.url.pathname === `/services/${id}/configuration/type`} Select a Service Type
Select a Service Type {:else if $page.url.pathname === `/services/${id}/configuration/version`}
{:else if $page.url.pathname === `/services/${id}/configuration/version`} Select a Service Version
Select a Service Version {:else if $page.url.pathname === `/services/${id}/configuration/destination`}
{:else if $page.url.pathname === `/services/${id}/configuration/destination`} Select a Destination
Select a Destination {:else}
Configurations
{/if}
</div>
{#if !$page.url.pathname.startsWith(`/services/${id}/configuration/`)}
<div
class="badge badge-lg rounded uppercase"
class:text-green-500={$status.service.overallStatus === 'healthy'}
class:text-yellow-400={$status.service.overallStatus === 'degraded'}
class:text-red-500={$status.service.overallStatus === 'stopped'}
>
{$status.service.overallStatus === 'healthy'
? 'Healthy'
: $status.service.overallStatus === 'degraded'
? 'Degraded'
: 'Stopped'}
</div>
{/if} {/if}
</div> </div>
</div> </div>
<ServiceLinks {service} /> {#if $page.url.pathname.startsWith(`/services/${id}/configuration/`)}
</div> <div class="px-2">
<div class="lg:block hidden flex-1" /> <button
<div class="flex flex-row flex-wrap space-x-3 justify-center lg:justify-start lg:py-0"> on:click={() => deleteService()}
{#if $location} disabled={!$appSession.isAdmin}
<a class:bg-red-600={$appSession.isAdmin}
id="open" class:hover:bg-red-500={$appSession.isAdmin}
href={$location} class="btn btn-sm btn-error text-sm"
target="_blank"
class="icons 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" /> Delete Service
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" /> </button>
<line x1="10" y1="14" x2="20" y2="4" /> </div>
<polyline points="15 4 20 4 20 9" />
</svg></a
>
<Tooltip triggeredBy="#open">Open</Tooltip>
<div class="hidden lg:block border border-coolgray-500 h-8" />
{/if}
{#if $status.service.isExited}
<a
id="error"
href={$isDeploymentEnabled ? `/services/${id}/logs` : null}
class="icons bg-transparent text-sm flex items-center text-red-500 tooltip-error"
sveltekit:prefetch
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentcolor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M8.7 3h6.6c.3 0 .5 .1 .7 .3l4.7 4.7c.2 .2 .3 .4 .3 .7v6.6c0 .3 -.1 .5 -.3 .7l-4.7 4.7c-.2 .2 -.4 .3 -.7 .3h-6.6c-.3 0 -.5 -.1 -.7 -.3l-4.7 -4.7c-.2 -.2 -.3 -.4 -.3 -.7v-6.6c0 -.3 .1 -.5 .3 -.7l4.7 -4.7c.2 -.2 .4 -.3 .7 -.3z"
/>
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
</a>
<Tooltip triggeredBy="#error">Service exited with an error!</Tooltip>
{/if} {/if}
</nav>
<div
class="pt-4 flex flex-row items-start justify-center lg:justify-end space-x-2 order-1 lg:order-2"
>
{#if $status.service.initialLoading} {#if $status.service.initialLoading}
<button <button class="btn btn-ghost btn-sm gap-2">
class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out"
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-6 w-6 animate-spin duration-500 ease-in-out"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@ -255,14 +248,34 @@
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" /> <line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
<line x1="11" y1="19.94" x2="11" y2="19.95" /> <line x1="11" y1="19.94" x2="11" y2="19.95" />
</svg> </svg>
Loading...
</button> </button>
{:else if $status.service.isRunning} {:else if $status.service.overallStatus === 'healthy'}
<button <button
id="stop"
on:click={stopService} on:click={stopService}
type="submit" type="submit"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2 text-red-500" class="btn btn-sm btn-error gap-2"
>
<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> Stop
</button>
<button
disabled={!$isDeploymentEnabled}
class="btn btn-sm gap-2"
on:click={() => startService()}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -275,21 +288,24 @@
stroke-linejoin="round" stroke-linejoin="round"
> >
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="6" y="5" width="4" height="14" rx="1" /> <path
<rect x="14" y="5" width="4" height="14" rx="1" /> d="M16.3 5h.7a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h5l-2.82 -2.82m0 5.64l2.82 -2.82"
transform="rotate(-45 12 12)"
/>
</svg> </svg>
Force Redeploy
</button> </button>
<Tooltip triggeredBy="#stop">Stop</Tooltip> {:else if $status.service.overallStatus === 'degraded'}
{:else}
<button <button
id="start" on:click={stopService}
on:click={startService}
type="submit" type="submit"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2 text-green-500" class="btn btn-sm btn-error gap-2"
><svg >
<svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="w-6 h-6 "
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@ -298,29 +314,21 @@
stroke-linejoin="round" stroke-linejoin="round"
> >
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" /> <rect x="6" y="5" width="4" height="14" rx="1" />
</svg> <rect x="14" y="5" width="4" height="14" rx="1" />
</svg> Stop
</button> </button>
<Tooltip triggeredBy="#start">Start</Tooltip> {:else if $status.service.overallStatus === 'stopped'}
{/if} <button
class="btn btn-sm gap-2"
{#if service.type && service.destinationDockerId && service.version} class:btn-primary={$status.application.overallStatus !== 'degraded'}
<div class="hidden lg:block border border-coolgray-500 h-8" /> disabled={!$isDeploymentEnabled}
<a on:click={() => startService()}
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 {#if $status.application.overallStatus !== 'degraded'}
id="configuration"
disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm"
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="w-6 h-6"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@ -329,27 +337,9 @@
stroke-linejoin="round" stroke-linejoin="round"
> >
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="4" y="8" width="4" height="4" /> <path d="M7 4v16l13 -8z" />
<line x1="6" y1="4" x2="6" y2="8" /> </svg>
<line x1="6" y1="12" x2="6" y2="20" /> {:else}
<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
>
<Tooltip triggeredBy="#configuration">Configuration</Tooltip>
<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 id="secrets" disabled={!$isDeploymentEnabled} class="icons bg-transparent text-sm ">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="w-6 h-6"
@ -362,88 +352,31 @@
> >
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path <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" d="M16.3 5h.7a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h5l-2.82 -2.82m0 5.64l2.82 -2.82"
transform="rotate(-45 12 12)"
/> />
<circle cx="12" cy="11" r="1" />
<line x1="12" y1="12" x2="12" y2="14.5" />
</svg></button
></a
>
<Tooltip triggeredBy="#secrets">Secrets</Tooltip>
<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
id="persistentstorage"
disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm"
>
<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> </svg>
</button></a {/if}
> {$status.application.overallStatus === 'degraded'
<Tooltip triggeredBy="#persistentstorage">Persistent Storages</Tooltip> ? $status.application.statuses.length === 1
<div class="hidden lg:block border border-coolgray-500 h-8" /> ? 'Force Redeploy'
<a : 'Redeploy Stack'
href={$isDeploymentEnabled && $status.service.isRunning ? `/services/${id}/logs` : null} : 'Deploy'}
sveltekit:prefetch </button>
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
id="logs"
disabled={!$status.service.isRunning}
class="icons 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="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
>
<Tooltip triggeredBy="#logs">Logs</Tooltip>
{/if} {/if}
<div class="hidden lg:block border border-coolgray-500 h-8" />
<button
id="delete"
on:click={deleteService}
type="submit"
disabled={!$appSession.isAdmin}
class:hover:text-red-500={$appSession.isAdmin}
class="icons bg-transparent text-sm"><DeleteIcon /></button
>
<Tooltip triggeredBy="#delete" placement="left">Delete</Tooltip>
</div> </div>
</nav> </div>
<slot />
<div
class="mx-auto max-w-screen-2xl px-0 lg:px-2 grid grid-cols-1"
class:lg:grid-cols-4={!$page.url.pathname.startsWith(`/services/${id}/configuration/`)}
>
{#if !$page.url.pathname.startsWith(`/services/${id}/configuration/`)}
<nav class="header flex flex-col lg:pt-0 ">
<Menu {service} />
</nav>
{/if}
<div class="pt-0 col-span-0 lg:col-span-3 pb-24">
<slot />
</div>
</div>

View File

@ -81,7 +81,7 @@
{#each destinations as destination} {#each destinations as destination}
<div class="p-2"> <div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(destination.id)}> <form on:submit|preventDefault={() => handleSubmit(destination.id)}>
<button type="submit" class="box-selection hover:bg-sky-700 font-bold"> <button type="submit" class="box-selection hover:bg-primary font-bold">
<div class="font-bold text-xl text-center truncate">{destination.name}</div> <div class="font-bold text-xl text-center truncate">{destination.name}</div>
<div class="text-center truncate">{destination.network}</div> <div class="text-center truncate">{destination.network}</div>
</button> </button>

View File

@ -25,30 +25,29 @@
</script> </script>
<script lang="ts"> <script lang="ts">
export let types: any; export let services: any;
let search = ''; let search = '';
let filteredTypes = types; let filteredServices = services;
import { page } from '$app/stores'; import { page } from '$app/stores';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import ServiceIcons from '$lib/components/svg/services/ServiceIcons.svelte';
const { id } = $page.params; const { id } = $page.params;
const from = $page.url.searchParams.get('from'); const from = $page.url.searchParams.get('from');
async function handleSubmit(type: any) { async function handleSubmit(service: any) {
try { try {
await post(`/services/${id}/configuration/type`, { type }); await post(`/services/${id}/configuration/type`, { ...service });
return await goto(from || `/services/${id}`); return await goto(from || `/services/${id}`);
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} }
} }
function doSearch() { function doSearch() {
filteredTypes = types.filter( filteredServices = services.filter(
(type: any) => (type: any) =>
type.name.toLowerCase().includes(search.toLowerCase()) || type.name.toLowerCase().includes(search.toLowerCase()) ||
type.labels.some((label: string) => label.toLowerCase().includes(search.toLowerCase())) type.labels.some((label: string) => label.toLowerCase().includes(search.toLowerCase()))
@ -56,7 +55,7 @@
} }
function cleanupSearch() { function cleanupSearch() {
search = ''; search = '';
filteredTypes = types; filteredServices = services;
} }
</script> </script>
@ -89,16 +88,16 @@
</div> </div>
</div> </div>
<div class="container lg:mx-auto lg:pt-20 lg:p-0 px-8 pt-20"> <div class="container lg:mx-auto lg:pt-20 lg:p-0 px-8 pt-20">
<div class="flex flex-wrap justify-center gap-8"> <div class="flex flex-wrap justify-center gap-8">
{#each filteredTypes as type} {#each filteredServices as service}
<div class="p-2"> <div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(type.name)}> <form on:submit|preventDefault={() => handleSubmit(service)}>
<button type="submit" class="box-selection relative text-xl font-bold hover:bg-pink-600"> <button type="submit" class="box-selection relative text-xl font-bold hover:bg-primary">
<ServiceIcons type={type.name} /> <!-- <ServiceIcons type={service.name} /> -->
{type.fancyName} {service.displayName}
</button> </button>
</form> </form>
</div> </div>
{/each} {/each}
</div> </div>
</div> </div>

View File

@ -0,0 +1,66 @@
<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}`);
return {
props: {
application: stuff.application,
...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 { del, get, post } from '$lib/api';
import { t } from '$lib/translations';
import { appSession, status } from '$lib/store';
import { errorNotification } from '$lib/common';
import { goto } from '$app/navigation';
const { id } = $page.params;
let forceDelete = false;
async function deleteService() {
const sure = confirm($t('application.confirm_to_delete', { name: service.name }));
if (sure) {
$status.service.initialLoading = 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('/');
} catch (error) {
return errorNotification(error);
} finally {
$status.service.initialLoading = false;
}
}
}
</script>
<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">Danger Zone</div>
</div>
<button
id="forcedelete"
on:click={() => deleteService()}
type="submit"
disabled={!$appSession.isAdmin}
class:bg-red-600={$appSession.isAdmin}
class:hover:bg-red-500={$appSession.isAdmin}
class="btn btn-lg btn-error text-sm"
>
Delete Application
</button>
</div>

View File

@ -8,64 +8,410 @@
</script> </script>
<script lang="ts"> <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 service: any;
export let readOnly: any; export let readOnly: any;
export let settings: any; export let settings: any;
const { id } = $page.params; import cuid from 'cuid';
let loading = { import { onMount } from 'svelte';
usage: false
};
let usage = {
MemUsage: 0,
CPUPerc: 0,
NetIO: 0
};
let usageInterval: any;
async function getUsage() { import { browser } from '$app/env';
if (loading.usage) return; import { page } from '$app/stores';
if (!$status.service.isRunning) return;
loading.usage = true; import { get, post } from '$lib/api';
const data = await get(`/services/${id}/usage`); import { errorNotification, getDomain } from '$lib/common';
usage = data.usage; import { t } from '$lib/translations';
loading.usage = false; import {
appSession,
status,
setLocation,
addToast,
checkIfDeploymentEnabledServices,
isDeploymentEnabled
} 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 DocLink from '$lib/components/DocLink.svelte';
import Explainer from '$lib/components/Explainer.svelte';
const { id } = $page.params;
let serviceName: any = service.type && service.type[0].toUpperCase() + service.type.substring(1);
$: isDisabled =
!$appSession.isAdmin || $status.service.isRunning || $status.service.initialLoading;
let forceSave = false;
let loading = {
save: false,
verification: false,
cleanup: false
};
let dualCerts = service.dualCerts;
let nonWWWDomain = service.fqdn && getDomain(service.fqdn).replace(/^www\./, '');
let isNonWWWDomainOK = false;
let isWWWDomainOK = false;
async function isDNSValid(domain: any, isWWW: any) {
try {
await get(`/services/${id}/check?domain=${domain}`);
addToast({
message: 'DNS configuration is valid.',
type: 'success'
});
isWWW ? (isWWWDomainOK = true) : (isNonWWWDomainOK = true);
return true;
} catch (error) {
errorNotification(error);
isWWW ? (isWWWDomainOK = false) : (isNonWWWDomainOK = false);
return false;
}
} }
onDestroy(() => { async function handleSubmit() {
clearInterval(usageInterval); 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}`, { ...service });
setLocation(service);
forceSave = false;
$isDeploymentEnabled = checkIfDeploymentEnabledServices($appSession.isAdmin, service);
return addToast({
message: 'Configuration saved.',
type: 'success'
});
} catch (error) {
//@ts-ignore
if (error?.message.startsWith($t('application.dns_not_set_partial_error'))) {
forceSave = true;
if (dualCerts) {
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
} else {
const isWWW = getDomain(service.fqdn).includes('www.');
if (isWWW) {
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
} else {
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
}
}
}
return errorNotification(error);
} finally {
loading.save = false;
}
}
async function setEmailsToVerified() {
loading.verification = true;
try {
await post(`/services/${id}/${service.type}/activate`, { id: service.id });
return addToast({
message: t.get('services.all_email_verified'),
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading.verification = false;
}
}
async function migrateAppwriteDB() {
loading.verification = true;
try {
await post(`/services/${id}/${service.type}/migrate`, { id: service.id });
return addToast({
message: "Appwrite's database has been migrated.",
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading.verification = false;
}
}
async function changeSettings(name: any) {
try {
if (name === 'dualCerts') {
dualCerts = !dualCerts;
}
await post(`/services/${id}/settings`, { dualCerts });
return addToast({
message: t.get('application.settings_saved'),
type: 'success'
});
} catch (error) {
return errorNotification(error);
}
}
async function cleanupLogs() {
loading.cleanup = true;
try {
await post(`/services/${id}/${service.type}/cleanup`, { id: service.id });
return addToast({
message: 'Cleared DB Logs',
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading.cleanup = false;
}
}
function doNothing() {
return;
}
onMount(async () => { onMount(async () => {
await getUsage(); if (browser && window.location.hostname === 'demo.coolify.io' && !service.fqdn) {
usageInterval = setInterval(async () => { service.fqdn = `http://${cuid()}.demo.coolify.io`;
await getUsage(); if (service.type === 'wordpress') {
}, 1000); 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> </script>
<div class="mx-auto max-w-6xl px-6 lg:my-0 my-4 lg:pt-0 pt-4 rounded"> <div class="w-full">
<div class="text-center"> <form on:submit|preventDefault={() => handleSubmit()}>
<div class="stat w-64"> <div class="mx-auto w-full">
<div class="stat-title">Used Memory / Memory Limit</div> <div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="stat-value text-xl">{usage?.MemUsage}</div> <div class="title font-bold pb-3 ">General</div>
{#if $appSession.isAdmin}
<button
type="submit"
class="btn btn-sm"
class:bg-orange-600={forceSave}
class:hover:bg-orange-400={forceSave}
class:loading={loading.save}
class:btn-primary={!loading.save}
disabled={loading.save}
>{loading.save
? $t('forms.save')
: forceSave
? $t('forms.confirm_continue')
: $t('forms.save')}</button
>
{/if}
{#if service.type === 'plausibleanalytics' && $status.service.isRunning}
<div class="btn-group">
<button
class="btn btn-sm"
on:click|preventDefault={setEmailsToVerified}
disabled={loading.verification}
class:loading={loading.verification}
>{loading.verification
? $t('forms.verifying')
: $t('forms.verify_emails_without_smtp')}</button
>
<button
class="btn btn-sm"
on:click|preventDefault={cleanupLogs}
disabled={loading.cleanup}
class:loading={loading.cleanup}>Cleanup Unnecessary Database Logs</button
>
</div>
{/if}
{#if service.type === 'appwrite' && $status.service.isRunning}
<button
class="btn btn-sm"
on:click|preventDefault={migrateAppwriteDB}
disabled={loading.verification}
class:loading={loading.verification}
>{loading.verification
? 'Migrating... it may take a while...'
: "Migrate Appwrite's Database"}</button
>
<DocLink url="https://appwrite.io/docs/upgrade#run-the-migration" />
{/if}
</div>
</div> </div>
<div class="stat w-64"> {#if service.type === 'minio' && !service.minio.apiFqdn && $status.service.isRunning}
<div class="stat-title">Used CPU</div> <div class="py-5">
<div class="stat-value text-xl">{usage?.CPUPerc}</div> <span class="font-bold text-red-500">IMPORTANT!</span> There was a small modification with Minio
</div> 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="stat w-64"> <div class="grid grid-flow-row gap-2 px-4">
<div class="stat-title">Network IO</div> <div class="mt-2 grid grid-cols-2 items-center">
<div class="stat-value text-xl">{usage?.NetIO}</div> <label for="name">{$t('forms.name')}</label>
<input name="name" id="name" class="w-full" bind:value={service.name} required />
</div>
<div class="grid grid-cols-2 items-center">
<label for="version">Version / Tag</label>
<a
href={$appSession.isAdmin && !$status.service.isRunning && !$status.service.initialLoading
? `/services/${id}/configuration/version?from=/services/${id}`
: ''}
class="no-underline"
>
<input
class="w-full"
value={service.version}
id="service"
readonly
disabled={$status.service.isRunning || $status.service.initialLoading}
class:cursor-pointer={!$status.service.isRunning}
/></a
>
</div>
<div class="grid grid-cols-2 items-center">
<label for="destination">{$t('application.destination')}</label>
<div>
{#if service.destinationDockerId}
<div class="no-underline">
<input
value={service.destinationDocker.name}
id="destination"
disabled
class="bg-transparent w-full"
/>
</div>
{/if}
</div>
</div>
{#if service.type === 'minio'}
<div class="grid grid-cols-2 items-center">
<label for="fqdn"
>Console URL <Explainer explanation={$t('application.https_explainer')} /></label
>
<CopyPasswordField
placeholder="eg: https://console.min.io"
readonly={isDisabled}
disabled={isDisabled}
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 items-center">
<label for="apiFqdn"
>API URL <Explainer explanation={$t('application.https_explainer')} /></label
>
<CopyPasswordField
placeholder="eg: https://min.io"
readonly={!$appSession.isAdmin && !$status.service.isRunning}
disabled={isDisabled}
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 items-center">
<label for="fqdn"
>{$t('application.url_fqdn')}
<Explainer explanation={$t('application.https_explainer')} />
</label>
<CopyPasswordField
placeholder="eg: https://analytics.coollabs.io"
readonly={!$appSession.isAdmin && !$status.service.isRunning}
disabled={!$appSession.isAdmin ||
$status.service.isRunning ||
$status.service.initialLoading}
name="fqdn"
id="fqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
bind:value={service.fqdn}
required
/>
</div>
{/if}
</div> </div>
</div> {#if forceSave}
<div class="flex-col space-y-2 pt-4 text-center">
{#if isNonWWWDomainOK}
<button
class="btn btn-sm bg-green-600 hover:bg-green-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="btn btn-sm bg-red-600 hover:bg-red-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{#if dualCerts}
{#if isWWWDomainOK}
<button
class="btn btn-sm bg-green-600 hover:bg-green-500"
on:click|preventDefault={() => isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="btn btn-sm bg-red-600 hover:bg-red-500"
on:click|preventDefault={() => isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{/if}
</div>
{/if}
<div class="grid grid-flow-row gap-2 px-4">
<div class="grid grid-cols-2 items-center">
<Setting
id="dualCerts"
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">
<label for="exposePort"
>Exposed Port <Explainer
explanation={'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.'}
/></label
>
<input
class="w-full"
readonly={!$appSession.isAdmin && !$status.service.isRunning}
disabled={!$appSession.isAdmin ||
$status.service.isRunning ||
$status.service.initialLoading}
name="exposePort"
id="exposePort"
bind:value={service.exposePort}
placeholder="12345"
/>
</div>
</div>
<svelte:component this={Services[serviceName]} bind:service {readOnly} {settings} />
</form>
</div> </div>
<Services bind:service bind:readOnly bind:settings />

View File

@ -8,6 +8,7 @@
import Tooltip from '$lib/components/Tooltip.svelte'; import Tooltip from '$lib/components/Tooltip.svelte';
let service: any = {}; let service: any = {};
let template: any = null;
let logsLoading = false; let logsLoading = false;
let loadLogsInterval: any = null; let loadLogsInterval: any = null;
let logs: any = []; let logs: any = [];
@ -16,11 +17,12 @@
let followingLogs: any; let followingLogs: any;
let logsEl: any; let logsEl: any;
let position = 0; let position = 0;
let selectedService: any = null;
const { id } = $page.params; const { id } = $page.params;
onMount(async () => { onMount(async () => {
const response = await get(`/services/${id}`); const response = await get(`/services/${id}`);
template = response.template;
service = response.service; service = response.service;
loadAllLogs(); loadAllLogs();
loadLogsInterval = setInterval(() => { loadLogsInterval = setInterval(() => {
@ -82,48 +84,83 @@
clearInterval(followingInterval); clearInterval(followingInterval);
} }
} }
async function selectService(service: any, init: boolean = false) {
if (loadLogsInterval) clearInterval(loadLogsInterval);
if (followingInterval) clearInterval(followingInterval);
logs = [];
lastLog = null;
followingLogs = false;
selectedService = service;
loadLogs();
loadLogsInterval = setInterval(() => {
loadLogs();
}, 1000);
}
</script> </script>
<div class="flex flex-row justify-center space-x-2 px-10 pt-6"> {#if template}
{#if logs.length === 0} <div class="flex gap-2 lg:gap-8 pb-4">
<div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div> {#each Object.keys(template.services) as service}
{:else} <button
<div class="relative w-full"> on:click={() => selectService(service, true)}
<div class="text-right " /> class:bg-primary={selectedService === service}
{#if loadLogsInterval} class:bg-coolgray-200={selectedService !== service}
<LoadingLogs /> class="w-full rounded p-5 hover:bg-primary font-bold"
{/if} >
<div class="flex justify-end sticky top-0 p-1 mx-1"> {service}</button
<button >
id="follow" {/each}
on:click={followBuild} </div>
class="bg-transparent btn btn-sm btn-link" {/if}
class:text-green-500={followingLogs} {#if selectedService}
> <div class="flex flex-row justify-center space-x-2">
<svg {#if logs.length === 0}
xmlns="http://www.w3.org/2000/svg" <div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div>
class="w-6 h-6" {:else}
viewBox="0 0 24 24" <div class="relative w-full">
stroke-width="1.5" <div class="flex justify-start sticky space-x-2 pb-2">
stroke="currentColor" <button
fill="none" on:click={followBuild}
stroke-linecap="round" class="btn btn-sm bg-coollabs"
stroke-linejoin="round" class:bg-coolgray-300={followingLogs}
class:text-applications={followingLogs}
> >
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <svg
<circle cx="12" cy="12" r="9" /> xmlns="http://www.w3.org/2000/svg"
<line x1="8" y1="12" x2="12" y2="16" /> class="w-6 h-6 mr-2"
<line x1="12" y1="8" x2="12" y2="16" /> viewBox="0 0 24 24"
<line x1="16" y1="12" x2="12" y2="16" /> stroke-width="1.5"
</svg> stroke="currentColor"
</button> fill="none"
<Tooltip triggeredBy="#follow">Follow Logs</Tooltip> 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>
{followingLogs ? 'Following Logs...' : 'Follow Logs'}
</button>
{#if loadLogsInterval}
<button id="streaming" class="btn btn-sm bg-transparent border-none loading"
>Streaming logs</button
>
{/if}
</div>
<div
bind:this={logsEl}
on:scroll={detect}
class="font-mono w-full bg-coolgray-100 border border-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded mb-20 flex flex-col scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1"
>
{#each logs as log}
<p>{log + '\n'}</p>
{/each}
</div>
</div> </div>
<div class="font-mono w-full bg-coolgray-200 p-5 overflow-x-auto overflox-y-auto rounded mb-20 flex flex-col -mt-12 scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1"> {/if}
{#each logs as log} </div>
<p>{log + '\n'}</p> {/if}
{/each}
</div>
</div>
{/if}
</div>

View File

@ -26,7 +26,6 @@
import { get } from '$lib/api'; import { get } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import pLimit from 'p-limit'; import pLimit from 'p-limit';
import ServiceLinks from './_ServiceLinks.svelte';
import { addToast } from '$lib/store'; import { addToast } from '$lib/store';
import { saveSecret } from './utils'; import { saveSecret } from './utils';
const limit = pLimit(1); const limit = pLimit(1);
@ -38,8 +37,8 @@
const data = await get(`/services/${id}/secrets`); const data = await get(`/services/${id}/secrets`);
secrets = [...data.secrets]; secrets = [...data.secrets];
} }
async function getValues(e: any) { async function getValues() {
e.preventDefault(); if (!batchSecrets) return;
const eachValuePair = batchSecrets.split('\n'); const eachValuePair = batchSecrets.split('\n');
const batchSecretsPairs = eachValuePair const batchSecretsPairs = eachValuePair
.filter((secret) => !secret.startsWith('#') && secret) .filter((secret) => !secret.startsWith('#') && secret)
@ -48,8 +47,8 @@
const value = rest.join('='); const value = rest.join('=');
const cleanValue = value?.replaceAll('"', '') || ''; const cleanValue = value?.replaceAll('"', '') || '';
return { return {
name, name: name.trim(),
value: cleanValue, value: cleanValue.trim(),
isNew: !secrets.find((secret: any) => name === secret.name) isNew: !secrets.find((secret: any) => name === secret.name)
}; };
}); });
@ -68,17 +67,20 @@
} }
</script> </script>
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4"> <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">Secrets</div>
</div>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="w-full border-separate text-left"> <table class="w-full border-separate text-left">
<thead> <thead>
<tr class="h-12"> <tr class="uppercase">
<th scope="col">{$t('forms.name')}</th> <th scope="col">{$t('forms.name')}</th>
<th scope="col">{$t('forms.value')}</th> <th scope="col uppercase">{$t('forms.value')}</th>
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th> <th scope="col uppercase" class="w-96 text-center">{$t('forms.action')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody class="space-y-2">
{#each secrets as secret} {#each secrets as secret}
{#key secret.id} {#key secret.id}
<tr> <tr>
@ -92,9 +94,18 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<h2 class="title my-6 font-bold">Paste .env file</h2>
<form on:submit|preventDefault={getValues} class="mb-12 w-full"> <form on:submit|preventDefault={getValues} class="mb-12 w-full">
<textarea bind:value={batchSecrets} class="mb-2 min-h-[200px] w-full" /> <div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2 pt-10">
<button class="btn btn-sm bg-services" type="submit">Batch add secrets</button> <div class="flex flex-row space-x-2">
<div class="title font-bold pb-3 ">Paste <code>.env</code> file</div>
<button type="submit" class="btn btn-sm bg-primary">Add Secrets in Batch</button>
</div>
</div>
<textarea
placeholder={`PORT=1337\nPASSWORD=supersecret`}
bind:value={batchSecrets}
class="mb-2 min-h-[200px] w-full"
/>
</form> </form>
</div> </div>

View File

@ -5,7 +5,6 @@
const response = await get(`/services/${params.id}/storages`); const response = await get(`/services/${params.id}/storages`);
return { return {
props: { props: {
service: stuff.service,
...response ...response
} }
}; };
@ -19,14 +18,12 @@
</script> </script>
<script lang="ts"> <script lang="ts">
export let service: any;
export let persistentStorages: any; export let persistentStorages: any;
import { page } from '$app/stores'; import { page } from '$app/stores';
import Storage from './_Storage.svelte'; import Storage from './_Storage.svelte';
import { get } from '$lib/api'; import { get } from '$lib/api';
import SimpleExplainer from '$lib/components/SimpleExplainer.svelte'; import { t } from '$lib/translations';
import ServiceLinks from './_ServiceLinks.svelte'; import Explainer from '$lib/components/Explainer.svelte';
const { id } = $page.params; const { id } = $page.params;
async function refreshStorage() { async function refreshStorage() {
@ -35,30 +32,22 @@
} }
</script> </script>
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4"> <div class="w-full">
<div class="flex justify-center py-4 text-center"> <div class="mx-auto w-full">
<SimpleExplainer <div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
customClass="w-full" <div class="title font-bold pb-3">
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.'} Persistent Volumes <Explainer
/> position="dropdown-bottom"
explanation={$t('application.storage.persistent_storage_explainer')}
/>
</div>
</div>
<label for="name" class="pb-2 uppercase font-bold">name</label>
{#each persistentStorages as storage}
{#key storage.id}
<Storage on:refresh={refreshStorage} {storage} />
{/key}
{/each}
<Storage on:refresh={refreshStorage} isNew />
</div> </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> </div>