From 68b2e2417488e6780ff56fe3d032af0b52be583c Mon Sep 17 00:00:00 2001 From: Wouter Admiraal Date: Thu, 18 Apr 2019 15:21:23 +0200 Subject: SONAR-11180 Add Standards facet to the Rules page --- .../main/js/apps/coding-rules/components/App.tsx | 68 ++++++++++++++---- .../js/apps/coding-rules/components/FacetsList.tsx | 27 +++++++- .../components/__tests__/FacetsList-test.tsx | 79 +++++++++++++++++++++ .../__snapshots__/FacetsList-test.tsx.snap | 81 ++++++++++++++++++++++ .../src/main/js/apps/coding-rules/query.ts | 21 +++++- .../main/js/apps/issues/sidebar/StandardFacet.tsx | 4 +- 6 files changed, 261 insertions(+), 19 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/FacetsList-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/FacetsList-test.tsx.snap (limited to 'server/sonar-web') 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 { 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 { 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 { }; 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 { this.closeRule(); }; - handleFilterChange = (changes: Partial) => + handleFilterChange = (changes: Partial) => { 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) => 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} /> + { + 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( + + ); +} 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`] = ` + + + + + + + + + + + + + + +`; 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; 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) => Promise; + loadSearchResultCount?: (property: string, changes: Partial) => Promise; onChange: (changes: Partial) => void; onToggle: (property: string) => void; open: boolean; owaspTop10: string[]; owaspTop10Open: boolean; owaspTop10Stats: T.Dict | undefined; - query: Query; + query: Partial; sansTop25: string[]; sansTop25Open: boolean; sansTop25Stats: T.Dict | undefined; -- cgit v1.2.3