Merge branch 'feat-pink-v2' of https://github.com/appwrite/console into feat-pink-v2

This commit is contained in:
Torsten Dittmann
2025-02-11 18:02:45 +01:00
11 changed files with 256 additions and 395 deletions
+1
View File
@@ -41,6 +41,7 @@
{/if}
<Card.Base padding="none" border="dashed">
<Empty
type="secondary"
title="No installation was added to the project yet"
description="Add an installation to connect repositories">
<svelte:fragment slot="actions">
+44
View File
@@ -1,4 +1,6 @@
import { addNotification } from '$lib/stores/notifications';
import ignore from 'ignore';
import { createTarGzip } from 'nanotar';
export interface FileData {
path: string;
@@ -42,6 +44,48 @@ export async function processFileList(files: FileList): Promise<FileData[]> {
return Promise.all(filePromises);
}
export async function gzipUpload(files: FileList) {
let uploadFile: File;
const tick = performance.now();
if (!files?.length) return;
// If the file is a tar.gz file, then return it as is
if (
files?.length === 1 &&
files.item(0).type === 'application/gzip' &&
files.item(0).name.split('.').pop() === 'tar'
) {
uploadFile = files.item(0);
}
// Else process the file to mantain the folder structure and create a .tar.gz file
else {
try {
const processedFiles = await processFileList(files);
const tar = await createTarGzip(
processedFiles.map((f) => ({
name: f.path,
data: f.buffer
}))
);
const blob = new Blob([tar], { type: 'application/gzip' });
const file = new File([blob], 'deployment.tar.gz', {
type: 'application/gzip'
});
uploadFile = file;
} catch (e) {
addNotification({
type: 'error',
message: e
});
}
}
console.log(uploadFile);
const tock = performance.now();
console.log('Time taken to process files:', tock - tick);
return uploadFile;
}
export const defaultIgnore = `
### Node ###
# Logs
+4
View File
@@ -117,4 +117,8 @@
max-height: 100dvh;
overflow-y: auto;
}
:globla(html) {
overflow-y: hidden;
}
</style>
@@ -1,5 +1,5 @@
<script lang="ts">
import { Button, InputChoice, InputPassword } from '$lib/elements/forms';
import { Button, InputPassword } from '$lib/elements/forms';
import { Modal } from '$lib/components';
import { InputText } from '$lib/elements/forms';
import { createEventDispatcher } from 'svelte';
@@ -7,7 +7,7 @@
import Alert from '$lib/components/alert.svelte';
import { project } from './store';
import { base } from '$app/paths';
import { Layout } from '@appwrite.io/pink-svelte';
import { Layout, Selector } from '@appwrite.io/pink-svelte';
export let isGlobal: boolean;
export let product: 'function' | 'site' = 'function';
@@ -81,8 +81,11 @@
placeholder="Enter value"
bind:value={pair.value}
minlength={0} />
<InputChoice id="secret" label="Secret" bind:value={pair.secret}>
If selected, you and your team won't be able to read the values after creation.</InputChoice>
<Selector.Checkbox
id="secret"
label="Secret"
bind:checked={pair.secret}
description="If selected, you and your team won't be able to read the values after creation." />
</Layout.Stack>
<svelte:fragment slot="footer">
<Button secondary on:click={close}>Cancel</Button>
@@ -21,12 +21,13 @@
IconEyeOff,
IconPencil
} from '@appwrite.io/pink-icons-svelte';
import SecretVariableModal from '../../secretVariableModal.svelte';
import ImportSiteVariablesModal from '../../importSiteVariablesModal.svelte';
import type { Models } from '@appwrite.io/console';
import { getFrameworkIcon } from '../../../store';
import { getFrameworkIcon } from '../store';
import { iconPath } from '$lib/stores/app';
import VariableEditorModal from '../../variableEditorModal.svelte';
import VariableEditorModal from './variableEditorModal.svelte';
import SecretVariableModal from './secretVariableModal.svelte';
import ImportSiteVariablesModal from './importSiteVariablesModal.svelte';
import CreateVariable from './createVariable.svelte';
export let frameworks: Models.Framework[];
export let selectedFramework: Models.Framework;
@@ -39,6 +40,7 @@
let showEditorModal = false;
let showImportModal = false;
let showSecretModal = false;
let showCreate = false;
let currentVariable: Partial<Models.Variable>;
let frameworkId = selectedFramework.key;
@@ -184,7 +186,8 @@
{/each}
</Table.Root>
{:else}
<Empty>Create variables to get started</Empty>
<Empty on:click={() => (showCreate = true)}
>Create variables to get started</Empty>
{/if}
</Layout.Stack>
<Layout.Stack direction="row">
@@ -202,7 +205,7 @@
<Icon icon={IconUpload} /> Import .env
</Button>
</Layout.Stack>
<Button secondary size="s" on:mousedown={() => (showImportModal = true)}>
<Button secondary size="s" on:mousedown={() => (showCreate = true)}>
<Icon icon={IconPlus} /> Create variable
</Button>
</Layout.Stack>
@@ -223,3 +226,7 @@
{#if showImportModal}
<ImportSiteVariablesModal bind:show={showImportModal} bind:variables />
{/if}
{#if showCreate}
<CreateVariable bind:show={showCreate} bind:variables />
{/if}
@@ -0,0 +1,74 @@
<script lang="ts">
import { Button, InputPassword } from '$lib/elements/forms';
import { Modal } from '$lib/components';
import { InputText } from '$lib/elements/forms';
import type { Models } from '@appwrite.io/console';
import { base } from '$app/paths';
import { Layout, Selector } from '@appwrite.io/pink-svelte';
import { Link } from '$lib/elements';
import { page } from '$app/stores';
export let show = false;
export let selectedVar: Partial<Models.Variable> = null;
export let variables: Partial<Models.Variable>[];
let pair = {
$id: selectedVar?.$id,
key: selectedVar?.key,
value: selectedVar?.value,
secret: selectedVar?.secret
};
function handleVariable() {
if (selectedVar) {
variables = variables.map((variable) => {
if (variable.$id === selectedVar.$id) {
return pair;
}
return variable;
});
} else {
variables = [...variables, pair];
}
show = false;
}
$: if (!show) {
selectedVar = null;
}
</script>
<Modal bind:show onSubmit={handleVariable} title={`${selectedVar ? 'Update' : 'Create'} variables`}>
<span slot="description">
Set the environment variables or secret that will be passed to your site. Global variables
can be set in <Link
variant="muted"
href={`${base}/project-${$page.params.project}/settings`}>project settings</Link
>.
</span>
<Layout.Stack>
<InputText
id="key"
label="Key"
placeholder="ENTER_KEY"
bind:value={pair.key}
required
autofocus
autocomplete={false} />
<InputPassword
id="value"
label="Value"
placeholder="Enter value"
bind:value={pair.value}
minlength={0} />
<Selector.Checkbox
id="secret"
label="Secret"
bind:checked={pair.secret}
description="If selected, you and your team won't be able to read the values after creation." />
</Layout.Stack>
<svelte:fragment slot="footer">
<Button secondary on:click={() => (show = false)}>Cancel</Button>
<Button submit>{selectedVar ? 'Update' : 'Create'}</Button>
</svelte:fragment>
</Modal>
@@ -11,12 +11,12 @@
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
import { writable } from 'svelte/store';
import Details from '../details.svelte';
import Configuration from './configuration.svelte';
import Aside from './aside.svelte';
import { BuildRuntime, Framework, ID, Query } from '@appwrite.io/console';
import { BuildRuntime, Framework, ID } from '@appwrite.io/console';
import type { Models } from '@appwrite.io/console';
import { processFileList } from '$lib/helpers/files';
import { createTarGzip } from 'nanotar';
import { gzipUpload } from '$lib/helpers/files';
import Configuration from '../configuration.svelte';
import Domain from '../domain.svelte';
export let data;
let showExitModal = false;
@@ -25,7 +25,9 @@
let isSubmitting = writable(false);
let name = '';
let id = '';
let id = ID.unique();
let domain = id;
let domainIsValid = true;
let framework: Models.Framework = data.frameworks.frameworks[0];
let adapter = framework?.adapters[0];
let installCommand = adapter?.installCommand;
@@ -36,40 +38,7 @@
let uploadFile: File;
async function handleChange() {
const tick = performance.now();
if (!files?.length) return;
// If the file is a tar.gz file, then return it as is
if (
files?.length === 1 &&
files.item(0).type === 'application/gzip' &&
files.item(0).name.split('.').pop() === 'tar'
) {
uploadFile = files.item(0);
}
// Else process the file to mantain the folder structure and create a .tar.gz file
else {
try {
const processedFiles = await processFileList(files);
const tar = await createTarGzip(
processedFiles.map((f) => ({
name: f.path,
data: f.buffer
}))
);
const blob = new Blob([tar], { type: 'application/gzip' });
const file = new File([blob], 'deployment.tar.gz', { type: 'application/gzip' });
uploadFile = file;
} catch (e) {
addNotification({
type: 'error',
message: e
});
}
}
console.log(uploadFile);
const tock = performance.now();
console.log('Time taken to process files:', tock - tick);
uploadFile = await gzipUpload(files);
}
async function create() {
@@ -85,9 +54,9 @@
buildRuntime,
undefined,
undefined,
installCommand,
buildCommand,
outputDirectory,
installCommand || undefined,
buildCommand || undefined,
outputDirectory || undefined,
undefined,
framework.adapters[Object.keys(framework.adapters)[0]].key, //TODO: fix this
undefined,
@@ -98,28 +67,31 @@
undefined
);
//Add variables
const promises = variables.map((variable) =>
sdk.forProject.sites.createVariable(
site.$id,
variable.key,
variable.value,
variable?.secret ?? false
)
);
await Promise.all(promises);
const deployment = await sdk.forProject.sites.createDeployment(
site.$id,
uploadFile,
true,
installCommand || undefined,
buildCommand || undefined,
outputDirectory || undefined
);
trackEvent(Submit.SiteCreate, {
source: 'manual',
framework: framework.key
});
//Add variables
await Promise.all(
Object.keys(variables).map(async (key) => {
await sdk.forProject.sites.createVariable(
site.$id,
key,
variables[key].value,
variables[key].secret
);
})
);
const { deployments } = await sdk.forProject.sites.listDeployments(site.$id, [
Query.limit(1)
]);
console.log(deployments);
const deployment = deployments[0];
await goto(
`${base}/project-${$page.params.project}/sites/create-site/deploying?site=${site.$id}&deployment=${deployment.$id}`
);
@@ -146,7 +118,10 @@
<Layout.Stack gap="xl">
<Layout.Stack gap="xxs">
<Label>Files or folder</Label>
<Upload.Dropzone folder bind:files on:change={handleChange}>
<Upload.Dropzone
bind:files
extensions={['tar', 'zip', 'gzip', 'gz']}
on:change={handleChange}>
<Layout.Stack alignItems="center" gap="s">
<Layout.Stack
alignItems="center"
@@ -159,7 +134,7 @@
<Tooltip>
<Icon icon={IconInfo} size="s" />
<svelte:fragment slot="tooltip">
Only PNG, JPEG, PDF files allowed
Only zipped files allowed
</svelte:fragment>
</Tooltip>
</Layout.Stack>
@@ -177,7 +152,7 @@
bind:variables
frameworks={data.frameworks.frameworks} />
<!-- <Domain /> -->
<Domain bind:domain bind:domainIsValid />
</Layout.Stack>
</Form>
<svelte:fragment slot="aside">
@@ -1,219 +0,0 @@
<script lang="ts">
import { Empty } from '$lib/components';
import { Button, InputSelect, InputText } from '$lib/elements/forms';
import {
Fieldset,
Layout,
Popover,
Icon,
Table,
Badge,
HiddenText,
ActionMenu,
Accordion
} from '@appwrite.io/pink-svelte';
import {
IconDotsHorizontal,
IconCode,
IconUpload,
IconPlus,
IconTrash,
IconEyeOff,
IconPencil
} from '@appwrite.io/pink-icons-svelte';
import SecretVariableModal from '../secretVariableModal.svelte';
import ImportSiteVariablesModal from '../importSiteVariablesModal.svelte';
import type { Models } from '@appwrite.io/console';
import VariableEditorModal from '../variableEditorModal.svelte';
export let frameworks: Models.Framework[];
export let selectedFramework: Models.Framework;
export let variables: Partial<Models.Variable>[] = [];
export let installCommand = '';
export let buildCommand = '';
export let outputDirectory = '';
let showEditorModal = false;
let showImportModal = false;
let showSecretModal = false;
let currentVariable: Partial<Models.Variable>;
let frameworkId = selectedFramework.key;
function markAsSecret() {
let variable = variables.find((v) => v.key === currentVariable.key);
if (variable) {
variable.secret = true;
}
}
$: frameworkData = frameworks.find((framework) => framework.key === selectedFramework.key);
</script>
<Fieldset legend="Configuration">
<Layout.Stack gap="l">
<InputSelect
id="framework"
label="Framework"
placeholder="Select framework"
bind:value={frameworkId}
options={frameworks.map((framework) => ({
value: framework.key,
label: framework.name
}))}
on:change={() => {
selectedFramework = frameworks.find((framework) => framework.key === frameworkId);
}} />
<Layout.Stack>
<Accordion title="Build settings" badge="Optional">
<Layout.Stack>
Set up how your project is built and where the output files are stored.
<Layout.Stack gap="s" direction="row" alignItems="flex-end">
<InputText
id="installCommand"
label="Install command"
bind:value={installCommand}
placeholder={frameworkData?.defaultInstallCommand} />
<Button secondary size="s" on:click={() => (installCommand = '')}>
Reset
</Button>
</Layout.Stack>
<Layout.Stack gap="s" direction="row" alignItems="flex-end">
<InputText
id="buildCommand"
label="Build command"
bind:value={buildCommand}
placeholder={frameworkData?.defaultBuildCommand} />
<Button secondary size="s" on:click={() => (buildCommand = '')}>
Reset
</Button>
</Layout.Stack>
<Layout.Stack gap="s" direction="row" alignItems="flex-end">
<InputText
id="outputDirectory"
label="Output directory"
bind:value={outputDirectory}
placeholder={frameworkData?.defaultOutputDirectory} />
<Button secondary size="s" on:click={() => (outputDirectory = '')}>
Reset
</Button>
</Layout.Stack>
</Layout.Stack>
</Accordion>
<Accordion title="Environment variables" badge="Optional" hideDivider>
<Layout.Stack gap="l">
<Layout.Stack gap="xl">
Set up environment variables to securely manage keys and settings for your
project.
{#if variables?.length}
<Table.Root>
<svelte:fragment slot="header">
<Table.Header.Cell width="200px">Key</Table.Header.Cell>
<Table.Header.Cell>Value</Table.Header.Cell>
<Table.Header.Cell width="10px"></Table.Header.Cell>
</svelte:fragment>
{#each variables as variable}
<Table.Row>
<Table.Cell>{variable.key}</Table.Cell>
<Table.Cell>
<!-- TODO: fix max width -->
<div style="max-width: 20rem">
{#if variable.secret}
<Badge content="Secret" variant="secondary" />
{:else}
<HiddenText
isVisible={false}
text={variable.value} />
{/if}
</div>
</Table.Cell>
<Table.Cell>
<div style="margin-inline-start: auto">
<Popover placement="bottom-end" let:toggle>
<Button
text
icon
on:click={(e) => {
e.preventDefault();
toggle(e);
}}>
<Icon size="s" icon={IconDotsHorizontal} />
</Button>
<svelte:fragment slot="tooltip">
<ActionMenu.Root>
<ActionMenu.Item.Button
trailingIcon={IconPencil}
on:click={() => {
showEditorModal = true;
}}>
Edit
</ActionMenu.Item.Button>
{#if !variable?.secret}
<ActionMenu.Item.Button
trailingIcon={IconEyeOff}
on:click={() => {
currentVariable = variable;
showSecretModal = true;
}}>
Secret
</ActionMenu.Item.Button>
{/if}
<ActionMenu.Item.Button
trailingIcon={IconTrash}
on:click={() => {
showImportModal = true;
}}>
Delete
</ActionMenu.Item.Button>
</ActionMenu.Root>
</svelte:fragment>
</Popover>
</div>
</Table.Cell>
</Table.Row>
{/each}
</Table.Root>
{:else}
<Empty>Create variables to get started</Empty>
{/if}
</Layout.Stack>
<Layout.Stack direction="row">
<Layout.Stack direction="row">
<Button
secondary
size="s"
on:mousedown={() => (showEditorModal = true)}>
<Icon icon={IconCode} /> Editor
</Button>
<Button
secondary
size="s"
on:mousedown={() => (showImportModal = true)}>
<Icon icon={IconUpload} /> Import .env
</Button>
</Layout.Stack>
<Button secondary size="s" on:mousedown={() => (showImportModal = true)}>
<Icon icon={IconPlus} /> Create variable
</Button>
</Layout.Stack>
</Layout.Stack>
</Accordion>
</Layout.Stack>
</Layout.Stack>
</Fieldset>
{#if showEditorModal}
<VariableEditorModal bind:variables bind:showEditor={showEditorModal} />
{/if}
{#if showSecretModal}
<SecretVariableModal bind:show={showSecretModal} on:click={markAsSecret} />
{/if}
{#if showImportModal}
<ImportSiteVariablesModal bind:show={showImportModal} bind:variables />
{/if}
@@ -14,11 +14,12 @@
import { writable } from 'svelte/store';
import Details from '../../details.svelte';
import ProductionBranch from '../../productionBranch.svelte';
import Configuration from './configuration.svelte';
import Aside from '../../aside.svelte';
import { BuildRuntime, Framework, ID, Query } from '@appwrite.io/console';
import type { Models } from '@appwrite.io/console';
import { onMount } from 'svelte';
import Configuration from '../../configuration.svelte';
import Domain from '../../domain.svelte';
export let data;
let showExitModal = false;
@@ -27,7 +28,7 @@
let isSubmitting = writable(false);
let name = '';
let id = '';
let id = ID.unique();
let framework: Models.Framework = data.frameworks.frameworks[0];
let adapter = framework?.adapters[0];
let branch: string;
@@ -37,6 +38,8 @@
let outputDirectory = adapter?.outputDirectory;
let variables: Partial<Models.Variable>[] = [];
let silentMode = false;
let domain = id;
let domainIsValid = true;
onMount(() => {
installation.set(data.installation);
@@ -60,64 +63,69 @@
}
async function create() {
try {
const fr = Object.values(Framework).find((f) => f === framework.key);
const buildRuntime = Object.values(BuildRuntime).find(
(f) => f === framework.buildRuntime
);
let site = await sdk.forProject.sites.create(
id || ID.unique(),
name,
fr,
buildRuntime,
undefined,
undefined,
installCommand,
buildCommand,
outputDirectory,
undefined,
framework.adapters[Object.keys(framework.adapters)[0]].key, //TODO: fix this
data.installation.$id,
null,
data.repository.id,
branch,
silentMode,
rootDir
);
trackEvent(Submit.SiteCreate, {
source: 'repository',
framework: framework.key
});
//Add variables
const promises = variables.map((variable) =>
sdk.forProject.sites.createVariable(
site.$id,
variable.key,
variable.value,
variable?.secret ?? false
)
);
await Promise.all(promises);
const { deployments } = await sdk.forProject.sites.listDeployments(site.$id, [
Query.limit(1)
]);
console.log(deployments);
const deployment = deployments[0];
await goto(
`${base}/project-${$page.params.project}/sites/create-site/deploying?site=${site.$id}&deployment=${deployment.$id}`
);
} catch (e) {
if (!domainIsValid) {
addNotification({
type: 'error',
message: e.message
message: 'Please enter a valid domain'
});
trackError(e, Submit.SiteCreate);
return;
} else {
try {
const fr = Object.values(Framework).find((f) => f === framework.key);
const buildRuntime = Object.values(BuildRuntime).find(
(f) => f === framework.buildRuntime
);
let site = await sdk.forProject.sites.create(
id || ID.unique(),
name,
fr,
buildRuntime,
undefined,
undefined,
installCommand,
buildCommand,
outputDirectory,
domain || undefined,
framework.adapters[Object.keys(framework.adapters)[0]].key, //TODO: fix this
data.installation.$id,
null,
data.repository.id,
branch,
silentMode,
rootDir
);
trackEvent(Submit.SiteCreate, {
source: 'repository',
framework: framework.key
});
//Add variables
const promises = variables.map((variable) =>
sdk.forProject.sites.createVariable(
site.$id,
variable.key,
variable.value,
variable?.secret ?? false
)
);
await Promise.all(promises);
const { deployments } = await sdk.forProject.sites.listDeployments(site.$id, [
Query.limit(1)
]);
const deployment = deployments[0];
await goto(
`${base}/project-${$page.params.project}/sites/create-site/deploying?site=${site.$id}&deployment=${deployment.$id}`
);
} catch (e) {
addNotification({
type: 'error',
message: e.message
});
trackError(e, Submit.SiteCreate);
}
}
}
$: console.log(variables);
</script>
<svelte:head>
@@ -179,7 +187,7 @@
bind:variables
frameworks={data.frameworks.frameworks} />
<!-- <Domain /> -->
<Domain bind:domain bind:domainIsValid />
</Layout.Stack>
</Form>
<svelte:fragment slot="aside">
@@ -187,11 +195,8 @@
</svelte:fragment>
<svelte:fragment slot="footer">
<Button size="s" fullWidthMobile secondary on:click={() => (showExitModal = true)}>
Cancel
</Button>
<Button fullWidthMobile secondary on:click={() => (showExitModal = true)}>Cancel</Button>
<Button
size="s"
fullWidthMobile
on:click={() => formComponent.triggerSubmit()}
disabled={$isSubmitting}>
@@ -3,7 +3,7 @@
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Card, SvgIcon } from '$lib/components';
import { Card } from '$lib/components';
import { Button, Form } from '$lib/elements/forms';
import { Wizard } from '$lib/layout';
import { addNotification } from '$lib/stores/notifications';
@@ -153,16 +153,15 @@
});
//Add variables
await Promise.all(
Object.keys(variables).map(async (key) => {
await sdk.forProject.sites.createVariable(
site.$id,
key,
variables[key].value,
variables[key].secret
);
})
const promises = variables.map((variable) =>
sdk.forProject.sites.createVariable(
site.$id,
variable.key,
variable.value,
variable?.secret ?? false
)
);
await Promise.all(promises);
const { deployments } = await sdk.forProject.sites.listDeployments(site.$id, [
Query.limit(1)
@@ -3,13 +3,12 @@
import { Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { processFileList } from '$lib/helpers/files';
import { gzipUpload } from '$lib/helpers/files';
import { addNotification } from '$lib/stores/notifications';
import { uploader } from '$lib/stores/uploader';
import type { Models } from '@appwrite.io/console';
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
import { Icon, Layout, Tooltip, Typography, Upload } from '@appwrite.io/pink-svelte';
import { createTarGzip } from 'nanotar';
export let show = false;
export let site: Models.Site;
@@ -19,39 +18,8 @@
let error: string = '';
async function handleChange() {
const tick = performance.now();
if (!files?.length) return;
// If the file is a tar.gz file, then return it as is
if (
files?.length === 1 &&
files.item(0).type === 'application/gzip' &&
files.item(0).name.split('.').pop() === 'tar'
) {
uploadFile = files.item(0);
}
// Else process the file to mantain the folder structure and create a .tar.gz file
else {
try {
const processedFiles = await processFileList(files);
const tar = await createTarGzip(
processedFiles.map((f) => ({
name: f.path,
data: f.buffer
}))
);
const blob = new Blob([tar], { type: 'application/gzip' });
const file = new File([blob], 'deployment.tar.gz', { type: 'application/gzip' });
uploadFile = file;
} catch (e) {
error = e;
}
}
console.log(uploadFile);
const tock = performance.now();
console.log('Time taken to process files:', tock - tick);
uploadFile = await gzipUpload(files);
}
async function createDeployment() {
try {
uploader.uploadSiteDeployment(site.$id, uploadFile);