diff --git a/.changeset/itchy-candles-type.md b/.changeset/itchy-candles-type.md
new file mode 100644
index 0000000000..3507ecd867
--- /dev/null
+++ b/.changeset/itchy-candles-type.md
@@ -0,0 +1,30 @@
+---
+'@backstage/plugin-org': patch
+---
+
+Introduced a new MyGroupsSidebarItem SidebarItem that links to one or more groups based on the logged in user's membership.
+
+To use MyGroupsSidebarItem you'll need to add it to your `Root.tsx` like this:
+
+```diff
+// app/src/components/Root/Root.tsx
++ import { MyGroupsSidebarItem } from '@backstage/plugin-org';
++ import GroupIcon from '@material-ui/icons/People';
+
+
+
+ //...
+ }>
+ {/* Global nav, not org-specific */}
+ //...
+
++
+ //...
+
+ Sidebar>
+
+```
diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx
index 2441ace27b..e3c36c9329 100644
--- a/packages/app/src/components/Root/Root.tsx
+++ b/packages/app/src/components/Root/Root.tsx
@@ -47,6 +47,8 @@ import {
SidebarScrollWrapper,
SidebarSpace,
} from '@backstage/core-components';
+import { MyGroupsSidebarItem } from '@backstage/plugin-org';
+import GroupIcon from '@material-ui/icons/People';
const useSidebarLogoStyles = makeStyles({
root: {
@@ -92,6 +94,11 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
}>
{/* Global nav, not org-specific */}
+
diff --git a/plugins/org/README.md b/plugins/org/README.md
index a117f36722..0c5452aa62 100644
--- a/plugins/org/README.md
+++ b/plugins/org/README.md
@@ -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:
+
+
+
+### User Profile
+
+Here's an example of what the User Profile looks like:
+
+
+
+### 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';
+
+
+
+ //...
+ }>
+ {/* Global nav, not org-specific */}
+ //...
+
++
+ //...
+
+ Sidebar>
+
+```
+
+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:
+
+ 
+
+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:
+
+ 
diff --git a/plugins/org/api-report.md b/plugins/org/api-report.md
index 723cbd43b7..1050b2aaff 100644
--- a/plugins/org/api-report.md
+++ b/plugins/org/api-report.md
@@ -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)
diff --git a/plugins/org/docs/group-page-example.png b/plugins/org/docs/group-page-example.png
new file mode 100644
index 0000000000..f9770bd494
Binary files /dev/null and b/plugins/org/docs/group-page-example.png differ
diff --git a/plugins/org/docs/mygroupssidebaritem-multiple.png b/plugins/org/docs/mygroupssidebaritem-multiple.png
new file mode 100644
index 0000000000..90cf709e23
Binary files /dev/null and b/plugins/org/docs/mygroupssidebaritem-multiple.png differ
diff --git a/plugins/org/docs/mygroupssidebaritem-single.png b/plugins/org/docs/mygroupssidebaritem-single.png
new file mode 100644
index 0000000000..6c2ed4a768
Binary files /dev/null and b/plugins/org/docs/mygroupssidebaritem-single.png differ
diff --git a/plugins/org/docs/user-profile-example.png b/plugins/org/docs/user-profile-example.png
new file mode 100644
index 0000000000..7679a03cf4
Binary files /dev/null and b/plugins/org/docs/user-profile-example.png differ
diff --git a/plugins/org/package.json b/plugins/org/package.json
index eeb8d9656a..034e32177f 100644
--- a/plugins/org/package.json
+++ b/plugins/org/package.json
@@ -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",
diff --git a/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.test.tsx b/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.test.tsx
new file mode 100644
index 0000000000..794dd23747
--- /dev/null
+++ b/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.test.tsx
@@ -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 = {
+ getBackstageIdentity: async () => ({
+ type: 'user',
+ userEntityRef: 'user:default/guest',
+ ownershipEntityRefs: ['user:default/guest'],
+ }),
+ };
+ const catalogApi: Partial = {
+ getEntities: () =>
+ Promise.resolve({
+ items: [] as Entity[],
+ }),
+ };
+ const rendered = await renderInTestApp(
+
+
+ ,
+ {
+ 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 = {
+ getBackstageIdentity: async () => ({
+ type: 'user',
+ userEntityRef: 'user:default/nigel.manning',
+ ownershipEntityRefs: ['user:default/nigel.manning'],
+ }),
+ };
+ const catalogApi: Partial = {
+ 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(
+
+
+ ,
+ {
+ 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 = {
+ getBackstageIdentity: async () => ({
+ type: 'user',
+ userEntityRef: 'user:default/nigel.manning',
+ ownershipEntityRefs: ['user:default/nigel.manning'],
+ }),
+ };
+ const catalogApi: Partial = {
+ 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(
+
+
+ ,
+ {
+ mountedRoutes: {
+ '/catalog/:namespace/:kind/:name': entityRouteRef,
+ },
+ },
+ );
+
+ expect(rendered.getByLabelText('My Squads')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.tsx b/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.tsx
new file mode 100644
index 0000000000..22f3e76848
--- /dev/null
+++ b/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.tsx
@@ -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 (
+
+ );
+ }
+
+ // Member of more than one group
+ return (
+
+
+ {groups?.map(function groupsMap(group) {
+ return (
+
+ );
+ })}
+
+
+ );
+};
diff --git a/plugins/org/src/components/MyGroupsSidebarItem/index.ts b/plugins/org/src/components/MyGroupsSidebarItem/index.ts
new file mode 100644
index 0000000000..088e768980
--- /dev/null
+++ b/plugins/org/src/components/MyGroupsSidebarItem/index.ts
@@ -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';
diff --git a/plugins/org/src/components/index.ts b/plugins/org/src/components/index.ts
index f0c92616fa..e8bb006171 100644
--- a/plugins/org/src/components/index.ts
+++ b/plugins/org/src/components/index.ts
@@ -15,3 +15,4 @@
*/
export * from './Cards';
+export { MyGroupsSidebarItem } from './MyGroupsSidebarItem';