diff --git a/packages/grafana-ui/src/components/Link/TextLink.mdx b/packages/grafana-ui/src/components/Link/TextLink.mdx
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/grafana-ui/src/components/Link/TextLink.story.tsx b/packages/grafana-ui/src/components/Link/TextLink.story.tsx
new file mode 100644
index 00000000000..8ef60faa363
--- /dev/null
+++ b/packages/grafana-ui/src/components/Link/TextLink.story.tsx
@@ -0,0 +1,80 @@
+import { Meta, StoryFn } from '@storybook/react';
+import React from 'react';
+
+import { StoryExample } from '../../utils/storybook/StoryExample';
+import { VerticalGroup } from '../Layout/Layout';
+import { P } from '../Text/TextElements';
+
+import { TextLink } from './TextLink';
+import mdx from './TextLink.mdx';
+
+const meta: Meta = {
+ title: 'General/TextLink',
+ component: TextLink,
+ parameters: {
+ docs: {
+ page: mdx,
+ },
+ controls: { exclude: ['href', 'external'] },
+ },
+ argTypes: {
+ variant: { control: 'select', options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'body', 'bodySmall', undefined] },
+ weight: {
+ control: 'select',
+ options: ['bold', 'medium', 'light', 'regular', undefined],
+ },
+ color: {
+ control: 'select',
+ options: ['primary', 'secondary', 'disabled', 'link', 'maxContrast', undefined],
+ },
+ inline: { control: 'boolean' },
+ },
+ args: {
+ variant: 'body',
+ weight: 'light',
+ color: undefined,
+ inline: false,
+ href: 'https://www.google.com',
+ external: true,
+ icon: 'external-link-alt',
+ },
+};
+
+export const Example: StoryFn = (args) => {
+ return (
+
+
+
+ To get started with a forever free Grafana Cloud account, sign up at
+
+ grafana.com
+
+ .
+
+
+
+
+ Learn how in the docs
+
+
+
+ *The examples cannot contemplate an internal link due to conflicts between Storybook and React Router
+
+ );
+};
+
+Example.parameters = {
+ controls: { exclude: ['href', 'external', 'variant', 'weight', 'color', 'inline', 'icon'] },
+};
+
+export const Basic: StoryFn = (args) => {
+ return (
+
+
+ Go to Google
+
+
+ );
+};
+
+export default meta;
diff --git a/packages/grafana-ui/src/components/Link/TextLink.tsx b/packages/grafana-ui/src/components/Link/TextLink.tsx
new file mode 100644
index 00000000000..2398e5dd11c
--- /dev/null
+++ b/packages/grafana-ui/src/components/Link/TextLink.tsx
@@ -0,0 +1,105 @@
+import { css } from '@emotion/css';
+import React, { AnchorHTMLAttributes, forwardRef } from 'react';
+
+import { GrafanaTheme2, locationUtil, textUtil, ThemeTypographyVariantTypes } from '@grafana/data';
+
+import { useTheme2 } from '../../themes';
+import { IconName, IconSize } from '../../types';
+import { Icon } from '../Icon/Icon';
+import { customWeight } from '../Text/utils';
+
+import { Link } from './Link';
+
+interface TextLinkProps extends Omit, 'target' | 'rel'> {
+ /** url to which redirect the user, external or internal */
+ href: string;
+ /** Color to use for text */
+ color?: keyof GrafanaTheme2['colors']['text'];
+ /** Specify if the link will redirect users to a page in or out Grafana */
+ external?: boolean;
+ /** True when the link will be displayed inline with surrounding text, false if it will be displayed as a block. Depending on this prop correspondant default styles will be applied */
+ inline?: boolean;
+ /** The default variant is 'body'. To fit another styles set the correspondent variant as it is necessary also to adjust the icon size */
+ variant?: keyof ThemeTypographyVariantTypes;
+ /** Override the default weight for the used variant */
+ weight?: 'light' | 'regular' | 'medium' | 'bold';
+ /** Set the icon to be shown. An external link will show the 'external-link-alt' icon as default.*/
+ icon?: IconName;
+ children: string;
+}
+
+const svgSizes: {
+ [key in keyof ThemeTypographyVariantTypes]: IconSize;
+} = {
+ h1: 'xl',
+ h2: 'xl',
+ h3: 'lg',
+ h4: 'lg',
+ h5: 'md',
+ h6: 'md',
+ body: 'md',
+ bodySmall: 'xs',
+};
+
+export const TextLink = forwardRef(
+ (
+ { href, color = 'link', external = false, inline = true, variant = 'body', weight, icon, children, ...rest },
+ ref
+ ) => {
+ const validUrl = locationUtil.stripBaseFromUrl(textUtil.sanitizeUrl(href ?? ''));
+
+ const theme = useTheme2();
+ const styles = getLinkStyles(theme, inline, variant, weight, color);
+ const externalIcon = icon || 'external-link-alt';
+
+ return external ? (
+
+ {children}
+
+
+ ) : (
+
+ {children}
+ {icon && }
+
+ );
+ }
+);
+
+TextLink.displayName = 'TextLink';
+
+export const getLinkStyles = (
+ theme: GrafanaTheme2,
+ inline: boolean,
+ variant?: keyof ThemeTypographyVariantTypes,
+ weight?: TextLinkProps['weight'],
+ color?: TextLinkProps['color']
+) => {
+ return css([
+ variant && {
+ ...theme.typography[variant],
+ },
+ weight && {
+ fontWeight: customWeight(weight, theme),
+ },
+ color && {
+ color: theme.colors.text[color],
+ },
+ {
+ alignItems: 'center',
+ gap: '0.25em',
+ display: 'inline-flex',
+ textDecoration: 'none',
+ '&:hover': {
+ textDecoration: 'underline',
+ color: theme.colors.text.link,
+ },
+ },
+ inline && {
+ textDecoration: 'underline',
+ '&:hover': {
+ textDecoration: 'none',
+ },
+ },
+ ]);
+};
diff --git a/packages/grafana-ui/src/components/Text/Text.tsx b/packages/grafana-ui/src/components/Text/Text.tsx
index fa191317f90..d8e7d460c4f 100644
--- a/packages/grafana-ui/src/components/Text/Text.tsx
+++ b/packages/grafana-ui/src/components/Text/Text.tsx
@@ -5,6 +5,8 @@ import { GrafanaTheme2, ThemeTypographyVariantTypes } from '@grafana/data';
import { useStyles2 } from '../../themes';
+import { customWeight, customColor } from './utils';
+
export interface TextProps {
/** Defines what HTML element is defined underneath */
as: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'span' | 'p' | 'legend';
@@ -75,32 +77,3 @@ const getTextStyles = (
},
]);
};
-
-const customWeight = (weight: TextProps['weight'], theme: GrafanaTheme2): number => {
- switch (weight) {
- case 'bold':
- return theme.typography.fontWeightBold;
- case 'medium':
- return theme.typography.fontWeightMedium;
- case 'light':
- return theme.typography.fontWeightLight;
- case 'regular':
- case undefined:
- return theme.typography.fontWeightRegular;
- }
-};
-
-const customColor = (color: TextProps['color'], theme: GrafanaTheme2): string | undefined => {
- switch (color) {
- case 'error':
- return theme.colors.error.text;
- case 'success':
- return theme.colors.success.text;
- case 'info':
- return theme.colors.info.text;
- case 'warning':
- return theme.colors.warning.text;
- default:
- return color ? theme.colors.text[color] : undefined;
- }
-};
diff --git a/packages/grafana-ui/src/components/Text/utils.ts b/packages/grafana-ui/src/components/Text/utils.ts
new file mode 100644
index 00000000000..650e6ecae74
--- /dev/null
+++ b/packages/grafana-ui/src/components/Text/utils.ts
@@ -0,0 +1,32 @@
+import { GrafanaTheme2 } from '@grafana/data';
+
+import { TextProps } from './Text';
+
+export const customWeight = (weight: TextProps['weight'], theme: GrafanaTheme2): number => {
+ switch (weight) {
+ case 'bold':
+ return theme.typography.fontWeightBold;
+ case 'medium':
+ return theme.typography.fontWeightMedium;
+ case 'light':
+ return theme.typography.fontWeightLight;
+ case 'regular':
+ case undefined:
+ return theme.typography.fontWeightRegular;
+ }
+};
+
+export const customColor = (color: TextProps['color'], theme: GrafanaTheme2): string | undefined => {
+ switch (color) {
+ case 'error':
+ return theme.colors.error.text;
+ case 'success':
+ return theme.colors.success.text;
+ case 'info':
+ return theme.colors.info.text;
+ case 'warning':
+ return theme.colors.warning.text;
+ default:
+ return color ? theme.colors.text[color] : undefined;
+ }
+};
diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts
index 65ca761d285..98969e5e40e 100644
--- a/packages/grafana-ui/src/components/index.ts
+++ b/packages/grafana-ui/src/components/index.ts
@@ -204,6 +204,7 @@ export { ToolbarButton, ToolbarButtonRow } from './ToolbarButton';
export { ValuePicker } from './ValuePicker/ValuePicker';
export { fieldMatchersUI } from './MatchersUI/fieldMatchersUI';
export { Link } from './Link/Link';
+export { TextLink } from './Link/TextLink';
export { Label } from './Forms/Label';
export { Field, type FieldProps } from './Forms/Field';