diff options
author | Wouter Admiraal <wouter.admiraal@sonarsource.com> | 2019-04-18 15:21:23 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2019-05-07 09:54:28 +0200 |
commit | 68b2e2417488e6780ff56fe3d032af0b52be583c (patch) | |
tree | dc393fcee0eb37486f446f18c0d35d96937ba7d3 /server/sonar-web | |
parent | e4123e7fd049f062e4ca1b9ef9f87790d3b293d9 (diff) | |
download | sonarqube-68b2e2417488e6780ff56fe3d032af0b52be583c.tar.gz sonarqube-68b2e2417488e6780ff56fe3d032af0b52be583c.zip |
SONAR-11180 Add Standards facet to the Rules page
Diffstat (limited to 'server/sonar-web')
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; |