diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts
index 24f2244ac83..71fa78c0d67 100644
--- a/packages/grafana-data/src/types/featureToggles.gen.ts
+++ b/packages/grafana-data/src/types/featureToggles.gen.ts
@@ -251,6 +251,7 @@ export interface FeatureToggles {
queryLibraryDashboards?: boolean;
grafanaAdvisor?: boolean;
elasticsearchImprovedParsing?: boolean;
+ exploreMetricsUseExternalAppPlugin?: boolean;
datasourceConnectionsTab?: boolean;
fetchRulesUsingPost?: boolean;
alertingAlertmanagerExtraDedupStage?: boolean;
diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go
index 7571ff6a37f..3e737bb1f45 100644
--- a/pkg/services/featuremgmt/registry.go
+++ b/pkg/services/featuremgmt/registry.go
@@ -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",
diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv
index 250833a78a3..3b653f76b4e 100644
--- a/pkg/services/featuremgmt/toggles_gen.csv
+++ b/pkg/services/featuremgmt/toggles_gen.csv
@@ -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
diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go
index 556275ed908..3a5db419907 100644
--- a/pkg/services/featuremgmt/toggles_gen.go
+++ b/pkg/services/featuremgmt/toggles_gen.go
@@ -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"
diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json
index 573ae344f02..e39d9ef150e 100644
--- a/pkg/services/featuremgmt/toggles_gen.json
+++ b/pkg/services/featuremgmt/toggles_gen.json
@@ -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",
diff --git a/pkg/services/navtree/navtreeimpl/applinks.go b/pkg/services/navtree/navtreeimpl/applinks.go
index 8fc88a5ed80..cf78f67e1d4 100644
--- a/pkg/services/navtree/navtreeimpl/applinks.go
+++ b/pkg/services/navtree/navtreeimpl/applinks.go
@@ -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},
}
diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go
index c67ee1bbfe8..1f461db67f3 100644
--- a/pkg/services/navtree/navtreeimpl/navtree.go
+++ b/pkg/services/navtree/navtreeimpl/navtree.go
@@ -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",
diff --git a/public/app/features/plugins/routes.tsx b/public/app/features/plugins/routes.tsx
index 46cc470549d..1504f6e2e48 100644
--- a/public/app/features/plugins/routes.tsx
+++ b/public/app/features/plugins/routes.tsx
@@ -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: () => ,
+ };
+}
+
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(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: () => ,
- };
- });
+ .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(isPluginNavModelItem)
+ .find((navItem) => navItem.pluginId === pluginId);
+
+ if (!navItem) {
+ return {
+ path: '/a/:pluginId/*',
+ component: () => ,
+ };
+ }
+
+ return getPathForNavItem(navItem);
+}
+
interface PluginNavModelItem extends Omit {
pluginId: string;
id: string;
diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx
index 8a59242f820..ddadc2f40b7 100644
--- a/public/app/routes/routes.tsx
+++ b/public/app/routes/routes.tsx
@@ -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',