fix: usage

This commit is contained in:
Torsten Dittmann
2023-12-13 15:42:46 +01:00
parent 8291bf3a2d
commit 155a12011b
9 changed files with 364 additions and 261 deletions
+1 -1
View File
@@ -13,7 +13,7 @@
<main class="grid-1-1 is-full-page" id="main">
<section class="u-flex u-flex-vertical side-bg">
<div class="logo u-flex u-gap-16 u-margin-inline-auto is-not-mobile">
<a href={user ? '/console' : '/'}>
<a href={$user ? '/console' : '/'}>
{#if $app.themeInUse === 'dark'}
<img
src={AppwriteLogoDark}
+17 -17
View File
@@ -38,6 +38,23 @@
export function total(set: Models.Metric[]): number {
return set?.reduce((prev, curr) => prev + curr.value, 0) ?? 0;
}
export function accumulateFromEndingTotal(
metrics: Models.Metric[],
endingTotal: number
): Array<[string, number]> {
return metrics.reduceRight(
(acc, curr) => {
acc.total -= curr.value;
acc.data.unshift([curr.date, acc.total]);
return acc;
},
{
total: endingTotal,
data: []
}
).data;
}
</script>
<script lang="ts">
@@ -58,23 +75,6 @@
export let count: Models.Metric[];
export let countMetadata: MetricMetadata;
export let path: string = null;
function accumulateFromEndingTotal(
metrics: Models.Metric[],
endingTotal: number
): Array<[string, number]> {
return metrics.reduceRight(
(acc, curr) => {
acc.total -= curr.value;
acc.data.unshift([curr.date, acc.total]);
return acc;
},
{
total: endingTotal,
data: []
}
).data;
}
</script>
<Container>
+7 -9
View File
@@ -1,4 +1,4 @@
import type { Client, Query } from '@appwrite.io/console';
import type { Client, Models, Query } from '@appwrite.io/console';
import type { Organization } from '../stores/organization';
import type { PaymentMethod } from '@stripe/stripe-js';
import type { Tier } from '$lib/stores/billing';
@@ -155,15 +155,13 @@ export type Aggregation = {
*/
resources: OrganizationUsage;
};
type UsageMetric = {
date: string;
value: number;
};
export type OrganizationUsage = {
bandwidth: Array<UsageMetric>;
executions: number;
storage: number;
users: Array<UsageMetric>;
bandwidth: Array<Models.Metric>;
executionsTotal: number;
storageTotal: number;
users: Array<Models.Metric>;
usersTotal: number;
projects: Array<{
projectId: string;
storage: number;
@@ -37,13 +37,13 @@
excess = {
bandwidth: totBandwidth > plan.bandwidth ? totBandwidth - plan.bandwidth : 0,
storage:
usage?.storage > sizeToBytes(plan.storage, 'GB')
? usage.storage - sizeToBytes(plan.storage, 'GB')
usage?.storageTotal > sizeToBytes(plan.storage, 'GB')
? usage.storageTotal - sizeToBytes(plan.storage, 'GB')
: 0,
users: totUsers > (plan.users || Infinity) ? totUsers - plan.users : 0,
executions:
usage?.executions > sizeToBytes(plan.executions, 'GB')
? usage.executions - sizeToBytes(plan.executions, 'GB')
usage?.executionsTotal > sizeToBytes(plan.executions, 'GB')
? usage.executionsTotal - sizeToBytes(plan.executions, 'GB')
: 0,
members:
members?.total > (plan.members || Infinity)
@@ -1,41 +1,36 @@
<script lang="ts">
import { Container } from '$lib/layout';
import { CardGrid, Heading, ProgressBarBig } from '$lib/components';
import { Card, CardGrid, Heading, ProgressBarBig } from '$lib/components';
import { getServiceLimit, 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 { page } from '$app/stores';
import { goto } from '$app/navigation';
import { toLocaleDate } from '$lib/helpers/date.js';
import { Button } from '$lib/elements/forms';
import { bytesToSize, humanFileSize } from '$lib/helpers/sizeConvertion';
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';
import { total } from '$lib/layout/usage.svelte';
import { accumulateFromEndingTotal, total } from '$lib/layout/usage.svelte';
export let data;
const tier = data?.currentInvoice?.tier ?? $organization?.billingPlan;
const plan = tierToPlan(tier).name;
let invoice = null;
// let invoice = null;
// async function handlePeriodChange() {
// const target = invoice
// ? `/console/organization-${$organization.$id}/usage/${invoice}`
// : `/console/organization-${$organization.$id}/usage`;
// if ($page.url.pathname !== target) {
// await goto(target);
// }
// }
async function handlePeriodChange() {
const target = invoice
? `/console/organization-${$organization.$id}/usage/${invoice}`
: `/console/organization-${$organization.$id}/usage`;
if ($page.url.pathname !== target) {
await goto(target);
}
}
const cycles = data.invoices.invoices.map((invoice) => ({
label: toLocaleDate(invoice.from),
value: invoice.$id
}));
// const cycles = data.invoices.invoices.map((invoice) => ({
// label: toLocaleDate(invoice.from),
// value: invoice.$id
// }));
</script>
<Container>
@@ -77,7 +72,7 @@
</p>
{/if}
<div class="u-flex u-gap-8 u-cross-center u-hide">
<!--<div class="u-flex u-gap-8 u-cross-center">
<p class="text">Usage period:</p>
<InputSelect
wrapperTag="div"
@@ -93,7 +88,7 @@
},
...cycles
]} />
</div>
</div>-->
</div>
<CardGrid>
@@ -104,42 +99,58 @@
</p>
<svelte:fragment slot="aside">
{@const current = total(data.organizationUsage.bandwidth)}
{@const currentHumanized = humanFileSize(current)}
{@const max = getServiceLimit('bandwidth', tier)}
<ProgressBarBig
currentUnit={currentHumanized.unit}
currentValue={currentHumanized.value}
maxUnit="GB"
maxValue={max.toString()}
progressValue={bytesToSize(current, 'GB')}
progressMax={max}
showBar={false} />
<BarChart
options={{
yAxis: {
axisLabel: {
formatter: (value) =>
value
? `${humanFileSize(+value).value} ${humanFileSize(+value).unit}`
: '0'
{#if data.organizationUsage.bandwidth}
{@const current = total(data.organizationUsage.bandwidth)}
{@const currentHumanized = humanFileSize(current)}
{@const max = getServiceLimit('bandwidth', tier)}
<ProgressBarBig
currentUnit={currentHumanized.unit}
currentValue={currentHumanized.value}
maxUnit="GB"
maxValue={max.toString()}
progressValue={bytesToSize(current, 'GB')}
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])],
tooltip: {
valueFormatter: (value) =>
`${humanFileSize(+value).value} ${humanFileSize(+value).unit}`
}}
series={[
{
name: 'Bandwidth',
data: [
...data.organizationUsage.bandwidth.map((e) => [e.date, e.value])
],
tooltip: {
valueFormatter: (value) =>
`${humanFileSize(+value).value} ${humanFileSize(+value).unit}`
}
}
}
]} />
<ProjectBreakdown
projects={data.organizationUsage.projects}
metric="bandwidth"
{data} />
]} />
<ProjectBreakdown
projects={data.organizationUsage.projects}
metric="bandwidth"
{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>
@@ -149,31 +160,49 @@
<p class="text">The total number of users across all projects in your organization.</p>
<svelte:fragment slot="aside">
{@const current = last(data.organizationUsage.users)?.value ?? 0}
{@const max = getServiceLimit('users', tier)}
<ProgressBarBig
currentUnit="Users"
currentValue={formatNum(current)}
maxUnit="Users"
maxValue={formatNum(max)}
progressValue={current}
progressMax={max}
showBar={false} />
<BarChart
options={{
yAxis: {
axisLabel: {
formatter: formatNum
{#if data.organizationUsage.users}
{@const current = data.organizationUsage.usersTotal}
{@const max = getServiceLimit('users', tier)}
<ProgressBarBig
currentUnit="Users"
currentValue={formatNum(current)}
maxUnit="Users"
maxValue={formatNum(max)}
progressValue={current}
progressMax={max}
showBar={false} />
<BarChart
options={{
yAxis: {
axisLabel: {
formatter: formatNum
}
}
}
}}
series={[
{
name: 'Users',
data: [...data.organizationUsage.users.map((e) => [e.date, e.value])]
}
]} />
<ProjectBreakdown projects={data.organizationUsage.projects} metric="users" {data} />
}}
series={[
{
name: 'Users',
data: accumulateFromEndingTotal(
data.organizationUsage.users,
data.organizationUsage.usersTotal
)
}
]} />
<ProjectBreakdown
projects={data.organizationUsage.projects}
metric="users"
{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>
@@ -185,19 +214,31 @@
</p>
<svelte:fragment slot="aside">
{@const current = data.organizationUsage.executions}
{@const max = getServiceLimit('executions', tier)}
<ProgressBarBig
currentUnit="Executions"
currentValue={current.toString()}
maxUnit="Executions"
maxValue={formatNum(max)}
progressValue={current}
progressMax={max} />
<ProjectBreakdown
projects={data.organizationUsage.projects}
metric="executions"
{data} />
{#if data.organizationUsage.executionsTotal}
{@const current = data.organizationUsage.executionsTotal}
{@const max = getServiceLimit('executions', tier)}
<ProgressBarBig
currentUnit="Executions"
currentValue={current.toString()}
maxUnit="Executions"
maxValue={formatNum(max)}
progressValue={current}
progressMax={max} />
<ProjectBreakdown
projects={data.organizationUsage.projects}
metric="executions"
{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>
@@ -210,17 +251,32 @@
</p>
<svelte:fragment slot="aside">
{@const current = data.organizationUsage.storage}
{@const currentHumanized = humanFileSize(current)}
{@const max = getServiceLimit('storage', tier)}
<ProgressBarBig
currentUnit={currentHumanized.unit}
currentValue={currentHumanized.value}
maxUnit="GB"
maxValue={max.toString()}
progressValue={bytesToSize(current, 'GB')}
progressMax={max} />
<ProjectBreakdown projects={data.organizationUsage.projects} metric="storage" {data} />
{#if data.organizationUsage.storageTotal}
{@const current = data.organizationUsage.storageTotal}
{@const currentHumanized = humanFileSize(current)}
{@const max = getServiceLimit('storage', tier)}
<ProgressBarBig
currentUnit={currentHumanized.unit}
currentValue={currentHumanized.value}
maxUnit="GB"
maxValue={max.toString()}
progressValue={bytesToSize(current, 'GB')}
progressMax={max} />
<ProjectBreakdown
projects={data.organizationUsage.projects}
metric="storage"
{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>
@@ -9,8 +9,23 @@ export const load: PageLoad = async ({ params, parent }) => {
const parentData = await parent();
const org = parentData.organization as Organization;
let startDate: string = undefined;
let endDate: string = undefined;
/**
* Temporary fix during migration to billing system
*/
if (org.billingCurrentInvoiceDate === null || org.billingNextInvoiceDate === null) {
return {
organizationUsage: {
bandwidth: null,
users: null,
usersTotal: null,
storageTotal: null,
executionsTotal: null
}
};
}
let startDate: string = org.billingCurrentInvoiceDate;
let endDate: string = org.billingNextInvoiceDate;
let currentInvoice: Invoice = undefined;
if (invoice) {
@@ -1,6 +1,4 @@
<script lang="ts">
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { Container } from '$lib/layout';
import { CardGrid, Heading, Card } from '$lib/components';
import {
@@ -15,8 +13,7 @@
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 { Button } from '$lib/elements/forms';
import { humanFileSize } from '$lib/helpers/sizeConvertion';
import { BarChart } from '$lib/charts';
import { formatNum } from '$lib/helpers/string';
@@ -36,19 +33,18 @@
const tier = data?.currentInvoice?.tier ?? $organization?.billingPlan;
const plan = tierToPlan(tier).name;
let invoice = null;
// let invoice = null;
// async function handlePeriodChange() {
// const target = invoice ? `${base}/settings/usage/${invoice}` : `${base}/settings/usage`;
// if ($page.url.pathname !== target) {
// await goto(target);
// }
// }
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
}));
// const cycles = data.invoices.invoices.map((invoice) => ({
// label: toLocaleDate(invoice.from),
// value: invoice.$id
// }));
</script>
<Container>
@@ -87,7 +83,7 @@
</p>
{/if}
<div class="u-flex u-gap-8 u-cross-center u-hide">
<!--<div class="u-flex u-gap-8 u-cross-center">
<p class="text">Usage period:</p>
<InputSelect
wrapperTag="div"
@@ -103,7 +99,7 @@
},
...cycles
]} />
</div>
</div>-->
</div>
<CardGrid>
<Heading tag="h4" size="7">Bandwidth</Heading>
@@ -157,29 +153,41 @@
<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>
{#if users}
{@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>
</div>
<BarChart
options={{
yAxis: {
axisLabel: {
formatter: formatNum
<BarChart
options={{
yAxis: {
axisLabel: {
formatter: formatNum
}
}
}
}}
series={[
{
name: 'Users',
data: [...users.map((e) => [e.date, e.value])]
}
]} />
}}
series={[
{
name: 'Users',
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>
@@ -190,54 +198,66 @@
</p>
<svelte:fragment slot="aside">
{@const current = formatNum(executionsTotal)}
<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>
{#if executions}
{@const current = formatNum(executionsTotal)}
<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>
</div>
<BarChart
options={{
yAxis: {
axisLabel: {
formatter: formatNum
<BarChart
options={{
yAxis: {
axisLabel: {
formatter: formatNum
}
}
}
}}
series={[
{
name: 'Executions',
data: [...executions.map((e) => [e.date, e.value])]
}
]} />
{#if data.usage.executionsBreakdown.length > 0}
<Table noMargin noStyles>
<TableHeader>
<TableCellHead width={285}>Function</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>
}}
series={[
{
name: 'Executions',
data: [...executions.map((e) => [e.date, e.value])]
}
]} />
{#if data.usage.executionsBreakdown.length > 0}
<Table noMargin noStyles>
<TableHeader>
<TableCellHead width={285}>Function</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}
{: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>
@@ -250,41 +270,53 @@
</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>
{#if storage}
{@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>
</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 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}
{: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>
@@ -53,7 +53,7 @@
const projectId = $page.params.project;
const bucketId = $page.params.bucket;
const usedStorage = bytesToSize(data.organizationUsage.storage, 'MB');
const usedStorage = bytesToSize(data.organizationUsage.storageTotal, 'MB');
const getPreview = (fileId: string) =>
sdk.forProject.storage.getFilePreview(bucketId, fileId, 32, 32).toString() + '&mode=admin';
@@ -46,12 +46,14 @@
$changeOrganizationTier.limitOverflow = {
bandwidth: totBandwidth > plan.bandwidth ? totBandwidth - plan.bandwidth : 0,
storage:
usage.storage[0] > sizeToBytes(plan.storage, 'GB')
? usage.storage[0] - plan.storage
usage.storageTotal > sizeToBytes(plan.storage, 'GB')
? usage.storageTotal - plan.storage
: 0,
users: totUsers > plan.users ? totUsers - plan.users : 0,
executions:
usage.executions[0] > plan.executions ? usage.executions[0] - plan.executions : 0,
usage.executionsTotal > plan.executions
? usage.executionsTotal - plan.executions
: 0,
members: members.total > plan.members ? members.total - (plan.members || Infinity) : 0
};
if (