aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorWouter Admiraal <wouter.admiraal@sonarsource.com>2019-04-18 15:21:23 +0200
committersonartech <sonartech@sonarsource.com>2019-05-07 09:54:28 +0200
commit68b2e2417488e6780ff56fe3d032af0b52be583c (patch)
treedc393fcee0eb37486f446f18c0d35d96937ba7d3 /server/sonar-web
parente4123e7fd049f062e4ca1b9ef9f87790d3b293d9 (diff)
downloadsonarqube-68b2e2417488e6780ff56fe3d032af0b52be583c.tar.gz
sonarqube-68b2e2417488e6780ff56fe3d032af0b52be583c.zip
SONAR-11180 Add Standards facet to the Rules page
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx68
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx27
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/FacetsList-test.tsx79
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/FacetsList-test.tsx.snap81
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/query.ts21
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx4
6 files changed, 261 insertions, 19 deletions
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx
index 0ae3109d4a0..e9e9ef63943 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx
@@ -58,6 +58,12 @@ import {
Store,
getAppState
} from '../../../store/rootReducer';
+import {
+ shouldOpenStandardsFacet,
+ shouldOpenSonarSourceSecurityFacet,
+ shouldOpenStandardsChildFacet,
+ STANDARDS
+} from '../../issues/utils';
import { translate } from '../../../helpers/l10n';
import { hasPrivateAccess } from '../../../helpers/organizations';
import {
@@ -107,10 +113,18 @@ export class App extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
+ const query = parseQuery(props.location.query);
this.state = {
loading: true,
- openFacets: { languages: true, types: true },
- query: parseQuery(props.location.query),
+ openFacets: {
+ languages: true,
+ owaspTop10: shouldOpenStandardsChildFacet({}, query, 'owaspTop10'),
+ sansTop25: shouldOpenStandardsChildFacet({}, query, 'sansTop25'),
+ sonarsourceSecurity: shouldOpenSonarSourceSecurityFacet({}, query),
+ standards: shouldOpenStandardsFacet({}, query),
+ types: true
+ },
+ query,
referencedProfiles: {},
referencedRepositories: {},
rules: []
@@ -184,11 +198,13 @@ export class App extends React.PureComponent<Props, State> {
return open && rules.find(rule => rule.key === open);
};
- getFacetsToFetch = () =>
- Object.keys(this.state.openFacets)
- .filter((facet: FacetKey) => this.state.openFacets[facet])
+ getFacetsToFetch = () => {
+ const { openFacets } = this.state;
+ return Object.keys(openFacets)
+ .filter((facet: FacetKey) => openFacets[facet])
.filter((facet: FacetKey) => shouldRequestFacet(facet))
.map((facet: FacetKey) => getServerFacet(facet));
+ };
getFieldsToFetch = () => {
const fields = [
@@ -282,7 +298,6 @@ export class App extends React.PureComponent<Props, State> {
};
fetchFacet = (facet: FacetKey) => {
- this.setState({ loading: true });
this.makeFetchRequest({ ps: 1, facets: getServerFacet(facet) }).then(({ facets }) => {
if (this.mounted) {
this.setState(state => ({ facets: { ...state.facets, ...facets }, loading: false }));
@@ -412,19 +427,46 @@ export class App extends React.PureComponent<Props, State> {
this.closeRule();
};
- handleFilterChange = (changes: Partial<Query>) =>
+ handleFilterChange = (changes: Partial<Query>) => {
this.props.router.push({
pathname: this.props.location.pathname,
query: serializeQuery({ ...this.state.query, ...changes })
});
- handleFacetToggle = (facet: keyof Query) => {
- this.setState(state => ({
- openFacets: { ...state.openFacets, [facet]: !state.openFacets[facet] }
+ this.setState(({ openFacets }) => ({
+ openFacets: {
+ ...openFacets,
+ sonarsourceSecurity: shouldOpenSonarSourceSecurityFacet(openFacets, changes),
+ standards: shouldOpenStandardsFacet(openFacets, changes)
+ }
}));
- if (shouldRequestFacet(facet) && (!this.state.facets || !this.state.facets[facet])) {
- this.fetchFacet(facet);
- }
+ };
+
+ handleFacetToggle = (property: string) => {
+ this.setState(state => {
+ const willOpenProperty = !state.openFacets[property];
+ const newState = {
+ loading: state.loading,
+ openFacets: { ...state.openFacets, [property]: willOpenProperty }
+ };
+
+ // Try to open sonarsource security "subfacet" by default if the standard facet is open
+ if (willOpenProperty && property === STANDARDS) {
+ newState.openFacets.sonarsourceSecurity = shouldOpenSonarSourceSecurityFacet(
+ newState.openFacets,
+ state.query
+ );
+ // Force loading of sonarsource security facet data
+ property = newState.openFacets.sonarsourceSecurity ? 'sonarsourceSecurity' : property;
+ }
+
+ if (shouldRequestFacet(property) && (!state.facets || !state.facets[property])) {
+ newState.loading = true;
+ this.fetchFacet(property);
+ }
+
+ return newState;
+ });
};
handleReload = () => this.fetchFirstRules();
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx
index b934ab4a1f7..1eb17fa8cfd 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx
@@ -29,13 +29,14 @@ import StatusFacet from './StatusFacet';
import TagFacet from './TagFacet';
import TemplateFacet from './TemplateFacet';
import TypeFacet from './TypeFacet';
-import { Facets, Query, FacetKey, OpenFacets } from '../query';
+import StandardFacet from '../../issues/sidebar/StandardFacet';
+import { Facets, Query, OpenFacets } from '../query';
import { Profile } from '../../../api/quality-profiles';
interface Props {
facets?: Facets;
hideProfileFacet?: boolean;
- onFacetToggle: (facet: FacetKey) => void;
+ onFacetToggle: (facet: string) => void;
onFilterChange: (changes: Partial<Query>) => void;
openFacets: OpenFacets;
organization: string | undefined;
@@ -105,6 +106,28 @@ export default function FacetsList(props: Props) {
stats={props.facets && props.facets.statuses}
values={props.query.statuses}
/>
+ <StandardFacet
+ cwe={props.query.cwe}
+ cweOpen={!!props.openFacets.cwe}
+ cweStats={props.facets && props.facets.cwe}
+ fetchingCwe={false}
+ fetchingOwaspTop10={false}
+ fetchingSansTop25={false}
+ fetchingSonarSourceSecurity={false}
+ onChange={props.onFilterChange}
+ onToggle={props.onFacetToggle}
+ open={!!props.openFacets.standards}
+ owaspTop10={props.query.owaspTop10}
+ owaspTop10Open={!!props.openFacets.owaspTop10}
+ owaspTop10Stats={props.facets && props.facets.owaspTop10}
+ query={props.query}
+ sansTop25={props.query.sansTop25}
+ sansTop25Open={!!props.openFacets.sansTop25}
+ sansTop25Stats={props.facets && props.facets.sansTop25}
+ sonarsourceSecurity={props.query.sonarsourceSecurity}
+ sonarsourceSecurityOpen={!!props.openFacets.sonarsourceSecurity}
+ sonarsourceSecurityStats={props.facets && props.facets.sonarsourceSecurity}
+ />
<AvailableSinceFacet
onChange={props.onFilterChange}
onToggle={props.onFacetToggle}
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/FacetsList-test.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/FacetsList-test.tsx
new file mode 100644
index 00000000000..51839b88b04
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/FacetsList-test.tsx
@@ -0,0 +1,79 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import FacetsList from '../FacetsList';
+import { Query } from '../../query';
+
+it('should render correctly', () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should correctly hide profile facets', () => {
+ const wrapper = shallowRender({ hideProfileFacet: true });
+ expect(wrapper.find('ProfileFacet').length).toEqual(0);
+ expect(wrapper.find('InheritanceFacet').length).toEqual(0);
+ expect(wrapper.find('ActivationSeverityFacet').length).toEqual(0);
+});
+
+it('should correctly hide the template facet', () => {
+ const wrapper = shallowRender({ organizationsEnabled: true });
+ expect(wrapper.find('TemplateFacet').length).toEqual(0);
+});
+
+it('should correctly enable/disable the language facet', () => {
+ const wrapper = shallowRender({ query: { profile: 'foo' } });
+ expect(wrapper.find('Connect(LanguageFacet)').prop('disabled')).toBe(true);
+
+ wrapper.setProps({ query: {} }).update();
+ expect(wrapper.find('Connect(LanguageFacet)').prop('disabled')).toBe(false);
+});
+
+it('should correctly enable/disable the activation severity facet', () => {
+ const wrapper = shallowRender();
+ expect(wrapper.find('ActivationSeverityFacet').prop('disabled')).toBe(true);
+
+ wrapper.setProps({ query: { activation: 'foo' }, selectedProfile: { key: 'foo' } }).update();
+ expect(wrapper.find('ActivationSeverityFacet').prop('disabled')).toBe(false);
+});
+
+it('should correctly enable/disable the inheritcance facet', () => {
+ const wrapper = shallowRender();
+ expect(wrapper.find('InheritanceFacet').prop('disabled')).toBe(true);
+
+ wrapper.setProps({ selectedProfile: { isInherited: true } }).update();
+ expect(wrapper.find('InheritanceFacet').prop('disabled')).toBe(false);
+});
+
+function shallowRender(props = {}) {
+ return shallow(
+ <FacetsList
+ onFacetToggle={jest.fn()}
+ onFilterChange={jest.fn()}
+ openFacets={{}}
+ organization="foo"
+ query={{} as Query}
+ referencedProfiles={{}}
+ referencedRepositories={{}}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/FacetsList-test.tsx.snap b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/FacetsList-test.tsx.snap
new file mode 100644
index 00000000000..83f37e7d7a4
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/FacetsList-test.tsx.snap
@@ -0,0 +1,81 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<Fragment>
+ <Connect(LanguageFacet)
+ disabled={false}
+ onChange={[MockFunction]}
+ onToggle={[MockFunction]}
+ open={false}
+ />
+ <TypeFacet
+ onChange={[MockFunction]}
+ onToggle={[MockFunction]}
+ open={false}
+ />
+ <TagFacet
+ onChange={[MockFunction]}
+ onToggle={[MockFunction]}
+ open={false}
+ organization="foo"
+ />
+ <Connect(RepositoryFacet)
+ onChange={[MockFunction]}
+ onToggle={[MockFunction]}
+ open={false}
+ referencedRepositories={Object {}}
+ />
+ <DefaultSeverityFacet
+ onChange={[MockFunction]}
+ onToggle={[MockFunction]}
+ open={false}
+ />
+ <StatusFacet
+ onChange={[MockFunction]}
+ onToggle={[MockFunction]}
+ open={false}
+ />
+ <StandardFacet
+ cweOpen={false}
+ fetchingCwe={false}
+ fetchingOwaspTop10={false}
+ fetchingSansTop25={false}
+ fetchingSonarSourceSecurity={false}
+ onChange={[MockFunction]}
+ onToggle={[MockFunction]}
+ open={false}
+ owaspTop10Open={false}
+ query={Object {}}
+ sansTop25Open={false}
+ sonarsourceSecurityOpen={false}
+ />
+ <InjectIntl(AvailableSinceFacet)
+ onChange={[MockFunction]}
+ onToggle={[MockFunction]}
+ open={false}
+ />
+ <TemplateFacet
+ onChange={[MockFunction]}
+ onToggle={[MockFunction]}
+ open={false}
+ />
+ <ProfileFacet
+ onChange={[MockFunction]}
+ onToggle={[MockFunction]}
+ open={false}
+ referencedProfiles={Object {}}
+ />
+ <InheritanceFacet
+ disabled={true}
+ onChange={[MockFunction]}
+ onToggle={[MockFunction]}
+ open={false}
+ />
+ <ActivationSeverityFacet
+ disabled={true}
+ onChange={[MockFunction]}
+ onToggle={[MockFunction]}
+ open={false}
+ />
+</Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/query.ts b/server/sonar-web/src/main/js/apps/coding-rules/query.ts
index 93ace75af10..54f84651a61 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/query.ts
+++ b/server/sonar-web/src/main/js/apps/coding-rules/query.ts
@@ -37,13 +37,17 @@ export interface Query {
activationSeverities: string[];
availableSince: Date | undefined;
compareToProfile: string | undefined;
+ cwe: string[];
inheritance: T.RuleInheritance | undefined;
languages: string[];
+ owaspTop10: string[];
profile: string | undefined;
repositories: string[];
ruleKey: string | undefined;
+ sansTop25: string[];
searchQuery: string | undefined;
severities: string[];
+ sonarsourceSecurity: string[];
statuses: string[];
tags: string[];
template: boolean | undefined;
@@ -58,7 +62,7 @@ export interface Facet {
export type Facets = { [F in FacetKey]?: Facet };
-export type OpenFacets = { [F in FacetKey]?: boolean };
+export type OpenFacets = T.Dict<boolean>;
export interface Activation {
inherit: T.RuleInheritance;
@@ -77,13 +81,17 @@ export function parseQuery(query: RawQuery): Query {
activationSeverities: parseAsArray(query.active_severities, parseAsString),
availableSince: parseAsDate(query.available_since),
compareToProfile: parseAsOptionalString(query.compareToProfile),
+ cwe: parseAsArray(query.cwe, parseAsString),
inheritance: parseAsInheritance(query.inheritance),
languages: parseAsArray(query.languages, parseAsString),
+ owaspTop10: parseAsArray(query.owaspTop10, parseAsString),
profile: parseAsOptionalString(query.qprofile),
repositories: parseAsArray(query.repositories, parseAsString),
ruleKey: parseAsOptionalString(query.rule_key),
+ sansTop25: parseAsArray(query.sansTop25, parseAsString),
searchQuery: parseAsOptionalString(query.q),
severities: parseAsArray(query.severities, parseAsString),
+ sonarsourceSecurity: parseAsArray(query.sonarsourceSecurity, parseAsString),
statuses: parseAsArray(query.statuses, parseAsString),
tags: parseAsArray(query.tags, parseAsString),
template: parseAsOptionalBoolean(query.is_template),
@@ -97,14 +105,18 @@ export function serializeQuery(query: Query): RawQuery {
active_severities: serializeStringArray(query.activationSeverities),
available_since: serializeDateShort(query.availableSince),
compareToProfile: serializeString(query.compareToProfile),
+ cwe: serializeStringArray(query.cwe),
inheritance: serializeInheritance(query.inheritance),
is_template: serializeOptionalBoolean(query.template),
languages: serializeStringArray(query.languages),
+ owaspTop10: serializeStringArray(query.owaspTop10),
q: serializeString(query.searchQuery),
qprofile: serializeString(query.profile),
repositories: serializeStringArray(query.repositories),
rule_key: serializeString(query.ruleKey),
+ sansTop25: serializeStringArray(query.sansTop25),
severities: serializeStringArray(query.severities),
+ sonarsourceSecurity: serializeStringArray(query.sonarsourceSecurity),
statuses: serializeStringArray(query.statuses),
tags: serializeStringArray(query.tags),
types: serializeStringArray(query.types)
@@ -115,12 +127,17 @@ export function areQueriesEqual(a: RawQuery, b: RawQuery) {
return queriesEqual(parseQuery(a), parseQuery(b));
}
-export function shouldRequestFacet(facet: FacetKey) {
+export function shouldRequestFacet(facet: string): facet is FacetKey {
const facetsToRequest = [
'activationSeverities',
+ 'cwe',
'languages',
+ 'owaspTop10',
'repositories',
+ 'sansTop25',
'severities',
+ 'sonarsourceSecurity',
+ 'standard',
'statuses',
'tags',
'types'
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx
index 6ef857b1bd2..46482c38c93 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx
@@ -45,14 +45,14 @@ interface Props {
fetchingOwaspTop10: boolean;
fetchingSansTop25: boolean;
fetchingSonarSourceSecurity: boolean;
- loadSearchResultCount: (property: string, changes: Partial<Query>) => Promise<Facet>;
+ loadSearchResultCount?: (property: string, changes: Partial<Query>) => Promise<Facet>;
onChange: (changes: Partial<Query>) => void;
onToggle: (property: string) => void;
open: boolean;
owaspTop10: string[];
owaspTop10Open: boolean;
owaspTop10Stats: T.Dict<number> | undefined;
- query: Query;
+ query: Partial<Query>;
sansTop25: string[];
sansTop25Open: boolean;
sansTop25Stats: T.Dict<number> | undefined;