Anonymous: Add device limits to stats (#79494)

* add device limits

* feat: tabs the anon and session stats w. highlight
This commit is contained in:
Eric Leijonmarck 2023-12-18 09:32:57 +01:00 committed by GitHub
parent 86ac431097
commit 57ca8fa368
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 74 additions and 22 deletions

View File

@ -591,6 +591,9 @@ org_role = Viewer
# mask the Grafana version number for unauthenticated users # mask the Grafana version number for unauthenticated users
hide_version = false hide_version = false
# number of devices in total
device_limit =
#################################### GitHub Auth ######################### #################################### GitHub Auth #########################
[auth.github] [auth.github]
name = GitHub name = GitHub

View File

@ -94,7 +94,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
theme2: GrafanaTheme2; theme2: GrafanaTheme2;
featureToggles: FeatureToggles = {}; featureToggles: FeatureToggles = {};
anonymousEnabled = false; anonymousEnabled = false;
anonymousDeviceLimit = undefined; anonymousDeviceLimit: number | undefined = undefined;
licenseInfo: LicenseInfo = {} as LicenseInfo; licenseInfo: LicenseInfo = {} as LicenseInfo;
rendererAvailable = false; rendererAvailable = false;
rendererVersion = ''; rendererVersion = '';

View File

@ -64,8 +64,8 @@ func (hs *HTTPServer) AdminGetStats(c *contextmodel.ReqContext) response.Respons
if err != nil { if err != nil {
return response.Error(500, "Failed to get admin stats from database", err) return response.Error(500, "Failed to get admin stats from database", err)
} }
thirtyDays := 30 * 24 * time.Hour anonymousDeviceExpiration := 30 * 24 * time.Hour
devicesCount, err := hs.anonService.CountDevices(c.Req.Context(), time.Now().Add(-thirtyDays), time.Now().Add(time.Minute)) devicesCount, err := hs.anonService.CountDevices(c.Req.Context(), time.Now().Add(-anonymousDeviceExpiration), time.Now().Add(time.Minute))
if err != nil { if err != nil {
return response.Error(500, "Failed to get anon stats from database", err) return response.Error(500, "Failed to get anon stats from database", err)
} }

View File

@ -46,15 +46,15 @@ describe('ServerStats', () => {
expect(screen.getByText('Snapshots')).toBeInTheDocument(); expect(screen.getByText('Snapshots')).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'Manage dashboards' })).toBeInTheDocument(); expect(screen.getByRole('link', { name: 'Manage dashboards' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'Manage data sources' })).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(); expect(screen.getByRole('link', { name: 'Manage users' })).toBeInTheDocument();
}); });
it('Should render page with anonymous stats', async () => { it('Should render page with anonymous stats', async () => {
config.featureToggles.displayAnonymousStats = true; config.featureToggles.displayAnonymousStats = true;
config.anonymousDeviceLimit = 10;
render(<ServerStats />); render(<ServerStats />);
expect(await screen.findByRole('heading', { name: /instance statistics/i })).toBeInTheDocument(); 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 devices')).toBeInTheDocument();
expect(screen.getByText('Active anonymous users in last 30 days')).toBeInTheDocument();
}); });
}); });

View File

@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime'; import { config, GrafanaBootConfig } from '@grafana/runtime';
import { LinkButton, useStyles2 } from '@grafana/ui'; import { LinkButton, useStyles2 } from '@grafana/ui';
import { AccessControlAction } from 'app/types'; import { AccessControlAction } from 'app/types';
@ -71,7 +71,7 @@ export const ServerStats = () => {
content={[{ name: 'Alerts', value: stats?.alerts }]} content={[{ name: 'Alerts', value: stats?.alerts }]}
footer={ footer={
<LinkButton href={'/alerting/list'} variant={'secondary'}> <LinkButton href={'/alerting/list'} variant={'secondary'}>
Alerts Manage alerts
</LinkButton> </LinkButton>
} }
/> />
@ -81,14 +81,9 @@ export const ServerStats = () => {
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 },
...(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: 'Active sessions', value: stats?.activeSessions },
{ name: 'Active users in last 30 days', value: stats?.activeUsers },
...getAnonymousStatsContent(stats, config),
]} ]}
footer={ footer={
hasAccessToAdminUsers && ( hasAccessToAdminUsers && (
@ -104,6 +99,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) => { const getStyles = (theme: GrafanaTheme2) => {
return { return {
title: css({ title: css({

View File

@ -1,12 +1,20 @@
import { css } from '@emotion/css'; import { css, cx } from '@emotion/css';
import React from 'react'; import React from 'react';
import Skeleton from 'react-loading-skeleton'; import Skeleton from 'react-loading-skeleton';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { CardContainer, Stack, useStyles2 } from '@grafana/ui'; import { Card, Stack, useStyles2, Tooltip, Icon } from '@grafana/ui';
interface StatItem {
name: string;
value: string | number | undefined;
tooltip?: string;
highlight?: boolean;
indent?: boolean;
}
export interface Props { export interface Props {
content: Array<Record<string, string | number | undefined>>; content: StatItem[];
isLoading?: boolean; isLoading?: boolean;
footer?: JSX.Element | boolean; footer?: JSX.Element | boolean;
} }
@ -14,15 +22,26 @@ export interface Props {
export const ServerStatsCard = ({ content, footer, isLoading }: Props) => { export const ServerStatsCard = ({ content, footer, isLoading }: Props) => {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
return ( return (
<CardContainer className={styles.container} disableHover> <Card className={styles.container}>
{content.map((item, index) => ( {content.map((item, index) => (
<Stack key={index} justifyContent="space-between" alignItems="center"> <Stack key={index} justifyContent="space-between" alignItems="center">
<span>{item.name}</span> <Stack alignItems={'center'}>
{isLoading ? <Skeleton width={60} /> : <span>{item.value}</span>} <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>
)}
</Stack>
{isLoading ? (
<Skeleton width={60} />
) : (
<span className={item.highlight ? styles.highlight : ''}>{item.value}</span>
)}
</Stack> </Stack>
))} ))}
{footer && <div>{footer}</div>} {footer && <div>{footer}</div>}
</CardContainer> </Card>
); );
}; };
@ -34,5 +53,16 @@ const getStyles = (theme: GrafanaTheme2) => {
gap: theme.spacing(2), gap: theme.spacing(2),
padding: theme.spacing(2), padding: theme.spacing(2),
}), }),
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)}`,
}),
}; };
}; };