Merge pull request from coollabsio/next

v3.10.12
This commit is contained in:
Andras Bacsai 2022-09-29 14:09:37 +02:00 committed by GitHub
commit ac5cc8b299
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 287 additions and 106 deletions

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

@ -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.11'; export const version = '3.10.12';
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';

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

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

@ -1,6 +1,6 @@
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, getBuildPack, getBuilds, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getPreviewStatus, getSecrets, getStorages, getUsage, listApplications, loadPreviews, newApplication, restartApplication, restartPreview, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication, updatePreviewSecret, updateSecret } from './handlers'; import { cancelDeployment, checkDNS, checkDomain, checkRepository, cleanupUnconfiguredApplications, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildPack, getBuilds, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getPreviewStatus, getSecrets, getStorages, getUsage, listApplications, loadPreviews, newApplication, restartApplication, restartPreview, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication, updatePreviewSecret, updateSecret } from './handlers';
import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuilds, GetImages, RestartPreviewApplication, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types'; import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuilds, GetImages, RestartPreviewApplication, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types';
@ -11,6 +11,8 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get('/', async (request) => await listApplications(request)); fastify.get('/', async (request) => await listApplications(request));
fastify.post<GetImages>('/images', async (request) => await getImages(request)); fastify.post<GetImages>('/images', async (request) => await getImages(request));
fastify.post<any>('/cleanup/unconfigured', async (request) => await cleanupUnconfiguredApplications(request));
fastify.post('/new', async (request, reply) => await newApplication(request, reply)); fastify.post('/new', async (request, reply) => await newApplication(request, reply));
fastify.get<OnlyId>('/:id', async (request) => await getApplication(request)); fastify.get<OnlyId>('/:id', async (request) => await getApplication(request));

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

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

@ -122,7 +122,7 @@ export async function showDashboard(request: FastifyRequest) {
try { try {
const userId = request.user.userId; const userId = request.user.userId;
const teamId = request.user.teamId; const teamId = request.user.teamId;
const applications = await prisma.application.findMany({ let applications = await prisma.application.findMany({
where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } }, where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } },
include: { settings: true, destinationDocker: true, teams: true }, include: { settings: true, destinationDocker: true, teams: true },
}); });
@ -143,7 +143,29 @@ export async function showDashboard(request: FastifyRequest) {
include: { teams: true }, include: { teams: true },
}); });
const settings = await listSettings(); const settings = await listSettings();
let foundUnconfiguredApplication = false;
for (const application of applications) {
if (!application.buildPack || !application.destinationDockerId || !application.branch || (!application.settings?.isBot && !application?.fqdn)) {
foundUnconfiguredApplication = true
}
}
let foundUnconfiguredService = false;
for (const service of services) {
if (!service.fqdn) {
foundUnconfiguredService = true
}
}
let foundUnconfiguredDatabase = false;
for (const database of databases) {
if (!database.version) {
foundUnconfiguredDatabase = true
}
}
return { return {
foundUnconfiguredApplication,
foundUnconfiguredDatabase,
foundUnconfiguredService,
applications, applications,
databases, databases,
services, services,

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

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

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

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

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

@ -328,8 +328,10 @@
</script> </script>
<form on:submit={handleSubmit}> <form on:submit={handleSubmit}>
<div class="flex lg:flex-row flex-col lg:space-y-0 space-y-2 space-x-0 lg:space-x-2 items-center lg:justify-center"> <div
<div class="custom-select-wrapper"> class="flex lg:flex-row flex-col lg:space-y-0 space-y-2 space-x-0 lg:space-x-2 items-center lg:justify-center lg:px-0 px-8"
>
<div class="custom-select-wrapper w-full">
<label for="groups" class="pb-1">Groups</label> <label for="groups" class="pb-1">Groups</label>
<Select <Select
placeholder={loading.base placeholder={loading.base
@ -355,7 +357,7 @@
optionIdentifier="id" optionIdentifier="id"
/> />
</div> </div>
<div class="custom-select-wrapper"> <div class="custom-select-wrapper w-full">
<label for="projects" class="pb-1">Projects</label> <label for="projects" class="pb-1">Projects</label>
<Select <Select
placeholder={loading.projects placeholder={loading.projects
@ -381,7 +383,7 @@
isSearchable={true} isSearchable={true}
/> />
</div> </div>
<div class="custom-select-wrapper"> <div class="custom-select-wrapper w-full">
<label for="branches" class="pb-1">Branches</label> <label for="branches" class="pb-1">Branches</label>
<Select <Select
placeholder={loading.branches placeholder={loading.branches

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

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

@ -25,7 +25,7 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { page, session } from '$app/stores'; import { page } from '$app/stores';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';

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

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

@ -21,6 +21,9 @@
<script lang="ts"> <script lang="ts">
export let applications: any; export let applications: any;
export let foundUnconfiguredApplication: boolean;
export let foundUnconfiguredService: boolean;
export let foundUnconfiguredDatabase: boolean;
export let databases: any; export let databases: any;
export let services: any; export let services: any;
export let settings: any; export let settings: any;
@ -28,9 +31,9 @@
export let destinations: any; export let destinations: any;
let filtered: any = setInitials(); let filtered: any = setInitials();
import { get } from '$lib/api'; import { get, post } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { asyncSleep, getRndInteger } from '$lib/common'; import { asyncSleep, errorNotification, getRndInteger } from '$lib/common';
import { appSession, search } from '$lib/store'; import { appSession, search } from '$lib/store';
import ApplicationsIcons from '$lib/components/svg/applications/ApplicationIcons.svelte'; import ApplicationsIcons from '$lib/components/svg/applications/ApplicationIcons.svelte';
@ -54,31 +57,28 @@
doSearch(); doSearch();
async function refreshStatusApplications() { async function refreshStatusApplications() {
loading.applications = true;
noInitialStatus.applications = false; noInitialStatus.applications = false;
numberOfGetStatus = 0; numberOfGetStatus = 0;
for (const application of applications) { for (const application of applications) {
await getStatus(application, true); status[application.id] = 'loading';
getStatus(application, true);
} }
loading.applications = false;
} }
async function refreshStatusServices() { async function refreshStatusServices() {
loading.services = true;
noInitialStatus.services = false; noInitialStatus.services = false;
numberOfGetStatus = 0; numberOfGetStatus = 0;
for (const service of services) { for (const service of services) {
await getStatus(service, true); status[service.id] = 'loading';
getStatus(service, true);
} }
loading.services = false;
} }
async function refreshStatusDatabases() { async function refreshStatusDatabases() {
loading.databases = true;
noInitialStatus.databases = false; noInitialStatus.databases = false;
numberOfGetStatus = 0; numberOfGetStatus = 0;
for (const database of databases) { for (const database of databases) {
await getStatus(database, true); status[database.id] = 'loading';
getStatus(database, true);
} }
loading.databases = false;
} }
function setInitials(onlyOthers: boolean = false) { function setInitials(onlyOthers: boolean = false) {
return { return {
@ -325,6 +325,45 @@
filtered = setInitials(); filtered = setInitials();
} }
} }
async function cleanupApplications() {
try {
const sure = confirm(
'Are you sure? This will delete all UNCONFIGURED applications and their data.'
);
if (sure) {
await post(`/applications/cleanup/unconfigured`, {});
return window.location.reload();
}
} catch (error) {
return errorNotification(error);
}
}
async function cleanupServices() {
try {
const sure = confirm(
'Are you sure? This will delete all UNCONFIGURED services and their data.'
);
if (sure) {
await post(`/services/cleanup/unconfigured`, {});
return window.location.reload();
}
} catch (error) {
return errorNotification(error);
}
}
async function cleanupDatabases() {
try {
const sure = confirm(
'Are you sure? This will delete all UNCONFIGURED databases and their data.'
);
if (sure) {
await post(`/databases/cleanup/unconfigured`, {});
return window.location.reload();
}
} catch (error) {
return errorNotification(error);
}
}
</script> </script>
<nav class="header"> <nav class="header">
@ -334,7 +373,7 @@
{/if} {/if}
</nav> </nav>
<div class="container lg:mx-auto lg:p-0 px-8 pt-5"> <div class="container lg:mx-auto lg:p-0 px-8 pt-5">
<div class="space-x-2 lg:flex lg:justify-center text-center mb-4 "> <div class="space-x-2 lg:flex lg:justify-center text-center mb-4 ">
<button <button
class="btn btn-sm btn-ghost" class="btn btn-sm btn-ghost"
class:bg-applications={$search === '!app'} class:bg-applications={$search === '!app'}
@ -521,15 +560,19 @@
</div> </div>
{/if} {/if}
{#if (filtered.applications.length > 0 && applications.length > 0) || filtered.otherApplications.length > 0} {#if (filtered.applications.length > 0 && applications.length > 0) || filtered.otherApplications.length > 0}
<div class="flex items-center mt-10"> <div class="flex items-center mt-10 space-x-2">
<h1 class="title lg:text-3xl pr-4">Applications</h1> <h1 class="title lg:text-3xl">Applications</h1>
<button <button class="btn btn-sm btn-primary" on:click={refreshStatusApplications}
class="btn btn-sm btn-primary"
class:loading={loading.applications}
disabled={loading.applications}
on:click={refreshStatusApplications}
>{noInitialStatus.applications ? 'Load Status' : 'Refresh Status'}</button >{noInitialStatus.applications ? 'Load Status' : 'Refresh Status'}</button
> >
{#if foundUnconfiguredApplication}
<button
class="btn btn-sm"
class:loading={loading.applications}
disabled={loading.applications}
on:click={cleanupApplications}>Cleanup Unconfigured Resources</button
>
{/if}
</div> </div>
{/if} {/if}
{#if filtered.applications.length > 0 && applications.length > 0} {#if filtered.applications.length > 0 && applications.length > 0}
@ -547,7 +590,7 @@
<span class="indicator-item badge bg-yellow-300 badge-sm" /> <span class="indicator-item badge bg-yellow-300 badge-sm" />
{:then} {:then}
{#if !noInitialStatus.applications} {#if !noInitialStatus.applications}
{#if loading.applications} {#if status[application.id] === 'loading'}
<span class="indicator-item badge bg-yellow-300 badge-sm" /> <span class="indicator-item badge bg-yellow-300 badge-sm" />
{:else if status[application.id] === 'running'} {:else if status[application.id] === 'running'}
<span class="indicator-item badge bg-success badge-sm" /> <span class="indicator-item badge bg-success badge-sm" />
@ -559,7 +602,7 @@
<div class="w-full flex flex-row"> <div class="w-full flex flex-row">
<ApplicationsIcons {application} isAbsolute={true} /> <ApplicationsIcons {application} isAbsolute={true} />
<div class="w-full flex flex-col"> <div class="w-full flex flex-col">
<h1 class="font-bold text-lg lg:text-xl truncate"> <h1 class="font-bold text-base truncate">
{application.name} {application.name}
{#if application.settings?.isBot} {#if application.settings?.isBot}
<span class="text-xs badge bg-coolblack border-none text-applications" <span class="text-xs badge bg-coolblack border-none text-applications"
@ -654,7 +697,7 @@
<span class="indicator-item badge bg-yellow-300 badge-sm" /> <span class="indicator-item badge bg-yellow-300 badge-sm" />
{:then} {:then}
{#if !noInitialStatus.applications} {#if !noInitialStatus.applications}
{#if loading.applications} {#if status[application.id] === 'loading'}
<span class="indicator-item badge bg-yellow-300 badge-sm" /> <span class="indicator-item badge bg-yellow-300 badge-sm" />
{:else if status[application.id] === 'running'} {:else if status[application.id] === 'running'}
<span class="indicator-item badge bg-success badge-sm" /> <span class="indicator-item badge bg-success badge-sm" />
@ -666,7 +709,7 @@
<div class="w-full flex flex-row"> <div class="w-full flex flex-row">
<ApplicationsIcons {application} isAbsolute={true} /> <ApplicationsIcons {application} isAbsolute={true} />
<div class="w-full flex flex-col"> <div class="w-full flex flex-col">
<h1 class="font-bold text-lg lg:text-xl truncate"> <h1 class="font-bold text-base truncate">
{application.name} {application.name}
{#if application.settings?.isBot} {#if application.settings?.isBot}
<span class="text-xs badge bg-coolblack border-none text-applications">BOT</span <span class="text-xs badge bg-coolblack border-none text-applications">BOT</span
@ -740,15 +783,19 @@
</div> </div>
{/if} {/if}
{#if (filtered.services.length > 0 && services.length > 0) || filtered.otherServices.length > 0} {#if (filtered.services.length > 0 && services.length > 0) || filtered.otherServices.length > 0}
<div class="flex items-center mt-10"> <div class="flex items-center mt-10 space-x-2">
<h1 class="title lg:text-3xl pr-4">Services</h1> <h1 class="title lg:text-3xl">Services</h1>
<button <button class="btn btn-sm btn-primary" on:click={refreshStatusServices}
class="btn btn-sm btn-primary"
class:loading={loading.services}
disabled={loading.services}
on:click={refreshStatusServices}
>{noInitialStatus.services ? 'Load Status' : 'Refresh Status'}</button >{noInitialStatus.services ? 'Load Status' : 'Refresh Status'}</button
> >
{#if foundUnconfiguredService}
<button
class="btn btn-sm"
class:loading={loading.services}
disabled={loading.services}
on:click={cleanupServices}>Cleanup Unconfigured Resources</button
>
{/if}
</div> </div>
{/if} {/if}
{#if filtered.services.length > 0 && services.length > 0} {#if filtered.services.length > 0 && services.length > 0}
@ -766,7 +813,7 @@
<span class="indicator-item badge bg-yellow-300 badge-sm" /> <span class="indicator-item badge bg-yellow-300 badge-sm" />
{:then} {:then}
{#if !noInitialStatus.services} {#if !noInitialStatus.services}
{#if loading.services} {#if status[service.id] === 'loading'}
<span class="indicator-item badge bg-yellow-300 badge-sm" /> <span class="indicator-item badge bg-yellow-300 badge-sm" />
{:else if status[service.id] === 'running'} {:else if status[service.id] === 'running'}
<span class="indicator-item badge bg-success badge-sm" /> <span class="indicator-item badge bg-success badge-sm" />
@ -778,7 +825,7 @@
<div class="w-full flex flex-row"> <div class="w-full flex flex-row">
<ServiceIcons type={service.type} isAbsolute={true} /> <ServiceIcons type={service.type} isAbsolute={true} />
<div class="w-full flex flex-col"> <div class="w-full flex flex-col">
<h1 class="font-bold text-lg lg:text-xl truncate">{service.name}</h1> <h1 class="font-bold text-base truncate">{service.name}</h1>
<div class="h-10 text-xs"> <div class="h-10 text-xs">
{#if service?.fqdn} {#if service?.fqdn}
<h2>{service?.fqdn.replace('https://', '').replace('http://', '')}</h2> <h2>{service?.fqdn.replace('https://', '').replace('http://', '')}</h2>
@ -839,7 +886,7 @@
<span class="indicator-item badge bg-yellow-300 badge-sm" /> <span class="indicator-item badge bg-yellow-300 badge-sm" />
{:then} {:then}
{#if !noInitialStatus.services} {#if !noInitialStatus.services}
{#if loading.services} {#if status[service.id] === 'loading'}
<span class="indicator-item badge bg-yellow-300 badge-sm" /> <span class="indicator-item badge bg-yellow-300 badge-sm" />
{:else if status[service.id] === 'running'} {:else if status[service.id] === 'running'}
<span class="indicator-item badge bg-success badge-sm" /> <span class="indicator-item badge bg-success badge-sm" />
@ -851,7 +898,7 @@
<div class="w-full flex flex-row"> <div class="w-full flex flex-row">
<ServiceIcons type={service.type} isAbsolute={true} /> <ServiceIcons type={service.type} isAbsolute={true} />
<div class="w-full flex flex-col"> <div class="w-full flex flex-col">
<h1 class="font-bold text-lg lg:text-xl truncate">{service.name}</h1> <h1 class="font-bold text-base truncate">{service.name}</h1>
<div class="h-10 text-xs"> <div class="h-10 text-xs">
{#if service?.fqdn} {#if service?.fqdn}
<h2>{service?.fqdn.replace('https://', '').replace('http://', '')}</h2> <h2>{service?.fqdn.replace('https://', '').replace('http://', '')}</h2>
@ -894,15 +941,19 @@
</div> </div>
{/if} {/if}
{#if (filtered.databases.length > 0 && databases.length > 0) || filtered.otherDatabases.length > 0} {#if (filtered.databases.length > 0 && databases.length > 0) || filtered.otherDatabases.length > 0}
<div class="flex items-center mt-10"> <div class="flex items-center mt-10 space-x-2">
<h1 class="title lg:text-3xl pr-4">Databases</h1> <h1 class="title lg:text-3xl">Databases</h1>
<button <button class="btn btn-sm btn-primary" on:click={refreshStatusDatabases}
class="btn btn-sm btn-primary"
on:click={refreshStatusDatabases}
class:loading={loading.databases}
disabled={loading.databases}
>{noInitialStatus.databases ? 'Load Status' : 'Refresh Status'}</button >{noInitialStatus.databases ? 'Load Status' : 'Refresh Status'}</button
> >
{#if foundUnconfiguredDatabase}
<button
class="btn btn-sm"
class:loading={loading.databases}
disabled={loading.databases}
on:click={cleanupDatabases}>Cleanup Unconfigured Resources</button
>
{/if}
</div> </div>
{/if} {/if}
{#if filtered.databases.length > 0 && databases.length > 0} {#if filtered.databases.length > 0 && databases.length > 0}
@ -920,9 +971,9 @@
<span class="indicator-item badge bg-yellow-300 badge-sm" /> <span class="indicator-item badge bg-yellow-300 badge-sm" />
{:then} {:then}
{#if !noInitialStatus.databases} {#if !noInitialStatus.databases}
{#if loading.databases} {#if status[database.id] === 'loading'}
<span class="indicator-item badge bg-yellow-300 badge-sm" /> <span class="indicator-item badge bg-yellow-300 badge-sm" />
{:else if status[databases.id] === 'running'} {:else if status[database.id] === 'running'}
<span class="indicator-item badge bg-success badge-sm" /> <span class="indicator-item badge bg-success badge-sm" />
{:else} {:else}
<span class="indicator-item badge bg-error badge-sm" /> <span class="indicator-item badge bg-error badge-sm" />
@ -933,7 +984,7 @@
<DatabaseIcons type={database.type} isAbsolute={true} /> <DatabaseIcons type={database.type} isAbsolute={true} />
<div class="w-full flex flex-col"> <div class="w-full flex flex-col">
<div class="h-10"> <div class="h-10">
<h1 class="font-bold text-lg lg:text-xl truncate">{database.name}</h1> <h1 class="font-bold text-base truncate">{database.name}</h1>
<div class="h-10 text-xs"> <div class="h-10 text-xs">
{#if database?.version} {#if database?.version}
<h2 class="">{database?.version}</h2> <h2 class="">{database?.version}</h2>
@ -997,9 +1048,9 @@
<span class="indicator-item badge bg-yellow-300 badge-sm" /> <span class="indicator-item badge bg-yellow-300 badge-sm" />
{:then} {:then}
{#if !noInitialStatus.databases} {#if !noInitialStatus.databases}
{#if loading.databases} {#if status[database.id] === 'loading'}
<span class="indicator-item badge bg-yellow-300 badge-sm" /> <span class="indicator-item badge bg-yellow-300 badge-sm" />
{:else if status[databases.id] === 'running'} {:else if status[database.id] === 'running'}
<span class="indicator-item badge bg-success badge-sm" /> <span class="indicator-item badge bg-success badge-sm" />
{:else} {:else}
<span class="indicator-item badge bg-error badge-sm" /> <span class="indicator-item badge bg-error badge-sm" />
@ -1010,7 +1061,7 @@
<DatabaseIcons type={database.type} isAbsolute={true} /> <DatabaseIcons type={database.type} isAbsolute={true} />
<div class="w-full flex flex-col"> <div class="w-full flex flex-col">
<div class="h-10"> <div class="h-10">
<h1 class="font-bold text-lg lg:text-xl truncate">{database.name}</h1> <h1 class="font-bold text-base truncate">{database.name}</h1>
<div class="h-10 text-xs"> <div class="h-10 text-xs">
{#if database?.version} {#if database?.version}
<h2 class="">{database?.version}</h2> <h2 class="">{database?.version}</h2>
@ -1129,7 +1180,7 @@
</div> </div>
<div class="w-full flex flex-col"> <div class="w-full flex flex-col">
<div class="h-10"> <div class="h-10">
<h1 class="font-bold text-lg lg:text-xl truncate">{source.name}</h1> <h1 class="font-bold text-base truncate">{source.name}</h1>
{#if source.teams.length > 0 && source.teams[0]?.name} {#if source.teams.length > 0 && source.teams[0]?.name}
<div class="truncate text-xs">{source.teams[0]?.name}</div> <div class="truncate text-xs">{source.teams[0]?.name}</div>
{/if} {/if}
@ -1218,7 +1269,7 @@
</div> </div>
<div class="w-full flex flex-col"> <div class="w-full flex flex-col">
<div class="h-10"> <div class="h-10">
<h1 class="font-bold text-lg lg:text-xl truncate">{source.name}</h1> <h1 class="font-bold text-base truncate">{source.name}</h1>
{#if source.teams.length > 0 && source.teams[0]?.name} {#if source.teams.length > 0 && source.teams[0]?.name}
<div class="truncate text-xs">{source.teams[0]?.name}</div> <div class="truncate text-xs">{source.teams[0]?.name}</div>
{/if} {/if}
@ -1292,7 +1343,7 @@
{/if} {/if}
</div> </div>
<div class="w-full flex flex-col"> <div class="w-full flex flex-col">
<h1 class="font-bold text-lg lg:text-xl truncate">{destination.name}</h1> <h1 class="font-bold text-base truncate">{destination.name}</h1>
<div class="h-10 text-xs"> <div class="h-10 text-xs">
{#if $appSession.teamId === '0' && destination.remoteVerified === false && destination.remoteEngine} {#if $appSession.teamId === '0' && destination.remoteVerified === false && destination.remoteEngine}
<h2 class="text-red-500">Not verified yet</h2> <h2 class="text-red-500">Not verified yet</h2>
@ -1373,7 +1424,7 @@
{/if} {/if}
</div> </div>
<div class="w-full flex flex-col"> <div class="w-full flex flex-col">
<h1 class="font-bold text-lg lg:text-xl truncate">{destination.name}</h1> <h1 class="font-bold text-base truncate">{destination.name}</h1>
<div class="h-10 text-xs"> <div class="h-10 text-xs">
{#if $appSession.teamId === '0' && destination.remoteVerified === false && destination.remoteEngine} {#if $appSession.teamId === '0' && destination.remoteVerified === false && destination.remoteEngine}
<h2 class="text-red-500">Not verified yet</h2> <h2 class="text-red-500">Not verified yet</h2>

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

@ -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.11", "version": "3.10.12",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": "github:coollabsio/coolify", "repository": "github:coollabsio/coolify",
"scripts": { "scripts": {