mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
cd6d6d1daf
commit
3336327306
@ -200,6 +200,7 @@ export interface GrafanaConfig {
|
||||
/** @deprecated Use `theme2` instead. */
|
||||
theme: GrafanaTheme;
|
||||
theme2: GrafanaTheme2;
|
||||
anonymousEnabled: boolean;
|
||||
featureToggles: FeatureToggles;
|
||||
licenseInfo: LicenseInfo;
|
||||
http2Enabled: boolean;
|
||||
|
@ -85,6 +85,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
||||
theme: GrafanaTheme;
|
||||
theme2: GrafanaTheme2;
|
||||
featureToggles: FeatureToggles = {};
|
||||
anonymousEnabled = false;
|
||||
licenseInfo: LicenseInfo = {} as LicenseInfo;
|
||||
rendererAvailable = false;
|
||||
dashboardPreviews: {
|
||||
|
@ -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"`
|
||||
|
@ -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,
|
||||
|
@ -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{
|
||||
|
@ -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 />
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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 [];
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user