Skip to content

Commit 355dbda

Browse files
authored
Merge pull request #2586 from appwrite/add-domain-callbacks
2 parents e3735b9 + d58ee51 commit 355dbda

File tree

6 files changed

+419
-111
lines changed

6 files changed

+419
-111
lines changed

src/lib/stores/domains.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import { writable } from 'svelte/store';
12
import { StatusCode } from '@appwrite.io/console';
23

4+
export const hideTypes = writable<boolean>(false);
5+
36
export const statusCodeOptions = [
47
{
58
label: '301 Moved permanently',

src/lib/studio/domainsTable.svelte

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
<script lang="ts">
2+
import { onMount } from 'svelte';
3+
import { Link } from '$lib/elements';
4+
import { Button } from '$lib/elements/forms';
5+
import { sdk, RuleType, DeploymentResourceType, RuleTrigger } from '$lib/stores/sdk';
6+
import { Query, type Models } from '@appwrite.io/console';
7+
import {
8+
IconDotsHorizontal,
9+
IconExternalLink,
10+
IconPlus,
11+
IconRefresh,
12+
IconTrash
13+
} from '@appwrite.io/pink-icons-svelte';
14+
import {
15+
ActionMenu,
16+
Badge,
17+
Icon,
18+
Layout,
19+
Popover,
20+
Table,
21+
Typography,
22+
Skeleton,
23+
Divider
24+
} from '@appwrite.io/pink-svelte';
25+
import { resolve } from '$app/paths';
26+
import { Click, trackEvent } from '$lib/actions/analytics';
27+
import { regionalProtocol } from '$routes/(console)/project-[region]-[project]/store';
28+
import { goto } from '$app/navigation';
29+
import DeleteDomainModal from '$routes/(console)/project-[region]-[project]/sites/site-[site]/domains/deleteDomainModal.svelte';
30+
import RetryDomainModal from '$routes/(console)/project-[region]-[project]/sites/site-[site]/domains/retryDomainModal.svelte';
31+
32+
let {
33+
siteId,
34+
region,
35+
projectId
36+
}: {
37+
siteId: string;
38+
region: string;
39+
projectId: string;
40+
} = $props();
41+
42+
let loading = $state(true);
43+
let showRetry = $state(false);
44+
let showDelete = $state(false);
45+
46+
let selectedProxyRule: Models.ProxyRule = $state(null);
47+
let proxyRules = $state<Models.ProxyRuleList | null>(null);
48+
49+
async function loadDomains() {
50+
loading = true;
51+
try {
52+
proxyRules = await sdk.forProject(region, projectId).proxy.listRules({
53+
queries: [
54+
Query.equal('type', [RuleType.DEPLOYMENT, RuleType.REDIRECT]),
55+
Query.equal('deploymentResourceType', DeploymentResourceType.SITE),
56+
Query.equal('deploymentResourceId', siteId),
57+
Query.equal('trigger', RuleTrigger.MANUAL),
58+
Query.limit(100)
59+
]
60+
});
61+
} catch (error) {
62+
console.error('Failed to load domains:', error);
63+
} finally {
64+
loading = false;
65+
}
66+
}
67+
68+
let previousDeleteState = $state(false);
69+
let previousRetryState = $state(false);
70+
71+
onMount(loadDomains);
72+
73+
$effect(() => {
74+
const wasDeleteOpen = previousDeleteState && !showDelete;
75+
const wasRetryOpen = previousRetryState && !showRetry;
76+
77+
if (wasDeleteOpen || wasRetryOpen) {
78+
loadDomains();
79+
}
80+
81+
previousDeleteState = showDelete;
82+
previousDeleteState = showDelete;
83+
previousRetryState = showRetry;
84+
});
85+
86+
const addDomainUrl = $derived.by(() => {
87+
const baseUrl = resolve(
88+
'/(console)/project-[region]-[project]/sites/site-[site]/domains/add-domain',
89+
{
90+
region,
91+
project: projectId,
92+
site: siteId
93+
}
94+
);
95+
return `${baseUrl}?types=false`;
96+
});
97+
</script>
98+
99+
<Table.Root columns={[{ id: 'domain' }, { id: 'actions', width: 40 }]} let:root>
100+
<svelte:fragment slot="header" let:root>
101+
<Table.Header.Cell column="domain" {root}>Domain</Table.Header.Cell>
102+
<Table.Header.Cell column="actions" {root} />
103+
</svelte:fragment>
104+
{#if loading}
105+
{#each Array(2) as _}
106+
<Table.Row.Base {root}>
107+
<Table.Cell column="domain" {root}>
108+
<Layout.Stack direction="row" gap="xs" alignItems="center">
109+
<Skeleton variant="line" width={200} height={20} />
110+
</Layout.Stack>
111+
</Table.Cell>
112+
<Table.Cell column="actions" {root}>
113+
<Layout.Stack direction="row" justifyContent="flex-end">
114+
<Skeleton variant="line" width={24} height={12} />
115+
</Layout.Stack>
116+
</Table.Cell>
117+
</Table.Row.Base>
118+
{/each}
119+
{:else if proxyRules && proxyRules.total > 0}
120+
{#each proxyRules.rules as rule}
121+
<Table.Row.Base {root}>
122+
<Table.Cell column="domain" {root}>
123+
<Layout.Stack direction="row" gap="xs" alignItems="center">
124+
<Link external variant="quiet" href={`${$regionalProtocol}${rule.domain}`}>
125+
<Layout.Stack direction="row" gap="xxs" alignItems="center">
126+
<Typography.Text truncate>
127+
{rule.domain}
128+
</Typography.Text>
129+
<Icon size="xs" icon={IconExternalLink} />
130+
</Layout.Stack>
131+
</Link>
132+
133+
{#if rule.status === 'verifying'}
134+
<Badge variant="secondary" content="Verifying" size="s" />
135+
{:else if rule.status !== 'verified'}
136+
<Badge
137+
size="s"
138+
type="warning"
139+
variant="secondary"
140+
content="Verification failed" />
141+
{/if}
142+
</Layout.Stack>
143+
</Table.Cell>
144+
<Table.Cell column="actions" {root}>
145+
<Popover let:toggle padding="none">
146+
<Button
147+
text
148+
icon
149+
on:click={(e) => {
150+
e.preventDefault();
151+
toggle(e);
152+
}}>
153+
<Icon icon={IconDotsHorizontal} size="s" />
154+
</Button>
155+
156+
<svelte:fragment slot="tooltip" let:toggle>
157+
{@render domainActions(rule, toggle)}
158+
</svelte:fragment>
159+
</Popover>
160+
</Table.Cell>
161+
</Table.Row.Base>
162+
{/each}
163+
{/if}
164+
</Table.Root>
165+
166+
<Layout.Stack style="width: min-content;">
167+
<Button compact on:onclick={async () => await goto(addDomainUrl)}>
168+
<Icon icon={IconPlus} size="s" />
169+
Add domain
170+
</Button>
171+
</Layout.Stack>
172+
173+
{#if showDelete}
174+
<DeleteDomainModal bind:show={showDelete} {selectedProxyRule} />
175+
{/if}
176+
177+
{#if showRetry}
178+
<RetryDomainModal bind:show={showRetry} {selectedProxyRule} />
179+
{/if}
180+
181+
{#snippet domainActions(rule: Models.ProxyRule, toggle: () => void)}
182+
<ActionMenu.Root>
183+
<ActionMenu.Item.Anchor href={`${$regionalProtocol}${rule.domain}`} external>
184+
Open domain
185+
</ActionMenu.Item.Anchor>
186+
{#if rule.status !== 'verified' && rule.status !== 'verifying'}
187+
<ActionMenu.Item.Button
188+
leadingIcon={IconRefresh}
189+
on:click={() => {
190+
selectedProxyRule = rule;
191+
showRetry = true;
192+
toggle();
193+
}}>
194+
Retry
195+
</ActionMenu.Item.Button>
196+
197+
<div class="action-menu-divider">
198+
<Divider />
199+
</div>
200+
{/if}
201+
202+
<ActionMenu.Item.Button
203+
status="danger"
204+
leadingIcon={IconTrash}
205+
on:click={() => {
206+
selectedProxyRule = rule;
207+
showDelete = true;
208+
toggle();
209+
trackEvent(Click.DomainDeleteClick, {
210+
source: 'studio_manage_domains'
211+
});
212+
}}>
213+
Delete
214+
</ActionMenu.Item.Button>
215+
</ActionMenu.Root>
216+
{/snippet}
217+
218+
<style>
219+
.action-menu-divider {
220+
margin-inline: -1rem;
221+
padding-block-end: 0.25rem;
222+
padding-block-start: 0.25rem;
223+
}
224+
</style>

src/lib/studio/studio-widget.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,11 @@ export function hideStudio() {
278278
export async function initImagine(
279279
region: string,
280280
projectId: string,
281-
callbacks?: { onProjectNameChange?: (name: string) => void }
281+
callbacks?: {
282+
onProjectNameChange: () => void;
283+
onAddDomain: () => void | Promise<void>;
284+
onManageDomains: (primaryDomain?: string) => void | Promise<void>;
285+
}
282286
) {
283287
try {
284288
const { initImagineConfig, initImagineRouting } = await getWebComponents();

src/lib/studio/studio.svelte

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,52 @@
55
<script lang="ts">
66
import './shim.css';
77
import { onMount } from 'svelte';
8-
import { ensureStudioComponent, initImagine, getWebComponents } from './studio-widget';
8+
import { resolve } from '$app/paths';
9+
import { Link } from '$lib/elements';
910
import { app } from '$lib/stores/app';
10-
import { invalidate } from '$app/navigation';
1111
import { Dependencies } from '$lib/constants';
12+
import { goto, invalidate } from '$app/navigation';
13+
import { IconExternalLink } from '@appwrite.io/pink-icons-svelte';
14+
import { Layout, Typography, Icon } from '@appwrite.io/pink-svelte';
15+
import { ensureStudioComponent, initImagine, getWebComponents } from './studio-widget';
16+
import DomainsTable from './domainsTable.svelte';
17+
import SideSheet from '$routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/layout/sidesheet.svelte';
1218
13-
const { region, projectId }: { region: string; projectId: string } = $props();
19+
const {
20+
region,
21+
projectId
22+
}: {
23+
region: string;
24+
projectId: string;
25+
} = $props();
26+
27+
const siteId = `project-${projectId}`;
28+
let showManageDomainsSheet = $state(false);
29+
let primaryDomainForSite = $state(`imagine-${projectId}.stage.appwrite.network`);
1430
1531
onMount(() => {
1632
ensureStudioComponent();
1733
1834
initImagine(region, projectId, {
1935
onProjectNameChange: () => {
2036
invalidate(Dependencies.PROJECT);
37+
},
38+
onAddDomain: async () => {
39+
const baseUrl = resolve(
40+
'/(console)/project-[region]-[project]/sites/site-[site]/domains/add-domain',
41+
{
42+
region,
43+
project: projectId,
44+
site: siteId
45+
}
46+
);
47+
await goto(`${baseUrl}?types=false`);
48+
},
49+
onManageDomains: (primaryDomain) => {
50+
if (primaryDomain) {
51+
primaryDomainForSite = primaryDomain;
52+
}
53+
showManageDomainsSheet = true;
2154
}
2255
});
2356
@@ -35,3 +68,27 @@
3568
</script>
3669

3770
<div aria-hidden="true" style:display="none"></div>
71+
72+
<SideSheet title="Domains" bind:show={showManageDomainsSheet}>
73+
<Layout.Stack gap="xl">
74+
<Layout.Stack gap="xxs">
75+
<Typography.Text color="--fgcolor-neutral-tertiary">Active domain</Typography.Text>
76+
77+
<Typography.Text>
78+
<Link size="m" external variant="quiet" href={primaryDomainForSite}>
79+
<Layout.Stack
80+
direction="row"
81+
gap="xxs"
82+
alignItems="center"
83+
alignContent="center">
84+
{primaryDomainForSite}
85+
86+
<Icon size="s" icon={IconExternalLink} />
87+
</Layout.Stack>
88+
</Link>
89+
</Typography.Text>
90+
</Layout.Stack>
91+
92+
<DomainsTable {siteId} {region} {projectId} />
93+
</Layout.Stack>
94+
</SideSheet>

0 commit comments

Comments
 (0)