diff --git a/apps/api/src/lib/common.ts b/apps/api/src/lib/common.ts index 69a77aa9a..bef297488 100644 --- a/apps/api/src/lib/common.ts +++ b/apps/api/src/lib/common.ts @@ -1920,3 +1920,4 @@ export function generateSecrets( } return envs; } + diff --git a/apps/client/package.json b/apps/client/package.json index b9fda399a..220c9b248 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -44,7 +44,10 @@ "daisyui": "2.41.0", "flowbite-svelte": "0.28.0", "js-cookie": "3.0.1", + "js-yaml": "4.1.0", + "p-limit": "4.0.0", "server": "workspace:*", - "superjson": "1.11.0" + "superjson": "1.11.0", + "svelte-select": "4.4.7" } } diff --git a/apps/client/src/app.d.ts b/apps/client/src/app.d.ts index 8f4d63895..b527fe7bd 100644 --- a/apps/client/src/app.d.ts +++ b/apps/client/src/app.d.ts @@ -7,3 +7,6 @@ declare namespace App { // interface Error {} // interface Platform {} } + +declare const GITPOD_WORKSPACE_URL: string; +declare const CODESANDBOX_HOST: string; diff --git a/apps/client/src/lib/common.ts b/apps/client/src/lib/common.ts index ab3b55370..168c36ed1 100644 --- a/apps/client/src/lib/common.ts +++ b/apps/client/src/lib/common.ts @@ -1,14 +1,17 @@ +import { dev } from '$app/environment'; import { addToast } from './store'; - +import Cookies from 'js-cookie'; export const asyncSleep = (delay: number) => new Promise((resolve) => setTimeout(resolve, delay)); export function errorNotification(error: any | { message: string }): void { if (error instanceof Error) { + console.error(error.message) addToast({ message: error.message, type: 'error' }); } else { + console.error(error) addToast({ message: error, type: 'error' @@ -18,3 +21,165 @@ export function errorNotification(error: any | { message: string }): void { export function getRndInteger(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1)) + min; } + +export function getDomain(domain: string) { + return domain?.replace('https://', '').replace('http://', ''); +} + +export const notNodeDeployments = ['php', 'docker', 'rust', 'python', 'deno', 'laravel', 'heroku']; +export const staticDeployments = [ + 'react', + 'vuejs', + 'static', + 'svelte', + 'gatsby', + 'php', + 'astro', + 'eleventy' +]; + +export function getAPIUrl() { + if (GITPOD_WORKSPACE_URL) { + const { href } = new URL(GITPOD_WORKSPACE_URL); + const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, ''); + return newURL; + } + if (CODESANDBOX_HOST) { + return `https://${CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`; + } + return dev ? `http://${window.location.hostname}:3001` : 'http://localhost:3000'; +} +export function getWebhookUrl(type: string) { + if (GITPOD_WORKSPACE_URL) { + const { href } = new URL(GITPOD_WORKSPACE_URL); + const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, ''); + if (type === 'github') { + return `${newURL}/webhooks/github/events`; + } + if (type === 'gitlab') { + return `${newURL}/webhooks/gitlab/events`; + } + } + if (CODESANDBOX_HOST) { + const newURL = `https://${CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`; + if (type === 'github') { + return `${newURL}/webhooks/github/events`; + } + if (type === 'gitlab') { + return `${newURL}/webhooks/gitlab/events`; + } + } + return `https://webhook.site/0e5beb2c-4e9b-40e2-a89e-32295e570c21/events`; +} + +async function send({ + method, + path, + data = null, + headers, + timeout = 120000 +}: { + method: string; + path: string; + data?: any; + headers?: any; + timeout?: number; +}): Promise> { + const token = Cookies.get('token'); + const controller = new AbortController(); + const id = setTimeout(() => controller.abort(), timeout); + const opts: any = { method, headers: {}, body: null, signal: controller.signal }; + if (data && Object.keys(data).length > 0) { + const parsedData = data; + for (const [key, value] of Object.entries(data)) { + if (value === '') { + parsedData[key] = null; + } + } + if (parsedData) { + opts.headers['Content-Type'] = 'application/json'; + opts.body = JSON.stringify(parsedData); + } + } + + if (headers) { + opts.headers = { + ...opts.headers, + ...headers + }; + } + if (token && !path.startsWith('https://')) { + opts.headers = { + ...opts.headers, + Authorization: `Bearer ${token}` + }; + } + if (!path.startsWith('https://')) { + path = `/api/v1${path}`; + } + + if (dev && !path.startsWith('https://')) { + path = `${getAPIUrl()}${path}`; + } + if (method === 'POST' && data && !opts.body) { + opts.body = data; + } + const response = await fetch(`${path}`, opts); + + clearTimeout(id); + + const contentType = response.headers.get('content-type'); + + let responseData = {}; + if (contentType) { + if (contentType?.indexOf('application/json') !== -1) { + responseData = await response.json(); + } else if (contentType?.indexOf('text/plain') !== -1) { + responseData = await response.text(); + } else { + return {}; + } + } else { + return {}; + } + if (!response.ok) { + if ( + response.status === 401 && + !path.startsWith('https://api.github') && + !path.includes('/v4/') + ) { + Cookies.remove('token'); + } + + throw responseData; + } + return responseData; +} + +export function get(path: string, headers?: Record): Promise> { + return send({ method: 'GET', path, headers }); +} + +export function del( + path: string, + data: Record, + headers?: Record +): Promise> { + return send({ method: 'DELETE', path, data, headers }); +} + +export function post( + path: string, + data: Record | FormData, + headers?: Record +): Promise> { + return send({ method: 'POST', path, data, headers }); +} + +export function put( + path: string, + data: Record, + headers?: Record +): Promise> { + return send({ method: 'PUT', path, data, headers }); +} diff --git a/apps/client/src/lib/components/Beta.svelte b/apps/client/src/lib/components/Beta.svelte new file mode 100644 index 000000000..279401fcf --- /dev/null +++ b/apps/client/src/lib/components/Beta.svelte @@ -0,0 +1 @@ + BETA \ No newline at end of file diff --git a/apps/client/src/lib/components/CopyPasswordField.svelte b/apps/client/src/lib/components/CopyPasswordField.svelte new file mode 100644 index 000000000..a0a474750 --- /dev/null +++ b/apps/client/src/lib/components/CopyPasswordField.svelte @@ -0,0 +1,156 @@ + + +
+ {#if !isPasswordField || showPassword} + {#if textarea} + + {:else} + + {/if} + {:else} + + {/if} + +
+
+ {#if isPasswordField} + +
(showPassword = !showPassword)}> + {#if showPassword} + + + + {:else} + + + + + {/if} +
+ {/if} + {#if value && isHttps} + +
+ + + + + +
+ {/if} +
+
+
diff --git a/apps/client/src/lib/components/Explainer.svelte b/apps/client/src/lib/components/Explainer.svelte new file mode 100644 index 000000000..924ce70d6 --- /dev/null +++ b/apps/client/src/lib/components/Explainer.svelte @@ -0,0 +1,38 @@ + + +
+ + + + + +
diff --git a/apps/client/src/lib/components/Setting.svelte b/apps/client/src/lib/components/Setting.svelte new file mode 100644 index 000000000..555323b37 --- /dev/null +++ b/apps/client/src/lib/components/Setting.svelte @@ -0,0 +1,87 @@ + + +
+
+ + +
+
+
+ +
+ Use setting + + + + +
+
+ +{#if dataTooltip} + {dataTooltip} +{/if} diff --git a/apps/client/src/lib/store.ts b/apps/client/src/lib/store.ts index 8e3687dca..77510ba95 100644 --- a/apps/client/src/lib/store.ts +++ b/apps/client/src/lib/store.ts @@ -21,7 +21,8 @@ export const trpc = createTRPCProxyClient({ }) ] }); - +export const disabledButton: Writable = writable(false); +export const location: Writable = writable(null) interface AppSession { isRegistrationEnabled: boolean; token?: string; @@ -139,3 +140,33 @@ export const status: Writable = writable({ isPublic: false } }); + +export function checkIfDeploymentEnabledApplications(isAdmin: boolean, application: any) { + return !!( + (isAdmin && application.buildPack === 'compose') || + ((application.fqdn || application.settings.isBot) && + ((application.gitSource && application.repository && application.buildPack) || + application.simpleDockerfile) && + application.destinationDocker) + ); +} +export const setLocation = (resource: any, settings?: any) => { + if (resource.settings.isBot && resource.exposePort) { + disabledButton.set(false); + return location.set(`http://${dev ? 'localhost' : settings.ipv4}:${resource.exposePort}`); + } + if (GITPOD_WORKSPACE_URL && resource.exposePort) { + const { href } = new URL(GITPOD_WORKSPACE_URL); + const newURL = href.replace('https://', `https://${resource.exposePort}-`).replace(/\/$/, ''); + return location.set(newURL); + } else if (CODESANDBOX_HOST) { + const newURL = `https://${CODESANDBOX_HOST.replace(/\$PORT/, resource.exposePort)}`; + return location.set(newURL); + } + if (resource.fqdn) { + return location.set(resource.fqdn); + } else { + location.set(null); + disabledButton.set(false); + } +}; diff --git a/apps/client/src/routes/+page.ts b/apps/client/src/routes/+page.ts index 4652e3d53..3465b727e 100644 --- a/apps/client/src/routes/+page.ts +++ b/apps/client/src/routes/+page.ts @@ -1,11 +1,8 @@ import { error } from '@sveltejs/kit'; import { trpc } from '$lib/store'; -import type { LayoutLoad } from './$types'; -import { redirect } from '@sveltejs/kit'; -import Cookies from 'js-cookie'; export const ssr = false; -export const load: LayoutLoad = async ({ url }) => { +export const load = async () => { try { return await trpc.dashboard.resources.query(); } catch (err) { diff --git a/apps/client/src/routes/applications/[id]/+page.svelte b/apps/client/src/routes/applications/[id]/+page.svelte index e69de29bb..0a431b054 100644 --- a/apps/client/src/routes/applications/[id]/+page.svelte +++ b/apps/client/src/routes/applications/[id]/+page.svelte @@ -0,0 +1,1228 @@ + + +
+
handleSubmit()}> +
+
+
General
+ {#if $appSession.isAdmin} + + {/if} +
+
+
+ + +
+ {#if !isSimpleDockerfile} +
+ + {#if isDisabled || application.settings?.isPublicRepository} + + {:else} + + {/if} +
+
+ + +
+
+ + {#if isDisabled || application.settings?.isPublicRepository} + + {:else} + + {/if} +
+ {/if} +
+ + {#if isDisabled} + + {:else} + + + {/if} +
+ {#if application.dockerRegistry?.id && application.gitSourceId} +
+ + +
+ {/if} + {#if !isSimpleDockerfile} +
+ + {#if isDisabled} + + {:else} + + + {/if} +
+ {/if} +
+ +
+ +
+
+ {#if application.buildPack !== 'compose'} +
+ changeSettings('isBot')} + title="Is your application a bot?" + description="You can deploy applications without domains or make them to listen on the Exposed Port.

Useful to host Twitch bots, regular jobs, or anything that does not require an incoming HTTP connection." + disabled={isDisabled} + /> +
+ {/if} + {#if !isBot && application.buildPack !== 'compose'} +
+ +
+ + {#if forceSave} +
+ {#if isNonWWWDomainOK} + + {:else} + + {/if} + {#if dualCerts} + {#if isWWWDomainOK} + + {:else} + + {/if} + {/if} +
+ {/if} +
+
+
+ !isDisabled && changeSettings('dualCerts')} + /> +
+ {#if isHttps && application.buildPack !== 'compose'} +
+ changeSettings('isCustomSSL')} + /> +
+ {/if} + {/if} +
+ {#if isSimpleDockerfile} +
+ Configuration +
+ +
+
+ +
+