Merge master

chore/tables-to-TS
ebb-tide 2018-05-14 13:08:51 -07:00
parent c665bc9618
commit 0f5bd2de54
159 changed files with 5213 additions and 3022 deletions

View File

@ -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]

View File

@ -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

View File

@ -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),

View File

@ -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{}
}

View File

@ -132,6 +132,7 @@ func Test_KapacitorRulesGet(t *testing.T) {
OpsGenie: []*chronograf.OpsGenie{},
OpsGenie2: []*chronograf.OpsGenie{},
Talk: []*chronograf.Talk{},
Kafka: []*chronograf.Kafka{},
},
},
},

View File

@ -3677,6 +3677,7 @@
"post",
"http",
"hipchat",
"kafka",
"opsgenie",
"opsgenie2",
"pagerduty",

View File

@ -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",

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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[]

View File

@ -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',
})

View File

@ -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',
})
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,
}

View File

@ -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,

View File

@ -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 {

View File

@ -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))
}
}

View File

@ -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))
}
}

View File

@ -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()))

View File

@ -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,
})

View File

@ -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}`
}

View File

@ -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

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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? -&nbsp;
<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

View File

@ -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? -&nbsp;
<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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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'},
]

View File

@ -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'

View File

@ -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

View File

@ -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 {

View File

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

View File

@ -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

View File

@ -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': {

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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}]

View File

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

View File

@ -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

View File

@ -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

View File

@ -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: {

View File

@ -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

View File

@ -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
}
}

View File

@ -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) {

View File

@ -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}

View File

@ -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 />,
},
]
}
}

View File

@ -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

View File

@ -1 +1,2 @@
export const FROM = 'from'
export const FILTER = 'filter'

View File

@ -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"))`,
}
}

View File

@ -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,
},
})

View File

@ -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,
},
})

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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">

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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,

View File

@ -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: [],

View File

@ -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

View File

@ -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: ''})

View File

@ -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,
})

View File

@ -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)
}

View File

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

View File

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

View File

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

View File

@ -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})
}
}

View File

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

View File

@ -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%)`,
})
}

View File

@ -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

View File

@ -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