mirror of
https://github.com/appwrite/console.git
synced 2026-04-07 19:17:46 +00:00
svelte5: cleanups, improvements (#1966)
* update: cleanups, improvements. * fix: protocol usage. * update: load failed invoice as non-blocking. * address comments, make stuff faster! * fix: missing type on org loads. * fix: connect button shown on org. page. * fix: error check. * update: comment, just ocd things. * update: flip `localStorage` check after `browser` check. * update: comment. * perf: parallelize fetch for faster loads [↓49%] * update: compute things later. * update: don't rely on dependency or reload if scope doesn't allow. * update: simplify. * update: do not load credits if the organization plan does not support it. * update: just some cleaning. * address comments for promise handling. * address comments; remove: unused api calls!!! * address comments; * address comment. * misc. * cleaner endpoint creation. * fix: required to optional. * update: url to quick load. * address comments. * update: load payments on ui. * update: load non-urgent calls on UI. * fix: unnecessary runs of root layout. * reduce: calls to countries, locale api. * reduce: calls to countries, locale api in org settings. * update: simplify api calls and data passing. * address comments. * remove: id from `UsageProjectInfo`. * update: make api call only when needed. * fix: text. * fix: endpoint flag design <> verified by design. * fix: `inline` inner stack so project name has enough space. * fix: tests. * update: improve orgs loading? * update: misc. * remove: todo. * updates: more billing cleanups. * misc. * misc. * fix: tests and a warning. * address comment. * fix: merge leftover issues. * fix: tab selection on overview. * fix: issues with user store on main project layout. * remove: `selectedTab` store. * fix: account menu regression. * fix: reactive logic. use `svelte5` syntax. * Remove trailing comma in invalidate array * remove: unused method. * updates: misc optimizations. * fix: lint. * update: changes after merge. * update: modal size for credits and show loader. * update: misc and PR freeze! * update: use a default credit card icon if one isn't supported by the API. * fix: project loading state as per updated org ID. LAST FIX. * address comments. --------- Co-authored-by: Torsten Dittmann <torsten.dittmann@googlemail.com>
This commit is contained in:
+1
-1
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/svelte": "^1.1.24",
|
||||
"@appwrite.io/console": "^1.9.0",
|
||||
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@56743f5",
|
||||
"@appwrite.io/pink-icons": "0.25.0",
|
||||
"@appwrite.io/pink-icons-svelte": "^2.0.0-RC.1",
|
||||
"@appwrite.io/pink-legacy": "^1.0.3",
|
||||
|
||||
Generated
+10
-9
@@ -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: ^1.9.0
|
||||
version: 1.9.0
|
||||
specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@56743f5
|
||||
version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@56743f5
|
||||
'@appwrite.io/pink-icons':
|
||||
specifier: 0.25.0
|
||||
version: 0.25.0
|
||||
@@ -257,8 +257,9 @@ packages:
|
||||
'@analytics/type-utils@0.6.2':
|
||||
resolution: {integrity: sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg==}
|
||||
|
||||
'@appwrite.io/console@1.9.0':
|
||||
resolution: {integrity: sha512-g8+zfdBF8mz7tRUER4CGVe5FHWQVLp8TlY/UOGCZ2TgUF1qnpNu/DhiQgph8uF+QZ9jDKCLAAZlP5//9t7ckWA==}
|
||||
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@56743f5':
|
||||
resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@56743f5}
|
||||
version: 1.9.0
|
||||
|
||||
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@973fff20fff9995eff19c3f9c270cb676d2dde12':
|
||||
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@973fff20fff9995eff19c3f9c270cb676d2dde12}
|
||||
@@ -1348,8 +1349,8 @@ packages:
|
||||
'@types/prop-types@15.7.14':
|
||||
resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==}
|
||||
|
||||
'@types/react@18.3.23':
|
||||
resolution: {integrity: sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==}
|
||||
'@types/react@18.3.22':
|
||||
resolution: {integrity: sha512-vUhG0YmQZ7kL/tmKLrD3g5zXbXXreZXB3pmROW8bg3CnLnpjkRVwUlLne7Ufa2r9yJ8+/6B73RzhAek5TBKh2Q==}
|
||||
|
||||
'@types/remarkable@2.0.8':
|
||||
resolution: {integrity: sha512-eKXqPZfpQl1kOADjdKchHrp2gwn9qMnGXhH/AtZe0UrklzhGJkawJo/Y/D0AlWcdWoWamFNIum8+/nkAISQVGg==}
|
||||
@@ -3643,7 +3644,7 @@ snapshots:
|
||||
|
||||
'@analytics/type-utils@0.6.2': {}
|
||||
|
||||
'@appwrite.io/console@1.9.0': {}
|
||||
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@56743f5': {}
|
||||
|
||||
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@973fff20fff9995eff19c3f9c270cb676d2dde12(svelte@5.25.3)':
|
||||
dependencies:
|
||||
@@ -4807,7 +4808,7 @@ snapshots:
|
||||
|
||||
'@types/prop-types@15.7.14': {}
|
||||
|
||||
'@types/react@18.3.23':
|
||||
'@types/react@18.3.22':
|
||||
dependencies:
|
||||
'@types/prop-types': 15.7.14
|
||||
csstype: 3.1.3
|
||||
@@ -6768,7 +6769,7 @@ snapshots:
|
||||
|
||||
svelte-motion@0.12.2(svelte@5.25.3):
|
||||
dependencies:
|
||||
'@types/react': 18.3.23
|
||||
'@types/react': 18.3.22
|
||||
framesync: 6.1.2
|
||||
popmotion: 11.0.5
|
||||
style-value-types: 5.1.2
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
|
||||
export let methods: PaymentList;
|
||||
export let value: string;
|
||||
export let taxId = '';
|
||||
export let methods: PaymentList;
|
||||
|
||||
let showTaxId = false;
|
||||
let showPaymentModal = false;
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:show title="Add credits" onSubmit={addCoupon} bind:error>
|
||||
<Modal size="s" bind:show title="Add credits" onSubmit={addCoupon} bind:error>
|
||||
<svelte:fragment slot="description">
|
||||
Credits will be applied automatically to your next invoice.
|
||||
</svelte:fragment>
|
||||
@@ -62,6 +62,6 @@
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button submit disabled={coupon === ''}>Add</Button>
|
||||
<Button submissionLoader submit disabled={coupon === ''}>Add</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
{#if menu?.title}
|
||||
<span class="menu-title">{menu.title}</span>
|
||||
{/if}
|
||||
<ActionMenu.Root>
|
||||
<ActionMenu.Root width="100%">
|
||||
{#each menu.items as menuItem}
|
||||
{#if menuItem.href}
|
||||
<ActionMenu.Item.Anchor
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { createMenubar, melt } from '@melt-ui/svelte';
|
||||
import { Badge, Icon, type SheetMenu, ActionMenu, Card } from '@appwrite.io/pink-svelte';
|
||||
import {
|
||||
Badge,
|
||||
Icon,
|
||||
type SheetMenu,
|
||||
Layout,
|
||||
ActionMenu,
|
||||
Card,
|
||||
Skeleton
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconChevronRight,
|
||||
@@ -14,20 +22,13 @@
|
||||
import { base } from '$app/paths';
|
||||
import { currentPlan, newOrgModal } from '$lib/stores/organization';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { page } from '$app/stores';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
type Project = {
|
||||
name: string;
|
||||
$id: string;
|
||||
isSelected: boolean;
|
||||
region: string;
|
||||
};
|
||||
type Organization = {
|
||||
name: string;
|
||||
$id: string;
|
||||
tierName: string;
|
||||
isSelected: boolean;
|
||||
projects: Array<Project>;
|
||||
};
|
||||
|
||||
const {
|
||||
@@ -62,13 +63,15 @@
|
||||
}
|
||||
} = createMenu();
|
||||
|
||||
let isLoadingProjects = true;
|
||||
let loadedProjects: Models.ProjectList = { total: 0, projects: [] };
|
||||
|
||||
export let organizations: Organization[] = [];
|
||||
export let currentProject: Models.Project | null = null;
|
||||
export let projects: Promise<Models.ProjectList> = Promise.resolve(loadedProjects);
|
||||
|
||||
$: selectedOrg = organizations.find((organization) => organization.isSelected);
|
||||
$: selectedProject = $page.data.project;
|
||||
|
||||
let organisationBottomSheetOpen = false;
|
||||
let projectsBottomSheetOpen = false;
|
||||
let organisationBottomSheetOpen = false;
|
||||
|
||||
function createOrg() {
|
||||
trackEvent(Click.OrganizationClickCreate, { source: 'breadcrumbs' });
|
||||
@@ -96,85 +99,76 @@
|
||||
}
|
||||
};
|
||||
|
||||
$: organizationsBottomSheet = !selectedOrg
|
||||
? switchOrganization
|
||||
: ({
|
||||
top: {
|
||||
items: [
|
||||
{
|
||||
name: 'Organization overview',
|
||||
href: `${base}/organization-${selectedOrg?.$id}`
|
||||
}
|
||||
]
|
||||
},
|
||||
bottom:
|
||||
organizations.length > 1
|
||||
? {
|
||||
items: [
|
||||
{
|
||||
name: 'Switch organization',
|
||||
trailingIcon: IconChevronRight,
|
||||
subMenu: switchOrganization
|
||||
}
|
||||
]
|
||||
}
|
||||
: {
|
||||
items: [
|
||||
{
|
||||
name: 'Create organization',
|
||||
leadingIcon: IconPlus,
|
||||
onClick: createOrg
|
||||
}
|
||||
]
|
||||
}
|
||||
} satisfies SheetMenu);
|
||||
async function createProjectsBottomSheet(organization: Organization): Promise<SheetMenu> {
|
||||
isLoadingProjects = true;
|
||||
loadedProjects = await projects;
|
||||
isLoadingProjects = false;
|
||||
|
||||
$: projectsBottomSheet = {
|
||||
top:
|
||||
selectedOrg?.projects.length > 1
|
||||
? {
|
||||
title: 'Switch project',
|
||||
items: !selectedOrg
|
||||
? []
|
||||
: selectedOrg?.projects
|
||||
.map((project, index) => {
|
||||
if (index < 4) {
|
||||
return {
|
||||
name: project.name,
|
||||
href: `${base}/project-${project.region}-${project.$id}/overview`
|
||||
};
|
||||
} else if (index === 4) {
|
||||
return {
|
||||
name: 'All projects',
|
||||
href: `${base}/organization-${selectedOrg?.$id}`
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((project) => project !== null)
|
||||
}
|
||||
: {
|
||||
const createProjectItem = {
|
||||
name: 'Create project',
|
||||
trailingIcon: IconPlus,
|
||||
href: `${base}/organization-${organization?.$id}?create-project`
|
||||
};
|
||||
|
||||
if (loadedProjects.total > 1 && selectedOrg) {
|
||||
const projectLinks = loadedProjects.projects.slice(0, 4).map((project) => ({
|
||||
name: project.name,
|
||||
href: `${base}/project-${project.region}-${project.$id}/overview/platforms`
|
||||
}));
|
||||
|
||||
if (loadedProjects.projects.length > 4) {
|
||||
projectLinks.push({
|
||||
name: 'All projects',
|
||||
href: `${base}/organization-${selectedOrg.$id}`
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
top: { title: 'Switch project', items: projectLinks },
|
||||
bottom: { items: [createProjectItem] }
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
top: { items: [createProjectItem] },
|
||||
bottom: { items: [createProjectItem] }
|
||||
};
|
||||
}
|
||||
|
||||
function createOrganizationBottomSheet(organization: Organization) {
|
||||
return !organization
|
||||
? switchOrganization
|
||||
: ({
|
||||
top: {
|
||||
items: [
|
||||
{
|
||||
name: 'Create project',
|
||||
trailingIcon: IconPlus,
|
||||
href: `${base}/organization-${selectedOrg?.$id}?create-project`
|
||||
name: 'Organization overview',
|
||||
href: `${base}/organization-${organization?.$id}`
|
||||
}
|
||||
]
|
||||
},
|
||||
bottom:
|
||||
selectedOrg?.projects.length > 1
|
||||
? {
|
||||
items: [
|
||||
{
|
||||
name: 'Create project',
|
||||
trailingIcon: IconPlus,
|
||||
href: `${base}/organization-${selectedOrg?.$id}?create-project`
|
||||
}
|
||||
]
|
||||
}
|
||||
: undefined
|
||||
} satisfies SheetMenu;
|
||||
bottom:
|
||||
organizations.length > 1
|
||||
? {
|
||||
items: [
|
||||
{
|
||||
name: 'Switch organization',
|
||||
trailingIcon: IconChevronRight,
|
||||
subMenu: switchOrganization
|
||||
}
|
||||
]
|
||||
}
|
||||
: {
|
||||
items: [
|
||||
{
|
||||
name: 'Create organization',
|
||||
leadingIcon: IconPlus,
|
||||
onClick: createOrg
|
||||
}
|
||||
]
|
||||
}
|
||||
} satisfies SheetMenu);
|
||||
}
|
||||
|
||||
function onResize() {
|
||||
if ((organisationBottomSheetOpen || projectsBottomSheetOpen) && !$isSmallViewport) {
|
||||
@@ -183,6 +177,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: selectedOrg = organizations.find((org) => org.isSelected);
|
||||
|
||||
$: projectsBottomSheet = createProjectsBottomSheet(selectedOrg);
|
||||
|
||||
$: organizationsBottomSheet = createOrganizationBottomSheet(selectedOrg);
|
||||
|
||||
$: 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`
|
||||
@@ -191,7 +191,7 @@
|
||||
? $currentPlan.name
|
||||
: selectedOrg?.tierName; // fallback
|
||||
|
||||
$: derivedKey = `${selectedOrg?.$id}-${selectedProject?.$id}`;
|
||||
$: derivedKey = `${selectedOrg?.$id}-${currentProject?.$id}`;
|
||||
</script>
|
||||
|
||||
<svelte:window on:resize={onResize} />
|
||||
@@ -220,7 +220,7 @@
|
||||
organisationBottomSheetOpen = true;
|
||||
}}
|
||||
aria-label="Open organizations tab">
|
||||
<span class="orgName" class:noProjects={!selectedProject}
|
||||
<span class="orgName" class:noProjects={!currentProject}
|
||||
>{selectedOrg?.name ?? 'Organization'}</span>
|
||||
<span class="not-mobile"
|
||||
><Badge variant="secondary" content={correctPlanName ?? ''} /></span>
|
||||
@@ -302,7 +302,7 @@
|
||||
</Card.Base>
|
||||
</div>
|
||||
|
||||
{#if selectedOrg && selectedProject}
|
||||
{#if selectedOrg && currentProject}
|
||||
<span class="breadcrumb-separator">/</span>
|
||||
{#if !$isSmallViewport}
|
||||
<button
|
||||
@@ -310,7 +310,7 @@
|
||||
class="trigger"
|
||||
use:melt={$triggerProjects}
|
||||
aria-label="Open projects tab">
|
||||
<span class="projectName">{selectedProject.name}</span>
|
||||
<span class="projectName">{currentProject.name}</span>
|
||||
<Icon icon={IconChevronDown} size="s" />
|
||||
</button>
|
||||
{:else}
|
||||
@@ -319,20 +319,28 @@
|
||||
class="trigger"
|
||||
on:click={() => (projectsBottomSheetOpen = true)}
|
||||
aria-label="Open projects tab">
|
||||
<span class="projectName">{selectedProject.name}</span>
|
||||
<span class="projectName">{currentProject.name}</span>
|
||||
<Icon icon={IconChevronDown} size="s" />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<div class="menu" use:melt={$menuProjects}>
|
||||
<Card.Base padding="xxxs" shadow={true}>
|
||||
{#if selectedOrg.projects.length > 1}
|
||||
{#each selectedOrg.projects as project, index}
|
||||
{#if isLoadingProjects}
|
||||
<div style:margin-inline="0.25rem" style:margin-block="0.25rem">
|
||||
<Layout.Stack gap="s">
|
||||
<!-- 2 should be enough -->
|
||||
<Skeleton width="100%" height={30} variant="line" />
|
||||
<Skeleton width="100%" height={30} variant="line" />
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
{:else if loadedProjects.total > 1}
|
||||
{#each loadedProjects.projects as project, index}
|
||||
{#if index < 4}
|
||||
<div use:melt={$itemProjects}>
|
||||
<ActionMenu.Root>
|
||||
<ActionMenu.Item.Anchor
|
||||
href={`${base}/project-${project.region}-${project.$id}`}>
|
||||
href={`${base}/project-${project.region}-${project.$id}/overview/platforms`}>
|
||||
{project.name}
|
||||
</ActionMenu.Item.Anchor>
|
||||
</ActionMenu.Root>
|
||||
@@ -365,7 +373,9 @@
|
||||
|
||||
<BottomSheet.Menu bind:isOpen={organisationBottomSheetOpen} menu={organizationsBottomSheet} />
|
||||
|
||||
<BottomSheet.Menu bind:isOpen={projectsBottomSheetOpen} menu={projectsBottomSheet} />
|
||||
{#await projectsBottomSheet then menu}
|
||||
<BottomSheet.Menu bind:isOpen={projectsBottomSheetOpen} {menu} />
|
||||
{/await}
|
||||
{/key}
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -19,12 +19,12 @@
|
||||
} = $props();
|
||||
|
||||
let maxHeight = $state('none');
|
||||
let containerRef;
|
||||
let containerRef = $state<HTMLElement>(null);
|
||||
|
||||
const calcMaxHeight = () => {
|
||||
if (containerRef) {
|
||||
// get parent row element for correct top position
|
||||
const parent = containerRef.parentElement.parentElement;
|
||||
const parent = containerRef?.parentElement?.parentElement;
|
||||
const { top } = parent.getBoundingClientRect();
|
||||
|
||||
maxHeight = `${window.innerHeight - top - 48}px`;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
export let action: string = 'Delete';
|
||||
export let canDelete: boolean = true;
|
||||
export let disabled: boolean = false;
|
||||
export let submissionLoader = false;
|
||||
export let confirmDeletion: boolean = false;
|
||||
export let onSubmit: (e: SubmitEvent) => Promise<void> | void = function () {
|
||||
return;
|
||||
@@ -58,6 +59,7 @@
|
||||
<Button
|
||||
danger
|
||||
submit
|
||||
{submissionLoader}
|
||||
disabled={disabled || (confirmDeletion ? !confirm : false)}
|
||||
>{action}</Button>
|
||||
{/if}
|
||||
|
||||
@@ -43,8 +43,12 @@
|
||||
style:cursor="pointer"
|
||||
on:click|preventDefault|stopPropagation={handleClick}
|
||||
on:keyup={clickOnEnter}
|
||||
on:mouseenter={() => setTimeout(() => (content = 'Click to copy'))}>
|
||||
on:mouseenter={() => setTimeout(() => (content = copyText))}>
|
||||
<slot />
|
||||
</span>
|
||||
<p slot="tooltip">{content}</p>
|
||||
<p slot="tooltip" let:showing>
|
||||
{#if showing}
|
||||
{content}
|
||||
{/if}
|
||||
</p>
|
||||
</Tooltip>
|
||||
|
||||
@@ -2,15 +2,21 @@
|
||||
import { isValueOfStringEnum } from '$lib/helpers/types';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { CreditCard } from '@appwrite.io/console';
|
||||
import { Icon } from '@appwrite.io/pink-svelte';
|
||||
import { IconCreditCard } from '@appwrite.io/pink-icons-svelte';
|
||||
|
||||
export let brand: string;
|
||||
export let width = 23;
|
||||
export let height = 16;
|
||||
|
||||
function getCreditCardImage(brand: string, width = 46, height = 32) {
|
||||
if (!isValueOfStringEnum(CreditCard, brand)) return '';
|
||||
return sdk.forConsole.avatars.getCreditCard(brand, width, height);
|
||||
}
|
||||
$: ccImage = isValueOfStringEnum(CreditCard, brand)
|
||||
? sdk.forConsole.avatars.getCreditCard(brand, width, height)
|
||||
: '';
|
||||
</script>
|
||||
|
||||
<img style="border-radius: 2.5px" {width} {height} src={getCreditCardImage(brand)} alt={brand} />
|
||||
{#if ccImage}
|
||||
<img alt={brand} src={ccImage} {width} {height} style:border-radius="2.5px" />
|
||||
{:else}
|
||||
<!-- fallback: unionpay image not in Avatars API -->
|
||||
<Icon icon={IconCreditCard} color="--fgcolor-neutral-tertiary" />
|
||||
{/if}
|
||||
|
||||
@@ -50,15 +50,17 @@
|
||||
export let event: string = null;
|
||||
</script>
|
||||
|
||||
<Copy {value} {event}>
|
||||
<Tag size="xs" variant="code">
|
||||
<Icon icon={IconDuplicate} size="s" slot="start" />
|
||||
<span
|
||||
style:white-space="nowrap"
|
||||
style:overflow="hidden"
|
||||
style:word-break="break-all"
|
||||
use:truncateText>
|
||||
<slot />
|
||||
</span>
|
||||
</Tag>
|
||||
</Copy>
|
||||
{#key value}
|
||||
<Copy {value} {event}>
|
||||
<Tag size="xs" variant="code">
|
||||
<Icon icon={IconDuplicate} size="s" slot="start" />
|
||||
<span
|
||||
style:white-space="nowrap"
|
||||
style:overflow="hidden"
|
||||
style:word-break="break-all"
|
||||
use:truncateText>
|
||||
<slot />
|
||||
</span>
|
||||
</Tag>
|
||||
</Copy>
|
||||
{/key}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { Alert, Layout, Modal } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let show = false;
|
||||
export let autoClose = true;
|
||||
export let error: string = null;
|
||||
export let dismissible = true;
|
||||
export let size: 's' | 'm' | 'l' = 'm';
|
||||
@@ -17,7 +18,7 @@
|
||||
let alert: HTMLElement;
|
||||
|
||||
beforeNavigate(() => {
|
||||
show = false;
|
||||
if (autoClose) show = false;
|
||||
});
|
||||
|
||||
$: $disableCommands(show);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts" context="module">
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
export type NavbarProject = {
|
||||
name: string;
|
||||
$id: string;
|
||||
@@ -53,8 +55,9 @@
|
||||
import { isCloud } from '$lib/system.js';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { beforeNavigate } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
let showSupport = false;
|
||||
|
||||
@@ -66,7 +69,6 @@
|
||||
isSelected: boolean;
|
||||
showUpgrade: boolean;
|
||||
tierName: string;
|
||||
projects: Array<NavbarProject>;
|
||||
}>;
|
||||
showAccountMenu: boolean;
|
||||
};
|
||||
@@ -103,6 +105,8 @@
|
||||
export let avatar: $$Props['avatar'];
|
||||
export let sideBarIsOpen: $$Props['sideBarIsOpen'] = false;
|
||||
export let showAccountMenu = false;
|
||||
export let currentProject: Models.Project = undefined;
|
||||
export let projects: Promise<Models.ProjectList> = undefined;
|
||||
|
||||
let activeTheme = $app.theme;
|
||||
let shouldAnimateThemeToggle = false;
|
||||
@@ -114,7 +118,7 @@
|
||||
}
|
||||
|
||||
$: currentOrg = organizations.find((org) => org.isSelected);
|
||||
$: selectedProject = currentOrg?.projects.find((project) => project.isSelected);
|
||||
|
||||
beforeNavigate(() => (showAccountMenu = false));
|
||||
</script>
|
||||
|
||||
@@ -134,13 +138,14 @@
|
||||
class="only-desktop">
|
||||
<img src={logo.src} alt={logo.alt} />
|
||||
</a>
|
||||
<Breadcrumbs {organizations} />
|
||||
{#if selectedProject && selectedProject.pingCount === 0}
|
||||
<Breadcrumbs {organizations} {projects} {currentProject} />
|
||||
{#if page.route?.id?.includes('/project-[region]-[project]') && currentProject && currentProject.pingCount === 0}
|
||||
<div class="only-desktop" style:margin-inline-start="-16px">
|
||||
<Button.Anchor
|
||||
href={`${base}/project-${selectedProject.region}-${selectedProject.$id}/get-started`}
|
||||
size="xs"
|
||||
variant="secondary"
|
||||
size="xs">Connect</Button.Anchor>
|
||||
href={`${base}/project-${currentProject.region}-${currentProject.$id}/get-started`}
|
||||
>Connect</Button.Anchor>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -222,7 +227,7 @@
|
||||
style:user-select="none">
|
||||
<Avatar size="s" src={avatar} />
|
||||
</button>
|
||||
<svelte:fragment slot="tooltip">
|
||||
<svelte:fragment slot="tooltip" let:toggle>
|
||||
<ActionMenu.Root noPadding>
|
||||
<Layout.Stack gap="xxs">
|
||||
<div
|
||||
@@ -234,9 +239,10 @@
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<ActionMenu.Item.Anchor
|
||||
trailingIcon={IconUser}
|
||||
size="l"
|
||||
href={`${base}/account`}>
|
||||
trailingIcon={IconUser}
|
||||
href={`${base}/account`}
|
||||
on:click={() => toggle()}>
|
||||
Account</ActionMenu.Item.Anchor>
|
||||
|
||||
<ActionMenu.Item.Button
|
||||
|
||||
@@ -1,42 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { Copy } from '.';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Flag } from '@appwrite.io/console';
|
||||
import { Layout, Tag } from '@appwrite.io/pink-svelte';
|
||||
import { Flag, type Models } from '@appwrite.io/console';
|
||||
import { truncateText } from '$lib/components/id.svelte';
|
||||
import { isValueOfStringEnum } from '$lib/helpers/types';
|
||||
import { getProjectEndpoint } from '$lib/helpers/project';
|
||||
import { projectRegion } from '$routes/(console)/project-[region]-[project]/store';
|
||||
|
||||
export let region: Models.ConsoleRegion;
|
||||
|
||||
$: flagSrc =
|
||||
$projectRegion && isValueOfStringEnum(Flag, $projectRegion.flag)
|
||||
? sdk.forConsole.avatars.getFlag($projectRegion.flag, 30, 20, 100)
|
||||
region && isValueOfStringEnum(Flag, region.flag)
|
||||
? sdk.forConsole.avatars.getFlag(region.flag, 30, 20, 100)
|
||||
: '';
|
||||
</script>
|
||||
|
||||
{#if $projectRegion}
|
||||
{#if region}
|
||||
<Copy value={getProjectEndpoint()} copyText="Copy endpoint">
|
||||
<div
|
||||
class="flex u-gap-8 u-cross-center interactive-text-output is-buttons-on-top u-text-center"
|
||||
style:min-inline-size="0"
|
||||
style:display="inline-flex">
|
||||
<span
|
||||
style:white-space="nowrap"
|
||||
class="text u-line-height-1-5"
|
||||
style:overflow="hidden"
|
||||
style:word-break="break-all"
|
||||
use:truncateText
|
||||
style:font-family="unset">
|
||||
{$projectRegion?.name}
|
||||
</span>
|
||||
<Tag size="xs" variant="default">
|
||||
<Layout.Stack direction="row" gap="s" alignItems="center" inline>
|
||||
{#if flagSrc}
|
||||
<img
|
||||
width={16}
|
||||
height={12}
|
||||
src={flagSrc}
|
||||
alt={region?.name}
|
||||
style:border-radius="2.5px" />
|
||||
{/if}
|
||||
|
||||
{#if flagSrc}
|
||||
<img
|
||||
style="border-radius: 2.5px"
|
||||
src={flagSrc}
|
||||
alt={$projectRegion?.name}
|
||||
width={16}
|
||||
height={12} />
|
||||
{/if}
|
||||
</div>
|
||||
<span
|
||||
style:white-space="nowrap"
|
||||
class="text u-line-height-1-5"
|
||||
style:overflow="hidden"
|
||||
style:word-break="break-all"
|
||||
use:truncateText
|
||||
style:font-family="unset">
|
||||
{region?.name}
|
||||
</span>
|
||||
</Layout.Stack>
|
||||
</Tag>
|
||||
</Copy>
|
||||
{/if}
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
<Layout.Stack direction="column" gap="s">
|
||||
<Tooltip placement="right" disabled={state !== 'icons'}>
|
||||
<a
|
||||
href={`/console/project-${project.region}-${project.$id}/overview`}
|
||||
href={`/console/project-${project.region}-${project.$id}/overview/platforms`}
|
||||
class="link"
|
||||
class:active={page.url.pathname.includes('overview')}
|
||||
on:click={() => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { getContext, hasContext } from 'svelte';
|
||||
import { readable } from 'svelte/store';
|
||||
import type { FormContext } from './form.svelte';
|
||||
import { Button } from '@appwrite.io/pink-svelte';
|
||||
import { Button, Spinner } from '@appwrite.io/pink-svelte';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
type Props = ComponentProps<Button.Button | Button.Anchor>;
|
||||
@@ -115,10 +115,7 @@
|
||||
type={submit ? 'submit' : 'button'}>
|
||||
<slot name="start" slot="start" />
|
||||
{#if ($isSubmitting && submissionLoader) || (forceShowLoader && submissionLoader)}
|
||||
<span
|
||||
class="loader is-small"
|
||||
style:--p-loader-base-full-color="transparent"
|
||||
aria-hidden="true"></span>
|
||||
<Spinner size="s" />
|
||||
{/if}
|
||||
<slot isSubmitting={$isSubmitting} />
|
||||
<slot slot="end" name="end" />
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { get } from 'svelte/store';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { Account } from '$lib/stores/user';
|
||||
|
||||
const userPreferences = () => get(user)?.prefs;
|
||||
|
||||
export function hasOnboardingDismissed(projectId: string) {
|
||||
const currentPrefs = userPreferences();
|
||||
export function hasOnboardingDismissed(projectId: string, account?: Account) {
|
||||
const currentPrefs = account?.prefs;
|
||||
const onboardingDismissed = currentPrefs?.onboardingDismissed;
|
||||
return (
|
||||
onboardingDismissed &&
|
||||
@@ -14,8 +11,8 @@ export function hasOnboardingDismissed(projectId: string) {
|
||||
);
|
||||
}
|
||||
|
||||
export async function setHasOnboardingDismissed(projectId: string) {
|
||||
const currentPrefs = userPreferences();
|
||||
export async function setHasOnboardingDismissed(projectId: string, account?: Account) {
|
||||
const currentPrefs = account?.prefs;
|
||||
const onboardingDismissed = Array.isArray(currentPrefs?.onboardingDismissed)
|
||||
? currentPrefs.onboardingDismissed
|
||||
: [];
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { beforeNavigate } from '$app/navigation';
|
||||
import { Navbar, Sidebar } from '$lib/components';
|
||||
import type { NavbarProject } from '$lib/components/navbar.svelte';
|
||||
import { isNewWizardStatusOpen, wizard } from '$lib/stores/wizard';
|
||||
import { activeHeaderAlert } from '$routes/(console)/store';
|
||||
import { setContext } from 'svelte';
|
||||
@@ -19,11 +18,11 @@
|
||||
import { page } from '$app/stores';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let showSideNavigation = false;
|
||||
export let showHeader = true;
|
||||
export let showFooter = true;
|
||||
export let loadedProjects: Array<NavbarProject> = [];
|
||||
export let selectedProject: Models.Project | null = null;
|
||||
export let showSideNavigation = false;
|
||||
export let selectedProject: Models.Project = null;
|
||||
export let projects: Promise<Models.ProjectList> = undefined;
|
||||
|
||||
let yOnMenuOpen: number;
|
||||
let showContentTransition = false;
|
||||
@@ -101,10 +100,13 @@
|
||||
$id: org.$id,
|
||||
showUpgrade: billingPlan === BillingPlan.FREE,
|
||||
tierName: isCloud ? tierToPlan(billingPlan).name : null,
|
||||
isSelected: $organization?.$id === org.$id,
|
||||
projects: loadedProjects
|
||||
isSelected: $organization?.$id === org.$id
|
||||
};
|
||||
})
|
||||
}),
|
||||
|
||||
projects: projects,
|
||||
|
||||
currentProject: selectedProject
|
||||
};
|
||||
|
||||
let showAccountMenu = false;
|
||||
@@ -121,7 +123,7 @@
|
||||
}
|
||||
|
||||
const progressCard = function getProgressCard() {
|
||||
if (selectedProject && !hasOnboardingDismissed(selectedProject.$id)) {
|
||||
if (selectedProject && !hasOnboardingDismissed(selectedProject.$id, $user)) {
|
||||
return {
|
||||
title: 'Get started',
|
||||
percentage: selectedProject && selectedProject.platforms.length ? 100 : 33
|
||||
|
||||
@@ -151,6 +151,10 @@ export type CreditList = {
|
||||
total: number;
|
||||
};
|
||||
|
||||
export type AvailableCredit = {
|
||||
available: number;
|
||||
};
|
||||
|
||||
export type Aggregation = {
|
||||
$id: string;
|
||||
/**
|
||||
@@ -859,6 +863,20 @@ export class Billing {
|
||||
);
|
||||
}
|
||||
|
||||
async getAvailableCredit(organizationId: string): Promise<AvailableCredit> {
|
||||
const path = `/organizations/${organizationId}/credits/available`;
|
||||
const params = {};
|
||||
const uri = new URL(this.client.config.endpoint + path);
|
||||
return await this.client.call(
|
||||
'GET',
|
||||
uri,
|
||||
{
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
params
|
||||
);
|
||||
}
|
||||
|
||||
async getCredit(organizationId: string, creditId: string): Promise<Credit> {
|
||||
const path = `/organizations/${organizationId}/credits/${creditId}`;
|
||||
const params = {
|
||||
|
||||
+26
-10
@@ -24,7 +24,7 @@ import type {
|
||||
} from '$lib/sdk/billing';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { activeHeaderAlert, orgMissingPaymentMethod } from '$routes/(console)/store';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
import { AppwriteException, Query } from '@appwrite.io/console';
|
||||
import { derived, get, writable } from 'svelte/store';
|
||||
import { headerAlert } from './headerAlert';
|
||||
import { addNotification, notifications } from './notifications';
|
||||
@@ -524,25 +524,41 @@ export async function checkForMissingPaymentMethod() {
|
||||
|
||||
// Display upgrade banner for new users after 1 week for 30 days
|
||||
export async function checkForNewDevUpgradePro(org: Organization) {
|
||||
if (org?.billingPlan !== BillingPlan.FREE || !browser) return;
|
||||
// browser or plan check.
|
||||
if (!browser || org?.billingPlan !== BillingPlan.FREE) return;
|
||||
|
||||
const orgs = await sdk.forConsole.billing.listOrganization([
|
||||
Query.notEqual('billingPlan', BillingPlan.FREE)
|
||||
]);
|
||||
if (orgs?.total) return;
|
||||
// already dismissed by user!
|
||||
if (localStorage.getItem('newDevUpgradePro')) return;
|
||||
|
||||
// saves one trip to backend!
|
||||
const notValidKey = `${org.$id}:isNotValid`;
|
||||
if (localStorage.getItem(notValidKey)) return;
|
||||
|
||||
const now = new Date().getTime();
|
||||
const account = get(user);
|
||||
const accountCreated = new Date(account.$createdAt).getTime();
|
||||
if (now - accountCreated < 1000 * 60 * 60 * 24 * 7) return;
|
||||
const isDismissed = !!localStorage.getItem('newDevUpgradePro');
|
||||
if (isDismissed) return;
|
||||
// check if coupon already applied
|
||||
|
||||
const organizations = await sdk.forConsole.billing.listOrganization([
|
||||
Query.notEqual('billingPlan', BillingPlan.FREE)
|
||||
]);
|
||||
|
||||
if (organizations?.total) return;
|
||||
|
||||
try {
|
||||
await sdk.forConsole.billing.getCouponAccount(NEW_DEV_PRO_UPGRADE_COUPON);
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
if (
|
||||
// already utilized if error is 409
|
||||
error instanceof AppwriteException &&
|
||||
error?.code === 409 &&
|
||||
error.type === 'billing_coupon_already_used'
|
||||
) {
|
||||
localStorage.setItem(notValidKey, 'true');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
headerAlert.add({
|
||||
id: 'newDevUpgradePro',
|
||||
component: newDevUpgradePro,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { page } from '$app/state';
|
||||
import { sdk } from './sdk';
|
||||
import { getApiEndpoint } from './sdk';
|
||||
|
||||
export function connectGitHub(callbackState: Record<string, string> = null) {
|
||||
const redirect = new URL(page.url);
|
||||
@@ -8,9 +8,7 @@ export function connectGitHub(callbackState: Record<string, string> = null) {
|
||||
redirect.searchParams.append(key, callbackState[key]);
|
||||
});
|
||||
}
|
||||
const target = new URL(
|
||||
`${sdk.forProject(page.params.region, page.params.project).client.config.endpoint}/vcs/github/authorize`
|
||||
);
|
||||
const target = new URL(`${getApiEndpoint(page.params.region)}/vcs/github/authorize`);
|
||||
target.searchParams.set('project', page.params.project);
|
||||
target.searchParams.set('success', redirect.toString());
|
||||
target.searchParams.set('failure', redirect.toString());
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
import Footer from '$lib/layout/footer.svelte';
|
||||
import Shell from '$lib/layout/shell.svelte';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { newOrgModal, organization, type Organization } from '$lib/stores/organization';
|
||||
import { database, checkForDatabaseBackupPolicies } from '$lib/stores/database';
|
||||
import { newOrgModal, organization, type Organization } from '$lib/stores/organization';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { afterUpdate, onMount } from 'svelte';
|
||||
import { loading } from '$routes/store';
|
||||
@@ -54,7 +54,10 @@
|
||||
IconSwitchHorizontal
|
||||
} from '@appwrite.io/pink-icons-svelte';
|
||||
import type { LayoutData } from './$types';
|
||||
import type { NavbarProject } from '$lib/components/navbar.svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
|
||||
export let data: LayoutData;
|
||||
|
||||
function kebabToSentenceCase(str: string) {
|
||||
return str
|
||||
@@ -63,20 +66,7 @@
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
const isAssistantEnabled = $consoleVariables?._APP_ASSISTANT_ENABLED === true;
|
||||
|
||||
export let data: LayoutData;
|
||||
|
||||
$: loadedProjects = data.projects.map((project) => {
|
||||
return {
|
||||
name: project?.name,
|
||||
$id: project.$id,
|
||||
isSelected: project.$id === page.params.project,
|
||||
region: project.region,
|
||||
platformCount: project.platforms.length,
|
||||
pingCount: project.pingCount
|
||||
};
|
||||
}) satisfies NavbarProject[];
|
||||
$: isAssistantEnabled = $consoleVariables?._APP_ASSISTANT_ENABLED === true;
|
||||
|
||||
$: isOnSettingsLayout = $project?.$id
|
||||
? page.url.pathname.includes(`project-${$project.region}-${$project.$id}/settings`)
|
||||
@@ -263,6 +253,7 @@
|
||||
rank: -1
|
||||
}
|
||||
]);
|
||||
|
||||
onMount(async () => {
|
||||
loading.set(false);
|
||||
if (!localStorage.getItem('feedbackElapsed')) {
|
||||
@@ -325,6 +316,16 @@
|
||||
|
||||
$: checkForUsageLimits($organization);
|
||||
|
||||
$: projects = sdk.forConsole.projects.list([
|
||||
Query.equal(
|
||||
'teamId',
|
||||
// id from page params ?? id from store ?? id from preferences
|
||||
page.params.organization ?? currentOrganizationId ?? data.currentOrgId
|
||||
),
|
||||
Query.limit(5),
|
||||
Query.orderDesc('$updatedAt')
|
||||
]);
|
||||
|
||||
$: if ($requestedMigration) {
|
||||
openMigrationWizard();
|
||||
}
|
||||
@@ -345,7 +346,7 @@
|
||||
!page.url.pathname.includes('/console/onboarding')}
|
||||
showHeader={!page.url.pathname.includes('/console/onboarding/create-project')}
|
||||
showFooter={!page.url.pathname.includes('/console/onboarding/create-project')}
|
||||
{loadedProjects}
|
||||
{projects}
|
||||
selectedProject={page.data?.project}>
|
||||
<!-- <Header slot="header" />-->
|
||||
<slot />
|
||||
|
||||
@@ -1,71 +1,54 @@
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import type { Plan } from '$lib/sdk/billing';
|
||||
import type { Tier } from '$lib/stores/billing';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { isCloud } from '$lib/system';
|
||||
import type { LayoutLoad } from './$types';
|
||||
import { Query, type Models } from '@appwrite.io/console';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import type { Tier } from '$lib/stores/billing';
|
||||
import type { Plan, PlanList } from '$lib/sdk/billing';
|
||||
|
||||
export const load: LayoutLoad = async ({ depends, parent }) => {
|
||||
const { organizations } = await parent();
|
||||
|
||||
export const load: LayoutLoad = async ({ params, fetch, depends, parent }) => {
|
||||
await parent();
|
||||
depends(Dependencies.RUNTIMES);
|
||||
depends(Dependencies.CONSOLE_VARIABLES);
|
||||
depends(Dependencies.ORGANIZATION);
|
||||
|
||||
const prefs = await sdk.forConsole.account.getPrefs();
|
||||
|
||||
const { endpoint, project } = sdk.forConsole.client.config;
|
||||
const versionPromise = fetch(`${endpoint}/health/version`, {
|
||||
headers: {
|
||||
'X-Appwrite-Project': project
|
||||
}
|
||||
}).then((response) => response.json() as { version?: string });
|
||||
const [preferences, plansArray, versionData, consoleVariables] = await Promise.all([
|
||||
sdk.forConsole.account.getPrefs(),
|
||||
isCloud ? sdk.forConsole.billing.getPlansInfo() : null,
|
||||
fetch(`${endpoint}/health/version`, {
|
||||
headers: { 'X-Appwrite-Project': project }
|
||||
}).then((response) => response.json() as { version?: string }),
|
||||
sdk.forConsole.console.variables()
|
||||
]);
|
||||
|
||||
const variablesPromise = sdk.forConsole.console.variables();
|
||||
const plansInfo = toPlanMap(plansArray);
|
||||
|
||||
const [data, variables] = await Promise.all([versionPromise, variablesPromise]);
|
||||
|
||||
let plansInfo = new Map<Tier, Plan>();
|
||||
if (isCloud) {
|
||||
const plansArray = await sdk.forConsole.billing.getPlansInfo();
|
||||
plansInfo = plansArray.plans.reduce((map, plan) => {
|
||||
map.set(plan.$id as Tier, plan);
|
||||
return map;
|
||||
}, new Map<Tier, Plan>());
|
||||
}
|
||||
|
||||
const organizations = !isCloud
|
||||
? await sdk.forConsole.teams.list()
|
||||
: await sdk.forConsole.billing.listOrganization();
|
||||
|
||||
let projects: Models.Project[] = [];
|
||||
let currentOrgId = params.organization ? params.organization : prefs.organization;
|
||||
|
||||
if (!currentOrgId && organizations.teams.length > 0) {
|
||||
currentOrgId = organizations.teams[0].$id;
|
||||
}
|
||||
if (currentOrgId) {
|
||||
const orgProjects = await sdk.forConsole.projects.list([
|
||||
Query.equal('teamId', currentOrgId),
|
||||
Query.limit(5),
|
||||
Query.orderDesc('$updatedAt')
|
||||
]);
|
||||
projects = orgProjects.projects.length > 0 ? orgProjects.projects : [];
|
||||
}
|
||||
|
||||
// set `default` if no region!
|
||||
for (const project of projects) {
|
||||
project.region ??= 'default';
|
||||
}
|
||||
const currentOrgId =
|
||||
preferences.organization ??
|
||||
(organizations.teams.length > 0 ? organizations.teams[0].$id : undefined);
|
||||
|
||||
return {
|
||||
consoleVariables: variables,
|
||||
version: data?.version ?? null,
|
||||
plansInfo,
|
||||
roles: [],
|
||||
scopes: [],
|
||||
projects: projects,
|
||||
currentProjectId: params.project ?? '',
|
||||
organizations: organizations
|
||||
preferences,
|
||||
currentOrgId,
|
||||
organizations,
|
||||
consoleVariables,
|
||||
version: versionData?.version ?? null
|
||||
};
|
||||
};
|
||||
|
||||
function toPlanMap(plansArray: PlanList | null): Map<Tier, Plan> {
|
||||
const map = new Map<Tier, Plan>();
|
||||
if (!plansArray?.plans.length) return map;
|
||||
|
||||
const plans = plansArray.plans;
|
||||
for (let i = 0; i < plans.length; i++) {
|
||||
const plan = plans[i];
|
||||
map.set(plan.$id as Tier, plan);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
import { page } from '$app/state';
|
||||
import { confirmPayment } from '$lib/stores/stripe';
|
||||
import { Typography } from '@appwrite.io/pink-svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let showPayment = false;
|
||||
|
||||
@@ -21,5 +24,5 @@
|
||||
<Container>
|
||||
<Typography.Title size="s">Payment details</Typography.Title>
|
||||
<PaymentMethods bind:showPayment />
|
||||
<BillingAddress />
|
||||
<BillingAddress {data} />
|
||||
</Container>
|
||||
|
||||
@@ -6,12 +6,17 @@ export const load: PageLoad = async ({ depends }) => {
|
||||
depends(Dependencies.PAYMENT_METHODS);
|
||||
depends(Dependencies.ADDRESS);
|
||||
|
||||
const [paymentMethods, addressList] = await Promise.all([
|
||||
const [paymentMethods, addressList, countryList, locale] = await Promise.all([
|
||||
sdk.forConsole.billing.listPaymentMethods(),
|
||||
sdk.forConsole.billing.listAddresses()
|
||||
sdk.forConsole.billing.listAddresses(),
|
||||
sdk.forConsole.locale.listCountries(),
|
||||
sdk.forConsole.locale.get()
|
||||
]);
|
||||
|
||||
return {
|
||||
paymentMethods,
|
||||
addressList
|
||||
addressList,
|
||||
countryList,
|
||||
locale
|
||||
};
|
||||
};
|
||||
|
||||
@@ -8,9 +8,12 @@
|
||||
import type { Organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let show = false;
|
||||
export let locale: Models.Locale;
|
||||
export let organization: string = null;
|
||||
export let countryList: Models.CountryList;
|
||||
|
||||
let country: string;
|
||||
let address: string;
|
||||
@@ -27,8 +30,6 @@
|
||||
let error: string = null;
|
||||
|
||||
onMount(async () => {
|
||||
const countryList = await sdk.forConsole.locale.listCountries();
|
||||
const locale = await sdk.forConsole.locale.get();
|
||||
if (locale.countryCode) {
|
||||
country = locale.countryCode;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
import { CardGrid, Empty } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { addressList } from '$lib/stores/billing';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import AddressModal from './addressModal.svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import DeleteAddress from './deleteAddressModal.svelte';
|
||||
@@ -28,17 +26,18 @@
|
||||
Tag,
|
||||
Typography
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
const locale: Models.Locale = data.locale;
|
||||
const countryList: Models.CountryList = data.countryList;
|
||||
|
||||
let show = false;
|
||||
let showEdit = false;
|
||||
let selectedAddress: Address;
|
||||
let selectedLinkedOrgs: Organization[] = [];
|
||||
let showDelete = false;
|
||||
let countryList: Models.CountryList;
|
||||
|
||||
onMount(async () => {
|
||||
countryList = await sdk.forConsole.locale.listCountries();
|
||||
});
|
||||
|
||||
$: orgList = $organizationList.teams as unknown as Organization[];
|
||||
</script>
|
||||
@@ -144,6 +143,6 @@
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
<AddressModal bind:show />
|
||||
<EditAddressModal bind:show={showEdit} {selectedAddress} />
|
||||
<AddressModal {locale} {countryList} bind:show />
|
||||
<EditAddressModal bind:show={showEdit} {selectedAddress} {locale} {countryList} />
|
||||
<DeleteAddress bind:showDelete {selectedAddress} linkedOrgs={selectedLinkedOrgs} />
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { Modal } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
@@ -9,9 +8,12 @@
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let show = false;
|
||||
export let selectedAddress: Address;
|
||||
export let locale: Models.Locale;
|
||||
export let countryList: Models.CountryList;
|
||||
|
||||
let error: string = null;
|
||||
let options = [
|
||||
@@ -22,7 +24,6 @@
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
const countryList = await sdk.forConsole.locale.listCountries();
|
||||
options = countryList.countries.map((country) => {
|
||||
return {
|
||||
value: country.code,
|
||||
@@ -63,13 +64,15 @@
|
||||
* which really shouldn't happen, but here we are, playing it safe!
|
||||
*/
|
||||
$: if (show && !selectedAddress.country) {
|
||||
sdk.forProject(page.params.region, page.params.project)
|
||||
.locale.get()
|
||||
.then((locale) => {
|
||||
if (locale) {
|
||||
selectedAddress.country = locale.countryCode;
|
||||
} else {
|
||||
sdk.forConsole.locale.get().then((locale) => {
|
||||
if (locale.countryCode) {
|
||||
selectedAddress.country = locale.countryCode;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ export const load: PageLoad = async ({ url, parent, depends }) => {
|
||||
const hasFreeOrganizations = organizations.teams?.some(
|
||||
(org) => (org as Organization)?.billingPlan === BillingPlan.FREE
|
||||
);
|
||||
|
||||
if (plan === BillingPlan.FREE && hasFreeOrganizations) {
|
||||
plan = BillingPlan.PRO;
|
||||
}
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
import { openMigrationWizard } from '../(migration-wizard)';
|
||||
import { base } from '$app/paths';
|
||||
import { isOwner } from '$lib/stores/roles';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data;
|
||||
export let data: PageData;
|
||||
|
||||
$: if ($requestedMigration) {
|
||||
openMigrationWizard();
|
||||
|
||||
@@ -10,51 +10,47 @@ import { headerAlert } from '$lib/stores/headerAlert';
|
||||
import ProjectsAtRisk from '$lib/components/billing/alerts/projectsAtRisk.svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import { preferences } from '$lib/stores/preferences';
|
||||
import type { Organization } from '$lib/stores/organization';
|
||||
import { defaultRoles, defaultScopes } from '$lib/constants';
|
||||
import type { Plan } from '$lib/sdk/billing';
|
||||
import { loadAvailableRegions } from '$routes/(console)/regions';
|
||||
import type { Organization } from '$lib/stores/organization';
|
||||
|
||||
export const load: LayoutLoad = async ({ params, depends, parent }) => {
|
||||
const { preferences: prefs } = await parent();
|
||||
|
||||
export const load: LayoutLoad = async ({ params, depends }) => {
|
||||
depends(Dependencies.ORGANIZATION);
|
||||
depends(Dependencies.MEMBERS);
|
||||
depends(Dependencies.PAYMENT_METHODS);
|
||||
|
||||
let roles = isCloud ? [] : defaultRoles;
|
||||
let scopes = isCloud ? [] : defaultScopes;
|
||||
let currentPlan: Plan = null;
|
||||
|
||||
try {
|
||||
if (isCloud) {
|
||||
const res = await sdk.forConsole.billing.getRoles(params.organization);
|
||||
roles = res.roles;
|
||||
scopes = res.scopes;
|
||||
currentPlan = await sdk.forConsole.billing.getOrganizationPlan(params.organization);
|
||||
[{ roles, scopes }, currentPlan] = await Promise.all([
|
||||
sdk.forConsole.billing.getRoles(params.organization),
|
||||
sdk.forConsole.billing.getOrganizationPlan(params.organization)
|
||||
]);
|
||||
|
||||
if (scopes.includes('billing.read')) {
|
||||
await failedInvoice.load(params.organization);
|
||||
if (get(failedInvoice)) {
|
||||
headerAlert.add({
|
||||
show: true,
|
||||
component: ProjectsAtRisk,
|
||||
id: 'projectsAtRisk',
|
||||
importance: 1
|
||||
});
|
||||
}
|
||||
loadFailedInvoices(params.organization);
|
||||
}
|
||||
}
|
||||
const prefs = await sdk.forConsole.account.getPrefs();
|
||||
if (prefs.organization !== params.organization) {
|
||||
const newPrefs = { ...prefs, organization: params.organization };
|
||||
sdk.forConsole.account.updatePrefs(newPrefs);
|
||||
}
|
||||
|
||||
const [organization, members] = await Promise.all([
|
||||
const [organization, members, countryList, locale] = await Promise.all([
|
||||
sdk.forConsole.teams.get(params.organization) as Promise<Organization>,
|
||||
sdk.forConsole.teams.listMemberships(params.organization),
|
||||
preferences.loadTeamPrefs(params.organization)
|
||||
sdk.forConsole.locale.listCountries(),
|
||||
sdk.forConsole.locale.get(),
|
||||
preferences.loadTeamPrefs(params.organization),
|
||||
loadAvailableRegions(params.organization)
|
||||
]);
|
||||
|
||||
await loadAvailableRegions(params.organization);
|
||||
|
||||
return {
|
||||
header: Header,
|
||||
breadcrumbs: Breadcrumbs,
|
||||
@@ -62,12 +58,27 @@ export const load: LayoutLoad = async ({ params, depends }) => {
|
||||
currentPlan,
|
||||
members,
|
||||
roles,
|
||||
scopes
|
||||
scopes,
|
||||
countryList,
|
||||
locale
|
||||
};
|
||||
} catch (e) {
|
||||
const prefs = await sdk.forConsole.account.getPrefs();
|
||||
const newPrefs = { ...prefs, organization: null };
|
||||
sdk.forConsole.account.updatePrefs(newPrefs);
|
||||
error(e.code, e.message);
|
||||
}
|
||||
};
|
||||
|
||||
// load the invoice and add a banner in bg
|
||||
function loadFailedInvoices(teamId: string) {
|
||||
failedInvoice.load(teamId).then(() => {
|
||||
if (get(failedInvoice)) {
|
||||
headerAlert.add({
|
||||
show: true,
|
||||
component: ProjectsAtRisk,
|
||||
id: 'projectsAtRisk',
|
||||
importance: 1
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -154,7 +154,8 @@
|
||||
{@const platforms = filterPlatforms(
|
||||
project.platforms.map((platform) => getPlatformInfo(platform.type))
|
||||
)}
|
||||
<GridItem1 href={`${base}/project-${project.region}-${project.$id}`}>
|
||||
<GridItem1
|
||||
href={`${base}/project-${project.region}-${project.$id}/overview/platforms`}>
|
||||
<svelte:fragment slot="eyebrow">
|
||||
{project?.platforms?.length ? project?.platforms?.length : 'No'} apps
|
||||
</svelte:fragment>
|
||||
@@ -209,6 +210,4 @@
|
||||
|
||||
<CreateOrganization bind:show={addOrganization} />
|
||||
<CreateProject bind:show={showCreate} teamId={page.params.organization} />
|
||||
{#if showCreateProjectCloud}
|
||||
<CreateProjectCloud bind:showCreateProjectCloud regions={$regionsStore.regions} />
|
||||
{/if}
|
||||
<CreateProjectCloud bind:showCreateProjectCloud regions={$regionsStore.regions} />
|
||||
|
||||
@@ -7,14 +7,16 @@ import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export const load: PageLoad = async ({ params, url, route, depends, parent }) => {
|
||||
const { scopes } = await parent();
|
||||
depends(Dependencies.ORGANIZATION);
|
||||
const page = getPage(url);
|
||||
const limit = getLimit(url, route, CARD_LIMIT);
|
||||
const offset = pageToOffset(page, limit);
|
||||
if (!scopes.includes('projects.read') && scopes.includes('billing.read')) {
|
||||
return redirect(301, `/console/organization-${params.organization}/billing`);
|
||||
}
|
||||
|
||||
depends(Dependencies.ORGANIZATION);
|
||||
|
||||
const page = getPage(url);
|
||||
const limit = getLimit(url, route, CARD_LIMIT);
|
||||
const offset = pageToOffset(page, limit);
|
||||
|
||||
const projects = await sdk.forConsole.projects.list([
|
||||
Query.offset(offset),
|
||||
Query.equal('teamId', params.organization),
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import AvailableCredit from './availableCredit.svelte';
|
||||
import PaymentHistory from './paymentHistory.svelte';
|
||||
import TaxId from './taxId.svelte';
|
||||
import { failedInvoice, paymentMethods, tierToPlan, upgradeURL } from '$lib/stores/billing';
|
||||
import { failedInvoice, tierToPlan, upgradeURL } from '$lib/stores/billing';
|
||||
import type { PaymentMethodData } from '$lib/sdk/billing';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/state';
|
||||
@@ -22,14 +22,16 @@
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { base } from '$app/paths';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data;
|
||||
export let data: PageData;
|
||||
|
||||
$: defaultPaymentMethod = $paymentMethods?.paymentMethods?.find(
|
||||
// why are these reactive?
|
||||
$: defaultPaymentMethod = data?.paymentMethods?.paymentMethods?.find(
|
||||
(method: PaymentMethodData) => method.$id === $organization?.paymentMethodId
|
||||
);
|
||||
|
||||
$: backupPaymentMethod = $paymentMethods?.paymentMethods?.find(
|
||||
$: backupPaymentMethod = data?.paymentMethods?.paymentMethods?.find(
|
||||
(method: PaymentMethodData) => method.$id === $organization?.backupPaymentMethodId
|
||||
);
|
||||
|
||||
@@ -127,16 +129,16 @@
|
||||
{/if}
|
||||
<Typography.Title>Billing</Typography.Title>
|
||||
<PlanSummary
|
||||
creditList={data?.creditList}
|
||||
availableCredit={data?.availableCredit}
|
||||
currentPlan={data?.aggregationBillingPlan}
|
||||
currentAggregation={data?.billingAggregation}
|
||||
currentInvoice={data?.billingInvoice} />
|
||||
<PaymentHistory />
|
||||
<PaymentMethods />
|
||||
<BillingAddress billingAddress={data?.billingAddress} />
|
||||
<PaymentMethods methods={data?.paymentMethods} />
|
||||
<BillingAddress {data} />
|
||||
<TaxId />
|
||||
<BudgetCap />
|
||||
<AvailableCredit />
|
||||
<AvailableCredit areCreditsSupported={data.areCreditsSupported} />
|
||||
</Container>
|
||||
|
||||
{#if $selectedInvoice}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { BillingPlan, Dependencies } from '$lib/constants';
|
||||
import type { Address } from '$lib/sdk/billing';
|
||||
import type { Organization } from '$lib/stores/organization';
|
||||
import { type Organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
import { isCloud } from '$lib/system';
|
||||
|
||||
export const load: PageLoad = async ({ parent, depends }) => {
|
||||
const { organization, scopes } = await parent();
|
||||
const { organization, scopes, currentPlan, countryList, locale, plansInfo } = await parent();
|
||||
|
||||
if (!scopes.includes('billing.read')) {
|
||||
return redirect(301, `/console/organization-${organization.$id}`);
|
||||
}
|
||||
|
||||
depends(Dependencies.PAYMENT_METHODS);
|
||||
depends(Dependencies.ORGANIZATION);
|
||||
depends(Dependencies.CREDIT);
|
||||
@@ -49,22 +51,36 @@ export const load: PageLoad = async ({ parent, depends }) => {
|
||||
// ignore error
|
||||
}
|
||||
|
||||
const [paymentMethods, addressList, billingAddress, creditList, aggregationBillingPlan] =
|
||||
await Promise.all([
|
||||
sdk.forConsole.billing.listPaymentMethods(),
|
||||
sdk.forConsole.billing.listAddresses(),
|
||||
billingAddressPromise,
|
||||
sdk.forConsole.billing.listCredits(organization.$id),
|
||||
sdk.forConsole.billing.getPlan(billingAggregation?.plan ?? organization.billingPlan)
|
||||
]);
|
||||
const areCreditsSupported = isCloud
|
||||
? (currentPlan?.supportsCredits ??
|
||||
(organization.billingPlan !== BillingPlan.FREE &&
|
||||
organization?.billingPlan !== BillingPlan.GITHUB_EDUCATION))
|
||||
: false;
|
||||
|
||||
const [paymentMethods, addressList, billingAddress, availableCredit] = await Promise.all([
|
||||
sdk.forConsole.billing.listPaymentMethods(),
|
||||
sdk.forConsole.billing.listAddresses(),
|
||||
billingAddressPromise,
|
||||
areCreditsSupported ? sdk.forConsole.billing.getAvailableCredit(organization.$id) : null
|
||||
]);
|
||||
|
||||
const aggregationBillingPlan = plansInfo.get(
|
||||
billingAggregation?.plan ?? organization.billingPlan
|
||||
);
|
||||
|
||||
// make number
|
||||
const credits = availableCredit ? availableCredit.available : null;
|
||||
|
||||
return {
|
||||
paymentMethods,
|
||||
addressList,
|
||||
billingAddress,
|
||||
aggregationBillingPlan,
|
||||
creditList,
|
||||
availableCredit: credits,
|
||||
billingAggregation,
|
||||
billingInvoice
|
||||
billingInvoice,
|
||||
areCreditsSupported,
|
||||
countryList,
|
||||
locale
|
||||
};
|
||||
};
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
import { Alert, Badge, Icon, Link, Table, Tooltip, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
|
||||
export let areCreditsSupported: boolean;
|
||||
|
||||
let offset = 0;
|
||||
let creditList: CreditList = {
|
||||
available: 0,
|
||||
@@ -42,10 +44,21 @@
|
||||
|
||||
async function request() {
|
||||
if (!$organization?.$id) return;
|
||||
|
||||
// fast path return!
|
||||
if (!areCreditsSupported) return;
|
||||
|
||||
/**
|
||||
* The initial `creditList` from `+page.ts` can include up to 25 items by default.
|
||||
*
|
||||
* Technically, we could reuse that for offsets < 25 (i.e., the first 5 pages with limit = 5)
|
||||
* to avoid an extra request. But for now, we always fetch fresh data.
|
||||
*/
|
||||
creditList = await sdk.forConsole.billing.listCredits($organization.$id, [
|
||||
Query.limit(limit),
|
||||
Query.offset(offset)
|
||||
]);
|
||||
|
||||
creditList = {
|
||||
...creditList,
|
||||
credits: creditList.credits
|
||||
@@ -70,10 +83,6 @@
|
||||
};
|
||||
}
|
||||
|
||||
$: if (offset !== null) {
|
||||
request();
|
||||
}
|
||||
|
||||
$: {
|
||||
if (reloadOnWizardClose && !$wizard.show) {
|
||||
request();
|
||||
@@ -84,11 +93,11 @@
|
||||
|
||||
<CardGrid hideFooter={$organization?.billingPlan !== BillingPlan.FREE}>
|
||||
<svelte:fragment slot="title">
|
||||
{$organization?.billingPlan === BillingPlan.FREE ? 'Credits' : 'Available credit'}
|
||||
{!areCreditsSupported ? 'Credits' : 'Available credit'}
|
||||
</svelte:fragment>
|
||||
Appwrite credit will automatically be applied to your next invoice.
|
||||
<svelte:fragment slot="aside">
|
||||
{#if $organization?.billingPlan === BillingPlan.FREE}
|
||||
{#if !areCreditsSupported}
|
||||
<Alert.Inline status="info" title="Upgrade to Pro to add credits">
|
||||
Upgrade to a Pro plan to add credits to your organization. For more information on
|
||||
what you can do with a Pro plan,
|
||||
@@ -158,7 +167,12 @@
|
||||
{#if creditList?.total > limit}
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p class="text">Total credits: {creditList?.total}</p>
|
||||
<PaginationInline {limit} bind:offset total={creditList?.total} hidePages />
|
||||
<PaginationInline
|
||||
{limit}
|
||||
hidePages
|
||||
bind:offset
|
||||
on:change={request}
|
||||
total={creditList?.total} />
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
|
||||
@@ -22,8 +22,15 @@
|
||||
IconSwitchHorizontal,
|
||||
IconTrash
|
||||
} from '@appwrite.io/pink-icons-svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let billingAddress: Address = null;
|
||||
export let data: PageData;
|
||||
|
||||
const locale: Models.Locale = data.locale;
|
||||
const countryList: Models.CountryList = data.countryList;
|
||||
|
||||
let billingAddress: Address = data?.billingAddress;
|
||||
|
||||
let showCreate = false;
|
||||
let showEdit = false;
|
||||
@@ -34,14 +41,14 @@
|
||||
try {
|
||||
await sdk.forConsole.billing.setBillingAddress($organization.$id, addressId);
|
||||
|
||||
await invalidate(Dependencies.ADDRESS);
|
||||
await invalidate(Dependencies.ORGANIZATION);
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `A new billing address has been added to ${$organization.name}`
|
||||
});
|
||||
trackEvent(Submit.OrganizationBillingAddressUpdate);
|
||||
|
||||
invalidate(Dependencies.ADDRESS);
|
||||
invalidate(Dependencies.ORGANIZATION);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
@@ -133,10 +140,14 @@
|
||||
</CardGrid>
|
||||
|
||||
{#if showCreate}
|
||||
<AddressModal bind:show={showCreate} organization={$organization?.$id} />
|
||||
<AddressModal bind:show={showCreate} organization={$organization?.$id} {countryList} {locale} />
|
||||
{/if}
|
||||
{#if showEdit}
|
||||
<EditAddressModal bind:show={showEdit} bind:selectedAddress={billingAddress} />
|
||||
<EditAddressModal
|
||||
{locale}
|
||||
{countryList}
|
||||
bind:show={showEdit}
|
||||
bind:selectedAddress={billingAddress} />
|
||||
{/if}
|
||||
{#if showReplace}
|
||||
<ReplaceAddress bind:show={showReplace} />
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
Layout,
|
||||
Link,
|
||||
Popover,
|
||||
Skeleton,
|
||||
Table
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import {
|
||||
@@ -28,6 +29,7 @@
|
||||
} from '@appwrite.io/pink-icons-svelte';
|
||||
|
||||
let offset = $state(0);
|
||||
let isLoadingInvoices = $state(false);
|
||||
let invoiceList: InvoiceList = $state({
|
||||
invoices: [],
|
||||
total: 0
|
||||
@@ -35,13 +37,17 @@
|
||||
|
||||
const limit = 5;
|
||||
const endpoint = getApiEndpoint();
|
||||
const hasPaymentError = $derived(invoiceList?.invoices.some((invoice) => invoice?.lastError));
|
||||
|
||||
async function request() {
|
||||
isLoadingInvoices = true;
|
||||
invoiceList = await sdk.forConsole.billing.listInvoices(page.params.organization, [
|
||||
Query.limit(limit),
|
||||
Query.offset(offset),
|
||||
Query.orderDesc('$createdAt')
|
||||
]);
|
||||
|
||||
isLoadingInvoices = false;
|
||||
}
|
||||
|
||||
function retryPayment(invoice: Invoice) {
|
||||
@@ -49,8 +55,6 @@
|
||||
$showRetryModal = true;
|
||||
}
|
||||
|
||||
const hasPaymentError = $derived(invoiceList?.invoices.some((invoice) => invoice?.lastError));
|
||||
|
||||
$effect(() => {
|
||||
if (page.url.searchParams.get('type') === 'validate-invoice') {
|
||||
window.history.replaceState({}, '', page.url.pathname);
|
||||
@@ -63,27 +67,40 @@
|
||||
request();
|
||||
}
|
||||
});
|
||||
|
||||
const columns = $derived([
|
||||
{ id: 'dueDate', width: { min: 120 } },
|
||||
{ id: 'status', width: { min: hasPaymentError ? 200 : 100 } },
|
||||
{ id: 'amount', width: { min: 120 } },
|
||||
{ id: 'action', width: 40 }
|
||||
]);
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Payment history</svelte:fragment>
|
||||
Transaction history for this organization. Download invoices for more details about your payments.
|
||||
<svelte:fragment slot="aside">
|
||||
{#if invoiceList.total > 0}
|
||||
<Table.Root
|
||||
let:root
|
||||
columns={[
|
||||
{ id: 'dueDate', width: { min: 120 } },
|
||||
{ id: 'status', width: { min: hasPaymentError ? 200 : 100 } },
|
||||
{ id: 'amount', width: { min: 120 } },
|
||||
{ id: 'action', width: 40 }
|
||||
]}>
|
||||
{#if invoiceList.total > 0 || isLoadingInvoices}
|
||||
<Table.Root let:root {columns}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell column="dueDate" {root}>Due date</Table.Header.Cell>
|
||||
<Table.Header.Cell column="status" {root}>Status</Table.Header.Cell>
|
||||
<Table.Header.Cell column="amount" {root}>Amount due</Table.Header.Cell>
|
||||
<Table.Header.Cell column="action" {root} />
|
||||
</svelte:fragment>
|
||||
|
||||
{#if isLoadingInvoices}
|
||||
{#each Array.from({ length: 2 }).keys() as index (index)}
|
||||
<Table.Row.Base {root}>
|
||||
{#each columns as column}
|
||||
<Table.Cell column={column.id} {root}>
|
||||
<Skeleton variant="line" height={20} width="100%" />
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#each invoiceList?.invoices as invoice}
|
||||
{@const status = invoice.status}
|
||||
<Table.Row.Base {root}>
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { hasStripePublicKey, isCloud } from '$lib/system';
|
||||
import { paymentMethods } from '$lib/stores/billing';
|
||||
import type { PaymentMethodData } from '$lib/sdk/billing';
|
||||
import type { PaymentList, PaymentMethodData } from '$lib/sdk/billing';
|
||||
import DeleteOrgPayment from './deleteOrgPayment.svelte';
|
||||
import ReplaceCard from './replaceCard.svelte';
|
||||
import EditPaymentModal from '$routes/(console)/account/payments/editPaymentModal.svelte';
|
||||
@@ -35,13 +34,15 @@
|
||||
IconTrash
|
||||
} from '@appwrite.io/pink-icons-svelte';
|
||||
|
||||
export let methods: PaymentList;
|
||||
|
||||
let showPayment = false;
|
||||
let showEdit = false;
|
||||
let showDelete = false;
|
||||
let showReplace = false;
|
||||
let isSelectedBackup = false;
|
||||
let defaultPaymentMethod: PaymentMethodData;
|
||||
let backupPaymentMethod: PaymentMethodData;
|
||||
let defaultPaymentMethod: PaymentMethodData;
|
||||
|
||||
async function addPaymentMethod(paymentMethodId: string) {
|
||||
try {
|
||||
@@ -209,7 +210,7 @@
|
||||
{/if}
|
||||
</Table.Root>
|
||||
{#if !$organization?.backupPaymentMethodId}
|
||||
{@const filteredPaymentMethods = $paymentMethods.paymentMethods.filter(
|
||||
{@const filteredPaymentMethods = methods.paymentMethods.filter(
|
||||
(o) => !!o.last4 && o.$id !== $organization?.paymentMethodId
|
||||
)}
|
||||
<div>
|
||||
@@ -237,7 +238,7 @@
|
||||
</Tooltip>
|
||||
</Layout.Stack>
|
||||
<ActionMenu.Root slot="tooltip" let:toggle>
|
||||
{#if $paymentMethods.total}
|
||||
{#if methods.total}
|
||||
{#each filteredPaymentMethods as paymentMethod}
|
||||
<ActionMenu.Item.Button
|
||||
on:click={() => addBackupPaymentMethod(paymentMethod?.$id)}>
|
||||
@@ -262,7 +263,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
{@const filteredPaymentMethods = $paymentMethods.paymentMethods.filter(
|
||||
{@const filteredPaymentMethods = methods.paymentMethods.filter(
|
||||
(o) => !!o.last4 && o.$id !== $organization?.backupPaymentMethodId
|
||||
)}
|
||||
<Card.Base>
|
||||
@@ -272,7 +273,7 @@
|
||||
<Icon icon={IconPlus} size="s" />
|
||||
</Button>
|
||||
<ActionMenu.Root slot="tooltip" let:toggle>
|
||||
{#if $paymentMethods.total}
|
||||
{#if methods.total}
|
||||
{#each filteredPaymentMethods as paymentMethod}
|
||||
<ActionMenu.Item.Button
|
||||
on:click={() => addPaymentMethod(paymentMethod?.$id)}>
|
||||
@@ -318,7 +319,7 @@
|
||||
bind:show={showEdit} />
|
||||
{/if}
|
||||
{#if isCloud && hasStripePublicKey}
|
||||
<ReplaceCard bind:show={showReplace} isBackup={isSelectedBackup} />
|
||||
<ReplaceCard {methods} bind:show={showReplace} isBackup={isSelectedBackup} />
|
||||
{/if}
|
||||
{#if showDelete && isCloud && hasStripePublicKey}
|
||||
{@const hasOtherMethod = isSelectedBackup
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import { plansInfo, upgradeURL } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import type { Aggregation, CreditList, Invoice, Plan } from '$lib/sdk/billing';
|
||||
import type { Aggregation, Invoice, Plan } from '$lib/sdk/billing';
|
||||
import { abbreviateNumber, formatCurrency, formatNumberWithCommas } from '$lib/helpers/numbers';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
@@ -22,13 +22,12 @@
|
||||
import CancelDowngradeModel from './cancelDowngradeModal.svelte';
|
||||
|
||||
export let currentPlan: Plan;
|
||||
export let creditList: CreditList;
|
||||
export let currentInvoice: Invoice | undefined = undefined;
|
||||
export let availableCredit: number | undefined = undefined;
|
||||
export let currentAggregation: Aggregation | undefined = undefined;
|
||||
|
||||
let showCancel: boolean = false;
|
||||
|
||||
const availableCredit = creditList.available;
|
||||
const today = new Date();
|
||||
const isTrial =
|
||||
new Date($organization?.billingStartDate).getTime() - today.getTime() > 0 &&
|
||||
|
||||
@@ -14,14 +14,13 @@
|
||||
|
||||
export let show = false;
|
||||
export let isBackup = false;
|
||||
let methods: PaymentList;
|
||||
let selectedPaymentMethodId: string;
|
||||
export let methods: PaymentList;
|
||||
|
||||
let name: string;
|
||||
let error: string;
|
||||
let selectedPaymentMethodId: string;
|
||||
|
||||
onMount(async () => {
|
||||
methods = await sdk.forConsole.billing.listPaymentMethods();
|
||||
|
||||
if (!$organization.paymentMethodId && !$organization.backupPaymentMethodId) {
|
||||
selectedPaymentMethodId = methods?.total ? methods.paymentMethods[0].$id : null;
|
||||
} else {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import { Button, Form, InputSelect, InputTags, InputTextarea } from '$lib/elements/forms';
|
||||
import { formatCurrency } from '$lib/helpers/numbers.js';
|
||||
import { Wizard } from '$lib/layout';
|
||||
import { type Coupon } from '$lib/sdk/billing';
|
||||
import { type Coupon, type PaymentMethodData } from '$lib/sdk/billing';
|
||||
import { isOrganization, plansInfo, tierToPlan } from '$lib/stores/billing';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { currentPlan, organization } from '$lib/stores/organization';
|
||||
@@ -26,6 +26,7 @@
|
||||
Icon,
|
||||
Layout,
|
||||
Link,
|
||||
Skeleton,
|
||||
Typography
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
@@ -35,15 +36,13 @@
|
||||
|
||||
export let data;
|
||||
|
||||
let selectedCoupon: Partial<Coupon> = null;
|
||||
|
||||
let selectedPlan: BillingPlan = data.plan as BillingPlan;
|
||||
let selectedCoupon: Partial<Coupon> | null = data.coupon;
|
||||
let previousPage: string = base;
|
||||
let showExitModal = false;
|
||||
let formComponent: Form;
|
||||
let isSubmitting = writable(false);
|
||||
let paymentMethodId: string =
|
||||
data.organization.paymentMethodId ??
|
||||
data.paymentMethods.paymentMethods.find((method) => !!method?.last4)?.$id;
|
||||
let collaborators: string[] =
|
||||
data?.members?.memberships
|
||||
?.map((m) => {
|
||||
@@ -56,45 +55,48 @@
|
||||
let feedbackDowngradeReason: string;
|
||||
let feedbackMessage: string;
|
||||
|
||||
$: paymentMethods = null;
|
||||
|
||||
$: paymentMethodId =
|
||||
data.organization.paymentMethodId ??
|
||||
paymentMethods?.paymentMethods?.find((method: PaymentMethodData) => !!method?.last4)?.$id;
|
||||
|
||||
afterNavigate(({ from }) => {
|
||||
previousPage = from?.url?.pathname || previousPage;
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
if (page.url.searchParams.has('code')) {
|
||||
const coupon = page.url.searchParams.get('code');
|
||||
const params = page.url.searchParams;
|
||||
|
||||
const couponCode = params.get('code');
|
||||
if (couponCode) {
|
||||
try {
|
||||
selectedCoupon = await sdk.forConsole.billing.getCouponAccount(coupon);
|
||||
} catch (e) {
|
||||
selectedCoupon = {
|
||||
code: null,
|
||||
status: null,
|
||||
credits: null
|
||||
};
|
||||
}
|
||||
}
|
||||
if (page.url.searchParams.has('plan')) {
|
||||
const plan = page.url.searchParams.get('plan');
|
||||
if (plan && plan in BillingPlan) {
|
||||
selectedPlan = plan as BillingPlan;
|
||||
selectedCoupon = await sdk.forConsole.billing.getCouponAccount(couponCode);
|
||||
} catch {
|
||||
selectedCoupon = { code: null, status: null, credits: null };
|
||||
}
|
||||
}
|
||||
|
||||
if (page.url.searchParams.has('type')) {
|
||||
const type = page.url.searchParams.get('type');
|
||||
if (type === 'payment_confirmed') {
|
||||
const organizationId = page.url.searchParams.get('id');
|
||||
const invites = page.url.searchParams.get('invites').split(',');
|
||||
await validate(organizationId, invites);
|
||||
}
|
||||
const plan = params.get('plan');
|
||||
if (plan && plan in BillingPlan) {
|
||||
selectedPlan = plan as BillingPlan;
|
||||
}
|
||||
if ($currentPlan?.$id === BillingPlan.SCALE) {
|
||||
selectedPlan = BillingPlan.SCALE;
|
||||
} else {
|
||||
selectedPlan = BillingPlan.PRO;
|
||||
|
||||
if (params.get('type') === 'payment_confirmed') {
|
||||
const organizationId = params.get('id');
|
||||
const invites = params.get('invites')?.split(',') ?? [];
|
||||
await validate(organizationId, invites);
|
||||
}
|
||||
|
||||
selectedPlan =
|
||||
$currentPlan?.$id === BillingPlan.SCALE ? BillingPlan.SCALE : BillingPlan.PRO;
|
||||
});
|
||||
|
||||
async function loadPaymentMethods() {
|
||||
paymentMethods = await sdk.forConsole.billing.listPaymentMethods();
|
||||
return paymentMethods;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (isDowngrade) {
|
||||
await downgrade();
|
||||
@@ -310,22 +312,41 @@
|
||||
</Fieldset>
|
||||
|
||||
{#if isUpgrade}
|
||||
<Fieldset legend="Payment">
|
||||
<SelectPaymentMethod
|
||||
methods={data.paymentMethods}
|
||||
bind:value={paymentMethodId}
|
||||
bind:taxId>
|
||||
<svelte:fragment slot="actions">
|
||||
{#if !selectedCoupon?.code}
|
||||
{#await loadPaymentMethods()}
|
||||
<Fieldset legend="Payment">
|
||||
<Layout.Stack gap="m">
|
||||
<Typography.Text variant="m-500">Payment method</Typography.Text>
|
||||
<Skeleton variant="line" width="100%" height={30} />
|
||||
|
||||
<Layout.Stack direction="row">
|
||||
<Skeleton variant="line" width="165px" height={30} />
|
||||
<Divider vertical style="height: 2rem" />
|
||||
<Button compact on:click={() => (showCreditModal = true)}>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Add credits
|
||||
</Button>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</SelectPaymentMethod>
|
||||
</Fieldset>
|
||||
<Skeleton variant="line" width="100px" height={30} />
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Fieldset>
|
||||
{:then paymentMethods}
|
||||
<Fieldset legend="Payment">
|
||||
<SelectPaymentMethod
|
||||
methods={paymentMethods}
|
||||
bind:value={paymentMethodId}
|
||||
bind:taxId>
|
||||
<svelte:fragment slot="actions">
|
||||
{#if !selectedCoupon?.code}
|
||||
{#if paymentMethodId}
|
||||
<Divider vertical style="height: 2rem" />
|
||||
{/if}
|
||||
|
||||
<Button compact on:click={() => (showCreditModal = true)}>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Add credits
|
||||
</Button>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</SelectPaymentMethod>
|
||||
</Fieldset>
|
||||
{/await}
|
||||
|
||||
<Fieldset legend="Invite members">
|
||||
<InputTags
|
||||
bind:tags={collaborators}
|
||||
@@ -372,6 +393,8 @@
|
||||
<Button fullWidthMobile secondary on:click={() => (showExitModal = true)}>Cancel</Button>
|
||||
<Button
|
||||
fullWidthMobile
|
||||
forceShowLoader
|
||||
submissionLoader={$isSubmitting}
|
||||
on:click={() => formComponent.triggerSubmit()}
|
||||
disabled={$isSubmitting || isButtonDisabled || !data.selfService}>
|
||||
Change plan
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
import { BillingPlan, Dependencies } from '$lib/constants';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { PageLoad } from './$types';
|
||||
import type { Coupon } from '$lib/sdk/billing';
|
||||
import type { Organization } from '$lib/stores/organization';
|
||||
import { BillingPlan, Dependencies } from '$lib/constants';
|
||||
|
||||
export const load: PageLoad = async ({ depends, parent, url }) => {
|
||||
const { members, organization, currentPlan, organizations } = await parent();
|
||||
export const load: PageLoad = async ({ depends, parent }) => {
|
||||
const { members, currentPlan, organizations } = await parent();
|
||||
depends(Dependencies.UPGRADE_PLAN);
|
||||
|
||||
const [coupon, paymentMethods] = await Promise.all([
|
||||
getCoupon(url),
|
||||
sdk.forConsole.billing.listPaymentMethods()
|
||||
]);
|
||||
let plan: BillingPlan;
|
||||
|
||||
let plan = getPlanFromUrl(url);
|
||||
|
||||
if (organization?.billingPlan === BillingPlan.SCALE) {
|
||||
if (currentPlan?.$id === BillingPlan.SCALE) {
|
||||
plan = BillingPlan.SCALE;
|
||||
} else {
|
||||
plan = BillingPlan.PRO;
|
||||
@@ -29,30 +22,7 @@ export const load: PageLoad = async ({ depends, parent, url }) => {
|
||||
return {
|
||||
members,
|
||||
plan,
|
||||
coupon,
|
||||
selfService,
|
||||
hasFreeOrgs,
|
||||
paymentMethods
|
||||
hasFreeOrgs
|
||||
};
|
||||
};
|
||||
|
||||
function getPlanFromUrl(url: URL): BillingPlan | null {
|
||||
if (url.searchParams.has('plan')) {
|
||||
const plan = url.searchParams.get('plan');
|
||||
if (plan && plan in BillingPlan) {
|
||||
return plan as BillingPlan;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getCoupon(url: URL): Promise<Coupon | null> {
|
||||
if (url.searchParams.has('code')) {
|
||||
const coupon = url.searchParams.get('code');
|
||||
try {
|
||||
return sdk.forConsole.billing.getCouponAccount(coupon);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -38,9 +38,11 @@
|
||||
`${page.url.origin}${base}/invite`,
|
||||
name || undefined
|
||||
);
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
await invalidate(Dependencies.ORGANIZATION);
|
||||
await invalidate(Dependencies.MEMBERS);
|
||||
await Promise.all([
|
||||
invalidate(Dependencies.ACCOUNT),
|
||||
invalidate(Dependencies.ORGANIZATION),
|
||||
invalidate(Dependencies.MEMBERS)
|
||||
]);
|
||||
|
||||
showCreate = false;
|
||||
addNotification({
|
||||
|
||||
@@ -2,67 +2,74 @@
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { ID, Region as ConsoleRegion, type Models, Region } from '@appwrite.io/console';
|
||||
import { Button } from '@appwrite.io/pink-svelte';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { ID, type Models, Region as ConsoleRegion, Region } from '@appwrite.io/console';
|
||||
import { base } from '$app/paths';
|
||||
import CreateProject from '$lib/layout/createProject.svelte';
|
||||
import { Modal } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
|
||||
const teamId = page.params.organization;
|
||||
export let regions: Array<Models.ConsoleRegion> = [];
|
||||
export let showCreateProjectCloud: boolean;
|
||||
export let regions: Array<Models.ConsoleRegion> = [];
|
||||
|
||||
let id: string = null;
|
||||
let name: string = 'New project';
|
||||
let region: string = Region.Fra;
|
||||
let error: string = null;
|
||||
let name: string = 'New project';
|
||||
let region: ConsoleRegion = Region.Fra;
|
||||
|
||||
async function onFinish() {
|
||||
await invalidate(Dependencies.FUNCTIONS);
|
||||
let showSubmissionLoader = false;
|
||||
const teamId = page.params.organization;
|
||||
|
||||
function onFinish() {
|
||||
addNotification({ type: 'success', message: `${name} has been created` });
|
||||
trackEvent(Submit.ProjectCreate, { customId: !!id, teamId, region: region });
|
||||
}
|
||||
|
||||
async function create() {
|
||||
showSubmissionLoader = true;
|
||||
|
||||
try {
|
||||
// TODO: fix typing once SDK is updated
|
||||
const project = await sdk.forConsole.projects.create(
|
||||
id ?? ID.unique(),
|
||||
name,
|
||||
teamId,
|
||||
region as ConsoleRegion
|
||||
region
|
||||
);
|
||||
trackEvent(Submit.ProjectCreate, {
|
||||
customId: !!id,
|
||||
teamId,
|
||||
region: region
|
||||
});
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${name} has been created`
|
||||
});
|
||||
await onFinish();
|
||||
|
||||
onFinish();
|
||||
await goto(`${base}/project-${project.region}-${project.$id}`);
|
||||
} catch (e) {
|
||||
trackError(e, Submit.ProjectCreate);
|
||||
error = e.message;
|
||||
trackError(e, Submit.ProjectCreate);
|
||||
} finally {
|
||||
showSubmissionLoader = false;
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
id = null;
|
||||
name = null;
|
||||
region = 'fra';
|
||||
error = null;
|
||||
region = Region.Fra;
|
||||
showCreateProjectCloud = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<Modal bind:show={showCreateProjectCloud} title={'Create project'} onSubmit={create} bind:error>
|
||||
<CreateProject showTitle={false} bind:id bind:projectName={name} bind:region {regions}>
|
||||
</CreateProject>
|
||||
<Modal
|
||||
bind:show={showCreateProjectCloud}
|
||||
autoClose={false}
|
||||
title={'Create project'}
|
||||
onSubmit={create}
|
||||
bind:error>
|
||||
<CreateProject showTitle={false} bind:id bind:projectName={name} bind:region {regions} />
|
||||
<svelte:fragment slot="footer">
|
||||
<Button.Button type="submit" variant="primary" size="s">Create</Button.Button>
|
||||
<Button
|
||||
submit
|
||||
size="s"
|
||||
forceShowLoader={showSubmissionLoader}
|
||||
submissionLoader={showSubmissionLoader}>Create</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
@@ -29,8 +29,9 @@
|
||||
} else {
|
||||
dispatch('deleted');
|
||||
}
|
||||
invalidate(Dependencies.ACCOUNT);
|
||||
invalidate(Dependencies.MEMBERS);
|
||||
|
||||
await Promise.all([invalidate(Dependencies.ACCOUNT), invalidate(Dependencies.MEMBERS)]);
|
||||
|
||||
if (isCloud && $organization) {
|
||||
await checkForUsageLimit($organization);
|
||||
}
|
||||
@@ -51,6 +52,7 @@
|
||||
|
||||
<Confirm
|
||||
onSubmit={deleteMembership}
|
||||
submissionLoader
|
||||
title={isUser ? 'Leave organization' : 'Delete member'}
|
||||
bind:open={showDelete}
|
||||
action={isUser ? 'Leave' : 'Delete'}
|
||||
|
||||
@@ -20,60 +20,64 @@
|
||||
import { IconGithub, IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Badge, Icon, Layout, Tooltip, Typography } from '@appwrite.io/pink-svelte';
|
||||
|
||||
let areMembersLimited: boolean;
|
||||
let areMembersLimited: boolean = $state(false);
|
||||
|
||||
$: {
|
||||
$effect(() => {
|
||||
const limit = getServiceLimit('members') || Infinity;
|
||||
const isLimited = limit !== 0 && limit < Infinity;
|
||||
areMembersLimited =
|
||||
isCloud &&
|
||||
(($readOnly && !GRACE_PERIOD_OVERRIDE) || (isLimited && $members?.total >= limit));
|
||||
}
|
||||
});
|
||||
|
||||
$: organization = page.data.organization as Organization;
|
||||
$: avatars = $members.memberships?.map((m) => m.userName || m.userEmail) ?? [];
|
||||
$: path = `${base}/organization-${organization.$id}`;
|
||||
$: tabs = [
|
||||
{
|
||||
href: path,
|
||||
title: 'Projects',
|
||||
event: 'projects',
|
||||
hasChildren: true,
|
||||
disabled: !$canSeeProjects
|
||||
},
|
||||
{
|
||||
href: `${path}/domains`,
|
||||
event: 'domains',
|
||||
title: 'Domains',
|
||||
disabled: !isCloud
|
||||
},
|
||||
{
|
||||
href: `${path}/members`,
|
||||
title: 'Members',
|
||||
event: 'members',
|
||||
hasChildren: true,
|
||||
disabled: !$canSeeTeams
|
||||
},
|
||||
{
|
||||
href: `${path}/usage`,
|
||||
event: 'usage',
|
||||
title: 'Usage',
|
||||
hasChildren: true,
|
||||
disabled: !(isCloud && ($isOwner || $isBilling))
|
||||
},
|
||||
{
|
||||
href: `${path}/billing`,
|
||||
event: 'billing',
|
||||
title: 'Billing',
|
||||
disabled: !(isCloud && $canSeeBilling)
|
||||
},
|
||||
{
|
||||
href: `${path}/settings`,
|
||||
event: 'settings',
|
||||
title: 'Settings',
|
||||
disabled: !$isOwner
|
||||
}
|
||||
].filter((tab) => !tab.disabled);
|
||||
const organization = $derived(page.data.organization as Organization);
|
||||
const path = $derived(`${base}/organization-${organization.$id}`);
|
||||
|
||||
const tabs = $derived(
|
||||
[
|
||||
{
|
||||
href: path,
|
||||
title: 'Projects',
|
||||
event: 'projects',
|
||||
hasChildren: true,
|
||||
disabled: !$canSeeProjects
|
||||
},
|
||||
{
|
||||
href: `${path}/domains`,
|
||||
event: 'domains',
|
||||
title: 'Domains',
|
||||
disabled: !isCloud
|
||||
},
|
||||
{
|
||||
href: `${path}/members`,
|
||||
title: 'Members',
|
||||
event: 'members',
|
||||
hasChildren: true,
|
||||
disabled: !$canSeeTeams
|
||||
},
|
||||
{
|
||||
href: `${path}/usage`,
|
||||
event: 'usage',
|
||||
title: 'Usage',
|
||||
hasChildren: true,
|
||||
disabled: !(isCloud && ($isOwner || $isBilling))
|
||||
},
|
||||
{
|
||||
href: `${path}/billing`,
|
||||
event: 'billing',
|
||||
title: 'Billing',
|
||||
disabled: !(isCloud && $canSeeBilling)
|
||||
},
|
||||
{
|
||||
href: `${path}/settings`,
|
||||
event: 'settings',
|
||||
title: 'Settings',
|
||||
disabled: !$isOwner
|
||||
}
|
||||
].filter((tab) => !tab.disabled)
|
||||
);
|
||||
|
||||
const avatars = $derived($members.memberships?.map((m) => m.userName || m.userEmail) ?? []);
|
||||
</script>
|
||||
|
||||
{#if organization?.$id}
|
||||
|
||||
@@ -15,8 +15,10 @@
|
||||
import { isCloud } from '$lib/system';
|
||||
import Baa from './BAA.svelte';
|
||||
import Soc2 from './Soc2.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
export let data;
|
||||
let name: string;
|
||||
let showDelete = false;
|
||||
|
||||
@@ -70,8 +72,8 @@
|
||||
|
||||
{#if isCloud}
|
||||
<DownloadDPA />
|
||||
<Baa />
|
||||
<Soc2 />
|
||||
<Baa locale={data.locale} countryList={data.countryList} />
|
||||
<Soc2 locale={data.locale} countryList={data.countryList} />
|
||||
{/if}
|
||||
|
||||
<CardGrid>
|
||||
|
||||
@@ -4,7 +4,8 @@ import { sdk } from '$lib/stores/sdk';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
import { isCloud } from '$lib/system';
|
||||
|
||||
export const load: PageLoad = async ({ depends, params }) => {
|
||||
export const load: PageLoad = async ({ depends, params, parent }) => {
|
||||
const { countryList, locale } = await parent();
|
||||
depends(Dependencies.ORGANIZATION);
|
||||
|
||||
const [projects, invoices] = await Promise.all([
|
||||
@@ -14,6 +15,8 @@ export const load: PageLoad = async ({ depends, params }) => {
|
||||
|
||||
return {
|
||||
projects,
|
||||
invoices
|
||||
invoices,
|
||||
countryList,
|
||||
locale
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
import { Box, CardGrid } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import BaaModal from './BAAModal.svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
let show = false;
|
||||
|
||||
export let locale: Models.Locale;
|
||||
export let countryList: Models.CountryList;
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
@@ -31,4 +35,4 @@
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
<BaaModal bind:show />
|
||||
<BaaModal {locale} {countryList} bind:show />
|
||||
|
||||
@@ -4,11 +4,15 @@
|
||||
import { Button, InputEmail, InputSelect, InputText } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { VARS } from '$lib/system';
|
||||
import { onMount } from 'svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let show = false;
|
||||
export let locale: Models.Locale;
|
||||
export let countryList: Models.CountryList;
|
||||
|
||||
let email = '';
|
||||
let employees: string = null;
|
||||
let employeesOptions = [
|
||||
@@ -39,10 +43,6 @@
|
||||
let error: string;
|
||||
|
||||
onMount(async () => {
|
||||
/* use console sdk as project is not always available here. */
|
||||
const locale = await sdk.forConsole.locale.get();
|
||||
const countryList = await sdk.forConsole.locale.listCountries();
|
||||
|
||||
if (locale.countryCode) {
|
||||
country = locale.countryCode;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
import { Box, CardGrid } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import Soc2Modal from './Soc2Modal.svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
let show = false;
|
||||
export let locale: Models.Locale;
|
||||
export let countryList: Models.CountryList;
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
@@ -31,4 +34,4 @@
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
<Soc2Modal bind:show />
|
||||
<Soc2Modal {locale} {countryList} bind:show />
|
||||
|
||||
@@ -5,11 +5,15 @@
|
||||
import Button from '$lib/elements/forms/button.svelte';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { VARS } from '$lib/system';
|
||||
import { onMount } from 'svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let show = false;
|
||||
export let locale: Models.Locale;
|
||||
export let countryList: Models.CountryList;
|
||||
|
||||
let email = '';
|
||||
let employees: string = null;
|
||||
let employeesOptions = [
|
||||
@@ -40,10 +44,6 @@
|
||||
let error: string;
|
||||
|
||||
onMount(async () => {
|
||||
/* use console sdk as project is not always available here. */
|
||||
const locale = await sdk.forConsole.locale.get();
|
||||
const countryList = await sdk.forConsole.locale.listCountries();
|
||||
|
||||
if (locale.countryCode) {
|
||||
country = locale.countryCode;
|
||||
}
|
||||
|
||||
+2
-3
@@ -15,7 +15,6 @@
|
||||
import { tierToPlan } from '$lib/stores/billing';
|
||||
import { Table, Tabs, Alert } from '@appwrite.io/pink-svelte';
|
||||
import DeleteOrganizationEstimation from './deleteOrganizationEstimation.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import type { EstimationDeleteOrganization, InvoiceList } from '$lib/sdk/billing';
|
||||
|
||||
export let showDelete = false;
|
||||
@@ -55,8 +54,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => getEstimate());
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
name: 'projects',
|
||||
@@ -75,6 +72,8 @@
|
||||
$: if (!showDelete) {
|
||||
// reset on close.
|
||||
organizationName = '';
|
||||
} else {
|
||||
getEstimate();
|
||||
}
|
||||
|
||||
async function getEstimate() {
|
||||
|
||||
@@ -4,6 +4,8 @@ import { sdk } from '$lib/stores/sdk';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { regions } from '$lib/stores/organization';
|
||||
|
||||
export type UsageProjectInfo = Pick<Models.Project, 'name' | 'region'>;
|
||||
|
||||
export const regionFlagUrls = derived(regions, ($regions) => {
|
||||
if (!$regions?.regions?.length) return [];
|
||||
|
||||
|
||||
@@ -21,32 +21,39 @@
|
||||
import { formatCurrency, formatNumberWithCommas } from '$lib/helpers/numbers';
|
||||
import { Icon, Layout, Link, Tooltip, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { IconChartSquareBar, IconInfo } from '@appwrite.io/pink-icons-svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import type { UsageProjectInfo } from '../../store';
|
||||
|
||||
export let data;
|
||||
|
||||
const tier = data?.plan
|
||||
? (data.plan.$id as Tier)
|
||||
: (data?.currentInvoice?.plan ?? $organization?.billingPlan);
|
||||
|
||||
const plan = data?.plan ?? undefined;
|
||||
|
||||
$: projects = (data.organizationUsage as OrganizationUsage).projects;
|
||||
|
||||
let usageProjects: Record<string, UsageProjectInfo> = {};
|
||||
|
||||
$: legendData = [
|
||||
{
|
||||
name: 'Reads',
|
||||
value: data.organizationUsage.databasesReads.reduce(
|
||||
(sum, singleDay) => sum + singleDay.value,
|
||||
(sum: number, singleDay: { date: string; value: number }) => sum + singleDay.value,
|
||||
0
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'Writes',
|
||||
value: data.organizationUsage.databasesWrites.reduce(
|
||||
(sum, singleDay) => sum + singleDay.value,
|
||||
(sum: number, singleDay: { date: string; value: number }) => sum + singleDay.value,
|
||||
0
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
onMount(async () => (usageProjects = await data.projects));
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
@@ -129,7 +136,7 @@
|
||||
}
|
||||
]} />
|
||||
{#if projects?.length > 0}
|
||||
<ProjectBreakdown {projects} metric="bandwidth" {data} />
|
||||
<ProjectBreakdown {projects} metric="bandwidth" {usageProjects} />
|
||||
{/if}
|
||||
{:else}
|
||||
<Card isDashed>
|
||||
@@ -177,7 +184,7 @@
|
||||
}
|
||||
]} />
|
||||
{#if projects?.length > 0}
|
||||
<ProjectBreakdown {projects} metric="users" {data} />
|
||||
<ProjectBreakdown {projects} metric="users" {usageProjects} />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
{:else}
|
||||
@@ -230,7 +237,7 @@
|
||||
|
||||
{#if projects?.length > 0}
|
||||
<ProjectBreakdown
|
||||
{data}
|
||||
{usageProjects}
|
||||
{projects}
|
||||
databaseOperationMetric={['databasesReads', 'databasesWrites']} />
|
||||
{/if}
|
||||
@@ -248,14 +255,10 @@
|
||||
|
||||
<CardGrid gap="none">
|
||||
<svelte:fragment slot="title">Image transformations</svelte:fragment>
|
||||
Calculated for all functions that are executed in all projects in your organization.
|
||||
<p class="text">
|
||||
The total number of unique image transformations across all projects in your
|
||||
organization. <a
|
||||
href="https://appwrite.io/docs/advanced/platform/image-transformations"
|
||||
class="link">Learn more</a
|
||||
>.
|
||||
</p>
|
||||
The total number of unique image transformations across all projects in your organization.
|
||||
<a href="https://appwrite.io/docs/advanced/platform/image-transformations" class="link"
|
||||
>Learn more</a
|
||||
>.
|
||||
<svelte:fragment slot="aside">
|
||||
{#if data.organizationUsage.imageTransformationsTotal}
|
||||
{@const current = data.organizationUsage.imageTransformationsTotal}
|
||||
@@ -284,7 +287,7 @@
|
||||
}
|
||||
]} />
|
||||
{#if projects?.length > 0}
|
||||
<ProjectBreakdown {projects} metric="imageTransformations" {data} />
|
||||
<ProjectBreakdown {projects} metric="imageTransformations" {usageProjects} />
|
||||
{/if}
|
||||
{:else}
|
||||
<Card isDashed>
|
||||
@@ -335,7 +338,7 @@
|
||||
}
|
||||
]} />
|
||||
{#if projects?.length > 0}
|
||||
<ProjectBreakdown {projects} metric="executions" {data} />
|
||||
<ProjectBreakdown {projects} metric="executions" {usageProjects} />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
{:else}
|
||||
@@ -401,7 +404,7 @@
|
||||
progressMax={max}
|
||||
progressBarData={progressBarStorageDate} />
|
||||
{#if projects?.length > 0}
|
||||
<ProjectBreakdown {projects} metric="storage" {data} />
|
||||
<ProjectBreakdown {projects} metric="storage" {usageProjects} />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
{:else}
|
||||
@@ -512,7 +515,7 @@
|
||||
{projects}
|
||||
metric="authPhoneTotal"
|
||||
estimate="authPhoneEstimate"
|
||||
{data} />
|
||||
{usageProjects} />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
{:else}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import type { Invoice } from '$lib/sdk/billing';
|
||||
import { type Organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Query, type Models } from '@appwrite.io/console';
|
||||
import type { PageLoad } from './$types';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
import type { UsageProjectInfo } from '../../store';
|
||||
import type { Invoice, OrganizationUsage } from '$lib/sdk/billing';
|
||||
|
||||
export const load: PageLoad = async ({ params, parent }) => {
|
||||
const { invoice } = params;
|
||||
const parentData = await parent();
|
||||
const org = parentData.organization as Organization;
|
||||
const { organization: org, currentPlan: plan } = await parent();
|
||||
|
||||
/**
|
||||
* Temporary fix during migration to billing system
|
||||
@@ -49,43 +48,48 @@ export const load: PageLoad = async ({ params, parent }) => {
|
||||
startDate = currentInvoice.from;
|
||||
endDate = currentInvoice.to;
|
||||
}
|
||||
const [invoices, usage, organizationMembers, plan] = await Promise.all([
|
||||
sdk.forConsole.billing.listInvoices(org.$id, [Query.orderDesc('from')]),
|
||||
sdk.forConsole.billing.listUsage(params.organization, startDate, endDate),
|
||||
sdk.forConsole.teams.listMemberships(params.organization, [Query.limit(100)]),
|
||||
sdk.forConsole.billing.getOrganizationPlan(org.$id)
|
||||
|
||||
const [usage, organizationMembers] = await Promise.all([
|
||||
sdk.forConsole.billing.listUsage(org.$id, startDate, endDate),
|
||||
// this section is cloud only,
|
||||
// so it is fine to use this check and fetch memberships conditionally!
|
||||
!plan?.addons?.seats?.supported
|
||||
? null
|
||||
: sdk.forConsole.teams.listMemberships(org.$id, [Query.limit(100)])
|
||||
]);
|
||||
|
||||
const projects: { [key: string]: Models.Project } = {};
|
||||
if (usage?.projects?.length > 0) {
|
||||
// in batches of 100 (the max number of values in a query)
|
||||
return {
|
||||
plan,
|
||||
currentInvoice,
|
||||
organizationMembers,
|
||||
organizationUsage: usage,
|
||||
projects: getUsageProjects(usage)
|
||||
};
|
||||
};
|
||||
|
||||
// all this to get the project's name and region!
|
||||
function getUsageProjects(usage: OrganizationUsage) {
|
||||
return (async () => {
|
||||
const projects: Record<string, UsageProjectInfo> = {};
|
||||
const limit = 100;
|
||||
const requests = [];
|
||||
const chunk = 100;
|
||||
for (let i = 0; i < usage.projects.length; i += chunk) {
|
||||
const queries = [
|
||||
Query.limit(chunk),
|
||||
Query.equal(
|
||||
'$id',
|
||||
usage.projects.slice(i, i + chunk).map((p) => p.projectId)
|
||||
)
|
||||
];
|
||||
requests.push(sdk.forConsole.projects.list(queries));
|
||||
for (let index = 0; index < usage.projects.length; index += limit) {
|
||||
const chunkIds = usage.projects.slice(index, index + limit).map((p) => p.projectId);
|
||||
requests.push(
|
||||
sdk.forConsole.projects.list([Query.limit(limit), Query.equal('$id', chunkIds)])
|
||||
);
|
||||
}
|
||||
|
||||
const responses = await Promise.all(requests);
|
||||
for (const response of responses) {
|
||||
for (const project of response.projects) {
|
||||
projects[project.$id] = project;
|
||||
projects[project.$id] = {
|
||||
name: project.name,
|
||||
region: project.region
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
plan,
|
||||
invoices,
|
||||
projects,
|
||||
currentInvoice,
|
||||
organizationMembers,
|
||||
organizationUsage: usage
|
||||
};
|
||||
};
|
||||
return projects;
|
||||
})();
|
||||
}
|
||||
|
||||
+5
-9
@@ -6,7 +6,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { Accordion, Table } from '@appwrite.io/pink-svelte';
|
||||
import { base } from '$app/paths';
|
||||
import type { PageData } from './$types';
|
||||
import type { UsageProjectInfo } from '../../store';
|
||||
|
||||
type Metric =
|
||||
| 'users'
|
||||
@@ -22,10 +22,10 @@
|
||||
|
||||
type DatabaseOperationMetric = Extract<Metric, 'databasesReads' | 'databasesWrites'>;
|
||||
|
||||
export let data: PageData;
|
||||
export let projects: OrganizationUsage['projects'];
|
||||
export let metric: Metric | undefined = undefined;
|
||||
export let estimate: Estimate | undefined = undefined;
|
||||
export let usageProjects: Record<string, UsageProjectInfo> = {};
|
||||
export let databaseOperationMetric: DatabaseOperationMetric[] | undefined = undefined;
|
||||
|
||||
function getMetricTitle(metric: Metric): string {
|
||||
@@ -38,14 +38,10 @@
|
||||
}
|
||||
|
||||
function getProjectUsageLink(projectId: string): string {
|
||||
const region = data.projects[projectId]?.region ?? 'fra';
|
||||
const region = usageProjects[projectId]?.region ?? 'default';
|
||||
return `${base}/project-${region}-${projectId}/settings/usage`;
|
||||
}
|
||||
|
||||
// function getProjectName(projectId: string): string {
|
||||
// return data.projects[projectId]?.name ?? 'Unknown';
|
||||
// }
|
||||
|
||||
function groupByProject(
|
||||
metric: Metric | undefined,
|
||||
estimate?: Estimate,
|
||||
@@ -138,7 +134,7 @@
|
||||
{#if !$canSeeProjects}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell column="project" {root}>
|
||||
{data.projects[project.projectId]?.name ?? 'Unknown'}
|
||||
{usageProjects[project.projectId]?.name ?? 'Unknown'}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="reads" {root}>
|
||||
{format(project.databasesReads ?? 0)}
|
||||
@@ -156,7 +152,7 @@
|
||||
{:else}
|
||||
<Table.Row.Link href={getProjectUsageLink(project.projectId)} {root}>
|
||||
<Table.Cell column="project" {root}>
|
||||
{data.projects[project.projectId]?.name ?? 'Unknown'}
|
||||
{usageProjects[project.projectId]?.name ?? 'Unknown'}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="reads" {root}>
|
||||
{format(project.databasesReads ?? 0)}
|
||||
|
||||
@@ -5,49 +5,48 @@ import { preferences } from '$lib/stores/preferences';
|
||||
import { failedInvoice } from '$lib/stores/billing';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { defaultRoles, defaultScopes } from '$lib/constants';
|
||||
import type { Plan } from '$lib/sdk/billing';
|
||||
import { get } from 'svelte/store';
|
||||
import { headerAlert } from '$lib/stores/headerAlert';
|
||||
import PaymentFailed from '$lib/components/billing/alerts/paymentFailed.svelte';
|
||||
import { loadAvailableRegions } from '$routes/(console)/regions';
|
||||
import type { Organization } from '$lib/stores/organization';
|
||||
import type { Organization, OrganizationList } from '$lib/stores/organization';
|
||||
|
||||
export const load: LayoutLoad = async ({ params, depends }) => {
|
||||
export const load: LayoutLoad = async ({ params, depends, parent }) => {
|
||||
const { plansInfo, organizations, preferences: prefs } = await parent();
|
||||
depends(Dependencies.PROJECT);
|
||||
let currentPlan: Plan = null;
|
||||
|
||||
const project = await sdk.forConsole.projects.get(params.project);
|
||||
const [organization, prefs, regionalConsoleVariables, _] = await Promise.all([
|
||||
sdk.forConsole.teams.get(project.teamId) as Promise<Organization>,
|
||||
sdk.forConsole.account.getPrefs(),
|
||||
|
||||
// fast path without a network call!
|
||||
let organization = (organizations as OrganizationList)?.teams?.find(
|
||||
(org) => org.$id === project.teamId
|
||||
);
|
||||
|
||||
const [org, regionalConsoleVariables, rolesResult] = await Promise.all([
|
||||
!organization
|
||||
? (sdk.forConsole.teams.get(project.teamId) as Promise<Organization>)
|
||||
: organization,
|
||||
sdk.forConsoleIn(project.region).console.variables(),
|
||||
isCloud ? sdk.forConsole.billing.getRoles(project.teamId) : null,
|
||||
loadAvailableRegions(project.teamId)
|
||||
]);
|
||||
|
||||
if (!organization) organization = org;
|
||||
|
||||
const roles = rolesResult?.roles ?? defaultRoles;
|
||||
const scopes = rolesResult?.scopes ?? defaultScopes;
|
||||
|
||||
if (prefs?.organization !== project.teamId) {
|
||||
sdk.forConsole.account.updatePrefs({
|
||||
...prefs,
|
||||
organization: project.teamId
|
||||
});
|
||||
}
|
||||
await preferences.loadTeamPrefs(project.teamId);
|
||||
let roles = isCloud ? [] : defaultRoles;
|
||||
let scopes = isCloud ? [] : defaultScopes;
|
||||
if (isCloud) {
|
||||
currentPlan = await sdk.forConsole.billing.getOrganizationPlan(project.teamId);
|
||||
const res = await sdk.forConsole.billing.getRoles(project.teamId);
|
||||
roles = res.roles;
|
||||
scopes = res.scopes;
|
||||
if (scopes.includes('billing.read')) {
|
||||
await failedInvoice.load(project.teamId);
|
||||
if (get(failedInvoice)) {
|
||||
headerAlert.add({
|
||||
show: true,
|
||||
component: PaymentFailed,
|
||||
id: 'paymentFailed',
|
||||
importance: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
preferences.loadTeamPrefs(project.teamId);
|
||||
|
||||
if (isCloud && scopes.includes('billing.read')) {
|
||||
loadFailedInvoices(project.teamId);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -56,6 +55,20 @@ export const load: LayoutLoad = async ({ params, depends }) => {
|
||||
regionalConsoleVariables,
|
||||
roles,
|
||||
scopes,
|
||||
currentPlan
|
||||
currentPlan: plansInfo.get(organization.billingPlan)
|
||||
};
|
||||
};
|
||||
|
||||
// load the invoice and add a banner in bg
|
||||
function loadFailedInvoices(teamId: string) {
|
||||
failedInvoice.load(teamId).then(() => {
|
||||
if (get(failedInvoice)) {
|
||||
headerAlert.add({
|
||||
show: true,
|
||||
component: PaymentFailed,
|
||||
id: 'paymentFailed',
|
||||
importance: 1
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { base } from '$app/paths';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
import { hasOnboardingDismissed } from '$lib/helpers/onboarding';
|
||||
|
||||
export const load: PageLoad = async ({ params }) => {
|
||||
if (hasOnboardingDismissed(params.project)) {
|
||||
redirect(302, `${base}/project-${params.region}-${params.project}/overview`);
|
||||
export const load: PageLoad = async ({ params, parent }) => {
|
||||
const { account } = await parent();
|
||||
const baseRedirectUrl = `${base}/project-${params.region}-${params.project}`;
|
||||
|
||||
if (!hasOnboardingDismissed(params.project, account)) {
|
||||
redirect(302, `${baseRedirectUrl}/get-started`);
|
||||
} else {
|
||||
redirect(302, `${baseRedirectUrl}/overview/platforms`);
|
||||
}
|
||||
redirect(302, `${base}/project-${params.region}-${params.project}/get-started`);
|
||||
};
|
||||
|
||||
@@ -12,40 +12,34 @@ export const load: PageLoad = async ({ url, depends, params }) => {
|
||||
runtimes: url.searchParams.getAll('runtime')
|
||||
};
|
||||
|
||||
let { templates } = await sdk
|
||||
const { templates: allTemplates } = await sdk
|
||||
.forProject(params.region, params.project)
|
||||
.functions.listTemplates(undefined, undefined, 100);
|
||||
|
||||
const [runtimes, useCases] = templates.reduce(
|
||||
([rt, uc], next) => {
|
||||
next.runtimes.forEach((runtime) => rt.add(runtime.name));
|
||||
next.useCases.forEach((useCase) => uc.add(useCase));
|
||||
return [rt, uc];
|
||||
},
|
||||
[new Set<string>(), new Set<string>()]
|
||||
);
|
||||
const runtimes = new Set<string>();
|
||||
const useCases = new Set<string>();
|
||||
|
||||
templates = templates.filter((template) => {
|
||||
for (const template of allTemplates) {
|
||||
template.runtimes.forEach((r) => runtimes.add(r.name));
|
||||
template.useCases.forEach((u) => useCases.add(u));
|
||||
}
|
||||
|
||||
const filterUseCasesLower = filter.useCases.map((u) => u.toLowerCase());
|
||||
|
||||
const templates = allTemplates.filter((template) => {
|
||||
if (
|
||||
filter.runtimes.length > 0 &&
|
||||
!template.runtimes.some((n) => filter.runtimes.includes(n.name))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const filterLowerCases = filter.useCases.map((n) => n.toLowerCase());
|
||||
if (
|
||||
filter.useCases.length > 0 &&
|
||||
!template.useCases.some((n) => filterLowerCases.includes(n.toLowerCase()))
|
||||
!template.useCases.some((u) => filterUseCasesLower.includes(u.toLowerCase()))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
return template.name.toLowerCase().includes(search.toLowerCase());
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return search ? template.name.toLowerCase().includes(search.toLowerCase()) : true;
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -53,7 +47,6 @@ export const load: PageLoad = async ({ url, depends, params }) => {
|
||||
runtimes,
|
||||
useCases,
|
||||
search,
|
||||
templates,
|
||||
functions: await sdk.forProject(params.region, params.project).functions.list()
|
||||
templates
|
||||
};
|
||||
};
|
||||
|
||||
+1
-2
@@ -8,7 +8,6 @@ export const load: PageLoad = async ({ params, depends }) => {
|
||||
return {
|
||||
template: await sdk
|
||||
.forProject(params.region, params.project)
|
||||
.functions.getTemplate(params.template),
|
||||
functions: await sdk.forProject(params.region, params.project).functions.list()
|
||||
.functions.getTemplate(params.template)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
}
|
||||
|
||||
function isTabSelected(key: string) {
|
||||
return page.url.pathname === `${path}/${key}`;
|
||||
return page.url.pathname.endsWith(`/${key}`);
|
||||
}
|
||||
|
||||
$: $registerCommands([
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { base } from '$app/paths';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { get } from 'svelte/store';
|
||||
import type { PageLoad } from './$types';
|
||||
import { selectedTab } from './store';
|
||||
|
||||
export const load: PageLoad = async ({ params }) => {
|
||||
if (get(selectedTab) === 'keys') {
|
||||
redirect(302, `${base}/project-${params.region}-${params.project}/overview/keys`);
|
||||
} else {
|
||||
redirect(302, `${base}/project-${params.region}-${params.project}/overview/platforms`);
|
||||
}
|
||||
redirect(302, `${base}/project-${params.region}-${params.project}/overview/platforms`);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { selectedTab } from '../store';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
selectedTab.set('dev-keys');
|
||||
import { Dependencies } from '$lib/constants';
|
||||
|
||||
export const load: PageLoad = async ({ params, depends }) => {
|
||||
depends(Dependencies.DEV_KEYS);
|
||||
|
||||
@@ -2,26 +2,33 @@
|
||||
import { page } from '$app/state';
|
||||
import { Id, RegionEndpoint } from '$lib/components';
|
||||
import { Cover } from '$lib/layout';
|
||||
import { project } from '../store';
|
||||
import { project, projectRegion } from '../store';
|
||||
import { hasOnboardingDismissed, setHasOnboardingDismissed } from '$lib/helpers/onboarding';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { goto } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { Layout, Button, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
|
||||
function dismissOnboarding() {
|
||||
setHasOnboardingDismissed($project.$id, $user);
|
||||
trackEvent('onboarding_hub_platform_dismiss');
|
||||
goto(`${base}/project-${$project.region}-${$project.$id}/overview/platforms`);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !page.url.pathname.includes('get-started')}
|
||||
<Cover>
|
||||
<svelte:fragment slot="header">
|
||||
<Typography.Title color="--fgcolor-neutral-primary" size="xl">
|
||||
{$project?.name}
|
||||
</Typography.Title>
|
||||
<Layout.Stack alignItems="center" direction="row" inline>
|
||||
<Id value={$project.$id}>{$project.$id}</Id>
|
||||
<RegionEndpoint />
|
||||
<Layout.Stack alignItems="baseline" direction={$isSmallViewport ? 'column' : 'row'}>
|
||||
<Typography.Title color="--fgcolor-neutral-primary" size="xl" truncate>
|
||||
{$project?.name}
|
||||
</Typography.Title>
|
||||
<Layout.Stack direction="row" inline>
|
||||
<Id value={$project.$id}>{$project.$id}</Id>
|
||||
<RegionEndpoint region={$projectRegion} />
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</svelte:fragment>
|
||||
</Cover>
|
||||
@@ -44,16 +51,10 @@
|
||||
>Follow a few quick steps to get started with Appwrite</Typography.Text>
|
||||
</Layout.Stack>
|
||||
<div class="dashboard-header-button">
|
||||
{#if !hasOnboardingDismissed($project.$id)}
|
||||
<Button.Button
|
||||
variant="secondary"
|
||||
size="s"
|
||||
on:click={async () => {
|
||||
trackEvent('onboarding_hub_platform_dismiss');
|
||||
await setHasOnboardingDismissed($project.$id);
|
||||
await invalidate(Dependencies.ORGANIZATION);
|
||||
goto(`${base}/project-${$project.region}-${$project.$id}/overview`);
|
||||
}}>Dismiss this page</Button.Button>
|
||||
{#if !hasOnboardingDismissed($project.$id, $user)}
|
||||
<Button.Button size="s" variant="secondary" on:click={dismissOnboarding}>
|
||||
Dismiss this page
|
||||
</Button.Button>
|
||||
{/if}
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { selectedTab } from '../store';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
selectedTab.set('keys');
|
||||
|
||||
export const load: PageLoad = async ({ params, depends }) => {
|
||||
depends(Dependencies.KEYS);
|
||||
return {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import CreateFlutter from './createFlutter.svelte';
|
||||
import CreateReactNative from './createReactNative.svelte';
|
||||
import CreateWeb from './createWeb.svelte';
|
||||
import { createPlatform, versions } from './wizard/store';
|
||||
import { createPlatform } from './wizard/store';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
|
||||
export enum Platform {
|
||||
@@ -17,13 +17,12 @@
|
||||
}
|
||||
|
||||
export async function addPlatform(type: Platform) {
|
||||
await versions.load();
|
||||
createPlatform.reset();
|
||||
wizard.start(platforms[type]);
|
||||
trackEvent(Click.PlatformCreateClick, {
|
||||
platform: platforms[type],
|
||||
source: 'platforms_page'
|
||||
});
|
||||
wizard.start(platforms[type]);
|
||||
}
|
||||
|
||||
export async function continuePlatform(
|
||||
@@ -32,7 +31,6 @@
|
||||
key: string,
|
||||
type: string
|
||||
) {
|
||||
await versions.load();
|
||||
createPlatform.set({
|
||||
name: name,
|
||||
key: key,
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { selectedTab } from '../store';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
selectedTab.set('platforms');
|
||||
import { Dependencies } from '$lib/constants';
|
||||
|
||||
export const load: PageLoad = async ({ params, depends }) => {
|
||||
depends(Dependencies.PLATFORMS);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { cachedStore } from '$lib/helpers/cache';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
function createPlatformStore() {
|
||||
@@ -31,38 +29,3 @@ function createPlatformStore() {
|
||||
}
|
||||
|
||||
export const createPlatform = createPlatformStore();
|
||||
|
||||
export const versions = cachedStore<
|
||||
{
|
||||
server: string;
|
||||
'client-web': string;
|
||||
'client-flutter': string;
|
||||
'client-apple': string;
|
||||
'client-android': string;
|
||||
'console-web': string;
|
||||
'console-cli': string;
|
||||
'server-nodejs': string;
|
||||
'server-deno': string;
|
||||
'server-php': string;
|
||||
'server-python': string;
|
||||
'server-ruby': string;
|
||||
'server-dart': string;
|
||||
'server-kotlin': string;
|
||||
'server-swift': string;
|
||||
},
|
||||
{
|
||||
load: () => Promise<void>;
|
||||
}
|
||||
>('versions', function ({ set }) {
|
||||
return {
|
||||
load: async () => {
|
||||
const { endpoint, project } = sdk.forConsole.client.config;
|
||||
const response = await fetch(`${endpoint}/../versions`, {
|
||||
headers: {
|
||||
'X-Appwrite-Project': project
|
||||
}
|
||||
});
|
||||
set(await response.json());
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -21,8 +21,6 @@ export const usage = cachedStore<
|
||||
};
|
||||
});
|
||||
|
||||
export const selectedTab: Writable<'platforms' | 'keys' | 'dev-keys'> = writable('platforms');
|
||||
|
||||
export const showDevKeysCreateModal: Writable<boolean> = writable(false);
|
||||
|
||||
export const devKeyColumns = readable<Column[]>([
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { page } from '$app/state';
|
||||
import { Avatar, CardGrid, PaginationInline } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { getApiEndpoint } from '$lib/stores/sdk';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import GitDisconnectModal from './GitDisconnectModal.svelte';
|
||||
import { isSelfHosted } from '$lib/system';
|
||||
@@ -58,9 +58,7 @@
|
||||
function configureGitHub() {
|
||||
const redirect = new URL(page.url);
|
||||
redirect.searchParams.append('alert', 'installation-updated');
|
||||
const target = new URL(
|
||||
`${sdk.forProject(page.params.region, page.params.project).client.config.endpoint}/vcs/github/authorize`
|
||||
);
|
||||
const target = new URL(`${getApiEndpoint(page.params.region)}/vcs/github/authorize`);
|
||||
target.searchParams.set('project', page.params.project);
|
||||
target.searchParams.set('success', redirect.toString());
|
||||
target.searchParams.set('failure', redirect.toString());
|
||||
|
||||
+1
-3
@@ -66,9 +66,7 @@
|
||||
searchText = '';
|
||||
const target = new URL(page.url);
|
||||
target.search = '';
|
||||
goto(target.toString(), {
|
||||
noScroll: true
|
||||
});
|
||||
goto(target.toString(), { noScroll: true });
|
||||
}
|
||||
|
||||
const isChecked = (useCase: string) => {
|
||||
|
||||
+28
-41
@@ -12,14 +12,13 @@ export const load = async ({ url, params }) => {
|
||||
.forProject(params.region, params.project)
|
||||
.sites.listTemplates(undefined, undefined, 100);
|
||||
|
||||
const [frameworksSet, useCasesSet] = siteTemplatesList.templates.reduce(
|
||||
([fr, uc], next) => {
|
||||
next.useCases.forEach((useCase) => uc.add(useCase));
|
||||
next.frameworks.forEach((framework) => fr.add(framework.name));
|
||||
return [fr, uc];
|
||||
},
|
||||
[new Set<string>(), new Set<string>()]
|
||||
);
|
||||
const useCasesSet = new Set<string>();
|
||||
const frameworksSet = new Set<string>();
|
||||
|
||||
for (const template of siteTemplatesList.templates) {
|
||||
template.frameworks.forEach((f) => frameworksSet.add(f.name));
|
||||
template.useCases.forEach((u) => useCasesSet.add(u));
|
||||
}
|
||||
|
||||
const frameworkOrder = [
|
||||
'Next.js',
|
||||
@@ -37,44 +36,32 @@ export const load = async ({ url, params }) => {
|
||||
'Vite',
|
||||
'Other'
|
||||
];
|
||||
const frameworks = Array.from(frameworksSet)
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.sort((a, b) => {
|
||||
const aIndex = frameworkOrder.indexOf(a);
|
||||
const bIndex = frameworkOrder.indexOf(b);
|
||||
if (aIndex === -1 && bIndex === -1) {
|
||||
return a.localeCompare(b);
|
||||
} else if (aIndex === -1) {
|
||||
return 1;
|
||||
} else if (bIndex === -1) {
|
||||
return -1;
|
||||
}
|
||||
return aIndex - bIndex;
|
||||
});
|
||||
|
||||
const useCases = Array.from(useCasesSet).sort((a, b) => a.localeCompare(b));
|
||||
const frameworks = Array.from(frameworksSet).sort((a, b) => {
|
||||
const aIndex = frameworkOrder.indexOf(a);
|
||||
const bIndex = frameworkOrder.indexOf(b);
|
||||
if (aIndex === -1 && bIndex === -1) return a.localeCompare(b);
|
||||
if (aIndex === -1) return 1;
|
||||
if (bIndex === -1) return -1;
|
||||
return aIndex - bIndex;
|
||||
});
|
||||
|
||||
const useCases = Array.from(useCasesSet).sort();
|
||||
|
||||
const templates = siteTemplatesList.templates.filter((template) => {
|
||||
if (
|
||||
filter.frameworks.length > 0 &&
|
||||
!template.frameworks.some((n) => filter.frameworks.includes(n.name))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const matchesFramework =
|
||||
filter.frameworks.length === 0 ||
|
||||
template.frameworks.some((f) => filter.frameworks.includes(f.name));
|
||||
|
||||
const filterLowerCases = filter.useCases.map((n) => n.toLowerCase());
|
||||
if (
|
||||
filter.useCases.length > 0 &&
|
||||
!template.useCases.some((n) => filterLowerCases.includes(n.toLowerCase()))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const matchesUseCase =
|
||||
filter.useCases.length === 0 ||
|
||||
template.useCases.some((u) =>
|
||||
filter.useCases.map((s) => s.toLowerCase()).includes(u.toLowerCase())
|
||||
);
|
||||
|
||||
if (search) {
|
||||
return template.name.toLowerCase().includes(search.toLowerCase());
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
const matchesSearch = !search || template.name.toLowerCase().includes(search.toLowerCase());
|
||||
|
||||
return matchesFramework && matchesUseCase && matchesSearch;
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -4,14 +4,11 @@ import { derived, get, writable } from 'svelte/store';
|
||||
import { regions } from '$lib/stores/organization';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
export const project = derived(
|
||||
page,
|
||||
($page) => $page.data.project as Models.Project & { region?: string }
|
||||
);
|
||||
export const project = derived(page, ($page) => $page.data.project as Models.Project);
|
||||
|
||||
export const projectRegion = derived(project, ($project) => {
|
||||
const availableRegions = get(regions);
|
||||
// region is marked as nullable above.
|
||||
// region could be null or undefined in project.
|
||||
if (!availableRegions || !availableRegions.regions || !$project || !$project.region)
|
||||
return null;
|
||||
|
||||
|
||||
@@ -4,15 +4,15 @@ import type { Organization } from '$lib/stores/organization';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
|
||||
export const version = derived(page, ($page) => $page.data.version as string | null);
|
||||
export const version = derived(page, ($page) => $page.data.version ?? null);
|
||||
|
||||
export const consoleVariables = derived(
|
||||
page,
|
||||
($page) => $page.data.consoleVariables as Models.ConsoleVariables
|
||||
);
|
||||
export const protocol = derived(page, ($page) =>
|
||||
($page.data.consoleVariables as Models.ConsoleVariables)?._APP_OPTIONS_FORCE_HTTPS === 'enabled'
|
||||
? 'https://'
|
||||
: 'http://'
|
||||
|
||||
export const protocol = derived(consoleVariables, ($vars) =>
|
||||
$vars?._APP_OPTIONS_FORCE_HTTPS === 'enabled' ? 'https://' : 'http://'
|
||||
);
|
||||
|
||||
export const activeHeaderAlert = writable<HeaderAlert>(null);
|
||||
|
||||
Reference in New Issue
Block a user