diff --git a/src/lib/charts/bar.svelte b/src/lib/charts/bar.svelte index 9d19e36053..237d111493 100644 --- a/src/lib/charts/bar.svelte +++ b/src/lib/charts/bar.svelte @@ -6,11 +6,17 @@ export let series: BarSeriesOption[]; export let options: EChartsOption = null; export let formatted: 'days' | 'hours' = 'days'; + export let setOptionConfig: { + notMerge?: boolean; + lazyUpdate?: boolean; + replaceMerge?: string[]; + } = undefined; { s.type = 'bar'; s.barMaxWidth = 6; diff --git a/src/lib/charts/base.svelte b/src/lib/charts/base.svelte index 98e8de5a26..cdea6ad7fd 100644 --- a/src/lib/charts/base.svelte +++ b/src/lib/charts/base.svelte @@ -15,6 +15,11 @@ export let options: EChartsOption; export let series: (BarSeriesOption | LineSeriesOption)[]; export let formatted: 'days' | 'hours' = 'days'; + export let setOptionConfig: { + notMerge?: boolean; + lazyUpdate?: boolean; + replaceMerge?: string[]; + } = {}; let chart: ECharts; let container: HTMLDivElement; @@ -54,7 +59,7 @@ const setOption = () => { if (chart && !chart.isDisposed()) { - chart.setOption(option); + chart.setOption(option, setOptionConfig); } }; diff --git a/src/lib/charts/line.svelte b/src/lib/charts/line.svelte index b5f4e3b8f1..24059eeb1c 100644 --- a/src/lib/charts/line.svelte +++ b/src/lib/charts/line.svelte @@ -7,19 +7,27 @@ export let series: LineSeriesOption[]; export let options: EChartsOption = null; export let formatted: 'days' | 'hours' = 'days'; + export let applyStyles: boolean = true; + export let setOptionConfig: { + notMerge?: boolean; + lazyUpdate?: boolean; + replaceMerge?: string[]; + } = undefined; { s.type = 'line'; s.stack = 'total'; s.lineStyle = { - shadowBlur: 38, - shadowColor: Colors.Primary, - shadowOffsetY: 15, - shadowOffsetX: 0 + shadowBlur: applyStyles ? 38 : undefined, + shadowColor: applyStyles ? Colors.Primary : undefined, + shadowOffsetY: applyStyles ? 15 : undefined, + shadowOffsetX: 0, + width: applyStyles ? undefined : 2 }; s.showSymbol = false; diff --git a/src/lib/components/card.svelte b/src/lib/components/card.svelte index c21c90ef55..8173ab222d 100644 --- a/src/lib/components/card.svelte +++ b/src/lib/components/card.svelte @@ -17,6 +17,7 @@ danger?: boolean; style?: string; class?: string; + fullHeightChild?: boolean; }; type ButtonProps = { @@ -42,6 +43,7 @@ export let padding: $$Props['padding'] = 'm'; export let radius: $$Props['radius'] = 'm'; export let variant: $$Props['variant'] = 'primary'; + export let fullHeightChild: $$Props['fullHeightChild'] = false; $: resolvedClasses = [classes].filter(Boolean).join(' '); @@ -56,13 +58,13 @@ on:click class={resolvedClasses} {...external ? { target: '_blank' } : {}}> - + {:else if isButton} - + @@ -74,7 +76,7 @@ {padding} {radius} {variant}> - + diff --git a/src/lib/helpers/faker.ts b/src/lib/helpers/faker.ts index 421288bb3b..e7545f2516 100644 --- a/src/lib/helpers/faker.ts +++ b/src/lib/helpers/faker.ts @@ -250,3 +250,30 @@ function generateSingleValue( } } } + +function seededRandom(seed: number) { + const x = Math.sin(seed) * 10000; + return x - Math.floor(x); +} + +const BASE_TIME_MS = Date.now(); + +export function generateFakeBarChartData(seed = 1) { + const fakeData: [number, number][] = []; + for (let i = 23; i >= 0; i--) { + const val = seededRandom(seed + i) * 1_000_000; + fakeData.push([BASE_TIME_MS - i * 60 * 60 * 1000, Math.round(val)]); + } + return fakeData; +} + +export function generateFakeLineChartData(seed = 2) { + const fakeData: [number, number][] = []; + let value = seededRandom(seed) * 5000 + 5000; + for (let i = 23; i >= 0; i--) { + value += (seededRandom(seed + i) - 0.5) * 1000; + value = Math.max(0, value); + fakeData.push([BASE_TIME_MS - i * 60 * 60 * 1000, Math.round(value)]); + } + return fakeData; +} diff --git a/src/routes/(console)/project-[region]-[project]/overview/(components)/skeletons/extended.svelte b/src/routes/(console)/project-[region]-[project]/overview/(components)/skeletons/extended.svelte new file mode 100644 index 0000000000..d71a108d4f --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/overview/(components)/skeletons/extended.svelte @@ -0,0 +1,69 @@ + + + + + + {#if loading} + + + + {:else if resourceMetric !== null} + + {#if isMetricObject(resourceMetric)} + {resourceMetric.value} + + {resourceMetric.unit} + + {:else} + {resourceMetric} + {/if} + + {/if} + + + + {metricName} + + + diff --git a/src/routes/(console)/project-[region]-[project]/overview/(components)/skeletons/simple.svelte b/src/routes/(console)/project-[region]-[project]/overview/(components)/skeletons/simple.svelte new file mode 100644 index 0000000000..b38c3a91eb --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/overview/(components)/skeletons/simple.svelte @@ -0,0 +1,61 @@ + + + + + {#if loading} + + + + {:else} + + {value} + {#if unit} + + {unit} + + {/if} + + {/if} + + + + + {resource} + + + diff --git a/src/routes/(console)/project-[region]-[project]/overview/+layout.svelte b/src/routes/(console)/project-[region]-[project]/overview/+layout.svelte index 4c6fb4341a..e4579325db 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/+layout.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/+layout.svelte @@ -21,7 +21,7 @@ import { onMount, setContext, type Component } from 'svelte'; import Bandwidth from './bandwidth.svelte'; import Requests from './requests.svelte'; - import { usage } from './store'; + import { loadingProjectUsage, usage } from './store'; import { formatNum } from '$lib/helpers/string'; import { periodToDates } from '$lib/layout/usage.svelte'; import { canWriteProjects } from '$lib/stores/roles'; @@ -29,21 +29,23 @@ import { writable, type Writable } from 'svelte/store'; import { IconPlus } from '@appwrite.io/pink-icons-svelte'; import { isSmallViewport } from '$lib/stores/viewport'; + import SimpleValueSkeleton from './(components)/skeletons/simple.svelte'; let period: UsagePeriods = '30d'; - $: path = `${base}/project-${page.params.region}-${page.params.project}/overview`; + + const action = setContext>('overview-action', writable(null)); onMount(handle); afterNavigate(handle); - const action = setContext>('overview-action', writable(null)); - async function handle() { + $usage = null; + $loadingProjectUsage = true; const promise = changePeriod(period); - if ($usage) { - await promise; - } + await promise.finally(() => { + $loadingProjectUsage = false; + }); } function changePeriod(newPeriod: UsagePeriods) { @@ -89,9 +91,9 @@ } ]); - $: $updateCommandGroupRanks({ - integrations: 10 - }); + $: $updateCommandGroupRanks({ integrations: 10 }); + + $: path = `${base}/project-${page.params.region}-${page.params.project}/overview`; @@ -100,124 +102,128 @@ - {#if $usage} - {@const storage = humanFileSize($usage.filesStorageTotal ?? 0)} - - - - changePeriod(e.detail)} /> - - - changePeriod(e.detail)} /> - - - - - - - - - - Database - + {@const storage = humanFileSize($usage?.filesStorageTotal ?? 0)} + + + + changePeriod(e.detail)} + loading={$loadingProjectUsage} /> + + + changePeriod(e.detail)} + loading={$loadingProjectUsage} /> + + + + + + + + + + Database + - + - - - {formatNum($usage.documentsTotal ?? 0)} - - Rows - + + - - - - - - - Storage - + + + + + + + + + Storage + - + - - - {storage.value} - {storage.unit} - - Storage - + + - - - - - - - Auth - + + + + + + + + + Auth + - + - - - {formatNum($usage.usersTotal ?? 0)} - - Users - + + - - - - - - - Functions - + + + + + + + + + Functions + - + - - - {formatNum($usage.executionsTotal ?? 0)} - - Executions - + + + - - - + + - - - - - - - + + + + + + + + - {/if} + Integrations diff --git a/src/routes/(console)/project-[region]-[project]/overview/bandwidth.svelte b/src/routes/(console)/project-[region]-[project]/overview/bandwidth.svelte index bc46005c0c..3436ad23a4 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/bandwidth.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/bandwidth.svelte @@ -19,75 +19,138 @@ IconChevronDown, IconChevronUp } from '@appwrite.io/pink-icons-svelte'; + import type { EChartsOption } from 'echarts'; + import { generateFakeBarChartData } from '$lib/helpers/faker'; + import ExtendedValueSkeleton from './(components)/skeletons/extended.svelte'; + import { fade } from 'svelte/transition'; - export let period: UsagePeriods; + let { + period, + loading + }: { + period: UsagePeriods; + loading: boolean; + } = $props(); const dispatch = createEventDispatcher(); + const fakeBarChartData = generateFakeBarChartData(); - $: network = $usage?.network as unknown as Array<{ - date: number; - value: number; - }>; + const network = $derived( + $usage?.network as unknown as Array<{ + date: number; + value: number; + }> + ); - $: bandwidth = humanFileSize(totalMetrics($usage?.network)); + const bandwidth = $derived(humanFileSize(totalMetrics($usage?.network))); + + const chartData = $derived(loading ? fakeBarChartData : network?.map((e) => [e.date, e.value])); + + const chartOptions = $derived.by(() => { + return { + animation: true, + animationDuration: 200, + animationEasing: 'quadraticInOut', + animationDurationUpdate: 500, + animationEasingUpdate: 'quadraticInOut', + universalTransition: true, + yAxis: { + axisLabel: { + formatter: (value: number) => { + return loading + ? '-- MB' + : !value + ? '0' + : `${humanFileSize(+value).value} ${humanFileSize(+value).unit}`; + } + } + }, + tooltip: { show: !loading }, + color: loading ? ['var(--border-neutral)'] : ['var(--bgcolor-accent)'] + } satisfies EChartsOption; + }); - - - {bandwidth.value} - {bandwidth.unit} - - Bandwidth - + + - + {period} - - - dispatch('change', '24h')} - >24h - dispatch('change', '30d')} - >30d - dispatch('change', '90d')} - >90d - - + + + { + toggle(event); + dispatch('change', '24h'); + }}> + 24h + + { + toggle(event); + dispatch('change', '30d'); + }}> + 30d + + { + toggle(event); + dispatch('change', '90d'); + }}> + 90d + + - {#if bandwidth.value !== '0'} - - - 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}` + + + {#if loading || bandwidth.value !== '0'} + + { + return `${humanFileSize(+value).value} ${humanFileSize(+value).unit}`; + } + } } - } - ]} /> - - {:else} - - - - No data to show - - - {/if} + ]} /> + + {:else} + + + + + No data to show + + + + {/if} + diff --git a/src/routes/(console)/project-[region]-[project]/overview/intro-dark.png b/src/routes/(console)/project-[region]-[project]/overview/intro-dark.png deleted file mode 100644 index 4e5bd12acd..0000000000 Binary files a/src/routes/(console)/project-[region]-[project]/overview/intro-dark.png and /dev/null differ diff --git a/src/routes/(console)/project-[region]-[project]/overview/intro-light.png b/src/routes/(console)/project-[region]-[project]/overview/intro-light.png deleted file mode 100644 index 35cd380430..0000000000 Binary files a/src/routes/(console)/project-[region]-[project]/overview/intro-light.png and /dev/null differ diff --git a/src/routes/(console)/project-[region]-[project]/overview/onboard-1-dark-desktop.svg b/src/routes/(console)/project-[region]-[project]/overview/onboard-1-dark-desktop.svg deleted file mode 100644 index 73498beffe..0000000000 --- a/src/routes/(console)/project-[region]-[project]/overview/onboard-1-dark-desktop.svg +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/routes/(console)/project-[region]-[project]/overview/onboard-1-dark-mobile.svg b/src/routes/(console)/project-[region]-[project]/overview/onboard-1-dark-mobile.svg deleted file mode 100644 index a370c6dd4f..0000000000 --- a/src/routes/(console)/project-[region]-[project]/overview/onboard-1-dark-mobile.svg +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/routes/(console)/project-[region]-[project]/overview/onboard-1-light-desktop.svg b/src/routes/(console)/project-[region]-[project]/overview/onboard-1-light-desktop.svg deleted file mode 100644 index 9764462ba9..0000000000 --- a/src/routes/(console)/project-[region]-[project]/overview/onboard-1-light-desktop.svg +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/routes/(console)/project-[region]-[project]/overview/onboard-1-light-mobile.svg b/src/routes/(console)/project-[region]-[project]/overview/onboard-1-light-mobile.svg deleted file mode 100644 index 380d252164..0000000000 --- a/src/routes/(console)/project-[region]-[project]/overview/onboard-1-light-mobile.svg +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/routes/(console)/project-[region]-[project]/overview/onboard-2-dark-desktop.svg b/src/routes/(console)/project-[region]-[project]/overview/onboard-2-dark-desktop.svg deleted file mode 100644 index f3fbc82fb1..0000000000 --- a/src/routes/(console)/project-[region]-[project]/overview/onboard-2-dark-desktop.svg +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/routes/(console)/project-[region]-[project]/overview/onboard-2-dark-mobile.svg b/src/routes/(console)/project-[region]-[project]/overview/onboard-2-dark-mobile.svg deleted file mode 100644 index d77cce1a28..0000000000 --- a/src/routes/(console)/project-[region]-[project]/overview/onboard-2-dark-mobile.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/routes/(console)/project-[region]-[project]/overview/onboard-2-light-desktop.svg b/src/routes/(console)/project-[region]-[project]/overview/onboard-2-light-desktop.svg deleted file mode 100644 index 9957c1ab59..0000000000 --- a/src/routes/(console)/project-[region]-[project]/overview/onboard-2-light-desktop.svg +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/routes/(console)/project-[region]-[project]/overview/onboard-2-light-mobile.svg b/src/routes/(console)/project-[region]-[project]/overview/onboard-2-light-mobile.svg deleted file mode 100644 index 4571f04ffe..0000000000 --- a/src/routes/(console)/project-[region]-[project]/overview/onboard-2-light-mobile.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/routes/(console)/project-[region]-[project]/overview/requests.svelte b/src/routes/(console)/project-[region]-[project]/overview/requests.svelte index c4d5aa9fd9..f4397e541c 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/requests.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/requests.svelte @@ -19,64 +19,123 @@ IconChevronDown, IconChevronUp } from '@appwrite.io/pink-icons-svelte'; + import type { EChartsOption } from 'echarts'; + import { generateFakeLineChartData } from '$lib/helpers/faker'; + import ExtendedValueSkeleton from './(components)/skeletons/extended.svelte'; + import { fade } from 'svelte/transition'; - export let period: UsagePeriods; + let { + period, + loading + }: { + period: UsagePeriods; + loading: boolean; + } = $props(); const dispatch = createEventDispatcher(); + const fakeLineChartData = generateFakeLineChartData(); - $: requests = $usage?.requests as unknown as Array<{ - date: number; - value: number; - }>; + let requests = $derived( + $usage?.requests as unknown as Array<{ + date: number; + value: number; + }> + ); + + let chartData = $derived( + loading ? fakeLineChartData : [...requests.map((e) => [e.date, e.value])] + ); + + let chartOptions = $derived.by(() => { + return { + animation: true, + animationDuration: 200, + animationEasing: 'quadraticInOut', + animationDurationUpdate: 200, + animationEasingUpdate: 'quadraticInOut', + universalTransition: true, + yAxis: { + axisLabel: { + formatter: (value: number) => (loading ? '--' : formatNum(value)) + } + }, + tooltip: { show: !loading }, + color: [loading ? 'var(--border-neutral-strong)' : 'var(--bgcolor-accent)'] + } satisfies EChartsOption; + }); - - - {formatNum(totalMetrics($usage?.requests))} - - Requests - + + - + {period} - - dispatch('change', '24h')} - >24h - dispatch('change', '30d')} - >30d - dispatch('change', '90d')} - >90d + + + { + toggle(event); + dispatch('change', '24h'); + }}>24h + { + toggle(event); + dispatch('change', '30d'); + }}>30d + { + toggle(event); + dispatch('change', '90d'); + }}>90d - {#if totalMetrics($usage?.requests) !== 0} - - + {#if loading || totalMetrics($usage?.requests) !== 0} + + [e.date, e.value])] - } - ]} /> - - {:else} - - - - No data to show - - - {/if} + ]} /> + + {:else} + + + + + No data to show + + + + {/if} + diff --git a/src/routes/(console)/project-[region]-[project]/overview/store.ts b/src/routes/(console)/project-[region]-[project]/overview/store.ts index 2e3171c0f5..94c3de36fb 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/store.ts +++ b/src/routes/(console)/project-[region]-[project]/overview/store.ts @@ -4,6 +4,11 @@ import { get, readable, writable, type Writable } from 'svelte/store'; import type { Models, ProjectUsageRange } from '@appwrite.io/console'; import { page } from '$app/state'; import type { Column } from '$lib/helpers/types'; +import { hash } from '$lib/helpers/string'; +import { sleep } from '$lib/helpers/promises'; +import { isDev } from '$lib/system'; + +export const loadingProjectUsage = writable(true); export const usage = cachedStore< Models.UsageProject, @@ -11,8 +16,29 @@ export const usage = cachedStore< load: (start: string, end: string, period: ProjectUsageRange) => Promise; } >('projectUsage', function ({ set }) { + const minTime = 1250; + let lastParamsHash: string | null = null; + return { load: async (start, end, period) => { + const currentData = get(usage); + const currentParamsHash = hash([ + page.params.project, + page.params.region, + start, + end, + period.toString() + ]); + + // don't hit the API call if we have the data! + if (lastParamsHash === currentParamsHash && currentData && !isDev) { + loadingProjectUsage.set(false); + return; + } + + const initTime = Date.now(); + loadingProjectUsage.set(true); + const usages = await sdk .forProject(page.params.region, page.params.project) .project.getUsage({ @@ -20,7 +46,17 @@ export const usage = cachedStore< endDate: end, period }); + + const elapsed = Date.now() - initTime; + const remainingTime = minTime - elapsed; + + if (remainingTime >= 0) { + await sleep(remainingTime); + } + set(usages); + lastParamsHash = currentParamsHash; + loadingProjectUsage.set(false); } }; });