diff --git a/packages/app/package.json b/packages/app/package.json index 738dbc2246..d1ab52a6a8 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -33,6 +33,7 @@ "@backstage/plugin-gcalendar": "^0.3.4-next.0", "@backstage/plugin-gcp-projects": "^0.3.27-next.0", "@backstage/plugin-github-actions": "^0.5.8-next.0", + "@backstage/plugin-github-issues": "^0.0.0", "@backstage/plugin-gocd": "^0.1.14-next.0", "@backstage/plugin-graphiql": "^0.2.40-next.0", "@backstage/plugin-home": "^0.4.24-next.0", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 289af8d40f..bc1d86b650 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -100,6 +100,7 @@ import { techDocsPage } from './components/techdocs/TechDocsPage'; import { ApacheAirflowPage } from '@backstage/plugin-apache-airflow'; import { PermissionedRoute } from '@backstage/plugin-permission-react'; import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common'; +import { GitHubIssuesPage } from '@backstage/plugin-github-issues'; const app = createApp({ apis, @@ -241,6 +242,7 @@ const routes = ( } /> } /> + } /> ); diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 288196266e..166157a2bc 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -150,6 +150,11 @@ import { ReportIssue, } from '@backstage/plugin-techdocs-module-addons-contrib'; +import { + GitHubIssuesCard, + GitHubIssuesPage, +} from '@backstage/plugin-github-issues'; + const customEntityFilterKind = ['Component', 'API', 'System']; const EntityLayoutWrapper = (props: { children?: ReactNode }) => { @@ -325,6 +330,10 @@ const overviewContent = ( + + + + @@ -624,6 +633,9 @@ const userPage = ( entityFilterKind={customEntityFilterKind} /> + + + @@ -646,8 +658,14 @@ const groupPage = ( + + + + + + ); 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..65757bef00 --- /dev/null +++ b/plugins/github-issues/README.md @@ -0,0 +1,13 @@ +# github-issues + +Welcome to the github-issues plugin! + +_This plugin was created through the Backstage CLI_ + +## Getting started + +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/github-issues](http://localhost:3000/github-issues). + +You can also serve the plugin in isolation by running `yarn start` in the plugin directory. +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. +It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. diff --git a/plugins/github-issues/dev/index.tsx b/plugins/github-issues/dev/index.tsx new file mode 100644 index 0000000000..4de5a78b2f --- /dev/null +++ b/plugins/github-issues/dev/index.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 { createDevApp } from '@backstage/dev-utils'; +import { gitHubIssuesPlugin, GitHubIssuesPage } from '../src/plugin'; + +createDevApp() + .registerPlugin(gitHubIssuesPlugin) + .addPage({ + element: , + title: 'Root Page', + path: '/github-issues', + }) + .render(); diff --git a/plugins/github-issues/package.json b/plugins/github-issues/package.json new file mode 100644 index 0000000000..ab2f7b914e --- /dev/null +++ b/plugins/github-issues/package.json @@ -0,0 +1,60 @@ +{ + "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.9.5", + "@backstage/core-plugin-api": "^1.0.3", + "@backstage/integration": "^1.2.1", + "@backstage/plugin-catalog-react": "^1.1.1", + "@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", + "@octokit/rest": "^18.12.0", + "luxon": "^2.4.0", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.17.2", + "@backstage/core-app-api": "^1.0.3", + "@backstage/dev-utils": "^1.0.3", + "@backstage/test-utils": "^1.1.1", + "@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": "*", + "cross-fetch": "^3.1.5", + "msw": "^0.42.0", + "prettier": "^2.7.1" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/github-issues/src/components/GitHubIssues/GitHubIssues.test.tsx b/plugins/github-issues/src/components/GitHubIssues/GitHubIssues.test.tsx new file mode 100644 index 0000000000..ebb9c78f19 --- /dev/null +++ b/plugins/github-issues/src/components/GitHubIssues/GitHubIssues.test.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 React from 'react'; +import { GitHubIssues } from './GitHubIssues'; +import { ThemeProvider } from '@material-ui/core'; +import { lightTheme } from '@backstage/theme'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { + setupRequestMockHandlers, + renderInTestApp, +} from '@backstage/test-utils'; + +// test for repo duplicates + +describe('GitHubIssues', () => { + const server = setupServer(); + // Enable sane handlers for network requests + setupRequestMockHandlers(server); + + // setup mock response + beforeEach(() => { + server.use( + rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))), + ); + }); + + it('should render', async () => { + const rendered = await renderInTestApp( + + + , + ); + expect(rendered.getByText('Welcome to github-issues!')).toBeInTheDocument(); + }); +}); 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..da31869000 --- /dev/null +++ b/plugins/github-issues/src/components/GitHubIssues/GitHubIssues.tsx @@ -0,0 +1,150 @@ +/* + * 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'; + +export type PluginMode = 'page' | 'card'; + +export type Props = { + mode: PluginMode; + itemsPerPage?: number; + itemsPerRepo?: number; +}; + +export const GitHubIssues = ({ + itemsPerPage = 10, + itemsPerRepo = 40, +}: Props) => { + const [isLoading, setIsLoading] = React.useState(true); + const [activeFilter, setActiveFilter] = React.useState>([]); + + const [issuesByRepository, setIssuesByRepository] = + React.useState>(); + + const { repositories } = useEntityGitHubRepositories(); + const getIssues = useGetIssuesByRepoFromGitHub(); + + const filters = React.useMemo( + () => + issuesByRepository + ? Object.keys(issuesByRepository) + .filter(repo => issuesByRepository[repo].issues.totalCount > 0) + .map(repo => ({ + label: `${repo} (${issuesByRepository[repo].issues.totalCount})`, + 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 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..e6abdbb6fe --- /dev/null +++ b/plugins/github-issues/src/components/GitHubIssues/IssueCard/Assignees.tsx @@ -0,0 +1,61 @@ +/* + * 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), + }, + noAssignees: { + height: theme.spacing(4), + }, +})); + +export const Assignees: FunctionComponent = (props: Props) => { + const { name, avatar } = props; + const classes = useStyles(); + + // todo: many assignees -> NUM assignees + stock images on each other + return name ? ( + + + {name} + + + + ) : ( + + + No assignees + + + ); +}; + +export default 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..d3e7f355e0 --- /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, { FunctionComponent } from 'react'; +import { ChatIcon } from '@backstage/core-components'; +import { Box, Badge } from '@material-ui/core'; + +type Props = { + commentsCount: number; +}; + +export const CommentsCount: FunctionComponent = (props: Props) => { + 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..73010d50c9 --- /dev/null +++ b/plugins/github-issues/src/components/GitHubIssues/IssueCard/IssueCard.tsx @@ -0,0 +1,93 @@ +/* + * 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 { DateTime } from 'luxon'; + +import { Box, Paper, Typography, CardActionArea } from '@material-ui/core'; +import Assignees from './Assignees'; +import { CommentsCount } from './CommentsCount'; + +import Divider from '@material-ui/core/Divider'; + +type Props = { + 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: FunctionComponent = (props: Props) => { + 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..7f654dd9e5 --- /dev/null +++ b/plugins/github-issues/src/components/GitHubIssues/IssuesList/Filters/Filters.tsx @@ -0,0 +1,64 @@ +/* + * 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 } from '@backstage/core-components'; +import { SelectedItems } from '@backstage/core-components'; +import { makeStyles, Box } from '@material-ui/core'; + +export type FilterItem = { + label: string; + value: string; +}; + +type Props = { + items: Array; + totalIssuesInGitHub: number; + onChange: (active: Array) => void; +}; + +const useStyles = makeStyles(theme => ({ + filters: { + margin: theme.spacing(0, 0, 2, 0), + '& > div': { + width: '600px', + '& > div': { + maxWidth: '600px', + }, + }, + }, +})); + +const checkSelectedItems: ( + onChange: (active: Array) => void, +) => (active: SelectedItems) => void = onChange => active => { + return onChange(active as Array); +}; + +export const Filters = ({ items, totalIssuesInGitHub, onChange }: Props) => { + const css = useStyles(); + + return ( + +