diff --git a/packages/app/package.json b/packages/app/package.json index e671e23b79..d46ce8b439 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -18,6 +18,7 @@ "@backstage/plugin-github-actions": "^0.2.3", "@backstage/plugin-gitops-profiles": "^0.2.1", "@backstage/plugin-graphiql": "^0.2.1", + "@backstage/plugin-org": "^0.3.0", "@backstage/plugin-jenkins": "^0.3.2", "@backstage/plugin-kubernetes": "^0.3.1", "@backstage/plugin-lighthouse": "^0.2.4", diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 13cbe324c6..2378afbc5e 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -13,7 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ApiEntity, Entity } from '@backstage/catalog-model'; +import { + ApiEntity, + Entity, + GroupEntity, + UserEntity, +} from '@backstage/catalog-model'; import { EmptyState } from '@backstage/core'; import { ApiDefinitionCard, @@ -48,6 +53,12 @@ import { isPluginApplicableToEntity as isLighthouseAvailable, LastLighthouseAuditCard, } from '@backstage/plugin-lighthouse'; +import { + OwnershipCard, + MembersListCard, + GroupProfileCard, + UserProfileCard, +} from '@backstage/plugin-org'; import { Router as SentryRouter } from '@backstage/plugin-sentry'; import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs'; import { Button, Grid } from '@material-ui/core'; @@ -323,6 +334,51 @@ const ApiEntityPage = ({ entity }: { entity: Entity }) => ( ); +const UserOverviewContent = ({ entity }: { entity: UserEntity }) => ( + + + + + + + + +); + +const UserEntityPage = ({ entity }: { entity: Entity }) => ( + + } + /> + +); + +const GroupOverviewContent = ({ entity }: { entity: GroupEntity }) => ( + + + + + + + + + + + +); + +const GroupEntityPage = ({ entity }: { entity: Entity }) => ( + + } + /> + +); + export const EntityPage = () => { const { entity } = useEntity(); @@ -331,6 +387,10 @@ export const EntityPage = () => { return ; case 'api': return ; + case 'group': + return ; + case 'user': + return ; default: return ; } diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index c6a2a0e7e0..6a4924cb0e 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -42,3 +42,4 @@ export { plugin as UserSettings } from '@backstage/plugin-user-settings'; export { plugin as PagerDuty } from '@backstage/plugin-pagerduty'; export { plugin as Buildkite } from '@roadiehq/backstage-plugin-buildkite'; export { plugin as Search } from '@backstage/plugin-search'; +export { plugin as Org } from '@backstage/plugin-org'; diff --git a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx index 8c48d69413..eb75593ffc 100644 --- a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx +++ b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx @@ -39,12 +39,12 @@ const EntityPageTitle = ({ ); -function headerProps( +const headerProps = ( kind: string, namespace: string | undefined, name: string, entity: Entity | undefined, -): { headerTitle: string; headerType: string } { +): { headerTitle: string; headerType: string } => { return { headerTitle: `${name}${ namespace && namespace !== ENTITY_DEFAULT_NAMESPACE @@ -60,7 +60,7 @@ function headerProps( return t; })(), }; -} +}; export const EntityPageLayout = ({ children }: PropsWithChildren<{}>) => { const { kind, namespace, name } = useEntityCompoundName(); @@ -88,7 +88,8 @@ export const EntityPageLayout = ({ children }: PropsWithChildren<{}>) => { pageTitleOverride={headerTitle} type={headerType} > - {entity && ( + {/* TODO: fix after catalog page customization is added */} + {entity && kind !== 'user' && ( <> + createStyles({ + avatar: { + width: '4rem', + height: '4rem', + color: '#fff', + fontWeight: theme.typography.fontWeightBold, + letterSpacing: '1px', + textTransform: 'uppercase', + }, + }), +); + +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 const Avatar = ({ + displayName, + picture, + customStyles, +}: { + displayName: string | undefined; + picture: string | undefined; + customStyles?: CSSProperties; +}) => { + const classes = useStyles(); + return ( + + {displayName && displayName.match(/\b\w/g)!.join('').substring(0, 2)} + + ); +}; diff --git a/plugins/org/src/components/Avatar/index.ts b/plugins/org/src/components/Avatar/index.ts new file mode 100644 index 0000000000..962414634e --- /dev/null +++ b/plugins/org/src/components/Avatar/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { Avatar } from './Avatar'; diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx new file mode 100644 index 0000000000..ce8829439c --- /dev/null +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx @@ -0,0 +1,141 @@ +/* + * 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. + */ + +/* + * 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 { 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, + GroupEntity, + RELATION_CHILD_OF, + RELATION_PARENT_OF, +} from '@backstage/catalog-model'; +import AccountTreeIcon from '@material-ui/icons/AccountTree'; +import GroupIcon from '@material-ui/icons/Group'; +import { Link as RouterLink, generatePath } from 'react-router-dom'; + +const GroupLink = ({ + groupName, + index = 0, + entity, +}: { + groupName: string; + index?: number; + entity: Entity; +}) => ( + <> + {index >= 1 ? ', ' : ''} + + [{groupName}] + + +); + +const CardTitle = ({ title }: { title: string }) => ( + + + {title} + +); + +export const GroupProfileCard = ({ + entity: group, + variant, +}: { + entity: GroupEntity; + variant: string; +}) => { + const { + metadata: { name, description }, + } = group; + const parent = group?.relations + ?.filter(r => r.type === RELATION_CHILD_OF) + ?.map(group => group.target.name) + .toString(); + + const childrens = group?.relations + ?.filter(r => r.type === RELATION_PARENT_OF) + ?.map(group => group.target.name); + + if (!group) return User not found; + + return ( + } + subheader={description} + variant={variant} + > + + + {parent ? ( + + + + + + + + + + + ) : null} + {childrens?.length ? ( + + + + + + + {childrens.map((children, index) => ( + + ))} + + + + ) : null} + + + + ); +}; diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/index.ts b/plugins/org/src/components/Cards/Group/GroupProfile/index.ts new file mode 100644 index 0000000000..44efe25a50 --- /dev/null +++ b/plugins/org/src/components/Cards/Group/GroupProfile/index.ts @@ -0,0 +1,16 @@ +/* + * 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 * from './GroupProfileCard'; diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx new file mode 100644 index 0000000000..0c188afc57 --- /dev/null +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx @@ -0,0 +1,100 @@ +/* + * 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 { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; +import React from 'react'; +import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog'; +import { Entity } from '@backstage/catalog-model'; +import { MembersListCard } from './MembersListCard'; + +describe('MemberTab Test', () => { + const groupEntity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name: 'team-d', + description: 'The evil-corp organization', + namespace: 'default', + }, + spec: { + type: 'team', + parent: 'boxoffice', + ancestors: ['boxoffice', 'acme-corp'], + children: [], + descendants: [], + }, + }; + + const catalogApi: Partial = { + getEntities: () => + Promise.resolve({ + items: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'tara.macgovern', + namespace: 'default', + uid: 'a5gerth56', + }, + relations: [ + { + type: 'memberOf', + target: { + kind: 'group', + name: 'team-d', + namespace: 'default', + }, + }, + ], + spec: { + profile: { + displayName: 'Tara MacGovern', + email: 'tara-macgovern@example.com', + picture: 'https://example.com/staff/tara.jpeg', + }, + memberOf: ['team-d'], + }, + }, + ] as Entity[], + }), + }; + + const apis = ApiRegistry.from([[catalogApiRef, catalogApi]]); + + it('Display Profile Card', async () => { + const rendered = await renderWithEffects( + wrapInTestApp( + + + , + ), + ); + + expect(rendered.getByAltText('Tara MacGovern')).toHaveAttribute( + 'src', + 'https://example.com/staff/tara.jpeg', + ); + expect( + rendered.getByText('tara-macgovern@example.com'), + ).toBeInTheDocument(); + expect(rendered.getByText('Tara MacGovern')).toHaveAttribute( + 'href', + '/catalog/default/user/tara.macgovern', + ); + }); +}); diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx new file mode 100644 index 0000000000..cd39241ca8 --- /dev/null +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx @@ -0,0 +1,155 @@ +/* + * 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 Alert from '@material-ui/lab/Alert'; +import { + Box, + createStyles, + Grid, + Link, + makeStyles, + 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 { useAsync } from 'react-use'; +import { Avatar } from '../../../Avatar'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + card: { + border: `1px solid ${theme.palette.divider}`, + boxShadow: theme.shadows[2], + borderRadius: '4px', + overflow: 'visible', + position: 'relative', + margin: theme.spacing(3, 0, 0), + }, + }), +); + +const MemberComponent = ({ + member, + groupEntity, +}: { + member: UserEntity; + groupEntity: Entity; +}) => { + const classes = useStyles(); + const { name: metaName } = member.metadata; + const { profile } = member.spec; + return ( + + + + + + + + {profile?.displayName} + + + {profile?.email} + + + + + ); +}; + +export const MembersListCard = ({ + entity: groupEntity, +}: { + entity: Entity; +}) => { + const { + metadata: { name: groupName }, + } = groupEntity; + const catalogApi = useApi(catalogApiRef); + + const { loading, error, value: members } = useAsync(async () => { + const membersList = await catalogApi.getEntities({ + filter: { + kind: 'User', + }, + }); + const groupMembersList = ((membersList.items as unknown) as Array< + UserEntity + >).filter(member => + member?.relations?.some( + r => r.type === RELATION_MEMBER_OF && r.target.name === groupName, + ), + ); + return groupMembersList; + }, [catalogApi]); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + return ( + + + + {members && members.length ? ( + members.map(member => ( + + )) + ) : ( + + This group has no members. + + )} + + + + ); +}; diff --git a/plugins/org/src/components/Cards/Group/MembersList/index.ts b/plugins/org/src/components/Cards/Group/MembersList/index.ts new file mode 100644 index 0000000000..c3f4ea9178 --- /dev/null +++ b/plugins/org/src/components/Cards/Group/MembersList/index.ts @@ -0,0 +1,16 @@ +/* + * 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 * from './MembersListCard'; diff --git a/plugins/org/src/components/Cards/Group/index.ts b/plugins/org/src/components/Cards/Group/index.ts new file mode 100644 index 0000000000..a011891f62 --- /dev/null +++ b/plugins/org/src/components/Cards/Group/index.ts @@ -0,0 +1,17 @@ +/* + * 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 * from './MembersList'; +export * from './GroupProfile'; diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx new file mode 100644 index 0000000000..7c4ad7a56a --- /dev/null +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx @@ -0,0 +1,196 @@ +/* + * 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 { InfoCard, useApi, Progress } from '@backstage/core'; +import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; +import { catalogApiRef } from '@backstage/plugin-catalog'; +import { useAsync } from 'react-use'; +import Alert from '@material-ui/lab/Alert'; +import { + Box, + createStyles, + Grid, + makeStyles, + Theme, + Typography, +} from '@material-ui/core'; +import { pageTheme } from '@backstage/theme'; + +type EntitiesKinds = 'Component' | 'API'; +type EntitiesTypes = + | 'service' + | 'website' + | 'library' + | 'documentation' + | 'api' + | 'tool'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + card: { + border: `1px solid ${theme.palette.divider}`, + boxShadow: theme.shadows[2], + borderRadius: '4px', + padding: theme.spacing(2), + color: '#fff', + transition: `${theme.transitions.duration.standard}ms`, + '&:hover': { + boxShadow: theme.shadows[4], + }, + }, + bold: { + fontWeight: theme.typography.fontWeightBold, + }, + service: { + background: `${pageTheme.home.shape}, linear-gradient(90deg, ${pageTheme.service.colors})`, + }, + website: { + background: `${pageTheme.home.shape}, linear-gradient(90deg, ${pageTheme.website.colors})`, + }, + library: { + background: `${pageTheme.home.shape}, linear-gradient(90deg, ${pageTheme.library.colors})`, + }, + documentation: { + background: `${pageTheme.home.shape}, linear-gradient(90deg, ${pageTheme.documentation.colors})`, + }, + api: { + background: `${pageTheme.home.shape}, linear-gradient(90deg, #005B4B, #005B4B)`, + }, + tool: { + background: `${pageTheme.home.shape}, linear-gradient(90deg, ${pageTheme.tool.colors})`, + }, + }), +); + +const countEntitiesBy = ( + entities: Array, + kind: EntitiesKinds, + type?: EntitiesTypes, +) => + entities.filter( + e => e.kind === kind && (type ? e?.spec?.type === type : true), + ).length; + +const EntityCountTile = ({ + counter, + className, + name, +}: { + counter: number; + className: EntitiesTypes; + name: string; +}) => { + const classes = useStyles(); + return ( + + + {counter} + + + {name} + + + ); +}; + +export const OwnershipCard = ({ + entity, + variant, +}: { + entity: Entity; + variant: string; +}) => { + const { + metadata: { name: groupName }, + } = entity; + const catalogApi = useApi(catalogApiRef); + const { + loading, + error, + value: componentsWithCounters, + } = useAsync(async () => { + const entitiesList = await catalogApi.getEntities(); + const ownedEntitiesList = entitiesList.items.filter(component => + component?.relations?.some( + r => r.type === RELATION_OWNED_BY && r.target.name === groupName, + ), + ) as Array; + + return [ + { + counter: countEntitiesBy(ownedEntitiesList, 'Component', 'service'), + className: 'service', + name: 'Services', + }, + { + counter: countEntitiesBy( + ownedEntitiesList, + 'Component', + 'documentation', + ), + className: 'documentation', + name: 'Documentation', + }, + { + counter: countEntitiesBy(ownedEntitiesList, 'API'), + className: 'api', + name: 'APIs', + }, + { + counter: countEntitiesBy(ownedEntitiesList, 'Component', 'library'), + className: 'library', + name: 'Libraries', + }, + { + counter: countEntitiesBy(ownedEntitiesList, 'Component', 'website'), + className: 'website', + name: 'Websites', + }, + { + counter: countEntitiesBy(ownedEntitiesList, 'Component', 'tool'), + className: 'tool', + name: 'Tools', + }, + ] as Array<{ counter: number; className: EntitiesTypes; name: string }>; + }, [catalogApi]); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + return ( + + + {componentsWithCounters?.map(c => ( + + + + ))} + + + ); +}; diff --git a/plugins/org/src/components/Cards/OwnershipCard/index.ts b/plugins/org/src/components/Cards/OwnershipCard/index.ts new file mode 100644 index 0000000000..1fa1bb4044 --- /dev/null +++ b/plugins/org/src/components/Cards/OwnershipCard/index.ts @@ -0,0 +1,16 @@ +/* + * 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 * from './OwnershipCard'; diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx new file mode 100644 index 0000000000..77708e6f27 --- /dev/null +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx @@ -0,0 +1,64 @@ +/* + * 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 { UserEntity } from '@backstage/catalog-model'; +import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; +import React from 'react'; +import { UserProfileCard } from './UserProfileCard'; + +describe('UserSummary Test', () => { + const userEntity: UserEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'calum.leavy', + }, + spec: { + profile: { + displayName: 'Calum Leavy', + email: 'calum-leavy@example.com', + picture: 'https://example.com/staff/calum.jpeg', + }, + memberOf: ['ExampleGroup'], + }, + relations: [ + { + type: 'memberOf', + target: { + kind: 'group', + name: 'ExampleGroup', + namespace: 'default', + }, + }, + ], + }; + + it('Display Profile Card', async () => { + const rendered = await renderWithEffects( + wrapInTestApp(), + ); + + expect(rendered.getByText('calum-leavy@example.com')).toBeInTheDocument(); + expect(rendered.getByAltText('Calum Leavy')).toHaveAttribute( + 'src', + 'https://example.com/staff/calum.jpeg', + ); + expect(rendered.getByText('[ExampleGroup]')).toHaveAttribute( + 'href', + '/catalog/default/group/ExampleGroup', + ); + }); +}); diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx new file mode 100644 index 0000000000..0f32835d70 --- /dev/null +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx @@ -0,0 +1,134 @@ +/* + * 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 { 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 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'; + +const GroupLink = ({ + groupName, + index, + entity, +}: { + groupName: string; + index: number; + entity: Entity; +}) => ( + <> + {index >= 1 ? ', ' : ''} + + [{groupName}] + + +); + +const CardTitle = ({ title }: { title?: string }) => + title ? ( + + + {title} + + ) : null; + +export const UserProfileCard = ({ + entity: user, + variant, +}: { + entity: UserEntity; + variant: string; +}) => { + const { + spec: { profile }, + } = user; + const groupNames = + user?.relations + ?.filter(r => r.type === RELATION_MEMBER_OF) + ?.map(group => group.target.name) || []; + + if (!user) return User not found; + + return ( + } + variant={variant} + > + + + + + + + + + + + + + {profile?.email && ( + + {profile.email} + + )} + + + + + + + + + {groupNames.map((groupName, index) => ( + + ))} + + + + + + + ); +}; diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/index.ts b/plugins/org/src/components/Cards/User/UserProfileCard/index.ts new file mode 100644 index 0000000000..dc5e2902b7 --- /dev/null +++ b/plugins/org/src/components/Cards/User/UserProfileCard/index.ts @@ -0,0 +1,16 @@ +/* + * 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 * from './UserProfileCard'; diff --git a/plugins/org/src/components/Cards/User/index.ts b/plugins/org/src/components/Cards/User/index.ts new file mode 100644 index 0000000000..dc5e2902b7 --- /dev/null +++ b/plugins/org/src/components/Cards/User/index.ts @@ -0,0 +1,16 @@ +/* + * 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 * from './UserProfileCard'; diff --git a/plugins/org/src/components/Cards/index.ts b/plugins/org/src/components/Cards/index.ts new file mode 100644 index 0000000000..62f63da3d4 --- /dev/null +++ b/plugins/org/src/components/Cards/index.ts @@ -0,0 +1,18 @@ +/* + * 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 * from './Group'; +export * from './User'; +export * from './OwnershipCard'; diff --git a/plugins/org/src/components/index.ts b/plugins/org/src/components/index.ts new file mode 100644 index 0000000000..975f66bd25 --- /dev/null +++ b/plugins/org/src/components/index.ts @@ -0,0 +1,17 @@ +/* + * 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 * from './Cards'; diff --git a/plugins/org/src/index.ts b/plugins/org/src/index.ts new file mode 100644 index 0000000000..77ad7f9266 --- /dev/null +++ b/plugins/org/src/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { plugin } from './plugin'; +export * from './components'; diff --git a/plugins/org/src/plugin.test.ts b/plugins/org/src/plugin.test.ts new file mode 100644 index 0000000000..d77cfd7ae8 --- /dev/null +++ b/plugins/org/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * 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 { plugin } from './plugin'; + +describe('groups', () => { + it('should export plugin', () => { + expect(plugin).toBeDefined(); + }); +}); diff --git a/plugins/org/src/plugin.ts b/plugins/org/src/plugin.ts new file mode 100644 index 0000000000..39c3502fb5 --- /dev/null +++ b/plugins/org/src/plugin.ts @@ -0,0 +1,20 @@ +/* + * 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 { createPlugin } from '@backstage/core'; + +export const plugin = createPlugin({ + id: 'org', +}); diff --git a/plugins/org/src/setupTests.ts b/plugins/org/src/setupTests.ts new file mode 100644 index 0000000000..43b8421558 --- /dev/null +++ b/plugins/org/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * 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 '@testing-library/jest-dom'; +import 'cross-fetch/polyfill';