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)}`,
+ }),
};
};