feat: centralize connect repo logic, functions domains

This commit is contained in:
Arman
2025-04-11 14:06:27 +02:00
parent 06801e4c4b
commit c0e5abe0c8
15 changed files with 474 additions and 602 deletions
@@ -4,32 +4,38 @@
import { Icon, Layout } from '@appwrite.io/pink-svelte';
import { installation, repository } from '$lib/stores/vcs';
import { sdk } from '$lib/stores/sdk';
import { createEventDispatcher, onMount } from 'svelte';
import { onMount } from 'svelte';
import { IconArrowSmRight } from '@appwrite.io/pink-icons-svelte';
import { Link } from '$lib/elements';
import { NewRepository, Repositories } from '$lib/components/git';
import ConnectGit from '$lib/components/git/connectGit.svelte';
import { Adapter, BuildRuntime, Framework, type Models } from '@appwrite.io/console';
import { addNotification } from '$lib/stores/notifications';
import { Click, trackEvent } from '$lib/actions/analytics';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import RepositoryBehaviour from '$lib/components/git/repositoryBehaviour.svelte';
const dispatch = createEventDispatcher();
let {
show = $bindable(false),
product,
callbackState = null,
onlyExisting = false,
connect = async () => {}
}: {
show?: boolean;
product: 'functions' | 'sites';
callbackState?: Record<string, string>;
onlyExisting?: boolean;
connect?: (installationId: string, repositoryId: string) => Promise<void>;
} = $props();
export let show = false;
export let site: Models.Site;
export let callbackState: Record<string, string> = null;
export let onlyExisting = false;
let repositoryBehaviour: 'new' | 'existing' | undefined = onlyExisting ? 'existing' : undefined;
let repositoryName = '';
let repositoryPrivate = true;
let selectedInstallationId = '';
let selectedRepository = '';
let installations = { installations: [], total: 0 };
let error = '';
let repositoryBehaviour: 'new' | 'existing' | undefined = $state(
onlyExisting ? 'existing' : undefined
);
let repositoryName = $state('');
let repositoryPrivate = $state(true);
let selectedInstallationId = $state('');
let selectedRepository = $state('');
let installations = $state({ installations: [], total: 0 });
let error = $state('');
onMount(async () => {
installations = await sdk.forProject.vcs.listInstallations();
@@ -40,7 +46,6 @@
if (!!installations?.total) {
repositoryBehaviour = 'existing';
}
console.log(selectedInstallationId);
});
async function connectRepo() {
@@ -55,29 +60,8 @@
selectedRepository = repo.id;
}
const s = await sdk.forProject.sites.update(
site.$id,
site.name,
site.framework as Framework,
site.enabled,
site.logging || undefined,
site.timeout,
site.installCommand,
site.buildCommand,
site.outputDirectory,
site.buildRuntime as BuildRuntime,
site.adapter as Adapter,
site.fallbackFile,
selectedInstallationId,
selectedRepository,
'main',
undefined,
undefined,
undefined
);
invalidate(Dependencies.SITE);
await connect(selectedInstallationId, selectedRepository);
show = false;
dispatch('connect', s);
addNotification({
type: 'success',
message: 'Repository connected successfully'
@@ -95,7 +79,8 @@
onSubmit={connectRepo}
bind:error>
<span slot="description">
Connect your site to an existing repository or create a new one.
Connect your {product === 'functions' ? 'function' : 'site'} to an existing repository or create
a new one.
</span>
{#if !!installations?.total}
<Layout.Stack gap="xl">
@@ -111,12 +96,12 @@
{:else}
<Repositories
bind:selectedRepository
product="sites"
{product}
action="button"
{callbackState}
connect={(e) => {
trackEvent(Click.ConnectRepositoryClick, {
from: 'sites'
from: product
});
repository.set(e);
repositoryName = e.name;
+1
View File
@@ -8,3 +8,4 @@ export { default as DeploymentDomains } from './deploymentDomains.svelte';
export { default as ConnectBehaviour } from './connectBehaviour.svelte';
export { default as ProductionBranchFieldset } from './productionBranchFieldset.svelte';
export { default as RepositoryCard } from './repositoryCard.svelte';
export { default as ConnectRepoModal } from './connectRepoModal.svelte';
@@ -20,7 +20,6 @@
import { VCSDetectionType, type Models } from '@appwrite.io/console';
import { getFrameworkIcon } from '$lib/stores/sites';
import { connectGitHub } from '$lib/stores/git';
import { debounce } from '$lib/helpers/debounce';
let {
action = $bindable('select'),
@@ -1,145 +0,0 @@
<script lang="ts">
import { Modal } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Icon, Layout } from '@appwrite.io/pink-svelte';
import { installation, repository } from '$lib/stores/vcs';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { IconArrowSmRight } from '@appwrite.io/pink-icons-svelte';
import { Link } from '$lib/elements';
import { NewRepository, Repositories } from '$lib/components/git';
import ConnectGit from '$lib/components/git/connectGit.svelte';
import { Runtime, type Models } from '@appwrite.io/console';
import { addNotification } from '$lib/stores/notifications';
import { Click, trackEvent } from '$lib/actions/analytics';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import RepositoryBehaviour from '$lib/components/git/repositoryBehaviour.svelte';
import { isValueOfStringEnum } from '$lib/helpers/types';
export let show = false;
export let func: Models.Function;
export let callbackState: Record<string, string> = null;
let repositoryBehaviour: 'new' | 'existing' | undefined = undefined;
let repositoryName = '';
let repositoryPrivate = true;
let selectedInstallationId = '';
let selectedRepository = '';
let installations = { installations: [], total: 0 };
let error = '';
onMount(async () => {
installations = await sdk.forProject.vcs.listInstallations();
if (!$installation?.$id && installations?.total) {
$installation = installations.installations[0];
}
selectedInstallationId = installations.total ? installations.installations[0]?.$id : '';
if (!!installations?.total) {
repositoryBehaviour = 'existing';
}
console.log(selectedInstallationId);
});
async function connectRepo() {
try {
if (repositoryBehaviour === 'new') {
const repo = await sdk.forProject.vcs.createRepository(
$installation.$id,
repositoryName,
repositoryPrivate
);
repository.set(repo);
selectedRepository = repo.id;
}
if (!isValueOfStringEnum(Runtime, func.runtime)) {
throw new Error(`Invalid runtime: ${func.runtime}`);
}
await sdk.forProject.functions.update(
func.$id,
func.name,
func.runtime,
func.execute || undefined,
func.events || undefined,
func.schedule || undefined,
func.timeout || undefined,
func.enabled || undefined,
func.logging || undefined,
func.entrypoint,
func.commands || undefined,
func.scopes || undefined,
selectedInstallationId,
selectedRepository,
'main',
undefined,
undefined,
undefined
);
await invalidate(Dependencies.FUNCTION);
show = false;
addNotification({
type: 'success',
message: 'Repository connected successfully'
});
} catch (e) {
error = e.message;
}
}
</script>
<Modal
title="Connect repository"
bind:show
hideFooter={!repositoryBehaviour}
onSubmit={connectRepo}
bind:error>
<span slot="description">
Connect your function to an existing repository or create a new one.
</span>
{#if !!installations?.total}
<Layout.Stack gap="xl">
<RepositoryBehaviour bind:repositoryBehaviour />
{#if repositoryBehaviour === 'new'}
<NewRepository
bind:repositoryName
bind:repositoryPrivate
bind:selectedInstallationId
{installations} />
{:else}
<Repositories
bind:selectedRepository
action="button"
{callbackState}
connect={(e) => {
trackEvent(Click.ConnectRepositoryClick, {
from: 'functions'
});
repository.set(e);
repositoryName = e.name;
selectedRepository = e.id;
connectRepo();
}} />
{/if}
</Layout.Stack>
{:else}
<ConnectGit {callbackState} />
{/if}
<svelte:fragment slot="footer">
{#if repositoryBehaviour === 'existing'}
<Layout.Stack>
<Link variant="quiet" href="#/">
<Layout.Stack direction="row" gap="xs">
Missing a repository? check your permissions <Icon
icon={IconArrowSmRight} />
</Layout.Stack>
</Link>
</Layout.Stack>
{:else if repositoryBehaviour === 'new'}
<Button text size="s" on:click={() => (show = false)}>Cancel</Button>
<Button size="s" submit disabled={!repositoryName || !$installation?.$id}>
Create
</Button>
{/if}
</svelte:fragment>
</Modal>
@@ -3,148 +3,40 @@
import { page } from '$app/state';
import { EmptySearch, PaginationWithLimit } from '$lib/components/index.js';
import { Button } from '$lib/elements/forms';
import Link from '$lib/elements/link.svelte';
import Container from '$lib/layout/container.svelte';
import { protocol } from '$routes/(console)/store.js';
import type { Models } from '@appwrite.io/console';
import {
IconDotsHorizontal,
IconExternalLink,
IconPlus,
IconRefresh,
IconTrash
} from '@appwrite.io/pink-icons-svelte';
import {
ActionMenu,
Card,
Empty,
Icon,
Layout,
Popover,
Table
} from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { Card, Empty, Icon, Layout } from '@appwrite.io/pink-svelte';
import DeleteDomainModal from './deleteDomainModal.svelte';
import RetryDomainModal from './retryDomainModal.svelte';
import SearchQuery from '$lib/components/searchQuery.svelte';
import { app } from '$lib/stores/app';
import { Click, trackEvent } from '$lib/actions/analytics';
import CreatePreviewDomainModal from './createPreviewDomainModal.svelte';
import Table from './table.svelte';
export let data;
let { data } = $props();
let showDelete = false;
let showRetry = false;
let showDelete = $state(false);
let showRetry = $state(false);
let selectedDomain: Models.ProxyRule = null;
let showPreviewDomainModal = false;
</script>
<Container>
<Layout.Stack direction="row" justifyContent="space-between">
<SearchQuery search={data.search} placeholder="Search domain" />
<Popover padding="none" let:toggle placement="bottom-end">
<Button
on:click={(event) => {
toggle(event);
trackEvent(Click.DomainCreateClick, {
source: 'functions_domain_overview'
});
}}>
<Icon icon={IconPlus} size="s" />
Add domain
</Button>
<svelte:fragment slot="tooltip">
<ActionMenu.Root>
<ActionMenu.Item.Anchor
href={`${base}/project-${page.params.project}/functions/function-${page.params.function}/domains/add-domain`}>
Custom domain
</ActionMenu.Item.Anchor>
<ActionMenu.Item.Button on:click={() => (showPreviewDomainModal = true)}>
Preview domain
</ActionMenu.Item.Button>
</ActionMenu.Root>
</svelte:fragment>
</Popover>
<SearchQuery placeholder="Search domain" />
<Button
href={`${base}/project-${page.params.project}/functions/function-${page.params.function}/domains/add-domain`}
on:click={() => {
trackEvent(Click.DomainCreateClick, {
source: 'functions_domain_overview'
});
}}>
<Icon icon={IconPlus} size="s" />
Add domain
</Button>
</Layout.Stack>
{#if data.domains.total}
<Table.Root
let:root
columns={[
{ id: 'domain', width: { min: 200 } },
{ id: 'redirect' },
{ id: 'branch' },
{ id: 'actions', width: 40 }
]}>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="domain" {root}>Domain</Table.Header.Cell>
<Table.Header.Cell column="redirect" {root}>Redirect to</Table.Header.Cell>
<Table.Header.Cell column="branch" {root}>Production branch</Table.Header.Cell>
<Table.Header.Cell column="actions" {root} />
</svelte:fragment>
{#each data.domains.rules as domain}
<Table.Row.Base {root}>
<Table.Cell column="domain" {root}>
<Link external href={`${$protocol}${domain.domain}`} variant="quiet">
<Layout.Stack direction="row" alignItems="center" gap="xs">
{domain.domain}
<Icon icon={IconExternalLink} size="s" />
</Layout.Stack>
</Link>
</Table.Cell>
<Table.Cell column="redirect" {root}>
{domain?.redirectUrl || 'No redirect'}
{domain?.redirectStatusCode ? `(${domain.redirectStatusCode})` : ''}
</Table.Cell>
<Table.Cell column="branch" {root}>
{domain.deploymentVcsProviderBranch || '-'}
</Table.Cell>
<Table.Cell column="actions" {root}>
<Popover let:toggle placement="bottom-start" padding="none">
<Button
text
icon
on:click={(e) => {
e.preventDefault();
toggle(e);
}}>
<Icon icon={IconDotsHorizontal} size="s" />
</Button>
<svelte:fragment slot="tooltip" let:toggle>
<ActionMenu.Root>
{#if domain.status !== 'verified'}
<ActionMenu.Item.Button
leadingIcon={IconRefresh}
on:click={(e) => {
e.preventDefault();
selectedDomain = domain;
showRetry = true;
toggle(e);
}}>
Retry
</ActionMenu.Item.Button>
{/if}
<ActionMenu.Item.Button
status="danger"
leadingIcon={IconTrash}
on:click={(e) => {
e.preventDefault();
selectedDomain = domain;
showDelete = true;
toggle(e);
trackEvent(Click.DomainDeleteClick, {
source: 'functions_domain_overview'
});
}}>
Delete
</ActionMenu.Item.Button>
</ActionMenu.Root>
</svelte:fragment>
</Popover>
</Table.Cell>
</Table.Row.Base>
{/each}
</Table.Root>
<Table domains={data.domains} />
<PaginationWithLimit
name="Domains"
@@ -197,7 +89,3 @@
{#if showRetry}
<RetryDomainModal bind:show={showRetry} {selectedDomain} />
{/if}
{#if showPreviewDomainModal}
<CreatePreviewDomainModal bind:show={showPreviewDomainModal} />
{/if}
@@ -1,26 +1,25 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import { Button, Form, InputSelect } from '$lib/elements/forms';
import { Button, Form, InputDomain, InputSelect, InputURL } from '$lib/elements/forms';
import { Wizard } from '$lib/layout';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { Fieldset, Layout, Tooltip, Icon, Alert } from '@appwrite.io/pink-svelte';
import { Fieldset, Layout, Tooltip, Icon, Input, Alert } from '@appwrite.io/pink-svelte';
import { goto, invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { sortBranches } from '$lib/stores/vcs';
import { organization } from '$lib/stores/organization';
import { consoleVariables } from '$routes/(console)/store';
import InputDomain from '$lib/elements/forms/inputDomain.svelte';
import { protocol } from '$routes/(console)/store';
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
import { LabelCard } from '$lib/components';
import { Runtime, StatusCode, type Models } from '@appwrite.io/console';
import { statusCodeOptions } from '$lib/stores/domains';
import { writable } from 'svelte/store';
import { isCloud } from '$lib/system';
import { onMount } from 'svelte';
import { StatusCode } from '@appwrite.io/console';
import ConnectRepoModal from '../../(modals)/connectRepoModal.svelte';
import { ConnectRepoModal } from '$lib/components/git/index.js';
import { isValueOfStringEnum } from '$lib/helpers/types.js';
const backPage = `${base}/project-${page.params.project}/functions/function-${page.params.function}/domains`;
const routeBase = `${base}/project-${page.params.project}/functions/function-${page.params.function}/domains`;
export let data;
@@ -28,42 +27,13 @@
let isSubmitting = writable(false);
let showConnectRepo = false;
let behaviour: 'REDIRECT' | 'CREATE' = 'CREATE';
let domain = '';
let behaviour: 'REDIRECT' | 'BRANCH' | 'ACTIVE' = 'ACTIVE';
let domainName = '';
let redirect: string = null;
let statusCode: number = null;
let statusCode = 307;
let branch = null;
const redirectOptions = data.domains.rules
.filter((d) => !d.domain.endsWith($consoleVariables._APP_DOMAIN_FUNCTIONS))
.map((domain) => ({
label: domain.domain,
value: domain.domain
}));
const statusCodeOptions = [
{
label: '301 Moved permanently',
value: 301
},
{
label: '302 Found',
value: 302
},
{
label: '303 See other',
value: 303
},
{
label: '307 Temporary redirect',
value: 307
},
{
label: '308 Permanent redirect',
value: 308
}
];
onMount(() => {
if (
page.url.searchParams.has('connectRepo') &&
@@ -71,75 +41,39 @@
) {
showConnectRepo = true;
}
if (page.url.searchParams.has('domain')) {
domainName = page.url.searchParams.get('domain');
}
});
async function addDomain() {
const isPreviewDomain = domain.endsWith($consoleVariables._APP_DOMAIN_FUNCTIONS);
const isNewDomain = data.domains.rules.findIndex((rule) => rule.domain === domain) === -1;
const isSubDomain = domain.split('.').length >= 2;
try {
if (behaviour === 'CREATE') {
if (isCloud && !isPreviewDomain) {
//Redirect if subdomain so user can choose how to proceed
if (isSubDomain) {
goto(
`${base}/project-${page.params.project}/functions/function-${page.params.function}/domains/add-domain/verify?domain=${domain}`
);
}
//Create domain if it's a new domain
else if (isNewDomain) {
const domainData = await sdk.forConsole.domains.create(
$organization.$id,
domain
);
await sdk.forProject.proxy.createFunctionRule(
domain,
page.params.function,
branch
);
goto(
`${base}/project-${page.params.project}/functions/function-${page.params.function}/domains/add-domain/verify-${domainData.$id}}`
);
}
}
//if selfhosted or preview domain create function rule
else {
await sdk.forProject.proxy.createFunctionRule(
domain,
page.params.function,
branch
);
addNotification({
type: 'success',
message: 'Domain added successfully'
});
await goto(backPage);
await invalidate(Dependencies.FUNCTION_DOMAINS);
}
let rule: Models.ProxyRule;
if (behaviour === 'BRANCH') {
rule = await sdk.forProject.proxy.createFunctionRule(
domainName,
page.params.function,
branch
);
} else if (behaviour === 'REDIRECT') {
if (isCloud && !isPreviewDomain) {
//Redirect if subdomain so user can choose how to proceed
if (isSubDomain) {
goto(
`${base}/project-${page.params.project}/functions/function-${page.params.function}/domains/add-domain/verify?domain=${domain}?redirect=${redirect}?statusCode=${statusCode}`
);
}
//Create domain if it's a new domain
else if (isNewDomain) {
const domainData = await sdk.forConsole.domains.create(
$organization.$id,
domain
);
const sc = Object.values(StatusCode).find(
(code) => parseInt(code) === statusCode
);
await sdk.forProject.proxy.createRedirectRule(domain, redirect, sc);
goto(
`${base}/project-${page.params.project}/functions/function-${page.params.function}/domains/add-domain/verify-${domainData.$id}}`
);
}
}
const sc = Object.values(StatusCode).find((code) => parseInt(code) === statusCode);
rule = await sdk.forProject.proxy.createRedirectRule(
domainName,
$protocol + redirect,
sc
);
} else if (behaviour === 'ACTIVE') {
rule = await sdk.forProject.proxy.createFunctionRule(
domainName,
page.params.function
);
}
if (rule?.status === 'verified') {
await goto(routeBase);
await invalidate(Dependencies.FUNCTION_DOMAINS);
} else {
await goto(`${routeBase}/add-domain/verify-${domainName}?rule=${rule.$id}`);
await invalidate(Dependencies.FUNCTION_DOMAINS);
}
} catch (error) {
addNotification({
@@ -148,12 +82,43 @@
});
}
}
async function connect(selectedInstallationId: string, selectedRepository: string) {
try {
if (!isValueOfStringEnum(Runtime, data.func.runtime)) {
throw new Error(`Invalid runtime: ${data.func.runtime}`);
}
await sdk.forProject.functions.update(
data.func.$id,
data.func.name,
data.func.runtime as Runtime,
data.func.execute || undefined,
data.func.events || undefined,
data.func.schedule || undefined,
data.func.timeout || undefined,
data.func.enabled || undefined,
data.func.logging || undefined,
data.func.entrypoint,
data.func.commands || undefined,
data.func.scopes || undefined,
selectedInstallationId,
selectedRepository,
'main',
undefined,
undefined,
undefined
);
await invalidate(Dependencies.FUNCTION);
} catch (error) {
console.log(error);
}
}
</script>
<Wizard title="Add custom domain" href={backPage} column columnSize="s">
<Wizard title="Add domain" href={routeBase} column columnSize="s" confirmExit>
<Form bind:this={formComponent} onSubmit={addDomain} bind:isSubmitting>
<Layout.Stack gap="xxl">
<Fieldset legend="Details">
<Fieldset legend="Domain">
<!-- <Input.ComboBox
label="Domain"
id="domain"
@@ -168,43 +133,47 @@
<InputDomain
label="Domain"
id="domain"
bind:value={domain}
bind:value={domainName}
required
placeholder="appwrite.example.com" />
</Fieldset>
<Layout.Grid columns={2} columnsXS={1}>
<LabelCard
value="CREATE"
bind:group={behaviour}
title="Connect to active deployment">
Connect this domain to your active deployment and configure the linked Git
branch as needed.
<Layout.Grid columns={3} columnsXS={1}>
<LabelCard value="ACTIVE" bind:group={behaviour} title="Active deployment">
Point this domain to the latest deployed version.
</LabelCard>
<LabelCard
value="REDIRECT"
bind:group={behaviour}
title="Redirect to another domain">
Automatically forward traffic from this domain to another URL of your choice.
<LabelCard value="BRANCH" bind:group={behaviour} title="Git branch">
Point this domain to a specific branch in your repository.
</LabelCard>
<LabelCard value="REDIRECT" bind:group={behaviour} title="Redirect">
Forward all traffic from this domain to another URL.
</LabelCard>
</Layout.Grid>
{#if behaviour === 'CREATE'}
<Fieldset legend="Configuration">
{#if behaviour === 'BRANCH'}
<Fieldset legend="Settings">
<Layout.Stack gap="xl">
{#if data.branches?.total}
{#if data.function?.providerRepositoryId}
{@const sortedBranches = sortBranches(data.branches.branches)}
{@const options = sortedBranches.map((branch) => ({
label: branch.name,
value: branch.name
}))}
<InputSelect
{options}
label="Production branch"
id="branch"
required
bind:value={branch}
placeholder="Select branch" />
<Layout.Stack gap="s">
<InputSelect
{options}
label="Production branch"
id="branch"
required
bind:value={branch}
placeholder="Select branch" />
{#if !data.branches?.total}
<Input.Helper state="default">
No branches found in the selected repository. Create a
branch to see it here.
</Input.Helper>
{/if}
</Layout.Stack>
{:else}
<InputSelect
disabled
@@ -214,7 +183,6 @@
required
value="main"
placeholder="Select branch" />
<Alert.Inline
title=" There is no repository connected to your function">
<Layout.Stack>
@@ -232,42 +200,20 @@
{/if}
</Layout.Stack>
</Fieldset>
<!-- {:else if data.function.installationId || data.installations?.total}
<PinkCard.Base padding="none" border="dashed">
<Empty
type="secondary"
title="Connect Git repository"
description="Link a repository to associate your domain with an active deployment.">
<svelte:fragment slot="actions">
<Button secondary on:click={() => (showConnectRepo = true)}>
<Icon icon={IconGithub} size="s" slot="start" />
Connect Git repository
</Button>
</svelte:fragment>
</Empty>
</PinkCard.Base>
{:else}
<ConnectGit callbackState={{ newInstallation: 'true' }} />
{/if} -->
{:else if behaviour === 'REDIRECT'}
<Fieldset legend="Configuration">
<Fieldset legend="Settings">
<Layout.Stack gap="xl">
<InputSelect
<InputURL
label="Redirect to"
id="redirect"
placeholder="Select domain"
options={redirectOptions}
placeholder="https://appwrite.io/docs"
bind:value={redirect}
required>
<Tooltip slot="info">
<Icon icon={IconInfo} size="s" />
<span slot="tooltip">
Redirect this domain. Domains added to your project will be
listed here.
</span>
<span slot="tooltip"> Redirect your domain to this URL.</span>
</Tooltip>
</InputSelect>
</InputURL>
<InputSelect
options={statusCodeOptions}
label="Status code"
@@ -280,8 +226,9 @@
{/if}
</Layout.Stack>
</Form>
<svelte:fragment slot="footer">
<Button secondary href={backPage}>Cancel</Button>
<Button secondary href={routeBase}>Cancel</Button>
<Button on:click={() => formComponent.triggerSubmit()} bind:disabled={$isSubmitting}>
Add
</Button>
@@ -291,6 +238,8 @@
{#if showConnectRepo}
<ConnectRepoModal
bind:show={showConnectRepo}
func={data.function}
{connect}
product="functions"
onlyExisting
callbackState={{ connectRepo: 'true' }} />
{/if}
@@ -1,104 +0,0 @@
<script lang="ts">
import { Modal } from '$lib/components';
import { Button, InputText } from '$lib/elements/forms';
import { sdk } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
import { invalidate } from '$app/navigation';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { Dependencies } from '$lib/constants';
import { consoleVariables } from '$routes/(console)/store';
import { page } from '$app/state';
import { Layout, Status, Typography } from '@appwrite.io/pink-svelte';
import { ConsoleResourceType, ID } from '@appwrite.io/console';
import { debounce } from '$lib/helpers/debounce';
export let show = false;
let baseDomain = ID.unique();
let domain = baseDomain;
let domainStatus: 'complete' | 'failed' | 'pending' = 'complete';
let error = null;
async function onSubmit() {
try {
await sdk.forProject.proxy.createFunctionRule(
`${domain}.${$consoleVariables._APP_DOMAIN_FUNCTIONS}`,
page.params.function
);
await invalidate(Dependencies.FUNCTION_DOMAINS);
show = false;
addNotification({
type: 'success',
message: 'Preview domain has been added'
});
trackEvent(Submit.DomainCreate);
} catch (e) {
error = e;
trackError(e, Submit.DomainCreate);
}
}
function setDomainLabel(status: typeof domainStatus) {
switch (status) {
case 'complete':
return 'Domain is available';
case 'failed':
return 'Domain is not available';
case 'pending':
return 'Checking domain availability';
}
}
const checkDomain = debounce(async (value: string) => {
if (!value) {
domainStatus = 'failed';
return;
}
try {
await sdk.forConsole.console.getResource(
`${value}.${$consoleVariables._APP_DOMAIN_FUNCTIONS}`,
ConsoleResourceType.Rules
);
domainStatus = 'complete';
baseDomain = domain;
} catch {
domainStatus = 'failed';
} finally {
baseDomain = domain;
}
}, 500);
$: if (domain !== baseDomain) {
domainStatus = 'pending';
checkDomain(domain);
}
$: if (!show) {
error = null;
}
</script>
<Modal title="Add preview domain" bind:show {onSubmit} bind:error>
<span slot="description">
Get an auto-generated domain to quickly access your project. You can customize its prefix.
</span>
<Layout.Stack>
<Layout.Stack gap="s">
<InputText id="domain" placeholder="my-domain" bind:value={domain}>
<svelte:fragment slot="end">
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
.{$consoleVariables._APP_DOMAIN_FUNCTIONS}
</Typography.Text>
</svelte:fragment>
</InputText>
<Status status={domainStatus} label={setDomainLabel(domainStatus)}></Status>
</Layout.Stack>
</Layout.Stack>
<svelte:fragment slot="footer">
<Button text on:click={() => (show = false)}>Cancel</Button>
<Button submit>Add</Button>
</svelte:fragment>
</Modal>
@@ -0,0 +1,19 @@
import type { Column } from '$lib/helpers/types';
import { writable } from 'svelte/store';
export const columns = writable<Column[]>([
{
id: 'domain',
title: 'Domain',
type: 'string',
format: 'string',
width: { min: 200 }
},
{
id: 'target',
title: 'Target',
type: 'string',
width: { min: 120, max: 400 }
}
]);
@@ -0,0 +1,121 @@
<script lang="ts">
import { Click, trackEvent } from '$lib/actions/analytics';
import { Link } from '$lib/elements';
import { Button } from '$lib/elements/forms';
import { protocol } from '$routes/(console)/store';
import type { Models } from '@appwrite.io/console';
import { IconDotsHorizontal, IconRefresh, IconTrash } from '@appwrite.io/pink-icons-svelte';
import {
ActionMenu,
Badge,
Icon,
Layout,
Popover,
Table,
Typography
} from '@appwrite.io/pink-svelte';
import DeleteDomainModal from './deleteDomainModal.svelte';
import RetryDomainModal from './retryDomainModal.svelte';
import { columns } from './store';
let {
domains
}: {
domains: Models.ProxyRuleList;
} = $props();
let showDelete = $state(false);
let showRetry = $state(false);
let selectedDomain: Models.ProxyRule = $state(null);
</script>
<Table.Root columns={[...$columns, { id: 'actions', width: 40 }]} let:root>
<svelte:fragment slot="header" let:root>
{#each $columns as { id, title }}
<Table.Header.Cell column={id} {root}>
{title}
</Table.Header.Cell>
{/each}
<Table.Header.Cell column="actions" {root} />
</svelte:fragment>
{#each domains.rules as domain}
<Table.Row.Base {root}>
{#each $columns as column}
<Table.Cell column={column.id} {root}>
{#if column.id === 'domain'}
<Link external href={`${$protocol}${domain.domain}`} variant="quiet" icon>
<Typography.Text truncate>
{domain.domain}
{#if domain.status !== 'verified'}
<Badge
variant="secondary"
type="error"
content="Verification failed"
size="s" />
{/if}
</Typography.Text>
</Link>
{:else if column.id === 'target'}
{domain?.redirectUrl
? 'Redirect to ' + domain.redirectUrl
: domain?.deploymentVcsProviderBranch
? 'Deployed from' + domain.deploymentVcsProviderBranch
: '-'}
{/if}
</Table.Cell>
{/each}
<Table.Cell column="actions" {root}>
<Layout.Stack direction="row" justifyContent="flex-end">
<Popover let:toggle placement="bottom-start" padding="none">
<Button
text
icon
on:click={(e) => {
e.preventDefault();
toggle(e);
}}>
<Icon icon={IconDotsHorizontal} size="s" />
</Button>
<svelte:fragment slot="tooltip" let:toggle>
<ActionMenu.Root>
{#if domain.status !== 'verified'}
<ActionMenu.Item.Button
leadingIcon={IconRefresh}
on:click={(e) => {
selectedDomain = domain;
showRetry = true;
toggle(e);
}}>
Retry
</ActionMenu.Item.Button>
{/if}
<ActionMenu.Item.Button
status="danger"
leadingIcon={IconTrash}
on:click={(e) => {
selectedDomain = domain;
showDelete = true;
toggle(e);
trackEvent(Click.DomainDeleteClick, {
source: 'sites_domain_overview'
});
}}>
Delete
</ActionMenu.Item.Button>
</ActionMenu.Root>
</svelte:fragment>
</Popover>
</Layout.Stack>
</Table.Cell>
</Table.Row.Base>
{/each}
</Table.Root>
{#if showDelete}
<DeleteDomainModal bind:show={showDelete} {selectedDomain} />
{/if}
{#if showRetry}
<RetryDomainModal bind:show={showRetry} {selectedDomain} />
{/if}
@@ -22,9 +22,8 @@
} from '@appwrite.io/pink-svelte';
import Card from '$lib/components/card.svelte';
import { IconGithub } from '@appwrite.io/pink-icons-svelte';
import { ConnectGit, RepositoryCard } from '$lib/components/git';
import { ConnectGit, ConnectRepoModal, RepositoryCard } from '$lib/components/git';
import { isValueOfStringEnum } from '$lib/helpers/types';
import ConnectRepoModal from '../(modals)/connectRepoModal.svelte';
export let func: Models.Function;
export let installations: Models.InstallationList;
@@ -114,6 +113,37 @@
selectedBranch !== func?.providerBranch ||
silentMode !== func?.providerSilentMode ||
selectedDir !== func?.providerRootDirectory;
async function connect(selectedInstallationId: string, selectedRepository: string) {
try {
if (!isValueOfStringEnum(Runtime, func.runtime)) {
throw new Error(`Invalid runtime: ${func.runtime}`);
}
await sdk.forProject.functions.update(
func.$id,
func.name,
func.runtime as Runtime,
func.execute || undefined,
func.events || undefined,
func.schedule || undefined,
func.timeout || undefined,
func.enabled || undefined,
func.logging || undefined,
func.entrypoint,
func.commands || undefined,
func.scopes || undefined,
selectedInstallationId,
selectedRepository,
'main',
undefined,
undefined,
undefined
);
await invalidate(Dependencies.FUNCTION);
} catch (error) {
console.log(error);
}
}
</script>
<Form onSubmit={updateConfiguration}>
@@ -224,7 +254,7 @@
</Form>
{#if showConnectRepo}
<ConnectRepoModal bind:show={showConnectRepo} {func} />
<ConnectRepoModal bind:show={showConnectRepo} {connect} product="functions" />
{/if}
{#if showDisconnect}
@@ -8,11 +8,15 @@
import Button from '$lib/elements/forms/button.svelte';
import OpenOnMobileModal from '../../(components)/openOnMobileModal.svelte';
import SiteCard from '../../(components)/siteCard.svelte';
import ConnectRepoModal from '../../(components)/connectRepoModal.svelte';
import { onMount } from 'svelte';
import AddCollaboratorModal from '../../(components)/addCollaboratorModal.svelte';
import { protocol } from '$routes/(console)/store';
import { Click, trackEvent } from '$lib/actions/analytics';
import { invalidate } from '$app/navigation';
import { sdk } from '$lib/stores/sdk';
import type { Adapter, BuildRuntime, Framework } from '@appwrite.io/console';
import { Dependencies } from '$lib/constants';
import { ConnectRepoModal } from '$lib/components/git';
export let data;
@@ -30,6 +34,34 @@
showConnectRepositry = true;
}
});
async function connect(selectedInstallationId: string, selectedRepository: string) {
try {
await sdk.forProject.sites.update(
data.site.$id,
data.site.name,
data.site.framework as Framework,
data.site.enabled,
data.site.logging || undefined,
data.site.timeout,
data.site.installCommand,
data.site.buildCommand,
data.site.outputDirectory,
data.site.buildRuntime as BuildRuntime,
data.site.adapter as Adapter,
data.site.fallbackFile,
selectedInstallationId,
selectedRepository,
'main',
undefined,
undefined,
undefined
);
invalidate(Dependencies.SITE);
} catch (error) {
console.log(error);
}
}
</script>
<Wizard column href={`${base}/project-${page.params.project}/sites/site-${data.site.$id}`}>
@@ -173,9 +205,11 @@
{#if showConnectRepositry}
<ConnectRepoModal
bind:show={showConnectRepositry}
site={data.site}
{connect}
product="sites"
callbackState={{ connectRepo: 'true' }} />
{/if}
{#if showOpenOnMobile}
<OpenOnMobileModal bind:show={showOpenOnMobile} proxyRuleList={data.proxyRuleList} />
{/if}
@@ -2,13 +2,12 @@
import { PaginationWithLimit, ViewSelector, EmptyFilter, Empty } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import { type Models } from '@appwrite.io/console';
import { Adapter, BuildRuntime, Framework, type Models } from '@appwrite.io/console';
import { View } from '$lib/helpers/load';
import { ActionMenu, Icon, Layout, Popover } from '@appwrite.io/pink-svelte';
import Table from './table.svelte';
import RedeployModal from '../../redeployModal.svelte';
import CreateGitDeploymentModal from './createGitDeploymentModal.svelte';
import ConnectRepoModal from '../../(components)/connectRepoModal.svelte';
import { columns } from './store';
import CreateManualDeploymentModal from './createManualDeploymentModal.svelte';
// import DeploymentMetrics from './deploymentMetrics.svelte';
@@ -20,6 +19,7 @@
import CreateCliModal from './createCliModal.svelte';
import { ParsedTagList, QuickFilters } from '$lib/components/filters';
import { page } from '$app/state';
import { ConnectRepoModal } from '$lib/components/git';
export let data;
@@ -42,6 +42,34 @@
}
});
});
async function connect(selectedInstallationId: string, selectedRepository: string) {
try {
await sdk.forProject.sites.update(
data.site.$id,
data.site.name,
data.site.framework as Framework,
data.site.enabled,
data.site.logging || undefined,
data.site.timeout,
data.site.installCommand,
data.site.buildCommand,
data.site.outputDirectory,
data.site.buildRuntime as BuildRuntime,
data.site.adapter as Adapter,
data.site.fallbackFile,
selectedInstallationId,
selectedRepository,
'main',
undefined,
undefined,
undefined
);
invalidate(Dependencies.SITE);
} catch (error) {
console.log(error);
}
}
</script>
<Container>
@@ -137,7 +165,8 @@
{#if showConnectRepo}
<ConnectRepoModal
bind:show={showConnectRepo}
site={data.site}
{connect}
product="sites"
callbackState={{ connectRepo: 'true' }} />
{/if}
@@ -12,11 +12,17 @@
import { protocol } from '$routes/(console)/store';
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
import { LabelCard } from '$lib/components';
import { StatusCode, type Models } from '@appwrite.io/console';
import {
Adapter,
BuildRuntime,
Framework,
StatusCode,
type Models
} from '@appwrite.io/console';
import { statusCodeOptions } from '$lib/stores/domains';
import ConnectRepoModal from '../../../(components)/connectRepoModal.svelte';
import { writable } from 'svelte/store';
import { onMount } from 'svelte';
import { ConnectRepoModal } from '$lib/components/git/index.js';
const routeBase = `${base}/project-${page.params.project}/sites/site-${page.params.site}/domains`;
@@ -78,6 +84,34 @@
});
}
}
async function connect(selectedInstallationId: string, selectedRepository: string) {
try {
await sdk.forProject.sites.update(
data.site.$id,
data.site.name,
data.site.framework as Framework,
data.site.enabled,
data.site.logging || undefined,
data.site.timeout,
data.site.installCommand,
data.site.buildCommand,
data.site.outputDirectory,
data.site.buildRuntime as BuildRuntime,
data.site.adapter as Adapter,
data.site.fallbackFile,
selectedInstallationId,
selectedRepository,
'main',
undefined,
undefined,
undefined
);
invalidate(Dependencies.SITE);
} catch (error) {
console.log(error);
}
}
</script>
<Wizard title="Add domain" href={routeBase} column columnSize="s" confirmExit>
@@ -202,7 +236,8 @@
{#if showConnectRepo}
<ConnectRepoModal
bind:show={showConnectRepo}
site={data.site}
{connect}
product="sites"
onlyExisting
callbackState={{ connectRepo: 'true' }} />
{/if}
@@ -18,11 +18,15 @@
import RetryDomainModal from './retryDomainModal.svelte';
import { columns } from './store';
export let domains: Models.ProxyRuleList;
let {
domains
}: {
domains: Models.ProxyRuleList;
} = $props();
let showDelete = false;
let showRetry = false;
let selectedDomain: Models.ProxyRule = null;
let showDelete = $state(false);
let showRetry = $state(false);
let selectedDomain: Models.ProxyRule = $state(null);
</script>
<Table.Root columns={[...$columns, { id: 'actions', width: 40 }]} let:root>
@@ -22,8 +22,7 @@
} from '@appwrite.io/pink-svelte';
import Card from '$lib/components/card.svelte';
import { IconGithub } from '@appwrite.io/pink-icons-svelte';
import { ConnectGit, RepositoryCard } from '$lib/components/git';
import ConnectRepoModal from '../../(components)/connectRepoModal.svelte';
import { ConnectGit, ConnectRepoModal, RepositoryCard } from '$lib/components/git';
import { showConnectRepo } from './store';
export let site: Models.Site;
@@ -104,6 +103,34 @@
selectedBranch = site?.providerBranch ?? branchesList.branches[0].name;
}
async function connect(selectedInstallationId: string, selectedRepository: string) {
try {
await sdk.forProject.sites.update(
site.$id,
site.name,
site.framework as Framework,
site.enabled,
site.logging || undefined,
site.timeout,
site.installCommand,
site.buildCommand,
site.outputDirectory,
site.buildRuntime as BuildRuntime,
site.adapter as Adapter,
site.fallbackFile,
selectedInstallationId,
selectedRepository,
'main',
undefined,
undefined,
undefined
);
invalidate(Dependencies.SITE);
} catch (error) {
console.log(error);
}
}
$: if (site?.installationId && site?.providerRepositoryId) {
getBranches(site.installationId, site.providerRepositoryId);
}
@@ -223,7 +250,7 @@
</Form>
{#if $showConnectRepo}
<ConnectRepoModal bind:show={$showConnectRepo} {site} />
<ConnectRepoModal bind:show={$showConnectRepo} {connect} product="sites" />
{/if}
{#if showDisconnect}