Compare commits

..

47 Commits

Author SHA1 Message Date
Andras Bacsai
7932c1c4a9 Merge pull request #648 from coollabsio/next
v3.10.13
2022-10-03 12:56:59 +02:00
Andras Bacsai
f776fb83e7 ui: settings icon 2022-10-03 11:45:24 +02:00
Andras Bacsai
a97521aba2 webhook: send 200 for ping and installation wh 2022-10-03 11:42:07 +02:00
Andras Bacsai
d1c0fe503e fix: remove unnecessary things 2022-10-03 11:32:15 +02:00
Andras Bacsai
ed02c1ae36 ui: iam & settings update 2022-10-03 11:31:50 +02:00
Andras Bacsai
9a67cf7355 fix: fork pr previews 2022-10-03 09:48:47 +02:00
Andras Bacsai
755eeda364 remove inspector 2022-10-03 09:25:31 +02:00
Andras Bacsai
136dee7747 ui: fix indicato 2022-10-03 09:20:57 +02:00
Andras Bacsai
e4e8428855 minify api 2022-10-02 11:08:04 +00:00
Andras Bacsai
de8dc021f9 fix: pr branches 2022-10-02 09:37:08 +00:00
Andras Bacsai
991587f252 fix: typo 2022-10-02 09:24:43 +00:00
Andras Bacsai
8dbcf257c4 fix: handle forked repositories 2022-10-02 09:16:51 +00:00
Andras Bacsai
0b067364a9 fix: default 0 pending invitations 2022-10-02 08:55:36 +00:00
Andras Bacsai
5367bd6134 show webhook details 2022-10-02 08:48:56 +00:00
Andras Bacsai
92228c4379 schema migration 2022-10-02 08:43:45 +00:00
Andras Bacsai
fb2c7896b3 update packages 2022-10-02 08:43:36 +00:00
Andras Bacsai
23265d9091 revert last changes 2022-10-02 10:38:08 +02:00
Andras Bacsai
2c9bb0e767 disable stuff 2022-10-01 13:58:50 +00:00
Andras Bacsai
f9e8400d83 temporary disable schedulers 2022-10-01 13:46:52 +00:00
Andras Bacsai
927a13cd76 temporary enable inspector 2022-10-01 13:03:55 +00:00
Andras Bacsai
51b3293e69 ui: inprogress version of iam 2022-09-29 15:46:52 +02:00
Andras Bacsai
3f76cadea9 fix: cleanup stucked tcp proxies 2022-09-29 14:44:20 +02:00
Andras Bacsai
6dbf53b558 chore: version++ 2022-09-29 14:32:55 +02:00
Andras Bacsai
22e937c798 fix: do not start tcp proxy without main container 2022-09-29 14:32:35 +02:00
Andras Bacsai
ac5cc8b299 Merge pull request #643 from coollabsio/next
v3.10.12
2022-09-29 14:09:37 +02:00
Andras Bacsai
c588ab723b fix: show logs better 2022-09-29 13:57:52 +02:00
Andras Bacsai
4b2dfc051d typo 2022-09-29 13:47:15 +02:00
Andras Bacsai
5238c83f3f fix: initial deploy status 2022-09-29 13:23:38 +02:00
Andras Bacsai
90bb580e50 ui: fixes 2022-09-29 13:23:29 +02:00
Andras Bacsai
f40e142704 ui: fix 2022-09-29 13:15:19 +02:00
Andras Bacsai
a67618675d fix: default buildImage and baseBuildImage 2022-09-29 13:15:16 +02:00
Andras Bacsai
4fe436e4d1 fix: dashboard statuses 2022-09-29 13:02:10 +02:00
Andras Bacsai
683b8c966f feat: cleanup unconfigured services and databases 2022-09-28 15:41:20 +02:00
Andras Bacsai
28377a156d feat: cleanup unconfigured applications 2022-09-28 11:45:02 +02:00
Andras Bacsai
3dcc4faabb Merge pull request #642 from coollabsio/next
v3.10.11
2022-09-28 11:18:01 +02:00
Andras Bacsai
60a033f93a ui: fix 2022-09-28 11:16:35 +02:00
Andras Bacsai
436bd73786 fix: baseDirectory 2022-09-28 11:14:23 +02:00
Andras Bacsai
5c69ff3339 fix: do not get status of more than 10 resources defined by category 2022-09-28 10:59:58 +02:00
Andras Bacsai
2105b1e7c4 ux: hasura console notification 2022-09-28 10:55:08 +02:00
Andras Bacsai
523004e5b2 chore: version++ 2022-09-28 10:54:57 +02:00
Andras Bacsai
5e02c386ec Merge pull request #641 from coollabsio/next
v3.10.10
2022-09-28 10:49:19 +02:00
Andras Bacsai
b4501fe52d ui: beta flag 2022-09-28 10:41:32 +02:00
Andras Bacsai
3c29eaa1b1 ui: small fix 2022-09-28 10:35:47 +02:00
Andras Bacsai
ee67e163b1 feat: system-wide github apps 2022-09-28 10:34:27 +02:00
Andras Bacsai
9662bc29fb ui: fix gitlab importer view 2022-09-28 09:56:27 +02:00
Andras Bacsai
96f2660b98 ui: loading button 2022-09-28 09:47:05 +02:00
Andras Bacsai
20f594c66c chore: version++ 2022-09-28 09:30:57 +02:00
66 changed files with 1792 additions and 961 deletions

View File

@@ -8,15 +8,15 @@
"db:studio": "prisma studio",
"db:migrate": "COOLIFY_DATABASE_URL=file:../db/migration.db prisma migrate dev --skip-seed --name",
"dev": "nodemon",
"build": "rimraf build && esbuild `find src \\( -name '*.ts' \\)| grep -v client/` --platform=node --outdir=build --format=cjs",
"build": "rimraf build && esbuild `find src \\( -name '*.ts' \\)| grep -v client/` --minify=true --platform=node --outdir=build --format=cjs",
"format": "prettier --write 'src/**/*.{js,ts,json,md}'",
"lint": "prettier --check 'src/**/*.{js,ts,json,md}' && eslint --ignore-path .eslintignore .",
"start": "NODE_ENV=production npx -y prisma migrate deploy && npx prisma generate && npx prisma db seed && node index.js"
},
"dependencies": {
"@breejs/ts-worker": "2.0.0",
"@fastify/autoload": "5.3.1",
"@fastify/cookie": "8.1.0",
"@fastify/autoload": "5.4.0",
"@fastify/cookie": "8.3.0",
"@fastify/cors": "8.1.0",
"@fastify/env": "4.1.0",
"@fastify/jwt": "6.3.2",
@@ -24,7 +24,7 @@
"@fastify/static": "6.5.0",
"@iarna/toml": "2.2.5",
"@ladjs/graceful": "3.0.2",
"@prisma/client": "4.3.1",
"@prisma/client": "4.4.0",
"axios": "0.27.2",
"bcryptjs": "2.4.3",
"bree": "9.1.2",
@@ -37,10 +37,10 @@
"dockerode": "3.3.4",
"dotenv-extended": "2.9.0",
"execa": "6.1.0",
"fastify": "4.5.3",
"fastify": "4.7.0",
"fastify-plugin": "4.2.1",
"generate-password": "1.7.0",
"got": "12.4.1",
"got": "12.5.1",
"is-ip": "5.0.0",
"is-port-reachable": "4.0.0",
"js-yaml": "4.1.0",
@@ -56,20 +56,20 @@
"unique-names-generator": "4.7.1"
},
"devDependencies": {
"@types/node": "18.7.15",
"@types/node": "18.7.23",
"@types/node-os-utils": "1.3.0",
"@typescript-eslint/eslint-plugin": "5.36.2",
"@typescript-eslint/parser": "5.36.2",
"esbuild": "0.15.7",
"@typescript-eslint/eslint-plugin": "5.38.1",
"@typescript-eslint/parser": "5.38.1",
"esbuild": "0.15.10",
"eslint": "8.23.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-prettier": "4.2.1",
"nodemon": "2.0.19",
"nodemon": "2.0.20",
"prettier": "2.7.1",
"prisma": "4.3.1",
"prisma": "4.4.0",
"rimraf": "3.0.2",
"tsconfig-paths": "4.1.0",
"typescript": "4.8.2"
"typescript": "4.8.4"
},
"prisma": {
"seed": "node prisma/seed.js"

View File

@@ -0,0 +1,26 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_GitSource" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"forPublic" BOOLEAN NOT NULL DEFAULT false,
"type" TEXT,
"apiUrl" TEXT,
"htmlUrl" TEXT,
"customPort" INTEGER NOT NULL DEFAULT 22,
"organization" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"githubAppId" TEXT,
"gitlabAppId" TEXT,
"isSystemWide" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "GitSource_gitlabAppId_fkey" FOREIGN KEY ("gitlabAppId") REFERENCES "GitlabApp" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "GitSource_githubAppId_fkey" FOREIGN KEY ("githubAppId") REFERENCES "GithubApp" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_GitSource" ("apiUrl", "createdAt", "customPort", "forPublic", "githubAppId", "gitlabAppId", "htmlUrl", "id", "name", "organization", "type", "updatedAt") SELECT "apiUrl", "createdAt", "customPort", "forPublic", "githubAppId", "gitlabAppId", "htmlUrl", "id", "name", "organization", "type", "updatedAt" FROM "GitSource";
DROP TABLE "GitSource";
ALTER TABLE "new_GitSource" RENAME TO "GitSource";
CREATE UNIQUE INDEX "GitSource_githubAppId_key" ON "GitSource"("githubAppId");
CREATE UNIQUE INDEX "GitSource_gitlabAppId_key" ON "GitSource"("gitlabAppId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,2 @@
-- DropIndex
DROP INDEX "PreviewApplication_applicationId_key";

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Build" ADD COLUMN "sourceRepository" TEXT;

View File

@@ -139,7 +139,7 @@ model PreviewApplication {
sourceBranch String
isRandomDomain Boolean @default(false)
customDomain String?
applicationId String @unique
applicationId String
application Application @relation(fields: [applicationId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -247,6 +247,7 @@ model Build {
previewApplicationId String?
forceRebuild Boolean @default(false)
sourceBranch String?
sourceRepository String?
branch String?
status String? @default("queued")
createdAt DateTime @default(now())
@@ -299,6 +300,7 @@ model GitSource {
updatedAt DateTime @updatedAt
githubAppId String? @unique
gitlabAppId String? @unique
isSystemWide Boolean @default(false)
gitlabApp GitlabApp? @relation(fields: [gitlabAppId], references: [id])
githubApp GithubApp? @relation(fields: [githubAppId], references: [id])
application Application[]

View File

@@ -156,11 +156,6 @@ prisma.setting.findFirst().then(async (settings) => {
setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:copySSLCertificates")
}, 2000)
// cleanupPrismaEngines
// setInterval(async () => {
// scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupPrismaEngines")
// }, 60000)
await Promise.all([
getArch(),

View File

@@ -38,7 +38,7 @@ import * as buildpacks from '../lib/buildPacks';
for (const queueBuild of queuedBuilds) {
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 { id: buildId, type, sourceBranch = null, pullmergeRequestId = null, previewApplicationId = null, forceRebuild } = queueBuild
let { id: buildId, type, sourceBranch = null, pullmergeRequestId = null, previewApplicationId = null, forceRebuild, sourceRepository = null } = queueBuild
application = decryptApplication(application)
const originalApplicationId = application.id
if (pullmergeRequestId) {
@@ -54,7 +54,6 @@ import * as buildpacks from '../lib/buildPacks';
}
const {
id: applicationId,
repository,
name,
destinationDocker,
destinationDockerId,
@@ -77,6 +76,7 @@ import * as buildpacks from '../lib/buildPacks';
} = application
let {
branch,
repository,
buildPack,
port,
installCommand,
@@ -135,6 +135,7 @@ import * as buildpacks from '../lib/buildPacks';
branch = sourceBranch;
domain = `${pullmergeRequestId}.${domain}`;
imageId = `${applicationId}-${pullmergeRequestId}`;
repository = sourceRepository || repository;
}
let deployNeeded = true;

View File

@@ -146,10 +146,7 @@ async function checkProxies() {
const { destinationDockerId, destinationDocker, publicPort, id } = database;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
const { privatePort } = generateDatabaseConfiguration(database, arch);
portReachable = await isReachable(publicPort, { host: destinationDocker.remoteIpAddress || ipv4 || ipv6 })
if (!portReachable) {
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
}
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
}
}
const wordpressWithFtp = await prisma.wordpress.findMany({
@@ -160,10 +157,7 @@ async function checkProxies() {
const { service, ftpPublicPort } = ftp;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
portReachable = await isReachable(ftpPublicPort, { host: destinationDocker.remoteIpAddress || ipv4 || ipv6 })
if (!portReachable) {
await startTraefikTCPProxy(destinationDocker, id, ftpPublicPort, 22, 'wordpressftp');
}
await startTraefikTCPProxy(destinationDocker, id, ftpPublicPort, 22, 'wordpressftp');
}
}
@@ -176,10 +170,7 @@ async function checkProxies() {
const { service, publicPort } = minio;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
portReachable = await isReachable(publicPort, { host: destinationDocker.remoteIpAddress || ipv4 || ipv6 })
if (!portReachable) {
await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000);
}
await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000);
}
}
} catch (error) {

View File

@@ -472,15 +472,15 @@ export const saveBuildLog = async ({
if (isDev) {
console.debug(`[${applicationId}] ${addTimestamp}`);
return
}
}
try {
return await got.post(`${fluentBitUrl}/${applicationId}_buildlog_${buildId}.csv`, {
json: {
line: encrypt(line)
}
})
} catch(error) {
} catch (error) {
if (isDev) return
return await prisma.buildLog.create({
data: {
line: addTimestamp, buildId, time: Number(day().valueOf()), applicationId

View File

@@ -9,7 +9,6 @@ import generator from 'generate-password';
import crypto from 'crypto';
import { promises as dns } from 'dns';
import { PrismaClient } from '@prisma/client';
import cuid from 'cuid';
import os from 'os';
import sshConfig from 'ssh-config';
@@ -21,7 +20,7 @@ import { scheduler } from './scheduler';
import { supportedServiceTypesAndVersions } from './services/supportedVersions';
import { includeServices } from './services/common';
export const version = '3.10.9';
export const version = '3.10.13';
export const isDev = process.env.NODE_ENV === 'development';
const algorithm = 'aes-256-ctr';
@@ -45,7 +44,7 @@ export function getAPIUrl() {
if (process.env.CODESANDBOX_HOST) {
return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`;
}
return isDev ? 'http://host.docker.internal:3001' : 'http://localhost:3000';
return isDev ? 'http://localhost:3001' : 'http://localhost:3000';
}
export function getUIUrl() {
@@ -1365,7 +1364,7 @@ export async function startTraefikTCPProxy(
let dependentId = id;
if (type === 'wordpressftp') dependentId = `${id}-ftp`;
const foundDependentContainer = await checkContainer({
const { found: foundDependentContainer } = await checkContainer({
dockerId,
container: dependentId,
remove: true

View File

@@ -1883,11 +1883,11 @@ async function stopServiceContainers(request: FastifyRequest<ServiceStartStop>)
if (destinationDockerId) {
await executeDockerCmd({
dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=com.docker.compose.project=${id}' --format {{.ID}}|xargs -n 1 docker stop -t 0`
command: `docker ps -a --filter 'label=com.docker.compose.project=${id}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0`
})
await executeDockerCmd({
dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=com.docker.compose.project=${id}' --format {{.ID}}|xargs -n 1 docker rm --force`
command: `docker ps -a --filter 'label=com.docker.compose.project=${id}' --format {{.ID}}|xargs -r -n 1 docker rm --force`
})
return {}
}

View File

@@ -69,6 +69,43 @@ export async function getImages(request: FastifyRequest<GetImages>) {
return errorHandler({ status, message })
}
}
export async function cleanupUnconfiguredApplications(request: FastifyRequest<any>) {
try {
const teamId = request.user.teamId
let applications = await prisma.application.findMany({
where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } },
include: { settings: true, destinationDocker: true, teams: true },
});
for (const application of applications) {
if (!application.buildPack || !application.destinationDockerId || !application.branch || (!application.settings?.isBot && !application?.fqdn)) {
if (application?.destinationDockerId && application.destinationDocker?.network) {
const { stdout: containers } = await executeDockerCmd({
dockerId: application.destinationDocker.id,
command: `docker ps -a --filter network=${application.destinationDocker.network} --filter name=${application.id} --format '{{json .}}'`
})
if (containers) {
const containersArray = containers.trim().split('\n');
for (const container of containersArray) {
const containerObj = JSON.parse(container);
const id = containerObj.ID;
await removeContainer({ id, dockerId: application.destinationDocker.id });
}
}
}
await prisma.applicationSettings.deleteMany({ where: { applicationId: application.id } });
await prisma.buildLog.deleteMany({ where: { applicationId: application.id } });
await prisma.build.deleteMany({ where: { applicationId: application.id } });
await prisma.secret.deleteMany({ where: { applicationId: application.id } });
await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: application.id } });
await prisma.applicationConnectedDatabase.deleteMany({ where: { applicationId: application.id } });
await prisma.application.deleteMany({ where: { id: application.id } });
}
}
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getApplicationStatus(request: FastifyRequest<OnlyId>) {
try {
const { id } = request.params
@@ -761,7 +798,10 @@ export async function saveBuildPack(request, reply) {
try {
const { id } = request.params
const { buildPack } = request.body
await prisma.application.update({ where: { id }, data: { buildPack } });
const { baseImage, baseBuildImage } = setDefaultBaseImage(
buildPack
);
await prisma.application.update({ where: { id }, data: { buildPack, baseImage, baseBuildImage } });
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })

View File

@@ -1,6 +1,6 @@
import { FastifyPluginAsync } from 'fastify';
import { OnlyId } from '../../../../types';
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, updatePreviewSecret, updateSecret } from './handlers';
import { cancelDeployment, checkDNS, checkDomain, checkRepository, cleanupUnconfiguredApplications, 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, updatePreviewSecret, updateSecret } from './handlers';
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';
@@ -11,6 +11,8 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get('/', async (request) => await listApplications(request));
fastify.post<GetImages>('/images', async (request) => await getImages(request));
fastify.post<any>('/cleanup/unconfigured', async (request) => await cleanupUnconfiguredApplications(request));
fastify.post('/new', async (request, reply) => await newApplication(request, reply));
fastify.get<OnlyId>('/:id', async (request) => await getApplication(request));

View File

@@ -51,6 +51,30 @@ export async function newDatabase(request: FastifyRequest, reply: FastifyReply)
return errorHandler({ status, message })
}
}
export async function cleanupUnconfiguredDatabases(request: FastifyRequest) {
try {
const teamId = request.user.teamId;
let databases = await prisma.database.findMany({
where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } },
include: { settings: true, destinationDocker: true, teams: true },
});
for (const database of databases) {
if (!database?.version) {
const { id } = database;
if (database.destinationDockerId) {
const everStarted = await stopDatabaseContainer(database);
if (everStarted) await stopTcpHttpProxy(id, database.destinationDocker, database.publicPort);
}
await prisma.databaseSettings.deleteMany({ where: { databaseId: id } });
await prisma.databaseSecret.deleteMany({ where: { databaseId: id } });
await prisma.database.delete({ where: { id } });
}
}
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getDatabaseStatus(request: FastifyRequest<OnlyId>) {
try {
const { id } = request.params;

View File

@@ -1,5 +1,5 @@
import { FastifyPluginAsync } from 'fastify';
import { deleteDatabase, deleteDatabaseSecret, getDatabase, getDatabaseLogs, getDatabaseSecrets, getDatabaseStatus, getDatabaseTypes, getDatabaseUsage, getVersions, listDatabases, newDatabase, saveDatabase, saveDatabaseDestination, saveDatabaseSecret, saveDatabaseSettings, saveDatabaseType, saveVersion, startDatabase, stopDatabase } from './handlers';
import { cleanupUnconfiguredDatabases, deleteDatabase, deleteDatabaseSecret, getDatabase, getDatabaseLogs, getDatabaseSecrets, getDatabaseStatus, getDatabaseTypes, getDatabaseUsage, getVersions, listDatabases, newDatabase, saveDatabase, saveDatabaseDestination, saveDatabaseSecret, saveDatabaseSettings, saveDatabaseType, saveVersion, startDatabase, stopDatabase } from './handlers';
import type { OnlyId } from '../../../../types';
@@ -12,6 +12,8 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get('/', async (request) => await listDatabases(request));
fastify.post('/new', async (request, reply) => await newDatabase(request, reply));
fastify.post<any>('/cleanup/unconfigured', async (request) => await cleanupUnconfiguredDatabases(request));
fastify.get<OnlyId>('/:id', async (request) => await getDatabase(request));
fastify.post<SaveDatabase>('/:id', async (request, reply) => await saveDatabase(request, reply));
fastify.delete<DeleteDatabase>('/:id', async (request) => await deleteDatabase(request));

View File

@@ -122,7 +122,7 @@ export async function showDashboard(request: FastifyRequest) {
try {
const userId = request.user.userId;
const teamId = request.user.teamId;
const applications = await prisma.application.findMany({
let applications = await prisma.application.findMany({
where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } },
include: { settings: true, destinationDocker: true, teams: true },
});
@@ -135,7 +135,7 @@ export async function showDashboard(request: FastifyRequest) {
include: { destinationDocker: true, teams: true },
});
const gitSources = await prisma.gitSource.findMany({
where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } },
where: { OR: [{ teams: { some: { id: teamId === "0" ? undefined : teamId } } }, { isSystemWide: true }] },
include: { teams: true },
});
const destinations = await prisma.destinationDocker.findMany({
@@ -143,7 +143,29 @@ export async function showDashboard(request: FastifyRequest) {
include: { teams: true },
});
const settings = await listSettings();
let foundUnconfiguredApplication = false;
for (const application of applications) {
if (!application.buildPack || !application.destinationDockerId || !application.branch || (!application.settings?.isBot && !application?.fqdn)) {
foundUnconfiguredApplication = true
}
}
let foundUnconfiguredService = false;
for (const service of services) {
if (!service.fqdn) {
foundUnconfiguredService = true
}
}
let foundUnconfiguredDatabase = false;
for (const database of databases) {
if (!database.version) {
foundUnconfiguredDatabase = true
}
}
return {
foundUnconfiguredApplication,
foundUnconfiguredDatabase,
foundUnconfiguredService,
applications,
databases,
services,
@@ -331,8 +353,10 @@ export async function getCurrentUser(
// No new token -> not switching teams
}
}
const pendingInvitations = await prisma.teamInvitation.findMany({ where: { uid: request.user.userId } })
return {
settings: await prisma.setting.findFirst(),
pendingInvitations,
supportedServiceTypesAndVersions,
token,
...request.user,

View File

@@ -5,9 +5,10 @@ import { decrypt, errorHandler, prisma, uniqueName } from '../../../../lib/commo
import { day } from '../../../../lib/dayjs';
import type { OnlyId } from '../../../../types';
import type { BodyId, InviteToTeam, SaveTeam, SetPermission } from './types';
import type { BodyId, DeleteUserFromTeam, InviteToTeam, SaveTeam, SetPermission } from './types';
export async function listTeams(request: FastifyRequest) {
export async function listAccounts(request: FastifyRequest) {
try {
const userId = request.user.userId;
const teamId = request.user.teamId;
@@ -15,10 +16,24 @@ export async function listTeams(request: FastifyRequest) {
where: { id: userId },
select: { id: true, email: true, teams: true }
});
let accounts = [];
let allTeams = [];
let accounts = await prisma.user.findMany({ where: { teams: { some: { id: teamId } } }, select: { id: true, email: true, teams: true } });
if (teamId === '0') {
accounts = await prisma.user.findMany({ select: { id: true, email: true, teams: true } });
}
return {
account,
accounts
};
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function listTeams(request: FastifyRequest) {
try {
const userId = request.user.userId;
const teamId = request.user.teamId;
let allTeams = [];
if (teamId === '0') {
allTeams = await prisma.team.findMany({
where: { users: { none: { id: userId } } },
include: { permissions: true }
@@ -28,18 +43,30 @@ export async function listTeams(request: FastifyRequest) {
where: { users: { some: { id: userId } } },
include: { permissions: true }
});
const invitations = await prisma.teamInvitation.findMany({ where: { uid: userId } });
return {
ownTeams,
allTeams,
invitations,
account,
accounts
};
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function removeUserFromTeam(request: FastifyRequest<DeleteUserFromTeam>, reply: FastifyReply) {
try {
const { uid } = request.body;
const { id } = request.params;
const userId = request.user.userId;
const foundUser = await prisma.team.findMany({ where: { id, users: { some: { id: userId } } } });
if (foundUser.length === 0) {
return errorHandler({ status: 404, message: 'Team not found' });
}
await prisma.team.update({ where: { id }, data: { users: { disconnect: { id: uid } } } });
await prisma.permission.deleteMany({ where: { teamId: id, userId: uid } })
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function deleteTeam(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
try {
const userId = request.user.userId;

View File

@@ -1,19 +1,22 @@
import { FastifyPluginAsync } from 'fastify';
import { acceptInvitation, changePassword, deleteTeam, getTeam, inviteToTeam, listTeams, newTeam, removeUser, revokeInvitation, saveTeam, setPermission } from './handlers';
import { acceptInvitation, changePassword, deleteTeam, getTeam, inviteToTeam, listAccounts, listTeams, newTeam, removeUser, removeUserFromTeam, revokeInvitation, saveTeam, setPermission } from './handlers';
import type { OnlyId } from '../../../../types';
import type { BodyId, InviteToTeam, SaveTeam, SetPermission } from './types';
import type { BodyId, DeleteUserFromTeam, InviteToTeam, SaveTeam, SetPermission } from './types';
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.addHook('onRequest', async (request) => {
return await request.jwtVerify()
})
fastify.get('/', async (request) => await listTeams(request));
fastify.get('/', async (request) => await listAccounts(request));
fastify.post('/new', async (request, reply) => await newTeam(request, reply));
fastify.get('/teams', async (request) => await listTeams(request));
fastify.get<OnlyId>('/team/:id', async (request, reply) => await getTeam(request, reply));
fastify.post<SaveTeam>('/team/:id', async (request, reply) => await saveTeam(request, reply));
fastify.delete<OnlyId>('/team/:id', async (request, reply) => await deleteTeam(request, reply));
fastify.post<DeleteUserFromTeam>('/team/:id/user/remove', async (request, reply) => await removeUserFromTeam(request, reply));
fastify.post<InviteToTeam>('/team/:id/invitation/invite', async (request, reply) => await inviteToTeam(request, reply))
fastify.post<BodyId>('/team/:id/invitation/accept', async (request) => await acceptInvitation(request));
@@ -23,7 +26,6 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.delete<BodyId>('/user/remove', async (request, reply) => await removeUser(request, reply));
fastify.post<BodyId>('/user/password', async (request, reply) => await changePassword(request, reply));
// fastify.delete('/user', async (request, reply) => await deleteUser(request, reply));
};

View File

@@ -5,6 +5,14 @@ export interface SaveTeam extends OnlyId {
name: string
}
}
export interface DeleteUserFromTeam {
Body: {
uid: string
},
Params: {
id: string
}
}
export interface InviteToTeam {
Body: {
email: string,

View File

@@ -36,6 +36,33 @@ export async function newService(request: FastifyRequest, reply: FastifyReply) {
return errorHandler({ status, message })
}
}
export async function cleanupUnconfiguredServices(request: FastifyRequest) {
try {
const teamId = request.user.teamId;
let services = await prisma.service.findMany({
where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } },
include: { destinationDocker: true, teams: true },
});
for (const service of services) {
if (!service.fqdn) {
if (service.destinationDockerId) {
await executeDockerCmd({
dockerId: service.destinationDockerId,
command: `docker ps -a --filter 'label=com.docker.compose.project=${service.id}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0`
})
await executeDockerCmd({
dockerId: service.destinationDockerId,
command: `docker ps -a --filter 'label=com.docker.compose.project=${service.id}' --format {{.ID}}|xargs -r -n 1 docker rm --force`
})
}
await removeService({ id: service.id });
}
}
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getServiceStatus(request: FastifyRequest<OnlyId>) {
try {
const teamId = request.user.teamId;

View File

@@ -5,6 +5,7 @@ import {
checkService,
checkServiceDomain,
cleanupPlausibleLogs,
cleanupUnconfiguredServices,
deleteService,
deleteServiceSecret,
deleteServiceStorage,
@@ -39,6 +40,8 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get('/', async (request) => await listServices(request));
fastify.post('/new', async (request, reply) => await newService(request, reply));
fastify.post<any>('/cleanup/unconfigured', async (request) => await cleanupUnconfiguredServices(request));
fastify.get<OnlyId>('/:id', async (request) => await getService(request));
fastify.post<SaveService>('/:id', async (request, reply) => await saveService(request, reply));
fastify.delete<OnlyId>('/:id', async (request) => await deleteService(request));

View File

@@ -9,7 +9,7 @@ export async function listSources(request: FastifyRequest) {
try {
const teamId = request.user?.teamId;
const sources = await prisma.gitSource.findMany({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
where: { OR: [{ teams: { some: { id: teamId === "0" ? undefined : teamId } } }, { isSystemWide: true }] },
include: { teams: true, githubApp: true, gitlabApp: true }
});
return {
@@ -22,11 +22,11 @@ export async function listSources(request: FastifyRequest) {
export async function saveSource(request, reply) {
try {
const { id } = request.params
let { name, htmlUrl, apiUrl, customPort } = request.body
let { name, htmlUrl, apiUrl, customPort, isSystemWide } = request.body
if (customPort) customPort = Number(customPort)
await prisma.gitSource.update({
where: { id },
data: { name, htmlUrl, apiUrl, customPort }
data: { name, htmlUrl, apiUrl, customPort, isSystemWide }
});
return reply.code(201).send()
} catch ({ status, message }) {
@@ -56,7 +56,7 @@ export async function getSource(request: FastifyRequest<OnlyId>) {
}
const source = await prisma.gitSource.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
where: { id, OR: [{ teams: { some: { id: teamId === "0" ? undefined : teamId } } }, { isSystemWide: true }] },
include: { githubApp: true, gitlabApp: true }
});
if (!source) {
@@ -104,7 +104,7 @@ export async function saveGitHubSource(request: FastifyRequest<SaveGitHubSource>
const { teamId } = request.user
const { id } = request.params
let { name, htmlUrl, apiUrl, organization, customPort } = request.body
let { name, htmlUrl, apiUrl, organization, customPort, isSystemWide } = request.body
if (customPort) customPort = Number(customPort)
if (id === 'new') {
@@ -117,6 +117,7 @@ export async function saveGitHubSource(request: FastifyRequest<SaveGitHubSource>
apiUrl,
organization,
customPort,
isSystemWide,
type: 'github',
teams: { connect: { id: teamId } }
}

View File

@@ -7,6 +7,7 @@ export interface SaveGitHubSource extends OnlyId {
apiUrl: string,
organization: string,
customPort: number,
isSystemWide: boolean
}
}
export interface SaveGitLabSource extends OnlyId {

View File

@@ -66,13 +66,19 @@ export async function configureGitHubApp(request, reply) {
}
export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promise<any> {
try {
const allowedGithubEvents = ['push', 'pull_request'];
const allowedGithubEvents = ['push', 'pull_request', 'ping', 'installation'];
const allowedActions = ['opened', 'reopened', 'synchronize', 'closed'];
const githubEvent = request.headers['x-github-event']?.toString().toLowerCase();
const githubSignature = request.headers['x-hub-signature-256']?.toString().toLowerCase();
if (!allowedGithubEvents.includes(githubEvent)) {
throw { status: 500, message: 'Event not allowed.' }
}
if (githubEvent === 'ping') {
return { pong: 'cool' }
}
if (githubEvent === 'installation') {
return { status: 'cool' }
}
let projectId, branch;
const body = request.body
if (githubEvent === 'push') {
@@ -80,7 +86,7 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
branch = body.ref.includes('/') ? body.ref.split('/')[2] : body.ref;
} else if (githubEvent === 'pull_request') {
projectId = body.pull_request.base.repo.id;
branch = body.pull_request.base.ref.includes('/') ? body.pull_request.base.ref.split('/')[2] : body.pull_request.base.ref;
branch = body.pull_request.base.ref
}
if (!projectId || !branch) {
throw { status: 500, message: 'Cannot parse projectId or branch from the webhook?!' }
@@ -147,7 +153,8 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
} else if (githubEvent === 'pull_request') {
const pullmergeRequestId = body.number.toString();
const pullmergeRequestAction = body.action;
const sourceBranch = body.pull_request.head.ref.includes('/') ? body.pull_request.head.ref.split('/')[2] : body.pull_request.head.ref;
const sourceBranch = body.pull_request.head.ref
const sourceRepository = body.pull_request.head.repo.full_name
if (!allowedActions.includes(pullmergeRequestAction)) {
throw { status: 500, message: 'Action not allowed.' }
}
@@ -205,6 +212,7 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
await prisma.build.create({
data: {
id: buildId,
sourceRepository,
pullmergeRequestId,
previewApplicationId,
sourceBranch,

View File

@@ -23,6 +23,7 @@ export interface GitHubEvents {
ref: string,
repo: {
id: string,
full_name: string,
}
}
}

View File

@@ -39,9 +39,7 @@ export async function configureGitLabApp(request: FastifyRequest<ConfigureGitLab
export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
const { object_kind: objectKind, ref, project_id } = request.body
try {
const allowedActions = ['opened', 'reopen', 'close', 'open', 'update'];
const webhookToken = request.headers['x-gitlab-token'];
if (!webhookToken && !isDev) {
throw { status: 500, message: 'Invalid webhookToken.' }
@@ -91,7 +89,7 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
}
}
} else if (objectKind === 'merge_request') {
const { object_attributes: { work_in_progress: isDraft, action, source_branch: sourceBranch, target_branch: targetBranch }, project: { id } } = request.body
const { object_attributes: { work_in_progress: isDraft, action, source_branch: sourceBranch, target_branch: targetBranch, source: { path_with_namespace: sourceRepository } }, project: { id } } = request.body
const pullmergeRequestId = request.body.object_attributes.iid.toString();
const projectId = Number(id);
if (!allowedActions.includes(action)) {
@@ -100,7 +98,6 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
if (isDraft) {
throw { status: 500, message: 'Draft MR, do nothing.' }
}
const applicationsFound = await getApplicationFromDBWebhook(projectId, targetBranch);
if (applicationsFound && applicationsFound.length > 0) {
for (const application of applicationsFound) {
@@ -153,6 +150,7 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
id: buildId,
pullmergeRequestId,
previewApplicationId,
sourceRepository,
sourceBranch,
applicationId: application.id,
destinationDockerId: application.destinationDocker.id,

View File

@@ -8,6 +8,9 @@ export interface GitLabEvents {
Body: {
object_attributes: {
work_in_progress: string
source: {
path_with_namespace: string
}
isDraft: string
action: string
source_branch: string

View File

@@ -0,0 +1 @@
<span class="badge bg-coollabs-gradient rounded text-white font-normal"> BETA </span>

View File

@@ -1,10 +1,13 @@
<script lang="ts">
import Beta from './Beta.svelte';
import Explaner from './Explainer.svelte';
import Tooltip from './Tooltip.svelte';
export let id: any;
export let customClass: any = null;
export let setting: any;
export let title: any;
export let isBeta: any = false;
export let description: any;
export let isCenter = true;
export let disabled = false;
@@ -18,13 +21,16 @@
<!-- svelte-ignore a11y-label-has-associated-control -->
<label>
{title}
{#if isBeta}
<Beta />
{/if}
{#if description && description !== ''}
<Explaner explanation={description} />
{/if}
</label>
</div>
</div>
<div class:text-center={isCenter} class="flex justify-center">
<div class:text-center={isCenter} class={`flex justify-center ${customClass}`}>
<div
on:click
aria-pressed="false"

View File

@@ -86,7 +86,7 @@
id="update"
disabled={updateStatus.success === false}
on:click={update}
class="icons bg-coollabs-gradient text-white duration-75 hover:scale-105 w-full"
class="icons bg-coollabs-gradient text-white duration-75 hover:scale-105 w-full"
>
{#if updateStatus.loading}
<svg

View File

@@ -27,6 +27,7 @@
import { onDestroy, onMount } from 'svelte';
import { get, post } from '$lib/api';
import { errorNotification } from '$lib/common';
import Beta from './Beta.svelte';
async function getStatus() {
if (loading.usage) return;
loading.usage = true;
@@ -78,7 +79,7 @@
<h1 class="font-bold text-lg lg:text-xl truncate">
{server.name}
{#if server.remoteEngine}
<span class="badge bg-coollabs-gradient rounded text-white"> BETA </span>
<Beta />
{/if}
</h1>
<div class="text-xs">

View File

@@ -328,7 +328,7 @@
"members": "Members",
"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",
"you": "(You)",
"you": "You",
"promote_to": "Promote to {{grade}}",
"revoke_invitation": "Revoke invitation",
"pending_invitation": "Pending invitation",

View File

@@ -318,6 +318,6 @@
"root": "(suprême)",
"root_team_explainer": "Il s'agit de l'équipe <span class='text-red-500 font-bold'>suprême</span>. \nCela signifie que les membres de ce groupe peuvent gérer les paramètres à l'échelle de l'instance et avoir tous les privilèges dans Coolify (imaginez comme un utilisateur root sous Linux).",
"send_invitation": "Envoyer une invitation",
"you": "(Toi)"
"you": "Toi"
}
}

View File

@@ -20,6 +20,7 @@ interface AppSession {
gitlab: string | null,
},
supportedServiceTypesAndVersions: Array<any>
pendingInvitations: Array<any>
}
interface AddToast {
type?: "info" | "success" | "error",
@@ -47,7 +48,8 @@ export const appSession: Writable<AppSession> = writable({
github: null,
gitlab: null
},
supportedServiceTypesAndVersions: []
supportedServiceTypesAndVersions: [],
pendingInvitations: []
});
export const disabledButton: Writable<boolean> = writable(false);
export const isDeploymentEnabled: Writable<boolean> = writable(false);

View File

@@ -19,7 +19,7 @@
<div class="dropdown dropdown-bottom">
<slot>
<label for="new" tabindex="0" class="btn btn-sm text-sm bg-coollabs hover:bg-coollabs-100">
Create New Resource <svg
<svg
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
@@ -31,8 +31,8 @@
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
></label
>
> Create New Resource
</label>
</slot>
<ul id="new" tabindex="0" class="dropdown-content menu p-2 shadow bg-coolgray-300 rounded w-52">

View File

@@ -66,6 +66,8 @@
<script lang="ts">
export let baseSettings: any;
export let supportedServiceTypesAndVersions: any;
export let pendingInvitations: any = 0;
$appSession.isRegistrationEnabled = baseSettings.isRegistrationEnabled;
$appSession.ipv4 = baseSettings.ipv4;
$appSession.ipv6 = baseSettings.ipv6;
@@ -74,10 +76,13 @@
$appSession.whiteLabeledDetails.icon = baseSettings.whiteLabeledIcon;
$appSession.supportedServiceTypesAndVersions = supportedServiceTypesAndVersions;
$appSession.pendingInvitations = pendingInvitations;
export let userId: string;
export let teamId: string;
export let permission: string;
export let isAdmin: boolean;
import '../tailwind.css';
import Cookies from 'js-cookie';
import { fade } from 'svelte/transition';
@@ -202,11 +207,16 @@
<a
id="iam"
sveltekit:prefetch
href="/iam"
class="icons hover:text-iam"
href={$appSession.pendingInvitations.length > 0 ? '/iam/pending' : '/iam'}
class="icons hover:text-iam indicator"
class:text-iam={$page.url.pathname.startsWith('/iam')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/iam')}
><svg
>
{#if $appSession.pendingInvitations.length > 0}
<span class="indicator-item rounded-full badge badge-primary mr-2"
>{pendingInvitations.length}</span
>
{/if}<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-9 w-9"
@@ -342,6 +352,7 @@
<li>
<a
id="servers"
class="no-underline icons hover:text-white hover:bg-sky-500"
sveltekit:prefetch
href="/servers"
@@ -387,7 +398,11 @@
<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
IAM {#if $appSession.pendingInvitations.length > 0}
<span class="indicator-item rounded-full badge badge-primary"
>{pendingInvitations.length}</span
>
{/if}
</a>
</li>
<li>

View File

@@ -91,12 +91,17 @@
forceDelete = true;
}
return errorNotification(error);
}
}
}
}
async function handleDeploySubmit(forceRebuild = false) {
if (!$isDeploymentEnabled) return;
if (!statusInterval) {
statusInterval = setInterval(async () => {
await getStatus();
}, 2000);
}
try {
const { buildId } = await post(`/applications/${id}/deploy`, {
...application,
@@ -212,30 +217,29 @@
{/if}
</div>
{#if $page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
<div class="px-2">
{#if forceDelete}
<button
on:click={() => deleteApplication(application.name, true)}
disabled={!$appSession.isAdmin}
class:bg-red-600={$appSession.isAdmin}
class:hover:bg-red-500={$appSession.isAdmin}
class="btn btn-sm btn-error text-sm"
>
Force Delete Application
</button>
{:else}
<button
on:click={() => deleteApplication(application.name, false)}
disabled={!$appSession.isAdmin}
class:bg-red-600={$appSession.isAdmin}
class:hover:bg-red-500={$appSession.isAdmin}
class="btn btn-sm btn-error text-sm"
>
Delete Application
</button>
{/if}
</div>
<div class="px-2">
{#if forceDelete}
<button
on:click={() => deleteApplication(application.name, true)}
disabled={!$appSession.isAdmin}
class:bg-red-600={$appSession.isAdmin}
class:hover:bg-red-500={$appSession.isAdmin}
class="btn btn-sm btn-error text-sm"
>
Force Delete Application
</button>
{:else}
<button
on:click={() => deleteApplication(application.name, false)}
disabled={!$appSession.isAdmin}
class:bg-red-600={$appSession.isAdmin}
class:hover:bg-red-500={$appSession.isAdmin}
class="btn btn-sm btn-error text-sm"
>
Delete Application
</button>
{/if}
</div>
{/if}
</nav>
<div
@@ -362,7 +366,7 @@
</svg>
</button>
<Tooltip triggeredBy="#forceredeploy">Force Redeploy (without cache)</Tooltip>
{:else if $isDeploymentEnabled}
{:else if $isDeploymentEnabled && !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
<button
class="icons flex items-center font-bold"
disabled={!$isDeploymentEnabled}

View File

@@ -152,8 +152,8 @@
</div>
{:else}
<form on:submit|preventDefault={handleSubmit} class="px-10">
<div class="flex lg:flex-row flex-col lg:space-y-0 space-y-2 space-x-0 lg:space-x-2 items-center">
<div class="custom-select-wrapper w-1/2"><label for="repository" class="pb-1">Repository</label>
<div class="flex lg:flex-row flex-col lg:space-y-0 space-y-2 space-x-0 lg:space-x-2 items-center lg:justify-center">
<div class="custom-select-wrapper w-full"><label for="repository" class="pb-1">Repository</label>
<Select
placeholder={loading.repositories
? $t('application.configuration.loading_repositories')
@@ -168,7 +168,7 @@
/>
</div>
<input class="hidden" bind:value={selected.projectId} name="projectId" />
<div class="custom-select-wrapper w-1/2"><label for="repository" class="pb-1">Branch</label>
<div class="custom-select-wrapper w-full"><label for="repository" class="pb-1">Branch</label>
<Select
placeholder={loading.branches
? $t('application.configuration.loading_branches')

View File

@@ -328,8 +328,11 @@
</script>
<form on:submit={handleSubmit}>
<div class="flex flex-col space-y-2 px-4 xl:flex-row xl:space-y-0 xl:space-x-2 ">
<div class="custom-select-wrapper">
<div
class="flex lg:flex-row flex-col lg:space-y-0 space-y-2 space-x-0 lg:space-x-2 items-center lg:justify-center lg:px-0 px-8"
>
<div class="custom-select-wrapper w-full">
<label for="groups" class="pb-1">Groups</label>
<Select
placeholder={loading.base
? $t('application.configuration.loading_groups')
@@ -354,7 +357,8 @@
optionIdentifier="id"
/>
</div>
<div class="custom-select-wrapper">
<div class="custom-select-wrapper w-full">
<label for="projects" class="pb-1">Projects</label>
<Select
placeholder={loading.projects
? $t('application.configuration.loading_projects')
@@ -379,7 +383,8 @@
isSearchable={true}
/>
</div>
<div class="custom-select-wrapper">
<div class="custom-select-wrapper w-full">
<label for="branches" class="pb-1">Branches</label>
<Select
placeholder={loading.branches
? $t('application.configuration.loading_branches')

View File

@@ -172,7 +172,6 @@
<div class="custom-select-wrapper">
<Select
class="w-full"
placeholder={loading.branches
? $t('application.configuration.loading_branches')
: branchSelectOptions.length ===0

View File

@@ -26,8 +26,6 @@
</script>
<script lang="ts">
import { t } from '$lib/translations';
export let application: any;
export let appId: string;
export let settings: any;

View File

@@ -25,7 +25,7 @@
</script>
<script lang="ts">
import { page, session } from '$app/stores';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { get, post } from '$lib/api';
import { t } from '$lib/translations';
@@ -53,7 +53,6 @@
return source;
}
});
async function handleSubmit(gitSourceId: string) {
try {
await post(`/applications/${id}/configuration/source`, { gitSourceId });
@@ -71,7 +70,7 @@
<div class="max-w-screen-2xl mx-auto px-9">
<div class="title pb-8">Git App</div>
<div class="flex flex-wrap justify-center">
{#if !filteredSources || ownSources.length === 0}
{#if !filteredSources}
<div class="flex-col">
<div class="pb-2 text-center font-bold">
{$t('application.configuration.no_configurable_git')}
@@ -169,11 +168,68 @@
<button
disabled={source.gitlabApp && !source.gitlabAppId}
type="submit"
class="disabled:opacity-95 bg-coolgray-200 disabled:text-white box-selection hover:bg-orange-700 group"
class="relative disabled:opacity-95 bg-coolgray-200 disabled:text-white box-selection hover:bg-orange-700 group"
class:border-red-500={source.gitlabApp && !source.gitlabAppId}
class:border-0={source.gitlabApp && !source.gitlabAppId}
class:border-l-4={source.gitlabApp && !source.gitlabAppId}
>
<div class="absolute top-0 left-0 -m-5 flex">
{#if source?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="h-10 w-10">
<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 source?.type === 'github'}
<svg viewBox="0 0 128 128" class="h-10 w-10">
<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}
{#if source.isSystemWide}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-10 w-10"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" />
<line x1="3.6" y1="9" x2="20.4" y2="9" />
<line x1="3.6" y1="15" x2="20.4" y2="15" />
<path d="M11.5 3a17 17 0 0 0 0 18" />
<path d="M12.5 3a17 17 0 0 1 0 18" />
</svg>
{/if}
</div>
<div class="font-bold text-xl text-center truncate">{source.name}</div>
{#if source.gitlabApp && !source.gitlabAppId}
<div class="font-bold text-center truncate text-red-500 group-hover:text-white">

View File

@@ -759,27 +759,32 @@
{/if}
{#if application.buildPack === 'docker'}
<div class="grid grid-cols-2 items-center pb-4">
<label for="dockerFileLocation"
class="mb-10"
<label for="dockerFileLocation" class=""
>Dockerfile Location <Explainer
explanation={"Should be absolute path, like <span class='text-settings font-bold'>/data/Dockerfile</span> or <span class='text-settings font-bold'>/Dockerfile.</span>"}
/></label
>
<div class="form-control w-full">
<input
class="w-full input"
disabled={isDisabled}
readonly={!$appSession.isAdmin}
name="dockerFileLocation"
id="dockerFileLocation"
bind:value={application.dockerFileLocation}
placeholder="default: /Dockerfile"
/>
<label class="label">
<span class="label-text-alt text-xs">Path: {application.baseDirectory.replace(/^\/$/,'')}{application.dockerFileLocation}</span>
</label>
</div>
class="w-full input"
disabled={isDisabled}
readonly={!$appSession.isAdmin}
name="dockerFileLocation"
id="dockerFileLocation"
bind:value={application.dockerFileLocation}
placeholder="default: /Dockerfile"
/>
{#if application.baseDirectory}
<label class="label">
<span class="label-text-alt text-xs"
>Path: {application.baseDirectory.replace(
/^\/$/,
''
)}{application.dockerFileLocation}</span
>
</label>
{/if}
</div>
</div>
{/if}
{#if !notNodeDeployments.includes(application.buildPack)}

View File

@@ -18,6 +18,7 @@
let fromDb = false;
let cancelInprogress = false;
let position = 0;
let loading = true;
const { id } = $page.params;
const cleanAnsiCodes = (str: string) => str.replace(/\x1B\[(\d+)m/g, '');
@@ -46,6 +47,7 @@
}
async function streamLogs(sequence = 0) {
try {
loading = true;
let {
logs: responseLogs,
status,
@@ -60,6 +62,7 @@
streamInterval = setInterval(async () => {
if (status !== 'running' && status !== 'queued') {
loading = false;
clearInterval(streamInterval);
return;
}
@@ -75,6 +78,7 @@
logs = logs.concat(
data.logs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) }))
);
loading = false;
} catch (error) {
return errorNotification(error);
}
@@ -171,13 +175,13 @@
<div
bind:this={logsEl}
on:scroll={detect}
class="font-mono w-full bg-coolgray-100 border border-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded mb-20 flex flex-col scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1"
class="font-mono w-full bg-coolgray-100 border border-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded mb-20 flex flex-col scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1 whitespace-pre"
>
{#each logs as log}
{#if fromDb}
<div>{log.line + '\n'}</div>
{log.line + '\n'}
{:else}
<div>[{day.unix(log.time).format('HH:mm:ss.SSS')}] {log.line + '\n'}</div>
[{day.unix(log.time).format('HH:mm:ss.SSS')}] {log.line + '\n'}
{/if}
{/each}
</div>
@@ -185,6 +189,10 @@
<div
class="font-mono w-full bg-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded mb-20 flex flex-col whitespace-nowrap scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1"
>
{dev ? 'In development, logs are shown in the console.' : 'No logs found yet.'}
{loading
? 'Loading logs...'
: dev
? 'In development, logs are shown in the console.'
: 'No logs found yet.'}
</div>
{/if}

View File

@@ -148,7 +148,7 @@
class="flex cursor-pointer items-center justify-center py-4 no-underline transition-all duration-150 hover:bg-coolgray-300 hover:shadow-xl"
class:bg-coolgray-200={$selectedBuildId === build.id}
>
<div class="flex-col px-2 text-center min-w-[10rem]">
<div class="flex-col px-2 text-center">
<div class="text-sm font-bold truncate">
{build.branch || application.branch}
</div>

View File

@@ -184,10 +184,10 @@
</div>
{:else if application.previewApplication.length > 0}
<div
class="grid grid-col gap-4 auto-cols-max grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
class="grid grid-col gap-4 auto-cols-max grid-cols-1 md:grid-cols-2 lg:grid-cols-2 px-6"
>
{#each application.previewApplication as preview}
<div class="no-underline mb-5 w-full lg:w-96">
<div class="no-underline mb-5 w-full">
<div class="w-full rounded p-5 bg-coolgray-200 indicator">
{#await getStatus(preview)}
<span class="indicator-item badge bg-yellow-500 badge-sm" />

View File

@@ -55,7 +55,6 @@
export let database: any;
import { del, get, post } from '$lib/api';
import { t } from '$lib/translations';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { errorNotification, handlerNotFoundLoad } from '$lib/common';
import { appSession, status, isDeploymentEnabled } from '$lib/store';

View File

@@ -0,0 +1,64 @@
<script lang="ts">
export let account: any;
export let accounts: any = [];
import { del, get, post } from '$lib/api';
import { errorNotification } from '$lib/common';
import { addToast, appSession } from '$lib/store';
async function resetPassword(id: any) {
const sure = window.confirm('Are you sure you want to reset the password?');
if (!sure) {
return;
}
try {
await post(`/iam/user/password`, { id });
return addToast({
message: 'Password reset successfully. Please relogin to reset it.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
}
}
async function deleteAccount(id: any) {
if (id === $appSession.userId || account.id === '0') return;
const sure = window.confirm('Are you sure you want to delete this user?');
if (!sure) {
return;
}
try {
await del(`/iam/user/remove`, { id });
addToast({
message: 'Account deleted.',
type: 'success'
});
const data = await get('/iam');
accounts = data.accounts;
} catch (error) {
return errorNotification(error);
}
}
</script>
<div class="flex flex-col lg:flex-row lg:space-y-0 space-y-2 lg:space-x-4">
<input
disabled
class="input w-full text-white"
readonly
placeholder="email"
value={account.email}
/>
<div class="flex flex-row items-center justify-center space-x-2 w-full lg:w-96">
<div class="flex items-center justify-center">
<button class="btn btn-sm btn-primary" on:click={() => resetPassword(account.id)}
>Reset Password</button
>
</div>
<div class="flex justify-center">
<button
class="btn btn-sm btn-error"
disabled={account.id === $appSession.userId || account.id === '0'}
on:click={() => deleteAccount(account.id)}>Delete Account</button
>
</div>
</div>
</div>

View File

@@ -0,0 +1,73 @@
<script lang="ts">
import { appSession } from '$lib/store';
import { page } from '$app/stores';
</script>
<ul class="menu border bg-coolgray-100 border-coolgray-200 rounded p-2 space-y-2 sticky top-4">
{#if $appSession.pendingInvitations.length > 0}
<li class="menu-title">
<span>IAM</span>
</li>
<li class="rounded" class:bg-coollabs={$page.url.pathname === `/iam/pending`}>
<a href={`/iam/pending`} class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M10 21v-6.5a3.5 3.5 0 0 0 -7 0v6.5h18v-6a4 4 0 0 0 -4 -4h-10.5" />
<path d="M12 11v-8h4l2 2l-2 2h-4" />
<path d="M6 15h1" />
</svg>Pending Invitations</a
>
</li>
{/if}
<li class="menu-title">
<span>IAM</span>
</li>
<li class="rounded" class:bg-coollabs={$page.url.pathname === `/iam`}>
<a href={`/iam`} class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="7" r="4" />
<path d="M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" />
</svg>{$appSession.userId === '0' && $appSession.teamId === '0' ? 'Accounts' : 'Account'}</a
>
</li>
<li class="rounded" class:bg-coollabs={$page.url.pathname.startsWith(`/iam/teams`)}>
<a href={`/iam/teams`} class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<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>Teams</a
>
</li>
</ul>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { appSession } from '$lib/store';
import Menu from './_Menu.svelte';
</script>
<div class="mx-auto max-w-screen-2xl px-6 grid grid-cols-1 lg:grid-cols-2">
<nav class="header flex flex-row order-2 lg:order-1 px-0 lg:px-4 items-start">
<div class="title lg:pb-10">Identity & Access Management</div>
</nav>
</div>
<div class="mx-auto max-w-screen-2xl px-0 lg:px-2 grid grid-cols-1 lg:grid-cols-4">
<nav class="header flex flex-col lg:pt-0 lg:col-span-1">
<Menu />
</nav>
<div class="pt-0 lg:col-span-3 pb-24 px-6">
<slot />
</div>
</div>

View File

@@ -20,224 +20,54 @@
<script lang="ts">
export let account: any;
export let accounts: any;
export let invitations: any;
export let ownTeams: any;
export let allTeams: any;
import { del, get, post } from '$lib/api';
import { errorNotification } from '$lib/common';
import { addToast, appSession } from '$lib/store';
import { appSession } from '$lib/store';
import { get, post } from '$lib/api';
import { goto } from '$app/navigation';
import Cookies from 'js-cookie';
if (accounts.length === 0) {
accounts.push(account);
}
import { page } from '$app/stores';
import Account from './_Account.svelte';
let search = '';
let searchResults: any = [];
async function resetPassword(id: any) {
const sure = window.confirm('Are you sure you want to reset the password?');
if (!sure) {
return;
}
try {
await post(`/iam/user/password`, { id });
return addToast({
message: 'Password reset successfully. Please relogin to reset it.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
}
}
async function deleteUser(id: any) {
const sure = window.confirm('Are you sure you want to delete this user?');
if (!sure) {
return;
}
try {
await del(`/iam/user/remove`, { id });
addToast({
message: 'Account deleted.',
type: 'success'
});
const data = await get('/iam');
accounts = data.accounts;
} catch (error) {
return errorNotification(error);
}
}
async function acceptInvitation(id: any, teamId: any) {
try {
await post(`/iam/team/${teamId}/invitation/accept`, { id });
return window.location.reload();
} catch (error) {
return errorNotification(error);
}
}
async function revokeInvitation(id: any, teamId: any) {
try {
await post(`/iam/team/${teamId}/invitation/revoke`, { id });
return window.location.reload();
} catch (error) {
return errorNotification(error);
}
}
async function switchTeam(selectedTeamId: any) {
try {
const payload = await get(`/user?teamId=${selectedTeamId}`);
if (payload.token) {
Cookies.set('token', payload.token, {
path: '/'
});
$appSession.teamId = payload.teamId;
$appSession.userId = payload.userId;
$appSession.permission = payload.permission;
$appSession.isAdmin = payload.isAdmin;
return window.location.reload();
}
} catch (error) {
console.error(error);
return errorNotification(error);
}
}
async function newTeam() {
const { id } = await post('/iam/new', {});
return await goto(`/iam/team/${id}`, { replaceState: true });
function searchAccount() {
searchResults = accounts.filter((account: { email: string | string[] }) => {
return account.email.includes(search);
});
}
</script>
<nav class="header">
<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">
<svg
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
>
</button>
</nav>
<br />
{#if invitations.length > 0}
<div class="mx-auto max-w-6xl px-6 py-4">
<div class="title font-bold">Pending invitations</div>
<div class="pt-10 text-center">
{#each invitations as invitation}
<div class="flex justify-center space-x-2">
<div>
Invited to <span class="font-bold text-pink-600">{invitation.teamName}</span> with
<span class="font-bold text-rose-600">{invitation.permission}</span> permission.
</div>
<button
class="btn btn-sm btn-success"
on:click={() => acceptInvitation(invitation.id, invitation.teamId)}>Accept</button
>
<button
class="btn btn-sm btn-error"
on:click={() => revokeInvitation(invitation.id, invitation.teamId)}>Delete</button
>
</div>
{/each}
</div>
</div>
{/if}
<div class="mx-auto max-w-6xl px-6 py-4">
{#if $appSession.teamId === '0' && accounts.length > 0}
<div class="title font-bold">Accounts</div>
{:else}
<div class="title font-bold">Account</div>
{/if}
<div class="flex items-center justify-center pt-10">
<table class="mx-2 text-left">
<thead class="mb-2">
<tr>
{#if accounts.length > 1}
<th class="px-2">Email</th>
<th>Actions</th>
{/if}
</tr>
</thead>
<tbody>
{#each accounts as account}
<tr class="grid items-center justify-center gap-2 lg:grid-flow-col">
<td class="px-2">{account.email}</td>
<td class="flex space-x-2">
<form on:submit|preventDefault={() => resetPassword(account.id)}>
<button class="my-4 btn btn-sm bg-iam">Reset Password</button>
</form>
<form on:submit|preventDefault={() => deleteUser(account.id)}>
<button
disabled={account.id === $appSession.userId}
class="my-4 btn btn-sm"
type="submit">Delete User</button
>
</form>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
<div class="mx-auto max-w-6xl px-6">
<div class="title font-bold">Teams</div>
<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">
{#each ownTeams as team}
<a href="/iam/team/{team.id}" class="p-2 no-underline">
<div class="box-selection relative">
<div>
<div class="truncate text-center text-xl font-bold">
{team.name}
</div>
<div class="mt-1 text-center text-xs">
{team.permissions?.length} member(s)
</div>
</div>
<div class="flex items-center justify-center pt-3">
<button
on:click|preventDefault={() => switchTeam(team.id)}
class="btn btn-sm"
class:bg-fuchsia-600={$appSession.teamId !== team.id}
class:hover:bg-fuchsia-500={$appSession.teamId !== team.id}
class:bg-transparent={$appSession.teamId === team.id}
disabled={$appSession.teamId === team.id}
>{$appSession.teamId === team.id ? 'Current Team' : 'Switch Team'}</button
>
</div>
</div>
</a>
{/each}
</div>
{#if $appSession.teamId === '0' && allTeams.length > 0}
<div class="pb-5 pt-10 text-xl font-bold">Other Teams</div>
<div class="flex flex-row flex-wrap justify-center px-2 md:flex-row">
{#each allTeams as team}
<a href="/iam/team/{team.id}" class="p-2 no-underline">
<div
class="box-selection relative"
class:hover:bg-fuchsia-600={team.id !== '0'}
class:hover:bg-red-500={team.id === '0'}
>
<div class="truncate text-center text-xl font-bold">
{team.name}
</div>
<div class="mt-1 text-center">{team.permissions?.length} member(s)</div>
</div>
</a>
{/each}
<div class="w-full">
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2 items-center">
<div class="title font-bold pb-3">
{$appSession.userId === '0' && $appSession.teamId === '0' ? 'Accounts' : 'Your account'}
</div>
{/if}
</div>
</div>
</div>
{#if $appSession.userId === '0' && $appSession.teamId === '0'}
<div class="w-full grid gap-2">
<input
class="input w-full mb-4"
bind:value={search}
on:input={searchAccount}
placeholder="Search for account..."
/>
<div class="flex flex-col pb-2 space-y-4 lg:space-y-2">
{#if searchResults.length > 0}
{#each searchResults as account}
<Account {account} {accounts} />
{/each}
{:else if searchResults.length === 0 && search !== ''}
<div>Nothing found.</div>
{:else}
{#each accounts as account}
<Account {account} {accounts} />
{/each}
{/if}
</div>
</div>
{:else}
<Account {account} />
{/if}

View File

@@ -0,0 +1,55 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { post } from '$lib/api';
import { errorNotification } from '$lib/common';
import { appSession } from '$lib/store';
if ($appSession.pendingInvitations.length === 0) {
goto('/iam/teams');
}
async function acceptInvitation(id: any, teamId: any) {
try {
await post(`/iam/team/${teamId}/invitation/accept`, { id });
return window.location.reload();
} catch (error) {
return errorNotification(error);
}
}
async function revokeInvitation(id: any, teamId: any) {
try {
await post(`/iam/team/${teamId}/invitation/revoke`, { id });
return window.location.reload();
} catch (error) {
return errorNotification(error);
}
}
</script>
<div class="w-full">
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2 items-center">
<div class="title font-bold pb-3">Pending Invitations</div>
</div>
</div>
</div>
<div class="w-full grid gap-2">
<div class="flex flex-col pb-2 space-y-4 lg:space-y-2">
{#each $appSession.pendingInvitations as invitation}
<div class="flex flex-col justify-center items-center">
<div class="text-xl pb-4 text-center">
Invited to <span class="font-bold text-pink-500">{invitation.teamName}</span> with
<span class="font-bold text-red-500">{invitation.permission}</span> permission.
</div>
<div class=" flex space-x-2">
<button
class="btn btn-primary"
on:click={() => acceptInvitation(invitation.id, invitation.teamId)}>Accept</button
>
<button class="btn" on:click={() => revokeInvitation(invitation.id, invitation.teamId)}
>Ignore</button
>
</div>
</div>
{/each}
</div>
</div>

View File

@@ -1,84 +0,0 @@
<script context="module" lang="ts">
import { del, get } from '$lib/api';
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ params, url }) => {
try {
const response = await get(`/iam/team/${params.id}`);
if (!response.permissions || Object.entries(response.permissions).length === 0) {
return {
status: 302,
redirect: '/iam'
};
}
return {
props: {
...response
},
stuff: {
...response
}
};
} catch (error) {
return handlerNotFoundLoad(error, url);
}
};
</script>
<script lang="ts">
export let team: any;
export let currentTeam: string;
export let teams: any;
import { page } from '$app/stores';
import { errorNotification, handlerNotFoundLoad } from '$lib/common';
import { appSession } from '$lib/store';
import { t } from '$lib/translations';
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import { goto } from '$app/navigation';
import Cookies from 'js-cookie';
import Tooltip from '$lib/components/Tooltip.svelte';
const { id } = $page.params;
async function deleteTeam() {
const sure = confirm('Are you sure you want to delete this team?');
if (sure) {
try {
await del(`/iam/team/${id}`, { id });
if (currentTeam === id) {
const switchTeam = teams.find((team: any) => team.id !== id);
const payload = await get(`/user?teamId=${switchTeam.id}`);
if (payload.token) {
Cookies.set('token', payload.token, {
path: '/'
});
$appSession.teamId = payload.teamId;
$appSession.userId = payload.userId;
$appSession.permission = payload.permission;
$appSession.isAdmin = payload.isAdmin;
return window.location.assign('/iam');
}
}
return await goto('/iam', { replaceState: true });
} catch (error) {
return errorNotification(error);
}
}
}
</script>
{#if id !== 'new'}
<nav class="nav-side">
{#if team.id !== '0'}
<button
id="delete"
on:click={deleteTeam}
type="submit"
disabled={!$appSession.isAdmin}
class:hover:text-red-500={$appSession.isAdmin}
class="icons bg-transparent text-sm"><DeleteIcon /></button
>
<Tooltip triggeredBy="#delete">Delete</Tooltip>
{/if}
</nav>
{/if}
<slot />

View File

@@ -0,0 +1,108 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async () => {
try {
const response = await get(`/iam/teams`);
return {
props: {
...response
}
};
} catch (error: any) {
return {
status: 500,
error: new Error(error)
};
}
};
</script>
<script lang="ts">
export let allTeams: any;
export let ownTeams: any;
import { get, post } from '$lib/api';
import Cookies from 'js-cookie';
import { appSession } from '$lib/store';
import { errorNotification } from '$lib/common';
import { goto } from '$app/navigation';
async function switchTeam(selectedTeamId: any) {
try {
const payload = await get(`/user?teamId=${selectedTeamId}`);
if (payload.token) {
Cookies.set('token', payload.token, {
path: '/'
});
$appSession.teamId = payload.teamId;
$appSession.userId = payload.userId;
$appSession.permission = payload.permission;
$appSession.isAdmin = payload.isAdmin;
return window.location.reload();
}
} catch (error) {
console.error(error);
return errorNotification(error);
}
}
async function newTeam() {
const { id } = await post('/iam/new', {});
return await goto(`/iam/teams/${id}`, { replaceState: true });
}
</script>
<div class="w-full">
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2 items-center pb-3">
<div class="title font-bold">Teams</div>
<button on:click={newTeam} class="btn btn-sm btn-primary"> Add New Team </button>
</div>
</div>
</div>
<div class="grid grid-col gap-4 auto-cols-max grid-cols-1 md:grid-cols-2 lg:grid-cols-2 px-6">
{#each ownTeams as team}
<a href="/iam/teams/{team.id}" class="p-2 no-underline">
<div
class="flex flex-col w-full rounded p-5 bg-coolgray-200 hover:bg-coolgray-300 indicator duration-150 h-36"
>
<div>
<div class="truncate text-center text-xl font-bold">
{team.name}
{#if $appSession.teamId === team.id}
<button class="badge bg-applications text-white font-bold rounded">Active Team</button
>
{/if}
</div>
<div class="mt-1 text-center text-xs">
{team.permissions?.length} member(s)
</div>
</div>
<div class="flex items-center justify-center pt-3">
{#if $appSession.teamId !== team.id}
<button
on:click|preventDefault={() => switchTeam(team.id)}
class="btn btn-sm btn-primary">Switch to this team</button
>
{/if}
</div>
</div>
</a>
{/each}
</div>
<div class="divider w-32 mx-auto" />
<div class="grid grid-col gap-4 auto-cols-max grid-cols-1 md:grid-cols-2 lg:grid-cols-3 px-6">
{#if $appSession.teamId === '0' && allTeams.length > 0}
{#each allTeams as team}
<a href="/iam/teams/{team.id}" class="p-2 no-underline">
<div
class="flex flex-col w-full rounded p-5 bg-coolgray-200 hover:bg-coolgray-300 indicator duration-150 relative"
>
<div class="truncate text-center text-xl font-bold">
{team.name}
</div>
<div class="mt-1 text-center text-xs">{team.permissions?.length} member(s)</div>
</div>
</a>
{/each}
{/if}
</div>

View File

@@ -0,0 +1,31 @@
<script context="module" lang="ts">
import { del, get } from '$lib/api';
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ params, url }) => {
try {
const response = await get(`/iam/team/${params.id}`);
if (!response.permissions || Object.entries(response.permissions).length === 0) {
return {
status: 302,
redirect: '/iam/teams'
};
}
return {
props: {
...response
},
stuff: {
...response
}
};
} catch (error) {
return handlerNotFoundLoad(error, url);
}
};
</script>
<script lang="ts">
import { handlerNotFoundLoad } from '$lib/common';
</script>
<slot />

View File

@@ -8,16 +8,23 @@
</script>
<script lang="ts">
export let currentTeam: string;
export let teams: any[];
export let permissions: any;
export let team: any;
export let invitations: any[];
import { page } from '$app/stores';
import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
import { post } from '$lib/api';
import { del, get, post } from '$lib/api';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
import { appSession } from '$lib/store';
import { addToast, appSession } from '$lib/store';
import Explainer from '$lib/components/Explainer.svelte';
import Cookies from 'js-cookie';
import { goto } from '$app/navigation';
const { id } = $page.params;
let invitation: any = {
teamName: team.name,
email: null,
@@ -54,7 +61,7 @@
}
async function removeFromTeam(uid: string) {
try {
await post(`/iam/team/${id}/user/remove`, { teamId: team.id, uid });
await post(`/iam/team/${id}/user/remove`, { uid });
return window.location.reload();
} catch (error) {
return errorNotification(error);
@@ -75,41 +82,124 @@
async function handleSubmit() {
try {
await post(`/iam/team/${id}`, { ...team });
return window.location.reload();
return addToast({
message: 'Settings updated.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
}
}
async function deleteTeam() {
const sure = confirm('Are you sure you want to delete this team?');
if (sure) {
try {
const switchTeam = teams.find((team: any) => team.id !== id);
if (!switchTeam) {
return addToast({
message: 'You cannot delete your last team.',
type: 'error'
});
}
await del(`/iam/team/${id}`, { id });
if (currentTeam === id) {
const payload = await get(`/user?teamId=${switchTeam.id}`);
if (payload.token) {
Cookies.set('token', payload.token, {
path: '/'
});
$appSession.teamId = payload.teamId;
$appSession.userId = payload.userId;
$appSession.permission = payload.permission;
$appSession.isAdmin = payload.isAdmin;
return window.location.assign('/iam');
}
}
return await goto('/iam/teams', { replaceState: true });
} catch (error) {
return errorNotification(error);
}
}
}
async function leaveTeam(uid: string) {
const sure = confirm('Are you sure you want to leave this team?');
if (sure) {
try {
const switchTeam = teams.find((team: any) => team.id !== id);
const foundAdmin = team.permissions.filter(
(permission: any) => permission.userId !== uid && permission.permission === 'admin'
);
if (!switchTeam) {
return addToast({
message: 'You cannot leave your last team.',
type: 'error'
});
}
if (!foundAdmin.length) {
return addToast({
message: 'You cannot leave this team without an admin.',
type: 'error'
});
}
await post(`/iam/team/${id}/user/remove`, { uid });
if (currentTeam === id) {
const payload = await get(`/user?teamId=${switchTeam.id}`);
if (payload.token) {
Cookies.set('token', payload.token, {
path: '/'
});
$appSession.teamId = payload.teamId;
$appSession.userId = payload.userId;
$appSession.permission = payload.permission;
$appSession.isAdmin = payload.isAdmin;
return window.location.assign('/iam');
}
}
return await goto('/iam/teams', { replaceState: true });
} catch (error) {
return errorNotification(error);
}
}
}
</script>
<div class="flex space-x-1 p-6 px-6 text-2xl font-bold">
<div class="tracking-tight">{$t('index.team')}</div>
<span class="arrow-right-applications px-1 text-fuchsia-500">></span>
<span class="pr-2">{team.name}</span>
<div class="w-full">
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2 items-center pb-3">
<div class="title font-bold">{team.name}</div>
<button class="btn btn-sm bg-primary" on:click={handleSubmit}>{$t('forms.save')}</button>
<button
id="delete"
on:click={deleteTeam}
type="submit"
disabled={!$appSession.isAdmin}
class="btn btn-sm bg-error">Remove Team</button
>
</div>
</div>
</div>
<div class="mx-auto max-w-6xl px-6">
<form on:submit|preventDefault={handleSubmit} class=" py-4">
<div class="flex space-x-1 pb-5">
<div class="title font-bold">{$t('index.settings')}</div>
<button class="btn btn-sm bg-iam" type="submit">{$t('forms.save')}</button>
</div>
<div class="grid grid-flow-row gap-2 px-10">
<div class="mt-2 grid grid-cols-2">
<div class="flex-col">
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
{#if team.id === '0'}
<SimpleExplainer customClass="w-full" text={$t('team.root_team_explainer')} />
{/if}
</div>
<input id="name" name="name" placeholder="name" bind:value={team.name} />
<div class="mx-auto">
<div class="flex space-x-1 pb-5">
<div class="title font-bold">{$t('index.settings')}</div>
</div>
<div class="grid grid-flow-row gap-2 px-4">
<div class="mt-2 grid grid-cols-2">
<div class="flex-col">
<label for="name">{$t('forms.name')}</label>
{#if team.id === '0'}
<Explainer explanation={$t('team.root_team_explainer')} />
{/if}
</div>
<input id="name" name="name" placeholder="name" bind:value={team.name} class="input w-full" />
</div>
</form>
</div>
<div class="flex space-x-1 py-5 pt-10 font-bold">
<div class="title">{$t('team.members')}</div>
</div>
<div class="px-4 sm:px-6">
<div class="px-4">
<table class="w-full border-separate text-left">
<thead>
<tr class="h-8 border-b border-coolgray-400">
@@ -122,25 +212,33 @@
<tr class="text-xs">
<td class="py-4"
>{permission.user.email}
<span class="font-bold"
>{permission.user.id === $appSession.userId ? $t('team.you') : ''}</span
></td
>
{#if permission.user.id === $appSession.userId}
<span class="font-bold badge badge-primary text-xs">{$t('team.you')}</span>
{/if}
</td>
<td class="py-4">{permission.permission}</td>
{#if $appSession.isAdmin && permission.user.id !== $appSession.userId && permission.permission !== 'owner'}
<td class="flex flex-col items-center justify-center space-y-2 py-4 text-center">
<button
class="btn btn-sm btn-error"
on:click={() => removeFromTeam(permission.user.id)}>{$t('forms.remove')}</button
>
<td
class="flex flex-col lg:flex-row justify-center lg:space-y-0 space-y-2 space-x-0 lg:space-x-2 text-center"
>
<button
class="btn btn-sm"
on:click={() =>
changePermission(permission.user.id, permission.id, permission.permission)}
>{$t('team.promote_to', {
grade: permission.permission === 'admin' ? 'read' : 'admin'
grade: permission.permission === 'admin' ? 'Read' : 'Admin'
})}</button
>
<button
class="btn btn-sm btn-error"
on:click={() => removeFromTeam(permission.user.id)}>{$t('forms.remove')}</button
>
</td>
{:else if permission.user.id === $appSession.userId}
<td class="py-4 flex flex-row justify-center">
<button class="btn btn-sm btn-primary" on:click={() => leaveTeam(permission.user.id)}
>Leave Team</button
>
</td>
{:else}
<td class="text-center py-4 flex-col space-y-2">
@@ -156,9 +254,7 @@
<td class="py-4 font-bold text-yellow-500">{invitation.permission}</td>
{#if isAdmin(team.permissions[0].permission)}
<td class="flex-col space-y-2 py-4 text-center">
<button
class="btn btn-sm btn-error"
on:click={() => revokeInvitation(invitation.id)}
<button class="btn btn-sm btn-error" on:click={() => revokeInvitation(invitation.id)}
>{$t('team.revoke_invitation')}</button
>
</td>
@@ -174,18 +270,16 @@
<div class="flex space-x-1">
<div class="flex space-x-1">
<div class="title font-bold">{$t('team.invite_new_member')}</div>
<button class="btn btn-sm bg-iam" type="submit"
>{$t('team.send_invitation')}</button
>
<button class="btn btn-sm bg-primary" type="submit">{$t('team.send_invitation')}</button>
</div>
</div>
<SimpleExplainer text={$t('team.invite_only_register_explainer')} />
<div class="flex-col space-y-2 px-4 pt-5 sm:px-6">
<div class="flex-col pt-5">
<div class="flex space-x-0">
<input
bind:value={invitation.email}
placeholder={$t('forms.email')}
class="mr-2 w-full"
class="input mr-2 w-full"
required
/>
<div class="flex-1" />

View File

@@ -21,6 +21,9 @@
<script lang="ts">
export let applications: any;
export let foundUnconfiguredApplication: boolean;
export let foundUnconfiguredService: boolean;
export let foundUnconfiguredDatabase: boolean;
export let databases: any;
export let services: any;
export let settings: any;
@@ -28,9 +31,9 @@
export let destinations: any;
let filtered: any = setInitials();
import { get } from '$lib/api';
import { get, post } from '$lib/api';
import { t } from '$lib/translations';
import { asyncSleep, getRndInteger } from '$lib/common';
import { asyncSleep, errorNotification, getRndInteger } from '$lib/common';
import { appSession, search } from '$lib/store';
import ApplicationsIcons from '$lib/components/svg/applications/ApplicationIcons.svelte';
@@ -41,23 +44,39 @@
let numberOfGetStatus = 0;
let status: any = {};
let noInitialStatus: any = {
applications: false,
services: false,
databases: false
};
let loading = {
applications: false,
services: false,
databases: false
};
doSearch();
async function refreshStatusApplications() {
noInitialStatus.applications = false;
numberOfGetStatus = 0;
for (const application of applications) {
status[application.id] = 'loading';
getStatus(application, true);
}
}
async function refreshStatusServices() {
noInitialStatus.services = false;
numberOfGetStatus = 0;
for (const service of services) {
status[service.id] = 'loading';
getStatus(service, true);
}
}
async function refreshStatusDatabases() {
noInitialStatus.databases = false;
numberOfGetStatus = 0;
for (const database of databases) {
status[database.id] = 'loading';
getStatus(database, true);
}
}
@@ -108,10 +127,16 @@
async function getStatus(resources: any, force: boolean = false) {
const { id, buildPack, dualCerts, type } = resources;
if (buildPack && applications.length > 10 && !force) {
if (buildPack && applications.length + filtered.otherApplications.length > 10 && !force) {
noInitialStatus.applications = true;
return;
}
if (type && services.length > 10 && !force) {
if (type && services.length + filtered.otherServices.length > 10 && !force) {
noInitialStatus.services = true;
return;
}
if (databases.length + filtered.otherDatabases.length > 10 && !force) {
noInitialStatus.databases = true;
return;
}
if (status[id] && !force) return status[id];
@@ -300,6 +325,45 @@
filtered = setInitials();
}
}
async function cleanupApplications() {
try {
const sure = confirm(
'Are you sure? This will delete all UNCONFIGURED applications and their data.'
);
if (sure) {
await post(`/applications/cleanup/unconfigured`, {});
return window.location.reload();
}
} catch (error) {
return errorNotification(error);
}
}
async function cleanupServices() {
try {
const sure = confirm(
'Are you sure? This will delete all UNCONFIGURED services and their data.'
);
if (sure) {
await post(`/services/cleanup/unconfigured`, {});
return window.location.reload();
}
} catch (error) {
return errorNotification(error);
}
}
async function cleanupDatabases() {
try {
const sure = confirm(
'Are you sure? This will delete all UNCONFIGURED databases and their data.'
);
if (sure) {
await post(`/databases/cleanup/unconfigured`, {});
return window.location.reload();
}
} catch (error) {
return errorNotification(error);
}
}
</script>
<nav class="header">
@@ -309,129 +373,129 @@
{/if}
</nav>
<div class="container lg:mx-auto lg:p-0 px-8 pt-5">
<div class="space-x-2 lg:flex lg:justify-center text-center mb-4 ">
<button
class="btn btn-sm btn-ghost"
class:bg-applications={$search === '!app'}
class:hover:bg-coollabs={$search !== '!app'}
on:click={() => doSearch('!app')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 mr-2 hidden lg:block "
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentcolor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="4" y="4" width="6" height="6" rx="1" />
<rect x="4" y="14" width="6" height="6" rx="1" />
<rect x="14" y="14" width="6" height="6" rx="1" />
<line x1="14" y1="7" x2="20" y2="7" />
<line x1="17" y1="4" x2="17" y2="10" />
</svg> Applications</button
>
<button
class="btn btn-sm btn-ghost"
class:bg-services={$search === '!service'}
class:hover:bg-coollabs={$search !== '!service'}
on:click={() => doSearch('!service')}
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 mr-2 hidden lg:block"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 18a4.6 4.4 0 0 1 0 -9a5 4.5 0 0 1 11 2h1a3.5 3.5 0 0 1 0 7h-12" />
</svg> Services</button
>
<button
class="btn btn-sm btn-ghost "
class:bg-databases={$search === '!db'}
class:hover:bg-coollabs={$search !== '!db'}
on:click={() => doSearch('!db')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 mr-2 hidden lg:block"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<ellipse cx="12" cy="6" rx="8" ry="3" />
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
</svg> Databases</button
>
<button
class="btn btn-sm btn-ghost"
class:bg-sources={$search === '!git'}
class:hover:bg-coollabs={$search !== '!git'}
on:click={() => doSearch('!git')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 mr-2 hidden lg:block"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="6" cy="6" r="2" />
<circle cx="18" cy="18" r="2" />
<path d="M11 6h5a2 2 0 0 1 2 2v8" />
<polyline points="14 9 11 6 14 3" />
<path d="M13 18h-5a2 2 0 0 1 -2 -2v-8" />
<polyline points="10 15 13 18 10 21" />
</svg> Git Sources</button
>
<button
class="btn btn-sm btn-ghost"
class:bg-destinations={$search === '!destination'}
class:hover:bg-coollabs={$search !== '!destination'}
on:click={() => doSearch('!destination')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 mr-2 hidden lg:block"
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="M22 12.54c-1.804 -.345 -2.701 -1.08 -3.523 -2.94c-.487 .696 -1.102 1.568 -.92 2.4c.028 .238 -.32 1.002 -.557 1h-14c0 5.208 3.164 7 6.196 7c4.124 .022 7.828 -1.376 9.854 -5c1.146 -.101 2.296 -1.505 2.95 -2.46z"
/>
<path d="M5 10h3v3h-3z" />
<path d="M8 10h3v3h-3z" />
<path d="M11 10h3v3h-3z" />
<path d="M8 7h3v3h-3z" />
<path d="M11 7h3v3h-3z" />
<path d="M11 4h3v3h-3z" />
<path d="M4.571 18c1.5 0 2.047 -.074 2.958 -.78" />
<line x1="10" y1="16" x2="10" y2="16.01" />
</svg>Destinations</button
>
</div>
{#if applications.length !== 0 || destinations.length !== 0 || databases.length !== 0 || services.length !== 0 || gitSources.length !== 0 || destinations.length !== 0}
<div class="space-x-2 lg:flex lg:justify-center text-center mb-4 ">
<button
class="btn btn-sm btn-ghost"
class:bg-applications={$search === '!app'}
class:hover:bg-coollabs={$search !== '!app'}
on:click={() => doSearch('!app')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 mr-2 hidden lg:block "
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentcolor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="4" y="4" width="6" height="6" rx="1" />
<rect x="4" y="14" width="6" height="6" rx="1" />
<rect x="14" y="14" width="6" height="6" rx="1" />
<line x1="14" y1="7" x2="20" y2="7" />
<line x1="17" y1="4" x2="17" y2="10" />
</svg> Applications</button
>
<button
class="btn btn-sm btn-ghost"
class:bg-services={$search === '!service'}
class:hover:bg-coollabs={$search !== '!service'}
on:click={() => doSearch('!service')}
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 mr-2 hidden lg:block"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 18a4.6 4.4 0 0 1 0 -9a5 4.5 0 0 1 11 2h1a3.5 3.5 0 0 1 0 7h-12" />
</svg> Services</button
>
<button
class="btn btn-sm btn-ghost "
class:bg-databases={$search === '!db'}
class:hover:bg-coollabs={$search !== '!db'}
on:click={() => doSearch('!db')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 mr-2 hidden lg:block"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<ellipse cx="12" cy="6" rx="8" ry="3" />
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
</svg> Databases</button
>
<button
class="btn btn-sm btn-ghost"
class:bg-sources={$search === '!git'}
class:hover:bg-coollabs={$search !== '!git'}
on:click={() => doSearch('!git')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 mr-2 hidden lg:block"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="6" cy="6" r="2" />
<circle cx="18" cy="18" r="2" />
<path d="M11 6h5a2 2 0 0 1 2 2v8" />
<polyline points="14 9 11 6 14 3" />
<path d="M13 18h-5a2 2 0 0 1 -2 -2v-8" />
<polyline points="10 15 13 18 10 21" />
</svg> Git Sources</button
>
<button
class="btn btn-sm btn-ghost"
class:bg-destinations={$search === '!destination'}
class:hover:bg-coollabs={$search !== '!destination'}
on:click={() => doSearch('!destination')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 mr-2 hidden lg:block"
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="M22 12.54c-1.804 -.345 -2.701 -1.08 -3.523 -2.94c-.487 .696 -1.102 1.568 -.92 2.4c.028 .238 -.32 1.002 -.557 1h-14c0 5.208 3.164 7 6.196 7c4.124 .022 7.828 -1.376 9.854 -5c1.146 -.101 2.296 -1.505 2.95 -2.46z"
/>
<path d="M5 10h3v3h-3z" />
<path d="M8 10h3v3h-3z" />
<path d="M11 10h3v3h-3z" />
<path d="M8 7h3v3h-3z" />
<path d="M11 7h3v3h-3z" />
<path d="M11 4h3v3h-3z" />
<path d="M4.571 18c1.5 0 2.047 -.074 2.958 -.78" />
<line x1="10" y1="16" x2="10" y2="16.01" />
</svg>Destinations</button
>
</div>
<div class="form-control">
<div class="input-group flex w-full">
<div
@@ -496,11 +560,19 @@
</div>
{/if}
{#if (filtered.applications.length > 0 && applications.length > 0) || filtered.otherApplications.length > 0}
<div class="flex items-center mt-10">
<h1 class="title lg:text-3xl pr-4">Applications</h1>
<div class="flex items-center mt-10 space-x-2">
<h1 class="title lg:text-3xl">Applications</h1>
<button class="btn btn-sm btn-primary" on:click={refreshStatusApplications}
>Refresh Status</button
>{noInitialStatus.applications ? 'Load Status' : 'Refresh Status'}</button
>
{#if foundUnconfiguredApplication}
<button
class="btn btn-sm"
class:loading={loading.applications}
disabled={loading.applications}
on:click={cleanupApplications}>Cleanup Unconfigured Resources</button
>
{/if}
</div>
{/if}
{#if filtered.applications.length > 0 && applications.length > 0}
@@ -515,18 +587,22 @@
class="w-full rounded p-5 bg-coolgray-200 hover:bg-green-600 indicator duration-150"
>
{#await getStatus(application)}
<span class="indicator-item badge bg-yellow-500 badge-sm" />
<span class="indicator-item badge bg-yellow-300 badge-sm" />
{:then}
{#if status[application.id] === 'running'}
<span class="indicator-item badge bg-success badge-sm" />
{:else}
<span class="indicator-item badge bg-error badge-sm" />
{#if !noInitialStatus.applications}
{#if status[application.id] === 'loading'}
<span class="indicator-item badge bg-yellow-300 badge-sm" />
{:else if status[application.id] === 'running'}
<span class="indicator-item badge bg-success badge-sm" />
{:else}
<span class="indicator-item badge bg-error badge-sm" />
{/if}
{/if}
{/await}
<div class="w-full flex flex-row">
<ApplicationsIcons {application} isAbsolute={true} />
<div class="w-full flex flex-col">
<h1 class="font-bold text-lg lg:text-xl truncate">
<h1 class="font-bold text-base truncate">
{application.name}
{#if application.settings?.isBot}
<span class="text-xs badge bg-coolblack border-none text-applications"
@@ -609,9 +685,6 @@
{#if filtered.applications.length > 0}
<div class="divider w-32 mx-auto" />
{/if}
<div class="flex items-center mt-10">
<h1 class="text-lg font-bold">Other Teams</h1>
</div>
{/if}
{#if filtered.otherApplications.length > 0}
<div
@@ -621,18 +694,22 @@
<a class="no-underline mb-5" href={`/applications/${application.id}`}>
<div class="w-full rounded p-5 bg-coolgray-200 hover:bg-green-600 indicator duration-150">
{#await getStatus(application)}
<span class="indicator-item badge bg-yellow-500 badge-sm" />
<span class="indicator-item badge bg-yellow-300 badge-sm" />
{:then}
{#if status[application.id] === 'running'}
<span class="indicator-item badge bg-success badge-sm" />
{:else}
<span class="indicator-item badge bg-error badge-sm" />
{#if !noInitialStatus.applications}
{#if status[application.id] === 'loading'}
<span class="indicator-item badge bg-yellow-300 badge-sm" />
{:else if status[application.id] === 'running'}
<span class="indicator-item badge bg-success badge-sm" />
{:else}
<span class="indicator-item badge bg-error badge-sm" />
{/if}
{/if}
{/await}
<div class="w-full flex flex-row">
<ApplicationsIcons {application} isAbsolute={true} />
<div class="w-full flex flex-col">
<h1 class="font-bold text-lg lg:text-xl truncate">
<h1 class="font-bold text-base truncate">
{application.name}
{#if application.settings?.isBot}
<span class="text-xs badge bg-coolblack border-none text-applications">BOT</span
@@ -706,10 +783,19 @@
</div>
{/if}
{#if (filtered.services.length > 0 && services.length > 0) || filtered.otherServices.length > 0}
<div class="flex items-center mt-10">
<h1 class="title lg:text-3xl pr-4">Services</h1>
<button class="btn btn-sm btn-primary" on:click={refreshStatusServices}>Refresh Status</button
<div class="flex items-center mt-10 space-x-2">
<h1 class="title lg:text-3xl">Services</h1>
<button class="btn btn-sm btn-primary" on:click={refreshStatusServices}
>{noInitialStatus.services ? 'Load Status' : 'Refresh Status'}</button
>
{#if foundUnconfiguredService}
<button
class="btn btn-sm"
class:loading={loading.services}
disabled={loading.services}
on:click={cleanupServices}>Cleanup Unconfigured Resources</button
>
{/if}
</div>
{/if}
{#if filtered.services.length > 0 && services.length > 0}
@@ -724,18 +810,22 @@
class="w-full rounded p-5 bg-coolgray-200 hover:bg-pink-600 indicator duration-150"
>
{#await getStatus(service)}
<span class="indicator-item badge bg-yellow-500 badge-sm" />
<span class="indicator-item badge bg-yellow-300 badge-sm" />
{:then}
{#if status[service.id] === 'running'}
<span class="indicator-item badge bg-success badge-sm" />
{:else}
<span class="indicator-item badge bg-error badge-sm" />
{#if !noInitialStatus.services}
{#if status[service.id] === 'loading'}
<span class="indicator-item badge bg-yellow-300 badge-sm" />
{:else if status[service.id] === 'running'}
<span class="indicator-item badge bg-success badge-sm" />
{:else}
<span class="indicator-item badge bg-error badge-sm" />
{/if}
{/if}
{/await}
<div class="w-full flex flex-row">
<ServiceIcons type={service.type} isAbsolute={true} />
<div class="w-full flex flex-col">
<h1 class="font-bold text-lg lg:text-xl truncate">{service.name}</h1>
<h1 class="font-bold text-base truncate">{service.name}</h1>
<div class="h-10 text-xs">
{#if service?.fqdn}
<h2>{service?.fqdn.replace('https://', '').replace('http://', '')}</h2>
@@ -784,9 +874,6 @@
{#if filtered.services.length > 0}
<div class="divider w-32 mx-auto" />
{/if}
<div class="flex items-center mt-10">
<h1 class="text-lg font-bold">Other Teams</h1>
</div>
{/if}
{#if filtered.otherServices.length > 0}
<div
@@ -796,18 +883,22 @@
<a class="no-underline mb-5" href={`/services/${service.id}`}>
<div class="w-full rounded p-5 bg-coolgray-200 hover:bg-pink-600 indicator duration-150">
{#await getStatus(service)}
<span class="indicator-item badge bg-yellow-500 badge-sm" />
<span class="indicator-item badge bg-yellow-300 badge-sm" />
{:then}
{#if status[service.id] === 'running'}
<span class="indicator-item badge bg-success badge-sm" />
{:else}
<span class="indicator-item badge bg-error badge-sm" />
{#if !noInitialStatus.services}
{#if status[service.id] === 'loading'}
<span class="indicator-item badge bg-yellow-300 badge-sm" />
{:else if status[service.id] === 'running'}
<span class="indicator-item badge bg-success badge-sm" />
{:else}
<span class="indicator-item badge bg-error badge-sm" />
{/if}
{/if}
{/await}
<div class="w-full flex flex-row">
<ServiceIcons type={service.type} isAbsolute={true} />
<div class="w-full flex flex-col">
<h1 class="font-bold text-lg lg:text-xl truncate">{service.name}</h1>
<h1 class="font-bold text-base truncate">{service.name}</h1>
<div class="h-10 text-xs">
{#if service?.fqdn}
<h2>{service?.fqdn.replace('https://', '').replace('http://', '')}</h2>
@@ -850,11 +941,19 @@
</div>
{/if}
{#if (filtered.databases.length > 0 && databases.length > 0) || filtered.otherDatabases.length > 0}
<div class="flex items-center mt-10">
<h1 class="title lg:text-3xl pr-4">Databases</h1>
<div class="flex items-center mt-10 space-x-2">
<h1 class="title lg:text-3xl">Databases</h1>
<button class="btn btn-sm btn-primary" on:click={refreshStatusDatabases}
>Refresh Status</button
>{noInitialStatus.databases ? 'Load Status' : 'Refresh Status'}</button
>
{#if foundUnconfiguredDatabase}
<button
class="btn btn-sm"
class:loading={loading.databases}
disabled={loading.databases}
on:click={cleanupDatabases}>Cleanup Unconfigured Resources</button
>
{/if}
</div>
{/if}
{#if filtered.databases.length > 0 && databases.length > 0}
@@ -869,19 +968,23 @@
class="w-full rounded p-5 bg-coolgray-200 hover:bg-databases indicator duration-150"
>
{#await getStatus(database)}
<span class="indicator-item badge bg-yellow-500 badge-sm" />
<span class="indicator-item badge bg-yellow-300 badge-sm" />
{:then}
{#if status[database.id] === 'running'}
<span class="indicator-item badge bg-success badge-sm" />
{:else}
<span class="indicator-item badge bg-error badge-sm" />
{#if !noInitialStatus.databases}
{#if status[database.id] === 'loading'}
<span class="indicator-item badge bg-yellow-300 badge-sm" />
{:else if status[database.id] === 'running'}
<span class="indicator-item badge bg-success badge-sm" />
{:else}
<span class="indicator-item badge bg-error badge-sm" />
{/if}
{/if}
{/await}
<div class="w-full flex flex-row">
<DatabaseIcons type={database.type} isAbsolute={true} />
<div class="w-full flex flex-col">
<div class="h-10">
<h1 class="font-bold text-lg lg:text-xl truncate">{database.name}</h1>
<h1 class="font-bold text-base truncate">{database.name}</h1>
<div class="h-10 text-xs">
{#if database?.version}
<h2 class="">{database?.version}</h2>
@@ -933,9 +1036,6 @@
{#if filtered.databases.length > 0}
<div class="divider w-32 mx-auto" />
{/if}
<div class="flex items-center mt-10">
<h1 class="text-lg font-bold">Other Teams</h1>
</div>
{/if}
{#if filtered.otherDatabases.length > 0}
<div
@@ -945,19 +1045,23 @@
<a class="no-underline mb-5" href={`/databases/${database.id}`}>
<div class="w-full rounded p-5 bg-coolgray-200 hover:bg-databases indicator duration-150">
{#await getStatus(database)}
<span class="indicator-item badge bg-yellow-500 badge-sm" />
<span class="indicator-item badge bg-yellow-300 badge-sm" />
{:then}
{#if status[database.id] === 'running'}
<span class="indicator-item badge bg-success badge-sm" />
{:else}
<span class="indicator-item badge bg-error badge-sm" />
{#if !noInitialStatus.databases}
{#if status[database.id] === 'loading'}
<span class="indicator-item badge bg-yellow-300 badge-sm" />
{:else if status[database.id] === 'running'}
<span class="indicator-item badge bg-success badge-sm" />
{:else}
<span class="indicator-item badge bg-error badge-sm" />
{/if}
{/if}
{/await}
<div class="w-full flex flex-row">
<DatabaseIcons type={database.type} isAbsolute={true} />
<div class="w-full flex flex-col">
<div class="h-10">
<h1 class="font-bold text-lg lg:text-xl truncate">{database.name}</h1>
<h1 class="font-bold text-base truncate">{database.name}</h1>
<div class="h-10 text-xs">
{#if database?.version}
<h2 class="">{database?.version}</h2>
@@ -1017,9 +1121,9 @@
<a class="no-underline mb-5" href={`/sources/${source.id}`}>
<div class="w-full rounded p-5 bg-coolgray-200 hover:bg-sources indicator duration-150">
<div class="w-full flex flex-row">
<div class="absolute top-0 left-0 -m-5 h-10 w-10">
<div class="absolute top-0 left-0 -m-5 flex">
{#if source?.type === 'gitlab'}
<svg viewBox="0 0 128 128">
<svg viewBox="0 0 128 128" class="h-10 w-10">
<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"
@@ -1041,7 +1145,7 @@
/>
</svg>
{:else if source?.type === 'github'}
<svg viewBox="0 0 128 128">
<svg viewBox="0 0 128 128" class="h-10 w-10">
<g fill="#ffffff"
><path
fill-rule="evenodd"
@@ -1053,10 +1157,30 @@
>
</svg>
{/if}
{#if source.isSystemWide}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-10 w-10"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" />
<line x1="3.6" y1="9" x2="20.4" y2="9" />
<line x1="3.6" y1="15" x2="20.4" y2="15" />
<path d="M11.5 3a17 17 0 0 0 0 18" />
<path d="M12.5 3a17 17 0 0 1 0 18" />
</svg>
{/if}
</div>
<div class="w-full flex flex-col">
<div class="h-10">
<h1 class="font-bold text-lg lg:text-xl truncate">{source.name}</h1>
<h1 class="font-bold text-base truncate">{source.name}</h1>
{#if source.teams.length > 0 && source.teams[0]?.name}
<div class="truncate text-xs">{source.teams[0]?.name}</div>
{/if}
@@ -1077,9 +1201,6 @@
{#if filtered.gitSources.length > 0}
<div class="divider w-32 mx-auto" />
{/if}
<div class="flex items-center mt-10">
<h1 class="text-lg font-bold">Other Teams</h1>
</div>
{/if}
{#if filtered.otherGitSources.length > 0}
<div
@@ -1089,9 +1210,9 @@
<a class="no-underline mb-5" href={`/sources/${source.id}`}>
<div class="w-full rounded p-5 bg-coolgray-200 hover:bg-sources indicator duration-150">
<div class="w-full flex flex-row">
<div class="absolute top-0 left-0 -m-5 h-10 w-10">
<div class="absolute top-0 left-0 -m-5 flex">
{#if source?.type === 'gitlab'}
<svg viewBox="0 0 128 128">
<svg viewBox="0 0 128 128" class="h-10 w-10">
<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"
@@ -1113,7 +1234,7 @@
/>
</svg>
{:else if source?.type === 'github'}
<svg viewBox="0 0 128 128">
<svg viewBox="0 0 128 128" class="h-10 w-10">
<g fill="#ffffff"
><path
fill-rule="evenodd"
@@ -1125,10 +1246,30 @@
>
</svg>
{/if}
{#if source.isSystemWide}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-10 w-10"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" />
<line x1="3.6" y1="9" x2="20.4" y2="9" />
<line x1="3.6" y1="15" x2="20.4" y2="15" />
<path d="M11.5 3a17 17 0 0 0 0 18" />
<path d="M12.5 3a17 17 0 0 1 0 18" />
</svg>
{/if}
</div>
<div class="w-full flex flex-col">
<div class="h-10">
<h1 class="font-bold text-lg lg:text-xl truncate">{source.name}</h1>
<h1 class="font-bold text-base truncate">{source.name}</h1>
{#if source.teams.length > 0 && source.teams[0]?.name}
<div class="truncate text-xs">{source.teams[0]?.name}</div>
{/if}
@@ -1202,7 +1343,7 @@
{/if}
</div>
<div class="w-full flex flex-col">
<h1 class="font-bold text-lg lg:text-xl truncate">{destination.name}</h1>
<h1 class="font-bold text-base truncate">{destination.name}</h1>
<div class="h-10 text-xs">
{#if $appSession.teamId === '0' && destination.remoteVerified === false && destination.remoteEngine}
<h2 class="text-red-500">Not verified yet</h2>
@@ -1228,9 +1369,6 @@
{#if filtered.destinations.length > 0}
<div class="divider w-32 mx-auto" />
{/if}
<div class="flex items-center mt-10">
<h1 class="text-lg font-bold">Other Teams</h1>
</div>
{/if}
{#if filtered.otherDestinations.length > 0}
<div
@@ -1286,7 +1424,7 @@
{/if}
</div>
<div class="w-full flex flex-col">
<h1 class="font-bold text-lg lg:text-xl truncate">{destination.name}</h1>
<h1 class="font-bold text-base truncate">{destination.name}</h1>
<div class="h-10 text-xs">
{#if $appSession.teamId === '0' && destination.remoteVerified === false && destination.remoteEngine}
<h2 class="text-red-500">Not verified yet</h2>

View File

@@ -19,6 +19,7 @@
disabled
/>
</div>
<div class="lg:px-10 px-2 py-4">Hasura Console is <span class="text-warning">not enabled by default</span> for security reasons. <br>To enable it, add the following secret:<br><br> <code>HASURA_GRAPHQL_ENABLE_CONSOLE=true</code></div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">PostgreSQL</div>

View File

@@ -4,21 +4,72 @@
</script>
<ul class="menu border bg-coolgray-100 border-coolgray-200 rounded-box p-2 space-y-2">
<li class="menu-title">
<span>General</span>
</li>
{#if $appSession.teamId === '0'}
<li class="menu-title">
<span>General</span>
</li>
<li class="rounded" class:bg-coollabs={$page.url.pathname === `/settings/coolify`}>
<a href={`/settings/coolify`} class="no-underline w-full">Coolify Settings</a>
<a href={`/settings/coolify`} class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M7 10h3v-3l-3.5 -3.5a6 6 0 0 1 8 8l6 6a2 2 0 0 1 -3 3l-6 -6a6 6 0 0 1 -8 -8l3.5 3.5"
/>
</svg>Coolify Settings</a
>
</li>
{/if}
<li class="menu-title">
<span>Keys & Certificates</span>
</li>
<li class="rounded" class:bg-coollabs={$page.url.pathname === `/settings/ssh`}>
<a href={`/settings/ssh`} class="no-underline w-full">SSH Keys</a>
<a href={`/settings/ssh`} class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="8" cy="15" r="4" />
<line x1="10.85" y1="12.15" x2="19" y2="4" />
<line x1="18" y1="5" x2="20" y2="7" />
<line x1="15" y1="8" x2="17" y2="10" />
</svg>SSH Keys</a
>
</li>
<li class="rounded" class:bg-coollabs={$page.url.pathname === `/settings/certificates`}>
<a href={`/settings/certificates`} class="no-underline w-full">SSL Certificates</a>
<a href={`/settings/certificates`} class="no-underline w-full">
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3"
/>
<circle cx="12" cy="11" r="1" />
<line x1="12" y1="12" x2="12" y2="14.5" />
</svg>SSL Certificates</a
>
</li>
</ul>

View File

@@ -20,6 +20,7 @@
export let certificates: any;
import { del, post } from '$lib/api';
import { errorNotification } from '$lib/common';
import Beta from '$lib/components/Beta.svelte';
let loading = {
save: false
@@ -55,40 +56,40 @@
}
</script>
<div class="mx-auto w-full">
<div class="flex border-b border-coolgray-500 mb-6">
<div class="title font-bold pb-3 pr-4">SSL Certificates <span class="badge rounded bg-coollabs-gradient text-white">BETA</span></div>
<label for="my-modal" class="btn btn-sm btn-primary" on:click={() => (isModalActive = true)}
>Add SSL Certificate</label
>
</div>
{#if certificates.length > 0}
<table class="table w-full">
<thead>
<tr>
<th>Common Name</th>
<th>CreatedAt</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{#each certificates as cert}
<tr>
<td>{cert.commonName}</td>
<td>{cert.createdAt}</td>
<td
><button on:click={() => deleteCertificate(cert.id)} class="btn btn-sm btn-error"
>Delete</button
></td
>
</tr>
{/each}
</tbody>
</table>
{:else}
<div class="text-sm">No SSL Certificate found</div>
{/if}
<div class="mx-auto w-full">
<div class="flex border-b border-coolgray-500 mb-6">
<div class="title font-bold pb-3 pr-4">SSL Certificates <Beta /></div>
<label for="my-modal" class="btn btn-sm btn-primary" on:click={() => (isModalActive = true)}
>Add SSL Certificate</label
>
</div>
{#if certificates.length > 0}
<table class="table w-full">
<thead>
<tr>
<th>Common Name</th>
<th>CreatedAt</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{#each certificates as cert}
<tr>
<td>{cert.commonName}</td>
<td>{cert.createdAt}</td>
<td
><button on:click={() => deleteCertificate(cert.id)} class="btn btn-sm btn-error"
>Delete</button
></td
>
</tr>
{/each}
</tbody>
</table>
{:else}
<div class="text-sm">No SSL Certificate found</div>
{/if}
</div>
{#if isModalActive}
<input type="checkbox" id="my-modal" class="modal-toggle" />

View File

@@ -58,19 +58,16 @@
</script>
<div class="w-full">
{#if sshKeys.length === 0}
<div class="text-sm">No SSH keys found</div>
<label for="my-modal" class="btn btn-primary mt-6" on:click={() => (isModalActive = true)}
<div class="flex border-b border-coolgray-500 mb-6">
<div class="title font-bold pb-3 pr-4">SSH Keys</div>
<label for="my-modal" class="btn btn-sm btn-primary" on:click={() => (isModalActive = true)}
>Add SSH Key</label
>
</div>
{#if sshKeys.length === 0}
<div class="text-sm">No SSH keys found</div>
{:else}
<div class="mx-auto w-full">
<div class="flex border-b border-coolgray-500 mb-6">
<div class="title font-bold pb-3 pr-4">SSH Keys</div>
<label for="my-modal" class="btn btn-sm btn-primary" on:click={() => (isModalActive = true)}
>Add SSH Key</label
>
</div>
<table class="table w-full">
<thead>
<tr>

View File

@@ -8,6 +8,7 @@
import { addToast, appSession } from '$lib/store';
import { dev } from '$app/env';
import Explainer from '$lib/components/Explainer.svelte';
import Setting from '$lib/components/Setting.svelte';
const { id } = $page.params;
@@ -21,7 +22,8 @@
await post(`/sources/${id}`, {
name: source.name,
htmlUrl: source.htmlUrl.replace(/\/$/, ''),
apiUrl: source.apiUrl.replace(/\/$/, '')
apiUrl: source.apiUrl.replace(/\/$/, ''),
isSystemWide: source.isSystemWide
});
return addToast({
message: 'Configuration saved.',
@@ -43,7 +45,8 @@
htmlUrl: source.htmlUrl.replace(/\/$/, ''),
apiUrl: source.apiUrl.replace(/\/$/, ''),
organization: source.organization,
customPort: source.customPort
customPort: source.customPort,
isSystemWide: source.isSystemWide
});
const { organization, htmlUrl } = source;
const { fqdn, ipv4, ipv6 } = settings;
@@ -88,6 +91,16 @@
return errorNotification(error);
}
}
async function changeSettings(name: any, save: boolean) {
if ($appSession.teamId === '0') {
if (name === 'isSystemWide') {
source.isSystemWide = !source.isSystemWide;
}
if (save) {
await handleSubmit();
}
}
}
</script>
<div class="mx-auto max-w-6xl lg:px-6 px-3">
@@ -104,13 +117,13 @@
{/if}
</div>
<div class="grid gap-2 grid-cols-2 auto-rows-max">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
<label for="name">Name</label>
<input class="w-full" name="name" id="name" required bind:value={source.name} />
<label for="htmlUrl" class="text-base font-bold text-stone-100">HTML URL</label>
<label for="htmlUrl">HTML URL</label>
<input class="w-full" name="htmlUrl" id="htmlUrl" required bind:value={source.htmlUrl} />
<label for="apiUrl" class="text-base font-bold text-stone-100">API URL</label>
<label for="apiUrl">API URL</label>
<input class="w-full" name="apiUrl" id="apiUrl" required bind:value={source.apiUrl} />
<label for="customPort" class="text-base font-bold text-stone-100"
<label for="customPort"
>Custom SSH Port <Explainer
explanation={'If you use a self-hosted version of Git, you can provide custom port for all the Git related actions.'}
/></label
@@ -124,7 +137,7 @@
required
value={source.customPort}
/>
<label for="organization" class="pt-2 text-base font-bold text-stone-100"
<label for="organization" class="pt-2"
>Organization
<Explainer
explanation={"Fill it if you would like to use an organization's as your Git Source. Otherwise your user will be used."}
@@ -137,14 +150,26 @@
placeholder="eg: coollabsio"
bind:value={source.organization}
/>
<Setting
customClass="pt-4"
isBeta={true}
id="autodeploy"
isCenter={false}
bind:setting={source.isSystemWide}
on:click={() => changeSettings('isSystemWide', false)}
title="System Wide Git Source"
description="System Wide Git Sources are available to all the users in your Coolify instance. <br><br> <span class='font-bold text-warning'>Use with caution, as it can be a security risk.</span>"
/>
</div>
</form>
{:else if source.githubApp?.installationId}
<form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex lg:flex-row lg:justify-between flex-col space-y-3 w-full lg:items-center">
<h1 class="title">{$t('general')}</h1>
{#if $appSession.isAdmin}
<div class="flex flex-col lg:flex-row lg:space-x-4 lg:w-fit space-y-2 lg:space-y-0 w-full">
{#if $appSession.isAdmin && $appSession.teamId === '0'}
<div
class="flex flex-col lg:flex-row lg:space-x-4 lg:w-fit space-y-2 lg:space-y-0 w-full"
>
<button class="btn btn-sm bg-sources" type="submit" disabled={loading}
>{loading ? 'Saving...' : 'Save'}</button
>
@@ -159,9 +184,16 @@
{/if}
</div>
<div class="grid gap-2 grid-cols-2 auto-rows-max mt-4">
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
<input class="w-full" name="name" id="name" required bind:value={source.name} />
<label for="htmlUrl" class="text-base font-bold text-stone-100">HTML URL</label>
<label for="name">{$t('forms.name')}</label>
<input
class="w-full"
name="name"
id="name"
required
bind:value={source.name}
disabled={$appSession.teamId !== '0'}
/>
<label for="htmlUrl">HTML URL</label>
<input
class="w-full"
name="htmlUrl"
@@ -171,7 +203,7 @@
required
bind:value={source.htmlUrl}
/>
<label for="apiUrl" class="text-base font-bold text-stone-100">API URL</label>
<label for="apiUrl">API URL</label>
<input
class="w-full"
name="apiUrl"
@@ -181,7 +213,7 @@
readonly={source.githubAppId}
bind:value={source.apiUrl}
/>
<label for="customPort" class="text-base font-bold text-stone-100"
<label for="customPort"
>Custom SSH Port <Explainer
explanation="If you use a self-hosted version of Git, you can provide custom port for all the Git related actions."
/></label
@@ -195,9 +227,7 @@
required
value={source.customPort}
/>
<label for="organization" class="pt-2 text-base font-bold text-stone-100"
>Organization</label
>
<label for="organization" class="pt-2">Organization</label>
<input
class="w-full"
readonly
@@ -207,6 +237,17 @@
placeholder="eg: coollabsio"
bind:value={source.organization}
/>
<Setting
customClass="pt-4"
isBeta={true}
id="autodeploy"
isCenter={false}
disabled={$appSession.teamId !== '0'}
bind:setting={source.isSystemWide}
on:click={() => changeSettings('isSystemWide', true)}
title="System Wide Git Source"
description="System Wide Git Sources are available to all the users in your Coolify instance. <br><br> <span class='font-bold text-warning'>Use with caution, as it can be a security risk.</span>"
/>
</div>
</form>
{:else}

View File

@@ -53,7 +53,7 @@
}
</script>
{#if id !== 'new'}
{#if id !== 'new' && $appSession.teamId === '0'}
<nav class="nav-side">
<button
id="delete"

View File

@@ -43,7 +43,7 @@ textarea {
}
#svelte .custom-select-wrapper .selectContainer {
@apply h-12 w-96 rounded bg-coolgray-200 p-2 px-0 text-xs tracking-tight outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 md:text-sm w-full ;
@apply h-12 rounded bg-coolgray-200 p-2 px-0 text-xs tracking-tight outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 md:text-sm ;
}
#svelte .listContainer {

View File

@@ -1,7 +1,7 @@
{
"name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "3.10.9",
"version": "3.10.13",
"license": "Apache-2.0",
"repository": "github:coollabsio/coolify",
"scripts": {

545
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff