diff --git a/.changeset/swift-badgers-train.md b/.changeset/swift-badgers-train.md new file mode 100644 index 000000000..656d7eb70 --- /dev/null +++ b/.changeset/swift-badgers-train.md @@ -0,0 +1,5 @@ +--- +'@roadiehq/backstage-plugin-jira': minor +--- + +Included the Issues view for the JIRA Plugin diff --git a/plugins/frontend/backstage-plugin-jira/README.md b/plugins/frontend/backstage-plugin-jira/README.md index a90345584..b821896a0 100644 --- a/plugins/frontend/backstage-plugin-jira/README.md +++ b/plugins/frontend/backstage-plugin-jira/README.md @@ -1,10 +1,12 @@ # Jira Plugin for Backstage ![a Jira plugin for Backstage](./docs/jira-plugin.gif). +![a Jira plugin Issues View for Backstage](./docs/jira-plugin-issues-view.png). ## Features - Show project details and tasks +- View JIRA issues of the Project - Activity Stream ## How to add Jira project dependency to Backstage app diff --git a/plugins/frontend/backstage-plugin-jira/docs/jira-plugin-issues-view.png b/plugins/frontend/backstage-plugin-jira/docs/jira-plugin-issues-view.png new file mode 100644 index 000000000..addc3b93d Binary files /dev/null and b/plugins/frontend/backstage-plugin-jira/docs/jira-plugin-issues-view.png differ diff --git a/plugins/frontend/backstage-plugin-jira/src/api/index.ts b/plugins/frontend/backstage-plugin-jira/src/api/index.ts index 719b37b35..3eb46f797 100644 --- a/plugins/frontend/backstage-plugin-jira/src/api/index.ts +++ b/plugins/frontend/backstage-plugin-jira/src/api/index.ts @@ -21,13 +21,13 @@ import { IdentityApi, } from '@backstage/core-plugin-api'; import { - IssueCountElement, IssueCountResult, IssueCountSearchParams, IssuesCounter, IssueType, Project, Status, + Ticket, } from '../types'; import fetch from 'cross-fetch'; @@ -96,7 +96,16 @@ export class JiraAPI { const data = { jql, maxResults: -1, - fields: ['key', 'issuetype'], + fields: [ + 'key', + 'issuetype', + 'summary', + 'status', + 'assignee', + 'priority', + 'created', + 'updated', + ], startAt, }; @@ -146,7 +155,7 @@ export class JiraAPI { `; let startAt: number | undefined = 0; - const issues: IssueCountElement[] = []; + const issues: Ticket[] = []; while (startAt !== undefined) { const res: IssueCountResult = await this.pagedIssueCountRequest( @@ -240,7 +249,14 @@ export class JiraAPI { // to fetch also the issue-keys of all the tasks for that component. let issuesCounter: IssuesCounter[] = []; let ticketIds: string[] = []; - + let tickets: Ticket[] = []; + const foundIssues = await this.getIssueCountPaged({ + apiUrl, + projectKey, + component, + label, + statusesNames, + }); if (!component && !label) { // Generate counters for each issue type const issuesTypes = project.issueTypes.map((status: IssueType) => ({ @@ -275,17 +291,9 @@ export class JiraAPI { total: 0, }), ); - const foundIssues = await this.getIssueCountPaged({ - apiUrl, - projectKey, - component, - label, - statusesNames, - }); - issuesCounter = foundIssues .reduce((prev, curr) => { - const name = curr.fields.issuetype.name; + const name = curr.fields?.issuetype.name; const idx = issuesTypes.findIndex(i => i.name === name); if (idx !== -1) { issuesTypes[idx].total++; @@ -296,7 +304,20 @@ export class JiraAPI { ticketIds = foundIssues.map(i => i.key); } - + tickets = foundIssues.map(index => { + return { + key: index.key, + summary: index?.fields?.summary, + assignee: { + displayName: index?.fields?.assignee?.displayName, + avatarUrl: index?.fields?.assignee?.avatarUrls['48x48'], + }, + status: index?.fields?.status?.name, + priority: index?.fields?.priority, + created: index?.fields?.created, + updated: index?.fields?.updated, + }; + }); return { project: { name: project.name, @@ -311,6 +332,7 @@ export class JiraAPI { })) : [], ticketIds: ticketIds, + tickets, }; } diff --git a/plugins/frontend/backstage-plugin-jira/src/components/JiraCard/JiraCard.test.tsx b/plugins/frontend/backstage-plugin-jira/src/components/JiraCard/JiraCard.test.tsx index 0e0bb3e24..6866dccb2 100644 --- a/plugins/frontend/backstage-plugin-jira/src/components/JiraCard/JiraCard.test.tsx +++ b/plugins/frontend/backstage-plugin-jira/src/components/JiraCard/JiraCard.test.tsx @@ -101,7 +101,7 @@ describe('JiraCard', () => { expect.stringContaining('mocked_icon_filename.gif'), ); expect( - await rendered.findByText(/filter issue status/), + await rendered.findByText(/Filter issue status/), ).toBeInTheDocument(); }); diff --git a/plugins/frontend/backstage-plugin-jira/src/components/JiraCard/JiraCard.tsx b/plugins/frontend/backstage-plugin-jira/src/components/JiraCard/JiraCard.tsx index fab9ab933..2030f5a6b 100644 --- a/plugins/frontend/backstage-plugin-jira/src/components/JiraCard/JiraCard.tsx +++ b/plugins/frontend/backstage-plugin-jira/src/components/JiraCard/JiraCard.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Avatar, Box, @@ -28,6 +28,14 @@ import { MenuItem, Checkbox, Divider, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TablePagination, + Button, } from '@material-ui/core'; import Alert from '@material-ui/lab/Alert'; import { InfoCard, Progress } from '@backstage/core-components'; @@ -39,6 +47,7 @@ import { ActivityStream } from './components/ActivityStream'; import { Selectors } from './components/Selectors'; import { useEmptyIssueTypeFilter } from '../../hooks/useEmptyIssueTypeFilter'; import MoreVertIcon from '@material-ui/icons/MoreVert'; +import ArrowForwardIcon from '@material-ui/icons/ArrowForward'; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -55,6 +64,14 @@ const useStyles = makeStyles((theme: Theme) => marginTop: theme.spacing(1), }, }, + ticketLink: { + color: theme.palette.link, + textDecoration: 'none', + transition: 'color 0.3s', + '&:hover': { + color: theme.palette.linkHover, + }, + }, }), ); @@ -87,6 +104,7 @@ export const JiraCard = (props: EntityProps & JiraCardOptionalProps) => { const { project, issues, + tickets, ticketIds, projectLoading, projectError, @@ -99,6 +117,12 @@ export const JiraCard = (props: EntityProps & JiraCardOptionalProps) => { } = useEmptyIssueTypeFilter(issues); const [anchorEl, setAnchorEl] = React.useState(null); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + + useEffect(() => { + setPage(0); + }, [tickets]); const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); @@ -114,41 +138,52 @@ export const JiraCard = (props: EntityProps & JiraCardOptionalProps) => { title="Jira" subheader={ project && ( - - - - - - - + + + + + + + + + + <>Show empty issue types + + + + + + + Open in JIRA + ) } - deepLink={{ - link: `${project?.url}/browse/${projectKey}`, - title: 'Go to project', - onClick: e => { - e.preventDefault(); - window.open(`${project?.url}/browse/${projectKey}`); - }, - }} > {projectLoading && !(project && issues) ? : null} {projectError ? ( @@ -183,6 +218,94 @@ export const JiraCard = (props: EntityProps & JiraCardOptionalProps) => { ))} + + + + + Key + Summary + Priority + Status + Created + Updated + Assignee + + + + {tickets && tickets.length > 0 ? ( + tickets + .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + .map((ticket: any) => ( + + + + {ticket?.key} + + + {ticket?.summary} + + {ticket?.priority?.name} + + {ticket?.status} + + {new Date(ticket?.created).toLocaleDateString()} + + + {new Date(ticket?.updated).toLocaleDateString()} + + + {ticket?.assignee?.displayName ? ( + <> + {ticket?.assignee?.displayName} + {ticket?.assignee?.displayName} + + ) : ( + Not Assigned + )} + + + )) + ) : ( + + + No Jira tickets available. + + + )} + +
+ setPage(newPage)} + onRowsPerPageChange={event => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }} + /> +
= 2 ? ( - + - filter issue status + Filter issue status