Browse Source

feat(ui): New date range selector (#10314)

New date range selector that includes a calendar view when selecting
absolute dates

Closes APP-682

![date-impliment](https://user-images.githubusercontent.com/435981/47878960-bbe9b780-dddc-11e8-8b93-76533bcd5503.gif)
Billy Vong 6 years ago
parent
commit
0c5975ab74

+ 1 - 0
package.json

@@ -65,6 +65,7 @@
     "react-addons-css-transition-group": "15.6.2",
     "react-addons-css-transition-group": "15.6.2",
     "react-autosize-textarea": "^4.0.0",
     "react-autosize-textarea": "^4.0.0",
     "react-bootstrap": "^0.32.0",
     "react-bootstrap": "^0.32.0",
+    "react-date-range": "^1.0.0-beta",
     "react-document-title": "2.0.3",
     "react-document-title": "2.0.3",
     "react-dom": "16.5.1",
     "react-dom": "16.5.1",
     "react-emotion": "9.1.2",
     "react-emotion": "9.1.2",

+ 9 - 4
src/sentry/static/sentry/app/actionCreators/health.jsx

@@ -1,10 +1,17 @@
 import moment from 'moment';
 import moment from 'moment';
 
 
+import {DEFAULT_STATS_PERIOD} from 'app/constants';
+import {getUtcDateString} from 'app/utils/dates';
+
 const BASE_URL = org => `/organizations/${org.slug}/health/`;
 const BASE_URL = org => `/organizations/${org.slug}/health/`;
 
 
 // Gets the period to query with if we need to double the initial period in order
 // Gets the period to query with if we need to double the initial period in order
 // to get data for the previous period
 // to get data for the previous period
 const getPeriod = ({period, start, end}, {shouldDoublePeriod}) => {
 const getPeriod = ({period, start, end}, {shouldDoublePeriod}) => {
+  if (!period && !start && !end) {
+    period = DEFAULT_STATS_PERIOD;
+  }
+
   // you can not specify both relative and absolute periods
   // you can not specify both relative and absolute periods
   // relative period takes precendence
   // relative period takes precendence
   if (period) {
   if (period) {
@@ -23,10 +30,8 @@ const getPeriod = ({period, start, end}, {shouldDoublePeriod}) => {
     const diff = moment(end).diff(moment(start));
     const diff = moment(end).diff(moment(start));
 
 
     return {
     return {
-      start: moment(start)
-        .subtract(diff)
-        .format(moment.HTML5_FMT.DATETIME_LOCAL_MS),
-      end,
+      start: getUtcDateString(moment(start).subtract(diff)),
+      end: getUtcDateString(end),
     };
     };
   }
   }
 
 

+ 17 - 2
src/sentry/static/sentry/app/components/asyncComponent.jsx

@@ -50,6 +50,12 @@ export default class AsyncComponent extends React.Component {
   // eslint-disable-next-line react/sort-comp
   // eslint-disable-next-line react/sort-comp
   shouldReloadOnVisible = false;
   shouldReloadOnVisible = false;
 
 
+  // This affects how the component behaves when `remountComponent` is called
+  // By default, the component gets put back into a "loading" state when re-fetching data.
+  // If this is true, then when we fetch data, the original ready component remains mounted
+  // and it will need to handle any additional "reloading" states
+  shouldReload = false;
+
   // should `renderError` render the `detail` attribute of a 400 error
   // should `renderError` render the `detail` attribute of a 400 error
   shouldRenderBadRequests = false;
   shouldRenderBadRequests = false;
 
 
@@ -116,7 +122,16 @@ export default class AsyncComponent extends React.Component {
   }
   }
 
 
   remountComponent = () => {
   remountComponent = () => {
-    this.setState(this.getDefaultState(), this.fetchData);
+    if (this.shouldReload) {
+      this.setState(
+        {
+          reloading: true,
+        },
+        this.fetchData
+      );
+    } else {
+      this.setState(this.getDefaultState(), this.fetchData);
+    }
   };
   };
 
 
   visibilityReloader = () =>
   visibilityReloader = () =>
@@ -327,7 +342,7 @@ export default class AsyncComponent extends React.Component {
   }
   }
 
 
   renderComponent() {
   renderComponent() {
-    return this.state.loading && !this.state.reloading
+    return this.state.loading && (!this.shouldReload || !this.state.reloading)
       ? this.renderLoading()
       ? this.renderLoading()
       : this.state.error
       : this.state.error
         ? this.renderError(new Error('Unable to load all required endpoints'))
         ? this.renderError(new Error('Unable to load all required endpoints'))

+ 8 - 1
src/sentry/static/sentry/app/components/charts/baseChart.jsx

@@ -105,6 +105,9 @@ class BaseChart extends React.Component {
     // If data is grouped by date, then apply default date formatting to
     // If data is grouped by date, then apply default date formatting to
     // x-axis and tooltips.
     // x-axis and tooltips.
     isGroupedByDate: PropTypes.bool,
     isGroupedByDate: PropTypes.bool,
+
+    // How is data grouped (affects formatting of axis labels and tooltips)
+    interval: PropTypes.oneOf(['hour', 'day']),
   };
   };
 
 
   static defaultProps = {
   static defaultProps = {
@@ -120,6 +123,7 @@ class BaseChart extends React.Component {
     xAxis: {},
     xAxis: {},
     yAxis: {},
     yAxis: {},
     isGroupedByDate: false,
     isGroupedByDate: false,
+    interval: 'day',
   };
   };
 
 
   handleChartReady = (...args) => {
   handleChartReady = (...args) => {
@@ -149,6 +153,7 @@ class BaseChart extends React.Component {
       toolBox,
       toolBox,
 
 
       isGroupedByDate,
       isGroupedByDate,
+      interval,
       previousPeriod,
       previousPeriod,
 
 
       devicePixelRatio,
       devicePixelRatio,
@@ -188,13 +193,15 @@ class BaseChart extends React.Component {
           ...options,
           ...options,
           color: colors || this.getColorPalette(),
           color: colors || this.getColorPalette(),
           grid: Grid(grid),
           grid: Grid(grid),
-          tooltip: tooltip !== null ? Tooltip({isGroupedByDate, ...tooltip}) : null,
+          tooltip:
+            tooltip !== null ? Tooltip({interval, isGroupedByDate, ...tooltip}) : null,
           legend: legend ? Legend({...legend}) : null,
           legend: legend ? Legend({...legend}) : null,
           yAxis: yAxis !== null ? YAxis(yAxis) : null,
           yAxis: yAxis !== null ? YAxis(yAxis) : null,
           xAxis:
           xAxis:
             xAxis !== null
             xAxis !== null
               ? XAxis({
               ? XAxis({
                   ...xAxis,
                   ...xAxis,
+                  interval,
                   isGroupedByDate,
                   isGroupedByDate,
                 })
                 })
               : null,
               : null,

+ 8 - 2
src/sentry/static/sentry/app/components/charts/components/xAxis.jsx

@@ -2,9 +2,15 @@ import moment from 'moment';
 
 
 import theme from 'app/utils/theme';
 import theme from 'app/utils/theme';
 
 
-export default function XAxis({isGroupedByDate, ...props} = {}) {
+export default function XAxis({isGroupedByDate, interval, ...props} = {}) {
   const axisLabelFormatter = isGroupedByDate
   const axisLabelFormatter = isGroupedByDate
-    ? (value, index) => moment.utc(value).format('MMM Do')
+    ? (value, index) => {
+        const format = interval === 'hour' ? 'LT' : 'MMM Do';
+        return moment
+          .utc(value)
+          .local()
+          .format(format);
+      }
     : undefined;
     : undefined;
 
 
   return {
   return {

+ 1 - 0
src/sentry/static/sentry/app/components/organizations/headerItem.jsx

@@ -83,6 +83,7 @@ const StyledHeaderItem = styled('div')`
 
 
 const Content = styled('div')`
 const Content = styled('div')`
   flex: 1;
   flex: 1;
+  margin-right: ${space(1.5)};
   ${overflowEllipsis};
   ${overflowEllipsis};
 `;
 `;
 
 

+ 1 - 1
src/sentry/static/sentry/app/components/organizations/multipleEnvironmentSelector.jsx

@@ -18,7 +18,7 @@ import InlineSvg from 'app/components/inlineSvg';
 /**
 /**
  * Environment Selector
  * Environment Selector
  */
  */
-class MultipleEnvironmentSelector extends React.Component {
+class MultipleEnvironmentSelector extends React.PureComponent {
   static propTypes = {
   static propTypes = {
     onChange: PropTypes.func,
     onChange: PropTypes.func,
     onUpdate: PropTypes.func,
     onUpdate: PropTypes.func,

+ 1 - 1
src/sentry/static/sentry/app/components/organizations/multipleProjectSelector.jsx

@@ -12,7 +12,7 @@ const rootContainerStyles = css`
   display: flex;
   display: flex;
 `;
 `;
 
 
-export default class MultipleProjectSelector extends React.Component {
+export default class MultipleProjectSelector extends React.PureComponent {
   static propTypes = {
   static propTypes = {
     value: PropTypes.array,
     value: PropTypes.array,
     projects: PropTypes.array,
     projects: PropTypes.array,

+ 0 - 55
src/sentry/static/sentry/app/components/organizations/timeRangeSelector/absoluteSelector.jsx

@@ -1,55 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import moment from 'moment';
-import {Box} from 'grid-emotion';
-
-import DateTimeField from 'app/components/forms/dateTimeField';
-import {t} from 'app/locale';
-
-export default class AbsoluteSelector extends React.Component {
-  static propTypes = {
-    /**
-     * Start date value for absolute date selector
-     */
-    start: PropTypes.string,
-    /**
-     * End date value for absolute date selector
-     */
-    end: PropTypes.string,
-
-    /**
-     * Callback when value changes
-     */
-    onChange: PropTypes.func,
-  };
-
-  formatDate(date) {
-    return moment(date).format('MMMM D, h:mm a');
-  }
-
-  render() {
-    const {className, start, end, onChange} = this.props;
-
-    return (
-      <Box className={className}>
-        <Box mb={1}>{t('Update time range (UTC)')}</Box>
-        <Box mb={1}>
-          <DateTimeField
-            name="start"
-            label={t('From')}
-            value={start}
-            onChange={val => onChange('start', val)}
-          />
-        </Box>
-        <Box mb={1}>
-          <DateTimeField
-            name="end"
-            label={t('To')}
-            value={end}
-            onChange={val => onChange('end', val)}
-          />
-        </Box>
-      </Box>
-    );
-  }
-}

+ 0 - 107
src/sentry/static/sentry/app/components/organizations/timeRangeSelector/combinedSelector.jsx

@@ -1,107 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import {Box} from 'grid-emotion';
-
-import SelectControl from 'app/components/forms/selectControl';
-import DateTimeField from 'app/components/forms/dateTimeField';
-import {t} from 'app/locale';
-
-import {parseStatsPeriod} from './utils';
-
-export default class CombinedSelector extends React.Component {
-  static propTypes = {
-    /**
-     * List of choice tuples to use for relative dates
-     */
-    choices: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
-
-    /**
-     * The value for selector. This will be 'custom' if absolute dates are being used
-     */
-    relative: PropTypes.string,
-
-    /**
-     * Start date value for absolute date selector
-     */
-    start: PropTypes.string,
-    /**
-     * End date value for absolute date selector
-     */
-    end: PropTypes.string,
-
-    /**
-     * Callback when value changes
-     */
-    onChange: PropTypes.func,
-  };
-
-  static defaultProps = {
-    relative: null,
-    start: null,
-    end: null,
-  };
-
-  handleChange(prop, val) {
-    const {start, end, relative, onChange} = this.props;
-    const prev = {
-      start,
-      end,
-      relative,
-    };
-
-    if (prop === 'relative') {
-      if (val === 'custom') {
-        // Convert previous relative range to absolute values
-        const statsPeriod = parseStatsPeriod(relative);
-        onChange({
-          relative: null,
-          start: statsPeriod.start,
-          end: statsPeriod.end,
-        });
-      } else {
-        onChange({relative: val, start: null, end: null});
-      }
-    } else {
-      onChange({...prev, relative: null, [prop]: val});
-    }
-  }
-
-  render() {
-    const {className, start, end, relative, choices} = this.props;
-
-    const value = relative || 'custom';
-
-    return (
-      <Box className={className}>
-        <Box mb={1}>{t('Update time range (UTC)')}</Box>
-        <Box mb={1}>
-          <SelectControl
-            value={value}
-            choices={[...choices, ['custom', t('Custom')]]}
-            onChange={val => this.handleChange('relative', val.value)}
-          />
-        </Box>
-        {!relative && (
-          <React.Fragment>
-            <Box mb={1}>
-              <DateTimeField
-                name="start"
-                label={t('From')}
-                value={start}
-                onChange={val => this.handleChange('start', val)}
-              />
-            </Box>
-            <Box mb={1}>
-              <DateTimeField
-                name="end"
-                label={t('To')}
-                value={end}
-                onChange={val => this.handleChange('end', val)}
-              />
-            </Box>
-          </React.Fragment>
-        )}
-      </Box>
-    );
-  }
-}

Some files were not shown because too many files changed in this diff