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:
@@ -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",
|
||||
|
||||
@@ -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 }) => (
|
||||
</EntityPageLayout>
|
||||
);
|
||||
|
||||
const UserOverviewContent = ({ entity }: { entity: UserEntity }) => (
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<UserProfileCard entity={entity} variant="gridItem" />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<OwnershipCard entity={entity} variant="gridItem" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
const UserEntityPage = ({ entity }: { entity: Entity }) => (
|
||||
<EntityPageLayout>
|
||||
<EntityPageLayout.Content
|
||||
path="/*"
|
||||
title="Overview"
|
||||
element={<UserOverviewContent entity={entity as UserEntity} />}
|
||||
/>
|
||||
</EntityPageLayout>
|
||||
);
|
||||
|
||||
const GroupOverviewContent = ({ entity }: { entity: GroupEntity }) => (
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<GroupProfileCard entity={entity} variant="gridItem" />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<OwnershipCard entity={entity} variant="gridItem" />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<MembersListCard entity={entity} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
const GroupEntityPage = ({ entity }: { entity: Entity }) => (
|
||||
<EntityPageLayout>
|
||||
<EntityPageLayout.Content
|
||||
path="/*"
|
||||
title="Overview"
|
||||
element={<GroupOverviewContent entity={entity as GroupEntity} />}
|
||||
/>
|
||||
</EntityPageLayout>
|
||||
);
|
||||
|
||||
export const EntityPage = () => {
|
||||
const { entity } = useEntity();
|
||||
|
||||
@@ -331,6 +387,10 @@ export const EntityPage = () => {
|
||||
return <ComponentEntityPage entity={entity} />;
|
||||
case 'api':
|
||||
return <ApiEntityPage entity={entity} />;
|
||||
case 'group':
|
||||
return <GroupEntityPage entity={entity} />;
|
||||
case 'user':
|
||||
return <UserEntityPage entity={entity} />;
|
||||
default:
|
||||
return <DefaultEntityPage entity={entity} />;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -39,12 +39,12 @@ const EntityPageTitle = ({
|
||||
</Box>
|
||||
);
|
||||
|
||||
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' && (
|
||||
<>
|
||||
<HeaderLabel
|
||||
label="Owner"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
# Org Plugin for Backstage
|
||||
|
||||
## Features
|
||||
|
||||
- Show Group Page
|
||||
- Show User Profile
|
||||
@@ -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();
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user