From 8ef71ed32e8c3cd7eb6f4533fc4c476023da5b24 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Wed, 9 Dec 2020 15:59:22 +0100 Subject: [PATCH] Add a Avatar component to @backstage/core --- .changeset/gorgeous-scissors-jog.md | 6 +++ .../src/components/Avatar/Avatar.stories.tsx | 42 +++++++++++++++++++ .../src/components/Avatar/Avatar.test.tsx | 27 ++++++++++++ .../core}/src/components/Avatar/Avatar.tsx | 30 ++++--------- .../core}/src/components/Avatar/index.ts | 0 .../core/src/components/Avatar/util.test.ts | 37 ++++++++++++++++ packages/core/src/components/Avatar/utils.ts | 32 ++++++++++++++ packages/core/src/components/index.ts | 1 + .../Group/MembersList/MembersListCard.tsx | 21 +++++----- .../User/UserProfileCard/UserProfileCard.tsx | 13 +++--- 10 files changed, 169 insertions(+), 40 deletions(-) create mode 100644 .changeset/gorgeous-scissors-jog.md create mode 100644 packages/core/src/components/Avatar/Avatar.stories.tsx create mode 100644 packages/core/src/components/Avatar/Avatar.test.tsx rename {plugins/org => packages/core}/src/components/Avatar/Avatar.tsx (67%) rename {plugins/org => packages/core}/src/components/Avatar/index.ts (100%) create mode 100644 packages/core/src/components/Avatar/util.test.ts create mode 100644 packages/core/src/components/Avatar/utils.ts diff --git a/.changeset/gorgeous-scissors-jog.md b/.changeset/gorgeous-scissors-jog.md new file mode 100644 index 0000000000..3f192808c1 --- /dev/null +++ b/.changeset/gorgeous-scissors-jog.md @@ -0,0 +1,6 @@ +--- +'@backstage/core': patch +'@backstage/plugin-org': patch +--- + +Add a `` component to `@backstage/core`. diff --git a/packages/core/src/components/Avatar/Avatar.stories.tsx b/packages/core/src/components/Avatar/Avatar.stories.tsx new file mode 100644 index 0000000000..5ac628d72b --- /dev/null +++ b/packages/core/src/components/Avatar/Avatar.stories.tsx @@ -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 = () => ( + +); + +export const NameFallback = () => ; + +export const Empty = () => ; + +export const CustomStyling = () => ( + +); diff --git a/packages/core/src/components/Avatar/Avatar.test.tsx b/packages/core/src/components/Avatar/Avatar.test.tsx new file mode 100644 index 0000000000..da6ca8f42e --- /dev/null +++ b/packages/core/src/components/Avatar/Avatar.test.tsx @@ -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('', () => { + it('renders without exploding', async () => { + const { getByText } = render(); + + expect(getByText('JD')).toBeInTheDocument(); + }); +}); diff --git a/plugins/org/src/components/Avatar/Avatar.tsx b/packages/core/src/components/Avatar/Avatar.tsx similarity index 67% rename from plugins/org/src/components/Avatar/Avatar.tsx rename to packages/core/src/components/Avatar/Avatar.tsx index 637973ec3b..95aa4a8ced 100644 --- a/plugins/org/src/components/Avatar/Avatar.tsx +++ b/packages/core/src/components/Avatar/Avatar.tsx @@ -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 ( - {displayName && displayName.match(/\b\w/g)!.join('').substring(0, 2)} + {displayName && extractInitials(displayName)} ); }; diff --git a/plugins/org/src/components/Avatar/index.ts b/packages/core/src/components/Avatar/index.ts similarity index 100% rename from plugins/org/src/components/Avatar/index.ts rename to packages/core/src/components/Avatar/index.ts diff --git a/packages/core/src/components/Avatar/util.test.ts b/packages/core/src/components/Avatar/util.test.ts new file mode 100644 index 0000000000..94de957e8e --- /dev/null +++ b/packages/core/src/components/Avatar/util.test.ts @@ -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'); + }); +}); diff --git a/packages/core/src/components/Avatar/utils.ts b/packages/core/src/components/Avatar/utils.ts new file mode 100644 index 0000000000..5990a72955 --- /dev/null +++ b/packages/core/src/components/Avatar/utils.ts @@ -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); +} diff --git a/packages/core/src/components/index.ts b/packages/core/src/components/index.ts index bcc56debea..a8ae0213c4 100644 --- a/packages/core/src/components/index.ts +++ b/packages/core/src/components/index.ts @@ -15,6 +15,7 @@ */ export * from './AlertDisplay'; +export * from './Avatar'; export * from './Button'; export * from './CodeSnippet'; export * from './CopyTextButton'; diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx index cd39241ca8..f16f3701c2 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx @@ -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({ diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx index 0f32835d70..889933a58b 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx @@ -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,