tons of updates
This commit is contained in:
parent
79c30dfc91
commit
462eea90c0
@ -111,13 +111,11 @@ ### Custom logo
|
||||
|
||||
> SVG is recommended, but you can use PNG as well. It should have the `isAbsolute` variable with the suitable CSS classes, primarily for sizing and positioning.
|
||||
|
||||
- At [here](apps/ui/src/lib/components/svg/services/ServiceIcons.svelte) include the new logo with `isAbsolute` property.
|
||||
|
||||
- At [here](apps/ui/src/routes/services/[id]/_ServiceLinks.svelte) add links to the documentation of the service.
|
||||
|
||||
### Custom fields on the UI
|
||||
By default the URL and name are shown on the UI. Everything else needs to be added [here](apps/ui/src/routes/services/[id]/_Services/_Services.svelte)
|
||||
By default the URL and name are shown on the UI. Everything else needs to be added [here](apps/ui/src/lib/components/Services)
|
||||
|
||||
> If you need to show more details on the frontend, such as users/passwords, you need to add Svelte component [here](apps/ui/src/routes/services/[id]/_Services) with an underscore. For example, see other [here](apps/ui/src/routes/services/[id]/_Services/_Umami.svelte).
|
||||
> If you need to show more details on the frontend, such as users/passwords, you need to add Svelte component [here](apps/ui/src/lib/components/Services). For example, see other [here](apps/ui/src/lib/components/Services/Umami.svelte).
|
||||
|
||||
Good job! 👏
|
@ -198,7 +198,7 @@ export const encrypt = (text: string) => {
|
||||
if (text) {
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv(algorithm, process.env['COOLIFY_SECRET_KEY'], iv);
|
||||
const encrypted = Buffer.concat([cipher.update(text), cipher.final()]);
|
||||
const encrypted = Buffer.concat([cipher.update(text.trim()), cipher.final()]);
|
||||
return JSON.stringify({
|
||||
iv: iv.toString('hex'),
|
||||
content: encrypted.toString('hex')
|
||||
@ -1681,11 +1681,13 @@ export function persistentVolumes(id, persistentStorage, config) {
|
||||
for (const [key, value] of Object.entries(config)) {
|
||||
if (value.volumes) {
|
||||
for (const volume of value.volumes) {
|
||||
if (!volume.startsWith('/var/run/docker.sock')) {
|
||||
volumeSet.add(volume);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const volumesArray = Array.from(volumeSet);
|
||||
const persistentVolume =
|
||||
persistentStorage?.map((storage) => {
|
||||
|
@ -6,82 +6,83 @@ import { ServiceStartStop } from '../../routes/api/v1/services/types';
|
||||
import { asyncSleep, ComposeFile, createDirectories, defaultComposeConfiguration, errorHandler, executeDockerCmd, getDomain, getFreePublicPort, getServiceFromDB, getServiceImage, getServiceMainPort, isARM, isDev, makeLabelForServices, persistentVolumes, prisma } from '../common';
|
||||
import { defaultServiceConfigurations } from '../services';
|
||||
import { OnlyId } from '../../types';
|
||||
import templates from '../templates'
|
||||
|
||||
export async function startService(request: FastifyRequest<ServiceStartStop>) {
|
||||
try {
|
||||
const { type } = request.params
|
||||
if (type === 'plausibleanalytics') {
|
||||
return await startPlausibleAnalyticsService(request)
|
||||
}
|
||||
if (type === 'nocodb') {
|
||||
return await startNocodbService(request)
|
||||
}
|
||||
if (type === 'minio') {
|
||||
return await startMinioService(request)
|
||||
}
|
||||
if (type === 'vscodeserver') {
|
||||
return await startVscodeService(request)
|
||||
}
|
||||
if (type === 'wordpress') {
|
||||
return await startWordpressService(request)
|
||||
}
|
||||
if (type === 'vaultwarden') {
|
||||
return await startVaultwardenService(request)
|
||||
}
|
||||
if (type === 'languagetool') {
|
||||
return await startLanguageToolService(request)
|
||||
}
|
||||
if (type === 'n8n') {
|
||||
return await startN8nService(request)
|
||||
}
|
||||
if (type === 'uptimekuma') {
|
||||
return await startUptimekumaService(request)
|
||||
}
|
||||
if (type === 'ghost') {
|
||||
return await startGhostService(request)
|
||||
}
|
||||
if (type === 'meilisearch') {
|
||||
return await startMeilisearchService(request)
|
||||
}
|
||||
if (type === 'umami') {
|
||||
return await startUmamiService(request)
|
||||
}
|
||||
if (type === 'hasura') {
|
||||
return await startHasuraService(request)
|
||||
}
|
||||
if (type === 'fider') {
|
||||
return await startFiderService(request)
|
||||
}
|
||||
if (type === 'moodle') {
|
||||
return await startMoodleService(request)
|
||||
}
|
||||
if (type === 'appwrite') {
|
||||
return await startAppWriteService(request)
|
||||
}
|
||||
if (type === 'glitchTip') {
|
||||
return await startGlitchTipService(request)
|
||||
}
|
||||
if (type === 'searxng') {
|
||||
return await startSearXNGService(request)
|
||||
}
|
||||
if (type === 'weblate') {
|
||||
return await startWeblateService(request)
|
||||
}
|
||||
if (type === 'taiga') {
|
||||
return await startTaigaService(request)
|
||||
}
|
||||
if (type === 'grafana') {
|
||||
return await startGrafanaService(request)
|
||||
}
|
||||
if (type === 'trilium') {
|
||||
return await startTriliumService(request)
|
||||
}
|
||||
// export async function startService(request: FastifyRequest<ServiceStartStop>) {
|
||||
// try {
|
||||
// const { type } = request.params
|
||||
// if (type === 'plausibleanalytics') {
|
||||
// return await startPlausibleAnalyticsService(request)
|
||||
// }
|
||||
// if (type === 'nocodb') {
|
||||
// return await startNocodbService(request)
|
||||
// }
|
||||
// if (type === 'minio') {
|
||||
// return await startMinioService(request)
|
||||
// }
|
||||
// if (type === 'vscodeserver') {
|
||||
// return await startVscodeService(request)
|
||||
// }
|
||||
// if (type === 'wordpress') {
|
||||
// return await startWordpressService(request)
|
||||
// }
|
||||
// if (type === 'vaultwarden') {
|
||||
// return await startVaultwardenService(request)
|
||||
// }
|
||||
// if (type === 'languagetool') {
|
||||
// return await startLanguageToolService(request)
|
||||
// }
|
||||
// if (type === 'n8n') {
|
||||
// return await startN8nService(request)
|
||||
// }
|
||||
// if (type === 'uptimekuma') {
|
||||
// return await startUptimekumaService(request)
|
||||
// }
|
||||
// if (type === 'ghost') {
|
||||
// return await startGhostService(request)
|
||||
// }
|
||||
// if (type === 'meilisearch') {
|
||||
// return await startMeilisearchService(request)
|
||||
// }
|
||||
// if (type === 'umami') {
|
||||
// return await startUmamiService(request)
|
||||
// }
|
||||
// if (type === 'hasura') {
|
||||
// return await startHasuraService(request)
|
||||
// }
|
||||
// if (type === 'fider') {
|
||||
// return await startFiderService(request)
|
||||
// }
|
||||
// if (type === 'moodle') {
|
||||
// return await startMoodleService(request)
|
||||
// }
|
||||
// if (type === 'appwrite') {
|
||||
// return await startAppWriteService(request)
|
||||
// }
|
||||
// if (type === 'glitchTip') {
|
||||
// return await startGlitchTipService(request)
|
||||
// }
|
||||
// if (type === 'searxng') {
|
||||
// return await startSearXNGService(request)
|
||||
// }
|
||||
// if (type === 'weblate') {
|
||||
// return await startWeblateService(request)
|
||||
// }
|
||||
// if (type === 'taiga') {
|
||||
// return await startTaigaService(request)
|
||||
// }
|
||||
// if (type === 'grafana') {
|
||||
// return await startGrafanaService(request)
|
||||
// }
|
||||
// if (type === 'trilium') {
|
||||
// return await startTriliumService(request)
|
||||
// }
|
||||
|
||||
throw `Service type ${type} not supported.`
|
||||
} catch (error) {
|
||||
throw { status: 500, message: error?.message || error }
|
||||
}
|
||||
}
|
||||
// throw `Service type ${type} not supported.`
|
||||
// } catch (error) {
|
||||
// throw { status: 500, message: error?.message || error }
|
||||
// }
|
||||
// }
|
||||
export async function stopService(request: FastifyRequest<ServiceStartStop>) {
|
||||
try {
|
||||
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 {
|
||||
const { id } = request.params;
|
||||
const teamId = request.user.teamId;
|
||||
|
||||
const service = await getServiceFromDB({ id, teamId });
|
||||
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } =
|
||||
service;
|
||||
|
||||
let template = templates.find((template) => template.name === type);
|
||||
|
||||
template = JSON.parse(JSON.stringify(template).replaceAll('$$id', id).replaceAll('$$fqdn', service.fqdn))
|
||||
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const port = getServiceMainPort('n8n');
|
||||
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
const image = getServiceImage(type);
|
||||
|
||||
const config = {
|
||||
n8n: {
|
||||
image: `${image}:${version}`,
|
||||
volumes: [`${id}-n8n:/root/.n8n`],
|
||||
environmentVariables: {
|
||||
WEBHOOK_URL: `${service.fqdn}`
|
||||
}
|
||||
}
|
||||
};
|
||||
if (serviceSecret.length > 0) {
|
||||
serviceSecret.forEach((secret) => {
|
||||
config.n8n.environmentVariables[secret.name] = secret.value;
|
||||
});
|
||||
}
|
||||
const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
|
||||
const composeFile: ComposeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[id]: {
|
||||
const config = {};
|
||||
for (const service in template.services) {
|
||||
config[service] = {
|
||||
container_name: id,
|
||||
image: config.n8n.image,
|
||||
volumes: config.n8n.volumes,
|
||||
environment: config.n8n.environmentVariables,
|
||||
labels: makeLabelForServices('n8n'),
|
||||
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
|
||||
image: template.services[service].image.replace('$$core_version', version),
|
||||
expose: template.services[service].ports,
|
||||
// ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
|
||||
volumes: template.services[service].volumes,
|
||||
environment: {},
|
||||
depends_on: template.services[service].depends_on,
|
||||
ulimits: template.services[service].ulimits,
|
||||
labels: makeLabelForServices(type),
|
||||
...defaultComposeConfiguration(network),
|
||||
}
|
||||
},
|
||||
if (serviceSecret.length > 0) {
|
||||
serviceSecret.forEach((secret) => {
|
||||
config[service].environment[secret.name] = secret.value;
|
||||
});
|
||||
}
|
||||
}
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
|
||||
|
||||
const composeFile: ComposeFile = {
|
||||
version: '3.8',
|
||||
services: config,
|
||||
networks: {
|
||||
[network]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
volumes: volumeMounts
|
||||
};
|
||||
}
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
await startServiceContainers(destinationDocker.id, composeFileDestination)
|
||||
|
175
apps/api/src/lib/templates.ts
Normal file
175
apps/api/src/lib/templates.ts
Normal 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^\/])+$/
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
@ -5,6 +5,7 @@ import { prisma, uniqueName, asyncExecShell, getServiceFromDB, getContainerUsage
|
||||
import { day } from '../../../../lib/dayjs';
|
||||
import { checkContainer, isContainerExited } from '../../../../lib/docker';
|
||||
import cuid from 'cuid';
|
||||
import templates from '../../../../lib/templates';
|
||||
|
||||
import type { OnlyId } from '../../../../types';
|
||||
import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetGlitchTipSettings, SetWordpressSettings } from './types';
|
||||
@ -73,25 +74,53 @@ export async function getServiceStatus(request: FastifyRequest<OnlyId>) {
|
||||
let isRestarting = false;
|
||||
const service = await getServiceFromDB({ id, teamId });
|
||||
const { destinationDockerId, settings } = service;
|
||||
|
||||
let payload = {}
|
||||
if (destinationDockerId) {
|
||||
const status = await checkContainer({ dockerId: service.destinationDocker.id, container: id });
|
||||
if (status?.found) {
|
||||
isRunning = status.status.isRunning;
|
||||
isExited = status.status.isExited;
|
||||
isRestarting = status.status.isRestarting
|
||||
const { stdout: containers } = await executeDockerCmd({
|
||||
dockerId: service.destinationDocker.id,
|
||||
command:
|
||||
`docker ps -a --filter "label=com.docker.compose.project=${id}" --format '{{json .}}'`
|
||||
});
|
||||
const containersArray = containers.trim().split('\n');
|
||||
if (containersArray.length > 0 && containersArray[0] !== '') {
|
||||
for (const container of containersArray) {
|
||||
let isRunning = false;
|
||||
let isExited = false;
|
||||
let isRestarting = false;
|
||||
const containerObj = JSON.parse(container);
|
||||
const status = containerObj.State
|
||||
if (status === 'running') {
|
||||
isRunning = true;
|
||||
}
|
||||
if (status === 'exited') {
|
||||
isExited = true;
|
||||
}
|
||||
return {
|
||||
if (status === 'restarting') {
|
||||
isRestarting = true;
|
||||
}
|
||||
payload[containerObj.Names] = {
|
||||
status: {
|
||||
isRunning,
|
||||
isExited,
|
||||
settings
|
||||
isRestarting
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return payload
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
function parseAndFindServiceTemplates(service: any) {
|
||||
const foundTemplate = templates.find(t => t.name === service.type)
|
||||
if (foundTemplate) {
|
||||
return JSON.parse(JSON.stringify(foundTemplate).replaceAll('$$id', service.id).replaceAll('$$fqdn', service.fqdn))
|
||||
}
|
||||
|
||||
}
|
||||
export async function getService(request: FastifyRequest<OnlyId>) {
|
||||
try {
|
||||
const teamId = request.user.teamId;
|
||||
@ -102,7 +131,8 @@ export async function getService(request: FastifyRequest<OnlyId>) {
|
||||
}
|
||||
return {
|
||||
settings: await listSettings(),
|
||||
service
|
||||
service,
|
||||
template: parseAndFindServiceTemplates(service)
|
||||
}
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
@ -111,7 +141,7 @@ export async function getService(request: FastifyRequest<OnlyId>) {
|
||||
export async function getServiceType(request: FastifyRequest) {
|
||||
try {
|
||||
return {
|
||||
types: supportedServiceTypesAndVersions
|
||||
services: templates
|
||||
}
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
@ -120,8 +150,21 @@ export async function getServiceType(request: FastifyRequest) {
|
||||
export async function saveServiceType(request: FastifyRequest<SaveServiceType>, reply: FastifyReply) {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
const { type } = request.body;
|
||||
await configureServiceType({ id, type });
|
||||
const { name, variables = [], serviceDefaultVersion = 'latest' } = request.body;
|
||||
if (variables.length > 0) {
|
||||
for (const variable of variables) {
|
||||
const { id: variableId, defaultValue, value = null } = variable;
|
||||
if (variableId.startsWith('$$secret_')) {
|
||||
const secretName = variableId.replace('$$secret_', '');
|
||||
let secretValue = defaultValue || value || null;
|
||||
if (secretValue) secretValue = encrypt(secretValue);
|
||||
await prisma.serviceSecret.create({
|
||||
data: { name: secretName, value: secretValue, service: { connect: { id } } }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
await prisma.service.update({ where: { id }, data: { type: name, version: serviceDefaultVersion } })
|
||||
return reply.code(201).send()
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
|
@ -38,6 +38,8 @@
|
||||
class={disabledClass}
|
||||
class:pr-10={true}
|
||||
class:pr-20={value && isHttps}
|
||||
class:border={required && !value}
|
||||
class:border-red-500={required && !value}
|
||||
{placeholder}
|
||||
type="text"
|
||||
{id}
|
||||
@ -54,6 +56,8 @@
|
||||
type="text"
|
||||
class:pr-10={true}
|
||||
class:pr-20={value && isHttps}
|
||||
class:border={required && !value}
|
||||
class:border-red-500={required && !value}
|
||||
{id}
|
||||
{name}
|
||||
{required}
|
||||
@ -70,6 +74,8 @@
|
||||
class={disabledClass}
|
||||
class:pr-10={true}
|
||||
class:pr-20={value && isHttps}
|
||||
class:border={required && !value}
|
||||
class:border-red-500={required && !value}
|
||||
type="password"
|
||||
{id}
|
||||
{name}
|
||||
|
@ -1,6 +1,9 @@
|
||||
<script lang="ts">
|
||||
import ExternalLink from './ExternalLink.svelte';
|
||||
import Tooltip from './Tooltip.svelte';
|
||||
export let url = 'https://docs.coollabs.io';
|
||||
export let text: any = '';
|
||||
export let isExternal = false;
|
||||
let id =
|
||||
'cool-' +
|
||||
url
|
||||
@ -10,10 +13,32 @@
|
||||
.slice(-16);
|
||||
</script>
|
||||
|
||||
<a {id} href={url} target="_blank" class="icons inline-block cursor-pointer text-xs mx-2">
|
||||
<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" />
|
||||
<a
|
||||
{id}
|
||||
href={url}
|
||||
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>
|
||||
{#if !text}
|
||||
<Tooltip triggeredBy={`#${id}`}>See details in the documentation</Tooltip>
|
||||
{/if}
|
||||
|
10
apps/ui/src/lib/components/ExternalLink.svelte
Normal file
10
apps/ui/src/lib/components/ExternalLink.svelte
Normal 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 |
31
apps/ui/src/lib/components/ServiceStatus.svelte
Normal file
31
apps/ui/src/lib/components/ServiceStatus.svelte
Normal 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}
|
@ -6,12 +6,12 @@
|
||||
export let readOnly: any;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Appwrite</div>
|
||||
<div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
|
||||
<div class="title font-bold pb-3">Appwrite</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
||||
<div class="space-y-2 px-4">
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="opensslKeyV1">Encryption Key</label>
|
||||
<CopyPasswordField
|
||||
name="opensslKeyV1"
|
||||
@ -22,7 +22,7 @@
|
||||
disabled
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
name="executorSecret"
|
||||
@ -34,11 +34,11 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">MariaDB</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">
|
||||
<div class="space-y-2 px-4">
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="mariadbUser">{$t('forms.username')}</label>
|
||||
<CopyPasswordField
|
||||
name="mariadbUser"
|
||||
@ -48,7 +48,7 @@
|
||||
disabled
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
id="mariadbPassword"
|
||||
@ -59,7 +59,7 @@
|
||||
value={service.appwrite.mariadbPassword}
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
name="mariadbRootUser"
|
||||
@ -69,7 +69,7 @@
|
||||
disabled
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
id="mariadbRootUserPassword"
|
||||
@ -80,7 +80,7 @@
|
||||
value={service.appwrite.mariadbRootUserPassword}
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
name="mariadbDatabase"
|
@ -17,12 +17,12 @@
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Fider</div>
|
||||
<div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
|
||||
<div class="title font-bold pb-3">Fider</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
||||
<div class="space-y-2 px-4">
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="jwtSecret">JWT Secret</label>
|
||||
<CopyPasswordField
|
||||
name="jwtSecret"
|
||||
@ -34,7 +34,7 @@
|
||||
/>
|
||||
</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>
|
||||
<input
|
||||
class="w-full"
|
||||
@ -49,11 +49,11 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Email</div>
|
||||
<div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
|
||||
<div class="title font-bold pb-3">Email</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
||||
<div class="space-y-2 px-4">
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="emailMailgunApiKey">Mailgun API Key</label>
|
||||
<CopyPasswordField
|
||||
name="emailMailgunApiKey"
|
||||
@ -66,7 +66,7 @@
|
||||
/>
|
||||
</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>
|
||||
<input
|
||||
class="w-full"
|
||||
@ -78,7 +78,7 @@
|
||||
placeholder="{$t('forms.eg')}: yourdomain.com"
|
||||
/>
|
||||
</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>
|
||||
<div class="custom-select-wrapper">
|
||||
<Select
|
||||
@ -92,10 +92,10 @@
|
||||
</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>
|
||||
<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>
|
||||
<input
|
||||
class="w-full"
|
||||
@ -107,7 +107,7 @@
|
||||
placeholder="{$t('forms.eg')}: smtp.yourdomain.com"
|
||||
/>
|
||||
</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>
|
||||
<input
|
||||
class="w-full"
|
||||
@ -119,7 +119,7 @@
|
||||
placeholder="{$t('forms.eg')}: 587"
|
||||
/>
|
||||
</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>
|
||||
<input
|
||||
class="w-full"
|
||||
@ -131,7 +131,7 @@
|
||||
placeholder="{$t('forms.eg')}: user@yourdomain.com"
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
name="emailSmtpPassword"
|
||||
@ -143,7 +143,7 @@
|
||||
placeholder="{$t('forms.eg')}: s0m3p4ssw0rd"
|
||||
/>
|
||||
</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>
|
||||
<input
|
||||
class="w-full"
|
||||
@ -156,11 +156,11 @@
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
||||
<div class="space-y-2 px-4">
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="postgresqlUser">{$t('forms.username')}</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlUser"
|
||||
@ -170,7 +170,7 @@
|
||||
disabled
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
id="postgresqlPassword"
|
||||
@ -181,7 +181,7 @@
|
||||
value={service.fider.postgresqlPassword}
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
name="postgresqlDatabase"
|
101
apps/ui/src/lib/components/Services/Ghost.svelte
Normal file
101
apps/ui/src/lib/components/Services/Ghost.svelte
Normal 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>
|
@ -49,11 +49,11 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">GlitchTip</div>
|
||||
<div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
|
||||
<div class="title font-bold pb-3">GlitchTip</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
id="enableOpenUserRegistration"
|
||||
bind:setting={service.glitchTip.enableOpenUserRegistration}
|
||||
@ -63,19 +63,13 @@
|
||||
title="Enable Open User Registration"
|
||||
description={''}
|
||||
/>
|
||||
<!-- <Setting
|
||||
bind:setting={service.glitchTip.enableOpenUserRegistration}
|
||||
on:click={toggleEnableOpenUserRegistration}
|
||||
title={'Enable Open User Registration'}
|
||||
description={''}
|
||||
/> -->
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1 py-2 font-bold">
|
||||
<div class="subtitle">Email settings</div>
|
||||
<div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
|
||||
<div class="title font-bold pb-3">Email Settings</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
||||
<div class="space-y-2 px-4">
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
id="emailSmtpUseTls"
|
||||
bind:setting={service.glitchTip.emailSmtpUseTls}
|
||||
@ -87,7 +81,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
id="emailSmtpUseSsl"
|
||||
bind:setting={service.glitchTip.emailSmtpUseSsl}
|
||||
@ -98,7 +92,7 @@
|
||||
description={''}
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
required
|
||||
@ -108,7 +102,7 @@
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
name="emailSmtpHost"
|
||||
@ -117,7 +111,7 @@
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
name="emailSmtpPort"
|
||||
@ -126,7 +120,7 @@
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
name="emailSmtpUser"
|
||||
@ -135,7 +129,7 @@
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
name="emailSmtpPassword"
|
||||
@ -145,7 +139,7 @@
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
name="emailBackend"
|
||||
@ -154,7 +148,7 @@
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
name="mailgunApiKey"
|
||||
@ -163,7 +157,7 @@
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
name="sendgridApiKey"
|
||||
@ -176,7 +170,7 @@
|
||||
<div class="subtitle">Default User & Superuser</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>
|
||||
<CopyPasswordField
|
||||
name="defaultEmail"
|
||||
@ -186,7 +180,7 @@
|
||||
disabled
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
name="defaultUsername"
|
||||
@ -196,7 +190,7 @@
|
||||
disabled
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
name="defaultPassword"
|
||||
@ -208,11 +202,11 @@
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
||||
<div class="space-y-2 px-4">
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="postgresqlUser">{$t('forms.username')}</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlUser"
|
||||
@ -222,7 +216,7 @@
|
||||
disabled
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
id="postgresqlPassword"
|
||||
@ -233,7 +227,7 @@
|
||||
bind:value={service.glitchTip.postgresqlPassword}
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
name="postgresqlDatabase"
|
@ -4,11 +4,11 @@
|
||||
export let service: any;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Hasura</div>
|
||||
<div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
|
||||
<div class="title font-bold pb-3">Hasura</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>
|
||||
<CopyPasswordField
|
||||
name="graphQLAdminPassword"
|
||||
@ -19,14 +19,17 @@
|
||||
disabled
|
||||
/>
|
||||
</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="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">PostgreSQL</div>
|
||||
<div class="px-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="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
|
||||
<div class="title font-bold pb-3">PostgreSQL</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
||||
<div class="space-y-2 px-4">
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="postgresqlUser">{$t('forms.username')}</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlUser"
|
||||
@ -36,7 +39,7 @@
|
||||
disabled
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
id="postgresqlPassword"
|
||||
@ -47,7 +50,7 @@
|
||||
value={service.hasura.postgresqlPassword}
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
name="postgresqlDatabase"
|
@ -4,10 +4,10 @@
|
||||
export let service: any;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">MeiliSearch</div>
|
||||
<div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
|
||||
<div class="title font-bold pb-3">MeiliSearch</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>
|
||||
<CopyPasswordField
|
||||
id="masterKey"
|
@ -5,11 +5,11 @@
|
||||
export let service: any;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">MinIO</div>
|
||||
<div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
|
||||
<div class="title font-bold pb-3">MinIO</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
||||
<div class="space-y-2 px-4">
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="rootUser">{$t('forms.root_user')}</label>
|
||||
<input
|
||||
class="w-full"
|
||||
@ -21,7 +21,7 @@
|
||||
readonly
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
id="rootUserPassword"
|
||||
@ -33,7 +33,7 @@
|
||||
/>
|
||||
</div>
|
||||
{#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>
|
||||
<input
|
||||
class="w-full"
|
@ -5,10 +5,10 @@
|
||||
export let service: any;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Moodle</div>
|
||||
<div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
|
||||
<div class="title font-bold pb-3">Moodle</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>
|
||||
<input
|
||||
class="w-full"
|
||||
@ -21,7 +21,7 @@
|
||||
value={service.moodle.defaultEmail}
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
id="defaultUsername"
|
||||
@ -32,7 +32,7 @@
|
||||
value={service.moodle.defaultUsername}
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
id="defaultPassword"
|
||||
@ -44,10 +44,10 @@
|
||||
value={service.moodle.defaultPassword}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">MariaDB</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="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>
|
||||
<CopyPasswordField
|
||||
name="mariadbUser"
|
||||
@ -57,7 +57,7 @@
|
||||
disabled
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
id="mariadbPassword"
|
||||
@ -68,7 +68,7 @@
|
||||
value={service.moodle.mariadbPassword}
|
||||
/>
|
||||
</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>
|
||||
<input
|
||||
class="w-full"
|
||||
@ -81,7 +81,7 @@
|
||||
placeholder="{$t('forms.eg')}: moodle_db"
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
id="mariadbRootUser"
|
||||
@ -91,7 +91,7 @@
|
||||
value={service.moodle.mariadbRootUser}
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
id="mariadbRootUserPassword"
|
@ -1,17 +1,34 @@
|
||||
<script lang="ts">
|
||||
export let service: any;
|
||||
export let readOnly: any;
|
||||
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { appSession, status } from '$lib/store';
|
||||
import { t } from '$lib/translations';
|
||||
export let service: any;
|
||||
export let readOnly: any;
|
||||
import ServiceStatus from '../ServiceStatus.svelte';
|
||||
let serviceStatus = {
|
||||
isExited: false,
|
||||
isRunning: false,
|
||||
isRestarting: false,
|
||||
isStopped: false
|
||||
};
|
||||
|
||||
$: if (Object.keys($status.service.statuses).length > 0) {
|
||||
let { isExited, isRunning, isRestarting } = $status.service.statuses[service.id].status;
|
||||
serviceStatus.isExited = isExited;
|
||||
serviceStatus.isRunning = isRunning;
|
||||
serviceStatus.isRestarting = isRestarting;
|
||||
serviceStatus.isStopped = !isExited && !isRunning && !isRestarting;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Plausible Analytics</div>
|
||||
<div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
|
||||
<div class="title font-bold pb-3">Plausible Analytics</div>
|
||||
<ServiceStatus id={service.id} />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
||||
<div class="space-y-2 px-4">
|
||||
<div class="grid grid-cols-2 items-center ">
|
||||
<label for="scriptName"
|
||||
>Script Name <Explainer
|
||||
explanation="Useful if you would like to rename the collector script to prevent it blocked by AdBlockers."
|
||||
@ -28,7 +45,7 @@
|
||||
required
|
||||
/>
|
||||
</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>
|
||||
<input
|
||||
class="w-full"
|
||||
@ -41,7 +58,7 @@
|
||||
required
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
name="username"
|
||||
@ -53,7 +70,7 @@
|
||||
required
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
id="password"
|
||||
@ -65,11 +82,13 @@
|
||||
/>
|
||||
</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 class="space-y-2">
|
||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
||||
<div class="space-y-2 px-4">
|
||||
<div class="grid grid-cols-2 items-center ">
|
||||
<label for="postgresqlUser">{$t('forms.username')}</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlUser"
|
||||
@ -79,7 +98,7 @@
|
||||
disabled
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
id="postgresqlPassword"
|
||||
@ -90,7 +109,7 @@
|
||||
value={service.plausibleAnalytics.postgresqlPassword}
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
name="postgresqlDatabase"
|
||||
@ -101,3 +120,7 @@
|
||||
/>
|
||||
</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>
|
@ -4,11 +4,11 @@
|
||||
export let service: any;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">SearXNG</div>
|
||||
<div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
|
||||
<div class="title font-bold pb-3">SearXNG</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>
|
||||
<CopyPasswordField
|
||||
name="secretKey"
|
||||
@ -19,11 +19,11 @@
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Redis</div>
|
||||
<div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
|
||||
<div class="title font-bold pb-3">Redis</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>
|
||||
<CopyPasswordField
|
||||
name="redisPassword"
|
@ -4,11 +4,11 @@
|
||||
export let service: any;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Taiga</div>
|
||||
<div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
|
||||
<div class="title font-bold pb-3">Taiga</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>
|
||||
<CopyPasswordField
|
||||
name="secretKey"
|
||||
@ -20,11 +20,11 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Django</div>
|
||||
<div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
|
||||
<div class="title font-bold pb-3">Django</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>
|
||||
<CopyPasswordField
|
||||
name="djangoAdminUser"
|
||||
@ -34,7 +34,7 @@
|
||||
disabled
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
name="djangoAdminPassword"
|
||||
@ -45,11 +45,11 @@
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">RabbitMQ</div>
|
||||
<div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
|
||||
<div class="title font-bold pb-3">RabbitMQ</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>
|
||||
<CopyPasswordField
|
||||
name="rabbitMQUser"
|
||||
@ -59,7 +59,7 @@
|
||||
disabled
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
name="rabbitMQPassword"
|
||||
@ -71,11 +71,11 @@
|
||||
/>
|
||||
</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>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
name="postgresqlHost"
|
||||
@ -85,7 +85,7 @@
|
||||
disabled
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
name="postgresqlPort"
|
||||
@ -95,7 +95,7 @@
|
||||
disabled
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
name="postgresqlUser"
|
||||
@ -105,7 +105,7 @@
|
||||
disabled
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
name="postgresqlPassword"
|
@ -4,11 +4,11 @@
|
||||
export let service: any;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Umami</div>
|
||||
<div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
|
||||
<div class="title font-bold pb-3">Umami</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
||||
<div class="space-y-2 px-4">
|
||||
<div class="grid grid-cols-2 items-center ">
|
||||
<label for="adminUser">Admin User</label>
|
||||
<input
|
||||
class="w-full"
|
||||
@ -20,7 +20,7 @@
|
||||
readonly
|
||||
/>
|
||||
</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"
|
||||
>Initial Admin Password <Explainer
|
||||
explanation="It could be changed in Umami. <br>This is just the password set initially after the first start."
|
@ -5,10 +5,10 @@
|
||||
export let service: any;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">VSCode Server</div>
|
||||
<div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
|
||||
<div class="title font-bold pb-3">VScode Server</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>
|
||||
<CopyPasswordField
|
||||
id="password"
|
@ -3,11 +3,11 @@
|
||||
export let service: any;
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Weblate</div>
|
||||
<div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
|
||||
<div class="title font-bold pb-3">Weblate</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>
|
||||
<CopyPasswordField
|
||||
name="adminPassword"
|
||||
@ -19,12 +19,12 @@
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
||||
<label for="postgresqlHost">PostgreSQL Host</label>
|
||||
<div class="grid grid-cols-2 items-center px-4">
|
||||
<label for="postgresqlHost">Host</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlHost"
|
||||
id="postgresqlHost"
|
||||
@ -33,8 +33,8 @@
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
||||
<label for="postgresqlPort">PostgreSQL Port</label>
|
||||
<div class="grid grid-cols-2 items-center px-4">
|
||||
<label for="postgresqlPort">Port</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlPort"
|
||||
id="postgresqlPort"
|
||||
@ -43,8 +43,8 @@
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
||||
<label for="postgresqlUser">PostgreSQL User</label>
|
||||
<div class="grid grid-cols-2 items-center px-4">
|
||||
<label for="postgresqlUser">User</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlUser"
|
||||
id="postgresqlUser"
|
||||
@ -53,8 +53,8 @@
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
||||
<label for="postgresqlPassword">PostgreSQL Password</label>
|
||||
<div class="grid grid-cols-2 items-center px-4">
|
||||
<label for="postgresqlPassword">Password</label>
|
||||
<CopyPasswordField
|
||||
name="postgresqlPassword"
|
||||
id="postgresqlPassword"
|
@ -66,11 +66,11 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">Wordpress</div>
|
||||
<div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
|
||||
<div class="title font-bold pb-3">WordPress</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>
|
||||
<textarea
|
||||
class="w-full"
|
||||
@ -90,7 +90,7 @@ define('SUBDOMAIN_INSTALL', false);`
|
||||
: 'N/A'}
|
||||
/>
|
||||
</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
|
||||
id="ftpEnabled"
|
||||
bind:setting={service.wordpress.ftpEnabled}
|
||||
@ -102,15 +102,15 @@ define('SUBDOMAIN_INSTALL', false);`
|
||||
/>
|
||||
</div>
|
||||
{#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>
|
||||
<CopyPasswordField id="ftpUrl" readonly disabled name="ftpUrl" value={ftpUrl} />
|
||||
</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>
|
||||
<CopyPasswordField id="ftpUser" readonly disabled name="ftpUser" value={ftpUser} />
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
id="ftpPassword"
|
||||
@ -122,11 +122,11 @@ define('SUBDOMAIN_INSTALL', false);`
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
<div class="title">MySQL</div>
|
||||
<div class="flex flex-row border-b border-coolgray-500 my-6 space-x-2">
|
||||
<div class="title font-bold pb-3">MySQL</div>
|
||||
</div>
|
||||
<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
|
||||
id="ownMysql"
|
||||
dataTooltip={$t('forms.must_be_stopped_to_modify')}
|
||||
@ -138,7 +138,7 @@ define('SUBDOMAIN_INSTALL', false);`
|
||||
/>
|
||||
</div>
|
||||
{#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>
|
||||
<input
|
||||
class="w-full"
|
||||
@ -151,7 +151,7 @@ define('SUBDOMAIN_INSTALL', false);`
|
||||
placeholder="{$t('forms.eg')}: db.coolify.io"
|
||||
/>
|
||||
</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>
|
||||
<input
|
||||
class="w-full"
|
||||
@ -165,7 +165,7 @@ define('SUBDOMAIN_INSTALL', false);`
|
||||
/>
|
||||
</div>
|
||||
{/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>
|
||||
<input
|
||||
class="w-full"
|
||||
@ -179,7 +179,7 @@ define('SUBDOMAIN_INSTALL', false);`
|
||||
/>
|
||||
</div>
|
||||
{#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>
|
||||
<input
|
||||
class="w-full"
|
||||
@ -191,7 +191,7 @@ define('SUBDOMAIN_INSTALL', false);`
|
||||
disabled={$status.service.isRunning || !service.wordpress.ownMysql}
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
id="mysqlRootUserPassword"
|
||||
@ -203,7 +203,7 @@ define('SUBDOMAIN_INSTALL', false);`
|
||||
/>
|
||||
</div>
|
||||
{/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>
|
||||
<input
|
||||
class="w-full"
|
||||
@ -214,7 +214,7 @@ define('SUBDOMAIN_INSTALL', false);`
|
||||
disabled={$status.service.isRunning || !service.wordpress.ownMysql}
|
||||
/>
|
||||
</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>
|
||||
<CopyPasswordField
|
||||
id="mysqlPassword"
|
15
apps/ui/src/lib/components/Services/index.ts
Normal file
15
apps/ui/src/lib/components/Services/index.ts
Normal 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';
|
15
apps/ui/src/lib/components/Services/utils.ts
Normal file
15
apps/ui/src/lib/components/Services/utils.ts
Normal 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';
|
||||
}
|
@ -4,7 +4,7 @@
|
||||
|
||||
<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"
|
||||
fill="none"
|
||||
>
|
||||
|
@ -5,7 +5,7 @@
|
||||
<svg
|
||||
viewBox="0 0 700 240"
|
||||
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
|
||||
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"
|
||||
|
@ -2,48 +2,7 @@
|
||||
export let type: string;
|
||||
export let isAbsolute = true;
|
||||
import * as Icons from '$lib/components/svg/services';
|
||||
const name: any = type && type[0].toUpperCase() + type.substring(1).toLowerCase();
|
||||
</script>
|
||||
|
||||
{#if type === 'plausibleanalytics'}
|
||||
<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}
|
||||
<svelte:component this={Icons[name]} {isAbsolute} />
|
||||
|
@ -1,21 +1,21 @@
|
||||
//@ts-nocheck
|
||||
export { default as PlausibleAnalytics } from './PlausibleAnalytics.svelte';
|
||||
export { default as NocoDb } from './NocoDB.svelte';
|
||||
export { default as MinIo } from './MinIO.svelte';
|
||||
export { default as VsCodeServer } from './VSCodeServer.svelte';
|
||||
export { default as Plausibleanalytics } from './PlausibleAnalytics.svelte';
|
||||
export { default as Nocodb } from './NocoDB.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 VaultWarden } from './VaultWarden.svelte';
|
||||
export { default as LanguageTool } from './LanguageTool.svelte';
|
||||
export { default as Vaultwarden } from './VaultWarden.svelte';
|
||||
export { default as Languagetool } from './LanguageTool.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 MeiliSearch } from './MeiliSearch.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 Glitchtip } from './GlitchTip.svelte';
|
||||
export { default as Searxng } from './Searxng.svelte';
|
||||
export { default as Weblate } from './Weblate.svelte';
|
||||
export { default as Grafana } from './Grafana.svelte';
|
||||
|
@ -81,8 +81,8 @@ export const status: Writable<any> = writable({
|
||||
initialLoading: true
|
||||
},
|
||||
service: {
|
||||
isRunning: false,
|
||||
isExited: false,
|
||||
statuses: [],
|
||||
overallStatus: 'stopped',
|
||||
loading: false,
|
||||
initialLoading: true
|
||||
},
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
<ul class="menu border bg-coolgray-100 border-coolgray-200 rounded p-2 space-y-2 sticky top-4">
|
||||
<li class="menu-title">
|
||||
<span>Configuration</span>
|
||||
<span>General</span>
|
||||
</li>
|
||||
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
|
||||
<li>
|
||||
|
@ -166,7 +166,7 @@
|
||||
</div>
|
||||
<div class="flex flex-row lg:flex-col lg:items-center items-start">
|
||||
{#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}
|
||||
|
||||
<div class="flex justify-center h-full items-center pt-3">
|
||||
|
@ -59,9 +59,8 @@
|
||||
}
|
||||
</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 lg:flex-row lg:space-y-0 space-y-2">
|
||||
<input
|
||||
class="w-full lg:w-64"
|
||||
|
@ -233,7 +233,7 @@
|
||||
class:text-red-500={$status.application.overallStatus === 'stopped'}
|
||||
>
|
||||
{$status.application.overallStatus === 'healthy'
|
||||
? 'Running'
|
||||
? 'Healthy'
|
||||
: $status.application.overallStatus === 'degraded'
|
||||
? 'Degraded'
|
||||
: 'Stopped'}
|
||||
|
@ -244,7 +244,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
<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" />
|
||||
</div>
|
||||
<PublicRepository />
|
||||
|
@ -61,7 +61,7 @@
|
||||
disabled={!$appSession.isAdmin}
|
||||
class:bg-red-600={$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
|
||||
</button>
|
||||
|
@ -135,7 +135,7 @@
|
||||
<div class="text-xl font-bold tracking-tighter">Container not found / exited.</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="relative w-full">
|
||||
<div class="relative w-full"></div>
|
||||
<div class="flex justify-start sticky space-x-2 pb-2">
|
||||
<button
|
||||
on:click={followBuild}
|
||||
@ -162,8 +162,9 @@
|
||||
{followingLogs ? 'Following Logs...' : 'Follow Logs'}
|
||||
</button>
|
||||
{#if loadLogsInterval}
|
||||
<button id="streaming" class="btn btn-sm bg-transparent border-none loading" />
|
||||
<Tooltip triggeredBy="#streaming">Streaming logs</Tooltip>
|
||||
<button id="streaming" class="btn btn-sm bg-transparent border-none loading"
|
||||
>Streaming logs</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
|
@ -88,10 +88,6 @@
|
||||
);
|
||||
batchSecrets = '';
|
||||
await refreshSecrets();
|
||||
// addToast({
|
||||
// message: 'Secrets saved.',
|
||||
// type: 'success'
|
||||
// });
|
||||
}
|
||||
</script>
|
||||
|
||||
|
136
apps/ui/src/routes/services/[id]/_Menu.svelte
Normal file
136
apps/ui/src/routes/services/[id]/_Menu.svelte
Normal 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>
|
@ -50,14 +50,17 @@
|
||||
|
||||
<td>
|
||||
<input
|
||||
|
||||
style="min-width: 350px !important;"
|
||||
id={isNewSecret ? 'secretName' : 'secretNameNew'}
|
||||
bind:value={name}
|
||||
required
|
||||
placeholder="EXAMPLE_VARIABLE"
|
||||
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}
|
||||
/>
|
||||
</td>
|
||||
@ -67,7 +70,6 @@
|
||||
name={isNewSecret ? 'secretValue' : 'secretValueNew'}
|
||||
isPasswordField={true}
|
||||
bind:value
|
||||
required
|
||||
placeholder="J$#@UIO%HO#$U%H"
|
||||
inputStyle="min-width: 350px; !important"
|
||||
/>
|
||||
@ -76,12 +78,12 @@
|
||||
<td>
|
||||
{#if isNewSecret}
|
||||
<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>
|
||||
{:else}
|
||||
<div class="flex flex-row justify-center space-x-2">
|
||||
<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 class="flex justify-center items-end">
|
||||
<button class="btn btn-sm bg-error" on:click={removeSecret}>Remove</button>
|
||||
|
@ -1,86 +1,45 @@
|
||||
<script lang="ts">
|
||||
import DocLink from '$lib/components/DocLink.svelte';
|
||||
export let service: any;
|
||||
export let linkToDocs: boolean = false;
|
||||
import ExternalLink from '$lib/components/ExternalLink.svelte';
|
||||
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>
|
||||
|
||||
{#if service.type === 'plausibleanalytics'}
|
||||
<a href="https://plausible.io" target="_blank">
|
||||
<Icons.PlausibleAnalytics />
|
||||
</a>
|
||||
{: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 linkToDocs}
|
||||
<DocLink url={links[service.type]} text={`Open documentation`} isExternal={true} />
|
||||
{:else}
|
||||
<svelte:component this={Icons[name]} />
|
||||
{/if}
|
||||
<!-- <a href={links[service.type]} target="_blank" class="no-underline">
|
||||
{#if linkToDocs}
|
||||
Open Documentation
|
||||
<ExternalLink />
|
||||
{:else}
|
||||
|
||||
{/if}
|
||||
</a> -->
|
||||
|
@ -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>
|
@ -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>
|
@ -8,6 +8,7 @@
|
||||
import { page } from '$app/stores';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { addToast } from '$lib/store';
|
||||
const { id } = $page.params;
|
||||
@ -15,7 +16,7 @@
|
||||
const dispatch = createEventDispatcher();
|
||||
async function saveStorage(newStorage = false) {
|
||||
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.endsWith('/') ? storage.path.slice(0, -1) : storage.path;
|
||||
storage.path.replace(/\/\//g, '/');
|
||||
@ -29,10 +30,17 @@
|
||||
storage.path = null;
|
||||
storage.id = null;
|
||||
}
|
||||
if (newStorage) {
|
||||
addToast({
|
||||
message: 'Storage saved.',
|
||||
message: $t('application.storage.storage_saved'),
|
||||
type: 'success'
|
||||
});
|
||||
} else {
|
||||
addToast({
|
||||
message: $t('application.storage.storage_updated'),
|
||||
type: 'success'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
@ -41,46 +49,45 @@
|
||||
try {
|
||||
await del(`/services/${id}/storages`, { path: storage.path });
|
||||
dispatch('refresh');
|
||||
return addToast({
|
||||
message: 'Storage deleted.',
|
||||
addToast({
|
||||
message: $t('application.storage.storage_deleted'),
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function handleSubmit() {
|
||||
if (isNew) {
|
||||
await saveStorage(true);
|
||||
} else {
|
||||
await saveStorage(false);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<td>
|
||||
<form on:submit|preventDefault={handleSubmit}>
|
||||
<div class="w-fullgrid gap-2">
|
||||
<div class="flex flex-col pb-2">
|
||||
<div class="flex flex-col lg:flex-row lg:space-y-0 space-y-2">
|
||||
<input
|
||||
class="w-full lg:w-64"
|
||||
bind:value={storage.path}
|
||||
required
|
||||
placeholder="eg: /data"
|
||||
placeholder="eg: /sqlite.db"
|
||||
/>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
{#if isNew}
|
||||
<div class="flex items-center justify-center">
|
||||
<button class="btn btn-sm btn-success" on:click={() => saveStorage(true)}>Add</button
|
||||
<div class="flex items-center justify-center w-full lg:w-64">
|
||||
<button class="btn btn-sm btn-primary" on:click={() => saveStorage(true)}
|
||||
>{$t('forms.add')}</button
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row justify-center space-x-2">
|
||||
<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-success" on:click={() => saveStorage(false)}>Set</button>
|
||||
<button class="btn btn-sm btn-primary" on:click={() => saveStorage(false)}
|
||||
>{$t('forms.set')}</button
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-center items-end">
|
||||
<button class="btn btn-sm btn-error" on:click={removeStorage}>Remove</button>
|
||||
<div class="flex justify-center">
|
||||
<button class="btn btn-sm btn-error" on:click={removeStorage}
|
||||
>{$t('forms.remove')}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,8 +4,6 @@
|
||||
let configurationPhase = null;
|
||||
if (!service.type) {
|
||||
configurationPhase = 'type';
|
||||
} else if (!service.version) {
|
||||
configurationPhase = 'version';
|
||||
} else if (!service.destinationDockerId) {
|
||||
configurationPhase = 'destination';
|
||||
}
|
||||
@ -15,7 +13,7 @@
|
||||
try {
|
||||
let readOnly = false;
|
||||
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) {
|
||||
return {
|
||||
status: 302,
|
||||
@ -38,7 +36,8 @@
|
||||
|
||||
return {
|
||||
props: {
|
||||
service
|
||||
service,
|
||||
template
|
||||
},
|
||||
stuff: {
|
||||
service,
|
||||
@ -53,8 +52,9 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let service: any;
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
|
||||
import { del, get, post } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification, handlerNotFoundLoad } from '$lib/common';
|
||||
@ -67,13 +67,10 @@
|
||||
checkIfDeploymentEnabledServices
|
||||
} from '$lib/store';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||
import ServiceLinks from './_ServiceLinks.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import Menu from './_Menu.svelte';
|
||||
const { id } = $page.params;
|
||||
|
||||
export let service: any;
|
||||
|
||||
$isDeploymentEnabled = checkIfDeploymentEnabledServices($appSession.isAdmin, service);
|
||||
|
||||
let statusInterval: any;
|
||||
@ -127,25 +124,46 @@
|
||||
if ($status.service.loading) return;
|
||||
$status.service.loading = true;
|
||||
const data = await get(`/services/${id}/status`);
|
||||
$status.service.isRunning = data.isRunning;
|
||||
$status.service.isExited = data.isExited;
|
||||
$status.service.initialLoading = false;
|
||||
|
||||
$status.service.statuses = data;
|
||||
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.initialLoading = false;
|
||||
}
|
||||
onDestroy(() => {
|
||||
$status.service.initialLoading = true;
|
||||
$status.service.isRunning = false;
|
||||
$status.service.isExited = false;
|
||||
$status.service.loading = false;
|
||||
$status.service.statuses = [];
|
||||
$location = null;
|
||||
$isDeploymentEnabled = false;
|
||||
clearInterval(statusInterval);
|
||||
});
|
||||
onMount(async () => {
|
||||
setLocation(service);
|
||||
$status.service.isRunning = false;
|
||||
$status.service.loading = false;
|
||||
if (service.type && service.destinationDockerId && service.version && service.fqdn) {
|
||||
if ($isDeploymentEnabled) {
|
||||
await getStatus();
|
||||
statusInterval = setInterval(async () => {
|
||||
await getStatus();
|
||||
@ -156,13 +174,12 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<nav class="header lg:flex-row flex-col-reverse">
|
||||
<div class="flex flex-row space-x-2 font-bold pt-10 lg:pt-0">
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div class="title">
|
||||
{#if $page.url.pathname === `/services/${id}`}
|
||||
Configurations
|
||||
{:else if $page.url.pathname === `/services/${id}/secrets`}
|
||||
<div class="mx-auto max-w-screen-2xl px-6 grid grid-cols-1 lg:grid-cols-2">
|
||||
<nav class="header flex flex-row order-2 lg:order-1 px-0 lg:px-4 items-start">
|
||||
<div class="title lg:pb-10">
|
||||
<div class="flex justify-center items-center space-x-2">
|
||||
<div>
|
||||
{#if $page.url.pathname === `/services/${id}/secrets`}
|
||||
Secrets
|
||||
{:else if $page.url.pathname === `/services/${id}/storages`}
|
||||
Persistent Storages
|
||||
@ -174,72 +191,48 @@
|
||||
Select a Service Version
|
||||
{:else if $page.url.pathname === `/services/${id}/configuration/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}
|
||||
</div>
|
||||
</div>
|
||||
<ServiceLinks {service} />
|
||||
</div>
|
||||
<div class="lg:block hidden flex-1" />
|
||||
<div class="flex flex-row flex-wrap space-x-3 justify-center lg:justify-start lg:py-0">
|
||||
{#if $location}
|
||||
<a
|
||||
id="open"
|
||||
href={$location}
|
||||
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" />
|
||||
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
|
||||
<line x1="10" y1="14" x2="20" y2="4" />
|
||||
<polyline points="15 4 20 4 20 9" />
|
||||
</svg></a
|
||||
>
|
||||
<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 $status.service.initialLoading}
|
||||
{#if $page.url.pathname.startsWith(`/services/${id}/configuration/`)}
|
||||
<div class="px-2">
|
||||
<button
|
||||
class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out"
|
||||
on:click={() => deleteService()}
|
||||
disabled={!$appSession.isAdmin}
|
||||
class:bg-red-600={$appSession.isAdmin}
|
||||
class:hover:bg-red-500={$appSession.isAdmin}
|
||||
class="btn btn-sm btn-error text-sm"
|
||||
>
|
||||
Delete Service
|
||||
</button>
|
||||
</div>
|
||||
{/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}
|
||||
<button class="btn btn-ghost btn-sm gap-2">
|
||||
<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"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
@ -255,14 +248,14 @@
|
||||
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
|
||||
<line x1="11" y1="19.94" x2="11" y2="19.95" />
|
||||
</svg>
|
||||
Loading...
|
||||
</button>
|
||||
{:else if $status.service.isRunning}
|
||||
{:else if $status.service.overallStatus === 'healthy'}
|
||||
<button
|
||||
id="stop"
|
||||
on:click={stopService}
|
||||
type="submit"
|
||||
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"
|
||||
@ -277,79 +270,13 @@
|
||||
<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>
|
||||
</svg> Stop
|
||||
</button>
|
||||
<Tooltip triggeredBy="#stop">Stop</Tooltip>
|
||||
{:else}
|
||||
<button
|
||||
id="start"
|
||||
on:click={startService}
|
||||
type="submit"
|
||||
disabled={!$isDeploymentEnabled}
|
||||
class="icons bg-transparent text-sm flex items-center space-x-2 text-green-500"
|
||||
><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"
|
||||
class="btn btn-sm gap-2"
|
||||
on:click={() => startService()}
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
</button>
|
||||
<Tooltip triggeredBy="#start">Start</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if service.type && service.destinationDockerId && service.version}
|
||||
<div class="hidden lg:block border border-coolgray-500 h-8" />
|
||||
<a
|
||||
href="/services/{id}"
|
||||
sveltekit:prefetch
|
||||
class="hover:text-yellow-500 rounded"
|
||||
class:text-yellow-500={$page.url.pathname === `/services/${id}`}
|
||||
class:bg-coolgray-500={$page.url.pathname === `/services/${id}`}
|
||||
>
|
||||
<button
|
||||
id="configuration"
|
||||
disabled={!$isDeploymentEnabled}
|
||||
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" />
|
||||
<rect x="4" y="8" width="4" height="4" />
|
||||
<line x1="6" y1="4" x2="6" y2="8" />
|
||||
<line x1="6" y1="12" x2="6" y2="20" />
|
||||
<rect x="10" y="14" width="4" height="4" />
|
||||
<line x1="12" y1="4" x2="12" y2="14" />
|
||||
<line x1="12" y1="18" x2="12" y2="20" />
|
||||
<rect x="16" y="5" width="4" height="4" />
|
||||
<line x1="18" y1="4" x2="18" y2="5" />
|
||||
<line x1="18" y1="9" x2="18" y2="20" />
|
||||
</svg></button
|
||||
></a
|
||||
>
|
||||
<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
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6"
|
||||
@ -362,25 +289,19 @@
|
||||
>
|
||||
<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"
|
||||
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`}
|
||||
>
|
||||
</svg>
|
||||
|
||||
Force Redeploy
|
||||
</button>
|
||||
{:else if $status.service.overallStatus === 'degraded'}
|
||||
<button
|
||||
id="persistentstorage"
|
||||
on:click={stopService}
|
||||
type="submit"
|
||||
disabled={!$isDeploymentEnabled}
|
||||
class="icons bg-transparent text-sm"
|
||||
class="btn btn-sm btn-error gap-2"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -393,29 +314,21 @@
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<ellipse cx="12" cy="6" rx="8" ry="3" />
|
||||
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
|
||||
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
|
||||
</svg>
|
||||
</button></a
|
||||
>
|
||||
<Tooltip triggeredBy="#persistentstorage">Persistent Storages</Tooltip>
|
||||
<div class="hidden lg:block border border-coolgray-500 h-8" />
|
||||
<a
|
||||
href={$isDeploymentEnabled && $status.service.isRunning ? `/services/${id}/logs` : null}
|
||||
sveltekit:prefetch
|
||||
class="hover:text-pink-500 rounded"
|
||||
class:text-pink-500={$page.url.pathname === `/services/${id}/logs`}
|
||||
class:bg-coolgray-500={$page.url.pathname === `/services/${id}/logs`}
|
||||
>
|
||||
<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>
|
||||
{:else if $status.service.overallStatus === 'stopped'}
|
||||
<button
|
||||
id="logs"
|
||||
disabled={!$status.service.isRunning}
|
||||
class="icons bg-transparent text-sm"
|
||||
class="btn btn-sm gap-2"
|
||||
class:btn-primary={$status.application.overallStatus !== 'degraded'}
|
||||
disabled={!$isDeploymentEnabled}
|
||||
on:click={() => startService()}
|
||||
>
|
||||
{#if $status.application.overallStatus !== 'degraded'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
class="w-6 h-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
@ -424,26 +337,46 @@
|
||||
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
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
{:else}
|
||||
<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"
|
||||
>
|
||||
<Tooltip triggeredBy="#logs">Logs</Tooltip>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
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>
|
||||
{/if}
|
||||
{$status.application.overallStatus === 'degraded'
|
||||
? $status.application.statuses.length === 1
|
||||
? 'Force Redeploy'
|
||||
: 'Redeploy Stack'
|
||||
: 'Deploy'}
|
||||
</button>
|
||||
{/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>
|
||||
|
||||
<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>
|
||||
|
@ -81,7 +81,7 @@
|
||||
{#each destinations as destination}
|
||||
<div class="p-2">
|
||||
<form on:submit|preventDefault={() => handleSubmit(destination.id)}>
|
||||
<button type="submit" class="box-selection hover:bg-sky-700 font-bold">
|
||||
<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="text-center truncate">{destination.network}</div>
|
||||
</button>
|
||||
|
@ -25,30 +25,29 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let types: any;
|
||||
export let services: any;
|
||||
|
||||
let search = '';
|
||||
let filteredTypes = types;
|
||||
let filteredServices = services;
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { get, post } from '$lib/api';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import ServiceIcons from '$lib/components/svg/services/ServiceIcons.svelte';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
|
||||
async function handleSubmit(type: any) {
|
||||
async function handleSubmit(service: any) {
|
||||
try {
|
||||
await post(`/services/${id}/configuration/type`, { type });
|
||||
await post(`/services/${id}/configuration/type`, { ...service });
|
||||
return await goto(from || `/services/${id}`);
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
function doSearch() {
|
||||
filteredTypes = types.filter(
|
||||
filteredServices = services.filter(
|
||||
(type: any) =>
|
||||
type.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
type.labels.some((label: string) => label.toLowerCase().includes(search.toLowerCase()))
|
||||
@ -56,7 +55,7 @@
|
||||
}
|
||||
function cleanupSearch() {
|
||||
search = '';
|
||||
filteredTypes = types;
|
||||
filteredServices = services;
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -90,12 +89,12 @@
|
||||
</div>
|
||||
<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">
|
||||
{#each filteredTypes as type}
|
||||
{#each filteredServices as service}
|
||||
<div class="p-2">
|
||||
<form on:submit|preventDefault={() => handleSubmit(type.name)}>
|
||||
<button type="submit" class="box-selection relative text-xl font-bold hover:bg-pink-600">
|
||||
<ServiceIcons type={type.name} />
|
||||
{type.fancyName}
|
||||
<form on:submit|preventDefault={() => handleSubmit(service)}>
|
||||
<button type="submit" class="box-selection relative text-xl font-bold hover:bg-primary">
|
||||
<!-- <ServiceIcons type={service.name} /> -->
|
||||
{service.displayName}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
66
apps/ui/src/routes/services/[id]/danger.svelte
Normal file
66
apps/ui/src/routes/services/[id]/danger.svelte
Normal 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>
|
@ -8,64 +8,410 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import Services from './_Services/_Services.svelte';
|
||||
import { get } from '$lib/api';
|
||||
import { page } from '$app/stores';
|
||||
import { status } from '$lib/store';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import ServiceLinks from './_ServiceLinks.svelte';
|
||||
|
||||
export let service: any;
|
||||
export let readOnly: any;
|
||||
export let settings: any;
|
||||
|
||||
const { id } = $page.params;
|
||||
let loading = {
|
||||
usage: false
|
||||
};
|
||||
let usage = {
|
||||
MemUsage: 0,
|
||||
CPUPerc: 0,
|
||||
NetIO: 0
|
||||
};
|
||||
let usageInterval: any;
|
||||
import cuid from 'cuid';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
async function getUsage() {
|
||||
if (loading.usage) return;
|
||||
if (!$status.service.isRunning) return;
|
||||
loading.usage = true;
|
||||
const data = await get(`/services/${id}/usage`);
|
||||
usage = data.usage;
|
||||
loading.usage = false;
|
||||
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 * 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(() => {
|
||||
clearInterval(usageInterval);
|
||||
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 () => {
|
||||
await getUsage();
|
||||
usageInterval = setInterval(async () => {
|
||||
await getUsage();
|
||||
}, 1000);
|
||||
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 lg:my-0 my-4 lg:pt-0 pt-4 rounded">
|
||||
<div class="text-center">
|
||||
<div class="stat w-64">
|
||||
<div class="stat-title">Used Memory / Memory Limit</div>
|
||||
<div class="stat-value text-xl">{usage?.MemUsage}</div>
|
||||
<div class="w-full">
|
||||
<form on:submit|preventDefault={() => handleSubmit()}>
|
||||
<div class="mx-auto w-full">
|
||||
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
|
||||
<div class="title font-bold pb-3 ">General</div>
|
||||
{#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 class="stat w-64">
|
||||
<div class="stat-title">Used CPU</div>
|
||||
<div class="stat-value text-xl">{usage?.CPUPerc}</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 grid-flow-row gap-2 px-4">
|
||||
<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>
|
||||
|
||||
<div class="stat w-64">
|
||||
<div class="stat-title">Network IO</div>
|
||||
<div class="stat-value text-xl">{usage?.NetIO}</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>
|
||||
{#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>
|
||||
<Services bind:service bind:readOnly bind:settings />
|
||||
|
@ -8,6 +8,7 @@
|
||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||
|
||||
let service: any = {};
|
||||
let template: any = null;
|
||||
let logsLoading = false;
|
||||
let loadLogsInterval: any = null;
|
||||
let logs: any = [];
|
||||
@ -16,11 +17,12 @@
|
||||
let followingLogs: any;
|
||||
let logsEl: any;
|
||||
let position = 0;
|
||||
|
||||
let selectedService: any = null;
|
||||
const { id } = $page.params;
|
||||
|
||||
onMount(async () => {
|
||||
const response = await get(`/services/${id}`);
|
||||
template = response.template;
|
||||
service = response.service;
|
||||
loadAllLogs();
|
||||
loadLogsInterval = setInterval(() => {
|
||||
@ -82,27 +84,52 @@
|
||||
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>
|
||||
|
||||
<div class="flex flex-row justify-center space-x-2 px-10 pt-6">
|
||||
{#if template}
|
||||
<div class="flex gap-2 lg:gap-8 pb-4">
|
||||
{#each Object.keys(template.services) as service}
|
||||
<button
|
||||
on:click={() => selectService(service, true)}
|
||||
class:bg-primary={selectedService === service}
|
||||
class:bg-coolgray-200={selectedService !== service}
|
||||
class="w-full rounded p-5 hover:bg-primary font-bold"
|
||||
>
|
||||
{service}</button
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if selectedService}
|
||||
<div class="flex flex-row justify-center space-x-2">
|
||||
{#if logs.length === 0}
|
||||
<div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div>
|
||||
{:else}
|
||||
<div class="relative w-full">
|
||||
<div class="text-right " />
|
||||
{#if loadLogsInterval}
|
||||
<LoadingLogs />
|
||||
{/if}
|
||||
<div class="flex justify-end sticky top-0 p-1 mx-1">
|
||||
<div class="flex justify-start sticky space-x-2 pb-2">
|
||||
<button
|
||||
id="follow"
|
||||
on:click={followBuild}
|
||||
class="bg-transparent btn btn-sm btn-link"
|
||||
class:text-green-500={followingLogs}
|
||||
class="btn btn-sm bg-coollabs"
|
||||
class:bg-coolgray-300={followingLogs}
|
||||
class:text-applications={followingLogs}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6"
|
||||
class="w-6 h-6 mr-2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
@ -116,10 +143,19 @@
|
||||
<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>
|
||||
<Tooltip triggeredBy="#follow">Follow Logs</Tooltip>
|
||||
{#if loadLogsInterval}
|
||||
<button id="streaming" class="btn btn-sm bg-transparent border-none loading"
|
||||
>Streaming logs</button
|
||||
>
|
||||
{/if}
|
||||
</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">
|
||||
<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}
|
||||
@ -127,3 +163,4 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -26,7 +26,6 @@
|
||||
import { get } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import pLimit from 'p-limit';
|
||||
import ServiceLinks from './_ServiceLinks.svelte';
|
||||
import { addToast } from '$lib/store';
|
||||
import { saveSecret } from './utils';
|
||||
const limit = pLimit(1);
|
||||
@ -38,8 +37,8 @@
|
||||
const data = await get(`/services/${id}/secrets`);
|
||||
secrets = [...data.secrets];
|
||||
}
|
||||
async function getValues(e: any) {
|
||||
e.preventDefault();
|
||||
async function getValues() {
|
||||
if (!batchSecrets) return;
|
||||
const eachValuePair = batchSecrets.split('\n');
|
||||
const batchSecretsPairs = eachValuePair
|
||||
.filter((secret) => !secret.startsWith('#') && secret)
|
||||
@ -48,8 +47,8 @@
|
||||
const value = rest.join('=');
|
||||
const cleanValue = value?.replaceAll('"', '') || '';
|
||||
return {
|
||||
name,
|
||||
value: cleanValue,
|
||||
name: name.trim(),
|
||||
value: cleanValue.trim(),
|
||||
isNew: !secrets.find((secret: any) => name === secret.name)
|
||||
};
|
||||
});
|
||||
@ -68,17 +67,20 @@
|
||||
}
|
||||
</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">
|
||||
<table class="w-full border-separate text-left">
|
||||
<thead>
|
||||
<tr class="h-12">
|
||||
<tr class="uppercase">
|
||||
<th scope="col">{$t('forms.name')}</th>
|
||||
<th scope="col">{$t('forms.value')}</th>
|
||||
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
|
||||
<th scope="col uppercase">{$t('forms.value')}</th>
|
||||
<th scope="col uppercase" class="w-96 text-center">{$t('forms.action')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody class="space-y-2">
|
||||
{#each secrets as secret}
|
||||
{#key secret.id}
|
||||
<tr>
|
||||
@ -92,9 +94,18 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<h2 class="title my-6 font-bold">Paste .env file</h2>
|
||||
<form on:submit|preventDefault={getValues} class="mb-12 w-full">
|
||||
<textarea bind:value={batchSecrets} class="mb-2 min-h-[200px] w-full" />
|
||||
<button class="btn btn-sm bg-services" type="submit">Batch add secrets</button>
|
||||
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2 pt-10">
|
||||
<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>
|
||||
</div>
|
||||
|
@ -5,7 +5,6 @@
|
||||
const response = await get(`/services/${params.id}/storages`);
|
||||
return {
|
||||
props: {
|
||||
service: stuff.service,
|
||||
...response
|
||||
}
|
||||
};
|
||||
@ -19,14 +18,12 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let service: any;
|
||||
export let persistentStorages: any;
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import Storage from './_Storage.svelte';
|
||||
import { get } from '$lib/api';
|
||||
import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
|
||||
import ServiceLinks from './_ServiceLinks.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
|
||||
const { id } = $page.params;
|
||||
async function refreshStorage() {
|
||||
@ -35,30 +32,22 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">
|
||||
<div class="flex justify-center py-4 text-center">
|
||||
<SimpleExplainer
|
||||
customClass="w-full"
|
||||
text={'You can specify any folder that you want to be persistent across restarts. <br>This is useful for storing data for VSCode server or WordPress.'}
|
||||
<div class="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="title font-bold pb-3">
|
||||
Persistent Volumes <Explainer
|
||||
position="dropdown-bottom"
|
||||
explanation={$t('application.storage.persistent_storage_explainer')}
|
||||
/>
|
||||
</div>
|
||||
<table class="mx-auto border-separate text-left">
|
||||
<thead>
|
||||
<tr class="h-12">
|
||||
<th scope="col">Path</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</div>
|
||||
<label for="name" class="pb-2 uppercase font-bold">name</label>
|
||||
{#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>
|
||||
|
Loading…
Reference in New Issue
Block a user