Accessibility: Add lint rule to prevent text anchor usage (#109207)

* attempt at a lint rule

* add comment

* fix the specific alerting links

* fix lint rule violations

* update translations

* fix unit tests

* kick CI

* add missing external
This commit is contained in:
Ashley Harrison 2025-08-06 16:04:03 +01:00 committed by GitHub
parent c91ad446ac
commit 797886e253
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 183 additions and 251 deletions

View File

@ -157,6 +157,14 @@ module.exports = [
'@typescript-eslint/no-redeclare': ['error'], '@typescript-eslint/no-redeclare': ['error'],
'unicorn/no-empty-file': 'error', 'unicorn/no-empty-file': 'error',
'no-constant-condition': 'error', 'no-constant-condition': 'error',
'no-restricted-syntax': [
'error',
{
// value regex is to filter out whitespace-only text nodes (e.g. new lines and spaces in the JSX)
selector: "JSXElement[openingElement.name.name='a'] > JSXText[value!=/^\\s*$/]",
message: 'No bare anchor nodes containing only text. Use `TextLink` instead.',
},
],
}, },
}, },
{ {

View File

@ -1,24 +1,48 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { TextLink } from '../Link/TextLink';
import { CallToActionCard } from './CallToActionCard'; import { CallToActionCard } from './CallToActionCard';
describe('CallToActionCard', () => { describe('CallToActionCard', () => {
describe('rendering', () => { describe('rendering', () => {
it('should render callToActionElement', () => { it('should render callToActionElement', () => {
render(<CallToActionCard callToActionElement={<a href="http://dummy.link">Click me</a>} />); render(
<CallToActionCard
callToActionElement={
<TextLink external href="http://dummy.link">
Click me
</TextLink>
}
/>
);
expect(screen.getByRole('link', { name: 'Click me' })).toBeInTheDocument(); expect(screen.getByRole('link', { name: 'Click me' })).toBeInTheDocument();
}); });
it('should render message when provided', () => { it('should render message when provided', () => {
render( render(
<CallToActionCard message="Click button below" callToActionElement={<a href="http://dummy.link">Click me</a>} /> <CallToActionCard
message="Click button below"
callToActionElement={
<TextLink external href="http://dummy.link">
Click me
</TextLink>
}
/>
); );
expect(screen.getByText('Click button below')).toBeInTheDocument(); expect(screen.getByText('Click button below')).toBeInTheDocument();
}); });
it('should render footer when provided', () => { it('should render footer when provided', () => {
render( render(
<CallToActionCard footer="footer content" callToActionElement={<a href="http://dummy.link">Click me</a>} /> <CallToActionCard
footer="footer content"
callToActionElement={
<TextLink external href="http://dummy.link">
Click me
</TextLink>
}
/>
); );
expect(screen.getByText('footer content')).toBeInTheDocument(); expect(screen.getByText('footer content')).toBeInTheDocument();
}); });
@ -28,7 +52,11 @@ describe('CallToActionCard', () => {
<CallToActionCard <CallToActionCard
message="Click button below" message="Click button below"
footer="footer content" footer="footer content"
callToActionElement={<a href="http://dummy.link">Click me</a>} callToActionElement={
<TextLink external href="http://dummy.link">
Click me
</TextLink>
}
/> />
); );
expect(screen.getByText('Click button below')).toBeInTheDocument(); expect(screen.getByText('Click button below')).toBeInTheDocument();

View File

@ -2,6 +2,7 @@ import { Meta, StoryFn } from '@storybook/react';
import { Button } from '../Button/Button'; import { Button } from '../Button/Button';
import { IconButton } from '../IconButton/IconButton'; import { IconButton } from '../IconButton/IconButton';
import { TextLink } from '../Link/TextLink';
import { TagList } from '../Tags/TagList'; import { TagList } from '../Tags/TagList';
import { Card } from './Card'; import { Card } from './Card';
@ -74,9 +75,9 @@ export const MultipleMetadataWithCustomSeparator: StoryFn<typeof Card> = (args)
<Card.Heading>Test dashboard</Card.Heading> <Card.Heading>Test dashboard</Card.Heading>
<Card.Meta separator={'-'}> <Card.Meta separator={'-'}>
Grafana Grafana
<a key="prom-link" href="https://ops-us-east4.grafana.net/api/prom"> <TextLink key="prom-link" href="https://ops-us-east4.grafana.net/api/prom" external>
https://ops-us-east4.grafana.net/api/prom https://ops-us-east4.grafana.net/api/prom
</a> </TextLink>
</Card.Meta> </Card.Meta>
</Card> </Card>
); );
@ -171,9 +172,9 @@ export const WithMediaElements: StoryFn<typeof Card> = (args) => {
</Card.Figure> </Card.Figure>
<Card.Meta> <Card.Meta>
Grafana Grafana
<a key="prom-link" href="https://ops-us-east4.grafana.net/api/prom"> <TextLink key="prom-link" href="https://ops-us-east4.grafana.net/api/prom" external>
https://ops-us-east4.grafana.net/api/prom https://ops-us-east4.grafana.net/api/prom
</a> </TextLink>
</Card.Meta> </Card.Meta>
</Card> </Card>
); );
@ -190,9 +191,9 @@ export const ActionCards: StoryFn<typeof Card> = (args) => {
<Card.Heading>1-ops-tools1-fallback</Card.Heading> <Card.Heading>1-ops-tools1-fallback</Card.Heading>
<Card.Meta> <Card.Meta>
Prometheus Prometheus
<a key="link2" href="https://ops-us-east4.grafana.net/api/prom"> <TextLink key="link2" href="https://ops-us-east4.grafana.net/api/prom" external>
https://ops-us-east4.grafana.net/api/prom https://ops-us-east4.grafana.net/api/prom
</a> </TextLink>
</Card.Meta> </Card.Meta>
<Card.Figure> <Card.Figure>
<img src={logo} alt="Prometheus Logo" height="40" width="40" /> <img src={logo} alt="Prometheus Logo" height="40" width="40" />
@ -223,9 +224,9 @@ export const DisabledState: StoryFn<typeof Card> = (args) => {
<Card.Heading>1-ops-tools1-fallback</Card.Heading> <Card.Heading>1-ops-tools1-fallback</Card.Heading>
<Card.Meta> <Card.Meta>
Grafana Grafana
<a key="prom-link" href="https://ops-us-east4.grafana.net/api/prom"> <TextLink key="prom-link" href="https://ops-us-east4.grafana.net/api/prom" external>
https://ops-us-east4.grafana.net/api/prom https://ops-us-east4.grafana.net/api/prom
</a> </TextLink>
</Card.Meta> </Card.Meta>
<Card.Figure> <Card.Figure>
<img src={logo} alt="Grafana Logo" width="40" height="40" /> <img src={logo} alt="Grafana Logo" width="40" height="40" />
@ -269,9 +270,9 @@ export const Full: StoryFn<typeof Card> = (args) => {
</Card.Description> </Card.Description>
<Card.Meta> <Card.Meta>
{['Subtitle', 'Meta info 1', 'Meta info 2']} {['Subtitle', 'Meta info 1', 'Meta info 2']}
<a key="link" href="https://ops-us-east4.grafana.net/api/prom"> <TextLink key="link" href="https://ops-us-east4.grafana.net/api/prom" external>
https://ops-us-east4.grafana.net/api/prom https://ops-us-east4.grafana.net/api/prom
</a> </TextLink>
</Card.Meta> </Card.Meta>
<Card.Figure> <Card.Figure>
<img src={logo} alt="Prometheus Logo" height="40" width="40" /> <img src={logo} alt="Prometheus Logo" height="40" width="40" />

View File

@ -9,6 +9,7 @@ import { useStyles2, useTheme2 } from '../../../themes/ThemeContext';
import { getFocusStyles } from '../../../themes/mixins'; import { getFocusStyles } from '../../../themes/mixins';
import { FilterInput } from '../../FilterInput/FilterInput'; import { FilterInput } from '../../FilterInput/FilterInput';
import { Icon } from '../../Icon/Icon'; import { Icon } from '../../Icon/Icon';
import { TextLink } from '../../Link/TextLink';
import { WeekStart } from '../WeekStartPicker'; import { WeekStart } from '../WeekStartPicker';
import { TimePickerFooter } from './TimePickerFooter'; import { TimePickerFooter } from './TimePickerFooter';
@ -234,13 +235,9 @@ const EmptyRecentList = memo(() => {
</div> </div>
<Trans i18nKey="time-picker.content.empty-recent-list-docs"> <Trans i18nKey="time-picker.content.empty-recent-list-docs">
<div> <div>
<a <TextLink href="https://grafana.com/docs/grafana/latest/dashboards/time-range-controls" external>
className={styles.link}
href="https://grafana.com/docs/grafana/latest/dashboards/time-range-controls"
target="_new"
>
Read the documentation Read the documentation
</a> </TextLink>
<span> to find out more about how to enter custom time ranges.</span> <span> to find out more about how to enter custom time ranges.</span>
</div> </div>
</Trans> </Trans>
@ -371,7 +368,4 @@ const getEmptyListStyles = (theme: GrafanaTheme2) => ({
fontSize: '13px', fontSize: '13px',
}, },
}), }),
link: css({
color: theme.colors.text.link,
}),
}); });

View File

@ -11,6 +11,7 @@ import { Button } from '../Button/Button';
import { RadioButtonGroup } from '../Forms/RadioButtonGroup/RadioButtonGroup'; import { RadioButtonGroup } from '../Forms/RadioButtonGroup/RadioButtonGroup';
import { Icon } from '../Icon/Icon'; import { Icon } from '../Icon/Icon';
import { Stack } from '../Layout/Stack/Stack'; import { Stack } from '../Layout/Stack/Stack';
import { TextLink } from '../Link/TextLink';
import { Menu } from '../Menu/Menu'; import { Menu } from '../Menu/Menu';
import { PanelChromeProps } from './PanelChrome'; import { PanelChromeProps } from './PanelChrome';
@ -252,10 +253,9 @@ export const Examples = () => {
{renderPanel('Panel with action link', { {renderPanel('Panel with action link', {
title: 'Panel with action link', title: 'Panel with action link',
actions: ( actions: (
<a className="external-link" href="/some/page"> <TextLink external href="http://www.example.com/some/page">
Error details Error details
<Icon name="arrow-right" /> </TextLink>
</a>
), ),
})} })}
{renderPanel('Action and menu (should be rare)', { {renderPanel('Action and menu (should be rare)', {
@ -322,10 +322,9 @@ export const ExamplesHoverHeader = () => {
title: 'With link in hover header', title: 'With link in hover header',
hoverHeader: true, hoverHeader: true,
actions: ( actions: (
<a className="external-link" href="/some/page"> <TextLink external href="http://www.example.com/some/page">
Error details Error details
<Icon name="arrow-right" /> </TextLink>
</a>
), ),
})} })}
</Stack> </Stack>

View File

@ -2,15 +2,17 @@
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { MutableRefObject } from 'react'; import { MutableRefObject } from 'react';
import { TextLink } from '../Link/TextLink';
import { Tooltip } from './Tooltip'; import { Tooltip } from './Tooltip';
describe('Tooltip', () => { describe('Tooltip', () => {
it('renders correctly', () => { it('renders correctly', () => {
render( render(
<Tooltip placement="auto" content="Tooltip text"> <Tooltip placement="auto" content="Tooltip text">
<a className="test-class" href="http://www.grafana.com"> <TextLink external href="http://www.grafana.com">
Link with tooltip Link with tooltip
</a> </TextLink>
</Tooltip> </Tooltip>
); );
expect(screen.getByText('Link with tooltip')).toBeInTheDocument(); expect(screen.getByText('Link with tooltip')).toBeInTheDocument();

View File

@ -4,7 +4,7 @@ import { useAsync } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Trans, t } from '@grafana/i18n'; import { Trans, t } from '@grafana/i18n';
import { useStyles2, Icon } from '@grafana/ui'; import { useStyles2, Icon, TextLink } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page'; import { Page } from 'app/core/components/Page/Page';
import { getTogglesAPI } from './AdminFeatureTogglesAPI'; import { getTogglesAPI } from './AdminFeatureTogglesAPI';
@ -45,13 +45,12 @@ export default function AdminFeatureTogglesPage() {
<div> <div>
<Trans i18nKey="admin.feature-toggles.sub-title"> <Trans i18nKey="admin.feature-toggles.sub-title">
View and edit feature toggles. Read more about feature toggles at{' '} View and edit feature toggles. Read more about feature toggles at{' '}
<a <TextLink
className="external-link"
target="_new"
href="https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/feature-toggles/" href="https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/feature-toggles/"
external
> >
grafana.com grafana.com
</a> </TextLink>
. .
</Trans> </Trans>
</div> </div>

View File

@ -15,6 +15,7 @@ import {
useStyles2, useStyles2,
withTheme2, withTheme2,
Stack, Stack,
TextLink,
} from '@grafana/ui'; } from '@grafana/ui';
import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker';
import { fetchRoleOptions, updateUserRoles } from 'app/core/components/RolePicker/api'; import { fetchRoleOptions, updateUserRoles } from 'app/core/components/RolePicker/api';
@ -446,14 +447,9 @@ export function ChangeOrgButton({
<Trans i18nKey="admin.user-orgs.role-not-editable"> <Trans i18nKey="admin.user-orgs.role-not-editable">
This user&apos;s role is not editable because it is synchronized from your auth provider. Refer to This user&apos;s role is not editable because it is synchronized from your auth provider. Refer to
the&nbsp; the&nbsp;
<a <TextLink href={'https://grafana.com/docs/grafana/latest/auth'} external>
className={styles.tooltipItemLink}
href={'https://grafana.com/docs/grafana/latest/auth'}
rel="noreferrer"
target="_blank"
>
Grafana authentication docs Grafana authentication docs
</a> </TextLink>
&nbsp;for details. &nbsp;for details.
</Trans> </Trans>
</div> </div>
@ -496,14 +492,9 @@ export const ExternalUserTooltip = ({ lockMessage }: ExternalUserTooltipProps) =
<Trans i18nKey="admin.user-orgs.external-user-tooltip"> <Trans i18nKey="admin.user-orgs.external-user-tooltip">
This user&apos;s built-in role is not editable because it is synchronized from your auth provider. Refer This user&apos;s built-in role is not editable because it is synchronized from your auth provider. Refer
to the&nbsp; to the&nbsp;
<a <TextLink href={'https://grafana.com/docs/grafana/latest/auth'} external>
className={styles.tooltipItemLink}
href={'https://grafana.com/docs/grafana/latest/auth'}
rel="noreferrer noopener"
target="_blank"
>
Grafana authentication docs Grafana authentication docs
</a> </TextLink>
&nbsp;for details. &nbsp;for details.
</Trans> </Trans>
</div> </div>
@ -519,9 +510,6 @@ const getTooltipStyles = (theme: GrafanaTheme2) => ({
disabledTooltip: css({ disabledTooltip: css({
display: 'flex', display: 'flex',
}), }),
tooltipItemLink: css({
color: theme.v1.palette.blue95,
}),
lockMessageClass: css({ lockMessageClass: css({
fontStyle: 'italic', fontStyle: 'italic',
marginLeft: '1.8rem', marginLeft: '1.8rem',

View File

@ -18,6 +18,7 @@ import {
Stack, Stack,
Tag, Tag,
Text, Text,
TextLink,
Tooltip, Tooltip,
} from '@grafana/ui'; } from '@grafana/ui';
import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker';
@ -189,15 +190,14 @@ export const OrgUsersTable = ({
<Trans i18nKey="admin.org-users.not-editable"> <Trans i18nKey="admin.org-users.not-editable">
This user&apos;s role is not editable because it is synchronized from your auth provider. Refer This user&apos;s role is not editable because it is synchronized from your auth provider. Refer
to the&nbsp; to the&nbsp;
<a <TextLink
href={ href={
'https://grafana.com/docs/grafana/latest/administration/user-management/manage-org-users/#change-a-users-organization-permissions' 'https://grafana.com/docs/grafana/latest/administration/user-management/manage-org-users/#change-a-users-organization-permissions'
} }
rel="noreferrer" external
target="_blank"
> >
Grafana authentication docs Grafana authentication docs
</a> </TextLink>
&nbsp;for details. &nbsp;for details.
</Trans> </Trans>
</div> </div>

View File

@ -196,9 +196,7 @@ function WelcomeCTABox({ title, description, href, hrefText }: WelcomeCTABoxProp
</Text> </Text>
<div className={styles.desc}>{description}</div> <div className={styles.desc}>{description}</div>
<div className={styles.actionRow}> <div className={styles.actionRow}>
<TextLink href={href} inline={false}> <TextLink href={href}>{hrefText}</TextLink>
{hrefText}
</TextLink>
</div> </div>
</div> </div>
); );

View File

@ -87,7 +87,7 @@ export function NoAccessModal({ item, isOpen, onDismiss }: NoAccessModalProps) {
<p> <p>
<Trans i18nKey="connections.no-access-modal.editor-warning"> <Trans i18nKey="connections.no-access-modal.editor-warning">
Editors cannot add new connections. You may check to see if it is already configured in{' '} Editors cannot add new connections. You may check to see if it is already configured in{' '}
<a href="/connections/datasources">Data sources</a>. <TextLink href="/connections/datasources">Data sources</TextLink>.
</Trans> </Trans>
</p> </p>
<p> <p>

View File

@ -17,7 +17,7 @@ import {
type CellProps, type CellProps,
type SortByFn, type SortByFn,
Pagination, Pagination,
Icon, TextLink,
} from '@grafana/ui'; } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page'; import { Page } from 'app/core/components/Page/Page';
import { contextSrv } from 'app/core/core'; import { contextSrv } from 'app/core/core';
@ -160,14 +160,9 @@ export default function CorrelationsPage() {
<> <>
<Trans i18nKey="correlations.sub-title"> <Trans i18nKey="correlations.sub-title">
Define how data living in different data sources relates to each other. Read more in the{' '} Define how data living in different data sources relates to each other. Read more in the{' '}
<a <TextLink href="https://grafana.com/docs/grafana/next/administration/correlations/" external>
href="https://grafana.com/docs/grafana/next/administration/correlations/"
target="_blank"
rel="noreferrer"
>
documentation documentation
<Icon name="external-link-alt" /> </TextLink>
</a>
</Trans> </Trans>
</> </>
} }

View File

@ -4,7 +4,7 @@ import { useAsync } from 'react-use';
import { CoreApp } from '@grafana/data'; import { CoreApp } from '@grafana/data';
import { Trans, t } from '@grafana/i18n'; import { Trans, t } from '@grafana/i18n';
import { getDataSourceSrv } from '@grafana/runtime'; import { getDataSourceSrv } from '@grafana/runtime';
import { Field, LoadingPlaceholder, Alert } from '@grafana/ui'; import { Field, LoadingPlaceholder, Alert, TextLink } from '@grafana/ui';
interface Props { interface Props {
dsUid?: string; dsUid?: string;
@ -34,13 +34,12 @@ export const QueryEditorField = ({ dsUid, invalid, error, name }: Props) => {
<span> <span>
<Trans i18nKey="correlations.query-editor.query-description"> <Trans i18nKey="correlations.query-editor.query-description">
Define the query that is run when the link is clicked. You can use{' '} Define the query that is run when the link is clicked. You can use{' '}
<a <TextLink
href="https://grafana.com/docs/grafana/latest/panels-visualizations/configure-data-links/" href="https://grafana.com/docs/grafana/latest/panels-visualizations/configure-data-links/"
target="_blank" external
rel="noreferrer"
> >
variables variables
</a>{' '} </TextLink>{' '}
to access specific field values. to access specific field values.
</Trans> </Trans>
</span> </span>

View File

@ -7,7 +7,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { Trans, t } from '@grafana/i18n'; import { Trans, t } from '@grafana/i18n';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes'; import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes';
import { Alert, Button, useStyles2 } from '@grafana/ui'; import { Alert, Button, TextLink, useStyles2 } from '@grafana/ui';
import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions';
import { getDashboardSceneFor } from 'app/features/dashboard-scene/utils/utils'; import { getDashboardSceneFor } from 'app/features/dashboard-scene/utils/utils';
@ -152,14 +152,9 @@ function RendererAlert() {
<div> <div>
<Trans i18nKey="share-modal.link.render-instructions"> <Trans i18nKey="share-modal.link.render-instructions">
To render an image, you must install the{' '} To render an image, you must install the{' '}
<a <TextLink href="https://grafana.com/grafana/plugins/grafana-image-renderer" external>
href="https://grafana.com/grafana/plugins/grafana-image-renderer"
target="_blank"
rel="noopener noreferrer"
className="external-link"
>
Grafana image renderer plugin Grafana image renderer plugin
</a> </TextLink>
. Please contact your Grafana administrator to install the plugin. . Please contact your Grafana administrator to install the plugin.
</Trans> </Trans>
</div> </div>

View File

@ -4,7 +4,7 @@ import { Trans, t } from '@grafana/i18n';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectRef, VizPanel } from '@grafana/scenes'; import { SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectRef, VizPanel } from '@grafana/scenes';
import { TimeZone } from '@grafana/schema'; import { TimeZone } from '@grafana/schema';
import { Alert, ClipboardButton, Field, FieldSet, Icon, Input, Switch } from '@grafana/ui'; import { Alert, ClipboardButton, Field, FieldSet, Icon, Input, Switch, TextLink } from '@grafana/ui';
import { createDashboardShareUrl, createShortLink, getShareUrlParams } from 'app/core/utils/shortLinks'; import { createDashboardShareUrl, createShortLink, getShareUrlParams } from 'app/core/utils/shortLinks';
import { ThemePicker } from 'app/features/dashboard/components/ShareModal/ThemePicker'; import { ThemePicker } from 'app/features/dashboard/components/ShareModal/ThemePicker';
import { getTrackingSource, shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils'; import { getTrackingSource, shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils';
@ -215,14 +215,9 @@ function ShareLinkTabRenderer({ model }: SceneComponentProps<ShareLinkTab>) {
> >
<Trans i18nKey="share-modal.link.render-instructions"> <Trans i18nKey="share-modal.link.render-instructions">
To render an image, you must install the{' '} To render an image, you must install the{' '}
<a <TextLink href="https://grafana.com/grafana/plugins/grafana-image-renderer" external>
href="https://grafana.com/grafana/plugins/grafana-image-renderer"
target="_blank"
rel="noopener noreferrer"
className="external-link"
>
Grafana image renderer plugin Grafana image renderer plugin
</a> </TextLink>
. Please contact your Grafana administrator to install the plugin. . Please contact your Grafana administrator to install the plugin.
</Trans> </Trans>
</Alert> </Alert>

View File

@ -4,7 +4,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { Trans, t } from '@grafana/i18n'; import { Trans, t } from '@grafana/i18n';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { SceneComponentProps } from '@grafana/scenes'; import { SceneComponentProps } from '@grafana/scenes';
import { Alert, ClipboardButton, Divider, Stack, Text, useStyles2 } from '@grafana/ui'; import { Alert, ClipboardButton, Divider, Stack, Text, TextLink, useStyles2 } from '@grafana/ui';
import { getDashboardSceneFor } from '../../utils/utils'; import { getDashboardSceneFor } from '../../utils/utils';
import ShareInternallyConfiguration from '../ShareInternallyConfiguration'; import ShareInternallyConfiguration from '../ShareInternallyConfiguration';
@ -79,14 +79,9 @@ function SharePanelInternallyRenderer({ model }: SceneComponentProps<SharePanelI
> >
<Trans i18nKey="share-modal.link.render-instructions"> <Trans i18nKey="share-modal.link.render-instructions">
To render an image, you must install the{' '} To render an image, you must install the{' '}
<a <TextLink href="https://grafana.com/grafana/plugins/grafana-image-renderer" external>
href="https://grafana.com/grafana/plugins/grafana-image-renderer"
target="_blank"
rel="noopener noreferrer"
className="external-link"
>
Grafana image renderer plugin Grafana image renderer plugin
</a> </TextLink>
. Please contact your Grafana administrator to install the plugin. . Please contact your Grafana administrator to install the plugin.
</Trans> </Trans>
</Alert> </Alert>

View File

@ -89,7 +89,7 @@ export function HelpWizard({ panel, plugin, onClose }: Props) {
<span className="muted"> <span className="muted">
<Trans i18nKey="help-wizard.support-bundle"> <Trans i18nKey="help-wizard.support-bundle">
You can also retrieve a support bundle containing information concerning your Grafana instance and You can also retrieve a support bundle containing information concerning your Grafana instance and
configured datasources in the <a href="/support-bundles">support bundles section</a>. configured datasources in the <TextLink href="/support-bundles">support bundles section</TextLink>.
</Trans> </Trans>
</span> </span>
)} )}

View File

@ -4,7 +4,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { Trans } from '@grafana/i18n'; import { Trans } from '@grafana/i18n';
import { locationService } from '@grafana/runtime'; import { locationService } from '@grafana/runtime';
import { Button, useStyles2, Text, Box, Stack } from '@grafana/ui'; import { Button, useStyles2, Text, Box, Stack, TextLink } from '@grafana/ui';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { import {
onAddLibraryPanel as onAddLibraryPanelImpl, onAddLibraryPanel as onAddLibraryPanelImpl,
@ -115,7 +115,11 @@ const DashboardEmpty = ({ dashboard, canCreate }: Props) => {
<Box marginBottom={2}> <Box marginBottom={2}>
<Text element="p" textAlignment="center" color="secondary"> <Text element="p" textAlignment="center" color="secondary">
<Trans i18nKey="dashboard.empty.import-a-dashboard-body"> <Trans i18nKey="dashboard.empty.import-a-dashboard-body">
Import dashboards from files or <a href="https://grafana.com/grafana/dashboards/">grafana.com</a>. Import dashboards from files or{' '}
<TextLink external href="https://grafana.com/grafana/dashboards/">
grafana.com
</TextLink>
.
</Trans> </Trans>
</Text> </Text>
</Box> </Box>

View File

@ -3,7 +3,7 @@ import Prism from 'prismjs';
import { useState } from 'react'; import { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Collapse, useStyles2, Text } from '@grafana/ui'; import { Collapse, useStyles2, Text, TextLink } from '@grafana/ui';
import { flattenTokens } from '@grafana/ui/internal'; import { flattenTokens } from '@grafana/ui/internal';
import { trackSampleQuerySelection } from '../../tracking'; import { trackSampleQuerySelection } from '../../tracking';
@ -145,14 +145,12 @@ const LogsCheatSheet = (props: Props) => {
))} ))}
<div> <div>
Note: If you are seeing masked data, you may have CloudWatch logs data protection enabled.{' '} Note: If you are seeing masked data, you may have CloudWatch logs data protection enabled.{' '}
<a <TextLink
className={styles.link}
href="https://grafana.com/docs/grafana/latest/datasources/aws-cloudwatch/#cloudwatch-logs-data-protection" href="https://grafana.com/docs/grafana/latest/datasources/aws-cloudwatch/#cloudwatch-logs-data-protection"
target="_blank" external
rel="noreferrer"
> >
See documentation for details See documentation for details
</a> </TextLink>
. .
</div> </div>
</div> </div>
@ -165,9 +163,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
heading: css({ heading: css({
marginBottom: theme.spacing(2), marginBottom: theme.spacing(2),
}), }),
link: css({
textDecoration: 'underline',
}),
cheatSheetExample: css({ cheatSheetExample: css({
margin: theme.spacing(0.5, 0), margin: theme.spacing(0.5, 0),
// element is interactive, clear button styles // element is interactive, clear button styles

View File

@ -1,3 +1,5 @@
import { TextLink } from '@grafana/ui';
export interface Props { export interface Props {
region: string; region: string;
} }
@ -5,23 +7,16 @@ export interface Props {
export const ThrottlingErrorMessage = ({ region }: Props) => ( export const ThrottlingErrorMessage = ({ region }: Props) => (
<p> <p>
Please visit the&nbsp; Please visit the&nbsp;
<a <TextLink
target="_blank" external
rel="noreferrer"
className="text-link"
href={`https://${region}.console.aws.amazon.com/servicequotas/home?region=${region}#!/services/monitoring/quotas/L-5E141212`} href={`https://${region}.console.aws.amazon.com/servicequotas/home?region=${region}#!/services/monitoring/quotas/L-5E141212`}
> >
AWS Service Quotas console AWS Service Quotas console
</a> </TextLink>
&nbsp;to request a quota increase or see our&nbsp; &nbsp;to request a quota increase or see our&nbsp;
<a <TextLink external href="https://grafana.com/docs/grafana/latest/datasources/cloudwatch/#manage-service-quotas">
target="_blank"
rel="noreferrer"
className="text-link"
href="https://grafana.com/docs/grafana/latest/datasources/cloudwatch/#manage-service-quotas"
>
documentation documentation
</a> </TextLink>
&nbsp;to learn more. &nbsp;to learn more.
</p> </p>
); );

View File

@ -3,7 +3,7 @@ import { css } from '@emotion/css';
import { GrafanaTheme2, QueryEditorProps, SelectableValue, toOption } from '@grafana/data'; import { GrafanaTheme2, QueryEditorProps, SelectableValue, toOption } from '@grafana/data';
import { EditorField } from '@grafana/plugin-ui'; import { EditorField } from '@grafana/plugin-ui';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { useStyles2 } from '@grafana/ui'; import { TextLink, useStyles2 } from '@grafana/ui';
import { CloudWatchDatasource } from '../../datasource'; import { CloudWatchDatasource } from '../../datasource';
import { import {
@ -257,13 +257,12 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
tooltip={ tooltip={
<> <>
{'Attribute or tag to query on. Tags should be formatted "Tags.<name>". '} {'Attribute or tag to query on. Tags should be formatted "Tags.<name>". '}
<a <TextLink
href="https://grafana.com/docs/grafana/latest/datasources/aws-cloudwatch/template-queries-cloudwatch/#selecting-attributes" href="https://grafana.com/docs/grafana/latest/datasources/aws-cloudwatch/template-queries-cloudwatch/#selecting-attributes"
target="_blank" external
rel="noreferrer"
> >
See the documentation for more details See the documentation for more details
</a> </TextLink>
</> </>
} }
/> />
@ -272,13 +271,12 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
tooltipInteractive tooltipInteractive
tooltip={ tooltip={
<> <>
<a <TextLink
href="https://grafana.com/docs/grafana/latest/datasources/aws-cloudwatch/template-queries-cloudwatch/#selecting-attributes" href="https://grafana.com/docs/grafana/latest/datasources/aws-cloudwatch/template-queries-cloudwatch/#selecting-attributes"
target="_blank" external
rel="noreferrer"
> >
Pre-defined ec2:DescribeInstances filters/tags Pre-defined ec2:DescribeInstances filters/tags
</a> </TextLink>
{' and the values to filter on. Tags should be formatted tag:<name>.'} {' and the values to filter on. Tags should be formatted tag:<name>.'}
</> </>
} }

View File

@ -2,7 +2,18 @@ import { useEffect, useMemo, useState } from 'react';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { EditorField } from '@grafana/plugin-ui'; import { EditorField } from '@grafana/plugin-ui';
import { Button, Checkbox, Icon, Label, LoadingPlaceholder, Modal, Select, Space, useStyles2 } from '@grafana/ui'; import {
Button,
Checkbox,
Icon,
Label,
LoadingPlaceholder,
Modal,
Select,
Space,
TextLink,
useStyles2,
} from '@grafana/ui';
import { DescribeLogGroupsRequest, ResourceResponse, LogGroupResponse } from '../../../resources/types'; import { DescribeLogGroupsRequest, ResourceResponse, LogGroupResponse } from '../../../resources/types';
import { LogGroup } from '../../../types'; import { LogGroup } from '../../../types';
@ -146,13 +157,12 @@ export const LogGroupsSelector = ({
search. search.
<p> <p>
A{' '} A{' '}
<a <TextLink
target="_blank" external
rel="noopener noreferrer"
href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/cloudwatch_limits_cwl.html" href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/cloudwatch_limits_cwl.html"
> >
maximum{' '} maximum{' '}
</a>{' '} </TextLink>{' '}
of 50 Cloudwatch log groups can be queried at one time. of 50 Cloudwatch log groups can be queried at one time.
</p> </p>
</div> </div>

View File

@ -4,7 +4,7 @@ import * as React from 'react';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { EditorField, EditorFieldGroup, EditorRow, EditorRows, EditorSwitch } from '@grafana/plugin-ui'; import { EditorField, EditorFieldGroup, EditorRow, EditorRows, EditorSwitch } from '@grafana/plugin-ui';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { Select } from '@grafana/ui'; import { Select, TextLink } from '@grafana/ui';
import { CloudWatchDatasource } from '../../../datasource'; import { CloudWatchDatasource } from '../../../datasource';
import { useAccountOptions, useMetrics, useNamespaces } from '../../../hooks'; import { useAccountOptions, useMetrics, useNamespaces } from '../../../hooks';
@ -152,13 +152,12 @@ export const MetricStatEditor = ({
{ {
'Only show metrics that contain exactly the dimensions defined in the query and match the specified values. If this is enabled, all dimensions of the metric being queried must be specified so that the ' 'Only show metrics that contain exactly the dimensions defined in the query and match the specified values. If this is enabled, all dimensions of the metric being queried must be specified so that the '
} }
<a <TextLink
href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/search-expression-syntax.html" href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/search-expression-syntax.html"
target="_blank" external
rel="noreferrer"
> >
metric schema metric schema
</a> </TextLink>
{ {
' matches exactly. If this is disabled, metrics that match the schema and have additional dimensions will also be returned.' ' matches exactly. If this is disabled, metrics that match the schema and have additional dimensions will also be returned.'
} }

View File

@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import { memo } from 'react'; import { memo } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Icon, Tooltip, useStyles2, type PopoverContent } from '@grafana/ui'; import { Icon, TextLink, Tooltip, useStyles2, type PopoverContent } from '@grafana/ui';
import { FuncInstance } from '../gfunc'; import { FuncInstance } from '../gfunc';
@ -64,14 +64,9 @@ const TooltipContent = memo(() => {
return ( return (
<span> <span>
This function is not supported. Check your function for typos and{' '} This function is not supported. Check your function for typos and{' '}
<a <TextLink external href="https://graphite.readthedocs.io/en/latest/functions.html">
target="_blank"
className="external-link"
rel="noreferrer noopener"
href="https://graphite.readthedocs.io/en/latest/functions.html"
>
read the docs read the docs
</a>{' '} </TextLink>{' '}
to see whether you need to upgrade your data sources version to make this function available. to see whether you need to upgrade your data sources version to make this function available.
</span> </span>
); );

View File

@ -8,7 +8,7 @@ import {
updateDatasourcePluginJsonDataOption, updateDatasourcePluginJsonDataOption,
} from '@grafana/data'; } from '@grafana/data';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { Alert, DataSourceHttpSettings, InlineField, Select, Field, Input, FieldSet } from '@grafana/ui'; import { Alert, DataSourceHttpSettings, InlineField, Select, Field, Input, FieldSet, TextLink } from '@grafana/ui';
import { BROWSER_MODE_DISABLED_MESSAGE } from '../../../constants'; import { BROWSER_MODE_DISABLED_MESSAGE } from '../../../constants';
import { InfluxOptions, InfluxOptionsV1, InfluxVersion } from '../../../types'; import { InfluxOptions, InfluxOptionsV1, InfluxVersion } from '../../../types';
@ -130,9 +130,9 @@ export class ConfigEditor extends PureComponent<Props, State> {
<Alert severity="info" title={this.versionNotice[options.jsonData.version!]}> <Alert severity="info" title={this.versionNotice[options.jsonData.version!]}>
<p> <p>
Please report any issues to: <br /> Please report any issues to: <br />
<a href="https://github.com/grafana/grafana/issues/new/choose"> <TextLink href="https://github.com/grafana/grafana/issues/new/choose" external>
https://github.com/grafana/grafana/issues https://github.com/grafana/grafana/issues
</a> </TextLink>
</p> </p>
</Alert> </Alert>
)} )}

View File

@ -1,7 +1,7 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui'; import { TextLink, useStyles2 } from '@grafana/ui';
export default function CheatSheet() { export default function CheatSheet() {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
@ -11,14 +11,9 @@ export default function CheatSheet() {
<p> <p>
This cheat sheet provides a quick overview of the query types that are available. For more details about the This cheat sheet provides a quick overview of the query types that are available. For more details about the
Jaeger data source, check out{' '} Jaeger data source, check out{' '}
<a <TextLink href="https://grafana.com/docs/grafana/latest/datasources/jaeger" external>
href="https://grafana.com/docs/grafana/latest/datasources/jaeger"
target="_blank"
rel="noreferrer"
className={styles.anchorTag}
>
the documentation the documentation
</a> </TextLink>
. .
</p> </p>
@ -32,14 +27,9 @@ export default function CheatSheet() {
<li> <li>
JSON File - you can upload a JSON file that contains a single trace to visualize it. If the file has multiple JSON File - you can upload a JSON file that contains a single trace to visualize it. If the file has multiple
traces then the first trace is used for visualization. An example of a valid JSON file can be found in{' '} traces then the first trace is used for visualization. An example of a valid JSON file can be found in{' '}
<a <TextLink href="https://grafana.com/docs/grafana/latest/datasources/jaeger/#upload-json-trace-file" external>
href="https://grafana.com/docs/grafana/latest/datasources/jaeger/#upload-json-trace-file"
target="_blank"
rel="noreferrer"
className={styles.anchorTag}
>
this section this section
</a>{' '} </TextLink>{' '}
of the documentation. of the documentation.
</li> </li>
</ul> </ul>
@ -48,9 +38,6 @@ export default function CheatSheet() {
} }
const getStyles = (theme: GrafanaTheme2) => ({ const getStyles = (theme: GrafanaTheme2) => ({
anchorTag: css({
color: theme.colors.text.link,
}),
unorderedList: css({ unorderedList: css({
listStyleType: 'none', listStyleType: 'none',
}), }),

View File

@ -4,7 +4,7 @@ import { PureComponent } from 'react';
import { GrafanaTheme2, QueryEditorHelpProps } from '@grafana/data'; import { GrafanaTheme2, QueryEditorHelpProps } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime'; import { reportInteraction } from '@grafana/runtime';
import { Themeable2, withTheme2 } from '@grafana/ui'; import { TextLink, Themeable2, withTheme2 } from '@grafana/ui';
import LokiLanguageProvider from '../LanguageProvider'; import LokiLanguageProvider from '../LanguageProvider';
import { escapeLabelValueInExactSelector } from '../languageUtils'; import { escapeLabelValueInExactSelector } from '../languageUtils';
@ -135,9 +135,9 @@ class UnthemedLokiCheatSheet extends PureComponent<
{this.renderExpression('{app="cassandra"} |~ "(duration|latency)s*(=|is|of)s*[d.]+"')} {this.renderExpression('{app="cassandra"} |~ "(duration|latency)s*(=|is|of)s*[d.]+"')}
{this.renderExpression('{app="cassandra"} |= "exact match"')} {this.renderExpression('{app="cassandra"} |= "exact match"')}
{this.renderExpression('{app="cassandra"} != "do not match"')} {this.renderExpression('{app="cassandra"} != "do not match"')}
<a href="https://grafana.com/docs/loki/latest/logql/#log-pipeline" target="logql"> <TextLink href="https://grafana.com/docs/loki/latest/logql/#log-pipeline" external>
LogQL LogQL
</a>{' '} </TextLink>{' '}
supports exact and regular expression filters. supports exact and regular expression filters.
</div> </div>
{LOGQL_EXAMPLES.map((item) => ( {LOGQL_EXAMPLES.map((item) => (

View File

@ -6,7 +6,7 @@ import { DataSourcePluginOptionsEditorProps, GrafanaTheme2 } from '@grafana/data
import { AdvancedHttpSettings, ConfigSection, DataSourceDescription } from '@grafana/plugin-ui'; import { AdvancedHttpSettings, ConfigSection, DataSourceDescription } from '@grafana/plugin-ui';
import { AlertingSettingsOverhaul, PromOptions, PromSettings } from '@grafana/prometheus'; import { AlertingSettingsOverhaul, PromOptions, PromSettings } from '@grafana/prometheus';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { Alert, FieldValidationMessage, useTheme2 } from '@grafana/ui'; import { Alert, FieldValidationMessage, TextLink, useTheme2 } from '@grafana/ui';
import { AzureAuthSettings } from './AzureAuthSettings'; import { AzureAuthSettings } from './AzureAuthSettings';
import { AzurePromDataSourceSettings, setDefaultCredentials, resetCredentials } from './AzureCredentialsConfig'; import { AzurePromDataSourceSettings, setDefaultCredentials, resetCredentials } from './AzureCredentialsConfig';
@ -78,9 +78,9 @@ export function docsTip(url?: string) {
const docsUrl = 'https://grafana.com/docs/grafana/latest/datasources/prometheus/#configure-the-data-source'; const docsUrl = 'https://grafana.com/docs/grafana/latest/datasources/prometheus/#configure-the-data-source';
return ( return (
<a href={url ? url : docsUrl} target="_blank" rel="noopener noreferrer"> <TextLink href={url ? url : docsUrl} external>
Visit docs for more details here. Visit docs for more details here.
</a> </TextLink>
); );
} }

View File

@ -4,7 +4,7 @@ import { useCallback, useEffect, useState } from 'react';
import { CoreApp, GrafanaTheme2, TimeRange } from '@grafana/data'; import { CoreApp, GrafanaTheme2, TimeRange } from '@grafana/data';
import { TemporaryAlert } from '@grafana/o11y-ds-frontend'; import { TemporaryAlert } from '@grafana/o11y-ds-frontend';
import { config, FetchError, getTemplateSrv, reportInteraction } from '@grafana/runtime'; import { config, FetchError, getTemplateSrv, reportInteraction } from '@grafana/runtime';
import { Alert, Button, Stack, Select, useStyles2 } from '@grafana/ui'; import { Alert, Button, Stack, Select, useStyles2, TextLink } from '@grafana/ui';
import { RawQuery } from '../_importedDependencies/datasources/prometheus/RawQuery'; import { RawQuery } from '../_importedDependencies/datasources/prometheus/RawQuery';
import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen'; import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen';
@ -304,7 +304,7 @@ const TraceQLSearch = ({
{error ? ( {error ? (
<Alert title="Unable to connect to Tempo search" severity="info" className={styles.alert}> <Alert title="Unable to connect to Tempo search" severity="info" className={styles.alert}>
Please ensure that Tempo is configured with search enabled. If you would like to hide this tab, you can Please ensure that Tempo is configured with search enabled. If you would like to hide this tab, you can
configure it in the <a href={`/datasources/edit/${datasource.uid}`}>datasource settings</a>. configure it in the <TextLink href={`/datasources/edit/${datasource.uid}`}>datasource settings</TextLink>.
</Alert> </Alert>
) : null} ) : null}
{alertText && <TemporaryAlert severity={'error'} text={alertText} />} {alertText && <TemporaryAlert severity={'error'} text={alertText} />}

View File

@ -3,7 +3,7 @@ import { useEffect, useState } from 'react';
import useAsync from 'react-use/lib/useAsync'; import useAsync from 'react-use/lib/useAsync';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Alert, InlineField, InlineFieldRow, useStyles2 } from '@grafana/ui'; import { Alert, InlineField, InlineFieldRow, TextLink, useStyles2 } from '@grafana/ui';
import { AdHocFilter } from './_importedDependencies/components/AdHocFilter/AdHocFilter'; import { AdHocFilter } from './_importedDependencies/components/AdHocFilter/AdHocFilter';
import { AdHocVariableFilter } from './_importedDependencies/components/AdHocFilter/types'; import { AdHocVariableFilter } from './_importedDependencies/components/AdHocFilter/types';
@ -116,18 +116,13 @@ export function ServiceGraphSection({
); );
} }
function getWarning(title: string, description: string, styles: { alert: string; link: string }) { function getWarning(title: string, description: string, styles: { alert: string }) {
return ( return (
<Alert title={title} severity="info" className={styles.alert}> <Alert title={title} severity="info" className={styles.alert}>
{description} according to the{' '} {description} according to the{' '}
<a <TextLink external href="https://grafana.com/docs/grafana/latest/datasources/tempo/service-graph/">
target="_blank"
rel="noreferrer noopener"
href="https://grafana.com/docs/grafana/latest/datasources/tempo/service-graph/"
className={styles.link}
>
Tempo documentation Tempo documentation
</a> </TextLink>
. .
</Alert> </Alert>
); );
@ -157,8 +152,4 @@ const getStyles = (theme: GrafanaTheme2) => ({
maxWidth: '75ch', maxWidth: '75ch',
marginTop: theme.spacing(2), marginTop: theme.spacing(2),
}), }),
link: css({
color: theme.colors.text.link,
textDecoration: 'underline',
}),
}); });

View File

@ -4,7 +4,7 @@ import {
updateDatasourcePluginJsonDataOption, updateDatasourcePluginJsonDataOption,
} from '@grafana/data'; } from '@grafana/data';
import { DataSourcePicker } from '@grafana/runtime'; import { DataSourcePicker } from '@grafana/runtime';
import { Button, InlineField, InlineFieldRow, useStyles2, Combobox } from '@grafana/ui'; import { Button, InlineField, InlineFieldRow, useStyles2, Combobox, TextLink } from '@grafana/ui';
import { TempoJsonData } from '../types'; import { TempoJsonData } from '../types';
@ -31,40 +31,28 @@ export function ServiceGraphSettings({ options, onOptionsChange }: Props) {
function metricsGeneratorDocsLink() { function metricsGeneratorDocsLink() {
return ( return (
<a <TextLink href="https://grafana.com/docs/tempo/latest/setup-and-configuration/metrics-generator/" external>
style={{ textDecoration: 'underline' }}
href="https://grafana.com/docs/tempo/latest/setup-and-configuration/metrics-generator/"
target="_blank"
rel="noopener noreferrer"
>
Tempo metrics generator Tempo metrics generator
</a> </TextLink>
); );
} }
function prometheusNativeHistogramsDocsLink() { function prometheusNativeHistogramsDocsLink() {
return ( return (
<a <TextLink href="https://prometheus.io/docs/specs/native_histograms/#native-histograms" external>
style={{ textDecoration: 'underline' }}
href="https://prometheus.io/docs/specs/native_histograms/#native-histograms"
target="_blank"
rel="noopener noreferrer"
>
Prometheus Prometheus
</a> </TextLink>
); );
} }
function mimirNativeHistogramsDocsLink() { function mimirNativeHistogramsDocsLink() {
return ( return (
<a <TextLink
style={{ textDecoration: 'underline' }}
href="https://grafana.com/docs/mimir/latest/configure/configure-native-histograms-ingestion/#configure-native-histograms-globally" href="https://grafana.com/docs/mimir/latest/configure/configure-native-histograms-ingestion/#configure-native-histograms-globally"
target="_blank" external
rel="noopener noreferrer"
> >
Mimir Mimir
</a> </TextLink>
); );
} }

View File

@ -1,14 +1,12 @@
import { css } from '@emotion/css';
import React from 'react'; import React from 'react';
import { import {
DataSourceJsonData, DataSourceJsonData,
DataSourcePluginOptionsEditorProps, DataSourcePluginOptionsEditorProps,
GrafanaTheme2,
updateDatasourcePluginJsonDataOption, updateDatasourcePluginJsonDataOption,
} from '@grafana/data'; } from '@grafana/data';
import { ConfigSection } from '@grafana/plugin-ui'; import { ConfigSection } from '@grafana/plugin-ui';
import { InlineFieldRow, InlineField, InlineSwitch, Alert, Stack, useStyles2 } from '@grafana/ui'; import { InlineFieldRow, InlineField, InlineSwitch, Alert, Stack, TextLink } from '@grafana/ui';
import { FeatureName, featuresToTempoVersion } from '../datasource'; import { FeatureName, featuresToTempoVersion } from '../datasource';
@ -21,7 +19,6 @@ interface StreamingOptions extends DataSourceJsonData {
interface Props extends DataSourcePluginOptionsEditorProps<StreamingOptions> {} interface Props extends DataSourcePluginOptionsEditorProps<StreamingOptions> {}
export const StreamingSection = ({ options, onOptionsChange }: Props) => { export const StreamingSection = ({ options, onOptionsChange }: Props) => {
const styles = useStyles2(getStyles);
return ( return (
<ConfigSection <ConfigSection
title="Streaming" title="Streaming"
@ -29,14 +26,9 @@ export const StreamingSection = ({ options, onOptionsChange }: Props) => {
description={ description={
<Stack gap={0.5}> <Stack gap={0.5}>
<div>Enable streaming for different Tempo features.</div> <div>Enable streaming for different Tempo features.</div>
<a <TextLink external href={'https://grafana.com/docs/tempo/latest/traceql/#stream-query-results'}>
href={'https://grafana.com/docs/tempo/latest/traceql/#stream-query-results'}
target={'_blank'}
rel="noreferrer"
className={styles.a}
>
Learn more Learn more
</a> </TextLink>
</Stack> </Stack>
} }
> >
@ -87,15 +79,3 @@ export const StreamingSection = ({ options, onOptionsChange }: Props) => {
</ConfigSection> </ConfigSection>
); );
}; };
const getStyles = (theme: GrafanaTheme2) => {
return {
a: css({
color: theme.colors.text.link,
textDecoration: 'underline',
marginLeft: '5px',
'&:hover': {
textDecoration: 'none',
},
}),
};
};

View File

@ -4,7 +4,7 @@ import { useState } from 'react';
import { CoreApp, GrafanaTheme2, QueryEditorProps } from '@grafana/data'; import { CoreApp, GrafanaTheme2, QueryEditorProps } from '@grafana/data';
import { config, reportInteraction } from '@grafana/runtime'; import { config, reportInteraction } from '@grafana/runtime';
import { Alert, Button, Icon, InlineLabel, useStyles2 } from '@grafana/ui'; import { Alert, Button, InlineLabel, TextLink, useStyles2 } from '@grafana/ui';
import { TempoDatasource } from '../datasource'; import { TempoDatasource } from '../datasource';
import { defaultQuery, MyDataSourceOptions, TempoQuery } from '../types'; import { defaultQuery, MyDataSourceOptions, TempoQuery } from '../types';
@ -32,14 +32,9 @@ export function QueryEditor(props: Props) {
<Alert title="Tempo metrics is an experimental feature" severity="warning"> <Alert title="Tempo metrics is an experimental feature" severity="warning">
Please note that TraceQL metrics is an experimental feature and should not be used in production. Read more about Please note that TraceQL metrics is an experimental feature and should not be used in production. Read more about
it in{' '} it in{' '}
<a <TextLink external href="https://grafana.com/docs/tempo/latest/operations/traceql-metrics/">
className={css({ textDecoration: 'underline' })}
href="https://grafana.com/docs/tempo/latest/operations/traceql-metrics/"
target="_blank"
>
documentation documentation
<Icon name="external-link-alt" /> </TextLink>
</a>
. .
</Alert> </Alert>
); );
@ -50,9 +45,9 @@ export function QueryEditor(props: Props) {
{inAlerting && alertingWarning} {inAlerting && alertingWarning}
<InlineLabel> <InlineLabel>
Build complex queries using TraceQL to select a list of traces.{' '} Build complex queries using TraceQL to select a list of traces.{' '}
<a rel="noreferrer" target="_blank" href="https://grafana.com/docs/tempo/latest/traceql/"> <TextLink external href="https://grafana.com/docs/tempo/latest/traceql/">
Documentation Documentation
</a> </TextLink>
</InlineLabel> </InlineLabel>
{!showCopyFromSearchButton && ( {!showCopyFromSearchButton && (
<div className={styles.copyContainer}> <div className={styles.copyContainer}>

View File

@ -4,7 +4,7 @@ import { useToggle } from 'react-use';
import { CoreApp, GrafanaTheme2 } from '@grafana/data'; import { CoreApp, GrafanaTheme2 } from '@grafana/data';
import { EditorField, EditorRow } from '@grafana/plugin-ui'; import { EditorField, EditorRow } from '@grafana/plugin-ui';
import { AutoSizeInput, RadioButtonGroup, useStyles2 } from '@grafana/ui'; import { AutoSizeInput, RadioButtonGroup, TextLink, useStyles2 } from '@grafana/ui';
import { QueryOptionGroup } from '../_importedDependencies/datasources/prometheus/QueryOptionGroup'; import { QueryOptionGroup } from '../_importedDependencies/datasources/prometheus/QueryOptionGroup';
import { SearchTableType, MetricsQueryType } from '../dataquery.gen'; import { SearchTableType, MetricsQueryType } from '../dataquery.gen';
@ -209,15 +209,14 @@ const StreamingTooltip = () => {
Indicates if streaming is currently enabled. Streaming allows you to view partial query results before the Indicates if streaming is currently enabled. Streaming allows you to view partial query results before the
entire query completes. entire query completes.
</span> </span>
<a <TextLink
external
href={'https://grafana.com/docs/tempo/latest/traceql/#stream-query-results'} href={'https://grafana.com/docs/tempo/latest/traceql/#stream-query-results'}
aria-label={'Learn more about streaming query results'} aria-label={'Learn more about streaming query results'}
target={'_blank'}
rel="noreferrer"
style={{ textDecoration: 'underline' }} style={{ textDecoration: 'underline' }}
> >
Learn more Learn more
</a> </TextLink>
</div> </div>
); );
}; };

View File

@ -4319,7 +4319,7 @@
"source-label": "Source", "source-label": "Source",
"sub-text": "<0>Define what data source will display the correlation, and what data will replace previously defined variables.</0>" "sub-text": "<0>Define what data source will display the correlation, and what data will replace previously defined variables.</0>"
}, },
"sub-title": "Define how data living in different data sources relates to each other. Read more in the <2>documentation<1></1></2>", "sub-title": "Define how data living in different data sources relates to each other. Read more in the <2>documentation</2>",
"target-form": { "target-form": {
"control-rules": "This field is required.", "control-rules": "This field is required.",
"sub-text": "<0>Define what the correlation will link to. With the query type, a query will run when the correlation is clicked. With the external type, clicking the correlation will open a URL.</0>", "sub-text": "<0>Define what the correlation will link to. With the query type, a query will run when the correlation is clicked. With the external type, clicking the correlation will open a URL.</0>",
@ -4722,7 +4722,7 @@
"add-visualization-body": "Select a data source and then query and visualize your data with charts, stats and tables or create lists, markdowns and other widgets.", "add-visualization-body": "Select a data source and then query and visualize your data with charts, stats and tables or create lists, markdowns and other widgets.",
"add-visualization-button": "Add visualization", "add-visualization-button": "Add visualization",
"add-visualization-header": "Start your new dashboard by adding a visualization", "add-visualization-header": "Start your new dashboard by adding a visualization",
"import-a-dashboard-body": "Import dashboards from files or <1>grafana.com</1>.", "import-a-dashboard-body": "Import dashboards from files or <2>grafana.com</2>.",
"import-a-dashboard-header": "Import a dashboard", "import-a-dashboard-header": "Import a dashboard",
"import-dashboard-button": "Import dashboard" "import-dashboard-button": "Import dashboard"
}, },