Merge branch 'feat-pink-v2' into fix-dualtimstamp

This commit is contained in:
Darshan
2025-03-24 17:33:11 +05:30
committed by GitHub
61 changed files with 944 additions and 663 deletions
+2 -2
View File
@@ -19,11 +19,11 @@
"e2e:ui": "playwright test tests/e2e --ui"
},
"dependencies": {
"@appwrite.io/console": "https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@74f9464",
"@appwrite.io/console": "https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@08b7400",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@9738320",
"@appwrite.io/pink-legacy": "^1.0.3",
"@appwrite.io/pink-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@9738320",
"@appwrite.io/pink-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@11e0653",
"@popperjs/core": "^2.11.8",
"@sentry/sveltekit": "^8.38.0",
"@stripe/stripe-js": "^3.5.0",
+18 -17
View File
@@ -9,8 +9,8 @@ importers:
.:
dependencies:
'@appwrite.io/console':
specifier: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@74f9464
version: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@74f9464
specifier: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@08b7400
version: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@08b7400
'@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@9738320
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@9738320(react-dom@18.3.1(react@18.3.1))(svelte@4.2.19)
specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@11e0653
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@11e0653(react-dom@18.3.1(react@18.3.1))(svelte@4.2.19)
'@popperjs/core':
specifier: ^2.11.8
version: 2.11.8
@@ -211,18 +211,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@74f9464':
resolution: {tarball: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@74f9464}
'@appwrite.io/console@https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@08b7400':
resolution: {tarball: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@08b7400}
version: 1.2.1
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@9738320':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@9738320}
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@11e065399ceac01b462887282f593271ec8b29a4':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@11e065399ceac01b462887282f593271ec8b29a4}
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@9738320c3694272255ba4cc7639586ed8bbcc094':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@9738320c3694272255ba4cc7639586ed8bbcc094}
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@64948b6':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@64948b6}
version: 1.0.0-next.7
peerDependencies:
svelte: ^4.0.0
@@ -236,8 +236,9 @@ 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@9738320':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@9738320}
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@11e0653':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@11e0653}
version: 1.0.0-next.85
peerDependencies:
react-dom: ^18.0.0
@@ -4241,13 +4242,13 @@ snapshots:
'@analytics/type-utils@0.6.2': {}
'@appwrite.io/console@https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@74f9464': {}
'@appwrite.io/console@https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@08b7400': {}
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@9738320(svelte@4.2.19)':
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@11e065399ceac01b462887282f593271ec8b29a4(svelte@4.2.19)':
dependencies:
svelte: 4.2.19
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@9738320c3694272255ba4cc7639586ed8bbcc094(svelte@4.2.19)':
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@64948b6(svelte@4.2.19)':
dependencies:
svelte: 4.2.19
@@ -4260,9 +4261,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@9738320(react-dom@18.3.1(react@18.3.1))(svelte@4.2.19)':
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@11e0653(react-dom@18.3.1(react@18.3.1))(svelte@4.2.19)':
dependencies:
'@appwrite.io/pink-icons-svelte': https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@9738320c3694272255ba4cc7639586ed8bbcc094(svelte@4.2.19)
'@appwrite.io/pink-icons-svelte': https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@11e065399ceac01b462887282f593271ec8b29a4(svelte@4.2.19)
'@floating-ui/dom': 1.6.13
'@melt-ui/pp': 0.3.2(@melt-ui/svelte@0.86.3(svelte@4.2.19))(svelte@4.2.19)
'@melt-ui/svelte': 0.86.3(svelte@4.2.19)
@@ -5,8 +5,16 @@
import { onMount } from 'svelte';
import PaymentModal from './paymentModal.svelte';
import { capitalize } from '$lib/helpers/string';
import { Alert, Fieldset, Icon, Layout, Selector } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import {
Alert,
Fieldset,
Icon,
Layout,
Selector,
Card,
Typography
} from '@appwrite.io/pink-svelte';
import { IconExclamationCircle, IconPlus } from '@appwrite.io/pink-icons-svelte';
import InputSelect from '$lib/elements/forms/inputSelect.svelte';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
@@ -33,48 +41,55 @@
$: selectedPaymentMethod = methods?.paymentMethods?.find((method) => method.$id === value);
</script>
<Fieldset legend="Payment">
<Layout.Stack>
{#if filteredMethods?.length}
{#if selectedPaymentMethod?.country?.toLowerCase() === 'in'}
<Alert.Inline status="warning">
<svelte:fragment slot="title"
>Indian credit or debit card-holders</svelte:fragment>
To comply with RBI regulations in India, Appwrite will ask for verification to charge
up to $150 USD on your payment method. We will never charge more than the cost of
your plan and the resources you use, or your budget cap limit. For higher usage limits,
please contact us.
</Alert.Inline>
{/if}
<InputSelect
id="method"
required
label="Payment method"
placeholder="Select payment method"
bind:value
options={filteredMethods.map((method) => {
return {
value: method.$id,
label: `${capitalize(method.brand)} ending in ${method.last4}`,
data: [method.brand]
};
})} />
<div>
<Layout.Stack gap="s">
{#if filteredMethods?.length}
{#if selectedPaymentMethod?.country?.toLowerCase() === 'in'}
<Alert.Inline status="warning">
<svelte:fragment slot="title">Indian credit or debit card-holders</svelte:fragment>
To comply with RBI regulations in India, Appwrite will ask for verification to charge
up to $150 USD on your payment method. We will never charge more than the cost of your
plan and the resources you use, or your budget cap limit. For higher usage limits, please
contact us.
</Alert.Inline>
{/if}
<InputSelect
id="method"
required
label="Payment method"
placeholder="Select payment method"
bind:value
options={filteredMethods.map((method) => {
return {
value: method.$id,
label: `${capitalize(method.brand)} ending in ${method.last4}`,
data: [method.brand]
};
})} />
<div>
<Button on:click={() => (showPaymentModal = true)} compact size="s">
<Icon icon={IconPlus} slot="start" size="s" />
Add payment method
</Button>
</div>
{:else}
<Card.Base variant="secondary" radius="s" padding="xs">
<Layout.Stack direction="row">
<Layout.Stack direction="row" gap="xxs" alignItems="center">
<Icon
icon={IconExclamationCircle}
size="m"
color="--fgcolor-neutral-tertiary" />
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary"
>No saved payment methods</Typography.Text>
</Layout.Stack>
<Button secondary on:click={() => (showPaymentModal = true)}>
<Icon icon={IconPlus} slot="start" size="s" />
Add new payment method
</Button>
</div>
{:else}
<Alert.Inline title="No saved payment methods">
<Button slot="actions" secondary on:click={() => (showPaymentModal = true)}>
<Icon icon={IconPlus} slot="start" size="s" />
Add
</Button>
</Alert.Inline>
{/if}
</Layout.Stack>
</Fieldset>
</Layout.Stack>
</Card.Base>
{/if}
</Layout.Stack>
{#if showPaymentModal && isCloud && hasStripePublicKey}
<PaymentModal bind:show={showPaymentModal} on:submit={cardSaved}>
@@ -5,7 +5,7 @@
import { sdk } from '$lib/stores/sdk';
import { sortBranches } from '$lib/stores/vcs';
export let branch: string;
export let branch = 'main';
export let rootDir: string;
export let silentMode: boolean;
export let installationId: string;
+6 -1
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import { page } from '$app/stores';
import { app } from '$lib/stores/app';
import {
Icon,
Sidebar,
@@ -82,7 +83,11 @@
];
</script>
<div class:only-mobile-tablet={project === undefined}>
<div
class:only-mobile-tablet={project === undefined}
style:--overlay-on-neutral={$app.themeInUse === 'dark'
? 'var(--neutral-750)'
: 'var(--neutral-100)'}>
<Sidebar.Base
{...$$props}
bind:state
+10 -8
View File
@@ -96,14 +96,16 @@
<ActionMenu.Root>
<Layout.Stack>
{#each $columns as column}
<InputCheckbox
id={column.id}
label={column.title}
on:change={() => (column.hide = !column.hide)}
checked={!column.hide}
disabled={allowNoColumns
? false
: selectedColumnsNumber <= 1 && column.hide !== true} />
{#if !column?.exclude}
<InputCheckbox
id={column.id}
label={column.title}
on:change={() => (column.hide = !column.hide)}
checked={!column.hide}
disabled={allowNoColumns
? false
: selectedColumnsNumber <= 1 && column.hide !== true} />
{/if}
{/each}
</Layout.Stack>
</ActionMenu.Root>
+2 -2
View File
@@ -380,13 +380,13 @@ export const scopes: {
},
{
scope: 'log.read',
description: "Access to read your sites's logs",
description: "Access to read your sites's logs",
category: 'Sites',
icon: 'globe'
},
{
scope: 'log.write',
description: "Access to execute your project's sites",
description: "Access to delete your site's logs",
category: 'Sites',
icon: 'globe'
}
+3 -1
View File
@@ -7,6 +7,7 @@
export let placeholder = '';
export let disabled = false;
export let helper: string | undefined = undefined;
export let pattern: string | undefined = undefined;
let value = '';
let error: string;
@@ -33,7 +34,8 @@
{id}
{placeholder}
{disabled}
{pattern}
bind:value={tags}
helper={error || helper}
on:invalid={handleInvalid}
state={error ? 'error' : 'default'} />
state={error ? 'error' : 'default'}><slot name="info" slot="info" /></Input.Tags>
+1 -1
View File
@@ -15,7 +15,7 @@
export let autocomplete = false;
export let maxlength: number = null;
let error: string;
export let error: string = null;
const handleInvalid = (event: Event) => {
event.preventDefault();
+5
View File
@@ -48,3 +48,8 @@ const formatter = Intl.NumberFormat('en', {
export function formatNum(number: number): string {
return formatter.format(number);
}
/**
* Returns a regex to check hostname validity. Supports wildcards too!
*/
export const hostnameRegex = String.raw`(\*)|(\*\.)?(?!-)[A-Za-z0-9\-]+([\-\.]{1}[a-z0-9]+)*\.[A-Za-z]{2,18}|localhost`;
+26
View File
@@ -114,3 +114,29 @@ export function formatTimeDetailed(time: number): string {
return parts.join(' ');
}
/**
* Calculates the time difference between a starting date/time and the current time.
* Returns the difference formatted in seconds, minutes, or hours depending on the magnitude.
*
* @export
* @param {Date | number | string} startTime - The starting date/time, timestamp in milliseconds, or ISO date string (e.g., '2025-03-20T15:50:43.495+00:00')
* @returns {string} The formatted time difference
*/
export function timer(startTime: Date | number | string): string {
let start: number;
if (startTime instanceof Date) {
start = startTime.getTime();
} else if (typeof startTime === 'number') {
start = startTime;
} else {
// Handle ISO date string format
start = new Date(startTime).getTime();
}
const now = new Date().getTime();
const diffInSeconds = (now - start) / 1000;
return calculateTime(diffInSeconds);
}
+1
View File
@@ -31,6 +31,7 @@ export type Column = PinkColumn & {
array?: boolean;
format?: string;
show?: boolean;
exclude?: boolean;
elements?: string[] | { value: string | number; label: string }[];
};
+67 -53
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { base } from '$app/paths';
import { Avatar, AvatarInitials, Card } from '$lib/components';
import { AvatarInitials, Card } from '$lib/components';
import AppwriteLogoDark from '$lib/images/appwrite-logo-dark.svg';
import AppwriteLogoLight from '$lib/images/appwrite-logo-light.svg';
import LoginDark from '$lib/images/login/login-dark-mode.png';
@@ -9,7 +9,7 @@
import { app } from '$lib/stores/app';
import type { Campaign } from '$lib/stores/campaigns';
import { getApiEndpoint } from '$lib/stores/sdk';
import { Typography } from '@appwrite.io/pink-svelte';
import { Typography, Layout, Avatar } from '@appwrite.io/pink-svelte';
export const imgLight = LoginLight;
export const imgDark = LoginDark;
@@ -85,60 +85,74 @@
<p>Build like a team of hundreds<span class="underscore">_</span></p>
</div>
{:else if variation === 'card'}
<section
class="u-flex u-flex-vertical u-main-center u-cross-center u-height-100-percent u-width-full-line">
<img
src={getImage(campaign?.image[$app.themeInUse])}
class="u-block u-image-object-fit-cover side-bg-img"
alt="promo" />
<div style:max-inline-size="30rem" style:height="100%">
<Layout.Stack justifyContent="center" alignItems="center" height="100%" gap="xxxl">
<img
src={getImage(campaign?.image[$app.themeInUse])}
class="u-block u-image-object-fit-cover side-bg-img"
alt="promo" />
<div class="u-text-center auth-container">
<Typography.Title size="m">{generateTitle()}</Typography.Title>
<p class="u-margin-block-start-16">
{generateDesc()}
</p>
</div>
</section>
<Layout.Stack gap="s">
<Typography.Title size="s" color="--fgcolor-neutral-primary" align="center"
>{generateTitle()}</Typography.Title>
<Typography.Text
variant="l-400"
color="--fgcolor-neutral-secondary"
align="center">
{generateDesc()}
</Typography.Text>
</Layout.Stack>
</Layout.Stack>
</div>
{:else if variation === 'review'}
<section
class="u-flex u-flex-vertical u-main-center u-cross-center u-height-100-percent u-width-full-line review-container">
<div class="u-text-center">
<Typography.Title size="m">{generateTitle()}</Typography.Title>
</div>
<p class="body-text-1 u-margin-block-start-16 u-text-center">{generateDesc()}</p>
<div class="review-container u-margin-block-start-64">
<Card style="--p-card-padding: 1.25rem">
<p class="body-text-1">
{currentReview.review}
</p>
<div class="u-margin-block-start-16 u-flex u-gap-16">
{#if currentReview?.image}
<Avatar
src={getImage(currentReview?.image)}
alt={currentReview.name}
size="m" />
{:else}
<AvatarInitials size="m" name={currentReview.name} />
{/if}
<div>
<p class="body-text-2 u-bold">{currentReview.name}</p>
<p class="body-text-2">{currentReview.description}</p>
</div>
</div>
<div style:max-inline-size="30rem" style:height="100%">
<Layout.Stack justifyContent="center" alignItems="center" height="100%" gap="xxxl">
<Card radius="m" padding="s">
<Layout.Stack gap="xl">
<Typography.Text variant="l-400" color="--fgcolor-neutral-secondary">
{currentReview.review}</Typography.Text>
<Layout.Stack gap="s" direction="row">
{#if currentReview?.image}
<Avatar
src={getImage(currentReview?.image)}
alt={currentReview.name}
size="m" />
{:else}
<AvatarInitials size="m" name={currentReview.name} />
{/if}
<Layout.Stack direction="column" gap="xxxs">
<Typography.Text
variant="m-500"
color="--fgcolor-neutral-primary"
>{currentReview.name}</Typography.Text>
<Typography.Text
variant="m-400"
color="--fgcolor-neutral-secondary"
>{currentReview.description}</Typography.Text>
</Layout.Stack>
</Layout.Stack>
</Layout.Stack>
</Card>
</div>
{#if campaign?.footer}
<div
class="u-flex u-gap-16 u-cross-center u-main-center review-footer-container">
<p class="u-bold" style:text-transform="uppercase">provided to you by</p>
<img
style:max-block-size="2.5rem"
src={getImage(campaign?.image[$app.themeInUse])}
alt={coupon?.campaign ?? campaign.$id} />
</div>
{/if}
</section>
<Layout.Stack gap="s">
<Typography.Title size="s" align="center" color="--fgcolor-neutral-primary"
>{generateTitle()}</Typography.Title>
<Typography.Text variant="l-400" align="center"
>{generateDesc()}</Typography.Text>
</Layout.Stack>
{#if campaign?.footer}
<div
class="u-flex u-gap-16 u-cross-center u-main-center review-footer-container">
<p class="u-bold" style:text-transform="uppercase">
provided to you by
</p>
<img
style:max-block-size="2.5rem"
src={getImage(campaign?.image[$app.themeInUse])}
alt={coupon?.campaign ?? campaign.$id} />
</div>
{/if}
</Layout.Stack>
</div>
{/if}
</section>
<section class="grid-1-1-col-2 u-flex u-flex-vertical u-cross-center u-main-center">
+92 -69
View File
@@ -25,6 +25,8 @@
import { onMount } from 'svelte';
import { writable } from 'svelte/store';
import { plansInfo } from '$lib/stores/billing';
import { Fieldset, Icon, Layout, Tooltip } from '@appwrite.io/pink-svelte';
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
export let data;
@@ -70,6 +72,11 @@
let couponData = data?.couponData;
let campaign = data?.campaign;
let billingPlan = BillingPlan.PRO;
let tempOrgId = null;
$: onlyNewOrgs = (campaign && campaign.onlyNewOrgs) || (couponData && couponData.onlyNewOrgs);
$: selectedOrgId = tempOrgId;
onMount(async () => {
await loadPaymentMethods();
@@ -83,6 +90,10 @@
if (campaign?.plan) {
billingPlan = campaign.plan;
}
if ($organizationList.total > 0 && tempOrgId === null) {
tempOrgId = $organizationList.teams[0].$id;
}
});
async function loadPaymentMethods() {
@@ -204,75 +215,87 @@
<WizardSecondaryContainer href={previousPage} bind:showExitModal confirmExit>
<svelte:fragment slot="title">Apply credits</svelte:fragment>
<WizardSecondaryContent>
<Form bind:this={formComponent} onSubmit={handleSubmit} bind:isSubmitting>
{#if $organizationList?.total && !campaign?.onlyNewOrgs && canSelectOrg}
<InputSelect
bind:value={selectedOrgId}
label="Select organization"
{options}
required
placeholder="Select organization"
id="organization" />
{/if}
{#if selectedOrgId && (selectedOrg?.billingPlan !== BillingPlan.PRO || !selectedOrg?.paymentMethodId)}
{#if selectedOrgId === newOrgId}
<InputText
label="Organization name"
placeholder="Enter organization name"
id="name"
required
bind:value={name} />
{/if}
<InputTags
bind:tags={collaborators}
label="Invite members by email"
tooltip="Invited members will have access to all services and payment data within your organization"
placeholder="Enter email address(es)"
validityRegex={emailRegex}
validityMessage="Invalid email address"
id="members" />
<SelectPaymentMethod bind:methods bind:value={paymentMethodId} bind:taxId />
{/if}
</Form>
<Form bind:this={couponForm} onSubmit={addCoupon}>
{#if !data?.couponData?.code && selectedOrgId}
<InputText
required
disabled={!!couponData?.credits}
bind:value={coupon}
placeholder="Enter coupon code"
id="code"
label="Coupon code">
<Button submit secondary disabled={!!couponData?.credits}>
<span class="text">Apply</span>
</Button>
</InputText>
{/if}
</Form>
<svelte:fragment slot="aside">
{#if campaign?.template === 'card'}
<div
class="box card-container u-position-relative"
style:--box-border-radius="var(--border-radius-small)">
<div class="card-bg"></div>
<div class="u-flex u-flex-vertical u-gap-24 u-cross-center u-position-relative">
<img
src={campaign?.image[$app.themeInUse]}
class="u-block u-image-object-fit-cover card-img"
alt="promo" />
<p class="text">
{#if couponData?.credits}
{campaign.title.replace('VALUE', couponData.credits.toString())}
{:else}
{campaign.title}
<Layout.Stack gap="l">
<Form bind:this={formComponent} onSubmit={handleSubmit} bind:isSubmitting>
<Layout.Stack gap="xl">
<Fieldset legend="Setup">
<Layout.Stack gap="l">
{#if $organizationList?.total && !onlyNewOrgs && canSelectOrg}
<InputSelect
bind:value={selectedOrgId}
label="Organization"
{options}
required
placeholder="Select organization"
id="organization" />
{/if}
</p>
</div>
</div>
{/if}
{#if selectedOrgId && (selectedOrg?.billingPlan !== BillingPlan.PRO || !selectedOrg?.paymentMethodId)}
{#if selectedOrgId === newOrgId}
<InputText
label="Organization name"
placeholder="Enter name"
id="name"
required
bind:value={name} />
{/if}
<InputTags
bind:tags={collaborators}
label="Invite members by email"
placeholder="Enter email address(es)"
pattern={emailRegex.toString()}
id="members">
<Tooltip slot="info">
<Icon icon={IconInfo} size="s" />
<span slot="tooltip">
Invited members will have access to all services and
payment data within your organization
</span>
</Tooltip>
</InputTags>
{/if}
</Layout.Stack>
</Fieldset>
{#if (selectedOrgId && (selectedOrg?.billingPlan !== BillingPlan.PRO || !selectedOrg?.paymentMethodId)) || (!data?.couponData?.code && selectedOrgId)}
<Fieldset legend="Billing">
<Layout.Stack gap="xl">
{#if selectedOrgId && (selectedOrg?.billingPlan !== BillingPlan.PRO || !selectedOrg?.paymentMethodId)}
<SelectPaymentMethod
bind:methods
bind:value={paymentMethodId}
bind:taxId />
{/if}
<Form bind:this={couponForm} onSubmit={addCoupon}>
{#if !data?.couponData?.code && selectedOrgId}
<Layout.Stack gap="s" direction="row" alignItems="flex-end">
<InputText
required
disabled={!!couponData?.credits}
bind:value={coupon}
placeholder="Enter code"
id="code"
label="Coupon code">
</InputText>
<Button
submit
secondary
size="s"
disabled={!!couponData?.credits}>
<span class="text">Add</span>
</Button>
</Layout.Stack>
{/if}
</Form>
</Layout.Stack>
</Fieldset>
{/if}
</Layout.Stack>
</Form>
</Layout.Stack>
<svelte:fragment slot="aside">
{#if selectedOrg?.$id && selectedOrg?.billingPlan !== BillingPlan.FREE && selectedOrg?.billingPlan !== BillingPlan.GITHUB_EDUCATION}
<section
class="card u-margin-block-start-24"
class="card"
style:--p-card-padding="1.5rem"
style:--p-card-border-radius="var(--border-radius-small)">
{#if couponData?.code && couponData?.status === 'active'}
@@ -286,7 +309,7 @@
{/if}
</section>
{:else if selectedOrgId}
<div class:u-margin-block-start-24={campaign?.template === 'card'}>
<div>
<EstimatedTotalBox
fixedCoupon={!!data?.couponData?.code}
{billingPlan}
@@ -326,9 +349,9 @@
<span class="loader is-small is-transparent u-line-height-1-5" aria-hidden="true" />
{/if}
{#if selectedOrgId === newOrgId}
Create Organization
Create organization
{:else}
Apply Credits
Apply
{/if}
</Button>
</WizardSecondaryFooter>
@@ -59,7 +59,9 @@
<Icon
icon={IconTable}
size="s"
color="--fgcolor-neutral-weak" />
color={isSelected
? '--fgcolor-neutral-tertiary'
: '--fgcolor-neutral-weak'} />
<span class="text collection-name" data-private
>{collection.name}</span>
</a>
@@ -187,7 +189,7 @@
li:hover {
color: var(--fgcolor-neutral-primary);
border-radius: var(--border-radius-xs, 4px);
background: var(--bgcolor-neutral-tertiary, #fafafb);
background: var(--bgcolor-neutral-secondary);
}
.collection-name {
@@ -40,7 +40,7 @@
required
placeholder="Enter name" />
{#if showCustomId}
<CustomId bind:id bind:show={showCustomId} name="Function" fullWidth />
<CustomId bind:id bind:show={showCustomId} name="Function" />
{:else}
<div>
<Tag size="s" on:click={() => (showCustomId = !showCustomId)}>
@@ -67,7 +67,17 @@
</Layout.Stack>
{#if requestTab === 'parameters'}
{#if parameters?.length}
<Table.Root columns={2} let:root>
<Table.Root
columns={[
{
id: 'key',
width: 200
},
{
id: 'value'
}
]}
let:root>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell {root}>Key</Table.Header.Cell>
<Table.Header.Cell {root}>Value</Table.Header.Cell>
@@ -86,7 +96,17 @@
{/if}
{:else if requestTab === 'headers'}
{#if selectedLog.requestHeaders?.length}
<Table.Root columns={2} let:root>
<Table.Root
columns={[
{
id: 'key',
width: 200
},
{
id: 'value'
}
]}
let:root>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell {root}>Key</Table.Header.Cell>
<Table.Header.Cell {root}>Value</Table.Header.Cell>
@@ -81,7 +81,17 @@
{/if}
{:else if responseTab === 'headers'}
{#if selectedLog.responseHeaders?.length}
<Table.Root columns={2} let:root>
<Table.Root
columns={[
{
id: 'key',
width: 200
},
{
id: 'value'
}
]}
let:root>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell {root}>Key</Table.Header.Cell>
<Table.Header.Cell {root}>Value</Table.Header.Cell>
@@ -2,12 +2,12 @@ import type { Column } from '$lib/helpers/types';
import { writable } from 'svelte/store';
export const columns = writable<Column[]>([
{ id: '$id', title: 'Execution ID', type: 'string', width: 200 },
{ id: '$id', title: 'Execution ID', type: 'string', width: { min: 200, max: 250 } },
{
id: 'requestPath',
title: 'Path',
type: 'string',
width: 90,
width: { min: 100, max: 200 },
format: 'string'
},
@@ -16,7 +16,7 @@ export const columns = writable<Column[]>([
title: 'Trigger',
type: 'string',
hide: true,
width: 90,
width: { min: 90, max: 200 },
array: true,
format: 'enum',
elements: [
@@ -30,7 +30,7 @@ export const columns = writable<Column[]>([
title: 'Method',
type: 'string',
hide: true,
width: 70,
width: { min: 90, max: 200 },
array: true,
format: 'enum',
elements: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
@@ -40,7 +40,7 @@ export const columns = writable<Column[]>([
title: 'Status code',
type: 'integer',
hide: true,
width: 100,
width: { min: 100, max: 200 },
format: 'integer',
elements: [
{
@@ -66,7 +66,7 @@ export const columns = writable<Column[]>([
id: 'status',
title: 'Status',
type: 'enum',
width: 130,
width: { min: 130, max: 200 },
array: true,
format: 'enum',
elements: ['completed', 'failed', 'waiting', 'scheduled', 'processing', 'cancelled']
@@ -75,7 +75,8 @@ export const columns = writable<Column[]>([
id: 'duration',
title: 'Duration',
type: 'integer',
width: 80,
width: { min: 80, max: 200 },
format: 'integer',
elements: [
{
@@ -96,7 +97,7 @@ export const columns = writable<Column[]>([
id: '$createdAt',
title: 'Created',
type: 'datetime',
width: 120,
width: { min: 120, max: 200 },
format: 'datetime',
elements: [
{
@@ -11,9 +11,11 @@
Fieldset,
InlineCode,
Card,
Button
Button,
Input,
Tooltip
} from '@appwrite.io/pink-svelte';
import { Form } from '$lib/elements/forms';
import { Form, InputText } from '$lib/elements/forms';
import {
IconVue,
IconAppwrite,
@@ -44,6 +46,7 @@
AngularFrameworkIcon,
JavascriptFrameworkIcon
} from './components/index';
import { hostnameRegex } from '$lib/helpers/string';
export let key;
@@ -69,6 +72,8 @@ ${prefix}APPWRITE_PUBLIC_ENDPOINT = "${sdk.forProject.client.config.endpoint}"
};
export let platform: PlatformType = PlatformType.Flutterandroid;
export let selectedFrameworkKey: string | undefined = key ? key : undefined;
let hostname;
let hostnameError = false;
let frameworks: Array<FrameworkType> = [
{
@@ -140,6 +145,11 @@ ${prefix}APPWRITE_PUBLIC_ENDPOINT = "${sdk.forProject.client.config.endpoint}"
$: selectedFrameworkIcon = selectedFramework ? selectedFramework.icon : NoFrameworkIcon;
async function createWebPlatform() {
hostnameError = !new RegExp(hostnameRegex).test(hostname);
if (hostnameError) {
return;
}
try {
isCreatingPlatform = true;
await sdk.forConsole.projects.createPlatform(
@@ -148,7 +158,7 @@ ${prefix}APPWRITE_PUBLIC_ENDPOINT = "${sdk.forProject.client.config.endpoint}"
`${selectedFramework.label} app`,
selectedFrameworkKey,
undefined,
undefined
hostname
);
isPlatformCreated = true;
@@ -211,20 +221,38 @@ ${prefix}APPWRITE_PUBLIC_ENDPOINT = "${sdk.forProject.client.config.endpoint}"
imageRadius="s" />
{/each}
</div>
<Layout.Stack direction="row" justifyContent="flex-end">
{#if isChangingFramework}
<Button.Button
disabled={!selectedFramework}
on:click={() => (isChangingFramework = false)}>
Save</Button.Button>
{:else}
<Button.Button type="submit" disabled={!selectedFramework}
>Create platform</Button.Button>
{/if}
</Layout.Stack>
</Layout.Stack>
</Fieldset>
{#if !isChangingFramework}
<Fieldset legend="Details">
<InputText
id="hostname"
label="Hostname"
placeholder="localhost"
error={hostnameError && 'Please enter a valid hostname'}
bind:value={hostname}>
<Tooltip slot="info">
<Icon icon={IconInfo} size="s" />
<span slot="tooltip">
The hostname that your website will use to interact with the
Appwrite APIs in production or development environments. No
protocol or port number required.
</span>
</Tooltip>
</InputText></Fieldset>
<Layout.Stack direction="row" justifyContent="flex-end"
><Button.Button type="submit" disabled={!selectedFramework}
>Create platform</Button.Button
></Layout.Stack>
{/if}
{:else}
<Card.Base padding="s"
><Layout.Stack
@@ -33,6 +33,7 @@
export let hideScrollButtons = false;
export let height = 'auto';
export let fullHeight = false;
export let emptyCopy = 'No logs available';
let { status, buildLogs } = deployment;
@@ -56,6 +57,18 @@
});
return () => unsubscribe();
});
function setCopy() {
if (status === 'failed') {
return 'Your deployment has failed.';
} else if (status === 'building') {
return 'Build is starting.';
} else if (status === 'processing') {
return 'Your deployment is processing.';
} else {
return emptyCopy;
}
}
</script>
<Layout.Stack gap="xl">
@@ -79,7 +92,7 @@
{fullHeight}
{height}
showScrollButton={!hideScrollButtons}
logs={buildLogs || 'No logs available'}
logs={buildLogs || setCopy()}
bind:theme={$app.themeInUse} />
{/key}
</Layout.Stack>
@@ -22,6 +22,10 @@
import { columns } from './store';
import { View } from '$lib/helpers/load';
import Table from './table.svelte';
import { onMount } from 'svelte';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { sdk } from '$lib/stores/sdk';
export let data;
let show = false;
@@ -41,6 +45,14 @@
]);
$updateCommandGroupRanks({ sites: 1000 });
onMount(() => {
return sdk.forConsole.client.subscribe('console', (response) => {
if (response.events.includes(`sites.*`)) {
invalidate(Dependencies.SITES);
}
});
});
</script>
<Container>
@@ -60,7 +72,7 @@
</Layout.Stack>
{#if data.siteList.total}
{#if data.view === View.Grid}
<Grid deployments={data.deployments} siteList={data.siteList} />
<Grid siteList={data.siteList} />
{:else}
<Table siteList={data.siteList} />
{/if}
@@ -17,35 +17,11 @@ export const load = async ({ url, depends, route }) => {
search || undefined
);
// TODO: @Meldiron Discuss better solution for getting screnshot IDs
const deployments: Models.Deployment[] = [];
await Promise.all(
siteList.sites.map(async (site) => {
if (!site.deploymentId) {
return;
}
// TODO: @Meldiron Tripple-check if this can ever happened
// It happened to me but on QA that might have corrupted data
try {
const deployment = await sdk.forProject.sites.getDeployment(
site.$id,
site.deploymentId
);
deployments.push(deployment);
} catch (err) {
// Active deployment no longer available (deleted?)
console.warn(err);
}
})
);
return {
offset,
limit,
search,
siteList,
deployments,
view
};
};
@@ -37,6 +37,7 @@
export let frameworks: Models.Framework[];
export let selectedFramework: Models.Framework;
$: frameworkData = frameworks.find((framework) => framework.key === selectedFramework?.key);
$: ssr = frameworkData?.adapters.find((adapter) => adapter.key === 'ssr');
export let variables: Partial<Models.Variable>[] = [];
export let installCommand = '';
@@ -54,9 +55,9 @@
let frameworkId = selectedFramework.key;
$: if (!installCommand || !buildCommand || !outputDirectory) {
installCommand ||= frameworkData?.adapters?.ssr?.installCommand;
buildCommand ||= frameworkData?.adapters?.ssr?.buildCommand;
outputDirectory ||= frameworkData?.adapters?.ssr?.outputDirectory;
installCommand ||= ssr?.installCommand;
buildCommand ||= ssr?.buildCommand;
outputDirectory ||= ssr?.outputDirectory;
}
</script>
@@ -86,15 +87,12 @@
id="installCommand"
label="Install command"
bind:value={installCommand}
placeholder={frameworkData?.adapters?.ssr?.installCommand} />
placeholder={ssr?.installCommand} />
<Button
secondary
size="s"
disabled={frameworkData?.adapters?.ssr?.installCommand ===
installCommand}
on:click={() =>
(installCommand =
frameworkData?.adapters?.ssr?.installCommand)}>
disabled={ssr?.installCommand === installCommand}
on:click={() => (installCommand = ssr?.installCommand)}>
Reset
</Button>
</Layout.Stack>
@@ -103,14 +101,12 @@
id="buildCommand"
label="Build command"
bind:value={buildCommand}
placeholder={frameworkData?.adapters?.ssr?.buildCommand} />
placeholder={ssr?.buildCommand} />
<Button
secondary
size="s"
disabled={frameworkData?.adapters?.ssr?.buildCommand ===
buildCommand}
on:click={() =>
(buildCommand = frameworkData?.adapters?.ssr?.buildCommand)}>
disabled={ssr?.buildCommand === buildCommand}
on:click={() => (buildCommand = ssr?.buildCommand)}>
Reset
</Button>
</Layout.Stack>
@@ -119,15 +115,12 @@
id="outputDirectory"
label="Output directory"
bind:value={outputDirectory}
placeholder={frameworkData?.adapters?.ssr?.outputDirectory} />
placeholder={ssr?.outputDirectory} />
<Button
secondary
size="s"
disabled={frameworkData?.adapters?.ssr?.outputDirectory ===
outputDirectory}
on:click={() =>
(outputDirectory =
frameworkData?.adapters?.ssr?.outputDirectory)}>
disabled={ssr?.outputDirectory === outputDirectory}
on:click={() => (outputDirectory = ssr?.outputDirectory)}>
Reset
</Button>
</Layout.Stack>
@@ -39,7 +39,8 @@
bind:deployment={data.deployment}
bind:site={data.site}
hideScrollButtons
height="calc(100dvh - 430px)" />
height="calc(100dvh - 430px)"
emptyCopy="No logs available yet..." />
</Fieldset>
</Layout.Stack>
<svelte:fragment slot="aside">
@@ -17,8 +17,6 @@
}[] = [];
let showCustomId = false;
$: console.log(options);
</script>
<Fieldset legend="Details">
@@ -32,7 +30,7 @@
required
placeholder="Enter name" />
{#if showCustomId}
<CustomId bind:id bind:show={showCustomId} name="Site" fullWidth />
<CustomId bind:id bind:show={showCustomId} name="Site" />
{:else}
<div>
<Tag size="s" on:click={() => (showCustomId = !showCustomId)}>
@@ -1,83 +1,74 @@
<script lang="ts">
import Card from '$lib/components/card.svelte';
import { Button, InputText } from '$lib/elements/forms';
import { InputText } from '$lib/elements/forms';
import Button from '$lib/elements/forms/button.svelte';
import { debounce } from '$lib/helpers/debounce';
import { sdk } from '$lib/stores/sdk';
import { consoleVariables } from '$routes/(console)/store';
import { ConsoleResourceType } from '@appwrite.io/console';
import { Fieldset, Layout, Divider, Status, Typography } from '@appwrite.io/pink-svelte';
import { Fieldset, Layout, Status, Typography } from '@appwrite.io/pink-svelte';
import { onMount } from 'svelte';
export let domain: string;
export let domainIsValid = true;
const orginalDomain = domain.split('').join('');
let showConfig = false;
const originalDomain = domain;
let newDomain = domain;
let domainStatus: 'complete' | 'failed' | 'pending' = 'complete';
onMount(async () => {});
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_SITES}`,
ConsoleResourceType.Rules
);
domainStatus = 'complete';
domainIsValid = true;
} catch {
domainStatus = 'failed';
domainIsValid = false;
} finally {
domain = newDomain;
}
}, 300);
}, 500);
$: if (domain) {
checkDomain(domain);
$: if (domain !== newDomain) {
domainStatus = 'pending';
checkDomain(newDomain);
}
</script>
{#if showConfig}
<Fieldset legend="Domains">
<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_SITES}
</Typography.Text>
</svelte:fragment>
</InputText>
<Status
status={domainIsValid ? 'complete' : 'failed'}
label={domainIsValid ? 'Domain is available' : 'Domain is not available'}>
</Status>
</Layout.Stack>
<Divider />
<Layout.Stack direction="row" justifyContent="flex-end" gap="s">
<Button
text
on:click={() => {
domain = orginalDomain;
showConfig = false;
}}>Cancel</Button>
<Button secondary disabled={!domainIsValid} on:click={() => (showConfig = false)}>
Update
</Button>
</Layout.Stack>
</Layout.Stack>
</Fieldset>
{:else}
<Card padding="s" radius="s">
<Layout.Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography.Text variant="m-400" color="--fgcolor-neutral-primary">
<Layout.Stack direction="row" gap="s" alignItems="center">
<span class="icon-globe-alt"></span>
{domain}.{$consoleVariables._APP_DOMAIN_SITES}
</Layout.Stack>
</Typography.Text>
<Fieldset legend="Domains">
<Layout.Stack gap="s">
<Layout.Stack gap="s" direction="row" alignItems="center">
<InputText id="domain" placeholder="my-domain" bind:value={newDomain}>
<svelte:fragment slot="end">
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
.{$consoleVariables._APP_DOMAIN_SITES}
</Typography.Text>
</svelte:fragment>
</InputText>
<Button
size="s"
secondary
on:click={() => {
showConfig = true;
}}>
Update
</Button>
disabled={originalDomain === newDomain}
on:click={() => (newDomain = originalDomain)}>Reset</Button>
</Layout.Stack>
</Card>
{/if}
<Status status={domainStatus} label={setDomainLabel(domainStatus)}></Status>
</Layout.Stack>
</Fieldset>
@@ -42,7 +42,7 @@
async function create() {
try {
domain = await buildVerboseDomain(name, $project.name, $organization.name, id);
domain = await buildVerboseDomain(name, $organization.name, $project.name, id);
const fr = Object.values(Framework).find((f) => f === framework.key);
const buildRuntime = Object.values(BuildRuntime).find(
@@ -125,13 +125,10 @@
<title>Create site - Appwrite</title>
</svelte:head>
<!-- TODO: re enable aside -->
<Wizard
title="Create site"
bind:showExitModal
href={`${base}/project-${$page.params.project}/sites/`}
column
columnSize="s"
confirmExit>
<Form bind:this={formComponent} onSubmit={create} bind:isSubmitting>
<Layout.Stack gap="xl">
@@ -184,7 +181,7 @@
</Layout.Stack>
</Form>
<svelte:fragment slot="aside">
<Aside />
<Aside template={data.template} />
</svelte:fragment>
<svelte:fragment slot="footer">
@@ -1,7 +1,10 @@
import { sdk } from '$lib/stores/sdk';
export const load = async ({ parent }) => {
const { frameworks } = await parent();
return {
frameworks
frameworks,
template: await sdk.forProject.sites.getTemplate('starter-for-js')
};
};
@@ -1,30 +1,35 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Card } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { app } from '$lib/stores/app';
import { IconDownload } from '@appwrite.io/pink-icons-svelte';
import { Icon, Image, Layout, Typography } from '@appwrite.io/pink-svelte';
import type { Models } from '@appwrite.io/console';
import { Image, Layout, Typography } from '@appwrite.io/pink-svelte';
export let template: Models.TemplateSite;
</script>
<Card padding="xs">
<Layout.Stack gap="xxs">
<Typography.Text variant="m-500">Don't have a site yet?</Typography.Text>
<Typography.Text variant="m-500" color="--fgcolor-neutral-primary"
>Don't have a site yet?</Typography.Text>
<Typography.Text variant="m-400">
Download our starter kit, and deploy in seconds.
Deploy the JavaScript starter template in seconds.
</Typography.Text>
</Layout.Stack>
<Layout.Stack>
<Image
ratio="16/9"
src={$app.themeInUse === 'dark'
? `${base}/images/sites/screenshot-placeholder-dark.svg`
: `${base}/images/sites/screenshot-placeholder-light.svg`}
src={$app.themeInUse === 'dark' ? template.screenshotDark : template.screenshotLight}
alt="preview" />
<Button fullWidth secondary href="#">
<Icon icon={IconDownload} slot="start" /> Starter kit
<Button
fullWidth
secondary
href={`${base}/project-${$page.params.project}/sites/create-site/templates/template-${template.key}`}>
Deploy
</Button>
</Layout.Stack>
</Card>
@@ -26,8 +26,7 @@
import { onMount } from 'svelte';
import Configuration from '../../configuration.svelte';
import { consoleVariables } from '$routes/(console)/store';
import { buildVerboseDomain } from '../../store';
import { project } from '$routes/(console)/project-[project]/store';
import Domain from '../../domain.svelte';
export let data;
let showExitModal = false;
@@ -46,7 +45,8 @@
let outputDirectory = adapter?.outputDirectory;
let variables: Partial<Models.Variable>[] = [];
let silentMode = false;
let domain = id;
let domain = data.domain;
let domainIsValid = true;
onMount(async () => {
installation.set(data.installation);
@@ -76,18 +76,18 @@
} catch (error) {
framework = data.frameworks.frameworks.find((f) => f.key === 'other');
trackError(error, Submit.FrameworkDetect);
console.log(error);
}
}
async function create() {
if (!domainIsValid) {
addNotification({
type: 'error',
message: 'Domain is not valid'
});
return;
}
try {
domain = await buildVerboseDomain(
data.repository.name,
data.repository.organization,
$project.name,
id
);
const fr = Object.values(Framework).find((f) => f === framework.key);
const buildRuntime = Object.values(BuildRuntime).find(
(f) => f === framework.buildRuntime
@@ -151,8 +151,6 @@
trackError(e, Submit.SiteCreate);
}
}
$: console.log(framework);
</script>
<svelte:head>
@@ -204,6 +202,8 @@
bind:variables
frameworks={data.frameworks.frameworks} />
{/key}
<Domain bind:domain bind:domainIsValid />
</Layout.Stack>
</Form>
<svelte:fragment slot="aside">
@@ -1,18 +1,28 @@
import { sdk } from '$lib/stores/sdk';
import { ID } from '@appwrite.io/console';
import { buildVerboseDomain } from '../../store';
export const load = async ({ parent, params, url }) => {
const { installations, frameworks } = await parent();
const { installations, frameworks, project, organization, consoleVariables } = await parent();
const [repository] = await Promise.all([
sdk.forProject.vcs.getRepository(url.searchParams.get('installation'), params.repository)
]);
const domain = await buildVerboseDomain(
consoleVariables._APP_DOMAIN_SITES,
repository.name,
organization.name,
project.name,
ID.unique()
);
return {
installations,
installation: installations.installations.find(
(installation) => installation.$id === url.searchParams.get('installation')
),
repository,
frameworks
frameworks,
domain
};
};
@@ -1,18 +1,13 @@
import { sdk } from '$lib/stores/sdk';
import { consoleVariables } from '$routes/(console)/store';
import { ConsoleResourceType, ID } from '@appwrite.io/console';
import { get } from 'svelte/store';
function toURLSafe(s: string) {
return s.replace(/[^a-zA-Z0-9-]/g, '-');
}
export async function checkDomain(domain: string) {
export async function checkDomain(domain: string, apex: string) {
try {
await sdk.forConsole.console.getResource(
`${domain}.${get(consoleVariables)._APP_DOMAIN_SITES}`,
ConsoleResourceType.Rules
);
await sdk.forConsole.console.getResource(`${domain}.${apex}`, ConsoleResourceType.Rules);
return true;
} catch {
return false;
@@ -20,6 +15,7 @@ export async function checkDomain(domain: string) {
}
export async function buildVerboseDomain(
apex: string,
name: string,
specifier: string,
secondarySpecifier?: string,
@@ -32,21 +28,21 @@ export async function buildVerboseDomain(
: '';
const safeUnique = unique ? toURLSafe(unique).toLowerCase() : ID.unique();
let domain = `${safeName}`;
if (await checkDomain(domain)) {
if (await checkDomain(domain, apex)) {
return domain;
}
domain += '-' + safeSpecifier;
if (await checkDomain(domain)) {
if (await checkDomain(domain, apex)) {
return domain;
}
if (secondarySpecifier) {
domain += '-' + safeSecondarySpecifier;
if (await checkDomain(domain)) {
if (await checkDomain(domain, apex)) {
return domain;
}
}
domain += '-' + safeUnique;
if (await checkDomain(domain)) {
if (await checkDomain(domain, apex)) {
return domain;
}
}
@@ -15,8 +15,6 @@ export const load = async ({ url, route }) => {
const siteTemplatesList = await sdk.forProject.sites.listTemplates(undefined, undefined, 100);
console.log(siteTemplatesList);
const [frameworksSet, useCasesSet] = siteTemplatesList.templates.reduce(
([fr, uc], next) => {
next.useCases.forEach((useCase) => uc.add(useCase));
@@ -35,10 +35,8 @@
import { getFrameworkIcon } from '../../../store';
import { app, iconPath } from '$lib/stores/app';
import { consoleVariables } from '$routes/(console)/store';
import { project } from '$routes/(console)/project-[project]/store';
import { buildVerboseDomain } from '../../store';
import { organization } from '$lib/stores/organization';
import { connectGitHub } from '$lib/stores/git';
import Domain from '../../domain.svelte';
export let data;
@@ -51,7 +49,8 @@
let name = data.template.name;
let id = ID.unique();
let domain = id;
let domain = data.domain;
let domainIsValid = true;
let framework = data?.template?.frameworks[0];
let branch = 'main';
let rootDir = './';
@@ -100,15 +99,14 @@
message: 'Please select a repository'
});
return;
} else if (!domainIsValid) {
addNotification({
type: 'error',
message: 'Domain is not valid'
});
return;
} else {
try {
domain = await buildVerboseDomain(
data.template.name,
$project.name,
$organization.name,
id
);
const fr = Object.values(Framework).find((f) => f === framework.key);
const buildRuntime = Object.values(BuildRuntime).find(
(f) => f === framework.buildRuntime
@@ -186,11 +184,6 @@
$: if (connectBehaviour === 'later') {
selectedRepository = null;
}
$: console.log(repositoryName);
$: console.log(data.template);
$: console.log(variables);
</script>
<svelte:head>
@@ -319,6 +312,9 @@
<Configuration bind:variables templateVariables={data.template.variables} />
{/if}
{/if}
{#if connectBehaviour === 'later' || selectedRepository}
<Domain bind:domain bind:domainIsValid />
{/if}
</Layout.Stack>
</Form>
<svelte:fragment slot="aside">
@@ -1,12 +1,22 @@
import { sdk } from '$lib/stores/sdk';
import { buildVerboseDomain } from '../../store.js';
import { ID } from '@appwrite.io/console';
export const load = async ({ parent, params }) => {
const { installations, frameworks } = await parent();
const { installations, frameworks, project, organization, consoleVariables } = await parent();
const template = await sdk.forProject.sites.getTemplate(params.template);
const domain = await buildVerboseDomain(
consoleVariables._APP_DOMAIN_SITES,
template.name,
organization.name,
project.name,
ID.unique()
);
return {
installations,
frameworks,
template
template,
domain
};
};
@@ -3,7 +3,7 @@
import { page } from '$app/stores';
import { timeFromNow } from '$lib/helpers/date';
import type { Models } from '@appwrite.io/console';
import { Card, Layout, Tooltip } from '@appwrite.io/pink-svelte';
import { Card, Icon, Layout, Popover, Tooltip, Typography } from '@appwrite.io/pink-svelte';
import { getFrameworkIcon } from './store';
import { SvgIcon } from '$lib/components';
import { app } from '$lib/stores/app';
@@ -11,25 +11,24 @@
import AddCollaboratorModal from './(components)/addCollaboratorModal.svelte';
import SitesActionMenu from './sitesActionMenu.svelte';
import { capitalize } from '$lib/helpers/string';
import { timer } from '$lib/helpers/timeConversion';
import { IconExclamation } from '@appwrite.io/pink-icons-svelte';
import { Link } from '$lib/elements';
export let siteList: Models.SiteList;
export let deployments: Models.Deployment[];
let showAddCollaborator = false;
let selectedSite: Models.Site = null;
function getScreenshot(theme: string, site: Models.Site) {
const deployment =
deployments.find((d) => site.deploymentId && d.$id === site.deploymentId) ?? null;
if (theme === 'dark') {
return deployment?.screenshotDark
? getFilePreview(deployment.screenshotDark)
return site?.deploymentScreenshotDark
? getFilePreview(site?.deploymentScreenshotDark)
: `${base}/images/sites/screenshot-placeholder-dark.svg`;
}
return deployment?.screenshotLight
? getFilePreview(deployment.screenshotLight)
return site?.deploymentScreenshotLight
? getFilePreview(site?.deploymentScreenshotLight)
: `${base}/images/sites/screenshot-placeholder-light.svg`;
}
@@ -38,6 +37,14 @@
const endpoint = getApiEndpoint();
return endpoint + `/storage/buckets/screenshots/files/${fileId}/view?project=console`;
}
function generateDesc(site: Models.Site) {
if (site.latestDeploymentStatus === 'building') {
return `Deployment building ${timer(site.latestDeploymentCreatedAt)}`;
} else {
return `Deployed ${timeFromNow(site.deploymentCreatedAt)}`;
}
}
</script>
<Layout.Grid columns={3} columnsXS={1} columnsXXS={1}>
@@ -47,7 +54,7 @@
padding="xxs">
<Card.Media
title={site.name}
description={`Updated ${timeFromNow(site.$updatedAt)}`}
description={generateDesc(site)}
src={getScreenshot($app.themeInUse, site)}
alt={site.name}
avatar>
@@ -60,6 +67,31 @@
</Tooltip>
</svelte:fragment>
<SitesActionMenu {site} bind:showAddCollaborator bind:selectedSite />
<svelte:fragment slot="description-end">
{#if site.latestDeploymentStatus === 'failed'}
<Popover let:toggle portal>
<button on:mouseenter={(e) => toggle(e)}>
<Layout.Stack alignItems="center">
<Icon
icon={IconExclamation}
size="s"
color="--bgcolor-warning" />
</Layout.Stack>
</button>
<svelte:fragment slot="tooltip">
<Typography.Text variant="m-400">
Last deployment failed {timeFromNow(
site.latestDeploymentCreatedAt
)}. <Link
href={`${base}/project-${$page.params.project}/sites/site-${site.$id}/deployments/deployment-${site.latestDeploymentId}`}>
View logs
</Link>
</Typography.Text>
</svelte:fragment>
</Popover>
{/if}
</svelte:fragment>
</Card.Media>
</Card.Link>
{/each}
@@ -25,8 +25,8 @@
}
if (message.events.includes('sites.*.deployments.*.update')) {
invalidate(Dependencies.DEPLOYMENTS);
invalidate(Dependencies.FUNCTION);
invalidate(Dependencies.SITE);
console.log(message);
return;
}
if (message.events.includes('sites.*.deployments.*.delete')) {
@@ -4,12 +4,12 @@ import type { Column } from '$lib/helpers/types';
export const showCreateDeployment: Writable<boolean> = writable(false);
export const columns = writable<Column[]>([
{ id: '$id', title: 'Deployment ID', type: 'string', width: 200 },
{ id: '$id', title: 'Deployment ID', type: 'string', width: { min: 200, max: 250 } },
{
id: 'status',
title: 'Status',
type: 'enum',
width: 110,
width: { min: 90, max: 130 },
array: true,
format: 'enum',
elements: ['ready', 'processing', 'building', 'waiting', 'cancelled', 'failed']
@@ -19,7 +19,8 @@ export const columns = writable<Column[]>([
id: 'buildDuration',
title: 'Build duration',
type: 'integer',
width: 90,
width: { min: 110, max: 190 },
elements: [
{
value: 15,
@@ -36,12 +37,32 @@ export const columns = writable<Column[]>([
],
filter: false
},
{
id: 'totalSize',
title: 'Total size',
type: 'integer',
width: { min: 110, max: 190 },
elements: [
{
value: 2 * 1000 * 1000,
label: 'more than 2MB'
},
{
value: 10 * 1000 * 1000,
label: 'more than 10MB'
},
{
value: 50 * 1000 * 1000,
label: 'more than 50MB'
}
]
},
{
id: 'sourceSize',
title: 'Source size',
type: 'integer',
hide: true,
width: 140,
width: { min: 110, max: 190 },
elements: [
{
value: 2 * 1000 * 1000,
@@ -61,14 +82,15 @@ export const columns = writable<Column[]>([
id: 'buildSize',
title: 'Build size',
type: 'integer',
hide: true,
filter: false,
width: 80
width: { min: 90, max: 190 }
},
{
id: 'type',
title: 'Source',
type: 'string',
width: 90,
width: { min: 90, max: 160 },
array: true,
format: 'enum',
elements: [
@@ -81,7 +103,8 @@ export const columns = writable<Column[]>([
id: '$updatedAt',
title: 'Updated',
type: 'datetime',
width: 150,
width: { min: 150, max: 180 },
format: 'datetime'
}
]);
@@ -68,6 +68,8 @@
{:else}
{formatTimeDetailed(deployment.buildDuration)}
{/if}
{:else if column.id === 'totalSize'}
{calculateSize(deployment.totalSize)}
{:else if column.id === 'sourceSize'}
{calculateSize(deployment.sourceSize)}
{:else if column.id === 'buildSize'}
@@ -43,7 +43,15 @@
</Button>
</Layout.Stack>
{#if deploymentList?.total}
<Table.Root columns={5} let:root>
<Table.Root
columns={[
{ id: '$id', width: 200 },
{ id: 'status' },
{ id: 'source' },
{ id: 'createdBy' },
{ id: 'actions' }
]}
let:root>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell {root}>Deployment ID</Table.Header.Cell>
<Table.Header.Cell {root}>Status</Table.Header.Cell>
@@ -12,8 +12,6 @@
import Table from './table.svelte';
export let data;
$: console.log(data.domains);
</script>
<Container>
@@ -5,120 +5,77 @@
import { Wizard } from '$lib/layout';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import {
Fieldset,
Layout,
Tooltip,
Icon,
Input,
Divider,
Typography,
Card
} 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, protocol } from '$routes/(console)/store';
import { IconGlobeAlt, IconInfo } from '@appwrite.io/pink-icons-svelte';
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
import { LabelCard } from '$lib/components';
import { isCloud } from '$lib/system';
import { StatusCode } from '@appwrite.io/console';
import VerificationFieldset from './verificationFieldset.svelte';
import type { Domain } from '$lib/sdk/domains';
import { statusCodeOptions } from '$lib/stores/domains';
import ConnectRepoModal from '../../../(components)/connectRepoModal.svelte';
import { writable } from 'svelte/store';
import { onMount } from 'svelte';
const backPage = `${base}/project-${$page.params.project}/sites/site-${$page.params.site}/domains`;
const routeBase = `${base}/project-${$page.params.project}/sites/site-${$page.params.site}/domains`;
export let data;
let formComponent: Form;
let isSubmitting = writable(false);
let showConnectRepo = false;
let behaviour: 'REDIRECT' | 'BRANCH' | 'ACTIVE' = 'ACTIVE';
let step: 'add' | 'verify' = 'add';
let domainName = '';
let redirect: string = null;
let statusCode: number = null;
let branch = null;
let domainData: Domain;
let selectedTab: 'cname' | 'nameserver';
const redirectOptions = data.domains.rules
const redirectOptions = data.rules.rules
.filter((d) => !d.domain.endsWith($consoleVariables._APP_DOMAIN_SITES))
.map((domain) => ({
label: domain.domain,
value: domain.domain
}));
async function addDomain() {
const isNewDomain =
data.domains.rules.findIndex((rule) => rule.domain === domainName) === -1;
try {
if (isNewDomain && isCloud) {
domainData = await sdk.forConsole.domains.create($organization.$id, domainName);
}
onMount(() => {
if (
$page.url.searchParams.has('connectRepo') &&
$page.url.searchParams.get('connectRepo') === 'true'
) {
showConnectRepo = true;
}
});
async function addDomain() {
try {
if (behaviour === 'BRANCH') {
await sdk.forProject.proxy.createSiteRule(domainName, $page.params.site, branch);
} else if (behaviour === 'REDIRECT') {
const sc = Object.values(StatusCode).find((code) => parseInt(code) === statusCode);
console.log(statusCode, sc);
await sdk.forProject.proxy.createRedirectRule(domainName, $protocol + redirect, sc);
} else if (behaviour === 'ACTIVE') {
await sdk.forProject.proxy.createSiteRule(domainName, $page.params.site);
}
addNotification({
type: 'success',
message: 'Domain added successfully'
});
await goto(backPage);
await invalidate(Dependencies.DOMAINS);
await goto(`${routeBase}/add-domain/verify-${domainName}`);
await invalidate(Dependencies.SITES_DOMAINS);
} catch (error) {
await invalidate(Dependencies.DOMAINS);
addNotification({
type: 'error',
message: error.message
});
}
}
$: isVerified = domainData?.nameservers
? domainData?.nameservers.toLocaleLowerCase() === 'appwrite'
: undefined;
</script>
<Wizard title="Add custom domain" href={backPage} column columnSize="s" hideFooter={step === 'add'}>
<Form onSubmit={() => (step = 'verify')}>
{#if step === 'add'}
<Layout.Stack gap="xxl">
<Layout.Grid columns={3} columnsXS={1}>
<LabelCard value="ACTIVE" bind:group={behaviour} title="Active deployment">
Point this domain to the latest deployed version.
</LabelCard>
<Tooltip disabled={!!data.site?.providerRepositoryId}>
<div>
<LabelCard
value="BRANCH"
bind:group={behaviour}
title="Branch"
disabled={!data.site?.providerRepositoryId}>
Point this domain to a specific branch in your repository.
</LabelCard>
</div>
<svelte:fragment slot="tooltip">
No repository connected to your site.
</svelte:fragment>
</Tooltip>
<LabelCard value="REDIRECT" bind:group={behaviour} title="Redirect">
Forward all traffic from this domain to another URL.
</LabelCard>
</Layout.Grid>
<Fieldset legend="Settings">
<Layout.Stack gap="xl">
<!-- <Input.ComboBox
<Wizard title="Add domain" href={routeBase} column columnSize="s" confirmExit>
<Form bind:this={formComponent} onSubmit={addDomain} bind:isSubmitting>
<Layout.Stack gap="xxl">
<Fieldset legend="Domain">
<!-- <Input.ComboBox
label="Domain"
id="domain"
name="domain"
@@ -129,93 +86,118 @@
}))}
required
placeholder="appwrite.example.com" /> -->
<InputDomain
label="Domain"
id="domain"
bind:value={domainName}
required
placeholder="appwrite.example.com" />
<InputDomain
label="Domain"
id="domain"
bind:value={domainName}
required
placeholder="appwrite.example.com" />
</Fieldset>
{#if behaviour === 'BRANCH'}
{#if data.site?.providerRepositoryId}
{@const sortedBranches = sortBranches(data.branches.branches)}
{@const options = sortedBranches.map((branch) => ({
label: branch.name,
value: branch.name
}))}
<Layout.Stack gap="s">
<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>
{/if}
{:else if behaviour === 'REDIRECT'}
<Layout.Grid columns={3} columnsXS={1}>
<LabelCard value="ACTIVE" bind:group={behaviour} title="Active deployment">
Point this domain to the latest deployed version.
</LabelCard>
<LabelCard value="BRANCH" bind:group={behaviour} title="Git branch">
Point this domain to a specific branch in your repository.
</LabelCard>
<LabelCard value="REDIRECT" bind:group={behaviour} title="Redirect">
Forward all traffic from this domain to another URL.
</LabelCard>
</Layout.Grid>
{#if behaviour === 'BRANCH'}
<Fieldset legend="Settings">
<Layout.Stack gap="xl">
{#if data.site?.providerRepositoryId}
{@const sortedBranches = sortBranches(data.branches.branches)}
{@const options = sortedBranches.map((branch) => ({
label: branch.name,
value: branch.name
}))}
<Layout.Stack gap="s">
<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
label="Redirect to"
id="redirect"
placeholder="Select domain"
options={redirectOptions}
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>
</Tooltip>
</InputSelect>
<InputSelect
options={statusCodeOptions}
label="Status code"
id="code"
disabled
options={[{ label: 'main', value: 'main' }]}
label="Production branch"
id="branch"
required
bind:value={statusCode}
placeholder="Select status code" />
value="main"
placeholder="Select branch" />
<Alert.Inline title=" There is no repository connected to your site">
<Layout.Stack>
<p>
The domain will be connected to your active deployment.
Connect your Git repository to link a production branch.
</p>
<div>
<Button compact on:click={() => (showConnectRepo = true)}>
Connect repository
</Button>
</div>
</Layout.Stack>
</Alert.Inline>
{/if}
<Divider />
<Layout.Stack direction="row" justifyContent="flex-end">
<Button submit>Add</Button>
</Layout.Stack>
</Layout.Stack>
</Fieldset>
</Layout.Stack>
{:else if step === 'verify'}
<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">
{domainName}
</Typography.Text>
</Layout.Stack>
<Button secondary on:click={() => (step = 'add')}>Change</Button>
{:else if behaviour === 'REDIRECT'}
<Fieldset legend="Settings">
<Layout.Stack gap="xl">
<InputSelect
label="Redirect to"
id="redirect"
placeholder="Select domain"
options={redirectOptions}
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>
</Tooltip>
</InputSelect>
<InputSelect
options={statusCodeOptions}
label="Status code"
id="code"
required
bind:value={statusCode}
placeholder="Select status code" />
</Layout.Stack>
</Card.Base>
<VerificationFieldset domain={domainName} verified={isVerified} bind:selectedTab />
</Layout.Stack>
{/if}
</Fieldset>
{/if}
</Layout.Stack>
</Form>
<svelte:fragment slot="footer">
<Button secondary href={backPage}>Cancel</Button>
<Button on:click={addDomain}>Verify</Button>
<Button secondary href={routeBase}>Cancel</Button>
<Button on:click={() => formComponent.triggerSubmit()} bind:disabled={$isSubmitting}>
Add
</Button>
</svelte:fragment>
</Wizard>
{#if showConnectRepo}
<ConnectRepoModal
bind:show={showConnectRepo}
site={data.site}
onlyExisting
callbackState={{ connectRepo: 'true' }} />
{/if}
@@ -5,9 +5,9 @@ import { Dependencies } from '$lib/constants.js';
export const load = async ({ parent, depends }) => {
const { site } = await parent();
depends(Dependencies.DOMAINS);
depends(Dependencies.DOMAINS, Dependencies.SITES_DOMAINS);
const [domains, installations] = await Promise.all([
const [rules, installations] = await Promise.all([
sdk.forProject.proxy.listRules([
Query.equal('type', RuleType.DEPLOYMENT),
Query.equal('trigger', RuleTrigger.MANUAL)
@@ -17,7 +17,7 @@ export const load = async ({ parent, depends }) => {
return {
site,
domains,
rules,
installations,
branches:
site?.installationId && site?.providerRepositoryId
@@ -0,0 +1,92 @@
<script lang="ts">
import { IconGlobeAlt } from '@appwrite.io/pink-icons-svelte';
import { Card, Divider, Icon, Layout, Typography } from '@appwrite.io/pink-svelte';
import VerificationFieldset from './verificationFieldset.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/stores';
import Wizard from '$lib/layout/wizard.svelte';
import { base } from '$app/paths';
import { writable } from 'svelte/store';
export let data;
let selectedTab: 'cname' | 'nameserver';
let domainData = data.domain;
let routeBase = `${base}/project-${$page.params.project}/sites/site-${$page.params.site}/domains`;
let formComponent: Form;
let isSubmitting = writable(false);
async function addDomain() {
const isNewDomain =
data.domainsList.domains.findIndex((rule) => rule.domain === $page.params.domain) ===
-1;
try {
if (isNewDomain && isCloud) {
domainData = await sdk.forConsole.domains.create(
$organization.$id,
$page.params.domain
);
}
addNotification({
type: 'success',
message: 'Domain added successfully'
});
await goto(routeBase);
await invalidate(Dependencies.DOMAINS);
await invalidate(Dependencies.SITES_DOMAINS);
} catch (error) {
await invalidate(Dependencies.DOMAINS);
addNotification({
type: 'error',
message: error.message
});
}
}
async function back() {}
$: isVerified = domainData?.nameservers
? domainData?.nameservers.toLocaleLowerCase() === 'appwrite'
: undefined;
</script>
<Wizard title="Add domain" href={routeBase} column columnSize="s">
<Form bind:this={formComponent} onSubmit={addDomain} 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>
<VerificationFieldset
domain={$page.params.domain}
verified={isVerified}
bind:selectedTab />
<Divider />
<Layout.Stack>
<Button submit disabled={$isSubmitting}>Verify</Button>
</Layout.Stack>
</Layout.Stack>
</Form>
</Wizard>
@@ -0,0 +1,24 @@
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 }) => {
const { site } = await parent();
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 {
site,
domain,
domainsList
};
};
@@ -1,53 +0,0 @@
<script lang="ts">
import { Copy } from '$lib/components';
import { Link } from '$lib/elements';
import type { Domain } from '$lib/sdk/domains';
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
import { Badge, Layout, Typography, Table, Fieldset, Icon } from '@appwrite.io/pink-svelte';
import type { Models } from '@appwrite.io/console';
export let domain: Domain | Models.ProxyRule;
</script>
<Fieldset legend="Verification">
<Layout.Stack gap="xl">
<Layout.Stack gap="xl">
<Layout.Stack gap="s">
<Layout.Stack gap="s" direction="row" alignItems="center">
<Typography.Text variant="l-500">{domain?.domain}</Typography.Text>
<Badge variant="secondary" type="warning" 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={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}>{domain?.domain}</Table.Cell>
<Table.Cell {root}>
<Copy value={globalThis?.location?.origin}
>{globalThis?.location?.origin}</Copy>
</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"
href="#">here</Link
>.
</Typography.Text>
</Layout.Stack>
</Layout.Stack>
<slot />
</Layout.Stack>
</Fieldset>
@@ -6,12 +6,11 @@
import { invalidate } from '$app/navigation';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { Dependencies } from '$lib/constants';
import type { Domain } from '$lib/sdk/domains';
import type { Models } from '@appwrite.io/console';
import CnameTable from '$lib/components/domains/cnameTable.svelte';
export let show = false;
export let selectedDomain: Domain | Models.ProxyRule;
export let selectedDomain: Models.ProxyRule;
let error = null;
async function retryDomain() {
@@ -7,19 +7,19 @@ export const columns = writable<Column[]>([
title: 'Domain',
type: 'string',
format: 'string',
width: { min: 120, max: 400 }
width: { min: 200, max: 550 }
},
{
id: 'redirectUrl',
title: 'Redirect to',
type: 'string',
width: { min: 120, max: 200 }
width: { min: 120, max: 400 }
},
{
id: 'deploymentVcsProviderBranch',
title: 'Production branch',
type: 'string',
width: { min: 70, max: 150 }
width: { min: 90, max: 220 }
}
]);
@@ -27,7 +27,7 @@
<Typography.Text variant="l-500" color="--fgcolor-neutral-primary">
Domains
</Typography.Text>
<Badge variant="secondary" content={proxyRuleList?.total.toString()} size="s" />
<Badge variant="secondary" content={(rules?.length ?? 0).toString()} size="s" />
</Layout.Stack>
<Button
secondary
@@ -86,7 +86,17 @@
{/if}
{:else if requestTab === 'headers'}
{#if selectedLog.requestHeaders?.length}
<Table.Root columns={2} let:root>
<Table.Root
columns={[
{
id: 'key',
width: 200
},
{
id: 'value'
}
]}
let:root>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell {root}>Key</Table.Header.Cell>
<Table.Header.Cell {root}>Value</Table.Header.Cell>
@@ -73,7 +73,17 @@
{/if}
{:else if responseTab === 'headers'}
{#if selectedLog.responseHeaders?.length}
<Table.Root columns={2} let:root>
<Table.Root
columns={[
{
id: 'key',
width: 200
},
{
id: 'value'
}
]}
let:root>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell {root}>Key</Table.Header.Cell>
<Table.Header.Cell {root}>Value</Table.Header.Cell>
@@ -2,12 +2,12 @@ import type { Column } from '$lib/helpers/types';
import { writable } from 'svelte/store';
export const columns = writable<Column[]>([
{ id: '$id', title: 'Log ID', type: 'string', width: 200 },
{ id: '$id', title: 'Log ID', type: 'string', width: { min: 200, max: 250 } },
{
id: 'requestPath',
title: 'Path',
type: 'string',
width: 90,
width: { min: 500, max: 900 },
format: 'string'
},
@@ -15,7 +15,7 @@ export const columns = writable<Column[]>([
id: 'trigger',
title: 'Trigger',
type: 'string',
hide: true,
exclude: true,
width: 90,
array: true,
format: 'enum',
@@ -30,7 +30,7 @@ export const columns = writable<Column[]>([
id: 'requestMethod',
title: 'Method',
type: 'string',
hide: true,
exclude: true,
width: 70,
array: true,
format: 'enum',
@@ -40,7 +40,7 @@ export const columns = writable<Column[]>([
id: 'responseStatusCode',
title: 'Status code',
type: 'integer',
hide: true,
exclude: true,
width: 100,
format: 'integer',
elements: [
@@ -84,13 +84,14 @@ export const columns = writable<Column[]>([
label: 'more than 30 seconds'
}
],
filter: false
filter: false,
exclude: true
},
{
id: '$createdAt',
title: 'Created',
width: { min: 150, max: 180 },
type: 'datetime',
width: 120,
format: 'datetime',
elements: [
{
@@ -15,7 +15,7 @@
<Table.Root {columns} let:root>
<svelte:fragment slot="header" let:root>
{#each columns as { id, title }}
{#each columns.filter((c) => !c.exclude) as { id, title }}
<Table.Header.Cell column={id} {root}>{title}</Table.Header.Cell>
{/each}
</svelte:fragment>
@@ -26,7 +26,7 @@
openSheet = true;
selectedLogId = log.$id;
}}>
{#each columns as column}
{#each columns.filter((c) => !c.exclude) as column}
<Table.Cell column={column.id} {root}>
{#if column.id === '$id'}
{#key column.id}
@@ -38,10 +38,12 @@
{log.responseStatusCode}
{:else if column.id === 'requestPath'}
<Layout.Stack direction="row" alignItems="center" gap="s">
<Badge
variant="secondary"
type={log.responseStatusCode >= 400 ? 'error' : 'success'}
content={log.responseStatusCode.toString()} />
<div>
<Badge
variant="secondary"
type={log.responseStatusCode >= 400 ? 'error' : 'success'}
content={log.responseStatusCode.toString()} />
</div>
<Typography.Code size="m">
{log.requestMethod}
</Typography.Code>
@@ -52,8 +52,6 @@
showConnectRepo.set(true);
}
});
$: console.log(data);
</script>
<Container>
@@ -28,15 +28,12 @@
let fallback = site.fallbackFile;
let isButtonDisabled = true;
let showFallback = site.adapter === Adapter.Static;
$: frameworkAdapterData = selectedFramework.adapters.find((a) => a.key === adapter);
onMount(async () => {
installCommand =
site?.installCommand ?? selectedFramework.adapters[site.adapter].defaultInstallCommand;
buildCommand =
site?.buildCommand ?? selectedFramework.adapters[site.adapter].defaultBuildCommand;
outputDirectory =
site?.outputDirectory ??
selectedFramework.adapters[site.adapter].defaultOutputDirectory;
installCommand = site?.installCommand ?? frameworkAdapterData?.installCommand;
buildCommand = site?.buildCommand ?? frameworkAdapterData?.buildCommand;
outputDirectory = site?.outputDirectory ?? frameworkAdapterData?.outputDirectory;
});
async function updateName() {
@@ -89,8 +86,7 @@
$: if (adapter === Adapter.Static) {
showFallback = true;
fallback ||= selectedFramework.adapters.static.fallbackFile;
console.log(fallback, selectedFramework.adapters.static);
fallback ||= selectedFramework.adapters.find((a) => a.key === Adapter.Static).fallbackFile;
} else {
showFallback = false;
fallback = undefined;
@@ -100,15 +96,12 @@
fallback = null;
}
$: frameworkDataAdapter = selectedFramework.adapters?.length
? selectedFramework.adapters[site.adapter]
: frameworks[0].adapters[site.adapter];
//TODO: fix after backend type is fixed
$: if (selectedFramework?.adapters?.length <= 1 || !selectedFramework?.adapters?.ssr?.key) {
$: hasSSR = selectedFramework?.adapters?.some((a) => a?.key === Adapter.Ssr);
$: hasStatic = selectedFramework?.adapters?.some((a) => a?.key === Adapter.Static);
$: if (selectedFramework?.adapters?.length <= 1 || !hasSSR) {
adapter = Adapter.Static;
}
$: if (selectedFramework?.adapters?.length <= 1 || !selectedFramework?.adapters?.static?.key) {
$: if (selectedFramework?.adapters?.length <= 1 || !hasStatic) {
adapter = Adapter.Ssr;
}
</script>
@@ -139,7 +132,7 @@
(framework) => framework.key === frameworkKey
);
}} />
{#if selectedFramework.adapters?.length || (selectedFramework.adapters?.ssr?.key && selectedFramework.adapters?.static?.key)}
{#if selectedFramework.adapters?.length > 2}
<Layout.Grid columnsXS={1} columns={2} gap="l">
<Card.Selector
title="Server side rendering"
@@ -202,7 +195,7 @@
id="installCommand"
label="Install command"
bind:value={installCommand}
placeholder={frameworkDataAdapter?.defaultInstallCommand} />
placeholder={frameworkAdapterData?.installCommand} />
<Button
secondary
@@ -220,7 +213,7 @@
id="buildCommand"
label="Build command"
bind:value={buildCommand}
placeholder={frameworkDataAdapter?.defaultBuildCommand} />
placeholder={frameworkAdapterData?.buildCommand} />
<Button
secondary
size="s"
@@ -237,7 +230,7 @@
id="outputDirectory"
label="Output directory"
bind:value={outputDirectory}
placeholder={frameworkDataAdapter?.defaultOutputDirectory} />
placeholder={frameworkAdapterData?.outputDirectory} />
<Button
secondary
size="s"
@@ -7,7 +7,7 @@
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { BuildRuntime, Framework, type Models } from '@appwrite.io/console';
import { Adapter, BuildRuntime, Framework, type Models } from '@appwrite.io/console';
export let site: Models.Site;
@@ -27,7 +27,7 @@
site.buildCommand || undefined,
site.outputDirectory || undefined,
(site?.buildRuntime as BuildRuntime) || undefined,
site.adapter,
site.adapter as Adapter,
site.fallbackFile || undefined,
site.installationId || undefined,
site.providerRepositoryId || undefined,
@@ -309,7 +309,11 @@
</Alert.Inline>
{/if}
<Table.Root
columns={[{ id: 'key' }, { id: 'value' }, { id: 'actions', width: 30 }]}
columns={[
{ id: 'key', width: { min: 200, max: 400 } },
{ id: 'value', width: { min: 200, max: 400 } },
{ id: 'actions', width: 50 }
]}
let:root>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="key" {root}>Key</Table.Header.Cell>
@@ -22,7 +22,7 @@
try {
disabled = true;
await sdk.forConsole.account.createEmailPasswordSession(mail, pass);
await invalidate(Dependencies.ACCOUNT);
if ($user) {
trackEvent(Submit.AccountLogin, { mfa_used: 'none' });
addNotification({
@@ -30,12 +30,7 @@
message: 'Successfully logged in.'
});
}
if ($redirectTo) {
window.location.href = $redirectTo;
return;
}
await invalidate(Dependencies.ACCOUNT);
if (data?.couponData?.code) {
trackEvent(Submit.AccountCreate, { campaign_name: data?.couponData?.code });
await goto(`${base}/apply-credit?code=${data?.couponData?.code}`);
@@ -45,6 +40,13 @@
await goto(`${base}/apply-credit?campaign=${data.campaign?.$id}`);
return;
}
if ($redirectTo) {
window.location.href = $redirectTo;
return;
}
// no specific redirect, so redirect will happen through invalidating the account
await invalidate(Dependencies.ACCOUNT);
} catch (error) {
disabled = false;
addNotification({