feat: previewApplications finalized
This commit is contained in:
parent
c40b80436a
commit
d9908b3d61
@ -4,7 +4,8 @@ ALTER TABLE "Build" ADD COLUMN "previewApplicationId" TEXT;
|
|||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "PreviewApplication" (
|
CREATE TABLE "PreviewApplication" (
|
||||||
"id" TEXT NOT NULL PRIMARY KEY,
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
"prMrId" TEXT NOT NULL,
|
"pullmergeRequestId" TEXT NOT NULL,
|
||||||
|
"sourceBranch" TEXT NOT NULL,
|
||||||
"isRandomDomain" BOOLEAN NOT NULL DEFAULT false,
|
"isRandomDomain" BOOLEAN NOT NULL DEFAULT false,
|
||||||
"customDomain" TEXT,
|
"customDomain" TEXT,
|
||||||
"applicationId" TEXT NOT NULL,
|
"applicationId" TEXT NOT NULL,
|
@ -119,18 +119,19 @@ model Application {
|
|||||||
secrets Secret[]
|
secrets Secret[]
|
||||||
teams Team[]
|
teams Team[]
|
||||||
connectedDatabase ApplicationConnectedDatabase?
|
connectedDatabase ApplicationConnectedDatabase?
|
||||||
previewApplication PreviewApplication[]
|
previewApplication PreviewApplication[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model PreviewApplication {
|
model PreviewApplication {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
prMrId String
|
pullmergeRequestId String
|
||||||
isRandomDomain Boolean @default(false)
|
sourceBranch String
|
||||||
customDomain String?
|
isRandomDomain Boolean @default(false)
|
||||||
applicationId String @unique
|
customDomain String?
|
||||||
application Application @relation(fields: [applicationId], references: [id])
|
applicationId String @unique
|
||||||
createdAt DateTime @default(now())
|
application Application @relation(fields: [applicationId], references: [id])
|
||||||
updatedAt DateTime @updatedAt
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model ApplicationConnectedDatabase {
|
model ApplicationConnectedDatabase {
|
||||||
@ -222,22 +223,22 @@ model BuildLog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Build {
|
model Build {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
type String
|
type String
|
||||||
applicationId String?
|
applicationId String?
|
||||||
destinationDockerId String?
|
destinationDockerId String?
|
||||||
gitSourceId String?
|
gitSourceId String?
|
||||||
githubAppId String?
|
githubAppId String?
|
||||||
gitlabAppId String?
|
gitlabAppId String?
|
||||||
commit String?
|
commit String?
|
||||||
pullmergeRequestId String?
|
pullmergeRequestId String?
|
||||||
previewApplicationId String?
|
previewApplicationId String?
|
||||||
forceRebuild Boolean @default(false)
|
forceRebuild Boolean @default(false)
|
||||||
sourceBranch String?
|
sourceBranch String?
|
||||||
branch String?
|
branch String?
|
||||||
status String? @default("queued")
|
status String? @default("queued")
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model DestinationDocker {
|
model DestinationDocker {
|
||||||
|
@ -42,7 +42,7 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
application = decryptApplication(application)
|
application = decryptApplication(application)
|
||||||
const originalApplicationId = application.id
|
const originalApplicationId = application.id
|
||||||
if (pullmergeRequestId) {
|
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) {
|
if (previewApplications.length > 0) {
|
||||||
previewApplicationId = previewApplications[0].id
|
previewApplicationId = previewApplications[0].id
|
||||||
}
|
}
|
||||||
|
@ -707,7 +707,6 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
|
|||||||
Dockerfile.push(`RUN ${installCommand}`);
|
Dockerfile.push(`RUN ${installCommand}`);
|
||||||
}
|
}
|
||||||
Dockerfile.push(`RUN ${buildCommand}`);
|
Dockerfile.push(`RUN ${buildCommand}`);
|
||||||
console.log(Dockerfile.join('\n'))
|
|
||||||
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
|
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
|
||||||
await buildImage({ ...data, isCache: true });
|
await buildImage({ ...data, isCache: true });
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ import { scheduler } from './scheduler';
|
|||||||
import { supportedServiceTypesAndVersions } from './services/supportedVersions';
|
import { supportedServiceTypesAndVersions } from './services/supportedVersions';
|
||||||
import { includeServices } from './services/common';
|
import { includeServices } from './services/common';
|
||||||
|
|
||||||
export const version = '3.10.3';
|
export const version = '3.10.4';
|
||||||
export const isDev = process.env.NODE_ENV === 'development';
|
export const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
const algorithm = 'aes-256-ctr';
|
const algorithm = 'aes-256-ctr';
|
||||||
|
@ -12,7 +12,7 @@ import { checkDomainsIsValidInDNS, checkDoubleBranch, checkExposedPort, createDi
|
|||||||
import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker';
|
import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker';
|
||||||
|
|
||||||
import type { FastifyRequest } from 'fastify';
|
import type { FastifyRequest } from 'fastify';
|
||||||
import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication } from './types';
|
import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication, RestartPreviewApplication } from './types';
|
||||||
import { OnlyId } from '../../../../types';
|
import { OnlyId } from '../../../../types';
|
||||||
|
|
||||||
function filterObject(obj, callback) {
|
function filterObject(obj, callback) {
|
||||||
@ -83,8 +83,6 @@ export async function getApplicationStatus(request: FastifyRequest<OnlyId>) {
|
|||||||
isExited = status.status.isExited;
|
isExited = status.status.isExited;
|
||||||
isRestarting = status.status.isRestarting
|
isRestarting = status.status.isRestarting
|
||||||
}
|
}
|
||||||
|
|
||||||
// isExited = await isContainerExited(application.destinationDocker.id, id);
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
isRunning,
|
isRunning,
|
||||||
@ -164,7 +162,8 @@ export async function getApplicationFromDB(id: string, teamId: string) {
|
|||||||
gitSource: { include: { githubApp: true, gitlabApp: true } },
|
gitSource: { include: { githubApp: true, gitlabApp: true } },
|
||||||
secrets: true,
|
secrets: true,
|
||||||
persistentStorage: true,
|
persistentStorage: true,
|
||||||
connectedDatabase: true
|
connectedDatabase: true,
|
||||||
|
previewApplication: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!application) {
|
if (!application) {
|
||||||
@ -350,6 +349,7 @@ export async function stopPreviewApplication(request: FastifyRequest<StopPreview
|
|||||||
if (found) {
|
if (found) {
|
||||||
await removeContainer({ id: container, dockerId: application.destinationDocker.id });
|
await removeContainer({ id: container, dockerId: application.destinationDocker.id });
|
||||||
}
|
}
|
||||||
|
await prisma.previewApplication.deleteMany({ where: { applicationId: application.id, pullmergeRequestId } })
|
||||||
}
|
}
|
||||||
return reply.code(201).send();
|
return reply.code(201).send();
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
@ -617,7 +617,7 @@ export async function deployApplication(request: FastifyRequest<DeployApplicatio
|
|||||||
githubAppId: application.gitSource?.githubApp?.id,
|
githubAppId: application.gitSource?.githubApp?.id,
|
||||||
gitlabAppId: application.gitSource?.gitlabApp?.id,
|
gitlabAppId: application.gitSource?.gitlabApp?.id,
|
||||||
status: 'queued',
|
status: 'queued',
|
||||||
type: 'manual'
|
type: pullmergeRequestId ? application.gitSource?.githubApp?.id ? 'manual_pr' : 'manual_mr' : 'manual'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@ -808,7 +808,6 @@ export async function saveSecret(request: FastifyRequest<SaveSecret>, reply: Fas
|
|||||||
try {
|
try {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
let { name, value, isBuildSecret, isPRMRSecret, isNew } = request.body
|
let { name, value, isBuildSecret, isPRMRSecret, isNew } = request.body
|
||||||
|
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
const found = await prisma.secret.findFirst({ where: { name, applicationId: id, isPRMRSecret } });
|
const found = await prisma.secret.findFirst({ where: { name, applicationId: id, isPRMRSecret } });
|
||||||
if (found) {
|
if (found) {
|
||||||
@ -820,14 +819,24 @@ export async function saveSecret(request: FastifyRequest<SaveSecret>, reply: Fas
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
value = encrypt(value.trim());
|
if (value) {
|
||||||
|
value = encrypt(value.trim());
|
||||||
|
}
|
||||||
const found = await prisma.secret.findFirst({ where: { applicationId: id, name, isPRMRSecret } });
|
const found = await prisma.secret.findFirst({ where: { applicationId: id, name, isPRMRSecret } });
|
||||||
|
|
||||||
if (found) {
|
if (found) {
|
||||||
await prisma.secret.updateMany({
|
if (!value && isPRMRSecret) {
|
||||||
where: { applicationId: id, name, isPRMRSecret },
|
await prisma.secret.deleteMany({
|
||||||
data: { value, isBuildSecret, isPRMRSecret }
|
where: { applicationId: id, name, isPRMRSecret }
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
|
||||||
|
await prisma.secret.updateMany({
|
||||||
|
where: { applicationId: id, name, isPRMRSecret },
|
||||||
|
data: { value, isBuildSecret, isPRMRSecret }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
await prisma.secret.create({
|
await prisma.secret.create({
|
||||||
data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } }
|
data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } }
|
||||||
@ -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>) {
|
export async function getPreviews(request: FastifyRequest<OnlyId>) {
|
||||||
try {
|
try {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
@ -909,26 +1093,7 @@ export async function getPreviews(request: FastifyRequest<OnlyId>) {
|
|||||||
|
|
||||||
const applicationSecrets = secrets.filter((secret) => !secret.isPRMRSecret);
|
const applicationSecrets = secrets.filter((secret) => !secret.isPRMRSecret);
|
||||||
const PRMRSecrets = secrets.filter((secret) => secret.isPRMRSecret);
|
const PRMRSecrets = secrets.filter((secret) => secret.isPRMRSecret);
|
||||||
const application = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } });
|
|
||||||
const { stdout } = await executeDockerCmd({ dockerId: application.destinationDocker.id, command: `docker container ls --filter 'name=${id}-' --format "{{json .}}"` })
|
|
||||||
if (stdout === '') {
|
|
||||||
return {
|
|
||||||
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 {
|
return {
|
||||||
containers: jsonContainers,
|
|
||||||
applicationSecrets: applicationSecrets.sort((a, b) => {
|
applicationSecrets: applicationSecrets.sort((a, b) => {
|
||||||
return ('' + a.name).localeCompare(b.name);
|
return ('' + a.name).localeCompare(b.name);
|
||||||
}),
|
}),
|
||||||
@ -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 {
|
return {
|
||||||
builds,
|
builds,
|
||||||
buildCount
|
buildCount
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { FastifyPluginAsync } from 'fastify';
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
import { OnlyId } from '../../../../types';
|
import { OnlyId } from '../../../../types';
|
||||||
import { cancelDeployment, checkDNS, checkDomain, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildLogs, getBuildPack, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getSecrets, getStorages, getUsage, listApplications, newApplication, restartApplication, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication } from './handlers';
|
import { cancelDeployment, checkDNS, checkDomain, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, 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> => {
|
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||||
fastify.addHook('onRequest', async (request) => {
|
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.delete<DeleteStorage>('/:id/storages', async (request) => await deleteStorage(request));
|
||||||
|
|
||||||
fastify.get<OnlyId>('/:id/previews', async (request) => await getPreviews(request));
|
fastify.get<OnlyId>('/:id/previews', async (request) => await getPreviews(request));
|
||||||
|
fastify.post<OnlyId>('/:id/previews/load', async (request) => await loadPreviews(request));
|
||||||
|
fastify.get<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/status', async (request) => await getPreviewStatus(request));
|
||||||
|
fastify.post<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/restart', async (request, reply) => await restartPreview(request, reply));
|
||||||
|
|
||||||
fastify.get<GetApplicationLogs>('/:id/logs', async (request) => await getApplicationLogs(request));
|
fastify.get<GetApplicationLogs>('/:id/logs', async (request) => await getApplicationLogs(request));
|
||||||
fastify.get<GetBuildLogs>('/:id/logs/build', async (request) => await getBuildLogs(request));
|
fastify.get<GetBuildLogs>('/:id/logs/build', async (request) => await getBuildLogs(request));
|
||||||
|
@ -126,4 +126,10 @@ export interface StopPreviewApplication extends OnlyId {
|
|||||||
Body: {
|
Body: {
|
||||||
pullmergeRequestId: string | null,
|
pullmergeRequestId: string | null,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
export interface RestartPreviewApplication {
|
||||||
|
Params: {
|
||||||
|
id: string,
|
||||||
|
pullmergeRequestId: string | null,
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import cuid from "cuid";
|
import cuid from "cuid";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { encrypt, errorHandler, getUIUrl, isDev, prisma } from "../../../lib/common";
|
import { encrypt, errorHandler, getDomain, getUIUrl, isDev, prisma } from "../../../lib/common";
|
||||||
import { checkContainer, removeContainer } from "../../../lib/docker";
|
import { checkContainer, removeContainer } from "../../../lib/docker";
|
||||||
import { createdBranchDatabase, getApplicationFromDBWebhook, removeBranchDatabase } from "../../api/v1/applications/handlers";
|
import { createdBranchDatabase, getApplicationFromDBWebhook, removeBranchDatabase } from "../../api/v1/applications/handlers";
|
||||||
|
|
||||||
@ -175,15 +175,23 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
|
|||||||
data: { updatedAt: new Date() }
|
data: { updatedAt: new Date() }
|
||||||
});
|
});
|
||||||
let previewApplicationId = undefined
|
let previewApplicationId = undefined
|
||||||
if (pullmergeRequestId) {
|
if (pullmergeRequestId) {
|
||||||
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) {
|
if (foundPreviewApplications.length > 0) {
|
||||||
previewApplicationId = foundPreviewApplications[0].id
|
previewApplicationId = foundPreviewApplications[0].id
|
||||||
} else {
|
} else {
|
||||||
const previewApplication = await prisma.previewApplication.create({ data: { prMrId: pullmergeRequestId, application: { connect: { id: application.id } } } })
|
const protocol = application.fqdn.includes('https://') ? 'https://' : 'http://'
|
||||||
previewApplicationId = previewApplication.id
|
const previewApplication = await prisma.previewApplication.create({
|
||||||
}
|
data: {
|
||||||
}
|
pullmergeRequestId,
|
||||||
|
sourceBranch,
|
||||||
|
customDomain: `${protocol}${pullmergeRequestId}.${getDomain(application.fqdn)}`,
|
||||||
|
application: { connect: { id: application.id } }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
previewApplicationId = previewApplication.id
|
||||||
|
}
|
||||||
|
}
|
||||||
// if (application.connectedDatabase && pullmergeRequestAction === 'opened' || pullmergeRequestAction === 'reopened') {
|
// if (application.connectedDatabase && pullmergeRequestAction === 'opened' || pullmergeRequestAction === 'reopened') {
|
||||||
// // Coolify hosted database
|
// // Coolify hosted database
|
||||||
// if (application.connectedDatabase.databaseId) {
|
// if (application.connectedDatabase.databaseId) {
|
||||||
@ -210,7 +218,9 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'Queued. Thank you!'
|
||||||
|
};
|
||||||
} else if (pullmergeRequestAction === 'closed') {
|
} else if (pullmergeRequestAction === 'closed') {
|
||||||
if (application.destinationDockerId) {
|
if (application.destinationDockerId) {
|
||||||
const id = `${application.id}-${pullmergeRequestId}`;
|
const id = `${application.id}-${pullmergeRequestId}`;
|
||||||
@ -218,12 +228,15 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
|
|||||||
await removeContainer({ id, dockerId: application.destinationDocker.id });
|
await removeContainer({ id, dockerId: application.destinationDocker.id });
|
||||||
} catch (error) { }
|
} catch (error) { }
|
||||||
}
|
}
|
||||||
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) {
|
if (foundPreviewApplications.length > 0) {
|
||||||
for (const preview of foundPreviewApplications) {
|
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) {
|
// if (application?.connectedDatabase?.databaseId) {
|
||||||
// const databaseId = application.connectedDatabase.databaseId;
|
// const databaseId = application.connectedDatabase.databaseId;
|
||||||
// const database = await prisma.database.findUnique({ where: { id: databaseId } });
|
// const database = await prisma.database.findUnique({ where: { id: databaseId } });
|
||||||
|
@ -2,7 +2,7 @@ import axios from "axios";
|
|||||||
import cuid from "cuid";
|
import cuid from "cuid";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import type { FastifyReply, FastifyRequest } from "fastify";
|
import type { FastifyReply, FastifyRequest } from "fastify";
|
||||||
import { errorHandler, getAPIUrl, getUIUrl, isDev, listSettings, prisma } from "../../../lib/common";
|
import { errorHandler, getAPIUrl, getDomain, getUIUrl, isDev, listSettings, prisma } from "../../../lib/common";
|
||||||
import { checkContainer, removeContainer } from "../../../lib/docker";
|
import { checkContainer, removeContainer } from "../../../lib/docker";
|
||||||
import { getApplicationFromDB, getApplicationFromDBWebhook } from "../../api/v1/applications/handlers";
|
import { getApplicationFromDB, getApplicationFromDBWebhook } from "../../api/v1/applications/handlers";
|
||||||
|
|
||||||
@ -91,8 +91,8 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (objectKind === 'merge_request') {
|
} else if (objectKind === 'merge_request') {
|
||||||
const { object_attributes: { work_in_progress: isDraft, action, source_branch: sourceBranch, target_branch: targetBranch, iid: pullmergeRequestId }, project: { id } } = request.body
|
const { object_attributes: { work_in_progress: isDraft, action, source_branch: sourceBranch, target_branch: targetBranch }, project: { id } } = request.body
|
||||||
|
const pullmergeRequestId = request.body.object_attributes.iid.toString();
|
||||||
const projectId = Number(id);
|
const projectId = Number(id);
|
||||||
if (!allowedActions.includes(action)) {
|
if (!allowedActions.includes(action)) {
|
||||||
throw { status: 500, message: 'Action not allowed.' }
|
throw { status: 500, message: 'Action not allowed.' }
|
||||||
@ -130,10 +130,29 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
|
|||||||
where: { id: application.id },
|
where: { id: application.id },
|
||||||
data: { updatedAt: new Date() }
|
data: { updatedAt: new Date() }
|
||||||
});
|
});
|
||||||
|
let previewApplicationId = undefined
|
||||||
|
if (pullmergeRequestId) {
|
||||||
|
const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, pullmergeRequestId } })
|
||||||
|
if (foundPreviewApplications.length > 0) {
|
||||||
|
previewApplicationId = foundPreviewApplications[0].id
|
||||||
|
} else {
|
||||||
|
const protocol = application.fqdn.includes('https://') ? 'https://' : 'http://'
|
||||||
|
const previewApplication = await prisma.previewApplication.create({
|
||||||
|
data: {
|
||||||
|
pullmergeRequestId,
|
||||||
|
sourceBranch,
|
||||||
|
customDomain: `${protocol}${pullmergeRequestId}.${getDomain(application.fqdn)}`,
|
||||||
|
application: { connect: { id: application.id } }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
previewApplicationId = previewApplication.id
|
||||||
|
}
|
||||||
|
}
|
||||||
await prisma.build.create({
|
await prisma.build.create({
|
||||||
data: {
|
data: {
|
||||||
id: buildId,
|
id: buildId,
|
||||||
pullmergeRequestId: pullmergeRequestId.toString(),
|
pullmergeRequestId,
|
||||||
|
previewApplicationId,
|
||||||
sourceBranch,
|
sourceBranch,
|
||||||
applicationId: application.id,
|
applicationId: application.id,
|
||||||
destinationDockerId: application.destinationDocker.id,
|
destinationDockerId: application.destinationDocker.id,
|
||||||
@ -150,8 +169,19 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
|
|||||||
} else if (action === 'close') {
|
} else if (action === 'close') {
|
||||||
if (application.destinationDockerId) {
|
if (application.destinationDockerId) {
|
||||||
const id = `${application.id}-${pullmergeRequestId}`;
|
const id = `${application.id}-${pullmergeRequestId}`;
|
||||||
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!'
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,7 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"dayjs": "1.11.5",
|
||||||
"@sveltejs/adapter-static": "1.0.0-next.39",
|
"@sveltejs/adapter-static": "1.0.0-next.39",
|
||||||
"@tailwindcss/typography": "^0.5.7",
|
"@tailwindcss/typography": "^0.5.7",
|
||||||
"cuid": "2.1.8",
|
"cuid": "2.1.8",
|
||||||
|
@ -83,4 +83,8 @@ export function handlerNotFoundLoad(error: any, url: URL) {
|
|||||||
status: 500,
|
status: 500,
|
||||||
error: new Error(`Could not load ${url}`)
|
error: new Error(`Could not load ${url}`)
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRndInteger(min: number, max: number) {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
}
|
}
|
@ -108,21 +108,21 @@
|
|||||||
<div class="stats stats-vertical min-w-[16rem] mb-5 rounded bg-transparent">
|
<div class="stats stats-vertical min-w-[16rem] mb-5 rounded bg-transparent">
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title">Total Memory</div>
|
<div class="stat-title">Total Memory</div>
|
||||||
<div class="stat-value text-2xl">
|
<div class="stat-value text-2xl text-white">
|
||||||
{(usage?.memory?.totalMemMb).toFixed(0)}<span class="text-sm">MB</span>
|
{(usage?.memory?.totalMemMb).toFixed(0)}<span class="text-sm">MB</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title">Used Memory</div>
|
<div class="stat-title">Used Memory</div>
|
||||||
<div class="stat-value text-2xl">
|
<div class="stat-value text-2xl text-white">
|
||||||
{(usage?.memory?.usedMemMb).toFixed(0)}<span class="text-sm">MB</span>
|
{(usage?.memory?.usedMemMb).toFixed(0)}<span class="text-sm">MB</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title">Free Memory</div>
|
<div class="stat-title">Free Memory</div>
|
||||||
<div class="stat-value text-2xl">
|
<div class="stat-value text-2xl text-white">
|
||||||
{(usage?.memory?.freeMemPercentage).toFixed(0)}<span class="text-sm">%</span>
|
{(usage?.memory?.freeMemPercentage).toFixed(0)}<span class="text-sm">%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -131,41 +131,41 @@
|
|||||||
<div class="stats stats-vertical min-w-[20rem] mb-5 bg-transparent rounded">
|
<div class="stats stats-vertical min-w-[20rem] mb-5 bg-transparent rounded">
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title">Total CPU</div>
|
<div class="stat-title">Total CPU</div>
|
||||||
<div class="stat-value text-2xl">
|
<div class="stat-value text-2xl text-white">
|
||||||
{usage?.cpu?.count}
|
{usage?.cpu?.count}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title">CPU Usage</div>
|
<div class="stat-title">CPU Usage</div>
|
||||||
<div class="stat-value text-2xl">
|
<div class="stat-value text-2xl text-white">
|
||||||
{usage?.cpu?.usage}<span class="text-sm">%</span>
|
{usage?.cpu?.usage}<span class="text-sm">%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title">Load Average (5,10,30mins)</div>
|
<div class="stat-title">Load Average (5,10,30mins)</div>
|
||||||
<div class="stat-value text-2xl">{usage?.cpu?.load}</div>
|
<div class="stat-value text-2xl text-white">{usage?.cpu?.load}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stats stats-vertical min-w-[16rem] mb-5 bg-transparent rounded">
|
<div class="stats stats-vertical min-w-[16rem] mb-5 bg-transparent rounded">
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title">Total Disk</div>
|
<div class="stat-title">Total Disk</div>
|
||||||
<div class="stat-value text-2xl">
|
<div class="stat-value text-2xl text-white">
|
||||||
{usage?.disk?.totalGb}<span class="text-sm">GB</span>
|
{usage?.disk?.totalGb}<span class="text-sm">GB</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title">Used Disk</div>
|
<div class="stat-title">Used Disk</div>
|
||||||
<div class="stat-value text-2xl">
|
<div class="stat-value text-2xl text-white">
|
||||||
{usage?.disk?.usedGb}<span class="text-sm">GB</span>
|
{usage?.disk?.usedGb}<span class="text-sm">GB</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title">Free Disk</div>
|
<div class="stat-title">Free Disk</div>
|
||||||
<div class="stat-value text-2xl">
|
<div class="stat-value text-2xl text-white">
|
||||||
{usage?.disk?.freePercentage}<span class="text-sm">%</span>
|
{usage?.disk?.freePercentage}<span class="text-sm">%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
7
apps/ui/src/lib/dayjs.ts
Normal file
7
apps/ui/src/lib/dayjs.ts
Normal 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 };
|
@ -290,7 +290,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
<main>
|
<main>
|
||||||
<div class={$appSession.userId ? 'pl-14 lg:px-20' : null}>
|
<div class={$appSession.userId ? 'pl-14 lg:pl-20' : null}>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
export let isNewSecret = false;
|
export let isNewSecret = false;
|
||||||
export let isPRMRSecret = false;
|
export let isPRMRSecret = false;
|
||||||
export let PRMRSecret: any = {};
|
export let PRMRSecret: any = {};
|
||||||
|
|
||||||
if (isPRMRSecret) value = PRMRSecret.value;
|
if (isPRMRSecret) value = PRMRSecret.value;
|
||||||
|
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
@ -39,7 +38,15 @@
|
|||||||
|
|
||||||
async function createSecret(isNew: any) {
|
async function createSecret(isNew: any) {
|
||||||
try {
|
try {
|
||||||
if (!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({
|
await saveSecret({
|
||||||
isNew,
|
isNew,
|
||||||
name,
|
name,
|
||||||
@ -108,7 +115,6 @@
|
|||||||
name={isNewSecret ? 'secretValue' : 'secretValueNew'}
|
name={isNewSecret ? 'secretValue' : 'secretValueNew'}
|
||||||
isPasswordField={true}
|
isPasswordField={true}
|
||||||
bind:value
|
bind:value
|
||||||
required
|
|
||||||
placeholder="J$#@UIO%HO#$U%H"
|
placeholder="J$#@UIO%HO#$U%H"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
@ -130,7 +136,7 @@
|
|||||||
class:translate-x-0={!isBuildSecret}
|
class:translate-x-0={!isBuildSecret}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class=" absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
|
class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
|
||||||
class:opacity-0={isBuildSecret}
|
class:opacity-0={isBuildSecret}
|
||||||
class:opacity-100={!isBuildSecret}
|
class:opacity-100={!isBuildSecret}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
@ -23,34 +23,30 @@
|
|||||||
export let application: any;
|
export let application: any;
|
||||||
export let buildCount: any;
|
export let buildCount: any;
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
console.log(builds[0].createdAt);
|
||||||
import {addToast} from '$lib/store';
|
import { addToast } from '$lib/store';
|
||||||
import BuildLog from './_BuildLog.svelte';
|
import BuildLog from './_BuildLog.svelte';
|
||||||
import { get, post } from '$lib/api';
|
import { get, post } from '$lib/api';
|
||||||
import { t } from '$lib/translations';
|
import { t } from '$lib/translations';
|
||||||
import { changeQueryParams, dateOptions, errorNotification, asyncSleep } from '$lib/common';
|
import { changeQueryParams, dateOptions, errorNotification, asyncSleep } from '$lib/common';
|
||||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||||
|
import { day } from '$lib/dayjs';
|
||||||
let buildId: any;
|
let buildId: any;
|
||||||
|
|
||||||
let skip = 0;
|
let skip = 0;
|
||||||
let noMoreBuilds = buildCount < 5 || buildCount <= skip;
|
let noMoreBuilds = buildCount < 5 || buildCount <= skip;
|
||||||
|
|
||||||
let buildTook = 0;
|
|
||||||
const { id } = $page.params;
|
const { id } = $page.params;
|
||||||
let preselectedBuildId = $page.url.searchParams.get('buildId');
|
let preselectedBuildId = $page.url.searchParams.get('buildId');
|
||||||
if (preselectedBuildId) buildId = preselectedBuildId;
|
if (preselectedBuildId) buildId = preselectedBuildId;
|
||||||
|
|
||||||
async function updateBuildStatus({ detail }: { detail: any }) {
|
async function updateBuildStatus({ detail }: { detail: any }) {
|
||||||
const { status, took } = detail;
|
const { status } = detail;
|
||||||
if (status !== 'running') {
|
if (status !== 'running') {
|
||||||
try {
|
try {
|
||||||
const data = await get(`/applications/${id}/logs/build?buildId=${buildId}`);
|
const data = await get(`/applications/${id}/logs/build?buildId=${buildId}`);
|
||||||
builds = builds.filter((build: any) => {
|
builds = builds.filter((build: any) => {
|
||||||
if (build.id === data.builds[0].id) {
|
if (build.id === data.builds[0].id) {
|
||||||
build.status = data.builds[0].status;
|
build.status = data.builds[0].status;
|
||||||
build.took = data.builds[0].took;
|
|
||||||
build.since = data.builds[0].since;
|
|
||||||
}
|
}
|
||||||
return build;
|
return build;
|
||||||
});
|
});
|
||||||
@ -62,7 +58,6 @@ import {addToast} from '$lib/store';
|
|||||||
if (build.id === buildId) build.status = status;
|
if (build.id === buildId) build.status = status;
|
||||||
return build;
|
return build;
|
||||||
});
|
});
|
||||||
buildTook = took;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function loadMoreBuilds() {
|
async function loadMoreBuilds() {
|
||||||
@ -84,23 +79,37 @@ import {addToast} from '$lib/store';
|
|||||||
buildId = build;
|
buildId = build;
|
||||||
return changeQueryParams(buildId);
|
return changeQueryParams(buildId);
|
||||||
}
|
}
|
||||||
async function resetQueue() {
|
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? ');
|
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) {
|
if (sure) {
|
||||||
|
try {
|
||||||
try {
|
await post(`/internal/resetQueue`, {});
|
||||||
await post(`/internal/resetQueue`, {});
|
addToast({
|
||||||
addToast({
|
|
||||||
message: 'Queue reset done.',
|
message: 'Queue reset done.',
|
||||||
type: 'success'
|
type: 'success'
|
||||||
});
|
});
|
||||||
await asyncSleep(500)
|
await asyncSleep(500);
|
||||||
return window.location.reload()
|
return window.location.reload();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorNotification(error);
|
return errorNotification(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
function generateBadgeColors(status: string) {
|
||||||
|
if (status === 'failed') {
|
||||||
|
return 'text-red-500';
|
||||||
|
} else if (status === 'running') {
|
||||||
|
return 'text-yellow-300';
|
||||||
|
} else if (status === 'success') {
|
||||||
|
return 'text-green-500';
|
||||||
|
} else if (status === 'canceled') {
|
||||||
|
return 'text-orange-500';
|
||||||
|
} else {
|
||||||
|
return 'text-white';
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
|
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
|
||||||
@ -156,7 +165,9 @@ import {addToast} from '$lib/store';
|
|||||||
</div>
|
</div>
|
||||||
<div class="block flex-row justify-start space-x-2 px-5 pt-6 sm:px-10 md:flex">
|
<div class="block flex-row justify-start space-x-2 px-5 pt-6 sm:px-10 md:flex">
|
||||||
<div class="mb-4 min-w-[16rem] space-y-2 md:mb-0 ">
|
<div class="mb-4 min-w-[16rem] space-y-2 md:mb-0 ">
|
||||||
<button class="btn btn-sm text-xs w-full bg-error" on:click={resetQueue}>Reset Build Queue</button>
|
<button class="btn btn-sm text-xs w-full bg-error" on:click={resetQueue}
|
||||||
|
>Reset Build Queue</button
|
||||||
|
>
|
||||||
<div class="top-4 md:sticky">
|
<div class="top-4 md:sticky">
|
||||||
{#each builds as build, index (build.id)}
|
{#each builds as build, index (build.id)}
|
||||||
<div
|
<div
|
||||||
@ -164,8 +175,8 @@ import {addToast} from '$lib/store';
|
|||||||
on:click={() => loadBuild(build.id)}
|
on:click={() => loadBuild(build.id)}
|
||||||
class:rounded-tr={index === 0}
|
class:rounded-tr={index === 0}
|
||||||
class:rounded-br={index === builds.length - 1}
|
class:rounded-br={index === builds.length - 1}
|
||||||
class="flex cursor-pointer items-center justify-center py-4 no-underline transition-all duration-100 hover:bg-coolgray-400 hover:shadow-xl"
|
class="flex cursor-pointer items-center justify-center py-4 no-underline transition-all duration-100 hover:bg-coolgray-300 hover:shadow-xl"
|
||||||
class:bg-coolgray-400={buildId === build.id}
|
class:bg-coolgray-200={buildId === build.id}
|
||||||
>
|
>
|
||||||
<div class="flex-col px-2 text-center min-w-[10rem]">
|
<div class="flex-col px-2 text-center min-w-[10rem]">
|
||||||
<div class="text-sm font-bold">
|
<div class="text-sm font-bold">
|
||||||
@ -174,41 +185,46 @@ import {addToast} from '$lib/store';
|
|||||||
<div class="text-xs">
|
<div class="text-xs">
|
||||||
{build.type}
|
{build.type}
|
||||||
</div>
|
</div>
|
||||||
<div class="badge badge-sm text-xs text-white uppercase rounded bg-coolgray-300 border-none font-bold"
|
<div
|
||||||
class:text-red-500={build.status === 'failed'}
|
class={`badge badge-sm text-xs uppercase rounded bg-coolgray-300 border-none font-bold ${generateBadgeColors(
|
||||||
class:text-orange-500={build.status === 'canceled'}
|
build.status
|
||||||
class:text-green-500={build.status === 'success'}
|
)}`}
|
||||||
class:text-yellow-500={build.status === 'running'}>{build.status}</div>
|
>
|
||||||
|
{build.status}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-48 text-center text-xs">
|
<div class="w-48 text-center text-xs">
|
||||||
{#if build.status === 'running'}
|
{#if build.status === 'running'}
|
||||||
<div class="font-bold">{$t('application.build.running')}</div>
|
|
||||||
<div>
|
<div>
|
||||||
Elapsed
|
<span class="font-bold text-xl"
|
||||||
<span class="font-bold">{buildTook}s</span>
|
>{(day().utc().diff(day(build.createdAt)) / 1000).toFixed(0)}s</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{:else if build.status === 'queued'}
|
|
||||||
<div class="font-bold">{$t('application.build.queued')}</div>
|
|
||||||
{:else}
|
{:else}
|
||||||
<div>{build.since}</div>
|
<div>{day(build.updatedAt).utc().fromNow()}</div>
|
||||||
<div>
|
<div>
|
||||||
{$t('application.build.finished_in')} <span class="font-bold">{build.took}s</span>
|
{$t('application.build.finished_in')}
|
||||||
|
<span class="font-bold"
|
||||||
|
>{day(build.updatedAt).utc().diff(day(build.createdAt)) / 1000}s</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Tooltip triggeredBy={`#building-${build.id}`}
|
<Tooltip triggeredBy={`#building-${build.id}`}
|
||||||
>{new Intl.DateTimeFormat('default', dateOptions).format(new Date(build.createdAt)) +
|
>{new Intl.DateTimeFormat('default', dateOptions).format(new Date(build.createdAt)) +
|
||||||
`\n${build.status}`}</Tooltip
|
`\n`}</Tooltip
|
||||||
>
|
>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{#if !noMoreBuilds}
|
{#if !noMoreBuilds}
|
||||||
{#if buildCount > 5}
|
{#if buildCount > 5}
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<button disabled={noMoreBuilds} class=" btn btn-sm w-full text-xs" on:click={loadMoreBuilds}
|
<button
|
||||||
>{$t('application.build.load_more')}</button
|
disabled={noMoreBuilds}
|
||||||
|
class=" btn btn-sm w-full text-xs"
|
||||||
|
on:click={loadMoreBuilds}>{$t('application.build.load_more')}</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -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}
|
|
413
apps/ui/src/routes/applications/[id]/previews/index.svelte
Normal file
413
apps/ui/src/routes/applications/[id]/previews/index.svelte
Normal file
@ -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
|
applicationId
|
||||||
}: Props): Promise<void> {
|
}: Props): Promise<void> {
|
||||||
if (!name) return errorNotification(`${t.get('forms.name')} ${t.get('forms.is_required')}`);
|
if (!name) return errorNotification(`${t.get('forms.name')} ${t.get('forms.is_required')}`);
|
||||||
if (!value) return errorNotification(`${t.get('forms.value')} ${t.get('forms.is_required')}`);
|
if (!value && isNew) return errorNotification(`${t.get('forms.value')} ${t.get('forms.is_required')}`);
|
||||||
try {
|
try {
|
||||||
await post(`/applications/${applicationId}/secrets`, {
|
await post(`/applications/${applicationId}/secrets`, {
|
||||||
name,
|
name,
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
import { get, post } from '$lib/api';
|
import { get, post } from '$lib/api';
|
||||||
import Usage from '$lib/components/Usage.svelte';
|
import Usage from '$lib/components/Usage.svelte';
|
||||||
import { t } from '$lib/translations';
|
import { t } from '$lib/translations';
|
||||||
import { asyncSleep } from '$lib/common';
|
import { asyncSleep, getRndInteger } from '$lib/common';
|
||||||
import { appSession, search, addToast} from '$lib/store';
|
import { appSession, search, addToast} from '$lib/store';
|
||||||
|
|
||||||
import ApplicationsIcons from '$lib/components/svg/applications/ApplicationIcons.svelte';
|
import ApplicationsIcons from '$lib/components/svg/applications/ApplicationIcons.svelte';
|
||||||
@ -87,9 +87,7 @@
|
|||||||
filtered.destinations = [];
|
filtered.destinations = [];
|
||||||
filtered.otherDestinations = [];
|
filtered.otherDestinations = [];
|
||||||
}
|
}
|
||||||
function getRndInteger(min: number, max: number) {
|
|
||||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getStatus(resources: any) {
|
async function getStatus(resources: any) {
|
||||||
const { id, buildPack, dualCerts } = resources;
|
const { id, buildPack, dualCerts } = resources;
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
<div class="grid grid-col gap-8 auto-cols-max grid-cols-1 p-4">
|
<div class="grid grid-col gap-8 auto-cols-max grid-cols-1 p-4">
|
||||||
{#each servers as server}
|
{#each servers as server}
|
||||||
<div class="no-underline mb-5">
|
<div class="no-underline mb-5">
|
||||||
<div class="w-full rounded bg-coolgray-100 indicator">
|
<div class="w-full rounded bg-coolgray-200 indicator">
|
||||||
{#if $appSession.teamId === '0'}
|
{#if $appSession.teamId === '0'}
|
||||||
<Usage {server} />
|
<Usage {server} />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "coolify",
|
"name": "coolify",
|
||||||
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
|
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
|
||||||
"version": "3.10.3",
|
"version": "3.10.4",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"repository": "github:coollabsio/coolify",
|
"repository": "github:coollabsio/coolify",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
2
pnpm-lock.yaml
generated
2
pnpm-lock.yaml
generated
@ -144,6 +144,7 @@ importers:
|
|||||||
classnames: 2.3.1
|
classnames: 2.3.1
|
||||||
cuid: 2.1.8
|
cuid: 2.1.8
|
||||||
daisyui: 2.24.2
|
daisyui: 2.24.2
|
||||||
|
dayjs: 1.11.5
|
||||||
eslint: 8.23.0
|
eslint: 8.23.0
|
||||||
eslint-config-prettier: 8.5.0
|
eslint-config-prettier: 8.5.0
|
||||||
eslint-plugin-svelte3: 4.0.0
|
eslint-plugin-svelte3: 4.0.0
|
||||||
@ -169,6 +170,7 @@ importers:
|
|||||||
'@tailwindcss/typography': 0.5.7_tailwindcss@3.1.8
|
'@tailwindcss/typography': 0.5.7_tailwindcss@3.1.8
|
||||||
cuid: 2.1.8
|
cuid: 2.1.8
|
||||||
daisyui: 2.24.2_25hquoklqeoqwmt7fwvvcyxm5e
|
daisyui: 2.24.2_25hquoklqeoqwmt7fwvvcyxm5e
|
||||||
|
dayjs: 1.11.5
|
||||||
js-cookie: 3.0.1
|
js-cookie: 3.0.1
|
||||||
p-limit: 4.0.0
|
p-limit: 4.0.0
|
||||||
svelte-select: 4.4.7
|
svelte-select: 4.4.7
|
||||||
|
Loading…
x
Reference in New Issue
Block a user