From aa09499343b5d369f70beee1a85d556edbc70e4a Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 10 Mar 2023 09:59:06 +0000 Subject: [PATCH] Backport 62658 to v9.4.x (#64545) * Navigation: Fix Home logo always going to `/login` (#62658) * only redirect to /login when anonymous access is disabled * only search for dashboards when not logged in if anon access is enabled * fix go logic * add unit tests (cherry picked from commit 3336327306606950192f93e512c9e4ef77a0961e) * remove file i accidentally left in :/ * import correct method --- packages/grafana-data/src/types/config.ts | 1 + packages/grafana-runtime/src/config.ts | 1 + pkg/api/frontendsettings.go | 1 + pkg/services/navtree/navtreeimpl/navtree.go | 10 +- .../components/AppChrome/TopSearchBar.tsx | 11 +- public/app/core/components/NavBar/NavBar.tsx | 9 +- .../actions/dashboardActions.test.ts | 166 ++++++++++++++++++ .../actions/dashboardActions.ts | 8 +- 8 files changed, 199 insertions(+), 8 deletions(-) create mode 100644 public/app/features/commandPalette/actions/dashboardActions.test.ts 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 []; }