mirror of
https://github.com/ershisan99/coolify.git
synced 2026-01-06 20:52:09 +00:00
Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7932c1c4a9 | ||
|
|
f776fb83e7 | ||
|
|
a97521aba2 | ||
|
|
d1c0fe503e | ||
|
|
ed02c1ae36 | ||
|
|
9a67cf7355 | ||
|
|
755eeda364 | ||
|
|
136dee7747 | ||
|
|
e4e8428855 | ||
|
|
de8dc021f9 | ||
|
|
991587f252 | ||
|
|
8dbcf257c4 | ||
|
|
0b067364a9 | ||
|
|
5367bd6134 | ||
|
|
92228c4379 | ||
|
|
fb2c7896b3 | ||
|
|
23265d9091 | ||
|
|
2c9bb0e767 | ||
|
|
f9e8400d83 | ||
|
|
927a13cd76 | ||
|
|
51b3293e69 | ||
|
|
3f76cadea9 | ||
|
|
6dbf53b558 | ||
|
|
22e937c798 | ||
|
|
ac5cc8b299 | ||
|
|
c588ab723b | ||
|
|
4b2dfc051d | ||
|
|
5238c83f3f | ||
|
|
90bb580e50 | ||
|
|
f40e142704 | ||
|
|
a67618675d | ||
|
|
4fe436e4d1 | ||
|
|
683b8c966f | ||
|
|
28377a156d | ||
|
|
3dcc4faabb | ||
|
|
60a033f93a | ||
|
|
436bd73786 | ||
|
|
5c69ff3339 | ||
|
|
2105b1e7c4 | ||
|
|
523004e5b2 | ||
|
|
5e02c386ec | ||
|
|
b4501fe52d | ||
|
|
3c29eaa1b1 | ||
|
|
ee67e163b1 | ||
|
|
9662bc29fb | ||
|
|
96f2660b98 | ||
|
|
20f594c66c |
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- DropIndex
|
||||
DROP INDEX "PreviewApplication_applicationId_key";
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Build" ADD COLUMN "sourceRepository" TEXT;
|
||||
@@ -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[]
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 } }
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface SaveGitHubSource extends OnlyId {
|
||||
apiUrl: string,
|
||||
organization: string,
|
||||
customPort: number,
|
||||
isSystemWide: boolean
|
||||
}
|
||||
}
|
||||
export interface SaveGitLabSource extends OnlyId {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface GitHubEvents {
|
||||
ref: string,
|
||||
repo: {
|
||||
id: string,
|
||||
full_name: string,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
1
apps/ui/src/lib/components/Beta.svelte
Normal file
1
apps/ui/src/lib/components/Beta.svelte
Normal file
@@ -0,0 +1 @@
|
||||
<span class="badge bg-coollabs-gradient rounded text-white font-normal"> BETA </span>
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -172,7 +172,6 @@
|
||||
|
||||
<div class="custom-select-wrapper">
|
||||
<Select
|
||||
class="w-full"
|
||||
placeholder={loading.branches
|
||||
? $t('application.configuration.loading_branches')
|
||||
: branchSelectOptions.length ===0
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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';
|
||||
|
||||
64
apps/ui/src/routes/iam/_Account.svelte
Normal file
64
apps/ui/src/routes/iam/_Account.svelte
Normal 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>
|
||||
73
apps/ui/src/routes/iam/_Menu.svelte
Normal file
73
apps/ui/src/routes/iam/_Menu.svelte
Normal 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>
|
||||
20
apps/ui/src/routes/iam/__layout.svelte
Normal file
20
apps/ui/src/routes/iam/__layout.svelte
Normal 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>
|
||||
@@ -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}
|
||||
|
||||
55
apps/ui/src/routes/iam/pending.svelte
Normal file
55
apps/ui/src/routes/iam/pending.svelte
Normal 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>
|
||||
@@ -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 />
|
||||
108
apps/ui/src/routes/iam/teams.svelte
Normal file
108
apps/ui/src/routes/iam/teams.svelte
Normal 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>
|
||||
31
apps/ui/src/routes/iam/teams/[id]/__layout.svelte
Normal file
31
apps/ui/src/routes/iam/teams/[id]/__layout.svelte
Normal 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 />
|
||||
@@ -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" />
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if id !== 'new'}
|
||||
{#if id !== 'new' && $appSession.teamId === '0'}
|
||||
<nav class="nav-side">
|
||||
<button
|
||||
id="delete"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
545
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user