diff --git a/.changeset/tender-jeans-clean.md b/.changeset/tender-jeans-clean.md new file mode 100644 index 0000000000..2729fa393a --- /dev/null +++ b/.changeset/tender-jeans-clean.md @@ -0,0 +1,12 @@ +--- +'@backstage/catalog-model': minor +'@backstage/plugin-org': minor +--- + +Updates the profile of Group and User to allow any extra string key pair value. + +Then updates the user profile and group profile cards to display any links and extra profile details. + +This allows extra customization without going down the full customization route. + +So for example if you wanted to add address, phone number, job title, slack link to users or departments this allows you to within the current spec diff --git a/ADOPTERS.md b/ADOPTERS.md index ca56a00890..d37aabde72 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -215,3 +215,4 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/ | [Ferrovial](https://ferrovial.com) | [Jose Luis Rosado](mailto:jlrosado@ferrovial.com) | Backstage is helping us to improve and acelerate dev experience helping teams to quickly find technical documentation, infrastructure templates, pipelines, software components and quickstarters that have been developed by our squads in a inner source friendly environment. | | [Inter&Co](https://bancointer.com.br) | [Arnaud Lanna](https://github.com/arnaudlanna), [Adriano Silva](https://github.com/adrianovss), [Bruno Grossi](https://github.com/begrossi) | We're using Backstage as our internal Developer Portal to catalog and collect repositories and microservices pieces of information like ownership, deployment time, and documentation. | | [StatusNeo](https://statusneo.com/) | [Karan Nangru](mailto:nangru@statusneo.com), [@NishkarshRaj](https://github.com/NishkarshRaj), and [Gaurav Sarien](mailto:gaurav.sarien@statusneo.com) | Harnessing the power of central catalog inventory and self-serving software templates | +| [Contino](https://www.contino.io/) | [Joseph Patterson](mailto:joseph.patterson@contino.io) | Building out a central catalog for our software community | diff --git a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts index 8d88817dbe..9e514b1000 100644 --- a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts @@ -18,6 +18,14 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/Group.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; +interface GroupProfileStatic { + displayName?: string; + email?: string; + picture?: string; +} + +type GroupProfile = Record & GroupProfileStatic; + /** * Backstage catalog Group kind Entity. * @@ -28,11 +36,7 @@ export interface GroupEntityV1alpha1 extends Entity { kind: 'Group'; spec: { type: string; - profile?: { - displayName?: string; - email?: string; - picture?: string; - }; + profile?: GroupProfile; parent?: string; children: string[]; members?: string[]; diff --git a/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts b/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts index 55c9b176ea..53446a5275 100644 --- a/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts @@ -18,6 +18,14 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/User.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; +interface UserProfileStatic { + displayName?: string; + email?: string; + picture?: string; +} + +type UserProfile = Record & UserProfileStatic; + /** * Backstage catalog User kind Entity. * @@ -27,11 +35,7 @@ export interface UserEntityV1alpha1 extends Entity { apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; kind: 'User'; spec: { - profile?: { - displayName?: string; - email?: string; - picture?: string; - }; + profile?: UserProfile; memberOf?: string[]; }; } diff --git a/packages/core-components/src/components/Table/Table.tsx b/packages/core-components/src/components/Table/Table.tsx index 49538b680a..13f77106e0 100644 --- a/packages/core-components/src/components/Table/Table.tsx +++ b/packages/core-components/src/components/Table/Table.tsx @@ -373,7 +373,7 @@ export function Table(props: TableProps) { const newData = (data as any[]).filter( el => !!Object.entries(selectedFilters) - .filter(([, value]) => !!value.length) + .filter(([, value]) => !!(value as []).length) .every(([key, filterValue]) => { const fieldValue = extractValueByField( el, diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx index fc45d9a8fe..7d28b5a2fa 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx @@ -87,3 +87,46 @@ export default { ), ], }; + +const extraDetailsEntity: GroupEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'team-a', + description: 'Team A', + links: [ + { + url: 'slack://user?team=T00000000&id=U00000000', + title: 'Slack', + icon: 'message', + }, + { + url: 'https://www.google.com', + title: 'Google', + }, + ], + }, + spec: { + profile: { + displayName: 'Team A', + email: 'team-a@example.com', + picture: + 'https://avatars.dicebear.com/api/identicon/team-a@example.com.svg?background=%23fff&margin=25', + Telephone: '123456789', + Location: 'London', + }, + type: 'group', + children: [], + }, + relations: [dummyDepartment], +}; + +export const ExtraDetails = () => ( + + + + + + + +); diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx index dfdb566b20..c12b4b8906 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx @@ -37,10 +37,13 @@ import { ListItemText, Tooltip, IconButton, + Divider, } from '@material-ui/core'; +import Icon from '@material-ui/core/Icon'; import AccountTreeIcon from '@material-ui/icons/AccountTree'; import EmailIcon from '@material-ui/icons/Email'; import GroupIcon from '@material-ui/icons/Group'; +import LinkIcon from '@material-ui/icons/Link'; import EditIcon from '@material-ui/icons/Edit'; import CachedIcon from '@material-ui/icons/Cached'; import Alert from '@material-ui/lab/Alert'; @@ -53,6 +56,8 @@ import { } from '@backstage/core-components'; import { alertApiRef, useApi } from '@backstage/core-plugin-api'; +const staticProfileKeys = ['displayName', 'email', 'picture']; + const CardTitle = (props: { title: string }) => ( @@ -76,7 +81,7 @@ export const GroupProfileCard = (props: { variant?: InfoCardVariants }) => { } const { - metadata: { name, description, annotations }, + metadata: { name, description, annotations, links }, spec: { profile }, } = group; @@ -94,6 +99,11 @@ export const GroupProfileCard = (props: { variant?: InfoCardVariants }) => { const entityMetadataEditUrl = group.metadata.annotations?.[ANNOTATION_EDIT_URL]; + const profileKeys = + profile !== undefined + ? Object.keys(profile).filter(key => !staticProfileKeys.includes(key)) + : []; + const displayName = profile?.displayName ?? name; const emailHref = profile?.email ? `mailto:${profile.email}` : '#'; const infoCardAction = entityMetadataEditUrl ? ( @@ -190,6 +200,42 @@ export const GroupProfileCard = (props: { variant?: InfoCardVariants }) => { secondary="Child Groups" /> + {links !== undefined && } + {links !== undefined && + links.map(link => { + return ( + + {link.icon ? ( + + + {link.icon} + + + ) : ( + + + + )} + {link.title} + + ); + })} + {profile !== undefined && profileKeys.length > 0 && } + {profile !== undefined && + profileKeys.length > 0 && + profileKeys.map(key => { + const value = profile[key]; + + return ( + + + {key} + + + {value} + + ); + })} diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx index eb7b8fadda..28fc3d3831 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx @@ -102,3 +102,46 @@ export default { }), ], }; + +const extraDetailsEntity: UserEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'guest', + description: 'Description for guest', + links: [ + { + url: 'slack://user?team=T00000000&id=U00000000', + title: 'Slack', + icon: 'message', + }, + { + url: 'https://www.google.com', + title: 'Google', + }, + ], + }, + spec: { + profile: { + displayName: 'Guest User', + email: 'guest@example.com', + picture: + 'https://avatars.dicebear.com/api/avataaars/guest@example.com.svg?background=%23fff', + 'Job Title': 'Software Engineer', + Department: 'Engineering', + Location: 'San Francisco, CA', + }, + memberOf: ['team-a'], + }, + relations: [dummyGroup], +}; + +export const ExtraDetails = () => ( + + + + + + + +); diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx index f339a5828c..7eabdf2903 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx @@ -155,4 +155,65 @@ describe('Edit Button', () => { ); expect(rendered.getByRole('button')).toBeInTheDocument(); }); + + it('Should show the extra fields if either links or extra profile are filled', async () => { + const annotations: Record = { + 'backstage.io/edit-url': 'https://example.com/user.yaml', + }; + const userEntity: UserEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'calum.leavy', + description: 'Super awesome human', + annotations, + links: [ + { + url: 'slack://user?team=T00000000&id=U00000000', + title: 'Slack', + icon: 'message', + }, + { + url: 'https://www.google.com', + title: 'Google', + }, + ], + }, + spec: { + profile: { + displayName: 'Calum Leavy', + email: 'calum-leavy@example.com', + 'Job Title': 'Software Engineer', + Department: 'Engineering', + Location: 'San Francisco, CA', + }, + memberOf: ['ExampleGroup'], + }, + relations: [ + { + type: 'memberOf', + targetRef: 'group:default/examplegroup', + }, + ], + }; + + const rendered = await renderWithEffects( + wrapInTestApp( + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ), + ); + expect(rendered.getByText('Software Engineer')).toBeInTheDocument(); + expect(rendered.getByText('Department')).toBeInTheDocument(); + expect(rendered.getByText('San Francisco, CA')).toBeInTheDocument(); + expect(rendered.getByText('Location')).toBeInTheDocument(); + expect(rendered.getByText('Slack')).toBeInTheDocument(); + expect(rendered.getByText('Google')).toBeInTheDocument(); + }); }); diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx index dd75578687..68a27764d8 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx @@ -33,12 +33,15 @@ import { ListItemIcon, ListItemText, Tooltip, + Divider, } from '@material-ui/core'; import EditIcon from '@material-ui/icons/Edit'; import EmailIcon from '@material-ui/icons/Email'; import GroupIcon from '@material-ui/icons/Group'; import PersonIcon from '@material-ui/icons/Person'; +import LinkIcon from '@material-ui/icons/Link'; import Alert from '@material-ui/lab/Alert'; +import Icon from '@material-ui/core/Icon'; import React from 'react'; import { Avatar, @@ -47,6 +50,8 @@ import { Link, } from '@backstage/core-components'; +const staticProfileKeys = ['displayName', 'email', 'picture']; + const CardTitle = (props: { title?: string }) => props.title ? ( @@ -66,7 +71,7 @@ export const UserProfileCard = (props: { variant?: InfoCardVariants }) => { user.metadata.annotations?.[ANNOTATION_EDIT_URL]; const { - metadata: { name: metaName, description }, + metadata: { name: metaName, description, links }, spec: { profile }, } = user; const displayName = profile?.displayName ?? metaName; @@ -75,6 +80,11 @@ export const UserProfileCard = (props: { variant?: InfoCardVariants }) => { kind: 'Group', }); + const profileKeys = + profile !== undefined + ? Object.keys(profile).filter(key => !staticProfileKeys.includes(key)) + : []; + return ( } @@ -128,6 +138,44 @@ export const UserProfileCard = (props: { variant?: InfoCardVariants }) => { /> + + {links !== undefined && } + {links !== undefined && + links.map(link => { + return ( + + {link.icon ? ( + + + {link.icon} + + + ) : ( + + + + )} + {link.title} + + ); + })} + + {profile !== undefined && profileKeys.length > 0 && } + {profile !== undefined && + profileKeys.length > 0 && + profileKeys.map(key => { + const value = profile[key]; + + return ( + + + {key} + + + {value} + + ); + })} diff --git a/storybook/.storybook/preview-head.html b/storybook/.storybook/preview-head.html new file mode 100644 index 0000000000..a21b9971e8 --- /dev/null +++ b/storybook/.storybook/preview-head.html @@ -0,0 +1,4 @@ +