diff --git a/.changeset/silver-carpets-grin.md b/.changeset/silver-carpets-grin.md
new file mode 100644
index 0000000000..9865393631
--- /dev/null
+++ b/.changeset/silver-carpets-grin.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-github-issues': minor
+---
+
+New plugin for displaying GitHub Issues added
diff --git a/plugins/github-issues/.eslintrc.js b/plugins/github-issues/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/plugins/github-issues/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/github-issues/README.md b/plugins/github-issues/README.md
new file mode 100644
index 0000000000..eeb93ab27b
--- /dev/null
+++ b/plugins/github-issues/README.md
@@ -0,0 +1,63 @@
+# GitHub Issues plugin
+
+Welcome to the GitHub Issues plugin!
+
+Based on the [well-known GitHub slug annotation](https://backstage.io/docs/features/software-catalog/well-known-annotations#githubcomproject-slug) associated with the Entity, it renders the list of Open issues in GitHub.
+
+The plugin is designed to work with four Entity kinds, and it behaves a bit differently depending on that kind:
+
+- Kind: Group/User: plugin renders issues from all repositories for which the Entity is the owner.
+- Kind: API/Component: plugin renders issues from only one repository assigned to the Entity
+
+**Issues are sorted from the recently updated DESC order (the plugin might not render all issues from a single repo next to each other).**
+
+## Prerequisites
+
+- [GitHub Authentication Provider](https://backstage.io/docs/auth/github/provider)
+
+## Usage
+
+Install the plugin by running the following command **from your Backstage root directory**
+
+`yarn --cwd packages/app add @backstage/plugin-github-issues`
+
+After installation, the plugin can be used as a Card or as a Page.
+
+```typescript
+import {
+ GitHubIssuesCard,
+ GitHubIssuesPage,
+} from '@backstage/plugin-github-issues';
+
+// To use as a page Plugin needs to be wrapped in EntityLayout.Route
+const RenderGitHubIssuesPage = () => (
+
+
+
+
+
+
+
+);
+
+// To use as a card and make it render correctly please place it inside appropriate Grid elements
+const RenderGitHubIssuesCard = () => (
+
+
+
+
+
+
+
+
+
+);
+```
+
+## Configuration
+
+Both `GitHubIssuesPage` and `GitHubIssuesCard` provide default configuration. It is ready to use out of the box.
+However, you can configure the plugin with props:
+
+- `itemsPerPage: number = 10` - Issues in the list are paginated, number of issues on a single page is controlled with this prop
+- `itemsPerRepo: number = 40` - the plugin doesn't download all Issues available on GitHub. By default, it will get at most 40 Issues - this prop controls this behaviour
diff --git a/plugins/github-issues/api-report.md b/plugins/github-issues/api-report.md
new file mode 100644
index 0000000000..eb69fc8e8e
--- /dev/null
+++ b/plugins/github-issues/api-report.md
@@ -0,0 +1,33 @@
+## API Report File for "@backstage/plugin-github-issues"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+///
+
+import { BackstagePlugin } from '@backstage/core-plugin-api';
+import { RouteRef } from '@backstage/core-plugin-api';
+
+// @public (undocumented)
+export const GitHubIssuesCard: (props: GitHubIssuesProps) => JSX.Element;
+
+// @public (undocumented)
+export const GitHubIssuesPage: (props: GitHubIssuesProps) => JSX.Element;
+
+// @public (undocumented)
+export const gitHubIssuesPlugin: BackstagePlugin<
+ {
+ root: RouteRef;
+ },
+ {},
+ {}
+>;
+
+// @public (undocumented)
+export type GitHubIssuesProps = {
+ itemsPerPage?: number;
+ itemsPerRepo?: number;
+};
+
+// (No @packageDocumentation comment for this package)
+```
diff --git a/plugins/github-issues/package.json b/plugins/github-issues/package.json
new file mode 100644
index 0000000000..5435f81324
--- /dev/null
+++ b/plugins/github-issues/package.json
@@ -0,0 +1,61 @@
+{
+ "name": "@backstage/plugin-github-issues",
+ "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"
+ },
+ "scripts": {
+ "start": "backstage-cli package start",
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "clean": "backstage-cli package clean",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack"
+ },
+ "prettier": "@spotify/prettier-config",
+ "dependencies": {
+ "@backstage/catalog-model": "^1.0.3",
+ "@backstage/core-components": "^0.10.1-next.0",
+ "@backstage/core-plugin-api": "^1.0.5-next.0",
+ "@backstage/integration": "^1.3.0-next.0",
+ "@backstage/plugin-catalog-react": "^1.1.3-next.0",
+ "@backstage/theme": "^0.2.15",
+ "@material-ui/core": "^4.12.4",
+ "@material-ui/icons": "^4.9.1",
+ "@material-ui/lab": "^4.0.0-alpha.61",
+ "luxon": "^2.4.0",
+ "octokit": "^2.0.4",
+ "react-use": "^17.2.4"
+ },
+ "peerDependencies": {
+ "react": "^16.13.1 || ^17.0.0"
+ },
+ "devDependencies": {
+ "@backstage/cli": "^0.18.1-next.0",
+ "@backstage/core-app-api": "^1.0.5-next.0",
+ "@backstage/dev-utils": "^1.0.5-next.0",
+ "@backstage/test-utils": "^1.1.3-next.0",
+ "@spotify/prettier-config": "^13.0.1",
+ "@testing-library/jest-dom": "^5.10.1",
+ "@testing-library/react": "^12.1.3",
+ "@testing-library/user-event": "^14.0.0",
+ "@types/jest": "*",
+ "@types/node": "*",
+ "@types/react": "^16.13.1 || ^17.0.0",
+ "cross-fetch": "^3.1.5",
+ "msw": "^0.44.0",
+ "prettier": "^2.7.1"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/plugins/github-issues/src/components/GitHubIssues/GitHubIssues.tsx b/plugins/github-issues/src/components/GitHubIssues/GitHubIssues.tsx
new file mode 100644
index 0000000000..5380c8eb22
--- /dev/null
+++ b/plugins/github-issues/src/components/GitHubIssues/GitHubIssues.tsx
@@ -0,0 +1,89 @@
+/*
+ * 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 { Box, IconButton, Typography } from '@material-ui/core';
+import { InfoCard, Progress } from '@backstage/core-components';
+import RefreshIcon from '@material-ui/icons/Refresh';
+
+import { useEntityGitHubRepositories } from '../../hooks/useEntityGitHubRepositories';
+import {
+ RepoIssues,
+ useGetIssuesByRepoFromGitHub,
+} from '../../hooks/useGetIssuesByRepoFromGitHub';
+
+import { IssueList } from './IssuesList';
+import { NoRepositoriesInfo } from './NoRepositoriesInfo';
+
+/**
+ * @public
+ */
+export type GitHubIssuesProps = {
+ itemsPerPage?: number;
+ itemsPerRepo?: number;
+};
+
+export const GitHubIssues = (props: GitHubIssuesProps) => {
+ const { itemsPerPage = 10, itemsPerRepo = 40 } = props;
+
+ const [isLoading, setIsLoading] = React.useState(true);
+
+ const [issuesByRepository, setIssuesByRepository] =
+ React.useState>();
+
+ const { repositories } = useEntityGitHubRepositories();
+ const getIssues = useGetIssuesByRepoFromGitHub();
+
+ const fetchGitHubIssues = React.useCallback(async () => {
+ setIsLoading(true);
+ const issuesByRepo = await getIssues(repositories, itemsPerRepo);
+
+ setIssuesByRepository(issuesByRepo);
+ setIsLoading(false);
+ }, [itemsPerRepo, getIssues, repositories]);
+
+ React.useEffect(() => {
+ if (repositories.length) {
+ fetchGitHubIssues();
+ } else {
+ setIsLoading(false);
+ }
+ }, [repositories.length, fetchGitHubIssues]);
+
+ if (!repositories.length) {
+ return ;
+ }
+
+ return (
+
+ Open GitHub Issues
+
+
+
+
+ }
+ >
+ {isLoading && }
+
+
+
+ );
+};
diff --git a/plugins/github-issues/src/components/GitHubIssues/IssueCard/Assignees.tsx b/plugins/github-issues/src/components/GitHubIssues/IssueCard/Assignees.tsx
new file mode 100644
index 0000000000..173c8d9439
--- /dev/null
+++ b/plugins/github-issues/src/components/GitHubIssues/IssueCard/Assignees.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 from 'react';
+import { Typography, Box, Avatar, makeStyles } from '@material-ui/core';
+
+type AssigneesProps = {
+ name?: string;
+ avatar?: string;
+};
+
+const useStyles = makeStyles(theme => ({
+ small: {
+ width: theme.spacing(4),
+ height: theme.spacing(4),
+ marginLeft: theme.spacing(1),
+ },
+ noAssignees: {
+ height: theme.spacing(4),
+ },
+}));
+
+export const Assignees = (props: AssigneesProps) => {
+ const { name, avatar } = props;
+ const classes = useStyles();
+
+ // todo: many assignees -> NUM assignees + stock images on each other
+ return name ? (
+
+
+ {name}
+
+
+
+ ) : (
+
+
+ No assignees
+
+
+ );
+};
diff --git a/plugins/github-issues/src/components/GitHubIssues/IssueCard/CommentsCount.tsx b/plugins/github-issues/src/components/GitHubIssues/IssueCard/CommentsCount.tsx
new file mode 100644
index 0000000000..80a365cbbb
--- /dev/null
+++ b/plugins/github-issues/src/components/GitHubIssues/IssueCard/CommentsCount.tsx
@@ -0,0 +1,40 @@
+/*
+ * 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 { ChatIcon } from '@backstage/core-components';
+import { Box, Badge } from '@material-ui/core';
+
+type CommentsCountProps = {
+ commentsCount: number;
+};
+
+export const CommentsCount = (props: CommentsCountProps) => {
+ const { commentsCount } = props;
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/plugins/github-issues/src/components/GitHubIssues/IssueCard/IssueCard.tsx b/plugins/github-issues/src/components/GitHubIssues/IssueCard/IssueCard.tsx
new file mode 100644
index 0000000000..95658fb343
--- /dev/null
+++ b/plugins/github-issues/src/components/GitHubIssues/IssueCard/IssueCard.tsx
@@ -0,0 +1,102 @@
+/*
+ * 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 { DateTime } from 'luxon';
+
+import {
+ Box,
+ Paper,
+ Typography,
+ CardActionArea,
+ Link,
+} from '@material-ui/core';
+import { Assignees } from './Assignees';
+import { CommentsCount } from './CommentsCount';
+
+import Divider from '@material-ui/core/Divider';
+
+type IssueCardProps = {
+ title: string;
+ createdAt: string;
+ updatedAt?: string;
+ url: string;
+ authorName: string;
+ assigneeName?: string;
+ assigneeAvatar?: string;
+ authorAvatar?: string;
+ repositoryName: string;
+ commentsCount: number;
+ even: boolean;
+};
+
+const getElapsedTime = (isoDate: string) =>
+ DateTime.fromISO(isoDate).toRelative();
+
+export const IssueCard = (props: IssueCardProps) => {
+ const {
+ title,
+ createdAt,
+ updatedAt,
+ url,
+ assigneeName,
+ assigneeAvatar,
+ authorName,
+ repositoryName,
+ commentsCount,
+ } = props;
+
+ return (
+
+
+
+
+
+
+ {repositoryName}
+
+
+
+
+
+ {title}
+
+
+
+
+
+
+ Created at: {getElapsedTime(createdAt)} by{' '}
+ {authorName}
+
+ {updatedAt && (
+
+ Last update at: {getElapsedTime(updatedAt)}
+
+ )}
+
+ {commentsCount > 0 && (
+
+ )}
+
+
+
+
+
+ );
+};
diff --git a/plugins/github-issues/src/components/GitHubIssues/IssueCard/index.ts b/plugins/github-issues/src/components/GitHubIssues/IssueCard/index.ts
new file mode 100644
index 0000000000..5c60abf6ab
--- /dev/null
+++ b/plugins/github-issues/src/components/GitHubIssues/IssueCard/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 { IssueCard } from './IssueCard';
diff --git a/plugins/github-issues/src/components/GitHubIssues/IssuesList/Filters/Filters.tsx b/plugins/github-issues/src/components/GitHubIssues/IssuesList/Filters/Filters.tsx
new file mode 100644
index 0000000000..de071ab5bb
--- /dev/null
+++ b/plugins/github-issues/src/components/GitHubIssues/IssuesList/Filters/Filters.tsx
@@ -0,0 +1,67 @@
+/*
+ * 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 { Select, SelectedItems, SelectItem } from '@backstage/core-components';
+import { makeStyles, Box, Typography } from '@material-ui/core';
+
+type RepositoryFiltersProps = {
+ items: Array;
+ totalIssuesInGitHub: number;
+ placeholder: string;
+ onChange: (active: Array) => void;
+};
+
+const useStyles = makeStyles(theme => ({
+ filters: {
+ margin: theme.spacing(0, 0, 2, 0),
+ '& > div': {
+ maxWidth: '800px',
+ '& > div': {
+ maxWidth: '800px',
+ },
+ },
+ },
+}));
+
+const checkSelectedItems: (
+ onChange: (active: Array) => void,
+) => (active: SelectedItems) => void = onChange => active => {
+ return onChange(active as Array);
+};
+
+export const RepositoryFilters = ({
+ items,
+ onChange,
+ placeholder,
+}: RepositoryFiltersProps) => {
+ const css = useStyles();
+
+ return (
+
+
+
+ *Repositories with more Issues on GitHub than available to view in
+ Backstage. To view them go to GitHub.
+
+
+ );
+};
diff --git a/plugins/github-issues/src/components/GitHubIssues/IssuesList/Filters/index.ts b/plugins/github-issues/src/components/GitHubIssues/IssuesList/Filters/index.ts
new file mode 100644
index 0000000000..331d017455
--- /dev/null
+++ b/plugins/github-issues/src/components/GitHubIssues/IssuesList/Filters/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 * from './Filters';
diff --git a/plugins/github-issues/src/components/GitHubIssues/IssuesList/IssuesList.tsx b/plugins/github-issues/src/components/GitHubIssues/IssuesList/IssuesList.tsx
new file mode 100644
index 0000000000..f279294928
--- /dev/null
+++ b/plugins/github-issues/src/components/GitHubIssues/IssuesList/IssuesList.tsx
@@ -0,0 +1,167 @@
+/*
+ * 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 { Box } from '@material-ui/core';
+import { Pagination } from '@material-ui/lab';
+
+import { IssueCard } from '../IssueCard';
+import { RepoIssues } from '../../../hooks/useGetIssuesByRepoFromGitHub';
+import { RepositoryFilters } from './Filters';
+
+export type PluginMode = 'page' | 'card';
+
+export type IssueListProps = {
+ itemsPerPage?: number;
+ issuesByRepository?: Record;
+};
+
+const getIssuesCountForFilterLabel = (
+ totalIssues: number,
+ issuesAvailable: number,
+) =>
+ `(${totalIssues} ${totalIssues === 1 ? 'Issue' : `Issues`})${
+ issuesAvailable < totalIssues ? '*' : ''
+ }`;
+
+export const IssueList = ({
+ itemsPerPage = 10,
+ issuesByRepository,
+}: IssueListProps) => {
+ const [currentPage, setCurrentPage] = React.useState(1);
+ const [activeFilter, setActiveFilter] = React.useState>([]);
+
+ const filters = React.useMemo(
+ () =>
+ issuesByRepository
+ ? Object.keys(issuesByRepository)
+ .filter(repo => issuesByRepository[repo].issues.totalCount > 0)
+ .map(repo => ({
+ label: `${repo} ${getIssuesCountForFilterLabel(
+ issuesByRepository[repo].issues.totalCount,
+ issuesByRepository[repo].issues.edges.length,
+ )}`,
+ value: repo,
+ }))
+ : [],
+ [issuesByRepository],
+ );
+
+ const totalIssuesInGitHub = React.useMemo(
+ () =>
+ issuesByRepository
+ ? Object.values(issuesByRepository).reduce(
+ (acc, { issues: { totalCount } }) => acc + totalCount,
+ 0,
+ )
+ : 0,
+ [issuesByRepository],
+ );
+
+ const filteredRepos = React.useMemo(
+ () =>
+ issuesByRepository && activeFilter.length
+ ? activeFilter.reduce(
+ (acc, val) => ({
+ [val]: issuesByRepository[val],
+ ...acc,
+ }),
+ {},
+ )
+ : issuesByRepository,
+ [issuesByRepository, activeFilter],
+ );
+
+ const issues = React.useMemo(
+ () =>
+ filteredRepos
+ ? Object.values(filteredRepos)
+ .map(({ issues: { edges } }) => edges)
+ .flat()
+ .sort((a, b) => {
+ if (a.node.updatedAt > b.node.updatedAt) {
+ return -1;
+ } else if (b.node.updatedAt > a.node.updatedAt) {
+ return 1;
+ }
+ return 0;
+ })
+ : [],
+ [filteredRepos],
+ );
+
+ const displayIssues = issues.slice(
+ (currentPage - 1) * itemsPerPage,
+ (currentPage - 1) * itemsPerPage + itemsPerPage,
+ );
+
+ return (
+
+ {issues.length > 0 && (
+
+ )}
+
+ {displayIssues.length > 0 ? (
+ displayIssues.map(
+ (
+ {
+ node: {
+ title,
+ comments,
+ author,
+ createdAt,
+ updatedAt,
+ repository,
+ assignees,
+ url,
+ },
+ },
+ index,
+ ) => (
+
+ ),
+ )
+ ) : (
+ Hurray! No Issues 🚀
+ )}
+ {issues.length / itemsPerPage > 1 ? (
+ setCurrentPage(page)}
+ />
+ ) : null}
+
+ );
+};
diff --git a/plugins/github-issues/src/components/GitHubIssues/IssuesList/index.tsx b/plugins/github-issues/src/components/GitHubIssues/IssuesList/index.tsx
new file mode 100644
index 0000000000..0b7514c720
--- /dev/null
+++ b/plugins/github-issues/src/components/GitHubIssues/IssuesList/index.tsx
@@ -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 * from './IssuesList';
diff --git a/plugins/github-issues/src/components/GitHubIssues/NoRepositoriesInfo/NoRepositoriesInfo.tsx b/plugins/github-issues/src/components/GitHubIssues/NoRepositoriesInfo/NoRepositoriesInfo.tsx
new file mode 100644
index 0000000000..dca4b872ae
--- /dev/null
+++ b/plugins/github-issues/src/components/GitHubIssues/NoRepositoriesInfo/NoRepositoriesInfo.tsx
@@ -0,0 +1,27 @@
+/*
+ * 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 { EmptyState } from '@backstage/core-components';
+
+export const NoRepositoriesInfo = () => {
+ return (
+
+ );
+};
diff --git a/plugins/github-issues/src/components/GitHubIssues/NoRepositoriesInfo/index.tsx b/plugins/github-issues/src/components/GitHubIssues/NoRepositoriesInfo/index.tsx
new file mode 100644
index 0000000000..577bb475e3
--- /dev/null
+++ b/plugins/github-issues/src/components/GitHubIssues/NoRepositoriesInfo/index.tsx
@@ -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 * from './NoRepositoriesInfo';
diff --git a/plugins/github-issues/src/components/GitHubIssues/index.ts b/plugins/github-issues/src/components/GitHubIssues/index.ts
new file mode 100644
index 0000000000..6fa55e3358
--- /dev/null
+++ b/plugins/github-issues/src/components/GitHubIssues/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 * from './GitHubIssues';
diff --git a/plugins/github-issues/src/hooks/useEntityGitHubRepositories.ts b/plugins/github-issues/src/hooks/useEntityGitHubRepositories.ts
new file mode 100644
index 0000000000..242de514ca
--- /dev/null
+++ b/plugins/github-issues/src/hooks/useEntityGitHubRepositories.ts
@@ -0,0 +1,69 @@
+/*
+ * 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 { useApi } from '@backstage/core-plugin-api';
+import {
+ catalogApiRef,
+ humanizeEntityRef,
+ useEntity,
+} from '@backstage/plugin-catalog-react';
+import { useCallback, useEffect, useState } from 'react';
+
+const GITHUB_PROJECT_SLUG_ANNOTATION = 'github.com/project-slug';
+
+export const getProjectNameFromEntity = (entity: Entity): string => {
+ return entity?.metadata.annotations?.[GITHUB_PROJECT_SLUG_ANNOTATION] ?? '';
+};
+
+export function useEntityGitHubRepositories() {
+ const { entity } = useEntity();
+
+ const catalogApi = useApi(catalogApiRef);
+ const [repositories, setRepositories] = useState([]);
+
+ const getRepositoriesNames = useCallback(async () => {
+ if (entity.kind === 'Component' || entity.kind === 'API') {
+ const entityName = getProjectNameFromEntity(entity);
+
+ if (entityName) {
+ setRepositories([entityName]);
+ }
+
+ return;
+ }
+
+ const entitiesList = await catalogApi.getEntities({
+ filter: {
+ kind: ['Component', 'API'],
+ 'spec.owner': humanizeEntityRef(entity, { defaultKind: 'group' }),
+ },
+ });
+
+ const entitiesNames: string[] = entitiesList.items.map(componentEntity =>
+ getProjectNameFromEntity(componentEntity),
+ );
+
+ setRepositories([...new Set(entitiesNames)].filter(name => name.length));
+ }, [catalogApi, entity]);
+
+ useEffect(() => {
+ getRepositoriesNames();
+ }, [getRepositoriesNames]);
+
+ return {
+ repositories,
+ };
+}
diff --git a/plugins/github-issues/src/hooks/useGetIssuesBeRepoFromGitHub.test.tsx b/plugins/github-issues/src/hooks/useGetIssuesBeRepoFromGitHub.test.tsx
new file mode 100644
index 0000000000..547e32db22
--- /dev/null
+++ b/plugins/github-issues/src/hooks/useGetIssuesBeRepoFromGitHub.test.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.
+ */
+const mockGraphQLQuery = jest.fn(() => ({}));
+jest.mock('./useOctokitGraphQL', () => ({
+ useOctokitGraphQL: jest.fn(() => mockGraphQLQuery),
+}));
+
+import React from 'react';
+import { render } from '@testing-library/react';
+import { useGetIssuesByRepoFromGitHub } from './useGetIssuesByRepoFromGitHub';
+
+describe('useGetIssuesBeRepoFromGitHub', () => {
+ it('should call GitHub API with correct query with fragment for each repo', async () => {
+ const Helper = () => {
+ const getIssues = useGetIssuesByRepoFromGitHub();
+
+ getIssues(['mrwolny/yo-yo', 'mrwolny/yoyo', 'mrwolny/yo.yo'], 10);
+
+ return ;
+ };
+
+ render();
+
+ expect(mockGraphQLQuery).toHaveBeenCalledTimes(1);
+ expect(mockGraphQLQuery).toHaveBeenCalledWith(
+ '\n' +
+ ' \n' +
+ ' fragment issues on Repository {\n' +
+ ' issues(\n' +
+ ' states: OPEN\n' +
+ ' first: 10\n' +
+ ' orderBy: { field: UPDATED_AT, direction: DESC }\n' +
+ ' ) {\n' +
+ ' totalCount\n' +
+ ' edges {\n' +
+ ' node {\n' +
+ ' assignees(first: 10) {\n' +
+ ' edges {\n' +
+ ' node {\n' +
+ ' avatarUrl\n' +
+ ' login\n' +
+ ' }\n' +
+ ' }\n' +
+ ' }\n' +
+ ' author {\n' +
+ ' login\n' +
+ ' avatarUrl\n' +
+ ' url\n' +
+ ' }\n' +
+ ' repository {\n' +
+ ' nameWithOwner\n' +
+ ' }\n' +
+ ' title\n' +
+ ' url\n' +
+ ' participants {\n' +
+ ' totalCount\n' +
+ ' }\n' +
+ ' updatedAt\n' +
+ ' createdAt\n' +
+ ' comments(last: 1) {\n' +
+ ' totalCount\n' +
+ ' }\n' +
+ ' }\n' +
+ ' }\n' +
+ ' }\n' +
+ ' }\n' +
+ ' \n' +
+ '\n' +
+ ' query {\n' +
+ ' \n' +
+ ' yoyo: repository(name: "yo-yo", owner: "mrwolny") {\n' +
+ ' ...issues\n' +
+ ' }\n' +
+ ' ,\n' +
+ ' yoyox: repository(name: "yoyo", owner: "mrwolny") {\n' +
+ ' ...issues\n' +
+ ' }\n' +
+ ' ,\n' +
+ ' yoyoxx: repository(name: "yo.yo", owner: "mrwolny") {\n' +
+ ' ...issues\n' +
+ ' }\n' +
+ ' \n' +
+ ' } \n' +
+ ' ',
+ );
+ });
+});
diff --git a/plugins/github-issues/src/hooks/useGetIssuesByRepoFromGitHub.ts b/plugins/github-issues/src/hooks/useGetIssuesByRepoFromGitHub.ts
new file mode 100644
index 0000000000..271cd6c741
--- /dev/null
+++ b/plugins/github-issues/src/hooks/useGetIssuesByRepoFromGitHub.ts
@@ -0,0 +1,173 @@
+/*
+ * 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 { useOctokitGraphQL } from './useOctokitGraphQL';
+
+type Assignee = {
+ avatarUrl: string;
+ login: string;
+};
+
+export type EdgesWithNodes = {
+ edges: Array<{
+ node: T;
+ }>;
+};
+
+export type Node = {
+ node: T;
+};
+
+type IssueAuthor = {
+ login: string;
+};
+
+export type Issue = {
+ assignees: EdgesWithNodes;
+ author: IssueAuthor;
+ repository: {
+ nameWithOwner: string;
+ };
+ title: string;
+ url: string;
+ participants: {
+ totalCount: number;
+ };
+ createdAt: string;
+ updatedAt: string;
+ comments: {
+ totalCount: number;
+ };
+};
+
+export type RepoIssues = {
+ issues: {
+ totalCount: number;
+ } & EdgesWithNodes;
+};
+
+export type RepoIssuesQueryResults = Record;
+
+const createQuery = (
+ repositories: Array<{
+ safeName: string;
+ name: string;
+ owner: string;
+ }>,
+ itemsPerRepo: number,
+): string => {
+ const fragment = `
+ fragment issues on Repository {
+ issues(
+ states: OPEN
+ first: ${itemsPerRepo}
+ orderBy: { field: UPDATED_AT, direction: DESC }
+ ) {
+ totalCount
+ edges {
+ node {
+ assignees(first: 10) {
+ edges {
+ node {
+ avatarUrl
+ login
+ }
+ }
+ }
+ author {
+ login
+ avatarUrl
+ url
+ }
+ repository {
+ nameWithOwner
+ }
+ title
+ url
+ participants {
+ totalCount
+ }
+ updatedAt
+ createdAt
+ comments(last: 1) {
+ totalCount
+ }
+ }
+ }
+ }
+ }
+ `;
+
+ const query = `
+ ${fragment}
+
+ query {
+ ${repositories.map(
+ ({ safeName, name, owner }) => `
+ ${safeName}: repository(name: "${name}", owner: "${owner}") {
+ ...issues
+ }
+ `,
+ )}
+ }
+ `;
+
+ return query;
+};
+
+export const useGetIssuesByRepoFromGitHub = () => {
+ const graphql = useOctokitGraphQL();
+
+ const fn = React.useRef(
+ async (
+ repos: Array,
+ itemsPerRepo: number,
+ ): Promise> => {
+ const safeNames: Array = [];
+
+ const repositories = repos.map(repo => {
+ const [owner, name] = repo.split('/');
+
+ const safeNameRegex = /-|\./gi;
+ let safeName = name.replace(safeNameRegex, '');
+
+ while (safeNames.includes(safeName)) {
+ safeName += 'x';
+ }
+
+ safeNames.push(safeName);
+
+ return {
+ safeName,
+ name,
+ owner,
+ };
+ });
+
+ const issuesByRepo: RepoIssuesQueryResults = await graphql(
+ createQuery(repositories, itemsPerRepo),
+ );
+
+ return repositories.reduce((acc, { safeName, name, owner }) => {
+ acc[`${owner}/${name}`] = issuesByRepo[safeName];
+
+ return acc;
+ }, {} as Record);
+ },
+ );
+
+ return fn.current;
+};
diff --git a/plugins/github-issues/src/hooks/useOctokitGraphQL.ts b/plugins/github-issues/src/hooks/useOctokitGraphQL.ts
new file mode 100644
index 0000000000..6b783efd6f
--- /dev/null
+++ b/plugins/github-issues/src/hooks/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';
+import {
+ useApi,
+ githubAuthApiRef,
+ configApiRef,
+} from '@backstage/core-plugin-api';
+import { readGitHubIntegrationConfigs } from '@backstage/integration';
+
+let octokit: Octokit;
+
+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-issues/src/index.ts b/plugins/github-issues/src/index.ts
new file mode 100644
index 0000000000..5f6c248db7
--- /dev/null
+++ b/plugins/github-issues/src/index.ts
@@ -0,0 +1,22 @@
+/*
+ * 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 {
+ gitHubIssuesPlugin,
+ GitHubIssuesPage,
+ GitHubIssuesCard,
+} from './plugin';
+
+export type { GitHubIssuesProps } from './components/GitHubIssues';
diff --git a/plugins/github-issues/src/plugin.test.ts b/plugins/github-issues/src/plugin.test.ts
new file mode 100644
index 0000000000..e6749dd970
--- /dev/null
+++ b/plugins/github-issues/src/plugin.test.ts
@@ -0,0 +1,22 @@
+/*
+ * 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 { gitHubIssuesPlugin } from './plugin';
+
+describe('github-issues', () => {
+ it('should export plugin', () => {
+ expect(gitHubIssuesPlugin).toBeDefined();
+ });
+});
diff --git a/plugins/github-issues/src/plugin.ts b/plugins/github-issues/src/plugin.ts
new file mode 100644
index 0000000000..ece3fd09a2
--- /dev/null
+++ b/plugins/github-issues/src/plugin.ts
@@ -0,0 +1,50 @@
+/*
+ * 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';
+
+/** @public */
+export const gitHubIssuesPlugin = createPlugin({
+ id: 'github-issues',
+ routes: {
+ root: rootRouteRef,
+ },
+});
+
+/** @public */
+export const GitHubIssuesCard = gitHubIssuesPlugin.provide(
+ createComponentExtension({
+ name: 'GitHubIssuesCard',
+ component: {
+ lazy: () => import('./components/GitHubIssues').then(m => m.GitHubIssues),
+ },
+ }),
+);
+
+/** @public */
+export const GitHubIssuesPage = gitHubIssuesPlugin.provide(
+ createRoutableExtension({
+ name: 'GitHubIssuesPage',
+ component: () =>
+ import('./components/GitHubIssues').then(m => m.GitHubIssues),
+ mountPoint: rootRouteRef,
+ }),
+);
diff --git a/plugins/github-issues/src/routes.ts b/plugins/github-issues/src/routes.ts
new file mode 100644
index 0000000000..e0f681261a
--- /dev/null
+++ b/plugins/github-issues/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-issues',
+});
diff --git a/plugins/github-issues/src/setupTests.ts b/plugins/github-issues/src/setupTests.ts
new file mode 100644
index 0000000000..9bb3e72355
--- /dev/null
+++ b/plugins/github-issues/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/yarn.lock b/yarn.lock
index 4d624b850e..b934d159e0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4859,7 +4859,7 @@
react-beautiful-dnd "^13.0.0"
react-double-scrollbar "0.0.15"
-"@material-ui/core@^4.11.0", "@material-ui/core@^4.11.3", "@material-ui/core@^4.12.1", "@material-ui/core@^4.12.2", "@material-ui/core@^4.9.10", "@material-ui/core@^4.9.13":
+"@material-ui/core@^4.11.0", "@material-ui/core@^4.11.3", "@material-ui/core@^4.12.1", "@material-ui/core@^4.12.2", "@material-ui/core@^4.12.4", "@material-ui/core@^4.9.10", "@material-ui/core@^4.9.13":
version "4.12.4"
resolved "https://registry.npmjs.org/@material-ui/core/-/core-4.12.4.tgz#4ac17488e8fcaf55eb6a7f5efb2a131e10138a73"
integrity sha512-tr7xekNlM9LjA6pagJmL8QCgZXaubWUwkJnoYcMKd4gw/t4XiyvnTkjdGrUVicyB2BsdaAv1tvow45bPM4sSwQ==
@@ -4906,7 +4906,7 @@
prop-types "^15.7.2"
react-is "^16.8.0 || ^17.0.0"
-"@material-ui/lab@^4.0.0-alpha.57", "@material-ui/lab@^4.0.0-alpha.60":
+"@material-ui/lab@^4.0.0-alpha.57", "@material-ui/lab@^4.0.0-alpha.60", "@material-ui/lab@^4.0.0-alpha.61":
version "4.0.0-alpha.61"
resolved "https://registry.npmjs.org/@material-ui/lab/-/lab-4.0.0-alpha.61.tgz#9bf8eb389c0c26c15e40933cc114d4ad85e3d978"
integrity sha512-rSzm+XKiNUjKegj8bzt5+pygZeckNLOr+IjykH8sYdVk7dE9y2ZuUSofiMV2bJk3qU+JHwexmw+q0RyNZB9ugg==
@@ -6270,6 +6270,11 @@
resolved "https://registry.npmjs.org/@spotify/eslint-config-typescript/-/eslint-config-typescript-14.0.1.tgz#850a435b0defebbb3de591d83e3fc369bbc51753"
integrity sha512-y/8on49Wtg3HvKd9A32Q7iJaOgngcSJR8hjGx/POFhJFcPRcZuTJSlhd31CF8fg78bMuzdvYH8au7FhNKjnEVw==
+"@spotify/prettier-config@^13.0.1":
+ version "13.0.1"
+ resolved "https://registry.npmjs.org/@spotify/prettier-config/-/prettier-config-13.0.1.tgz#0fdceb3d4ab543259ce6adc0ec1d10e34898b812"
+ integrity sha512-oVd4hjx2+y0MeUdk1l+ItwVLwlrDlvTlGwXBWMMzPYc7DLyxuxFvDfoHGkAQkrikfAgtdnzxrW6u9a8ywUqdfw==
+
"@spotify/prettier-config@^14.0.0":
version "14.0.1"
resolved "https://registry.npmjs.org/@spotify/prettier-config/-/prettier-config-14.0.1.tgz#a3a2342ec07693647ab76a64a623fbb79d464fa7"
@@ -18266,6 +18271,11 @@ luxon@^1.23.x:
resolved "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz#e7f96daad3938c06a62de0fb027115d251251fbf"
integrity sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ==
+luxon@^2.4.0:
+ version "2.5.0"
+ resolved "https://registry.npmjs.org/luxon/-/luxon-2.5.0.tgz#098090f67d690b247e83c090267a60b1aa8ea96c"
+ integrity sha512-IDkEPB80Rb6gCAU+FEib0t4FeJ4uVOuX1CQ9GsvU3O+JAGIgu0J7sf1OarXKaKDygTZIoJyU6YdZzTFRu+YR0A==
+
luxon@^3.0.0:
version "3.0.1"
resolved "https://registry.npmjs.org/luxon/-/luxon-3.0.1.tgz#6901111d10ad06fd267ad4e4128a84bef8a77299"
@@ -20166,7 +20176,7 @@ octokit-plugin-create-pull-request@^3.10.0:
dependencies:
"@octokit/types" "^6.8.2"
-octokit@^2.0.0:
+octokit@^2.0.0, octokit@^2.0.4:
version "2.0.4"
resolved "https://registry.npmjs.org/octokit/-/octokit-2.0.4.tgz#cfd3adee6b775d3fa8cd8746590bed36127cc0a0"
integrity sha512-9QvgYGzrSTGmr3koSGtbgeMgqYI20QI0Vv8Bk9y6phchk6L2aHFhcrUOIeNUPj1Z+KZnEBd6A/8faNpDFNfVjg==
@@ -21611,7 +21621,7 @@ prettier@^1.16.4, prettier@^1.19.1:
resolved "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
-prettier@^2.2.1:
+prettier@^2.2.1, prettier@^2.7.1:
version "2.7.1"
resolved "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64"
integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==