diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 3cc6fa27b0b..e140056e6ee 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -59,4 +59,5 @@ export interface FeatureToggles { canvasPanelNesting?: boolean; cloudMonitoringExperimentalUI?: boolean; logRequestsInstrumentedAsUnknown?: boolean; + dataConnectionsConsole?: boolean; } diff --git a/pkg/api/dtos/index.go b/pkg/api/dtos/index.go index caba326a2a8..d951bcfc87a 100644 --- a/pkg/api/dtos/index.go +++ b/pkg/api/dtos/index.go @@ -42,6 +42,7 @@ const ( WeightDashboard WeightExplore WeightAlerting + WeightDataConnections WeightPlugin WeightConfig WeightAdmin @@ -61,7 +62,7 @@ type NavLink struct { Description string `json:"description,omitempty"` Section string `json:"section,omitempty"` SubTitle string `json:"subTitle,omitempty"` - Icon string `json:"icon,omitempty"` + Icon string `json:"icon,omitempty"` // Available icons can be browsed in Storybook: https://developers.grafana.com/ui/latest/index.html?path=/story/docs-overview-icon--icons-overview Img string `json:"img,omitempty"` Url string `json:"url,omitempty"` Target string `json:"target,omitempty"` diff --git a/pkg/api/index.go b/pkg/api/index.go index 8aa9a327461..c288c653817 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -245,6 +245,10 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool, prefs * navTree = append(navTree, hs.buildAlertNavLinks(c)...) } + if hs.Features.IsEnabled(featuremgmt.FlagDataConnectionsConsole) { + navTree = append(navTree, hs.buildDataConnectionsNavLink(c)) + } + appLinks, err := hs.getAppLinks(c) if err != nil { return nil, err @@ -630,6 +634,58 @@ func (hs *HTTPServer) buildCreateNavLinks(c *models.ReqContext) []*dtos.NavLink return children } +func (hs *HTTPServer) buildDataConnectionsNavLink(c *models.ReqContext) *dtos.NavLink { + var children []*dtos.NavLink + var navLink *dtos.NavLink + + baseId := "data-connections" + baseUrl := hs.Cfg.AppSubURL + "/" + baseId + + children = append(children, &dtos.NavLink{ + Id: baseId + "-datasources", + Text: "Data sources", + Icon: "database", + Description: "Add and configure data sources", + Url: baseUrl + "/datasources", + }) + + children = append(children, &dtos.NavLink{ + Id: baseId + "-plugins", + Text: "Plugins", + Icon: "plug", + Description: "Manage plugins", + Url: baseUrl + "/plugins", + }) + + children = append(children, &dtos.NavLink{ + Id: baseId + "-cloud-integrations", + Text: "Cloud integrations", + Icon: "bolt", + Description: "Manage your cloud integrations", + Url: baseUrl + "/cloud-integrations", + }) + + children = append(children, &dtos.NavLink{ + Id: baseId + "-recorded-queries", + Text: "Recorded queries", + Icon: "record-audio", + Description: "Manage your recorded queries", + Url: baseUrl + "/recorded-queries", + }) + + navLink = &dtos.NavLink{ + Text: "Data Connections", + Icon: "link", + Id: baseId, + Url: baseUrl, + Children: children, + Section: dtos.NavSectionCore, + SortWeight: dtos.WeightDataConnections, + } + + return navLink +} + func (hs *HTTPServer) buildAdminNavLinks(c *models.ReqContext) []*dtos.NavLink { hasAccess := ac.HasAccess(hs.AccessControl, c) hasGlobalAccess := ac.HasGlobalAccess(hs.AccessControl, c) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 1490b6be669..416f5da8ef8 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -240,6 +240,10 @@ var ( { Name: "logRequestsInstrumentedAsUnknown", Description: "Logs the path for requests that are instrumented as unknown", + }, + { + Name: "dataConnectionsConsole", + Description: "Enables a new top-level page called Data Connections. This page is an experiment for better grouping of installing / configuring data sources and other plugins.", State: FeatureStateAlpha, }, } diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index bbc6b55dfd7..ec83161bcf8 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -178,4 +178,8 @@ const ( // FlagLogRequestsInstrumentedAsUnknown // Logs the path for requests that are instrumented as unknown FlagLogRequestsInstrumentedAsUnknown = "logRequestsInstrumentedAsUnknown" + + // FlagDataConnectionsConsole + // Enables a new top-level page called Data Connections. This page is an experiment for better grouping of installing / configuring data sources and other plugins. + FlagDataConnectionsConsole = "dataConnectionsConsole" ) diff --git a/public/app/features/data-connections/DataConnectionsPage.test.tsx b/public/app/features/data-connections/DataConnectionsPage.test.tsx new file mode 100644 index 00000000000..3498905301f --- /dev/null +++ b/public/app/features/data-connections/DataConnectionsPage.test.tsx @@ -0,0 +1,42 @@ +import { render, RenderResult, screen } from '@testing-library/react'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { Router } from 'react-router-dom'; + +import { locationService } from '@grafana/runtime'; +import { configureStore } from 'app/store/configureStore'; + +import DataConnectionsPage from './DataConnectionsPage'; +import navIndex from './__mocks__/store.navIndex.mock'; +import { ROUTE_BASE_ID } from './constants'; + +const renderPage = (path = `/${ROUTE_BASE_ID}`): RenderResult => { + // @ts-ignore + const store = configureStore({ navIndex }); + locationService.push(path); + + return render( + + + + + + ); +}; + +describe('Data Connections Page', () => { + test('shows all the four tabs', async () => { + renderPage(); + + expect(await screen.findByLabelText('Tab Data sources')).toBeVisible(); + expect(await screen.findByLabelText('Tab Plugins')).toBeVisible(); + expect(await screen.findByLabelText('Tab Cloud integrations')).toBeVisible(); + expect(await screen.findByLabelText('Tab Recorded queries')).toBeVisible(); + }); + + test('shows the "Data sources" tab by default', async () => { + renderPage(); + + expect(await screen.findByText('The list of data sources is under development.')).toBeVisible(); + }); +}); diff --git a/public/app/features/data-connections/DataConnectionsPage.tsx b/public/app/features/data-connections/DataConnectionsPage.tsx new file mode 100644 index 00000000000..0cf34e69871 --- /dev/null +++ b/public/app/features/data-connections/DataConnectionsPage.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import { Route, Switch } from 'react-router-dom'; + +import { Page } from 'app/core/components/Page/Page'; + +import { ROUTES } from './constants'; +import { useNavModel } from './hooks/useNavModel'; +import { CloudIntegrations } from './tabs/CloudIntegrations'; +import { DataSources } from './tabs/DataSources'; +import { Plugins } from './tabs/Plugins'; +import { RecordedQueries } from './tabs/RecordedQueries'; + +export default function DataConnectionsPage(): React.ReactElement | null { + const navModel = useNavModel(); + + return ( + + + + + + + + {/* Default page */} + + + + + ); +} diff --git a/public/app/features/data-connections/__mocks__/store.navIndex.mock.ts b/public/app/features/data-connections/__mocks__/store.navIndex.mock.ts new file mode 100644 index 00000000000..4546a29ec7c --- /dev/null +++ b/public/app/features/data-connections/__mocks__/store.navIndex.mock.ts @@ -0,0 +1,2519 @@ +export default { + dashboards: { + id: 'dashboards', + text: 'Dashboards', + section: 'core', + subTitle: 'Manage dashboards and folders', + icon: 'apps', + url: '/dashboards', + sortWeight: -1800, + children: [ + { + id: 'manage-dashboards', + text: 'Browse', + icon: 'sitemap', + url: '/dashboards', + }, + { + id: 'playlists', + text: 'Playlists', + icon: 'presentation-play', + url: '/playlists', + }, + { + id: 'snapshots', + text: 'Snapshots', + icon: 'camera', + url: '/dashboard/snapshots', + }, + { + id: 'library-panels', + text: 'Library panels', + icon: 'library-panel', + url: '/library-panels', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'new-dashboard', + text: 'New dashboard', + icon: 'plus', + url: '/dashboard/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'new-folder', + text: 'New folder', + subTitle: 'Create a new folder to organize your dashboards', + icon: 'plus', + url: '/dashboards/folder/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'import', + text: 'Import', + subTitle: 'Import dashboard from file or Grafana.com', + icon: 'plus', + url: '/dashboard/import', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + 'manage-dashboards': { + id: 'manage-dashboards', + text: 'Browse', + icon: 'sitemap', + url: '/dashboards', + parentItem: { + id: 'dashboards', + text: 'Dashboards', + section: 'core', + subTitle: 'Manage dashboards and folders', + icon: 'apps', + url: '/dashboards', + sortWeight: -1800, + children: [ + { + id: 'manage-dashboards', + text: 'Browse', + icon: 'sitemap', + url: '/dashboards', + }, + { + id: 'playlists', + text: 'Playlists', + icon: 'presentation-play', + url: '/playlists', + }, + { + id: 'snapshots', + text: 'Snapshots', + icon: 'camera', + url: '/dashboard/snapshots', + }, + { + id: 'library-panels', + text: 'Library panels', + icon: 'library-panel', + url: '/library-panels', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'new-dashboard', + text: 'New dashboard', + icon: 'plus', + url: '/dashboard/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'new-folder', + text: 'New folder', + subTitle: 'Create a new folder to organize your dashboards', + icon: 'plus', + url: '/dashboards/folder/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'import', + text: 'Import', + subTitle: 'Import dashboard from file or Grafana.com', + icon: 'plus', + url: '/dashboard/import', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + playlists: { + id: 'playlists', + text: 'Playlists', + icon: 'presentation-play', + url: '/playlists', + parentItem: { + id: 'dashboards', + text: 'Dashboards', + section: 'core', + subTitle: 'Manage dashboards and folders', + icon: 'apps', + url: '/dashboards', + sortWeight: -1800, + children: [ + { + id: 'manage-dashboards', + text: 'Browse', + icon: 'sitemap', + url: '/dashboards', + }, + { + id: 'playlists', + text: 'Playlists', + icon: 'presentation-play', + url: '/playlists', + }, + { + id: 'snapshots', + text: 'Snapshots', + icon: 'camera', + url: '/dashboard/snapshots', + }, + { + id: 'library-panels', + text: 'Library panels', + icon: 'library-panel', + url: '/library-panels', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'new-dashboard', + text: 'New dashboard', + icon: 'plus', + url: '/dashboard/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'new-folder', + text: 'New folder', + subTitle: 'Create a new folder to organize your dashboards', + icon: 'plus', + url: '/dashboards/folder/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'import', + text: 'Import', + subTitle: 'Import dashboard from file or Grafana.com', + icon: 'plus', + url: '/dashboard/import', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + snapshots: { + id: 'snapshots', + text: 'Snapshots', + icon: 'camera', + url: '/dashboard/snapshots', + parentItem: { + id: 'dashboards', + text: 'Dashboards', + section: 'core', + subTitle: 'Manage dashboards and folders', + icon: 'apps', + url: '/dashboards', + sortWeight: -1800, + children: [ + { + id: 'manage-dashboards', + text: 'Browse', + icon: 'sitemap', + url: '/dashboards', + }, + { + id: 'playlists', + text: 'Playlists', + icon: 'presentation-play', + url: '/playlists', + }, + { + id: 'snapshots', + text: 'Snapshots', + icon: 'camera', + url: '/dashboard/snapshots', + }, + { + id: 'library-panels', + text: 'Library panels', + icon: 'library-panel', + url: '/library-panels', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'new-dashboard', + text: 'New dashboard', + icon: 'plus', + url: '/dashboard/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'new-folder', + text: 'New folder', + subTitle: 'Create a new folder to organize your dashboards', + icon: 'plus', + url: '/dashboards/folder/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'import', + text: 'Import', + subTitle: 'Import dashboard from file or Grafana.com', + icon: 'plus', + url: '/dashboard/import', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + 'library-panels': { + id: 'library-panels', + text: 'Library panels', + icon: 'library-panel', + url: '/library-panels', + parentItem: { + id: 'dashboards', + text: 'Dashboards', + section: 'core', + subTitle: 'Manage dashboards and folders', + icon: 'apps', + url: '/dashboards', + sortWeight: -1800, + children: [ + { + id: 'manage-dashboards', + text: 'Browse', + icon: 'sitemap', + url: '/dashboards', + }, + { + id: 'playlists', + text: 'Playlists', + icon: 'presentation-play', + url: '/playlists', + }, + { + id: 'snapshots', + text: 'Snapshots', + icon: 'camera', + url: '/dashboard/snapshots', + }, + { + id: 'library-panels', + text: 'Library panels', + icon: 'library-panel', + url: '/library-panels', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'new-dashboard', + text: 'New dashboard', + icon: 'plus', + url: '/dashboard/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'new-folder', + text: 'New folder', + subTitle: 'Create a new folder to organize your dashboards', + icon: 'plus', + url: '/dashboards/folder/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'import', + text: 'Import', + subTitle: 'Import dashboard from file or Grafana.com', + icon: 'plus', + url: '/dashboard/import', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + divider: { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + parentItem: { + id: 'alerting', + text: 'Alerting', + section: 'core', + subTitle: 'Alert rules and notifications', + icon: 'bell', + url: '/alerting/list', + sortWeight: -1600, + children: [ + { + id: 'alert-list', + text: 'Alert rules', + icon: 'list-ul', + url: '/alerting/list', + }, + { + id: 'receivers', + text: 'Contact points', + icon: 'comment-alt-share', + url: '/alerting/notifications', + }, + { + id: 'am-routes', + text: 'Notification policies', + icon: 'sitemap', + url: '/alerting/routes', + }, + { + id: 'silences', + text: 'Silences', + icon: 'bell-slash', + url: '/alerting/silences', + }, + { + id: 'groups', + text: 'Alert groups', + icon: 'layer-group', + url: '/alerting/groups', + }, + { + id: 'alerting-admin', + text: 'Admin', + icon: 'cog', + url: '/alerting/admin', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'alert', + text: 'New alert rule', + subTitle: 'Create an alert rule', + icon: 'plus', + url: '/alerting/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + 'new-dashboard': { + id: 'new-dashboard', + text: 'New dashboard', + icon: 'plus', + url: '/dashboard/new', + hideFromTabs: true, + showIconInNavbar: true, + parentItem: { + id: 'dashboards', + text: 'Dashboards', + section: 'core', + subTitle: 'Manage dashboards and folders', + icon: 'apps', + url: '/dashboards', + sortWeight: -1800, + children: [ + { + id: 'manage-dashboards', + text: 'Browse', + icon: 'sitemap', + url: '/dashboards', + }, + { + id: 'playlists', + text: 'Playlists', + icon: 'presentation-play', + url: '/playlists', + }, + { + id: 'snapshots', + text: 'Snapshots', + icon: 'camera', + url: '/dashboard/snapshots', + }, + { + id: 'library-panels', + text: 'Library panels', + icon: 'library-panel', + url: '/library-panels', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'new-dashboard', + text: 'New dashboard', + icon: 'plus', + url: '/dashboard/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'new-folder', + text: 'New folder', + subTitle: 'Create a new folder to organize your dashboards', + icon: 'plus', + url: '/dashboards/folder/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'import', + text: 'Import', + subTitle: 'Import dashboard from file or Grafana.com', + icon: 'plus', + url: '/dashboard/import', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + 'new-folder': { + id: 'new-folder', + text: 'New folder', + subTitle: 'Create a new folder to organize your dashboards', + icon: 'plus', + url: '/dashboards/folder/new', + hideFromTabs: true, + showIconInNavbar: true, + parentItem: { + id: 'dashboards', + text: 'Dashboards', + section: 'core', + subTitle: 'Manage dashboards and folders', + icon: 'apps', + url: '/dashboards', + sortWeight: -1800, + children: [ + { + id: 'manage-dashboards', + text: 'Browse', + icon: 'sitemap', + url: '/dashboards', + }, + { + id: 'playlists', + text: 'Playlists', + icon: 'presentation-play', + url: '/playlists', + }, + { + id: 'snapshots', + text: 'Snapshots', + icon: 'camera', + url: '/dashboard/snapshots', + }, + { + id: 'library-panels', + text: 'Library panels', + icon: 'library-panel', + url: '/library-panels', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'new-dashboard', + text: 'New dashboard', + icon: 'plus', + url: '/dashboard/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'new-folder', + text: 'New folder', + subTitle: 'Create a new folder to organize your dashboards', + icon: 'plus', + url: '/dashboards/folder/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'import', + text: 'Import', + subTitle: 'Import dashboard from file or Grafana.com', + icon: 'plus', + url: '/dashboard/import', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + import: { + id: 'import', + text: 'Import', + subTitle: 'Import dashboard from file or Grafana.com', + icon: 'plus', + url: '/dashboard/import', + hideFromTabs: true, + showIconInNavbar: true, + parentItem: { + id: 'dashboards', + text: 'Dashboards', + section: 'core', + subTitle: 'Manage dashboards and folders', + icon: 'apps', + url: '/dashboards', + sortWeight: -1800, + children: [ + { + id: 'manage-dashboards', + text: 'Browse', + icon: 'sitemap', + url: '/dashboards', + }, + { + id: 'playlists', + text: 'Playlists', + icon: 'presentation-play', + url: '/playlists', + }, + { + id: 'snapshots', + text: 'Snapshots', + icon: 'camera', + url: '/dashboard/snapshots', + }, + { + id: 'library-panels', + text: 'Library panels', + icon: 'library-panel', + url: '/library-panels', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'new-dashboard', + text: 'New dashboard', + icon: 'plus', + url: '/dashboard/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'new-folder', + text: 'New folder', + subTitle: 'Create a new folder to organize your dashboards', + icon: 'plus', + url: '/dashboards/folder/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + { + id: 'import', + text: 'Import', + subTitle: 'Import dashboard from file or Grafana.com', + icon: 'plus', + url: '/dashboard/import', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + 'not-found': { + text: 'Page not found', + subTitle: '404 Error', + icon: 'exclamation-triangle', + }, + explore: { + id: 'explore', + text: 'Explore', + section: 'core', + subTitle: 'Explore your data', + icon: 'compass', + url: '/explore', + sortWeight: -1700, + }, + alerting: { + id: 'alerting', + text: 'Alerting', + section: 'core', + subTitle: 'Alert rules and notifications', + icon: 'bell', + url: '/alerting/list', + sortWeight: -1600, + children: [ + { + id: 'alert-list', + text: 'Alert rules', + icon: 'list-ul', + url: '/alerting/list', + }, + { + id: 'receivers', + text: 'Contact points', + icon: 'comment-alt-share', + url: '/alerting/notifications', + }, + { + id: 'am-routes', + text: 'Notification policies', + icon: 'sitemap', + url: '/alerting/routes', + }, + { + id: 'silences', + text: 'Silences', + icon: 'bell-slash', + url: '/alerting/silences', + }, + { + id: 'groups', + text: 'Alert groups', + icon: 'layer-group', + url: '/alerting/groups', + }, + { + id: 'alerting-admin', + text: 'Admin', + icon: 'cog', + url: '/alerting/admin', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'alert', + text: 'New alert rule', + subTitle: 'Create an alert rule', + icon: 'plus', + url: '/alerting/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + 'alert-list': { + id: 'alert-list', + text: 'Alert rules', + icon: 'list-ul', + url: '/alerting/list', + parentItem: { + id: 'alerting', + text: 'Alerting', + section: 'core', + subTitle: 'Alert rules and notifications', + icon: 'bell', + url: '/alerting/list', + sortWeight: -1600, + children: [ + { + id: 'alert-list', + text: 'Alert rules', + icon: 'list-ul', + url: '/alerting/list', + }, + { + id: 'receivers', + text: 'Contact points', + icon: 'comment-alt-share', + url: '/alerting/notifications', + }, + { + id: 'am-routes', + text: 'Notification policies', + icon: 'sitemap', + url: '/alerting/routes', + }, + { + id: 'silences', + text: 'Silences', + icon: 'bell-slash', + url: '/alerting/silences', + }, + { + id: 'groups', + text: 'Alert groups', + icon: 'layer-group', + url: '/alerting/groups', + }, + { + id: 'alerting-admin', + text: 'Admin', + icon: 'cog', + url: '/alerting/admin', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'alert', + text: 'New alert rule', + subTitle: 'Create an alert rule', + icon: 'plus', + url: '/alerting/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + receivers: { + id: 'receivers', + text: 'Contact points', + icon: 'comment-alt-share', + url: '/alerting/notifications', + parentItem: { + id: 'alerting', + text: 'Alerting', + section: 'core', + subTitle: 'Alert rules and notifications', + icon: 'bell', + url: '/alerting/list', + sortWeight: -1600, + children: [ + { + id: 'alert-list', + text: 'Alert rules', + icon: 'list-ul', + url: '/alerting/list', + }, + { + id: 'receivers', + text: 'Contact points', + icon: 'comment-alt-share', + url: '/alerting/notifications', + }, + { + id: 'am-routes', + text: 'Notification policies', + icon: 'sitemap', + url: '/alerting/routes', + }, + { + id: 'silences', + text: 'Silences', + icon: 'bell-slash', + url: '/alerting/silences', + }, + { + id: 'groups', + text: 'Alert groups', + icon: 'layer-group', + url: '/alerting/groups', + }, + { + id: 'alerting-admin', + text: 'Admin', + icon: 'cog', + url: '/alerting/admin', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'alert', + text: 'New alert rule', + subTitle: 'Create an alert rule', + icon: 'plus', + url: '/alerting/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + 'am-routes': { + id: 'am-routes', + text: 'Notification policies', + icon: 'sitemap', + url: '/alerting/routes', + parentItem: { + id: 'alerting', + text: 'Alerting', + section: 'core', + subTitle: 'Alert rules and notifications', + icon: 'bell', + url: '/alerting/list', + sortWeight: -1600, + children: [ + { + id: 'alert-list', + text: 'Alert rules', + icon: 'list-ul', + url: '/alerting/list', + }, + { + id: 'receivers', + text: 'Contact points', + icon: 'comment-alt-share', + url: '/alerting/notifications', + }, + { + id: 'am-routes', + text: 'Notification policies', + icon: 'sitemap', + url: '/alerting/routes', + }, + { + id: 'silences', + text: 'Silences', + icon: 'bell-slash', + url: '/alerting/silences', + }, + { + id: 'groups', + text: 'Alert groups', + icon: 'layer-group', + url: '/alerting/groups', + }, + { + id: 'alerting-admin', + text: 'Admin', + icon: 'cog', + url: '/alerting/admin', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'alert', + text: 'New alert rule', + subTitle: 'Create an alert rule', + icon: 'plus', + url: '/alerting/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + silences: { + id: 'silences', + text: 'Silences', + icon: 'bell-slash', + url: '/alerting/silences', + parentItem: { + id: 'alerting', + text: 'Alerting', + section: 'core', + subTitle: 'Alert rules and notifications', + icon: 'bell', + url: '/alerting/list', + sortWeight: -1600, + children: [ + { + id: 'alert-list', + text: 'Alert rules', + icon: 'list-ul', + url: '/alerting/list', + }, + { + id: 'receivers', + text: 'Contact points', + icon: 'comment-alt-share', + url: '/alerting/notifications', + }, + { + id: 'am-routes', + text: 'Notification policies', + icon: 'sitemap', + url: '/alerting/routes', + }, + { + id: 'silences', + text: 'Silences', + icon: 'bell-slash', + url: '/alerting/silences', + }, + { + id: 'groups', + text: 'Alert groups', + icon: 'layer-group', + url: '/alerting/groups', + }, + { + id: 'alerting-admin', + text: 'Admin', + icon: 'cog', + url: '/alerting/admin', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'alert', + text: 'New alert rule', + subTitle: 'Create an alert rule', + icon: 'plus', + url: '/alerting/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + groups: { + id: 'groups', + text: 'Alert groups', + icon: 'layer-group', + url: '/alerting/groups', + parentItem: { + id: 'alerting', + text: 'Alerting', + section: 'core', + subTitle: 'Alert rules and notifications', + icon: 'bell', + url: '/alerting/list', + sortWeight: -1600, + children: [ + { + id: 'alert-list', + text: 'Alert rules', + icon: 'list-ul', + url: '/alerting/list', + }, + { + id: 'receivers', + text: 'Contact points', + icon: 'comment-alt-share', + url: '/alerting/notifications', + }, + { + id: 'am-routes', + text: 'Notification policies', + icon: 'sitemap', + url: '/alerting/routes', + }, + { + id: 'silences', + text: 'Silences', + icon: 'bell-slash', + url: '/alerting/silences', + }, + { + id: 'groups', + text: 'Alert groups', + icon: 'layer-group', + url: '/alerting/groups', + }, + { + id: 'alerting-admin', + text: 'Admin', + icon: 'cog', + url: '/alerting/admin', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'alert', + text: 'New alert rule', + subTitle: 'Create an alert rule', + icon: 'plus', + url: '/alerting/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + 'alerting-admin': { + id: 'alerting-admin', + text: 'Admin', + icon: 'cog', + url: '/alerting/admin', + parentItem: { + id: 'alerting', + text: 'Alerting', + section: 'core', + subTitle: 'Alert rules and notifications', + icon: 'bell', + url: '/alerting/list', + sortWeight: -1600, + children: [ + { + id: 'alert-list', + text: 'Alert rules', + icon: 'list-ul', + url: '/alerting/list', + }, + { + id: 'receivers', + text: 'Contact points', + icon: 'comment-alt-share', + url: '/alerting/notifications', + }, + { + id: 'am-routes', + text: 'Notification policies', + icon: 'sitemap', + url: '/alerting/routes', + }, + { + id: 'silences', + text: 'Silences', + icon: 'bell-slash', + url: '/alerting/silences', + }, + { + id: 'groups', + text: 'Alert groups', + icon: 'layer-group', + url: '/alerting/groups', + }, + { + id: 'alerting-admin', + text: 'Admin', + icon: 'cog', + url: '/alerting/admin', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'alert', + text: 'New alert rule', + subTitle: 'Create an alert rule', + icon: 'plus', + url: '/alerting/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + alert: { + id: 'alert', + text: 'New alert rule', + subTitle: 'Create an alert rule', + icon: 'plus', + url: '/alerting/new', + hideFromTabs: true, + showIconInNavbar: true, + parentItem: { + id: 'alerting', + text: 'Alerting', + section: 'core', + subTitle: 'Alert rules and notifications', + icon: 'bell', + url: '/alerting/list', + sortWeight: -1600, + children: [ + { + id: 'alert-list', + text: 'Alert rules', + icon: 'list-ul', + url: '/alerting/list', + }, + { + id: 'receivers', + text: 'Contact points', + icon: 'comment-alt-share', + url: '/alerting/notifications', + }, + { + id: 'am-routes', + text: 'Notification policies', + icon: 'sitemap', + url: '/alerting/routes', + }, + { + id: 'silences', + text: 'Silences', + icon: 'bell-slash', + url: '/alerting/silences', + }, + { + id: 'groups', + text: 'Alert groups', + icon: 'layer-group', + url: '/alerting/groups', + }, + { + id: 'alerting-admin', + text: 'Admin', + icon: 'cog', + url: '/alerting/admin', + }, + { + id: 'divider', + text: 'Divider', + divider: true, + hideFromTabs: true, + }, + { + id: 'alert', + text: 'New alert rule', + subTitle: 'Create an alert rule', + icon: 'plus', + url: '/alerting/new', + hideFromTabs: true, + showIconInNavbar: true, + }, + ], + }, + }, + 'data-connections': { + id: 'data-connections', + text: 'Data Connections', + section: 'core', + icon: 'link', + url: '/data-connections', + sortWeight: -1500, + children: [ + { + id: 'data-connections-datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/data-connections/datasources', + active: false, + }, + { + id: 'data-connections-plugins', + text: 'Plugins', + description: 'Manage plugins', + icon: 'plug', + url: '/data-connections/plugins', + active: false, + }, + { + id: 'data-connections-cloud-integrations', + text: 'Cloud integrations', + description: 'Manage your cloud integrations', + icon: 'bolt', + url: '/data-connections/cloud-integrations', + active: true, + }, + { + id: 'data-connections-recorded-queries', + text: 'Recorded queries', + description: 'Manage your recorded queries', + icon: 'record-audio', + url: '/data-connections/recorded-queries', + active: false, + }, + ], + }, + 'data-connections-datasources': { + id: 'data-connections-datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/data-connections/datasources', + parentItem: { + id: 'data-connections', + text: 'Data Connections', + section: 'core', + icon: 'link', + url: '/data-connections', + sortWeight: -1500, + children: [ + { + id: 'data-connections-datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/data-connections/datasources', + }, + { + id: 'data-connections-plugins', + text: 'Plugins', + description: 'Manage plugins', + icon: 'plug', + url: '/data-connections/plugins', + }, + { + id: 'data-connections-cloud-integrations', + text: 'Cloud integrations', + description: 'Manage your cloud integrations', + icon: 'bolt', + url: '/data-connections/cloud-integrations', + }, + { + id: 'data-connections-recorded-queries', + text: 'Recorded queries', + description: 'Manage your recorded queries', + icon: 'record-audio', + url: '/data-connections/recorded-queries', + }, + ], + }, + }, + 'data-connections-plugins': { + id: 'data-connections-plugins', + text: 'Plugins', + description: 'Manage plugins', + icon: 'plug', + url: '/data-connections/plugins', + parentItem: { + id: 'data-connections', + text: 'Data Connections', + section: 'core', + icon: 'link', + url: '/data-connections', + sortWeight: -1500, + children: [ + { + id: 'data-connections-datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/data-connections/datasources', + }, + { + id: 'data-connections-plugins', + text: 'Plugins', + description: 'Manage plugins', + icon: 'plug', + url: '/data-connections/plugins', + }, + { + id: 'data-connections-cloud-integrations', + text: 'Cloud integrations', + description: 'Manage your cloud integrations', + icon: 'bolt', + url: '/data-connections/cloud-integrations', + }, + { + id: 'data-connections-recorded-queries', + text: 'Recorded queries', + description: 'Manage your recorded queries', + icon: 'record-audio', + url: '/data-connections/recorded-queries', + }, + ], + }, + }, + 'data-connections-cloud-integrations': { + id: 'data-connections-cloud-integrations', + text: 'Cloud integrations', + description: 'Manage your cloud integrations', + icon: 'bolt', + url: '/data-connections/cloud-integrations', + parentItem: { + id: 'data-connections', + text: 'Data Connections', + section: 'core', + icon: 'link', + url: '/data-connections', + sortWeight: -1500, + children: [ + { + id: 'data-connections-datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/data-connections/datasources', + }, + { + id: 'data-connections-plugins', + text: 'Plugins', + description: 'Manage plugins', + icon: 'plug', + url: '/data-connections/plugins', + }, + { + id: 'data-connections-cloud-integrations', + text: 'Cloud integrations', + description: 'Manage your cloud integrations', + icon: 'bolt', + url: '/data-connections/cloud-integrations', + }, + { + id: 'data-connections-recorded-queries', + text: 'Recorded queries', + description: 'Manage your recorded queries', + icon: 'record-audio', + url: '/data-connections/recorded-queries', + }, + ], + }, + }, + 'data-connections-recorded-queries': { + id: 'data-connections-recorded-queries', + text: 'Recorded queries', + description: 'Manage your recorded queries', + icon: 'record-audio', + url: '/data-connections/recorded-queries', + parentItem: { + id: 'data-connections', + text: 'Data Connections', + section: 'core', + icon: 'link', + url: '/data-connections', + sortWeight: -1500, + children: [ + { + id: 'data-connections-datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/data-connections/datasources', + }, + { + id: 'data-connections-plugins', + text: 'Plugins', + description: 'Manage plugins', + icon: 'plug', + url: '/data-connections/plugins', + }, + { + id: 'data-connections-cloud-integrations', + text: 'Cloud integrations', + description: 'Manage your cloud integrations', + icon: 'bolt', + url: '/data-connections/cloud-integrations', + }, + { + id: 'data-connections-recorded-queries', + text: 'Recorded queries', + description: 'Manage your recorded queries', + icon: 'record-audio', + url: '/data-connections/recorded-queries', + }, + ], + }, + }, + 'plugin-page-basic-app': { + id: 'plugin-page-basic-app', + text: 'Basic App', + section: 'plugin', + img: 'public/plugins/basic-app/img/logo.svg', + url: '/a/basic-app/one', + sortWeight: -1400, + children: [ + { + text: 'Page One', + url: '/a/basic-app/one', + }, + { + text: 'Page Two', + url: '/a/basic-app/two', + }, + { + text: 'Page Three', + url: '/a/basic-app/three', + }, + { + text: 'Page Four', + url: '/a/basic-app/four', + }, + { + text: 'Configuration', + icon: 'fa fa-cog', + url: '/plugins/basic-app', + }, + ], + }, + undefined: { + text: 'Config', + url: '/plugins/grafana-synthetic-monitoring-app', + parentItem: { + id: 'plugin-page-grafana-synthetic-monitoring-app', + text: 'Synthetic Monitoring', + section: 'plugin', + img: 'public/plugins/grafana-synthetic-monitoring-app/img/logo.svg', + url: '/a/grafana-synthetic-monitoring-app/home', + sortWeight: -1400, + children: [ + { + text: 'Home', + url: '/a/grafana-synthetic-monitoring-app/home', + }, + { + text: 'Summary', + url: '/a/grafana-synthetic-monitoring-app/redirect?dashboard=summary', + }, + { + text: 'Checks', + url: '/a/grafana-synthetic-monitoring-app/checks', + }, + { + text: 'Probes', + url: '/a/grafana-synthetic-monitoring-app/probes', + }, + { + text: 'Alerts', + url: '/a/grafana-synthetic-monitoring-app/alerts', + }, + { + text: 'Config', + url: '/plugins/grafana-synthetic-monitoring-app', + }, + ], + }, + }, + 'plugin-page-cloudflare-app': { + id: 'plugin-page-cloudflare-app', + text: 'Cloudflare Grafana App', + section: 'plugin', + img: 'public/plugins/cloudflare-app/img/cf_icon.png', + sortWeight: -1400, + children: [ + { + text: 'Zones', + url: '/d/KAVdMAw9k', + }, + { + text: 'DNS Firewall', + url: '/d/QrKttDVqu', + }, + ], + }, + 'plugin-page-grafana-easystart-app': { + id: 'plugin-page-grafana-easystart-app', + text: 'Integrations and Connections', + section: 'plugin', + img: 'public/plugins/grafana-easystart-app/img/logo.svg', + url: '/a/grafana-easystart-app', + sortWeight: -1400, + }, + 'plugin-page-redis-explorer-app': { + id: 'plugin-page-redis-explorer-app', + text: 'Redis Explorer', + section: 'plugin', + img: 'public/plugins/redis-explorer-app/img/logo.svg', + url: '/a/redis-explorer-app/', + sortWeight: -1400, + children: [ + { + text: 'Home', + icon: 'home-alt', + url: '/a/redis-explorer-app/', + }, + { + text: 'Enterprise Clusters', + icon: 'apps', + url: '/d/1dKhTjtGk', + }, + { + text: 'Cluster Overview', + icon: 'monitor', + url: '/d/viroIzSGz', + }, + { + text: 'Cluster Nodes', + icon: 'sitemap', + url: '/d/hqze6rtGz', + }, + { + text: 'Cluster Databases', + icon: 'database', + url: '/d/k_A8MjtMk', + }, + { + text: 'Cluster Alerts', + icon: 'info-circle', + url: '/d/xESAiFcnk', + }, + ], + }, + 'plugin-page-grafana-synthetic-monitoring-app': { + id: 'plugin-page-grafana-synthetic-monitoring-app', + text: 'Synthetic Monitoring', + section: 'plugin', + img: 'public/plugins/grafana-synthetic-monitoring-app/img/logo.svg', + url: '/a/grafana-synthetic-monitoring-app/home', + sortWeight: -1400, + children: [ + { + text: 'Home', + url: '/a/grafana-synthetic-monitoring-app/home', + }, + { + text: 'Summary', + url: '/a/grafana-synthetic-monitoring-app/redirect?dashboard=summary', + }, + { + text: 'Checks', + url: '/a/grafana-synthetic-monitoring-app/checks', + }, + { + text: 'Probes', + url: '/a/grafana-synthetic-monitoring-app/probes', + }, + { + text: 'Alerts', + url: '/a/grafana-synthetic-monitoring-app/alerts', + }, + { + text: 'Config', + url: '/plugins/grafana-synthetic-monitoring-app', + }, + ], + }, + 'plugin-page-grafana-k6-app': { + id: 'plugin-page-grafana-k6-app', + text: 'k6 Cloud App', + section: 'plugin', + img: 'public/plugins/grafana-k6-app/img/logo.svg', + url: '/a/grafana-k6-app', + sortWeight: -1400, + }, + cfg: { + id: 'cfg', + text: 'Configuration', + section: 'config', + subTitle: 'Organization: Main Org.', + icon: 'cog', + url: '/datasources', + sortWeight: -1300, + children: [ + { + id: 'datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/datasources', + }, + { + id: 'users', + text: 'Users', + description: 'Manage org members', + icon: 'user', + url: '/org/users', + }, + { + id: 'teams', + text: 'Teams', + description: 'Manage org groups', + icon: 'users-alt', + url: '/org/teams', + }, + { + id: 'plugins', + text: 'Plugins', + description: 'View and configure plugins', + icon: 'plug', + url: '/plugins', + }, + { + id: 'org-settings', + text: 'Preferences', + description: 'Organization preferences', + icon: 'sliders-v-alt', + url: '/org', + }, + { + id: 'apikeys', + text: 'API keys', + description: 'Create & manage API keys', + icon: 'key-skeleton-alt', + url: '/org/apikeys', + }, + ], + }, + datasources: { + id: 'datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/datasources', + parentItem: { + id: 'cfg', + text: 'Configuration', + section: 'config', + subTitle: 'Organization: Main Org.', + icon: 'cog', + url: '/datasources', + sortWeight: -1300, + children: [ + { + id: 'datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/datasources', + }, + { + id: 'users', + text: 'Users', + description: 'Manage org members', + icon: 'user', + url: '/org/users', + }, + { + id: 'teams', + text: 'Teams', + description: 'Manage org groups', + icon: 'users-alt', + url: '/org/teams', + }, + { + id: 'plugins', + text: 'Plugins', + description: 'View and configure plugins', + icon: 'plug', + url: '/plugins', + }, + { + id: 'org-settings', + text: 'Preferences', + description: 'Organization preferences', + icon: 'sliders-v-alt', + url: '/org', + }, + { + id: 'apikeys', + text: 'API keys', + description: 'Create & manage API keys', + icon: 'key-skeleton-alt', + url: '/org/apikeys', + }, + ], + }, + }, + users: { + id: 'users', + text: 'Users', + description: 'Manage org members', + icon: 'user', + url: '/org/users', + parentItem: { + id: 'cfg', + text: 'Configuration', + section: 'config', + subTitle: 'Organization: Main Org.', + icon: 'cog', + url: '/datasources', + sortWeight: -1300, + children: [ + { + id: 'datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/datasources', + }, + { + id: 'users', + text: 'Users', + description: 'Manage org members', + icon: 'user', + url: '/org/users', + }, + { + id: 'teams', + text: 'Teams', + description: 'Manage org groups', + icon: 'users-alt', + url: '/org/teams', + }, + { + id: 'plugins', + text: 'Plugins', + description: 'View and configure plugins', + icon: 'plug', + url: '/plugins', + }, + { + id: 'org-settings', + text: 'Preferences', + description: 'Organization preferences', + icon: 'sliders-v-alt', + url: '/org', + }, + { + id: 'apikeys', + text: 'API keys', + description: 'Create & manage API keys', + icon: 'key-skeleton-alt', + url: '/org/apikeys', + }, + ], + }, + }, + teams: { + id: 'teams', + text: 'Teams', + description: 'Manage org groups', + icon: 'users-alt', + url: '/org/teams', + parentItem: { + id: 'cfg', + text: 'Configuration', + section: 'config', + subTitle: 'Organization: Main Org.', + icon: 'cog', + url: '/datasources', + sortWeight: -1300, + children: [ + { + id: 'datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/datasources', + }, + { + id: 'users', + text: 'Users', + description: 'Manage org members', + icon: 'user', + url: '/org/users', + }, + { + id: 'teams', + text: 'Teams', + description: 'Manage org groups', + icon: 'users-alt', + url: '/org/teams', + }, + { + id: 'plugins', + text: 'Plugins', + description: 'View and configure plugins', + icon: 'plug', + url: '/plugins', + }, + { + id: 'org-settings', + text: 'Preferences', + description: 'Organization preferences', + icon: 'sliders-v-alt', + url: '/org', + }, + { + id: 'apikeys', + text: 'API keys', + description: 'Create & manage API keys', + icon: 'key-skeleton-alt', + url: '/org/apikeys', + }, + ], + }, + }, + plugins: { + id: 'plugins', + text: 'Plugins', + description: 'View and configure plugins', + icon: 'plug', + url: '/plugins', + parentItem: { + id: 'cfg', + text: 'Configuration', + section: 'config', + subTitle: 'Organization: Main Org.', + icon: 'cog', + url: '/datasources', + sortWeight: -1300, + children: [ + { + id: 'datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/datasources', + }, + { + id: 'users', + text: 'Users', + description: 'Manage org members', + icon: 'user', + url: '/org/users', + }, + { + id: 'teams', + text: 'Teams', + description: 'Manage org groups', + icon: 'users-alt', + url: '/org/teams', + }, + { + id: 'plugins', + text: 'Plugins', + description: 'View and configure plugins', + icon: 'plug', + url: '/plugins', + }, + { + id: 'org-settings', + text: 'Preferences', + description: 'Organization preferences', + icon: 'sliders-v-alt', + url: '/org', + }, + { + id: 'apikeys', + text: 'API keys', + description: 'Create & manage API keys', + icon: 'key-skeleton-alt', + url: '/org/apikeys', + }, + ], + }, + }, + 'org-settings': { + id: 'org-settings', + text: 'Preferences', + description: 'Organization preferences', + icon: 'sliders-v-alt', + url: '/org', + parentItem: { + id: 'cfg', + text: 'Configuration', + section: 'config', + subTitle: 'Organization: Main Org.', + icon: 'cog', + url: '/datasources', + sortWeight: -1300, + children: [ + { + id: 'datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/datasources', + }, + { + id: 'users', + text: 'Users', + description: 'Manage org members', + icon: 'user', + url: '/org/users', + }, + { + id: 'teams', + text: 'Teams', + description: 'Manage org groups', + icon: 'users-alt', + url: '/org/teams', + }, + { + id: 'plugins', + text: 'Plugins', + description: 'View and configure plugins', + icon: 'plug', + url: '/plugins', + }, + { + id: 'org-settings', + text: 'Preferences', + description: 'Organization preferences', + icon: 'sliders-v-alt', + url: '/org', + }, + { + id: 'apikeys', + text: 'API keys', + description: 'Create & manage API keys', + icon: 'key-skeleton-alt', + url: '/org/apikeys', + }, + ], + }, + }, + apikeys: { + id: 'apikeys', + text: 'API keys', + description: 'Create & manage API keys', + icon: 'key-skeleton-alt', + url: '/org/apikeys', + parentItem: { + id: 'cfg', + text: 'Configuration', + section: 'config', + subTitle: 'Organization: Main Org.', + icon: 'cog', + url: '/datasources', + sortWeight: -1300, + children: [ + { + id: 'datasources', + text: 'Data sources', + description: 'Add and configure data sources', + icon: 'database', + url: '/datasources', + }, + { + id: 'users', + text: 'Users', + description: 'Manage org members', + icon: 'user', + url: '/org/users', + }, + { + id: 'teams', + text: 'Teams', + description: 'Manage org groups', + icon: 'users-alt', + url: '/org/teams', + }, + { + id: 'plugins', + text: 'Plugins', + description: 'View and configure plugins', + icon: 'plug', + url: '/plugins', + }, + { + id: 'org-settings', + text: 'Preferences', + description: 'Organization preferences', + icon: 'sliders-v-alt', + url: '/org', + }, + { + id: 'apikeys', + text: 'API keys', + description: 'Create & manage API keys', + icon: 'key-skeleton-alt', + url: '/org/apikeys', + }, + ], + }, + }, + admin: { + id: 'admin', + text: 'Server Admin', + section: 'config', + subTitle: 'Manage all users and orgs', + icon: 'shield', + url: '/admin/users', + sortWeight: -1200, + hideFromTabs: true, + children: [ + { + id: 'global-users', + text: 'Users', + icon: 'user', + url: '/admin/users', + }, + { + id: 'global-orgs', + text: 'Orgs', + icon: 'building', + url: '/admin/orgs', + }, + { + id: 'server-settings', + text: 'Settings', + icon: 'sliders-v-alt', + url: '/admin/settings', + }, + { + id: 'admin-plugins', + text: 'Plugins', + icon: 'plug', + url: '/admin/plugins', + }, + { + id: 'upgrading', + text: 'Stats and license', + icon: 'unlock', + url: '/admin/upgrading', + }, + ], + }, + 'global-users': { + id: 'global-users', + text: 'Users', + icon: 'user', + url: '/admin/users', + parentItem: { + id: 'admin', + text: 'Server Admin', + section: 'config', + subTitle: 'Manage all users and orgs', + icon: 'shield', + url: '/admin/users', + sortWeight: -1200, + hideFromTabs: true, + children: [ + { + id: 'global-users', + text: 'Users', + icon: 'user', + url: '/admin/users', + }, + { + id: 'global-orgs', + text: 'Orgs', + icon: 'building', + url: '/admin/orgs', + }, + { + id: 'server-settings', + text: 'Settings', + icon: 'sliders-v-alt', + url: '/admin/settings', + }, + { + id: 'admin-plugins', + text: 'Plugins', + icon: 'plug', + url: '/admin/plugins', + }, + { + id: 'upgrading', + text: 'Stats and license', + icon: 'unlock', + url: '/admin/upgrading', + }, + ], + }, + }, + 'global-orgs': { + id: 'global-orgs', + text: 'Orgs', + icon: 'building', + url: '/admin/orgs', + parentItem: { + id: 'admin', + text: 'Server Admin', + section: 'config', + subTitle: 'Manage all users and orgs', + icon: 'shield', + url: '/admin/users', + sortWeight: -1200, + hideFromTabs: true, + children: [ + { + id: 'global-users', + text: 'Users', + icon: 'user', + url: '/admin/users', + }, + { + id: 'global-orgs', + text: 'Orgs', + icon: 'building', + url: '/admin/orgs', + }, + { + id: 'server-settings', + text: 'Settings', + icon: 'sliders-v-alt', + url: '/admin/settings', + }, + { + id: 'admin-plugins', + text: 'Plugins', + icon: 'plug', + url: '/admin/plugins', + }, + { + id: 'upgrading', + text: 'Stats and license', + icon: 'unlock', + url: '/admin/upgrading', + }, + ], + }, + }, + 'server-settings': { + id: 'server-settings', + text: 'Settings', + icon: 'sliders-v-alt', + url: '/admin/settings', + parentItem: { + id: 'admin', + text: 'Server Admin', + section: 'config', + subTitle: 'Manage all users and orgs', + icon: 'shield', + url: '/admin/users', + sortWeight: -1200, + hideFromTabs: true, + children: [ + { + id: 'global-users', + text: 'Users', + icon: 'user', + url: '/admin/users', + }, + { + id: 'global-orgs', + text: 'Orgs', + icon: 'building', + url: '/admin/orgs', + }, + { + id: 'server-settings', + text: 'Settings', + icon: 'sliders-v-alt', + url: '/admin/settings', + }, + { + id: 'admin-plugins', + text: 'Plugins', + icon: 'plug', + url: '/admin/plugins', + }, + { + id: 'upgrading', + text: 'Stats and license', + icon: 'unlock', + url: '/admin/upgrading', + }, + ], + }, + }, + 'admin-plugins': { + id: 'admin-plugins', + text: 'Plugins', + icon: 'plug', + url: '/admin/plugins', + parentItem: { + id: 'admin', + text: 'Server Admin', + section: 'config', + subTitle: 'Manage all users and orgs', + icon: 'shield', + url: '/admin/users', + sortWeight: -1200, + hideFromTabs: true, + children: [ + { + id: 'global-users', + text: 'Users', + icon: 'user', + url: '/admin/users', + }, + { + id: 'global-orgs', + text: 'Orgs', + icon: 'building', + url: '/admin/orgs', + }, + { + id: 'server-settings', + text: 'Settings', + icon: 'sliders-v-alt', + url: '/admin/settings', + }, + { + id: 'admin-plugins', + text: 'Plugins', + icon: 'plug', + url: '/admin/plugins', + }, + { + id: 'upgrading', + text: 'Stats and license', + icon: 'unlock', + url: '/admin/upgrading', + }, + ], + }, + }, + upgrading: { + id: 'upgrading', + text: 'Stats and license', + icon: 'unlock', + url: '/admin/upgrading', + parentItem: { + id: 'admin', + text: 'Server Admin', + section: 'config', + subTitle: 'Manage all users and orgs', + icon: 'shield', + url: '/admin/users', + sortWeight: -1200, + hideFromTabs: true, + children: [ + { + id: 'global-users', + text: 'Users', + icon: 'user', + url: '/admin/users', + }, + { + id: 'global-orgs', + text: 'Orgs', + icon: 'building', + url: '/admin/orgs', + }, + { + id: 'server-settings', + text: 'Settings', + icon: 'sliders-v-alt', + url: '/admin/settings', + }, + { + id: 'admin-plugins', + text: 'Plugins', + icon: 'plug', + url: '/admin/plugins', + }, + { + id: 'upgrading', + text: 'Stats and license', + icon: 'unlock', + url: '/admin/upgrading', + }, + ], + }, + }, + profile: { + id: 'profile', + text: 'admin', + section: 'config', + img: '/avatar/46d229b033af06a191ff2267bca9ae56', + url: '/profile', + sortWeight: -1100, + children: [ + { + id: 'profile-settings', + text: 'Preferences', + icon: 'sliders-v-alt', + url: '/profile', + }, + { + id: 'notifications', + text: 'Notification history', + icon: 'bell', + url: '/notifications', + }, + { + id: 'change-password', + text: 'Change password', + icon: 'lock', + url: '/profile/password', + }, + { + id: 'sign-out', + text: 'Sign out', + icon: 'arrow-from-right', + url: '/logout', + target: '_self', + hideFromTabs: true, + }, + ], + }, + 'profile-settings': { + id: 'profile-settings', + text: 'Preferences', + icon: 'sliders-v-alt', + url: '/profile', + parentItem: { + id: 'profile', + text: 'admin', + section: 'config', + img: '/avatar/46d229b033af06a191ff2267bca9ae56', + url: '/profile', + sortWeight: -1100, + children: [ + { + id: 'profile-settings', + text: 'Preferences', + icon: 'sliders-v-alt', + url: '/profile', + }, + { + id: 'notifications', + text: 'Notification history', + icon: 'bell', + url: '/notifications', + }, + { + id: 'change-password', + text: 'Change password', + icon: 'lock', + url: '/profile/password', + }, + { + id: 'sign-out', + text: 'Sign out', + icon: 'arrow-from-right', + url: '/logout', + target: '_self', + hideFromTabs: true, + }, + ], + }, + }, + notifications: { + id: 'notifications', + text: 'Notification history', + icon: 'bell', + url: '/notifications', + parentItem: { + id: 'profile', + text: 'admin', + section: 'config', + img: '/avatar/46d229b033af06a191ff2267bca9ae56', + url: '/profile', + sortWeight: -1100, + children: [ + { + id: 'profile-settings', + text: 'Preferences', + icon: 'sliders-v-alt', + url: '/profile', + }, + { + id: 'notifications', + text: 'Notification history', + icon: 'bell', + url: '/notifications', + }, + { + id: 'change-password', + text: 'Change password', + icon: 'lock', + url: '/profile/password', + }, + { + id: 'sign-out', + text: 'Sign out', + icon: 'arrow-from-right', + url: '/logout', + target: '_self', + hideFromTabs: true, + }, + ], + }, + }, + 'change-password': { + id: 'change-password', + text: 'Change password', + icon: 'lock', + url: '/profile/password', + parentItem: { + id: 'profile', + text: 'admin', + section: 'config', + img: '/avatar/46d229b033af06a191ff2267bca9ae56', + url: '/profile', + sortWeight: -1100, + children: [ + { + id: 'profile-settings', + text: 'Preferences', + icon: 'sliders-v-alt', + url: '/profile', + }, + { + id: 'notifications', + text: 'Notification history', + icon: 'bell', + url: '/notifications', + }, + { + id: 'change-password', + text: 'Change password', + icon: 'lock', + url: '/profile/password', + }, + { + id: 'sign-out', + text: 'Sign out', + icon: 'arrow-from-right', + url: '/logout', + target: '_self', + hideFromTabs: true, + }, + ], + }, + }, + 'sign-out': { + id: 'sign-out', + text: 'Sign out', + icon: 'arrow-from-right', + url: '/logout', + target: '_self', + hideFromTabs: true, + parentItem: { + id: 'profile', + text: 'admin', + section: 'config', + img: '/avatar/46d229b033af06a191ff2267bca9ae56', + url: '/profile', + sortWeight: -1100, + children: [ + { + id: 'profile-settings', + text: 'Preferences', + icon: 'sliders-v-alt', + url: '/profile', + }, + { + id: 'notifications', + text: 'Notification history', + icon: 'bell', + url: '/notifications', + }, + { + id: 'change-password', + text: 'Change password', + icon: 'lock', + url: '/profile/password', + }, + { + id: 'sign-out', + text: 'Sign out', + icon: 'arrow-from-right', + url: '/logout', + target: '_self', + hideFromTabs: true, + }, + ], + }, + }, + help: { + id: 'help', + text: 'Help', + section: 'config', + subTitle: 'Grafana v9.0.0-pre (abb5c6109a)', + icon: 'question-circle', + url: '#', + sortWeight: -1000, + }, +}; diff --git a/public/app/features/data-connections/constants.ts b/public/app/features/data-connections/constants.ts new file mode 100644 index 00000000000..c35a30aba1b --- /dev/null +++ b/public/app/features/data-connections/constants.ts @@ -0,0 +1,12 @@ +// The ID of the app plugin that we render under that "Cloud Integrations" tab +export const CLOUD_ONBOARDING_APP_ID = 'grafana-easystart-app'; + +// The ID of the main nav-tree item (the main item in the NavIndex) +export const ROUTE_BASE_ID = 'data-connections'; + +export enum ROUTES { + DataSources = '/data-connections/data-sources', + Plugins = '/data-connections/plugins', + CloudIntegrations = '/data-connections/cloud-integrations', + RecordedQueries = '/data-connections/recorded-queries', +} diff --git a/public/app/features/data-connections/hooks/useNavModel.ts b/public/app/features/data-connections/hooks/useNavModel.ts new file mode 100644 index 00000000000..b030c3e0b66 --- /dev/null +++ b/public/app/features/data-connections/hooks/useNavModel.ts @@ -0,0 +1,26 @@ +import { useSelector } from 'react-redux'; +import { useLocation } from 'react-router-dom'; + +import { StoreState } from 'app/types/store'; + +import { ROUTE_BASE_ID } from '../constants'; + +// We need this utility logic to make sure that the tab with the current URL is marked as active. +// (In case we were using `getNavModel()` from app/core/selectors/navModel, then we would need to set +// the child nav-model-item's ID on the call-site.) +export const useNavModel = () => { + const { pathname } = useLocation(); + const navIndex = useSelector((state: StoreState) => state.navIndex); + const node = navIndex[ROUTE_BASE_ID]; + const main = node; + + main.children = main.children?.map((item) => ({ + ...item, + active: item.url === pathname, + })); + + return { + node, + main, + }; +}; diff --git a/public/app/features/data-connections/routes.tsx b/public/app/features/data-connections/routes.tsx new file mode 100644 index 00000000000..5366ec53caa --- /dev/null +++ b/public/app/features/data-connections/routes.tsx @@ -0,0 +1,21 @@ +import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport'; +import { config } from 'app/core/config'; +import { RouteDescriptor } from 'app/core/navigation/types'; + +import { ROUTE_BASE_ID } from './constants'; + +export function getRoutes(): RouteDescriptor[] { + if (config.featureToggles.dataConnectionsConsole) { + return [ + { + path: `/${ROUTE_BASE_ID}`, + exact: false, + component: SafeDynamicImport( + () => import(/* webpackChunkName: "DataConnectionsPage"*/ 'app/features/data-connections/DataConnectionsPage') + ), + }, + ]; + } + + return []; +} diff --git a/public/app/features/data-connections/tabs/CloudIntegrations/CloudIntegrations.tsx b/public/app/features/data-connections/tabs/CloudIntegrations/CloudIntegrations.tsx new file mode 100644 index 00000000000..c912fb8d9b7 --- /dev/null +++ b/public/app/features/data-connections/tabs/CloudIntegrations/CloudIntegrations.tsx @@ -0,0 +1,25 @@ +import { css } from '@emotion/css'; +import React, { ReactElement } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { useStyles2 } from '@grafana/ui'; +import { AppPluginLoader } from 'app/features/plugins/components/AppPluginLoader'; + +import { CLOUD_ONBOARDING_APP_ID } from '../../constants'; + +export function CloudIntegrations(): ReactElement | null { + const s = useStyles2(getStyles); + + return ( +
+ +
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + // We would like to force the app to stay inside the provided tab + container: css` + position: relative; + `, +}); diff --git a/public/app/features/data-connections/tabs/CloudIntegrations/index.tsx b/public/app/features/data-connections/tabs/CloudIntegrations/index.tsx new file mode 100644 index 00000000000..41c904956aa --- /dev/null +++ b/public/app/features/data-connections/tabs/CloudIntegrations/index.tsx @@ -0,0 +1 @@ +export * from './CloudIntegrations'; diff --git a/public/app/features/data-connections/tabs/DataSources/DataSources.tsx b/public/app/features/data-connections/tabs/DataSources/DataSources.tsx new file mode 100644 index 00000000000..a809c21511e --- /dev/null +++ b/public/app/features/data-connections/tabs/DataSources/DataSources.tsx @@ -0,0 +1,5 @@ +import React, { ReactElement } from 'react'; + +export function DataSources(): ReactElement | null { + return
The list of data sources is under development.
; +} diff --git a/public/app/features/data-connections/tabs/DataSources/index.tsx b/public/app/features/data-connections/tabs/DataSources/index.tsx new file mode 100644 index 00000000000..5d681837186 --- /dev/null +++ b/public/app/features/data-connections/tabs/DataSources/index.tsx @@ -0,0 +1 @@ +export * from './DataSources'; diff --git a/public/app/features/data-connections/tabs/Plugins/Plugins.tsx b/public/app/features/data-connections/tabs/Plugins/Plugins.tsx new file mode 100644 index 00000000000..e1befaab13f --- /dev/null +++ b/public/app/features/data-connections/tabs/Plugins/Plugins.tsx @@ -0,0 +1,5 @@ +import React, { ReactElement } from 'react'; + +export function Plugins(): ReactElement | null { + return
The list of plugins is under development
; +} diff --git a/public/app/features/data-connections/tabs/Plugins/index.tsx b/public/app/features/data-connections/tabs/Plugins/index.tsx new file mode 100644 index 00000000000..20c2507f6d8 --- /dev/null +++ b/public/app/features/data-connections/tabs/Plugins/index.tsx @@ -0,0 +1 @@ +export * from './Plugins'; diff --git a/public/app/features/data-connections/tabs/RecordedQueries/RecordedQueries.tsx b/public/app/features/data-connections/tabs/RecordedQueries/RecordedQueries.tsx new file mode 100644 index 00000000000..90ec2c24f76 --- /dev/null +++ b/public/app/features/data-connections/tabs/RecordedQueries/RecordedQueries.tsx @@ -0,0 +1,5 @@ +import React, { ReactElement } from 'react'; + +export function RecordedQueries(): ReactElement | null { + return
The recorded queries tab is under development.
; +} diff --git a/public/app/features/data-connections/tabs/RecordedQueries/index.tsx b/public/app/features/data-connections/tabs/RecordedQueries/index.tsx new file mode 100644 index 00000000000..fc4ff37714f --- /dev/null +++ b/public/app/features/data-connections/tabs/RecordedQueries/index.tsx @@ -0,0 +1 @@ +export * from './RecordedQueries'; diff --git a/public/app/features/plugins/components/AppPluginLoader.test.tsx b/public/app/features/plugins/components/AppPluginLoader.test.tsx new file mode 100644 index 00000000000..4c1430c7372 --- /dev/null +++ b/public/app/features/plugins/components/AppPluginLoader.test.tsx @@ -0,0 +1,132 @@ +import { render, screen } from '@testing-library/react'; +import React, { Component } from 'react'; +import { Router } from 'react-router-dom'; + +import { AppPlugin, PluginType, AppRootProps, NavModelItem } from '@grafana/data'; +import { locationService, setEchoSrv } from '@grafana/runtime'; +import { Echo } from 'app/core/services/echo/Echo'; + +import { getMockPlugin } from '../__mocks__/pluginMocks'; +import { useImportAppPlugin } from '../hooks/useImportAppPlugin'; + +import { AppPluginLoader } from './AppPluginLoader'; + +jest.mock('../hooks/useImportAppPlugin', () => ({ + useImportAppPlugin: jest.fn(), +})); + +const useImportAppPluginMock = useImportAppPlugin as jest.Mock< + ReturnType, + Parameters +>; + +const TEXTS = { + PLUGIN_TITLE: 'Amazing App', + PLUGIN_CONTENT: 'This is my amazing app plugin!', + PLUGIN_TAB_TITLE_A: 'Tab (A)', + PLUGIN_TAB_TITLE_B: 'Tab (B)', +}; + +describe('AppPluginLoader', () => { + beforeEach(() => { + jest.resetAllMocks(); + AppPluginComponent.timesMounted = 0; + setEchoSrv(new Echo()); + }); + + test('renders the app plugin correctly', async () => { + useImportAppPluginMock.mockReturnValue({ value: getAppPluginMock(), loading: false, error: undefined }); + + renderAppPlugin(); + + expect(await screen.findByText(TEXTS.PLUGIN_TITLE)).toBeVisible(); + expect(await screen.findByText(TEXTS.PLUGIN_CONTENT)).toBeVisible(); + expect(await screen.findByLabelText(`Tab ${TEXTS.PLUGIN_TAB_TITLE_A}`)).toBeVisible(); + expect(await screen.findByLabelText(`Tab ${TEXTS.PLUGIN_TAB_TITLE_B}`)).toBeVisible(); + expect(screen.queryByText('Loading ...')).not.toBeInTheDocument(); + }); + + test('renders the app plugin only once', async () => { + useImportAppPluginMock.mockReturnValue({ value: getAppPluginMock(), loading: false, error: undefined }); + + renderAppPlugin(); + + expect(await screen.findByText(TEXTS.PLUGIN_TITLE)).toBeVisible(); + expect(AppPluginComponent.timesMounted).toEqual(1); + }); + + test('renders a loader while the plugin is loading', async () => { + useImportAppPluginMock.mockReturnValue({ value: undefined, loading: true, error: undefined }); + + renderAppPlugin(); + + expect(await screen.findByText('Loading ...')).toBeVisible(); + expect(screen.queryByText(TEXTS.PLUGIN_TITLE)).not.toBeInTheDocument(); + }); + + test('renders an error message if there are any errors while importing the plugin', async () => { + const errorMsg = 'Unable to find plugin'; + useImportAppPluginMock.mockReturnValue({ value: undefined, loading: false, error: new Error(errorMsg) }); + + renderAppPlugin(); + + expect(await screen.findByText(errorMsg)).toBeVisible(); + expect(screen.queryByText(TEXTS.PLUGIN_TITLE)).not.toBeInTheDocument(); + }); +}); + +function renderAppPlugin() { + render( + + ; + + ); +} +class AppPluginComponent extends Component { + static timesMounted = 0; + + componentDidMount() { + AppPluginComponent.timesMounted += 1; + + const node: NavModelItem = { + text: TEXTS.PLUGIN_TITLE, + children: [ + { + text: TEXTS.PLUGIN_TAB_TITLE_A, + url: '/tab-a', + id: 'a', + }, + { + text: TEXTS.PLUGIN_TAB_TITLE_B, + url: '/tab-b', + id: 'b', + }, + ], + }; + + this.props.onNavChanged({ + main: node, + node, + }); + } + + render() { + return

{TEXTS.PLUGIN_CONTENT}

; + } +} + +function getAppPluginMeta() { + return getMockPlugin({ + type: PluginType.app, + enabled: true, + }); +} + +function getAppPluginMock() { + const plugin = new AppPlugin(); + + plugin.root = AppPluginComponent; + plugin.init(getAppPluginMeta()); + + return plugin; +} diff --git a/public/app/features/plugins/components/AppPluginLoader.tsx b/public/app/features/plugins/components/AppPluginLoader.tsx new file mode 100644 index 00000000000..fe263276930 --- /dev/null +++ b/public/app/features/plugins/components/AppPluginLoader.tsx @@ -0,0 +1,44 @@ +import React, { useState } from 'react'; +import { useLocation, useParams } from 'react-router-dom'; + +import { NavModel } from '@grafana/data'; +import { getWarningNav } from 'app/angular/services/nav_model_srv'; +import Page from 'app/core/components/Page/Page'; +import PageLoader from 'app/core/components/PageLoader/PageLoader'; + +import { useImportAppPlugin } from '../hooks/useImportAppPlugin'; + +type AppPluginLoaderProps = { + // The id of the app plugin to be loaded + id: string; + // The base URL path - defaults to the current path + basePath?: string; +}; + +// This component can be used to render an app-plugin based on its plugin ID. +export const AppPluginLoader = ({ id, basePath }: AppPluginLoaderProps) => { + const [nav, setNav] = useState(null); + const { value: plugin, error, loading } = useImportAppPlugin(id); + const queryParams = useParams(); + const { pathname } = useLocation(); + + if (error) { + return ; + } + + return ( + <> + {loading && } + {nav && } + {!loading && plugin && plugin.root && ( + + )} + + ); +}; diff --git a/public/app/features/plugins/hooks/tests/useImportAppPlugin.test.tsx b/public/app/features/plugins/hooks/tests/useImportAppPlugin.test.tsx new file mode 100644 index 00000000000..5d90c9700ce --- /dev/null +++ b/public/app/features/plugins/hooks/tests/useImportAppPlugin.test.tsx @@ -0,0 +1,150 @@ +import { render, act, waitFor } from '@testing-library/react'; +import React from 'react'; + +import { AppPlugin, PluginType } from '@grafana/data'; + +import { getMockPlugin } from '../../__mocks__/pluginMocks'; +import { getPluginSettings } from '../../pluginSettings'; +import { importAppPlugin } from '../../plugin_loader'; +import { useImportAppPlugin } from '../useImportAppPlugin'; + +jest.mock('../../pluginSettings', () => ({ + getPluginSettings: jest.fn(), +})); +jest.mock('../../plugin_loader', () => ({ + importAppPlugin: jest.fn(), +})); + +const importAppPluginMock = importAppPlugin as jest.Mock< + ReturnType, + Parameters +>; + +const getPluginSettingsMock = getPluginSettings as jest.Mock< + ReturnType, + Parameters +>; + +const PLUGIN_ID = 'sample-plugin'; + +describe('useImportAppPlugin()', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + test('returns the imported plugin in case it exists', async () => { + let response: any; + getPluginSettingsMock.mockResolvedValue(getAppPluginMeta()); + importAppPluginMock.mockResolvedValue(getAppPluginMock()); + + act(() => { + response = runHook(PLUGIN_ID); + }); + + await waitFor(() => expect(response.value).not.toBeUndefined()); + await waitFor(() => expect(response.error).toBeUndefined()); + await waitFor(() => expect(response.loading).toBe(false)); + }); + + test('returns an error if the plugin does not exist', async () => { + let response: any; + + act(() => { + response = runHook(PLUGIN_ID); + }); + + await waitFor(() => expect(response.value).toBeUndefined()); + await waitFor(() => expect(response.error).not.toBeUndefined()); + await waitFor(() => expect(response.error.message).toMatch(/unknown plugin/i)); + await waitFor(() => expect(response.loading).toBe(false)); + }); + + test('returns an error if the plugin is not an app', async () => { + let response: any; + getPluginSettingsMock.mockResolvedValue(getAppPluginMeta({ type: PluginType.panel })); + importAppPluginMock.mockResolvedValue(getAppPluginMock()); + + act(() => { + response = runHook(PLUGIN_ID); + }); + + await waitFor(() => expect(response.value).toBeUndefined()); + await waitFor(() => expect(response.error).not.toBeUndefined()); + await waitFor(() => expect(response.error.message).toMatch(/plugin must be an app/i)); + await waitFor(() => expect(response.loading).toBe(false)); + }); + + test('returns an error if the plugin is not enabled', async () => { + let response: any; + getPluginSettingsMock.mockResolvedValue(getAppPluginMeta({ enabled: false })); + importAppPluginMock.mockResolvedValue(getAppPluginMock()); + + act(() => { + response = runHook(PLUGIN_ID); + }); + + await waitFor(() => expect(response.value).toBeUndefined()); + await waitFor(() => expect(response.error).not.toBeUndefined()); + await waitFor(() => expect(response.error.message).toMatch(/is not enabled/i)); + await waitFor(() => expect(response.loading).toBe(false)); + }); + + test('returns errors that happen during fetching plugin settings', async () => { + let response: any; + const errorMsg = 'Error while fetching plugin data'; + getPluginSettingsMock.mockRejectedValue(new Error(errorMsg)); + importAppPluginMock.mockResolvedValue(getAppPluginMock()); + + act(() => { + response = runHook(PLUGIN_ID); + }); + + await waitFor(() => expect(response.value).toBeUndefined()); + await waitFor(() => expect(response.error).not.toBeUndefined()); + await waitFor(() => expect(response.error.message).toBe(errorMsg)); + await waitFor(() => expect(response.loading).toBe(false)); + }); + + test('returns errors that happen during importing a plugin', async () => { + let response: any; + const errorMsg = 'Error while importing the plugin'; + getPluginSettingsMock.mockResolvedValue(getAppPluginMeta()); + importAppPluginMock.mockRejectedValue(new Error(errorMsg)); + + act(() => { + response = runHook(PLUGIN_ID); + }); + + await waitFor(() => expect(response.value).toBeUndefined()); + await waitFor(() => expect(response.error).not.toBeUndefined()); + await waitFor(() => expect(response.error.message).toBe(errorMsg)); + await waitFor(() => expect(response.loading).toBe(false)); + }); +}); + +function runHook(id: string): any { + const returnVal = {}; + function TestComponent() { + Object.assign(returnVal, useImportAppPlugin(id)); + return null; + } + render(); + return returnVal; +} + +function getAppPluginMeta(overrides?: Record) { + return getMockPlugin({ + id: PLUGIN_ID, + type: PluginType.app, + enabled: true, + ...overrides, + }); +} + +function getAppPluginMock() { + const plugin = new AppPlugin(); + + plugin.init(getAppPluginMeta()); + + return plugin; +} diff --git a/public/app/features/plugins/hooks/useImportAppPlugin.ts b/public/app/features/plugins/hooks/useImportAppPlugin.ts new file mode 100644 index 00000000000..cd48d9ce22f --- /dev/null +++ b/public/app/features/plugins/hooks/useImportAppPlugin.ts @@ -0,0 +1,26 @@ +import useAsync from 'react-use/lib/useAsync'; + +import { PluginType } from '@grafana/data'; + +import { getPluginSettings } from '../pluginSettings'; +import { importAppPlugin } from '../plugin_loader'; + +export const useImportAppPlugin = (id: string) => { + return useAsync(async () => { + const pluginMeta = await getPluginSettings(id); + + if (!pluginMeta) { + throw new Error(`Unknown plugin: "${id}"`); + } + + if (pluginMeta.type !== PluginType.app) { + throw new Error(`Plugin must be an app (currently "${pluginMeta.type}")`); + } + + if (!pluginMeta.enabled) { + throw new Error(`Application "${id}" is not enabled`); + } + + return await importAppPlugin(pluginMeta); + }); +}; diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 420d887f8af..e69f684807a 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -8,6 +8,7 @@ import { contextSrv } from 'app/core/services/context_srv'; import UserAdminPage from 'app/features/admin/UserAdminPage'; import LdapPage from 'app/features/admin/ldap/LdapPage'; import { getAlertingRoutes } from 'app/features/alerting/routes'; +import { getRoutes as getDataConnectionsRoutes } from 'app/features/data-connections/routes'; import { getLiveRoutes } from 'app/features/live/pages/routes'; import { getRoutes as getPluginCatalogRoutes } from 'app/features/plugins/admin/routes'; import { getProfileRoutes } from 'app/features/profile/routes'; @@ -431,6 +432,7 @@ export function getAppRoutes(): RouteDescriptor[] { ...getProfileRoutes(), ...extraRoutes, ...getPublicDashboardRoutes(), + ...getDataConnectionsRoutes(), { path: '/*', component: ErrorPage,