feat: manual flow, fixes to repository flow

This commit is contained in:
Arman
2025-01-27 17:26:34 +01:00
parent 67ca09f8b6
commit e2e11e3d39
11 changed files with 622 additions and 212 deletions
+186
View File
@@ -0,0 +1,186 @@
import ignore from 'ignore';
export interface FileData {
path: string;
buffer: ArrayBuffer;
}
export async function processFileList(files: FileList): Promise<FileData[]> {
const fileArray = Array.from(files);
const ignorer = ignore();
// Check for .gitignore file
const gitignoreFile = fileArray.find(
(file) => file.webkitRelativePath.endsWith('.gitignore') || file.name === '.gitignore'
);
if (gitignoreFile) {
const gitignoreContent = await gitignoreFile.text();
ignorer.add(gitignoreContent);
} else {
ignorer.add(defaultIgnore);
}
// Filter files using ignorer
const filteredFiles = fileArray.filter((file) => {
const path = file.webkitRelativePath || file.name;
return !ignorer.ignores(path);
});
const filePromises = filteredFiles.map(async (file) => {
try {
const buffer = await file.arrayBuffer();
return {
path: file?.webkitRelativePath || file.name,
buffer: buffer
};
} catch (e) {
// console.log(file);
return null;
}
});
return Promise.all(filePromises);
}
export const defaultIgnore = `
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
`;
@@ -0,0 +1,181 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Button, Form } from '$lib/elements/forms';
import { Wizard } from '$lib/layout';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { Layout, Icon, Upload, Typography, Tooltip } from '@appwrite.io/pink-svelte';
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 type { Models } from '@appwrite.io/console';
import { processFileList } from '$lib/helpers/files';
import { createTarGzip } from 'nanotar';
export let data;
let showExitModal = false;
let formComponent: Form;
let isSubmitting = writable(false);
let name = '';
let id = '';
let framework: Models.Framework = data.frameworks.frameworks[0];
let adapter = framework?.adapters[0];
let installCommand = adapter?.installCommand;
let buildCommand = adapter?.buildCommand;
let outputDirectory = adapter?.outputDirectory;
let variables: Partial<Models.Variable>[] = [];
let files: FileList;
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);
}
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
undefined,
undefined,
undefined,
undefined,
undefined,
undefined
);
trackEvent(Submit.SiteCreate, {});
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) {
addNotification({
type: 'error',
message: e.message
});
trackError(e, Submit.SiteCreate);
}
}
</script>
<svelte:head>
<title>Create site - Appwrite</title>
</svelte:head>
<Wizard
title="Create site"
bind:showExitModal
href={`${base}/project-${$page.params.project}/sites/`}
confirmExit>
<Form bind:this={formComponent} onSubmit={create} bind:isSubmitting>
<Layout.Stack gap="xl">
<Upload.Dropzone folder bind:files on:change={handleChange}>
<Layout.Stack alignItems="center" gap="s">
<Layout.Stack
alignItems="center"
justifyContent="center"
direction="row"
gap="s">
<Typography.Text variant="l-500">
Drag and drop files here or click to upload
</Typography.Text>
<Tooltip>
<Icon icon={IconInfo} size="s" />
<svelte:fragment slot="tooltip">
Only PNG, JPEG, PDF files allowed
</svelte:fragment>
</Tooltip>
</Layout.Stack>
<Typography.Caption variant="400">Max file size: 10MB</Typography.Caption>
</Layout.Stack>
</Upload.Dropzone>
<Details bind:name bind:id />
<Configuration
bind:installCommand
bind:buildCommand
bind:outputDirectory
bind:selectedFramework={framework}
bind:variables
frameworks={data.frameworks.frameworks} />
<!-- <Domain /> -->
</Layout.Stack>
</Form>
<svelte:fragment slot="aside">
<Aside />
</svelte:fragment>
<svelte:fragment slot="footer">
<Button size="s" fullWidthMobile secondary on:click={() => (showExitModal = true)}>
Cancel
</Button>
<Button
size="s"
fullWidthMobile
on:click={() => formComponent.triggerSubmit()}
disabled={$isSubmitting}>
Deploy
</Button>
</svelte:fragment>
</Wizard>
@@ -0,0 +1,7 @@
export const load = async ({ parent }) => {
const { frameworks } = await parent();
return {
frameworks
};
};
@@ -0,0 +1,7 @@
<script lang="ts">
import { Card } from '$lib/components';
</script>
<Card padding="xs">
<slot />
</Card>
@@ -0,0 +1,224 @@
<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';
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}
<!-- <RawVariableEditor
{isGlobal}
{sdkCreateVariable}
{sdkUpdateVariable}
{sdkDeleteVariable}
{variableList}
bind:showEditor={showEditorModal} /> -->
{/if}
{#if showSecretModal}
<SecretVariableModal bind:show={showSecretModal} on:click={markAsSecret} />
{/if}
{#if showImportModal}
<ImportSiteVariablesModal bind:show={showImportModal} bind:variables />
{/if}
@@ -1,16 +1,16 @@
<script lang="ts">
import { Collapsible, CollapsibleItem, Empty } from '$lib/components';
import { Empty } from '$lib/components';
import { Button, InputSelect, InputText } from '$lib/elements/forms';
import {
Fieldset,
Layout,
Tag,
Popover,
Icon,
Table,
Badge,
HiddenText,
ActionMenu
ActionMenu,
Accordion
} from '@appwrite.io/pink-svelte';
import {
IconDotsHorizontal,
@@ -21,8 +21,8 @@
IconEyeOff,
IconPencil
} from '@appwrite.io/pink-icons-svelte';
import SecretVariableModal from './secretVariableModal.svelte';
import ImportSiteVariablesModal from './importSiteVariablesModal.svelte';
import SecretVariableModal from '../../secretVariableModal.svelte';
import ImportSiteVariablesModal from '../../importSiteVariablesModal.svelte';
import type { Models } from '@appwrite.io/console';
export let frameworks: Models.Framework[];
@@ -65,11 +65,8 @@
selectedFramework = frameworks.find((framework) => framework.key === frameworkId);
}} />
<Collapsible>
<CollapsibleItem>
<svelte:fragment slot="title">
Build settings <Tag size="s">Optional</Tag>
</svelte:fragment>
<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">
@@ -103,11 +100,11 @@
</Button>
</Layout.Stack>
</Layout.Stack>
</CollapsibleItem>
</Accordion>
<CollapsibleItem>
<Accordion title="Environment variables" badge="Optional" hideDivider>
<svelte:fragment slot="title">
Environment variables <Tag size="s">Optional</Tag>
Environment variables <Badge content="Optional" variant="secondary" />
</svelte:fragment>
<Layout.Stack gap="l">
<Layout.Stack gap="xl">
@@ -206,8 +203,8 @@
</Button>
</Layout.Stack>
</Layout.Stack>
</CollapsibleItem>
</Collapsible>
</Accordion>
</Layout.Stack>
</Layout.Stack>
</Fieldset>
@@ -6,7 +6,7 @@
export let show = false;
</script>
<Modal title="Secret variable" size="big" bind:show>
<Modal title="Secret variable" bind:show>
<Layout.Stack gap="l">
<p>
Secret variables are hidden from both the UI and API. Once a variable is marked as
@@ -1,6 +1,5 @@
<script lang="ts">
import { Card, LabelCard, Modal } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Card, Modal } from '$lib/components';
import { Image, Layout, Typography } from '@appwrite.io/pink-svelte';
import { base } from '$app/paths';
import { page } from '$app/stores';
@@ -12,11 +11,6 @@
import { Link } from '$lib/elements';
export let show = false;
let behaviour: 'repositories' | 'templates';
$: if (!show) {
behaviour = null;
}
</script>
<Modal title="Create site" bind:show hideFooter>
@@ -26,7 +20,7 @@
variant="secondary"
padding="xs"
radius="s"
href={`${base}/project-${$page.params.project}/sites/create-site/repositories`}>
href={`${base}/project-${$page.params.project}/sites/create-site/templates`}>
<Image
alt="repositories"
radius="xxs"
@@ -42,7 +36,7 @@
variant="secondary"
padding="xs"
radius="s"
href={`${base}/project-${$page.params.project}/sites/create-site/templates`}>
href={`${base}/project-${$page.params.project}/sites/create-site/repositories`}>
<Image
alt="repositories"
radius="xxs"
@@ -3,18 +3,13 @@
import { Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { processFileList } 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';
import { defaultIgnore } from './store';
import ignore from 'ignore';
interface FileData {
path: string;
buffer: ArrayBuffer;
}
export let show = false;
export let site: Models.Site;
@@ -57,44 +52,6 @@
console.log('Time taken to process files:', tock - tick);
}
async function processFileList(files: FileList): Promise<FileData[]> {
const fileArray = Array.from(files);
const ignorer = ignore();
// Check for .gitignore file
const gitignoreFile = fileArray.find(
(file) => file.webkitRelativePath.endsWith('.gitignore') || file.name === '.gitignore'
);
if (gitignoreFile) {
const gitignoreContent = await gitignoreFile.text();
ignorer.add(gitignoreContent);
} else {
ignorer.add(defaultIgnore);
}
// Filter files using ignorer
const filteredFiles = fileArray.filter((file) => {
const path = file.webkitRelativePath || file.name;
return !ignorer.ignores(path);
});
const filePromises = filteredFiles.map(async (file) => {
try {
const buffer = await file.arrayBuffer();
return {
path: file?.webkitRelativePath || file.name,
buffer: buffer
};
} catch (e) {
console.error(error);
console.log(file);
error = e;
}
});
return Promise.all(filePromises);
}
async function createDeployment() {
try {
uploader.uploadSiteDeployment(site.$id, uploadFile);
@@ -100,146 +100,3 @@ export const columns = writable<Column[]>([
format: 'datetime'
}
]);
export const defaultIgnore = `
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
`;