@roadiehq/backstage-plugin-jira
Version:
279 lines (276 loc) • 8.33 kB
JavaScript
import { createApiRef } from '@backstage/core-plugin-api';
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;
confluenceActivityFilter;
fetchApi;
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;
this.confluenceActivityFilter = options.configApi.getOptionalString(
"jira.confluenceActivityFilter"
);
this.fetchApi = options.fetchApi;
}
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 pagedIssuesRequest(apiUrl, jql, startAt, maxResults) {
const data = {
jql,
maxResults: maxResults ?? -1,
fields: [
"key",
"issuetype",
"summary",
"status",
"assignee",
"priority",
"parent",
"created",
"updated",
"project"
],
startAt
};
const request = await this.fetchApi.fetch(`${apiUrl}search`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(data)
});
if (!request.ok) {
throw new Error(
`failed to fetch data, status ${request.status}: ${request.statusText}`
);
}
const response = await request.json();
const lastElement = response.startAt + response.maxResults;
return {
issues: response.issues,
next: response.total > lastElement ? lastElement : void 0
};
}
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")
`;
let startAt = 0;
const issues = [];
while (startAt !== void 0) {
const res = await this.pagedIssuesRequest(
apiUrl,
jql,
startAt
);
startAt = res.next;
issues.push(...res.issues);
}
return issues;
}
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)
)
];
}
async getUserDetails(userId) {
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();
let tickets = [];
const jql = `assignee = "${userId}" AND statusCategory in ("To Do", "In Progress")`;
let startAt = 0;
const foundIssues = [];
while (startAt !== void 0) {
const res = 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)
},
tickets
};
}
async jqlQuery(query, maxResults) {
const { apiUrl } = await this.getUrls();
const issues = [];
let startAt = 0;
while (startAt !== void 0) {
const res = await this.pagedIssuesRequest(
apiUrl,
query,
startAt,
maxResults
);
startAt = res.next;
issues.push(...res.issues);
}
return issues;
}
}
export { JiraAPI, jiraApiRef };
//# sourceMappingURL=index.esm.js.map