From e54149e551ce95ea466bb72c931cd14808045537 Mon Sep 17 00:00:00 2001 From: Andre Pereira Date: Thu, 20 Feb 2025 17:56:55 +0000 Subject: [PATCH] Explore: Move drilldown apps from Explore to a new navbar item "Drilldown" (#100409) * Move drilldown apps from Explore to a new navbar item "Drilldown" * Commit make i18n-extract * Update drilldown icon * Added alert to explore with call out to drilldown apps * Add isNew field for nav item which shows a "New!" badge on the navbar and expands it by default * Fix e2e test --- .../loki-table-explore-to-dash.spec.ts | 1 + packages/grafana-data/src/types/icon.ts | 1 + packages/grafana-data/src/types/navModel.ts | 1 + packages/grafana-icons/svg/drilldown.svg | 6 ++++ pkg/api/accesscontrol.go | 5 +++- pkg/api/api.go | 1 + pkg/services/accesscontrol/models.go | 3 +- pkg/services/navtree/models.go | 3 ++ pkg/services/navtree/navtreeimpl/applinks.go | 8 +++--- pkg/services/navtree/navtreeimpl/navtree.go | 24 ++++++++++++---- .../AppChrome/MegaMenu/MegaMenuItem.tsx | 6 ++-- .../AppChrome/MegaMenu/MegaMenuItemText.tsx | 2 +- .../app/core/utils/navBarItem-translations.ts | 2 ++ public/app/features/explore/Explore.tsx | 28 +++++++++++++++++++ public/app/routes/routes.tsx | 4 +++ public/img/icons/unicons/drilldown.svg | 6 ++++ public/locales/en-US/grafana.json | 9 ++++++ public/locales/pseudo-LOCALE/grafana.json | 9 ++++++ 18 files changed, 104 insertions(+), 15 deletions(-) create mode 100644 packages/grafana-icons/svg/drilldown.svg create mode 100644 public/img/icons/unicons/drilldown.svg diff --git a/e2e/various-suite/loki-table-explore-to-dash.spec.ts b/e2e/various-suite/loki-table-explore-to-dash.spec.ts index 1477f6f0165..f4867855ca3 100644 --- a/e2e/various-suite/loki-table-explore-to-dash.spec.ts +++ b/e2e/various-suite/loki-table-explore-to-dash.spec.ts @@ -175,6 +175,7 @@ describe('Loki Query Editor', () => { // One row with two cells cy.get('[role="cell"]').should('have.length', 2); + cy.contains('label', 'targetLabelName').scrollIntoView(); cy.contains('label', 'targetLabelName').should('be.visible'); cy.contains('label', 'targetLabelName').click(); cy.contains('label', 'targetLabelName').within(() => { diff --git a/packages/grafana-data/src/types/icon.ts b/packages/grafana-data/src/types/icon.ts index 357cf07e46e..b1092680181 100644 --- a/packages/grafana-data/src/types/icon.ts +++ b/packages/grafana-data/src/types/icon.ts @@ -93,6 +93,7 @@ export const availableIconsIndex = { 'document-layout-left': true, 'download-alt': true, draggabledots: true, + drilldown: true, edit: true, 'ellipsis-v': true, enter: true, diff --git a/packages/grafana-data/src/types/navModel.ts b/packages/grafana-data/src/types/navModel.ts index 066d879df3d..f9ebb23fc07 100644 --- a/packages/grafana-data/src/types/navModel.ts +++ b/packages/grafana-data/src/types/navModel.ts @@ -14,6 +14,7 @@ export interface NavLinkDTO { sortWeight?: number; hideFromTabs?: boolean; roundIcon?: boolean; + isNew?: boolean; /** * This is true for some sections that have no children (but is still a section) **/ diff --git a/packages/grafana-icons/svg/drilldown.svg b/packages/grafana-icons/svg/drilldown.svg new file mode 100644 index 00000000000..27f627ce19f --- /dev/null +++ b/packages/grafana-icons/svg/drilldown.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/pkg/api/accesscontrol.go b/pkg/api/accesscontrol.go index f74e32f1b50..1855531c0ce 100644 --- a/pkg/api/accesscontrol.go +++ b/pkg/api/accesscontrol.go @@ -60,12 +60,15 @@ func (hs *HTTPServer) declareFixedRoles() error { Role: ac.RoleDTO{ Name: "fixed:datasources:explorer", DisplayName: "Explorer", - Description: "Enable the Explore feature. Data source permissions still apply; you can only query data sources for which you have query permissions.", + Description: "Enable the Explore and Drilldown features. Data source permissions still apply; you can only query data sources for which you have query permissions.", Group: "Data sources", Permissions: []ac.Permission{ { Action: ac.ActionDatasourcesExplore, }, + { + Action: ac.ActionDatasourcesDrilldown, + }, }, }, Grants: []string{string(org.RoleEditor)}, diff --git a/pkg/api/api.go b/pkg/api/api.go index acc8a29c21f..5b085c89d00 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -183,6 +183,7 @@ func (hs *HTTPServer) registerRoutes() { } r.Get("/explore", authorize(ac.EvalPermission(ac.ActionDatasourcesExplore)), hs.Index) + r.Get("/drilldown", authorize(ac.EvalPermission(ac.ActionDatasourcesDrilldown)), hs.Index) r.Get("/playlists/", reqSignedIn, hs.Index) r.Get("/playlists/*", reqSignedIn, hs.Index) diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index dc12171eaa5..4c15e3c5e56 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -386,7 +386,8 @@ const ( ActionSettingsWrite = "settings:write" // Datasources actions - ActionDatasourcesExplore = "datasources:explore" + ActionDatasourcesExplore = "datasources:explore" + ActionDatasourcesDrilldown = "datasources:drilldown" // Global Scopes ScopeGlobalUsersAll = "global.users:*" diff --git a/pkg/services/navtree/models.go b/pkg/services/navtree/models.go index bf324abc076..97d7ab1112b 100644 --- a/pkg/services/navtree/models.go +++ b/pkg/services/navtree/models.go @@ -16,6 +16,7 @@ const ( WeightSavedItems WeightDashboard WeightExplore + WeightDrilldown WeightAlerting WeightAlertsAndIncidents WeightTestingAndSynthetics @@ -37,6 +38,7 @@ const ( NavIDRoot = "root" NavIDDashboards = "dashboards/browse" NavIDExplore = "explore" + NavIDDrilldown = "drilldown" NavIDCfg = "cfg" // NavIDCfg is the id for org configuration navigation node NavIDAlertsAndIncidents = "alerts-and-incidents" NavIDTestingAndSynthetics = "testing-and-synthetics" @@ -70,6 +72,7 @@ type NavLink struct { EmptyMessageId string `json:"emptyMessageId,omitempty"` PluginID string `json:"pluginId,omitempty"` // (Optional) The ID of the plugin that registered nav link (e.g. as a standalone plugin page) IsCreateAction bool `json:"isCreateAction,omitempty"` + IsNew bool `json:"isNew,omitempty"` // (Optional) Adds "New!" badge to the nav link and expands it by default Keywords []string `json:"keywords,omitempty"` ParentItem *NavLink `json:"parentItem,omitempty"` // (Optional) The parent item of the nav link } diff --git a/pkg/services/navtree/navtreeimpl/applinks.go b/pkg/services/navtree/navtreeimpl/applinks.go index 1adc19e0d57..5c6b88756d3 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: 2, Text: "Logs"}, - "grafana-exploretraces-app": {SectionID: navtree.NavIDExplore, SortWeight: 3, Text: "Traces"}, - "grafana-pyroscope-app": {SectionID: navtree.NavIDExplore, SortWeight: 4, Text: "Profiles"}, + "grafana-lokiexplore-app": {SectionID: navtree.NavIDDrilldown, SortWeight: 2, Text: "Logs"}, + "grafana-exploretraces-app": {SectionID: navtree.NavIDDrilldown, SortWeight: 3, Text: "Traces"}, + "grafana-pyroscope-app": {SectionID: navtree.NavIDDrilldown, 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"}, @@ -310,7 +310,7 @@ func (s *ServiceImpl) readNavigationSettings() { } if s.features.IsEnabledGlobally(featuremgmt.FlagExploreMetricsUseExternalAppPlugin) { - s.navigationAppConfig["grafana-metricsdrilldown-app"] = NavigationAppConfig{SectionID: navtree.NavIDExplore, SortWeight: 1, Text: "Metrics"} + s.navigationAppConfig["grafana-metricsdrilldown-app"] = NavigationAppConfig{SectionID: navtree.NavIDDrilldown, SortWeight: 1, Text: "Metrics"} } if s.features.IsEnabledGlobally(featuremgmt.FlagGrafanaAdvisor) { diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index 1f461db67f3..7158c8884dc 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -120,7 +120,6 @@ func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, prefs *pref.Prefere } if s.cfg.ExploreEnabled && hasAccess(ac.EvalPermission(ac.ActionDatasourcesExplore)) { - exploreChildNavLinks := s.buildExploreNavLinks(c) treeRoot.AddSection(&navtree.NavLink{ Text: "Explore", Id: navtree.NavIDExplore, @@ -128,7 +127,20 @@ func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, prefs *pref.Prefere Icon: "compass", SortWeight: navtree.WeightExplore, Url: s.cfg.AppSubURL + "/explore", - Children: exploreChildNavLinks, + }) + } + + if hasAccess(ac.EvalPermission(ac.ActionDatasourcesDrilldown)) { + drilldownChildNavLinks := s.buildDrilldownNavLinks(c) + treeRoot.AddSection(&navtree.NavLink{ + Text: "Drilldown", + Id: navtree.NavIDDrilldown, + SubTitle: "Drill down into your data using Grafana's powerful queryless apps", + Icon: "drilldown", + IsNew: true, + SortWeight: navtree.WeightDrilldown, + Url: s.cfg.AppSubURL + "/drilldown", + Children: drilldownChildNavLinks, }) } @@ -560,10 +572,10 @@ func (s *ServiceImpl) buildDataConnectionsNavLink(c *contextmodel.ReqContext) *n return nil } -func (s *ServiceImpl) buildExploreNavLinks(c *contextmodel.ReqContext) []*navtree.NavLink { - exploreChildNavs := []*navtree.NavLink{} +func (s *ServiceImpl) buildDrilldownNavLinks(c *contextmodel.ReqContext) []*navtree.NavLink { + drilldownChildNavs := []*navtree.NavLink{} if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagExploreMetrics) && !s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagExploreMetricsUseExternalAppPlugin) { - exploreChildNavs = append(exploreChildNavs, &navtree.NavLink{ + drilldownChildNavs = append(drilldownChildNavs, &navtree.NavLink{ Text: "Metrics", SubTitle: "Queryless exploration of your metrics", Id: "explore/metrics", @@ -571,5 +583,5 @@ func (s *ServiceImpl) buildExploreNavLinks(c *contextmodel.ReqContext) []*navtre Icon: "code-branch", }) } - return exploreChildNavs + return drilldownChildNavs } diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx index ce8b9a53d40..4379283f3c5 100644 --- a/public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx @@ -5,8 +5,9 @@ import { useLocation } from 'react-router-dom-v5-compat'; import { useLocalStorage } from 'react-use'; import { GrafanaTheme2, NavModelItem, toIconName } from '@grafana/data'; -import { useStyles2, Text, IconButton, Icon, Stack } from '@grafana/ui'; +import { useStyles2, Text, IconButton, Icon, Stack, Badge } from '@grafana/ui'; import { useGrafana } from 'app/core/context/GrafanaContext'; +import { t } from 'app/core/internationalization'; import { Indent } from '../../Indent/Indent'; @@ -35,7 +36,7 @@ export function MegaMenuItem({ link, activeItem, level = 0, onClick, onPin, isPi const isActive = link === activeItem || (level === MAX_DEPTH && hasActiveChild); const [sectionExpanded, setSectionExpanded] = useLocalStorage( `grafana.navigation.expanded[${link.text}]`, - Boolean(hasActiveChild) + Boolean(hasActiveChild || link.isNew) ); const showExpandButton = level < MAX_DEPTH && Boolean(linkHasChildren(link) || link.emptyMessage); const item = useRef(null); @@ -107,6 +108,7 @@ export function MegaMenuItem({ link, activeItem, level = 0, onClick, onPin, isPi > {level === 0 && iconElement && {iconElement}} {link.text} + {link.isNew && } diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenuItemText.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenuItemText.tsx index 471450c7d23..b6d18b50d9d 100644 --- a/public/app/core/components/AppChrome/MegaMenu/MegaMenuItemText.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenuItemText.tsx @@ -114,7 +114,7 @@ const getStyles = (theme: GrafanaTheme2, isActive: Props['isActive']) => ({ position: 'relative', width: '100%', - '&:hover, &:focus-visible': { + '&:hover span, &:focus-visible span': { color: theme.colors.text.primary, textDecoration: 'underline', }, diff --git a/public/app/core/utils/navBarItem-translations.ts b/public/app/core/utils/navBarItem-translations.ts index 993a914d2c7..eb533d88b07 100644 --- a/public/app/core/utils/navBarItem-translations.ts +++ b/public/app/core/utils/navBarItem-translations.ts @@ -61,6 +61,8 @@ export function getNavTitle(navId: string | undefined) { return t('nav.scenes.title', 'Scenes'); case 'explore': return t('nav.explore.title', 'Explore'); + case 'drilldown': + return t('nav.drilldown.title', 'Drilldown'); case 'alerting': return t('nav.alerting.title', 'Alerting'); case 'plugin-page-grafana-oncall-app': diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 28ad370648e..4927fc5be4a 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -23,14 +23,18 @@ import { getDataSourceSrv, reportInteraction } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { AdHocFilterItem, + Alert, ErrorBoundaryAlert, + LinkButton, PanelContainer, ScrollContainer, + Stack, Themeable2, withTheme2, } from '@grafana/ui'; import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR } from '@grafana/ui/src/components/Table/types'; import { supportedFeatures } from 'app/core/history/richHistoryStorageProvider'; +import { t, Trans } from 'app/core/internationalization'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; import { StoreState } from 'app/types'; @@ -564,6 +568,9 @@ export class Explore extends PureComponent { if (showCorrelationHelper && correlationEditorHelperData !== undefined) { correlationsBox = ; } + const isDsCompatibleWithDrilldown = ['prometheus', 'loki', 'tempo', 'grafana-pyroscope-datasource'].includes( + datasourceInstance?.type || '' + ); return ( @@ -593,6 +600,27 @@ export class Explore extends PureComponent { <> + {isDsCompatibleWithDrilldown && ( + + + + + Looking for the Grafana Explore apps? They are now called the Grafana Drilldown apps + and can be found under Menu > Drilldown + + + + Go to Grafana Drilldown + + + + )} {correlationsBox} , + }, { path: '/apps', component: () => , diff --git a/public/img/icons/unicons/drilldown.svg b/public/img/icons/unicons/drilldown.svg new file mode 100644 index 00000000000..27f627ce19f --- /dev/null +++ b/public/img/icons/unicons/drilldown.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index e80a59d9129..d7cba3f8990 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1419,6 +1419,11 @@ }, "explore": { "add-to-dashboard": "Add to dashboard", + "drilldownInfo": { + "action": "Go to Grafana Drilldown", + "description": "Looking for the Grafana Explore apps? They are now called the Grafana Drilldown apps and can be found under <1>Menu > Drilldown", + "title": "Explore Metrics, Logs, Traces and Profiles have moved!" + }, "logs": { "logs-volume": { "add-filters": "Add more labels to your query to narrow down your search.", @@ -2499,6 +2504,9 @@ "detect": { "title": "Detect" }, + "drilldown": { + "title": "Drilldown" + }, "explore": { "title": "Explore" }, @@ -2714,6 +2722,7 @@ "close": "Close menu", "dock": "Dock menu", "list-label": "Navigation", + "new": "New!", "open": "Open menu", "undock": "Undock menu" }, diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 43096c5ce25..2dc0f928c44 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1419,6 +1419,11 @@ }, "explore": { "add-to-dashboard": "Åđđ ŧő đäşĥþőäřđ", + "drilldownInfo": { + "action": "Ğő ŧő Ğřäƒäʼnä Đřįľľđőŵʼn", + "description": "Ŀőőĸįʼnģ ƒőř ŧĥę Ğřäƒäʼnä Ēχpľőřę äppş? Ŧĥęy äřę ʼnőŵ čäľľęđ ŧĥę Ğřäƒäʼnä Đřįľľđőŵʼn äppş äʼnđ čäʼn þę ƒőūʼnđ ūʼnđęř <1>Męʼnū > Đřįľľđőŵʼn", + "title": "Ēχpľőřę Męŧřįčş, Ŀőģş, Ŧřäčęş äʼnđ Přőƒįľęş ĥävę mővęđ!" + }, "logs": { "logs-volume": { "add-filters": "Åđđ mőřę ľäþęľş ŧő yőūř qūęřy ŧő ʼnäřřőŵ đőŵʼn yőūř şęäřčĥ.", @@ -2499,6 +2504,9 @@ "detect": { "title": "Đęŧęčŧ" }, + "drilldown": { + "title": "Đřįľľđőŵʼn" + }, "explore": { "title": "Ēχpľőřę" }, @@ -2714,6 +2722,7 @@ "close": "Cľőşę męʼnū", "dock": "Đőčĸ męʼnū", "list-label": "Ńävįģäŧįőʼn", + "new": "Ńęŵ!", "open": "Øpęʼn męʼnū", "undock": "Ůʼnđőčĸ męʼnū" },