mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'feat-pink-v2' into mgn-dv
# Conflicts: # src/routes/(console)/organization-[organization]/+page.svelte
This commit is contained in:
+2
-2
@@ -21,11 +21,11 @@
|
||||
"e2e:ui": "playwright test --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appwrite.io/console": "https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@9c6551f",
|
||||
"@appwrite.io/console": "https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@e2f082e",
|
||||
"@appwrite.io/pink-icons": "0.25.0",
|
||||
"@appwrite.io/pink-icons-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@207628e",
|
||||
"@appwrite.io/pink-legacy": "^1.0.3",
|
||||
"@appwrite.io/pink-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@207628e",
|
||||
"@appwrite.io/pink-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@16d19d7",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sentry/sveltekit": "^8.38.0",
|
||||
"@stripe/stripe-js": "^3.5.0",
|
||||
|
||||
Generated
+17
-17
@@ -9,8 +9,8 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
'@appwrite.io/console':
|
||||
specifier: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@9c6551f
|
||||
version: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@9c6551f
|
||||
specifier: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@e2f082e
|
||||
version: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@e2f082e
|
||||
'@appwrite.io/pink-icons':
|
||||
specifier: 0.25.0
|
||||
version: 0.25.0
|
||||
@@ -21,8 +21,8 @@ importers:
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3
|
||||
'@appwrite.io/pink-svelte':
|
||||
specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@207628e
|
||||
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@207628e(react-dom@18.3.1(react@18.3.1))(svelte@5.25.3)
|
||||
specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@16d19d7
|
||||
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@16d19d7(react-dom@18.3.1(react@18.3.1))(svelte@5.25.3)
|
||||
'@popperjs/core':
|
||||
specifier: ^2.11.8
|
||||
version: 2.11.8
|
||||
@@ -238,18 +238,18 @@ packages:
|
||||
'@analytics/type-utils@0.6.2':
|
||||
resolution: {integrity: sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg==}
|
||||
|
||||
'@appwrite.io/console@https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@9c6551f':
|
||||
resolution: {tarball: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@9c6551f}
|
||||
'@appwrite.io/console@https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@e2f082e':
|
||||
resolution: {tarball: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@e2f082e}
|
||||
version: 1.2.1
|
||||
|
||||
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@207628e':
|
||||
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@207628e}
|
||||
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@16d19d7471dc8cb55ee3893b3e88470f383d60b1':
|
||||
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@16d19d7471dc8cb55ee3893b3e88470f383d60b1}
|
||||
version: 1.0.0-next.7
|
||||
peerDependencies:
|
||||
svelte: ^4.0.0
|
||||
|
||||
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@207628e41f5ce1df4b7880030c2865452408d8ed':
|
||||
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@207628e41f5ce1df4b7880030c2865452408d8ed}
|
||||
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@207628e':
|
||||
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@207628e}
|
||||
version: 1.0.0-next.7
|
||||
peerDependencies:
|
||||
svelte: ^4.0.0
|
||||
@@ -263,8 +263,8 @@ packages:
|
||||
'@appwrite.io/pink-legacy@1.0.3':
|
||||
resolution: {integrity: sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ==}
|
||||
|
||||
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@207628e':
|
||||
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@207628e}
|
||||
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@16d19d7':
|
||||
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@16d19d7}
|
||||
version: 1.0.0-next.85
|
||||
peerDependencies:
|
||||
react-dom: ^18.0.0
|
||||
@@ -3733,13 +3733,13 @@ snapshots:
|
||||
|
||||
'@analytics/type-utils@0.6.2': {}
|
||||
|
||||
'@appwrite.io/console@https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@9c6551f': {}
|
||||
'@appwrite.io/console@https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@e2f082e': {}
|
||||
|
||||
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@207628e(svelte@5.25.3)':
|
||||
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@16d19d7471dc8cb55ee3893b3e88470f383d60b1(svelte@5.25.3)':
|
||||
dependencies:
|
||||
svelte: 5.25.3
|
||||
|
||||
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@207628e41f5ce1df4b7880030c2865452408d8ed(svelte@5.25.3)':
|
||||
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@207628e(svelte@5.25.3)':
|
||||
dependencies:
|
||||
svelte: 5.25.3
|
||||
|
||||
@@ -3752,9 +3752,9 @@ snapshots:
|
||||
'@appwrite.io/pink-icons': 1.0.0
|
||||
the-new-css-reset: 1.11.3
|
||||
|
||||
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@207628e(react-dom@18.3.1(react@18.3.1))(svelte@5.25.3)':
|
||||
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@16d19d7(react-dom@18.3.1(react@18.3.1))(svelte@5.25.3)':
|
||||
dependencies:
|
||||
'@appwrite.io/pink-icons-svelte': https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@207628e41f5ce1df4b7880030c2865452408d8ed(svelte@5.25.3)
|
||||
'@appwrite.io/pink-icons-svelte': https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@16d19d7471dc8cb55ee3893b3e88470f383d60b1(svelte@5.25.3)
|
||||
'@floating-ui/dom': 1.6.13
|
||||
'@melt-ui/pp': 0.3.2(@melt-ui/svelte@0.86.5(svelte@5.25.3))(svelte@5.25.3)
|
||||
'@melt-ui/svelte': 0.86.5(svelte@5.25.3)
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const card = await submitStripeCard(name, page?.params?.organization ?? null);
|
||||
show = false;
|
||||
invalidate(Dependencies.PAYMENT_METHODS);
|
||||
dispatch('submit', card);
|
||||
show = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'A new payment method has been added to your account'
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { isCloud } from '$lib/system';
|
||||
import CardPlanLimit from './cardPlanLimit.svelte';
|
||||
|
||||
export let showEmpty = true;
|
||||
export let disableEmpty = true;
|
||||
export let offset = 0;
|
||||
export let total = 0;
|
||||
export let event: string = null;
|
||||
@@ -25,8 +25,8 @@
|
||||
{#if total > 3 ? total < limit + offset : total % 2 !== 0}
|
||||
{#if isCloud && serviceId && total >= planLimit}
|
||||
<CardPlanLimit {service} />
|
||||
{:else if showEmpty}
|
||||
<Empty on:click target={event}>
|
||||
{:else}
|
||||
<Empty on:click target={event} disabled={disableEmpty}>
|
||||
<slot name="empty" />
|
||||
</Empty>
|
||||
{/if}
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
role="button"
|
||||
tabindex="0"
|
||||
style:cursor="pointer"
|
||||
on:click|preventDefault={handleClick}
|
||||
on:click|preventDefault|stopPropagation={handleClick}
|
||||
on:keyup={clickOnEnter}
|
||||
on:mouseenter={() => setTimeout(() => (content = 'Click to copy'))}>
|
||||
<slot />
|
||||
|
||||
@@ -54,8 +54,8 @@
|
||||
<InteractiveText
|
||||
variant="copy"
|
||||
isVisible
|
||||
text={$consoleVariables._APP_DOMAIN_TARGET}>
|
||||
{$consoleVariables._APP_DOMAIN_TARGET}</InteractiveText>
|
||||
text={$consoleVariables._APP_DOMAIN_TARGET_CNAME}>
|
||||
{$consoleVariables._APP_DOMAIN_TARGET_CNAME}</InteractiveText>
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
</Table.Root>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
export let verified = false;
|
||||
|
||||
const nameserverList = $consoleVariables?._APP_DOMAINS_NAMESERVERS
|
||||
? $consoleVariables?._APP_DOMAINS_NAMESERVERS.split(',')
|
||||
? $consoleVariables?._APP_DOMAINS_NAMESERVERS?.split(',')
|
||||
: ['ns1.appwrite.io', 'ns2.appwrite.io'];
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<script lang="ts">
|
||||
import { Link } from '$lib/elements';
|
||||
import { consoleVariables } from '$routes/(console)/store';
|
||||
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
|
||||
import {
|
||||
Badge,
|
||||
Layout,
|
||||
Typography,
|
||||
Table,
|
||||
Icon,
|
||||
InteractiveText
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let domain: string;
|
||||
export let verified = false;
|
||||
export let variant: 'cname' | 'a' | 'aaaa';
|
||||
|
||||
let subdomain = domain?.split('.')?.slice(0, -2)?.join('.');
|
||||
|
||||
function setTarget() {
|
||||
switch (variant) {
|
||||
case 'cname':
|
||||
return $consoleVariables._APP_DOMAIN_TARGET_CNAME;
|
||||
case 'a':
|
||||
return $consoleVariables._APP_DOMAIN_TARGET_A;
|
||||
case 'aaaa':
|
||||
return $consoleVariables._APP_DOMAIN_TARGET_AAAA;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Layout.Stack gap="xl">
|
||||
<Layout.Stack gap="s">
|
||||
<Layout.Stack gap="s" direction="row" alignItems="center">
|
||||
<Typography.Text variant="l-500" color="--fgcolor-neutral-primary">
|
||||
{domain}
|
||||
</Typography.Text>
|
||||
{#if verified}
|
||||
<Badge variant="secondary" type="success" content="Verified" />
|
||||
{:else if verified === false}
|
||||
<Badge variant="secondary" type="error" content="Verification failed" />
|
||||
{:else}
|
||||
<Badge
|
||||
variant="secondary"
|
||||
type="warning"
|
||||
size="xs"
|
||||
content="Pending verification" />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
<Typography.Text variant="m-400">
|
||||
Add the following record on your DNS provider. Note that DNS changes may take time to
|
||||
propagate fully.
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
|
||||
<Table.Root columns={3} let:root>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell {root}>Type</Table.Header.Cell>
|
||||
<Table.Header.Cell {root}>Name</Table.Header.Cell>
|
||||
<Table.Header.Cell {root}>Value</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell {root}>{variant.toUpperCase()}</Table.Cell>
|
||||
<Table.Cell {root}>{subdomain || '@'}</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
<InteractiveText variant="copy" isVisible text={setTarget()}>
|
||||
{setTarget()}</InteractiveText>
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
</Table.Root>
|
||||
<Layout.Stack gap="s" direction="row" alignItems="center">
|
||||
<Icon icon={IconInfo} size="s" color="--fgcolor-neutral-secondary" />
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-secondary">
|
||||
A list of all domain providers and their DNS setting is available <Link
|
||||
variant="muted"
|
||||
external
|
||||
href="https://appwrite.io/docs/advanced/platform/custom-domains">here</Link
|
||||
>.
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
@@ -1,8 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { type ComponentProps } from 'svelte';
|
||||
import { capitalize } from '$lib/helpers/string';
|
||||
import { timeFromNow, toLocaleDateTime, toLocalDateTimeISO } from '$lib/helpers/date';
|
||||
import {
|
||||
timeFromNow,
|
||||
toLocaleDateTime,
|
||||
toLocalDateTimeISO,
|
||||
toISOString
|
||||
} from '$lib/helpers/date';
|
||||
import { Badge, InteractiveText, Layout, Popover, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { menuOpen } from '$lib/components/menu/store';
|
||||
|
||||
export let time: string = '';
|
||||
export let placement: ComponentProps<Popover>['placement'] = 'bottom';
|
||||
@@ -70,7 +76,13 @@
|
||||
</script>
|
||||
|
||||
<Popover let:show let:hide {placement} portal>
|
||||
<button on:mouseenter={() => setTimeout(show, 150)} on:mouseleave={() => hidePopover(hide)}>
|
||||
<button
|
||||
on:mouseenter={() => {
|
||||
if (!$menuOpen) {
|
||||
setTimeout(show, 150);
|
||||
}
|
||||
}}
|
||||
on:mouseleave={() => hidePopover(hide)}>
|
||||
<slot>{capitalize(timeFromNow(time))}</slot>
|
||||
</button>
|
||||
|
||||
@@ -99,7 +111,7 @@
|
||||
isVisible
|
||||
variant="copy"
|
||||
text={toLocaleDateTime(time, 'UTC')}
|
||||
value={new Date(time).toISOString()} />
|
||||
value={toISOString(time)} />
|
||||
|
||||
<Badge variant="secondary" content="UTC" size="xs" />
|
||||
</Layout.Stack>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
export let href: string = null;
|
||||
export let allowCreate = true;
|
||||
export let description = 'Need a hand? Learn more in our documentation.';
|
||||
export let disabled = false;
|
||||
export let src: string = null;
|
||||
|
||||
function track() {
|
||||
@@ -47,7 +48,7 @@
|
||||
</Empty>
|
||||
</Card.Base>
|
||||
{:else}
|
||||
<Card.Button on:click on:click={track} aria-label="create">
|
||||
<Card.Button on:click on:click={track} aria-label="create" {disabled}>
|
||||
<Layout.Stack justifyContent="center" alignItems="center" gap="m">
|
||||
<Avatar size="s"><Icon icon={IconPlus} size="s" /></Avatar>
|
||||
<Typography.Text variation="m-400" align="center"><slot /></Typography.Text>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: if (backdrop) {
|
||||
$: if (backdrop && show && !document.body.contains(backdrop)) {
|
||||
document.body.appendChild(backdrop);
|
||||
}
|
||||
|
||||
|
||||
@@ -138,45 +138,48 @@
|
||||
{/if}
|
||||
</Button>
|
||||
<svelte:fragment slot="tooltip">
|
||||
<Layout.Stack gap="s">
|
||||
{#if displayQuickFilters}
|
||||
<slot name="quick" />
|
||||
{:else}
|
||||
<p>Apply filter rules to refine the table view</p>
|
||||
<Content
|
||||
bind:columnId={selectedColumn}
|
||||
bind:operatorKey
|
||||
bind:value
|
||||
bind:arrayValues
|
||||
{columns}
|
||||
{singleCondition}
|
||||
on:apply={afterApply}
|
||||
on:clear={() => (applied = 0)} />
|
||||
{/if}
|
||||
<div
|
||||
class="u-flex u-cross-center u-margin-block-start-16"
|
||||
class:u-main-end={!quickFilters}
|
||||
class:u-main-space-between={quickFilters}>
|
||||
{#if quickFilters}
|
||||
<Button
|
||||
text
|
||||
on:click={() => (displayQuickFilters = !displayQuickFilters)}
|
||||
class="u-margin-block-end-auto">
|
||||
{displayQuickFilters ? 'Advanced filters' : 'Quick filters'}
|
||||
</Button>
|
||||
<div style:width="420px">
|
||||
<Layout.Stack gap="s">
|
||||
{#if displayQuickFilters}
|
||||
<slot name="quick" />
|
||||
{:else}
|
||||
<p>Apply filter rules to refine the table view</p>
|
||||
<Content
|
||||
bind:columnId={selectedColumn}
|
||||
bind:operatorKey
|
||||
bind:value
|
||||
bind:arrayValues
|
||||
{columns}
|
||||
{singleCondition}
|
||||
on:apply={afterApply}
|
||||
on:clear={() => (applied = 0)} />
|
||||
{/if}
|
||||
<div class="u-flex u-gap-8">
|
||||
{#if singleCondition}
|
||||
<Button size="s" text on:click={toggleDropdown}>Cancel</Button>
|
||||
{:else}
|
||||
<Button size="s" disabled={applied === 0} text on:click={clearAll}>
|
||||
Clear all
|
||||
<div
|
||||
class="u-flex u-cross-center u-margin-block-start-16"
|
||||
class:u-main-end={!quickFilters}
|
||||
class:u-main-space-between={quickFilters}>
|
||||
{#if quickFilters}
|
||||
<Button
|
||||
text
|
||||
on:click={() => (displayQuickFilters = !displayQuickFilters)}
|
||||
class="u-margin-block-end-auto">
|
||||
{displayQuickFilters ? 'Advanced filters' : 'Quick filters'}
|
||||
</Button>
|
||||
{/if}
|
||||
<Button size="s" on:click={apply} disabled={isButtonDisabled}>Apply</Button>
|
||||
<div class="u-flex u-gap-8">
|
||||
{#if singleCondition}
|
||||
<Button size="s" text on:click={toggleDropdown}>Cancel</Button>
|
||||
{:else}
|
||||
<Button size="s" disabled={applied === 0} text on:click={clearAll}>
|
||||
Clear all
|
||||
</Button>
|
||||
{/if}
|
||||
<Button size="s" on:click={apply} disabled={isButtonDisabled}
|
||||
>Apply</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</div>
|
||||
|
||||
<div class="menu subMenu" use:melt={$subMenu}>
|
||||
<Card.Base padding="xxxs">
|
||||
<Card.Base padding="none">
|
||||
<div use:melt={$radioGroup}>
|
||||
{#each filter.options as option (option.value + option.checked)}
|
||||
{#if variant === 'radio'}
|
||||
|
||||
+34
-52
@@ -4,33 +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 hasInstallations = !!installations?.total;
|
||||
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();
|
||||
@@ -38,11 +43,9 @@
|
||||
$installation = installations.installations[0];
|
||||
}
|
||||
selectedInstallationId = installations.total ? installations.installations[0]?.$id : '';
|
||||
hasInstallations = !!installations?.total;
|
||||
if (hasInstallations) {
|
||||
if (!!installations?.total) {
|
||||
repositoryBehaviour = 'existing';
|
||||
}
|
||||
console.log(selectedInstallationId);
|
||||
});
|
||||
|
||||
async function connectRepo() {
|
||||
@@ -57,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'
|
||||
@@ -97,9 +79,10 @@
|
||||
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 hasInstallations}
|
||||
{#if !!installations?.total}
|
||||
<Layout.Stack gap="xl">
|
||||
{#if !onlyExisting}
|
||||
<RepositoryBehaviour bind:repositoryBehaviour />
|
||||
@@ -112,18 +95,17 @@
|
||||
{installations} />
|
||||
{:else}
|
||||
<Repositories
|
||||
bind:hasInstallations
|
||||
bind:selectedRepository
|
||||
product="sites"
|
||||
{product}
|
||||
action="button"
|
||||
{callbackState}
|
||||
on:connect={(e) => {
|
||||
connect={(e) => {
|
||||
trackEvent(Click.ConnectRepositoryClick, {
|
||||
from: 'sites'
|
||||
from: product
|
||||
});
|
||||
repository.set(e.detail);
|
||||
repositoryName = e.detail.name;
|
||||
selectedRepository = e.detail.id;
|
||||
repository.set(e);
|
||||
repositoryName = e.name;
|
||||
selectedRepository = e.id;
|
||||
connectRepo();
|
||||
}} />
|
||||
{/if}
|
||||
@@ -6,34 +6,49 @@
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { IconExternalLink, IconQrcode } from '@appwrite.io/pink-icons-svelte';
|
||||
import { ActionMenu, Icon, Layout, Popover, Tag, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let domains: Models.ProxyRuleList;
|
||||
export let hideQRCode = true;
|
||||
let {
|
||||
domains,
|
||||
hideQRCode = true,
|
||||
showQR = () => {}
|
||||
}: {
|
||||
domains: Models.ProxyRuleList;
|
||||
hideQRCode?: boolean;
|
||||
showQR?: (show: boolean) => void;
|
||||
} = $props();
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let sortedDomains = $derived(
|
||||
domains?.rules?.sort((a, b) => {
|
||||
if (a?.trigger === 'manual' && b?.trigger !== 'manual') {
|
||||
return -1;
|
||||
} else if (a?.trigger !== 'manual' && b?.trigger === 'manual') {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
<Layout.Stack gap="xxs" direction="row" alignItems="center">
|
||||
{#if domains?.total}
|
||||
<Link external href={`${$protocol}${domains.rules[0]?.domain}`} variant="muted">
|
||||
<Link external href={`${$protocol}${sortedDomains[0]?.domain}`} variant="muted">
|
||||
<Layout.Stack gap="xxs" direction="row" alignItems="center">
|
||||
<Trim alternativeTrim>
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-primary">
|
||||
{domains.rules[0]?.domain}
|
||||
{sortedDomains[0]?.domain}
|
||||
</Typography.Text>
|
||||
</Trim>
|
||||
</Layout.Stack>
|
||||
</Link>
|
||||
|
||||
{#if domains.rules.length > 1}
|
||||
{#if sortedDomains.length > 1}
|
||||
<Popover padding="none" let:toggle>
|
||||
<Tag size="xs" on:click={toggle}>
|
||||
+{domains.rules.length - 1}
|
||||
+{sortedDomains.length - 1}
|
||||
</Tag>
|
||||
<svelte:fragment slot="tooltip">
|
||||
<ActionMenu.Root>
|
||||
{#each domains.rules as rule, i}
|
||||
{#each sortedDomains as rule, i}
|
||||
{#if i !== 0}
|
||||
<ActionMenu.Item.Anchor
|
||||
href={`${$protocol}${rule.domain}`}
|
||||
@@ -50,7 +65,7 @@
|
||||
</Popover>
|
||||
{/if}
|
||||
{#if !hideQRCode}
|
||||
<Button icon secondary size="xs" on:click={() => dispatch('showQR')}>
|
||||
<Button icon secondary size="xs" on:click={() => showQR(true)}>
|
||||
<Icon icon={IconQrcode} size="s" />
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
@@ -11,7 +11,11 @@
|
||||
} from '@appwrite.io/pink-icons-svelte';
|
||||
import { ActionMenu, Layout, Popover, Icon } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let deployment: Models.Deployment;
|
||||
let {
|
||||
deployment
|
||||
}: {
|
||||
deployment: Models.Deployment;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
{#if deployment.type === 'vcs'}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Button, InputSelect, InputText } from '$lib/elements/forms';
|
||||
import { Fieldset, Layout, Selector, Skeleton } from '@appwrite.io/pink-svelte';
|
||||
import { Fieldset, Input, Layout, Selector, Skeleton } from '@appwrite.io/pink-svelte';
|
||||
import SelectRootModal from './selectRootModal.svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { sortBranches } from '$lib/stores/vcs';
|
||||
@@ -60,7 +60,7 @@
|
||||
return a.label > b.label ? 1 : -1;
|
||||
}) ?? []}
|
||||
<Layout.Stack gap="xl">
|
||||
<InputSelect
|
||||
<Input.ComboBox
|
||||
required
|
||||
id="branch"
|
||||
label="Production branch"
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { repositories } from '$routes/(console)/project-[project]/functions/function-[function]/store';
|
||||
import { installation, installations, repository } from '$lib/stores/vcs';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import {
|
||||
Layout,
|
||||
Table,
|
||||
@@ -15,27 +14,33 @@
|
||||
Skeleton,
|
||||
Button as PinkButton
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import { IconLockClosed } from '@appwrite.io/pink-icons-svelte';
|
||||
import { IconLockClosed, IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
import ConnectGit from './connectGit.svelte';
|
||||
import SvgIcon from '../svgIcon.svelte';
|
||||
import { VCSDetectionType, type Models } from '@appwrite.io/console';
|
||||
import { getFrameworkIcon } from '$lib/stores/sites';
|
||||
import { connectGitHub } from '$lib/stores/git';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let {
|
||||
action = $bindable('select'),
|
||||
selectedRepository = $bindable(undefined),
|
||||
installationList = $bindable($installations),
|
||||
hasInstallations = $bindable($installations?.total > 0),
|
||||
product = 'functions',
|
||||
callbackState = null,
|
||||
connect = () => {}
|
||||
}: {
|
||||
action?: 'button' | 'select';
|
||||
selectedRepository?: string;
|
||||
installationList?: Models.InstallationList;
|
||||
hasInstallations?: boolean;
|
||||
product?: 'functions' | 'sites';
|
||||
callbackState?: Record<string, string> | null;
|
||||
connect?: (repository: Models.ProviderRepository) => void;
|
||||
} = $props();
|
||||
|
||||
export let callbackState: Record<string, string> = null;
|
||||
export let selectedRepository: string = null;
|
||||
export let hasInstallations = false;
|
||||
export let action: 'button' | 'select' = 'select';
|
||||
export let installationList = $installations;
|
||||
export let product: 'functions' | 'sites' = 'functions';
|
||||
|
||||
let search = '';
|
||||
let selectedInstallation = null;
|
||||
|
||||
$: {
|
||||
hasInstallations = installationList?.total > 0;
|
||||
}
|
||||
let search = $state('');
|
||||
let selectedInstallation = $state(null);
|
||||
|
||||
async function loadInstallations() {
|
||||
if (installationList) {
|
||||
@@ -64,24 +69,7 @@
|
||||
$repositories.installationId !== installationId ||
|
||||
$repositories.search !== search
|
||||
) {
|
||||
//TODO: remove forced cast after backend fixes
|
||||
if (product === 'functions') {
|
||||
$repositories.repositories = (
|
||||
(await sdk.forProject.vcs.listRepositories(
|
||||
installationId,
|
||||
VCSDetectionType.Runtime,
|
||||
search || undefined
|
||||
)) as unknown as Models.ProviderRepositoryRuntimeList
|
||||
).runtimeProviderRepositories;
|
||||
} else {
|
||||
$repositories.repositories = (
|
||||
await sdk.forProject.vcs.listRepositories(
|
||||
installationId,
|
||||
VCSDetectionType.Framework,
|
||||
search || undefined
|
||||
)
|
||||
).frameworkProviderRepositories;
|
||||
}
|
||||
await fetchRepos(installationId, search);
|
||||
}
|
||||
|
||||
$repositories.search = search;
|
||||
@@ -94,6 +82,26 @@
|
||||
|
||||
return $repositories.repositories;
|
||||
}
|
||||
|
||||
async function fetchRepos(installationId: string, search: string) {
|
||||
if (product === 'functions') {
|
||||
$repositories.repositories = (
|
||||
(await sdk.forProject.vcs.listRepositories(
|
||||
installationId,
|
||||
VCSDetectionType.Runtime,
|
||||
search || undefined
|
||||
)) as unknown as Models.ProviderRepositoryRuntimeList
|
||||
).runtimeProviderRepositories; //TODO: remove forced cast after backend fixes
|
||||
} else {
|
||||
$repositories.repositories = (
|
||||
(await sdk.forProject.vcs.listRepositories(
|
||||
installationId,
|
||||
VCSDetectionType.Framework,
|
||||
search || undefined
|
||||
)) as unknown as Models.ProviderRepositoryFrameworkList
|
||||
).frameworkProviderRepositories;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if hasInstallations}
|
||||
@@ -117,13 +125,23 @@
|
||||
<Layout.Stack direction="row">
|
||||
<InputSelect
|
||||
id="installation"
|
||||
options={installations.map((entry) => {
|
||||
return {
|
||||
label: entry.organization,
|
||||
value: entry.$id
|
||||
};
|
||||
})}
|
||||
options={[
|
||||
...installations.map((entry) => {
|
||||
return {
|
||||
label: entry.organization,
|
||||
value: entry.$id
|
||||
};
|
||||
}),
|
||||
{
|
||||
label: 'Add installation',
|
||||
leadingIcon: IconPlus,
|
||||
value: 'new'
|
||||
}
|
||||
]}
|
||||
on:change={() => {
|
||||
if (selectedInstallation === 'new') {
|
||||
window.location.href = connectGitHub(callbackState).toString();
|
||||
}
|
||||
search = '';
|
||||
installation.set(
|
||||
installations.find((entry) => entry.$id === selectedInstallation)
|
||||
@@ -154,84 +172,90 @@
|
||||
</Table.Root>
|
||||
{:then response}
|
||||
{#if response?.length}
|
||||
<Paginator
|
||||
items={response}
|
||||
let:paginatedItems
|
||||
hideFooter={response?.length <= 6}
|
||||
limit={6}>
|
||||
<Table.Root columns={1} let:root>
|
||||
{#each paginatedItems as repo}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell {root}>
|
||||
<Layout.Stack direction="row" alignItems="center" gap="s">
|
||||
{#if action === 'select'}
|
||||
<input
|
||||
class="is-small u-margin-inline-end-8"
|
||||
type="radio"
|
||||
name="repositories"
|
||||
bind:group={selectedRepository}
|
||||
on:change={() => repository.set(repo)}
|
||||
value={repo.id} />
|
||||
{/if}
|
||||
{#if product === 'sites'}
|
||||
{#if repo?.framework && repo.framework !== 'other'}
|
||||
<Avatar size="xs" alt={repo.name}>
|
||||
<SvgIcon
|
||||
name={getFrameworkIcon(repo.framework)}
|
||||
iconSize="small" />
|
||||
</Avatar>
|
||||
{:else}
|
||||
<Avatar size="xs" alt={repo.name} empty />
|
||||
{/if}
|
||||
{:else}
|
||||
{@const iconName = repo?.runtime
|
||||
? repo.runtime.split('-')[0]
|
||||
: undefined}
|
||||
<Avatar size="xs" alt={repo.name} empty={!iconName}>
|
||||
<SvgIcon name={iconName} iconSize="small" />
|
||||
</Avatar>
|
||||
{/if}
|
||||
<Paginator items={response} hideFooter={response?.length <= 6} limit={6}>
|
||||
{#snippet children(paginatedItems)}
|
||||
<Table.Root columns={1} let:root>
|
||||
{#each paginatedItems as repo}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell {root}>
|
||||
<Layout.Stack
|
||||
gap="s"
|
||||
direction="row"
|
||||
alignItems="center">
|
||||
<Typography.Text
|
||||
truncate
|
||||
color="--fgcolor-neutral-secondary">
|
||||
{repo.name}
|
||||
</Typography.Text>
|
||||
{#if repo.private}
|
||||
<Icon
|
||||
size="s"
|
||||
icon={IconLockClosed}
|
||||
color="--fgcolor-neutral-tertiary" />
|
||||
alignItems="center"
|
||||
gap="s">
|
||||
{#if action === 'select'}
|
||||
<input
|
||||
class="is-small u-margin-inline-end-8"
|
||||
type="radio"
|
||||
name="repositories"
|
||||
bind:group={selectedRepository}
|
||||
onchange={() => repository.set(repo)}
|
||||
value={repo.id} />
|
||||
{/if}
|
||||
<time datetime={repo.pushedAt}>
|
||||
<Typography.Caption
|
||||
variant="400"
|
||||
truncate
|
||||
color="--fgcolor-neutral-tertiary">
|
||||
{timeFromNow(repo.pushedAt)}
|
||||
</Typography.Caption>
|
||||
</time>
|
||||
</Layout.Stack>
|
||||
{#if action === 'button'}
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end">
|
||||
<PinkButton.Button
|
||||
{#if product === 'sites'}
|
||||
{#if repo?.framework && repo.framework !== 'other'}
|
||||
<Avatar size="xs" alt={repo.name}>
|
||||
<SvgIcon
|
||||
name={getFrameworkIcon(
|
||||
repo.framework
|
||||
)}
|
||||
iconSize="small" />
|
||||
</Avatar>
|
||||
{:else}
|
||||
<Avatar size="xs" alt={repo.name} empty />
|
||||
{/if}
|
||||
{:else}
|
||||
{@const iconName = repo?.runtime
|
||||
? repo.runtime.split('-')[0]
|
||||
: undefined}
|
||||
<Avatar
|
||||
size="xs"
|
||||
variant="secondary"
|
||||
on:click={() => dispatch('connect', repo)}>
|
||||
Connect
|
||||
</PinkButton.Button>
|
||||
alt={repo.name}
|
||||
empty={!iconName}>
|
||||
<SvgIcon name={iconName} iconSize="small" />
|
||||
</Avatar>
|
||||
{/if}
|
||||
<Layout.Stack
|
||||
gap="s"
|
||||
direction="row"
|
||||
alignItems="center">
|
||||
<Typography.Text
|
||||
truncate
|
||||
color="--fgcolor-neutral-secondary">
|
||||
{repo.name}
|
||||
</Typography.Text>
|
||||
{#if repo.private}
|
||||
<Icon
|
||||
size="s"
|
||||
icon={IconLockClosed}
|
||||
color="--fgcolor-neutral-tertiary" />
|
||||
{/if}
|
||||
<time datetime={repo.pushedAt}>
|
||||
<Typography.Caption
|
||||
variant="400"
|
||||
truncate
|
||||
color="--fgcolor-neutral-tertiary">
|
||||
{timeFromNow(repo.pushedAt)}
|
||||
</Typography.Caption>
|
||||
</time>
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
{#if action === 'button'}
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end">
|
||||
<PinkButton.Button
|
||||
size="xs"
|
||||
variant="secondary"
|
||||
on:click={() => connect(repo)}>
|
||||
Connect
|
||||
</PinkButton.Button>
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
{/snippet}
|
||||
</Paginator>
|
||||
{:else}
|
||||
<EmptySearch hidePages hidePagination bind:search target="repositories">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Card } from '@appwrite.io/pink-svelte';
|
||||
import { createMenubar, melt } from '@melt-ui/svelte';
|
||||
import { menuOpen } from '$lib/components/menu/store';
|
||||
|
||||
const {
|
||||
elements: { menubar },
|
||||
@@ -15,15 +16,20 @@
|
||||
function toggle() {
|
||||
open.update((state) => !state);
|
||||
}
|
||||
|
||||
$: menuOpen.set($open);
|
||||
</script>
|
||||
|
||||
<div use:melt={$menubar}>
|
||||
{#if $open}
|
||||
<div style:display="none" id="actionmenuopen"></div>
|
||||
{/if}
|
||||
<div use:melt={$trigger}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div class="menu" use:melt={$menu}>
|
||||
<Card.Base padding="xxxs">
|
||||
<Card.Base padding="none">
|
||||
{#if $$slots.start}
|
||||
<slot name="start" />
|
||||
<div class="separator" use:melt={$separator}></div>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const menuOpen = writable(false);
|
||||
@@ -21,7 +21,7 @@
|
||||
</div>
|
||||
|
||||
<div class="subMenu" use:melt={$subMenu}>
|
||||
<Card.Base padding="xxxs">
|
||||
<Card.Base padding="none">
|
||||
{#if $$slots.start}
|
||||
<slot name="start" />
|
||||
<div class="separator" use:melt={$separator}></div>
|
||||
|
||||
@@ -243,8 +243,11 @@
|
||||
class:keepTransformTransition={shouldAnimateThemeToggle}>
|
||||
<ToggleButton
|
||||
bind:active={activeTheme}
|
||||
on:change={() =>
|
||||
(shouldAnimateThemeToggle = true)}
|
||||
on:change={() => {
|
||||
setTimeout(() => {
|
||||
shouldAnimateThemeToggle = true;
|
||||
}, 150);
|
||||
}}
|
||||
buttons={[
|
||||
{
|
||||
id: 'light',
|
||||
|
||||
@@ -54,14 +54,7 @@
|
||||
</script>
|
||||
|
||||
{#if !hidePages}
|
||||
<Pagination
|
||||
{limit}
|
||||
page={currentPage}
|
||||
{total}
|
||||
type="button"
|
||||
on:next={next}
|
||||
on:page={handleOptionClick}
|
||||
on:prev={prev} />
|
||||
<Pagination {limit} page={currentPage} {total} type="button" on:page={handleOptionClick} />
|
||||
{:else}
|
||||
<Layout.Stack direction="row" inline>
|
||||
<Button.Button
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
hideFooter = false,
|
||||
hidePages = true,
|
||||
hasLimit = false,
|
||||
name = 'items'
|
||||
name = 'items',
|
||||
children
|
||||
} = $props();
|
||||
|
||||
let total = $derived(items.length);
|
||||
@@ -20,7 +21,7 @@
|
||||
</script>
|
||||
|
||||
<Layout.Stack gap="s">
|
||||
<slot {paginatedItems} {limit} />
|
||||
{@render children(paginatedItems, limit)}
|
||||
|
||||
{#if !hideFooter}
|
||||
<Layout.Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
|
||||
@@ -215,7 +215,7 @@
|
||||
{groups}
|
||||
on:create={create}
|
||||
let:toggle>
|
||||
<Button secondary icon on:click={toggle}>
|
||||
<Button compact icon on:click={toggle}>
|
||||
<Icon icon={IconPlus} size="s" />
|
||||
</Button>
|
||||
</Actions>
|
||||
|
||||
@@ -1,61 +1,50 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { onMount } from 'svelte';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { Icon, Input } from '@appwrite.io/pink-svelte';
|
||||
import { IconSearch, IconX } from '@appwrite.io/pink-icons-svelte';
|
||||
import { debounce as createDebounce } from '$lib/helpers/debounce.js';
|
||||
|
||||
export let search = '';
|
||||
export let placeholder = '';
|
||||
export let debounce = 250;
|
||||
export let required = false;
|
||||
export let disabled = false;
|
||||
export let autofocus = false;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
let inputValue = page.url.searchParams.get('search') ?? '';
|
||||
|
||||
onMount(() => {
|
||||
if (element && autofocus) {
|
||||
element.focus();
|
||||
const runSearch = createDebounce((value: string) => {
|
||||
const trimmed = value.trim();
|
||||
const url = new URL(page.url);
|
||||
const previous = url.searchParams.get('search') ?? '';
|
||||
|
||||
if (previous === trimmed) return;
|
||||
|
||||
if (page.data.page > 1) {
|
||||
url.searchParams.delete('page');
|
||||
}
|
||||
});
|
||||
|
||||
if (trimmed === '') {
|
||||
url.searchParams.delete('search');
|
||||
} else {
|
||||
url.searchParams.set('search', trimmed);
|
||||
}
|
||||
|
||||
trackEvent('search');
|
||||
goto(url, { keepFocus: true });
|
||||
}, debounce);
|
||||
|
||||
$: runSearch(inputValue);
|
||||
|
||||
function clearInput() {
|
||||
inputValue = '';
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
search = '';
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
runSearch.cancel?.();
|
||||
});
|
||||
|
||||
$: valueChange(search ?? '');
|
||||
|
||||
function valueChange(value: string) {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
const url = new URL(page.url);
|
||||
const previous = url.searchParams.get('search') ?? '';
|
||||
|
||||
if (previous === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (page.data.page > 1) {
|
||||
url.searchParams.delete('page');
|
||||
}
|
||||
|
||||
if (value === '') {
|
||||
url.searchParams.delete('search');
|
||||
} else {
|
||||
url.searchParams.set('search', value);
|
||||
}
|
||||
|
||||
trackEvent('search');
|
||||
goto(url, { keepFocus: true });
|
||||
}, debounce);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div style:max-width="360px" style:width="100%">
|
||||
@@ -63,14 +52,15 @@
|
||||
{placeholder}
|
||||
{disabled}
|
||||
{required}
|
||||
bind:value={search}
|
||||
{autofocus}
|
||||
bind:value={inputValue}
|
||||
--bgcolor-neutral-default="var(--bgcolor-neutral-primary)">
|
||||
<svelte:fragment slot="start">
|
||||
<Icon icon={IconSearch} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="end">
|
||||
{#if search}
|
||||
<Input.Action icon={IconX} on:click={() => (search = '')} />
|
||||
{#if inputValue}
|
||||
<Input.Action icon={IconX} on:click={clearInput} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Input.Text>
|
||||
|
||||
@@ -70,8 +70,7 @@
|
||||
}
|
||||
|
||||
$: state = $isTabletViewport ? 'closed' : getSidebarState();
|
||||
$: pathname = page.url.pathname;
|
||||
$: isOnProjectSettings = /^\/console\/project-[a-zA-Z0-9-]+\/settings$/.test(pathname);
|
||||
$: isOnProjectSettings = /^\/console\/project-[a-zA-Z0-9-]+\/settings$/.test(page.url.pathname);
|
||||
|
||||
const projectOptions = [
|
||||
{ name: 'Auth', icon: IconUserGroup, slug: 'auth', category: 'build' },
|
||||
@@ -145,7 +144,7 @@
|
||||
<a
|
||||
href={`/console/project-${project.$id}/overview`}
|
||||
class="link"
|
||||
class:active={pathname.includes('overview')}
|
||||
class:active={page.url.pathname.includes('overview')}
|
||||
on:click={() => {
|
||||
trackEvent(Click.MenuOverviewClick);
|
||||
sideBarIsOpen = false;
|
||||
@@ -175,7 +174,7 @@
|
||||
<a
|
||||
href={`/console/project-${project.$id}/${projectOption.slug}`}
|
||||
class="link"
|
||||
class:active={pathname.includes(projectOption.slug)}
|
||||
class:active={page.url.pathname.includes(projectOption.slug)}
|
||||
on:click={() => {
|
||||
trackEvent(`click_menu_${projectOption.slug}`);
|
||||
sideBarIsOpen = false;
|
||||
@@ -206,7 +205,7 @@
|
||||
<a
|
||||
href={`/console/project-${project.$id}/${projectOption.slug}`}
|
||||
class="link"
|
||||
class:active={pathname.includes(projectOption.slug)}
|
||||
class:active={page.url.pathname.includes(projectOption.slug)}
|
||||
on:click={() => {
|
||||
trackEvent(`click_menu_${projectOption.slug}`);
|
||||
sideBarIsOpen = false;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import FilePicker from '$lib/components/filePicker.svelte';
|
||||
import { humanFileSize } from '$lib/helpers/sizeConvertion';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { Label } from '.';
|
||||
import { Button, Label } from '.';
|
||||
|
||||
export let label: string = null;
|
||||
export let value: Models.File = null;
|
||||
@@ -62,13 +62,7 @@
|
||||
</div>
|
||||
<div class="u-flex u-main-center u-cross-center u-gap-16 u-flex-vertical-mobile">
|
||||
Max file size: 1MB
|
||||
<button
|
||||
class="button is-secondary is-full-width-mobile"
|
||||
type="button"
|
||||
{disabled}
|
||||
on:click={() => (show = true)}>
|
||||
<span class="text">Browse</span>
|
||||
</button>
|
||||
<Button secondary {disabled} on:click={() => (show = true)}>Browse</Button>
|
||||
</div>
|
||||
|
||||
{#if value}
|
||||
|
||||
@@ -190,6 +190,13 @@ export const diffDays = (date1: Date, date2: Date) => {
|
||||
};
|
||||
|
||||
export function timeFromNow(datetime: string): string {
|
||||
if (!datetime) {
|
||||
return 'unknown time';
|
||||
}
|
||||
if (!isValidDate(datetime)) {
|
||||
return 'invalid date';
|
||||
}
|
||||
|
||||
return dayjs().to(dayjs(datetime));
|
||||
}
|
||||
|
||||
@@ -216,3 +223,12 @@ export function getUTCOffset(): string {
|
||||
|
||||
return `${hours >= 0 ? '+' : ''}${hours}${minutes ? `:${minutes.toString().padStart(2, '0')}` : ''}`;
|
||||
}
|
||||
|
||||
export function toISOString(date: string): string {
|
||||
const d = new Date(date);
|
||||
|
||||
if (isNaN(d.getTime())) {
|
||||
return 'n/a';
|
||||
}
|
||||
return d.toISOString();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { parseDnsRecords } from './domains';
|
||||
|
||||
describe('parseDnsRecords', () => {
|
||||
it('should parse standard zone file with origin', () => {
|
||||
const content = `$ORIGIN example.com. ; designates the start of this zone file in the namespace
|
||||
$TTL 3600 ; default expiration time (in seconds) of all RRs without their own TTL value
|
||||
example.com. IN SOA ns.example.com. username.example.com. ( 2020091025 7200 3600 1209600 3600 )
|
||||
example.com. IN NS ns ; ns.example.com is a nameserver for example.com
|
||||
example.com. IN NS ns.somewhere.example. ; ns.somewhere.example is a backup nameserver for example.com
|
||||
example.com. IN MX 10 mail.example.com. ; mail.example.com is the mailserver for example.com
|
||||
@ IN MX 20 mail2.example.com. ; equivalent to above line, "@" represents zone origin
|
||||
@ IN MX 50 mail3 ; equivalent to above line, but using a relative host name
|
||||
example.com. IN A 192.0.2.1 ; IPv4 address for example.com
|
||||
IN AAAA 2001:db8:10::1 ; IPv6 address for example.com
|
||||
ns IN A 192.0.2.2 ; IPv4 address for ns.example.com
|
||||
IN AAAA 2001:db8:10::2 ; IPv6 address for ns.example.com
|
||||
www IN CNAME example.com. ; www.example.com is an alias for example.com
|
||||
wwwtest IN CNAME www ; wwwtest.example.com is another alias for www.example.com
|
||||
mail IN A 192.0.2.3 ; IPv4 address for mail.example.com
|
||||
mail2 IN A 192.0.2.4 ; IPv4 address for mail2.example.com
|
||||
mail3 IN A 192.0.2.5 ; IPv4 address for mail3.example.com`;
|
||||
|
||||
const result = parseDnsRecords(content);
|
||||
|
||||
expect(result.NS).toHaveLength(2);
|
||||
expect(result.NS[0].name).toBe('example.com');
|
||||
expect(result.NS[0].value).toBe('ns');
|
||||
expect(result.NS[1].name).toBe('example.com');
|
||||
expect(result.NS[1].value).toBe('ns.somewhere.example');
|
||||
|
||||
expect(result.MX).toHaveLength(3);
|
||||
expect(result.MX[0].name).toBe('example.com');
|
||||
expect(result.MX[0].priority).toBe(10);
|
||||
expect(result.MX[0].value).toBe('mail.example.com');
|
||||
expect(result.MX[1].name).toBe('');
|
||||
expect(result.MX[1].priority).toBe(20);
|
||||
expect(result.MX[1].value).toBe('mail2.example.com');
|
||||
expect(result.MX[2].name).toBe('');
|
||||
expect(result.MX[2].priority).toBe(50);
|
||||
expect(result.MX[2].value).toBe('mail3');
|
||||
|
||||
expect(result.A).toHaveLength(5);
|
||||
expect(result.A[0].name).toBe('example.com');
|
||||
expect(result.A[0].value).toBe('192.0.2.1');
|
||||
expect(result.A[1].name).toBe('ns');
|
||||
expect(result.A[1].value).toBe('192.0.2.2');
|
||||
expect(result.A[2].name).toBe('mail');
|
||||
expect(result.A[2].value).toBe('192.0.2.3');
|
||||
expect(result.A[3].name).toBe('mail2');
|
||||
expect(result.A[3].value).toBe('192.0.2.4');
|
||||
expect(result.A[4].name).toBe('mail3');
|
||||
expect(result.A[4].value).toBe('192.0.2.5');
|
||||
|
||||
expect(result.AAAA).toHaveLength(2);
|
||||
expect(result.AAAA[0].name).toBe('example.com');
|
||||
expect(result.AAAA[0].value).toBe('2001:db8:10::1');
|
||||
expect(result.AAAA[1].name).toBe('ns');
|
||||
expect(result.AAAA[1].value).toBe('2001:db8:10::2');
|
||||
|
||||
expect(result.CNAME).toHaveLength(2);
|
||||
expect(result.CNAME[0].name).toBe('www');
|
||||
expect(result.CNAME[0].value).toBe('example.com');
|
||||
expect(result.CNAME[1].name).toBe('wwwtest');
|
||||
expect(result.CNAME[1].value).toBe('www');
|
||||
});
|
||||
|
||||
it('should parse localhost zone file', () => {
|
||||
const content = `$ORIGIN localhost.
|
||||
@ 86400 IN SOA @ root (
|
||||
1999010100 ; serial
|
||||
10800 ; refresh (3 hours)
|
||||
900 ; retry (15 minutes)
|
||||
604800 ; expire (1 week)
|
||||
86400 ; minimum (1 day)
|
||||
)
|
||||
@ 86400 IN NS @
|
||||
@ 86400 IN A 127.0.0.1
|
||||
@ 86400 IN AAAA ::1`;
|
||||
|
||||
const result = parseDnsRecords(content);
|
||||
|
||||
expect(result.NS).toHaveLength(1);
|
||||
expect(result.NS[0].name).toBe('');
|
||||
expect(result.NS[0].value).toBe('@');
|
||||
expect(result.NS[0].ttl).toBe(86400);
|
||||
|
||||
expect(result.A).toHaveLength(1);
|
||||
expect(result.A[0].name).toBe('');
|
||||
expect(result.A[0].value).toBe('127.0.0.1');
|
||||
expect(result.A[0].ttl).toBe(86400);
|
||||
|
||||
expect(result.AAAA).toHaveLength(1);
|
||||
expect(result.AAAA[0].name).toBe('');
|
||||
expect(result.AAAA[0].value).toBe('::1');
|
||||
expect(result.AAAA[0].ttl).toBe(86400);
|
||||
});
|
||||
|
||||
it('should parse complex zone file with multiple record types and comments', () => {
|
||||
const content = `; Exported (y-m-d hh:mm:ss): 2019-01-10 13:05:04
|
||||
;
|
||||
; This file is intended for use for informational and archival
|
||||
; purposes ONLY and MUST be edited before use on a production
|
||||
; DNS server.
|
||||
;
|
||||
; In particular, you must update the SOA record with the correct
|
||||
; authoritative name server and contact e-mail address information,
|
||||
; and add the correct NS records for the name servers which will
|
||||
; be authoritative for this domain.
|
||||
;
|
||||
; For further information, please consult the BIND documentation
|
||||
; located on the following website:
|
||||
;
|
||||
; http://www.isc.org/
|
||||
;
|
||||
; And RFC 1035:
|
||||
;
|
||||
; http://www.ietf.org/rfc/rfc1035.txt
|
||||
;
|
||||
; Please note that we do NOT offer technical support for any use
|
||||
; of this zone data, the BIND name server, or any other third-
|
||||
; party DNS software.
|
||||
;
|
||||
; Use at your own risk.
|
||||
; SOA Record
|
||||
example.com. 3600 IN SOA ns41.domaincontrol.com. dns.net. (
|
||||
2018122702
|
||||
28800
|
||||
7200
|
||||
604800
|
||||
3600
|
||||
)
|
||||
; A Records
|
||||
@ 600 IN A 192.0.2.249
|
||||
blog 10800 IN A 192.0.2.255
|
||||
dev 1800 IN A 192.0.2.254
|
||||
dev01 1800 IN A 192.0.2.253
|
||||
dev02 1800 IN A 192.0.2.252
|
||||
dev03 1800 IN A 192.0.2.251
|
||||
dev04 1800 IN A 192.0.2.250
|
||||
; CNAME Records
|
||||
abc123b432dc7785b7ef31f04f25c3e71 1800 IN CNAME verify.bing.com.
|
||||
akamai 600 IN CNAME www.example.com.edgekey.net.
|
||||
email 3600 IN CNAME email.secureserver.net.
|
||||
; MX Records
|
||||
@ 604800 IN MX 10 amlxe.l.google.com.
|
||||
@ 604800 IN MX 10 aplxe.l.google.com.
|
||||
; TXT Records
|
||||
@ 3600 IN TXT "google-site-verification=3J82-80dbMyCo5Q5C1G11JszeOnZPGCSYlHcPcXg"
|
||||
@ 3600 IN TXT "google-site-verification=eS_QPYLE_W4nduSrlN-cddxG7ZqOnB743xsbX918"`;
|
||||
|
||||
const result = parseDnsRecords(content);
|
||||
|
||||
expect(result.A).toHaveLength(7);
|
||||
expect(result.A[0].name).toBe('');
|
||||
expect(result.A[0].value).toBe('192.0.2.249');
|
||||
expect(result.A[0].ttl).toBe(600);
|
||||
expect(result.A[1].name).toBe('blog');
|
||||
expect(result.A[1].value).toBe('192.0.2.255');
|
||||
expect(result.A[1].ttl).toBe(10800);
|
||||
|
||||
expect(result.CNAME).toHaveLength(3);
|
||||
expect(result.CNAME[0].name).toBe('abc123b432dc7785b7ef31f04f25c3e71');
|
||||
expect(result.CNAME[0].value).toBe('verify.bing.com');
|
||||
expect(result.CNAME[0].ttl).toBe(1800);
|
||||
expect(result.CNAME[1].name).toBe('akamai');
|
||||
expect(result.CNAME[1].value).toBe('www.example.com.edgekey.net');
|
||||
expect(result.CNAME[1].ttl).toBe(600);
|
||||
|
||||
expect(result.MX).toHaveLength(2);
|
||||
expect(result.MX[0].name).toBe('');
|
||||
expect(result.MX[0].value).toBe('amlxe.l.google.com');
|
||||
expect(result.MX[0].priority).toBe(10);
|
||||
expect(result.MX[0].ttl).toBe(604800);
|
||||
expect(result.MX[1].name).toBe('');
|
||||
expect(result.MX[1].value).toBe('aplxe.l.google.com');
|
||||
expect(result.MX[1].priority).toBe(10);
|
||||
expect(result.MX[1].ttl).toBe(604800);
|
||||
|
||||
expect(result.TXT).toHaveLength(2);
|
||||
expect(result.TXT[0].name).toBe('');
|
||||
expect(result.TXT[0].value).toBe(
|
||||
'google-site-verification=3J82-80dbMyCo5Q5C1G11JszeOnZPGCSYlHcPcXg'
|
||||
);
|
||||
expect(result.TXT[0].ttl).toBe(3600);
|
||||
expect(result.TXT[1].name).toBe('');
|
||||
expect(result.TXT[1].value).toBe(
|
||||
'google-site-verification=eS_QPYLE_W4nduSrlN-cddxG7ZqOnB743xsbX918'
|
||||
);
|
||||
expect(result.TXT[1].ttl).toBe(3600);
|
||||
});
|
||||
|
||||
it('should parse zone file with origin prefix', () => {
|
||||
const content = `$ORIGIN example.com.
|
||||
example.com. 3600 IN SOA ns41.domaincontrol.com. dns.net. (
|
||||
2018122702
|
||||
28800
|
||||
7200
|
||||
604800
|
||||
3600
|
||||
)
|
||||
|
||||
; A Records
|
||||
@ 600 IN A 192.0.2.249
|
||||
blog 10800 IN A 192.0.2.255
|
||||
dev 1800 IN A 192.0.2.254
|
||||
dev01 1800 IN A 192.0.2.253
|
||||
dev02 1800 IN A 192.0.2.252
|
||||
dev03 1800 IN A 192.0.2.251
|
||||
dev04 1800 IN A 192.0.2.250
|
||||
abc123b432dc7785b7ef31f04f25c3e71 1800 IN CNAME verify.bing.com.
|
||||
; CNAME Records
|
||||
akamai 600 IN CNAME www.example.edgekey.net.
|
||||
email 3600 IN CNAME email.secureserver.net.
|
||||
; MX Records
|
||||
@ 604800 IN MX 10 amlxe.l.google.com.
|
||||
@ 604800 IN MX 10 aplxe.l.google.com.
|
||||
; TXT Records
|
||||
@ 3600 IN TXT "google-site-verification=3J82-80dbMyCo5Q5C1GM8os1VYVEOnZPGCSYlHcPcXg"
|
||||
@ 3600 IN TXT "google-site-verification=eS_QPYLE_W4nduSrlN-cddxG7ZqOnB7k7uIG7qrsyu8"`;
|
||||
|
||||
const result = parseDnsRecords(content);
|
||||
|
||||
expect(result.A).toHaveLength(7);
|
||||
expect(result.A[0].name).toBe('');
|
||||
expect(result.A[0].value).toBe('192.0.2.249');
|
||||
expect(result.A[0].ttl).toBe(600);
|
||||
|
||||
expect(result.CNAME).toHaveLength(3);
|
||||
expect(result.CNAME[0].name).toBe('abc123b432dc7785b7ef31f04f25c3e71');
|
||||
expect(result.CNAME[0].value).toBe('verify.bing.com');
|
||||
expect(result.CNAME[1].name).toBe('akamai');
|
||||
expect(result.CNAME[1].value).toBe('www.example.edgekey.net');
|
||||
|
||||
expect(result.MX).toHaveLength(2);
|
||||
expect(result.MX[0].name).toBe('');
|
||||
expect(result.MX[0].priority).toBe(10);
|
||||
expect(result.MX[0].value).toBe('amlxe.l.google.com');
|
||||
|
||||
expect(result.TXT).toHaveLength(2);
|
||||
expect(result.TXT[0].name).toBe('');
|
||||
expect(result.TXT[0].value).toBe(
|
||||
'google-site-verification=3J82-80dbMyCo5Q5C1GM8os1VYVEOnZPGCSYlHcPcXg'
|
||||
);
|
||||
});
|
||||
|
||||
it('should parse complex zone file with multiple servers', () => {
|
||||
const content = `$ORIGIN example.com.
|
||||
$TTL 86400
|
||||
@ IN SOA dns1.example.com. hostmaster.example.com. (
|
||||
2001062501 ; serial
|
||||
21600 ; refresh after 6 hours
|
||||
3600 ; retry after 1 hour
|
||||
604800 ; expire after 1 week
|
||||
86400 ) ; minimum TTL of 1 day
|
||||
|
||||
|
||||
IN NS dns1.example.com.
|
||||
IN NS dns2.example.com.
|
||||
|
||||
|
||||
IN MX 10 mail.example.com.
|
||||
IN MX 20 mail2.example.com.
|
||||
|
||||
|
||||
dns1 IN A 10.0.1.1
|
||||
dns2 IN A 10.0.1.2
|
||||
|
||||
|
||||
server1 IN A 10.0.1.5
|
||||
server2 IN A 10.0.1.6
|
||||
|
||||
|
||||
ftp IN A 10.0.1.3
|
||||
IN A 10.0.1.4
|
||||
|
||||
mail IN CNAME server1
|
||||
mail2 IN CNAME server2
|
||||
|
||||
|
||||
www IN CNAME server1`;
|
||||
|
||||
const result = parseDnsRecords(content);
|
||||
|
||||
expect(result.NS).toHaveLength(2);
|
||||
expect(result.NS[0].name).toBe('');
|
||||
expect(result.NS[0].value).toBe('dns1.example.com');
|
||||
expect(result.NS[1].name).toBe('');
|
||||
expect(result.NS[1].value).toBe('dns2.example.com');
|
||||
|
||||
expect(result.MX).toHaveLength(2);
|
||||
expect(result.MX[0].name).toBe('');
|
||||
expect(result.MX[0].priority).toBe(10);
|
||||
expect(result.MX[0].value).toBe('mail.example.com');
|
||||
expect(result.MX[1].name).toBe('');
|
||||
expect(result.MX[1].priority).toBe(20);
|
||||
expect(result.MX[1].value).toBe('mail2.example.com');
|
||||
|
||||
expect(result.A).toHaveLength(6);
|
||||
expect(result.A[0].name).toBe('dns1');
|
||||
expect(result.A[0].value).toBe('10.0.1.1');
|
||||
expect(result.A[1].name).toBe('dns2');
|
||||
expect(result.A[1].value).toBe('10.0.1.2');
|
||||
expect(result.A[2].name).toBe('server1');
|
||||
expect(result.A[2].value).toBe('10.0.1.5');
|
||||
expect(result.A[3].name).toBe('server2');
|
||||
expect(result.A[3].value).toBe('10.0.1.6');
|
||||
expect(result.A[4].name).toBe('ftp');
|
||||
expect(result.A[4].value).toBe('10.0.1.3');
|
||||
expect(result.A[5].name).toBe('ftp');
|
||||
expect(result.A[5].value).toBe('10.0.1.4');
|
||||
|
||||
expect(result.CNAME).toHaveLength(3);
|
||||
expect(result.CNAME[0].name).toBe('mail');
|
||||
expect(result.CNAME[0].value).toBe('server1');
|
||||
expect(result.CNAME[1].name).toBe('mail2');
|
||||
expect(result.CNAME[1].value).toBe('server2');
|
||||
expect(result.CNAME[2].name).toBe('www');
|
||||
expect(result.CNAME[2].value).toBe('server1');
|
||||
});
|
||||
|
||||
it('should parse reverse zone file with PTR records', () => {
|
||||
const content = `;; reverse zone file for 127.0.0.1 and ::1
|
||||
$TTL 1814400 ; 3 weeks
|
||||
@ 1814400 IN SOA localhost. root.localhost. (
|
||||
1999010100 ; serial
|
||||
10800 ; refresh (3 hours)
|
||||
900 ; retry (15 minutes)
|
||||
604800 ; expire (1 week)
|
||||
86400 ; minimum (1 day)
|
||||
)
|
||||
@ 1814400 IN NS localhost.
|
||||
1 1814400 IN PTR localhost.`;
|
||||
|
||||
const result = parseDnsRecords(content);
|
||||
|
||||
expect(result.NS).toHaveLength(1);
|
||||
expect(result.NS[0].name).toBe('');
|
||||
expect(result.NS[0].value).toBe('localhost');
|
||||
expect(result.NS[0].ttl).toBe(1814400);
|
||||
|
||||
expect(result.PTR).toHaveLength(1);
|
||||
expect(result.PTR[0].name).toBe('1');
|
||||
expect(result.PTR[0].value).toBe('localhost');
|
||||
expect(result.PTR[0].ttl).toBe(1814400);
|
||||
});
|
||||
|
||||
it('should handle empty content', () => {
|
||||
const result = parseDnsRecords('');
|
||||
expect(result.A).toHaveLength(0);
|
||||
expect(result.AAAA).toHaveLength(0);
|
||||
expect(result.CNAME).toHaveLength(0);
|
||||
expect(result.MX).toHaveLength(0);
|
||||
expect(result.NS).toHaveLength(0);
|
||||
expect(result.TXT).toHaveLength(0);
|
||||
expect(result.PTR).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle invalid content', () => {
|
||||
const result = parseDnsRecords('This is not a valid zone file');
|
||||
expect(result.A).toHaveLength(0);
|
||||
expect(result.AAAA).toHaveLength(0);
|
||||
expect(result.CNAME).toHaveLength(0);
|
||||
expect(result.MX).toHaveLength(0);
|
||||
expect(result.NS).toHaveLength(0);
|
||||
expect(result.TXT).toHaveLength(0);
|
||||
expect(result.PTR).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle content with only comments', () => {
|
||||
const result = parseDnsRecords(`; This is a comment
|
||||
; This is another comment
|
||||
; And one more comment`);
|
||||
expect(result.A).toHaveLength(0);
|
||||
expect(result.AAAA).toHaveLength(0);
|
||||
expect(result.CNAME).toHaveLength(0);
|
||||
expect(result.MX).toHaveLength(0);
|
||||
expect(result.NS).toHaveLength(0);
|
||||
expect(result.TXT).toHaveLength(0);
|
||||
expect(result.PTR).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { DnsRecord } from '$lib/sdk/domains';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
|
||||
export async function createRecord(record: Partial<DnsRecord>, domainId: string) {
|
||||
switch (record.type) {
|
||||
case 'A':
|
||||
return await sdk.forConsole.domains.createRecordA(
|
||||
domainId,
|
||||
record.name,
|
||||
record.value,
|
||||
record.ttl,
|
||||
record?.comment || undefined
|
||||
);
|
||||
case 'AAAA':
|
||||
return await sdk.forConsole.domains.createRecordAAAA(
|
||||
domainId,
|
||||
record.name,
|
||||
record.value,
|
||||
record.ttl,
|
||||
record?.comment || undefined
|
||||
);
|
||||
case 'CNAME':
|
||||
return await sdk.forConsole.domains.createRecordCNAME(
|
||||
domainId,
|
||||
record.name,
|
||||
record.value,
|
||||
record.ttl,
|
||||
record?.comment || undefined
|
||||
);
|
||||
case 'MX':
|
||||
return await sdk.forConsole.domains.createRecordMX(
|
||||
domainId,
|
||||
record.name,
|
||||
record.value,
|
||||
record.priority || 10,
|
||||
record.ttl,
|
||||
record?.comment || undefined
|
||||
);
|
||||
case 'TXT':
|
||||
return await sdk.forConsole.domains.createRecordTXT(
|
||||
domainId,
|
||||
record.name,
|
||||
record.value,
|
||||
record.ttl,
|
||||
record?.comment || undefined
|
||||
);
|
||||
case 'NS':
|
||||
return await sdk.forConsole.domains.createRecordNS(
|
||||
domainId,
|
||||
record.name,
|
||||
record.value,
|
||||
record.ttl,
|
||||
record?.comment || undefined
|
||||
);
|
||||
case 'SRV':
|
||||
return await sdk.forConsole.domains.createRecordSRV(
|
||||
domainId,
|
||||
record.name,
|
||||
record.value,
|
||||
record.ttl,
|
||||
record.priority,
|
||||
record.weight,
|
||||
record.port,
|
||||
record?.comment || undefined
|
||||
);
|
||||
case 'CAA':
|
||||
return await sdk.forConsole.domains.createRecordCAA(
|
||||
domainId,
|
||||
record.name,
|
||||
record.value,
|
||||
record.ttl,
|
||||
record?.comment || undefined
|
||||
);
|
||||
case 'HTTPS':
|
||||
return await sdk.forConsole.domains.createRecordHTTPS(
|
||||
domainId,
|
||||
record.name,
|
||||
record.value,
|
||||
record.ttl,
|
||||
record?.comment || undefined
|
||||
);
|
||||
case 'ALIAS':
|
||||
return await sdk.forConsole.domains.createRecordAlias(
|
||||
domainId,
|
||||
record.name,
|
||||
record.value,
|
||||
record.ttl,
|
||||
record?.comment || undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export type ParsedRecords = {
|
||||
A: Partial<DnsRecord>[];
|
||||
AAAA: Partial<DnsRecord>[];
|
||||
CNAME: Partial<DnsRecord>[];
|
||||
MX: Partial<DnsRecord>[];
|
||||
TXT: Partial<DnsRecord>[];
|
||||
NS: Partial<DnsRecord>[];
|
||||
SRV: Partial<DnsRecord>[];
|
||||
CAA: Partial<DnsRecord>[];
|
||||
PTR: Partial<DnsRecord>[];
|
||||
HTTPS: Partial<DnsRecord>[];
|
||||
ALIAS: Partial<DnsRecord>[];
|
||||
[key: string]: Partial<DnsRecord>[];
|
||||
};
|
||||
@@ -58,7 +58,7 @@
|
||||
size="s"
|
||||
on:click={() => {
|
||||
showCustomId = true;
|
||||
}}><Icon slot="start" icon={IconPencil} /> Project ID</Tag>
|
||||
}}><Icon slot="start" icon={IconPencil} size="s" /> Project ID</Tag>
|
||||
</div>
|
||||
{/if}
|
||||
<CustomId bind:show={showCustomId} name="Project" isProject bind:id />
|
||||
|
||||
@@ -20,3 +20,15 @@ export const statusCodeOptions = [
|
||||
value: 308
|
||||
}
|
||||
];
|
||||
|
||||
export type RecordType =
|
||||
| 'A'
|
||||
| 'AAAA'
|
||||
| 'CNAME'
|
||||
| 'MX'
|
||||
| 'TXT'
|
||||
| 'NS'
|
||||
| 'SRV'
|
||||
| 'CAA'
|
||||
| 'HTTPS'
|
||||
| 'ALIAS';
|
||||
|
||||
@@ -62,7 +62,7 @@ export async function submitStripeCard(name: string, organizationId?: string) {
|
||||
}
|
||||
|
||||
// Element needs to be submitted before confirming the setup intent
|
||||
elements.submit();
|
||||
await elements.submit();
|
||||
|
||||
const baseUrl = 'https://cloud.appwrite.io/console';
|
||||
const accountUrl = `${baseUrl}/account/payments?clientSecret=${clientSecret}`;
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
on:submit={createProject}>
|
||||
<svelte:fragment slot="submit">
|
||||
<Layout.Stack direction="row" justifyContent="flex-end">
|
||||
<Button.Button type="submit" variant="primary" size="s">
|
||||
<Button.Button autofocus type="submit" variant="primary" size="s">
|
||||
Create
|
||||
</Button.Button>
|
||||
</Layout.Stack>
|
||||
|
||||
@@ -269,6 +269,7 @@
|
||||
transition: opacity 0.5s var(--animation-type);
|
||||
|
||||
@media (min-width: 768px) {
|
||||
margin-top: 50px;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,16 +47,21 @@
|
||||
let showCreate = false;
|
||||
let showCreateProjectCloud = false;
|
||||
|
||||
function allServiceDisabled(project: Models.Project): boolean {
|
||||
return false;
|
||||
async function allServiceDisabled(project: Models.Project) {
|
||||
let disabled = true;
|
||||
services.load(project);
|
||||
$services.list.forEach((service) => {
|
||||
if (service.value) {
|
||||
disabled = false;
|
||||
}
|
||||
});
|
||||
return disabled;
|
||||
try {
|
||||
if (!project.$id) return false;
|
||||
if (!project.platforms.length) return false;
|
||||
services.load(project);
|
||||
$services.list.forEach((service) => {
|
||||
if (service.value) {
|
||||
disabled = false;
|
||||
}
|
||||
});
|
||||
return disabled;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function filterPlatforms(platforms: { name: string; icon: string }[]) {
|
||||
@@ -209,12 +214,14 @@
|
||||
<svelte:fragment slot="title">
|
||||
{project.name}
|
||||
</svelte:fragment>
|
||||
{#if allServiceDisabled(project)}
|
||||
<p>
|
||||
<span class="icon-pause" aria-hidden="true"></span> All services are
|
||||
disabled.
|
||||
</p>
|
||||
{/if}
|
||||
{#await allServiceDisabled(project) then isDisabled}
|
||||
{#if isDisabled}
|
||||
<p>
|
||||
<span class="icon-pause" aria-hidden="true"></span> All services are
|
||||
disabled.
|
||||
</p>
|
||||
{/if}
|
||||
{/await}
|
||||
|
||||
{#each platforms as platform, i}
|
||||
{#if i < 3}
|
||||
|
||||
@@ -230,7 +230,7 @@
|
||||
</Typography.Text>
|
||||
</Tooltip>
|
||||
</Layout.Stack>
|
||||
<ActionMenu.Root slot="tooltip">
|
||||
<ActionMenu.Root slot="tooltip" let:toggle>
|
||||
{#if $paymentMethods.total}
|
||||
{#each filteredPaymentMethods as paymentMethod}
|
||||
<ActionMenu.Item.Button
|
||||
@@ -245,7 +245,10 @@
|
||||
<Divider />
|
||||
<ActionMenu.Item.Button
|
||||
leadingIcon={IconPlus}
|
||||
on:click={() => (showPayment = true)}>
|
||||
on:click={(e) => {
|
||||
toggle(e);
|
||||
showPayment = true;
|
||||
}}>
|
||||
Add new payment method
|
||||
</ActionMenu.Item.Button>
|
||||
</ActionMenu.Root>
|
||||
@@ -262,7 +265,7 @@
|
||||
<Button secondary icon on:click={toggle}>
|
||||
<Icon icon={IconPlus} size="s" />
|
||||
</Button>
|
||||
<ActionMenu.Root slot="tooltip">
|
||||
<ActionMenu.Root slot="tooltip" let:toggle>
|
||||
{#if $paymentMethods.total}
|
||||
{#each filteredPaymentMethods as paymentMethod}
|
||||
<ActionMenu.Item.Button
|
||||
@@ -277,7 +280,10 @@
|
||||
<Divider />
|
||||
<ActionMenu.Item.Button
|
||||
leadingIcon={IconPlus}
|
||||
on:click={() => (showPayment = true)}>
|
||||
on:click={(e) => {
|
||||
toggle(e);
|
||||
showPayment = true;
|
||||
}}>
|
||||
Add new payment method
|
||||
</ActionMenu.Item.Button>
|
||||
</ActionMenu.Root>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { EmptySearch, PaginationWithLimit } from '$lib/components/index.js';
|
||||
import { EmptySearch, PaginationWithLimit, ViewSelector } from '$lib/components/index.js';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import Link from '$lib/elements/link.svelte';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
@@ -29,6 +29,8 @@
|
||||
import { app } from '$lib/stores/app';
|
||||
import type { Domain } from '$lib/sdk/domains';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { columns } from './store';
|
||||
import { View } from '$lib/helpers/load';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -40,59 +42,59 @@
|
||||
<Container>
|
||||
<Layout.Stack direction="row" justifyContent="space-between">
|
||||
<SearchQuery search={data.search} placeholder="Search domains" />
|
||||
<Button
|
||||
on:click={() => {
|
||||
trackEvent(Click.DomainCreateClick, {
|
||||
source: 'organization_domain_overview'
|
||||
});
|
||||
}}
|
||||
href={`${base}/organization-${page.params.organization}/domains/add-domain`}>
|
||||
<Icon icon={IconPlus} size="s" />
|
||||
Add domain
|
||||
</Button>
|
||||
<Layout.Stack direction="row" gap="m" inline>
|
||||
<ViewSelector view={View.Table} {columns} hideView />
|
||||
<Button
|
||||
on:click={() => {
|
||||
trackEvent(Click.DomainCreateClick, {
|
||||
source: 'organization_domain_overview'
|
||||
});
|
||||
}}
|
||||
href={`${base}/organization-${page.params.organization}/domains/add-domain`}>
|
||||
<Icon icon={IconPlus} size="s" />
|
||||
Add domain
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
|
||||
{#if data.domains.total}
|
||||
<Table.Root
|
||||
let:root
|
||||
columns={[
|
||||
{ id: 'domain' },
|
||||
{ id: 'registrar' },
|
||||
{ id: 'nameservers' },
|
||||
{ id: 'expiry_date' },
|
||||
{ id: 'renewal' },
|
||||
{ id: 'auto_renewal' },
|
||||
{ id: 'actions', width: 40 }
|
||||
]}>
|
||||
<Table.Root let:root columns={[...$columns, { id: 'actions', width: 40 }]}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell column="domain" {root}>Domain</Table.Header.Cell>
|
||||
<Table.Header.Cell column="registrar" {root}>Registrar</Table.Header.Cell>
|
||||
<Table.Header.Cell column="nameservers" {root}>Nameservers</Table.Header.Cell>
|
||||
<Table.Header.Cell column="expiry_date" {root}>Expiry date</Table.Header.Cell>
|
||||
<Table.Header.Cell column="renewal" {root}>Renewal</Table.Header.Cell>
|
||||
<Table.Header.Cell column="auto_renewal" {root}>Auto renewal</Table.Header.Cell>
|
||||
<Table.Header.Cell column="actions" {root} />
|
||||
{#each $columns as { id, title }}
|
||||
<Table.Header.Cell column={id} {root}>
|
||||
{title}
|
||||
</Table.Header.Cell>
|
||||
{/each}
|
||||
<Table.Header.Cell {root} column="actions"></Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
{#each data.domains.domains as domain}
|
||||
<Table.Row.Link
|
||||
{root}
|
||||
href={`${base}/organization-${page.params.organization}/domains/domain-${domain.$id}`}>
|
||||
<Table.Cell column="domain" {root}>
|
||||
<Link external icon href={`${$protocol}${domain.domain}`} variant="quiet">
|
||||
{domain.domain}
|
||||
</Link>
|
||||
</Table.Cell>
|
||||
<Table.Cell column="registrar" {root}>{domain?.registrar || '-'}</Table.Cell>
|
||||
<Table.Cell column="nameservers" {root}
|
||||
>{domain?.nameservers || '-'}</Table.Cell>
|
||||
<Table.Cell column="expiry_date" {root}>
|
||||
{domain?.expiry ? toLocaleDateTime(domain.expiry) : '-'}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="renewal" {root}>
|
||||
{domain.renewal ? toLocaleDateTime(domain.renewal) : '-'}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="auto_renewal" {root}
|
||||
>{domain.autoRenewal ? 'On' : 'Off'}</Table.Cell>
|
||||
{#each $columns as column}
|
||||
<Table.Cell column={column.id} {root}>
|
||||
{#if column.id === 'domain'}
|
||||
<Link
|
||||
external
|
||||
icon
|
||||
href={`${$protocol}${domain.domain}`}
|
||||
variant="quiet">
|
||||
{domain.domain}
|
||||
</Link>
|
||||
{:else if column.id === 'registrar'}
|
||||
{domain.registrar || '-'}
|
||||
{:else if column.id === 'nameservers'}
|
||||
{domain.nameservers || '-'}
|
||||
{:else if column.id === 'expiry_date'}
|
||||
{domain?.expiry ? toLocaleDateTime(domain.expiry) : '-'}
|
||||
{:else if column.id === 'renewal'}
|
||||
{domain?.renewal ? toLocaleDateTime(domain.renewal) : '-'}
|
||||
{:else if column.id === 'auto_renewal'}
|
||||
{domain?.autoRenewal ? 'On' : 'Off'}
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
|
||||
<Table.Cell column="actions" {root}>
|
||||
<Layout.Stack direction="row" justifyContent="flex-end">
|
||||
<Popover let:toggle placement="bottom-end" padding="none">
|
||||
|
||||
+29
-1
@@ -23,6 +23,8 @@
|
||||
import Table from './table.svelte';
|
||||
import AddPresetModal from './addPresetModal.svelte';
|
||||
import ImportRecordModal from './importRecordModal.svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -30,6 +32,32 @@
|
||||
let showPresetModal = false;
|
||||
let showImportModal = false;
|
||||
let selectedPreset = '';
|
||||
|
||||
async function downloadRecords() {
|
||||
try {
|
||||
const zone = await sdk.forConsole.domains.getZone(data.domain.$id);
|
||||
|
||||
if ('message' in zone) {
|
||||
const blob = new Blob([zone.message as string], { type: 'text/plain' });
|
||||
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.href = URL.createObjectURL(blob);
|
||||
downloadLink.download = `${data.domain.domain}.txt`;
|
||||
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
URL.revokeObjectURL(downloadLink.href);
|
||||
} else {
|
||||
throw new Error('Failed to download records');
|
||||
}
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
@@ -45,7 +73,7 @@
|
||||
Import zone file
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<PinkButton.Button variant="secondary" icon>
|
||||
<PinkButton.Button variant="secondary" icon on:click={downloadRecords}>
|
||||
<Icon icon={IconDownload} size="s" />
|
||||
</PinkButton.Button>
|
||||
<svelte:fragment slot="tooltip">Export as .txt</svelte:fragment>
|
||||
|
||||
+3
-11
@@ -11,6 +11,7 @@
|
||||
import { presets } from './store';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { createRecord } from '$lib/helpers/domains';
|
||||
|
||||
export let show = false;
|
||||
export let selectedPreset: (typeof presets)[number];
|
||||
@@ -35,16 +36,9 @@
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
//TODO: create DNS records
|
||||
if (records.total) {
|
||||
const promises = records.dnsRecords.map((record) =>
|
||||
sdk.forConsole.domains.createRecordMX(
|
||||
page.params.domain,
|
||||
record.name,
|
||||
record.value,
|
||||
record.ttl,
|
||||
record.priority
|
||||
)
|
||||
const promises = records.dnsRecords.map(
|
||||
async (record) => await createRecord(record, page.params.domain)
|
||||
);
|
||||
await Promise.all(promises);
|
||||
}
|
||||
@@ -62,8 +56,6 @@
|
||||
trackError(e, Submit.RecordCreate);
|
||||
}
|
||||
}
|
||||
|
||||
$: console.log(selectedPreset);
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
|
||||
+18
-111
@@ -16,19 +16,8 @@
|
||||
import { recordTypes } from './store';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { invalidate } from '$app/navigation';
|
||||
|
||||
type RecordType =
|
||||
| 'A'
|
||||
| 'AAAA'
|
||||
| 'CNAME'
|
||||
| 'MX'
|
||||
| 'TXT'
|
||||
| 'NS'
|
||||
| 'SRV'
|
||||
| 'CAA'
|
||||
| 'PTR'
|
||||
| 'HTTPS'
|
||||
| 'ALIAS';
|
||||
import type { RecordType } from '$lib/stores/domains';
|
||||
import { createRecord } from '$lib/helpers/domains';
|
||||
|
||||
export let show = false;
|
||||
|
||||
@@ -41,105 +30,23 @@
|
||||
let error = '';
|
||||
|
||||
async function handleSubmit() {
|
||||
const record = {
|
||||
name,
|
||||
type,
|
||||
value,
|
||||
ttl,
|
||||
priority,
|
||||
comment
|
||||
};
|
||||
try {
|
||||
switch (type) {
|
||||
case 'A':
|
||||
await sdk.forConsole.domains.createRecordA(
|
||||
page.params.domain,
|
||||
name,
|
||||
value,
|
||||
ttl,
|
||||
comment || undefined
|
||||
);
|
||||
|
||||
break;
|
||||
case 'AAAA':
|
||||
await sdk.forConsole.domains.createRecordAAAA(
|
||||
page.params.domain,
|
||||
name,
|
||||
value,
|
||||
ttl,
|
||||
comment || undefined
|
||||
);
|
||||
|
||||
break;
|
||||
case 'CNAME':
|
||||
await sdk.forConsole.domains.createRecordCNAME(
|
||||
page.params.domain,
|
||||
name,
|
||||
value,
|
||||
ttl,
|
||||
comment || undefined
|
||||
);
|
||||
|
||||
break;
|
||||
case 'MX':
|
||||
await sdk.forConsole.domains.createRecordMX(
|
||||
page.params.domain,
|
||||
name,
|
||||
value,
|
||||
ttl,
|
||||
priority,
|
||||
comment || undefined
|
||||
);
|
||||
|
||||
break;
|
||||
case 'TXT':
|
||||
await sdk.forConsole.domains.createRecordTXT(
|
||||
page.params.domain,
|
||||
name,
|
||||
value,
|
||||
ttl,
|
||||
comment || undefined
|
||||
);
|
||||
|
||||
break;
|
||||
case 'NS':
|
||||
await sdk.forConsole.domains.createRecordNS(
|
||||
page.params.domain,
|
||||
name,
|
||||
value,
|
||||
ttl,
|
||||
comment || undefined
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case 'CAA':
|
||||
await sdk.forConsole.domains.createRecordCAA(
|
||||
page.params.domain,
|
||||
name,
|
||||
value,
|
||||
ttl,
|
||||
comment || undefined
|
||||
);
|
||||
|
||||
break;
|
||||
case 'HTTPS':
|
||||
await sdk.forConsole.domains.createRecordHTTPS(
|
||||
page.params.domain,
|
||||
name,
|
||||
value,
|
||||
ttl,
|
||||
comment || undefined
|
||||
);
|
||||
|
||||
break;
|
||||
case 'ALIAS':
|
||||
await sdk.forConsole.domains.createRecordAlias(
|
||||
page.params.domain,
|
||||
name,
|
||||
value,
|
||||
ttl,
|
||||
comment || undefined
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
await createRecord(record, page.params.domain);
|
||||
name = '';
|
||||
type = 'A';
|
||||
value = '';
|
||||
ttl = 3600;
|
||||
priority = undefined;
|
||||
comment = '';
|
||||
error = '';
|
||||
show = false;
|
||||
invalidate(Dependencies.DOMAINS);
|
||||
addNotification({
|
||||
|
||||
+6
-224
@@ -6,246 +6,28 @@
|
||||
import { Icon, Layout, Tooltip, Typography, Upload } from '@appwrite.io/pink-svelte';
|
||||
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
|
||||
import { removeFile } from '$lib/helpers/files';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { DnsRecord } from '$lib/sdk/domains';
|
||||
import { page } from '$app/state';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
|
||||
export let show = false;
|
||||
let files: FileList;
|
||||
let error = '';
|
||||
|
||||
type ParsedRecords = {
|
||||
A: Partial<DnsRecord>[];
|
||||
AAAA: Partial<DnsRecord>[];
|
||||
CNAME: Partial<DnsRecord>[];
|
||||
MX: Partial<DnsRecord>[];
|
||||
TXT: Partial<DnsRecord>[];
|
||||
NS: Partial<DnsRecord>[];
|
||||
SRV: Partial<DnsRecord>[];
|
||||
CAA: Partial<DnsRecord>[];
|
||||
PTR: Partial<DnsRecord>[];
|
||||
HTTPS: Partial<DnsRecord>[];
|
||||
ALIAS: Partial<DnsRecord>[];
|
||||
[key: string]: Partial<DnsRecord>[];
|
||||
};
|
||||
|
||||
function parseZoneFile(content: string): ParsedRecords {
|
||||
const records: ParsedRecords = {
|
||||
A: [],
|
||||
AAAA: [],
|
||||
CNAME: [],
|
||||
MX: [],
|
||||
TXT: [],
|
||||
NS: [],
|
||||
SRV: [],
|
||||
CAA: [],
|
||||
PTR: [],
|
||||
HTTPS: [],
|
||||
ALIAS: []
|
||||
};
|
||||
|
||||
// Split the content into lines and process each line
|
||||
const lines = content.split('\n');
|
||||
|
||||
for (let line of lines) {
|
||||
// Remove comments, but save them for record comment field
|
||||
let comment = '';
|
||||
const commentIndex = line.indexOf(';');
|
||||
if (commentIndex !== -1) {
|
||||
comment = line.substring(commentIndex + 1).trim();
|
||||
line = line.substring(0, commentIndex).trim();
|
||||
}
|
||||
|
||||
// Skip empty lines or pure comment lines
|
||||
if (!line.trim()) continue;
|
||||
|
||||
// Split line into parts (whitespace separated)
|
||||
const parts = line.trim().split(/\s+/);
|
||||
|
||||
// Need at least a name, TTL/class/type, and value
|
||||
if (parts.length < 3) continue;
|
||||
|
||||
let recordType = '';
|
||||
let name = '';
|
||||
let value = '';
|
||||
let ttl = 3600;
|
||||
let priority: number | undefined;
|
||||
let weight: number | undefined;
|
||||
let port: number | undefined;
|
||||
|
||||
// Try to determine the record type from the parts
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i].toUpperCase();
|
||||
if (['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'SRV', 'CAA'].includes(part)) {
|
||||
recordType = part;
|
||||
name = parts[0] === '@' ? '' : parts[0]; // @ represents the domain itself
|
||||
|
||||
// Handle TTL if it appears to be a number
|
||||
const possibleTtl = parseInt(parts[i - 1]);
|
||||
if (!isNaN(possibleTtl) && i > 0) {
|
||||
ttl = possibleTtl;
|
||||
}
|
||||
|
||||
if (recordType === 'MX' && i + 2 < parts.length) {
|
||||
priority = parseInt(parts[i + 1]);
|
||||
value = parts[i + 2];
|
||||
} else if (recordType === 'SRV' && i + 4 < parts.length) {
|
||||
priority = parseInt(parts[i + 1]);
|
||||
weight = parseInt(parts[i + 2]);
|
||||
port = parseInt(parts[i + 3]);
|
||||
value = parts[i + 4];
|
||||
} else if (i + 1 < parts.length) {
|
||||
value = parts.slice(i + 1).join(' ');
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (recordType && value) {
|
||||
if (recordType === 'TXT' && value.startsWith('"') && value.endsWith('"')) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
|
||||
const record: Partial<DnsRecord> = {
|
||||
name,
|
||||
value,
|
||||
ttl,
|
||||
comment: comment || undefined
|
||||
};
|
||||
|
||||
if (priority !== undefined) {
|
||||
record.priority = priority;
|
||||
}
|
||||
|
||||
if (!records[recordType]) {
|
||||
records[recordType] = [];
|
||||
}
|
||||
|
||||
records[recordType].push(record);
|
||||
}
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
if (!files?.length) return;
|
||||
|
||||
const file = files[0];
|
||||
const content = await file.text();
|
||||
const parsedRecords = parseZoneFile(content);
|
||||
|
||||
let recordCount = 0;
|
||||
|
||||
for (const [type, records] of Object.entries(parsedRecords)) {
|
||||
recordCount += records.length;
|
||||
|
||||
for (const record of records) {
|
||||
switch (type) {
|
||||
case 'A':
|
||||
await sdk.forConsole.domains.createRecordA(
|
||||
page.params.domain,
|
||||
record.name,
|
||||
record.value,
|
||||
record.ttl,
|
||||
record.comment
|
||||
);
|
||||
break;
|
||||
case 'AAAA':
|
||||
await sdk.forConsole.domains.createRecordAAAA(
|
||||
page.params.domain,
|
||||
record.name,
|
||||
record.value,
|
||||
record.ttl,
|
||||
record.comment
|
||||
);
|
||||
break;
|
||||
case 'CNAME':
|
||||
await sdk.forConsole.domains.createRecordCNAME(
|
||||
page.params.domain,
|
||||
record.name,
|
||||
record.value,
|
||||
record.ttl,
|
||||
record.comment
|
||||
);
|
||||
break;
|
||||
case 'MX':
|
||||
await sdk.forConsole.domains.createRecordMX(
|
||||
page.params.domain,
|
||||
record.name,
|
||||
record.value,
|
||||
record.priority || 10,
|
||||
record.ttl,
|
||||
record.comment
|
||||
);
|
||||
break;
|
||||
case 'TXT':
|
||||
await sdk.forConsole.domains.createRecordTXT(
|
||||
page.params.domain,
|
||||
record.name,
|
||||
record.value,
|
||||
record.ttl,
|
||||
record.comment
|
||||
);
|
||||
break;
|
||||
case 'NS':
|
||||
await sdk.forConsole.domains.createRecordNS(
|
||||
page.params.domain,
|
||||
record.name,
|
||||
record.value,
|
||||
record.ttl,
|
||||
record.comment
|
||||
);
|
||||
break;
|
||||
case 'SRV':
|
||||
await sdk.forConsole.domains.createRecordSRV(
|
||||
page.params.domain,
|
||||
record.name,
|
||||
record.value,
|
||||
record.ttl,
|
||||
record.priority,
|
||||
record.weight,
|
||||
record.port,
|
||||
record.comment
|
||||
);
|
||||
break;
|
||||
case 'CAA':
|
||||
await sdk.forConsole.domains.createRecordCAA(
|
||||
page.params.domain,
|
||||
record.name,
|
||||
record.value,
|
||||
record.ttl,
|
||||
record.comment
|
||||
);
|
||||
break;
|
||||
case 'PTR':
|
||||
await sdk.forConsole.domains.createRecordHTTPS(
|
||||
page.params.domain,
|
||||
record.name,
|
||||
record.value,
|
||||
record.ttl,
|
||||
record.comment
|
||||
);
|
||||
break;
|
||||
case 'ALIAS':
|
||||
await sdk.forConsole.domains.createRecordAlias(
|
||||
page.params.domain,
|
||||
record.name,
|
||||
record.value,
|
||||
record.ttl,
|
||||
record.comment
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await sdk.forConsole.domains.updateZone(page.params.domain, content);
|
||||
invalidate(Dependencies.DOMAIN);
|
||||
show = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${recordCount} DNS records have been imported successfully`
|
||||
message: `DNS records have been imported successfully`
|
||||
});
|
||||
trackEvent(Submit.RecordCreate);
|
||||
} catch (e) {
|
||||
|
||||
-3
@@ -20,8 +20,6 @@
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
|
||||
// type RecordType = 'A' | 'AAAA' | 'CNAME' | 'MX' | 'TXT' | 'NS' | 'SRV' | 'CAA' | 'PTR';
|
||||
|
||||
export let show = false;
|
||||
export let selectedRecord: DnsRecord;
|
||||
|
||||
@@ -30,7 +28,6 @@
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
//TODO: update DNS record
|
||||
switch (record.type) {
|
||||
case 'A':
|
||||
await sdk.forConsole.domains.updateRecordA(
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const columns = writable<Column[]>([
|
||||
{ id: 'domain', title: 'Domain', type: 'string', width: { min: 300 } },
|
||||
{ id: 'registrar', title: 'Registrar', type: 'string' },
|
||||
{ id: 'nameservers', title: 'Nameservers', type: 'string' },
|
||||
{ id: 'expiry_date', title: 'Expiry Date', type: 'datetime' },
|
||||
{ id: 'renewal', title: 'Renewal', type: 'boolean' },
|
||||
{ id: 'auto_renewal', title: 'Auto Renewal', type: 'boolean' }
|
||||
]);
|
||||
+31
-30
@@ -52,37 +52,38 @@
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
<Paginator
|
||||
items={members.memberships}
|
||||
let:paginatedItems
|
||||
hideFooter={members?.total <= 5}>
|
||||
<Table.Root columns={2} let:root>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell {root}>Members</Table.Header.Cell>
|
||||
<Table.Header.Cell {root}>Joined</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
{#each paginatedItems as member}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell {root}>
|
||||
<Layout.Stack direction="row" alignItems="center" gap="s">
|
||||
<AvatarInitials
|
||||
size="xs"
|
||||
name={member.userName
|
||||
? member.userName
|
||||
: member.userEmail} />
|
||||
<Layout.Stack gap="none">
|
||||
<Typography.Text>
|
||||
{member.userName ? member.userName : member.userEmail}
|
||||
</Typography.Text>
|
||||
<Paginator items={members.memberships} hideFooter={members?.total <= 5}>
|
||||
{#snippet children(paginatedItems: typeof members.memberships)}
|
||||
<Table.Root columns={2} let:root>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell {root}>Members</Table.Header.Cell>
|
||||
<Table.Header.Cell {root}>Joined</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
{#each paginatedItems as member}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell {root}>
|
||||
<Layout.Stack direction="row" alignItems="center" gap="s">
|
||||
<AvatarInitials
|
||||
size="xs"
|
||||
name={member.userName
|
||||
? member.userName
|
||||
: member.userEmail} />
|
||||
<Layout.Stack gap="none">
|
||||
<Typography.Text>
|
||||
{member.userName
|
||||
? member.userName
|
||||
: member.userEmail}
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
<DualTimeView time={member.joined ?? member.$createdAt} />
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
<DualTimeView time={member.joined ?? member.$createdAt} />
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
{/snippet}
|
||||
</Paginator>
|
||||
{:else}
|
||||
<EmptyCardCloud service="members" eventSource="organization_usage" />
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
<SearchQuery search={data.search} placeholder="Search by name, email, phone, or ID" />
|
||||
</Layout.Stack>
|
||||
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
|
||||
<ViewSelector view={View.Table} {columns} hideView allowNoColumns />
|
||||
<ViewSelector view={View.Table} {columns} hideView />
|
||||
<Button on:mousedown={() => ($showCreateUser = true)} event="create_user" size="s">
|
||||
<Icon size="s" icon={IconPlus} slot="start" />
|
||||
<span class="text">Create user</span>
|
||||
|
||||
@@ -48,13 +48,13 @@
|
||||
<div class="u-flex u-main-end u-gap-16 is-not-mobile">
|
||||
<Filters query={data.query} {columns} />
|
||||
<div>
|
||||
<ViewSelector view={View.Table} {columns} hideView allowNoColumns />
|
||||
<ViewSelector view={View.Table} {columns} hideView />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16 is-only-mobile u-margin-block-start-16">
|
||||
<div class="u-flex-basis-50-percent">
|
||||
<ViewSelector view={View.Table} {columns} hideView allowNoColumns />
|
||||
<ViewSelector view={View.Table} {columns} hideView />
|
||||
</div>
|
||||
<div class="u-flex-basis-50-percent">
|
||||
<Filters query={data.query} {columns} fullWidthMobile />
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
<div class="u-flex u-main-end u-gap-16 is-not-mobile">
|
||||
<Filters query={data.query} {columns} />
|
||||
<ViewSelector view={View.Table} {columns} hideView allowNoColumns />
|
||||
<ViewSelector view={View.Table} {columns} hideView />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { user } from './store';
|
||||
import { Tag, Input, Layout } from '@appwrite.io/pink-svelte';
|
||||
import { Tag, Input, Layout, Icon } from '@appwrite.io/pink-svelte';
|
||||
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
|
||||
const alphaNumericRegExp = /^[a-zA-Z0-9]+$/;
|
||||
let suggestedLabels = ['admin', 'premium', 'mvp'];
|
||||
@@ -39,7 +40,6 @@
|
||||
trackError(error, Submit.UserUpdateLabels);
|
||||
}
|
||||
}
|
||||
$: console.log(labels);
|
||||
// TODO: Remove type cast when console SDK is updated
|
||||
$: isDisabled =
|
||||
!!error ||
|
||||
@@ -68,11 +68,13 @@
|
||||
New label-based roles will be assigned.
|
||||
<svelte:fragment slot="aside">
|
||||
<Layout.Stack gap="s">
|
||||
<InputTags
|
||||
id="user-labels"
|
||||
label="Labels"
|
||||
placeholder="Select or type user labels"
|
||||
bind:tags={labels} />
|
||||
{#key labels.length}
|
||||
<InputTags
|
||||
id="user-labels"
|
||||
label="Labels"
|
||||
placeholder="Select or type user labels"
|
||||
bind:tags={labels} />
|
||||
{/key}
|
||||
<Layout.Stack direction="row">
|
||||
{#each suggestedLabels as suggestedLabel}
|
||||
<Tag
|
||||
@@ -85,7 +87,7 @@
|
||||
labels = labels.filter((e) => e !== suggestedLabel);
|
||||
}
|
||||
}}>
|
||||
<span class="icon-plus" aria-hidden="true"></span>
|
||||
<Icon icon={IconPlus} size="s" slot="start" />
|
||||
{suggestedLabel}
|
||||
</Tag>
|
||||
{/each}
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<Container>
|
||||
<Layout.Stack direction="row" justifyContent="space-between">
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
<SearchQuery search={data.search} placeholder="Search databases" />
|
||||
<SearchQuery search={data.search} placeholder="Search by name or ID" />
|
||||
</Layout.Stack>
|
||||
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
|
||||
<ViewSelector
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<Container>
|
||||
<Layout.Stack direction="row" justifyContent="space-between">
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
<SearchQuery search={data.search} placeholder="Search collections" />
|
||||
<SearchQuery placeholder="Search by ID or name" />
|
||||
</Layout.Stack>
|
||||
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
|
||||
<ViewSelector
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<Card.Base padding="s">
|
||||
<Card.Base padding="xs">
|
||||
<div class="u-flex u-gap-24 u-flex-vertical-mobile u-cross-center">
|
||||
<div
|
||||
style:--p-file-preview-border-color="transparent"
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@
|
||||
disabled={!(hasAttributes && hasValidAttributes)}
|
||||
analyticsSource="database_documents" />
|
||||
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
|
||||
<ViewSelector view={data.view} {columns} hideView allowNoColumns />
|
||||
<ViewSelector view={data.view} {columns} hideView />
|
||||
<Button
|
||||
disabled={!(hasAttributes && hasValidAttributes)}
|
||||
href={`${base}/project-${page.params.project}/databases/database-${page.params.database}/collection-${page.params.collection}/create`}
|
||||
|
||||
+1
@@ -86,6 +86,7 @@
|
||||
href={`${base}/project-${page.params.project}/databases/database-${page.params.database}/collection-${page.params.collection}/`}
|
||||
bind:showExitModal
|
||||
column
|
||||
columnSize="s"
|
||||
confirmExit>
|
||||
<Form bind:this={formComponent} onSubmit={create} bind:isSubmitting>
|
||||
<Layout.Stack gap="xxl">
|
||||
|
||||
+9
-1
@@ -6,6 +6,14 @@
|
||||
export let label: string;
|
||||
export let value: string | null;
|
||||
export let attribute: Models.AttributeUrl;
|
||||
|
||||
$: nullable = attribute.required ? false : !value;
|
||||
</script>
|
||||
|
||||
<InputEmail {id} {label} placeholder="Enter URL" required={attribute.required} bind:value />
|
||||
<InputEmail
|
||||
{id}
|
||||
{label}
|
||||
{nullable}
|
||||
placeholder="Enter URL"
|
||||
required={attribute.required}
|
||||
bind:value />
|
||||
|
||||
+3
-1
@@ -6,12 +6,14 @@
|
||||
export let label: string;
|
||||
export let value: number;
|
||||
export let attribute: Models.AttributeInteger;
|
||||
|
||||
$: nullable = attribute.required ? false : !value;
|
||||
</script>
|
||||
|
||||
<InputNumber
|
||||
{id}
|
||||
{label}
|
||||
nullable
|
||||
{nullable}
|
||||
required={attribute.required}
|
||||
min={attribute.min}
|
||||
max={attribute.max}
|
||||
|
||||
+4
-2
@@ -6,13 +6,15 @@
|
||||
export let label: string;
|
||||
export let value: string;
|
||||
export let attribute: Models.AttributeString;
|
||||
|
||||
$: nullable = attribute.required ? false : !value;
|
||||
</script>
|
||||
|
||||
{#if attribute.size >= 50}
|
||||
<InputTextarea
|
||||
{id}
|
||||
{label}
|
||||
nullable={!attribute.required}
|
||||
{nullable}
|
||||
placeholder="Enter string"
|
||||
required={attribute.required}
|
||||
maxlength={attribute.size}
|
||||
@@ -21,7 +23,7 @@
|
||||
<InputText
|
||||
{id}
|
||||
{label}
|
||||
nullable={!attribute.required}
|
||||
{nullable}
|
||||
placeholder="Enter string"
|
||||
required={attribute.required}
|
||||
maxlength={attribute.size}
|
||||
|
||||
+3
-1
@@ -6,12 +6,14 @@
|
||||
export let label: string;
|
||||
export let value: string | null;
|
||||
export let attribute: Models.AttributeUrl;
|
||||
|
||||
$: nullable = attribute.required ? false : !value;
|
||||
</script>
|
||||
|
||||
<InputURL
|
||||
{id}
|
||||
{label}
|
||||
{nullable}
|
||||
placeholder="Enter URL"
|
||||
nullable={!attribute.required}
|
||||
required={attribute.required}
|
||||
bind:value />
|
||||
|
||||
+3
-3
@@ -3,7 +3,7 @@
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { Modal, Trim } from '$lib/components';
|
||||
import { Confirm, Trim } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
@@ -52,7 +52,7 @@
|
||||
) as Models.AttributeRelationship[];
|
||||
</script>
|
||||
|
||||
<Modal title="Delete document" onSubmit={handleDelete} bind:show={showDelete}>
|
||||
<Confirm title="Delete document" onSubmit={handleDelete} bind:open={showDelete}>
|
||||
<p data-private>
|
||||
Are you sure you want to delete <b
|
||||
>the document from <span data-private>{$collection.name}</span></b
|
||||
@@ -108,4 +108,4 @@
|
||||
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
|
||||
<Button danger submit disabled={relAttributes?.length && !checked}>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
</Confirm>
|
||||
|
||||
+1
-1
@@ -158,7 +158,7 @@
|
||||
</Layout.Stack>
|
||||
{/each}
|
||||
<div>
|
||||
<Button secondary on:click={addAttribute} disabled={addAttributeDisabled}>
|
||||
<Button compact on:click={addAttribute} disabled={addAttributeDisabled}>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Add attribute
|
||||
</Button>
|
||||
|
||||
+26
-24
@@ -27,34 +27,36 @@
|
||||
|
||||
<Modal bind:show title={selectedRelationship?.key} hideFooter>
|
||||
{#if data?.length}
|
||||
<Paginator items={data} {limit} let:paginatedItems>
|
||||
<Table.Root
|
||||
let:root
|
||||
columns={[{ id: '$id', width: 150 }, ...args.map((id) => ({ id }))]}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell column="$id" {root}>Document ID</Table.Header.Cell>
|
||||
{#if args?.length}
|
||||
{#each args as arg}
|
||||
<Table.Header.Cell column={arg} {root}>{arg}</Table.Header.Cell>
|
||||
{/each}
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
{#each paginatedItems as doc}
|
||||
<Table.Row.Link
|
||||
{root}
|
||||
href={`${base}/project-${projectId}/databases/database-${databaseId}/collection-${selectedRelationship.relatedCollection}/document-${doc.$id}`}
|
||||
on:click={() => (show = false)}>
|
||||
<Table.Cell column="$id" {root}>
|
||||
<Id value={doc.$id}>{doc.$id}</Id>
|
||||
</Table.Cell>
|
||||
<Paginator items={data} {limit}>
|
||||
{#snippet children(paginatedItems: typeof data)}
|
||||
<Table.Root
|
||||
let:root
|
||||
columns={[{ id: '$id', width: 150 }, ...args.map((id) => ({ id }))]}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell column="$id" {root}>Document ID</Table.Header.Cell>
|
||||
{#if args?.length}
|
||||
{#each args as arg}
|
||||
<Table.Cell column={arg} {root}>{doc[arg]}</Table.Cell>
|
||||
<Table.Header.Cell column={arg} {root}>{arg}</Table.Header.Cell>
|
||||
{/each}
|
||||
{/if}
|
||||
</Table.Row.Link>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
</svelte:fragment>
|
||||
{#each paginatedItems as doc}
|
||||
<Table.Row.Link
|
||||
{root}
|
||||
href={`${base}/project-${projectId}/databases/database-${databaseId}/collection-${selectedRelationship.relatedCollection}/document-${doc.$id}`}
|
||||
on:click={() => (show = false)}>
|
||||
<Table.Cell column="$id" {root}>
|
||||
<Id value={doc.$id}>{doc.$id}</Id>
|
||||
</Table.Cell>
|
||||
{#if args?.length}
|
||||
{#each args as arg}
|
||||
<Table.Cell column={arg} {root}>{doc[arg]}</Table.Cell>
|
||||
{/each}
|
||||
{/if}
|
||||
</Table.Row.Link>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
{/snippet}
|
||||
</Paginator>
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
+3
-4
@@ -57,9 +57,8 @@
|
||||
<Form onSubmit={updateDisplayName}>
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Display name</svelte:fragment>
|
||||
Select string attributes as display names for your documents. The selected names will be used
|
||||
as short forms to identify documents in the Appwrite console, like when creating database relationships.
|
||||
You can specify up to 5 names.
|
||||
Select up to 5 string attributes to display as document names in the Appwrite console. These
|
||||
help identify documents in places like relationships.
|
||||
|
||||
<svelte:fragment slot="aside">
|
||||
<Layout.Stack gap="s">
|
||||
@@ -94,7 +93,7 @@
|
||||
{/if}
|
||||
<div>
|
||||
<Button
|
||||
secondary
|
||||
compact
|
||||
disabled={addAttributeDisabled}
|
||||
on:click={() => {
|
||||
names[names.length] = null;
|
||||
|
||||
+4
-7
@@ -48,22 +48,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: if (
|
||||
$: arePermsDisabled = !(
|
||||
collectionPermissions &&
|
||||
symmetricDifference(collectionPermissions, $collection.$permissions).length
|
||||
) {
|
||||
arePermsDisabled = false;
|
||||
} else arePermsDisabled = true;
|
||||
);
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Permissions</svelte:fragment>
|
||||
Choose who can access your collection and documents. For more about
|
||||
<Link.Anchor
|
||||
Choose who can access your collection and documents. <Link.Anchor
|
||||
href="https://appwrite.io/docs/products/databases/permissions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
Permissions
|
||||
Learn more
|
||||
</Link.Anchor>.
|
||||
<svelte:fragment slot="aside">
|
||||
{#if collectionPermissions}
|
||||
|
||||
+8
-8
@@ -14,17 +14,17 @@
|
||||
import { isTabletViewport } from '$lib/stores/viewport';
|
||||
import { BottomSheet } from '$lib/components';
|
||||
|
||||
$: data = page.data as PageData;
|
||||
$: project = page.params.project;
|
||||
$: databaseId = page.params.database;
|
||||
$: collectionId = page.params.collection;
|
||||
let data = $derived(page.data) as PageData;
|
||||
let project = $derived(page.params.project);
|
||||
let databaseId = $derived(page.params.database);
|
||||
let collectionId = $derived(page.params.collection);
|
||||
|
||||
$: sortedCollections = data?.allCollections?.collections?.sort((a, b) =>
|
||||
a.name.localeCompare(b.name)
|
||||
const sortedCollections = $derived.by(() =>
|
||||
data?.allCollections?.collections?.slice().sort((a, b) => a.name.localeCompare(b.name))
|
||||
);
|
||||
|
||||
$: selectedCollection = sortedCollections?.find(
|
||||
(collection) => collection.$id === collectionId
|
||||
const selectedCollection = $derived.by(() =>
|
||||
sortedCollections()?.find((collection) => collection.$id === collectionId)
|
||||
);
|
||||
|
||||
let openBottomSheet = false;
|
||||
|
||||
+1
-1
@@ -157,7 +157,7 @@
|
||||
let:root
|
||||
allowSelection
|
||||
bind:selectedRows
|
||||
columns={[{ id: '$id', width: 150 }, ...$columns, { id: '$created' }, { id: '$updated' }]}>
|
||||
columns={[{ id: '$id', width: 200 }, ...$columns, { id: '$created' }, { id: '$updated' }]}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell column="$id" {root}>Document ID</Table.Header.Cell>
|
||||
{#each $columns as column}
|
||||
|
||||
+45
-44
@@ -10,7 +10,7 @@
|
||||
import { database } from './store';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import { type Models, Query } from '@appwrite.io/console';
|
||||
import { Table } from '@appwrite.io/pink-svelte';
|
||||
import { Spinner, Table } from '@appwrite.io/pink-svelte';
|
||||
|
||||
const databaseId = page.params.database;
|
||||
|
||||
@@ -86,6 +86,8 @@
|
||||
$: if (!showDelete) {
|
||||
collections = null;
|
||||
collectionItems = [];
|
||||
} else {
|
||||
listCollections();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -98,53 +100,52 @@
|
||||
Are you sure you want to delete <b>{$database.name}</b>?
|
||||
{/if}
|
||||
</p>
|
||||
{#await listCollections()}
|
||||
|
||||
{#if isLoadingDocumentsCount}
|
||||
<div class="u-flex u-main-center">
|
||||
<div class="loader"></div>
|
||||
<Spinner />
|
||||
</div>
|
||||
{:then}
|
||||
{#if error}
|
||||
<p class="text">
|
||||
Are you sure you want to delete <b>{$database.name}</b>?
|
||||
</p>
|
||||
{:else if collectionItems.length > 0}
|
||||
<div class="u-flex-vertical u-gap-16">
|
||||
<Table.Root columns={2} let:root>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell {root}>Collection</Table.Header.Cell>
|
||||
<Table.Header.Cell {root}>Last Updated</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
{#each collectionItems as collection}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell {root}>{collection.name}</Table.Cell>
|
||||
<Table.Cell {root}>{toLocaleDate(collection.updatedAt)}</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
{:else if error}
|
||||
<p class="text">
|
||||
Are you sure you want to delete <b>{$database.name}</b>?
|
||||
</p>
|
||||
{:else if collectionItems.length > 0}
|
||||
<div class="u-flex-vertical u-gap-16">
|
||||
<Table.Root columns={2} let:root>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell {root}>Collection</Table.Header.Cell>
|
||||
<Table.Header.Cell {root}>Last Updated</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
{#each collectionItems as collection}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell {root}>{collection.name}</Table.Cell>
|
||||
<Table.Cell {root}>{toLocaleDate(collection.updatedAt)}</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
|
||||
{#if collectionItems.length < collections.total}
|
||||
<div class="u-flex u-gap-16 u-cross-center">
|
||||
<button class="u-underline" on:click={listCollections} type="button">
|
||||
Show more
|
||||
</button>
|
||||
|
||||
{#if isLoadingDocumentsCount}
|
||||
<div class="loader is-small"></div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if collectionItems.length > 25}
|
||||
<button
|
||||
class="u-underline"
|
||||
on:click={() => {
|
||||
collectionItems = collectionItems.slice(0, 3);
|
||||
}}
|
||||
type="button">
|
||||
Show less
|
||||
{#if collectionItems.length < collections.total}
|
||||
<div class="u-flex u-gap-16 u-cross-center">
|
||||
<button class="u-underline" on:click={listCollections} type="button">
|
||||
Show more
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
|
||||
{#if isLoadingDocumentsCount}
|
||||
<div class="loader is-small"></div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if collectionItems.length > 25}
|
||||
<button
|
||||
class="u-underline"
|
||||
on:click={() => {
|
||||
collectionItems = collectionItems.slice(0, 3);
|
||||
}}
|
||||
type="button">
|
||||
Show less
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !isLoadingDocumentsCount}
|
||||
<p class="text" data-private>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</script>
|
||||
|
||||
<CardContainer
|
||||
showEmpty={$canWriteCollections}
|
||||
disableEmpty={!$canWriteCollections}
|
||||
total={data.collections.total}
|
||||
on:click={() => (showCreate = true)}
|
||||
event="collection">
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</script>
|
||||
|
||||
<CardContainer
|
||||
showEmpty={$canWriteDatabases}
|
||||
disableEmpty={!$canWriteDatabases}
|
||||
total={data.databases.total}
|
||||
on:click={() => (showCreate = true)}
|
||||
event="database"
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
|
||||
<Container>
|
||||
<Layout.Stack direction="row" justifyContent="space-between">
|
||||
<SearchQuery search={data.search} placeholder="Search functions" />
|
||||
<SearchQuery placeholder="Search by name or ID" />
|
||||
|
||||
<Button href={`${base}/project-${project}/functions/create-function`}>
|
||||
<Icon icon={IconPlus} slot="start" />
|
||||
@@ -88,12 +88,12 @@
|
||||
{#if data.functions.total}
|
||||
<CardContainer
|
||||
{offset}
|
||||
showEmpty={$canWriteFunctions}
|
||||
disableEmpty={!$canWriteFunctions}
|
||||
event="functions"
|
||||
total={data.functions.total}
|
||||
service="functions"
|
||||
on:click={() => goto(`${base}/project-${project}/functions/create-function`)}>
|
||||
{#each data.functions.functions as func}
|
||||
{#each data.functions.functions as func (func.$id)}
|
||||
<GridItem1 href={`${base}/project-${project}/functions/function-${func.$id}`}>
|
||||
<svelte:fragment slot="title">
|
||||
<Layout.Stack gap="l" alignItems="center" direction="row" inline>
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
previousPage = from?.url?.pathname || previousPage;
|
||||
});
|
||||
|
||||
let hasInstallations: boolean;
|
||||
let selectedRepository: string;
|
||||
|
||||
const featuredTemplatesList = data.templatesList.templates
|
||||
@@ -57,12 +56,10 @@
|
||||
baseRuntimesList.some((base) => base.$id === r.name)
|
||||
);
|
||||
|
||||
function connect(e: CustomEvent<Models.ProviderRepository>) {
|
||||
function connect(e: Models.ProviderRepository) {
|
||||
trackEvent(Click.ConnectRepositoryClick, { from: 'cover' });
|
||||
repository.set(e.detail);
|
||||
goto(
|
||||
`${wizardBase}/create-function/repository-${e.detail.id}?installation=${$installation.$id}`
|
||||
);
|
||||
repository.set(e);
|
||||
goto(`${wizardBase}/create-function/repository-${e.id}?installation=${$installation.$id}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -103,16 +100,15 @@
|
||||
<Typography.Title size="s">Connect Git repository</Typography.Title>
|
||||
|
||||
<Repositories
|
||||
bind:hasInstallations
|
||||
bind:selectedRepository
|
||||
action="button"
|
||||
callbackState={{
|
||||
from: 'github',
|
||||
to: 'cover'
|
||||
}}
|
||||
on:connect={connect} />
|
||||
{connect} />
|
||||
</Layout.Stack>
|
||||
{#if hasInstallations}
|
||||
{#if data.installations.total}
|
||||
<Layout.Stack gap="l">
|
||||
<Divider />
|
||||
<Link variant="quiet" href="#/">
|
||||
|
||||
+6
-8
@@ -48,28 +48,26 @@
|
||||
let runtime: Runtime;
|
||||
let entrypoint = '';
|
||||
let buildCommand = '';
|
||||
let scopes: string[] = [];
|
||||
let roles: string[] = [];
|
||||
let variables: Partial<Models.Variable>[] = [];
|
||||
let files: FileList;
|
||||
let specification = specificationOptions[0].value;
|
||||
|
||||
async function create() {
|
||||
try {
|
||||
console.log(runtime);
|
||||
|
||||
const func = await sdk.forProject.functions.create(
|
||||
id || ID.unique(),
|
||||
name,
|
||||
runtime,
|
||||
undefined,
|
||||
roles?.length ? roles : undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
undefined,
|
||||
entrypoint,
|
||||
buildCommand,
|
||||
undefined,
|
||||
scopes,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
@@ -99,8 +97,8 @@
|
||||
func.$id,
|
||||
files[0],
|
||||
true,
|
||||
entrypoint,
|
||||
buildCommand
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
|
||||
trackEvent(Submit.FunctionCreate, {
|
||||
@@ -188,7 +186,7 @@
|
||||
options={runtimeOptions}
|
||||
showEntrypoint />
|
||||
|
||||
<Configuration bind:buildCommand bind:scopes />
|
||||
<Configuration bind:buildCommand bind:roles />
|
||||
</Layout.Stack>
|
||||
</Form>
|
||||
<svelte:fragment slot="aside">
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@
|
||||
import { Accordion, Fieldset, Layout } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let buildCommand = '';
|
||||
export let scopes: string[] = [];
|
||||
export let roles: string[] = [];
|
||||
</script>
|
||||
|
||||
<Fieldset legend="Settings">
|
||||
@@ -28,7 +28,7 @@
|
||||
>Learn more</Link>
|
||||
</span>
|
||||
|
||||
<Roles bind:roles={scopes} />
|
||||
<Roles bind:roles />
|
||||
</Layout.Stack>
|
||||
</Accordion>
|
||||
</Layout.Stack>
|
||||
|
||||
+1
-3
@@ -17,9 +17,7 @@
|
||||
{repository?.organization}/{repository?.name}
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
<Button
|
||||
secondary
|
||||
href={`${base}/project-${page.params.project}/sites/create-site/repositories`}>
|
||||
<Button secondary href={`${base}/project-${page.params.project}/functions/create-function`}>
|
||||
Change
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
|
||||
+12
-16
@@ -44,7 +44,6 @@
|
||||
|
||||
let showExitModal = false;
|
||||
let isCreatingRepository = false;
|
||||
let hasInstallations = !!data?.installations?.total;
|
||||
|
||||
let formComponent: Form;
|
||||
let isSubmitting = writable(false);
|
||||
@@ -69,9 +68,6 @@
|
||||
let specification = specificationOptions[0].value;
|
||||
|
||||
onMount(async () => {
|
||||
if (page.url.searchParams.has('runtime')) {
|
||||
runtime = page.url.searchParams.get('runtime') as Runtime;
|
||||
}
|
||||
if (!$installation?.$id) {
|
||||
$installation = data.installations.installations[0];
|
||||
}
|
||||
@@ -87,7 +83,11 @@
|
||||
const versionB = b.split('-')[1];
|
||||
return versionB.localeCompare(versionA, undefined, { numeric: true });
|
||||
});
|
||||
runtime = matchingRuntimes[0];
|
||||
if (page.url.searchParams.has('runtime')) {
|
||||
runtime = page.url.searchParams.get('runtime') as Runtime;
|
||||
} else {
|
||||
runtime = matchingRuntimes[0];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -133,8 +133,8 @@
|
||||
data.template.timeout ? data.template.timeout : undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
entrypoint || rt.entrypoint,
|
||||
undefined,
|
||||
entrypoint || rt?.entrypoint || undefined,
|
||||
rt?.commands || undefined,
|
||||
selectedScopes?.length ? selectedScopes : undefined,
|
||||
connectBehaviour === 'later' ? undefined : $installation?.$id || undefined,
|
||||
connectBehaviour === 'later' ? undefined : $repository?.id || undefined,
|
||||
@@ -150,8 +150,6 @@
|
||||
func.$id
|
||||
);
|
||||
|
||||
console.log(variables);
|
||||
|
||||
// Add variables
|
||||
const promises = variables.map((variable) =>
|
||||
sdk.forProject.functions.createVariable(
|
||||
@@ -249,7 +247,6 @@
|
||||
bind:specification
|
||||
{specificationOptions}
|
||||
{options} />
|
||||
|
||||
<Permissions
|
||||
templateScopes={data.template.scopes}
|
||||
bind:selectedScopes
|
||||
@@ -258,7 +255,7 @@
|
||||
<ConnectBehaviour bind:connectBehaviour />
|
||||
</Layout.Stack>
|
||||
{#if connectBehaviour === 'now'}
|
||||
{#if hasInstallations}
|
||||
{#if !!data?.installations?.total}
|
||||
<Fieldset legend="Git repository">
|
||||
<Layout.Stack gap="xl">
|
||||
<RepositoryBehaviour bind:repositoryBehaviour />
|
||||
@@ -285,16 +282,15 @@
|
||||
</Layout.Stack>
|
||||
{:else}
|
||||
<Repositories
|
||||
bind:hasInstallations
|
||||
bind:selectedRepository
|
||||
action="button"
|
||||
on:connect={(e) => {
|
||||
connect={(e) => {
|
||||
trackEvent(Click.ConnectRepositoryClick, {
|
||||
from: 'template-wizard'
|
||||
});
|
||||
repository.set(e.detail);
|
||||
repositoryName = e.detail.name;
|
||||
selectedRepository = e.detail.id;
|
||||
repository.set(e);
|
||||
repositoryName = e.name;
|
||||
selectedRepository = e.id;
|
||||
showConfig = true;
|
||||
}} />
|
||||
{/if}
|
||||
|
||||
+86
-79
@@ -13,20 +13,70 @@
|
||||
Typography
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import { DeploymentSource, DeploymentCreatedBy, DeploymentDomains } from '$lib/components/git';
|
||||
|
||||
import { func } from '../store';
|
||||
import { capitalize } from '$lib/helpers/string';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
|
||||
import Link from '$lib/elements/link.svelte';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
export let deployment: Models.Deployment;
|
||||
export let proxyRuleList: Models.ProxyRuleList;
|
||||
export let variant: 'primary' | 'secondary' = 'primary';
|
||||
export let activeDeployment = false;
|
||||
let {
|
||||
deployment,
|
||||
proxyRuleList,
|
||||
variant = 'primary',
|
||||
activeDeployment = false,
|
||||
footer
|
||||
}: {
|
||||
deployment: Models.Deployment;
|
||||
proxyRuleList: Models.ProxyRuleList;
|
||||
variant?: 'primary' | 'secondary';
|
||||
activeDeployment?: boolean;
|
||||
footer?: Snippet;
|
||||
} = $props();
|
||||
|
||||
$: totalSize = humanFileSize(deployment?.totalSize ?? 0);
|
||||
let totalSize = $derived(humanFileSize(deployment?.totalSize ?? 0));
|
||||
</script>
|
||||
|
||||
{#snippet titleSnippet(title: string)}
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
|
||||
{title}
|
||||
</Typography.Text>
|
||||
{/snippet}
|
||||
|
||||
{#snippet textGroup(title: string, text: string)}
|
||||
<Layout.Stack gap="xxs" inline>
|
||||
{@render titleSnippet(title)}
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-primary">
|
||||
{text}
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
{/snippet}
|
||||
|
||||
{#snippet protection(text: string, tooltip: string)}
|
||||
<Layout.Stack gap="xxs" inline>
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
|
||||
<Layout.Stack direction="row" gap="xxs" alignItems="center">
|
||||
{text}
|
||||
<Tooltip>
|
||||
<Icon icon={IconInfo} size="s" />
|
||||
<span slot="tooltip">
|
||||
{tooltip}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Layout.Stack>
|
||||
</Typography.Text>
|
||||
<Layout.Stack inline alignItems="flex-start">
|
||||
<Badge
|
||||
size="xs"
|
||||
variant="secondary"
|
||||
type={isCloud ? 'success' : null}
|
||||
content={isCloud ? 'Connected' : 'Available on Cloud'} />
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
{/snippet}
|
||||
|
||||
<Card padding="m" radius="m" {variant}>
|
||||
<Layout.Stack gap="xxl">
|
||||
<Layout.GridFraction start={4} end={6}>
|
||||
@@ -57,20 +107,24 @@
|
||||
<Layout.Stack gap="xl">
|
||||
<Layout.Stack direction="row" alignItems="flex-start">
|
||||
<Layout.Stack gap="xxs">
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
|
||||
Domains
|
||||
</Typography.Text>
|
||||
{@render titleSnippet('Domains')}
|
||||
|
||||
<DeploymentDomains domains={proxyRuleList} />
|
||||
{#if $func.deploymentId === deployment.$id}
|
||||
<DeploymentDomains domains={proxyRuleList} />
|
||||
{:else}
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-primary">
|
||||
Only the <Link
|
||||
href={`${base}/project-${page.params.project}/functions/function-${$func.$id}/deployment-${$func.deploymentId}`}
|
||||
variant="default">active deployment</Link> has a domain.
|
||||
</Typography.Text>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
|
||||
<Layout.Stack direction="row" gap="xl">
|
||||
{#if deployment.status === 'failed'}
|
||||
<Layout.Stack gap="xxs" inline>
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
|
||||
Status
|
||||
</Typography.Text>
|
||||
{@render titleSnippet('Status')}
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-primary">
|
||||
<Status status={deployment.status} label={deployment.status} />
|
||||
</Typography.Text>
|
||||
@@ -90,27 +144,15 @@
|
||||
<Layout.Stack gap="xxl" direction="row" wrap="wrap">
|
||||
<Layout.Stack gap="xxl" direction="row" wrap="wrap" inline>
|
||||
{#if deployment?.buildDuration}
|
||||
<Layout.Stack gap="xxs" inline>
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
|
||||
Build duration
|
||||
</Typography.Text>
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-primary">
|
||||
{formatTimeDetailed(deployment.buildDuration)}
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
{@render textGroup(
|
||||
'Build duration',
|
||||
formatTimeDetailed(deployment.buildDuration)
|
||||
)}
|
||||
{/if}
|
||||
{@render textGroup('Total size', `${totalSize.value} ${totalSize.unit}`)}
|
||||
|
||||
<Layout.Stack gap="xxs" inline>
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
|
||||
Total size
|
||||
</Typography.Text>
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-primary">
|
||||
{totalSize.value}{totalSize.unit}
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
<Layout.Stack gap="xxs" inline>
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
|
||||
Runtime
|
||||
</Typography.Text>
|
||||
{@render titleSnippet('Runtime')}
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-primary">
|
||||
<Layout.Stack direction="row" gap="xxs" alignItems="center">
|
||||
<SvgIcon
|
||||
@@ -122,53 +164,18 @@
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
<Layout.Stack gap="xxl" direction="row" wrap="wrap" inline>
|
||||
<Layout.Stack gap="xxs" inline>
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
|
||||
<Layout.Stack direction="row" gap="xxs" alignItems="center">
|
||||
Global CDN <Tooltip>
|
||||
<Icon icon={IconInfo} size="s" />
|
||||
<span slot="tooltip">
|
||||
Optimized speed by caching content on servers closer to
|
||||
users.
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Layout.Stack>
|
||||
</Typography.Text>
|
||||
<Layout.Stack inline alignItems="flex-start">
|
||||
<Badge
|
||||
size="xs"
|
||||
variant="secondary"
|
||||
type={isCloud ? 'success' : null}
|
||||
content={isCloud ? 'Connected' : 'Available on Cloud'} />
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
<Layout.Stack gap="xxs" inline>
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
|
||||
<Layout.Stack direction="row" gap="xxs" alignItems="center">
|
||||
DDoS protection <Tooltip>
|
||||
<Icon icon={IconInfo} size="s" />
|
||||
<span slot="tooltip">
|
||||
Safeguards your site by detecting and blocking malicious
|
||||
traffic.
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Layout.Stack>
|
||||
</Typography.Text>
|
||||
<Layout.Stack inline alignItems="flex-start">
|
||||
<Badge
|
||||
size="xs"
|
||||
variant="secondary"
|
||||
type={isCloud ? 'success' : null}
|
||||
content={isCloud ? 'Connected' : 'Available on Cloud'} />
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
|
||||
{@render protection(
|
||||
'Global CDN',
|
||||
'Optimized speed by caching content on servers closer to users.'
|
||||
)}
|
||||
{@render protection(
|
||||
'DDoS protection',
|
||||
'Safeguards your site by detecting and blocking malicious traffic.'
|
||||
)}
|
||||
</Layout.Stack>
|
||||
<Layout.Stack gap="xxs" inline style="width: min-content;">
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
|
||||
Source
|
||||
</Typography.Text>
|
||||
{@render titleSnippet('Source')}
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-primary">
|
||||
<div>
|
||||
<DeploymentSource {deployment} />
|
||||
@@ -178,13 +185,13 @@
|
||||
</Layout.Stack>
|
||||
</Layout.GridFraction>
|
||||
|
||||
{#if $$slots.footer}
|
||||
{#if footer}
|
||||
<span
|
||||
style="margin-left: calc(-1* var(--space-9));margin-right: calc(-1* var(--space-9));width:auto;">
|
||||
<Divider />
|
||||
</span>
|
||||
<Layout.Stack direction="row-reverse">
|
||||
<slot name="footer" />
|
||||
{@render footer()}
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
|
||||
-148
@@ -1,148 +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 hasInstallations = !!installations?.total;
|
||||
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 : '';
|
||||
hasInstallations = !!installations?.total;
|
||||
if (hasInstallations) {
|
||||
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 hasInstallations}
|
||||
<Layout.Stack gap="xl">
|
||||
<RepositoryBehaviour bind:repositoryBehaviour />
|
||||
{#if repositoryBehaviour === 'new'}
|
||||
<NewRepository
|
||||
bind:repositoryName
|
||||
bind:repositoryPrivate
|
||||
bind:selectedInstallationId
|
||||
{installations} />
|
||||
{:else}
|
||||
<Repositories
|
||||
bind:hasInstallations
|
||||
bind:selectedRepository
|
||||
action="button"
|
||||
{callbackState}
|
||||
on:connect={(e) => {
|
||||
trackEvent(Click.ConnectRepositoryClick, {
|
||||
from: 'functions'
|
||||
});
|
||||
repository.set(e.detail);
|
||||
repositoryName = e.detail.name;
|
||||
selectedRepository = e.detail.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>
|
||||
+2
-2
@@ -209,8 +209,8 @@
|
||||
callbackState={{
|
||||
createDeployment: 'true'
|
||||
}}
|
||||
on:connect={(e) => {
|
||||
repository.set(e.detail);
|
||||
connect={(e) => {
|
||||
repository.set(e);
|
||||
hasRepository = true;
|
||||
}} />
|
||||
{/if}
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@
|
||||
await sdk.forProject.functions.createDeployment(
|
||||
$func.$id,
|
||||
files[0],
|
||||
undefined,
|
||||
true,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
deployment={activeDeployment}
|
||||
proxyRuleList={data.proxyRuleList}
|
||||
activeDeployment>
|
||||
<svelte:fragment slot="footer">
|
||||
{#snippet footer()}
|
||||
<Layout.Stack direction="row" gap="s" alignItems="center" inline>
|
||||
<Menu>
|
||||
<Button secondary icon text>
|
||||
@@ -112,7 +112,7 @@
|
||||
Execute
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
</svelte:fragment>
|
||||
{/snippet}
|
||||
</DeploymentCard>
|
||||
{:else if data.activeDeployment?.status === 'building'}
|
||||
<Card.Base padding="none">
|
||||
@@ -171,9 +171,7 @@
|
||||
<Layout.Stack>
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
<Layout.Stack direction="row" gap="s" wrap="wrap">
|
||||
{#if data.deploymentList.total}
|
||||
<QuickFilters {columns} analyticsSource="function_deployments" />
|
||||
{/if}
|
||||
<QuickFilters {columns} analyticsSource="function_deployments" />
|
||||
</Layout.Stack>
|
||||
<Layout.Stack direction="row" gap="s" inline>
|
||||
{#if data.deploymentList.total}
|
||||
|
||||
+2
-2
@@ -72,7 +72,7 @@
|
||||
|
||||
<Container>
|
||||
<DeploymentCard proxyRuleList={data.proxyRuleList} deployment={data.deployment}>
|
||||
<svelte:fragment slot="footer">
|
||||
{#snippet footer()}
|
||||
<Layout.Stack direction="row" inline>
|
||||
{#if data.deployment.status === 'processing' || data.deployment.status === 'building' || data.deployment.status === 'waiting'}
|
||||
<Button
|
||||
@@ -120,7 +120,7 @@
|
||||
}}>Activate</Button>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</svelte:fragment>
|
||||
{/snippet}
|
||||
</DeploymentCard>
|
||||
|
||||
<Card.Base padding="s">
|
||||
|
||||
+18
-130
@@ -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}
|
||||
|
||||
+114
-167
@@ -1,68 +1,36 @@
|
||||
<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;
|
||||
let { data } = $props();
|
||||
|
||||
let formComponent: Form;
|
||||
let isSubmitting = writable(false);
|
||||
|
||||
let showConnectRepo = false;
|
||||
let behaviour: 'REDIRECT' | 'CREATE' = 'CREATE';
|
||||
let domain = '';
|
||||
let redirect: string = null;
|
||||
let statusCode: number = null;
|
||||
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
|
||||
}
|
||||
];
|
||||
let isSubmitting = $state(writable(false));
|
||||
let showConnectRepo = $state(false);
|
||||
let behaviour: 'REDIRECT' | 'BRANCH' | 'ACTIVE' = $state('ACTIVE');
|
||||
let domainName = $state('');
|
||||
let redirect: string = $state(null);
|
||||
let statusCode = $state(307);
|
||||
let branch: string = $state(null);
|
||||
|
||||
onMount(() => {
|
||||
if (
|
||||
@@ -71,75 +39,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 +80,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 +131,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 +181,6 @@
|
||||
required
|
||||
value="main"
|
||||
placeholder="Select branch" />
|
||||
|
||||
<Alert.Inline
|
||||
title=" There is no repository connected to your function">
|
||||
<Layout.Stack>
|
||||
@@ -232,42 +198,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 +224,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 +236,8 @@
|
||||
{#if showConnectRepo}
|
||||
<ConnectRepoModal
|
||||
bind:show={showConnectRepo}
|
||||
func={data.function}
|
||||
{connect}
|
||||
product="functions"
|
||||
onlyExisting
|
||||
callbackState={{ connectRepo: 'true' }} />
|
||||
{/if}
|
||||
|
||||
+3
-1
@@ -1,9 +1,11 @@
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
import { RuleTrigger, RuleType } from '$lib/stores/sdk';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
|
||||
export const load = async ({ parent }) => {
|
||||
export const load = async ({ parent, depends }) => {
|
||||
const { function: func } = await parent();
|
||||
depends(Dependencies.DOMAINS, Dependencies.FUNCTION_DOMAINS);
|
||||
|
||||
const [domains, installations] = await Promise.all([
|
||||
sdk.forProject.proxy.listRules([
|
||||
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
<script lang="ts">
|
||||
import { IconGlobeAlt } from '@appwrite.io/pink-icons-svelte';
|
||||
import {
|
||||
Card,
|
||||
Divider,
|
||||
Fieldset,
|
||||
Icon,
|
||||
Layout,
|
||||
Tabs,
|
||||
Typography
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import { Button, Form } from '$lib/elements/forms';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { page } from '$app/state';
|
||||
import Wizard from '$lib/layout/wizard.svelte';
|
||||
import { base } from '$app/paths';
|
||||
import { writable } from 'svelte/store';
|
||||
import { consoleVariables } from '$routes/(console)/store';
|
||||
import NameserverTable from '$lib/components/domains/nameserverTable.svelte';
|
||||
import RecordTable from '$lib/components/domains/recordTable.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
const ruleId = page.url.searchParams.get('rule');
|
||||
let isSubDomain = $derived(page.params.domain?.split('.')?.length >= 3);
|
||||
|
||||
let selectedTab: 'cname' | 'nameserver' | 'a' | 'aaaa' = $state(
|
||||
!!$consoleVariables._APP_DOMAIN_TARGET_CNAME && isSubDomain
|
||||
? 'cname'
|
||||
: !!$consoleVariables._APP_DOMAIN_TARGET_A
|
||||
? 'a'
|
||||
: !!$consoleVariables._APP_DOMAIN_TARGET_AAAA
|
||||
? 'aaaa'
|
||||
: 'nameserver'
|
||||
);
|
||||
let verified = $state(false);
|
||||
|
||||
let routeBase = `${base}/project-${page.params.project}/functions/function-${page.params.function}/domains`;
|
||||
let isSubmitting = $state(writable(false));
|
||||
|
||||
async function verify() {
|
||||
const isNewDomain =
|
||||
data.domainsList.domains.findIndex((rule) => rule.domain === page.params.domain) === -1;
|
||||
try {
|
||||
if (selectedTab !== 'nameserver') {
|
||||
const ruleData = await sdk.forProject.proxy.updateRuleVerification(ruleId);
|
||||
verified = ruleData.status === 'verified';
|
||||
} else if (isNewDomain && isCloud) {
|
||||
const domainData = await sdk.forConsole.domains.create(
|
||||
$organization.$id,
|
||||
page.params.domain
|
||||
);
|
||||
verified = domainData.nameservers.toLocaleLowerCase() === 'appwrite';
|
||||
}
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Domain added successfully'
|
||||
});
|
||||
await goto(routeBase);
|
||||
await invalidate(Dependencies.DOMAINS);
|
||||
await invalidate(Dependencies.FUNCTION_DOMAINS);
|
||||
} catch (error) {
|
||||
verified = false;
|
||||
isSubmitting.set(false);
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function back() {
|
||||
if (ruleId) {
|
||||
await sdk.forProject.proxy.deleteRule(ruleId);
|
||||
}
|
||||
await goto(`${routeBase}/add-domain?domain=${page.params.domain}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Wizard title="Add domain" href={routeBase} column columnSize="s">
|
||||
<Form onSubmit={verify} bind:isSubmitting>
|
||||
<Layout.Stack gap="xxl">
|
||||
<Card.Base radius="s" padding="s">
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
gap="xs">
|
||||
<Layout.Stack direction="row" alignItems="center" gap="xs">
|
||||
<Icon icon={IconGlobeAlt} color="--fgcolor-neutral-primary" />
|
||||
|
||||
<Typography.Text variation="m-500" color="--fgcolor-neutral-primary">
|
||||
{page.params.domain}
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
<Button secondary on:click={back}>Change</Button>
|
||||
</Layout.Stack>
|
||||
</Card.Base>
|
||||
|
||||
<Fieldset legend="Verification">
|
||||
<Layout.Stack gap="xl">
|
||||
<div>
|
||||
<Tabs.Root variant="secondary" let:root>
|
||||
{#if isSubDomain && !!$consoleVariables._APP_DOMAIN_TARGET_CNAME && $consoleVariables._APP_DOMAIN_TARGET_CNAME !== 'localhost'}
|
||||
<Tabs.Item.Button
|
||||
{root}
|
||||
on:click={() => (selectedTab = 'cname')}
|
||||
active={selectedTab === 'cname'}>
|
||||
CNAME
|
||||
</Tabs.Item.Button>
|
||||
{/if}
|
||||
{#if isCloud}
|
||||
<Tabs.Item.Button
|
||||
{root}
|
||||
on:click={() => (selectedTab = 'nameserver')}
|
||||
active={selectedTab === 'nameserver'}>
|
||||
Nameservers
|
||||
</Tabs.Item.Button>
|
||||
{/if}
|
||||
{#if !!$consoleVariables._APP_DOMAIN_TARGET_A && $consoleVariables._APP_DOMAIN_TARGET_A !== '127.0.0.1'}
|
||||
<Tabs.Item.Button
|
||||
{root}
|
||||
on:click={() => (selectedTab = 'a')}
|
||||
active={selectedTab === 'a'}>
|
||||
A
|
||||
</Tabs.Item.Button>
|
||||
{/if}
|
||||
{#if !!$consoleVariables._APP_DOMAIN_TARGET_AAAA && $consoleVariables._APP_DOMAIN_TARGET_AAAA !== '::1'}
|
||||
<Tabs.Item.Button
|
||||
{root}
|
||||
on:click={() => (selectedTab = 'aaaa')}
|
||||
active={selectedTab === 'aaaa'}>
|
||||
AAAA
|
||||
</Tabs.Item.Button>
|
||||
{/if}
|
||||
</Tabs.Root>
|
||||
<Divider />
|
||||
</div>
|
||||
{#if selectedTab === 'nameserver'}
|
||||
<NameserverTable domain={page.params.domain} {verified} />
|
||||
{:else}
|
||||
<RecordTable domain={page.params.domain} {verified} variant={selectedTab} />
|
||||
{/if}
|
||||
<Divider />
|
||||
<Layout.Stack direction="row" justifyContent="flex-end">
|
||||
<div>
|
||||
<Button submit disabled={$isSubmitting}>Verify</Button>
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Fieldset>
|
||||
</Layout.Stack>
|
||||
</Form>
|
||||
</Wizard>
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Dependencies } from '$lib/constants.js';
|
||||
import { isCloud } from '$lib/system';
|
||||
import type { Domain, DomainsList } from '$lib/sdk/domains.js';
|
||||
|
||||
export const load = async ({ parent, depends, params }) => {
|
||||
depends(Dependencies.DOMAINS);
|
||||
|
||||
let domain: Domain;
|
||||
let domainsList: DomainsList;
|
||||
if (isCloud) {
|
||||
[domain, domainsList] = await Promise.all([
|
||||
sdk.forConsole.domains.get(params.domain),
|
||||
sdk.forConsole.domains.list()
|
||||
]);
|
||||
}
|
||||
|
||||
return {
|
||||
domain,
|
||||
domainsList
|
||||
};
|
||||
};
|
||||
-182
@@ -1,182 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
Badge,
|
||||
Divider,
|
||||
Fieldset,
|
||||
Icon,
|
||||
InteractiveText,
|
||||
Layout,
|
||||
Table,
|
||||
Tabs,
|
||||
Typography
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import { Card } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { Wizard } from '$lib/layout';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Link } from '$lib/elements';
|
||||
import { consoleVariables } from '$routes/(console)/store';
|
||||
|
||||
const backPage = `${base}/project-${page.params.project}/sites/site-${page.params.site}/domains`;
|
||||
|
||||
let nameservers = $consoleVariables?._APP_DOMAINS_NAMESERVERS
|
||||
? $consoleVariables?._APP_DOMAINS_NAMESERVERS.split(',')
|
||||
: ['ns1.appwrite.io', 'ns2.appwrite.io'];
|
||||
|
||||
export let data;
|
||||
async function verifyStatus() {
|
||||
try {
|
||||
await sdk.forProject.proxy.updateRuleVerification(data.domain.$id);
|
||||
console.log(data.domain);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let selectedTab: 'cname' | 'nameserver' = 'cname';
|
||||
|
||||
$: isVerified = data.domain?.nameservers?.toLocaleLowerCase() === 'appwrite';
|
||||
</script>
|
||||
|
||||
<Wizard title="Add custom domain" href={backPage} column columnSize="s" hideFooter>
|
||||
{#if isVerified}
|
||||
<Card radius="s">
|
||||
<Layout.Stack gap="s" direction="row" alignItems="center">
|
||||
<Typography.Text variant="l-500">{data.domain.domain}</Typography.Text>
|
||||
<Badge variant="secondary" type="success" content="Verified" />
|
||||
</Layout.Stack>
|
||||
</Card>
|
||||
{:else}
|
||||
<Fieldset legend="Verification">
|
||||
<Layout.Stack gap="xl">
|
||||
<div>
|
||||
<Tabs.Root variant="secondary" let:root>
|
||||
<Tabs.Item.Button
|
||||
{root}
|
||||
on:click={() => (selectedTab = 'cname')}
|
||||
active={selectedTab === 'cname'}>
|
||||
CNAME
|
||||
</Tabs.Item.Button>
|
||||
<Tabs.Item.Button
|
||||
{root}
|
||||
on:click={() => (selectedTab = 'nameserver')}
|
||||
active={selectedTab === 'nameserver'}>
|
||||
Nameservers
|
||||
</Tabs.Item.Button>
|
||||
</Tabs.Root>
|
||||
<Divider />
|
||||
</div>
|
||||
{#if selectedTab === 'cname'}
|
||||
<Layout.Stack gap="s">
|
||||
<Layout.Stack gap="s" direction="row" alignItems="center">
|
||||
<Typography.Text variant="l-500" color="--fgcolor-neutral-primary"
|
||||
>{data.domain?.domain}</Typography.Text>
|
||||
{#if isVerified}
|
||||
<Badge variant="secondary" type="success" content="Verified" />
|
||||
{:else}
|
||||
<Badge
|
||||
variant="secondary"
|
||||
type="warning"
|
||||
size="xs"
|
||||
content="Pending verification" />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
<Typography.Text variant="m-400">
|
||||
Add the following record on your DNS provider. Note that DNS changes may
|
||||
take time to propagate fully.
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
|
||||
<Table.Root columns={3} let:root>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell {root}>Type</Table.Header.Cell>
|
||||
<Table.Header.Cell {root}>Name</Table.Header.Cell>
|
||||
<Table.Header.Cell {root}>Value</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell {root}>CNAME</Table.Cell>
|
||||
<Table.Cell {root}>{data.domain?.domain}</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
<InteractiveText
|
||||
variant="copy"
|
||||
isVisible
|
||||
text={globalThis?.location?.origin}>
|
||||
{globalThis?.location?.origin}</InteractiveText>
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
</Table.Root>
|
||||
<Layout.Stack gap="s" direction="row" alignItems="center">
|
||||
<Icon icon={IconInfo} size="s" color="--fgcolor-neutral-secondary" />
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-secondary">
|
||||
A list of all domain providers and their DNS setting is available <Link
|
||||
variant="muted"
|
||||
external
|
||||
href="https://appwrite.io/docs/advanced/platform/custom-domains"
|
||||
>here</Link
|
||||
>.
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
|
||||
<Divider />
|
||||
<Layout.Stack direction="row" justifyContent="flex-end">
|
||||
<Button on:click={verifyStatus}>Verify</Button>
|
||||
</Layout.Stack>
|
||||
{:else}
|
||||
<Layout.Stack gap="s">
|
||||
<Layout.Stack gap="s" direction="row" alignItems="center">
|
||||
<Typography.Text variant="l-500" color="--fgcolor-neutral-primary"
|
||||
>{data.domain?.domain}</Typography.Text>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
type="warning"
|
||||
size="xs"
|
||||
content="Pending verification" />
|
||||
</Layout.Stack>
|
||||
<Typography.Text variant="m-400">
|
||||
Add the following nameservers on your DNS provider. Note that DNS
|
||||
changes may take time to propagate fully.
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
|
||||
<Table.Root columns={2} let:root>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell {root}>Type</Table.Header.Cell>
|
||||
<Table.Header.Cell {root}>Value</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
{#each nameservers as nameserver}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell {root}>NS</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
<InteractiveText variant="copy" isVisible text={nameserver}>
|
||||
{nameserver}</InteractiveText>
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
<!-- <Layout.Stack gap="s" direction="row" alignItems="center">
|
||||
<Icon icon={IconInfo} size="s" color="--fgcolor-neutral-secondary" />
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-secondary">
|
||||
A list of all domain providers and their DNS setting is available <Link
|
||||
variant="muted"
|
||||
href="https://appwrite.io/docs/advanced/platform/custom-domains"
|
||||
>here</Link
|
||||
>.
|
||||
</Typography.Text>
|
||||
</Layout.Stack> -->
|
||||
|
||||
<Divider />
|
||||
<Layout.Stack direction="row" justifyContent="flex-end">
|
||||
<Button on:click={verifyStatus}>Verify</Button>
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Fieldset>
|
||||
{/if}
|
||||
</Wizard>
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
// import { sdk } from '$lib/stores/sdk';
|
||||
import { isCloud } from '$lib/system';
|
||||
|
||||
export const load = async (/*{ params }*/) => {
|
||||
if (isCloud) {
|
||||
// if (params?.id) {
|
||||
// return {
|
||||
// domain: await sdk.forConsole.domains.get(params.id)
|
||||
// };
|
||||
// }
|
||||
} else {
|
||||
return {
|
||||
domain: undefined
|
||||
};
|
||||
}
|
||||
};
|
||||
-104
@@ -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 }
|
||||
}
|
||||
]);
|
||||
+121
@@ -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}
|
||||
+5
-3
@@ -21,10 +21,10 @@
|
||||
import { logStatusConverter } from './store';
|
||||
import { LogsRequest, LogsResponse } from '$lib/components/logs';
|
||||
|
||||
export let open = false;
|
||||
export let selectedLogId: string;
|
||||
export let logs: Models.Execution[];
|
||||
export let logging: boolean;
|
||||
export let open = false;
|
||||
|
||||
function nextLog() {
|
||||
const currentIndex = logs.findIndex((log) => log.$id === selectedLogId);
|
||||
@@ -38,6 +38,7 @@
|
||||
selectedLogId = logs[currentIndex - 1].$id;
|
||||
}
|
||||
}
|
||||
|
||||
$: if (!open) {
|
||||
selectedLogId = null;
|
||||
}
|
||||
@@ -47,7 +48,9 @@
|
||||
$: isLastLog = logs.findIndex((log) => log.$id === selectedLogId) === logs.length - 1;
|
||||
</script>
|
||||
|
||||
<Sheet bind:open>
|
||||
<svelte:window onclick={() => (open = false)} />
|
||||
|
||||
<Sheet bind:open closeOnBlur={false}>
|
||||
<div slot="header" style:width="100%">
|
||||
<Layout.Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Layout.Stack direction="row" gap="m" alignItems="center">
|
||||
@@ -107,7 +110,6 @@
|
||||
<Status
|
||||
status={logStatusConverter(selectedLog.status)}
|
||||
label={capitalize(selectedLog.status)}>
|
||||
{capitalize(selectedLog.status)}
|
||||
</Status>
|
||||
</div>
|
||||
<span slot="tooltip">
|
||||
|
||||
+5
-5
@@ -14,8 +14,8 @@
|
||||
export let columns: Column[];
|
||||
export let logs: Models.ExecutionList;
|
||||
|
||||
let openSheet = false;
|
||||
let selectedLogId: string = null;
|
||||
let open = false;
|
||||
</script>
|
||||
|
||||
<Table.Root {columns} let:root>
|
||||
@@ -27,8 +27,9 @@
|
||||
{#each logs.executions as log}
|
||||
<Table.Row.Button
|
||||
{root}
|
||||
on:click={() => {
|
||||
openSheet = true;
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
open = true;
|
||||
selectedLogId = log.$id;
|
||||
}}>
|
||||
{#each columns as column}
|
||||
@@ -67,7 +68,6 @@
|
||||
<Status
|
||||
status={logStatusConverter(status)}
|
||||
label={capitalize(status)}>
|
||||
{status}
|
||||
</Status>
|
||||
</div>
|
||||
<span slot="tooltip">
|
||||
@@ -83,4 +83,4 @@
|
||||
{/each}
|
||||
</Table.Root>
|
||||
|
||||
<Sheet bind:open={openSheet} bind:selectedLogId logs={logs.executions} logging={$func.logging} />
|
||||
<Sheet bind:open bind:selectedLogId logs={logs.executions} logging={$func.logging} />
|
||||
|
||||
+1
-6
@@ -26,12 +26,7 @@
|
||||
<Layout.Stack gap="xxxs" inline>
|
||||
<Typography.Caption variant="400">Updated</Typography.Caption>
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-primary">
|
||||
<Tooltip>
|
||||
{capitalize(timeFromNow($func.$updatedAt))}
|
||||
<span slot="tooltip">
|
||||
{toLocaleDateTime($func.$updatedAt)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
{toLocaleDateTime($func.$updatedAt)}
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
<Layout.Stack gap="xxxs" inline>
|
||||
|
||||
+33
-3
@@ -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}
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
<Container>
|
||||
<Layout.GridFraction start={1} end={3} gap="xxl">
|
||||
<Layout.Stack gap="xl">
|
||||
<SearchQuery search={data.search} placeholder="Search template" />
|
||||
<SearchQuery placeholder="Search template" />
|
||||
<Layout.Stack>
|
||||
<Accordion title="Use case">
|
||||
<Layout.Stack>
|
||||
@@ -140,86 +140,88 @@
|
||||
</Layout.Stack>
|
||||
<Layout.Stack gap="l">
|
||||
{#if data.templates?.length > 0}
|
||||
<Paginator
|
||||
items={data.templates}
|
||||
let:paginatedItems
|
||||
limit={12}
|
||||
hidePages={false}
|
||||
hasLimit>
|
||||
<Layout.Grid columns={2} columnsXL={3} columnsXS={1}>
|
||||
{#each paginatedItems as template}
|
||||
{@const baseRuntimes = getBaseRuntimes(template.runtimes)}
|
||||
{@const displayed = baseRuntimes.slice(0, 2)}
|
||||
{@const hidden = baseRuntimes.slice(1, -1)}
|
||||
<PinkCard.Base radius="m" padding="xs">
|
||||
<Layout.Stack height="100%" justifyContent="space-between" gap="xl">
|
||||
<Layout.Stack gap="xxxs">
|
||||
<Typography.Text
|
||||
variant="m-500"
|
||||
color="--fgcolor-neutral-primary">
|
||||
{template.name}
|
||||
</Typography.Text>
|
||||
|
||||
<Typography.Text variant="m-400">
|
||||
{template.tagline}
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
|
||||
<Paginator items={data.templates} limit={12} hidePages={false} hasLimit>
|
||||
{#snippet children(paginatedItems: typeof data.templates)}
|
||||
<Layout.Grid columns={2} columnsXL={3} columnsXS={1}>
|
||||
{#each paginatedItems as template}
|
||||
{@const baseRuntimes = getBaseRuntimes(template.runtimes)}
|
||||
{@const displayed = baseRuntimes.slice(0, 2)}
|
||||
{@const hidden = baseRuntimes.slice(1, -1)}
|
||||
<PinkCard.Base radius="m" padding="xs">
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
height="100%"
|
||||
justifyContent="space-between"
|
||||
alignItems="center">
|
||||
<AvatarGroup>
|
||||
{#each displayed as runtime}
|
||||
{@const icon = getIconFromRuntime(runtime.name)}
|
||||
{#if icon}
|
||||
<Avatar alt={runtime.name} size="xs">
|
||||
<SvgIcon name={icon} iconSize="small" />
|
||||
</Avatar>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if hidden.length}
|
||||
<Tooltip>
|
||||
<Avatar alt="hidden runtime number" size="xs">
|
||||
<span style:font-size="10px">
|
||||
+{hidden.length}
|
||||
</span>
|
||||
</Avatar>
|
||||
<span slot="tooltip">
|
||||
{hidden.map((n) => n.name).join(', ')}
|
||||
</span>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</AvatarGroup>
|
||||
gap="xl">
|
||||
<Layout.Stack gap="xxxs">
|
||||
<Typography.Text
|
||||
variant="m-500"
|
||||
color="--fgcolor-neutral-primary">
|
||||
{template.name}
|
||||
</Typography.Text>
|
||||
|
||||
<Typography.Text variant="m-400">
|
||||
{template.tagline}
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
gap="s"
|
||||
alignItems="center"
|
||||
inline>
|
||||
<Button
|
||||
href={`${base}/project-${page.params.project}/functions/templates/template-${template.id}`}
|
||||
text>
|
||||
<span class="text">Details</span>
|
||||
</Button>
|
||||
{#if $canWriteFunctions}
|
||||
<ContainerButton
|
||||
title="functions"
|
||||
disabled={buttonDisabled}
|
||||
buttonType="secondary"
|
||||
buttonHref={`${base}/project-${page.params.project}/functions/create-function/template-${template.id}`}
|
||||
showIcon={false}
|
||||
buttonText="Create"
|
||||
buttonEventData={{
|
||||
source: 'functions_template'
|
||||
}}
|
||||
buttonEvent="create_function" />
|
||||
{/if}
|
||||
justifyContent="space-between"
|
||||
alignItems="center">
|
||||
<AvatarGroup>
|
||||
{#each displayed as runtime}
|
||||
{@const icon = getIconFromRuntime(runtime.name)}
|
||||
{#if icon}
|
||||
<Avatar alt={runtime.name} size="xs">
|
||||
<SvgIcon name={icon} iconSize="small" />
|
||||
</Avatar>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if hidden.length}
|
||||
<Tooltip>
|
||||
<Avatar
|
||||
alt="hidden runtime number"
|
||||
size="xs">
|
||||
<span style:font-size="10px">
|
||||
+{hidden.length}
|
||||
</span>
|
||||
</Avatar>
|
||||
<span slot="tooltip">
|
||||
{hidden.map((n) => n.name).join(', ')}
|
||||
</span>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</AvatarGroup>
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
gap="s"
|
||||
alignItems="center"
|
||||
inline>
|
||||
<Button
|
||||
href={`${base}/project-${page.params.project}/functions/templates/template-${template.id}`}
|
||||
text>
|
||||
<span class="text">Details</span>
|
||||
</Button>
|
||||
{#if $canWriteFunctions}
|
||||
<ContainerButton
|
||||
title="functions"
|
||||
disabled={buttonDisabled}
|
||||
buttonType="secondary"
|
||||
buttonHref={`${base}/project-${page.params.project}/functions/create-function/template-${template.id}`}
|
||||
showIcon={false}
|
||||
buttonText="Create"
|
||||
buttonEventData={{
|
||||
source: 'functions_template'
|
||||
}}
|
||||
buttonEvent="create_function" />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</PinkCard.Base>
|
||||
{/each}
|
||||
</Layout.Grid>
|
||||
</PinkCard.Base>
|
||||
{/each}
|
||||
</Layout.Grid>
|
||||
{/snippet}
|
||||
</Paginator>
|
||||
{:else if data?.search}
|
||||
<EmptySearch hidePagination search={data.search}>
|
||||
|
||||
@@ -90,11 +90,11 @@
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
<SearchQuery
|
||||
search={data.search}
|
||||
placeholder="Search by ID, description, type, or status" />
|
||||
placeholder="Search by description, type, status, or ID" />
|
||||
</Layout.Stack>
|
||||
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
|
||||
<Filters query={data.query} {columns} analyticsSource="messaging_messages" />
|
||||
<ViewSelector view={data.view} {columns} hideView allowNoColumns />
|
||||
<ViewSelector view={data.view} {columns} hideView />
|
||||
{#if $canWriteMessages}
|
||||
<CreateMessageDropdown />
|
||||
{/if}
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
|
||||
<Wizard
|
||||
title="Create email message"
|
||||
columnSize="s"
|
||||
href={`${base}/project-${page.params.project}/messaging/`}
|
||||
bind:showExitModal
|
||||
column
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
let showCustomId = false;
|
||||
let id: string;
|
||||
let file: Models.File;
|
||||
let data: Writable<[string, string][]> = writable([]);
|
||||
let data: Writable<[string, string][]> = writable([['', '']]);
|
||||
let title: string;
|
||||
let body: string;
|
||||
let topics: string[];
|
||||
@@ -135,51 +135,53 @@
|
||||
</Layout.Stack>
|
||||
</Fieldset>
|
||||
<Fieldset legend="Custom data">
|
||||
<Layout.Stack>
|
||||
<Layout.Stack gap="l">
|
||||
<Typography.Text>
|
||||
A key/value payload of additional metadata that's hidden from users. Use
|
||||
this to include information to support logic such as redirection and
|
||||
routing.
|
||||
</Typography.Text>
|
||||
{#each $data as [key, value], index}
|
||||
<Layout.Stack direction="row" alignItems="flex-end">
|
||||
<InputText
|
||||
id={`key-${index}`}
|
||||
bind:value={key}
|
||||
placeholder="Enter key"
|
||||
label={index === 0 ? 'Key' : undefined} />
|
||||
<Layout.Stack direction="row" alignItems="flex-end" gap="xs">
|
||||
<Layout.Stack gap="s">
|
||||
{#each $data as [key, value], index}
|
||||
<Layout.Stack direction="row" alignItems="flex-end">
|
||||
<InputText
|
||||
id={`value-${index}`}
|
||||
bind:value
|
||||
placeholder="Enter value"
|
||||
label={index === 0 ? 'Value' : undefined} />
|
||||
<Button
|
||||
icon
|
||||
compact
|
||||
disabled={(!key || !value) && index === 0}
|
||||
on:click={() => {
|
||||
if (index === 0 && $data?.length === 1) {
|
||||
$data = [['', '']];
|
||||
} else {
|
||||
$data.splice(index, 1);
|
||||
$data = $data;
|
||||
}
|
||||
}}>
|
||||
<span class="icon-x" aria-hidden="true"></span>
|
||||
</Button>
|
||||
id={`key-${index}`}
|
||||
bind:value={key}
|
||||
placeholder="Enter key"
|
||||
label={index === 0 ? 'Key' : undefined} />
|
||||
<Layout.Stack direction="row" alignItems="flex-end" gap="xs">
|
||||
<InputText
|
||||
id={`value-${index}`}
|
||||
bind:value
|
||||
placeholder="Enter value"
|
||||
label={index === 0 ? 'Value' : undefined} />
|
||||
<Button
|
||||
icon
|
||||
compact
|
||||
disabled={(!key || !value) && index === 0}
|
||||
on:click={() => {
|
||||
if (index === 0 && $data?.length === 1) {
|
||||
$data = [['', '']];
|
||||
} else {
|
||||
$data.splice(index, 1);
|
||||
$data = $data;
|
||||
}
|
||||
}}>
|
||||
<span class="icon-x" aria-hidden="true"></span>
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
{/each}
|
||||
<div>
|
||||
<Button
|
||||
secondary
|
||||
disabled={$data.length > 0 && $data[$data.length - 1][0] === ''}
|
||||
on:click={() => ($data = [...$data, ['', '']])}>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Add data
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
<div>
|
||||
<Button
|
||||
compact
|
||||
disabled={$data.length > 0 && $data[$data.length - 1][0] === ''}
|
||||
on:click={() => ($data = [...$data, ['', '']])}>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Add data
|
||||
</Button>
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Fieldset>
|
||||
<Fieldset legend="Targets">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user