Merge pull request #2586 from appwrite/add-domain-callbacks

This commit is contained in:
Darshan
2025-11-12 19:26:56 +05:30
committed by GitHub
6 changed files with 421 additions and 113 deletions
+3
View File
@@ -1,5 +1,8 @@
import { writable } from 'svelte/store';
import { StatusCode } from '@appwrite.io/console';
export const hideTypes = writable<boolean>(false);
export const statusCodeOptions = [
{
label: '301 Moved permanently',
+224
View File
@@ -0,0 +1,224 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Link } from '$lib/elements';
import { Button } from '$lib/elements/forms';
import { sdk, RuleType, DeploymentResourceType, RuleTrigger } from '$lib/stores/sdk';
import { Query, type Models } from '@appwrite.io/console';
import {
IconDotsHorizontal,
IconExternalLink,
IconPlus,
IconRefresh,
IconTrash
} from '@appwrite.io/pink-icons-svelte';
import {
ActionMenu,
Badge,
Icon,
Layout,
Popover,
Table,
Typography,
Skeleton,
Divider
} from '@appwrite.io/pink-svelte';
import { resolve } from '$app/paths';
import { Click, trackEvent } from '$lib/actions/analytics';
import { regionalProtocol } from '$routes/(console)/project-[region]-[project]/store';
import { goto } from '$app/navigation';
import DeleteDomainModal from '$routes/(console)/project-[region]-[project]/sites/site-[site]/domains/deleteDomainModal.svelte';
import RetryDomainModal from '$routes/(console)/project-[region]-[project]/sites/site-[site]/domains/retryDomainModal.svelte';
let {
siteId,
region,
projectId
}: {
siteId: string;
region: string;
projectId: string;
} = $props();
let loading = $state(true);
let showRetry = $state(false);
let showDelete = $state(false);
let selectedProxyRule: Models.ProxyRule = $state(null);
let proxyRules = $state<Models.ProxyRuleList | null>(null);
async function loadDomains() {
loading = true;
try {
proxyRules = await sdk.forProject(region, projectId).proxy.listRules({
queries: [
Query.equal('type', [RuleType.DEPLOYMENT, RuleType.REDIRECT]),
Query.equal('deploymentResourceType', DeploymentResourceType.SITE),
Query.equal('deploymentResourceId', siteId),
Query.equal('trigger', RuleTrigger.MANUAL),
Query.limit(100)
]
});
} catch (error) {
console.error('Failed to load domains:', error);
} finally {
loading = false;
}
}
let previousDeleteState = $state(false);
let previousRetryState = $state(false);
onMount(loadDomains);
$effect(() => {
const wasDeleteOpen = previousDeleteState && !showDelete;
const wasRetryOpen = previousRetryState && !showRetry;
if (wasDeleteOpen || wasRetryOpen) {
loadDomains();
}
previousDeleteState = showDelete;
previousDeleteState = showDelete;
previousRetryState = showRetry;
});
const addDomainUrl = $derived.by(() => {
const baseUrl = resolve(
'/(console)/project-[region]-[project]/sites/site-[site]/domains/add-domain',
{
region,
project: projectId,
site: siteId
}
);
return `${baseUrl}?types=false`;
});
</script>
<Table.Root columns={[{ id: 'domain' }, { id: 'actions', width: 40 }]} let:root>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="domain" {root}>Domain</Table.Header.Cell>
<Table.Header.Cell column="actions" {root} />
</svelte:fragment>
{#if loading}
{#each Array(2) as _}
<Table.Row.Base {root}>
<Table.Cell column="domain" {root}>
<Layout.Stack direction="row" gap="xs" alignItems="center">
<Skeleton variant="line" width={200} height={20} />
</Layout.Stack>
</Table.Cell>
<Table.Cell column="actions" {root}>
<Layout.Stack direction="row" justifyContent="flex-end">
<Skeleton variant="line" width={24} height={12} />
</Layout.Stack>
</Table.Cell>
</Table.Row.Base>
{/each}
{:else if proxyRules && proxyRules.total > 0}
{#each proxyRules.rules as rule}
<Table.Row.Base {root}>
<Table.Cell column="domain" {root}>
<Layout.Stack direction="row" gap="xs" alignItems="center">
<Link external variant="quiet" href={`${$regionalProtocol}${rule.domain}`}>
<Layout.Stack direction="row" gap="xxs" alignItems="center">
<Typography.Text truncate>
{rule.domain}
</Typography.Text>
<Icon size="xs" icon={IconExternalLink} />
</Layout.Stack>
</Link>
{#if rule.status === 'verifying'}
<Badge variant="secondary" content="Verifying" size="s" />
{:else if rule.status !== 'verified'}
<Badge
size="s"
type="warning"
variant="secondary"
content="Verification failed" />
{/if}
</Layout.Stack>
</Table.Cell>
<Table.Cell column="actions" {root}>
<Popover let:toggle padding="none">
<Button
text
icon
on:click={(e) => {
e.preventDefault();
toggle(e);
}}>
<Icon icon={IconDotsHorizontal} size="s" />
</Button>
<svelte:fragment slot="tooltip" let:toggle>
{@render domainActions(rule, toggle)}
</svelte:fragment>
</Popover>
</Table.Cell>
</Table.Row.Base>
{/each}
{/if}
</Table.Root>
<Layout.Stack style="width: min-content;">
<Button compact on:onclick={async () => await goto(addDomainUrl)}>
<Icon icon={IconPlus} size="s" />
Add domain
</Button>
</Layout.Stack>
{#if showDelete}
<DeleteDomainModal bind:show={showDelete} {selectedProxyRule} />
{/if}
{#if showRetry}
<RetryDomainModal bind:show={showRetry} {selectedProxyRule} />
{/if}
{#snippet domainActions(rule: Models.ProxyRule, toggle: () => void)}
<ActionMenu.Root>
<ActionMenu.Item.Anchor href={`${$regionalProtocol}${rule.domain}`} external>
Open domain
</ActionMenu.Item.Anchor>
{#if rule.status !== 'verified' && rule.status !== 'verifying'}
<ActionMenu.Item.Button
leadingIcon={IconRefresh}
on:click={() => {
selectedProxyRule = rule;
showRetry = true;
toggle();
}}>
Retry
</ActionMenu.Item.Button>
<div class="action-menu-divider">
<Divider />
</div>
{/if}
<ActionMenu.Item.Button
status="danger"
leadingIcon={IconTrash}
on:click={() => {
selectedProxyRule = rule;
showDelete = true;
toggle();
trackEvent(Click.DomainDeleteClick, {
source: 'studio_manage_domains'
});
}}>
Delete
</ActionMenu.Item.Button>
</ActionMenu.Root>
{/snippet}
<style>
.action-menu-divider {
margin-inline: -1rem;
padding-block-end: 0.25rem;
padding-block-start: 0.25rem;
}
</style>
+5 -1
View File
@@ -278,7 +278,11 @@ export function hideStudio() {
export async function initImagine(
region: string,
projectId: string,
callbacks?: { onProjectNameChange?: (name: string) => void }
callbacks?: {
onProjectNameChange: () => void;
onAddDomain: () => void | Promise<void>;
onManageDomains: (primaryDomain?: string) => void | Promise<void>;
}
) {
try {
const { initImagineConfig, initImagineRouting } = await getWebComponents();
+60 -3
View File
@@ -5,12 +5,28 @@
<script lang="ts">
import './shim.css';
import { onMount } from 'svelte';
import { ensureStudioComponent, initImagine, getWebComponents } from './studio-widget';
import { resolve } from '$app/paths';
import { Link } from '$lib/elements';
import { app } from '$lib/stores/app';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { goto, invalidate } from '$app/navigation';
import { IconExternalLink } from '@appwrite.io/pink-icons-svelte';
import { Layout, Typography, Icon } from '@appwrite.io/pink-svelte';
import { ensureStudioComponent, initImagine, getWebComponents } from './studio-widget';
import DomainsTable from './domainsTable.svelte';
import SideSheet from '$routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/layout/sidesheet.svelte';
const { region, projectId }: { region: string; projectId: string } = $props();
const {
region,
projectId
}: {
region: string;
projectId: string;
} = $props();
const siteId = `project-${projectId}`;
let showManageDomainsSheet = $state(false);
let primaryDomainForSite = $state(`imagine-${projectId}.stage.appwrite.network`);
onMount(() => {
ensureStudioComponent();
@@ -18,6 +34,23 @@
initImagine(region, projectId, {
onProjectNameChange: () => {
invalidate(Dependencies.PROJECT);
},
onAddDomain: async () => {
const baseUrl = resolve(
'/(console)/project-[region]-[project]/sites/site-[site]/domains/add-domain',
{
region,
project: projectId,
site: siteId
}
);
await goto(`${baseUrl}?types=false`);
},
onManageDomains: (primaryDomain) => {
if (primaryDomain) {
primaryDomainForSite = primaryDomain;
}
showManageDomainsSheet = true;
}
});
@@ -35,3 +68,27 @@
</script>
<div aria-hidden="true" style:display="none"></div>
<SideSheet title="Domains" bind:show={showManageDomainsSheet}>
<Layout.Stack gap="xl">
<Layout.Stack gap="xxs">
<Typography.Text color="--fgcolor-neutral-tertiary">Active domain</Typography.Text>
<Typography.Text>
<Link size="m" external variant="quiet" href={primaryDomainForSite}>
<Layout.Stack
direction="row"
gap="xxs"
alignItems="center"
alignContent="center">
{primaryDomainForSite}
<Icon size="s" icon={IconExternalLink} />
</Layout.Stack>
</Link>
</Typography.Text>
</Layout.Stack>
<DomainsTable {siteId} {region} {projectId} />
</Layout.Stack>
</SideSheet>
@@ -6,7 +6,7 @@
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { Fieldset, Layout, Tooltip, Icon, Input, Alert } from '@appwrite.io/pink-svelte';
import { goto, invalidate } from '$app/navigation';
import { afterNavigate, goto, invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { sortBranches } from '$lib/stores/vcs';
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
@@ -19,7 +19,7 @@
ProxyResourceType,
StatusCode
} from '@appwrite.io/console';
import { statusCodeOptions } from '$lib/stores/domains';
import { statusCodeOptions, hideTypes } from '$lib/stores/domains';
import { writable } from 'svelte/store';
import { onMount } from 'svelte';
import { ConnectRepoModal } from '$lib/components/git/index.js';
@@ -29,10 +29,9 @@
} from '$routes/(console)/project-[region]-[project]/store';
import { isCloud } from '$lib/system';
import { getApexDomain } from '$lib/helpers/tlds';
import type { PageProps } from './$types';
const routeBase = `${base}/project-${page.params.region}-${page.params.project}/sites/site-${page.params.site}/domains`;
let { data } = $props();
let { data, params }: PageProps = $props();
let formComponent: Form;
let isSubmitting = $state(writable(false));
@@ -43,7 +42,18 @@
let branch: string = $state(null);
let statusCode = $state(StatusCode.TemporaryRedirect307);
let routeBase = `${base}/project-${params.region}-${params.project}/sites/site-${params.site}/domains`;
let previousPage = $state(routeBase);
afterNavigate(({ from }) => {
if ($hideTypes) {
previousPage = from?.url?.pathname || routeBase;
}
});
onMount(() => {
hideTypes.set(page.url.searchParams.get('types') === 'false');
if (
page.url.searchParams.has('connectRepo') &&
page.url.searchParams.get('connectRepo') === 'true'
@@ -83,32 +93,30 @@
try {
let rule: Models.ProxyRule;
if (behaviour === 'BRANCH') {
rule = await sdk
.forProject(page.params.region, page.params.project)
.proxy.createSiteRule({
domain: domainName,
siteId: page.params.site,
branch
});
rule = await sdk.forProject(params.region, params.project).proxy.createSiteRule({
domain: domainName,
siteId: params.site,
branch
});
} else if (behaviour === 'REDIRECT') {
rule = await sdk
.forProject(page.params.region, page.params.project)
.forProject(params.region, params.project)
.proxy.createRedirectRule({
domain: domainName,
url: redirect,
statusCode,
resourceId: page.params.site,
resourceId: params.site,
resourceType: ProxyResourceType.Site
});
} else if (behaviour === 'ACTIVE') {
rule = await sdk
.forProject(page.params.region, page.params.project)
.proxy.createSiteRule({
domain: domainName,
siteId: page.params.site
});
rule = await sdk.forProject(params.region, params.project).proxy.createSiteRule({
domain: domainName,
siteId: params.site
});
}
if (rule?.status === 'verified') {
hideTypes.set(false);
await goto(routeBase);
await invalidate(Dependencies.SITES_DOMAINS);
} else {
@@ -125,7 +133,7 @@
async function connect(selectedInstallationId: string, selectedRepository: string) {
try {
await sdk.forProject(page.params.region, page.params.project).sites.update({
await sdk.forProject(params.region, params.project).sites.update({
siteId: data.site.$id,
name: data.site.name,
framework: data.site.framework as Framework,
@@ -143,14 +151,14 @@
providerBranch: 'main'
});
invalidate(Dependencies.SITE);
await invalidate(Dependencies.SITE);
} catch {
return;
}
}
</script>
<Wizard title="Add domain" href={routeBase} column columnSize="s" confirmExit>
<Wizard title="Add domain" href={previousPage} column columnSize="s" confirmExit>
<Form bind:this={formComponent} onSubmit={addDomain} bind:isSubmitting>
<Layout.Stack gap="xxl">
<Fieldset legend="Domain">
@@ -173,93 +181,98 @@
placeholder="appwrite.example.com" />
</Fieldset>
<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="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 !$hideTypes}
<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="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 === 'BRANCH'}
<Fieldset legend="Settings">
<Layout.Stack gap="xl">
{#if data.site?.providerRepositoryId}
{@const sortedBranches = sortBranches(data.branches.branches)}
{@const options = sortedBranches.map((branch) => ({
label: branch.name,
value: branch.name
}))}
<Layout.Stack gap="s">
<Input.ComboBox
required
id="branch"
label="Production branch"
placeholder="Select branch"
bind:value={branch}
on:select={(event) => {
branch = event.detail.value;
}}
{options} />
{#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
options={[{ label: 'main', value: 'main' }]}
label="Production branch"
id="branch"
required
value="main"
placeholder="Select branch" />
<Alert.Inline title=" There is no repository connected to your site">
<Layout.Stack>
<p>
The domain will be connected to your active deployment.
Connect your Git repository to link a production branch.
</p>
<div>
<Button compact on:click={() => (showConnectRepo = true)}>
Connect repository
</Button>
</div>
{#if behaviour === 'BRANCH'}
<Fieldset legend="Settings">
<Layout.Stack gap="xl">
{#if data.site?.providerRepositoryId}
{@const sortedBranches = sortBranches(data.branches.branches)}
{@const options = sortedBranches.map((branch) => ({
label: branch.name,
value: branch.name
}))}
<Layout.Stack gap="s">
<Input.ComboBox
required
id="branch"
label="Production branch"
placeholder="Select branch"
bind:value={branch}
on:select={(event) => {
branch = event.detail.value;
}}
{options} />
{#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>
</Alert.Inline>
{/if}
</Layout.Stack>
</Fieldset>
{:else if behaviour === 'REDIRECT'}
<Fieldset legend="Settings">
<Layout.Stack gap="xl">
<InputURL
label="Redirect to"
id="redirect"
placeholder="https://appwrite.io/docs"
bind:value={redirect}
required>
<Tooltip slot="info">
<Icon icon={IconInfo} size="s" />
<span slot="tooltip"> Redirect your domain to this URL.</span>
</Tooltip>
</InputURL>
<InputSelect
options={statusCodeOptions}
label="Status code"
id="code"
required
bind:value={statusCode}
placeholder="Select status code" />
</Layout.Stack>
</Fieldset>
{:else}
<InputSelect
disabled
options={[{ label: 'main', value: 'main' }]}
label="Production branch"
id="branch"
required
value="main"
placeholder="Select branch" />
<Alert.Inline
title=" There is no repository connected to your site">
<Layout.Stack>
<p>
The domain will be connected to your active deployment.
Connect your Git repository to link a production branch.
</p>
<div>
<Button
compact
on:click={() => (showConnectRepo = true)}>
Connect repository
</Button>
</div>
</Layout.Stack>
</Alert.Inline>
{/if}
</Layout.Stack>
</Fieldset>
{:else if behaviour === 'REDIRECT'}
<Fieldset legend="Settings">
<Layout.Stack gap="xl">
<InputURL
label="Redirect to"
id="redirect"
placeholder="https://appwrite.io/docs"
bind:value={redirect}
required>
<Tooltip slot="info">
<Icon icon={IconInfo} size="s" />
<span slot="tooltip"> Redirect your domain to this URL.</span>
</Tooltip>
</InputURL>
<InputSelect
options={statusCodeOptions}
label="Status code"
id="code"
required
bind:value={statusCode}
placeholder="Select status code" />
</Layout.Stack>
</Fieldset>
{/if}
{/if}
</Layout.Stack>
</Form>
@@ -13,6 +13,7 @@
import { sdk } from '$lib/stores/sdk';
import { organization } from '$lib/stores/organization';
import { addNotification } from '$lib/stores/notifications';
import { hideTypes } from '$lib/stores/domains';
import { goto, invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { isCloud } from '$lib/system';
@@ -24,6 +25,7 @@
import RecordTable from '$lib/components/domains/recordTable.svelte';
import NameserverTable from '$lib/components/domains/nameserverTable.svelte';
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
import { onDestroy } from 'svelte';
let { data } = $props();
@@ -76,6 +78,7 @@
type: 'success',
message: 'Domain added successfully'
});
await goto(routeBase);
await invalidate(Dependencies.DOMAINS);
await invalidate(Dependencies.SITES_DOMAINS);
@@ -95,8 +98,12 @@
.forProject(page.params.region, page.params.project)
.proxy.deleteRule({ ruleId });
}
await goto(`${routeBase}/add-domain?domain=${page.params.domain}`);
const typesParam = $hideTypes ? '&types=false' : '';
await goto(`${routeBase}/add-domain?domain=${page.params.domain}${typesParam}`);
}
onDestroy(() => hideTypes.set(false));
</script>
<Wizard title="Add domain" href={routeBase} column columnSize="s">