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": [
|
"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"],
|
||||||
|
@ -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;
|
|
||||||
`,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
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