Add a Avatar component to @backstage/core

This commit is contained in:
Oliver Sand
2020-12-09 15:59:22 +01:00
parent cbd3a44c09
commit 8ef71ed32e
10 changed files with 169 additions and 40 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/core': patch
'@backstage/plugin-org': patch
---
Add a `<Avatar>` component to `@backstage/core`.
@@ -0,0 +1,42 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Avatar } from './Avatar';
export default {
title: 'Data Display/Avatar',
component: Avatar,
};
export const Default = () => (
<Avatar
displayName="Jenny Doe"
// Avatar of the backstage GitHub org
picture="https://avatars1.githubusercontent.com/u/72526453?s=200&v=4"
/>
);
export const NameFallback = () => <Avatar displayName="Jenny Doe" />;
export const Empty = () => <Avatar />;
export const CustomStyling = () => (
<Avatar
displayName="Jenny Doe"
customStyles={{ width: '24px', height: '24px', fontSize: '8px' }}
/>
);
@@ -0,0 +1,27 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { render } from '@testing-library/react';
import React from 'react';
import { Avatar } from './Avatar';
describe('<Avatar />', () => {
it('renders without exploding', async () => {
const { getByText } = render(<Avatar displayName="John Doe" />);
expect(getByText('JD')).toBeInTheDocument();
});
});
@@ -20,6 +20,7 @@ import {
makeStyles,
Theme,
} from '@material-ui/core';
import { extractInitials, stringToColor } from './utils';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
@@ -34,28 +35,13 @@ const useStyles = makeStyles((theme: Theme) =>
}),
);
const stringToColour = (str: string) => {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
let colour = '#';
for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 0xff;
colour += `00${value.toString(16)}`.substr(-2);
}
return colour;
export type AvatarProps = {
displayName?: string;
picture?: string;
customStyles?: CSSProperties;
};
export const Avatar = ({
displayName,
picture,
customStyles,
}: {
displayName: string | undefined;
picture: string | undefined;
customStyles?: CSSProperties;
}) => {
export const Avatar = ({ displayName, picture, customStyles }: AvatarProps) => {
const classes = useStyles();
return (
<MaterialAvatar
@@ -63,11 +49,11 @@ export const Avatar = ({
src={picture}
className={classes.avatar}
style={{
backgroundColor: stringToColour(displayName || picture || ''),
backgroundColor: stringToColor(displayName || picture || ''),
...customStyles,
}}
>
{displayName && displayName.match(/\b\w/g)!.join('').substring(0, 2)}
{displayName && extractInitials(displayName)}
</MaterialAvatar>
);
};
@@ -0,0 +1,37 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { extractInitials, stringToColor } from './utils';
describe('stringToColor', () => {
it('extract color', async () => {
expect(stringToColor('Jenny Doe')).toEqual('#7809fa');
});
});
describe('extractInitials', () => {
it('extract initials', async () => {
expect(extractInitials('Jenny Doe')).toEqual('JD');
});
it('extract single letter for short name', async () => {
expect(extractInitials('Doe')).toEqual('D');
});
it('limit the initials to two letters', async () => {
expect(extractInitials('John Jonathan Doe')).toEqual('JJ');
});
});
@@ -0,0 +1,32 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export function stringToColor(str: string) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
let color = '#';
for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 0xff;
color += `00${value.toString(16)}`.substr(-2);
}
return color;
}
export function extractInitials(value: string) {
return value.match(/\b\w/g)!.join('').substring(0, 2);
}
+1
View File
@@ -15,6 +15,7 @@
*/
export * from './AlertDisplay';
export * from './Avatar';
export * from './Button';
export * from './CodeSnippet';
export * from './CopyTextButton';
@@ -13,8 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import Alert from '@material-ui/lab/Alert';
import {
Entity,
RELATION_MEMBER_OF,
UserEntity,
} from '@backstage/catalog-model';
import { Avatar, InfoCard, Progress, useApi } from '@backstage/core';
import { catalogApiRef, entityRouteParams } from '@backstage/plugin-catalog';
import {
Box,
createStyles,
@@ -24,16 +29,10 @@ import {
Theme,
Typography,
} from '@material-ui/core';
import { InfoCard, Progress, useApi } from '@backstage/core';
import {
UserEntity,
RELATION_MEMBER_OF,
Entity,
} from '@backstage/catalog-model';
import { Link as RouterLink, generatePath } from 'react-router-dom';
import { catalogApiRef, entityRouteParams } from '@backstage/plugin-catalog';
import Alert from '@material-ui/lab/Alert';
import React from 'react';
import { generatePath, Link as RouterLink } from 'react-router-dom';
import { useAsync } from 'react-use';
import { Avatar } from '../../../Avatar';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
@@ -13,21 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Box, Grid, Link, Tooltip, Typography } from '@material-ui/core';
import Alert from '@material-ui/lab/Alert';
import { InfoCard } from '@backstage/core';
import { entityRouteParams } from '@backstage/plugin-catalog';
import {
Entity,
RELATION_MEMBER_OF,
UserEntity,
} from '@backstage/catalog-model';
import { Avatar, InfoCard } from '@backstage/core';
import { entityRouteParams } from '@backstage/plugin-catalog';
import { Box, Grid, Link, Tooltip, Typography } from '@material-ui/core';
import EmailIcon from '@material-ui/icons/Email';
import GroupIcon from '@material-ui/icons/Group';
import PersonIcon from '@material-ui/icons/Person';
import { Link as RouterLink, generatePath } from 'react-router-dom';
import { Avatar } from '../../../Avatar';
import Alert from '@material-ui/lab/Alert';
import React from 'react';
import { generatePath, Link as RouterLink } from 'react-router-dom';
const GroupLink = ({
groupName,