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
hide_version = false
# number of devices in total
device_limit =
#################################### GitHub Auth #########################
[auth.github]
name = GitHub

View File

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

View File

@ -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)
}

View File

@ -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();
});
});

View File

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

View File

@ -1,12 +1,20 @@
import { css } from '@emotion/css';
import { css, cx } 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';
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 {
content: Array<Record<string, string | number | undefined>>;
content: StatItem[];
isLoading?: boolean;
footer?: JSX.Element | boolean;
}
@ -14,15 +22,26 @@ export interface Props {
export const ServerStatsCard = ({ content, footer, isLoading }: Props) => {
const styles = useStyles2(getStyles);
return (
<CardContainer className={styles.container} disableHover>
<Card className={styles.container}>
{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 alignItems={'center'}>
<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>
))}
{footer && <div>{footer}</div>}
</CardContainer>
</Card>
);
};
@ -34,5 +53,16 @@ const getStyles = (theme: GrafanaTheme2) => {
gap: 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)}`,
}),
};
};