diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts
index 2eddf5af5cd..5cc5d984938 100644
--- a/packages/grafana-data/src/types/config.ts
+++ b/packages/grafana-data/src/types/config.ts
@@ -212,6 +212,7 @@ export interface GrafanaConfig {
theme2: GrafanaTheme2;
pluginsToPreload: PreloadPlugin[];
featureToggles: FeatureToggles;
+ anonymousEnabled: boolean;
licenseInfo: LicenseInfo;
http2Enabled: boolean;
dateFormats?: SystemDateFormatSettings;
diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts
index c53b35d0fbb..297c0d4a8a6 100644
--- a/packages/grafana-runtime/src/config.ts
+++ b/packages/grafana-runtime/src/config.ts
@@ -79,6 +79,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
theme2: GrafanaTheme2;
pluginsToPreload: PreloadPlugin[] = [];
featureToggles: FeatureToggles = {};
+ anonymousEnabled = false;
licenseInfo: LicenseInfo = {} as LicenseInfo;
rendererAvailable = false;
dashboardPreviews: {
diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go
index 005ea871534..c102776f67d 100644
--- a/pkg/api/frontendsettings.go
+++ b/pkg/api/frontendsettings.go
@@ -175,6 +175,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *contextmodel.ReqContext) (map[st
"enabledFeatures": hs.License.EnabledFeatures(),
},
"featureToggles": hs.Features.GetEnabled(c.Req.Context()),
+ "anonymousEnabled": hs.Cfg.AnonymousEnabled,
"rendererAvailable": hs.RenderService.IsAvailable(c.Req.Context()),
"rendererVersion": hs.RenderService.Version(),
"secretsManagerPluginEnabled": secretsManagerPluginEnabled,
diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go
index c80ddb36406..e4ec7c9fcdd 100644
--- a/pkg/services/navtree/navtreeimpl/navtree.go
+++ b/pkg/services/navtree/navtreeimpl/navtree.go
@@ -214,10 +214,14 @@ func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, hasEditPerm bool, p
func (s *ServiceImpl) getHomeNode(c *contextmodel.ReqContext, prefs *pref.Preference) *navtree.NavLink {
homeUrl := s.cfg.AppSubURL + "/"
- homePage := s.cfg.HomePage
+ if !c.IsSignedIn && !s.cfg.AnonymousEnabled {
+ homeUrl = s.cfg.AppSubURL + "/login"
+ } else {
+ homePage := s.cfg.HomePage
- if prefs.HomeDashboardID == 0 && len(homePage) > 0 {
- homeUrl = homePage
+ if prefs.HomeDashboardID == 0 && len(homePage) > 0 {
+ homeUrl = homePage
+ }
}
homeNode := &navtree.NavLink{
diff --git a/public/app/core/components/AppChrome/TopSearchBar.tsx b/public/app/core/components/AppChrome/TopSearchBar.tsx
index 726171f4ddc..48e8695fe58 100644
--- a/public/app/core/components/AppChrome/TopSearchBar.tsx
+++ b/public/app/core/components/AppChrome/TopSearchBar.tsx
@@ -1,7 +1,8 @@
import { css } from '@emotion/css';
import React from 'react';
+import { useLocation } from 'react-router-dom';
-import { GrafanaTheme2 } from '@grafana/data';
+import { GrafanaTheme2, locationUtil } from '@grafana/data';
import { Dropdown, ToolbarButton, useStyles2 } from '@grafana/ui';
import { config } from 'app/core/config';
import { contextSrv } from 'app/core/core';
@@ -22,6 +23,7 @@ import { TOP_BAR_LEVEL_HEIGHT } from './types';
export function TopSearchBar() {
const styles = useStyles2(getStyles);
const navIndex = useSelector((state) => state.navIndex);
+ const location = useLocation();
const helpNode = navIndex['help'];
const profileNode = navIndex['profile'];
@@ -33,10 +35,15 @@ export function TopSearchBar() {
);
+ let homeUrl = config.appSubUrl || '/';
+ if (!config.bootData.user.isSignedIn && !config.anonymousEnabled) {
+ homeUrl = locationUtil.getUrlForPartial(location, { forceLogin: 'true' });
+ }
+
return (
-
+
diff --git a/public/app/core/components/NavBar/NavBar.tsx b/public/app/core/components/NavBar/NavBar.tsx
index d0a5e58ac86..ece5b77992a 100644
--- a/public/app/core/components/NavBar/NavBar.tsx
+++ b/public/app/core/components/NavBar/NavBar.tsx
@@ -5,7 +5,7 @@ import { cloneDeep } from 'lodash';
import React, { useState } from 'react';
import { useLocation } from 'react-router-dom';
-import { GrafanaTheme2, NavModelItem, NavSection } from '@grafana/data';
+import { GrafanaTheme2, locationUtil, NavModelItem, NavSection } from '@grafana/data';
import { config, locationSearchToObject, locationService, reportInteraction } from '@grafana/runtime';
import { useTheme2, CustomScrollbar, IconButton } from '@grafana/ui';
import { getKioskMode } from 'app/core/navigation/kiosk';
@@ -51,11 +51,16 @@ export const NavBar = React.memo(() => {
menuOpen
);
+ let homeUrl = config.appSubUrl || '/';
+ if (!config.bootData.user.isSignedIn && !config.anonymousEnabled) {
+ homeUrl = locationUtil.getUrlForPartial(location, { forceLogin: 'true' });
+ }
+
const homeItem: NavModelItem = enrichWithInteractionTracking(
{
id: 'home',
text: 'Home',
- url: config.bootData.user.isSignedIn ? config.appSubUrl || '/' : '/login',
+ url: homeUrl,
icon: 'grafana',
},
menuOpen
diff --git a/public/app/features/commandPalette/actions/dashboardActions.test.ts b/public/app/features/commandPalette/actions/dashboardActions.test.ts
new file mode 100644
index 00000000000..da670406546
--- /dev/null
+++ b/public/app/features/commandPalette/actions/dashboardActions.test.ts
@@ -0,0 +1,166 @@
+import { ArrayVector, DataFrame, DataFrameView, FieldType } from '@grafana/data';
+import { config } from '@grafana/runtime';
+import { ContextSrv, contextSrv } from 'app/core/services/context_srv';
+import impressionSrv from 'app/core/services/impression_srv';
+import { DashboardQueryResult, getGrafanaSearcher, QueryResponse } from 'app/features/search/service';
+
+import { getRecentDashboardActions, getDashboardSearchResultActions } from './dashboardActions';
+
+describe('dashboardActions', () => {
+ let grafanaSearcherSpy: jest.SpyInstance;
+ let mockContextSrv: jest.MockedObjectDeep;
+ const mockRecentDashboardUids = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];
+
+ const searchData: DataFrame = {
+ fields: [
+ { name: 'kind', type: FieldType.string, config: {}, values: new ArrayVector(['dashboard']) },
+ { name: 'name', type: FieldType.string, config: {}, values: new ArrayVector(['My dashboard 1']) },
+ { name: 'uid', type: FieldType.string, config: {}, values: new ArrayVector(['my-dashboard-1']) },
+ { name: 'url', type: FieldType.string, config: {}, values: new ArrayVector(['/my-dashboard-1']) },
+ { name: 'tags', type: FieldType.other, config: {}, values: new ArrayVector([['foo', 'bar']]) },
+ { name: 'location', type: FieldType.string, config: {}, values: new ArrayVector(['my-folder-1']) },
+ ],
+ meta: {
+ custom: {
+ locationInfo: {
+ 'my-folder-1': {
+ name: 'My folder 1',
+ kind: 'folder',
+ url: '/my-folder-1',
+ },
+ },
+ },
+ },
+ length: 1,
+ };
+
+ const mockSearchResult: QueryResponse = {
+ isItemLoaded: jest.fn(),
+ loadMoreItems: jest.fn(),
+ totalRows: searchData.length,
+ view: new DataFrameView(searchData),
+ };
+
+ beforeAll(() => {
+ mockContextSrv = jest.mocked(contextSrv);
+ grafanaSearcherSpy = jest.spyOn(getGrafanaSearcher(), 'search').mockResolvedValue(mockSearchResult);
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('getRecentDashboardActions', () => {
+ let impressionSrvSpy: jest.SpyInstance;
+
+ beforeAll(() => {
+ impressionSrvSpy = jest.spyOn(impressionSrv, 'getDashboardOpened').mockResolvedValue(mockRecentDashboardUids);
+ });
+
+ describe('when not signed in', () => {
+ beforeAll(() => {
+ mockContextSrv.user.isSignedIn = false;
+ });
+
+ it('returns an empty array, does not call the impressionSrv and does not call the search backend', async () => {
+ const results = await getRecentDashboardActions();
+ expect(impressionSrvSpy).not.toHaveBeenCalled();
+ expect(grafanaSearcherSpy).not.toHaveBeenCalled();
+ expect(results).toEqual([]);
+ });
+ });
+
+ describe('when signed in', () => {
+ beforeAll(() => {
+ mockContextSrv.user.isSignedIn = true;
+ });
+
+ it('calls the search backend with recent dashboards and returns an array of CommandPaletteActions', async () => {
+ const results = await getRecentDashboardActions();
+ expect(impressionSrvSpy).toHaveBeenCalled();
+ expect(grafanaSearcherSpy).toHaveBeenCalledWith({
+ kind: ['dashboard'],
+ limit: 5,
+ uid: ['1', '2', '3', '4', '5'],
+ });
+ expect(results).toEqual([
+ {
+ id: 'recent-dashboards/my-dashboard-1',
+ name: 'My dashboard 1',
+ priority: 5,
+ section: 'Recent dashboards',
+ url: '/my-dashboard-1',
+ },
+ ]);
+ });
+ });
+ });
+
+ describe('getDashboardSearchResultActions', () => {
+ it('returns an empty array if the search query is empty', async () => {
+ const searchQuery = '';
+ const results = await getDashboardSearchResultActions(searchQuery);
+ expect(grafanaSearcherSpy).not.toHaveBeenCalled();
+ expect(results).toEqual([]);
+ });
+
+ describe('when not signed in', () => {
+ beforeAll(() => {
+ mockContextSrv.user.isSignedIn = false;
+ });
+
+ it('returns an empty array if anonymous access is not enabled', async () => {
+ config.bootData.settings.anonymousEnabled = false;
+ const searchQuery = 'mySearchQuery';
+ const results = await getDashboardSearchResultActions(searchQuery);
+ expect(grafanaSearcherSpy).not.toHaveBeenCalled();
+ expect(results).toEqual([]);
+ });
+
+ it('calls the search backend and returns an array of CommandPaletteActions if anonymous access is enabled', async () => {
+ config.bootData.settings.anonymousEnabled = true;
+ const searchQuery = 'mySearchQuery';
+ const results = await getDashboardSearchResultActions(searchQuery);
+ expect(grafanaSearcherSpy).toHaveBeenCalledWith({
+ kind: ['dashboard'],
+ query: searchQuery,
+ limit: 100,
+ });
+ expect(results).toEqual([
+ {
+ id: 'go/dashboard/my-dashboard-1',
+ name: 'My dashboard 1',
+ priority: 1,
+ section: 'Dashboards',
+ url: '/my-dashboard-1',
+ },
+ ]);
+ });
+ });
+
+ describe('when signed in', () => {
+ beforeAll(() => {
+ mockContextSrv.user.isSignedIn = true;
+ });
+
+ it('calls the search backend with recent dashboards and returns an array of CommandPaletteActions', async () => {
+ const searchQuery = 'mySearchQuery';
+ const results = await getDashboardSearchResultActions(searchQuery);
+ expect(grafanaSearcherSpy).toHaveBeenCalledWith({
+ kind: ['dashboard'],
+ query: searchQuery,
+ limit: 100,
+ });
+ expect(results).toEqual([
+ {
+ id: 'go/dashboard/my-dashboard-1',
+ name: 'My dashboard 1',
+ priority: 1,
+ section: 'Dashboards',
+ url: '/my-dashboard-1',
+ },
+ ]);
+ });
+ });
+ });
+});
diff --git a/public/app/features/commandPalette/actions/dashboardActions.ts b/public/app/features/commandPalette/actions/dashboardActions.ts
index 76ff3e60bb1..a7f2abc3d0c 100644
--- a/public/app/features/commandPalette/actions/dashboardActions.ts
+++ b/public/app/features/commandPalette/actions/dashboardActions.ts
@@ -2,7 +2,9 @@ import debounce from 'debounce-promise';
import { useEffect, useState } from 'react';
import { locationUtil } from '@grafana/data';
+import { config } from '@grafana/runtime';
import { t } from 'app/core/internationalization';
+import { contextSrv } from 'app/core/services/context_srv';
import impressionSrv from 'app/core/services/impression_srv';
import { getGrafanaSearcher } from 'app/features/search/service';
@@ -15,6 +17,10 @@ const MAX_RECENT_DASHBOARDS = 5;
const debouncedDashboardSearch = debounce(getDashboardSearchResultActions, 200);
export async function getRecentDashboardActions(): Promise {
+ if (!contextSrv.user.isSignedIn) {
+ return [];
+ }
+
const recentUids = (await impressionSrv.getDashboardOpened()).slice(0, MAX_RECENT_DASHBOARDS);
const resultsDataFrame = await getGrafanaSearcher().search({
kind: ['dashboard'],
@@ -46,7 +52,7 @@ export async function getRecentDashboardActions(): Promise {
// Empty strings should not come through to here
- if (searchQuery.length === 0) {
+ if (searchQuery.length === 0 || (!contextSrv.user.isSignedIn && !config.bootData.settings.anonymousEnabled)) {
return [];
}