Merge branch 'master' of github.com:influxdata/chronograf

pull/10616/head
Jared Scheib 2018-05-14 14:53:59 -07:00
commit 4ca38fee6f
102 changed files with 3364 additions and 2471 deletions

View File

@ -339,7 +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 {

View File

@ -147,7 +147,7 @@
"react-grid-layout": "^0.16.6",
"react-onclickoutside": "^5.2.0",
"react-redux": "^4.4.0",
"react-resizable": "^1.7.5",
"react-resize-detector": "^2.3.0",
"react-router": "^3.0.2",
"react-router-redux": "^4.0.8",
"react-tooltip": "^3.2.1",

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

@ -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 componentDidMount() {
const {source} = this.props
AJAX({
url: source.links.kapacitors,
@ -59,13 +78,49 @@ class AlertsApp extends Component {
})
}
componentDidUpdate(prevProps, prevState) {
public componentDidUpdate(__, prevState) {
if (!_.isEqual(prevState.timeRange, this.state.timeRange)) {
this.fetchAlerts()
}
}
public render() {
const {isWidget, source} = this.props
const {loading, timeRange} = this.state
fetchAlerts = () => {
if (loading || !source) {
return <div className="page-spinner" />
}
return isWidget ? (
this.renderSubComponents()
) : (
<div className="page alert-history-page">
<div className="page-header">
<div className="page-header__container">
<div className="page-header__left">
<h1 className="page-header__title">Alert History</h1>
</div>
<div className="page-header__right">
<SourceIndicator />
<CustomTimeRangeDropdown
onApplyTimeRange={this.handleApplyTime}
timeRange={timeRange}
/>
</div>
</div>
</div>
<div className="page-contents">
<div className="container-fluid">
<div className="row">
<div className="col-md-12">{this.renderSubComponents()}</div>
</div>
</div>
</div>
</div>
)
}
private fetchAlerts = (): void => {
getAlerts(
this.props.source.links.proxy,
this.state.timeRange,
@ -112,13 +167,13 @@ class AlertsApp extends Component {
})
}
handleGetMoreAlerts = () => {
private handleGetMoreAlerts = (): void => {
this.setState({limitMultiplier: this.state.limitMultiplier + 1}, () => {
this.fetchAlerts(this.state.limitMultiplier)
this.fetchAlerts()
})
}
renderSubComponents = () => {
private renderSubComponents = (): JSX.Element => {
const {source, isWidget, limit} = this.props
const {isAlertsMaxedOut, alerts} = this.state
@ -137,65 +192,9 @@ class AlertsApp extends Component {
)
}
handleApplyTime = timeRange => {
private handleApplyTime = (timeRange: TimeRange): void => {
this.setState({timeRange})
}
render() {
const {isWidget, source} = this.props
const {loading, timeRange} = this.state
if (loading || !source) {
return <div className="page-spinner" />
}
return isWidget ? (
this.renderSubComponents()
) : (
<div className="page alert-history-page">
<div className="page-header">
<div className="page-header__container">
<div className="page-header__left">
<h1 className="page-header__title">Alert History</h1>
</div>
<div className="page-header__right">
<SourceIndicator />
<CustomTimeRangeDropdown
onApplyTimeRange={this.handleApplyTime}
timeRange={timeRange}
/>
</div>
</div>
</div>
<div className="page-contents">
<div className="container-fluid">
<div className="row">
<div className="col-md-12">{this.renderSubComponents()}</div>
</div>
</div>
</div>
</div>
)
}
}
const {bool, number, oneOfType, shape, string} = PropTypes
AlertsApp.propTypes = {
source: shape({
id: string.isRequired,
name: string.isRequired,
type: string, // 'influx-enterprise'
links: shape({
proxy: string.isRequired,
}).isRequired,
}),
timeRange: shape({
lower: string.isRequired,
upper: oneOfType([shape(), string]),
}),
isWidget: bool,
limit: number,
}
export default AlertsApp

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

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

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

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

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

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

View File

@ -1,26 +1,43 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import Dropdown from 'shared/components/Dropdown'
import Dropdown from 'src/shared/components/Dropdown'
import {showDatabases} from 'shared/apis/metaQuery'
import parsers from 'shared/parsing'
import {showDatabases} from 'src/shared/apis/metaQuery'
import parsers from 'src/shared/parsing'
import {Source} from 'src/types/sources'
import {ErrorHandling} from 'src/shared/decorators/errors'
const {databases: showDatabasesParser} = parsers
interface Database {
text: string
}
interface Props {
database: string
onSelectDatabase: (database: Database) => void
onStartEdit?: () => void
onErrorThrown: (error: string) => void
source: Source
}
interface State {
databases: Database[]
}
@ErrorHandling
class DatabaseDropdown extends Component {
class DatabaseDropdown extends Component<Props, State> {
constructor(props) {
super(props)
this.state = {
databases: [],
}
}
componentDidMount() {
this._getDatabases()
public componentDidMount() {
this.getDatabasesAsync()
}
render() {
public render() {
const {databases} = this.state
const {database, onSelectDatabase, onStartEdit} = this.props
@ -30,7 +47,9 @@ class DatabaseDropdown extends Component {
return (
<Dropdown
items={databases.map(text => ({text}))}
items={databases.map(text => ({
text,
}))}
selected={database || 'Loading...'}
onChoose={onSelectDatabase}
onClick={onStartEdit ? onStartEdit : null}
@ -38,7 +57,7 @@ class DatabaseDropdown extends Component {
)
}
_getDatabases = async () => {
private getDatabasesAsync = async (): Promise<void> => {
const {source, database, onSelectDatabase, onErrorThrown} = this.props
const proxy = source.links.proxy
try {
@ -50,11 +69,15 @@ class DatabaseDropdown extends Component {
const nonSystemDatabases = databases.filter(name => name !== '_internal')
this.setState({databases: nonSystemDatabases})
this.setState({
databases: nonSystemDatabases,
})
const selectedDatabaseText = nonSystemDatabases.includes(database)
? database
: nonSystemDatabases[0] || 'No databases'
onSelectDatabase({text: selectedDatabaseText})
onSelectDatabase({
text: selectedDatabaseText,
})
} catch (error) {
console.error(error)
onErrorThrown(error)
@ -62,18 +85,4 @@ class DatabaseDropdown extends Component {
}
}
const {func, shape, string} = PropTypes
DatabaseDropdown.propTypes = {
database: string,
onSelectDatabase: func.isRequired,
onStartEdit: func,
onErrorThrown: func.isRequired,
source: shape({
links: shape({
proxy: string.isRequired,
}).isRequired,
}).isRequired,
}
export default DatabaseDropdown

View File

@ -4,6 +4,7 @@ import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import _ from 'lodash'
import NanoDate from 'nano-date'
import ReactResizeDetector from 'react-resize-detector'
import Dygraphs from 'src/external/dygraph'
import DygraphLegend from 'src/shared/components/DygraphLegend'
@ -353,6 +354,11 @@ class Dygraph extends Component {
/>
)}
{nestedGraph && React.cloneElement(nestedGraph, {staticLegendHeight})}
<ReactResizeDetector
handleWidth={true}
handleHeight={true}
onResize={this.resize}
/>
</div>
)
}

View File

@ -1,7 +1,10 @@
import React from 'react'
import PropTypes from 'prop-types'
import React, {SFC} from 'react'
const EmptyQueryState = ({onAddQuery}) => (
interface Props {
onAddQuery: () => void
}
const EmptyQueryState: SFC<Props> = ({onAddQuery}) => (
<div className="query-maker--empty">
<h5>This Graph has no Queries</h5>
<br />
@ -11,10 +14,4 @@ const EmptyQueryState = ({onAddQuery}) => (
</div>
)
const {func} = PropTypes
EmptyQueryState.propTypes = {
onAddQuery: func.isRequired,
}
export default EmptyQueryState

View File

@ -1,12 +1,21 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import React, {PureComponent, MouseEvent} from 'react'
import classnames from 'classnames'
import _ from 'lodash'
import {INFLUXQL_FUNCTIONS} from 'src/data_explorer/constants'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
onApply: (item: string[]) => void
selectedItems: string[]
singleSelect: boolean
}
interface State {
localSelectedItems: string[]
}
@ErrorHandling
class FunctionSelector extends Component {
class FunctionSelector extends PureComponent<Props, State> {
constructor(props) {
super(props)
@ -15,60 +24,20 @@ class FunctionSelector extends Component {
}
}
componentWillUpdate(nextProps) {
public componentWillUpdate(nextProps) {
if (!_.isEqual(this.props.selectedItems, nextProps.selectedItems)) {
this.setState({localSelectedItems: nextProps.selectedItems})
}
}
onSelect = (item, e) => {
e.stopPropagation()
const {localSelectedItems} = this.state
let nextItems
if (this.isSelected(item)) {
nextItems = localSelectedItems.filter(i => i !== item)
} else {
nextItems = [...localSelectedItems, item]
}
this.setState({localSelectedItems: nextItems})
}
onSingleSelect = item => {
if (item === this.state.localSelectedItems[0]) {
this.props.onApply([])
this.setState({localSelectedItems: []})
} else {
this.props.onApply([item])
this.setState({localSelectedItems: [item]})
}
}
isSelected = item => {
return !!this.state.localSelectedItems.find(text => text === item)
}
handleApplyFunctions = e => {
e.stopPropagation()
this.props.onApply(this.state.localSelectedItems)
}
render() {
const {localSelectedItems} = this.state
public render() {
const {singleSelect} = this.props
return (
<div className="function-selector">
{singleSelect ? null : (
{!singleSelect && (
<div className="function-selector--header">
<span>
{localSelectedItems.length > 0
? `${localSelectedItems.length} Selected`
: 'Select functions below'}
</span>
<span>{this.headerText}</span>
<div
className="btn btn-xs btn-success"
onClick={this.handleApplyFunctions}
@ -100,14 +69,49 @@ class FunctionSelector extends Component {
</div>
)
}
}
const {arrayOf, bool, func, string} = PropTypes
private get headerText(): string {
const numItems = this.state.localSelectedItems.length
if (!numItems) {
return 'Select functions below'
}
FunctionSelector.propTypes = {
onApply: func.isRequired,
selectedItems: arrayOf(string.isRequired).isRequired,
singleSelect: bool,
return `${numItems} Selected`
}
private onSelect = (item: string, e: MouseEvent<HTMLDivElement>): void => {
e.stopPropagation()
const {localSelectedItems} = this.state
let nextItems
if (this.isSelected(item)) {
nextItems = localSelectedItems.filter(i => i !== item)
} else {
nextItems = [...localSelectedItems, item]
}
this.setState({localSelectedItems: nextItems})
}
private onSingleSelect = (item: string): void => {
if (item === this.state.localSelectedItems[0]) {
this.props.onApply([])
this.setState({localSelectedItems: []})
} else {
this.props.onApply([item])
this.setState({localSelectedItems: [item]})
}
}
private isSelected = (item: string): boolean => {
return !!this.state.localSelectedItems.find(text => text === item)
}
private handleApplyFunctions = (e: MouseEvent<HTMLDivElement>): void => {
e.stopPropagation()
this.props.onApply(this.state.localSelectedItems)
}
}
export default FunctionSelector

View File

@ -1,7 +1,8 @@
import React, {PureComponent} from 'react'
import _ from 'lodash'
import getLastValues, {TimeSeriesResponse} from 'src/shared/parsing/lastValues'
import Gauge from 'src/shared/components/Gauge'
import _ from 'lodash'
import {DEFAULT_GAUGE_COLORS} from 'src/shared/constants/thresholds'
import {stringifyColorValues} from 'src/shared/constants/colorOperations'
@ -25,10 +26,6 @@ interface Props {
prefix: string
suffix: string
resizerTopHeight?: number
resizeCoords?: {
i: string
h: number
}
}
@ErrorHandling
@ -52,11 +49,11 @@ class GaugeChart extends PureComponent<Props> {
<div className="single-stat">
<Gauge
width="900"
height={this.height}
colors={colors}
gaugePosition={this.lastValueForGauge}
height={this.height}
prefix={prefix}
suffix={suffix}
gaugePosition={this.lastValueForGauge}
/>
</div>
)
@ -65,22 +62,7 @@ class GaugeChart extends PureComponent<Props> {
private get height(): string {
const {resizerTopHeight} = this.props
return (
this.resizeCoordsHeight ||
this.initialCellHeight ||
resizerTopHeight ||
300
).toString()
}
private get resizeCoordsHeight(): string {
const {resizeCoords} = this.props
if (resizeCoords && this.isResizing) {
return (resizeCoords.h * DASHBOARD_LAYOUT_ROW_HEIGHT).toString()
}
return null
return (this.initialCellHeight || resizerTopHeight || 300).toString()
}
private get initialCellHeight(): string {
@ -93,11 +75,6 @@ class GaugeChart extends PureComponent<Props> {
return null
}
private get isResizing(): boolean {
const {resizeCoords, cellID} = this.props
return resizeCoords ? cellID === resizeCoords.i : false
}
private get lastValueForGauge(): number {
const {data} = this.props
const {lastValues} = getLastValues(data)

View File

@ -69,7 +69,6 @@ const Layout = (
autoRefresh,
manualRefresh,
onDeleteCell,
resizeCoords,
onCancelEditCell,
onStopAddAnnotation,
onSummonOverlayTechnologies,
@ -110,7 +109,6 @@ const Layout = (
manualRefresh={manualRefresh}
onStopAddAnnotation={onStopAddAnnotation}
grabDataForDownload={grabDataForDownload}
resizeCoords={resizeCoords}
queries={buildQueriesForLayouts(
cell,
getSource(cell, source, sources, defaultSource),
@ -191,7 +189,6 @@ const propTypes = {
isStatusPage: bool,
isEditable: bool,
onCancelEditCell: func,
resizeCoords: shape(),
onZoom: func,
sources: arrayOf(shape()),
}

View File

@ -1,7 +1,6 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import ReactGridLayout, {WidthProvider} from 'react-grid-layout'
import {ResizableBox} from 'react-resizable'
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
@ -26,7 +25,6 @@ class LayoutRenderer extends Component {
this.state = {
rowHeight: this.calculateRowHeight(),
resizeCoords: null,
}
}
@ -36,8 +34,16 @@ class LayoutRenderer extends Component {
}
const newCells = this.props.cells.map(cell => {
const l = layout.find(ly => ly.i === cell.i)
const newLayout = {x: l.x, y: l.y, h: l.h, w: l.w}
return {...cell, ...newLayout}
const newLayout = {
x: l.x,
y: l.y,
h: l.h,
w: l.w,
}
return {
...cell,
...newLayout,
}
})
this.props.onPositionChange(newCells)
}
@ -56,10 +62,6 @@ class LayoutRenderer extends Component {
: DASHBOARD_LAYOUT_ROW_HEIGHT
}
handleCellResize = () => {
this.resizeCoords = this.setState({resizeCoords: new Date()})
}
render() {
const {
host,
@ -79,15 +81,10 @@ class LayoutRenderer extends Component {
onSummonOverlayTechnologies,
} = this.props
const {rowHeight, resizeCoords} = this.state
const {rowHeight} = this.state
const isDashboard = !!this.props.onPositionChange
return (
<ResizableBox
height={Infinity}
width={Infinity}
onResize={this.handleCellResize}
>
<Authorized
requiredRole={EDITOR_ROLE}
propsOverride={{
@ -129,7 +126,6 @@ class LayoutRenderer extends Component {
isEditable={isEditable}
onEditCell={onEditCell}
autoRefresh={autoRefresh}
resizeCoords={resizeCoords}
onDeleteCell={onDeleteCell}
onCloneCell={onCloneCell}
manualRefresh={manualRefresh}
@ -142,7 +138,6 @@ class LayoutRenderer extends Component {
))}
</GridLayout>
</Authorized>
</ResizableBox>
)
}
}

View File

@ -65,7 +65,6 @@ class LineGraph extends Component {
cellHeight,
ruleValues,
isBarGraph,
resizeCoords,
isRefreshing,
setResolution,
isGraphFilled,
@ -127,7 +126,6 @@ class LineGraph extends Component {
isBarGraph={isBarGraph}
timeSeries={timeSeries}
ruleValues={ruleValues}
resizeCoords={resizeCoords}
dygraphSeries={dygraphSeries}
setResolution={setResolution}
handleSetHoverTime={handleSetHoverTime}
@ -211,7 +209,6 @@ LineGraph.propTypes = {
setResolution: func,
cellHeight: number,
onZoom: func,
resizeCoords: shape(),
queries: arrayOf(shape({}).isRequired).isRequired,
data: arrayOf(shape({}).isRequired).isRequired,
colors: colorsStringSchema,

View File

@ -1,24 +1,21 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import ColorScaleDropdown from 'shared/components/ColorScaleDropdown'
import ColorScaleDropdown from 'src/shared/components/ColorScaleDropdown'
import {updateLineColors} from 'src/dashboards/actions/cellEditorOverlay'
import {colorsStringSchema} from 'shared/schemas'
import {ColorNumber} from 'src/types/colors'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
lineColors: ColorNumber[]
handleUpdateLineColors: (colors: ColorNumber[]) => void
}
@ErrorHandling
class LineGraphColorSelector extends Component {
handleSelectColors = colorScale => {
const {handleUpdateLineColors} = this.props
const {colors} = colorScale
handleUpdateLineColors(colors)
}
render() {
class LineGraphColorSelector extends Component<Props> {
public render() {
const {lineColors} = this.props
return (
@ -32,13 +29,13 @@ class LineGraphColorSelector extends Component {
</div>
)
}
}
const {func} = PropTypes
public handleSelectColors = (colorScale): void => {
const {handleUpdateLineColors} = this.props
const {colors} = colorScale
LineGraphColorSelector.propTypes = {
lineColors: colorsStringSchema.isRequired,
handleUpdateLineColors: func.isRequired,
handleUpdateLineColors(colors)
}
}
const mapStateToProps = ({cellEditorOverlay: {lineColors}}) => ({

View File

@ -1,18 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
const LoadingDots = ({className}) => (
<div className={`loading-dots ${className}`}>
<div />
<div />
<div />
</div>
)
const {string} = PropTypes
LoadingDots.propTypes = {
className: string,
}
export default LoadingDots

View File

@ -0,0 +1,24 @@
import React, {Component} from 'react'
interface Props {
className?: string
}
class LoadingDots extends Component<Props> {
public static defaultProps: Partial<Props> = {
className: '',
}
public render() {
const {className} = this.props
return (
<div className={`loading-dots ${className}`}>
<div />
<div />
<div />
</div>
)
}
}
export default LoadingDots

View File

@ -3,7 +3,6 @@ import classnames from 'classnames'
import React, {PureComponent, MouseEvent} from 'react'
import TagList from 'src/shared/components/TagList'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {QueryConfig, Tag} from 'src/types'
interface SourceLinks {

View File

@ -1,14 +1,42 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import React, {Component, ChangeEvent, KeyboardEvent, MouseEvent} from 'react'
import classnames from 'classnames'
import uuid from 'uuid'
import ClickOutsideInput from 'shared/components/ClickOutsideInput'
import ClickOutsideInput from 'src/shared/components/ClickOutsideInput'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
min: string
fixedPlaceholder: string
fixedValue: string
customPlaceholder: string
customValue: string
onSetValue: (value: string) => void
type: string | number
}
interface State {
fixedValue: string
customValue: string
useCustomValue: boolean
}
@ErrorHandling
class OptIn extends Component {
export default class OptIn extends Component<Props, State> {
public static defaultProps: Partial<Props> = {
fixedValue: '',
customPlaceholder: 'Custom Value',
fixedPlaceholder: 'auto',
customValue: '',
}
private id: string
private isCustomValueInputFocused: boolean
private grooveKnobContainer: HTMLElement
private grooveKnob: HTMLElement
private customValueInput: HTMLInputElement
constructor(props) {
super(props)
@ -24,84 +52,7 @@ class OptIn extends Component {
this.isCustomValueInputFocused = false
}
useFixedValue = () => {
this.setState({useCustomValue: false, customValue: ''}, () =>
this.setValue()
)
}
useCustomValue = () => {
this.setState({useCustomValue: true}, () => this.setValue())
}
handleClickToggle = () => {
const useCustomValueNext = !this.state.useCustomValue
if (useCustomValueNext) {
this.useCustomValue()
this.customValueInput.focus()
} else {
this.useFixedValue()
}
}
handleFocusCustomValueInput = () => {
this.isCustomValueInputFocused = true
this.useCustomValue()
}
handleChangeCustomValue = e => {
this.setCustomValue(e.target.value)
}
handleKeyDownCustomValueInput = e => {
if (e.key === 'Enter' || e.key === 'Tab') {
if (e.key === 'Enter') {
this.customValueInput.blur()
}
this.considerResetCustomValue()
}
}
handleClickOutsideInput = e => {
if (
e.target.id !== this.grooveKnob.id &&
e.target.id !== this.grooveKnobContainer.id &&
this.isCustomValueInputFocused
) {
this.considerResetCustomValue()
}
}
considerResetCustomValue = () => {
const customValue = this.customValueInput.value.trim()
this.setState({customValue})
if (customValue === '') {
this.useFixedValue()
}
this.isCustomValueInputFocused = false
}
setCustomValue = value => {
this.setState({customValue: value}, this.setValue)
}
setValue = () => {
const {onSetValue} = this.props
const {useCustomValue, fixedValue, customValue} = this.state
if (useCustomValue) {
onSetValue(customValue)
} else {
onSetValue(fixedValue)
}
}
handleInputRef = el => (this.customValueInput = el)
render() {
public render() {
const {fixedPlaceholder, customPlaceholder, type, min} = this.props
const {useCustomValue, customValue} = this.state
@ -143,25 +94,86 @@ class OptIn extends Component {
</div>
)
}
private useFixedValue = (): void => {
this.setState({useCustomValue: false, customValue: ''}, () =>
this.setValue()
)
}
private useCustomValue = (): void => {
this.setState({useCustomValue: true}, () => this.setValue())
}
private handleClickToggle = (): void => {
const useCustomValueNext = !this.state.useCustomValue
if (useCustomValueNext) {
this.useCustomValue()
this.customValueInput.focus()
} else {
this.useFixedValue()
}
}
private handleFocusCustomValueInput = (): void => {
this.isCustomValueInputFocused = true
this.useCustomValue()
}
private handleChangeCustomValue = (
e: ChangeEvent<HTMLInputElement>
): void => {
this.setCustomValue(e.target.value)
}
private handleKeyDownCustomValueInput = (
e: KeyboardEvent<HTMLInputElement>
): void => {
if (e.key === 'Enter' || e.key === 'Tab') {
if (e.key === 'Enter') {
this.customValueInput.blur()
}
this.considerResetCustomValue()
}
}
private handleClickOutsideInput = (e: MouseEvent<HTMLElement>): void => {
if (
e.currentTarget.id !== this.grooveKnob.id &&
e.currentTarget.id !== this.grooveKnobContainer.id &&
this.isCustomValueInputFocused
) {
this.considerResetCustomValue()
}
}
private considerResetCustomValue = (): void => {
const customValue = this.customValueInput.value.trim()
this.setState({customValue})
if (customValue === '') {
this.useFixedValue()
}
this.isCustomValueInputFocused = false
}
private setCustomValue = (value): void => {
this.setState({customValue: value}, this.setValue)
}
private setValue = (): void => {
const {onSetValue} = this.props
const {useCustomValue, fixedValue, customValue} = this.state
if (useCustomValue) {
onSetValue(customValue)
} else {
onSetValue(fixedValue)
}
}
private handleInputRef = (el: HTMLInputElement) =>
(this.customValueInput = el)
}
OptIn.defaultProps = {
fixedValue: '',
customPlaceholder: 'Custom Value',
fixedPlaceholder: 'auto',
customValue: '',
}
const {func, oneOf, string} = PropTypes
OptIn.propTypes = {
min: string,
fixedPlaceholder: string,
fixedValue: string,
customPlaceholder: string,
customValue: string,
onSetValue: func.isRequired,
type: oneOf(['text', 'number']),
}
export default OptIn

View File

@ -1,9 +1,14 @@
import React from 'react'
import PropTypes from 'prop-types'
import LoadingDots from 'shared/components/LoadingDots'
import React, {SFC, ReactNode} from 'react'
import LoadingDots from 'src/shared/components/LoadingDots'
import classnames from 'classnames'
import {Status} from 'src/types'
const QueryStatus = ({status, children}) => {
interface Props {
status: Status
children: ReactNode
}
const QueryStatus: SFC<Props> = ({status, children}) => {
if (!status) {
return <div className="query-editor--status">{children}</div>
}
@ -11,7 +16,7 @@ const QueryStatus = ({status, children}) => {
if (status.loading) {
return (
<div className="query-editor--status">
<LoadingDots />
<LoadingDots className="query-editor--loading" />
{children}
</div>
)
@ -40,15 +45,4 @@ const QueryStatus = ({status, children}) => {
)
}
const {node, shape, string} = PropTypes
QueryStatus.propTypes = {
status: shape({
error: string,
success: string,
warn: string,
}),
children: node,
}
export default QueryStatus

View File

@ -1,8 +1,12 @@
import React from 'react'
import PropTypes from 'prop-types'
import React, {SFC} from 'react'
import ReactTooltip from 'react-tooltip'
const QuestionMarkTooltip = ({tipID, tipContent}) => (
interface Props {
tipID: string
tipContent: string
}
const QuestionMarkTooltip: SFC<Props> = ({tipID, tipContent}) => (
<div className="question-mark-tooltip">
<div
className="question-mark-tooltip--icon"
@ -21,11 +25,4 @@ const QuestionMarkTooltip = ({tipID, tipContent}) => (
</div>
)
const {string} = PropTypes
QuestionMarkTooltip.propTypes = {
tipID: string.isRequired,
tipContent: string.isRequired,
}
export default QuestionMarkTooltip

View File

@ -43,7 +43,6 @@ const RefreshingGraph = ({
resizerTopHeight,
staticLegend,
manualRefresh, // when changed, re-mounts the component
resizeCoords,
editQueryStatus,
handleSetHoverTime,
grabDataForDownload,
@ -89,7 +88,6 @@ const RefreshingGraph = ({
cellHeight={cellHeight}
resizerTopHeight={resizerTopHeight}
editQueryStatus={editQueryStatus}
resizeCoords={resizeCoords}
cellID={cellID}
prefix={prefix}
suffix={suffix}
@ -111,7 +109,6 @@ const RefreshingGraph = ({
templates={templates}
autoRefresh={autoRefresh}
cellHeight={cellHeight}
resizeCoords={resizeCoords}
tableOptions={tableOptions}
fieldOptions={fieldOptions}
timeFormat={timeFormat}
@ -144,7 +141,6 @@ const RefreshingGraph = ({
timeRange={timeRange}
autoRefresh={autoRefresh}
isBarGraph={type === 'bar'}
resizeCoords={resizeCoords}
staticLegend={staticLegend}
displayOptions={displayOptions}
editQueryStatus={editQueryStatus}
@ -172,7 +168,6 @@ RefreshingGraph.propTypes = {
editQueryStatus: func,
staticLegend: bool,
onZoom: func,
resizeCoords: shape(),
grabDataForDownload: func,
colors: colorsStringSchema,
cellID: string,

View File

@ -1,98 +0,0 @@
import React, {PureComponent} from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import getLastValues from 'shared/parsing/lastValues'
import _ from 'lodash'
import {SMALL_CELL_HEIGHT} from 'shared/graphs/helpers'
import {DYGRAPH_CONTAINER_V_MARGIN} from 'shared/constants'
import {generateThresholdsListHexs} from 'shared/constants/colorOperations'
import {colorsStringSchema} from 'shared/schemas'
import {ErrorHandling} from 'src/shared/decorators/errors'
@ErrorHandling
class SingleStat extends PureComponent {
render() {
const {
data,
cellHeight,
isFetchingInitially,
colors,
prefix,
suffix,
lineGraph,
staticLegendHeight,
} = this.props
// If data for this graph is being fetched for the first time, show a graph-wide spinner.
if (isFetchingInitially) {
return (
<div className="graph-empty">
<h3 className="graph-spinner" />
</div>
)
}
const {lastValues, series} = getLastValues(data)
const firstAlphabeticalSeriesName = _.sortBy(series)[0]
const firstAlphabeticalindex = _.indexOf(
series,
firstAlphabeticalSeriesName
)
const lastValue = lastValues[firstAlphabeticalindex]
const precision = 100.0
const roundedValue = Math.round(+lastValue * precision) / precision
const {bgColor, textColor} = generateThresholdsListHexs({
colors,
lastValue,
cellType: lineGraph ? 'line-plus-single-stat' : 'single-stat',
})
const backgroundColor = bgColor
const color = textColor
const height = `calc(100% - ${staticLegendHeight +
DYGRAPH_CONTAINER_V_MARGIN * 2}px)`
const singleStatStyles = staticLegendHeight
? {
backgroundColor,
color,
height,
}
: {
backgroundColor,
color,
}
return (
<div className="single-stat" style={singleStatStyles}>
<span
className={classnames('single-stat--value', {
'single-stat--small': cellHeight === SMALL_CELL_HEIGHT,
})}
>
{prefix}
{roundedValue}
{suffix}
{lineGraph && <div className="single-stat--shadow" />}
</span>
</div>
)
}
}
const {arrayOf, bool, number, shape, string} = PropTypes
SingleStat.propTypes = {
data: arrayOf(shape()).isRequired,
isFetchingInitially: bool,
cellHeight: number,
colors: colorsStringSchema,
prefix: string,
suffix: string,
lineGraph: bool,
staticLegendHeight: number,
}
export default SingleStat

View File

@ -0,0 +1,125 @@
import React, {PureComponent} from 'react'
import classnames from 'classnames'
import getLastValues from 'src/shared/parsing/lastValues'
import _ from 'lodash'
import {SMALL_CELL_HEIGHT} from 'src/shared/graphs/helpers'
import {DYGRAPH_CONTAINER_V_MARGIN} from 'src/shared/constants'
import {generateThresholdsListHexs} from 'src/shared/constants/colorOperations'
import {ColorNumber} from 'src/types/colors'
import {Data} from 'src/types/dygraphs'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
isFetchingInitially: boolean
cellHeight: number
colors: ColorNumber[]
prefix?: string
suffix?: string
lineGraph: boolean
staticLegendHeight: number
data: Data
}
@ErrorHandling
class SingleStat extends PureComponent<Props> {
public static defaultProps: Partial<Props> = {
prefix: '',
suffix: '',
}
public render() {
const {isFetchingInitially} = this.props
if (isFetchingInitially) {
return (
<div className="graph-empty">
<h3 className="graph-spinner" />
</div>
)
}
return (
<div className="single-stat" style={this.styles}>
<span className={this.className}>
{this.completeValue}
{this.renderShadow}
</span>
</div>
)
}
private get renderShadow(): JSX.Element {
const {lineGraph} = this.props
return lineGraph && <div className="single-stat--shadow" />
}
private get completeValue(): string {
const {prefix, suffix} = this.props
return `${prefix}${this.roundedLastValue}${suffix}`
}
private get roundedLastValue(): string {
const {data} = this.props
const {lastValues, series} = getLastValues(data)
const firstAlphabeticalSeriesName = _.sortBy(series)[0]
const firstAlphabeticalindex = _.indexOf(
series,
firstAlphabeticalSeriesName
)
const lastValue = lastValues[firstAlphabeticalindex]
const HUNDRED = 100.0
const roundedValue = Math.round(+lastValue * HUNDRED) / HUNDRED
return `${roundedValue}`
}
private get styles() {
const {data, colors, lineGraph, staticLegendHeight} = this.props
const {lastValues, series} = getLastValues(data)
const firstAlphabeticalSeriesName = _.sortBy(series)[0]
const firstAlphabeticalindex = _.indexOf(
series,
firstAlphabeticalSeriesName
)
const lastValue = lastValues[firstAlphabeticalindex]
const {bgColor, textColor} = generateThresholdsListHexs({
colors,
lastValue,
cellType: lineGraph ? 'line-plus-single-stat' : 'single-stat',
})
const backgroundColor = bgColor
const color = textColor
const height = `calc(100% - ${staticLegendHeight +
DYGRAPH_CONTAINER_V_MARGIN * 2}px)`
return staticLegendHeight
? {
backgroundColor,
color,
height,
}
: {
backgroundColor,
color,
}
}
private get className(): string {
const {cellHeight} = this.props
return classnames('single-stat--value', {
'single-stat--small': cellHeight === SMALL_CELL_HEIGHT,
})
}
}
export default SingleStat

View File

@ -1,7 +1,10 @@
import React from 'react'
import PropTypes from 'prop-types'
import React, {SFC, ReactElement} from 'react'
const SplashPage = ({children}) => (
interface Props {
children: ReactElement<any>
}
const SplashPage: SFC<Props> = ({children}) => (
<div className="auth-page">
<div className="auth-box">
<div className="auth-logo" />
@ -14,9 +17,4 @@ const SplashPage = ({children}) => (
</div>
)
const {node} = PropTypes
SplashPage.propTypes = {
children: node,
}
export default SplashPage

View File

@ -9,7 +9,6 @@ import {showTagKeys, showTagValues} from 'src/shared/apis/metaQuery'
import showTagKeysParser from 'src/shared/parsing/showTagKeys'
import showTagValuesParser from 'src/shared/parsing/showTagValues'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {QueryConfig, Tag} from 'src/types'
const {shape} = PropTypes

View File

@ -305,7 +305,7 @@ class Threesizer extends Component<Props, State> {
if (first && !before) {
const second = this.state.divisions[1]
if (second.size === 0) {
if (second && second.size === 0) {
return {...d, size: this.shorter(d.size)}
}
@ -338,7 +338,7 @@ class Threesizer extends Component<Props, State> {
if (first && !before) {
const second = this.state.divisions[1]
if (second.size === 0) {
if (second && second.size === 0) {
return {...d, size: this.thinner(d.size)}
}
@ -377,7 +377,7 @@ class Threesizer extends Component<Props, State> {
const leftIndex = i - 1
const left = _.get(divs, leftIndex, {size: 'none'})
if (left.size === 0) {
if (left && left.size === 0) {
return {...d, size: this.thinner(d.size)}
}
@ -406,7 +406,7 @@ class Threesizer extends Component<Props, State> {
if (after) {
const above = divs[i - 1]
if (above.size === 0) {
if (above && above.size === 0) {
return {...d, size: this.shorter(d.size)}
}

View File

@ -21,11 +21,6 @@ import {
} from 'shared/constants/thresholds'
import {ErrorHandling} from 'src/shared/decorators/errors'
const formatColor = color => {
const {hex, name} = color
return {hex, name}
}
@ErrorHandling
class ThresholdsList extends Component {
handleAddThreshold = () => {
@ -85,14 +80,23 @@ class ThresholdsList extends Component {
onResetFocus()
}
handleChooseColor = threshold => chosenColor => {
handleChangeBaseColor = updatedColor => {
const {handleUpdateThresholdsListColors} = this.props
const {hex, name} = updatedColor
const thresholdsListColors = this.props.thresholdsListColors.map(
color =>
color.id === threshold.id
? {...color, hex: chosenColor.hex, name: chosenColor.name}
: color
color.id === THRESHOLD_TYPE_BASE ? {...color, hex, name} : color
)
handleUpdateThresholdsListColors(thresholdsListColors)
}
handleChooseColor = updatedColor => {
const {handleUpdateThresholdsListColors} = this.props
const thresholdsListColors = this.props.thresholdsListColors.map(
color => (color.id === updatedColor.id ? updatedColor : color)
)
handleUpdateThresholdsListColors(thresholdsListColors)
@ -147,8 +151,8 @@ class ThresholdsList extends Component {
<div className="threshold-item--label">Base Color</div>
<ColorDropdown
colors={THRESHOLD_COLORS}
selected={formatColor(color)}
onChoose={this.handleChooseColor(color)}
selected={color}
onChoose={this.handleChangeBaseColor}
stretchToFit={true}
/>
</div>

View File

@ -1,8 +1,11 @@
import React from 'react'
import PropTypes from 'prop-types'
import React, {SFC, ReactElement} from 'react'
import ReactTooltip from 'react-tooltip'
const Tooltip = ({tip, children}) => (
interface Props {
tip: string
children: ReactElement<any>
}
const Tooltip: SFC<Props> = ({tip, children}) => (
<div>
<div data-tip={tip}>{children}</div>
<ReactTooltip
@ -14,11 +17,4 @@ const Tooltip = ({tip, children}) => (
</div>
)
const {shape, string} = PropTypes
Tooltip.propTypes = {
tip: string,
children: shape({}),
}
export default Tooltip

View File

@ -1,13 +1,25 @@
import React from 'react'
import PropTypes from 'prop-types'
import React, {SFC} from 'react'
import AlertsApp from 'src/alerts/containers/AlertsApp'
import NewsFeed from 'src/status/components/NewsFeed'
import GettingStarted from 'src/status/components/GettingStarted'
import {Cell} from 'src/types/dashboard'
import {Source} from 'src/types/sources'
import {RECENT_ALERTS_LIMIT} from 'src/status/constants'
const WidgetCell = ({cell, source, timeRange}) => {
interface TimeRange {
lower: string
upper: string
}
interface Props {
timeRange: TimeRange
cell: Cell
source: Source
}
const WidgetCell: SFC<Props> = ({cell, source, timeRange}) => {
switch (cell.type) {
case 'alerts': {
return (
@ -35,15 +47,4 @@ const WidgetCell = ({cell, source, timeRange}) => {
}
}
const {shape, string} = PropTypes
WidgetCell.propTypes = {
timeRange: shape({
lower: string,
upper: string,
}),
source: shape({}),
cell: shape({}),
}
export default WidgetCell

View File

@ -1,4 +1,5 @@
import _ from 'lodash'
import {Data} from 'src/types/dygraphs'
interface Result {
lastValues: number[]
@ -24,7 +25,7 @@ export interface TimeSeriesResponse {
}
export default function(
timeSeriesResponse: TimeSeriesResponse[] | null
timeSeriesResponse: TimeSeriesResponse[] | Data | null
): Result {
const values = _.get(
timeSeriesResponse,

7
ui/src/types/alerts.ts Normal file
View File

@ -0,0 +1,7 @@
export interface Alert {
name: string
time: string
value: string
host: string
level: string
}

View File

@ -7,3 +7,8 @@ interface ColorBase {
export type ColorString = ColorBase & {value: string}
export type ColorNumber = ColorBase & {value: number}
export interface ThresholdColor {
hex: string
name: string
}

View File

@ -1,5 +1,5 @@
import {AuthLinks, Organization, Role, User, Me} from './auth'
import {Template, Cell, CellQuery, Legend} from './dashboard'
import {Template, Cell, CellQuery, Legend, Axes} from './dashboard'
import {
GroupBy,
QueryConfig,
@ -52,4 +52,5 @@ export {
Task,
Notification,
NotificationFunc,
Axes,
}

View File

@ -1,18 +1,20 @@
export interface QueryConfig {
id?: string
database: string
measurement: string
retentionPolicy: string
fields: Field[]
database?: string
measurement?: string
retentionPolicy?: string
fields?: Field[]
tags: Tags
groupBy: GroupBy
groupBy?: GroupBy
areTagsAccepted: boolean
rawText: string
rawText?: string
range?: DurationRange | null
sourceLink?: string
fill?: string
status?: Status
shifts: TimeShift[]
shifts?: TimeShift[]
lower?: string
upper?: string
isQuerySupportedByExplorer?: boolean // doesn't come from server -- is set in CellEditorOverlay
}

View File

@ -1,10 +1,8 @@
import {ReactNode} from 'react'
export type DropdownItem =
| {
export interface DropdownItem {
text: string
}
| string
}
export interface DropdownAction {
icon: string

View File

@ -72,11 +72,11 @@ function generateResponseWithLinks<T extends object>(
}
interface RequestParams {
url: string
url: string | string[]
resource?: string | null
id?: string | null
method?: string
data?: object
data?: object | string
params?: object
headers?: object
}

View File

@ -1,10 +1,16 @@
import uuid from 'uuid'
import {NULL_STRING} from 'src/shared/constants/queryFillOptions'
import {QueryConfig} from 'src/types'
interface DefaultQueryArgs {
id?: string
isKapacitorRule?: boolean
}
const defaultQueryConfig = (
{id, isKapacitorRule = false} = {id: uuid.v4()}
) => {
{id, isKapacitorRule = false}: DefaultQueryArgs = {id: uuid.v4()}
): QueryConfig => {
const queryConfig = {
id,
database: null,

View File

@ -1,8 +1,9 @@
const KMB_LABELS = ['K', 'M', 'B', 'T', 'Q']
const KMG2_BIG_LABELS = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
const KMG2_SMALL_LABELS = ['m', 'u', 'n', 'p', 'f', 'a', 'z', 'y']
import _ from 'lodash'
const KMB_LABELS: string[] = ['K', 'M', 'B', 'T', 'Q']
const KMG2_BIG_LABELS: string[] = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
const KMG2_SMALL_LABELS: string[] = ['m', 'u', 'n', 'p', 'f', 'a', 'z', 'y']
const pow = (base, exp) => {
const pow = (base: number, exp: number): number => {
if (exp < 0) {
return 1.0 / Math.pow(base, -exp)
}
@ -10,12 +11,12 @@ const pow = (base, exp) => {
return Math.pow(base, exp)
}
const round_ = (num, places) => {
const roundNum = (num, places): number => {
const shift = Math.pow(10, places)
return Math.round(num * shift) / shift
}
const floatFormat = (x, optPrecision) => {
const floatFormat = (x: number, optPrecision: number): string => {
// Avoid invalid precision values; [1, 21] is the valid range.
const p = Math.min(Math.max(1, optPrecision || 2), 21)
@ -41,7 +42,12 @@ const floatFormat = (x, optPrecision) => {
}
// taken from https://github.com/danvk/dygraphs/blob/aaec6de56dba8ed712fd7b9d949de47b46a76ccd/src/dygraph-utils.js#L1103
export const numberValueFormatter = (x, opts, prefix, suffix) => {
export const numberValueFormatter = (
x: number,
opts: (name: string) => number,
prefix: string,
suffix: string
): string => {
const sigFigs = opts('sigFigs')
if (sigFigs !== null) {
@ -65,7 +71,7 @@ export const numberValueFormatter = (x, opts, prefix, suffix) => {
) {
label = x.toExponential(digits)
} else {
label = `${round_(x, digits)}`
label = `${roundNum(x, digits)}`
}
if (kmb || kmg2) {
@ -89,15 +95,17 @@ export const numberValueFormatter = (x, opts, prefix, suffix) => {
let n = pow(k, kLabels.length)
for (let j = kLabels.length - 1; j >= 0; j -= 1, n /= k) {
if (absx >= n) {
label = round_(x / n, digits) + kLabels[j]
label = roundNum(x / n, digits) + kLabels[j]
break
}
}
if (kmg2) {
const xParts = String(x.toExponential()).split('e-')
const xParts = String(x.toExponential())
.split('e-')
.map(Number)
if (xParts.length === 2 && xParts[1] >= 3 && xParts[1] <= 24) {
if (xParts[1] % 3 > 0) {
label = round_(xParts[0] / pow(10, xParts[1] % 3), digits)
label = roundNum(xParts[0] / pow(10, xParts[1] % 3), digits)
} else {
label = Number(xParts[0]).toFixed(2)
}
@ -109,7 +117,7 @@ export const numberValueFormatter = (x, opts, prefix, suffix) => {
return `${prefix}${label}${suffix}`
}
export const formatBytes = bytes => {
export const formatBytes = (bytes: number) => {
if (bytes === 0) {
return '0 Bytes'
}
@ -126,7 +134,7 @@ export const formatBytes = bytes => {
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
}
export const formatRPDuration = duration => {
export const formatRPDuration = (duration: string | null): string => {
if (!duration) {
return
}
@ -137,23 +145,21 @@ export const formatRPDuration = duration => {
let adjustedTime = duration
const durationMatcher = /(?:(\d*)d)?(?:(\d*)h)?(?:(\d*)m)?(?:(\d*)s)?/
const [
_match, // eslint-disable-line no-unused-vars
days = 0,
hours = 0,
minutes = 0,
seconds = 0,
] = duration.match(durationMatcher)
const result = duration.match(durationMatcher)
const days = _.get<string, string>(result, 1, '0')
const hours = _.get<string, string>(result, 2, '0')
const minutes = _.get<string, string>(result, 3, '0')
const seconds = _.get<string, string>(result, 4, '0')
const hoursInDay = 24
if (days) {
if (+days) {
adjustedTime = `${days}d`
adjustedTime += +hours === 0 ? '' : `${hours}h`
adjustedTime += +minutes === 0 ? '' : `${minutes}m`
adjustedTime += +seconds === 0 ? '' : `${seconds}s`
} else if (hours > hoursInDay) {
const hoursRemainder = hours % hoursInDay
const daysQuotient = (hours - hoursRemainder) / hoursInDay
} else if (+hours > hoursInDay) {
const hoursRemainder = +hours % hoursInDay
const daysQuotient = (+hours - hoursRemainder) / hoursInDay
adjustedTime = `${daysQuotient}d`
adjustedTime += +hoursRemainder === 0 ? '' : `${hoursRemainder}h`
adjustedTime += +minutes === 0 ? '' : `${minutes}m`

View File

@ -1,16 +1,22 @@
import _ from 'lodash'
import {TEMP_VAR_INTERVAL, AUTO_GROUP_BY} from 'shared/constants'
import {NULL_STRING} from 'shared/constants/queryFillOptions'
import {TEMP_VAR_INTERVAL, AUTO_GROUP_BY} from 'src/shared/constants'
import {NULL_STRING} from 'src/shared/constants/queryFillOptions'
import {
TYPE_QUERY_CONFIG,
TYPE_SHIFTED,
TYPE_IFQL,
} from 'src/dashboards/constants'
import {shiftTimeRange} from 'shared/query/helpers'
import {shiftTimeRange} from 'src/shared/query/helpers'
import {QueryConfig, Field, GroupBy, TimeShift} from 'src/types'
/* eslint-disable quotes */
export const quoteIfTimestamp = ({lower, upper}) => {
export const quoteIfTimestamp = ({
lower,
upper,
}: {
lower: string
upper: string
}): {lower: string; upper: string} => {
if (lower && lower.includes('Z') && !lower.includes("'")) {
lower = `'${lower}'`
}
@ -21,43 +27,53 @@ export const quoteIfTimestamp = ({lower, upper}) => {
return {lower, upper}
}
/* eslint-enable quotes */
export default function buildInfluxQLQuery(timeRange, config, shift) {
export default function buildInfluxQLQuery(
timeRange,
config: QueryConfig,
shift: string = ''
): string {
const {groupBy, fill = NULL_STRING, tags, areTagsAccepted} = config
const {upper, lower} = quoteIfTimestamp(timeRange)
const select = _buildSelect(config, shift)
const select = buildSelect(config, shift)
if (select === null) {
return null
}
const condition = _buildWhereClause({lower, upper, tags, areTagsAccepted})
const dimensions = _buildGroupBy(groupBy)
const fillClause = groupBy.time ? _buildFill(fill) : ''
const condition = buildWhereClause({lower, upper, tags, areTagsAccepted})
const dimensions = buildGroupBy(groupBy)
const fillClause = groupBy.time ? buildFill(fill) : ''
return `${select}${condition}${dimensions}${fillClause}`
}
function _buildSelect({fields, database, retentionPolicy, measurement}, shift) {
if (!database || !measurement || !fields || !fields.length) {
function buildSelect(
{fields, database, retentionPolicy, measurement}: QueryConfig,
shift: string | null = null
): string {
if (!database || !measurement || _.isEmpty(fields)) {
return null
}
const rpSegment = retentionPolicy ? `"${retentionPolicy}"` : ''
const fieldsClause = _buildFields(fields, shift)
const fieldsClause = buildFields(fields, shift)
const fullyQualifiedMeasurement = `"${database}".${rpSegment}."${measurement}"`
const statement = `SELECT ${fieldsClause} FROM ${fullyQualifiedMeasurement}`
return statement
}
// type arg will reason about new query types i.e. IFQL, GraphQL, or queryConfig
export const buildQuery = (type, timeRange, config, shift) => {
export const buildQuery = (
type: string,
timeRange: object,
config: QueryConfig,
shift: TimeShift | null = null
): string => {
switch (type) {
case TYPE_QUERY_CONFIG: {
return buildInfluxQLQuery(timeRange, config)
}
case TYPE_SHIFTED: {
const {quantity, unit} = shift
return buildInfluxQLQuery(
@ -75,11 +91,11 @@ export const buildQuery = (type, timeRange, config, shift) => {
return buildInfluxQLQuery(timeRange, config)
}
export function buildSelectStatement(config) {
return _buildSelect(config)
export function buildSelectStatement(config: QueryConfig): string {
return buildSelect(config)
}
function _buildFields(fieldFuncs, shift = '') {
function buildFields(fieldFuncs: Field[], shift = ''): string {
if (!fieldFuncs) {
return ''
}
@ -103,7 +119,7 @@ function _buildFields(fieldFuncs, shift = '') {
return `${f.value}`
}
case 'func': {
const args = _buildFields(f.args)
const args = buildFields(f.args)
const alias = f.alias ? ` AS "${f.alias}${shift}"` : ''
return `${f.value}(${args})${alias}`
}
@ -112,7 +128,12 @@ function _buildFields(fieldFuncs, shift = '') {
.join(', ')
}
function _buildWhereClause({lower, upper, tags, areTagsAccepted}) {
function buildWhereClause({
lower,
upper,
tags,
areTagsAccepted,
}: QueryConfig): string {
const timeClauses = []
const timeClause = quoteIfTimestamp({lower, upper})
@ -148,11 +169,11 @@ function _buildWhereClause({lower, upper, tags, areTagsAccepted}) {
return ` WHERE ${subClauses.join(' AND ')}`
}
function _buildGroupBy(groupBy) {
return `${_buildGroupByTime(groupBy)}${_buildGroupByTags(groupBy)}`
function buildGroupBy(groupBy: GroupBy): string {
return `${buildGroupByTime(groupBy)}${buildGroupByTags(groupBy)}`
}
function _buildGroupByTime(groupBy) {
function buildGroupByTime(groupBy: GroupBy): string {
if (!groupBy || !groupBy.time) {
return ''
}
@ -162,7 +183,7 @@ function _buildGroupByTime(groupBy) {
})`
}
function _buildGroupByTags(groupBy) {
function buildGroupByTags(groupBy: GroupBy): string {
if (!groupBy || !groupBy.tags.length) {
return ''
}
@ -176,9 +197,9 @@ function _buildGroupByTags(groupBy) {
return ` GROUP BY ${tags}`
}
function _buildFill(fill) {
function buildFill(fill: string): string {
return ` FILL(${fill})`
}
export const buildRawText = (q, timeRange) =>
q.rawText || buildInfluxQLQuery(timeRange, q) || ''
export const buildRawText = (config: QueryConfig, timeRange: object): string =>
config.rawText || buildInfluxQLQuery(timeRange, config) || ''

View File

@ -8,16 +8,23 @@ import {
} from 'src/shared/reducers/helpers/fields'
import {
Tag,
Field,
GroupBy,
Namespace,
QueryConfig,
Tag,
TagValues,
TimeShift,
QueryConfig,
ApplyFuncsToFieldArgs,
} from 'src/types'
export const editRawText = (
query: QueryConfig,
rawText: string
): QueryConfig => {
return {...query, rawText}
}
export const chooseNamespace = (
query: QueryConfig,
namespace: Namespace,

View File

@ -1,20 +0,0 @@
import AJAX from 'utils/ajax'
export const proxy = async ({source, query, db, rp, tempVars, resolution}) => {
try {
return await AJAX({
method: 'POST',
url: source,
data: {
tempVars,
query,
resolution,
db,
rp,
},
})
} catch (error) {
console.error(error)
throw error
}
}

View File

@ -0,0 +1,36 @@
import AJAX from 'src/utils/ajax'
interface ProxyQuery {
source: string | string[]
query: string
db?: string
rp?: string
tempVars?: any[]
resolution?: number
}
export async function proxy<T = any>({
source,
query,
db,
rp,
tempVars,
resolution,
}: ProxyQuery) {
try {
return await AJAX<T>({
method: 'POST',
url: source,
data: {
tempVars,
query,
resolution,
db,
rp,
},
})
} catch (error) {
console.error(error)
throw error
}
}

5
ui/src/utils/wrappers.ts Normal file
View File

@ -0,0 +1,5 @@
import _ from 'lodash'
export function get<T = any>(obj: any, path: string, fallack: T): T {
return _.get<T>(obj, path, fallack)
}

View File

@ -16,6 +16,14 @@ const queryConfigActions = {
editRawTextAsync: () => {},
addInitialField: () => {},
editQueryStatus: () => {},
deleteQuery: () => {},
fill: () => {},
removeFuncs: () => {},
editRawText: () => {},
setTimeRange: () => {},
updateRawQuery: () => {},
updateQueryConfig: () => {},
timeShift: () => {},
}
const setup = () => {

View File

@ -1,6 +1,6 @@
import reducer from 'src/data_explorer/reducers/queryConfigs'
import defaultQueryConfig from 'utils/defaultQueryConfig'
import defaultQueryConfig from 'src/utils/defaultQueryConfig'
import {
fill,
timeShift,
@ -9,6 +9,7 @@ import {
groupByTime,
toggleField,
removeFuncs,
editRawText,
updateRawQuery,
editQueryStatus,
chooseNamespace,
@ -17,36 +18,46 @@ import {
addInitialField,
updateQueryConfig,
toggleTagAcceptance,
ActionAddQuery,
} from 'src/data_explorer/actions/view'
import {LINEAR, NULL_STRING} from 'shared/constants/queryFillOptions'
import {LINEAR, NULL_STRING} from 'src/shared/constants/queryFillOptions'
const fakeAddQueryAction = (panelID, queryID) => {
const fakeAddQueryAction = (queryID: string): ActionAddQuery => {
return {
type: 'DE_ADD_QUERY',
payload: {panelID, queryID},
payload: {
queryID,
},
}
}
function buildInitialState(queryID, params) {
return Object.assign({}, defaultQueryConfig({id: queryID}), params)
function buildInitialState(queryID, params?) {
return {
...defaultQueryConfig({
id: queryID,
}),
...params,
}
}
describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
const queryID = 123
const queryID = '123'
it('can add a query', () => {
const state = reducer({}, fakeAddQueryAction('blah', queryID))
const state = reducer({}, fakeAddQueryAction(queryID))
const actual = state[queryID]
const expected = defaultQueryConfig({id: queryID})
const expected = defaultQueryConfig({
id: queryID,
})
expect(actual).toEqual(expected)
})
describe('choosing db, rp, and measurement', () => {
let state
beforeEach(() => {
state = reducer({}, fakeAddQueryAction('any', queryID))
state = reducer({}, fakeAddQueryAction(queryID))
})
it('sets the db and rp', () => {
@ -72,7 +83,7 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
describe('a query has measurements and fields', () => {
let state
beforeEach(() => {
const one = reducer({}, fakeAddQueryAction('any', queryID))
const one = reducer({}, fakeAddQueryAction(queryID))
const two = reducer(
one,
chooseNamespace(queryID, {
@ -81,14 +92,13 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
})
)
const three = reducer(two, chooseMeasurement(queryID, 'disk'))
state = reducer(
three,
addInitialField(queryID, {
const field = {
value: 'a great field',
type: 'field',
})
)
}
const groupBy = {}
state = reducer(three, addInitialField(queryID, field, groupBy))
})
describe('choosing a new namespace', () => {
@ -143,7 +153,10 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
expect(newState[queryID].fields.length).toBe(2)
expect(newState[queryID].fields[1].alias).toEqual('mean_f2')
expect(newState[queryID].fields[1].args).toEqual([
{value: 'f2', type: 'field'},
{
value: 'f2',
type: 'field',
},
])
expect(newState[queryID].fields[1].value).toEqual('mean')
})
@ -164,7 +177,10 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
expect(newState[queryID].fields[1].value).toBe('mean')
expect(newState[queryID].fields[1].alias).toBe('mean_f2')
expect(newState[queryID].fields[1].args).toEqual([
{value: 'f2', type: 'field'},
{
value: 'f2',
type: 'field',
},
])
expect(newState[queryID].fields[1].type).toBe('func')
})
@ -175,7 +191,10 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
const newState = reducer(
state,
toggleField(queryID, {value: 'fk1', type: 'field'})
toggleField(queryID, {
value: 'fk1',
type: 'field',
})
)
expect(newState[queryID].fields.length).toBe(1)
@ -185,58 +204,122 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
describe('DE_APPLY_FUNCS_TO_FIELD', () => {
it('applies new functions to a field', () => {
const f1 = {value: 'f1', type: 'field'}
const f2 = {value: 'f2', type: 'field'}
const f1 = {
value: 'f1',
type: 'field',
}
const f2 = {
value: 'f2',
type: 'field',
}
const initialState = {
[queryID]: {
id: 123,
[queryID]: buildInitialState(queryID, {
id: '123',
database: 'db1',
measurement: 'm1',
fields: [
{value: 'fn1', type: 'func', args: [f1], alias: `fn1_${f1.value}`},
{value: 'fn1', type: 'func', args: [f2], alias: `fn1_${f2.value}`},
{value: 'fn2', type: 'func', args: [f1], alias: `fn2_${f1.value}`},
],
{
value: 'fn1',
type: 'func',
args: [f1],
alias: `fn1_${f1.value}`,
},
{
value: 'fn1',
type: 'func',
args: [f2],
alias: `fn1_${f2.value}`,
},
{
value: 'fn2',
type: 'func',
args: [f1],
alias: `fn2_${f1.value}`,
},
],
}),
}
const action = applyFuncsToField(queryID, {
field: {value: 'f1', type: 'field'},
field: {
value: 'f1',
type: 'field',
},
funcs: [
{value: 'fn3', type: 'func', args: []},
{value: 'fn4', type: 'func', args: []},
{
value: 'fn3',
type: 'func',
},
{
value: 'fn4',
type: 'func',
},
],
})
const nextState = reducer(initialState, action)
expect(nextState[queryID].fields).toEqual([
{value: 'fn3', type: 'func', args: [f1], alias: `fn3_${f1.value}`},
{value: 'fn4', type: 'func', args: [f1], alias: `fn4_${f1.value}`},
{value: 'fn1', type: 'func', args: [f2], alias: `fn1_${f2.value}`},
{
value: 'fn3',
type: 'func',
args: [f1],
alias: `fn3_${f1.value}`,
},
{
value: 'fn4',
type: 'func',
args: [f1],
alias: `fn4_${f1.value}`,
},
{
value: 'fn1',
type: 'func',
args: [f2],
alias: `fn1_${f2.value}`,
},
])
})
})
describe('DE_REMOVE_FUNCS', () => {
it('removes all functions and group by time when one field has no funcs applied', () => {
const f1 = {value: 'f1', type: 'field'}
const f2 = {value: 'f2', type: 'field'}
const f1 = {
value: 'f1',
type: 'field',
}
const f2 = {
value: 'f2',
type: 'field',
}
const fields = [
{value: 'fn1', type: 'func', args: [f1], alias: `fn1_${f1.value}`},
{value: 'fn1', type: 'func', args: [f2], alias: `fn1_${f2.value}`},
{
value: 'fn1',
type: 'func',
args: [f1],
alias: `fn1_${f1.value}`,
},
{
value: 'fn1',
type: 'func',
args: [f2],
alias: `fn1_${f2.value}`,
},
]
const groupBy = {time: '1m', tags: []}
const groupBy = {
time: '1m',
tags: [],
}
const initialState = {
[queryID]: {
id: 123,
[queryID]: buildInitialState(queryID, {
id: '123',
database: 'db1',
measurement: 'm1',
fields,
groupBy,
},
}),
}
const action = removeFuncs(queryID, fields, groupBy)
@ -260,6 +343,7 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
},
}),
}
const action = chooseTag(queryID, {
key: 'k1',
value: 'v1',
@ -314,14 +398,17 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
describe('DE_GROUP_BY_TAG', () => {
it('adds a tag key/value to the query', () => {
const initialState = {
[queryID]: {
id: 123,
[queryID]: buildInitialState(queryID, {
id: '123',
database: 'db1',
measurement: 'm1',
fields: [],
tags: {},
groupBy: {tags: [], time: null},
groupBy: {
tags: [],
time: null,
},
}),
}
const action = groupByTag(queryID, 'k1')
@ -334,16 +421,21 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
})
it('removes a tag if the given tag key is already in the GROUP BY list', () => {
const initialState = {
[queryID]: {
id: 123,
const query = {
id: '123',
database: 'db1',
measurement: 'm1',
fields: [],
tags: {},
groupBy: {tags: ['k1'], time: null},
groupBy: {
tags: ['k1'],
time: null,
},
}
const initialState = {
[queryID]: buildInitialState(queryID, query),
}
const action = groupByTag(queryID, 'k1')
const nextState = reducer(initialState, action)
@ -389,7 +481,8 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
const initialState = {
[queryID]: buildInitialState(queryID),
}
const expected = defaultQueryConfig({id: queryID}, {rawText: 'hello'})
const id = {id: queryID}
const expected = defaultQueryConfig(id)
const action = updateQueryConfig(expected)
const nextState = reducer(initialState, action)
@ -413,12 +506,12 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
const initialState = {
[queryID]: buildInitialState(queryID),
}
const status = 'your query was sweet'
const status = {success: 'Your query was very nice'}
const action = editQueryStatus(queryID, status)
const nextState = reducer(initialState, action)
expect(nextState[queryID].status).toBe(status)
expect(nextState[queryID].status).toEqual(status)
})
describe('DE_FILL', () => {
@ -476,11 +569,31 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
[queryID]: buildInitialState(queryID),
}
const shift = {quantity: 1, unit: 'd', duration: '1d'}
const shift = {
quantity: '1',
unit: 'd',
duration: '1d',
label: 'label',
}
const action = timeShift(queryID, shift)
const nextState = reducer(initialState, action)
expect(nextState[queryID].shifts).toEqual([shift])
})
})
describe('DE_EDIT_RAW_TEXT', () => {
it('can edit the raw text', () => {
const initialState = {
[queryID]: buildInitialState(queryID),
}
const rawText = 'im the raw text'
const action = editRawText(queryID, rawText)
const nextState = reducer(initialState, action)
expect(nextState[queryID].rawText).toEqual(rawText)
})
})
})

Some files were not shown because too many files have changed in this diff Show More