Browse Source

SONAR-8884 Group languages related settings in a single Languages tab

tags/8.0
Philippe Perrin 4 years ago
parent
commit
daf5a60dd2

+ 61
- 0
server/sonar-web/src/main/js/apps/settings/components/AdditionalCategories.tsx View File

@@ -0,0 +1,61 @@
/*
* SonarQube
* Copyright (C) 2009-2019 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import * as React from 'react';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { LANGUAGES_CATEGORY, NEW_CODE_PERIOD_CATEGORY } from './AdditionalCategoryKeys';
import Languages from './Languages';
import NewCodePeriod from './NewCodePeriod';

export interface AdditionalCategory {
key: string;
name: string;
renderComponent: (
parentComponent: T.Component | undefined,
selectedCategory: string
) => JSX.Element;
availableGlobally: boolean;
availableForProject: boolean;
}

export const ADDITIONAL_CATEGORIES: AdditionalCategory[] = [
{
key: LANGUAGES_CATEGORY,
name: translate('property.category.languages'),
renderComponent: getLanguagesComponent,
availableGlobally: true,
availableForProject: true
},
{
key: NEW_CODE_PERIOD_CATEGORY,
name: translate('settings.new_code_period.category'),
renderComponent: getNewCodePeriodComponent,
availableGlobally: true,
availableForProject: false
}
];

function getLanguagesComponent(component: any, originalCategory: string) {
return <Languages component={component} selectedCategory={originalCategory} />;
}

function getNewCodePeriodComponent() {
return <NewCodePeriod />;
}

+ 22
- 0
server/sonar-web/src/main/js/apps/settings/components/AdditionalCategoryKeys.ts View File

@@ -0,0 +1,22 @@
/*
* SonarQube
* Copyright (C) 2009-2019 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

export const LANGUAGES_CATEGORY = 'languages';
export const NEW_CODE_PERIOD_CATEGORY = 'new_code_period';

+ 12
- 8
server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.tsx View File

@@ -22,27 +22,24 @@ import { sortBy } from 'lodash';
import * as React from 'react';
import { connect } from 'react-redux';
import { IndexLink } from 'react-router';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { getSettingsAppAllCategories, Store } from '../../../store/rootReducer';
import { getCategoryName } from '../utils';
import { ADDITIONAL_CATEGORIES } from './AdditionalCategories';
import { CATEGORY_OVERRIDES } from './CategoryOverrides';

interface Category {
key: string;
name: string;
}

interface Props {
export interface CategoriesListProps {
categories: string[];
component?: T.Component;
defaultCategory: string;
selectedCategory: string;
}

const FIXED_CATEGORIES = [
{ key: 'new_code_period', name: translate('settings.new_code_period.category') }
];

export class CategoriesList extends React.PureComponent<Props> {
export class CategoriesList extends React.PureComponent<CategoriesListProps> {
renderLink(category: Category) {
const { component, defaultCategory, selectedCategory } = this.props;
const pathname = this.props.component ? '/project/settings' : '/settings';
@@ -64,11 +61,18 @@ export class CategoriesList extends React.PureComponent<Props> {

render() {
const categoriesWithName = this.props.categories
.filter(key => !CATEGORY_OVERRIDES[key.toLowerCase()])
.map(key => ({
key,
name: getCategoryName(key)
}))
.concat(!this.props.component ? FIXED_CATEGORIES : []);
.concat(
this.props.component
? // Project settings
ADDITIONAL_CATEGORIES.filter(c => c.availableForProject)
: // Global settings
ADDITIONAL_CATEGORIES.filter(c => c.availableGlobally)
);
const sortedCategories = sortBy(categoriesWithName, category => category.name.toLowerCase());
return (
<ul className="side-tabs-menu">

+ 16
- 4
server/sonar-web/src/main/js/apps/settings/components/AppContainer.tsx View File

@@ -17,6 +17,8 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import { find } from 'lodash';
import * as React from 'react';
import Helmet from 'react-helmet';
import { connect } from 'react-redux';
@@ -27,9 +29,10 @@ import { getSettingsAppDefaultCategory, Store } from '../../../store/rootReducer
import '../side-tabs.css';
import { fetchSettings } from '../store/actions';
import '../styles.css';
import { ADDITIONAL_CATEGORIES } from './AdditionalCategories';
import AllCategoriesList from './AllCategoriesList';
import CategoryDefinitionsList from './CategoryDefinitionsList';
import NewCodePeriod from './NewCodePeriod';
import { CATEGORY_OVERRIDES } from './CategoryOverrides';
import PageHeader from './PageHeader';
import WildcardsHelp from './WildcardsHelp';

@@ -79,7 +82,16 @@ export class App extends React.PureComponent<Props & WithRouterProps, State> {
}

const { query } = this.props.location;
const selectedCategory = query.category || this.props.defaultCategory;

const originalCategory = (query.category as string) || this.props.defaultCategory;
const overriddenCategory = CATEGORY_OVERRIDES[originalCategory.toLowerCase()];
const selectedCategory = overriddenCategory || originalCategory;
const foundAdditionalCategory = find(ADDITIONAL_CATEGORIES, c => c.key === selectedCategory);
const isProjectSettings = this.props.component;
const shouldRenderAdditionalCategory =
foundAdditionalCategory &&
((isProjectSettings && foundAdditionalCategory.availableForProject) ||
(!isProjectSettings && foundAdditionalCategory.availableGlobally));

return (
<div className="page page-limited" id="settings-page">
@@ -97,8 +109,8 @@ export class App extends React.PureComponent<Props & WithRouterProps, State> {
/>
</div>
<div className="side-tabs-main">
{!this.props.component && selectedCategory === 'new_code_period' ? (
<NewCodePeriod />
{foundAdditionalCategory && shouldRenderAdditionalCategory ? (
foundAdditionalCategory.renderComponent(this.props.component, originalCategory)
) : (
<CategoryDefinitionsList
category={selectedCategory}

+ 49
- 0
server/sonar-web/src/main/js/apps/settings/components/CategoryOverrides.ts View File

@@ -0,0 +1,49 @@
/*
* SonarQube
* Copyright (C) 2009-2019 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import { LANGUAGES_CATEGORY } from './AdditionalCategoryKeys';

export const CATEGORY_OVERRIDES: T.Dict<string> = {
abap: LANGUAGES_CATEGORY,
apex: LANGUAGES_CATEGORY,
'c / c++ / objective-c': LANGUAGES_CATEGORY,
'c#': LANGUAGES_CATEGORY,
cobol: LANGUAGES_CATEGORY,
css: LANGUAGES_CATEGORY,
flex: LANGUAGES_CATEGORY,
go: LANGUAGES_CATEGORY,
html: LANGUAGES_CATEGORY,
java: LANGUAGES_CATEGORY,
javascript: LANGUAGES_CATEGORY,
kotlin: LANGUAGES_CATEGORY,
php: LANGUAGES_CATEGORY,
'pl/i': LANGUAGES_CATEGORY,
'pl/sql': LANGUAGES_CATEGORY,
python: LANGUAGES_CATEGORY,
rpg: LANGUAGES_CATEGORY,
ruby: LANGUAGES_CATEGORY,
scala: LANGUAGES_CATEGORY,
swift: LANGUAGES_CATEGORY,
't-sql': LANGUAGES_CATEGORY,
typescript: LANGUAGES_CATEGORY,
'vb.net': LANGUAGES_CATEGORY,
'visual basic': LANGUAGES_CATEGORY,
xml: LANGUAGES_CATEGORY
};

+ 130
- 0
server/sonar-web/src/main/js/apps/settings/components/Languages.tsx View File

@@ -0,0 +1,130 @@
/*
* SonarQube
* Copyright (C) 2009-2019 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import * as React from 'react';
import { connect } from 'react-redux';
import Select from 'sonar-ui-common/components/controls/Select';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
import { getSettingsAppAllCategories, Store } from '../../../store/rootReducer';
import { getCategoryName } from '../utils';
import { LANGUAGES_CATEGORY } from './AdditionalCategoryKeys';
import CategoryDefinitionsList from './CategoryDefinitionsList';
import { CATEGORY_OVERRIDES } from './CategoryOverrides';

export interface LanguagesProps {
categories: string[];
component?: T.Component;
location: Location;
selectedCategory: string;
router: Router;
}

interface LanguagesState {
availableLanguages: SelectOption[];
selectedLanguage: string | undefined;
}

interface SelectOption {
label: string;
originalValue: string;
value: string;
}

export class Languages extends React.PureComponent<LanguagesProps, LanguagesState> {
constructor(props: LanguagesProps) {
super(props);

this.state = {
availableLanguages: [],
selectedLanguage: undefined
};
}

componentDidMount() {
const { selectedCategory, categories } = this.props;
const lowerCasedLanguagesCategory = LANGUAGES_CATEGORY.toLowerCase();
const lowerCasedSelectedCategory = selectedCategory.toLowerCase();

const availableLanguages = categories
.filter(c => CATEGORY_OVERRIDES[c.toLowerCase()] === lowerCasedLanguagesCategory)
.map(c => ({
label: getCategoryName(c),
value: c.toLowerCase(),
originalValue: c
}));

let selectedLanguage = undefined;

if (
lowerCasedSelectedCategory !== lowerCasedLanguagesCategory &&
availableLanguages.find(c => c.value === lowerCasedSelectedCategory)
) {
selectedLanguage = lowerCasedSelectedCategory;
}

this.setState({
availableLanguages,
selectedLanguage
});
}

handleOnChange = (newOption: SelectOption) => {
this.setState({ selectedLanguage: newOption.value });

const { location, router } = this.props;

router.push({
...location,
query: { ...location.query, category: newOption.originalValue }
});
};

render() {
const { component } = this.props;
const { availableLanguages, selectedLanguage } = this.state;

return (
<>
<h2 className="settings-sub-category-name">{translate('property.category.languages')}</h2>
<div data-test="language-select">
<Select
className="input-large"
onChange={this.handleOnChange}
options={availableLanguages}
placeholder={translate('settings.languages.select_a_language_placeholder')}
value={selectedLanguage}
/>
</div>
{selectedLanguage && (
<div className="settings-sub-category">
<CategoryDefinitionsList category={selectedLanguage} component={component} />
</div>
)}
</>
);
}
}

export default withRouter(
connect((state: Store) => ({
categories: getSettingsAppAllCategories(state)
}))(Languages)
);

+ 37
- 7
server/sonar-web/src/main/js/apps/settings/components/__tests__/AllCategoriesList-test.tsx View File

@@ -19,16 +19,46 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { CategoriesList } from '../AllCategoriesList';
import { mockComponent } from '../../../../helpers/testMocks';
import { AdditionalCategory } from '../AdditionalCategories';
import { CategoriesList, CategoriesListProps } from '../AllCategoriesList';

it('should render correctly', () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('li')).toHaveLength(3);
jest.mock('../AdditionalCategories', () => ({
ADDITIONAL_CATEGORIES: [
{
key: 'CAT_1',
name: 'CAT_1_NAME',
renderComponent: jest.fn(),
availableGlobally: true,
availableForProject: true
},
{
key: 'CAT_2',
name: 'CAT_2_NAME',
renderComponent: jest.fn(),
availableGlobally: true,
availableForProject: false
},
{
key: 'CAT_3',
name: 'CAT_3_NAME',
renderComponent: jest.fn(),
availableGlobally: false,
availableForProject: true
}
] as AdditionalCategory[]
}));

it('should render correctly in global mode', () => {
expect(shallowRender()).toMatchSnapshot();
});

it('should render correctly in project mode', () => {
expect(shallowRender({ component: mockComponent() })).toMatchSnapshot();
});

function shallowRender(props: Partial<CategoriesList['props']> = {}) {
const categories = ['COBOL', 'general'];
function shallowRender(props?: Partial<CategoriesListProps>) {
const categories = ['general'];

return shallow(
<CategoriesList

+ 10
- 2
server/sonar-web/src/main/js/apps/settings/components/__tests__/AppContainer-test.tsx View File

@@ -19,8 +19,9 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { App } from '../AppContainer';
import { mockLocation, mockRouter } from '../../../../helpers/testMocks';
import { LANGUAGES_CATEGORY, NEW_CODE_PERIOD_CATEGORY } from '../AdditionalCategoryKeys';
import { App } from '../AppContainer';

it('should render correctly', () => {
const wrapper = shallowRender();
@@ -29,7 +30,14 @@ it('should render correctly', () => {

it('should render newCodePeriod correctly', () => {
const wrapper = shallowRender({
location: mockLocation({ query: { category: 'new_code_period' } })
location: mockLocation({ query: { category: NEW_CODE_PERIOD_CATEGORY } })
});
expect(wrapper).toMatchSnapshot();
});

it('should render languages correctly', () => {
const wrapper = shallowRender({
location: mockLocation({ query: { category: LANGUAGES_CATEGORY } })
});
expect(wrapper).toMatchSnapshot();
});

+ 56
- 0
server/sonar-web/src/main/js/apps/settings/components/__tests__/Languages-test.tsx View File

@@ -0,0 +1,56 @@
/*
* SonarQube
* Copyright (C) 2009-2019 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockLocation, mockRouter } from '../../../../helpers/testMocks';
import { Languages, LanguagesProps } from '../Languages';

it('should render correctly', () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
});

it('should render correctly with an unknow language', () => {
const wrapper = shallowRender({ selectedCategory: 'unknown' });
expect(wrapper).toMatchSnapshot();
});

it('should correctly handle a change of the selected language', () => {
const push = jest.fn();
const router = mockRouter({ push });
const wrapper = shallowRender({ router });
expect(wrapper.state().selectedLanguage).toBe('java');

wrapper.instance().handleOnChange({ label: '', originalValue: 'CoBoL', value: 'cobol' });
expect(wrapper.state().selectedLanguage).toBe('cobol');
expect(push).toHaveBeenCalledWith(expect.objectContaining({ query: { category: 'CoBoL' } }));
});

function shallowRender(props: Partial<LanguagesProps> = {}) {
return shallow<Languages>(
<Languages
categories={['Java', 'JavaScript', 'COBOL']}
location={mockLocation()}
router={mockRouter()}
selectedCategory="java"
{...props}
/>
);
}

+ 75
- 11
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AllCategoriesList-test.tsx.snap View File

@@ -1,26 +1,45 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
exports[`should render correctly in global mode 1`] = `
<ul
className="side-tabs-menu"
>
<li
key="COBOL"
key="CAT_1"
>
<IndexLink
className=""
title="COBOL"
title="CAT_1_NAME"
to={
Object {
"pathname": "/settings",
"query": Object {
"category": "cobol",
"category": "cat_1",
"id": undefined,
},
}
}
>
COBOL
CAT_1_NAME
</IndexLink>
</li>
<li
key="CAT_2"
>
<IndexLink
className=""
title="CAT_2_NAME"
to={
Object {
"pathname": "/settings",
"query": Object {
"category": "cat_2",
"id": undefined,
},
}
}
>
CAT_2_NAME
</IndexLink>
</li>
<li
@@ -42,23 +61,68 @@ exports[`should render correctly 1`] = `
general
</IndexLink>
</li>
</ul>
`;

exports[`should render correctly in project mode 1`] = `
<ul
className="side-tabs-menu"
>
<li
key="new_code_period"
key="CAT_1"
>
<IndexLink
className=""
title="settings.new_code_period.category"
title="CAT_1_NAME"
to={
Object {
"pathname": "/settings",
"pathname": "/project/settings",
"query": Object {
"category": "new_code_period",
"id": undefined,
"category": "cat_1",
"id": "my-project",
},
}
}
>
settings.new_code_period.category
CAT_1_NAME
</IndexLink>
</li>
<li
key="CAT_3"
>
<IndexLink
className=""
title="CAT_3_NAME"
to={
Object {
"pathname": "/project/settings",
"query": Object {
"category": "cat_3",
"id": "my-project",
},
}
}
>
CAT_3_NAME
</IndexLink>
</li>
<li
key="general"
>
<IndexLink
className=""
title="general"
to={
Object {
"pathname": "/project/settings",
"query": Object {
"category": undefined,
"id": "my-project",
},
}
}
>
general
</IndexLink>
</li>
</ul>

+ 2
- 0
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AppContainer-test.tsx.snap View File

@@ -2,4 +2,6 @@

exports[`should render correctly 1`] = `""`;

exports[`should render languages correctly 1`] = `""`;

exports[`should render newCodePeriod correctly 1`] = `""`;

+ 85
- 0
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/Languages-test.tsx.snap View File

@@ -0,0 +1,85 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<Fragment>
<h2
className="settings-sub-category-name"
>
property.category.languages
</h2>
<div
data-test="language-select"
>
<Select
className="input-large"
onChange={[Function]}
options={
Array [
Object {
"label": "Java",
"originalValue": "Java",
"value": "java",
},
Object {
"label": "JavaScript",
"originalValue": "JavaScript",
"value": "javascript",
},
Object {
"label": "COBOL",
"originalValue": "COBOL",
"value": "cobol",
},
]
}
placeholder="settings.languages.select_a_language_placeholder"
value="java"
/>
</div>
<div
className="settings-sub-category"
>
<Connect(SubCategoryDefinitionsList)
category="java"
/>
</div>
</Fragment>
`;

exports[`should render correctly with an unknow language 1`] = `
<Fragment>
<h2
className="settings-sub-category-name"
>
property.category.languages
</h2>
<div
data-test="language-select"
>
<Select
className="input-large"
onChange={[Function]}
options={
Array [
Object {
"label": "Java",
"originalValue": "Java",
"value": "java",
},
Object {
"label": "JavaScript",
"originalValue": "JavaScript",
"value": "javascript",
},
Object {
"label": "COBOL",
"originalValue": "COBOL",
"value": "cobol",
},
]
}
placeholder="settings.languages.select_a_language_placeholder"
/>
</div>
</Fragment>
`;

+ 2
- 7
server/sonar-web/src/main/js/apps/settings/styles.css View File

@@ -82,13 +82,8 @@
border-top: 1px dotted var(--barBorderColor);
}

.settings-sub-categories-list {
}

.settings-sub-categories-list > li {
}

.settings-sub-categories-list > li + li {
.settings-sub-categories-list > li + li,
.settings-sub-category {
margin: 30px -20px 0;
padding: 30px 20px;
border-top: 1px solid var(--barBorderColor);

+ 2
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -920,6 +920,7 @@ settings.new_code_period.title=Default New Code Period behavior
settings.new_code_period.description=The New Code Period is the period used to compare measures and track new issues. {link}
settings.new_code_period.description2=This setting is the default for all projects. A specific New Code Period setting can be configured at project level.

settings.languages.select_a_language_placeholder=Select a language

property.category.general=General
property.category.general.email=Email
@@ -941,6 +942,7 @@ property.category.duplications=Duplications
property.category.localization=Localization
property.category.exclusions=Analysis Scope
property.category.webhooks=Webhooks
property.category.languages=Languages
property.sonar.inclusions.name=Source File Inclusions
property.sonar.inclusions.description=Patterns used to include some source files and only these ones in analysis.
property.sonar.test.inclusions.name=Test File Inclusions

Loading…
Cancel
Save