feat: previewApplications finalized

This commit is contained in:
Andras Bacsai
2022-09-13 15:50:20 +02:00
parent c40b80436a
commit d9908b3d61
24 changed files with 805 additions and 368 deletions
@@ -4,7 +4,8 @@ ALTER TABLE "Build" ADD COLUMN "previewApplicationId" TEXT;
-- CreateTable
CREATE TABLE "PreviewApplication" (
"id" TEXT NOT NULL PRIMARY KEY,
"prMrId" TEXT NOT NULL,
"pullmergeRequestId" TEXT NOT NULL,
"sourceBranch" TEXT NOT NULL,
"isRandomDomain" BOOLEAN NOT NULL DEFAULT false,
"customDomain" TEXT,
"applicationId" TEXT NOT NULL,
+26 -25
View File
@@ -119,18 +119,19 @@ model Application {
secrets Secret[]
teams Team[]
connectedDatabase ApplicationConnectedDatabase?
previewApplication PreviewApplication[]
previewApplication PreviewApplication[]
}
model PreviewApplication {
id String @id @default(cuid())
prMrId String
isRandomDomain Boolean @default(false)
customDomain String?
applicationId String @unique
application Application @relation(fields: [applicationId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id String @id @default(cuid())
pullmergeRequestId String
sourceBranch String
isRandomDomain Boolean @default(false)
customDomain String?
applicationId String @unique
application Application @relation(fields: [applicationId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ApplicationConnectedDatabase {
@@ -222,22 +223,22 @@ model BuildLog {
}
model Build {
id String @id @default(cuid())
type String
applicationId String?
destinationDockerId String?
gitSourceId String?
githubAppId String?
gitlabAppId String?
commit String?
pullmergeRequestId String?
previewApplicationId String?
forceRebuild Boolean @default(false)
sourceBranch String?
branch String?
status String? @default("queued")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id String @id @default(cuid())
type String
applicationId String?
destinationDockerId String?
gitSourceId String?
githubAppId String?
gitlabAppId String?
commit String?
pullmergeRequestId String?
previewApplicationId String?
forceRebuild Boolean @default(false)
sourceBranch String?
branch String?
status String? @default("queued")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model DestinationDocker {
+1 -1
View File
@@ -42,7 +42,7 @@ import * as buildpacks from '../lib/buildPacks';
application = decryptApplication(application)
const originalApplicationId = application.id
if (pullmergeRequestId) {
const previewApplications = await prisma.previewApplication.findMany({where: {applicationId: originalApplicationId, prMrId: pullmergeRequestId}})
const previewApplications = await prisma.previewApplication.findMany({where: {applicationId: originalApplicationId, pullmergeRequestId}})
if (previewApplications.length > 0) {
previewApplicationId = previewApplications[0].id
}
-1
View File
@@ -707,7 +707,6 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
Dockerfile.push(`RUN ${installCommand}`);
}
Dockerfile.push(`RUN ${buildCommand}`);
console.log(Dockerfile.join('\n'))
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ ...data, isCache: true });
}
+1 -1
View File
@@ -21,7 +21,7 @@ import { scheduler } from './scheduler';
import { supportedServiceTypesAndVersions } from './services/supportedVersions';
import { includeServices } from './services/common';
export const version = '3.10.3';
export const version = '3.10.4';
export const isDev = process.env.NODE_ENV === 'development';
const algorithm = 'aes-256-ctr';
@@ -12,7 +12,7 @@ import { checkDomainsIsValidInDNS, checkDoubleBranch, checkExposedPort, createDi
import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker';
import type { FastifyRequest } from 'fastify';
import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication } from './types';
import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication, RestartPreviewApplication } from './types';
import { OnlyId } from '../../../../types';
function filterObject(obj, callback) {
@@ -83,8 +83,6 @@ export async function getApplicationStatus(request: FastifyRequest<OnlyId>) {
isExited = status.status.isExited;
isRestarting = status.status.isRestarting
}
// isExited = await isContainerExited(application.destinationDocker.id, id);
}
return {
isRunning,
@@ -164,7 +162,8 @@ export async function getApplicationFromDB(id: string, teamId: string) {
gitSource: { include: { githubApp: true, gitlabApp: true } },
secrets: true,
persistentStorage: true,
connectedDatabase: true
connectedDatabase: true,
previewApplication: true
}
});
if (!application) {
@@ -350,6 +349,7 @@ export async function stopPreviewApplication(request: FastifyRequest<StopPreview
if (found) {
await removeContainer({ id: container, dockerId: application.destinationDocker.id });
}
await prisma.previewApplication.deleteMany({ where: { applicationId: application.id, pullmergeRequestId } })
}
return reply.code(201).send();
} catch ({ status, message }) {
@@ -617,7 +617,7 @@ export async function deployApplication(request: FastifyRequest<DeployApplicatio
githubAppId: application.gitSource?.githubApp?.id,
gitlabAppId: application.gitSource?.gitlabApp?.id,
status: 'queued',
type: 'manual'
type: pullmergeRequestId ? application.gitSource?.githubApp?.id ? 'manual_pr' : 'manual_mr' : 'manual'
}
});
return {
@@ -808,7 +808,6 @@ export async function saveSecret(request: FastifyRequest<SaveSecret>, reply: Fas
try {
const { id } = request.params
let { name, value, isBuildSecret, isPRMRSecret, isNew } = request.body
if (isNew) {
const found = await prisma.secret.findFirst({ where: { name, applicationId: id, isPRMRSecret } });
if (found) {
@@ -820,14 +819,24 @@ export async function saveSecret(request: FastifyRequest<SaveSecret>, reply: Fas
});
}
} else {
value = encrypt(value.trim());
if (value) {
value = encrypt(value.trim());
}
const found = await prisma.secret.findFirst({ where: { applicationId: id, name, isPRMRSecret } });
if (found) {
await prisma.secret.updateMany({
where: { applicationId: id, name, isPRMRSecret },
data: { value, isBuildSecret, isPRMRSecret }
});
if (!value && isPRMRSecret) {
await prisma.secret.deleteMany({
where: { applicationId: id, name, isPRMRSecret }
});
} else {
await prisma.secret.updateMany({
where: { applicationId: id, name, isPRMRSecret },
data: { value, isBuildSecret, isPRMRSecret }
});
}
} else {
await prisma.secret.create({
data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } }
@@ -894,6 +903,181 @@ export async function deleteStorage(request: FastifyRequest<DeleteStorage>) {
}
}
export async function restartPreview(request: FastifyRequest<RestartPreviewApplication>, reply: FastifyReply) {
try {
const { id, pullmergeRequestId } = request.params
const { teamId } = request.user
let application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId) {
const buildId = cuid();
const { id: dockerId, network } = application.destinationDocker;
const { secrets, port, repository, persistentStorage, id: applicationId, buildPack, exposePort } = application;
const envs = [
`PORT=${port}`
];
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (pullmergeRequestId) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
envs.push(`${secret.name}=${isSecretFound[0].value}`);
} else {
envs.push(`${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
envs.push(`${secret.name}=${secret.value}`);
}
}
});
}
const { workdir } = await createDirectories({ repository, buildId });
const labels = []
let image = null
const { stdout: container } = await executeDockerCmd({ dockerId, command: `docker container ls --filter 'label=com.docker.compose.service=${id}-${pullmergeRequestId}' --format '{{json .}}'` })
const containersArray = container.trim().split('\n');
for (const container of containersArray) {
const containerObj = formatLabelsOnDocker(container);
image = containerObj[0].Image
Object.keys(containerObj[0].Labels).forEach(function (key) {
if (key.startsWith('coolify')) {
labels.push(`${key}=${containerObj[0].Labels[key]}`)
}
})
}
let imageFound = false;
try {
await executeDockerCmd({
dockerId,
command: `docker image inspect ${image}`
})
imageFound = true;
} catch (error) {
//
}
if (!imageFound) {
throw { status: 500, message: 'Image not found, cannot restart application.' }
}
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
let envFound = false;
try {
envFound = !!(await fs.stat(`${workdir}/.env`));
} catch (error) {
//
}
const volumes =
persistentStorage?.map((storage) => {
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${buildPack !== 'docker' ? '/app' : ''
}${storage.path}`;
}) || [];
const composeVolumes = volumes.map((volume) => {
return {
[`${volume.split(':')[0]}`]: {
name: volume.split(':')[0]
}
};
});
const composeFile = {
version: '3.8',
services: {
[`${applicationId}-${pullmergeRequestId}`]: {
image,
container_name: `${applicationId}-${pullmergeRequestId}`,
volumes,
env_file: envFound ? [`${workdir}/.env`] : [],
labels,
depends_on: [],
expose: [port],
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
...defaultComposeConfiguration(network),
}
},
networks: {
[network]: {
external: true
}
},
volumes: Object.assign({}, ...composeVolumes)
};
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}-${pullmergeRequestId}` })
await executeDockerCmd({ dockerId, command: `docker rm ${id}-${pullmergeRequestId}` })
await executeDockerCmd({ dockerId, command: `docker compose --project-directory ${workdir} up -d` })
return reply.code(201).send();
}
throw { status: 500, message: 'Application cannot be restarted.' }
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getPreviewStatus(request: FastifyRequest<RestartPreviewApplication>) {
try {
const { id, pullmergeRequestId } = request.params
const { teamId } = request.user
let isRunning = false;
let isExited = false;
let isRestarting = false;
let isBuilding = false
const application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId) {
const status = await checkContainer({ dockerId: application.destinationDocker.id, container: `${id}-${pullmergeRequestId}` });
if (status?.found) {
isRunning = status.status.isRunning;
isExited = status.status.isExited;
isRestarting = status.status.isRestarting
}
const building = await prisma.build.findMany({ where: { applicationId: id, pullmergeRequestId, status: { in: ['queued', 'running'] } } })
isBuilding = building.length > 0
}
return {
isBuilding,
isRunning,
isRestarting,
isExited,
};
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function loadPreviews(request: FastifyRequest<OnlyId>) {
try {
const { id } = request.params
const application = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } });
const { stdout } = await executeDockerCmd({ dockerId: application.destinationDocker.id, command: `docker container ls --filter 'name=${id}-' --format "{{json .}}"` })
if (stdout === '') {
throw { status: 500, message: 'No previews found.' }
}
const containers = formatLabelsOnDocker(stdout).filter(container => container.Labels['coolify.configuration'] && container.Labels['coolify.type'] === 'standalone-application')
const jsonContainers = containers
.map((container) =>
JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString())
)
.filter((container) => {
return container.pullmergeRequestId && container.applicationId === id;
});
for (const container of jsonContainers) {
const found = await prisma.previewApplication.findMany({ where: { applicationId: container.applicationId, pullmergeRequestId: container.pullmergeRequestId } })
if (found.length === 0) {
await prisma.previewApplication.create({
data: {
pullmergeRequestId: container.pullmergeRequestId,
sourceBranch: container.branch,
customDomain: container.fqdn,
application: { connect: { id: container.applicationId } }
}
})
}
}
return {
previews: await prisma.previewApplication.findMany({ where: { applicationId: id } })
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getPreviews(request: FastifyRequest<OnlyId>) {
try {
const { id } = request.params
@@ -909,26 +1093,7 @@ export async function getPreviews(request: FastifyRequest<OnlyId>) {
const applicationSecrets = secrets.filter((secret) => !secret.isPRMRSecret);
const PRMRSecrets = secrets.filter((secret) => secret.isPRMRSecret);
const application = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } });
const { stdout } = await executeDockerCmd({ dockerId: application.destinationDocker.id, command: `docker container ls --filter 'name=${id}-' --format "{{json .}}"` })
if (stdout === '') {
return {
containers: [],
applicationSecrets: [],
PRMRSecrets: []
}
}
const containers = formatLabelsOnDocker(stdout).filter(container => container.Labels['coolify.configuration'] && container.Labels['coolify.type'] === 'standalone-application')
const jsonContainers = containers
.map((container) =>
JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString())
)
.filter((container) => {
return container.pullmergeRequestId && container.applicationId === id;
});
return {
containers: jsonContainers,
applicationSecrets: applicationSecrets.sort((a, b) => {
return ('' + a.name).localeCompare(b.name);
}),
@@ -1002,12 +1167,6 @@ export async function getBuildLogs(request: FastifyRequest<GetBuildLogs>) {
});
}
builds = builds.map((build) => {
const updatedAt = day(build.updatedAt).utc();
build.took = updatedAt.diff(day(build.createdAt)) / 1000;
build.since = updatedAt.fromNow();
return build;
});
return {
builds,
buildCount
@@ -1,8 +1,8 @@
import { FastifyPluginAsync } from 'fastify';
import { OnlyId } from '../../../../types';
import { cancelDeployment, checkDNS, checkDomain, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildLogs, getBuildPack, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getSecrets, getStorages, getUsage, listApplications, newApplication, restartApplication, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication } from './handlers';
import { cancelDeployment, checkDNS, checkDomain, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildLogs, getBuildPack, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getPreviewStatus, getSecrets, getStorages, getUsage, listApplications, loadPreviews, newApplication, restartApplication, restartPreview, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication } from './handlers';
import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, GetImages, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types';
import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, GetImages, RestartPreviewApplication, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types';
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.addHook('onRequest', async (request) => {
@@ -37,6 +37,9 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.delete<DeleteStorage>('/:id/storages', async (request) => await deleteStorage(request));
fastify.get<OnlyId>('/:id/previews', async (request) => await getPreviews(request));
fastify.post<OnlyId>('/:id/previews/load', async (request) => await loadPreviews(request));
fastify.get<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/status', async (request) => await getPreviewStatus(request));
fastify.post<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/restart', async (request, reply) => await restartPreview(request, reply));
fastify.get<GetApplicationLogs>('/:id/logs', async (request) => await getApplicationLogs(request));
fastify.get<GetBuildLogs>('/:id/logs/build', async (request) => await getBuildLogs(request));
@@ -126,4 +126,10 @@ export interface StopPreviewApplication extends OnlyId {
Body: {
pullmergeRequestId: string | null,
}
}
export interface RestartPreviewApplication {
Params: {
id: string,
pullmergeRequestId: string | null,
}
}
+26 -13
View File
@@ -1,7 +1,7 @@
import axios from "axios";
import cuid from "cuid";
import crypto from "crypto";
import { encrypt, errorHandler, getUIUrl, isDev, prisma } from "../../../lib/common";
import { encrypt, errorHandler, getDomain, getUIUrl, isDev, prisma } from "../../../lib/common";
import { checkContainer, removeContainer } from "../../../lib/docker";
import { createdBranchDatabase, getApplicationFromDBWebhook, removeBranchDatabase } from "../../api/v1/applications/handlers";
@@ -175,15 +175,23 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
data: { updatedAt: new Date() }
});
let previewApplicationId = undefined
if (pullmergeRequestId) {
const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, prMrId: pullmergeRequestId } })
if (foundPreviewApplications.length > 0) {
previewApplicationId = foundPreviewApplications[0].id
} else {
const previewApplication = await prisma.previewApplication.create({ data: { prMrId: pullmergeRequestId, application: { connect: { id: application.id } } } })
previewApplicationId = previewApplication.id
}
}
if (pullmergeRequestId) {
const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, pullmergeRequestId } })
if (foundPreviewApplications.length > 0) {
previewApplicationId = foundPreviewApplications[0].id
} else {
const protocol = application.fqdn.includes('https://') ? 'https://' : 'http://'
const previewApplication = await prisma.previewApplication.create({
data: {
pullmergeRequestId,
sourceBranch,
customDomain: `${protocol}${pullmergeRequestId}.${getDomain(application.fqdn)}`,
application: { connect: { id: application.id } }
}
})
previewApplicationId = previewApplication.id
}
}
// if (application.connectedDatabase && pullmergeRequestAction === 'opened' || pullmergeRequestAction === 'reopened') {
// // Coolify hosted database
// if (application.connectedDatabase.databaseId) {
@@ -210,7 +218,9 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
}
});
return {
message: 'Queued. Thank you!'
};
} else if (pullmergeRequestAction === 'closed') {
if (application.destinationDockerId) {
const id = `${application.id}-${pullmergeRequestId}`;
@@ -218,12 +228,15 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
await removeContainer({ id, dockerId: application.destinationDocker.id });
} catch (error) { }
}
const foundPreviewApplications = await prisma.previewApplication.findMany({ where: {applicationId: application.id, prMrId: pullmergeRequestId}})
const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, pullmergeRequestId } })
if (foundPreviewApplications.length > 0) {
for (const preview of foundPreviewApplications) {
await prisma.previewApplication.delete({where: {id: preview.id}})
await prisma.previewApplication.delete({ where: { id: preview.id } })
}
}
return {
message: 'PR closed. Thank you!'
};
// if (application?.connectedDatabase?.databaseId) {
// const databaseId = application.connectedDatabase.databaseId;
// const database = await prisma.database.findUnique({ where: { id: databaseId } });
@@ -2,7 +2,7 @@ import axios from "axios";
import cuid from "cuid";
import crypto from "crypto";
import type { FastifyReply, FastifyRequest } from "fastify";
import { errorHandler, getAPIUrl, getUIUrl, isDev, listSettings, prisma } from "../../../lib/common";
import { errorHandler, getAPIUrl, getDomain, getUIUrl, isDev, listSettings, prisma } from "../../../lib/common";
import { checkContainer, removeContainer } from "../../../lib/docker";
import { getApplicationFromDB, getApplicationFromDBWebhook } from "../../api/v1/applications/handlers";
@@ -91,8 +91,8 @@ 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, iid: pullmergeRequestId }, project: { id } } = request.body
const { object_attributes: { work_in_progress: isDraft, action, source_branch: sourceBranch, target_branch: targetBranch }, project: { id } } = request.body
const pullmergeRequestId = request.body.object_attributes.iid.toString();
const projectId = Number(id);
if (!allowedActions.includes(action)) {
throw { status: 500, message: 'Action not allowed.' }
@@ -130,10 +130,29 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
where: { id: application.id },
data: { updatedAt: new Date() }
});
let previewApplicationId = undefined
if (pullmergeRequestId) {
const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, pullmergeRequestId } })
if (foundPreviewApplications.length > 0) {
previewApplicationId = foundPreviewApplications[0].id
} else {
const protocol = application.fqdn.includes('https://') ? 'https://' : 'http://'
const previewApplication = await prisma.previewApplication.create({
data: {
pullmergeRequestId,
sourceBranch,
customDomain: `${protocol}${pullmergeRequestId}.${getDomain(application.fqdn)}`,
application: { connect: { id: application.id } }
}
})
previewApplicationId = previewApplication.id
}
}
await prisma.build.create({
data: {
id: buildId,
pullmergeRequestId: pullmergeRequestId.toString(),
pullmergeRequestId,
previewApplicationId,
sourceBranch,
applicationId: application.id,
destinationDockerId: application.destinationDocker.id,
@@ -150,8 +169,19 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
} else if (action === 'close') {
if (application.destinationDockerId) {
const id = `${application.id}-${pullmergeRequestId}`;
await removeContainer({ id, dockerId: application.destinationDocker.id });
try {
await removeContainer({ id, dockerId: application.destinationDocker.id });
} catch (error) { }
}
const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, pullmergeRequestId } })
if (foundPreviewApplications.length > 0) {
for (const preview of foundPreviewApplications) {
await prisma.previewApplication.delete({ where: { id: preview.id } })
}
}
return {
message: 'MR closed. Thank you!'
};
}
}
+1
View File
@@ -42,6 +42,7 @@
},
"type": "module",
"dependencies": {
"dayjs": "1.11.5",
"@sveltejs/adapter-static": "1.0.0-next.39",
"@tailwindcss/typography": "^0.5.7",
"cuid": "2.1.8",
+4
View File
@@ -83,4 +83,8 @@ export function handlerNotFoundLoad(error: any, url: URL) {
status: 500,
error: new Error(`Could not load ${url}`)
};
}
export function getRndInteger(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
+9 -9
View File
@@ -108,21 +108,21 @@
<div class="stats stats-vertical min-w-[16rem] mb-5 rounded bg-transparent">
<div class="stat">
<div class="stat-title">Total Memory</div>
<div class="stat-value text-2xl">
<div class="stat-value text-2xl text-white">
{(usage?.memory?.totalMemMb).toFixed(0)}<span class="text-sm">MB</span>
</div>
</div>
<div class="stat">
<div class="stat-title">Used Memory</div>
<div class="stat-value text-2xl">
<div class="stat-value text-2xl text-white">
{(usage?.memory?.usedMemMb).toFixed(0)}<span class="text-sm">MB</span>
</div>
</div>
<div class="stat">
<div class="stat-title">Free Memory</div>
<div class="stat-value text-2xl">
<div class="stat-value text-2xl text-white">
{(usage?.memory?.freeMemPercentage).toFixed(0)}<span class="text-sm">%</span>
</div>
</div>
@@ -131,41 +131,41 @@
<div class="stats stats-vertical min-w-[20rem] mb-5 bg-transparent rounded">
<div class="stat">
<div class="stat-title">Total CPU</div>
<div class="stat-value text-2xl">
<div class="stat-value text-2xl text-white">
{usage?.cpu?.count}
</div>
</div>
<div class="stat">
<div class="stat-title">CPU Usage</div>
<div class="stat-value text-2xl">
<div class="stat-value text-2xl text-white">
{usage?.cpu?.usage}<span class="text-sm">%</span>
</div>
</div>
<div class="stat">
<div class="stat-title">Load Average (5,10,30mins)</div>
<div class="stat-value text-2xl">{usage?.cpu?.load}</div>
<div class="stat-value text-2xl text-white">{usage?.cpu?.load}</div>
</div>
</div>
<div class="stats stats-vertical min-w-[16rem] mb-5 bg-transparent rounded">
<div class="stat">
<div class="stat-title">Total Disk</div>
<div class="stat-value text-2xl">
<div class="stat-value text-2xl text-white">
{usage?.disk?.totalGb}<span class="text-sm">GB</span>
</div>
</div>
<div class="stat">
<div class="stat-title">Used Disk</div>
<div class="stat-value text-2xl">
<div class="stat-value text-2xl text-white">
{usage?.disk?.usedGb}<span class="text-sm">GB</span>
</div>
</div>
<div class="stat">
<div class="stat-title">Free Disk</div>
<div class="stat-value text-2xl">
<div class="stat-value text-2xl text-white">
{usage?.disk?.freePercentage}<span class="text-sm">%</span>
</div>
</div>
+7
View File
@@ -0,0 +1,7 @@
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc.js';
import relativeTime from 'dayjs/plugin/relativeTime.js';
dayjs.extend(utc);
dayjs.extend(relativeTime);
export { dayjs as day };
+1 -1
View File
@@ -290,7 +290,7 @@
{/if}
{/if}
<main>
<div class={$appSession.userId ? 'pl-14 lg:px-20' : null}>
<div class={$appSession.userId ? 'pl-14 lg:pl-20' : null}>
<slot />
</div>
</main>
@@ -5,7 +5,6 @@
export let isNewSecret = false;
export let isPRMRSecret = false;
export let PRMRSecret: any = {};
if (isPRMRSecret) value = PRMRSecret.value;
import { page } from '$app/stores';
@@ -39,7 +38,15 @@
async function createSecret(isNew: any) {
try {
if (!name || !value) return;
if (isNew) {
if (!name || !value) return;
}
if (value === undefined && isPRMRSecret) {
return
}
if (value === '' && !isPRMRSecret) {
throw new Error('Value is required.')
}
await saveSecret({
isNew,
name,
@@ -108,7 +115,6 @@
name={isNewSecret ? 'secretValue' : 'secretValueNew'}
isPasswordField={true}
bind:value
required
placeholder="J$#@UIO%HO#$U%H"
/>
</td>
@@ -130,7 +136,7 @@
class:translate-x-0={!isBuildSecret}
>
<span
class=" absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
class:opacity-0={isBuildSecret}
class:opacity-100={!isBuildSecret}
aria-hidden="true"
@@ -23,34 +23,30 @@
export let application: any;
export let buildCount: any;
import { page } from '$app/stores';
import {addToast} from '$lib/store';
console.log(builds[0].createdAt);
import { addToast } from '$lib/store';
import BuildLog from './_BuildLog.svelte';
import { get, post } from '$lib/api';
import { t } from '$lib/translations';
import { changeQueryParams, dateOptions, errorNotification, asyncSleep } from '$lib/common';
import Tooltip from '$lib/components/Tooltip.svelte';
import { day } from '$lib/dayjs';
let buildId: any;
let skip = 0;
let noMoreBuilds = buildCount < 5 || buildCount <= skip;
let buildTook = 0;
const { id } = $page.params;
let preselectedBuildId = $page.url.searchParams.get('buildId');
if (preselectedBuildId) buildId = preselectedBuildId;
async function updateBuildStatus({ detail }: { detail: any }) {
const { status, took } = detail;
const { status } = detail;
if (status !== 'running') {
try {
const data = await get(`/applications/${id}/logs/build?buildId=${buildId}`);
builds = builds.filter((build: any) => {
if (build.id === data.builds[0].id) {
build.status = data.builds[0].status;
build.took = data.builds[0].took;
build.since = data.builds[0].since;
}
return build;
});
@@ -62,7 +58,6 @@ import {addToast} from '$lib/store';
if (build.id === buildId) build.status = status;
return build;
});
buildTook = took;
}
}
async function loadMoreBuilds() {
@@ -84,23 +79,37 @@ import {addToast} from '$lib/store';
buildId = build;
return changeQueryParams(buildId);
}
async function resetQueue() {
const sure = confirm('It will reset all build queues for all applications. If something is queued, it will be canceled automatically. Are you sure? ');
async function resetQueue() {
const sure = confirm(
'It will reset all build queues for all applications. If something is queued, it will be canceled automatically. Are you sure? '
);
if (sure) {
try {
await post(`/internal/resetQueue`, {});
addToast({
try {
await post(`/internal/resetQueue`, {});
addToast({
message: 'Queue reset done.',
type: 'success'
});
await asyncSleep(500)
return window.location.reload()
} catch (error) {
return errorNotification(error);
});
await asyncSleep(500);
return window.location.reload();
} catch (error) {
return errorNotification(error);
}
}
}
}
}
function generateBadgeColors(status: string) {
if (status === 'failed') {
return 'text-red-500';
} else if (status === 'running') {
return 'text-yellow-300';
} else if (status === 'success') {
return 'text-green-500';
} else if (status === 'canceled') {
return 'text-orange-500';
} else {
return 'text-white';
}
}
</script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
@@ -156,7 +165,9 @@ import {addToast} from '$lib/store';
</div>
<div class="block flex-row justify-start space-x-2 px-5 pt-6 sm:px-10 md:flex">
<div class="mb-4 min-w-[16rem] space-y-2 md:mb-0 ">
<button class="btn btn-sm text-xs w-full bg-error" on:click={resetQueue}>Reset Build Queue</button>
<button class="btn btn-sm text-xs w-full bg-error" on:click={resetQueue}
>Reset Build Queue</button
>
<div class="top-4 md:sticky">
{#each builds as build, index (build.id)}
<div
@@ -164,8 +175,8 @@ import {addToast} from '$lib/store';
on:click={() => loadBuild(build.id)}
class:rounded-tr={index === 0}
class:rounded-br={index === builds.length - 1}
class="flex cursor-pointer items-center justify-center py-4 no-underline transition-all duration-100 hover:bg-coolgray-400 hover:shadow-xl"
class:bg-coolgray-400={buildId === build.id}
class="flex cursor-pointer items-center justify-center py-4 no-underline transition-all duration-100 hover:bg-coolgray-300 hover:shadow-xl"
class:bg-coolgray-200={buildId === build.id}
>
<div class="flex-col px-2 text-center min-w-[10rem]">
<div class="text-sm font-bold">
@@ -174,41 +185,46 @@ import {addToast} from '$lib/store';
<div class="text-xs">
{build.type}
</div>
<div class="badge badge-sm text-xs text-white uppercase rounded bg-coolgray-300 border-none font-bold"
class:text-red-500={build.status === 'failed'}
class:text-orange-500={build.status === 'canceled'}
class:text-green-500={build.status === 'success'}
class:text-yellow-500={build.status === 'running'}>{build.status}</div>
<div
class={`badge badge-sm text-xs uppercase rounded bg-coolgray-300 border-none font-bold ${generateBadgeColors(
build.status
)}`}
>
{build.status}
</div>
</div>
<div class="w-48 text-center text-xs">
{#if build.status === 'running'}
<div class="font-bold">{$t('application.build.running')}</div>
<div>
Elapsed
<span class="font-bold">{buildTook}s</span>
<span class="font-bold text-xl"
>{(day().utc().diff(day(build.createdAt)) / 1000).toFixed(0)}s</span
>
</div>
{:else if build.status === 'queued'}
<div class="font-bold">{$t('application.build.queued')}</div>
{:else}
<div>{build.since}</div>
<div>{day(build.updatedAt).utc().fromNow()}</div>
<div>
{$t('application.build.finished_in')} <span class="font-bold">{build.took}s</span>
{$t('application.build.finished_in')}
<span class="font-bold"
>{day(build.updatedAt).utc().diff(day(build.createdAt)) / 1000}s</span
>
</div>
{/if}
</div>
</div>
<Tooltip triggeredBy={`#building-${build.id}`}
>{new Intl.DateTimeFormat('default', dateOptions).format(new Date(build.createdAt)) +
`\n${build.status}`}</Tooltip
`\n`}</Tooltip
>
{/each}
</div>
{#if !noMoreBuilds}
{#if buildCount > 5}
<div class="flex space-x-2">
<button disabled={noMoreBuilds} class=" btn btn-sm w-full text-xs" on:click={loadMoreBuilds}
>{$t('application.build.load_more')}</button
<button
disabled={noMoreBuilds}
class=" btn btn-sm w-full text-xs"
on:click={loadMoreBuilds}>{$t('application.build.load_more')}</button
>
</div>
{/if}
@@ -1,222 +0,0 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ stuff, url }) => {
try {
return {
props: {
application: stuff.application
}
};
} catch (error) {
return {
status: 500,
error: new Error(`Could not load ${url}`)
};
}
};
</script>
<script lang="ts">
export let application: any;
import Secret from './_Secret.svelte';
import { get, post } from '$lib/api';
import { page } from '$app/stores';
import { t } from '$lib/translations';
import { goto } from '$app/navigation';
import { errorNotification, getDomain } from '$lib/common';
import { onMount } from 'svelte';
import Loading from '$lib/components/Loading.svelte';
import { addToast } from '$lib/store';
import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
const { id } = $page.params;
let containers: any;
let PRMRSecrets: any;
let applicationSecrets: any;
let loading = {
init: true,
removing: false
};
async function refreshSecrets() {
const data = await get(`/applications/${id}/secrets`);
PRMRSecrets = [...data.secrets];
}
async function removeApplication(container: any) {
try {
loading.removing = true;
await post(`/applications/${id}/stop/preview`, {
pullmergeRequestId: container.pullmergeRequestId
});
return window.location.reload();
} catch (error) {
return errorNotification(error);
}
}
async function redeploy(container: any) {
try {
const { buildId } = await post(`/applications/${id}/deploy`, {
pullmergeRequestId: container.pullmergeRequestId,
branch: container.branch
});
addToast({
message: 'Deployment queued',
type: 'success'
});
if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) {
return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`);
} else {
return await goto(`/applications/${id}/logs/build?buildId=${buildId}`, {
replaceState: true
});
}
} catch (error) {
return errorNotification(error);
}
}
onMount(async () => {
try {
loading.init = true;
const response = await get(`/applications/${id}/previews`);
containers = response.containers;
PRMRSecrets = response.PRMRSecrets;
applicationSecrets = response.applicationSecrets;
} catch (error) {
return errorNotification(error);
} finally {
loading.init = false;
}
});
</script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Preview Deployments
</div>
<span class="text-xs">{application?.name}</span>
</div>
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
<a
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
{/if}
</div>
{#if loading.init}
<Loading />
{:else}
<div class="mx-auto max-w-6xl px-6 pt-4">
<div class="flex justify-center py-4 text-center">
<SimpleExplainer
customClass="w-full"
text={applicationSecrets.length === 0
? "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."
: "These values overwrite application secrets in PR/MR deployments. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."}
/>
</div>
{#if applicationSecrets.length !== 0}
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
<th scope="col">{$t('forms.name')}</th>
<th scope="col">{$t('forms.value')}</th>
<th scope="col" class="w-64 text-center"
>{$t('application.preview.need_during_buildtime')}</th
>
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
</tr>
</thead>
<tbody>
{#each applicationSecrets as secret}
{#key secret.id}
<tr>
<Secret
PRMRSecret={PRMRSecrets.find((s) => s.name === secret.name)}
isPRMRSecret
name={secret.name}
value={secret.value}
isBuildSecret={secret.isBuildSecret}
on:refresh={refreshSecrets}
/>
</tr>
{/key}
{/each}
</tbody>
</table>
{/if}
</div>
<div class="mx-auto max-w-4xl py-10">
<div class="flex flex-wrap justify-center space-x-2">
{#if containers.length > 0}
{#each containers as container}
<a href={container.fqdn} class="p-2 no-underline" target="_blank">
<div class="box-selection text-center hover:border-transparent hover:bg-green-600">
<div class="truncate text-center text-xl font-bold">{getDomain(container.fqdn)}</div>
</div>
</a>
<div class="flex items-center justify-center">
<button
class="btn btn-sm bg-coollabs hover:bg-coollabs-100"
on:click={() => redeploy(container)}>{$t('application.preview.redeploy')}</button
>
</div>
<div class="flex items-center justify-center">
<button
class="btn btn-sm"
class:bg-red-600={!loading.removing}
class:hover:bg-red-500={!loading.removing}
disabled={loading.removing}
on:click={() => removeApplication(container)}
>{loading.removing ? 'Removing...' : 'Remove Application'}
</button>
</div>
{/each}
{:else}
<div class="flex-col">
<div class="text-center font-bold text-xl">
{$t('application.preview.no_previews_available')}
</div>
</div>
{/if}
</div>
</div>
{/if}
@@ -0,0 +1,413 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, stuff, url }) => {
try {
return {
props: {
application: stuff.application
}
};
} catch (error) {
return {
status: 500,
error: new Error(`Could not load ${url}`)
};
}
};
</script>
<script lang="ts">
export let application: any;
import Secret from '../_Secret.svelte';
import { get, post } from '$lib/api';
import { page } from '$app/stores';
import { t } from '$lib/translations';
import { goto } from '$app/navigation';
import { asyncSleep, errorNotification, getDomain, getRndInteger } from '$lib/common';
import { onMount } from 'svelte';
import { addToast } from '$lib/store';
import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
const { id } = $page.params;
let PRMRSecrets: any;
let applicationSecrets: any;
let loading = {
init: true,
restart: false,
removing: false
};
let numberOfGetStatus = 0;
let status: any = {};
async function refreshSecrets() {
const data = await get(`/applications/${id}/secrets`);
PRMRSecrets = [...data.secrets];
}
async function removeApplication(preview: any) {
try {
loading.removing = true;
await post(`/applications/${id}/stop/preview`, {
pullmergeRequestId: preview.pullmergeRequestId
});
return window.location.reload();
} catch (error) {
return errorNotification(error);
}
}
async function redeploy(preview: any) {
try {
const { buildId } = await post(`/applications/${id}/deploy`, {
pullmergeRequestId: preview.pullmergeRequestId,
branch: preview.sourceBranch
});
addToast({
message: 'Deployment queued',
type: 'success'
});
if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) {
return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`);
} else {
return await goto(`/applications/${id}/logs/build?buildId=${buildId}`, {
replaceState: true
});
}
} catch (error) {
return errorNotification(error);
}
}
async function loadPreviewsFromDocker() {
try {
const { previews } = await post(`/applications/${id}/previews/load`, {});
addToast({
message: 'Previews loaded.',
type: 'success'
});
application.previewApplication = previews;
} catch (error) {
return errorNotification(error);
}
}
async function getStatus(resources: any) {
const { applicationId, pullmergeRequestId, id } = resources;
if (status[id]) return status[id];
while (numberOfGetStatus > 1) {
await asyncSleep(getRndInteger(100, 200));
}
try {
numberOfGetStatus++;
let isRunning = false;
let isBuilding = false;
const response = await get(
`/applications/${applicationId}/previews/${pullmergeRequestId}/status`
);
isRunning = response.isRunning;
isBuilding = response.isBuilding;
if (isBuilding) {
status[id] = 'building';
return 'building';
} else if (isRunning) {
status[id] = 'running';
return 'running';
} else {
status[id] = 'stopped';
return 'stopped';
}
} catch (error) {
status[id] = 'error';
return 'error';
} finally {
numberOfGetStatus--;
}
}
async function restartPreview(preview: any) {
try {
loading.restart = true;
const { pullmergeRequestId } = preview;
await post(`/applications/${id}/previews/${pullmergeRequestId}/restart`, {});
addToast({
type: 'success',
message: 'Restart successful.'
});
} catch (error) {
return errorNotification(error);
} finally {
await getStatus(preview);
loading.restart = false;
}
}
onMount(async () => {
try {
loading.init = true;
loading.restart = true;
const response = await get(`/applications/${id}/previews`);
PRMRSecrets = response.PRMRSecrets;
applicationSecrets = response.applicationSecrets;
} catch (error) {
return errorNotification(error);
} finally {
loading.init = false;
loading.restart = false;
}
});
</script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Preview Deployments
</div>
<span class="text-xs">{application?.name}</span>
</div>
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
<a
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
{/if}
</div>
{#if loading.init}
<div class="mx-auto max-w-6xl px-6 pt-4">
<div class="flex justify-center py-4 text-center text-xl font-bold">Loading...</div>
</div>
{:else}
<div class="mx-auto max-w-6xl px-6 pt-4">
<div class="flex justify-center py-4 text-center">
<SimpleExplainer
customClass="w-full"
text={applicationSecrets.length === 0
? "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."
: "These values overwrite application secrets in PR/MR deployments. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."}
/>
</div>
<div class="text-center">
<SimpleExplainer
customClass="w-full"
text={'If your preview is not shown, try load them directly from Docker Engine.<br>(Changed previews process flow in <span class="font-bold text-white">v3.10.4</span>)'}
/>
<button class="btn btn-sm bg-coollabs" on:click={loadPreviewsFromDocker}
>Fetch Previews</button
>
</div>
{#if applicationSecrets.length !== 0}
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
<th scope="col">{$t('forms.name')}</th>
<th scope="col">{$t('forms.value')}</th>
<th scope="col" class="w-64 text-center"
>{$t('application.preview.need_during_buildtime')}</th
>
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
</tr>
</thead>
<tbody>
{#each applicationSecrets as secret}
{#key secret.id}
<tr>
<Secret
PRMRSecret={PRMRSecrets.find((s) => s.name === secret.name)}
isPRMRSecret
name={secret.name}
value={secret.value}
isBuildSecret={secret.isBuildSecret}
on:refresh={refreshSecrets}
/>
</tr>
{/key}
{/each}
</tbody>
</table>
{/if}
</div>
<div class="container lg:mx-auto lg:p-0 px-8 p-5 lg:pt-10">
{#if application.previewApplication.length > 0}
<div
class="grid grid-col gap-8 auto-cols-max grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 p-4"
>
{#each application.previewApplication as preview}
<div class="no-underline mb-5">
<div class="w-full rounded p-5 bg-coolgray-200 indicator">
{#await getStatus(preview)}
<span class="indicator-item badge bg-yellow-500 badge-sm" />
{:then status}
{#if status === 'running'}
<span class="indicator-item badge bg-success badge-sm" />
{:else}
<span class="indicator-item badge bg-error badge-sm" />
{/if}
{/await}
<div class="w-full flex flex-row">
<div class="w-full flex flex-col">
<h1 class="font-bold text-lg lg:text-xl truncate">
PR #{preview.pullmergeRequestId}
{#if status[preview.id] === 'building'}
<span
class="badge badge-sm text-xs uppercase rounded bg-coolgray-300 text-green-500 border-none font-bold"
>
BUILDING
</span>
{/if}
</h1>
<div class="h-10 text-xs">
<h2>{preview.customDomain.replace('https://', '').replace('http://', '')}</h2>
</div>
<div class="flex justify-end items-end space-x-2 h-10">
{#if preview.customDomain}
<a id="openpreview" href={preview.customDomain} target="_blank" class="icons">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg>
</a>
{/if}
<Tooltip triggeredBy="#openpreview">Open Preview</Tooltip>
<div class="border border-coolgray-500 h-8" />
{#if loading.restart}
<button
class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out hover:bg-transparent"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
<line x1="5.63" y1="7.16" x2="5.63" y2="7.17" />
<line x1="4.06" y1="11" x2="4.06" y2="11.01" />
<line x1="4.63" y1="15.1" x2="4.63" y2="15.11" />
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
<line x1="11" y1="19.94" x2="11" y2="19.95" />
</svg>
</button>
{:else}
<button
id="restart"
on:click={() => restartPreview(preview)}
type="submit"
class="icons bg-transparent text-sm flex items-center space-x-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" />
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
</svg>
</button>
{/if}
<Tooltip triggeredBy="#restart">Restart (useful to change secrets)</Tooltip>
<button
id="forceredeploypreview"
class="icons"
on:click={() => redeploy(preview)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M16.3 5h.7a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h5l-2.82 -2.82m0 5.64l2.82 -2.82"
transform="rotate(-45 12 12)"
/>
</svg></button
>
<Tooltip triggeredBy="#forceredeploypreview"
>Force redeploy (without cache)</Tooltip
>
<div class="border border-coolgray-500 h-8" />
<button
id="deletepreview"
class="icons"
class:hover:text-error={!loading.removing}
disabled={loading.removing}
on:click={() => removeApplication(preview)}
><DeleteIcon />
</button>
<Tooltip triggeredBy="#deletepreview">Delete Preview</Tooltip>
</div>
</div>
</div>
</div>
</div>
{/each}
</div>
{:else}
<div class="flex-col">
<div class="text-center font-bold text-xl pb-10">Previews will shown here.</div>
</div>
{/if}
</div>
{/if}
@@ -22,7 +22,7 @@ export async function saveSecret({
applicationId
}: Props): Promise<void> {
if (!name) return errorNotification(`${t.get('forms.name')} ${t.get('forms.is_required')}`);
if (!value) return errorNotification(`${t.get('forms.value')} ${t.get('forms.is_required')}`);
if (!value && isNew) return errorNotification(`${t.get('forms.value')} ${t.get('forms.is_required')}`);
try {
await post(`/applications/${applicationId}/secrets`, {
name,
+2 -4
View File
@@ -31,7 +31,7 @@
import { get, post } from '$lib/api';
import Usage from '$lib/components/Usage.svelte';
import { t } from '$lib/translations';
import { asyncSleep } from '$lib/common';
import { asyncSleep, getRndInteger } from '$lib/common';
import { appSession, search, addToast} from '$lib/store';
import ApplicationsIcons from '$lib/components/svg/applications/ApplicationIcons.svelte';
@@ -87,9 +87,7 @@
filtered.destinations = [];
filtered.otherDestinations = [];
}
function getRndInteger(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
async function getStatus(resources: any) {
const { id, buildPack, dualCerts } = resources;
+1 -1
View File
@@ -37,7 +37,7 @@
<div class="grid grid-col gap-8 auto-cols-max grid-cols-1 p-4">
{#each servers as server}
<div class="no-underline mb-5">
<div class="w-full rounded bg-coolgray-100 indicator">
<div class="w-full rounded bg-coolgray-200 indicator">
{#if $appSession.teamId === '0'}
<Usage {server} />
{/if}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "3.10.3",
"version": "3.10.4",
"license": "Apache-2.0",
"repository": "github:coollabsio/coolify",
"scripts": {
+2
View File
@@ -144,6 +144,7 @@ importers:
classnames: 2.3.1
cuid: 2.1.8
daisyui: 2.24.2
dayjs: 1.11.5
eslint: 8.23.0
eslint-config-prettier: 8.5.0
eslint-plugin-svelte3: 4.0.0
@@ -169,6 +170,7 @@ importers:
'@tailwindcss/typography': 0.5.7_tailwindcss@3.1.8
cuid: 2.1.8
daisyui: 2.24.2_25hquoklqeoqwmt7fwvvcyxm5e
dayjs: 1.11.5
js-cookie: 3.0.1
p-limit: 4.0.0
svelte-select: 4.4.7