diff --git a/.changeset/itchy-candles-type.md b/.changeset/itchy-candles-type.md
new file mode 100644
index 0000000000..219f95de5b
--- /dev/null
+++ b/.changeset/itchy-candles-type.md
@@ -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';
+
+
+
+ //...
+ }>
+ {/* Global nav, not org-specific */}
+ //...
+
++
+ //...
+
+ Sidebar>
+
+```
+
+// app/src/components/home/HomePage.tsx
diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx
index 2441ace27b..0ff4507b52 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 { 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<{}>) => (
}>
{/* Global nav, not org-specific */}
+
diff --git a/plugins/org/api-report.md b/plugins/org/api-report.md
index 723cbd43b7..d725958176 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,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)
diff --git a/plugins/org/src/components/MySquads/MySquads.test.tsx b/plugins/org/src/components/MySquads/MySquads.test.tsx
new file mode 100644
index 0000000000..8424072d89
--- /dev/null
+++ b/plugins/org/src/components/MySquads/MySquads.test.tsx
@@ -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 = {
+ 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(
+
+
+ ,
+ );
+
+ 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 = {
+ 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(
+
+
+ ,
+ );
+ 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 = {
+ 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(
+
+
+ ,
+ );
+
+ expect(rendered.getByLabelText('My Squads')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/plugins/org/src/components/MySquads/MySquads.tsx b/plugins/org/src/components/MySquads/MySquads.tsx
new file mode 100644
index 0000000000..e1882708cb
--- /dev/null
+++ b/plugins/org/src/components/MySquads/MySquads.tsx
@@ -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 (
+
+ );
+ }
+
+ // Member of more than one group
+ // or not a member of any groups
+ return groups && groups.length > 1 ? (
+
+
+ {groups?.map(function groupsMap(group) {
+ return (
+
+ );
+ })}
+
+
+ ) : (
+ <>>
+ );
+};
diff --git a/plugins/org/src/components/MySquads/index.ts b/plugins/org/src/components/MySquads/index.ts
new file mode 100644
index 0000000000..d529a78592
--- /dev/null
+++ b/plugins/org/src/components/MySquads/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 { MySquads } from './MySquads';
diff --git a/plugins/org/src/components/index.ts b/plugins/org/src/components/index.ts
index f0c92616fa..6ec70343c5 100644
--- a/plugins/org/src/components/index.ts
+++ b/plugins/org/src/components/index.ts
@@ -15,3 +15,4 @@
*/
export * from './Cards';
+export { MySquads } from './MySquads';