From: 7PH Date: Thu, 13 Jul 2023 14:39:53 +0000 (+0200) Subject: SONAR-18424 Migrate rules page facet tests to RTL X-Git-Tag: 10.2.0.77647~376 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=52ea9dba8c99845d6e00693fdee1e23c1a54e29c;p=sonarqube.git SONAR-18424 Migrate rules page facet tests to RTL --- diff --git a/server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts index e76367f8005..1132bbe8d15 100644 --- a/server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts @@ -19,6 +19,7 @@ */ import { cloneDeep, countBy, pick, trim } from 'lodash'; import { RuleDescriptionSections } from '../../apps/coding-rules/rule'; +import { getStandards } from '../../helpers/security-standard'; import { mockCurrentUser, mockPaging, @@ -30,6 +31,7 @@ import { import { RuleRepository, SearchRulesResponse } from '../../types/coding-rules'; import { RawIssuesResponse } from '../../types/issues'; import { SearchRulesQuery } from '../../types/rules'; +import { SecurityStandard, Standards } from '../../types/security'; import { Dict, Rule, RuleActivation, RuleDetails, RulesUpdateRequest } from '../../types/types'; import { NoticeType } from '../../types/users'; import { getFacet } from '../issues'; @@ -48,6 +50,7 @@ import { createRule, deleteRule, getRuleDetails, + getRuleRepositories, getRuleTags, getRulesApp, searchRules, @@ -55,19 +58,29 @@ import { } from '../rules'; import { dismissNotice, getCurrentUser } from '../users'; -interface FacetFilter { - languages?: string; - tags?: string; - available_since?: string; - q?: string; - types?: string; - severities?: string; - is_template?: string | boolean; -} +type FacetFilter = Pick< + SearchRulesQuery, + | 'languages' + | 'tags' + | 'available_since' + | 'q' + | 'types' + | 'severities' + | 'repositories' + | 'qprofile' + | 'sonarsourceSecurity' + | 'owaspTop10' + | 'owaspTop10-2021' + | 'cwe' + | 'is_template' +>; const FACET_RULE_MAP: { [key: string]: keyof Rule } = { languages: 'lang', types: 'type', + severities: 'severity', + statuses: 'status', + tags: 'tags', }; export const RULE_TAGS_MOCK = ['awesome', 'cute', 'nice']; @@ -82,11 +95,15 @@ export default class CodingRulesServiceMock { isAdmin = false; applyWithWarning = false; dismissedNoticesEP = false; + standardsToRules: Partial<{ [category in keyof Standards]: { [standard: string]: string[] } }> = + {}; + + qualityProfilesToRules: { [qp: string]: string[] } = {}; constructor() { this.repositories = [ - mockRuleRepository({ key: 'repo1' }), - mockRuleRepository({ key: 'repo2' }), + mockRuleRepository({ key: 'repo1', name: 'Repository 1' }), + mockRuleRepository({ key: 'repo2', name: 'Repository 2' }), ]; this.qualityProfile = [ mockQualityProfile({ key: 'p1', name: 'QP Foo', language: 'java', languageName: 'Java' }), @@ -108,10 +125,12 @@ export default class CodingRulesServiceMock { this.defaultRules = [ mockRuleDetails({ key: 'rule1', + repo: 'repo1', type: 'BUG', lang: 'java', langName: 'Java', name: 'Awsome java rule', + tags: ['awesome'], params: [ { key: '1', type: 'TEXT', htmlDesc: 'html description for key 1' }, { key: '2', type: 'NUMBER', defaultValue: 'default value for key 2' }, @@ -119,7 +138,9 @@ export default class CodingRulesServiceMock { }), mockRuleDetails({ key: 'rule2', + repo: 'repo1', name: 'Hot hotspot', + tags: ['awesome'], type: 'SECURITY_HOTSPOT', lang: 'js', descriptionSections: [ @@ -134,7 +155,13 @@ export default class CodingRulesServiceMock { ], langName: 'JavaScript', }), - mockRuleDetails({ key: 'rule3', name: 'Unknown rule', lang: 'js', langName: 'JavaScript' }), + mockRuleDetails({ + key: 'rule3', + repo: 'repo2', + name: 'Unknown rule', + lang: 'js', + langName: 'JavaScript', + }), mockRuleDetails({ key: 'rule4', type: 'BUG', @@ -214,7 +241,7 @@ export default class CodingRulesServiceMock { severity: 'MINOR', lang: 'py', langName: 'Python', - tags: ['awesome'], + tags: ['awesome', 'cute'], name: 'Custom Rule based on rule8', params: [ { key: '1', type: 'TEXT', htmlDesc: 'html description for key 1' }, @@ -248,11 +275,32 @@ export default class CodingRulesServiceMock { [this.defaultRules[0].key]: [mockRuleActivation({ qProfile: 'p1' })], }; + this.standardsToRules = { + [SecurityStandard.SONARSOURCE]: { + 'buffer-overflow': ['rule1', 'rule2', 'rule3', 'rule4', 'rule5', 'rule6'], + }, + [SecurityStandard.OWASP_TOP10_2021]: { + a2: ['rule1', 'rule2', 'rule3', 'rule4', 'rule5'], + }, + [SecurityStandard.OWASP_TOP10]: { + a3: ['rule1', 'rule2', 'rule3', 'rule4'], + }, + [SecurityStandard.CWE]: { + '102': ['rule1', 'rule2', 'rule3'], + '297': ['rule1', 'rule4'], + }, + }; + + this.qualityProfilesToRules = { + p3: ['rule1', 'rule2', 'rule3', 'rule4', 'rule5', 'rule6', 'rule7', 'rule8'], + }; + jest.mocked(updateRule).mockImplementation(this.handleUpdateRule); jest.mocked(createRule).mockImplementation(this.handleCreateRule); jest.mocked(deleteRule).mockImplementation(this.handleDeleteRule); jest.mocked(searchRules).mockImplementation(this.handleSearchRules); jest.mocked(getRuleDetails).mockImplementation(this.handleGetRuleDetails); + jest.mocked(getRuleRepositories).mockImplementation(this.handleGetRuleRepositories); jest.mocked(searchQualityProfiles).mockImplementation(this.handleSearchQualityProfiles); jest.mocked(getRulesApp).mockImplementation(this.handleGetRulesApp); jest.mocked(bulkActivateRules).mockImplementation(this.handleBulkActivateRules); @@ -293,6 +341,12 @@ export default class CodingRulesServiceMock { types, tags, is_template, + repositories, + qprofile, + sonarsourceSecurity, + owaspTop10, + 'owaspTop10-2021': owasp2021Top10, + cwe, }: FacetFilter) { let filteredRules = this.rules; if (types) { @@ -312,10 +366,34 @@ export default class CodingRulesServiceMock { if (is_template !== undefined) { filteredRules = filteredRules.filter((r) => (is_template ? r.isTemplate : !r.isTemplate)); } + if (repositories) { + filteredRules = filteredRules.filter((r) => r.lang && repositories.includes(r.repo)); + } + if (qprofile) { + const rules = this.qualityProfilesToRules[qprofile] ?? []; + filteredRules = filteredRules.filter((r) => rules.includes(r.key)); + } + if (sonarsourceSecurity) { + const matchingRules = + this.standardsToRules[SecurityStandard.SONARSOURCE]?.[sonarsourceSecurity] ?? []; + filteredRules = filteredRules.filter((r) => matchingRules.includes(r.key)); + } + if (owasp2021Top10) { + const matchingRules = + this.standardsToRules[SecurityStandard.OWASP_TOP10_2021]?.[owasp2021Top10] ?? []; + filteredRules = filteredRules.filter((r) => matchingRules.includes(r.key)); + } + if (owaspTop10) { + const matchingRules = this.standardsToRules[SecurityStandard.OWASP_TOP10]?.[owaspTop10] ?? []; + filteredRules = filteredRules.filter((r) => matchingRules.includes(r.key)); + } + if (cwe) { + const matchingRules = this.standardsToRules[SecurityStandard.CWE]?.[cwe] ?? []; + filteredRules = filteredRules.filter((r) => matchingRules.includes(r.key)); + } if (q && q.length > 2) { filteredRules = filteredRules.filter((r) => r.name.includes(q)); } - if (tags) { filteredRules = filteredRules.filter((r) => r.tags && r.tags.some((t) => tags.includes(t))); } @@ -383,6 +461,12 @@ export default class CodingRulesServiceMock { }); }; + handleGetRuleRepositories = (parameters: { + q: string; + }): Promise> => { + return this.reply(this.repositories.filter((r) => r.name.includes(parameters.q))); + }; + handleUpdateRule = (data: RulesUpdateRequest): Promise => { const rule = this.rules.find((r) => r.key === data.key); if (rule === undefined) { @@ -450,7 +534,7 @@ export default class CodingRulesServiceMock { return this.reply(undefined); }; - handleSearchRules = ({ + handleSearchRules = async ({ facets, types, languages, @@ -458,22 +542,54 @@ export default class CodingRulesServiceMock { ps, available_since, severities, + repositories, + qprofile, + sonarsourceSecurity, + owaspTop10, + 'owaspTop10-2021': owasp2021Top10, + cwe, tags, q, rule_key, is_template, }: SearchRulesQuery): Promise => { - const countFacet = (facets || '').split(',').map((facet: keyof Rule) => { - const facetCount = countBy( - this.rules.map((r) => r[FACET_RULE_MAP[facet] || facet] as string) - ); - return { - property: facet, - values: Object.keys(facetCount).map((val) => ({ val, count: facetCount[val] })), - }; - }); - const currentPs = ps || 10; - const currentP = p || 1; + const standards = await getStandards(); + const facetCounts: Array<{ property: string; values: { val: string; count: number }[] }> = []; + for (const facet of facets?.split(',') ?? []) { + // If we can count facet values from the list of rules + if (FACET_RULE_MAP[facet]) { + const counts = countBy(this.rules.map((r) => r[FACET_RULE_MAP[facet]])); + const values = Object.keys(counts).map((val) => ({ val, count: counts[val] })); + facetCounts.push({ + property: facet, + values, + }); + } else if (facet === 'repositories') { + facetCounts.push({ + property: facet, + values: this.repositories.map((repo) => ({ + val: repo.key, + count: this.rules.filter((r) => r.repo === repo.key).length, + })), + }); + } else if (typeof (standards as Dict)[facet] === 'object') { + // When a standards facet is requested, we return all the values with a count of 1 + facetCounts.push({ + property: facet, + values: Object.keys((standards as any)[facet]).map((val: string) => ({ + val, + count: 1, + })), + }); + } else { + facetCounts.push({ + property: facet, + values: [], + }); + } + } + const currentPs = ps ?? 10; + const currentP = p ?? 1; let filteredRules: Rule[] = []; if (rule_key) { filteredRules = this.getRulesWithoutDetails(this.rules).filter((r) => r.key === rule_key); @@ -483,15 +599,21 @@ export default class CodingRulesServiceMock { available_since, q, severities, + repositories, types, tags, is_template, + qprofile, + sonarsourceSecurity, + owaspTop10, + 'owaspTop10-2021': owasp2021Top10, + cwe, }); } const responseRules = filteredRules.slice((currentP - 1) * currentPs, currentP * currentPs); return this.reply({ rules: responseRules, - facets: countFacet, + facets: facetCounts, paging: mockPaging({ total: filteredRules.length, pageIndex: currentP, diff --git a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts index 9dbe08d6be5..39a5ef3e8e6 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts +++ b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts @@ -82,7 +82,7 @@ describe('Rules app', () => { }); describe('filtering', () => { - it('filters by facets', async () => { + it('combine facet filters', async () => { const { ui, user } = getPageObjects(); const { pickDate } = dateInputEvent(user); renderCodingRulesApp(mockCurrentUser()); @@ -91,16 +91,117 @@ describe('Rules app', () => { expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10); // Filter by language facet - await user.click(ui.facetItem('py').get()); + await act(async () => { + await user.type(ui.facetSearchInput('search.search_for_languages').get(), 'ja'); + await user.click(ui.facetItem('JavaScript').get()); + }); + expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(2); + + // Clear language facet and search box, and filter by python language + await act(async () => { + await user.clear(ui.facetSearchInput('search.search_for_languages').get()); + await user.click(ui.facetItem('py').get()); + }); expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(6); // Filter by date facet - await user.click(await ui.availableSinceFacet.find()); - await pickDate(ui.availableSinceDateField.get(), parseDate('Nov 1, 2022')); + await act(async () => { + await user.click(await ui.availableSinceFacet.find()); + await pickDate(ui.availableSinceDateField.get(), parseDate('Nov 1, 2022')); + }); expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(1); // Clear filters - await user.click(ui.clearAllFiltersButton.get()); + await act(async () => { + await user.click(ui.clearAllFiltersButton.get()); + }); + expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10); + + // Filter by repository + await act(async () => { + await user.click(ui.repositoriesFacet.get()); + await user.click(ui.facetItem('Repository 1').get()); + }); + expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(2); + + // Search second repository + await act(async () => { + await user.type(ui.facetSearchInput('search.search_for_repositories').get(), 'y 2'); + await user.click(ui.facetItem('Repository 2').get()); + }); + expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(1); + + // Clear filters + await act(async () => { + await user.click(ui.clearAllFiltersButton.get()); + }); + expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10); + + // Filter by quality profile + await act(async () => { + await user.click(ui.qpFacet.get()); + await user.click(ui.facetItem('QP FooBar Java').get()); + }); + expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(8); + + // Filter by tag + await act(async () => { + await user.click(ui.facetClear('coding_rules.facet.qprofile').get()); // Clear quality profile facet + await user.click(ui.tagsFacet.get()); + await user.click(ui.facetItem('awesome').get()); + }); + expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(5); + + // Search by tag + await act(async () => { + await user.type(ui.facetSearchInput('search.search_for_tags').get(), 'te'); + await user.click(ui.facetItem('cute').get()); + }); + expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(1); + }); + + it('filter by standards', async () => { + const { ui, user } = getPageObjects(); + renderCodingRulesApp(mockCurrentUser()); + await ui.appLoaded(); + + expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10); + await act(async () => { + await user.click(ui.standardsFacet.get()); + await user.click(ui.facetItem('Buffer Overflow').get()); + }); + expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(6); + + await act(async () => { + await user.click(ui.standardsOwasp2021Top10Facet.get()); + await user.click(ui.facetItem('A2 - Cryptographic Failures').get()); + await user.click(ui.standardsOwasp2021Top10Facet.get()); // Close facet + }); + expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(5); + + await act(async () => { + await user.click(ui.standardsOwasp2017Top10Facet.get()); + await user.click(ui.facetItem('A3 - Sensitive Data Exposure').get()); + }); + expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(4); + + await act(async () => { + await user.click(ui.standardsCweFacet.get()); + await user.click(ui.facetItem('CWE-102 - Struts: Duplicate Validation Forms').get()); + }); + expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(3); + + await act(async () => { + await user.type(ui.facetSearchInput('search.search_for_cwe').get(), 'Certificate'); + await user.click( + ui.facetItem('CWE-297 - Improper Validation of Certificate with Host Mismatch').get() + ); + }); + expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(2); + + await act(async () => { + await user.click(ui.facetClear('issues.facet.standards').get()); + }); expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10); }); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacet.tsx index eabd93eaa72..1e6fc871fbf 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacet.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacet.tsx @@ -31,7 +31,7 @@ interface Props extends BasicProps { languages: Languages; } -export class LanguageFacet extends React.PureComponent { +class LanguageFacet extends React.PureComponent { getLanguageName = (languageKey: string) => { const language = this.props.languages[languageKey]; return language ? language.name : languageKey; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RepositoryFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RepositoryFacet.tsx index 321e49e4c65..b8c16f7ceb5 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RepositoryFacet.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RepositoryFacet.tsx @@ -35,7 +35,7 @@ interface Props extends BasicProps, StateProps { referencedRepositories: Dict<{ key: string; language: string; name: string }>; } -export class RepositoryFacet extends React.PureComponent { +class RepositoryFacet extends React.PureComponent { getLanguageName = (languageKey: string) => { const { languages } = this.props; const language = languages[languageKey]; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/StandardFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/StandardFacet.tsx index 28f61ce9234..3b6382f99ea 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/StandardFacet.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/StandardFacet.tsx @@ -28,6 +28,7 @@ import FacetItemsList from '../../../components/facet/FacetItemsList'; import ListStyleFacet from '../../../components/facet/ListStyleFacet'; import ListStyleFacetFooter from '../../../components/facet/ListStyleFacetFooter'; import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint'; +import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { translate } from '../../../helpers/l10n'; import { highlightTerm } from '../../../helpers/search'; import { @@ -171,22 +172,6 @@ export class StandardFacet extends React.PureComponent { return `facet_${property}`; }; - handleHeaderClick = () => { - this.props.onToggle(this.property); - }; - - handleOwaspTop10HeaderClick = () => { - this.props.onToggle('owaspTop10'); - }; - - handleOwaspTop102021HeaderClick = () => { - this.props.onToggle('owaspTop10-2021'); - }; - - handleSonarSourceSecurityHeaderClick = () => { - this.props.onToggle('sonarsourceSecurity'); - }; - handleClear = () => { this.props.onChange({ [this.property]: [], @@ -241,7 +226,7 @@ export class StandardFacet extends React.PureComponent { : Promise.resolve({}); }; - renderList = ( + renderOwaspList = ( statsProp: StatsProp, valuesProp: ValuesProp, renderName: (standards: Standards, category: string) => string, @@ -251,7 +236,7 @@ export class StandardFacet extends React.PureComponent { const values = this.props[valuesProp]; if (!stats) { - return null; + return ; } const categories = sortBy(Object.keys(stats), (key) => -stats[key]); @@ -314,7 +299,7 @@ export class StandardFacet extends React.PureComponent { }; renderOwaspTop10List() { - return this.renderList( + return this.renderOwaspList( 'owaspTop10Stats', SecurityStandard.OWASP_TOP10, renderOwaspTop10Category, @@ -323,7 +308,7 @@ export class StandardFacet extends React.PureComponent { } renderOwaspTop102021List() { - return this.renderList( + return this.renderOwaspList( 'owaspTop10-2021Stats', SecurityStandard.OWASP_TOP10_2021, renderOwaspTop102021Category, @@ -336,7 +321,7 @@ export class StandardFacet extends React.PureComponent { const values = this.props.sonarsourceSecurity; if (!stats) { - return null; + return ; } const sortedItems = sortBy( @@ -440,7 +425,7 @@ export class StandardFacet extends React.PureComponent { fetching={fetchingSonarSourceSecurity} id={this.getFacetHeaderId(SecurityStandard.SONARSOURCE)} name={translate('issues.facet.sonarsourceSecurity')} - onClick={this.handleSonarSourceSecurityHeaderClick} + onClick={() => this.props.onToggle('sonarsourceSecurity')} open={sonarsourceSecurityOpen} values={sonarsourceSecurity.map((item) => renderSonarSourceSecurityCategory(this.state.standards, item) @@ -460,7 +445,7 @@ export class StandardFacet extends React.PureComponent { fetching={fetchingOwaspTop102021} id={this.getFacetHeaderId(SecurityStandard.OWASP_TOP10_2021)} name={translate('issues.facet.owaspTop10_2021')} - onClick={this.handleOwaspTop102021HeaderClick} + onClick={() => this.props.onToggle('owaspTop10-2021')} open={owaspTop102021Open} values={owaspTop102021.map((item) => renderOwaspTop102021Category(this.state.standards, item) @@ -480,7 +465,7 @@ export class StandardFacet extends React.PureComponent { fetching={fetchingOwaspTop10} id={this.getFacetHeaderId(SecurityStandard.OWASP_TOP10)} name={translate('issues.facet.owaspTop10')} - onClick={this.handleOwaspTop10HeaderClick} + onClick={() => this.props.onToggle('owaspTop10')} open={owaspTop10Open} values={owaspTop10.map((item) => renderOwaspTop10Category(this.state.standards, item))} /> @@ -528,7 +513,7 @@ export class StandardFacet extends React.PureComponent { id={this.getFacetHeaderId(this.property)} name={translate('issues.facet', this.property)} onClear={this.handleClear} - onClick={this.handleHeaderClick} + onClick={() => this.props.onToggle(this.property)} open={open} values={this.getValues()} /> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/utils-test.tsx b/server/sonar-web/src/main/js/apps/coding-rules/utils-test.tsx index 54d92c3ce74..d7e9723e903 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/utils-test.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/utils-test.tsx @@ -52,9 +52,14 @@ const selectors = { severetiesFacet: byRole('button', { name: 'coding_rules.facet.severities' }), statusesFacet: byRole('button', { name: 'coding_rules.facet.statuses' }), standardsFacet: byRole('button', { name: 'issues.facet.standards' }), + standardsOwasp2017Top10Facet: byRole('button', { name: 'issues.facet.owaspTop10' }), + standardsOwasp2021Top10Facet: byRole('button', { name: 'issues.facet.owaspTop10_2021' }), + standardsCweFacet: byRole('button', { name: 'issues.facet.cwe' }), availableSinceFacet: byRole('button', { name: 'coding_rules.facet.available_since' }), templateFacet: byRole('button', { name: 'coding_rules.facet.template' }), qpFacet: byRole('button', { name: 'coding_rules.facet.qprofile' }), + facetClear: (name: string) => byRole('button', { name: `clear_x_filter.${name}` }), + facetSearchInput: (name: string) => byRole('searchbox', { name }), facetItem: (name: string) => byRole('checkbox', { name }), availableSinceDateField: byPlaceholderText('date'), diff --git a/server/sonar-web/src/main/js/types/rules.ts b/server/sonar-web/src/main/js/types/rules.ts index fb53a9f364a..4e85cb1e1a7 100644 --- a/server/sonar-web/src/main/js/types/rules.ts +++ b/server/sonar-web/src/main/js/types/rules.ts @@ -37,6 +37,7 @@ export interface SearchRulesQuery { is_template?: boolean | string; languages?: string; owaspTop10?: string; + ['owaspTop10-2021']?: string; p?: number; ps?: number; q?: string;