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));