mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
refactor: charts and overview
This commit is contained in:
Generated
+17
@@ -13,6 +13,7 @@
|
||||
"@aw-labs/ui": "0.0.0-69",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"echarts": "^5.4.0",
|
||||
"pretty-bytes": "^6.0.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"web-vitals": "^3.0.3"
|
||||
@@ -5942,6 +5943,17 @@
|
||||
"svelte": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-bytes": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.0.0.tgz",
|
||||
"integrity": "sha512-6UqkYefdogmzqAZWzJ7laYeJnaXDy2/J+ZqiiMtS7t7OfpXWTlaeGMwX8U6EFvPV/YWWEKRkS8hKS4k60WHTOg==",
|
||||
"engines": {
|
||||
"node": "^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-format": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
||||
@@ -11977,6 +11989,11 @@
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"pretty-bytes": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.0.0.tgz",
|
||||
"integrity": "sha512-6UqkYefdogmzqAZWzJ7laYeJnaXDy2/J+ZqiiMtS7t7OfpXWTlaeGMwX8U6EFvPV/YWWEKRkS8hKS4k60WHTOg=="
|
||||
},
|
||||
"pretty-format": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"@aw-labs/ui": "0.0.0-69",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"echarts": "^5.4.0",
|
||||
"pretty-bytes": "^6.0.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"web-vitals": "^3.0.3"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"borderWidth": 1
|
||||
},
|
||||
"lineStyle": {
|
||||
"width": 2
|
||||
"width": 1
|
||||
},
|
||||
"symbolSize": 4,
|
||||
"symbol": "emptyCircle",
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
(await import('echarts/components')).TransformComponent,
|
||||
(await import('echarts/components')).LegendComponent,
|
||||
(await import('echarts/features')).LabelLayout,
|
||||
(await import('echarts/renderers')).CanvasRenderer
|
||||
(await import('echarts/renderers')).SVGRenderer
|
||||
]);
|
||||
makeChart();
|
||||
});
|
||||
|
||||
@@ -27,6 +27,11 @@ export const defaultConfig: EChartsOption = {
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitNumber: 3,
|
||||
minInterval: 1
|
||||
minInterval: 1,
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
width: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -45,11 +45,11 @@
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"color": "#666666"
|
||||
"color": "#868EA3"
|
||||
},
|
||||
"splitLine": {
|
||||
"lineStyle": {
|
||||
"color": ["#333333"]
|
||||
"color": ["#4F5769"]
|
||||
}
|
||||
},
|
||||
"splitArea": {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import type { LineSeriesOption } from 'echarts/charts';
|
||||
import type { EChartsOption } from 'echarts';
|
||||
import Base from './base.svelte';
|
||||
import { Colors } from './config';
|
||||
import Base from './base.svelte';
|
||||
|
||||
export let series: LineSeriesOption[];
|
||||
export let options: EChartsOption = null;
|
||||
@@ -14,9 +14,10 @@
|
||||
s.type = 'line';
|
||||
s.stack = 'total';
|
||||
s.lineStyle = {
|
||||
shadowBlur: 1,
|
||||
shadowBlur: 38,
|
||||
shadowColor: Colors.Primary,
|
||||
shadowOffsetY: 0
|
||||
shadowOffsetY: 15,
|
||||
shadowOffsetX: 0
|
||||
};
|
||||
s.showSymbol = false;
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
export function calculateSize(bytes: number, decimals = 1) {
|
||||
@@ -20,55 +22,16 @@ export function bytesToSize(value: number, unit: string) {
|
||||
return value / Math.pow(1024, index);
|
||||
}
|
||||
|
||||
export function humanFileSize(value: number): {
|
||||
export function humanFileSize(bytes: number): {
|
||||
value: string;
|
||||
unit: string;
|
||||
} {
|
||||
if (!value) return { value: 'No', unit: 'Bytes' };
|
||||
const length = value.toString().length;
|
||||
const unit =
|
||||
length < 7
|
||||
? 'kilobyte'
|
||||
: length < 11
|
||||
? 'megabyte'
|
||||
: length < 15
|
||||
? 'gigabyte'
|
||||
: length < 19
|
||||
? 'terabyte'
|
||||
: 'petabyte';
|
||||
|
||||
const formatter = Intl.NumberFormat('en', {
|
||||
style: 'unit',
|
||||
maximumSignificantDigits: 3,
|
||||
maximumFractionDigits: 2,
|
||||
unit
|
||||
});
|
||||
|
||||
while (value > 1000) {
|
||||
value /= 1000;
|
||||
}
|
||||
|
||||
const parts: {
|
||||
integer: string;
|
||||
unit: string;
|
||||
fraction?: string;
|
||||
decimal?: string;
|
||||
} = formatter.formatToParts(value).reduce(
|
||||
(prev, curr) => {
|
||||
prev[curr.type] = curr.value;
|
||||
|
||||
return prev;
|
||||
},
|
||||
{
|
||||
integer: '',
|
||||
unit: '',
|
||||
decimal: '',
|
||||
fraction: ''
|
||||
}
|
||||
);
|
||||
const value = prettyBytes(bytes, {
|
||||
locale: 'en'
|
||||
}).split(' ');
|
||||
|
||||
return {
|
||||
value: `${parts.integer}${parts.decimal}${parts.fraction}`,
|
||||
unit: parts.unit
|
||||
value: value[0],
|
||||
unit: value[1]
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,6 +5,6 @@ import type { LayoutLoad } from './$types';
|
||||
export const load: LayoutLoad = async () => {
|
||||
return {
|
||||
header: Header,
|
||||
breadcrumb: Breadcrumbs
|
||||
breadcrumbs: Breadcrumbs
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,21 +1,41 @@
|
||||
<script context="module" lang="ts">
|
||||
const formatter = Intl.NumberFormat('en', {
|
||||
notation: 'compact'
|
||||
});
|
||||
|
||||
// TODO: metric type is wrong
|
||||
export function last(set: Array<unknown>): Models.Metric | null {
|
||||
return (set as Models.Metric[]).slice(-1)[0] ?? null;
|
||||
}
|
||||
|
||||
// TODO: metric type is wrong
|
||||
export function total(set: Array<unknown>): number {
|
||||
return (set as Models.Metric[]).reduce((prev, curr) => prev + curr.value, 0);
|
||||
}
|
||||
|
||||
export function format(number: number): string {
|
||||
return formatter.format(number);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { Models } from '@aw-labs/appwrite-console';
|
||||
import { Container, type UsagePeriods } from '$lib/layout';
|
||||
import { page } from '$app/stores';
|
||||
import { project, stats } from '../store';
|
||||
import { project } from '../store';
|
||||
import { usage } from './store';
|
||||
import { onMount } from 'svelte';
|
||||
import { afterNavigate } from '$app/navigation';
|
||||
import { DropList, DropListItem, Heading } from '$lib/components';
|
||||
import { BarChart, LineChart } from '$lib/charts';
|
||||
import { Heading } from '$lib/components';
|
||||
import { humanFileSize } from '$lib/helpers/sizeConvertion';
|
||||
import { base } from '$app/paths';
|
||||
import Realtime from './realtime.svelte';
|
||||
import Bandwith from './bandwith.svelte';
|
||||
import Requests from './requests.svelte';
|
||||
|
||||
$: projectId = $page.params.project;
|
||||
$: path = `/console/project-${projectId}/overview`;
|
||||
let period: UsagePeriods = '30d';
|
||||
let showPeriodBandwidth = false;
|
||||
let showPeriodRequests = false;
|
||||
|
||||
onMount(handle);
|
||||
afterNavigate(handle);
|
||||
@@ -28,40 +48,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
const formatter = Intl.NumberFormat('en', {
|
||||
notation: 'compact'
|
||||
});
|
||||
|
||||
// TODO: metric type is wrong
|
||||
function last(set: Array<unknown>): Models.Metric | null {
|
||||
return (set as Models.Metric[]).slice(-1)[0] ?? null;
|
||||
}
|
||||
|
||||
// TODO: metric type is wrong
|
||||
function total(set: Array<unknown>): number {
|
||||
return (set as Models.Metric[]).reduce((prev, curr) => prev + curr.value, 0);
|
||||
}
|
||||
|
||||
function format(number: number): string {
|
||||
return formatter.format(number);
|
||||
}
|
||||
|
||||
function changePeriod(newPeriod: UsagePeriods) {
|
||||
period = newPeriod;
|
||||
usage.load(projectId, period);
|
||||
showPeriodBandwidth = false;
|
||||
showPeriodRequests = false;
|
||||
}
|
||||
|
||||
//TODO: workaround for broken types
|
||||
$: network = $usage?.network as unknown as Array<{
|
||||
date: number;
|
||||
value: number;
|
||||
}>;
|
||||
$: requests = $usage?.requests as unknown as Array<{
|
||||
date: number;
|
||||
value: number;
|
||||
}>;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -71,98 +61,14 @@
|
||||
{#if $project}
|
||||
<Container overlapCover>
|
||||
{#if $usage}
|
||||
{@const bandwith = humanFileSize(total($usage.network))}
|
||||
{@const storage = humanFileSize(last($usage.storage).value)}
|
||||
<section class="common-section">
|
||||
<div class="grid-dashboard-1s-2m-6l">
|
||||
<div class="card is-2-columns-medium-screen is-3-columns-large-screen">
|
||||
<div class="u-flex u-gap-16 u-main-space-between">
|
||||
<div>
|
||||
<div class="heading-level-4">
|
||||
{bandwith.value}
|
||||
<span class="body-text-2">{bandwith.unit}</span>
|
||||
</div>
|
||||
<div>Bandwidth</div>
|
||||
</div>
|
||||
<DropList
|
||||
bind:show={showPeriodBandwidth}
|
||||
placement="bottom-start"
|
||||
childStart
|
||||
noArrow>
|
||||
<button
|
||||
class="transparent-button"
|
||||
on:click={() => (showPeriodBandwidth = !showPeriodBandwidth)}>
|
||||
<span class="text">{period}</span>
|
||||
<span class="icon-cheveron-down" aria-hidden="true" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<DropListItem on:click={() => changePeriod('24h')}>
|
||||
24h
|
||||
</DropListItem>
|
||||
<DropListItem on:click={() => changePeriod('30d')}>
|
||||
30d
|
||||
</DropListItem>
|
||||
<DropListItem on:click={() => changePeriod('90d')}>
|
||||
90d
|
||||
</DropListItem>
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
</div>
|
||||
{#if network.length}
|
||||
<div style="height: 12rem;">
|
||||
<BarChart
|
||||
series={[
|
||||
{
|
||||
name: 'Bandwidth',
|
||||
data: [...network.map((e) => [e.date, e.value])]
|
||||
}
|
||||
]} />
|
||||
</div>
|
||||
{/if}
|
||||
<Bandwith {period} on:change={(e) => changePeriod(e.detail)} />
|
||||
</div>
|
||||
<div class="card is-2-columns-medium-screen is-3-columns-large-screen">
|
||||
<div class="u-flex u-gap-16 u-main-space-between">
|
||||
<div>
|
||||
<div class="heading-level-4">
|
||||
{format(total($usage.requests))}
|
||||
</div>
|
||||
<div>Requests</div>
|
||||
</div>
|
||||
<DropList
|
||||
bind:show={showPeriodRequests}
|
||||
placement="bottom-start"
|
||||
childStart
|
||||
noArrow>
|
||||
<button
|
||||
class="transparent-button"
|
||||
on:click={() => (showPeriodRequests = !showPeriodRequests)}>
|
||||
<span class="text">{period}</span>
|
||||
<span class="icon-cheveron-down" aria-hidden="true" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<DropListItem on:click={() => changePeriod('24h')}>
|
||||
24h
|
||||
</DropListItem>
|
||||
<DropListItem on:click={() => changePeriod('30d')}>
|
||||
30d
|
||||
</DropListItem>
|
||||
<DropListItem on:click={() => changePeriod('90d')}>
|
||||
90d
|
||||
</DropListItem>
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
</div>
|
||||
{#if network.length}
|
||||
<div style="height: 12rem;">
|
||||
<LineChart
|
||||
series={[
|
||||
{
|
||||
name: 'Requests',
|
||||
data: [...requests.map((e) => [e.date, e.value])]
|
||||
}
|
||||
]} />
|
||||
</div>
|
||||
{/if}
|
||||
<Requests {period} on:change={(e) => changePeriod(e.detail)} />
|
||||
</div>
|
||||
<a
|
||||
href={`${base}/console/project-${projectId}/databases`}
|
||||
@@ -178,12 +84,14 @@
|
||||
<div class="grid-item-1-start-end" />
|
||||
|
||||
<div class="grid-item-1-end-start">
|
||||
<div class="heading-level-4">XX</div>
|
||||
<div>Databases</div>
|
||||
<div class="heading-level-4">
|
||||
{format(last($usage.documents).value)}
|
||||
</div>
|
||||
<div>Documents</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-item-1-end-end">
|
||||
<div class="text">Documents: {last($usage.documents).value}</div>
|
||||
<div class="text">Databases: XX</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@@ -261,23 +169,7 @@
|
||||
</a>
|
||||
<div
|
||||
class="card is-2-columns-medium-screen is-2-columns-large-screen is-2-rows-large-screen is-location-row-2-end-large-screen">
|
||||
{#if $stats.get(projectId)}
|
||||
<div class="heading-level-4">
|
||||
{format($stats.get(projectId)[11][1])}
|
||||
</div>
|
||||
<div>Realtime Connections</div>
|
||||
<div style="height: 18em">
|
||||
<BarChart
|
||||
series={[
|
||||
{
|
||||
name: 'Realtime connection',
|
||||
data: $stats.get(projectId)
|
||||
}
|
||||
]} />
|
||||
</div>
|
||||
{:else}
|
||||
<div>waiting for realtime connections</div>
|
||||
{/if}
|
||||
<Realtime />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<script lang="ts">
|
||||
import { Card, DropList, DropListItem } from '$lib/components';
|
||||
import { humanFileSize } from '$lib/helpers/sizeConvertion';
|
||||
import { total } from './+layout.svelte';
|
||||
import { usage } from './store';
|
||||
import type { UsagePeriods } from '$lib/layout';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { BarChart } from '$lib/charts';
|
||||
|
||||
export let period: UsagePeriods;
|
||||
|
||||
let showPeriod = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
$: network = $usage?.network as unknown as Array<{
|
||||
date: number;
|
||||
value: number;
|
||||
}>;
|
||||
$: bandwith = humanFileSize(total($usage.network));
|
||||
</script>
|
||||
|
||||
<div class="u-flex u-gap-16 u-main-space-between">
|
||||
<div>
|
||||
<div class="heading-level-4">
|
||||
{bandwith.value}
|
||||
<span class="body-text-2">{bandwith.unit}</span>
|
||||
</div>
|
||||
<div>Bandwidth</div>
|
||||
</div>
|
||||
<DropList bind:show={showPeriod} placement="bottom-start" childStart noArrow>
|
||||
<button class="transparent-button" on:click={() => (showPeriod = !showPeriod)}>
|
||||
<span class="text">{period}</span>
|
||||
<span class="icon-cheveron-down" aria-hidden="true" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<DropListItem on:click={() => dispatch('change', '24h')}>24h</DropListItem>
|
||||
<DropListItem on:click={() => dispatch('change', '30d')}>30d</DropListItem>
|
||||
<DropListItem on:click={() => dispatch('change', '90d')}>90d</DropListItem>
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
</div>
|
||||
{#if bandwith.value !== '0'}
|
||||
<div style="height: 12rem;">
|
||||
<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}`
|
||||
}
|
||||
}
|
||||
]} />
|
||||
</div>
|
||||
{:else}
|
||||
<Card>
|
||||
<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: var(--icon-size-large);" />
|
||||
<p class="u-bold">No data to show</p>
|
||||
</div>
|
||||
</Card>
|
||||
{/if}
|
||||
@@ -9,7 +9,7 @@ export const load: PageLoad = async ({ params, depends }) => {
|
||||
|
||||
return {
|
||||
header: Header,
|
||||
breadcrumb: Breadcrumbs,
|
||||
breadcrumbs: Breadcrumbs,
|
||||
platform: await sdkForConsole.projects.getPlatform(params.project, params.platform)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { BarChart } from '$lib/charts';
|
||||
import { Card } from '$lib/components';
|
||||
import { stats } from '../store';
|
||||
import { format } from './+layout.svelte';
|
||||
|
||||
$: projectId = $page.params.project;
|
||||
$: projectStats = $stats?.get(projectId);
|
||||
$: current = projectStats ? projectStats[11][1] : 0;
|
||||
$: total = projectStats?.reduce((prev, next) => prev + next[1], 0) ?? 0;
|
||||
</script>
|
||||
|
||||
<div class="heading-level-4">
|
||||
{format(current)}
|
||||
</div>
|
||||
<div>Realtime Connections</div>
|
||||
{#if total}
|
||||
<div style="height: 18em">
|
||||
<BarChart
|
||||
series={[
|
||||
{
|
||||
name: 'Realtime connection',
|
||||
data: $stats.get(projectId)
|
||||
}
|
||||
]} />
|
||||
</div>
|
||||
{:else}
|
||||
<Card>
|
||||
<div
|
||||
class="u-flex u-cross-center u-flex-vertical u-main-center u-flex"
|
||||
style="height: 10rem;">
|
||||
<span
|
||||
class="icon-chart-square-bar text-large"
|
||||
aria-hidden="true"
|
||||
style="font-size: var(--icon-size-large);" />
|
||||
<p class="u-bold">No data to show</p>
|
||||
<a
|
||||
class="link"
|
||||
href="https://appwrite.io/docs/realtime"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">Get started with Realtime</a>
|
||||
</div>
|
||||
</Card>
|
||||
{/if}
|
||||
@@ -0,0 +1,67 @@
|
||||
<script lang="ts">
|
||||
import { Card, DropList, DropListItem } from '$lib/components';
|
||||
import { format, total } from './+layout.svelte';
|
||||
import { usage } from './store';
|
||||
import type { UsagePeriods } from '$lib/layout';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { LineChart } from '$lib/charts';
|
||||
|
||||
export let period: UsagePeriods;
|
||||
|
||||
let showPeriod = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
$: requests = $usage?.requests as unknown as Array<{
|
||||
date: number;
|
||||
value: number;
|
||||
}>;
|
||||
</script>
|
||||
|
||||
<div class="u-flex u-gap-16 u-main-space-between">
|
||||
<div>
|
||||
<div class="heading-level-4">
|
||||
{format(total($usage.requests))}
|
||||
</div>
|
||||
<div>Requests</div>
|
||||
</div>
|
||||
<DropList bind:show={showPeriod} placement="bottom-start" childStart noArrow>
|
||||
<button class="transparent-button" on:click={() => (showPeriod = !showPeriod)}>
|
||||
<span class="text">{period}</span>
|
||||
<span class="icon-cheveron-down" aria-hidden="true" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<DropListItem on:click={() => dispatch('change', '24h')}>24h</DropListItem>
|
||||
<DropListItem on:click={() => dispatch('change', '30d')}>30d</DropListItem>
|
||||
<DropListItem on:click={() => dispatch('change', '90d')}>90d</DropListItem>
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
</div>
|
||||
{#if total($usage.requests) !== 0}
|
||||
<div style="height: 12rem;">
|
||||
<LineChart
|
||||
options={{
|
||||
yAxis: {
|
||||
axisLabel: {
|
||||
formatter: format
|
||||
}
|
||||
}
|
||||
}}
|
||||
series={[
|
||||
{
|
||||
name: 'Requests',
|
||||
data: [...requests.map((e) => [e.date, e.value])]
|
||||
}
|
||||
]} />
|
||||
</div>
|
||||
{:else}
|
||||
<Card>
|
||||
<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: var(--icon-size-large);" />
|
||||
<p class="u-bold">No data to show</p>
|
||||
</div>
|
||||
</Card>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user