Merge pull request #10026 from awanlin/topic/add-mysquads-sidebar-item

Added MySquads
This commit is contained in:
Johan Haals
2022-03-31 10:19:07 +02:00
committed by GitHub
13 changed files with 425 additions and 0 deletions
+52
View File
@@ -4,3 +4,55 @@
- Show Group Page
- Show User Profile
- Quick access to Groups
### Group Page
Here's an example of what the Group Page looks like:
![Group Page example](./docs/group-page-example.png)
### User Profile
Here's an example of what the User Profile looks like:
![Group Page example](./docs/user-profile-example.png)
### MyGroupsSidebarItem
The MyGroupsSidebarItem provides quick access to the group(s) the logged in user is a member of directly in the sidebar.
To use the MyGroupsSidebarItem you'll need to add it to your `Root.tsx` - found at `packages\app\src\components\Root` - like this:
```diff
+ import { MyGroupsSidebarItem } 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" />
+ <MyGroupsSidebarItem
+ singularTitle="My Squad"
+ pluralTitle="My Squads"
+ icon={GroupIcon}
+ />
//...
</SidebarGroup>
</ Sidebar>
</SidebarPage>
```
Once added MyGroupsSidebarItem will work in three ways:
1. The user is not logged in or the logged in user is not a member of any group: the MyGroupsSidebarItem will not display anything in the sidebar
2. The user is logged in and a member of only one group: the MyGroupsSidebarItem will display a single item in the sidebar like this:
![MyGroupsSidebarItem single example](./docs/mygroupssidebaritem-single.png)
3. The user is logged in and a member of more than one group: the MyGroupsSidebarItem will display a single items with a sub-menu with all the related groups like this:
![MyGroupsSidebarItem multiple example](./docs/mygroupssidebaritem-multiple.png)
+12
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,17 @@ export const MembersListCard: (_props: {
pageSize?: number;
}) => JSX.Element;
// @public
export const MyGroupsSidebarItem: ({
singularTitle,
pluralTitle,
icon,
}: {
singularTitle: string;
pluralTitle: string;
icon: IconComponent;
}) => JSX.Element | null;
// 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)
Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

+5
View File
@@ -13,6 +13,11 @@
"backstage": {
"role": "frontend-plugin"
},
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/org"
},
"scripts": {
"build": "backstage-cli package build",
"start": "backstage-cli package start",
@@ -0,0 +1,201 @@
/*
* 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 { MyGroupsSidebarItem } from './MyGroupsSidebarItem';
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, entityRouteRef } from '@backstage/plugin-catalog-react';
describe('MyGroupsSidebarItem Test', () => {
describe('For guests or users with no groups', () => {
it('MyGroupsSidebarItem 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],
]}
>
<MyGroupsSidebarItem
singularTitle="My Squad"
pluralTitle="My Squads"
icon={GroupIcon}
/>
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
},
);
expect(
rendered.getByText('Mounted at /catalog/:namespace/:kind/:name'),
).toBeInTheDocument();
});
});
describe('For users that are members of a single group', () => {
it('MyGroupsSidebarItem 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],
]}
>
<MyGroupsSidebarItem
singularTitle="My Squad"
pluralTitle="My Squads"
icon={GroupIcon}
/>
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
},
);
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('MyGroupsSidebarItem 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],
]}
>
<MyGroupsSidebarItem
singularTitle="My Squad"
pluralTitle="My Squads"
icon={GroupIcon}
/>
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
},
);
expect(rendered.getByLabelText('My Squads')).toBeInTheDocument();
});
});
});
@@ -0,0 +1,100 @@
/*
* 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,
useRouteRef,
} from '@backstage/core-plugin-api';
import useAsync from 'react-use/lib/useAsync';
import {
catalogApiRef,
CatalogApi,
entityRouteRef,
} from '@backstage/plugin-catalog-react';
import { getCompoundEntityRef } from '@backstage/catalog-model';
/**
* MyGroupsSidebarItem can be added to your sidebar providing quick access to groups the logged in user is a member of
*
* @public
*/
export const MyGroupsSidebarItem = ({
singularTitle,
pluralTitle,
icon,
}: {
singularTitle: string;
pluralTitle: string;
icon: IconComponent;
}) => {
const identityApi = useApi(identityApiRef);
const catalogApi: CatalogApi = useApi(catalogApiRef);
const catalogEntityRoute = useRouteRef(entityRouteRef);
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;
}, []);
// Not a member of any groups
if (!groups?.length) {
return null;
}
// Only member of one group
if (groups.length === 1) {
const group = groups[0];
return (
<SidebarItem
text={singularTitle}
to={catalogEntityRoute(getCompoundEntityRef(group))}
icon={icon}
/>
);
}
// Member of more than one group
return (
<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={catalogEntityRoute(getCompoundEntityRef(group))}
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 { MyGroupsSidebarItem } from './MyGroupsSidebarItem';
+1
View File
@@ -15,3 +15,4 @@
*/
export * from './Cards';
export { MyGroupsSidebarItem } from './MyGroupsSidebarItem';