diff --git a/CHANGELOG.md b/CHANGELOG.md index 49517decd..8785affed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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] diff --git a/kapacitor.go b/kapacitor.go index c5c3cc3ce..f6e3f0d4f 100644 --- a/kapacitor.go +++ b/kapacitor.go @@ -22,7 +22,8 @@ type AlertNodes struct { Alerta []*Alerta `json:"alerta"` // Alerta will send alert to all Alerta OpsGenie []*OpsGenie `json:"opsGenie"` // OpsGenie will send alert to all OpsGenie OpsGenie2 []*OpsGenie `json:"opsGenie2"` // OpsGenie2 will send alert to all OpsGenie v2 - Talk []*Talk `json:"talk"` // Talk will send alert to all Talk + Talk []*Talk `json:"talk"` // Talk will send alert to all Talk + Kafka []*Kafka `json:"kafka"` // Kafka will send alert to all Kafka } // Post will POST alerts to a destination URL @@ -135,6 +136,13 @@ type OpsGenie struct { // Talk sends alerts to Jane Talk (https://jianliao.com/site) type Talk struct{} +// Kafka sends alerts to any Kafka brokers specified in the handler config +type Kafka struct { + Cluster string `json:"cluster"` + Topic string `json:"topic"` + Template string `json:"template"` +} + // MarshalJSON converts AlertNodes to JSON func (n *AlertNodes) MarshalJSON() ([]byte, error) { type Alias AlertNodes diff --git a/kapacitor/client.go b/kapacitor/client.go index 2ceda3a38..3fdb0dad4 100644 --- a/kapacitor/client.go +++ b/kapacitor/client.go @@ -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), diff --git a/server/kapacitors.go b/server/kapacitors.go index a528ba894..97b581f6d 100644 --- a/server/kapacitors.go +++ b/server/kapacitors.go @@ -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{} } diff --git a/server/kapacitors_test.go b/server/kapacitors_test.go index f528a540a..b6c9f9150 100644 --- a/server/kapacitors_test.go +++ b/server/kapacitors_test.go @@ -132,6 +132,7 @@ func Test_KapacitorRulesGet(t *testing.T) { OpsGenie: []*chronograf.OpsGenie{}, OpsGenie2: []*chronograf.OpsGenie{}, Talk: []*chronograf.Talk{}, + Kafka: []*chronograf.Kafka{}, }, }, }, diff --git a/server/swagger.json b/server/swagger.json index 070d6df48..24c79e5fa 100644 --- a/server/swagger.json +++ b/server/swagger.json @@ -3677,6 +3677,7 @@ "post", "http", "hipchat", + "kafka", "opsgenie", "opsgenie2", "pagerduty", diff --git a/ui/package.json b/ui/package.json index aae5e61f9..d25494042 100644 --- a/ui/package.json +++ b/ui/package.json @@ -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", diff --git a/ui/src/CheckSources.tsx b/ui/src/CheckSources.tsx index dfe6d2abc..03fa5bd0e 100644 --- a/ui/src/CheckSources.tsx +++ b/ui/src/CheckSources.tsx @@ -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 } diff --git a/ui/src/admin/components/EmptyRow.js b/ui/src/admin/components/EmptyRow.js deleted file mode 100644 index af2179038..000000000 --- a/ui/src/admin/components/EmptyRow.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' - -const EmptyRow = ({tableName}) => ( - - -

- You don't have any {tableName},
why not create one? -

- - -) - -const {string} = PropTypes - -EmptyRow.propTypes = { - tableName: string.isRequired, -} - -export default EmptyRow diff --git a/ui/src/admin/components/EmptyRow.tsx b/ui/src/admin/components/EmptyRow.tsx new file mode 100644 index 000000000..664de5e7e --- /dev/null +++ b/ui/src/admin/components/EmptyRow.tsx @@ -0,0 +1,16 @@ +import React, {SFC} from 'react' + +interface Props { + tableName: string +} +const EmptyRow: SFC = ({tableName}) => ( + + +

+ You don't have any {tableName},
why not create one? +

+ + +) + +export default EmptyRow diff --git a/ui/src/admin/containers/chronograf/AllUsersPage.tsx b/ui/src/admin/containers/chronograf/AllUsersPage.tsx index 4f74b6683..8f42680e5 100644 --- a/ui/src/admin/containers/chronograf/AllUsersPage.tsx +++ b/ui/src/admin/containers/chronograf/AllUsersPage.tsx @@ -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[] diff --git a/ui/src/alerts/apis/index.js b/ui/src/alerts/apis/index.js deleted file mode 100644 index 4c41ce09e..000000000 --- a/ui/src/alerts/apis/index.js +++ /dev/null @@ -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', - }) diff --git a/ui/src/alerts/apis/index.ts b/ui/src/alerts/apis/index.ts new file mode 100644 index 000000000..4222e7fe7 --- /dev/null +++ b/ui/src/alerts/apis/index.ts @@ -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', + }) +} diff --git a/ui/src/alerts/components/AlertsTable.js b/ui/src/alerts/components/AlertsTable.tsx similarity index 75% rename from ui/src/alerts/components/AlertsTable.js rename to ui/src/alerts/components/AlertsTable.tsx index 72e7fd3c1..58f955ab7 100644 --- a/ui/src/alerts/components/AlertsTable.js +++ b/ui/src/alerts/components/AlertsTable.tsx @@ -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 { 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 ? ( +
+ {this.renderTable()} + {limit && alertsCount ? ( + + ) : null} +
+ ) : ( +
+
+

{this.props.alerts.length} Alerts

+ {this.props.alerts.length ? ( + + ) : null} +
+
{this.renderTable()}
+
+ ) + } + + 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(alerts, e => e[key]) + case Direction.DESC: + return _.sortBy(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 { ) } - - render() { - const { - shouldNotBeFilterable, - limit, - onGetMoreAlerts, - isAlertsMaxedOut, - alertsCount, - } = this.props - - return shouldNotBeFilterable ? ( -
- {this.renderTable()} - {limit && alertsCount ? ( - - ) : null} -
- ) : ( -
-
-

{this.props.alerts.length} Alerts

- {this.props.alerts.length ? ( - - ) : null} -
-
{this.renderTable()}
-
- ) - } -} - -@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 ( -
- - -
- ) - } -} - -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 diff --git a/ui/src/alerts/components/SearchBar.tsx b/ui/src/alerts/components/SearchBar.tsx new file mode 100644 index 000000000..ce27e9364 --- /dev/null +++ b/ui/src/alerts/components/SearchBar.tsx @@ -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 { + constructor(props) { + super(props) + + this.state = { + searchTerm: '', + } + } + + public componentWillMount() { + const waitPeriod = 300 + this.handleSearch = _.debounce(this.handleSearch, waitPeriod) + } + + public render() { + return ( +
+ + +
+ ) + } + + private handleSearch = (): void => { + this.props.onSearch(this.state.searchTerm) + } + + private handleChange = (e: ChangeEvent): void => { + this.setState({searchTerm: e.target.value}, this.handleSearch) + } +} + +export default SearchBar diff --git a/ui/src/alerts/constants/tableSizing.js b/ui/src/alerts/constants/tableSizing.ts similarity index 100% rename from ui/src/alerts/constants/tableSizing.js rename to ui/src/alerts/constants/tableSizing.ts diff --git a/ui/src/alerts/containers/AlertsApp.js b/ui/src/alerts/containers/AlertsApp.tsx similarity index 80% rename from ui/src/alerts/containers/AlertsApp.js rename to ui/src/alerts/containers/AlertsApp.tsx index 31af501a3..20b721834 100644 --- a/ui/src/alerts/containers/AlertsApp.js +++ b/ui/src/alerts/containers/AlertsApp.tsx @@ -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 { 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
+ } + + return isWidget ? ( + this.renderSubComponents() + ) : ( +
+
+
+
+

Alert History

+
+
+ + +
+
+
+
+
+
+
{this.renderSubComponents()}
+
+
+
+
+ ) + } + + 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
- } - - return isWidget ? ( - this.renderSubComponents() - ) : ( -
-
-
-
-

Alert History

-
-
- - -
-
-
-
-
-
-
{this.renderSubComponents()}
-
-
-
-
- ) - } -} - -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 diff --git a/ui/src/alerts/index.js b/ui/src/alerts/index.ts similarity index 100% rename from ui/src/alerts/index.js rename to ui/src/alerts/index.ts diff --git a/ui/src/dashboards/components/CellEditorOverlay.tsx b/ui/src/dashboards/components/CellEditorOverlay.tsx index c22680c91..7c218d682 100644 --- a/ui/src/dashboards/components/CellEditorOverlay.tsx +++ b/ui/src/dashboards/components/CellEditorOverlay.tsx @@ -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 +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 { 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 { } private get queryActions(): CellEditorOverlayActions { - const original = { - editRawTextAsync: () => Promise.resolve(), - ...queryModifiers, - } - const mapped = _.reduce( - 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, } diff --git a/ui/src/dashboards/components/QueryMaker.tsx b/ui/src/dashboards/components/QueryMaker.tsx index 64ada3487..13e4fcecc 100644 --- a/ui/src/dashboards/components/QueryMaker.tsx +++ b/ui/src/dashboards/components/QueryMaker.tsx @@ -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, diff --git a/ui/src/dashboards/components/Threshold.tsx b/ui/src/dashboards/components/Threshold.tsx index 5a521761c..9c5285d2e 100644 --- a/ui/src/dashboards/components/Threshold.tsx +++ b/ui/src/dashboards/components/Threshold.tsx @@ -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 { } 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 {
) } - 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 { diff --git a/ui/src/data_explorer/actions/view/index.js b/ui/src/data_explorer/actions/view/index.js deleted file mode 100644 index a0071e9fe..000000000 --- a/ui/src/data_explorer/actions/view/index.js +++ /dev/null @@ -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)) - } -} diff --git a/ui/src/data_explorer/actions/view/index.ts b/ui/src/data_explorer/actions/view/index.ts new file mode 100644 index 000000000..4b0cfa24b --- /dev/null +++ b/ui/src/data_explorer/actions/view/index.ts @@ -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 => { + 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)) + } +} diff --git a/ui/src/data_explorer/actions/view/write.js b/ui/src/data_explorer/actions/view/write.ts similarity index 57% rename from ui/src/data_explorer/actions/view/write.js rename to ui/src/data_explorer/actions/view/write.ts index 2dca304ae..364a9b574 100644 --- a/ui/src/data_explorer/actions/view/write.js +++ b/ui/src/data_explorer/actions/view/write.ts @@ -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 => { try { await writeLineProtocolAJAX(source, db, data) dispatch(notify(notifyDataWritten())) diff --git a/ui/src/data_explorer/apis/index.js b/ui/src/data_explorer/apis/index.js deleted file mode 100644 index 0abe191a6..000000000 --- a/ui/src/data_explorer/apis/index.js +++ /dev/null @@ -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, - }) diff --git a/ui/src/data_explorer/apis/index.ts b/ui/src/data_explorer/apis/index.ts new file mode 100644 index 000000000..d93335455 --- /dev/null +++ b/ui/src/data_explorer/apis/index.ts @@ -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 => + 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}` +} diff --git a/ui/src/data_explorer/components/FieldListItem.tsx b/ui/src/data_explorer/components/FieldListItem.tsx index 59f3a9ceb..c15b1a7b6 100644 --- a/ui/src/data_explorer/components/FieldListItem.tsx +++ b/ui/src/data_explorer/components/FieldListItem.tsx @@ -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 diff --git a/ui/src/data_explorer/components/QueryEditor.js b/ui/src/data_explorer/components/QueryEditor.tsx similarity index 61% rename from ui/src/data_explorer/components/QueryEditor.js rename to ui/src/data_explorer/components/QueryEditor.tsx index 220f5b609..0f64bdec8 100644 --- a/ui/src/data_explorer/components/QueryEditor.js +++ b/ui/src/data_explorer/components/QueryEditor.tsx @@ -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 { + private editor: React.RefObject + constructor(props) { super(props) this.state = { value: this.props.query, } + + this.editor = React.createRef() } - 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 ( +
+