Skip to content

Commit 4376da7

Browse files
Hartesicsonartech
authored andcommitted
SONAR-25925 Add Jira integration to Project settings in SQS
GitOrigin-RevId: cea38b672c65ce382435d6aea7bcb0b2f26f6367
1 parent 1c99e10 commit 4376da7

File tree

11 files changed

+152
-28
lines changed

11 files changed

+152
-28
lines changed

apps/sq-server/src/main/js/apps/settings/components/AdditionalCategories.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020

2121
import * as React from 'react';
2222
import { ExtendedSettingDefinition } from '~shared/types/settings';
23+
import { addons } from '~sq-server-addons/index';
2324
import { NEW_CODE_PERIOD_CATEGORY } from '~sq-server-commons/constants/settings';
2425
import { translate } from '~sq-server-commons/helpers/l10n';
26+
import { Feature } from '~sq-server-commons/types/features';
2527
import { Component } from '~sq-server-commons/types/types';
2628
import {
2729
ADVANCED_SECURITY_CATEGORY,
@@ -31,6 +33,7 @@ import {
3133
AUTHENTICATION_CATEGORY,
3234
EARLY_ACCESS_FEATURES_CATEGORY,
3335
EMAIL_NOTIFICATION_CATEGORY,
36+
JIRA_PROJECT_BINDING_CATEGORY,
3437
LANGUAGES_CATEGORY,
3538
MODE_CATEGORY,
3639
PULL_REQUEST_DECORATION_BINDING_CATEGORY,
@@ -61,6 +64,7 @@ export interface AdditionalCategory {
6164
key: string;
6265
name: string;
6366
renderComponent: (props: AdditionalCategoryComponentProps) => React.ReactNode;
67+
requiredFeatures?: Feature[];
6468
requiresBranchSupport?: boolean;
6569
}
6670

@@ -172,6 +176,15 @@ export const ADDITIONAL_CATEGORIES: AdditionalCategory[] = [
172176
availableForProject: false,
173177
displayTab: true,
174178
},
179+
{
180+
key: JIRA_PROJECT_BINDING_CATEGORY,
181+
name: translate('project_settings.category.jira_binding'),
182+
renderComponent: getProjectJiraBindingComponent,
183+
availableGlobally: false,
184+
availableForProject: true,
185+
displayTab: true,
186+
requiredFeatures: [Feature.JiraIntegration],
187+
},
175188
];
176189

177190
function getLanguagesComponent(props: AdditionalCategoryComponentProps) {
@@ -217,3 +230,11 @@ function getAdvancedSecurityComponent(props: AdditionalCategoryComponentProps) {
217230
function getEarlyAccessFeaturesComponent() {
218231
return <EarlyAccessFeatures />;
219232
}
233+
234+
function getProjectJiraBindingComponent({ component }: AdditionalCategoryComponentProps) {
235+
if (addons.jira === undefined || component === undefined) {
236+
return null;
237+
}
238+
239+
return <addons.jira.JiraProjectBinding component={component} />;
240+
}

apps/sq-server/src/main/js/apps/settings/components/AllCategoriesList.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ function AllCategoriesList(props: Readonly<CategoriesListProps>) {
8282
(props.hasFeature(Feature.BranchSupport) || !c.requiresBranchSupport) &&
8383
(props.hasFeature(Feature.FixSuggestions) ||
8484
props.hasFeature(Feature.FixSuggestionsMarketing) ||
85-
c.key !== AI_CODE_FIX_CATEGORY)
85+
c.key !== AI_CODE_FIX_CATEGORY) &&
86+
(c.requiredFeatures === undefined || c.requiredFeatures.every(props.hasFeature))
8687
);
8788
}),
8889
);

apps/sq-server/src/main/js/apps/settings/components/__tests__/SettingsApp-it.tsx

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ import { Feature } from '~sq-server-commons/types/features';
4141
import { Component } from '~sq-server-commons/types/types';
4242
import routes from '../../routes';
4343

44+
jest.mock('~sq-server-addons/index', () => ({
45+
addons: {
46+
...jest.requireActual<typeof import('~sq-server-addons/index')>('~sq-server-addons/index')
47+
.addons,
48+
jira: {
49+
JiraProjectBinding: () => <h1>JiraProjectBindingHeding</h1>,
50+
},
51+
},
52+
}));
53+
4454
let settingsMock: SettingsServiceMock;
4555
let scaSettingsMock: ScaServiceSettingsMock;
4656
let modeHandler: ModeServiceMock;
@@ -66,25 +76,23 @@ beforeEach(() => {
6676
});
6777

6878
const ui = {
69-
categoryLink: (category: string) => byRole('link', { name: category }),
7079
announcementHeading: byRole('heading', { name: 'property.category.general.Announcement' }),
71-
72-
languagesHeading: byRole('heading', { name: 'property.category.languages' }),
73-
languagesSelect: byRole('combobox', { name: 'property.category.languages' }),
74-
jsGeneralSubCategoryHeading: byRole('heading', { name: 'property.category.javascript.General' }),
75-
scaHeading: byRole('heading', { name: 'property.sca.admin.title' }),
76-
77-
settingsSearchInput: byRole('searchbox', { name: 'settings.search.placeholder' }),
78-
searchResultsList: byRole('menu'),
79-
searchItem: (key: string) => byRole('link', { name: new RegExp(key) }),
80-
searchClear: byRole('button', { name: 'clear' }),
81-
80+
categoryLink: (category: string) => byRole('link', { name: category }),
8281
externalAnalyzersAndroidHeading: byRole('heading', {
8382
name: 'property.category.External Analyzers.Android',
8483
}),
8584
generalComputeEngineHeading: byRole('heading', {
8685
name: 'property.category.general.Compute Engine',
8786
}),
87+
jsGeneralSubCategoryHeading: byRole('heading', { name: 'property.category.javascript.General' }),
88+
languagesHeading: byRole('heading', { name: 'property.category.languages' }),
89+
languagesSelect: byRole('combobox', { name: 'property.category.languages' }),
90+
mockedJiraProjectBindingHeading: byRole('heading', { name: 'JiraProjectBindingHeding' }),
91+
scaHeading: byRole('heading', { name: 'property.sca.admin.title' }),
92+
searchClear: byRole('button', { name: 'clear' }),
93+
searchItem: (key: string) => byRole('link', { name: new RegExp(key) }),
94+
searchResultsList: byRole('menu'),
95+
settingsSearchInput: byRole('searchbox', { name: 'settings.search.placeholder' }),
8896
};
8997

9098
describe('Global Settings', () => {
@@ -209,7 +217,9 @@ describe('Global Settings', () => {
209217
describe('Project Settings', () => {
210218
it('renders categories list and definitions', async () => {
211219
const user = userEvent.setup();
212-
renderSettingsApp(mockComponent(), { featureList: [Feature.BranchSupport] });
220+
renderSettingsApp(mockComponent(), {
221+
featureList: [Feature.BranchSupport, Feature.JiraIntegration],
222+
});
213223

214224
const projectCategories = [
215225
'property.category.general',
@@ -226,11 +236,16 @@ describe('Project Settings', () => {
226236
// Visible only for global settings
227237
expect(ui.categoryLink('property.category.almintegration').query()).not.toBeInTheDocument();
228238

239+
expect(ui.categoryLink('project_settings.category.jira_binding').get()).toBeInTheDocument();
240+
229241
expect(await ui.announcementHeading.find()).toBeInTheDocument();
230242

231243
// Navigating to Languages category
232244
await user.click(await ui.categoryLink('property.category.languages').find());
233245
expect(await ui.languagesHeading.find()).toBeInTheDocument();
246+
247+
await user.click(await ui.categoryLink('project_settings.category.jira_binding').find());
248+
expect(await ui.mockedJiraProjectBindingHeading.find()).toBeInTheDocument();
234249
});
235250
});
236251

apps/sq-server/src/main/js/apps/settings/constants.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export const PULL_REQUEST_DECORATION_BINDING_CATEGORY = 'pull_request_decoration
3737
export const EMAIL_NOTIFICATION_CATEGORY = 'email_notification';
3838
export const MODE_CATEGORY = 'mode';
3939
export const EARLY_ACCESS_FEATURES_CATEGORY = 'early_access_features';
40+
export const JIRA_PROJECT_BINDING_CATEGORY = 'jira_project_binding';
4041

4142
export const CATEGORY_OVERRIDES: Record<string, string> = {
4243
abap: LANGUAGES_CATEGORY,

libs/sq-server-commons/src/api/mocks/ProjectsManagementServiceMock.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,51 +36,64 @@ import SettingsServiceMock from './SettingsServiceMock';
3636
jest.mock('../project-management');
3737

3838
const defaultProject = [
39-
mockProject({ key: 'project1', name: 'Project 1' }),
39+
mockProject({ key: 'project1', name: 'Project 1', uuid: 'project1-uuid' }),
4040
mockProject({
4141
key: 'project2',
4242
name: 'Project 2',
4343
visibility: Visibility.Private,
44+
uuid: 'project2-uuid',
4445
}),
4546
mockProject({
4647
key: 'project3',
4748
name: 'Project 3',
4849
lastAnalysisDate: undefined,
50+
uuid: 'project3-uuid',
51+
}),
52+
mockProject({
53+
key: 'projectProvisioned',
54+
name: 'Project 4',
55+
managed: true,
56+
uuid: 'projectProvisioned-uuid',
4957
}),
50-
mockProject({ key: 'projectProvisioned', name: 'Project 4', managed: true }),
5158
mockProject({
5259
key: 'portfolio1',
5360
name: 'Portfolio 1',
5461
qualifier: ComponentQualifier.Portfolio,
62+
uuid: 'portfolio1-uuid',
5563
}),
5664
mockProject({
5765
key: 'portfolio2',
5866
name: 'Portfolio 2',
5967
qualifier: ComponentQualifier.Portfolio,
6068
visibility: Visibility.Private,
69+
uuid: 'portfolio2-uuid',
6170
}),
6271
mockProject({
6372
key: 'portfolio3',
6473
name: 'Portfolio 3',
6574
qualifier: ComponentQualifier.Portfolio,
6675
lastAnalysisDate: undefined,
76+
uuid: 'portfolio3-uuid',
6777
}),
6878
mockProject({
6979
key: 'application1',
7080
name: 'Application 1',
7181
qualifier: ComponentQualifier.Application,
82+
uuid: 'application1-uuid',
7283
}),
7384
mockProject({
7485
key: 'application2',
7586
name: 'Application 2',
7687
qualifier: ComponentQualifier.Application,
7788
visibility: Visibility.Private,
89+
uuid: 'application2-uuid',
7890
}),
7991
mockProject({
8092
key: 'application3',
8193
name: 'Application 3',
8294
qualifier: ComponentQualifier.Application,
8395
lastAnalysisDate: undefined,
96+
uuid: 'application3-uuid',
8497
}),
8598
];
8699

libs/sq-server-commons/src/api/project-management.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export interface ProjectBase {
4040
| ComponentQualifier.Application
4141
| ComponentQualifier.Portfolio
4242
| ComponentQualifier.Project;
43+
uuid: string;
4344
visibility: Visibility;
4445
}
4546

libs/sq-server-commons/src/helpers/mocks/projects.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export function mockProject(overrides: Partial<Project> = {}): Project {
2828
qualifier: ComponentQualifier.Project,
2929
visibility: Visibility.Public,
3030
lastAnalysisDate: '2019-01-04T09:51:48Z',
31+
uuid: 'project-uuid',
3132
...overrides,
3233
};
3334
}

libs/sq-server-commons/src/l10n/default.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8748,4 +8748,57 @@ export const defaultMessages = {
87488748
'beamer.news.error': 'Failed to load news',
87498749
'beamer.news.community_post': 'Community post',
87508750
'beamer.news.no_news': 'No news available',
8751+
8752+
// ----------------------------------------------------------------------------
8753+
//
8754+
// Jira integration
8755+
//
8756+
// ----------------------------------------------------------------------------
8757+
8758+
'jira_binding.server_error': 'Internal server error. Please try again later.',
8759+
'project_settings.category.jira_binding': 'Jira',
8760+
'project_settings.jira_binding.header': 'Jira',
8761+
'project_settings.jira_binding.bound': 'Connected',
8762+
'project_settings.jira_binding.not_bound': 'Not connected',
8763+
'project_settings.jira_binding.description.bound':
8764+
'<p>You can now push SonarQube issues directly to Jira, display relevant release-related Jira information within SonarQube. For any errors, follow our <link>troubleshooting steps</link>.</p>',
8765+
'project_settings.jira_binding.description.not_bound':
8766+
'<p>Enable seamless issue management and workflow visibility.</p> <p>Push SonarQube issues directly to Jira and display relevant release-related Jira information within SonarQube <link>by setting up your connection</link></p>',
8767+
'project_settings.jira_binding.key': 'Project key:',
8768+
'project_settings.jira_binding.remove_binding': 'Remove jira connection for project: {name}',
8769+
'project_settings.jira_binding.disconnect': 'Disconnect',
8770+
'project_settings.jira_binding.create_binding.title': 'Select a Jira project',
8771+
'project_settings.jira_binding.create_binding.description':
8772+
'By default, you will be able to push all possible issue types',
8773+
'project_settings.jira_binding.edit_binding.title': 'Change Jira project binding?',
8774+
'project_settings.jira_binding.edit_binding.description':
8775+
'Issues previously pushed to "{name}" and earlier will be preserved, but all new issues will be pushed to your new Jira project.',
8776+
'project_settings.jira_binding.organization_not_bound':
8777+
'Cannot connect until you bind your instance to Jira in <link>Administration -> Jira</link>',
8778+
'project_settings.jira_binding.no_summary':
8779+
'To see issues for this release, configure Jira releases and versions. See <link>instructions in our documentation</link>',
8780+
'project_settings.jira.binding.bind_success': '"{sqProject}" has been connected to Jira',
8781+
'project_settings.jira.binding.update_bind_success':
8782+
'You have successfully changed your Jira connection to "{name}"',
8783+
'project_settings.jira.binding.unbind_success': '"{sqProject}" has been disconnected from Jira',
8784+
'project_settings.jira_binding.work_types.at_least_one_required':
8785+
'At least one work type must be selected',
8786+
'project_settings.jira_binding.work_types.mandatory_custom_fields':
8787+
'Mandatory custom fields: {fields}',
8788+
'project_settings.jira_binding.work_types.mandatory_fields_warning.title':
8789+
'Some work item types can’t be pushed from SonarQube',
8790+
'project_settings.jira_binding.work_types.mandatory_fields_warning.text':
8791+
'SonarQube can’t create Jira work items with mandatory custom fields because it can’t automatically assign values to them. Remove the mandatory settings for those custom fields in Jira first.',
8792+
'project_settings.jira.binding.work_types.save_failed':
8793+
'Failed to save permitted Jira work types. Please try again later.',
8794+
'project_settings.jira.binding.work_types.saved_successfully':
8795+
'Permitted Jira work types were updated.',
8796+
'project_settings.jira_binding.work_types.title':
8797+
'Choose work types that can be pushed from SonarQube to Jira',
8798+
'project_settings.jira_binding.unbind_confirm.title':
8799+
'Do you want to disconnect this Jira project?',
8800+
'project_settings.jira_binding.unbind_confirm.description':
8801+
'{count} SonarQube {count, plural, one {issue} other {issues}} and corresponding Jira work items will no longer be synchronized. However, existing Jira work items will not be deleted.',
8802+
'project_settings.jira_binding.unbind_confirm.alert':
8803+
'Previous connections cannot be restored. Rebinding or creating a new project binding with Jira can lead to duplicate Jira work items.',
87518804
};

libs/sq-server-commons/src/queries/project-managements.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,17 @@
1818
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1919
*/
2020

21+
import { queryOptions } from '@tanstack/react-query';
2122
import { createQueryHook, StaleTime } from '~shared/queries/common';
2223
import { ComponentQualifier } from '~shared/types/component';
2324
import { getComponents, Project } from '../api/project-management';
2425

2526
const PROJECT_QUERY_PAGE_SIZE = 500;
27+
const PROJECTS_QUERY_KEY = 'project-search';
2628

2729
export const useGetAllProjectsQuery = createQueryHook(() => {
2830
return {
29-
queryKey: ['project-search', 'all'] as const,
31+
queryKey: [PROJECTS_QUERY_KEY, 'all'] as const,
3032
queryFn: async () => {
3133
let pageIndex = 1;
3234
let totalElements = 0;
@@ -50,3 +52,15 @@ export const useGetAllProjectsQuery = createQueryHook(() => {
5052
staleTime: StaleTime.LIVE,
5153
};
5254
});
55+
56+
export const useGetProjectQuery = createQueryHook((projectKey: string) => {
57+
return queryOptions({
58+
queryKey: [PROJECTS_QUERY_KEY, 'details', projectKey],
59+
queryFn: () =>
60+
getComponents({
61+
projects: projectKey,
62+
}),
63+
select: (data) => data.components[0],
64+
staleTime: StaleTime.SHORT,
65+
});
66+
});

libs/sq-server-commons/src/sq-server-adapters/helpers/docs.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,15 @@ import { useUncataloguedDocUrl } from '../../helpers/docs';
2727
* they deal with the same subject matter */
2828
export enum SharedDocLink {
2929
AnalyzingDependencies = '/advanced-security/analyzing-projects-for-dependencies/',
30-
DependencyRisks = '/advanced-security/reviewing-and-fixing-dependency-risks/',
31-
LicenseProfiles = '/advanced-security/managing-license-profiles-and-policies/',
32-
SCATroubleshooting = '/advanced-security/troubleshooting/',
3330
ArchitectureModel = '/design-and-architecture/configuring-the-architecture-analysis/',
3431
ArchitectureModelDeclaration = '/design-and-architecture/configuring-the-architecture-analysis/#declaration',
32+
DependencyRisks = '/advanced-security/reviewing-and-fixing-dependency-risks/',
33+
JiraIntegration = '/instance-administration/jira-integration',
34+
JiraIntegrationProjectBinding = '/project-administration/jira-integration',
35+
JiraIntegrationTroubleshooting = '/instance-administration/jira-integration#troubleshooting',
36+
LicenseProfiles = '/advanced-security/managing-license-profiles-and-policies/',
3537
NewCodeDefinition = '/user-guide/about-new-code#new-code-definitions',
38+
SCATroubleshooting = '/advanced-security/troubleshooting/',
3639
}
3740

3841
/**

0 commit comments

Comments
 (0)