Compare commits

...

50 Commits

Author SHA1 Message Date
Andras Bacsai
4159804052 update github actions 2022-09-02 17:56:33 +00:00
Andras Bacsai
adb27cf143 rc test 2022-09-02 17:52:45 +00:00
Andras Bacsai
a4879d854f github-actions: added rc release 2022-09-02 17:36:47 +00:00
Andras Bacsai
8b92dfb889 test: dockerfile 2022-09-02 15:36:24 +02:00
Andras Bacsai
eb4868cb6e test: native binary target 2022-09-02 15:35:24 +02:00
Andras Bacsai
e06e6e05ae disable taiga for now 2022-09-02 15:31:22 +02:00
Andras Bacsai
4fce4f81c7 fix: taiga 2022-09-02 15:11:21 +02:00
Andras Bacsai
ae11283574 fix 2022-09-02 14:33:52 +02:00
Andras Bacsai
15fc9aa483 fix nginxconf 2022-09-02 14:24:26 +02:00
Andras Bacsai
2ebfb8e6a9 fix 2022-09-02 14:14:20 +02:00
Andras Bacsai
d098ea675f feat: Taiga 2022-09-02 14:11:36 +02:00
Andras Bacsai
8ad152e5fc update autoupdate process 2022-09-02 13:12:45 +02:00
Andras Bacsai
14077fcf51 fix: database name on logs view 2022-09-02 09:05:36 +02:00
Andras Bacsai
b427573e19 fix: explainer component 2022-09-02 09:00:35 +02:00
Andras Bacsai
46268f0dcf fix: Settings missing id 2022-09-02 08:43:02 +02:00
Andras Bacsai
006c178eb1 fix: rename components + remove PR/MR deployment from public repos 2022-09-01 15:32:19 +02:00
Andras Bacsai
d63b20dabb revert git submodule 2022-09-01 15:02:49 +02:00
Andras Bacsai
fcf0a391ed fix: finally works! :) 2022-09-01 14:51:47 +02:00
Andras Bacsai
263b9c4b3e fix: traefik 2022-09-01 14:41:29 +02:00
Andras Bacsai
1dc7355952 fix: traefik appwrite 2022-09-01 14:36:07 +02:00
Andras Bacsai
67e4a72a28 fix: appwrite letsencrypt 2022-09-01 14:23:52 +02:00
Andras Bacsai
e6ea07f9b7 fix: exposedport on save 2022-09-01 14:01:31 +02:00
Andras Bacsai
44a691ae29 fix: UI + refactor 2022-09-01 13:58:44 +02:00
Andras Bacsai
290dbc43cb fix: gitlab webhooks 2022-09-01 13:58:27 +02:00
Andras Bacsai
219f1f9f3f feat: gitlab dual branch 2022-09-01 12:17:20 +02:00
Andras Bacsai
582170f26e feat: github allow fual branches 2022-09-01 12:14:06 +02:00
Andras Bacsai
4e2dad7720 feat: show elapsed time on running builds 2022-09-01 12:13:54 +02:00
Andras Bacsai
d002ec72ad update lock file 2022-09-01 11:20:36 +02:00
Andras Bacsai
f6bb14f7c4 ui: change tooltips and info boxes 2022-09-01 11:20:22 +02:00
Andras Bacsai
e1697848a5 fix: submodule 2022-09-01 09:30:24 +02:00
Andras Bacsai
4d48bba350 fix: ui 2022-08-31 15:40:07 +02:00
Andras Bacsai
a690cc5564 ui: fixes 2022-08-31 15:17:18 +02:00
Andras Bacsai
be16f76034 Merge pull request #583 from coollabsio/ui
UI updates
2022-08-31 15:07:01 +02:00
Andras Bacsai
ae4cf44728 Merge branch 'next' into ui 2022-08-31 15:06:53 +02:00
Andras Bacsai
4ac0df71b1 Merge pull request #581 from ArticaDev/main
More responsiveness improvements to UI
2022-08-31 15:06:23 +02:00
Andras Bacsai
dbd948867c fix: loading state on start 2022-08-31 15:03:36 +02:00
Andras Bacsai
a9b5cd6c31 fix: glitchtip things 2022-08-31 15:03:27 +02:00
Andras Bacsai
92f513d514 feat: restart application 2022-08-31 15:03:04 +02:00
Andras Bacsai
b239d21961 chore: version++ 2022-08-31 15:02:36 +02:00
Andras Bacsai
40e8dd4a8d feat: new service - weblate 2022-08-31 15:02:05 +02:00
Andras Bacsai
a667435ef2 fix contribution guide 2022-08-31 13:22:54 +02:00
Andras Bacsai
042b4e7587 typo 2022-08-31 11:39:20 +02:00
Andras Bacsai
c46a1b4a59 i18n converter 2022-08-31 11:36:34 +02:00
LL
e6035d5479 improving localdocker responsiveness 2022-08-31 01:05:44 -03:00
LL
008d090093 improving identity and access responsiveness 2022-08-31 01:03:27 -03:00
LL
6ff080c36b improving github page responsiveness 2022-08-31 01:02:33 -03:00
Andras Bacsai
086ca30323 fix: oh god Prisma 2022-08-30 15:15:57 +00:00
Andras Bacsai
17f3ecbbcb Merge pull request #579 from coollabsio/next
v3.8.8
2022-08-30 17:00:22 +02:00
Andras Bacsai
1f2c8c4ad2 chore: version++ 2022-08-30 14:59:16 +00:00
Andras Bacsai
bc03331c66 revert prisma 2022-08-30 14:58:52 +00:00
95 changed files with 3599 additions and 2019 deletions

View File

@@ -2,7 +2,7 @@ name: production-release
on: on:
release: release:
types: [published] types: [released]
jobs: jobs:
making-something-cool: making-something-cool:

39
.github/workflows/release-candidate.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: release-candidate
on:
release:
types: [prereleased]
jobs:
making-something-cool:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: 'next'
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get current package version
uses: martinbeentjes/npm-get-version-action@v1.2.3
id: package-version
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: coollabsio/coolify:${{github.event.release.name}}
cache-from: type=registry,ref=coollabsio/coolify:buildcache-rc
cache-to: type=registry,ref=coollabsio/coolify:buildcache-rc,mode=max
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}

View File

@@ -125,7 +125,7 @@ If you are finished with the Prisma schema, you should update the database schem
Supported versions are hardcoded into Coolify (for now). Supported versions are hardcoded into Coolify (for now).
You need to update `supportedServiceTypesAndVersions` function at [src/apps/api/src/lib/supportedVersions.ts](src/apps/api/src/lib/supportedVersions.ts). Example JSON: You need to update `supportedServiceTypesAndVersions` function at [apps/api/src/lib/services/supportedVersions.ts](apps/api/src/lib/services/supportedVersions.ts). Example JSON:
```js ```js
{ {
@@ -208,22 +208,22 @@ export const umami = [{
4. Add service deletion query to `removeService` function in [apps/api/src/lib/services/common.ts](apps/api/src/lib/services/common.ts) 4. Add service deletion query to `removeService` function in [apps/api/src/lib/services/common.ts](apps/api/src/lib/services/common.ts)
5. Add start process for the new service in [apps/api/src/lib/services/handlers.ts](apps/api/src/lib/services/handlers.ts)
5. You need to add start process for the new service in [apps/api/src/routes/api/v1/services/handlers.ts](apps/api/src/routes/api/v1/services/handlers.ts)
> See startUmamiService() function as example. > See startUmamiService() function as example.
6. Add the newly added start process to `startService` in [apps/api/src/routes/api/v1/services/handlers.ts](apps/api/src/routes/api/v1/services/handlers.ts)
6. You need to add a custom logo at [apps/ui/src/lib/components/svg/services](apps/ui/src/lib/components/svg/services) as a svelte component and export it in [apps/ui/src/lib/components/svg/services/index.ts](apps/ui/src/lib/components/svg/services/index.ts) 7. You need to add a custom logo at [apps/ui/src/lib/components/svg/services](apps/ui/src/lib/components/svg/services) as a svelte component and export it in [apps/ui/src/lib/components/svg/services/index.ts](apps/ui/src/lib/components/svg/services/index.ts)
SVG is recommended, but you can use PNG as well. It should have the `isAbsolute` variable with the suitable CSS classes, primarily for sizing and positioning. SVG is recommended, but you can use PNG as well. It should have the `isAbsolute` variable with the suitable CSS classes, primarily for sizing and positioning.
7. You need to include it the logo at: 8. You need to include it the logo at:
- [apps/ui/src/lib/components/svg/services/ServiceIcons.svelte](apps/ui/src/lib/components/svg/services/ServiceIcons.svelte) with `isAbsolute`. - [apps/ui/src/lib/components/svg/services/ServiceIcons.svelte](apps/ui/src/lib/components/svg/services/ServiceIcons.svelte) with `isAbsolute`.
- [apps/ui/src/routes/services/[id]/_ServiceLinks.svelte](apps/ui/src/routes/services/[id]/_ServiceLinks.svelte) with the link to the docs/main site of the service - [apps/ui/src/routes/services/[id]/_ServiceLinks.svelte](apps/ui/src/routes/services/[id]/_ServiceLinks.svelte) with the link to the docs/main site of the service
8. By default the URL and the name frontend forms are included in [apps/ui/src/routes/services/[id]/_Services/_Services.svelte](apps/ui/src/routes/services/[id]/_Services/_Services.svelte). 9. By default the URL and the name frontend forms are included in [apps/ui/src/routes/services/[id]/_Services/_Services.svelte](apps/ui/src/routes/services/[id]/_Services/_Services.svelte).
If you need to show more details on the frontend, such as users/passwords, you need to add Svelte component to [apps/ui/src/routes/services/[id]/_Services](apps/ui/src/routes/services/[id]/_Services) with an underscore. If you need to show more details on the frontend, such as users/passwords, you need to add Svelte component to [apps/ui/src/routes/services/[id]/_Services](apps/ui/src/routes/services/[id]/_Services) with an underscore.

View File

@@ -14,14 +14,14 @@ WORKDIR /app
ENV NODE_ENV production ENV NODE_ENV production
ARG TARGETPLATFORM ARG TARGETPLATFORM
ENV PRISMA_QUERY_ENGINE_BINARY=/app/prisma-engines/query-engine \ # ENV PRISMA_QUERY_ENGINE_BINARY=/app/prisma-engines/query-engine \
PRISMA_MIGRATION_ENGINE_BINARY=/app/prisma-engines/migration-engine \ # PRISMA_MIGRATION_ENGINE_BINARY=/app/prisma-engines/migration-engine \
PRISMA_INTROSPECTION_ENGINE_BINARY=/app/prisma-engines/introspection-engine \ # PRISMA_INTROSPECTION_ENGINE_BINARY=/app/prisma-engines/introspection-engine \
PRISMA_FMT_BINARY=/app/prisma-engines/prisma-fmt \ # PRISMA_FMT_BINARY=/app/prisma-engines/prisma-fmt \
PRISMA_CLI_QUERY_ENGINE_TYPE=binary \ # PRISMA_CLI_QUERY_ENGINE_TYPE=binary \
PRISMA_CLIENT_ENGINE_TYPE=binary # PRISMA_CLIENT_ENGINE_TYPE=binary
COPY --from=coollabsio/prisma-engine:4.2.0 /prisma-engines/query-engine /prisma-engines/migration-engine /prisma-engines/introspection-engine /prisma-engines/prisma-fmt /app/prisma-engines/ # COPY --from=coollabsio/prisma-engine:3.15 /prisma-engines/query-engine /prisma-engines/migration-engine /prisma-engines/introspection-engine /prisma-engines/prisma-fmt /app/prisma-engines/
RUN apk add --no-cache git git-lfs openssh-client curl jq cmake sqlite openssl psmisc RUN apk add --no-cache git git-lfs openssh-client curl jq cmake sqlite openssl psmisc
RUN curl -sL https://unpkg.com/@pnpm/self-installer | node RUN curl -sL https://unpkg.com/@pnpm/self-installer | node

View File

@@ -23,7 +23,7 @@
"@fastify/static": "6.5.0", "@fastify/static": "6.5.0",
"@iarna/toml": "2.2.5", "@iarna/toml": "2.2.5",
"@ladjs/graceful": "3.0.2", "@ladjs/graceful": "3.0.2",
"@prisma/client": "4.2.1", "@prisma/client": "3.15.2",
"axios": "0.27.2", "axios": "0.27.2",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"bree": "9.1.2", "bree": "9.1.2",
@@ -62,7 +62,7 @@
"eslint-plugin-prettier": "4.2.1", "eslint-plugin-prettier": "4.2.1",
"nodemon": "2.0.19", "nodemon": "2.0.19",
"prettier": "2.7.1", "prettier": "2.7.1",
"prisma": "4.2.1", "prisma": "3.15.2",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"tsconfig-paths": "4.1.0", "tsconfig-paths": "4.1.0",
"typescript": "4.7.4" "typescript": "4.7.4"

View File

@@ -0,0 +1,18 @@
-- CreateTable
CREATE TABLE "Weblate" (
"id" TEXT NOT NULL PRIMARY KEY,
"adminPassword" TEXT NOT NULL,
"postgresqlHost" TEXT NOT NULL,
"postgresqlPort" INTEGER NOT NULL,
"postgresqlUser" TEXT NOT NULL,
"postgresqlPassword" TEXT NOT NULL,
"postgresqlDatabase" TEXT NOT NULL,
"postgresqlPublicPort" INTEGER,
"serviceId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Weblate_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "Weblate_serviceId_key" ON "Weblate"("serviceId");

View File

@@ -0,0 +1,23 @@
-- CreateTable
CREATE TABLE "Taiga" (
"id" TEXT NOT NULL PRIMARY KEY,
"secretKey" TEXT NOT NULL,
"erlangSecret" TEXT NOT NULL,
"djangoAdminPassword" TEXT NOT NULL,
"djangoAdminUser" TEXT NOT NULL,
"rabbitMQUser" TEXT NOT NULL,
"rabbitMQPassword" TEXT NOT NULL,
"postgresqlHost" TEXT NOT NULL,
"postgresqlPort" INTEGER NOT NULL,
"postgresqlUser" TEXT NOT NULL,
"postgresqlPassword" TEXT NOT NULL,
"postgresqlDatabase" TEXT NOT NULL,
"postgresqlPublicPort" INTEGER,
"serviceId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Taiga_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "Taiga_serviceId_key" ON "Taiga"("serviceId");

View File

@@ -1,6 +1,6 @@
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
binaryTargets = ["native", "linux-musl"] binaryTargets = ["native"]
} }
datasource db { datasource db {
@@ -200,7 +200,7 @@ model Build {
commit String? commit String?
pullmergeRequestId String? pullmergeRequestId String?
forceRebuild Boolean @default(false) forceRebuild Boolean @default(false)
sourceBranch String? sourceBranch String?
branch String? branch String?
status String? @default("queued") status String? @default("queued")
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@ -348,6 +348,8 @@ model Service {
wordpress Wordpress? wordpress Wordpress?
appwrite Appwrite? appwrite Appwrite?
searxng Searxng? searxng Searxng?
weblate Weblate?
taiga Taiga?
} }
model PlausibleAnalytics { model PlausibleAnalytics {
@@ -559,3 +561,38 @@ model Searxng {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id]) service Service @relation(fields: [serviceId], references: [id])
} }
model Weblate {
id String @id @default(cuid())
adminPassword String
postgresqlHost String
postgresqlPort Int
postgresqlUser String
postgresqlPassword String
postgresqlDatabase String
postgresqlPublicPort Int?
serviceId String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
}
model Taiga {
id String @id @default(cuid())
secretKey String
erlangSecret String
djangoAdminPassword String
djangoAdminUser String
rabbitMQUser String
rabbitMQPassword String
postgresqlHost String
postgresqlPort Int
postgresqlUser String
postgresqlPassword String
postgresqlDatabase String
postgresqlPublicPort Int?
serviceId String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
}

View File

@@ -3,9 +3,6 @@ import axios from 'axios';
import { compareVersions } from 'compare-versions'; import { compareVersions } from 'compare-versions';
import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, listSettings, version } from '../lib/common'; import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, listSettings, version } from '../lib/common';
async function disconnect() {
await prisma.$disconnect();
}
async function autoUpdater() { async function autoUpdater() {
try { try {
const currentVersion = version; const currentVersion = version;
@@ -27,6 +24,9 @@ async function autoUpdater() {
console.log(`Updating Coolify to ${latestVersion}.`); console.log(`Updating Coolify to ${latestVersion}.`);
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`); await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
await asyncExecShell(`env | grep COOLIFY > .env`); await asyncExecShell(`env | grep COOLIFY > .env`);
await asyncExecShell(
`sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=true' .env`
);
await asyncExecShell( await asyncExecShell(
`docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify && docker rm coolify && docker compose up -d --force-recreate"` `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify && docker rm coolify && docker compose up -d --force-recreate"`
); );

View File

@@ -21,7 +21,7 @@ import { scheduler } from './scheduler';
import { supportedServiceTypesAndVersions } from './services/supportedVersions'; import { supportedServiceTypesAndVersions } from './services/supportedVersions';
import { includeServices } from './services/common'; import { includeServices } from './services/common';
export const version = '3.8.7'; export const version = '3.9.0-rc.1';
export const isDev = process.env.NODE_ENV === 'development'; export const isDev = process.env.NODE_ENV === 'development';
const algorithm = 'aes-256-ctr'; const algorithm = 'aes-256-ctr';
@@ -139,10 +139,10 @@ export const prisma = new PrismaClient({
}); });
// prisma.$on('query', (e) => { // prisma.$on('query', (e) => {
// console.log({e}) // console.log({e})
// console.log('Query: ' + e.query) // console.log('Query: ' + e.query)
// console.log('Params: ' + e.params) // console.log('Params: ' + e.params)
// console.log('Duration: ' + e.duration + 'ms') // console.log('Duration: ' + e.duration + 'ms')
// }) // })
export const base64Encode = (text: string): string => { export const base64Encode = (text: string): string => {
return Buffer.from(text).toString('base64'); return Buffer.from(text).toString('base64');
@@ -1089,7 +1089,6 @@ export async function checkExposedPort({ id, configuredPort, exposePort, dockerI
if (exposePort < 1024 || exposePort > 65535) { if (exposePort < 1024 || exposePort > 65535) {
throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` } throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` }
} }
if (configuredPort) { if (configuredPort) {
if (configuredPort !== exposePort) { if (configuredPort !== exposePort) {
const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress); const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress);
@@ -1309,6 +1308,9 @@ export function saveUpdateableFields(type: string, data: any) {
temp = Boolean(temp) temp = Boolean(temp)
} }
} }
if (k.isNumber && temp === '') {
temp = null
}
update[k.name] = temp update[k.name] = temp
}); });
} }
@@ -1351,9 +1353,9 @@ export const getServiceMainPort = (service: string) => {
export function makeLabelForServices(type) { export function makeLabelForServices(type) {
return [ return [
'coolify.managed=true', 'coolify.managed=true',
`coolify.version = ${version}`, `coolify.version=${version}`,
`coolify.type = service`, `coolify.type=service`,
`coolify.service.type = ${type}` `coolify.service.type=${type}`
]; ];
} }
export function errorHandler({ status = 500, message = 'Unknown error.' }: { status: number, message: string | any }) { export function errorHandler({ status = 500, message = 'Unknown error.' }: { status: number, message: string | any }) {
@@ -1473,14 +1475,25 @@ export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) {
} }
export function persistentVolumes(id, persistentStorage, config) { export function persistentVolumes(id, persistentStorage, config) {
let volumeSet = new Set();
if (Object.keys(config).length > 0) {
for (const [key, value] of Object.entries(config)) {
if (value.volumes) {
for (const volume of value.volumes) {
volumeSet.add(volume);
}
}
}
}
const volumesArray = Array.from(volumeSet);
const persistentVolume = const persistentVolume =
persistentStorage?.map((storage) => { persistentStorage?.map((storage) => {
return `${id}${storage.path.replace(/\//gi, '-')}:${storage.path}`; return `${id}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
}) || []; }) || [];
let volumes = [...persistentVolume] let volumes = [...persistentVolume]
if (config.volume) volumes = [config.volume, ...volumes] if (volumesArray) volumes = [...volumesArray, ...volumes]
const composeVolumes = volumes.length > 0 && volumes.map((volume) => { const composeVolumes = volumes.length > 0 && volumes.map((volume) => {
return { return {
[`${volume.split(':')[0]}`]: { [`${volume.split(':')[0]}`]: {
@@ -1489,16 +1502,11 @@ export function persistentVolumes(id, persistentStorage, config) {
}; };
}) || [] }) || []
const volumeMounts = config.volume && Object.assign( const volumeMounts = Object.assign(
{}, {},
{
[config.volume.split(':')[0]]: {
name: config.volume.split(':')[0]
}
},
...composeVolumes ...composeVolumes
) || {} ) || {}
return { volumes, volumeMounts } return { volumeMounts }
} }
export function defaultComposeConfiguration(network: string): any { export function defaultComposeConfiguration(network: string): any {
return { return {
@@ -1515,26 +1523,26 @@ export function defaultComposeConfiguration(network: string): any {
} }
} }
export function decryptApplication(application: any) { export function decryptApplication(application: any) {
if (application) { if (application) {
if (application?.gitSource?.githubApp?.clientSecret) { if (application?.gitSource?.githubApp?.clientSecret) {
application.gitSource.githubApp.clientSecret = decrypt(application.gitSource.githubApp.clientSecret) || null; application.gitSource.githubApp.clientSecret = decrypt(application.gitSource.githubApp.clientSecret) || null;
} }
if (application?.gitSource?.githubApp?.webhookSecret) { if (application?.gitSource?.githubApp?.webhookSecret) {
application.gitSource.githubApp.webhookSecret = decrypt(application.gitSource.githubApp.webhookSecret) || null; application.gitSource.githubApp.webhookSecret = decrypt(application.gitSource.githubApp.webhookSecret) || null;
} }
if (application?.gitSource?.githubApp?.privateKey) { if (application?.gitSource?.githubApp?.privateKey) {
application.gitSource.githubApp.privateKey = decrypt(application.gitSource.githubApp.privateKey) || null; application.gitSource.githubApp.privateKey = decrypt(application.gitSource.githubApp.privateKey) || null;
} }
if (application?.gitSource?.gitlabApp?.appSecret) { if (application?.gitSource?.gitlabApp?.appSecret) {
application.gitSource.gitlabApp.appSecret = decrypt(application.gitSource.gitlabApp.appSecret) || null; application.gitSource.gitlabApp.appSecret = decrypt(application.gitSource.gitlabApp.appSecret) || null;
} }
if (application?.secrets.length > 0) { if (application?.secrets.length > 0) {
application.secrets = application.secrets.map((s: any) => { application.secrets = application.secrets.map((s: any) => {
s.value = decrypt(s.value) || null s.value = decrypt(s.value) || null
return s; return s;
}); });
} }
return application; return application;
} }
} }

View File

@@ -1,23 +1,7 @@
import { exec } from 'node:child_process'
import util from 'util';
import fs from 'fs/promises';
import yaml from 'js-yaml';
import forge from 'node-forge';
import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator';
import type { Config } from 'unique-names-generator';
import generator from 'generate-password';
import crypto from 'crypto';
import { promises as dns } from 'dns';
import { PrismaClient } from '@prisma/client';
import cuid from 'cuid'; import cuid from 'cuid';
import os from 'os';
import sshConfig from 'ssh-config'
import { encrypt, generatePassword, prisma } from '../common'; import { encrypt, generatePassword, prisma } from '../common';
export const version = '3.8.2';
export const isDev = process.env.NODE_ENV === 'development';
export const includeServices: any = { export const includeServices: any = {
destinationDocker: true, destinationDocker: true,
persistentStorage: true, persistentStorage: true,
@@ -34,7 +18,9 @@ export const includeServices: any = {
moodle: true, moodle: true,
appwrite: true, appwrite: true,
glitchTip: true, glitchTip: true,
searxng: true searxng: true,
weblate: true,
taiga: true
}; };
export async function configureServiceType({ export async function configureServiceType({
id, id,
@@ -312,6 +298,58 @@ export async function configureServiceType({
} }
} }
}); });
} else if (type === 'weblate') {
const adminPassword = encrypt(generatePassword({}))
const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword({}));
const postgresqlDatabase = 'weblate';
await prisma.service.update({
where: { id },
data: {
type,
weblate: {
create: {
adminPassword,
postgresqlHost: `${id}-postgresql`,
postgresqlPort: 5432,
postgresqlUser,
postgresqlPassword,
postgresqlDatabase,
}
}
}
});
} else if (type === 'taiga') {
const secretKey = encrypt(generatePassword({}))
const erlangSecret = encrypt(generatePassword({}))
const rabbitMQUser = cuid();
const djangoAdminUser = cuid();
const djangoAdminPassword = encrypt(generatePassword({}))
const rabbitMQPassword = encrypt(generatePassword({}))
const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword({}));
const postgresqlDatabase = 'taiga';
await prisma.service.update({
where: { id },
data: {
type,
taiga: {
create: {
secretKey,
erlangSecret,
djangoAdminUser,
djangoAdminPassword,
rabbitMQUser,
rabbitMQPassword,
postgresqlHost: `${id}-postgresql`,
postgresqlPort: 5432,
postgresqlUser,
postgresqlPassword,
postgresqlDatabase,
}
}
}
});
} else { } else {
await prisma.service.update({ await prisma.service.update({
where: { id }, where: { id },
@@ -338,7 +376,8 @@ export async function removeService({ id }: { id: string }): Promise<void> {
await prisma.moodle.deleteMany({ where: { serviceId: id } }); await prisma.moodle.deleteMany({ where: { serviceId: id } });
await prisma.appwrite.deleteMany({ where: { serviceId: id } }); await prisma.appwrite.deleteMany({ where: { serviceId: id } });
await prisma.searxng.deleteMany({ where: { serviceId: id } }); await prisma.searxng.deleteMany({ where: { serviceId: id } });
await prisma.weblate.deleteMany({ where: { serviceId: id } });
await prisma.taiga.deleteMany({ where: { serviceId: id } });
await prisma.service.delete({ where: { id } }); await prisma.service.delete({ where: { id } });
} }

File diff suppressed because it is too large Load Diff

View File

@@ -599,6 +599,54 @@ export const glitchTip = [{
isBoolean: false, isBoolean: false,
isEncrypted: true isEncrypted: true
}, },
{
name: 'emailSmtpHost',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'emailSmtpPassword',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'emailSmtpUseSsl',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: true,
isEncrypted: false
},
{
name: 'emailSmtpUseSsl',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: true,
isEncrypted: false
},
{
name: 'emailSmtpPort',
isEditable: true,
isLowerCase: false,
isNumber: true,
isBoolean: false,
isEncrypted: false
},
{
name: 'emailSmtpUser',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{ {
name: 'defaultEmail', name: 'defaultEmail',
isEditable: false, isEditable: false,
@@ -624,7 +672,7 @@ export const glitchTip = [{
isEncrypted: true isEncrypted: true
}, },
{ {
name: 'defaultFromEmail', name: 'defaultEmailFrom',
isEditable: true, isEditable: true,
isLowerCase: false, isLowerCase: false,
isNumber: false, isNumber: false,
@@ -687,4 +735,133 @@ export const searxng = [{
isNumber: false, isNumber: false,
isBoolean: false, isBoolean: false,
isEncrypted: true isEncrypted: true
}]
export const weblate = [{
name: 'adminPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'postgresqlHost',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'postgresqlPort',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'postgresqlUser',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'postgresqlPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'postgresqlDatabase',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
}]
export const taiga = [{
name: 'secretKey',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'djangoAdminUser',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'djangoAdminPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'rabbitMQUser',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'rabbitMQPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'postgresqlHost',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'postgresqlPort',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'postgresqlUser',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'postgresqlPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'postgresqlDatabase',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
}] }]

View File

@@ -190,4 +190,26 @@ export const supportedServiceTypesAndVersions = [
main: 8080 main: 8080
} }
}, },
{
name: 'weblate',
fancyName: 'Weblate',
baseImage: 'weblate/weblate',
images: ['postgres:14-alpine', 'redis:6-alpine'],
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 8080
}
},
// {
// name: 'taiga',
// fancyName: 'Taiga',
// baseImage: 'taigaio/taiga-front',
// images: ['postgres:12.3', 'rabbitmq:3.8-management-alpine', 'taigaio/taiga-back', 'taigaio/taiga-events', 'taigaio/taiga-protected'],
// versions: ['latest'],
// recommendedVersion: 'latest',
// ports: {
// main: 80
// }
// },
]; ];

View File

@@ -3,16 +3,23 @@ import crypto from 'node:crypto'
import jsonwebtoken from 'jsonwebtoken'; import jsonwebtoken from 'jsonwebtoken';
import axios from 'axios'; import axios from 'axios';
import { FastifyReply } from 'fastify'; import { FastifyReply } from 'fastify';
import fs from 'fs/promises';
import yaml from 'js-yaml';
import { day } from '../../../../lib/dayjs'; import { day } from '../../../../lib/dayjs';
import { setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common'; import { makeLabelForStandaloneApplication, setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common';
import { checkDomainsIsValidInDNS, checkDoubleBranch, checkExposedPort, decrypt, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, getFreeExposedPort, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common'; import { checkDomainsIsValidInDNS, checkDoubleBranch, checkExposedPort, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common';
import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker'; import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker';
import { scheduler } from '../../../../lib/scheduler';
import type { FastifyRequest } from 'fastify'; import type { FastifyRequest } from 'fastify';
import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication } from './types'; import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication } from './types';
import { OnlyId } from '../../../../types'; import { OnlyId } from '../../../../types';
function filterObject(obj, callback) {
return Object.fromEntries(Object.entries(obj).
filter(([key, val]) => callback(val, key)));
}
export async function listApplications(request: FastifyRequest) { export async function listApplications(request: FastifyRequest) {
try { try {
const { teamId } = request.user const { teamId } = request.user
@@ -176,7 +183,7 @@ export async function getApplicationFromDB(id: string, teamId: string) {
} }
export async function getApplicationFromDBWebhook(projectId: number, branch: string) { export async function getApplicationFromDBWebhook(projectId: number, branch: string) {
try { try {
let application = await prisma.application.findFirst({ let applications = await prisma.application.findMany({
where: { projectId, branch, settings: { autodeploy: true } }, where: { projectId, branch, settings: { autodeploy: true } },
include: { include: {
destinationDocker: true, destinationDocker: true,
@@ -186,22 +193,28 @@ export async function getApplicationFromDBWebhook(projectId: number, branch: str
persistentStorage: true persistentStorage: true
} }
}); });
if (!application) { if (applications.length === 0) {
throw { status: 500, message: 'Application not configured.' } throw { status: 500, message: 'Application not configured.' }
} }
application = decryptApplication(application); applications = applications.map((application: any) => {
const { baseImage, baseBuildImage, baseBuildImages, baseImages } = setDefaultBaseImage( application = decryptApplication(application);
application.buildPack const { baseImage, baseBuildImage, baseBuildImages, baseImages } = setDefaultBaseImage(
); application.buildPack
);
// Set default build images // Set default build images
if (!application.baseImage) { if (!application.baseImage) {
application.baseImage = baseImage; application.baseImage = baseImage;
} }
if (!application.baseBuildImage) { if (!application.baseBuildImage) {
application.baseBuildImage = baseBuildImage; application.baseBuildImage = baseBuildImage;
} }
return { ...application, baseBuildImages, baseImages }; application.baseBuildImages = baseBuildImages;
application.baseImages = baseImages;
return application
})
return applications;
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
@@ -236,8 +249,8 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
exposePort = Number(exposePort); exposePort = Number(exposePort);
} }
const { destinationDocker: { id: dockerId, remoteIpAddress } } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } }) const { destinationDocker: { id: dockerId, remoteIpAddress }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } })
if (exposePort) await checkExposedPort({ id, exposePort, dockerId, remoteIpAddress }) if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, dockerId, remoteIpAddress })
if (denoOptions) denoOptions = denoOptions.trim(); if (denoOptions) denoOptions = denoOptions.trim();
const defaultConfiguration = await setDefaultConfiguration({ const defaultConfiguration = await setDefaultConfiguration({
buildPack, buildPack,
@@ -277,11 +290,11 @@ export async function saveApplicationSettings(request: FastifyRequest<SaveApplic
try { try {
const { id } = request.params const { id } = request.params
const { debug, previews, dualCerts, autodeploy, branch, projectId, isBot } = request.body const { debug, previews, dualCerts, autodeploy, branch, projectId, isBot } = request.body
const isDouble = await checkDoubleBranch(branch, projectId); // const isDouble = await checkDoubleBranch(branch, projectId);
if (isDouble && autodeploy) { // if (isDouble && autodeploy) {
await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false } }) // await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false } })
throw { status: 500, message: 'Cannot activate automatic deployments until only one application is defined for this repository / branch.' } // throw { status: 500, message: 'Cannot activate automatic deployments until only one application is defined for this repository / branch.' }
} // }
await prisma.application.update({ await prisma.application.update({
where: { id }, where: { id },
data: { fqdn: isBot ? null : undefined, settings: { update: { debug, previews, dualCerts, autodeploy, isBot } } }, data: { fqdn: isBot ? null : undefined, settings: { update: { debug, previews, dualCerts, autodeploy, isBot } } },
@@ -312,6 +325,113 @@ export async function stopPreviewApplication(request: FastifyRequest<StopPreview
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function restartApplication(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
try {
const { id } = request.params
const { teamId } = request.user
let application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId) {
const buildId = cuid();
const { id: dockerId, network } = application.destinationDocker;
const { secrets, pullmergeRequestId, port, repository, persistentStorage, id: applicationId, buildPack, exposePort } = application;
const envs = [
`PORT=${port}`
];
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
envs.push(`${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
envs.push(`${secret.name}=${secret.value}`);
}
}
});
}
const { workdir } = await createDirectories({ repository, buildId });
const labels = []
let image = null
const { stdout: container } = await executeDockerCmd({ dockerId, command: `docker container ls --filter 'label=com.docker.compose.service=${id}' --format '{{json .}}'` })
const containersArray = container.trim().split('\n');
for (const container of containersArray) {
const containerObj = formatLabelsOnDocker(container);
image = containerObj[0].Image
Object.keys(containerObj[0].Labels).forEach(function (key) {
if (key.startsWith('coolify')) {
labels.push(`${key}=${containerObj[0].Labels[key]}`)
}
})
}
let imageFound = false;
try {
await executeDockerCmd({
dockerId,
command: `docker image inspect ${image}`
})
imageFound = true;
} catch (error) {
//
}
if (!imageFound) {
throw { status: 500, message: 'Image not found, cannot restart application.' }
}
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
let envFound = false;
try {
envFound = !!(await fs.stat(`${workdir}/.env`));
} catch (error) {
//
}
const volumes =
persistentStorage?.map((storage) => {
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${buildPack !== 'docker' ? '/app' : ''
}${storage.path}`;
}) || [];
const composeVolumes = volumes.map((volume) => {
return {
[`${volume.split(':')[0]}`]: {
name: volume.split(':')[0]
}
};
});
const composeFile = {
version: '3.8',
services: {
[applicationId]: {
image,
container_name: applicationId,
volumes,
env_file: envFound ? [`${workdir}/.env`] : [],
labels,
depends_on: [],
expose: [port],
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
...defaultComposeConfiguration(network),
}
},
networks: {
[network]: {
external: true
}
},
volumes: Object.assign({}, ...composeVolumes)
};
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` })
await executeDockerCmd({ dockerId, command: `docker rm ${id}` })
await executeDockerCmd({ dockerId, command: `docker compose --project-directory ${workdir} up -d` })
return reply.code(201).send();
}
throw { status: 500, message: 'Application cannot be restarted.' }
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function stopApplication(request: FastifyRequest<OnlyId>, reply: FastifyReply) { export async function stopApplication(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
try { try {
const { id } = request.params const { id } = request.params
@@ -332,12 +452,14 @@ export async function stopApplication(request: FastifyRequest<OnlyId>, reply: Fa
export async function deleteApplication(request: FastifyRequest<DeleteApplication>, reply: FastifyReply) { export async function deleteApplication(request: FastifyRequest<DeleteApplication>, reply: FastifyReply) {
try { try {
const { id } = request.params const { id } = request.params
const { force } = request.body
const { teamId } = request.user const { teamId } = request.user
const application = await prisma.application.findUnique({ const application = await prisma.application.findUnique({
where: { id }, where: { id },
include: { destinationDocker: true } include: { destinationDocker: true }
}); });
if (application?.destinationDockerId && application.destinationDocker?.network) { if (!force && application?.destinationDockerId && application.destinationDocker?.network) {
const { stdout: containers } = await executeDockerCmd({ const { stdout: containers } = await executeDockerCmd({
dockerId: application.destinationDocker.id, dockerId: application.destinationDocker.id,
command: `docker ps -a --filter network=${application.destinationDocker.network} --filter name=${id} --format '{{json .}}'` command: `docker ps -a --filter network=${application.destinationDocker.network} --filter name=${id} --format '{{json .}}'`
@@ -558,12 +680,12 @@ export async function saveRepository(request, reply) {
data: { repository, branch, projectId, settings: { update: { autodeploy, isPublicRepository } } } data: { repository, branch, projectId, settings: { update: { autodeploy, isPublicRepository } } }
}); });
} }
if (!isPublicRepository) { // if (!isPublicRepository) {
const isDouble = await checkDoubleBranch(branch, projectId); // const isDouble = await checkDoubleBranch(branch, projectId);
if (isDouble) { // if (isDouble) {
await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false, isPublicRepository } }) // await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false, isPublicRepository } })
} // }
} // }
return reply.code(201).send() return reply.code(201).send()
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
@@ -860,9 +982,12 @@ export async function getBuildIdLogs(request: FastifyRequest<GetBuildIdLogs>) {
where: { buildId, time: { gt: sequence } }, where: { buildId, time: { gt: sequence } },
orderBy: { time: 'asc' } orderBy: { time: 'asc' }
}); });
const data = await prisma.build.findFirst({ where: { id: buildId } }); const data = await prisma.build.findFirst({ where: { id: buildId } });
const createdAt = day(data.createdAt).utc();
return { return {
logs, logs,
took: day().diff(createdAt) / 1000,
status: data?.status || 'queued' status: data?.status || 'queued'
} }
} catch ({ status, message }) { } catch ({ status, message }) {

View File

@@ -1,6 +1,6 @@
import { FastifyPluginAsync } from 'fastify'; import { FastifyPluginAsync } from 'fastify';
import { OnlyId } from '../../../../types'; import { OnlyId } from '../../../../types';
import { cancelDeployment, checkDNS, checkDomain, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildLogs, getBuildPack, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getSecrets, getStorages, getUsage, listApplications, newApplication, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication } from './handlers'; import { cancelDeployment, checkDNS, checkDomain, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildLogs, getBuildPack, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getSecrets, getStorages, getUsage, listApplications, newApplication, restartApplication, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication } from './handlers';
import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, GetImages, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types'; import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, GetImages, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types';
@@ -19,6 +19,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get<OnlyId>('/:id/status', async (request) => await getApplicationStatus(request)); fastify.get<OnlyId>('/:id/status', async (request) => await getApplicationStatus(request));
fastify.post<OnlyId>('/:id/restart', async (request, reply) => await restartApplication(request, reply));
fastify.post<OnlyId>('/:id/stop', async (request, reply) => await stopApplication(request, reply)); fastify.post<OnlyId>('/:id/stop', async (request, reply) => await stopApplication(request, reply));
fastify.post<StopPreviewApplication>('/:id/stop/preview', async (request, reply) => await stopPreviewApplication(request, reply)); fastify.post<StopPreviewApplication>('/:id/stop/preview', async (request, reply) => await stopPreviewApplication(request, reply));

View File

@@ -29,6 +29,7 @@ export interface SaveApplicationSettings extends OnlyId {
} }
export interface DeleteApplication extends OnlyId { export interface DeleteApplication extends OnlyId {
Querystring: { domain: string; }; Querystring: { domain: string; };
Body: { force: boolean }
} }
export interface CheckDomain extends OnlyId { export interface CheckDomain extends OnlyId {
Querystring: { domain: string; }; Querystring: { domain: string; };

View File

@@ -7,7 +7,7 @@ import { ComposeFile, createDirectories, decrypt, encrypt, errorHandler, execute
import { day } from '../../../../lib/dayjs'; import { day } from '../../../../lib/dayjs';
import { GetDatabaseLogs, OnlyId, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSettings, SaveVersion } from '../../../../types'; import { GetDatabaseLogs, OnlyId, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSettings, SaveVersion } from '../../../../types';
import { SaveDatabaseType } from './types'; import { DeleteDatabase, SaveDatabaseType } from './types';
export async function listDatabases(request: FastifyRequest) { export async function listDatabases(request: FastifyRequest) {
try { try {
@@ -167,6 +167,7 @@ export async function saveDatabaseDestination(request: FastifyRequest<SaveDataba
const { id } = request.params; const { id } = request.params;
const { destinationId } = request.body; const { destinationId } = request.body;
const { arch } = await listSettings();
await prisma.database.update({ await prisma.database.update({
where: { id }, where: { id },
data: { destinationDocker: { connect: { id: destinationId } } } data: { destinationDocker: { connect: { id: destinationId } } }
@@ -181,7 +182,7 @@ export async function saveDatabaseDestination(request: FastifyRequest<SaveDataba
if (destinationDockerId) { if (destinationDockerId) {
if (type && version) { if (type && version) {
const baseImage = getDatabaseImage(type); const baseImage = getDatabaseImage(type, arch);
executeDockerCmd({ dockerId, command: `docker pull ${baseImage}:${version}` }) executeDockerCmd({ dockerId, command: `docker pull ${baseImage}:${version}` })
} }
} }
@@ -360,19 +361,22 @@ export async function getDatabaseLogs(request: FastifyRequest<GetDatabaseLogs>)
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function deleteDatabase(request: FastifyRequest<OnlyId>) { export async function deleteDatabase(request: FastifyRequest<DeleteDatabase>) {
try { try {
const teamId = request.user.teamId; const teamId = request.user.teamId;
const { id } = request.params; const { id } = request.params;
const { force } = request.body;
const database = await prisma.database.findFirst({ const database = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } }, where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true } include: { destinationDocker: true, settings: true }
}); });
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword); if (!force) {
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword); if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
if (database.destinationDockerId) { if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
const everStarted = await stopDatabaseContainer(database); if (database.destinationDockerId) {
if (everStarted) await stopTcpHttpProxy(id, database.destinationDocker, database.publicPort); const everStarted = await stopDatabaseContainer(database);
if (everStarted) await stopTcpHttpProxy(id, database.destinationDocker, database.publicPort);
}
} }
await prisma.databaseSettings.deleteMany({ where: { databaseId: id } }); await prisma.databaseSettings.deleteMany({ where: { databaseId: id } });
await prisma.database.delete({ where: { id } }); await prisma.database.delete({ where: { id } });
@@ -436,7 +440,7 @@ export async function saveDatabaseSettings(request: FastifyRequest<SaveDatabaseS
let publicPort = null let publicPort = null
const { destinationDocker: { id: dockerId } } = await prisma.database.findUnique({ where: { id }, include: { destinationDocker: true } }) const { destinationDocker: { id: dockerId } } = await prisma.database.findUnique({ where: { id }, include: { destinationDocker: true } })
if (isPublic) { if (isPublic) {
publicPort = await getFreePublicPort(id, dockerId); publicPort = await getFreePublicPort(id, dockerId);
} }

View File

@@ -1,7 +1,7 @@
import { FastifyPluginAsync } from 'fastify'; import { FastifyPluginAsync } from 'fastify';
import { deleteDatabase, getDatabase, getDatabaseLogs, getDatabaseStatus, getDatabaseTypes, getDatabaseUsage, getVersions, listDatabases, newDatabase, saveDatabase, saveDatabaseDestination, saveDatabaseSettings, saveDatabaseType, saveVersion, startDatabase, stopDatabase } from './handlers'; import { deleteDatabase, getDatabase, getDatabaseLogs, getDatabaseStatus, getDatabaseTypes, getDatabaseUsage, getVersions, listDatabases, newDatabase, saveDatabase, saveDatabaseDestination, saveDatabaseSettings, saveDatabaseType, saveVersion, startDatabase, stopDatabase } from './handlers';
import type { GetDatabaseLogs, OnlyId, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSettings, SaveVersion } from '../../../../types'; import type { DeleteDatabase, GetDatabaseLogs, OnlyId, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSettings, SaveVersion } from '../../../../types';
import type { SaveDatabaseType } from './types'; import type { SaveDatabaseType } from './types';
const root: FastifyPluginAsync = async (fastify): Promise<void> => { const root: FastifyPluginAsync = async (fastify): Promise<void> => {
@@ -13,7 +13,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get<OnlyId>('/:id', async (request) => await getDatabase(request)); fastify.get<OnlyId>('/:id', async (request) => await getDatabase(request));
fastify.post<SaveDatabase>('/:id', async (request, reply) => await saveDatabase(request, reply)); fastify.post<SaveDatabase>('/:id', async (request, reply) => await saveDatabase(request, reply));
fastify.delete<OnlyId>('/:id', async (request) => await deleteDatabase(request)); fastify.delete<DeleteDatabase>('/:id', async (request) => await deleteDatabase(request));
fastify.get<OnlyId>('/:id/status', async (request) => await getDatabaseStatus(request)); fastify.get<OnlyId>('/:id/status', async (request) => await getDatabaseStatus(request));

View File

@@ -2,4 +2,7 @@ import type { OnlyId } from "../../../../types";
export interface SaveDatabaseType extends OnlyId { export interface SaveDatabaseType extends OnlyId {
Body: { type: string } Body: { type: string }
}
export interface DeleteDatabase extends OnlyId {
Body: { force: string }
} }

View File

@@ -1,15 +1,13 @@
import type { FastifyReply, FastifyRequest } from 'fastify'; import type { FastifyReply, FastifyRequest } from 'fastify';
import fs from 'fs/promises'; import fs from 'fs/promises';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import bcrypt from 'bcryptjs'; import { prisma, uniqueName, asyncExecShell, getServiceFromDB, getContainerUsage, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, ComposeFile, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, executeDockerCmd, checkDomainsIsValidInDNS, checkExposedPort } from '../../../../lib/common';
import { prisma, uniqueName, asyncExecShell, getServiceImage, getServiceFromDB, getContainerUsage,isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, getServiceMainPort, createDirectories, ComposeFile, makeLabelForServices, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, executeDockerCmd, checkDomainsIsValidInDNS, persistentVolumes, asyncSleep, isARM, defaultComposeConfiguration, checkExposedPort } from '../../../../lib/common';
import { day } from '../../../../lib/dayjs'; import { day } from '../../../../lib/dayjs';
import { checkContainer, isContainerExited, removeContainer } from '../../../../lib/docker'; import { checkContainer, isContainerExited } from '../../../../lib/docker';
import cuid from 'cuid'; import cuid from 'cuid';
import type { OnlyId } from '../../../../types'; import type { OnlyId } from '../../../../types';
import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetWordpressSettings } from './types'; import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetGlitchTipSettings, SetWordpressSettings } from './types';
import { defaultServiceConfigurations } from '../../../../lib/services';
import { supportedServiceTypesAndVersions } from '../../../../lib/services/supportedVersions'; import { supportedServiceTypesAndVersions } from '../../../../lib/services/supportedVersions';
import { configureServiceType, removeService } from '../../../../lib/services/common'; import { configureServiceType, removeService } from '../../../../lib/services/common';
@@ -269,7 +267,6 @@ export async function saveService(request: FastifyRequest<SaveService>, reply: F
if (exposePort) exposePort = Number(exposePort); if (exposePort) exposePort = Number(exposePort);
type = fixType(type) type = fixType(type)
const update = saveUpdateableFields(type, request.body[type]) const update = saveUpdateableFields(type, request.body[type])
const data = { const data = {
fqdn, fqdn,
@@ -400,17 +397,33 @@ export async function deleteServiceStorage(request: FastifyRequest<DeleteService
} }
} }
export async function setSettingsService(request: FastifyRequest<ServiceStartStop & SetWordpressSettings>, reply: FastifyReply) { export async function setSettingsService(request: FastifyRequest<ServiceStartStop & SetWordpressSettings & SetGlitchTipSettings>, reply: FastifyReply) {
try { try {
const { type } = request.params const { type } = request.params
if (type === 'wordpress') { if (type === 'wordpress') {
return await setWordpressSettings(request, reply) return await setWordpressSettings(request, reply)
} }
if (type === 'glitchtip') {
return await setGlitchTipSettings(request, reply)
}
throw `Service type ${type} not supported.` throw `Service type ${type} not supported.`
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
async function setGlitchTipSettings(request: FastifyRequest<SetGlitchTipSettings>, reply: FastifyReply) {
try {
const { id } = request.params
const { enableOpenUserRegistration, emailSmtpUseSsl, emailSmtpUseTls } = request.body
await prisma.glitchTip.update({
where: { serviceId: id },
data: { enableOpenUserRegistration, emailSmtpUseSsl, emailSmtpUseTls }
});
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
async function setWordpressSettings(request: FastifyRequest<ServiceStartStop & SetWordpressSettings>, reply: FastifyReply) { async function setWordpressSettings(request: FastifyRequest<ServiceStartStop & SetWordpressSettings>, reply: FastifyReply) {
try { try {
const { id } = request.params const { id } = request.params

View File

@@ -29,7 +29,7 @@ import {
} from './handlers'; } from './handlers';
import type { OnlyId } from '../../../../types'; import type { OnlyId } from '../../../../types';
import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetWordpressSettings } from './types'; import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetGlitchTipSettings, SetWordpressSettings } from './types';
import { startService, stopService } from '../../../../lib/services/handlers'; import { startService, stopService } from '../../../../lib/services/handlers';
const root: FastifyPluginAsync = async (fastify): Promise<void> => { const root: FastifyPluginAsync = async (fastify): Promise<void> => {
@@ -71,7 +71,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.post<ServiceStartStop>('/:id/:type/start', async (request) => await startService(request)); fastify.post<ServiceStartStop>('/:id/:type/start', async (request) => await startService(request));
fastify.post<ServiceStartStop>('/:id/:type/stop', async (request) => await stopService(request)); fastify.post<ServiceStartStop>('/:id/:type/stop', async (request) => await stopService(request));
fastify.post<ServiceStartStop & SetWordpressSettings>('/:id/:type/settings', async (request, reply) => await setSettingsService(request, reply)); fastify.post<ServiceStartStop & SetWordpressSettings & SetGlitchTipSettings>('/:id/:type/settings', async (request, reply) => await setSettingsService(request, reply));
fastify.post<OnlyId>('/:id/plausibleanalytics/activate', async (request, reply) => await activatePlausibleUsers(request, reply)); fastify.post<OnlyId>('/:id/plausibleanalytics/activate', async (request, reply) => await activatePlausibleUsers(request, reply));
fastify.post<OnlyId>('/:id/plausibleanalytics/cleanup', async (request, reply) => await cleanupPlausibleLogs(request, reply)); fastify.post<OnlyId>('/:id/plausibleanalytics/cleanup', async (request, reply) => await cleanupPlausibleLogs(request, reply));

View File

@@ -89,6 +89,10 @@ export interface ActivateWordpressFtp extends OnlyId {
} }
} }
export interface SetGlitchTipSettings extends OnlyId {
Body: {
enableOpenUserRegistration: boolean,
emailSmtpUseSsl: boolean,
emailSmtpUseTls: boolean
}
}

View File

@@ -67,7 +67,7 @@ export async function configureGitHubApp(request, reply) {
} }
export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promise<any> { export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promise<any> {
try { try {
const buildId = cuid();
const allowedGithubEvents = ['push', 'pull_request']; const allowedGithubEvents = ['push', 'pull_request'];
const allowedActions = ['opened', 'reopened', 'synchronize', 'closed']; const allowedActions = ['opened', 'reopened', 'synchronize', 'closed'];
const githubEvent = request.headers['x-github-event']?.toString().toLowerCase(); const githubEvent = request.headers['x-github-event']?.toString().toLowerCase();
@@ -87,126 +87,124 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
if (!projectId || !branch) { if (!projectId || !branch) {
throw { status: 500, message: 'Cannot parse projectId or branch from the webhook?!' } throw { status: 500, message: 'Cannot parse projectId or branch from the webhook?!' }
} }
const applicationFound = await getApplicationFromDBWebhook(projectId, branch); const applicationsFound = await getApplicationFromDBWebhook(projectId, branch);
if (applicationFound) { if (applicationsFound && applicationsFound.length > 0) {
const webhookSecret = applicationFound.gitSource.githubApp.webhookSecret || null; for (const application of applicationsFound) {
//@ts-ignore const buildId = cuid();
const hmac = crypto.createHmac('sha256', webhookSecret); const webhookSecret = application.gitSource.githubApp.webhookSecret || null;
const digest = Buffer.from(
'sha256=' + hmac.update(JSON.stringify(body)).digest('hex'),
'utf8'
);
if (!isDev) {
const checksum = Buffer.from(githubSignature, 'utf8');
//@ts-ignore //@ts-ignore
if (checksum.length !== digest.length || !crypto.timingSafeEqual(digest, checksum)) { const hmac = crypto.createHmac('sha256', webhookSecret);
throw { status: 500, message: 'SHA256 checksum failed. Are you doing something fishy?' } const digest = Buffer.from(
}; 'sha256=' + hmac.update(JSON.stringify(body)).digest('hex'),
} 'utf8'
);
if (!isDev) {
if (githubEvent === 'push') { const checksum = Buffer.from(githubSignature, 'utf8');
if (!applicationFound.configHash) { //@ts-ignore
const configHash = crypto if (checksum.length !== digest.length || !crypto.timingSafeEqual(digest, checksum)) {
//@ts-ignore console.log('SHA256 checksum failed. Are you doing something fishy?')
.createHash('sha256') // throw { status: 500, message: 'SHA256 checksum failed. Are you doing something fishy?'
.update( };
JSON.stringify({
buildPack: applicationFound.buildPack,
port: applicationFound.port,
exposePort: applicationFound.exposePort,
installCommand: applicationFound.installCommand,
buildCommand: applicationFound.buildCommand,
startCommand: applicationFound.startCommand
})
)
.digest('hex');
await prisma.application.updateMany({
where: { branch, projectId },
data: { configHash }
});
}
await prisma.application.update({
where: { id: applicationFound.id },
data: { updatedAt: new Date() }
});
await prisma.build.create({
data: {
id: buildId,
applicationId: applicationFound.id,
destinationDockerId: applicationFound.destinationDocker.id,
gitSourceId: applicationFound.gitSource.id,
githubAppId: applicationFound.gitSource.githubApp?.id,
gitlabAppId: applicationFound.gitSource.gitlabApp?.id,
status: 'queued',
type: 'webhook_commit'
}
});
return {
message: 'Queued. Thank you!'
};
} else if (githubEvent === 'pull_request') {
const pullmergeRequestId = body.number.toString();
const pullmergeRequestAction = body.action;
const sourceBranch = body.pull_request.head.ref.includes('/') ? body.pull_request.head.ref.split('/')[2] : body.pull_request.head.ref;
if (!allowedActions.includes(pullmergeRequestAction)) {
throw { status: 500, message: 'Action not allowed.' }
} }
if (applicationFound.settings.previews) { if (githubEvent === 'push') {
if (applicationFound.destinationDockerId) { if (!application.configHash) {
const isRunning = await checkContainer( const configHash = crypto
{ //@ts-ignore
dockerId: applicationFound.destinationDocker.id, .createHash('sha256')
container: applicationFound.id .update(
} JSON.stringify({
); buildPack: application.buildPack,
if (!isRunning) { port: application.port,
throw { status: 500, message: 'Application not running.' } exposePort: application.exposePort,
} installCommand: application.installCommand,
} buildCommand: application.buildCommand,
if ( startCommand: application.startCommand
pullmergeRequestAction === 'opened' || })
pullmergeRequestAction === 'reopened' || )
pullmergeRequestAction === 'synchronize' .digest('hex');
) {
await prisma.application.update({ await prisma.application.update({
where: { id: applicationFound.id }, where: { id: application.id },
data: { updatedAt: new Date() } data: { configHash }
}); });
await prisma.build.create({
data: {
id: buildId,
pullmergeRequestId,
sourceBranch,
applicationId: applicationFound.id,
destinationDockerId: applicationFound.destinationDocker.id,
gitSourceId: applicationFound.gitSource.id,
githubAppId: applicationFound.gitSource.githubApp?.id,
gitlabAppId: applicationFound.gitSource.gitlabApp?.id,
status: 'queued',
type: 'webhook_pr'
}
});
return {
message: 'Queued. Thank you!'
};
} else if (pullmergeRequestAction === 'closed') {
if (applicationFound.destinationDockerId) {
const id = `${applicationFound.id}-${pullmergeRequestId}`;
await removeContainer({ id, dockerId: applicationFound.destinationDocker.id });
}
return {
message: 'Removed preview. Thank you!'
};
} }
} else {
throw { status: 500, message: 'Pull request previews are not enabled.' } await prisma.application.update({
where: { id: application.id },
data: { updatedAt: new Date() }
});
console.log(application.id)
await prisma.build.create({
data: {
id: buildId,
applicationId: application.id,
destinationDockerId: application.destinationDocker.id,
gitSourceId: application.gitSource.id,
githubAppId: application.gitSource.githubApp?.id,
gitlabAppId: application.gitSource.gitlabApp?.id,
status: 'queued',
type: 'webhook_commit'
}
});
console.log(`Webhook for ${application.name} queued.`)
} else if (githubEvent === 'pull_request') {
const pullmergeRequestId = body.number.toString();
const pullmergeRequestAction = body.action;
const sourceBranch = body.pull_request.head.ref.includes('/') ? body.pull_request.head.ref.split('/')[2] : body.pull_request.head.ref;
if (!allowedActions.includes(pullmergeRequestAction)) {
throw { status: 500, message: 'Action not allowed.' }
}
if (application.settings.previews) {
if (application.destinationDockerId) {
const isRunning = await checkContainer(
{
dockerId: application.destinationDocker.id,
container: application.id
}
);
if (!isRunning) {
throw { status: 500, message: 'Application not running.' }
}
}
if (
pullmergeRequestAction === 'opened' ||
pullmergeRequestAction === 'reopened' ||
pullmergeRequestAction === 'synchronize'
) {
await prisma.application.update({
where: { id: application.id },
data: { updatedAt: new Date() }
});
await prisma.build.create({
data: {
id: buildId,
pullmergeRequestId,
sourceBranch,
applicationId: application.id,
destinationDockerId: application.destinationDocker.id,
gitSourceId: application.gitSource.id,
githubAppId: application.gitSource.githubApp?.id,
gitlabAppId: application.gitSource.gitlabApp?.id,
status: 'queued',
type: 'webhook_pr'
}
});
} else if (pullmergeRequestAction === 'closed') {
if (application.destinationDockerId) {
const id = `${application.id}-${pullmergeRequestId}`;
await removeContainer({ id, dockerId: application.destinationDocker.id });
}
}
}
} }
} }
} }
throw { status: 500, message: 'Not handled event.' }
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
} }

View File

@@ -2,9 +2,8 @@ import axios from "axios";
import cuid from "cuid"; import cuid from "cuid";
import crypto from "crypto"; import crypto from "crypto";
import type { FastifyReply, FastifyRequest } from "fastify"; import type { FastifyReply, FastifyRequest } from "fastify";
import { errorHandler, getAPIUrl, isDev, listSettings, prisma } from "../../../lib/common"; import { errorHandler, getAPIUrl, getUIUrl, isDev, listSettings, prisma } from "../../../lib/common";
import { checkContainer, removeContainer } from "../../../lib/docker"; import { checkContainer, removeContainer } from "../../../lib/docker";
import { scheduler } from "../../../lib/scheduler";
import { getApplicationFromDB, getApplicationFromDBWebhook } from "../../api/v1/applications/handlers"; import { getApplicationFromDB, getApplicationFromDBWebhook } from "../../api/v1/applications/handlers";
import type { ConfigureGitLabApp, GitLabEvents } from "./types"; import type { ConfigureGitLabApp, GitLabEvents } from "./types";
@@ -30,7 +29,7 @@ export async function configureGitLabApp(request: FastifyRequest<ConfigureGitLab
}); });
const { data } = await axios.post(`${htmlUrl}/oauth/token`, params) const { data } = await axios.post(`${htmlUrl}/oauth/token`, params)
if (isDev) { if (isDev) {
return reply.redirect(`${getAPIUrl()}/webhooks/success?token=${data.access_token}`) return reply.redirect(`${getUIUrl()}/webhooks/success?token=${data.access_token}`)
} }
return reply.redirect(`/webhooks/success?token=${data.access_token}`) return reply.redirect(`/webhooks/success?token=${data.access_token}`)
} catch ({ status, message, ...other }) { } catch ({ status, message, ...other }) {
@@ -40,59 +39,56 @@ export async function configureGitLabApp(request: FastifyRequest<ConfigureGitLab
export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) { export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
const { object_kind: objectKind, ref, project_id } = request.body const { object_kind: objectKind, ref, project_id } = request.body
try { try {
const buildId = cuid();
const allowedActions = ['opened', 'reopen', 'close', 'open', 'update']; const allowedActions = ['opened', 'reopen', 'close', 'open', 'update'];
const webhookToken = request.headers['x-gitlab-token']; const webhookToken = request.headers['x-gitlab-token'];
if (!webhookToken) { if (!webhookToken && !isDev) {
throw { status: 500, message: 'Invalid webhookToken.' } throw { status: 500, message: 'Invalid webhookToken.' }
} }
if (objectKind === 'push') { if (objectKind === 'push') {
const projectId = Number(project_id); const projectId = Number(project_id);
const branch = ref.split('/')[2]; const branch = ref.split('/')[2];
const applicationFound = await getApplicationFromDBWebhook(projectId, branch); const applicationsFound = await getApplicationFromDBWebhook(projectId, branch);
if (applicationFound) { if (applicationsFound && applicationsFound.length > 0) {
if (!applicationFound.configHash) { for (const application of applicationsFound) {
const configHash = crypto const buildId = cuid();
.createHash('sha256') if (!application.configHash) {
.update( const configHash = crypto
JSON.stringify({ .createHash('sha256')
buildPack: applicationFound.buildPack, .update(
port: applicationFound.port, JSON.stringify({
exposePort: applicationFound.exposePort, buildPack: application.buildPack,
installCommand: applicationFound.installCommand, port: application.port,
buildCommand: applicationFound.buildCommand, exposePort: application.exposePort,
startCommand: applicationFound.startCommand installCommand: application.installCommand,
}) buildCommand: application.buildCommand,
) startCommand: application.startCommand
.digest('hex'); })
await prisma.application.updateMany({ )
where: { branch, projectId }, .digest('hex');
data: { configHash } await prisma.application.update({
where: { id: application.id },
data: { configHash }
});
}
await prisma.application.update({
where: { id: application.id },
data: { updatedAt: new Date() }
});
await prisma.build.create({
data: {
id: buildId,
applicationId: application.id,
destinationDockerId: application.destinationDocker.id,
gitSourceId: application.gitSource.id,
githubAppId: application.gitSource.githubApp?.id,
gitlabAppId: application.gitSource.gitlabApp?.id,
status: 'queued',
type: 'webhook_commit'
}
}); });
} }
await prisma.application.update({
where: { id: applicationFound.id },
data: { updatedAt: new Date() }
});
await prisma.build.create({
data: {
id: buildId,
applicationId: applicationFound.id,
destinationDockerId: applicationFound.destinationDocker.id,
gitSourceId: applicationFound.gitSource.id,
githubAppId: applicationFound.gitSource.githubApp?.id,
gitlabAppId: applicationFound.gitSource.gitlabApp?.id,
status: 'queued',
type: 'webhook_commit'
}
});
return {
message: 'Queued. Thank you!'
};
} }
} else if (objectKind === 'merge_request') { } else if (objectKind === 'merge_request') {
const { object_attributes: { work_in_progress: isDraft, action, source_branch: sourceBranch, target_branch: targetBranch, iid: pullmergeRequestId }, project: { id } } = request.body const { object_attributes: { work_in_progress: isDraft, action, source_branch: sourceBranch, target_branch: targetBranch, iid: pullmergeRequestId }, project: { id } } = request.body
@@ -105,64 +101,63 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
throw { status: 500, message: 'Draft MR, do nothing.' } throw { status: 500, message: 'Draft MR, do nothing.' }
} }
const applicationFound = await getApplicationFromDBWebhook(projectId, targetBranch); const applicationsFound = await getApplicationFromDBWebhook(projectId, targetBranch);
if (applicationFound) { if (applicationsFound && applicationsFound.length > 0) {
if (applicationFound.settings.previews) { for (const application of applicationsFound) {
if (applicationFound.destinationDockerId) { const buildId = cuid();
const isRunning = await checkContainer( if (application.settings.previews) {
{ if (application.destinationDockerId) {
dockerId: applicationFound.destinationDocker.id, const isRunning = await checkContainer(
container: applicationFound.id {
dockerId: application.destinationDocker.id,
container: application.id
}
);
if (!isRunning) {
throw { status: 500, message: 'Application not running.' }
} }
);
if (!isRunning) {
throw { status: 500, message: 'Application not running.' }
} }
} if (!isDev && application.gitSource.gitlabApp.webhookToken !== webhookToken) {
if (!isDev && applicationFound.gitSource.gitlabApp.webhookToken !== webhookToken) { throw { status: 500, message: 'Invalid webhookToken. Are you doing something nasty?!' }
throw { status: 500, message: 'Invalid webhookToken. Are you doing something nasty?!' } }
} if (
if ( action === 'opened' ||
action === 'opened' || action === 'reopen' ||
action === 'reopen' || action === 'open' ||
action === 'open' || action === 'update'
action === 'update' ) {
) { await prisma.application.update({
await prisma.application.update({ where: { id: application.id },
where: { id: applicationFound.id }, data: { updatedAt: new Date() }
data: { updatedAt: new Date() } });
}); await prisma.build.create({
await prisma.build.create({ data: {
data: { id: buildId,
id: buildId, pullmergeRequestId,
pullmergeRequestId, sourceBranch,
sourceBranch, applicationId: application.id,
applicationId: applicationFound.id, destinationDockerId: application.destinationDocker.id,
destinationDockerId: applicationFound.destinationDocker.id, gitSourceId: application.gitSource.id,
gitSourceId: applicationFound.gitSource.id, githubAppId: application.gitSource.githubApp?.id,
githubAppId: applicationFound.gitSource.githubApp?.id, gitlabAppId: application.gitSource.gitlabApp?.id,
gitlabAppId: applicationFound.gitSource.gitlabApp?.id, status: 'queued',
status: 'queued', type: 'webhook_mr'
type: 'webhook_mr' }
});
return {
message: 'Queued. Thank you!'
};
} else if (action === 'close') {
if (application.destinationDockerId) {
const id = `${application.id}-${pullmergeRequestId}`;
await removeContainer({ id, dockerId: application.destinationDocker.id });
} }
});
return {
message: 'Queued. Thank you!'
};
} else if (action === 'close') {
if (applicationFound.destinationDockerId) {
const id = `${applicationFound.id}-${pullmergeRequestId}`;
await removeContainer({ id, dockerId: applicationFound.destinationDocker.id });
} }
return {
message: 'Removed preview. Thank you!'
};
} }
} }
throw { status: 500, message: 'Merge request previews are not enabled.' }
} }
} }
throw { status: 500, message: 'Not handled event.' }
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
} }

View File

@@ -3,6 +3,7 @@ import { errorHandler, getDomain, isDev, prisma, executeDockerCmd } from "../../
import { supportedServiceTypesAndVersions } from "../../../lib/services/supportedVersions"; import { supportedServiceTypesAndVersions } from "../../../lib/services/supportedVersions";
import { includeServices } from "../../../lib/services/common"; import { includeServices } from "../../../lib/services/common";
import { TraefikOtherConfiguration } from "./types"; import { TraefikOtherConfiguration } from "./types";
import { OnlyId } from "../../../types";
function configureMiddleware( function configureMiddleware(
{ id, container, port, domain, nakedDomain, isHttps, isWWW, isDualCerts, scriptName, type }, { id, container, port, domain, nakedDomain, isHttps, isWWW, isDualCerts, scriptName, type },
@@ -25,7 +26,30 @@ function configureMiddleware(
] ]
} }
}; };
if (type === 'appwrite') {
traefik.http.routers[`${id}-realtime`] = {
entrypoints: ['websecure'],
rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/v1/realtime\`)`,
service: `${`${id}-realtime`}`,
tls: {
domains: {
main: `${domain}`
}
},
middlewares: []
};
traefik.http.services[`${id}-realtime`] = {
loadbalancer: {
servers: [
{
url: `http://${container}-realtime:${port}`
}
]
}
};
}
if (isDualCerts) { if (isDualCerts) {
traefik.http.routers[`${id}-secure`] = { traefik.http.routers[`${id}-secure`] = {
entrypoints: ['websecure'], entrypoints: ['websecure'],
@@ -112,6 +136,23 @@ function configureMiddleware(
] ]
} }
}; };
if (type === 'appwrite') {
traefik.http.routers[`${id}-realtime`] = {
entrypoints: ['web'],
rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/v1/realtime\`)`,
service: `${id}-realtime`,
middlewares: []
};
traefik.http.services[`${id}-realtime`] = {
loadbalancer: {
servers: [
{
url: `http://${container}-realtime:${port}`
}
]
}
};
}
if (!isDualCerts) { if (!isDualCerts) {
if (isWWW) { if (isWWW) {
@@ -490,7 +531,7 @@ export async function traefikOtherConfiguration(request: FastifyRequest<TraefikO
} }
} }
export async function remoteTraefikConfiguration(request: FastifyRequest) { export async function remoteTraefikConfiguration(request: FastifyRequest<OnlyId>) {
const { id } = request.params const { id } = request.params
try { try {
const traefik = { const traefik = {

View File

@@ -36,4 +36,3 @@ export interface SaveDatabaseSettings extends OnlyId {
} }

4
apps/i18n/.env.example Normal file
View File

@@ -0,0 +1,4 @@
WEBLATE_INSTANCE_URL=http://localhost
WEBLATE_COMPONENT_NAME=coolify
WEBLATE_TOKEN=
TRANSLATION_DIR=

1
apps/i18n/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
locales/*

63
apps/i18n/index.mjs Normal file
View File

@@ -0,0 +1,63 @@
import dotenv from 'dotenv';
dotenv.config()
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url';
import Gettext from 'node-gettext'
import { po } from 'gettext-parser'
import got from 'got';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const weblateInstanceURL = process.env.WEBLATE_INSTANCE_URL;
const weblateComponentName = process.env.WEBLATE_COMPONENT_NAME
const token = process.env.WEBLATE_TOKEN;
const translationsDir = process.env.TRANSLATION_DIR;
const translationsPODir = './locales';
const locales = []
const domain = 'locale'
const translations = await got(`${weblateInstanceURL}/api/components/${weblateComponentName}/glossary/translations/?format=json`, {
headers: {
"Authorization": `Token ${token}`
}
}).json()
for (const translation of translations.results) {
const code = translation.language_code
locales.push(code)
const fileUrl = translation.file_url.replace('=json', '=po')
const file = await got(fileUrl, {
headers: {
"Authorization": `Token ${token}`
}
}).text()
fs.writeFileSync(path.join(__dirname, translationsPODir, domain + '-' + code + '.po'), file)
}
const gt = new Gettext()
locales.forEach((locale) => {
let json = {}
const fileName = `${domain}-${locale}.po`
const translationsFilePath = path.join(translationsPODir, fileName)
const translationsContent = fs.readFileSync(translationsFilePath)
const parsedTranslations = po.parse(translationsContent)
const a = gt.gettext(parsedTranslations)
for (const [key, value] of Object.entries(a)) {
if (key === 'translations') {
for (const [key1, value1] of Object.entries(value)) {
if (key1 !== '') {
for (const [key2, value2] of Object.entries(value1)) {
json[value2.msgctxt] = value2.msgstr[0]
}
}
}
}
}
fs.writeFileSync(`${translationsDir}/${locale}.json`, JSON.stringify(json))
})

15
apps/i18n/package.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "i18n-converter",
"description": "Convert Weblate translations to sveltekit-i18n",
"license": "Apache-2.0",
"scripts": {
"translate": "node index.mjs"
},
"type": "module",
"dependencies": {
"node-gettext": "3.0.0",
"gettext-parser": "6.0.0",
"got": "12.3.1",
"dotenv": "16.0.2"
}
}

View File

@@ -14,15 +14,20 @@
"format": "prettier --write --plugin-search-dir=. ." "format": "prettier --write --plugin-search-dir=. ."
}, },
"devDependencies": { "devDependencies": {
"@floating-ui/dom": "1.0.1",
"@playwright/test": "1.25.1", "@playwright/test": "1.25.1",
"@popperjs/core": "2.11.6",
"@sveltejs/kit": "1.0.0-next.405", "@sveltejs/kit": "1.0.0-next.405",
"@types/js-cookie": "3.0.2", "@types/js-cookie": "3.0.2",
"@typescript-eslint/eslint-plugin": "5.35.1", "@typescript-eslint/eslint-plugin": "5.35.1",
"@typescript-eslint/parser": "5.35.1", "@typescript-eslint/parser": "5.35.1",
"autoprefixer": "10.4.8", "autoprefixer": "10.4.8",
"classnames": "2.3.1",
"eslint": "8.22.0", "eslint": "8.22.0",
"eslint-config-prettier": "8.5.0", "eslint-config-prettier": "8.5.0",
"eslint-plugin-svelte3": "4.0.0", "eslint-plugin-svelte3": "4.0.0",
"flowbite": "1.5.2",
"flowbite-svelte": "0.26.2",
"postcss": "8.4.16", "postcss": "8.4.16",
"prettier": "2.7.1", "prettier": "2.7.1",
"prettier-plugin-svelte": "2.7.0", "prettier-plugin-svelte": "2.7.0",

View File

@@ -0,0 +1,32 @@
<script lang="ts">
import Tooltip from './Tooltip.svelte';
export let url = 'https://docs.coollabs.io';
let id =
'cool-' +
url
.split('')
.map((c) => c.charCodeAt(0).toString(16).padStart(2, '0'))
.join('')
.slice(-16);
</script>
<a {id} href={url} target="_blank" class="icons inline-block text-pink-500 cursor-pointer text-xs">
<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="M6 4h11a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-11a1 1 0 0 1 -1 -1v-14a1 1 0 0 1 1 -1m3 0v18"
/>
<line x1="13" y1="8" x2="15" y2="8" />
<line x1="13" y1="12" x2="15" y2="12" />
</svg>
</a>
<Tooltip triggeredBy={`#${id}`}>See details in the documentation</Tooltip>

View File

@@ -1,6 +1,31 @@
<script lang="ts"> <script lang="ts">
export let text: string; import { onMount } from 'svelte';
export let customClass = 'max-w-[24rem]';
import Tooltip from './Tooltip.svelte';
export let explanation = '';
let id: any;
let self: any;
onMount(() => {
id = `info-${self.offsetLeft}-${self.offsetTop}`;
});
</script> </script>
<div class="p-2 text-xs text-stone-400 {customClass}">{@html text}</div> <div {id} class="inline-block mx-2 text-pink-500 cursor-pointer" bind:this={self}>
<svg
fill="none"
height="18"
shape-rendering="geometricPrecision"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
viewBox="0 0 24 24"
width="18"
><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" /><path
d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3"
/><circle cx="12" cy="17" r=".5" />
</svg>
</div>
{#if id}
<Tooltip triggeredBy={`#${id}`}>{@html explanation}</Tooltip>
{/if}

View File

@@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
import Explainer from '$lib/components/Explainer.svelte'; import Explaner from './Explainer.svelte';
import Tooltip from './Tooltip.svelte';
export let id: any;
export let setting: any; export let setting: any;
export let title: any; export let title: any;
export let description: any; export let description: any;
@@ -8,22 +10,17 @@
export let disabled = false; export let disabled = false;
export let dataTooltip: any = null; export let dataTooltip: any = null;
export let loading = false; export let loading = false;
let triggeredBy = `#${id}`;
</script> </script>
<div class="flex items-center py-4 pr-8"> <div class="flex items-center py-4 pr-8">
<div class="flex w-96 flex-col"> <div class="flex w-96 flex-col">
<div class="text-xs font-bold text-stone-100 md:text-base">{title}</div> <div class="text-xs font-bold text-stone-100 md:text-base">
<Explainer text={description} /> {title}<Explaner explanation={description} />
</div>
</div> </div>
</div> </div>
<div <div class:text-center={isCenter} class="flex justify-center">
class:tooltip-right={dataTooltip}
class:tooltip-primary={dataTooltip}
class:tooltip={dataTooltip}
class:text-center={isCenter}
data-tip={dataTooltip}
class="flex justify-center"
>
<div <div
on:click on:click
aria-pressed="false" aria-pressed="false"
@@ -32,6 +29,7 @@
class:bg-green-600={!loading && setting} class:bg-green-600={!loading && setting}
class:bg-stone-700={!loading && !setting} class:bg-stone-700={!loading && !setting}
class:bg-yellow-500={loading} class:bg-yellow-500={loading}
{id}
> >
<span class="sr-only">Use setting</span> <span class="sr-only">Use setting</span>
<span <span
@@ -72,3 +70,7 @@
</span> </span>
</div> </div>
</div> </div>
{#if dataTooltip}
<Tooltip {triggeredBy} placement="top">{dataTooltip}</Tooltip>
{/if}

View File

@@ -0,0 +1,6 @@
<script lang="ts">
export let text: string;
export let customClass = 'max-w-[24rem]';
</script>
<div class="p-2 text-xs text-stone-400 {customClass}">{@html text}</div>

View File

@@ -0,0 +1,8 @@
<script lang="ts">
import { Tooltip } from 'flowbite-svelte';
export let placement = 'bottom';
export let color = 'bg-coollabs text-left';
export let triggeredBy = '#tooltip-default';
</script>
<Tooltip {triggeredBy} {placement} arrow={false} {color} style="custom"><slot /></Tooltip>

View File

@@ -4,7 +4,7 @@
<svg <svg
viewBox="0 0 127 74" viewBox="0 0 127 74"
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 h-8mx-auto'} class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 h-8 mx-auto'}
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
><path ><path
d="M.825 73.993l23.244-59.47A21.85 21.85 0 0144.42.625h14.014L35.19 60.096a21.85 21.85 0 01-20.352 13.897H.825z" d="M.825 73.993l23.244-59.47A21.85 21.85 0 0144.42.625h14.014L35.19 60.096a21.85 21.85 0 01-20.352 13.897H.825z"

View File

@@ -40,4 +40,6 @@
<Icons.GlitchTip {isAbsolute} /> <Icons.GlitchTip {isAbsolute} />
{:else if type === 'searxng'} {:else if type === 'searxng'}
<Icons.Searxng {isAbsolute} /> <Icons.Searxng {isAbsolute} />
{:else if type === 'weblate'}
<Icons.Weblate {isAbsolute} />
{/if} {/if}

View File

@@ -0,0 +1,61 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
class={isAbsolute ? 'w-16 h-16 absolute top-0 left-0 -m-7' : 'w-12 h-12 mx-auto'}
version="1.1"
viewBox="0 0 300 300"
><linearGradient
id="a"
x1=".3965"
x2="98.808"
y1="55.253"
y2="55.253"
gradientTransform="scale(.98308 1.0172)"
gradientUnits="userSpaceOnUse"
><stop stop-color="#00d2e6" offset="0" /><stop
stop-color="#2eccaa"
offset="1"
/></linearGradient
><linearGradient
id="b"
x1="49.017"
x2="99.793"
y1="137.89"
y2="113.96"
gradientTransform="scale(1.1631 .8598)"
gradientUnits="userSpaceOnUse"
><stop stop-opacity="0" offset="0" /><stop offset=".51413" /><stop
stop-opacity="0"
offset="1"
/></linearGradient
><linearGradient
id="c"
x1="201.82"
x2="103.58"
y1="57.649"
y2="57.649"
gradientTransform="scale(.98308 1.0172)"
gradientUnits="userSpaceOnUse"
><stop stop-color="#1fa385" offset="0" /><stop
stop-color="#2eccaa"
offset="1"
/></linearGradient
><g transform="translate(50,76)" fill-rule="evenodd"
><path
d="m127.25 111.61c-2.8884-0.0145-5.7666-0.6024-8.4797-1.7847-6.1117-2.6626-11.493-7.6912-15.872-14.495 1.2486-2.2193 2.3738-4.5173 3.3784-6.8535 4.4051-10.243 6.5-21.46 6.6607-32.593-0.0233-0.22082-0.0416-0.44244-0.0552-0.66483l-0.0121-0.57132c-0.01-4.3654-0.67459-8.7898-2.1767-12.909-1.7304-4.7458-4.4887-9.4955-8.865-11.348-0.79519-0.33595-1.6316-0.47701-2.4642-0.45737-5.5049-10.289-5.6799-20.149 0-29.537 0.10115 0 0.20619 3.9293e-4 0.30734 0.001179 6.7012 0.07387 13.34 2.1418 19.021 5.7536 15.469 9.835 23.182 29.001 23.352 47.818 2e-3 0.22083-3.9e-4 0.44126-7e-3 0.66169h0.0868c-0.0226 19.887-4.8049 40.054-14.875 56.979zm-34.3 31.216c-14.448 5.9425-31.228 5.6236-45.549-1.025-16.476-7.6476-29.065-22.512-36.818-39.479-13.262-29.022-13.566-63.715-0.98815-93.182 9.4458 3.7788 17.845-2.2397 17.845-2.2397s-0.01945 9.2605 8.9478 13.905c-9.2007 21.556-8.979 47.167 0.2412 68.173 4.4389 10.107 11.22 19.519 20.619 24.842 3.3547 1.8996 7.041 3.126 10.833 3.5862 0.01404 0.0219 0.02808 0.0439 0.04214 0.0658 6.6965 10.449 15.132 19.157 24.828 25.354z"
fill="url(#a)"
fill-rule="nonzero"
/><path
d="m127.24 111.61c-2.8869-0.0151-5.7636-0.60296-8.4755-1.7846-6.1127-2.663-11.495-7.6928-15.874-14.498 1.2494-2.2205 2.3754-4.5198 3.3806-6.8572 1.3282-3.0884 2.4463-6.2648 3.3644-9.501 2.128-7.4978 30.382 2.0181 26.072 14.371-2.2239 6.373-5.0394 12.509-8.4675 18.27zm-34.302 31.212c-14.446 5.9396-31.224 5.6198-45.543-1.0278-16.476-7.6476 0.44739-33.303 9.8465-27.981 3.3533 1.8988 7.0378 3.125 10.828 3.5856 0.01567 0.0245 0.03135 0.049 0.04704 0.0735 6.695 10.447 15.128 19.153 24.821 25.349z"
fill="url(#b)"
opacity=".3"
/><path
d="m56.762 54.628c-0.0066-0.22043-0.0093-0.44086-7e-3 -0.66169 0.17001-18.817 7.8827-37.983 23.352-47.818 5.6811-3.6118 12.32-5.6798 19.021-5.7536 0.10115-7.8585e-4 0.20619-0.001179 0.30734-0.001179v29.537c-0.83254-0.01965-1.669 0.12141-2.4642 0.45737-4.3763 1.8523-7.1345 6.602-8.865 11.348-1.5021 4.1191-2.1669 8.5434-2.1767 12.909l-0.01206 0.57132c-0.01362 0.2224-0.0319 0.44401-0.05524 0.66483 0.16067 11.134 2.2556 22.35 6.6607 32.593 4.9334 11.472 12.775 22.025 23.847 26.849 8.3526 3.6397 17.612 2.7811 25.182-1.5057 9.3991-5.3226 16.18-14.734 20.619-24.842 9.2202-21.006 9.4419-46.617 0.24121-68.173 8.9673-4.6444 8.9478-13.905 8.9478-13.905s8.3993 6.0185 17.845 2.2397c12.578 29.466 12.274 64.16-0.98815 93.182-7.7535 16.967-20.343 31.831-36.818 39.479-14.667 6.809-31.913 6.9792-46.591 0.58389-13.19-5.7489-23.918-16.106-31.637-28.15-11.179-17.443-16.472-38.678-16.496-59.604z"
fill="url(#c)"
fill-rule="nonzero"
/></g
></svg
>

View File

@@ -16,4 +16,5 @@ export { default as Fider } from './Fider.svelte';
export { default as Appwrite } from './Appwrite.svelte'; export { default as Appwrite } from './Appwrite.svelte';
export { default as Moodle } from './Moodle.svelte'; export { default as Moodle } from './Moodle.svelte';
export { default as GlitchTip } from './GlitchTip.svelte'; export { default as GlitchTip } from './GlitchTip.svelte';
export { default as Searxng } from './Searxng.svelte'; export { default as Searxng } from './Searxng.svelte';
export { default as Weblate } from './Weblate.svelte';

View File

@@ -209,7 +209,7 @@
"expose_a_port": "Expose a port", "expose_a_port": "Expose a port",
"enable_preview_deploy_mr_pr_requests": "Enable preview deployments from pull or merge requests.", "enable_preview_deploy_mr_pr_requests": "Enable preview deployments from pull or merge requests.",
"debug_logs": "Debug Logs", "debug_logs": "Debug Logs",
"enable_debug_log_during_build": "Enable debug logs during build phase.<br><span class='text-red-500 font-bold'>Sensitive information</span> could be visible and saved in logs.", "enable_debug_log_during_build": "Enable debug logs during build phase.<br><span class='text-settings font-bold'>Sensitive information</span> could be visible and saved in logs.",
"cant_activate_auto_deploy_without_repo": "Cannot activate automatic deployments until only one application is defined for this repository / branch.", "cant_activate_auto_deploy_without_repo": "Cannot activate automatic deployments until only one application is defined for this repository / branch.",
"no_applications_found": "No applications found", "no_applications_found": "No applications found",
"secret__batch_dot_env": "Paste .env file", "secret__batch_dot_env": "Paste .env file",
@@ -275,7 +275,7 @@
"application_id": "Application ID", "application_id": "Application ID",
"group_name": "Group Name", "group_name": "Group Name",
"oauth_id": "OAuth ID", "oauth_id": "OAuth ID",
"oauth_id_explainer": "The OAuth ID is the unique identifier of the GitLab application. <br>You can find it <span class='font-bold text-orange-600' >in the URL</span> of your GitLab OAuth Application.", "oauth_id_explainer": "The OAuth ID is the unique identifier of the GitLab application. <br>You can find it <span class='font-bold text-settings' >in the URL</span> of your GitLab OAuth Application.",
"register_oauth_gitlab": "Register new OAuth application on GitLab", "register_oauth_gitlab": "Register new OAuth application on GitLab",
"gitlab": { "gitlab": {
"self_hosted": "Instance-wide application (self-hosted)", "self_hosted": "Instance-wide application (self-hosted)",

View File

@@ -71,7 +71,7 @@
$appSession.version = baseSettings.version; $appSession.version = baseSettings.version;
$appSession.whiteLabeled = baseSettings.whiteLabeled; $appSession.whiteLabeled = baseSettings.whiteLabeled;
$appSession.whiteLabeledDetails.icon = baseSettings.whiteLabeledIcon; $appSession.whiteLabeledDetails.icon = baseSettings.whiteLabeledIcon;
$appSession.supportedServiceTypesAndVersions = supportedServiceTypesAndVersions $appSession.supportedServiceTypesAndVersions = supportedServiceTypesAndVersions;
export let userId: string; export let userId: string;
export let teamId: string; export let teamId: string;
@@ -88,6 +88,7 @@
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import { appSession } from '$lib/store'; import { appSession } from '$lib/store';
import Toasts from '$lib/components/Toasts.svelte'; import Toasts from '$lib/components/Toasts.svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
if (userId) $appSession.userId = userId; if (userId) $appSession.userId = userId;
if (teamId) $appSession.teamId = teamId; if (teamId) $appSession.teamId = teamId;
@@ -132,12 +133,12 @@
{/if} {/if}
<div class="flex flex-col space-y-2 py-2" class:mt-2={$appSession.whiteLabeled}> <div class="flex flex-col space-y-2 py-2" class:mt-2={$appSession.whiteLabeled}>
<a <a
id="dashboard"
sveltekit:prefetch sveltekit:prefetch
href="/" href="/"
class="icons tooltip tooltip-primary tooltip-right bg-coolgray-200 hover:text-white" class="icons bg-coolgray-200 hover:text-white"
class:text-white={$page.url.pathname === '/'} class:text-white={$page.url.pathname === '/'}
class:bg-coolgray-500={$page.url.pathname === '/'} class:bg-coolgray-500={$page.url.pathname === '/'}
data-tip="Dashboard"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -156,12 +157,13 @@
<path d="M16 15c-2.21 1.333 -5.792 1.333 -8 0" /> <path d="M16 15c-2.21 1.333 -5.792 1.333 -8 0" />
</svg> </svg>
</a> </a>
<div class="border-t border-stone-700" />
<div class="border-t border-stone-700" />
<a <a
id="applications"
sveltekit:prefetch sveltekit:prefetch
href="/applications" href="/applications"
class="icons tooltip tooltip-primary tooltip-right bg-coolgray-200" class="icons bg-coolgray-200"
class:text-applications={$page.url.pathname.startsWith('/applications') || class:text-applications={$page.url.pathname.startsWith('/applications') ||
$page.url.pathname.startsWith('/new/application')} $page.url.pathname.startsWith('/new/application')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/applications') || class:bg-coolgray-500={$page.url.pathname.startsWith('/applications') ||
@@ -186,10 +188,12 @@
<line x1="17" y1="4" x2="17" y2="10" /> <line x1="17" y1="4" x2="17" y2="10" />
</svg> </svg>
</a> </a>
<a <a
id="sources"
sveltekit:prefetch sveltekit:prefetch
href="/sources" href="/sources"
class="icons tooltip tooltip-primary tooltip-right bg-coolgray-200" class="icons bg-coolgray-200"
class:text-sources={$page.url.pathname.startsWith('/sources') || class:text-sources={$page.url.pathname.startsWith('/sources') ||
$page.url.pathname.startsWith('/new/source')} $page.url.pathname.startsWith('/new/source')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/sources') || class:bg-coolgray-500={$page.url.pathname.startsWith('/sources') ||
@@ -216,9 +220,10 @@
</svg> </svg>
</a> </a>
<a <a
id="destinations"
sveltekit:prefetch sveltekit:prefetch
href="/destinations" href="/destinations"
class="icons tooltip tooltip-primary tooltip-right bg-coolgray-200" class="icons bg-coolgray-200"
class:text-destinations={$page.url.pathname.startsWith('/destinations') || class:text-destinations={$page.url.pathname.startsWith('/destinations') ||
$page.url.pathname.startsWith('/new/destination')} $page.url.pathname.startsWith('/new/destination')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/destinations') || class:bg-coolgray-500={$page.url.pathname.startsWith('/destinations') ||
@@ -251,9 +256,10 @@
</a> </a>
<div class="border-t border-stone-700" /> <div class="border-t border-stone-700" />
<a <a
id="databases"
sveltekit:prefetch sveltekit:prefetch
href="/databases" href="/databases"
class="icons tooltip tooltip-primary tooltip-right bg-coolgray-200" class="icons bg-coolgray-200"
class:text-databases={$page.url.pathname.startsWith('/databases') || class:text-databases={$page.url.pathname.startsWith('/databases') ||
$page.url.pathname.startsWith('/new/database')} $page.url.pathname.startsWith('/new/database')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/databases') || class:bg-coolgray-500={$page.url.pathname.startsWith('/databases') ||
@@ -277,9 +283,10 @@
</svg> </svg>
</a> </a>
<a <a
id="services"
sveltekit:prefetch sveltekit:prefetch
href="/services" href="/services"
class="icons tooltip tooltip-primary tooltip-right bg-coolgray-200" class="icons bg-coolgray-200"
class:text-services={$page.url.pathname.startsWith('/services') || class:text-services={$page.url.pathname.startsWith('/services') ||
$page.url.pathname.startsWith('/new/service')} $page.url.pathname.startsWith('/new/service')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/services') || class:bg-coolgray-500={$page.url.pathname.startsWith('/services') ||
@@ -306,9 +313,10 @@
<UpdateAvailable /> <UpdateAvailable />
<div class="flex flex-col space-y-2 py-2"> <div class="flex flex-col space-y-2 py-2">
<a <a
id="iam"
sveltekit:prefetch sveltekit:prefetch
href="/iam" href="/iam"
class="icons tooltip tooltip-primary tooltip-right bg-coolgray-200" class="icons bg-coolgray-200"
class:text-iam={$page.url.pathname.startsWith('/iam')} class:text-iam={$page.url.pathname.startsWith('/iam')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/iam')} class:bg-coolgray-500={$page.url.pathname.startsWith('/iam')}
data-tip="IAM" data-tip="IAM"
@@ -330,9 +338,10 @@
</svg> </svg>
</a> </a>
<a <a
id="settings"
sveltekit:prefetch sveltekit:prefetch
href={$appSession.teamId === '0' ? '/settings/global' : '/settings/ssh-keys'} href={$appSession.teamId === '0' ? '/settings/global' : '/settings/ssh-keys'}
class="icons tooltip tooltip-primary tooltip-right bg-coolgray-200" class="icons bg-coolgray-200"
class:text-settings={$page.url.pathname.startsWith('/settings')} class:text-settings={$page.url.pathname.startsWith('/settings')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/settings')} class:bg-coolgray-500={$page.url.pathname.startsWith('/settings')}
data-tip="Settings" data-tip="Settings"
@@ -356,7 +365,8 @@
</a> </a>
<div <div
class="icons tooltip tooltip-primary tooltip-right bg-coolgray-200 hover:text-error" id="logout"
class="icons bg-coolgray-200 hover:text-error"
data-tip="Logout" data-tip="Logout"
on:click={logout} on:click={logout}
> >
@@ -396,7 +406,20 @@
{/if} {/if}
{/if} {/if}
<main> <main>
<div class="px-20"> <div class="pl-14 lg:px-20">
<slot /> <slot />
</div> </div>
</main> </main>
<Tooltip triggeredBy="#dashboard" placement="right">Dashboard</Tooltip>
<Tooltip triggeredBy="#applications" placement="right" color="bg-applications">Applications</Tooltip
>
<Tooltip triggeredBy="#sources" placement="right" color="bg-sources">Git Sources</Tooltip>
<Tooltip triggeredBy="#destinations" placement="right" color="bg-destinations">Destinations</Tooltip
>
<Tooltip triggeredBy="#databases" placement="right" color="bg-databases">Databases</Tooltip>
<Tooltip triggeredBy="#services" placement="right" color="bg-services">Services</Tooltip>
<Tooltip triggeredBy="#iam" placement="right" color="bg-iam">IAM</Tooltip>
<Tooltip triggeredBy="#settings" placement="right" color="bg-settings text-black">Settings</Tooltip
>
<Tooltip triggeredBy="#logout" placement="right" color="bg-red-600">Logout</Tooltip>

View File

@@ -1,72 +0,0 @@
<script lang="ts">
import Explainer from '$lib/components/Explainer.svelte';
export let setting: any;
export let title: any;
export let description: any;
export let isCenter = true;
export let disabled = false;
export let dataTooltip: any = null;
export let loading = false;
</script>
<div class="flex items-center py-4 pr-8">
<div class="flex w-96 flex-col">
<div class="text-xs font-bold text-stone-100 md:text-base">{title}</div>
<Explainer text={description} />
</div>
</div>
<div
class:tooltip={dataTooltip}
class:text-center={isCenter}
data-tip={dataTooltip}
class="flex justify-center"
>
<div
on:click
aria-pressed="false"
class="relative mx-20 inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out"
class:opacity-50={disabled || loading}
class:bg-green-600={!loading && setting}
class:bg-stone-700={!loading && !setting}
class:bg-yellow-500={loading}
>
<span class="sr-only">Use setting</span>
<span
class="pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow transition duration-200 ease-in-out"
class:translate-x-5={setting}
class:translate-x-0={!setting}
>
<span
class=" absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
class:opacity-0={setting}
class:opacity-100={!setting}
class:animate-spin={loading}
aria-hidden="true"
>
<svg class="h-3 w-3 bg-white text-red-600" fill="none" viewBox="0 0 12 12">
<path
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</span>
<span
class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-100 ease-out"
aria-hidden="true"
class:opacity-100={setting}
class:opacity-0={!setting}
class:animate-spin={loading}
>
<svg class="h-3 w-3 bg-white text-green-600" fill="currentColor" viewBox="0 0 12 12">
<path
d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"
/>
</svg>
</span>
</span>
</div>
</div>

View File

@@ -62,10 +62,10 @@
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { appSession, disabledButton, status, location, setLocation, addToast } from '$lib/store'; import { appSession, disabledButton, status, location, setLocation, addToast } from '$lib/store';
import { errorNotification, handlerNotFoundLoad } from '$lib/common'; import { errorNotification, handlerNotFoundLoad } from '$lib/common';
import Loading from '$lib/components/Loading.svelte'; import Tooltip from '$lib/components/Tooltip.svelte';
let loading = false;
let statusInterval: any; let statusInterval: any;
let forceDelete = false;
$disabledButton = $disabledButton =
!$appSession.isAdmin || !$appSession.isAdmin ||
(!application.fqdn && !application.settings.isBot) || (!application.fqdn && !application.settings.isBot) ||
@@ -78,7 +78,10 @@
async function handleDeploySubmit(forceRebuild = false) { async function handleDeploySubmit(forceRebuild = false) {
try { try {
const { buildId } = await post(`/applications/${id}/deploy`, { ...application, forceRebuild }); const { buildId } = await post(`/applications/${id}/deploy`, {
...application,
forceRebuild
});
addToast({ addToast({
message: $t('application.deployment_queued'), message: $t('application.deployment_queued'),
type: 'success' type: 'success'
@@ -95,25 +98,51 @@
} }
} }
async function deleteApplication(name: string) { async function deleteApplication(name: string, force: boolean) {
const sure = confirm($t('application.confirm_to_delete', { name })); const sure = confirm($t('application.confirm_to_delete', { name }));
if (sure) { if (sure) {
loading = true; $status.application.initialLoading = true;
try { try {
await del(`/applications/${id}`, { id }); await del(`/applications/${id}`, { id, force });
return await goto(`/applications`); return await goto(`/applications`);
} catch (error) { } catch (error) {
if (error.message.startsWith(`Command failed: SSH_AUTH_SOCK=/tmp/ssh-agent.pid`)) {
forceDelete = true;
}
return errorNotification(error); return errorNotification(error);
} finally {
$status.application.initialLoading = false;
} }
} }
} }
async function restartApplication() {
try {
$status.application.initialLoading = true;
$status.application.loading = true;
await post(`/applications/${id}/restart`, {});
addToast({
type: 'success',
message: 'Restart successful.'
});
} catch (error) {
return errorNotification(error);
} finally {
$status.application.initialLoading = false;
$status.application.loading = false;
await getStatus();
}
}
async function stopApplication() { async function stopApplication() {
try { try {
loading = true; $status.application.initialLoading = true;
// $status.application.loading = true;
await post(`/applications/${id}/stop`, {}); await post(`/applications/${id}/stop`, {});
return window.location.reload();
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally {
$status.application.initialLoading = false;
// $status.application.loading = false;
await getStatus();
} }
} }
async function getStatus() { async function getStatus() {
@@ -128,6 +157,9 @@
onDestroy(() => { onDestroy(() => {
$status.application.initialLoading = true; $status.application.initialLoading = true;
$status.application.isRunning = false;
$status.application.isExited = false;
$status.application.loading = false;
$location = null; $location = null;
clearInterval(statusInterval); clearInterval(statusInterval);
}); });
@@ -152,371 +184,396 @@
</script> </script>
<nav class="nav-side"> <nav class="nav-side">
{#if loading} {#if $location}
<Loading fullscreen cover /> <a
{:else} id="open"
{#if $location} href={$location}
<a target="_blank"
href={$location} class="icons flex items-center bg-transparent text-sm"
target="_blank" ><svg
class="icons tooltip-bottom flex items-center bg-transparent text-sm" xmlns="http://www.w3.org/2000/svg"
><svg class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
class="h-6 w-6" stroke-width="1.5"
viewBox="0 0 24 24" stroke="currentColor"
stroke-width="1.5" fill="none"
stroke="currentColor" stroke-linecap="round"
fill="none" stroke-linejoin="round"
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
> >
<div class="border border-coolgray-500 h-8" /> <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" />
{/if} <line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
{#if $status.application.isExited} </svg></a
<a >
href={!$disabledButton ? `/applications/${id}/logs` : null} <Tooltip triggeredBy="#open">Open</Tooltip>
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm flex items-center text-error"
data-tip="Application exited with an 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>
{/if}
{#if $status.application.initialLoading}
<button
class="icons tooltip-bottom flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
<line x1="5.63" y1="7.16" x2="5.63" y2="7.17" />
<line x1="4.06" y1="11" x2="4.06" y2="11.01" />
<line x1="4.63" y1="15.1" x2="4.63" y2="15.11" />
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
<line x1="11" y1="19.94" x2="11" y2="19.95" />
</svg>
</button>
{:else if $status.application.isRunning}
<button
on:click={stopApplication}
type="submit"
disabled={$disabledButton}
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm flex items-center space-x-2 text-error"
data-tip={$appSession.isAdmin
? $t('application.stop_application')
: $t('application.permission_denied_stop_application')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg>
</button>
<form on:submit|preventDefault={() => handleDeploySubmit(true)}>
<button
type="submit"
disabled={$disabledButton}
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm flex items-center space-x-2"
data-tip={$appSession.isAdmin
? 'Force Rebuild Application'
: 'You do not have permission to rebuild application.'}
>
<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="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>
</button>
</form>
{:else}
<form on:submit|preventDefault={() => handleDeploySubmit(false)}>
<button
type="submit"
disabled={$disabledButton}
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm flex items-center space-x-2 text-success"
data-tip={$appSession.isAdmin
? 'Deploy'
: 'You do not have permission to deploy application.'}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
</button>
</form>
{/if}
<div class="border border-coolgray-500 h-8" /> <div class="border border-coolgray-500 h-8" />
{/if}
{#if $status.application.isExited}
<a <a
href={!$disabledButton ? `/applications/${id}` : null} id="applicationerror"
href={!$disabledButton ? `/applications/${id}/logs` : null}
class="icons bg-transparent text-sm flex items-center text-error"
sveltekit:prefetch sveltekit:prefetch
class="hover:text-yellow-500 rounded"
class:text-yellow-500={$page.url.pathname === `/applications/${id}`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}`}
> >
<button <svg
disabled={$disabledButton} xmlns="http://www.w3.org/2000/svg"
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm" class="w-6 h-6"
data-tip="Configurations" viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentcolor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
> >
<svg <path stroke="none" d="M0 0h24v24H0z" fill="none" />
xmlns="http://www.w3.org/2000/svg" <path
class="h-6 w-6" 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"
viewBox="0 0 24 24" />
stroke-width="1.5" <line x1="12" y1="8" x2="12" y2="12" />
stroke="currentColor" <line x1="12" y1="16" x2="12.01" y2="16" />
fill="none" </svg>
stroke-linecap="round" </a>
stroke-linejoin="round" <Tooltip triggeredBy="#applicationerror">Application exited with an error!</Tooltip>
> {/if}
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> {#if $status.application.initialLoading}
<rect x="4" y="8" width="4" height="4" /> <button
<line x1="6" y1="4" x2="6" y2="8" /> class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out"
<line x1="6" y1="12" x2="6" y2="20" />
<rect x="10" y="14" width="4" height="4" />
<line x1="12" y1="4" x2="12" y2="14" />
<line x1="12" y1="18" x2="12" y2="20" />
<rect x="16" y="5" width="4" height="4" />
<line x1="18" y1="4" x2="18" y2="5" />
<line x1="18" y1="9" x2="18" y2="20" />
</svg></button
></a
> >
<a <svg
href={!$disabledButton ? `/applications/${id}/secrets` : null} xmlns="http://www.w3.org/2000/svg"
sveltekit:prefetch class="h-6 w-6"
class="hover:text-pink-500 rounded" viewBox="0 0 24 24"
class:text-pink-500={$page.url.pathname === `/applications/${id}/secrets`} stroke-width="1.5"
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/secrets`} stroke="currentColor"
> fill="none"
<button stroke-linecap="round"
disabled={$disabledButton} stroke-linejoin="round"
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm"
data-tip="Secrets"
> >
<svg <path stroke="none" d="M0 0h24v24H0z" fill="none" />
xmlns="http://www.w3.org/2000/svg" <path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
class="w-6 h-6" <line x1="5.63" y1="7.16" x2="5.63" y2="7.17" />
viewBox="0 0 24 24" <line x1="4.06" y1="11" x2="4.06" y2="11.01" />
stroke-width="1.5" <line x1="4.63" y1="15.1" x2="4.63" y2="15.11" />
stroke="currentColor" <line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
fill="none" <line x1="11" y1="19.94" x2="11" y2="19.95" />
stroke-linecap="round" </svg>
stroke-linejoin="round" </button>
> {:else if $status.application.isRunning}
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <button
<path id="stop"
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" on:click={stopApplication}
/> type="submit"
<circle cx="12" cy="11" r="1" /> disabled={$disabledButton}
<line x1="12" y1="12" x2="12" y2="14.5" /> class="icons bg-transparent text-sm flex items-center space-x-2 text-error"
</svg></button
></a
> >
<a <svg
href={!$disabledButton ? `/applications/${id}/storages` : null} xmlns="http://www.w3.org/2000/svg"
sveltekit:prefetch class="w-6 h-6"
class="hover:text-pink-500 rounded" viewBox="0 0 24 24"
class:text-pink-500={$page.url.pathname === `/applications/${id}/storages`} stroke-width="1.5"
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/storages`} stroke="currentColor"
> fill="none"
<button stroke-linecap="round"
disabled={$disabledButton} stroke-linejoin="round"
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm"
data-tip="Persistent Storages"
> >
<svg <path stroke="none" d="M0 0h24v24H0z" fill="none" />
xmlns="http://www.w3.org/2000/svg" <rect x="6" y="5" width="4" height="14" rx="1" />
class="w-6 h-6" <rect x="14" y="5" width="4" height="14" rx="1" />
viewBox="0 0 24 24" </svg>
stroke-width="1.5" </button>
stroke="currentColor" <Tooltip triggeredBy="#stop">Stop</Tooltip>
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<ellipse cx="12" cy="6" rx="8" ry="3" />
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
</svg>
</button></a
>
{#if !application.settings.isBot}
<a
href={!$disabledButton ? `/applications/${id}/previews` : null}
sveltekit:prefetch
class="hover:text-orange-500 rounded"
class:text-orange-500={$page.url.pathname === `/applications/${id}/previews`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/previews`}
>
<button
disabled={$disabledButton}
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm"
data-tip="Previews"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="7" cy="18" r="2" />
<circle cx="7" cy="6" r="2" />
<circle cx="17" cy="12" r="2" />
<line x1="7" y1="8" x2="7" y2="16" />
<path d="M7 8a4 4 0 0 0 4 4h4" />
</svg></button
></a
>
{/if}
<div class="border border-coolgray-500 h-8" />
<a
href={!$disabledButton && $status.application.isRunning ? `/applications/${id}/logs` : null}
sveltekit:prefetch
class="hover:text-sky-500 rounded"
class:text-sky-500={$page.url.pathname === `/applications/${id}/logs`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs`}
>
<button
disabled={$disabledButton || !$status.application.isRunning}
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm"
data-tip={$t('application.logs')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<line x1="3" y1="6" x2="3" y2="19" />
<line x1="12" y1="6" x2="12" y2="19" />
<line x1="21" y1="6" x2="21" y2="19" />
</svg>
</button></a
>
<a
href={!$disabledButton ? `/applications/${id}/logs/build` : null}
sveltekit:prefetch
class="hover:text-red-500 rounded"
class:text-red-500={$page.url.pathname === `/applications/${id}/logs/build`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs/build`}
>
<button
disabled={$disabledButton}
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm"
data-tip="Build Logs"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="19" cy="13" r="2" />
<circle cx="4" cy="17" r="2" />
<circle cx="13" cy="17" r="2" />
<line x1="13" y1="19" x2="4" y2="19" />
<line x1="4" y1="15" x2="13" y2="15" />
<path d="M8 12v-5h2a3 3 0 0 1 3 3v5" />
<path d="M5 15v-2a1 1 0 0 1 1 -1h7" />
<path d="M19 11v-7l-6 7" />
</svg>
</button></a
>
<div class="border border-coolgray-500 h-8" />
<button <button
on:click={() => deleteApplication(application.name)} id="restart"
on:click={restartApplication}
type="submit"
disabled={$disabledButton}
class="icons bg-transparent text-sm flex items-center space-x-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" />
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
</svg>
</button>
<Tooltip triggeredBy="#restart">Restart (useful to change secrets)</Tooltip>
<form on:submit|preventDefault={() => handleDeploySubmit(true)}>
<button
id="forceredeploy"
type="submit"
disabled={$disabledButton}
class="icons bg-transparent text-sm flex items-center space-x-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<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>
</button>
<Tooltip triggeredBy="#forceredeploy">Force redeploy (without cache)</Tooltip>
</form>
{:else}
<form on:submit|preventDefault={() => handleDeploySubmit(false)}>
<button
id="deploy"
type="submit"
disabled={$disabledButton}
class="icons bg-transparent text-sm flex items-center space-x-2 text-success"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
</button>
<Tooltip triggeredBy="#deploy">Deploy</Tooltip>
</form>
{/if}
<div class="border border-coolgray-500 h-8" />
<a
id="configurations"
href={!$disabledButton ? `/applications/${id}` : null}
sveltekit:prefetch
class="hover:text-yellow-500 rounded"
class:text-yellow-500={$page.url.pathname === `/applications/${id}`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}`}
>
<button disabled={$disabledButton} 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
>
<a
id="secrets"
href={!$disabledButton ? `/applications/${id}/secrets` : null}
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/applications/${id}/secrets`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/secrets`}
>
<button disabled={$disabledButton} class="icons bg-transparent text-sm">
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3"
/>
<circle cx="12" cy="11" r="1" />
<line x1="12" y1="12" x2="12" y2="14.5" />
</svg></button
></a
>
<a
id="persistentstorages"
href={!$disabledButton ? `/applications/${id}/storages` : null}
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/applications/${id}/storages`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/storages`}
>
<button disabled={$disabledButton} class="icons bg-transparent text-sm">
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<ellipse cx="12" cy="6" rx="8" ry="3" />
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
</svg>
</button></a
>
{#if !application.settings.isBot}
<a
id="previews"
href={!$disabledButton ? `/applications/${id}/previews` : null}
sveltekit:prefetch
class="hover:text-orange-500 rounded"
class:text-orange-500={$page.url.pathname === `/applications/${id}/previews`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/previews`}
>
<button disabled={$disabledButton} class="icons bg-transparent text-sm">
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="7" cy="18" r="2" />
<circle cx="7" cy="6" r="2" />
<circle cx="17" cy="12" r="2" />
<line x1="7" y1="8" x2="7" y2="16" />
<path d="M7 8a4 4 0 0 0 4 4h4" />
</svg></button
></a
>
{/if}
<div class="border border-coolgray-500 h-8" />
<a
id="applicationlogs"
href={!$disabledButton && $status.application.isRunning ? `/applications/${id}/logs` : null}
sveltekit:prefetch
class="hover:text-sky-500 rounded"
class:text-sky-500={$page.url.pathname === `/applications/${id}/logs`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs`}
>
<button
disabled={$disabledButton || !$status.application.isRunning}
class="icons bg-transparent text-sm"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<line x1="3" y1="6" x2="3" y2="19" />
<line x1="12" y1="6" x2="12" y2="19" />
<line x1="21" y1="6" x2="21" y2="19" />
</svg>
</button></a
>
<a
id="buildlogs"
href={!$disabledButton ? `/applications/${id}/logs/build` : null}
sveltekit:prefetch
class="hover:text-red-500 rounded"
class:text-red-500={$page.url.pathname === `/applications/${id}/logs/build`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs/build`}
>
<button disabled={$disabledButton} 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" />
<circle cx="19" cy="13" r="2" />
<circle cx="4" cy="17" r="2" />
<circle cx="13" cy="17" r="2" />
<line x1="13" y1="19" x2="4" y2="19" />
<line x1="4" y1="15" x2="13" y2="15" />
<path d="M8 12v-5h2a3 3 0 0 1 3 3v5" />
<path d="M5 15v-2a1 1 0 0 1 1 -1h7" />
<path d="M19 11v-7l-6 7" />
</svg>
</button></a
>
<div class="border border-coolgray-500 h-8" />
{#if forceDelete}
<button
on:click={() => deleteApplication(application.name, true)}
type="submit"
disabled={!$appSession.isAdmin}
class:bg-red-600={$appSession.isAdmin}
class:hover:bg-red-500={$appSession.isAdmin}
class="icons bg-transparent text-sm"
>
Force Delete
</button>
{:else}
<button
id="delete"
on:click={() => deleteApplication(application.name, false)}
type="submit" type="submit"
disabled={!$appSession.isAdmin} disabled={!$appSession.isAdmin}
class:hover:text-red-500={$appSession.isAdmin} class:hover:text-red-500={$appSession.isAdmin}
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm" class="icons bg-transparent text-sm"
data-tip={$appSession.isAdmin
? $t('application.delete_application')
: $t('application.permission_denied_delete_application')}
> >
<DeleteIcon /> <DeleteIcon />
</button> </button>
{/if} {/if}
</nav> </nav>
<slot /> <slot />
<Tooltip triggeredBy="#configurations">Configurations</Tooltip>
<Tooltip triggeredBy="#secrets">Secrets</Tooltip>
<Tooltip triggeredBy="#persistentstorages">Persistent Storages</Tooltip>
<Tooltip triggeredBy="#previews">Previews</Tooltip>
<Tooltip triggeredBy="#applicationlogs">Application Logs</Tooltip>
<Tooltip triggeredBy="#buildlogs">Build Logs</Tooltip>
<Tooltip triggeredBy="#delete">Delete</Tooltip>

View File

@@ -95,19 +95,19 @@
async function isBranchAlreadyUsed(event: any) { async function isBranchAlreadyUsed(event: any) {
selected.branch = event.detail.value; selected.branch = event.detail.value;
try { try {
const data = await get( // const data = await get(
`/applications/${id}/configuration/repository?repository=${selected.repository}&branch=${selected.branch}` // `/applications/${id}/configuration/repository?repository=${selected.repository}&branch=${selected.branch}`
); // );
if (data.used) { // if (data.used) {
const sure = confirm($t('application.configuration.branch_already_in_use')); // const sure = confirm($t('application.configuration.branch_already_in_use'));
if (sure) { // if (sure) {
selected.autodeploy = false; // selected.autodeploy = false;
showSave = true; // showSave = true;
return true; // return true;
} // }
showSave = false; // showSave = false;
return true; // return true;
} // }
showSave = true; showSave = true;
} catch (error) { } catch (error) {
showSave = false; showSave = false;

View File

@@ -169,10 +169,6 @@
} }
} }
} }
function selectBranch(event: any) {
selected.branch = event.detail;
isBranchAlreadyUsed();
}
async function loadBranches(page: number = 1) { async function loadBranches(page: number = 1) {
let perPage = 100; let perPage = 100;
//@ts-ignore //@ts-ignore
@@ -199,21 +195,22 @@
} }
} }
async function isBranchAlreadyUsed() { async function isBranchAlreadyUsed(event) {
selected.branch = event.detail;
try { try {
const data = await get( // const data = await get(
`/applications/${id}/configuration/repository?repository=${selected.project.path_with_namespace}&branch=${selected.branch.name}` // `/applications/${id}/configuration/repository?repository=${selected.project.path_with_namespace}&branch=${selected.branch.name}`
); // );
if (data.used) { // if (data.used) {
const sure = confirm($t('application.configuration.branch_already_in_use')); // const sure = confirm($t('application.configuration.branch_already_in_use'));
if (sure) { // if (sure) {
autodeploy = false; // autodeploy = false;
showSave = true; // showSave = true;
return true; // return true;
} // }
showSave = false; // showSave = false;
return true; // return true;
} // }
showSave = true; showSave = true;
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
@@ -227,9 +224,7 @@
} }
} }
async function setWebhook(url: any, webhookToken: any) { async function setWebhook(url: any, webhookToken: any) {
const host = dev const host = dev ? getWebhookUrl('gitlab') : `${window.location.origin}/webhooks/gitlab/events`;
? getWebhookUrl('gitlab')
: `${window.location.origin}/webhooks/gitlab/events`;
try { try {
await post( await post(
url, url,
@@ -294,17 +289,15 @@
); );
await post(updateDeployKeyIdUrl, { deployKeyId: id }); await post(updateDeployKeyIdUrl, { deployKeyId: id });
} catch (error) { } catch (error) {
return errorNotification(error);
} finally {
loading.save = false; loading.save = false;
return errorNotification(error);
} }
try { try {
await setWebhook(webhookUrl, webhookToken); await setWebhook(webhookUrl, webhookToken);
} catch (error) { } catch (error) {
return errorNotification(error);
} finally {
loading.save = false; loading.save = false;
return errorNotification(error);
} }
const url = `/applications/${id}/configuration/repository`; const url = `/applications/${id}/configuration/repository`;
@@ -317,11 +310,11 @@
autodeploy, autodeploy,
webhookToken webhookToken
}); });
loading.save = false;
return await goto(from || `/applications/${id}/configuration/buildpack`); return await goto(from || `/applications/${id}/configuration/buildpack`);
} catch (error) { } catch (error) {
return errorNotification(error);
} finally {
loading.save = false; loading.save = false;
return errorNotification(error);
} }
} }
async function handleSubmit() { async function handleSubmit() {
@@ -396,7 +389,7 @@
showIndicator={!loading.branches} showIndicator={!loading.branches}
isWaiting={loading.branches} isWaiting={loading.branches}
isDisabled={loading.branches || !selected.project} isDisabled={loading.branches || !selected.project}
on:select={selectBranch} on:select={isBranchAlreadyUsed}
on:clear={() => { on:clear={() => {
showSave = false; showSave = false;
selected.branch = null; selected.branch = null;
@@ -425,7 +418,7 @@
configuration <a href={`/sources/${application.gitSource.id}`}>here.</a> configuration <a href={`/sources/${application.gitSource.id}`}>here.</a>
</div> </div>
<button <button
class="w-40 bg-green-600" class="btn btn-sm w-40 bg-green-600"
on:click|stopPropagation|preventDefault={() => window.location.reload()} on:click|stopPropagation|preventDefault={() => window.location.reload()}
> >
Try again Try again

View File

@@ -4,7 +4,6 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import Select from 'svelte-select'; import Select from 'svelte-select';
import Explainer from '$lib/components/Explainer.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
@@ -164,7 +163,6 @@
<div class="space-y-4"> <div class="space-y-4">
<input <input
placeholder="eg: https://github.com/coollabsio/nodejs-example/tree/main" placeholder="eg: https://github.com/coollabsio/nodejs-example/tree/main"
class="text-xs"
bind:value={publicRepositoryLink} bind:value={publicRepositoryLink}
/> />
{#if branchSelectOptions.length > 0} {#if branchSelectOptions.length > 0}
@@ -193,7 +191,5 @@
</form> </form>
</div> </div>
</div> </div>
<Explainer
text="Examples:<br><br>https://github.com/coollabsio/nodejs-example<br>https://github.com/coollabsio/nodejs-example/tree/main<br>https://gitlab.com/aleveha/fastify-example<br>https://gitlab.com/aleveha/fastify-example/-/tree/master<br><br>Only works with Github.com and Gitlab.com."
/>
</div> </div>

View File

@@ -32,7 +32,7 @@
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import { appSession } from '$lib/store'; import { appSession } from '$lib/store';
import PublicRepository from './_PublicRepository.svelte'; import PublicRepository from './_PublicRepository.svelte';
import Explainer from '$lib/components/Explainer.svelte'; import DocLink from '$lib/components/DocLink.svelte';
const { id } = $page.params; const { id } = $page.params;
const from = $page.url.searchParams.get('from'); const from = $page.url.searchParams.get('from');
@@ -192,7 +192,9 @@ import Explainer from '$lib/components/Explainer.svelte';
</div> </div>
{/if} {/if}
</div> </div>
<div class="title py-4">Public Repository</div> <div class="flex items-center">
<div class="title py-4">Public Repository</div>
<PublicRepository /> <DocLink url="https://docs.coollabs.io/coolify/applications/#public-repository" />
</div>
<PublicRepository />
</div> </div>

View File

@@ -32,14 +32,15 @@
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import Select from 'svelte-select'; import Select from 'svelte-select';
import Explainer from '$lib/components/Explainer.svelte';
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import cuid from 'cuid'; import cuid from 'cuid';
import { browser } from '$app/env';
import { addToast, appSession, disabledButton, setLocation, status } from '$lib/store'; import { addToast, appSession, disabledButton, setLocation, status } from '$lib/store';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common'; import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common';
import Setting from './_Setting.svelte'; import Setting from '$lib/components/Setting.svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
import Explainer from '$lib/components/Explainer.svelte';
const { id } = $page.params; const { id } = $page.params;
$: isDisabled = $: isDisabled =
@@ -281,6 +282,7 @@
</div> </div>
{#if application.gitSource?.htmlUrl && application.repository && application.branch} {#if application.gitSource?.htmlUrl && application.repository && application.branch}
<a <a
id="git"
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}" href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank" target="_blank"
class="w-10" class="w-10"
@@ -321,6 +323,7 @@
</svg> </svg>
{/if} {/if}
</a> </a>
<Tooltip triggeredBy="#git">Open on Git</Tooltip>
{/if} {/if}
</div> </div>
@@ -426,7 +429,7 @@
> >
{/if} {/if}
</div> </div>
<div class="grid grid-cols-2 items-center pb-8"> <div class="grid grid-cols-2 items-center">
<label for="destination" class="text-base font-bold text-stone-100" <label for="destination" class="text-base font-bold text-stone-100"
>{$t('application.destination')}</label >{$t('application.destination')}</label
> >
@@ -440,10 +443,15 @@
</div> </div>
</div> </div>
{#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'} {#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'}
<div class="grid grid-cols-2 items-center pb-8"> <div class="grid grid-cols-2 items-center">
<label for="baseBuildImage" class="text-base font-bold text-stone-100" <label for="baseBuildImage" class="text-base font-bold text-stone-100"
>{$t('application.base_build_image')}</label >{$t('application.base_build_image')}
> <Explainer
explanation={application.buildPack === 'laravel'
? 'For building frontend assets with webpack.'
: 'Image that will be used during the build process.'}
/>
</label>
<div class="custom-select-wrapper"> <div class="custom-select-wrapper">
<Select <Select
@@ -457,17 +465,13 @@
isClearable={false} isClearable={false}
/> />
</div> </div>
{#if application.buildPack === 'laravel'}
<Explainer text="For building frontend assets with webpack." />
{:else}
<Explainer text={$t('application.base_build_image_explainer')} />
{/if}
</div> </div>
{/if} {/if}
{#if application.buildPack !== 'docker'} {#if application.buildPack !== 'docker'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="baseImage" class="text-base font-bold text-stone-100" <label for="baseImage" class="text-base font-bold text-stone-100"
>{$t('application.base_image')}</label >{$t('application.base_image')}
<Explainer explanation={'Image that will be used for the deployment.'} /></label
> >
<div class="custom-select-wrapper"> <div class="custom-select-wrapper">
<Select <Select
@@ -481,13 +485,15 @@
isClearable={false} isClearable={false}
/> />
</div> </div>
<Explainer text={$t('application.base_image_explainer')} />
</div> </div>
{/if} {/if}
{#if application.buildPack !== 'docker' && (application.buildPack === 'nextjs' || application.buildPack === 'nuxtjs')} {#if application.buildPack !== 'docker' && (application.buildPack === 'nextjs' || application.buildPack === 'nuxtjs')}
<div class="grid grid-cols-2 items-center pb-8"> <div class="grid grid-cols-2 items-center pb-8">
<label for="deploymentType" class="text-base font-bold text-stone-100" <label for="deploymentType" class="text-base font-bold text-stone-100"
>Deployment Type</label >Deployment Type
<Explainer
explanation={"Defines how to deploy your application. <br><br><span class='text-green-500 font-bold'>Static</span> is for static websites, <span class='text-green-500 font-bold'>node</span> is for server-side applications."}
/></label
> >
<div class="custom-select-wrapper"> <div class="custom-select-wrapper">
<Select <Select
@@ -501,9 +507,6 @@
isClearable={false} isClearable={false}
/> />
</div> </div>
<Explainer
text="Defines how to deploy your application. <br><br><span class='text-green-500 font-bold'>Static</span> is for static websites, <span class='text-green-500 font-bold'>node</span> is for server-side applications."
/>
</div> </div>
{/if} {/if}
</div> </div>
@@ -513,32 +516,39 @@
<div class="grid grid-flow-row gap-2 px-10"> <div class="grid grid-flow-row gap-2 px-10">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
id="isBot"
isCenter={false} isCenter={false}
bind:setting={isBot} bind:setting={isBot}
on:click={() => changeSettings('isBot')} on:click={() => changeSettings('isBot')}
title="Is your application a bot?" title="Is your application a bot?"
description="You can deploy applications without domains. <br>You can also make them to listen on <span class='text-green-500 font-bold'>IP:EXPOSEDPORT</span> as well.<br></Setting><br>Useful to host <span class='text-green-500 font-bold'>Twitch bots, regular jobs, or anything that does not require an incoming connection.</span>" description="You can deploy applications without domains or make them to listen on the <span class='text-settings font-bold'>Exposed Port</span>.<br></Setting><br>Useful to host <span class='text-settings font-bold'>Twitch bots, regular jobs, or anything that does not require an incoming HTTP connection.</span>"
disabled={$status.application.isRunning} disabled={$status.application.isRunning}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center">
<Setting
id="dualCerts"
dataTooltip={$t('forms.must_be_stopped_to_modify')}
disabled={$status.application.isRunning}
isCenter={false}
bind:setting={dualCerts}
title={$t('application.ssl_www_and_non_www')}
description="It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-settings'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both."
on:click={() => !$status.application.isRunning && changeSettings('dualCerts')}
/>
</div>
{#if !isBot} {#if !isBot}
<div class="grid grid-cols-2"> <div class="grid grid-cols-2 items-center">
<div class="flex-col"> <label for="fqdn" class="text-base font-bold text-stone-100"
<label for="fqdn" class="pt-2 text-base font-bold text-stone-100" >{$t('application.url_fqdn')}
>{$t('application.url_fqdn')}</label <Explainer
> explanation={"If you specify <span class='text-settings font-bold'>https</span>, the application will be accessible only over https.<br>SSL certificate will be generated automatically.<br><br>If you specify <span class='text-settings font-bold'>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the domain, you must first stop the application.<br><br><span class='text-settings font-bold'>You must set your DNS to point to the server IP in advance.</span>"}
{#if browser && window.location.hostname === 'demo.coolify.io'} />
<Explainer </label>
text="<span class='text-white font-bold'>You can use the predefined random url name or enter your own domain name.</span>"
/>
{/if}
<Explainer text={$t('application.https_explainer')} />
</div>
<div> <div>
<input <input
readonly={isDisabled} readonly={isDisabled}
disabled={isDisabled} disabled={isDisabled}
bind:this={domainEl}
name="fqdn" name="fqdn"
id="fqdn" id="fqdn"
required required
@@ -582,17 +592,6 @@
{/if} {/if}
</div> </div>
</div> </div>
<div class="grid grid-cols-2 items-center pb-8">
<Setting
dataTooltip={$t('forms.must_be_stopped_to_modify')}
disabled={$status.application.isRunning}
isCenter={false}
bind:setting={dualCerts}
title={$t('application.ssl_www_and_non_www')}
description={$t('application.ssl_explainer')}
on:click={() => !$status.application.isRunning && changeSettings('dualCerts')}
/>
</div>
{/if} {/if}
{#if application.buildPack === 'python'} {#if application.buildPack === 'python'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
@@ -645,7 +644,10 @@
{/if} {/if}
{#if !staticDeployments.includes(application.buildPack)} {#if !staticDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="port" class="text-base font-bold text-stone-100">{$t('forms.port')}</label> <label for="port" class="text-base font-bold text-stone-100"
>{$t('forms.port')}
<Explainer explanation={'The port your application listens on.'} /></label
>
<input <input
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
@@ -654,11 +656,14 @@
bind:value={application.port} bind:value={application.port}
placeholder="{$t('forms.default')}: 'python' ? '8000' : '3000'" placeholder="{$t('forms.default')}: 'python' ? '8000' : '3000'"
/> />
<Explainer text={'The port your application listens on.'} />
</div> </div>
{/if} {/if}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="exposePort" class="text-base font-bold text-stone-100">Exposed Port</label> <label for="exposePort" class="text-base font-bold text-stone-100"
>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 <input
readonly={!$appSession.isAdmin && !$status.application.isRunning} readonly={!$appSession.isAdmin && !$status.application.isRunning}
disabled={isDisabled} disabled={isDisabled}
@@ -667,12 +672,9 @@
bind:value={application.exposePort} bind:value={application.exposePort}
placeholder="12345" placeholder="12345"
/> />
<Explainer
text={'You can expose your application to a port on the host system.<br><br>Useful if you would like to use your own reverse proxy or tunnel and also in development mode. Otherwise leave empty.'}
/>
</div> </div>
{#if !notNodeDeployments.includes(application.buildPack)} {#if !notNodeDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center pt-4"> <div class="grid grid-cols-2 items-center">
<label for="installCommand" class="text-base font-bold text-stone-100" <label for="installCommand" class="text-base font-bold text-stone-100"
>{$t('application.install_command')}</label >{$t('application.install_command')}</label
> >
@@ -715,7 +717,9 @@
{#if application.buildPack === 'docker'} {#if application.buildPack === 'docker'}
<div class="grid grid-cols-2 items-center pt-4"> <div class="grid grid-cols-2 items-center pt-4">
<label for="dockerFileLocation" class="text-base font-bold text-stone-100" <label for="dockerFileLocation" class="text-base font-bold text-stone-100"
>Dockerfile Location</label >Dockerfile Location <Explainer
explanation={"Should be absolute path, like <span class='text-settings font-bold'>/data/Dockerfile</span> or <span class='text-settings font-bold'>/Dockerfile.</span>"}
/></label
> >
<input <input
disabled={isDisabled} disabled={isDisabled}
@@ -725,9 +729,6 @@
bind:value={application.dockerFileLocation} bind:value={application.dockerFileLocation}
placeholder="default: /Dockerfile" placeholder="default: /Dockerfile"
/> />
<Explainer
text="Should be absolute path, like <span class='text-green-500 font-bold'>/data/Dockerfile</span> or <span class='text-green-500 font-bold'>/Dockerfile.</span>"
/>
</div> </div>
{/if} {/if}
{#if application.buildPack === 'deno'} {#if application.buildPack === 'deno'}
@@ -743,7 +744,11 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="denoOptions" class="text-base font-bold text-stone-100">Arguments</label> <label for="denoOptions" class="text-base font-bold text-stone-100"
>Arguments <Explainer
explanation={"List of arguments to pass to <span class='text-settings font-bold'>deno run</span> command. Could include permissions, configurations files, etc."}
/></label
>
<input <input
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
@@ -752,18 +757,17 @@
bind:value={application.denoOptions} bind:value={application.denoOptions}
placeholder="eg: --allow-net --allow-hrtime --config path/to/file.json" placeholder="eg: --allow-net --allow-hrtime --config path/to/file.json"
/> />
<Explainer
text="List of arguments to pass to <span class='text-green-500 font-bold'>deno run</span> command. Could include permissions, configurations files, etc."
/>
</div> </div>
{/if} {/if}
{#if application.buildPack !== 'laravel' && application.buildPack !== 'heroku'} {#if application.buildPack !== 'laravel' && application.buildPack !== 'heroku'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<div class="flex-col"> <div class="flex-col">
<label for="baseDirectory" class="pt-2 text-base font-bold text-stone-100" <label for="baseDirectory" class="pt-2 text-base font-bold text-stone-100"
>{$t('forms.base_directory')}</label >{$t('forms.base_directory')}
<Explainer
explanation={"Directory to use as the base for all commands.<br>Could be useful with <span class='text-settings font-bold'>monorepos</span>."}
/></label
> >
<Explainer text={$t('application.directory_to_use_explainer')} />
</div> </div>
<input <input
disabled={isDisabled} disabled={isDisabled}
@@ -779,9 +783,11 @@
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<div class="flex-col"> <div class="flex-col">
<label for="publishDirectory" class="pt-2 text-base font-bold text-stone-100" <label for="publishDirectory" class="pt-2 text-base font-bold text-stone-100"
>{$t('forms.publish_directory')}</label >{$t('forms.publish_directory')}
<Explainer
explanation={"Directory containing all the assets for deployment. <br> For example: <span class='text-settings font-bold'>dist</span>,<span class='text-settings font-bold'>_site</span> or <span class='text-settings font-bold'>public</span>."}
/></label
> >
<Explainer text={$t('application.publish_directory_explainer')} />
</div> </div>
<input <input
@@ -804,6 +810,7 @@
{#if !application.settings.isPublicRepository} {#if !application.settings.isPublicRepository}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
id="autodeploy"
isCenter={false} isCenter={false}
bind:setting={autodeploy} bind:setting={autodeploy}
on:click={() => changeSettings('autodeploy')} on:click={() => changeSettings('autodeploy')}
@@ -812,9 +819,10 @@
/> />
</div> </div>
{/if} {/if}
{#if !application.settings.isBot} {#if !application.settings.isBot && !application.settings.isPublicRepository}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
id="previews"
isCenter={false} isCenter={false}
bind:setting={previews} bind:setting={previews}
on:click={() => changeSettings('previews')} on:click={() => changeSettings('previews')}
@@ -825,6 +833,7 @@
{/if} {/if}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
id="debug"
isCenter={false} isCenter={false}
bind:setting={debug} bind:setting={debug}
on:click={() => changeSettings('debug')} on:click={() => changeSettings('debug')}

View File

@@ -6,14 +6,13 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import Loading from '$lib/components/Loading.svelte';
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import LoadingLogs from '$lib/components/LoadingLogs.svelte'; import LoadingLogs from '$lib/components/LoadingLogs.svelte';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import Tooltip from '$lib/components/Tooltip.svelte';
let logs: any = []; let logs: any = [];
let loading = true;
let currentStatus: any; let currentStatus: any;
let streamInterval: any; let streamInterval: any;
let followingBuild: any; let followingBuild: any;
@@ -46,7 +45,6 @@
logs = logs.concat( logs = logs.concat(
responseLogs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) })) responseLogs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) }))
); );
loading = false;
streamInterval = setInterval(async () => { streamInterval = setInterval(async () => {
if (status !== 'running' && status !== 'queued') { if (status !== 'running' && status !== 'queued') {
clearInterval(streamInterval); clearInterval(streamInterval);
@@ -63,7 +61,7 @@
logs = logs.concat( logs = logs.concat(
data.logs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) })) data.logs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) }))
); );
dispatch('updateBuildStatus', { status }); dispatch('updateBuildStatus', { status, took: data.took });
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} }
@@ -96,84 +94,82 @@
}); });
</script> </script>
{#if loading} <div class="relative ">
<Loading /> {#if currentStatus === 'running'}
{:else} <LoadingLogs />
<div class="relative "> {/if}
{#if currentStatus === 'running'} {#if currentStatus === 'queued'}
<LoadingLogs /> <div class="text-center font-bold text-xl">{$t('application.build.queued_waiting_exec')}</div>
{/if} {:else}
{#if currentStatus === 'queued'} <div class="flex justify-end sticky top-0 p-2 mx-1">
<div class="text-center font-bold text-xl">{$t('application.build.queued_waiting_exec')}</div> <button
{:else} id="follow"
<div class="flex justify-end sticky top-0 p-2 mx-1"> on:click={followBuild}
class="bg-transparent btn btn-sm btn-linkhover:text-green-500 hover:bg-coolgray-500"
class:text-green-500={followingBuild}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" />
<line x1="8" y1="12" x2="12" y2="16" />
<line x1="12" y1="8" x2="12" y2="16" />
<line x1="16" y1="12" x2="12" y2="16" />
</svg>
</button>
<Tooltip triggeredBy="#follow">Follow Logs</Tooltip>
{#if currentStatus === 'running'}
<button <button
on:click={followBuild} id="cancel"
class="bg-transparent btn btn-sm btn-link tooltip tooltip-primary tooltip-bottom hover:text-green-500 hover:bg-coolgray-500" on:click={cancelBuild}
data-tip="Follow logs" class:animation-spin={cancelInprogress}
class:text-green-500={followingBuild} class="bg-transparent btn btn-sm btn-link hover:text-red-500 hover:bg-coolgray-500"
> >
<svg {#if cancelInprogress}
xmlns="http://www.w3.org/2000/svg" Cancelling...
class="w-6 h-6" {:else}
viewBox="0 0 24 24" <svg
stroke-width="1.5" xmlns="http://www.w3.org/2000/svg"
stroke="currentColor" class="w-6 h-6"
fill="none" viewBox="0 0 24 24"
stroke-linecap="round" stroke-width="1.5"
stroke-linejoin="round" stroke="currentColor"
> fill="none"
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> stroke-linecap="round"
<circle cx="12" cy="12" r="9" /> stroke-linejoin="round"
<line x1="8" y1="12" x2="12" y2="16" /> >
<line x1="12" y1="8" x2="12" y2="16" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<line x1="16" y1="12" x2="12" y2="16" /> <circle cx="12" cy="12" r="9" />
</svg> <path d="M10 10l4 4m0 -4l-4 4" />
</svg>
{/if}
</button> </button>
{#if currentStatus === 'running'} <Tooltip triggeredBy="#cancel">Cancel build</Tooltip>
<button
on:click={cancelBuild}
class:animation-spin={cancelInprogress}
class="bg-transparent btn btn-sm btn-link hover:text-red-500 hover:bg-coolgray-500 tooltip tooltip-primary tooltip-bottom"
data-tip="Cancel build"
>
{#if cancelInprogress}
Cancelling...
{: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"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" />
<path d="M10 10l4 4m0 -4l-4 4" />
</svg>
{/if}
</button>
{/if}
</div>
{#if logs.length > 0}
<div
class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
bind:this={logsEl}
>
{#each logs as log}
<div>{log.line + '\n'}</div>
{/each}
</div>
{:else}
<div
class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
>
No logs found.
</div>
{/if} {/if}
</div>
{#if logs.length > 0}
<div
class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
bind:this={logsEl}
>
{#each logs as log}
<div>{log.line + '\n'}</div>
{/each}
</div>
{:else}
<div
class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
>
No logs found.
</div>
{/if} {/if}
</div> {/if}
{/if} </div>

View File

@@ -28,17 +28,20 @@
import { get } from '$lib/api'; import { get } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { changeQueryParams, dateOptions, errorNotification } from '$lib/common'; import { changeQueryParams, dateOptions, errorNotification } from '$lib/common';
import Tooltip from '$lib/components/Tooltip.svelte';
let buildId: any; let buildId: any;
let skip = 0; let skip = 0;
let noMoreBuilds = buildCount < 5 || buildCount <= skip; let noMoreBuilds = buildCount < 5 || buildCount <= skip;
let buildTook = 0;
const { id } = $page.params; const { id } = $page.params;
let preselectedBuildId = $page.url.searchParams.get('buildId'); let preselectedBuildId = $page.url.searchParams.get('buildId');
if (preselectedBuildId) buildId = preselectedBuildId; if (preselectedBuildId) buildId = preselectedBuildId;
async function updateBuildStatus({ detail }: { detail: any }) { async function updateBuildStatus({ detail }: { detail: any }) {
const { status } = detail; const { status, took } = detail;
if (status !== 'running') { if (status !== 'running') {
try { try {
const data = await get(`/applications/${id}/logs/build?buildId=${buildId}`); const data = await get(`/applications/${id}/logs/build?buildId=${buildId}`);
@@ -58,6 +61,7 @@
if (build.id === buildId) build.status = status; if (build.id === buildId) build.status = status;
return build; return build;
}); });
buildTook = took;
} }
} }
async function loadMoreBuilds() { async function loadMoreBuilds() {
@@ -137,20 +141,18 @@
<div class="top-4 md:sticky"> <div class="top-4 md:sticky">
{#each builds as build, index (build.id)} {#each builds as build, index (build.id)}
<div <div
data-tip={new Intl.DateTimeFormat('default', dateOptions).format( id={`building-${build.id}`}
new Date(build.createdAt)
) + `\n${build.status}`}
on:click={() => loadBuild(build.id)} on:click={() => loadBuild(build.id)}
class:rounded-tr={index === 0} class:rounded-tr={index === 0}
class:rounded-br={index === builds.length - 1} class:rounded-br={index === builds.length - 1}
class="tooltip tooltip-primary tooltip-top flex cursor-pointer items-center justify-center border-l-2 py-4 no-underline transition-all duration-100 hover:bg-coolgray-400 hover:shadow-xl" class="flex cursor-pointer items-center justify-center border-l-2 py-4 no-underline transition-all duration-100 hover:bg-coolgray-400 hover:shadow-xl"
class:bg-coolgray-400={buildId === build.id} class:bg-coolgray-400={buildId === build.id}
class:border-red-500={build.status === 'failed'} class:border-red-500={build.status === 'failed'}
class:border-orange-500={build.status === 'canceled'} class:border-orange-500={build.status === 'canceled'}
class:border-green-500={build.status === 'success'} class:border-green-500={build.status === 'success'}
class:border-yellow-500={build.status === 'running'} class:border-yellow-500={build.status === 'running'}
> >
<div class="flex-col px-2"> <div class="flex-col px-2 text-center min-w-[10rem]">
<div class="text-sm font-bold"> <div class="text-sm font-bold">
{build.branch || application.branch} {build.branch || application.branch}
</div> </div>
@@ -162,6 +164,10 @@
<div class="w-48 text-center text-xs"> <div class="w-48 text-center text-xs">
{#if build.status === 'running'} {#if build.status === 'running'}
<div class="font-bold">{$t('application.build.running')}</div> <div class="font-bold">{$t('application.build.running')}</div>
<div>
Elapsed
<span class="font-bold">{buildTook}s</span>
</div>
{:else if build.status === 'queued'} {:else if build.status === 'queued'}
<div class="font-bold">{$t('application.build.queued')}</div> <div class="font-bold">{$t('application.build.queued')}</div>
{:else} {:else}
@@ -172,6 +178,10 @@
{/if} {/if}
</div> </div>
</div> </div>
<Tooltip triggeredBy={`#building-${build.id}`}
>{new Intl.DateTimeFormat('default', dateOptions).format(new Date(build.createdAt)) +
`\n${build.status}`}</Tooltip
>
{/each} {/each}
</div> </div>
{#if !noMoreBuilds} {#if !noMoreBuilds}

View File

@@ -5,6 +5,7 @@
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import LoadingLogs from '$lib/components/LoadingLogs.svelte'; import LoadingLogs from '$lib/components/LoadingLogs.svelte';
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
let application: any = {}; let application: any = {};
let logsLoading = false; let logsLoading = false;
@@ -146,9 +147,9 @@
{/if} {/if}
<div class="flex justify-end sticky top-0 p-1 mx-1"> <div class="flex justify-end sticky top-0 p-1 mx-1">
<button <button
id="follow"
on:click={followBuild} on:click={followBuild}
class="bg-transparent btn btn-sm btn-link tooltip tooltip-primary tooltip-bottom" class="bg-transparent btn btn-sm btn-link"
data-tip="Follow logs"
class:text-green-500={followingLogs} class:text-green-500={followingLogs}
> >
<svg <svg
@@ -168,6 +169,7 @@
<line x1="16" y1="12" x2="12" y2="16" /> <line x1="16" y1="12" x2="12" y2="16" />
</svg> </svg>
</button> </button>
<Tooltip triggeredBy="#follow">Follow Logs</Tooltip>
</div> </div>
<div <div
class="font-mono w-full leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200" class="font-mono w-full leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"

View File

@@ -21,13 +21,13 @@
import Secret from './_Secret.svelte'; import Secret from './_Secret.svelte';
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import { page } from '$app/stores'; import { page } from '$app/stores';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { errorNotification, getDomain } from '$lib/common'; import { errorNotification, getDomain } from '$lib/common';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Loading from '$lib/components/Loading.svelte'; import Loading from '$lib/components/Loading.svelte';
import { addToast } from '$lib/store'; import { addToast } from '$lib/store';
import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
const { id } = $page.params; const { id } = $page.params;
@@ -145,7 +145,7 @@
{:else} {:else}
<div class="mx-auto max-w-6xl px-6 pt-4"> <div class="mx-auto max-w-6xl px-6 pt-4">
<div class="flex justify-center py-4 text-center"> <div class="flex justify-center py-4 text-center">
<Explainer <SimpleExplainer
customClass="w-full" customClass="w-full"
text={applicationSecrets.length === 0 text={applicationSecrets.length === 0
? "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments." ? "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."
@@ -194,8 +194,9 @@
</div> </div>
</a> </a>
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<button class="btn btn-sm bg-coollabs hover:bg-coollabs-100" on:click={() => redeploy(container)} <button
>{$t('application.preview.redeploy')}</button class="btn btn-sm bg-coollabs hover:bg-coollabs-100"
on:click={() => redeploy(container)}>{$t('application.preview.redeploy')}</button
> >
</div> </div>
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">

View File

@@ -24,7 +24,7 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import Storage from './_Storage.svelte'; import Storage from './_Storage.svelte';
import { get } from '$lib/api'; import { get } from '$lib/api';
import Explainer from '$lib/components/Explainer.svelte'; import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
const { id } = $page.params; const { id } = $page.params;
@@ -87,7 +87,9 @@
</div> </div>
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4"> <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={$t('application.storage.persistent_storage_explainer')} />
</div>
<table class="mx-auto border-separate text-left"> <table class="mx-auto border-separate text-left">
<thead> <thead>
<tr class="h-12"> <tr class="h-12">
@@ -107,7 +109,4 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="flex justify-center py-4 text-center">
<Explainer customClass="w-full" text={$t('application.storage.persistent_storage_explainer')} />
</div>
</div> </div>

View File

@@ -209,13 +209,13 @@
<div class="grid grid-cols-2 items-center px-10 pb-8"> <div class="grid grid-cols-2 items-center px-10 pb-8">
<div> <div>
<label for="url" class="text-base font-bold text-stone-100" <label for="url" class="text-base font-bold text-stone-100"
>{$t('database.connection_string')}</label >{$t('database.connection_string')}
{#if !isPublic && database.destinationDocker.remoteEngine}
<Explainer
explanation="You can only access the database with this URL if your application is deployed to the same Destination."
/>
{/if}</label
> >
{#if !isPublic && database.destinationDocker.remoteEngine}
<Explainer
text="You can only access the database with this URL if your application is deployed to the same Destination."
/>
{/if}
</div> </div>
<CopyPasswordField <CopyPasswordField
textarea={true} textarea={true}
@@ -236,6 +236,7 @@
<div class="px-10 pb-10"> <div class="px-10 pb-10">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
id="isPublic"
loading={publicLoading} loading={publicLoading}
bind:setting={isPublic} bind:setting={isPublic}
on:click={() => changeSettings('isPublic')} on:click={() => changeSettings('isPublic')}
@@ -247,6 +248,7 @@
{#if database.type === 'redis'} {#if database.type === 'redis'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
id="appendOnly"
loading={publicLoading} loading={publicLoading}
bind:setting={appendOnly} bind:setting={appendOnly}
on:click={() => changeSettings('appendOnly')} on:click={() => changeSettings('appendOnly')}

View File

@@ -2,8 +2,8 @@
export let database: any; export let database: any;
import { status } from '$lib/store'; import { status } from '$lib/store';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import Explainer from '$lib/components/Explainer.svelte';
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
@@ -37,7 +37,8 @@
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100" <label for="dbUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.password')}</label >{$t('forms.password')}
<Explainer explanation="Could be changed while the database is running." /></label
> >
<CopyPasswordField <CopyPasswordField
disabled={!$status.database.isRunning} disabled={!$status.database.isRunning}
@@ -48,7 +49,6 @@
name="dbUserPassword" name="dbUserPassword"
bind:value={database.dbUserPassword} bind:value={database.dbUserPassword}
/> />
<Explainer text="Could be changed while the database is running." />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label> <label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label>
@@ -63,7 +63,7 @@
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUserPassword" class="text-base font-bold text-stone-100" <label for="rootUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.roots_password')}</label >{$t('forms.roots_password')} <Explainer explanation="Could be changed while the database is running." /></label
> >
<CopyPasswordField <CopyPasswordField
disabled={!$status.database.isRunning} disabled={!$status.database.isRunning}
@@ -74,6 +74,5 @@
name="rootUserPassword" name="rootUserPassword"
bind:value={database.rootUserPassword} bind:value={database.rootUserPassword}
/> />
<Explainer text="Could be changed while the database is running." />
</div> </div>
</div> </div>

View File

@@ -2,8 +2,8 @@
export let database: any; export let database: any;
import { status } from '$lib/store'; import { status } from '$lib/store';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import Explainer from '$lib/components/Explainer.svelte';
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
@@ -23,7 +23,8 @@
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUserPassword" class="text-base font-bold text-stone-100" <label for="rootUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.roots_password')}</label >{$t('forms.roots_password')}
<Explainer explanation="Could be changed while the database is running." /></label
> >
<CopyPasswordField <CopyPasswordField
disabled={!$status.database.isRunning} disabled={!$status.database.isRunning}
@@ -34,6 +35,5 @@
name="rootUserPassword" name="rootUserPassword"
bind:value={database.rootUserPassword} bind:value={database.rootUserPassword}
/> />
<Explainer text="Could be changed while the database is running." />
</div> </div>
</div> </div>

View File

@@ -2,8 +2,8 @@
export let database: any; export let database: any;
import { status } from '$lib/store'; import { status } from '$lib/store';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import Explainer from '$lib/components/Explainer.svelte';
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
@@ -37,7 +37,8 @@
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100" <label for="dbUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.password')}</label >{$t('forms.password')}
<Explainer explanation="Could be changed while the database is running." /></label
> >
<CopyPasswordField <CopyPasswordField
disabled={!$status.database.isRunning} disabled={!$status.database.isRunning}
@@ -48,7 +49,6 @@
name="dbUserPassword" name="dbUserPassword"
bind:value={database.dbUserPassword} bind:value={database.dbUserPassword}
/> />
<Explainer text="Could be changed while the database is running." />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label> <label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label>
@@ -63,7 +63,8 @@
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUserPassword" class="text-base font-bold text-stone-100" <label for="rootUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.roots_password')}</label >{$t('forms.roots_password')}
<Explainer explanation="Could be changed while the database is running." /></label
> >
<CopyPasswordField <CopyPasswordField
disabled={!$status.database.isRunning} disabled={!$status.database.isRunning}
@@ -74,6 +75,5 @@
name="rootUserPassword" name="rootUserPassword"
bind:value={database.rootUserPassword} bind:value={database.rootUserPassword}
/> />
<Explainer text="Could be changed while the database is running." />
</div> </div>
</div> </div>

View File

@@ -2,8 +2,8 @@
export let database: any; export let database: any;
import { status } from '$lib/store'; import { status } from '$lib/store';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import Explainer from '$lib/components/Explainer.svelte';
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
@@ -26,7 +26,9 @@
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100" <label for="rootUser" class="text-base font-bold text-stone-100"
>Root (postgres) User Password</label >Postgres User Password <Explainer
explanation="Could be changed while the database is running."
/></label
> >
<CopyPasswordField <CopyPasswordField
disabled={!$status.database.isRunning} disabled={!$status.database.isRunning}
@@ -37,7 +39,6 @@
name="rootUserPassword" name="rootUserPassword"
bind:value={database.rootUserPassword} bind:value={database.rootUserPassword}
/> />
<Explainer text="Could be changed while the database is running." />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label> <label for="dbUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label>
@@ -52,7 +53,8 @@
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100" <label for="dbUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.password')}</label >{$t('forms.password')}
<Explainer explanation="Could be changed while the database is running." /></label
> >
<CopyPasswordField <CopyPasswordField
disabled={!$status.database.isRunning} disabled={!$status.database.isRunning}
@@ -63,6 +65,5 @@
name="dbUserPassword" name="dbUserPassword"
bind:value={database.dbUserPassword} bind:value={database.dbUserPassword}
/> />
<Explainer text="Could be changed while the database is running." />
</div> </div>
</div> </div>

View File

@@ -2,8 +2,8 @@
export let database: any; export let database: any;
import { status } from '$lib/store'; import { status } from '$lib/store';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import Explainer from '$lib/components/Explainer.svelte';
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
@@ -12,7 +12,8 @@
<div class="space-y-2 px-10"> <div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100" <label for="dbUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.password')}</label >{$t('forms.password')}
<Explainer explanation="Could be changed while the database is running." /></label
> >
<CopyPasswordField <CopyPasswordField
disabled={!$status.database.isRunning} disabled={!$status.database.isRunning}
@@ -23,6 +24,5 @@
name="dbUserPassword" name="dbUserPassword"
bind:value={database.dbUserPassword} bind:value={database.dbUserPassword}
/> />
<Explainer text="Could be changed while the database is running." />
</div> </div>
</div> </div>

View File

@@ -60,48 +60,53 @@
import { errorNotification, handlerNotFoundLoad } from '$lib/common'; import { errorNotification, handlerNotFoundLoad } from '$lib/common';
import { appSession, status, disabledButton } from '$lib/store'; import { appSession, status, disabledButton } from '$lib/store';
import DeleteIcon from '$lib/components/DeleteIcon.svelte'; import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import Loading from '$lib/components/Loading.svelte';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
const { id } = $page.params; const { id } = $page.params;
let loading = false;
let statusInterval: any = false; let statusInterval: any = false;
let forceDelete = false;
$disabledButton = !$appSession.isAdmin; $disabledButton = !$appSession.isAdmin;
async function deleteDatabase() { async function deleteDatabase(force: boolean) {
const sure = confirm(`Are you sure you would like to delete '${database.name}'?`); const sure = confirm(`Are you sure you would like to delete '${database.name}'?`);
if (sure) { if (sure) {
loading = true; $status.database.initialLoading = true;
try { try {
await del(`/databases/${database.id}`, { id: database.id }); await del(`/databases/${database.id}`, { id: database.id, force });
return await goto('/databases'); return await goto('/databases');
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
loading = false; $status.database.initialLoading = false;
} }
} }
} }
async function stopDatabase() { async function stopDatabase() {
const sure = confirm($t('database.confirm_stop', { name: database.name })); const sure = confirm($t('database.confirm_stop', { name: database.name }));
if (sure) { if (sure) {
loading = true; $status.database.initialLoading = true;
try { try {
await post(`/databases/${database.id}/stop`, {}); await post(`/databases/${database.id}/stop`, {});
return window.location.reload();
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally {
$status.database.initialLoading = false;
} }
} }
} }
async function startDatabase() { async function startDatabase() {
loading = true; $status.database.initialLoading = true;
$status.database.loading = true;
try { try {
await post(`/databases/${database.id}/start`, {}); await post(`/databases/${database.id}/start`, {});
return window.location.reload();
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally {
$status.database.initialLoading = false;
$status.database.loading = false;
await getStatus();
} }
} }
async function getStatus() { async function getStatus() {
@@ -114,6 +119,9 @@
} }
onDestroy(() => { onDestroy(() => {
$status.database.initialLoading = true; $status.database.initialLoading = true;
$status.database.isRunning = false;
$status.database.isExited = false;
$status.database.loading = false;
clearInterval(statusInterval); clearInterval(statusInterval);
}); });
onMount(async () => { onMount(async () => {
@@ -137,120 +145,37 @@
{#if id !== 'new'} {#if id !== 'new'}
<nav class="nav-side"> <nav class="nav-side">
{#if loading} {#if database.type && database.destinationDockerId && database.version && database.defaultDatabase}
<Loading fullscreen cover /> {#if $status.database.isExited}
{:else} <a
{#if database.type && database.destinationDockerId && database.version && database.defaultDatabase} id="exited"
{#if $status.database.isExited} href={!$disabledButton ? `/databases/${id}/logs` : null}
<a class="icons bg-transparent text-sm flex items-center text-red-500 tooltip-error"
href={!$disabledButton ? `/databases/${id}/logs` : null} sveltekit:prefetch
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm flex items-center text-red-500 tooltip-error" >
data-tip="Service exited with an error!" <svg
sveltekit:prefetch 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"
> >
<svg <path stroke="none" d="M0 0h24v24H0z" fill="none" />
xmlns="http://www.w3.org/2000/svg" <path
class="w-6 h-6" 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"
viewBox="0 0 24 24" />
stroke-width="1.5" <line x1="12" y1="8" x2="12" y2="12" />
stroke="currentcolor" <line x1="12" y1="16" x2="12.01" y2="16" />
fill="none" </svg>
stroke-linecap="round" </a>
stroke-linejoin="round" <Tooltip triggeredBy="#exited">{'Service exited with an error!'}</Tooltip>
>
<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>
{/if}
{#if $status.database.initialLoading}
<button
class="icons tooltip-bottom flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
<line x1="5.63" y1="7.16" x2="5.63" y2="7.17" />
<line x1="4.06" y1="11" x2="4.06" y2="11.01" />
<line x1="4.63" y1="15.1" x2="4.63" y2="15.11" />
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
<line x1="11" y1="19.94" x2="11" y2="19.95" />
</svg>
</button>
{:else if $status.database.isRunning}
<button
on:click={stopDatabase}
type="submit"
disabled={!$appSession.isAdmin}
class="icons bg-transparent tooltip tooltip-bottom text-sm flex items-center space-x-2 text-red-500"
data-tip={$appSession.isAdmin
? $t('database.stop_database')
: $t('database.permission_denied_stop_database')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg>
</button>
{:else}
<button
on:click={startDatabase}
type="submit"
disabled={!$appSession.isAdmin}
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm flex items-center space-x-2 text-green-500"
data-tip={$appSession.isAdmin
? $t('database.start_database')
: $t('database.permission_denied_start_database')}
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
</button>
{/if}
{/if} {/if}
<div class="border border-stone-700 h-8" /> {#if $status.database.initialLoading}
<a
href="/databases/{id}"
sveltekit:prefetch
class="hover:text-yellow-500 rounded"
class:text-yellow-500={$page.url.pathname === `/databases/${id}`}
class:bg-coolgray-500={$page.url.pathname === `/databases/${id}`}
>
<button <button
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm disabled:text-red-500" class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out"
data-tip={$t('application.configurations')}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -263,34 +188,25 @@
stroke-linejoin="round" stroke-linejoin="round"
> >
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="4" y="8" width="4" height="4" /> <path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
<line x1="6" y1="4" x2="6" y2="8" /> <line x1="5.63" y1="7.16" x2="5.63" y2="7.17" />
<line x1="6" y1="12" x2="6" y2="20" /> <line x1="4.06" y1="11" x2="4.06" y2="11.01" />
<rect x="10" y="14" width="4" height="4" /> <line x1="4.63" y1="15.1" x2="4.63" y2="15.11" />
<line x1="12" y1="4" x2="12" y2="14" /> <line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
<line x1="12" y1="18" x2="12" y2="20" /> <line x1="11" y1="19.94" x2="11" y2="19.95" />
<rect x="16" y="5" width="4" height="4" /> </svg>
<line x1="18" y1="4" x2="18" y2="5" /> </button>
<line x1="18" y1="9" x2="18" y2="20" /> {:else if $status.database.isRunning}
</svg></button
></a
>
<div class="border border-stone-700 h-8" />
<a
href={$status.database.isRunning ? `/databases/${id}/logs` : null}
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/databases/${id}/logs`}
class:bg-coolgray-500={$page.url.pathname === `/databases/${id}/logs`}
>
<button <button
disabled={!$status.database.isRunning} id="stop"
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm" on:click={stopDatabase}
data-tip={$t('database.logs')} type="submit"
disabled={!$appSession.isAdmin}
class="icons bg-transparent text-sm flex items-center space-x-2 text-red-500"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="w-6 h-6"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@@ -299,25 +215,120 @@
stroke-linejoin="round" stroke-linejoin="round"
> >
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0" /> <rect x="6" y="5" width="4" height="14" rx="1" />
<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0" /> <rect x="14" y="5" width="4" height="14" rx="1" />
<line x1="3" y1="6" x2="3" y2="19" /> </svg>
<line x1="12" y1="6" x2="12" y2="19" /> </button>
<line x1="21" y1="6" x2="21" y2="19" /> <Tooltip triggeredBy="#stop">{'Stop'}</Tooltip>
</svg></button {:else}
></a <button
> id="start"
on:click={startDatabase}
type="submit"
disabled={!$appSession.isAdmin}
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"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
</button>
<Tooltip triggeredBy="#start">{'Start'}</Tooltip>
{/if}
{/if}
<div class="border border-stone-700 h-8" />
<a
id="configuration"
href="/databases/{id}"
sveltekit:prefetch
class="hover:text-yellow-500 rounded"
class:text-yellow-500={$page.url.pathname === `/databases/${id}`}
class:bg-coolgray-500={$page.url.pathname === `/databases/${id}`}
>
<button class="icons bg-transparent m text-sm disabled:text-red-500">
<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>
<div class="border border-stone-700 h-8" />
<a
id="databaselogs"
href={$status.database.isRunning ? `/databases/${id}/logs` : null}
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/databases/${id}/logs`}
class:bg-coolgray-500={$page.url.pathname === `/databases/${id}/logs`}
>
<button disabled={!$status.database.isRunning} class="icons bg-transparent text-sm">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<line x1="3" y1="6" x2="3" y2="19" />
<line x1="12" y1="6" x2="12" y2="19" />
<line x1="21" y1="6" x2="21" y2="19" />
</svg></button
></a
>
<Tooltip triggeredBy="#databaselogs">{'Logs'}</Tooltip>
{#if forceDelete}
<button <button
on:click={deleteDatabase} on:click={() => deleteDatabase(true)}
type="submit" type="submit"
disabled={!$appSession.isAdmin} disabled={!$appSession.isAdmin}
class:hover:text-red-500={$appSession.isAdmin} class:hover:text-red-500={$appSession.isAdmin}
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm" class="icons bg-transparent text-sm"
data-tip={$appSession.isAdmin >
? $t('database.delete_database') Force Delete</button
: $t('database.permission_denied_delete_database')}><DeleteIcon /></button >{:else}
<button
id="delete"
on:click={() => deleteDatabase(false)}
type="submit"
disabled={!$appSession.isAdmin}
class:hover:text-red-500={$appSession.isAdmin}
class="icons bg-transparent text-sm"><DeleteIcon /></button
> >
{/if} {/if}
<Tooltip triggeredBy="#delete">{'Delete'}</Tooltip>
</nav> </nav>
{/if} {/if}
<slot /> <slot />

View File

@@ -6,6 +6,7 @@
import { get } from '$lib/api'; import { get } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import Tooltip from '$lib/components/Tooltip.svelte';
const { id } = $page.params; const { id } = $page.params;
@@ -17,9 +18,13 @@
let logsEl: any; let logsEl: any;
let position = 0; let position = 0;
let loadingLogs = false; let loadingLogs = false;
let database: any = {}; let database = {
name: null
};
onMount(async () => { onMount(async () => {
const response = await get(`/databases/${id}`);
database = response.database;
const { logs: firstLogs } = await get(`/databases/${id}/logs`); const { logs: firstLogs } = await get(`/databases/${id}/logs`);
logs = firstLogs; logs = firstLogs;
loadAllLogs(); loadAllLogs();
@@ -94,29 +99,6 @@
</div> </div>
<span class="text-xs">{database.name}</span> <span class="text-xs">{database.name}</span>
</div> </div>
{#if database.fqdn}
<a
href={database.fqdn}
target="_blank"
class="icons tooltip-bottom flex items-center bg-transparent text-sm"
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
>
{/if}
</div> </div>
<div class="flex flex-row justify-center space-x-2 px-10 pt-6"> <div class="flex flex-row justify-center space-x-2 px-10 pt-6">
{#if logs.length === 0} {#if logs.length === 0}
@@ -129,9 +111,9 @@
{/if} {/if}
<div class="flex justify-end sticky top-0 p-1 mx-1"> <div class="flex justify-end sticky top-0 p-1 mx-1">
<button <button
id="follow"
on:click={followBuild} on:click={followBuild}
class="bg-transparent btn btn-sm tooltip tooltip-primary tooltip-bottom" class="bg-transparent btn btn-sm"
data-tip="Follow logs"
class:text-green-500={followingLogs} class:text-green-500={followingLogs}
> >
<svg <svg
@@ -151,6 +133,7 @@
<line x1="16" y1="12" x2="12" y2="16" /> <line x1="16" y1="12" x2="12" y2="16" />
</svg> </svg>
</button> </button>
<Tooltip triggeredBy="#follow">Follow Logs</Tooltip>
</div> </div>
<div <div
class="font-mono w-full leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200" class="font-mono w-full leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"

View File

@@ -142,8 +142,8 @@
</script> </script>
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4"> <form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex space-x-1 pb-5"> <div class="flex md:flex-row space-y-2 md:space-y-0 space-x-0 md:space-x-2 flex-col pb-5">
<div class="title font-bold">{$t('forms.configuration')}</div> <div class="title">{$t('forms.configuration')}</div>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<button <button
type="submit" type="submit"
@@ -162,7 +162,7 @@
> >
{/if} {/if}
</div> </div>
<div class="grid grid-cols-2 items-center px-10 "> <div class="grid lg:grid-cols-2 items-center px-10 ">
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label> <label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
<input <input
name="name" name="name"
@@ -173,7 +173,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid lg:grid-cols-2 items-center px-10">
<label for="engine" class="text-base font-bold text-stone-100">{$t('forms.engine')}</label> <label for="engine" class="text-base font-bold text-stone-100">{$t('forms.engine')}</label>
<CopyPasswordField <CopyPasswordField
id="engine" id="engine"
@@ -184,7 +184,7 @@
value={destination.engine} value={destination.engine}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid lg:grid-cols-2 items-center px-10">
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label> <label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
<CopyPasswordField <CopyPasswordField
id="network" id="network"
@@ -196,14 +196,15 @@
/> />
</div> </div>
{#if $appSession.teamId === '0'} {#if $appSession.teamId === '0'}
<div class="grid grid-cols-2 items-center"> <div class="grid lg:grid-cols-2 items-center px-10">
<Setting <Setting
id="changeProxySetting"
loading={loading.proxy} loading={loading.proxy}
disabled={cannotDisable} disabled={cannotDisable}
bind:setting={destination.isCoolifyProxyUsed} bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting} on:click={changeProxySetting}
title={$t('destination.use_coolify_proxy')} title={$t('destination.use_coolify_proxy')}
description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration. Databases will have their own proxy. <br><br>${ description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration.${
cannotDisable cannotDisable
? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>' ? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>'
: '' : ''

View File

@@ -33,11 +33,7 @@
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4"> <form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex items-center space-x-2 pb-5"> <div class="flex items-center space-x-2 pb-5">
<div class="title font-bold">{$t('forms.configuration')}</div> <div class="title font-bold">{$t('forms.configuration')}</div>
<button <button type="submit" class="btn btn-sm bg-destinations" class:loading disabled={loading}
type="submit"
class="btn btn-sm bg-destinations"
class:loading={loading}
disabled={loading}
>{loading >{loading
? payload.isCoolifyProxyUsed ? payload.isCoolifyProxyUsed
? $t('destination.new.saving_and_configuring_proxy') ? $t('destination.new.saving_and_configuring_proxy')
@@ -69,12 +65,13 @@
/> />
</div> </div>
{#if $appSession.teamId === '0'} {#if $appSession.teamId === '0'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center px-10">
<Setting <Setting
id="changeProxySetting"
bind:setting={payload.isCoolifyProxyUsed} bind:setting={payload.isCoolifyProxyUsed}
on:click={() => (payload.isCoolifyProxyUsed = !payload.isCoolifyProxyUsed)} on:click={() => (payload.isCoolifyProxyUsed = !payload.isCoolifyProxyUsed)}
title={$t('destination.use_coolify_proxy')} title={$t('destination.use_coolify_proxy')}
description={$t('destination.new.install_proxy')} description={'This will install a proxy on the destination to allow you to access your applications and services without any manual configuration.'}
/> />
</div> </div>
{/if} {/if}

View File

@@ -5,7 +5,7 @@
import { post } from '$lib/api'; import { post } from '$lib/api';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import Explainer from '$lib/components/Explainer.svelte'; import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
import Setting from '$lib/components/Setting.svelte'; import Setting from '$lib/components/Setting.svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
@@ -29,22 +29,18 @@
</script> </script>
<div class="text-center flex justify-center"> <div class="text-center flex justify-center">
<Explainer <SimpleExplainer
customClass="max-w-[32rem]" customClass="max-w-[32rem]"
text="Remote Docker Engines are using <span class='text-white font-bold'>SSH</span> to communicate with the remote docker engine. text="Remote Docker Engines are using <span class='text-white font-bold'>SSH</span> to communicate with the remote docker engine.
You need to setup an <span class='text-white font-bold'>SSH key</span> in advance on the server and install Docker. You need to setup an <span class='text-white font-bold'>SSH key</span> in advance on the server and install Docker.
<br>See <a class='text-white' href='https://docs.coollabs.io/coolify/destinations#remote-docker-engine'>docs</a> for more details." <br>See <a class='text-white' href='https://docs.coollabs.io/coolify/destinations#remote-docker-engine' target='blank'>docs</a> for more details."
/> />
</div> </div>
<div class="flex justify-center px-6 pb-8"> <div class="flex justify-center px-6 pb-8">
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4"> <form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex items-center space-x-2 pb-5"> <div class="flex items-center space-x-2 pb-5">
<div class="title font-bold">{$t('forms.configuration')}</div> <div class="title font-bold">{$t('forms.configuration')}</div>
<button <button type="submit" class="btn btn-sm bg-destinations" class:loading disabled={loading}
type="submit"
class="btn btn-sm bg-destinations"
class:loading={loading}
disabled={loading}
>{loading >{loading
? payload.isCoolifyProxyUsed ? payload.isCoolifyProxyUsed
? $t('destination.new.saving_and_configuring_proxy') ? $t('destination.new.saving_and_configuring_proxy')
@@ -97,12 +93,13 @@
bind:value={payload.network} bind:value={payload.network}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center px-10">
<Setting <Setting
id="isCoolifyProxyUsed"
bind:setting={payload.isCoolifyProxyUsed} bind:setting={payload.isCoolifyProxyUsed}
on:click={() => (payload.isCoolifyProxyUsed = !payload.isCoolifyProxyUsed)} on:click={() => (payload.isCoolifyProxyUsed = !payload.isCoolifyProxyUsed)}
title={$t('destination.use_coolify_proxy')} title={$t('destination.use_coolify_proxy')}
description={$t('destination.new.install_proxy')} description={'This will install a proxy on the destination to allow you to access your applications and services without any manual configuration.'}
/> />
</div> </div>
</form> </form>

View File

@@ -259,14 +259,15 @@
/></a /></a
> >
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center px-10">
<Setting <Setting
id="changeProxySetting"
disabled={cannotDisable} disabled={cannotDisable}
loading={loading.proxy} loading={loading.proxy}
bind:setting={destination.isCoolifyProxyUsed} bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting} on:click={changeProxySetting}
title={$t('destination.use_coolify_proxy')} title={$t('destination.use_coolify_proxy')}
description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration. Databases will have their own proxy. <br><br>${ description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration.${
cannotDisable cannotDisable
? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>' ? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>'
: '' : ''

View File

@@ -40,7 +40,7 @@
} }
}; };
} catch (error) { } catch (error) {
console.log(error) console.log(error);
return handlerNotFoundLoad(error, url); return handlerNotFoundLoad(error, url);
} }
}; };
@@ -56,12 +56,16 @@
import { errorNotification, handlerNotFoundLoad } from '$lib/common'; import { errorNotification, handlerNotFoundLoad } from '$lib/common';
import { appSession } from '$lib/store'; import { appSession } from '$lib/store';
import DeleteIcon from '$lib/components/DeleteIcon.svelte'; import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
const { id } = $page.params; const { id } = $page.params;
const isDestinationDeletable = destination?.application.length === 0 && destination?.database.length === 0 && destination?.service.length === 0 const isDestinationDeletable =
destination?.application.length === 0 &&
destination?.database.length === 0 &&
destination?.service.length === 0;
async function deleteDestination(destination: any) { async function deleteDestination(destination: any) {
if (!isDestinationDeletable) return if (!isDestinationDeletable) return;
const sure = confirm($t('application.confirm_to_delete', { name: destination.name })); const sure = confirm($t('application.confirm_to_delete', { name: destination.name }));
if (sure) { if (sure) {
try { try {
@@ -74,12 +78,12 @@
} }
function deletable() { function deletable() {
if (!isDestinationDeletable) { if (!isDestinationDeletable) {
return "Please delete all resources before deleting this." return 'Please delete all resources before deleting this.';
} }
if ($appSession.isAdmin) { if ($appSession.isAdmin) {
return $t('destination.delete_destination') return $t('destination.delete_destination');
} else { } else {
return $t('destination.permission_denied_delete_destination') return $t('destination.permission_denied_delete_destination');
} }
} }
</script> </script>
@@ -87,14 +91,15 @@
{#if id !== 'new'} {#if id !== 'new'}
<nav class="nav-side"> <nav class="nav-side">
<button <button
id="delete"
on:click={() => deleteDestination(destination)} on:click={() => deleteDestination(destination)}
type="submit" type="submit"
disabled={!$appSession.isAdmin && isDestinationDeletable} disabled={!$appSession.isAdmin && isDestinationDeletable}
class:hover:text-red-500={$appSession.isAdmin && isDestinationDeletable} class:hover:text-red-500={$appSession.isAdmin && isDestinationDeletable}
class="icons tooltip tooltip-primary tooltip-left bg-transparent text-sm" class="icons bg-transparent text-sm"
class:text-stone-600={!isDestinationDeletable} class:text-stone-600={!isDestinationDeletable}><DeleteIcon /></button
data-tip={deletable()}><DeleteIcon /></button
> >
</nav> </nav>
<Tooltip triggeredBy="#delete">{deletable()}</Tooltip>
{/if} {/if}
<slot /> <slot />

View File

@@ -108,24 +108,21 @@
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Identity and Access Management</div> <div class="mr-4 text-2xl tracking-tight">Identity and Access Management</div>
<button <button on:click={newTeam} class="btn btn-square btn-sm bg-iam">
on:click={newTeam} <svg
class="btn btn-square btn-sm bg-iam" class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
> >
<svg </button>
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
>
</button>
</div> </div>
{#if invitations.length > 0} {#if invitations.length > 0}
@@ -170,14 +167,11 @@
<tbody> <tbody>
{#each accounts as account} {#each accounts as account}
<tr> <tr class="grid items-center justify-center gap-2 lg:grid-flow-col">
<td class="px-2">{account.email}</td> <td class="px-2">{account.email}</td>
<td class="flex space-x-2"> <td class="flex space-x-2">
<form on:submit|preventDefault={() => resetPassword(account.id)}> <form on:submit|preventDefault={() => resetPassword(account.id)}>
<button <button class="my-4 btn btn-sm bg-iam">Reset Password</button>
class="my-4 btn btn-sm bg-iam"
>Reset Password</button
>
</form> </form>
<form on:submit|preventDefault={() => deleteUser(account.id)}> <form on:submit|preventDefault={() => deleteUser(account.id)}>
<button <button

View File

@@ -35,6 +35,7 @@
import DeleteIcon from '$lib/components/DeleteIcon.svelte'; import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import Tooltip from '$lib/components/Tooltip.svelte';
const { id } = $page.params; const { id } = $page.params;
async function deleteTeam() { async function deleteTeam() {
@@ -69,15 +70,14 @@
<nav class="nav-side"> <nav class="nav-side">
{#if team.id !== '0'} {#if team.id !== '0'}
<button <button
id="delete"
on:click={deleteTeam} on:click={deleteTeam}
type="submit" type="submit"
disabled={!$appSession.isAdmin} disabled={!$appSession.isAdmin}
class:hover:text-red-500={$appSession.isAdmin} class:hover:text-red-500={$appSession.isAdmin}
class="icons tooltip tooltip-primary tooltip-left bg-transparent text-sm" class="icons bg-transparent text-sm"><DeleteIcon /></button
data-tip={$appSession.isAdmin
? 'Delete'
: $t('destination.permission_denied_delete_destination')}><DeleteIcon /></button
> >
<Tooltip triggeredBy="#delete">Delete</Tooltip>
{/if} {/if}
</nav> </nav>
{/if} {/if}

View File

@@ -12,7 +12,7 @@
export let team: any; export let team: any;
export let invitations: any[]; export let invitations: any[];
import { page } from '$app/stores'; import { page } from '$app/stores';
import Explainer from '$lib/components/Explainer.svelte'; import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
import { post } from '$lib/api'; import { post } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
@@ -98,7 +98,7 @@
<div class="flex-col"> <div class="flex-col">
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label> <label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
{#if team.id === '0'} {#if team.id === '0'}
<Explainer customClass="w-full" text={$t('team.root_team_explainer')} /> <SimpleExplainer customClass="w-full" text={$t('team.root_team_explainer')} />
{/if} {/if}
</div> </div>
<input id="name" name="name" placeholder="name" bind:value={team.name} /> <input id="name" name="name" placeholder="name" bind:value={team.name} />
@@ -179,7 +179,7 @@
> >
</div> </div>
</div> </div>
<Explainer text={$t('team.invite_only_register_explainer')} /> <SimpleExplainer text={$t('team.invite_only_register_explainer')} />
<div class="flex-col space-y-2 px-4 pt-5 sm:px-6"> <div class="flex-col space-y-2 px-4 pt-5 sm:px-6">
<div class="flex space-x-0"> <div class="flex space-x-0">
<input <input

View File

@@ -1,4 +1,4 @@
<script> <script lang="ts">
export let name = ''; export let name = '';
export let value = ''; export let value = '';
export let isNewSecret = false; export let isNewSecret = false;

View File

@@ -71,4 +71,8 @@
<a href="https://searxng.org" target="_blank"> <a href="https://searxng.org" target="_blank">
<Icons.Searxng /> <Icons.Searxng />
</a> </a>
{:else if service.type === 'weblate'}
<a href="https://weblate.org" target="_blank">
<Icons.Weblate />
</a>
{/if} {/if}

View File

@@ -7,8 +7,9 @@
</script> </script>
<div class="flex space-x-1 py-5"> <div class="flex space-x-1 py-5">
<div class="title">Ghost</div> <div class="title">
<Explainer text={'You can change these values in the Ghost admin panel.'} /> Ghost <Explainer explanation="You can change these values in the Ghost admin panel." />
</div>
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="email">{$t('forms.default_email_address')}</label> <label for="email">{$t('forms.default_email_address')}</label>

View File

@@ -1,17 +1,51 @@
<script lang="ts"> <script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte'; import { addToast, status } from '$lib/store';
import Setting from '$lib/components/Setting.svelte'; import Setting from '$lib/components/Setting.svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { post } from '$lib/api';
import { page } from '$app/stores';
import { errorNotification } from '$lib/common';
export let service: any; export let service: any;
function toggleEmailSmtpUseTls() {
service.glitchTip.emailSmtpUseTls = !service.glitchTip.emailSmtpUseTls; const { id } = $page.params;
} let loading = false;
function toggleEmailSmtpUseSsl() {
service.glitchTip.emailSmtpUseSsl = !service.glitchTip.emailSmtpUseSsl; async function changeSettings(name: any) {
} if (loading || $status.service.isRunning) return;
function toggleEnableOpenUserRegistration() {
service.glitchTip.enableOpenUserRegistration = !service.glitchTip.enableOpenUserRegistration; let enableOpenUserRegistration = service.glitchTip.enableOpenUserRegistration;
let emailSmtpUseSsl = service.glitchTip.emailSmtpUseSsl;
let emailSmtpUseTls = service.glitchTip.emailSmtpUseTls;
loading = true;
if (name === 'enableOpenUserRegistration') {
enableOpenUserRegistration = !enableOpenUserRegistration;
}
if (name === 'emailSmtpUseSsl') {
emailSmtpUseSsl = !emailSmtpUseSsl;
}
if (name === 'emailSmtpUseTls') {
emailSmtpUseTls = !emailSmtpUseTls;
}
try {
await post(`/services/${id}/glitchtip/settings`, {
enableOpenUserRegistration,
emailSmtpUseSsl,
emailSmtpUseTls
});
service.glitchTip.emailSmtpUseTls = emailSmtpUseTls;
service.glitchTip.emailSmtpUseSsl = emailSmtpUseSsl;
service.glitchTip.enableOpenUserRegistration = enableOpenUserRegistration;
return addToast({
message: 'Settings updated.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading = false;
}
} }
</script> </script>
@@ -19,109 +53,121 @@
<div class="title">GlitchTip</div> <div class="title">GlitchTip</div>
</div> </div>
<div class="flex space-x-1 py-2 font-bold">
<div class="subtitle">Settings</div>
</div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<Setting <Setting
id="enableOpenUserRegistration"
bind:setting={service.glitchTip.enableOpenUserRegistration}
{loading}
disabled={$status.service.isRunning}
on:click={() => changeSettings('enableOpenUserRegistration')}
title="Enable Open User Registration"
description={''}
/>
<!-- <Setting
bind:setting={service.glitchTip.enableOpenUserRegistration} bind:setting={service.glitchTip.enableOpenUserRegistration}
on:click={toggleEnableOpenUserRegistration} on:click={toggleEnableOpenUserRegistration}
title={'Enable Open User Registration'} title={'Enable Open User Registration'}
description={''} description={''}
/> /> -->
</div> </div>
<div class="flex space-x-1 py-2 font-bold"> <div class="flex space-x-1 py-2 font-bold">
<div class="subtitle">Email settings</div> <div class="subtitle">Email settings</div>
</div> </div>
<div class="grid grid-cols-2 items-center px-10">
<Setting
id="emailSmtpUseTls"
bind:setting={service.glitchTip.emailSmtpUseTls}
{loading}
disabled={$status.service.isRunning}
on:click={() => changeSettings('emailSmtpUseTls')}
title="Use TLS for SMTP"
description={''}
/>
</div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="defaultEmailFrom" class="text-base font-bold text-stone-100">Default Email From</label <Setting
> id="emailSmtpUseSsl"
bind:setting={service.glitchTip.emailSmtpUseSsl}
{loading}
disabled={$status.service.isRunning}
on:click={() => changeSettings('emailSmtpUseSsl')}
title="Use SSL for SMTP"
description={''}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="defaultEmailFrom">Default Email From</label>
<CopyPasswordField <CopyPasswordField
required required
name="defaultEmailFrom" name="defaultEmailFrom"
id="defaultEmailFrom" id="defaultEmailFrom"
value={service.glitchTip.defaultEmailFrom} bind:value={service.glitchTip.defaultEmailFrom}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="emailSmtpHost" class="text-base font-bold text-stone-100">SMTP Host</label> <label for="emailSmtpHost">SMTP Host</label>
<CopyPasswordField <CopyPasswordField
name="emailSmtpHost" name="emailSmtpHost"
id="emailSmtpHost" id="emailSmtpHost"
value={service.glitchTip.emailSmtpHost} bind:value={service.glitchTip.emailSmtpHost}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="emailSmtpPort" class="text-base font-bold text-stone-100">SMTP Port</label> <label for="emailSmtpPort">SMTP Port</label>
<CopyPasswordField <CopyPasswordField
name="emailSmtpPort" name="emailSmtpPort"
id="emailSmtpPort" id="emailSmtpPort"
value={service.glitchTip.emailSmtpPort} bind:value={service.glitchTip.emailSmtpPort}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="emailSmtpUser" class="text-base font-bold text-stone-100">SMTP User</label> <label for="emailSmtpUser">SMTP User</label>
<CopyPasswordField <CopyPasswordField
name="emailSmtpUser" name="emailSmtpUser"
id="emailSmtpUser" id="emailSmtpUser"
value={service.glitchTip.emailSmtpUser} bind:value={service.glitchTip.emailSmtpUser}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="emailSmtpPassword" class="text-base font-bold text-stone-100">SMTP Password</label> <label for="emailSmtpPassword">SMTP Password</label>
<CopyPasswordField <CopyPasswordField
name="emailSmtpPassword" name="emailSmtpPassword"
id="emailSmtpPassword" id="emailSmtpPassword"
value={service.glitchTip.emailSmtpPassword} bind:value={service.glitchTip.emailSmtpPassword}
isPasswordField isPasswordField
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<Setting <label for="emailBackend">Email Backend</label>
bind:setting={service.glitchTip.emailSmtpUseTls} <CopyPasswordField
on:click={toggleEmailSmtpUseTls} name="emailBackend"
title={'SMTP Use TLS'} id="emailBackend"
description={''} bind:value={service.glitchTip.emailBackend}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<Setting <label for="mailgunApiKey">Mailgun API Key</label>
bind:setting={service.glitchTip.emailSmtpUseSsl}
on:click={toggleEmailSmtpUseSsl}
title={'SMTP Use SSL'}
description={''}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailBackend" class="text-base font-bold text-stone-100">Email Backend</label>
<CopyPasswordField name="emailBackend" id="emailBackend" value={service.glitchTip.emailBackend} />
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mailgunApiKey" class="text-base font-bold text-stone-100">Mailgun API Key</label>
<CopyPasswordField <CopyPasswordField
name="mailgunApiKey" name="mailgunApiKey"
id="mailgunApiKey" id="mailgunApiKey"
value={service.glitchTip.mailgunApiKey} bind:value={service.glitchTip.mailgunApiKey}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="sendgridApiKey" class="text-base font-bold text-stone-100">SendGrid API Key</label> <label for="sendgridApiKey">SendGrid API Key</label>
<CopyPasswordField <CopyPasswordField
name="sendgridApiKey" name="sendgridApiKey"
id="sendgridApiKey" id="sendgridApiKey"
value={service.glitchTip.sendgridApiKey} bind:value={service.glitchTip.sendgridApiKey}
/> />
</div> </div>
@@ -130,35 +176,31 @@
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="defaultEmail" class="text-base font-bold text-stone-100">{$t('forms.email')}</label> <label for="defaultEmail">{$t('forms.email')}</label>
<CopyPasswordField <CopyPasswordField
name="defaultEmail" name="defaultEmail"
id="defaultEmail" id="defaultEmail"
value={service.glitchTip.defaultEmail} bind:value={service.glitchTip.defaultEmail}
readonly readonly
disabled disabled
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="defaultUsername" class="text-base font-bold text-stone-100" <label for="defaultUsername">{$t('forms.username')}</label>
>{$t('forms.username')}</label
>
<CopyPasswordField <CopyPasswordField
name="defaultUsername" name="defaultUsername"
id="defaultUsername" id="defaultUsername"
value={service.glitchTip.defaultUsername} bind:value={service.glitchTip.defaultUsername}
readonly readonly
disabled disabled
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="defaultPassword" class="text-base font-bold text-stone-100" <label for="defaultPassword">{$t('forms.password')}</label>
>{$t('forms.password')}</label
>
<CopyPasswordField <CopyPasswordField
name="defaultPassword" name="defaultPassword"
id="defaultPassword" id="defaultPassword"
value={service.glitchTip.defaultPassword} bind:value={service.glitchTip.defaultPassword}
readonly readonly
disabled disabled
isPasswordField isPasswordField
@@ -170,38 +212,32 @@
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlUser" class="text-base font-bold text-stone-100" <label for="postgresqlUser">{$t('forms.username')}</label>
>{$t('forms.username')}</label
>
<CopyPasswordField <CopyPasswordField
name="postgresqlUser" name="postgresqlUser"
id="postgresqlUser" id="postgresqlUser"
value={service.glitchTip.postgresqlUser} bind:value={service.glitchTip.postgresqlUser}
readonly readonly
disabled disabled
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlPassword" class="text-base font-bold text-stone-100" <label for="postgresqlPassword">{$t('forms.password')}</label>
>{$t('forms.password')}</label
>
<CopyPasswordField <CopyPasswordField
id="postgresqlPassword" id="postgresqlPassword"
isPasswordField isPasswordField
readonly readonly
disabled disabled
name="postgresqlPassword" name="postgresqlPassword"
value={service.glitchTip.postgresqlPassword} bind:value={service.glitchTip.postgresqlPassword}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlDatabase" class="text-base font-bold text-stone-100" <label for="postgresqlDatabase">{$t('index.database')}</label>
>{$t('index.database')}</label
>
<CopyPasswordField <CopyPasswordField
name="postgresqlDatabase" name="postgresqlDatabase"
id="postgresqlDatabase" id="postgresqlDatabase"
value={service.glitchTip.postgresqlDatabase} bind:value={service.glitchTip.postgresqlDatabase}
readonly readonly
disabled disabled
/> />

View File

@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
export let service: any; export let service: any;
</script> </script>

View File

@@ -11,7 +11,11 @@
<div class="title">Plausible Analytics</div> <div class="title">Plausible Analytics</div>
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="scriptName">Script Name</label> <label for="scriptName"
>Script Name <Explainer
explanation="Useful if you would like to rename the collector script to prevent it blocked by AdBlockers."
/></label
>
<input <input
name="scriptName" name="scriptName"
id="scriptName" id="scriptName"
@@ -21,9 +25,6 @@
bind:value={service.plausibleAnalytics.scriptName} bind:value={service.plausibleAnalytics.scriptName}
required required
/> />
<Explainer
text="Useful if you would like to rename the collector script to prevent it blocked by AdBlockers."
/>
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="email">{$t('forms.email')}</label> <label for="email">{$t('forms.email')}</label>

View File

@@ -14,7 +14,6 @@
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { appSession, disabledButton, status, location, setLocation, addToast } from '$lib/store'; import { appSession, disabledButton, status, location, setLocation, addToast } from '$lib/store';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import Setting from '$lib/components/Setting.svelte'; import Setting from '$lib/components/Setting.svelte';
import Fider from './_Fider.svelte'; import Fider from './_Fider.svelte';
@@ -30,6 +29,9 @@
import Appwrite from './_Appwrite.svelte'; import Appwrite from './_Appwrite.svelte';
import Moodle from './_Moodle.svelte'; import Moodle from './_Moodle.svelte';
import Searxng from './_Searxng.svelte'; import Searxng from './_Searxng.svelte';
import Weblate from './_Weblate.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import Taiga from './_Taiga.svelte';
const { id } = $page.params; const { id } = $page.params;
$: isDisabled = $: isDisabled =
@@ -282,8 +284,9 @@
</div> </div>
<div class="grid grid-cols-2 px-10"> <div class="grid grid-cols-2 px-10">
<div class="flex-col "> <div class="flex-col ">
<label for="apiFqdn" class="pt-2 text-base font-bold text-stone-100">API URL</label> <label for="apiFqdn" class="pt-2 text-base font-bold text-stone-100"
<Explainer text={$t('application.https_explainer')} /> >API URL <Explainer explanation={$t('application.https_explainer')} /></label
>
</div> </div>
<CopyPasswordField <CopyPasswordField
@@ -301,9 +304,9 @@
<div class="grid grid-cols-2 px-10"> <div class="grid grid-cols-2 px-10">
<div class="flex-col "> <div class="flex-col ">
<label for="fqdn" class="pt-2 text-base font-bold text-stone-100" <label for="fqdn" class="pt-2 text-base font-bold text-stone-100"
>{$t('application.url_fqdn')}</label >{$t('application.url_fqdn')}
> <Explainer explanation={$t('application.https_explainer')} />
<Explainer text={$t('application.https_explainer')} /> </label>
</div> </div>
<CopyPasswordField <CopyPasswordField
@@ -354,6 +357,7 @@
{/if} {/if}
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<Setting <Setting
id="dualCerts"
disabled={$status.service.isRunning} disabled={$status.service.isRunning}
dataTooltip={$t('forms.must_be_stopped_to_modify')} dataTooltip={$t('forms.must_be_stopped_to_modify')}
bind:setting={dualCerts} bind:setting={dualCerts}
@@ -363,7 +367,11 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="exposePort" class="text-base font-bold text-stone-100">Exposed Port</label> <label for="exposePort" class="text-base font-bold text-stone-100"
>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 <input
readonly={!$appSession.isAdmin && !$status.service.isRunning} readonly={!$appSession.isAdmin && !$status.service.isRunning}
disabled={!$appSession.isAdmin || disabled={!$appSession.isAdmin ||
@@ -374,9 +382,6 @@
bind:value={service.exposePort} bind:value={service.exposePort}
placeholder="12345" placeholder="12345"
/> />
<Explainer
text={'You can expose your application to a port on the host system.<br><br>Useful if you would like to use your own reverse proxy or tunnel and also in development mode. Otherwise leave empty.'}
/>
</div> </div>
{#if service.type === 'plausibleanalytics'} {#if service.type === 'plausibleanalytics'}
@@ -405,6 +410,10 @@
<GlitchTip bind:service /> <GlitchTip bind:service />
{:else if service.type === 'searxng'} {:else if service.type === 'searxng'}
<Searxng bind:service /> <Searxng bind:service />
{:else if service.type === 'weblate'}
<Weblate bind:service />
{:else if service.type === 'taiga'}
<Taiga bind:service />
{/if} {/if}
</div> </div>
</form> </form>

View File

@@ -0,0 +1,118 @@
<script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { t } from '$lib/translations';
export let service: any;
</script>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Taiga</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="secretKey">Secret Key</label>
<CopyPasswordField
name="secretKey"
id="secretKey"
isPasswordField
value={service.taiga.secretKey}
readonly
disabled
/>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Django</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="djangoAdminUser">Admin User</label>
<CopyPasswordField
name="djangoAdminUser"
id="djangoAdminUser"
value={service.taiga.djangoAdminUser}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="djangoAdminPassword">Admin Password</label>
<CopyPasswordField
name="djangoAdminPassword"
id="djangoAdminPassword"
isPasswordField
value={service.taiga.djangoAdminPassword}
readonly
disabled
/>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">RabbitMQ</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="rabbitMQUser">User</label>
<CopyPasswordField
name="rabbitMQUser"
id="rabbitMQUser"
value={service.taiga.rabbitMQUser}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="rabbitMQPassword">Password</label>
<CopyPasswordField
name="rabbitMQPassword"
id="rabbitMQPassword"
isPasswordField
value={service.taiga.rabbitMQPassword}
readonly
disabled
/>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">PostgreSQL</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlHost">PostgreSQL Host</label>
<CopyPasswordField
name="postgresqlHost"
id="postgresqlHost"
value={service.taiga.postgresqlHost}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlPort">PostgreSQL Port</label>
<CopyPasswordField
name="postgresqlPort"
id="postgresqlPort"
value={service.taiga.postgresqlPort}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlUser">PostgreSQL User</label>
<CopyPasswordField
name="postgresqlUser"
id="postgresqlUser"
value={service.taiga.postgresqlUser}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlPassword">PostgreSQL Password</label>
<CopyPasswordField
name="postgresqlPassword"
id="postgresqlPassword"
isPasswordField
value={service.taiga.postgresqlPassword}
readonly
disabled
/>
</div>

View File

@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte'; import Explainer from '$lib/components/Explainer.svelte';
export let service: any; export let service: any;
</script> </script>
@@ -13,7 +12,11 @@
<input name="adminUser" id="adminUser" placeholder="admin" value="admin" disabled readonly /> <input name="adminUser" id="adminUser" placeholder="admin" value="admin" disabled readonly />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="umamiAdminPassword">Initial Admin Password</label> <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."
/></label
>
<CopyPasswordField <CopyPasswordField
isPasswordField isPasswordField
name="umamiAdminPassword" name="umamiAdminPassword"
@@ -23,7 +26,4 @@
disabled disabled
readonly readonly
/> />
<Explainer
text="It could be changed in Umami. <br>This is just the password set initially after the first start."
/>
</div> </div>

View File

@@ -0,0 +1,66 @@
<script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
export let service: any;
</script>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Weblate</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="adminPassword">Admin password</label>
<CopyPasswordField
name="adminPassword"
id="adminPassword"
isPasswordField
value={service.weblate.adminPassword}
readonly
disabled
/>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">PostgreSQL</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlHost">PostgreSQL Host</label>
<CopyPasswordField
name="postgresqlHost"
id="postgresqlHost"
value={service.weblate.postgresqlHost}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlPort">PostgreSQL Port</label>
<CopyPasswordField
name="postgresqlPort"
id="postgresqlPort"
value={service.weblate.postgresqlPort}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlUser">PostgreSQL User</label>
<CopyPasswordField
name="postgresqlUser"
id="postgresqlUser"
value={service.weblate.postgresqlUser}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlPassword">PostgreSQL Password</label>
<CopyPasswordField
name="postgresqlPassword"
id="postgresqlPassword"
isPasswordField
value={service.weblate.postgresqlPassword}
readonly
disabled
/>
</div>

View File

@@ -93,6 +93,7 @@ define('SUBDOMAIN_INSTALL', false);`
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<Setting <Setting
id="ftpEnabled"
bind:setting={service.wordpress.ftpEnabled} bind:setting={service.wordpress.ftpEnabled}
loading={ftpLoading} loading={ftpLoading}
disabled={!$status.service.isRunning} disabled={!$status.service.isRunning}
@@ -127,6 +128,7 @@ define('SUBDOMAIN_INSTALL', false);`
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<Setting <Setting
id="ownMysql"
dataTooltip={$t('forms.must_be_stopped_to_modify')} dataTooltip={$t('forms.must_be_stopped_to_modify')}
bind:setting={service.wordpress.ownMysql} bind:setting={service.wordpress.ownMysql}
disabled={$status.service.isRunning} disabled={$status.service.isRunning}

View File

@@ -56,13 +56,13 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/stores';
import DeleteIcon from '$lib/components/DeleteIcon.svelte'; import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import Loading from '$lib/components/Loading.svelte';
import { del, get, post } from '$lib/api'; import { del, get, post } from '$lib/api';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification, handlerNotFoundLoad } from '$lib/common'; import { errorNotification, handlerNotFoundLoad } from '$lib/common';
import { appSession, disabledButton, status, location, setLocation } from '$lib/store'; import { appSession, disabledButton, status, location, setLocation } from '$lib/store';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
const { id } = $page.params; const { id } = $page.params;
export let service: any; export let service: any;
@@ -74,13 +74,12 @@
!service.version || !service.version ||
!service.type; !service.type;
let loading = false;
let statusInterval: any; let statusInterval: any;
async function deleteService() { async function deleteService() {
const sure = confirm($t('application.confirm_to_delete', { name: service.name })); const sure = confirm($t('application.confirm_to_delete', { name: service.name }));
if (sure) { if (sure) {
loading = true; $status.service.initialLoading = true;
try { try {
if (service.type && $status.service.isRunning) if (service.type && $status.service.isRunning)
await post(`/services/${service.id}/${service.type}/stop`, {}); await post(`/services/${service.id}/${service.type}/stop`, {});
@@ -89,31 +88,34 @@
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
loading = false; $status.service.initialLoading = false;
} }
} }
} }
async function stopService() { async function stopService() {
const sure = confirm($t('database.confirm_stop', { name: service.name })); const sure = confirm($t('database.confirm_stop', { name: service.name }));
if (sure) { if (sure) {
loading = true; $status.service.initialLoading = true;
try { try {
await post(`/services/${service.id}/${service.type}/stop`, {}); await post(`/services/${service.id}/${service.type}/stop`, {});
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
loading = false; $status.service.initialLoading = false;
} }
} }
} }
async function startService() { async function startService() {
loading = true; $status.service.initialLoading = true;
$status.service.loading = true;
try { try {
await post(`/services/${service.id}/${service.type}/start`, {}); await post(`/services/${service.id}/${service.type}/start`, {});
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
loading = false; $status.service.initialLoading = false;
$status.service.loading = false;
await getStatus();
} }
} }
async function getStatus() { async function getStatus() {
@@ -127,6 +129,9 @@
} }
onDestroy(() => { onDestroy(() => {
$status.service.initialLoading = true; $status.service.initialLoading = true;
$status.service.isRunning = false;
$status.service.isExited = false;
$status.service.loading = false;
$location = null; $location = null;
clearInterval(statusInterval); clearInterval(statusInterval);
}); });
@@ -146,269 +151,260 @@
</script> </script>
<nav class="nav-side"> <nav class="nav-side">
{#if loading} {#if service.type && service.destinationDockerId && service.version && service.fqdn}
<Loading fullscreen cover /> {#if $location}
{:else} <a
{#if service.type && service.destinationDockerId && service.version} id="open"
{#if $location} href={$location}
<a target="_blank"
href={$location} class="icons flex items-center bg-transparent text-sm"
target="_blank" ><svg
class="icons tooltip-bottom flex items-center bg-transparent text-sm" xmlns="http://www.w3.org/2000/svg"
><svg class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
class="h-6 w-6" stroke-width="1.5"
viewBox="0 0 24 24" stroke="currentColor"
stroke-width="1.5" fill="none"
stroke="currentColor" stroke-linecap="round"
fill="none" stroke-linejoin="round"
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
> >
<div class="border border-stone-700 h-8" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
{/if} <path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
{#if $status.service.isExited} <line x1="10" y1="14" x2="20" y2="4" />
<a <polyline points="15 4 20 4 20 9" />
href={!$disabledButton ? `/services/${id}/logs` : null} </svg></a
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm flex items-center text-red-500 tooltip-error" >
data-tip="Service exited with an error!" <Tooltip triggeredBy="#open">Open</Tooltip>
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>
{/if}
{#if $status.service.initialLoading}
<button
class="icons tooltip-bottom flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
<line x1="5.63" y1="7.16" x2="5.63" y2="7.17" />
<line x1="4.06" y1="11" x2="4.06" y2="11.01" />
<line x1="4.63" y1="15.1" x2="4.63" y2="15.11" />
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
<line x1="11" y1="19.94" x2="11" y2="19.95" />
</svg>
</button>
{:else if $status.service.isRunning}
<button
on:click={stopService}
type="submit"
disabled={$disabledButton}
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm flex items-center space-x-2 text-red-500"
data-tip={$appSession.isAdmin
? $t('service.stop_service')
: $t('service.permission_denied_stop_service')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg>
</button>
{:else}
<button
on:click={startService}
type="submit"
disabled={$disabledButton}
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm flex items-center space-x-2 text-green-500"
data-tip={$appSession.isAdmin
? $t('service.start_service')
: $t('service.permission_denied_start_service')}
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
</button>
{/if}
<div class="border border-stone-700 h-8" /> <div class="border border-stone-700 h-8" />
{/if} {/if}
{#if service.type && service.destinationDockerId && service.version} {#if $status.service.isExited}
<a <a
href="/services/{id}" id="error"
href={!$disabledButton ? `/services/${id}/logs` : null}
class="icons bg-transparent text-sm flex items-center text-red-500 tooltip-error"
sveltekit:prefetch 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 <svg
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm disabled:text-red-500" xmlns="http://www.w3.org/2000/svg"
data-tip={$t('application.configurations')} class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentcolor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
> >
<svg <path stroke="none" d="M0 0h24v24H0z" fill="none" />
xmlns="http://www.w3.org/2000/svg" <path
class="h-6 w-6" 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"
viewBox="0 0 24 24" />
stroke-width="1.5" <line x1="12" y1="8" x2="12" y2="12" />
stroke="currentColor" <line x1="12" y1="16" x2="12.01" y2="16" />
fill="none" </svg>
stroke-linecap="round" </a>
stroke-linejoin="round" <Tooltip triggeredBy="#error">Service exited with an error!</Tooltip>
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="4" y="8" width="4" height="4" />
<line x1="6" y1="4" x2="6" y2="8" />
<line x1="6" y1="12" x2="6" y2="20" />
<rect x="10" y="14" width="4" height="4" />
<line x1="12" y1="4" x2="12" y2="14" />
<line x1="12" y1="18" x2="12" y2="20" />
<rect x="16" y="5" width="4" height="4" />
<line x1="18" y1="4" x2="18" y2="5" />
<line x1="18" y1="9" x2="18" y2="20" />
</svg></button
></a
>
<a
href="/services/{id}/secrets"
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/services/${id}/secrets`}
class:bg-coolgray-500={$page.url.pathname === `/services/${id}/secrets`}
>
<button
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm disabled:text-red-500"
data-tip={$t('application.secret')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3"
/>
<circle cx="12" cy="11" r="1" />
<line x1="12" y1="12" x2="12" y2="14.5" />
</svg></button
></a
>
<a
href="/services/{id}/storages"
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/services/${id}/storages`}
class:bg-coolgray-500={$page.url.pathname === `/services/${id}/storages`}
>
<button
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm disabled:text-red-500"
data-tip="Persistent Storage"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<ellipse cx="12" cy="6" rx="8" ry="3" />
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
</svg>
</button></a
>
<div class="border border-stone-700 h-8" />
<a
href={!$disabledButton && $status.service.isRunning ? `/services/${id}/logs` : null}
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/services/${id}/logs`}
class:bg-coolgray-500={$page.url.pathname === `/services/${id}/logs`}
>
<button
disabled={!$status.service.isRunning}
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm"
data-tip={$t('service.logs')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<line x1="3" y1="6" x2="3" y2="19" />
<line x1="12" y1="6" x2="12" y2="19" />
<line x1="21" y1="6" x2="21" y2="19" />
</svg></button
></a
>
{/if} {/if}
<button {#if $status.service.initialLoading}
on:click={deleteService} <button
type="submit" class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out"
disabled={!$appSession.isAdmin} >
class:hover:text-red-500={$appSession.isAdmin} <svg
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm" xmlns="http://www.w3.org/2000/svg"
data-tip={$appSession.isAdmin class="h-6 w-6"
? $t('service.delete_service') viewBox="0 0 24 24"
: $t('service.permission_denied_delete_service')}><DeleteIcon /></button stroke-width="1.5"
> stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
<line x1="5.63" y1="7.16" x2="5.63" y2="7.17" />
<line x1="4.06" y1="11" x2="4.06" y2="11.01" />
<line x1="4.63" y1="15.1" x2="4.63" y2="15.11" />
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
<line x1="11" y1="19.94" x2="11" y2="19.95" />
</svg>
</button>
{:else if $status.service.isRunning}
<button
id="stop"
on:click={stopService}
type="submit"
disabled={$disabledButton}
class="icons bg-transparent text-sm flex items-center space-x-2 text-red-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"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg>
</button>
<Tooltip triggeredBy="#stop">Stop</Tooltip>
{:else}
<button
id="start"
on:click={startService}
type="submit"
disabled={$disabledButton}
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"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
</button>
<Tooltip triggeredBy="#start">Start</Tooltip>
{/if}
<div class="border border-stone-700 h-8" />
{/if} {/if}
{#if service.type && service.destinationDockerId && service.version}
<a
id="configuration"
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 class="icons bg-transparent text-sm disabled:text-red-500">
<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
id="secrets"
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 class="icons bg-transparent text-sm disabled:text-red-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"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3"
/>
<circle cx="12" cy="11" r="1" />
<line x1="12" y1="12" x2="12" y2="14.5" />
</svg></button
></a
>
<Tooltip triggeredBy="#secrets">Secrets</Tooltip>
<a
id="persistentstorage"
href="/services/{id}/storages"
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/services/${id}/storages`}
class:bg-coolgray-500={$page.url.pathname === `/services/${id}/storages`}
>
<button class="icons bg-transparent text-sm disabled:text-red-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"
>
<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 Storage</Tooltip>
<div class="border border-stone-700 h-8" />
<a
id="logs"
href={!$disabledButton && $status.service.isRunning ? `/services/${id}/logs` : null}
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/services/${id}/logs`}
class:bg-coolgray-500={$page.url.pathname === `/services/${id}/logs`}
>
<button disabled={!$status.service.isRunning} class="icons bg-transparent text-sm">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<line x1="3" y1="6" x2="3" y2="19" />
<line x1="12" y1="6" x2="12" y2="19" />
<line x1="21" y1="6" x2="21" y2="19" />
</svg></button
></a
>
<Tooltip triggeredBy="#logs">Logs</Tooltip>
{/if}
<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">Delete</Tooltip>
</nav> </nav>
<slot /> <slot />

View File

@@ -5,6 +5,7 @@
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
let service: any = {}; let service: any = {};
let logsLoading = false; let logsLoading = false;
@@ -126,9 +127,9 @@
{/if} {/if}
<div class="flex justify-end sticky top-0 p-1 mx-1"> <div class="flex justify-end sticky top-0 p-1 mx-1">
<button <button
id="follow"
on:click={followBuild} on:click={followBuild}
class="bg-transparent btn btn-sm tooltip tooltip-primary tooltip-bottom" class="bg-transparent btn btn-sm"
data-tip="Follow logs"
class:text-green-500={followingLogs} class:text-green-500={followingLogs}
> >
<svg <svg
@@ -148,6 +149,7 @@
<line x1="16" y1="12" x2="12" y2="16" /> <line x1="16" y1="12" x2="12" y2="16" />
</svg> </svg>
</button> </button>
<Tooltip triggeredBy="#follow">Follow Logs</Tooltip>
</div> </div>
<div <div
class="font-mono w-full leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200" class="font-mono w-full leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"

View File

@@ -25,14 +25,47 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import { get } from '$lib/api'; import { get } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import pLimit from 'p-limit';
import ServiceLinks from './_ServiceLinks.svelte'; import ServiceLinks from './_ServiceLinks.svelte';
import { addToast } from '$lib/store';
import { saveSecret } from './utils';
const limit = pLimit(1);
const { id } = $page.params; const { id } = $page.params;
let batchSecrets = '';
async function refreshSecrets() { async function refreshSecrets() {
const data = await get(`/services/${id}/secrets`); const data = await get(`/services/${id}/secrets`);
secrets = [...data.secrets]; secrets = [...data.secrets];
} }
async function getValues(e: any) {
e.preventDefault();
const eachValuePair = batchSecrets.split('\n');
const batchSecretsPairs = eachValuePair
.filter((secret) => !secret.startsWith('#') && secret)
.map((secret) => {
const [name, ...rest] = secret.split('=');
const value = rest.join('=');
const cleanValue = value?.replaceAll('"', '') || '';
return {
name,
value: cleanValue,
isNew: !secrets.find((secret: any) => name === secret.name)
};
});
await Promise.all(
batchSecretsPairs.map(({ name, value, isNew }) =>
limit(() => saveSecret({ name, value, serviceId: id, isNew }))
)
);
batchSecrets = '';
await refreshSecrets();
addToast({
message: 'Secrets saved.',
type: 'success'
});
}
</script> </script>
<div <div
@@ -93,4 +126,9 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<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-applications" type="submit">Batch add secrets</button>
</form>
</div> </div>

View File

@@ -25,7 +25,7 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import Storage from './_Storage.svelte'; import Storage from './_Storage.svelte';
import { get } from '$lib/api'; import { get } from '$lib/api';
import Explainer from '$lib/components/Explainer.svelte'; import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
import ServiceLinks from './_ServiceLinks.svelte'; import ServiceLinks from './_ServiceLinks.svelte';
const { id } = $page.params; const { id } = $page.params;
@@ -74,7 +74,7 @@
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4"> <div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">
<div class="flex justify-center py-4 text-center"> <div class="flex justify-center py-4 text-center">
<Explainer <SimpleExplainer
customClass="w-full" 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.'} 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.'}
/> />

View File

@@ -0,0 +1,42 @@
import { post } from '$lib/api';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
type Props = {
isNew: boolean;
name: string;
value: string;
isBuildSecret?: boolean;
isPRMRSecret?: boolean;
isNewSecret?: boolean;
serviceId: string;
};
export async function saveSecret({
isNew,
name,
value,
isBuildSecret,
isPRMRSecret,
isNewSecret,
serviceId
}: Props): Promise<void> {
if (!name) return errorNotification(`${t.get('forms.name')} ${t.get('forms.is_required')}`);
if (!value) return errorNotification(`${t.get('forms.value')} ${t.get('forms.is_required')}`);
try {
await post(`/services/${serviceId}/secrets`, {
name,
value,
isBuildSecret,
isPRMRSecret,
isNew: isNew || false
});
if (isNewSecret) {
name = '';
value = '';
isBuildSecret = false;
}
} catch (error) {
throw error
}
}

View File

@@ -19,13 +19,13 @@
<script lang="ts"> <script lang="ts">
export let settings: any; export let settings: any;
import Setting from '$lib/components/Setting.svelte'; import Setting from '$lib/components/Setting.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { del, get, post } from '$lib/api'; import { del, get, post } from '$lib/api';
import { browser } from '$app/env'; import { browser } from '$app/env';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { addToast, appSession, features } from '$lib/store'; import { addToast, appSession, features } from '$lib/store';
import { errorNotification, getDomain } from '$lib/common'; import { errorNotification, getDomain } from '$lib/common';
import Menu from './_Menu.svelte'; import Menu from './_Menu.svelte';
import Explainer from '$lib/components/Explainer.svelte';
let isRegistrationEnabled = settings.isRegistrationEnabled; let isRegistrationEnabled = settings.isRegistrationEnabled;
let dualCerts = settings.dualCerts; let dualCerts = settings.dualCerts;
@@ -195,8 +195,8 @@
<div class="flex-col"> <div class="flex-col">
<div class="pt-2 text-base font-bold text-stone-100"> <div class="pt-2 text-base font-bold text-stone-100">
{$t('application.url_fqdn')} {$t('application.url_fqdn')}
<Explainer explanation={$t('setting.ssl_explainer')} />
</div> </div>
<Explainer text={$t('setting.ssl_explainer')} />
</div> </div>
<div class="justify-start text-left"> <div class="justify-start text-left">
<input <input
@@ -250,8 +250,8 @@
<div class="flex-col"> <div class="flex-col">
<div class="pt-2 text-base font-bold text-stone-100"> <div class="pt-2 text-base font-bold text-stone-100">
{$t('forms.public_port_range')} {$t('forms.public_port_range')}
<Explainer explanation={$t('forms.public_port_range_explainer')} />
</div> </div>
<Explainer text={$t('forms.public_port_range_explainer')} />
</div> </div>
<div class="mx-auto flex-row items-center justify-center space-y-2"> <div class="mx-auto flex-row items-center justify-center space-y-2">
<input <input
@@ -273,6 +273,7 @@
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
id="isDNSCheckEnabled"
bind:setting={isDNSCheckEnabled} bind:setting={isDNSCheckEnabled}
title={$t('setting.is_dns_check_enabled')} title={$t('setting.is_dns_check_enabled')}
description={$t('setting.is_dns_check_enabled_explainer')} description={$t('setting.is_dns_check_enabled_explainer')}
@@ -280,18 +281,19 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<div class="flex-col"> <div class="text-base font-bold text-stone-100">
<div class="pt-2 text-base font-bold text-stone-100"> Custom DNS servers <Explainer
Custom DNS servers explanation="You can specify a custom DNS server to verify your domains all over Coolify.<br><br>By default, the OS defined DNS servers are used."
</div> />
<Explainer text="You can specify a custom DNS server to verify your domains all over Coolify.<br><br>By default, the OS defined DNS servers are used." />
</div> </div>
<div class="mx-auto flex-row items-center justify-center space-y-2">
<div class="flex-row items-center justify-center">
<input placeholder="1.1.1.1,8.8.8.8" bind:value={DNSServers} /> <input placeholder="1.1.1.1,8.8.8.8" bind:value={DNSServers} />
</div> </div>
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
id="dualCerts"
dataTooltip={$t('setting.must_remove_domain_before_changing')} dataTooltip={$t('setting.must_remove_domain_before_changing')}
disabled={isFqdnSet} disabled={isFqdnSet}
bind:setting={dualCerts} bind:setting={dualCerts}
@@ -302,6 +304,7 @@
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
id="isRegistrationEnabled"
bind:setting={isRegistrationEnabled} bind:setting={isRegistrationEnabled}
title={$t('setting.registration_allowed')} title={$t('setting.registration_allowed')}
description={$t('setting.registration_allowed_explainer')} description={$t('setting.registration_allowed_explainer')}
@@ -311,6 +314,7 @@
{#if browser && $features.beta} {#if browser && $features.beta}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
id="isAutoUpdateEnabled"
bind:setting={isAutoUpdateEnabled} bind:setting={isAutoUpdateEnabled}
title={$t('setting.auto_update_enabled')} title={$t('setting.auto_update_enabled')}
description={$t('setting.auto_update_enabled_explainer')} description={$t('setting.auto_update_enabled_explainer')}

View File

@@ -3,11 +3,11 @@
export let settings: any; export let settings: any;
import { page } from '$app/stores'; import { page } from '$app/stores';
import { getAPIUrl, getWebhookUrl, post } from '$lib/api'; import { getAPIUrl, getWebhookUrl, post } from '$lib/api';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { dashify, errorNotification, getDomain } from '$lib/common'; import { dashify, errorNotification, getDomain } from '$lib/common';
import { addToast, appSession } from '$lib/store'; import { addToast, appSession } from '$lib/store';
import { dev } from '$app/env'; import { dev } from '$app/env';
import Explainer from '$lib/components/Explainer.svelte';
const { id } = $page.params; const { id } = $page.params;
@@ -24,7 +24,7 @@
apiUrl: source.apiUrl.replace(/\/$/, '') apiUrl: source.apiUrl.replace(/\/$/, '')
}); });
return addToast({ return addToast({
message:'Configuration saved.', message: 'Configuration saved.',
type: 'success' type: 'success'
}); });
} catch (error) { } catch (error) {
@@ -93,7 +93,7 @@
<div class="mx-auto max-w-4xl px-6"> <div class="mx-auto max-w-4xl px-6">
{#if !source.githubAppId} {#if !source.githubAppId}
<form on:submit|preventDefault={newGithubApp} class="py-4"> <form on:submit|preventDefault={newGithubApp} class="py-4">
<div class="flex space-x-1 pb-7"> <div class="grid gap-1 lg:grid-flow-col pb-7">
<div class="title">General</div> <div class="title">General</div>
{#if !source.githubAppId} {#if !source.githubAppId}
<button class="btn btn-sm bg-sources" type="submit">Save & Redirect to GitHub</button> <button class="btn btn-sm bg-sources" type="submit">Save & Redirect to GitHub</button>
@@ -101,21 +101,25 @@
</div> </div>
<div class="grid grid-flow-row gap-2 px-10"> <div class="grid grid-flow-row gap-2 px-10">
<div class="grid grid-flow-row gap-2"> <div class="grid grid-flow-row gap-2">
<div class="mt-2 grid grid-cols-2 items-center"> <div class="mt-2 grid lg:grid-cols-2 items-center">
<label for="name" class="text-base font-bold text-stone-100">Name</label> <label for="name" class="text-base font-bold text-stone-100">Name</label>
<input name="name" id="name" required bind:value={source.name} /> <input name="name" id="name" required bind:value={source.name} />
</div> </div>
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid lg:grid-cols-2 items-center">
<label for="htmlUrl" class="text-base font-bold text-stone-100">HTML URL</label> <label for="htmlUrl" class="text-base font-bold text-stone-100">HTML URL</label>
<input name="htmlUrl" id="htmlUrl" required bind:value={source.htmlUrl} /> <input name="htmlUrl" id="htmlUrl" required bind:value={source.htmlUrl} />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid lg:grid-cols-2 items-center">
<label for="apiUrl" class="text-base font-bold text-stone-100">API URL</label> <label for="apiUrl" class="text-base font-bold text-stone-100">API URL</label>
<input name="apiUrl" id="apiUrl" required bind:value={source.apiUrl} /> <input name="apiUrl" id="apiUrl" required bind:value={source.apiUrl} />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid lg:grid-cols-2 items-center">
<label for="customPort" class="text-base font-bold text-stone-100">Custom SSH Port</label> <label for="customPort" class="text-base font-bold text-stone-100"
>Custom SSH Port <Explainer
explanation={'If you use a self-hosted version of Git, you can provide custom port for all the Git related actions.'}
/></label
>
<input <input
name="customPort" name="customPort"
id="customPort" id="customPort"
@@ -124,18 +128,15 @@
required required
value={source.customPort} value={source.customPort}
/> />
<Explainer
text="If you use a self-hosted version of Git, you can provide custom port for all the Git related actions."
/>
</div> </div>
<div class="grid grid-cols-2"> <div class="grid lg:grid-cols-2">
<div class="flex flex-col"> <div class="flex flex-col">
<label for="organization" class="pt-2 text-base font-bold text-stone-100" <label for="organization" class="pt-2 text-base font-bold text-stone-100"
>Organization</label >Organization
<Explainer
explanation={"Fill it if you would like to use an organization's as your Git Source. Otherwise your user will be used."}
/></label
> >
<Explainer
text="Fill it if you would like to use an organization's as your Git Source. Otherwise your user will be used."
/>
</div> </div>
<input <input
name="organization" name="organization"
@@ -148,29 +149,30 @@
</form> </form>
{:else if source.githubApp?.installationId} {:else if source.githubApp?.installationId}
<form on:submit|preventDefault={handleSubmit} class="py-4"> <form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-5 "> <div class="flex md:flex-row space-y-2 md:space-y-0 space-x-0 md:space-x-2 flex-col pb-5">
<div class="title">{$t('general')}</div> <div class="title">{$t('general')}</div>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<button <button class="btn btn-sm bg-sources" type="submit" disabled={loading}
class="btn btn-sm bg-sources" >{loading ? 'Saving...' : 'Save'}</button
type="submit"
disabled={loading}>{loading ? 'Saving...' : 'Save'}</button
> >
<a <a
class="btn btn-sm" class="btn btn-sm"
href={`${source.htmlUrl}/${source.htmlUrl === 'https://github.com' ? 'apps' : 'github-apps'}/${source.githubApp.name}/installations/new`} href={`${source.htmlUrl}/${
source.htmlUrl === 'https://github.com' ? 'apps' : 'github-apps'
}/${source.githubApp.name}/installations/new`}
>{$t('source.change_app_settings', { name: 'GitHub' })}</a >{$t('source.change_app_settings', { name: 'GitHub' })}</a
> >
{/if} {/if}
</div> </div>
<div class="grid grid-flow-row gap-2 px-10"> <div class="grid grid-flow-row gap-2 px-10">
<div class="grid grid-flow-row gap-2"> <div class="grid grid-flow-row gap-2">
<div class="mt-2 grid grid-cols-2 items-center"> <div class="mt-2 grid lg:grid-cols-2 items-center">
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label> <label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
<input name="name" id="name" required bind:value={source.name} /> <input name="name" id="name" required bind:value={source.name} />
</div> </div>
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid lg:grid-cols-2 items-center">
<label for="htmlUrl" class="text-base font-bold text-stone-100">HTML URL</label> <label for="htmlUrl" class="text-base font-bold text-stone-100">HTML URL</label>
<input <input
name="htmlUrl" name="htmlUrl"
@@ -181,7 +183,7 @@
bind:value={source.htmlUrl} bind:value={source.htmlUrl}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid lg:grid-cols-2 items-center">
<label for="apiUrl" class="text-base font-bold text-stone-100">API URL</label> <label for="apiUrl" class="text-base font-bold text-stone-100">API URL</label>
<input <input
name="apiUrl" name="apiUrl"
@@ -193,9 +195,11 @@
/> />
</div> </div>
{#if selfHosted} {#if selfHosted}
<div class="grid grid-cols-2 items-center"> <div class="grid lg:grid-cols-2 items-center">
<label for="customPort" class="text-base font-bold text-stone-100" <label for="customPort" class="text-base font-bold text-stone-100"
>Custom SSH Port</label >Custom SSH Port <Explainer
explanation="If you use a self-hosted version of Git, you can provide custom port for all the Git related actions."
/></label
> >
<input <input
name="customPort" name="customPort"
@@ -205,12 +209,9 @@
required required
value={source.customPort} value={source.customPort}
/> />
<Explainer
text="If you use a self-hosted version of Git, you can provide custom port for all the Git related actions."
/>
</div> </div>
{/if} {/if}
<div class="grid grid-cols-2"> <div class="grid lg:grid-cols-2">
<div class="flex flex-col"> <div class="flex flex-col">
<label for="organization" class="pt-2 text-base font-bold text-stone-100" <label for="organization" class="pt-2 text-base font-bold text-stone-100"
>Organization</label >Organization</label
@@ -229,10 +230,12 @@
</form> </form>
{:else} {:else}
<div class="text-center"> <div class="text-center">
<a href={`${source.htmlUrl}/${source.htmlUrl === 'https://github.com' ? 'apps' : 'github-apps'}/${source.githubApp.name}/installations/new`}> <a
<button class="box-selection bg-sources text-xl font-bold" href={`${source.htmlUrl}/${
>Install Repositories</button source.htmlUrl === 'https://github.com' ? 'apps' : 'github-apps'
></a }/${source.githubApp.name}/installations/new`}
>
<button class="box-selection bg-sources text-xl font-bold">Install Repositories</button></a
> >
</div> </div>
{/if} {/if}

View File

@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
export let source: any; export let source: any;
export let settings: any; export let settings: any;
import Explainer from '$lib/components/Explainer.svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { getAPIUrl, post } from '$lib/api'; import { getAPIUrl, post } from '$lib/api';
@@ -11,6 +10,7 @@
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import { addToast, appSession } from '$lib/store'; import { addToast, appSession } from '$lib/store';
import Explainer from '$lib/components/Explainer.svelte';
const { id } = $page.params; const { id } = $page.params;
let url = settings.fqdn ? settings.fqdn : window.location.origin; let url = settings.fqdn ? settings.fqdn : window.location.origin;
@@ -148,10 +148,8 @@
<div class="flex space-x-1 pb-7"> <div class="flex space-x-1 pb-7">
<div class="title">General</div> <div class="title">General</div>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<button <button type="submit" class="btn btn-sm bg-sources" disabled={loading}
type="submit" >{loading ? $t('forms.saving') : $t('forms.save')}</button
class="btn btn-sm bg-sources"
disabled={loading}>{loading ? $t('forms.saving') : $t('forms.save')}</button
> >
{#if source.gitlabAppId} {#if source.gitlabAppId}
<button class="btn btn-sm" on:click|preventDefault={changeSettings} <button class="btn btn-sm" on:click|preventDefault={changeSettings}
@@ -166,16 +164,12 @@
</div> </div>
<div class="grid grid-flow-row gap-2 px-10"> <div class="grid grid-flow-row gap-2 px-10">
{#if !source.gitlabAppId} {#if !source.gitlabAppId}
<Explainer <a
customClass="w-full" href="https://docs.coollabs.io/coolify/sources#how-to-integrate-with-gitlab"
text="<span class='font-bold text-base text-white'>Scopes required:</span> class="font-bold "
<br>- <span class='text-sources font-bold'>api</span> (Access the authenticated user's API) target="_blank"
<br>- <span class='text-sources font-bold'>read_repository</span> (Allows read-only access to the repository) rel="noopener noreferrer">Documentation and detailed instructions.</a
<br>- <span class='text-sources font-bold'>email</span> (Allows read-only access to the user's primary email address using OpenID Connect) >
<br>
<br>For extra security, you can set <span class='text-sources font-bold'>Expire Access Tokens</span>
<br><br>Webhook URL: <span class='text-sources font-bold'>{url}/webhooks/gitlab</span>"
/>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="type" class="text-base font-bold text-stone-100">Application Type</label> <label for="type" class="text-base font-bold text-stone-100">Application Type</label>
<select name="type" id="type" class="w-96" bind:value={applicationType}> <select name="type" id="type" class="w-96" bind:value={applicationType}>
@@ -245,7 +239,11 @@
</div> </div>
{#if selfHosted} {#if selfHosted}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="customPort" class="text-base font-bold text-stone-100">Custom SSH Port</label> <label for="customPort" class="text-base font-bold text-stone-100"
>Custom SSH Port <Explainer
explanation={'If you use a self-hosted version of Git, you can provide custom port for all the Git related actions.'}
/></label
>
<input <input
name="customPort" name="customPort"
id="customPort" id="customPort"
@@ -254,19 +252,16 @@
required required
bind:value={source.customPort} bind:value={source.customPort}
/> />
<Explainer
text="If you use a self-hosted version of Git, you can provide custom port for all the Git related actions."
/>
</div> </div>
{/if} {/if}
<div class="grid grid-cols-2 items-start"> <div class="grid grid-cols-2 items-start">
<div class="flex-col"> <div class="flex-col">
<label for="oauthId" class="pt-2 text-base font-bold text-stone-100" <label for="oauthId" class="pt-2 text-base font-bold text-stone-100"
>{$t('source.oauth_id')}</label >{$t('source.oauth_id')}
{#if !source.gitlabAppId}
<Explainer explanation={$t('source.oauth_id_explainer')} />
{/if}</label
> >
{#if !source.gitlabAppId}
<Explainer text={$t('source.oauth_id_explainer')} />
{/if}
</div> </div>
<input <input
disabled={source.gitlabAppId} disabled={source.gitlabAppId}

View File

@@ -37,6 +37,7 @@
import { appSession } from '$lib/store'; import { appSession } from '$lib/store';
import DeleteIcon from '$lib/components/DeleteIcon.svelte'; import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import Tooltip from '$lib/components/Tooltip.svelte';
const { id } = $page.params; const { id } = $page.params;
async function deleteSource(name: string) { async function deleteSource(name: string) {
@@ -55,15 +56,14 @@
{#if id !== 'new'} {#if id !== 'new'}
<nav class="nav-side"> <nav class="nav-side">
<button <button
id="delete"
on:click={() => deleteSource(source.name)} on:click={() => deleteSource(source.name)}
type="submit" type="submit"
disabled={!$appSession.isAdmin} disabled={!$appSession.isAdmin}
class:hover:text-red-500={$appSession.isAdmin} class:hover:text-red-500={$appSession.isAdmin}
class="icons tooltip tooltip-primary tooltip-bottom bg-transparent text-sm" class="icons bg-transparent text-sm"><DeleteIcon /></button
data-tip={$appSession.isAdmin
? $t('source.delete_git_source')
: $t('source.permission_denied')}><DeleteIcon /></button
> >
</nav> </nav>
<Tooltip triggeredBy="#delete">Delete</Tooltip>
{/if} {/if}
<slot /> <slot />

View File

@@ -85,7 +85,7 @@ label {
@apply inline-block w-64 text-xs tracking-tight md:text-sm; @apply inline-block w-64 text-xs tracking-tight md:text-sm;
} }
.btn { .btn {
@apply text-white text-base; @apply text-white text-base min-w-fit;
} }
a { a {
@@ -195,4 +195,8 @@ a {
} }
.table *{ .table *{
border: none; border: none;
}
input {
@apply w-48 lg:w-96;
} }

View File

@@ -1,15 +1,15 @@
const defaultTheme = require('tailwindcss/defaultTheme'); const defaultTheme = require('tailwindcss/defaultTheme');
module.exports = { module.exports = {
content: ['./**/*.html', './src/**/*.{js,jsx,ts,tsx,svelte}'], content: ['./**/*.html', './src/**/*.{js,jsx,ts,tsx,svelte}', "./node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}",],
important: true, important: true,
daisyui: { daisyui: {
themes: [ themes: [
{ {
coollabs: { coollabs: {
"base-100":"#323232", "base-100": "#323232",
"base-200":"#242424", "base-200": "#242424",
"base-300":"#181818", "base-300": "#181818",
"primary": "#6d28d9", "primary": "#6d28d9",
"primary-content": "#fff", "primary-content": "#fff",
"secondary": "#343232", "secondary": "#343232",
@@ -40,13 +40,13 @@ module.exports = {
sans: ['Poppins', ...defaultTheme.fontFamily.sans] sans: ['Poppins', ...defaultTheme.fontFamily.sans]
}, },
colors: { colors: {
"applications":"#16A34A", "applications": "#16A34A",
"databases":"#9333EA", "databases": "#9333EA",
"destinations":"#0284C7", "destinations": "#0284C7",
"sources":"#EA580C", "sources": "#EA580C",
"services":"#DB2777", "services": "#DB2777",
"settings":"#FEE440", "settings": "#FEE440",
"iam":"#C026D3", "iam": "#C026D3",
coollabs: '#6B16ED', coollabs: '#6B16ED',
'coollabs-100': '#7317FF', 'coollabs-100': '#7317FF',
coolblack: '#161616', coolblack: '#161616',
@@ -62,5 +62,6 @@ module.exports = {
scrollbar: ['dark'], scrollbar: ['dark'],
extend: {} extend: {}
}, },
darkMode: 'class',
plugins: [require('tailwindcss-scrollbar'), require('daisyui'), require("@tailwindcss/typography")] plugins: [require('tailwindcss-scrollbar'), require('daisyui'), require("@tailwindcss/typography")]
}; };

View File

@@ -1,11 +1,12 @@
{ {
"name": "coolify", "name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.", "description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "3.8.7", "version": "3.9.0-rc.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": "github:coollabsio/coolify", "repository": "github:coollabsio/coolify",
"scripts": { "scripts": {
"oc": "opencollective-setup", "oc": "opencollective-setup",
"translate": "pnpm run --filter i18n-converter translate",
"db:studio": "pnpm run --filter api db:studio", "db:studio": "pnpm run --filter api db:studio",
"db:push": "pnpm run --filter api db:push", "db:push": "pnpm run --filter api db:push",
"db:seed": "pnpm run --filter api db:seed", "db:seed": "pnpm run --filter api db:seed",

150
pnpm-lock.yaml generated
View File

@@ -23,7 +23,7 @@ importers:
'@fastify/static': 6.5.0 '@fastify/static': 6.5.0
'@iarna/toml': 2.2.5 '@iarna/toml': 2.2.5
'@ladjs/graceful': 3.0.2 '@ladjs/graceful': 3.0.2
'@prisma/client': 4.2.1 '@prisma/client': 3.15.2
'@types/node': 18.7.13 '@types/node': 18.7.13
'@types/node-os-utils': 1.3.0 '@types/node-os-utils': 1.3.0
'@typescript-eslint/eslint-plugin': 5.35.1 '@typescript-eslint/eslint-plugin': 5.35.1
@@ -56,7 +56,7 @@ importers:
p-all: 4.0.0 p-all: 4.0.0
p-throttle: 5.0.0 p-throttle: 5.0.0
prettier: 2.7.1 prettier: 2.7.1
prisma: 4.2.1 prisma: 3.15.2
public-ip: 6.0.1 public-ip: 6.0.1
rimraf: 3.0.2 rimraf: 3.0.2
ssh-config: 4.1.6 ssh-config: 4.1.6
@@ -74,7 +74,7 @@ importers:
'@fastify/static': 6.5.0 '@fastify/static': 6.5.0
'@iarna/toml': 2.2.5 '@iarna/toml': 2.2.5
'@ladjs/graceful': 3.0.2 '@ladjs/graceful': 3.0.2
'@prisma/client': 4.2.1_prisma@4.2.1 '@prisma/client': 3.15.2_prisma@3.15.2
axios: 0.27.2 axios: 0.27.2
bcryptjs: 2.4.3 bcryptjs: 2.4.3
bree: 9.1.2 bree: 9.1.2
@@ -112,14 +112,28 @@ importers:
eslint-plugin-prettier: 4.2.1_tgumt6uwl2md3n6uqnggd6wvce eslint-plugin-prettier: 4.2.1_tgumt6uwl2md3n6uqnggd6wvce
nodemon: 2.0.19 nodemon: 2.0.19
prettier: 2.7.1 prettier: 2.7.1
prisma: 4.2.1 prisma: 3.15.2
rimraf: 3.0.2 rimraf: 3.0.2
tsconfig-paths: 4.1.0 tsconfig-paths: 4.1.0
typescript: 4.7.4 typescript: 4.7.4
apps/i18n:
specifiers:
dotenv: 16.0.2
gettext-parser: 6.0.0
got: 12.3.1
node-gettext: 3.0.0
dependencies:
dotenv: 16.0.2
gettext-parser: 6.0.0
got: 12.3.1
node-gettext: 3.0.0
apps/ui: apps/ui:
specifiers: specifiers:
'@floating-ui/dom': 1.0.1
'@playwright/test': 1.25.1 '@playwright/test': 1.25.1
'@popperjs/core': 2.11.6
'@sveltejs/adapter-static': 1.0.0-next.39 '@sveltejs/adapter-static': 1.0.0-next.39
'@sveltejs/kit': 1.0.0-next.405 '@sveltejs/kit': 1.0.0-next.405
'@tailwindcss/typography': ^0.5.4 '@tailwindcss/typography': ^0.5.4
@@ -127,11 +141,14 @@ importers:
'@typescript-eslint/eslint-plugin': 5.35.1 '@typescript-eslint/eslint-plugin': 5.35.1
'@typescript-eslint/parser': 5.35.1 '@typescript-eslint/parser': 5.35.1
autoprefixer: 10.4.8 autoprefixer: 10.4.8
classnames: 2.3.1
cuid: 2.1.8 cuid: 2.1.8
daisyui: 2.24.0 daisyui: 2.24.0
eslint: 8.22.0 eslint: 8.22.0
eslint-config-prettier: 8.5.0 eslint-config-prettier: 8.5.0
eslint-plugin-svelte3: 4.0.0 eslint-plugin-svelte3: 4.0.0
flowbite: 1.5.2
flowbite-svelte: 0.26.2
js-cookie: 3.0.1 js-cookie: 3.0.1
p-limit: 4.0.0 p-limit: 4.0.0
postcss: 8.4.16 postcss: 8.4.16
@@ -157,15 +174,20 @@ importers:
svelte-select: 4.4.7 svelte-select: 4.4.7
sveltekit-i18n: 2.2.2_svelte@3.49.0 sveltekit-i18n: 2.2.2_svelte@3.49.0
devDependencies: devDependencies:
'@floating-ui/dom': 1.0.1
'@playwright/test': 1.25.1 '@playwright/test': 1.25.1
'@popperjs/core': 2.11.6
'@sveltejs/kit': 1.0.0-next.405_svelte@3.49.0+vite@3.0.5 '@sveltejs/kit': 1.0.0-next.405_svelte@3.49.0+vite@3.0.5
'@types/js-cookie': 3.0.2 '@types/js-cookie': 3.0.2
'@typescript-eslint/eslint-plugin': 5.35.1_ktjxjibzrfqejavile4bhmzhjq '@typescript-eslint/eslint-plugin': 5.35.1_ktjxjibzrfqejavile4bhmzhjq
'@typescript-eslint/parser': 5.35.1_4rv7y5c6xz3vfxwhbrcxxi73bq '@typescript-eslint/parser': 5.35.1_4rv7y5c6xz3vfxwhbrcxxi73bq
autoprefixer: 10.4.8_postcss@8.4.16 autoprefixer: 10.4.8_postcss@8.4.16
classnames: 2.3.1
eslint: 8.22.0 eslint: 8.22.0
eslint-config-prettier: 8.5.0_eslint@8.22.0 eslint-config-prettier: 8.5.0_eslint@8.22.0
eslint-plugin-svelte3: 4.0.0_laaqauvsmoyypsiqkozwyi2fn4 eslint-plugin-svelte3: 4.0.0_laaqauvsmoyypsiqkozwyi2fn4
flowbite: 1.5.2
flowbite-svelte: 0.26.2
postcss: 8.4.16 postcss: 8.4.16
prettier: 2.7.1 prettier: 2.7.1
prettier-plugin-svelte: 2.7.0_o3ioganyptcsrh6x4hnxvjkpqi prettier-plugin-svelte: 2.7.0_o3ioganyptcsrh6x4hnxvjkpqi
@@ -357,6 +379,16 @@ packages:
- supports-color - supports-color
dev: false dev: false
/@floating-ui/core/1.0.1:
resolution: {integrity: sha512-bO37brCPfteXQfFY0DyNDGB3+IMe4j150KFQcgJ5aBP295p9nBGeHEs/p0czrRbtlHq4Px/yoPXO/+dOCcF4uA==}
dev: true
/@floating-ui/dom/1.0.1:
resolution: {integrity: sha512-wBDiLUKWU8QNPNOTAFHiIAkBv1KlHauG2AhqjSeh2H+wR8PX+AArXfz8NkRexH5PgMJMmSOS70YS89AbWYh5dA==}
dependencies:
'@floating-ui/core': 1.0.1
dev: true
/@humanwhocodes/config-array/0.10.4: /@humanwhocodes/config-array/0.10.4:
resolution: {integrity: sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==} resolution: {integrity: sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==}
engines: {node: '>=10.10.0'} engines: {node: '>=10.10.0'}
@@ -446,9 +478,13 @@ packages:
playwright-core: 1.25.1 playwright-core: 1.25.1
dev: true dev: true
/@prisma/client/4.2.1_prisma@4.2.1: /@popperjs/core/2.11.6:
resolution: {integrity: sha512-PZBkY60+k5oix+e6IUfl3ub8TbRLNsPLdfWrdy2eh80WcHTaT+/UfvXf/B7gXedH7FRtbPFHZXk1hZenJiJZFQ==} resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==}
engines: {node: '>=14.17'} dev: true
/@prisma/client/3.15.2_prisma@3.15.2:
resolution: {integrity: sha512-ErqtwhX12ubPhU4d++30uFY/rPcyvjk+mdifaZO5SeM21zS3t4jQrscy8+6IyB0GIYshl5ldTq6JSBo1d63i8w==}
engines: {node: '>=12.6'}
requiresBuild: true requiresBuild: true
peerDependencies: peerDependencies:
prisma: '*' prisma: '*'
@@ -456,16 +492,16 @@ packages:
prisma: prisma:
optional: true optional: true
dependencies: dependencies:
'@prisma/engines-version': 4.2.0-33.2920a97877e12e055c1333079b8d19cee7f33826 '@prisma/engines-version': 3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e
prisma: 4.2.1 prisma: 3.15.2
dev: false dev: false
/@prisma/engines-version/4.2.0-33.2920a97877e12e055c1333079b8d19cee7f33826: /@prisma/engines-version/3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e:
resolution: {integrity: sha512-tktkqdiwqE4QhmE088boPt+FwPj1Jub/zk+5F6sEfcRHzO5yz9jyMD5HFVtiwxZPLx/8Xg9ElnuTi8E5lWVQFQ==} resolution: {integrity: sha512-e3k2Vd606efd1ZYy2NQKkT4C/pn31nehyLhVug6To/q8JT8FpiMrDy7zmm3KLF0L98NOQQcutaVtAPhzKhzn9w==}
dev: false dev: false
/@prisma/engines/4.2.1: /@prisma/engines/3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e:
resolution: {integrity: sha512-0KqBwREUOjBiHwITsQzw2DWfLHjntvbqzGRawj4sBMnIiL5CXwyDUKeHOwXzKMtNr1rEjxEsypM14g0CzLRK3g==} resolution: {integrity: sha512-NHlojO1DFTsSi3FtEleL9QWXeSF/UjhCW0fgpi7bumnNZ4wj/eQ+BJJ5n2pgoOliTOGv9nX2qXvmHap7rJMNmg==}
requiresBuild: true requiresBuild: true
/@rollup/pluginutils/4.2.1: /@rollup/pluginutils/4.2.1:
@@ -1988,6 +2024,10 @@ packages:
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
dev: false dev: false
/classnames/2.3.1:
resolution: {integrity: sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==}
dev: true
/clean-stack/4.2.0: /clean-stack/4.2.0:
resolution: {integrity: sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==} resolution: {integrity: sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -2101,6 +2141,11 @@ packages:
safe-buffer: 5.2.1 safe-buffer: 5.2.1
dev: false dev: false
/content-type/1.0.4:
resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==}
engines: {node: '>= 0.6'}
dev: false
/convert-hrtime/3.0.0: /convert-hrtime/3.0.0:
resolution: {integrity: sha512-7V+KqSvMiHp8yWDuwfww06XleMWVVB9b9tURBx+G7UTADuo5hYPuowKloz4OzOqbPezxgo+fdQ1522WzPG4OeA==} resolution: {integrity: sha512-7V+KqSvMiHp8yWDuwfww06XleMWVVB9b9tURBx+G7UTADuo5hYPuowKloz4OzOqbPezxgo+fdQ1522WzPG4OeA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -2448,6 +2493,11 @@ packages:
engines: {node: '>=12'} engines: {node: '>=12'}
dev: false dev: false
/dotenv/16.0.2:
resolution: {integrity: sha512-JvpYKUmzQhYoIFgK2MOnF3bciIZoItIIoryihy0rIA+H4Jy0FmgyKYAHCTN98P5ybGSJcIFbh6QKeJdtZd1qhA==}
engines: {node: '>=12'}
dev: false
/dotenv/8.6.0: /dotenv/8.6.0:
resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -2481,6 +2531,12 @@ packages:
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
dev: false dev: false
/encoding/0.1.13:
resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==}
dependencies:
iconv-lite: 0.6.3
dev: false
/end-of-stream/1.4.4: /end-of-stream/1.4.4:
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
dependencies: dependencies:
@@ -3433,6 +3489,24 @@ packages:
resolution: {integrity: sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==} resolution: {integrity: sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==}
dev: true dev: true
/flowbite-svelte/0.26.2:
resolution: {integrity: sha512-CpZ33fyMuzrYvECKN2rD5zNa/ZY0c4cDMcY6wE/hvajFybbCqwMYzd0AkA88zTNm5fknEN0/E23TVmD4Qkhzzw==}
engines: {node: '>=16.0.0', npm: '>=7.0.0'}
dependencies:
'@floating-ui/dom': 1.0.1
'@popperjs/core': 2.11.6
classnames: 2.3.1
flowbite: 1.5.2
svelte-heros: 2.3.5
dev: true
/flowbite/1.5.2:
resolution: {integrity: sha512-oSKhPkg0bYb4dZG4ypSh++dPrFM3IOSER6HOwJHYRFPW5u9PXL4AQFMQh7Kytgws2xrDQyl/zqefZmymeWt2IA==}
dependencies:
'@popperjs/core': 2.11.6
mini-svg-data-uri: 1.4.4
dev: true
/follow-redirects/1.15.0: /follow-redirects/1.15.0:
resolution: {integrity: sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ==} resolution: {integrity: sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ==}
engines: {node: '>=4.0'} engines: {node: '>=4.0'}
@@ -3560,6 +3634,15 @@ packages:
get-intrinsic: 1.1.1 get-intrinsic: 1.1.1
dev: true dev: true
/gettext-parser/6.0.0:
resolution: {integrity: sha512-eWFsR78gc/eKnzDgc919Us3cbxQbzxK1L8vAIZrKMQqOUgULyeqmczNlBjTlVTk2FaB7nV9C1oobd/PGBOqNmg==}
dependencies:
content-type: 1.0.4
encoding: 0.1.13
readable-stream: 4.1.0
safe-buffer: 5.2.1
dev: false
/glob-parent/5.1.2: /glob-parent/5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@@ -3759,6 +3842,13 @@ packages:
safer-buffer: 2.1.2 safer-buffer: 2.1.2
dev: true dev: true
/iconv-lite/0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
dependencies:
safer-buffer: 2.1.2
dev: false
/ieee754/1.2.1: /ieee754/1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
dev: false dev: false
@@ -4264,6 +4354,10 @@ packages:
resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==}
dev: false dev: false
/lodash.get/4.4.2:
resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==}
dev: false
/lodash.includes/4.3.0: /lodash.includes/4.3.0:
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
dev: false dev: false
@@ -4434,6 +4528,11 @@ packages:
engines: {node: '>=4'} engines: {node: '>=4'}
dev: true dev: true
/mini-svg-data-uri/1.4.4:
resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==}
hasBin: true
dev: true
/minimalistic-assert/1.0.1: /minimalistic-assert/1.0.1:
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
dev: false dev: false
@@ -4535,6 +4634,12 @@ packages:
engines: {node: '>= 6.13.0'} engines: {node: '>= 6.13.0'}
dev: false dev: false
/node-gettext/3.0.0:
resolution: {integrity: sha512-/VRYibXmVoN6tnSAY2JWhNRhWYJ8Cd844jrZU/DwLVoI4vBI6ceYbd8i42sYZ9uOgDH3S7vslIKOWV/ZrT2YBA==}
dependencies:
lodash.get: 4.4.2
dev: false
/node-os-utils/1.3.7: /node-os-utils/1.3.7:
resolution: {integrity: sha512-fvnX9tZbR7WfCG5BAy3yO/nCLyjVWD6MghEq0z5FDfN+ZXpLWNITBdbifxQkQ25ebr16G0N7eRWJisOcMEHG3Q==} resolution: {integrity: sha512-fvnX9tZbR7WfCG5BAy3yO/nCLyjVWD6MghEq0z5FDfN+ZXpLWNITBdbifxQkQ25ebr16G0N7eRWJisOcMEHG3Q==}
dev: false dev: false
@@ -5071,13 +5176,13 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/prisma/4.2.1: /prisma/3.15.2:
resolution: {integrity: sha512-HuYqnTDgH8atjPGtYmY0Ql9XrrJnfW7daG1PtAJRW0E6gJxc50lY3vrIDn0yjMR3TvRlypjTcspQX8DT+xD4Sg==} resolution: {integrity: sha512-nMNSMZvtwrvoEQ/mui8L/aiCLZRCj5t6L3yujKpcDhIPk7garp8tL4nMx2+oYsN0FWBacevJhazfXAbV1kfBzA==}
engines: {node: '>=14.17'} engines: {node: '>=12.6'}
hasBin: true hasBin: true
requiresBuild: true requiresBuild: true
dependencies: dependencies:
'@prisma/engines': 4.2.1 '@prisma/engines': 3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e
/private/0.1.8: /private/0.1.8:
resolution: {integrity: sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==} resolution: {integrity: sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==}
@@ -5210,6 +5315,13 @@ packages:
abort-controller: 3.0.0 abort-controller: 3.0.0
dev: false dev: false
/readable-stream/4.1.0:
resolution: {integrity: sha512-sVisi3+P2lJ2t0BPbpK629j8wRW06yKGJUcaLAGXPAUhyUxVJm7VsCTit1PFgT4JHUDMrGNR+ZjSKpzGaRF3zw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
abort-controller: 3.0.0
dev: false
/readdirp/3.6.0: /readdirp/3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'} engines: {node: '>=8.10.0'}
@@ -5795,6 +5907,10 @@ packages:
- sugarss - sugarss
dev: true dev: true
/svelte-heros/2.3.5:
resolution: {integrity: sha512-08PdccaeRPP1pVa90AGieTwGzrNtXpC1Fry+i95OTvcR3xbGRU/hxK4rnaFYvGgk1Pxj9YT6GKGTEX8uXE9XJQ==}
dev: true
/svelte-hmr/0.14.12_svelte@3.49.0: /svelte-hmr/0.14.12_svelte@3.49.0:
resolution: {integrity: sha512-4QSW/VvXuqVcFZ+RhxiR8/newmwOCTlbYIezvkeN6302YFRE8cXy0naamHcjz8Y9Ce3ITTZtrHrIL0AGfyo61w==} resolution: {integrity: sha512-4QSW/VvXuqVcFZ+RhxiR8/newmwOCTlbYIezvkeN6302YFRE8cXy0naamHcjz8Y9Ce3ITTZtrHrIL0AGfyo61w==}
engines: {node: ^12.20 || ^14.13.1 || >= 16} engines: {node: ^12.20 || ^14.13.1 || >= 16}