diff --git a/conf/defaults.ini b/conf/defaults.ini index 6bb25d0f65a..121001ca2e6 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -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 diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index deedcfeee59..c0f8fad4b7f 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -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 = ''; diff --git a/pkg/api/admin.go b/pkg/api/admin.go index d88818325e3..d298445eb8b 100644 --- a/pkg/api/admin.go +++ b/pkg/api/admin.go @@ -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) } diff --git a/public/app/features/admin/ServerStats.test.tsx b/public/app/features/admin/ServerStats.test.tsx index 9d26f6f264a..4c1d85d189c 100644 --- a/public/app/features/admin/ServerStats.test.tsx +++ b/public/app/features/admin/ServerStats.test.tsx @@ -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(); 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(); }); }); diff --git a/public/app/features/admin/ServerStats.tsx b/public/app/features/admin/ServerStats.tsx index d8728426600..acb1ae19644 100644 --- a/public/app/features/admin/ServerStats.tsx +++ b/public/app/features/admin/ServerStats.tsx @@ -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={ - Alerts + Manage alerts } /> @@ -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({ diff --git a/public/app/features/admin/ServerStatsCard.tsx b/public/app/features/admin/ServerStatsCard.tsx index 6bb891720e9..66fdabde6d8 100644 --- a/public/app/features/admin/ServerStatsCard.tsx +++ b/public/app/features/admin/ServerStatsCard.tsx @@ -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>; + 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 ( - + {content.map((item, index) => ( - {item.name} - {isLoading ? : {item.value}} + + {item.name} + {item.tooltip && ( + + + + )} + + {isLoading ? ( + + ) : ( + {item.value} + )} ))} {footer &&
{footer}
} -
+ ); }; @@ -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)}`, + }), }; };