UNPKG

@roadiehq/backstage-plugin-jira

Version:
374 lines (371 loc) 12 kB
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