mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Implement Custom Domains Wizard and List
This commit is contained in:
@@ -1,38 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Modal } from '$lib/components';
|
||||
import { InputText, Button, Form } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdkForConsole } from '$lib/stores/sdk';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { project } from '../store';
|
||||
|
||||
export let show = false;
|
||||
|
||||
let domain: string;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const create = async () => {
|
||||
try {
|
||||
await sdkForConsole.projects.createDomain($project.$id, domain);
|
||||
domain = null;
|
||||
show = false;
|
||||
dispatch('created');
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<Form on:submit={create}>
|
||||
<Modal bind:show>
|
||||
<svelte:fragment slot="header">Add Domain</svelte:fragment>
|
||||
<InputText id="domain" label="Domain" bind:value={domain} required />
|
||||
<svelte:fragment slot="footer">
|
||||
<Button submit>Create</Button>
|
||||
<Button secondary on:click={() => (show = false)}>Cancel</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
</Form>
|
||||
@@ -1,64 +1,161 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { Empty } from '$lib/components';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import { Container } from '$lib/layout';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdkForConsole } from '$lib/stores/sdk';
|
||||
import CreateDomain from '../_createDomain.svelte';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import Create from './_create.svelte';
|
||||
import Delete from './_delete.svelte';
|
||||
import { domainList } from './store';
|
||||
import type { Models } from '@aw-labs/appwrite-console';
|
||||
|
||||
const projectId = $page.params.project;
|
||||
const listDomains = () => sdkForConsole.projects.listDomains(projectId);
|
||||
let showDelete = false;
|
||||
let selectedDomain: Models.Domain;
|
||||
let isVerifying = {};
|
||||
|
||||
let request = listDomains();
|
||||
let addDomain = false;
|
||||
const openWizard = () => {
|
||||
wizard.start(Create);
|
||||
};
|
||||
|
||||
const refreshDomain = async (domain: Models.Domain, i: number) => {
|
||||
const domainId = domain.$id;
|
||||
try {
|
||||
isVerifying[domainId] = true;
|
||||
|
||||
if (domain.verification) {
|
||||
await domainList.load(projectId);
|
||||
return;
|
||||
}
|
||||
const result = await sdkForConsole.projects.updateDomainVerification(
|
||||
projectId,
|
||||
domainId
|
||||
);
|
||||
$domainList.domains[i] = result;
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
} finally {
|
||||
isVerifying[domainId] = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<h1>Custom Domains</h1>
|
||||
{#await request}
|
||||
<div aria-busy="true" />
|
||||
{:then response}
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableCellHead width={96} />
|
||||
<TableCellHead>Domain</TableCellHead>
|
||||
<TableCellHead>TLS</TableCellHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each response.domains as domain}
|
||||
<TableRow>
|
||||
<TableCellText title="Scopes">
|
||||
<Pill danger={!domain.verification} success={domain.verification}>
|
||||
{domain.verification ? 'verified' : 'unverified'}
|
||||
</Pill>
|
||||
</TableCellText>
|
||||
<TableCellText title="Domain">
|
||||
{domain.domain}
|
||||
</TableCellText>
|
||||
<TableCellText title="TLS">
|
||||
{#if domain.certificateId}
|
||||
Verified
|
||||
{:else if domain.verification}
|
||||
In Progress
|
||||
{:else}
|
||||
Pending Verification
|
||||
{/if}
|
||||
</TableCellText>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{/await}
|
||||
<div class="u-flex u-gap-12 common-section u-main-space-between">
|
||||
<h2 class="heading-level-5">Custom Domains</h2>
|
||||
|
||||
<Button on:click={() => (addDomain = true)}>Add Domain</Button>
|
||||
<Button on:click={openWizard}>
|
||||
<span class="icon-plus" aria-hidden="true" /> <span class="text">Create domain</span>
|
||||
</Button>
|
||||
</div>
|
||||
{#await domainList.load(projectId)}
|
||||
<div aria-busy="true" />
|
||||
{:then}
|
||||
{#if $domainList.total}
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableCellHead />
|
||||
<TableCellHead>Domain Name</TableCellHead>
|
||||
<TableCellHead>TLS</TableCellHead>
|
||||
<TableCellHead />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each $domainList.domains as domain, i}
|
||||
<TableRow>
|
||||
<TableCellText title="Status">
|
||||
<Pill warning={!domain.verification} success={domain.verification}>
|
||||
{domain.verification ? 'verified' : 'unverified'}
|
||||
</Pill>
|
||||
</TableCellText>
|
||||
<TableCellText title="Domain">
|
||||
{domain.domain}
|
||||
</TableCellText>
|
||||
<TableCellText title="TLS">
|
||||
{#if domain.certificateId}
|
||||
<Pill success>enabled</Pill>
|
||||
{:else}
|
||||
<span
|
||||
use:tooltip={{
|
||||
content:
|
||||
"The process might take a while, click the retry button to see if it's finished"
|
||||
}}>
|
||||
<Pill warning>
|
||||
{!domain.verification
|
||||
? 'pending verification'
|
||||
: 'in progress'}
|
||||
</Pill>
|
||||
</span>
|
||||
{/if}
|
||||
</TableCellText>
|
||||
<TableCell title="Actions">
|
||||
<div class="u-flex u-gap-8 u-cross-center u-main-end">
|
||||
{#if isVerifying[domain.$id]}
|
||||
<!-- TODO: remove inline styles -->
|
||||
<div
|
||||
class="loader"
|
||||
style="color: hsl(var(--color-neutral-50)); inline-size: 1.25rem; block-size: 1.25rem" />
|
||||
{:else if !domain.certificateId}
|
||||
<!-- TODO: remove inline styles -->
|
||||
<button
|
||||
class="button is-text is-only-icon u-padding-inline-0"
|
||||
style="--p-button-size: var(--button-size, 2.0rem);"
|
||||
aria-label="Verify item"
|
||||
on:click={() => {
|
||||
refreshDomain(domain, i);
|
||||
}}>
|
||||
<span class="icon-refresh" aria-hidden="true" />
|
||||
</button>
|
||||
{/if}
|
||||
<!-- TODO: remove inline styles -->
|
||||
<button
|
||||
class="button tooltip is-text is-only-icon u-padding-inline-0"
|
||||
style="--p-button-size: var(--button-size, 2.0rem);"
|
||||
aria-label="Delete item"
|
||||
on:click={async () => {
|
||||
showDelete = true;
|
||||
selectedDomain = domain;
|
||||
}}>
|
||||
<span class="icon-trash" aria-hidden="true" />
|
||||
<span class="tooltip-popup is-bottom" role="tooltip">
|
||||
Delete
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{:else}
|
||||
<Empty isButton single on:click={openWizard}>
|
||||
<div class="common-section">
|
||||
<div class="u-text-center common-section">
|
||||
<p>Create your first Domain to get started</p>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16 common-section u-main-center">
|
||||
<Button external href="#/" text>Documentation</Button>
|
||||
<Button secondary>Create domain</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Empty>
|
||||
{/if}
|
||||
{/await}
|
||||
</Container>
|
||||
|
||||
<CreateDomain bind:show={addDomain} on:created={() => (request = listDomains())} />
|
||||
<Delete bind:showDelete bind:selectedDomain />
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { Wizard } from '$lib/layout';
|
||||
import { beforeNavigate } from '$app/navigation';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
|
||||
import { domain } from './wizard/store';
|
||||
import Step1 from './wizard/step1.svelte';
|
||||
import Step2 from './wizard/step2.svelte';
|
||||
import Step3 from './wizard/step3.svelte';
|
||||
import Step4 from './wizard/step4.svelte';
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
onDestroy(() => {
|
||||
domain.set({ id: '', name: '' });
|
||||
});
|
||||
|
||||
beforeNavigate(() => {
|
||||
wizard.hide();
|
||||
});
|
||||
|
||||
const stepsComponents: WizardStepsType = new Map();
|
||||
stepsComponents.set(1, {
|
||||
label: 'Add your domain',
|
||||
component: Step1
|
||||
});
|
||||
stepsComponents.set(2, {
|
||||
label: 'Add a CNAME Record',
|
||||
component: Step2
|
||||
});
|
||||
stepsComponents.set(3, {
|
||||
label: 'Verify domain',
|
||||
component: Step3
|
||||
});
|
||||
stepsComponents.set(4, {
|
||||
label: 'SSL Certificate',
|
||||
component: Step4
|
||||
});
|
||||
</script>
|
||||
|
||||
<Wizard
|
||||
title="Create domain"
|
||||
steps={stepsComponents}
|
||||
finalAction="Go to console"
|
||||
on:finish={wizard.hide} />
|
||||
@@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
import { Modal } from '$lib/components';
|
||||
import { Button, Form } from '$lib/elements/forms';
|
||||
import { project } from '../../store';
|
||||
import type { Models } from '@aw-labs/appwrite-console';
|
||||
import { sdkForConsole } from '$lib/stores/sdk';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { domainList } from './store';
|
||||
|
||||
export let showDelete = false;
|
||||
export let selectedDomain: Models.Domain;
|
||||
|
||||
const deleteDomain = async () => {
|
||||
try {
|
||||
await sdkForConsole.projects.deleteDomain($project.$id, selectedDomain.$id);
|
||||
domainList.load($project.$id);
|
||||
showDelete = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${selectedDomain.domain} has been deleted`
|
||||
});
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<Form on:submit={deleteDomain}>
|
||||
<Modal bind:show={showDelete} warning>
|
||||
<svelte:fragment slot="header">Delete Domain</svelte:fragment>
|
||||
<p>
|
||||
Are you sure you want to delete <b>{selectedDomain.domain}</b> from '{$project.name}'?
|
||||
</p>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
|
||||
<Button secondary submit>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
</Form>
|
||||
@@ -0,0 +1,17 @@
|
||||
import { sdkForConsole } from '$lib/stores/sdk';
|
||||
import type { Models } from '@aw-labs/appwrite-console';
|
||||
import { cachedStore } from '$lib/helpers/cache';
|
||||
|
||||
export const domainList = cachedStore<
|
||||
Models.DomainList,
|
||||
{
|
||||
load: (projectId: string) => Promise<void>;
|
||||
}
|
||||
>('domainList', function ({ set }) {
|
||||
return {
|
||||
load: async (projectId) => {
|
||||
const response = await sdkForConsole.projects.listDomains(projectId);
|
||||
set(response);
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { Copy } from '$lib/components';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import { domain } from './store';
|
||||
|
||||
const parts = $domain.name.split('.');
|
||||
const registerable = [parts[parts.length - 2], parts[parts.length - 1]].join('.');
|
||||
const cnameValue = $domain.name.replace('.' + registerable, '');
|
||||
</script>
|
||||
|
||||
<Table noStyles noMargin>
|
||||
<TableHeader>
|
||||
<TableCellHead>Type</TableCellHead>
|
||||
<TableCellHead>Name</TableCellHead>
|
||||
<TableCellHead>Value</TableCellHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCellText title="Type">CNAME</TableCellText>
|
||||
<TableCellText title="Name">{cnameValue}</TableCellText>
|
||||
<TableCell title="Value">
|
||||
<div class="u-flex u-main-space-between u-cross-center">
|
||||
<span class="text">{$domain.name}</span>
|
||||
<Copy value={$domain.name}>
|
||||
<span class="icon-duplicate" aria-hidden="true" />
|
||||
</Copy>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import Button from '$lib/elements/forms/button.svelte';
|
||||
import { sdkForConsole } from '$lib/stores/sdk';
|
||||
import { domain } from './store';
|
||||
import { project } from '../../../store';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
|
||||
const projectId = $project.$id;
|
||||
|
||||
export let isVerifying = false;
|
||||
export let isVerified = false;
|
||||
|
||||
const verifyDomain = async () => {
|
||||
isVerifying = true;
|
||||
try {
|
||||
const result = await sdkForConsole.projects.updateDomainVerification(
|
||||
projectId,
|
||||
$domain.id
|
||||
);
|
||||
isVerified = result.verification;
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
} finally {
|
||||
isVerifying = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="box u-flex u-gap-16 u-cross-center">
|
||||
{#if isVerifying}
|
||||
<div
|
||||
class="loader"
|
||||
style="color: hsl(var(--color-neutral-50)); inline-size: 1.25rem; block-size: 1.25rem" />
|
||||
{:else}
|
||||
<span
|
||||
class:u-hide={isVerifying}
|
||||
class:icon-x={!isVerified}
|
||||
class:icon-check={isVerified}
|
||||
aria-hidden="true"
|
||||
style="color: hsl(var(--color-neutral-50))" />
|
||||
{/if}
|
||||
<p class="u-stretch">
|
||||
{!isVerified ? 'Domain is pending verification' : 'Verified domain'}
|
||||
</p>
|
||||
{#if !isVerifying && !isVerified}
|
||||
<Button secondary on:click={verifyDomain}>Verify</Button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,64 @@
|
||||
<script lang="ts">
|
||||
import { FormList, InputDomain } from '$lib/elements/forms';
|
||||
import { WizardStep } from '$lib/layout';
|
||||
import { sdkForConsole } from '$lib/stores/sdk';
|
||||
import { project } from '../../../store';
|
||||
import { domain } from './store';
|
||||
import { domainList } from '../store';
|
||||
|
||||
// Handle if user goes back to this step to update
|
||||
// the domain name after creating the domain
|
||||
const originalDomainName = $domain.name;
|
||||
const originalDomainId = $domain.id;
|
||||
const unsubscribe = domain.subscribe((newDomain) => {
|
||||
if (
|
||||
originalDomainName !== '' &&
|
||||
newDomain.id !== '' &&
|
||||
newDomain.name !== originalDomainName
|
||||
) {
|
||||
$domain.id = '';
|
||||
}
|
||||
});
|
||||
|
||||
const projectId = $project.$id;
|
||||
const createDomain = async () => {
|
||||
unsubscribe();
|
||||
if ($domain.name !== originalDomainName && originalDomainId) {
|
||||
await sdkForConsole.projects.deleteDomain(projectId, originalDomainId);
|
||||
}
|
||||
|
||||
if ($domain.id == '') {
|
||||
const newDomain = await sdkForConsole.projects.createDomain(projectId, $domain.name);
|
||||
domainList.load(projectId);
|
||||
$domain.id = newDomain.$id;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<WizardStep beforeSubmit={createDomain}>
|
||||
<svelte:fragment slot="title">Add your domain</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">
|
||||
Use your self-owned domain as the endpoint of your Appwrite API.
|
||||
</svelte:fragment>
|
||||
|
||||
<FormList>
|
||||
<InputDomain
|
||||
id="domain"
|
||||
label="Custom Domain"
|
||||
placeholder="appwrite.example.com"
|
||||
autocomplete={false}
|
||||
required
|
||||
bind:value={$domain.name} />
|
||||
</FormList>
|
||||
<div class="common-section ">
|
||||
<p>
|
||||
You can find a list of domain providers and their DNS setting documentation <a
|
||||
class="link"
|
||||
href="https://appwrite.io/docs/custom-domains#addCNAME"
|
||||
target="_blank"
|
||||
rel="noreferrer">here</a
|
||||
>. If your domain provider isn't listed, please contact us, and we'll include their
|
||||
settings as well.
|
||||
</p>
|
||||
</div>
|
||||
</WizardStep>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { WizardStep } from '$lib/layout';
|
||||
import CnameTable from './_cnameTable.svelte';
|
||||
</script>
|
||||
|
||||
<WizardStep>
|
||||
<svelte:fragment slot="title">Add a CNAME record</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">
|
||||
In order to continue, set the following record on your DNS provider
|
||||
</svelte:fragment>
|
||||
|
||||
<CnameTable />
|
||||
</WizardStep>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { WizardStep } from '$lib/layout';
|
||||
import CnameTable from './_cnameTable.svelte';
|
||||
import VerificationBox from './_verificationBox.svelte';
|
||||
|
||||
let isVerifying = false;
|
||||
let isVerified = false;
|
||||
</script>
|
||||
|
||||
<WizardStep>
|
||||
<svelte:fragment slot="title">Add a CNAME record</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">
|
||||
In order to continue, set the following record on your DNS provider
|
||||
</svelte:fragment>
|
||||
<input class="u-hide" type="checkbox" name="isVerified" required bind:checked={isVerified} />
|
||||
<CnameTable />
|
||||
|
||||
<div class="container u-padding-inline-0">
|
||||
<p>Changes may take some time to take effect depending on your provider.</p>
|
||||
|
||||
<div class="container u-padding-inline-0">
|
||||
<VerificationBox {isVerifying} bind:isVerified />
|
||||
</div>
|
||||
</div>
|
||||
</WizardStep>
|
||||
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import { WizardStep } from '$lib/layout';
|
||||
import { sdkForConsole } from '$lib/stores/sdk';
|
||||
import { domain } from './store';
|
||||
import { project } from '../../../store';
|
||||
import { domainList } from '../store';
|
||||
import CnameTable from './_cnameTable.svelte';
|
||||
import VerificationBox from './_verificationBox.svelte';
|
||||
|
||||
const projectId = $project.$id;
|
||||
|
||||
let certificate = false;
|
||||
const checkCertificate = () => {
|
||||
setTimeout(async () => {
|
||||
const result = await sdkForConsole.projects.getDomain(projectId, $domain.id);
|
||||
if (!result.certificateId) {
|
||||
checkCertificate();
|
||||
return;
|
||||
}
|
||||
const i = $domainList.domains.findIndex((d) => d.$id === $domain.id);
|
||||
if (i >= 0) {
|
||||
$domainList.domains[i] = result;
|
||||
}
|
||||
certificate = true;
|
||||
}, 2000);
|
||||
};
|
||||
checkCertificate();
|
||||
</script>
|
||||
|
||||
<WizardStep>
|
||||
<svelte:fragment slot="title">Add a CNAME record</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">
|
||||
In order to continue, set the following record on your DNS provider
|
||||
</svelte:fragment>
|
||||
|
||||
<CnameTable />
|
||||
|
||||
<div class="boxes-wrapper u-margin-block-start-24">
|
||||
<VerificationBox isVerifying={false} isVerified={true} />
|
||||
<div class="box">
|
||||
<div class="u-flex u-gap-16 u-cross-center">
|
||||
{#if !certificate}
|
||||
<div
|
||||
class="loader"
|
||||
style="color: hsl(var(--color-neutral-50)); inline-size: 1.25rem; block-size: 1.25rem" />
|
||||
{:else}
|
||||
<span
|
||||
class="icon-check"
|
||||
aria-hidden="true"
|
||||
style="color: hsl(var(--color-neutral-50))" />
|
||||
{/if}
|
||||
<p class="u-stretch">
|
||||
{!certificate ? 'Generating SSL certificate' : 'Generated SSL certificate'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</WizardStep>
|
||||
@@ -0,0 +1,3 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const domain = writable<{ id: string; name: string }>({ id: '', name: '' });
|
||||
Reference in New Issue
Block a user