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{
|
||||
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{
|
||||
{
|
||||
var alertNav = 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,
|
||||
},
|
||||
}
|
||||
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{
|
||||
{
|
||||
var alertNav = 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,
|
||||
},
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
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 { 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" />,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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}`))
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user