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
This commit is contained in:
Ashley Harrison 2023-03-09 16:42:45 +00:00 committed by GitHub
parent cd6d6d1daf
commit 3336327306
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 203 additions and 8 deletions

View File

@ -200,6 +200,7 @@ export interface GrafanaConfig {
/** @deprecated Use `theme2` instead. */
theme: GrafanaTheme;
theme2: GrafanaTheme2;
anonymousEnabled: boolean;
featureToggles: FeatureToggles;
licenseInfo: LicenseInfo;
http2Enabled: boolean;

View File

@ -85,6 +85,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
theme: GrafanaTheme;
theme2: GrafanaTheme2;
featureToggles: FeatureToggles = {};
anonymousEnabled = false;
licenseInfo: LicenseInfo = {} as LicenseInfo;
rendererAvailable = false;
dashboardPreviews: {

View File

@ -181,6 +181,7 @@ type FrontendSettingsDTO struct {
LicenseInfo FrontendSettingsLicenseInfoDTO `json:"licenseInfo"`
FeatureToggles map[string]bool `json:"featureToggles"`
AnonymousEnabled bool `json:"anonymousEnabled"`
RendererAvailable bool `json:"rendererAvailable"`
RendererVersion string `json:"rendererVersion"`
SecretsManagerPluginEnabled bool `json:"secretsManagerPluginEnabled"`

View File

@ -174,6 +174,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
},
FeatureToggles: hs.Features.GetEnabled(c.Req.Context()),
AnonymousEnabled: hs.Cfg.AnonymousEnabled,
RendererAvailable: hs.RenderService.IsAvailable(c.Req.Context()),
RendererVersion: hs.RenderService.Version(),
SecretsManagerPluginEnabled: secretsManagerPluginEnabled,

View File

@ -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{

View File

@ -1,8 +1,10 @@
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';
import { useSelector } from 'app/types';
@ -20,14 +22,20 @@ 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'];
let homeUrl = config.appSubUrl || '/';
if (!config.bootData.user.isSignedIn && !config.anonymousEnabled) {
homeUrl = locationUtil.getUrlForPartial(location, { forceLogin: 'true' });
}
return (
<div className={styles.layout}>
<TopSearchBarSection>
<a className={styles.logo} href="/" title="Go to home">
<a className={styles.logo} href={homeUrl} title="Go to home">
<Branding.MenuLogo className={styles.img} />
</a>
<OrganizationSwitcher />

View File

@ -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

View File

@ -0,0 +1,168 @@
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, getSearchResultActions } from './dashboardActions';
describe('dashboardActions', () => {
let grafanaSearcherSpy: jest.SpyInstance;
let mockContextSrv: jest.MockedObjectDeep<ContextSrv>;
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<DashboardQueryResult>(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('getSearchResultActions', () => {
it('returns an empty array if the search query is empty', async () => {
const searchQuery = '';
const results = await getSearchResultActions(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 getSearchResultActions(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 getSearchResultActions(searchQuery);
expect(grafanaSearcherSpy).toHaveBeenCalledWith({
kind: ['dashboard', 'folder'],
query: searchQuery,
limit: 100,
});
expect(results).toEqual([
{
id: 'go/dashboard/my-dashboard-1',
name: 'My dashboard 1',
priority: 1,
section: 'Dashboards',
subtitle: 'My folder 1',
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 getSearchResultActions(searchQuery);
expect(grafanaSearcherSpy).toHaveBeenCalledWith({
kind: ['dashboard', 'folder'],
query: searchQuery,
limit: 100,
});
expect(results).toEqual([
{
id: 'go/dashboard/my-dashboard-1',
name: 'My dashboard 1',
priority: 1,
section: 'Dashboards',
subtitle: 'My folder 1',
url: '/my-dashboard-1',
},
]);
});
});
});
});

View File

@ -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 debouncedSearch = debounce(getSearchResultActions, 200);
export async function getRecentDashboardActions(): Promise<CommandPaletteAction[]> {
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<CommandPaletteAction[
export async function getSearchResultActions(searchQuery: string): Promise<CommandPaletteAction[]> {
// Empty strings should not come through to here
if (searchQuery.length === 0) {
if (searchQuery.length === 0 || (!contextSrv.user.isSignedIn && !config.bootData.settings.anonymousEnabled)) {
return [];
}