diff --git a/src/lib/charts/line.svelte b/src/lib/charts/line.svelte index b5f4e3b8f1..6650492fd8 100644 --- a/src/lib/charts/line.svelte +++ b/src/lib/charts/line.svelte @@ -7,6 +7,8 @@ export let series: LineSeriesOption[]; export let options: EChartsOption = null; export let formatted: 'days' | 'hours' = 'days'; + + export let applyStyles: boolean = true; {#if href} - + {:else if isButton} - + @@ -64,7 +66,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 44027867ac..2ddb483671 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/+layout.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/+layout.svelte @@ -20,7 +20,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'; @@ -28,21 +28,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) { @@ -88,9 +90,9 @@ } ]); - $: $updateCommandGroupRanks({ - integrations: 10 - }); + $: $updateCommandGroupRanks({ integrations: 10 }); + + $: path = `${base}/project-${page.params.region}-${page.params.project}/overview`; @@ -99,124 +101,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..9509d874a4 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/bandwidth.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/bandwidth.svelte @@ -19,75 +19,133 @@ 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; - }>; + let network = $derived( + $usage?.network as unknown as Array<{ + date: number; + value: number; + }> + ); - $: bandwidth = humanFileSize(totalMetrics($usage?.network)); + let bandwidth = $derived(humanFileSize(totalMetrics($usage?.network))); + + let chartData = $derived(loading ? fakeBarChartData : network?.map((e) => [e.date, e.value])); + + let chartOptions = $derived.by(() => { + return { + animation: true, + animationDuration: 500, + 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..2222e7c316 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/requests.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/requests.svelte @@ -19,64 +19,122 @@ 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: 500, + animationEasing: 'quadraticInOut', + animationDurationUpdate: 500, + 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; + }); + + $effect(() => { + console.log(totalMetrics($usage?.requests)); + }); - - - {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); } }; });