From a4e66a2a019b734a6c0e8b630d22560123ecf1bb Mon Sep 17 00:00:00 2001 From: mikkancso Date: Thu, 20 Oct 2022 15:53:10 +0200 Subject: [PATCH] Connections: New "Connect data" page with data source catalog (#56772) * remove Plugins and CloudIntegrations tab and add ConnectData tab * ConnectData: add Search component and use it * ConnectData: add DataSourcePluginList component * add CardGrid component * add CategoryHeader component * ConnectData: restructure content DataSourcePluginList is removed, because its responsibilities are actually the same as ConnectData's responsibilities. NoResults was added as a reusable component, and was moved out of CardGrid, since there could be more CardGrid on one page, but only one NoResults. * fix spacer * use LoadingPlaceholder * CardGrid: add margin * generalize CardGridProps * move isLoading and error into CardGrid We'd like CardGrid to be reusable, even multiple times within a page. In this case, it's better UX if we show the loading or error states per card grid, not for the whole page. * ConnectData: fix NoResults condition * fix and add meaningful tests * fix indentation * move isLoading and error back to ConnectData * make `url` required for CardGrid items --- pkg/services/navtree/navtreeimpl/navtree.go | 16 ++--- .../connections/ConnectionsPage.test.tsx | 16 ++--- .../features/connections/ConnectionsPage.tsx | 6 +- .../__mocks__/store.navIndex.mock.ts | 24 ++----- public/app/features/connections/constants.ts | 3 +- .../CloudIntegrations/CloudIntegrations.tsx | 25 ------- .../tabs/CloudIntegrations/index.tsx | 1 - .../tabs/ConnectData/CardGrid/CardGrid.tsx | 67 +++++++++++++++++++ .../tabs/ConnectData/CardGrid/index.tsx | 1 + .../CategoryHeader/CategoryHeader.tsx | 27 ++++++++ .../tabs/ConnectData/CategoryHeader/index.tsx | 1 + .../tabs/ConnectData/ConnectData.test.tsx | 48 +++++++++++++ .../tabs/ConnectData/ConnectData.tsx | 56 ++++++++++++++++ .../tabs/ConnectData/NoResults/NoResults.tsx | 18 +++++ .../tabs/ConnectData/NoResults/index.tsx | 1 + .../tabs/ConnectData/Search/Search.tsx | 22 ++++++ .../tabs/ConnectData/Search/index.tsx | 1 + .../connections/tabs/ConnectData/index.tsx | 1 + .../connections/tabs/Plugins/Plugins.tsx | 5 -- .../connections/tabs/Plugins/index.tsx | 1 - 20 files changed, 261 insertions(+), 79 deletions(-) delete mode 100644 public/app/features/connections/tabs/CloudIntegrations/CloudIntegrations.tsx delete mode 100644 public/app/features/connections/tabs/CloudIntegrations/index.tsx create mode 100644 public/app/features/connections/tabs/ConnectData/CardGrid/CardGrid.tsx create mode 100644 public/app/features/connections/tabs/ConnectData/CardGrid/index.tsx create mode 100644 public/app/features/connections/tabs/ConnectData/CategoryHeader/CategoryHeader.tsx create mode 100644 public/app/features/connections/tabs/ConnectData/CategoryHeader/index.tsx create mode 100644 public/app/features/connections/tabs/ConnectData/ConnectData.test.tsx create mode 100644 public/app/features/connections/tabs/ConnectData/ConnectData.tsx create mode 100644 public/app/features/connections/tabs/ConnectData/NoResults/NoResults.tsx create mode 100644 public/app/features/connections/tabs/ConnectData/NoResults/index.tsx create mode 100644 public/app/features/connections/tabs/ConnectData/Search/Search.tsx create mode 100644 public/app/features/connections/tabs/ConnectData/Search/index.tsx create mode 100644 public/app/features/connections/tabs/ConnectData/index.tsx delete mode 100644 public/app/features/connections/tabs/Plugins/Plugins.tsx delete mode 100644 public/app/features/connections/tabs/Plugins/index.tsx 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 && ( +
    + {`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';