Added MySquads

Signed-off-by: Andre Wanlin <awanlin@rapidrtc.com>
This commit is contained in:
Andre Wanlin
2022-03-05 17:30:43 -06:00
parent 9c01fe543d
commit d014fe2cb4
7 changed files with 341 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
---
'@backstage/plugin-org': patch
---
Introduced a new MySquads SidebarItem that links to one or more groups based on the logged in user's membership.
To use MySquads you'll need to add it to your `Root.tsx` like this:
```diff
// app/src/components/Root/Root.tsx
+ import { MySquads } from '@backstage/plugin-org';
+ import GroupIcon from '@material-ui/icons/People';
<SidebarPage>
<Sidebar>
//...
<SidebarGroup label="Menu" icon={<MenuIcon />}>
{/* Global nav, not org-specific */}
//...
<SidebarItem icon={HomeIcon} to="catalog" text="Home" />
+ <MySquads
+ singularTitle="My Squad"
+ pluralTitle="My Squads"
+ icon={GroupIcon}
+ />
//...
</SidebarGroup>
</ Sidebar>
</SidebarPage>
```
// app/src/components/home/HomePage.tsx
@@ -47,6 +47,8 @@ import {
SidebarScrollWrapper,
SidebarSpace,
} from '@backstage/core-components';
import { MySquads } from '@backstage/plugin-org';
import GroupIcon from '@material-ui/icons/People';
const useSidebarLogoStyles = makeStyles({
root: {
@@ -92,6 +94,11 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarGroup label="Menu" icon={<MenuIcon />}>
{/* Global nav, not org-specific */}
<SidebarItem icon={HomeIcon} to="catalog" text="Home" />
<MySquads
singularTitle="My Squad"
pluralTitle="My Squads"
icon={GroupIcon}
/>
<SidebarItem icon={ExtensionIcon} to="api-docs" text="APIs" />
<SidebarItem icon={LibraryBooks} to="docs" text="Docs" />
<SidebarItem icon={LayersIcon} to="explore" text="Explore" />
+14
View File
@@ -7,6 +7,7 @@
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { ExternalRouteRef } from '@backstage/core-plugin-api';
import { IconComponent } from '@backstage/core-plugin-api';
import { InfoCardVariants } from '@backstage/core-components';
// Warning: (ae-missing-release-tag) "EntityGroupProfileCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -63,6 +64,19 @@ export const MembersListCard: (_props: {
pageSize?: number;
}) => JSX.Element;
// Warning: (ae-missing-release-tag) "MySquads" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const MySquads: ({
singularTitle,
pluralTitle,
icon,
}: {
singularTitle: string;
pluralTitle: string;
icon: IconComponent;
}) => JSX.Element;
// Warning: (ae-missing-release-tag) "orgPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -0,0 +1,185 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import React from 'react';
import { MySquads } from '.';
import GroupIcon from '@material-ui/icons/People';
import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api';
import { CatalogApi } from '@backstage/catalog-client';
import { Entity } from '@backstage/catalog-model';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
describe('MySquads Test', () => {
describe('For guests or users with no groups', () => {
it('MySquads should be empty', async () => {
const identityApi: Partial<IdentityApi> = {
getBackstageIdentity: async () => ({
type: 'user',
userEntityRef: 'user:default/guest',
ownershipEntityRefs: ['user:default/guest'],
}),
};
const catalogApi: Partial<CatalogApi> = {
getEntities: () =>
Promise.resolve({
items: [] as Entity[],
}),
};
const rendered = await renderInTestApp(
<TestApiProvider
apis={[
[identityApiRef, identityApi],
[catalogApiRef, catalogApi],
]}
>
<MySquads
singularTitle="My Squad"
pluralTitle="My Squads"
icon={GroupIcon}
/>
</TestApiProvider>,
);
expect(rendered.container).toBeEmptyDOMElement();
});
});
describe('For users that are members of a single group', () => {
it('MySquads should display a single item that links to their group', async () => {
const identityApi: Partial<IdentityApi> = {
getBackstageIdentity: async () => ({
type: 'user',
userEntityRef: 'user:default/nigel.manning',
ownershipEntityRefs: ['user:default/nigel.manning'],
}),
};
const catalogApi: Partial<CatalogApi> = {
getEntities: () =>
Promise.resolve({
items: [
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: {
name: 'team-a',
title: 'Team A',
namespace: 'default',
},
spec: {
type: 'team',
children: [],
},
},
] as Entity[],
}),
};
const rendered = await renderInTestApp(
<TestApiProvider
apis={[
[identityApiRef, identityApi],
[catalogApiRef, catalogApi],
]}
>
<MySquads
singularTitle="My Squad"
pluralTitle="My Squads"
icon={GroupIcon}
/>
</TestApiProvider>,
);
expect(rendered.getByLabelText('My Squad')).toBeInTheDocument();
expect(rendered.getByLabelText('My Squad')).toHaveAttribute(
'href',
'/catalog/default/Group/team-a',
);
});
});
describe('For users that are members of multiple groups', () => {
it('MySquads should display a sub-menu with all their groups and a link to each group', async () => {
const identityApi: Partial<IdentityApi> = {
getBackstageIdentity: async () => ({
type: 'user',
userEntityRef: 'user:default/nigel.manning',
ownershipEntityRefs: ['user:default/nigel.manning'],
}),
};
const catalogApi: Partial<CatalogApi> = {
getEntities: () =>
Promise.resolve({
items: [
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: {
name: 'team-a',
title: 'Team A',
namespace: 'default',
},
spec: {
type: 'team',
children: [],
},
},
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: {
name: 'team-b',
title: 'Team B',
namespace: 'default',
},
spec: {
type: 'team',
children: [],
},
},
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: {
name: 'team-c',
title: 'Team C',
namespace: 'default',
},
spec: {
type: 'team',
children: [],
},
},
] as Entity[],
}),
};
const rendered = await renderInTestApp(
<TestApiProvider
apis={[
[identityApiRef, identityApi],
[catalogApiRef, catalogApi],
]}
>
<MySquads
singularTitle="My Squad"
pluralTitle="My Squads"
icon={GroupIcon}
/>
</TestApiProvider>,
);
expect(rendered.getByLabelText('My Squads')).toBeInTheDocument();
});
});
});
@@ -0,0 +1,85 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 {
SidebarItem,
SidebarSubmenu,
SidebarSubmenuItem,
} from '@backstage/core-components';
import {
IconComponent,
identityApiRef,
useApi,
} from '@backstage/core-plugin-api';
import useAsync from 'react-use/lib/useAsync';
import { catalogApiRef, CatalogApi } from '@backstage/plugin-catalog-react';
export const MySquads = ({
singularTitle,
pluralTitle,
icon,
}: {
singularTitle: string;
pluralTitle: string;
icon: IconComponent;
}) => {
const identityApi = useApi(identityApiRef);
const catalogApi: CatalogApi = useApi(catalogApiRef);
const { value: groups } = useAsync(async () => {
const profile = await identityApi.getBackstageIdentity();
const response = await catalogApi.getEntities({
filter: [{ kind: 'group', 'relations.hasMember': profile.userEntityRef }],
fields: ['metadata', 'kind'],
});
return response.items;
}, []);
if (groups && groups.length === 1) {
// Only member of one group
const group = groups[0];
return (
<SidebarItem
text={singularTitle}
to={`/catalog/${group.metadata.namespace}/${group.kind}/${group.metadata.name}`}
icon={icon}
/>
);
}
// Member of more than one group
// or not a member of any groups
return groups && groups.length > 1 ? (
<SidebarItem icon={icon} to="catalog" text={pluralTitle}>
<SidebarSubmenu title={pluralTitle}>
{groups?.map(function groupsMap(group) {
return (
<SidebarSubmenuItem
title={group.metadata.title || group.metadata.name}
to={`/catalog/${group.metadata.namespace}/${group.kind}/${group.metadata.name}`}
icon={icon}
key={group.metadata.name}
/>
);
})}
</SidebarSubmenu>
</SidebarItem>
) : (
<></>
);
};
@@ -0,0 +1,17 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 { MySquads } from './MySquads';
+1
View File
@@ -15,3 +15,4 @@
*/
export * from './Cards';
export { MySquads } from './MySquads';