diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go
index fbe13fc8d73..be880be8dad 100644
--- a/pkg/services/navtree/navtreeimpl/navtree.go
+++ b/pkg/services/navtree/navtreeimpl/navtree.go
@@ -546,19 +546,11 @@ func (s *ServiceImpl) buildDataConnectionsNavLink(c *models.ReqContext) *navtree
})
children = append(children, &navtree.NavLink{
- Id: baseId + "-plugins",
- Text: "Plugins",
+ Id: baseId + "-connect-data",
+ Text: "Connect Data",
Icon: "plug",
- SubTitle: "Manage plugins",
- Url: baseUrl + "/plugins",
- })
-
- children = append(children, &navtree.NavLink{
- Id: baseId + "-cloud-integrations",
- Text: "Cloud integrations",
- Icon: "bolt",
- SubTitle: "Manage your cloud integrations",
- Url: baseUrl + "/cloud-integrations",
+ SubTitle: "Manage data sources",
+ Url: baseUrl + "/connect-data",
})
navLink = &navtree.NavLink{
diff --git a/public/app/features/connections/ConnectionsPage.test.tsx b/public/app/features/connections/ConnectionsPage.test.tsx
index 7e1c33fa469..489b581dad9 100644
--- a/public/app/features/connections/ConnectionsPage.test.tsx
+++ b/public/app/features/connections/ConnectionsPage.test.tsx
@@ -8,6 +8,8 @@ import { getMockDataSources } from 'app/features/datasources/__mocks__';
import * as api from 'app/features/datasources/api';
import { configureStore } from 'app/store/configureStore';
+import { getPluginsStateMock } from '../plugins/admin/__mocks__';
+
import ConnectionsPage from './ConnectionsPage';
import { navIndex } from './__mocks__/store.navIndex.mock';
import { ROUTE_BASE_ID, ROUTES } from './constants';
@@ -16,7 +18,7 @@ jest.mock('app/features/datasources/api');
const renderPage = (path = `/${ROUTE_BASE_ID}`): RenderResult => {
// @ts-ignore
- const store = configureStore({ navIndex });
+ const store = configureStore({ navIndex, plugins: getPluginsStateMock([]) });
locationService.push(path);
return render(
@@ -35,13 +37,11 @@ describe('Connections Page', () => {
(api.getDataSources as jest.Mock) = jest.fn().mockResolvedValue(mockDatasources);
});
- test('shows all the four tabs', async () => {
+ test('shows all 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();
+ expect(await screen.findByLabelText('Tab Connect Data')).toBeVisible();
});
test('shows the "Data sources" tab by default', async () => {
@@ -52,10 +52,8 @@ describe('Connections Page', () => {
});
test('renders the correct tab even if accessing it with a "sub-url"', async () => {
- renderPage(`${ROUTES.Plugins}/foo`);
+ renderPage(`${ROUTES.ConnectData}`);
- // Check if it still renders the plugins tab
- expect(await screen.findByText('The list of plugins is under development')).toBeVisible();
- expect(screen.queryByText('The list of data sources is under development.')).not.toBeInTheDocument();
+ expect(screen.queryByText('No results matching your query were found.')).toBeInTheDocument();
});
});
diff --git a/public/app/features/connections/ConnectionsPage.tsx b/public/app/features/connections/ConnectionsPage.tsx
index 410e4eebf28..74849e6b105 100644
--- a/public/app/features/connections/ConnectionsPage.tsx
+++ b/public/app/features/connections/ConnectionsPage.tsx
@@ -8,9 +8,8 @@ import { DataSourcesRoutesContext } from 'app/features/datasources/state';
import { ROUTES } from './constants';
import { useNavModel } from './hooks/useNavModel';
-import { CloudIntegrations } from './tabs/CloudIntegrations';
+import { ConnectData } from './tabs/ConnectData';
import { DataSourcesEdit } from './tabs/DataSourcesEdit';
-import { Plugins } from './tabs/Plugins';
export default function ConnectionsPage() {
const navModel = useNavModel();
@@ -30,8 +29,7 @@ export default function ConnectionsPage() {
-
-
+
{/* Default page */}
diff --git a/public/app/features/connections/__mocks__/store.navIndex.mock.ts b/public/app/features/connections/__mocks__/store.navIndex.mock.ts
index 7ce642f2493..6b81e6e5fbf 100644
--- a/public/app/features/connections/__mocks__/store.navIndex.mock.ts
+++ b/public/app/features/connections/__mocks__/store.navIndex.mock.ts
@@ -1227,27 +1227,11 @@ export const navIndex: NavIndex = {
active: false,
},
{
- id: 'connections-plugins',
- text: 'Plugins',
- description: 'Manage plugins',
+ id: 'connections-connect-data',
+ text: 'Connect Data',
+ description: 'Manage data sources',
icon: 'plug',
- url: '/connections/plugins',
- active: false,
- },
- {
- id: 'connections-cloud-integrations',
- text: 'Cloud integrations',
- description: 'Manage your cloud integrations',
- icon: 'bolt',
- url: '/connections/cloud-integrations',
- active: true,
- },
- {
- id: 'connections-recorded-queries',
- text: 'Recorded queries',
- description: 'Manage your recorded queries',
- icon: 'record-audio',
- url: '/connections/recorded-queries',
+ url: '/connections/connect-data',
active: false,
},
],
diff --git a/public/app/features/connections/constants.ts b/public/app/features/connections/constants.ts
index e609bd6d929..98dfb78826d 100644
--- a/public/app/features/connections/constants.ts
+++ b/public/app/features/connections/constants.ts
@@ -9,6 +9,5 @@ export const ROUTES = {
DataSourcesNew: `/${ROUTE_BASE_ID}/datasources/new`,
DataSourcesEdit: `/${ROUTE_BASE_ID}/datasources/edit/:uid`,
DataSourcesDashboards: `/${ROUTE_BASE_ID}/datasources/edit/:uid/dashboards`,
- Plugins: `/${ROUTE_BASE_ID}/plugins`,
- CloudIntegrations: `/${ROUTE_BASE_ID}/cloud-integrations`,
+ ConnectData: `/${ROUTE_BASE_ID}/connect-data`,
} as const;
diff --git a/public/app/features/connections/tabs/CloudIntegrations/CloudIntegrations.tsx b/public/app/features/connections/tabs/CloudIntegrations/CloudIntegrations.tsx
deleted file mode 100644
index 92d44783bfa..00000000000
--- a/public/app/features/connections/tabs/CloudIntegrations/CloudIntegrations.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { css } from '@emotion/css';
-import React 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, ROUTES } from '../../constants';
-
-export function CloudIntegrations() {
- 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/connections/tabs/CloudIntegrations/index.tsx b/public/app/features/connections/tabs/CloudIntegrations/index.tsx
deleted file mode 100644
index 41c904956aa..00000000000
--- a/public/app/features/connections/tabs/CloudIntegrations/index.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export * from './CloudIntegrations';
diff --git a/public/app/features/connections/tabs/ConnectData/CardGrid/CardGrid.tsx b/public/app/features/connections/tabs/ConnectData/CardGrid/CardGrid.tsx
new file mode 100644
index 00000000000..61aa4933e13
--- /dev/null
+++ b/public/app/features/connections/tabs/ConnectData/CardGrid/CardGrid.tsx
@@ -0,0 +1,67 @@
+import { css } from '@emotion/css';
+import React, { FC } from 'react';
+
+import { GrafanaTheme2 } from '@grafana/data';
+import { Card, useStyles2 } from '@grafana/ui';
+
+const getStyles = (theme: GrafanaTheme2) => ({
+ sourcesList: css`
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ gap: 12px;
+ list-style: none;
+ margin-bottom: 80px;
+ `,
+ card: css`
+ height: 90px;
+ padding: 0px 24px;
+ box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.6);
+ `,
+ cardContent: css`
+ display: flex;
+ align-items: center;
+ `,
+ logoWrapper: css`
+ display: flex;
+ justify-content: center;
+ margin-right: 8px;
+ width: 32px;
+ height: 32px;
+ img {
+ max-width: 100%;
+ max-height: 100%;
+ align-self: center;
+ }
+ `,
+ label: css`
+ color: ${theme.colors.text.primary};
+ margin-bottom: 0;
+ `,
+});
+
+export interface CardGridProps {
+ items: Array<{ id: string; name: string; url: string; logo?: string }>;
+}
+
+export const CardGrid: FC = ({ items }) => {
+ const styles = useStyles2(getStyles);
+
+ return (
+
+ {items.map((item) => (
+
+
+
+ {item.logo && (
+
+

+
+ )}
+
{item.name}
+
+
+
+ ))}
+
+ );
+};
diff --git a/public/app/features/connections/tabs/ConnectData/CardGrid/index.tsx b/public/app/features/connections/tabs/ConnectData/CardGrid/index.tsx
new file mode 100644
index 00000000000..8ad53f84835
--- /dev/null
+++ b/public/app/features/connections/tabs/ConnectData/CardGrid/index.tsx
@@ -0,0 +1 @@
+export * from './CardGrid';
diff --git a/public/app/features/connections/tabs/ConnectData/CategoryHeader/CategoryHeader.tsx b/public/app/features/connections/tabs/ConnectData/CategoryHeader/CategoryHeader.tsx
new file mode 100644
index 00000000000..4ad2aa06d66
--- /dev/null
+++ b/public/app/features/connections/tabs/ConnectData/CategoryHeader/CategoryHeader.tsx
@@ -0,0 +1,27 @@
+import { css } from '@emotion/css';
+import React from 'react';
+
+import { GrafanaTheme2, IconName } from '@grafana/data';
+import { Icon, useStyles2 } from '@grafana/ui';
+
+const getStyles = (theme: GrafanaTheme2) => ({
+ categoryHeader: css`
+ align-items: center;
+ display: flex;
+ margin-bottom: 24px;
+ `,
+ categoryLabel: css`
+ margin-bottom: 0px;
+ margin-left: 8px;
+ `,
+});
+
+export const CategoryHeader: React.FC<{ iconName: IconName; label: string }> = ({ iconName, label }) => {
+ const styles = useStyles2(getStyles);
+ return (
+
+
+
{label}
+
+ );
+};
diff --git a/public/app/features/connections/tabs/ConnectData/CategoryHeader/index.tsx b/public/app/features/connections/tabs/ConnectData/CategoryHeader/index.tsx
new file mode 100644
index 00000000000..715e80b70b5
--- /dev/null
+++ b/public/app/features/connections/tabs/ConnectData/CategoryHeader/index.tsx
@@ -0,0 +1 @@
+export * from './CategoryHeader';
diff --git a/public/app/features/connections/tabs/ConnectData/ConnectData.test.tsx b/public/app/features/connections/tabs/ConnectData/ConnectData.test.tsx
new file mode 100644
index 00000000000..e0e293e3986
--- /dev/null
+++ b/public/app/features/connections/tabs/ConnectData/ConnectData.test.tsx
@@ -0,0 +1,48 @@
+import { fireEvent, render, RenderResult, screen } from '@testing-library/react';
+import React from 'react';
+import { Provider } from 'react-redux';
+
+import { CatalogPlugin } from 'app/features/plugins/admin/types';
+import { configureStore } from 'app/store/configureStore';
+
+import { getCatalogPluginMock, getPluginsStateMock } from '../../../plugins/admin/__mocks__';
+
+import { ConnectData } from './ConnectData';
+
+jest.mock('app/features/datasources/api');
+
+const renderPage = (plugins: CatalogPlugin[] = []): RenderResult => {
+ // @ts-ignore
+ const store = configureStore({ plugins: getPluginsStateMock(plugins) });
+
+ return render(
+
+
+
+ );
+};
+
+describe('Connect Data', () => {
+ test('renders no results if the plugins list is empty', async () => {
+ renderPage();
+
+ expect(screen.queryByText('No results matching your query were found.')).toBeInTheDocument();
+ });
+
+ test('renders card if plugins list is populated', async () => {
+ renderPage([getCatalogPluginMock()]);
+
+ expect(await screen.findByText('Zabbix')).toBeVisible();
+ });
+
+ test('renders card if search term matches', async () => {
+ renderPage([getCatalogPluginMock()]);
+ const searchField = await screen.findByRole('textbox');
+
+ fireEvent.change(searchField, { target: { value: 'abbi' } });
+ expect(await screen.findByText('Zabbix')).toBeVisible();
+
+ fireEvent.change(searchField, { target: { value: 'rabbit' } });
+ expect(screen.queryByText('No results matching your query were found.')).toBeInTheDocument();
+ });
+});
diff --git a/public/app/features/connections/tabs/ConnectData/ConnectData.tsx b/public/app/features/connections/tabs/ConnectData/ConnectData.tsx
new file mode 100644
index 00000000000..24d45059ec0
--- /dev/null
+++ b/public/app/features/connections/tabs/ConnectData/ConnectData.tsx
@@ -0,0 +1,56 @@
+import { css } from '@emotion/css';
+import React, { useMemo, useState } from 'react';
+
+import { useStyles2, LoadingPlaceholder } from '@grafana/ui';
+import { useGetAllWithFilters } from 'app/features/plugins/admin/state/hooks';
+
+import { CardGrid } from './CardGrid';
+import { CategoryHeader } from './CategoryHeader';
+import { NoResults } from './NoResults';
+import { Search } from './Search';
+
+const getStyles = () => ({
+ spacer: css`
+ height: 16px;
+ `,
+});
+
+export function ConnectData() {
+ const [searchTerm, setSearchTerm] = useState('');
+ const styles = useStyles2(getStyles);
+
+ const handleSearchChange = (e: React.FormEvent) => {
+ setSearchTerm(e.currentTarget.value.toLowerCase());
+ };
+
+ const { isLoading, error, plugins } = useGetAllWithFilters({ query: searchTerm, filterBy: '' });
+
+ const cardGridItems = useMemo(
+ () =>
+ plugins.map((plugin) => ({
+ id: plugin.id,
+ name: plugin.name,
+ logo: plugin.info.logos.small,
+ url: `plugins/${plugin.id}`,
+ })),
+ [plugins]
+ );
+ const showNoResults = useMemo(() => !isLoading && !error && plugins.length < 1, [isLoading, error, plugins]);
+
+ return (
+ <>
+
+ {/* We need this extra spacing when there are no filters */}
+
+
+ {isLoading ? (
+
+ ) : !!error ? (
+ Error: {error.message}
+ ) : (
+
+ )}
+ {showNoResults && }
+ >
+ );
+}
diff --git a/public/app/features/connections/tabs/ConnectData/NoResults/NoResults.tsx b/public/app/features/connections/tabs/ConnectData/NoResults/NoResults.tsx
new file mode 100644
index 00000000000..92d80078744
--- /dev/null
+++ b/public/app/features/connections/tabs/ConnectData/NoResults/NoResults.tsx
@@ -0,0 +1,18 @@
+import { css } from '@emotion/css';
+import React, { FC } from 'react';
+
+import { useStyles2 } from '@grafana/ui';
+
+const getStyles = () => ({
+ noResults: css`
+ text-align: center;
+ padding: 50px 0;
+ font-style: italic;
+ `,
+});
+
+export const NoResults: FC = () => {
+ const styles = useStyles2(getStyles);
+
+ return No results matching your query were found.
;
+};
diff --git a/public/app/features/connections/tabs/ConnectData/NoResults/index.tsx b/public/app/features/connections/tabs/ConnectData/NoResults/index.tsx
new file mode 100644
index 00000000000..20eae4db4b6
--- /dev/null
+++ b/public/app/features/connections/tabs/ConnectData/NoResults/index.tsx
@@ -0,0 +1 @@
+export * from './NoResults';
diff --git a/public/app/features/connections/tabs/ConnectData/Search/Search.tsx b/public/app/features/connections/tabs/ConnectData/Search/Search.tsx
new file mode 100644
index 00000000000..3ee3613b55c
--- /dev/null
+++ b/public/app/features/connections/tabs/ConnectData/Search/Search.tsx
@@ -0,0 +1,22 @@
+import { css } from '@emotion/css';
+import React, { FC } from 'react';
+
+import { Icon, Input, useStyles } from '@grafana/ui';
+
+const getStyles = () => ({
+ searchContainer: css`
+ display: flex;
+ margin: 16px 0;
+ justify-content: space-between;
+ `,
+});
+
+export const Search: FC<{ onChange: (e: React.FormEvent) => void }> = ({ onChange }) => {
+ const styles = useStyles(getStyles);
+
+ return (
+
+ } placeholder="Search all" aria-label="Search all" />
+
+ );
+};
diff --git a/public/app/features/connections/tabs/ConnectData/Search/index.tsx b/public/app/features/connections/tabs/ConnectData/Search/index.tsx
new file mode 100644
index 00000000000..addd53308b6
--- /dev/null
+++ b/public/app/features/connections/tabs/ConnectData/Search/index.tsx
@@ -0,0 +1 @@
+export * from './Search';
diff --git a/public/app/features/connections/tabs/ConnectData/index.tsx b/public/app/features/connections/tabs/ConnectData/index.tsx
new file mode 100644
index 00000000000..0360f7a6097
--- /dev/null
+++ b/public/app/features/connections/tabs/ConnectData/index.tsx
@@ -0,0 +1 @@
+export * from './ConnectData';
diff --git a/public/app/features/connections/tabs/Plugins/Plugins.tsx b/public/app/features/connections/tabs/Plugins/Plugins.tsx
deleted file mode 100644
index 6b1fc3f57d0..00000000000
--- a/public/app/features/connections/tabs/Plugins/Plugins.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import React from 'react';
-
-export function Plugins() {
- return The list of plugins is under development
;
-}
diff --git a/public/app/features/connections/tabs/Plugins/index.tsx b/public/app/features/connections/tabs/Plugins/index.tsx
deleted file mode 100644
index 20c2507f6d8..00000000000
--- a/public/app/features/connections/tabs/Plugins/index.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export * from './Plugins';