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'],
'unicorn/no-empty-file': '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 { TextLink } from '../Link/TextLink';
import { CallToActionCard } from './CallToActionCard';
describe('CallToActionCard', () => {
describe('rendering', () => {
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();
});
it('should render message when provided', () => {
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();
});
it('should render footer when provided', () => {
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();
});
@ -28,7 +52,11 @@ describe('CallToActionCard', () => {
<CallToActionCard
message="Click button below"
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();

View File

@ -2,6 +2,7 @@ import { Meta, StoryFn } from '@storybook/react';
import { Button } from '../Button/Button';
import { IconButton } from '../IconButton/IconButton';
import { TextLink } from '../Link/TextLink';
import { TagList } from '../Tags/TagList';
import { Card } from './Card';
@ -74,9 +75,9 @@ export const MultipleMetadataWithCustomSeparator: StoryFn<typeof Card> = (args)
<Card.Heading>Test dashboard</Card.Heading>
<Card.Meta separator={'-'}>
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
</a>
</TextLink>
</Card.Meta>
</Card>
);
@ -171,9 +172,9 @@ export const WithMediaElements: StoryFn<typeof Card> = (args) => {
</Card.Figure>
<Card.Meta>
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
</a>
</TextLink>
</Card.Meta>
</Card>
);
@ -190,9 +191,9 @@ export const ActionCards: StoryFn<typeof Card> = (args) => {
<Card.Heading>1-ops-tools1-fallback</Card.Heading>
<Card.Meta>
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
</a>
</TextLink>
</Card.Meta>
<Card.Figure>
<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.Meta>
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
</a>
</TextLink>
</Card.Meta>
<Card.Figure>
<img src={logo} alt="Grafana Logo" width="40" height="40" />
@ -269,9 +270,9 @@ export const Full: StoryFn<typeof Card> = (args) => {
</Card.Description>
<Card.Meta>
{['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
</a>
</TextLink>
</Card.Meta>
<Card.Figure>
<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 { FilterInput } from '../../FilterInput/FilterInput';
import { Icon } from '../../Icon/Icon';
import { TextLink } from '../../Link/TextLink';
import { WeekStart } from '../WeekStartPicker';
import { TimePickerFooter } from './TimePickerFooter';
@ -234,13 +235,9 @@ const EmptyRecentList = memo(() => {
</div>
<Trans i18nKey="time-picker.content.empty-recent-list-docs">
<div>
<a
className={styles.link}
href="https://grafana.com/docs/grafana/latest/dashboards/time-range-controls"
target="_new"
>
<TextLink href="https://grafana.com/docs/grafana/latest/dashboards/time-range-controls" external>
Read the documentation
</a>
</TextLink>
<span> to find out more about how to enter custom time ranges.</span>
</div>
</Trans>
@ -371,7 +368,4 @@ const getEmptyListStyles = (theme: GrafanaTheme2) => ({
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 { Icon } from '../Icon/Icon';
import { Stack } from '../Layout/Stack/Stack';
import { TextLink } from '../Link/TextLink';
import { Menu } from '../Menu/Menu';
import { PanelChromeProps } from './PanelChrome';
@ -252,10 +253,9 @@ export const Examples = () => {
{renderPanel('Panel with action link', {
title: 'Panel with action link',
actions: (
<a className="external-link" href="/some/page">
<TextLink external href="http://www.example.com/some/page">
Error details
<Icon name="arrow-right" />
</a>
</TextLink>
),
})}
{renderPanel('Action and menu (should be rare)', {
@ -322,10 +322,9 @@ export const ExamplesHoverHeader = () => {
title: 'With link in hover header',
hoverHeader: true,
actions: (
<a className="external-link" href="/some/page">
<TextLink external href="http://www.example.com/some/page">
Error details
<Icon name="arrow-right" />
</a>
</TextLink>
),
})}
</Stack>

View File

@ -2,15 +2,17 @@
import userEvent from '@testing-library/user-event';
import { MutableRefObject } from 'react';
import { TextLink } from '../Link/TextLink';
import { Tooltip } from './Tooltip';
describe('Tooltip', () => {
it('renders correctly', () => {
render(
<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
</a>
</TextLink>
</Tooltip>
);
expect(screen.getByText('Link with tooltip')).toBeInTheDocument();

View File

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

View File

@ -15,6 +15,7 @@ import {
useStyles2,
withTheme2,
Stack,
TextLink,
} from '@grafana/ui';
import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker';
import { fetchRoleOptions, updateUserRoles } from 'app/core/components/RolePicker/api';
@ -446,14 +447,9 @@ export function ChangeOrgButton({
<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
the&nbsp;
<a
className={styles.tooltipItemLink}
href={'https://grafana.com/docs/grafana/latest/auth'}
rel="noreferrer"
target="_blank"
>
<TextLink href={'https://grafana.com/docs/grafana/latest/auth'} external>
Grafana authentication docs
</a>
</TextLink>
&nbsp;for details.
</Trans>
</div>
@ -496,14 +492,9 @@ export const ExternalUserTooltip = ({ lockMessage }: ExternalUserTooltipProps) =
<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
to the&nbsp;
<a
className={styles.tooltipItemLink}
href={'https://grafana.com/docs/grafana/latest/auth'}
rel="noreferrer noopener"
target="_blank"
>
<TextLink href={'https://grafana.com/docs/grafana/latest/auth'} external>
Grafana authentication docs
</a>
</TextLink>
&nbsp;for details.
</Trans>
</div>
@ -519,9 +510,6 @@ const getTooltipStyles = (theme: GrafanaTheme2) => ({
disabledTooltip: css({
display: 'flex',
}),
tooltipItemLink: css({
color: theme.v1.palette.blue95,
}),
lockMessageClass: css({
fontStyle: 'italic',
marginLeft: '1.8rem',

View File

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

View File

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

View File

@ -87,7 +87,7 @@ export function NoAccessModal({ item, isOpen, onDismiss }: NoAccessModalProps) {
<p>
<Trans i18nKey="connections.no-access-modal.editor-warning">
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>
</p>
<p>

View File

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

View File

@ -4,7 +4,7 @@ import { useAsync } from 'react-use';
import { CoreApp } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { getDataSourceSrv } from '@grafana/runtime';
import { Field, LoadingPlaceholder, Alert } from '@grafana/ui';
import { Field, LoadingPlaceholder, Alert, TextLink } from '@grafana/ui';
interface Props {
dsUid?: string;
@ -34,13 +34,12 @@ export const QueryEditorField = ({ dsUid, invalid, error, name }: Props) => {
<span>
<Trans i18nKey="correlations.query-editor.query-description">
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/"
target="_blank"
rel="noreferrer"
external
>
variables
</a>{' '}
</TextLink>{' '}
to access specific field values.
</Trans>
</span>

View File

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

View File

@ -4,7 +4,7 @@ import { Trans, t } from '@grafana/i18n';
import { config } from '@grafana/runtime';
import { SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectRef, VizPanel } from '@grafana/scenes';
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 { ThemePicker } from 'app/features/dashboard/components/ShareModal/ThemePicker';
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">
To render an image, you must install the{' '}
<a
href="https://grafana.com/grafana/plugins/grafana-image-renderer"
target="_blank"
rel="noopener noreferrer"
className="external-link"
>
<TextLink href="https://grafana.com/grafana/plugins/grafana-image-renderer" external>
Grafana image renderer plugin
</a>
</TextLink>
. Please contact your Grafana administrator to install the plugin.
</Trans>
</Alert>

View File

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

View File

@ -89,7 +89,7 @@ export function HelpWizard({ panel, plugin, onClose }: Props) {
<span className="muted">
<Trans i18nKey="help-wizard.support-bundle">
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>
</span>
)}

View File

@ -4,7 +4,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Trans } from '@grafana/i18n';
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 {
onAddLibraryPanel as onAddLibraryPanelImpl,
@ -115,7 +115,11 @@ const DashboardEmpty = ({ dashboard, canCreate }: Props) => {
<Box marginBottom={2}>
<Text element="p" textAlignment="center" color="secondary">
<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>
</Text>
</Box>

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import { css } from '@emotion/css';
import { GrafanaTheme2, QueryEditorProps, SelectableValue, toOption } from '@grafana/data';
import { EditorField } from '@grafana/plugin-ui';
import { config } from '@grafana/runtime';
import { useStyles2 } from '@grafana/ui';
import { TextLink, useStyles2 } from '@grafana/ui';
import { CloudWatchDatasource } from '../../datasource';
import {
@ -257,13 +257,12 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
tooltip={
<>
{'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"
target="_blank"
rel="noreferrer"
external
>
See the documentation for more details
</a>
</TextLink>
</>
}
/>
@ -272,13 +271,12 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
tooltipInteractive
tooltip={
<>
<a
<TextLink
href="https://grafana.com/docs/grafana/latest/datasources/aws-cloudwatch/template-queries-cloudwatch/#selecting-attributes"
target="_blank"
rel="noreferrer"
external
>
Pre-defined ec2:DescribeInstances filters/tags
</a>
</TextLink>
{' 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 { 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 { LogGroup } from '../../../types';
@ -146,13 +157,12 @@ export const LogGroupsSelector = ({
search.
<p>
A{' '}
<a
target="_blank"
rel="noopener noreferrer"
<TextLink
external
href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/cloudwatch_limits_cwl.html"
>
maximum{' '}
</a>{' '}
</TextLink>{' '}
of 50 Cloudwatch log groups can be queried at one time.
</p>
</div>

View File

@ -4,7 +4,7 @@ import * as React from 'react';
import { SelectableValue } from '@grafana/data';
import { EditorField, EditorFieldGroup, EditorRow, EditorRows, EditorSwitch } from '@grafana/plugin-ui';
import { config } from '@grafana/runtime';
import { Select } from '@grafana/ui';
import { Select, TextLink } from '@grafana/ui';
import { CloudWatchDatasource } from '../../../datasource';
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 '
}
<a
<TextLink
href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/search-expression-syntax.html"
target="_blank"
rel="noreferrer"
external
>
metric schema
</a>
</TextLink>
{
' 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 { 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';
@ -64,14 +64,9 @@ const TooltipContent = memo(() => {
return (
<span>
This function is not supported. Check your function for typos and{' '}
<a
target="_blank"
className="external-link"
rel="noreferrer noopener"
href="https://graphite.readthedocs.io/en/latest/functions.html"
>
<TextLink external href="https://graphite.readthedocs.io/en/latest/functions.html">
read the docs
</a>{' '}
</TextLink>{' '}
to see whether you need to upgrade your data sources version to make this function available.
</span>
);

View File

@ -8,7 +8,7 @@ import {
updateDatasourcePluginJsonDataOption,
} from '@grafana/data';
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 { 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!]}>
<p>
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
</a>
</TextLink>
</p>
</Alert>
)}

View File

@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { TextLink, useStyles2 } from '@grafana/ui';
export default function CheatSheet() {
const styles = useStyles2(getStyles);
@ -11,14 +11,9 @@ export default function CheatSheet() {
<p>
This cheat sheet provides a quick overview of the query types that are available. For more details about the
Jaeger data source, check out{' '}
<a
href="https://grafana.com/docs/grafana/latest/datasources/jaeger"
target="_blank"
rel="noreferrer"
className={styles.anchorTag}
>
<TextLink href="https://grafana.com/docs/grafana/latest/datasources/jaeger" external>
the documentation
</a>
</TextLink>
.
</p>
@ -32,14 +27,9 @@ export default function CheatSheet() {
<li>
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{' '}
<a
href="https://grafana.com/docs/grafana/latest/datasources/jaeger/#upload-json-trace-file"
target="_blank"
rel="noreferrer"
className={styles.anchorTag}
>
<TextLink href="https://grafana.com/docs/grafana/latest/datasources/jaeger/#upload-json-trace-file" external>
this section
</a>{' '}
</TextLink>{' '}
of the documentation.
</li>
</ul>
@ -48,9 +38,6 @@ export default function CheatSheet() {
}
const getStyles = (theme: GrafanaTheme2) => ({
anchorTag: css({
color: theme.colors.text.link,
}),
unorderedList: css({
listStyleType: 'none',
}),

View File

@ -4,7 +4,7 @@ import { PureComponent } from 'react';
import { GrafanaTheme2, QueryEditorHelpProps } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { Themeable2, withTheme2 } from '@grafana/ui';
import { TextLink, Themeable2, withTheme2 } from '@grafana/ui';
import LokiLanguageProvider from '../LanguageProvider';
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"} |= "exact 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
</a>{' '}
</TextLink>{' '}
supports exact and regular expression filters.
</div>
{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 { AlertingSettingsOverhaul, PromOptions, PromSettings } from '@grafana/prometheus';
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 { 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';
return (
<a href={url ? url : docsUrl} target="_blank" rel="noopener noreferrer">
<TextLink href={url ? url : docsUrl} external>
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 { TemporaryAlert } from '@grafana/o11y-ds-frontend';
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 { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen';
@ -304,7 +304,7 @@ const TraceQLSearch = ({
{error ? (
<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
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>
) : null}
{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 { 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 { 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 (
<Alert title={title} severity="info" className={styles.alert}>
{description} according to the{' '}
<a
target="_blank"
rel="noreferrer noopener"
href="https://grafana.com/docs/grafana/latest/datasources/tempo/service-graph/"
className={styles.link}
>
<TextLink external href="https://grafana.com/docs/grafana/latest/datasources/tempo/service-graph/">
Tempo documentation
</a>
</TextLink>
.
</Alert>
);
@ -157,8 +152,4 @@ const getStyles = (theme: GrafanaTheme2) => ({
maxWidth: '75ch',
marginTop: theme.spacing(2),
}),
link: css({
color: theme.colors.text.link,
textDecoration: 'underline',
}),
});

View File

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

View File

@ -1,14 +1,12 @@
import { css } from '@emotion/css';
import React from 'react';
import {
DataSourceJsonData,
DataSourcePluginOptionsEditorProps,
GrafanaTheme2,
updateDatasourcePluginJsonDataOption,
} from '@grafana/data';
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';
@ -21,7 +19,6 @@ interface StreamingOptions extends DataSourceJsonData {
interface Props extends DataSourcePluginOptionsEditorProps<StreamingOptions> {}
export const StreamingSection = ({ options, onOptionsChange }: Props) => {
const styles = useStyles2(getStyles);
return (
<ConfigSection
title="Streaming"
@ -29,14 +26,9 @@ export const StreamingSection = ({ options, onOptionsChange }: Props) => {
description={
<Stack gap={0.5}>
<div>Enable streaming for different Tempo features.</div>
<a
href={'https://grafana.com/docs/tempo/latest/traceql/#stream-query-results'}
target={'_blank'}
rel="noreferrer"
className={styles.a}
>
<TextLink external href={'https://grafana.com/docs/tempo/latest/traceql/#stream-query-results'}>
Learn more
</a>
</TextLink>
</Stack>
}
>
@ -87,15 +79,3 @@ export const StreamingSection = ({ options, onOptionsChange }: Props) => {
</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 { 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 { defaultQuery, MyDataSourceOptions, TempoQuery } from '../types';
@ -32,14 +32,9 @@ export function QueryEditor(props: Props) {
<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
it in{' '}
<a
className={css({ textDecoration: 'underline' })}
href="https://grafana.com/docs/tempo/latest/operations/traceql-metrics/"
target="_blank"
>
<TextLink external href="https://grafana.com/docs/tempo/latest/operations/traceql-metrics/">
documentation
<Icon name="external-link-alt" />
</a>
</TextLink>
.
</Alert>
);
@ -50,9 +45,9 @@ export function QueryEditor(props: Props) {
{inAlerting && alertingWarning}
<InlineLabel>
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
</a>
</TextLink>
</InlineLabel>
{!showCopyFromSearchButton && (
<div className={styles.copyContainer}>

View File

@ -4,7 +4,7 @@ import { useToggle } from 'react-use';
import { CoreApp, GrafanaTheme2 } from '@grafana/data';
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 { 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
entire query completes.
</span>
<a
<TextLink
external
href={'https://grafana.com/docs/tempo/latest/traceql/#stream-query-results'}
aria-label={'Learn more about streaming query results'}
target={'_blank'}
rel="noreferrer"
style={{ textDecoration: 'underline' }}
>
Learn more
</a>
</TextLink>
</div>
);
};

View File

@ -4319,7 +4319,7 @@
"source-label": "Source",
"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": {
"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>",
@ -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-button": "Add 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-dashboard-button": "Import dashboard"
},