diff --git a/.changeset/moody-worms-rhyme.md b/.changeset/moody-worms-rhyme.md
new file mode 100644
index 0000000000..39517a6416
--- /dev/null
+++ b/.changeset/moody-worms-rhyme.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-github-pull-requests-board': minor
+---
+
+Add Github Pull Requests board plugin
diff --git a/microsite/data/plugins/github-pull-requests-board.yaml b/microsite/data/plugins/github-pull-requests-board.yaml
new file mode 100644
index 0000000000..fa710f5fc8
--- /dev/null
+++ b/microsite/data/plugins/github-pull-requests-board.yaml
@@ -0,0 +1,9 @@
+---
+title: Github Pull Requests Board
+author: DAZN
+authorUrl: https://engineering.dazn.com
+category: Source Control Mgmt
+description: View all open GitHub pull requests owned by your team in Backstage.
+documentation: https://github.com/backstage/backstage/tree/master/plugins/github-pull-requests-board
+iconUrl: img/github-pull-requests-board-logo.svg
+npmPackageName: '@backstage/plugin-github-pull-requests-board'
diff --git a/microsite/static/img/github-pull-requests-board-logo.svg b/microsite/static/img/github-pull-requests-board-logo.svg
new file mode 100644
index 0000000000..0114b50d08
--- /dev/null
+++ b/microsite/static/img/github-pull-requests-board-logo.svg
@@ -0,0 +1,30 @@
+
diff --git a/plugins/github-pull-requests-board/.eslintrc.js b/plugins/github-pull-requests-board/.eslintrc.js
new file mode 100644
index 0000000000..13573efa9c
--- /dev/null
+++ b/plugins/github-pull-requests-board/.eslintrc.js
@@ -0,0 +1,3 @@
+module.exports = {
+ extends: [require.resolve('@backstage/cli/config/eslint')],
+};
diff --git a/plugins/github-pull-requests-board/README.md b/plugins/github-pull-requests-board/README.md
new file mode 100644
index 0000000000..c8b14fe581
--- /dev/null
+++ b/plugins/github-pull-requests-board/README.md
@@ -0,0 +1,79 @@
+# GitHub Pull Requests Board Plugin
+
+The GitHub Pull Requests Board Plugin helps to visualise all **Open Pull Requests** related to the owned team repository.
+
+
+
+It will help you and your team stay on top of open pull requests, hopefully reducing the time from open to merged. It's particularly useful when your team deals with many repositories.
+
+## Prerequisites
+
+- [GitHub Authentication Provider](https://backstage.io/docs/auth/github/provider)
+
+## Getting started
+
+Install the plugin by running the following command **from your Backstage root directory**
+
+`yarn --cwd packages/app add @backstage/plugin-github-pull-requests-board`
+
+The plugin exports the **EntityTeamPullRequestsCard** component which can be added to the Overview page of the team at `backstage/packages/app/src/components/catalog/EntityPage.tsx`
+
+```javascript
+import { EntityTeamPullRequestsCard } from '@backstage/plugin-github-pull-requests-board';
+
+const groupPage = (
+
+
+
+ {entityWarningContent}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+```
+
+Or you can also import the **EntityTeamPullRequestsContent** component which can be used to add a new tab under the group page at `backstage/packages/app/src/components/catalog/EntityPage.tsx`
+
+```javascript
+import { EntityTeamPullRequestsContent } from '@backstage/plugin-github-pull-requests-board';
+
+const groupPage = (
+
+
+
+ {entityWarningContent}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ;
+)
+```
diff --git a/plugins/github-pull-requests-board/api-report.md b/plugins/github-pull-requests-board/api-report.md
new file mode 100644
index 0000000000..33814c76b4
--- /dev/null
+++ b/plugins/github-pull-requests-board/api-report.md
@@ -0,0 +1,21 @@
+## API Report File for "@backstage/plugin-github-pull-requests-board"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+///
+
+import { FunctionComponent } from 'react';
+
+// Warning: (ae-missing-release-tag) "EntityTeamPullRequestsCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public (undocumented)
+export const EntityTeamPullRequestsCard: FunctionComponent<{}>;
+
+// Warning: (ae-missing-release-tag) "EntityTeamPullRequestsContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public (undocumented)
+export const EntityTeamPullRequestsContent: FunctionComponent<{}>;
+
+// (No @packageDocumentation comment for this package)
+```
diff --git a/plugins/github-pull-requests-board/docs/pull-requests-board.png b/plugins/github-pull-requests-board/docs/pull-requests-board.png
new file mode 100644
index 0000000000..9b7daa981a
Binary files /dev/null and b/plugins/github-pull-requests-board/docs/pull-requests-board.png differ
diff --git a/plugins/github-pull-requests-board/package.json b/plugins/github-pull-requests-board/package.json
new file mode 100644
index 0000000000..402e21f94e
--- /dev/null
+++ b/plugins/github-pull-requests-board/package.json
@@ -0,0 +1,71 @@
+{
+ "name": "@backstage/plugin-github-pull-requests-board",
+ "description": "A Backstage plugin that allows you to see all open Pull Requests for all the repositories owned by your team",
+ "version": "0.0.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.esm.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "frontend-plugin"
+ },
+ "homepage": "https://backstage.io",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/backstage/backstage",
+ "directory": "plugins/github-pull-requests-board"
+ },
+ "keywords": [
+ "backstage",
+ "github",
+ "pull requests"
+ ],
+ "scripts": {
+ "build": "backstage-cli package build",
+ "start": "backstage-cli package start",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "diff": "backstage-cli plugin:diff",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack",
+ "clean": "backstage-cli package clean"
+ },
+ "dependencies": {
+ "@backstage/catalog-model": "^1.0.2-next.0",
+ "@backstage/core-components": "^0.9.5-next.0",
+ "@backstage/core-plugin-api": "^1.0.2",
+ "@backstage/integration": "^1.2.1-next.0",
+ "@backstage/plugin-catalog-react": "^1.1.1-next.0",
+ "@backstage/theme": "^0.2.15",
+ "@material-ui/core": "^4.12.2",
+ "@material-ui/icons": "^4.9.1",
+ "@material-ui/lab": "4.0.0-alpha.57",
+ "@octokit/rest": "^18.12.0",
+ "moment": "^2.29.1",
+ "react-use": "^17.2.4"
+ },
+ "devDependencies": {
+ "@backstage/cli": "^0.17.2-next.0",
+ "@backstage/dev-utils": "^1.0.3-next.0",
+ "@backstage/test-utils": "^1.1.0",
+ "@testing-library/jest-dom": "^5.10.1",
+ "@testing-library/react": "^12.1.3",
+ "@testing-library/user-event": "^14.0.0",
+ "@types/jest": "^26.0.7",
+ "@types/node": "^16.11.26",
+ "cross-fetch": "^3.1.5",
+ "msw": "^0.35.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.13.1 || ^17.0.0",
+ "react": "^16.13.1 || ^17.0.0",
+ "react-dom": "^16.13.1 || ^17.0.0"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/plugins/github-pull-requests-board/src/api/useGetPullRequestDetails.ts b/plugins/github-pull-requests-board/src/api/useGetPullRequestDetails.ts
new file mode 100644
index 0000000000..d44c36b54d
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/api/useGetPullRequestDetails.ts
@@ -0,0 +1,91 @@
+/*
+ * 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 { GraphQlPullRequest, PullRequest } from '../utils/types';
+import { useOctokitGraphQl } from './useOctokitGraphQl';
+
+export const useGetPullRequestDetails = () => {
+ const graphql = useOctokitGraphQl>();
+
+ const fn = React.useRef(
+ async (repo: string, number: number): Promise => {
+ const [organisation, repositoryName] = repo.split('/');
+
+ const { repository } = await graphql(
+ `
+ query ($name: String!, $owner: String!, $pull_number: Int!) {
+ repository(name: $name, owner: $owner) {
+ pullRequest(number: $pull_number) {
+ id
+ repository {
+ name
+ }
+ title
+ url
+ createdAt
+ lastEditedAt
+ latestReviews(first: 10) {
+ nodes {
+ author {
+ login
+ avatarUrl
+ ... on User {
+ id
+ email
+ name
+ login
+ }
+ }
+ state
+ }
+ }
+ mergeable
+ state
+ reviewDecision
+ isDraft
+ createdAt
+ author {
+ ... on User {
+ id
+ email
+ avatarUrl
+ name
+ login
+ }
+ ... on Bot {
+ id
+ avatarUrl
+ login
+ }
+ }
+ }
+ }
+ }
+ `,
+ {
+ name: repositoryName,
+ owner: organisation,
+ pull_number: number,
+ },
+ );
+
+ return repository.pullRequest;
+ },
+ );
+
+ return fn.current;
+};
diff --git a/plugins/github-pull-requests-board/src/api/useGetPullRequestsFromRepository.ts b/plugins/github-pull-requests-board/src/api/useGetPullRequestsFromRepository.ts
new file mode 100644
index 0000000000..bd5df89833
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/api/useGetPullRequestsFromRepository.ts
@@ -0,0 +1,54 @@
+/*
+ * 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 { GraphQlPullRequests, PullRequestsNumber } from '../utils/types';
+import { useOctokitGraphQl } from './useOctokitGraphQl';
+
+export const useGetPullRequestsFromRepository = () => {
+ const graphql =
+ useOctokitGraphQl>();
+
+ const fn = React.useRef(
+ async (repo: string): Promise => {
+ const [organisation, repositoryName] = repo.split('/');
+
+ const { repository } = await graphql(
+ `
+ query ($name: String!, $owner: String!) {
+ repository(name: $name, owner: $owner) {
+ pullRequests(states: OPEN, first: 10) {
+ edges {
+ node {
+ number
+ }
+ }
+ }
+ }
+ }
+ `,
+ {
+ name: repositoryName,
+ owner: organisation,
+ },
+ );
+
+ return repository.pullRequests.edges;
+ },
+ );
+
+ return fn.current;
+};
diff --git a/plugins/github-pull-requests-board/src/api/useOctokitGraphQl.ts b/plugins/github-pull-requests-board/src/api/useOctokitGraphQl.ts
new file mode 100644
index 0000000000..a864f9950e
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/api/useOctokitGraphQl.ts
@@ -0,0 +1,47 @@
+/*
+ * 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 { Octokit } from '@octokit/rest';
+import {
+ useApi,
+ githubAuthApiRef,
+ configApiRef,
+} from '@backstage/core-plugin-api';
+import { readGitHubIntegrationConfigs } from '@backstage/integration';
+
+let octokit: any;
+
+export const useOctokitGraphQl = () => {
+ const auth = useApi(githubAuthApiRef);
+ const config = useApi(configApiRef);
+
+ const baseUrl = readGitHubIntegrationConfigs(
+ config.getOptionalConfigArray('integrations.github') ?? [],
+ )[0].apiBaseUrl;
+
+ return (path: string, options?: any): Promise =>
+ auth
+ .getAccessToken(['repo'])
+ .then((token: string) => {
+ if (!octokit) {
+ octokit = new Octokit({ auth: token, ...(baseUrl && { baseUrl }) });
+ }
+
+ return octokit;
+ })
+ .then(octokitInstance => {
+ return octokitInstance.graphql(path, options);
+ });
+};
diff --git a/plugins/github-pull-requests-board/src/components/Card/Card.tsx b/plugins/github-pull-requests-board/src/components/Card/Card.tsx
new file mode 100644
index 0000000000..56e8c6a077
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/components/Card/Card.tsx
@@ -0,0 +1,63 @@
+/*
+ * 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, { PropsWithChildren, FunctionComponent } from 'react';
+import { Box, Paper, CardActionArea } from '@material-ui/core';
+import CardHeader from './CardHeader';
+
+type Props = {
+ title: string;
+ createdAt: string;
+ updatedAt?: string;
+ prUrl: string;
+ authorName: string;
+ authorAvatar?: string;
+ repositoryName: string;
+};
+
+const Card: FunctionComponent = (props: PropsWithChildren) => {
+ const {
+ title,
+ createdAt,
+ updatedAt,
+ prUrl,
+ authorName,
+ authorAvatar,
+ repositoryName,
+ children,
+ } = props;
+
+ return (
+
+
+
+
+
+ {children}
+
+
+
+
+ );
+};
+
+export default Card;
diff --git a/plugins/github-pull-requests-board/src/components/Card/CardHeader.tsx b/plugins/github-pull-requests-board/src/components/Card/CardHeader.tsx
new file mode 100644
index 0000000000..831423a464
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/components/Card/CardHeader.tsx
@@ -0,0 +1,65 @@
+/*
+ * 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, { FunctionComponent } from 'react';
+import { Typography, Box } from '@material-ui/core';
+import { getElapsedTime } from '../../utils/functions';
+import { UserHeader } from '../UserHeader';
+
+type Props = {
+ title: string;
+ createdAt: string;
+ updatedAt?: string;
+ authorName: string;
+ authorAvatar?: string;
+ repositoryName: string;
+};
+
+const CardHeader: FunctionComponent = (props: Props) => {
+ const {
+ title,
+ createdAt,
+ updatedAt,
+ authorName,
+ authorAvatar,
+ repositoryName,
+ } = props;
+
+ return (
+ <>
+
+
+ {repositoryName}
+
+
+
+
+ {title}
+
+
+
+ Created at: {getElapsedTime(createdAt)}
+
+ {updatedAt && (
+
+ Last update: {getElapsedTime(updatedAt)}
+
+ )}
+
+ >
+ );
+};
+
+export default CardHeader;
diff --git a/plugins/github-pull-requests-board/src/components/Card/index.ts b/plugins/github-pull-requests-board/src/components/Card/index.ts
new file mode 100644
index 0000000000..527bd23115
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/components/Card/index.ts
@@ -0,0 +1,16 @@
+/*
+ * 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 { default as Card } from './Card';
diff --git a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/EntityTeamPullRequestsCard.tsx b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/EntityTeamPullRequestsCard.tsx
new file mode 100644
index 0000000000..b19651536a
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/EntityTeamPullRequestsCard.tsx
@@ -0,0 +1,113 @@
+/*
+ * 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, { FunctionComponent, useState } from 'react';
+import { Grid, Typography } from '@material-ui/core';
+import FullscreenIcon from '@material-ui/icons/Fullscreen';
+
+import { Progress, InfoCard } from '@backstage/core-components';
+
+import { InfoCardHeader } from '../InfoCardHeader';
+import { PullRequestBoardOptions } from '../PullRequestBoardOptions';
+import { Wrapper } from '../Wrapper';
+import { PullRequestCard } from '../PullRequestCard';
+import { usePullRequestsByTeam } from '../../hooks/usePullRequestsByTeam';
+import { PRCardFormating } from '../../utils/types';
+import { DraftPrIcon } from '../icons/DraftPr';
+import { useUserRepositories } from '../../hooks/useUserRepositories';
+
+const EntityTeamPullRequestsCard: FunctionComponent = () => {
+ const [infoCardFormat, setInfoCardFormat] = useState([]);
+ const { repositories } = useUserRepositories();
+ const { loading, pullRequests, refreshPullRequests } =
+ usePullRequestsByTeam(repositories);
+
+ const header = (
+
+ setInfoCardFormat(newFormats)}
+ value={infoCardFormat}
+ options={[
+ {
+ icon: ,
+ value: 'draft',
+ ariaLabel: 'Show draft PRs',
+ },
+ {
+ icon: ,
+ value: 'fullscreen',
+ ariaLabel: 'Info card is set to fullscreen',
+ },
+ ]}
+ />
+
+ );
+
+ const getContent = () => {
+ if (loading) {
+ return ;
+ }
+
+ return (
+
+ {pullRequests.length ? (
+ pullRequests.map(({ title: columnTitle, content }) => (
+
+ {columnTitle}
+ {content.map(
+ (
+ {
+ id,
+ title,
+ createdAt,
+ lastEditedAt,
+ author,
+ url,
+ latestReviews,
+ repository,
+ isDraft,
+ },
+ index,
+ ) =>
+ infoCardFormat.includes('draft') === isDraft && (
+
+ ),
+ )}
+
+ ))
+ ) : (
+ No pull requests found
+ )}
+
+ );
+ };
+
+ return {getContent()};
+};
+
+export default EntityTeamPullRequestsCard;
diff --git a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/index.ts b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/index.ts
new file mode 100644
index 0000000000..bac16f1424
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsCard/index.ts
@@ -0,0 +1,16 @@
+/*
+ * 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 { default as EntityTeamPullRequestsCard } from './EntityTeamPullRequestsCard';
diff --git a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/EntityTeamPullRequestsContent.tsx b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/EntityTeamPullRequestsContent.tsx
new file mode 100644
index 0000000000..33980e2364
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/EntityTeamPullRequestsContent.tsx
@@ -0,0 +1,103 @@
+/*
+ * 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, { FunctionComponent, useState } from 'react';
+import { Grid, Typography } from '@material-ui/core';
+import { Progress, InfoCard } from '@backstage/core-components';
+
+import { InfoCardHeader } from '../InfoCardHeader';
+import { PullRequestBoardOptions } from '../PullRequestBoardOptions';
+import { Wrapper } from '../Wrapper';
+import { PullRequestCard } from '../PullRequestCard';
+import { usePullRequestsByTeam } from '../../hooks/usePullRequestsByTeam';
+import { PRCardFormating } from '../../utils/types';
+import { DraftPrIcon } from '../icons/DraftPr';
+import { useUserRepositories } from '../../hooks/useUserRepositories';
+
+const EntityTeamPullRequestsContent: FunctionComponent = () => {
+ const [infoCardFormat, setInfoCardFormat] = useState([]);
+ const { repositories } = useUserRepositories();
+ const { loading, pullRequests, refreshPullRequests } =
+ usePullRequestsByTeam(repositories);
+
+ const header = (
+
+ setInfoCardFormat(newFormats)}
+ value={infoCardFormat}
+ options={[
+ {
+ icon: ,
+ value: 'draft',
+ ariaLabel: 'Show draft PRs',
+ },
+ ]}
+ />
+
+ );
+
+ const getContent = () => {
+ if (loading) {
+ return ;
+ }
+
+ return (
+
+ {pullRequests.length ? (
+ pullRequests.map(({ title: columnTitle, content }) => (
+
+ {columnTitle}
+ {content.map(
+ (
+ {
+ id,
+ title,
+ createdAt,
+ lastEditedAt,
+ author,
+ url,
+ latestReviews,
+ repository,
+ isDraft,
+ },
+ index,
+ ) =>
+ infoCardFormat.includes('draft') === isDraft && (
+
+ ),
+ )}
+
+ ))
+ ) : (
+ No pull requests found
+ )}
+
+ );
+ };
+
+ return {getContent()};
+};
+
+export default EntityTeamPullRequestsContent;
diff --git a/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/index.ts b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/index.ts
new file mode 100644
index 0000000000..c2be57464c
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/components/EntityTeamPullRequestsContent/index.ts
@@ -0,0 +1,16 @@
+/*
+ * 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 { default as EntityTeamPullRequestsContent } from './EntityTeamPullRequestsContent';
diff --git a/plugins/github-pull-requests-board/src/components/InfoCardHeader/InfoCardHeader.tsx b/plugins/github-pull-requests-board/src/components/InfoCardHeader/InfoCardHeader.tsx
new file mode 100644
index 0000000000..8c126bcaa4
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/components/InfoCardHeader/InfoCardHeader.tsx
@@ -0,0 +1,42 @@
+/*
+ * 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, { PropsWithChildren, FunctionComponent } from 'react';
+import { Typography, Box, IconButton } from '@material-ui/core';
+import RefreshIcon from '@material-ui/icons/Refresh';
+
+type Props = {
+ onRefresh: () => void;
+};
+
+const InfoCardHeader: FunctionComponent = (
+ props: PropsWithChildren,
+) => {
+ const { children, onRefresh } = props;
+
+ return (
+
+
+ Open pull requests
+
+
+
+
+ {children}
+
+ );
+};
+
+export default InfoCardHeader;
diff --git a/plugins/github-pull-requests-board/src/components/InfoCardHeader/index.ts b/plugins/github-pull-requests-board/src/components/InfoCardHeader/index.ts
new file mode 100644
index 0000000000..393a04af55
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/components/InfoCardHeader/index.ts
@@ -0,0 +1,16 @@
+/*
+ * 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 { default as InfoCardHeader } from './InfoCardHeader';
diff --git a/plugins/github-pull-requests-board/src/components/PullRequestBoardOptions/PullRequestBoardOptions.tsx b/plugins/github-pull-requests-board/src/components/PullRequestBoardOptions/PullRequestBoardOptions.tsx
new file mode 100644
index 0000000000..6938210e68
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/components/PullRequestBoardOptions/PullRequestBoardOptions.tsx
@@ -0,0 +1,59 @@
+/*
+ * 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, { ReactNode, FunctionComponent } from 'react';
+import { ToggleButton, ToggleButtonGroup } from '@material-ui/lab';
+import { Tooltip, Box } from '@material-ui/core';
+import { PRCardFormating } from '../../utils/types';
+
+type Option = {
+ icon: ReactNode;
+ value: string;
+ ariaLabel: string;
+};
+
+type Props = {
+ value: string[];
+ onClickOption: (selectedOptions: PRCardFormating[]) => void;
+ options: Option[];
+};
+
+const PullRequestBoardOptions: FunctionComponent = (props: Props) => {
+ const { value, onClickOption, options } = props;
+ return (
+ onClickOption(selectedOptions)}
+ aria-label="Pull Request board settings"
+ >
+ {options.map(({ icon, value: toggleValue, ariaLabel }, index) => (
+
+
+
+ {icon}
+
+
+
+ ))}
+
+ );
+};
+
+export default PullRequestBoardOptions;
diff --git a/plugins/github-pull-requests-board/src/components/PullRequestBoardOptions/index.ts b/plugins/github-pull-requests-board/src/components/PullRequestBoardOptions/index.ts
new file mode 100644
index 0000000000..793ae8d0b7
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/components/PullRequestBoardOptions/index.ts
@@ -0,0 +1,16 @@
+/*
+ * 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 { default as PullRequestBoardOptions } from './PullRequestBoardOptions';
diff --git a/plugins/github-pull-requests-board/src/components/PullRequestCard/PullRequestCard.tsx b/plugins/github-pull-requests-board/src/components/PullRequestCard/PullRequestCard.tsx
new file mode 100644
index 0000000000..7e1596f5c2
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/components/PullRequestCard/PullRequestCard.tsx
@@ -0,0 +1,91 @@
+/*
+ * 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, { FunctionComponent } from 'react';
+import {
+ getApprovedReviews,
+ getChangeRequests,
+ getCommentedReviews,
+} from '../../utils/functions';
+import { Reviews, Author } from '../../utils/types';
+import { Card } from '../Card';
+import { UserHeaderList } from '../UserHeaderList';
+
+type Props = {
+ title: string;
+ createdAt: string;
+ updatedAt?: string;
+ author: Author;
+ url: string;
+ reviews: Reviews;
+ repositoryName: string;
+ isDraft: boolean;
+};
+
+const PullRequestCard: FunctionComponent = (props: Props) => {
+ const {
+ title,
+ createdAt,
+ updatedAt,
+ author,
+ url,
+ reviews,
+ repositoryName,
+ isDraft,
+ } = props;
+
+ const approvedReviews = getApprovedReviews(reviews);
+ const commentsReviews = getCommentedReviews(reviews);
+ const changeRequests = getChangeRequests(reviews);
+
+ const cardTitle = isDraft ? `🔧 DRAFT - ${title}` : title;
+
+ return (
+
+ {!!approvedReviews.length && (
+ reviewAuthor,
+ )}
+ />
+ )}
+ {!!commentsReviews.length && (
+ reviewAuthor,
+ )}
+ />
+ )}
+ {!!changeRequests.length && (
+ reviewAuthor)}
+ />
+ )}
+
+ );
+};
+
+export default PullRequestCard;
diff --git a/plugins/github-pull-requests-board/src/components/PullRequestCard/index.ts b/plugins/github-pull-requests-board/src/components/PullRequestCard/index.ts
new file mode 100644
index 0000000000..86da8f79af
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/components/PullRequestCard/index.ts
@@ -0,0 +1,16 @@
+/*
+ * 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 { default as PullRequestCard } from './PullRequestCard';
diff --git a/plugins/github-pull-requests-board/src/components/UserHeader/UserHeader.tsx b/plugins/github-pull-requests-board/src/components/UserHeader/UserHeader.tsx
new file mode 100644
index 0000000000..85083925b7
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/components/UserHeader/UserHeader.tsx
@@ -0,0 +1,46 @@
+/*
+ * 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, { FunctionComponent } from 'react';
+import { Typography, Box, Avatar, makeStyles } from '@material-ui/core';
+
+type Props = {
+ name: string;
+ avatar?: string;
+};
+
+const useStyles = makeStyles(theme => ({
+ small: {
+ width: theme.spacing(4),
+ height: theme.spacing(4),
+ marginLeft: theme.spacing(1),
+ },
+}));
+
+const UserHeader: FunctionComponent = (props: Props) => {
+ const { name, avatar } = props;
+ const classes = useStyles();
+
+ return (
+
+
+ {name}
+
+
+
+ );
+};
+
+export default UserHeader;
diff --git a/plugins/github-pull-requests-board/src/components/UserHeader/index.ts b/plugins/github-pull-requests-board/src/components/UserHeader/index.ts
new file mode 100644
index 0000000000..3125396644
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/components/UserHeader/index.ts
@@ -0,0 +1,16 @@
+/*
+ * 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 { default as UserHeader } from './UserHeader';
diff --git a/plugins/github-pull-requests-board/src/components/UserHeaderList/UserHeaderList.tsx b/plugins/github-pull-requests-board/src/components/UserHeaderList/UserHeaderList.tsx
new file mode 100644
index 0000000000..a2c52a6d5e
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/components/UserHeaderList/UserHeaderList.tsx
@@ -0,0 +1,47 @@
+/*
+ * 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, { FunctionComponent } from 'react';
+import { Typography, Box } from '@material-ui/core';
+import { filterSameUser } from '../../utils/functions';
+
+import { UserHeader } from '../UserHeader';
+import { Author } from '../../utils/types';
+
+type Props = {
+ label?: string;
+ users: Author[];
+};
+
+const UserHeaderList: FunctionComponent = (props: Props) => {
+ const { users, label } = props;
+
+ return (
+
+ {label && {label}}
+ {filterSameUser(users).map(({ login, avatarUrl }) => (
+
+ ))}
+
+ );
+};
+
+export default UserHeaderList;
diff --git a/plugins/github-pull-requests-board/src/components/UserHeaderList/index.ts b/plugins/github-pull-requests-board/src/components/UserHeaderList/index.ts
new file mode 100644
index 0000000000..81a19b68bc
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/components/UserHeaderList/index.ts
@@ -0,0 +1,16 @@
+/*
+ * 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 { default as UserHeaderList } from './UserHeaderList';
diff --git a/plugins/github-pull-requests-board/src/components/Wrapper/Wrapper.tsx b/plugins/github-pull-requests-board/src/components/Wrapper/Wrapper.tsx
new file mode 100644
index 0000000000..6d8f17cb72
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/components/Wrapper/Wrapper.tsx
@@ -0,0 +1,35 @@
+/*
+ * 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, { PropsWithChildren, FunctionComponent } from 'react';
+import { Grid, Box } from '@material-ui/core';
+
+type Props = {
+ fullscreen: boolean;
+};
+
+const Wrapper: FunctionComponent = (props: PropsWithChildren) => {
+ const { children, fullscreen } = props;
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+export default Wrapper;
diff --git a/plugins/github-pull-requests-board/src/components/Wrapper/index.ts b/plugins/github-pull-requests-board/src/components/Wrapper/index.ts
new file mode 100644
index 0000000000..18a368cd73
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/components/Wrapper/index.ts
@@ -0,0 +1,16 @@
+/*
+ * 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 { default as Wrapper } from './Wrapper';
diff --git a/plugins/github-pull-requests-board/src/components/icons/DraftPr/DraftPr.tsx b/plugins/github-pull-requests-board/src/components/icons/DraftPr/DraftPr.tsx
new file mode 100644
index 0000000000..05d21b21b9
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/components/icons/DraftPr/DraftPr.tsx
@@ -0,0 +1,32 @@
+/*
+ * 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, { FunctionComponent } from 'react';
+
+const DraftPr: FunctionComponent = () => (
+
+);
+
+export default DraftPr;
diff --git a/plugins/github-pull-requests-board/src/components/icons/DraftPr/index.ts b/plugins/github-pull-requests-board/src/components/icons/DraftPr/index.ts
new file mode 100644
index 0000000000..f2ef0a43c1
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/components/icons/DraftPr/index.ts
@@ -0,0 +1,16 @@
+/*
+ * 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 { default as DraftPrIcon } from './DraftPr';
diff --git a/plugins/github-pull-requests-board/src/hooks/usePullRequestsByTeam.tsx b/plugins/github-pull-requests-board/src/hooks/usePullRequestsByTeam.tsx
new file mode 100644
index 0000000000..234a0baefd
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/hooks/usePullRequestsByTeam.tsx
@@ -0,0 +1,82 @@
+/*
+ * 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 { useCallback, useEffect, useState } from 'react';
+import { formatPRsByReviewDecision } from '../utils/functions';
+import { PullRequests, PullRequestsColumn } from '../utils/types';
+import { useGetPullRequestsFromRepository } from '../api/useGetPullRequestsFromRepository';
+import { useGetPullRequestDetails } from '../api/useGetPullRequestDetails';
+
+export function usePullRequestsByTeam(repositories: string[]) {
+ const [pullRequests, setPullRequests] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const getPullRequests = useGetPullRequestsFromRepository();
+ const getPullRequestDetails = useGetPullRequestDetails();
+
+ const getPRsPerRepository = useCallback(
+ async (repository: string): Promise => {
+ const pullRequestsNumbers = await getPullRequests(repository);
+
+ const pullRequestsWithDetails = await Promise.all(
+ pullRequestsNumbers.map(({ node }) =>
+ getPullRequestDetails(repository, node.number),
+ ),
+ );
+
+ return pullRequestsWithDetails;
+ },
+ [getPullRequests, getPullRequestDetails],
+ );
+
+ const getPRsFromTeam = useCallback(
+ async (teamRepositories: string[]): Promise => {
+ const teamRepositoriesPromises = teamRepositories.map(repository =>
+ getPRsPerRepository(repository),
+ );
+
+ const teamPullRequests = await Promise.allSettled(
+ teamRepositoriesPromises,
+ ).then(promises =>
+ promises.reduce((acc, curr) => {
+ if (curr.status === 'fulfilled') {
+ return [...acc, ...curr.value];
+ }
+ return acc;
+ }, [] as PullRequests),
+ );
+
+ return teamPullRequests;
+ },
+ [getPRsPerRepository],
+ );
+
+ const getAllPullRequests = useCallback(async () => {
+ setLoading(true);
+
+ const teamPullRequests = await getPRsFromTeam(repositories);
+ setPullRequests(formatPRsByReviewDecision(teamPullRequests));
+ setLoading(false);
+ }, [getPRsFromTeam, repositories]);
+
+ useEffect(() => {
+ getAllPullRequests();
+ }, [getAllPullRequests]);
+
+ return {
+ pullRequests,
+ loading,
+ refreshPullRequests: getAllPullRequests,
+ };
+}
diff --git a/plugins/github-pull-requests-board/src/hooks/useUserRepositories.tsx b/plugins/github-pull-requests-board/src/hooks/useUserRepositories.tsx
new file mode 100644
index 0000000000..68c4af96e4
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/hooks/useUserRepositories.tsx
@@ -0,0 +1,49 @@
+/*
+ * 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 { useApi } from '@backstage/core-plugin-api';
+import { useEntity, catalogApiRef } from '@backstage/plugin-catalog-react';
+import { useCallback, useEffect, useState } from 'react';
+import { getProjectNameFromEntity } from '../utils/functions';
+
+export function useUserRepositories() {
+ const { entity: teamEntity } = useEntity();
+ const catalogApi = useApi(catalogApiRef);
+ const [repositories, setRepositories] = useState([]);
+
+ const getRepositoriesNames = useCallback(async () => {
+ const entitiesList = await catalogApi.getEntities({
+ filter: {
+ kind: 'Component',
+ 'spec.type': 'service',
+ 'spec.owner': teamEntity?.metadata?.name,
+ },
+ });
+
+ const entitiesNames: string[] = entitiesList.items.map(componentEntity =>
+ getProjectNameFromEntity(componentEntity),
+ );
+
+ setRepositories([...new Set(entitiesNames)]);
+ }, [catalogApi, teamEntity?.metadata?.name]);
+
+ useEffect(() => {
+ getRepositoriesNames();
+ }, [getRepositoriesNames]);
+
+ return {
+ repositories,
+ };
+}
diff --git a/plugins/github-pull-requests-board/src/index.ts b/plugins/github-pull-requests-board/src/index.ts
new file mode 100644
index 0000000000..d9cffaa468
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/index.ts
@@ -0,0 +1,19 @@
+/*
+ * 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 {
+ EntityTeamPullRequestsCard,
+ EntityTeamPullRequestsContent,
+} from './plugin';
diff --git a/plugins/github-pull-requests-board/src/plugin.test.ts b/plugins/github-pull-requests-board/src/plugin.test.ts
new file mode 100644
index 0000000000..84f5d801e6
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/plugin.test.ts
@@ -0,0 +1,28 @@
+/*
+ * 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 {
+ EntityTeamPullRequestsCard,
+ EntityTeamPullRequestsContent,
+} from './plugin';
+
+describe('github-pull-requests-board', () => {
+ it('should export EntityTeamPullRequestsCard', () => {
+ expect(EntityTeamPullRequestsCard).toBeDefined();
+ });
+ it('should export EntityTeamPullRequestsContent', () => {
+ expect(EntityTeamPullRequestsContent).toBeDefined();
+ });
+});
diff --git a/plugins/github-pull-requests-board/src/plugin.ts b/plugins/github-pull-requests-board/src/plugin.ts
new file mode 100644
index 0000000000..c3ed426dec
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/plugin.ts
@@ -0,0 +1,52 @@
+/*
+ * 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 {
+ createPlugin,
+ createComponentExtension,
+ createRoutableExtension,
+} from '@backstage/core-plugin-api';
+import { rootRouteRef } from './routes';
+
+const githubPullRequestsBoardPlugin = createPlugin({
+ id: 'github-pull-requests-board',
+ routes: {
+ root: rootRouteRef,
+ },
+});
+
+export const EntityTeamPullRequestsCard = githubPullRequestsBoardPlugin.provide(
+ createComponentExtension({
+ name: 'EntityTeamPullRequestsCard',
+ component: {
+ lazy: () =>
+ import('./components/EntityTeamPullRequestsCard').then(
+ m => m.EntityTeamPullRequestsCard,
+ ),
+ },
+ }),
+);
+
+export const EntityTeamPullRequestsContent =
+ githubPullRequestsBoardPlugin.provide(
+ createRoutableExtension({
+ name: 'PullRequestPage',
+ component: () =>
+ import('./components/EntityTeamPullRequestsContent').then(
+ m => m.EntityTeamPullRequestsContent,
+ ),
+ mountPoint: rootRouteRef,
+ }),
+ );
diff --git a/plugins/github-pull-requests-board/src/routes.ts b/plugins/github-pull-requests-board/src/routes.ts
new file mode 100644
index 0000000000..13e21ecacf
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/routes.ts
@@ -0,0 +1,20 @@
+/*
+ * 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 { createRouteRef } from '@backstage/core-plugin-api';
+
+export const rootRouteRef = createRouteRef({
+ id: 'github-pull-requests-board',
+});
diff --git a/plugins/github-pull-requests-board/src/setupTests.ts b/plugins/github-pull-requests-board/src/setupTests.ts
new file mode 100644
index 0000000000..9bb3e72355
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/setupTests.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.
+ */
+import '@testing-library/jest-dom';
+import 'cross-fetch/polyfill';
diff --git a/plugins/github-pull-requests-board/src/utils/constants.ts b/plugins/github-pull-requests-board/src/utils/constants.ts
new file mode 100644
index 0000000000..525fd9e1ce
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/utils/constants.ts
@@ -0,0 +1,20 @@
+/*
+ * 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 const COLUMNS = Object.freeze({
+ REVIEW_REQUIRED: '🔍 Review required',
+ REVIEW_IN_PROGRESS: '📝 Review in progress',
+ APPROVED: '👍 Approved',
+});
diff --git a/plugins/github-pull-requests-board/src/utils/functions.ts b/plugins/github-pull-requests-board/src/utils/functions.ts
new file mode 100644
index 0000000000..bfb0e0edc5
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/utils/functions.ts
@@ -0,0 +1,105 @@
+/*
+ * 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 { Entity } from '@backstage/catalog-model';
+import moment from 'moment';
+import {
+ Reviews,
+ PullRequests,
+ ReviewDecision,
+ PullRequestsColumn,
+ Author,
+} from './types';
+import { COLUMNS } from './constants';
+
+const GITHUB_PULL_REQUESTS_ANNOTATION = 'github.com/project-slug';
+
+export const getProjectNameFromEntity = (entity: Entity): string => {
+ return entity?.metadata.annotations?.[GITHUB_PULL_REQUESTS_ANNOTATION] ?? '';
+};
+
+export const getApprovedReviews = (reviews: Reviews = []): Reviews => {
+ return reviews.filter(({ state }) => state === 'APPROVED');
+};
+
+export const getCommentedReviews = (reviews: Reviews = []): Reviews => {
+ return reviews.filter(({ state }) => state === 'COMMENTED');
+};
+export const getChangeRequests = (reviews: Reviews = []): Reviews => {
+ return reviews.filter(({ state }) => state === 'CHANGES_REQUESTED');
+};
+
+export const filterSameUser = (users: Author[]): Author[] => {
+ return users.reduce((acc, curr) => {
+ const containsUser = acc.find(({ login }) => login === curr.login);
+
+ if (!containsUser) {
+ return [...acc, curr];
+ }
+
+ return acc;
+ }, [] as Author[]);
+};
+
+export const getElapsedTime = (start: string): string => {
+ return moment(start).fromNow();
+};
+
+export const formatPRsByReviewDecision = (
+ prs: PullRequests,
+): PullRequestsColumn[] => {
+ const reviewDecisions = prs.reduce(
+ (acc, curr) => {
+ const decision = curr.reviewDecision || 'REVIEW_REQUIRED';
+
+ if (decision !== 'APPROVED' && curr.latestReviews.nodes.length === 0) {
+ return {
+ ...acc,
+ REVIEW_REQUIRED: [...acc.REVIEW_REQUIRED, curr],
+ };
+ }
+
+ if (decision !== 'APPROVED' && curr.latestReviews.nodes.length > 0) {
+ return {
+ ...acc,
+ IN_PROGRESS: [...acc.IN_PROGRESS, curr],
+ };
+ }
+
+ if (decision === 'APPROVED') {
+ return {
+ ...acc,
+ APPROVED: [...acc.APPROVED, curr],
+ };
+ }
+
+ return acc;
+ },
+ {
+ REVIEW_REQUIRED: [],
+ IN_PROGRESS: [],
+ APPROVED: [],
+ } as Record,
+ );
+
+ return [
+ {
+ title: COLUMNS.REVIEW_REQUIRED,
+ content: reviewDecisions.REVIEW_REQUIRED,
+ },
+ { title: COLUMNS.REVIEW_IN_PROGRESS, content: reviewDecisions.IN_PROGRESS },
+ { title: COLUMNS.APPROVED, content: reviewDecisions.APPROVED },
+ ];
+};
diff --git a/plugins/github-pull-requests-board/src/utils/types.tsx b/plugins/github-pull-requests-board/src/utils/types.tsx
new file mode 100644
index 0000000000..a2b121a1b8
--- /dev/null
+++ b/plugins/github-pull-requests-board/src/utils/types.tsx
@@ -0,0 +1,84 @@
+/*
+ * 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 type GraphQlPullRequest = {
+ repository: {
+ pullRequest: T;
+ };
+};
+
+export type GraphQlPullRequests = {
+ repository: {
+ pullRequests: {
+ edges: T;
+ };
+ };
+};
+
+export type PullRequestsNumber = {
+ node: {
+ number: number;
+ };
+};
+
+export type Review = {
+ state:
+ | 'PENDING'
+ | 'COMMENTED'
+ | 'APPROVED'
+ | 'CHANGES_REQUESTED'
+ | 'DISMISSED';
+ author: Author;
+};
+
+export type Reviews = Review[];
+
+export type Author = {
+ login: string;
+ avatarUrl: string;
+ id: string;
+ email: string;
+ name: string;
+};
+
+export type PullRequest = {
+ id: string;
+ repository: {
+ name: string;
+ };
+ title: string;
+ url: string;
+ lastEditedAt: string;
+ latestReviews: {
+ nodes: Reviews;
+ };
+ mergeable: boolean;
+ state: string;
+ reviewDecision: ReviewDecision | null;
+ isDraft: boolean;
+ createdAt: string;
+ author: Author;
+};
+
+export type PullRequests = PullRequest[];
+
+export type PullRequestsColumn = {
+ title: string;
+ content: PullRequests;
+};
+
+export type PRCardFormating = 'compacted' | 'fullscreen' | 'draft';
+
+export type ReviewDecision = 'IN_PROGRESS' | 'APPROVED' | 'REVIEW_REQUIRED';