diff --git a/public/app/core/components/AppChrome/AppChromeService.test.tsx b/public/app/core/components/AppChrome/AppChromeService.test.tsx index ea04bddf3bc..050d1b25eab 100644 --- a/public/app/core/components/AppChrome/AppChromeService.test.tsx +++ b/public/app/core/components/AppChrome/AppChromeService.test.tsx @@ -7,4 +7,33 @@ describe('AppChromeService', () => { chromeService.onToggleKioskMode(); expect(chromeService.state.getValue().chromeless).toBe(true); }); + + it('Ignore state updates when sectionNav and pageNav have new instance but same text, url or active child', () => { + const chromeService = new AppChromeService(); + let stateChanges = 0; + + chromeService.state.subscribe(() => stateChanges++); + chromeService.update({ sectionNav: { text: 'hello' }, pageNav: { text: 'test', url: 'A' } }); + chromeService.update({ sectionNav: { text: 'hello' }, pageNav: { text: 'test', url: 'A' } }); + + expect(stateChanges).toBe(2); + + // if url change we should update + chromeService.update({ sectionNav: { text: 'hello' }, pageNav: { text: 'test', url: 'new/url' } }); + expect(stateChanges).toBe(3); + + // if active child changed should update state + chromeService.update({ + sectionNav: { text: 'hello' }, + pageNav: { text: 'test', url: 'A', children: [{ text: 'child', active: true }] }, + }); + expect(stateChanges).toBe(4); + + // If active child is the same we should not update state + chromeService.update({ + sectionNav: { text: 'hello' }, + pageNav: { text: 'test', url: 'A', children: [{ text: 'child', active: true }] }, + }); + expect(stateChanges).toBe(4); + }); }); diff --git a/public/app/core/components/AppChrome/AppChromeService.tsx b/public/app/core/components/AppChrome/AppChromeService.tsx index 10a1a28dcb6..bc919918d99 100644 --- a/public/app/core/components/AppChrome/AppChromeService.tsx +++ b/public/app/core/components/AppChrome/AppChromeService.tsx @@ -24,7 +24,7 @@ export interface AppChromeState { export class AppChromeService { searchBarStorageKey = 'SearchBar_Hidden'; private currentRoute?: RouteDescriptor; - private routeChangeHandled?: boolean; + private routeChangeHandled = true; readonly state = new BehaviorSubject({ chromeless: true, // start out hidden to not flash it on pages without chrome @@ -60,11 +60,29 @@ export class AppChromeService { // KioskMode overrides chromeless state newState.chromeless = newState.kioskMode === KioskMode.Full || this.currentRoute?.chromeless; - if (!isShallowEqual(current, newState)) { + if (!this.ignoreStateUpdate(newState, current)) { this.state.next(newState); } } + ignoreStateUpdate(newState: AppChromeState, current: AppChromeState) { + if (isShallowEqual(newState, current)) { + return true; + } + + // Some updates can have new instance of sectionNav or pageNav but with same values + if (newState.sectionNav !== current.sectionNav || newState.pageNav !== current.pageNav) { + if ( + navItemsAreTheSame(newState.sectionNav, current.sectionNav) && + navItemsAreTheSame(newState.pageNav, current.pageNav) + ) { + return true; + } + } + + return false; + } + useState() { // eslint-disable-next-line react-hooks/rules-of-hooks return useObservable(this.state, this.state.getValue()); @@ -134,3 +152,17 @@ export class AppChromeService { return null; } } + +/** + * Checks if text, url and active child url are the same + **/ +function navItemsAreTheSame(a: NavModelItem | undefined, b: NavModelItem | undefined) { + if (a === b) { + return true; + } + + const aActiveChild = a?.children?.find((child) => child.active); + const bActiveChild = b?.children?.find((child) => child.active); + + return a?.text === b?.text && a?.url === b?.url && aActiveChild?.url === bActiveChild?.url; +}