wip: trpc
This commit is contained in:
@@ -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) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
118
apps/client/src/routes/applications/[id]/features/+page.svelte
Normal file
118
apps/client/src/routes/applications/[id]/features/+page.svelte
Normal file
@@ -0,0 +1,118 @@
|
||||
<script lang="ts">
|
||||
import type { PageParentData } from './$types';
|
||||
|
||||
export let data: PageParentData;
|
||||
const application = data.application.data;
|
||||
const settings = data.settings.data;
|
||||
import { page } from '$app/stores';
|
||||
const { id } = $page.params;
|
||||
import {
|
||||
addToast,
|
||||
appSession,
|
||||
checkIfDeploymentEnabledApplications,
|
||||
setLocation,
|
||||
status,
|
||||
isDeploymentEnabled,
|
||||
trpc
|
||||
} from '$lib/store';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import Setting from '$lib/components/Setting.svelte';
|
||||
|
||||
let previews = application.settings.previews;
|
||||
let dualCerts = application.settings.dualCerts;
|
||||
let autodeploy = application.settings.autodeploy;
|
||||
let isBot = application.settings.isBot;
|
||||
let isDBBranching = application.settings.isDBBranching;
|
||||
|
||||
async function changeSettings(name: any) {
|
||||
if (name === 'previews') {
|
||||
previews = !previews;
|
||||
}
|
||||
if (name === 'dualCerts') {
|
||||
dualCerts = !dualCerts;
|
||||
}
|
||||
if (name === 'autodeploy') {
|
||||
autodeploy = !autodeploy;
|
||||
}
|
||||
if (name === 'isBot') {
|
||||
if ($status.application.isRunning) return;
|
||||
isBot = !isBot;
|
||||
application.settings.isBot = isBot;
|
||||
application.fqdn = null;
|
||||
setLocation(application, settings);
|
||||
}
|
||||
if (name === 'isDBBranching') {
|
||||
isDBBranching = !isDBBranching;
|
||||
}
|
||||
try {
|
||||
await trpc.applications.saveSettings.mutate({
|
||||
id,
|
||||
previews,
|
||||
dualCerts,
|
||||
isBot,
|
||||
autodeploy,
|
||||
isDBBranching
|
||||
});
|
||||
|
||||
return addToast({
|
||||
message: 'Settings saved',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
if (name === 'previews') {
|
||||
previews = !previews;
|
||||
}
|
||||
if (name === 'dualCerts') {
|
||||
dualCerts = !dualCerts;
|
||||
}
|
||||
if (name === 'autodeploy') {
|
||||
autodeploy = !autodeploy;
|
||||
}
|
||||
if (name === 'isBot') {
|
||||
isBot = !isBot;
|
||||
}
|
||||
if (name === 'isDBBranching') {
|
||||
isDBBranching = !isDBBranching;
|
||||
}
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
<div class="mx-auto w-full">
|
||||
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
|
||||
<div class="title font-bold pb-3">Features</div>
|
||||
</div>
|
||||
<div class="px-4 lg:pb-10 pb-6">
|
||||
{#if !application.settings.isPublicRepository}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
id="autodeploy"
|
||||
isCenter={false}
|
||||
bind:setting={autodeploy}
|
||||
on:click={() => changeSettings('autodeploy')}
|
||||
title="Enable Automatic Deployment"
|
||||
description="Enable automatic deployment through webhooks."
|
||||
/>
|
||||
</div>
|
||||
{#if !application.settings.isBot && !application.simpleDockerfile}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
id="previews"
|
||||
isCenter={false}
|
||||
bind:setting={previews}
|
||||
on:click={() => changeSettings('previews')}
|
||||
title="Enable MR/PR Previews"
|
||||
description="Enable preview deployments from pull or merge requests."
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
No features available for this application
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
138
apps/client/src/routes/applications/[id]/secrets/+page.svelte
Normal file
138
apps/client/src/routes/applications/[id]/secrets/+page.svelte
Normal file
@@ -0,0 +1,138 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
let secrets = data.secrets;
|
||||
let previewSecrets = data.previewSecrets;
|
||||
const application = data.application.data;
|
||||
|
||||
import pLimit from 'p-limit';
|
||||
import { page } from '$app/stores';
|
||||
import { addToast, trpc } from '$lib/store';
|
||||
import Secret from './_components/Secret.svelte';
|
||||
import PreviewSecret from './_components/PreviewSecret.svelte';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
|
||||
const limit = pLimit(1);
|
||||
const { id } = $page.params;
|
||||
|
||||
let batchSecrets = '';
|
||||
async function refreshSecrets() {
|
||||
const { data } = await trpc.applications.getSecrets.query({ id });
|
||||
previewSecrets = [...data.previewSecrets];
|
||||
secrets = [...data.secrets];
|
||||
}
|
||||
async function getValues() {
|
||||
if (!batchSecrets) return;
|
||||
const eachValuePair = batchSecrets.split('\n');
|
||||
const batchSecretsPairs = eachValuePair
|
||||
.filter((secret) => !secret.startsWith('#') && secret)
|
||||
.map((secret) => {
|
||||
const [name, ...rest] = secret.split('=');
|
||||
const value = rest.join('=');
|
||||
return {
|
||||
name: name.trim(),
|
||||
value: value.trim(),
|
||||
createSecret: !secrets.find((secret: any) => name === secret.name)
|
||||
};
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
batchSecretsPairs.map(({ name, value, createSecret }) =>
|
||||
limit(async () => {
|
||||
try {
|
||||
if (!name || !value) return;
|
||||
if (createSecret) {
|
||||
await trpc.applications.newSecret.mutate({
|
||||
id,
|
||||
name,
|
||||
value
|
||||
});
|
||||
|
||||
addToast({
|
||||
message: 'Secret created.',
|
||||
type: 'success'
|
||||
});
|
||||
} else {
|
||||
await trpc.applications.updateSecret.mutate({
|
||||
id,
|
||||
name,
|
||||
value
|
||||
});
|
||||
|
||||
addToast({
|
||||
message: 'Secret updated.',
|
||||
type: 'success'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
batchSecrets = '';
|
||||
await refreshSecrets();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mx-auto w-full">
|
||||
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
|
||||
<div class="title font-bold pb-3">Secrets</div>
|
||||
</div>
|
||||
{#each secrets as secret, index}
|
||||
{#key secret.id}
|
||||
<Secret
|
||||
{index}
|
||||
length={secrets.length}
|
||||
name={secret.name}
|
||||
value={secret.value}
|
||||
isBuildSecret={secret.isBuildSecret}
|
||||
on:refresh={refreshSecrets}
|
||||
/>
|
||||
{/key}
|
||||
{/each}
|
||||
<div class="lg:pt-0 pt-10">
|
||||
<Secret on:refresh={refreshSecrets} length={secrets.length} isNewSecret />
|
||||
</div>
|
||||
{#if !application.settings.isBot && !application.simpleDockerfile}
|
||||
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
|
||||
<div class="title font-bold pb-3 pt-8">
|
||||
Preview Secrets <Explainer
|
||||
explanation="These values overwrite application secrets in PR/MR deployments. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if previewSecrets.length !== 0}
|
||||
{#each previewSecrets as secret, index}
|
||||
{#key index}
|
||||
<PreviewSecret
|
||||
{index}
|
||||
length={secrets.length}
|
||||
name={secret.name}
|
||||
value={secret.value}
|
||||
isBuildSecret={secret.isBuildSecret}
|
||||
on:refresh={refreshSecrets}
|
||||
/>
|
||||
{/key}
|
||||
{/each}
|
||||
{:else}
|
||||
Add secrets first to see Preview Secrets.
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
<form on:submit|preventDefault={getValues} class="mb-12 w-full">
|
||||
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2 pt-10">
|
||||
<div class="flex flex-row space-x-2">
|
||||
<div class="title font-bold pb-3 ">Paste <code>.env</code> file</div>
|
||||
<button type="submit" class="btn btn-sm bg-primary">Add Secrets in Batch</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
placeholder={`PORT=1337\nPASSWORD=supersecret`}
|
||||
bind:value={batchSecrets}
|
||||
class="mb-2 min-h-[200px] w-full"
|
||||
/>
|
||||
</form>
|
||||
16
apps/client/src/routes/applications/[id]/secrets/+page.ts
Normal file
16
apps/client/src/routes/applications/[id]/secrets/+page.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { trpc } from '$lib/store';
|
||||
import type { PageLoad } from './$types';
|
||||
export const ssr = false;
|
||||
|
||||
export const load: PageLoad = async ({ params }) => {
|
||||
try {
|
||||
const { id } = params;
|
||||
const { data } = await trpc.applications.getSecrets.query({ id });
|
||||
return data;
|
||||
} catch (err) {
|
||||
throw error(500, {
|
||||
message: 'An unexpected error occurred, please try again later.'
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
<script lang="ts">
|
||||
export let length = 0;
|
||||
export let index: number = 0;
|
||||
export let name = '';
|
||||
export let value = '';
|
||||
export let isBuildSecret = false;
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import { addToast, trpc } from '$lib/store';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const { id } = $page.params;
|
||||
|
||||
async function updatePreviewSecret() {
|
||||
try {
|
||||
await trpc.applications.updateSecret.mutate({
|
||||
id,
|
||||
name: name.trim(),
|
||||
value: value.trim(),
|
||||
isPreview: true
|
||||
});
|
||||
addToast({
|
||||
message: 'Secret updated.',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full grid grid-cols-1 lg:grid-cols-4 gap-2 pb-2">
|
||||
<div class="flex flex-col">
|
||||
{#if index === 0 || length === 0}
|
||||
<label for="name" class="pb-2 uppercase font-bold">name</label>
|
||||
{/if}
|
||||
|
||||
<input
|
||||
id="secretName"
|
||||
readonly
|
||||
disabled
|
||||
value={name}
|
||||
required
|
||||
placeholder="EXAMPLE_VARIABLE"
|
||||
class=" w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
{#if index === 0 || length === 0}
|
||||
<label for="value" class="pb-2 uppercase font-bold">value</label>
|
||||
{/if}
|
||||
|
||||
<CopyPasswordField
|
||||
id="secretValue"
|
||||
name="secretValue"
|
||||
isPasswordField={true}
|
||||
bind:value
|
||||
placeholder="J$#@UIO%HO#$U%H"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex lg:flex-col flex-row justify-start items-center pt-3 lg:pt-0">
|
||||
{#if index === 0 || length === 0}
|
||||
<label for="name" class="pb-2 uppercase lg:block hidden font-bold"
|
||||
>Need during buildtime?</label
|
||||
>
|
||||
{/if}
|
||||
<label for="name" class="pb-2 uppercase lg:hidden block font-bold">Need during buildtime?</label
|
||||
>
|
||||
|
||||
<div class="flex justify-center h-full items-center pt-0 lg:pt-0 pl-4 lg:pl-0">
|
||||
<button
|
||||
aria-pressed="false"
|
||||
class="opacity-50 cursor-pointer cursor-not-allowedrelative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out "
|
||||
class:bg-green-600={isBuildSecret}
|
||||
class:bg-stone-700={!isBuildSecret}
|
||||
>
|
||||
<span class="sr-only">Is build secret?</span>
|
||||
<span
|
||||
class="pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow transition duration-200 ease-in-out"
|
||||
class:translate-x-5={isBuildSecret}
|
||||
class:translate-x-0={!isBuildSecret}
|
||||
>
|
||||
<span
|
||||
class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
|
||||
class:opacity-0={isBuildSecret}
|
||||
class:opacity-100={!isBuildSecret}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg class="h-3 w-3 bg-white 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="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-100 ease-out"
|
||||
aria-hidden="true"
|
||||
class:opacity-100={isBuildSecret}
|
||||
class:opacity-0={!isBuildSecret}
|
||||
>
|
||||
<svg class="h-3 w-3 bg-white 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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row lg:flex-col lg:items-center items-start">
|
||||
{#if index === 0 || length === 0}
|
||||
<label for="name" class="pb-5 uppercase lg:block hidden font-bold" />
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-center h-full items-center pt-3">
|
||||
<div class="flex flex-row justify-center space-x-2">
|
||||
<div class="flex items-center justify-center">
|
||||
<button class="btn btn-sm btn-primary" on:click={updatePreviewSecret}>Update</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,193 @@
|
||||
<script lang="ts">
|
||||
export let length = 0;
|
||||
export let index: number = 0;
|
||||
export let name = '';
|
||||
export let value = '';
|
||||
export let isBuildSecret = false;
|
||||
export let isNewSecret = false;
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import { addToast, trpc } from '$lib/store';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const { id } = $page.params;
|
||||
function cleanupState() {
|
||||
if (isNewSecret) {
|
||||
name = '';
|
||||
value = '';
|
||||
isBuildSecret = false;
|
||||
}
|
||||
}
|
||||
async function removeSecret() {
|
||||
try {
|
||||
await trpc.applications.deleteSecret.mutate({ id, name });
|
||||
cleanupState();
|
||||
addToast({
|
||||
message: 'Secret removed.',
|
||||
type: 'success'
|
||||
});
|
||||
dispatch('refresh');
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function addNewSecret() {
|
||||
try {
|
||||
if (!name.trim()) return errorNotification({ message: 'Name is required.' });
|
||||
if (!value.trim()) return errorNotification({ message: 'Value is required.' });
|
||||
await trpc.applications.newSecret.mutate({
|
||||
id,
|
||||
name: name.trim(),
|
||||
value: value.trim(),
|
||||
isBuildSecret
|
||||
});
|
||||
cleanupState();
|
||||
addToast({
|
||||
message: 'Secret added.',
|
||||
type: 'success'
|
||||
});
|
||||
dispatch('refresh');
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSecret({
|
||||
changeIsBuildSecret = false
|
||||
}: { changeIsBuildSecret?: boolean } = {}) {
|
||||
if (changeIsBuildSecret) isBuildSecret = !isBuildSecret;
|
||||
if (isNewSecret) return;
|
||||
try {
|
||||
await trpc.applications.updateSecret.mutate({
|
||||
id,
|
||||
name: name.trim(),
|
||||
value: value.trim(),
|
||||
isBuildSecret,
|
||||
isPreview: false
|
||||
});
|
||||
addToast({
|
||||
message: 'Secret updated.',
|
||||
type: 'success'
|
||||
});
|
||||
dispatch('refresh');
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full grid grid-cols-1 lg:grid-cols-4 gap-2 pb-2">
|
||||
<div class="flex flex-col">
|
||||
{#if (index === 0 && !isNewSecret) || length === 0}
|
||||
<label for="name" class="pb-2 uppercase font-bold">name</label>
|
||||
{/if}
|
||||
|
||||
<input
|
||||
id={isNewSecret ? 'secretName' : 'secretNameNew'}
|
||||
bind:value={name}
|
||||
required
|
||||
placeholder="EXAMPLE_VARIABLE"
|
||||
readonly={!isNewSecret}
|
||||
class="w-full"
|
||||
class:bg-coolblack={!isNewSecret}
|
||||
class:border={!isNewSecret}
|
||||
class:border-dashed={!isNewSecret}
|
||||
class:border-coolgray-300={!isNewSecret}
|
||||
class:cursor-not-allowed={!isNewSecret}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
{#if (index === 0 && !isNewSecret) || length === 0}
|
||||
<label for="value" class="pb-2 uppercase font-bold">value</label>
|
||||
{/if}
|
||||
|
||||
<CopyPasswordField
|
||||
id={isNewSecret ? 'secretValue' : 'secretValueNew'}
|
||||
name={isNewSecret ? 'secretValue' : 'secretValueNew'}
|
||||
isPasswordField={true}
|
||||
bind:value
|
||||
placeholder="J$#@UIO%HO#$U%H"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex lg:flex-col flex-row justify-start items-center pt-3 lg:pt-0">
|
||||
{#if (index === 0 && !isNewSecret) || length === 0}
|
||||
<label for="name" class="pb-2 uppercase lg:block hidden font-bold"
|
||||
>Need during buildtime?</label
|
||||
>
|
||||
{/if}
|
||||
<label for="name" class="pb-2 uppercase lg:hidden block font-bold">Need during buildtime?</label
|
||||
>
|
||||
|
||||
<div class="flex justify-center h-full items-center pt-0 lg:pt-0 pl-4 lg:pl-0">
|
||||
<button
|
||||
on:click={() => updateSecret({ changeIsBuildSecret: true })}
|
||||
aria-pressed="false"
|
||||
class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out "
|
||||
class:bg-green-600={isBuildSecret}
|
||||
class:bg-stone-700={!isBuildSecret}
|
||||
>
|
||||
<span class="sr-only">Is build secret?</span>
|
||||
<span
|
||||
class="pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow transition duration-200 ease-in-out"
|
||||
class:translate-x-5={isBuildSecret}
|
||||
class:translate-x-0={!isBuildSecret}
|
||||
>
|
||||
<span
|
||||
class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
|
||||
class:opacity-0={isBuildSecret}
|
||||
class:opacity-100={!isBuildSecret}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg class="h-3 w-3 bg-white 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="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-100 ease-out"
|
||||
aria-hidden="true"
|
||||
class:opacity-100={isBuildSecret}
|
||||
class:opacity-0={!isBuildSecret}
|
||||
>
|
||||
<svg class="h-3 w-3 bg-white 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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row lg:flex-col lg:items-center items-start">
|
||||
{#if (index === 0 && !isNewSecret) || length === 0}
|
||||
<label for="name" class="pb-5 uppercase lg:block hidden font-bold" />
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-center h-full items-center pt-3">
|
||||
{#if isNewSecret}
|
||||
<div class="flex items-center justify-center">
|
||||
<button class="btn btn-sm btn-primary" on:click={addNewSecret}>Add</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row justify-center space-x-2">
|
||||
<div class="flex items-center justify-center">
|
||||
<button class="btn btn-sm btn-primary" on:click={() => updateSecret()}>Set</button>
|
||||
</div>
|
||||
<div class="flex justify-center items-end">
|
||||
<button class="btn btn-sm btn-error" on:click={removeSecret}>Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,78 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
const application = data.application.data;
|
||||
let persistentStorages = data.persistentStorages;
|
||||
import { page } from '$app/stores';
|
||||
import Storage from './components/Storage.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { trpc } from '$lib/store';
|
||||
|
||||
let composeJson: any = JSON.parse(application?.dockerComposeFile || '{}');
|
||||
let predefinedVolumes: any[] = [];
|
||||
if (composeJson?.services) {
|
||||
for (const [_, service] of Object.entries(composeJson.services)) {
|
||||
if (service?.volumes) {
|
||||
for (const [_, volumeName] of Object.entries(service.volumes)) {
|
||||
let [volume, target] = volumeName.split(':');
|
||||
if (volume === '.') {
|
||||
volume = target;
|
||||
}
|
||||
if (!target) {
|
||||
target = volume;
|
||||
volume = `${application.id}${volume.replace(/\//gi, '-').replace(/\./gi, '')}`;
|
||||
} else {
|
||||
volume = `${application.id}${volume.replace(/\//gi, '-').replace(/\./gi, '')}`;
|
||||
}
|
||||
predefinedVolumes.push({ id: volume, path: target, predefined: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const { id } = $page.params;
|
||||
async function refreshStorage() {
|
||||
const { data } = await trpc.applications.getStorages.query({ id });
|
||||
persistentStorages = [...data.persistentStorages];
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
<div class="mx-auto w-full">
|
||||
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
|
||||
<div class="title font-bold pb-3">Persistent Volumes</div>
|
||||
</div>
|
||||
{#if predefinedVolumes.length > 0}
|
||||
<div class="title">Predefined Volumes</div>
|
||||
<div class="w-full lg:px-0 px-4">
|
||||
<div class="grid grid-col-1 lg:grid-cols-2 py-2 gap-2">
|
||||
<div class="font-bold uppercase">Volume Id</div>
|
||||
<div class="font-bold uppercase">Mount Dir</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gap-4">
|
||||
{#each predefinedVolumes as storage}
|
||||
{#key storage.id}
|
||||
<Storage on:refresh={refreshStorage} {storage} />
|
||||
{/key}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if persistentStorages.length > 0}
|
||||
<div class="title" class:pt-10={predefinedVolumes.length > 0}>Custom Volumes</div>
|
||||
{/if}
|
||||
{#each persistentStorages as storage}
|
||||
{#key storage.id}
|
||||
<Storage on:refresh={refreshStorage} {storage} />
|
||||
{/key}
|
||||
{/each}
|
||||
<div class="Preview Secrets" class:pt-10={predefinedVolumes.length > 0}>
|
||||
Add New Volume <Explainer
|
||||
position="dropdown-bottom"
|
||||
explanation="You can specify any folder that you want to be persistent across deployments.<br><br><span class='text-settings '>/example</span> means it will preserve <span class='text-settings '>/example</span> between deployments.<br><br>Your application's data is copied to <span class='text-settings '>/app</span> inside the container, you can preserve data under it as well, like <span class='text-settings '>/app/db</span>.<br><br>This is useful for storing data such as a <span class='text-settings '>database (SQLite)</span> or a <span class='text-settings '>cache</span>."
|
||||
/>
|
||||
</div>
|
||||
<Storage on:refresh={refreshStorage} isNew />
|
||||
</div>
|
||||
</div>
|
||||
16
apps/client/src/routes/applications/[id]/storages/+page.ts
Normal file
16
apps/client/src/routes/applications/[id]/storages/+page.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { trpc } from '$lib/store';
|
||||
import type { PageLoad } from './$types';
|
||||
export const ssr = false;
|
||||
|
||||
export const load: PageLoad = async ({ params }) => {
|
||||
try {
|
||||
const { id } = params;
|
||||
const { data } = await trpc.applications.getStorages.query({ id });
|
||||
return data;
|
||||
} catch (err) {
|
||||
throw error(500, {
|
||||
message: 'An unexpected error occurred, please try again later.'
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
<script lang="ts">
|
||||
export let isNew = false;
|
||||
export let storage: any = {
|
||||
id: null,
|
||||
path: null
|
||||
};
|
||||
import { page } from '$app/stores';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { addToast, trpc } from '$lib/store';
|
||||
const { id } = $page.params;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
async function saveStorage(newStorage = false) {
|
||||
try {
|
||||
if (!storage.path) return errorNotification('Path is required');
|
||||
storage.path = storage.path.startsWith('/') ? storage.path : `/${storage.path}`;
|
||||
storage.path = storage.path.endsWith('/') ? storage.path.slice(0, -1) : storage.path;
|
||||
storage.path.replace(/\/\//g, '/');
|
||||
await trpc.applications.updateStorage.mutate({
|
||||
id,
|
||||
path: storage.path,
|
||||
storageId: storage.id,
|
||||
newStorage
|
||||
});
|
||||
|
||||
dispatch('refresh');
|
||||
if (isNew) {
|
||||
storage.path = null;
|
||||
storage.id = null;
|
||||
}
|
||||
if (newStorage) {
|
||||
addToast({
|
||||
message: 'Storage created',
|
||||
type: 'success'
|
||||
});
|
||||
} else {
|
||||
addToast({
|
||||
message: 'Storage updated',
|
||||
type: 'success'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function removeStorage() {
|
||||
try {
|
||||
await trpc.applications.deleteStorage.mutate({
|
||||
id,
|
||||
path: storage.path
|
||||
});
|
||||
dispatch('refresh');
|
||||
addToast({
|
||||
message: 'Storage removed',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full lg:px-0 px-4">
|
||||
{#if storage.predefined}
|
||||
<div class="flex flex-col lg:flex-row gap-4 pb-2">
|
||||
<input disabled readonly class="w-full" value={storage.id} />
|
||||
<input disabled readonly class="w-full" bind:value={storage.path} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex gap-4 pb-2" class:pt-8={isNew}>
|
||||
{#if storage.applicationId}
|
||||
{#if storage.oldPath}
|
||||
<input
|
||||
disabled
|
||||
readonly
|
||||
class="w-full"
|
||||
value="{storage.applicationId}{storage.path.replace(/\//gi, '-').replace('-app', '')}"
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
disabled
|
||||
readonly
|
||||
class="w-full"
|
||||
value="{storage.applicationId}{storage.path.replace(/\//gi, '-')}"
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
<input
|
||||
disabled={!isNew}
|
||||
readonly={!isNew}
|
||||
class="w-full"
|
||||
bind:value={storage.path}
|
||||
required
|
||||
placeholder="eg: /data"
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-center">
|
||||
{#if isNew}
|
||||
<div class="w-full lg:w-64">
|
||||
<button class="btn btn-sm btn-primary w-full" on:click={() => saveStorage(true)}
|
||||
>Add</button
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex justify-center">
|
||||
<button class="btn btn-sm btn-error" on:click={removeStorage}>Remove</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -2,6 +2,60 @@ import { goto } from '$app/navigation';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { trpc } from '$lib/store';
|
||||
|
||||
export async function saveForm() {
|
||||
return await trpc.applications.save.mutate();
|
||||
export async function saveForm(id, application, baseDatabaseBranch, dockerComposeConfiguration) {
|
||||
let {
|
||||
name,
|
||||
buildPack,
|
||||
fqdn,
|
||||
port,
|
||||
exposePort,
|
||||
installCommand,
|
||||
buildCommand,
|
||||
startCommand,
|
||||
baseDirectory,
|
||||
publishDirectory,
|
||||
pythonWSGI,
|
||||
pythonModule,
|
||||
pythonVariable,
|
||||
dockerFileLocation,
|
||||
denoMainFile,
|
||||
denoOptions,
|
||||
gitCommitHash,
|
||||
baseImage,
|
||||
baseBuildImage,
|
||||
deploymentType,
|
||||
dockerComposeFile,
|
||||
dockerComposeFileLocation,
|
||||
simpleDockerfile,
|
||||
dockerRegistryImageName
|
||||
} = application;
|
||||
return await trpc.applications.save.mutate({
|
||||
id,
|
||||
name,
|
||||
buildPack,
|
||||
fqdn,
|
||||
port,
|
||||
exposePort,
|
||||
installCommand,
|
||||
buildCommand,
|
||||
startCommand,
|
||||
baseDirectory,
|
||||
publishDirectory,
|
||||
pythonWSGI,
|
||||
pythonModule,
|
||||
pythonVariable,
|
||||
dockerFileLocation,
|
||||
denoMainFile,
|
||||
denoOptions,
|
||||
gitCommitHash,
|
||||
baseImage,
|
||||
baseBuildImage,
|
||||
deploymentType,
|
||||
dockerComposeFile,
|
||||
dockerComposeFileLocation,
|
||||
simpleDockerfile,
|
||||
dockerRegistryImageName,
|
||||
baseDatabaseBranch,
|
||||
dockerComposeConfiguration: JSON.stringify(dockerComposeConfiguration)
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user