mirror of
https://github.com/grafana/grafana.git
synced 2025-01-13 09:32:12 -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
|
# 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
|
||||||
|
@ -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 = '';
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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({
|
||||||
|
@ -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)}`,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user