Server Stats: Add skeleton loader (#79138)

* add server stat skeleton loader

* prevent flicker
This commit is contained in:
Ashley Harrison 2023-12-06 16:06:30 +00:00 committed by GitHub
parent bfde6f2c8a
commit 30ead91d38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 91 additions and 119 deletions

View File

@ -1402,17 +1402,6 @@ exports[`better eslint`] = {
"public/app/features/admin/OrgRolePicker.tsx:5381": [ "public/app/features/admin/OrgRolePicker.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"] [0, 0, 0, "Do not use any type assertions.", "0"]
], ],
"public/app/features/admin/ServerStats.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],
[0, 0, 0, "Styles should be written using objects.", "2"],
[0, 0, 0, "Styles should be written using objects.", "3"],
[0, 0, 0, "Styles should be written using objects.", "4"],
[0, 0, 0, "Styles should be written using objects.", "5"],
[0, 0, 0, "Styles should be written using objects.", "6"],
[0, 0, 0, "Styles should be written using objects.", "7"],
[0, 0, 0, "Styles should be written using objects.", "8"]
],
"public/app/features/admin/UpgradePage.tsx:5381": [ "public/app/features/admin/UpgradePage.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"], [0, 0, 0, "Styles should be written using objects.", "1"],

View File

@ -3,17 +3,17 @@ import React, { useEffect, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { CardContainer, LinkButton, useStyles2 } from '@grafana/ui'; import { LinkButton, useStyles2 } from '@grafana/ui';
import { AccessControlAction } from 'app/types'; import { AccessControlAction } from 'app/types';
import { contextSrv } from '../../core/services/context_srv'; import { contextSrv } from '../../core/services/context_srv';
import { Loader } from '../plugins/admin/components/Loader';
import { ServerStatsCard } from './ServerStatsCard';
import { getServerStats, ServerStat } from './state/apis'; import { getServerStats, ServerStat } from './state/apis';
export const ServerStats = () => { export const ServerStats = () => {
const [stats, setStats] = useState<ServerStat | null>(null); const [stats, setStats] = useState<ServerStat | null>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(true);
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const hasAccessToDataSources = contextSrv.hasPermission(AccessControlAction.DataSourcesRead); const hasAccessToDataSources = contextSrv.hasPermission(AccessControlAction.DataSourcesRead);
@ -21,7 +21,6 @@ export const ServerStats = () => {
useEffect(() => { useEffect(() => {
if (contextSrv.hasPermission(AccessControlAction.ActionServerStatsRead)) { if (contextSrv.hasPermission(AccessControlAction.ActionServerStatsRead)) {
setIsLoading(true);
getServerStats().then((stats) => { getServerStats().then((stats) => {
setStats(stats); setStats(stats);
setIsLoading(false); setIsLoading(false);
@ -36,18 +35,17 @@ export const ServerStats = () => {
return ( return (
<> <>
<h2 className={styles.title}>Instance statistics</h2> <h2 className={styles.title}>Instance statistics</h2>
{isLoading ? ( {!isLoading && !stats ? (
<div className={styles.loader}> <p className={styles.notFound}>No stats found.</p>
<Loader text={'Loading instance stats...'} /> ) : (
</div>
) : stats ? (
<div className={styles.row}> <div className={styles.row}>
<StatCard <ServerStatsCard
isLoading={isLoading}
content={[ content={[
{ name: 'Dashboards (starred)', value: `${stats.dashboards} (${stats.stars})` }, { name: 'Dashboards (starred)', value: `${stats?.dashboards} (${stats?.stars})` },
{ name: 'Tags', value: stats.tags }, { name: 'Tags', value: stats?.tags },
{ name: 'Playlists', value: stats.playlists }, { name: 'Playlists', value: stats?.playlists },
{ name: 'Snapshots', value: stats.snapshots }, { name: 'Snapshots', value: stats?.snapshots },
]} ]}
footer={ footer={
<LinkButton href={'/dashboards'} variant={'secondary'}> <LinkButton href={'/dashboards'} variant={'secondary'}>
@ -57,8 +55,9 @@ export const ServerStats = () => {
/> />
<div className={styles.doubleRow}> <div className={styles.doubleRow}>
<StatCard <ServerStatsCard
content={[{ name: 'Data sources', value: stats.datasources }]} isLoading={isLoading}
content={[{ name: 'Data sources', value: stats?.datasources }]}
footer={ footer={
hasAccessToDataSources && ( hasAccessToDataSources && (
<LinkButton href={'/datasources'} variant={'secondary'}> <LinkButton href={'/datasources'} variant={'secondary'}>
@ -67,8 +66,9 @@ export const ServerStats = () => {
) )
} }
/> />
<StatCard <ServerStatsCard
content={[{ name: 'Alerts', value: stats.alerts }]} isLoading={isLoading}
content={[{ name: 'Alerts', value: stats?.alerts }]}
footer={ footer={
<LinkButton href={'/alerting/list'} variant={'secondary'}> <LinkButton href={'/alerting/list'} variant={'secondary'}>
Alerts Alerts
@ -76,18 +76,19 @@ export const ServerStats = () => {
} }
/> />
</div> </div>
<StatCard <ServerStatsCard
isLoading={isLoading}
content={[ content={[
{ name: 'Organisations', value: stats.orgs }, { name: 'Organisations', value: stats?.orgs },
{ name: 'Users total', value: stats.users }, { name: 'Users total', value: stats?.users },
{ name: 'Active users in last 30 days', value: stats.activeUsers }, { name: 'Active users in last 30 days', value: stats?.activeUsers },
...(config.featureToggles.displayAnonymousStats && stats.activeDevices ...(config.featureToggles.displayAnonymousStats && stats?.activeDevices
? [ ? [
{ name: 'Active anonymous devices in last 30 days', value: stats.activeDevices }, { name: 'Active anonymous devices in last 30 days', value: stats?.activeDevices },
{ name: 'Active anonymous users in last 30 days', value: Math.floor(stats.activeDevices / 3) }, { name: 'Active anonymous users in last 30 days', value: Math.floor(stats?.activeDevices / 3) },
] ]
: []), : []),
{ name: 'Active sessions', value: stats.activeSessions }, { name: 'Active sessions', value: stats?.activeSessions },
]} ]}
footer={ footer={
hasAccessToAdminUsers && ( hasAccessToAdminUsers && (
@ -98,8 +99,6 @@ export const ServerStats = () => {
} }
/> />
</div> </div>
) : (
<p className={styles.notFound}>No stats found.</p>
)} )}
</> </>
); );
@ -107,88 +106,34 @@ export const ServerStats = () => {
const getStyles = (theme: GrafanaTheme2) => { const getStyles = (theme: GrafanaTheme2) => {
return { return {
title: css` title: css({
margin-bottom: ${theme.spacing(4)}; marginBottom: theme.spacing(4),
`, }),
row: css` row: css({
display: flex; display: 'flex',
justify-content: space-between; justifyContent: 'space-between',
width: 100%; width: '100%',
& > div:not(:last-of-type) { '& > div:not(:last-of-type)': {
margin-right: ${theme.spacing(2)}; marginRight: theme.spacing(2),
} },
& > div { '& > div': {
width: 33.3%; width: '33.3%',
} },
`, }),
doubleRow: css` doubleRow: css({
display: flex; display: 'flex',
flex-direction: column; flexDirection: 'column',
& > div:first-of-type { '& > div:first-of-type': {
margin-bottom: ${theme.spacing(2)}; marginBottom: theme.spacing(2),
} },
`, }),
notFound: css({
loader: css` fontSize: theme.typography.h6.fontSize,
height: 290px; textAlign: 'center',
`, height: '290px',
}),
notFound: css`
font-size: ${theme.typography.h6.fontSize};
text-align: center;
height: 290px;
`,
};
};
type StatCardProps = {
content: Array<Record<string, number | string>>;
footer?: JSX.Element | boolean;
};
const StatCard = ({ content, footer }: StatCardProps) => {
const styles = useStyles2(getCardStyles);
return (
<CardContainer className={styles.container} disableHover>
<div className={styles.inner}>
<div className={styles.content}>
{content.map((item) => {
return (
<div key={item.name} className={styles.row}>
<span>{item.name}</span>
<span>{item.value}</span>
</div>
);
})}
</div>
{footer && <div>{footer}</div>}
</div>
</CardContainer>
);
};
const getCardStyles = (theme: GrafanaTheme2) => {
return {
container: css`
padding: ${theme.spacing(2)};
`,
inner: css`
display: flex;
flex-direction: column;
width: 100%;
`,
content: css`
flex: 1 0 auto;
`,
row: css`
display: flex;
justify-content: space-between;
width: 100%;
margin-bottom: ${theme.spacing(2)};
align-items: center;
`,
}; };
}; };

View File

@ -0,0 +1,38 @@
import { css } from '@emotion/css';
import React from 'react';
import Skeleton from 'react-loading-skeleton';
import { GrafanaTheme2 } from '@grafana/data';
import { CardContainer, Stack, useStyles2 } from '@grafana/ui';
export interface Props {
content: Array<Record<string, string | number | undefined>>;
isLoading?: boolean;
footer?: JSX.Element | boolean;
}
export const ServerStatsCard = ({ content, footer, isLoading }: Props) => {
const styles = useStyles2(getStyles);
return (
<CardContainer className={styles.container} disableHover>
{content.map((item, index) => (
<Stack key={index} justifyContent="space-between" alignItems="center">
<span>{item.name}</span>
{isLoading ? <Skeleton width={60} /> : <span>{item.value}</span>}
</Stack>
))}
{footer && <div>{footer}</div>}
</CardContainer>
);
};
const getStyles = (theme: GrafanaTheme2) => {
return {
container: css({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
padding: theme.spacing(2),
}),
};
};