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,