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 {alt}; +}; + +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 {alt}; -}; - -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',