Plugins: Add Connections homepage (#108316)

This commit is contained in:
Hugo Kiyodi Oshiro
2025-08-05 09:42:05 -03:00
committed by GitHub
parent 066163d710
commit 0584f91a64
6 changed files with 346 additions and 4 deletions
@@ -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 () => {
@@ -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 (
<Routes>
{/* Redirect to "Add new connection" by default */}
<Route caseSensitive path={'/'} element={<Navigate replace to={ROUTES.AddNewConnection} />} />
<Route caseSensitive path={'/'} element={<ConnectionsHomePage />} />
{/* The route paths need to be relative to the parent path (ROUTES.Base), so we need to remove that part */}
<Route caseSensitive path={ROUTES.DataSources.replace(ROUTES.Base, '')} element={<DataSourcesListPage />} />
<Route caseSensitive path={ROUTES.DataSourcesNew.replace(ROUTES.Base, '')} element={<NewDataSourcePage />} />
@@ -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',
},
];
}
@@ -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<HTMLDivElement>) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
locationService.push(url);
}
};
return (
<div
className={styles.card}
role="button"
tabIndex={0}
onClick={() => locationService.push(url)}
onKeyDown={onKeyDown}
>
<Icon name={icon} className={`${styles.logo} ${index % 2 === 0 ? styles.evenLogo : styles.oddLogo}`} />
<div className={styles.contentColumn}>
<h3 className={styles.title}>{title}</h3>
<p className={styles.description}>{description}</p>
</div>
</div>
);
}
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)',
}),
});
@@ -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 (
<Page
navId="connections"
pageNav={{
text: '',
active: true,
}}
>
<Page.Contents>
<div className={styles.centeredContainer}>
<h1 className={styles.mainTitle}>
<Trans i18nKey="connections.connections-home-page.welcome-to-connections">Welcome to Connections</Trans>
</h1>
<p className={styles.subTitle}>
{isOSS ? (
<Trans i18nKey="connections.oss.connections-home-page.subtitle">
Manage your data source connections in one place. Use this page to add a new data source or manage your
existing connections.
</Trans>
) : (
<Trans i18nKey="connections.cloud.connections-home-page.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.
</Trans>
)}
</p>
{cardsData && cardsData.length > 0 && (
<section className={styles.cardsSection}>
{cardsData?.map((child, index) => (
<PageCard
key={index}
title={child.text}
description={child.subTitle}
icon={child.icon}
url={child.url}
index={index}
/>
))}
</section>
)}
</div>
</Page.Contents>
</Page>
);
}
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',
},
}),
});
+41
View File
@@ -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></1> is unavailable.<3></3>To see a list of available datasources please <5>click here</5>."
},
"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"