Merge branch 'next' into grafana-service

This commit is contained in:
Andras Bacsai 2022-09-20 14:50:37 +02:00 committed by GitHub
commit 78076f7854
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
144 changed files with 4815 additions and 4349 deletions

1
.gitignore vendored
View File

@ -13,3 +13,4 @@ apps/api/db/*.db
local-serve local-serve
apps/api/db/migration.db-journal apps/api/db/migration.db-journal
apps/api/core* apps/api/core*
logs

View File

@ -1,256 +0,0 @@
# 👋 Welcome
First of all, thank you for considering contributing to my project! It means a lot 💜.
## 🙋 Want to help?
If you begin in GitHub contribution, you can find the [first contribution](https://github.com/firstcontributions/first-contributions) and follow this guide.
Follow the [introduction](#introduction) to get started then start contributing!
This is a little list of what you can do to help the project:
- [🧑‍💻 Develop your own ideas](#developer-contribution)
- [🌐 Translate the project](#translation)
## 👋 Introduction
### Setup with Github codespaces
If you have github codespaces enabled then you can just create a codespace and run `pnpm dev` to run your the dev environment. All the required dependencies and packages has been configured for you already.
### Setup with Gitpod
If you have a [Gitpod](https://gitpod.io), you can just create a workspace from this repository, run `pnpm install && pnpm db:push && pnpm db:seed` and then `pnpm dev`. All the required dependencies and packages has been configured for you already.
### Setup locally in your machine
> 🔴 At the moment, Coolify **doesn't support Windows**. You must use Linux or MacOS. Consider using Gitpod or Github Codespaces.
#### Recommended Pull Request Guideline
- Fork the project
- Clone your fork repo to local
- Create a new branch
- Push to your fork repo
- Create a pull request: https://github.com/coollabsio/coolify/compare
- Write a proper description
- Open the pull request to review against `next` branch
---
# 🧑‍💻 Developer contribution
## Technical skills required
- **Languages**: Node.js / Javascript / Typescript
- **Framework JS/TS**: [SvelteKit](https://kit.svelte.dev/) & [Fastify](https://www.fastify.io/)
- **Database ORM**: [Prisma.io](https://www.prisma.io/)
- **Docker Engine API**
---
## How to start after you set up your local fork?
### Prerequisites
1. Due to the lock file, this repository is best with [pnpm](https://pnpm.io). I recommend you try and use `pnpm` because it is cool and efficient!
2. You need to have [Docker Engine](https://docs.docker.com/engine/install/) installed locally.
3. You need to have [Docker Compose Plugin](https://docs.docker.com/compose/install/compose-plugin/) installed locally.
4. You need to have [GIT LFS Support](https://git-lfs.github.com/) installed locally.
Optional:
4. To test Heroku buildpacks, you need [pack](https://github.com/buildpacks/pack) binary installed locally.
### Steps for local setup
1. Copy `apps/api/.env.template` to `apps/api/.env.template` and set the `COOLIFY_APP_ID` environment variable to something cool.
2. Install dependencies with `pnpm install`.
3. Need to create a local SQlite database with `pnpm db:push`.
This will apply all migrations at `db/dev.db`.
4. Seed the database with base entities with `pnpm db:seed`
5. You can start coding after starting `pnpm dev`.
---
## Database migrations
During development, if you change the database layout, you need to run `pnpm db:push` to migrate the database and create types for Prisma. You also need to restart the development process.
If the schema is finalized, you need to create a migration file with `pnpm db:migrate <nameOfMigration>` where `nameOfMigration` is given by you. Make it sense. :)
---
## How to add new services
You can add any open-source and self-hostable software (service/application) to Coolify if the following statements are true:
- Self-hostable (obviously)
- Open-source
- Maintained (I do not want to add software full of bugs)
## Backend
There are 5 steps you should make on the backend side.
1. Create Prisma / database schema for the new service.
2. Add supported versions of the service.
3. Update global functions.
4. Create API endpoints.
5. Define automatically generated variables.
> I will use [Umami](https://umami.is/) as an example service.
### Create Prisma / Database schema for the new service.
You only need to do this if you store passwords or any persistent configuration. Mostly it is required by all services, but there are some exceptions, like NocoDB.
Update Prisma schema in [prisma/schema.prisma](prisma/schema.prisma).
- Add new model with the new service name.
- Make a relationship with `Service` model.
- In the `Service` model, the name of the new field should be with low-capital.
- If the service needs a database, define a `publicPort` field to be able to make it's database public, example field name in case of PostgreSQL: `postgresqlPublicPort`. It should be a optional field.
If you are finished with the Prisma schema, you should update the database schema with `pnpm db:push` command.
> You must restart the running development environment to be able to use the new model
> If you use VSCode/TLS, you probably need to restart the `Typescript Language Server` to get the new types loaded in the running environment.
### Add supported versions
Supported versions are hardcoded into Coolify (for now).
You need to update `supportedServiceTypesAndVersions` function at [apps/api/src/lib/services/supportedVersions.ts](apps/api/src/lib/services/supportedVersions.ts). Example JSON:
```js
{
// Name used to identify the service internally
name: 'umami',
// Fancier name to show to the user
fancyName: 'Umami',
// Docker base image for the service
baseImage: 'ghcr.io/mikecao/umami',
// Optional: If there is any dependent image, you should list it here
images: [],
// Usable tags
versions: ['postgresql-latest'],
// Which tag is the recommended
recommendedVersion: 'postgresql-latest',
// Application's default port, Umami listens on 3000
ports: {
main: 3000
}
}
```
### Add required functions/properties
1. Add the new service to the `includeServices` variable in [apps/api/src/lib/services/common.ts](apps/api/src/lib/services/common.ts), so it will be included in all places in the database queries where it is required.
```js
const include: any = {
destinationDocker: true,
persistentStorage: true,
serviceSecret: true,
minio: true,
plausibleAnalytics: true,
vscodeserver: true,
wordpress: true,
ghost: true,
meiliSearch: true,
umami: true // This line!
};
```
2. Update the database update query with the new service type to `configureServiceType` function in [apps/api/src/lib/services/common.ts](apps/api/src/lib/services/common.ts). This function defines the automatically generated variables (passwords, users, etc.) and it's encryption process (if applicable).
```js
[...]
else if (type === 'umami') {
const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword());
const postgresqlDatabase = 'umami';
const hashSalt = encrypt(generatePassword(64));
await prisma.service.update({
where: { id },
data: {
type,
umami: {
create: {
postgresqlDatabase,
postgresqlPassword,
postgresqlUser,
hashSalt,
}
}
}
});
}
```
3. Add field details to [apps/api/src/lib/services/serviceFields.ts](apps/api/src/lib/services/serviceFields.ts), so every component will know what to do with the values (decrypt/show it by default/readonly)
```js
export const umami = [{
name: 'postgresqlUser',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
}]
```
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)
> 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)
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.
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/routes/services/[id]/_ServiceLinks.svelte](apps/ui/src/routes/services/[id]/_ServiceLinks.svelte) with the link to the docs/main site of the service
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.
> For example, see other [here](apps/ui/src/routes/services/[id]/_Services/_Umami.svelte).
Good job! 👏
<!-- # 🌐 Translate the project
The project use [sveltekit-i18n](https://github.com/sveltekit-i18n/lib) to translate the project.
It follows the [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) to name languages.
### Installation
You must have gone throw all the [intro](#introduction) steps before you can start translating.
It's only an advice, but I recommend you to use:
- Visual Studio Code
- [i18n Ally for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=Lokalise.i18n-ally): ideal to see the progress of the translation.
- [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode): to get the syntax color for the project
### Adding a language
If your language doesn't appear in the [locales folder list](src/lib/locales/), follow the step below:
1. In `src/lib/locales/`, Copy paste `en.json` and rename it with your language (eg: `cz.json`).
2. In the [lang.json](src/lib/lang.json) file, add a line after the first bracket (`{`) with `"ISO of your language": "Language",` (eg: `"cz": "Czech",`).
3. Have fun translating! -->

View File

@ -1,45 +1,3 @@
---
head:
- - meta
- name: description
content: Coolify - Databases
- - meta
- name: keywords
content: databases coollabs coolify
- - meta
- name: twitter:card
content: summary_large_image
- - meta
- name: twitter:site
content: '@andrasbacsai'
- - meta
- name: twitter:title
content: Coolify
- - meta
- name: twitter:description
content: An open-source & self-hostable Heroku / Netlify alternative.
- - meta
- name: twitter:image
content: https://cdn.coollabs.io/assets/coollabs/og-image-databases.png
- - meta
- property: og:type
content: website
- - meta
- property: og:url
content: https://coolify.io
- - meta
- property: og:title
content: Coolify
- - meta
- property: og:description
content: An open-source & self-hostable Heroku / Netlify alternative.
- - meta
- property: og:site_name
content: Coolify
- - meta
- property: og:image
content: https://cdn.coollabs.io/assets/coollabs/og-image-databases.png
---
# Contribution # Contribution
First, thanks for considering to contribute to my project. It really means a lot! :) First, thanks for considering to contribute to my project. It really means a lot! :)
@ -100,9 +58,58 @@ All data that needs to be persist for a service should be saved to the database
very password/api key/passphrase needs to be encrypted. If you are not sure, whether it should be encrypted or not, just encrypt it. very password/api key/passphrase needs to be encrypted. If you are not sure, whether it should be encrypted or not, just encrypt it.
Update Prisma schema in [src/api/prisma/schema.prisma](https://github.com/coollabsio/coolify/blob/main/apps/api/prisma/schema.prisma). Update Prisma schema in [src/apps/api/prisma/schema.prisma](https://github.com/coollabsio/coolify/blob/main/apps/api/prisma/schema.prisma).
- Add new model with the new service name. - Add new model with the new service name.
- Make a relationship with `Service` model. - Make a relationship with `Service` model.
- In the `Service` model, the name of the new field should be with low-capital. - In the `Service` model, the name of the new field should be with low-capital.
- If the service needs a database, define a `publicPort` field to be able to make it's database public, example field name in case of PostgreSQL: `postgresqlPublicPort`. It should be a optional field. - If the service needs a database, define a `publicPort` field to be able to make it's database public, example field name in case of PostgreSQL: `postgresqlPublicPort`. It should be a optional field.
Once done, create Prisma schema with `pnpm db:push`.
> You may also need to restart `Typescript Language Server` in your IDE to get the new types.
### Add available versions
Versions are hardcoded into Coolify at the moment and based on Docker image tags.
- Update `supportedServiceTypesAndVersions` function [here](apps/api/src/lib/services/supportedVersions.ts)
### Include the new service in queries
At [here](apps/api/src/lib/services/common.ts) in `includeServices` function add the new table name, so it will be included in all places in the database queries where it is required.
### Define auto-generated fields
At [here](apps/api/src/lib/services/common.ts) in `configureServiceType` function add the initial auto-generated details such as password, users etc, and the encryption process of secrets (if applicable).
### Define input field details
At [here](apps/api/src/lib/services/serviceFields.ts) add details about the input fields shown in the UI, so every component (API/UI) will know what to do with the values (decrypt/show it by default/readonly/etc).
### Define the start process
- At [here](apps/api/src/lib/services/handlers.ts), define how the service should start. It could be complex and based on `docker-compose` definitions.
> See `startUmamiService()` function as example.
- At [here](apps/api/src/routes/api/v1/services/handlers.ts), add the new start service process to `startService` function.
### Define the deletion process
[Here](apps/api/src/lib/services/common.ts) in `removeService` add the database deletion process.
### Custom logo
- At [here](apps/ui/src/lib/components/svg/services) add the service custom log as a Svelte component and export it [here](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.
- At [here](apps/ui/src/lib/components/svg/services/ServiceIcons.svelte) include the new logo with `isAbsolute` property.
- At [here](apps/ui/src/routes/services/[id]/_ServiceLinks.svelte) add links to the documentation of the service.
### Custom fields on the UI
By default the URL and name are shown on the UI. Everything else needs to be added [here](apps/ui/src/routes/services/[id]/_Services/_Services.svelte)
> If you need to show more details on the frontend, such as users/passwords, you need to add Svelte component [here](apps/ui/src/routes/services/[id]/_Services) with an underscore. For example, see other [here](apps/ui/src/routes/services/[id]/_Services/_Umami.svelte).
Good job! 👏

View File

@ -33,6 +33,7 @@ RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker
RUN (curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.27.0/pack-v0.27.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack) RUN (curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.27.0/pack-v0.27.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack)
COPY --from=build /app/apps/api/build/ . COPY --from=build /app/apps/api/build/ .
COPY --from=build /app/others/fluentbit/ ./fluentbit
COPY --from=build /app/apps/ui/build/ ./public COPY --from=build /app/apps/ui/build/ ./public
COPY --from=build /app/apps/api/prisma/ ./prisma COPY --from=build /app/apps/api/prisma/ ./prisma
COPY --from=build /app/apps/api/package.json . COPY --from=build /app/apps/api/package.json .

View File

@ -29,6 +29,8 @@
"bree": "9.1.2", "bree": "9.1.2",
"cabin": "9.1.2", "cabin": "9.1.2",
"compare-versions": "5.0.1", "compare-versions": "5.0.1",
"csv-parse": "^5.3.0",
"csvtojson": "^2.0.10",
"cuid": "2.1.8", "cuid": "2.1.8",
"dayjs": "1.11.5", "dayjs": "1.11.5",
"dockerode": "3.3.4", "dockerode": "3.3.4",

View File

@ -0,0 +1,18 @@
-- AlterTable
ALTER TABLE "Build" ADD COLUMN "previewApplicationId" TEXT;
-- CreateTable
CREATE TABLE "PreviewApplication" (
"id" TEXT NOT NULL PRIMARY KEY,
"pullmergeRequestId" TEXT NOT NULL,
"sourceBranch" TEXT NOT NULL,
"isRandomDomain" BOOLEAN NOT NULL DEFAULT false,
"customDomain" TEXT,
"applicationId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "PreviewApplication_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "PreviewApplication_applicationId_key" ON "PreviewApplication"("applicationId");

View File

@ -119,6 +119,19 @@ model Application {
secrets Secret[] secrets Secret[]
teams Team[] teams Team[]
connectedDatabase ApplicationConnectedDatabase? connectedDatabase ApplicationConnectedDatabase?
previewApplication PreviewApplication[]
}
model PreviewApplication {
id String @id @default(cuid())
pullmergeRequestId String
sourceBranch String
isRandomDomain Boolean @default(false)
customDomain String?
applicationId String @unique
application Application @relation(fields: [applicationId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
} }
model ApplicationConnectedDatabase { model ApplicationConnectedDatabase {
@ -219,6 +232,7 @@ model Build {
gitlabAppId String? gitlabAppId String?
commit String? commit String?
pullmergeRequestId String? pullmergeRequestId String?
previewApplicationId String?
forceRebuild Boolean @default(false) forceRebuild Boolean @default(false)
sourceBranch String? sourceBranch String?
branch String? branch String?

View File

@ -38,8 +38,16 @@ import * as buildpacks from '../lib/buildPacks';
for (const queueBuild of queuedBuilds) { for (const queueBuild of queuedBuilds) {
actions.push(async () => { actions.push(async () => {
let application = await prisma.application.findUnique({ where: { id: queueBuild.applicationId }, include: { destinationDocker: true, gitSource: { include: { githubApp: true, gitlabApp: true } }, persistentStorage: true, secrets: true, settings: true, teams: true } }) let application = await prisma.application.findUnique({ where: { id: queueBuild.applicationId }, include: { destinationDocker: true, gitSource: { include: { githubApp: true, gitlabApp: true } }, persistentStorage: true, secrets: true, settings: true, teams: true } })
let { id: buildId, type, sourceBranch = null, pullmergeRequestId = null, forceRebuild } = queueBuild let { id: buildId, type, sourceBranch = null, pullmergeRequestId = null, previewApplicationId = null, forceRebuild } = queueBuild
application = decryptApplication(application) application = decryptApplication(application)
const originalApplicationId = application.id
if (pullmergeRequestId) {
const previewApplications = await prisma.previewApplication.findMany({ where: { applicationId: originalApplicationId, pullmergeRequestId } })
if (previewApplications.length > 0) {
previewApplicationId = previewApplications[0].id
}
}
const usableApplicationId = previewApplicationId || originalApplicationId
try { try {
if (queueBuild.status === 'running') { if (queueBuild.status === 'running') {
await saveBuildLog({ line: 'Building halted, restarting...', buildId, applicationId: application.id }); await saveBuildLog({ line: 'Building halted, restarting...', buildId, applicationId: application.id });
@ -104,17 +112,17 @@ import * as buildpacks from '../lib/buildPacks';
) )
.digest('hex'); .digest('hex');
const { debug } = settings; const { debug } = settings;
if (concurrency === 1) { // if (concurrency === 1) {
await prisma.build.updateMany({ // await prisma.build.updateMany({
where: { // where: {
status: { in: ['queued', 'running'] }, // status: { in: ['queued', 'running'] },
id: { not: buildId }, // id: { not: buildId },
applicationId, // applicationId,
createdAt: { lt: new Date(new Date().getTime() - 10 * 1000) } // createdAt: { lt: new Date(new Date().getTime() - 10 * 1000) }
}, // },
data: { status: 'failed' } // data: { status: 'failed' }
}); // });
} // }
let imageId = applicationId; let imageId = applicationId;
let domain = getDomain(fqdn); let domain = getDomain(fqdn);
const volumes = const volumes =
@ -261,7 +269,10 @@ import * as buildpacks from '../lib/buildPacks';
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (pullmergeRequestId) { if (pullmergeRequestId) {
if (secret.isPRMRSecret) { const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
envs.push(`${secret.name}=${isSecretFound[0].value}`);
} else {
envs.push(`${secret.name}=${secret.value}`); envs.push(`${secret.name}=${secret.value}`);
} }
} else { } else {
@ -335,10 +346,15 @@ import * as buildpacks from '../lib/buildPacks';
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId }); await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
} catch (error) { } catch (error) {
await saveBuildLog({ line: error, buildId, applicationId }); await saveBuildLog({ line: error, buildId, applicationId });
await prisma.build.updateMany({ const foundBuild = await prisma.build.findUnique({ where: { id: buildId } })
where: { id: buildId, status: { in: ['queued', 'running'] } }, if (foundBuild) {
data: { status: 'failed' } await prisma.build.update({
where: { id: buildId },
data: {
status: 'failed'
}
}); });
}
throw new Error(error); throw new Error(error);
} }
await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId }); await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
@ -350,12 +366,19 @@ import * as buildpacks from '../lib/buildPacks';
} }
} }
catch (error) { catch (error) {
await prisma.build.updateMany({ const foundBuild = await prisma.build.findUnique({ where: { id: buildId } })
where: { id: buildId, status: { in: ['queued', 'running'] } }, if (foundBuild) {
data: { status: 'failed' } await prisma.build.update({
where: { id: buildId },
data: {
status: 'failed'
}
}); });
}
if (error !== 1) {
await saveBuildLog({ line: error, buildId, applicationId: application.id }); await saveBuildLog({ line: error, buildId, applicationId: application.id });
} }
}
}); });
} }
await pAll.default(actions, { concurrency }) await pAll.default(actions, { concurrency })

View File

@ -29,7 +29,7 @@ async function autoUpdater() {
`sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env` `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .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 coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"`
); );
} }
} else { } else {

View File

@ -1,4 +1,4 @@
import { base64Encode, executeDockerCmd, generateTimestamp, getDomain, isDev, prisma, version } from "../common"; import { base64Encode, encrypt, executeDockerCmd, generateTimestamp, getDomain, isDev, prisma, version } from "../common";
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { day } from "../dayjs"; import { day } from "../dayjs";
@ -461,17 +461,32 @@ export const saveBuildLog = async ({
buildId: string; buildId: string;
applicationId: string; applicationId: string;
}): Promise<any> => { }): Promise<any> => {
const { default: got } = await import('got')
if (line && typeof line === 'string' && line.includes('ghs_')) { if (line && typeof line === 'string' && line.includes('ghs_')) {
const regex = /ghs_.*@/g; const regex = /ghs_.*@/g;
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@'); line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
} }
const addTimestamp = `[${generateTimestamp()}] ${line}`; const addTimestamp = `[${generateTimestamp()}] ${line}`;
if (isDev) console.debug(`[${applicationId}] ${addTimestamp}`); const fluentBitUrl = isDev ? 'http://localhost:24224' : 'http://coolify-fluentbit:24224';
if (isDev) {
console.debug(`[${applicationId}] ${addTimestamp}`);
}
try {
return await got.post(`${fluentBitUrl}/${applicationId}_buildlog_${buildId}.csv`, {
json: {
line: encrypt(line)
}
})
} catch(error) {
return await prisma.buildLog.create({ return await prisma.buildLog.create({
data: { data: {
line: addTimestamp, buildId, time: Number(day().valueOf()), applicationId line: addTimestamp, buildId, time: Number(day().valueOf()), applicationId
} }
}); });
}
}; };
export async function copyBaseConfigurationFiles( export async function copyBaseConfigurationFiles(
@ -556,7 +571,6 @@ export function checkPnpm(installCommand = null, buildCommand = null, startComma
); );
} }
export async function buildImage({ export async function buildImage({
applicationId, applicationId,
tag, tag,
@ -677,8 +691,6 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
secrets, secrets,
pullmergeRequestId pullmergeRequestId
} = data; } = data;
const isPnpm = checkPnpm(installCommand, buildCommand); const isPnpm = checkPnpm(installCommand, buildCommand);
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild}`); Dockerfile.push(`FROM ${imageForBuild}`);
@ -688,7 +700,10 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (secret.isBuildSecret) { if (secret.isBuildSecret) {
if (pullmergeRequestId) { if (pullmergeRequestId) {
if (secret.isPRMRSecret) { const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
} else {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`); Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
} }
} else { } else {
@ -722,7 +737,10 @@ export async function buildCacheImageForLaravel(data, imageForBuild) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (secret.isBuildSecret) { if (secret.isBuildSecret) {
if (pullmergeRequestId) { if (pullmergeRequestId) {
if (secret.isPRMRSecret) { const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
} else {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`); Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
} }
} else { } else {

View File

@ -27,7 +27,10 @@ const createDockerfile = async (data, image): Promise<void> => {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (secret.isBuildSecret) { if (secret.isBuildSecret) {
if (pullmergeRequestId) { if (pullmergeRequestId) {
if (secret.isPRMRSecret) { const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
} else {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`); Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
} }
} else { } else {

View File

@ -28,6 +28,7 @@ export default async function (data) {
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (secret.isBuildSecret) { if (secret.isBuildSecret) {
// TODO: fix secrets
if ( if (
(pullmergeRequestId && secret.isPRMRSecret) || (pullmergeRequestId && secret.isPRMRSecret) ||
(!pullmergeRequestId && !secret.isPRMRSecret) (!pullmergeRequestId && !secret.isPRMRSecret)

View File

@ -27,7 +27,10 @@ const createDockerfile = async (data, image): Promise<void> => {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (secret.isBuildSecret) { if (secret.isBuildSecret) {
if (pullmergeRequestId) { if (pullmergeRequestId) {
if (secret.isPRMRSecret) { const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
} else {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`); Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
} }
} else { } else {

View File

@ -23,7 +23,10 @@ const createDockerfile = async (data, image): Promise<void> => {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (secret.isBuildSecret) { if (secret.isBuildSecret) {
if (pullmergeRequestId) { if (pullmergeRequestId) {
if (secret.isPRMRSecret) { const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
} else {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`); Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
} }
} else { } else {

View File

@ -27,7 +27,10 @@ const createDockerfile = async (data, image): Promise<void> => {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (secret.isBuildSecret) { if (secret.isBuildSecret) {
if (pullmergeRequestId) { if (pullmergeRequestId) {
if (secret.isPRMRSecret) { const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
} else {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`); Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
} }
} else { } else {

View File

@ -16,7 +16,10 @@ const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (secret.isBuildSecret) { if (secret.isBuildSecret) {
if (pullmergeRequestId) { if (pullmergeRequestId) {
if (secret.isPRMRSecret) { const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
} else {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`); Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
} }
} else { } else {

View File

@ -21,7 +21,10 @@ const createDockerfile = async (data, image): Promise<void> => {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (secret.isBuildSecret) { if (secret.isBuildSecret) {
if (pullmergeRequestId) { if (pullmergeRequestId) {
if (secret.isPRMRSecret) { const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
} else {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`); Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
} }
} else { } else {

View File

@ -24,7 +24,10 @@ const createDockerfile = async (data, image): Promise<void> => {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (secret.isBuildSecret) { if (secret.isBuildSecret) {
if (pullmergeRequestId) { if (pullmergeRequestId) {
if (secret.isPRMRSecret) { const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
} else {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`); Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
} }
} else { } else {

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@ export function formatLabelsOnDocker(data) {
return container return container
}) })
} }
export async function checkContainer({ dockerId, container, remove = false }: { dockerId: string, container: string, remove?: boolean }): Promise<boolean> { export async function checkContainer({ dockerId, container, remove = false }: { dockerId: string, container: string, remove?: boolean }): Promise<{ found: boolean, status?: { isExited: boolean, isRunning: boolean, isRestarting: boolean } }> {
let containerFound = false; let containerFound = false;
try { try {
const { stdout } = await executeDockerCmd({ const { stdout } = await executeDockerCmd({
@ -21,10 +21,12 @@ export async function checkContainer({ dockerId, container, remove = false }: {
command: command:
`docker inspect --format '{{json .State}}' ${container}` `docker inspect --format '{{json .State}}' ${container}`
}); });
containerFound = true
const parsedStdout = JSON.parse(stdout); const parsedStdout = JSON.parse(stdout);
const status = parsedStdout.Status; const status = parsedStdout.Status;
const isRunning = status === 'running'; const isRunning = status === 'running';
const isRestarting = status === 'restarting'
const isExited = status === 'exited'
if (status === 'created') { if (status === 'created') {
await executeDockerCmd({ await executeDockerCmd({
dockerId, dockerId,
@ -39,13 +41,23 @@ export async function checkContainer({ dockerId, container, remove = false }: {
`docker rm ${container}` `docker rm ${container}`
}); });
} }
if (isRunning) {
containerFound = true; return {
found: containerFound,
status: {
isRunning,
isRestarting,
isExited
} }
};
} catch (err) { } catch (err) {
// Container not found // Container not found
} }
return containerFound; return {
found: false
};
} }
export async function isContainerExited(dockerId: string, containerName: string): Promise<boolean> { export async function isContainerExited(dockerId: string, containerName: string): Promise<boolean> {

View File

@ -10,7 +10,8 @@ export default async function ({
branch, branch,
buildId, buildId,
privateSshKey, privateSshKey,
customPort customPort,
forPublic
}: { }: {
applicationId: string; applicationId: string;
workdir: string; workdir: string;
@ -21,11 +22,15 @@ export default async function ({
repodir: string; repodir: string;
privateSshKey: string; privateSshKey: string;
customPort: number; customPort: number;
forPublic: boolean;
}): Promise<string> { }): Promise<string> {
const url = htmlUrl.replace('https://', '').replace('http://', '').replace(/\/$/, ''); const url = htmlUrl.replace('https://', '').replace('http://', '').replace(/\/$/, '');
await saveBuildLog({ line: 'GitLab importer started.', buildId, applicationId }); await saveBuildLog({ line: 'GitLab importer started.', buildId, applicationId });
if (!forPublic) {
await asyncExecShell(`echo '${privateSshKey}' > ${repodir}/id.rsa`); await asyncExecShell(`echo '${privateSshKey}' > ${repodir}/id.rsa`);
await asyncExecShell(`chmod 600 ${repodir}/id.rsa`); await asyncExecShell(`chmod 600 ${repodir}/id.rsa`);
}
await saveBuildLog({ await saveBuildLog({
line: `Cloning ${repository}:${branch} branch.`, line: `Cloning ${repository}:${branch} branch.`,
@ -33,9 +38,16 @@ export default async function ({
applicationId applicationId
}); });
if (forPublic) {
await asyncExecShell(
`git clone -q -b ${branch} https://${url}/${repository}.git ${workdir}/ && cd ${workdir}/ && git submodule update --init --recursive && git lfs pull && cd .. `
);
} else {
await asyncExecShell( await asyncExecShell(
`git clone -q -b ${branch} git@${url}:${repository}.git --config core.sshCommand="ssh -p ${customPort} -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git submodule update --init --recursive && git lfs pull && cd .. ` `git clone -q -b ${branch} git@${url}:${repository}.git --config core.sshCommand="ssh -p ${customPort} -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git submodule update --init --recursive && git lfs pull && cd .. `
); );
}
const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`); const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`);
return commit.replace('\n', ''); return commit.replace('\n', '');
} }

View File

@ -5,6 +5,7 @@ import bcrypt from 'bcryptjs';
import { ServiceStartStop } from '../../routes/api/v1/services/types'; import { ServiceStartStop } from '../../routes/api/v1/services/types';
import { asyncSleep, ComposeFile, createDirectories, defaultComposeConfiguration, errorHandler, executeDockerCmd, getDomain, getFreePublicPort, getServiceFromDB, getServiceImage, getServiceMainPort, isARM, isDev, makeLabelForServices, persistentVolumes, prisma } from '../common'; import { asyncSleep, ComposeFile, createDirectories, defaultComposeConfiguration, errorHandler, executeDockerCmd, getDomain, getFreePublicPort, getServiceFromDB, getServiceImage, getServiceMainPort, isARM, isDev, makeLabelForServices, persistentVolumes, prisma } from '../common';
import { defaultServiceConfigurations } from '../services'; import { defaultServiceConfigurations } from '../services';
import { OnlyId } from '../../types';
export async function startService(request: FastifyRequest<ServiceStartStop>) { export async function startService(request: FastifyRequest<ServiceStartStop>) {
try { try {
@ -317,7 +318,7 @@ async function startMinioService(request: FastifyRequest<ServiceStartStop>) {
destinationDocker, destinationDocker,
persistentStorage, persistentStorage,
exposePort, exposePort,
minio: { rootUser, rootUserPassword }, minio: { rootUser, rootUserPassword, apiFqdn },
serviceSecret serviceSecret
} = service; } = service;
@ -336,7 +337,7 @@ async function startMinioService(request: FastifyRequest<ServiceStartStop>) {
image: `${image}:${version}`, image: `${image}:${version}`,
volumes: [`${id}-minio-data:/data`], volumes: [`${id}-minio-data:/data`],
environmentVariables: { environmentVariables: {
MINIO_SERVER_URL: fqdn, MINIO_SERVER_URL: apiFqdn,
MINIO_DOMAIN: getDomain(fqdn), MINIO_DOMAIN: getDomain(fqdn),
MINIO_ROOT_USER: rootUser, MINIO_ROOT_USER: rootUser,
MINIO_ROOT_PASSWORD: rootUserPassword, MINIO_ROOT_PASSWORD: rootUserPassword,
@ -658,7 +659,7 @@ async function startLanguageToolService(request: FastifyRequest<ServiceStartStop
image: config.languagetool.image, image: config.languagetool.image,
environment: config.languagetool.environmentVariables, environment: config.languagetool.environmentVariables,
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
volumes: config.languagetool, volumes: config.languagetool.volumes,
labels: makeLabelForServices('languagetool'), labels: makeLabelForServices('languagetool'),
...defaultComposeConfiguration(network), ...defaultComposeConfiguration(network),
} }
@ -713,7 +714,7 @@ async function startN8nService(request: FastifyRequest<ServiceStartStop>) {
[id]: { [id]: {
container_name: id, container_name: id,
image: config.n8n.image, image: config.n8n.image,
volumes: config.n8n, volumes: config.n8n.volumes,
environment: config.n8n.environmentVariables, environment: config.n8n.environmentVariables,
labels: makeLabelForServices('n8n'), labels: makeLabelForServices('n8n'),
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
@ -1009,78 +1010,135 @@ async function startUmamiService(request: FastifyRequest<ServiceStartStop>) {
} }
const initDbSQL = ` const initDbSQL = `
drop table if exists event; -- CreateTable
drop table if exists pageview; CREATE TABLE "account" (
drop table if exists session; "user_id" SERIAL NOT NULL,
drop table if exists website; "username" VARCHAR(255) NOT NULL,
drop table if exists account; "password" VARCHAR(60) NOT NULL,
"is_admin" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
create table account ( PRIMARY KEY ("user_id")
user_id serial primary key, );
username varchar(255) unique not null,
password varchar(60) not null,
is_admin bool not null default false,
created_at timestamp with time zone default current_timestamp,
updated_at timestamp with time zone default current_timestamp
);
create table website ( -- CreateTable
website_id serial primary key, CREATE TABLE "event" (
website_uuid uuid unique not null, "event_id" SERIAL NOT NULL,
user_id int not null references account(user_id) on delete cascade, "website_id" INTEGER NOT NULL,
name varchar(100) not null, "session_id" INTEGER NOT NULL,
domain varchar(500), "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
share_id varchar(64) unique, "url" VARCHAR(500) NOT NULL,
created_at timestamp with time zone default current_timestamp "event_type" VARCHAR(50) NOT NULL,
); "event_value" VARCHAR(50) NOT NULL,
create table session ( PRIMARY KEY ("event_id")
session_id serial primary key, );
session_uuid uuid unique not null,
website_id int not null references website(website_id) on delete cascade,
created_at timestamp with time zone default current_timestamp,
hostname varchar(100),
browser varchar(20),
os varchar(20),
device varchar(20),
screen varchar(11),
language varchar(35),
country char(2)
);
create table pageview ( -- CreateTable
view_id serial primary key, CREATE TABLE "pageview" (
website_id int not null references website(website_id) on delete cascade, "view_id" SERIAL NOT NULL,
session_id int not null references session(session_id) on delete cascade, "website_id" INTEGER NOT NULL,
created_at timestamp with time zone default current_timestamp, "session_id" INTEGER NOT NULL,
url varchar(500) not null, "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
referrer varchar(500) "url" VARCHAR(500) NOT NULL,
); "referrer" VARCHAR(500),
create table event ( PRIMARY KEY ("view_id")
event_id serial primary key, );
website_id int not null references website(website_id) on delete cascade,
session_id int not null references session(session_id) on delete cascade,
created_at timestamp with time zone default current_timestamp,
url varchar(500) not null,
event_type varchar(50) not null,
event_value varchar(50) not null
);
create index website_user_id_idx on website(user_id); -- CreateTable
CREATE TABLE "session" (
"session_id" SERIAL NOT NULL,
"session_uuid" UUID NOT NULL,
"website_id" INTEGER NOT NULL,
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
"hostname" VARCHAR(100),
"browser" VARCHAR(20),
"os" VARCHAR(20),
"device" VARCHAR(20),
"screen" VARCHAR(11),
"language" VARCHAR(35),
"country" CHAR(2),
create index session_created_at_idx on session(created_at); PRIMARY KEY ("session_id")
create index session_website_id_idx on session(website_id); );
create index pageview_created_at_idx on pageview(created_at); -- CreateTable
create index pageview_website_id_idx on pageview(website_id); CREATE TABLE "website" (
create index pageview_session_id_idx on pageview(session_id); "website_id" SERIAL NOT NULL,
create index pageview_website_id_created_at_idx on pageview(website_id, created_at); "website_uuid" UUID NOT NULL,
create index pageview_website_id_session_id_created_at_idx on pageview(website_id, session_id, created_at); "user_id" INTEGER NOT NULL,
"name" VARCHAR(100) NOT NULL,
"domain" VARCHAR(500),
"share_id" VARCHAR(64),
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
create index event_created_at_idx on event(created_at); PRIMARY KEY ("website_id")
create index event_website_id_idx on event(website_id); );
create index event_session_id_idx on event(session_id);
-- CreateIndex
CREATE UNIQUE INDEX "account.username_unique" ON "account"("username");
-- CreateIndex
CREATE INDEX "event_created_at_idx" ON "event"("created_at");
-- CreateIndex
CREATE INDEX "event_session_id_idx" ON "event"("session_id");
-- CreateIndex
CREATE INDEX "event_website_id_idx" ON "event"("website_id");
-- CreateIndex
CREATE INDEX "pageview_created_at_idx" ON "pageview"("created_at");
-- CreateIndex
CREATE INDEX "pageview_session_id_idx" ON "pageview"("session_id");
-- CreateIndex
CREATE INDEX "pageview_website_id_created_at_idx" ON "pageview"("website_id", "created_at");
-- CreateIndex
CREATE INDEX "pageview_website_id_idx" ON "pageview"("website_id");
-- CreateIndex
CREATE INDEX "pageview_website_id_session_id_created_at_idx" ON "pageview"("website_id", "session_id", "created_at");
-- CreateIndex
CREATE UNIQUE INDEX "session.session_uuid_unique" ON "session"("session_uuid");
-- CreateIndex
CREATE INDEX "session_created_at_idx" ON "session"("created_at");
-- CreateIndex
CREATE INDEX "session_website_id_idx" ON "session"("website_id");
-- CreateIndex
CREATE UNIQUE INDEX "website.website_uuid_unique" ON "website"("website_uuid");
-- CreateIndex
CREATE UNIQUE INDEX "website.share_id_unique" ON "website"("share_id");
-- CreateIndex
CREATE INDEX "website_user_id_idx" ON "website"("user_id");
-- AddForeignKey
ALTER TABLE "event" ADD FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "event" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "pageview" ADD FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "pageview" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "session" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "website" ADD FOREIGN KEY ("user_id") REFERENCES "account"("user_id") ON DELETE CASCADE ON UPDATE CASCADE;
insert into account (username, password, is_admin) values ('admin', '${bcrypt.hashSync( insert into account (username, password, is_admin) values ('admin', '${bcrypt.hashSync(
umamiAdminPassword, umamiAdminPassword,
@ -1119,7 +1177,6 @@ async function startUmamiService(request: FastifyRequest<ServiceStartStop>) {
}, },
volumes: volumeMounts volumes: volumeMounts
}; };
console.log(composeFile)
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await startServiceContainers(destinationDocker.id, composeFileDestination) await startServiceContainers(destinationDocker.id, composeFileDestination)
@ -1321,10 +1378,6 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
const teamId = request.user.teamId; const teamId = request.user.teamId;
const { version, fqdn, destinationDocker, secrets, exposePort, network, port, workdir, image, appwrite } = await defaultServiceConfigurations({ id, teamId }) const { version, fqdn, destinationDocker, secrets, exposePort, network, port, workdir, image, appwrite } = await defaultServiceConfigurations({ id, teamId })
let isStatsEnabled = false
if (secrets.find(s => s === '_APP_USAGE_STATS=enabled')) {
isStatsEnabled = true
}
const { const {
opensslKeyV1, opensslKeyV1,
executorSecret, executorSecret,
@ -1702,7 +1755,6 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
}, },
}; };
if (isStatsEnabled) {
dockerCompose[id].depends_on.push(`${id}-influxdb`); dockerCompose[id].depends_on.push(`${id}-influxdb`);
dockerCompose[`${id}-usage`] = { dockerCompose[`${id}-usage`] = {
image: `${image}:${version}`, image: `${image}:${version}`,
@ -1746,7 +1798,6 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
], ],
...defaultComposeConfiguration(network), ...defaultComposeConfiguration(network),
} }
}
const composeFile: any = { const composeFile: any = {
version: '3.8', version: '3.8',
@ -2647,3 +2698,24 @@ async function startGrafanaService(request: FastifyRequest<ServiceStartStop>) {
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function migrateAppwriteDB(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
try {
const { id } = request.params
const teamId = request.user.teamId;
const {
destinationDockerId,
destinationDocker,
} = await getServiceFromDB({ id, teamId });
if (destinationDockerId) {
await executeDockerCmd({
dockerId: destinationDocker.id,
command: `docker exec ${id} migrate`
})
return await reply.code(201).send()
}
throw { status: 500, message: 'Could cleanup logs.' }
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}

View File

@ -1,3 +1,24 @@
/*
Example of a supported version:
{
// Name used to identify the service internally
name: 'umami',
// Fancier name to show to the user
fancyName: 'Umami',
// Docker base image for the service
baseImage: 'ghcr.io/mikecao/umami',
// Optional: If there is any dependent image, you should list it here
images: [],
// Usable tags
versions: ['postgresql-latest'],
// Which tag is the recommended
recommendedVersion: 'postgresql-latest',
// Application's default port, Umami listens on 3000
ports: {
main: 3000
}
}
*/
export const supportedServiceTypesAndVersions = [ export const supportedServiceTypesAndVersions = [
{ {
name: 'plausibleanalytics', name: 'plausibleanalytics',
@ -116,7 +137,7 @@ export const supportedServiceTypesAndVersions = [
{ {
name: 'umami', name: 'umami',
fancyName: 'Umami', fancyName: 'Umami',
baseImage: 'ghcr.io/mikecao/umami', baseImage: 'ghcr.io/umami-software/umami',
images: ['postgres:12-alpine'], images: ['postgres:12-alpine'],
versions: ['postgresql-latest'], versions: ['postgresql-latest'],
recommendedVersion: 'postgresql-latest', recommendedVersion: 'postgresql-latest',
@ -151,8 +172,8 @@ export const supportedServiceTypesAndVersions = [
fancyName: 'Appwrite', fancyName: 'Appwrite',
baseImage: 'appwrite/appwrite', baseImage: 'appwrite/appwrite',
images: ['mariadb:10.7', 'redis:6.2-alpine', 'appwrite/telegraf:1.4.0'], images: ['mariadb:10.7', 'redis:6.2-alpine', 'appwrite/telegraf:1.4.0'],
versions: ['latest', '0.15.3'], versions: ['latest', '1.0','0.15.3'],
recommendedVersion: '0.15.3', recommendedVersion: '1.0',
ports: { ports: {
main: 80 main: 80
} }

View File

@ -5,6 +5,7 @@ import axios from 'axios';
import { FastifyReply } from 'fastify'; import { FastifyReply } from 'fastify';
import fs from 'fs/promises'; import fs from 'fs/promises';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import csv from 'csvtojson';
import { day } from '../../../../lib/dayjs'; import { day } from '../../../../lib/dayjs';
import { makeLabelForStandaloneApplication, setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common'; import { makeLabelForStandaloneApplication, setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common';
@ -12,8 +13,9 @@ import { checkDomainsIsValidInDNS, checkDoubleBranch, checkExposedPort, createDi
import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker'; import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker';
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, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication, RestartPreviewApplication, GetBuilds } from './types';
import { OnlyId } from '../../../../types'; import { OnlyId } from '../../../../types';
import path from 'node:path';
function filterObject(obj, callback) { function filterObject(obj, callback) {
return Object.fromEntries(Object.entries(obj). return Object.fromEntries(Object.entries(obj).
@ -74,14 +76,19 @@ export async function getApplicationStatus(request: FastifyRequest<OnlyId>) {
const { teamId } = request.user const { teamId } = request.user
let isRunning = false; let isRunning = false;
let isExited = false; let isExited = false;
let isRestarting = false;
const application: any = await getApplicationFromDB(id, teamId); const application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId) { if (application?.destinationDockerId) {
isRunning = await checkContainer({ dockerId: application.destinationDocker.id, container: id }); const status = await checkContainer({ dockerId: application.destinationDocker.id, container: id });
isExited = await isContainerExited(application.destinationDocker.id, id); if (status?.found) {
isRunning = status.status.isRunning;
isExited = status.status.isExited;
isRestarting = status.status.isRestarting
}
} }
return { return {
isRunning, isRunning,
isRestarting,
isExited, isExited,
}; };
} catch ({ status, message }) { } catch ({ status, message }) {
@ -157,7 +164,8 @@ export async function getApplicationFromDB(id: string, teamId: string) {
gitSource: { include: { githubApp: true, gitlabApp: true } }, gitSource: { include: { githubApp: true, gitlabApp: true } },
secrets: true, secrets: true,
persistentStorage: true, persistentStorage: true,
connectedDatabase: true connectedDatabase: true,
previewApplication: true
} }
}); });
if (!application) { if (!application) {
@ -339,10 +347,11 @@ export async function stopPreviewApplication(request: FastifyRequest<StopPreview
if (application?.destinationDockerId) { if (application?.destinationDockerId) {
const container = `${id}-${pullmergeRequestId}` const container = `${id}-${pullmergeRequestId}`
const { id: dockerId } = application.destinationDocker; const { id: dockerId } = application.destinationDocker;
const found = await checkContainer({ dockerId, container }); const { found } = await checkContainer({ dockerId, container });
if (found) { if (found) {
await removeContainer({ id: container, dockerId: application.destinationDocker.id }); await removeContainer({ id: container, dockerId: application.destinationDocker.id });
} }
await prisma.previewApplication.deleteMany({ where: { applicationId: application.id, pullmergeRequestId } })
} }
return reply.code(201).send(); return reply.code(201).send();
} catch ({ status, message }) { } catch ({ status, message }) {
@ -366,7 +375,10 @@ export async function restartApplication(request: FastifyRequest<OnlyId>, reply:
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (pullmergeRequestId) { if (pullmergeRequestId) {
if (secret.isPRMRSecret) { const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
envs.push(`${secret.name}=${isSecretFound[0].value}`);
} else {
envs.push(`${secret.name}=${secret.value}`); envs.push(`${secret.name}=${secret.value}`);
} }
} else { } else {
@ -463,7 +475,7 @@ export async function stopApplication(request: FastifyRequest<OnlyId>, reply: Fa
const application: any = await getApplicationFromDB(id, teamId); const application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId) { if (application?.destinationDockerId) {
const { id: dockerId } = application.destinationDocker; const { id: dockerId } = application.destinationDocker;
const found = await checkContainer({ dockerId, container: id }); const { found } = await checkContainer({ dockerId, container: id });
if (found) { if (found) {
await removeContainer({ id, dockerId: application.destinationDocker.id }); await removeContainer({ id, dockerId: application.destinationDocker.id });
} }
@ -607,7 +619,7 @@ export async function deployApplication(request: FastifyRequest<DeployApplicatio
githubAppId: application.gitSource?.githubApp?.id, githubAppId: application.gitSource?.githubApp?.id,
gitlabAppId: application.gitSource?.gitlabApp?.id, gitlabAppId: application.gitSource?.gitlabApp?.id,
status: 'queued', status: 'queued',
type: 'manual' type: pullmergeRequestId ? application.gitSource?.githubApp?.id ? 'manual_pr' : 'manual_mr' : 'manual'
} }
}); });
return { return {
@ -798,7 +810,6 @@ export async function saveSecret(request: FastifyRequest<SaveSecret>, reply: Fas
try { try {
const { id } = request.params const { id } = request.params
let { name, value, isBuildSecret, isPRMRSecret, isNew } = request.body let { name, value, isBuildSecret, isPRMRSecret, isNew } = request.body
if (isNew) { if (isNew) {
const found = await prisma.secret.findFirst({ where: { name, applicationId: id, isPRMRSecret } }); const found = await prisma.secret.findFirst({ where: { name, applicationId: id, isPRMRSecret } });
if (found) { if (found) {
@ -810,14 +821,24 @@ export async function saveSecret(request: FastifyRequest<SaveSecret>, reply: Fas
}); });
} }
} else { } else {
if (value) {
value = encrypt(value.trim()); value = encrypt(value.trim());
}
const found = await prisma.secret.findFirst({ where: { applicationId: id, name, isPRMRSecret } }); const found = await prisma.secret.findFirst({ where: { applicationId: id, name, isPRMRSecret } });
if (found) { if (found) {
if (!value && isPRMRSecret) {
await prisma.secret.deleteMany({
where: { applicationId: id, name, isPRMRSecret }
});
} else {
await prisma.secret.updateMany({ await prisma.secret.updateMany({
where: { applicationId: id, name, isPRMRSecret }, where: { applicationId: id, name, isPRMRSecret },
data: { value, isBuildSecret, isPRMRSecret } data: { value, isBuildSecret, isPRMRSecret }
}); });
}
} else { } else {
await prisma.secret.create({ await prisma.secret.create({
data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } } data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } }
@ -884,6 +905,181 @@ export async function deleteStorage(request: FastifyRequest<DeleteStorage>) {
} }
} }
export async function restartPreview(request: FastifyRequest<RestartPreviewApplication>, reply: FastifyReply) {
try {
const { id, pullmergeRequestId } = 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, port, repository, persistentStorage, id: applicationId, buildPack, exposePort } = application;
const envs = [
`PORT=${port}`
];
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (pullmergeRequestId) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
envs.push(`${secret.name}=${isSecretFound[0].value}`);
} else {
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}-${pullmergeRequestId}' --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}-${pullmergeRequestId}`]: {
image,
container_name: `${applicationId}-${pullmergeRequestId}`,
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}-${pullmergeRequestId}` })
await executeDockerCmd({ dockerId, command: `docker rm ${id}-${pullmergeRequestId}` })
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 getPreviewStatus(request: FastifyRequest<RestartPreviewApplication>) {
try {
const { id, pullmergeRequestId } = request.params
const { teamId } = request.user
let isRunning = false;
let isExited = false;
let isRestarting = false;
let isBuilding = false
const application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId) {
const status = await checkContainer({ dockerId: application.destinationDocker.id, container: `${id}-${pullmergeRequestId}` });
if (status?.found) {
isRunning = status.status.isRunning;
isExited = status.status.isExited;
isRestarting = status.status.isRestarting
}
const building = await prisma.build.findMany({ where: { applicationId: id, pullmergeRequestId, status: { in: ['queued', 'running'] } } })
isBuilding = building.length > 0
}
return {
isBuilding,
isRunning,
isRestarting,
isExited,
};
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function loadPreviews(request: FastifyRequest<OnlyId>) {
try {
const { id } = request.params
const application = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } });
const { stdout } = await executeDockerCmd({ dockerId: application.destinationDocker.id, command: `docker container ls --filter 'name=${id}-' --format "{{json .}}"` })
if (stdout === '') {
throw { status: 500, message: 'No previews found.' }
}
const containers = formatLabelsOnDocker(stdout).filter(container => container.Labels['coolify.configuration'] && container.Labels['coolify.type'] === 'standalone-application')
const jsonContainers = containers
.map((container) =>
JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString())
)
.filter((container) => {
return container.pullmergeRequestId && container.applicationId === id;
});
for (const container of jsonContainers) {
const found = await prisma.previewApplication.findMany({ where: { applicationId: container.applicationId, pullmergeRequestId: container.pullmergeRequestId } })
if (found.length === 0) {
await prisma.previewApplication.create({
data: {
pullmergeRequestId: container.pullmergeRequestId,
sourceBranch: container.branch,
customDomain: container.fqdn,
application: { connect: { id: container.applicationId } }
}
})
}
}
return {
previews: await prisma.previewApplication.findMany({ where: { applicationId: id } })
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getPreviews(request: FastifyRequest<OnlyId>) { export async function getPreviews(request: FastifyRequest<OnlyId>) {
try { try {
const { id } = request.params const { id } = request.params
@ -899,26 +1095,7 @@ export async function getPreviews(request: FastifyRequest<OnlyId>) {
const applicationSecrets = secrets.filter((secret) => !secret.isPRMRSecret); const applicationSecrets = secrets.filter((secret) => !secret.isPRMRSecret);
const PRMRSecrets = secrets.filter((secret) => secret.isPRMRSecret); const PRMRSecrets = secrets.filter((secret) => secret.isPRMRSecret);
const application = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } });
const { stdout } = await executeDockerCmd({ dockerId: application.destinationDocker.id, command: `docker container ls --filter 'name=${id}-' --format "{{json .}}"` })
if (stdout === '') {
return { return {
containers: [],
applicationSecrets: [],
PRMRSecrets: []
}
}
const containers = formatLabelsOnDocker(stdout).filter(container => container.Labels['coolify.configuration'] && container.Labels['coolify.type'] === 'standalone-application')
const jsonContainers = containers
.map((container) =>
JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString())
)
.filter((container) => {
return container.pullmergeRequestId && container.applicationId === id;
});
return {
containers: jsonContainers,
applicationSecrets: applicationSecrets.sort((a, b) => { applicationSecrets: applicationSecrets.sort((a, b) => {
return ('' + a.name).localeCompare(b.name); return ('' + a.name).localeCompare(b.name);
}), }),
@ -970,7 +1147,7 @@ export async function getApplicationLogs(request: FastifyRequest<GetApplicationL
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function getBuildLogs(request: FastifyRequest<GetBuildLogs>) { export async function getBuilds(request: FastifyRequest<GetBuilds>) {
try { try {
const { id } = request.params const { id } = request.params
let { buildId, skip = 0 } = request.query let { buildId, skip = 0 } = request.query
@ -987,17 +1164,15 @@ export async function getBuildLogs(request: FastifyRequest<GetBuildLogs>) {
builds = await prisma.build.findMany({ builds = await prisma.build.findMany({
where: { applicationId: id }, where: { applicationId: id },
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' },
take: 5, take: 5 + skip
skip
}); });
} }
builds = builds.map((build) => { builds = builds.map((build) => {
const updatedAt = day(build.updatedAt).utc(); if (build.status === 'running') {
build.took = updatedAt.diff(day(build.createdAt)) / 1000; build.elapsed = (day().utc().diff(day(build.createdAt)) / 1000).toFixed(0);
build.since = updatedAt.fromNow(); }
return build; return build
}); })
return { return {
builds, builds,
buildCount buildCount
@ -1009,11 +1184,21 @@ export async function getBuildLogs(request: FastifyRequest<GetBuildLogs>) {
export async function getBuildIdLogs(request: FastifyRequest<GetBuildIdLogs>) { export async function getBuildIdLogs(request: FastifyRequest<GetBuildIdLogs>) {
try { try {
const { buildId } = request.params // TODO: Fluentbit could still hold the logs, so we need to check if the logs are done
const { buildId, id } = request.params
let { sequence = 0 } = request.query let { sequence = 0 } = request.query
if (typeof sequence !== 'number') { if (typeof sequence !== 'number') {
sequence = Number(sequence) sequence = Number(sequence)
} }
let file = `/app/logs/${id}_buildlog_${buildId}.csv`
if (isDev) {
file = `${process.cwd()}/../../logs/${id}_buildlog_${buildId}.csv`
}
const data = await prisma.build.findFirst({ where: { id: buildId } });
const createdAt = day(data.createdAt).utc();
try {
await fs.stat(file)
} catch (error) {
let logs = await prisma.buildLog.findMany({ let logs = await prisma.buildLog.findMany({
where: { buildId, time: { gt: sequence } }, where: { buildId, time: { gt: sequence } },
orderBy: { time: 'asc' } orderBy: { time: 'asc' }
@ -1025,6 +1210,23 @@ export async function getBuildIdLogs(request: FastifyRequest<GetBuildIdLogs>) {
log.time = Number(log.time) log.time = Number(log.time)
return log return log
}), }),
fromDb: true,
took: day().diff(createdAt) / 1000,
status: data?.status || 'queued'
}
}
let fileLogs = (await fs.readFile(file)).toString()
let decryptedLogs = await csv({ noheader: true }).fromString(fileLogs)
let logs = decryptedLogs.map(log => {
const parsed = {
time: log['field1'],
line: decrypt(log['field2'] + '","' + log['field3'])
}
return parsed
}).filter(log => log.time > sequence)
return {
logs,
fromDb: false,
took: day().diff(createdAt) / 1000, took: day().diff(createdAt) / 1000,
status: data?.status || 'queued' status: data?.status || 'queued'
} }

View File

@ -1,8 +1,8 @@
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, restartApplication, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication } from './handlers'; import { cancelDeployment, checkDNS, checkDomain, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildPack, getBuilds, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getPreviewStatus, getSecrets, getStorages, getUsage, listApplications, loadPreviews, newApplication, restartApplication, restartPreview, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, 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, GetBuilds, GetImages, RestartPreviewApplication, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types';
const root: FastifyPluginAsync = async (fastify): Promise<void> => { const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.addHook('onRequest', async (request) => { fastify.addHook('onRequest', async (request) => {
@ -37,9 +37,12 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.delete<DeleteStorage>('/:id/storages', async (request) => await deleteStorage(request)); fastify.delete<DeleteStorage>('/:id/storages', async (request) => await deleteStorage(request));
fastify.get<OnlyId>('/:id/previews', async (request) => await getPreviews(request)); fastify.get<OnlyId>('/:id/previews', async (request) => await getPreviews(request));
fastify.post<OnlyId>('/:id/previews/load', async (request) => await loadPreviews(request));
fastify.get<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/status', async (request) => await getPreviewStatus(request));
fastify.post<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/restart', async (request, reply) => await restartPreview(request, reply));
fastify.get<GetApplicationLogs>('/:id/logs', async (request) => await getApplicationLogs(request)); fastify.get<GetApplicationLogs>('/:id/logs', async (request) => await getApplicationLogs(request));
fastify.get<GetBuildLogs>('/:id/logs/build', async (request) => await getBuildLogs(request)); fastify.get<GetBuilds>('/:id/logs/build', async (request) => await getBuilds(request));
fastify.get<GetBuildIdLogs>('/:id/logs/build/:buildId', async (request) => await getBuildIdLogs(request)); fastify.get<GetBuildIdLogs>('/:id/logs/build/:buildId', async (request) => await getBuildIdLogs(request));
fastify.get('/:id/usage', async (request) => await getUsage(request)) fastify.get('/:id/usage', async (request) => await getUsage(request))

View File

@ -89,7 +89,7 @@ export interface GetApplicationLogs extends OnlyId {
since: number, since: number,
} }
} }
export interface GetBuildLogs extends OnlyId { export interface GetBuilds extends OnlyId {
Querystring: { Querystring: {
buildId: string buildId: string
skip: number, skip: number,
@ -97,6 +97,7 @@ export interface GetBuildLogs extends OnlyId {
} }
export interface GetBuildIdLogs { export interface GetBuildIdLogs {
Params: { Params: {
id: string,
buildId: string buildId: string
}, },
Querystring: { Querystring: {
@ -127,3 +128,9 @@ export interface StopPreviewApplication extends OnlyId {
pullmergeRequestId: string | null, pullmergeRequestId: string | null,
} }
} }
export interface RestartPreviewApplication {
Params: {
id: string,
pullmergeRequestId: string | null,
}
}

View File

@ -229,7 +229,7 @@ export async function getDestinationStatus(request: FastifyRequest<OnlyId>) {
try { try {
const { id } = request.params const { id } = request.params
const destination = await prisma.destinationDocker.findUnique({ where: { id } }) const destination = await prisma.destinationDocker.findUnique({ where: { id } })
const isRunning = await checkContainer({ dockerId: destination.id, container: 'coolify-proxy', remove: true }) const { found: isRunning } = await checkContainer({ dockerId: destination.id, container: 'coolify-proxy', remove: true })
return { return {
isRunning isRunning
} }

View File

@ -1,13 +1,23 @@
import axios from "axios";
import axios from 'axios'; import { compareVersions } from "compare-versions";
import { compareVersions } from 'compare-versions'; import cuid from "cuid";
import cuid from 'cuid'; import bcrypt from "bcryptjs";
import bcrypt from 'bcryptjs'; import {
import { asyncExecShell, asyncSleep, cleanupDockerStorage, errorHandler, isDev, listSettings, prisma, uniqueName, version } from '../../../lib/common'; asyncExecShell,
import { supportedServiceTypesAndVersions } from '../../../lib/services/supportedVersions'; asyncSleep,
import type { FastifyReply, FastifyRequest } from 'fastify'; cleanupDockerStorage,
import type { Login, Update } from '.'; errorHandler,
import type { GetCurrentUser } from './types'; isDev,
listSettings,
prisma,
uniqueName,
version,
} from "../../../lib/common";
import { supportedServiceTypesAndVersions } from "../../../lib/services/supportedVersions";
import { scheduler } from "../../../lib/scheduler";
import type { FastifyReply, FastifyRequest } from "fastify";
import type { Login, Update } from ".";
import type { GetCurrentUser } from "./types";
export async function hashPassword(password: string): Promise<string> { export async function hashPassword(password: string): Promise<string> {
const saltRounds = 15; const saltRounds = 15;
@ -17,34 +27,38 @@ export async function hashPassword(password: string): Promise<string> {
export async function cleanupManually(request: FastifyRequest) { export async function cleanupManually(request: FastifyRequest) {
try { try {
const { serverId } = request.body; const { serverId } = request.body;
const destination = await prisma.destinationDocker.findUnique({ where: { id: serverId } }) const destination = await prisma.destinationDocker.findUnique({
await cleanupDockerStorage(destination.id, true, true) where: { id: serverId },
return {} });
await cleanupDockerStorage(destination.id, true, true);
return {};
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message });
} }
} }
export async function checkUpdate(request: FastifyRequest) { export async function checkUpdate(request: FastifyRequest) {
try { try {
const isStaging = request.hostname === 'staging.coolify.io' const isStaging =
request.hostname === "staging.coolify.io" ||
request.hostname === "arm.coolify.io";
const currentVersion = version; const currentVersion = version;
const { data: versions } = await axios.get( const { data: versions } = await axios.get(
`https://get.coollabs.io/versions.json?appId=${process.env['COOLIFY_APP_ID']}&version=${currentVersion}` `https://get.coollabs.io/versions.json?appId=${process.env["COOLIFY_APP_ID"]}&version=${currentVersion}`
); );
const latestVersion = versions['coolify'].main.version const latestVersion = versions["coolify"].main.version;
const isUpdateAvailable = compareVersions(latestVersion, currentVersion); const isUpdateAvailable = compareVersions(latestVersion, currentVersion);
if (isStaging) { if (isStaging) {
return { return {
isUpdateAvailable: true, isUpdateAvailable: true,
latestVersion: 'next' latestVersion: "next",
} };
} }
return { return {
isUpdateAvailable: isStaging ? true : isUpdateAvailable === 1, isUpdateAvailable: isStaging ? true : isUpdateAvailable === 1,
latestVersion latestVersion,
}; };
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message });
} }
} }
@ -59,7 +73,7 @@ export async function update(request: FastifyRequest<Update>) {
`sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env` `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .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 coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"`
); );
return {}; return {};
} else { } else {
@ -67,13 +81,27 @@ export async function update(request: FastifyRequest<Update>) {
return {}; return {};
} }
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message });
}
}
export async function resetQueue(request: FastifyRequest<any>) {
try {
const teamId = request.user.teamId;
if (teamId === "0") {
await prisma.build.updateMany({
where: { status: { in: ["queued", "running"] } },
data: { status: "canceled" },
});
scheduler.workers.get("deployApplication").postMessage("cancel");
}
} catch ({ status, message }) {
return errorHandler({ status, message });
} }
} }
export async function restartCoolify(request: FastifyRequest<any>) { export async function restartCoolify(request: FastifyRequest<any>) {
try { try {
const teamId = request.user.teamId; const teamId = request.user.teamId;
if (teamId === '0') { if (teamId === "0") {
if (!isDev) { if (!isDev) {
asyncExecShell(`docker restart coolify`); asyncExecShell(`docker restart coolify`);
return {}; return {};
@ -81,9 +109,12 @@ export async function restartCoolify(request: FastifyRequest<any>) {
return {}; return {};
} }
} }
throw { status: 500, message: 'You are not authorized to restart Coolify.' }; throw {
status: 500,
message: "You are not authorized to restart Coolify.",
};
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message });
} }
} }
@ -92,24 +123,24 @@ export async function showDashboard(request: FastifyRequest) {
const userId = request.user.userId; const userId = request.user.userId;
const teamId = request.user.teamId; const teamId = request.user.teamId;
const applications = await prisma.application.findMany({ const applications = await prisma.application.findMany({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } },
include: { settings: true, destinationDocker: true, teams: true } include: { settings: true, destinationDocker: true, teams: true },
}); });
const databases = await prisma.database.findMany({ const databases = await prisma.database.findMany({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } },
include: { settings: true, destinationDocker: true, teams: true } include: { settings: true, destinationDocker: true, teams: true },
}); });
const services = await prisma.service.findMany({ const services = await prisma.service.findMany({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } },
include: { destinationDocker: true, teams: true } include: { destinationDocker: true, teams: true },
}); });
const gitSources = await prisma.gitSource.findMany({ const gitSources = await prisma.gitSource.findMany({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } },
include: { teams: true } include: { teams: true },
}); });
const destinations = await prisma.destinationDocker.findMany({ const destinations = await prisma.destinationDocker.findMany({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } },
include: { teams: true } include: { teams: true },
}); });
const settings = await listSettings(); const settings = await listSettings();
return { return {
@ -121,88 +152,98 @@ export async function showDashboard(request: FastifyRequest) {
settings, settings,
}; };
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message });
} }
} }
export async function login(request: FastifyRequest<Login>, reply: FastifyReply) { export async function login(
request: FastifyRequest<Login>,
reply: FastifyReply
) {
if (request.user) { if (request.user) {
return reply.redirect('/dashboard'); return reply.redirect("/dashboard");
} else { } else {
const { email, password, isLogin } = request.body || {}; const { email, password, isLogin } = request.body || {};
if (!email || !password) { if (!email || !password) {
throw { status: 500, message: 'Email and password are required.' }; throw { status: 500, message: "Email and password are required." };
} }
const users = await prisma.user.count(); const users = await prisma.user.count();
const userFound = await prisma.user.findUnique({ const userFound = await prisma.user.findUnique({
where: { email }, where: { email },
include: { teams: true, permission: true }, include: { teams: true, permission: true },
rejectOnNotFound: false rejectOnNotFound: false,
}); });
if (!userFound && isLogin) { if (!userFound && isLogin) {
throw { status: 500, message: 'User not found.' }; throw { status: 500, message: "User not found." };
} }
const { isRegistrationEnabled, id } = await prisma.setting.findFirst() const { isRegistrationEnabled, id } = await prisma.setting.findFirst();
let uid = cuid(); let uid = cuid();
let permission = 'read'; let permission = "read";
let isAdmin = false; let isAdmin = false;
if (users === 0) { if (users === 0) {
await prisma.setting.update({ where: { id }, data: { isRegistrationEnabled: false } }); await prisma.setting.update({
uid = '0'; where: { id },
data: { isRegistrationEnabled: false },
});
uid = "0";
} }
if (userFound) { if (userFound) {
if (userFound.type === 'email') { if (userFound.type === "email") {
if (userFound.password === 'RESETME') { if (userFound.password === "RESETME") {
const hashedPassword = await hashPassword(password); const hashedPassword = await hashPassword(password);
if (userFound.updatedAt < new Date(Date.now() - 1000 * 60 * 10)) { if (userFound.updatedAt < new Date(Date.now() - 1000 * 60 * 10)) {
if (userFound.id === '0') { if (userFound.id === "0") {
await prisma.user.update({ await prisma.user.update({
where: { email: userFound.email }, where: { email: userFound.email },
data: { password: 'RESETME' } data: { password: "RESETME" },
}); });
} else { } else {
await prisma.user.update({ await prisma.user.update({
where: { email: userFound.email }, where: { email: userFound.email },
data: { password: 'RESETTIMEOUT' } data: { password: "RESETTIMEOUT" },
}); });
} }
throw { throw {
status: 500, status: 500,
message: 'Password reset link has expired. Please request a new one.' message:
"Password reset link has expired. Please request a new one.",
}; };
} else { } else {
await prisma.user.update({ await prisma.user.update({
where: { email: userFound.email }, where: { email: userFound.email },
data: { password: hashedPassword } data: { password: hashedPassword },
}); });
return { return {
userId: userFound.id, userId: userFound.id,
teamId: userFound.id, teamId: userFound.id,
permission: userFound.permission, permission: userFound.permission,
isAdmin: true isAdmin: true,
}; };
} }
} }
const passwordMatch = await bcrypt.compare(password, userFound.password); const passwordMatch = await bcrypt.compare(
password,
userFound.password
);
if (!passwordMatch) { if (!passwordMatch) {
throw { throw {
status: 500, status: 500,
message: 'Wrong password or email address.' message: "Wrong password or email address.",
}; };
} }
uid = userFound.id; uid = userFound.id;
isAdmin = true; isAdmin = true;
} }
} else { } else {
permission = 'owner'; permission = "owner";
isAdmin = true; isAdmin = true;
if (!isRegistrationEnabled) { if (!isRegistrationEnabled) {
throw { throw {
status: 404, status: 404,
message: 'Registration disabled by administrator.' message: "Registration disabled by administrator.",
}; };
} }
const hashedPassword = await hashPassword(password); const hashedPassword = await hashPassword(password);
@ -212,17 +253,17 @@ export async function login(request: FastifyRequest<Login>, reply: FastifyReply)
id: uid, id: uid,
email, email,
password: hashedPassword, password: hashedPassword,
type: 'email', type: "email",
teams: { teams: {
create: { create: {
id: uid, id: uid,
name: uniqueName(), name: uniqueName(),
destinationDocker: { connect: { network: 'coolify' } } destinationDocker: { connect: { network: "coolify" } },
}
}, },
permission: { create: { teamId: uid, permission: 'owner' } }
}, },
include: { teams: true } permission: { create: { teamId: uid, permission: "owner" } },
},
include: { teams: true },
}); });
} else { } else {
await prisma.user.create({ await prisma.user.create({
@ -230,16 +271,16 @@ export async function login(request: FastifyRequest<Login>, reply: FastifyReply)
id: uid, id: uid,
email, email,
password: hashedPassword, password: hashedPassword,
type: 'email', type: "email",
teams: { teams: {
create: { create: {
id: uid, id: uid,
name: uniqueName() name: uniqueName(),
}
}, },
permission: { create: { teamId: uid, permission: 'owner' } }
}, },
include: { teams: true } permission: { create: { teamId: uid, permission: "owner" } },
},
include: { teams: true },
}); });
} }
} }
@ -247,18 +288,21 @@ export async function login(request: FastifyRequest<Login>, reply: FastifyReply)
userId: uid, userId: uid,
teamId: uid, teamId: uid,
permission, permission,
isAdmin isAdmin,
}; };
} }
} }
export async function getCurrentUser(request: FastifyRequest<GetCurrentUser>, fastify) { export async function getCurrentUser(
let token = null request: FastifyRequest<GetCurrentUser>,
const { teamId } = request.query fastify
) {
let token = null;
const { teamId } = request.query;
try { try {
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { id: request.user.userId } where: { id: request.user.userId },
}) });
if (!user) { if (!user) {
throw "User not found"; throw "User not found";
} }
@ -269,20 +313,20 @@ export async function getCurrentUser(request: FastifyRequest<GetCurrentUser>, fa
try { try {
const user = await prisma.user.findFirst({ const user = await prisma.user.findFirst({
where: { id: request.user.userId, teams: { some: { id: teamId } } }, where: { id: request.user.userId, teams: { some: { id: teamId } } },
include: { teams: true, permission: true } include: { teams: true, permission: true },
}) });
if (user) { if (user) {
const permission = user.permission.find(p => p.teamId === teamId).permission const permission = user.permission.find(
(p) => p.teamId === teamId
).permission;
const payload = { const payload = {
...request.user, ...request.user,
teamId, teamId,
permission: permission || null, permission: permission || null,
isAdmin: permission === 'owner' || permission === 'admin' isAdmin: permission === "owner" || permission === "admin",
};
token = fastify.jwt.sign(payload);
} }
token = fastify.jwt.sign(payload)
}
} catch (error) { } catch (error) {
// No new token -> not switching teams // No new token -> not switching teams
} }
@ -291,6 +335,6 @@ export async function getCurrentUser(request: FastifyRequest<GetCurrentUser>, fa
settings: await prisma.setting.findFirst(), settings: await prisma.setting.findFirst(),
supportedServiceTypesAndVersions, supportedServiceTypesAndVersions,
token, token,
...request.user ...request.user,
} };
} }

View File

@ -1,5 +1,5 @@
import { FastifyPluginAsync } from 'fastify'; import { FastifyPluginAsync } from 'fastify';
import { checkUpdate, login, showDashboard, update, showUsage, getCurrentUser, cleanupManually, restartCoolify } from './handlers'; import { checkUpdate, login, showDashboard, update, resetQueue, getCurrentUser, cleanupManually, restartCoolify } from './handlers';
import { GetCurrentUser } from './types'; import { GetCurrentUser } from './types';
export interface Update { export interface Update {
@ -23,9 +23,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
onRequest: [fastify.authenticate] onRequest: [fastify.authenticate]
}, async (request) => await getCurrentUser(request, fastify)); }, async (request) => await getCurrentUser(request, fastify));
fastify.get('/undead', { fastify.get('/undead', async function () {
onRequest: [fastify.authenticate]
}, async function () {
return { message: 'nope' }; return { message: 'nope' };
}); });
@ -47,6 +45,10 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
onRequest: [fastify.authenticate] onRequest: [fastify.authenticate]
}, async (request) => await restartCoolify(request)); }, async (request) => await restartCoolify(request));
fastify.post('/internal/resetQueue', {
onRequest: [fastify.authenticate]
}, async (request) => await resetQueue(request));
fastify.post('/internal/cleanup', { fastify.post('/internal/cleanup', {
onRequest: [fastify.authenticate] onRequest: [fastify.authenticate]
}, async (request) => await cleanupManually(request)); }, async (request) => await cleanupManually(request));

View File

@ -8,9 +8,7 @@ export async function listServers(request: FastifyRequest) {
try { try {
const userId = request.user.userId; const userId = request.user.userId;
const teamId = request.user.teamId; const teamId = request.user.teamId;
const servers = await prisma.destinationDocker.findMany({ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } }, remoteEngine: false }, distinct: ['engine'] }) const servers = await prisma.destinationDocker.findMany({ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } }}, distinct: ['remoteIpAddress', 'engine'] })
// const remoteServers = await prisma.destinationDocker.findMany({ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, distinct: ['remoteIpAddress', 'engine'] })
return { return {
servers servers
} }
@ -67,8 +65,7 @@ export async function showUsage(request: FastifyRequest) {
const { stdout: stats } = await executeSSHCmd({ dockerId: id, command: `vmstat -s` }) const { stdout: stats } = await executeSSHCmd({ dockerId: id, command: `vmstat -s` })
const { stdout: disks } = await executeSSHCmd({ dockerId: id, command: `df -m / --output=size,used,pcent|grep -v 'Used'| xargs` }) const { stdout: disks } = await executeSSHCmd({ dockerId: id, command: `df -m / --output=size,used,pcent|grep -v 'Used'| xargs` })
const { stdout: cpus } = await executeSSHCmd({ dockerId: id, command: `nproc --all` }) const { stdout: cpus } = await executeSSHCmd({ dockerId: id, command: `nproc --all` })
// const { stdout: cpuUsage } = await executeSSHCmd({ dockerId: id, command: `echo $[100-$(vmstat 1 2|tail -1|awk '{print $15}')]` }) const { stdout: cpuUsage } = await executeSSHCmd({ dockerId: id, command: `echo $[100-$(vmstat 1 2|tail -1|awk '{print $15}')]` })
// console.log(cpuUsage)
const parsed: any = parseFromText(stats) const parsed: any = parseFromText(stats)
return { return {
usage: { usage: {
@ -81,8 +78,8 @@ export async function showUsage(request: FastifyRequest) {
freeMemPercentage: (parsed.totalMemoryKB - parsed.usedMemoryKB) / parsed.totalMemoryKB * 100 freeMemPercentage: (parsed.totalMemoryKB - parsed.usedMemoryKB) / parsed.totalMemoryKB * 100
}, },
cpu: { cpu: {
load: 0, load: [0,0,0],
usage: 0, usage: cpuUsage,
count: cpus count: cpus
}, },
disk: { disk: {

View File

@ -43,13 +43,17 @@ export async function getServiceStatus(request: FastifyRequest<OnlyId>) {
let isRunning = false; let isRunning = false;
let isExited = false let isExited = false
let isRestarting = false;
const service = await getServiceFromDB({ id, teamId }); const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, settings } = service; const { destinationDockerId, settings } = service;
if (destinationDockerId) { if (destinationDockerId) {
isRunning = await checkContainer({ dockerId: service.destinationDocker.id, container: id }); const status = await checkContainer({ dockerId: service.destinationDocker.id, container: id });
isExited = await isContainerExited(service.destinationDocker.id, id); if (status?.found) {
isRunning = status.status.isRunning;
isExited = status.status.isExited;
isRestarting = status.status.isRestarting
}
} }
return { return {
isRunning, isRunning,
@ -452,7 +456,7 @@ export async function activatePlausibleUsers(request: FastifyRequest<OnlyId>, re
if (destinationDockerId) { if (destinationDockerId) {
await executeDockerCmd({ await executeDockerCmd({
dockerId: destinationDocker.id, dockerId: destinationDocker.id,
command: `docker exec ${id} 'psql -H postgresql://${postgresqlUser}:${postgresqlPassword}@localhost:5432/${postgresqlDatabase} -c "UPDATE users SET email_verified = true;"'` command: `docker exec ${id}-postgresql psql -H postgresql://${postgresqlUser}:${postgresqlPassword}@localhost:5432/${postgresqlDatabase} -c "UPDATE users SET email_verified = true;"`
}) })
return await reply.code(201).send() return await reply.code(201).send()
} }
@ -472,7 +476,7 @@ export async function cleanupPlausibleLogs(request: FastifyRequest<OnlyId>, repl
if (destinationDockerId) { if (destinationDockerId) {
await executeDockerCmd({ await executeDockerCmd({
dockerId: destinationDocker.id, dockerId: destinationDocker.id,
command: `docker exec ${id}-clickhouse sh -c "/usr/bin/clickhouse-client -q \\"SELECT name FROM system.tables WHERE name LIKE '%log%';\\"| xargs -I{} /usr/bin/clickhouse-client -q \"TRUNCATE TABLE system.{};\""` command: `docker exec ${id}-clickhouse /usr/bin/clickhouse-client -q \\"SELECT name FROM system.tables WHERE name LIKE '%log%';\\"| xargs -I{} /usr/bin/clickhouse-client -q \"TRUNCATE TABLE system.{};\"`
}) })
return await reply.code(201).send() return await reply.code(201).send()
} }
@ -554,7 +558,7 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
}); });
try { try {
const isRunning = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-ftp` }); const { found: isRunning } = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-ftp` });
if (isRunning) { if (isRunning) {
await executeDockerCmd({ await executeDockerCmd({
dockerId: destinationDocker.id, dockerId: destinationDocker.id,

View File

@ -30,7 +30,7 @@ import {
import type { OnlyId } from '../../../../types'; import type { OnlyId } from '../../../../types';
import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetGlitchTipSettings, SetWordpressSettings } from './types'; import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetGlitchTipSettings, SetWordpressSettings } from './types';
import { startService, stopService } from '../../../../lib/services/handlers'; import { migrateAppwriteDB, startService, stopService } from '../../../../lib/services/handlers';
const root: FastifyPluginAsync = async (fastify): Promise<void> => { const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.addHook('onRequest', async (request) => { fastify.addHook('onRequest', async (request) => {
@ -76,6 +76,8 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
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));
fastify.post<ActivateWordpressFtp>('/:id/wordpress/ftp', async (request, reply) => await activateWordpressFtp(request, reply)); fastify.post<ActivateWordpressFtp>('/:id/wordpress/ftp', async (request, reply) => await activateWordpressFtp(request, reply));
fastify.post<OnlyId>('/:id/appwrite/migrate', async (request, reply) => await migrateAppwriteDB(request, reply));
}; };
export default root; export default root;

View File

@ -1,7 +1,7 @@
import axios from "axios"; import axios from "axios";
import cuid from "cuid"; import cuid from "cuid";
import crypto from "crypto"; import crypto from "crypto";
import { encrypt, errorHandler, getUIUrl, isDev, prisma } from "../../../lib/common"; import { encrypt, errorHandler, getDomain, getUIUrl, isDev, prisma } from "../../../lib/common";
import { checkContainer, removeContainer } from "../../../lib/docker"; import { checkContainer, removeContainer } from "../../../lib/docker";
import { createdBranchDatabase, getApplicationFromDBWebhook, removeBranchDatabase } from "../../api/v1/applications/handlers"; import { createdBranchDatabase, getApplicationFromDBWebhook, removeBranchDatabase } from "../../api/v1/applications/handlers";
@ -154,7 +154,7 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
if (application.settings.previews) { if (application.settings.previews) {
if (application.destinationDockerId) { if (application.destinationDockerId) {
const isRunning = await checkContainer( const { found: isRunning } = await checkContainer(
{ {
dockerId: application.destinationDocker.id, dockerId: application.destinationDocker.id,
container: application.id container: application.id
@ -169,10 +169,29 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
pullmergeRequestAction === 'reopened' || pullmergeRequestAction === 'reopened' ||
pullmergeRequestAction === 'synchronize' pullmergeRequestAction === 'synchronize'
) { ) {
await prisma.application.update({ await prisma.application.update({
where: { id: application.id }, where: { id: application.id },
data: { updatedAt: new Date() } data: { updatedAt: new Date() }
}); });
let previewApplicationId = undefined
if (pullmergeRequestId) {
const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, pullmergeRequestId } })
if (foundPreviewApplications.length > 0) {
previewApplicationId = foundPreviewApplications[0].id
} else {
const protocol = application.fqdn.includes('https://') ? 'https://' : 'http://'
const previewApplication = await prisma.previewApplication.create({
data: {
pullmergeRequestId,
sourceBranch,
customDomain: `${protocol}${pullmergeRequestId}.${getDomain(application.fqdn)}`,
application: { connect: { id: application.id } }
}
})
previewApplicationId = previewApplication.id
}
}
// if (application.connectedDatabase && pullmergeRequestAction === 'opened' || pullmergeRequestAction === 'reopened') { // if (application.connectedDatabase && pullmergeRequestAction === 'opened' || pullmergeRequestAction === 'reopened') {
// // Coolify hosted database // // Coolify hosted database
// if (application.connectedDatabase.databaseId) { // if (application.connectedDatabase.databaseId) {
@ -187,6 +206,7 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
data: { data: {
id: buildId, id: buildId,
pullmergeRequestId, pullmergeRequestId,
previewApplicationId,
sourceBranch, sourceBranch,
applicationId: application.id, applicationId: application.id,
destinationDockerId: application.destinationDocker.id, destinationDockerId: application.destinationDocker.id,
@ -198,7 +218,9 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
} }
}); });
return {
message: 'Queued. Thank you!'
};
} else if (pullmergeRequestAction === 'closed') { } else if (pullmergeRequestAction === 'closed') {
if (application.destinationDockerId) { if (application.destinationDockerId) {
const id = `${application.id}-${pullmergeRequestId}`; const id = `${application.id}-${pullmergeRequestId}`;
@ -206,13 +228,22 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
await removeContainer({ id, dockerId: application.destinationDocker.id }); await removeContainer({ id, dockerId: application.destinationDocker.id });
} catch (error) { } } catch (error) { }
} }
if (application.connectedDatabase.databaseId) { const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, pullmergeRequestId } })
const databaseId = application.connectedDatabase.databaseId; if (foundPreviewApplications.length > 0) {
const database = await prisma.database.findUnique({ where: { id: databaseId } }); for (const preview of foundPreviewApplications) {
if (database) { await prisma.previewApplication.delete({ where: { id: preview.id } })
await removeBranchDatabase(database, pullmergeRequestId);
} }
} }
return {
message: 'PR closed. Thank you!'
};
// if (application?.connectedDatabase?.databaseId) {
// const databaseId = application.connectedDatabase.databaseId;
// const database = await prisma.database.findUnique({ where: { id: databaseId } });
// if (database) {
// await removeBranchDatabase(database, pullmergeRequestId);
// }
// }
} }
} }
} }

View File

@ -2,7 +2,7 @@ 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, getUIUrl, isDev, listSettings, prisma } from "../../../lib/common"; import { errorHandler, getAPIUrl, getDomain, getUIUrl, isDev, listSettings, prisma } from "../../../lib/common";
import { checkContainer, removeContainer } from "../../../lib/docker"; import { checkContainer, removeContainer } from "../../../lib/docker";
import { getApplicationFromDB, getApplicationFromDBWebhook } from "../../api/v1/applications/handlers"; import { getApplicationFromDB, getApplicationFromDBWebhook } from "../../api/v1/applications/handlers";
@ -91,8 +91,8 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
} }
} }
} 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 }, project: { id } } = request.body
const pullmergeRequestId = request.body.object_attributes.iid.toString();
const projectId = Number(id); const projectId = Number(id);
if (!allowedActions.includes(action)) { if (!allowedActions.includes(action)) {
throw { status: 500, message: 'Action not allowed.' } throw { status: 500, message: 'Action not allowed.' }
@ -107,7 +107,7 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
const buildId = cuid(); const buildId = cuid();
if (application.settings.previews) { if (application.settings.previews) {
if (application.destinationDockerId) { if (application.destinationDockerId) {
const isRunning = await checkContainer( const { found: isRunning } = await checkContainer(
{ {
dockerId: application.destinationDocker.id, dockerId: application.destinationDocker.id,
container: application.id container: application.id
@ -130,10 +130,29 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
where: { id: application.id }, where: { id: application.id },
data: { updatedAt: new Date() } data: { updatedAt: new Date() }
}); });
let previewApplicationId = undefined
if (pullmergeRequestId) {
const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, pullmergeRequestId } })
if (foundPreviewApplications.length > 0) {
previewApplicationId = foundPreviewApplications[0].id
} else {
const protocol = application.fqdn.includes('https://') ? 'https://' : 'http://'
const previewApplication = await prisma.previewApplication.create({
data: {
pullmergeRequestId,
sourceBranch,
customDomain: `${protocol}${pullmergeRequestId}.${getDomain(application.fqdn)}`,
application: { connect: { id: application.id } }
}
})
previewApplicationId = previewApplication.id
}
}
await prisma.build.create({ await prisma.build.create({
data: { data: {
id: buildId, id: buildId,
pullmergeRequestId: pullmergeRequestId.toString(), pullmergeRequestId,
previewApplicationId,
sourceBranch, sourceBranch,
applicationId: application.id, applicationId: application.id,
destinationDockerId: application.destinationDocker.id, destinationDockerId: application.destinationDocker.id,
@ -150,8 +169,19 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
} else if (action === 'close') { } else if (action === 'close') {
if (application.destinationDockerId) { if (application.destinationDockerId) {
const id = `${application.id}-${pullmergeRequestId}`; const id = `${application.id}-${pullmergeRequestId}`;
try {
await removeContainer({ id, dockerId: application.destinationDocker.id }); await removeContainer({ id, dockerId: application.destinationDocker.id });
} catch (error) { }
} }
const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, pullmergeRequestId } })
if (foundPreviewApplications.length > 0) {
for (const preview of foundPreviewApplications) {
await prisma.previewApplication.delete({ where: { id: preview.id } })
}
}
return {
message: 'MR closed. Thank you!'
};
} }
} }

View File

@ -12,7 +12,7 @@ function configureMiddleware(
if (isHttps) { if (isHttps) {
traefik.http.routers[id] = { traefik.http.routers[id] = {
entrypoints: ['web'], entrypoints: ['web'],
rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`, rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/\`)`,
service: `${id}`, service: `${id}`,
middlewares: ['redirect-to-https'] middlewares: ['redirect-to-https']
}; };
@ -53,7 +53,7 @@ function configureMiddleware(
if (isDualCerts) { if (isDualCerts) {
traefik.http.routers[`${id}-secure`] = { traefik.http.routers[`${id}-secure`] = {
entrypoints: ['websecure'], entrypoints: ['websecure'],
rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`, rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/\`)`,
service: `${id}`, service: `${id}`,
tls: { tls: {
certresolver: 'letsencrypt' certresolver: 'letsencrypt'
@ -64,7 +64,7 @@ function configureMiddleware(
if (isWWW) { if (isWWW) {
traefik.http.routers[`${id}-secure-www`] = { traefik.http.routers[`${id}-secure-www`] = {
entrypoints: ['websecure'], entrypoints: ['websecure'],
rule: `Host(\`www.${nakedDomain}\`)`, rule: `Host(\`www.${nakedDomain}\`) && PathPrefix(\`/\`)`,
service: `${id}`, service: `${id}`,
tls: { tls: {
certresolver: 'letsencrypt' certresolver: 'letsencrypt'
@ -73,7 +73,7 @@ function configureMiddleware(
}; };
traefik.http.routers[`${id}-secure`] = { traefik.http.routers[`${id}-secure`] = {
entrypoints: ['websecure'], entrypoints: ['websecure'],
rule: `Host(\`${nakedDomain}\`)`, rule: `Host(\`${nakedDomain}\`) && PathPrefix(\`/\`)`,
service: `${id}`, service: `${id}`,
tls: { tls: {
domains: { domains: {
@ -86,7 +86,7 @@ function configureMiddleware(
} else { } else {
traefik.http.routers[`${id}-secure-www`] = { traefik.http.routers[`${id}-secure-www`] = {
entrypoints: ['websecure'], entrypoints: ['websecure'],
rule: `Host(\`www.${nakedDomain}\`)`, rule: `Host(\`www.${nakedDomain}\`) && PathPrefix(\`/\`)`,
service: `${id}`, service: `${id}`,
tls: { tls: {
domains: { domains: {
@ -97,7 +97,7 @@ function configureMiddleware(
}; };
traefik.http.routers[`${id}-secure`] = { traefik.http.routers[`${id}-secure`] = {
entrypoints: ['websecure'], entrypoints: ['websecure'],
rule: `Host(\`${domain}\`)`, rule: `Host(\`${domain}\`) && PathPrefix(\`/\`)`,
service: `${id}`, service: `${id}`,
tls: { tls: {
certresolver: 'letsencrypt' certresolver: 'letsencrypt'
@ -110,14 +110,14 @@ function configureMiddleware(
} else { } else {
traefik.http.routers[id] = { traefik.http.routers[id] = {
entrypoints: ['web'], entrypoints: ['web'],
rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`, rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/\`)`,
service: `${id}`, service: `${id}`,
middlewares: [] middlewares: []
}; };
traefik.http.routers[`${id}-secure`] = { traefik.http.routers[`${id}-secure`] = {
entrypoints: ['websecure'], entrypoints: ['websecure'],
rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`, rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/\`)`,
service: `${id}`, service: `${id}`,
tls: { tls: {
domains: { domains: {

View File

@ -42,6 +42,7 @@
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"dayjs": "1.11.5",
"@sveltejs/adapter-static": "1.0.0-next.39", "@sveltejs/adapter-static": "1.0.0-next.39",
"@tailwindcss/typography": "^0.5.7", "@tailwindcss/typography": "^0.5.7",
"cuid": "2.1.8", "cuid": "2.1.8",

View File

@ -3,33 +3,35 @@ import Cookies from 'js-cookie';
export function getAPIUrl() { export function getAPIUrl() {
if (GITPOD_WORKSPACE_URL) { if (GITPOD_WORKSPACE_URL) {
const { href } = new URL(GITPOD_WORKSPACE_URL) const { href } = new URL(GITPOD_WORKSPACE_URL);
const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, '') const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, '');
return newURL return newURL;
} }
if (CODESANDBOX_HOST) { if (CODESANDBOX_HOST) {
return `https://${CODESANDBOX_HOST.replace(/\$PORT/,'3001')}` return `https://${CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`;
} }
return dev ? 'http://localhost:3001' : 'http://localhost:3000'; return dev
? 'http://localhost:3001'
: 'http://localhost:3000';
} }
export function getWebhookUrl(type: string) { export function getWebhookUrl(type: string) {
if (GITPOD_WORKSPACE_URL) { if (GITPOD_WORKSPACE_URL) {
const { href } = new URL(GITPOD_WORKSPACE_URL) const { href } = new URL(GITPOD_WORKSPACE_URL);
const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, '') const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, '');
if (type === 'github') { if (type === 'github') {
return `${newURL}/webhooks/github/events` return `${newURL}/webhooks/github/events`;
} }
if (type === 'gitlab') { if (type === 'gitlab') {
return `${newURL}/webhooks/gitlab/events` return `${newURL}/webhooks/gitlab/events`;
} }
} }
if (CODESANDBOX_HOST) { if (CODESANDBOX_HOST) {
const newURL = `https://${CODESANDBOX_HOST.replace(/\$PORT/,'3001')}` const newURL = `https://${CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`;
if (type === 'github') { if (type === 'github') {
return `${newURL}/webhooks/github/events` return `${newURL}/webhooks/github/events`;
} }
if (type === 'gitlab') { if (type === 'gitlab') {
return `${newURL}/webhooks/gitlab/events` return `${newURL}/webhooks/gitlab/events`;
} }
} }
return `https://webhook.site/0e5beb2c-4e9b-40e2-a89e-32295e570c21/events`; return `https://webhook.site/0e5beb2c-4e9b-40e2-a89e-32295e570c21/events`;
@ -103,7 +105,11 @@ async function send({
return {}; return {};
} }
if (!response.ok) { if (!response.ok) {
if (response.status === 401 && !path.startsWith('https://api.github') && !path.includes('/v4/user')) { if (
response.status === 401 &&
!path.startsWith('https://api.github') &&
!path.includes('/v4/user')
) {
Cookies.remove('token'); Cookies.remove('token');
} }

View File

@ -84,3 +84,7 @@ export function handlerNotFoundLoad(error: any, url: URL) {
error: new Error(`Could not load ${url}`) error: new Error(`Could not load ${url}`)
}; };
} }
export function getRndInteger(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}

View File

@ -13,8 +13,9 @@
export let id: string; export let id: string;
export let name: string; export let name: string;
export let placeholder = ''; export let placeholder = '';
export let inputStyle = '';
let disabledClass = 'bg-coolback disabled:bg-coolblack'; let disabledClass = 'bg-coolback disabled:bg-coolblack w-full';
let isHttps = browser && window.location.protocol === 'https:'; let isHttps = browser && window.location.protocol === 'https:';
function copyToClipboard() { function copyToClipboard() {
@ -32,6 +33,7 @@
{#if !isPasswordField || showPassword} {#if !isPasswordField || showPassword}
{#if textarea} {#if textarea}
<textarea <textarea
style={inputStyle}
rows="5" rows="5"
class={disabledClass} class={disabledClass}
class:pr-10={true} class:pr-10={true}
@ -47,6 +49,7 @@
> >
{:else} {:else}
<input <input
style={inputStyle}
class={disabledClass} class={disabledClass}
type="text" type="text"
class:pr-10={true} class:pr-10={true}
@ -63,6 +66,7 @@
{/if} {/if}
{:else} {:else}
<input <input
style={inputStyle}
class={disabledClass} class={disabledClass}
class:pr-10={true} class:pr-10={true}
class:pr-20={value && isHttps} class:pr-20={value && isHttps}

View File

@ -10,23 +10,10 @@
.slice(-16); .slice(-16);
</script> </script>
<a {id} href={url} target="_blank" class="icons inline-block text-pink-500 cursor-pointer text-xs"> <a {id} href={url} target="_blank" class="icons inline-block cursor-pointer text-xs mx-2">
<svg <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
xmlns="http://www.w3.org/2000/svg" <path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />
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> </svg>
</a> </a>
<Tooltip triggeredBy={`#${id}`}>See details in the documentation</Tooltip> <Tooltip triggeredBy={`#${id}`}>See details in the documentation</Tooltip>

View File

@ -1,26 +1,38 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; // import { onMount } from 'svelte';
import Tooltip from './Tooltip.svelte'; // import Tooltip from './Tooltip.svelte';
export let explanation = ''; export let explanation = '';
let id: any; export let position = 'dropdown-right'
let self: any; // let id: any;
onMount(() => { // let self: any;
id = `info-${self.offsetLeft}-${self.offsetTop}`; // onMount(() => {
}); // id = `info-${self.offsetLeft}-${self.offsetTop}`;
// });
</script> </script>
<div {id} class="inline-block mx-2 text-pink-500 cursor-pointer" bind:this={self}> <div class={`dropdown dropdown-end ${position}`}>
<label tabindex="0" class="btn btn-circle btn-ghost btn-xs text-sky-500">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-4 h-4 stroke-current"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
</label>
<div tabindex="0" class="card compact dropdown-content shadow bg-coolgray-400 rounded w-64">
<div class="card-body">
<!-- <h2 class="card-title">You needed more info?</h2> -->
<p class="text-xs font-normal">{@html explanation}</p>
</div>
</div>
</div>
<!-- <div {id} class="inline-block mx-2 cursor-pointer" bind:this={self}>
<svg <svg
fill="none" fill="none"
height="18" height="14"
shape-rendering="geometricPrecision" shape-rendering="geometricPrecision"
stroke="currentColor" stroke="currentColor"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="1.5" stroke-width="1.4"
viewBox="0 0 24 24" viewBox="0 0 24 24"
width="18" width="14"
><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 ><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" d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3"
/><circle cx="12" cy="17" r=".5" /> /><circle cx="12" cy="17" r=".5" />
@ -28,4 +40,4 @@
</div> </div>
{#if id} {#if id}
<Tooltip triggeredBy={`#${id}`}>{@html explanation}</Tooltip> <Tooltip triggeredBy={`#${id}`}>{@html explanation}</Tooltip>
{/if} {/if} -->

View File

@ -15,9 +15,13 @@
<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"> <!-- svelte-ignore a11y-label-has-associated-control -->
{title}<Explaner explanation={description} /> <label>
</div> {title}
{#if description && description !== ''}
<Explaner explanation={description} />
{/if}
</label>
</div> </div>
</div> </div>
<div class:text-center={isCenter} class="flex justify-center"> <div class:text-center={isCenter} class="flex justify-center">

View File

@ -2,6 +2,11 @@
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
export let type = 'info'; export let type = 'info';
function success() {
if (type === 'success') {
return 'bg-gradient-to-r from-purple-500 via-pink-500 to-red-500';
}
}
</script> </script>
<div <div
@ -10,8 +15,7 @@
on:focus={() => dispatch('pause')} on:focus={() => dispatch('pause')}
on:mouseout={() => dispatch('resume')} on:mouseout={() => dispatch('resume')}
on:blur={() => dispatch('resume')} on:blur={() => dispatch('resume')}
class="alert shadow-lg text-white rounded hover:scale-105 transition-all duration-100 cursor-pointer" class={`flex flex-row alert shadow-lg text-white hover:scale-105 transition-all duration-100 cursor-pointer rounded ${success()}`}
class:bg-coollabs={type === 'success'}
class:alert-error={type === 'error'} class:alert-error={type === 'error'}
class:alert-info={type === 'info'} class:alert-info={type === 'info'}
> >

View File

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { fade } from 'svelte/transition';
import Toast from './Toast.svelte'; import Toast from './Toast.svelte';
import { dismissToast, pauseToast, resumeToast, toasts } from '$lib/store'; import { dismissToast, pauseToast, resumeToast, toasts } from '$lib/store';
@ -7,7 +6,7 @@
{#if $toasts} {#if $toasts}
<section> <section>
<article class="toast toast-top toast-end rounded-none" role="alert" transition:fade> <article class="toast toast-top toast-end rounded-none px-10" role="alert" >
{#each $toasts as toast (toast.id)} {#each $toasts as toast (toast.id)}
<Toast <Toast
type={toast.type} type={toast.type}

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Tooltip } from 'flowbite-svelte'; import { Tooltip } from 'flowbite-svelte';
export let placement = 'bottom'; export let placement = 'bottom';
export let color = 'bg-coollabs text-left'; export let color = 'bg-coollabs font-thin text-left';
export let triggeredBy = '#tooltip-default'; export let triggeredBy = '#tooltip-default';
</script> </script>

View File

@ -1,12 +1,11 @@
<script lang="ts"> <script lang="ts">
import { dev } from '$app/env'; import { dev } from '$app/env';
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import { addToast, appSession, features } from '$lib/store'; import { addToast, appSession, features, updateLoading, isUpdateAvailable } from '$lib/store';
import { asyncSleep, errorNotification } from '$lib/common'; import { asyncSleep, errorNotification } from '$lib/common';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Tooltip from './Tooltip.svelte'; import Tooltip from './Tooltip.svelte';
let isUpdateAvailable = false;
let updateStatus: any = { let updateStatus: any = {
found: false, found: false,
loading: false, loading: false,
@ -58,37 +57,41 @@
if ($appSession.userId) { if ($appSession.userId) {
const overrideVersion = $features.latestVersion; const overrideVersion = $features.latestVersion;
if ($appSession.teamId === '0') { if ($appSession.teamId === '0') {
if ($updateLoading === true) return;
try { try {
$updateLoading = true;
const data = await get(`/update`); const data = await get(`/update`);
if (overrideVersion || data?.isUpdateAvailable) { if (overrideVersion || data?.isUpdateAvailable) {
latestVersion = overrideVersion || data.latestVersion; latestVersion = overrideVersion || data.latestVersion;
if (overrideVersion) { if (overrideVersion) {
isUpdateAvailable = true; $isUpdateAvailable = true;
} else { } else {
isUpdateAvailable = data.isUpdateAvailable; $isUpdateAvailable = data.isUpdateAvailable;
} }
} }
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally {
$updateLoading = false;
} }
} }
} }
}); });
</script> </script>
<div class="py-2"> <div class="py-0 lg:py-2">
{#if $appSession.teamId === '0'} {#if $appSession.teamId === '0'}
{#if isUpdateAvailable} {#if $isUpdateAvailable}
<button <button
id="update" id="update"
disabled={updateStatus.success === false} disabled={updateStatus.success === false}
on:click={update} on:click={update}
class="icons bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 text-white duration-75 hover:scale-105" class="icons bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 text-white duration-75 hover:scale-105 w-full"
> >
{#if updateStatus.loading} {#if updateStatus.loading}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="lds-heart h-9 w-8" class="lds-heart h-8 w-8"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@ -102,9 +105,10 @@
/> />
</svg> </svg>
{:else if updateStatus.success === null} {:else if updateStatus.success === null}
<div class="flex items-center justify-center space-x-2">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-9 w-8" class="h-8 w-8"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@ -118,8 +122,10 @@
<line x1="12" y1="8" x2="12" y2="16" /> <line x1="12" y1="8" x2="12" y2="16" />
<line x1="16" y1="12" x2="12" y2="8" /> <line x1="16" y1="12" x2="12" y2="8" />
</svg> </svg>
<span class="flex lg:hidden">Update available</span>
</div>
{:else if updateStatus.success} {:else if updateStatus.success}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" class="h-9 w-8" <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" class="h-8 w-8"
><path ><path
fill="#DD2E44" fill="#DD2E44"
d="M11.626 7.488c-.112.112-.197.247-.268.395l-.008-.008L.134 33.141l.011.011c-.208.403.14 1.223.853 1.937.713.713 1.533 1.061 1.936.853l.01.01L28.21 24.735l-.008-.009c.147-.07.282-.155.395-.269 1.562-1.562-.971-6.627-5.656-11.313-4.687-4.686-9.752-7.218-11.315-5.656z" d="M11.626 7.488c-.112.112-.197.247-.268.395l-.008-.008L.134 33.141l.011.011c-.208.403.14 1.223.853 1.937.713.713 1.533 1.061 1.936.853l.01.01L28.21 24.735l-.008-.009c.147-.07.282-.155.395-.269 1.562-1.562-.971-6.627-5.656-11.313-4.687-4.686-9.752-7.218-11.315-5.656z"
@ -184,7 +190,9 @@
> >
{/if} {/if}
</button> </button>
<Tooltip triggeredBy="#update" placement="right" color="bg-gradient-to-r from-purple-500 via-pink-500 to-red-500">New Version Available!</Tooltip> <Tooltip triggeredBy="#update" placement="right" color="bg-coolgray-200 text-white"
>New Version Available!</Tooltip
>
{/if} {/if}
{/if} {/if}
</div> </div>

View File

@ -79,12 +79,12 @@
BETA BETA
</div> </div>
{/if} {/if}
<div class="w-full flex flex-row space-x-4"> <div class="w-full flex flex-col lg:flex-row space-y-4 lg:space-y-0 space-x-4">
<div class="flex flex-col"> <div class="flex flex-col">
<h1 class="font-bold text-lg lg:text-xl truncate"> <h1 class="font-bold text-lg lg:text-xl truncate">
{server.name} {server.name}
</h1> </h1>
<div class="text-xs "> <div class="text-xs">
{#if server?.remoteIpAddress} {#if server?.remoteIpAddress}
<h2>{server?.remoteIpAddress}</h2> <h2>{server?.remoteIpAddress}</h2>
{:else} {:else}
@ -94,9 +94,11 @@
</div> </div>
{#if $appSession.teamId === '0'} {#if $appSession.teamId === '0'}
<button <button
disabled={loading.cleanup}
on:click={manuallyCleanupStorage} on:click={manuallyCleanupStorage}
class:loading={loading.cleanup} class:loading={loading.cleanup}
class="btn btn-sm bg-coollabs">Cleanup Storage</button class:bg-coollabs={!loading.cleanup}
class="btn btn-sm">Cleanup Storage</button
> >
{/if} {/if}
</div> </div>
@ -108,21 +110,21 @@
<div class="stats stats-vertical min-w-[16rem] mb-5 rounded bg-transparent"> <div class="stats stats-vertical min-w-[16rem] mb-5 rounded bg-transparent">
<div class="stat"> <div class="stat">
<div class="stat-title">Total Memory</div> <div class="stat-title">Total Memory</div>
<div class="stat-value text-2xl"> <div class="stat-value text-2xl text-white">
{(usage?.memory?.totalMemMb).toFixed(0)}<span class="text-sm">MB</span> {(usage?.memory?.totalMemMb).toFixed(0)}<span class="text-sm">MB</span>
</div> </div>
</div> </div>
<div class="stat"> <div class="stat">
<div class="stat-title">Used Memory</div> <div class="stat-title">Used Memory</div>
<div class="stat-value text-2xl"> <div class="stat-value text-2xl text-white">
{(usage?.memory?.usedMemMb).toFixed(0)}<span class="text-sm">MB</span> {(usage?.memory?.usedMemMb).toFixed(0)}<span class="text-sm">MB</span>
</div> </div>
</div> </div>
<div class="stat"> <div class="stat">
<div class="stat-title">Free Memory</div> <div class="stat-title">Free Memory</div>
<div class="stat-value text-2xl"> <div class="stat-value text-2xl text-white">
{(usage?.memory?.freeMemPercentage).toFixed(0)}<span class="text-sm">%</span> {(usage?.memory?.freeMemPercentage).toFixed(0)}<span class="text-sm">%</span>
</div> </div>
</div> </div>
@ -131,41 +133,41 @@
<div class="stats stats-vertical min-w-[20rem] mb-5 bg-transparent rounded"> <div class="stats stats-vertical min-w-[20rem] mb-5 bg-transparent rounded">
<div class="stat"> <div class="stat">
<div class="stat-title">Total CPU</div> <div class="stat-title">Total CPU</div>
<div class="stat-value text-2xl"> <div class="stat-value text-2xl text-white">
{usage?.cpu?.count} {usage?.cpu?.count}
</div> </div>
</div> </div>
<div class="stat"> <div class="stat">
<div class="stat-title">CPU Usage</div> <div class="stat-title">CPU Usage</div>
<div class="stat-value text-2xl"> <div class="stat-value text-2xl text-white">
{usage?.cpu?.usage}<span class="text-sm">%</span> {usage?.cpu?.usage}<span class="text-sm">%</span>
</div> </div>
</div> </div>
<div class="stat"> <div class="stat">
<div class="stat-title">Load Average (5,10,30mins)</div> <div class="stat-title">Load Average (5,10,30mins)</div>
<div class="stat-value text-2xl">{usage?.cpu?.load}</div> <div class="stat-value text-2xl text-white">{usage?.cpu?.load}</div>
</div> </div>
</div> </div>
<div class="stats stats-vertical min-w-[16rem] mb-5 bg-transparent rounded"> <div class="stats stats-vertical min-w-[16rem] mb-5 bg-transparent rounded">
<div class="stat"> <div class="stat">
<div class="stat-title">Total Disk</div> <div class="stat-title">Total Disk</div>
<div class="stat-value text-2xl"> <div class="stat-value text-2xl text-white">
{usage?.disk?.totalGb}<span class="text-sm">GB</span> {usage?.disk?.totalGb}<span class="text-sm">GB</span>
</div> </div>
</div> </div>
<div class="stat"> <div class="stat">
<div class="stat-title">Used Disk</div> <div class="stat-title">Used Disk</div>
<div class="stat-value text-2xl"> <div class="stat-value text-2xl text-white">
{usage?.disk?.usedGb}<span class="text-sm">GB</span> {usage?.disk?.usedGb}<span class="text-sm">GB</span>
</div> </div>
</div> </div>
<div class="stat"> <div class="stat">
<div class="stat-title">Free Disk</div> <div class="stat-title">Free Disk</div>
<div class="stat-value text-2xl"> <div class="stat-value text-2xl text-white">
{usage?.disk?.freePercentage}<span class="text-sm">%</span> {usage?.disk?.freePercentage}<span class="text-sm">%</span>
</div> </div>
</div> </div>

View File

@ -9,15 +9,8 @@
viewBox="0 0 309.88 252.72" viewBox="0 0 309.88 252.72"
class={isAbsolute ? 'absolute top-0 left-0 -m-5 h-12 w-12 ' : 'mx-auto w-8 h-8'} class={isAbsolute ? 'absolute top-0 left-0 -m-5 h-12 w-12 ' : 'mx-auto w-8 h-8'}
> >
<defs>
<style>
.cls-1 {
fill: #fff;
}
</style>
</defs>
<path <path
class="cls-1" fill="#fff"
d="M316,10.05a4.2,4.2,0,0,0-2.84-1c-2.84,0-6.5,1.92-8.46,3l-.79.4a26.81,26.81,0,0,1-10.57,2.66c-3.76.12-7,.34-11.22.77-25,2.58-36.15,21.74-46.89,40.27-5.84,10.08-11.88,20.5-20.16,28.57a55.71,55.71,0,0,1-5.46,4.63c-8.57,6.39-19.33,10.9-27.74,14.12-8.07,3.08-16.86,5.85-25.37,8.53-7.78,2.45-15.14,4.76-21.9,7.28-3.05,1.13-5.64,2-7.93,2.76-6.15,2-10.6,3.53-17.08,8-2.53,1.73-5.07,3.6-6.8,5a71.26,71.26,0,0,0-13.54,14.27A84.81,84.81,0,0,1,77.88,163c-1.36,1.34-3.8,2-7.43,2-4.27,0-9.43-.88-14.91-1.81s-11.46-2-16.46-2c-4.07,0-7.17.66-9.5,2,0,0-3.9,2.28-5.56,5.23l1.62.73a33.56,33.56,0,0,1,6.93,5,33.68,33.68,0,0,0,7.19,5.12A6.37,6.37,0,0,1,42,180.72c-.69,1-1.69,2.29-2.74,3.67-5.77,7.55-9.13,12.32-7.2,14.92a6,6,0,0,0,3,.68c12.59,0,19.34-3.27,27.9-7.41,2.47-1.2,5-2.44,8-3.7,5-2.17,10.38-5.63,16.08-9.29,7.55-4.85,15.36-9.87,22.92-12.3a62.3,62.3,0,0,1,19.23-2.7c8,0,16.42,1.07,24.54,2.11,6.06.78,12.32,1.58,18.47,2,2.39.14,4.6.21,6.76.21a78.48,78.48,0,0,0,8.61-.45l.68-.24c4.32-2.65,6.34-8.34,8.29-13.84,1.26-3.54,2.32-6.72,4-8.74a2.06,2.06,0,0,1,.33-.27.4.4,0,0,1,.49.08.25.25,0,0,1,0,.16c-1,21.51-9.67,35.16-18.42,47.3L177,199.14s8.18,0,12.84-1.8c17-5.08,29.84-16.28,39.18-34.14a144.39,144.39,0,0,0,6.16-14.09c.16-.4,1.64-1.14,1.49.93,0,.61-.08,1.29-.13,2h0c0,.42-.06.85-.08,1.28-.25,3-1,9.34-1,9.34l5.25-2.81c12.66-8,22.42-24.14,29.82-49.25,3.09-10.46,5.34-20.85,7.33-30,2.38-11,4.43-20.43,6.78-24.09,3.69-5.74,9.32-9.62,14.77-13.39.75-.51,1.49-1,2.22-1.54,6.86-4.81,13.67-10.36,15.16-20.71l0-.23C317.93,12.92,317,11,316,10.05Z" d="M316,10.05a4.2,4.2,0,0,0-2.84-1c-2.84,0-6.5,1.92-8.46,3l-.79.4a26.81,26.81,0,0,1-10.57,2.66c-3.76.12-7,.34-11.22.77-25,2.58-36.15,21.74-46.89,40.27-5.84,10.08-11.88,20.5-20.16,28.57a55.71,55.71,0,0,1-5.46,4.63c-8.57,6.39-19.33,10.9-27.74,14.12-8.07,3.08-16.86,5.85-25.37,8.53-7.78,2.45-15.14,4.76-21.9,7.28-3.05,1.13-5.64,2-7.93,2.76-6.15,2-10.6,3.53-17.08,8-2.53,1.73-5.07,3.6-6.8,5a71.26,71.26,0,0,0-13.54,14.27A84.81,84.81,0,0,1,77.88,163c-1.36,1.34-3.8,2-7.43,2-4.27,0-9.43-.88-14.91-1.81s-11.46-2-16.46-2c-4.07,0-7.17.66-9.5,2,0,0-3.9,2.28-5.56,5.23l1.62.73a33.56,33.56,0,0,1,6.93,5,33.68,33.68,0,0,0,7.19,5.12A6.37,6.37,0,0,1,42,180.72c-.69,1-1.69,2.29-2.74,3.67-5.77,7.55-9.13,12.32-7.2,14.92a6,6,0,0,0,3,.68c12.59,0,19.34-3.27,27.9-7.41,2.47-1.2,5-2.44,8-3.7,5-2.17,10.38-5.63,16.08-9.29,7.55-4.85,15.36-9.87,22.92-12.3a62.3,62.3,0,0,1,19.23-2.7c8,0,16.42,1.07,24.54,2.11,6.06.78,12.32,1.58,18.47,2,2.39.14,4.6.21,6.76.21a78.48,78.48,0,0,0,8.61-.45l.68-.24c4.32-2.65,6.34-8.34,8.29-13.84,1.26-3.54,2.32-6.72,4-8.74a2.06,2.06,0,0,1,.33-.27.4.4,0,0,1,.49.08.25.25,0,0,1,0,.16c-1,21.51-9.67,35.16-18.42,47.3L177,199.14s8.18,0,12.84-1.8c17-5.08,29.84-16.28,39.18-34.14a144.39,144.39,0,0,0,6.16-14.09c.16-.4,1.64-1.14,1.49.93,0,.61-.08,1.29-.13,2h0c0,.42-.06.85-.08,1.28-.25,3-1,9.34-1,9.34l5.25-2.81c12.66-8,22.42-24.14,29.82-49.25,3.09-10.46,5.34-20.85,7.33-30,2.38-11,4.43-20.43,6.78-24.09,3.69-5.74,9.32-9.62,14.77-13.39.75-.51,1.49-1,2.22-1.54,6.86-4.81,13.67-10.36,15.16-20.71l0-.23C317.93,12.92,317,11,316,10.05Z"
transform="translate(-7.45 -9.1)" transform="translate(-7.45 -9.1)"
/> />

View File

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

View File

@ -4,7 +4,7 @@
<svg <svg
xmlns="http://www.w3.org/2000/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'} class={isAbsolute ? 'w-16 h-16 absolute top-0 left-0 -m-7' : 'w-10 h-10 mx-auto'}
version="1.1" version="1.1"
viewBox="0 0 300 300" viewBox="0 0 300 300"
><linearGradient ><linearGradient

7
apps/ui/src/lib/dayjs.ts Normal file
View File

@ -0,0 +1,7 @@
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc.js';
import relativeTime from 'dayjs/plugin/relativeTime.js';
dayjs.extend(utc);
dayjs.extend(relativeTime);
export { dayjs as day };

View File

@ -88,7 +88,7 @@
"removing": "Removing...", "removing": "Removing...",
"remove_domain": "Remove domain", "remove_domain": "Remove domain",
"public_port_range": "Public Port Range", "public_port_range": "Public Port Range",
"public_port_range_explainer": "Ports used to expose databases/services/internal services.<br> Add them to your firewall (if applicable).<br><br>You can specify a range of ports, eg: <span class='text-settings font-bold'>9000-9100</span>", "public_port_range_explainer": "Ports used to expose databases/services/internal services.<br> Add them to your firewall (if applicable).<br><br>You can specify a range of ports, eg: <span class='text-settings '>9000-9100</span>",
"no_actions_available": "No actions available", "no_actions_available": "No actions available",
"admin_api_key": "Admin API key" "admin_api_key": "Admin API key"
}, },
@ -144,8 +144,8 @@
}, },
"preview": { "preview": {
"need_during_buildtime": "Need during buildtime?", "need_during_buildtime": "Need during buildtime?",
"setup_secret_app_first": "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.", "setup_secret_app_first": "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-settings '>staging</span> environments.",
"values_overwriting_app_secrets": "These values overwrite application secrets in PR/MR deployments. Useful for creating <span class='text-green-500 font-bold'>staging</span> environments.", "values_overwriting_app_secrets": "These values overwrite application secrets in PR/MR deployments. Useful for creating <span class='text-settings '>staging</span> environments.",
"redeploy": "Redeploy", "redeploy": "Redeploy",
"no_previews_available": "No previews available" "no_previews_available": "No previews available"
}, },
@ -159,7 +159,7 @@
"storage_saved": "Storage saved.", "storage_saved": "Storage saved.",
"storage_updated": "Storage updated.", "storage_updated": "Storage updated.",
"storage_deleted": "Storage deleted.", "storage_deleted": "Storage deleted.",
"persistent_storage_explainer": "You can specify any folder that you want to be persistent across deployments.<br><span class='text-green-500 font-bold'>/example</span> means it will preserve <span class='text-green-500 font-bold'>/app/example</span> in the container as <span class='text-green-500 font-bold'>/app</span> is <span class='text-green-500 font-bold'>the root directory</span> for your application.<br><br>This is useful for storing data such as a <span class='text-green-500 font-bold'>database (SQLite)</span> or a <span class='text-green-500 font-bold'>cache</span>." "persistent_storage_explainer": "You can specify any folder that you want to be persistent across deployments.<br><span class='text-settings '>/example</span> means it will preserve <span class='text-settings '>/app/example</span> in the container as <span class='text-settings '>/app</span> is <span class='text-settings '>the root directory</span> for your application.<br><br>This is useful for storing data such as a <span class='text-settings '>database (SQLite)</span> or a <span class='text-settings '>cache</span>."
}, },
"deployment_queued": "Deployment queued.", "deployment_queued": "Deployment queued.",
"confirm_to_delete": "Are you sure you would like to delete '{{name}}'?", "confirm_to_delete": "Are you sure you would like to delete '{{name}}'?",
@ -194,14 +194,14 @@
"application": "Application", "application": "Application",
"url_fqdn": "URL (FQDN)", "url_fqdn": "URL (FQDN)",
"domain_fqdn": "Domain (FQDN)", "domain_fqdn": "Domain (FQDN)",
"https_explainer": "If you specify <span class='text-green-500 font-bold'>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-green-500 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-white font-bold'>You must set your DNS to point to the server IP in advance.</span>", "https_explainer": "If you specify <span class='text-settings '>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-settings '>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-white '>You must set your DNS to point to the server IP in advance.</span>",
"ssl_www_and_non_www": "Generate SSL for www and non-www?", "ssl_www_and_non_www": "Generate SSL for www and non-www?",
"ssl_explainer": "It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-green-500'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both.", "ssl_explainer": "It will generate certificates for both www and non-www. <br>You need to have <span class=' text-settings'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both.",
"install_command": "Install Command", "install_command": "Install Command",
"build_command": "Build Command", "build_command": "Build Command",
"start_command": "Start Command", "start_command": "Start Command",
"directory_to_use_explainer": "Directory to use as the base for all commands.<br>Could be useful with <span class='text-green-500 font-bold'>monorepos</span>.", "directory_to_use_explainer": "Directory to use as the base for all commands.<br>Could be useful with <span class='text-settings '>monorepos</span>.",
"publish_directory_explainer": "Directory containing all the assets for deployment. <br> For example: <span class='text-green-500 font-bold'>dist</span>,<span class='text-green-500 font-bold'>_site</span> or <span class='text-green-500 font-bold'>public</span>.", "publish_directory_explainer": "Directory containing all the assets for deployment. <br> For example: <span class='text-settings '>dist</span>,<span class='text-settings '>_site</span> or <span class='text-settings '>public</span>.",
"features": "Features", "features": "Features",
"enable_automatic_deployment": "Enable Automatic Deployment", "enable_automatic_deployment": "Enable Automatic Deployment",
"enable_auto_deploy_webhooks": "Enable automatic deployment through webhooks.", "enable_auto_deploy_webhooks": "Enable automatic deployment through webhooks.",
@ -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-settings 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 '>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",
@ -223,7 +223,7 @@
"set_public": "Set it public", "set_public": "Set it public",
"warning_database_public": "Your database will be reachable over the internet. <br>Take security seriously in this case!", "warning_database_public": "Your database will be reachable over the internet. <br>Take security seriously in this case!",
"change_append_only_mode": "Change append only mode", "change_append_only_mode": "Change append only mode",
"warning_append_only": "Useful if you would like to restore redis data from a backup.<br><span class='font-bold text-white'>Database restart is required.</span>", "warning_append_only": "Useful if you would like to restore redis data from a backup.<br><span class=' text-white'>Database restart is required.</span>",
"select_database_type": "Select a Database type", "select_database_type": "Select a Database type",
"select_database_version": "Select a Database version", "select_database_version": "Select a Database version",
"confirm_stop": "Are you sure you would like to stop {{name}}?", "confirm_stop": "Are you sure you would like to stop {{name}}?",
@ -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-settings' >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=' 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)",
@ -290,7 +290,7 @@
}, },
"services": { "services": {
"all_email_verified": "All emails are verified. You can login now.", "all_email_verified": "All emails are verified. You can login now.",
"generate_www_non_www_ssl": "It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-pink-600'>both DNS entries</span> set in advance.<br><br>Service needs to be restarted." "generate_www_non_www_ssl": "It will generate certificates for both www and non-www. <br>You need to have <span class='text-settings'>both DNS entries</span> set in advance.<br><br>Service needs to be restarted."
}, },
"service": { "service": {
"stop_service": "Stop", "stop_service": "Stop",
@ -306,15 +306,15 @@
"change_language": "Change Language", "change_language": "Change Language",
"permission_denied": "You do not have permission to do this. \\nAsk an admin to modify your permissions.", "permission_denied": "You do not have permission to do this. \\nAsk an admin to modify your permissions.",
"domain_removed": "Domain removed", "domain_removed": "Domain removed",
"ssl_explainer": "If you specify <span class='text-settings font-bold'>https</span>, Coolify will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-settings font-bold'>www</span>, Coolify will be redirected (302) from non-www and vice versa.<br><br><span class='text-settings font-bold'>WARNING:</span> If you change an already set domain, it will break webhooks and other integrations! You need to manually update them.", "ssl_explainer": "If you specify <span class='text-settings'>https</span>, Coolify will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-settings '>www</span>, Coolify will be redirected (302) from non-www and vice versa.<br><br><span class='text-settings '>WARNING:</span> If you change an already set domain, it will break webhooks and other integrations! You need to manually update them.",
"must_remove_domain_before_changing": "Must remove the domain before you can change this setting.", "must_remove_domain_before_changing": "Must remove the domain before you can change this setting.",
"registration_allowed": "Registration allowed?", "registration_allowed": "Registration allowed?",
"registration_allowed_explainer": "Allow further registrations to the application. <br>It's turned off after the first registration.", "registration_allowed_explainer": "Allow further registrations to the application. <br>It's turned off after the first registration.",
"coolify_proxy_settings": "Coolify Proxy Settings", "coolify_proxy_settings": "Coolify Proxy Settings",
"credential_stat_explainer": "Credentials for <a class=\"text-white font-bold\" href=\"{{link}}\" target=\"_blank\">stats</a> page.", "credential_stat_explainer": "Credentials for <a class=\"text-white \" href=\"{{link}}\" target=\"_blank\">stats</a> page.",
"auto_update_enabled": "Auto update enabled?", "auto_update_enabled": "Auto update enabled?",
"auto_update_enabled_explainer": "Enable automatic updates for Coolify. It will be done automatically behind the scenes, if there is no build process running.", "auto_update_enabled_explainer": "Enable automatic updates for Coolify. It will be done automatically behind the scenes, if there is no build process running.",
"generate_www_non_www_ssl": "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.", "generate_www_non_www_ssl": "It will generate certificates for both www and non-www. <br>You need to have <span class=' text-settings'>both DNS entries</span> set in advance.",
"is_dns_check_enabled": "DNS check enabled?", "is_dns_check_enabled": "DNS check enabled?",
"is_dns_check_enabled_explainer": "You can disable DNS check before creating SSL certificates.<br><br>Turning it off is useful when Coolify is behind a reverse proxy or tunnel." "is_dns_check_enabled_explainer": "You can disable DNS check before creating SSL certificates.<br><br>Turning it off is useful when Coolify is behind a reverse proxy or tunnel."
}, },
@ -324,9 +324,9 @@
"delete": "Delete", "delete": "Delete",
"member": "member(s)", "member": "member(s)",
"root": "(root)", "root": "(root)",
"invited_with_permissions": "Invited to <span class=\"font-bold text-pink-600\">{{teamName}}</span> with <span class=\"font-bold text-rose-600\">{{permission}}</span> permission.", "invited_with_permissions": "Invited to <span class=\" text-settings\">{{teamName}}</span> with <span class=\" text-rose-600\">{{permission}}</span> permission.",
"members": "Members", "members": "Members",
"root_team_explainer": "This is the <span class='text-red-500 font-bold'>root</span> team. That means members of this group can manage instance wide settings and have all the priviliges in Coolify (imagine like root user on Linux).", "root_team_explainer": "This is the <span class='text-red-500 '>root</span> team. That means members of this group can manage instance wide settings and have all the priviliges in Coolify (imagine like root user on Linux).",
"permission": "Permission", "permission": "Permission",
"you": "(You)", "you": "(You)",
"promote_to": "Promote to {{grade}}", "promote_to": "Promote to {{grade}}",

View File

@ -26,7 +26,8 @@ interface AddToast {
message: string, message: string,
timeout?: number | undefined timeout?: number | undefined
} }
export const updateLoading: Writable<boolean> = writable(false);
export const isUpdateAvailable: Writable<boolean> = writable(false);
export const search: any = writable('') export const search: any = writable('')
export const loginEmail: Writable<string | undefined> = writable() export const loginEmail: Writable<string | undefined> = writable()
export const appSession: Writable<AppSession> = writable({ export const appSession: Writable<AppSession> = writable({
@ -73,6 +74,7 @@ export const status: Writable<any> = writable({
application: { application: {
isRunning: false, isRunning: false,
isExited: false, isExited: false,
isRestarting: false,
loading: false, loading: false,
initialLoading: true initialLoading: true
}, },
@ -156,3 +158,5 @@ export const addToast = (toast: AddToast) => {
if (t.timeout) t.timeoutInterval = setTimeout(() => dismissToast(id), t.timeout) if (t.timeout) t.timeoutInterval = setTimeout(() => dismissToast(id), t.timeout)
toasts.update((all: any) => [t, ...all]) toasts.update((all: any) => [t, ...all])
} }
export const selectedBuildId: any = writable(null)

View File

@ -16,7 +16,7 @@
} }
</script> </script>
<div class="dropdown dropdown-hover"> <div class="dropdown dropdown-bottom">
<slot> <slot>
<label for="new" tabindex="0" class="btn btn-square btn-sm bg-coollabs"> <label for="new" tabindex="0" class="btn btn-square btn-sm bg-coollabs">
<svg <svg

View File

@ -107,10 +107,11 @@
</script> </script>
<svelte:head> <svelte:head>
<title>Coolify</title>
{#if !$appSession.whiteLabeled} {#if !$appSession.whiteLabeled}
<title>Coolify</title>
<link rel="icon" href="/favicon.png" /> <link rel="icon" href="/favicon.png" />
{:else if $appSession.whiteLabeledDetails.icon} {:else if $appSession.whiteLabeledDetails.icon}
<title>Coolify</title>
<link rel="icon" href={$appSession.whiteLabeledDetails.icon} /> <link rel="icon" href={$appSession.whiteLabeledDetails.icon} />
{/if} {/if}
</svelte:head> </svelte:head>
@ -120,8 +121,11 @@
<PageLoader /> <PageLoader />
</div> </div>
{/if} {/if}
{#if $appSession.userId} <div class="drawer">
<nav class="nav-main"> <input id="main-drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer-content">
{#if $appSession.userId}
<nav class="nav-main hidden lg:block z-20">
<div class="flex h-screen w-full flex-col items-center transition-all duration-100"> <div class="flex h-screen w-full flex-col items-center transition-all duration-100">
{#if !$appSession.whiteLabeled} {#if !$appSession.whiteLabeled}
<div class="mb-2 mt-4 h-10 w-10"> <div class="mb-2 mt-4 h-10 w-10">
@ -137,8 +141,8 @@
id="dashboard" id="dashboard"
sveltekit:prefetch sveltekit:prefetch
href="/" href="/"
class="icons hover:text-white" class="icons hover:text-pink-500"
class:text-white={$page.url.pathname === '/'} class:text-pink-500={$page.url.pathname === '/'}
class:bg-coolgray-500={$page.url.pathname === '/'} class:bg-coolgray-500={$page.url.pathname === '/'}
class:bg-coolgray-200={!($page.url.pathname === '/')} class:bg-coolgray-200={!($page.url.pathname === '/')}
> >
@ -164,8 +168,8 @@
id="servers" id="servers"
sveltekit:prefetch sveltekit:prefetch
href="/servers" href="/servers"
class="icons hover:text-white" class="icons hover:text-sky-500"
class:text-white={$page.url.pathname === '/servers'} class:text-sky-500={$page.url.pathname === '/servers'}
class:bg-coolgray-500={$page.url.pathname === '/servers'} class:bg-coolgray-500={$page.url.pathname === '/servers'}
class:bg-coolgray-200={!($page.url.pathname === '/servers')} class:bg-coolgray-200={!($page.url.pathname === '/servers')}
> >
@ -191,17 +195,17 @@
<Tooltip triggeredBy="#dashboard" placement="right">Dashboard</Tooltip> <Tooltip triggeredBy="#dashboard" placement="right">Dashboard</Tooltip>
<Tooltip triggeredBy="#servers" placement="right">Servers</Tooltip> <Tooltip triggeredBy="#servers" placement="right">Servers</Tooltip>
<div class="flex-1" /> <div class="flex-1" />
<div class="lg:block hidden">
<UpdateAvailable /> <UpdateAvailable />
</div>
<div class="flex flex-col space-y-2 py-2"> <div class="flex flex-col space-y-2 py-2">
<a <a
id="iam" id="iam"
sveltekit:prefetch sveltekit:prefetch
href="/iam" href="/iam"
class="icons bg-coolgray-200" class="icons hover:text-iam"
class:text-iam={$page.url.pathname.startsWith('/iam')} class:text-iam={$page.url.pathname.startsWith('/iam')}
class:bg-coolgray-500={$page.url.pathname === '/iam'} class:bg-coolgray-500={$page.url.pathname.startsWith('/iam')}
class:bg-coolgray-200={!($page.url.pathname === '/iam')}
><svg ><svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -223,10 +227,9 @@
id="settings" 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 bg-coolgray-200" class="icons hover:text-settings"
class:text-settings={$page.url.pathname.startsWith('/settings')} class:text-settings={$page.url.pathname.startsWith('/settings')}
class:bg-coolgray-500={$page.url.pathname === '/settings'} class:bg-coolgray-500={$page.url.pathname.startsWith('/settings')}
class:bg-coolgray-200={!($page.url.pathname === '/settings')}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -246,7 +249,11 @@
</svg> </svg>
</a> </a>
<div id="logout" class="icons bg-coolgray-200 hover:text-error" on:click={logout}> <div
id="logout"
class="icons bg-coolgray-200 hover:text-error cursor-pointer"
on:click={logout}
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="ml-1 h-8 w-8" class="ml-1 h-8 w-8"
@ -281,14 +288,170 @@
>Powered by <a href="https://coolify.io" target="_blank">Coolify</a></span >Powered by <a href="https://coolify.io" target="_blank">Coolify</a></span
> >
{/if} {/if}
{/if} {/if}
<main> <div
<div class={$appSession.userId ? 'pl-14 lg:px-20' : null}> class="navbar lg:hidden space-x-2 flex flex-row items-center bg-coollabs"
class:hidden={!$appSession.userId}
>
<label for="main-drawer" class="drawer-button btn btn-square btn-ghost flex-col">
<span class="burger bg-white" />
<span class="burger bg-white" />
<span class="burger bg-white" />
</label>
<div class="prose flex flex-row justify-between space-x-1 w-full items-center pr-3">
{#if !$appSession.whiteLabeled}
<h3 class="mb-0 text-white">Coolify</h3>
{/if}
</div>
</div>
<main>
<div class={$appSession.userId ? 'lg:pl-16' : null}>
<slot /> <slot />
</div> </div>
</main> </main>
</div>
<div class="drawer-side">
<label for="main-drawer" class="drawer-overlay w-full" />
<ul class="menu bg-coolgray-300 w-60 p-2 space-y-3 pt-4 ">
<li>
<a
class="no-underline icons hover:text-white hover:bg-pink-500"
sveltekit:prefetch
href="/"
class:bg-pink-500={$page.url.pathname === '/'}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8"
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="M19 8.71l-5.333 -4.148a2.666 2.666 0 0 0 -3.274 0l-5.334 4.148a2.665 2.665 0 0 0 -1.029 2.105v7.2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-7.2c0 -.823 -.38 -1.6 -1.03 -2.105"
/>
<path d="M16 15c-2.21 1.333 -5.792 1.333 -8 0" />
</svg>
Dashboard
</a>
</li>
<li>
<a
class="no-underline icons hover:text-white hover:bg-sky-500"
sveltekit:prefetch
href="/servers"
class:bg-sky-500={$page.url.pathname.startsWith('/servers')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-8 h-8"
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="3" y="4" width="18" height="8" rx="3" />
<rect x="3" y="12" width="18" height="8" rx="3" />
<line x1="7" y1="8" x2="7" y2="8.01" />
<line x1="7" y1="16" x2="7" y2="16.01" />
</svg>
Servers
</a>
</li>
<li>
<a
class="no-underline icons hover:text-white hover:bg-iam"
href="/iam"
class:bg-iam={$page.url.pathname.startsWith('/iam')}
><svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-8 w-8"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="9" cy="7" r="4" />
<path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
</svg>
IAM
</a>
</li>
<li>
<a
class="no-underline icons hover:text-black hover:bg-settings"
href={$appSession.teamId === '0' ? '/settings/global' : '/settings/ssh-keys'}
class:bg-settings={$page.url.pathname.startsWith('/settings')}
class:text-black={$page.url.pathname.startsWith('/settings')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-8 w-8"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"
/>
<circle cx="12" cy="12" r="3" />
</svg>
Settings
</a>
</li>
<li class="flex-1 bg-transparent" />
<div class="block lg:hidden">
<UpdateAvailable />
</div>
<li>
<div class="no-underline icons hover:bg-error" on:click={logout}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="ml-1 h-8 w-8"
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="M14 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2"
/>
<path d="M7 12h14l-3 -3m0 6l3 -3" />
</svg>
<div class="-ml-1">Logout</div>
</div>
</li>
<li class="w-full">
<a
class="text-xs hover:bg-coolgray-200 no-underline hover:text-white text-right"
href={`https://github.com/coollabsio/coolify/releases/tag/v${$appSession.version}`}
target="_blank">v{$appSession.version}</a
>
</li>
</ul>
</div>
</div>
<Tooltip triggeredBy="#iam" placement="right" color="bg-iam">IAM</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="#settings" placement="right" color="bg-settings text-black">Settings</Tooltip>
>
<Tooltip triggeredBy="#logout" placement="right" color="bg-red-600">Logout</Tooltip> <Tooltip triggeredBy="#logout" placement="right" color="bg-red-600">Logout</Tooltip>

View File

@ -5,7 +5,6 @@
export let isNewSecret = false; export let isNewSecret = false;
export let isPRMRSecret = false; export let isPRMRSecret = false;
export let PRMRSecret: any = {}; export let PRMRSecret: any = {};
if (isPRMRSecret) value = PRMRSecret.value; if (isPRMRSecret) value = PRMRSecret.value;
import { page } from '$app/stores'; import { page } from '$app/stores';
@ -39,7 +38,15 @@
async function createSecret(isNew: any) { async function createSecret(isNew: any) {
try { try {
if (isNew) {
if (!name || !value) return; if (!name || !value) return;
}
if (value === undefined && isPRMRSecret) {
return
}
if (value === '' && !isPRMRSecret) {
throw new Error('Value is required.')
}
await saveSecret({ await saveSecret({
isNew, isNew,
name, name,
@ -93,6 +100,7 @@
<td> <td>
<input <input
style="min-width: 350px !important;"
id={isNewSecret ? 'secretName' : 'secretNameNew'} id={isNewSecret ? 'secretName' : 'secretNameNew'}
bind:value={name} bind:value={name}
required required
@ -108,8 +116,8 @@
name={isNewSecret ? 'secretValue' : 'secretValueNew'} name={isNewSecret ? 'secretValue' : 'secretValueNew'}
isPasswordField={true} isPasswordField={true}
bind:value bind:value
required
placeholder="J$#@UIO%HO#$U%H" placeholder="J$#@UIO%HO#$U%H"
inputStyle="min-width: 350px; !important"
/> />
</td> </td>
<td class="text-center"> <td class="text-center">
@ -130,7 +138,7 @@
class:translate-x-0={!isBuildSecret} class:translate-x-0={!isBuildSecret}
> >
<span <span
class=" absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in" class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
class:opacity-0={isBuildSecret} class:opacity-0={isBuildSecret}
class:opacity-100={!isBuildSecret} class:opacity-100={!isBuildSecret}
aria-hidden="true" aria-hidden="true"

View File

@ -67,7 +67,8 @@
setLocation, setLocation,
addToast, addToast,
isDeploymentEnabled, isDeploymentEnabled,
checkIfDeploymentEnabledApplications checkIfDeploymentEnabledApplications,
selectedBuildId
} from '$lib/store'; } from '$lib/store';
import { errorNotification, handlerNotFoundLoad } from '$lib/common'; import { errorNotification, handlerNotFoundLoad } from '$lib/common';
import Tooltip from '$lib/components/Tooltip.svelte'; import Tooltip from '$lib/components/Tooltip.svelte';
@ -89,13 +90,10 @@
message: $t('application.deployment_queued'), message: $t('application.deployment_queued'),
type: 'success' type: 'success'
}); });
if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) { $selectedBuildId = buildId;
return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`);
} else {
return await goto(`/applications/${id}/logs/build?buildId=${buildId}`, { return await goto(`/applications/${id}/logs/build?buildId=${buildId}`, {
replaceState: true replaceState: true
}); });
}
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} }
@ -107,7 +105,7 @@
$status.application.initialLoading = true; $status.application.initialLoading = true;
try { try {
await del(`/applications/${id}`, { id, force }); await del(`/applications/${id}`, { id, force });
return await goto(`/`, { replaceState: true }); return await window.location.assign(`/`);
} catch (error) { } catch (error) {
if (error.message.startsWith(`Command failed: SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid`)) { if (error.message.startsWith(`Command failed: SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid`)) {
forceDelete = true; forceDelete = true;
@ -154,6 +152,7 @@
const data = await get(`/applications/${id}/status`); const data = await get(`/applications/${id}/status`);
$status.application.isRunning = data.isRunning; $status.application.isRunning = data.isRunning;
$status.application.isExited = data.isExited; $status.application.isExited = data.isExited;
$status.application.isRestarting = data.isRestarting;
$status.application.loading = false; $status.application.loading = false;
$status.application.initialLoading = false; $status.application.initialLoading = false;
} }
@ -162,6 +161,7 @@
$status.application.initialLoading = true; $status.application.initialLoading = true;
$status.application.isRunning = false; $status.application.isRunning = false;
$status.application.isExited = false; $status.application.isExited = false;
$status.application.isRestarting = false;
$status.application.loading = false; $status.application.loading = false;
$location = null; $location = null;
$isDeploymentEnabled = false; $isDeploymentEnabled = false;
@ -171,6 +171,7 @@
setLocation(application, settings); setLocation(application, settings);
$status.application.isRunning = false; $status.application.isRunning = false;
$status.application.isExited = false; $status.application.isExited = false;
$status.application.isRestarting = false;
$status.application.loading = false; $status.application.loading = false;
if ( if (
application.gitSourceId && application.gitSourceId &&
@ -187,13 +188,81 @@
}); });
</script> </script>
<nav class="nav-side"> <nav class="header lg:flex-row flex-col-reverse">
{#if $location} <div class="flex flex-row space-x-2 font-bold pt-10 lg:pt-0">
<div class="flex flex-col items-center justify-center">
<div class="title">
{#if $page.url.pathname === `/applications/${id}`}
Configurations
{:else if $page.url.pathname === `/applications/${id}/secrets`}
Secrets
{:else if $page.url.pathname === `/applications/${id}/storages`}
Persistent Storages
{:else if $page.url.pathname === `/applications/${id}/previews`}
Preview Deployments
{:else if $page.url.pathname === `/applications/${id}/logs`}
Application Logs
{:else if $page.url.pathname === `/applications/${id}/logs/build`}
Build Logs
{:else if $page.url.pathname === `/applications/${id}/configuration/source`}
Select a Git Source
{:else if $page.url.pathname === `/applications/${id}/configuration/destination`}
Select a Destination
{:else if $page.url.pathname === `/applications/${id}/configuration/buildpack`}
Select a Build Pack
{/if}
</div>
</div>
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
<a <a
id="open" id="git"
href={$location} href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank" target="_blank"
class="icons flex items-center bg-transparent text-sm" class="w-6 h-6"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
<Tooltip triggeredBy="#git">Open on Git</Tooltip>
{/if}
</div>
<div class="lg:block hidden flex-1" />
<div class="flex flex-row flex-wrap space-x-3 justify-center lg:justify-start lg:py-0 px-12 lg:px-0">
{#if $location}
<a id="open" href={$location} target="_blank" class="icons bg-transparent"
><svg ><svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-6 w-6"
@ -212,14 +281,13 @@
> >
<Tooltip triggeredBy="#open">Open</Tooltip> <Tooltip triggeredBy="#open">Open</Tooltip>
<div class="border border-coolgray-500 h-8" /> <div class="hidden lg:block border border-coolgray-500 h-8" />
{/if} {/if}
{#if $status.application.isExited || $status.application.isRestarting}
{#if $status.application.isExited}
<a <a
id="applicationerror" id="applicationerror"
href={$isDeploymentEnabled ? `/applications/${id}/logs` : null} href={$isDeploymentEnabled ? `/applications/${id}/logs` : null}
class="icons bg-transparent text-sm flex items-center text-error" class="icons bg-transparent text-sm text-error"
sveltekit:prefetch sveltekit:prefetch
> >
<svg <svg
@ -243,9 +311,7 @@
<Tooltip triggeredBy="#applicationerror">Application exited with an error!</Tooltip> <Tooltip triggeredBy="#applicationerror">Application exited with an error!</Tooltip>
{/if} {/if}
{#if $status.application.initialLoading} {#if $status.application.initialLoading}
<button <button class="icons animate-spin bg-transparent duration-500 ease-in-out">
class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out hover:bg-transparent"
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-6 w-6"
@ -271,7 +337,7 @@
on:click={stopApplication} on:click={stopApplication}
type="submit" type="submit"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2 text-error" class="icons bg-transparent text-error"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -295,7 +361,7 @@
on:click={restartApplication} on:click={restartApplication}
type="submit" type="submit"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2" class="icons bg-transparent"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -319,7 +385,7 @@
id="forceredeploy" id="forceredeploy"
type="submit" type="submit"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2" class="icons bg-transparent"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -346,7 +412,7 @@
id="deploy" id="deploy"
type="submit" type="submit"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2 text-success" class="icons bg-transparent text-success"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -366,7 +432,7 @@
</form> </form>
{/if} {/if}
<div class="border border-coolgray-500 h-8" /> <div class="hidden lg:block border border-coolgray-500 h-8" />
<a <a
href={$isDeploymentEnabled ? `/applications/${id}` : null} href={$isDeploymentEnabled ? `/applications/${id}` : null}
sveltekit:prefetch sveltekit:prefetch
@ -492,9 +558,11 @@
> >
<Tooltip triggeredBy="#previews">Previews</Tooltip> <Tooltip triggeredBy="#previews">Previews</Tooltip>
{/if} {/if}
<div class="border border-coolgray-500 h-8" /> <div class="hidden lg:block border border-coolgray-500 h-8" />
<a <a
href={$isDeploymentEnabled && $status.application.isRunning ? `/applications/${id}/logs` : null} href={$isDeploymentEnabled && $status.application.isRunning
? `/applications/${id}/logs`
: null}
sveltekit:prefetch sveltekit:prefetch
class="hover:text-sky-500 rounded" class="hover:text-sky-500 rounded"
class:text-sky-500={$page.url.pathname === `/applications/${id}/logs`} class:text-sky-500={$page.url.pathname === `/applications/${id}/logs`}
@ -556,7 +624,7 @@
</button></a </button></a
> >
<Tooltip triggeredBy="#buildlogs">Build Logs</Tooltip> <Tooltip triggeredBy="#buildlogs">Build Logs</Tooltip>
<div class="border border-coolgray-500 h-8" /> <div class="hidden lg:block border border-coolgray-500 h-8" />
{#if forceDelete} {#if forceDelete}
<button <button
@ -570,7 +638,7 @@
> >
Force Delete Force Delete
</button> </button>
<Tooltip triggeredBy="#forcedelete">Force Delete</Tooltip> <Tooltip triggeredBy="#forcedelete" placement="left">Force Delete</Tooltip>
{:else} {:else}
<button <button
id="delete" id="delete"
@ -582,7 +650,8 @@
> >
<DeleteIcon /> <DeleteIcon />
</button> </button>
<Tooltip triggeredBy="#delete">Delete</Tooltip> <Tooltip triggeredBy="#delete" placement="left">Delete</Tooltip>
{/if} {/if}
</div>
</nav> </nav>
<slot /> <slot />

View File

@ -413,7 +413,7 @@
>{loading.save ? $t('forms.saving') : $t('forms.save')}</button >{loading.save ? $t('forms.saving') : $t('forms.save')}</button
> >
{#if tryAgain} {#if tryAgain}
<div> <div class="p-5">
An error occured during authenticating with GitLab. Please check your GitLab Source An error occured during authenticating with GitLab. Please check your GitLab Source
configuration <a href={`/sources/${application.gitSource.id}`}>here.</a> configuration <a href={`/sources/${application.gitSource.id}`}>here.</a>
</div> </div>

View File

@ -156,18 +156,21 @@
} }
</script> </script>
<div class="mx-auto max-w-5xl"> <div class="mx-auto max-w-6xl">
<div class="grid grid-flow-row gap-2 px-10"> <form
<div class="flex"> class="flex flex-col lg:flex-row w-full lg:px-32 space-y-5 lg:space-y-0 lg:space-x-5 justify-start"
<form class="flex" on:submit|preventDefault={loadBranches}> on:submit|preventDefault={loadBranches}
<div class="space-y-4"> >
<div class="space-y-2 w-full">
<input <input
class="w-full"
placeholder="eg: https://github.com/coollabsio/nodejs-example/tree/main" placeholder="eg: https://github.com/coollabsio/nodejs-example/tree/main"
bind:value={publicRepositoryLink} bind:value={publicRepositoryLink}
/> />
{#if branchSelectOptions.length > 0} {#if branchSelectOptions.length > 0}
<div class="custom-select-wrapper"> <div class="custom-select-wrapper">
<Select <Select
class="w-full"
placeholder={loading.branches placeholder={loading.branches
? $t('application.configuration.loading_branches') ? $t('application.configuration.loading_branches')
: !publicRepositoryLink : !publicRepositoryLink
@ -185,11 +188,8 @@
{/if} {/if}
</div> </div>
<button class="btn mx-4 bg-orange-600" class:loading={loading.branches} type="submit" <button class="btn bg-orange-600" class:loading={loading.branches} type="submit">
>Load Repository</button Load Repository
> </button>
</form> </form>
</div>
</div>
</div> </div>

View File

@ -254,12 +254,6 @@
}); });
</script> </script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">
{$t('application.configuration.configure_build_pack')}
</div>
</div>
{#if scanning} {#if scanning}
<div class="flex justify-center space-x-1 p-6 font-bold"> <div class="flex justify-center space-x-1 p-6 font-bold">
<div class="text-xl tracking-tight"> <div class="text-xl tracking-tight">
@ -267,18 +261,7 @@
</div> </div>
</div> </div>
{:else} {:else}
<div class="max-w-5xl mx-auto "> <div class="max-w-6xl mx-auto px-5">
<div class="title pb-2">Coolify</div>
<div class="flex flex-wrap justify-center">
{#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true) as buildPack}
<div class="p-2">
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
</div>
{/each}
</div>
</div>
<div class="max-w-5xl mx-auto ">
<div class="title pb-2">Other</div> <div class="title pb-2">Other</div>
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">
{#each buildPacks.filter((bp) => bp.isHerokuBuildPack === true) as buildPack} {#each buildPacks.filter((bp) => bp.isHerokuBuildPack === true) as buildPack}
@ -288,4 +271,14 @@
{/each} {/each}
</div> </div>
</div> </div>
<div class="max-w-6xl mx-auto px-5">
<div class="title pb-2">Coolify Custom</div>
<div class="flex flex-wrap justify-center">
{#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true) as buildPack}
<div class="p-2">
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
</div>
{/each}
</div>
</div>
{/if} {/if}

View File

@ -126,7 +126,7 @@
</div> </div>
{/if} {/if}
<div class="mx-auto max-w-4xl p-6"> <div class="mx-auto max-w-6xl p-6">
<div class="grid grid-flow-row gap-2 px-10"> <div class="grid grid-flow-row gap-2 px-10">
<div class="font-bold text-xl tracking-tight">Connect a Hosted / Remote Database</div> <div class="font-bold text-xl tracking-tight">Connect a Hosted / Remote Database</div>
<div class="mt-2 grid grid-cols-2 items-center px-4"> <div class="mt-2 grid grid-cols-2 items-center px-4">

View File

@ -63,19 +63,14 @@
}); });
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex flex-col justify-center w-full">
<div class="mr-4 text-2xl tracking-tight">
{$t('application.configuration.configure_destination')}
</div>
</div>
<div class="flex flex-col justify-center">
{#if !destinations || ownDestinations.length === 0} {#if !destinations || ownDestinations.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="pb-2 text-center font-bold"> <div class="pb-2 text-center font-bold">
{$t('application.configuration.no_configurable_destination')} {$t('application.configuration.no_configurable_destination')}
</div> </div>
<div class="flex justify-center"> <div class="flex justify-center">
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500"> <a href="/destinations/new" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
<svg <svg
class="w-6" class="w-6"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -93,7 +88,7 @@
</div> </div>
</div> </div>
{:else} {:else}
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row"> <div class="flex flex-col flex-wrap justify-center px-2 md:flex-row mx-auto">
{#each ownDestinations as destination} {#each ownDestinations as destination}
<div class="p-2"> <div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(destination.id)}> <form on:submit|preventDefault={() => handleSubmit(destination.id)}>
@ -106,9 +101,9 @@
{/each} {/each}
</div> </div>
{#if otherDestinations.length > 0 && $appSession.teamId === '0'} {#if otherDestinations.length > 0 && $appSession.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Destinations</div> <div class="px-6 pb-5 pt-10 title">Other Destinations</div>
{/if} {/if}
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row"> <div class="flex flex-col flex-wrap justify-center px-2 md:flex-row mx-auto">
{#each otherDestinations as destination} {#each otherDestinations as destination}
<div class="p-2"> <div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(destination.id)}> <form on:submit|preventDefault={() => handleSubmit(destination.id)}>

View File

@ -68,12 +68,7 @@
} }
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="max-w-6xl mx-auto px-5">
<div class="mr-4 text-2xl tracking-tight">
{$t('application.configuration.select_a_git_source')}
</div>
</div>
<div class="max-w-5xl mx-auto ">
<div class="title pb-8">Git App</div> <div class="title pb-8">Git App</div>
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">
{#if !filteredSources || ownSources.length === 0} {#if !filteredSources || ownSources.length === 0}
@ -192,7 +187,7 @@
</div> </div>
{/if} {/if}
</div> </div>
<div class="flex items-center"> <div class="flex flex-row items-center">
<div class="title py-4">Public Repository</div> <div class="title py-4">Public Repository</div>
<DocLink url="https://docs.coollabs.io/coolify/applications/#public-repository" /> <DocLink url="https://docs.coollabs.io/coolify/applications/#public-repository" />
</div> </div>

View File

@ -48,6 +48,7 @@
import Tooltip from '$lib/components/Tooltip.svelte'; import Tooltip from '$lib/components/Tooltip.svelte';
import Explainer from '$lib/components/Explainer.svelte'; import Explainer from '$lib/components/Explainer.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { fade } from 'svelte/transition';
const { id } = $page.params; const { id } = $page.params;
@ -94,7 +95,7 @@
} }
]; ];
function containerClass() { function containerClass() {
return 'text-white bg-transparent font-thin px-0'; return 'text-white bg-transparent font-thin px-0 w-full border-dashed border-coolgray-300';
} }
async function getUsage() { async function getUsage() {
@ -220,7 +221,6 @@
loading = true; loading = true;
try { try {
nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, ''); nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
console.log({debug: nonWWWDomain})
if (application.deploymentType) if (application.deploymentType)
application.deploymentType = application.deploymentType.toLowerCase(); application.deploymentType = application.deploymentType.toLowerCase();
!isBot && !isBot &&
@ -295,62 +295,7 @@
} }
</script> </script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold"> <div class="mx-auto max-w-6xl px-6 lg:my-0 my-4 lg:pt-0 pt-4 rounded" in:fade>
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Configuration
</div>
<span class="text-xs">{application.name}</span>
</div>
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
<a
id="git"
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
<Tooltip triggeredBy="#git">Open on Git</Tooltip>
{/if}
</div>
<div class="mx-auto max-w-4xl px-6 py-4">
<div class="text-2xl font-bold">Application Usage</div>
<div class="text-center"> <div class="text-center">
<div class="stat w-64"> <div class="stat w-64">
<div class="stat-title">Used Memory / Memory Limit</div> <div class="stat-title">Used Memory / Memory Limit</div>
@ -368,11 +313,11 @@
</div> </div>
</div> </div>
</div> </div>
<div class="mx-auto max-w-4xl px-6"> <div class="mx-auto max-w-6xl px-6 pb-12">
<!-- svelte-ignore missing-declaration --> <!-- svelte-ignore missing-declaration -->
<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 space-x-1 pb-5 items-center">
<div class="title">{$t('general')}</div> <h1 class="title">{$t('general')}</h1>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<button <button
class="btn btn-sm" class="btn btn-sm"
@ -385,18 +330,18 @@
> >
{/if} {/if}
</div> </div>
<div class="grid grid-flow-row gap-2 px-10">
<div class="grid grid-flow-row gap-2 lg:px-10 px-2 pr-5">
<div class="mt-2 grid grid-cols-2 items-center"> <div class="mt-2 grid grid-cols-2 items-center">
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label> <label for="name">{$t('forms.name')}</label>
<input name="name" id="name" bind:value={application.name} required /> <input name="name" id="name" class="w-full" bind:value={application.name} required />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="gitSource" class="text-base font-bold text-stone-100" <label for="gitSource">{$t('application.git_source')}</label>
>{$t('application.git_source')}</label
>
{#if isDisabled || application.settings.isPublicRepository} {#if isDisabled || application.settings.isPublicRepository}
<input <input
disabled={isDisabled || application.settings.isPublicRepository} disabled={isDisabled || application.settings.isPublicRepository}
class="w-full"
value={application.gitSource.name} value={application.gitSource.name}
/> />
{:else} {:else}
@ -406,17 +351,16 @@
><input ><input
value={application.gitSource.name} value={application.gitSource.name}
id="gitSource" id="gitSource"
class="cursor-pointer hover:bg-coolgray-500" class="cursor-pointer hover:bg-coolgray-500 w-full"
/></a /></a
> >
{/if} {/if}
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="repository" class="text-base font-bold text-stone-100" <label for="repository">{$t('application.git_repository')}</label>
>{$t('application.git_repository')}</label
>
{#if isDisabled || application.settings.isPublicRepository} {#if isDisabled || application.settings.isPublicRepository}
<input <input
class="w-full"
disabled={isDisabled || application.settings.isPublicRepository} disabled={isDisabled || application.settings.isPublicRepository}
value="{application.repository}/{application.branch}" value="{application.repository}/{application.branch}"
/> />
@ -427,46 +371,42 @@
><input ><input
value="{application.repository}/{application.branch}" value="{application.repository}/{application.branch}"
id="repository" id="repository"
class="cursor-pointer hover:bg-coolgray-500" class="cursor-pointer hover:bg-coolgray-500 w-full"
/></a /></a
> >
{/if} {/if}
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="buildPack" class="text-base font-bold text-stone-100" <label for="buildPack">{$t('application.build_pack')}</label>
>{$t('application.build_pack')}</label
>
{#if isDisabled} {#if isDisabled}
<input class="capitalize" disabled={isDisabled} value={application.buildPack} /> <input class="uppercase w-full" disabled={isDisabled} value={application.buildPack} />
{:else} {:else}
<a <a
href={`/applications/${id}/configuration/buildpack?from=/applications/${id}`} href={`/applications/${id}/configuration/buildpack?from=/applications/${id}`}
class="no-underline " class="no-underline"
> >
<input <input
value={application.buildPack} value={application.buildPack}
id="buildPack" id="buildPack"
class="cursor-pointer hover:bg-coolgray-500 capitalize" class="cursor-pointer hover:bg-coolgray-500 capitalize w-full"
/></a /></a
> >
{/if} {/if}
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="destination" class="text-base font-bold text-stone-100" <label for="destination">{$t('application.destination')}</label>
>{$t('application.destination')}</label
>
<div class="no-underline"> <div class="no-underline">
<input <input
value={application.destinationDocker.name} value={application.destinationDocker.name}
id="destination" id="destination"
disabled disabled
class="bg-transparent" class="bg-transparent w-full"
/> />
</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"> <div class="grid grid-cols-2 items-center">
<label for="baseBuildImage" class="text-base font-bold text-stone-100" <label for="baseBuildImage"
>{$t('application.base_build_image')} >{$t('application.base_build_image')}
<Explainer <Explainer
explanation={application.buildPack === 'laravel' explanation={application.buildPack === 'laravel'
@ -474,8 +414,6 @@
: 'Image that will be used during the build process.'} : 'Image that will be used during the build process.'}
/> />
</label> </label>
<div class="custom-select-wrapper">
<Select <Select
{isDisabled} {isDisabled}
containerClasses={isDisabled && containerClass()} containerClasses={isDisabled && containerClass()}
@ -487,11 +425,10 @@
isClearable={false} isClearable={false}
/> />
</div> </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"
>{$t('application.base_image')} >{$t('application.base_image')}
<Explainer explanation={'Image that will be used for the deployment.'} /></label <Explainer explanation={'Image that will be used for the deployment.'} /></label
> >
@ -511,7 +448,7 @@
{/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"
>Deployment Type >Deployment Type
<Explainer <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."} 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."}
@ -551,7 +488,7 @@
> >
{#if application.connectedDatabase} {#if application.connectedDatabase}
<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"
>Base Database >Base Database
<Explainer <Explainer
explanation={'The name of the database that will be used as base when branching.'} explanation={'The name of the database that will be used as base when branching.'}
@ -573,9 +510,9 @@
{/if} {/if}
</div> </div>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">{$t('application.application')}</div> <h1 class="title">{$t('application.application')}</h1>
</div> </div>
<div class="grid grid-flow-row gap-2 px-10"> <div class="grid grid-flow-row gap-2 lg:px-10 px-2 pr-5">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
id="isBot" id="isBot"
@ -595,13 +532,13 @@
isCenter={false} isCenter={false}
bind:setting={dualCerts} bind:setting={dualCerts}
title={$t('application.ssl_www_and_non_www')} 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." description="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')} on:click={() => !$status.application.isRunning && changeSettings('dualCerts')}
/> />
</div> </div>
{#if !isBot} {#if !isBot}
<div class="grid grid-cols-2 items-center pb-8"> <div class="grid grid-cols-2 items-center pb-8">
<label for="fqdn" class="text-base font-bold text-stone-100" <label for="fqdn"
>{$t('application.url_fqdn')} >{$t('application.url_fqdn')}
<Explainer <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>"} 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>"}
@ -609,6 +546,8 @@
</label> </label>
<div> <div>
<input <input
class="w-full"
required={!application.settings.isBot}
readonly={isDisabled} readonly={isDisabled}
disabled={isDisabled} disabled={isDisabled}
name="fqdn" name="fqdn"
@ -656,14 +595,14 @@
{/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">
<label for="pythonModule" class="text-base font-bold text-stone-100">WSGI / ASGI</label> <label for="pythonModule">WSGI / ASGI</label>
<div class="custom-select-wrapper"> <div class="custom-select-wrapper">
<Select id="wsgi" items={wsgis} on:select={selectWSGI} value={application.pythonWSGI} /> <Select id="wsgi" items={wsgis} on:select={selectWSGI} value={application.pythonWSGI} />
</div> </div>
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="pythonModule" class="text-base font-bold text-stone-100">Module</label> <label for="pythonModule">Module</label>
<input <input
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
@ -676,7 +615,7 @@
</div> </div>
{#if application.pythonWSGI?.toLowerCase() === 'gunicorn'} {#if application.pythonWSGI?.toLowerCase() === 'gunicorn'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="pythonVariable" class="text-base font-bold text-stone-100">Variable</label> <label for="pythonVariable">Variable</label>
<input <input
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
@ -690,7 +629,7 @@
{/if} {/if}
{#if application.pythonWSGI?.toLowerCase() === 'uvicorn'} {#if application.pythonWSGI?.toLowerCase() === 'uvicorn'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="pythonVariable" class="text-base font-bold text-stone-100">Variable</label> <label for="pythonVariable">Variable</label>
<input <input
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
@ -705,11 +644,12 @@
{/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" <label for="port"
>{$t('forms.port')} >{$t('forms.port')}
<Explainer explanation={'The port your application listens on.'} /></label <Explainer explanation={'The port your application listens on.'} /></label
> >
<input <input
class="w-full"
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="port" name="port"
@ -719,13 +659,14 @@
/> />
</div> </div>
{/if} {/if}
<div class="grid grid-cols-2 items-center pb-8"> <div class="grid grid-cols-2 items-center">
<label for="exposePort" class="text-base font-bold text-stone-100" <label for="exposePort"
>Exposed Port <Explainer >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.'} 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 /></label
> >
<input <input
class="w-full"
readonly={!$appSession.isAdmin && !$status.application.isRunning} readonly={!$appSession.isAdmin && !$status.application.isRunning}
disabled={isDisabled} disabled={isDisabled}
name="exposePort" name="exposePort"
@ -736,10 +677,9 @@
</div> </div>
{#if !notNodeDeployments.includes(application.buildPack)} {#if !notNodeDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="installCommand" class="text-base font-bold text-stone-100" <label for="installCommand">{$t('application.install_command')}</label>
>{$t('application.install_command')}</label
>
<input <input
class="w-full"
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="installCommand" name="installCommand"
@ -749,10 +689,9 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="buildCommand" class="text-base font-bold text-stone-100" <label for="buildCommand">{$t('application.build_command')}</label>
>{$t('application.build_command')}</label
>
<input <input
class="w-full"
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="buildCommand" name="buildCommand"
@ -762,10 +701,9 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center pb-8"> <div class="grid grid-cols-2 items-center pb-8">
<label for="startCommand" class="text-base font-bold text-stone-100" <label for="startCommand">{$t('application.start_command')}</label>
>{$t('application.start_command')}</label
>
<input <input
class="w-full"
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="startCommand" name="startCommand"
@ -777,12 +715,13 @@
{/if} {/if}
{#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"
>Dockerfile Location <Explainer >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>"} 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 /></label
> >
<input <input
class="w-full"
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="dockerFileLocation" name="dockerFileLocation"
@ -794,8 +733,9 @@
{/if} {/if}
{#if application.buildPack === 'deno'} {#if application.buildPack === 'deno'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="denoMainFile" class="text-base font-bold text-stone-100">Main File</label> <label for="denoMainFile">Main File</label>
<input <input
class="w-full"
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="denoMainFile" name="denoMainFile"
@ -805,12 +745,13 @@
/> />
</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" <label for="denoOptions"
>Arguments <Explainer >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."} explanation={"List of arguments to pass to <span class='text-settings font-bold'>deno run</span> command. Could include permissions, configurations files, etc."}
/></label /></label
> >
<input <input
class="w-full"
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="denoOptions" name="denoOptions"
@ -823,7 +764,7 @@
{#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"
>{$t('forms.base_directory')} >{$t('forms.base_directory')}
<Explainer <Explainer
explanation={"Directory to use as the base for all commands.<br>Could be useful with <span class='text-settings font-bold'>monorepos</span>."} explanation={"Directory to use as the base for all commands.<br>Could be useful with <span class='text-settings font-bold'>monorepos</span>."}
@ -831,6 +772,7 @@
> >
</div> </div>
<input <input
class="w-full"
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="baseDirectory" name="baseDirectory"
@ -843,7 +785,7 @@
{#if !notNodeDeployments.includes(application.buildPack)} {#if !notNodeDeployments.includes(application.buildPack)}
<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"
>{$t('forms.publish_directory')} >{$t('forms.publish_directory')}
<Explainer <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>."} 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>."}
@ -852,6 +794,7 @@
</div> </div>
<input <input
class="w-full"
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="publishDirectory" name="publishDirectory"
@ -867,7 +810,7 @@
<div class="flex space-x-1 pb-5 font-bold"> <div class="flex space-x-1 pb-5 font-bold">
<div class="title">{$t('application.features')}</div> <div class="title">{$t('application.features')}</div>
</div> </div>
<div class="px-10 pb-10"> <div class="lg:px-10 px-2 lg:pb-10 pb-6">
{#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
@ -892,7 +835,7 @@
/> />
</div> </div>
{/if} {/if}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center w-full">
<Setting <Setting
id="debug" id="debug"
isCenter={false} isCenter={false}

View File

@ -1,6 +1,4 @@
<script lang="ts"> <script lang="ts">
export let buildId: any;
import { createEventDispatcher, onDestroy, onMount } from 'svelte'; import { createEventDispatcher, onDestroy, onMount } from 'svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@ -11,6 +9,8 @@
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'; import Tooltip from '$lib/components/Tooltip.svelte';
import { day } from '$lib/dayjs';
import { selectedBuildId } from '$lib/store';
let logs: any = []; let logs: any = [];
let currentStatus: any; let currentStatus: any;
@ -18,7 +18,7 @@
let followingBuild: any; let followingBuild: any;
let followingInterval: any; let followingInterval: any;
let logsEl: any; let logsEl: any;
let fromDb = false;
let cancelInprogress = false; let cancelInprogress = false;
const { id } = $page.params; const { id } = $page.params;
@ -38,13 +38,18 @@
} }
async function streamLogs(sequence = 0) { async function streamLogs(sequence = 0) {
try { try {
let { logs: responseLogs, status } = await get( let {
`/applications/${id}/logs/build/${buildId}?sequence=${sequence}` logs: responseLogs,
); status,
fromDb: from
} = await get(`/applications/${id}/logs/build/${$selectedBuildId}?sequence=${sequence}`);
currentStatus = status; currentStatus = status;
logs = logs.concat( logs = logs.concat(
responseLogs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) })) responseLogs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) }))
); );
fromDb = from;
streamInterval = setInterval(async () => { streamInterval = setInterval(async () => {
if (status !== 'running' && status !== 'queued') { if (status !== 'running' && status !== 'queued') {
clearInterval(streamInterval); clearInterval(streamInterval);
@ -53,10 +58,11 @@
const nextSequence = logs[logs.length - 1]?.time || 0; const nextSequence = logs[logs.length - 1]?.time || 0;
try { try {
const data = await get( const data = await get(
`/applications/${id}/logs/build/${buildId}?sequence=${nextSequence}` `/applications/${id}/logs/build/${$selectedBuildId}?sequence=${nextSequence}`
); );
status = data.status; status = data.status;
currentStatus = status; currentStatus = status;
fromDb = data.fromDb;
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) }))
@ -75,7 +81,7 @@
try { try {
cancelInprogress = true; cancelInprogress = true;
await post(`/applications/${id}/cancel`, { await post(`/applications/${id}/cancel`, {
buildId, buildId: $selectedBuildId,
applicationId: id applicationId: id
}); });
} catch (error) { } catch (error) {
@ -103,7 +109,7 @@
<button <button
id="follow" id="follow"
on:click={followBuild} on:click={followBuild}
class="bg-transparent btn btn-sm btn-linkhover:text-green-500 hover:bg-coolgray-500" class="bg-transparent btn btn-sm btn-link hover:text-green-500 hover:bg-coolgray-500"
class:text-green-500={followingBuild} class:text-green-500={followingBuild}
> >
<svg <svg
@ -154,18 +160,17 @@
{/if} {/if}
</div> </div>
{#if logs.length > 0} {#if logs.length > 0}
<div <div class="font-mono w-full rounder bg-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded-md mb-20 flex flex-col whitespace-nowrap -mt-12 scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1">
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} {#each logs as log}
{#if fromDb}
<div>{log.line + '\n'}</div> <div>{log.line + '\n'}</div>
{:else}
<div>[{day.unix(log.time).format('HH:mm:ss.SSS')}] {log.line + '\n'}</div>
{/if}
{/each} {/each}
</div> </div>
{:else} {:else}
<div <div class="font-mono w-full rounder bg-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded-md mb-20 flex flex-col whitespace-nowrap -mt-12 scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1">
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. No logs found.
</div> </div>
{/if} {/if}

View File

@ -23,54 +23,45 @@
export let application: any; export let application: any;
export let buildCount: any; export let buildCount: any;
import { page } from '$app/stores'; import { page } from '$app/stores';
import { addToast, selectedBuildId } from '$lib/store';
import BuildLog from './_BuildLog.svelte'; import BuildLog from './_BuildLog.svelte';
import { get } from '$lib/api'; import { get, post } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { changeQueryParams, dateOptions, errorNotification } from '$lib/common'; import { changeQueryParams, dateOptions, errorNotification, asyncSleep } from '$lib/common';
import Tooltip from '$lib/components/Tooltip.svelte'; import Tooltip from '$lib/components/Tooltip.svelte';
import { day } from '$lib/dayjs';
import { onDestroy, onMount } from 'svelte';
const { id } = $page.params;
let buildId: any; let loadBuildLogsInterval: any = null;
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;
let preselectedBuildId = $page.url.searchParams.get('buildId'); let preselectedBuildId = $page.url.searchParams.get('buildId');
if (preselectedBuildId) buildId = preselectedBuildId; if (preselectedBuildId) $selectedBuildId = preselectedBuildId;
onMount(async () => {
getBuildLogs();
loadBuildLogsInterval = setInterval(() => {
getBuildLogs();
}, 2000);
async function updateBuildStatus({ detail }: { detail: any }) {
const { status, took } = detail;
if (status !== 'running') {
try {
const data = await get(`/applications/${id}/logs/build?buildId=${buildId}`);
builds = builds.filter((build: any) => {
if (build.id === data.builds[0].id) {
build.status = data.builds[0].status;
build.took = data.builds[0].took;
build.since = data.builds[0].since;
}
return build;
}); });
} catch (error) { onDestroy(() => {
return errorNotification(error); clearInterval(loadBuildLogsInterval);
}
} else {
builds = builds.filter((build: any) => {
if (build.id === buildId) build.status = status;
return build;
}); });
buildTook = took; async function getBuildLogs() {
} const response = await get(`/applications/${$page.params.id}/logs/build?skip=${skip}`);
builds = response.builds;
} }
async function loadMoreBuilds() { async function loadMoreBuilds() {
if (buildCount >= skip) { if (buildCount >= skip) {
skip = skip + 5; skip = skip + 5;
noMoreBuilds = buildCount >= skip; noMoreBuilds = buildCount <= skip;
try { try {
const data = await get(`/applications/${id}/logs/build?skip=${skip}`); const data = await get(`/applications/${id}/logs/build?skip=${skip}`);
builds = builds.concat(data.builds); builds = data.builds
return; return;
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
@ -80,64 +71,47 @@
} }
} }
function loadBuild(build: any) { function loadBuild(build: any) {
buildId = build; $selectedBuildId = build;
return changeQueryParams(buildId); return changeQueryParams($selectedBuildId);
}
async function resetQueue() {
const sure = confirm(
'It will reset all build queues for all applications. If something is queued, it will be canceled automatically. Are you sure? '
);
if (sure) {
try {
await post(`/internal/resetQueue`, {});
addToast({
message: 'Queue reset done.',
type: 'success'
});
await asyncSleep(500);
return window.location.reload();
} catch (error) {
return errorNotification(error);
}
}
}
function generateBadgeColors(status: string) {
if (status === 'failed') {
return 'text-red-500';
} else if (status === 'running') {
return 'text-yellow-300';
} else if (status === 'success') {
return 'text-green-500';
} else if (status === 'canceled') {
return 'text-orange-500';
} else {
return 'text-white';
}
} }
</script> </script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
{$t('application.build_logs')}
</div>
<span class="text-xs">{application.name} </span>
</div>
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
<a
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
{/if}
</div>
<div class="block flex-row justify-start space-x-2 px-5 pt-6 sm:px-10 md:flex"> <div class="block flex-row justify-start space-x-2 px-5 pt-6 sm:px-10 md:flex">
<div class="mb-4 min-w-[16rem] space-y-2 md:mb-0 "> <div class="mb-4 min-w-[16rem] space-y-2 md:mb-0 ">
<button class="btn btn-sm text-xs w-full bg-error" on:click={resetQueue}
>Reset Build Queue</button
>
<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
@ -145,12 +119,8 @@
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="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 py-4 no-underline transition-all duration-100 hover:bg-coolgray-300 hover:shadow-xl"
class:bg-coolgray-400={buildId === build.id} class:bg-coolgray-200={$selectedBuildId === build.id}
class:border-red-500={build.status === 'failed'}
class:border-orange-500={build.status === 'canceled'}
class:border-green-500={build.status === 'success'}
class:border-yellow-500={build.status === 'running'}
> >
<div class="flex-col px-2 text-center min-w-[10rem]"> <div class="flex-col px-2 text-center min-w-[10rem]">
<div class="text-sm font-bold"> <div class="text-sm font-bold">
@ -159,45 +129,55 @@
<div class="text-xs"> <div class="text-xs">
{build.type} {build.type}
</div> </div>
<div
class={`badge badge-sm text-xs uppercase rounded bg-coolgray-300 border-none font-bold ${generateBadgeColors(
build.status
)}`}
>
{build.status}
</div>
</div> </div>
<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> <div>
Elapsed <span class="font-bold text-xl"
<span class="font-bold">{buildTook}s</span> >{build.elapsed}s</span
>
</div> </div>
{:else if build.status === 'queued'} {:else if build.status !== 'queued'}
<div class="font-bold">{$t('application.build.queued')}</div> <div>{day(build.updatedAt).utc().fromNow()}</div>
{:else}
<div>{build.since}</div>
<div> <div>
{$t('application.build.finished_in')} <span class="font-bold">{build.took}s</span> {$t('application.build.finished_in')}
<span class="font-bold"
>{day(build.updatedAt).utc().diff(day(build.createdAt)) / 1000}s</span
>
</div> </div>
{/if} {/if}
</div> </div>
</div> </div>
<Tooltip triggeredBy={`#building-${build.id}`} <Tooltip triggeredBy={`#building-${build.id}`}
>{new Intl.DateTimeFormat('default', dateOptions).format(new Date(build.createdAt)) + >{new Intl.DateTimeFormat('default', dateOptions).format(new Date(build.createdAt)) +
`\n${build.status}`}</Tooltip `\n`}</Tooltip
> >
{/each} {/each}
</div> </div>
{#if !noMoreBuilds} {#if !noMoreBuilds}
{#if buildCount > 5} {#if buildCount > 5}
<div class="flex space-x-2"> <div class="flex space-x-2 pb-10">
<button disabled={noMoreBuilds} class=" btn btn-sm w-full" on:click={loadMoreBuilds} <button
>{$t('application.build.load_more')}</button disabled={noMoreBuilds}
class=" btn btn-sm w-full text-xs"
on:click={loadMoreBuilds}>{$t('application.build.load_more')}</button
> >
</div> </div>
{/if} {/if}
{/if} {/if}
</div> </div>
<div class="flex-1 md:w-96"> <div class="flex-1 md:w-96">
{#if buildId} {#if $selectedBuildId}
{#key buildId} {#key $selectedBuildId}
<svelte:component this={BuildLog} {buildId} on:updateBuildStatus={updateBuildStatus} /> <svelte:component this={BuildLog} />
{/key} {/key}
{/if} {/if}
</div> </div>

View File

@ -6,7 +6,8 @@
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'; import Tooltip from '$lib/components/Tooltip.svelte';
import { status } from '$lib/store';
import { goto } from '$app/navigation';
let application: any = {}; let application: any = {};
let logsLoading = false; let logsLoading = false;
let loadLogsInterval: any = null; let loadLogsInterval: any = null;
@ -16,7 +17,13 @@
let followingLogs: any; let followingLogs: any;
let logsEl: any; let logsEl: any;
let position = 0; let position = 0;
if (
!$status.application.isExited &&
!$status.application.isRestarting &&
!$status.application.isRunning
) {
goto(`/applications/${$page.params.id}/`, { replaceState: true });
}
const { id } = $page.params; const { id } = $page.params;
onMount(async () => { onMount(async () => {
const response = await get(`/applications/${id}`); const response = await get(`/applications/${id}`);
@ -84,57 +91,7 @@
} }
</script> </script>
<div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold">
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Application Logs
</div>
<span class="text-xs">{application.name}</span>
</div>
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
<a
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
{/if}
</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}
<div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div> <div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div>
@ -170,17 +127,11 @@
</button> </button>
<Tooltip triggeredBy="#follow">Follow Logs</Tooltip> <Tooltip triggeredBy="#follow">Follow Logs</Tooltip>
</div> </div>
<div <div class="font-mono w-full rounder bg-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded-md mb-20 flex flex-col whitespace-nowrap -mt-12 scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1">
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"
bind:this={logsEl}
on:scroll={detect}
>
<div class="px-2 pr-14">
{#each logs as log} {#each logs as log}
{log + '\n'} <p>{log + '\n'}</p>
{/each} {/each}
</div> </div>
</div> </div>
</div>
{/if} {/if}
</div> </div>

View File

@ -1,222 +0,0 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ stuff, url }) => {
try {
return {
props: {
application: stuff.application
}
};
} catch (error) {
return {
status: 500,
error: new Error(`Could not load ${url}`)
};
}
};
</script>
<script lang="ts">
export let application: any;
import Secret from './_Secret.svelte';
import { get, post } from '$lib/api';
import { page } from '$app/stores';
import { t } from '$lib/translations';
import { goto } from '$app/navigation';
import { errorNotification, getDomain } from '$lib/common';
import { onMount } from 'svelte';
import Loading from '$lib/components/Loading.svelte';
import { addToast } from '$lib/store';
import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
const { id } = $page.params;
let containers: any;
let PRMRSecrets: any;
let applicationSecrets: any;
let loading = {
init: true,
removing: false
};
async function refreshSecrets() {
const data = await get(`/applications/${id}/secrets`);
PRMRSecrets = [...data.secrets];
}
async function removeApplication(container: any) {
try {
loading.removing = true;
await post(`/applications/${id}/stop/preview`, {
pullmergeRequestId: container.pullmergeRequestId
});
return window.location.reload();
} catch (error) {
return errorNotification(error);
}
}
async function redeploy(container: any) {
try {
const { buildId } = await post(`/applications/${id}/deploy`, {
pullmergeRequestId: container.pullmergeRequestId,
branch: container.branch
});
addToast({
message: 'Deployment queued',
type: 'success'
});
if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) {
return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`);
} else {
return await goto(`/applications/${id}/logs/build?buildId=${buildId}`, {
replaceState: true
});
}
} catch (error) {
return errorNotification(error);
}
}
onMount(async () => {
try {
loading.init = true;
const response = await get(`/applications/${id}/previews`);
containers = response.containers;
PRMRSecrets = response.PRMRSecrets;
applicationSecrets = response.applicationSecrets;
} catch (error) {
return errorNotification(error);
} finally {
loading.init = false;
}
});
</script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Preview Deployments
</div>
<span class="text-xs">{application?.name}</span>
</div>
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
<a
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
{/if}
</div>
{#if loading.init}
<Loading />
{:else}
<div class="mx-auto max-w-6xl px-6 pt-4">
<div class="flex justify-center py-4 text-center">
<SimpleExplainer
customClass="w-full"
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."
: "These values overwrite application secrets in PR/MR deployments. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."}
/>
</div>
{#if applicationSecrets.length !== 0}
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
<th scope="col">{$t('forms.name')}</th>
<th scope="col">{$t('forms.value')}</th>
<th scope="col" class="w-64 text-center"
>{$t('application.preview.need_during_buildtime')}</th
>
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
</tr>
</thead>
<tbody>
{#each applicationSecrets as secret}
{#key secret.id}
<tr>
<Secret
PRMRSecret={PRMRSecrets.find((s) => s.name === secret.name)}
isPRMRSecret
name={secret.name}
value={secret.value}
isBuildSecret={secret.isBuildSecret}
on:refresh={refreshSecrets}
/>
</tr>
{/key}
{/each}
</tbody>
</table>
{/if}
</div>
<div class="mx-auto max-w-4xl py-10">
<div class="flex flex-wrap justify-center space-x-2">
{#if containers.length > 0}
{#each containers as container}
<a href={container.fqdn} class="p-2 no-underline" target="_blank">
<div class="box-selection text-center hover:border-transparent hover:bg-green-600">
<div class="truncate text-center text-xl font-bold">{getDomain(container.fqdn)}</div>
</div>
</a>
<div class="flex items-center justify-center">
<button
class="btn btn-sm bg-coollabs hover:bg-coollabs-100"
on:click={() => redeploy(container)}>{$t('application.preview.redeploy')}</button
>
</div>
<div class="flex items-center justify-center">
<button
class="btn btn-sm"
class:bg-red-600={!loading.removing}
class:hover:bg-red-500={!loading.removing}
disabled={loading.removing}
on:click={() => removeApplication(container)}
>{loading.removing ? 'Removing...' : 'Remove Application'}
</button>
</div>
{/each}
{:else}
<div class="flex-col">
<div class="text-center font-bold text-xl">
{$t('application.preview.no_previews_available')}
</div>
</div>
{/if}
</div>
</div>
{/if}

View File

@ -0,0 +1,376 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, stuff, url }) => {
try {
return {
props: {
application: stuff.application
}
};
} catch (error) {
return {
status: 500,
error: new Error(`Could not load ${url}`)
};
}
};
</script>
<script lang="ts">
export let application: any;
import Secret from '../_Secret.svelte';
import { get, post } from '$lib/api';
import { page } from '$app/stores';
import { t } from '$lib/translations';
import { goto } from '$app/navigation';
import { asyncSleep, errorNotification, getDomain, getRndInteger } from '$lib/common';
import { onDestroy, onMount } from 'svelte';
import { addToast } from '$lib/store';
import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import Explainer from '$lib/components/Explainer.svelte';
const { id } = $page.params;
let loadBuildingStatusInterval: any = null;
let PRMRSecrets: any;
let applicationSecrets: any;
let loading = {
init: true,
restart: false,
removing: false
};
let numberOfGetStatus = 0;
let status: any = {};
async function refreshSecrets() {
const data = await get(`/applications/${id}/secrets`);
PRMRSecrets = [...data.secrets];
}
async function removeApplication(preview: any) {
try {
loading.removing = true;
await post(`/applications/${id}/stop/preview`, {
pullmergeRequestId: preview.pullmergeRequestId
});
return window.location.reload();
} catch (error) {
return errorNotification(error);
}
}
async function redeploy(preview: any) {
try {
const { buildId } = await post(`/applications/${id}/deploy`, {
pullmergeRequestId: preview.pullmergeRequestId,
branch: preview.sourceBranch
});
addToast({
message: 'Deployment queued',
type: 'success'
});
if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) {
return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`);
} else {
return await goto(`/applications/${id}/logs/build?buildId=${buildId}`, {
replaceState: true
});
}
} catch (error) {
return errorNotification(error);
}
}
async function loadPreviewsFromDocker() {
try {
const { previews } = await post(`/applications/${id}/previews/load`, {});
addToast({
message: 'Previews loaded.',
type: 'success'
});
application.previewApplication = previews;
} catch (error) {
return errorNotification(error);
}
}
async function getStatus(resources: any) {
const { applicationId, pullmergeRequestId, id } = resources;
if (status[id]) return status[id];
while (numberOfGetStatus > 1) {
await asyncSleep(getRndInteger(100, 200));
}
try {
numberOfGetStatus++;
let isRunning = false;
let isBuilding = false;
const response = await get(
`/applications/${applicationId}/previews/${pullmergeRequestId}/status`
);
isRunning = response.isRunning;
isBuilding = response.isBuilding;
if (isBuilding) {
status[id] = 'building';
return 'building';
} else if (isRunning) {
status[id] = 'running';
return 'running';
} else {
status[id] = 'stopped';
return 'stopped';
}
} catch (error) {
status[id] = 'error';
return 'error';
} finally {
numberOfGetStatus--;
status = status;
}
}
async function restartPreview(preview: any) {
try {
loading.restart = true;
const { pullmergeRequestId } = preview;
await post(`/applications/${id}/previews/${pullmergeRequestId}/restart`, {});
addToast({
type: 'success',
message: 'Restart successful.'
});
} catch (error) {
return errorNotification(error);
} finally {
await getStatus(preview);
loading.restart = false;
}
}
onDestroy(() => {
clearInterval(loadBuildingStatusInterval);
});
onMount(async () => {
loadBuildingStatusInterval = setInterval(() => {
application.previewApplication.forEach(async (preview: any) => {
const { applicationId, pullmergeRequestId } = preview;
if (status[preview.id] === 'building') {
const response = await get(
`/applications/${applicationId}/previews/${pullmergeRequestId}/status`
);
if (response.isBuilding) {
status[preview.id] = 'building';
} else if (response.isRunning) {
status[preview.id] = 'running';
return 'running';
} else {
status[preview.id] = 'stopped';
return 'stopped';
}
}
});
}, 2000);
try {
loading.init = true;
loading.restart = true;
const response = await get(`/applications/${id}/previews`);
PRMRSecrets = response.PRMRSecrets;
applicationSecrets = response.applicationSecrets;
} catch (error) {
return errorNotification(error);
} finally {
loading.init = false;
loading.restart = false;
}
});
</script>
<div class="flex justify-center">
<SimpleExplainer
text={applicationSecrets && applicationSecrets.length === 0
? "To have Preview Secerts, please add them to the main application. <br><br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."
: "These values overwrite application secrets in PR/MR deployments. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."}
/>
</div>
{#if loading.init}
<div class="mx-auto max-w-6xl px-6 pt-4">
<div class="flex justify-center py-4 text-center text-xl font-bold">Loading...</div>
</div>
{:else}
<div class="mx-auto max-w-6xl px-6 pt-4">
<div class="text-center">
<button class="btn btn-sm bg-coollabs" on:click={loadPreviewsFromDocker}>Load Previews</button
>
</div>
{#if applicationSecrets.length !== 0}
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
<th scope="col">{$t('forms.name')}</th>
<th scope="col">{$t('forms.value')}</th>
<th scope="col" class="w-64 text-center"
>{$t('application.preview.need_during_buildtime')}</th
>
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
</tr>
</thead>
<tbody>
{#each applicationSecrets as secret}
{#key secret.id}
<tr>
<Secret
PRMRSecret={PRMRSecrets.find((s) => s.name === secret.name)}
isPRMRSecret
name={secret.name}
value={secret.value}
isBuildSecret={secret.isBuildSecret}
on:refresh={refreshSecrets}
/>
</tr>
{/key}
{/each}
</tbody>
</table>
{/if}
</div>
<div class="container lg:mx-auto lg:p-0 px-8 p-5 lg:pt-10">
{#if application.previewApplication.length > 0}
<div
class="grid grid-col gap-8 auto-cols-max grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 p-4"
>
{#each application.previewApplication as preview}
<div class="no-underline mb-5">
<div class="w-full rounded p-5 bg-coolgray-200 indicator">
{#await getStatus(preview)}
<span class="indicator-item badge bg-yellow-500 badge-sm" />
{:then}
{#if status[preview.id] === 'running'}
<span class="indicator-item badge bg-success badge-sm" />
{:else}
<span class="indicator-item badge bg-error badge-sm" />
{/if}
{/await}
<div class="w-full flex flex-row">
<div class="w-full flex flex-col">
<h1 class="font-bold text-lg lg:text-xl truncate">
PR #{preview.pullmergeRequestId}
{#if status[preview.id] === 'building'}
<span
class="badge badge-sm text-xs uppercase rounded bg-coolgray-300 text-green-500 border-none font-bold"
>
BUILDING
</span>
{/if}
</h1>
<div class="h-10 text-xs">
<h2>{preview.customDomain.replace('https://', '').replace('http://', '')}</h2>
</div>
<div class="flex justify-end items-end space-x-2 h-10">
{#if preview.customDomain}
<a id="openpreview" href={preview.customDomain} target="_blank" class="icons">
<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}
<Tooltip triggeredBy="#openpreview">Open Preview</Tooltip>
<div class="border border-coolgray-500 h-8" />
{#if loading.restart}
<button
class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out hover:bg-transparent"
>
<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}
<button
id="restart"
on:click={() => restartPreview(preview)}
type="submit"
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>
{/if}
<Tooltip triggeredBy="#restart">Restart (useful to change secrets)</Tooltip>
<button
id="forceredeploypreview"
class="icons"
on:click={() => redeploy(preview)}
>
<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="#forceredeploypreview"
>Force redeploy (without cache)</Tooltip
>
<div class="border border-coolgray-500 h-8" />
<button
id="deletepreview"
class="icons"
class:hover:text-error={!loading.removing}
disabled={loading.removing}
on:click={() => removeApplication(preview)}
><DeleteIcon />
</button>
<Tooltip triggeredBy="#deletepreview">Delete Preview</Tooltip>
</div>
</div>
</div>
</div>
</div>
{/each}
</div>
{/if}
</div>
{/if}

View File

@ -67,58 +67,8 @@
} }
</script> </script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
{$t('application.secret')}
</div>
<span class="text-xs">{application.name} </span>
</div>
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
<a
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
{/if}
</div>
<div class="mx-auto max-w-6xl px-6 pt-4"> <div class="mx-auto max-w-6xl px-6 pt-4">
<div class="overflow-x-auto">
<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">
@ -148,6 +98,7 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div>
<h2 class="title my-6 font-bold">Paste .env file</h2> <h2 class="title my-6 font-bold">Paste .env file</h2>
<form on:submit|preventDefault={getValues} class="mb-12 w-full"> <form on:submit|preventDefault={getValues} class="mb-12 w-full">
<textarea bind:value={batchSecrets} class="mb-2 min-h-[200px] w-full" /> <textarea bind:value={batchSecrets} class="mb-2 min-h-[200px] w-full" />

View File

@ -5,7 +5,6 @@
const response = await get(`/applications/${params.id}/storages`); const response = await get(`/applications/${params.id}/storages`);
return { return {
props: { props: {
application: stuff.application,
...response ...response
} }
}; };
@ -19,13 +18,12 @@
</script> </script>
<script lang="ts"> <script lang="ts">
export let application: any;
export let persistentStorages: any; export let persistentStorages: any;
import { page } from '$app/stores'; import { page } from '$app/stores';
import Storage from './_Storage.svelte'; import Storage from './_Storage.svelte';
import { get } from '$lib/api'; import { get } from '$lib/api';
import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import Explainer from '$lib/components/Explainer.svelte';
const { id } = $page.params; const { id } = $page.params;
async function refreshStorage() { async function refreshStorage() {
@ -34,66 +32,11 @@
} }
</script> </script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Persistent Storage
</div>
<span class="text-xs">{application.name} </span>
</div>
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
<a
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
{/if}
</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">
<th scope="col">{$t('forms.path')}</th> <th scope="col">{$t('forms.path')} <Explainer position="dropdown-bottom" explanation={$t('application.storage.persistent_storage_explainer')} /></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@ -22,7 +22,7 @@ export async function saveSecret({
applicationId applicationId
}: Props): Promise<void> { }: Props): Promise<void> {
if (!name) return errorNotification(`${t.get('forms.name')} ${t.get('forms.is_required')}`); 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')}`); if (!value && isNew) return errorNotification(`${t.get('forms.value')} ${t.get('forms.is_required')}`);
try { try {
await post(`/applications/${applicationId}/secrets`, { await post(`/applications/${applicationId}/secrets`, {
name, name,

View File

@ -44,8 +44,8 @@
} }
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <nav class="header">
<div class="mr-4 text-2xl">{$t('index.applications')}</div> <h1 class="mr-4 text-2xl font-bold">{$t('index.applications')}</h1>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<button on:click={newApplication} class="btn btn-square btn-sm bg-applications"> <button on:click={newApplication} class="btn btn-square btn-sm bg-applications">
<svg <svg
@ -63,8 +63,9 @@
> >
</button> </button>
{/if} {/if}
</div> </nav>
<div class="flex-col justify-center mt-10 pb-12 sm:pb-16"> <br />
<div class="flex flex-col justify-center mt-10 pb-12 lg:pt-16 sm:pb-16">
{#if !applications || ownApplications.length === 0} {#if !applications || ownApplications.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="text-center text-xl font-bold">{$t('application.no_applications_found')}</div> <div class="text-center text-xl font-bold">{$t('application.no_applications_found')}</div>

View File

@ -5,13 +5,11 @@
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">CouchDB</div> <h1 class="title">CouchDB</h1>
</div> </div>
<div class="space-y-2 px-10"> <div class="space-y-2 lg:px-10 px-2">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="defaultDatabase" class="text-base font-bold text-stone-100" <label for="defaultDatabase">{$t('database.default_database')}</label>
>{$t('database.default_database')}</label
>
<CopyPasswordField <CopyPasswordField
required required
readonly={database.defaultDatabase} readonly={database.defaultDatabase}
@ -23,7 +21,7 @@
/> />
</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">{$t('forms.user')}</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -34,9 +32,7 @@
/> />
</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">{$t('forms.password')}</label>
>{$t('forms.password')}</label
>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -48,7 +44,7 @@
/> />
</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">{$t('forms.root_user')}</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -59,9 +55,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">{$t('forms.roots_password')}</label>
>{$t('forms.roots_password')}</label
>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled

View File

@ -107,10 +107,10 @@
} }
</script> </script>
<div class="mx-auto max-w-4xl px-6"> <div class="mx-auto max-w-6xl p-4">
<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 space-x-1 pb-5 items-center">
<div class="title">{$t('general')}</div> <h1 class="title">{$t('general')}</h1>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<button <button
type="submit" type="submit"
@ -121,22 +121,17 @@
> >
{/if} {/if}
</div> </div>
<div class="grid gap-4 grid-cols-2 auto-rows-max lg:px-10 px-2">
<div class="grid grid-flow-row gap-2 px-10"> <label for="name">{$t('forms.name')}</label>
<div class="grid grid-cols-2 items-center">
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
<input <input
class="w-full"
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="name" name="name"
id="name" id="name"
bind:value={database.name} bind:value={database.name}
required required
/> />
</div> <label for="destination">{$t('application.destination')}</label>
<div class="grid grid-cols-2 items-center">
<label for="destination" class="text-base font-bold text-stone-100"
>{$t('application.destination')}</label
>
{#if database.destinationDockerId} {#if database.destinationDockerId}
<div class="no-underline"> <div class="no-underline">
<input <input
@ -144,14 +139,11 @@
id="destination" id="destination"
disabled disabled
readonly readonly
class="bg-transparent " class="bg-transparent w-full"
/> />
</div> </div>
{/if} {/if}
</div> <label for="version">Version / Tag</label>
<div class="grid grid-cols-2 items-center">
<label for="version" class="text-base font-bold text-stone-100">Version / Tag</label>
<a <a
href={$appSession.isAdmin && !$status.database.isRunning href={$appSession.isAdmin && !$status.database.isRunning
? `/databases/${id}/configuration/version?from=/databases/${id}` ? `/databases/${id}/configuration/version?from=/databases/${id}`
@ -159,18 +151,14 @@
class="no-underline" class="no-underline"
> >
<input <input
class="w-full"
value={database.version} value={database.version}
readonly readonly
disabled={$status.database.isRunning || $status.database.initialLoading} disabled={$status.database.isRunning || $status.database.initialLoading}
class:cursor-pointer={!$status.database.isRunning} class:cursor-pointer={!$status.database.isRunning}
/></a /></a
> >
</div> <label for="host">{$t('forms.host')}</label>
</div>
<div class="grid grid-flow-row gap-2 px-10 pt-2">
<div class="grid grid-cols-2 items-center">
<label for="host" class="text-base font-bold text-stone-100">{$t('forms.host')}</label>
<CopyPasswordField <CopyPasswordField
placeholder={$t('forms.generated_automatically_after_start')} placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField={false} isPasswordField={false}
@ -180,21 +168,20 @@
name="host" name="host"
value={database.id} value={database.id}
/> />
</div> <label for="publicPort">{$t('forms.port')}</label>
<div class="grid grid-cols-2 items-center">
<label for="publicPort" class="text-base font-bold text-stone-100">{$t('forms.port')}</label
>
<CopyPasswordField <CopyPasswordField
placeholder={$t('database.generated_automatically_after_set_to_public')} placeholder={$t('database.generated_automatically_after_set_to_public')}
id="publicPort" id="publicPort"
readonly readonly
disabled disabled
name="publicPort" name="publicPort"
value={publicLoading ? 'Loading...' : $status.database.isPublic ? database.publicPort : privatePort} value={publicLoading
? 'Loading...'
: $status.database.isPublic
? database.publicPort
: privatePort}
/> />
</div> </div>
</div>
<div class="grid grid-flow-row gap-2">
{#if database.type === 'mysql'} {#if database.type === 'mysql'}
<MySql bind:database /> <MySql bind:database />
{:else if database.type === 'postgresql'} {:else if database.type === 'postgresql'}
@ -210,9 +197,9 @@
{:else if database.type === 'edgedb'} {:else if database.type === 'edgedb'}
<EdgeDB {database} /> <EdgeDB {database} />
{/if} {/if}
<div class="grid grid-cols-2 items-center px-10 pb-8"> <div class="flex flex-col space-y-2 mt-5">
<div> <div>
<label for="url" class="text-base font-bold text-stone-100" <label class="px-2" for="url"
>{$t('database.connection_string')} >{$t('database.connection_string')}
{#if !$status.database.isPublic && database.destinationDocker.remoteEngine} {#if !$status.database.isPublic && database.destinationDocker.remoteEngine}
<Explainer <Explainer
@ -221,6 +208,7 @@
{/if}</label {/if}</label
> >
</div> </div>
<div class="lg:px-10 px-2">
<CopyPasswordField <CopyPasswordField
textarea={true} textarea={true}
placeholder={$t('forms.generated_automatically_after_start')} placeholder={$t('forms.generated_automatically_after_start')}
@ -235,10 +223,9 @@
</div> </div>
</form> </form>
<div class="flex space-x-1 pb-5 font-bold"> <div class="flex space-x-1 pb-5 font-bold">
<div class="title">{$t('application.features')}</div> <h1 class="title">{$t('application.features')}</h1>
</div> </div>
<div class="px-10 pb-10"> <div class="grid gap-4 grid-cols-2 auto-rows-max lg:px-10 px-2">
<div class="grid grid-cols-2 items-center">
<Setting <Setting
id="isPublic" id="isPublic"
loading={publicLoading} loading={publicLoading}
@ -248,9 +235,7 @@
description={$t('database.warning_database_public')} description={$t('database.warning_database_public')}
disabled={!$status.database.isRunning} disabled={!$status.database.isRunning}
/> />
</div>
{#if database.type === 'redis'} {#if database.type === 'redis'}
<div class="grid grid-cols-2 items-center">
<Setting <Setting
id="appendOnly" id="appendOnly"
loading={publicLoading} loading={publicLoading}
@ -259,7 +244,6 @@
title={$t('database.change_append_only_mode')} title={$t('database.change_append_only_mode')}
description={$t('database.warning_append_only')} description={$t('database.warning_append_only')}
/> />
</div>
{/if} {/if}
</div> </div>
</div> </div>

View File

@ -9,11 +9,9 @@
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">EdgeDB</div> <div class="title">EdgeDB</div>
</div> </div>
<div class="space-y-2 px-10"> <div class="space-y-2 lg:px-10 px-2">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="defaultDatabase" class="text-base font-bold text-stone-100" <label for="defaultDatabase">{$t('database.default_database')}</label>
>{$t('database.default_database')}</label
>
<CopyPasswordField <CopyPasswordField
required required
readonly={database.defaultDatabase} readonly={database.defaultDatabase}
@ -25,7 +23,7 @@
/> />
</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">{$t('forms.root_user')}</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -36,7 +34,7 @@
/> />
</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"
>Root Password <Explainer >Root Password <Explainer
explanation="Could be changed while the database is running." explanation="Could be changed while the database is running."
/></label /></label

View File

@ -7,11 +7,11 @@
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">MariaDB</div> <h1 class="title">MariaDB</h1>
</div> </div>
<div class="space-y-2 px-10"> <div class="space-y-2 lg:px-10 px-2">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="defaultDatabase" class="text-base font-bold text-stone-100" <label for="defaultDatabase"
>{$t('database.default_database')}</label >{$t('database.default_database')}</label
> >
<CopyPasswordField <CopyPasswordField
@ -25,7 +25,7 @@
/> />
</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" >{$t('forms.user')}</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -36,7 +36,7 @@
/> />
</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"
>{$t('forms.password')} >{$t('forms.password')}
<Explainer explanation="Could be changed while the database is running." /></label <Explainer explanation="Could be changed while the database is running." /></label
> >
@ -51,7 +51,7 @@
/> />
</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" >{$t('forms.root_user')}</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -62,8 +62,9 @@
/> />
</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"
>{$t('forms.roots_password')} <Explainer explanation="Could be changed while the database is running." /></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}

View File

@ -7,11 +7,11 @@
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">MongoDB</div> <h1 class="title">MongoDB</h1>
</div> </div>
<div class="space-y-2 px-10"> <div class="space-y-2 lg:px-10 px-2">
<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">{$t('forms.root_user')}</label>
<CopyPasswordField <CopyPasswordField
placeholder={$t('forms.generated_automatically_after_start')} placeholder={$t('forms.generated_automatically_after_start')}
id="rootUser" id="rootUser"
@ -22,7 +22,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"
>{$t('forms.roots_password')} >{$t('forms.roots_password')}
<Explainer explanation="Could be changed while the database is running." /></label <Explainer explanation="Could be changed while the database is running." /></label
> >

View File

@ -7,13 +7,11 @@
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">MySQL</div> <h1 class="title">MySQL</h1>
</div> </div>
<div class="space-y-2 px-10"> <div class="space-y-2 lg:px-10 px-2">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="defaultDatabase" class="text-base font-bold text-stone-100" <label for="defaultDatabase">{$t('database.default_database')}</label>
>{$t('database.default_database')}</label
>
<CopyPasswordField <CopyPasswordField
required required
readonly={database.defaultDatabase} readonly={database.defaultDatabase}
@ -25,7 +23,7 @@
/> />
</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">{$t('forms.user')}</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -36,7 +34,7 @@
/> />
</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"
>{$t('forms.password')} >{$t('forms.password')}
<Explainer explanation="Could be changed while the database is running." /></label <Explainer explanation="Could be changed while the database is running." /></label
> >
@ -51,7 +49,7 @@
/> />
</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">{$t('forms.root_user')}</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -62,7 +60,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"
>{$t('forms.roots_password')} >{$t('forms.roots_password')}
<Explainer explanation="Could be changed while the database is running." /></label <Explainer explanation="Could be changed while the database is running." /></label
> >

View File

@ -7,13 +7,11 @@
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">PostgreSQL</div> <h1 class="title">PostgreSQL</h1>
</div> </div>
<div class="space-y-2 px-10"> <div class="space-y-2 lg:px-10 px-2">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="defaultDatabase" class="text-base font-bold text-stone-100" <label for="defaultDatabase">{$t('database.default_database')}</label>
>{$t('database.default_database')}</label
>
<CopyPasswordField <CopyPasswordField
required required
readonly={database.defaultDatabase} readonly={database.defaultDatabase}
@ -25,7 +23,7 @@
/> />
</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"
>Postgres User Password <Explainer >Postgres User Password <Explainer
explanation="Could be changed while the database is running." explanation="Could be changed while the database is running."
/></label /></label
@ -41,7 +39,7 @@
/> />
</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">{$t('forms.user')}</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -52,7 +50,7 @@
/> />
</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"
>{$t('forms.password')} >{$t('forms.password')}
<Explainer explanation="Could be changed while the database is running." /></label <Explainer explanation="Could be changed while the database is running." /></label
> >

View File

@ -7,11 +7,11 @@
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">Redis</div> <h1 class="title">Redis</h1>
</div> </div>
<div class="space-y-2 px-10"> <div class="space-y-2 lg:px-10 px-2">
<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"
>{$t('forms.password')} >{$t('forms.password')}
<Explainer explanation="Could be changed while the database is running." /></label <Explainer explanation="Could be changed while the database is running." /></label
> >

View File

@ -62,6 +62,7 @@
import DeleteIcon from '$lib/components/DeleteIcon.svelte'; import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import Tooltip from '$lib/components/Tooltip.svelte'; import Tooltip from '$lib/components/Tooltip.svelte';
import DatabaseLinks from './_DatabaseLinks.svelte';
const { id } = $page.params; const { id } = $page.params;
$status.database.isPublic = database.settings.isPublic || false; $status.database.isPublic = database.settings.isPublic || false;
@ -76,7 +77,7 @@
$status.database.initialLoading = true; $status.database.initialLoading = true;
try { try {
await del(`/databases/${database.id}`, { id: database.id, force }); await del(`/databases/${database.id}`, { id: database.id, force });
return await goto('/', { replaceState: true }); return await window.location.assign('/');
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
@ -149,13 +150,33 @@
</script> </script>
{#if id !== 'new'} {#if id !== 'new'}
<nav class="nav-side"> <nav class="header lg:flex-row flex-col-reverse">
<div class="flex flex-row space-x-2 font-bold pt-10 lg:pt-0">
<div class="flex flex-col items-center justify-center">
<div class="title">
{#if $page.url.pathname === `/databases/${id}`}
Configurations
{:else if $page.url.pathname === `/databases/${id}/logs`}
Database Logs
{:else if $page.url.pathname === `/databases/${id}/configuration/type`}
Select a Database Type
{:else if $page.url.pathname === `/databases/${id}/configuration/version`}
Select a Database Version
{:else if $page.url.pathname === `/databases/${id}/configuration/destination`}
Select a Destination
{/if}
</div>
</div>
<DatabaseLinks {database} />
</div>
<div class="lg:block hidden flex-1" />
<div class="flex flex-row flex-wrap space-x-3 justify-center lg:justify-start lg:py-0">
{#if database.type && database.destinationDockerId && database.version} {#if database.type && database.destinationDockerId && database.version}
{#if $status.database.isExited} {#if $status.database.isExited}
<a <a
id="exited" id="exited"
href={!$status.database.isRunning ? `/databases/${id}/logs` : null} href={!$status.database.isRunning ? `/databases/${id}/logs` : null}
class="icons bg-transparent text-sm flex items-center text-red-500 tooltip-error" class="icons bg-transparent text-red-500 tooltip-error"
sveltekit:prefetch sveltekit:prefetch
> >
<svg <svg
@ -179,9 +200,7 @@
<Tooltip triggeredBy="#exited">{'Service exited with an error!'}</Tooltip> <Tooltip triggeredBy="#exited">{'Service exited with an error!'}</Tooltip>
{/if} {/if}
{#if $status.database.initialLoading} {#if $status.database.initialLoading}
<button <button class="icons flex animate-spin duration-500 ease-in-out">
class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out hover:bg-transparent"
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-6 w-6"
@ -207,7 +226,7 @@
on:click={stopDatabase} on:click={stopDatabase}
type="submit" type="submit"
disabled={!$appSession.isAdmin} disabled={!$appSession.isAdmin}
class="icons bg-transparent text-sm flex items-center space-x-2 text-red-500" class="icons bg-transparent text-red-500"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -248,6 +267,7 @@
</button> </button>
<Tooltip triggeredBy="#start">{'Start'}</Tooltip> <Tooltip triggeredBy="#start">{'Start'}</Tooltip>
{/if} {/if}
{/if}
<div class="border border-stone-700 h-8" /> <div class="border border-stone-700 h-8" />
<a <a
id="configuration" id="configuration"
@ -282,34 +302,6 @@
></a ></a
> >
<Tooltip triggeredBy="#configuration">{'Configuration'}</Tooltip> <Tooltip triggeredBy="#configuration">{'Configuration'}</Tooltip>
<a
href="/databases/{id}/secrets"
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/databases/${id}/secrets`}
class:bg-coolgray-500={$page.url.pathname === `/databases/${id}/secrets`}
>
<button id="secrets" disabled={$isDeploymentEnabled} class="icons bg-transparent text-sm ">
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
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>
<div class="border border-stone-700 h-8" /> <div class="border border-stone-700 h-8" />
<a <a
id="databaselogs" id="databaselogs"
@ -340,8 +332,6 @@
></a ></a
> >
<Tooltip triggeredBy="#databaselogs">{'Logs'}</Tooltip> <Tooltip triggeredBy="#databaselogs">{'Logs'}</Tooltip>
{/if}
{#if forceDelete} {#if forceDelete}
<button <button
on:click={() => deleteDatabase(true)} on:click={() => deleteDatabase(true)}
@ -362,7 +352,8 @@
> >
{/if} {/if}
<Tooltip triggeredBy="#delete">{'Delete'}</Tooltip> <Tooltip triggeredBy="#delete" placement="left">Delete</Tooltip>
</div>
</nav> </nav>
{/if} {/if}
<slot /> <slot />

View File

@ -53,11 +53,6 @@
}); });
</script> </script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">
{$t('application.configuration.configure_destination')}
</div>
</div>
<div class="flex justify-center"> <div class="flex justify-center">
{#if !destinations || destinations.length === 0} {#if !destinations || destinations.length === 0}
<div class="flex-col"> <div class="flex-col">

View File

@ -47,10 +47,6 @@
} }
</script> </script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">{$t('database.select_database_type')}</div>
</div>
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">
{#each types as type} {#each types as type}
<div class="p-2"> <div class="p-2">

View File

@ -46,9 +46,6 @@
} }
</script> </script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">{$t('database.select_database_version')}</div>
</div>
{#if from} {#if from}
<div class="pb-10 text-center"> <div class="pb-10 text-center">
Warning: you are about to change the version of this database.<br />This could cause problem Warning: you are about to change the version of this database.<br />This could cause problem

View File

@ -48,18 +48,7 @@
}); });
</script> </script>
<div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold"> <div class="mx-auto max-w-6xl p-5">
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Configuration
</div>
<span class="text-xs">{database.name}</span>
</div>
<DatabaseLinks {database} />
</div>
<div class="mx-auto max-w-4xl px-6 py-4">
<div class="text-2xl font-bold">Database Usage</div>
<div class="text-center"> <div class="text-center">
<div class="stat w-64"> <div class="stat w-64">
<div class="stat-title">Used Memory / Memory Limit</div> <div class="stat-title">Used Memory / Memory Limit</div>

View File

@ -91,14 +91,6 @@
} }
</script> </script>
<div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold">
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Database Logs
</div>
<span class="text-xs">{database.name}</span>
</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}
<div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div> <div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div>
@ -112,7 +104,7 @@
<button <button
id="follow" id="follow"
on:click={followBuild} on:click={followBuild}
class="bg-transparent btn btn-sm" class="bg-transparent btn btn-sm btn-link"
class:text-green-500={followingLogs} class:text-green-500={followingLogs}
> >
<svg <svg
@ -134,17 +126,11 @@
</button> </button>
<Tooltip triggeredBy="#follow">Follow Logs</Tooltip> <Tooltip triggeredBy="#follow">Follow Logs</Tooltip>
</div> </div>
<div <div class="font-mono w-full rounder bg-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded-md mb-20 flex flex-col whitespace-nowrap -mt-12 scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1 lg:text-base text-[10px]">
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"
bind:this={logsEl}
on:scroll={detect}
>
<div class="px-2 pr-14">
{#each logs as log} {#each logs as log}
{log + '\n'} <p>{log + '\n'}</p>
{/each} {/each}
</div> </div>
</div> </div>
</div>
{/if} {/if}
</div> </div>

View File

@ -42,8 +42,8 @@
}); });
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <nav class="header">
<div class="mr-4 text-2xl tracking-tight">{$t('index.databases')}</div> <h1 class="mr-4 text-2xl font-bold">{$t('index.databases')}</h1>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<button on:click={newDatabase} class="btn btn-square btn-sm bg-databases"> <button on:click={newDatabase} class="btn btn-square btn-sm bg-databases">
<svg <svg
@ -61,9 +61,9 @@
> >
</button> </button>
{/if} {/if}
</div> </nav>
<br />
<div class="flex-col justify-center mt-10 pb-12 sm:pb-16"> <div class="flex-col justify-center mt-10 pb-12 sm:pb-16 lg:pt-16">
{#if !databases || ownDatabases.length === 0} {#if !databases || ownDatabases.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="text-center text-xl font-bold">{$t('database.no_databases_found')}</div> <div class="text-center text-xl font-bold">{$t('database.no_databases_found')}</div>

View File

@ -16,7 +16,7 @@
</div> </div>
</div> </div>
<div class="mx-auto max-w-4xl px-6"> <div class="mx-auto max-w-6xl px-6">
{#if destination.remoteEngine} {#if destination.remoteEngine}
<RemoteDocker bind:destination {settings} {state} /> <RemoteDocker bind:destination {settings} {state} />
{:else} {:else}

View File

@ -141,40 +141,37 @@
} }
</script> </script>
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4"> <form on:submit|preventDefault={handleSubmit} class="py-4">
<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="grid gap-4 grid-cols-2 grid-rows-1">
<div class="title">{$t('forms.configuration')}</div>
{#if $appSession.isAdmin}
<button <button
type="submit" type="submit"
class="btn btn-sm" class="btn btn-sm w-full"
class:bg-destinations={!loading.save} class:bg-destinations={!loading.save}
class:loading={loading.save} class:loading={loading.save}
disabled={loading.save} disabled={loading.save}
>{$t('forms.save')} >{$t('forms.save')}
</button> </button>
<button <button
class="btn btn-sm" class="btn btn-sm w-full"
class:loading={loading.restart} class:loading={loading.restart}
class:bg-error={!loading.restart} class:bg-error={!loading.restart}
disabled={loading.restart} disabled={loading.restart}
on:click|preventDefault={forceRestartProxy}>{$t('destination.force_restart_proxy')}</button on:click|preventDefault={forceRestartProxy}>{$t('destination.force_restart_proxy')}</button
> >
{/if}
</div> </div>
<div class="grid lg:grid-cols-2 items-center px-10 "> <div class="grid gap-4 grid-cols-2 auto-rows-max mt-10 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 w-full">{$t('forms.name')}</label>
<input <input
class="w-full"
name="name" name="name"
placeholder={$t('forms.name')} placeholder={$t('forms.name')}
disabled={!$appSession.isAdmin} disabled={!$appSession.isAdmin}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
bind:value={destination.name} bind:value={destination.name}
/> />
</div> <label for="engine" class="text-base font-bold text-stone-100 w-full"
>{$t('forms.engine')}</label
<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>
<CopyPasswordField <CopyPasswordField
id="engine" id="engine"
readonly readonly
@ -183,8 +180,6 @@
placeholder="{$t('forms.eg')}: /var/run/docker.sock" placeholder="{$t('forms.eg')}: /var/run/docker.sock"
value={destination.engine} value={destination.engine}
/> />
</div>
<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"
@ -194,9 +189,7 @@
placeholder="{$t('forms.default')}: coolify" placeholder="{$t('forms.default')}: coolify"
value={destination.network} value={destination.network}
/> />
</div>
{#if $appSession.teamId === '0'} {#if $appSession.teamId === '0'}
<div class="grid lg:grid-cols-2 items-center px-10">
<Setting <Setting
id="changeProxySetting" id="changeProxySetting"
loading={loading.proxy} loading={loading.proxy}
@ -210,6 +203,6 @@
: '' : ''
}`} }`}
/> />
</div>
{/if} {/if}
</div>
</form> </form>

View File

@ -31,9 +31,9 @@
<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-start lg:items-center space-x-0 lg:space-x-4 pb-5 flex-col space-y-4 lg:space-y-0">
<div class="title font-bold">{$t('forms.configuration')}</div> <div class="title font-bold">{$t('forms.configuration')}</div>
<button type="submit" class="btn btn-sm bg-destinations" class:loading disabled={loading} <button type="submit" class="btn btn-sm bg-destinations w-full lg:w-fit" class:loading disabled={loading}
>{loading >{loading
? payload.isCoolifyProxyUsed ? payload.isCoolifyProxyUsed
? $t('destination.new.saving_and_configuring_proxy') ? $t('destination.new.saving_and_configuring_proxy')
@ -41,12 +41,12 @@
: $t('forms.save')}</button : $t('forms.save')}</button
> >
</div> </div>
<div class="mt-2 grid grid-cols-2 items-center px-10"> <div class="mt-2 grid grid-cols-2 items-center lg:pl-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 required name="name" placeholder={$t('forms.name')} bind:value={payload.name} /> <input required name="name" placeholder={$t('forms.name')} bind:value={payload.name} />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center lg:pl-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>
<input <input
required required
@ -55,7 +55,7 @@
bind:value={payload.engine} bind:value={payload.engine}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center lg:pl-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>
<input <input
required required
@ -65,7 +65,7 @@
/> />
</div> </div>
{#if $appSession.teamId === '0'} {#if $appSession.teamId === '0'}
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center lg:pl-10">
<Setting <Setting
id="changeProxySetting" id="changeProxySetting"
bind:setting={payload.isCoolifyProxyUsed} bind:setting={payload.isCoolifyProxyUsed}

View File

@ -38,9 +38,9 @@
</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-start lg:items-center space-x-0 lg:space-x-4 pb-5 flex-col lg:flex-row space-y-4 lg:space-y-0">
<div class="title font-bold">{$t('forms.configuration')}</div> <div class="title font-bold">{$t('forms.configuration')}</div>
<button type="submit" class="btn btn-sm bg-destinations" class:loading disabled={loading} <button type="submit" class="btn btn-sm bg-destinations w-full lg:w-fit" class:loading disabled={loading}
>{loading >{loading
? payload.isCoolifyProxyUsed ? payload.isCoolifyProxyUsed
? $t('destination.new.saving_and_configuring_proxy') ? $t('destination.new.saving_and_configuring_proxy')
@ -48,12 +48,12 @@
: $t('forms.save')}</button : $t('forms.save')}</button
> >
</div> </div>
<div class="mt-2 grid grid-cols-2 items-center px-10"> <div class="mt-2 grid grid-cols-2 items-center lg:pl-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 required name="name" placeholder={$t('forms.name')} bind:value={payload.name} /> <input required name="name" placeholder={$t('forms.name')} bind:value={payload.name} />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center lg:pl-10">
<label for="remoteIpAddress" class="text-base font-bold text-stone-100" <label for="remoteIpAddress" class="text-base font-bold text-stone-100"
>{$t('forms.ip_address')}</label >{$t('forms.ip_address')}</label
> >
@ -65,7 +65,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center lg:pl-10">
<label for="remoteUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label> <label for="remoteUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label>
<input <input
required required
@ -75,7 +75,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center lg:pl-10">
<label for="remotePort" class="text-base font-bold text-stone-100">{$t('forms.port')}</label> <label for="remotePort" class="text-base font-bold text-stone-100">{$t('forms.port')}</label>
<input <input
required required
@ -84,7 +84,7 @@
bind:value={payload.remotePort} bind:value={payload.remotePort}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center lg:pl-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>
<input <input
required required
@ -93,7 +93,7 @@
bind:value={payload.network} bind:value={payload.network}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center lg:pl-10">
<Setting <Setting
id="isCoolifyProxyUsed" id="isCoolifyProxyUsed"
bind:setting={payload.isCoolifyProxyUsed} bind:setting={payload.isCoolifyProxyUsed}

View File

@ -36,8 +36,8 @@
}); });
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <nav class="header">
<div class="mr-4 text-2xl tracking-tight">{$t('index.destinations')}</div> <h1 class="mr-4 text-2xl font-bold">{$t('index.destinations')}</h1>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<a href="/destinations/new" class="btn btn-square btn-sm bg-destinations"> <a href="/destinations/new" class="btn btn-square btn-sm bg-destinations">
<svg <svg
@ -55,8 +55,9 @@
> >
</a> </a>
{/if} {/if}
</div> </nav>
<div class="flex-col justify-center mt-10 pb-12 sm:pb-16"> <br />
<div class="flex-col justify-center mt-10 pb-12 sm:pb-16 lg:pt-16">
{#if !destinations || ownDestinations.length === 0} {#if !destinations || ownDestinations.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="text-center text-xl font-bold">{$t('destination.no_destination_found')}</div> <div class="text-center text-xl font-bold">{$t('destination.no_destination_found')}</div>

View File

@ -106,8 +106,8 @@
} }
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <nav class="header">
<div class="mr-4 text-2xl tracking-tight">Identity and Access Management</div> <h1 class="mr-4 text-2xl tracking-tight font-bold">Identity and Access Management</h1>
<button on:click={newTeam} class="btn btn-square btn-sm bg-iam"> <button on:click={newTeam} class="btn btn-square btn-sm bg-iam">
<svg <svg
class="h-6 w-6" class="h-6 w-6"
@ -123,10 +123,11 @@
/></svg /></svg
> >
</button> </button>
</div> </nav>
<br />
{#if invitations.length > 0} {#if invitations.length > 0}
<div class="mx-auto max-w-4xl px-6 py-4"> <div class="mx-auto max-w-6xl px-6 py-4">
<div class="title font-bold">Pending invitations</div> <div class="title font-bold">Pending invitations</div>
<div class="pt-10 text-center"> <div class="pt-10 text-center">
{#each invitations as invitation} {#each invitations as invitation}
@ -148,7 +149,7 @@
</div> </div>
</div> </div>
{/if} {/if}
<div class="mx-auto max-w-4xl px-6 py-4"> <div class="mx-auto max-w-6xl px-6 py-4">
{#if $appSession.teamId === '0' && accounts.length > 0} {#if $appSession.teamId === '0' && accounts.length > 0}
<div class="title font-bold">Accounts</div> <div class="title font-bold">Accounts</div>
{:else} {:else}
@ -188,7 +189,7 @@
</div> </div>
</div> </div>
<div class="mx-auto max-w-4xl px-6"> <div class="mx-auto max-w-6xl px-6">
<div class="title font-bold">Teams</div> <div class="title font-bold">Teams</div>
<div class="flex-col items-center justify-center pt-10"> <div class="flex-col items-center justify-center pt-10">
<div class="flex flex-row flex-wrap justify-center px-2 pb-10 md:flex-row"> <div class="flex flex-row flex-wrap justify-center px-2 pb-10 md:flex-row">

View File

@ -87,7 +87,7 @@
<span class="arrow-right-applications px-1 text-fuchsia-500">></span> <span class="arrow-right-applications px-1 text-fuchsia-500">></span>
<span class="pr-2">{team.name}</span> <span class="pr-2">{team.name}</span>
</div> </div>
<div class="mx-auto max-w-4xl px-6"> <div class="mx-auto max-w-6xl px-6">
<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 space-x-1 pb-5">
<div class="title font-bold">{$t('index.settings')}</div> <div class="title font-bold">{$t('index.settings')}</div>

View File

@ -28,11 +28,10 @@
export let destinations: any; export let destinations: any;
let filtered: any = setInitials(); let filtered: any = setInitials();
import { get, post } from '$lib/api'; import { get } from '$lib/api';
import Usage from '$lib/components/Usage.svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { asyncSleep } from '$lib/common'; import { asyncSleep, getRndInteger } from '$lib/common';
import { appSession, search } from '$lib/store'; import { appSession, search, addToast} from '$lib/store';
import ApplicationsIcons from '$lib/components/svg/applications/ApplicationIcons.svelte'; import ApplicationsIcons from '$lib/components/svg/applications/ApplicationIcons.svelte';
import DatabaseIcons from '$lib/components/svg/databases/DatabaseIcons.svelte'; import DatabaseIcons from '$lib/components/svg/databases/DatabaseIcons.svelte';
@ -87,9 +86,7 @@
filtered.destinations = []; filtered.destinations = [];
filtered.otherDestinations = []; filtered.otherDestinations = [];
} }
function getRndInteger(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
async function getStatus(resources: any) { async function getStatus(resources: any) {
const { id, buildPack, dualCerts } = resources; const { id, buildPack, dualCerts } = resources;
@ -188,6 +185,7 @@
return ( return (
(service.name && service.name.toLowerCase().includes($search.toLowerCase())) || (service.name && service.name.toLowerCase().includes($search.toLowerCase())) ||
(service.type && service.type.toLowerCase().includes($search.toLowerCase())) || (service.type && service.type.toLowerCase().includes($search.toLowerCase())) ||
(service.fqdn && service.fqdn.toLowerCase().includes($search.toLowerCase())) ||
(service.version && service.version.toLowerCase().includes($search.toLowerCase())) || (service.version && service.version.toLowerCase().includes($search.toLowerCase())) ||
(service.destinationDockerId && (service.destinationDockerId &&
service.destinationDocker.name.toLowerCase().includes($search.toLowerCase())) service.destinationDocker.name.toLowerCase().includes($search.toLowerCase()))
@ -272,18 +270,16 @@
filtered = setInitials(); filtered = setInitials();
} }
} }
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <nav class="header">
<div class="mr-4 text-2xl tracking-tight">{$t('index.dashboard')}</div> <h1 class="mr-4 text-2xl font-bold">{$t('index.dashboard')}</h1>
{#if $appSession.isAdmin && (applications.length !== 0 || destinations.length !== 0 || databases.length !== 0 || services.length !== 0 || gitSources.length !== 0 || destinations.length !== 0)} {#if $appSession.isAdmin && (applications.length !== 0 || destinations.length !== 0 || databases.length !== 0 || services.length !== 0 || gitSources.length !== 0 || destinations.length !== 0)}
<NewResource /> <NewResource />
{/if} {/if}
</div> </nav>
<div class="container lg:mx-auto lg:p-0 px-8 p-5"> <div class="container lg:mx-auto lg:p-0 px-8 pt-5">
<!-- {#if $appSession.teamId === '0'}
<Usage />
{/if} -->
{#if applications.length !== 0 || destinations.length !== 0 || databases.length !== 0 || services.length !== 0 || gitSources.length !== 0 || destinations.length !== 0} {#if applications.length !== 0 || destinations.length !== 0 || databases.length !== 0 || services.length !== 0 || gitSources.length !== 0 || destinations.length !== 0}
<div class="form-control"> <div class="form-control">
<div class="input-group flex w-full"> <div class="input-group flex w-full">
@ -316,7 +312,7 @@
on:input={() => doSearch()} on:input={() => doSearch()}
/> />
</div> </div>
<label for="search" class="label w-full"> <label for="search" class="label w-full mt-3">
<span class="label-text text-xs flex flex-wrap gap-2 items-center"> <span class="label-text text-xs flex flex-wrap gap-2 items-center">
<button <button
class:bg-coollabs={$search === '!notmine'} class:bg-coollabs={$search === '!notmine'}

View File

@ -28,11 +28,7 @@
Cookies.set('token', token, { Cookies.set('token', token, {
path: '/' path: '/'
}); });
$appSession.teamId = payload.teamId; return window.location.assign('/');
$appSession.userId = payload.userId;
$appSession.permission = payload.permission;
$appSession.isAdmin = payload.isAdmin;
return await goto('/');
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {

View File

@ -29,15 +29,15 @@
} }
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="header">
<div class="mr-4 text-2xl tracking-tight">Servers</div> <h1 class="text-2xl font-bold">Servers</h1>
</div> </div>
<div class="container lg:mx-auto lg:p-0 px-8 p-5"> <div class="container lg:mx-auto lg:p-0 px-8 p-5">
{#if servers.length > 0} {#if servers.length > 0}
<div class="grid grid-col gap-8 auto-cols-max grid-cols-1 p-4"> <div class="grid grid-col gap-8 auto-cols-max grid-cols-1 p-4">
{#each servers as server} {#each servers as server}
<div class="no-underline mb-5"> <div class="no-underline mb-5">
<div class="w-full rounded bg-coolgray-100 indicator"> <div class="w-full rounded bg-coolgray-200 indicator">
{#if $appSession.teamId === '0'} {#if $appSession.teamId === '0'}
<Usage {server} /> <Usage {server} />
{/if} {/if}
@ -49,4 +49,3 @@
<h1 class="text-center text-xs">Nothing here.</h1> <h1 class="text-center text-xs">Nothing here.</h1>
{/if} {/if}
</div> </div>
<div class="text-xs text-center">Remote servers will be here soon</div>

View File

@ -50,6 +50,8 @@
<td> <td>
<input <input
style="min-width: 350px !important;"
id={isNewSecret ? 'secretName' : 'secretNameNew'} id={isNewSecret ? 'secretName' : 'secretNameNew'}
bind:value={name} bind:value={name}
required required
@ -67,6 +69,7 @@
bind:value bind:value
required required
placeholder="J$#@UIO%HO#$U%H" placeholder="J$#@UIO%HO#$U%H"
inputStyle="min-width: 350px; !important"
/> />
</td> </td>

View File

@ -10,7 +10,8 @@
<div class="title">Appwrite</div> <div class="title">Appwrite</div>
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="space-y-2">
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="opensslKeyV1">Encryption Key</label> <label for="opensslKeyV1">Encryption Key</label>
<CopyPasswordField <CopyPasswordField
name="opensslKeyV1" name="opensslKeyV1"
@ -20,8 +21,8 @@
readonly readonly
disabled disabled
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="executorSecret">Executor Secret</label> <label for="executorSecret">Executor Secret</label>
<CopyPasswordField <CopyPasswordField
name="executorSecret" name="executorSecret"
@ -31,48 +32,13 @@
readonly readonly
disabled disabled
/> />
</div>
</div> </div>
<!-- <div class="flex space-x-1 py-5 font-bold">
<div class="title">Redis</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="redisPassword">Password</label>
<CopyPasswordField
name="redisPassword"
id="redisPassword"
isPasswordField
value={service.appwrite.redisPassword}
readonly
disabled
/>
</div> -->
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">MariaDB</div> <div class="title">MariaDB</div>
</div> </div>
<div class="space-y-2">
<!-- <div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="mariadbHost">MariaDB Host</label>
<CopyPasswordField
name="mariadbHost"
id="mariadbHost"
value={service.appwrite.mariadbHost}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbPort">MariaDB Port</label>
<CopyPasswordField
name="mariadbPort"
id="mariadbPort"
value={service.appwrite.mariadbPort}
readonly
disabled
/>
</div> -->
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbUser">{$t('forms.username')}</label> <label for="mariadbUser">{$t('forms.username')}</label>
<CopyPasswordField <CopyPasswordField
name="mariadbUser" name="mariadbUser"
@ -81,8 +47,8 @@
readonly readonly
disabled disabled
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center lg:px-10 px-2 ">
<label for="mariadbPassword">{$t('forms.password')}</label> <label for="mariadbPassword">{$t('forms.password')}</label>
<CopyPasswordField <CopyPasswordField
id="mariadbPassword" id="mariadbPassword"
@ -92,8 +58,8 @@
name="mariadbPassword" name="mariadbPassword"
value={service.appwrite.mariadbPassword} value={service.appwrite.mariadbPassword}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="mariadbRootUser">Root User</label> <label for="mariadbRootUser">Root User</label>
<CopyPasswordField <CopyPasswordField
name="mariadbRootUser" name="mariadbRootUser"
@ -102,8 +68,8 @@
readonly readonly
disabled disabled
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center lg:px-10 px-2 ">
<label for="mariadbRootUserPassword">Root Password</label> <label for="mariadbRootUserPassword">Root Password</label>
<CopyPasswordField <CopyPasswordField
id="mariadbRootUserPassword" id="mariadbRootUserPassword"
@ -113,8 +79,8 @@
name="mariadbRootUserPassword" name="mariadbRootUserPassword"
value={service.appwrite.mariadbRootUserPassword} value={service.appwrite.mariadbRootUserPassword}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center lg:px-10 px-2">
<label for="mariadbDatabase">{$t('index.database')}</label> <label for="mariadbDatabase">{$t('index.database')}</label>
<CopyPasswordField <CopyPasswordField
name="mariadbDatabase" name="mariadbDatabase"
@ -123,4 +89,5 @@
readonly readonly
disabled disabled
/> />
</div>
</div> </div>

Some files were not shown because too many files have changed in this diff Show More