mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
feat: project usage
This commit is contained in:
Generated
+4
-4
@@ -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"
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
</script>
|
||||
|
||||
<section class="progress-bar">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export function getProjectId() {
|
||||
const pathname = window.location.pathname + '/';
|
||||
const projectMatch = pathname.match(/\/project-(.*?)\//);
|
||||
|
||||
return projectMatch?.[1] || null;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,38 @@
|
||||
<script context="module" lang="ts">
|
||||
export type UsagePeriods = '24h' | '30d' | '90d';
|
||||
|
||||
export function periodToDates(period: UsagePeriods): {
|
||||
start: string;
|
||||
end: string;
|
||||
period: '1h' | '1d';
|
||||
} {
|
||||
const end = new Date();
|
||||
switch (period) {
|
||||
case '24h':
|
||||
end.setHours(end.getHours() + 24);
|
||||
break;
|
||||
case '30d':
|
||||
end.setDate(end.getDate() + 30);
|
||||
break;
|
||||
case '90d':
|
||||
end.setDate(end.getDate() + 90);
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
start: new Date().toISOString(),
|
||||
end: end.toISOString(),
|
||||
period: period === '24h' ? '1h' : '1d'
|
||||
};
|
||||
}
|
||||
|
||||
export function last(set: Models.Metric[]): Models.Metric | null {
|
||||
if (!set) return null;
|
||||
return set.slice(-1)[0] ?? null;
|
||||
}
|
||||
|
||||
export function total(set: Models.Metric[]): number {
|
||||
return set.reduce((prev, curr) => prev + curr.value, 0);
|
||||
return set?.reduce((prev, curr) => prev + curr.value, 0) ?? 0;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
+11
-5
@@ -512,15 +512,21 @@ export class Billing {
|
||||
|
||||
async listUsage(
|
||||
organizationId: string,
|
||||
startDate: string = null,
|
||||
endDate: string = null
|
||||
startDate: string = undefined,
|
||||
endDate: string = undefined
|
||||
): Promise<OrganizationUsage> {
|
||||
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',
|
||||
|
||||
@@ -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 @@
|
||||
</p>
|
||||
|
||||
<svelte:fragment slot="aside">
|
||||
{@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)}
|
||||
<ProgressBarBig
|
||||
@@ -106,10 +107,24 @@
|
||||
progressMax={max}
|
||||
showBar={false} />
|
||||
<BarChart
|
||||
options={{
|
||||
yAxis: {
|
||||
axisLabel: {
|
||||
formatter: (value) =>
|
||||
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}`
|
||||
}
|
||||
}
|
||||
]} />
|
||||
<ProjectBreakdown
|
||||
@@ -125,17 +140,24 @@
|
||||
<p class="text">The total number of users across all projects in your organization.</p>
|
||||
|
||||
<svelte:fragment slot="aside">
|
||||
{@const current = data.organizationUsage.users[0]?.value ?? 0}
|
||||
{@const current = last(data.organizationUsage.users)?.value ?? 0}
|
||||
{@const max = getServiceLimit('users', tier)}
|
||||
<ProgressBarBig
|
||||
currentUnit="Users"
|
||||
currentValue={current.toString()}
|
||||
currentValue={formatNum(current)}
|
||||
maxUnit="Users"
|
||||
maxValue={abbreviateNumber(max)}
|
||||
maxValue={formatNum(max)}
|
||||
progressValue={current}
|
||||
progressMax={max}
|
||||
showBar={false} />
|
||||
<BarChart
|
||||
options={{
|
||||
yAxis: {
|
||||
axisLabel: {
|
||||
formatter: formatNum
|
||||
}
|
||||
}
|
||||
}}
|
||||
series={[
|
||||
{
|
||||
name: 'Users',
|
||||
@@ -160,7 +182,7 @@
|
||||
currentUnit="Executions"
|
||||
currentValue={current.toString()}
|
||||
maxUnit="Executions"
|
||||
maxValue={abbreviateNumber(max)}
|
||||
maxValue={formatNum(max)}
|
||||
progressValue={current}
|
||||
progressMax={max} />
|
||||
<ProjectBreakdown
|
||||
|
||||
@@ -1,31 +1,26 @@
|
||||
import { 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';
|
||||
import { getTomorrow } from '$lib/helpers/date';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
|
||||
export const load: PageLoad = async ({ params, parent }) => {
|
||||
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)
|
||||
]);
|
||||
|
||||
|
||||
+5
-4
@@ -54,8 +54,8 @@
|
||||
|
||||
<Collapsible>
|
||||
<CollapsibleItem>
|
||||
<svelte:fragment slot="tgit restore --stageditle">Project breakdown</svelte:fragment>
|
||||
<div class="table-wrapper">
|
||||
<svelte:fragment slot="title">Project breakdown</svelte:fragment>
|
||||
<div class="table-wrapper" data-sveltekit-preload-data="off">
|
||||
<Table noMargin noStyles>
|
||||
<TableHeader>
|
||||
<TableCellHead width={285}>Project</TableCellHead>
|
||||
@@ -71,8 +71,9 @@
|
||||
<TableCell title="Usage">{format(project.usage)}</TableCell>
|
||||
<TableCellLink
|
||||
title="Go to project usage"
|
||||
href={getProjectUsageLink(project.projectId)}
|
||||
>View project usage</TableCellLink>
|
||||
href={getProjectUsageLink(project.projectId)}>
|
||||
View project usage
|
||||
</TableCellLink>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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<void>;
|
||||
load: (start: string, end: string, period: '1h' | '1d') => Promise<void>;
|
||||
}
|
||||
>('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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
<script lang="ts">
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Container } from '$lib/layout';
|
||||
import { CardGrid, Heading, Card } from '$lib/components';
|
||||
import {
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellLink,
|
||||
TableCellHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Table
|
||||
} from '$lib/elements/table';
|
||||
import { showUsageRatesModal, tierToPlan } from '$lib/stores/billing';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { Button, InputSelect } from '$lib/elements/forms';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import { humanFileSize } from '$lib/helpers/sizeConvertion';
|
||||
import { BarChart } from '$lib/charts';
|
||||
import { formatNum } from '$lib/helpers/string';
|
||||
import { total } from '$lib/layout/usage.svelte';
|
||||
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
$: base = `/console/project-${data.project.$id}`;
|
||||
$: network = data.usage.network as unknown as Models.Metric[];
|
||||
$: users = data.usage.users;
|
||||
$: usersTotal = data.usage.usersTotal;
|
||||
$: executions = data.usage.executionsTotal;
|
||||
$: storage = data.usage.filesStorageTotal;
|
||||
|
||||
const tier = data?.currentInvoice?.tier ?? $organization?.billingPlan;
|
||||
const plan = tierToPlan(tier).name;
|
||||
|
||||
let invoice = null;
|
||||
|
||||
async function handlePeriodChange() {
|
||||
const target = invoice ? `${base}/settings/usage/${invoice}` : `${base}/settings/usage`;
|
||||
if ($page.url.pathname !== target) {
|
||||
await goto(target);
|
||||
}
|
||||
}
|
||||
|
||||
const cycles = data.invoices.invoices.map((invoice) => ({
|
||||
label: toLocaleDate(invoice.from),
|
||||
value: invoice.$id
|
||||
}));
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<div class="u-flex u-cross-center u-main-space-between">
|
||||
<Heading tag="h2" size="5">Usage</Heading>
|
||||
|
||||
{#if $organization?.billingPlan === 'tier-0'}
|
||||
<Button on:click={() => wizard.start(ChangeOrganizationTierCloud)}>
|
||||
<span class="text">Upgrade</span>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="u-flex u-main-space-between common-section u-cross-center">
|
||||
{#if $organization.billingPlan === 'tier-2'}
|
||||
<p class="text">
|
||||
On the Scale plan, you'll be charged only for any usage that exceeds the thresholds
|
||||
per resource listed below. <button
|
||||
on:click={() => ($showUsageRatesModal = true)}
|
||||
class="link"
|
||||
type="button">Learn more about plan usage limits.</button>
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text">
|
||||
If you exceed the limits of the {plan} plan, services for your projects may be disrupted.
|
||||
<button
|
||||
on:click={() => wizard.start(ChangeOrganizationTierCloud)}
|
||||
class="link"
|
||||
type="button">Upgrade for greater capacity</button
|
||||
>.
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div class="u-flex u-gap-8 u-cross-center">
|
||||
<p class="text">Usage period:</p>
|
||||
<InputSelect
|
||||
wrapperTag="div"
|
||||
id="period"
|
||||
label="Usage period"
|
||||
showLabel={false}
|
||||
bind:value={invoice}
|
||||
on:change={handlePeriodChange}
|
||||
options={[
|
||||
{
|
||||
label: 'Current billing cycle',
|
||||
value: null
|
||||
},
|
||||
...cycles
|
||||
]} />
|
||||
</div>
|
||||
</div>
|
||||
<CardGrid>
|
||||
<Heading tag="h4" size="7">Bandwidth</Heading>
|
||||
<p class="text">Calculated for all bandwidth used in your project.</p>
|
||||
<svelte:fragment slot="aside">
|
||||
{#if network}
|
||||
{@const humanized = humanFileSize(total(network))}
|
||||
<div class="u-flex u-gap-8 u-cross-baseline">
|
||||
<Heading tag="h5" size="4">{humanized.value}</Heading>
|
||||
<Heading tag="h6" size="6">{humanized.unit}</Heading>
|
||||
</div>
|
||||
<BarChart
|
||||
options={{
|
||||
yAxis: {
|
||||
axisLabel: {
|
||||
formatter: (value) =>
|
||||
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}
|
||||
<Card isDashed>
|
||||
<div class="u-flex u-cross-center u-flex-vertical u-main-center u-flex">
|
||||
<span
|
||||
class="icon-chart-square-bar text-large"
|
||||
aria-hidden="true"
|
||||
style="font-size: 32px;" />
|
||||
<p class="u-bold">No data to show</p>
|
||||
</div>
|
||||
</Card>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Users</Heading>
|
||||
|
||||
<p class="text">Total user in your project.</p>
|
||||
|
||||
<svelte:fragment slot="aside">
|
||||
{@const current = formatNum(usersTotal)}
|
||||
<div class="u-flex u-flex-vertical">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p>
|
||||
<span class="heading-level-4">{current}</span>
|
||||
<span class="body-text-1 u-bold">Users</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<BarChart
|
||||
options={{
|
||||
yAxis: {
|
||||
axisLabel: {
|
||||
formatter: formatNum
|
||||
}
|
||||
}
|
||||
}}
|
||||
series={[
|
||||
{
|
||||
name: 'Users',
|
||||
data: [...users.map((e) => [e.date, e.value])]
|
||||
}
|
||||
]} />
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Function executions</Heading>
|
||||
|
||||
<p class="text">
|
||||
Calculated for all functions that are executed in all projects in your project.
|
||||
</p>
|
||||
|
||||
<svelte:fragment slot="aside">
|
||||
{@const current = formatNum(executions)}
|
||||
<div class="u-flex u-flex-vertical">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p>
|
||||
<span class="heading-level-4">{current}</span>
|
||||
<span class="body-text-1 u-bold">Executions</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{#if data.usage.executionsBreakdown.length > 0}
|
||||
<Table noMargin noStyles>
|
||||
<TableHeader>
|
||||
<TableCellHead width={285}>Project</TableCellHead>
|
||||
<TableCellHead>Usage</TableCellHead>
|
||||
<TableCellHead width={140} />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each data.usage.executionsBreakdown as func}
|
||||
<TableRow>
|
||||
<TableCell title="Function">
|
||||
{func.name ?? func.resourceId}
|
||||
</TableCell>
|
||||
<TableCell title="Usage">
|
||||
{formatNum(func.value)} executions
|
||||
</TableCell>
|
||||
<TableCellLink
|
||||
href={`${base}/functions/function-${func.resourceId}`}
|
||||
title="View function">
|
||||
View function
|
||||
</TableCellLink>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Storage</Heading>
|
||||
|
||||
<p class="text">
|
||||
Calculated for all your files, deployments, builds and databases. While in beta, only
|
||||
file storage is counted against your plan limits.
|
||||
</p>
|
||||
|
||||
<svelte:fragment slot="aside">
|
||||
{@const humanized = humanFileSize(storage)}
|
||||
<div class="u-flex u-flex-vertical">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p>
|
||||
<span class="heading-level-4">{humanized.value}</span>
|
||||
<span class="body-text-1 u-bold">{humanized.unit}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{#if data.usage.bucketsBreakdown.length > 0}
|
||||
<Table noMargin noStyles>
|
||||
<TableHeader>
|
||||
<TableCellHead width={285}>Bucket</TableCellHead>
|
||||
<TableCellHead>Usage</TableCellHead>
|
||||
<TableCellHead width={140} />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each data.usage.bucketsBreakdown.sort((a, b) => b.value - a.value) as bucket}
|
||||
{@const humanized = humanFileSize(bucket.value)}
|
||||
<TableRow>
|
||||
<TableCell title="Function">
|
||||
{bucket.name ?? bucket.resourceId}
|
||||
</TableCell>
|
||||
<TableCell title="Usage">
|
||||
{humanized.value}{humanized.unit}
|
||||
</TableCell>
|
||||
<TableCellLink
|
||||
href={`${base}/storage/bucket-${bucket.resourceId}`}
|
||||
title="View bucket">
|
||||
View bucket
|
||||
</TableCellLink>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
<p class="text u-text-color-gray u-margin-block-start-16">
|
||||
Metrics are estimates updated every 24 hours and may not accurately reflect your invoice.
|
||||
</p>
|
||||
</Container>
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -1,195 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Container } from '$lib/layout';
|
||||
import { Card, SecondaryTabsItem, SecondaryTabs, Heading, CardGrid } from '$lib/components';
|
||||
import { total } from '$lib/layout/usage.svelte';
|
||||
import { BarChart } from '$lib/charts';
|
||||
import { page } from '$app/stores';
|
||||
import { bytesToSize, humanFileSize } from '$lib/helpers/sizeConvertion';
|
||||
import { toDecimals } from '$lib/helpers/numbers.js';
|
||||
import { showUsageRatesModal, tierToPlan } from '$lib/stores/billing.js';
|
||||
import { organization } from '$lib/stores/organization.js';
|
||||
import { isCloud } from '$lib/system';
|
||||
|
||||
export let data;
|
||||
|
||||
$: files = data.filesTotal ? normalizeFileSize(data.filesTotal) : null;
|
||||
$: executions = data.executionsTotal;
|
||||
$: users = data.usersTotal;
|
||||
|
||||
//All file sizes get converted to the bigger file size unit
|
||||
function normalizeFileSize(
|
||||
files: {
|
||||
date: string;
|
||||
value: number;
|
||||
}[]
|
||||
) {
|
||||
console.log(data.filesTotal);
|
||||
const sizes = files?.map((e) => e.value);
|
||||
const biggestSize = Math.max(...sizes);
|
||||
const unit = humanFileSize(biggestSize).unit;
|
||||
return {
|
||||
data: files?.map((e) => ({
|
||||
date: new Date(e.date).toLocaleDateString(),
|
||||
value: toDecimals(bytesToSize(e.value, unit), 2)
|
||||
})),
|
||||
unit
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<div class="u-flex u-main-space-between common-section u-cross-center">
|
||||
<Heading tag="h2" size="5">Usage</Heading>
|
||||
<SecondaryTabs>
|
||||
<SecondaryTabsItem
|
||||
href={`/console/project-${data.project.$id}/settings/usage/24h`}
|
||||
disabled={($page.params.period ?? '24h') === '24h'}>
|
||||
24h
|
||||
</SecondaryTabsItem>
|
||||
<SecondaryTabsItem
|
||||
href={`/console/project-${data.project.$id}/settings/usage/30d`}
|
||||
disabled={$page.params.period === '30d'}>
|
||||
30d
|
||||
</SecondaryTabsItem>
|
||||
<SecondaryTabsItem
|
||||
href={`/console/project-${data.project.$id}/settings/usage/90d`}
|
||||
disabled={$page.params.period === '90d'}>
|
||||
90d
|
||||
</SecondaryTabsItem>
|
||||
</SecondaryTabs>
|
||||
</div>
|
||||
{#if isCloud}
|
||||
<p class="u-margin-block-start-8">
|
||||
Project resources contribute to your organization's monthly free usage limits on the {tierToPlan(
|
||||
$organization.billingPlan
|
||||
).name} plan.
|
||||
<button
|
||||
class="link"
|
||||
type="button"
|
||||
on:click|preventDefault={() => ($showUsageRatesModal = true)}
|
||||
>Learn more about usage rates.</button>
|
||||
</p>
|
||||
{/if}
|
||||
<CardGrid>
|
||||
<Heading tag="h4" size="7">Bandwidth</Heading>
|
||||
<p class="text">Calculated for all bandwidth used across your project.</p>
|
||||
<svelte:fragment slot="aside">
|
||||
{#if data?.requestsTotal && total(data.requestsTotal)}
|
||||
<div class="u-flex u-gap-8 u-cross-baseline">
|
||||
<Heading tag="h5" size="4">{total(data.requestsTotal)}</Heading>
|
||||
<Heading tag="h6" size="6">GB</Heading>
|
||||
</div>
|
||||
<BarChart
|
||||
series={[
|
||||
{
|
||||
name: 'Bandwidth over time',
|
||||
data: [...data.requestsTotal.map((e) => [e.date, e.value])]
|
||||
}
|
||||
]} />
|
||||
{:else}
|
||||
<Card isDashed>
|
||||
<div class="u-flex u-cross-center u-flex-vertical u-main-center u-flex">
|
||||
<span
|
||||
class="icon-chart-square-bar text-large"
|
||||
aria-hidden="true"
|
||||
style="font-size: 32px;" />
|
||||
<p class="u-bold">No data to show</p>
|
||||
</div>
|
||||
</Card>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
<CardGrid>
|
||||
<Heading tag="h4" size="7">Users</Heading>
|
||||
<p class="text">The total number of users of your project.</p>
|
||||
<svelte:fragment slot="aside">
|
||||
{#if total(users)}
|
||||
<div class="u-flex u-gap-8 u-cross-baseline">
|
||||
<Heading tag="h5" size="4">{total(users)}</Heading>
|
||||
<Heading tag="h6" size="6">Users</Heading>
|
||||
</div>
|
||||
<BarChart
|
||||
series={[
|
||||
{
|
||||
name: 'Users over time',
|
||||
data: [...users.map((e) => [e.date, e.value])]
|
||||
}
|
||||
]} />
|
||||
{:else}
|
||||
<Card isDashed>
|
||||
<div class="u-flex u-cross-center u-flex-vertical u-main-center u-flex">
|
||||
<span
|
||||
class="icon-chart-square-bar text-large"
|
||||
aria-hidden="true"
|
||||
style="font-size: 32px;" />
|
||||
<p class="u-bold">No data to show</p>
|
||||
</div>
|
||||
</Card>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
<CardGrid>
|
||||
<Heading tag="h4" size="7">Function executions</Heading>
|
||||
<p class="text">Calculated for all functions executed in your project.</p>
|
||||
<svelte:fragment slot="aside">
|
||||
{#if total(executions)}
|
||||
<div class="u-flex u-gap-8 u-cross-baseline">
|
||||
<Heading tag="h5" size="4">{total(executions)}</Heading>
|
||||
<Heading tag="h6" size="6">Executions</Heading>
|
||||
</div>
|
||||
<BarChart
|
||||
series={[
|
||||
{
|
||||
name: 'Executions over time',
|
||||
data: [...executions.map((e) => [e.date, e.value])]
|
||||
}
|
||||
]} />
|
||||
{:else}
|
||||
<Card isDashed>
|
||||
<div class="u-flex u-cross-center u-flex-vertical u-main-center u-flex">
|
||||
<span
|
||||
class="icon-chart-square-bar text-large"
|
||||
aria-hidden="true"
|
||||
style="font-size: 32px;" />
|
||||
<p class="u-bold">No data to show</p>
|
||||
</div>
|
||||
</Card>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
{#if data.filesTotal}
|
||||
<CardGrid>
|
||||
<Heading tag="h4" size="7">Storage</Heading>
|
||||
<p class="text">Calculated for all storage operations in your project.</p>
|
||||
<svelte:fragment slot="aside">
|
||||
{@const size = humanFileSize(total(data.filesTotal))}
|
||||
{#if size}
|
||||
<div class="u-flex u-gap-8 u-cross-baseline">
|
||||
<Heading tag="h5" size="4">{size.value}</Heading>
|
||||
<Heading tag="h6" size="6">{size.unit}</Heading>
|
||||
</div>
|
||||
<BarChart
|
||||
series={[
|
||||
{
|
||||
name: `Storage over time in ${files.unit}`,
|
||||
data: [...files.data]
|
||||
}
|
||||
]} />
|
||||
{:else}
|
||||
<Card isDashed>
|
||||
<div class="u-flex u-cross-center u-flex-vertical u-main-center u-flex">
|
||||
<span
|
||||
class="icon-chart-square-bar text-large"
|
||||
aria-hidden="true"
|
||||
style="font-size: 32px;" />
|
||||
<p class="u-bold">No data to show</p>
|
||||
</div>
|
||||
</Card>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
{/if}
|
||||
<p class="text u-text-color-gray u-margin-block-start-16">
|
||||
Metrics are estimates updated every 24 hours and may not accurately reflect your invoice.
|
||||
</p>
|
||||
</Container>
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user