diff --git a/packages/grafana-ui/src/components/UsersIndicator/Avatar.mdx b/packages/grafana-ui/src/components/UsersIndicator/Avatar.mdx
new file mode 100644
index 00000000000..b2f333dd0ef
--- /dev/null
+++ b/packages/grafana-ui/src/components/UsersIndicator/Avatar.mdx
@@ -0,0 +1,27 @@
+import { Meta, ArgTypes } from '@storybook/blocks';
+import { Avatar } from './Avatar';
+
+
+
+# Avatar
+
+A basic component for displaying a user's avatar. The default dimensions (width and height) of this component are set to 24 pixels, but these can be overridden by passing a `width` and `height` prop. Both props are `ThemeSpacingTokens`, meaning that instead of passing a specific pixel value, you should pass a token value which will be converted into pixels by multiplying it with the base spacing value - `8`.
+
+## Usage
+
+```jsx
+import { Avatar } from '@grafana/ui';
+
+const user = {
+ id: 5,
+ name: 'Admin',
+ email: 'admin@org.com',
+ avatarUrl: 'https://secure.gravatar.com/avatar',
+};
+
+const Example = () => {
+ return ;
+};
+```
+
+
diff --git a/packages/grafana-ui/src/components/UsersIndicator/Avatar.story.tsx b/packages/grafana-ui/src/components/UsersIndicator/Avatar.story.tsx
new file mode 100644
index 00000000000..23aec35c409
--- /dev/null
+++ b/packages/grafana-ui/src/components/UsersIndicator/Avatar.story.tsx
@@ -0,0 +1,32 @@
+import { Meta, StoryFn } from '@storybook/react';
+import React from 'react';
+
+import { Avatar } from '@grafana/ui';
+
+import mdx from './Avatar.mdx';
+
+const meta: Meta = {
+ title: 'General/UsersIndicator/Avatar',
+ component: Avatar,
+ parameters: {
+ docs: { page: mdx },
+ controls: { exclude: ['alt'] },
+ },
+ argTypes: {
+ width: { control: 'number' },
+ height: { control: 'number' },
+ },
+};
+
+const Template: StoryFn = (args) => ;
+
+export const Basic = Template.bind({});
+
+Basic.args = {
+ src: 'https://secure.gravatar.com/avatar',
+ alt: 'User avatar',
+ width: 3,
+ height: 3,
+};
+
+export default meta;
diff --git a/packages/grafana-ui/src/components/UsersIndicator/Avatar.tsx b/packages/grafana-ui/src/components/UsersIndicator/Avatar.tsx
new file mode 100644
index 00000000000..c5485058094
--- /dev/null
+++ b/packages/grafana-ui/src/components/UsersIndicator/Avatar.tsx
@@ -0,0 +1,33 @@
+import { css } from '@emotion/css';
+import React from 'react';
+
+import { GrafanaTheme2, ThemeSpacingTokens } from '@grafana/data';
+
+import { useStyles2 } from '../../themes';
+import { getResponsiveStyle, ResponsiveProp } from '../Layout/utils/responsiveness';
+
+export interface AvatarProps {
+ src: string;
+ alt: string;
+ width?: ResponsiveProp;
+ height?: ResponsiveProp;
+}
+export const Avatar = ({ src, alt, width, height }: AvatarProps) => {
+ const styles = useStyles2(getStyles, width, height);
+
+ return
;
+};
+
+const getStyles = (theme: GrafanaTheme2, width: AvatarProps['width'] = 3, height: AvatarProps['height'] = 3) => {
+ return {
+ image: css([
+ getResponsiveStyle(theme, width, (val) => ({
+ width: theme.spacing(val),
+ })),
+ getResponsiveStyle(theme, height, (val) => ({
+ height: theme.spacing(val),
+ })),
+ { borderRadius: theme.shape.radius.circle },
+ ]),
+ };
+};
diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts
index 1a7f161adcf..3d967e90118 100644
--- a/packages/grafana-ui/src/components/index.ts
+++ b/packages/grafana-ui/src/components/index.ts
@@ -261,6 +261,7 @@ export { Dropdown } from './Dropdown/Dropdown';
export { PluginSignatureBadge, type PluginSignatureBadgeProps } from './PluginSignatureBadge/PluginSignatureBadge';
export { UserIcon, type UserIconProps } from './UsersIndicator/UserIcon';
export { type UserView } from './UsersIndicator/types';
+export { Avatar } from './UsersIndicator/Avatar';
// Export this until we've figured out a good approach to inline form styles.
export { InlineFormLabel } from './FormLabel/FormLabel';
export { Divider } from './Divider/Divider';
diff --git a/public/app/features/admin/Users/Avatar.tsx b/public/app/features/admin/Users/Avatar.tsx
deleted file mode 100644
index 40f5a0bf3ea..00000000000
--- a/public/app/features/admin/Users/Avatar.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { css } from '@emotion/css';
-import React from 'react';
-
-import { GrafanaTheme2 } from '@grafana/data';
-import { useStyles2 } from '@grafana/ui';
-
-export interface AvatarProps {
- src?: string;
- alt: string;
-}
-export const Avatar = ({ src, alt }: AvatarProps) => {
- const styles = useStyles2(getStyles);
-
- if (!src) {
- return null;
- }
- return
;
-};
-
-const getStyles = (theme: GrafanaTheme2) => {
- return {
- image: css({
- width: theme.spacing(3),
- height: theme.spacing(3),
- borderRadius: theme.shape.radius.circle,
- }),
- };
-};
diff --git a/public/app/features/admin/Users/OrgUsersTable.tsx b/public/app/features/admin/Users/OrgUsersTable.tsx
index 27d41f9c9ae..84a30b5ceb9 100644
--- a/public/app/features/admin/Users/OrgUsersTable.tsx
+++ b/public/app/features/admin/Users/OrgUsersTable.tsx
@@ -17,6 +17,7 @@ import {
Pagination,
HorizontalGroup,
VerticalGroup,
+ Avatar,
} from '@grafana/ui';
import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker';
import { fetchRoleOptions } from 'app/core/components/RolePicker/api';
@@ -27,8 +28,6 @@ import { AccessControlAction, OrgUser, Role } from 'app/types';
import { OrgRolePicker } from '../OrgRolePicker';
-import { Avatar } from './Avatar';
-
type Cell = CellProps;
const disabledRoleMessage = `This user's role is not editable because it is synchronized from your auth provider.
@@ -95,7 +94,7 @@ export const OrgUsersTable = ({
{
id: 'avatarUrl',
header: '',
- cell: ({ cell: { value } }: Cell<'avatarUrl'>) => ,
+ cell: ({ cell: { value } }: Cell<'avatarUrl'>) => value && ,
},
{
id: 'login',
diff --git a/public/app/features/admin/Users/UsersTable.tsx b/public/app/features/admin/Users/UsersTable.tsx
index e45ac10a6e9..1f9ec1b7e4c 100644
--- a/public/app/features/admin/Users/UsersTable.tsx
+++ b/public/app/features/admin/Users/UsersTable.tsx
@@ -14,11 +14,11 @@ import {
VerticalGroup,
HorizontalGroup,
FetchDataFunc,
+ Avatar,
} from '@grafana/ui';
import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
import { UserDTO } from 'app/types';
-import { Avatar } from './Avatar';
import { OrgUnits } from './OrgUnits';
type Cell = CellProps;
@@ -46,7 +46,7 @@ export const UsersTable = ({
{
id: 'avatarUrl',
header: '',
- cell: ({ cell: { value } }: Cell<'avatarUrl'>) => ,
+ cell: ({ cell: { value } }: Cell<'avatarUrl'>) => value && ,
},
{
id: 'login',
diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx
index 85d54da7bed..04a269e23bb 100644
--- a/public/app/features/teams/TeamList.tsx
+++ b/public/app/features/teams/TeamList.tsx
@@ -17,6 +17,7 @@ import {
Pagination,
VerticalGroup,
useStyles2,
+ Avatar,
} from '@grafana/ui';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import { Page } from 'app/core/components/Page/Page';
@@ -25,7 +26,6 @@ import { contextSrv } from 'app/core/services/context_srv';
import { AccessControlAction, Role, StoreState, Team } from 'app/types';
import { TeamRolePicker } from '../../core/components/RolePicker/TeamRolePicker';
-import { Avatar } from '../admin/Users/Avatar';
import { deleteTeam, loadTeams, changePage, changeQuery, changeSort } from './state/actions';
@@ -70,7 +70,7 @@ export const TeamList = ({
{
id: 'avatarUrl',
header: '',
- cell: ({ cell: { value } }: Cell<'avatarUrl'>) => ,
+ cell: ({ cell: { value } }: Cell<'avatarUrl'>) => value && ,
},
{
id: 'name',