Implement Custom Domains Wizard and List

This commit is contained in:
Steven Nguyen
2022-10-17 13:50:07 -07:00
parent 60370d52b3
commit bc9694ea4d
12 changed files with 494 additions and 79 deletions
@@ -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: '' });