mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
feat: one click deployments for appwrite sites
This commit is contained in:
+365
@@ -0,0 +1,365 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { page as pageStore } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Card } from '$lib/components';
|
||||
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 {
|
||||
Fieldset,
|
||||
Layout,
|
||||
Icon,
|
||||
Typography,
|
||||
Input,
|
||||
Tag,
|
||||
Selector
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import { IconGithub, IconExternalLink, IconPencil } from '@appwrite.io/pink-icons-svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import Domain from '../domain.svelte';
|
||||
import { Adapter, BuildRuntime, Framework, ID } from '@appwrite.io/console';
|
||||
import { CustomId } from '$lib/components';
|
||||
import { getFrameworkIcon } from '$lib/stores/sites';
|
||||
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
|
||||
import { iconPath } from '$lib/stores/app';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let showExitModal = false;
|
||||
let showCustomId = false;
|
||||
let formComponent: Form;
|
||||
let isSubmitting = writable(false);
|
||||
|
||||
// Default values
|
||||
let name = data.repository.name;
|
||||
let id = ID.unique();
|
||||
let domain = data.repository.name.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
||||
let domainIsValid = true;
|
||||
// Get URL params
|
||||
const urlParams = new URLSearchParams($pageStore.url.search);
|
||||
const preset = urlParams.get('preset') || 'nextjs';
|
||||
|
||||
// Map preset string to Framework enum
|
||||
const presetMap = {
|
||||
nextjs: Framework.Nextjs,
|
||||
react: Framework.React,
|
||||
vue: Framework.Vue,
|
||||
nuxt: Framework.Nuxt,
|
||||
sveltekit: Framework.Sveltekit,
|
||||
astro: Framework.Astro,
|
||||
vite: Framework.Vite,
|
||||
other: Framework.Other
|
||||
};
|
||||
let framework = presetMap[preset.toLowerCase()] || Framework.Nextjs;
|
||||
let branch = '';
|
||||
let rootDir = '';
|
||||
let variables: Array<{ key: string; value: string; secret: boolean }> = [];
|
||||
|
||||
// Build configuration - use from URL params or defaults
|
||||
let installCommand = urlParams.get('install') || '';
|
||||
let buildCommand = urlParams.get('build') || '';
|
||||
let outputDirectory = urlParams.get('output') || '';
|
||||
|
||||
// Initialize environment variables from query params
|
||||
if (data.envKeys.length > 0) {
|
||||
variables = data.envKeys.map((key) => ({ key, value: '', secret: false }));
|
||||
}
|
||||
|
||||
// Framework options
|
||||
const frameworkOptions = [
|
||||
{ key: Framework.Nextjs, name: 'Next.js', buildRuntime: BuildRuntime.Node210 },
|
||||
{ key: Framework.React, name: 'React', buildRuntime: BuildRuntime.Node210 },
|
||||
{ key: Framework.Vue, name: 'Vue', buildRuntime: BuildRuntime.Node210 },
|
||||
{ key: Framework.Nuxt, name: 'Nuxt', buildRuntime: BuildRuntime.Node210 },
|
||||
{ key: Framework.Sveltekit, name: 'SvelteKit', buildRuntime: BuildRuntime.Node210 },
|
||||
{ key: Framework.Astro, name: 'Astro', buildRuntime: BuildRuntime.Node210 },
|
||||
{ key: Framework.Vite, name: 'Vite', buildRuntime: BuildRuntime.Node210 },
|
||||
{ key: Framework.Other, name: 'Other', buildRuntime: BuildRuntime.Static1 }
|
||||
];
|
||||
|
||||
$: selectedFramework = frameworkOptions.find((f) => f.key === framework) || frameworkOptions[0];
|
||||
|
||||
// Update build commands when framework changes (only if not provided via URL)
|
||||
const hasCustomCommands =
|
||||
urlParams.get('install') || urlParams.get('build') || urlParams.get('output');
|
||||
$: if (framework && data.frameworks && !hasCustomCommands) {
|
||||
const fw = data.frameworks.frameworks.find((f) => f.key === framework);
|
||||
if (fw && fw.adapters && fw.adapters.length > 0) {
|
||||
const adapter = fw.adapters[0];
|
||||
installCommand = adapter.installCommand || '';
|
||||
buildCommand = adapter.buildCommand || '';
|
||||
outputDirectory = adapter.outputDirectory || '';
|
||||
}
|
||||
}
|
||||
|
||||
async function create() {
|
||||
if (!domainIsValid) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: 'Domain is not valid'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Create site with build configuration
|
||||
let site = await sdk.forProject(page.params.region, page.params.project).sites.create(
|
||||
id || ID.unique(),
|
||||
name,
|
||||
framework,
|
||||
selectedFramework.buildRuntime,
|
||||
undefined, // enabled
|
||||
undefined, // logging
|
||||
undefined, // timeout
|
||||
installCommand || undefined,
|
||||
buildCommand || undefined,
|
||||
outputDirectory || undefined,
|
||||
framework === Framework.Other ? Adapter.Static : undefined, // adapter
|
||||
undefined, // installationId
|
||||
undefined, // fallbackFile
|
||||
undefined, // providerRepositoryId
|
||||
undefined, // branch
|
||||
false, // silentMode
|
||||
rootDir || undefined
|
||||
);
|
||||
|
||||
// Add domain
|
||||
await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.proxy.createSiteRule(
|
||||
`${domain}.${$regionalConsoleVariables._APP_DOMAIN_SITES}`,
|
||||
site.$id
|
||||
);
|
||||
|
||||
// Add variables
|
||||
const promises = variables.map((variable) =>
|
||||
sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.sites.createVariable(site.$id, variable.key, variable.value, variable.secret)
|
||||
);
|
||||
await Promise.all(promises);
|
||||
|
||||
// Fetch tags from GitHub API
|
||||
let latestTag = '';
|
||||
try {
|
||||
const tagsResponse = await fetch(
|
||||
`https://api.github.com/repos/${data.repository.owner}/${data.repository.name}/tags`
|
||||
);
|
||||
if (tagsResponse.ok) {
|
||||
const tags = await tagsResponse.json();
|
||||
if (tags.length > 0) {
|
||||
latestTag = tags[0].name;
|
||||
} else {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message:
|
||||
'No tags found in repository. Please create a tag before deploying.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: 'Failed to fetch tags from GitHub.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: 'Failed to fetch tags from GitHub: ' + error.message
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Create deployment from GitHub repository using the latest tag
|
||||
const deployment = await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.sites.createTemplateDeployment(
|
||||
site.$id,
|
||||
data.repository.name,
|
||||
data.repository.owner,
|
||||
rootDir || '.',
|
||||
latestTag,
|
||||
true
|
||||
);
|
||||
|
||||
trackEvent(Submit.SiteCreate, {
|
||||
source: 'deploy-button',
|
||||
framework: framework,
|
||||
repository: data.repository.url
|
||||
});
|
||||
|
||||
await goto(
|
||||
`${base}/project-${page.params.region}-${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>Deploy {data.repository.name} - Appwrite</title>
|
||||
</svelte:head>
|
||||
|
||||
<Wizard
|
||||
title="Deploy site"
|
||||
bind:showExitModal
|
||||
href={`${base}/project-${page.params.region}-${page.params.project}/sites/`}
|
||||
confirmExit>
|
||||
<Form bind:this={formComponent} onSubmit={create} bind:isSubmitting>
|
||||
<Layout.Stack gap="xl">
|
||||
<Card padding="s" radius="s">
|
||||
<Layout.Stack gap="m">
|
||||
<Typography.Text variant="m-500" color="--fgcolor-neutral-primary">
|
||||
Repository
|
||||
</Typography.Text>
|
||||
<Layout.Stack direction="row" alignItems="center" gap="s">
|
||||
<Icon icon={IconGithub} size="m" />
|
||||
<Typography.Text variant="m-400">
|
||||
{data.repository.owner}/{data.repository.name}
|
||||
</Typography.Text>
|
||||
<Button secondary size="s" external href={data.repository.url}>
|
||||
<Icon icon={IconExternalLink} slot="end" size="s" />
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Card>
|
||||
|
||||
<Fieldset legend="Details">
|
||||
<Layout.Stack gap="l">
|
||||
<Layout.Stack gap="s">
|
||||
<Input.Text
|
||||
label="Name"
|
||||
id="name"
|
||||
name="name"
|
||||
bind:value={name}
|
||||
required
|
||||
placeholder="Enter name" />
|
||||
{#if showCustomId}
|
||||
<CustomId bind:id bind:show={showCustomId} name="Site" />
|
||||
{:else}
|
||||
<div>
|
||||
<Tag size="s" on:click={() => (showCustomId = !showCustomId)}>
|
||||
<Icon icon={IconPencil} size="s" />
|
||||
Site ID
|
||||
</Tag>
|
||||
</div>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
<Input.Select
|
||||
id="framework"
|
||||
label="Framework"
|
||||
placeholder="Select framework"
|
||||
bind:value={framework}
|
||||
options={frameworkOptions.map((fw) => ({
|
||||
value: fw.key,
|
||||
label: fw.name,
|
||||
leadingHtml: `<img src='${$iconPath(getFrameworkIcon(fw.key), 'color')}' style='inline-size: var(--icon-size-m)' />`
|
||||
}))} />
|
||||
</Layout.Stack>
|
||||
</Fieldset>
|
||||
|
||||
<Fieldset legend="Git configuration">
|
||||
<Layout.Stack gap="m">
|
||||
<Input.Text
|
||||
label="Production branch"
|
||||
placeholder="Leave empty to use default branch"
|
||||
bind:value={branch} />
|
||||
<Input.Text label="Root directory" placeholder="./" bind:value={rootDir} />
|
||||
</Layout.Stack>
|
||||
</Fieldset>
|
||||
|
||||
<Fieldset legend="Build configuration">
|
||||
<Layout.Stack gap="m">
|
||||
<Input.Text
|
||||
label="Install command"
|
||||
placeholder={installCommand || 'npm install'}
|
||||
bind:value={installCommand} />
|
||||
<Input.Text
|
||||
label="Build command"
|
||||
placeholder={buildCommand || 'npm run build'}
|
||||
bind:value={buildCommand} />
|
||||
<Input.Text
|
||||
label="Output directory"
|
||||
placeholder={outputDirectory || 'dist'}
|
||||
bind:value={outputDirectory} />
|
||||
</Layout.Stack>
|
||||
</Fieldset>
|
||||
|
||||
{#if variables.length > 0}
|
||||
<Fieldset legend="Environment variables">
|
||||
<Layout.Stack gap="m">
|
||||
{#each variables as variable, i}
|
||||
<Layout.Stack direction="row" gap="s" alignItems="flex-end">
|
||||
<Input.Text
|
||||
label={i === 0 ? 'Key' : null}
|
||||
value={variable.key}
|
||||
readonly
|
||||
style="flex: 1" />
|
||||
<Input.Text
|
||||
label={i === 0 ? 'Value' : null}
|
||||
placeholder="Enter value"
|
||||
required
|
||||
bind:value={variable.value}
|
||||
style="flex: 2" />
|
||||
<div
|
||||
style="display: flex; flex-direction: column; gap: 0.25rem; min-width: 80px; align-items: center;">
|
||||
{#if i === 0}
|
||||
<span
|
||||
style="font-size: 0.875rem; color: var(--color-neutral-70);"
|
||||
>Secret</span>
|
||||
{/if}
|
||||
<div
|
||||
style="height: 2.5rem; display: flex; align-items: center; justify-content: center;">
|
||||
<Selector.Checkbox
|
||||
size="s"
|
||||
id="secret-{i}"
|
||||
bind:checked={variable.secret} />
|
||||
</div>
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
{/each}
|
||||
</Layout.Stack>
|
||||
</Fieldset>
|
||||
{/if}
|
||||
|
||||
<Domain bind:domain bind:domainIsValid />
|
||||
|
||||
{#if !data.installations?.total}
|
||||
<Card isDashed padding="xs">
|
||||
<Layout.Stack direction="row" gap="s" alignItems="center">
|
||||
<Icon icon={IconGithub} size="m" />
|
||||
<Typography.Text variant="m-400">
|
||||
Note: You can connect your GitHub account later to enable automatic
|
||||
deployments
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
</Card>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Form>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button fullWidthMobile size="s" secondary on:click={() => (showExitModal = true)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
fullWidthMobile
|
||||
size="s"
|
||||
on:click={() => formComponent.triggerSubmit()}
|
||||
disabled={$isSubmitting || !domainIsValid}>
|
||||
Deploy
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</Wizard>
|
||||
@@ -0,0 +1,50 @@
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { base } from '$app/paths';
|
||||
import type { PageLoad } from './$types';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export const load: PageLoad = async ({ url, params }) => {
|
||||
// Get repository URL from query params
|
||||
const repoUrl = url.searchParams.get('repo');
|
||||
if (!repoUrl) {
|
||||
redirect(302, `${base}/project-${params.region}-${params.project}/sites`);
|
||||
}
|
||||
|
||||
// Parse repository information
|
||||
const repoMatch = repoUrl.match(/github\.com[\/:]([^\/]+)\/([^\/\?\s]+)/);
|
||||
if (!repoMatch) {
|
||||
redirect(302, `${base}/project-${params.region}-${params.project}/sites`);
|
||||
}
|
||||
|
||||
const [, owner, repoName] = repoMatch;
|
||||
// Clean repository name (remove .git extension if present)
|
||||
const cleanRepoName = repoName.replace(/\.git$/, '');
|
||||
|
||||
// Get environment variables from query params
|
||||
const envParam = url.searchParams.get('env');
|
||||
const envKeys = envParam ? envParam.split(',').map((key: string) => key.trim()) : [];
|
||||
|
||||
// Try to fetch installations
|
||||
let installations: Models.InstallationList | null = null;
|
||||
try {
|
||||
installations = await sdk.forProject(params.region, params.project).vcs.listInstallations();
|
||||
} catch (error) {
|
||||
// If error, user might not have GitHub connected
|
||||
installations = null;
|
||||
}
|
||||
|
||||
// Fetch frameworks list
|
||||
const frameworks = await sdk.forProject(params.region, params.project).sites.listFrameworks();
|
||||
|
||||
return {
|
||||
repository: {
|
||||
url: repoUrl,
|
||||
owner,
|
||||
name: cleanRepoName
|
||||
},
|
||||
envKeys,
|
||||
installations,
|
||||
frameworks
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,263 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import CustomId from '$lib/components/customId.svelte';
|
||||
import { Button, Form, InputSelect } from '$lib/elements/forms';
|
||||
import type { AllowedRegions } from '$lib/sdk/billing.js';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { ID, Query, type Models, Region } from '@appwrite.io/console';
|
||||
import { IconGithub, IconPencil, IconPlusSm } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Card, Divider, Icon, Input, Layout, Tag, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { filterRegions } from '$lib/helpers/regions';
|
||||
import { loadAvailableRegions } from '$routes/(console)/regions';
|
||||
import { regions as regionsStore } from '$lib/stores/organization';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
let projects = $state<Models.ProjectList>();
|
||||
let selectedProject = $state<string>();
|
||||
let selectedOrg = $state(
|
||||
data?.organizations?.total ? data.organizations.teams[0].$id : undefined
|
||||
);
|
||||
let projectName = $state<string>();
|
||||
let showCustomId = $state(false);
|
||||
let region = $state<AllowedRegions>();
|
||||
let id = $state<string>();
|
||||
|
||||
async function fetchProjects() {
|
||||
projects = await sdk.forConsole.projects.list([
|
||||
Query.equal('teamId', selectedOrg),
|
||||
Query.orderDesc('')
|
||||
]);
|
||||
selectedProject = projects?.total ? projects.projects[0].$id : null;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (selectedProject === null) {
|
||||
try {
|
||||
const p = await sdk.forConsole.projects.create(
|
||||
id ?? ID.unique(),
|
||||
projectName,
|
||||
selectedOrg,
|
||||
isCloud ? (region as Region) : undefined
|
||||
);
|
||||
trackEvent(Submit.ProjectCreate, {
|
||||
customId: !!id,
|
||||
selectedOrg,
|
||||
teamId: selectedOrg,
|
||||
source: 'deploy-button'
|
||||
});
|
||||
|
||||
selectedProject = p.$id;
|
||||
|
||||
const deployUrl = buildDeployUrl(p);
|
||||
await goto(deployUrl);
|
||||
} catch (e) {
|
||||
trackError(e, Submit.ProjectCreate);
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: e.message
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const project = projects.projects.find((p) => p.$id === selectedProject);
|
||||
const deployUrl = buildDeployUrl(project);
|
||||
await goto(deployUrl);
|
||||
}
|
||||
}
|
||||
|
||||
function buildDeployUrl(project: Models.Project) {
|
||||
// Use the selected region or default to 'default' if not available
|
||||
const projectRegion = isCloud ? region : 'default';
|
||||
const url = new URL(
|
||||
`${base}/project-${projectRegion}-${project.$id}/sites/create-site/deploy`,
|
||||
window.location.origin
|
||||
);
|
||||
url.searchParams.set('repo', data.repository.url);
|
||||
if (data.envKeys.length > 0) {
|
||||
url.searchParams.set('env', data.envKeys.join(','));
|
||||
}
|
||||
// Pass through all the original URL params
|
||||
const currentUrl = new URL(window.location.href);
|
||||
const preset = currentUrl.searchParams.get('preset');
|
||||
const install = currentUrl.searchParams.get('install');
|
||||
const build = currentUrl.searchParams.get('build');
|
||||
const output = currentUrl.searchParams.get('output');
|
||||
|
||||
if (preset) url.searchParams.set('preset', preset);
|
||||
if (install) url.searchParams.set('install', install);
|
||||
if (build) url.searchParams.set('build', build);
|
||||
if (output) url.searchParams.set('output', output);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (selectedOrg !== undefined) {
|
||||
fetchProjects();
|
||||
}
|
||||
});
|
||||
|
||||
// Load regions when organization is selected
|
||||
$effect(() => {
|
||||
if (isCloud && selectedOrg) {
|
||||
loadAvailableRegions(selectedOrg);
|
||||
}
|
||||
});
|
||||
|
||||
// Set default region when regions are loaded
|
||||
$effect(() => {
|
||||
if (isCloud && $regionsStore.regions?.length > 0 && !region) {
|
||||
region = $regionsStore.regions.find((r) => r.default)?.$id as AllowedRegions;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Deploy {data.repository.name} - Appwrite</title>
|
||||
</svelte:head>
|
||||
|
||||
<div
|
||||
style="display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 2rem;">
|
||||
<div style="max-width: 480px; width: 100%;">
|
||||
<div style="text-align: center; margin-bottom: 2rem;">
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img
|
||||
src="/console/images/appwrite-logo-dark.svg"
|
||||
alt="Appwrite"
|
||||
width="160"
|
||||
height="30" />
|
||||
{:else}
|
||||
<img
|
||||
src="/console/images/appwrite-logo-light.svg"
|
||||
alt="Appwrite"
|
||||
width="160"
|
||||
height="30" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Card.Base padding="m" radius="l">
|
||||
<Layout.Stack gap="xl">
|
||||
<Layout.Stack gap="l">
|
||||
<Card.Base variant="secondary" padding="s" radius="s">
|
||||
<Layout.Stack gap="m">
|
||||
<Typography.Text variant="m-500" color="--fgcolor-neutral-primary">
|
||||
Repository
|
||||
</Typography.Text>
|
||||
<Layout.Stack direction="row" alignItems="center" gap="s">
|
||||
<Icon icon={IconGithub} size="m" />
|
||||
<Typography.Text variant="m-400">
|
||||
{data.repository.owner}/{data.repository.name}
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
{#if data.envKeys.length > 0}
|
||||
<Divider />
|
||||
<Layout.Stack gap="s">
|
||||
<Typography.Text
|
||||
variant="m-500"
|
||||
color="--fgcolor-neutral-primary">
|
||||
Environment Variables Required
|
||||
</Typography.Text>
|
||||
<Layout.Stack direction="row" gap="xs" wrap="wrap">
|
||||
{#each data.envKeys as envKey}
|
||||
<Tag size="s">{envKey}</Tag>
|
||||
{/each}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Card.Base>
|
||||
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Layout.Stack gap="xl">
|
||||
<InputSelect
|
||||
id="organization"
|
||||
label="Organization"
|
||||
required
|
||||
placeholder="Select an organization"
|
||||
options={data.organizations.teams.map((o: any) => ({
|
||||
label: o.name,
|
||||
value: o.$id
|
||||
}))}
|
||||
bind:value={selectedOrg} />
|
||||
|
||||
{#if projects?.total}
|
||||
{#key selectedProject}
|
||||
<InputSelect
|
||||
id="project"
|
||||
label="Project"
|
||||
required
|
||||
options={[
|
||||
...projects.projects.map((p) => ({
|
||||
label: p.name,
|
||||
value: p.$id
|
||||
})),
|
||||
{
|
||||
label: 'Create project',
|
||||
leadingIcon: IconPlusSm,
|
||||
value: null
|
||||
}
|
||||
]}
|
||||
bind:value={selectedProject} />
|
||||
{/key}
|
||||
{/if}
|
||||
{#if selectedProject === null}
|
||||
<Layout.Stack direction="column" gap="s">
|
||||
<Input.Text
|
||||
label="Name"
|
||||
placeholder="Project name"
|
||||
required
|
||||
bind:value={projectName} />
|
||||
{#if !showCustomId}
|
||||
<div>
|
||||
<Tag
|
||||
size="s"
|
||||
on:click={() => {
|
||||
showCustomId = true;
|
||||
}}
|
||||
><Icon slot="start" icon={IconPencil} size="s" /> Project
|
||||
ID</Tag>
|
||||
</div>
|
||||
{/if}
|
||||
<CustomId
|
||||
bind:show={showCustomId}
|
||||
name="Project"
|
||||
isProject
|
||||
bind:id />
|
||||
</Layout.Stack>
|
||||
{#if isCloud}
|
||||
<Layout.Stack gap="xs">
|
||||
<Input.Select
|
||||
required
|
||||
bind:value={region}
|
||||
placeholder="Select a region"
|
||||
options={filterRegions($regionsStore.regions || [])}
|
||||
label="Region" />
|
||||
<Typography.Text>
|
||||
Region cannot be changed after creation
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<Divider />
|
||||
<Layout.Stack direction="row-reverse">
|
||||
<div>
|
||||
<Button
|
||||
disabled={!selectedOrg ||
|
||||
(!selectedProject && !projectName && !region)}
|
||||
submit>
|
||||
<span class="text">Continue</span>
|
||||
</Button>
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Form>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Card.Base>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,90 @@
|
||||
import { sdk } from '$lib/stores/sdk.js';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { base } from '$app/paths';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { ID, type Models } from '@appwrite.io/console';
|
||||
import type { OrganizationList } from '$lib/stores/organization';
|
||||
import { redirectTo } from '$routes/store';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ parent, url }) => {
|
||||
const { account } = await parent();
|
||||
|
||||
// Store the full URL for redirect after auth
|
||||
const fullUrl = url.pathname + url.search;
|
||||
|
||||
// Check if user is authenticated
|
||||
if (!account) {
|
||||
redirectTo.set(fullUrl);
|
||||
redirect(302, base + '/login?redirect=' + encodeURIComponent(fullUrl));
|
||||
}
|
||||
|
||||
// Get repository URL from query params
|
||||
const repoUrl = url.searchParams.get('repo');
|
||||
if (!repoUrl) {
|
||||
redirect(302, base + '/');
|
||||
}
|
||||
|
||||
// Parse repository information
|
||||
const repoMatch = repoUrl.match(/github\.com[\/:]([^\/]+)\/([^\/\?\s]+)/);
|
||||
if (!repoMatch) {
|
||||
redirect(302, base + '/');
|
||||
}
|
||||
|
||||
const [, owner, repoName] = repoMatch;
|
||||
// Clean repository name (remove .git extension if present)
|
||||
const cleanRepoName = repoName.replace(/\.git$/, '');
|
||||
|
||||
// Get environment variables from query params
|
||||
const envParam = url.searchParams.get('env');
|
||||
const envKeys = envParam ? envParam.split(',').map((key: string) => key.trim()) : [];
|
||||
|
||||
// Get organizations
|
||||
let organizations: Models.TeamList<Record<string, unknown>> | OrganizationList | undefined;
|
||||
if (isCloud) {
|
||||
organizations = await sdk.forConsole.billing.listOrganization();
|
||||
} else {
|
||||
organizations = await sdk.forConsole.teams.list();
|
||||
}
|
||||
|
||||
// Create default organization if none exists - matches console's onboarding behavior
|
||||
if (!organizations?.total) {
|
||||
let org = null;
|
||||
try {
|
||||
if (isCloud) {
|
||||
org = await sdk.forConsole.billing.createOrganization(
|
||||
ID.unique(),
|
||||
'Personal Projects',
|
||||
BillingPlan.FREE,
|
||||
null,
|
||||
null
|
||||
);
|
||||
} else {
|
||||
org = await sdk.forConsole.teams.create(ID.unique(), 'Personal Projects');
|
||||
}
|
||||
|
||||
// Refetch organizations after creation
|
||||
if (isCloud) {
|
||||
organizations = await sdk.forConsole.billing.listOrganization();
|
||||
} else {
|
||||
organizations = await sdk.forConsole.teams.list();
|
||||
}
|
||||
} catch (e) {
|
||||
// If organization creation fails, still redirect to deploy page
|
||||
// The page will handle showing an error
|
||||
console.error('Failed to create default organization:', e);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
account,
|
||||
organizations,
|
||||
repository: {
|
||||
url: repoUrl,
|
||||
owner,
|
||||
name: cleanRepoName
|
||||
},
|
||||
envKeys
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user