UNPKG

@roadiehq/backstage-plugin-jira

Version:
322 lines (319 loc) 10.8 kB
import { createApiRef } from '@backstage/core-plugin-api'; import { DateTime } from 'luxon'; import { JiraProductStrategyFactory } from './strategies/factory.esm.js'; const jiraApiRef = createApiRef({ id: "plugin.jira.service" }); const DEFAULT_PROXY_PATH = "/jira/api"; const DEFAULT_REST_API_VERSION = "latest"; const DONE_STATUS_CATEGORY = "Done"; class JiraAPI { discoveryApi; proxyPath; apiVersion; strategy; confluenceActivityFilter; fetchApi; errorApi; constructor(options) { this.discoveryApi = options.discoveryApi; const proxyPath = options.configApi.getOptionalString("jira.proxyPath"); this.proxyPath = proxyPath ?? DEFAULT_PROXY_PATH; const apiVersion = options.configApi.getOptionalNumber("jira.apiVersion"); this.apiVersion = apiVersion ? apiVersion.toString() : DEFAULT_REST_API_VERSION; const product = options.configApi.getOptionalString("jira.product") ?? "cloud"; this.strategy = JiraProductStrategyFactory.createStrategy(product, options); this.confluenceActivityFilter = options.configApi.getOptionalString( "jira.confluenceActivityFilter" ); this.fetchApi = options.fetchApi; this.errorApi = options.errorApi; } // Helper method to log errors in a consistent way logError(message, error) { if (this.errorApi) { this.errorApi.post(new Error(`${message}: ${error.message}`)); } } getDomainFromApiUrl(apiUrl) { const url = new URL(apiUrl); return url.origin; } generateProjectUrl = (url) => new URL(url).origin + new URL(url).pathname.replace(/\/rest\/api\/.*$/g, ""); async getUrls() { const proxyUrl = await this.discoveryApi.getBaseUrl("proxy"); return { apiUrl: `${proxyUrl}${this.proxyPath}/rest/api/${this.apiVersion}/`, baseUrl: `${proxyUrl}${this.proxyPath}` }; } convertToString = (arrayElement) => arrayElement.filter(Boolean).map((i) => `'${i}'`).join(","); async getIssuesPaged({ apiUrl, projectKey, component, label, statusesNames }) { const statusesString = this.convertToString(statusesNames); const jql = `project = "${projectKey}" ${statusesString ? `AND status in (${statusesString})` : ""} ${component ? `AND component = "${component}"` : ""} ${label ? `AND labels in (${label})` : ""} AND statuscategory not in ("Done") `; return this.strategy.pagedIssuesRequest(apiUrl, jql); } async getProjectDetails(projectKey, component, label, statusesNames) { const { apiUrl } = await this.getUrls(); const request = await this.fetchApi.fetch( `${apiUrl}project/${projectKey}`, { headers: { "Content-Type": "application/json" } } ); if (!request.ok) { throw new Error( `failed to fetch data, status ${request.status}: ${request.statusText}` ); } const project = await request.json(); const foundIssues = await this.getIssuesPaged({ apiUrl, projectKey, component, label, statusesNames }); const issuesCounter = project.issueTypes.filter((issueType) => issueType.name !== "Sub-task").map( (issueType) => ({ name: issueType.name, iconUrl: issueType.iconUrl, total: foundIssues.filter( (issue) => issue.fields?.issuetype.name === issueType.name ).length }) ); const ticketIds = foundIssues.map((issue) => issue.key); const 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, iconUrl: project.avatarUrls["48x48"], type: project.projectTypeKey, url: this.generateProjectUrl(project.self) }, issues: issuesCounter, ticketIds, tickets }; } async getActivityStream(size, projectKey, componentName, ticketIds, label, isBearerAuth) { const { baseUrl } = await this.getUrls(); let filterUrl = `streams=key+IS+${projectKey}`; if (ticketIds && (componentName || label)) { filterUrl += `&streams=issue-key+IS+${ticketIds.join("+")}`; filterUrl += this.confluenceActivityFilter ? `&${this.confluenceActivityFilter}=activity+IS+NOT+*` : ""; } const request = await this.fetchApi.fetch( isBearerAuth ? `${baseUrl}/activity?maxResults=${size}&${filterUrl}` : `${baseUrl}/activity?maxResults=${size}&${filterUrl}&os_authType=basic`, {} ); if (!request.ok) { throw new Error( `failed to fetch data, status ${request.status}: ${request.statusText}` ); } const activityStream = await request.text(); return activityStream; } async getStatuses(projectKey) { const { apiUrl } = await this.getUrls(); const request = await this.fetchApi.fetch( `${apiUrl}project/${projectKey}/statuses`, { headers: { "Content-Type": "application/json" } } ); if (!request.ok) { throw new Error( `failed to fetch data, status ${request.status}: ${request.statusText}` ); } const statuses = await request.json(); return [ ...new Set( statuses.flatMap((status) => status.statuses).filter( (status) => status.statusCategory?.name !== DONE_STATUS_CATEGORY ).map((it) => it.name) ) ]; } // Fetch detailed issue information including changelog and comments async getIssueDetails(issueKey) { const { apiUrl } = await this.getUrls(); const request = await this.fetchApi.fetch( `${apiUrl}issue/${issueKey}?expand=changelog`, { headers: { "Content-Type": "application/json" } } ); if (!request.ok) { throw new Error( `Failed to fetch issue details, status ${request.status}: ${request.statusText}` ); } return await request.json(); } // Fetch pull request information linked to a Jira issue async getLinkedPullRequests(issueId) { const { baseUrl } = await this.getUrls(); try { const request = await this.fetchApi.fetch( `${baseUrl}/rest/dev-status/1.0/issue/detail?issueId=${issueId}&applicationType=stash&dataType=pullrequest`, { headers: { "Content-Type": "application/json" } } ); if (!request.ok) { throw new Error( `Failed to fetch linked PRs, status ${request.status}: ${request.statusText}` ); } const response = await request.json(); return response.detail?.[0]?.pullRequests || []; } catch (error) { this.logError(`Error fetching linked PRs for ${issueId}`, error); return []; } } async getUserDetails(userId, fetchLinkedPRs) { 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(); const jql = `assignee = "${userId}" AND statusCategory in ("To Do", "In Progress") ORDER BY updated desc`; const foundIssues = await this.strategy.pagedIssuesRequest(apiUrl, jql); const enhancedTickets = await Promise.all( foundIssues.map(async (index) => { let lastComment = ""; let assignedDate = ""; let assignedRelativeTime = ""; let linkedPullRequests = []; try { const details = await this.getIssueDetails(index.key); if (details.fields?.comment?.comments?.length > 0) { const comments = details.fields.comment.comments; lastComment = comments[comments.length - 1].body; } if (details.changelog?.histories?.length > 0) { const assignmentHistory = details.changelog.histories.filter( (h) => h.items.some( (i) => i.field === "assignee" && i.to === userId ) ).sort( (a, b) => new Date(b.created).getTime() - new Date(a.created).getTime() ); if (assignmentHistory.length > 0) { assignedDate = new Date( assignmentHistory[0].created ).toLocaleDateString(); assignedRelativeTime = DateTime.fromISO(assignmentHistory[0].created).toRelative() || ""; } } if (index.id && fetchLinkedPRs) { try { const prs = await this.getLinkedPullRequests(index.id); linkedPullRequests = prs.map((pr) => ({ id: pr.id, name: pr.name, url: pr.url, status: pr.status, lastUpdate: pr.lastUpdate, author: pr.author ? { name: pr.author.name, avatar: pr.author.avatar } : void 0 })); } catch (prError) { this.logError( `Error fetching PRs for ${index.key}`, prError ); } } } catch (error) { this.logError( `Error fetching details for ${index.key}`, error ); } return { key: index.key, id: index.id, 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, lastComment, assignedDate, assignedRelativeTime, linkedPullRequests }; }) ); return { user: { name: user.displayName, avatarUrl: user.avatarUrls["48x48"], url: this.getDomainFromApiUrl(user.self) }, tickets: enhancedTickets }; } async jqlQuery(query, maxResults) { const { apiUrl } = await this.getUrls(); return this.strategy.pagedIssuesRequest(apiUrl, query, maxResults); } } export { JiraAPI, jiraApiRef }; //# sourceMappingURL=index.esm.js.map