mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Navigation: Landing pages behind feature toggles (#54576)
* super quick attempt * feature toggle everything * only construct alertNav if there are navChildren * fix toggle name * plugin landing pages poc * add apps route + put behind feature toggle * use toIconName * rename to NavLandingPage * feature toggle new routes * don't modify GetServerAdminNode * some fairly hacky code to check if the plugin has a root page * remove trailing slash
This commit is contained in:
parent
6d2352735d
commit
8d489dfd9b
@ -99,12 +99,17 @@ func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error)
|
|||||||
appLink := &dtos.NavLink{
|
appLink := &dtos.NavLink{
|
||||||
Text: plugin.Name,
|
Text: plugin.Name,
|
||||||
Id: "plugin-page-" + plugin.ID,
|
Id: "plugin-page-" + plugin.ID,
|
||||||
Url: path.Join(hs.Cfg.AppSubURL, plugin.DefaultNavURL),
|
|
||||||
Img: plugin.Info.Logos.Small,
|
Img: plugin.Info.Logos.Small,
|
||||||
Section: dtos.NavSectionPlugin,
|
Section: dtos.NavSectionPlugin,
|
||||||
SortWeight: dtos.WeightPlugin,
|
SortWeight: dtos.WeightPlugin,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hs.Features.IsEnabled(featuremgmt.FlagTopnav) {
|
||||||
|
appLink.Url = path.Join(hs.Cfg.AppSubURL, "a", plugin.ID)
|
||||||
|
} else {
|
||||||
|
appLink.Url = path.Join(hs.Cfg.AppSubURL, plugin.DefaultNavURL)
|
||||||
|
}
|
||||||
|
|
||||||
for _, include := range plugin.Includes {
|
for _, include := range plugin.Includes {
|
||||||
if !c.HasUserRole(include.Role) {
|
if !c.HasUserRole(include.Role) {
|
||||||
continue
|
continue
|
||||||
@ -117,7 +122,7 @@ func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error)
|
|||||||
Url: hs.Cfg.AppSubURL + include.Path,
|
Url: hs.Cfg.AppSubURL + include.Path,
|
||||||
Text: include.Name,
|
Text: include.Name,
|
||||||
}
|
}
|
||||||
if include.DefaultNav {
|
if include.DefaultNav && !hs.Features.IsEnabled(featuremgmt.FlagTopnav) {
|
||||||
appLink.Url = link.Url // Overwrite the hardcoded page logic
|
appLink.Url = link.Url // Overwrite the hardcoded page logic
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -298,6 +303,11 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool, prefs *
|
|||||||
SortWeight: dtos.WeightConfig,
|
SortWeight: dtos.WeightConfig,
|
||||||
Children: configNodes,
|
Children: configNodes,
|
||||||
}
|
}
|
||||||
|
if hs.Features.IsEnabled(featuremgmt.FlagTopnav) {
|
||||||
|
configNode.Url = "/admin"
|
||||||
|
} else {
|
||||||
|
configNode.Url = configNodes[0].Url
|
||||||
|
}
|
||||||
navTree = append(navTree, configNode)
|
navTree = append(navTree, configNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -312,6 +322,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool, prefs *
|
|||||||
// Move server admin into Configuration and rename to administration
|
// Move server admin into Configuration and rename to administration
|
||||||
if configNode != nil && serverAdminNode != nil {
|
if configNode != nil && serverAdminNode != nil {
|
||||||
configNode.Text = "Administration"
|
configNode.Text = "Administration"
|
||||||
|
serverAdminNode.Url = "/admin"
|
||||||
configNode.Children = append(configNode.Children, serverAdminNode)
|
configNode.Children = append(configNode.Children, serverAdminNode)
|
||||||
adminNodeIndex := len(navTree) - 1
|
adminNodeIndex := len(navTree) - 1
|
||||||
navTree = navTree[:adminNodeIndex]
|
navTree = navTree[:adminNodeIndex]
|
||||||
@ -412,7 +423,6 @@ func (hs *HTTPServer) setupConfigNodes(c *models.ReqContext) ([]*dtos.NavLink, e
|
|||||||
Url: hs.Cfg.AppSubURL + "/org/serviceaccounts",
|
Url: hs.Cfg.AppSubURL + "/org/serviceaccounts",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return configNodes, nil
|
return configNodes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -570,18 +580,21 @@ func (hs *HTTPServer) buildLegacyAlertNavLinks(c *models.ReqContext) []*dtos.Nav
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return []*dtos.NavLink{
|
var alertNav = dtos.NavLink{
|
||||||
{
|
|
||||||
Text: "Alerting",
|
Text: "Alerting",
|
||||||
SubTitle: "Alert rules and notifications",
|
SubTitle: "Alert rules and notifications",
|
||||||
Id: "alerting-legacy",
|
Id: "alerting-legacy",
|
||||||
Icon: "bell",
|
Icon: "bell",
|
||||||
Url: hs.Cfg.AppSubURL + "/alerting/list",
|
|
||||||
Children: alertChildNavs,
|
Children: alertChildNavs,
|
||||||
Section: dtos.NavSectionCore,
|
Section: dtos.NavSectionCore,
|
||||||
SortWeight: dtos.WeightAlerting,
|
SortWeight: dtos.WeightAlerting,
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
if hs.Features.IsEnabled(featuremgmt.FlagTopnav) {
|
||||||
|
alertNav.Url = hs.Cfg.AppSubURL + "/alerting"
|
||||||
|
} else {
|
||||||
|
alertNav.Url = hs.Cfg.AppSubURL + "/alerting/list"
|
||||||
|
}
|
||||||
|
return []*dtos.NavLink{&alertNav}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hs *HTTPServer) buildAlertNavLinks(c *models.ReqContext) []*dtos.NavLink {
|
func (hs *HTTPServer) buildAlertNavLinks(c *models.ReqContext) []*dtos.NavLink {
|
||||||
@ -626,18 +639,23 @@ func (hs *HTTPServer) buildAlertNavLinks(c *models.ReqContext) []*dtos.NavLink {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(alertChildNavs) > 0 {
|
if len(alertChildNavs) > 0 {
|
||||||
return []*dtos.NavLink{
|
var alertNav = dtos.NavLink{
|
||||||
{
|
|
||||||
Text: "Alerting",
|
Text: "Alerting",
|
||||||
SubTitle: "Alert rules and notifications",
|
SubTitle: "Alert rules and notifications",
|
||||||
Id: "alerting",
|
Id: "alerting",
|
||||||
Icon: "bell",
|
Icon: "bell",
|
||||||
Url: hs.Cfg.AppSubURL + "/alerting/list",
|
|
||||||
Children: alertChildNavs,
|
Children: alertChildNavs,
|
||||||
Section: dtos.NavSectionCore,
|
Section: dtos.NavSectionCore,
|
||||||
SortWeight: dtos.WeightAlerting,
|
SortWeight: dtos.WeightAlerting,
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hs.Features.IsEnabled(featuremgmt.FlagTopnav) {
|
||||||
|
alertNav.Url = hs.Cfg.AppSubURL + "/alerting"
|
||||||
|
} else {
|
||||||
|
alertNav.Url = hs.Cfg.AppSubURL + "/alerting/list"
|
||||||
|
}
|
||||||
|
|
||||||
|
return []*dtos.NavLink{&alertNav}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
82
public/app/core/components/AppChrome/NavLandingPage.tsx
Normal file
82
public/app/core/components/AppChrome/NavLandingPage.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { toIconName, useStyles2 } from '@grafana/ui';
|
||||||
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
|
import { useNavModel } from 'app/core/hooks/useNavModel';
|
||||||
|
|
||||||
|
import { NavLandingPageCard } from './NavLandingPageCard';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
navId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NavLandingPage({ navId }: Props) {
|
||||||
|
const { node } = useNavModel(navId);
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const directChildren = node.children?.filter((child) => !child.hideFromTabs && !child.children);
|
||||||
|
const nestedChildren = node.children?.filter((child) => child.children && child.children.length);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page navId={node.id}>
|
||||||
|
<Page.Contents>
|
||||||
|
<div className={styles.content}>
|
||||||
|
{directChildren && directChildren.length > 0 && (
|
||||||
|
<section className={styles.grid}>
|
||||||
|
{directChildren?.map((child) => (
|
||||||
|
<NavLandingPageCard
|
||||||
|
key={child.id}
|
||||||
|
description={child.description}
|
||||||
|
icon={child.icon ? toIconName(child.icon) : undefined}
|
||||||
|
text={child.text}
|
||||||
|
url={child.url ?? ''}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
{nestedChildren?.map((child) => (
|
||||||
|
<section key={child.id}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<h2 className={styles.nestedTitle}>{child.text}</h2>
|
||||||
|
</div>
|
||||||
|
<div className={styles.nestedDescription}>{child.subTitle}</div>
|
||||||
|
<div className={styles.grid}>
|
||||||
|
{child.children?.map((child) => (
|
||||||
|
<NavLandingPageCard
|
||||||
|
key={child.id}
|
||||||
|
description={child.description}
|
||||||
|
icon={child.icon ? toIconName(child.icon) : undefined}
|
||||||
|
text={child.text}
|
||||||
|
url={child.url ?? ''}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Page.Contents>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
content: css({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
}),
|
||||||
|
grid: css({
|
||||||
|
display: 'grid',
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
|
||||||
|
gridAutoRows: '200px',
|
||||||
|
padding: theme.spacing(2, 1),
|
||||||
|
}),
|
||||||
|
nestedTitle: css({
|
||||||
|
margin: theme.spacing(2, 0),
|
||||||
|
}),
|
||||||
|
nestedDescription: css({
|
||||||
|
color: theme.colors.text.secondary,
|
||||||
|
}),
|
||||||
|
});
|
29
public/app/core/components/AppChrome/NavLandingPageCard.tsx
Normal file
29
public/app/core/components/AppChrome/NavLandingPageCard.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { Card, Icon, IconName, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
description?: string;
|
||||||
|
icon?: IconName;
|
||||||
|
text: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NavLandingPageCard({ description, icon, text, url }: Props) {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
return (
|
||||||
|
<Card className={styles.card} href={url}>
|
||||||
|
<Card.Heading>{text}</Card.Heading>
|
||||||
|
<Card.Figure align={'center'}>{icon && <Icon name={icon} size="xxxl" />}</Card.Figure>
|
||||||
|
<Card.Description>{description}</Card.Description>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
card: css({
|
||||||
|
marginBottom: 0,
|
||||||
|
}),
|
||||||
|
});
|
@ -3,6 +3,7 @@ import React from 'react';
|
|||||||
import { Redirect } from 'react-router-dom';
|
import { Redirect } from 'react-router-dom';
|
||||||
|
|
||||||
import { OrgRole } from '@grafana/data';
|
import { OrgRole } from '@grafana/data';
|
||||||
|
import { NavLandingPage } from 'app/core/components/AppChrome/NavLandingPage';
|
||||||
import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport';
|
import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
import { RouteDescriptor } from 'app/core/navigation/types';
|
import { RouteDescriptor } from 'app/core/navigation/types';
|
||||||
@ -13,8 +14,8 @@ import { evaluateAccess } from './unified/utils/access-control';
|
|||||||
const commonRoutes: RouteDescriptor[] = [
|
const commonRoutes: RouteDescriptor[] = [
|
||||||
{
|
{
|
||||||
path: '/alerting',
|
path: '/alerting',
|
||||||
// eslint-disable-next-line react/display-name
|
component: () =>
|
||||||
component: () => <Redirect to="/alerting/list" />,
|
config.featureToggles.topnav ? <NavLandingPage navId="alerting" /> : <Redirect to="/alerting/list" />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Redirect } from 'react-router-dom';
|
import { Redirect } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { NavLandingPage } from 'app/core/components/AppChrome/NavLandingPage';
|
||||||
import ErrorPage from 'app/core/components/ErrorPage/ErrorPage';
|
import ErrorPage from 'app/core/components/ErrorPage/ErrorPage';
|
||||||
import { LoginPage } from 'app/core/components/Login/LoginPage';
|
import { LoginPage } from 'app/core/components/Login/LoginPage';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
@ -12,6 +13,7 @@ import { getRoutes as getDataConnectionsRoutes } from 'app/features/data-connect
|
|||||||
import { DATASOURCES_ROUTES } from 'app/features/datasources/constants';
|
import { DATASOURCES_ROUTES } from 'app/features/datasources/constants';
|
||||||
import { getLiveRoutes } from 'app/features/live/pages/routes';
|
import { getLiveRoutes } from 'app/features/live/pages/routes';
|
||||||
import { getRoutes as getPluginCatalogRoutes } from 'app/features/plugins/admin/routes';
|
import { getRoutes as getPluginCatalogRoutes } from 'app/features/plugins/admin/routes';
|
||||||
|
import AppRootPage from 'app/features/plugins/components/AppRootPage';
|
||||||
import { getProfileRoutes } from 'app/features/profile/routes';
|
import { getProfileRoutes } from 'app/features/profile/routes';
|
||||||
import { ServiceAccountPage } from 'app/features/serviceaccounts/ServiceAccountPage';
|
import { ServiceAccountPage } from 'app/features/serviceaccounts/ServiceAccountPage';
|
||||||
import { AccessControlAction, DashboardRoutes } from 'app/types';
|
import { AccessControlAction, DashboardRoutes } from 'app/types';
|
||||||
@ -20,9 +22,33 @@ import { SafeDynamicImport } from '../core/components/DynamicImports/SafeDynamic
|
|||||||
import { RouteDescriptor } from '../core/navigation/types';
|
import { RouteDescriptor } from '../core/navigation/types';
|
||||||
import { getPublicDashboardRoutes } from '../features/dashboard/routes';
|
import { getPublicDashboardRoutes } from '../features/dashboard/routes';
|
||||||
|
|
||||||
|
import { pluginHasRootPage } from './utils';
|
||||||
|
|
||||||
export const extraRoutes: RouteDescriptor[] = [];
|
export const extraRoutes: RouteDescriptor[] = [];
|
||||||
|
|
||||||
export function getAppRoutes(): RouteDescriptor[] {
|
export function getAppRoutes(): RouteDescriptor[] {
|
||||||
|
const topnavRoutes: RouteDescriptor[] = config.featureToggles.topnav
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
path: '/apps',
|
||||||
|
component: () => <NavLandingPage navId="apps" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/a/:pluginId',
|
||||||
|
exact: true,
|
||||||
|
component: (props) => {
|
||||||
|
const hasRoot = pluginHasRootPage(props.match.params.pluginId, config.bootData.navTree);
|
||||||
|
const hasQueryParams = Object.keys(props.queryParams).length > 0;
|
||||||
|
return hasRoot || hasQueryParams ? (
|
||||||
|
<AppRootPage {...props} />
|
||||||
|
) : (
|
||||||
|
<NavLandingPage navId={`plugin-page-${props.match.params.pluginId}`} />
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
@ -179,6 +205,7 @@ export function getAppRoutes(): RouteDescriptor[] {
|
|||||||
: import(/* webpackChunkName: "explore-feature-toggle-page" */ 'app/features/explore/FeatureTogglePage')
|
: import(/* webpackChunkName: "explore-feature-toggle-page" */ 'app/features/explore/FeatureTogglePage')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
...topnavRoutes,
|
||||||
{
|
{
|
||||||
path: '/a/:pluginId/',
|
path: '/a/:pluginId/',
|
||||||
exact: false,
|
exact: false,
|
||||||
@ -272,8 +299,7 @@ export function getAppRoutes(): RouteDescriptor[] {
|
|||||||
|
|
||||||
{
|
{
|
||||||
path: '/admin',
|
path: '/admin',
|
||||||
// eslint-disable-next-line react/display-name
|
component: () => (config.featureToggles.topnav ? <NavLandingPage navId="cfg" /> : <Redirect to="/admin/users" />),
|
||||||
component: () => <Redirect to="/admin/users" />,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/settings',
|
path: '/admin/settings',
|
||||||
|
@ -1,3 +1,14 @@
|
|||||||
|
import { NavLinkDTO } from '@grafana/data';
|
||||||
|
|
||||||
export function isSoloRoute(path: string): boolean {
|
export function isSoloRoute(path: string): boolean {
|
||||||
return /(d-solo|dashboard-solo)/.test(path?.toLowerCase());
|
return /(d-solo|dashboard-solo)/.test(path?.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function pluginHasRootPage(pluginId: string, navTree: NavLinkDTO[]): boolean {
|
||||||
|
return Boolean(
|
||||||
|
navTree
|
||||||
|
.find((navLink) => navLink.id === 'apps')
|
||||||
|
?.children?.find((app) => app.id === `plugin-page-${pluginId}`)
|
||||||
|
?.children?.some((page) => page.url?.endsWith(`/a/${pluginId}`))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user