@@ -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" />
|
||||
|
||||
@@ -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';
|
||||
@@ -15,3 +15,4 @@
|
||||
*/
|
||||
|
||||
export * from './Cards';
|
||||
export { MySquads } from './MySquads';
|
||||
|
||||
Reference in New Issue
Block a user