diff options
author | Wouter Admiraal <wouter.admiraal@sonarsource.com> | 2023-05-09 15:52:44 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-05-16 20:02:50 +0000 |
commit | a412e8133c13dad2a64aeda574c556d26a68abe0 (patch) | |
tree | d7abb00947803852ec58344f84859749b1d87248 | |
parent | fd7ad162b2200b8aee72b9760e9c7aef9eae5182 (diff) | |
download | sonarqube-a412e8133c13dad2a64aeda574c556d26a68abe0.tar.gz sonarqube-a412e8133c13dad2a64aeda574c556d26a68abe0.zip |
SONAR-19197 Allow issues to be filtered by code variant
14 files changed, 251 insertions, 25 deletions
diff --git a/server/sonar-web/src/main/js/api/issues.ts b/server/sonar-web/src/main/js/api/issues.ts index a0675a1dc09..af5ee866a1c 100644 --- a/server/sonar-web/src/main/js/api/issues.ts +++ b/server/sonar-web/src/main/js/api/issues.ts @@ -35,6 +35,7 @@ type FacetName = | 'assigned_to_me' | 'assignees' | 'author' + | 'codeVariants' | 'createdAt' | 'cwe' | 'directories' diff --git a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts index 1a7c78136c6..d79208bedcd 100644 --- a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts @@ -418,6 +418,7 @@ export default class IssuesServiceMock { ruleStatus: 'DEPRECATED', quickFixAvailable: true, tags: ['unused'], + codeVariants: ['variant 1', 'variant 2'], project: 'org.project2', assignee: 'email1@sonarsource.com', author: 'email3@sonarsource.com', @@ -477,7 +478,7 @@ export default class IssuesServiceMock { this.list = cloneDeep(this.defaultList); - (searchIssues as jest.Mock).mockImplementation(this.handleSearchIssues); + jest.mocked(searchIssues).mockImplementation(this.handleSearchIssues); (getRuleDetails as jest.Mock).mockImplementation(this.handleGetRuleDetails); jest.mocked(searchRules).mockImplementation(this.handleSearchRules); (getIssueFlowSnippets as jest.Mock).mockImplementation(this.handleGetIssueFlowSnippets); @@ -648,6 +649,27 @@ export default class IssuesServiceMock { ], }; } + if (name === 'codeVariants') { + return { + property: 'codeVariants', + values: this.list.reduce((acc, { issue }) => { + if (issue.codeVariants?.length) { + issue.codeVariants.forEach((codeVariant) => { + const item = acc.find(({ val }) => val === codeVariant); + if (item) { + item.count++; + } else { + acc.push({ + val: codeVariant, + count: 1, + }); + } + }); + } + return acc; + }, [] as RawFacet['values']), + }; + } if (name === 'projects') { return { property: name, @@ -757,7 +779,18 @@ export default class IssuesServiceMock { .filter( (item) => !query.inNewCodePeriod || new Date(item.issue.creationDate) > new Date('2023-01-10') - ); + ) + .filter((item) => { + if (!query.codeVariants) { + return true; + } + if (!item.issue.codeVariants) { + return false; + } + return item.issue.codeVariants.some((codeVariant) => + query.codeVariants?.split(',').includes(codeVariant) + ); + }); // Splice list items according to paging using a fixed page size const pageIndex = query.p || 1; diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx index a7c11cbc3ca..c7a6a5f9c81 100644 --- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx @@ -22,7 +22,7 @@ import userEvent from '@testing-library/user-event'; import selectEvent from 'react-select-event'; import { TabKeys } from '../../../components/rules/RuleTabViewer'; import { renderOwaspTop102021Category } from '../../../helpers/security-standard'; -import { mockLoggedInUser } from '../../../helpers/testMocks'; +import { mockLoggedInUser, mockRawIssue } from '../../../helpers/testMocks'; import { ComponentQualifier } from '../../../types/component'; import { IssueType } from '../../../types/issues'; import { @@ -419,6 +419,35 @@ describe('issues app', () => { expect(ui.issueItem7.get()).toBeInTheDocument(); }); + it('should properly filter by code variants', async () => { + const user = userEvent.setup(); + renderProjectIssuesApp(); + await waitOnDataLoaded(); + + await user.click(ui.codeVariantsFacet.get()); + await user.click(screen.getByRole('checkbox', { name: /variant 1/ })); + + expect(ui.issueItem1.query()).not.toBeInTheDocument(); + expect(ui.issueItem7.get()).toBeInTheDocument(); + + // Clear filter + await user.click(ui.clearCodeVariantsFacet.get()); + expect(ui.issueItem1.get()).toBeInTheDocument(); + }); + + it('should properly hide the code variants filter if no issue has any code variants', async () => { + issuesHandler.setIssueList([ + { + issue: mockRawIssue(), + snippets: {}, + }, + ]); + renderProjectIssuesApp(); + await waitOnDataLoaded(); + + expect(ui.codeVariantsFacet.query()).not.toBeInTheDocument(); + }); + it('should allow to set creation date', async () => { const user = userEvent.setup(); const currentUser = mockLoggedInUser(); diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts index 50d36594ac6..8668a496f6c 100644 --- a/server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts +++ b/server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts @@ -36,6 +36,7 @@ describe('serialize/deserialize', () => { assigned: true, assignees: ['a', 'b'], author: ['a', 'b'], + codeVariants: ['variant1', 'variant2'], createdAfter: new Date(1000000), createdAt: 'a', createdBefore: new Date(1000000), @@ -67,6 +68,7 @@ describe('serialize/deserialize', () => { ).toStrictEqual({ assignees: 'a,b', author: ['a', 'b'], + codeVariants: 'variant1,variant2', createdAt: 'a', createdBefore: '1970-01-01', createdAfter: '1970-01-01', diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx index ffad87ea9bb..5626e1f10f0 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx @@ -64,7 +64,7 @@ import { } from '../../../helpers/pages'; import { serializeDate } from '../../../helpers/query'; import { BranchLike } from '../../../types/branch-like'; -import { ComponentQualifier, isPortfolioLike } from '../../../types/component'; +import { ComponentQualifier, isPortfolioLike, isProject } from '../../../types/component'; import { ASSIGNEE_ME, Facet, @@ -128,6 +128,7 @@ export interface State { locationsNavigator: boolean; myIssues: boolean; openFacets: Dict<boolean>; + showVariantsFilter: boolean; openIssue?: Issue; openPopup?: { issue: string; name: string }; openRuleDetails?: RuleDetails; @@ -146,6 +147,7 @@ export interface State { const DEFAULT_QUERY = { resolved: 'false' }; const MAX_INITAL_FETCH = 1000; const BRANCH_STATUS_REFRESH_INTERVAL = 1000; +const VARIANTS_FACET = 'codeVariants'; export class App extends React.PureComponent<Props, State> { mounted = false; @@ -178,6 +180,7 @@ export class App extends React.PureComponent<Props, State> { standards: shouldOpenStandardsFacet({}, query), types: true, }, + showVariantsFilter: false, query, referencedComponentsById: {}, referencedComponentsByKey: {}, @@ -212,7 +215,7 @@ export class App extends React.PureComponent<Props, State> { addWhitePageClass(); addSideBarClass(); this.attachShortcuts(); - this.fetchFirstIssues(); + this.fetchFirstIssues(true); } componentDidUpdate(prevProps: Props, prevState: State) { @@ -226,7 +229,7 @@ export class App extends React.PureComponent<Props, State> { !areQueriesEqual(prevQuery, query) || areMyIssuesSelected(prevQuery) !== areMyIssuesSelected(query) ) { - this.fetchFirstIssues(); + this.fetchFirstIssues(false); this.setState({ checkAll: false }); } else if (openIssue && openIssue.key !== this.state.selected) { this.setState({ @@ -439,16 +442,24 @@ export class App extends React.PureComponent<Props, State> { }); }; - fetchIssues = (additional: RawQuery, requestFacets = false): Promise<FetchIssuesPromise> => { + fetchIssues = ( + additional: RawQuery, + requestFacets = false, + firstRequest = false + ): Promise<FetchIssuesPromise> => { const { component } = this.props; const { myIssues, openFacets, query } = this.state; - const facets = requestFacets + let facets = requestFacets ? Object.keys(openFacets) .filter((facet) => facet !== STANDARDS && openFacets[facet]) .join(',') : undefined; + if (firstRequest && isProject(component?.qualifier)) { + facets = facets ? `${facets},${VARIANTS_FACET}` : VARIANTS_FACET; + } + const parameters: Dict<string | undefined> = { ...getBranchLikeQuery(this.props.branchLike), componentKeys: component && component.key, @@ -475,7 +486,7 @@ export class App extends React.PureComponent<Props, State> { return this.fetchIssuesHelper(parameters); }; - fetchFirstIssues() { + fetchFirstIssues(firstRequest: boolean) { const prevQuery = this.props.location.query; const openIssueKey = getOpen(this.props.location.query); let fetchPromise; @@ -492,7 +503,7 @@ export class App extends React.PureComponent<Props, State> { return pageIssues.some((issue) => issue.key === openIssueKey); }); } else { - fetchPromise = this.fetchIssues({}, true); + fetchPromise = this.fetchIssues({}, true, firstRequest); } return fetchPromise.then( @@ -503,10 +514,13 @@ export class App extends React.PureComponent<Props, State> { if (issues.length > 0) { selected = openIssue ? openIssue.key : issues[0].key; } - this.setState({ + this.setState(({ showVariantsFilter }) => ({ cannotShowOpenIssue: Boolean(openIssueKey && !openIssue), effortTotal, facets: parseFacets(facets), + showVariantsFilter: firstRequest + ? Boolean(facets.find((f) => f.property === VARIANTS_FACET)?.values.length) + : showVariantsFilter, loading: false, locationsNavigator: true, issues, @@ -520,7 +534,7 @@ export class App extends React.PureComponent<Props, State> { selected, selectedFlowIndex: 0, selectedLocationIndex: undefined, - }); + })); } return issues; }, @@ -786,7 +800,7 @@ export class App extends React.PureComponent<Props, State> { handleBulkChangeDone = () => { this.setState({ checkAll: false }); this.refreshBranchStatus(); - this.fetchFirstIssues(); + this.fetchFirstIssues(false); this.handleCloseBulkChange(); }; @@ -891,7 +905,19 @@ export class App extends React.PureComponent<Props, State> { renderFacets() { const { component, currentUser, branchLike } = this.props; - const { query } = this.state; + const { + query, + facets, + loadingFacets, + myIssues, + openFacets, + showVariantsFilter, + referencedComponentsById, + referencedComponentsByKey, + referencedLanguages, + referencedRules, + referencedUsers, + } = this.state; return ( <div className="layout-page-filters"> @@ -912,19 +938,20 @@ export class App extends React.PureComponent<Props, State> { branchLike={branchLike} component={component} createdAfterIncludesTime={this.createdAfterIncludesTime()} - facets={this.state.facets} + facets={facets} loadSearchResultCount={this.loadSearchResultCount} - loadingFacets={this.state.loadingFacets} - myIssues={this.state.myIssues} + loadingFacets={loadingFacets} + myIssues={myIssues} onFacetToggle={this.handleFacetToggle} onFilterChange={this.handleFilterChange} - openFacets={this.state.openFacets} + openFacets={openFacets} + showVariantsFilter={showVariantsFilter} query={query} - referencedComponentsById={this.state.referencedComponentsById} - referencedComponentsByKey={this.state.referencedComponentsByKey} - referencedLanguages={this.state.referencedLanguages} - referencedRules={this.state.referencedRules} - referencedUsers={this.state.referencedUsers} + referencedComponentsById={referencedComponentsById} + referencedComponentsByKey={referencedComponentsByKey} + referencedLanguages={referencedLanguages} + referencedRules={referencedRules} + referencedUsers={referencedUsers} /> </div> ); diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx index 90460b7b8aa..ea5ce2bfbed 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx @@ -26,6 +26,7 @@ import { ComponentQualifier, isApplication, isPortfolioLike, + isProject, isView, } from '../../../types/component'; import { @@ -54,6 +55,7 @@ import StandardFacet from './StandardFacet'; import StatusFacet from './StatusFacet'; import TagFacet from './TagFacet'; import TypeFacet from './TypeFacet'; +import VariantFacet from './VariantFacet'; export interface Props { appState: AppState; @@ -67,6 +69,7 @@ export interface Props { onFacetToggle: (property: string) => void; onFilterChange: (changes: Partial<Query>) => void; openFacets: Dict<boolean>; + showVariantsFilter: boolean; query: Query; referencedComponentsById: Dict<ReferencedComponent>; referencedComponentsByKey: Dict<ReferencedComponent>; @@ -77,7 +80,8 @@ export interface Props { export class Sidebar extends React.PureComponent<Props> { renderComponentFacets() { - const { component, facets, loadingFacets, openFacets, query, branchLike } = this.props; + const { component, facets, loadingFacets, openFacets, query, branchLike, showVariantsFilter } = + this.props; const hasFileOrDirectory = !isApplication(component?.qualifier) && !isPortfolioLike(component?.qualifier); if (!component || !hasFileOrDirectory) { @@ -102,6 +106,15 @@ export class Sidebar extends React.PureComponent<Props> { {...commonProps} /> )} + {showVariantsFilter && isProject(component?.qualifier) && ( + <VariantFacet + fetching={loadingFacets.codeVariants === true} + open={!!openFacets.codeVariants} + stats={facets.codeVariants} + values={query.codeVariants} + {...commonProps} + /> + )} <FileFacet branchLike={branchLike} fetching={loadingFacets.files === true} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/VariantFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/VariantFacet.tsx new file mode 100644 index 00000000000..954f0bb5757 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/VariantFacet.tsx @@ -0,0 +1,110 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 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 { orderBy, sortBy, without } from 'lodash'; +import * as React from 'react'; +import FacetBox from '../../../components/facet/FacetBox'; +import FacetHeader from '../../../components/facet/FacetHeader'; +import FacetItem from '../../../components/facet/FacetItem'; +import FacetItemsList from '../../../components/facet/FacetItemsList'; +import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint'; +import { translate } from '../../../helpers/l10n'; +import { Dict } from '../../../types/types'; +import { Query, formatFacetStat } from '../utils'; + +interface VariantFacetProps { + fetching: boolean; + onChange: (changes: Partial<Query>) => void; + onToggle: (property: string) => void; + open: boolean; + stats?: Dict<number>; + values: string[]; +} + +const FACET_NAME = 'codeVariants'; + +export default function VariantFacet(props: VariantFacetProps) { + const { open, fetching, stats = {}, values, onToggle, onChange } = props; + + const handleClear = React.useCallback(() => { + onChange({ [FACET_NAME]: undefined }); + }, [onChange]); + + const handleHeaderClick = React.useCallback(() => { + onToggle(FACET_NAME); + }, [onToggle]); + + const handleItemClick = React.useCallback( + (value: string, multiple: boolean) => { + if (value === '') { + onChange({ [FACET_NAME]: undefined }); + } else if (multiple) { + const newValues = orderBy( + values.includes(value) ? without(values, value) : [...values, value] + ); + onChange({ [FACET_NAME]: newValues }); + } else { + onChange({ + [FACET_NAME]: values.includes(value) && values.length === 1 ? [] : [value], + }); + } + }, + [values, onChange] + ); + + const id = `facet_${FACET_NAME}`; + + return ( + <FacetBox property={FACET_NAME}> + <FacetHeader + fetching={fetching} + name={translate('issues.facet', FACET_NAME)} + id={id} + onClear={handleClear} + onClick={handleHeaderClick} + open={open} + values={values} + /> + {open && ( + <> + <FacetItemsList labelledby={id}> + {Object.keys(stats).length === 0 && ( + <div className="note spacer-bottom">{translate('no_results')}</div> + )} + {sortBy( + Object.keys(stats), + (key) => -stats[key], + (key) => key + ).map((codeVariant) => ( + <FacetItem + active={values.includes(codeVariant)} + key={codeVariant} + name={codeVariant} + onClick={handleItemClick} + stat={formatFacetStat(stats[codeVariant])} + value={codeVariant} + /> + ))} + </FacetItemsList> + <MultipleSelectionHint options={Object.keys(stats).length} values={values.length} /> + </> + )} + </FacetBox> + ); +} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-it.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-it.tsx index 52fdec70fee..06d816f076e 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-it.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-it.tsx @@ -117,6 +117,7 @@ function renderSidebar(props: Partial<Sidebar['props']> = {}) { onFacetToggle={jest.fn()} onFilterChange={jest.fn()} openFacets={{}} + showVariantsFilter={false} query={mockQuery()} referencedComponentsById={{}} referencedComponentsByKey={{}} diff --git a/server/sonar-web/src/main/js/apps/issues/test-utils.tsx b/server/sonar-web/src/main/js/apps/issues/test-utils.tsx index 7a9b2d9ff73..2f0129612df 100644 --- a/server/sonar-web/src/main/js/apps/issues/test-utils.tsx +++ b/server/sonar-web/src/main/js/apps/issues/test-utils.tsx @@ -76,9 +76,11 @@ export const ui = { projectFacet: byRole('button', { name: 'issues.facet.projects' }), clearProjectFacet: byRole('button', { name: 'clear_x_filter.issues.facet.projects' }), assigneeFacet: byRole('button', { name: 'issues.facet.assignees' }), + codeVariantsFacet: byRole('button', { name: 'issues.facet.codeVariants' }), clearAssigneeFacet: byRole('button', { name: 'clear_x_filter.issues.facet.assignees' }), authorFacet: byRole('button', { name: 'issues.facet.authors' }), clearAuthorFacet: byRole('button', { name: 'clear_x_filter.issues.facet.authors' }), + clearCodeVariantsFacet: byRole('button', { name: 'clear_x_filter.issues.facet.codeVariants' }), dateInputMonthSelect: byRole('combobox', { name: 'Month:' }), dateInputYearSelect: byRole('combobox', { name: 'Year:' }), diff --git a/server/sonar-web/src/main/js/apps/issues/utils.ts b/server/sonar-web/src/main/js/apps/issues/utils.ts index 400718aa381..d2aee46d244 100644 --- a/server/sonar-web/src/main/js/apps/issues/utils.ts +++ b/server/sonar-web/src/main/js/apps/issues/utils.ts @@ -34,6 +34,7 @@ import { import { get, save } from '../../helpers/storage'; import { isDefined } from '../../helpers/types'; import { Facet, RawFacet } from '../../types/issues'; +import { MetricType } from '../../types/metrics'; import { SecurityStandard } from '../../types/security'; import { Dict, Issue, Paging, RawQuery } from '../../types/types'; import { UserBase } from '../../types/users'; @@ -44,6 +45,7 @@ export interface Query { assigned: boolean; assignees: string[]; author: string[]; + codeVariants: string[]; createdAfter: Date | undefined; createdAt: string; createdBefore: Date | undefined; @@ -111,6 +113,7 @@ export function parseQuery(query: RawQuery): Query { statuses: parseAsArray(query.statuses, parseAsString), tags: parseAsArray(query.tags, parseAsString), types: parseAsArray(query.types, parseAsString), + codeVariants: parseAsArray(query.codeVariants, parseAsString), }; } @@ -157,6 +160,7 @@ export function serializeQuery(query: Query): RawQuery { statuses: serializeStringArray(query.statuses), tags: serializeStringArray(query.tags), types: serializeStringArray(query.types), + codeVariants: serializeStringArray(query.codeVariants), }; return cleanQuery(filter); @@ -182,7 +186,7 @@ export function parseFacets(facets: RawFacet[]): Dict<Facet> { } export function formatFacetStat(stat: number | undefined) { - return stat && formatMeasure(stat, 'SHORT_INT'); + return stat && formatMeasure(stat, MetricType.ShortInteger); } export const searchAssignees = ( diff --git a/server/sonar-web/src/main/js/helpers/mocks/issues.ts b/server/sonar-web/src/main/js/helpers/mocks/issues.ts index 7eb14844271..fe626de3c3a 100644 --- a/server/sonar-web/src/main/js/helpers/mocks/issues.ts +++ b/server/sonar-web/src/main/js/helpers/mocks/issues.ts @@ -71,6 +71,7 @@ export function mockQuery(overrides: Partial<Query> = {}): Query { assigned: false, assignees: [], author: [], + codeVariants: [], createdAfter: undefined, createdAt: '', createdBefore: undefined, diff --git a/server/sonar-web/src/main/js/types/issues.ts b/server/sonar-web/src/main/js/types/issues.ts index 3e96f1cf3c7..7b55991936b 100644 --- a/server/sonar-web/src/main/js/types/issues.ts +++ b/server/sonar-web/src/main/js/types/issues.ts @@ -109,6 +109,7 @@ export interface RawIssue { tags?: string[]; assignee?: string; author?: string; + codeVariants?: string[]; comments?: Comment[]; creationDate: string; component: string; diff --git a/server/sonar-web/src/main/js/types/types.ts b/server/sonar-web/src/main/js/types/types.ts index 29415171dfd..94901ddc87a 100644 --- a/server/sonar-web/src/main/js/types/types.ts +++ b/server/sonar-web/src/main/js/types/types.ts @@ -246,6 +246,7 @@ export interface Issue { assigneeName?: string; author?: string; branch?: string; + codeVariants?: string[]; comments?: IssueComment[]; component: string; componentEnabled?: boolean; diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 9daa1ddc8ff..ac422445838 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1019,6 +1019,7 @@ issues.facet.tags=Tag issues.facet.rules=Rule issues.facet.resolutions=Resolution issues.facet.languages=Language +issues.facet.codeVariants=Code Variant issues.facet.createdAt=Creation Date issues.facet.createdAt.all=All issues.facet.createdAt.last_week=Last week |