mirror of
https://github.com/grafana/grafana.git
synced 2024-12-25 16:31:28 -06:00
Anonymous: Add device limits to stats (#79494)
* add device limits * feat: tabs the anon and session stats w. highlight
This commit is contained in:
parent
86ac431097
commit
57ca8fa368
@ -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
|
||||
|
@ -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 = '';
|
||||
|
@ -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,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({
|
||||
|
@ -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)}`,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user