ExploreMetrics: Add toggle to enable routing to externalized Explore Metrics app plugin (#99481)
* feat: toggle for explore metrics app plugin * chore: put app navigation settings behind feature toggle * chore: require restart * feat: context-driven applinks toggling * fix: toggle ownership * docs: note restart requirement * refactor: prefer global feature toggle check * chore: undo rm newlines * fix: minimize changes * fix: app id, `chromeless`, sidebar ordering
This commit is contained in:
@@ -251,6 +251,7 @@ export interface FeatureToggles {
|
||||
queryLibraryDashboards?: boolean;
|
||||
grafanaAdvisor?: boolean;
|
||||
elasticsearchImprovedParsing?: boolean;
|
||||
exploreMetricsUseExternalAppPlugin?: boolean;
|
||||
datasourceConnectionsTab?: boolean;
|
||||
fetchRulesUsingPost?: boolean;
|
||||
alertingAlertmanagerExtraDedupStage?: boolean;
|
||||
|
||||
@@ -912,7 +912,7 @@ var (
|
||||
Stage: FeatureStageGeneralAvailability,
|
||||
Expression: "true", // enabled by default
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaDashboardsSquad,
|
||||
Owner: grafanaObservabilityMetricsSquad,
|
||||
},
|
||||
{
|
||||
Name: "alertingSimplifiedRouting",
|
||||
@@ -1742,6 +1742,15 @@ var (
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: awsDatasourcesSquad,
|
||||
},
|
||||
{
|
||||
Name: "exploreMetricsUseExternalAppPlugin",
|
||||
Description: "Use the externalized Metrics Drilldown (formerly known as Explore Metrics) app plugin",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaObservabilityMetricsSquad,
|
||||
FrontendOnly: true,
|
||||
RequiresRestart: true,
|
||||
HideFromDocs: true,
|
||||
},
|
||||
{
|
||||
Name: "datasourceConnectionsTab",
|
||||
Description: "Shows defined connections for a data source in the plugins detail page",
|
||||
|
||||
@@ -120,7 +120,7 @@ pdfTables,preview,@grafana/sharing-squad,false,false,false
|
||||
ssoSettingsApi,GA,@grafana/identity-access-team,false,false,false
|
||||
canvasPanelPanZoom,preview,@grafana/dataviz-squad,false,false,true
|
||||
logsInfiniteScrolling,GA,@grafana/observability-logs,false,false,true
|
||||
exploreMetrics,GA,@grafana/dashboards-squad,false,false,true
|
||||
exploreMetrics,GA,@grafana/observability-metrics,false,false,true
|
||||
alertingSimplifiedRouting,GA,@grafana/alerting-squad,false,false,false
|
||||
logRowsPopoverMenu,GA,@grafana/observability-logs,false,false,true
|
||||
pluginsSkipHostEnvVars,experimental,@grafana/plugins-platform-backend,false,false,false
|
||||
@@ -232,6 +232,7 @@ ABTestFeatureToggleB,experimental,@grafana/sharing-squad,false,false,false
|
||||
queryLibraryDashboards,experimental,@grafana/grafana-frontend-platform,false,false,false
|
||||
grafanaAdvisor,experimental,@grafana/plugins-platform-backend,false,false,false
|
||||
elasticsearchImprovedParsing,experimental,@grafana/aws-datasources,false,false,false
|
||||
exploreMetricsUseExternalAppPlugin,experimental,@grafana/observability-metrics,false,true,true
|
||||
datasourceConnectionsTab,experimental,@grafana/plugins-platform-backend,false,false,true
|
||||
fetchRulesUsingPost,experimental,@grafana/alerting-squad,false,false,false
|
||||
alertingAlertmanagerExtraDedupStage,experimental,@grafana/alerting-squad,false,true,false
|
||||
|
||||
|
@@ -939,6 +939,10 @@ const (
|
||||
// Enables less memory intensive Elasticsearch result parsing
|
||||
FlagElasticsearchImprovedParsing = "elasticsearchImprovedParsing"
|
||||
|
||||
// FlagExploreMetricsUseExternalAppPlugin
|
||||
// Use the externalized Metrics Drilldown (formerly known as Explore Metrics) app plugin
|
||||
FlagExploreMetricsUseExternalAppPlugin = "exploreMetricsUseExternalAppPlugin"
|
||||
|
||||
// FlagDatasourceConnectionsTab
|
||||
// Shows defined connections for a data source in the plugins detail page
|
||||
FlagDatasourceConnectionsTab = "datasourceConnectionsTab"
|
||||
|
||||
@@ -1529,16 +1529,16 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "exploreMetrics",
|
||||
"resourceVersion": "1720021873452",
|
||||
"resourceVersion": "1737658563230",
|
||||
"creationTimestamp": "2024-04-09T18:15:18Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2024-07-03 15:51:13.452477 +0000 UTC"
|
||||
"grafana.app/updatedTimestamp": "2025-01-23 18:56:03.23086 +0000 UTC"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables the new Explore Metrics core app",
|
||||
"stage": "GA",
|
||||
"codeowner": "@grafana/dashboards-squad",
|
||||
"codeowner": "@grafana/observability-metrics",
|
||||
"frontend": true,
|
||||
"expression": "true"
|
||||
}
|
||||
@@ -1557,6 +1557,24 @@
|
||||
"hideFromDocs": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "exploreMetricsUseExternalAppPlugin",
|
||||
"resourceVersion": "1738596266973",
|
||||
"creationTimestamp": "2025-01-21T23:24:50Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2025-02-03 15:24:26.973231 +0000 UTC"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"description": "Use the externalized Metrics Drilldown (formerly known as Explore Metrics) app plugin",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/observability-metrics",
|
||||
"frontend": true,
|
||||
"requiresRestart": true,
|
||||
"hideFromDocs": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "expressionParser",
|
||||
|
||||
@@ -286,9 +286,9 @@ func (s *ServiceImpl) readNavigationSettings() {
|
||||
"grafana-k8s-app": {SectionID: navtree.NavIDInfrastructure, SortWeight: 1, Text: "Kubernetes"},
|
||||
"grafana-dbo11y-app": {SectionID: navtree.NavIDInfrastructure, SortWeight: 2, Text: "Databases"},
|
||||
"grafana-app-observability-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightApplication, Text: "Application", Icon: "graph-bar"},
|
||||
"grafana-lokiexplore-app": {SectionID: navtree.NavIDExplore, SortWeight: 1, Text: "Logs"},
|
||||
"grafana-exploretraces-app": {SectionID: navtree.NavIDExplore, SortWeight: 2, Text: "Traces"},
|
||||
"grafana-pyroscope-app": {SectionID: navtree.NavIDExplore, SortWeight: 3, Text: "Profiles"},
|
||||
"grafana-lokiexplore-app": {SectionID: navtree.NavIDExplore, SortWeight: 2, Text: "Logs"},
|
||||
"grafana-exploretraces-app": {SectionID: navtree.NavIDExplore, SortWeight: 3, Text: "Traces"},
|
||||
"grafana-pyroscope-app": {SectionID: navtree.NavIDExplore, SortWeight: 4, Text: "Profiles"},
|
||||
"grafana-kowalski-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightFrontend, Text: "Frontend", Icon: "frontend-observability"},
|
||||
"grafana-synthetic-monitoring-app": {SectionID: navtree.NavIDTestingAndSynthetics, SortWeight: 2, Text: "Synthetics"},
|
||||
"grafana-irm-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 1, Text: "IRM"},
|
||||
@@ -309,6 +309,10 @@ func (s *ServiceImpl) readNavigationSettings() {
|
||||
"grafana-csp-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightCloudServiceProviders, Icon: "cloud"},
|
||||
}
|
||||
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagExploreMetricsUseExternalAppPlugin) {
|
||||
s.navigationAppConfig["grafana-metricsdrilldown-app"] = NavigationAppConfig{SectionID: navtree.NavIDExplore, SortWeight: 1, Text: "Metrics"}
|
||||
}
|
||||
|
||||
s.navigationAppPathConfig = map[string]NavigationAppConfig{
|
||||
"/a/grafana-auth-app": {SectionID: navtree.NavIDCfgAccess, SortWeight: 2},
|
||||
}
|
||||
|
||||
@@ -562,7 +562,7 @@ func (s *ServiceImpl) buildDataConnectionsNavLink(c *contextmodel.ReqContext) *n
|
||||
|
||||
func (s *ServiceImpl) buildExploreNavLinks(c *contextmodel.ReqContext) []*navtree.NavLink {
|
||||
exploreChildNavs := []*navtree.NavLink{}
|
||||
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagExploreMetrics) {
|
||||
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagExploreMetrics) && !s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagExploreMetricsUseExternalAppPlugin) {
|
||||
exploreChildNavs = append(exploreChildNavs, &navtree.NavLink{
|
||||
Text: "Metrics",
|
||||
SubTitle: "Queryless exploration of your metrics",
|
||||
|
||||
@@ -4,26 +4,28 @@ import { getRootSectionForNode } from 'app/core/selectors/navModel';
|
||||
import AppRootPage from 'app/features/plugins/components/AppRootPage';
|
||||
import { getState } from 'app/store/store';
|
||||
|
||||
const isPluginNavModelItem = (model: NavModelItem): model is PluginNavModelItem => 'pluginId' in model && 'id' in model;
|
||||
const isStandalonePluginPage = (id: string) => id.startsWith('standalone-plugin-page-/');
|
||||
|
||||
function getPathForNavItem(navItem: PluginNavModelItem) {
|
||||
const pluginNavSection = getRootSectionForNode(navItem);
|
||||
const appPluginUrl = `/a/${navItem.pluginId}`;
|
||||
const path = isStandalonePluginPage(navItem.id) ? navItem.url || appPluginUrl : appPluginUrl; // Only standalone pages can use core URLs, otherwise we fall back to "/a/:pluginId"
|
||||
const isSensitive = isStandalonePluginPage(navItem.id) && !navItem.url?.startsWith('/a/'); // Have case-sensitive URLs only for standalone pages that have custom URLs
|
||||
|
||||
return {
|
||||
path: `${path}/*`,
|
||||
sensitive: isSensitive,
|
||||
component: () => <AppRootPage pluginId={navItem.pluginId} pluginNavSection={pluginNavSection} />,
|
||||
};
|
||||
}
|
||||
|
||||
export function getAppPluginRoutes(): RouteDescriptor[] {
|
||||
const state = getState();
|
||||
const { navIndex } = state;
|
||||
const isStandalonePluginPage = (id: string) => id.startsWith('standalone-plugin-page-/');
|
||||
const isPluginNavModelItem = (model: NavModelItem): model is PluginNavModelItem =>
|
||||
'pluginId' in model && 'id' in model;
|
||||
const explicitAppPluginRoutes = Object.values(navIndex)
|
||||
.filter<PluginNavModelItem>(isPluginNavModelItem)
|
||||
.map((navItem) => {
|
||||
const pluginNavSection = getRootSectionForNode(navItem);
|
||||
const appPluginUrl = `/a/${navItem.pluginId}`;
|
||||
const path = isStandalonePluginPage(navItem.id) ? navItem.url || appPluginUrl : appPluginUrl; // Only standalone pages can use core URLs, otherwise we fall back to "/a/:pluginId"
|
||||
const isSensitive = isStandalonePluginPage(navItem.id) && !navItem.url?.startsWith('/a/'); // Have case-sensitive URLs only for standalone pages that have custom URLs
|
||||
|
||||
return {
|
||||
path: `${path}/*`,
|
||||
sensitive: isSensitive,
|
||||
component: () => <AppRootPage pluginId={navItem.pluginId} pluginNavSection={pluginNavSection} />,
|
||||
};
|
||||
});
|
||||
.map(getPathForNavItem);
|
||||
|
||||
return [
|
||||
...explicitAppPluginRoutes,
|
||||
@@ -36,6 +38,23 @@ export function getAppPluginRoutes(): RouteDescriptor[] {
|
||||
];
|
||||
}
|
||||
|
||||
export function getRouteForAppPlugin(pluginId: string): RouteDescriptor {
|
||||
const state = getState();
|
||||
const { navIndex } = state;
|
||||
const navItem = Object.values(navIndex)
|
||||
.filter<PluginNavModelItem>(isPluginNavModelItem)
|
||||
.find((navItem) => navItem.pluginId === pluginId);
|
||||
|
||||
if (!navItem) {
|
||||
return {
|
||||
path: '/a/:pluginId/*',
|
||||
component: () => <AppRootPage pluginNavSection={navIndex.home} />,
|
||||
};
|
||||
}
|
||||
|
||||
return getPathForNavItem(navItem);
|
||||
}
|
||||
|
||||
interface PluginNavModelItem extends Omit<NavModelItem, 'pluginId' | 'id'> {
|
||||
pluginId: string;
|
||||
id: string;
|
||||
|
||||
@@ -15,7 +15,7 @@ import { getRoutes as getDataConnectionsRoutes } from 'app/features/connections/
|
||||
import { DATASOURCES_ROUTES } from 'app/features/datasources/constants';
|
||||
import { ConfigureIRM } from 'app/features/gops/configuration-tracker/components/ConfigureIRM';
|
||||
import { getRoutes as getPluginCatalogRoutes } from 'app/features/plugins/admin/routes';
|
||||
import { getAppPluginRoutes } from 'app/features/plugins/routes';
|
||||
import { getAppPluginRoutes, getRouteForAppPlugin } from 'app/features/plugins/routes';
|
||||
import { getProfileRoutes } from 'app/features/profile/routes';
|
||||
import { AccessControlAction, DashboardRoutes } from 'app/types';
|
||||
|
||||
@@ -515,11 +515,17 @@ export function getAppRoutes(): RouteDescriptor[] {
|
||||
},
|
||||
config.featureToggles.exploreMetrics && {
|
||||
path: '/explore/metrics/*',
|
||||
chromeless: false,
|
||||
roles: () => contextSrv.evaluatePermission([AccessControlAction.DataSourcesExplore]),
|
||||
component: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "DataTrailsPage"*/ 'app/features/trails/DataTrailsPage')
|
||||
),
|
||||
...(config.featureToggles.exploreMetricsUseExternalAppPlugin
|
||||
? {
|
||||
component: getRouteForAppPlugin('grafana-metricsdrilldown-app').component,
|
||||
}
|
||||
: {
|
||||
chromeless: false,
|
||||
component: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "DataTrailsPage"*/ 'app/features/trails/DataTrailsPage')
|
||||
),
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: '/bookmarks',
|
||||
|
||||
Reference in New Issue
Block a user