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';