Merge master
parent
c665bc9618
commit
0f5bd2de54
|
@ -5,6 +5,7 @@
|
|||
1. [#3233](https://github.com/influxdata/chronograf/pull/3233): Add default retention policy field as option in source configuration for use in querying hosts from Host List page & Host pages
|
||||
1. [#3290](https://github.com/influxdata/chronograf/pull/3290): Add support for PagerDuty v2 in UI
|
||||
1. [#3369](https://github.com/influxdata/chronograf/pull/3369): Add support for OpsGenie v2 in UI
|
||||
1. [#3386](https://github.com/influxdata/chronograf/pull/3386): Add support for Kafka in UI to configure and create alert handlers
|
||||
1. [#3416](https://github.com/influxdata/chronograf/pull/3416): Allow kapacitor services to be disabled
|
||||
|
||||
### UI Improvements
|
||||
|
@ -34,6 +35,9 @@
|
|||
1. [#3357](https://github.com/influxdata/chronograf/pull/3357): Fix only the selected template variable value getting loaded
|
||||
1. [#3389](https://github.com/influxdata/chronograf/pull/3389): Fix Generic OAuth bug for GitHub Enterprise where the principal was incorrectly being checked for email being Primary and Verified
|
||||
1. [#3402](https://github.com/influxdata/chronograf/pull/3402): Fix missing icons when using basepath
|
||||
1. [#3412](https://github.com/influxdata/chronograf/pull/3412): Limit max-width of TICKScript editor.
|
||||
1. [#3166](https://github.com/influxdata/chronograf/pull/3166): Fixes naming of new TICKScripts
|
||||
|
||||
|
||||
## v1.4.4.1 [2018-04-16]
|
||||
|
||||
|
|
10
kapacitor.go
10
kapacitor.go
|
@ -22,7 +22,8 @@ type AlertNodes struct {
|
|||
Alerta []*Alerta `json:"alerta"` // Alerta will send alert to all Alerta
|
||||
OpsGenie []*OpsGenie `json:"opsGenie"` // OpsGenie will send alert to all OpsGenie
|
||||
OpsGenie2 []*OpsGenie `json:"opsGenie2"` // OpsGenie2 will send alert to all OpsGenie v2
|
||||
Talk []*Talk `json:"talk"` // Talk will send alert to all Talk
|
||||
Talk []*Talk `json:"talk"` // Talk will send alert to all Talk
|
||||
Kafka []*Kafka `json:"kafka"` // Kafka will send alert to all Kafka
|
||||
}
|
||||
|
||||
// Post will POST alerts to a destination URL
|
||||
|
@ -135,6 +136,13 @@ type OpsGenie struct {
|
|||
// Talk sends alerts to Jane Talk (https://jianliao.com/site)
|
||||
type Talk struct{}
|
||||
|
||||
// Kafka sends alerts to any Kafka brokers specified in the handler config
|
||||
type Kafka struct {
|
||||
Cluster string `json:"cluster"`
|
||||
Topic string `json:"topic"`
|
||||
Template string `json:"template"`
|
||||
}
|
||||
|
||||
// MarshalJSON converts AlertNodes to JSON
|
||||
func (n *AlertNodes) MarshalJSON() ([]byte, error) {
|
||||
type Alias AlertNodes
|
||||
|
|
|
@ -160,7 +160,7 @@ func (c *Client) createFromTick(rule chronograf.AlertRule) (*client.CreateTaskOp
|
|||
}
|
||||
|
||||
return &client.CreateTaskOptions{
|
||||
ID: rule.ID,
|
||||
ID: rule.Name,
|
||||
Type: taskType,
|
||||
DBRPs: dbrps,
|
||||
TICKscript: string(rule.TICKScript),
|
||||
|
|
|
@ -339,6 +339,10 @@ func (s *Service) KapacitorRulesPost(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
*/
|
||||
|
||||
if req.Name == "" {
|
||||
req.Name = req.ID
|
||||
}
|
||||
|
||||
req.ID = ""
|
||||
task, err := c.Create(ctx, req)
|
||||
if err != nil {
|
||||
|
@ -409,6 +413,10 @@ func newAlertResponse(task *kapa.Task, srcID, kapaID int) *alertResponse {
|
|||
res.AlertNodes.HipChat = []*chronograf.HipChat{}
|
||||
}
|
||||
|
||||
if res.AlertNodes.Kafka == nil {
|
||||
res.AlertNodes.Kafka = []*chronograf.Kafka{}
|
||||
}
|
||||
|
||||
if res.AlertNodes.Log == nil {
|
||||
res.AlertNodes.Log = []*chronograf.Log{}
|
||||
}
|
||||
|
|
|
@ -132,6 +132,7 @@ func Test_KapacitorRulesGet(t *testing.T) {
|
|||
OpsGenie: []*chronograf.OpsGenie{},
|
||||
OpsGenie2: []*chronograf.OpsGenie{},
|
||||
Talk: []*chronograf.Talk{},
|
||||
Kafka: []*chronograf.Kafka{},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -3677,6 +3677,7 @@
|
|||
"post",
|
||||
"http",
|
||||
"hipchat",
|
||||
"kafka",
|
||||
"opsgenie",
|
||||
"opsgenie2",
|
||||
"pagerduty",
|
||||
|
|
|
@ -146,7 +146,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",
|
||||
|
|
|
@ -24,7 +24,7 @@ import {DEFAULT_HOME_PAGE} from 'src/shared/constants'
|
|||
|
||||
import * as copy from 'src/shared/copy/notifications'
|
||||
|
||||
import {Source, Me} from 'src/types'
|
||||
import {Source, Me, Notification, NotificationFunc} from 'src/types'
|
||||
|
||||
interface Auth {
|
||||
isUsingAuth: boolean
|
||||
|
@ -47,7 +47,7 @@ interface Props {
|
|||
router: InjectedRouter
|
||||
location: Location
|
||||
auth: Auth
|
||||
notify: () => void
|
||||
notify: (message: Notification | NotificationFunc) => void
|
||||
errorThrown: () => void
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
|
@ -8,10 +8,17 @@ import {notify as notifyAction} from 'src/shared/actions/notifications'
|
|||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
import AllUsersTable from 'src/admin/components/chronograf/AllUsersTable'
|
||||
import {AuthLinks, Organization, Role, User} from 'src/types'
|
||||
import {
|
||||
AuthLinks,
|
||||
Organization,
|
||||
Role,
|
||||
User,
|
||||
Notification,
|
||||
NotificationFunc,
|
||||
} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
notify: () => void
|
||||
notify: (message: Notification | NotificationFunc) => void
|
||||
links: AuthLinks
|
||||
meID: string
|
||||
users: User[]
|
||||
|
|
|
@ -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 omponentDidMount() {
|
||||
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,11 +3,6 @@ import React, {Component} from 'react'
|
|||
import _ from 'lodash'
|
||||
import uuid from 'uuid'
|
||||
|
||||
import {
|
||||
CellEditorOverlayActions,
|
||||
CellEditorOverlayActionsFunc,
|
||||
} from 'src/types/dashboard'
|
||||
|
||||
import ResizeContainer from 'src/shared/components/ResizeContainer'
|
||||
import QueryMaker from 'src/dashboards/components/QueryMaker'
|
||||
import Visualization from 'src/dashboards/components/Visualization'
|
||||
|
@ -15,7 +10,7 @@ import OverlayControls from 'src/dashboards/components/OverlayControls'
|
|||
import DisplayOptions from 'src/dashboards/components/DisplayOptions'
|
||||
import CEOBottom from 'src/dashboards/components/CEOBottom'
|
||||
|
||||
import * as queryModifiers from 'src/utils/queryTransitions'
|
||||
import * as queryTransitions from 'src/utils/queryTransitions'
|
||||
|
||||
import defaultQueryConfig from 'src/utils/defaultQueryConfig'
|
||||
import {buildQuery} from 'src/utils/influxql'
|
||||
|
@ -36,6 +31,9 @@ import {
|
|||
TEMP_VAR_DASHBOARD_TIME,
|
||||
} from 'src/shared/constants'
|
||||
import {getCellTypeColors} from 'src/dashboards/constants/cellEditor'
|
||||
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
import {
|
||||
TimeRange,
|
||||
Source,
|
||||
|
@ -45,7 +43,19 @@ import {
|
|||
Legend,
|
||||
Status,
|
||||
} from 'src/types'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
type QueryTransitions = typeof queryTransitions
|
||||
type EditRawTextAsyncFunc = (
|
||||
url: string,
|
||||
id: string,
|
||||
text: string
|
||||
) => Promise<void>
|
||||
type CellEditorOverlayActionsFunc = (queryID: string, ...args: any[]) => void
|
||||
type QueryActions = {
|
||||
[K in keyof QueryTransitions]: CellEditorOverlayActionsFunc
|
||||
}
|
||||
export type CellEditorOverlayActions = QueryActions & {
|
||||
editRawTextAsync: EditRawTextAsyncFunc
|
||||
}
|
||||
|
||||
const staticLegend: Legend = {
|
||||
type: 'static',
|
||||
|
@ -248,17 +258,16 @@ class CellEditorOverlay extends Component<Props, State> {
|
|||
this.overlayRef = r
|
||||
}
|
||||
|
||||
private queryStateReducer = (queryModifier): CellEditorOverlayActionsFunc => (
|
||||
queryID: string,
|
||||
...payload: any[]
|
||||
) => {
|
||||
private queryStateReducer = (
|
||||
queryTransition
|
||||
): CellEditorOverlayActionsFunc => (queryID: string, ...payload: any[]) => {
|
||||
const {queriesWorkingDraft} = this.state
|
||||
const query = queriesWorkingDraft.find(q => q.id === queryID)
|
||||
const queryWorkingDraft = queriesWorkingDraft.find(q => q.id === queryID)
|
||||
|
||||
const nextQuery = queryModifier(query, ...payload)
|
||||
const nextQuery = queryTransition(queryWorkingDraft, ...payload)
|
||||
|
||||
const nextQueries = queriesWorkingDraft.map(q => {
|
||||
if (q.id === query.id) {
|
||||
if (q.id === queryWorkingDraft.id) {
|
||||
return {...nextQuery, source: nextSource(q, nextQuery)}
|
||||
}
|
||||
|
||||
|
@ -492,20 +501,12 @@ class CellEditorOverlay extends Component<Props, State> {
|
|||
}
|
||||
|
||||
private get queryActions(): CellEditorOverlayActions {
|
||||
const original = {
|
||||
editRawTextAsync: () => Promise.resolve(),
|
||||
...queryModifiers,
|
||||
}
|
||||
const mapped = _.reduce<CellEditorOverlayActions, CellEditorOverlayActions>(
|
||||
original,
|
||||
(acc, v, k) => {
|
||||
acc[k] = this.queryStateReducer(v)
|
||||
return acc
|
||||
},
|
||||
original
|
||||
)
|
||||
const mapped: QueryActions = _.mapValues<
|
||||
QueryActions,
|
||||
CellEditorOverlayActionsFunc
|
||||
>(queryTransitions, v => this.queryStateReducer(v)) as QueryActions
|
||||
|
||||
const result = {
|
||||
const result: CellEditorOverlayActions = {
|
||||
...mapped,
|
||||
editRawTextAsync: this.handleEditRawText,
|
||||
}
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import React, {SFC} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {QueryConfig, Source, SourceLinks, TimeRange} from 'src/types'
|
||||
import {CellEditorOverlayActions} from 'src/types/dashboard'
|
||||
|
||||
import EmptyQuery from 'src/shared/components/EmptyQuery'
|
||||
import QueryTabList from 'src/shared/components/QueryTabList'
|
||||
import QueryTextArea from 'src/dashboards/components/QueryTextArea'
|
||||
|
@ -11,6 +8,9 @@ import SchemaExplorer from 'src/shared/components/SchemaExplorer'
|
|||
import {buildQuery} from 'src/utils/influxql'
|
||||
import {TYPE_QUERY_CONFIG, TEMPLATE_RANGE} from 'src/dashboards/constants'
|
||||
|
||||
import {QueryConfig, Source, SourceLinks, TimeRange} from 'src/types'
|
||||
import {CellEditorOverlayActions} from 'src/dashboards/components/CellEditorOverlay'
|
||||
|
||||
const rawTextBinder = (
|
||||
links: SourceLinks,
|
||||
id: string,
|
||||
|
|
|
@ -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}`
|
||||
}
|
|
@ -6,24 +6,8 @@ import FunctionSelector from 'src/shared/components/FunctionSelector'
|
|||
import {firstFieldName} from 'src/shared/reducers/helpers/fields'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface Field {
|
||||
type: string
|
||||
value: string
|
||||
}
|
||||
import {ApplyFuncsToFieldArgs, Field, FieldFunc, FuncArg} from 'src/types'
|
||||
|
||||
interface FuncArg {
|
||||
value: string
|
||||
type: string
|
||||
}
|
||||
|
||||
interface FieldFunc extends Field {
|
||||
args: FuncArg[]
|
||||
}
|
||||
|
||||
interface ApplyFuncsToFieldArgs {
|
||||
field: Field
|
||||
funcs: FuncArg[]
|
||||
}
|
||||
interface Props {
|
||||
fieldFuncs: FieldFunc[]
|
||||
isSelected: boolean
|
||||
|
|
|
@ -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
|
|
@ -2,9 +2,10 @@ import React, {PureComponent} from 'react'
|
|||
|
||||
import QueryEditor from './QueryEditor'
|
||||
import SchemaExplorer from 'src/shared/components/SchemaExplorer'
|
||||
import {Source, QueryConfig} from 'src/types'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
import {Source, QueryConfig} from 'src/types'
|
||||
|
||||
const rawTextBinder = (links, id, action) => text =>
|
||||
action(links.queries, id, text)
|
||||
|
||||
|
|
|
@ -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
|
|
@ -1,6 +1,11 @@
|
|||
// Texas Ranger
|
||||
import _ from 'lodash'
|
||||
import {FlatBody, Func} from 'src/types/ifql'
|
||||
import {
|
||||
Func,
|
||||
FlatBody,
|
||||
BinaryExpressionNode,
|
||||
MemberExpressionNode,
|
||||
} from 'src/types/ifql'
|
||||
|
||||
interface Expression {
|
||||
argument: object
|
||||
|
@ -29,6 +34,8 @@ interface AST {
|
|||
body: Body[]
|
||||
}
|
||||
|
||||
type InOrderNode = BinaryExpressionNode | MemberExpressionNode
|
||||
|
||||
export default class Walker {
|
||||
private ast: AST
|
||||
|
||||
|
@ -51,21 +58,117 @@ export default class Walker {
|
|||
})
|
||||
}
|
||||
|
||||
private variable(variable) {
|
||||
const {location} = variable
|
||||
const declarations = variable.declarations.map(({init, id}) => {
|
||||
const {type} = init
|
||||
if (type.includes('Expression')) {
|
||||
const {source, funcs} = this.expression(init, location)
|
||||
return {name: id.name, type, source, funcs}
|
||||
public get inOrderExpression(): InOrderNode[] {
|
||||
const tree = _.get(this.ast, 'body.0.expression.body', new Array<Body>())
|
||||
return this.inOrder(tree)
|
||||
}
|
||||
|
||||
private hasParen = (parent, child): boolean => {
|
||||
if (parent.operator && parent.operator.toLowerCase() === 'and') {
|
||||
// for mathematical operations:
|
||||
// if parent and child have operators
|
||||
// // return child precedence < parent precedence
|
||||
// return false
|
||||
if (
|
||||
child.type === 'LogicalExpression' &&
|
||||
child.operator.toLowerCase() === 'or'
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private inOrder = (node, paren = false): InOrderNode[] => {
|
||||
let results = []
|
||||
if (node) {
|
||||
if (paren) {
|
||||
results = [...results, {source: '(', type: 'OpenParen'}]
|
||||
}
|
||||
|
||||
return {name: id.name, type, value: init.value}
|
||||
const isLeftParen = this.hasParen(node, node.left)
|
||||
|
||||
results = [...results, ...this.inOrder(node.left, isLeftParen)]
|
||||
|
||||
if (
|
||||
node.type === 'MemberExpression' ||
|
||||
node.type === 'ObjectExpression'
|
||||
) {
|
||||
const {location, object, property} = node
|
||||
const {name, type, value} = property
|
||||
const {source} = location
|
||||
|
||||
results = [
|
||||
...results,
|
||||
{
|
||||
source,
|
||||
object: {name: object.name, type: object.type},
|
||||
property: {name: name || value, type},
|
||||
type: node.type,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
if (node.operator) {
|
||||
results = [...results, {type: 'Operator', source: node.operator}]
|
||||
}
|
||||
|
||||
if (node.name) {
|
||||
results = [...results, {type: node.type, source: node.location.source}]
|
||||
}
|
||||
|
||||
if (node.value) {
|
||||
results = [...results, {type: node.type, source: node.location.source}]
|
||||
}
|
||||
|
||||
const isRightParen = this.hasParen(node, node.right)
|
||||
|
||||
results = [...results, ...this.inOrder(node.right, isRightParen)]
|
||||
|
||||
if (paren) {
|
||||
results = [...results, {source: ')', type: 'CloseParen'}]
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
private variable(variable) {
|
||||
const {location} = variable
|
||||
const declarations = variable.declarations.map(d => {
|
||||
const {init} = d
|
||||
const {name} = d.id
|
||||
const {type, value} = init
|
||||
|
||||
if (type === 'ArrowFunctionExpression') {
|
||||
return {
|
||||
name,
|
||||
type,
|
||||
params: this.params(init.params),
|
||||
body: this.inOrder(init.body),
|
||||
source: init.location.source,
|
||||
}
|
||||
}
|
||||
|
||||
if (type.includes('Expression')) {
|
||||
const {source, funcs} = this.expression(d.init, location)
|
||||
return {name, type, source, funcs}
|
||||
}
|
||||
|
||||
return {name, type, value}
|
||||
})
|
||||
|
||||
return {source: location.source, declarations, type: variable.type}
|
||||
}
|
||||
|
||||
private params = params => {
|
||||
return params.map(p => {
|
||||
return {source: p.key.location.source, type: p.type}
|
||||
})
|
||||
}
|
||||
|
||||
// returns an in order flattening of a binary expression
|
||||
private expression(expression, location): FlatExpression {
|
||||
const funcs = this.buildFuncNodes(this.walk(expression))
|
||||
|
||||
|
@ -115,6 +218,12 @@ export default class Walker {
|
|||
return [...this.walk(currentNode.argument), {name, args, source}]
|
||||
}
|
||||
|
||||
if (currentNode.type === 'ArrowFunctionExpression') {
|
||||
const params = currentNode.params
|
||||
const body = currentNode.body
|
||||
return [{name, params, body}]
|
||||
}
|
||||
|
||||
name = currentNode.callee.name
|
||||
args = currentNode.arguments
|
||||
return [{name, args, source}]
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
import {PureComponent, ReactNode} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import {getAST} from 'src/ifql/apis'
|
||||
import {Links, BinaryExpressionNode, MemberExpressionNode} from 'src/types/ifql'
|
||||
import Walker from 'src/ifql/ast/walker'
|
||||
|
||||
interface Props {
|
||||
value: string
|
||||
links: Links
|
||||
render: (nodes: FilterNode[]) => ReactNode
|
||||
}
|
||||
|
||||
type FilterNode = BinaryExpressionNode | MemberExpressionNode
|
||||
|
||||
interface State {
|
||||
nodes: FilterNode[]
|
||||
}
|
||||
|
||||
export class Filter extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
nodes: [],
|
||||
}
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
const {links, value} = this.props
|
||||
try {
|
||||
const ast = await getAST({url: links.ast, body: value})
|
||||
const nodes = new Walker(ast).inOrderExpression
|
||||
this.setState({nodes})
|
||||
} catch (error) {
|
||||
console.error('Could not parse AST', error)
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return this.props.render(this.state.nodes)
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = ({links}) => {
|
||||
return {links: links.ifql}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(Filter)
|
|
@ -0,0 +1,52 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import {MemberExpressionNode} from 'src/types/ifql'
|
||||
|
||||
type FilterNode = MemberExpressionNode
|
||||
|
||||
interface Props {
|
||||
nodes: FilterNode[]
|
||||
}
|
||||
|
||||
interface State {
|
||||
tags: Tags
|
||||
}
|
||||
|
||||
interface Tags {
|
||||
[x: string]: string[]
|
||||
}
|
||||
|
||||
export class FilterBuilder extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
tags: this.tags,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return <div>Filter Builder</div>
|
||||
}
|
||||
|
||||
private get tags(): Tags {
|
||||
const {nodes} = this.props
|
||||
return nodes.reduce((acc, node, i) => {
|
||||
if (node.type === 'MemberExpression') {
|
||||
const tagKey = node.property.name
|
||||
const remainingNodes = nodes.slice(i + 1, nodes.length)
|
||||
const tagValue = remainingNodes.find(n => {
|
||||
return n.type !== 'Operator'
|
||||
})
|
||||
|
||||
if (!(tagKey in acc)) {
|
||||
return {...acc, [tagKey]: [tagValue.source]}
|
||||
}
|
||||
|
||||
return {...acc, [tagKey]: [...acc[tagKey], tagValue.source]}
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
}
|
||||
|
||||
export default FilterBuilder
|
|
@ -0,0 +1,66 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import {BinaryExpressionNode, MemberExpressionNode} from 'src/types/ifql'
|
||||
|
||||
type FilterNode = BinaryExpressionNode & MemberExpressionNode
|
||||
|
||||
interface Props {
|
||||
nodes: FilterNode[]
|
||||
}
|
||||
|
||||
class FilterPreview extends PureComponent<Props> {
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
{this.props.nodes.map((n, i) => <FilterPreviewNode node={n} key={i} />)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
interface FilterPreviewNodeProps {
|
||||
node: FilterNode
|
||||
}
|
||||
|
||||
/* tslint:disable */
|
||||
class FilterPreviewNode extends PureComponent<FilterPreviewNodeProps> {
|
||||
public render() {
|
||||
return this.className
|
||||
}
|
||||
|
||||
private get className(): JSX.Element {
|
||||
const {node} = this.props
|
||||
|
||||
switch (node.type) {
|
||||
case 'ObjectExpression': {
|
||||
return <div className="ifql-filter--key">{node.source}</div>
|
||||
}
|
||||
case 'MemberExpression': {
|
||||
return <div className="ifql-filter--key">{node.property.name}</div>
|
||||
}
|
||||
case 'OpenParen': {
|
||||
return <div className="ifql-filter--paren-open" />
|
||||
}
|
||||
case 'CloseParen': {
|
||||
return <div className="ifql-filter--paren-close" />
|
||||
}
|
||||
case 'NumberLiteral':
|
||||
case 'IntegerLiteral': {
|
||||
return <div className="ifql-filter--value number">{node.source}</div>
|
||||
}
|
||||
case 'BooleanLiteral': {
|
||||
return <div className="ifql-filter--value boolean">{node.source}</div>
|
||||
}
|
||||
case 'StringLiteral': {
|
||||
return <div className="ifql-filter--value string">{node.source}</div>
|
||||
}
|
||||
case 'Operator': {
|
||||
return <div className="ifql-filter--operator">{node.source}</div>
|
||||
}
|
||||
default: {
|
||||
return <div />
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default FilterPreview
|
|
@ -1,6 +1,7 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
|
||||
import FuncArgInput from 'src/ifql/components/FuncArgInput'
|
||||
import FuncArgTextArea from 'src/ifql/components/FuncArgTextArea'
|
||||
import FuncArgBool from 'src/ifql/components/FuncArgBool'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import From from 'src/ifql/components/From'
|
||||
|
@ -56,6 +57,7 @@ class FuncArg extends PureComponent<Props> {
|
|||
case argTypes.FLOAT:
|
||||
case argTypes.INT:
|
||||
case argTypes.UINT:
|
||||
case argTypes.INVALID:
|
||||
case argTypes.ARRAY: {
|
||||
return (
|
||||
<FuncArgInput
|
||||
|
@ -85,12 +87,17 @@ class FuncArg extends PureComponent<Props> {
|
|||
)
|
||||
}
|
||||
case argTypes.FUNCTION: {
|
||||
// TODO: make separate function component
|
||||
return (
|
||||
<div className="func-arg">
|
||||
<label className="func-arg--label">{argKey}</label>
|
||||
<div className="func-arg--value">{value}</div>
|
||||
</div>
|
||||
<FuncArgTextArea
|
||||
type={type}
|
||||
value={this.value}
|
||||
argKey={argKey}
|
||||
funcID={funcID}
|
||||
bodyID={bodyID}
|
||||
onChangeArg={onChangeArg}
|
||||
declarationID={declarationID}
|
||||
onGenerateScript={onGenerateScript}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case argTypes.NIL: {
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {OnChangeArg} from 'src/types/ifql'
|
||||
|
||||
interface Props {
|
||||
funcID: string
|
||||
argKey: string
|
||||
value: string
|
||||
type: string
|
||||
bodyID: string
|
||||
declarationID: string
|
||||
onChangeArg: OnChangeArg
|
||||
onGenerateScript: () => void
|
||||
inputType?: string
|
||||
}
|
||||
|
||||
interface State {
|
||||
height: string
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class FuncArgInput extends PureComponent<Props, State> {
|
||||
private ref: React.RefObject<HTMLTextAreaElement>
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.ref = React.createRef()
|
||||
this.state = {
|
||||
height: '100px',
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.setState({height: this.height})
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {argKey, value, type} = this.props
|
||||
|
||||
return (
|
||||
<div className="func-arg">
|
||||
<label className="func-arg--label" htmlFor={argKey}>
|
||||
{argKey}
|
||||
</label>
|
||||
<div className="func-arg--value">
|
||||
<textarea
|
||||
className="func-arg--textarea form-control input-sm"
|
||||
name={argKey}
|
||||
value={value}
|
||||
spellCheck={false}
|
||||
autoFocus={true}
|
||||
autoComplete="off"
|
||||
placeholder={type}
|
||||
ref={this.ref}
|
||||
onChange={this.handleChange}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
style={this.textAreaStyle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get textAreaStyle() {
|
||||
const style = {
|
||||
height: this.state.height,
|
||||
}
|
||||
|
||||
return style
|
||||
}
|
||||
|
||||
private get height(): string {
|
||||
const ref = this.ref.current
|
||||
if (!ref) {
|
||||
return '200px'
|
||||
}
|
||||
|
||||
const {scrollHeight} = ref
|
||||
|
||||
return `${scrollHeight}px`
|
||||
}
|
||||
|
||||
private handleKeyUp = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
const target = e.target as HTMLTextAreaElement
|
||||
const height = `${target.scrollHeight}px`
|
||||
this.setState({height})
|
||||
}
|
||||
|
||||
private handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const {funcID, argKey, bodyID, declarationID} = this.props
|
||||
|
||||
this.props.onChangeArg({
|
||||
funcID,
|
||||
key: argKey,
|
||||
value: e.target.value,
|
||||
declarationID,
|
||||
bodyID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default FuncArgInput
|
|
@ -1,8 +1,9 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import React, {PureComponent, ReactElement} from 'react'
|
||||
import FuncArg from 'src/ifql/components/FuncArg'
|
||||
import {OnChangeArg} from 'src/types/ifql'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {Func} from 'src/types/ifql'
|
||||
import {funcNames} from 'src/ifql/constants'
|
||||
|
||||
interface Props {
|
||||
func: Func
|
||||
|
@ -43,13 +44,32 @@ export default class FuncArgs extends PureComponent<Props> {
|
|||
/>
|
||||
)
|
||||
})}
|
||||
<div
|
||||
className="btn btn-sm btn-danger func-node--delete"
|
||||
onClick={onDeleteFunc}
|
||||
>
|
||||
Delete
|
||||
<div className="func-node--buttons">
|
||||
<div
|
||||
className="btn btn-sm btn-danger func-node--delete"
|
||||
onClick={onDeleteFunc}
|
||||
>
|
||||
Delete
|
||||
</div>
|
||||
{this.build}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
get build(): ReactElement<HTMLDivElement> {
|
||||
const {func, onGenerateScript} = this.props
|
||||
if (func.name === funcNames.FILTER) {
|
||||
return (
|
||||
<div
|
||||
className="btn btn-sm btn-primary func-node--build"
|
||||
onClick={onGenerateScript}
|
||||
>
|
||||
Build
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import {Arg} from 'src/types/ifql'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {Func} from 'src/types/ifql'
|
||||
import {funcNames} from 'src/ifql/constants'
|
||||
import Filter from 'src/ifql/components/Filter'
|
||||
import FilterPreview from 'src/ifql/components/FilterPreview'
|
||||
|
||||
import uuid from 'uuid'
|
||||
|
||||
interface Props {
|
||||
args: Arg[]
|
||||
func: Func
|
||||
}
|
||||
|
||||
export default class FuncArgsPreview extends PureComponent<Props> {
|
||||
|
@ -12,17 +18,32 @@ export default class FuncArgsPreview extends PureComponent<Props> {
|
|||
}
|
||||
|
||||
private get summarizeArguments(): JSX.Element | JSX.Element[] {
|
||||
const {args} = this.props
|
||||
const {func} = this.props
|
||||
const {args} = func
|
||||
|
||||
if (!args) {
|
||||
return
|
||||
}
|
||||
|
||||
if (func.name === funcNames.FILTER) {
|
||||
const value = _.get(args, '0.value', '')
|
||||
if (!value) {
|
||||
return this.colorizedArguments
|
||||
}
|
||||
|
||||
return <Filter value={value} render={this.filterPreview} />
|
||||
}
|
||||
|
||||
return this.colorizedArguments
|
||||
}
|
||||
|
||||
private filterPreview = nodes => {
|
||||
return <FilterPreview nodes={nodes} />
|
||||
}
|
||||
|
||||
private get colorizedArguments(): JSX.Element | JSX.Element[] {
|
||||
const {args} = this.props
|
||||
const {func} = this.props
|
||||
const {args} = func
|
||||
|
||||
return args.map((arg, i): JSX.Element => {
|
||||
if (!arg.value) {
|
||||
|
|
|
@ -34,7 +34,6 @@ export default class FuncNode extends PureComponent<Props, State> {
|
|||
public render() {
|
||||
const {
|
||||
func,
|
||||
func: {args},
|
||||
bodyID,
|
||||
onChangeArg,
|
||||
declarationID,
|
||||
|
@ -49,7 +48,7 @@ export default class FuncNode extends PureComponent<Props, State> {
|
|||
onMouseLeave={this.handleMouseLeave}
|
||||
>
|
||||
<div className="func-node--name">{func.name}</div>
|
||||
<FuncArgsPreview args={args} />
|
||||
<FuncArgsPreview func={func} />
|
||||
{isExpanded && (
|
||||
<FuncArgs
|
||||
func={func}
|
||||
|
|
|
@ -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
|
||||
|
@ -52,20 +52,24 @@ class TimeMachine extends PureComponent<Props> {
|
|||
private get divisions() {
|
||||
const {body, suggestions, script, onChangeScript} = this.props
|
||||
return [
|
||||
{
|
||||
name: 'Explore',
|
||||
render: () => <SchemaExplorer />,
|
||||
},
|
||||
{
|
||||
name: 'Script',
|
||||
render: () => (
|
||||
<TimeMachineEditor script={script} onChangeScript={onChangeScript} />
|
||||
render: visibility => (
|
||||
<TimeMachineEditor
|
||||
script={script}
|
||||
onChangeScript={onChangeScript}
|
||||
visibility={visibility}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Build',
|
||||
render: () => <BodyBuilder body={body} suggestions={suggestions} />,
|
||||
},
|
||||
{
|
||||
name: 'Explore',
|
||||
render: () => <SchemaExplorer />,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import {editor} from 'src/ifql/constants'
|
|||
interface Props {
|
||||
script: string
|
||||
onChangeScript: OnChangeScript
|
||||
visibility: string
|
||||
}
|
||||
|
||||
interface EditorInstance extends IInstance {
|
||||
|
@ -17,10 +18,22 @@ interface EditorInstance extends IInstance {
|
|||
|
||||
@ErrorHandling
|
||||
class TimeMachineEditor extends PureComponent<Props> {
|
||||
private editor: EditorInstance
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps) {
|
||||
if (prevProps.visibility === this.props.visibility) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.props.visibility === 'visible') {
|
||||
setTimeout(() => this.editor.refresh(), 60)
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {script} = this.props
|
||||
|
||||
|
@ -31,6 +44,7 @@ class TimeMachineEditor extends PureComponent<Props> {
|
|||
readonly: false,
|
||||
extraKeys: {'Ctrl-Space': 'autocomplete'},
|
||||
completeSingle: false,
|
||||
autoRefresh: true,
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -40,14 +54,19 @@ class TimeMachineEditor extends PureComponent<Props> {
|
|||
autoCursor={true}
|
||||
value={script}
|
||||
options={options}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
onBeforeChange={this.updateCode}
|
||||
onTouchStart={this.onTouchStart}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
editorDidMount={this.handleMount}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleMount = (instance: EditorInstance) => {
|
||||
this.editor = instance
|
||||
}
|
||||
|
||||
private handleKeyUp = (instance: EditorInstance, e: KeyboardEvent) => {
|
||||
const {key} = e
|
||||
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export const FROM = 'from'
|
||||
export const FILTER = 'filter'
|
||||
|
|
|
@ -5,7 +5,7 @@ import _ from 'lodash'
|
|||
|
||||
import TimeMachine from 'src/ifql/components/TimeMachine'
|
||||
import KeyboardShortcuts from 'src/shared/components/KeyboardShortcuts'
|
||||
import {Suggestion, FlatBody} from 'src/types/ifql'
|
||||
import {Suggestion, FlatBody, Links} from 'src/types/ifql'
|
||||
import {InputArg, Handlers, DeleteFuncNodeArgs, Func} from 'src/types/ifql'
|
||||
|
||||
import {bodyNodes} from 'src/ifql/helpers'
|
||||
|
@ -13,12 +13,6 @@ import {getSuggestions, getAST} from 'src/ifql/apis'
|
|||
import * as argTypes from 'src/ifql/constants/argumentTypes'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface Links {
|
||||
self: string
|
||||
suggestions: string
|
||||
ast: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
links: Links
|
||||
}
|
||||
|
@ -44,8 +38,10 @@ export class IFQLPage extends PureComponent<Props, State> {
|
|||
body: [],
|
||||
ast: null,
|
||||
suggestions: [],
|
||||
script:
|
||||
'baz = "baz"\n\nfoo = from(db: "telegraf")\n\t|> filter() \n\t|> range(start: -15m)\n\nbar = from(db: "telegraf")\n\t|> filter() \n\t|> range(start: -15m)\n\n',
|
||||
script: `from(db:"foo")
|
||||
|> filter(fn: (r) =>
|
||||
(r["a"] == 1 OR r.b == "two") AND
|
||||
(r["b"] == true OR r.d == "four"))`,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
export const chooseNamespace = (queryID, {database, retentionPolicy}) => ({
|
||||
type: 'KAPA_CHOOSE_NAMESPACE',
|
||||
payload: {
|
||||
queryID,
|
||||
database,
|
||||
retentionPolicy,
|
||||
},
|
||||
})
|
||||
|
||||
export const chooseMeasurement = (queryID, measurement) => ({
|
||||
type: 'KAPA_CHOOSE_MEASUREMENT',
|
||||
payload: {
|
||||
queryID,
|
||||
measurement,
|
||||
},
|
||||
})
|
||||
|
||||
export const chooseTag = (queryID, tag) => ({
|
||||
type: 'KAPA_CHOOSE_TAG',
|
||||
payload: {
|
||||
queryID,
|
||||
tag,
|
||||
},
|
||||
})
|
||||
|
||||
export const groupByTag = (queryID, tagKey) => ({
|
||||
type: 'KAPA_GROUP_BY_TAG',
|
||||
payload: {
|
||||
queryID,
|
||||
tagKey,
|
||||
},
|
||||
})
|
||||
|
||||
export const toggleTagAcceptance = queryID => ({
|
||||
type: 'KAPA_TOGGLE_TAG_ACCEPTANCE',
|
||||
payload: {
|
||||
queryID,
|
||||
},
|
||||
})
|
||||
|
||||
export const toggleField = (queryID, fieldFunc) => ({
|
||||
type: 'KAPA_TOGGLE_FIELD',
|
||||
payload: {
|
||||
queryID,
|
||||
fieldFunc,
|
||||
},
|
||||
})
|
||||
|
||||
export const applyFuncsToField = (queryID, fieldFunc) => ({
|
||||
type: 'KAPA_APPLY_FUNCS_TO_FIELD',
|
||||
payload: {
|
||||
queryID,
|
||||
fieldFunc,
|
||||
},
|
||||
})
|
||||
|
||||
export const groupByTime = (queryID, time) => ({
|
||||
type: 'KAPA_GROUP_BY_TIME',
|
||||
payload: {
|
||||
queryID,
|
||||
time,
|
||||
},
|
||||
})
|
||||
|
||||
export const removeFuncs = (queryID, fields) => ({
|
||||
type: 'KAPA_REMOVE_FUNCS',
|
||||
payload: {
|
||||
queryID,
|
||||
fields,
|
||||
},
|
||||
})
|
||||
|
||||
export const timeShift = (queryID, shift) => ({
|
||||
type: 'KAPA_TIME_SHIFT',
|
||||
payload: {
|
||||
queryID,
|
||||
shift,
|
||||
},
|
||||
})
|
|
@ -0,0 +1,183 @@
|
|||
import {
|
||||
ApplyFuncsToFieldArgs,
|
||||
Field,
|
||||
Namespace,
|
||||
Tag,
|
||||
TimeShift,
|
||||
} from 'src/types'
|
||||
|
||||
interface ChooseNamespaceAction {
|
||||
type: 'KAPA_CHOOSE_NAMESPACE'
|
||||
payload: {
|
||||
queryID: string
|
||||
database: string
|
||||
retentionPolicy: string
|
||||
}
|
||||
}
|
||||
export const chooseNamespace = (
|
||||
queryID: string,
|
||||
{database, retentionPolicy}: Namespace
|
||||
): ChooseNamespaceAction => ({
|
||||
type: 'KAPA_CHOOSE_NAMESPACE',
|
||||
payload: {
|
||||
queryID,
|
||||
database,
|
||||
retentionPolicy,
|
||||
},
|
||||
})
|
||||
|
||||
interface ChooseMeasurementAction {
|
||||
type: 'KAPA_CHOOSE_MEASUREMENT'
|
||||
payload: {
|
||||
queryID: string
|
||||
measurement: string
|
||||
}
|
||||
}
|
||||
export const chooseMeasurement = (
|
||||
queryID: string,
|
||||
measurement: string
|
||||
): ChooseMeasurementAction => ({
|
||||
type: 'KAPA_CHOOSE_MEASUREMENT',
|
||||
payload: {
|
||||
queryID,
|
||||
measurement,
|
||||
},
|
||||
})
|
||||
|
||||
interface ChooseTagAction {
|
||||
type: 'KAPA_CHOOSE_TAG'
|
||||
payload: {
|
||||
queryID: string
|
||||
tag: Tag
|
||||
}
|
||||
}
|
||||
export const chooseTag = (queryID: string, tag: Tag): ChooseTagAction => ({
|
||||
type: 'KAPA_CHOOSE_TAG',
|
||||
payload: {
|
||||
queryID,
|
||||
tag,
|
||||
},
|
||||
})
|
||||
|
||||
interface GroupByTagAction {
|
||||
type: 'KAPA_GROUP_BY_TAG'
|
||||
payload: {
|
||||
queryID: string
|
||||
tagKey: string
|
||||
}
|
||||
}
|
||||
export const groupByTag = (
|
||||
queryID: string,
|
||||
tagKey: string
|
||||
): GroupByTagAction => ({
|
||||
type: 'KAPA_GROUP_BY_TAG',
|
||||
payload: {
|
||||
queryID,
|
||||
tagKey,
|
||||
},
|
||||
})
|
||||
|
||||
interface ToggleTagAcceptanceAction {
|
||||
type: 'KAPA_TOGGLE_TAG_ACCEPTANCE'
|
||||
payload: {
|
||||
queryID: string
|
||||
}
|
||||
}
|
||||
export const toggleTagAcceptance = (
|
||||
queryID: string
|
||||
): ToggleTagAcceptanceAction => ({
|
||||
type: 'KAPA_TOGGLE_TAG_ACCEPTANCE',
|
||||
payload: {
|
||||
queryID,
|
||||
},
|
||||
})
|
||||
|
||||
interface ToggleFieldAction {
|
||||
type: 'KAPA_TOGGLE_FIELD'
|
||||
payload: {
|
||||
queryID: string
|
||||
fieldFunc: Field
|
||||
}
|
||||
}
|
||||
export const toggleField = (
|
||||
queryID: string,
|
||||
fieldFunc: Field
|
||||
): ToggleFieldAction => ({
|
||||
type: 'KAPA_TOGGLE_FIELD',
|
||||
payload: {
|
||||
queryID,
|
||||
fieldFunc,
|
||||
},
|
||||
})
|
||||
|
||||
interface ApplyFuncsToFieldAction {
|
||||
type: 'KAPA_APPLY_FUNCS_TO_FIELD'
|
||||
payload: {
|
||||
queryID: string
|
||||
fieldFunc: ApplyFuncsToFieldArgs
|
||||
}
|
||||
}
|
||||
export const applyFuncsToField = (
|
||||
queryID: string,
|
||||
fieldFunc: ApplyFuncsToFieldArgs
|
||||
): ApplyFuncsToFieldAction => ({
|
||||
type: 'KAPA_APPLY_FUNCS_TO_FIELD',
|
||||
payload: {
|
||||
queryID,
|
||||
fieldFunc,
|
||||
},
|
||||
})
|
||||
|
||||
interface GroupByTimeAction {
|
||||
type: 'KAPA_GROUP_BY_TIME'
|
||||
payload: {
|
||||
queryID: string
|
||||
time: string
|
||||
}
|
||||
}
|
||||
export const groupByTime = (
|
||||
queryID: string,
|
||||
time: string
|
||||
): GroupByTimeAction => ({
|
||||
type: 'KAPA_GROUP_BY_TIME',
|
||||
payload: {
|
||||
queryID,
|
||||
time,
|
||||
},
|
||||
})
|
||||
|
||||
interface RemoveFuncsAction {
|
||||
type: 'KAPA_REMOVE_FUNCS'
|
||||
payload: {
|
||||
queryID: string
|
||||
fields: Field[]
|
||||
}
|
||||
}
|
||||
export const removeFuncs = (
|
||||
queryID: string,
|
||||
fields: Field[]
|
||||
): RemoveFuncsAction => ({
|
||||
type: 'KAPA_REMOVE_FUNCS',
|
||||
payload: {
|
||||
queryID,
|
||||
fields,
|
||||
},
|
||||
})
|
||||
|
||||
interface TimeShiftAction {
|
||||
type: 'KAPA_TIME_SHIFT'
|
||||
payload: {
|
||||
queryID: string
|
||||
shift: TimeShift
|
||||
}
|
||||
}
|
||||
export const timeShift = (
|
||||
queryID: string,
|
||||
shift: TimeShift
|
||||
): TimeShiftAction => ({
|
||||
type: 'KAPA_TIME_SHIFT',
|
||||
payload: {
|
||||
queryID,
|
||||
shift,
|
||||
},
|
||||
})
|
|
@ -2,17 +2,7 @@ import React, {SFC} from 'react'
|
|||
|
||||
import AlertTabs from 'src/kapacitor/components/AlertTabs'
|
||||
|
||||
import {Kapacitor, Source} from 'src/types'
|
||||
|
||||
export interface Notification {
|
||||
id?: string
|
||||
type: string
|
||||
icon: string
|
||||
duration: number
|
||||
message: string
|
||||
}
|
||||
|
||||
type NotificationFunc = () => Notification
|
||||
import {Kapacitor, Source, Notification, NotificationFunc} from 'src/types'
|
||||
|
||||
interface AlertOutputProps {
|
||||
exists: boolean
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
import {
|
||||
AlertaConfig,
|
||||
HipChatConfig,
|
||||
KafkaConfig,
|
||||
OpsGenieConfig,
|
||||
PagerDutyConfig,
|
||||
PushoverConfig,
|
||||
|
@ -72,6 +73,7 @@ interface Sections {
|
|||
hipchat: Section
|
||||
httppost: Section
|
||||
influxdb: Section
|
||||
kafka: Section
|
||||
mqtt: Section
|
||||
opsgenie: Section
|
||||
opsgenie2: Section
|
||||
|
@ -96,6 +98,7 @@ interface Config {
|
|||
interface SupportedConfig {
|
||||
alerta: Config
|
||||
hipchat: Config
|
||||
kafka: Config
|
||||
opsgenie: Config
|
||||
opsgenie2: Config
|
||||
pagerduty: Config
|
||||
|
@ -222,6 +225,21 @@ class AlertTabs extends PureComponent<Props, State> {
|
|||
/>
|
||||
),
|
||||
},
|
||||
kafka: {
|
||||
type: 'Kafka',
|
||||
enabled: this.getEnabled(configSections, 'kafka'),
|
||||
renderComponent: () => (
|
||||
<KafkaConfig
|
||||
onSave={this.handleSaveConfig('kafka')}
|
||||
config={this.getSection(configSections, 'kafka')}
|
||||
onTest={this.handleTestConfig('kafka', {
|
||||
cluster: this.getProperty(configSections, 'kafka', 'id'),
|
||||
})}
|
||||
enabled={this.getEnabled(configSections, 'kafka')}
|
||||
notify={this.props.notify}
|
||||
/>
|
||||
),
|
||||
},
|
||||
opsgenie: {
|
||||
type: 'OpsGenie',
|
||||
enabled: this.getEnabled(configSections, 'opsgenie'),
|
||||
|
@ -434,6 +452,18 @@ class AlertTabs extends PureComponent<Props, State> {
|
|||
)
|
||||
}
|
||||
|
||||
private getProperty = (
|
||||
sections: Sections,
|
||||
section: string,
|
||||
property: string
|
||||
): boolean => {
|
||||
return _.get(
|
||||
sections,
|
||||
[section, 'elements', '0', 'options', property],
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
private handleSaveConfig = (section: string) => async (
|
||||
properties
|
||||
): Promise<boolean> => {
|
||||
|
@ -451,19 +481,23 @@ class AlertTabs extends PureComponent<Props, State> {
|
|||
} catch ({
|
||||
data: {error},
|
||||
}) {
|
||||
const errorMsg = _.join(_.drop(_.split(error, ': '), 2), ': ')
|
||||
const errorMsg = error.split(': ').pop()
|
||||
this.props.notify(notifyAlertEndpointSaveFailed(section, errorMsg))
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
private handleTestConfig = (section: string) => async (
|
||||
private handleTestConfig = (section: string, options?: object) => async (
|
||||
e: MouseEvent<HTMLButtonElement>
|
||||
): Promise<void> => {
|
||||
e.preventDefault()
|
||||
|
||||
try {
|
||||
const {data} = await testAlertOutput(this.props.kapacitor, section)
|
||||
const {data} = await testAlertOutput(
|
||||
this.props.kapacitor,
|
||||
section,
|
||||
options
|
||||
)
|
||||
if (data.success) {
|
||||
this.props.notify(notifyTestAlertSent(section))
|
||||
} else {
|
||||
|
|
|
@ -1,126 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import DatabaseList from 'src/shared/components/DatabaseList'
|
||||
import MeasurementList from 'src/shared/components/MeasurementList'
|
||||
import FieldList from 'src/shared/components/FieldList'
|
||||
|
||||
import {defaultEveryFrequency} from 'src/kapacitor/constants'
|
||||
|
||||
import {SourceContext} from 'src/CheckSources'
|
||||
|
||||
const makeQueryHandlers = (actions, query) => ({
|
||||
handleChooseNamespace: namespace => {
|
||||
actions.chooseNamespace(query.id, namespace)
|
||||
},
|
||||
|
||||
handleChooseMeasurement: measurement => {
|
||||
actions.chooseMeasurement(query.id, measurement)
|
||||
},
|
||||
|
||||
handleToggleField: field => {
|
||||
actions.toggleField(query.id, field)
|
||||
},
|
||||
|
||||
handleGroupByTime: time => {
|
||||
actions.groupByTime(query.id, time)
|
||||
},
|
||||
|
||||
handleApplyFuncsToField: onAddEvery => fieldFunc => {
|
||||
actions.applyFuncsToField(query.id, fieldFunc)
|
||||
onAddEvery(defaultEveryFrequency)
|
||||
},
|
||||
|
||||
handleChooseTag: tag => {
|
||||
actions.chooseTag(query.id, tag)
|
||||
},
|
||||
|
||||
handleToggleTagAcceptance: () => {
|
||||
actions.toggleTagAcceptance(query.id)
|
||||
},
|
||||
|
||||
handleGroupByTag: tagKey => {
|
||||
actions.groupByTag(query.id, tagKey)
|
||||
},
|
||||
|
||||
handleRemoveFuncs: fields => {
|
||||
actions.removeFuncs(query.id, fields)
|
||||
},
|
||||
})
|
||||
|
||||
const DataSection = ({
|
||||
actions,
|
||||
query,
|
||||
isDeadman,
|
||||
isKapacitorRule,
|
||||
onAddEvery,
|
||||
}) => {
|
||||
const {
|
||||
handleChooseTag,
|
||||
handleGroupByTag,
|
||||
handleToggleField,
|
||||
handleGroupByTime,
|
||||
handleRemoveFuncs,
|
||||
handleChooseNamespace,
|
||||
handleApplyFuncsToField,
|
||||
handleChooseMeasurement,
|
||||
handleToggleTagAcceptance,
|
||||
} = makeQueryHandlers(actions, query)
|
||||
|
||||
return (
|
||||
<SourceContext.Consumer>
|
||||
{source => (
|
||||
<div className="rule-section">
|
||||
<div className="query-builder">
|
||||
<DatabaseList
|
||||
query={query}
|
||||
onChooseNamespace={handleChooseNamespace}
|
||||
/>
|
||||
<MeasurementList
|
||||
query={query}
|
||||
onChooseMeasurement={handleChooseMeasurement}
|
||||
onChooseTag={handleChooseTag}
|
||||
onGroupByTag={handleGroupByTag}
|
||||
onToggleTagAcceptance={handleToggleTagAcceptance}
|
||||
/>
|
||||
{isDeadman ? null : (
|
||||
<FieldList
|
||||
query={query}
|
||||
onToggleField={handleToggleField}
|
||||
isKapacitorRule={isKapacitorRule}
|
||||
onGroupByTime={handleGroupByTime}
|
||||
removeFuncs={handleRemoveFuncs}
|
||||
applyFuncsToField={handleApplyFuncsToField(onAddEvery)}
|
||||
source={source}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</SourceContext.Consumer>
|
||||
)
|
||||
}
|
||||
|
||||
const {bool, func, shape, string} = PropTypes
|
||||
|
||||
DataSection.propTypes = {
|
||||
query: shape({
|
||||
id: string.isRequired,
|
||||
}).isRequired,
|
||||
actions: shape({
|
||||
chooseNamespace: func.isRequired,
|
||||
chooseMeasurement: func.isRequired,
|
||||
applyFuncsToField: func.isRequired,
|
||||
chooseTag: func.isRequired,
|
||||
groupByTag: func.isRequired,
|
||||
toggleField: func.isRequired,
|
||||
groupByTime: func.isRequired,
|
||||
toggleTagAcceptance: func.isRequired,
|
||||
}).isRequired,
|
||||
onAddEvery: func.isRequired,
|
||||
timeRange: shape({}).isRequired,
|
||||
isKapacitorRule: bool,
|
||||
isDeadman: bool,
|
||||
}
|
||||
|
||||
export default DataSection
|
|
@ -0,0 +1,119 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
|
||||
import DatabaseList from 'src/shared/components/DatabaseList'
|
||||
import MeasurementList from 'src/shared/components/MeasurementList'
|
||||
import FieldList from 'src/shared/components/FieldList'
|
||||
|
||||
import {defaultEveryFrequency} from 'src/kapacitor/constants'
|
||||
|
||||
import {SourceContext} from 'src/CheckSources'
|
||||
|
||||
import {
|
||||
ApplyFuncsToFieldArgs,
|
||||
Field,
|
||||
Namespace,
|
||||
QueryConfig,
|
||||
Source,
|
||||
TimeRange,
|
||||
Tag,
|
||||
} from 'src/types'
|
||||
import {KapacitorQueryConfigActions} from 'src/types/actions'
|
||||
|
||||
interface Props {
|
||||
actions: KapacitorQueryConfigActions
|
||||
query: QueryConfig
|
||||
isDeadman: boolean
|
||||
isKapacitorRule: boolean
|
||||
onAddEvery: () => void
|
||||
timeRange: TimeRange
|
||||
}
|
||||
|
||||
class DataSection extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {query, isDeadman, isKapacitorRule, onAddEvery} = this.props
|
||||
|
||||
return (
|
||||
<SourceContext.Consumer>
|
||||
{(source: Source) => (
|
||||
<div className="rule-section">
|
||||
<div className="query-builder">
|
||||
<DatabaseList
|
||||
query={query}
|
||||
onChooseNamespace={this.handleChooseNamespace}
|
||||
/>
|
||||
<MeasurementList
|
||||
query={query}
|
||||
onChooseMeasurement={this.handleChooseMeasurement}
|
||||
onChooseTag={this.handleChooseTag}
|
||||
onGroupByTag={this.handleGroupByTag}
|
||||
onToggleTagAcceptance={this.handleToggleTagAcceptance}
|
||||
isKapacitorRule={isKapacitorRule}
|
||||
/>
|
||||
{isDeadman ? null : (
|
||||
<FieldList
|
||||
query={query}
|
||||
applyFuncsToField={this.handleApplyFuncsToField(onAddEvery)}
|
||||
onGroupByTime={this.handleGroupByTime}
|
||||
onToggleField={this.handleToggleField}
|
||||
removeFuncs={this.handleRemoveFuncs}
|
||||
isKapacitorRule={isKapacitorRule}
|
||||
source={source}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</SourceContext.Consumer>
|
||||
)
|
||||
}
|
||||
|
||||
private handleChooseNamespace = (namespace: Namespace): void => {
|
||||
const {actions, query} = this.props
|
||||
actions.chooseNamespace(query.id, namespace)
|
||||
}
|
||||
|
||||
private handleChooseMeasurement = (measurement: string): void => {
|
||||
const {actions, query} = this.props
|
||||
actions.chooseMeasurement(query.id, measurement)
|
||||
}
|
||||
|
||||
private handleToggleField = (field: Field): void => {
|
||||
const {actions, query} = this.props
|
||||
actions.toggleField(query.id, field)
|
||||
}
|
||||
|
||||
private handleGroupByTime = (time: string): void => {
|
||||
const {actions, query} = this.props
|
||||
actions.groupByTime(query.id, time)
|
||||
}
|
||||
|
||||
private handleApplyFuncsToField = (onAddEvery: (every: string) => void) => (
|
||||
fieldFunc: ApplyFuncsToFieldArgs
|
||||
): void => {
|
||||
const {actions, query} = this.props
|
||||
actions.applyFuncsToField(query.id, fieldFunc)
|
||||
onAddEvery(defaultEveryFrequency)
|
||||
}
|
||||
|
||||
private handleChooseTag = (tag: Tag): void => {
|
||||
const {actions, query} = this.props
|
||||
actions.chooseTag(query.id, tag)
|
||||
}
|
||||
|
||||
private handleToggleTagAcceptance = (): void => {
|
||||
const {actions, query} = this.props
|
||||
actions.toggleTagAcceptance(query.id)
|
||||
}
|
||||
|
||||
private handleGroupByTag = (tagKey: string): void => {
|
||||
const {actions, query} = this.props
|
||||
actions.groupByTag(query.id, tagKey)
|
||||
}
|
||||
|
||||
private handleRemoveFuncs = (fields: Field[]): void => {
|
||||
const {actions, query} = this.props
|
||||
actions.removeFuncs(query.id, fields)
|
||||
}
|
||||
}
|
||||
|
||||
export default DataSection
|
|
@ -8,6 +8,7 @@ import {
|
|||
EmailHandler,
|
||||
AlertaHandler,
|
||||
HipchatHandler,
|
||||
KafkaHandler,
|
||||
OpsgenieHandler,
|
||||
PagerdutyHandler,
|
||||
PushoverHandler,
|
||||
|
@ -92,6 +93,15 @@ class HandlerOptions extends Component {
|
|||
validationError={validationError}
|
||||
/>
|
||||
)
|
||||
case 'kafka':
|
||||
return (
|
||||
<KafkaHandler
|
||||
selectedHandler={selectedHandler}
|
||||
handleModifyHandler={handleModifyHandler}
|
||||
onGoToConfig={onGoToConfig('kafka')}
|
||||
validationError={validationError}
|
||||
/>
|
||||
)
|
||||
case 'opsGenie':
|
||||
return (
|
||||
<OpsgenieHandler
|
||||
|
|
|
@ -3,19 +3,9 @@ import React, {ChangeEvent, MouseEvent, PureComponent} from 'react'
|
|||
import AlertOutputs from 'src/kapacitor/components/AlertOutputs'
|
||||
import Input from 'src/kapacitor/components/KapacitorFormInput'
|
||||
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
||||
|
||||
import {Kapacitor, Source} from 'src/types'
|
||||
import KapacitorFormSkipVerify from 'src/kapacitor/components/KapacitorFormSkipVerify'
|
||||
|
||||
export interface Notification {
|
||||
id?: string
|
||||
type: string
|
||||
icon: string
|
||||
duration: number
|
||||
message: string
|
||||
}
|
||||
|
||||
type NotificationFunc = () => Notification
|
||||
import {Kapacitor, Source, Notification, NotificationFunc} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
kapacitor: Kapacitor
|
||||
|
|
|
@ -93,7 +93,7 @@ export class TaskRow extends PureComponent<TaskRowProps> {
|
|||
checked={task.status === 'enabled'}
|
||||
onChange={this.handleClickRuleStatusEnabled(task)}
|
||||
/>
|
||||
<label htmlFor={`kapacitor-task-row-task-enabled ${task.id}`} />
|
||||
<label htmlFor={`kapacitor-task-row-task-enabled ${task.name}`} />
|
||||
</div>
|
||||
</td>
|
||||
<td style={{width: colActions}} className="text-right">
|
||||
|
|
|
@ -0,0 +1,261 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
|
||||
import TagInput from 'src/shared/components/TagInput'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
import {Notification, NotificationFunc} from 'src/types'
|
||||
|
||||
import {notifyInvalidBatchSizeValue} from 'src/shared/copy/notifications'
|
||||
|
||||
interface Properties {
|
||||
brokers: string[]
|
||||
timeout: string
|
||||
'batch-size': number
|
||||
'batch-timeout': string
|
||||
'use-ssl': boolean
|
||||
'ssl-ca': string
|
||||
'ssl-cert': string
|
||||
'ssl-key': string
|
||||
'insecure-skip-verify': boolean
|
||||
}
|
||||
|
||||
interface Config {
|
||||
options: Properties & {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
interface Item {
|
||||
name?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
config: Config
|
||||
onSave: (properties: Properties) => void
|
||||
onTest: (event: React.MouseEvent<HTMLButtonElement>) => void
|
||||
enabled: boolean
|
||||
notify: (message: Notification | NotificationFunc) => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
currentBrokers: string[]
|
||||
testEnabled: boolean
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class KafkaConfig extends PureComponent<Props, State> {
|
||||
private id: HTMLInputElement
|
||||
private timeout: HTMLInputElement
|
||||
private batchSize: HTMLInputElement
|
||||
private batchTimeout: HTMLInputElement
|
||||
private useSSL: HTMLInputElement
|
||||
private sslCA: HTMLInputElement
|
||||
private sslCert: HTMLInputElement
|
||||
private sslKey: HTMLInputElement
|
||||
private insecureSkipVerify: HTMLInputElement
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
const {brokers} = props.config.options
|
||||
|
||||
this.state = {
|
||||
currentBrokers: brokers || [],
|
||||
testEnabled: this.props.enabled,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {options} = this.props.config
|
||||
const id = options.id
|
||||
const timeout = options.timeout
|
||||
const batchSize = options['batch-size']
|
||||
const batchTimeout = options['batch-timeout']
|
||||
const useSSL = options['use-ssl']
|
||||
const sslCA = options['ssl-ca']
|
||||
const sslCert = options['ssl-cert']
|
||||
const sslKey = options['ssl-key']
|
||||
const insecureSkipVerify = options['insecure-skip-verify']
|
||||
|
||||
return (
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="id">ID</label>
|
||||
<input
|
||||
className="form-control"
|
||||
id="id"
|
||||
type="text"
|
||||
ref={r => (this.id = r)}
|
||||
defaultValue={id || ''}
|
||||
onChange={this.disableTest}
|
||||
readOnly={true}
|
||||
/>
|
||||
</div>
|
||||
<TagInput
|
||||
title="Brokers"
|
||||
onAddTag={this.handleAddBroker}
|
||||
onDeleteTag={this.handleDeleteBroker}
|
||||
tags={this.currentBrokersForTags}
|
||||
disableTest={this.disableTest}
|
||||
/>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="timeout">Timeout</label>
|
||||
<input
|
||||
className="form-control"
|
||||
id="timeout"
|
||||
type="text"
|
||||
ref={r => (this.timeout = r)}
|
||||
defaultValue={timeout || ''}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="batchSize">Batch Size</label>
|
||||
<input
|
||||
className="form-control"
|
||||
id="batchSize"
|
||||
type="number"
|
||||
ref={r => (this.batchSize = r)}
|
||||
defaultValue={batchSize.toString() || '0'}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="batchTimeout">Batch Timeout</label>
|
||||
<input
|
||||
className="form-control"
|
||||
id="batchTimeout"
|
||||
type="text"
|
||||
ref={r => (this.batchTimeout = r)}
|
||||
defaultValue={batchTimeout || ''}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group col-xs-12">
|
||||
<div className="form-control-static">
|
||||
<input
|
||||
id="useSSL"
|
||||
type="checkbox"
|
||||
defaultChecked={useSSL}
|
||||
ref={r => (this.useSSL = r)}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
<label htmlFor="useSSL">Use SSL</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="sslCA">SSL CA</label>
|
||||
<input
|
||||
className="form-control"
|
||||
id="sslCA"
|
||||
type="text"
|
||||
ref={r => (this.sslCA = r)}
|
||||
defaultValue={sslCA || ''}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="sslCert">SSL Cert</label>
|
||||
<input
|
||||
className="form-control"
|
||||
id="sslCert"
|
||||
type="text"
|
||||
ref={r => (this.sslCert = r)}
|
||||
defaultValue={sslCert || ''}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="sslKey">SSL Key</label>
|
||||
<input
|
||||
className="form-control"
|
||||
id="sslKey"
|
||||
type="text"
|
||||
ref={r => (this.sslKey = r)}
|
||||
defaultValue={sslKey || ''}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group col-xs-12">
|
||||
<div className="form-control-static">
|
||||
<input
|
||||
id="insecureSkipVerify"
|
||||
type="checkbox"
|
||||
defaultChecked={insecureSkipVerify}
|
||||
ref={r => (this.insecureSkipVerify = r)}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
<label htmlFor="insecureSkipVerify">Insecure Skip Verify</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group form-group-submit col-xs-12 text-center">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
type="submit"
|
||||
disabled={this.state.testEnabled}
|
||||
>
|
||||
<span className="icon checkmark" />
|
||||
Save Changes
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={!this.state.testEnabled}
|
||||
onClick={this.props.onTest}
|
||||
>
|
||||
<span className="icon pulse-c" />
|
||||
Send Test Alert
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
private get currentBrokersForTags(): Item[] {
|
||||
const {currentBrokers} = this.state
|
||||
return currentBrokers.map(broker => ({name: broker}))
|
||||
}
|
||||
|
||||
private handleSubmit = async e => {
|
||||
e.preventDefault()
|
||||
|
||||
const batchSize = parseInt(this.batchSize.value, 10)
|
||||
if (isNaN(batchSize)) {
|
||||
this.props.notify(notifyInvalidBatchSizeValue())
|
||||
return
|
||||
}
|
||||
|
||||
const properties = {
|
||||
brokers: this.state.currentBrokers,
|
||||
timeout: this.timeout.value,
|
||||
'batch-size': batchSize,
|
||||
'batch-timeout': this.batchTimeout.value,
|
||||
'use-ssl': this.useSSL.checked,
|
||||
'ssl-ca': this.sslCA.value,
|
||||
'ssl-cert': this.sslCert.value,
|
||||
'ssl-key': this.sslKey.value,
|
||||
'insecure-skip-verify': this.insecureSkipVerify.checked,
|
||||
}
|
||||
|
||||
const success = await this.props.onSave(properties)
|
||||
if (success) {
|
||||
this.setState({testEnabled: true})
|
||||
}
|
||||
}
|
||||
|
||||
private disableTest = () => {
|
||||
this.setState({testEnabled: false})
|
||||
}
|
||||
|
||||
private handleAddBroker = broker => {
|
||||
this.setState({currentBrokers: this.state.currentBrokers.concat(broker)})
|
||||
}
|
||||
|
||||
private handleDeleteBroker = broker => {
|
||||
this.setState({
|
||||
currentBrokers: this.state.currentBrokers.filter(t => t !== broker.name),
|
||||
testEnabled: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default KafkaConfig
|
|
@ -1,5 +1,6 @@
|
|||
import AlertaConfig from './AlertaConfig'
|
||||
import HipChatConfig from './HipChatConfig'
|
||||
import KafkaConfig from './KafkaConfig'
|
||||
import OpsGenieConfig from './OpsGenieConfig'
|
||||
import PagerDutyConfig from './PagerDutyConfig'
|
||||
import PushoverConfig from './PushoverConfig'
|
||||
|
@ -13,6 +14,7 @@ import VictorOpsConfig from './VictorOpsConfig'
|
|||
export {
|
||||
AlertaConfig,
|
||||
HipChatConfig,
|
||||
KafkaConfig,
|
||||
OpsGenieConfig,
|
||||
PagerDutyConfig,
|
||||
PushoverConfig,
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
import React, {SFC} from 'react'
|
||||
|
||||
import HandlerInput from 'src/kapacitor/components/HandlerInput'
|
||||
|
||||
interface Handler {
|
||||
alias: string
|
||||
enabled: boolean
|
||||
headerKey: string
|
||||
headerValue: string
|
||||
headers: {
|
||||
[key: string]: string
|
||||
}
|
||||
text: string
|
||||
type: string
|
||||
url: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
selectedHandler: object
|
||||
handleModifyHandler: (
|
||||
selectedHandler: Handler,
|
||||
fieldName: string,
|
||||
parseToArray: string
|
||||
) => void
|
||||
}
|
||||
|
||||
const KafkaHandler: SFC<Props> = ({selectedHandler, handleModifyHandler}) => (
|
||||
<div className="endpoint-tab-contents">
|
||||
<div className="endpoint-tab--parameters">
|
||||
<h4>Parameters for this Alert Handler</h4>
|
||||
<div className="faux-form">
|
||||
<HandlerInput
|
||||
selectedHandler={selectedHandler}
|
||||
handleModifyHandler={handleModifyHandler}
|
||||
fieldName="cluster"
|
||||
fieldDisplay="Cluster"
|
||||
placeholder=""
|
||||
fieldColumns="col-md-12"
|
||||
/>
|
||||
<HandlerInput
|
||||
selectedHandler={selectedHandler}
|
||||
handleModifyHandler={handleModifyHandler}
|
||||
fieldName="topic"
|
||||
fieldDisplay="Topic"
|
||||
placeholder=""
|
||||
/>
|
||||
<HandlerInput
|
||||
selectedHandler={selectedHandler}
|
||||
handleModifyHandler={handleModifyHandler}
|
||||
fieldName="template"
|
||||
fieldDisplay="Template"
|
||||
placeholder=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default KafkaHandler
|
|
@ -4,6 +4,7 @@ import ExecHandler from './ExecHandler'
|
|||
import LogHandler from './LogHandler'
|
||||
import AlertaHandler from './AlertaHandler'
|
||||
import HipchatHandler from './HipchatHandler'
|
||||
import KafkaHandler from './KafkaHandler'
|
||||
import OpsgenieHandler from './OpsgenieHandler'
|
||||
import PagerdutyHandler from './PagerdutyHandler'
|
||||
import PushoverHandler from './PushoverHandler'
|
||||
|
@ -22,6 +23,7 @@ export {
|
|||
EmailHandler,
|
||||
AlertaHandler,
|
||||
HipchatHandler,
|
||||
KafkaHandler,
|
||||
OpsgenieHandler,
|
||||
PagerdutyHandler,
|
||||
PushoverHandler,
|
||||
|
|
|
@ -114,6 +114,7 @@ export const MAP_KEYS_FROM_CONFIG = {
|
|||
export const ALERTS_FROM_CONFIG = {
|
||||
alerta: ['environment', 'origin', 'token'], // token = bool
|
||||
hipChat: ['url', 'room', 'token'], // token = bool
|
||||
kafka: [],
|
||||
opsGenie: ['api-key', 'teams', 'recipients'], // api-key = bool
|
||||
opsGenie2: ['api-key', 'teams', 'recipients'], // api-key = bool
|
||||
pagerDuty: ['service-key'], // service-key = bool
|
||||
|
@ -172,6 +173,7 @@ export const HANDLERS_TO_RULE = {
|
|||
'service',
|
||||
],
|
||||
hipChat: ['room'],
|
||||
kafka: ['cluster', 'topic', 'template'],
|
||||
opsGenie: ['teams', 'recipients'],
|
||||
opsGenie2: ['teams', 'recipients'],
|
||||
pagerDuty: [],
|
||||
|
|
|
@ -5,7 +5,7 @@ import {bindActionCreators} from 'redux'
|
|||
|
||||
import {notify as notifyAction} from 'src/shared/actions/notifications'
|
||||
|
||||
import {Source} from 'src/types'
|
||||
import {Source, Notification, NotificationFunc} from 'src/types'
|
||||
|
||||
import {
|
||||
createKapacitor,
|
||||
|
@ -29,16 +29,6 @@ import {ErrorHandling} from 'src/shared/decorators/errors'
|
|||
export const defaultName = 'My Kapacitor'
|
||||
export const kapacitorPort = '9092'
|
||||
|
||||
export interface Notification {
|
||||
id?: string
|
||||
type: string
|
||||
icon: string
|
||||
duration: number
|
||||
message: string
|
||||
}
|
||||
|
||||
export type NotificationFunc = () => Notification
|
||||
|
||||
interface Kapacitor {
|
||||
url: string
|
||||
name: string
|
||||
|
|
|
@ -10,7 +10,14 @@ import {getActiveKapacitor} from 'src/shared/apis'
|
|||
import {getLogStreamByRuleID, pingKapacitorVersion} from 'src/kapacitor/apis'
|
||||
import {notify as notifyAction} from 'src/shared/actions/notifications'
|
||||
|
||||
import {Source, Kapacitor, Task, AlertRule} from 'src/types'
|
||||
import {
|
||||
Source,
|
||||
Kapacitor,
|
||||
Task,
|
||||
AlertRule,
|
||||
Notification,
|
||||
NotificationFunc,
|
||||
} from 'src/types'
|
||||
|
||||
import {
|
||||
notifyTickscriptLoggingUnavailable,
|
||||
|
@ -19,6 +26,12 @@ import {
|
|||
} from 'src/shared/copy/notifications'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface TaskResponse {
|
||||
id: number
|
||||
code: number
|
||||
message: string
|
||||
}
|
||||
|
||||
interface ErrorActions {
|
||||
errorThrown: (notify: string | object) => void
|
||||
}
|
||||
|
@ -34,13 +47,13 @@ interface KapacitorActions {
|
|||
ruleID: string,
|
||||
router: Router,
|
||||
sourceID: string
|
||||
) => void
|
||||
) => Promise<TaskResponse>
|
||||
createTask: (
|
||||
kapacitor: Kapacitor,
|
||||
task: Task,
|
||||
router: Router,
|
||||
sourceID: string
|
||||
) => void
|
||||
) => Promise<TaskResponse>
|
||||
getRule: (kapacitor: Kapacitor, ruleID: string) => void
|
||||
}
|
||||
|
||||
|
@ -55,7 +68,7 @@ interface Props {
|
|||
router: Router
|
||||
params: Params
|
||||
rules: AlertRule[]
|
||||
notify: any
|
||||
notify: (message: Notification | NotificationFunc) => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -166,28 +179,53 @@ export class TickscriptPage extends PureComponent<Props, State> {
|
|||
)
|
||||
}
|
||||
|
||||
private handleSave = async () => {
|
||||
private async updateTask(): Promise<TaskResponse> {
|
||||
const {kapacitor, task} = this.state
|
||||
const {
|
||||
source: {id: sourceID},
|
||||
router,
|
||||
kapacitorActions: {createTask, updateTask},
|
||||
kapacitorActions: {updateTask},
|
||||
params: {ruleID},
|
||||
} = this.props
|
||||
|
||||
let response
|
||||
return await updateTask(kapacitor, task, ruleID, router, sourceID)
|
||||
}
|
||||
|
||||
private async createTask(): Promise<TaskResponse> {
|
||||
const {kapacitor, task} = this.state
|
||||
|
||||
const {
|
||||
source: {id: sourceID},
|
||||
router,
|
||||
kapacitorActions: {createTask},
|
||||
} = this.props
|
||||
|
||||
return await createTask(kapacitor, task, router, sourceID)
|
||||
}
|
||||
|
||||
private async persist(): Promise<TaskResponse> {
|
||||
if (this.isEditing) {
|
||||
return await this.updateTask()
|
||||
} else {
|
||||
return await this.createTask()
|
||||
}
|
||||
}
|
||||
|
||||
private handleSave = async () => {
|
||||
const {
|
||||
source: {id: sourceID},
|
||||
router,
|
||||
} = this.props
|
||||
|
||||
try {
|
||||
if (this.isEditing) {
|
||||
response = await updateTask(kapacitor, task, ruleID, router, sourceID)
|
||||
} else {
|
||||
response = await createTask(kapacitor, task, router, sourceID)
|
||||
}
|
||||
const response = await this.persist()
|
||||
|
||||
if (response.code === 422) {
|
||||
this.setState({unsavedChanges: true, consoleMessage: response.message})
|
||||
return
|
||||
} else if (response.code) {
|
||||
}
|
||||
|
||||
if (response.code) {
|
||||
this.setState({unsavedChanges: true, consoleMessage: response.message})
|
||||
} else {
|
||||
this.setState({unsavedChanges: false, consoleMessage: ''})
|
||||
|
|
|
@ -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,
|
||||
})
|
|
@ -173,13 +173,13 @@ export function updateKapacitorConfigSection(kapacitor, section, properties) {
|
|||
return AJAX(params)
|
||||
}
|
||||
|
||||
export const testAlertOutput = async (kapacitor, outputName) => {
|
||||
export const testAlertOutput = async (kapacitor, outputName, options) => {
|
||||
try {
|
||||
const {
|
||||
data: {services},
|
||||
} = await kapacitorProxy(kapacitor, 'GET', '/kapacitor/v1/service-tests')
|
||||
const service = services.find(s => s.name === outputName)
|
||||
return kapacitorProxy(kapacitor, 'POST', service.link.href, {})
|
||||
return kapacitorProxy(kapacitor, 'POST', service.link.href, options)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue