mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Server Stats: Add skeleton loader (#79138)
* add server stat skeleton loader * prevent flicker
This commit is contained in:
parent
bfde6f2c8a
commit
30ead91d38
@ -1402,17 +1402,6 @@ exports[`better eslint`] = {
|
||||
"public/app/features/admin/OrgRolePicker.tsx:5381": [
|
||||
[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": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "1"],
|
||||
|
@ -3,17 +3,17 @@ import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { CardContainer, LinkButton, useStyles2 } from '@grafana/ui';
|
||||
import { LinkButton, useStyles2 } from '@grafana/ui';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
import { contextSrv } from '../../core/services/context_srv';
|
||||
import { Loader } from '../plugins/admin/components/Loader';
|
||||
|
||||
import { ServerStatsCard } from './ServerStatsCard';
|
||||
import { getServerStats, ServerStat } from './state/apis';
|
||||
|
||||
export const ServerStats = () => {
|
||||
const [stats, setStats] = useState<ServerStat | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const hasAccessToDataSources = contextSrv.hasPermission(AccessControlAction.DataSourcesRead);
|
||||
@ -21,7 +21,6 @@ export const ServerStats = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (contextSrv.hasPermission(AccessControlAction.ActionServerStatsRead)) {
|
||||
setIsLoading(true);
|
||||
getServerStats().then((stats) => {
|
||||
setStats(stats);
|
||||
setIsLoading(false);
|
||||
@ -36,18 +35,17 @@ export const ServerStats = () => {
|
||||
return (
|
||||
<>
|
||||
<h2 className={styles.title}>Instance statistics</h2>
|
||||
{isLoading ? (
|
||||
<div className={styles.loader}>
|
||||
<Loader text={'Loading instance stats...'} />
|
||||
</div>
|
||||
) : stats ? (
|
||||
{!isLoading && !stats ? (
|
||||
<p className={styles.notFound}>No stats found.</p>
|
||||
) : (
|
||||
<div className={styles.row}>
|
||||
<StatCard
|
||||
<ServerStatsCard
|
||||
isLoading={isLoading}
|
||||
content={[
|
||||
{ name: 'Dashboards (starred)', value: `${stats.dashboards} (${stats.stars})` },
|
||||
{ name: 'Tags', value: stats.tags },
|
||||
{ name: 'Playlists', value: stats.playlists },
|
||||
{ name: 'Snapshots', value: stats.snapshots },
|
||||
{ name: 'Dashboards (starred)', value: `${stats?.dashboards} (${stats?.stars})` },
|
||||
{ name: 'Tags', value: stats?.tags },
|
||||
{ name: 'Playlists', value: stats?.playlists },
|
||||
{ name: 'Snapshots', value: stats?.snapshots },
|
||||
]}
|
||||
footer={
|
||||
<LinkButton href={'/dashboards'} variant={'secondary'}>
|
||||
@ -57,8 +55,9 @@ export const ServerStats = () => {
|
||||
/>
|
||||
|
||||
<div className={styles.doubleRow}>
|
||||
<StatCard
|
||||
content={[{ name: 'Data sources', value: stats.datasources }]}
|
||||
<ServerStatsCard
|
||||
isLoading={isLoading}
|
||||
content={[{ name: 'Data sources', value: stats?.datasources }]}
|
||||
footer={
|
||||
hasAccessToDataSources && (
|
||||
<LinkButton href={'/datasources'} variant={'secondary'}>
|
||||
@ -67,8 +66,9 @@ export const ServerStats = () => {
|
||||
)
|
||||
}
|
||||
/>
|
||||
<StatCard
|
||||
content={[{ name: 'Alerts', value: stats.alerts }]}
|
||||
<ServerStatsCard
|
||||
isLoading={isLoading}
|
||||
content={[{ name: 'Alerts', value: stats?.alerts }]}
|
||||
footer={
|
||||
<LinkButton href={'/alerting/list'} variant={'secondary'}>
|
||||
Alerts
|
||||
@ -76,18 +76,19 @@ export const ServerStats = () => {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<StatCard
|
||||
<ServerStatsCard
|
||||
isLoading={isLoading}
|
||||
content={[
|
||||
{ name: 'Organisations', value: stats.orgs },
|
||||
{ name: 'Users total', value: stats.users },
|
||||
{ name: 'Active users in last 30 days', value: stats.activeUsers },
|
||||
...(config.featureToggles.displayAnonymousStats && stats.activeDevices
|
||||
{ name: 'Organisations', value: stats?.orgs },
|
||||
{ name: 'Users total', value: stats?.users },
|
||||
{ name: 'Active users in last 30 days', value: stats?.activeUsers },
|
||||
...(config.featureToggles.displayAnonymousStats && 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 devices in last 30 days', value: stats?.activeDevices },
|
||||
{ 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={
|
||||
hasAccessToAdminUsers && (
|
||||
@ -98,8 +99,6 @@ export const ServerStats = () => {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<p className={styles.notFound}>No stats found.</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@ -107,88 +106,34 @@ export const ServerStats = () => {
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
title: css`
|
||||
margin-bottom: ${theme.spacing(4)};
|
||||
`,
|
||||
row: css`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
title: css({
|
||||
marginBottom: theme.spacing(4),
|
||||
}),
|
||||
row: css({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
|
||||
& > div:not(:last-of-type) {
|
||||
margin-right: ${theme.spacing(2)};
|
||||
}
|
||||
'& > div:not(:last-of-type)': {
|
||||
marginRight: theme.spacing(2),
|
||||
},
|
||||
|
||||
& > div {
|
||||
width: 33.3%;
|
||||
}
|
||||
`,
|
||||
doubleRow: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
'& > div': {
|
||||
width: '33.3%',
|
||||
},
|
||||
}),
|
||||
doubleRow: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
||||
& > div:first-of-type {
|
||||
margin-bottom: ${theme.spacing(2)};
|
||||
}
|
||||
`,
|
||||
|
||||
loader: css`
|
||||
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;
|
||||
`,
|
||||
'& > div:first-of-type': {
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
}),
|
||||
notFound: css({
|
||||
fontSize: theme.typography.h6.fontSize,
|
||||
textAlign: 'center',
|
||||
height: '290px',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
38
public/app/features/admin/ServerStatsCard.tsx
Normal file
38
public/app/features/admin/ServerStatsCard.tsx
Normal 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),
|
||||
}),
|
||||
};
|
||||
};
|
Loading…
Reference in New Issue
Block a user