m.userName) ?? [];
+ $: avatars = $members.memberships?.map((m) => m.userName || m.userEmail) ?? [];
$: organizationId = $page.params.organization;
$: path = `${base}/organization-${organizationId}`;
$: tabs = [
diff --git a/src/routes/(console)/organization-[organization]/settings/+page.svelte b/src/routes/(console)/organization-[organization]/settings/+page.svelte
index c70eb1a8e..98b236e6d 100644
--- a/src/routes/(console)/organization-[organization]/settings/+page.svelte
+++ b/src/routes/(console)/organization-[organization]/settings/+page.svelte
@@ -42,7 +42,7 @@
}
}
- $: avatars = $members.memberships.map((team) => team.userName);
+ $: avatars = $members.memberships.map((m) => m.userName || m.userEmail);
$: orgProjects = `${$projects.total} ${$projects.total === 1 ? 'project' : 'projects'}`;
$: orgMembers = `${$organization.total} ${$organization.total === 1 ? 'member' : 'members'}`;
diff --git a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte
index 82c211c19..3cebea75e 100644
--- a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte
+++ b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte
@@ -18,6 +18,8 @@
import { BillingPlan } from '$lib/constants';
import { trackEvent } from '$lib/actions/analytics';
import TotalMembers from './totalMembers.svelte';
+ import { tooltip } from '$lib/actions/tooltip';
+ import { formatCurrency, formatNumberWithCommas } from '$lib/helpers/numbers';
export let data;
@@ -143,7 +145,6 @@
Users
The total number of users across all projects in your organization.
-
{#if data.organizationUsage.users}
{@const current = data.organizationUsage.usersTotal}
@@ -169,7 +170,7 @@
{
name: 'Users',
data: accumulateFromEndingTotal(
- data.organizationUsage.users,
+ data.usersUsageToDate,
data.organizationUsage.usersTotal
)
}
@@ -377,6 +378,58 @@
{/if}
+
+ Phone OTP
+
+ OTPs are billed per SMS message, with rates varying by recipient country. For a detailed
+ cost breakdown, see the pricing page.
+
+ You will not be charged for Phone OTPs before February 10th.
+
+ {#if data.organizationUsage.authPhoneTotal}
+
+
+ {formatNumberWithCommas(data.organizationUsage.authPhoneTotal)}
+ OTPs
+
+
+ Estimated cost
+
+ {formatCurrency(data.organizationUsage.authPhoneEstimate)}
+
+
+
+
+
+ {#if project?.length > 0}
+
+ {/if}
+ {:else}
+
+
+
+ {/if}
+
+
diff --git a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.ts b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.ts
index b3546b01d..a8cfd0793 100644
--- a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.ts
+++ b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.ts
@@ -27,7 +27,9 @@ export const load: PageLoad = async ({ params, parent }) => {
executionsTotal: null,
projects: null,
executionsMBSecondsTotal: null,
- buildsMBSecondsTotal: null
+ buildsMBSecondsTotal: null,
+ authPhoneTotal: null,
+ authPhoneEstimate: null
}
};
}
@@ -72,12 +74,15 @@ export const load: PageLoad = async ({ params, parent }) => {
}
}
+ const usersUsageToDate = usage.users.filter((user) => new Date(user.date) < new Date());
+
return {
organizationUsage: usage,
projectNames,
invoices,
currentInvoice,
organizationMembers,
- plan
+ plan,
+ usersUsageToDate
};
};
diff --git a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/ProjectBreakdown.svelte b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/ProjectBreakdown.svelte
index 5516b61b7..8542fe75c 100644
--- a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/ProjectBreakdown.svelte
+++ b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/ProjectBreakdown.svelte
@@ -10,42 +10,62 @@
TableRowLink,
TableScroll
} from '$lib/elements/table';
- import { abbreviateNumber } from '$lib/helpers/numbers';
+ import { abbreviateNumber, formatCurrency, formatNumberWithCommas } from '$lib/helpers/numbers';
import { humanFileSize } from '$lib/helpers/sizeConvertion';
import type { OrganizationUsage } from '$lib/sdk/billing';
import { base } from '$app/paths';
import { canSeeProjects } from '$lib/stores/roles';
- type Metric = 'users' | 'storage' | 'bandwidth' | 'executions';
+ type Metric = 'users' | 'storage' | 'bandwidth' | 'executions' | 'authPhoneTotal';
+ type Estimate = 'authPhoneEstimate';
+
export let data: PageData;
export let projects: OrganizationUsage['projects'];
export let metric: Metric;
+ export let estimate: Estimate | undefined = undefined;
+
+ function getMetricTitle(metric: Metric): string {
+ switch (metric) {
+ case 'authPhoneTotal':
+ return 'Amount';
+ default:
+ return 'Usage';
+ }
+ }
function getProjectUsageLink(projectId: string): string {
return `${base}/project-${projectId}/settings/usage`;
}
- function groupByProject(metric: Metric): Array<{ projectId: string; usage: number }> {
+ function groupByProject(
+ metric: Metric,
+ estimate?: Estimate
+ ): Array<{ projectId: string; usage: number; estimate?: number }> {
const data = [];
for (const project of projects) {
const usage = project[metric];
+ if (!usage) {
+ continue;
+ }
data.push({
projectId: project.projectId,
- usage: usage ?? 0
+ usage: usage ?? 0,
+ estimate: estimate ? project[estimate] : undefined
});
}
return data;
}
function format(value: number): string {
- const humanized = humanFileSize(value);
switch (metric) {
+ case 'authPhoneTotal':
+ return formatNumberWithCommas(value);
case 'executions':
case 'users':
return abbreviateNumber(value);
case 'storage':
case 'bandwidth':
- return humanized.value + humanized.unit;
+ return humanFileSize(value).value + humanFileSize(value).unit;
}
}
@@ -53,29 +73,42 @@
Project breakdown
-
+
Project
- Usage
+ {getMetricTitle(metric)}
+ {#if estimate}
+ Estimated cost
+ {/if}
{#if $canSeeProjects}
{/if}
- {#each groupByProject(metric).sort((a, b) => b.usage - a.usage) as project}
+ {#each groupByProject(metric, estimate).sort((a, b) => b.usage - a.usage) as project}
{#if !$canSeeProjects}
{data.projectNames[project.projectId]?.name ?? 'Unknown'}
- {format(project.usage)}
+ {format(project.usage)}
+ {#if project.estimate}
+ {formatCurrency(project.estimate)}
+ {/if}
{:else}
{data.projectNames[project.projectId]?.name ?? 'Unknown'}
- {format(project.usage)}
+ {format(project.usage)}
+ {#if project.estimate}
+ {formatCurrency(project.estimate)}
+ {/if}
diff --git a/src/routes/(console)/project-[project]/databases/database-[database]/collection-[collection]/attributes/edit.svelte b/src/routes/(console)/project-[project]/databases/database-[database]/collection-[collection]/attributes/edit.svelte
index 1676df802..1ba209dee 100644
--- a/src/routes/(console)/project-[project]/databases/database-[database]/collection-[collection]/attributes/edit.svelte
+++ b/src/routes/(console)/project-[project]/databases/database-[database]/collection-[collection]/attributes/edit.svelte
@@ -65,13 +65,19 @@
}
-
+
{option?.name}
{#if option?.type === 'relationship'}
- Beta
+ Experimental
{/if}
diff --git a/src/routes/(console)/project-[project]/databases/database-[database]/collection-[collection]/createAttribute.svelte b/src/routes/(console)/project-[project]/databases/database-[database]/collection-[collection]/createAttribute.svelte
index 06420a1a1..f873a94fd 100644
--- a/src/routes/(console)/project-[project]/databases/database-[database]/collection-[collection]/createAttribute.svelte
+++ b/src/routes/(console)/project-[project]/databases/database-[database]/collection-[collection]/createAttribute.svelte
@@ -66,13 +66,19 @@
}
-
+
{#if selectedOption === 'Relationship'}
{selectedOption}
- Beta
+ Experimental
{:else}
diff --git a/src/routes/(console)/project-[project]/databases/database-[database]/collection-[collection]/indexes/createIndex.svelte b/src/routes/(console)/project-[project]/databases/database-[database]/collection-[collection]/indexes/createIndex.svelte
index 5c0bba01f..634de7ffa 100644
--- a/src/routes/(console)/project-[project]/databases/database-[database]/collection-[collection]/indexes/createIndex.svelte
+++ b/src/routes/(console)/project-[project]/databases/database-[database]/collection-[collection]/indexes/createIndex.svelte
@@ -2,7 +2,7 @@
import { goto, invalidate } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/stores';
- import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
+ import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, FormList, InputSelect, InputText } from '$lib/elements/forms';
@@ -11,8 +11,7 @@
import { sdk } from '$lib/stores/sdk';
import { IndexType } from '@appwrite.io/console';
import { isRelationship } from '../document-[document]/attributes/store';
- import { indexes, type Attributes } from '../store';
- import { collection } from '../store';
+ import { type Attributes, collection, indexes } from '../store';
import Select from './select.svelte';
export let showCreateIndex = false;
@@ -20,8 +19,8 @@
const databaseId = $page.params.database;
+ let key = '';
let error: string;
- let key = `index_${$indexes.length + 1}`;
let types = [
{ value: IndexType.Key, label: 'Key' },
{ value: IndexType.Unique, label: 'Unique' },
@@ -38,6 +37,17 @@
let attributeList = [{ value: '', order: '' }];
+ function generateIndexKey() {
+ let indexKeys = $indexes.map((index) => index.key);
+
+ let highestIndex = indexKeys.reduce((max, key) => {
+ const match = key.match(/^index_(\d+)$/);
+ return match ? Math.max(max, parseInt(match[1], 10)) : max;
+ }, indexKeys.length);
+
+ return `index_${highestIndex + 1}`;
+ }
+
function initialize() {
attributeList = externalAttribute
? [{ value: externalAttribute.key, order: 'ASC' }]
@@ -49,6 +59,7 @@
$: if (showCreateIndex) {
error = null;
initialize();
+ key = generateIndexKey();
}
$: addAttributeDisabled = !attributeList.at(-1)?.value || !attributeList.at(-1)?.order;
diff --git a/src/routes/(console)/project-[project]/functions/+layout.ts b/src/routes/(console)/project-[project]/functions/+layout.ts
index cab65d7c3..8d7b68a16 100644
--- a/src/routes/(console)/project-[project]/functions/+layout.ts
+++ b/src/routes/(console)/project-[project]/functions/+layout.ts
@@ -8,10 +8,11 @@ import type { LayoutLoad } from './$types';
export const load: LayoutLoad = async ({ depends }) => {
depends(Dependencies.FUNCTION_INSTALLATIONS);
- const [runtimesList, installations, templatesList] = await Promise.all([
+ const [runtimesList, installations, templatesList, specificationsList] = await Promise.all([
sdk.forProject.functions.listRuntimes(),
sdk.forProject.vcs.listInstallations([Query.limit(100)]),
- sdk.forProject.functions.listTemplates(undefined, undefined, 100)
+ sdk.forProject.functions.listTemplates(undefined, undefined, 100),
+ sdk.forProject.functions.listSpecifications()
]);
return {
@@ -19,6 +20,7 @@ export const load: LayoutLoad = async ({ depends }) => {
breadcrumbs: Breadcrumbs,
runtimesList,
installations,
- templatesList
+ templatesList,
+ specificationsList
};
};
diff --git a/src/routes/(console)/project-[project]/functions/function-[function]/settings/updateRuntime.svelte b/src/routes/(console)/project-[project]/functions/function-[function]/settings/updateRuntime.svelte
index d7925fea5..1f06dbe9c 100644
--- a/src/routes/(console)/project-[project]/functions/function-[function]/settings/updateRuntime.svelte
+++ b/src/routes/(console)/project-[project]/functions/function-[function]/settings/updateRuntime.svelte
@@ -3,30 +3,46 @@
import { page } from '$app/stores';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid, Heading } from '$lib/components';
- import { Dependencies } from '$lib/constants';
+ import { BillingPlan, Dependencies } from '$lib/constants';
import { Button, Form, FormList } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { func } from '../store';
import InputSelect from '$lib/elements/forms/inputSelect.svelte';
+ import { specificationsList } from '$lib/stores/specifications';
import { runtimesList } from '$lib/stores/runtimes';
import { isValueOfStringEnum } from '$lib/helpers/types';
import { Runtime } from '@appwrite.io/console';
+ import { isCloud } from '$lib/system';
+ import { organization } from '$lib/stores/organization';
+ import SpecificationsTooltip from '$lib/wizards/functions/components/specificationsTooltip.svelte';
const functionId = $page.params.function;
let runtime: string = null;
+ let specification: string = null;
let options = [];
+ let specificationOptions = [];
onMount(async () => {
runtime ??= $func.runtime;
+ specification ??= $func.specification;
let runtimes = await $runtimesList;
+ let allowedSpecifications = (await $specificationsList).specifications;
options = runtimes.runtimes.map((runtime) => ({
label: `${runtime.name} - ${runtime.version}`,
value: runtime.$id
}));
+
+ specificationOptions = allowedSpecifications.map((size) => ({
+ label:
+ `${size.cpus} CPU, ${size.memory} MB RAM` +
+ (!size.enabled ? ` (Upgrade to use this)` : ''),
+ value: size.slug,
+ disabled: !size.enabled
+ }));
});
async function updateRuntime() {
@@ -51,11 +67,12 @@
$func.providerRepositoryId || undefined,
$func.providerBranch || undefined,
$func.providerSilentMode || undefined,
- $func.providerRootDirectory || undefined
+ $func.providerRootDirectory || undefined,
+ specification
);
await invalidate(Dependencies.FUNCTION);
addNotification({
- message: 'Runtime has been updated',
+ message: 'Runtime settings have been updated',
type: 'success'
});
trackEvent(Submit.FunctionUpdateName);
@@ -67,6 +84,8 @@
trackError(error, Submit.FunctionUpdateName);
}
}
+
+ $: isUpdateButtonEnabled = runtime !== $func?.runtime || specification !== $func?.specification;