mirror of https://github.com/grafana/grafana
TopNav: Plugin page layouts / information architecture (#53174)
* Change nav structure when topnav is enable to do initial tests with new information architecture * Support for nested sections * Updated * sentance case * Progress on plugin challange * Rewrite to functional component * Progress * Updates * Progress * Progress on things * missing file * Fixing issue with runtime, need to use setter way to set component exposed via runtime * Move PageLayoutType to grafana/data * Fixing breadcrumb issue, adding more tests * reverted backend change * fix recursive issue with cleanuppull/54622/head^2
parent
a423c7f22e
commit
11de1dfe40
@ -0,0 +1,25 @@ |
||||
import React from 'react'; |
||||
|
||||
import { NavModelItem, PageLayoutType } from '@grafana/data'; |
||||
|
||||
export interface PluginPageProps { |
||||
pageNav?: NavModelItem; |
||||
children: React.ReactNode; |
||||
layout?: PageLayoutType; |
||||
} |
||||
|
||||
export type PluginPageType = React.ComponentType<PluginPageProps>; |
||||
|
||||
export let PluginPage: PluginPageType = ({ children }) => { |
||||
return <div>{children}</div>; |
||||
}; |
||||
|
||||
/** |
||||
* Used to bootstrap the PluginPage during application start |
||||
* is exposed via runtime. |
||||
* |
||||
* @internal |
||||
*/ |
||||
export function setPluginPage(component: PluginPageType) { |
||||
PluginPage = component; |
||||
} |
@ -0,0 +1,16 @@ |
||||
import React, { useContext } from 'react'; |
||||
|
||||
import { PluginPageProps } from '@grafana/runtime'; |
||||
import { PluginPageContext } from 'app/features/plugins/components/PluginPageContext'; |
||||
|
||||
import { Page } from '../Page/Page'; |
||||
|
||||
export function PluginPage({ children, pageNav, layout }: PluginPageProps) { |
||||
const context = useContext(PluginPageContext); |
||||
|
||||
return ( |
||||
<Page navModel={context.sectionNav} pageNav={pageNav} layout={layout}> |
||||
<Page.Contents>{children}</Page.Contents> |
||||
</Page> |
||||
); |
||||
} |
@ -1,16 +1,16 @@ |
||||
import { useEffect, useRef } from 'react'; |
||||
import { useDispatch } from 'react-redux'; |
||||
|
||||
import { cleanUpAction, StateSelector } from '../actions/cleanUp'; |
||||
import { cleanUpAction, CleanUpAction } from '../actions/cleanUp'; |
||||
|
||||
export function useCleanup<T>(stateSelector: StateSelector<T>) { |
||||
export function useCleanup(cleanupAction: CleanUpAction) { |
||||
const dispatch = useDispatch(); |
||||
//bit of a hack to unburden user from having to wrap stateSelcetor in a useCallback. Otherwise cleanup would happen on every render
|
||||
const selectorRef = useRef(stateSelector); |
||||
selectorRef.current = stateSelector; |
||||
const selectorRef = useRef(cleanupAction); |
||||
selectorRef.current = cleanupAction; |
||||
useEffect(() => { |
||||
return () => { |
||||
dispatch(cleanUpAction({ stateSelector: selectorRef.current })); |
||||
dispatch(cleanUpAction({ cleanupAction: selectorRef.current })); |
||||
}; |
||||
}, [dispatch]); |
||||
} |
||||
|
@ -0,0 +1,26 @@ |
||||
import React from 'react'; |
||||
|
||||
import { NavModel } from '@grafana/data'; |
||||
|
||||
export interface PluginPageContextType { |
||||
sectionNav: NavModel; |
||||
} |
||||
|
||||
export const PluginPageContext = React.createContext(getInitialPluginPageContext()); |
||||
|
||||
PluginPageContext.displayName = 'PluginPageContext'; |
||||
|
||||
function getInitialPluginPageContext(): PluginPageContextType { |
||||
return { |
||||
sectionNav: { |
||||
main: { text: 'Plugin page' }, |
||||
node: { text: 'Plugin page' }, |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
export function buildPluginPageContext(sectionNav: NavModel | null): PluginPageContextType { |
||||
return { |
||||
sectionNav: sectionNav ?? getInitialPluginPageContext().sectionNav, |
||||
}; |
||||
} |
@ -0,0 +1,52 @@ |
||||
import { Location as HistoryLocation } from 'history'; |
||||
|
||||
import { config } from '@grafana/runtime'; |
||||
|
||||
import { buildPluginSectionNav } from './utils'; |
||||
|
||||
describe('buildPluginSectionNav', () => { |
||||
const pluginNav = { main: { text: 'Plugin nav' }, node: { text: 'Plugin nav' } }; |
||||
const appsSection = { |
||||
text: 'apps', |
||||
id: 'apps', |
||||
children: [ |
||||
{ |
||||
text: 'App1', |
||||
children: [ |
||||
{ |
||||
text: 'page1', |
||||
url: '/a/plugin1/page1', |
||||
}, |
||||
{ |
||||
text: 'page2', |
||||
url: '/a/plugin1/page2', |
||||
}, |
||||
], |
||||
}, |
||||
], |
||||
}; |
||||
const navIndex = { apps: appsSection }; |
||||
|
||||
it('Should return pluginNav if topnav is disabled', () => { |
||||
config.featureToggles.topnav = false; |
||||
const result = buildPluginSectionNav({} as HistoryLocation, pluginNav, {}); |
||||
expect(result).toBe(pluginNav); |
||||
}); |
||||
|
||||
it('Should return return section nav if topnav is enabled', () => { |
||||
config.featureToggles.topnav = true; |
||||
const result = buildPluginSectionNav({} as HistoryLocation, pluginNav, navIndex); |
||||
expect(result?.main.text).toBe('apps'); |
||||
}); |
||||
|
||||
it('Should set active page', () => { |
||||
config.featureToggles.topnav = true; |
||||
const result = buildPluginSectionNav( |
||||
{ pathname: '/a/plugin1/page2', search: '' } as HistoryLocation, |
||||
null, |
||||
navIndex |
||||
); |
||||
expect(result?.main.children![0].children![1].active).toBe(true); |
||||
expect(result?.node.text).toBe('page2'); |
||||
}); |
||||
}); |
Loading…
Reference in new issue