github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/views/cluster/components/range/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 { Button, TimePicker, notification, Calendar, Icon } from "antd";
    12  import moment, { Moment } from "moment";
    13  import { TimeWindow } from "src/redux/timewindow";
    14  import { trackTimeScaleSelected } from "src/util/analytics";
    15  import React from "react";
    16  import "./range.styl";
    17  import { arrowRenderer } from "src/views/shared/components/dropdown";
    18  
    19  export enum DateTypes {
    20    DATE_FROM,
    21    DATE_TO,
    22  }
    23  
    24  export type RangeOption = {
    25    value: string;
    26    label: string;
    27    timeLabel: string;
    28  };
    29  
    30  export type Selected = {
    31    dateStart?: string;
    32    dateEnd?: string;
    33    timeStart?: string;
    34    timeEnd?: string;
    35    title?: string;
    36  };
    37  
    38  interface RangeSelectProps {
    39    options: RangeOption[];
    40    onChange: (arg0: RangeOption) => void;
    41    changeDate: (arg0: moment.Moment, arg1: DateTypes) => void;
    42    onOpened?: () => void;
    43    onClosed?: () => void;
    44    value: TimeWindow;
    45    selected: Selected;
    46    useTimeRange: boolean;
    47  }
    48  
    49  type Nullable<T> = T | null;
    50  
    51  interface RangeSelectState {
    52    opened: boolean;
    53    width: number;
    54    custom: boolean;
    55    selectMonthStart: Nullable<moment.Moment>;
    56    selectMonthEnd: Nullable<moment.Moment>;
    57  }
    58  
    59  class RangeSelect extends React.Component<RangeSelectProps, RangeSelectState> {
    60    state = {
    61      opened: false,
    62      width: window.innerWidth,
    63      custom: false,
    64      selectMonthStart: null as moment.Moment,
    65      selectMonthEnd: null as moment.Moment,
    66    };
    67  
    68    private rangeContainer = React.createRef<HTMLDivElement>();
    69  
    70    componentDidMount() {
    71      window.addEventListener("resize", this.updateDimensions);
    72    }
    73  
    74    componentWillUnmount() {
    75      window.removeEventListener("resize", this.updateDimensions);
    76    }
    77  
    78    updateDimensions = () => {
    79      this.setState({
    80        width: window.innerWidth,
    81      });
    82    }
    83  
    84    isValid = (date: moment.Moment, direction: DateTypes) => {
    85      const { value } = this.props;
    86      let valid = true;
    87      switch (direction) {
    88        case DateTypes.DATE_FROM:
    89          valid = !(date >= value.end);
    90          break;
    91        case DateTypes.DATE_TO:
    92          valid = !(date <= value.start);
    93          break;
    94        default:
    95          valid = true;
    96      }
    97      return valid;
    98    }
    99  
   100    onChangeDate = (direction: DateTypes) => (date: Moment) => {
   101      const { changeDate, value } = this.props;
   102      this.clearPanelValues();
   103      if (this.isValid(date, direction)) {
   104        changeDate(moment.utc(date), direction);
   105      } else {
   106        if (direction === DateTypes.DATE_TO) {
   107          changeDate(moment(value.start).add(10, "minute"), direction);
   108        } else {
   109          changeDate(moment(value.end).add(-10, "minute"), direction);
   110        }
   111        notification.info({
   112          message: "The timeframe has been set to a 10 minute range",
   113          description: "An invalid timeframe was entered. The timeframe has been set to a 10 minute range.",
   114        });
   115      }
   116    }
   117  
   118    renderTimePickerAddon = (direction: DateTypes) => () => <Button type="default" onClick={() => this.onChangeDate(direction)(moment())} size="small">Now</Button>;
   119  
   120    renderDatePickerAddon = (direction: DateTypes) => <div className="calendar-today-btn"><Button type="default" onClick={() => this.onChangeDate(direction)(moment())} size="small">Today</Button></div>;
   121  
   122    clearPanelValues = () => this.setState({ selectMonthEnd: null, selectMonthStart: null });
   123  
   124    onChangeOption = (option: RangeOption) => () => {
   125      const { onChange } = this.props;
   126      this.toggleDropDown();
   127      onChange(option);
   128    }
   129  
   130    toggleCustomPicker = (custom: boolean) => () => this.setState({ custom }, this.clearPanelValues);
   131  
   132    toggleDropDown = () => {
   133      this.setState(
   134        (prevState) => {
   135          return {
   136            opened: !prevState.opened,
   137            /*
   138            Always close the custom date picker pane when toggling the dropdown.
   139  
   140            The user must always manually choose to open it because right now we have
   141            no button to "go back" to the list of presets from the custom timepicker.
   142             */
   143            custom: false,
   144          };
   145        },
   146        () => {
   147        if (this.state.opened) {
   148          this.props.onOpened();
   149        } else {
   150          this.props.onClosed();
   151        }
   152      });
   153    }
   154  
   155    handleOptionButtonOnClick = (option: RangeOption) => () => {
   156      trackTimeScaleSelected(option.label);
   157      (option.value === "Custom" ? this.toggleCustomPicker(true) : this.onChangeOption(option))();
   158    }
   159  
   160    optionButton = (option: RangeOption) => (
   161      <Button
   162        type="default"
   163        className={`_time-button ${this.props.selected.title === option.value && "active" || ""}`}
   164        onClick={this.handleOptionButtonOnClick(option)}
   165        ghost
   166      >
   167        <span className="dropdown__range-title">{this.props.selected.title !== "Custom" && option.value === "Custom" ? "--" : option.timeLabel}</span>
   168        <span className="__option-label">{option.value === "Custom" ? "Custom date range" : option.value}</span>
   169      </Button>
   170    )
   171  
   172    renderOptions = () => {
   173      const { options } = this.props;
   174      return options.map(option => this.optionButton(option));
   175    }
   176  
   177    findSelectedValue = () => {
   178      const { options, selected } = this.props;
   179      const value = options.find(option => option.value === selected.title);
   180      return value && value.label !== "Custom" ? (
   181        <span className="Select-value-label">{value.label}</span>
   182      ) : (
   183        <span className="Select-value-label">{selected.dateStart} <span className="_label-time">{selected.timeStart}</span> - {selected.dateEnd} <span className="_label-time">{selected.timeEnd}</span></span>
   184      );
   185    }
   186  
   187    getDisabledHours = (isStart?: boolean) => () => {
   188      const { value } = this.props;
   189      const start = Number(moment.utc(value.start).format("HH"));
   190      const end = Number(moment.utc(value.end).format("HH"));
   191      const hours = [];
   192      for (let i = 0 ; i < (isStart ? moment().hour() : start); i++) {
   193        if (isStart) {
   194          hours.push((end + 1) + i);
   195        } else {
   196          hours.push(i);
   197        }
   198      }
   199      return hours;
   200    }
   201  
   202    getDisabledMinutes = (isStart?: boolean) => () => {
   203      const { value } = this.props;
   204      const startHour = Number(moment.utc(value.start).format("HH"));
   205      const endHour = Number(moment.utc(value.end).format("HH"));
   206      const startMinutes = Number(moment.utc(value.start).format("mm"));
   207      const endMinutes = Number(moment.utc(value.end).format("mm"));
   208      const minutes = [];
   209      if (startHour === endHour) {
   210        for (let i = 0 ; i < (isStart ? moment().minute() : startMinutes); i++) {
   211          if (isStart) {
   212            minutes.push((endMinutes + 1) + i);
   213          } else {
   214            minutes.push(i);
   215          }
   216        }
   217      }
   218      return minutes;
   219    }
   220  
   221    getDisabledSeconds = (isStart?: boolean) => () => {
   222      const { value } = this.props;
   223      const startHour = Number(moment.utc(value.start).format("HH"));
   224      const endHour = Number(moment.utc(value.end).format("HH"));
   225      const startMinutes = Number(moment.utc(value.start).format("mm"));
   226      const endMinutes = Number(moment.utc(value.end).format("mm"));
   227      const startSeconds = Number(moment.utc(value.start).format("ss"));
   228      const endSeconds = Number(moment.utc(value.end).format("ss"));
   229      const seconds = [];
   230      if (startHour === endHour && startMinutes === endMinutes) {
   231        for (let i = 0 ; i < (isStart ? moment().second() : startSeconds + 1); i++) {
   232          if (isStart) {
   233            seconds.push(endSeconds + i);
   234          } else {
   235            seconds.push(i);
   236          }
   237        }
   238      }
   239      return seconds;
   240    }
   241  
   242    headerRender = (item: any) => (
   243      <div className="calendar-month-picker">
   244        <Button type="default" onClick={() => item.onChange(moment(item.value).subtract(1, "months"))}><Icon type="left" /></Button>
   245        <span>{moment(item.value).format("MMM YYYY")}</span>
   246        <Button type="default" onClick={() => item.onChange(moment(item.value).add(1, "months"))}><Icon type="right" /></Button>
   247      </div>
   248    )
   249  
   250    onPanelChange = (direction: DateTypes) => (date: Moment) => {
   251      const { selectMonthStart, selectMonthEnd } = this.state;
   252  
   253      this.setState({
   254        selectMonthStart: direction === DateTypes.DATE_FROM ? date : selectMonthStart,
   255        selectMonthEnd: direction === DateTypes.DATE_TO ? date : selectMonthEnd,
   256      });
   257    }
   258  
   259    renderContent = () => {
   260      const { value, useTimeRange } = this.props;
   261      const { custom , selectMonthStart, selectMonthEnd } = this.state;
   262      const start = useTimeRange ? moment.utc(value.start) : null;
   263      const end = useTimeRange ? moment.utc(value.end) : null;
   264      const timePickerFormat = "h:mm:ss A";
   265      const isSameDate = useTimeRange && moment(start).isSame(end, "day");
   266      const calendarStartValue = selectMonthStart ? selectMonthStart : start ? start : moment();
   267      const calendarEndValue = selectMonthEnd ? selectMonthEnd : end ? end : moment();
   268  
   269      if (!custom) {
   270        return <div className="_quick-view">{this.renderOptions()}</div>;
   271      }
   272  
   273      return (
   274        <React.Fragment>
   275          <div className="_start">
   276            <span className="_title">From</span>
   277            <div className="range-calendar">
   278              <Calendar
   279                fullscreen={false}
   280                value={calendarStartValue}
   281                disabledDate={(currentDate) => (currentDate > (end || moment()))}
   282                headerRender={this.headerRender}
   283                onSelect={this.onChangeDate(DateTypes.DATE_FROM)}
   284                onPanelChange={this.onPanelChange(DateTypes.DATE_FROM)}
   285              />
   286              {this.renderDatePickerAddon(DateTypes.DATE_FROM)}
   287            </div>
   288            <TimePicker
   289              value={start}
   290              allowClear={false}
   291              format={`${timePickerFormat} ${moment(start).isSame(moment.utc(), "minute") && "[- Now]" || ""}`}
   292              use12Hours
   293              addon={this.renderTimePickerAddon(DateTypes.DATE_FROM)}
   294              onChange={this.onChangeDate(DateTypes.DATE_FROM)}
   295              disabledHours={isSameDate && this.getDisabledHours(true) || undefined}
   296              disabledMinutes={isSameDate && this.getDisabledMinutes(true) || undefined}
   297              disabledSeconds={isSameDate && this.getDisabledSeconds(true) || undefined}
   298            />
   299          </div>
   300          <div className="_end">
   301            <span className="_title">To</span>
   302            <div className="range-calendar">
   303              <Calendar
   304                fullscreen={false}
   305                value={calendarEndValue}
   306                disabledDate={(currentDate) => (currentDate > moment() || currentDate < (start || moment()))}
   307                headerRender={this.headerRender}
   308                onSelect={this.onChangeDate(DateTypes.DATE_TO)}
   309                onPanelChange={this.onPanelChange(DateTypes.DATE_TO)}
   310              />
   311              {this.renderDatePickerAddon(DateTypes.DATE_TO)}
   312            </div>
   313            <TimePicker
   314              value={end}
   315              allowClear={false}
   316              format={`${timePickerFormat} ${moment(end).isSame(moment.utc(), "minute") && "[- Now]" || ""}`}
   317              use12Hours
   318              addon={this.renderTimePickerAddon(DateTypes.DATE_TO)}
   319              onChange={this.onChangeDate(DateTypes.DATE_TO)}
   320              disabledHours={isSameDate && this.getDisabledHours() || undefined}
   321              disabledMinutes={isSameDate && this.getDisabledMinutes() || undefined}
   322              disabledSeconds={isSameDate && this.getDisabledSeconds() || undefined}
   323            />
   324          </div>
   325        </React.Fragment>
   326      );
   327    }
   328  
   329    render() {
   330      const { opened, width, custom } = this.state;
   331      const selectedValue = this.findSelectedValue();
   332      const containerLeft = this.rangeContainer.current ? this.rangeContainer.current.getBoundingClientRect().left : 0;
   333      const left = width >= (containerLeft + (custom ? 555 : 453)) ? 0 : width - (containerLeft + (custom ? 555 : 453));
   334  
   335      return (
   336        <div ref={this.rangeContainer} className="Range">
   337          <div className="click-zone" onClick={this.toggleDropDown}/>
   338          {opened && <div className="trigger-container" onClick={this.toggleDropDown} />}
   339          <div className="trigger-wrapper">
   340            <div
   341              className={`trigger Select ${opened ? "is-open" : ""}`}
   342            >
   343              <span className="Select-value-label">
   344                {selectedValue}
   345              </span>
   346              <div className="Select-control">
   347                <div className="Select-arrow-zone">
   348                  {arrowRenderer({ isOpen: opened })}
   349                </div>
   350              </div>
   351            </div>
   352            {opened && (
   353              <div className={`range-selector ${custom ? "__custom" : "__options"}`} style={{ left }}>
   354                {this.renderContent()}
   355              </div>
   356            )}
   357          </div>
   358        </div>
   359      );
   360    }
   361  }
   362  
   363  export default RangeSelect;