]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13924 Improve visibility of new code facet on issues page
authorJeremy Davis <jeremy.davis@sonarsource.com>
Wed, 13 Jul 2022 13:03:00 +0000 (15:03 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 14 Jul 2022 20:03:48 +0000 (20:03 +0000)
server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/PeriodFilter.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/PeriodFilter-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/CreationDateFacet-test.tsx.snap
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 26169778ac2f9a801a51aaec6429fab92aeab6ba..0f47feba734c77aed3a0cda3d753319e7ca4da41 100644 (file)
@@ -34,7 +34,6 @@ import DateTimeFormatter, {
 import { parseDate } from '../../../helpers/dates';
 import { translate } from '../../../helpers/l10n';
 import { formatMeasure } from '../../../helpers/measures';
-import { isPortfolioLike } from '../../../types/component';
 import { Component, Dict } from '../../../types/types';
 import { Query } from '../utils';
 
@@ -102,16 +101,13 @@ export class CreationDateFacet extends React.PureComponent<Props & WrappedCompon
 
   handlePeriodClick = (period: string) => this.resetTo({ createdInLast: period });
 
-  handleLeakPeriodClick = () => this.resetTo({ inNewCodePeriod: true });
-
   getValues() {
     const {
       createdAfter,
       createdAfterIncludesTime,
       createdAt,
       createdBefore,
-      createdInLast,
-      inNewCodePeriod
+      createdInLast
     } = this.props;
     const { formatDate } = this.props.intl;
     const values = [];
@@ -138,9 +134,6 @@ export class CreationDateFacet extends React.PureComponent<Props & WrappedCompon
     if (createdInLast === '1y') {
       values.push(translate('issues.facet.createdAt.last_year'));
     }
-    if (inNewCodePeriod) {
-      values.push(translate('issues.new_code'));
-    }
     return values;
   }
 
@@ -221,7 +214,7 @@ export class CreationDateFacet extends React.PureComponent<Props & WrappedCompon
   }
 
   renderPredefinedPeriods() {
-    const { component, createdInLast, inNewCodePeriod } = this.props;
+    const { createdInLast } = this.props;
     return (
       <div className="spacer-top issues-predefined-periods">
         <FacetItem
@@ -231,39 +224,28 @@ export class CreationDateFacet extends React.PureComponent<Props & WrappedCompon
           tooltip={translate('issues.facet.createdAt.all')}
           value=""
         />
-        {component && !isPortfolioLike(component.qualifier) ? (
-          <FacetItem
-            active={inNewCodePeriod}
-            name={translate('issues.new_code')}
-            onClick={this.handleLeakPeriodClick}
-            tooltip={translate('issues.new_code_period')}
-            value=""
-          />
-        ) : (
-          <>
-            <FacetItem
-              active={createdInLast === '1w'}
-              name={translate('issues.facet.createdAt.last_week')}
-              onClick={this.handlePeriodClick}
-              tooltip={translate('issues.facet.createdAt.last_week')}
-              value="1w"
-            />
-            <FacetItem
-              active={createdInLast === '1m'}
-              name={translate('issues.facet.createdAt.last_month')}
-              onClick={this.handlePeriodClick}
-              tooltip={translate('issues.facet.createdAt.last_month')}
-              value="1m"
-            />
-            <FacetItem
-              active={createdInLast === '1y'}
-              name={translate('issues.facet.createdAt.last_year')}
-              onClick={this.handlePeriodClick}
-              tooltip={translate('issues.facet.createdAt.last_year')}
-              value="1y"
-            />
-          </>
-        )}
+
+        <FacetItem
+          active={createdInLast === '1w'}
+          name={translate('issues.facet.createdAt.last_week')}
+          onClick={this.handlePeriodClick}
+          tooltip={translate('issues.facet.createdAt.last_week')}
+          value="1w"
+        />
+        <FacetItem
+          active={createdInLast === '1m'}
+          name={translate('issues.facet.createdAt.last_month')}
+          onClick={this.handlePeriodClick}
+          tooltip={translate('issues.facet.createdAt.last_month')}
+          value="1m"
+        />
+        <FacetItem
+          active={createdInLast === '1y'}
+          name={translate('issues.facet.createdAt.last_year')}
+          onClick={this.handlePeriodClick}
+          tooltip={translate('issues.facet.createdAt.last_year')}
+          value="1y"
+        />
       </div>
     );
   }
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/PeriodFilter.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/PeriodFilter.tsx
new file mode 100644 (file)
index 0000000..ea5a1b6
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 FacetBox from '../../../components/facet/FacetBox';
+import FacetHeader from '../../../components/facet/FacetHeader';
+import FacetItem from '../../../components/facet/FacetItem';
+import FacetItemsList from '../../../components/facet/FacetItemsList';
+import { translate } from '../../../helpers/l10n';
+import { Dict } from '../../../types/types';
+import { formatFacetStat, Query } from '../utils';
+
+export interface PeriodFilterProps {
+  fetching: boolean;
+  onChange: (changes: Partial<Query>) => void;
+  stats: Dict<number> | undefined;
+  newCodeSelected: boolean;
+}
+
+enum Period {
+  NewCode = 'inNewCodePeriod'
+}
+
+const PROPERTY = 'period';
+
+export default function PeriodFilter(props: PeriodFilterProps) {
+  const { fetching, newCodeSelected, stats = {} } = props;
+
+  const [open, setOpen] = React.useState(true);
+
+  const { onChange } = props;
+  const handleClick = React.useCallback(() => {
+    // We need to clear creation date filters they conflict with the new code period
+    onChange({
+      createdAfter: undefined,
+      createdAt: undefined,
+      createdBefore: undefined,
+      createdInLast: undefined,
+      [Period.NewCode]: !newCodeSelected
+    });
+  }, [newCodeSelected, onChange]);
+
+  const handleClear = React.useCallback(() => {
+    onChange({ [Period.NewCode]: undefined });
+  }, [onChange]);
+
+  return (
+    <FacetBox property={PROPERTY}>
+      <FacetHeader
+        fetching={fetching}
+        name={translate('issues.facet', PROPERTY)}
+        onClear={handleClear}
+        onClick={() => setOpen(!open)}
+        open={open}
+        values={newCodeSelected ? [translate('issues.new_code')] : undefined}
+      />
+
+      {open && (
+        <FacetItemsList>
+          <FacetItem
+            active={newCodeSelected}
+            name={translate('issues.new_code')}
+            onClick={handleClick}
+            stat={formatFacetStat(stats[Period.NewCode])}
+            value={Period.NewCode}
+          />
+        </FacetItemsList>
+      )}
+    </FacetBox>
+  );
+}
index 29dc3ed819b481941cbb86aff3dd13a2d832bc90..2496a4dcf9bcdbc85f8b0e405443abb692410496 100644 (file)
@@ -22,7 +22,12 @@ import withAppStateContext from '../../../app/components/app-state/withAppStateC
 import { isBranch, isPullRequest } from '../../../helpers/branch-like';
 import { AppState } from '../../../types/appstate';
 import { BranchLike } from '../../../types/branch-like';
-import { ComponentQualifier, isApplication, isPortfolioLike } from '../../../types/component';
+import {
+  ComponentQualifier,
+  isApplication,
+  isPortfolioLike,
+  isView
+} from '../../../types/component';
 import {
   Facet,
   ReferencedComponent,
@@ -39,6 +44,7 @@ import CreationDateFacet from './CreationDateFacet';
 import DirectoryFacet from './DirectoryFacet';
 import FileFacet from './FileFacet';
 import LanguageFacet from './LanguageFacet';
+import PeriodFilter from './PeriodFilter';
 import ProjectFacet from './ProjectFacet';
 import ResolutionFacet from './ResolutionFacet';
 import RuleFacet from './RuleFacet';
@@ -127,12 +133,20 @@ export class Sidebar extends React.PureComponent<Props> {
       (isPullRequest(branchLike) && branchLike.branch) ||
       undefined;
 
-    const displayProjectsFacet =
-      !component || !['TRK', 'BRC', 'DIR', 'DEV_PRJ'].includes(component.qualifier);
+    const displayPeriodFilter = component !== undefined && !isPortfolioLike(component.qualifier);
+    const displayProjectsFacet = !component || isView(component.qualifier);
     const displayAuthorFacet = !component || component.qualifier !== 'DEV';
 
     return (
       <>
+        {displayPeriodFilter && (
+          <PeriodFilter
+            fetching={this.props.loadingFacets.period === true}
+            onChange={this.props.onFilterChange}
+            stats={facets.period}
+            newCodeSelected={query.inNewCodePeriod}
+          />
+        )}
         <TypeFacet
           fetching={this.props.loadingFacets.types === true}
           onChange={this.props.onFilterChange}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/PeriodFilter-test.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/PeriodFilter-test.tsx
new file mode 100644 (file)
index 0000000..69dfee3
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+import * as React from 'react';
+import PeriodFilter, { PeriodFilterProps } from '../PeriodFilter';
+
+it('should be collapsible', async () => {
+  const user = userEvent.setup();
+
+  renderPeriodFilter();
+
+  expect(screen.getByText('issues.new_code')).toBeInTheDocument();
+
+  await user.click(screen.getByText('issues.facet.period'));
+
+  expect(screen.queryByText('issues.new_code')).not.toBeInTheDocument();
+});
+
+it('should filter when clicked', async () => {
+  const user = userEvent.setup();
+  const onChange = jest.fn();
+
+  renderPeriodFilter({ onChange });
+
+  await user.click(screen.getByText('issues.new_code'));
+
+  expect(onChange).toBeCalledWith({
+    createdAfter: undefined,
+    createdAt: undefined,
+    createdBefore: undefined,
+    createdInLast: undefined,
+    inNewCodePeriod: true
+  });
+});
+
+it('should be clearable', async () => {
+  const user = userEvent.setup();
+  const onChange = jest.fn();
+
+  renderPeriodFilter({ onChange, newCodeSelected: true });
+
+  await user.click(screen.getByText('clear'));
+
+  expect(onChange).toBeCalledWith({
+    inNewCodePeriod: undefined
+  });
+});
+
+function renderPeriodFilter(overrides: Partial<PeriodFilterProps> = {}) {
+  return render(
+    <PeriodFilter
+      fetching={false}
+      newCodeSelected={false}
+      onChange={jest.fn()}
+      stats={{}}
+      {...overrides}
+    />
+  );
+}
index 8e6c345e8680dc74bbe70365c8f52c6e8946917f..bdfb4b033bddfeb268eb79555ae9efb56749cf3a 100644 (file)
@@ -38,9 +38,7 @@ it('should render facets for project', () => {
 it.each([
   [ComponentQualifier.Application],
   [ComponentQualifier.Portfolio],
-  [ComponentQualifier.SubPortfolio],
-  [ComponentQualifier.Directory],
-  [ComponentQualifier.Developper]
+  [ComponentQualifier.SubPortfolio]
 ])('should render facets for %p', qualifier => {
   expect(renderSidebar({ component: mockComponent({ qualifier }) })).toMatchSnapshot();
 });
index d3936e2f771cc578839ec52cd1381521ff12fd5b..62237efc8b6e27cb9d78e59b3bfcdf3d52bdecbd 100644 (file)
@@ -48,10 +48,30 @@ exports[`should render correctly for createdInLast month 1`] = `
         disabled={false}
         halfWidth={false}
         loading={false}
-        name="issues.new_code"
+        name="issues.facet.createdAt.last_week"
         onClick={[Function]}
-        tooltip="issues.new_code_period"
-        value=""
+        tooltip="issues.facet.createdAt.last_week"
+        value="1w"
+      />
+      <FacetItem
+        active={true}
+        disabled={false}
+        halfWidth={false}
+        loading={false}
+        name="issues.facet.createdAt.last_month"
+        onClick={[Function]}
+        tooltip="issues.facet.createdAt.last_month"
+        value="1m"
+      />
+      <FacetItem
+        active={false}
+        disabled={false}
+        halfWidth={false}
+        loading={false}
+        name="issues.facet.createdAt.last_year"
+        onClick={[Function]}
+        tooltip="issues.facet.createdAt.last_year"
+        value="1y"
       />
     </div>
   </div>
@@ -101,15 +121,35 @@ exports[`should render correctly for createdInLast week 1`] = `
         tooltip="issues.facet.createdAt.all"
         value=""
       />
+      <FacetItem
+        active={true}
+        disabled={false}
+        halfWidth={false}
+        loading={false}
+        name="issues.facet.createdAt.last_week"
+        onClick={[Function]}
+        tooltip="issues.facet.createdAt.last_week"
+        value="1w"
+      />
       <FacetItem
         active={false}
         disabled={false}
         halfWidth={false}
         loading={false}
-        name="issues.new_code"
+        name="issues.facet.createdAt.last_month"
         onClick={[Function]}
-        tooltip="issues.new_code_period"
-        value=""
+        tooltip="issues.facet.createdAt.last_month"
+        value="1m"
+      />
+      <FacetItem
+        active={false}
+        disabled={false}
+        halfWidth={false}
+        loading={false}
+        name="issues.facet.createdAt.last_year"
+        onClick={[Function]}
+        tooltip="issues.facet.createdAt.last_year"
+        value="1y"
       />
     </div>
   </div>
@@ -164,10 +204,30 @@ exports[`should render correctly for createdInLast year 1`] = `
         disabled={false}
         halfWidth={false}
         loading={false}
-        name="issues.new_code"
+        name="issues.facet.createdAt.last_week"
         onClick={[Function]}
-        tooltip="issues.new_code_period"
-        value=""
+        tooltip="issues.facet.createdAt.last_week"
+        value="1w"
+      />
+      <FacetItem
+        active={false}
+        disabled={false}
+        halfWidth={false}
+        loading={false}
+        name="issues.facet.createdAt.last_month"
+        onClick={[Function]}
+        tooltip="issues.facet.createdAt.last_month"
+        value="1m"
+      />
+      <FacetItem
+        active={true}
+        disabled={false}
+        halfWidth={false}
+        loading={false}
+        name="issues.facet.createdAt.last_year"
+        onClick={[Function]}
+        tooltip="issues.facet.createdAt.last_year"
+        value="1y"
       />
     </div>
   </div>
@@ -475,10 +535,30 @@ exports[`should render correctly: project 1`] = `
         disabled={false}
         halfWidth={false}
         loading={false}
-        name="issues.new_code"
+        name="issues.facet.createdAt.last_week"
         onClick={[Function]}
-        tooltip="issues.new_code_period"
-        value=""
+        tooltip="issues.facet.createdAt.last_week"
+        value="1w"
+      />
+      <FacetItem
+        active={false}
+        disabled={false}
+        halfWidth={false}
+        loading={false}
+        name="issues.facet.createdAt.last_month"
+        onClick={[Function]}
+        tooltip="issues.facet.createdAt.last_month"
+        value="1m"
+      />
+      <FacetItem
+        active={false}
+        disabled={false}
+        halfWidth={false}
+        loading={false}
+        name="issues.facet.createdAt.last_year"
+        onClick={[Function]}
+        tooltip="issues.facet.createdAt.last_year"
+        value="1y"
       />
     </div>
   </div>
index e15890e007898153e6ae640ffb578c69f5dfb258..ebac70305d19f852447d4b4ef13f24e42499123b 100644 (file)
@@ -18,6 +18,7 @@ Array [
 
 exports[`should render facets for "APP" 1`] = `
 Array [
+  "PeriodFilter",
   "TypeFacet",
   "SeverityFacet",
   "ScopeFacet",
@@ -34,43 +35,6 @@ Array [
 ]
 `;
 
-exports[`should render facets for "DEV" 1`] = `
-Array [
-  "TypeFacet",
-  "SeverityFacet",
-  "ScopeFacet",
-  "ResolutionFacet",
-  "StatusFacet",
-  "StandardFacet",
-  "injectIntl(CreationDateFacet)",
-  "withLanguagesContext(LanguageFacet)",
-  "RuleFacet",
-  "TagFacet",
-  "ProjectFacet",
-  "DirectoryFacet",
-  "FileFacet",
-  "AssigneeFacet",
-]
-`;
-
-exports[`should render facets for "DIR" 1`] = `
-Array [
-  "TypeFacet",
-  "SeverityFacet",
-  "ScopeFacet",
-  "ResolutionFacet",
-  "StatusFacet",
-  "StandardFacet",
-  "injectIntl(CreationDateFacet)",
-  "withLanguagesContext(LanguageFacet)",
-  "RuleFacet",
-  "TagFacet",
-  "FileFacet",
-  "AssigneeFacet",
-  "AuthorFacet",
-]
-`;
-
 exports[`should render facets for "SVW" 1`] = `
 Array [
   "TypeFacet",
@@ -127,6 +91,7 @@ Array [
 
 exports[`should render facets for project 1`] = `
 Array [
+  "PeriodFilter",
   "TypeFacet",
   "SeverityFacet",
   "ScopeFacet",
index 0e35f3cee6f39390dc1505e3c2f2a88eec39a544..08aaf512328f3bc8c4434bf1e96daa116381e61f 100644 (file)
@@ -959,6 +959,7 @@ issue.changelog.field.file=File
 # ISSUES FACETS
 #
 #------------------------------------------------------------------------------
+issues.facet.period=Period
 issues.facet.types=Type
 issues.facet.severities=Severity
 issues.facet.scopes=Scope