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;