Ver código fonte

SONAR-10980 Add Standards facet on the Issues page (#473)

tags/7.5
Stas Vilchik 6 anos atrás
pai
commit
051e9f592d

+ 1
- 0
server/sonar-web/build.gradle Ver arquivo

@@ -73,6 +73,7 @@ def sources = fileTree(dir: "src") + fileTree(dir: "scripts") + fileTree(dir: "c

task licenseCheckWeb(type: com.hierynomus.gradle.license.tasks.LicenseCheck) {
source = sources
exclude 'main/js/helpers/standards.json'
if (official) exclude 'main/js/app/components/GlobalFooterBranding.js'
}
licenseMain.dependsOn licenseCheckWeb

+ 4
- 2
server/sonar-web/src/main/js/apps/issues/components/App.tsx Ver arquivo

@@ -49,7 +49,8 @@ import {
ReferencedLanguage,
ReferencedUser,
saveMyIssues,
serializeQuery
serializeQuery,
STANDARDS
} from '../utils';
import { Component, CurrentUser, Issue, Paging, BranchLike } from '../../../app/types';
import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
@@ -409,6 +410,7 @@ export default class App extends React.PureComponent<Props, State> {
const facets = requestFacets
? Object.keys(openFacets)
.filter(facet => openFacets[facet])
.filter(facet => facet !== STANDARDS)
.map(mapFacet)
.join(',')
: undefined;
@@ -640,7 +642,7 @@ export default class App extends React.PureComponent<Props, State> {
this.setState(state => ({
openFacets: { ...state.openFacets, [property]: !state.openFacets[property] }
}));
if (!this.state.facets[property]) {
if (property !== STANDARDS && !this.state.facets[property]) {
this.fetchFacet(property);
}
};

+ 24
- 1
server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx Ver arquivo

@@ -30,10 +30,18 @@ import ProjectFacet from './ProjectFacet';
import ResolutionFacet from './ResolutionFacet';
import RuleFacet from './RuleFacet';
import SeverityFacet from './SeverityFacet';
import StandardFacet from './StandardFacet';
import StatusFacet from './StatusFacet';
import TagFacet from './TagFacet';
import TypeFacet from './TypeFacet';
import { Query, Facet, ReferencedComponent, ReferencedUser, ReferencedLanguage } from '../utils';
import {
Query,
Facet,
ReferencedComponent,
ReferencedUser,
ReferencedLanguage,
STANDARDS
} from '../utils';
import { Component } from '../../../app/types';

export interface Props {
@@ -143,6 +151,21 @@ export default class Sidebar extends React.PureComponent<Props> {
rules={query.rules}
stats={facets.rules}
/>
<StandardFacet
cwe={query.cwe}
cweOpen={!!openFacets.cwe}
cweStats={facets.cwe}
loading={this.props.loading}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets[STANDARDS]}
owaspTop10={query.owaspTop10}
owaspTop10Open={!!openFacets.owaspTop10}
owaspTop10Stats={facets.owaspTop10}
sansTop25={query.sansTop25}
sansTop25Open={!!openFacets.sansTop25}
sansTop25Stats={facets.sansTop25}
/>
<TagFacet
component={component}
facetMode={query.facetMode}

+ 308
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx Ver arquivo

@@ -0,0 +1,308 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 { sortBy, without } from 'lodash';
import { Query, STANDARDS, formatFacetStat } from '../utils';
import FacetBox from '../../../components/facet/FacetBox';
import FacetHeader from '../../../components/facet/FacetHeader';
import { translate } from '../../../helpers/l10n';
import FacetItemsList from '../../../components/facet/FacetItemsList';
import FacetItem from '../../../components/facet/FacetItem';
import Select from '../../../components/controls/Select';

export interface Props {
cwe: string[];
cweOpen: boolean;
cweStats: { [x: string]: number } | undefined;
loading?: boolean;
onChange: (changes: Partial<Query>) => void;
onToggle: (property: string) => void;
open: boolean;
owaspTop10: string[];
owaspTop10Open: boolean;
owaspTop10Stats: { [x: string]: number } | undefined;
sansTop25: string[];
sansTop25Open: boolean;
sansTop25Stats: { [x: string]: number } | undefined;
}

interface Standards {
owaspTop10: { [x: string]: { title: string } };
sansTop25: { [x: string]: { title: string } };
cwe: { [x: string]: { title: string } };
}

interface State {
standards: Standards;
}

export default class StandardFacet extends React.PureComponent<Props, State> {
mounted = false;
property = STANDARDS;
state: State = { standards: { owaspTop10: {}, sansTop25: {}, cwe: {} } };

componentDidMount() {
this.mounted = true;
if (this.props.open) {
this.loadStandards();
}
}

componentDidUpdate(prevProps: Props) {
if (!prevProps.open && this.props.open) {
this.loadStandards();
}
}

componentWillUnmount() {
this.mounted = false;
}

loadStandards = () => {
import('../../../helpers/standards.json')
.then(x => x.default)
.then(
({ owaspTop10, sansTop25, cwe }: Standards) => {
if (this.mounted) {
this.setState({ standards: { owaspTop10, sansTop25, cwe } });
}
},
() => {}
);
};

getValues = () => {
return [
...this.props.owaspTop10.map(this.renderOwaspTop10Category),
...this.props.sansTop25.map(this.renderSansTop25Category),
...this.props.cwe.map(this.renderCWECategory)
];
};

handleHeaderClick = () => {
this.props.onToggle(this.property);
};

handleOwaspTop10HeaderClick = () => {
this.props.onToggle('owaspTop10');
};

handleSansTop25HeaderClick = () => {
this.props.onToggle('sansTop25');
};

handleCWEHeaderClick = () => {
this.props.onToggle('cwe');
};

handleClear = () => {
this.props.onChange({ [this.property]: [], owaspTop10: [], sansTop25: [], cwe: [] });
};

handleItemClick = (
prop: 'owaspTop10' | 'sansTop25' | 'cwe',
itemValue: string,
multiple: boolean
) => {
const items = this.props[prop];
if (multiple) {
const newValue = sortBy(
items.includes(itemValue) ? without(items, itemValue) : [...items, itemValue]
);
this.props.onChange({ [prop]: newValue });
} else {
this.props.onChange({
[prop]: items.includes(itemValue) && items.length < 2 ? [] : [itemValue]
});
}
};

handleOwaspTop10ItemClick = (itemValue: string, multiple: boolean) => {
this.handleItemClick('owaspTop10', itemValue, multiple);
};

handleCWEItemClick = (itemValue: string, multiple: boolean) => {
this.handleItemClick('cwe', itemValue, multiple);
};

handleSansTop25ItemClick = (itemValue: string, multiple: boolean) => {
this.handleItemClick('sansTop25', itemValue, multiple);
};

handleCWESelect = ({ value }: { value: string }) => {
this.handleItemClick('cwe', value, true);
};

renderOwaspTop10Category = (category: string) => {
const record = this.state.standards.owaspTop10[category];
if (!record) {
return category.toUpperCase();
} else if (category === 'unknown') {
return record.title;
} else {
return `${category.toUpperCase()} - ${record.title}`;
}
};

renderCWECategory = (category: string) => {
const record = this.state.standards.cwe[category];
if (!record) {
return `CWE-${category}`;
} else if (category === 'unknown') {
return record.title;
} else {
return `CWE-${category} - ${record.title}`;
}
};

renderSansTop25Category = (category: string) => {
const record = this.state.standards.sansTop25[category];
return record ? record.title : category;
};

renderList = (
statsProp: 'owaspTop10Stats' | 'cweStats' | 'sansTop25Stats',
valuesProp: 'owaspTop10' | 'cwe' | 'sansTop25',
renderName: (category: string) => string,
onClick: (x: string, multiple?: boolean) => void
) => {
const stats = this.props[statsProp];
const values = this.props[valuesProp];

if (!stats) {
return null;
}

const categories = sortBy(Object.keys(stats), key => -stats[key]);
const getStat = (category: string) => {
return stats ? stats[category] : undefined;
};

return (
<FacetItemsList>
{categories.map(category => (
<FacetItem
active={values.includes(category)}
key={category}
loading={this.props.loading}
name={renderName(category)}
onClick={onClick}
stat={formatFacetStat(getStat(category), '')}
tooltip={values.length === 1 && !values.includes(category)}
value={category}
/>
))}
</FacetItemsList>
);
};

renderOwaspTop10List() {
return this.renderList(
'owaspTop10Stats',
'owaspTop10',
this.renderOwaspTop10Category,
this.handleOwaspTop10ItemClick
);
}

renderCWEList() {
return this.renderList('cweStats', 'cwe', this.renderCWECategory, this.handleCWEItemClick);
}

renderCWESearch() {
const options = Object.keys(this.state.standards.cwe).map(cwe => ({
label: this.renderCWECategory(cwe),
value: cwe
}));
return (
<div className="search-navigator-facet-footer">
<Select
className="input-super-large"
clearable={false}
noResultsText={translate('select2.noMatches')}
onChange={this.handleCWESelect}
options={options}
placeholder={translate('search.search_for_cwe')}
searchable={true}
/>
</div>
);
}

renderSansTop25List() {
return this.renderList(
'sansTop25Stats',
'sansTop25',
this.renderSansTop25Category,
this.handleSansTop25ItemClick
);
}

renderSubFacets() {
return (
<>
<FacetBox className="is-inner" property="owaspTop10">
<FacetHeader
name={translate('issues.facet.owaspTop10')}
onClick={this.handleOwaspTop10HeaderClick}
open={this.props.owaspTop10Open}
values={this.props.owaspTop10.map(this.renderOwaspTop10Category)}
/>
{this.props.owaspTop10Open && this.renderOwaspTop10List()}
</FacetBox>
<FacetBox className="is-inner" property="sansTop25">
<FacetHeader
name={translate('issues.facet.sansTop25')}
onClick={this.handleSansTop25HeaderClick}
open={this.props.sansTop25Open}
values={this.props.sansTop25.map(this.renderSansTop25Category)}
/>
{this.props.sansTop25Open && this.renderSansTop25List()}
</FacetBox>
<FacetBox className="is-inner" property="cwe">
<FacetHeader
name={translate('issues.facet.cwe')}
onClick={this.handleCWEHeaderClick}
open={this.props.cweOpen}
values={this.props.cwe.map(this.renderCWECategory)}
/>
{this.props.cweOpen && this.renderCWEList()}
{this.props.cweOpen && this.renderCWESearch()}
</FacetBox>
</>
);
}

render() {
return (
<FacetBox property={this.property}>
<FacetHeader
name={translate('issues.facet', this.property)}
onClear={this.handleClear}
onClick={this.handleHeaderClick}
open={this.props.open}
values={this.getValues()}
/>

{this.props.open && this.renderSubFacets()}
</FacetBox>
);
}
}

+ 170
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx Ver arquivo

@@ -0,0 +1,170 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
import StandardFacet, { Props } from '../StandardFacet';
import { click } from '../../../../helpers/testUtils';

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

it('should toggle standards facet', () => {
const onToggle = jest.fn();
const wrapper = shallowRender({ onToggle });
click(wrapper.children('FacetHeader'));
expect(onToggle).toBeCalledWith('standards');
});

it('should clear standards facet', () => {
const onChange = jest.fn();
const wrapper = shallowRender({ onChange });
wrapper.children('FacetHeader').prop<Function>('onClear')();
expect(onChange).toBeCalledWith({ cwe: [], owaspTop10: [], sansTop25: [], standards: [] });
});

it('should render sub-facets', () => {
expect(
shallowRender({
cwe: ['42'],
cweOpen: true,
cweStats: { 42: 5, 173: 3 },
open: true,
owaspTop10: ['a3'],
owaspTop10Open: true,
owaspTop10Stats: { a1: 15, a3: 5 },
sansTop25: ['risky-resource'],
sansTop25Open: true,
sansTop25Stats: { foo: 12, 'risky-resource': 10 }
})
).toMatchSnapshot();
});

it('should select items', () => {
const onChange = jest.fn();
const wrapper = shallowRender({
cwe: ['42'],
cweOpen: true,
cweStats: { 42: 5, 173: 3 },
onChange,
open: true,
owaspTop10: ['a3'],
owaspTop10Open: true,
owaspTop10Stats: { a1: 15, a3: 5 },
sansTop25: ['risky-resource'],
sansTop25Open: true,
sansTop25Stats: { foo: 12, 'risky-resource': 10 }
});

selectAndCheck('owaspTop10', 'a1');
selectAndCheck('owaspTop10', 'a1', true, ['a1', 'a3']);
selectAndCheck('sansTop25', 'foo');
selectAndCheck('cwe', '173');

function selectAndCheck(facet: string, value: string, multiple = false, expectedValue = [value]) {
wrapper
.find(`FacetBox[property="${facet}"]`)
.find(`FacetItem[value="${value}"]`)
.prop<Function>('onClick')(value, multiple);
expect(onChange).lastCalledWith({ [facet]: expectedValue });
}
});

it('should toggle sub-facets', () => {
const onToggle = jest.fn();
const wrapper = shallowRender({ onToggle, open: true });
click(wrapper.find('FacetBox[property="owaspTop10"]').children('FacetHeader'));
expect(onToggle).lastCalledWith('owaspTop10');
click(wrapper.find('FacetBox[property="cwe"]').children('FacetHeader'));
expect(onToggle).lastCalledWith('cwe');
click(wrapper.find('FacetBox[property="sansTop25"]').children('FacetHeader'));
expect(onToggle).lastCalledWith('sansTop25');
});

it('should display correct selection', () => {
const wrapper = shallowRender({
open: true,
owaspTop10: ['a1', 'a3', 'unknown'],
sansTop25: ['risky-resource', 'foo'],
cwe: ['42', '1111', 'unknown']
});
checkValues('standards', [
'A1 - a1 title',
'A3',
'Not OWAPS',
'Risky Resource Management',
'foo',
'CWE-42 - cwe-42 title',
'CWE-1111',
'Unknown CWE'
]);
checkValues('owaspTop10', ['A1 - a1 title', 'A3', 'Not OWAPS']);
checkValues('cwe', ['CWE-42 - cwe-42 title', 'CWE-1111', 'Unknown CWE']);
checkValues('sansTop25', ['Risky Resource Management', 'foo']);

function checkValues(property: string, values: string[]) {
expect(
wrapper
.find(`FacetBox[property="${property}"]`)
.children('FacetHeader')
.prop('values')
).toEqual(values);
}
});

it('should search CWE', () => {
const onChange = jest.fn();
const wrapper = shallowRender({ onChange, open: true, cwe: ['42'], cweOpen: true });
wrapper
.find('FacetBox[property="cwe"]')
.find('Select')
.prop<Function>('onChange')({ value: '111' });
expect(onChange).toBeCalledWith({ cwe: ['111', '42'] });
});

function shallowRender(props: Partial<Props> = {}) {
const wrapper = shallow(
<StandardFacet
cwe={[]}
cweOpen={false}
cweStats={{}}
onChange={jest.fn()}
onToggle={jest.fn()}
open={false}
owaspTop10={[]}
owaspTop10Open={false}
owaspTop10Stats={{}}
sansTop25={[]}
sansTop25Open={false}
sansTop25Stats={{}}
{...props}
/>,
// disable loading of standards.json
{ disableLifecycleMethods: true }
);
wrapper.setState({
standards: {
owaspTop10: { a1: { title: 'a1 title' }, unknown: { title: 'Not OWAPS' } },
sansTop25: { 'risky-resource': { title: 'Risky Resource Management' } },
cwe: { 42: { title: 'cwe-42 title' }, unknown: { title: 'Unknown CWE' } }
}
});
return wrapper;
}

+ 6
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap Ver arquivo

@@ -10,6 +10,7 @@ Array [
"CreationDateFacet",
"LanguageFacet",
"RuleFacet",
"StandardFacet",
"TagFacet",
"ProjectFacet",
"ModuleFacet",
@@ -29,6 +30,7 @@ Array [
"CreationDateFacet",
"LanguageFacet",
"RuleFacet",
"StandardFacet",
"TagFacet",
"FileFacet",
"AssigneeFacet",
@@ -46,6 +48,7 @@ Array [
"CreationDateFacet",
"LanguageFacet",
"RuleFacet",
"StandardFacet",
"TagFacet",
"ProjectFacet",
"AssigneeFacet",
@@ -63,6 +66,7 @@ Array [
"CreationDateFacet",
"LanguageFacet",
"RuleFacet",
"StandardFacet",
"TagFacet",
"ModuleFacet",
"DirectoryFacet",
@@ -82,6 +86,7 @@ Array [
"CreationDateFacet",
"LanguageFacet",
"RuleFacet",
"StandardFacet",
"TagFacet",
"ModuleFacet",
"DirectoryFacet",
@@ -101,6 +106,7 @@ Array [
"CreationDateFacet",
"LanguageFacet",
"RuleFacet",
"StandardFacet",
"TagFacet",
"ProjectFacet",
"AuthorFacet",

+ 184
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StandardFacet-test.tsx.snap Ver arquivo

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

exports[`should render closed 1`] = `
<FacetBox
property="standards"
>
<FacetHeader
name="issues.facet.standards"
onClear={[Function]}
onClick={[Function]}
open={false}
values={Array []}
/>
</FacetBox>
`;

exports[`should render sub-facets 1`] = `
<FacetBox
property="standards"
>
<FacetHeader
name="issues.facet.standards"
onClear={[Function]}
onClick={[Function]}
open={true}
values={
Array [
"A3",
"Risky Resource Management",
"CWE-42 - cwe-42 title",
]
}
/>
<React.Fragment>
<FacetBox
className="is-inner"
property="owaspTop10"
>
<FacetHeader
name="issues.facet.owaspTop10"
onClick={[Function]}
open={true}
values={
Array [
"A3",
]
}
/>
<FacetItemsList>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="a1"
loading={false}
name="A1 - a1 title"
onClick={[Function]}
stat="15"
tooltip={true}
value="a1"
/>
<FacetItem
active={true}
disabled={false}
halfWidth={false}
key="a3"
loading={false}
name="A3"
onClick={[Function]}
stat="5"
tooltip={false}
value="a3"
/>
</FacetItemsList>
</FacetBox>
<FacetBox
className="is-inner"
property="sansTop25"
>
<FacetHeader
name="issues.facet.sansTop25"
onClick={[Function]}
open={true}
values={
Array [
"Risky Resource Management",
]
}
/>
<FacetItemsList>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="foo"
loading={false}
name="foo"
onClick={[Function]}
stat="12"
tooltip={true}
value="foo"
/>
<FacetItem
active={true}
disabled={false}
halfWidth={false}
key="risky-resource"
loading={false}
name="Risky Resource Management"
onClick={[Function]}
stat="10"
tooltip={false}
value="risky-resource"
/>
</FacetItemsList>
</FacetBox>
<FacetBox
className="is-inner"
property="cwe"
>
<FacetHeader
name="issues.facet.cwe"
onClick={[Function]}
open={true}
values={
Array [
"CWE-42 - cwe-42 title",
]
}
/>
<FacetItemsList>
<FacetItem
active={true}
disabled={false}
halfWidth={false}
key="42"
loading={false}
name="CWE-42 - cwe-42 title"
onClick={[Function]}
stat="5"
tooltip={false}
value="42"
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="173"
loading={false}
name="CWE-173"
onClick={[Function]}
stat="3"
tooltip={true}
value="173"
/>
</FacetItemsList>
<div
className="search-navigator-facet-footer"
>
<Select
className="input-super-large"
clearable={false}
noResultsText="select2.noMatches"
onChange={[Function]}
options={
Array [
Object {
"label": "CWE-42 - cwe-42 title",
"value": "42",
},
Object {
"label": "Unknown CWE",
"value": "unknown",
},
]
}
placeholder="search.search_for_cwe"
searchable={true}
/>
</div>
</FacetBox>
</React.Fragment>
</FacetBox>
`;

+ 17
- 6
server/sonar-web/src/main/js/apps/issues/utils.ts Ver arquivo

@@ -44,24 +44,29 @@ export interface Query {
createdAt: string;
createdBefore: Date | undefined;
createdInLast: string;
cwe: string[];
directories: string[];
facetMode: string;
files: string[];
issues: string[];
languages: string[];
modules: string[];
owaspTop10: string[];
projects: string[];
resolved: boolean;
resolutions: string[];
resolved: boolean;
rules: string[];
sort: string;
sansTop25: string[];
severities: string[];
sinceLeakPeriod: boolean;
sort: string;
statuses: string[];
tags: string[];
types: string[];
}

export const STANDARDS = 'standards';

// allow sorting by CREATION_DATE only
const parseAsSort = (sort: string) => (sort === 'CREATION_DATE' ? 'CREATION_DATE' : '');
const ISSUES_DEFAULT = 'sonarqube.issues.default';
@@ -75,19 +80,22 @@ export function parseQuery(query: RawQuery): Query {
createdAt: parseAsString(query.createdAt),
createdBefore: parseAsDate(query.createdBefore),
createdInLast: parseAsString(query.createdInLast),
cwe: parseAsArray(query.cwe, parseAsString),
directories: parseAsArray(query.directories, parseAsString),
facetMode: parseAsFacetMode(query.facetMode),
files: parseAsArray(query.fileUuids, parseAsString),
issues: parseAsArray(query.issues, parseAsString),
languages: parseAsArray(query.languages, parseAsString),
modules: parseAsArray(query.moduleUuids, parseAsString),
owaspTop10: parseAsArray(query.owaspTop10, parseAsString),
projects: parseAsArray(query.projectUuids, parseAsString),
resolved: parseAsBoolean(query.resolved),
resolutions: parseAsArray(query.resolutions, parseAsString),
resolved: parseAsBoolean(query.resolved),
rules: parseAsArray(query.rules, parseAsString),
sort: parseAsSort(query.s),
sansTop25: parseAsArray(query.sansTop25, parseAsString),
severities: parseAsArray(query.severities, parseAsString),
sinceLeakPeriod: parseAsBoolean(query.sinceLeakPeriod, false),
sort: parseAsSort(query.s),
statuses: parseAsArray(query.statuses, parseAsString),
tags: parseAsArray(query.tags, parseAsString),
types: parseAsArray(query.types, parseAsString)
@@ -109,20 +117,23 @@ export function serializeQuery(query: Query): RawQuery {
createdAt: serializeString(query.createdAt),
createdBefore: serializeDateShort(query.createdBefore),
createdInLast: serializeString(query.createdInLast),
cwe: serializeStringArray(query.cwe),
directories: serializeStringArray(query.directories),
facetMode: query.facetMode === 'effort' ? serializeString(query.facetMode) : undefined,
fileUuids: serializeStringArray(query.files),
issues: serializeStringArray(query.issues),
languages: serializeStringArray(query.languages),
moduleUuids: serializeStringArray(query.modules),
owaspTop10: serializeStringArray(query.owaspTop10),
projectUuids: serializeStringArray(query.projects),
resolved: query.resolved ? undefined : 'false',
resolutions: serializeStringArray(query.resolutions),
resolved: query.resolved ? undefined : 'false',
rules: serializeStringArray(query.rules),
s: serializeString(query.sort),
sansTop25: serializeStringArray(query.sansTop25),
severities: serializeStringArray(query.severities),
sinceLeakPeriod: query.sinceLeakPeriod ? 'true' : undefined,
statuses: serializeStringArray(query.statuses),
rules: serializeStringArray(query.rules),
tags: serializeStringArray(query.tags),
types: serializeStringArray(query.types)
};

+ 11
- 0
server/sonar-web/src/main/js/components/search-navigator.css Ver arquivo

@@ -43,6 +43,17 @@
border: 1px solid var(--leakBorderColor);
}

.search-navigator-facet-box.is-inner {
margin-left: 8px;
padding-left: 12px;
border-left: 1px solid var(--barBorderColor);
}

.search-navigator-facet-box.is-inner .search-navigator-facet-header {
padding-top: 6px;
padding-bottom: 6px;
}

.leak-facet-box:not(.hidden) + .leak-facet-box {
border-top: none;
}

+ 3626
- 0
server/sonar-web/src/main/js/helpers/standards.json
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 5
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties Ver arquivo

@@ -708,7 +708,10 @@ issues.facet.issues=Issue Key
issues.facet.mode=Display Mode
issues.facet.mode.count=Issues
issues.facet.mode.effort=Effort

issues.facet.standards=Standard
issues.facet.owaspTop10=OWASP Top 10
issues.facet.sansTop25=SANS Top 25
issues.facet.cwe=CWE

#------------------------------------------------------------------------------
#
@@ -905,6 +908,7 @@ search.search_by_name_or_key=Search by name or key...
search.search_for_tags=Search for tags...
search.search_for_rules=Search for rules...
search.search_for_languages=Search for languages...
search.search_for_cwe=Search for CWEs...


#------------------------------------------------------------------------------

Carregando…
Cancelar
Salvar