]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-14156 Handle time in createdAfter issue filter
authorJeremy Davis <jeremy.davis@sonarsource.com>
Thu, 26 Nov 2020 16:24:50 +0000 (17:24 +0100)
committersonartech <sonartech@sonarsource.com>
Mon, 30 Nov 2020 20:07:06 +0000 (20:07 +0000)
server/sonar-web/src/main/js/apps/issues/components/App.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/CreationDateFacet-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 [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 0c8163a83614209791ae8944776847aa628ee597..704fe4fdde351e78c47183c21fffe40b9ef98771 100644 (file)
@@ -35,6 +35,7 @@ import {
   removeSideBarClass,
   removeWhitePageClass
 } from 'sonar-ui-common/helpers/pages';
+import { serializeDate } from 'sonar-ui-common/helpers/query';
 import A11ySkipTarget from '../../../app/components/a11y/A11ySkipTarget';
 import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
 import EmptySearch from '../../../components/common/EmptySearch';
@@ -409,6 +410,8 @@ export default class App extends React.PureComponent<Props, State> {
     }
   };
 
+  createdAfterIncludesTime = () => Boolean(this.props.location.query.createdAfter?.includes('T'));
+
   fetchIssues = (additional: T.RawQuery, requestFacets = false): Promise<FetchIssuesPromise> => {
     const { component } = this.props;
     const { myIssues, openFacets, query } = this.state;
@@ -425,7 +428,7 @@ export default class App extends React.PureComponent<Props, State> {
       (component && component.organization) ||
       (this.props.organization && this.props.organization.key);
 
-    const parameters = {
+    const parameters: T.Dict<string | undefined> = {
       ...getBranchLikeQuery(this.props.branchLike),
       componentKeys: component && component.key,
       s: 'FILE_LINE',
@@ -436,6 +439,10 @@ export default class App extends React.PureComponent<Props, State> {
       ...additional
     };
 
+    if (query.createdAfter !== undefined && this.createdAfterIncludesTime()) {
+      parameters.createdAfter = serializeDate(query.createdAfter);
+    }
+
     // only sorting by CREATION_DATE is allowed, so let's sort DESC
     if (query.sort) {
       Object.assign(parameters, { asc: 'false' });
@@ -944,6 +951,7 @@ export default class App extends React.PureComponent<Props, State> {
         <Sidebar
           branchLike={branchLike}
           component={component}
+          createdAfterIncludesTime={this.createdAfterIncludesTime()}
           facets={this.state.facets}
           hideAuthorFacet={hideAuthorFacet}
           loadSearchResultCount={this.loadSearchResultCount}
index 12f8db6b696ab218e1964c54e6f5a0bafbb82b41..5cb0f444f46f6d0a734a0528f4ba04e1c2d871f2 100644 (file)
@@ -365,6 +365,27 @@ it('should refresh branch status if issues are updated', async () => {
   expect(fetchBranchStatus).toBeCalled();
 });
 
+it('should handle createAfter query param with time', async () => {
+  const fetchIssues = fetchIssuesMockFactory();
+  const wrapper = shallowRender({
+    fetchIssues,
+    location: mockLocation({ query: { createdAfter: '2020-10-21' } })
+  });
+  expect(wrapper.instance().createdAfterIncludesTime()).toBe(false);
+  await waitAndUpdate(wrapper);
+
+  wrapper.setProps({ location: mockLocation({ query: { createdAfter: '2020-10-21T17:21:00Z' } }) });
+  expect(wrapper.instance().createdAfterIncludesTime()).toBe(true);
+
+  fetchIssues.mockClear();
+
+  wrapper.instance().fetchIssues({});
+  expect(fetchIssues).toBeCalledWith(
+    expect.objectContaining({ createdAfter: '2020-10-21T17:21:00+0000' }),
+    false
+  );
+});
+
 function fetchIssuesMockFactory(keyCount = 0, lineCount = 1) {
   return jest.fn().mockImplementation(({ p }: any) =>
     Promise.resolve({
index ea29fa62561734ab9ec530372660898a9646d75c..75d91b8dcc3d0cc1eb169f723f4b2116b1cfbc6e 100644 (file)
@@ -24,7 +24,9 @@ import { InjectedIntlProps, injectIntl } from 'react-intl';
 import BarChart from 'sonar-ui-common/components/charts/BarChart';
 import { longFormatterOption } from 'sonar-ui-common/components/intl/DateFormatter';
 import DateFromNow from 'sonar-ui-common/components/intl/DateFromNow';
-import DateTimeFormatter from 'sonar-ui-common/components/intl/DateTimeFormatter';
+import DateTimeFormatter, {
+  formatterOption as dateTimeFormatterOption
+} from 'sonar-ui-common/components/intl/DateTimeFormatter';
 import { parseDate } from 'sonar-ui-common/helpers/dates';
 import { translate } from 'sonar-ui-common/helpers/l10n';
 import { formatMeasure } from 'sonar-ui-common/helpers/measures';
@@ -37,6 +39,7 @@ import { Query } from '../utils';
 interface Props {
   component: T.Component | undefined;
   createdAfter: Date | undefined;
+  createdAfterIncludesTime: boolean;
   createdAt: string;
   createdBefore: Date | undefined;
   createdInLast: string;
@@ -48,7 +51,7 @@ interface Props {
   stats: T.Dict<number> | undefined;
 }
 
-class CreationDateFacet extends React.PureComponent<Props & InjectedIntlProps> {
+export class CreationDateFacet extends React.PureComponent<Props & InjectedIntlProps> {
   property = 'createdAt';
 
   static defaultProps = {
@@ -100,11 +103,23 @@ class CreationDateFacet extends React.PureComponent<Props & InjectedIntlProps> {
   handleLeakPeriodClick = () => this.resetTo({ sinceLeakPeriod: true });
 
   getValues() {
-    const { createdAfter, createdAt, createdBefore, createdInLast, sinceLeakPeriod } = this.props;
+    const {
+      createdAfter,
+      createdAfterIncludesTime,
+      createdAt,
+      createdBefore,
+      createdInLast,
+      sinceLeakPeriod
+    } = this.props;
     const { formatDate } = this.props.intl;
     const values = [];
     if (createdAfter) {
-      values.push(formatDate(createdAfter, longFormatterOption));
+      values.push(
+        formatDate(
+          createdAfter,
+          createdAfterIncludesTime ? dateTimeFormatterOption : longFormatterOption
+        )
+      );
     }
     if (createdAt) {
       values.push(formatDate(createdAt, longFormatterOption));
@@ -191,18 +206,6 @@ class CreationDateFacet extends React.PureComponent<Props & InjectedIntlProps> {
     );
   }
 
-  renderExactDate() {
-    return (
-      <div className="search-navigator-facet-container">
-        <DateTimeFormatter date={this.props.createdAt} />
-        <br />
-        <span className="note">
-          <DateFromNow date={this.props.createdAt} />
-        </span>
-      </div>
-    );
-  }
-
   renderPeriodSelectors() {
     const { createdAfter, createdBefore } = this.props;
     return (
@@ -264,10 +267,30 @@ class CreationDateFacet extends React.PureComponent<Props & InjectedIntlProps> {
   }
 
   renderInner() {
-    const { createdAt } = this.props;
-    return createdAt ? (
-      this.renderExactDate()
-    ) : (
+    const { createdAfter, createdAfterIncludesTime, createdAt } = this.props;
+
+    if (createdAt) {
+      return (
+        <div className="search-navigator-facet-container">
+          <DateTimeFormatter date={this.props.createdAt} />
+          <br />
+          <span className="note">
+            <DateFromNow date={this.props.createdAt} />
+          </span>
+        </div>
+      );
+    }
+
+    if (createdAfter && createdAfterIncludesTime) {
+      return (
+        <div className="search-navigator-facet-container">
+          <strong>{translate('after')} </strong>
+          <DateTimeFormatter date={createdAfter} />
+        </div>
+      );
+    }
+
+    return (
       <div>
         {this.renderBarChart()}
         {this.renderPeriodSelectors()}
index 374f6d6a2d174f4ae08e5fb7d2d7280e79fcbc99..f39e1a0062eeb019c845253ac09d67992505b068 100644 (file)
@@ -42,6 +42,7 @@ import TypeFacet from './TypeFacet';
 export interface Props {
   branchLike?: BranchLike;
   component: T.Component | undefined;
+  createdAfterIncludesTime: boolean;
   facets: T.Dict<Facet | undefined>;
   hideAuthorFacet?: boolean;
   loadSearchResultCount: (property: string, changes: Partial<Query>) => Promise<Facet>;
@@ -98,7 +99,14 @@ export class Sidebar extends React.PureComponent<Props> {
   }
 
   render() {
-    const { component, facets, hideAuthorFacet, openFacets, query } = this.props;
+    const {
+      component,
+      createdAfterIncludesTime,
+      facets,
+      hideAuthorFacet,
+      openFacets,
+      query
+    } = this.props;
 
     const displayProjectsFacet =
       !component || !['TRK', 'BRC', 'DIR', 'DEV_PRJ'].includes(component.qualifier);
@@ -177,6 +185,7 @@ export class Sidebar extends React.PureComponent<Props> {
         <CreationDateFacet
           component={component}
           createdAfter={query.createdAfter}
+          createdAfterIncludesTime={createdAfterIncludesTime}
           createdAt={query.createdAt}
           createdBefore={query.createdBefore}
           createdInLast={query.createdInLast}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/CreationDateFacet-test.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/CreationDateFacet-test.tsx
new file mode 100644 (file)
index 0000000..0398ff3
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { shallow } from 'enzyme';
+import * as React from 'react';
+import { InjectedIntlProps } from 'react-intl';
+import { mockComponent } from '../../../../helpers/testMocks';
+import { CreationDateFacet } from '../CreationDateFacet';
+
+it('should render correctly', () => {
+  expect(shallowRender({ open: false })).toMatchSnapshot('closed');
+  expect(shallowRender()).toMatchSnapshot('clear');
+  expect(shallowRender({ createdAt: '2019.05.21T13:33:00Z' })).toMatchSnapshot('created at');
+  expect(
+    shallowRender({
+      createdAfter: new Date('2019.04.29T13:33:00Z'),
+      createdAfterIncludesTime: true
+    })
+  ).toMatchSnapshot('created after');
+  expect(
+    shallowRender({
+      createdAfter: new Date('2019.04.29T13:33:00Z'),
+      createdAfterIncludesTime: true
+    })
+  ).toMatchSnapshot('created after timestamp');
+  expect(shallowRender({ component: mockComponent() })).toMatchSnapshot('project');
+});
+
+it.each([
+  ['week', '1w'],
+  ['month', '1m'],
+  ['year', '1y']
+])('should render correctly for createdInLast %s', (_, createdInLast) => {
+  expect(shallowRender({ component: mockComponent(), createdInLast })).toMatchSnapshot();
+});
+
+function shallowRender(props?: Partial<CreationDateFacet['props']>) {
+  return shallow<CreationDateFacet>(
+    <CreationDateFacet
+      component={undefined}
+      fetching={false}
+      createdAfter={undefined}
+      createdAfterIncludesTime={false}
+      createdAt=""
+      createdBefore={undefined}
+      createdInLast=""
+      sinceLeakPeriod={false}
+      intl={
+        {
+          formatDate: (date: string) => 'formatted.' + date
+        } as InjectedIntlProps['intl']
+      }
+      onChange={jest.fn()}
+      onToggle={jest.fn()}
+      open={true}
+      stats={undefined}
+      {...props}
+    />
+  );
+}
index 2f7b3be7769bf8b2d72c34e5367ddaccd3b14515..a72ba4cb6450db80728ba5eebe87aa374e9b6da8 100644 (file)
@@ -77,6 +77,7 @@ const renderSidebar = (props?: Partial<Sidebar['props']>) => {
       shallow<Sidebar>(
         <Sidebar
           component={undefined}
+          createdAfterIncludesTime={false}
           facets={{}}
           loadSearchResultCount={jest.fn()}
           loadingFacets={{}}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/CreationDateFacet-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/CreationDateFacet-test.tsx.snap
new file mode 100644 (file)
index 0000000..e073a3a
--- /dev/null
@@ -0,0 +1,412 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly for createdInLast month 1`] = `
+<FacetBox
+  property="createdAt"
+>
+  <FacetHeader
+    fetching={false}
+    name="issues.facet.createdAt"
+    onClear={[Function]}
+    onClick={[Function]}
+    open={true}
+    values={
+      Array [
+        "issues.facet.createdAt.last_month",
+      ]
+    }
+  />
+  <div>
+    <div
+      className="search-navigator-date-facet-selection"
+    >
+      <DateRangeInput
+        onChange={[Function]}
+        value={
+          Object {
+            "from": undefined,
+            "to": undefined,
+          }
+        }
+      />
+    </div>
+    <div
+      className="spacer-top issues-predefined-periods"
+    >
+      <FacetItem
+        active={false}
+        disabled={false}
+        halfWidth={false}
+        loading={false}
+        name="issues.facet.createdAt.all"
+        onClick={[Function]}
+        tooltip="issues.facet.createdAt.all"
+        value=""
+      />
+      <FacetItem
+        active={false}
+        disabled={false}
+        halfWidth={false}
+        loading={false}
+        name="issues.new_code"
+        onClick={[Function]}
+        tooltip="issues.new_code_period"
+        value=""
+      />
+    </div>
+  </div>
+</FacetBox>
+`;
+
+exports[`should render correctly for createdInLast week 1`] = `
+<FacetBox
+  property="createdAt"
+>
+  <FacetHeader
+    fetching={false}
+    name="issues.facet.createdAt"
+    onClear={[Function]}
+    onClick={[Function]}
+    open={true}
+    values={
+      Array [
+        "issues.facet.createdAt.last_week",
+      ]
+    }
+  />
+  <div>
+    <div
+      className="search-navigator-date-facet-selection"
+    >
+      <DateRangeInput
+        onChange={[Function]}
+        value={
+          Object {
+            "from": undefined,
+            "to": undefined,
+          }
+        }
+      />
+    </div>
+    <div
+      className="spacer-top issues-predefined-periods"
+    >
+      <FacetItem
+        active={false}
+        disabled={false}
+        halfWidth={false}
+        loading={false}
+        name="issues.facet.createdAt.all"
+        onClick={[Function]}
+        tooltip="issues.facet.createdAt.all"
+        value=""
+      />
+      <FacetItem
+        active={false}
+        disabled={false}
+        halfWidth={false}
+        loading={false}
+        name="issues.new_code"
+        onClick={[Function]}
+        tooltip="issues.new_code_period"
+        value=""
+      />
+    </div>
+  </div>
+</FacetBox>
+`;
+
+exports[`should render correctly for createdInLast year 1`] = `
+<FacetBox
+  property="createdAt"
+>
+  <FacetHeader
+    fetching={false}
+    name="issues.facet.createdAt"
+    onClear={[Function]}
+    onClick={[Function]}
+    open={true}
+    values={
+      Array [
+        "issues.facet.createdAt.last_year",
+      ]
+    }
+  />
+  <div>
+    <div
+      className="search-navigator-date-facet-selection"
+    >
+      <DateRangeInput
+        onChange={[Function]}
+        value={
+          Object {
+            "from": undefined,
+            "to": undefined,
+          }
+        }
+      />
+    </div>
+    <div
+      className="spacer-top issues-predefined-periods"
+    >
+      <FacetItem
+        active={false}
+        disabled={false}
+        halfWidth={false}
+        loading={false}
+        name="issues.facet.createdAt.all"
+        onClick={[Function]}
+        tooltip="issues.facet.createdAt.all"
+        value=""
+      />
+      <FacetItem
+        active={false}
+        disabled={false}
+        halfWidth={false}
+        loading={false}
+        name="issues.new_code"
+        onClick={[Function]}
+        tooltip="issues.new_code_period"
+        value=""
+      />
+    </div>
+  </div>
+</FacetBox>
+`;
+
+exports[`should render correctly: clear 1`] = `
+<FacetBox
+  property="createdAt"
+>
+  <FacetHeader
+    fetching={false}
+    name="issues.facet.createdAt"
+    onClear={[Function]}
+    onClick={[Function]}
+    open={true}
+    values={Array []}
+  />
+  <div>
+    <div
+      className="search-navigator-date-facet-selection"
+    >
+      <DateRangeInput
+        onChange={[Function]}
+        value={
+          Object {
+            "from": undefined,
+            "to": undefined,
+          }
+        }
+      />
+    </div>
+    <div
+      className="spacer-top issues-predefined-periods"
+    >
+      <FacetItem
+        active={true}
+        disabled={false}
+        halfWidth={false}
+        loading={false}
+        name="issues.facet.createdAt.all"
+        onClick={[Function]}
+        tooltip="issues.facet.createdAt.all"
+        value=""
+      />
+      <FacetItem
+        active={false}
+        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.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>
+</FacetBox>
+`;
+
+exports[`should render correctly: closed 1`] = `
+<FacetBox
+  property="createdAt"
+>
+  <FacetHeader
+    fetching={false}
+    name="issues.facet.createdAt"
+    onClear={[Function]}
+    onClick={[Function]}
+    open={false}
+    values={Array []}
+  />
+</FacetBox>
+`;
+
+exports[`should render correctly: created after 1`] = `
+<FacetBox
+  property="createdAt"
+>
+  <FacetHeader
+    fetching={false}
+    name="issues.facet.createdAt"
+    onClear={[Function]}
+    onClick={[Function]}
+    open={true}
+    values={
+      Array [
+        "formatted.Invalid Date",
+      ]
+    }
+  />
+  <div
+    className="search-navigator-facet-container"
+  >
+    <strong>
+      after
+       
+    </strong>
+    <DateTimeFormatter
+      date={Date { NaN }}
+    />
+  </div>
+</FacetBox>
+`;
+
+exports[`should render correctly: created after timestamp 1`] = `
+<FacetBox
+  property="createdAt"
+>
+  <FacetHeader
+    fetching={false}
+    name="issues.facet.createdAt"
+    onClear={[Function]}
+    onClick={[Function]}
+    open={true}
+    values={
+      Array [
+        "formatted.Invalid Date",
+      ]
+    }
+  />
+  <div
+    className="search-navigator-facet-container"
+  >
+    <strong>
+      after
+       
+    </strong>
+    <DateTimeFormatter
+      date={Date { NaN }}
+    />
+  </div>
+</FacetBox>
+`;
+
+exports[`should render correctly: created at 1`] = `
+<FacetBox
+  property="createdAt"
+>
+  <FacetHeader
+    fetching={false}
+    name="issues.facet.createdAt"
+    onClear={[Function]}
+    onClick={[Function]}
+    open={true}
+    values={
+      Array [
+        "formatted.2019.05.21T13:33:00Z",
+      ]
+    }
+  />
+  <div
+    className="search-navigator-facet-container"
+  >
+    <DateTimeFormatter
+      date="2019.05.21T13:33:00Z"
+    />
+    <br />
+    <span
+      className="note"
+    >
+      <DateFromNow
+        date="2019.05.21T13:33:00Z"
+      />
+    </span>
+  </div>
+</FacetBox>
+`;
+
+exports[`should render correctly: project 1`] = `
+<FacetBox
+  property="createdAt"
+>
+  <FacetHeader
+    fetching={false}
+    name="issues.facet.createdAt"
+    onClear={[Function]}
+    onClick={[Function]}
+    open={true}
+    values={Array []}
+  />
+  <div>
+    <div
+      className="search-navigator-date-facet-selection"
+    >
+      <DateRangeInput
+        onChange={[Function]}
+        value={
+          Object {
+            "from": undefined,
+            "to": undefined,
+          }
+        }
+      />
+    </div>
+    <div
+      className="spacer-top issues-predefined-periods"
+    >
+      <FacetItem
+        active={true}
+        disabled={false}
+        halfWidth={false}
+        loading={false}
+        name="issues.facet.createdAt.all"
+        onClick={[Function]}
+        tooltip="issues.facet.createdAt.all"
+        value=""
+      />
+      <FacetItem
+        active={false}
+        disabled={false}
+        halfWidth={false}
+        loading={false}
+        name="issues.new_code"
+        onClick={[Function]}
+        tooltip="issues.new_code_period"
+        value=""
+      />
+    </div>
+  </div>
+</FacetBox>
+`;
index 6b5b98f328a1e2a1d125c6413a38ee5ccf90ee5e..1917398eb95423d2340f06ede85b4e0f69c218bb 100644 (file)
@@ -10,6 +10,7 @@ active=Active
 activate=Activate
 add_verb=Add
 admin=Admin
+after=After
 apply=Apply
 all=All
 and=And