Merge pull request #1629 from appwrite/database-reads-writes

Add: database read, writes to usage
This commit is contained in:
Darshan
2025-01-29 14:50:27 +05:30
committed by GitHub
16 changed files with 414 additions and 58 deletions
-1
View File
@@ -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]
+1
View File
@@ -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';
+23
View File
@@ -0,0 +1,23 @@
<script context="module" lang="ts">
export type LegendData = {
name: string;
value: string | number | boolean;
};
</script>
<script lang="ts">
import { Colors } from '$lib/charts/config';
import { Status } from '$lib/components';
export let legendData: LegendData[] = [];
let colors = Object.values(Colors);
</script>
<div class="u-flex u-cross-center u-gap-16">
{#each legendData as { name, value }, index}
<Status status="none" statusIconStyle="background-color: {colors[index % colors.length]}">
{name} ({value})
</Status>
{/each}
</div>
+21 -3
View File
@@ -6,17 +6,35 @@
| 'completed'
| 'processing'
| 'ready'
| 'building';
| 'building'
| 'none';
export let statusIconStyle: string | undefined = undefined;
</script>
<div
class="status"
class="status u-cross-center"
class:is-pending={status === 'pending'}
class:is-failed={status === 'failed'}
class:is-complete={status === 'completed' || status === 'ready'}
class:is-processing={status === 'processing' || status === 'building'}>
{#if status}
<span class="status-icon" />
<span class="status-icon" style={statusIconStyle} />
{/if}
<span class="text" data-private><slot /></span>
</div>
<style>
.status-icon {
width: 8px;
height: 8px;
}
.status {
gap: 6px;
}
.text {
line-height: 140%;
}
</style>
+1
View File
@@ -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';
+1 -1
View File
@@ -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;
+74
View File
@@ -0,0 +1,74 @@
<script lang="ts">
import { Container } from '$lib/layout';
import { BarChart, Legend, type LegendData } from '$lib/charts';
import { accumulateFromEndingTotal } from '$lib/layout/usage.svelte';
import { Card, Heading, SecondaryTabs, SecondaryTabsItem } from '$lib/components';
import { page } from '$app/stores';
import { type Models } from '@appwrite.io/console';
import { formatNumberWithCommas } from '$lib/helpers/numbers';
export let title: string;
export let total: number[];
export let path: string = null;
export let count: Models.Metric[][];
export let legendData: LegendData[];
export let showHeader: boolean = true;
export let overlapContainerCover = false;
</script>
<Container overlapCover={overlapContainerCover}>
<div class="u-flex u-main-space-between common-section">
{#if showHeader}
<Heading tag="h2" size="5">{title}</Heading>
{/if}
{#if path}
<SecondaryTabs>
<SecondaryTabsItem href={`${path}/24h`} disabled={$page.params.period === '24h'}>
24h
</SecondaryTabsItem>
<SecondaryTabsItem
href={`${path}/30d`}
disabled={!$page.params.period || $page.params.period === '30d'}>
30d
</SecondaryTabsItem>
<SecondaryTabsItem href={`${path}/90d`} disabled={$page.params.period === '90d'}>
90d
</SecondaryTabsItem>
</SecondaryTabs>
{/if}
</div>
<Card>
{#if count}
{@const totalCount = total.reduce((a, b) => a + b, 0)}
<Heading tag="h6" size="6">{formatNumberWithCommas(totalCount)}</Heading>
<p>Total {title.toLocaleLowerCase()}</p>
<div class="u-margin-block-start-16" />
<div class="multiple-chart-container u-flex-vertical u-gap-16">
<BarChart
formatted={$page.params.period === '24h' ? 'hours' : 'days'}
series={count.map((c, index) => ({
name: legendData[index].name,
data: accumulateFromEndingTotal(c, total[index])
}))} />
{#if legendData}
<Legend {legendData} />
{/if}
</div>
{/if}
</Card>
</Container>
<style lang="scss">
.multiple-chart-container {
height: 12rem;
}
:global(.multiple-chart-container .echart) {
margin-top: -1em;
margin-bottom: -1em;
}
</style>
+4
View File
@@ -179,9 +179,13 @@ export type Aggregation = {
export type OrganizationUsage = {
bandwidth: Array<Models.Metric>;
executions: Array<Models.Metric>;
databasesReads: Array<Models.Metric>;
databasesWrites: Array<Models.Metric>;
executionsTotal: number;
filesStorageTotal: number;
buildsStorageTotal: number;
databasesReadsTotal: number;
databasesWritesTotal: number;
deploymentsStorageTotal: number;
executionsMBSecondsTotal: number;
buildsMBSecondsTotal: number;
@@ -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 }
];
</script>
<Container>
@@ -133,7 +138,7 @@
<span
class="icon-chart-square-bar text-large"
aria-hidden="true"
style="font-size: 32px;" />
style:font-size="32px" />
<p class="u-bold">No data to show</p>
</div>
</Card>
@@ -193,6 +198,67 @@
</svelte:fragment>
</CardGrid>
<CardGrid>
<Heading tag="h6" size="7">Database reads and writes</Heading>
<p class="text">
The total number of database reads and writes across all projects in your organization.
</p>
<svelte:fragment slot="aside">
{#if data.organizationUsage.databasesReads || data.organizationUsage.databasesWrites}
<div style:margin-top="-1.5em" style:margin-bottom="-1em">
<BarChart
options={{
yAxis: {
axisLabel: {
formatter: formatNum
}
}
}}
series={[
{
name: 'Reads',
data: [
...(data.organizationUsage.databasesReads ?? []).map((e) => [
e.date,
e.value
])
]
},
{
name: 'Writes',
data: [
...(data.organizationUsage.databasesWrites ?? []).map((e) => [
e.date,
e.value
])
]
}
]} />
</div>
<Legend {legendData} />
{#if project?.length > 0}
<ProjectBreakdown
{data}
projects={project}
databaseOperationMetric={['databasesReads', 'databasesWrites']} />
{/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>
<CardGrid>
<Heading tag="h6" size="7">Executions</Heading>
@@ -318,6 +384,7 @@
{/if}
</svelte:fragment>
</CardGrid>
<CardGrid>
<Heading tag="h6" size="7">GB hours</Heading>
@@ -378,6 +445,7 @@
{/if}
</svelte:fragment>
</CardGrid>
<CardGrid>
<Heading tag="h6" size="7">Phone OTP</Heading>
<p class="text">
@@ -430,6 +498,7 @@
{/if}
</svelte:fragment>
</CardGrid>
<TotalMembers members={data?.organizationMembers} />
<p class="text common-section u-color-text-gray">
@@ -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
}
};
}
@@ -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<Metric, 'databasesReads' | 'databasesWrites'>;
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`);
}
});
</script>
<Collapsible>
@@ -76,7 +117,13 @@
<TableScroll dense noStyles noMargin style="table-layout: auto">
<TableHeader>
<TableCellHead>Project</TableCellHead>
<TableCellHead>{getMetricTitle(metric)}</TableCellHead>
{#if databaseOperationMetric}
<TableCellHead>Reads</TableCellHead>
<TableCellHead>Writes</TableCellHead>
{:else}
<TableCellHead>{getMetricTitle(metric)}</TableCellHead>
{/if}
{#if estimate}
<TableCellHead>Estimated cost</TableCellHead>
{/if}
@@ -85,17 +132,33 @@
{/if}
</TableHeader>
<TableBody>
{#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}
<TableRow>
<TableCell title="Project">
{data.projectNames[project.projectId]?.name ?? 'Unknown'}
</TableCell>
<TableCell title={getMetricTitle(metric)}
>{format(project.usage)}</TableCell>
{#if databaseOperationMetric}
<TableCell title="Reads">
{format(project.databasesReads ?? 0)}
</TableCell>
<TableCell title="Writes">
{format(project.databasesWrites ?? 0)}
</TableCell>
{:else}
<TableCell>
{format(project.usage)}
</TableCell>
{/if}
{#if project.estimate}
<TableCell title="Estimated cost"
>{formatCurrency(project.estimate)}</TableCell>
<TableCell title="Estimated cost">
{formatCurrency(project.estimate)}
</TableCell>
{/if}
</TableRow>
{:else}
@@ -103,11 +166,23 @@
<TableCell title="Project">
{data.projectNames[project.projectId]?.name ?? 'Unknown'}
</TableCell>
<TableCell title={getMetricTitle(metric)}
>{format(project.usage)}</TableCell>
{#if databaseOperationMetric}
<TableCell title="Reads">
{format(project.databasesReads ?? 0)}
</TableCell>
<TableCell title="Writes">
{format(project.databasesWrites ?? 0)}
</TableCell>
{:else}
<TableCell>
{format(project.usage)}
</TableCell>
{/if}
{#if project.estimate}
<TableCell title="Estimated cost"
>{formatCurrency(project.estimate)}</TableCell>
<TableCell title="Estimated cost">
{formatCurrency(project.estimate)}
</TableCell>
{/if}
<TableCell right={true}>
<span
@@ -1,21 +1,40 @@
<script lang="ts">
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;
</script>
<Usage
title="Databases"
path={`${base}/project-${$page.params.project}/databases/database-${$page.params.database}/usage`}
{total}
{count}
countMetadata={{
legend: 'Collections',
title: 'Total collections'
}} />
<div class="u-flex u-flex-vertical u-gap-16">
<Usage
title="Usage"
path={`${base}/project-${$page.params.project}/databases/database-${$page.params.database}/usage`}
{total}
{count}
countMetadata={{
legend: 'Collections',
title: 'Total collections'
}} />
<UsageMultiple
title="Reads and writes"
showHeader={false}
overlapContainerCover
total={[readsTotal, writesTotal]}
count={[reads, writes]}
legendData={[
{ name: 'Reads', value: readsTotal },
{ name: 'Writes', value: writesTotal }
]} />
</div>
@@ -1,20 +1,39 @@
<script lang="ts">
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.databasesTotal;
$: count = data.databases;
$: reads = data.databasesReads;
$: readsTotal = data.databasesReadsTotal;
$: writes = data.databasesWrites;
$: writesTotal = data.databasesWritesTotal;
</script>
<Usage
title="Databases"
path={`${base}/project-${$page.params.project}/databases/usage`}
{total}
{count}
countMetadata={{
legend: 'Databases',
title: 'Total databases'
}} />
<div class="u-flex u-flex-vertical u-gap-16">
<Usage
title="Usage"
path={`${base}/project-${$page.params.project}/databases/usage`}
{total}
{count}
countMetadata={{
legend: 'Databases',
title: 'Total databases'
}} />
<UsageMultiple
title="Reads and writes"
showHeader={false}
overlapContainerCover
total={[readsTotal, writesTotal]}
count={[reads, writes]}
legendData={[
{ name: 'Reads', value: readsTotal },
{ name: 'Writes', value: writesTotal }
]} />
</div>
@@ -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}
</svelte:fragment>
</CardGrid>
<CardGrid>
<Heading tag="h6" size="7">Database reads and writes</Heading>
<p class="text">Total database reads and writes in your project.</p>
<svelte:fragment slot="aside">
{#if dbReads || dbWrites}
<div style:margin-top="-1.5em" style:margin-bottom="-1em">
<BarChart
options={{
yAxis: {
axisLabel: {
formatter: formatNum
}
}
}}
series={[
{
name: 'Reads',
data: [...dbReads.map((e) => [e.date, e.value])]
},
{
name: 'Writes',
data: [...dbWrites.map((e) => [e.date, e.value])]
}
]} />
</div>
<Legend {legendData} />
{: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">Executions</Heading>