Grafana UI: Add Avatar component (#76429)

* Grafana/UI: Add Avatar component

* Use the new component

* Add docs and story

* Update type check

* Codeformat
This commit is contained in:
Alex Khomenko 2023-10-16 12:59:54 +02:00 committed by GitHub
parent c04e96b3ed
commit bc98f3d139
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 99 additions and 35 deletions

View File

@ -0,0 +1,27 @@
import { Meta, ArgTypes } from '@storybook/blocks';
import { Avatar } from './Avatar';
<Meta title="MDX|Avatar" component={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 <Avatar src={user.avatarUrl} alt={`Avatar for the user ${user.name}`} width={4} height={4} />;
};
```
<ArgTypes of={Avatar} />

View File

@ -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<typeof Avatar> = {
title: 'General/UsersIndicator/Avatar',
component: Avatar,
parameters: {
docs: { page: mdx },
controls: { exclude: ['alt'] },
},
argTypes: {
width: { control: 'number' },
height: { control: 'number' },
},
};
const Template: StoryFn<typeof Avatar> = (args) => <Avatar {...args} />;
export const Basic = Template.bind({});
Basic.args = {
src: 'https://secure.gravatar.com/avatar',
alt: 'User avatar',
width: 3,
height: 3,
};
export default meta;

View File

@ -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<ThemeSpacingTokens>;
height?: ResponsiveProp<ThemeSpacingTokens>;
}
export const Avatar = ({ src, alt, width, height }: AvatarProps) => {
const styles = useStyles2(getStyles, width, height);
return <img className={styles.image} src={src} alt={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 },
]),
};
};

View File

@ -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';

View File

@ -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 <img className={styles.image} src={src} alt={alt} />;
};
const getStyles = (theme: GrafanaTheme2) => {
return {
image: css({
width: theme.spacing(3),
height: theme.spacing(3),
borderRadius: theme.shape.radius.circle,
}),
};
};

View File

@ -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<T extends keyof OrgUser = keyof OrgUser> = CellProps<OrgUser, OrgUser[T]>;
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'>) => <Avatar src={value} alt="User avatar" />,
cell: ({ cell: { value } }: Cell<'avatarUrl'>) => value && <Avatar src={value} alt="User avatar" />,
},
{
id: 'login',

View File

@ -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<T extends keyof UserDTO = keyof UserDTO> = CellProps<UserDTO, UserDTO[T]>;
@ -46,7 +46,7 @@ export const UsersTable = ({
{
id: 'avatarUrl',
header: '',
cell: ({ cell: { value } }: Cell<'avatarUrl'>) => <Avatar src={value} alt={'User avatar'} />,
cell: ({ cell: { value } }: Cell<'avatarUrl'>) => value && <Avatar src={value} alt={'User avatar'} />,
},
{
id: 'login',

View File

@ -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'>) => <Avatar src={value} alt="User avatar" />,
cell: ({ cell: { value } }: Cell<'avatarUrl'>) => value && <Avatar src={value} alt="User avatar" />,
},
{
id: 'name',