* add device limits
* feat: tabs the anon and session stats w. highlight
(cherry picked from commit 57ca8fa368)
This commit is contained in:
@@ -587,6 +587,9 @@ org_role = Viewer
|
||||
# mask the Grafana version number for unauthenticated users
|
||||
hide_version = false
|
||||
|
||||
# number of devices in total
|
||||
device_limit =
|
||||
|
||||
#################################### GitHub Auth #########################
|
||||
[auth.github]
|
||||
name = GitHub
|
||||
|
||||
@@ -92,6 +92,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
||||
theme2: GrafanaTheme2;
|
||||
featureToggles: FeatureToggles = {};
|
||||
anonymousEnabled = false;
|
||||
anonymousDeviceLimit: number | undefined = undefined;
|
||||
licenseInfo: LicenseInfo = {} as LicenseInfo;
|
||||
rendererAvailable = false;
|
||||
rendererVersion = '';
|
||||
|
||||
+2
-2
@@ -64,8 +64,8 @@ func (hs *HTTPServer) AdminGetStats(c *contextmodel.ReqContext) response.Respons
|
||||
if err != nil {
|
||||
return response.Error(500, "Failed to get admin stats from database", err)
|
||||
}
|
||||
thirtyDays := 30 * 24 * time.Hour
|
||||
devicesCount, err := hs.anonService.CountDevices(c.Req.Context(), time.Now().Add(-thirtyDays), time.Now().Add(time.Minute))
|
||||
anonymousDeviceExpiration := 30 * 24 * time.Hour
|
||||
devicesCount, err := hs.anonService.CountDevices(c.Req.Context(), time.Now().Add(-anonymousDeviceExpiration), time.Now().Add(time.Minute))
|
||||
if err != nil {
|
||||
return response.Error(500, "Failed to get anon stats from database", err)
|
||||
}
|
||||
|
||||
@@ -46,15 +46,15 @@ describe('ServerStats', () => {
|
||||
expect(screen.getByText('Snapshots')).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Manage dashboards' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Manage data sources' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Alerts' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Manage alerts' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Manage users' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render page with anonymous stats', async () => {
|
||||
config.featureToggles.displayAnonymousStats = true;
|
||||
config.anonymousDeviceLimit = 10;
|
||||
render(<ServerStats />);
|
||||
expect(await screen.findByRole('heading', { name: /instance statistics/i })).toBeInTheDocument();
|
||||
expect(screen.getByText('Active anonymous devices in last 30 days')).toBeInTheDocument();
|
||||
expect(screen.getByText('Active anonymous users in last 30 days')).toBeInTheDocument();
|
||||
expect(screen.getByText('Active anonymous devices')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,13 +2,14 @@ import { css } from '@emotion/css';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { CardContainer, LinkButton, useStyles2 } from '@grafana/ui';
|
||||
import { config, GrafanaBootConfig } from '@grafana/runtime';
|
||||
import { LinkButton, useStyles2, CardContainer } 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 = () => {
|
||||
@@ -71,23 +72,18 @@ export const ServerStats = () => {
|
||||
content={[{ name: 'Alerts', value: stats.alerts }]}
|
||||
footer={
|
||||
<LinkButton href={'/alerting/list'} variant={'secondary'}>
|
||||
Alerts
|
||||
Manage alerts
|
||||
</LinkButton>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<StatCard
|
||||
<ServerStatsCard
|
||||
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: '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: 'Organisations', value: stats?.orgs },
|
||||
{ name: 'Users total', value: stats?.users },
|
||||
{ name: 'Active sessions', value: stats?.activeSessions },
|
||||
{ name: 'Active users in last 30 days', value: stats?.activeUsers },
|
||||
...getAnonymousStatsContent(stats, config),
|
||||
]}
|
||||
footer={
|
||||
hasAccessToAdminUsers && (
|
||||
@@ -105,6 +101,30 @@ export const ServerStats = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const getAnonymousStatsContent = (stats: ServerStat | null, config: GrafanaBootConfig) => {
|
||||
if (!config.featureToggles.displayAnonymousStats || !stats?.activeDevices) {
|
||||
return [];
|
||||
}
|
||||
if (!config.anonymousDeviceLimit) {
|
||||
return [
|
||||
{
|
||||
name: 'Active anonymous devices',
|
||||
value: `${stats.activeDevices}`,
|
||||
tooltip: 'Detected devices that are not logged in, in last 30 days.',
|
||||
},
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
{
|
||||
name: 'Active anonymous devices',
|
||||
value: `${stats.activeDevices} / ${config.anonymousDeviceLimit}`,
|
||||
tooltip: 'Detected devices that are not logged in, in last 30 days.',
|
||||
highlight: stats.activeDevices > config.anonymousDeviceLimit,
|
||||
},
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
title: css`
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React from 'react';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { CardContainer, useStyles2, Tooltip, Icon } from '@grafana/ui';
|
||||
|
||||
interface StatItem {
|
||||
name: string;
|
||||
value: string | number | undefined;
|
||||
tooltip?: string;
|
||||
highlight?: boolean;
|
||||
indent?: boolean;
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
content: StatItem[];
|
||||
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) => (
|
||||
<div key={index} className={styles.inner}>
|
||||
<span className={cx({ [styles.indent]: !!item.indent })}>{item.name}</span>
|
||||
{item.tooltip && (
|
||||
<Tooltip content={String(item.tooltip)} placement="auto-start">
|
||||
<Icon name="info-circle" className={styles.tooltip} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{isLoading ? (
|
||||
<Skeleton width={60} />
|
||||
) : (
|
||||
<span className={item.highlight ? styles.highlight : ''}>{item.value}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{footer && <div>{footer}</div>}
|
||||
</CardContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
container: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(2),
|
||||
padding: theme.spacing(2),
|
||||
}),
|
||||
inner: css({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}),
|
||||
indent: css({
|
||||
marginLeft: theme.spacing(2),
|
||||
}),
|
||||
tooltip: css({
|
||||
color: theme.colors.secondary.text,
|
||||
}),
|
||||
highlight: css({
|
||||
color: theme.colors.warning.text,
|
||||
padding: `${theme.spacing(0.5)} ${theme.spacing(1)}`,
|
||||
marginRight: `-${theme.spacing(1)}`,
|
||||
}),
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user