Plugins: Add feature highlights tabs (#110712)

This commit is contained in:
Hugo Kiyodi Oshiro 2025-10-06 08:46:47 -03:00 committed by GitHub
parent 825f8fc7ff
commit 548e739d75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 621 additions and 29 deletions

View File

@ -1,19 +1,25 @@
import { css } from '@emotion/css';
import { css, cx } from '@emotion/css';
import { HTMLAttributes } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Icon, useStyles2 } from '@grafana/ui';
export function OrangeBadge({ text }: { text: string }) {
const styles = useStyles2(getStyles);
interface Props extends HTMLAttributes<HTMLDivElement> {
text?: string;
className?: string;
}
export function OrangeBadge({ text, className, ...htmlProps }: Props) {
const styles = useStyles2(getStyles, text);
return (
<div className={styles.wrapper}>
<div className={cx(styles.wrapper, className)} {...htmlProps}>
<Icon name="cloud" size="sm" />
{text}
</div>
);
}
const getStyles = (theme: GrafanaTheme2) => {
const getStyles = (theme: GrafanaTheme2, text: string | undefined) => {
return {
wrapper: css({
display: 'inline-flex',
@ -26,6 +32,11 @@ const getStyles = (theme: GrafanaTheme2) => {
fontSize: theme.typography.bodySmall.fontSize,
lineHeight: theme.typography.bodySmall.lineHeight,
alignItems: 'center',
...(text === undefined && {
svg: {
marginRight: 0,
},
}),
}),
};
};

View File

@ -5,13 +5,14 @@ import { GrafanaTheme2 } from '@grafana/data';
import { reportExperimentView } from '@grafana/runtime';
import { useStyles2 } from '@grafana/ui';
import { OrangeBadge } from '../Branding/OrangeBadge';
export interface Props extends HTMLAttributes<HTMLSpanElement> {
text?: string;
experimentId?: string;
eventVariant?: string;
}
export const ProBadge = ({ text = 'PRO', className, experimentId, eventVariant = '', ...htmlProps }: Props) => {
export const ProBadge = ({ className, experimentId, eventVariant = '', ...htmlProps }: Props) => {
const styles = useStyles2(getStyles);
useEffect(() => {
@ -20,23 +21,13 @@ export const ProBadge = ({ text = 'PRO', className, experimentId, eventVariant =
}
}, [experimentId, eventVariant]);
return (
<span className={cx(styles.badge, className)} {...htmlProps}>
{text}
</span>
);
return <OrangeBadge className={cx(styles.badge, className)} {...htmlProps} />;
};
const getStyles = (theme: GrafanaTheme2) => {
return {
badge: css({
marginLeft: theme.spacing(1.25),
borderRadius: theme.shape.borderRadius(5),
backgroundColor: theme.colors.success.main,
padding: theme.spacing(0.25, 0.75),
color: 'white', // use the same color for both themes
fontWeight: theme.typography.fontWeightMedium,
fontSize: theme.typography.pxToRem(10),
}),
};
};

View File

@ -2,14 +2,19 @@ import { Navigate, Routes, Route, useLocation } from 'react-router-dom-v5-compat
import { StoreState, useSelector } from 'app/types/store';
import { isOpenSourceBuildOrUnlicenced } from '../admin/EnterpriseAuthFeaturesCard';
import { ROUTES } from './constants';
import { AddNewConnectionPage } from './pages/AddNewConnectionPage';
import { CacheFeatureHighlightPage } from './pages/CacheFeatureHighlightPage';
import ConnectionsHomePage from './pages/ConnectionsHomePage';
import { DataSourceDashboardsPage } from './pages/DataSourceDashboardsPage';
import { DataSourceDetailsPage } from './pages/DataSourceDetailsPage';
import { DataSourcesListPage } from './pages/DataSourcesListPage';
import { EditDataSourcePage } from './pages/EditDataSourcePage';
import { InsightsFeatureHighlightPage } from './pages/InsightsFeatureHighlightPage';
import { NewDataSourcePage } from './pages/NewDataSourcePage';
import { PermissionsFeatureHighlightPage } from './pages/PermissionsFeatureHighlightPage';
function RedirectToAddNewConnection() {
const { search } = useLocation();
@ -27,6 +32,7 @@ function RedirectToAddNewConnection() {
export default function Connections() {
const navIndex = useSelector((state: StoreState) => state.navIndex);
const isAddNewConnectionPageOverridden = Boolean(navIndex['standalone-plugin-page-/connections/add-new-connection']);
const shouldEnableFeatureHighlights = isOpenSourceBuildOrUnlicenced();
return (
<Routes>
@ -41,6 +47,27 @@ export default function Connections() {
element={<DataSourceDetailsPage />}
/>
<Route caseSensitive path={ROUTES.DataSourcesEdit.replace(ROUTES.Base, '')} element={<EditDataSourcePage />} />
{shouldEnableFeatureHighlights && (
<>
<Route
caseSensitive
path={ROUTES.DataSourcesEdit.replace(ROUTES.Base, '') + '/permissions'}
element={<PermissionsFeatureHighlightPage />}
/>
<Route
caseSensitive
path={ROUTES.DataSourcesEdit.replace(ROUTES.Base, '') + '/insights'}
element={<InsightsFeatureHighlightPage />}
/>
<Route
caseSensitive
path={ROUTES.DataSourcesEdit.replace(ROUTES.Base, '') + '/cache'}
element={<CacheFeatureHighlightPage />}
/>
</>
)}
<Route
caseSensitive
path={ROUTES.DataSourcesDashboards.replace(ROUTES.Base, '')}

View File

@ -0,0 +1,172 @@
import { css } from '@emotion/css';
import { useParams } from 'react-router-dom-v5-compat';
import { GrafanaTheme2 } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { Icon, LinkButton, TextLink, useStyles2 } from '@grafana/ui';
import { CloudEnterpriseBadge } from 'app/core/components/Branding/CloudEnterpriseBadge';
import { Page } from 'app/core/components/Page/Page';
import { DataSourceTitle } from 'app/features/datasources/components/DataSourceTitle';
import { EditDataSourceActions } from 'app/features/datasources/components/EditDataSourceActions';
import { useDataSourceInfo } from 'app/features/datasources/components/useDataSourceInfo';
import { useInitDataSourceSettings } from 'app/features/datasources/state/hooks';
import { useDataSourceTabNav } from '../hooks/useDataSourceTabNav';
type FeatureHighlightsTabPageProps = {
pageName: string;
title: string;
header: string;
items: string[];
buttonLink: string;
screenshotPath: string;
};
export function FeatureHighlightsTabPage({
pageName,
title,
header,
items,
buttonLink,
screenshotPath,
}: FeatureHighlightsTabPageProps) {
const { uid = '' } = useParams<{ uid: string }>();
useInitDataSourceSettings(uid);
const { navId, pageNav, dataSourceHeader } = useDataSourceTabNav(pageName);
const styles = useStyles2(getStyles);
const info = useDataSourceInfo({
dataSourcePluginName: pageNav.dataSourcePluginName,
alertingSupported: dataSourceHeader.alertingSupported,
});
return (
<Page
navId={navId}
pageNav={pageNav}
renderTitle={(title) => <DataSourceTitle title={title} />}
info={info}
actions={<EditDataSourceActions uid={uid} />}
>
<Page.Contents>
<div className={styles.container}>
<div className={styles.content}>
<div className={styles.badge}>
<CloudEnterpriseBadge />
</div>
<h1 className={styles.title}>{title}</h1>
<div className={styles.header}>{header}</div>
<div className={styles.itemsList}>
{items.map((item) => (
<div key={item} className={styles.listItem}>
<Icon className={styles.icon} name="check" />
{item}
</div>
))}
</div>
<div className={styles.footer}>
<Trans i18nKey="connections.feature-highlight-page.footer">
Create a Grafana Cloud Free account to start using data source permissions. This feature is also
available with a Grafana Enterprise license.
</Trans>
<div>
<TextLink href="https://grafana.com/products/enterprise/grafana/">
<Icon name="external-link-alt" />
<Trans i18nKey="connections.feature-highlight-page.footer-link">Learn about Enterprise</Trans>
</TextLink>
</div>
</div>
<LinkButton className={styles.linkButton} href={buttonLink}>
<Icon name="external-link-alt" className={styles.buttonIcon} />
<Trans i18nKey="connections.feature-highlight-page.link-button-label">Create account</Trans>
</LinkButton>
<p className={styles.footNote}>
<Trans i18nKey="connections.feature-highlight-page.foot-note">
After creating an account, you can easily{' '}
<TextLink href="https://grafana.com/docs/grafana/latest/administration/migration-guide/cloud-migration-assistant/">
migrate this instance to Grafana Cloud
</TextLink>{' '}
with our Migration Assistant.
</Trans>
</p>
</div>
<div className={styles.imageContainer}>
<img className={styles.image} src={screenshotPath} alt={`${pageName} screenshot`} />
</div>
</div>
</Page.Contents>
</Page>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
container: css({
display: 'flex',
gap: theme.spacing(4),
alignItems: 'flex-start',
[theme.breakpoints.down('lg')]: {
flexDirection: 'column',
},
}),
content: css({
flex: '0 0 40%',
}),
imageContainer: css({
flex: '0 0 60%',
display: 'flex',
[theme.breakpoints.down('lg')]: {
flex: '1 1 auto',
},
padding: `${theme.spacing(5)} 10% 0 ${theme.spacing(5)}`,
}),
image: css({
width: '100%',
borderRadius: theme.shape.radius.default,
boxShadow: theme.shadows.z3,
}),
buttonIcon: css({
marginRight: theme.spacing(1),
}),
badge: css({
marginBottom: theme.spacing(1),
}),
title: css({
marginBottom: theme.spacing(2),
marginTop: theme.spacing(2),
}),
header: css({
color: theme.colors.text.primary,
}),
itemsList: css({
marginBottom: theme.spacing(3),
marginTop: theme.spacing(3),
}),
listItem: css({
display: 'flex',
alignItems: 'flex-start',
color: theme.colors.text.primary,
lineHeight: theme.typography.bodySmall.lineHeight,
marginBottom: theme.spacing(2),
}),
linkButton: css({
marginBottom: theme.spacing(2),
}),
footer: css({
marginBottom: theme.spacing(3),
marginTop: theme.spacing(3),
}),
icon: css({
marginRight: theme.spacing(1),
color: theme.colors.success.main,
}),
footNote: css({
color: theme.colors.text.secondary,
fontSize: theme.typography.bodySmall.fontSize,
}),
});

View File

@ -0,0 +1,95 @@
import { useLocation, useParams } from 'react-router-dom-v5-compat';
import { NavModel, NavModelItem } from '@grafana/data';
import { t } from '@grafana/i18n';
import { getDataSourceSrv } from '@grafana/runtime';
import { getNavModel } from 'app/core/selectors/navModel';
import { useDataSource, useDataSourceMeta, useDataSourceSettings } from 'app/features/datasources/state/hooks';
import { getDataSourceLoadingNav, buildNavModel, getDataSourceNav } from 'app/features/datasources/state/navModel';
import { useGetSingle } from 'app/features/plugins/admin/state/hooks';
import { useSelector } from 'app/types/store';
export function useDataSourceTabNav(pageName: string, pageIdParam?: string) {
const { uid = '' } = useParams<{ uid: string }>();
const location = useLocation();
const datasource = useDataSource(uid);
const dataSourceMeta = useDataSourceMeta(datasource.type);
const datasourcePlugin = useGetSingle(datasource.type);
const params = new URLSearchParams(location.search);
const pageId = pageIdParam || params.get('page');
const { plugin, loadError, loading } = useDataSourceSettings();
const dsi = getDataSourceSrv()?.getInstanceSettings(uid);
const hasAlertingEnabled = Boolean(dsi?.meta?.alerting ?? false);
const isAlertManagerDatasource = dsi?.type === 'alertmanager';
const alertingSupported = hasAlertingEnabled || isAlertManagerDatasource;
const navIndex = useSelector((state) => state.navIndex);
const navIndexId = pageId ? `datasource-${pageId}-${uid}` : `datasource-${pageName}-${uid}`;
let pageNav: NavModel = {
node: {
text: t('connections.use-data-source-settings-nav.page-nav.text.data-source-nav-node', 'Data Source Nav Node'),
},
main: {
text: t('connections.use-data-source-settings-nav.page-nav.text.data-source-nav-node', 'Data Source Nav Node'),
},
};
if (loadError) {
const node: NavModelItem = {
text: loadError,
subTitle: t('connections.use-data-source-settings-nav.node.subTitle.data-source-error', 'Data Source Error'),
icon: 'exclamation-triangle',
};
pageNav = {
node: node,
main: node,
};
}
if (loading || !plugin) {
pageNav = getNavModel(navIndex, navIndexId, getDataSourceLoadingNav(pageName));
}
if (!datasource.uid) {
const node: NavModelItem = {
text: t('connections.use-data-source-settings-nav.node.subTitle.data-source-error', 'Data Source Error'),
icon: 'exclamation-triangle',
};
pageNav = {
node: node,
main: node,
};
}
if (plugin) {
pageNav = getNavModel(
navIndex,
navIndexId,
getDataSourceNav(buildNavModel(datasource, plugin), pageId || pageName)
);
}
const connectionsPageNav = {
...pageNav.main,
dataSourcePluginName: datasourcePlugin?.name || plugin?.meta.name || '',
active: true,
text: datasource.name || '',
subTitle: dataSourceMeta.name ? `Type: ${dataSourceMeta.name}` : '',
children: (pageNav.main.children || []).map((navModelItem) => ({
...navModelItem,
url: navModelItem.url?.replace('datasources/edit/', '/connections/datasources/edit/'),
})),
};
return {
navId: 'connections-datasources',
pageNav: connectionsPageNav,
dataSourceHeader: {
alertingSupported,
},
};
}

View File

@ -0,0 +1,33 @@
import { t } from '@grafana/i18n';
import cacheScreenshot from 'img/cache-screenshot.png';
import { FeatureHighlightsTabPage } from '../components/FeatureHighlightsTabPage';
export function CacheFeatureHighlightPage() {
return (
<FeatureHighlightsTabPage
pageName="cache"
title={t(
'connections.cache-feature-highlight-page.title',
'Optimize queries with Query Caching in Grafana Cloud'
)}
header={t(
'connections.cache-feature-highlight-page.header',
'Query caching can improve load times and reduce API costs by temporarily storing the results of data source queries. When you or other users submit the same query, the results will come back from the cache instead of from the data source.'
)}
items={[
t(
'connections.cache-feature-highlight-page.item-1',
'Faster dashboard load times, especially for popular dashboards.'
),
t('connections.cache-feature-highlight-page.item-2', 'Reduced API costs.'),
t(
'connections.cache-feature-highlight-page.item-3',
'Reduced likelihood that APIs will rate-limit or throttle requests.'
),
]}
buttonLink={'https://grafana.com/auth/sign-up/create-user?src=oss-grafana&cnt=datasource-caching'}
screenshotPath={cacheScreenshot}
/>
);
}

View File

@ -0,0 +1,40 @@
import { t } from '@grafana/i18n';
import insightsScreenshot from 'img/insights-screenshot.png';
import { FeatureHighlightsTabPage } from '../components/FeatureHighlightsTabPage';
export function InsightsFeatureHighlightPage() {
return (
<FeatureHighlightsTabPage
pageName="insights"
title={t(
'connections.insights-feature-highlight-page.title',
'Understand usage of your data sources with Grafana Cloud'
)}
header={t(
'connections.insights-feature-highlight-page.header',
'Usage Insights provide detailed information about data source usage, like the number of views, queries, and errors users have experienced. You can use this to improve users experience and troubleshoot issues.'
)}
items={[
t(
'connections.insights-feature-highlight-page.item-1',
'Demonstrate and improve the value of your observability service by keeping track of user engagement'
),
t(
'connections.insights-feature-highlight-page.item-2',
'Keep Grafana performant and by identifying and fixing slow, error-prone data sources'
),
t(
'connections.insights-feature-highlight-page.item-3',
'Clean up your instance by finding and removing unused data sources'
),
t(
'connections.insights-feature-highlight-page.item-4',
'Review individual data source usage insights at a glance in the UI, sort search results by usage and errors, or dig into detailed usage logs'
),
]}
buttonLink={'https://grafana.com/auth/sign-up/create-user?src=oss-grafana&cnt=datasource-insights'}
screenshotPath={insightsScreenshot}
/>
);
}

View File

@ -0,0 +1,36 @@
import { t } from '@grafana/i18n';
import permissionsScreenshot from 'img/permissions-screenshot.png';
import { FeatureHighlightsTabPage } from '../components/FeatureHighlightsTabPage';
export function PermissionsFeatureHighlightPage() {
return (
<FeatureHighlightsTabPage
pageName="permissions"
title={t(
'connections.permissions-feature-highlight-page.title',
'Secure access to data with data source permissions in Grafana Cloud'
)}
header={t(
'connections.permissions-feature-highlight-page.header',
'With data source permissions, you can protect sensitive data by limiting access to this data source to specific users, teams, and roles.'
)}
items={[
t(
'connections.permissions-feature-highlight-page.item-1',
'Protect sensitive data, like security logs, production databases, and personally-identifiable information'
),
t(
'connections.permissions-feature-highlight-page.item-2',
'Clean up users experience by hiding data sources they dont need to use'
),
t(
'connections.permissions-feature-highlight-page.item-3',
'Share Grafana access more freely, knowing that users will not unwittingly see sensitive data'
),
]}
buttonLink={'https://grafana.com/auth/sign-up/create-user?src=oss-grafana&cnt=datasource-permissions'}
screenshotPath={permissionsScreenshot}
/>
);
}

View File

@ -0,0 +1,150 @@
import { RenderResult, screen } from '@testing-library/react';
import { Route, Routes } from 'react-router-dom-v5-compat';
import { render } from 'test/test-utils';
import { LayoutModes, PluginType } from '@grafana/data';
import { setPluginLinksHook, setPluginComponentsHook } 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';
import { configureStore } from 'app/store/configureStore';
import { getPluginsStateMock } from '../../../plugins/admin/mocks/mockHelpers';
import Connections from '../../Connections';
import { ROUTES } from '../../constants';
import { navIndex } from '../../mocks/store.navIndex.mock';
setPluginLinksHook(() => ({ links: [], isLoading: false }));
setPluginComponentsHook(() => ({ components: [], isLoading: false }));
const mockDatasources = getMockDataSources(3);
const renderPage = (
path: string = ROUTES.Base,
store = configureStore({
navIndex,
plugins: getPluginsStateMock([]),
dataSources: {
dataSources: mockDatasources,
dataSourcesCount: mockDatasources.length,
isLoadingDataSources: false,
searchQuery: '',
dataSourceTypeSearchQuery: '',
layoutMode: LayoutModes.List,
dataSource: mockDatasources[0],
dataSourceMeta: {
id: '',
name: '',
type: PluginType.panel,
info: {
author: {
name: '',
url: undefined,
},
description: '',
links: [],
logos: {
large: '',
small: '',
},
screenshots: [],
updated: '',
version: '',
},
module: '',
baseUrl: '',
backend: true,
isBackend: true,
},
isLoadingDataSourcePlugins: false,
plugins: [],
categories: [],
isSortAscending: true,
},
})
): RenderResult => {
return render(
<Routes>
<Route path={`${ROUTES.Base}/*`} element={<Connections />} />
</Routes>,
{
store,
historyOptions: { initialEntries: [path] },
}
);
};
jest.mock('@grafana/runtime', () => {
const original = jest.requireActual('@grafana/runtime');
return {
...original,
config: {
...original.config,
bootData: {
user: {
orgId: 1,
timezone: 'UTC',
},
navTree: [],
},
featureToggles: {
...original.config.featureToggles,
},
datasources: {},
defaultDatasource: '',
buildInfo: {
...original.config.buildInfo,
edition: 'Open Source',
},
caching: {
...original.config.caching,
enabled: true,
},
},
getTemplateSrv: () => ({
replace: (str: string) => str,
}),
getDataSourceSrv: () => {
return {
getInstanceSettings: (uid: string) => {
return {
id: uid,
uid: uid,
type: PluginType.datasource,
name: uid,
meta: {
id: uid,
name: uid,
type: PluginType.datasource,
backend: true,
isBackend: true,
},
};
},
};
},
};
});
describe('DataSourceEditTabs', () => {
beforeEach(() => {
process.env.NODE_ENV = 'test';
(api.getDataSources as jest.Mock) = jest.fn().mockResolvedValue(mockDatasources);
(contextSrv.hasPermission as jest.Mock) = jest.fn().mockReturnValue(true);
});
it('should render Permissions and Insights tabs', () => {
const path = ROUTES.DataSourcesEdit.replace(':uid', mockDatasources[0].uid);
renderPage(path);
const permissionsTab = screen.getByTestId('data-testid Tab Permissions');
expect(permissionsTab).toBeInTheDocument();
expect(permissionsTab).toHaveTextContent('Permissions');
expect(permissionsTab).toHaveAttribute('href', '/connections/datasources/edit/x/permissions');
const insightsTab = screen.getByTestId('data-testid Tab Insights');
expect(insightsTab).toBeInTheDocument();
expect(insightsTab).toHaveTextContent('Insights');
expect(insightsTab).toHaveAttribute('href', '/connections/datasources/edit/x/insights');
});
});

View File

@ -13,7 +13,7 @@ export interface Props {
}
export function DataSourceTabPage({ uid, pageId }: Props) {
const { navId, pageNav, dataSourceHeader } = useDataSourceSettingsNav();
const { navId, pageNav, dataSourceHeader } = useDataSourceSettingsNav('settings');
const info = useDataSourceInfo({
dataSourcePluginName: pageNav.dataSourcePluginName,

View File

@ -4,6 +4,7 @@ import { featureEnabled } from '@grafana/runtime';
import { ProBadge } from 'app/core/components/Upgrade/ProBadge';
import config from 'app/core/config';
import { contextSrv } from 'app/core/core';
import { isOpenSourceBuildOrUnlicenced } from 'app/features/admin/EnterpriseAuthFeaturesCard';
import { highlightTrial } from 'app/features/admin/utils';
import { AccessControlAction } from 'app/types/accessControl';
import icnDatasourceSvg from 'img/icn-datasource.svg';
@ -53,6 +54,8 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat
});
}
const shouldEnableFeatureHighlights = isOpenSourceBuildOrUnlicenced();
const isLoadingNav = dataSource.type === loadingDSType;
const permissionsExperimentId = 'feature-highlights-data-source-permissions-badge';
@ -64,12 +67,15 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat
url: `datasources/edit/${dataSource.uid}/permissions`,
};
if (highlightTrial() && !isLoadingNav) {
if ((highlightTrial() && !isLoadingNav) || shouldEnableFeatureHighlights) {
dsPermissions.tabSuffix = () => ProBadge({ experimentId: permissionsExperimentId, eventVariant: 'trial' });
}
if (featureEnabled('dspermissions.enforcement')) {
if (contextSrv.hasPermissionInMetadata(AccessControlAction.DataSourcesPermissionsRead, dataSource)) {
if (featureEnabled('dspermissions.enforcement') || shouldEnableFeatureHighlights) {
if (
contextSrv.hasPermissionInMetadata(AccessControlAction.DataSourcesPermissionsRead, dataSource) ||
shouldEnableFeatureHighlights
) {
navModel.children!.push(dsPermissions);
}
} else if (highlightsEnabled && !isLoadingNav) {
@ -80,7 +86,7 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat
});
}
if (config.analytics?.enabled) {
if (config.analytics?.enabled || shouldEnableFeatureHighlights) {
const analyticsExperimentId = 'feature-highlights-data-source-insights-badge';
const analytics: NavModelItem = {
active: false,
@ -90,12 +96,12 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat
url: `datasources/edit/${dataSource.uid}/insights`,
};
if (highlightTrial() && !isLoadingNav) {
if ((highlightTrial() && !isLoadingNav) || shouldEnableFeatureHighlights) {
analytics.tabSuffix = () => ProBadge({ experimentId: analyticsExperimentId, eventVariant: 'trial' });
}
if (featureEnabled('analytics')) {
if (contextSrv.hasPermission(AccessControlAction.DataSourcesInsightsRead)) {
if (featureEnabled('analytics') || shouldEnableFeatureHighlights) {
if (contextSrv.hasPermission(AccessControlAction.DataSourcesInsightsRead) || shouldEnableFeatureHighlights) {
navModel.children!.push(analytics);
}
} else if (highlightsEnabled && !isLoadingNav) {
@ -118,12 +124,15 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat
hideFromTabs: !pluginMeta.isBackend || !config.caching.enabled,
};
if (highlightTrial() && !isLoadingNav) {
if ((highlightTrial() && !isLoadingNav) || shouldEnableFeatureHighlights) {
caching.tabSuffix = () => ProBadge({ experimentId: cachingExperimentId, eventVariant: 'trial' });
}
if (featureEnabled('caching')) {
if (contextSrv.hasPermissionInMetadata(AccessControlAction.DataSourcesCachingRead, dataSource)) {
if (featureEnabled('caching') || shouldEnableFeatureHighlights) {
if (
contextSrv.hasPermissionInMetadata(AccessControlAction.DataSourcesCachingRead, dataSource) ||
shouldEnableFeatureHighlights
) {
navModel.children!.push(caching);
}
} else if (highlightsEnabled && !isLoadingNav) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

View File

@ -4142,6 +4142,13 @@
"body": "Try the new Advisor to uncover potential issues with your data sources and plugins.",
"go-to-advisor": "Go to Advisor"
},
"cache-feature-highlight-page": {
"header": "Query caching can improve load times and reduce API costs by temporarily storing the results of data source queries. When you or other users submit the same query, the results will come back from the cache instead of from the data source.",
"item-1": "Faster dashboard load times, especially for popular dashboards.",
"item-2": "Reduced API costs.",
"item-3": "Reduced likelihood that APIs will rate-limit or throttle requests.",
"title": "Optimize queries with Query Caching in Grafana Cloud"
},
"cloud": {
"connections-home-page": {
"add-new-connection": {
@ -4190,6 +4197,20 @@
"unknown-datasource": "Unknown datasource"
}
},
"feature-highlight-page": {
"foot-note": "After creating an account, you can easily <2>migrate this instance to Grafana Cloud</2> with our Migration Assistant.",
"footer": "Create a Grafana Cloud Free account to start using data source permissions. This feature is also available with a Grafana Enterprise license.",
"footer-link": "Learn about Enterprise",
"link-button-label": "Create account"
},
"insights-feature-highlight-page": {
"header": "Usage Insights provide detailed information about data source usage, like the number of views, queries, and errors users have experienced. You can use this to improve users experience and troubleshoot issues.",
"item-1": "Demonstrate and improve the value of your observability service by keeping track of user engagement",
"item-2": "Keep Grafana performant and by identifying and fixing slow, error-prone data sources",
"item-3": "Clean up your instance by finding and removing unused data sources",
"item-4": "Review individual data source usage insights at a glance in the UI, sort search results by usage and errors, or dig into detailed usage logs",
"title": "Understand usage of your data sources with Grafana Cloud"
},
"new-data-source-page": {
"subTitle": {
"choose-a-data-source-type": "Choose a data source type"
@ -4220,6 +4241,13 @@
"subtitle": "Manage your data source connections in one place. Use this page to add a new data source or manage your existing connections."
}
},
"permissions-feature-highlight-page": {
"header": "With data source permissions, you can protect sensitive data by limiting access to this data source to specific users, teams, and roles.",
"item-1": "Protect sensitive data, like security logs, production databases, and personally-identifiable information",
"item-2": "Clean up users experience by hiding data sources they dont need to use",
"item-3": "Share Grafana access more freely, knowing that users will not unwittingly see sensitive data",
"title": "Secure access to data with data source permissions in Grafana Cloud"
},
"search": {
"aria-label-search-all": "Search all",
"placeholder": "Search all"