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:
Ashley Harrison 2022-09-05 10:07:13 +01:00 committed by GitHub
parent 6d2352735d
commit 8d489dfd9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 196 additions and 29 deletions

View File

@ -99,12 +99,17 @@ func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error)
appLink := &dtos.NavLink{
Text: plugin.Name,
Id: "plugin-page-" + plugin.ID,
Url: path.Join(hs.Cfg.AppSubURL, plugin.DefaultNavURL),
Img: plugin.Info.Logos.Small,
Section: dtos.NavSectionPlugin,
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 {
if !c.HasUserRole(include.Role) {
continue
@ -117,7 +122,7 @@ func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error)
Url: hs.Cfg.AppSubURL + include.Path,
Text: include.Name,
}
if include.DefaultNav {
if include.DefaultNav && !hs.Features.IsEnabled(featuremgmt.FlagTopnav) {
appLink.Url = link.Url // Overwrite the hardcoded page logic
}
} else {
@ -298,6 +303,11 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool, prefs *
SortWeight: dtos.WeightConfig,
Children: configNodes,
}
if hs.Features.IsEnabled(featuremgmt.FlagTopnav) {
configNode.Url = "/admin"
} else {
configNode.Url = configNodes[0].Url
}
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
if configNode != nil && serverAdminNode != nil {
configNode.Text = "Administration"
serverAdminNode.Url = "/admin"
configNode.Children = append(configNode.Children, serverAdminNode)
adminNodeIndex := len(navTree) - 1
navTree = navTree[:adminNodeIndex]
@ -412,7 +423,6 @@ func (hs *HTTPServer) setupConfigNodes(c *models.ReqContext) ([]*dtos.NavLink, e
Url: hs.Cfg.AppSubURL + "/org/serviceaccounts",
})
}
return configNodes, nil
}
@ -570,18 +580,21 @@ func (hs *HTTPServer) buildLegacyAlertNavLinks(c *models.ReqContext) []*dtos.Nav
})
}
return []*dtos.NavLink{
{
Text: "Alerting",
SubTitle: "Alert rules and notifications",
Id: "alerting-legacy",
Icon: "bell",
Url: hs.Cfg.AppSubURL + "/alerting/list",
Children: alertChildNavs,
Section: dtos.NavSectionCore,
SortWeight: dtos.WeightAlerting,
},
var alertNav = dtos.NavLink{
Text: "Alerting",
SubTitle: "Alert rules and notifications",
Id: "alerting-legacy",
Icon: "bell",
Children: alertChildNavs,
Section: dtos.NavSectionCore,
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 {
@ -626,18 +639,23 @@ func (hs *HTTPServer) buildAlertNavLinks(c *models.ReqContext) []*dtos.NavLink {
}
if len(alertChildNavs) > 0 {
return []*dtos.NavLink{
{
Text: "Alerting",
SubTitle: "Alert rules and notifications",
Id: "alerting",
Icon: "bell",
Url: hs.Cfg.AppSubURL + "/alerting/list",
Children: alertChildNavs,
Section: dtos.NavSectionCore,
SortWeight: dtos.WeightAlerting,
},
var alertNav = dtos.NavLink{
Text: "Alerting",
SubTitle: "Alert rules and notifications",
Id: "alerting",
Icon: "bell",
Children: alertChildNavs,
Section: dtos.NavSectionCore,
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
}

View 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,
}),
});

View 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,
}),
});

View File

@ -3,6 +3,7 @@ import React from 'react';
import { Redirect } from 'react-router-dom';
import { OrgRole } from '@grafana/data';
import { NavLandingPage } from 'app/core/components/AppChrome/NavLandingPage';
import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport';
import { config } from 'app/core/config';
import { RouteDescriptor } from 'app/core/navigation/types';
@ -13,8 +14,8 @@ import { evaluateAccess } from './unified/utils/access-control';
const commonRoutes: RouteDescriptor[] = [
{
path: '/alerting',
// eslint-disable-next-line react/display-name
component: () => <Redirect to="/alerting/list" />,
component: () =>
config.featureToggles.topnav ? <NavLandingPage navId="alerting" /> : <Redirect to="/alerting/list" />,
},
];

View File

@ -1,6 +1,7 @@
import React from 'react';
import { Redirect } from 'react-router-dom';
import { NavLandingPage } from 'app/core/components/AppChrome/NavLandingPage';
import ErrorPage from 'app/core/components/ErrorPage/ErrorPage';
import { LoginPage } from 'app/core/components/Login/LoginPage';
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 { getLiveRoutes } from 'app/features/live/pages/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 { ServiceAccountPage } from 'app/features/serviceaccounts/ServiceAccountPage';
import { AccessControlAction, DashboardRoutes } from 'app/types';
@ -20,9 +22,33 @@ import { SafeDynamicImport } from '../core/components/DynamicImports/SafeDynamic
import { RouteDescriptor } from '../core/navigation/types';
import { getPublicDashboardRoutes } from '../features/dashboard/routes';
import { pluginHasRootPage } from './utils';
export const extraRoutes: 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 [
{
path: '/',
@ -179,6 +205,7 @@ export function getAppRoutes(): RouteDescriptor[] {
: import(/* webpackChunkName: "explore-feature-toggle-page" */ 'app/features/explore/FeatureTogglePage')
),
},
...topnavRoutes,
{
path: '/a/:pluginId/',
exact: false,
@ -272,8 +299,7 @@ export function getAppRoutes(): RouteDescriptor[] {
{
path: '/admin',
// eslint-disable-next-line react/display-name
component: () => <Redirect to="/admin/users" />,
component: () => (config.featureToggles.topnav ? <NavLandingPage navId="cfg" /> : <Redirect to="/admin/users" />),
},
{
path: '/admin/settings',

View File

@ -1,3 +1,14 @@
import { NavLinkDTO } from '@grafana/data';
export function isSoloRoute(path: string): boolean {
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}`))
);
}