Merge branch 'master' of github.com:influxdata/chronograf
commit
4ca38fee6f
|
@ -339,7 +339,10 @@ func (s *Service) KapacitorRulesPost(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
*/
|
||||
|
||||
req.Name = req.ID
|
||||
if req.Name == "" {
|
||||
req.Name = req.ID
|
||||
}
|
||||
|
||||
req.ID = ""
|
||||
task, err := c.Create(ctx, req)
|
||||
if err != nil {
|
||||
|
|
|
@ -147,7 +147,7 @@
|
|||
"react-grid-layout": "^0.16.6",
|
||||
"react-onclickoutside": "^5.2.0",
|
||||
"react-redux": "^4.4.0",
|
||||
"react-resizable": "^1.7.5",
|
||||
"react-resize-detector": "^2.3.0",
|
||||
"react-router": "^3.0.2",
|
||||
"react-router-redux": "^4.0.8",
|
||||
"react-tooltip": "^3.2.1",
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const EmptyRow = ({tableName}) => (
|
||||
<tr className="table-empty-state">
|
||||
<th colSpan="5">
|
||||
<p>
|
||||
You don't have any {tableName},<br />why not create one?
|
||||
</p>
|
||||
</th>
|
||||
</tr>
|
||||
)
|
||||
|
||||
const {string} = PropTypes
|
||||
|
||||
EmptyRow.propTypes = {
|
||||
tableName: string.isRequired,
|
||||
}
|
||||
|
||||
export default EmptyRow
|
|
@ -0,0 +1,16 @@
|
|||
import React, {SFC} from 'react'
|
||||
|
||||
interface Props {
|
||||
tableName: string
|
||||
}
|
||||
const EmptyRow: SFC<Props> = ({tableName}) => (
|
||||
<tr className="table-empty-state">
|
||||
<th colSpan={5}>
|
||||
<p>
|
||||
You don't have any {tableName},<br />why not create one?
|
||||
</p>
|
||||
</th>
|
||||
</tr>
|
||||
)
|
||||
|
||||
export default EmptyRow
|
|
@ -1,12 +0,0 @@
|
|||
import {proxy} from 'utils/queryUrlGenerator'
|
||||
|
||||
export const getAlerts = (source, timeRange, limit) =>
|
||||
proxy({
|
||||
source,
|
||||
query: `SELECT host, value, level, alertName FROM alerts WHERE time >= '${
|
||||
timeRange.lower
|
||||
}' AND time <= '${timeRange.upper}' ORDER BY time desc ${
|
||||
limit ? `LIMIT ${limit}` : ''
|
||||
}`,
|
||||
db: 'chronograf',
|
||||
})
|
|
@ -0,0 +1,19 @@
|
|||
import {proxy} from 'src/utils/queryUrlGenerator'
|
||||
import {TimeRange} from '../../types'
|
||||
|
||||
export const getAlerts = (
|
||||
source: string,
|
||||
timeRange: TimeRange,
|
||||
limit: number
|
||||
) => {
|
||||
const query = `SELECT host, value, level, alertName FROM alerts WHERE time >= '${
|
||||
timeRange.lower
|
||||
}' AND time <= '${timeRange.upper}' ORDER BY time desc ${
|
||||
limit ? `LIMIT ${limit}` : ''
|
||||
}`
|
||||
return proxy({
|
||||
source,
|
||||
query,
|
||||
db: 'chronograf',
|
||||
})
|
||||
}
|
|
@ -1,34 +1,95 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, {PureComponent} from 'react'
|
||||
|
||||
import _ from 'lodash'
|
||||
import classnames from 'classnames'
|
||||
import {Link} from 'react-router'
|
||||
import uuid from 'uuid'
|
||||
|
||||
import InfiniteScroll from 'shared/components/InfiniteScroll'
|
||||
import InfiniteScroll from 'src/shared/components/InfiniteScroll'
|
||||
import SearchBar from 'src/alerts/components/SearchBar'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
import {ALERTS_TABLE} from 'src/alerts/constants/tableSizing'
|
||||
import {Alert} from 'src/types/alerts'
|
||||
import {Source} from 'src/types'
|
||||
|
||||
enum Direction {
|
||||
ASC = 'asc',
|
||||
DESC = 'desc',
|
||||
NONE = 'none',
|
||||
}
|
||||
interface Props {
|
||||
alerts: Alert[]
|
||||
source: Source
|
||||
shouldNotBeFilterable: boolean
|
||||
limit: number
|
||||
isAlertsMaxedOut: boolean
|
||||
alertsCount: number
|
||||
onGetMoreAlerts: () => void
|
||||
}
|
||||
interface State {
|
||||
searchTerm: string
|
||||
filteredAlerts: Alert[]
|
||||
sortDirection: Direction
|
||||
sortKey: string
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class AlertsTable extends Component {
|
||||
class AlertsTable extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
searchTerm: '',
|
||||
filteredAlerts: this.props.alerts,
|
||||
sortDirection: null,
|
||||
sortKey: null,
|
||||
sortDirection: Direction.NONE,
|
||||
sortKey: '',
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
public componentWillReceiveProps(newProps) {
|
||||
this.filterAlerts(this.state.searchTerm, newProps.alerts)
|
||||
}
|
||||
|
||||
filterAlerts = (searchTerm, newAlerts) => {
|
||||
public render() {
|
||||
const {
|
||||
shouldNotBeFilterable,
|
||||
limit,
|
||||
onGetMoreAlerts,
|
||||
isAlertsMaxedOut,
|
||||
alertsCount,
|
||||
} = this.props
|
||||
|
||||
return shouldNotBeFilterable ? (
|
||||
<div className="alerts-widget">
|
||||
{this.renderTable()}
|
||||
{limit && alertsCount ? (
|
||||
<button
|
||||
className="btn btn-sm btn-default btn-block"
|
||||
onClick={onGetMoreAlerts}
|
||||
disabled={isAlertsMaxedOut}
|
||||
style={{marginBottom: '20px'}}
|
||||
>
|
||||
{isAlertsMaxedOut
|
||||
? `All ${alertsCount} Alerts displayed`
|
||||
: 'Load next 30 Alerts'}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="panel">
|
||||
<div className="panel-heading">
|
||||
<h2 className="panel-title">{this.props.alerts.length} Alerts</h2>
|
||||
{this.props.alerts.length ? (
|
||||
<SearchBar onSearch={this.filterAlerts} />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="panel-body">{this.renderTable()}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private filterAlerts = (searchTerm: string, newAlerts?: Alert[]): void => {
|
||||
const alerts = newAlerts || this.props.alerts
|
||||
const filterText = searchTerm.toLowerCase()
|
||||
const filteredAlerts = alerts.filter(({name, host, level}) => {
|
||||
|
@ -41,20 +102,22 @@ class AlertsTable extends Component {
|
|||
this.setState({searchTerm, filteredAlerts})
|
||||
}
|
||||
|
||||
changeSort = key => () => {
|
||||
private changeSort = (key: string): (() => void) => (): void => {
|
||||
// if we're using the key, reverse order; otherwise, set it with ascending
|
||||
if (this.state.sortKey === key) {
|
||||
const reverseDirection =
|
||||
this.state.sortDirection === 'asc' ? 'desc' : 'asc'
|
||||
const reverseDirection: Direction =
|
||||
this.state.sortDirection === Direction.ASC
|
||||
? Direction.DESC
|
||||
: Direction.ASC
|
||||
this.setState({sortDirection: reverseDirection})
|
||||
} else {
|
||||
this.setState({sortKey: key, sortDirection: 'asc'})
|
||||
this.setState({sortKey: key, sortDirection: Direction.ASC})
|
||||
}
|
||||
}
|
||||
|
||||
sortableClasses = key => {
|
||||
private sortableClasses = (key: string): string => {
|
||||
if (this.state.sortKey === key) {
|
||||
if (this.state.sortDirection === 'asc') {
|
||||
if (this.state.sortDirection === Direction.ASC) {
|
||||
return 'alert-history-table--th sortable-header sorting-ascending'
|
||||
}
|
||||
return 'alert-history-table--th sortable-header sorting-descending'
|
||||
|
@ -62,18 +125,22 @@ class AlertsTable extends Component {
|
|||
return 'alert-history-table--th sortable-header'
|
||||
}
|
||||
|
||||
sort = (alerts, key, direction) => {
|
||||
private sort = (
|
||||
alerts: Alert[],
|
||||
key: string,
|
||||
direction: Direction
|
||||
): Alert[] => {
|
||||
switch (direction) {
|
||||
case 'asc':
|
||||
return _.sortBy(alerts, e => e[key])
|
||||
case 'desc':
|
||||
return _.sortBy(alerts, e => e[key]).reverse()
|
||||
case Direction.ASC:
|
||||
return _.sortBy<Alert>(alerts, e => e[key])
|
||||
case Direction.DESC:
|
||||
return _.sortBy<Alert>(alerts, e => e[key]).reverse()
|
||||
default:
|
||||
return alerts
|
||||
}
|
||||
}
|
||||
|
||||
renderTable() {
|
||||
private renderTable(): JSX.Element {
|
||||
const {
|
||||
source: {id},
|
||||
} = this.props
|
||||
|
@ -176,7 +243,7 @@ class AlertsTable extends Component {
|
|||
)
|
||||
}
|
||||
|
||||
renderTableEmpty() {
|
||||
private renderTableEmpty(): JSX.Element {
|
||||
const {
|
||||
source: {id},
|
||||
shouldNotBeFilterable,
|
||||
|
@ -206,110 +273,6 @@ class AlertsTable extends Component {
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
shouldNotBeFilterable,
|
||||
limit,
|
||||
onGetMoreAlerts,
|
||||
isAlertsMaxedOut,
|
||||
alertsCount,
|
||||
} = this.props
|
||||
|
||||
return shouldNotBeFilterable ? (
|
||||
<div className="alerts-widget">
|
||||
{this.renderTable()}
|
||||
{limit && alertsCount ? (
|
||||
<button
|
||||
className="btn btn-sm btn-default btn-block"
|
||||
onClick={onGetMoreAlerts}
|
||||
disabled={isAlertsMaxedOut}
|
||||
style={{marginBottom: '20px'}}
|
||||
>
|
||||
{isAlertsMaxedOut
|
||||
? `All ${alertsCount} Alerts displayed`
|
||||
: 'Load next 30 Alerts'}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="panel">
|
||||
<div className="panel-heading">
|
||||
<h2 className="panel-title">{this.props.alerts.length} Alerts</h2>
|
||||
{this.props.alerts.length ? (
|
||||
<SearchBar onSearch={this.filterAlerts} />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="panel-body">{this.renderTable()}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class SearchBar extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
searchTerm: '',
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const waitPeriod = 300
|
||||
this.handleSearch = _.debounce(this.handleSearch, waitPeriod)
|
||||
}
|
||||
|
||||
handleSearch = () => {
|
||||
this.props.onSearch(this.state.searchTerm)
|
||||
}
|
||||
|
||||
handleChange = e => {
|
||||
this.setState({searchTerm: e.target.value}, this.handleSearch)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="search-widget" style={{width: '260px'}}>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control input-sm"
|
||||
placeholder="Filter Alerts..."
|
||||
onChange={this.handleChange}
|
||||
value={this.state.searchTerm}
|
||||
/>
|
||||
<span className="icon search" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, bool, func, number, shape, string} = PropTypes
|
||||
|
||||
AlertsTable.propTypes = {
|
||||
alerts: arrayOf(
|
||||
shape({
|
||||
name: string,
|
||||
time: string,
|
||||
value: string,
|
||||
host: string,
|
||||
level: string,
|
||||
})
|
||||
),
|
||||
source: shape({
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
}).isRequired,
|
||||
shouldNotBeFilterable: bool,
|
||||
limit: number,
|
||||
onGetMoreAlerts: func,
|
||||
isAlertsMaxedOut: bool,
|
||||
alertsCount: number,
|
||||
}
|
||||
|
||||
SearchBar.propTypes = {
|
||||
onSearch: func.isRequired,
|
||||
}
|
||||
|
||||
export default AlertsTable
|
|
@ -0,0 +1,52 @@
|
|||
import React, {PureComponent, ChangeEvent} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface Props {
|
||||
onSearch: (s: string) => void
|
||||
}
|
||||
interface State {
|
||||
searchTerm: string
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class SearchBar extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
searchTerm: '',
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillMount() {
|
||||
const waitPeriod = 300
|
||||
this.handleSearch = _.debounce(this.handleSearch, waitPeriod)
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="search-widget" style={{width: '260px'}}>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control input-sm"
|
||||
placeholder="Filter Alerts..."
|
||||
onChange={this.handleChange}
|
||||
value={this.state.searchTerm}
|
||||
/>
|
||||
<span className="icon search" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleSearch = (): void => {
|
||||
this.props.onSearch(this.state.searchTerm)
|
||||
}
|
||||
|
||||
private handleChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({searchTerm: e.target.value}, this.handleSearch)
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchBar
|
|
@ -1,22 +1,41 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, {PureComponent} from 'react'
|
||||
|
||||
import SourceIndicator from 'shared/components/SourceIndicator'
|
||||
import SourceIndicator from 'src/shared/components/SourceIndicator'
|
||||
import AlertsTable from 'src/alerts/components/AlertsTable'
|
||||
import NoKapacitorError from 'shared/components/NoKapacitorError'
|
||||
import CustomTimeRangeDropdown from 'shared/components/CustomTimeRangeDropdown'
|
||||
import NoKapacitorError from 'src/shared/components/NoKapacitorError'
|
||||
import CustomTimeRangeDropdown from 'src/shared/components/CustomTimeRangeDropdown'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
import {getAlerts} from 'src/alerts/apis'
|
||||
import AJAX from 'utils/ajax'
|
||||
import AJAX from 'src/utils/ajax'
|
||||
|
||||
import _ from 'lodash'
|
||||
import moment from 'moment'
|
||||
|
||||
import {timeRanges} from 'shared/data/timeRanges'
|
||||
import {timeRanges} from 'src/shared/data/timeRanges'
|
||||
|
||||
import {Source, TimeRange} from 'src/types'
|
||||
import {Alert} from '../../types/alerts'
|
||||
|
||||
interface Props {
|
||||
source: Source
|
||||
timeRange: TimeRange
|
||||
isWidget: boolean
|
||||
limit: number
|
||||
}
|
||||
|
||||
interface State {
|
||||
loading: boolean
|
||||
hasKapacitor: boolean
|
||||
alerts: Alert[]
|
||||
timeRange: TimeRange
|
||||
limit: number
|
||||
limitMultiplier: number
|
||||
isAlertsMaxedOut: boolean
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class AlertsApp extends Component {
|
||||
class AlertsApp extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
|
@ -43,7 +62,7 @@ class AlertsApp extends Component {
|
|||
}
|
||||
|
||||
// TODO: show a loading screen until we figure out if there is a kapacitor and fetch the alerts
|
||||
componentDidMount() {
|
||||
public componentDidMount() {
|
||||
const {source} = this.props
|
||||
AJAX({
|
||||
url: source.links.kapacitors,
|
||||
|
@ -59,13 +78,49 @@ class AlertsApp extends Component {
|
|||
})
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
public componentDidUpdate(__, prevState) {
|
||||
if (!_.isEqual(prevState.timeRange, this.state.timeRange)) {
|
||||
this.fetchAlerts()
|
||||
}
|
||||
}
|
||||
public render() {
|
||||
const {isWidget, source} = this.props
|
||||
const {loading, timeRange} = this.state
|
||||
|
||||
fetchAlerts = () => {
|
||||
if (loading || !source) {
|
||||
return <div className="page-spinner" />
|
||||
}
|
||||
|
||||
return isWidget ? (
|
||||
this.renderSubComponents()
|
||||
) : (
|
||||
<div className="page alert-history-page">
|
||||
<div className="page-header">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">Alert History</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<SourceIndicator />
|
||||
<CustomTimeRangeDropdown
|
||||
onApplyTimeRange={this.handleApplyTime}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="page-contents">
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-md-12">{this.renderSubComponents()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private fetchAlerts = (): void => {
|
||||
getAlerts(
|
||||
this.props.source.links.proxy,
|
||||
this.state.timeRange,
|
||||
|
@ -112,13 +167,13 @@ class AlertsApp extends Component {
|
|||
})
|
||||
}
|
||||
|
||||
handleGetMoreAlerts = () => {
|
||||
private handleGetMoreAlerts = (): void => {
|
||||
this.setState({limitMultiplier: this.state.limitMultiplier + 1}, () => {
|
||||
this.fetchAlerts(this.state.limitMultiplier)
|
||||
this.fetchAlerts()
|
||||
})
|
||||
}
|
||||
|
||||
renderSubComponents = () => {
|
||||
private renderSubComponents = (): JSX.Element => {
|
||||
const {source, isWidget, limit} = this.props
|
||||
const {isAlertsMaxedOut, alerts} = this.state
|
||||
|
||||
|
@ -137,65 +192,9 @@ class AlertsApp extends Component {
|
|||
)
|
||||
}
|
||||
|
||||
handleApplyTime = timeRange => {
|
||||
private handleApplyTime = (timeRange: TimeRange): void => {
|
||||
this.setState({timeRange})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {isWidget, source} = this.props
|
||||
const {loading, timeRange} = this.state
|
||||
|
||||
if (loading || !source) {
|
||||
return <div className="page-spinner" />
|
||||
}
|
||||
|
||||
return isWidget ? (
|
||||
this.renderSubComponents()
|
||||
) : (
|
||||
<div className="page alert-history-page">
|
||||
<div className="page-header">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">Alert History</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<SourceIndicator />
|
||||
<CustomTimeRangeDropdown
|
||||
onApplyTimeRange={this.handleApplyTime}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="page-contents">
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-md-12">{this.renderSubComponents()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {bool, number, oneOfType, shape, string} = PropTypes
|
||||
|
||||
AlertsApp.propTypes = {
|
||||
source: shape({
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
type: string, // 'influx-enterprise'
|
||||
links: shape({
|
||||
proxy: string.isRequired,
|
||||
}).isRequired,
|
||||
}),
|
||||
timeRange: shape({
|
||||
lower: string.isRequired,
|
||||
upper: oneOfType([shape(), string]),
|
||||
}),
|
||||
isWidget: bool,
|
||||
limit: number,
|
||||
}
|
||||
|
||||
export default AlertsApp
|
|
@ -3,30 +3,16 @@ import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react'
|
|||
import ColorDropdown from 'src/shared/components/ColorDropdown'
|
||||
import {THRESHOLD_COLORS} from 'src/shared/constants/thresholds'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface SelectedColor {
|
||||
hex: string
|
||||
name: string
|
||||
}
|
||||
interface ThresholdProps {
|
||||
type: string
|
||||
hex: string
|
||||
id: string
|
||||
name: string
|
||||
value: number
|
||||
}
|
||||
import {ColorNumber, ThresholdColor} from 'src/types/colors'
|
||||
|
||||
interface Props {
|
||||
visualizationType: string
|
||||
threshold: ThresholdProps
|
||||
threshold: ColorNumber
|
||||
disableMaxColor: boolean
|
||||
onChooseColor: (threshold: ThresholdProps) => void
|
||||
onValidateColorValue: (
|
||||
threshold: ThresholdProps,
|
||||
targetValue: number
|
||||
) => boolean
|
||||
onUpdateColorValue: (threshold: ThresholdProps, targetValue: number) => void
|
||||
onDeleteThreshold: (threshold: ThresholdProps) => void
|
||||
onChooseColor: (threshold: ColorNumber) => void
|
||||
onValidateColorValue: (threshold: ColorNumber, targetValue: number) => boolean
|
||||
onUpdateColorValue: (threshold: ColorNumber, targetValue: number) => void
|
||||
onDeleteThreshold: (threshold: ColorNumber) => void
|
||||
isMin: boolean
|
||||
isMax: boolean
|
||||
}
|
||||
|
@ -50,7 +36,7 @@ class Threshold extends PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const {threshold, disableMaxColor, onChooseColor, isMax} = this.props
|
||||
const {disableMaxColor, isMax} = this.props
|
||||
const {workingValue} = this.state
|
||||
|
||||
return (
|
||||
|
@ -76,18 +62,25 @@ class Threshold extends PureComponent<Props, State> {
|
|||
<ColorDropdown
|
||||
colors={THRESHOLD_COLORS}
|
||||
selected={this.selectedColor}
|
||||
onChoose={onChooseColor(threshold)}
|
||||
onChoose={this.handleChooseColor}
|
||||
disabled={isMax && disableMaxColor}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get selectedColor(): SelectedColor {
|
||||
private handleChooseColor = (color: ThresholdColor): void => {
|
||||
const {onChooseColor, threshold} = this.props
|
||||
const {hex, name} = color
|
||||
|
||||
onChooseColor({...threshold, hex, name})
|
||||
}
|
||||
|
||||
private get selectedColor(): ColorNumber {
|
||||
const {
|
||||
threshold: {hex, name},
|
||||
threshold: {hex, name, type, value, id},
|
||||
} = this.props
|
||||
return {hex, name}
|
||||
return {hex, name, type, value, id}
|
||||
}
|
||||
|
||||
private get inputClass(): string {
|
||||
|
|
|
@ -1,167 +0,0 @@
|
|||
import uuid from 'uuid'
|
||||
|
||||
import {getQueryConfigAndStatus} from 'shared/apis'
|
||||
|
||||
import {errorThrown} from 'shared/actions/errors'
|
||||
|
||||
export const addQuery = (queryID = uuid.v4()) => ({
|
||||
type: 'DE_ADD_QUERY',
|
||||
payload: {
|
||||
queryID,
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteQuery = queryID => ({
|
||||
type: 'DE_DELETE_QUERY',
|
||||
payload: {
|
||||
queryID,
|
||||
},
|
||||
})
|
||||
|
||||
export const toggleField = (queryID, fieldFunc) => ({
|
||||
type: 'DE_TOGGLE_FIELD',
|
||||
payload: {
|
||||
queryID,
|
||||
fieldFunc,
|
||||
},
|
||||
})
|
||||
|
||||
export const groupByTime = (queryID, time) => ({
|
||||
type: 'DE_GROUP_BY_TIME',
|
||||
payload: {
|
||||
queryID,
|
||||
time,
|
||||
},
|
||||
})
|
||||
|
||||
export const fill = (queryID, value) => ({
|
||||
type: 'DE_FILL',
|
||||
payload: {
|
||||
queryID,
|
||||
value,
|
||||
},
|
||||
})
|
||||
|
||||
export const removeFuncs = (queryID, fields, groupBy) => ({
|
||||
type: 'DE_REMOVE_FUNCS',
|
||||
payload: {
|
||||
queryID,
|
||||
fields,
|
||||
groupBy,
|
||||
},
|
||||
})
|
||||
|
||||
export const applyFuncsToField = (queryID, fieldFunc, groupBy) => ({
|
||||
type: 'DE_APPLY_FUNCS_TO_FIELD',
|
||||
payload: {
|
||||
queryID,
|
||||
fieldFunc,
|
||||
groupBy,
|
||||
},
|
||||
})
|
||||
|
||||
export const chooseTag = (queryID, tag) => ({
|
||||
type: 'DE_CHOOSE_TAG',
|
||||
payload: {
|
||||
queryID,
|
||||
tag,
|
||||
},
|
||||
})
|
||||
|
||||
export const chooseNamespace = (queryID, {database, retentionPolicy}) => ({
|
||||
type: 'DE_CHOOSE_NAMESPACE',
|
||||
payload: {
|
||||
queryID,
|
||||
database,
|
||||
retentionPolicy,
|
||||
},
|
||||
})
|
||||
|
||||
export const chooseMeasurement = (queryID, measurement) => ({
|
||||
type: 'DE_CHOOSE_MEASUREMENT',
|
||||
payload: {
|
||||
queryID,
|
||||
measurement,
|
||||
},
|
||||
})
|
||||
|
||||
export const editRawText = (queryID, rawText) => ({
|
||||
type: 'DE_EDIT_RAW_TEXT',
|
||||
payload: {
|
||||
queryID,
|
||||
rawText,
|
||||
},
|
||||
})
|
||||
|
||||
export const setTimeRange = bounds => ({
|
||||
type: 'DE_SET_TIME_RANGE',
|
||||
payload: {
|
||||
bounds,
|
||||
},
|
||||
})
|
||||
|
||||
export const groupByTag = (queryID, tagKey) => ({
|
||||
type: 'DE_GROUP_BY_TAG',
|
||||
payload: {
|
||||
queryID,
|
||||
tagKey,
|
||||
},
|
||||
})
|
||||
|
||||
export const toggleTagAcceptance = queryID => ({
|
||||
type: 'DE_TOGGLE_TAG_ACCEPTANCE',
|
||||
payload: {
|
||||
queryID,
|
||||
},
|
||||
})
|
||||
|
||||
export const updateRawQuery = (queryID, text) => ({
|
||||
type: 'DE_UPDATE_RAW_QUERY',
|
||||
payload: {
|
||||
queryID,
|
||||
text,
|
||||
},
|
||||
})
|
||||
|
||||
export const updateQueryConfig = config => ({
|
||||
type: 'DE_UPDATE_QUERY_CONFIG',
|
||||
payload: {
|
||||
config,
|
||||
},
|
||||
})
|
||||
|
||||
export const addInitialField = (queryID, field, groupBy) => ({
|
||||
type: 'DE_ADD_INITIAL_FIELD',
|
||||
payload: {
|
||||
queryID,
|
||||
field,
|
||||
groupBy,
|
||||
},
|
||||
})
|
||||
|
||||
export const editQueryStatus = (queryID, status) => ({
|
||||
type: 'DE_EDIT_QUERY_STATUS',
|
||||
payload: {
|
||||
queryID,
|
||||
status,
|
||||
},
|
||||
})
|
||||
|
||||
export const timeShift = (queryID, shift) => ({
|
||||
type: 'DE_TIME_SHIFT',
|
||||
payload: {
|
||||
queryID,
|
||||
shift,
|
||||
},
|
||||
})
|
||||
|
||||
// Async actions
|
||||
export const editRawTextAsync = (url, id, text) => async dispatch => {
|
||||
try {
|
||||
const {data} = await getQueryConfigAndStatus(url, [{query: text, id}])
|
||||
const config = data.queries.find(q => q.id === id)
|
||||
dispatch(updateQueryConfig(config.queryConfig))
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,407 @@
|
|||
import uuid from 'uuid'
|
||||
|
||||
import {getQueryConfigAndStatus} from 'src/shared/apis'
|
||||
|
||||
import {errorThrown} from 'src/shared/actions/errors'
|
||||
import {
|
||||
QueryConfig,
|
||||
Status,
|
||||
Field,
|
||||
GroupBy,
|
||||
Tag,
|
||||
TimeRange,
|
||||
TimeShift,
|
||||
ApplyFuncsToFieldArgs,
|
||||
} from 'src/types'
|
||||
|
||||
export type Action =
|
||||
| ActionAddQuery
|
||||
| ActionDeleteQuery
|
||||
| ActionToggleField
|
||||
| ActionGroupByTime
|
||||
| ActionFill
|
||||
| ActionRemoveFuncs
|
||||
| ActionApplyFuncsToField
|
||||
| ActionChooseTag
|
||||
| ActionChooseNamspace
|
||||
| ActionChooseMeasurement
|
||||
| ActionEditRawText
|
||||
| ActionSetTimeRange
|
||||
| ActionGroupByTime
|
||||
| ActionToggleField
|
||||
| ActionUpdateRawQuery
|
||||
| ActionQueryConfig
|
||||
| ActionTimeShift
|
||||
| ActionToggleTagAcceptance
|
||||
| ActionToggleField
|
||||
| ActionGroupByTag
|
||||
| ActionEditQueryStatus
|
||||
| ActionAddInitialField
|
||||
|
||||
export interface ActionAddQuery {
|
||||
type: 'DE_ADD_QUERY'
|
||||
payload: {
|
||||
queryID: string
|
||||
}
|
||||
}
|
||||
|
||||
export const addQuery = (queryID: string = uuid.v4()): ActionAddQuery => ({
|
||||
type: 'DE_ADD_QUERY',
|
||||
payload: {
|
||||
queryID,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionDeleteQuery {
|
||||
type: 'DE_DELETE_QUERY'
|
||||
payload: {
|
||||
queryID: string
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteQuery = (queryID: string): ActionDeleteQuery => ({
|
||||
type: 'DE_DELETE_QUERY',
|
||||
payload: {
|
||||
queryID,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionToggleField {
|
||||
type: 'DE_TOGGLE_FIELD'
|
||||
payload: {
|
||||
queryID: string
|
||||
fieldFunc: Field
|
||||
}
|
||||
}
|
||||
|
||||
export const toggleField = (
|
||||
queryID: string,
|
||||
fieldFunc: Field
|
||||
): ActionToggleField => ({
|
||||
type: 'DE_TOGGLE_FIELD',
|
||||
payload: {
|
||||
queryID,
|
||||
fieldFunc,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionGroupByTime {
|
||||
type: 'DE_GROUP_BY_TIME'
|
||||
payload: {
|
||||
queryID: string
|
||||
time: string
|
||||
}
|
||||
}
|
||||
|
||||
export const groupByTime = (
|
||||
queryID: string,
|
||||
time: string
|
||||
): ActionGroupByTime => ({
|
||||
type: 'DE_GROUP_BY_TIME',
|
||||
payload: {
|
||||
queryID,
|
||||
time,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionFill {
|
||||
type: 'DE_FILL'
|
||||
payload: {
|
||||
queryID: string
|
||||
value: string
|
||||
}
|
||||
}
|
||||
|
||||
export const fill = (queryID: string, value: string): ActionFill => ({
|
||||
type: 'DE_FILL',
|
||||
payload: {
|
||||
queryID,
|
||||
value,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionRemoveFuncs {
|
||||
type: 'DE_REMOVE_FUNCS'
|
||||
payload: {
|
||||
queryID: string
|
||||
fields: Field[]
|
||||
groupBy: GroupBy
|
||||
}
|
||||
}
|
||||
|
||||
export const removeFuncs = (
|
||||
queryID: string,
|
||||
fields: Field[],
|
||||
groupBy: GroupBy
|
||||
): ActionRemoveFuncs => ({
|
||||
type: 'DE_REMOVE_FUNCS',
|
||||
payload: {
|
||||
queryID,
|
||||
fields,
|
||||
groupBy,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionApplyFuncsToField {
|
||||
type: 'DE_APPLY_FUNCS_TO_FIELD'
|
||||
payload: {
|
||||
queryID: string
|
||||
fieldFunc: ApplyFuncsToFieldArgs
|
||||
groupBy: GroupBy
|
||||
}
|
||||
}
|
||||
|
||||
export const applyFuncsToField = (
|
||||
queryID: string,
|
||||
fieldFunc: ApplyFuncsToFieldArgs,
|
||||
groupBy?: GroupBy
|
||||
): ActionApplyFuncsToField => ({
|
||||
type: 'DE_APPLY_FUNCS_TO_FIELD',
|
||||
payload: {
|
||||
queryID,
|
||||
fieldFunc,
|
||||
groupBy,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionChooseTag {
|
||||
type: 'DE_CHOOSE_TAG'
|
||||
payload: {
|
||||
queryID: string
|
||||
tag: Tag
|
||||
}
|
||||
}
|
||||
|
||||
export const chooseTag = (queryID: string, tag: Tag): ActionChooseTag => ({
|
||||
type: 'DE_CHOOSE_TAG',
|
||||
payload: {
|
||||
queryID,
|
||||
tag,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionChooseNamspace {
|
||||
type: 'DE_CHOOSE_NAMESPACE'
|
||||
payload: {
|
||||
queryID: string
|
||||
database: string
|
||||
retentionPolicy: string
|
||||
}
|
||||
}
|
||||
|
||||
interface DBRP {
|
||||
database: string
|
||||
retentionPolicy: string
|
||||
}
|
||||
|
||||
export const chooseNamespace = (
|
||||
queryID: string,
|
||||
{database, retentionPolicy}: DBRP
|
||||
): ActionChooseNamspace => ({
|
||||
type: 'DE_CHOOSE_NAMESPACE',
|
||||
payload: {
|
||||
queryID,
|
||||
database,
|
||||
retentionPolicy,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionChooseMeasurement {
|
||||
type: 'DE_CHOOSE_MEASUREMENT'
|
||||
payload: {
|
||||
queryID: string
|
||||
measurement: string
|
||||
}
|
||||
}
|
||||
|
||||
export const chooseMeasurement = (
|
||||
queryID: string,
|
||||
measurement: string
|
||||
): ActionChooseMeasurement => ({
|
||||
type: 'DE_CHOOSE_MEASUREMENT',
|
||||
payload: {
|
||||
queryID,
|
||||
measurement,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionEditRawText {
|
||||
type: 'DE_EDIT_RAW_TEXT'
|
||||
payload: {
|
||||
queryID: string
|
||||
rawText: string
|
||||
}
|
||||
}
|
||||
|
||||
export const editRawText = (
|
||||
queryID: string,
|
||||
rawText: string
|
||||
): ActionEditRawText => ({
|
||||
type: 'DE_EDIT_RAW_TEXT',
|
||||
payload: {
|
||||
queryID,
|
||||
rawText,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionSetTimeRange {
|
||||
type: 'DE_SET_TIME_RANGE'
|
||||
payload: {
|
||||
bounds: TimeRange
|
||||
}
|
||||
}
|
||||
|
||||
export const setTimeRange = (bounds: TimeRange): ActionSetTimeRange => ({
|
||||
type: 'DE_SET_TIME_RANGE',
|
||||
payload: {
|
||||
bounds,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionGroupByTag {
|
||||
type: 'DE_GROUP_BY_TAG'
|
||||
payload: {
|
||||
queryID: string
|
||||
tagKey: string
|
||||
}
|
||||
}
|
||||
|
||||
export const groupByTag = (
|
||||
queryID: string,
|
||||
tagKey: string
|
||||
): ActionGroupByTag => ({
|
||||
type: 'DE_GROUP_BY_TAG',
|
||||
payload: {
|
||||
queryID,
|
||||
tagKey,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionToggleTagAcceptance {
|
||||
type: 'DE_TOGGLE_TAG_ACCEPTANCE'
|
||||
payload: {
|
||||
queryID: string
|
||||
}
|
||||
}
|
||||
|
||||
export const toggleTagAcceptance = (
|
||||
queryID: string
|
||||
): ActionToggleTagAcceptance => ({
|
||||
type: 'DE_TOGGLE_TAG_ACCEPTANCE',
|
||||
payload: {
|
||||
queryID,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionUpdateRawQuery {
|
||||
type: 'DE_UPDATE_RAW_QUERY'
|
||||
payload: {
|
||||
queryID: string
|
||||
text: string
|
||||
}
|
||||
}
|
||||
|
||||
export const updateRawQuery = (
|
||||
queryID: string,
|
||||
text: string
|
||||
): ActionUpdateRawQuery => ({
|
||||
type: 'DE_UPDATE_RAW_QUERY',
|
||||
payload: {
|
||||
queryID,
|
||||
text,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionQueryConfig {
|
||||
type: 'DE_UPDATE_QUERY_CONFIG'
|
||||
payload: {
|
||||
config: QueryConfig
|
||||
}
|
||||
}
|
||||
|
||||
export const updateQueryConfig = (config: QueryConfig): ActionQueryConfig => ({
|
||||
type: 'DE_UPDATE_QUERY_CONFIG',
|
||||
payload: {
|
||||
config,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionAddInitialField {
|
||||
type: 'DE_ADD_INITIAL_FIELD'
|
||||
payload: {
|
||||
queryID: string
|
||||
field: Field
|
||||
groupBy?: GroupBy
|
||||
}
|
||||
}
|
||||
|
||||
export const addInitialField = (
|
||||
queryID: string,
|
||||
field: Field,
|
||||
groupBy: GroupBy
|
||||
): ActionAddInitialField => ({
|
||||
type: 'DE_ADD_INITIAL_FIELD',
|
||||
payload: {
|
||||
queryID,
|
||||
field,
|
||||
groupBy,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionEditQueryStatus {
|
||||
type: 'DE_EDIT_QUERY_STATUS'
|
||||
payload: {
|
||||
queryID: string
|
||||
status: Status
|
||||
}
|
||||
}
|
||||
|
||||
export const editQueryStatus = (
|
||||
queryID: string,
|
||||
status: Status
|
||||
): ActionEditQueryStatus => ({
|
||||
type: 'DE_EDIT_QUERY_STATUS',
|
||||
payload: {
|
||||
queryID,
|
||||
status,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionTimeShift {
|
||||
type: 'DE_TIME_SHIFT'
|
||||
payload: {
|
||||
queryID: string
|
||||
shift: TimeShift
|
||||
}
|
||||
}
|
||||
|
||||
export const timeShift = (
|
||||
queryID: string,
|
||||
shift: TimeShift
|
||||
): ActionTimeShift => ({
|
||||
type: 'DE_TIME_SHIFT',
|
||||
payload: {
|
||||
queryID,
|
||||
shift,
|
||||
},
|
||||
})
|
||||
|
||||
// Async actions
|
||||
export const editRawTextAsync = (
|
||||
url: string,
|
||||
id: string,
|
||||
text: string
|
||||
) => async (dispatch): Promise<void> => {
|
||||
try {
|
||||
const {data} = await getQueryConfigAndStatus(url, [
|
||||
{
|
||||
query: text,
|
||||
id,
|
||||
},
|
||||
])
|
||||
const config = data.queries.find(q => q.id === id)
|
||||
dispatch(updateQueryConfig(config.queryConfig))
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
}
|
||||
}
|
|
@ -1,13 +1,18 @@
|
|||
import {writeLineProtocol as writeLineProtocolAJAX} from 'src/data_explorer/apis'
|
||||
|
||||
import {notify} from 'shared/actions/notifications'
|
||||
import {notify} from 'src/shared/actions/notifications'
|
||||
import {Source} from 'src/types'
|
||||
|
||||
import {
|
||||
notifyDataWritten,
|
||||
notifyDataWriteFailed,
|
||||
} from 'shared/copy/notifications'
|
||||
} from 'src/shared/copy/notifications'
|
||||
|
||||
export const writeLineProtocolAsync = (source, db, data) => async dispatch => {
|
||||
export const writeLineProtocolAsync = (
|
||||
source: Source,
|
||||
db: string,
|
||||
data: string
|
||||
) => async (dispatch): Promise<void> => {
|
||||
try {
|
||||
await writeLineProtocolAJAX(source, db, data)
|
||||
dispatch(notify(notifyDataWritten()))
|
|
@ -1,8 +0,0 @@
|
|||
import AJAX from 'src/utils/ajax'
|
||||
|
||||
export const writeLineProtocol = async (source, db, data) =>
|
||||
await AJAX({
|
||||
url: `${source.links.write}?db=${db}`,
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
|
@ -0,0 +1,67 @@
|
|||
import AJAX from 'src/utils/ajax'
|
||||
import _ from 'lodash'
|
||||
import moment from 'moment'
|
||||
import download from 'src/external/download'
|
||||
|
||||
import {proxy} from 'src/utils/queryUrlGenerator'
|
||||
import {timeSeriesToTableGraph} from 'src/utils/timeSeriesTransformers'
|
||||
import {dataToCSV} from 'src/shared/parsing/dataToCSV'
|
||||
import {TEMPLATES} from 'src/shared/constants'
|
||||
import {Source, QueryConfig} from 'src/types'
|
||||
|
||||
export const writeLineProtocol = async (
|
||||
source: Source,
|
||||
db: string,
|
||||
data: string
|
||||
): Promise<void> =>
|
||||
await AJAX({
|
||||
url: `${source.links.write}?db=${db}`,
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
|
||||
interface DeprecatedQuery {
|
||||
id: string
|
||||
host: string
|
||||
queryConfig: QueryConfig
|
||||
text: string
|
||||
}
|
||||
|
||||
export const getDataForCSV = (
|
||||
query: DeprecatedQuery,
|
||||
errorThrown
|
||||
) => async () => {
|
||||
try {
|
||||
const response = await fetchTimeSeriesForCSV({
|
||||
source: query.host,
|
||||
query: query.text,
|
||||
tempVars: TEMPLATES,
|
||||
})
|
||||
|
||||
const {data} = timeSeriesToTableGraph([{response}])
|
||||
const name = csvName(query.queryConfig)
|
||||
download(dataToCSV(data), `${name}.csv`, 'text/plain')
|
||||
} catch (error) {
|
||||
errorThrown(error, 'Unable to download .csv file')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchTimeSeriesForCSV = async ({source, query, tempVars}) => {
|
||||
try {
|
||||
const {data} = await proxy({source, query, tempVars})
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const csvName = (query: QueryConfig): string => {
|
||||
const db = _.get(query, 'database', '')
|
||||
const rp = _.get(query, 'retentionPolicy', '')
|
||||
const measurement = _.get(query, 'measurement', '')
|
||||
|
||||
const timestring = moment().format('YYYY-MM-DD-HH-mm')
|
||||
return `${db}.${rp}.${measurement}.${timestring}`
|
||||
}
|
|
@ -1,27 +1,80 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, {PureComponent, KeyboardEvent} from 'react'
|
||||
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
import {QUERY_TEMPLATES} from 'src/data_explorer/constants'
|
||||
import QueryStatus from 'shared/components/QueryStatus'
|
||||
import Dropdown from 'src/shared/components/Dropdown'
|
||||
import {QUERY_TEMPLATES, QueryTemplate} from 'src/data_explorer/constants'
|
||||
import QueryStatus from 'src/shared/components/QueryStatus'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {QueryConfig} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
query: string
|
||||
config: QueryConfig
|
||||
onUpdate: (value: string) => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
value: string
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class QueryEditor extends Component {
|
||||
class QueryEditor extends PureComponent<Props, State> {
|
||||
private editor: React.RefObject<HTMLTextAreaElement>
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
value: this.props.query,
|
||||
}
|
||||
|
||||
this.editor = React.createRef<HTMLTextAreaElement>()
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
if (this.props.query !== nextProps.query) {
|
||||
this.setState({value: nextProps.query})
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown = e => {
|
||||
public render() {
|
||||
const {
|
||||
config: {status},
|
||||
} = this.props
|
||||
const {value} = this.state
|
||||
|
||||
return (
|
||||
<div className="query-editor">
|
||||
<textarea
|
||||
className="query-editor--field"
|
||||
ref={this.editor}
|
||||
value={value}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
onBlur={this.handleUpdate}
|
||||
onChange={this.handleChange}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
data-test="query-editor-field"
|
||||
placeholder="Enter a query or select database, measurement, and field below and have us build one for you..."
|
||||
/>
|
||||
<div className="varmoji">
|
||||
<div className="varmoji-container">
|
||||
<div className="varmoji-front">
|
||||
<QueryStatus status={status}>
|
||||
<Dropdown
|
||||
items={QUERY_TEMPLATES}
|
||||
selected="Query Templates"
|
||||
onChoose={this.handleChooseMetaQuery}
|
||||
className="dropdown-140 query-editor--templates"
|
||||
buttonSize="btn-xs"
|
||||
/>
|
||||
</QueryStatus>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>): void => {
|
||||
const {value} = this.state
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
|
@ -35,64 +88,18 @@ class QueryEditor extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleChange = () => {
|
||||
this.setState({value: this.editor.value})
|
||||
private handleChange = (): void => {
|
||||
const value = this.editor.current.value
|
||||
this.setState({value})
|
||||
}
|
||||
|
||||
handleUpdate = () => {
|
||||
private handleUpdate = (): void => {
|
||||
this.props.onUpdate(this.state.value)
|
||||
}
|
||||
|
||||
handleChooseMetaQuery = template => {
|
||||
private handleChooseMetaQuery = (template: QueryTemplate): void => {
|
||||
this.setState({value: template.query})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
config: {status},
|
||||
} = this.props
|
||||
const {value} = this.state
|
||||
|
||||
return (
|
||||
<div className="query-editor">
|
||||
<textarea
|
||||
className="query-editor--field"
|
||||
onChange={this.handleChange}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onBlur={this.handleUpdate}
|
||||
ref={editor => (this.editor = editor)}
|
||||
value={value}
|
||||
placeholder="Enter a query or select database, measurement, and field below and have us build one for you..."
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
data-test="query-editor-field"
|
||||
/>
|
||||
<div className="varmoji">
|
||||
<div className="varmoji-container">
|
||||
<div className="varmoji-front">
|
||||
<QueryStatus status={status}>
|
||||
<Dropdown
|
||||
items={QUERY_TEMPLATES}
|
||||
selected={'Query Templates'}
|
||||
onChoose={this.handleChooseMetaQuery}
|
||||
className="dropdown-140 query-editor--templates"
|
||||
buttonSize="btn-xs"
|
||||
/>
|
||||
</QueryStatus>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {func, shape, string} = PropTypes
|
||||
|
||||
QueryEditor.propTypes = {
|
||||
query: string,
|
||||
onUpdate: func.isRequired,
|
||||
config: shape().isRequired,
|
||||
}
|
||||
|
||||
export default QueryEditor
|
|
@ -1,216 +0,0 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Dimensions from 'react-dimensions'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {Table, Column, Cell} from 'fixed-data-table'
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
import CustomCell from 'src/data_explorer/components/CustomCell'
|
||||
import TabItem from 'src/data_explorer/components/TableTabItem'
|
||||
import {TEMPLATES} from 'src/shared/constants'
|
||||
|
||||
import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
const emptySeries = {columns: [], values: []}
|
||||
|
||||
@ErrorHandling
|
||||
class ChronoTable extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
series: [emptySeries],
|
||||
columnWidths: {},
|
||||
activeSeriesIndex: 0,
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchCellData(this.props.query)
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.query.text === nextProps.query.text) {
|
||||
return
|
||||
}
|
||||
|
||||
this.fetchCellData(nextProps.query)
|
||||
}
|
||||
|
||||
fetchCellData = async query => {
|
||||
if (!query || !query.text) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({isLoading: true})
|
||||
// second param is db, we want to leave this blank
|
||||
try {
|
||||
const {results} = await fetchTimeSeriesAsync({
|
||||
source: query.host,
|
||||
query,
|
||||
tempVars: TEMPLATES,
|
||||
})
|
||||
this.setState({isLoading: false})
|
||||
|
||||
let series = _.get(results, ['0', 'series'], [])
|
||||
|
||||
if (!series.length) {
|
||||
return this.setState({series: []})
|
||||
}
|
||||
|
||||
series = series.map(s => (s.values ? s : {...s, values: []}))
|
||||
this.setState({series})
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
series: [],
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
handleColumnResize = (newColumnWidth, columnKey) => {
|
||||
const columnWidths = {
|
||||
...this.state.columnWidths,
|
||||
[columnKey]: newColumnWidth,
|
||||
}
|
||||
|
||||
this.setState({
|
||||
columnWidths,
|
||||
})
|
||||
}
|
||||
|
||||
handleClickTab = activeSeriesIndex => () => {
|
||||
this.setState({activeSeriesIndex})
|
||||
}
|
||||
|
||||
handleClickDropdown = item => {
|
||||
this.setState({activeSeriesIndex: item.index})
|
||||
}
|
||||
|
||||
handleCustomCell = (columnName, values, colIndex) => ({rowIndex}) => {
|
||||
return (
|
||||
<CustomCell columnName={columnName} data={values[rowIndex][colIndex]} />
|
||||
)
|
||||
}
|
||||
|
||||
makeTabName = ({name, tags}) => {
|
||||
if (!tags) {
|
||||
return name
|
||||
}
|
||||
const tagKeys = Object.keys(tags).sort()
|
||||
const tagValues = tagKeys.map(key => tags[key]).join('.')
|
||||
return `${name}.${tagValues}`
|
||||
}
|
||||
|
||||
render() {
|
||||
const {containerWidth, height, query} = this.props
|
||||
const {series, columnWidths, isLoading, activeSeriesIndex} = this.state
|
||||
const {columns, values} = _.get(
|
||||
series,
|
||||
[`${activeSeriesIndex}`],
|
||||
emptySeries
|
||||
)
|
||||
|
||||
const maximumTabsCount = 11
|
||||
// adjust height to proper value by subtracting the heights of the UI around it
|
||||
// tab height, graph-container vertical padding, graph-heading height, multitable-header height
|
||||
const minWidth = 70
|
||||
const rowHeight = 34
|
||||
const headerHeight = 30
|
||||
const stylePixelOffset = 130
|
||||
const defaultColumnWidth = 200
|
||||
const styleAdjustedHeight = height - stylePixelOffset
|
||||
const width =
|
||||
columns && columns.length > 1 ? defaultColumnWidth : containerWidth
|
||||
|
||||
if (!query) {
|
||||
return <div className="generic-empty-state">Please add a query below</div>
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="generic-empty-state">Loading...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{width: '100%', height: '100%', position: 'relative'}}>
|
||||
{series.length < maximumTabsCount ? (
|
||||
<div className="table--tabs">
|
||||
{series.map((s, i) => (
|
||||
<TabItem
|
||||
isActive={i === activeSeriesIndex}
|
||||
key={i}
|
||||
name={this.makeTabName(s)}
|
||||
index={i}
|
||||
onClickTab={this.handleClickTab}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Dropdown
|
||||
className="dropdown-160 table--tabs-dropdown"
|
||||
items={series.map((s, index) => ({
|
||||
...s,
|
||||
text: this.makeTabName(s),
|
||||
index,
|
||||
}))}
|
||||
onChoose={this.handleClickDropdown}
|
||||
selected={this.makeTabName(series[activeSeriesIndex])}
|
||||
buttonSize="btn-xs"
|
||||
/>
|
||||
)}
|
||||
<div className="table--tabs-content">
|
||||
{(columns && !columns.length) || (values && !values.length) ? (
|
||||
<div className="generic-empty-state">This series is empty</div>
|
||||
) : (
|
||||
<Table
|
||||
onColumnResizeEndCallback={this.handleColumnResize}
|
||||
isColumnResizing={false}
|
||||
rowHeight={rowHeight}
|
||||
rowsCount={values.length}
|
||||
width={containerWidth}
|
||||
ownerHeight={styleAdjustedHeight}
|
||||
height={styleAdjustedHeight}
|
||||
headerHeight={headerHeight}
|
||||
>
|
||||
{columns.map((columnName, colIndex) => {
|
||||
return (
|
||||
<Column
|
||||
isResizable={true}
|
||||
key={columnName}
|
||||
columnKey={columnName}
|
||||
header={<Cell>{columnName}</Cell>}
|
||||
cell={this.handleCustomCell(columnName, values, colIndex)}
|
||||
width={columnWidths[columnName] || width}
|
||||
minWidth={minWidth}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ChronoTable.defaultProps = {
|
||||
height: 500,
|
||||
}
|
||||
|
||||
const {arrayOf, func, number, shape, string} = PropTypes
|
||||
|
||||
ChronoTable.propTypes = {
|
||||
query: shape({
|
||||
host: arrayOf(string.isRequired).isRequired,
|
||||
text: string.isRequired,
|
||||
id: string.isRequired,
|
||||
}).isRequired,
|
||||
containerWidth: number.isRequired,
|
||||
height: number,
|
||||
editQueryStatus: func.isRequired,
|
||||
}
|
||||
|
||||
export default Dimensions({elementResize: true})(ChronoTable)
|
|
@ -0,0 +1,299 @@
|
|||
import React, {PureComponent, CSSProperties} from 'react'
|
||||
|
||||
import Dimensions from 'react-dimensions'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {Table, Column, Cell} from 'fixed-data-table'
|
||||
import Dropdown from 'src/shared/components/Dropdown'
|
||||
import CustomCell from 'src/data_explorer/components/CustomCell'
|
||||
import TabItem from 'src/data_explorer/components/TableTabItem'
|
||||
import {TEMPLATES} from 'src/shared/constants'
|
||||
|
||||
import {fetchTimeSeriesAsync} from 'src/shared/actions/timeSeries'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {
|
||||
emptySeries,
|
||||
maximumTabsCount,
|
||||
minWidth,
|
||||
rowHeight,
|
||||
headerHeight,
|
||||
stylePixelOffset,
|
||||
defaultColumnWidth,
|
||||
} from 'src/data_explorer/constants/table'
|
||||
|
||||
interface DataExplorerTableQuery {
|
||||
host: string[]
|
||||
text: string
|
||||
id: string
|
||||
}
|
||||
|
||||
interface Series {
|
||||
columns: string[]
|
||||
name: string
|
||||
values: any[]
|
||||
}
|
||||
|
||||
interface ColumnWidths {
|
||||
[key: string]: number
|
||||
}
|
||||
|
||||
interface Props {
|
||||
height: number
|
||||
query: DataExplorerTableQuery
|
||||
editQueryStatus: () => void
|
||||
containerHeight: number
|
||||
containerWidth: number
|
||||
}
|
||||
|
||||
interface State {
|
||||
series: Series[]
|
||||
columnWidths: ColumnWidths
|
||||
activeSeriesIndex: number
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class ChronoTable extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
series: [emptySeries],
|
||||
columnWidths: {},
|
||||
activeSeriesIndex: 0,
|
||||
isLoading: false,
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.fetchCellData(this.props.query)
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps) {
|
||||
if (this.props.query.text === nextProps.query.text) {
|
||||
return
|
||||
}
|
||||
|
||||
this.fetchCellData(nextProps.query)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {query, containerWidth} = this.props
|
||||
const {series, columnWidths, isLoading, activeSeriesIndex} = this.state
|
||||
const {columns, values} = _.get(series, `${activeSeriesIndex}`, emptySeries)
|
||||
|
||||
if (!query) {
|
||||
return (
|
||||
<div className="generic-empty-state"> Please add a query below </div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="generic-empty-state"> Loading... </div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={this.style}>
|
||||
{this.tableSelector}
|
||||
<div className="table--tabs-content">
|
||||
{this.isEmpty ? (
|
||||
<div className="generic-empty-state"> This series is empty </div>
|
||||
) : (
|
||||
<Table
|
||||
isColumnResizing={false}
|
||||
width={containerWidth}
|
||||
rowHeight={rowHeight}
|
||||
height={this.height}
|
||||
ownerHeight={this.height}
|
||||
rowsCount={values.length}
|
||||
headerHeight={headerHeight}
|
||||
onColumnResizeEndCallback={this.handleColumnResize}
|
||||
>
|
||||
{columns.map((columnName, colIndex) => {
|
||||
return (
|
||||
<Column
|
||||
isResizable={true}
|
||||
key={columnName}
|
||||
minWidth={minWidth}
|
||||
columnKey={columnName}
|
||||
header={<Cell> {columnName} </Cell>}
|
||||
width={columnWidths[columnName] || this.columnWidth}
|
||||
cell={this.handleCustomCell(columnName, values, colIndex)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get isEmpty(): boolean {
|
||||
const {columns, values} = this.series
|
||||
return (columns && !columns.length) || (values && !values.length)
|
||||
}
|
||||
|
||||
private get height(): number {
|
||||
return this.props.containerHeight || 500 - stylePixelOffset
|
||||
}
|
||||
|
||||
private get tableSelector() {
|
||||
if (this.isTabbed) {
|
||||
return this.tabs
|
||||
}
|
||||
|
||||
return this.dropdown
|
||||
}
|
||||
|
||||
private get dropdown(): JSX.Element {
|
||||
const {series, activeSeriesIndex} = this.state
|
||||
return (
|
||||
<Dropdown
|
||||
className="dropdown-160 table--tabs-dropdown"
|
||||
items={this.dropdownItems}
|
||||
onChoose={this.handleClickDropdown}
|
||||
selected={this.makeTabName(series[activeSeriesIndex])}
|
||||
buttonSize="btn-xs"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private get dropdownItems(): Series[] {
|
||||
return this.state.series.map((s, index) => ({
|
||||
...s,
|
||||
index,
|
||||
text: this.makeTabName(s),
|
||||
}))
|
||||
}
|
||||
|
||||
private get tabs(): JSX.Element {
|
||||
const {series, activeSeriesIndex} = this.state
|
||||
return (
|
||||
<div className="table--tabs">
|
||||
{series.map((s, i) => (
|
||||
<TabItem
|
||||
key={i}
|
||||
index={i}
|
||||
name={s.name}
|
||||
onClickTab={this.handleClickTab}
|
||||
isActive={i === activeSeriesIndex}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private isTabbed(): boolean {
|
||||
const {series} = this.state
|
||||
|
||||
return series.length < maximumTabsCount
|
||||
}
|
||||
|
||||
private get style(): CSSProperties {
|
||||
return {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
}
|
||||
}
|
||||
|
||||
private get columnWidth(): number {
|
||||
return defaultColumnWidth
|
||||
}
|
||||
|
||||
private get series(): Series {
|
||||
const {activeSeriesIndex} = this.state
|
||||
const {series} = this.state
|
||||
|
||||
return _.get(series, `${activeSeriesIndex}`, emptySeries)
|
||||
}
|
||||
|
||||
private get source(): string {
|
||||
return _.get(this.props.query, 'host.0', '')
|
||||
}
|
||||
|
||||
private fetchCellData = async (query: DataExplorerTableQuery) => {
|
||||
if (!query || !query.text) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isLoading: true,
|
||||
})
|
||||
|
||||
try {
|
||||
const {results} = await fetchTimeSeriesAsync({
|
||||
source: this.source,
|
||||
query: query.text,
|
||||
tempVars: TEMPLATES,
|
||||
})
|
||||
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
let series = _.get(results, ['0', 'series'], [])
|
||||
|
||||
if (!series.length) {
|
||||
return this.setState({series: []})
|
||||
}
|
||||
|
||||
series = series.map(s => {
|
||||
if (s.values) {
|
||||
return s
|
||||
}
|
||||
|
||||
return {...s, values: []}
|
||||
})
|
||||
|
||||
this.setState({series})
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
series: [],
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private handleColumnResize = (
|
||||
newColumnWidth: number,
|
||||
columnKey: string
|
||||
): void => {
|
||||
const columnWidths = {
|
||||
...this.state.columnWidths,
|
||||
[columnKey]: newColumnWidth,
|
||||
}
|
||||
|
||||
this.setState({
|
||||
columnWidths,
|
||||
})
|
||||
}
|
||||
|
||||
private handleClickTab = activeSeriesIndex => {
|
||||
this.setState({
|
||||
activeSeriesIndex,
|
||||
})
|
||||
}
|
||||
|
||||
private handleClickDropdown = item => {
|
||||
this.setState({
|
||||
activeSeriesIndex: item.index,
|
||||
})
|
||||
}
|
||||
|
||||
private handleCustomCell = (columnName, values, colIndex) => ({rowIndex}) => {
|
||||
return (
|
||||
<CustomCell columnName={columnName} data={values[rowIndex][colIndex]} />
|
||||
)
|
||||
}
|
||||
|
||||
private makeTabName = ({name}): string => {
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
export default Dimensions({
|
||||
elementResize: true,
|
||||
})(ChronoTable)
|
|
@ -1,23 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
const TableTabItem = ({name, index, onClickTab, isActive}) => (
|
||||
<div
|
||||
className={classnames('table--tab', {active: isActive})}
|
||||
onClick={onClickTab(index)}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
)
|
||||
|
||||
const {bool, func, number, string} = PropTypes
|
||||
|
||||
TableTabItem.propTypes = {
|
||||
name: string,
|
||||
onClickTab: func.isRequired,
|
||||
index: number.isRequired,
|
||||
isActive: bool,
|
||||
}
|
||||
|
||||
export default TableTabItem
|
|
@ -0,0 +1,31 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
index: number
|
||||
onClickTab: (index: number) => void
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
class TableTabItem extends PureComponent<Props> {
|
||||
public render() {
|
||||
return (
|
||||
<div className={this.className} onClick={this.handleClick}>
|
||||
{this.props.name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleClick = (): void => {
|
||||
this.props.onClickTab(this.props.index)
|
||||
}
|
||||
|
||||
get className(): string {
|
||||
return classnames('table--tab', {
|
||||
active: this.props.isActive,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default TableTabItem
|
|
@ -1,72 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import _ from 'lodash'
|
||||
import moment from 'moment'
|
||||
|
||||
import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries'
|
||||
import {timeSeriesToTableGraph} from 'src/utils/timeSeriesTransformers'
|
||||
import {dataToCSV} from 'src/shared/parsing/dataToCSV'
|
||||
import download from 'src/external/download.js'
|
||||
import {TEMPLATES} from 'src/shared/constants'
|
||||
|
||||
const getDataForCSV = (query, errorThrown) => async () => {
|
||||
try {
|
||||
const response = await fetchTimeSeriesAsync({
|
||||
source: query.host,
|
||||
query,
|
||||
tempVars: TEMPLATES,
|
||||
})
|
||||
const {data} = timeSeriesToTableGraph([{response}])
|
||||
const db = _.get(query, ['queryConfig', 'database'], '')
|
||||
const rp = _.get(query, ['queryConfig', 'retentionPolicy'], '')
|
||||
const measurement = _.get(query, ['queryConfig', 'measurement'], '')
|
||||
|
||||
const timestring = moment().format('YYYY-MM-DD-HH-mm')
|
||||
const name = `${db}.${rp}.${measurement}.${timestring}`
|
||||
download(dataToCSV(data), `${name}.csv`, 'text/plain')
|
||||
} catch (error) {
|
||||
errorThrown(error, 'Unable to download .csv file')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const VisHeader = ({views, view, onToggleView, query, errorThrown}) => (
|
||||
<div className="graph-heading">
|
||||
{views.length ? (
|
||||
<ul className="nav nav-tablist nav-tablist-sm">
|
||||
{views.map(v => (
|
||||
<li
|
||||
key={v}
|
||||
onClick={onToggleView(v)}
|
||||
className={classnames({active: view === v})}
|
||||
data-test={`data-${v}`}
|
||||
>
|
||||
{_.upperFirst(v)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
{query ? (
|
||||
<div
|
||||
className="btn btn-sm btn-default dlcsv"
|
||||
onClick={getDataForCSV(query, errorThrown)}
|
||||
>
|
||||
<span className="icon download dlcsv" />
|
||||
.csv
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
|
||||
VisHeader.propTypes = {
|
||||
views: arrayOf(string).isRequired,
|
||||
view: string.isRequired,
|
||||
onToggleView: func.isRequired,
|
||||
query: shape(),
|
||||
errorThrown: func.isRequired,
|
||||
}
|
||||
|
||||
export default VisHeader
|
|
@ -0,0 +1,42 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import {getDataForCSV} from 'src/data_explorer/apis'
|
||||
import VisHeaderTabs from 'src/data_explorer/components/VisHeaderTabs'
|
||||
import {OnToggleView} from 'src/data_explorer/components/VisHeaderTab'
|
||||
|
||||
interface Props {
|
||||
views: string[]
|
||||
view: string
|
||||
query: any
|
||||
onToggleView: OnToggleView
|
||||
errorThrown: () => void
|
||||
}
|
||||
|
||||
class VisHeader extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {views, view, onToggleView, query, errorThrown} = this.props
|
||||
|
||||
return (
|
||||
<div className="graph-heading">
|
||||
{!!views.length && (
|
||||
<VisHeaderTabs
|
||||
view={view}
|
||||
views={views}
|
||||
currentView={view}
|
||||
onToggleView={onToggleView}
|
||||
/>
|
||||
)}
|
||||
{query && (
|
||||
<div
|
||||
className="btn btn-sm btn-default dlcsv"
|
||||
onClick={getDataForCSV(query, errorThrown)}
|
||||
>
|
||||
<span className="icon download dlcsv" />
|
||||
.csv
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default VisHeader
|
|
@ -0,0 +1,36 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import _ from 'lodash'
|
||||
|
||||
export type OnToggleView = (view: string) => void
|
||||
|
||||
interface TabProps {
|
||||
view: string
|
||||
currentView: string
|
||||
onToggleView: OnToggleView
|
||||
}
|
||||
|
||||
class VisHeaderTab extends PureComponent<TabProps> {
|
||||
public render() {
|
||||
return (
|
||||
<li className={this.className} onClick={this.handleClick}>
|
||||
{this.text}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
private get className(): string {
|
||||
const {view, currentView} = this.props
|
||||
return classnames({active: view === currentView})
|
||||
}
|
||||
|
||||
private handleClick = () => {
|
||||
this.props.onToggleView(this.props.view)
|
||||
}
|
||||
|
||||
private get text(): string {
|
||||
return _.upperFirst(this.props.view)
|
||||
}
|
||||
}
|
||||
|
||||
export default VisHeaderTab
|
|
@ -0,0 +1,28 @@
|
|||
import React, {SFC} from 'react'
|
||||
import VisHeaderTab, {
|
||||
OnToggleView,
|
||||
} from 'src/data_explorer/components/VisHeaderTab'
|
||||
|
||||
interface Props {
|
||||
views: string[]
|
||||
view: string
|
||||
currentView: string
|
||||
onToggleView: OnToggleView
|
||||
}
|
||||
|
||||
const VisHeaderTabs: SFC<Props> = ({views, currentView, onToggleView}) => {
|
||||
return (
|
||||
<ul className="nav nav-tablist nav-tablist-sm">
|
||||
{views.map(v => (
|
||||
<VisHeaderTab
|
||||
key={v}
|
||||
view={v}
|
||||
currentView={currentView}
|
||||
onToggleView={onToggleView}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export default VisHeaderTabs
|
|
@ -1,71 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Table from './Table'
|
||||
import RefreshingGraph from 'shared/components/RefreshingGraph'
|
||||
import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes'
|
||||
|
||||
const VisView = ({
|
||||
axes,
|
||||
view,
|
||||
query,
|
||||
queries,
|
||||
cellType,
|
||||
templates,
|
||||
autoRefresh,
|
||||
heightPixels,
|
||||
manualRefresh,
|
||||
editQueryStatus,
|
||||
resizerBottomHeight,
|
||||
}) => {
|
||||
if (view === 'table') {
|
||||
if (!query) {
|
||||
return (
|
||||
<div className="graph-empty">
|
||||
<p>Build a Query above</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
query={query}
|
||||
height={resizerBottomHeight}
|
||||
editQueryStatus={editQueryStatus}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<RefreshingGraph
|
||||
colors={DEFAULT_LINE_COLORS}
|
||||
axes={axes}
|
||||
type={cellType}
|
||||
queries={queries}
|
||||
templates={templates}
|
||||
cellHeight={heightPixels}
|
||||
autoRefresh={autoRefresh}
|
||||
manualRefresh={manualRefresh}
|
||||
editQueryStatus={editQueryStatus}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const {arrayOf, func, number, shape, string} = PropTypes
|
||||
|
||||
VisView.propTypes = {
|
||||
view: string.isRequired,
|
||||
axes: shape(),
|
||||
query: shape(),
|
||||
queries: arrayOf(shape()).isRequired,
|
||||
cellType: string,
|
||||
templates: arrayOf(shape()),
|
||||
autoRefresh: number.isRequired,
|
||||
heightPixels: number,
|
||||
editQueryStatus: func.isRequired,
|
||||
manualRefresh: number,
|
||||
activeQueryIndex: number,
|
||||
resizerBottomHeight: number,
|
||||
}
|
||||
|
||||
export default VisView
|
|
@ -0,0 +1,53 @@
|
|||
import React, {SFC} from 'react'
|
||||
|
||||
import Table from './Table'
|
||||
import RefreshingGraph from 'src/shared/components/RefreshingGraph'
|
||||
import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes'
|
||||
|
||||
import {QueryConfig, Template} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
view: string
|
||||
query?: QueryConfig
|
||||
queries: QueryConfig[]
|
||||
templates: Template[]
|
||||
autoRefresh: number
|
||||
editQueryStatus: () => void
|
||||
manualRefresh: number
|
||||
}
|
||||
|
||||
const DataExplorerVisView: SFC<Props> = ({
|
||||
view,
|
||||
query,
|
||||
queries,
|
||||
templates,
|
||||
autoRefresh,
|
||||
manualRefresh,
|
||||
editQueryStatus,
|
||||
}) => {
|
||||
if (view === 'table') {
|
||||
if (!query) {
|
||||
return (
|
||||
<div className="graph-empty">
|
||||
<p> Build a Query above </p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return <Table query={query} editQueryStatus={editQueryStatus} />
|
||||
}
|
||||
|
||||
return (
|
||||
<RefreshingGraph
|
||||
type="line-graph"
|
||||
queries={queries}
|
||||
templates={templates}
|
||||
autoRefresh={autoRefresh}
|
||||
colors={DEFAULT_LINE_COLORS}
|
||||
manualRefresh={manualRefresh}
|
||||
editQueryStatus={editQueryStatus}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default DataExplorerVisView
|
|
@ -1,156 +0,0 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import VisHeader from 'src/data_explorer/components/VisHeader'
|
||||
import VisView from 'src/data_explorer/components/VisView'
|
||||
import {GRAPH, TABLE} from 'shared/constants'
|
||||
import buildQueries from 'utils/buildQueriesForGraphs'
|
||||
import _ from 'lodash'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
const META_QUERY_REGEX = /^(show|create|drop)/i
|
||||
|
||||
@ErrorHandling
|
||||
class Visualization extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
const {activeQueryIndex, queryConfigs} = this.props
|
||||
const activeQueryText = this.getQueryText(queryConfigs, activeQueryIndex)
|
||||
|
||||
this.state = activeQueryText.match(META_QUERY_REGEX)
|
||||
? {view: TABLE}
|
||||
: {view: GRAPH}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {activeQueryIndex, queryConfigs} = nextProps
|
||||
const nextQueryText = this.getQueryText(queryConfigs, activeQueryIndex)
|
||||
const queryText = this.getQueryText(
|
||||
this.props.queryConfigs,
|
||||
this.props.activeQueryIndex
|
||||
)
|
||||
|
||||
if (queryText === nextQueryText) {
|
||||
return
|
||||
}
|
||||
|
||||
if (nextQueryText.match(META_QUERY_REGEX)) {
|
||||
return this.setState({view: TABLE})
|
||||
}
|
||||
|
||||
this.setState({view: GRAPH})
|
||||
}
|
||||
|
||||
handleToggleView = view => () => {
|
||||
this.setState({view})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
axes,
|
||||
views,
|
||||
height,
|
||||
cellType,
|
||||
timeRange,
|
||||
templates,
|
||||
autoRefresh,
|
||||
heightPixels,
|
||||
queryConfigs,
|
||||
manualRefresh,
|
||||
editQueryStatus,
|
||||
activeQueryIndex,
|
||||
resizerBottomHeight,
|
||||
errorThrown,
|
||||
} = this.props
|
||||
|
||||
const {
|
||||
source: {
|
||||
links: {proxy},
|
||||
},
|
||||
} = this.context
|
||||
const {view} = this.state
|
||||
|
||||
const queries = buildQueries(proxy, queryConfigs, timeRange)
|
||||
const activeQuery = queries[activeQueryIndex]
|
||||
const defaultQuery = queries[0]
|
||||
const query = activeQuery || defaultQuery
|
||||
|
||||
return (
|
||||
<div className="graph" style={{height}}>
|
||||
<VisHeader
|
||||
view={view}
|
||||
views={views}
|
||||
query={query}
|
||||
errorThrown={errorThrown}
|
||||
onToggleView={this.handleToggleView}
|
||||
/>
|
||||
<div
|
||||
className={classnames({
|
||||
'graph-container': view === GRAPH,
|
||||
'table-container': view === TABLE,
|
||||
})}
|
||||
>
|
||||
<VisView
|
||||
view={view}
|
||||
axes={axes}
|
||||
query={query}
|
||||
queries={queries}
|
||||
cellType={cellType}
|
||||
templates={templates}
|
||||
autoRefresh={autoRefresh}
|
||||
heightPixels={heightPixels}
|
||||
manualRefresh={manualRefresh}
|
||||
editQueryStatus={editQueryStatus}
|
||||
resizerBottomHeight={resizerBottomHeight}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
getQueryText(queryConfigs, index) {
|
||||
// rawText can be null
|
||||
return _.get(queryConfigs, [`${index}`, 'rawText'], '') || ''
|
||||
}
|
||||
}
|
||||
|
||||
Visualization.defaultProps = {
|
||||
cellType: '',
|
||||
}
|
||||
|
||||
const {arrayOf, func, number, shape, string} = PropTypes
|
||||
|
||||
Visualization.contextTypes = {
|
||||
source: shape({
|
||||
links: shape({
|
||||
proxy: string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
}
|
||||
|
||||
Visualization.propTypes = {
|
||||
cellType: string,
|
||||
autoRefresh: number.isRequired,
|
||||
templates: arrayOf(shape()),
|
||||
timeRange: shape({
|
||||
upper: string,
|
||||
lower: string,
|
||||
}).isRequired,
|
||||
queryConfigs: arrayOf(shape({})).isRequired,
|
||||
activeQueryIndex: number,
|
||||
height: string,
|
||||
heightPixels: number,
|
||||
editQueryStatus: func.isRequired,
|
||||
views: arrayOf(string).isRequired,
|
||||
axes: shape({
|
||||
y: shape({
|
||||
bounds: arrayOf(string),
|
||||
}),
|
||||
}),
|
||||
resizerBottomHeight: number,
|
||||
errorThrown: func.isRequired,
|
||||
manualRefresh: number,
|
||||
}
|
||||
|
||||
export default Visualization
|
|
@ -0,0 +1,137 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import _ from 'lodash'
|
||||
|
||||
import VisHeader from 'src/data_explorer/components/VisHeader'
|
||||
import VisView from 'src/data_explorer/components/VisView'
|
||||
|
||||
import {GRAPH, TABLE} from 'src/shared/constants'
|
||||
import buildQueries from 'src/utils/buildQueriesForGraphs'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
import {Source, QueryConfig, Template, TimeRange} from 'src/types'
|
||||
|
||||
const META_QUERY_REGEX = /^(show|create|drop)/i
|
||||
|
||||
interface Props {
|
||||
source: Source
|
||||
views: string[]
|
||||
autoRefresh: number
|
||||
templates: Template[]
|
||||
timeRange: TimeRange
|
||||
queryConfigs: QueryConfig[]
|
||||
activeQueryIndex: number
|
||||
manualRefresh: number
|
||||
editQueryStatus: () => void
|
||||
errorThrown: () => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
view: string
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class DataExplorerVisualization extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = this.initialState
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
const {activeQueryIndex, queryConfigs} = nextProps
|
||||
const nextQueryText = this.getQueryText(queryConfigs, activeQueryIndex)
|
||||
const queryText = this.getQueryText(
|
||||
this.props.queryConfigs,
|
||||
this.props.activeQueryIndex
|
||||
)
|
||||
|
||||
if (queryText === nextQueryText) {
|
||||
return
|
||||
}
|
||||
|
||||
if (nextQueryText.match(META_QUERY_REGEX)) {
|
||||
return this.setState({view: TABLE})
|
||||
}
|
||||
|
||||
this.setState({view: GRAPH})
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
views,
|
||||
templates,
|
||||
autoRefresh,
|
||||
manualRefresh,
|
||||
editQueryStatus,
|
||||
errorThrown,
|
||||
} = this.props
|
||||
|
||||
const {view} = this.state
|
||||
|
||||
return (
|
||||
<div className="graph">
|
||||
<VisHeader
|
||||
view={view}
|
||||
views={views}
|
||||
query={this.query}
|
||||
errorThrown={errorThrown}
|
||||
onToggleView={this.handleToggleView}
|
||||
/>
|
||||
<div className={this.visualizationClass}>
|
||||
<VisView
|
||||
view={view}
|
||||
query={this.query}
|
||||
templates={templates}
|
||||
queries={this.queries}
|
||||
autoRefresh={autoRefresh}
|
||||
manualRefresh={manualRefresh}
|
||||
editQueryStatus={editQueryStatus}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get visualizationClass(): string {
|
||||
const {view} = this.state
|
||||
|
||||
return classnames({
|
||||
'graph-container': view === GRAPH,
|
||||
'table-container': view === TABLE,
|
||||
})
|
||||
}
|
||||
|
||||
private get queries(): QueryConfig[] {
|
||||
const {source, queryConfigs, timeRange} = this.props
|
||||
return buildQueries(source.links.proxy, queryConfigs, timeRange)
|
||||
}
|
||||
|
||||
private get query(): QueryConfig {
|
||||
const {activeQueryIndex} = this.props
|
||||
const activeQuery = this.queries[activeQueryIndex]
|
||||
const defaultQuery = this.queries[0]
|
||||
return activeQuery || defaultQuery
|
||||
}
|
||||
|
||||
private handleToggleView = (view: string): void => {
|
||||
this.setState({view})
|
||||
}
|
||||
|
||||
private getQueryText(queryConfigs: QueryConfig[], index: number): string {
|
||||
// rawText can be null
|
||||
return _.get(queryConfigs, [`${index}`, 'rawText'], '') || ''
|
||||
}
|
||||
|
||||
private get initialState(): {view: string} {
|
||||
const {activeQueryIndex, queryConfigs} = this.props
|
||||
const activeQueryText = this.getQueryText(queryConfigs, activeQueryIndex)
|
||||
|
||||
if (activeQueryText.match(META_QUERY_REGEX)) {
|
||||
return {view: TABLE}
|
||||
}
|
||||
|
||||
return {view: GRAPH}
|
||||
}
|
||||
}
|
||||
|
||||
export default DataExplorerVisualization
|
|
@ -1,105 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import WriteDataFooter from 'src/data_explorer/components/WriteDataFooter'
|
||||
|
||||
const WriteDataBody = ({
|
||||
handleKeyUp,
|
||||
handleCancelFile,
|
||||
handleFile,
|
||||
handleEdit,
|
||||
handleSubmit,
|
||||
inputContent,
|
||||
uploadContent,
|
||||
fileName,
|
||||
isManual,
|
||||
fileInput,
|
||||
handleFileOpen,
|
||||
isUploading,
|
||||
}) => (
|
||||
<div className="write-data-form--body">
|
||||
{isManual ? (
|
||||
<textarea
|
||||
className="form-control write-data-form--input"
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
placeholder="<measurement>,<tag_key>=<tag_value> <field_key>=<field_value>"
|
||||
onKeyUp={handleKeyUp}
|
||||
onChange={handleEdit}
|
||||
autoFocus={true}
|
||||
data-test="manual-entry-field"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={
|
||||
uploadContent
|
||||
? 'write-data-form--file'
|
||||
: 'write-data-form--file write-data-form--file_active'
|
||||
}
|
||||
onClick={handleFileOpen}
|
||||
>
|
||||
{uploadContent ? (
|
||||
<h3 className="write-data-form--filepath_selected">{fileName}</h3>
|
||||
) : (
|
||||
<h3 className="write-data-form--filepath_empty">
|
||||
Drop a file here or click to upload
|
||||
</h3>
|
||||
)}
|
||||
<div
|
||||
className={
|
||||
uploadContent
|
||||
? 'write-data-form--graphic write-data-form--graphic_success'
|
||||
: 'write-data-form--graphic'
|
||||
}
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
onChange={handleFile(false)}
|
||||
className="write-data-form--upload"
|
||||
ref={fileInput}
|
||||
accept="text/*, application/gzip"
|
||||
/>
|
||||
{uploadContent && (
|
||||
<span className="write-data-form--file-submit">
|
||||
<button className="btn btn-md btn-success" onClick={handleSubmit}>
|
||||
Write this File
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-md btn-default"
|
||||
onClick={handleCancelFile}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isManual && (
|
||||
<WriteDataFooter
|
||||
isUploading={isUploading}
|
||||
isManual={isManual}
|
||||
inputContent={inputContent}
|
||||
handleSubmit={handleSubmit}
|
||||
uploadContent={uploadContent}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
const {func, string, bool} = PropTypes
|
||||
|
||||
WriteDataBody.propTypes = {
|
||||
handleKeyUp: func.isRequired,
|
||||
handleEdit: func.isRequired,
|
||||
handleCancelFile: func.isRequired,
|
||||
handleFile: func.isRequired,
|
||||
handleSubmit: func.isRequired,
|
||||
inputContent: string,
|
||||
uploadContent: string,
|
||||
fileName: string,
|
||||
isManual: bool,
|
||||
fileInput: func.isRequired,
|
||||
handleFileOpen: func.isRequired,
|
||||
isUploading: bool.isRequired,
|
||||
}
|
||||
|
||||
export default WriteDataBody
|
|
@ -0,0 +1,162 @@
|
|||
import React, {
|
||||
PureComponent,
|
||||
ChangeEvent,
|
||||
KeyboardEvent,
|
||||
MouseEvent,
|
||||
DragEvent,
|
||||
ReactElement,
|
||||
} from 'react'
|
||||
import WriteDataFooter from 'src/data_explorer/components/WriteDataFooter'
|
||||
|
||||
interface Props {
|
||||
handleCancelFile: (e: MouseEvent<HTMLButtonElement>) => void
|
||||
handleEdit: (e: ChangeEvent<HTMLTextAreaElement>) => void
|
||||
handleKeyUp: (e: KeyboardEvent<HTMLTextAreaElement>) => void
|
||||
handleFile: (drop: boolean) => (e: DragEvent<HTMLInputElement>) => void
|
||||
handleSubmit: (e: MouseEvent<HTMLButtonElement>) => void
|
||||
inputContent: string
|
||||
uploadContent: string
|
||||
fileName: string
|
||||
isManual: boolean
|
||||
fileInput: (ref: any) => any
|
||||
handleFileOpen: () => void
|
||||
isUploading: boolean
|
||||
}
|
||||
|
||||
class WriteDataBody extends PureComponent<Props> {
|
||||
public render() {
|
||||
return (
|
||||
<div className="write-data-form--body">
|
||||
{this.input}
|
||||
{this.footer}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleFile = (e: any): void => {
|
||||
this.props.handleFile(false)(e)
|
||||
}
|
||||
|
||||
private get input(): JSX.Element {
|
||||
const {isManual} = this.props
|
||||
if (isManual) {
|
||||
return this.textarea
|
||||
}
|
||||
|
||||
return this.dragArea
|
||||
}
|
||||
|
||||
private get textarea(): ReactElement<HTMLTextAreaElement> {
|
||||
const {handleKeyUp, handleEdit} = this.props
|
||||
return (
|
||||
<textarea
|
||||
spellCheck={false}
|
||||
autoFocus={true}
|
||||
autoComplete="off"
|
||||
onKeyUp={handleKeyUp}
|
||||
onChange={handleEdit}
|
||||
data-test="manual-entry-field"
|
||||
className="form-control write-data-form--input"
|
||||
placeholder="<measurement>,<tag_key>=<tag_value> <field_key>=<field_value>"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private get dragArea(): ReactElement<HTMLDivElement> {
|
||||
const {fileInput, handleFileOpen} = this.props
|
||||
|
||||
return (
|
||||
<div className={this.dragAreaClass} onClick={handleFileOpen}>
|
||||
{this.dragAreaHeader}
|
||||
<div className={this.infoClass} />
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInput}
|
||||
className="write-data-form--upload"
|
||||
accept="text/*, application/gzip"
|
||||
onChange={this.handleFile}
|
||||
/>
|
||||
{this.buttons}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get dragAreaHeader(): ReactElement<HTMLHeadElement> {
|
||||
const {uploadContent, fileName} = this.props
|
||||
|
||||
if (uploadContent) {
|
||||
return <h3 className="write-data-form--filepath_selected">{fileName}</h3>
|
||||
}
|
||||
|
||||
return (
|
||||
<h3 className="write-data-form--filepath_empty">
|
||||
Drop a file here or click to upload
|
||||
</h3>
|
||||
)
|
||||
}
|
||||
|
||||
private get infoClass(): string {
|
||||
const {uploadContent} = this.props
|
||||
|
||||
if (uploadContent) {
|
||||
return 'write-data-form--graphic write-data-form--graphic_success'
|
||||
}
|
||||
|
||||
return 'write-data-form--graphic'
|
||||
}
|
||||
|
||||
private get buttons(): ReactElement<HTMLSpanElement> | null {
|
||||
const {uploadContent, handleSubmit, handleCancelFile} = this.props
|
||||
|
||||
if (!uploadContent) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="write-data-form--file-submit">
|
||||
<button className="btn btn-md btn-success" onClick={handleSubmit}>
|
||||
Write this File
|
||||
</button>
|
||||
<button className="btn btn-md btn-default" onClick={handleCancelFile}>
|
||||
Cancel
|
||||
</button>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
private get dragAreaClass(): string {
|
||||
const {uploadContent} = this.props
|
||||
|
||||
if (uploadContent) {
|
||||
return 'write-data-form--file'
|
||||
}
|
||||
|
||||
return 'write-data-form--file write-data-form--file_active'
|
||||
}
|
||||
|
||||
private get footer(): JSX.Element | null {
|
||||
const {
|
||||
isUploading,
|
||||
isManual,
|
||||
inputContent,
|
||||
handleSubmit,
|
||||
uploadContent,
|
||||
} = this.props
|
||||
|
||||
if (!isManual) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<WriteDataFooter
|
||||
isUploading={isUploading}
|
||||
isManual={isManual}
|
||||
inputContent={inputContent}
|
||||
handleSubmit={handleSubmit}
|
||||
uploadContent={uploadContent}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default WriteDataBody
|
|
@ -1,60 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const submitButton = 'btn btn-sm btn-success write-data-form--submit'
|
||||
const spinner = 'btn-spinner'
|
||||
|
||||
const WriteDataFooter = ({
|
||||
isManual,
|
||||
inputContent,
|
||||
uploadContent,
|
||||
handleSubmit,
|
||||
isUploading,
|
||||
}) => (
|
||||
<div className="write-data-form--footer">
|
||||
{isManual ? (
|
||||
<span className="write-data-form--helper">
|
||||
Need help writing InfluxDB Line Protocol? -
|
||||
<a
|
||||
href="https://docs.influxdata.com/influxdb/latest/write_protocols/line_protocol_tutorial/"
|
||||
target="_blank"
|
||||
>
|
||||
See Documentation
|
||||
</a>
|
||||
</span>
|
||||
) : (
|
||||
<span className="write-data-form--helper">
|
||||
<a
|
||||
href="https://docs.influxdata.com/influxdb/v1.2//tools/shell/#import-data-from-a-file-with-import"
|
||||
target="_blank"
|
||||
>
|
||||
File Upload Documentation
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
className={isUploading ? `${submitButton} ${spinner}` : submitButton}
|
||||
onClick={handleSubmit}
|
||||
disabled={
|
||||
(!inputContent && isManual) ||
|
||||
(!uploadContent && !isManual) ||
|
||||
isUploading
|
||||
}
|
||||
data-test="write-data-submit-button"
|
||||
>
|
||||
Write
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
const {bool, func, string} = PropTypes
|
||||
|
||||
WriteDataFooter.propTypes = {
|
||||
isManual: bool.isRequired,
|
||||
isUploading: bool.isRequired,
|
||||
uploadContent: string,
|
||||
inputContent: string,
|
||||
handleSubmit: func,
|
||||
}
|
||||
|
||||
export default WriteDataFooter
|
|
@ -0,0 +1,70 @@
|
|||
import React, {PureComponent, MouseEvent} from 'react'
|
||||
import {
|
||||
WRITE_DATA_DOCS_LINK,
|
||||
DATA_IMPORT_DOCS_LINK,
|
||||
} from 'src/data_explorer/constants'
|
||||
|
||||
const submitButton = 'btn btn-sm btn-success write-data-form--submit'
|
||||
const spinner = 'btn-spinner'
|
||||
|
||||
interface Props {
|
||||
isManual: boolean
|
||||
isUploading: boolean
|
||||
uploadContent: string
|
||||
inputContent: string
|
||||
handleSubmit: (e: MouseEvent<HTMLButtonElement>) => void
|
||||
}
|
||||
|
||||
class WriteDataFooter extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {isManual, handleSubmit} = this.props
|
||||
|
||||
return (
|
||||
<div className="write-data-form--footer">
|
||||
{isManual ? (
|
||||
<span className="write-data-form--helper">
|
||||
Need help writing InfluxDB Line Protocol? -
|
||||
<a href={WRITE_DATA_DOCS_LINK} target="_blank">
|
||||
See Documentation
|
||||
</a>
|
||||
</span>
|
||||
) : (
|
||||
<span className="write-data-form--helper">
|
||||
<a href={DATA_IMPORT_DOCS_LINK} target="_blank">
|
||||
File Upload Documentation
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
className={this.buttonClass}
|
||||
onClick={handleSubmit}
|
||||
disabled={this.buttonDisabled}
|
||||
data-test="write-data-submit-button"
|
||||
>
|
||||
Write
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
get buttonDisabled(): boolean {
|
||||
const {inputContent, isManual, uploadContent, isUploading} = this.props
|
||||
return (
|
||||
(!inputContent && isManual) ||
|
||||
(!uploadContent && !isManual) ||
|
||||
isUploading
|
||||
)
|
||||
}
|
||||
|
||||
get buttonClass(): string {
|
||||
const {isUploading} = this.props
|
||||
|
||||
if (isUploading) {
|
||||
return `${submitButton} ${spinner}`
|
||||
}
|
||||
|
||||
return submitButton
|
||||
}
|
||||
}
|
||||
|
||||
export default WriteDataFooter
|
|
@ -1,17 +1,43 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, {
|
||||
PureComponent,
|
||||
DragEvent,
|
||||
ChangeEvent,
|
||||
KeyboardEvent,
|
||||
} from 'react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import OnClickOutside from 'shared/components/OnClickOutside'
|
||||
import OnClickOutside from 'src/shared/components/OnClickOutside'
|
||||
import WriteDataBody from 'src/data_explorer/components/WriteDataBody'
|
||||
import WriteDataHeader from 'src/data_explorer/components/WriteDataHeader'
|
||||
|
||||
import {OVERLAY_TECHNOLOGY} from 'shared/constants/classNames'
|
||||
import {OVERLAY_TECHNOLOGY} from 'src/shared/constants/classNames'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {Source, DropdownItem} from 'src/types'
|
||||
let dragCounter = 0
|
||||
|
||||
interface Props {
|
||||
source: Source
|
||||
selectedDatabase: string
|
||||
onClose: () => void
|
||||
errorThrown: () => void
|
||||
writeLineProtocol: (source: Source, database: string, content: string) => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
selectedDatabase: string
|
||||
inputContent: string | null
|
||||
uploadContent: string
|
||||
fileName: string
|
||||
progress: string
|
||||
isManual: boolean
|
||||
dragClass: string
|
||||
isUploading: boolean
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class WriteDataForm extends Component {
|
||||
class WriteDataForm extends PureComponent<Props, State> {
|
||||
private fileInput: HTMLInputElement
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
|
@ -26,23 +52,52 @@ class WriteDataForm extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
toggleWriteView = isManual => () => {
|
||||
public render() {
|
||||
const {onClose, errorThrown, source} = this.props
|
||||
const {dragClass} = this.state
|
||||
|
||||
return (
|
||||
<div
|
||||
onDrop={this.handleFile(true)}
|
||||
onDragOver={this.handleDragOver}
|
||||
onDragEnter={this.handleDragEnter}
|
||||
onDragExit={this.handleDragLeave}
|
||||
onDragLeave={this.handleDragLeave}
|
||||
className={classnames(OVERLAY_TECHNOLOGY, dragClass)}
|
||||
>
|
||||
<div className="write-data-form">
|
||||
<WriteDataHeader
|
||||
{...this.state}
|
||||
source={source}
|
||||
onClose={onClose}
|
||||
errorThrown={errorThrown}
|
||||
toggleWriteView={this.toggleWriteView}
|
||||
handleSelectDatabase={this.handleSelectDatabase}
|
||||
/>
|
||||
<WriteDataBody
|
||||
{...this.state}
|
||||
fileInput={this.handleFileInputRef}
|
||||
handleEdit={this.handleEdit}
|
||||
handleFile={this.handleFile}
|
||||
handleKeyUp={this.handleKeyUp}
|
||||
handleSubmit={this.handleSubmit}
|
||||
handleFileOpen={this.handleFileOpen}
|
||||
handleCancelFile={this.handleCancelFile}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private toggleWriteView = (isManual: boolean) => {
|
||||
this.setState({isManual})
|
||||
}
|
||||
|
||||
handleSelectDatabase = item => {
|
||||
private handleSelectDatabase = (item: DropdownItem): void => {
|
||||
this.setState({selectedDatabase: item.text})
|
||||
}
|
||||
|
||||
handleClickOutside = e => {
|
||||
// guard against clicking to close error notification
|
||||
if (e.target.className === OVERLAY_TECHNOLOGY) {
|
||||
const {onClose} = this.props
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyUp = e => {
|
||||
private handleKeyUp = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
e.stopPropagation()
|
||||
if (e.key === 'Escape') {
|
||||
const {onClose} = this.props
|
||||
|
@ -50,7 +105,7 @@ class WriteDataForm extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleSubmit = async () => {
|
||||
private handleSubmit = async () => {
|
||||
const {onClose, source, writeLineProtocol} = this.props
|
||||
const {inputContent, uploadContent, selectedDatabase, isManual} = this.state
|
||||
const content = isManual ? inputContent : uploadContent
|
||||
|
@ -67,11 +122,11 @@ class WriteDataForm extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleEdit = e => {
|
||||
private handleEdit = (e: ChangeEvent<HTMLTextAreaElement>): void => {
|
||||
this.setState({inputContent: e.target.value.trim()})
|
||||
}
|
||||
|
||||
handleFile = drop => e => {
|
||||
private handleFile = (drop: boolean) => (e: any): void => {
|
||||
let file
|
||||
if (drop) {
|
||||
file = e.dataTransfer.files[0]
|
||||
|
@ -79,7 +134,7 @@ class WriteDataForm extends Component {
|
|||
dragClass: 'drag-none',
|
||||
})
|
||||
} else {
|
||||
file = e.target.files[0]
|
||||
file = e.currentTarget.files[0]
|
||||
}
|
||||
|
||||
if (!file) {
|
||||
|
@ -99,23 +154,23 @@ class WriteDataForm extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleCancelFile = () => {
|
||||
private handleCancelFile = (): void => {
|
||||
this.setState({uploadContent: ''})
|
||||
this.fileInput.value = ''
|
||||
}
|
||||
|
||||
handleDragOver = e => {
|
||||
private handleDragOver = (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
handleDragEnter = e => {
|
||||
private handleDragEnter = (e: DragEvent<HTMLDivElement>): void => {
|
||||
dragCounter += 1
|
||||
e.preventDefault()
|
||||
this.setState({dragClass: 'drag-over'})
|
||||
}
|
||||
|
||||
handleDragLeave = e => {
|
||||
private handleDragLeave = (e: DragEvent<HTMLDivElement>): void => {
|
||||
dragCounter -= 1
|
||||
e.preventDefault()
|
||||
if (dragCounter === 0) {
|
||||
|
@ -123,67 +178,14 @@ class WriteDataForm extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleFileOpen = () => {
|
||||
private handleFileOpen = (): void => {
|
||||
const {uploadContent} = this.state
|
||||
if (uploadContent === '') {
|
||||
this.fileInput.click()
|
||||
}
|
||||
}
|
||||
|
||||
handleFileInputRef = el => (this.fileInput = el)
|
||||
|
||||
render() {
|
||||
const {onClose, errorThrown, source} = this.props
|
||||
const {dragClass} = this.state
|
||||
|
||||
return (
|
||||
<div
|
||||
onDrop={this.handleFile(true)}
|
||||
onDragOver={this.handleDragOver}
|
||||
onDragEnter={this.handleDragEnter}
|
||||
onDragExit={this.handleDragLeave}
|
||||
onDragLeave={this.handleDragLeave}
|
||||
className={classnames(OVERLAY_TECHNOLOGY, dragClass)}
|
||||
>
|
||||
<div className="write-data-form">
|
||||
<WriteDataHeader
|
||||
{...this.state}
|
||||
source={source}
|
||||
handleSelectDatabase={this.handleSelectDatabase}
|
||||
errorThrown={errorThrown}
|
||||
toggleWriteView={this.toggleWriteView}
|
||||
onClose={onClose}
|
||||
/>
|
||||
<WriteDataBody
|
||||
{...this.state}
|
||||
fileInput={this.handleFileInputRef}
|
||||
handleEdit={this.handleEdit}
|
||||
handleFile={this.handleFile}
|
||||
handleKeyUp={this.handleKeyUp}
|
||||
handleSubmit={this.handleSubmit}
|
||||
handleFileOpen={this.handleFileOpen}
|
||||
handleCancelFile={this.handleCancelFile}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {func, shape, string} = PropTypes
|
||||
|
||||
WriteDataForm.propTypes = {
|
||||
source: shape({
|
||||
links: shape({
|
||||
proxy: string.isRequired,
|
||||
self: string.isRequired,
|
||||
queries: string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
onClose: func.isRequired,
|
||||
writeLineProtocol: func.isRequired,
|
||||
errorThrown: func.isRequired,
|
||||
selectedDatabase: string,
|
||||
private handleFileInputRef = (r: HTMLInputElement) => (this.fileInput = r)
|
||||
}
|
||||
|
||||
export default OnClickOutside(WriteDataForm)
|
|
@ -1,61 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import DatabaseDropdown from 'shared/components/DatabaseDropdown'
|
||||
|
||||
const WriteDataHeader = ({
|
||||
handleSelectDatabase,
|
||||
selectedDatabase,
|
||||
errorThrown,
|
||||
toggleWriteView,
|
||||
isManual,
|
||||
onClose,
|
||||
source,
|
||||
}) => (
|
||||
<div className="write-data-form--header">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">Write Data To</h1>
|
||||
<DatabaseDropdown
|
||||
source={source}
|
||||
onSelectDatabase={handleSelectDatabase}
|
||||
database={selectedDatabase}
|
||||
onErrorThrown={errorThrown}
|
||||
/>
|
||||
<ul className="nav nav-tablist nav-tablist-sm">
|
||||
<li
|
||||
onClick={toggleWriteView(false)}
|
||||
className={isManual ? '' : 'active'}
|
||||
>
|
||||
File Upload
|
||||
</li>
|
||||
<li
|
||||
onClick={toggleWriteView(true)}
|
||||
className={isManual ? 'active' : ''}
|
||||
data-test="manual-entry-button"
|
||||
>
|
||||
Manual Entry
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<span className="page-header__dismiss" onClick={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const {func, shape, string, bool} = PropTypes
|
||||
|
||||
WriteDataHeader.propTypes = {
|
||||
handleSelectDatabase: func.isRequired,
|
||||
selectedDatabase: string,
|
||||
toggleWriteView: func.isRequired,
|
||||
errorThrown: func.isRequired,
|
||||
onClose: func.isRequired,
|
||||
isManual: bool,
|
||||
source: shape({
|
||||
links: shape({
|
||||
proxy: string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
}
|
||||
|
||||
export default WriteDataHeader
|
|
@ -0,0 +1,80 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import DatabaseDropdown from 'src/shared/components/DatabaseDropdown'
|
||||
import {Source, DropdownItem} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
handleSelectDatabase: (item: DropdownItem) => void
|
||||
selectedDatabase: string
|
||||
toggleWriteView: (isWriteViewToggled: boolean) => void
|
||||
errorThrown: () => void
|
||||
onClose: () => void
|
||||
isManual: boolean
|
||||
source: Source
|
||||
}
|
||||
|
||||
class WriteDataHeader extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {
|
||||
handleSelectDatabase,
|
||||
selectedDatabase,
|
||||
errorThrown,
|
||||
onClose,
|
||||
source,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<div className="write-data-form--header">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">Write Data To</h1>
|
||||
<DatabaseDropdown
|
||||
source={source}
|
||||
onSelectDatabase={handleSelectDatabase}
|
||||
database={selectedDatabase}
|
||||
onErrorThrown={errorThrown}
|
||||
/>
|
||||
<ul className="nav nav-tablist nav-tablist-sm">
|
||||
<li onClick={this.handleToggleOff} className={this.fileUploadClass}>
|
||||
File Upload
|
||||
</li>
|
||||
<li
|
||||
onClick={this.handleToggleOn}
|
||||
className={this.manualEntryClass}
|
||||
data-test="manual-entry-button"
|
||||
>
|
||||
Manual Entry
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<span className="page-header__dismiss" onClick={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get fileUploadClass(): string {
|
||||
if (this.props.isManual) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return 'active'
|
||||
}
|
||||
|
||||
private get manualEntryClass(): string {
|
||||
if (this.props.isManual) {
|
||||
return 'active'
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
private handleToggleOff = (): void => {
|
||||
this.props.toggleWriteView(false)
|
||||
}
|
||||
|
||||
private handleToggleOn = (): void => {
|
||||
this.props.toggleWriteView(true)
|
||||
}
|
||||
}
|
||||
|
||||
export default WriteDataHeader
|
|
@ -1,84 +0,0 @@
|
|||
export const INFLUXQL_FUNCTIONS = [
|
||||
'mean',
|
||||
'median',
|
||||
'count',
|
||||
'min',
|
||||
'max',
|
||||
'sum',
|
||||
'first',
|
||||
'last',
|
||||
'spread',
|
||||
'stddev',
|
||||
]
|
||||
|
||||
export const MINIMUM_HEIGHTS = {
|
||||
queryMaker: 350,
|
||||
visualization: 200,
|
||||
}
|
||||
|
||||
export const INITIAL_HEIGHTS = {
|
||||
queryMaker: '66.666%',
|
||||
visualization: '33.334%',
|
||||
}
|
||||
|
||||
const SEPARATOR = 'SEPARATOR'
|
||||
|
||||
export const QUERY_TEMPLATES = [
|
||||
{text: 'Show Databases', query: 'SHOW DATABASES'},
|
||||
{text: 'Create Database', query: 'CREATE DATABASE "db_name"'},
|
||||
{text: 'Drop Database', query: 'DROP DATABASE "db_name"'},
|
||||
{text: `${SEPARATOR}`},
|
||||
{text: 'Show Measurements', query: 'SHOW MEASUREMENTS ON "db_name"'},
|
||||
{
|
||||
text: 'Show Tag Keys',
|
||||
query: 'SHOW TAG KEYS ON "db_name" FROM "measurement_name"',
|
||||
},
|
||||
{
|
||||
text: 'Show Tag Values',
|
||||
query:
|
||||
'SHOW TAG VALUES ON "db_name" FROM "measurement_name" WITH KEY = "tag_key"',
|
||||
},
|
||||
{text: `${SEPARATOR}`},
|
||||
{
|
||||
text: 'Show Retention Policies',
|
||||
query: 'SHOW RETENTION POLICIES on "db_name"',
|
||||
},
|
||||
{
|
||||
text: 'Create Retention Policy',
|
||||
query:
|
||||
'CREATE RETENTION POLICY "rp_name" ON "db_name" DURATION 30d REPLICATION 1 DEFAULT',
|
||||
},
|
||||
{
|
||||
text: 'Drop Retention Policy',
|
||||
query: 'DROP RETENTION POLICY "rp_name" ON "db_name"',
|
||||
},
|
||||
{text: `${SEPARATOR}`},
|
||||
{
|
||||
text: 'Show Continuous Queries',
|
||||
query: 'SHOW CONTINUOUS QUERIES',
|
||||
},
|
||||
{
|
||||
text: 'Create Continuous Query',
|
||||
query:
|
||||
'CREATE CONTINUOUS QUERY "cq_name" ON "db_name" BEGIN SELECT min("field") INTO "target_measurement" FROM "current_measurement" GROUP BY time(30m) END',
|
||||
},
|
||||
{
|
||||
text: 'Drop Continuous Query',
|
||||
query: 'DROP CONTINUOUS QUERY "cq_name" ON "db_name"',
|
||||
},
|
||||
{text: `${SEPARATOR}`},
|
||||
{text: 'Show Users', query: 'SHOW USERS'},
|
||||
{
|
||||
text: 'Create User',
|
||||
query: 'CREATE USER "username" WITH PASSWORD \'password\'',
|
||||
},
|
||||
{
|
||||
text: 'Create Admin User',
|
||||
query:
|
||||
'CREATE USER "username" WITH PASSWORD \'password\' WITH ALL PRIVILEGES',
|
||||
},
|
||||
{text: 'Drop User', query: 'DROP USER "username"'},
|
||||
{text: `${SEPARATOR}`},
|
||||
{text: 'Show Stats', query: 'SHOW STATS'},
|
||||
{text: 'Show Diagnostics', query: 'SHOW DIAGNOSTICS'},
|
||||
]
|
|
@ -0,0 +1,144 @@
|
|||
export const INFLUXQL_FUNCTIONS: string[] = [
|
||||
'mean',
|
||||
'median',
|
||||
'count',
|
||||
'min',
|
||||
'max',
|
||||
'sum',
|
||||
'first',
|
||||
'last',
|
||||
'spread',
|
||||
'stddev',
|
||||
]
|
||||
|
||||
interface MinHeights {
|
||||
queryMaker: number
|
||||
visualization: number
|
||||
}
|
||||
|
||||
export const MINIMUM_HEIGHTS: MinHeights = {
|
||||
queryMaker: 350,
|
||||
visualization: 200,
|
||||
}
|
||||
|
||||
interface InitialHeights {
|
||||
queryMaker: '66.666%'
|
||||
visualization: '33.334%'
|
||||
}
|
||||
|
||||
export const INITIAL_HEIGHTS: InitialHeights = {
|
||||
queryMaker: '66.666%',
|
||||
visualization: '33.334%',
|
||||
}
|
||||
|
||||
const SEPARATOR: string = 'SEPARATOR'
|
||||
|
||||
export interface QueryTemplate {
|
||||
text: string
|
||||
query: string
|
||||
}
|
||||
|
||||
export interface Separator {
|
||||
text: string
|
||||
}
|
||||
|
||||
type Template = QueryTemplate | Separator
|
||||
|
||||
export const QUERY_TEMPLATES: Template[] = [
|
||||
{
|
||||
text: 'Show Databases',
|
||||
query: 'SHOW DATABASES',
|
||||
},
|
||||
{
|
||||
text: 'Create Database',
|
||||
query: 'CREATE DATABASE "db_name"',
|
||||
},
|
||||
{
|
||||
text: 'Drop Database',
|
||||
query: 'DROP DATABASE "db_name"',
|
||||
},
|
||||
{
|
||||
text: `${SEPARATOR}`,
|
||||
},
|
||||
{
|
||||
text: 'Show Measurements',
|
||||
query: 'SHOW MEASUREMENTS ON "db_name"',
|
||||
},
|
||||
{
|
||||
text: 'Show Tag Keys',
|
||||
query: 'SHOW TAG KEYS ON "db_name" FROM "measurement_name"',
|
||||
},
|
||||
{
|
||||
text: 'Show Tag Values',
|
||||
query:
|
||||
'SHOW TAG VALUES ON "db_name" FROM "measurement_name" WITH KEY = "tag_key"',
|
||||
},
|
||||
{
|
||||
text: `${SEPARATOR}`,
|
||||
},
|
||||
{
|
||||
text: 'Show Retention Policies',
|
||||
query: 'SHOW RETENTION POLICIES on "db_name"',
|
||||
},
|
||||
{
|
||||
text: 'Create Retention Policy',
|
||||
query:
|
||||
'CREATE RETENTION POLICY "rp_name" ON "db_name" DURATION 30d REPLICATION 1 DEFAULT',
|
||||
},
|
||||
{
|
||||
text: 'Drop Retention Policy',
|
||||
query: 'DROP RETENTION POLICY "rp_name" ON "db_name"',
|
||||
},
|
||||
{
|
||||
text: `${SEPARATOR}`,
|
||||
},
|
||||
{
|
||||
text: 'Show Continuous Queries',
|
||||
query: 'SHOW CONTINUOUS QUERIES',
|
||||
},
|
||||
{
|
||||
text: 'Create Continuous Query',
|
||||
query:
|
||||
'CREATE CONTINUOUS QUERY "cq_name" ON "db_name" BEGIN SELECT min("field") INTO "target_measurement" FROM "current_measurement" GROUP BY time(30m) END',
|
||||
},
|
||||
{
|
||||
text: 'Drop Continuous Query',
|
||||
query: 'DROP CONTINUOUS QUERY "cq_name" ON "db_name"',
|
||||
},
|
||||
{
|
||||
text: `${SEPARATOR}`,
|
||||
},
|
||||
{
|
||||
text: 'Show Users',
|
||||
query: 'SHOW USERS',
|
||||
},
|
||||
{
|
||||
text: 'Create User',
|
||||
query: 'CREATE USER "username" WITH PASSWORD \'password\'',
|
||||
},
|
||||
{
|
||||
text: 'Create Admin User',
|
||||
query:
|
||||
'CREATE USER "username" WITH PASSWORD \'password\' WITH ALL PRIVILEGES',
|
||||
},
|
||||
{
|
||||
text: 'Drop User',
|
||||
query: 'DROP USER "username"',
|
||||
},
|
||||
{
|
||||
text: `${SEPARATOR}`,
|
||||
},
|
||||
{
|
||||
text: 'Show Stats',
|
||||
query: 'SHOW STATS',
|
||||
},
|
||||
{
|
||||
text: 'Show Diagnostics',
|
||||
query: 'SHOW DIAGNOSTICS',
|
||||
},
|
||||
]
|
||||
|
||||
export const WRITE_DATA_DOCS_LINK =
|
||||
'https://docs.influxdata.com/influxdb/latest/write_protocols/line_protocol_tutorial/'
|
||||
export const DATA_IMPORT_DOCS_LINK =
|
||||
'https://docs.influxdata.com/influxdb/v1.2//tools/shell/#import-data-from-a-file-with-import'
|
|
@ -0,0 +1,9 @@
|
|||
export const emptySeries = {columns: [], values: [], name: ''}
|
||||
export const maximumTabsCount = 11
|
||||
// adjust height to proper value by subtracting the heights of the UI around it
|
||||
// tab height, graph-container vertical padding, graph-heading height, multitable-header height
|
||||
export const minWidth = 70
|
||||
export const rowHeight = 34
|
||||
export const headerHeight = 30
|
||||
export const stylePixelOffset = 130
|
||||
export const defaultColumnWidth = 200
|
|
@ -32,7 +32,7 @@ import {ErrorHandling} from 'src/shared/decorators/errors'
|
|||
interface Props {
|
||||
source: Source
|
||||
queryConfigs: QueryConfig[]
|
||||
queryConfigActions: any // TODO: actually type these
|
||||
queryConfigActions: any
|
||||
autoRefresh: number
|
||||
handleChooseAutoRefresh: () => void
|
||||
router?: InjectedRouter
|
||||
|
@ -72,7 +72,7 @@ export class DataExplorer extends PureComponent<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps) {
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
const {router} = this.props
|
||||
const {queryConfigs, timeRange} = nextProps
|
||||
|
||||
|
@ -138,6 +138,7 @@ export class DataExplorer extends PureComponent<Props, State> {
|
|||
initialGroupByTime={AUTO_GROUP_BY}
|
||||
/>
|
||||
<Visualization
|
||||
source={source}
|
||||
views={VIS_VIEWS}
|
||||
activeQueryIndex={0}
|
||||
timeRange={timeRange}
|
||||
|
@ -161,8 +162,8 @@ export class DataExplorer extends PureComponent<Props, State> {
|
|||
this.setState({showWriteForm: true})
|
||||
}
|
||||
|
||||
private handleChooseTimeRange = (bounds: TimeRange): void => {
|
||||
this.props.setTimeRange(bounds)
|
||||
private handleChooseTimeRange = (timeRange: TimeRange): void => {
|
||||
this.props.setTimeRange(timeRange)
|
||||
}
|
||||
|
||||
private get selectedDatabase(): string {
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {withRouter} from 'react-router'
|
||||
|
||||
import AutoRefreshDropdown from 'shared/components/AutoRefreshDropdown'
|
||||
import TimeRangeDropdown from 'shared/components/TimeRangeDropdown'
|
||||
import SourceIndicator from 'shared/components/SourceIndicator'
|
||||
import GraphTips from 'shared/components/GraphTips'
|
||||
|
||||
const {func, number, shape, string} = PropTypes
|
||||
|
||||
const Header = ({
|
||||
timeRange,
|
||||
autoRefresh,
|
||||
showWriteForm,
|
||||
onManualRefresh,
|
||||
onChooseTimeRange,
|
||||
onChooseAutoRefresh,
|
||||
}) => (
|
||||
<div className="page-header full-width">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">Data Explorer</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<GraphTips />
|
||||
<SourceIndicator />
|
||||
<div
|
||||
className="btn btn-sm btn-default"
|
||||
onClick={showWriteForm}
|
||||
data-test="write-data-button"
|
||||
>
|
||||
<span className="icon pencil" />
|
||||
Write Data
|
||||
</div>
|
||||
<AutoRefreshDropdown
|
||||
iconName="refresh"
|
||||
selected={autoRefresh}
|
||||
onChoose={onChooseAutoRefresh}
|
||||
onManualRefresh={onManualRefresh}
|
||||
/>
|
||||
<TimeRangeDropdown
|
||||
selected={timeRange}
|
||||
page="DataExplorer"
|
||||
onChooseTimeRange={onChooseTimeRange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Header.propTypes = {
|
||||
onChooseAutoRefresh: func.isRequired,
|
||||
onChooseTimeRange: func.isRequired,
|
||||
onManualRefresh: func.isRequired,
|
||||
autoRefresh: number.isRequired,
|
||||
showWriteForm: func.isRequired,
|
||||
timeRange: shape({
|
||||
lower: string,
|
||||
upper: string,
|
||||
}).isRequired,
|
||||
}
|
||||
|
||||
export default withRouter(Header)
|
|
@ -0,0 +1,63 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
|
||||
import AutoRefreshDropdown from 'src/shared/components/AutoRefreshDropdown'
|
||||
import TimeRangeDropdown from 'src/shared/components/TimeRangeDropdown'
|
||||
import SourceIndicator from 'src/shared/components/SourceIndicator'
|
||||
import GraphTips from 'src/shared/components/GraphTips'
|
||||
import {TimeRange} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
onChooseAutoRefresh: () => void
|
||||
onManualRefresh: () => void
|
||||
onChooseTimeRange: (timeRange: TimeRange) => void
|
||||
showWriteForm: () => void
|
||||
autoRefresh: number
|
||||
timeRange: TimeRange
|
||||
}
|
||||
|
||||
class Header extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {
|
||||
timeRange,
|
||||
autoRefresh,
|
||||
showWriteForm,
|
||||
onManualRefresh,
|
||||
onChooseTimeRange,
|
||||
onChooseAutoRefresh,
|
||||
} = this.props
|
||||
return (
|
||||
<div className="page-header full-width">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">Data Explorer</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<GraphTips />
|
||||
<SourceIndicator />
|
||||
<div
|
||||
className="btn btn-sm btn-default"
|
||||
onClick={showWriteForm}
|
||||
data-test="write-data-button"
|
||||
>
|
||||
<span className="icon pencil" />
|
||||
Write Data
|
||||
</div>
|
||||
<AutoRefreshDropdown
|
||||
iconName="refresh"
|
||||
selected={autoRefresh}
|
||||
onChoose={onChooseAutoRefresh}
|
||||
onManualRefresh={onManualRefresh}
|
||||
/>
|
||||
<TimeRangeDropdown
|
||||
selected={timeRange}
|
||||
page="DataExplorer"
|
||||
onChooseTimeRange={onChooseTimeRange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Header
|
|
@ -1,6 +1,9 @@
|
|||
import _ from 'lodash'
|
||||
|
||||
import defaultQueryConfig from 'src/utils/defaultQueryConfig'
|
||||
import {QueryConfig} from 'src/types'
|
||||
import {Action} from 'src/data_explorer/actions/view'
|
||||
|
||||
import {
|
||||
fill,
|
||||
timeShift,
|
||||
|
@ -18,7 +21,11 @@ import {
|
|||
toggleTagAcceptance,
|
||||
} from 'src/utils/queryTransitions'
|
||||
|
||||
const queryConfigs = (state = {}, action) => {
|
||||
interface State {
|
||||
[queryID: string]: Readonly<QueryConfig>
|
||||
}
|
||||
|
||||
const queryConfigs = (state: State = {}, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case 'DE_CHOOSE_NAMESPACE': {
|
||||
const {queryID, database, retentionPolicy} = action.payload
|
||||
|
@ -27,9 +34,7 @@ const queryConfigs = (state = {}, action) => {
|
|||
retentionPolicy,
|
||||
})
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryID]: Object.assign(nextQueryConfig, {rawText: null}),
|
||||
})
|
||||
return {...state, [queryID]: {...nextQueryConfig, rawText: null}}
|
||||
}
|
||||
|
||||
case 'DE_CHOOSE_MEASUREMENT': {
|
||||
|
@ -71,36 +76,31 @@ const queryConfigs = (state = {}, action) => {
|
|||
const {queryID, rawText} = action.payload
|
||||
const nextQueryConfig = editRawText(state[queryID], rawText)
|
||||
|
||||
return Object.assign({}, state, {
|
||||
return {
|
||||
...state,
|
||||
[queryID]: nextQueryConfig,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
case 'DE_GROUP_BY_TIME': {
|
||||
const {queryID, time} = action.payload
|
||||
const nextQueryConfig = groupByTime(state[queryID], time)
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryID]: nextQueryConfig,
|
||||
})
|
||||
return {...state, [queryID]: nextQueryConfig}
|
||||
}
|
||||
|
||||
case 'DE_TOGGLE_TAG_ACCEPTANCE': {
|
||||
const {queryID} = action.payload
|
||||
const nextQueryConfig = toggleTagAcceptance(state[queryID])
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryID]: nextQueryConfig,
|
||||
})
|
||||
return {...state, [queryID]: nextQueryConfig}
|
||||
}
|
||||
|
||||
case 'DE_TOGGLE_FIELD': {
|
||||
const {queryID, fieldFunc} = action.payload
|
||||
const nextQueryConfig = toggleField(state[queryID], fieldFunc)
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryID]: {...nextQueryConfig, rawText: null},
|
||||
})
|
||||
return {...state, [queryID]: {...nextQueryConfig, rawText: null}}
|
||||
}
|
||||
|
||||
case 'DE_APPLY_FUNCS_TO_FIELD': {
|
||||
|
@ -111,26 +111,20 @@ const queryConfigs = (state = {}, action) => {
|
|||
groupBy
|
||||
)
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryID]: nextQueryConfig,
|
||||
})
|
||||
return {...state, [queryID]: nextQueryConfig}
|
||||
}
|
||||
|
||||
case 'DE_CHOOSE_TAG': {
|
||||
const {queryID, tag} = action.payload
|
||||
const nextQueryConfig = chooseTag(state[queryID], tag)
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryID]: nextQueryConfig,
|
||||
})
|
||||
return {...state, [queryID]: nextQueryConfig}
|
||||
}
|
||||
|
||||
case 'DE_GROUP_BY_TAG': {
|
||||
const {queryID, tagKey} = action.payload
|
||||
const nextQueryConfig = groupByTag(state[queryID], tagKey)
|
||||
return Object.assign({}, state, {
|
||||
[queryID]: nextQueryConfig,
|
||||
})
|
||||
return {...state, [queryID]: nextQueryConfig}
|
||||
}
|
||||
|
||||
case 'DE_FILL': {
|
|
@ -1,19 +0,0 @@
|
|||
import {timeRanges} from 'shared/data/timeRanges'
|
||||
|
||||
const {lower, upper} = timeRanges.find(tr => tr.lower === 'now() - 1h')
|
||||
|
||||
const initialState = {
|
||||
upper,
|
||||
lower,
|
||||
}
|
||||
|
||||
export default function timeRange(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case 'DE_SET_TIME_RANGE': {
|
||||
const {bounds} = action.payload
|
||||
|
||||
return {...state, ...bounds}
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import {timeRanges} from 'src/shared/data/timeRanges'
|
||||
import {TimeRange} from 'src/types'
|
||||
|
||||
const {lower, upper} = timeRanges.find(tr => tr.lower === 'now() - 1h')
|
||||
|
||||
const initialState = {
|
||||
upper,
|
||||
lower,
|
||||
}
|
||||
|
||||
type State = Readonly<TimeRange>
|
||||
|
||||
interface ActionSetTimeRange {
|
||||
type: 'DE_SET_TIME_RANGE'
|
||||
payload: {
|
||||
bounds: TimeRange
|
||||
}
|
||||
}
|
||||
|
||||
type Action = ActionSetTimeRange
|
||||
|
||||
const timeRange = (state: State = initialState, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case 'DE_SET_TIME_RANGE': {
|
||||
const {bounds} = action.payload
|
||||
|
||||
return {...state, ...bounds}
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
export default timeRange
|
|
@ -1,8 +1,31 @@
|
|||
interface DataExplorerState {
|
||||
queryIDs: ReadonlyArray<string>
|
||||
}
|
||||
|
||||
interface ActionAddQuery {
|
||||
type: 'DE_ADD_QUERY'
|
||||
payload: {
|
||||
queryID: string
|
||||
}
|
||||
}
|
||||
|
||||
interface ActionDeleteQuery {
|
||||
type: 'DE_DELETE_QUERY'
|
||||
payload: {
|
||||
queryID: string
|
||||
}
|
||||
}
|
||||
|
||||
type Action = ActionAddQuery | ActionDeleteQuery
|
||||
|
||||
const initialState = {
|
||||
queryIDs: [],
|
||||
}
|
||||
|
||||
export default function ui(state = initialState, action) {
|
||||
const ui = (
|
||||
state: DataExplorerState = initialState,
|
||||
action: Action
|
||||
): DataExplorerState => {
|
||||
switch (action.type) {
|
||||
// there is an additional reducer for this same action in the queryConfig reducer
|
||||
case 'DE_ADD_QUERY': {
|
||||
|
@ -27,3 +50,5 @@ export default function ui(state = initialState, action) {
|
|||
|
||||
return state
|
||||
}
|
||||
|
||||
export default ui
|
|
@ -6,7 +6,7 @@ import TimeMachineVis from 'src/ifql/components/TimeMachineVis'
|
|||
import Threesizer from 'src/shared/components/Threesizer'
|
||||
import {Suggestion, OnChangeScript, FlatBody} from 'src/types/ifql'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {HANDLE_VERTICAL, HANDLE_HORIZONTAL} from 'src/shared/constants/index'
|
||||
import {HANDLE_VERTICAL, HANDLE_HORIZONTAL} from 'src/shared/constants'
|
||||
|
||||
interface Props {
|
||||
script: string
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
import {proxy} from 'utils/queryUrlGenerator'
|
||||
import {noop} from 'shared/actions/app'
|
||||
import {proxy} from 'src/utils/queryUrlGenerator'
|
||||
import {noop} from 'src/shared/actions/app'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {errorThrown} from 'shared/actions/errors'
|
||||
import {errorThrown} from 'src/shared/actions/errors'
|
||||
|
||||
export const handleLoading = (query, editQueryStatus) => {
|
||||
editQueryStatus(query.id, {loading: true})
|
||||
editQueryStatus(query.id, {
|
||||
loading: true,
|
||||
})
|
||||
}
|
||||
|
||||
// {results: [{}]}
|
||||
export const handleSuccess = (data, query, editQueryStatus) => {
|
||||
const {results} = data
|
||||
|
@ -22,12 +25,16 @@ export const handleSuccess = (data, query, editQueryStatus) => {
|
|||
|
||||
// 200 from chrono server but influx returns an "error" = warning
|
||||
if (error) {
|
||||
editQueryStatus(query.id, {warn: error})
|
||||
editQueryStatus(query.id, {
|
||||
warn: error,
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
// 200 from server and results contains data = success
|
||||
editQueryStatus(query.id, {success: 'Success!'})
|
||||
editQueryStatus(query.id, {
|
||||
success: 'Success!',
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
|
@ -39,11 +46,21 @@ export const handleError = (error, query, editQueryStatus) => {
|
|||
)
|
||||
|
||||
// 400 from chrono server = fail
|
||||
editQueryStatus(query.id, {error: message})
|
||||
editQueryStatus(query.id, {
|
||||
error: message,
|
||||
})
|
||||
}
|
||||
|
||||
interface Payload {
|
||||
source: string
|
||||
query: string
|
||||
tempVars: any[]
|
||||
db?: string
|
||||
rp?: string
|
||||
resolution?: number
|
||||
}
|
||||
export const fetchTimeSeriesAsync = async (
|
||||
{source, db, rp, query, tempVars, resolution},
|
||||
{source, db, rp, query, tempVars, resolution}: Payload,
|
||||
editQueryStatus = noop
|
||||
) => {
|
||||
handleLoading(query, editQueryStatus)
|
||||
|
@ -52,7 +69,7 @@ export const fetchTimeSeriesAsync = async (
|
|||
source,
|
||||
db,
|
||||
rp,
|
||||
query: query.text,
|
||||
query,
|
||||
tempVars,
|
||||
resolution,
|
||||
})
|
|
@ -31,14 +31,22 @@ interface Query {
|
|||
rp: string
|
||||
}
|
||||
|
||||
const parseSource = source => {
|
||||
if (Array.isArray(source)) {
|
||||
return _.get(source, '0', '')
|
||||
}
|
||||
|
||||
return source
|
||||
}
|
||||
|
||||
export const fetchTimeSeries = async (
|
||||
queries: Query[],
|
||||
resolution: number,
|
||||
templates: Template[],
|
||||
editQueryStatus: () => void
|
||||
editQueryStatus: () => any
|
||||
) => {
|
||||
const timeSeriesPromises = queries.map(query => {
|
||||
const {host, database, rp} = query
|
||||
const {host, database, rp, text} = query
|
||||
// the key `database` was used upstream in HostPage.js, and since as of this writing
|
||||
// the codebase has not been fully converted to TypeScript, it's not clear where else
|
||||
// it may be used, but this slight modification is intended to allow for the use of
|
||||
|
@ -63,11 +71,9 @@ export const fetchTimeSeries = async (
|
|||
|
||||
const tempVars = removeUnselectedTemplateValues(templatesWithIntervalVals)
|
||||
|
||||
const source = host
|
||||
return fetchTimeSeriesAsync(
|
||||
{source, db, rp, query, tempVars, resolution},
|
||||
editQueryStatus
|
||||
)
|
||||
const source = parseSource(host)
|
||||
const payload = {source, db, rp, query: text, tempVars, resolution}
|
||||
return fetchTimeSeriesAsync(payload, editQueryStatus)
|
||||
})
|
||||
|
||||
return Promise.all(timeSeriesPromises)
|
||||
|
|
|
@ -1,20 +1,28 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, {Component, MouseEvent, ChangeEvent} from 'react'
|
||||
|
||||
import onClickOutside from 'shared/components/OnClickOutside'
|
||||
import onClickOutside from 'src/shared/components/OnClickOutside'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface Props {
|
||||
min: string
|
||||
id: string
|
||||
type: string
|
||||
customPlaceholder: string
|
||||
customValue: string
|
||||
onGetRef: (el: HTMLInputElement) => void
|
||||
onFocus: () => void
|
||||
onChange: (e: ChangeEvent<HTMLInputElement>) => void
|
||||
onKeyDown: () => void
|
||||
handleClickOutsideInput: (e: MouseEvent<HTMLElement>) => void
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class ClickOutsideInput extends Component {
|
||||
class ClickOutsideInput extends Component<Props> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
handleClickOutside = e => {
|
||||
this.props.handleClickOutsideInput(e)
|
||||
}
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
const {
|
||||
id,
|
||||
min,
|
||||
|
@ -43,21 +51,10 @@ class ClickOutsideInput extends Component {
|
|||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {func, string} = PropTypes
|
||||
|
||||
ClickOutsideInput.propTypes = {
|
||||
min: string,
|
||||
id: string.isRequired,
|
||||
type: string.isRequired,
|
||||
customPlaceholder: string.isRequired,
|
||||
customValue: string.isRequired,
|
||||
onGetRef: func.isRequired,
|
||||
onFocus: func.isRequired,
|
||||
onChange: func.isRequired,
|
||||
onKeyDown: func.isRequired,
|
||||
handleClickOutsideInput: func.isRequired,
|
||||
public handleClickOutside = (e): void => {
|
||||
this.props.handleClickOutsideInput(e)
|
||||
}
|
||||
}
|
||||
|
||||
export default onClickOutside(ClickOutsideInput)
|
|
@ -1,110 +0,0 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import classnames from 'classnames'
|
||||
import OnClickOutside from 'shared/components/OnClickOutside'
|
||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
@ErrorHandling
|
||||
class ColorDropdown extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
visible: false,
|
||||
}
|
||||
}
|
||||
|
||||
handleToggleMenu = () => {
|
||||
const {disabled} = this.props
|
||||
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
this.setState({visible: !this.state.visible})
|
||||
}
|
||||
|
||||
handleClickOutside = () => {
|
||||
this.setState({visible: false})
|
||||
}
|
||||
|
||||
handleColorClick = color => () => {
|
||||
this.props.onChoose(color)
|
||||
this.setState({visible: false})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {visible} = this.state
|
||||
const {colors, selected, disabled, stretchToFit} = this.props
|
||||
|
||||
const dropdownClassNames = classnames('color-dropdown', {
|
||||
open: visible,
|
||||
'color-dropdown--stretch': stretchToFit,
|
||||
})
|
||||
const toggleClassNames = classnames(
|
||||
'btn btn-sm btn-default color-dropdown--toggle',
|
||||
{active: visible, 'color-dropdown__disabled': disabled}
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={dropdownClassNames}>
|
||||
<div
|
||||
className={toggleClassNames}
|
||||
onClick={this.handleToggleMenu}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div
|
||||
className="color-dropdown--swatch"
|
||||
style={{backgroundColor: selected.hex}}
|
||||
/>
|
||||
<div className="color-dropdown--name">{selected.name}</div>
|
||||
<span className="caret" />
|
||||
</div>
|
||||
{visible ? (
|
||||
<div className="color-dropdown--menu">
|
||||
<FancyScrollbar autoHide={false} autoHeight={true}>
|
||||
{colors.map((color, i) => (
|
||||
<div
|
||||
className={
|
||||
color.name === selected.name
|
||||
? 'color-dropdown--item active'
|
||||
: 'color-dropdown--item'
|
||||
}
|
||||
key={i}
|
||||
onClick={this.handleColorClick(color)}
|
||||
>
|
||||
<span
|
||||
className="color-dropdown--swatch"
|
||||
style={{backgroundColor: color.hex}}
|
||||
/>
|
||||
<span className="color-dropdown--name">{color.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</FancyScrollbar>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||
|
||||
ColorDropdown.propTypes = {
|
||||
selected: shape({
|
||||
hex: string.isRequired,
|
||||
name: string.isRequired,
|
||||
}).isRequired,
|
||||
onChoose: func.isRequired,
|
||||
colors: arrayOf(
|
||||
shape({
|
||||
hex: string.isRequired,
|
||||
name: string.isRequired,
|
||||
}).isRequired
|
||||
).isRequired,
|
||||
stretchToFit: bool,
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
export default OnClickOutside(ColorDropdown)
|
|
@ -0,0 +1,130 @@
|
|||
import React, {Component} from 'react'
|
||||
|
||||
import classnames from 'classnames'
|
||||
import {ClickOutside} from 'src/shared/components/ClickOutside'
|
||||
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {ColorNumber, ThresholdColor} from 'src/types/colors'
|
||||
import {DROPDOWN_MENU_MAX_HEIGHT} from 'src/shared/constants/index'
|
||||
|
||||
interface Props {
|
||||
selected: ColorNumber
|
||||
disabled?: boolean
|
||||
stretchToFit?: boolean
|
||||
colors: ThresholdColor[]
|
||||
onChoose: (colors: ThresholdColor) => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
export default class ColorDropdown extends Component<Props, State> {
|
||||
public static defaultProps: Partial<Props> = {
|
||||
stretchToFit: false,
|
||||
disabled: false,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
visible: false,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {visible} = this.state
|
||||
const {selected} = this.props
|
||||
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.handleClickOutside}>
|
||||
<div className={this.dropdownClassNames}>
|
||||
<div
|
||||
className={this.buttonClassNames}
|
||||
onClick={this.handleToggleMenu}
|
||||
>
|
||||
<div
|
||||
className="color-dropdown--swatch"
|
||||
style={{backgroundColor: selected.hex}}
|
||||
/>
|
||||
<div className="color-dropdown--name">{selected.name}</div>
|
||||
<span className="caret" />
|
||||
</div>
|
||||
{visible && this.renderMenu}
|
||||
</div>
|
||||
</ClickOutside>
|
||||
)
|
||||
}
|
||||
|
||||
private get dropdownClassNames(): string {
|
||||
const {stretchToFit} = this.props
|
||||
const {visible} = this.state
|
||||
|
||||
return classnames('color-dropdown', {
|
||||
open: visible,
|
||||
'color-dropdown--stretch': stretchToFit,
|
||||
})
|
||||
}
|
||||
|
||||
private get buttonClassNames(): string {
|
||||
const {disabled} = this.props
|
||||
const {visible} = this.state
|
||||
|
||||
return classnames('btn btn-sm btn-default color-dropdown--toggle', {
|
||||
active: visible,
|
||||
'color-dropdown__disabled': disabled,
|
||||
})
|
||||
}
|
||||
|
||||
private get renderMenu(): JSX.Element {
|
||||
const {colors, selected} = this.props
|
||||
|
||||
return (
|
||||
<div className="color-dropdown--menu">
|
||||
<FancyScrollbar
|
||||
autoHide={false}
|
||||
autoHeight={true}
|
||||
maxHeight={DROPDOWN_MENU_MAX_HEIGHT}
|
||||
>
|
||||
{colors.map((color, i) => (
|
||||
<div
|
||||
className={
|
||||
color.name === selected.name
|
||||
? 'color-dropdown--item active'
|
||||
: 'color-dropdown--item'
|
||||
}
|
||||
key={i}
|
||||
onClick={this.handleColorClick(color)}
|
||||
>
|
||||
<span
|
||||
className="color-dropdown--swatch"
|
||||
style={{backgroundColor: color.hex}}
|
||||
/>
|
||||
<span className="color-dropdown--name">{color.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</FancyScrollbar>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleToggleMenu = (): void => {
|
||||
const {disabled} = this.props
|
||||
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
this.setState({visible: !this.state.visible})
|
||||
}
|
||||
|
||||
private handleClickOutside = (): void => {
|
||||
this.setState({visible: false})
|
||||
}
|
||||
|
||||
private handleColorClick = color => (): void => {
|
||||
this.props.onChoose(color)
|
||||
this.setState({visible: false})
|
||||
}
|
||||
}
|
|
@ -1,119 +0,0 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import uuid from 'uuid'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import OnClickOutside from 'shared/components/OnClickOutside'
|
||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||
|
||||
import {LINE_COLOR_SCALES} from 'src/shared/constants/graphColorPalettes'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
@ErrorHandling
|
||||
class ColorScaleDropdown extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
expanded: false,
|
||||
}
|
||||
}
|
||||
|
||||
handleToggleMenu = () => {
|
||||
const {disabled} = this.props
|
||||
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
this.setState({expanded: !this.state.expanded})
|
||||
}
|
||||
|
||||
handleClickOutside = () => {
|
||||
this.setState({expanded: false})
|
||||
}
|
||||
|
||||
handleDropdownClick = colorScale => () => {
|
||||
this.props.onChoose(colorScale)
|
||||
this.setState({expanded: false})
|
||||
}
|
||||
|
||||
generateGradientStyle = colors => ({
|
||||
background: `linear-gradient(to right, ${colors[0].hex} 0%,${
|
||||
colors[1].hex
|
||||
} 50%,${colors[2].hex} 100%)`,
|
||||
})
|
||||
|
||||
render() {
|
||||
const {expanded} = this.state
|
||||
const {selected, disabled, stretchToFit} = this.props
|
||||
|
||||
const dropdownClassNames = classnames('color-dropdown', {
|
||||
open: expanded,
|
||||
'color-dropdown--stretch': stretchToFit,
|
||||
})
|
||||
const toggleClassNames = classnames(
|
||||
'btn btn-sm btn-default color-dropdown--toggle',
|
||||
{active: expanded, 'color-dropdown__disabled': disabled}
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={dropdownClassNames}>
|
||||
<div
|
||||
className={toggleClassNames}
|
||||
onClick={this.handleToggleMenu}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div
|
||||
className="color-dropdown--swatches"
|
||||
style={this.generateGradientStyle(selected)}
|
||||
/>
|
||||
<div className="color-dropdown--name">{selected[0].name}</div>
|
||||
<span className="caret" />
|
||||
</div>
|
||||
{expanded ? (
|
||||
<div className="color-dropdown--menu">
|
||||
<FancyScrollbar autoHide={false} autoHeight={true}>
|
||||
{LINE_COLOR_SCALES.map(colorScale => (
|
||||
<div
|
||||
className={
|
||||
colorScale.name === selected[0].name
|
||||
? 'color-dropdown--item active'
|
||||
: 'color-dropdown--item'
|
||||
}
|
||||
key={uuid.v4()}
|
||||
onClick={this.handleDropdownClick(colorScale)}
|
||||
>
|
||||
<div
|
||||
className="color-dropdown--swatches"
|
||||
style={this.generateGradientStyle(colorScale.colors)}
|
||||
/>
|
||||
<span className="color-dropdown--name">
|
||||
{colorScale.name}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</FancyScrollbar>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||
|
||||
ColorScaleDropdown.propTypes = {
|
||||
selected: arrayOf(
|
||||
shape({
|
||||
type: string.isRequired,
|
||||
hex: string.isRequired,
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
}).isRequired
|
||||
).isRequired,
|
||||
onChoose: func.isRequired,
|
||||
stretchToFit: bool,
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
export default OnClickOutside(ColorScaleDropdown)
|
|
@ -0,0 +1,130 @@
|
|||
import React, {Component, CSSProperties} from 'react'
|
||||
import uuid from 'uuid'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import {ClickOutside} from 'src/shared/components/ClickOutside'
|
||||
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
||||
|
||||
import {ColorNumber} from 'src/types/colors'
|
||||
import {LINE_COLOR_SCALES} from 'src/shared/constants/graphColorPalettes'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface Props {
|
||||
onChoose: (colors: ColorNumber[]) => void
|
||||
stretchToFit?: boolean
|
||||
disabled?: boolean
|
||||
selected: ColorNumber[]
|
||||
}
|
||||
|
||||
interface State {
|
||||
expanded: boolean
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
export default class ColorScaleDropdown extends Component<Props, State> {
|
||||
public static defaultProps: Partial<Props> = {
|
||||
disabled: false,
|
||||
stretchToFit: false,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
expanded: false,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {expanded} = this.state
|
||||
const {selected} = this.props
|
||||
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.handleClickOutside}>
|
||||
<div className={this.dropdownClassName}>
|
||||
<div className={this.buttonClassName} onClick={this.handleToggleMenu}>
|
||||
<div
|
||||
className="color-dropdown--swatches"
|
||||
style={this.generateGradientStyle(selected)}
|
||||
/>
|
||||
<div className="color-dropdown--name">{selected[0].name}</div>
|
||||
<span className="caret" />
|
||||
</div>
|
||||
{expanded && this.renderMenu}
|
||||
</div>
|
||||
</ClickOutside>
|
||||
)
|
||||
}
|
||||
|
||||
private get renderMenu(): JSX.Element {
|
||||
const {selected} = this.props
|
||||
|
||||
return (
|
||||
<div className="color-dropdown--menu">
|
||||
<FancyScrollbar autoHide={false} autoHeight={true}>
|
||||
{LINE_COLOR_SCALES.map(colorScale => (
|
||||
<div
|
||||
className={
|
||||
colorScale.name === selected[0].name
|
||||
? 'color-dropdown--item active'
|
||||
: 'color-dropdown--item'
|
||||
}
|
||||
key={uuid.v4()}
|
||||
onClick={this.handleDropdownClick(colorScale)}
|
||||
>
|
||||
<div
|
||||
className="color-dropdown--swatches"
|
||||
style={this.generateGradientStyle(colorScale.colors)}
|
||||
/>
|
||||
<span className="color-dropdown--name">{colorScale.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</FancyScrollbar>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get buttonClassName(): string {
|
||||
const {disabled} = this.props
|
||||
const {expanded} = this.state
|
||||
|
||||
return classnames('btn btn-sm btn-default color-dropdown--toggle', {
|
||||
active: expanded,
|
||||
'color-dropdown__disabled': disabled,
|
||||
})
|
||||
}
|
||||
|
||||
private get dropdownClassName(): string {
|
||||
const {stretchToFit} = this.props
|
||||
const {expanded} = this.state
|
||||
|
||||
return classnames('color-dropdown', {
|
||||
open: expanded,
|
||||
'color-dropdown--stretch': stretchToFit,
|
||||
})
|
||||
}
|
||||
|
||||
private handleToggleMenu = (): void => {
|
||||
const {disabled} = this.props
|
||||
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
this.setState({expanded: !this.state.expanded})
|
||||
}
|
||||
|
||||
private handleClickOutside = (): void => {
|
||||
this.setState({expanded: false})
|
||||
}
|
||||
|
||||
private handleDropdownClick = colorScale => (): void => {
|
||||
this.props.onChoose(colorScale)
|
||||
this.setState({expanded: false})
|
||||
}
|
||||
|
||||
private generateGradientStyle = (colors): CSSProperties => ({
|
||||
background: `linear-gradient(to right, ${colors[0].hex} 0%,${
|
||||
colors[1].hex
|
||||
} 50%,${colors[2].hex} 100%)`,
|
||||
})
|
||||
}
|
|
@ -1,10 +1,17 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, {SFC} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {TEMP_VAR_DASHBOARD_TIME} from 'src/shared/constants'
|
||||
|
||||
const CustomTimeIndicator = ({queries}) => {
|
||||
interface Query {
|
||||
query: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
queries: Query[]
|
||||
}
|
||||
|
||||
const CustomTimeIndicator: SFC<Props> = ({queries}) => {
|
||||
const q = queries.find(({query}) => !query.includes(TEMP_VAR_DASHBOARD_TIME))
|
||||
const customLower = _.get(q, ['queryConfig', 'range', 'lower'], null)
|
||||
const customUpper = _.get(q, ['queryConfig', 'range', 'upper'], null)
|
||||
|
@ -20,10 +27,4 @@ const CustomTimeIndicator = ({queries}) => {
|
|||
return <span className="custom-indicator">{customTimeRange}</span>
|
||||
}
|
||||
|
||||
const {arrayOf, shape} = PropTypes
|
||||
|
||||
CustomTimeIndicator.propTypes = {
|
||||
queries: arrayOf(shape()),
|
||||
}
|
||||
|
||||
export default CustomTimeIndicator
|
|
@ -5,17 +5,15 @@ import classnames from 'classnames'
|
|||
import {ClickOutside} from 'src/shared/components/ClickOutside'
|
||||
import CustomTimeRange from 'src/shared/components/CustomTimeRange'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {TimeRange} from 'src/types'
|
||||
|
||||
interface State {
|
||||
expanded: boolean
|
||||
}
|
||||
|
||||
interface Props {
|
||||
timeRange: {
|
||||
upper?: string
|
||||
lower: string
|
||||
}
|
||||
onApplyTimeRange: () => void
|
||||
timeRange: TimeRange
|
||||
onApplyTimeRange: (tr: TimeRange) => void
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
|
|
|
@ -1,26 +1,43 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
import Dropdown from 'src/shared/components/Dropdown'
|
||||
|
||||
import {showDatabases} from 'shared/apis/metaQuery'
|
||||
import parsers from 'shared/parsing'
|
||||
import {showDatabases} from 'src/shared/apis/metaQuery'
|
||||
import parsers from 'src/shared/parsing'
|
||||
import {Source} from 'src/types/sources'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
const {databases: showDatabasesParser} = parsers
|
||||
|
||||
interface Database {
|
||||
text: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
database: string
|
||||
onSelectDatabase: (database: Database) => void
|
||||
onStartEdit?: () => void
|
||||
onErrorThrown: (error: string) => void
|
||||
source: Source
|
||||
}
|
||||
|
||||
interface State {
|
||||
databases: Database[]
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class DatabaseDropdown extends Component {
|
||||
class DatabaseDropdown extends Component<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
databases: [],
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._getDatabases()
|
||||
public componentDidMount() {
|
||||
this.getDatabasesAsync()
|
||||
}
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
const {databases} = this.state
|
||||
const {database, onSelectDatabase, onStartEdit} = this.props
|
||||
|
||||
|
@ -30,7 +47,9 @@ class DatabaseDropdown extends Component {
|
|||
|
||||
return (
|
||||
<Dropdown
|
||||
items={databases.map(text => ({text}))}
|
||||
items={databases.map(text => ({
|
||||
text,
|
||||
}))}
|
||||
selected={database || 'Loading...'}
|
||||
onChoose={onSelectDatabase}
|
||||
onClick={onStartEdit ? onStartEdit : null}
|
||||
|
@ -38,7 +57,7 @@ class DatabaseDropdown extends Component {
|
|||
)
|
||||
}
|
||||
|
||||
_getDatabases = async () => {
|
||||
private getDatabasesAsync = async (): Promise<void> => {
|
||||
const {source, database, onSelectDatabase, onErrorThrown} = this.props
|
||||
const proxy = source.links.proxy
|
||||
try {
|
||||
|
@ -50,11 +69,15 @@ class DatabaseDropdown extends Component {
|
|||
|
||||
const nonSystemDatabases = databases.filter(name => name !== '_internal')
|
||||
|
||||
this.setState({databases: nonSystemDatabases})
|
||||
this.setState({
|
||||
databases: nonSystemDatabases,
|
||||
})
|
||||
const selectedDatabaseText = nonSystemDatabases.includes(database)
|
||||
? database
|
||||
: nonSystemDatabases[0] || 'No databases'
|
||||
onSelectDatabase({text: selectedDatabaseText})
|
||||
onSelectDatabase({
|
||||
text: selectedDatabaseText,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
onErrorThrown(error)
|
||||
|
@ -62,18 +85,4 @@ class DatabaseDropdown extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
const {func, shape, string} = PropTypes
|
||||
|
||||
DatabaseDropdown.propTypes = {
|
||||
database: string,
|
||||
onSelectDatabase: func.isRequired,
|
||||
onStartEdit: func,
|
||||
onErrorThrown: func.isRequired,
|
||||
source: shape({
|
||||
links: shape({
|
||||
proxy: string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
}
|
||||
|
||||
export default DatabaseDropdown
|
|
@ -4,6 +4,7 @@ import PropTypes from 'prop-types'
|
|||
import {connect} from 'react-redux'
|
||||
import _ from 'lodash'
|
||||
import NanoDate from 'nano-date'
|
||||
import ReactResizeDetector from 'react-resize-detector'
|
||||
|
||||
import Dygraphs from 'src/external/dygraph'
|
||||
import DygraphLegend from 'src/shared/components/DygraphLegend'
|
||||
|
@ -353,6 +354,11 @@ class Dygraph extends Component {
|
|||
/>
|
||||
)}
|
||||
{nestedGraph && React.cloneElement(nestedGraph, {staticLegendHeight})}
|
||||
<ReactResizeDetector
|
||||
handleWidth={true}
|
||||
handleHeight={true}
|
||||
onResize={this.resize}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, {SFC} from 'react'
|
||||
|
||||
const EmptyQueryState = ({onAddQuery}) => (
|
||||
interface Props {
|
||||
onAddQuery: () => void
|
||||
}
|
||||
|
||||
const EmptyQueryState: SFC<Props> = ({onAddQuery}) => (
|
||||
<div className="query-maker--empty">
|
||||
<h5>This Graph has no Queries</h5>
|
||||
<br />
|
||||
|
@ -11,10 +14,4 @@ const EmptyQueryState = ({onAddQuery}) => (
|
|||
</div>
|
||||
)
|
||||
|
||||
const {func} = PropTypes
|
||||
|
||||
EmptyQueryState.propTypes = {
|
||||
onAddQuery: func.isRequired,
|
||||
}
|
||||
|
||||
export default EmptyQueryState
|
|
@ -1,12 +1,21 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, {PureComponent, MouseEvent} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import _ from 'lodash'
|
||||
import {INFLUXQL_FUNCTIONS} from 'src/data_explorer/constants'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface Props {
|
||||
onApply: (item: string[]) => void
|
||||
selectedItems: string[]
|
||||
singleSelect: boolean
|
||||
}
|
||||
|
||||
interface State {
|
||||
localSelectedItems: string[]
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class FunctionSelector extends Component {
|
||||
class FunctionSelector extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
|
@ -15,60 +24,20 @@ class FunctionSelector extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps) {
|
||||
public componentWillUpdate(nextProps) {
|
||||
if (!_.isEqual(this.props.selectedItems, nextProps.selectedItems)) {
|
||||
this.setState({localSelectedItems: nextProps.selectedItems})
|
||||
}
|
||||
}
|
||||
|
||||
onSelect = (item, e) => {
|
||||
e.stopPropagation()
|
||||
|
||||
const {localSelectedItems} = this.state
|
||||
|
||||
let nextItems
|
||||
if (this.isSelected(item)) {
|
||||
nextItems = localSelectedItems.filter(i => i !== item)
|
||||
} else {
|
||||
nextItems = [...localSelectedItems, item]
|
||||
}
|
||||
|
||||
this.setState({localSelectedItems: nextItems})
|
||||
}
|
||||
|
||||
onSingleSelect = item => {
|
||||
if (item === this.state.localSelectedItems[0]) {
|
||||
this.props.onApply([])
|
||||
this.setState({localSelectedItems: []})
|
||||
} else {
|
||||
this.props.onApply([item])
|
||||
this.setState({localSelectedItems: [item]})
|
||||
}
|
||||
}
|
||||
|
||||
isSelected = item => {
|
||||
return !!this.state.localSelectedItems.find(text => text === item)
|
||||
}
|
||||
|
||||
handleApplyFunctions = e => {
|
||||
e.stopPropagation()
|
||||
|
||||
this.props.onApply(this.state.localSelectedItems)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {localSelectedItems} = this.state
|
||||
public render() {
|
||||
const {singleSelect} = this.props
|
||||
|
||||
return (
|
||||
<div className="function-selector">
|
||||
{singleSelect ? null : (
|
||||
{!singleSelect && (
|
||||
<div className="function-selector--header">
|
||||
<span>
|
||||
{localSelectedItems.length > 0
|
||||
? `${localSelectedItems.length} Selected`
|
||||
: 'Select functions below'}
|
||||
</span>
|
||||
<span>{this.headerText}</span>
|
||||
<div
|
||||
className="btn btn-xs btn-success"
|
||||
onClick={this.handleApplyFunctions}
|
||||
|
@ -100,14 +69,49 @@ class FunctionSelector extends Component {
|
|||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, bool, func, string} = PropTypes
|
||||
private get headerText(): string {
|
||||
const numItems = this.state.localSelectedItems.length
|
||||
if (!numItems) {
|
||||
return 'Select functions below'
|
||||
}
|
||||
|
||||
FunctionSelector.propTypes = {
|
||||
onApply: func.isRequired,
|
||||
selectedItems: arrayOf(string.isRequired).isRequired,
|
||||
singleSelect: bool,
|
||||
return `${numItems} Selected`
|
||||
}
|
||||
|
||||
private onSelect = (item: string, e: MouseEvent<HTMLDivElement>): void => {
|
||||
e.stopPropagation()
|
||||
|
||||
const {localSelectedItems} = this.state
|
||||
|
||||
let nextItems
|
||||
if (this.isSelected(item)) {
|
||||
nextItems = localSelectedItems.filter(i => i !== item)
|
||||
} else {
|
||||
nextItems = [...localSelectedItems, item]
|
||||
}
|
||||
|
||||
this.setState({localSelectedItems: nextItems})
|
||||
}
|
||||
|
||||
private onSingleSelect = (item: string): void => {
|
||||
if (item === this.state.localSelectedItems[0]) {
|
||||
this.props.onApply([])
|
||||
this.setState({localSelectedItems: []})
|
||||
} else {
|
||||
this.props.onApply([item])
|
||||
this.setState({localSelectedItems: [item]})
|
||||
}
|
||||
}
|
||||
|
||||
private isSelected = (item: string): boolean => {
|
||||
return !!this.state.localSelectedItems.find(text => text === item)
|
||||
}
|
||||
|
||||
private handleApplyFunctions = (e: MouseEvent<HTMLDivElement>): void => {
|
||||
e.stopPropagation()
|
||||
this.props.onApply(this.state.localSelectedItems)
|
||||
}
|
||||
}
|
||||
|
||||
export default FunctionSelector
|
|
@ -1,7 +1,8 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import getLastValues, {TimeSeriesResponse} from 'src/shared/parsing/lastValues'
|
||||
import Gauge from 'src/shared/components/Gauge'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {DEFAULT_GAUGE_COLORS} from 'src/shared/constants/thresholds'
|
||||
import {stringifyColorValues} from 'src/shared/constants/colorOperations'
|
||||
|
@ -25,10 +26,6 @@ interface Props {
|
|||
prefix: string
|
||||
suffix: string
|
||||
resizerTopHeight?: number
|
||||
resizeCoords?: {
|
||||
i: string
|
||||
h: number
|
||||
}
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
|
@ -52,11 +49,11 @@ class GaugeChart extends PureComponent<Props> {
|
|||
<div className="single-stat">
|
||||
<Gauge
|
||||
width="900"
|
||||
height={this.height}
|
||||
colors={colors}
|
||||
gaugePosition={this.lastValueForGauge}
|
||||
height={this.height}
|
||||
prefix={prefix}
|
||||
suffix={suffix}
|
||||
gaugePosition={this.lastValueForGauge}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
@ -65,22 +62,7 @@ class GaugeChart extends PureComponent<Props> {
|
|||
private get height(): string {
|
||||
const {resizerTopHeight} = this.props
|
||||
|
||||
return (
|
||||
this.resizeCoordsHeight ||
|
||||
this.initialCellHeight ||
|
||||
resizerTopHeight ||
|
||||
300
|
||||
).toString()
|
||||
}
|
||||
|
||||
private get resizeCoordsHeight(): string {
|
||||
const {resizeCoords} = this.props
|
||||
|
||||
if (resizeCoords && this.isResizing) {
|
||||
return (resizeCoords.h * DASHBOARD_LAYOUT_ROW_HEIGHT).toString()
|
||||
}
|
||||
|
||||
return null
|
||||
return (this.initialCellHeight || resizerTopHeight || 300).toString()
|
||||
}
|
||||
|
||||
private get initialCellHeight(): string {
|
||||
|
@ -93,11 +75,6 @@ class GaugeChart extends PureComponent<Props> {
|
|||
return null
|
||||
}
|
||||
|
||||
private get isResizing(): boolean {
|
||||
const {resizeCoords, cellID} = this.props
|
||||
return resizeCoords ? cellID === resizeCoords.i : false
|
||||
}
|
||||
|
||||
private get lastValueForGauge(): number {
|
||||
const {data} = this.props
|
||||
const {lastValues} = getLastValues(data)
|
||||
|
|
|
@ -69,7 +69,6 @@ const Layout = (
|
|||
autoRefresh,
|
||||
manualRefresh,
|
||||
onDeleteCell,
|
||||
resizeCoords,
|
||||
onCancelEditCell,
|
||||
onStopAddAnnotation,
|
||||
onSummonOverlayTechnologies,
|
||||
|
@ -110,7 +109,6 @@ const Layout = (
|
|||
manualRefresh={manualRefresh}
|
||||
onStopAddAnnotation={onStopAddAnnotation}
|
||||
grabDataForDownload={grabDataForDownload}
|
||||
resizeCoords={resizeCoords}
|
||||
queries={buildQueriesForLayouts(
|
||||
cell,
|
||||
getSource(cell, source, sources, defaultSource),
|
||||
|
@ -191,7 +189,6 @@ const propTypes = {
|
|||
isStatusPage: bool,
|
||||
isEditable: bool,
|
||||
onCancelEditCell: func,
|
||||
resizeCoords: shape(),
|
||||
onZoom: func,
|
||||
sources: arrayOf(shape()),
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ReactGridLayout, {WidthProvider} from 'react-grid-layout'
|
||||
import {ResizableBox} from 'react-resizable'
|
||||
|
||||
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
|
@ -26,7 +25,6 @@ class LayoutRenderer extends Component {
|
|||
|
||||
this.state = {
|
||||
rowHeight: this.calculateRowHeight(),
|
||||
resizeCoords: null,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,8 +34,16 @@ class LayoutRenderer extends Component {
|
|||
}
|
||||
const newCells = this.props.cells.map(cell => {
|
||||
const l = layout.find(ly => ly.i === cell.i)
|
||||
const newLayout = {x: l.x, y: l.y, h: l.h, w: l.w}
|
||||
return {...cell, ...newLayout}
|
||||
const newLayout = {
|
||||
x: l.x,
|
||||
y: l.y,
|
||||
h: l.h,
|
||||
w: l.w,
|
||||
}
|
||||
return {
|
||||
...cell,
|
||||
...newLayout,
|
||||
}
|
||||
})
|
||||
this.props.onPositionChange(newCells)
|
||||
}
|
||||
|
@ -56,10 +62,6 @@ class LayoutRenderer extends Component {
|
|||
: DASHBOARD_LAYOUT_ROW_HEIGHT
|
||||
}
|
||||
|
||||
handleCellResize = () => {
|
||||
this.resizeCoords = this.setState({resizeCoords: new Date()})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
host,
|
||||
|
@ -79,70 +81,63 @@ class LayoutRenderer extends Component {
|
|||
onSummonOverlayTechnologies,
|
||||
} = this.props
|
||||
|
||||
const {rowHeight, resizeCoords} = this.state
|
||||
const {rowHeight} = this.state
|
||||
const isDashboard = !!this.props.onPositionChange
|
||||
|
||||
return (
|
||||
<ResizableBox
|
||||
height={Infinity}
|
||||
width={Infinity}
|
||||
onResize={this.handleCellResize}
|
||||
<Authorized
|
||||
requiredRole={EDITOR_ROLE}
|
||||
propsOverride={{
|
||||
isDraggable: false,
|
||||
isResizable: false,
|
||||
draggableHandle: null,
|
||||
}}
|
||||
>
|
||||
<Authorized
|
||||
requiredRole={EDITOR_ROLE}
|
||||
propsOverride={{
|
||||
isDraggable: false,
|
||||
isResizable: false,
|
||||
draggableHandle: null,
|
||||
}}
|
||||
<GridLayout
|
||||
layout={cells}
|
||||
cols={12}
|
||||
rowHeight={rowHeight}
|
||||
margin={[LAYOUT_MARGIN, LAYOUT_MARGIN]}
|
||||
containerPadding={[0, 0]}
|
||||
useCSSTransforms={false}
|
||||
onResize={this.handleCellResize}
|
||||
onLayoutChange={this.handleLayoutChange}
|
||||
draggableHandle={'.dash-graph--draggable'}
|
||||
isDraggable={isDashboard}
|
||||
isResizable={isDashboard}
|
||||
>
|
||||
<GridLayout
|
||||
layout={cells}
|
||||
cols={12}
|
||||
rowHeight={rowHeight}
|
||||
margin={[LAYOUT_MARGIN, LAYOUT_MARGIN]}
|
||||
containerPadding={[0, 0]}
|
||||
useCSSTransforms={false}
|
||||
onResize={this.handleCellResize}
|
||||
onLayoutChange={this.handleLayoutChange}
|
||||
draggableHandle={'.dash-graph--draggable'}
|
||||
isDraggable={isDashboard}
|
||||
isResizable={isDashboard}
|
||||
>
|
||||
{cells.map(cell => (
|
||||
<div key={cell.i}>
|
||||
<Authorized
|
||||
requiredRole={EDITOR_ROLE}
|
||||
propsOverride={{
|
||||
isEditable: false,
|
||||
}}
|
||||
>
|
||||
<Layout
|
||||
key={cell.i}
|
||||
cell={cell}
|
||||
host={host}
|
||||
source={source}
|
||||
onZoom={onZoom}
|
||||
sources={sources}
|
||||
templates={templates}
|
||||
timeRange={timeRange}
|
||||
isEditable={isEditable}
|
||||
onEditCell={onEditCell}
|
||||
autoRefresh={autoRefresh}
|
||||
resizeCoords={resizeCoords}
|
||||
onDeleteCell={onDeleteCell}
|
||||
onCloneCell={onCloneCell}
|
||||
manualRefresh={manualRefresh}
|
||||
onCancelEditCell={onCancelEditCell}
|
||||
onStopAddAnnotation={this.handleStopAddAnnotation}
|
||||
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
|
||||
/>
|
||||
</Authorized>
|
||||
</div>
|
||||
))}
|
||||
</GridLayout>
|
||||
</Authorized>
|
||||
</ResizableBox>
|
||||
{cells.map(cell => (
|
||||
<div key={cell.i}>
|
||||
<Authorized
|
||||
requiredRole={EDITOR_ROLE}
|
||||
propsOverride={{
|
||||
isEditable: false,
|
||||
}}
|
||||
>
|
||||
<Layout
|
||||
key={cell.i}
|
||||
cell={cell}
|
||||
host={host}
|
||||
source={source}
|
||||
onZoom={onZoom}
|
||||
sources={sources}
|
||||
templates={templates}
|
||||
timeRange={timeRange}
|
||||
isEditable={isEditable}
|
||||
onEditCell={onEditCell}
|
||||
autoRefresh={autoRefresh}
|
||||
onDeleteCell={onDeleteCell}
|
||||
onCloneCell={onCloneCell}
|
||||
manualRefresh={manualRefresh}
|
||||
onCancelEditCell={onCancelEditCell}
|
||||
onStopAddAnnotation={this.handleStopAddAnnotation}
|
||||
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
|
||||
/>
|
||||
</Authorized>
|
||||
</div>
|
||||
))}
|
||||
</GridLayout>
|
||||
</Authorized>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,7 +65,6 @@ class LineGraph extends Component {
|
|||
cellHeight,
|
||||
ruleValues,
|
||||
isBarGraph,
|
||||
resizeCoords,
|
||||
isRefreshing,
|
||||
setResolution,
|
||||
isGraphFilled,
|
||||
|
@ -127,7 +126,6 @@ class LineGraph extends Component {
|
|||
isBarGraph={isBarGraph}
|
||||
timeSeries={timeSeries}
|
||||
ruleValues={ruleValues}
|
||||
resizeCoords={resizeCoords}
|
||||
dygraphSeries={dygraphSeries}
|
||||
setResolution={setResolution}
|
||||
handleSetHoverTime={handleSetHoverTime}
|
||||
|
@ -211,7 +209,6 @@ LineGraph.propTypes = {
|
|||
setResolution: func,
|
||||
cellHeight: number,
|
||||
onZoom: func,
|
||||
resizeCoords: shape(),
|
||||
queries: arrayOf(shape({}).isRequired).isRequired,
|
||||
data: arrayOf(shape({}).isRequired).isRequired,
|
||||
colors: colorsStringSchema,
|
||||
|
|
|
@ -1,24 +1,21 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {connect} from 'react-redux'
|
||||
import {bindActionCreators} from 'redux'
|
||||
|
||||
import ColorScaleDropdown from 'shared/components/ColorScaleDropdown'
|
||||
import ColorScaleDropdown from 'src/shared/components/ColorScaleDropdown'
|
||||
|
||||
import {updateLineColors} from 'src/dashboards/actions/cellEditorOverlay'
|
||||
import {colorsStringSchema} from 'shared/schemas'
|
||||
import {ColorNumber} from 'src/types/colors'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface Props {
|
||||
lineColors: ColorNumber[]
|
||||
handleUpdateLineColors: (colors: ColorNumber[]) => void
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class LineGraphColorSelector extends Component {
|
||||
handleSelectColors = colorScale => {
|
||||
const {handleUpdateLineColors} = this.props
|
||||
const {colors} = colorScale
|
||||
|
||||
handleUpdateLineColors(colors)
|
||||
}
|
||||
|
||||
render() {
|
||||
class LineGraphColorSelector extends Component<Props> {
|
||||
public render() {
|
||||
const {lineColors} = this.props
|
||||
|
||||
return (
|
||||
|
@ -32,13 +29,13 @@ class LineGraphColorSelector extends Component {
|
|||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {func} = PropTypes
|
||||
public handleSelectColors = (colorScale): void => {
|
||||
const {handleUpdateLineColors} = this.props
|
||||
const {colors} = colorScale
|
||||
|
||||
LineGraphColorSelector.propTypes = {
|
||||
lineColors: colorsStringSchema.isRequired,
|
||||
handleUpdateLineColors: func.isRequired,
|
||||
handleUpdateLineColors(colors)
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = ({cellEditorOverlay: {lineColors}}) => ({
|
|
@ -1,18 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const LoadingDots = ({className}) => (
|
||||
<div className={`loading-dots ${className}`}>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
)
|
||||
|
||||
const {string} = PropTypes
|
||||
|
||||
LoadingDots.propTypes = {
|
||||
className: string,
|
||||
}
|
||||
|
||||
export default LoadingDots
|
|
@ -0,0 +1,24 @@
|
|||
import React, {Component} from 'react'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
}
|
||||
class LoadingDots extends Component<Props> {
|
||||
public static defaultProps: Partial<Props> = {
|
||||
className: '',
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {className} = this.props
|
||||
|
||||
return (
|
||||
<div className={`loading-dots ${className}`}>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default LoadingDots
|
|
@ -3,7 +3,6 @@ import classnames from 'classnames'
|
|||
import React, {PureComponent, MouseEvent} from 'react'
|
||||
import TagList from 'src/shared/components/TagList'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
import {QueryConfig, Tag} from 'src/types'
|
||||
|
||||
interface SourceLinks {
|
||||
|
|
|
@ -1,14 +1,42 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, {Component, ChangeEvent, KeyboardEvent, MouseEvent} from 'react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import uuid from 'uuid'
|
||||
|
||||
import ClickOutsideInput from 'shared/components/ClickOutsideInput'
|
||||
import ClickOutsideInput from 'src/shared/components/ClickOutsideInput'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface Props {
|
||||
min: string
|
||||
fixedPlaceholder: string
|
||||
fixedValue: string
|
||||
customPlaceholder: string
|
||||
customValue: string
|
||||
onSetValue: (value: string) => void
|
||||
type: string | number
|
||||
}
|
||||
|
||||
interface State {
|
||||
fixedValue: string
|
||||
customValue: string
|
||||
useCustomValue: boolean
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class OptIn extends Component {
|
||||
export default class OptIn extends Component<Props, State> {
|
||||
public static defaultProps: Partial<Props> = {
|
||||
fixedValue: '',
|
||||
customPlaceholder: 'Custom Value',
|
||||
fixedPlaceholder: 'auto',
|
||||
customValue: '',
|
||||
}
|
||||
|
||||
private id: string
|
||||
private isCustomValueInputFocused: boolean
|
||||
private grooveKnobContainer: HTMLElement
|
||||
private grooveKnob: HTMLElement
|
||||
private customValueInput: HTMLInputElement
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
|
@ -24,84 +52,7 @@ class OptIn extends Component {
|
|||
this.isCustomValueInputFocused = false
|
||||
}
|
||||
|
||||
useFixedValue = () => {
|
||||
this.setState({useCustomValue: false, customValue: ''}, () =>
|
||||
this.setValue()
|
||||
)
|
||||
}
|
||||
|
||||
useCustomValue = () => {
|
||||
this.setState({useCustomValue: true}, () => this.setValue())
|
||||
}
|
||||
|
||||
handleClickToggle = () => {
|
||||
const useCustomValueNext = !this.state.useCustomValue
|
||||
if (useCustomValueNext) {
|
||||
this.useCustomValue()
|
||||
this.customValueInput.focus()
|
||||
} else {
|
||||
this.useFixedValue()
|
||||
}
|
||||
}
|
||||
|
||||
handleFocusCustomValueInput = () => {
|
||||
this.isCustomValueInputFocused = true
|
||||
this.useCustomValue()
|
||||
}
|
||||
|
||||
handleChangeCustomValue = e => {
|
||||
this.setCustomValue(e.target.value)
|
||||
}
|
||||
|
||||
handleKeyDownCustomValueInput = e => {
|
||||
if (e.key === 'Enter' || e.key === 'Tab') {
|
||||
if (e.key === 'Enter') {
|
||||
this.customValueInput.blur()
|
||||
}
|
||||
this.considerResetCustomValue()
|
||||
}
|
||||
}
|
||||
|
||||
handleClickOutsideInput = e => {
|
||||
if (
|
||||
e.target.id !== this.grooveKnob.id &&
|
||||
e.target.id !== this.grooveKnobContainer.id &&
|
||||
this.isCustomValueInputFocused
|
||||
) {
|
||||
this.considerResetCustomValue()
|
||||
}
|
||||
}
|
||||
|
||||
considerResetCustomValue = () => {
|
||||
const customValue = this.customValueInput.value.trim()
|
||||
|
||||
this.setState({customValue})
|
||||
|
||||
if (customValue === '') {
|
||||
this.useFixedValue()
|
||||
}
|
||||
|
||||
this.isCustomValueInputFocused = false
|
||||
}
|
||||
|
||||
setCustomValue = value => {
|
||||
this.setState({customValue: value}, this.setValue)
|
||||
}
|
||||
|
||||
setValue = () => {
|
||||
const {onSetValue} = this.props
|
||||
const {useCustomValue, fixedValue, customValue} = this.state
|
||||
|
||||
if (useCustomValue) {
|
||||
onSetValue(customValue)
|
||||
} else {
|
||||
onSetValue(fixedValue)
|
||||
}
|
||||
}
|
||||
|
||||
handleInputRef = el => (this.customValueInput = el)
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
const {fixedPlaceholder, customPlaceholder, type, min} = this.props
|
||||
const {useCustomValue, customValue} = this.state
|
||||
|
||||
|
@ -143,25 +94,86 @@ class OptIn extends Component {
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private useFixedValue = (): void => {
|
||||
this.setState({useCustomValue: false, customValue: ''}, () =>
|
||||
this.setValue()
|
||||
)
|
||||
}
|
||||
|
||||
private useCustomValue = (): void => {
|
||||
this.setState({useCustomValue: true}, () => this.setValue())
|
||||
}
|
||||
|
||||
private handleClickToggle = (): void => {
|
||||
const useCustomValueNext = !this.state.useCustomValue
|
||||
if (useCustomValueNext) {
|
||||
this.useCustomValue()
|
||||
this.customValueInput.focus()
|
||||
} else {
|
||||
this.useFixedValue()
|
||||
}
|
||||
}
|
||||
|
||||
private handleFocusCustomValueInput = (): void => {
|
||||
this.isCustomValueInputFocused = true
|
||||
this.useCustomValue()
|
||||
}
|
||||
|
||||
private handleChangeCustomValue = (
|
||||
e: ChangeEvent<HTMLInputElement>
|
||||
): void => {
|
||||
this.setCustomValue(e.target.value)
|
||||
}
|
||||
|
||||
private handleKeyDownCustomValueInput = (
|
||||
e: KeyboardEvent<HTMLInputElement>
|
||||
): void => {
|
||||
if (e.key === 'Enter' || e.key === 'Tab') {
|
||||
if (e.key === 'Enter') {
|
||||
this.customValueInput.blur()
|
||||
}
|
||||
this.considerResetCustomValue()
|
||||
}
|
||||
}
|
||||
|
||||
private handleClickOutsideInput = (e: MouseEvent<HTMLElement>): void => {
|
||||
if (
|
||||
e.currentTarget.id !== this.grooveKnob.id &&
|
||||
e.currentTarget.id !== this.grooveKnobContainer.id &&
|
||||
this.isCustomValueInputFocused
|
||||
) {
|
||||
this.considerResetCustomValue()
|
||||
}
|
||||
}
|
||||
|
||||
private considerResetCustomValue = (): void => {
|
||||
const customValue = this.customValueInput.value.trim()
|
||||
|
||||
this.setState({customValue})
|
||||
|
||||
if (customValue === '') {
|
||||
this.useFixedValue()
|
||||
}
|
||||
|
||||
this.isCustomValueInputFocused = false
|
||||
}
|
||||
|
||||
private setCustomValue = (value): void => {
|
||||
this.setState({customValue: value}, this.setValue)
|
||||
}
|
||||
|
||||
private setValue = (): void => {
|
||||
const {onSetValue} = this.props
|
||||
const {useCustomValue, fixedValue, customValue} = this.state
|
||||
|
||||
if (useCustomValue) {
|
||||
onSetValue(customValue)
|
||||
} else {
|
||||
onSetValue(fixedValue)
|
||||
}
|
||||
}
|
||||
|
||||
private handleInputRef = (el: HTMLInputElement) =>
|
||||
(this.customValueInput = el)
|
||||
}
|
||||
|
||||
OptIn.defaultProps = {
|
||||
fixedValue: '',
|
||||
customPlaceholder: 'Custom Value',
|
||||
fixedPlaceholder: 'auto',
|
||||
customValue: '',
|
||||
}
|
||||
|
||||
const {func, oneOf, string} = PropTypes
|
||||
|
||||
OptIn.propTypes = {
|
||||
min: string,
|
||||
fixedPlaceholder: string,
|
||||
fixedValue: string,
|
||||
customPlaceholder: string,
|
||||
customValue: string,
|
||||
onSetValue: func.isRequired,
|
||||
type: oneOf(['text', 'number']),
|
||||
}
|
||||
|
||||
export default OptIn
|
|
@ -1,9 +1,14 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import LoadingDots from 'shared/components/LoadingDots'
|
||||
import React, {SFC, ReactNode} from 'react'
|
||||
import LoadingDots from 'src/shared/components/LoadingDots'
|
||||
import classnames from 'classnames'
|
||||
import {Status} from 'src/types'
|
||||
|
||||
const QueryStatus = ({status, children}) => {
|
||||
interface Props {
|
||||
status: Status
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const QueryStatus: SFC<Props> = ({status, children}) => {
|
||||
if (!status) {
|
||||
return <div className="query-editor--status">{children}</div>
|
||||
}
|
||||
|
@ -11,7 +16,7 @@ const QueryStatus = ({status, children}) => {
|
|||
if (status.loading) {
|
||||
return (
|
||||
<div className="query-editor--status">
|
||||
<LoadingDots />
|
||||
<LoadingDots className="query-editor--loading" />
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
@ -40,15 +45,4 @@ const QueryStatus = ({status, children}) => {
|
|||
)
|
||||
}
|
||||
|
||||
const {node, shape, string} = PropTypes
|
||||
|
||||
QueryStatus.propTypes = {
|
||||
status: shape({
|
||||
error: string,
|
||||
success: string,
|
||||
warn: string,
|
||||
}),
|
||||
children: node,
|
||||
}
|
||||
|
||||
export default QueryStatus
|
|
@ -1,8 +1,12 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, {SFC} from 'react'
|
||||
import ReactTooltip from 'react-tooltip'
|
||||
|
||||
const QuestionMarkTooltip = ({tipID, tipContent}) => (
|
||||
interface Props {
|
||||
tipID: string
|
||||
tipContent: string
|
||||
}
|
||||
|
||||
const QuestionMarkTooltip: SFC<Props> = ({tipID, tipContent}) => (
|
||||
<div className="question-mark-tooltip">
|
||||
<div
|
||||
className="question-mark-tooltip--icon"
|
||||
|
@ -21,11 +25,4 @@ const QuestionMarkTooltip = ({tipID, tipContent}) => (
|
|||
</div>
|
||||
)
|
||||
|
||||
const {string} = PropTypes
|
||||
|
||||
QuestionMarkTooltip.propTypes = {
|
||||
tipID: string.isRequired,
|
||||
tipContent: string.isRequired,
|
||||
}
|
||||
|
||||
export default QuestionMarkTooltip
|
|
@ -43,7 +43,6 @@ const RefreshingGraph = ({
|
|||
resizerTopHeight,
|
||||
staticLegend,
|
||||
manualRefresh, // when changed, re-mounts the component
|
||||
resizeCoords,
|
||||
editQueryStatus,
|
||||
handleSetHoverTime,
|
||||
grabDataForDownload,
|
||||
|
@ -89,7 +88,6 @@ const RefreshingGraph = ({
|
|||
cellHeight={cellHeight}
|
||||
resizerTopHeight={resizerTopHeight}
|
||||
editQueryStatus={editQueryStatus}
|
||||
resizeCoords={resizeCoords}
|
||||
cellID={cellID}
|
||||
prefix={prefix}
|
||||
suffix={suffix}
|
||||
|
@ -111,7 +109,6 @@ const RefreshingGraph = ({
|
|||
templates={templates}
|
||||
autoRefresh={autoRefresh}
|
||||
cellHeight={cellHeight}
|
||||
resizeCoords={resizeCoords}
|
||||
tableOptions={tableOptions}
|
||||
fieldOptions={fieldOptions}
|
||||
timeFormat={timeFormat}
|
||||
|
@ -144,7 +141,6 @@ const RefreshingGraph = ({
|
|||
timeRange={timeRange}
|
||||
autoRefresh={autoRefresh}
|
||||
isBarGraph={type === 'bar'}
|
||||
resizeCoords={resizeCoords}
|
||||
staticLegend={staticLegend}
|
||||
displayOptions={displayOptions}
|
||||
editQueryStatus={editQueryStatus}
|
||||
|
@ -172,7 +168,6 @@ RefreshingGraph.propTypes = {
|
|||
editQueryStatus: func,
|
||||
staticLegend: bool,
|
||||
onZoom: func,
|
||||
resizeCoords: shape(),
|
||||
grabDataForDownload: func,
|
||||
colors: colorsStringSchema,
|
||||
cellID: string,
|
||||
|
|
|
@ -1,98 +0,0 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import getLastValues from 'shared/parsing/lastValues'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {SMALL_CELL_HEIGHT} from 'shared/graphs/helpers'
|
||||
import {DYGRAPH_CONTAINER_V_MARGIN} from 'shared/constants'
|
||||
import {generateThresholdsListHexs} from 'shared/constants/colorOperations'
|
||||
import {colorsStringSchema} from 'shared/schemas'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
@ErrorHandling
|
||||
class SingleStat extends PureComponent {
|
||||
render() {
|
||||
const {
|
||||
data,
|
||||
cellHeight,
|
||||
isFetchingInitially,
|
||||
colors,
|
||||
prefix,
|
||||
suffix,
|
||||
lineGraph,
|
||||
staticLegendHeight,
|
||||
} = this.props
|
||||
|
||||
// If data for this graph is being fetched for the first time, show a graph-wide spinner.
|
||||
if (isFetchingInitially) {
|
||||
return (
|
||||
<div className="graph-empty">
|
||||
<h3 className="graph-spinner" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const {lastValues, series} = getLastValues(data)
|
||||
const firstAlphabeticalSeriesName = _.sortBy(series)[0]
|
||||
|
||||
const firstAlphabeticalindex = _.indexOf(
|
||||
series,
|
||||
firstAlphabeticalSeriesName
|
||||
)
|
||||
const lastValue = lastValues[firstAlphabeticalindex]
|
||||
const precision = 100.0
|
||||
const roundedValue = Math.round(+lastValue * precision) / precision
|
||||
|
||||
const {bgColor, textColor} = generateThresholdsListHexs({
|
||||
colors,
|
||||
lastValue,
|
||||
cellType: lineGraph ? 'line-plus-single-stat' : 'single-stat',
|
||||
})
|
||||
const backgroundColor = bgColor
|
||||
const color = textColor
|
||||
|
||||
const height = `calc(100% - ${staticLegendHeight +
|
||||
DYGRAPH_CONTAINER_V_MARGIN * 2}px)`
|
||||
|
||||
const singleStatStyles = staticLegendHeight
|
||||
? {
|
||||
backgroundColor,
|
||||
color,
|
||||
height,
|
||||
}
|
||||
: {
|
||||
backgroundColor,
|
||||
color,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="single-stat" style={singleStatStyles}>
|
||||
<span
|
||||
className={classnames('single-stat--value', {
|
||||
'single-stat--small': cellHeight === SMALL_CELL_HEIGHT,
|
||||
})}
|
||||
>
|
||||
{prefix}
|
||||
{roundedValue}
|
||||
{suffix}
|
||||
{lineGraph && <div className="single-stat--shadow" />}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, bool, number, shape, string} = PropTypes
|
||||
|
||||
SingleStat.propTypes = {
|
||||
data: arrayOf(shape()).isRequired,
|
||||
isFetchingInitially: bool,
|
||||
cellHeight: number,
|
||||
colors: colorsStringSchema,
|
||||
prefix: string,
|
||||
suffix: string,
|
||||
lineGraph: bool,
|
||||
staticLegendHeight: number,
|
||||
}
|
||||
|
||||
export default SingleStat
|
|
@ -0,0 +1,125 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import getLastValues from 'src/shared/parsing/lastValues'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {SMALL_CELL_HEIGHT} from 'src/shared/graphs/helpers'
|
||||
import {DYGRAPH_CONTAINER_V_MARGIN} from 'src/shared/constants'
|
||||
import {generateThresholdsListHexs} from 'src/shared/constants/colorOperations'
|
||||
import {ColorNumber} from 'src/types/colors'
|
||||
import {Data} from 'src/types/dygraphs'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface Props {
|
||||
isFetchingInitially: boolean
|
||||
cellHeight: number
|
||||
colors: ColorNumber[]
|
||||
prefix?: string
|
||||
suffix?: string
|
||||
lineGraph: boolean
|
||||
staticLegendHeight: number
|
||||
data: Data
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class SingleStat extends PureComponent<Props> {
|
||||
public static defaultProps: Partial<Props> = {
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {isFetchingInitially} = this.props
|
||||
|
||||
if (isFetchingInitially) {
|
||||
return (
|
||||
<div className="graph-empty">
|
||||
<h3 className="graph-spinner" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="single-stat" style={this.styles}>
|
||||
<span className={this.className}>
|
||||
{this.completeValue}
|
||||
{this.renderShadow}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get renderShadow(): JSX.Element {
|
||||
const {lineGraph} = this.props
|
||||
|
||||
return lineGraph && <div className="single-stat--shadow" />
|
||||
}
|
||||
|
||||
private get completeValue(): string {
|
||||
const {prefix, suffix} = this.props
|
||||
|
||||
return `${prefix}${this.roundedLastValue}${suffix}`
|
||||
}
|
||||
|
||||
private get roundedLastValue(): string {
|
||||
const {data} = this.props
|
||||
const {lastValues, series} = getLastValues(data)
|
||||
const firstAlphabeticalSeriesName = _.sortBy(series)[0]
|
||||
|
||||
const firstAlphabeticalindex = _.indexOf(
|
||||
series,
|
||||
firstAlphabeticalSeriesName
|
||||
)
|
||||
const lastValue = lastValues[firstAlphabeticalindex]
|
||||
const HUNDRED = 100.0
|
||||
const roundedValue = Math.round(+lastValue * HUNDRED) / HUNDRED
|
||||
|
||||
return `${roundedValue}`
|
||||
}
|
||||
|
||||
private get styles() {
|
||||
const {data, colors, lineGraph, staticLegendHeight} = this.props
|
||||
|
||||
const {lastValues, series} = getLastValues(data)
|
||||
const firstAlphabeticalSeriesName = _.sortBy(series)[0]
|
||||
|
||||
const firstAlphabeticalindex = _.indexOf(
|
||||
series,
|
||||
firstAlphabeticalSeriesName
|
||||
)
|
||||
const lastValue = lastValues[firstAlphabeticalindex]
|
||||
|
||||
const {bgColor, textColor} = generateThresholdsListHexs({
|
||||
colors,
|
||||
lastValue,
|
||||
cellType: lineGraph ? 'line-plus-single-stat' : 'single-stat',
|
||||
})
|
||||
|
||||
const backgroundColor = bgColor
|
||||
const color = textColor
|
||||
|
||||
const height = `calc(100% - ${staticLegendHeight +
|
||||
DYGRAPH_CONTAINER_V_MARGIN * 2}px)`
|
||||
|
||||
return staticLegendHeight
|
||||
? {
|
||||
backgroundColor,
|
||||
color,
|
||||
height,
|
||||
}
|
||||
: {
|
||||
backgroundColor,
|
||||
color,
|
||||
}
|
||||
}
|
||||
|
||||
private get className(): string {
|
||||
const {cellHeight} = this.props
|
||||
|
||||
return classnames('single-stat--value', {
|
||||
'single-stat--small': cellHeight === SMALL_CELL_HEIGHT,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default SingleStat
|
|
@ -1,7 +1,10 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, {SFC, ReactElement} from 'react'
|
||||
|
||||
const SplashPage = ({children}) => (
|
||||
interface Props {
|
||||
children: ReactElement<any>
|
||||
}
|
||||
|
||||
const SplashPage: SFC<Props> = ({children}) => (
|
||||
<div className="auth-page">
|
||||
<div className="auth-box">
|
||||
<div className="auth-logo" />
|
||||
|
@ -14,9 +17,4 @@ const SplashPage = ({children}) => (
|
|||
</div>
|
||||
)
|
||||
|
||||
const {node} = PropTypes
|
||||
SplashPage.propTypes = {
|
||||
children: node,
|
||||
}
|
||||
|
||||
export default SplashPage
|
|
@ -9,7 +9,6 @@ import {showTagKeys, showTagValues} from 'src/shared/apis/metaQuery'
|
|||
import showTagKeysParser from 'src/shared/parsing/showTagKeys'
|
||||
import showTagValuesParser from 'src/shared/parsing/showTagValues'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
import {QueryConfig, Tag} from 'src/types'
|
||||
|
||||
const {shape} = PropTypes
|
||||
|
|
|
@ -305,7 +305,7 @@ class Threesizer extends Component<Props, State> {
|
|||
|
||||
if (first && !before) {
|
||||
const second = this.state.divisions[1]
|
||||
if (second.size === 0) {
|
||||
if (second && second.size === 0) {
|
||||
return {...d, size: this.shorter(d.size)}
|
||||
}
|
||||
|
||||
|
@ -338,7 +338,7 @@ class Threesizer extends Component<Props, State> {
|
|||
|
||||
if (first && !before) {
|
||||
const second = this.state.divisions[1]
|
||||
if (second.size === 0) {
|
||||
if (second && second.size === 0) {
|
||||
return {...d, size: this.thinner(d.size)}
|
||||
}
|
||||
|
||||
|
@ -377,7 +377,7 @@ class Threesizer extends Component<Props, State> {
|
|||
const leftIndex = i - 1
|
||||
const left = _.get(divs, leftIndex, {size: 'none'})
|
||||
|
||||
if (left.size === 0) {
|
||||
if (left && left.size === 0) {
|
||||
return {...d, size: this.thinner(d.size)}
|
||||
}
|
||||
|
||||
|
@ -406,7 +406,7 @@ class Threesizer extends Component<Props, State> {
|
|||
|
||||
if (after) {
|
||||
const above = divs[i - 1]
|
||||
if (above.size === 0) {
|
||||
if (above && above.size === 0) {
|
||||
return {...d, size: this.shorter(d.size)}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,11 +21,6 @@ import {
|
|||
} from 'shared/constants/thresholds'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
const formatColor = color => {
|
||||
const {hex, name} = color
|
||||
return {hex, name}
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class ThresholdsList extends Component {
|
||||
handleAddThreshold = () => {
|
||||
|
@ -85,14 +80,23 @@ class ThresholdsList extends Component {
|
|||
onResetFocus()
|
||||
}
|
||||
|
||||
handleChooseColor = threshold => chosenColor => {
|
||||
handleChangeBaseColor = updatedColor => {
|
||||
const {handleUpdateThresholdsListColors} = this.props
|
||||
const {hex, name} = updatedColor
|
||||
|
||||
const thresholdsListColors = this.props.thresholdsListColors.map(
|
||||
color =>
|
||||
color.id === threshold.id
|
||||
? {...color, hex: chosenColor.hex, name: chosenColor.name}
|
||||
: color
|
||||
color.id === THRESHOLD_TYPE_BASE ? {...color, hex, name} : color
|
||||
)
|
||||
|
||||
handleUpdateThresholdsListColors(thresholdsListColors)
|
||||
}
|
||||
|
||||
handleChooseColor = updatedColor => {
|
||||
const {handleUpdateThresholdsListColors} = this.props
|
||||
|
||||
const thresholdsListColors = this.props.thresholdsListColors.map(
|
||||
color => (color.id === updatedColor.id ? updatedColor : color)
|
||||
)
|
||||
|
||||
handleUpdateThresholdsListColors(thresholdsListColors)
|
||||
|
@ -147,8 +151,8 @@ class ThresholdsList extends Component {
|
|||
<div className="threshold-item--label">Base Color</div>
|
||||
<ColorDropdown
|
||||
colors={THRESHOLD_COLORS}
|
||||
selected={formatColor(color)}
|
||||
onChoose={this.handleChooseColor(color)}
|
||||
selected={color}
|
||||
onChoose={this.handleChangeBaseColor}
|
||||
stretchToFit={true}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, {SFC, ReactElement} from 'react'
|
||||
import ReactTooltip from 'react-tooltip'
|
||||
|
||||
const Tooltip = ({tip, children}) => (
|
||||
interface Props {
|
||||
tip: string
|
||||
children: ReactElement<any>
|
||||
}
|
||||
const Tooltip: SFC<Props> = ({tip, children}) => (
|
||||
<div>
|
||||
<div data-tip={tip}>{children}</div>
|
||||
<ReactTooltip
|
||||
|
@ -14,11 +17,4 @@ const Tooltip = ({tip, children}) => (
|
|||
</div>
|
||||
)
|
||||
|
||||
const {shape, string} = PropTypes
|
||||
|
||||
Tooltip.propTypes = {
|
||||
tip: string,
|
||||
children: shape({}),
|
||||
}
|
||||
|
||||
export default Tooltip
|
|
@ -1,13 +1,25 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, {SFC} from 'react'
|
||||
|
||||
import AlertsApp from 'src/alerts/containers/AlertsApp'
|
||||
import NewsFeed from 'src/status/components/NewsFeed'
|
||||
import GettingStarted from 'src/status/components/GettingStarted'
|
||||
|
||||
import {Cell} from 'src/types/dashboard'
|
||||
import {Source} from 'src/types/sources'
|
||||
import {RECENT_ALERTS_LIMIT} from 'src/status/constants'
|
||||
|
||||
const WidgetCell = ({cell, source, timeRange}) => {
|
||||
interface TimeRange {
|
||||
lower: string
|
||||
upper: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
timeRange: TimeRange
|
||||
cell: Cell
|
||||
source: Source
|
||||
}
|
||||
|
||||
const WidgetCell: SFC<Props> = ({cell, source, timeRange}) => {
|
||||
switch (cell.type) {
|
||||
case 'alerts': {
|
||||
return (
|
||||
|
@ -35,15 +47,4 @@ const WidgetCell = ({cell, source, timeRange}) => {
|
|||
}
|
||||
}
|
||||
|
||||
const {shape, string} = PropTypes
|
||||
|
||||
WidgetCell.propTypes = {
|
||||
timeRange: shape({
|
||||
lower: string,
|
||||
upper: string,
|
||||
}),
|
||||
source: shape({}),
|
||||
cell: shape({}),
|
||||
}
|
||||
|
||||
export default WidgetCell
|
|
@ -1,4 +1,5 @@
|
|||
import _ from 'lodash'
|
||||
import {Data} from 'src/types/dygraphs'
|
||||
|
||||
interface Result {
|
||||
lastValues: number[]
|
||||
|
@ -24,7 +25,7 @@ export interface TimeSeriesResponse {
|
|||
}
|
||||
|
||||
export default function(
|
||||
timeSeriesResponse: TimeSeriesResponse[] | null
|
||||
timeSeriesResponse: TimeSeriesResponse[] | Data | null
|
||||
): Result {
|
||||
const values = _.get(
|
||||
timeSeriesResponse,
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
export interface Alert {
|
||||
name: string
|
||||
time: string
|
||||
value: string
|
||||
host: string
|
||||
level: string
|
||||
}
|
|
@ -7,3 +7,8 @@ interface ColorBase {
|
|||
|
||||
export type ColorString = ColorBase & {value: string}
|
||||
export type ColorNumber = ColorBase & {value: number}
|
||||
|
||||
export interface ThresholdColor {
|
||||
hex: string
|
||||
name: string
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {AuthLinks, Organization, Role, User, Me} from './auth'
|
||||
import {Template, Cell, CellQuery, Legend} from './dashboard'
|
||||
import {Template, Cell, CellQuery, Legend, Axes} from './dashboard'
|
||||
import {
|
||||
GroupBy,
|
||||
QueryConfig,
|
||||
|
@ -52,4 +52,5 @@ export {
|
|||
Task,
|
||||
Notification,
|
||||
NotificationFunc,
|
||||
Axes,
|
||||
}
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
export interface QueryConfig {
|
||||
id?: string
|
||||
database: string
|
||||
measurement: string
|
||||
retentionPolicy: string
|
||||
fields: Field[]
|
||||
database?: string
|
||||
measurement?: string
|
||||
retentionPolicy?: string
|
||||
fields?: Field[]
|
||||
tags: Tags
|
||||
groupBy: GroupBy
|
||||
groupBy?: GroupBy
|
||||
areTagsAccepted: boolean
|
||||
rawText: string
|
||||
rawText?: string
|
||||
range?: DurationRange | null
|
||||
sourceLink?: string
|
||||
fill?: string
|
||||
status?: Status
|
||||
shifts: TimeShift[]
|
||||
shifts?: TimeShift[]
|
||||
lower?: string
|
||||
upper?: string
|
||||
isQuerySupportedByExplorer?: boolean // doesn't come from server -- is set in CellEditorOverlay
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import {ReactNode} from 'react'
|
||||
|
||||
export type DropdownItem =
|
||||
| {
|
||||
text: string
|
||||
}
|
||||
| string
|
||||
export interface DropdownItem {
|
||||
text: string
|
||||
}
|
||||
|
||||
export interface DropdownAction {
|
||||
icon: string
|
||||
|
|
|
@ -72,11 +72,11 @@ function generateResponseWithLinks<T extends object>(
|
|||
}
|
||||
|
||||
interface RequestParams {
|
||||
url: string
|
||||
url: string | string[]
|
||||
resource?: string | null
|
||||
id?: string | null
|
||||
method?: string
|
||||
data?: object
|
||||
data?: object | string
|
||||
params?: object
|
||||
headers?: object
|
||||
}
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
import uuid from 'uuid'
|
||||
|
||||
import {NULL_STRING} from 'src/shared/constants/queryFillOptions'
|
||||
import {QueryConfig} from 'src/types'
|
||||
|
||||
interface DefaultQueryArgs {
|
||||
id?: string
|
||||
isKapacitorRule?: boolean
|
||||
}
|
||||
|
||||
const defaultQueryConfig = (
|
||||
{id, isKapacitorRule = false} = {id: uuid.v4()}
|
||||
) => {
|
||||
{id, isKapacitorRule = false}: DefaultQueryArgs = {id: uuid.v4()}
|
||||
): QueryConfig => {
|
||||
const queryConfig = {
|
||||
id,
|
||||
database: null,
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
const KMB_LABELS = ['K', 'M', 'B', 'T', 'Q']
|
||||
const KMG2_BIG_LABELS = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
|
||||
const KMG2_SMALL_LABELS = ['m', 'u', 'n', 'p', 'f', 'a', 'z', 'y']
|
||||
import _ from 'lodash'
|
||||
const KMB_LABELS: string[] = ['K', 'M', 'B', 'T', 'Q']
|
||||
const KMG2_BIG_LABELS: string[] = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
|
||||
const KMG2_SMALL_LABELS: string[] = ['m', 'u', 'n', 'p', 'f', 'a', 'z', 'y']
|
||||
|
||||
const pow = (base, exp) => {
|
||||
const pow = (base: number, exp: number): number => {
|
||||
if (exp < 0) {
|
||||
return 1.0 / Math.pow(base, -exp)
|
||||
}
|
||||
|
@ -10,12 +11,12 @@ const pow = (base, exp) => {
|
|||
return Math.pow(base, exp)
|
||||
}
|
||||
|
||||
const round_ = (num, places) => {
|
||||
const roundNum = (num, places): number => {
|
||||
const shift = Math.pow(10, places)
|
||||
return Math.round(num * shift) / shift
|
||||
}
|
||||
|
||||
const floatFormat = (x, optPrecision) => {
|
||||
const floatFormat = (x: number, optPrecision: number): string => {
|
||||
// Avoid invalid precision values; [1, 21] is the valid range.
|
||||
const p = Math.min(Math.max(1, optPrecision || 2), 21)
|
||||
|
||||
|
@ -41,7 +42,12 @@ const floatFormat = (x, optPrecision) => {
|
|||
}
|
||||
|
||||
// taken from https://github.com/danvk/dygraphs/blob/aaec6de56dba8ed712fd7b9d949de47b46a76ccd/src/dygraph-utils.js#L1103
|
||||
export const numberValueFormatter = (x, opts, prefix, suffix) => {
|
||||
export const numberValueFormatter = (
|
||||
x: number,
|
||||
opts: (name: string) => number,
|
||||
prefix: string,
|
||||
suffix: string
|
||||
): string => {
|
||||
const sigFigs = opts('sigFigs')
|
||||
|
||||
if (sigFigs !== null) {
|
||||
|
@ -65,7 +71,7 @@ export const numberValueFormatter = (x, opts, prefix, suffix) => {
|
|||
) {
|
||||
label = x.toExponential(digits)
|
||||
} else {
|
||||
label = `${round_(x, digits)}`
|
||||
label = `${roundNum(x, digits)}`
|
||||
}
|
||||
|
||||
if (kmb || kmg2) {
|
||||
|
@ -89,15 +95,17 @@ export const numberValueFormatter = (x, opts, prefix, suffix) => {
|
|||
let n = pow(k, kLabels.length)
|
||||
for (let j = kLabels.length - 1; j >= 0; j -= 1, n /= k) {
|
||||
if (absx >= n) {
|
||||
label = round_(x / n, digits) + kLabels[j]
|
||||
label = roundNum(x / n, digits) + kLabels[j]
|
||||
break
|
||||
}
|
||||
}
|
||||
if (kmg2) {
|
||||
const xParts = String(x.toExponential()).split('e-')
|
||||
const xParts = String(x.toExponential())
|
||||
.split('e-')
|
||||
.map(Number)
|
||||
if (xParts.length === 2 && xParts[1] >= 3 && xParts[1] <= 24) {
|
||||
if (xParts[1] % 3 > 0) {
|
||||
label = round_(xParts[0] / pow(10, xParts[1] % 3), digits)
|
||||
label = roundNum(xParts[0] / pow(10, xParts[1] % 3), digits)
|
||||
} else {
|
||||
label = Number(xParts[0]).toFixed(2)
|
||||
}
|
||||
|
@ -109,7 +117,7 @@ export const numberValueFormatter = (x, opts, prefix, suffix) => {
|
|||
return `${prefix}${label}${suffix}`
|
||||
}
|
||||
|
||||
export const formatBytes = bytes => {
|
||||
export const formatBytes = (bytes: number) => {
|
||||
if (bytes === 0) {
|
||||
return '0 Bytes'
|
||||
}
|
||||
|
@ -126,7 +134,7 @@ export const formatBytes = bytes => {
|
|||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
|
||||
}
|
||||
|
||||
export const formatRPDuration = duration => {
|
||||
export const formatRPDuration = (duration: string | null): string => {
|
||||
if (!duration) {
|
||||
return
|
||||
}
|
||||
|
@ -137,23 +145,21 @@ export const formatRPDuration = duration => {
|
|||
|
||||
let adjustedTime = duration
|
||||
const durationMatcher = /(?:(\d*)d)?(?:(\d*)h)?(?:(\d*)m)?(?:(\d*)s)?/
|
||||
const [
|
||||
_match, // eslint-disable-line no-unused-vars
|
||||
days = 0,
|
||||
hours = 0,
|
||||
minutes = 0,
|
||||
seconds = 0,
|
||||
] = duration.match(durationMatcher)
|
||||
const result = duration.match(durationMatcher)
|
||||
const days = _.get<string, string>(result, 1, '0')
|
||||
const hours = _.get<string, string>(result, 2, '0')
|
||||
const minutes = _.get<string, string>(result, 3, '0')
|
||||
const seconds = _.get<string, string>(result, 4, '0')
|
||||
|
||||
const hoursInDay = 24
|
||||
if (days) {
|
||||
if (+days) {
|
||||
adjustedTime = `${days}d`
|
||||
adjustedTime += +hours === 0 ? '' : `${hours}h`
|
||||
adjustedTime += +minutes === 0 ? '' : `${minutes}m`
|
||||
adjustedTime += +seconds === 0 ? '' : `${seconds}s`
|
||||
} else if (hours > hoursInDay) {
|
||||
const hoursRemainder = hours % hoursInDay
|
||||
const daysQuotient = (hours - hoursRemainder) / hoursInDay
|
||||
} else if (+hours > hoursInDay) {
|
||||
const hoursRemainder = +hours % hoursInDay
|
||||
const daysQuotient = (+hours - hoursRemainder) / hoursInDay
|
||||
adjustedTime = `${daysQuotient}d`
|
||||
adjustedTime += +hoursRemainder === 0 ? '' : `${hoursRemainder}h`
|
||||
adjustedTime += +minutes === 0 ? '' : `${minutes}m`
|
|
@ -1,16 +1,22 @@
|
|||
import _ from 'lodash'
|
||||
|
||||
import {TEMP_VAR_INTERVAL, AUTO_GROUP_BY} from 'shared/constants'
|
||||
import {NULL_STRING} from 'shared/constants/queryFillOptions'
|
||||
import {TEMP_VAR_INTERVAL, AUTO_GROUP_BY} from 'src/shared/constants'
|
||||
import {NULL_STRING} from 'src/shared/constants/queryFillOptions'
|
||||
import {
|
||||
TYPE_QUERY_CONFIG,
|
||||
TYPE_SHIFTED,
|
||||
TYPE_IFQL,
|
||||
} from 'src/dashboards/constants'
|
||||
import {shiftTimeRange} from 'shared/query/helpers'
|
||||
import {shiftTimeRange} from 'src/shared/query/helpers'
|
||||
import {QueryConfig, Field, GroupBy, TimeShift} from 'src/types'
|
||||
|
||||
/* eslint-disable quotes */
|
||||
export const quoteIfTimestamp = ({lower, upper}) => {
|
||||
export const quoteIfTimestamp = ({
|
||||
lower,
|
||||
upper,
|
||||
}: {
|
||||
lower: string
|
||||
upper: string
|
||||
}): {lower: string; upper: string} => {
|
||||
if (lower && lower.includes('Z') && !lower.includes("'")) {
|
||||
lower = `'${lower}'`
|
||||
}
|
||||
|
@ -21,43 +27,53 @@ export const quoteIfTimestamp = ({lower, upper}) => {
|
|||
|
||||
return {lower, upper}
|
||||
}
|
||||
/* eslint-enable quotes */
|
||||
|
||||
export default function buildInfluxQLQuery(timeRange, config, shift) {
|
||||
export default function buildInfluxQLQuery(
|
||||
timeRange,
|
||||
config: QueryConfig,
|
||||
shift: string = ''
|
||||
): string {
|
||||
const {groupBy, fill = NULL_STRING, tags, areTagsAccepted} = config
|
||||
const {upper, lower} = quoteIfTimestamp(timeRange)
|
||||
|
||||
const select = _buildSelect(config, shift)
|
||||
const select = buildSelect(config, shift)
|
||||
if (select === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const condition = _buildWhereClause({lower, upper, tags, areTagsAccepted})
|
||||
const dimensions = _buildGroupBy(groupBy)
|
||||
const fillClause = groupBy.time ? _buildFill(fill) : ''
|
||||
const condition = buildWhereClause({lower, upper, tags, areTagsAccepted})
|
||||
const dimensions = buildGroupBy(groupBy)
|
||||
const fillClause = groupBy.time ? buildFill(fill) : ''
|
||||
|
||||
return `${select}${condition}${dimensions}${fillClause}`
|
||||
}
|
||||
|
||||
function _buildSelect({fields, database, retentionPolicy, measurement}, shift) {
|
||||
if (!database || !measurement || !fields || !fields.length) {
|
||||
function buildSelect(
|
||||
{fields, database, retentionPolicy, measurement}: QueryConfig,
|
||||
shift: string | null = null
|
||||
): string {
|
||||
if (!database || !measurement || _.isEmpty(fields)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const rpSegment = retentionPolicy ? `"${retentionPolicy}"` : ''
|
||||
const fieldsClause = _buildFields(fields, shift)
|
||||
const fieldsClause = buildFields(fields, shift)
|
||||
const fullyQualifiedMeasurement = `"${database}".${rpSegment}."${measurement}"`
|
||||
const statement = `SELECT ${fieldsClause} FROM ${fullyQualifiedMeasurement}`
|
||||
return statement
|
||||
}
|
||||
|
||||
// type arg will reason about new query types i.e. IFQL, GraphQL, or queryConfig
|
||||
export const buildQuery = (type, timeRange, config, shift) => {
|
||||
export const buildQuery = (
|
||||
type: string,
|
||||
timeRange: object,
|
||||
config: QueryConfig,
|
||||
shift: TimeShift | null = null
|
||||
): string => {
|
||||
switch (type) {
|
||||
case TYPE_QUERY_CONFIG: {
|
||||
return buildInfluxQLQuery(timeRange, config)
|
||||
}
|
||||
|
||||
case TYPE_SHIFTED: {
|
||||
const {quantity, unit} = shift
|
||||
return buildInfluxQLQuery(
|
||||
|
@ -75,11 +91,11 @@ export const buildQuery = (type, timeRange, config, shift) => {
|
|||
return buildInfluxQLQuery(timeRange, config)
|
||||
}
|
||||
|
||||
export function buildSelectStatement(config) {
|
||||
return _buildSelect(config)
|
||||
export function buildSelectStatement(config: QueryConfig): string {
|
||||
return buildSelect(config)
|
||||
}
|
||||
|
||||
function _buildFields(fieldFuncs, shift = '') {
|
||||
function buildFields(fieldFuncs: Field[], shift = ''): string {
|
||||
if (!fieldFuncs) {
|
||||
return ''
|
||||
}
|
||||
|
@ -103,7 +119,7 @@ function _buildFields(fieldFuncs, shift = '') {
|
|||
return `${f.value}`
|
||||
}
|
||||
case 'func': {
|
||||
const args = _buildFields(f.args)
|
||||
const args = buildFields(f.args)
|
||||
const alias = f.alias ? ` AS "${f.alias}${shift}"` : ''
|
||||
return `${f.value}(${args})${alias}`
|
||||
}
|
||||
|
@ -112,7 +128,12 @@ function _buildFields(fieldFuncs, shift = '') {
|
|||
.join(', ')
|
||||
}
|
||||
|
||||
function _buildWhereClause({lower, upper, tags, areTagsAccepted}) {
|
||||
function buildWhereClause({
|
||||
lower,
|
||||
upper,
|
||||
tags,
|
||||
areTagsAccepted,
|
||||
}: QueryConfig): string {
|
||||
const timeClauses = []
|
||||
|
||||
const timeClause = quoteIfTimestamp({lower, upper})
|
||||
|
@ -148,11 +169,11 @@ function _buildWhereClause({lower, upper, tags, areTagsAccepted}) {
|
|||
return ` WHERE ${subClauses.join(' AND ')}`
|
||||
}
|
||||
|
||||
function _buildGroupBy(groupBy) {
|
||||
return `${_buildGroupByTime(groupBy)}${_buildGroupByTags(groupBy)}`
|
||||
function buildGroupBy(groupBy: GroupBy): string {
|
||||
return `${buildGroupByTime(groupBy)}${buildGroupByTags(groupBy)}`
|
||||
}
|
||||
|
||||
function _buildGroupByTime(groupBy) {
|
||||
function buildGroupByTime(groupBy: GroupBy): string {
|
||||
if (!groupBy || !groupBy.time) {
|
||||
return ''
|
||||
}
|
||||
|
@ -162,7 +183,7 @@ function _buildGroupByTime(groupBy) {
|
|||
})`
|
||||
}
|
||||
|
||||
function _buildGroupByTags(groupBy) {
|
||||
function buildGroupByTags(groupBy: GroupBy): string {
|
||||
if (!groupBy || !groupBy.tags.length) {
|
||||
return ''
|
||||
}
|
||||
|
@ -176,9 +197,9 @@ function _buildGroupByTags(groupBy) {
|
|||
return ` GROUP BY ${tags}`
|
||||
}
|
||||
|
||||
function _buildFill(fill) {
|
||||
function buildFill(fill: string): string {
|
||||
return ` FILL(${fill})`
|
||||
}
|
||||
|
||||
export const buildRawText = (q, timeRange) =>
|
||||
q.rawText || buildInfluxQLQuery(timeRange, q) || ''
|
||||
export const buildRawText = (config: QueryConfig, timeRange: object): string =>
|
||||
config.rawText || buildInfluxQLQuery(timeRange, config) || ''
|
|
@ -8,16 +8,23 @@ import {
|
|||
} from 'src/shared/reducers/helpers/fields'
|
||||
|
||||
import {
|
||||
Tag,
|
||||
Field,
|
||||
GroupBy,
|
||||
Namespace,
|
||||
QueryConfig,
|
||||
Tag,
|
||||
TagValues,
|
||||
TimeShift,
|
||||
QueryConfig,
|
||||
ApplyFuncsToFieldArgs,
|
||||
} from 'src/types'
|
||||
|
||||
export const editRawText = (
|
||||
query: QueryConfig,
|
||||
rawText: string
|
||||
): QueryConfig => {
|
||||
return {...query, rawText}
|
||||
}
|
||||
|
||||
export const chooseNamespace = (
|
||||
query: QueryConfig,
|
||||
namespace: Namespace,
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
import AJAX from 'utils/ajax'
|
||||
|
||||
export const proxy = async ({source, query, db, rp, tempVars, resolution}) => {
|
||||
try {
|
||||
return await AJAX({
|
||||
method: 'POST',
|
||||
url: source,
|
||||
data: {
|
||||
tempVars,
|
||||
query,
|
||||
resolution,
|
||||
db,
|
||||
rp,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import AJAX from 'src/utils/ajax'
|
||||
|
||||
interface ProxyQuery {
|
||||
source: string | string[]
|
||||
query: string
|
||||
db?: string
|
||||
rp?: string
|
||||
tempVars?: any[]
|
||||
resolution?: number
|
||||
}
|
||||
|
||||
export async function proxy<T = any>({
|
||||
source,
|
||||
query,
|
||||
db,
|
||||
rp,
|
||||
tempVars,
|
||||
resolution,
|
||||
}: ProxyQuery) {
|
||||
try {
|
||||
return await AJAX<T>({
|
||||
method: 'POST',
|
||||
url: source,
|
||||
data: {
|
||||
tempVars,
|
||||
query,
|
||||
resolution,
|
||||
db,
|
||||
rp,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import _ from 'lodash'
|
||||
|
||||
export function get<T = any>(obj: any, path: string, fallack: T): T {
|
||||
return _.get<T>(obj, path, fallack)
|
||||
}
|
|
@ -16,6 +16,14 @@ const queryConfigActions = {
|
|||
editRawTextAsync: () => {},
|
||||
addInitialField: () => {},
|
||||
editQueryStatus: () => {},
|
||||
deleteQuery: () => {},
|
||||
fill: () => {},
|
||||
removeFuncs: () => {},
|
||||
editRawText: () => {},
|
||||
setTimeRange: () => {},
|
||||
updateRawQuery: () => {},
|
||||
updateQueryConfig: () => {},
|
||||
timeShift: () => {},
|
||||
}
|
||||
|
||||
const setup = () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import reducer from 'src/data_explorer/reducers/queryConfigs'
|
||||
|
||||
import defaultQueryConfig from 'utils/defaultQueryConfig'
|
||||
import defaultQueryConfig from 'src/utils/defaultQueryConfig'
|
||||
import {
|
||||
fill,
|
||||
timeShift,
|
||||
|
@ -9,6 +9,7 @@ import {
|
|||
groupByTime,
|
||||
toggleField,
|
||||
removeFuncs,
|
||||
editRawText,
|
||||
updateRawQuery,
|
||||
editQueryStatus,
|
||||
chooseNamespace,
|
||||
|
@ -17,36 +18,46 @@ import {
|
|||
addInitialField,
|
||||
updateQueryConfig,
|
||||
toggleTagAcceptance,
|
||||
ActionAddQuery,
|
||||
} from 'src/data_explorer/actions/view'
|
||||
|
||||
import {LINEAR, NULL_STRING} from 'shared/constants/queryFillOptions'
|
||||
import {LINEAR, NULL_STRING} from 'src/shared/constants/queryFillOptions'
|
||||
|
||||
const fakeAddQueryAction = (panelID, queryID) => {
|
||||
const fakeAddQueryAction = (queryID: string): ActionAddQuery => {
|
||||
return {
|
||||
type: 'DE_ADD_QUERY',
|
||||
payload: {panelID, queryID},
|
||||
payload: {
|
||||
queryID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function buildInitialState(queryID, params) {
|
||||
return Object.assign({}, defaultQueryConfig({id: queryID}), params)
|
||||
function buildInitialState(queryID, params?) {
|
||||
return {
|
||||
...defaultQueryConfig({
|
||||
id: queryID,
|
||||
}),
|
||||
...params,
|
||||
}
|
||||
}
|
||||
|
||||
describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
||||
const queryID = 123
|
||||
const queryID = '123'
|
||||
|
||||
it('can add a query', () => {
|
||||
const state = reducer({}, fakeAddQueryAction('blah', queryID))
|
||||
const state = reducer({}, fakeAddQueryAction(queryID))
|
||||
|
||||
const actual = state[queryID]
|
||||
const expected = defaultQueryConfig({id: queryID})
|
||||
const expected = defaultQueryConfig({
|
||||
id: queryID,
|
||||
})
|
||||
expect(actual).toEqual(expected)
|
||||
})
|
||||
|
||||
describe('choosing db, rp, and measurement', () => {
|
||||
let state
|
||||
beforeEach(() => {
|
||||
state = reducer({}, fakeAddQueryAction('any', queryID))
|
||||
state = reducer({}, fakeAddQueryAction(queryID))
|
||||
})
|
||||
|
||||
it('sets the db and rp', () => {
|
||||
|
@ -72,7 +83,7 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
describe('a query has measurements and fields', () => {
|
||||
let state
|
||||
beforeEach(() => {
|
||||
const one = reducer({}, fakeAddQueryAction('any', queryID))
|
||||
const one = reducer({}, fakeAddQueryAction(queryID))
|
||||
const two = reducer(
|
||||
one,
|
||||
chooseNamespace(queryID, {
|
||||
|
@ -81,14 +92,13 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
})
|
||||
)
|
||||
const three = reducer(two, chooseMeasurement(queryID, 'disk'))
|
||||
const field = {
|
||||
value: 'a great field',
|
||||
type: 'field',
|
||||
}
|
||||
const groupBy = {}
|
||||
|
||||
state = reducer(
|
||||
three,
|
||||
addInitialField(queryID, {
|
||||
value: 'a great field',
|
||||
type: 'field',
|
||||
})
|
||||
)
|
||||
state = reducer(three, addInitialField(queryID, field, groupBy))
|
||||
})
|
||||
|
||||
describe('choosing a new namespace', () => {
|
||||
|
@ -143,7 +153,10 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
expect(newState[queryID].fields.length).toBe(2)
|
||||
expect(newState[queryID].fields[1].alias).toEqual('mean_f2')
|
||||
expect(newState[queryID].fields[1].args).toEqual([
|
||||
{value: 'f2', type: 'field'},
|
||||
{
|
||||
value: 'f2',
|
||||
type: 'field',
|
||||
},
|
||||
])
|
||||
expect(newState[queryID].fields[1].value).toEqual('mean')
|
||||
})
|
||||
|
@ -164,7 +177,10 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
expect(newState[queryID].fields[1].value).toBe('mean')
|
||||
expect(newState[queryID].fields[1].alias).toBe('mean_f2')
|
||||
expect(newState[queryID].fields[1].args).toEqual([
|
||||
{value: 'f2', type: 'field'},
|
||||
{
|
||||
value: 'f2',
|
||||
type: 'field',
|
||||
},
|
||||
])
|
||||
expect(newState[queryID].fields[1].type).toBe('func')
|
||||
})
|
||||
|
@ -175,7 +191,10 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
|
||||
const newState = reducer(
|
||||
state,
|
||||
toggleField(queryID, {value: 'fk1', type: 'field'})
|
||||
toggleField(queryID, {
|
||||
value: 'fk1',
|
||||
type: 'field',
|
||||
})
|
||||
)
|
||||
|
||||
expect(newState[queryID].fields.length).toBe(1)
|
||||
|
@ -185,58 +204,122 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
|
||||
describe('DE_APPLY_FUNCS_TO_FIELD', () => {
|
||||
it('applies new functions to a field', () => {
|
||||
const f1 = {value: 'f1', type: 'field'}
|
||||
const f2 = {value: 'f2', type: 'field'}
|
||||
const f1 = {
|
||||
value: 'f1',
|
||||
type: 'field',
|
||||
}
|
||||
const f2 = {
|
||||
value: 'f2',
|
||||
type: 'field',
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
[queryID]: {
|
||||
id: 123,
|
||||
[queryID]: buildInitialState(queryID, {
|
||||
id: '123',
|
||||
database: 'db1',
|
||||
measurement: 'm1',
|
||||
fields: [
|
||||
{value: 'fn1', type: 'func', args: [f1], alias: `fn1_${f1.value}`},
|
||||
{value: 'fn1', type: 'func', args: [f2], alias: `fn1_${f2.value}`},
|
||||
{value: 'fn2', type: 'func', args: [f1], alias: `fn2_${f1.value}`},
|
||||
{
|
||||
value: 'fn1',
|
||||
type: 'func',
|
||||
args: [f1],
|
||||
alias: `fn1_${f1.value}`,
|
||||
},
|
||||
{
|
||||
value: 'fn1',
|
||||
type: 'func',
|
||||
args: [f2],
|
||||
alias: `fn1_${f2.value}`,
|
||||
},
|
||||
{
|
||||
value: 'fn2',
|
||||
type: 'func',
|
||||
args: [f1],
|
||||
alias: `fn2_${f1.value}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const action = applyFuncsToField(queryID, {
|
||||
field: {value: 'f1', type: 'field'},
|
||||
field: {
|
||||
value: 'f1',
|
||||
type: 'field',
|
||||
},
|
||||
funcs: [
|
||||
{value: 'fn3', type: 'func', args: []},
|
||||
{value: 'fn4', type: 'func', args: []},
|
||||
{
|
||||
value: 'fn3',
|
||||
type: 'func',
|
||||
},
|
||||
{
|
||||
value: 'fn4',
|
||||
type: 'func',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryID].fields).toEqual([
|
||||
{value: 'fn3', type: 'func', args: [f1], alias: `fn3_${f1.value}`},
|
||||
{value: 'fn4', type: 'func', args: [f1], alias: `fn4_${f1.value}`},
|
||||
{value: 'fn1', type: 'func', args: [f2], alias: `fn1_${f2.value}`},
|
||||
{
|
||||
value: 'fn3',
|
||||
type: 'func',
|
||||
args: [f1],
|
||||
alias: `fn3_${f1.value}`,
|
||||
},
|
||||
{
|
||||
value: 'fn4',
|
||||
type: 'func',
|
||||
args: [f1],
|
||||
alias: `fn4_${f1.value}`,
|
||||
},
|
||||
{
|
||||
value: 'fn1',
|
||||
type: 'func',
|
||||
args: [f2],
|
||||
alias: `fn1_${f2.value}`,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('DE_REMOVE_FUNCS', () => {
|
||||
it('removes all functions and group by time when one field has no funcs applied', () => {
|
||||
const f1 = {value: 'f1', type: 'field'}
|
||||
const f2 = {value: 'f2', type: 'field'}
|
||||
const f1 = {
|
||||
value: 'f1',
|
||||
type: 'field',
|
||||
}
|
||||
const f2 = {
|
||||
value: 'f2',
|
||||
type: 'field',
|
||||
}
|
||||
const fields = [
|
||||
{value: 'fn1', type: 'func', args: [f1], alias: `fn1_${f1.value}`},
|
||||
{value: 'fn1', type: 'func', args: [f2], alias: `fn1_${f2.value}`},
|
||||
{
|
||||
value: 'fn1',
|
||||
type: 'func',
|
||||
args: [f1],
|
||||
alias: `fn1_${f1.value}`,
|
||||
},
|
||||
{
|
||||
value: 'fn1',
|
||||
type: 'func',
|
||||
args: [f2],
|
||||
alias: `fn1_${f2.value}`,
|
||||
},
|
||||
]
|
||||
const groupBy = {time: '1m', tags: []}
|
||||
const groupBy = {
|
||||
time: '1m',
|
||||
tags: [],
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
[queryID]: {
|
||||
id: 123,
|
||||
[queryID]: buildInitialState(queryID, {
|
||||
id: '123',
|
||||
database: 'db1',
|
||||
measurement: 'm1',
|
||||
fields,
|
||||
groupBy,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const action = removeFuncs(queryID, fields, groupBy)
|
||||
|
@ -260,6 +343,7 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const action = chooseTag(queryID, {
|
||||
key: 'k1',
|
||||
value: 'v1',
|
||||
|
@ -314,14 +398,17 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
describe('DE_GROUP_BY_TAG', () => {
|
||||
it('adds a tag key/value to the query', () => {
|
||||
const initialState = {
|
||||
[queryID]: {
|
||||
id: 123,
|
||||
[queryID]: buildInitialState(queryID, {
|
||||
id: '123',
|
||||
database: 'db1',
|
||||
measurement: 'm1',
|
||||
fields: [],
|
||||
tags: {},
|
||||
groupBy: {tags: [], time: null},
|
||||
},
|
||||
groupBy: {
|
||||
tags: [],
|
||||
time: null,
|
||||
},
|
||||
}),
|
||||
}
|
||||
const action = groupByTag(queryID, 'k1')
|
||||
|
||||
|
@ -334,16 +421,21 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
})
|
||||
|
||||
it('removes a tag if the given tag key is already in the GROUP BY list', () => {
|
||||
const initialState = {
|
||||
[queryID]: {
|
||||
id: 123,
|
||||
database: 'db1',
|
||||
measurement: 'm1',
|
||||
fields: [],
|
||||
tags: {},
|
||||
groupBy: {tags: ['k1'], time: null},
|
||||
const query = {
|
||||
id: '123',
|
||||
database: 'db1',
|
||||
measurement: 'm1',
|
||||
fields: [],
|
||||
tags: {},
|
||||
groupBy: {
|
||||
tags: ['k1'],
|
||||
time: null,
|
||||
},
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
[queryID]: buildInitialState(queryID, query),
|
||||
}
|
||||
const action = groupByTag(queryID, 'k1')
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
@ -389,7 +481,8 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
const initialState = {
|
||||
[queryID]: buildInitialState(queryID),
|
||||
}
|
||||
const expected = defaultQueryConfig({id: queryID}, {rawText: 'hello'})
|
||||
const id = {id: queryID}
|
||||
const expected = defaultQueryConfig(id)
|
||||
const action = updateQueryConfig(expected)
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
@ -413,12 +506,12 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
const initialState = {
|
||||
[queryID]: buildInitialState(queryID),
|
||||
}
|
||||
const status = 'your query was sweet'
|
||||
const status = {success: 'Your query was very nice'}
|
||||
const action = editQueryStatus(queryID, status)
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryID].status).toBe(status)
|
||||
expect(nextState[queryID].status).toEqual(status)
|
||||
})
|
||||
|
||||
describe('DE_FILL', () => {
|
||||
|
@ -476,11 +569,31 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
[queryID]: buildInitialState(queryID),
|
||||
}
|
||||
|
||||
const shift = {quantity: 1, unit: 'd', duration: '1d'}
|
||||
const shift = {
|
||||
quantity: '1',
|
||||
unit: 'd',
|
||||
duration: '1d',
|
||||
label: 'label',
|
||||
}
|
||||
|
||||
const action = timeShift(queryID, shift)
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryID].shifts).toEqual([shift])
|
||||
})
|
||||
})
|
||||
|
||||
describe('DE_EDIT_RAW_TEXT', () => {
|
||||
it('can edit the raw text', () => {
|
||||
const initialState = {
|
||||
[queryID]: buildInitialState(queryID),
|
||||
}
|
||||
|
||||
const rawText = 'im the raw text'
|
||||
const action = editRawText(queryID, rawText)
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryID].rawText).toEqual(rawText)
|
||||
})
|
||||
})
|
||||
})
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue