Feat: Org Plugin (#3448)

* Feat: Create Groups plugin

* Feat: update code after CR

* Feat: change routing, replace filters to use relations, modify EntityPageLayout to support users

* Feat: update CLI version

* Feat: add some tests

* Update daily cost data to return groupedCosts

* Add aggregation sum util

* Add top panel breakdown view

* Add top panel tabs

* Add mock data for grouped Costs

* Add comments on groupedCosts

* Update wording to product in cost by product component

* Move cost overview chart legend to separate component

* Move mock data utils

* Add data viz colors for both themes

* Move mock data utils

* Update data viz dark theme colors

* Update bar chart legend type usage

* catalog-backend: gracefully handle missing codeowners

* Derive the owned entities in the catalog from group memberships

* Filter the response headers in the proxy backend

* Make backend-commons tests work on Windows

* backend: remove yarn cache from built docker image

* Refactor the hooks and also support users owning components

* backend,yarnrc: bring yarn network-timeout down to 300s

* Fix another Windows test issue

This one is a bit tricky. Instead of testing at the root folder of the drive, this test is now relative to the current directory. This resolves the difference between "/pkgs/a" and "d:\pkgs\a"

* Drop fill opacity for lighter colors

* Update dark theme colors

* Fix review comments

* techdocs-backend: update Gitlab clone auth

* Use case-insensitive filters

* Add Kubernetes plugin (#3505)

* backend-common: allow port in config to be both a number or a string

* chore: set the port as number if it comes through as a string

* Feat: Bump GitHub Insights plugin version (#3509)

* Feat: groups and components card

* Feat: update Org plugin according to CR

* Feat: update TS stuff

* Feat: change tile titles

* Feat: bump packages

* Code review fixes

Co-authored-by: Brenda Sukh <brendasukh@gmail.com>
Co-authored-by: Fredrik Adelöw <freben@gmail.com>
Co-authored-by: Dominik Henneke <dominik.henneke@sda-se.com>
Co-authored-by: Oliver Sand <oliver.sand@tentaclelabs.com>
Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com>
Co-authored-by: Chongyang Adrian, Ke <ftt.adrian.ke@grabtaxi.com>
Co-authored-by: Adam Harvey <adam.harvey@dxc.com>
Co-authored-by: blam <ben@blam.sh>
Co-authored-by: Marek Calus <marek-calus@wp.pl>
This commit is contained in:
Mateusz Lewtak
2020-12-07 16:46:15 +01:00
committed by GitHub
parent 9878ec02d5
commit 040f453c36
28 changed files with 1233 additions and 5 deletions
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+6
View File
@@ -0,0 +1,6 @@
# Org Plugin for Backstage
## Features
- Show Group Page
- Show User Profile
+19
View File
@@ -0,0 +1,19 @@
/*
* 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 { createDevApp } from '@backstage/dev-utils';
import { plugin } from '../src/plugin';
createDevApp().registerPlugin(plugin).render();
+50
View File
@@ -0,0 +1,50 @@
{
"name": "@backstage/plugin-org",
"version": "0.3.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"scripts": {
"build": "backstage-cli plugin:build",
"start": "backstage-cli plugin:serve",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"diff": "backstage-cli plugin:diff",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.4.0",
"@backstage/core": "^0.3.2",
"@backstage/plugin-catalog": "^0.2.5",
"@backstage/theme": "^0.2.1",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.4.0",
"@backstage/dev-utils": "^0.1.5",
"@backstage/test-utils": "^0.1.4",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
"cross-fetch": "^3.0.6",
"msw": "^0.21.2"
},
"files": [
"dist"
]
}
@@ -0,0 +1,73 @@
/*
* 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, { CSSProperties } from 'react';
import {
Avatar as MaterialAvatar,
createStyles,
makeStyles,
Theme,
} from '@material-ui/core';
const useStyles = makeStyles((theme: Theme) =>
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 (
<MaterialAvatar
alt={displayName}
src={picture}
className={classes.avatar}
style={{
backgroundColor: stringToColour(displayName || picture || ''),
...customStyles,
}}
>
{displayName && displayName.match(/\b\w/g)!.join('').substring(0, 2)}
</MaterialAvatar>
);
};
@@ -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';
@@ -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 ? ', ' : ''}
<Link
component={RouterLink}
to={generatePath(
`/catalog/:namespace/group/${groupName}`,
entityRouteParams(entity),
)}
>
[{groupName}]
</Link>
</>
);
const CardTitle = ({ title }: { title: string }) => (
<Box display="flex" alignItems="center">
<GroupIcon fontSize="inherit" />
<Box ml={1}>{title}</Box>
</Box>
);
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 <Alert severity="error">User not found</Alert>;
return (
<InfoCard
title={<CardTitle title={name} />}
subheader={description}
variant={variant}
>
<Grid container spacing={3}>
<Grid item>
{parent ? (
<Typography variant="subtitle1">
<Box display="flex" alignItems="center">
<Tooltip title="Group Parent">
<AccountTreeIcon fontSize="inherit" />
</Tooltip>
<Box ml={1} display="inline">
<GroupLink groupName={parent} entity={group} />
</Box>
</Box>
</Typography>
) : null}
{childrens?.length ? (
<Typography variant="subtitle1">
<Box display="flex" alignItems="center">
<Tooltip title="Parent of">
<GroupIcon fontSize="inherit" />
</Tooltip>
<Box ml={1} display="inline">
{childrens.map((children, index) => (
<GroupLink
groupName={children}
entity={group}
index={index}
key={children}
/>
))}
</Box>
</Box>
</Typography>
) : null}
</Grid>
</Grid>
</InfoCard>
);
};
@@ -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';
@@ -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<CatalogApi> = {
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(
<ApiProvider apis={apis}>
<MembersListCard entity={groupEntity} />
</ApiProvider>,
),
);
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',
);
});
});
@@ -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 (
<Grid item xs={12} sm={6} md={3} xl={2}>
<Box className={classes.card}>
<Box
display="flex"
flexDirection="column"
m={3}
alignItems="center"
justifyContent="center"
>
<Avatar
displayName={profile?.displayName}
picture={profile?.picture}
customStyles={{
position: 'absolute',
top: '-2rem',
}}
/>
<Box pt={2} textAlign="center">
<Typography variant="h5">
<Link
component={RouterLink}
to={generatePath(
`/catalog/:namespace/user/${metaName}`,
entityRouteParams(groupEntity),
)}
>
{profile?.displayName}
</Link>
</Typography>
<Typography variant="caption">{profile?.email}</Typography>
</Box>
</Box>
</Box>
</Grid>
);
};
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 <Progress />;
} else if (error) {
return <Alert severity="error">{error.message}</Alert>;
}
return (
<Grid item>
<InfoCard
title={`Members (${members?.length || 0})`}
subheader={`of ${groupName}`}
>
<Grid container spacing={3}>
{members && members.length ? (
members.map(member => (
<MemberComponent
member={member}
groupEntity={groupEntity}
key={member.metadata.uid}
/>
))
) : (
<Box p={2}>
<Typography>This group has no members.</Typography>
</Box>
)}
</Grid>
</InfoCard>
</Grid>
);
};
@@ -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';
@@ -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';
@@ -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<Entity>,
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 (
<Box
className={`${classes.card} ${classes[className]}`}
display="flex"
flexDirection="column"
alignItems="center"
>
<Typography className={classes.bold} variant="h6">
{counter}
</Typography>
<Typography className={classes.bold} variant="h6">
{name}
</Typography>
</Box>
);
};
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<Entity>;
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 <Progress />;
} else if (error) {
return <Alert severity="error">{error.message}</Alert>;
}
return (
<InfoCard title="Ownership" variant={variant}>
<Grid container>
{componentsWithCounters?.map(c => (
<Grid item xs={12} md={6} lg={4} key={c.name}>
<EntityCountTile
counter={c.counter}
className={c.className}
name={c.name}
/>
</Grid>
))}
</Grid>
</InfoCard>
);
};
@@ -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';
@@ -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(<UserProfileCard entity={userEntity} variant="gridItem" />),
);
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',
);
});
});
@@ -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 ? ', ' : ''}
<Link
component={RouterLink}
to={generatePath(
`/catalog/:namespace/group/${groupName}`,
entityRouteParams(entity),
)}
>
[{groupName}]
</Link>
</>
);
const CardTitle = ({ title }: { title?: string }) =>
title ? (
<Box display="flex" alignItems="center">
<PersonIcon fontSize="inherit" />
<Box ml={1}>{title}</Box>
</Box>
) : 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 <Alert severity="error">User not found</Alert>;
return (
<InfoCard
title={<CardTitle title={profile?.displayName} />}
variant={variant}
>
<Grid container spacing={3}>
<Grid item xs={12} sm={2} xl={1}>
<Box
display="flex"
alignItems="flex-start"
justifyContent="center"
height="100%"
width="100%"
>
<Avatar
displayName={profile?.displayName}
picture={profile?.picture}
/>
</Box>
</Grid>
<Grid item md={10} xl={11}>
<Typography variant="subtitle1">
<Box display="flex" alignItems="center">
<Tooltip title="Email">
<EmailIcon fontSize="inherit" />
</Tooltip>
{profile?.email && (
<Box ml={1} display="inline">
{profile.email}
</Box>
)}
</Box>
</Typography>
<Typography variant="subtitle1">
<Box display="flex" alignItems="center">
<Tooltip title="Member of">
<GroupIcon />
</Tooltip>
<Box ml={1} display="inline">
{groupNames.map((groupName, index) => (
<GroupLink
groupName={groupName}
index={index}
key={groupName}
entity={user}
/>
))}
</Box>
</Box>
</Typography>
</Grid>
</Grid>
</InfoCard>
);
};
@@ -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';
@@ -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';
+18
View File
@@ -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';
+17
View File
@@ -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';
+17
View File
@@ -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';
+22
View File
@@ -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();
});
});
+20
View File
@@ -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',
});
+17
View File
@@ -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';