From ed2722e3e627090130d6b6d895fa50d2255bb601 Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Tue, 19 Dec 2023 12:01:55 +0100 Subject: [PATCH] Anonymous: Add device limits to stats (#79494) (#79650) * add device limits * feat: tabs the anon and session stats w. highlight (cherry picked from commit 57ca8fa36814055a22699582a2d7f7ef9fac25e0) --- conf/defaults.ini | 3 + packages/grafana-runtime/src/config.ts | 1 + pkg/api/admin.go | 4 +- .../app/features/admin/ServerStats.test.tsx | 6 +- public/app/features/admin/ServerStats.tsx | 48 +++++++++---- public/app/features/admin/ServerStatsCard.tsx | 71 +++++++++++++++++++ 6 files changed, 114 insertions(+), 19 deletions(-) create mode 100644 public/app/features/admin/ServerStatsCard.tsx diff --git a/conf/defaults.ini b/conf/defaults.ini index ad12367e493..e50f88cb8b3 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -587,6 +587,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 8ba941af6ab..7e296d77058 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -92,6 +92,7 @@ export class GrafanaBootConfig implements GrafanaConfig { theme2: GrafanaTheme2; featureToggles: FeatureToggles = {}; anonymousEnabled = false; + 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 3d0f97d7cd7..d0752d46204 100644 --- a/public/app/features/admin/ServerStats.tsx +++ b/public/app/features/admin/ServerStats.tsx @@ -2,13 +2,14 @@ import { css } from '@emotion/css'; import React, { useEffect, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { config } from '@grafana/runtime'; -import { CardContainer, LinkButton, useStyles2 } from '@grafana/ui'; +import { config, GrafanaBootConfig } from '@grafana/runtime'; +import { LinkButton, useStyles2, CardContainer } from '@grafana/ui'; import { AccessControlAction } from 'app/types'; import { contextSrv } from '../../core/services/context_srv'; import { Loader } from '../plugins/admin/components/Loader'; +import { ServerStatsCard } from './ServerStatsCard'; import { getServerStats, ServerStat } from './state/apis'; export const ServerStats = () => { @@ -71,23 +72,18 @@ export const ServerStats = () => { content={[{ name: 'Alerts', value: stats.alerts }]} footer={ - Alerts + Manage alerts } /> - { ); }; +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 new file mode 100644 index 00000000000..cff8500f2b8 --- /dev/null +++ b/public/app/features/admin/ServerStatsCard.tsx @@ -0,0 +1,71 @@ +import { css, cx } from '@emotion/css'; +import React from 'react'; +import Skeleton from 'react-loading-skeleton'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { CardContainer, useStyles2, Tooltip, Icon } from '@grafana/ui'; + +interface StatItem { + name: string; + value: string | number | undefined; + tooltip?: string; + highlight?: boolean; + indent?: boolean; +} + +export interface Props { + content: StatItem[]; + isLoading?: boolean; + footer?: JSX.Element | boolean; +} + +export const ServerStatsCard = ({ content, footer, isLoading }: Props) => { + const styles = useStyles2(getStyles); + return ( + + {content.map((item, index) => ( +
+ {item.name} + {item.tooltip && ( + + + + )} + {isLoading ? ( + + ) : ( + {item.value} + )} +
+ ))} + {footer &&
{footer}
} +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => { + return { + container: css({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + padding: theme.spacing(2), + }), + inner: css({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + }), + 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)}`, + }), + }; +};