github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/views/cluster/containers/timescale/index.tsx (about)

     1  // Copyright 2018 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  import _ from "lodash";
    12  import moment from "moment";
    13  import { queryByName, queryToObj, queryToString } from "src/util/query";
    14  import React from "react";
    15  import { connect } from "react-redux";
    16  import { RouteComponentProps, withRouter } from "react-router-dom";
    17  import { refreshNodes } from "src/redux/apiReducers";
    18  import { AdminUIState } from "src/redux/state";
    19  import * as timewindow from "src/redux/timewindow";
    20  import { INodeStatus } from "src/util/proto";
    21  import { trackTimeFrameChange } from "src/util/analytics";
    22  import Dropdown, { ArrowDirection, DropdownOption } from "src/views/shared/components/dropdown";
    23  import TimeFrameControls from "../../components/controls";
    24  import RangeSelect, { DateTypes } from "../../components/range";
    25  import "./timescale.styl";
    26  import { Divider } from "antd";
    27  import classNames from "classnames";
    28  
    29  interface TimeScaleDropdownProps extends RouteComponentProps {
    30    currentScale: timewindow.TimeScale;
    31    currentWindow: timewindow.TimeWindow;
    32    availableScales: timewindow.TimeScaleCollection;
    33    setTimeScale: typeof timewindow.setTimeScale;
    34    setTimeRange: typeof timewindow.setTimeRange;
    35    // Track node data to find the oldest node and set the default timescale.
    36    refreshNodes: typeof refreshNodes;
    37    nodeStatuses: INodeStatus[];
    38    nodeStatusesValid: boolean;
    39    // Track whether the default has been set.
    40    useTimeRange: boolean;
    41  }
    42  
    43  export const getTimeLabel = (currentWindow?: timewindow.TimeWindow, windowSize?: moment.Duration) => {
    44    const time = windowSize ? windowSize : currentWindow ? moment.duration(moment(currentWindow.end).diff(currentWindow.start)) : moment.duration(10, "minutes");
    45    const seconds = time.asSeconds();
    46    const minutes = 60;
    47    const hour = minutes * 60;
    48    const day = hour * 24;
    49    const week = day * 7;
    50    const month = day * moment().daysInMonth();
    51    switch (true) {
    52      case seconds < hour:
    53        return time.asMinutes().toFixed() + "m";
    54      case (seconds >= hour && seconds < day):
    55        return time.asHours().toFixed() + "h";
    56      case seconds < week:
    57        return time.asDays().toFixed() + "d";
    58      case seconds < month:
    59        return time.asWeeks().toFixed() + "w";
    60      default:
    61        return time.asMonths().toFixed() + "m";
    62    }
    63  };
    64  
    65  // TimeScaleDropdown is the dropdown that allows users to select the time range
    66  // for graphs.
    67  class TimeScaleDropdown extends React.Component<TimeScaleDropdownProps, {}> {
    68    state = {
    69      is_opened: false,
    70    };
    71  
    72    changeSettings = (newTimescaleKey: DropdownOption) => {
    73      const newSettings = timewindow.availableTimeScales[newTimescaleKey.value];
    74      newSettings.windowEnd = null;
    75      if (newSettings) {
    76        this.setQueryParamsByDates(newSettings.windowSize, moment());
    77        this.props.setTimeScale({ ...newSettings, key: newTimescaleKey.value });
    78      }
    79    }
    80  
    81    arrowClick = (direction: ArrowDirection) => {
    82      const { currentWindow, currentScale } = this.props;
    83      const windowSize: any = moment.duration(moment(currentWindow.end).diff(currentWindow.start));
    84      const seconds = windowSize.asSeconds();
    85      let selected = {};
    86      let key = currentScale.key;
    87      let windowEnd = currentWindow.end || moment.utc();
    88  
    89      switch (direction) {
    90        case ArrowDirection.RIGHT:
    91          trackTimeFrameChange("next frame");
    92          if (windowEnd) {
    93            windowEnd = windowEnd.add(seconds, "seconds");
    94          }
    95          break;
    96        case ArrowDirection.LEFT:
    97          trackTimeFrameChange("previous frame");
    98          windowEnd = windowEnd.subtract(seconds, "seconds");
    99          break;
   100        case ArrowDirection.CENTER:
   101          trackTimeFrameChange("now");
   102          windowEnd = moment.utc();
   103          break;
   104        default:
   105          console.error("Unknown direction: ", direction);
   106      }
   107      // If the timescale extends into the future then fallback to a default
   108      // timescale. Otherwise set the key to "Custom" so it appears correctly.
   109      if (!windowEnd || windowEnd > moment().subtract(currentScale.windowValid)) {
   110        const size = { windowSize: { _data: windowSize._data } } ;
   111        if (_.find(timewindow.availableTimeScales, size as any)) {
   112          const data = {
   113            ..._.find(timewindow.availableTimeScales, size as any),
   114            key: _.findKey(timewindow.availableTimeScales, size as any),
   115          };
   116          selected = data;
   117        } else {
   118          key = "Custom";
   119        }
   120      } else {
   121        key = "Custom";
   122      }
   123  
   124      this.setQueryParamsByDates(windowSize, windowEnd);
   125      this.props.setTimeScale({ ...currentScale, windowEnd, windowSize, key, ...selected });
   126    }
   127  
   128    getTimescaleOptions = () => {
   129      const { currentWindow } = this.props;
   130      const timescaleOptions = _.map(timewindow.availableTimeScales, (_ts, k) => {
   131        return { value: k, label: k, timeLabel: getTimeLabel(null, _ts.windowSize) };
   132      });
   133  
   134      // This just ensures that if the key is "Custom" it will show up in the
   135      // dropdown options. If a custom value isn't currently selected, "Custom"
   136      // won't show up in the list of options.
   137      timescaleOptions.push({
   138        value: "Custom",
   139        label: "Custom",
   140        timeLabel: getTimeLabel(currentWindow),
   141      });
   142      return timescaleOptions;
   143    }
   144  
   145    componentDidMount() {
   146      this.props.refreshNodes();
   147      this.getQueryParams();
   148    }
   149  
   150    getQueryParams = () => {
   151      const { location } = this.props;
   152      const queryStart = queryByName(location, "start");
   153      const queryEnd = queryByName(location, "end");
   154      const start = queryStart && moment.unix(Number(queryStart)).utc();
   155      const end = queryEnd && moment.unix(Number(queryEnd)).utc();
   156  
   157      this.setDatesByQueryParams({ start, end });
   158    }
   159  
   160    setQueryParams = (date: moment.Moment, type: DateTypes) => {
   161      const { location, history } = this.props;
   162      const dataType = type === DateTypes.DATE_FROM ? "start" : "end";
   163      const timestamp = moment(date).format("X");
   164      const query = queryToObj(location, dataType, timestamp);
   165      history.push({
   166        pathname: location.pathname,
   167        search: `?${queryToString(query)}`,
   168      });
   169    }
   170  
   171    componentDidUpdate() {
   172      if (!this.props.nodeStatusesValid) {
   173        this.props.refreshNodes();
   174      }
   175    }
   176  
   177    setQueryParamsByDates = (duration: moment.Duration, dateEnd: moment.Moment) => {
   178      const { location, history } = this.props;
   179      const { pathname, search } = location;
   180      const urlParams = new URLSearchParams(search);
   181      const seconds = duration.clone().asSeconds();
   182      const end = dateEnd.clone();
   183      const start =  moment.utc(end.subtract(seconds, "seconds")).format("X");
   184  
   185      urlParams.set("start", start);
   186      urlParams.set("end", moment.utc(dateEnd).format("X"));
   187  
   188      history.push({
   189        pathname,
   190        search: urlParams.toString(),
   191      });
   192    }
   193  
   194    setDatesByQueryParams = (dates?: timewindow.TimeWindow) => {
   195      const currentWindow = _.clone(this.props.currentWindow);
   196      const end = dates.end || currentWindow && currentWindow.end || moment();
   197      const start = dates.start || currentWindow && currentWindow.start || moment().subtract(10, "minutes");
   198      const seconds = moment.duration(moment(end).diff(start)).asSeconds();
   199      const timeScale = timewindow.findClosestTimeScale(seconds);
   200      const now = moment();
   201      if (moment.duration(now.diff(end)).asMinutes() > timeScale.sampleSize.asMinutes()) {
   202        timeScale.key = "Custom";
   203      }
   204      timeScale.windowEnd = null;
   205      this.props.setTimeRange({ end, start });
   206      this.props.setTimeScale(timeScale);
   207    }
   208  
   209    setDate = (date: moment.Moment, type: DateTypes) => {
   210      const currentWindow = _.clone(this.props.currentWindow);
   211      const selected = _.clone(this.props.currentScale);
   212      const end  = currentWindow.end || moment().utc().set({hours: 23, minutes: 59, seconds: 0});
   213      const start = currentWindow.start || moment().utc().set({hours: 0, minutes: 0, seconds: 0});
   214      switch (type) {
   215        case DateTypes.DATE_FROM:
   216          this.setQueryParams(date, DateTypes.DATE_FROM);
   217          currentWindow.start = date;
   218          currentWindow.end = end;
   219          break;
   220        case DateTypes.DATE_TO:
   221          this.setQueryParams(date, DateTypes.DATE_TO);
   222          currentWindow.start = start;
   223          currentWindow.end = date;
   224          break;
   225        default:
   226          console.error("Unknown type: ", type);
   227      }
   228  
   229      selected.key = "Custom";
   230      this.props.setTimeScale(selected);
   231      this.props.setTimeRange(currentWindow);
   232    }
   233  
   234    getTimeRangeTitle = () => {
   235      const { currentWindow, currentScale } = this.props;
   236      const dateFormat = "MMM DD,";
   237      const timeFormat = "h:mmA";
   238      if (currentScale.key === "Custom" && currentWindow) {
   239        const isSameStartDay = moment(currentWindow.start).isSame(moment(), "day");
   240        const isSameEndDay = moment(currentWindow.end).isSame(moment(), "day");
   241        return {
   242          dateStart: isSameStartDay ? "" : moment.utc(currentWindow.start).format(dateFormat),
   243          dateEnd: isSameEndDay ? "" : moment.utc(currentWindow.end).format(dateFormat),
   244          timeStart: moment.utc(currentWindow.start).format(timeFormat),
   245          timeEnd: moment.utc(currentWindow.end).format(timeFormat),
   246          title: "Custom",
   247        };
   248      } else {
   249        return {
   250          title: currentScale.key,
   251        };
   252      }
   253    }
   254  
   255    generateDisabledArrows = () => {
   256      const { currentWindow } = this.props;
   257      const disabledArrows = [];
   258      if (currentWindow) {
   259        const differenceEndToNow = moment.duration(moment().diff(currentWindow.end)).asMinutes();
   260        const differenceEndToStart = moment.duration(moment(currentWindow.end).diff(currentWindow.start)).asMinutes();
   261        if (differenceEndToNow < differenceEndToStart) {
   262          if (differenceEndToNow < 10) {
   263            disabledArrows.push(ArrowDirection.CENTER);
   264          }
   265          disabledArrows.push(ArrowDirection.RIGHT);
   266        }
   267      }
   268      return disabledArrows;
   269    }
   270  
   271    onOpened = () => {
   272      this.setState({ is_opened: true });
   273    }
   274  
   275    onClosed = () => {
   276      this.setState({ is_opened: false });
   277    }
   278  
   279    render() {
   280      const { useTimeRange, currentScale, currentWindow } = this.props;
   281      return (
   282        <div className="timescale">
   283          <Divider type="vertical" />
   284          <Dropdown
   285            title={getTimeLabel(currentWindow)}
   286            options={[]}
   287            selected={currentScale.key}
   288            onChange={this.changeSettings}
   289            className={classNames({ "dropdown__focused": this.state.is_opened })}
   290            isTimeRange
   291            content={
   292              <RangeSelect
   293                onOpened={this.onOpened}
   294                onClosed={this.onClosed}
   295                value={currentWindow}
   296                useTimeRange={useTimeRange}
   297                selected={this.getTimeRangeTitle()}
   298                onChange={this.changeSettings}
   299                changeDate={this.setDate}
   300                options={this.getTimescaleOptions()}
   301              />
   302            }
   303          />
   304          <TimeFrameControls disabledArrows={this.generateDisabledArrows()} onArrowClick={this.arrowClick} />
   305        </div>
   306      );
   307    }
   308  }
   309  
   310  export default withRouter(connect(
   311    (state: AdminUIState) => {
   312      return {
   313        nodeStatusesValid: state.cachedData.nodes.valid,
   314        nodeStatuses: state.cachedData.nodes.data,
   315        currentScale: (state.timewindow as timewindow.TimeWindowState).scale,
   316        currentWindow: (state.timewindow as timewindow.TimeWindowState).currentWindow,
   317        availableScales: timewindow.availableTimeScales,
   318        useTimeRange: state.timewindow.useTimeRange,
   319      };
   320    },
   321    {
   322      setTimeScale: timewindow.setTimeScale,
   323      setTimeRange: timewindow.setTimeRange,
   324      refreshNodes: refreshNodes,
   325    },
   326  )(TimeScaleDropdown));