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 ( + +