From 0584f91a649025160404ace410f4d7231ebe98fc Mon Sep 17 00:00:00 2001 From: Hugo Kiyodi Oshiro Date: Tue, 5 Aug 2025 09:42:05 -0300 Subject: [PATCH] Plugins: Add Connections homepage (#108316) --- .../features/connections/Connections.test.tsx | 34 +++++- .../app/features/connections/Connections.tsx | 3 +- .../components/PageCard/CardData.ts | 79 ++++++++++++++ .../components/PageCard/PageCard.tsx | 90 +++++++++++++++ .../connections/pages/ConnectionsHomePage.tsx | 103 ++++++++++++++++++ public/locales/en-US/grafana.json | 41 +++++++ 6 files changed, 346 insertions(+), 4 deletions(-) create mode 100644 public/app/features/connections/components/PageCard/CardData.ts create mode 100644 public/app/features/connections/components/PageCard/PageCard.tsx create mode 100644 public/app/features/connections/pages/ConnectionsHomePage.tsx diff --git a/public/app/features/connections/Connections.test.tsx b/public/app/features/connections/Connections.test.tsx index 16f611e585d..1d3519426f7 100644 --- a/public/app/features/connections/Connections.test.tsx +++ b/public/app/features/connections/Connections.test.tsx @@ -2,6 +2,8 @@ import { RenderResult, screen } from '@testing-library/react'; import { Route, Routes } from 'react-router-dom-v5-compat'; import { render } from 'test/test-utils'; +import { GrafanaEdition } from '@grafana/data/internal'; +import { config } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; import * as api from 'app/features/datasources/api'; import { getMockDataSources } from 'app/features/datasources/mocks/dataSourcesMocks'; @@ -43,15 +45,41 @@ describe('Connections', () => { (contextSrv.hasPermission as jest.Mock) = jest.fn().mockReturnValue(true); }); - test('shows the "Add new connection" page by default', async () => { + test('shows the "Connections Homepage" page by default when edition is Cloud', async () => { + config.buildInfo.edition = GrafanaEdition.Enterprise; renderPage(); - // Data sources group + // Add new connection card + expect(await screen.findByText('Add new connection')).toBeVisible(); + expect(await screen.findByText('Collector:')).toBeVisible(); expect(await screen.findByText('Data sources')).toBeVisible(); + expect(await screen.findByText('Integrations')).toBeVisible(); + expect(await screen.findByText('Private data source connect')).toBeVisible(); // Heading + expect(await screen.findByText('Welcome to Connections')).toBeVisible(); + expect( + await screen.findByText( + 'Connect your infrastructure to Grafana Cloud using data sources, integrations and apps. Use this page to add to manage everything from data ingestion to private connections and telemetry pipelines.' + ) + ).toBeVisible(); + }); + + test('shows the OSS "Connections Homepage" page by default when edition is OpenSource', async () => { + config.buildInfo.edition = GrafanaEdition.OpenSource; + renderPage(); + + // Add new connection card expect(await screen.findByText('Add new connection')).toBeVisible(); - expect(await screen.findByText('Browse and create new connections')).toBeVisible(); + expect(await screen.findByText('View configured data sources')).toBeVisible(); + + // Heading + expect(await screen.findByText('Welcome to Connections')).toBeVisible(); + expect( + await screen.findByText( + 'Manage your data source connections in one place. Use this page to add a new data source or manage your existing connections.' + ) + ).toBeVisible(); }); test('renders the correct tab even if accessing it with a "sub-url"', async () => { diff --git a/public/app/features/connections/Connections.tsx b/public/app/features/connections/Connections.tsx index e4119a6bf3f..6d40a294c55 100644 --- a/public/app/features/connections/Connections.tsx +++ b/public/app/features/connections/Connections.tsx @@ -4,6 +4,7 @@ import { StoreState, useSelector } from 'app/types/store'; import { ROUTES } from './constants'; import { AddNewConnectionPage } from './pages/AddNewConnectionPage'; +import ConnectionsHomePage from './pages/ConnectionsHomePage'; import { DataSourceDashboardsPage } from './pages/DataSourceDashboardsPage'; import { DataSourceDetailsPage } from './pages/DataSourceDetailsPage'; import { DataSourcesListPage } from './pages/DataSourcesListPage'; @@ -30,7 +31,7 @@ export default function Connections() { return ( {/* Redirect to "Add new connection" by default */} - } /> + } /> {/* The route paths need to be relative to the parent path (ROUTES.Base), so we need to remove that part */} } /> } /> diff --git a/public/app/features/connections/components/PageCard/CardData.ts b/public/app/features/connections/components/PageCard/CardData.ts new file mode 100644 index 00000000000..8355fdc5aa8 --- /dev/null +++ b/public/app/features/connections/components/PageCard/CardData.ts @@ -0,0 +1,79 @@ +import type { IconName } from '@grafana/data'; +import { t } from '@grafana/i18n'; + +type CardData = { + text: string; + subTitle: string; + url: string; + icon: IconName; +}; + +export function getCloudCardData(): CardData[] { + return [ + { + text: t('connections.cloud.connections-home-page.add-new-connection.title', 'Add new connection'), + subTitle: t( + 'connections.cloud.connections-home-page.add-new-connection.subtitle', + 'Connect data to Grafana through data sources, integrations and apps' + ), + url: '/connections/add-new-connection', + icon: 'plus-circle', + }, + { + text: t('connections.cloud.connections-home-page.collector.title', 'Collector:'), + subTitle: t( + 'connections.cloud.connections-home-page.collector.subtitle', + 'Manage the configuration of Grafana Alloy, our distribution of the OpenTelemetry Collector' + ), + url: '/a/grafana-collector-app', + icon: 'frontend-observability', + }, + { + text: t('connections.cloud.connections-home-page.data-sources.title', 'Data sources'), + subTitle: t( + 'connections.cloud.connections-home-page.data-sources.subtitle', + 'Manage your existing data source connections' + ), + url: '/connections/datasources', + icon: 'database', + }, + { + text: t('connections.cloud.connections-home-page.integrations.title', 'Integrations'), + subTitle: t('connections.cloud.connections-home-page.integrations.subtitle', 'Manage your active integrations'), + url: '/connections/infrastructure', + icon: 'apps', + }, + { + text: t( + 'connections.cloud.connections-home-page.private-data-source-connections.title', + 'Private data source connect' + ), + subTitle: t( + 'connections.cloud.connections-home-page.private-data-source-connections.subtitle', + 'Manage your private network connections for data sources' + ), + url: '/connections/private-data-source-connections', + icon: 'sitemap', + }, + ]; +} + +export function getOssCardData(): CardData[] { + return [ + { + text: t('connections.oss.connections-home-page.add-new-connection.title', 'Add new connection'), + subTitle: t('connections.oss.connections-home-page.add-new-connection.subtitle', 'Connect to a new data source'), + url: '/connections/add-new-connection', + icon: 'plus-circle', + }, + { + text: t('connections.oss.connections-home-page.data-sources.title', 'View configured data sources'), + subTitle: t( + 'connections.oss.connections-home-page.data-sources.subtitle', + 'Manage your existing data source connections' + ), + url: '/connections/datasources', + icon: 'database', + }, + ]; +} diff --git a/public/app/features/connections/components/PageCard/PageCard.tsx b/public/app/features/connections/components/PageCard/PageCard.tsx new file mode 100644 index 00000000000..b51d3ca7fc0 --- /dev/null +++ b/public/app/features/connections/components/PageCard/PageCard.tsx @@ -0,0 +1,90 @@ +import { css } from '@emotion/css'; + +import { GrafanaTheme2, IconName } from '@grafana/data'; +import { locationService } from '@grafana/runtime'; +import { Icon, useStyles2 } from '@grafana/ui'; + +type PageCardProps = { + title: string; + description: string; + icon: IconName; + url: string; + index: number; +}; + +export default function PageCard({ title, description, icon, url, index }: PageCardProps) { + const styles = useStyles2(getStyles); + + const onKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + locationService.push(url); + } + }; + + return ( +
locationService.push(url)} + onKeyDown={onKeyDown} + > + +
+

{title}

+

{description}

+
+
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + card: css({ + display: 'flex', + alignItems: 'flex-start', + gap: theme.spacing(2), + paddingTop: theme.spacing(2), + backgroundColor: theme.colors.background.secondary, + borderRadius: theme.shape.radius.default, + padding: `${theme.spacing(3)} ${theme.spacing(4)} ${theme.spacing(2.25)} ${theme.spacing(4)}`, + minHeight: theme.spacing(19.4), + '&:hover': { + cursor: 'pointer', + backgroundColor: theme.colors.emphasize(theme.colors.background.secondary, 0.03), + }, + width: theme.spacing(48), + }), + contentColumn: css({ + flex: 1, + }), + title: css({ + marginBottom: theme.spacing(1), + fontSize: theme.typography.h4.fontSize, + fontWeight: theme.typography.h4.fontWeight, + color: theme.colors.text.primary, + }), + description: css({ + WebkitLineClamp: 3, + WebkitBoxOrient: 'vertical', + display: '-webkit-box', + overflow: 'hidden', + margin: '0', + color: theme.colors.text.secondary, + }), + logo: css({ + objectFit: 'contain', + width: '47px', + height: '47px', + padding: theme.spacing(1.2), + borderRadius: theme.spacing(1), + }), + evenLogo: css({ + color: '#4ADE80', + backgroundColor: 'rgba(34, 197, 94, 0.10)', + }), + oddLogo: css({ + color: '#FB923C', + backgroundColor: 'rgba(249, 115, 22, 0.10)', + }), +}); diff --git a/public/app/features/connections/pages/ConnectionsHomePage.tsx b/public/app/features/connections/pages/ConnectionsHomePage.tsx new file mode 100644 index 00000000000..647c3acfb50 --- /dev/null +++ b/public/app/features/connections/pages/ConnectionsHomePage.tsx @@ -0,0 +1,103 @@ +import { css } from '@emotion/css'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { GrafanaEdition } from '@grafana/data/internal'; +import { Trans } from '@grafana/i18n'; +import { config } from '@grafana/runtime'; +import { useStyles2 } from '@grafana/ui'; +import { Page } from 'app/core/components/Page/Page'; + +import { getCloudCardData, getOssCardData } from '../components/PageCard/CardData'; +import PageCard from '../components/PageCard/PageCard'; + +export default function ConnectionsHomePage() { + const styles = useStyles2(getStyles); + + const isOSS = config.buildInfo.edition === GrafanaEdition.OpenSource; + + let cardsData = isOSS ? getOssCardData() : getCloudCardData(); + + return ( + + +
+

+ Welcome to Connections +

+

+ {isOSS ? ( + + Manage your data source connections in one place. Use this page to add a new data source or manage your + existing connections. + + ) : ( + + Connect your infrastructure to Grafana Cloud using data sources, integrations and apps. Use this page to + add to manage everything from data ingestion to private connections and telemetry pipelines. + + )} +

+ {cardsData && cardsData.length > 0 && ( +
+ {cardsData?.map((child, index) => ( + + ))} +
+ )} +
+
+
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + mainTitle: css({ + textAlign: 'left', + [theme.breakpoints.up('sm')]: { + textAlign: 'center', + }, + }), + subTitle: css({ + color: theme.colors.text.secondary, + textAlign: 'left', + [theme.breakpoints.up('sm')]: { + textAlign: 'center', + maxWidth: theme.spacing(119), + }, + }), + cardsSection: css({ + display: 'flex', + flexWrap: 'wrap', + justifyContent: 'flex-start', + gap: theme.spacing(4), + paddingTop: theme.spacing(2), + paddingBottom: theme.spacing(2), + width: '100%', + [theme.breakpoints.up('sm')]: { + justifyContent: 'center', + paddingTop: theme.spacing(6), + paddingBottom: theme.spacing(6), + }, + }), + centeredContainer: css({ + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + [theme.breakpoints.up('sm')]: { + alignItems: 'center', + }, + }), +}); diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 22c6feba34e..c193c023bee 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -4134,12 +4134,40 @@ "body": "Try the new Advisor to uncover potential issues with your data sources and plugins.", "go-to-advisor": "Go to Advisor" }, + "cloud": { + "connections-home-page": { + "add-new-connection": { + "subtitle": "Connect data to Grafana through data sources, integrations and apps", + "title": "Add new connection" + }, + "collector": { + "subtitle": "Manage the configuration of Grafana Alloy, our distribution of the OpenTelemetry Collector", + "title": "Collector:" + }, + "data-sources": { + "subtitle": "Manage your existing data source connections", + "title": "Data sources" + }, + "integrations": { + "subtitle": "Manage your active integrations", + "title": "Integrations" + }, + "private-data-source-connections": { + "subtitle": "Manage your private network connections for data sources", + "title": "Private data source connect" + }, + "subtitle": "Connect your infrastructure to Grafana Cloud using data sources, integrations and apps. Use this page to add to manage everything from data ingestion to private connections and telemetry pipelines." + } + }, "connect-data": { "category-header-label": "Data sources", "empty-message": "No results matching your query were found", "request-data-source": "Request a new data source", "roadmap": "View roadmap" }, + "connections-home-page": { + "welcome-to-connections": "Welcome to Connections" + }, "connections-redirect-notice": { "aria-label-link-to-connections": "Link to Connections", "body": "Data sources have a new home! You can discover new data sources or manage existing ones in the Connections page, accessible from the main menu.", @@ -4170,6 +4198,19 @@ "not-found-datasource": { "body": "Maybe you mistyped the URL or the plugin with the id <1> is unavailable.<3>To see a list of available datasources please <5>click here." }, + "oss": { + "connections-home-page": { + "add-new-connection": { + "subtitle": "Connect to a new data source", + "title": "Add new connection" + }, + "data-sources": { + "subtitle": "Manage your existing data source connections", + "title": "View configured data sources" + }, + "subtitle": "Manage your data source connections in one place. Use this page to add a new data source or manage your existing connections." + } + }, "search": { "aria-label-search-all": "Search all", "placeholder": "Search all"