@roadiehq/backstage-plugin-jira
Version:
374 lines (371 loc) • 12 kB
JavaScript
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
import { useState, useCallback } from 'react';
import { Typography, Dialog, DialogTitle, DialogContent, DialogActions, Button, Chip, Tooltip } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import { Progress, Table, InfoCard, Link } from '@backstage/core-components';
import { useApi, featureFlagsApiRef } from '@backstage/core-plugin-api';
import 'xml-js';
import 'luxon';
import 'uuid';
import 'react-use';
import '../../../api/index.esm.js';
import { useUserInfo } from '../../../hooks/useUserInfo.esm.js';
const useShowLinkedPRs = () => {
const featureFlagsApi = useApi(featureFlagsApiRef);
return featureFlagsApi.isActive("jira-show-linked-prs");
};
const useStyles = makeStyles((theme) => ({
statusChip: {
fontWeight: "bold"
},
titleContainer: {
display: "flex",
alignItems: "center",
gap: theme.spacing(1),
width: "100%",
justifyContent: "space-between"
},
titleText: {
flex: 1,
overflow: "hidden",
textOverflow: "ellipsis"
},
prChip: {
margin: theme.spacing(0.5),
"&.open": { backgroundColor: theme.palette.info.light },
"&.merged": { backgroundColor: theme.palette.success.light },
"&.declined": { backgroundColor: theme.palette.error.light }
},
prCount: {
color: theme.palette.primary.main,
fontWeight: 500,
cursor: "pointer",
"&:hover": {
textDecoration: "underline"
}
},
prDialogContent: {
minWidth: "500px",
maxHeight: "400px",
padding: 0
// Remove all padding
},
truncatedText: {
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
display: "block",
cursor: "default"
}
}));
const LinkedPRsDialog = ({ open, ticket, onClose }) => {
const classes = useStyles();
const pullRequests = ticket.linkedPullRequests || [];
const getStatusClassName = (status) => {
const statusLower = status.toLowerCase();
if (statusLower.includes("open")) return "open";
if (statusLower.includes("merged")) return "merged";
if (statusLower.includes("declined") || statusLower.includes("rejected"))
return "declined";
return "";
};
const columns = [
{
title: "Name",
field: "name",
render: (row) => /* @__PURE__ */ jsx(Link, { to: row.url, target: "_blank", children: row.name })
},
{
title: "Status",
field: "status",
render: (row) => /* @__PURE__ */ jsx(
Chip,
{
label: row.status,
size: "small",
className: `${classes.prChip} ${getStatusClassName(row.status)}`
}
)
},
{
title: "Author",
field: "author",
render: (row) => row.author?.name || "Unknown"
},
{
title: "Last Updated",
field: "lastUpdate",
render: (row) => new Date(row.lastUpdate).toLocaleString()
}
];
return /* @__PURE__ */ jsxs(Dialog, { open, onClose, maxWidth: "md", children: [
/* @__PURE__ */ jsxs(DialogTitle, { style: { paddingBottom: 8 }, children: [
"Linked Pull Requests for ",
ticket.key,
" - ",
ticket.summary
] }),
/* @__PURE__ */ jsx(
DialogContent,
{
className: classes.prDialogContent,
style: { padding: "8px 24px 0" },
children: pullRequests.length > 0 ? /* @__PURE__ */ jsx("div", { style: { display: "flex", justifyContent: "center" }, children: /* @__PURE__ */ jsx(
Table,
{
options: {
paging: false,
padding: "dense",
search: false,
header: true,
toolbar: false,
headerStyle: {
fontWeight: "bold",
textTransform: "uppercase",
fontSize: "0.75rem"
},
tableLayout: "fixed"
},
data: pullRequests,
columns
}
) }) : /* @__PURE__ */ jsx(Typography, { variant: "body2", children: "No pull requests linked to this ticket." })
}
),
/* @__PURE__ */ jsx(DialogActions, { children: /* @__PURE__ */ jsx(Button, { onClick: onClose, color: "primary", children: "Close" }) })
] });
};
const formatTicketsForDisplay = (tickets) => {
return tickets.map((ticket) => {
return {
...ticket,
// Use actual linked PRs data if available, otherwise show '0'
linkedPRs: ticket.linkedPullRequests?.length ? ticket.linkedPullRequests.length.toString() : "0",
// Use real comment data if available, otherwise empty string
lastComment: ticket.lastComment || "",
// Use real assigned date if available, otherwise empty string
assignedDate: ticket.assignedDate || "",
// Use relative time format for display if available
assignedRelativeTime: ticket.assignedRelativeTime || ""
};
});
};
const JiraTicketsTable = ({
user,
tickets
}) => {
const classes = useStyles();
const [selectedTicket, setSelectedTicket] = useState(
null
);
const [prDialogOpen, setPrDialogOpen] = useState(false);
const showLinkedPRs = useShowLinkedPRs();
const handleOpenPrDialog = useCallback((ticket) => {
setSelectedTicket(ticket);
setPrDialogOpen(true);
}, []);
const handleClosePrDialog = useCallback(() => {
setPrDialogOpen(false);
}, []);
const truncateText = (text, maxLength) => {
if (!text) return "";
return text.length > maxLength ? `${text.substring(0, maxLength)}...` : text;
};
const baseColumns = [
{
title: "Issue Key",
field: "key",
width: "120px",
render: (row) => /* @__PURE__ */ jsx(Link, { to: `${user?.url}/browse/${row.key}`, target: "_blank", children: /* @__PURE__ */ jsx(
Typography,
{
variant: "body2",
component: "span",
style: { fontWeight: 600, whiteSpace: "nowrap" },
children: row.key
}
) })
},
{
title: "Summary",
field: "summary",
// Removed highlight: true to make the column header consistent with others
render: (row) => /* @__PURE__ */ jsx("div", { className: classes.titleContainer, children: /* @__PURE__ */ jsx("div", { className: classes.titleText, children: /* @__PURE__ */ jsx(
Tooltip,
{
title: row.summary || "",
enterDelay: 0,
enterNextDelay: 0,
arrow: true,
placement: "top",
children: /* @__PURE__ */ jsx(
Link,
{
to: `${user?.url}/browse/${row.key}`,
target: "_blank",
style: { fontWeight: "normal" },
children: truncateText(row.summary, 50)
}
)
}
) }) })
},
{
title: "Status",
field: "status.name",
width: "120px",
render: (row) => /* @__PURE__ */ jsx(
Chip,
{
label: row.status?.name || "Open",
size: "small",
color: "default",
className: classes.statusChip
}
)
},
// Linked PRs column definition (will be conditionally added)
{
title: "Last Comment",
field: "lastComment",
render: (row) => {
const commentText = row.lastComment || "No comments";
return commentText !== "No comments" ? /* @__PURE__ */ jsx(
Tooltip,
{
title: commentText,
enterDelay: 0,
enterNextDelay: 0,
arrow: true,
placement: "top",
children: /* @__PURE__ */ jsx(
Typography,
{
variant: "body2",
component: "span",
className: classes.truncatedText,
children: truncateText(commentText, 50)
}
)
}
) : /* @__PURE__ */ jsx(Typography, { variant: "body2", component: "span", children: commentText });
}
},
{
title: "Assigned",
field: "assignedRelativeTime",
width: "120px",
render: (row) => /* @__PURE__ */ jsx(
Tooltip,
{
title: row.assignedDate,
enterDelay: 0,
enterNextDelay: 0,
arrow: true,
placement: "top",
children: /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "textSecondary", children: row.assignedRelativeTime })
}
)
}
];
const tableTitle = /* @__PURE__ */ jsxs(Typography, { variant: "h5", style: { display: "flex", alignItems: "center" }, children: [
"My Tickets (",
tickets.length,
")"
] });
const linkedPRsColumn = {
title: "Linked PR(s)",
field: "linkedPRs",
width: "80px",
render: (row) => {
const prCount = parseInt(row.linkedPRs, 10);
return prCount > 0 ? /* @__PURE__ */ jsx(
"span",
{
className: classes.prCount,
onClick: () => handleOpenPrDialog(row),
role: "button",
tabIndex: 0,
onKeyDown: (e) => {
if (e.key === "Enter" || e.key === " ") {
handleOpenPrDialog(row);
}
},
"aria-label": `View ${prCount} linked pull request${prCount > 1 ? "s" : ""}`,
children: row.linkedPRs
}
) : /* @__PURE__ */ jsx("span", { children: "-" });
}
};
const columns = showLinkedPRs ? [...baseColumns.slice(0, 3), linkedPRsColumn, ...baseColumns.slice(3)] : baseColumns;
return /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx("div", { "data-testid": "jira-tickets-table", children: /* @__PURE__ */ jsx(
Table,
{
options: {
paging: true,
padding: "dense",
search: true,
toolbar: true,
header: true,
pageSize: 10,
pageSizeOptions: [5, 10, 25],
headerStyle: {
fontWeight: "bold",
textTransform: "uppercase",
fontSize: "0.75rem"
},
searchFieldAlignment: "right",
searchFieldStyle: {
fontFamily: "Inter, sans-serif"
// Match Backstage font style
}
},
localization: {
toolbar: {
searchPlaceholder: "Search tickets"
}
},
title: tableTitle,
data: tickets,
columns
}
) }),
showLinkedPRs && selectedTicket && /* @__PURE__ */ jsx(
LinkedPRsDialog,
{
ticket: selectedTicket,
open: prDialogOpen,
onClose: handleClosePrDialog
}
)
] });
};
const Content = (props) => {
const { userId } = props;
const showLinkedPRs = useShowLinkedPRs();
const { user, loading, error, tickets } = useUserInfo(userId, {
showLinkedPRs
});
const TicketCard = ({
title,
children
}) => /* @__PURE__ */ jsx("div", { style: { margin: "-20px" }, children: /* @__PURE__ */ jsx(InfoCard, { noPadding: true, title, children }) });
if (loading) return /* @__PURE__ */ jsx(Progress, {});
if (error) {
return /* @__PURE__ */ jsx(TicketCard, { title: "My Tickets", children: /* @__PURE__ */ jsxs(Typography, { color: "error", style: { padding: "16px" }, children: [
"Error loading jira tickets: ",
error?.message
] }) });
}
if (!user) {
return /* @__PURE__ */ jsx(TicketCard, { title: "My Tickets", children: /* @__PURE__ */ jsx(Typography, { style: { padding: "16px" }, children: "User not found" }) });
}
if (!tickets || tickets.length === 0) {
return /* @__PURE__ */ jsx(TicketCard, { title: "My Tickets", children: /* @__PURE__ */ jsx(Typography, { style: { padding: "16px" }, children: "No tickets to show" }) });
}
const enhancedTickets = formatTicketsForDisplay(tickets);
return /* @__PURE__ */ jsx(TicketCard, { children: /* @__PURE__ */ jsx(JiraTicketsTable, { user, tickets: enhancedTickets }) });
};
export { Content };
//# sourceMappingURL=Content.esm.js.map