Skip to content

Commit

Permalink
Enable Support for Multiple Jira Project Keys
Browse files Browse the repository at this point in the history
  • Loading branch information
SaachiNayyer committed Dec 20, 2024
1 parent a5e56e4 commit ed5823c
Show file tree
Hide file tree
Showing 8 changed files with 231 additions and 51 deletions.
8 changes: 8 additions & 0 deletions .changeset/twenty-apples-sit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@axis-backstage/plugin-jira-dashboard-backend': minor
---

Support Multiple Project Keys in JQL Query Builder
Issue https://github.com/AxisCommunications/backstage-plugins/issues/232

Signed-off-by: enaysaa <[email protected]>
2 changes: 1 addition & 1 deletion plugins/jira-dashboard-backend/api-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const jqlQueryBuilder: ({

// @public
export type JqlQueryBuilderArgs = {
project: string;
project: string | string[];
components?: string[];
query?: string;
};
Expand Down
114 changes: 109 additions & 5 deletions plugins/jira-dashboard-backend/src/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import fetch from 'node-fetch';

import { mockServices } from '@backstage/backend-test-utils';

import { getIssuesByComponent, getProjectAvatar } from './api';
import { JiraConfig } from './config';
import { JiraProject } from './lib';

jest.mock('node-fetch', () => jest.fn());
afterEach(() => {
jest.clearAllMocks();
});

describe('api', () => {
const mockConfig = mockServices.rootConfig({
Expand Down Expand Up @@ -47,27 +49,129 @@ describe('api', () => {
() => response as any,
);

const issues = await getIssuesByComponent(
const projects = [
{
instance,
fullProjectKey: 'default/ppp',
projectKey: 'ppp',
},
'ccc',
{
instance,
fullProjectKey: 'default/bbb',
projectKey: 'bbb',
},
];

const issues = await getIssuesByComponent(projects, 'ccc');

expect(fetch).toHaveBeenCalledWith(
"http://jira.com/search?jql=project in (ppp,bbb) AND component in ('ccc')",
{
method: 'GET',
headers: {
Accept: 'application/json',
'Custom-Header': 'custom value',
Authorization: 'token',
},
},
);

expect(issues).toEqual(['issue1']);
});
it('should handle no projects gracefully', async () => {
const projects: JiraProject[] = [];
const issues = await getIssuesByComponent(projects, 'ccc');

expect(fetch).not.toHaveBeenCalled();
expect(issues).toEqual([]);
});
it('should handle a single project correctly', async () => {
const projects = [
{
instance,
fullProjectKey: 'default/ppp',
projectKey: 'ppp',
},
];
const issues = await getIssuesByComponent(projects, 'ccc');

expect(fetch).toHaveBeenCalledWith(
"http://jira.com/search?jql=project in (ppp) AND component in ('ccc')",
{
method: 'GET',
headers: {
Accept: 'application/json',
'Custom-Header': 'custom value',
Authorization: 'token',
'Custom-Header': 'custom value',
},
},
);
expect(issues).toEqual(['issue1']);
});
it('should handle multiple components correctly', async () => {
const projects = [
{
instance,
fullProjectKey: 'default/ppp',
projectKey: 'ppp',
},
{
instance,
fullProjectKey: 'default/bbb',
projectKey: 'bbb',
},
];
const issues = await getIssuesByComponent(projects, 'ccc,ddd');

expect(fetch).toHaveBeenCalledWith(
"http://jira.com/search?jql=project in (ppp,bbb) AND component in ('ccc','ddd')",
{
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'token',
'Custom-Header': 'custom value',
},
},
);
expect(issues).toEqual(['issue1']);
});
it('should handle invalid project keys', async () => {
const projects = [
{
instance,
fullProjectKey: 'default/invalid',
projectKey: 'invalid',
},
];

// Mock the fetch call to simulate a Jira response for an invalid project key
(fetch as unknown as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({
ok: false,
status: 400,
json: () =>
Promise.resolve({
errorMessages: [
"The value 'invalid' does not exist for the field 'project'",
],
}),
}),
);

const issues = await getIssuesByComponent(projects, 'ccc');

expect(fetch).toHaveBeenCalledWith(
"http://jira.com/search?jql=project in (invalid) AND component in ('ccc')",
{
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'token',
'Custom-Header': 'custom value',
},
},
);
expect(issues).toEqual([]);
});
});
85 changes: 54 additions & 31 deletions plugins/jira-dashboard-backend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,23 +51,27 @@ export const getFilterById = async (
};

export const getIssuesByFilter = async (
project: JiraProject,
projects: JiraProject[],
components: string[],
query: string,
): Promise<Issue[]> => {
const { projectKey, instance } = project;
const jql = jqlQueryBuilder({ project: projectKey, components, query });
const response = await callApi(
instance,
`${instance.baseUrl}search?jql=${jql}`,
{
method: 'GET',
headers: {
Accept: 'application/json',
const issues: Issue[] = [];
for (const project of projects) {
const { projectKey, instance } = project;
const jql = jqlQueryBuilder({ project: [projectKey], components, query });
const response = await callApi(
instance,
`${instance.baseUrl}search?jql=${jql}`,
{
method: 'GET',
headers: {
Accept: 'application/json',
},
},
},
).then(resp => resp.json());
return response.issues;
).then(resp => resp.json());
issues.push(...response.issues);
}
return issues;
};

/**
Expand Down Expand Up @@ -117,28 +121,47 @@ export const searchJira = async (
};

export const getIssuesByComponent = async (
project: JiraProject,
componentKey: string,
projects: JiraProject[],
componentKeys: string,
): Promise<Issue[]> => {
const { projectKey, instance } = project;
// Return an empty array if no projects are provided
if (projects.length === 0) {
return [];
}

const jql = jqlQueryBuilder({
project: projectKey,
components: [componentKey],
});
const response = await callApi(
instance,
`${instance.baseUrl}search?jql=${jql}`,
{
method: 'GET',
headers: {
Accept: 'application/json',
const projectKeys = projects.map(project => project.projectKey).join(',');
const components = componentKeys
.split(',')
.map(component => `'${component.trim()}'`)
.join(',');

const jql = `project in (${projectKeys}) AND component in (${components})`;
const { instance } = projects[0];

try {
const response = await callApi(
instance,
`${instance.baseUrl}search?jql=${jql}`,
{
method: 'GET',
headers: {
Accept: 'application/json',
},
},
},
).then(resp => resp.json());
return response.issues;
};
).then(resp => resp.json());

if (!response.issues || response.issues.length === 0) {
return [];
}

return response.issues;
} catch (error: any) {
if (error.message.includes("does not exist for the field 'project'")) {
return [];
}
throw error;
}
};
export async function getProjectAvatar(url: string, instance: ConfigInstance) {
return callApi(instance, url);
}
Expand Down
4 changes: 2 additions & 2 deletions plugins/jira-dashboard-backend/src/queries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { jqlQueryBuilder } from './queries';
describe('queries', () => {
it('can use all arguments build a query.', async () => {
const jql = jqlQueryBuilder({
project: 'BS',
project: 'BS,SB',
components: ['comp 1', 'comp 2'],
query: 'filter=example',
});
expect(jql).toBe(
"project in (BS) AND component in ('comp 1','comp 2') AND filter=example",
"project in (BS,SB) AND component in ('comp 1','comp 2') AND filter=example",
);
});
it('can create a query using only a project as argument.', async () => {
Expand Down
5 changes: 3 additions & 2 deletions plugins/jira-dashboard-backend/src/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* @public
*/
export type JqlQueryBuilderArgs = {
project: string;
project: string | string[];
components?: string[];
query?: string;
};
Expand All @@ -18,7 +18,8 @@ export const jqlQueryBuilder = ({
components,
query,
}: JqlQueryBuilderArgs) => {
let jql = `project in (${project})`;
const projectList = Array.isArray(project) ? project : [project];
let jql = `project in (${projectList.join(',')})`;
if (components && components.length > 0) {
let componentsInclude = '(';
for (let index = 0; index < components.length; index++) {
Expand Down
18 changes: 14 additions & 4 deletions plugins/jira-dashboard-backend/src/service/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,21 +176,31 @@ export async function createRouter(
)),
);
}
const instance = projects[0]?.instance;

let components =
entity.metadata.annotations?.[componentsAnnotation]?.split(',') ?? [];
let issues = await getIssuesFromFilters(projects[0], components, filters);
const projectKeys = projects.map(project => project.projectKey);
let issues = await getIssuesFromFilters(
projectKeys,
components,
filters,
instance,
cache,
);

/* Adding support for Roadie's component annotation */
/* Adding support for Roadie's component annotation */
components = components.concat(
entity.metadata.annotations?.[componentRoadieAnnotation]?.split(',') ??
[],
);

if (components) {
if (components.length > 0) {
const componentIssues = await getIssuesFromComponents(
projects[0],
projectKeys,
components,
instance,
cache,
);
issues = issues.concat(componentIssues);
}
Expand Down
Loading

0 comments on commit ed5823c

Please sign in to comment.