mirror of
https://github.com/grafana/grafana.git
synced 2025-07-30 08:12:53 +08:00

* NewIA: Plugin nav config * progress * Progress * Things are working * Add monitoring node * Add alerts and incidents * added experiment with standalone page * Refactoring by adding a type for navtree root * First test working * More tests * more tests * Progress on richer config and sorting * Sort weight working * Path config * Improving logic for not including admin or cfg nodes, making it the last step so that enterprise can add admin nodes without having to worry about the section not existing * fixed index routes * removed file * Fixes * Fixing tests * Fixing more tests and adding support for weight config * Updates * Remove unused fake * More fixes * Minor tweak * Minor fix * Can now control position using sortweight even when existing items have no sortweight * Added tests for frontend standalone page logic * more tests * Remove unused fake and fixed lint issue * Moving reading settings to navtree impl package * remove nav_id setting prefix * Remove old test file * Fix trailing newline * Fixed bug with adding nil node * fixing lint issue * remove some code we have to rethink * move read settings to PrivideService and switch to util.SplitString
178 lines
5.3 KiB
TypeScript
178 lines
5.3 KiB
TypeScript
// Libraries
|
|
import { AnyAction, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|
import React, { useCallback, useEffect, useMemo, useReducer } from 'react';
|
|
import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal';
|
|
import { createSelector } from 'reselect';
|
|
|
|
import { AppEvents, AppPlugin, AppPluginMeta, KeyValue, NavModel, PluginType } from '@grafana/data';
|
|
import { config } from '@grafana/runtime';
|
|
import { getNotFoundNav, getWarningNav, getExceptionNav } from 'app/angular/services/nav_model_srv';
|
|
import { Page } from 'app/core/components/Page/Page';
|
|
import { PageProps } from 'app/core/components/Page/types';
|
|
import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
|
import { appEvents } from 'app/core/core';
|
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
|
import { StoreState, useSelector } from 'app/types';
|
|
|
|
import { getPluginSettings } from '../pluginSettings';
|
|
import { importAppPlugin } from '../plugin_loader';
|
|
import { buildPluginSectionNav } from '../utils';
|
|
|
|
import { buildPluginPageContext, PluginPageContext } from './PluginPageContext';
|
|
|
|
interface RouteParams {
|
|
pluginId: string;
|
|
}
|
|
|
|
interface Props extends GrafanaRouteComponentProps<RouteParams> {}
|
|
|
|
interface State {
|
|
loading: boolean;
|
|
plugin?: AppPlugin | null;
|
|
pluginNav: NavModel | null;
|
|
}
|
|
|
|
const initialState: State = { loading: true, pluginNav: null, plugin: null };
|
|
|
|
export function AppRootPage({ match, queryParams, location }: Props) {
|
|
const [state, dispatch] = useReducer(stateSlice.reducer, initialState);
|
|
const portalNode = useMemo(() => createHtmlPortalNode(), []);
|
|
const { plugin, loading, pluginNav } = state;
|
|
const sectionNav = useSelector(
|
|
createSelector(getNavIndex, (navIndex) =>
|
|
buildPluginSectionNav(location, pluginNav, navIndex, match.params.pluginId)
|
|
)
|
|
);
|
|
const context = useMemo(() => buildPluginPageContext(sectionNav), [sectionNav]);
|
|
|
|
useEffect(() => {
|
|
loadAppPlugin(match.params.pluginId, dispatch);
|
|
}, [match.params.pluginId]);
|
|
|
|
const onNavChanged = useCallback(
|
|
(newPluginNav: NavModel) => dispatch(stateSlice.actions.changeNav(newPluginNav)),
|
|
[]
|
|
);
|
|
|
|
if (!plugin || match.params.pluginId !== plugin.meta.id) {
|
|
return <Page {...getLoadingPageProps(sectionNav)}>{loading && <PageLoader />}</Page>;
|
|
}
|
|
|
|
if (!plugin.root) {
|
|
return (
|
|
<Page navModel={sectionNav ?? getWarningNav('Plugin load error')}>
|
|
<div>No root app page component found</div>;
|
|
</Page>
|
|
);
|
|
}
|
|
|
|
const pluginRoot = plugin.root && (
|
|
<plugin.root
|
|
meta={plugin.meta}
|
|
basename={match.url}
|
|
onNavChanged={onNavChanged}
|
|
query={queryParams as KeyValue}
|
|
path={location.pathname}
|
|
/>
|
|
);
|
|
|
|
if (config.featureToggles.topnav && !pluginNav) {
|
|
return <PluginPageContext.Provider value={context}>{pluginRoot}</PluginPageContext.Provider>;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<InPortal node={portalNode}>{pluginRoot}</InPortal>
|
|
{sectionNav ? (
|
|
<Page navModel={sectionNav} pageNav={pluginNav?.node}>
|
|
<Page.Contents isLoading={loading}>
|
|
<OutPortal node={portalNode} />
|
|
</Page.Contents>
|
|
</Page>
|
|
) : (
|
|
<Page>
|
|
<OutPortal node={portalNode} />
|
|
</Page>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
const stateSlice = createSlice({
|
|
name: 'prom-builder-container',
|
|
initialState: initialState,
|
|
reducers: {
|
|
setState: (state, action: PayloadAction<Partial<State>>) => {
|
|
Object.assign(state, action.payload);
|
|
},
|
|
changeNav: (state, action: PayloadAction<NavModel>) => {
|
|
let pluginNav = action.payload;
|
|
// This is to hide the double breadcrumbs the old nav model can cause
|
|
if (pluginNav && pluginNav.node.children) {
|
|
pluginNav = {
|
|
...pluginNav,
|
|
node: {
|
|
...pluginNav.main,
|
|
hideFromBreadcrumbs: true,
|
|
},
|
|
};
|
|
}
|
|
state.pluginNav = pluginNav;
|
|
},
|
|
},
|
|
});
|
|
|
|
function getLoadingPageProps(sectionNav: NavModel | null): Partial<PageProps> {
|
|
if (config.featureToggles.topnav && sectionNav) {
|
|
return { navModel: sectionNav };
|
|
}
|
|
|
|
const loading = { text: 'Loading plugin' };
|
|
|
|
return {
|
|
navModel: { main: loading, node: loading },
|
|
};
|
|
}
|
|
|
|
async function loadAppPlugin(pluginId: string, dispatch: React.Dispatch<AnyAction>) {
|
|
try {
|
|
const app = await getPluginSettings(pluginId).then((info) => {
|
|
const error = getAppPluginPageError(info);
|
|
if (error) {
|
|
appEvents.emit(AppEvents.alertError, [error]);
|
|
dispatch(stateSlice.actions.setState({ pluginNav: getWarningNav(error) }));
|
|
return null;
|
|
}
|
|
return importAppPlugin(info);
|
|
});
|
|
dispatch(stateSlice.actions.setState({ plugin: app, loading: false, pluginNav: null }));
|
|
} catch (err) {
|
|
dispatch(
|
|
stateSlice.actions.setState({
|
|
plugin: null,
|
|
loading: false,
|
|
pluginNav: process.env.NODE_ENV === 'development' ? getExceptionNav(err) : getNotFoundNav(),
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
function getNavIndex(store: StoreState) {
|
|
return store.navIndex;
|
|
}
|
|
|
|
export function getAppPluginPageError(meta: AppPluginMeta) {
|
|
if (!meta) {
|
|
return 'Unknown Plugin';
|
|
}
|
|
if (meta.type !== PluginType.app) {
|
|
return 'Plugin must be an app';
|
|
}
|
|
if (!meta.enabled) {
|
|
return 'Application Not Enabled';
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export default AppRootPage;
|