Merge remote-tracking branch 'origin/main' into feat-project-changes

This commit is contained in:
Damodar Lohani
2025-06-17 03:39:07 +00:00
53 changed files with 1505 additions and 990 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
PUBLIC_CONSOLE_MODE=self-hosted
PUBLIC_CONSOLE_FEATURE_FLAGS=
PUBLIC_CONSOLE_FEATURE_FLAGS=sites,csv-import,attribute-encrypt,index-lengths
PUBLIC_APPWRITE_MULTI_REGION=false
PUBLIC_APPWRITE_ENDPOINT=http://localhost/v1
PUBLIC_STRIPE_KEY=
+3 -3
View File
@@ -79,7 +79,7 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
build-args: |
"PUBLIC_CONSOLE_MODE=cloud"
"PUBLIC_CONSOLE_FEATURE_FLAGS=sites,csv-import"
"PUBLIC_CONSOLE_FEATURE_FLAGS=sites,csv-import,attribute-encrypt,index-lengths"
"PUBLIC_APPWRITE_MULTI_REGION=true"
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}"
@@ -118,7 +118,7 @@ jobs:
build-args: |
"PUBLIC_CONSOLE_MODE=self-hosted"
"PUBLIC_APPWRITE_MULTI_REGION=false"
"PUBLIC_CONSOLE_FEATURE_FLAGS=sites,csv-import"
"PUBLIC_CONSOLE_FEATURE_FLAGS=sites,csv-import,attribute-encrypt,index-lengths"
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
publish-cloud-no-regions:
@@ -156,6 +156,6 @@ jobs:
build-args: |
"PUBLIC_CONSOLE_MODE=cloud"
"PUBLIC_APPWRITE_MULTI_REGION=false"
"PUBLIC_CONSOLE_FEATURE_FLAGS=sites,csv-import"
"PUBLIC_CONSOLE_FEATURE_FLAGS=sites,csv-import,attribute-encrypt,index-lengths"
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}"
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
+2 -2
View File
@@ -22,11 +22,11 @@
},
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@a5e5564",
"@appwrite.io/console": "^1.9.0",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "^2.0.0-RC.1",
"@appwrite.io/pink-legacy": "^1.0.3",
"@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@40bae6b",
"@appwrite.io/pink-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@ee1b778",
"@popperjs/core": "^2.11.8",
"@sentry/sveltekit": "^8.38.0",
"@stripe/stripe-js": "^3.5.0",
+19 -19
View File
@@ -12,8 +12,8 @@ importers:
specifier: ^1.1.24
version: 1.1.24(svelte@5.25.3)(zod@3.24.3)
'@appwrite.io/console':
specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@a5e5564
version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@a5e5564
specifier: ^1.9.0
version: 1.9.0
'@appwrite.io/pink-icons':
specifier: 0.25.0
version: 0.25.0
@@ -24,8 +24,8 @@ importers:
specifier: ^1.0.3
version: 1.0.3
'@appwrite.io/pink-svelte':
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@40bae6b
version: https://pkg.vc/-/@appwrite/%40appwrite.io%2Fpink-svelte@40bae6b(svelte@5.25.3)
specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@ee1b778
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@ee1b778(svelte@5.25.3)
'@popperjs/core':
specifier: ^2.11.8
version: 2.11.8
@@ -257,12 +257,12 @@ packages:
'@analytics/type-utils@0.6.2':
resolution: {integrity: sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg==}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@a5e5564':
resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@a5e5564}
version: 1.8.0
'@appwrite.io/console@1.9.0':
resolution: {integrity: sha512-g8+zfdBF8mz7tRUER4CGVe5FHWQVLp8TlY/UOGCZ2TgUF1qnpNu/DhiQgph8uF+QZ9jDKCLAAZlP5//9t7ckWA==}
'@appwrite.io/pink-icons-svelte@2.0.0-RC.1':
resolution: {integrity: sha512-iLFlV55hj8mGuAbmxJGenxN5RaZMmVT4GJb9dv/MP1xBAtYibFq7JvBcxm18qV2KU8c31Rntf+Ub4GL7HwqTYg==}
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@ee1b7788cd3f877c9aa1b6487976bd63a6196870':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@ee1b7788cd3f877c9aa1b6487976bd63a6196870}
version: 2.0.0-RC.1
peerDependencies:
svelte: ^4.0.0
@@ -281,8 +281,8 @@ packages:
'@appwrite.io/pink-legacy@1.0.3':
resolution: {integrity: sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ==}
'@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/%40appwrite.io%2Fpink-svelte@40bae6b':
resolution: {tarball: https://pkg.vc/-/@appwrite/%40appwrite.io%2Fpink-svelte@40bae6b}
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@ee1b778':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@ee1b778}
version: 2.0.0-RC.2
peerDependencies:
svelte: ^4.0.0
@@ -1340,8 +1340,8 @@ packages:
'@types/prop-types@15.7.14':
resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==}
'@types/react@18.3.22':
resolution: {integrity: sha512-vUhG0YmQZ7kL/tmKLrD3g5zXbXXreZXB3pmROW8bg3CnLnpjkRVwUlLne7Ufa2r9yJ8+/6B73RzhAek5TBKh2Q==}
'@types/react@18.3.23':
resolution: {integrity: sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==}
'@types/remarkable@2.0.8':
resolution: {integrity: sha512-eKXqPZfpQl1kOADjdKchHrp2gwn9qMnGXhH/AtZe0UrklzhGJkawJo/Y/D0AlWcdWoWamFNIum8+/nkAISQVGg==}
@@ -3635,9 +3635,9 @@ snapshots:
'@analytics/type-utils@0.6.2': {}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@a5e5564': {}
'@appwrite.io/console@1.9.0': {}
'@appwrite.io/pink-icons-svelte@2.0.0-RC.1(svelte@5.25.3)':
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@ee1b7788cd3f877c9aa1b6487976bd63a6196870(svelte@5.25.3)':
dependencies:
svelte: 5.25.3
@@ -3654,9 +3654,9 @@ snapshots:
'@appwrite.io/pink-icons': 1.0.0
the-new-css-reset: 1.11.3
'@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/%40appwrite.io%2Fpink-svelte@40bae6b(svelte@5.25.3)':
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@ee1b778(svelte@5.25.3)':
dependencies:
'@appwrite.io/pink-icons-svelte': 2.0.0-RC.1(svelte@5.25.3)
'@appwrite.io/pink-icons-svelte': https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@ee1b7788cd3f877c9aa1b6487976bd63a6196870(svelte@5.25.3)
'@floating-ui/dom': 1.6.13
'@melt-ui/pp': 0.3.2(@melt-ui/svelte@0.86.6(svelte@5.25.3))(svelte@5.25.3)
'@melt-ui/svelte': 0.86.6(svelte@5.25.3)
@@ -4791,7 +4791,7 @@ snapshots:
'@types/prop-types@15.7.14': {}
'@types/react@18.3.22':
'@types/react@18.3.23':
dependencies:
'@types/prop-types': 15.7.14
csstype: 3.1.3
@@ -6752,7 +6752,7 @@ snapshots:
svelte-motion@0.12.2(svelte@5.25.3):
dependencies:
'@types/react': 18.3.22
'@types/react': 18.3.23
framesync: 6.1.2
popmotion: 11.0.5
style-value-types: 5.1.2
@@ -1,10 +1,13 @@
<script lang="ts">
import { base } from '$app/paths';
import { isTabletViewport } from '$lib/stores/viewport';
import { createEventDispatcher, onMount, onDestroy } from 'svelte';
import { Button } from '$lib/elements/forms';
import { Icon } from '@appwrite.io/pink-svelte';
import { IconX } from '@appwrite.io/pink-icons-svelte';
import { isTabletViewport } from '$lib/stores/viewport';
import PinkBackground from '$lib/images/pink-background.svg';
import { createEventDispatcher, onMount, onDestroy } from 'svelte';
export let variant: 'gradient' | 'image' = 'gradient';
let container: HTMLElement;
const dispatch = createEventDispatcher();
@@ -19,8 +22,7 @@
const alertHeight = container?.getBoundingClientRect()?.height || 0;
const { header, sidebar, content } = queryLayoutElements();
const headerHeight = header?.getBoundingClientRect().height || 0;
const offset = alertHeight + (!isTabletViewport && header ? headerHeight : 0);
const offset = alertHeight + (!$isTabletViewport && header ? headerHeight : 0);
if (header) header.style.top = `${alertHeight}px`;
if (sidebar) {
sidebar.style.top = `${offset}px`;
@@ -35,23 +37,32 @@
<svelte:window on:resize={setNavigationHeight} />
<div bind:this={container} class="top-banner alert is-action is-action-and-top-sticky">
<div class="top-banner-bg">
<div class="top-banner-bg-1">
<img
src={`${base}/images/top-banner/bg-pink-desktop.svg`}
width="1283"
height="1278"
alt="" />
<div
bind:this={container}
class:darker={variant === 'image'}
class="top-banner alert is-action is-action-and-top-sticky">
{#if variant === 'gradient'}
<div class="top-banner-bg">
<div class="top-banner-bg-1">
<img
src={`${base}/images/top-banner/bg-pink-desktop.svg`}
width="1283"
height="1278"
alt="" />
</div>
<div class="top-banner-bg-2">
<img
src={`${base}/images/top-banner/bg-mint-desktop.svg`}
width="1051"
height="1271"
alt="" />
</div>
</div>
<div class="top-banner-bg-2">
<img
src={`${base}/images/top-banner/bg-mint-desktop.svg`}
width="1051"
height="1271"
alt="" />
{:else}
<div class="centered-image-only">
<img src={PinkBackground} width="1283" height="1278" alt="" />
</div>
</div>
{/if}
<div class="top-banner-content u-color-text-primary">
<slot />
@@ -62,17 +73,39 @@
</Button>
</div>
<style>
<style lang="scss">
.alert {
top: 0;
width: 100%;
z-index: 100;
position: fixed;
padding: 0.8rem;
&.darker {
background: var(--bgcolor-neutral-default);
}
}
@media (max-width: 768px) {
:global(.top-banner-button.position) {
.centered-image-only {
top: 0;
width: 100%;
height: 100%;
padding-left: 25vw;
position: absolute;
border-bottom: 1px solid var(--border-neutral);
@media (max-width: 768px) {
& img {
max-width: 100vw;
}
}
}
:global(.top-banner-button.position) {
z-index: 1;
@media (max-width: 768px) {
position: absolute;
padding-block-start: 1rem;
padding-block-end: 3.7625rem;
}
}
@@ -1,8 +1,8 @@
<script lang="ts">
import { BillingPlan } from '$lib/constants';
import { BASE_BILLING_PLANS, BillingPlan } from '$lib/constants';
import { formatCurrency } from '$lib/helpers/numbers';
import { plansInfo, type Tier, tierFree, tierPro, tierScale } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import { currentPlan, organization } from '$lib/stores/organization';
import { Badge, Layout, Typography } from '@appwrite.io/pink-svelte';
import { LabelCard } from '..';
@@ -14,6 +14,8 @@
$: freePlan = $plansInfo.get(BillingPlan.FREE);
$: proPlan = $plansInfo.get(BillingPlan.PRO);
$: scalePlan = $plansInfo.get(BillingPlan.SCALE);
$: isBasePlan = BASE_BILLING_PLANS.includes($currentPlan?.$id);
</script>
<Layout.Stack>
@@ -72,4 +74,25 @@
{formatCurrency(scalePlan?.price ?? 0)} per month + usage
</Typography.Text>
</LabelCard>
{#if $currentPlan && !isBasePlan}
<LabelCard
name="plan"
bind:group={billingPlan}
value={$currentPlan.$id}
title={$currentPlan.name}>
<svelte:fragment slot="action">
{#if $organization?.billingPlan === $currentPlan.$id && !isNewOrg}
<Badge variant="secondary" size="xs" content="Current plan" />
{/if}
</svelte:fragment>
<Typography.Caption variant="400">
{$currentPlan.desc}
</Typography.Caption>
<Typography.Text>
{@const isZeroPrice = ($currentPlan?.price ?? 0) <= 0}
{@const price = formatCurrency($currentPlan?.price ?? 0)}
{isZeroPrice ? price : `${price} per month + usage`}
</Typography.Text>
</LabelCard>
{/if}
</Layout.Stack>
+12 -4
View File
@@ -12,7 +12,7 @@
import { isCloud } from '$lib/system';
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { newOrgModal } from '$lib/stores/organization';
import { currentPlan, newOrgModal } from '$lib/stores/organization';
import { Click, trackEvent } from '$lib/actions/analytics';
import { page } from '$app/stores';
@@ -182,6 +182,14 @@
projectsBottomSheetOpen = false;
}
}
$: correctPlanName =
// the plan names are hardcoded in some cases and are not available locally,
// so we rely on the plan's source of truth - `$currentPlan`
$currentPlan &&
$currentPlan?.name.toLocaleLowerCase() !== selectedOrg?.tierName.toLocaleLowerCase()
? $currentPlan.name
: selectedOrg?.tierName; // fallback
</script>
<svelte:window on:resize={onResize} />
@@ -195,9 +203,9 @@
aria-label="Open organizations tab">
<span class="orgName">{selectedOrg?.name ?? 'Organization'}</span>
<span class="not-mobile"
>{#if selectedOrg?.tierName}<Badge
>{#if correctPlanName}<Badge
variant="secondary"
content={selectedOrg?.tierName} />{/if}</span>
content={correctPlanName} />{/if}</span>
<Icon icon={IconChevronDown} size="s" color="--fgcolor-neutral-secondary" />
</button>
{:else}
@@ -211,7 +219,7 @@
<span class="orgName" class:noProjects={!selectedProject}
>{selectedOrg?.name ?? 'Organization'}</span>
<span class="not-mobile"
><Badge variant="secondary" content={selectedOrg?.tierName ?? ''} /></span>
><Badge variant="secondary" content={correctPlanName ?? ''} /></span>
<Icon icon={IconChevronDown} size="s" color="--fgcolor-neutral-secondary" />
</button>
{/if}
+37 -19
View File
@@ -18,6 +18,19 @@
children: Snippet<[toggle: () => void, selectedColumnsNumber: number]>;
} = $props();
let maxHeight = $state('none');
let containerRef;
const calcMaxHeight = () => {
if (containerRef) {
// get parent row element for correct top position
const parent = containerRef.parentElement.parentElement;
const { top } = parent.getBoundingClientRect();
maxHeight = `${window.innerHeight - top - 48}px`;
}
};
onMount(async () => {
if (isCustomCollection) {
const prefs = preferences.getCustomCollectionColumns(page.params.collection);
@@ -50,6 +63,8 @@
preferences.setColumns(columns);
}
});
calcMaxHeight();
});
let selectedColumnsNumber = $derived(
@@ -61,29 +76,32 @@
);
</script>
<svelte:window on:resize={calcMaxHeight} />
{#if $columns?.length}
<Popover let:toggle placement="bottom-end" padding="none">
{@render children(toggle, selectedColumnsNumber)}
<svelte:fragment slot="tooltip">
<ActionMenu.Root>
{#each $columns as column}
{#if !column?.exclude}
<ActionMenu.Item.Button
on:click={() => (column.hide = !column.hide)}
disabled={allowNoColumns
? false
: selectedColumnsNumber <= 1 && column.hide !== true}>
<Layout.Stack direction="row" gap="s">
<Selector.Checkbox
checked={!column.hide}
size="s"
on:click={() => (column.hide = !column.hide)} />
{column.title}
</Layout.Stack>
</ActionMenu.Item.Button>
{/if}
{/each}
</ActionMenu.Root>
<div style:max-height={maxHeight} style:overflow="scroll" bind:this={containerRef}>
<ActionMenu.Root>
{#each $columns as column}
{#if !column?.exclude}
<ActionMenu.Item.Button
on:click={() => (column.hide = !column.hide)}
disabled={allowNoColumns
? false
: selectedColumnsNumber <= 1 && column.hide !== true}>
<Layout.Stack direction="row" gap="s">
<Selector.Checkbox
checked={!column.hide}
size="s"
on:click={() => (column.hide = !column.hide)} />
{column.title}
</Layout.Stack>
</ActionMenu.Item.Button>
{/if}
{/each}
</ActionMenu.Root>
</div>
</svelte:fragment>
</Popover>
{/if}
+13 -8
View File
@@ -23,7 +23,7 @@
import { page } from '$app/state';
import Card from '../card.svelte';
import SkeletonRepoList from './skeletonRepoList.svelte';
import { untrack } from 'svelte';
import { onMount, untrack } from 'svelte';
let {
action = $bindable('select'),
@@ -46,6 +46,11 @@
let search = $state('');
let selectedInstallation = $state(null);
let isLoadingRepositories = $state(null);
let installationsMap = $state(null);
onMount(() => {
loadInstallations();
});
async function loadInstallations() {
if (installationList) {
@@ -59,7 +64,7 @@
)
);
}
return installationList.installations;
installationsMap = installationList.installations;
} else {
const { installations } = await sdk
.forProject(page.params.region, page.params.project)
@@ -70,7 +75,7 @@
}
installation.set(installations.find((entry) => entry.$id === selectedInstallation));
}
return installations;
installationsMap = installations;
}
}
@@ -123,7 +128,7 @@
{#if hasInstallations}
<Layout.Stack>
<Layout.Stack gap="s">
{#await loadInstallations()}
{#if !installationsMap}
<Layout.Stack direction="row">
<InputSelect
disabled
@@ -137,12 +142,12 @@
value={null} />
<InputSearch placeholder="Search repositories" disabled />
</Layout.Stack>
{:then installations}
{:else}
<Layout.Stack direction={$isSmallViewport ? 'column' : 'row'}>
<InputSelect
id="installation"
options={[
...installations.map((entry) => {
...installationsMap.map((entry) => {
return {
label: entry.organization,
value: entry.$id
@@ -160,7 +165,7 @@
}
search = '';
installation.set(
installations.find((entry) => entry.$id === selectedInstallation)
installationsMap.find((entry) => entry.$id === selectedInstallation)
);
isLoadingRepositories = true;
@@ -174,7 +179,7 @@
bind:value={search}
disabled={!search && !$repositories?.repositories?.length} />
</Layout.Stack>
{/await}
{/if}
</Layout.Stack>
{#if selectedInstallation}
<!-- manual installation change -->
+3 -3
View File
@@ -211,6 +211,7 @@
<Popover let:toggle let:showing>
<button
type="button"
style:cursor="pointer"
on:click|preventDefault={(e) => {
toggle(e);
shouldAnimateThemeToggle = false;
@@ -221,7 +222,7 @@
style:user-select="none">
<Avatar size="s" src={avatar} />
</button>
<svelte:fragment slot="tooltip" let:toggle>
<svelte:fragment slot="tooltip">
<ActionMenu.Root noPadding>
<Layout.Stack gap="xxs">
<div
@@ -229,12 +230,11 @@
style:padding-inline-end="8px"
style:padding-block="4px">
<Typography.Text variant="m-500">
{$user.email}
{$user?.email}
</Typography.Text>
</div>
<ActionMenu.Item.Anchor
trailingIcon={IconUser}
on:click={toggle}
size="l"
href={`${base}/account`}>
Account</ActionMenu.Item.Anchor>
+2
View File
@@ -530,6 +530,8 @@ export enum BillingPlan {
ENTERPRISE = 'ent-1'
}
export const BASE_BILLING_PLANS: string[] = [BillingPlan.FREE, BillingPlan.PRO, BillingPlan.SCALE];
export const feedbackDowngradeOptions = [
{
value: 'availableFeatures',
+3 -1
View File
@@ -20,5 +20,7 @@ function isFlagEnabled(name: string) {
export const flags = {
showSites: isFlagEnabled('sites'),
showCsvImport: isFlagEnabled('csv-import')
showCsvImport: isFlagEnabled('csv-import'),
showAttributeEncrypt: isFlagEnabled('attribute-encrypt'),
showIndexLengths: isFlagEnabled('index-lengths')
};
+1
View File
@@ -44,6 +44,7 @@ export type Column = PinkColumn & {
format?: string;
exclude?: boolean;
elements?: string[] | { value: string | number; label: string }[];
encrypt?: boolean;
};
export function isValueOfStringEnum<T extends Record<string, string>>(
+76
View File
@@ -0,0 +1,76 @@
<svg width="481" height="44" viewBox="0 0 481 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<g style="mix-blend-mode:screen" opacity="0.25" filter="url(#filter0_f_2804_2539)">
<circle cx="242.19" cy="52.7439" r="110.744" fill="url(#paint0_radial_2804_2539)"/>
</g>
<g filter="url(#filter1_f_2804_2539)">
<ellipse cx="240.479" cy="56.2318" rx="40.0747" ry="119.968" transform="rotate(66.0796 240.479 56.2318)" fill="url(#paint1_radial_2804_2539)" fill-opacity="0.4"/>
</g>
<g filter="url(#filter2_f_2804_2539)">
<ellipse cx="228.238" cy="51.0802" rx="23.6296" ry="70.7377" transform="rotate(66.0796 228.238 51.0802)" fill="url(#paint2_radial_2804_2539)" fill-opacity="0.5"/>
</g>
<g filter="url(#filter3_f_2804_2539)">
<path d="M191.132 89.2277C140.692 116.602 92.3886 125.135 83.2442 108.286C74.0998 91.4362 107.577 55.5854 158.017 28.2107C208.458 0.835917 256.761 -7.69658 265.906 9.15282C275.05 26.0022 241.573 61.853 191.132 89.2277Z" fill="url(#paint3_radial_2804_2539)" fill-opacity="0.5"/>
</g>
<g filter="url(#filter4_f_2804_2539)">
<ellipse cx="128.998" cy="25.9598" rx="16.2007" ry="66.1069" transform="rotate(66.0796 128.998 25.9598)" fill="url(#paint4_radial_2804_2539)" fill-opacity="0.05"/>
</g>
<g filter="url(#filter5_f_2804_2539)">
<path d="M238.738 55.4355C188.298 82.8103 145.088 100.728 142.227 95.4567C139.366 90.1851 177.937 63.72 228.378 36.3452C278.818 8.97044 322.028 -8.94768 324.889 -3.67603C327.75 1.59561 289.179 28.0608 238.738 55.4355Z" fill="url(#paint5_radial_2804_2539)" fill-opacity="0.35"/>
</g>
<defs>
<filter id="filter0_f_2804_2539" x="70.3866" y="-119.06" width="343.607" height="343.607" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="30.5299" result="effect1_foregroundBlur_2804_2539"/>
</filter>
<filter id="filter1_f_2804_2539" x="0.195038" y="-134.073" width="480.567" height="380.61" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="64.7038" result="effect1_foregroundBlur_2804_2539"/>
</filter>
<filter id="filter2_f_2804_2539" x="62.8613" y="-84.8274" width="330.753" height="271.815" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="50" result="effect1_foregroundBlur_2804_2539"/>
</filter>
<filter id="filter3_f_2804_2539" x="49.7393" y="-31.4919" width="249.671" height="180.422" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="16" result="effect1_foregroundBlur_2804_2539"/>
</filter>
<filter id="filter4_f_2804_2539" x="47.2728" y="-25.6037" width="163.45" height="103.127" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="10.4674" result="effect1_foregroundBlur_2804_2539"/>
</filter>
<filter id="filter5_f_2804_2539" x="110.078" y="-36.5962" width="246.96" height="164.973" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="16" result="effect1_foregroundBlur_2804_2539"/>
</filter>
<radialGradient id="paint0_radial_2804_2539" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(242.19 52.7439) rotate(90) scale(110.744)">
<stop stop-color="#FD366E"/>
<stop offset="1" stop-color="#FD366E" stop-opacity="0"/>
</radialGradient>
<radialGradient id="paint1_radial_2804_2539" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(240.479 56.2318) rotate(90) scale(119.968 40.0747)">
<stop stop-color="#FE9567"/>
<stop offset="1" stop-color="#F02E65" stop-opacity="0"/>
</radialGradient>
<radialGradient id="paint2_radial_2804_2539" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(228.238 51.0802) rotate(90) scale(70.7377 23.6296)">
<stop stop-color="#FFEAE1"/>
<stop offset="1" stop-color="#F02E65" stop-opacity="0"/>
</radialGradient>
<radialGradient id="paint3_radial_2804_2539" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(174.575 58.7192) rotate(151.511) scale(103.914 34.7119)">
<stop stop-color="#F02E65"/>
<stop offset="1" stop-color="#F02E65" stop-opacity="0"/>
</radialGradient>
<radialGradient id="paint4_radial_2804_2539" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(128.998 25.9598) rotate(90) scale(66.1069 16.2007)">
<stop stop-color="#FFEAE1"/>
<stop offset="1" stop-color="#F02E65" stop-opacity="0"/>
</radialGradient>
<radialGradient id="paint5_radial_2804_2539" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(233.558 45.8904) rotate(151.511) scale(103.914 10.8603)">
<stop stop-color="#F02E65"/>
<stop offset="1" stop-color="#F02E65" stop-opacity="0"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

@@ -17,7 +17,7 @@
let {
columns,
view = View.Table,
view = $bindable(View.Table),
hideView = false,
hideColumns = false,
hasSearch = false,
+1
View File
@@ -311,6 +311,7 @@ export type Plan = {
teams: number;
projects: number;
databases: number;
databasesAllowEncrypt: boolean;
buckets: number;
fileSize: number;
functions: number;
+35 -3
View File
@@ -11,7 +11,7 @@ import PaymentAuthRequired from '$lib/components/billing/alerts/paymentAuthRequi
import PaymentMandate from '$lib/components/billing/alerts/paymentMandate.svelte';
import { BillingPlan, NEW_DEV_PRO_UPGRADE_COUPON } from '$lib/constants';
import { cachedStore } from '$lib/helpers/cache';
import { sizeToBytes, type Size } from '$lib/helpers/sizeConvertion';
import { type Size, sizeToBytes } from '$lib/helpers/sizeConvertion';
import type {
AddressesList,
Aggregation,
@@ -28,13 +28,19 @@ import { Query } from '@appwrite.io/console';
import { derived, get, writable } from 'svelte/store';
import { headerAlert } from './headerAlert';
import { addNotification, notifications } from './notifications';
import { organization, type Organization, type OrganizationError } from './organization';
import {
currentPlan,
organization,
type Organization,
type OrganizationError
} from './organization';
import { canSeeBilling } from './roles';
import { sdk } from './sdk';
import { user } from './user';
import BudgetLimitAlert from '$routes/(console)/organization-[organization]/budgetLimitAlert.svelte';
import TeamReadonlyAlert from '$routes/(console)/organization-[organization]/teamReadonlyAlert.svelte';
import ProjectsLimit from '$lib/components/billing/alerts/projectsLimit.svelte';
import EnterpriseTrial from '$routes/(console)/organization-[organization]/enterpriseTrial.svelte';
export type Tier = 'tier-0' | 'tier-1' | 'tier-2' | 'auto-1' | 'cont-1' | 'ent-1';
@@ -269,6 +275,32 @@ export function isServiceLimited(serviceId: PlanServices, plan: Tier, total: num
return isLimited && total >= limit && !hasUsageFees;
}
export function checkForEnterpriseTrial(org: Organization) {
if (!org || !org.billingNextInvoiceDate) return;
if (calculateEnterpriseTrial(org) > 0) {
headerAlert.add({
id: 'teamEnterpriseTrial',
component: EnterpriseTrial,
show: true,
importance: 11
});
}
}
export function calculateEnterpriseTrial(org: Organization) {
const endDate = new Date(org.billingNextInvoiceDate);
const startDate = new Date(org.billingCurrentInvoiceDate);
const today = new Date();
let diffCycle = endDate.getTime() - startDate.getTime();
diffCycle = Math.ceil(diffCycle / (1000 * 60 * 60 * 24));
if (diffCycle === 14) {
const remaining = endDate.getTime() - today.getTime();
return Math.ceil(remaining / (1000 * 60 * 60 * 24));
}
return 0;
}
export function calculateTrialDay(org: Organization) {
if (org?.billingPlan === BillingPlan.FREE) return false;
const endDate = new Date(org?.billingStartDate);
@@ -342,7 +374,7 @@ export async function checkForUsageLimit(org: Organization) {
];
const members = org.total;
const plan = get(plansInfo)?.get(org.billingPlan);
const plan = get(currentPlan);
const membersOverflow =
members > plan.addons.seats.limit ? members - (plan.addons.seats.limit || members) : 0;
+6 -2
View File
@@ -13,6 +13,10 @@ export type Account = Models.User<
>;
export const user = derived(page, ($page) => {
if (browser) sessionStorage.setItem('account', JSON.stringify($page.data.account));
return $page.data.account as Account;
if ($page.data?.account) {
if (browser) {
sessionStorage.setItem('account', JSON.stringify($page.data.account));
}
return $page.data.account as Account;
} else return null;
});
+2
View File
@@ -13,6 +13,7 @@
import Create from './createOrganization.svelte';
import {
calculateTrialDay,
checkForEnterpriseTrial,
checkForMandate,
checkForMarkedForDeletion,
checkForMissingPaymentMethod,
@@ -306,6 +307,7 @@
if (isCloud) {
currentOrganizationId = org.$id;
checkForProjectsLimit(org, data.projects?.length || 0);
checkForEnterpriseTrial(org);
await checkForUsageLimit(org);
checkForMarkedForDeletion(org);
await checkForNewDevUpgradePro(org);
@@ -0,0 +1,69 @@
<script lang="ts">
import { page } from '$app/state';
import { Button } from '$lib/elements/forms';
import { organization } from '$lib/stores/organization';
import { calculateEnterpriseTrial, hideBillingHeaderRoutes } from '$lib/stores/billing';
import { base } from '$app/paths';
import GradientBanner from '$lib/components/billing/gradientBanner.svelte';
import { Badge, Typography } from '@appwrite.io/pink-svelte';
import { activeHeaderAlert } from '../store';
$: upgradeUrl = `${base}/organization-${$organization?.$id}/change-plan`;
$: remainingDays = calculateEnterpriseTrial($organization);
let show = true;
function handleClose() {
show = false;
const now = new Date().getTime();
localStorage.setItem($activeHeaderAlert.id, now.toString());
}
</script>
{#if $organization?.$id && remainingDays > 0 && !hideBillingHeaderRoutes.includes(page.url.pathname) && show}
<GradientBanner variant="image" on:close={handleClose}>
<div class="banner-fullwidth-grid">
<Typography.Text color="--fgcolor-neutral-primary">
Your enterprise trial expires in <Badge
size="xs"
variant="secondary"
content={remainingDays.toString()} /> days
</Typography.Text>
<Button size="xs" secondary fullWidthMobile href={upgradeUrl}>Upgrade</Button>
</div>
</GradientBanner>
{/if}
<style lang="scss">
.banner-fullwidth-grid {
width: 100vw;
display: grid;
padding: 0 3rem;
align-items: center;
grid-template-columns: 1fr auto 1fr;
> :global(:first-child) {
grid-column: 2;
justify-self: center;
@media (max-width: 768px) {
grid-column: unset;
}
}
> :global(:last-child) {
grid-column: 3;
justify-self: end;
margin-inline-end: 4px;
}
@media (max-width: 768px) {
gap: 12px;
width: 100%;
display: flex;
padding: unset;
flex-direction: column;
}
}
</style>
@@ -1,37 +1,31 @@
<script lang="ts">
import { Empty, PaginationWithLimit, SearchQuery, ViewSelector } from '$lib/components';
import { Empty, PaginationWithLimit } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import { Container, ResponsiveContainerHeader } from '$lib/layout';
import { columns, showCreate } from './store';
import Table from './table.svelte';
import Grid from './grid.svelte';
import type { PageData } from './$types';
import { canWriteCollections } from '$lib/stores/roles';
import { Icon, Layout } from '@appwrite.io/pink-svelte';
import { Icon } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
export let data: PageData;
</script>
<Container>
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center">
<SearchQuery placeholder="Search by name or ID" />
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<ViewSelector
{columns}
view={data.view}
hideColumns={!data.collections.total}
hideView={!data.collections.total} />
{#if $canWriteCollections}
<Button on:click={() => ($showCreate = true)} event="create_collection">
<Icon icon={IconPlus} slot="start" size="s" />
Create collection
</Button>
{/if}
</Layout.Stack>
</Layout.Stack>
<ResponsiveContainerHeader
bind:view={data.view}
{columns}
hasSearch
searchPlaceholder="Search by name or ID">
{#if $canWriteCollections}
<Button on:click={() => ($showCreate = true)} event="create_collection">
<Icon icon={IconPlus} slot="start" size="s" />
Create collection
</Button>
{/if}
</ResponsiveContainerHeader>
{#if data.collections.total}
{#if data.view === 'grid'}
@@ -13,10 +13,11 @@
Link,
Popover,
Table,
Tooltip,
Typography
} from '@appwrite.io/pink-svelte';
import Create from '../createAttribute.svelte';
import { isRelationship } from '../document-[document]/attributes/store';
import { isRelationship, isString } from '../document-[document]/attributes/store';
import FailedModal from '../failedModal.svelte';
import CreateIndex from '../indexes/createIndex.svelte';
import { attributes, type Attributes, isCsvImportInProgress } from '../store';
@@ -33,7 +34,8 @@
IconPlus,
IconSwitchHorizontal,
IconTrash,
IconViewList
IconViewList,
IconLockClosed
} from '@appwrite.io/pink-icons-svelte';
import type { ComponentProps } from 'svelte';
import { Click, trackEvent } from '$lib/actions/analytics';
@@ -112,25 +114,38 @@
{:else}
<Icon icon={option.icon} size="s" />
{/if}
<span class="text u-trim-1" data-private>{attribute.key}</span>
{#if attribute.status !== 'available'}
<Badge
size="s"
variant="secondary"
content={attribute.status}
type={getAttributeStatusBadge(attribute.status)} />
{#if attribute.error}
<Link.Button
variant="muted"
on:click={(e) => {
e.preventDefault();
error = attribute.error;
showFailed = true;
}}>Details</Link.Button>
<Layout.Stack direction="row" alignItems="center" gap="s">
<Layout.Stack inline direction="row" alignItems="center" gap="xxs">
<span class="text u-trim-1" data-private>{attribute.key}</span>
{#if isString(attribute) && attribute.encrypt}
<Tooltip>
<Icon
size="s"
icon={IconLockClosed}
color="--fgcolor-neutral-tertiary" />
<div slot="tooltip">Encrypted</div>
</Tooltip>
{/if}
</Layout.Stack>
{#if attribute.status !== 'available'}
<Badge
size="s"
variant="secondary"
content={attribute.status}
type={getAttributeStatusBadge(attribute.status)} />
{#if attribute.error}
<Link.Button
variant="muted"
on:click={(e) => {
e.preventDefault();
error = attribute.error;
showFailed = true;
}}>Details</Link.Button>
{/if}
{:else if attribute.required}
<Badge size="xs" variant="secondary" content="required" />
{/if}
{:else if attribute.required}
<Badge size="s" variant="secondary" content="required" />
{/if}
</Layout.Stack>
</Layout.Stack>
</Table.Cell>
<Table.Cell column="type" {root}>
@@ -100,5 +100,4 @@
label="Array"
bind:checked={data.array}
disabled={data.required || editing}
description="Indicate whether this attribute should act as an array, with the default value set as an empty
array." />
description="Indicate whether this attribute is an array. Defaults to an empty array." />
@@ -92,5 +92,4 @@
label="Array"
bind:checked={data.array}
disabled={data.required || editing}
description="Indicate whether this attribute should act as an array, with the default value set as an empty
array." />
description="Indicate whether this attribute is an array. Defaults to an empty array." />
@@ -93,5 +93,4 @@
label="Array"
bind:checked={data.array}
disabled={data.required || editing}
description="Indicate whether this attribute should act as an array, with the default value set as an empty
array." />
description="Indicate whether this attribute is an array. Defaults to an empty array." />
@@ -124,5 +124,4 @@
label="Array"
bind:checked={data.array}
disabled={data.required || editing}
description="Indicate whether this attribute should act as an array, with the default value set as an empty
array." />
description="Indicate whether this attribute is an array. Defaults to an empty array." />
@@ -122,5 +122,4 @@
label="Array"
bind:checked={data.array}
disabled={data.required || editing}
description="Indicate whether this attribute should act as an array, with the default value set as an empty
array." />
description="Indicate whether this attribute is an array. Defaults to an empty array." />
@@ -120,5 +120,4 @@
label="Array"
bind:checked={data.array}
disabled={data.required || editing}
description="Indicate whether this attribute should act as an array, with the default value set as an empty
array." />
description="Indicate whether this attribute is an array. Defaults to an empty array." />
@@ -92,5 +92,4 @@
label="Array"
bind:checked={data.array}
disabled={data.required || editing}
description="Indicate whether this attribute should act as an array, with the default value set as an empty
array." />
description="Indicate whether this attribute is an array. Defaults to an empty array." />
@@ -18,7 +18,8 @@
data.size,
data.required,
data.default,
data.array
data.array,
data.encrypt
);
}
export async function updateString(
@@ -42,20 +43,28 @@
</script>
<script lang="ts">
import { Selector } from '@appwrite.io/pink-svelte';
import { isCloud } from '$lib/system';
import { upgradeURL } from '$lib/stores/billing';
import { currentPlan } from '$lib/stores/organization';
import { createConservative } from '$lib/helpers/stores';
import { ActionMenu, Selector } from '@appwrite.io/pink-svelte';
import { InputNumber, InputText, InputTextarea } from '$lib/elements/forms';
import { Popover, Layout, Tag, Typography, Link } from '@appwrite.io/pink-svelte';
import { flags } from '$lib/flags';
export let data: Partial<Models.AttributeString> = {
required: false,
size: 0,
default: null,
array: false
array: false,
encrypt: false
};
export let editing = false;
let savedDefault = data.default;
const showEncrypt = flags.showAttributeEncrypt(page.data);
function handleDefaultState(hideDefault: boolean) {
if (hideDefault) {
savedDefault = data.default;
@@ -73,48 +82,110 @@
array: false,
...data
});
$: listen(data);
$: handleDefaultState($required || $array);
// Check plan on cloud, always allow on self-hosted
$: supportsStringEncryption = isCloud ? $currentPlan?.databasesAllowEncrypt : true;
</script>
<InputNumber
id="size"
label="Size"
required
placeholder="Enter size"
bind:value={data.size}
required={true} />
{#if data.size >= 50}
<InputTextarea
id="default"
label="Default"
placeholder="Enter string"
disabled={data.required || data.array}
nullable={!data.required && !data.array}
maxlength={data.size}
bind:value={data.default} />
{:else}
<InputText
id="default"
label="Default"
placeholder="Enter string"
disabled={data.required || data.array}
nullable={!data.required && !data.array}
maxlength={data.size}
bind:value={data.default} />
{/if}
min={supportsStringEncryption && data.encrypt ? 150 : undefined}
helper={supportsStringEncryption && data.encrypt
? 'Encrypted string attributes require a minimum size of 150.'
: undefined} />
<svelte:component
this={data.size >= 50 ? InputTextarea : InputText}
id="default"
label="Default"
placeholder="Enter string"
maxlength={data.size}
bind:value={data.default}
disabled={data.required || data.array}
nullable={!data.required && !data.array} />
<Selector.Checkbox
size="s"
id="required"
label="Required"
bind:checked={data.required}
disabled={data.array}
description="Indicate whether this attribute is required" />
description="Indicate whether this attribute is required." />
<Selector.Checkbox
size="s"
id="array"
label="Array"
bind:checked={data.array}
disabled={data.required || editing}
description="Indicate whether this attribute should act as an array, with the default value set as an empty
array." />
description="Indicate whether this attribute is an array. Defaults to an empty array." />
{#if showEncrypt}
<Layout.Stack gap="xs" direction="column">
<div class="popover-holder" class:disabled-checkbox={!supportsStringEncryption || editing}>
<Layout.Stack inline gap="s" alignItems="flex-start" direction="row">
<Popover let:toggle placement="bottom-start">
<Selector.Checkbox
size="s"
id="encrypt"
bind:checked={data.encrypt}
disabled={!supportsStringEncryption || editing} />
<Layout.Stack gap="xxs" direction="column">
<button
type="button"
class="u-cursor-pointer"
on:click={(e) => {
if (!supportsStringEncryption) {
toggle(e);
} else {
data.encrypt = !data.encrypt;
}
}}>
<Layout.Stack inline gap="xxs" direction="row" alignItems="center">
<Typography.Text variant="m-500">Encrypted</Typography.Text>
{#if !supportsStringEncryption}
<Tag variant="default" size="xs" on:click={toggle}>Pro</Tag>
{/if}
</Layout.Stack>
</button>
<Typography.Text color="--fgcolor-neutral-tertiary">
Indicate whether this attribute is encrypted. Encrypted attributes
cannot be queried.
</Typography.Text>
</Layout.Stack>
<ActionMenu.Root width="180px" slot="tooltip">
<Typography.Text variant="m-500">
Available on Pro plan. <Link.Anchor href={$upgradeURL}
>Upgrade</Link.Anchor>
to enable encrypted attributes.
</Typography.Text>
</ActionMenu.Root>
</Popover>
</Layout.Stack>
</div>
</Layout.Stack>
{/if}
<style lang="scss">
.popover-holder {
& :global([role='tooltip']) {
margin-top: 4px;
left: 8rem !important;
}
// no cursor when disabled
&.disabled-checkbox :global(button) {
cursor: unset !important;
}
}
</style>
@@ -93,5 +93,4 @@
label="Array"
bind:checked={data.array}
disabled={data.required || editing}
description="Indicate whether this attribute should act as an array, with the default value set as an empty
array." />
description="Indicate whether this attribute is an array. Defaults to an empty array." />
@@ -20,7 +20,6 @@
const databaseId = page.params.database;
const collectionId = page.params.collection;
const documentId = page.params.document;
const editing = true;
function initWork() {
const prohibitedKeys = [
@@ -124,7 +123,7 @@
<svelte:fragment slot="title">{label}</svelte:fragment>
<svelte:fragment slot="aside">
<AttributeItem {attribute} bind:formValues={$work} {label} {editing} />
<AttributeItem {attribute} bind:formValues={$work} {label} editing />
</svelte:fragment>
<svelte:fragment slot="actions">
@@ -1,24 +1,22 @@
<script lang="ts">
import { page } from '$app/state';
import { PaginationInline } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Button, InputSelect } from '$lib/elements/forms';
import { preferences } from '$lib/stores/preferences';
import { sdk } from '$lib/stores/sdk';
import { Query, type Models } from '@appwrite.io/console';
import { onMount } from 'svelte';
import { doc } from '../store';
import { isRelationshipToMany } from './store';
import { IconPlus, IconX } from '@appwrite.io/pink-icons-svelte';
import { Icon, Layout, Typography } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import InputSelect from '$lib/elements/forms/inputSelect.svelte';
export let id: string;
export let label: string;
export let editing = false;
export let value: string | string[];
export let attribute: Models.AttributeRelationship;
export let optionalText: string | undefined = undefined;
export let editing = false;
const databaseId = page.params.database;
@@ -28,6 +26,7 @@
let relatedList: string[] = [];
let singleRel: string;
let showInput = false;
let newItemValue: string = '';
let limit = 10;
let offset = 0;
@@ -39,6 +38,7 @@
singleRel = value as string;
}
}
documentList = await getDocuments();
if (editing && $doc?.[attribute.key]) {
@@ -59,20 +59,48 @@
});
async function getDocuments(search: string = null) {
if (search) {
const documents = await sdk
.forProject(page.params.region, page.params.project)
.databases.listDocuments(databaseId, attribute.relatedCollection, [
Query.startsWith('$id', search),
Query.orderDesc('')
]);
return documents;
const queries = search ? [Query.startsWith('$id', search), Query.orderDesc('')] : [];
return await sdk
.forProject(page.params.region, page.params.project)
.databases.listDocuments(databaseId, attribute.relatedCollection, queries);
}
function getAvailableOptions(excludeIndex?: number) {
return options?.filter((option) => {
const otherItems =
excludeIndex !== undefined
? relatedList.filter((_, idx) => idx !== excludeIndex)
: relatedList;
return !otherItems.includes(option.value);
});
}
function updateRelatedList() {
relatedList = relatedList;
value = relatedList;
}
function removeItem(index: number) {
if (relatedList.length === 1) {
relatedList[index] = '';
} else {
const documents = await sdk
.forProject(page.params.region, page.params.project)
.databases.listDocuments(databaseId, attribute.relatedCollection);
return documents;
relatedList.splice(index, 1);
}
updateRelatedList();
}
function addNewItem() {
if (newItemValue) {
relatedList = [...relatedList, newItemValue];
value = relatedList;
newItemValue = '';
showInput = false;
}
}
function cancelAddItem() {
newItemValue = '';
showInput = false;
}
//Reactive statements
@@ -84,106 +112,140 @@
.reverse()
.slice(offset, offset + limit)
: [];
$: total = relatedList?.length ?? 0;
$: totalCount = relatedList?.length ?? 0;
$: options =
documentList?.documents?.map((n) => {
const data = displayNames.filter((name) => name !== '$id').map((name) => n?.[name]);
return {
value: n.$id,
label: n.$id,
data
};
return { value: n.$id, label: n.$id, data };
}) ?? [];
$: hasItems = totalCount > 0;
$: showTopAddButton = !editing && totalCount === 0 && !showInput;
$: showBottomAddButton =
(!editing && hasItems && !showInput) ||
(editing && hasItems && relatedList.every((item) => item) && !showInput);
$: showEmptyInput = editing && totalCount === 0 && !showInput;
</script>
{#if isRelationshipToMany(attribute)}
<div class="u-width-full-line">
<Layout.Stack direction="row" alignContent="space-between">
<Layout.Stack gap="xxs" direction="row" alignItems="center">
<Typography.Text variant="m-500">{label}</Typography.Text>
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
{optionalText}
</Typography.Text>
<Layout.Stack gap="xxl">
<Layout.Stack gap="m">
<Layout.Stack direction="row" alignContent="space-between">
<Layout.Stack gap="xxs" direction="row" alignItems="center">
<Typography.Text variant="m-500">{label}</Typography.Text>
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
{optionalText}
</Typography.Text>
</Layout.Stack>
{#if showTopAddButton}
<Button secondary on:click={() => (showInput = true)}>
<Icon icon={IconPlus} slot="start" size="s" />
Add item
</Button>
{/if}
</Layout.Stack>
{#if editing || total === 0}
<Button
secondary
on:click={() => {
showInput = true;
}}>
<Icon icon={IconPlus} slot="start" size="s" />
Add item
</Button>
<Layout.Stack gap="m">
<!-- Empty input for editing mode when no items exist -->
{#if showEmptyInput}
<Layout.Stack direction="row">
<InputSelect
{id}
required
{options}
bind:value={relatedList[0]}
placeholder={`Select ${attribute.key}`}
on:change={() => {
if (!relatedList[0]) relatedList = [''];
updateRelatedList();
}} />
</Layout.Stack>
{/if}
<!-- Existing items in editing mode -->
{#if editing && hasItems}
{#each paginatedItems as _, paginatedIndex}
{@const actualIndex = offset + paginatedIndex}
<Layout.Stack direction="row">
<InputSelect
{id}
required
options={getAvailableOptions(actualIndex)}
bind:value={relatedList[actualIndex]}
placeholder={`Select ${attribute.key}`}
on:change={updateRelatedList} />
{#if relatedList[actualIndex]}
<div style:padding-block-start="0.5rem">
<Button
icon
extraCompact
on:click={() => removeItem(actualIndex)}>
<Icon icon={IconX} size="s" />
</Button>
</div>
{/if}
</Layout.Stack>
{/each}
{/if}
<!-- Existing items in creation mode -->
{#if !editing && hasItems}
{#each relatedList as item, i}
<Layout.Stack direction="row">
<InputSelect
{id}
required
options={getAvailableOptions(i)}
bind:value={item}
on:change={updateRelatedList} />
<div style:padding-block-start="0.5rem">
<Button
icon
extraCompact
ariaLabel={`Delete item ${i}`}
on:click={() => {
relatedList.splice(i, 1);
updateRelatedList();
}}>
<Icon icon={IconX} size="s" />
</Button>
</div>
</Layout.Stack>
{/each}
{/if}
<!-- Input for adding new items -->
{#if showInput}
<Layout.Stack direction="row">
<InputSelect
{id}
required
placeholder={`Select ${attribute.key}`}
bind:value={newItemValue}
options={getAvailableOptions()}
on:change={addNewItem} />
<div style:padding-block-start="0.5rem">
<Button icon extraCompact on:click={cancelAddItem}>
<Icon icon={IconX} size="s" />
</Button>
</div>
</Layout.Stack>
{/if}
</Layout.Stack>
{#if showBottomAddButton}
<Layout.Stack direction="row" alignContent="flex-start">
<Button extraCompact on:click={() => (showInput = true)}>
<Icon icon={IconPlus} slot="start" size="s" />
Add item
</Button>
</Layout.Stack>
{/if}
</Layout.Stack>
<ul class="u-flex-vertical u-gap-4 u-margin-block-start-4">
{#if !editing && relatedList?.length}
{#each relatedList as item, i}
<InputSelect {id} required bind:value={item} {options} />
<Button
extraCompact
ariaLabel={`Delete item ${i}`}
on:click={() => {
relatedList.splice(i, 1);
relatedList = relatedList;
value = relatedList;
}}>
<span class="icon-x" aria-hidden="true"></span>
</Button>
{/each}
{/if}
{#if showInput}
<InputSelect
{id}
label="Rel"
required
placeholder={`Select ${attribute.key}`}
bind:value={relatedList[total]}
options={options?.filter((n) => !relatedList.includes(n.value))}
on:change={() => {
value = relatedList;
showInput = false;
}} />
<Button extraCompact ariaLabel={`Hide input`} on:click={() => (showInput = false)}>
<span class="icon-x" aria-hidden="true"></span>
</Button>
{/if}
{#if paginatedItems && editing}
{#each paginatedItems as item, i}
<InputSelect {id} label="Rel" required bind:value={item} {options} />
<Button
extraCompact
ariaLabel={`Delete item ${i}`}
on:click={() => {
relatedList.splice(i, 1);
relatedList = relatedList;
value = relatedList;
}}>
<span class="icon-x" aria-hidden="true"></span>
</Button>
{/each}
{/if}
</ul>
{#if editing}
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: {total}</p>
<PaginationInline {limit} bind:offset {total} hidePages />
</div>
{/if}
{#if total > 0 && !editing}
<Button
extraCompact
disabled={showInput}
on:click={() => {
showInput = true;
}}>
<Icon icon={IconPlus} slot="start" size="s" />
Add item
</Button>
{/if}
</div>
</Layout.Stack>
{:else}
<InputSelect
{id}
@@ -15,3 +15,8 @@ export function isRelationship(attribute: Attributes): attribute is Models.Attri
if (!attribute) return false;
return attribute?.type === 'relationship';
}
export function isString(attribute: Attributes): attribute is Models.AttributeString {
if (!attribute) return false;
return attribute?.type === 'string';
}
@@ -79,13 +79,15 @@
{ id: 'type' },
{ id: 'attributes' },
{ id: 'orders' },
{ id: 'lengths' },
{ id: 'actions', width: 40 }
]}>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="key" {root}>Key</Table.Header.Cell>
<Table.Header.Cell column="type" {root}>Type</Table.Header.Cell>
<Table.Header.Cell column="attributes" {root}>Attributes</Table.Header.Cell>
<Table.Header.Cell column="orders" {root}>Asc/Desc</Table.Header.Cell>
<Table.Header.Cell column="orders" {root}>Orders</Table.Header.Cell>
<Table.Header.Cell column="lengths" {root}>Lengths</Table.Header.Cell>
<Table.Header.Cell column="actions" {root} />
</svelte:fragment>
{#each data.collection.indexes as index}
@@ -118,6 +120,9 @@
<Table.Cell column="orders" {root}>
{index.orders}
</Table.Cell>
<Table.Cell column="lengths" {root}>
{index.lengths}
</Table.Cell>
<Table.Cell column="actions" {root}>
<Popover let:toggle padding="none" placement="bottom-end">
<Button text icon ariaLabel="more options" on:click={toggle}>
@@ -5,7 +5,7 @@
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, InputSelect, InputText } from '$lib/elements/forms';
import { Button, InputNumber, InputSelect, InputText } from '$lib/elements/forms';
import { remove } from '$lib/helpers/array';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
@@ -14,6 +14,7 @@
import { type Attributes, collection, indexes } from '../store';
import { Icon, Layout } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { flags } from '$lib/flags';
export let showCreateIndex = false;
export let externalAttribute: Attributes = null;
@@ -36,7 +37,9 @@
label: attribute.key
}));
let attributeList = [{ value: '', order: '' }];
let attributeList = [{ value: '', order: '', length: null }];
const showLengths = flags.showIndexLengths(page.data);
function generateIndexKey() {
let indexKeys = $indexes.map((index) => index.key);
@@ -51,8 +54,8 @@
function initialize() {
attributeList = externalAttribute
? [{ value: externalAttribute.key, order: 'ASC' }]
: [{ value: '', order: 'ASC' }];
? [{ value: externalAttribute.key, order: 'ASC', length: null }]
: [{ value: '', order: 'ASC', length: null }];
selectedType = IndexType.Key;
key = `index_${$indexes.length + 1}`;
}
@@ -78,8 +81,10 @@
key,
selectedType,
attributeList.map((a) => a.value),
attributeList.map((a) => a.order)
attributeList.map((a) => a.order),
attributeList.map((a) => (a.length ? Number(a.length) : null))
);
await Promise.allSettled([
invalidate(Dependencies.COLLECTION),
invalidate(Dependencies.DATABASE)
@@ -105,7 +110,7 @@
if (addAttributeDisabled) return;
// We assign instead of pushing to trigger Svelte's reactivity
attributeList = [...attributeList, { value: '', order: '' }];
attributeList = [...attributeList, { value: '', order: '', length: null }];
}
</script>
@@ -134,17 +139,26 @@
label={i === 0 ? 'Attribute' : undefined}
placeholder="Select Attribute"
bind:value={attribute.value} />
<InputSelect
options={[
{ value: 'ASC', label: 'ASC' },
{ value: 'DESC', label: 'DESC' }
]}
required
id={`order-${i}`}
label={i === 0 ? 'Order' : undefined}
bind:value={attribute.order}
placeholder="Select Order" />
<Layout.Stack direction="row" alignItems="flex-end" gap="xs">
<InputSelect
options={[
{ value: 'ASC', label: 'ASC' },
{ value: 'DESC', label: 'DESC' }
]}
required
id={`order-${i}`}
label={i === 0 ? 'Order' : undefined}
bind:value={attribute.order}
placeholder="Select Order" />
{#if selectedType === IndexType.Key && showLengths}
<InputNumber
id={`length-${i}`}
label={i === 0 ? 'Length' : undefined}
placeholder="Enter Length"
bind:value={attribute.length} />
{/if}
<Button
icon
compact
@@ -3,9 +3,13 @@
import { InputText } from '$lib/elements/forms';
import { Layout } from '@appwrite.io/pink-svelte';
import type { Models } from '@appwrite.io/console';
import { page } from '$app/state';
import { flags } from '$lib/flags';
export let showOverview = false;
export let selectedIndex: Models.Index = null;
const showLengths = flags.showIndexLengths(page.data);
</script>
<Modal title="Overview" bind:show={showOverview}>
@@ -39,6 +43,14 @@
id={`value-${selectedIndex.orders[i]}`}
value={selectedIndex.orders[i]}
readonly />
{#if showLengths}
<InputText
required
label={i === 0 ? 'Length' : ''}
id={`value-${selectedIndex.lengths[i]}`}
value={selectedIndex.lengths[i]?.toString() ?? null}
readonly />
{/if}
</Layout.Stack>
{/each}
{/if}
@@ -12,7 +12,11 @@
import type { Models } from '@appwrite.io/console';
import { afterUpdate, onMount } from 'svelte';
import type { PageData } from './$types';
import { isRelationship, isRelationshipToMany } from './document-[document]/attributes/store';
import {
isRelationship,
isRelationshipToMany,
isString
} from './document-[document]/attributes/store';
import RelationshipsModal from './relationshipsModal.svelte';
import { attributes, collection, columns } from './store';
import type { ColumnType } from '$lib/helpers/types';
@@ -23,11 +27,12 @@
Link,
Badge,
FloatingActionBar,
InteractiveText,
Typography
} from '@appwrite.io/pink-svelte';
import { toLocaleDateTime } from '$lib/helpers/date';
import DualTimeView from '$lib/components/dualTimeView.svelte';
import { flags } from '$lib/flags';
export let data: PageData;
const databaseId = page.params.database;
@@ -163,6 +168,8 @@
) as Models.AttributeRelationship[];
let checked = false;
const showEncrypt = flags.showAttributeEncrypt(data);
</script>
<Table.Root
@@ -198,77 +205,88 @@
{#each $columns as { id } (id)}
{@const attr = $attributes.find((n) => n.key === id)}
<Table.Cell column={id} {root}>
{#if isRelationship(attr)}
{@const args = displayNames?.[attr.relatedCollection] ?? ['$id']}
{#if !isRelationshipToMany(attr)}
{#if document[id]}
{@const related = document[id]}
<Link.Button
variant="muted"
on:click={(e) => {
e.preventDefault();
e.stopPropagation();
goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/collection-${attr.relatedCollection}/document-${related.$id}`
);
}}>
{#each args as arg, i}
{#if arg !== undefined}
{#if i}
&nbsp;|
{#if attr}
<Table.Cell column={id} {root}>
{#if isRelationship(attr)}
{@const args = displayNames?.[attr.relatedCollection] ?? ['$id']}
{#if !isRelationshipToMany(attr)}
{#if document[id]}
{@const related = document[id]}
<Link.Button
variant="muted"
on:click={(e) => {
e.preventDefault();
e.stopPropagation();
goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/collection-${attr.relatedCollection}/document-${related.$id}`
);
}}>
{#each args as arg, i}
{#if arg !== undefined}
{#if i}
&nbsp;|
{/if}
<span class="text" data-private>
{related?.[arg]}
</span>
{/if}
<span class="text" data-private>
{related?.[arg]}
</span>
{/if}
{/each}
</Link.Button>
{/each}
</Link.Button>
{:else}
<span class="text">n/a</span>
{/if}
{:else}
<span class="text">n/a</span>
{@const itemsNum = document[id]?.length}
<Button.Button
variant="extra-compact"
disabled={!itemsNum}
badge={itemsNum ?? 0}
on:click={(e) => {
e.stopPropagation();
e.preventDefault();
relationshipData = document[id];
showRelationships = true;
selectedRelationship = attr;
}}>
Items
</Button.Button>
{/if}
{:else}
{@const itemsNum = document[id]?.length}
<Button.Button
variant="extra-compact"
disabled={!itemsNum}
badge={itemsNum ?? 0}
on:click={(e) => {
e.stopPropagation();
e.preventDefault();
relationshipData = document[id];
showRelationships = true;
selectedRelationship = attr;
}}>
Items
</Button.Button>
{/if}
{:else}
{@const formatted = formatColumn(document[id])}
{@const isDatetimeAttribute = attr.type === 'datetime'}
{#if isDatetimeAttribute}
<DualTimeView time={formatted.whole}>
<span slot="title">Timestamp</span>
{toLocaleDateTime(formatted.whole, true, 'UTC')}
</DualTimeView>
{:else if formatted.truncated}
<Tooltip placement="bottom" disabled={!formatted.truncated}>
{@const datetime = document[id]}
{@const formatted = formatColumn(document[id])}
{@const isDatetimeAttribute = attr.type === 'datetime'}
{#if isDatetimeAttribute}
<DualTimeView time={datetime}>
<span slot="title">Timestamp</span>
{toLocaleDateTime(datetime, true)}
</DualTimeView>
{:else if isString(attr) && attr.encrypt && showEncrypt}
<button on:click={(e) => e.preventDefault()}>
<InteractiveText
copy={false}
variant="secret"
isVisible={false}
text={formatted.value} />
</button>
{:else if formatted.truncated}
<Tooltip placement="bottom" disabled={!formatted.truncated}>
<Typography.Text truncate>{formatted.value}</Typography.Text>
<span
let:showing
slot="tooltip"
style:white-space="pre-wrap"
style:word-break="break-all">
{#if showing}
{formatted.whole}
{/if}
</span>
</Tooltip>
{:else}
<Typography.Text truncate>{formatted.value}</Typography.Text>
<span
let:showing
slot="tooltip"
style:white-space="pre-wrap"
style:word-break="break-all">
{#if showing}
{formatted.whole}
{/if}
</span>
</Tooltip>
{:else}
<Typography.Text truncate>{formatted.value}</Typography.Text>
{/if}
{/if}
{/if}
</Table.Cell>
</Table.Cell>
{/if}
{/each}
<Table.Cell column="$created" {root}>
<DualTimeView time={document.$createdAt} />
@@ -1,6 +1,6 @@
<script lang="ts">
import { Card, SvgIcon } from '$lib/components';
import { Icon, Layout, Typography } from '@appwrite.io/pink-svelte';
import { Icon, Layout, Skeleton, Typography } from '@appwrite.io/pink-svelte';
import { IconGithub, IconGitBranch } from '@appwrite.io/pink-icons-svelte';
import type { Models, Runtime } from '@appwrite.io/console';
@@ -10,6 +10,7 @@
export let rootDir: string = undefined;
export let showGitData = true;
export let runtimes: Models.RuntimeList;
export let loading = false;
$: selectedRuntime = runtimes?.runtimes.find((r) => r.$id === runtime);
</script>
@@ -18,7 +19,7 @@
<Layout.Stack gap="xl">
<slot />
<Layout.Stack gap="l">
{#if selectedRuntime?.name}
{#if selectedRuntime?.name && !loading}
<Layout.Stack gap="xxxs">
<Typography.Caption variant="400">Runtime</Typography.Caption>
<Layout.Stack gap="xxs" alignItems="center" direction="row">
@@ -30,6 +31,11 @@
</Typography.Text>
</Layout.Stack>
</Layout.Stack>
{:else if loading}
<Layout.Stack gap="xs">
<Skeleton variant="line" width={100} height={16} />
<Skeleton variant="line" width="100%" height={20} />
</Layout.Stack>
{/if}
{#if showGitData}
{#if repositoryName}
@@ -7,7 +7,7 @@
import { organization } from '$lib/stores/organization';
import { isCloud } from '$lib/system';
import { IconPencil } from '@appwrite.io/pink-icons-svelte';
import { Fieldset, Icon, Input, Layout, Tag } from '@appwrite.io/pink-svelte';
import { Fieldset, Icon, Input, Layout, Skeleton, Tag } from '@appwrite.io/pink-svelte';
import type { ComponentType } from 'svelte';
export let name: string;
@@ -25,6 +25,7 @@
value: string;
label: string;
}[] = [];
export let loading = false;
let showCustomId = false;
</script>
@@ -51,13 +52,20 @@
{/if}
</Layout.Stack>
{#key runtime}
<InputSelect
id="runtime"
label="Runtime"
placeholder="Select runtime"
bind:value={runtime}
required
{options} />
{#if loading}
<Layout.Stack gap="xs">
<Skeleton variant="line" width={100} height={20} />
<Skeleton variant="line" width="100%" height={32} />
</Layout.Stack>
{:else}
<InputSelect
id="runtime"
label="Runtime"
placeholder="Select runtime"
bind:value={runtime}
required
{options} />
{/if}
{/key}
{#if isCloud}
@@ -79,12 +87,19 @@
</Layout.Stack>
{/if}
{#if showEntrypoint}
<InputText
label="Entrypoint"
id="entrypoint"
bind:value={entrypoint}
required
placeholder="Enter entrypoint" />
{#if loading}
<Layout.Stack gap="xs">
<Skeleton variant="line" width={100} height={20} />
<Skeleton variant="line" width="100%" height={32} />
</Layout.Stack>
{:else}
<InputText
label="Entrypoint"
id="entrypoint"
bind:value={entrypoint}
required
placeholder="Enter entrypoint" />
{/if}
{/if}
</Layout.Stack>
</Fieldset>
@@ -10,7 +10,7 @@
import { installation, repository } from '$lib/stores/vcs';
import { Layout } from '@appwrite.io/pink-svelte';
import { writable } from 'svelte/store';
import { ID, Runtime, VCSDeploymentType } from '@appwrite.io/console';
import { ID, Runtime, VCSDeploymentType, VCSDetectionType } from '@appwrite.io/console';
import type { Models } from '@appwrite.io/console';
import { onMount } from 'svelte';
import Details from '../(components)/details.svelte';
@@ -57,12 +57,40 @@
let silentMode = false;
let specification = specificationOptions[0].value;
let detectingRuntime = true;
onMount(async () => {
installation.set(data.installation);
repository.set(data.repository);
name = data.repository.name;
await detectRuntime();
});
async function detectRuntime() {
try {
detectingRuntime = true;
const detections = (await sdk
.forProject(page.params.regionn, page.params.project)
.vcs.createRepositoryDetection(
data.installation.$id,
page.params.repository,
VCSDetectionType.Runtime
)) as unknown as Models.DetectionRuntime; /* SDK return type is wrong atm */
entrypoint = detections.entrypoint;
buildCommand = detections.commands;
runtime = detections.runtime as Runtime;
trackEvent(Submit.FrameworkDetect, { runtime, source: 'repository' });
} catch (error) {
trackError(error, Submit.FrameworkDetect);
} finally {
detectingRuntime = false;
}
}
async function create() {
try {
const func = await sdk
@@ -154,7 +182,8 @@
bind:specification
{specificationOptions}
options={runtimeOptions}
showEntrypoint />
showEntrypoint
loading={detectingRuntime} />
<ProductionBranchFieldset
bind:branch
@@ -173,7 +202,8 @@
runtimes={data.runtimesList}
repositoryName={data.repository.name}
{branch}
{rootDir} />
{rootDir}
loading={detectingRuntime} />
</svelte:fragment>
<svelte:fragment slot="footer">
@@ -3,17 +3,17 @@ import { sdk } from '$lib/stores/sdk';
export const load = async ({ parent, params, url }) => {
const { installations, runtimesList, specificationsList } = await parent();
const [repository] = await Promise.all([
sdk
.forProject(params.region, params.project)
.vcs.getRepository(url.searchParams.get('installation'), params.repository)
]);
const repository = await sdk
.forProject(params.region, params.project)
.vcs.getRepository(url.searchParams.get('installation'), params.repository);
const installation = installations.installations.find(
(installation) => installation.$id === url.searchParams.get('installation')
);
return {
installation,
installations,
installation: installations.installations.find(
(installation) => installation.$id === url.searchParams.get('installation')
),
repository,
runtimesList,
specificationsList
@@ -1,19 +1,11 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import {
Empty,
EmptyFilter,
EmptySearch,
Id,
PaginationWithLimit,
SearchQuery,
ViewSelector
} from '$lib/components';
import { Filters, hasPageQueries } from '$lib/components/filters';
import { Empty, EmptyFilter, EmptySearch, Id, PaginationWithLimit } from '$lib/components';
import { hasPageQueries } from '$lib/components/filters';
import { Button } from '$lib/elements/forms';
import { toLocaleDateTime } from '$lib/helpers/date';
import { Container } from '$lib/layout';
import { Container, ResponsiveContainerHeader } from '$lib/layout';
import { MessagingProviderType } from '@appwrite.io/console';
import CreateMessageDropdown from './createMessageDropdown.svelte';
import FailedModal from './failedModal.svelte';
@@ -105,18 +97,18 @@
</script>
<Container>
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center">
<SearchQuery 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 />
{#if $canWriteMessages}
<CreateMessageDropdown />
{/if}
</Layout.Stack>
</Layout.Stack>
<ResponsiveContainerHeader
{columns}
bind:view={data.view}
hideView
hasFilters
hasSearch
analyticsSource="messaging_messages"
searchPlaceholder="Search by description, type, status, or ID">
{#if $canWriteMessages}
<CreateMessageDropdown />
{/if}
</ResponsiveContainerHeader>
{#if data.messages.total}
<Table.Root
@@ -1,27 +1,20 @@
<script lang="ts">
import { page } from '$app/state';
import { Button } from '$lib/elements/forms';
import {
Empty,
EmptySearch,
SearchQuery,
PaginationWithLimit,
EmptyFilter,
ViewSelector
} from '$lib/components';
import { Empty, EmptySearch, PaginationWithLimit, EmptyFilter } from '$lib/components';
import Create from './create.svelte';
import { goto } from '$app/navigation';
import { Container } from '$lib/layout';
import { Container, ResponsiveContainerHeader } from '$lib/layout';
import { base } from '$app/paths';
import type { Models } from '@appwrite.io/console';
import type { PageData } from './$types';
import { showCreate } from './store';
import { Filters, hasPageQueries } from '$lib/components/filters';
import { hasPageQueries } from '$lib/components/filters';
import Table from './table.svelte';
import type { Column } from '$lib/helpers/types';
import { writable } from 'svelte/store';
import { canWriteTopics } from '$lib/stores/roles';
import { Icon, Layout } from '@appwrite.io/pink-svelte';
import { Icon } from '@appwrite.io/pink-svelte';
import { View } from '$lib/helpers/load';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { Click, trackEvent } from '$lib/actions/analytics';
@@ -72,26 +65,26 @@
</script>
<Container>
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center">
<SearchQuery placeholder="Search by name or ID" />
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<Filters query={data.query} {columns} analyticsSource="messaging_topics_filter" />
<ViewSelector view={View.Table} {columns} hideView />
{#if $canWriteTopics}
<Button
on:click={() => {
$showCreate = true;
trackEvent(Click.MessagingTopicCreateClick);
}}
event="create_topic">
<Icon icon={IconPlus} slot="start" size="s" />
Create topic
</Button>
{/if}
</Layout.Stack>
</Layout.Stack>
<ResponsiveContainerHeader
{columns}
view={View.Table}
hideView
hasFilters
hasSearch
analyticsSource="messaging_topics_filter"
searchPlaceholder="Search by name or ID">
{#if $canWriteTopics}
<Button
on:click={() => {
$showCreate = true;
trackEvent(Click.MessagingTopicCreateClick);
}}
event="create_topic">
<Icon icon={IconPlus} slot="start" size="s" />
Create topic
</Button>
{/if}
</ResponsiveContainerHeader>
{#if data.topics.total}
<Table columns={$columns} {data} />
@@ -98,7 +98,7 @@
<div class="u-flex u-gap-16">
<div class="u-cross-child-center u-line-height-1-5">
<h6 class="u-bold">{$platform.name}</h6>
<p>{$platform.hostname}</p>
<p>{$platform.hostname || $platform.key}</p>
</div>
</div>
</Box>
@@ -91,11 +91,11 @@ const val APPWRITE_PUBLIC_ENDPOINT = "${sdk.forProject(page.params.region, page.
});
</script>
<Wizard title="Add Android platform" bind:showExitModal confirmExit>
<Form onSubmit={createAndroidPlatform}>
<Layout.Stack gap="xxl">
<!-- Step One -->
{#if !isPlatformCreated}
<Wizard title="Add Android platform" bind:showExitModal confirmExit={!isPlatformCreated}>
<Layout.Stack gap="xxl">
<!-- Step One -->
{#if !isPlatformCreated}
<Form onSubmit={createAndroidPlatform}>
<Fieldset legend="Details">
<Layout.Stack gap="l" alignItems="flex-end">
<Layout.Stack gap="s">
@@ -136,59 +136,60 @@ const val APPWRITE_PUBLIC_ENDPOINT = "${sdk.forProject(page.params.region, page.
</Button>
</Layout.Stack>
</Fieldset>
{:else}
<Layout.Stack gap="xxl">
<Card padding="s" radius="s">
<Layout.Stack
direction="row"
justifyContent="space-between"
alignItems="center"
gap="xs">
<Layout.Stack direction="row" alignItems="center" gap="s">
<Icon size="m" icon={IconAndroid} />
<Typography.Text variant="m-400" color="--fgcolor-neutral-primary">
{$createPlatform.name} ({$createPlatform.key})
</Typography.Text>
</Layout.Stack>
</Form>
{:else}
<Layout.Stack gap="xxl">
<Card padding="s" radius="s">
<Layout.Stack
direction="row"
justifyContent="space-between"
alignItems="center"
gap="xs">
<Layout.Stack direction="row" alignItems="center" gap="s">
<Icon size="m" icon={IconAndroid} />
<Typography.Text variant="m-400" color="--fgcolor-neutral-primary">
{$createPlatform.name} ({$createPlatform.key})
</Typography.Text>
</Layout.Stack>
</Card>
</Layout.Stack>
{/if}
<!-- Step Two -->
{#if isPlatformCreated}
<Fieldset legend="Clone starter">
<Layout.Stack gap="l">
<Typography.Text variant="m-500">
1. Clone the starter kit from GitHub using the terminal or Android
Studio.
</Typography.Text>
<!-- Temporary fix: Remove this div once Code splitting issue with stack spacing is resolved -->
<div class="pink2-code-margin-fix">
<Code lang="bash" lineNumbers code={gitCloneCode} />
</div>
<Typography.Text variant="m-500"
>2. Open the file <InlineCode
size="s"
code="data/repository/AppwriteRepository.kt" /> and update the configuration
settings.</Typography.Text>
<!-- Temporary fix: Remove this div once Code splitting issue with stack spacing is resolved -->
<div class="pink2-code-margin-fix">
<Code lang="kotlin" lineNumbers code={updateConfigCode} />
</div>
<Typography.Text variant="m-500"
>3. Run the app on a connected device or emulator, then click the <InlineCode
size="s"
code="Send a ping" /> button to verify the setup.</Typography.Text>
</Layout.Stack>
</Fieldset>
{/if}
</Layout.Stack>
</Form>
</Card>
</Layout.Stack>
{/if}
<!-- Step Two -->
{#if isPlatformCreated}
<Fieldset legend="Clone starter">
<Layout.Stack gap="l">
<Typography.Text variant="m-500">
1. If you're starting a new project, you can clone our starter kit from
GitHub using the terminal, VSCode or Android Studio.
</Typography.Text>
<!-- Temporary fix: Remove this div once Code splitting issue with stack spacing is resolved -->
<div class="pink2-code-margin-fix">
<Code lang="bash" lineNumbers code={gitCloneCode} />
</div>
<Typography.Text variant="m-500"
>2. Open the file <InlineCode
size="s"
code="data/repository/AppwriteRepository.kt" /> and update the configuration
settings.</Typography.Text>
<!-- Temporary fix: Remove this div once Code splitting issue with stack spacing is resolved -->
<div class="pink2-code-margin-fix">
<Code lang="kotlin" lineNumbers code={updateConfigCode} />
</div>
<Typography.Text variant="m-500"
>3. Run the app on a connected device or emulator, then click the <InlineCode
size="s"
code="Send a ping" /> button to verify the setup.</Typography.Text>
</Layout.Stack>
</Fieldset>
{/if}
</Layout.Stack>
<svelte:fragment slot="aside">
<Card padding="l" class="responsive-padding">
<Layout.Stack gap="xxl">
@@ -102,116 +102,121 @@ APPWRITE_PUBLIC_ENDPOINT: "${sdk.forProject(page.params.region, page.params.proj
});
</script>
<Wizard title="Add Apple platform" bind:showExitModal confirmExit>
<Form onSubmit={createApplePlatform}>
<Layout.Stack gap="xxl">
<!-- Step One -->
<Layout.Grid gap="l" rowGap="l" columns={4} columnsXS={2}>
{#each Object.entries(platforms) as [key, value]}
<Pink2Card.Selector
{value}
id={key}
title={key}
imageRadius="s"
name="framework"
bind:group={platform}
disabled={isCreatingPlatform || isPlatformCreated} />
{/each}
</Layout.Grid>
<Wizard title="Add Apple platform" bind:showExitModal confirmExit={!isPlatformCreated}>
<Layout.Stack gap="xxl">
<Form onSubmit={createApplePlatform}>
<Layout.Stack gap="xxl">
<!-- Step One -->
<Layout.Grid gap="l" rowGap="l" columns={4} columnsXS={2}>
{#each Object.entries(platforms) as [key, value]}
<Pink2Card.Selector
{value}
id={key}
title={key}
imageRadius="s"
name="framework"
bind:group={platform}
disabled={isCreatingPlatform || isPlatformCreated} />
{/each}
</Layout.Grid>
<!-- Step Two -->
{#if !isPlatformCreated}
<Fieldset legend="Details">
<Layout.Stack gap="l" alignItems="flex-end">
<Layout.Stack gap="s">
<InputText
id="name"
label="Name"
placeholder="My Apple App"
required
bind:value={$createPlatform.name} />
<!-- Step Two -->
{#if !isPlatformCreated}
<Fieldset legend="Details">
<Layout.Stack gap="l" alignItems="flex-end">
<Layout.Stack gap="s">
<InputText
id="name"
label="Name"
placeholder="My Apple App"
required
bind:value={$createPlatform.name} />
<!-- Tooltips on InputText don't work as of now -->
<InputText
id="hostname"
label="Bundle ID"
placeholder="com.company.appname"
required
bind:value={$createPlatform.key}>
<Tooltip slot="info" maxWidth="15rem">
<Icon icon={IconInfo} size="s" />
<Typography.Caption variant="400" slot="tooltip">
You can find your Bundle Identifier in the General tab for
your app's primary target in Xcode.
</Typography.Caption>
</Tooltip>
</InputText>
</Layout.Stack>
<Button
fullWidthMobile
size="s"
submit
forceShowLoader
submissionLoader={isCreatingPlatform}
disabled={!platform ||
!$createPlatform.name ||
!$createPlatform.key ||
isCreatingPlatform}>
Create platform
</Button>
</Layout.Stack>
</Fieldset>
{:else}
<Layout.Stack gap="xxl">
<Card padding="s" radius="s">
<Layout.Stack
direction="row"
justifyContent="space-between"
alignItems="center"
gap="xs">
<Layout.Stack direction="row" alignItems="center" gap="s">
<Icon size="m" icon={IconApple} />
<Typography.Text variant="m-400" color="--fgcolor-neutral-primary">
{$createPlatform.name} ({$createPlatform.key})
</Typography.Text>
<!-- Tooltips on InputText don't work as of now -->
<InputText
id="hostname"
label="Bundle ID"
placeholder="com.company.appname"
required
bind:value={$createPlatform.key}>
<Tooltip slot="info" maxWidth="15rem">
<Icon icon={IconInfo} size="s" />
<Typography.Caption variant="400" slot="tooltip">
You can find your Bundle Identifier in the General tab
for your app's primary target in Xcode.
</Typography.Caption>
</Tooltip>
</InputText>
</Layout.Stack>
</Layout.Stack>
</Card>
</Layout.Stack>
{/if}
<!-- Step Three -->
{#if isPlatformCreated}
<Fieldset legend="Clone starter">
<Layout.Stack gap="l">
<Typography.Text variant="m-500">
1. Clone the starter kit from GitHub using the terminal or XCode.
</Typography.Text>
<!-- Temporary fix: Remove this div once Code splitting issue with stack spacing is resolved -->
<div class="pink2-code-margin-fix">
<Code lang="bash" lineNumbers code={gitCloneCode} />
</div>
<Typography.Text variant="m-500"
>2. Open the file <InlineCode size="s" code="Sources/Config.plist" /> and
update the configuration settings.</Typography.Text>
<!-- Temporary fix: Remove this div once Code splitting issue with stack spacing is resolved -->
<div class="pink2-code-margin-fix">
<Code lang="plaintext" lineNumbers code={updateConfigCode} />
</div>
<Typography.Text variant="m-500"
>3. Run the app on a connected device or simulator, then click the <InlineCode
<Button
fullWidthMobile
size="s"
code="Send a ping" /> button to verify the setup.</Typography.Text>
submit
forceShowLoader
submissionLoader={isCreatingPlatform}
disabled={!platform ||
!$createPlatform.name ||
!$createPlatform.key ||
isCreatingPlatform}>
Create platform
</Button>
</Layout.Stack>
</Fieldset>
{:else}
<Layout.Stack gap="xxl">
<Card padding="s" radius="s">
<Layout.Stack
direction="row"
justifyContent="space-between"
alignItems="center"
gap="xs">
<Layout.Stack direction="row" alignItems="center" gap="s">
<Icon size="m" icon={IconApple} />
<Typography.Text
variant="m-400"
color="--fgcolor-neutral-primary">
{$createPlatform.name} ({$createPlatform.key})
</Typography.Text>
</Layout.Stack>
</Layout.Stack>
</Card>
</Layout.Stack>
</Fieldset>
{/if}
</Layout.Stack>
</Form>
{/if}
</Layout.Stack>
</Form>
<!-- Step Three -->
{#if isPlatformCreated}
<Fieldset legend="Clone starter">
<Layout.Stack gap="l">
<Typography.Text variant="m-500">
1. If you're starting a new project, you can clone our starter kit from
GitHub using the terminal or XCode.
</Typography.Text>
<!-- Temporary fix: Remove this div once Code splitting issue with stack spacing is resolved -->
<div class="pink2-code-margin-fix">
<Code lang="bash" lineNumbers code={gitCloneCode} />
</div>
<Typography.Text variant="m-500"
>2. Open the file <InlineCode size="s" code="Sources/Config.plist" /> and update
the configuration settings.</Typography.Text>
<!-- Temporary fix: Remove this div once Code splitting issue with stack spacing is resolved -->
<div class="pink2-code-margin-fix">
<Code lang="plaintext" lineNumbers code={updateConfigCode} />
</div>
<Typography.Text variant="m-500"
>3. Run the app on a connected device or simulator, then click the <InlineCode
size="s"
code="Send a ping" /> button to verify the setup.</Typography.Text>
</Layout.Stack>
</Fieldset>
{/if}
</Layout.Stack>
<svelte:fragment slot="aside">
<Card padding="l" class="responsive-padding">
<Layout.Stack gap="xxl">
@@ -161,133 +161,137 @@ static const String APPWRITE_PUBLIC_ENDPOINT = "${sdk.forProject(page.params.reg
});
</script>
<Wizard title="Add Flutter platform" bind:showExitModal confirmExit>
<Form onSubmit={createFlutterPlatform}>
<Layout.Stack gap="xxl">
<!-- Step One -->
<Layout.Grid gap="l" rowGap="l" columns={3} columnsXS={2} columnsXXS={1}>
{#each Object.entries(platforms) as [key, value]}
<Pink2Card.Selector
{value}
id={key}
title={key}
imageRadius="s"
name="framework"
bind:group={platform}
disabled={isCreatingPlatform || isPlatformCreated} />
{/each}
</Layout.Grid>
<Wizard title="Add Flutter platform" bind:showExitModal confirmExit={!isPlatformCreated}>
<Layout.Stack gap="xxl">
<Form onSubmit={createFlutterPlatform}>
<Layout.Stack gap="xxl">
<!-- Step One -->
<Layout.Grid gap="l" rowGap="l" columns={3} columnsXS={2} columnsXXS={1}>
{#each Object.entries(platforms) as [key, value]}
<Pink2Card.Selector
{value}
id={key}
title={key}
imageRadius="s"
name="framework"
bind:group={platform}
disabled={isCreatingPlatform || isPlatformCreated} />
{/each}
</Layout.Grid>
<!-- Step Two -->
{#if !isPlatformCreated}
<Fieldset legend="Details">
<Layout.Stack gap="l" alignItems="flex-end">
<Layout.Stack gap="s">
<InputText
id="name"
label="Name"
placeholder={placeholder[platform].name}
required
bind:value={$createPlatform.name} />
<!-- Tooltips on InputText don't work as of now -->
{#if platform === PlatformType.Flutterweb}
<!-- Step Two -->
{#if !isPlatformCreated}
<Fieldset legend="Details">
<Layout.Stack gap="l" alignItems="flex-end">
<Layout.Stack gap="s">
<InputText
id="hostname"
label={hostname[platform]}
placeholder={placeholder[platform].hostname}
id="name"
label="Name"
placeholder={placeholder[platform].name}
required
bind:value={$createPlatform.hostname}>
<Tooltip slot="info" maxWidth="15rem">
<Icon icon={IconInfo} size="s" />
<Typography.Caption variant="400" slot="tooltip">
{placeholder[platform].tooltip}
</Typography.Caption>
</Tooltip>
</InputText>
{:else}
<InputText
id="key"
label={hostname[platform]}
placeholder={placeholder[platform].hostname}
required
bind:value={$createPlatform.key}>
<Tooltip slot="info" maxWidth="15rem">
<Icon icon={IconInfo} size="s" />
<Typography.Caption variant="400" slot="tooltip">
{placeholder[platform].tooltip}
</Typography.Caption>
</Tooltip>
</InputText>
{/if}
</Layout.Stack>
bind:value={$createPlatform.name} />
<Button
fullWidthMobile
size="s"
submit
forceShowLoader
submissionLoader={isCreatingPlatform}
disabled={!platform ||
!$createPlatform.name ||
(!$createPlatform.key && !$createPlatform.hostname) ||
isCreatingPlatform}>
Create platform
</Button>
</Layout.Stack>
</Fieldset>
{:else}
<Layout.Stack gap="xxl">
<Card padding="s" radius="s">
<Layout.Stack
direction="row"
justifyContent="space-between"
alignItems="center"
gap="xs">
<Layout.Stack direction="row" alignItems="center" gap="s">
<Icon size="m" icon={IconFlutter} />
<Typography.Text variant="m-400" color="--fgcolor-neutral-primary">
{$createPlatform.name} ({$createPlatform.key})
</Typography.Text>
<!-- Tooltips on InputText don't work as of now -->
{#if platform === PlatformType.Flutterweb}
<InputText
id="hostname"
label={hostname[platform]}
placeholder={placeholder[platform].hostname}
required
bind:value={$createPlatform.hostname}>
<Tooltip slot="info" maxWidth="15rem">
<Icon icon={IconInfo} size="s" />
<Typography.Caption variant="400" slot="tooltip">
{placeholder[platform].tooltip}
</Typography.Caption>
</Tooltip>
</InputText>
{:else}
<InputText
id="key"
label={hostname[platform]}
placeholder={placeholder[platform].hostname}
required
bind:value={$createPlatform.key}>
<Tooltip slot="info" maxWidth="15rem">
<Icon icon={IconInfo} size="s" />
<Typography.Caption variant="400" slot="tooltip">
{placeholder[platform].tooltip}
</Typography.Caption>
</Tooltip>
</InputText>
{/if}
</Layout.Stack>
<Button
fullWidthMobile
size="s"
submit
forceShowLoader
submissionLoader={isCreatingPlatform}
disabled={!platform ||
!$createPlatform.name ||
(!$createPlatform.key && !$createPlatform.hostname) ||
isCreatingPlatform}>
Create platform
</Button>
</Layout.Stack>
</Card>
</Layout.Stack>
{/if}
<!-- Step Three -->
{#if isPlatformCreated}
<Fieldset legend="Clone starter">
<Layout.Stack gap="l">
<Typography.Text variant="m-500">
1. Clone the starter kit from GitHub using the terminal, Android Studio
or VSCode.
</Typography.Text>
<!-- Temporary fix: Remove this div once Code splitting issue with stack spacing is resolved -->
<div class="pink2-code-margin-fix">
<Code lang="bash" lineNumbers code={gitCloneCode} />
</div>
<Typography.Text variant="m-500"
>2. Open the file <InlineCode
size="s"
code="lib/constants/appwrite.dart" /> and update the configuration settings.</Typography.Text>
<!-- Temporary fix: Remove this div once Code splitting issue with stack spacing is resolved -->
<div class="pink2-code-margin-fix">
<Code lang="dart" lineNumbers code={updateConfigCode} />
</div>
<Typography.Text variant="m-500"
>3. Run the app on a connected device or simulator, then click the <InlineCode
size="s"
code="Send a ping" /> button to verify the setup.</Typography.Text>
</Fieldset>
{:else}
<Layout.Stack gap="xxl">
<Card padding="s" radius="s">
<Layout.Stack
direction="row"
justifyContent="space-between"
alignItems="center"
gap="xs">
<Layout.Stack direction="row" alignItems="center" gap="s">
<Icon size="m" icon={IconFlutter} />
<Typography.Text
variant="m-400"
color="--fgcolor-neutral-primary">
{$createPlatform.name} ({$createPlatform.hostname ||
$createPlatform.key})
</Typography.Text>
</Layout.Stack>
</Layout.Stack>
</Card>
</Layout.Stack>
</Fieldset>
{/if}
</Layout.Stack>
</Form>
{/if}
</Layout.Stack>
</Form>
<!-- Step Three -->
{#if isPlatformCreated}
<Fieldset legend="Clone starter">
<Layout.Stack gap="l">
<Typography.Text variant="m-500">
1. If you're starting a new project, you can clone our starter kit from
GitHub using the terminal, VSCode or Android Studio.
</Typography.Text>
<!-- Temporary fix: Remove this div once Code splitting issue with stack spacing is resolved -->
<div class="pink2-code-margin-fix">
<Code lang="bash" lineNumbers code={gitCloneCode} />
</div>
<Typography.Text variant="m-500"
>2. Open the file <InlineCode size="s" code="lib/constants/appwrite.dart" />
and update the configuration settings.</Typography.Text>
<!-- Temporary fix: Remove this div once Code splitting issue with stack spacing is resolved -->
<div class="pink2-code-margin-fix">
<Code lang="dart" lineNumbers code={updateConfigCode} />
</div>
<Typography.Text variant="m-500"
>3. Run the app on a connected device or simulator, then click the <InlineCode
size="s"
code="Send a ping" /> button to verify the setup.</Typography.Text>
</Layout.Stack>
</Fieldset>
{/if}
</Layout.Stack>
<svelte:fragment slot="aside">
<Card padding="l" class="responsive-padding">
<Layout.Stack gap="xxl">
@@ -25,7 +25,6 @@
import ConnectionLine from './components/ConnectionLine.svelte';
import OnboardingPlatformCard from './components/OnboardingPlatformCard.svelte';
import { PlatformType } from '@appwrite.io/console';
import { isCloud } from '$lib/system';
let showExitModal = false;
let isPlatformCreated = false;
@@ -36,10 +35,8 @@
const gitCloneCode =
'\ngit clone https://github.com/appwrite/starter-for-react-native\ncd starter-for-react-native\n';
const updateConfigCode = isCloud
? `const APPWRITE_PROJECT_ID = "${projectId}";`
: `const APPWRITE_PROJECT_ID = "${projectId}";
const APPWRITE_PUBLIC_ENDPOINT = "${sdk.forProject(page.params.region, page.params.project).client.config.endpoint}";
const updateConfigCode = `const EXPO_PUBLIC_APPWRITE_PROJECT_ID = "${projectId}";
const EXPO_PUBLIC_APPWRITE_ENDPOINT = "${sdk.forProject(page.params.region, page.params.project).client.config.endpoint}";
`;
export let platform: PlatformType = PlatformType.Reactnativeandroid;
@@ -129,116 +126,123 @@ const APPWRITE_PUBLIC_ENDPOINT = "${sdk.forProject(page.params.region, page.para
});
</script>
<Wizard title="Add React Native platform" bind:showExitModal confirmExit>
<Form onSubmit={createReactNativePlatform}>
<Layout.Stack gap="xxl">
<!-- Step One -->
<Layout.Stack gap="l" direction="row">
{#each Object.entries(platforms) as [key, value]}
<Pink2Card.Selector
{value}
id={key}
title={key}
imageRadius="s"
name="framework"
bind:group={platform}
disabled={isCreatingPlatform || isPlatformCreated} />
{/each}
</Layout.Stack>
<!-- Step Two -->
{#if !isPlatformCreated}
<Fieldset legend="Details">
<Layout.Stack gap="l" alignItems="flex-end">
<Layout.Stack gap="s">
<InputText
id="name"
label="Name"
placeholder={placeholder[platform].name}
required
bind:value={$createPlatform.name} />
<!-- Tooltips on InputText don't work as of now -->
<InputText
id="hostname"
label={hostname[platform]}
placeholder={placeholder[platform].hostname}
required
bind:value={$createPlatform.key}>
<Tooltip slot="info" maxWidth="15rem">
<Icon icon={IconInfo} size="s" />
<Typography.Caption variant="400" slot="tooltip">
{placeholder[platform].tooltip}
</Typography.Caption>
</Tooltip>
</InputText>
</Layout.Stack>
<Button
fullWidthMobile
size="s"
submit
forceShowLoader
submissionLoader={isCreatingPlatform}
disabled={!platform ||
!$createPlatform.name ||
!$createPlatform.key ||
isCreatingPlatform}>
Create platform
</Button>
</Layout.Stack>
</Fieldset>
{:else}
<Layout.Stack gap="xxl">
<Card padding="s" radius="s">
<Layout.Stack
direction="row"
justifyContent="space-between"
alignItems="center"
gap="xs">
<Layout.Stack direction="row" alignItems="center" gap="s">
<Icon size="m" icon={IconReact} />
<Typography.Text variant="m-400" color="--fgcolor-neutral-primary">
{$createPlatform.name} ({$createPlatform.key})
</Typography.Text>
</Layout.Stack>
</Layout.Stack>
</Card>
<Wizard title="Add React Native platform" bind:showExitModal confirmExit={!isPlatformCreated}>
<Layout.Stack gap="xxl">
<Form onSubmit={createReactNativePlatform}>
<Layout.Stack gap="xxl">
<!-- Step One -->
<Layout.Stack gap="l" direction="row">
{#each Object.entries(platforms) as [key, value]}
<Pink2Card.Selector
{value}
id={key}
title={key}
imageRadius="s"
name="framework"
bind:group={platform}
disabled={isCreatingPlatform || isPlatformCreated} />
{/each}
</Layout.Stack>
{/if}
<!-- Step Three -->
{#if isPlatformCreated}
<Fieldset legend="Clone starter">
<Layout.Stack gap="l">
<Typography.Text variant="m-500">
1. Clone the starter kit from GitHub using the terminal or VSCode.
</Typography.Text>
<!-- Step Two -->
{#if !isPlatformCreated}
<Fieldset legend="Details">
<Layout.Stack gap="l" alignItems="flex-end">
<Layout.Stack gap="s">
<InputText
id="name"
label="Name"
placeholder={placeholder[platform].name}
required
bind:value={$createPlatform.name} />
<!-- Temporary fix: Remove this div once Code splitting issue with stack spacing is resolved -->
<div class="pink2-code-margin-fix">
<Code lang="bash" lineNumbers code={gitCloneCode} />
</div>
<!-- Tooltips on InputText don't work as of now -->
<InputText
id="hostname"
label={hostname[platform]}
placeholder={placeholder[platform].hostname}
required
bind:value={$createPlatform.key}>
<Tooltip slot="info" maxWidth="15rem">
<Icon icon={IconInfo} size="s" />
<Typography.Caption variant="400" slot="tooltip">
{placeholder[platform].tooltip}
</Typography.Caption>
</Tooltip>
</InputText>
</Layout.Stack>
<Typography.Text variant="m-500"
>2. Open the file <InlineCode
<Button
fullWidthMobile
size="s"
code="lib/constants/appwrite.js" /> and update the configuration settings.</Typography.Text>
<!-- Temporary fix: Remove this div once Code splitting issue with stack spacing is resolved -->
<div class="pink2-code-margin-fix">
<Code lang="javascript" lineNumbers code={updateConfigCode} />
</div>
<Typography.Text variant="m-500"
>3. Run the app on a connected device or simulator, then click the <InlineCode
size="s"
code="Send a ping" /> button to verify the setup.</Typography.Text>
submit
forceShowLoader
submissionLoader={isCreatingPlatform}
disabled={!platform ||
!$createPlatform.name ||
!$createPlatform.key ||
isCreatingPlatform}>
Create platform
</Button>
</Layout.Stack>
</Fieldset>
{:else}
<Layout.Stack gap="xxl">
<Card padding="s" radius="s">
<Layout.Stack
direction="row"
justifyContent="space-between"
alignItems="center"
gap="xs">
<Layout.Stack direction="row" alignItems="center" gap="s">
<Icon size="m" icon={IconReact} />
<Typography.Text
variant="m-400"
color="--fgcolor-neutral-primary">
{$createPlatform.name} ({$createPlatform.key})
</Typography.Text>
</Layout.Stack>
</Layout.Stack>
</Card>
</Layout.Stack>
</Fieldset>
{/if}
</Layout.Stack>
</Form>
{/if}
</Layout.Stack>
</Form>
<!-- Step Three -->
{#if isPlatformCreated}
<Fieldset legend="Clone starter">
<Layout.Stack gap="l">
<Typography.Text variant="m-500">
1. If you're starting a new project, you can clone our starter kit from
GitHub using the terminal or VSCode.
</Typography.Text>
<!-- Temporary fix: Remove this div once Code splitting issue with stack spacing is resolved -->
<div class="pink2-code-margin-fix">
<Code lang="bash" lineNumbers code={gitCloneCode} />
</div>
<Typography.Text variant="m-500"
>2. Add your Appwrite credentials to <InlineCode
size="s"
code=".env.example" /> then rename it to <InlineCode
size="s"
code=".env" /> if needed.</Typography.Text>
<!-- Temporary fix: Remove this div once Code splitting issue with stack spacing is resolved -->
<div class="pink2-code-margin-fix">
<Code lang="javascript" lineNumbers code={updateConfigCode} />
</div>
<Typography.Text variant="m-500"
>3. Run the app on a connected device or simulator, then click the <InlineCode
size="s"
code="Send a ping" /> button to verify the setup.</Typography.Text>
</Layout.Stack>
</Fieldset>
{/if}
</Layout.Stack>
<svelte:fragment slot="aside">
<Card padding="l" class="responsive-padding">
<Layout.Stack gap="xxl">
@@ -199,155 +199,150 @@ ${prefix}APPWRITE_ENDPOINT = "${sdk.forProject(page.params.region, page.params.p
});
</script>
<Wizard title="Add web platform" bind:showExitModal>
<Form onSubmit={createWebPlatform}>
<Layout.Stack gap="xxl">
<!-- Step One -->
{#if !isPlatformCreated || isChangingFramework}
<Fieldset legend="Type">
<Layout.Stack>
<div class="frameworks">
{#each frameworks as framework}
<Card.Selector
bind:group={selectedFrameworkKey}
name="framework"
id={framework.key}
value={framework.key}
title={framework.label}
icon={framework.icon}
imageRadius="s" />
{/each}
</div>
<Wizard title="Add web platform" bind:showExitModal confirmExit={!isPlatformCreated}>
<Layout.Stack gap="xxl">
<!-- Step One -->
{#if !isPlatformCreated || isChangingFramework}
<Form onSubmit={createWebPlatform}>
<Layout.Stack>
<Fieldset legend="Type">
<Layout.Stack>
<div class="frameworks">
{#each frameworks as framework}
<Card.Selector
bind:group={selectedFrameworkKey}
name="framework"
id={framework.key}
value={framework.key}
title={framework.label}
icon={framework.icon}
imageRadius="s" />
{/each}
</div>
<Layout.Stack direction="row" justifyContent="flex-end">
{#if isChangingFramework}
<Button
disabled={!selectedFramework}
on:click={() => (isChangingFramework = false)}>
Save</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">
{#if isChangingFramework}
<Button
disabled={!selectedFramework}
on:click={() => (isChangingFramework = false)}>
Save</Button>
{/if}
<Button submit disabled={!selectedFramework}>Create platform</Button>
</Layout.Stack>
{/if}
</Layout.Stack>
</Form>
{:else}
<Card.Base padding="s"
><Layout.Stack direction="row" justifyContent="space-between" alignItems="center">
<Layout.Stack gap="xxs" direction="row">
<Icon icon={selectedFramework.smallIcon} />
<Typography.Text variant="m-500">{selectedFramework.label}</Typography.Text>
</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 submit disabled={!selectedFramework}>Create platform</Button
></Layout.Stack>
{/if}
{:else}
<Card.Base padding="s"
><Layout.Stack
direction="row"
justifyContent="space-between"
alignItems="center">
<Layout.Stack gap="xxs" direction="row">
<Icon icon={selectedFramework.smallIcon} />
<Typography.Text variant="m-500"
>{selectedFramework.label}</Typography.Text>
</Layout.Stack>
<Button
size="s"
secondary
on:click={() => {
isChangingFramework = true;
}}>Change</Button>
</Layout.Stack></Card.Base>
{/if}
<Button
size="s"
secondary
on:click={() => {
isChangingFramework = true;
}}>Change</Button>
</Layout.Stack></Card.Base>
{/if}
<!-- Step Three -->
{#if isPlatformCreated && !isChangingFramework}
<Fieldset legend="Clone starter">
<Layout.Stack gap="l">
<Typography.Text variant="m-500">
1. Clone the starter kit from GitHub using the terminal or VSCode.
</Typography.Text>
<!-- Step Three -->
{#if isPlatformCreated && !isChangingFramework}
<Fieldset legend="Clone starter">
<Layout.Stack gap="l">
<Typography.Text variant="m-500">
1. If you're starting a new project, you can clone our starter kit from
GitHub using the terminal or VSCode.
</Typography.Text>
<!-- Temporary fix: Remove this div once Code splitting issue with stack spacing is resolved -->
<div class="pink2-code-margin-fix">
<Code
lang="bash"
lineNumbers
code={`\ngit clone https://github.com/appwrite/starter-for-${selectedFramework.key}\ncd starter-for-${selectedFramework.key}`} />
</div>
{#if selectedFramework.key === 'angular'}
<Typography.Text variant="m-500"
>2. Change <InlineCode
size="s"
code="src/environments/environment.ts" />
to reflect the values below:</Typography.Text>
{:else}
<Typography.Text variant="m-500"
>2. Rename <InlineCode size="s" code=".env.example" /> into <InlineCode
size="s"
code=".env" /> and update the values.</Typography.Text>
{/if}
<!-- Temporary fix: Remove this div once Code splitting issue with stack spacing is resolved -->
<div class="pink2-code-margin-fix">
<Code
lang="bash"
lineNumbers
code={selectedFramework.updateConfigCode} />
</div>
<!-- Temporary fix: Remove this div once Code splitting issue with stack spacing is resolved -->
<div class="pink2-code-margin-fix">
<Code
lang="bash"
lineNumbers
code={`\ngit clone https://github.com/appwrite/starter-for-${selectedFramework.key}\ncd starter-for-${selectedFramework.key}`} />
</div>
{#if selectedFramework.key === 'angular'}
<Typography.Text variant="m-500"
>3. Install project dependencies</Typography.Text>
<!-- Temporary fix: Remove this div once Code splitting issue with stack spacing is resolved -->
<div class="pink2-code-margin-fix">
<Code lang="bash" lineNumbers code={'npm install'} />
</div>
<Typography.Text variant="m-500"
>4. Run the app, then click the <InlineCode
>2. Change <InlineCode
size="s"
code="Send a ping" /> button to verify the setup.</Typography.Text>
<!-- Temporary fix: Remove this div once Code splitting issue with stack spacing is resolved -->
<div class="pink2-code-margin-fix">
<Code lang="bash" lineNumbers code={selectedFramework.runCommand} />
</div>
</Layout.Stack>
</Fieldset>
<Card.Base padding="s"
><Layout.Stack direction="row" justifyContent="space-between"
><Layout.Stack direction="row" alignItems="center">
<Icon
icon={IconInfo}
color="--fgcolor-neutral-tertiary" /><Typography.Text
variant="m-500"
color="--fgcolor-neutral-primary">
Demo app runs on http://localhost:{selectedFramework.portNumber}</Typography.Text
></Layout.Stack>
<Button
external
secondary
href={`http://localhost:${selectedFramework.portNumber}`}
><Layout.Stack direction="row" gap="xs"
>Open <Icon
icon={IconExternalLink}
color="--fgcolor-neutral-tertiary" /></Layout.Stack
></Button
></Layout.Stack
></Card.Base>
{/if}
</Layout.Stack>
</Form>
code="src/environments/environment.ts" />
to reflect the values below:</Typography.Text>
{:else}
<Typography.Text variant="m-500"
>2. Add your Appwrite credentials to <InlineCode
size="s"
code=".env.example" /> then rename it to <InlineCode
size="s"
code=".env" /> if needed.</Typography.Text>
{/if}
<!-- Temporary fix: Remove this div once Code splitting issue with stack spacing is resolved -->
<div class="pink2-code-margin-fix">
<Code lang="bash" lineNumbers code={selectedFramework.updateConfigCode} />
</div>
<Typography.Text variant="m-500"
>3. Install project dependencies</Typography.Text>
<!-- Temporary fix: Remove this div once Code splitting issue with stack spacing is resolved -->
<div class="pink2-code-margin-fix">
<Code lang="bash" lineNumbers code={'npm install'} />
</div>
<Typography.Text variant="m-500"
>4. Run the app, then click the <InlineCode size="s" code="Send a ping" /> button
to verify the setup.</Typography.Text>
<!-- Temporary fix: Remove this div once Code splitting issue with stack spacing is resolved -->
<div class="pink2-code-margin-fix">
<Code lang="bash" lineNumbers code={selectedFramework.runCommand} />
</div>
</Layout.Stack>
</Fieldset>
<Card.Base padding="s"
><Layout.Stack direction="row" justifyContent="space-between"
><Layout.Stack direction="row" alignItems="center">
<Icon icon={IconInfo} color="--fgcolor-neutral-tertiary" /><Typography.Text
variant="m-500"
color="--fgcolor-neutral-primary">
Demo app runs on http://localhost:{selectedFramework.portNumber}</Typography.Text
></Layout.Stack>
<Button
external
secondary
href={`http://localhost:${selectedFramework.portNumber}`}
><Layout.Stack direction="row" gap="xs"
>Open <Icon
icon={IconExternalLink}
color="--fgcolor-neutral-tertiary" /></Layout.Stack
></Button
></Layout.Stack
></Card.Base>
{/if}
</Layout.Stack>
<svelte:fragment slot="aside">
<Card.Base>
<Layout.Stack gap="xxl">
@@ -34,8 +34,14 @@
$: dbWrites = data.usage.databasesWrites;
$: legendData = [
{ name: 'Reads', value: data.usage.databasesReadsTotal },
{ name: 'Writes', value: data.usage.databasesWritesTotal }
{
name: 'Reads',
value: data.usage.databasesReads.reduce((sum, item) => sum + item.value, 0)
},
{
name: 'Writes',
value: data.usage.databasesWrites.reduce((sum, item) => sum + item.value, 0)
}
];
const tier = data?.currentInvoice?.plan ?? $organization?.billingPlan;
@@ -62,7 +62,7 @@
.forProject(page.params.region, page.params.project)
.vcs.createRepositoryDetection(
$installation.$id,
data.repository.id,
page.params.repository,
VCSDetectionType.Framework,
rootDir
);
@@ -5,11 +5,14 @@ import { buildVerboseDomain } from '../../store';
export const load = async ({ parent, params, url }) => {
const { installations, frameworks, project, organization, regionalConsoleVariables } =
await parent();
const [repository] = await Promise.all([
sdk
.forProject(params.region, params.project)
.vcs.getRepository(url.searchParams.get('installation'), params.repository)
]);
const repository = await sdk
.forProject(params.region, params.project)
.vcs.getRepository(url.searchParams.get('installation'), params.repository);
const installation = installations.installations.find(
(installation) => installation.$id === url.searchParams.get('installation')
);
const domain = await buildVerboseDomain(
regionalConsoleVariables._APP_DOMAIN_SITES,
@@ -21,9 +24,7 @@ export const load = async ({ parent, params, url }) => {
return {
installations,
installation: installations.installations.find(
(installation) => installation.$id === url.searchParams.get('installation')
),
installation,
repository,
frameworks,
domain