diff --git a/package.json b/package.json index 428927dc57..348f13dbee 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "e2e:ui": "playwright test tests/e2e --ui" }, "dependencies": { - "@appwrite.io/console": "1.4.7", + "@appwrite.io/console": "1.5.1", "@appwrite.io/pink": "0.25.0", "@appwrite.io/pink-icons": "0.25.0", "@popperjs/core": "^2.11.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a31a13a9ef..911bb0c01e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@appwrite.io/console': - specifier: 1.4.7 - version: 1.4.7 + specifier: 1.5.1 + version: 1.5.1 '@appwrite.io/pink': specifier: 0.25.0 version: 0.25.0 @@ -199,8 +199,8 @@ packages: '@analytics/type-utils@0.6.2': resolution: {integrity: sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg==} - '@appwrite.io/console@1.4.7': - resolution: {integrity: sha512-5zx+c5nWRm/UJNxgtOi4vj1pYW+wusfnOX2hqEGDLLuNZRc1rViYRefNuezosp5SPtjClCIYn9TGuFlz/XLwhw==} + '@appwrite.io/console@1.5.1': + resolution: {integrity: sha512-H0fkBprsxXjOhbrE+MqXt1e4Gx4QeRdHuvMs7UxqGr2fAVEqh4ez2yk40A0ZSQvvN+rqLduoItMHQFl+sCPbtQ==} '@appwrite.io/pink-icons@0.25.0': resolution: {integrity: sha512-0O3i2oEuh5mWvjO80i+X6rbzrWLJ1m5wmv2/M3a1p2PyBJsFxN8xQMTEmTn3Wl/D26SsM7SpzbdW6gmfgoVU9Q==} @@ -3834,7 +3834,7 @@ snapshots: '@analytics/type-utils@0.6.2': {} - '@appwrite.io/console@1.4.7': {} + '@appwrite.io/console@1.5.1': {} '@appwrite.io/pink-icons@0.25.0': {} diff --git a/src/lib/charts/bar.svelte b/src/lib/charts/bar.svelte index a447492d27..9d19e36053 100644 --- a/src/lib/charts/bar.svelte +++ b/src/lib/charts/bar.svelte @@ -13,7 +13,6 @@ {formatted} series={series.map((s) => { s.type = 'bar'; - s.stack = 'total'; s.barMaxWidth = 6; s.itemStyle = { borderRadius: [10, 10, 0, 0] diff --git a/src/lib/charts/index.ts b/src/lib/charts/index.ts index c73108a134..488756ecb5 100644 --- a/src/lib/charts/index.ts +++ b/src/lib/charts/index.ts @@ -1,2 +1,3 @@ export { default as BarChart } from './bar.svelte'; export { default as LineChart } from './line.svelte'; +export { default as Legend, type LegendData } from './legend.svelte'; diff --git a/src/lib/charts/legend.svelte b/src/lib/charts/legend.svelte new file mode 100644 index 0000000000..f8ddc63558 --- /dev/null +++ b/src/lib/charts/legend.svelte @@ -0,0 +1,23 @@ + + + + +
+ {#each legendData as { name, value }, index} + + {name} ({value}) + + {/each} +
diff --git a/src/lib/components/status.svelte b/src/lib/components/status.svelte index 00ed3353e5..7dc78c84ec 100644 --- a/src/lib/components/status.svelte +++ b/src/lib/components/status.svelte @@ -6,17 +6,35 @@ | 'completed' | 'processing' | 'ready' - | 'building'; + | 'building' + | 'none'; + + export let statusIconStyle: string | undefined = undefined;
{#if status} - + {/if}
+ + diff --git a/src/lib/layout/index.ts b/src/lib/layout/index.ts index e45069dd11..fa8289f73b 100644 --- a/src/lib/layout/index.ts +++ b/src/lib/layout/index.ts @@ -13,6 +13,7 @@ export { default as WizardStep } from './wizardStep.svelte'; export { default as Breadcrumbs } from './breadcrumbs.svelte'; export { default as Unauthenticated } from './unauthenticated.svelte'; export { default as Usage, type UsagePeriods } from './usage.svelte'; +export { default as UsageMultiple } from './usageMultiple.svelte'; export { default as Activity } from './activity.svelte'; export { default as Progress } from './progress.svelte'; export { default as GridHeader } from './gridHeader.svelte'; diff --git a/src/lib/layout/usage.svelte b/src/lib/layout/usage.svelte index 8f392847f8..d9f31bc11b 100644 --- a/src/lib/layout/usage.svelte +++ b/src/lib/layout/usage.svelte @@ -43,7 +43,7 @@ metrics: Models.Metric[], endingTotal: number ): Array<[string, number]> { - return metrics.reduceRight( + return (metrics ?? []).reduceRight( (acc, curr) => { acc.data.unshift([curr.date, acc.total]); acc.total -= curr.value; diff --git a/src/lib/layout/usageMultiple.svelte b/src/lib/layout/usageMultiple.svelte new file mode 100644 index 0000000000..81e9710b82 --- /dev/null +++ b/src/lib/layout/usageMultiple.svelte @@ -0,0 +1,74 @@ + + + +
+ {#if showHeader} + {title} + {/if} + + {#if path} + + + 24h + + + 30d + + + 90d + + + {/if} +
+ + {#if count} + {@const totalCount = total.reduce((a, b) => a + b, 0)} + + {formatNumberWithCommas(totalCount)} +

Total {title.toLocaleLowerCase()}

+
+ +
+ ({ + name: legendData[index].name, + data: accumulateFromEndingTotal(c, total[index]) + }))} /> + + {#if legendData} + + {/if} +
+ {/if} + + + + diff --git a/src/lib/sdk/billing.ts b/src/lib/sdk/billing.ts index 95c9f5c77d..32e65b6a1e 100644 --- a/src/lib/sdk/billing.ts +++ b/src/lib/sdk/billing.ts @@ -179,9 +179,13 @@ export type Aggregation = { export type OrganizationUsage = { bandwidth: Array; executions: Array; + databasesReads: Array; + databasesWrites: Array; executionsTotal: number; filesStorageTotal: number; buildsStorageTotal: number; + databasesReadsTotal: number; + databasesWritesTotal: number; deploymentsStorageTotal: number; executionsMBSecondsTotal: number; buildsMBSecondsTotal: number; diff --git a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte index 3cebea75e6..d4b23112ab 100644 --- a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte +++ b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte @@ -10,7 +10,7 @@ import { organization } from '$lib/stores/organization'; import { Button } from '$lib/elements/forms'; import { bytesToSize, humanFileSize, mbSecondsToGBHours } from '$lib/helpers/sizeConvertion'; - import { BarChart } from '$lib/charts'; + import { BarChart, Legend } from '$lib/charts'; import ProjectBreakdown from './ProjectBreakdown.svelte'; import { formatNum } from '$lib/helpers/string'; import { accumulateFromEndingTotal, total } from '$lib/layout/usage.svelte'; @@ -29,6 +29,11 @@ const plan = data?.plan ?? undefined; $: project = (data.organizationUsage as OrganizationUsage).projects; + + $: legendData = [ + { name: 'Reads', value: data.organizationUsage.databasesReadsTotal }, + { name: 'Writes', value: data.organizationUsage.databasesWritesTotal } + ]; @@ -133,7 +138,7 @@
@@ -193,6 +198,67 @@ + + Database reads and writes + +

+ The total number of database reads and writes across all projects in your organization. +

+ + {#if data.organizationUsage.databasesReads || data.organizationUsage.databasesWrites} +
+ [ + e.date, + e.value + ]) + ] + }, + { + name: 'Writes', + data: [ + ...(data.organizationUsage.databasesWrites ?? []).map((e) => [ + e.date, + e.value + ]) + ] + } + ]} /> +
+ + + + {#if project?.length > 0} + + {/if} + {:else} + +
+
+
+ {/if} +
+
+ Executions @@ -318,6 +384,7 @@ {/if} + GB hours @@ -378,6 +445,7 @@ {/if} + Phone OTP

@@ -430,6 +498,7 @@ {/if} +

diff --git a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.ts b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.ts index 4138acfa16..90d64d7d12 100644 --- a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.ts +++ b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.ts @@ -29,7 +29,11 @@ export const load: PageLoad = async ({ params, parent }) => { executionsMBSecondsTotal: null, buildsMBSecondsTotal: null, authPhoneTotal: null, - authPhoneEstimate: null + authPhoneEstimate: null, + databasesReads: null, + databasesWrites: null, + databasesReadsTotal: null, + databasesWritesTotal: null } }; } diff --git a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/ProjectBreakdown.svelte b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/ProjectBreakdown.svelte index 8542fe75c5..7bd50227c2 100644 --- a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/ProjectBreakdown.svelte +++ b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/ProjectBreakdown.svelte @@ -15,14 +15,26 @@ import type { OrganizationUsage } from '$lib/sdk/billing'; import { base } from '$app/paths'; import { canSeeProjects } from '$lib/stores/roles'; + import { onMount } from 'svelte'; + + type Metric = + | 'users' + | 'storage' + | 'bandwidth' + | 'executions' + | 'authPhoneTotal' + | 'databasesReads' + | 'databasesWrites'; - type Metric = 'users' | 'storage' | 'bandwidth' | 'executions' | 'authPhoneTotal'; type Estimate = 'authPhoneEstimate'; + type DatabaseOperationMetric = Extract; + export let data: PageData; export let projects: OrganizationUsage['projects']; - export let metric: Metric; + export let metric: Metric | undefined = undefined; export let estimate: Estimate | undefined = undefined; + export let databaseOperationMetric: DatabaseOperationMetric[] | undefined = undefined; function getMetricTitle(metric: Metric): string { switch (metric) { @@ -38,25 +50,48 @@ } function groupByProject( - metric: Metric, - estimate?: Estimate - ): Array<{ projectId: string; usage: number; estimate?: number }> { + metric: Metric | undefined, + estimate?: Estimate, + databaseOps?: DatabaseOperationMetric[] + ): Array<{ + projectId: string; + databasesReads?: number; + databasesWrites?: number; + usage?: number; + estimate?: number; + }> { const data = []; for (const project of projects) { - const usage = project[metric]; - if (!usage) { - continue; + if (metric) { + const usage = project[metric]; + if (!usage) continue; + + data.push({ + projectId: project.projectId, + usage: usage ?? 0, + estimate: estimate ? project[estimate] : undefined + }); + } else if (databaseOps) { + const reads = project['databasesReads'] ?? 0; + const writes = project['databasesWrites'] ?? 0; + + if (reads || writes) { + data.push({ + projectId: project.projectId, + databasesReads: reads, + databasesWrites: writes + }); + } } - data.push({ - projectId: project.projectId, - usage: usage ?? 0, - estimate: estimate ? project[estimate] : undefined - }); } return data; } function format(value: number): string { + if (databaseOperationMetric) { + return abbreviateNumber(value); + } + switch (metric) { case 'authPhoneTotal': return formatNumberWithCommas(value); @@ -68,6 +103,12 @@ return humanFileSize(value).value + humanFileSize(value).unit; } } + + onMount(() => { + if (metric === undefined && databaseOperationMetric === undefined) { + throw new Error(`metric or database operations must be defined`); + } + }); @@ -76,7 +117,13 @@ Project - {getMetricTitle(metric)} + {#if databaseOperationMetric} + Reads + Writes + {:else} + {getMetricTitle(metric)} + {/if} + {#if estimate} Estimated cost {/if} @@ -85,17 +132,33 @@ {/if} - {#each groupByProject(metric, estimate).sort((a, b) => b.usage - a.usage) as project} + {#each groupByProject(metric, estimate, databaseOperationMetric).sort((a, b) => { + const aValue = a.usage ?? a.databasesReads ?? 0; + const bValue = b.usage ?? b.databasesReads ?? 0; + return bValue - aValue; + }) as project} {#if !$canSeeProjects} {data.projectNames[project.projectId]?.name ?? 'Unknown'} - {format(project.usage)} + {#if databaseOperationMetric} + + {format(project.databasesReads ?? 0)} + + + {format(project.databasesWrites ?? 0)} + + {:else} + + {format(project.usage)} + + {/if} + {#if project.estimate} - {formatCurrency(project.estimate)} + + {formatCurrency(project.estimate)} + {/if} {:else} @@ -103,11 +166,23 @@ {data.projectNames[project.projectId]?.name ?? 'Unknown'} - {format(project.usage)} + {#if databaseOperationMetric} + + {format(project.databasesReads ?? 0)} + + + {format(project.databasesWrites ?? 0)} + + {:else} + + {format(project.usage)} + + {/if} + {#if project.estimate} - {formatCurrency(project.estimate)} + + {formatCurrency(project.estimate)} + {/if} import { base } from '$app/paths'; import { page } from '$app/stores'; - import { Usage } from '$lib/layout'; + import { Usage, UsageMultiple } from '$lib/layout'; import type { PageData } from './$types'; export let data: PageData; $: total = data.collectionsTotal; $: count = data.collections; + + $: reads = data.databaseReads; + $: readsTotal = data.databaseReadsTotal; + + $: writes = data.databaseWrites; + $: writesTotal = data.databaseWritesTotal; - +

+ + + +
diff --git a/src/routes/(console)/project-[project]/databases/usage/[[period]]/+page.svelte b/src/routes/(console)/project-[project]/databases/usage/[[period]]/+page.svelte index d261dd5f98..8c41fdc755 100644 --- a/src/routes/(console)/project-[project]/databases/usage/[[period]]/+page.svelte +++ b/src/routes/(console)/project-[project]/databases/usage/[[period]]/+page.svelte @@ -1,20 +1,39 @@ - +
+ + + +
diff --git a/src/routes/(console)/project-[project]/settings/usage/[[invoice]]/+page.svelte b/src/routes/(console)/project-[project]/settings/usage/[[invoice]]/+page.svelte index 49a6215280..a0d8c24109 100644 --- a/src/routes/(console)/project-[project]/settings/usage/[[invoice]]/+page.svelte +++ b/src/routes/(console)/project-[project]/settings/usage/[[invoice]]/+page.svelte @@ -14,7 +14,7 @@ import { organization } from '$lib/stores/organization'; import { Button } from '$lib/elements/forms'; import { bytesToSize, humanFileSize, mbSecondsToGBHours } from '$lib/helpers/sizeConvertion'; - import { BarChart } from '$lib/charts'; + import { BarChart, Legend } from '$lib/charts'; import { formatNum } from '$lib/helpers/string'; import { total } from '$lib/layout/usage.svelte'; import { BillingPlan } from '$lib/constants.js'; @@ -37,6 +37,14 @@ data.usage.deploymentsStorageTotal + data.usage.buildsStorageTotal; + $: dbReads = data.usage.databasesReads; + $: dbWrites = data.usage.databasesWrites; + + $: legendData = [ + { name: 'Reads', value: data.usage.databasesReadsTotal }, + { name: 'Writes', value: data.usage.databasesWritesTotal } + ]; + const tier = data?.currentInvoice?.plan ?? $organization?.billingPlan; const plan = tierToPlan(tier).name; @@ -196,6 +204,48 @@ {/if}
+ + Database reads and writes + +

Total database reads and writes in your project.

+ + + {#if dbReads || dbWrites} +
+ [e.date, e.value])] + }, + { + name: 'Writes', + data: [...dbWrites.map((e) => [e.date, e.value])] + } + ]} /> +
+ + + {:else} + +
+
+
+ {/if} +
+
Executions