Skip to content

Commit

Permalink
Merge pull request #1610 from hanelka/my-jira-tickets
Browse files Browse the repository at this point in the history
Add My Jira tickets homepage component
  • Loading branch information
Xantier authored Sep 19, 2024
2 parents 6a19519 + 6cd7995 commit 004b020
Show file tree
Hide file tree
Showing 16 changed files with 680 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/home-page-my-jira-tickets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@roadiehq/backstage-plugin-jira': minor
---

Add My Jira Tickets Home Page component
4 changes: 4 additions & 0 deletions packages/app/src/components/home/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
import { Content, PageWithHeader } from '@backstage/core-components';
import { HomepageStoriesCard } from '@roadiehq/backstage-plugin-shortcut';
import { HomePageIFrameCard } from '@roadiehq/backstage-plugin-iframe';
import { HomePageMyJiraTicketsCard } from '@roadiehq/backstage-plugin-jira';

export const HomePage = () => {
return (
Expand Down Expand Up @@ -76,6 +77,9 @@ export const HomePage = () => {
src="https://example.com"
/>
</Grid>
<Grid item xs={12} md={6}>
<HomePageMyJiraTicketsCard userId="roadie" />
</Grid>
</Grid>
</Content>
</PageWithHeader>
Expand Down
29 changes: 29 additions & 0 deletions plugins/frontend/backstage-plugin-jira/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@

## Features

Entity Page components:

- Show project details and tasks
- View JIRA issues of the Project
- Activity Stream

Home Page components:

- Show my assigned JIRA tickets

## How to add Jira project dependency to Backstage app

1. If you have standalone app (i.e., you didn't clone this repo), then do
Expand Down Expand Up @@ -129,6 +135,29 @@ Even though you can use Bearer token please keep in mind that Activity stream fe

3. Save the environmental variable `JIRA_TOKEN` with `Basic ` prefix, eg: `JIRA_TOKEN='Basic amlyYS1tYWlsQGV4YW1wbGUuY29tOmhUQmdxVmNyY3hSWXBUNVRDelRBOUMwRg=='`

## Add Homepage components to your home page

The `HomePageMyJiraTicketsCard` component displays the Open and In Progress JIRA tickets that are assigned to the provided `userId`.
![HomePageMyJiraTicketsCard](./docs/my-jira-tickets-card.png)

To add the component to your Homepage:

```ts
//packages/app/src/components/home/HomePage.tsx
import { HomePageMyJiraTicketsCard } from '@roadiehq/backstage-plugin-jira';
export const HomePage = () => {
return (
...
<Grid item md={6} xs={12}>
<HomePageMyJiraTicketsCard userId="roadie" />
</Grid>
...
);
};
```

## Links

- [Backstage](https://backstage.io)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions plugins/frontend/backstage-plugin-jira/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@backstage/core-components": "^0.14.10",
"@backstage/core-plugin-api": "^1.9.3",
"@backstage/plugin-catalog-react": "^1.12.3",
"@backstage/plugin-home-react": "^0.1.16",
"@backstage/theme": "^0.5.6",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
Expand All @@ -52,6 +53,8 @@
"history": "^5.0.0",
"html-react-parser": "^0.14.1",
"moment": "^2.29.1",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-use": "^17.2.4",
"sanitize-html": "^2.3.3",
"uuid": "8.3.2",
Expand Down
71 changes: 71 additions & 0 deletions plugins/frontend/backstage-plugin-jira/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ import {
Project,
Status,
Ticket,
UserSummary,
User,
TicketSummary,
} from '../types';

export const jiraApiRef = createApiRef<JiraAPI>({
Expand Down Expand Up @@ -69,6 +72,11 @@ export class JiraAPI {
this.fetchApi = options.fetchApi;
}

private getDomainFromApiUrl(apiUrl: string): string {
const url = new URL(apiUrl);
return url.origin;
}

private generateProjectUrl = (url: string) =>
new URL(url).origin +
new URL(url).pathname.replace(/\/rest\/api\/.*$/g, '');
Expand Down Expand Up @@ -102,6 +110,7 @@ export class JiraAPI {
'status',
'assignee',
'priority',
'parent',
'created',
'updated',
],
Expand Down Expand Up @@ -303,4 +312,66 @@ export class JiraAPI {
),
];
}

async getUserDetails(userId: string) {
const { apiUrl } = await this.getUrls();

const request = await this.fetchApi.fetch(
`${apiUrl}user?username=${userId}`,
{
headers: {
'Content-Type': 'application/json',
},
},
);
if (!request.ok) {
throw new Error(
`failed to fetch data, status ${request.status}: ${request.statusText}`,
);
}
const user = (await request.json()) as User;

let tickets: TicketSummary[] = [];

const jql = `assignee = "${userId}" AND statusCategory in ("To Do", "In Progress")`;

let startAt: number | undefined = 0;
const foundIssues: Ticket[] = [];

while (startAt !== undefined) {
const res: IssuesResult = await this.pagedIssuesRequest(
apiUrl,
jql,
startAt,
);
startAt = res.next;
foundIssues.push(...res.issues);
}

tickets = foundIssues.map(index => {
return {
key: index.key,
parent: index?.fields?.parent?.key,
summary: index?.fields?.summary,
assignee: {
displayName: index?.fields?.assignee?.displayName,
avatarUrl: index?.fields?.assignee?.avatarUrls['48x48'],
},
status: index?.fields?.status,
issuetype: index?.fields?.issuetype,
priority: index?.fields?.priority,
created: index?.fields?.created,
updated: index?.fields?.updated,
};
});

return {
user: {
name: user.displayName,
avatarUrl: user.avatarUrls['48x48'],
url: this.getDomainFromApiUrl(user.self),
} as UserSummary,
tickets,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React, { ReactNode, useRef } from 'react';
import { useDrag, useDrop, DragSourceMonitor } from 'react-dnd';

const ItemType = 'CARD';

interface DraggableCardProps {
id: string;
index: number;
moveCard: (dragIndex: number, hoverIndex: number) => void;
children: ReactNode;
}

interface DragItem {
id: string;
index: number;
}

const DraggableCard = ({
id,
index,
moveCard,
children,
}: DraggableCardProps) => {
const ref = useRef<HTMLDivElement>(null);

const [, drop] = useDrop<DragItem>({
accept: ItemType,
hover(item, monitor) {
if (!ref.current) {
return;
}
const dragIndex = item.index;
const hoverIndex = index;

if (dragIndex === hoverIndex) {
return;
}

const hoverBoundingRect = ref.current?.getBoundingClientRect();
const hoverMiddleY =
(hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
const clientOffset = monitor.getClientOffset();
const hoverClientY = clientOffset!.y - hoverBoundingRect.top;

if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return;
}

if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return;
}

moveCard(dragIndex, hoverIndex);
item.index = hoverIndex;
},
});

const [{ isDragging }, drag] = useDrag({
type: ItemType,
item: { id, index },
collect: (monitor: DragSourceMonitor) => ({
isDragging: monitor.isDragging(),
}),
});

drag(drop(ref));

return (
<div ref={ref} style={{ opacity: isDragging ? 0.5 : 1 }}>
{children}
</div>
);
};

export default DraggableCard;
Loading

0 comments on commit 004b020

Please sign in to comment.