mirror of
https://github.com/ershisan99/coolify.git
synced 2025-12-18 12:33:06 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d14b03eb1 | ||
|
|
04a5b1bd4f | ||
|
|
31b3f58b2c |
@@ -54,7 +54,7 @@ With Github integration
|
||||
- Twitter: [@andrasbacsai](https://twitter.com/andrasbacsai)
|
||||
- Telegram: [@andrasbacsai](https://t.me/andrasbacsai)
|
||||
- Email: [andras@coollabs.io](mailto:andras@coollabs.io)
|
||||
- Discord: [Invitation](https://discord.com/invite/bvS3WhR)
|
||||
- Discord: [Invitation](https://discord.gg/xhBCC7eGKw)
|
||||
|
||||
## Roadmap
|
||||
|
||||
|
||||
30
package.json
30
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "coolify",
|
||||
"description": "An open-source, hassle-free, self-hostable Heroku & Netlify alternative.",
|
||||
"version": "1.0.15",
|
||||
"version": "1.0.17",
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
"dev:docker:start": "docker-compose -f docker-compose-dev.yml up -d",
|
||||
@@ -14,27 +14,27 @@
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-node": "^1.0.0-next.20",
|
||||
"@sveltejs/kit": "1.0.0-next.107",
|
||||
"@sveltejs/adapter-node": "^1.0.0-next.24",
|
||||
"@sveltejs/kit": "1.0.0-next.113",
|
||||
"@types/dockerode": "^3.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "^4.23.0",
|
||||
"@typescript-eslint/parser": "^4.23.0",
|
||||
"autoprefixer": "^10.2.5",
|
||||
"cssnano": "^5.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^4.26.1",
|
||||
"@typescript-eslint/parser": "^4.26.1",
|
||||
"autoprefixer": "^10.2.6",
|
||||
"cssnano": "^5.0.5",
|
||||
"dotenv-extended": "^2.9.0",
|
||||
"eslint": "^7.26.0",
|
||||
"eslint": "^7.28.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-svelte3": "^3.2.0",
|
||||
"postcss": "^8.2.15",
|
||||
"postcss": "^8.3.0",
|
||||
"postcss-load-config": "^3.0.1",
|
||||
"prettier": "~2.3.0",
|
||||
"prettier": "~2.3.1",
|
||||
"prettier-plugin-svelte": "^2.3.0",
|
||||
"svelte": "^3.38.2",
|
||||
"svelte-preprocess": "^4.7.3",
|
||||
"tailwindcss": "2.2.0-canary.8",
|
||||
"tslib": "^2.2.0",
|
||||
"typescript": "^4.2.4",
|
||||
"vite": "^2.3.2"
|
||||
"typescript": "^4.3.2",
|
||||
"vite": "^2.3.6"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
@@ -44,14 +44,14 @@
|
||||
"compare-versions": "^3.6.0",
|
||||
"cookie": "^0.4.1",
|
||||
"cuid": "^2.1.8",
|
||||
"dayjs": "^1.10.4",
|
||||
"dayjs": "^1.10.5",
|
||||
"dockerode": "^3.3.0",
|
||||
"generate-password": "^1.6.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mongoose": "^5.12.9",
|
||||
"mongoose": "^5.12.13",
|
||||
"shelljs": "^0.8.4",
|
||||
"svelte-kit-cookie-session": "^0.4.3",
|
||||
"svelte-kit-cookie-session": "^1.0.6",
|
||||
"svelte-select": "^3.17.0",
|
||||
"unique-names-generator": "^4.5.0"
|
||||
}
|
||||
|
||||
755
pnpm-lock.yaml
generated
755
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -92,7 +92,9 @@ body {
|
||||
input {
|
||||
@apply text-sm rounded py-2 px-6 font-bold bg-warmGray-800 text-white transition duration-150 outline-none border border-transparent !important;
|
||||
}
|
||||
input:hover {
|
||||
input:hover,
|
||||
input:focus-visible,
|
||||
input:focus {
|
||||
@apply bg-warmGray-700 !important;
|
||||
}
|
||||
textarea {
|
||||
@@ -107,6 +109,7 @@ select {
|
||||
select:hover {
|
||||
@apply bg-warmGray-700 !important;
|
||||
}
|
||||
|
||||
label {
|
||||
@apply text-left text-base font-bold text-warmGray-400 !important;
|
||||
}
|
||||
@@ -116,6 +119,11 @@ button {
|
||||
.button {
|
||||
@apply rounded text-sm font-bold transition-all duration-100 !important;
|
||||
}
|
||||
|
||||
.button:focus-visible,
|
||||
.button:focus {
|
||||
@apply bg-warmGray-700 !important;
|
||||
}
|
||||
.h-271 {
|
||||
min-height: 271px !important;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
<script>
|
||||
import { application } from '$store';
|
||||
import { VITE_GITHUB_APP_NAME } from '$lib/consts';
|
||||
import { application, isPullRequestPermissionsGranted } from '$store';
|
||||
import { onMount } from 'svelte';
|
||||
import TooltipInfo from '$components/TooltipInfo.svelte';
|
||||
import { request } from '$lib/request';
|
||||
import { page, session } from '$app/stores';
|
||||
import { browser } from '$app/env';
|
||||
|
||||
let domainInput;
|
||||
let loading = {
|
||||
previewDeployment: false
|
||||
};
|
||||
let howToActivate = false;
|
||||
const buildpacks = {
|
||||
static: {
|
||||
port: {
|
||||
@@ -110,6 +119,45 @@
|
||||
custom: true
|
||||
}
|
||||
};
|
||||
async function setPreviewDeployment() {
|
||||
if ($application.general.isPreviewDeploymentEnabled) {
|
||||
const result = window.confirm(
|
||||
"Are you sure? It will delete all PR deployments - it's NOT reversible!"
|
||||
);
|
||||
if (result) {
|
||||
loading.previewDeployment = true;
|
||||
$application.general.isPreviewDeploymentEnabled =
|
||||
!$application.general.isPreviewDeploymentEnabled;
|
||||
if ($page.path !== '/application/new') {
|
||||
const config = await request(`/api/v1/application/config/previewDeployment`, $session, {
|
||||
body: {
|
||||
name: $application.repository.name,
|
||||
organization: $application.repository.organization,
|
||||
branch: $application.repository.branch,
|
||||
isPreviewDeploymentEnabled: $application.general.isPreviewDeploymentEnabled
|
||||
}
|
||||
});
|
||||
}
|
||||
loading.previewDeployment = false;
|
||||
}
|
||||
} else {
|
||||
loading.previewDeployment = true;
|
||||
$application.general.isPreviewDeploymentEnabled =
|
||||
!$application.general.isPreviewDeploymentEnabled;
|
||||
$application.general.pullRequest = 0;
|
||||
if ($page.path !== '/application/new') {
|
||||
const config = await request(`/api/v1/application/config/previewDeployment`, $session, {
|
||||
body: {
|
||||
name: $application.repository.name,
|
||||
organization: $application.repository.organization,
|
||||
branch: $application.repository.branch,
|
||||
isPreviewDeploymentEnabled: $application.general.isPreviewDeploymentEnabled
|
||||
}
|
||||
});
|
||||
}
|
||||
loading.previewDeployment = false;
|
||||
}
|
||||
}
|
||||
function selectBuildPack(event) {
|
||||
if (event.target.innerText === 'React/Preact') {
|
||||
$application.build.pack = 'react';
|
||||
@@ -117,6 +165,34 @@
|
||||
$application.build.pack = event.target.innerText.replace(/\./g, '').toLowerCase();
|
||||
}
|
||||
}
|
||||
async function openGithub() {
|
||||
if (browser) {
|
||||
const config = await request(`https://api.github.com/apps/${VITE_GITHUB_APP_NAME}`, $session);
|
||||
|
||||
let url = `https://github.com/settings/apps/${VITE_GITHUB_APP_NAME}/permissions`;
|
||||
if (config.owner.type === 'Organization') {
|
||||
url = `https://github.com/organizations/${config.owner.login}/settings/apps/${VITE_GITHUB_APP_NAME}/permissions`;
|
||||
}
|
||||
|
||||
const left = screen.width / 2 - 1020 / 2;
|
||||
const top = screen.height / 2 - 618 / 2;
|
||||
const newWindow = open(
|
||||
url,
|
||||
'Permission Update',
|
||||
'resizable=1, scrollbars=1, fullscreen=1, height=1000, width=1220,top=' +
|
||||
top +
|
||||
', left=' +
|
||||
left +
|
||||
', toolbar=0, menubar=0, status=0'
|
||||
);
|
||||
const timer = setInterval(async () => {
|
||||
if (newWindow?.closed) {
|
||||
clearInterval(timer);
|
||||
location.reload();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
onMount(() => {
|
||||
if (!$application.publish.domain) domainInput.focus();
|
||||
});
|
||||
@@ -235,6 +311,127 @@
|
||||
</div>
|
||||
<div class="text-2xl font-bold border-gradient w-52">General settings</div>
|
||||
<div class="grid grid-cols-1 max-w-2xl md:mx-auto mx-6 justify-center items-center pt-10">
|
||||
<div>
|
||||
<ul class="divide-y divide-warmGray-800">
|
||||
<li class="pb-6 flex items-center justify-between">
|
||||
<div class="flex flex-col">
|
||||
<p class="text-base font-bold text-warmGray-100">Preview deployments</p>
|
||||
<p class="text-sm font-medium text-warmGray-400">
|
||||
PR's will be deployed so you could review them easily
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if $isPullRequestPermissionsGranted}
|
||||
<div
|
||||
class="relative"
|
||||
class:animate-wiggle={loading.previewDeployment}
|
||||
class:opacity-25={loading.previewDeployment}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
disabled={loading.previewDeployment}
|
||||
on:click={setPreviewDeployment}
|
||||
aria-pressed="false"
|
||||
class="relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200"
|
||||
class:bg-green-600={$application.general.isPreviewDeploymentEnabled}
|
||||
class:bg-warmGray-700={!$application.general.isPreviewDeploymentEnabled}
|
||||
class:cursor-not-allowed={loading.previewDeployment}
|
||||
>
|
||||
<span class="sr-only">Use setting</span>
|
||||
<span
|
||||
class="pointer-events-none relative inline-block h-5 w-5 rounded-full bg-white shadow transform transition ease-in-out duration-200"
|
||||
class:translate-x-5={$application.general.isPreviewDeploymentEnabled}
|
||||
class:translate-x-0={!$application.general.isPreviewDeploymentEnabled}
|
||||
>
|
||||
<span
|
||||
class=" ease-in duration-200 absolute inset-0 h-full w-full flex items-center justify-center transition-opacity"
|
||||
class:opacity-0={$application.general.isPreviewDeploymentEnabled}
|
||||
class:opacity-100={!$application.general.isPreviewDeploymentEnabled}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg class="bg-white h-3 w-3 text-red-600" fill="none" viewBox="0 0 12 12">
|
||||
<path
|
||||
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="ease-out duration-100 absolute inset-0 h-full w-full flex items-center justify-center transition-opacity"
|
||||
aria-hidden="true"
|
||||
class:opacity-100={$application.general.isPreviewDeploymentEnabled}
|
||||
class:opacity-0={!$application.general.isPreviewDeploymentEnabled}
|
||||
>
|
||||
<svg
|
||||
class="bg-white h-3 w-3 text-green-600"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 12 12"
|
||||
>
|
||||
<path
|
||||
d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{#if loading.previewDeployment}
|
||||
<div class="absolute left-0 bottom-0 -mb-4 -ml-2 text-xs font-bold">{$application.general.isPreviewDeploymentEnabled ? 'Enabling...' : 'Disabling...' }</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="relative">
|
||||
{#if !howToActivate}
|
||||
<button
|
||||
class="button py-2 px-2 bg-warmGray-800 hover:bg-warmGray-700 text-white"
|
||||
on:click={() => (howToActivate = !howToActivate)}>How to active this?</button
|
||||
>
|
||||
{:else}
|
||||
<button
|
||||
class="button py-2 px-2 bg-green-600 hover:bg-green-500 text-white"
|
||||
on:click={openGithub}>Open Github</button
|
||||
>
|
||||
{/if}
|
||||
{#if howToActivate}
|
||||
<div class="absolute right-0 w-64 z-10">
|
||||
<div class="bg-warmGray-800 p-4 my-2 rounded text-white">
|
||||
<div
|
||||
class="absolute right-0 top-0 p-2 my-3 mx-1 text-xs font-bold cursor-pointer hover:bg-warmGray-700"
|
||||
on:click={() => (howToActivate = false)}
|
||||
>
|
||||
X
|
||||
</div>
|
||||
<p class="text-sm font-medium text-warmGray-400">
|
||||
You need to add <span class="text-white">two new permissions</span> to your GitHub
|
||||
App:
|
||||
</p>
|
||||
<br />
|
||||
<p class="text-sm font-medium text-warmGray-400">
|
||||
1. In <span class="text-white">Repository permissions</span>, add
|
||||
<span class="text-white">Read-only</span>
|
||||
access to <span class="text-white">Pull requests</span>.
|
||||
</p>
|
||||
<br />
|
||||
<p class="text-sm font-medium text-warmGray-400">
|
||||
2. In <span class="text-white">Subscribe to events</span> section,
|
||||
<span class="text-white"> check Pull request</span> field.
|
||||
</p>
|
||||
<br />
|
||||
<p class="text-sm font-medium text-warmGray-400">
|
||||
3. You <span class="text-white">receive an email</span> where you need to
|
||||
<span class="text-white">accept the new permissions</span>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-flow-col gap-2 items-center pb-6">
|
||||
<div class="grid grid-flow-row">
|
||||
<label for="Domain" class="">Domain</label>
|
||||
@@ -298,11 +495,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="text-2xl font-bold w-40 border-gradient"
|
||||
>
|
||||
Commands
|
||||
</div>
|
||||
<div class="text-2xl font-bold w-40 border-gradient">Commands</div>
|
||||
<div class=" max-w-2xl md:mx-auto mx-6 justify-center items-center pt-10 pb-32">
|
||||
<div class="grid grid-flow-col gap-2 items-center">
|
||||
<div class="grid grid-flow-row">
|
||||
@@ -320,17 +513,17 @@
|
||||
placeholder="main"
|
||||
/>
|
||||
<label for="ApplicationInstance"
|
||||
>Application Instance<TooltipInfo
|
||||
label="The instance name (the main function name. See gunicorn.org for more details)"
|
||||
/>
|
||||
</label>
|
||||
>Application Instance<TooltipInfo
|
||||
label="The instance name (the main function name. See gunicorn.org for more details)"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<input
|
||||
class="mb-6"
|
||||
id="ApplicationInstance"
|
||||
bind:value={$application.build.command.python.instance}
|
||||
placeholder="app"
|
||||
/>
|
||||
<input
|
||||
class="mb-6"
|
||||
id="ApplicationInstance"
|
||||
bind:value={$application.build.command.python.instance}
|
||||
placeholder="app"
|
||||
/>
|
||||
{:else}
|
||||
<label
|
||||
for="installCommand"
|
||||
|
||||
110
src/components/Application/ActiveTab/PullRequests.svelte
Normal file
110
src/components/Application/ActiveTab/PullRequests.svelte
Normal file
@@ -0,0 +1,110 @@
|
||||
<script>
|
||||
import { browser } from '$app/env';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { session } from '$app/stores';
|
||||
import { request } from '$lib/request';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { application, prApplication } from '$store';
|
||||
let loadPRDeployments = null;
|
||||
onMount(async () => {
|
||||
await getPRDeployments();
|
||||
loadPRDeployments = setInterval(async () => {
|
||||
await getPRDeployments();
|
||||
}, 1000);
|
||||
});
|
||||
onDestroy(() => {
|
||||
clearInterval(loadPRDeployments);
|
||||
});
|
||||
async function getPRDeployments() {
|
||||
const { configuration } = await request(`/api/v1/application/config`, $session, {
|
||||
body: {
|
||||
name: $application.repository.name,
|
||||
organization: $application.repository.organization,
|
||||
branch: $application.repository.branch
|
||||
}
|
||||
});
|
||||
$prApplication = configuration.filter((c) => c.general.pullRequest !== 0);
|
||||
}
|
||||
async function removePR(prConfiguration) {
|
||||
const result = window.confirm("Are you sure? It's NOT reversible!");
|
||||
if (result) {
|
||||
await request(`/api/v1/application/remove`, $session, {
|
||||
body: {
|
||||
organization: prConfiguration.repository.organization,
|
||||
name: prConfiguration.repository.name,
|
||||
branch: prConfiguration.repository.branch,
|
||||
domain: prConfiguration.publish.domain
|
||||
}
|
||||
});
|
||||
|
||||
browser && toast.push('PR deployment removed.');
|
||||
const { configuration } = await request(`/api/v1/application/config`, $session, {
|
||||
body: {
|
||||
name: prConfiguration.repository.name,
|
||||
organization: prConfiguration.repository.organization,
|
||||
branch: prConfiguration.repository.branch
|
||||
}
|
||||
});
|
||||
$prApplication = configuration.filter((c) => c.general.pullRequest !== 0);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="text-2xl font-bold border-gradient w-48">Pull Requests</div>
|
||||
<div class="text-center pt-4">
|
||||
{#if $prApplication.length > 0}
|
||||
<div class="py-4 ">
|
||||
{#each $prApplication as pr}
|
||||
<div class="flex space-x-4 justify-center items-center">
|
||||
<div class="text-left font-bold tracking-tight ">
|
||||
{pr.publish.domain}
|
||||
</div>
|
||||
<a
|
||||
target="_blank"
|
||||
class="icon mx-2 "
|
||||
href={'https://' + pr.publish.domain + pr.publish.path}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
||||
/>
|
||||
</svg></a
|
||||
>
|
||||
<!-- <div class="flex-1" /> -->
|
||||
<button
|
||||
class="icon hover:text-red-500 hover:bg-warmGray-800"
|
||||
on:click={() => removePR(pr)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="font-bold text-center">No PR deployments found</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -2,7 +2,14 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { request } from '$lib/request';
|
||||
import { session } from '$app/stores';
|
||||
import { githubRepositories, application, githubInstallations } from '$store';
|
||||
import {
|
||||
githubRepositories,
|
||||
application,
|
||||
githubInstallations,
|
||||
prApplication,
|
||||
initConf,
|
||||
isPullRequestPermissionsGranted
|
||||
} from '$store';
|
||||
|
||||
import { fade } from 'svelte/transition';
|
||||
import Loading from '$components/Loading.svelte';
|
||||
@@ -18,6 +25,7 @@
|
||||
};
|
||||
let branches = [];
|
||||
let relogin = false;
|
||||
let permissions = {};
|
||||
function dashify(str: string, options?: any) {
|
||||
if (typeof str !== 'string') return str;
|
||||
return str
|
||||
@@ -110,8 +118,17 @@
|
||||
$session
|
||||
);
|
||||
loading.branches = false;
|
||||
await loadPermissions();
|
||||
}
|
||||
async function loadPermissions() {
|
||||
const config = await request(
|
||||
`https://api.github.com/apps/${import.meta.env.VITE_GITHUB_APP_NAME}`,
|
||||
$session
|
||||
);
|
||||
if (config.permissions['pull_requests'] && config.events.includes('pull_request')) {
|
||||
$isPullRequestPermissionsGranted = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function modifyGithubAppConfig() {
|
||||
if (browser) {
|
||||
const left = screen.width / 2 - 1020 / 2;
|
||||
@@ -128,20 +145,23 @@
|
||||
', toolbar=0, menubar=0, status=0'
|
||||
);
|
||||
const timer = setInterval(async () => {
|
||||
if (newWindow.closed) {
|
||||
if (newWindow?.closed) {
|
||||
clearInterval(timer);
|
||||
loading.github = true;
|
||||
|
||||
if ($application.repository.name) {
|
||||
try {
|
||||
const config = await request(`/api/v1/application/config`, $session, {
|
||||
const { configuration } = await request(`/api/v1/application/config`, $session, {
|
||||
body: {
|
||||
name: $application.repository.name,
|
||||
organization: $application.repository.organization,
|
||||
branch: $application.repository.branch
|
||||
}
|
||||
});
|
||||
$application = { ...config };
|
||||
|
||||
$prApplication = configuration.filter((c) => c.general.pullRequest !== 0);
|
||||
$application = configuration.find((c) => c.general.pullRequest === 0);
|
||||
$initConf = JSON.parse(JSON.stringify($application));
|
||||
} catch (error) {
|
||||
browser && goto('/dashboard/applications', { replaceState: true });
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
', toolbar=0, menubar=0, status=0'
|
||||
);
|
||||
const timer = setInterval(() => {
|
||||
if (newWindow.closed) {
|
||||
if (newWindow?.closed) {
|
||||
clearInterval(timer);
|
||||
location.reload()
|
||||
}
|
||||
|
||||
@@ -8,17 +8,23 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/env';
|
||||
async function removeApplication() {
|
||||
await request(`/api/v1/application/remove`, $session, {
|
||||
body: {
|
||||
organization: $application.repository.organization,
|
||||
name: $application.repository.name,
|
||||
branch: $application.repository.branch
|
||||
}
|
||||
});
|
||||
const result = window.confirm(
|
||||
"Are you sure? It will delete all deployments, including PR's - it's NOT reversible!"
|
||||
);
|
||||
if (result) {
|
||||
await request(`/api/v1/application/remove`, $session, {
|
||||
body: {
|
||||
organization: $application.repository.organization,
|
||||
name: $application.repository.name,
|
||||
branch: $application.repository.branch,
|
||||
domain: $application.publish.domain
|
||||
}
|
||||
});
|
||||
|
||||
browser && toast.push('Application removed.');
|
||||
$application = JSON.parse(JSON.stringify(initialApplication));
|
||||
browser && goto(`/dashboard/applications`, { replaceState: true });
|
||||
browser && toast.push('Application removed.');
|
||||
$application = JSON.parse(JSON.stringify(initialApplication));
|
||||
browser && goto(`/dashboard/applications`, { replaceState: true });
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
@@ -50,6 +56,7 @@
|
||||
// toast.push(error.error || error || 'Ooops something went wrong.');
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<nav class="flex text-white justify-end items-center m-4 fixed right-0 top-0 space-x-4 z-50">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import templates from '$lib/api/applications/packs/templates';
|
||||
import { application, dashboard } from '$store';
|
||||
import { application, dashboard, initConf, prApplication } from '$store';
|
||||
import General from '$components/Application/ActiveTab/General.svelte';
|
||||
import Secrets from '$components/Application/ActiveTab/Secrets.svelte';
|
||||
import Loading from '$components/Loading.svelte';
|
||||
@@ -9,17 +9,18 @@
|
||||
import { page, session } from '$app/stores';
|
||||
import { request } from '$lib/request';
|
||||
import { browser } from '$app/env';
|
||||
import PullRequests from './ActiveTab/PullRequests.svelte';
|
||||
|
||||
let activeTab = {
|
||||
general: true,
|
||||
buildStep: false,
|
||||
secrets: false
|
||||
secrets: false,
|
||||
pullRequests: false
|
||||
};
|
||||
function activateTab(tab) {
|
||||
if (activeTab.hasOwnProperty(tab)) {
|
||||
activeTab = {
|
||||
general: false,
|
||||
buildStep: false,
|
||||
pullRequests: false,
|
||||
secrets: false
|
||||
};
|
||||
activeTab[tab] = true;
|
||||
@@ -48,16 +49,7 @@
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ($page.path !== '/application/new') {
|
||||
const config = await request(`/api/v1/application/config`, $session, {
|
||||
body: {
|
||||
name: $application.repository.name,
|
||||
organization: $application.repository.organization,
|
||||
branch: $application.repository.branch
|
||||
}
|
||||
});
|
||||
$application = { ...config };
|
||||
} else {
|
||||
if ($page.path === '/application/new') {
|
||||
try {
|
||||
const dir = await request(
|
||||
`https://api.github.com/repos/${$application.repository.organization}/${$application.repository.name}/contents/?ref=${$application.repository.branch}`,
|
||||
@@ -103,17 +95,18 @@
|
||||
$application.build.pack = 'rust';
|
||||
browser && toast.push(`Rust language detected. Default values set.`);
|
||||
} else if (requirementsTXT) {
|
||||
$application.build.pack = 'python'
|
||||
$application.build.pack = 'python';
|
||||
browser && toast.push('Python language detected. Default values set.');
|
||||
} else if (Dockerfile) {
|
||||
$application.build.pack = 'docker';
|
||||
browser && toast.push('Custom Dockerfile found. Build pack set to docker.');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Nothing detected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
{#await load()}
|
||||
@@ -135,6 +128,15 @@
|
||||
>
|
||||
Secrets
|
||||
</div>
|
||||
{#if $application.general.isPreviewDeploymentEnabled}
|
||||
<div
|
||||
on:click={() => activateTab('pullRequests')}
|
||||
class:text-green-500={activeTab.pullRequests}
|
||||
class="px-3 py-2 cursor-pointer hover:bg-warmGray-700 rounded-lg transition duration-100"
|
||||
>
|
||||
Pull Requests
|
||||
</div>
|
||||
{/if}
|
||||
</nav>
|
||||
</div>
|
||||
<div class="max-w-4xl mx-auto">
|
||||
@@ -143,6 +145,8 @@
|
||||
<General />
|
||||
{:else if activeTab.secrets}
|
||||
<Secrets />
|
||||
{:else if activeTab.pullRequests && $page.path !== '/application/new'}
|
||||
<PullRequests />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
5
src/global.d.ts
vendored
5
src/global.d.ts
vendored
@@ -36,6 +36,8 @@ export type Application = {
|
||||
deployId: string;
|
||||
nickname: string;
|
||||
workdir: string;
|
||||
isPreviewDeploymentEnabled: boolean;
|
||||
pullRequest: number;
|
||||
};
|
||||
build: {
|
||||
pack: string;
|
||||
@@ -47,8 +49,7 @@ export type Application = {
|
||||
python: {
|
||||
module?: string;
|
||||
instance?: string;
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
container: {
|
||||
name: string;
|
||||
|
||||
@@ -31,11 +31,9 @@ async function connectMongoDB() {
|
||||
);
|
||||
}
|
||||
console.log('Connected to mongodb.');
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
(async () => {
|
||||
@@ -48,7 +46,7 @@ async function connectMongoDB() {
|
||||
try {
|
||||
await cleanupStuckedDeploymentsInDB();
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
console.log(error);
|
||||
}
|
||||
try {
|
||||
const dockerServices = await docker.engine.listServices();
|
||||
@@ -76,26 +74,42 @@ async function connectMongoDB() {
|
||||
).values()
|
||||
];
|
||||
for (const application of applications) {
|
||||
await Configuration.findOneAndUpdate({
|
||||
'repository.name': application.configuration.repository.name,
|
||||
'repository.organization': application.configuration.repository.organization,
|
||||
'repository.branch': application.configuration.repository.branch,
|
||||
}, {
|
||||
...application.configuration
|
||||
}, { upsert: true })
|
||||
await Configuration.findOneAndUpdate(
|
||||
{
|
||||
'repository.name': application.configuration.repository.name,
|
||||
'repository.organization': application.configuration.repository.organization,
|
||||
'repository.branch': application.configuration.repository.branch,
|
||||
'publish.domain': application.configuration.publish.domain
|
||||
},
|
||||
{
|
||||
...application.configuration
|
||||
},
|
||||
{ upsert: true, new: true }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
console.log(error);
|
||||
}
|
||||
})()
|
||||
})();
|
||||
|
||||
|
||||
export async function handle({ request, render }) {
|
||||
export async function handle({ request, resolve }) {
|
||||
const { SECRETS_ENCRYPTION_KEY } = process.env;
|
||||
const session = initializeSession(request.headers, {
|
||||
secret: SECRETS_ENCRYPTION_KEY,
|
||||
cookie: { path: '/' }
|
||||
});
|
||||
let session;
|
||||
try {
|
||||
session = initializeSession(request.headers, {
|
||||
secret: SECRETS_ENCRYPTION_KEY,
|
||||
cookie: { path: '/' }
|
||||
});
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 302,
|
||||
headers: {
|
||||
'set-cookie': 'kit.session=deleted;path=/;expires=Wed, 21 Oct 2015 07:28:00 GMT',
|
||||
location: '/'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
request.locals.session = session;
|
||||
if (session?.data?.coolToken) {
|
||||
try {
|
||||
@@ -105,7 +119,7 @@ export async function handle({ request, render }) {
|
||||
request.locals.session.destroy = true;
|
||||
}
|
||||
}
|
||||
const response = await render(request);
|
||||
const response = await resolve(request);
|
||||
if (!session['set-cookie']) {
|
||||
if (!session?.data?.coolToken && !publicPages.includes(request.path)) {
|
||||
return {
|
||||
|
||||
@@ -11,7 +11,8 @@ export async function deleteSameDeployments(configuration) {
|
||||
const running = JSON.parse(s.Spec.Labels.configuration);
|
||||
if (
|
||||
running.repository.id === configuration.repository.id &&
|
||||
running.repository.branch === configuration.repository.branch
|
||||
running.repository.branch === configuration.repository.branch &&
|
||||
running.publish.domain === configuration.publish.domain
|
||||
) {
|
||||
await execShellAsync(`docker stack rm ${s.Spec.Labels['com.docker.stack.namespace']}`);
|
||||
}
|
||||
@@ -54,7 +55,6 @@ export async function purgeImagesContainers(configuration, deleteAll = false) {
|
||||
.split('\n');
|
||||
if (IDsToDelete.length > 1) await execShellAsync(`docker rmi -f ${IDsToDelete.join(' ')}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { execShellAsync } from '../common';
|
||||
export default async function (configuration) {
|
||||
try {
|
||||
const { GITHUB_APP_PRIVATE_KEY } = process.env;
|
||||
const { workdir } = configuration.general;
|
||||
const { workdir, isPreviewDeploymentEnabled, pullRequest } = configuration.general;
|
||||
const { organization, name, branch } = configuration.repository;
|
||||
const github = configuration.github;
|
||||
if (!github.installation.id || !github.app.id) {
|
||||
@@ -37,8 +37,14 @@ export default async function (configuration) {
|
||||
await execShellAsync(
|
||||
`mkdir -p ${workdir} && git clone -q -b ${branch} https://x-access-token:${token}@github.com/${organization}/${name}.git ${workdir}/`
|
||||
);
|
||||
|
||||
if (isPreviewDeploymentEnabled && pullRequest && pullRequest !== 0) {
|
||||
await execShellAsync(
|
||||
`cd ${workdir} && git fetch origin pull/${pullRequest}/head:pull_${pullRequest} && git checkout pull_${pullRequest}`
|
||||
);
|
||||
}
|
||||
configuration.build.container.tag = (
|
||||
await execShellAsync(`cd ${configuration.general.workdir}/ && git rev-parse HEAD`)
|
||||
await execShellAsync(`cd ${workdir}/ && git rev-parse HEAD`)
|
||||
)
|
||||
.replace('\n', '')
|
||||
.slice(0, 7);
|
||||
|
||||
@@ -10,9 +10,8 @@ function getUniq() {
|
||||
}
|
||||
|
||||
export function setDefaultConfiguration(configuration) {
|
||||
const nickname = getUniq();
|
||||
const nickname = configuration.general.nickname || getUniq();
|
||||
const deployId = cuid();
|
||||
|
||||
const shaBase = JSON.stringify({ repository: configuration.repository });
|
||||
const sha256 = crypto.createHash('sha256').update(shaBase).digest('hex');
|
||||
|
||||
@@ -21,12 +20,17 @@ export function setDefaultConfiguration(configuration) {
|
||||
configuration.general.nickname = nickname;
|
||||
configuration.general.deployId = deployId;
|
||||
configuration.general.workdir = `/tmp/${deployId}`;
|
||||
|
||||
if (configuration.general.isPreviewDeploymentEnabled && configuration.general.pullRequest !== 0) {
|
||||
configuration.build.container.name = `pr${configuration.general.pullRequest}-${sha256.slice(
|
||||
0,
|
||||
8
|
||||
)}`;
|
||||
configuration.publish.domain = `pr${configuration.general.pullRequest}.${configuration.publish.domain}`;
|
||||
}
|
||||
if (!configuration.publish.path) configuration.publish.path = '/';
|
||||
if (!configuration.publish.port) {
|
||||
if (
|
||||
configuration.build.pack === 'nodejs' ||
|
||||
configuration.build.pack === 'vuejs' ||
|
||||
configuration.build.pack === 'nuxtjs' ||
|
||||
configuration.build.pack === 'rust' ||
|
||||
configuration.build.pack === 'nextjs' ||
|
||||
@@ -58,11 +62,13 @@ export function setDefaultConfiguration(configuration) {
|
||||
configuration.build.pack === 'nextjs' ||
|
||||
configuration.build.pack === 'nestjs'
|
||||
) {
|
||||
if (!configuration.build.command.start) configuration.build.command.start = 'yarn start'
|
||||
if (!configuration.build.command.start) configuration.build.command.start = 'yarn start';
|
||||
}
|
||||
if (configuration.build.pack === 'python') {
|
||||
if (!configuration.build.command.python.module) configuration.build.command.python.module = 'main'
|
||||
if (!configuration.build.command.python.instance) configuration.build.command.python.instance = 'app'
|
||||
if (!configuration.build.command.python.module)
|
||||
configuration.build.command.python.module = 'main';
|
||||
if (!configuration.build.command.python.instance)
|
||||
configuration.build.command.python.instance = 'app';
|
||||
}
|
||||
|
||||
configuration.build.container.baseSHA = crypto
|
||||
@@ -74,13 +80,17 @@ export function setDefaultConfiguration(configuration) {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
export async function precheckDeployment({ services, configuration }) {
|
||||
export async function precheckDeployment(configuration) {
|
||||
const services = (await docker.engine.listServices()).filter(
|
||||
(r) =>
|
||||
r.Spec.Labels.managedBy === 'coolify' &&
|
||||
r.Spec.Labels.type === 'application' &&
|
||||
JSON.parse(r.Spec.Labels.configuration).publish.domain === configuration.publish.domain
|
||||
);
|
||||
let foundService = false;
|
||||
let configChanged = false;
|
||||
let imageChanged = false;
|
||||
|
||||
let forceUpdate = false;
|
||||
|
||||
for (const service of services) {
|
||||
const running = JSON.parse(service.Spec.Labels.configuration);
|
||||
if (running) {
|
||||
@@ -88,6 +98,7 @@ export async function precheckDeployment({ services, configuration }) {
|
||||
running.repository.id === configuration.repository.id &&
|
||||
running.repository.branch === configuration.repository.branch
|
||||
) {
|
||||
foundService = true;
|
||||
// Base service configuration changed
|
||||
if (
|
||||
!running.build.container.baseSHA ||
|
||||
@@ -107,8 +118,32 @@ export async function precheckDeployment({ services, configuration }) {
|
||||
(n) =>
|
||||
n.DesiredState !== 'Running' && n.Image.split(':')[1] === running.build.container.tag
|
||||
);
|
||||
if (isError.length > 0) forceUpdate = true;
|
||||
foundService = true;
|
||||
if (isError.length > 0) {
|
||||
forceUpdate = true;
|
||||
}
|
||||
|
||||
const compareObjects = (a, b) => {
|
||||
if (a === b) return true;
|
||||
|
||||
if (typeof a != 'object' || typeof b != 'object' || a == null || b == null) return false;
|
||||
|
||||
const keysA = Object.keys(a),
|
||||
keysB = Object.keys(b);
|
||||
|
||||
if (keysA.length != keysB.length) return false;
|
||||
|
||||
for (const key of keysA) {
|
||||
if (!keysB.includes(key)) return false;
|
||||
|
||||
if (typeof a[key] === 'function' || typeof b[key] === 'function') {
|
||||
if (a[key].toString() != b[key].toString()) return false;
|
||||
} else {
|
||||
if (!compareObjects(a[key], b[key])) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const runningWithoutContainer = JSON.parse(JSON.stringify(running));
|
||||
delete runningWithoutContainer.build.container;
|
||||
@@ -118,16 +153,23 @@ export async function precheckDeployment({ services, configuration }) {
|
||||
|
||||
// If only the configuration changed
|
||||
if (
|
||||
JSON.stringify(runningWithoutContainer.build) !==
|
||||
JSON.stringify(configurationWithoutContainer.build) ||
|
||||
JSON.stringify(runningWithoutContainer.publish) !==
|
||||
JSON.stringify(configurationWithoutContainer.publish)
|
||||
)
|
||||
!compareObjects(runningWithoutContainer.build, configurationWithoutContainer.build) ||
|
||||
!compareObjects(runningWithoutContainer.publish, configurationWithoutContainer.publish) ||
|
||||
runningWithoutContainer.general.isPreviewDeploymentEnabled !==
|
||||
configurationWithoutContainer.general.isPreviewDeploymentEnabled
|
||||
) {
|
||||
configChanged = true;
|
||||
}
|
||||
|
||||
// If only the image changed
|
||||
if (running.build.container.tag !== configuration.build.container.tag) imageChanged = true;
|
||||
// If build pack changed, forceUpdate the service
|
||||
if (running.build.pack !== configuration.build.pack) forceUpdate = true;
|
||||
if (
|
||||
configuration.general.isPreviewDeploymentEnabled &&
|
||||
configuration.general.pullRequest !== 0
|
||||
)
|
||||
forceUpdate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,28 +186,9 @@ export async function precheckDeployment({ services, configuration }) {
|
||||
}
|
||||
|
||||
export async function updateServiceLabels(configuration) {
|
||||
// In case of any failure during deployment, still update the current configuration.
|
||||
const services = (await docker.engine.listServices()).filter(
|
||||
(r) => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application'
|
||||
return await execShellAsync(
|
||||
`docker service update --label-add configuration='${JSON.stringify(configuration)}' ${
|
||||
configuration.build.container.name
|
||||
}_${configuration.build.container.name}`
|
||||
);
|
||||
const found = services.find((s) => {
|
||||
const config = JSON.parse(s.Spec.Labels.configuration);
|
||||
if (
|
||||
config.repository.id === configuration.repository.id &&
|
||||
config.repository.branch === configuration.repository.branch
|
||||
) {
|
||||
return config;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
if (found) {
|
||||
const { ID } = found;
|
||||
const Labels = { ...JSON.parse(found.Spec.Labels.configuration), ...configuration };
|
||||
await execShellAsync(
|
||||
`docker service update --label-add configuration='${JSON.stringify(
|
||||
Labels
|
||||
)}' --label-add com.docker.stack.image='${configuration.build.container.name}:${configuration.build.container.tag
|
||||
}' ${ID}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { docker } from '$lib/api/docker';
|
||||
import { saveAppLog } from './logging';
|
||||
import { promises as fs } from 'fs';
|
||||
import { deleteSameDeployments } from './cleanup';
|
||||
import { deleteSameDeployments, purgeImagesContainers } from './cleanup';
|
||||
import yaml from 'js-yaml';
|
||||
import { execShellAsync } from '../common';
|
||||
import { delay, execShellAsync } from '../common';
|
||||
|
||||
export default async function (configuration, imageChanged) {
|
||||
const generateEnvs = {};
|
||||
@@ -11,6 +11,7 @@ export default async function (configuration, imageChanged) {
|
||||
generateEnvs[secret.name] = secret.value;
|
||||
}
|
||||
const containerName = configuration.build.container.name;
|
||||
const containerTag = configuration.build.container.tag;
|
||||
|
||||
// Only save SHA256 of it in the configuration label
|
||||
const baseServiceConfiguration = configuration.baseServiceConfiguration;
|
||||
@@ -20,7 +21,7 @@ export default async function (configuration, imageChanged) {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[containerName]: {
|
||||
image: `${configuration.build.container.name}:${configuration.build.container.tag}`,
|
||||
image: `${containerName}:${containerTag}`,
|
||||
networks: [`${docker.network}`],
|
||||
environment: generateEnvs,
|
||||
deploy: {
|
||||
@@ -31,22 +32,18 @@ export default async function (configuration, imageChanged) {
|
||||
'configuration=' + JSON.stringify(configuration),
|
||||
'traefik.enable=true',
|
||||
'traefik.http.services.' +
|
||||
configuration.build.container.name +
|
||||
containerName +
|
||||
`.loadbalancer.server.port=${configuration.publish.port}`,
|
||||
'traefik.http.routers.' + configuration.build.container.name + '.entrypoints=websecure',
|
||||
'traefik.http.routers.' + containerName + '.entrypoints=websecure',
|
||||
'traefik.http.routers.' +
|
||||
configuration.build.container.name +
|
||||
containerName +
|
||||
'.rule=Host(`' +
|
||||
configuration.publish.domain +
|
||||
'`) && PathPrefix(`' +
|
||||
configuration.publish.path +
|
||||
'`)',
|
||||
'traefik.http.routers.' +
|
||||
configuration.build.container.name +
|
||||
'.tls.certresolver=letsencrypt',
|
||||
'traefik.http.routers.' +
|
||||
configuration.build.container.name +
|
||||
'.middlewares=global-compress'
|
||||
'traefik.http.routers.' + containerName + '.tls.certresolver=letsencrypt',
|
||||
'traefik.http.routers.' + containerName + '.middlewares=global-compress'
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -62,7 +59,7 @@ export default async function (configuration, imageChanged) {
|
||||
if (imageChanged) {
|
||||
// console.log('image changed')
|
||||
await execShellAsync(
|
||||
`docker service update --image ${configuration.build.container.name}:${configuration.build.container.tag} ${configuration.build.container.name}_${configuration.build.container.name}`
|
||||
`docker service update --image ${containerName}:${containerTag} ${containerName}_${containerName}`
|
||||
);
|
||||
} else {
|
||||
// console.log('new deployment or force deployment or config changed')
|
||||
@@ -71,6 +68,11 @@ export default async function (configuration, imageChanged) {
|
||||
`cat ${configuration.general.workdir}/stack.yml | docker stack deploy --prune -c - ${containerName}`
|
||||
);
|
||||
}
|
||||
async function purgeImagesAsync(found) {
|
||||
await delay(10000);
|
||||
await purgeImagesContainers(found, true);
|
||||
}
|
||||
purgeImagesAsync(configuration);
|
||||
|
||||
await saveAppLog('### Published done!', configuration);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import ApplicationLog from '$models/ApplicationLog';
|
||||
import dayjs from 'dayjs';
|
||||
import { version } from '../../../../package.json';
|
||||
|
||||
|
||||
function generateTimestamp() {
|
||||
return `${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')} `;
|
||||
}
|
||||
|
||||
@@ -15,10 +15,7 @@ const publishPython = (configuration) => {
|
||||
};
|
||||
|
||||
export default async function (configuration) {
|
||||
await fs.writeFile(
|
||||
`${configuration.general.workdir}/Dockerfile`,
|
||||
publishPython(configuration)
|
||||
);
|
||||
await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, publishPython(configuration));
|
||||
const stream = await docker.engine.buildImage(
|
||||
{ src: ['.'], context: configuration.general.workdir },
|
||||
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
|
||||
|
||||
@@ -46,6 +46,7 @@ const templates = {
|
||||
pack: 'vuejs',
|
||||
...defaultBuildAndDeploy,
|
||||
directory: 'dist',
|
||||
port: 80,
|
||||
name: 'Vue'
|
||||
},
|
||||
gatsby: {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import Deployment from '$models/Deployment';
|
||||
import dayjs from 'dayjs';
|
||||
import buildContainer from './buildContainer';
|
||||
@@ -10,7 +9,7 @@ import { saveAppLog } from './logging';
|
||||
export default async function (configuration, imageChanged) {
|
||||
const { id, organization, name, branch } = configuration.repository;
|
||||
const { domain } = configuration.publish;
|
||||
const { deployId} = configuration.general;
|
||||
const { deployId } = configuration.general;
|
||||
try {
|
||||
await saveAppLog(`${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')} Queued.`, configuration);
|
||||
await copyFiles(configuration);
|
||||
@@ -20,6 +19,7 @@ export default async function (configuration, imageChanged) {
|
||||
{ repoId: id, branch, deployId, organization, name, domain },
|
||||
{ repoId: id, branch, deployId, organization, name, domain, progress: 'done' }
|
||||
);
|
||||
|
||||
await updateServiceLabels(configuration);
|
||||
} catch (error) {
|
||||
await Deployment.findOneAndUpdate(
|
||||
|
||||
@@ -9,7 +9,7 @@ export function execShellAsync(cmd, opts = {}) {
|
||||
shell.config.silent = true;
|
||||
shell.exec(cmd, opts, async function (code, stdout, stderr) {
|
||||
if (code !== 0) {
|
||||
await saveServerLog({ message: JSON.stringify({ cmd, opts, code, stdout, stderr }) })
|
||||
await saveServerLog({ message: JSON.stringify({ cmd, opts, code, stdout, stderr }) });
|
||||
return reject(new Error(stderr));
|
||||
}
|
||||
return resolve(stdout);
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export const publicPages = ['/', '/api/v1/login/github/app', '/api/v1/webhooks/deploy', '/success'];
|
||||
export const VITE_GITHUB_APP_NAME = import.meta.env.VITE_GITHUB_APP_NAME;
|
||||
|
||||
@@ -63,7 +63,6 @@ export async function request(
|
||||
} else {
|
||||
if (response.headers.get('content-disposition')) {
|
||||
const blob = await response.blob();
|
||||
console.log(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = response.headers.get('content-disposition').split('=')[1] || 'backup.gz';
|
||||
|
||||
@@ -8,4 +8,5 @@ const ApplicationLogsSchema = new Schema({
|
||||
|
||||
ApplicationLogsSchema.set('timestamps', true);
|
||||
|
||||
export default mongoose.models['logs-application'] || mongoose.model('logs-application', ApplicationLogsSchema);
|
||||
export default mongoose.models['logs-application'] ||
|
||||
mongoose.model('logs-application', ApplicationLogsSchema);
|
||||
|
||||
@@ -13,12 +13,14 @@ const ConfigurationSchema = new Schema({
|
||||
id: { type: Number, required: true },
|
||||
organization: { type: String, required: true },
|
||||
name: { type: String, required: true },
|
||||
branch: { type: String, required: true },
|
||||
branch: { type: String, required: true }
|
||||
},
|
||||
general: {
|
||||
deployId: { type: String, required: true },
|
||||
nickname: { type: String, required: true },
|
||||
workdir: { type: String, required: true },
|
||||
isPreviewDeploymentEnabled: { type: Boolean, required: true, default: false },
|
||||
pullRequest: { type: Number, required: true, default: 0 }
|
||||
},
|
||||
build: {
|
||||
pack: { type: String, required: true },
|
||||
@@ -29,25 +31,25 @@ const ConfigurationSchema = new Schema({
|
||||
start: { type: String },
|
||||
python: {
|
||||
module: { type: String },
|
||||
instance: { type: String },
|
||||
instance: { type: String }
|
||||
}
|
||||
|
||||
},
|
||||
container: {
|
||||
name: { type: String, required: true },
|
||||
tag: { type: String, required: true },
|
||||
baseSHA: { type: String, required: true },
|
||||
},
|
||||
baseSHA: { type: String, required: true }
|
||||
}
|
||||
},
|
||||
publish: {
|
||||
directory: { type: String },
|
||||
domain: { type: String, required: true },
|
||||
path: { type: String },
|
||||
port: { type: Number },
|
||||
secrets: { type: Array },
|
||||
secrets: { type: Array }
|
||||
}
|
||||
});
|
||||
|
||||
ConfigurationSchema.set('timestamps', true);
|
||||
|
||||
export default mongoose.models['configuration'] || mongoose.model('configuration', ConfigurationSchema);
|
||||
export default mongoose.models['configuration'] ||
|
||||
mongoose.model('configuration', ConfigurationSchema);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { version } from '../../package.json'
|
||||
import { version } from '../../package.json';
|
||||
const { Schema, Document } = mongoose;
|
||||
|
||||
// export interface ILogsServer extends Document {
|
||||
|
||||
@@ -306,7 +306,7 @@
|
||||
>
|
||||
{:else}
|
||||
<button
|
||||
class="opacity-50 tracking-tight font-bold text-xs rounded px-2 cursor-not-allowed"
|
||||
class="opacity-50 tracking-tight font-bold text-xs rounded px-2 cursor-not-allowed"
|
||||
disabled={upgradeDisabled}>Upgrading. It could take a while, please wait...</button
|
||||
>
|
||||
{/if}
|
||||
|
||||
@@ -1,32 +1,18 @@
|
||||
import { setDefaultConfiguration } from '$lib/api/applications/configuration';
|
||||
import { saveServerLog } from '$lib/api/applications/logging';
|
||||
import { docker } from '$lib/api/docker';
|
||||
import Configuration from '$models/Configuration';
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
|
||||
export async function post(request: Request) {
|
||||
try {
|
||||
const { DOMAIN } = process.env;
|
||||
const configuration = setDefaultConfiguration(request.body);
|
||||
|
||||
const services = (await docker.engine.listServices()).filter(
|
||||
(r) => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application'
|
||||
);
|
||||
let foundDomain = false;
|
||||
|
||||
for (const service of services) {
|
||||
const running = JSON.parse(service.Spec.Labels.configuration);
|
||||
if (running) {
|
||||
if (
|
||||
running.publish.domain === configuration.publish.domain &&
|
||||
running.repository.id !== configuration.repository.id &&
|
||||
running.publish.path === configuration.publish.path
|
||||
) {
|
||||
foundDomain = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (DOMAIN === configuration.publish.domain) foundDomain = true;
|
||||
if (foundDomain) {
|
||||
const configurationFound = await Configuration.find({
|
||||
'repository.id': { $ne: configuration.repository.id },
|
||||
'publish.domain': configuration.publish.domain
|
||||
}).select('-_id -__v -createdAt -updatedAt');
|
||||
if (configurationFound.length > 0 || configuration.publish.domain === DOMAIN) {
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
|
||||
@@ -5,17 +5,17 @@ import type { Request } from '@sveltejs/kit';
|
||||
export async function post(request: Request) {
|
||||
const { name, organization, branch }: any = request.body || {};
|
||||
if (name && organization && branch) {
|
||||
const configurationFound = await Configuration.findOne({
|
||||
const configurationFound = await Configuration.find({
|
||||
'repository.name': name,
|
||||
'repository.organization': organization,
|
||||
'repository.branch': branch,
|
||||
}).lean()
|
||||
'repository.branch': branch
|
||||
}).select('-_id -__v -createdAt -updatedAt');
|
||||
|
||||
if (configurationFound) {
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
success: true,
|
||||
...configurationFound
|
||||
configuration: [...configurationFound]
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -46,6 +46,7 @@ export async function post(request: Request) {
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (found) {
|
||||
return {
|
||||
status: 200,
|
||||
75
src/routes/api/v1/application/config/previewDeployment.ts
Normal file
75
src/routes/api/v1/application/config/previewDeployment.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { updateServiceLabels } from '$lib/api/applications/configuration';
|
||||
import { execShellAsync } from '$lib/api/common';
|
||||
import { docker } from '$lib/api/docker';
|
||||
import ApplicationLog from '$models/ApplicationLog';
|
||||
import Configuration from '$models/Configuration';
|
||||
import Deployment from '$models/Deployment';
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
|
||||
export async function post(request: Request) {
|
||||
const { name, organization, branch, isPreviewDeploymentEnabled }: any = request.body || {};
|
||||
if (name && organization && branch) {
|
||||
const configuration = await Configuration.findOneAndUpdate(
|
||||
{
|
||||
'repository.name': name,
|
||||
'repository.organization': organization,
|
||||
'repository.branch': branch
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
'general.isPreviewDeploymentEnabled': isPreviewDeploymentEnabled,
|
||||
'general.pullRequest': 0
|
||||
}
|
||||
},
|
||||
{ new: true }
|
||||
).select('-_id -__v -createdAt -updatedAt');
|
||||
if (!isPreviewDeploymentEnabled) {
|
||||
const found = await Configuration.find({
|
||||
'repository.name': name,
|
||||
'repository.organization': organization,
|
||||
'repository.branch': branch,
|
||||
'general.pullRequest': { $ne: 0 }
|
||||
});
|
||||
for (const prDeployment of found) {
|
||||
await Configuration.findOneAndRemove({
|
||||
'repository.name': name,
|
||||
'repository.organization': organization,
|
||||
'repository.branch': branch,
|
||||
'publish.domain': prDeployment.publish.domain
|
||||
});
|
||||
const deploys = await Deployment.find({
|
||||
organization,
|
||||
branch,
|
||||
name,
|
||||
domain: prDeployment.publish.domain
|
||||
});
|
||||
for (const deploy of deploys) {
|
||||
await ApplicationLog.deleteMany({ deployId: deploy.deployId });
|
||||
await Deployment.deleteMany({ deployId: deploy.deployId });
|
||||
}
|
||||
await execShellAsync(`docker stack rm ${prDeployment.build.container.name}`);
|
||||
}
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
organization,
|
||||
name,
|
||||
branch
|
||||
}
|
||||
};
|
||||
}
|
||||
updateServiceLabels(configuration);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
success: true
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'Cannot save.'
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,32 +1,26 @@
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
import Deployment from '$models/Deployment';
|
||||
import { docker } from '$lib/api/docker';
|
||||
import { precheckDeployment, setDefaultConfiguration } from '$lib/api/applications/configuration';
|
||||
import cloneRepository from '$lib/api/applications/cloneRepository';
|
||||
import { cleanupTmp } from '$lib/api/common';
|
||||
import queueAndBuild from '$lib/api/applications/queueAndBuild';
|
||||
import Configuration from '$models/Configuration';
|
||||
export async function post(request: Request) {
|
||||
let configuration;
|
||||
try {
|
||||
const services = (await docker.engine.listServices()).filter(
|
||||
(r) => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application'
|
||||
);
|
||||
configuration = setDefaultConfiguration(request.body);
|
||||
|
||||
if (!configuration) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'Whaaat?'
|
||||
}
|
||||
};
|
||||
}
|
||||
export async function post(request: Request) {
|
||||
const configuration = setDefaultConfiguration(request.body);
|
||||
if (!configuration) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'Whaaat?'
|
||||
}
|
||||
};
|
||||
}
|
||||
try {
|
||||
await cloneRepository(configuration);
|
||||
const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment({
|
||||
services,
|
||||
const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment(
|
||||
configuration
|
||||
});
|
||||
);
|
||||
if (foundService && !forceUpdate && !imageChanged && !configChanged) {
|
||||
cleanupTmp(configuration.general.workdir);
|
||||
return {
|
||||
@@ -56,7 +50,8 @@ export async function post(request: Request) {
|
||||
}
|
||||
const { id, organization, name, branch } = configuration.repository;
|
||||
const { domain } = configuration.publish;
|
||||
const { deployId, nickname } = configuration.general;
|
||||
const { deployId, nickname, pullRequest } = configuration.general;
|
||||
|
||||
await new Deployment({
|
||||
repoId: id,
|
||||
branch,
|
||||
@@ -66,14 +61,18 @@ export async function post(request: Request) {
|
||||
name,
|
||||
nickname
|
||||
}).save();
|
||||
await Configuration.findOneAndUpdate({
|
||||
'repository.id': id,
|
||||
'repository.organization': organization,
|
||||
'repository.name': name,
|
||||
'repository.branch': branch,
|
||||
},
|
||||
|
||||
await Configuration.findOneAndUpdate(
|
||||
{
|
||||
'repository.id': id,
|
||||
'repository.organization': organization,
|
||||
'repository.name': name,
|
||||
'repository.branch': branch,
|
||||
'general.pullRequest': { $in: [null, 0] }
|
||||
},
|
||||
{ ...configuration },
|
||||
{ upsert: true, new: true })
|
||||
{ upsert: true, new: true }
|
||||
);
|
||||
|
||||
queueAndBuild(configuration, imageChanged);
|
||||
return {
|
||||
@@ -86,6 +85,7 @@ export async function post(request: Request) {
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await Deployment.findOneAndUpdate(
|
||||
{
|
||||
repoId: configuration.repository.id,
|
||||
|
||||
@@ -10,10 +10,7 @@ export async function get(request: Request) {
|
||||
.select('-_id -__v')
|
||||
.sort({ createdAt: 'asc' });
|
||||
|
||||
const deploy: any = await Deployment.findOne({ deployId })
|
||||
.select('-_id -__v')
|
||||
.sort({ createdAt: 'desc' });
|
||||
|
||||
const deploy: any = await Deployment.findOne({ deployId }).select('-_id -__v');
|
||||
const finalLogs: any = {};
|
||||
finalLogs.progress = deploy.progress;
|
||||
finalLogs.events = logs.map((log) => log.event);
|
||||
|
||||
@@ -16,12 +16,12 @@ export async function get(request: Request) {
|
||||
.select('-_id -__v -repoId')
|
||||
.sort({ createdAt: 'desc' })
|
||||
.limit(show);
|
||||
|
||||
const finalLogs = deploy.map((d) => {
|
||||
const finalLogs = { ...d._doc };
|
||||
const updatedAt = dayjs(d.updatedAt).utc();
|
||||
finalLogs.took = updatedAt.diff(dayjs(d.createdAt)) / 1000;
|
||||
finalLogs.since = updatedAt.fromNow();
|
||||
finalLogs.isPr = d.domain.startsWith('pr');
|
||||
return finalLogs;
|
||||
});
|
||||
return {
|
||||
|
||||
@@ -16,7 +16,7 @@ export async function get(request: Request) {
|
||||
body: { success: true, logs }
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
console.log(error);
|
||||
await saveServerLog(error);
|
||||
return {
|
||||
status: 500,
|
||||
|
||||
@@ -1,61 +1,69 @@
|
||||
import { purgeImagesContainers } from '$lib/api/applications/cleanup';
|
||||
import { docker } from '$lib/api/docker';
|
||||
import Deployment from '$models/Deployment';
|
||||
import ApplicationLog from '$models/ApplicationLog';
|
||||
import { delay, execShellAsync } from '$lib/api/common';
|
||||
import Configuration from '$models/Configuration';
|
||||
|
||||
async function call(found) {
|
||||
async function purgeImagesAsync(found) {
|
||||
await delay(10000);
|
||||
await purgeImagesContainers(found, true);
|
||||
}
|
||||
export async function post(request: Request) {
|
||||
const { organization, name, branch } = request.body;
|
||||
let found = false;
|
||||
const { organization, name, branch, domain } = request.body;
|
||||
try {
|
||||
(await docker.engine.listServices())
|
||||
.filter((r) => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
|
||||
.map((s) => {
|
||||
const running = JSON.parse(s.Spec.Labels.configuration);
|
||||
if (
|
||||
running.repository.organization === organization &&
|
||||
running.repository.name === name &&
|
||||
running.repository.branch === branch
|
||||
) {
|
||||
found = running;
|
||||
const configurationFound = await Configuration.findOne({
|
||||
'repository.organization': organization,
|
||||
'repository.name': name,
|
||||
'repository.branch': branch,
|
||||
'publish.domain': domain
|
||||
});
|
||||
if (configurationFound) {
|
||||
const id = configurationFound._id;
|
||||
if (configurationFound?.general?.pullRequest === 0) {
|
||||
// Main deployment deletion request; deleting main + PRs
|
||||
const allConfiguration = await Configuration.find({
|
||||
'repository.name': name,
|
||||
'repository.organization': organization,
|
||||
'repository.branch': branch
|
||||
});
|
||||
for (const config of allConfiguration) {
|
||||
await Configuration.findOneAndRemove({
|
||||
'repository.name': config.repository.name,
|
||||
'repository.organization': config.repository.organization,
|
||||
'repository.branch': config.repository.branch
|
||||
});
|
||||
await execShellAsync(`docker stack rm ${config.build.container.name}`);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
if (found) {
|
||||
await Configuration.findOneAndRemove({
|
||||
'repository.name': name,
|
||||
'repository.organization': organization,
|
||||
'repository.branch': branch,
|
||||
})
|
||||
const deploys = await Deployment.find({ organization, branch, name });
|
||||
for (const deploy of deploys) {
|
||||
await ApplicationLog.deleteMany({ deployId: deploy.deployId });
|
||||
await Deployment.deleteMany({ deployId: deploy.deployId });
|
||||
const deploys = await Deployment.find({ organization, branch, name });
|
||||
for (const deploy of deploys) {
|
||||
await ApplicationLog.deleteMany({ deployId: deploy.deployId });
|
||||
await Deployment.deleteMany({ deployId: deploy.deployId });
|
||||
}
|
||||
|
||||
purgeImagesAsync(configurationFound);
|
||||
} else {
|
||||
// Delete only PRs
|
||||
await Configuration.findByIdAndRemove(id);
|
||||
await execShellAsync(`docker stack rm ${configurationFound.build.container.name}`);
|
||||
const deploys = await Deployment.find({ organization, branch, name, domain });
|
||||
for (const deploy of deploys) {
|
||||
await ApplicationLog.deleteMany({ deployId: deploy.deployId });
|
||||
await Deployment.deleteMany({ deployId: deploy.deployId });
|
||||
}
|
||||
purgeImagesAsync(configurationFound);
|
||||
}
|
||||
await execShellAsync(`docker stack rm ${found.build.container.name}`);
|
||||
call(found);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
organization,
|
||||
name,
|
||||
branch
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 500,
|
||||
error: {
|
||||
message: 'Nothing to do.'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
organization,
|
||||
name,
|
||||
branch
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {
|
||||
status: 500,
|
||||
error: {
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { docker } from '$lib/api/docker';
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
|
||||
import Configuration from '$models/Configuration';
|
||||
export async function get(request: Request) {
|
||||
// Should update this to get data from mongodb and update db with the currently running services on start!
|
||||
const dockerServices = await docker.engine.listServices();
|
||||
let applications: any = dockerServices.filter(
|
||||
(r) =>
|
||||
r.Spec.Labels.managedBy === 'coolify' &&
|
||||
r.Spec.Labels.type === 'application' &&
|
||||
r.Spec.Labels.configuration
|
||||
);
|
||||
let databases: any = dockerServices.filter(
|
||||
(r) =>
|
||||
r.Spec.Labels.managedBy === 'coolify' &&
|
||||
@@ -21,15 +16,6 @@ export async function get(request: Request) {
|
||||
r.Spec.Labels.type === 'service' &&
|
||||
r.Spec.Labels.configuration
|
||||
);
|
||||
applications = applications.map((r) => {
|
||||
if (JSON.parse(r.Spec.Labels.configuration)) {
|
||||
return {
|
||||
configuration: JSON.parse(r.Spec.Labels.configuration),
|
||||
UpdatedAt: r.UpdatedAt
|
||||
};
|
||||
}
|
||||
return {};
|
||||
});
|
||||
databases = databases.map((r) => {
|
||||
if (JSON.parse(r.Spec.Labels.configuration)) {
|
||||
return {
|
||||
@@ -47,14 +33,23 @@ export async function get(request: Request) {
|
||||
}
|
||||
return {};
|
||||
});
|
||||
applications = [
|
||||
...new Map(
|
||||
applications.map((item) => [
|
||||
item.configuration.publish.domain + item.configuration.publish.path,
|
||||
item
|
||||
])
|
||||
).values()
|
||||
];
|
||||
const configurations = await Configuration.find({
|
||||
'general.pullRequest': { $in: [null, 0] }
|
||||
}).select('-_id -__v -createdAt');
|
||||
const applications = [];
|
||||
for (const configuration of configurations) {
|
||||
const foundPRDeployments = await Configuration.find({
|
||||
'repository.id': configuration.repository.id,
|
||||
'repository.branch': configuration.repository.branch,
|
||||
'general.pullRequest': { $ne: 0 }
|
||||
}).select('-_id -__v -createdAt');
|
||||
const payload = {
|
||||
configuration,
|
||||
UpdatedAt: configuration.updatedAt,
|
||||
prBuilds: foundPRDeployments.length > 0 ? true : false
|
||||
};
|
||||
applications.push(payload);
|
||||
}
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Request } from '@sveltejs/kit';
|
||||
|
||||
export async function get(request: Request) {
|
||||
const { serviceName } = request.params;
|
||||
|
||||
try {
|
||||
const service = (await docker.engine.listServices()).find(
|
||||
(r) =>
|
||||
|
||||
59
src/routes/api/v1/services/deploy/nocodb/index.ts
Normal file
59
src/routes/api/v1/services/deploy/nocodb/index.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { Request } from '@sveltejs/kit';
|
||||
import yaml from 'js-yaml';
|
||||
import { promises as fs } from 'fs';
|
||||
import { docker } from '$lib/api/docker';
|
||||
import { baseServiceConfiguration } from '$lib/api/applications/common';
|
||||
import { cleanupTmp, execShellAsync } from '$lib/api/common';
|
||||
|
||||
export async function post(request: Request) {
|
||||
let { baseURL } = request.body;
|
||||
const traefikURL = baseURL;
|
||||
baseURL = `https://${baseURL}`;
|
||||
const workdir = '/tmp/nocodb';
|
||||
const deployId = 'nocodb';
|
||||
const stack = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[deployId]: {
|
||||
image: 'nocodb/nocodb',
|
||||
networks: [`${docker.network}`],
|
||||
deploy: {
|
||||
...baseServiceConfiguration,
|
||||
labels: [
|
||||
'managedBy=coolify',
|
||||
'type=service',
|
||||
'serviceName=nocodb',
|
||||
'configuration=' +
|
||||
JSON.stringify({
|
||||
baseURL
|
||||
}),
|
||||
'traefik.enable=true',
|
||||
'traefik.http.services.' + deployId + '.loadbalancer.server.port=8080',
|
||||
'traefik.http.routers.' + deployId + '.entrypoints=websecure',
|
||||
'traefik.http.routers.' +
|
||||
deployId +
|
||||
'.rule=Host(`' +
|
||||
traefikURL +
|
||||
'`) && PathPrefix(`/`)',
|
||||
'traefik.http.routers.' + deployId + '.tls.certresolver=letsencrypt',
|
||||
'traefik.http.routers.' + deployId + '.middlewares=global-compress'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[`${docker.network}`]: {
|
||||
external: true
|
||||
}
|
||||
}
|
||||
};
|
||||
await execShellAsync(`mkdir -p ${workdir}`);
|
||||
await fs.writeFile(`${workdir}/stack.yml`, yaml.dump(stack));
|
||||
await execShellAsync('docker stack rm nocodb');
|
||||
await execShellAsync(`cat ${workdir}/stack.yml | docker stack deploy --prune -c - ${deployId}`);
|
||||
cleanupTmp(workdir);
|
||||
return {
|
||||
status: 200,
|
||||
body: { message: 'OK' }
|
||||
};
|
||||
}
|
||||
@@ -4,11 +4,16 @@ import Deployment from '$models/Deployment';
|
||||
import { docker } from '$lib/api/docker';
|
||||
import { precheckDeployment, setDefaultConfiguration } from '$lib/api/applications/configuration';
|
||||
import cloneRepository from '$lib/api/applications/cloneRepository';
|
||||
import { cleanupTmp } from '$lib/api/common';
|
||||
import { cleanupTmp, execShellAsync } from '$lib/api/common';
|
||||
import queueAndBuild from '$lib/api/applications/queueAndBuild';
|
||||
import Configuration from '$models/Configuration';
|
||||
import ApplicationLog from '$models/ApplicationLog';
|
||||
import { cleanupStuckedDeploymentsInDB } from '$lib/api/applications/cleanup';
|
||||
export async function post(request: Request) {
|
||||
let configuration;
|
||||
const allowedGithubEvents = ['push', 'pull_request'];
|
||||
const allowedPRActions = ['opened', 'reopened', 'synchronize', 'closed'];
|
||||
const githubEvent = request.headers['x-github-event'];
|
||||
const { GITHUP_APP_WEBHOOK_SECRET } = process.env;
|
||||
const hmac = crypto.createHmac('sha256', GITHUP_APP_WEBHOOK_SECRET);
|
||||
const digest = Buffer.from(
|
||||
@@ -20,52 +25,96 @@ export async function post(request: Request) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'Invalid request'
|
||||
error: 'Invalid request.'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (request.headers['x-github-event'] !== 'push') {
|
||||
if (!allowedGithubEvents.includes(githubEvent)) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'Not a push event.'
|
||||
error: 'Event not allowed.'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const services = (await docker.engine.listServices()).filter(
|
||||
(r) => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application'
|
||||
);
|
||||
|
||||
configuration = services.find((r) => {
|
||||
if (request.body.ref.startsWith('refs')) {
|
||||
const branch = request.body.ref.split('/')[2];
|
||||
if (
|
||||
JSON.parse(r.Spec.Labels.configuration).repository.id === request.body.repository.id &&
|
||||
JSON.parse(r.Spec.Labels.configuration).repository.branch === branch
|
||||
) {
|
||||
return r;
|
||||
const applications = await Configuration.find({
|
||||
'repository.id': request.body.repository.id
|
||||
}).select('-_id -__v -createdAt -updatedAt');
|
||||
if (githubEvent === 'push') {
|
||||
configuration = applications.find((r) => {
|
||||
if (request.body.ref.startsWith('refs')) {
|
||||
if (r.repository.branch === request.body.ref.split('/')[2]) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
} else if (githubEvent === 'pull_request') {
|
||||
if (!allowedPRActions.includes(request.body.action)) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'PR action is not allowed.'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
configuration = setDefaultConfiguration(JSON.parse(configuration.Spec.Labels.configuration));
|
||||
|
||||
configuration = applications.find(
|
||||
(r) => r.repository.branch === request.body['pull_request'].base.ref
|
||||
);
|
||||
if (configuration) {
|
||||
if (!configuration.general.isPreviewDeploymentEnabled) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'PR deployments are not enabled.'
|
||||
}
|
||||
};
|
||||
}
|
||||
configuration.general.pullRequest = request.body.number;
|
||||
}
|
||||
}
|
||||
if (!configuration) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: 'Whaaat?'
|
||||
error: 'No configuration found.'
|
||||
}
|
||||
};
|
||||
}
|
||||
configuration = setDefaultConfiguration(configuration);
|
||||
const { id, organization, name, branch } = configuration.repository;
|
||||
const { domain } = configuration.publish;
|
||||
const { deployId, nickname, pullRequest } = configuration.general;
|
||||
|
||||
if (request.body.action === 'closed') {
|
||||
const deploys = await Deployment.find({ organization, branch, name, domain });
|
||||
for (const deploy of deploys) {
|
||||
await ApplicationLog.deleteMany({ deployId: deploy.deployId });
|
||||
await Deployment.deleteMany({ deployId: deploy.deployId });
|
||||
}
|
||||
await Configuration.findOneAndRemove({
|
||||
'repository.id': id,
|
||||
'repository.organization': organization,
|
||||
'repository.name': name,
|
||||
'repository.branch': branch,
|
||||
'general.pullRequest': pullRequest
|
||||
});
|
||||
await execShellAsync(`docker stack rm ${configuration.build.container.name}`);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
success: true,
|
||||
message: 'Removed'
|
||||
}
|
||||
};
|
||||
}
|
||||
await cloneRepository(configuration);
|
||||
const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment({
|
||||
services,
|
||||
const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment(
|
||||
configuration
|
||||
});
|
||||
);
|
||||
if (foundService && !forceUpdate && !imageChanged && !configChanged) {
|
||||
cleanupTmp(configuration.general.workdir);
|
||||
return {
|
||||
@@ -77,11 +126,11 @@ export async function post(request: Request) {
|
||||
};
|
||||
}
|
||||
const alreadyQueued = await Deployment.find({
|
||||
repoId: configuration.repository.id,
|
||||
branch: configuration.repository.branch,
|
||||
organization: configuration.repository.organization,
|
||||
name: configuration.repository.name,
|
||||
domain: configuration.publish.domain,
|
||||
repoId: id,
|
||||
branch: branch,
|
||||
organization: organization,
|
||||
name: name,
|
||||
domain: domain,
|
||||
progress: { $in: ['queued', 'inprogress'] }
|
||||
});
|
||||
if (alreadyQueued.length > 0) {
|
||||
@@ -93,9 +142,7 @@ export async function post(request: Request) {
|
||||
}
|
||||
};
|
||||
}
|
||||
const { id, organization, name, branch } = configuration.repository;
|
||||
const { domain } = configuration.publish;
|
||||
const { deployId, nickname } = configuration.general;
|
||||
|
||||
await new Deployment({
|
||||
repoId: id,
|
||||
branch,
|
||||
@@ -105,14 +152,32 @@ export async function post(request: Request) {
|
||||
name,
|
||||
nickname
|
||||
}).save();
|
||||
await Configuration.findOneAndUpdate({
|
||||
'repository.id': id,
|
||||
'repository.organization': organization,
|
||||
'repository.name': name,
|
||||
'repository.branch': branch,
|
||||
},
|
||||
{ ...configuration },
|
||||
{ upsert: true, new: true })
|
||||
|
||||
if (githubEvent === 'pull_request') {
|
||||
await Configuration.findOneAndUpdate(
|
||||
{
|
||||
'repository.id': id,
|
||||
'repository.organization': organization,
|
||||
'repository.name': name,
|
||||
'repository.branch': branch,
|
||||
'general.pullRequest': pullRequest
|
||||
},
|
||||
{ ...configuration },
|
||||
{ upsert: true, new: true }
|
||||
);
|
||||
} else {
|
||||
await Configuration.findOneAndUpdate(
|
||||
{
|
||||
'repository.id': id,
|
||||
'repository.organization': organization,
|
||||
'repository.name': name,
|
||||
'repository.branch': branch,
|
||||
'general.pullRequest': { $in: [null, 0] }
|
||||
},
|
||||
{ ...configuration },
|
||||
{ upsert: true, new: true }
|
||||
);
|
||||
}
|
||||
|
||||
queueAndBuild(configuration, imageChanged);
|
||||
return {
|
||||
@@ -125,28 +190,40 @@ export async function post(request: Request) {
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
await Deployment.findOneAndUpdate(
|
||||
{
|
||||
repoId: configuration.repository.id,
|
||||
branch: configuration.repository.branch,
|
||||
organization: configuration.repository.organization,
|
||||
name: configuration.repository.name,
|
||||
domain: configuration.publish.domain
|
||||
},
|
||||
{
|
||||
repoId: configuration.repository.id,
|
||||
branch: configuration.repository.branch,
|
||||
organization: configuration.repository.organization,
|
||||
name: configuration.repository.name,
|
||||
domain: configuration.publish.domain,
|
||||
progress: 'failed'
|
||||
}
|
||||
);
|
||||
console.log(error);
|
||||
// console.log(configuration)
|
||||
if (configuration) {
|
||||
cleanupTmp(configuration.general.workdir);
|
||||
await Deployment.findOneAndUpdate(
|
||||
{
|
||||
repoId: configuration.repository.id,
|
||||
branch: configuration.repository.branch,
|
||||
organization: configuration.repository.organization,
|
||||
name: configuration.repository.name,
|
||||
domain: configuration.publish.domain
|
||||
},
|
||||
{
|
||||
repoId: configuration.repository.id,
|
||||
branch: configuration.repository.branch,
|
||||
organization: configuration.repository.organization,
|
||||
name: configuration.repository.name,
|
||||
domain: configuration.publish.domain,
|
||||
progress: 'failed'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: error.message || error
|
||||
}
|
||||
};
|
||||
} finally {
|
||||
try {
|
||||
await cleanupStuckedDeploymentsInDB();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,13 +84,15 @@
|
||||
deployment.progress !== 'failed'}
|
||||
class:bg-warmGray-800={deployment.progress !== 'done' &&
|
||||
deployment.progress !== 'failed'}
|
||||
class:hover:bg-red-200={deployment.progress === 'failed'}
|
||||
class:hover:border-red-500={deployment.progress === 'failed'}
|
||||
on:click={() => goto(`./logs/${deployment.deployId}`)}
|
||||
>
|
||||
<div class="font-bold text-sm px-3 flex justify-center items-center">
|
||||
{deployment.branch}
|
||||
<div class="flex space-x-2 px-2">
|
||||
<div class="font-bold text-sm flex justify-center items-center">
|
||||
{deployment.branch}
|
||||
</div>
|
||||
<div class="font-bold text-xs flex justify-center items-center text-warmGray-500">{deployment.isPr ? 'PR' : ''}</div>
|
||||
</div>
|
||||
<div class="flex-1" />
|
||||
<div class="px-3 w-48">
|
||||
<div
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { application, initialApplication, initConf, dashboard } from '$store';
|
||||
import { application, initialApplication, initConf, dashboard, prApplication } from '$store';
|
||||
import { onDestroy } from 'svelte';
|
||||
import Loading from '$components/Loading.svelte';
|
||||
import Navbar from '$components/Application/Navbar.svelte';
|
||||
@@ -14,15 +14,16 @@
|
||||
|
||||
async function setConfiguration() {
|
||||
try {
|
||||
const config = await request(`/api/v1/application/config`, $session, {
|
||||
const { configuration } = await request(`/api/v1/application/config`, $session, {
|
||||
body: {
|
||||
name: $application.repository.name,
|
||||
organization: $application.repository.organization,
|
||||
branch: $application.repository.branch
|
||||
}
|
||||
});
|
||||
$application = { ...config };
|
||||
$initConf = JSON.parse(JSON.stringify($application));
|
||||
$prApplication = configuration.filter((c) => c.general.pullRequest !== 0);
|
||||
$application = configuration.find((c) => c.general.pullRequest === 0);
|
||||
if (!$application) browser && goto('/dashboard/applications');
|
||||
} catch (error) {
|
||||
browser && goto('/dashboard/applications');
|
||||
}
|
||||
|
||||
@@ -49,6 +49,15 @@
|
||||
/>
|
||||
<div class="text-white font-bold">Plausible Analytics</div>
|
||||
</div>
|
||||
{:else if service.serviceName == 'nocodb'}
|
||||
<div>
|
||||
<img
|
||||
alt="nocodedb"
|
||||
class="w-10 absolute top-0 left-0 -m-6"
|
||||
src="https://cdn.coollabs.io/assets/coolify/services/nocodb/nocodb.png"
|
||||
/>
|
||||
<div class="text-white font-bold">NocoDB</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -67,9 +67,7 @@
|
||||
<div class="pb-2 pt-5 space-y-4">
|
||||
<div class="text-2xl font-bold border-gradient w-32">Database</div>
|
||||
<div class="flex items-center pt-4">
|
||||
{#if $database.config.general.type !== 'redis'}
|
||||
<div class="font-bold w-64 text-warmGray-400">Connection string</div>
|
||||
{/if}
|
||||
<div class="font-bold w-64 text-warmGray-400">Connection string</div>
|
||||
{#if $database.config.general.type === 'mongodb'}
|
||||
<PasswordField
|
||||
value={`mongodb://${$database.envs.MONGODB_USERNAME}:${$database.envs.MONGODB_PASSWORD}@${$database.config.general.deployId}:27017/${$database.envs.MONGODB_DATABASE}`}
|
||||
@@ -86,6 +84,10 @@
|
||||
<PasswordField
|
||||
value={`http://${$database.envs.COUCHDB_USER}:${$database.envs.COUCHDB_PASSWORD}@${$database.config.general.deployId}:5984`}
|
||||
/>
|
||||
{:else if $database.config.general.type === 'redis'}
|
||||
<PasswordField
|
||||
value={`redis://${$database.envs.REDIS_PASSWORD}@${$database.config.general.deployId}:6379`}
|
||||
/>
|
||||
{:else if $database.config.general.type === 'clickhouse'}
|
||||
<!-- {JSON.stringify($database)} -->
|
||||
<!-- <textarea
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
', toolbar=0, menubar=0, status=0'
|
||||
);
|
||||
const timer = setInterval(() => {
|
||||
if (newWindow.closed) {
|
||||
if (newWindow?.closed) {
|
||||
clearInterval(timer);
|
||||
browser && location.reload()
|
||||
}
|
||||
|
||||
@@ -14,23 +14,14 @@
|
||||
try {
|
||||
service = await request(`/api/v1/services/${$page.params.name}`, $session);
|
||||
} catch (error) {
|
||||
browser && toast.push(`Cannot find service ${$page.params.name}?!`);
|
||||
goto(`/dashboard/services`, { replaceState: true });
|
||||
if (browser) {
|
||||
toast.push(`Cannot find service ${$page.params.name}?!`);
|
||||
goto(`/dashboard/services`, { replaceState: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
async function activate() {
|
||||
try {
|
||||
await request(`/api/v1/services/deploy/${$page.params.name}/activate`, $session, {
|
||||
method: 'PATCH',
|
||||
body: {}
|
||||
});
|
||||
browser && toast.push(`All users are activated for Plausible.`);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
browser && toast.push(`Ooops, there was an error activating users for Plausible?!`);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
{#await loadServiceConfig()}
|
||||
@@ -38,7 +29,12 @@
|
||||
{:then}
|
||||
<div class="min-h-full text-white">
|
||||
<div class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center">
|
||||
<div>{$page.params.name === 'plausible' ? 'Plausible Analytics' : $page.params.name}</div>
|
||||
{#if $page.params.name === 'plausible'}
|
||||
<div>Plausible Analytics</div>
|
||||
{:else if $page.params.name === 'nocodb'}
|
||||
<div>NocoDB</div>
|
||||
{/if}
|
||||
|
||||
<div class="px-4">
|
||||
{#if $page.params.name === 'plausible'}
|
||||
<img
|
||||
@@ -46,37 +42,38 @@
|
||||
class="w-6 mx-auto"
|
||||
src="https://cdn.coollabs.io/assets/coolify/services/plausible/logo_sm.png"
|
||||
/>
|
||||
{:else if $page.params.name === 'nocodb'}
|
||||
<img
|
||||
alt="nocodb logo"
|
||||
class="w-8 mx-auto"
|
||||
src="https://cdn.coollabs.io/assets/coolify/services/nocodb/nocodb.png"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<a
|
||||
target="_blank"
|
||||
class="icon mx-2"
|
||||
href={service.config.baseURL}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
<a target="_blank" class="icon mx-2" href={service.config.baseURL}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
||||
/>
|
||||
</svg></a
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
||||
/>
|
||||
</svg></a
|
||||
>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2 max-w-4xl mx-auto px-6" in:fade={{ duration: 100 }}>
|
||||
<div class="block text-center py-4">
|
||||
{#if $page.params.name === 'plausible'}
|
||||
<Plausible {service} />
|
||||
{:else if $page.params.name === 'nocodb'}
|
||||
<div class="font-bold">Nothing to show here. Enjoy using NocoDB!</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,13 +14,11 @@
|
||||
async function checkService() {
|
||||
try {
|
||||
const data = await request(`/api/v1/services/${$page.params.type}`, $session);
|
||||
if (!data?.success) {
|
||||
if (data?.success) {
|
||||
if (browser) {
|
||||
goto(`/dashboard/services`, { replaceState: true });
|
||||
goto(`/service/${$page.params.type}/configuration`, { replaceState: true });
|
||||
toast.push(
|
||||
`${
|
||||
$page.params.type === 'plausible' ? 'Plausible Analytics' : $page.params.type
|
||||
} already deployed.`
|
||||
`Service already deployed.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import TooltipInfo from '$components/TooltipInfo.svelte';
|
||||
import { browser } from '$app/env';
|
||||
|
||||
$: deployable =
|
||||
$: deployablePlausible =
|
||||
$newService.baseURL === '' ||
|
||||
$newService.baseURL === null ||
|
||||
$newService.email === '' ||
|
||||
@@ -22,7 +22,7 @@
|
||||
$newService.userPassword.length <= 6 ||
|
||||
$newService.userPassword !== $newService.userPasswordAgain;
|
||||
let loading = false;
|
||||
async function deploy() {
|
||||
async function deployPlausible() {
|
||||
try {
|
||||
loading = true;
|
||||
const payload = $newService;
|
||||
@@ -44,6 +44,30 @@
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
async function deployNocodb() {
|
||||
|
||||
try {
|
||||
loading = true;
|
||||
await request(`/api/v1/services/deploy/${$page.params.type}`, $session, {
|
||||
body: {
|
||||
baseURL: $newService.baseURL
|
||||
}
|
||||
});
|
||||
if (browser) {
|
||||
toast.push(
|
||||
'Service deployment queued.<br><br><br>It could take 2-5 minutes to be ready, be patient and grab a coffee/tea!',
|
||||
{ duration: 4000 }
|
||||
);
|
||||
goto(`/dashboard/services`, { replaceState: true });
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
browser && toast.push('Oops something went wrong. See console.log.');
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="min-h-full text-white">
|
||||
@@ -51,18 +75,20 @@
|
||||
Deploy new
|
||||
{#if $page.params.type === 'plausible'}
|
||||
<span class="text-blue-500 px-2 capitalize">Plausible Analytics</span>
|
||||
{:else if $page.params.type === 'nocodb'}
|
||||
<span class="text-blue-500 px-2 capitalize">NocoDB</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if loading}
|
||||
<Loading />
|
||||
{:else}
|
||||
{:else if $page.params.type === 'plausible'}
|
||||
<div class="space-y-2 max-w-4xl mx-auto px-6 flex-col text-center" in:fade={{ duration: 100 }}>
|
||||
<div class="grid grid-flow-row">
|
||||
<label for="Domain"
|
||||
>Domain <TooltipInfo
|
||||
position="right"
|
||||
label={`You will have your Plausible instance at here.`}
|
||||
label={`You could reach your Plausible Analytics instance here.`}
|
||||
/></label
|
||||
>
|
||||
<input
|
||||
@@ -114,15 +140,39 @@
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
disabled={deployable}
|
||||
class:cursor-not-allowed={deployable}
|
||||
class:bg-blue-500={!deployable}
|
||||
class:hover:bg-blue-400={!deployable}
|
||||
class:hover:bg-transparent={deployable}
|
||||
class:text-warmGray-700={deployable}
|
||||
class:text-white={!deployable}
|
||||
disabled={deployablePlausible}
|
||||
class:cursor-not-allowed={deployablePlausible}
|
||||
class:bg-blue-500={!deployablePlausible}
|
||||
class:hover:bg-blue-400={!deployablePlausible}
|
||||
class:hover:bg-transparent={deployablePlausible}
|
||||
class:text-warmGray-700={deployablePlausible}
|
||||
class:text-white={!deployablePlausible}
|
||||
class="button p-2"
|
||||
on:click={deploy}
|
||||
on:click={deployPlausible}
|
||||
>
|
||||
Deploy
|
||||
</button>
|
||||
</div>
|
||||
{:else if $page.params.type === 'nocodb'}
|
||||
<div class="space-y-2 max-w-xl mx-auto px-6 flex-col text-center" in:fade={{ duration: 100 }}>
|
||||
<div class="grid grid-flow-row pb-5">
|
||||
<label for="Domain"
|
||||
>Domain <TooltipInfo
|
||||
position="right"
|
||||
label={`You could reach your NocoDB instance here.`}
|
||||
/></label
|
||||
>
|
||||
<input
|
||||
id="Domain"
|
||||
class:border-red-500={$newService.baseURL == null || $newService.baseURL == ''}
|
||||
bind:value={$newService.baseURL}
|
||||
placeholder="nocodb.coollabs.io"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="button p-2 w-64 bg-blue-500 hover:bg-blue-400 text-white"
|
||||
on:click={deployNocodb}
|
||||
>
|
||||
Deploy
|
||||
</button>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
import { fade } from 'svelte/transition';
|
||||
</script>
|
||||
|
||||
@@ -14,7 +13,7 @@
|
||||
{#if $page.path === '/service/new'}
|
||||
<div class="flex justify-center space-x-4 font-bold pb-6">
|
||||
<div
|
||||
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-blue-500 p-2 rounded bg-warmGray-800"
|
||||
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-blue-500 p-2 rounded bg-warmGray-800 w-48"
|
||||
on:click={() => goto('/service/new/plausible')}
|
||||
>
|
||||
<img
|
||||
@@ -24,6 +23,18 @@
|
||||
/>
|
||||
<div class="text-white">Plausible Analytics</div>
|
||||
</div>
|
||||
<div
|
||||
class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-white p-2 rounded bg-warmGray-800 w-48"
|
||||
on:click={() => goto('/service/new/nocodb')}
|
||||
>
|
||||
<img
|
||||
alt="nocodb logo"
|
||||
class="w-14 mx-auto pb-2"
|
||||
src="https://cdn.coollabs.io/assets/coolify/services/nocodb/nocodb.png"
|
||||
/>
|
||||
<div class="flex-1" />
|
||||
<div class="text-white">NocoDB</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
<script>
|
||||
export let allowRegistration;
|
||||
export let sendErrors;
|
||||
|
||||
import { browser } from '$app/env';
|
||||
import { session } from '$app/stores';
|
||||
|
||||
import { request } from '$lib/request';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
@@ -48,7 +48,9 @@ export const application = writable<Application>({
|
||||
general: {
|
||||
deployId: null,
|
||||
nickname: null,
|
||||
workdir: null
|
||||
workdir: null,
|
||||
isPreviewDeploymentEnabled: false,
|
||||
pullRequest: 0
|
||||
},
|
||||
build: {
|
||||
pack: 'static',
|
||||
@@ -76,6 +78,7 @@ export const application = writable<Application>({
|
||||
secrets: []
|
||||
}
|
||||
});
|
||||
export const prApplication = writable([]);
|
||||
|
||||
export const initConf = writable({});
|
||||
|
||||
@@ -97,7 +100,9 @@ export const initialApplication: Application = {
|
||||
general: {
|
||||
deployId: null,
|
||||
nickname: null,
|
||||
workdir: null
|
||||
workdir: null,
|
||||
isPreviewDeploymentEnabled: false,
|
||||
pullRequest: 0
|
||||
},
|
||||
build: {
|
||||
pack: 'static',
|
||||
@@ -162,3 +167,5 @@ export const initialNewService = {
|
||||
userPasswordAgain: null,
|
||||
baseURL: null
|
||||
};
|
||||
|
||||
export const isPullRequestPermissionsGranted = writable(false);
|
||||
|
||||
BIN
static/nocodb.png
Normal file
BIN
static/nocodb.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Reference in New Issue
Block a user