diff --git a/package-lock.json b/package-lock.json index 9f8794aea9..ed28f0e906 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "@appwrite/console", "dependencies": { - "@appwrite.io/console": "^0.3.0", + "@appwrite.io/console": "^0.4.1", "@appwrite.io/pink": "0.2.0", "@appwrite.io/pink-icons": "0.2.0", "@popperjs/core": "^2.11.8", @@ -159,9 +159,9 @@ "integrity": "sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg==" }, "node_modules/@appwrite.io/console": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@appwrite.io/console/-/console-0.3.0.tgz", - "integrity": "sha512-XHKQHirzQliXoDpMLakSdXwpq9aPoeTABNhW8Z7MK3p6oBfp2gMplDD43Fbd91JREXoJ+RG8okX0z0nIc6hDiQ==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@appwrite.io/console/-/console-0.4.1.tgz", + "integrity": "sha512-GxT6bz1ct0dpvvp6xjVSp/VVUCGxaEAG11vi2KTWjJvRlITwHLYNXt5iycx+pe65gc2FNGTsApcCr1d6sRqLsQ==", "dependencies": { "cross-fetch": "3.1.5", "isomorphic-form-data": "2.0.0" diff --git a/package.json b/package.json index cdea2dea34..8f85b2d766 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "e2e": "playwright test tests/e2e" }, "dependencies": { - "@appwrite.io/console": "^0.3.0", + "@appwrite.io/console": "^0.4.1", "@appwrite.io/pink": "0.2.0", "@appwrite.io/pink-icons": "0.2.0", "@popperjs/core": "^2.11.8", diff --git a/src/lib/components/progressBarBig.svelte b/src/lib/components/progressBarBig.svelte index 4bd00e6258..62777b97c1 100644 --- a/src/lib/components/progressBarBig.svelte +++ b/src/lib/components/progressBarBig.svelte @@ -7,7 +7,7 @@ export let progressMax: number; export let showBar = true; - $: progress = Math.round((progressValue / progressMax) * 100); + $: progress = Math.min(Math.max(Math.round((progressValue / progressMax) * 100), 0), 100);
diff --git a/src/lib/helpers/project.ts b/src/lib/helpers/project.ts index a8a88cd3f7..91cfc90ac8 100644 --- a/src/lib/helpers/project.ts +++ b/src/lib/helpers/project.ts @@ -1,5 +1,6 @@ export function getProjectId() { const pathname = window.location.pathname + '/'; const projectMatch = pathname.match(/\/project-(.*?)\//); + return projectMatch?.[1] || null; } diff --git a/src/lib/layout/usage.svelte b/src/lib/layout/usage.svelte index ea571a4c60..05f4407f73 100644 --- a/src/lib/layout/usage.svelte +++ b/src/lib/layout/usage.svelte @@ -1,13 +1,38 @@ diff --git a/src/lib/sdk/billing.ts b/src/lib/sdk/billing.ts index 1d785bf989..c073606cde 100644 --- a/src/lib/sdk/billing.ts +++ b/src/lib/sdk/billing.ts @@ -512,15 +512,21 @@ export class Billing { async listUsage( organizationId: string, - startDate: string = null, - endDate: string = null + startDate: string = undefined, + endDate: string = undefined ): Promise { const path = `/organizations/${organizationId}/usage`; const params = { - organizationId, - startDate, - endDate + organizationId }; + + if (startDate !== undefined) { + params['startDate'] = startDate; + } + if (endDate !== undefined) { + params['endDate'] = endDate; + } + const uri = new URL(this.client.config.endpoint + path); return await this.client.call( 'get', diff --git a/src/routes/console/organization-[organization]/usage/[[invoice]]/+page.svelte b/src/routes/console/organization-[organization]/usage/[[invoice]]/+page.svelte index e096c7390d..e2ea123e3f 100644 --- a/src/routes/console/organization-[organization]/usage/[[invoice]]/+page.svelte +++ b/src/routes/console/organization-[organization]/usage/[[invoice]]/+page.svelte @@ -9,10 +9,11 @@ import { goto } from '$app/navigation'; import { toLocaleDate } from '$lib/helpers/date.js'; import { bytesToSize, humanFileSize } from '$lib/helpers/sizeConvertion'; - import { abbreviateNumber } from '$lib/helpers/numbers'; import { BarChart } from '$lib/charts'; import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte'; import ProjectBreakdown from './ProjectBreakdown.svelte'; + import { last } from '$lib/helpers/array'; + import { formatNum } from '$lib/helpers/string'; export let data; @@ -94,7 +95,7 @@

- {@const current = data.organizationUsage.bandwidth[0]?.value ?? 0} + {@const current = last(data.organizationUsage.bandwidth)?.value ?? 0} {@const currentHumanized = humanFileSize(current)} {@const max = getServiceLimit('bandwidth', tier)} + value + ? `${humanFileSize(+value).value} ${humanFileSize(+value).unit}` + : '0' + } + } + }} series={[ { name: 'Bandwidth', - data: [...data.organizationUsage.bandwidth.map((e) => [e.date, e.value])] + data: [...data.organizationUsage.bandwidth.map((e) => [e.date, e.value])], + tooltip: { + valueFormatter: (value) => + `${humanFileSize(+value).value} ${humanFileSize(+value).unit}` + } } ]} /> The total number of users across all projects in your organization.

- {@const current = data.organizationUsage.users[0]?.value ?? 0} + {@const current = last(data.organizationUsage.users)?.value ?? 0} {@const max = getServiceLimit('users', tier)} { const { invoice } = params; const parentData = await parent(); const org = parentData.organization as Organization; - const tomorrow = getTomorrow(new Date()); - let startDate: string; - let endDate: string; - let currentInvoice: Invoice | null = null; + let startDate: string = undefined; + let endDate: string = undefined; + let currentInvoice: Invoice = undefined; if (invoice) { currentInvoice = await sdk.forConsole.billing.getInvoice(org.$id, invoice); startDate = currentInvoice.from; endDate = currentInvoice.to; - } else { - startDate = org.billingCurrentInvoiceDate; - endDate = tomorrow.toISOString(); } const [invoices, usage] = await Promise.all([ - sdk.forConsole.billing.listInvoices(org.$id), + sdk.forConsole.billing.listInvoices(org.$id, [Query.orderDesc('from')]), sdk.forConsole.billing.listUsage(params.organization, startDate, endDate) ]); diff --git a/src/routes/console/organization-[organization]/usage/[[invoice]]/ProjectBreakdown.svelte b/src/routes/console/organization-[organization]/usage/[[invoice]]/ProjectBreakdown.svelte index d553efc80b..edb226d101 100644 --- a/src/routes/console/organization-[organization]/usage/[[invoice]]/ProjectBreakdown.svelte +++ b/src/routes/console/organization-[organization]/usage/[[invoice]]/ProjectBreakdown.svelte @@ -54,8 +54,8 @@ - Project breakdown -
+ Project breakdown +
Project @@ -71,8 +71,9 @@ {format(project.usage)} View project usage + href={getProjectUsageLink(project.projectId)}> + View project usage + {/each} diff --git a/src/routes/console/project-[project]/overview/+layout.svelte b/src/routes/console/project-[project]/overview/+layout.svelte index 1568617ddb..fe98b91657 100644 --- a/src/routes/console/project-[project]/overview/+layout.svelte +++ b/src/routes/console/project-[project]/overview/+layout.svelte @@ -25,6 +25,7 @@ import { formatNum } from '$lib/helpers/string'; import { total } from '$lib/helpers/array'; import type { Metric } from '$lib/sdk/usage'; + import { periodToDates } from '$lib/layout/usage.svelte'; $: projectId = $page.params.project; $: path = `/console/project-${projectId}/overview`; @@ -34,7 +35,7 @@ afterNavigate(handle); async function handle() { - const promise = usage.load(period); + const promise = changePeriod(period); if ($usage) { await promise; @@ -43,7 +44,8 @@ function changePeriod(newPeriod: UsagePeriods) { period = newPeriod; - usage.load(period); + const dates = periodToDates(newPeriod); + return usage.load(dates.start, dates.end, dates.period); } $: $registerCommands([ diff --git a/src/routes/console/project-[project]/overview/store.ts b/src/routes/console/project-[project]/overview/store.ts index 9a1734507e..ea055c522f 100644 --- a/src/routes/console/project-[project]/overview/store.ts +++ b/src/routes/console/project-[project]/overview/store.ts @@ -1,19 +1,17 @@ import { sdk } from '$lib/stores/sdk'; import { cachedStore } from '$lib/helpers/cache'; -import type { UsageProject } from '$lib/sdk/usage'; import { writable, type Writable } from 'svelte/store'; +import type { Models } from '@appwrite.io/console'; export const usage = cachedStore< - UsageProject, + Models.UsageProject, { - load: (range: string) => Promise; + load: (start: string, end: string, period: '1h' | '1d') => Promise; } >('projectUsage', function ({ set }) { return { - load: async (range) => { - const usages: UsageProject = (await sdk.forProject.project.getUsage( - range - )) as unknown as UsageProject; + load: async (start, end, period) => { + const usages = await sdk.forProject.project.getUsage(start, end, period); set(usages); } }; diff --git a/src/routes/console/project-[project]/settings/usage/[[invoice]]/+page.svelte b/src/routes/console/project-[project]/settings/usage/[[invoice]]/+page.svelte new file mode 100644 index 0000000000..e0ee9538b2 --- /dev/null +++ b/src/routes/console/project-[project]/settings/usage/[[invoice]]/+page.svelte @@ -0,0 +1,274 @@ + + + +
+ Usage + + {#if $organization?.billingPlan === 'tier-0'} + + {/if} +
+
+ {#if $organization.billingPlan === 'tier-2'} +

+ On the Scale plan, you'll be charged only for any usage that exceeds the thresholds + per resource listed below. +

+ {:else} +

+ If you exceed the limits of the {plan} plan, services for your projects may be disrupted. + . +

+ {/if} + +
+

Usage period:

+ +
+
+ + Bandwidth +

Calculated for all bandwidth used in your project.

+ + {#if network} + {@const humanized = humanFileSize(total(network))} +
+ {humanized.value} + {humanized.unit} +
+ + value + ? `${humanFileSize(+value).value} ${ + humanFileSize(+value).unit + }` + : '0' + } + } + }} + series={[ + { + name: 'Bandwidth', + data: [...network.map((e) => [e.date, e.value])], + tooltip: { + valueFormatter: (value) => + `${humanFileSize(+value).value} ${humanFileSize(+value).unit}` + } + } + ]} /> + {:else} + +
+
+
+ {/if} +
+
+ + Users + +

Total user in your project.

+ + + {@const current = formatNum(usersTotal)} +
+
+

+ {current} + Users +

+
+
+ [e.date, e.value])] + } + ]} /> +
+
+ + Function executions + +

+ Calculated for all functions that are executed in all projects in your project. +

+ + + {@const current = formatNum(executions)} +
+
+

+ {current} + Executions +

+
+
+ {#if data.usage.executionsBreakdown.length > 0} +
+ + Project + Usage + + + + {#each data.usage.executionsBreakdown as func} + + + {func.name ?? func.resourceId} + + + {formatNum(func.value)} executions + + + View function + + + {/each} + +
+ {/if} + + + + Storage + +

+ Calculated for all your files, deployments, builds and databases. While in beta, only + file storage is counted against your plan limits. +

+ + + {@const humanized = humanFileSize(storage)} +
+
+

+ {humanized.value} + {humanized.unit} +

+
+
+ {#if data.usage.bucketsBreakdown.length > 0} + + + Bucket + Usage + + + + {#each data.usage.bucketsBreakdown.sort((a, b) => b.value - a.value) as bucket} + {@const humanized = humanFileSize(bucket.value)} + + + {bucket.name ?? bucket.resourceId} + + + {humanized.value}{humanized.unit} + + + View bucket + + + {/each} + +
+ {/if} +
+
+

+ Metrics are estimates updated every 24 hours and may not accurately reflect your invoice. +

+ diff --git a/src/routes/console/project-[project]/settings/usage/[[invoice]]/+page.ts b/src/routes/console/project-[project]/settings/usage/[[invoice]]/+page.ts new file mode 100644 index 0000000000..c22f296989 --- /dev/null +++ b/src/routes/console/project-[project]/settings/usage/[[invoice]]/+page.ts @@ -0,0 +1,35 @@ +import { getSdkForProject, sdk } from '$lib/stores/sdk'; +import { Query } from '@appwrite.io/console'; +import type { PageLoad } from './$types'; +import type { Organization } from '$lib/stores/organization'; +import type { Invoice } from '$lib/sdk/billing'; + +export const load: PageLoad = async ({ params, parent }) => { + const { invoice, project } = params; + const parentData = await parent(); + const org = parentData.organization as Organization; + + let startDate: string = org.billingCurrentInvoiceDate; + let endDate: string = org.billingNextInvoiceDate; + let currentInvoice: Invoice = undefined; + + if (invoice) { + currentInvoice = await sdk.forConsole.billing.getInvoice(org.$id, invoice); + startDate = currentInvoice.from; + endDate = currentInvoice.to; + } + + const [invoices, usage] = await Promise.all([ + sdk.forConsole.billing.listInvoices(org.$id, [Query.orderDesc('from')]), + /** + * Workaround because project id might not be populated yet. + */ + getSdkForProject(project).project.getUsage(startDate, endDate) + ]); + + return { + usage, + invoices, + currentInvoice + }; +}; diff --git a/src/routes/console/project-[project]/settings/usage/[[period]]/+page.svelte b/src/routes/console/project-[project]/settings/usage/[[period]]/+page.svelte deleted file mode 100644 index 8379ad3c70..0000000000 --- a/src/routes/console/project-[project]/settings/usage/[[period]]/+page.svelte +++ /dev/null @@ -1,195 +0,0 @@ - - - -
- Usage - - - 24h - - - 30d - - - 90d - - -
- {#if isCloud} -

- Project resources contribute to your organization's monthly free usage limits on the {tierToPlan( - $organization.billingPlan - ).name} plan. - -

- {/if} - - Bandwidth -

Calculated for all bandwidth used across your project.

- - {#if data?.requestsTotal && total(data.requestsTotal)} -
- {total(data.requestsTotal)} - GB -
- [e.date, e.value])] - } - ]} /> - {:else} - -
-
-
- {/if} -
-
- - Users -

The total number of users of your project.

- - {#if total(users)} -
- {total(users)} - Users -
- [e.date, e.value])] - } - ]} /> - {:else} - -
-
-
- {/if} -
-
- - Function executions -

Calculated for all functions executed in your project.

- - {#if total(executions)} -
- {total(executions)} - Executions -
- [e.date, e.value])] - } - ]} /> - {:else} - -
-
-
- {/if} -
-
- {#if data.filesTotal} - - Storage -

Calculated for all storage operations in your project.

- - {@const size = humanFileSize(total(data.filesTotal))} - {#if size} -
- {size.value} - {size.unit} -
- - {:else} - -
-
-
- {/if} -
-
- {/if} -

- Metrics are estimates updated every 24 hours and may not accurately reflect your invoice. -

-
diff --git a/src/routes/console/project-[project]/settings/usage/[[period]]/+page.ts b/src/routes/console/project-[project]/settings/usage/[[period]]/+page.ts deleted file mode 100644 index dee7ea1517..0000000000 --- a/src/routes/console/project-[project]/settings/usage/[[period]]/+page.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Models } from '@appwrite.io/console'; -import { sdk } from '$lib/stores/sdk'; -import type { PageLoad } from './$types'; -import { error } from '@sveltejs/kit'; - -export const load: PageLoad = async ({ params }) => { - try { - const response = await sdk.forProject.project.getUsage(params.period ?? '30d'); - console.log(response); - return { - range: response.range, - bucketsTotal: response.bucketsTotal as unknown as Models.Metric[], - filesTotal: response.filesStorage as unknown as Models.Metric[], - databasesTotal: response.databasesTotal as unknown as Models.Metric[], - documentsTotal: response.documentsTotal as unknown as Models.Metric[], - executionsTotal: response.executionsTotal as unknown as Models.Metric[], - networkTotal: response.network as unknown as Models.Metric[], - requestsTotal: response.requestsTotal as unknown as Models.Metric[], - usersTotal: response.usersTotal as unknown as Models.Metric[] - }; - } catch (e) { - throw error(e.code, e.message); - } -};