Merge pull request #3431 from influxdata/chore/ts-data-explorer
Chore/ts data explorerpull/3441/head
commit
c55276ccb4
|
@ -147,7 +147,7 @@
|
|||
"react-grid-layout": "^0.16.6",
|
||||
"react-onclickoutside": "^5.2.0",
|
||||
"react-redux": "^4.4.0",
|
||||
"react-resizable": "^1.7.5",
|
||||
"react-resize-detector": "^2.3.0",
|
||||
"react-router": "^3.0.2",
|
||||
"react-router-redux": "^4.0.8",
|
||||
"react-tooltip": "^3.2.1",
|
||||
|
|
|
@ -1,167 +0,0 @@
|
|||
import uuid from 'uuid'
|
||||
|
||||
import {getQueryConfigAndStatus} from 'shared/apis'
|
||||
|
||||
import {errorThrown} from 'shared/actions/errors'
|
||||
|
||||
export const addQuery = (queryID = uuid.v4()) => ({
|
||||
type: 'DE_ADD_QUERY',
|
||||
payload: {
|
||||
queryID,
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteQuery = queryID => ({
|
||||
type: 'DE_DELETE_QUERY',
|
||||
payload: {
|
||||
queryID,
|
||||
},
|
||||
})
|
||||
|
||||
export const toggleField = (queryID, fieldFunc) => ({
|
||||
type: 'DE_TOGGLE_FIELD',
|
||||
payload: {
|
||||
queryID,
|
||||
fieldFunc,
|
||||
},
|
||||
})
|
||||
|
||||
export const groupByTime = (queryID, time) => ({
|
||||
type: 'DE_GROUP_BY_TIME',
|
||||
payload: {
|
||||
queryID,
|
||||
time,
|
||||
},
|
||||
})
|
||||
|
||||
export const fill = (queryID, value) => ({
|
||||
type: 'DE_FILL',
|
||||
payload: {
|
||||
queryID,
|
||||
value,
|
||||
},
|
||||
})
|
||||
|
||||
export const removeFuncs = (queryID, fields, groupBy) => ({
|
||||
type: 'DE_REMOVE_FUNCS',
|
||||
payload: {
|
||||
queryID,
|
||||
fields,
|
||||
groupBy,
|
||||
},
|
||||
})
|
||||
|
||||
export const applyFuncsToField = (queryID, fieldFunc, groupBy) => ({
|
||||
type: 'DE_APPLY_FUNCS_TO_FIELD',
|
||||
payload: {
|
||||
queryID,
|
||||
fieldFunc,
|
||||
groupBy,
|
||||
},
|
||||
})
|
||||
|
||||
export const chooseTag = (queryID, tag) => ({
|
||||
type: 'DE_CHOOSE_TAG',
|
||||
payload: {
|
||||
queryID,
|
||||
tag,
|
||||
},
|
||||
})
|
||||
|
||||
export const chooseNamespace = (queryID, {database, retentionPolicy}) => ({
|
||||
type: 'DE_CHOOSE_NAMESPACE',
|
||||
payload: {
|
||||
queryID,
|
||||
database,
|
||||
retentionPolicy,
|
||||
},
|
||||
})
|
||||
|
||||
export const chooseMeasurement = (queryID, measurement) => ({
|
||||
type: 'DE_CHOOSE_MEASUREMENT',
|
||||
payload: {
|
||||
queryID,
|
||||
measurement,
|
||||
},
|
||||
})
|
||||
|
||||
export const editRawText = (queryID, rawText) => ({
|
||||
type: 'DE_EDIT_RAW_TEXT',
|
||||
payload: {
|
||||
queryID,
|
||||
rawText,
|
||||
},
|
||||
})
|
||||
|
||||
export const setTimeRange = bounds => ({
|
||||
type: 'DE_SET_TIME_RANGE',
|
||||
payload: {
|
||||
bounds,
|
||||
},
|
||||
})
|
||||
|
||||
export const groupByTag = (queryID, tagKey) => ({
|
||||
type: 'DE_GROUP_BY_TAG',
|
||||
payload: {
|
||||
queryID,
|
||||
tagKey,
|
||||
},
|
||||
})
|
||||
|
||||
export const toggleTagAcceptance = queryID => ({
|
||||
type: 'DE_TOGGLE_TAG_ACCEPTANCE',
|
||||
payload: {
|
||||
queryID,
|
||||
},
|
||||
})
|
||||
|
||||
export const updateRawQuery = (queryID, text) => ({
|
||||
type: 'DE_UPDATE_RAW_QUERY',
|
||||
payload: {
|
||||
queryID,
|
||||
text,
|
||||
},
|
||||
})
|
||||
|
||||
export const updateQueryConfig = config => ({
|
||||
type: 'DE_UPDATE_QUERY_CONFIG',
|
||||
payload: {
|
||||
config,
|
||||
},
|
||||
})
|
||||
|
||||
export const addInitialField = (queryID, field, groupBy) => ({
|
||||
type: 'DE_ADD_INITIAL_FIELD',
|
||||
payload: {
|
||||
queryID,
|
||||
field,
|
||||
groupBy,
|
||||
},
|
||||
})
|
||||
|
||||
export const editQueryStatus = (queryID, status) => ({
|
||||
type: 'DE_EDIT_QUERY_STATUS',
|
||||
payload: {
|
||||
queryID,
|
||||
status,
|
||||
},
|
||||
})
|
||||
|
||||
export const timeShift = (queryID, shift) => ({
|
||||
type: 'DE_TIME_SHIFT',
|
||||
payload: {
|
||||
queryID,
|
||||
shift,
|
||||
},
|
||||
})
|
||||
|
||||
// Async actions
|
||||
export const editRawTextAsync = (url, id, text) => async dispatch => {
|
||||
try {
|
||||
const {data} = await getQueryConfigAndStatus(url, [{query: text, id}])
|
||||
const config = data.queries.find(q => q.id === id)
|
||||
dispatch(updateQueryConfig(config.queryConfig))
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,407 @@
|
|||
import uuid from 'uuid'
|
||||
|
||||
import {getQueryConfigAndStatus} from 'src/shared/apis'
|
||||
|
||||
import {errorThrown} from 'src/shared/actions/errors'
|
||||
import {
|
||||
QueryConfig,
|
||||
Status,
|
||||
Field,
|
||||
GroupBy,
|
||||
Tag,
|
||||
TimeRange,
|
||||
TimeShift,
|
||||
ApplyFuncsToFieldArgs,
|
||||
} from 'src/types'
|
||||
|
||||
export type Action =
|
||||
| ActionAddQuery
|
||||
| ActionDeleteQuery
|
||||
| ActionToggleField
|
||||
| ActionGroupByTime
|
||||
| ActionFill
|
||||
| ActionRemoveFuncs
|
||||
| ActionApplyFuncsToField
|
||||
| ActionChooseTag
|
||||
| ActionChooseNamspace
|
||||
| ActionChooseMeasurement
|
||||
| ActionEditRawText
|
||||
| ActionSetTimeRange
|
||||
| ActionGroupByTime
|
||||
| ActionToggleField
|
||||
| ActionUpdateRawQuery
|
||||
| ActionQueryConfig
|
||||
| ActionTimeShift
|
||||
| ActionToggleTagAcceptance
|
||||
| ActionToggleField
|
||||
| ActionGroupByTag
|
||||
| ActionEditQueryStatus
|
||||
| ActionAddInitialField
|
||||
|
||||
export interface ActionAddQuery {
|
||||
type: 'DE_ADD_QUERY'
|
||||
payload: {
|
||||
queryID: string
|
||||
}
|
||||
}
|
||||
|
||||
export const addQuery = (queryID: string = uuid.v4()): ActionAddQuery => ({
|
||||
type: 'DE_ADD_QUERY',
|
||||
payload: {
|
||||
queryID,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionDeleteQuery {
|
||||
type: 'DE_DELETE_QUERY'
|
||||
payload: {
|
||||
queryID: string
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteQuery = (queryID: string): ActionDeleteQuery => ({
|
||||
type: 'DE_DELETE_QUERY',
|
||||
payload: {
|
||||
queryID,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionToggleField {
|
||||
type: 'DE_TOGGLE_FIELD'
|
||||
payload: {
|
||||
queryID: string
|
||||
fieldFunc: Field
|
||||
}
|
||||
}
|
||||
|
||||
export const toggleField = (
|
||||
queryID: string,
|
||||
fieldFunc: Field
|
||||
): ActionToggleField => ({
|
||||
type: 'DE_TOGGLE_FIELD',
|
||||
payload: {
|
||||
queryID,
|
||||
fieldFunc,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionGroupByTime {
|
||||
type: 'DE_GROUP_BY_TIME'
|
||||
payload: {
|
||||
queryID: string
|
||||
time: string
|
||||
}
|
||||
}
|
||||
|
||||
export const groupByTime = (
|
||||
queryID: string,
|
||||
time: string
|
||||
): ActionGroupByTime => ({
|
||||
type: 'DE_GROUP_BY_TIME',
|
||||
payload: {
|
||||
queryID,
|
||||
time,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionFill {
|
||||
type: 'DE_FILL'
|
||||
payload: {
|
||||
queryID: string
|
||||
value: string
|
||||
}
|
||||
}
|
||||
|
||||
export const fill = (queryID: string, value: string): ActionFill => ({
|
||||
type: 'DE_FILL',
|
||||
payload: {
|
||||
queryID,
|
||||
value,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionRemoveFuncs {
|
||||
type: 'DE_REMOVE_FUNCS'
|
||||
payload: {
|
||||
queryID: string
|
||||
fields: Field[]
|
||||
groupBy: GroupBy
|
||||
}
|
||||
}
|
||||
|
||||
export const removeFuncs = (
|
||||
queryID: string,
|
||||
fields: Field[],
|
||||
groupBy: GroupBy
|
||||
): ActionRemoveFuncs => ({
|
||||
type: 'DE_REMOVE_FUNCS',
|
||||
payload: {
|
||||
queryID,
|
||||
fields,
|
||||
groupBy,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionApplyFuncsToField {
|
||||
type: 'DE_APPLY_FUNCS_TO_FIELD'
|
||||
payload: {
|
||||
queryID: string
|
||||
fieldFunc: ApplyFuncsToFieldArgs
|
||||
groupBy: GroupBy
|
||||
}
|
||||
}
|
||||
|
||||
export const applyFuncsToField = (
|
||||
queryID: string,
|
||||
fieldFunc: ApplyFuncsToFieldArgs,
|
||||
groupBy?: GroupBy
|
||||
): ActionApplyFuncsToField => ({
|
||||
type: 'DE_APPLY_FUNCS_TO_FIELD',
|
||||
payload: {
|
||||
queryID,
|
||||
fieldFunc,
|
||||
groupBy,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionChooseTag {
|
||||
type: 'DE_CHOOSE_TAG'
|
||||
payload: {
|
||||
queryID: string
|
||||
tag: Tag
|
||||
}
|
||||
}
|
||||
|
||||
export const chooseTag = (queryID: string, tag: Tag): ActionChooseTag => ({
|
||||
type: 'DE_CHOOSE_TAG',
|
||||
payload: {
|
||||
queryID,
|
||||
tag,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionChooseNamspace {
|
||||
type: 'DE_CHOOSE_NAMESPACE'
|
||||
payload: {
|
||||
queryID: string
|
||||
database: string
|
||||
retentionPolicy: string
|
||||
}
|
||||
}
|
||||
|
||||
interface DBRP {
|
||||
database: string
|
||||
retentionPolicy: string
|
||||
}
|
||||
|
||||
export const chooseNamespace = (
|
||||
queryID: string,
|
||||
{database, retentionPolicy}: DBRP
|
||||
): ActionChooseNamspace => ({
|
||||
type: 'DE_CHOOSE_NAMESPACE',
|
||||
payload: {
|
||||
queryID,
|
||||
database,
|
||||
retentionPolicy,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionChooseMeasurement {
|
||||
type: 'DE_CHOOSE_MEASUREMENT'
|
||||
payload: {
|
||||
queryID: string
|
||||
measurement: string
|
||||
}
|
||||
}
|
||||
|
||||
export const chooseMeasurement = (
|
||||
queryID: string,
|
||||
measurement: string
|
||||
): ActionChooseMeasurement => ({
|
||||
type: 'DE_CHOOSE_MEASUREMENT',
|
||||
payload: {
|
||||
queryID,
|
||||
measurement,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionEditRawText {
|
||||
type: 'DE_EDIT_RAW_TEXT'
|
||||
payload: {
|
||||
queryID: string
|
||||
rawText: string
|
||||
}
|
||||
}
|
||||
|
||||
export const editRawText = (
|
||||
queryID: string,
|
||||
rawText: string
|
||||
): ActionEditRawText => ({
|
||||
type: 'DE_EDIT_RAW_TEXT',
|
||||
payload: {
|
||||
queryID,
|
||||
rawText,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionSetTimeRange {
|
||||
type: 'DE_SET_TIME_RANGE'
|
||||
payload: {
|
||||
bounds: TimeRange
|
||||
}
|
||||
}
|
||||
|
||||
export const setTimeRange = (bounds: TimeRange): ActionSetTimeRange => ({
|
||||
type: 'DE_SET_TIME_RANGE',
|
||||
payload: {
|
||||
bounds,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionGroupByTag {
|
||||
type: 'DE_GROUP_BY_TAG'
|
||||
payload: {
|
||||
queryID: string
|
||||
tagKey: string
|
||||
}
|
||||
}
|
||||
|
||||
export const groupByTag = (
|
||||
queryID: string,
|
||||
tagKey: string
|
||||
): ActionGroupByTag => ({
|
||||
type: 'DE_GROUP_BY_TAG',
|
||||
payload: {
|
||||
queryID,
|
||||
tagKey,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionToggleTagAcceptance {
|
||||
type: 'DE_TOGGLE_TAG_ACCEPTANCE'
|
||||
payload: {
|
||||
queryID: string
|
||||
}
|
||||
}
|
||||
|
||||
export const toggleTagAcceptance = (
|
||||
queryID: string
|
||||
): ActionToggleTagAcceptance => ({
|
||||
type: 'DE_TOGGLE_TAG_ACCEPTANCE',
|
||||
payload: {
|
||||
queryID,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionUpdateRawQuery {
|
||||
type: 'DE_UPDATE_RAW_QUERY'
|
||||
payload: {
|
||||
queryID: string
|
||||
text: string
|
||||
}
|
||||
}
|
||||
|
||||
export const updateRawQuery = (
|
||||
queryID: string,
|
||||
text: string
|
||||
): ActionUpdateRawQuery => ({
|
||||
type: 'DE_UPDATE_RAW_QUERY',
|
||||
payload: {
|
||||
queryID,
|
||||
text,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionQueryConfig {
|
||||
type: 'DE_UPDATE_QUERY_CONFIG'
|
||||
payload: {
|
||||
config: QueryConfig
|
||||
}
|
||||
}
|
||||
|
||||
export const updateQueryConfig = (config: QueryConfig): ActionQueryConfig => ({
|
||||
type: 'DE_UPDATE_QUERY_CONFIG',
|
||||
payload: {
|
||||
config,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionAddInitialField {
|
||||
type: 'DE_ADD_INITIAL_FIELD'
|
||||
payload: {
|
||||
queryID: string
|
||||
field: Field
|
||||
groupBy?: GroupBy
|
||||
}
|
||||
}
|
||||
|
||||
export const addInitialField = (
|
||||
queryID: string,
|
||||
field: Field,
|
||||
groupBy: GroupBy
|
||||
): ActionAddInitialField => ({
|
||||
type: 'DE_ADD_INITIAL_FIELD',
|
||||
payload: {
|
||||
queryID,
|
||||
field,
|
||||
groupBy,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionEditQueryStatus {
|
||||
type: 'DE_EDIT_QUERY_STATUS'
|
||||
payload: {
|
||||
queryID: string
|
||||
status: Status
|
||||
}
|
||||
}
|
||||
|
||||
export const editQueryStatus = (
|
||||
queryID: string,
|
||||
status: Status
|
||||
): ActionEditQueryStatus => ({
|
||||
type: 'DE_EDIT_QUERY_STATUS',
|
||||
payload: {
|
||||
queryID,
|
||||
status,
|
||||
},
|
||||
})
|
||||
|
||||
interface ActionTimeShift {
|
||||
type: 'DE_TIME_SHIFT'
|
||||
payload: {
|
||||
queryID: string
|
||||
shift: TimeShift
|
||||
}
|
||||
}
|
||||
|
||||
export const timeShift = (
|
||||
queryID: string,
|
||||
shift: TimeShift
|
||||
): ActionTimeShift => ({
|
||||
type: 'DE_TIME_SHIFT',
|
||||
payload: {
|
||||
queryID,
|
||||
shift,
|
||||
},
|
||||
})
|
||||
|
||||
// Async actions
|
||||
export const editRawTextAsync = (
|
||||
url: string,
|
||||
id: string,
|
||||
text: string
|
||||
) => async (dispatch): Promise<void> => {
|
||||
try {
|
||||
const {data} = await getQueryConfigAndStatus(url, [
|
||||
{
|
||||
query: text,
|
||||
id,
|
||||
},
|
||||
])
|
||||
const config = data.queries.find(q => q.id === id)
|
||||
dispatch(updateQueryConfig(config.queryConfig))
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
}
|
||||
}
|
|
@ -1,13 +1,18 @@
|
|||
import {writeLineProtocol as writeLineProtocolAJAX} from 'src/data_explorer/apis'
|
||||
|
||||
import {notify} from 'shared/actions/notifications'
|
||||
import {notify} from 'src/shared/actions/notifications'
|
||||
import {Source} from 'src/types'
|
||||
|
||||
import {
|
||||
notifyDataWritten,
|
||||
notifyDataWriteFailed,
|
||||
} from 'shared/copy/notifications'
|
||||
} from 'src/shared/copy/notifications'
|
||||
|
||||
export const writeLineProtocolAsync = (source, db, data) => async dispatch => {
|
||||
export const writeLineProtocolAsync = (
|
||||
source: Source,
|
||||
db: string,
|
||||
data: string
|
||||
) => async (dispatch): Promise<void> => {
|
||||
try {
|
||||
await writeLineProtocolAJAX(source, db, data)
|
||||
dispatch(notify(notifyDataWritten()))
|
|
@ -1,8 +0,0 @@
|
|||
import AJAX from 'src/utils/ajax'
|
||||
|
||||
export const writeLineProtocol = async (source, db, data) =>
|
||||
await AJAX({
|
||||
url: `${source.links.write}?db=${db}`,
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
|
@ -0,0 +1,67 @@
|
|||
import AJAX from 'src/utils/ajax'
|
||||
import _ from 'lodash'
|
||||
import moment from 'moment'
|
||||
import download from 'src/external/download'
|
||||
|
||||
import {proxy} from 'src/utils/queryUrlGenerator'
|
||||
import {timeSeriesToTableGraph} from 'src/utils/timeSeriesTransformers'
|
||||
import {dataToCSV} from 'src/shared/parsing/dataToCSV'
|
||||
import {TEMPLATES} from 'src/shared/constants'
|
||||
import {Source, QueryConfig} from 'src/types'
|
||||
|
||||
export const writeLineProtocol = async (
|
||||
source: Source,
|
||||
db: string,
|
||||
data: string
|
||||
): Promise<void> =>
|
||||
await AJAX({
|
||||
url: `${source.links.write}?db=${db}`,
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
|
||||
interface DeprecatedQuery {
|
||||
id: string
|
||||
host: string
|
||||
queryConfig: QueryConfig
|
||||
text: string
|
||||
}
|
||||
|
||||
export const getDataForCSV = (
|
||||
query: DeprecatedQuery,
|
||||
errorThrown
|
||||
) => async () => {
|
||||
try {
|
||||
const response = await fetchTimeSeriesForCSV({
|
||||
source: query.host,
|
||||
query: query.text,
|
||||
tempVars: TEMPLATES,
|
||||
})
|
||||
|
||||
const {data} = timeSeriesToTableGraph([{response}])
|
||||
const name = csvName(query.queryConfig)
|
||||
download(dataToCSV(data), `${name}.csv`, 'text/plain')
|
||||
} catch (error) {
|
||||
errorThrown(error, 'Unable to download .csv file')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchTimeSeriesForCSV = async ({source, query, tempVars}) => {
|
||||
try {
|
||||
const {data} = await proxy({source, query, tempVars})
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const csvName = (query: QueryConfig): string => {
|
||||
const db = _.get(query, 'database', '')
|
||||
const rp = _.get(query, 'retentionPolicy', '')
|
||||
const measurement = _.get(query, 'measurement', '')
|
||||
|
||||
const timestring = moment().format('YYYY-MM-DD-HH-mm')
|
||||
return `${db}.${rp}.${measurement}.${timestring}`
|
||||
}
|
|
@ -1,27 +1,80 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, {PureComponent, KeyboardEvent} from 'react'
|
||||
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
import {QUERY_TEMPLATES} from 'src/data_explorer/constants'
|
||||
import QueryStatus from 'shared/components/QueryStatus'
|
||||
import Dropdown from 'src/shared/components/Dropdown'
|
||||
import {QUERY_TEMPLATES, QueryTemplate} from 'src/data_explorer/constants'
|
||||
import QueryStatus from 'src/shared/components/QueryStatus'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {QueryConfig} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
query: string
|
||||
config: QueryConfig
|
||||
onUpdate: (value: string) => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
value: string
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class QueryEditor extends Component {
|
||||
class QueryEditor extends PureComponent<Props, State> {
|
||||
private editor: React.RefObject<HTMLTextAreaElement>
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
value: this.props.query,
|
||||
}
|
||||
|
||||
this.editor = React.createRef<HTMLTextAreaElement>()
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
if (this.props.query !== nextProps.query) {
|
||||
this.setState({value: nextProps.query})
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown = e => {
|
||||
public render() {
|
||||
const {
|
||||
config: {status},
|
||||
} = this.props
|
||||
const {value} = this.state
|
||||
|
||||
return (
|
||||
<div className="query-editor">
|
||||
<textarea
|
||||
className="query-editor--field"
|
||||
ref={this.editor}
|
||||
value={value}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
onBlur={this.handleUpdate}
|
||||
onChange={this.handleChange}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
data-test="query-editor-field"
|
||||
placeholder="Enter a query or select database, measurement, and field below and have us build one for you..."
|
||||
/>
|
||||
<div className="varmoji">
|
||||
<div className="varmoji-container">
|
||||
<div className="varmoji-front">
|
||||
<QueryStatus status={status}>
|
||||
<Dropdown
|
||||
items={QUERY_TEMPLATES}
|
||||
selected="Query Templates"
|
||||
onChoose={this.handleChooseMetaQuery}
|
||||
className="dropdown-140 query-editor--templates"
|
||||
buttonSize="btn-xs"
|
||||
/>
|
||||
</QueryStatus>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>): void => {
|
||||
const {value} = this.state
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
|
@ -35,64 +88,18 @@ class QueryEditor extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleChange = () => {
|
||||
this.setState({value: this.editor.value})
|
||||
private handleChange = (): void => {
|
||||
const value = this.editor.current.value
|
||||
this.setState({value})
|
||||
}
|
||||
|
||||
handleUpdate = () => {
|
||||
private handleUpdate = (): void => {
|
||||
this.props.onUpdate(this.state.value)
|
||||
}
|
||||
|
||||
handleChooseMetaQuery = template => {
|
||||
private handleChooseMetaQuery = (template: QueryTemplate): void => {
|
||||
this.setState({value: template.query})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
config: {status},
|
||||
} = this.props
|
||||
const {value} = this.state
|
||||
|
||||
return (
|
||||
<div className="query-editor">
|
||||
<textarea
|
||||
className="query-editor--field"
|
||||
onChange={this.handleChange}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onBlur={this.handleUpdate}
|
||||
ref={editor => (this.editor = editor)}
|
||||
value={value}
|
||||
placeholder="Enter a query or select database, measurement, and field below and have us build one for you..."
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
data-test="query-editor-field"
|
||||
/>
|
||||
<div className="varmoji">
|
||||
<div className="varmoji-container">
|
||||
<div className="varmoji-front">
|
||||
<QueryStatus status={status}>
|
||||
<Dropdown
|
||||
items={QUERY_TEMPLATES}
|
||||
selected={'Query Templates'}
|
||||
onChoose={this.handleChooseMetaQuery}
|
||||
className="dropdown-140 query-editor--templates"
|
||||
buttonSize="btn-xs"
|
||||
/>
|
||||
</QueryStatus>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {func, shape, string} = PropTypes
|
||||
|
||||
QueryEditor.propTypes = {
|
||||
query: string,
|
||||
onUpdate: func.isRequired,
|
||||
config: shape().isRequired,
|
||||
}
|
||||
|
||||
export default QueryEditor
|
|
@ -1,216 +0,0 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Dimensions from 'react-dimensions'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {Table, Column, Cell} from 'fixed-data-table'
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
import CustomCell from 'src/data_explorer/components/CustomCell'
|
||||
import TabItem from 'src/data_explorer/components/TableTabItem'
|
||||
import {TEMPLATES} from 'src/shared/constants'
|
||||
|
||||
import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
const emptySeries = {columns: [], values: []}
|
||||
|
||||
@ErrorHandling
|
||||
class ChronoTable extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
series: [emptySeries],
|
||||
columnWidths: {},
|
||||
activeSeriesIndex: 0,
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchCellData(this.props.query)
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.query.text === nextProps.query.text) {
|
||||
return
|
||||
}
|
||||
|
||||
this.fetchCellData(nextProps.query)
|
||||
}
|
||||
|
||||
fetchCellData = async query => {
|
||||
if (!query || !query.text) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({isLoading: true})
|
||||
// second param is db, we want to leave this blank
|
||||
try {
|
||||
const {results} = await fetchTimeSeriesAsync({
|
||||
source: query.host,
|
||||
query,
|
||||
tempVars: TEMPLATES,
|
||||
})
|
||||
this.setState({isLoading: false})
|
||||
|
||||
let series = _.get(results, ['0', 'series'], [])
|
||||
|
||||
if (!series.length) {
|
||||
return this.setState({series: []})
|
||||
}
|
||||
|
||||
series = series.map(s => (s.values ? s : {...s, values: []}))
|
||||
this.setState({series})
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
series: [],
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
handleColumnResize = (newColumnWidth, columnKey) => {
|
||||
const columnWidths = {
|
||||
...this.state.columnWidths,
|
||||
[columnKey]: newColumnWidth,
|
||||
}
|
||||
|
||||
this.setState({
|
||||
columnWidths,
|
||||
})
|
||||
}
|
||||
|
||||
handleClickTab = activeSeriesIndex => () => {
|
||||
this.setState({activeSeriesIndex})
|
||||
}
|
||||
|
||||
handleClickDropdown = item => {
|
||||
this.setState({activeSeriesIndex: item.index})
|
||||
}
|
||||
|
||||
handleCustomCell = (columnName, values, colIndex) => ({rowIndex}) => {
|
||||
return (
|
||||
<CustomCell columnName={columnName} data={values[rowIndex][colIndex]} />
|
||||
)
|
||||
}
|
||||
|
||||
makeTabName = ({name, tags}) => {
|
||||
if (!tags) {
|
||||
return name
|
||||
}
|
||||
const tagKeys = Object.keys(tags).sort()
|
||||
const tagValues = tagKeys.map(key => tags[key]).join('.')
|
||||
return `${name}.${tagValues}`
|
||||
}
|
||||
|
||||
render() {
|
||||
const {containerWidth, height, query} = this.props
|
||||
const {series, columnWidths, isLoading, activeSeriesIndex} = this.state
|
||||
const {columns, values} = _.get(
|
||||
series,
|
||||
[`${activeSeriesIndex}`],
|
||||
emptySeries
|
||||
)
|
||||
|
||||
const maximumTabsCount = 11
|
||||
// adjust height to proper value by subtracting the heights of the UI around it
|
||||
// tab height, graph-container vertical padding, graph-heading height, multitable-header height
|
||||
const minWidth = 70
|
||||
const rowHeight = 34
|
||||
const headerHeight = 30
|
||||
const stylePixelOffset = 130
|
||||
const defaultColumnWidth = 200
|
||||
const styleAdjustedHeight = height - stylePixelOffset
|
||||
const width =
|
||||
columns && columns.length > 1 ? defaultColumnWidth : containerWidth
|
||||
|
||||
if (!query) {
|
||||
return <div className="generic-empty-state">Please add a query below</div>
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="generic-empty-state">Loading...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{width: '100%', height: '100%', position: 'relative'}}>
|
||||
{series.length < maximumTabsCount ? (
|
||||
<div className="table--tabs">
|
||||
{series.map((s, i) => (
|
||||
<TabItem
|
||||
isActive={i === activeSeriesIndex}
|
||||
key={i}
|
||||
name={this.makeTabName(s)}
|
||||
index={i}
|
||||
onClickTab={this.handleClickTab}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Dropdown
|
||||
className="dropdown-160 table--tabs-dropdown"
|
||||
items={series.map((s, index) => ({
|
||||
...s,
|
||||
text: this.makeTabName(s),
|
||||
index,
|
||||
}))}
|
||||
onChoose={this.handleClickDropdown}
|
||||
selected={this.makeTabName(series[activeSeriesIndex])}
|
||||
buttonSize="btn-xs"
|
||||
/>
|
||||
)}
|
||||
<div className="table--tabs-content">
|
||||
{(columns && !columns.length) || (values && !values.length) ? (
|
||||
<div className="generic-empty-state">This series is empty</div>
|
||||
) : (
|
||||
<Table
|
||||
onColumnResizeEndCallback={this.handleColumnResize}
|
||||
isColumnResizing={false}
|
||||
rowHeight={rowHeight}
|
||||
rowsCount={values.length}
|
||||
width={containerWidth}
|
||||
ownerHeight={styleAdjustedHeight}
|
||||
height={styleAdjustedHeight}
|
||||
headerHeight={headerHeight}
|
||||
>
|
||||
{columns.map((columnName, colIndex) => {
|
||||
return (
|
||||
<Column
|
||||
isResizable={true}
|
||||
key={columnName}
|
||||
columnKey={columnName}
|
||||
header={<Cell>{columnName}</Cell>}
|
||||
cell={this.handleCustomCell(columnName, values, colIndex)}
|
||||
width={columnWidths[columnName] || width}
|
||||
minWidth={minWidth}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ChronoTable.defaultProps = {
|
||||
height: 500,
|
||||
}
|
||||
|
||||
const {arrayOf, func, number, shape, string} = PropTypes
|
||||
|
||||
ChronoTable.propTypes = {
|
||||
query: shape({
|
||||
host: arrayOf(string.isRequired).isRequired,
|
||||
text: string.isRequired,
|
||||
id: string.isRequired,
|
||||
}).isRequired,
|
||||
containerWidth: number.isRequired,
|
||||
height: number,
|
||||
editQueryStatus: func.isRequired,
|
||||
}
|
||||
|
||||
export default Dimensions({elementResize: true})(ChronoTable)
|
|
@ -0,0 +1,299 @@
|
|||
import React, {PureComponent, CSSProperties} from 'react'
|
||||
|
||||
import Dimensions from 'react-dimensions'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {Table, Column, Cell} from 'fixed-data-table'
|
||||
import Dropdown from 'src/shared/components/Dropdown'
|
||||
import CustomCell from 'src/data_explorer/components/CustomCell'
|
||||
import TabItem from 'src/data_explorer/components/TableTabItem'
|
||||
import {TEMPLATES} from 'src/shared/constants'
|
||||
|
||||
import {fetchTimeSeriesAsync} from 'src/shared/actions/timeSeries'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {
|
||||
emptySeries,
|
||||
maximumTabsCount,
|
||||
minWidth,
|
||||
rowHeight,
|
||||
headerHeight,
|
||||
stylePixelOffset,
|
||||
defaultColumnWidth,
|
||||
} from 'src/data_explorer/constants/table'
|
||||
|
||||
interface DataExplorerTableQuery {
|
||||
host: string[]
|
||||
text: string
|
||||
id: string
|
||||
}
|
||||
|
||||
interface Series {
|
||||
columns: string[]
|
||||
name: string
|
||||
values: any[]
|
||||
}
|
||||
|
||||
interface ColumnWidths {
|
||||
[key: string]: number
|
||||
}
|
||||
|
||||
interface Props {
|
||||
height: number
|
||||
query: DataExplorerTableQuery
|
||||
editQueryStatus: () => void
|
||||
containerHeight: number
|
||||
containerWidth: number
|
||||
}
|
||||
|
||||
interface State {
|
||||
series: Series[]
|
||||
columnWidths: ColumnWidths
|
||||
activeSeriesIndex: number
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class ChronoTable extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
series: [emptySeries],
|
||||
columnWidths: {},
|
||||
activeSeriesIndex: 0,
|
||||
isLoading: false,
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.fetchCellData(this.props.query)
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps) {
|
||||
if (this.props.query.text === nextProps.query.text) {
|
||||
return
|
||||
}
|
||||
|
||||
this.fetchCellData(nextProps.query)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {query, containerWidth} = this.props
|
||||
const {series, columnWidths, isLoading, activeSeriesIndex} = this.state
|
||||
const {columns, values} = _.get(series, `${activeSeriesIndex}`, emptySeries)
|
||||
|
||||
if (!query) {
|
||||
return (
|
||||
<div className="generic-empty-state"> Please add a query below </div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="generic-empty-state"> Loading... </div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={this.style}>
|
||||
{this.tableSelector}
|
||||
<div className="table--tabs-content">
|
||||
{this.isEmpty ? (
|
||||
<div className="generic-empty-state"> This series is empty </div>
|
||||
) : (
|
||||
<Table
|
||||
isColumnResizing={false}
|
||||
width={containerWidth}
|
||||
rowHeight={rowHeight}
|
||||
height={this.height}
|
||||
ownerHeight={this.height}
|
||||
rowsCount={values.length}
|
||||
headerHeight={headerHeight}
|
||||
onColumnResizeEndCallback={this.handleColumnResize}
|
||||
>
|
||||
{columns.map((columnName, colIndex) => {
|
||||
return (
|
||||
<Column
|
||||
isResizable={true}
|
||||
key={columnName}
|
||||
minWidth={minWidth}
|
||||
columnKey={columnName}
|
||||
header={<Cell> {columnName} </Cell>}
|
||||
width={columnWidths[columnName] || this.columnWidth}
|
||||
cell={this.handleCustomCell(columnName, values, colIndex)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get isEmpty(): boolean {
|
||||
const {columns, values} = this.series
|
||||
return (columns && !columns.length) || (values && !values.length)
|
||||
}
|
||||
|
||||
private get height(): number {
|
||||
return this.props.containerHeight || 500 - stylePixelOffset
|
||||
}
|
||||
|
||||
private get tableSelector() {
|
||||
if (this.isTabbed) {
|
||||
return this.tabs
|
||||
}
|
||||
|
||||
return this.dropdown
|
||||
}
|
||||
|
||||
private get dropdown(): JSX.Element {
|
||||
const {series, activeSeriesIndex} = this.state
|
||||
return (
|
||||
<Dropdown
|
||||
className="dropdown-160 table--tabs-dropdown"
|
||||
items={this.dropdownItems}
|
||||
onChoose={this.handleClickDropdown}
|
||||
selected={this.makeTabName(series[activeSeriesIndex])}
|
||||
buttonSize="btn-xs"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private get dropdownItems(): Series[] {
|
||||
return this.state.series.map((s, index) => ({
|
||||
...s,
|
||||
index,
|
||||
text: this.makeTabName(s),
|
||||
}))
|
||||
}
|
||||
|
||||
private get tabs(): JSX.Element {
|
||||
const {series, activeSeriesIndex} = this.state
|
||||
return (
|
||||
<div className="table--tabs">
|
||||
{series.map((s, i) => (
|
||||
<TabItem
|
||||
key={i}
|
||||
index={i}
|
||||
name={s.name}
|
||||
onClickTab={this.handleClickTab}
|
||||
isActive={i === activeSeriesIndex}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private isTabbed(): boolean {
|
||||
const {series} = this.state
|
||||
|
||||
return series.length < maximumTabsCount
|
||||
}
|
||||
|
||||
private get style(): CSSProperties {
|
||||
return {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
}
|
||||
}
|
||||
|
||||
private get columnWidth(): number {
|
||||
return defaultColumnWidth
|
||||
}
|
||||
|
||||
private get series(): Series {
|
||||
const {activeSeriesIndex} = this.state
|
||||
const {series} = this.state
|
||||
|
||||
return _.get(series, `${activeSeriesIndex}`, emptySeries)
|
||||
}
|
||||
|
||||
private get source(): string {
|
||||
return _.get(this.props.query, 'host.0', '')
|
||||
}
|
||||
|
||||
private fetchCellData = async (query: DataExplorerTableQuery) => {
|
||||
if (!query || !query.text) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isLoading: true,
|
||||
})
|
||||
|
||||
try {
|
||||
const {results} = await fetchTimeSeriesAsync({
|
||||
source: this.source,
|
||||
query: query.text,
|
||||
tempVars: TEMPLATES,
|
||||
})
|
||||
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
let series = _.get(results, ['0', 'series'], [])
|
||||
|
||||
if (!series.length) {
|
||||
return this.setState({series: []})
|
||||
}
|
||||
|
||||
series = series.map(s => {
|
||||
if (s.values) {
|
||||
return s
|
||||
}
|
||||
|
||||
return {...s, values: []}
|
||||
})
|
||||
|
||||
this.setState({series})
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
series: [],
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private handleColumnResize = (
|
||||
newColumnWidth: number,
|
||||
columnKey: string
|
||||
): void => {
|
||||
const columnWidths = {
|
||||
...this.state.columnWidths,
|
||||
[columnKey]: newColumnWidth,
|
||||
}
|
||||
|
||||
this.setState({
|
||||
columnWidths,
|
||||
})
|
||||
}
|
||||
|
||||
private handleClickTab = activeSeriesIndex => {
|
||||
this.setState({
|
||||
activeSeriesIndex,
|
||||
})
|
||||
}
|
||||
|
||||
private handleClickDropdown = item => {
|
||||
this.setState({
|
||||
activeSeriesIndex: item.index,
|
||||
})
|
||||
}
|
||||
|
||||
private handleCustomCell = (columnName, values, colIndex) => ({rowIndex}) => {
|
||||
return (
|
||||
<CustomCell columnName={columnName} data={values[rowIndex][colIndex]} />
|
||||
)
|
||||
}
|
||||
|
||||
private makeTabName = ({name}): string => {
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
export default Dimensions({
|
||||
elementResize: true,
|
||||
})(ChronoTable)
|
|
@ -1,23 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
const TableTabItem = ({name, index, onClickTab, isActive}) => (
|
||||
<div
|
||||
className={classnames('table--tab', {active: isActive})}
|
||||
onClick={onClickTab(index)}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
)
|
||||
|
||||
const {bool, func, number, string} = PropTypes
|
||||
|
||||
TableTabItem.propTypes = {
|
||||
name: string,
|
||||
onClickTab: func.isRequired,
|
||||
index: number.isRequired,
|
||||
isActive: bool,
|
||||
}
|
||||
|
||||
export default TableTabItem
|
|
@ -0,0 +1,31 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
index: number
|
||||
onClickTab: (index: number) => void
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
class TableTabItem extends PureComponent<Props> {
|
||||
public render() {
|
||||
return (
|
||||
<div className={this.className} onClick={this.handleClick}>
|
||||
{this.props.name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleClick = (): void => {
|
||||
this.props.onClickTab(this.props.index)
|
||||
}
|
||||
|
||||
get className(): string {
|
||||
return classnames('table--tab', {
|
||||
active: this.props.isActive,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default TableTabItem
|
|
@ -1,72 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import _ from 'lodash'
|
||||
import moment from 'moment'
|
||||
|
||||
import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries'
|
||||
import {timeSeriesToTableGraph} from 'src/utils/timeSeriesTransformers'
|
||||
import {dataToCSV} from 'src/shared/parsing/dataToCSV'
|
||||
import download from 'src/external/download.js'
|
||||
import {TEMPLATES} from 'src/shared/constants'
|
||||
|
||||
const getDataForCSV = (query, errorThrown) => async () => {
|
||||
try {
|
||||
const response = await fetchTimeSeriesAsync({
|
||||
source: query.host,
|
||||
query,
|
||||
tempVars: TEMPLATES,
|
||||
})
|
||||
const {data} = timeSeriesToTableGraph([{response}])
|
||||
const db = _.get(query, ['queryConfig', 'database'], '')
|
||||
const rp = _.get(query, ['queryConfig', 'retentionPolicy'], '')
|
||||
const measurement = _.get(query, ['queryConfig', 'measurement'], '')
|
||||
|
||||
const timestring = moment().format('YYYY-MM-DD-HH-mm')
|
||||
const name = `${db}.${rp}.${measurement}.${timestring}`
|
||||
download(dataToCSV(data), `${name}.csv`, 'text/plain')
|
||||
} catch (error) {
|
||||
errorThrown(error, 'Unable to download .csv file')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const VisHeader = ({views, view, onToggleView, query, errorThrown}) => (
|
||||
<div className="graph-heading">
|
||||
{views.length ? (
|
||||
<ul className="nav nav-tablist nav-tablist-sm">
|
||||
{views.map(v => (
|
||||
<li
|
||||
key={v}
|
||||
onClick={onToggleView(v)}
|
||||
className={classnames({active: view === v})}
|
||||
data-test={`data-${v}`}
|
||||
>
|
||||
{_.upperFirst(v)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
{query ? (
|
||||
<div
|
||||
className="btn btn-sm btn-default dlcsv"
|
||||
onClick={getDataForCSV(query, errorThrown)}
|
||||
>
|
||||
<span className="icon download dlcsv" />
|
||||
.csv
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
|
||||
VisHeader.propTypes = {
|
||||
views: arrayOf(string).isRequired,
|
||||
view: string.isRequired,
|
||||
onToggleView: func.isRequired,
|
||||
query: shape(),
|
||||
errorThrown: func.isRequired,
|
||||
}
|
||||
|
||||
export default VisHeader
|
|
@ -0,0 +1,42 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import {getDataForCSV} from 'src/data_explorer/apis'
|
||||
import VisHeaderTabs from 'src/data_explorer/components/VisHeaderTabs'
|
||||
import {OnToggleView} from 'src/data_explorer/components/VisHeaderTab'
|
||||
|
||||
interface Props {
|
||||
views: string[]
|
||||
view: string
|
||||
query: any
|
||||
onToggleView: OnToggleView
|
||||
errorThrown: () => void
|
||||
}
|
||||
|
||||
class VisHeader extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {views, view, onToggleView, query, errorThrown} = this.props
|
||||
|
||||
return (
|
||||
<div className="graph-heading">
|
||||
{!!views.length && (
|
||||
<VisHeaderTabs
|
||||
view={view}
|
||||
views={views}
|
||||
currentView={view}
|
||||
onToggleView={onToggleView}
|
||||
/>
|
||||
)}
|
||||
{query && (
|
||||
<div
|
||||
className="btn btn-sm btn-default dlcsv"
|
||||
onClick={getDataForCSV(query, errorThrown)}
|
||||
>
|
||||
<span className="icon download dlcsv" />
|
||||
.csv
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default VisHeader
|
|
@ -0,0 +1,36 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import _ from 'lodash'
|
||||
|
||||
export type OnToggleView = (view: string) => void
|
||||
|
||||
interface TabProps {
|
||||
view: string
|
||||
currentView: string
|
||||
onToggleView: OnToggleView
|
||||
}
|
||||
|
||||
class VisHeaderTab extends PureComponent<TabProps> {
|
||||
public render() {
|
||||
return (
|
||||
<li className={this.className} onClick={this.handleClick}>
|
||||
{this.text}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
private get className(): string {
|
||||
const {view, currentView} = this.props
|
||||
return classnames({active: view === currentView})
|
||||
}
|
||||
|
||||
private handleClick = () => {
|
||||
this.props.onToggleView(this.props.view)
|
||||
}
|
||||
|
||||
private get text(): string {
|
||||
return _.upperFirst(this.props.view)
|
||||
}
|
||||
}
|
||||
|
||||
export default VisHeaderTab
|
|
@ -0,0 +1,28 @@
|
|||
import React, {SFC} from 'react'
|
||||
import VisHeaderTab, {
|
||||
OnToggleView,
|
||||
} from 'src/data_explorer/components/VisHeaderTab'
|
||||
|
||||
interface Props {
|
||||
views: string[]
|
||||
view: string
|
||||
currentView: string
|
||||
onToggleView: OnToggleView
|
||||
}
|
||||
|
||||
const VisHeaderTabs: SFC<Props> = ({views, currentView, onToggleView}) => {
|
||||
return (
|
||||
<ul className="nav nav-tablist nav-tablist-sm">
|
||||
{views.map(v => (
|
||||
<VisHeaderTab
|
||||
key={v}
|
||||
view={v}
|
||||
currentView={currentView}
|
||||
onToggleView={onToggleView}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export default VisHeaderTabs
|
|
@ -1,71 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Table from './Table'
|
||||
import RefreshingGraph from 'shared/components/RefreshingGraph'
|
||||
import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes'
|
||||
|
||||
const VisView = ({
|
||||
axes,
|
||||
view,
|
||||
query,
|
||||
queries,
|
||||
cellType,
|
||||
templates,
|
||||
autoRefresh,
|
||||
heightPixels,
|
||||
manualRefresh,
|
||||
editQueryStatus,
|
||||
resizerBottomHeight,
|
||||
}) => {
|
||||
if (view === 'table') {
|
||||
if (!query) {
|
||||
return (
|
||||
<div className="graph-empty">
|
||||
<p>Build a Query above</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
query={query}
|
||||
height={resizerBottomHeight}
|
||||
editQueryStatus={editQueryStatus}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<RefreshingGraph
|
||||
colors={DEFAULT_LINE_COLORS}
|
||||
axes={axes}
|
||||
type={cellType}
|
||||
queries={queries}
|
||||
templates={templates}
|
||||
cellHeight={heightPixels}
|
||||
autoRefresh={autoRefresh}
|
||||
manualRefresh={manualRefresh}
|
||||
editQueryStatus={editQueryStatus}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const {arrayOf, func, number, shape, string} = PropTypes
|
||||
|
||||
VisView.propTypes = {
|
||||
view: string.isRequired,
|
||||
axes: shape(),
|
||||
query: shape(),
|
||||
queries: arrayOf(shape()).isRequired,
|
||||
cellType: string,
|
||||
templates: arrayOf(shape()),
|
||||
autoRefresh: number.isRequired,
|
||||
heightPixels: number,
|
||||
editQueryStatus: func.isRequired,
|
||||
manualRefresh: number,
|
||||
activeQueryIndex: number,
|
||||
resizerBottomHeight: number,
|
||||
}
|
||||
|
||||
export default VisView
|
|
@ -0,0 +1,53 @@
|
|||
import React, {SFC} from 'react'
|
||||
|
||||
import Table from './Table'
|
||||
import RefreshingGraph from 'src/shared/components/RefreshingGraph'
|
||||
import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes'
|
||||
|
||||
import {QueryConfig, Template} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
view: string
|
||||
query?: QueryConfig
|
||||
queries: QueryConfig[]
|
||||
templates: Template[]
|
||||
autoRefresh: number
|
||||
editQueryStatus: () => void
|
||||
manualRefresh: number
|
||||
}
|
||||
|
||||
const DataExplorerVisView: SFC<Props> = ({
|
||||
view,
|
||||
query,
|
||||
queries,
|
||||
templates,
|
||||
autoRefresh,
|
||||
manualRefresh,
|
||||
editQueryStatus,
|
||||
}) => {
|
||||
if (view === 'table') {
|
||||
if (!query) {
|
||||
return (
|
||||
<div className="graph-empty">
|
||||
<p> Build a Query above </p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return <Table query={query} editQueryStatus={editQueryStatus} />
|
||||
}
|
||||
|
||||
return (
|
||||
<RefreshingGraph
|
||||
type="line-graph"
|
||||
queries={queries}
|
||||
templates={templates}
|
||||
autoRefresh={autoRefresh}
|
||||
colors={DEFAULT_LINE_COLORS}
|
||||
manualRefresh={manualRefresh}
|
||||
editQueryStatus={editQueryStatus}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default DataExplorerVisView
|
|
@ -1,156 +0,0 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import VisHeader from 'src/data_explorer/components/VisHeader'
|
||||
import VisView from 'src/data_explorer/components/VisView'
|
||||
import {GRAPH, TABLE} from 'shared/constants'
|
||||
import buildQueries from 'utils/buildQueriesForGraphs'
|
||||
import _ from 'lodash'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
const META_QUERY_REGEX = /^(show|create|drop)/i
|
||||
|
||||
@ErrorHandling
|
||||
class Visualization extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
const {activeQueryIndex, queryConfigs} = this.props
|
||||
const activeQueryText = this.getQueryText(queryConfigs, activeQueryIndex)
|
||||
|
||||
this.state = activeQueryText.match(META_QUERY_REGEX)
|
||||
? {view: TABLE}
|
||||
: {view: GRAPH}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {activeQueryIndex, queryConfigs} = nextProps
|
||||
const nextQueryText = this.getQueryText(queryConfigs, activeQueryIndex)
|
||||
const queryText = this.getQueryText(
|
||||
this.props.queryConfigs,
|
||||
this.props.activeQueryIndex
|
||||
)
|
||||
|
||||
if (queryText === nextQueryText) {
|
||||
return
|
||||
}
|
||||
|
||||
if (nextQueryText.match(META_QUERY_REGEX)) {
|
||||
return this.setState({view: TABLE})
|
||||
}
|
||||
|
||||
this.setState({view: GRAPH})
|
||||
}
|
||||
|
||||
handleToggleView = view => () => {
|
||||
this.setState({view})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
axes,
|
||||
views,
|
||||
height,
|
||||
cellType,
|
||||
timeRange,
|
||||
templates,
|
||||
autoRefresh,
|
||||
heightPixels,
|
||||
queryConfigs,
|
||||
manualRefresh,
|
||||
editQueryStatus,
|
||||
activeQueryIndex,
|
||||
resizerBottomHeight,
|
||||
errorThrown,
|
||||
} = this.props
|
||||
|
||||
const {
|
||||
source: {
|
||||
links: {proxy},
|
||||
},
|
||||
} = this.context
|
||||
const {view} = this.state
|
||||
|
||||
const queries = buildQueries(proxy, queryConfigs, timeRange)
|
||||
const activeQuery = queries[activeQueryIndex]
|
||||
const defaultQuery = queries[0]
|
||||
const query = activeQuery || defaultQuery
|
||||
|
||||
return (
|
||||
<div className="graph" style={{height}}>
|
||||
<VisHeader
|
||||
view={view}
|
||||
views={views}
|
||||
query={query}
|
||||
errorThrown={errorThrown}
|
||||
onToggleView={this.handleToggleView}
|
||||
/>
|
||||
<div
|
||||
className={classnames({
|
||||
'graph-container': view === GRAPH,
|
||||
'table-container': view === TABLE,
|
||||
})}
|
||||
>
|
||||
<VisView
|
||||
view={view}
|
||||
axes={axes}
|
||||
query={query}
|
||||
queries={queries}
|
||||
cellType={cellType}
|
||||
templates={templates}
|
||||
autoRefresh={autoRefresh}
|
||||
heightPixels={heightPixels}
|
||||
manualRefresh={manualRefresh}
|
||||
editQueryStatus={editQueryStatus}
|
||||
resizerBottomHeight={resizerBottomHeight}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
getQueryText(queryConfigs, index) {
|
||||
// rawText can be null
|
||||
return _.get(queryConfigs, [`${index}`, 'rawText'], '') || ''
|
||||
}
|
||||
}
|
||||
|
||||
Visualization.defaultProps = {
|
||||
cellType: '',
|
||||
}
|
||||
|
||||
const {arrayOf, func, number, shape, string} = PropTypes
|
||||
|
||||
Visualization.contextTypes = {
|
||||
source: shape({
|
||||
links: shape({
|
||||
proxy: string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
}
|
||||
|
||||
Visualization.propTypes = {
|
||||
cellType: string,
|
||||
autoRefresh: number.isRequired,
|
||||
templates: arrayOf(shape()),
|
||||
timeRange: shape({
|
||||
upper: string,
|
||||
lower: string,
|
||||
}).isRequired,
|
||||
queryConfigs: arrayOf(shape({})).isRequired,
|
||||
activeQueryIndex: number,
|
||||
height: string,
|
||||
heightPixels: number,
|
||||
editQueryStatus: func.isRequired,
|
||||
views: arrayOf(string).isRequired,
|
||||
axes: shape({
|
||||
y: shape({
|
||||
bounds: arrayOf(string),
|
||||
}),
|
||||
}),
|
||||
resizerBottomHeight: number,
|
||||
errorThrown: func.isRequired,
|
||||
manualRefresh: number,
|
||||
}
|
||||
|
||||
export default Visualization
|
|
@ -0,0 +1,137 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import _ from 'lodash'
|
||||
|
||||
import VisHeader from 'src/data_explorer/components/VisHeader'
|
||||
import VisView from 'src/data_explorer/components/VisView'
|
||||
|
||||
import {GRAPH, TABLE} from 'src/shared/constants'
|
||||
import buildQueries from 'src/utils/buildQueriesForGraphs'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
import {Source, QueryConfig, Template, TimeRange} from 'src/types'
|
||||
|
||||
const META_QUERY_REGEX = /^(show|create|drop)/i
|
||||
|
||||
interface Props {
|
||||
source: Source
|
||||
views: string[]
|
||||
autoRefresh: number
|
||||
templates: Template[]
|
||||
timeRange: TimeRange
|
||||
queryConfigs: QueryConfig[]
|
||||
activeQueryIndex: number
|
||||
manualRefresh: number
|
||||
editQueryStatus: () => void
|
||||
errorThrown: () => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
view: string
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class DataExplorerVisualization extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = this.initialState
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
const {activeQueryIndex, queryConfigs} = nextProps
|
||||
const nextQueryText = this.getQueryText(queryConfigs, activeQueryIndex)
|
||||
const queryText = this.getQueryText(
|
||||
this.props.queryConfigs,
|
||||
this.props.activeQueryIndex
|
||||
)
|
||||
|
||||
if (queryText === nextQueryText) {
|
||||
return
|
||||
}
|
||||
|
||||
if (nextQueryText.match(META_QUERY_REGEX)) {
|
||||
return this.setState({view: TABLE})
|
||||
}
|
||||
|
||||
this.setState({view: GRAPH})
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
views,
|
||||
templates,
|
||||
autoRefresh,
|
||||
manualRefresh,
|
||||
editQueryStatus,
|
||||
errorThrown,
|
||||
} = this.props
|
||||
|
||||
const {view} = this.state
|
||||
|
||||
return (
|
||||
<div className="graph">
|
||||
<VisHeader
|
||||
view={view}
|
||||
views={views}
|
||||
query={this.query}
|
||||
errorThrown={errorThrown}
|
||||
onToggleView={this.handleToggleView}
|
||||
/>
|
||||
<div className={this.visualizationClass}>
|
||||
<VisView
|
||||
view={view}
|
||||
query={this.query}
|
||||
templates={templates}
|
||||
queries={this.queries}
|
||||
autoRefresh={autoRefresh}
|
||||
manualRefresh={manualRefresh}
|
||||
editQueryStatus={editQueryStatus}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get visualizationClass(): string {
|
||||
const {view} = this.state
|
||||
|
||||
return classnames({
|
||||
'graph-container': view === GRAPH,
|
||||
'table-container': view === TABLE,
|
||||
})
|
||||
}
|
||||
|
||||
private get queries(): QueryConfig[] {
|
||||
const {source, queryConfigs, timeRange} = this.props
|
||||
return buildQueries(source.links.proxy, queryConfigs, timeRange)
|
||||
}
|
||||
|
||||
private get query(): QueryConfig {
|
||||
const {activeQueryIndex} = this.props
|
||||
const activeQuery = this.queries[activeQueryIndex]
|
||||
const defaultQuery = this.queries[0]
|
||||
return activeQuery || defaultQuery
|
||||
}
|
||||
|
||||
private handleToggleView = (view: string): void => {
|
||||
this.setState({view})
|
||||
}
|
||||
|
||||
private getQueryText(queryConfigs: QueryConfig[], index: number): string {
|
||||
// rawText can be null
|
||||
return _.get(queryConfigs, [`${index}`, 'rawText'], '') || ''
|
||||
}
|
||||
|
||||
private get initialState(): {view: string} {
|
||||
const {activeQueryIndex, queryConfigs} = this.props
|
||||
const activeQueryText = this.getQueryText(queryConfigs, activeQueryIndex)
|
||||
|
||||
if (activeQueryText.match(META_QUERY_REGEX)) {
|
||||
return {view: TABLE}
|
||||
}
|
||||
|
||||
return {view: GRAPH}
|
||||
}
|
||||
}
|
||||
|
||||
export default DataExplorerVisualization
|
|
@ -1,105 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import WriteDataFooter from 'src/data_explorer/components/WriteDataFooter'
|
||||
|
||||
const WriteDataBody = ({
|
||||
handleKeyUp,
|
||||
handleCancelFile,
|
||||
handleFile,
|
||||
handleEdit,
|
||||
handleSubmit,
|
||||
inputContent,
|
||||
uploadContent,
|
||||
fileName,
|
||||
isManual,
|
||||
fileInput,
|
||||
handleFileOpen,
|
||||
isUploading,
|
||||
}) => (
|
||||
<div className="write-data-form--body">
|
||||
{isManual ? (
|
||||
<textarea
|
||||
className="form-control write-data-form--input"
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
placeholder="<measurement>,<tag_key>=<tag_value> <field_key>=<field_value>"
|
||||
onKeyUp={handleKeyUp}
|
||||
onChange={handleEdit}
|
||||
autoFocus={true}
|
||||
data-test="manual-entry-field"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={
|
||||
uploadContent
|
||||
? 'write-data-form--file'
|
||||
: 'write-data-form--file write-data-form--file_active'
|
||||
}
|
||||
onClick={handleFileOpen}
|
||||
>
|
||||
{uploadContent ? (
|
||||
<h3 className="write-data-form--filepath_selected">{fileName}</h3>
|
||||
) : (
|
||||
<h3 className="write-data-form--filepath_empty">
|
||||
Drop a file here or click to upload
|
||||
</h3>
|
||||
)}
|
||||
<div
|
||||
className={
|
||||
uploadContent
|
||||
? 'write-data-form--graphic write-data-form--graphic_success'
|
||||
: 'write-data-form--graphic'
|
||||
}
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
onChange={handleFile(false)}
|
||||
className="write-data-form--upload"
|
||||
ref={fileInput}
|
||||
accept="text/*, application/gzip"
|
||||
/>
|
||||
{uploadContent && (
|
||||
<span className="write-data-form--file-submit">
|
||||
<button className="btn btn-md btn-success" onClick={handleSubmit}>
|
||||
Write this File
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-md btn-default"
|
||||
onClick={handleCancelFile}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isManual && (
|
||||
<WriteDataFooter
|
||||
isUploading={isUploading}
|
||||
isManual={isManual}
|
||||
inputContent={inputContent}
|
||||
handleSubmit={handleSubmit}
|
||||
uploadContent={uploadContent}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
const {func, string, bool} = PropTypes
|
||||
|
||||
WriteDataBody.propTypes = {
|
||||
handleKeyUp: func.isRequired,
|
||||
handleEdit: func.isRequired,
|
||||
handleCancelFile: func.isRequired,
|
||||
handleFile: func.isRequired,
|
||||
handleSubmit: func.isRequired,
|
||||
inputContent: string,
|
||||
uploadContent: string,
|
||||
fileName: string,
|
||||
isManual: bool,
|
||||
fileInput: func.isRequired,
|
||||
handleFileOpen: func.isRequired,
|
||||
isUploading: bool.isRequired,
|
||||
}
|
||||
|
||||
export default WriteDataBody
|
|
@ -0,0 +1,162 @@
|
|||
import React, {
|
||||
PureComponent,
|
||||
ChangeEvent,
|
||||
KeyboardEvent,
|
||||
MouseEvent,
|
||||
DragEvent,
|
||||
ReactElement,
|
||||
} from 'react'
|
||||
import WriteDataFooter from 'src/data_explorer/components/WriteDataFooter'
|
||||
|
||||
interface Props {
|
||||
handleCancelFile: (e: MouseEvent<HTMLButtonElement>) => void
|
||||
handleEdit: (e: ChangeEvent<HTMLTextAreaElement>) => void
|
||||
handleKeyUp: (e: KeyboardEvent<HTMLTextAreaElement>) => void
|
||||
handleFile: (drop: boolean) => (e: DragEvent<HTMLInputElement>) => void
|
||||
handleSubmit: (e: MouseEvent<HTMLButtonElement>) => void
|
||||
inputContent: string
|
||||
uploadContent: string
|
||||
fileName: string
|
||||
isManual: boolean
|
||||
fileInput: (ref: any) => any
|
||||
handleFileOpen: () => void
|
||||
isUploading: boolean
|
||||
}
|
||||
|
||||
class WriteDataBody extends PureComponent<Props> {
|
||||
public render() {
|
||||
return (
|
||||
<div className="write-data-form--body">
|
||||
{this.input}
|
||||
{this.footer}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleFile = (e: any): void => {
|
||||
this.props.handleFile(false)(e)
|
||||
}
|
||||
|
||||
private get input(): JSX.Element {
|
||||
const {isManual} = this.props
|
||||
if (isManual) {
|
||||
return this.textarea
|
||||
}
|
||||
|
||||
return this.dragArea
|
||||
}
|
||||
|
||||
private get textarea(): ReactElement<HTMLTextAreaElement> {
|
||||
const {handleKeyUp, handleEdit} = this.props
|
||||
return (
|
||||
<textarea
|
||||
spellCheck={false}
|
||||
autoFocus={true}
|
||||
autoComplete="off"
|
||||
onKeyUp={handleKeyUp}
|
||||
onChange={handleEdit}
|
||||
data-test="manual-entry-field"
|
||||
className="form-control write-data-form--input"
|
||||
placeholder="<measurement>,<tag_key>=<tag_value> <field_key>=<field_value>"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private get dragArea(): ReactElement<HTMLDivElement> {
|
||||
const {fileInput, handleFileOpen} = this.props
|
||||
|
||||
return (
|
||||
<div className={this.dragAreaClass} onClick={handleFileOpen}>
|
||||
{this.dragAreaHeader}
|
||||
<div className={this.infoClass} />
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInput}
|
||||
className="write-data-form--upload"
|
||||
accept="text/*, application/gzip"
|
||||
onChange={this.handleFile}
|
||||
/>
|
||||
{this.buttons}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get dragAreaHeader(): ReactElement<HTMLHeadElement> {
|
||||
const {uploadContent, fileName} = this.props
|
||||
|
||||
if (uploadContent) {
|
||||
return <h3 className="write-data-form--filepath_selected">{fileName}</h3>
|
||||
}
|
||||
|
||||
return (
|
||||
<h3 className="write-data-form--filepath_empty">
|
||||
Drop a file here or click to upload
|
||||
</h3>
|
||||
)
|
||||
}
|
||||
|
||||
private get infoClass(): string {
|
||||
const {uploadContent} = this.props
|
||||
|
||||
if (uploadContent) {
|
||||
return 'write-data-form--graphic write-data-form--graphic_success'
|
||||
}
|
||||
|
||||
return 'write-data-form--graphic'
|
||||
}
|
||||
|
||||
private get buttons(): ReactElement<HTMLSpanElement> | null {
|
||||
const {uploadContent, handleSubmit, handleCancelFile} = this.props
|
||||
|
||||
if (!uploadContent) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="write-data-form--file-submit">
|
||||
<button className="btn btn-md btn-success" onClick={handleSubmit}>
|
||||
Write this File
|
||||
</button>
|
||||
<button className="btn btn-md btn-default" onClick={handleCancelFile}>
|
||||
Cancel
|
||||
</button>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
private get dragAreaClass(): string {
|
||||
const {uploadContent} = this.props
|
||||
|
||||
if (uploadContent) {
|
||||
return 'write-data-form--file'
|
||||
}
|
||||
|
||||
return 'write-data-form--file write-data-form--file_active'
|
||||
}
|
||||
|
||||
private get footer(): JSX.Element | null {
|
||||
const {
|
||||
isUploading,
|
||||
isManual,
|
||||
inputContent,
|
||||
handleSubmit,
|
||||
uploadContent,
|
||||
} = this.props
|
||||
|
||||
if (!isManual) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<WriteDataFooter
|
||||
isUploading={isUploading}
|
||||
isManual={isManual}
|
||||
inputContent={inputContent}
|
||||
handleSubmit={handleSubmit}
|
||||
uploadContent={uploadContent}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default WriteDataBody
|
|
@ -1,60 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const submitButton = 'btn btn-sm btn-success write-data-form--submit'
|
||||
const spinner = 'btn-spinner'
|
||||
|
||||
const WriteDataFooter = ({
|
||||
isManual,
|
||||
inputContent,
|
||||
uploadContent,
|
||||
handleSubmit,
|
||||
isUploading,
|
||||
}) => (
|
||||
<div className="write-data-form--footer">
|
||||
{isManual ? (
|
||||
<span className="write-data-form--helper">
|
||||
Need help writing InfluxDB Line Protocol? -
|
||||
<a
|
||||
href="https://docs.influxdata.com/influxdb/latest/write_protocols/line_protocol_tutorial/"
|
||||
target="_blank"
|
||||
>
|
||||
See Documentation
|
||||
</a>
|
||||
</span>
|
||||
) : (
|
||||
<span className="write-data-form--helper">
|
||||
<a
|
||||
href="https://docs.influxdata.com/influxdb/v1.2//tools/shell/#import-data-from-a-file-with-import"
|
||||
target="_blank"
|
||||
>
|
||||
File Upload Documentation
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
className={isUploading ? `${submitButton} ${spinner}` : submitButton}
|
||||
onClick={handleSubmit}
|
||||
disabled={
|
||||
(!inputContent && isManual) ||
|
||||
(!uploadContent && !isManual) ||
|
||||
isUploading
|
||||
}
|
||||
data-test="write-data-submit-button"
|
||||
>
|
||||
Write
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
const {bool, func, string} = PropTypes
|
||||
|
||||
WriteDataFooter.propTypes = {
|
||||
isManual: bool.isRequired,
|
||||
isUploading: bool.isRequired,
|
||||
uploadContent: string,
|
||||
inputContent: string,
|
||||
handleSubmit: func,
|
||||
}
|
||||
|
||||
export default WriteDataFooter
|
|
@ -0,0 +1,70 @@
|
|||
import React, {PureComponent, MouseEvent} from 'react'
|
||||
import {
|
||||
WRITE_DATA_DOCS_LINK,
|
||||
DATA_IMPORT_DOCS_LINK,
|
||||
} from 'src/data_explorer/constants'
|
||||
|
||||
const submitButton = 'btn btn-sm btn-success write-data-form--submit'
|
||||
const spinner = 'btn-spinner'
|
||||
|
||||
interface Props {
|
||||
isManual: boolean
|
||||
isUploading: boolean
|
||||
uploadContent: string
|
||||
inputContent: string
|
||||
handleSubmit: (e: MouseEvent<HTMLButtonElement>) => void
|
||||
}
|
||||
|
||||
class WriteDataFooter extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {isManual, handleSubmit} = this.props
|
||||
|
||||
return (
|
||||
<div className="write-data-form--footer">
|
||||
{isManual ? (
|
||||
<span className="write-data-form--helper">
|
||||
Need help writing InfluxDB Line Protocol? -
|
||||
<a href={WRITE_DATA_DOCS_LINK} target="_blank">
|
||||
See Documentation
|
||||
</a>
|
||||
</span>
|
||||
) : (
|
||||
<span className="write-data-form--helper">
|
||||
<a href={DATA_IMPORT_DOCS_LINK} target="_blank">
|
||||
File Upload Documentation
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
className={this.buttonClass}
|
||||
onClick={handleSubmit}
|
||||
disabled={this.buttonDisabled}
|
||||
data-test="write-data-submit-button"
|
||||
>
|
||||
Write
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
get buttonDisabled(): boolean {
|
||||
const {inputContent, isManual, uploadContent, isUploading} = this.props
|
||||
return (
|
||||
(!inputContent && isManual) ||
|
||||
(!uploadContent && !isManual) ||
|
||||
isUploading
|
||||
)
|
||||
}
|
||||
|
||||
get buttonClass(): string {
|
||||
const {isUploading} = this.props
|
||||
|
||||
if (isUploading) {
|
||||
return `${submitButton} ${spinner}`
|
||||
}
|
||||
|
||||
return submitButton
|
||||
}
|
||||
}
|
||||
|
||||
export default WriteDataFooter
|
|
@ -1,17 +1,43 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, {
|
||||
PureComponent,
|
||||
DragEvent,
|
||||
ChangeEvent,
|
||||
KeyboardEvent,
|
||||
} from 'react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import OnClickOutside from 'shared/components/OnClickOutside'
|
||||
import OnClickOutside from 'src/shared/components/OnClickOutside'
|
||||
import WriteDataBody from 'src/data_explorer/components/WriteDataBody'
|
||||
import WriteDataHeader from 'src/data_explorer/components/WriteDataHeader'
|
||||
|
||||
import {OVERLAY_TECHNOLOGY} from 'shared/constants/classNames'
|
||||
import {OVERLAY_TECHNOLOGY} from 'src/shared/constants/classNames'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {Source, DropdownItem} from 'src/types'
|
||||
let dragCounter = 0
|
||||
|
||||
interface Props {
|
||||
source: Source
|
||||
selectedDatabase: string
|
||||
onClose: () => void
|
||||
errorThrown: () => void
|
||||
writeLineProtocol: (source: Source, database: string, content: string) => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
selectedDatabase: string
|
||||
inputContent: string | null
|
||||
uploadContent: string
|
||||
fileName: string
|
||||
progress: string
|
||||
isManual: boolean
|
||||
dragClass: string
|
||||
isUploading: boolean
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class WriteDataForm extends Component {
|
||||
class WriteDataForm extends PureComponent<Props, State> {
|
||||
private fileInput: HTMLInputElement
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
|
@ -26,23 +52,52 @@ class WriteDataForm extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
toggleWriteView = isManual => () => {
|
||||
public render() {
|
||||
const {onClose, errorThrown, source} = this.props
|
||||
const {dragClass} = this.state
|
||||
|
||||
return (
|
||||
<div
|
||||
onDrop={this.handleFile(true)}
|
||||
onDragOver={this.handleDragOver}
|
||||
onDragEnter={this.handleDragEnter}
|
||||
onDragExit={this.handleDragLeave}
|
||||
onDragLeave={this.handleDragLeave}
|
||||
className={classnames(OVERLAY_TECHNOLOGY, dragClass)}
|
||||
>
|
||||
<div className="write-data-form">
|
||||
<WriteDataHeader
|
||||
{...this.state}
|
||||
source={source}
|
||||
onClose={onClose}
|
||||
errorThrown={errorThrown}
|
||||
toggleWriteView={this.toggleWriteView}
|
||||
handleSelectDatabase={this.handleSelectDatabase}
|
||||
/>
|
||||
<WriteDataBody
|
||||
{...this.state}
|
||||
fileInput={this.handleFileInputRef}
|
||||
handleEdit={this.handleEdit}
|
||||
handleFile={this.handleFile}
|
||||
handleKeyUp={this.handleKeyUp}
|
||||
handleSubmit={this.handleSubmit}
|
||||
handleFileOpen={this.handleFileOpen}
|
||||
handleCancelFile={this.handleCancelFile}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private toggleWriteView = (isManual: boolean) => {
|
||||
this.setState({isManual})
|
||||
}
|
||||
|
||||
handleSelectDatabase = item => {
|
||||
private handleSelectDatabase = (item: DropdownItem): void => {
|
||||
this.setState({selectedDatabase: item.text})
|
||||
}
|
||||
|
||||
handleClickOutside = e => {
|
||||
// guard against clicking to close error notification
|
||||
if (e.target.className === OVERLAY_TECHNOLOGY) {
|
||||
const {onClose} = this.props
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyUp = e => {
|
||||
private handleKeyUp = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
e.stopPropagation()
|
||||
if (e.key === 'Escape') {
|
||||
const {onClose} = this.props
|
||||
|
@ -50,7 +105,7 @@ class WriteDataForm extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleSubmit = async () => {
|
||||
private handleSubmit = async () => {
|
||||
const {onClose, source, writeLineProtocol} = this.props
|
||||
const {inputContent, uploadContent, selectedDatabase, isManual} = this.state
|
||||
const content = isManual ? inputContent : uploadContent
|
||||
|
@ -67,11 +122,11 @@ class WriteDataForm extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleEdit = e => {
|
||||
private handleEdit = (e: ChangeEvent<HTMLTextAreaElement>): void => {
|
||||
this.setState({inputContent: e.target.value.trim()})
|
||||
}
|
||||
|
||||
handleFile = drop => e => {
|
||||
private handleFile = (drop: boolean) => (e: any): void => {
|
||||
let file
|
||||
if (drop) {
|
||||
file = e.dataTransfer.files[0]
|
||||
|
@ -79,7 +134,7 @@ class WriteDataForm extends Component {
|
|||
dragClass: 'drag-none',
|
||||
})
|
||||
} else {
|
||||
file = e.target.files[0]
|
||||
file = e.currentTarget.files[0]
|
||||
}
|
||||
|
||||
if (!file) {
|
||||
|
@ -99,23 +154,23 @@ class WriteDataForm extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleCancelFile = () => {
|
||||
private handleCancelFile = (): void => {
|
||||
this.setState({uploadContent: ''})
|
||||
this.fileInput.value = ''
|
||||
}
|
||||
|
||||
handleDragOver = e => {
|
||||
private handleDragOver = (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
handleDragEnter = e => {
|
||||
private handleDragEnter = (e: DragEvent<HTMLDivElement>): void => {
|
||||
dragCounter += 1
|
||||
e.preventDefault()
|
||||
this.setState({dragClass: 'drag-over'})
|
||||
}
|
||||
|
||||
handleDragLeave = e => {
|
||||
private handleDragLeave = (e: DragEvent<HTMLDivElement>): void => {
|
||||
dragCounter -= 1
|
||||
e.preventDefault()
|
||||
if (dragCounter === 0) {
|
||||
|
@ -123,67 +178,14 @@ class WriteDataForm extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleFileOpen = () => {
|
||||
private handleFileOpen = (): void => {
|
||||
const {uploadContent} = this.state
|
||||
if (uploadContent === '') {
|
||||
this.fileInput.click()
|
||||
}
|
||||
}
|
||||
|
||||
handleFileInputRef = el => (this.fileInput = el)
|
||||
|
||||
render() {
|
||||
const {onClose, errorThrown, source} = this.props
|
||||
const {dragClass} = this.state
|
||||
|
||||
return (
|
||||
<div
|
||||
onDrop={this.handleFile(true)}
|
||||
onDragOver={this.handleDragOver}
|
||||
onDragEnter={this.handleDragEnter}
|
||||
onDragExit={this.handleDragLeave}
|
||||
onDragLeave={this.handleDragLeave}
|
||||
className={classnames(OVERLAY_TECHNOLOGY, dragClass)}
|
||||
>
|
||||
<div className="write-data-form">
|
||||
<WriteDataHeader
|
||||
{...this.state}
|
||||
source={source}
|
||||
handleSelectDatabase={this.handleSelectDatabase}
|
||||
errorThrown={errorThrown}
|
||||
toggleWriteView={this.toggleWriteView}
|
||||
onClose={onClose}
|
||||
/>
|
||||
<WriteDataBody
|
||||
{...this.state}
|
||||
fileInput={this.handleFileInputRef}
|
||||
handleEdit={this.handleEdit}
|
||||
handleFile={this.handleFile}
|
||||
handleKeyUp={this.handleKeyUp}
|
||||
handleSubmit={this.handleSubmit}
|
||||
handleFileOpen={this.handleFileOpen}
|
||||
handleCancelFile={this.handleCancelFile}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {func, shape, string} = PropTypes
|
||||
|
||||
WriteDataForm.propTypes = {
|
||||
source: shape({
|
||||
links: shape({
|
||||
proxy: string.isRequired,
|
||||
self: string.isRequired,
|
||||
queries: string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
onClose: func.isRequired,
|
||||
writeLineProtocol: func.isRequired,
|
||||
errorThrown: func.isRequired,
|
||||
selectedDatabase: string,
|
||||
private handleFileInputRef = (r: HTMLInputElement) => (this.fileInput = r)
|
||||
}
|
||||
|
||||
export default OnClickOutside(WriteDataForm)
|
|
@ -1,61 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import DatabaseDropdown from 'shared/components/DatabaseDropdown'
|
||||
|
||||
const WriteDataHeader = ({
|
||||
handleSelectDatabase,
|
||||
selectedDatabase,
|
||||
errorThrown,
|
||||
toggleWriteView,
|
||||
isManual,
|
||||
onClose,
|
||||
source,
|
||||
}) => (
|
||||
<div className="write-data-form--header">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">Write Data To</h1>
|
||||
<DatabaseDropdown
|
||||
source={source}
|
||||
onSelectDatabase={handleSelectDatabase}
|
||||
database={selectedDatabase}
|
||||
onErrorThrown={errorThrown}
|
||||
/>
|
||||
<ul className="nav nav-tablist nav-tablist-sm">
|
||||
<li
|
||||
onClick={toggleWriteView(false)}
|
||||
className={isManual ? '' : 'active'}
|
||||
>
|
||||
File Upload
|
||||
</li>
|
||||
<li
|
||||
onClick={toggleWriteView(true)}
|
||||
className={isManual ? 'active' : ''}
|
||||
data-test="manual-entry-button"
|
||||
>
|
||||
Manual Entry
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<span className="page-header__dismiss" onClick={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const {func, shape, string, bool} = PropTypes
|
||||
|
||||
WriteDataHeader.propTypes = {
|
||||
handleSelectDatabase: func.isRequired,
|
||||
selectedDatabase: string,
|
||||
toggleWriteView: func.isRequired,
|
||||
errorThrown: func.isRequired,
|
||||
onClose: func.isRequired,
|
||||
isManual: bool,
|
||||
source: shape({
|
||||
links: shape({
|
||||
proxy: string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
}
|
||||
|
||||
export default WriteDataHeader
|
|
@ -0,0 +1,80 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import DatabaseDropdown from 'src/shared/components/DatabaseDropdown'
|
||||
import {Source, DropdownItem} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
handleSelectDatabase: (item: DropdownItem) => void
|
||||
selectedDatabase: string
|
||||
toggleWriteView: (isWriteViewToggled: boolean) => void
|
||||
errorThrown: () => void
|
||||
onClose: () => void
|
||||
isManual: boolean
|
||||
source: Source
|
||||
}
|
||||
|
||||
class WriteDataHeader extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {
|
||||
handleSelectDatabase,
|
||||
selectedDatabase,
|
||||
errorThrown,
|
||||
onClose,
|
||||
source,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<div className="write-data-form--header">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">Write Data To</h1>
|
||||
<DatabaseDropdown
|
||||
source={source}
|
||||
onSelectDatabase={handleSelectDatabase}
|
||||
database={selectedDatabase}
|
||||
onErrorThrown={errorThrown}
|
||||
/>
|
||||
<ul className="nav nav-tablist nav-tablist-sm">
|
||||
<li onClick={this.handleToggleOff} className={this.fileUploadClass}>
|
||||
File Upload
|
||||
</li>
|
||||
<li
|
||||
onClick={this.handleToggleOn}
|
||||
className={this.manualEntryClass}
|
||||
data-test="manual-entry-button"
|
||||
>
|
||||
Manual Entry
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<span className="page-header__dismiss" onClick={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get fileUploadClass(): string {
|
||||
if (this.props.isManual) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return 'active'
|
||||
}
|
||||
|
||||
private get manualEntryClass(): string {
|
||||
if (this.props.isManual) {
|
||||
return 'active'
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
private handleToggleOff = (): void => {
|
||||
this.props.toggleWriteView(false)
|
||||
}
|
||||
|
||||
private handleToggleOn = (): void => {
|
||||
this.props.toggleWriteView(true)
|
||||
}
|
||||
}
|
||||
|
||||
export default WriteDataHeader
|
|
@ -1,84 +0,0 @@
|
|||
export const INFLUXQL_FUNCTIONS = [
|
||||
'mean',
|
||||
'median',
|
||||
'count',
|
||||
'min',
|
||||
'max',
|
||||
'sum',
|
||||
'first',
|
||||
'last',
|
||||
'spread',
|
||||
'stddev',
|
||||
]
|
||||
|
||||
export const MINIMUM_HEIGHTS = {
|
||||
queryMaker: 350,
|
||||
visualization: 200,
|
||||
}
|
||||
|
||||
export const INITIAL_HEIGHTS = {
|
||||
queryMaker: '66.666%',
|
||||
visualization: '33.334%',
|
||||
}
|
||||
|
||||
const SEPARATOR = 'SEPARATOR'
|
||||
|
||||
export const QUERY_TEMPLATES = [
|
||||
{text: 'Show Databases', query: 'SHOW DATABASES'},
|
||||
{text: 'Create Database', query: 'CREATE DATABASE "db_name"'},
|
||||
{text: 'Drop Database', query: 'DROP DATABASE "db_name"'},
|
||||
{text: `${SEPARATOR}`},
|
||||
{text: 'Show Measurements', query: 'SHOW MEASUREMENTS ON "db_name"'},
|
||||
{
|
||||
text: 'Show Tag Keys',
|
||||
query: 'SHOW TAG KEYS ON "db_name" FROM "measurement_name"',
|
||||
},
|
||||
{
|
||||
text: 'Show Tag Values',
|
||||
query:
|
||||
'SHOW TAG VALUES ON "db_name" FROM "measurement_name" WITH KEY = "tag_key"',
|
||||
},
|
||||
{text: `${SEPARATOR}`},
|
||||
{
|
||||
text: 'Show Retention Policies',
|
||||
query: 'SHOW RETENTION POLICIES on "db_name"',
|
||||
},
|
||||
{
|
||||
text: 'Create Retention Policy',
|
||||
query:
|
||||
'CREATE RETENTION POLICY "rp_name" ON "db_name" DURATION 30d REPLICATION 1 DEFAULT',
|
||||
},
|
||||
{
|
||||
text: 'Drop Retention Policy',
|
||||
query: 'DROP RETENTION POLICY "rp_name" ON "db_name"',
|
||||
},
|
||||
{text: `${SEPARATOR}`},
|
||||
{
|
||||
text: 'Show Continuous Queries',
|
||||
query: 'SHOW CONTINUOUS QUERIES',
|
||||
},
|
||||
{
|
||||
text: 'Create Continuous Query',
|
||||
query:
|
||||
'CREATE CONTINUOUS QUERY "cq_name" ON "db_name" BEGIN SELECT min("field") INTO "target_measurement" FROM "current_measurement" GROUP BY time(30m) END',
|
||||
},
|
||||
{
|
||||
text: 'Drop Continuous Query',
|
||||
query: 'DROP CONTINUOUS QUERY "cq_name" ON "db_name"',
|
||||
},
|
||||
{text: `${SEPARATOR}`},
|
||||
{text: 'Show Users', query: 'SHOW USERS'},
|
||||
{
|
||||
text: 'Create User',
|
||||
query: 'CREATE USER "username" WITH PASSWORD \'password\'',
|
||||
},
|
||||
{
|
||||
text: 'Create Admin User',
|
||||
query:
|
||||
'CREATE USER "username" WITH PASSWORD \'password\' WITH ALL PRIVILEGES',
|
||||
},
|
||||
{text: 'Drop User', query: 'DROP USER "username"'},
|
||||
{text: `${SEPARATOR}`},
|
||||
{text: 'Show Stats', query: 'SHOW STATS'},
|
||||
{text: 'Show Diagnostics', query: 'SHOW DIAGNOSTICS'},
|
||||
]
|
|
@ -0,0 +1,144 @@
|
|||
export const INFLUXQL_FUNCTIONS: string[] = [
|
||||
'mean',
|
||||
'median',
|
||||
'count',
|
||||
'min',
|
||||
'max',
|
||||
'sum',
|
||||
'first',
|
||||
'last',
|
||||
'spread',
|
||||
'stddev',
|
||||
]
|
||||
|
||||
interface MinHeights {
|
||||
queryMaker: number
|
||||
visualization: number
|
||||
}
|
||||
|
||||
export const MINIMUM_HEIGHTS: MinHeights = {
|
||||
queryMaker: 350,
|
||||
visualization: 200,
|
||||
}
|
||||
|
||||
interface InitialHeights {
|
||||
queryMaker: '66.666%'
|
||||
visualization: '33.334%'
|
||||
}
|
||||
|
||||
export const INITIAL_HEIGHTS: InitialHeights = {
|
||||
queryMaker: '66.666%',
|
||||
visualization: '33.334%',
|
||||
}
|
||||
|
||||
const SEPARATOR: string = 'SEPARATOR'
|
||||
|
||||
export interface QueryTemplate {
|
||||
text: string
|
||||
query: string
|
||||
}
|
||||
|
||||
export interface Separator {
|
||||
text: string
|
||||
}
|
||||
|
||||
type Template = QueryTemplate | Separator
|
||||
|
||||
export const QUERY_TEMPLATES: Template[] = [
|
||||
{
|
||||
text: 'Show Databases',
|
||||
query: 'SHOW DATABASES',
|
||||
},
|
||||
{
|
||||
text: 'Create Database',
|
||||
query: 'CREATE DATABASE "db_name"',
|
||||
},
|
||||
{
|
||||
text: 'Drop Database',
|
||||
query: 'DROP DATABASE "db_name"',
|
||||
},
|
||||
{
|
||||
text: `${SEPARATOR}`,
|
||||
},
|
||||
{
|
||||
text: 'Show Measurements',
|
||||
query: 'SHOW MEASUREMENTS ON "db_name"',
|
||||
},
|
||||
{
|
||||
text: 'Show Tag Keys',
|
||||
query: 'SHOW TAG KEYS ON "db_name" FROM "measurement_name"',
|
||||
},
|
||||
{
|
||||
text: 'Show Tag Values',
|
||||
query:
|
||||
'SHOW TAG VALUES ON "db_name" FROM "measurement_name" WITH KEY = "tag_key"',
|
||||
},
|
||||
{
|
||||
text: `${SEPARATOR}`,
|
||||
},
|
||||
{
|
||||
text: 'Show Retention Policies',
|
||||
query: 'SHOW RETENTION POLICIES on "db_name"',
|
||||
},
|
||||
{
|
||||
text: 'Create Retention Policy',
|
||||
query:
|
||||
'CREATE RETENTION POLICY "rp_name" ON "db_name" DURATION 30d REPLICATION 1 DEFAULT',
|
||||
},
|
||||
{
|
||||
text: 'Drop Retention Policy',
|
||||
query: 'DROP RETENTION POLICY "rp_name" ON "db_name"',
|
||||
},
|
||||
{
|
||||
text: `${SEPARATOR}`,
|
||||
},
|
||||
{
|
||||
text: 'Show Continuous Queries',
|
||||
query: 'SHOW CONTINUOUS QUERIES',
|
||||
},
|
||||
{
|
||||
text: 'Create Continuous Query',
|
||||
query:
|
||||
'CREATE CONTINUOUS QUERY "cq_name" ON "db_name" BEGIN SELECT min("field") INTO "target_measurement" FROM "current_measurement" GROUP BY time(30m) END',
|
||||
},
|
||||
{
|
||||
text: 'Drop Continuous Query',
|
||||
query: 'DROP CONTINUOUS QUERY "cq_name" ON "db_name"',
|
||||
},
|
||||
{
|
||||
text: `${SEPARATOR}`,
|
||||
},
|
||||
{
|
||||
text: 'Show Users',
|
||||
query: 'SHOW USERS',
|
||||
},
|
||||
{
|
||||
text: 'Create User',
|
||||
query: 'CREATE USER "username" WITH PASSWORD \'password\'',
|
||||
},
|
||||
{
|
||||
text: 'Create Admin User',
|
||||
query:
|
||||
'CREATE USER "username" WITH PASSWORD \'password\' WITH ALL PRIVILEGES',
|
||||
},
|
||||
{
|
||||
text: 'Drop User',
|
||||
query: 'DROP USER "username"',
|
||||
},
|
||||
{
|
||||
text: `${SEPARATOR}`,
|
||||
},
|
||||
{
|
||||
text: 'Show Stats',
|
||||
query: 'SHOW STATS',
|
||||
},
|
||||
{
|
||||
text: 'Show Diagnostics',
|
||||
query: 'SHOW DIAGNOSTICS',
|
||||
},
|
||||
]
|
||||
|
||||
export const WRITE_DATA_DOCS_LINK =
|
||||
'https://docs.influxdata.com/influxdb/latest/write_protocols/line_protocol_tutorial/'
|
||||
export const DATA_IMPORT_DOCS_LINK =
|
||||
'https://docs.influxdata.com/influxdb/v1.2//tools/shell/#import-data-from-a-file-with-import'
|
|
@ -0,0 +1,9 @@
|
|||
export const emptySeries = {columns: [], values: [], name: ''}
|
||||
export const maximumTabsCount = 11
|
||||
// adjust height to proper value by subtracting the heights of the UI around it
|
||||
// tab height, graph-container vertical padding, graph-heading height, multitable-header height
|
||||
export const minWidth = 70
|
||||
export const rowHeight = 34
|
||||
export const headerHeight = 30
|
||||
export const stylePixelOffset = 130
|
||||
export const defaultColumnWidth = 200
|
|
@ -32,7 +32,7 @@ import {ErrorHandling} from 'src/shared/decorators/errors'
|
|||
interface Props {
|
||||
source: Source
|
||||
queryConfigs: QueryConfig[]
|
||||
queryConfigActions: any // TODO: actually type these
|
||||
queryConfigActions: any
|
||||
autoRefresh: number
|
||||
handleChooseAutoRefresh: () => void
|
||||
router?: InjectedRouter
|
||||
|
@ -72,7 +72,7 @@ export class DataExplorer extends PureComponent<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps) {
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
const {router} = this.props
|
||||
const {queryConfigs, timeRange} = nextProps
|
||||
|
||||
|
@ -138,6 +138,7 @@ export class DataExplorer extends PureComponent<Props, State> {
|
|||
initialGroupByTime={AUTO_GROUP_BY}
|
||||
/>
|
||||
<Visualization
|
||||
source={source}
|
||||
views={VIS_VIEWS}
|
||||
activeQueryIndex={0}
|
||||
timeRange={timeRange}
|
||||
|
@ -161,8 +162,8 @@ export class DataExplorer extends PureComponent<Props, State> {
|
|||
this.setState({showWriteForm: true})
|
||||
}
|
||||
|
||||
private handleChooseTimeRange = (bounds: TimeRange): void => {
|
||||
this.props.setTimeRange(bounds)
|
||||
private handleChooseTimeRange = (timeRange: TimeRange): void => {
|
||||
this.props.setTimeRange(timeRange)
|
||||
}
|
||||
|
||||
private get selectedDatabase(): string {
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {withRouter} from 'react-router'
|
||||
|
||||
import AutoRefreshDropdown from 'shared/components/AutoRefreshDropdown'
|
||||
import TimeRangeDropdown from 'shared/components/TimeRangeDropdown'
|
||||
import SourceIndicator from 'shared/components/SourceIndicator'
|
||||
import GraphTips from 'shared/components/GraphTips'
|
||||
|
||||
const {func, number, shape, string} = PropTypes
|
||||
|
||||
const Header = ({
|
||||
timeRange,
|
||||
autoRefresh,
|
||||
showWriteForm,
|
||||
onManualRefresh,
|
||||
onChooseTimeRange,
|
||||
onChooseAutoRefresh,
|
||||
}) => (
|
||||
<div className="page-header full-width">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">Data Explorer</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<GraphTips />
|
||||
<SourceIndicator />
|
||||
<div
|
||||
className="btn btn-sm btn-default"
|
||||
onClick={showWriteForm}
|
||||
data-test="write-data-button"
|
||||
>
|
||||
<span className="icon pencil" />
|
||||
Write Data
|
||||
</div>
|
||||
<AutoRefreshDropdown
|
||||
iconName="refresh"
|
||||
selected={autoRefresh}
|
||||
onChoose={onChooseAutoRefresh}
|
||||
onManualRefresh={onManualRefresh}
|
||||
/>
|
||||
<TimeRangeDropdown
|
||||
selected={timeRange}
|
||||
page="DataExplorer"
|
||||
onChooseTimeRange={onChooseTimeRange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Header.propTypes = {
|
||||
onChooseAutoRefresh: func.isRequired,
|
||||
onChooseTimeRange: func.isRequired,
|
||||
onManualRefresh: func.isRequired,
|
||||
autoRefresh: number.isRequired,
|
||||
showWriteForm: func.isRequired,
|
||||
timeRange: shape({
|
||||
lower: string,
|
||||
upper: string,
|
||||
}).isRequired,
|
||||
}
|
||||
|
||||
export default withRouter(Header)
|
|
@ -0,0 +1,63 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
|
||||
import AutoRefreshDropdown from 'src/shared/components/AutoRefreshDropdown'
|
||||
import TimeRangeDropdown from 'src/shared/components/TimeRangeDropdown'
|
||||
import SourceIndicator from 'src/shared/components/SourceIndicator'
|
||||
import GraphTips from 'src/shared/components/GraphTips'
|
||||
import {TimeRange} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
onChooseAutoRefresh: () => void
|
||||
onManualRefresh: () => void
|
||||
onChooseTimeRange: (timeRange: TimeRange) => void
|
||||
showWriteForm: () => void
|
||||
autoRefresh: number
|
||||
timeRange: TimeRange
|
||||
}
|
||||
|
||||
class Header extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {
|
||||
timeRange,
|
||||
autoRefresh,
|
||||
showWriteForm,
|
||||
onManualRefresh,
|
||||
onChooseTimeRange,
|
||||
onChooseAutoRefresh,
|
||||
} = this.props
|
||||
return (
|
||||
<div className="page-header full-width">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">Data Explorer</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<GraphTips />
|
||||
<SourceIndicator />
|
||||
<div
|
||||
className="btn btn-sm btn-default"
|
||||
onClick={showWriteForm}
|
||||
data-test="write-data-button"
|
||||
>
|
||||
<span className="icon pencil" />
|
||||
Write Data
|
||||
</div>
|
||||
<AutoRefreshDropdown
|
||||
iconName="refresh"
|
||||
selected={autoRefresh}
|
||||
onChoose={onChooseAutoRefresh}
|
||||
onManualRefresh={onManualRefresh}
|
||||
/>
|
||||
<TimeRangeDropdown
|
||||
selected={timeRange}
|
||||
page="DataExplorer"
|
||||
onChooseTimeRange={onChooseTimeRange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Header
|
|
@ -1,6 +1,9 @@
|
|||
import _ from 'lodash'
|
||||
|
||||
import defaultQueryConfig from 'src/utils/defaultQueryConfig'
|
||||
import {QueryConfig} from 'src/types'
|
||||
import {Action} from 'src/data_explorer/actions/view'
|
||||
|
||||
import {
|
||||
fill,
|
||||
timeShift,
|
||||
|
@ -18,7 +21,11 @@ import {
|
|||
toggleTagAcceptance,
|
||||
} from 'src/utils/queryTransitions'
|
||||
|
||||
const queryConfigs = (state = {}, action) => {
|
||||
interface State {
|
||||
[queryID: string]: Readonly<QueryConfig>
|
||||
}
|
||||
|
||||
const queryConfigs = (state: State = {}, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case 'DE_CHOOSE_NAMESPACE': {
|
||||
const {queryID, database, retentionPolicy} = action.payload
|
||||
|
@ -27,9 +34,7 @@ const queryConfigs = (state = {}, action) => {
|
|||
retentionPolicy,
|
||||
})
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryID]: Object.assign(nextQueryConfig, {rawText: null}),
|
||||
})
|
||||
return {...state, [queryID]: {...nextQueryConfig, rawText: null}}
|
||||
}
|
||||
|
||||
case 'DE_CHOOSE_MEASUREMENT': {
|
||||
|
@ -71,36 +76,31 @@ const queryConfigs = (state = {}, action) => {
|
|||
const {queryID, rawText} = action.payload
|
||||
const nextQueryConfig = editRawText(state[queryID], rawText)
|
||||
|
||||
return Object.assign({}, state, {
|
||||
return {
|
||||
...state,
|
||||
[queryID]: nextQueryConfig,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
case 'DE_GROUP_BY_TIME': {
|
||||
const {queryID, time} = action.payload
|
||||
const nextQueryConfig = groupByTime(state[queryID], time)
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryID]: nextQueryConfig,
|
||||
})
|
||||
return {...state, [queryID]: nextQueryConfig}
|
||||
}
|
||||
|
||||
case 'DE_TOGGLE_TAG_ACCEPTANCE': {
|
||||
const {queryID} = action.payload
|
||||
const nextQueryConfig = toggleTagAcceptance(state[queryID])
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryID]: nextQueryConfig,
|
||||
})
|
||||
return {...state, [queryID]: nextQueryConfig}
|
||||
}
|
||||
|
||||
case 'DE_TOGGLE_FIELD': {
|
||||
const {queryID, fieldFunc} = action.payload
|
||||
const nextQueryConfig = toggleField(state[queryID], fieldFunc)
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryID]: {...nextQueryConfig, rawText: null},
|
||||
})
|
||||
return {...state, [queryID]: {...nextQueryConfig, rawText: null}}
|
||||
}
|
||||
|
||||
case 'DE_APPLY_FUNCS_TO_FIELD': {
|
||||
|
@ -111,26 +111,20 @@ const queryConfigs = (state = {}, action) => {
|
|||
groupBy
|
||||
)
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryID]: nextQueryConfig,
|
||||
})
|
||||
return {...state, [queryID]: nextQueryConfig}
|
||||
}
|
||||
|
||||
case 'DE_CHOOSE_TAG': {
|
||||
const {queryID, tag} = action.payload
|
||||
const nextQueryConfig = chooseTag(state[queryID], tag)
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryID]: nextQueryConfig,
|
||||
})
|
||||
return {...state, [queryID]: nextQueryConfig}
|
||||
}
|
||||
|
||||
case 'DE_GROUP_BY_TAG': {
|
||||
const {queryID, tagKey} = action.payload
|
||||
const nextQueryConfig = groupByTag(state[queryID], tagKey)
|
||||
return Object.assign({}, state, {
|
||||
[queryID]: nextQueryConfig,
|
||||
})
|
||||
return {...state, [queryID]: nextQueryConfig}
|
||||
}
|
||||
|
||||
case 'DE_FILL': {
|
|
@ -1,19 +0,0 @@
|
|||
import {timeRanges} from 'shared/data/timeRanges'
|
||||
|
||||
const {lower, upper} = timeRanges.find(tr => tr.lower === 'now() - 1h')
|
||||
|
||||
const initialState = {
|
||||
upper,
|
||||
lower,
|
||||
}
|
||||
|
||||
export default function timeRange(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case 'DE_SET_TIME_RANGE': {
|
||||
const {bounds} = action.payload
|
||||
|
||||
return {...state, ...bounds}
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import {timeRanges} from 'src/shared/data/timeRanges'
|
||||
import {TimeRange} from 'src/types'
|
||||
|
||||
const {lower, upper} = timeRanges.find(tr => tr.lower === 'now() - 1h')
|
||||
|
||||
const initialState = {
|
||||
upper,
|
||||
lower,
|
||||
}
|
||||
|
||||
type State = Readonly<TimeRange>
|
||||
|
||||
interface ActionSetTimeRange {
|
||||
type: 'DE_SET_TIME_RANGE'
|
||||
payload: {
|
||||
bounds: TimeRange
|
||||
}
|
||||
}
|
||||
|
||||
type Action = ActionSetTimeRange
|
||||
|
||||
const timeRange = (state: State = initialState, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case 'DE_SET_TIME_RANGE': {
|
||||
const {bounds} = action.payload
|
||||
|
||||
return {...state, ...bounds}
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
export default timeRange
|
|
@ -1,8 +1,31 @@
|
|||
interface DataExplorerState {
|
||||
queryIDs: ReadonlyArray<string>
|
||||
}
|
||||
|
||||
interface ActionAddQuery {
|
||||
type: 'DE_ADD_QUERY'
|
||||
payload: {
|
||||
queryID: string
|
||||
}
|
||||
}
|
||||
|
||||
interface ActionDeleteQuery {
|
||||
type: 'DE_DELETE_QUERY'
|
||||
payload: {
|
||||
queryID: string
|
||||
}
|
||||
}
|
||||
|
||||
type Action = ActionAddQuery | ActionDeleteQuery
|
||||
|
||||
const initialState = {
|
||||
queryIDs: [],
|
||||
}
|
||||
|
||||
export default function ui(state = initialState, action) {
|
||||
const ui = (
|
||||
state: DataExplorerState = initialState,
|
||||
action: Action
|
||||
): DataExplorerState => {
|
||||
switch (action.type) {
|
||||
// there is an additional reducer for this same action in the queryConfig reducer
|
||||
case 'DE_ADD_QUERY': {
|
||||
|
@ -27,3 +50,5 @@ export default function ui(state = initialState, action) {
|
|||
|
||||
return state
|
||||
}
|
||||
|
||||
export default ui
|
|
@ -6,7 +6,7 @@ import TimeMachineVis from 'src/ifql/components/TimeMachineVis'
|
|||
import Threesizer from 'src/shared/components/Threesizer'
|
||||
import {Suggestion, OnChangeScript, FlatBody} from 'src/types/ifql'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {HANDLE_VERTICAL, HANDLE_HORIZONTAL} from 'src/shared/constants/index'
|
||||
import {HANDLE_VERTICAL, HANDLE_HORIZONTAL} from 'src/shared/constants'
|
||||
|
||||
interface Props {
|
||||
script: string
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
import {proxy} from 'utils/queryUrlGenerator'
|
||||
import {noop} from 'shared/actions/app'
|
||||
import {proxy} from 'src/utils/queryUrlGenerator'
|
||||
import {noop} from 'src/shared/actions/app'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {errorThrown} from 'shared/actions/errors'
|
||||
import {errorThrown} from 'src/shared/actions/errors'
|
||||
|
||||
export const handleLoading = (query, editQueryStatus) => {
|
||||
editQueryStatus(query.id, {loading: true})
|
||||
editQueryStatus(query.id, {
|
||||
loading: true,
|
||||
})
|
||||
}
|
||||
|
||||
// {results: [{}]}
|
||||
export const handleSuccess = (data, query, editQueryStatus) => {
|
||||
const {results} = data
|
||||
|
@ -22,12 +25,16 @@ export const handleSuccess = (data, query, editQueryStatus) => {
|
|||
|
||||
// 200 from chrono server but influx returns an "error" = warning
|
||||
if (error) {
|
||||
editQueryStatus(query.id, {warn: error})
|
||||
editQueryStatus(query.id, {
|
||||
warn: error,
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
// 200 from server and results contains data = success
|
||||
editQueryStatus(query.id, {success: 'Success!'})
|
||||
editQueryStatus(query.id, {
|
||||
success: 'Success!',
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
|
@ -39,11 +46,21 @@ export const handleError = (error, query, editQueryStatus) => {
|
|||
)
|
||||
|
||||
// 400 from chrono server = fail
|
||||
editQueryStatus(query.id, {error: message})
|
||||
editQueryStatus(query.id, {
|
||||
error: message,
|
||||
})
|
||||
}
|
||||
|
||||
interface Payload {
|
||||
source: string
|
||||
query: string
|
||||
tempVars: any[]
|
||||
db?: string
|
||||
rp?: string
|
||||
resolution?: number
|
||||
}
|
||||
export const fetchTimeSeriesAsync = async (
|
||||
{source, db, rp, query, tempVars, resolution},
|
||||
{source, db, rp, query, tempVars, resolution}: Payload,
|
||||
editQueryStatus = noop
|
||||
) => {
|
||||
handleLoading(query, editQueryStatus)
|
||||
|
@ -52,7 +69,7 @@ export const fetchTimeSeriesAsync = async (
|
|||
source,
|
||||
db,
|
||||
rp,
|
||||
query: query.text,
|
||||
query,
|
||||
tempVars,
|
||||
resolution,
|
||||
})
|
|
@ -31,14 +31,22 @@ interface Query {
|
|||
rp: string
|
||||
}
|
||||
|
||||
const parseSource = source => {
|
||||
if (Array.isArray(source)) {
|
||||
return _.get(source, '0', '')
|
||||
}
|
||||
|
||||
return source
|
||||
}
|
||||
|
||||
export const fetchTimeSeries = async (
|
||||
queries: Query[],
|
||||
resolution: number,
|
||||
templates: Template[],
|
||||
editQueryStatus: () => void
|
||||
editQueryStatus: () => any
|
||||
) => {
|
||||
const timeSeriesPromises = queries.map(query => {
|
||||
const {host, database, rp} = query
|
||||
const {host, database, rp, text} = query
|
||||
// the key `database` was used upstream in HostPage.js, and since as of this writing
|
||||
// the codebase has not been fully converted to TypeScript, it's not clear where else
|
||||
// it may be used, but this slight modification is intended to allow for the use of
|
||||
|
@ -63,11 +71,9 @@ export const fetchTimeSeries = async (
|
|||
|
||||
const tempVars = removeUnselectedTemplateValues(templatesWithIntervalVals)
|
||||
|
||||
const source = host
|
||||
return fetchTimeSeriesAsync(
|
||||
{source, db, rp, query, tempVars, resolution},
|
||||
editQueryStatus
|
||||
)
|
||||
const source = parseSource(host)
|
||||
const payload = {source, db, rp, query: text, tempVars, resolution}
|
||||
return fetchTimeSeriesAsync(payload, editQueryStatus)
|
||||
})
|
||||
|
||||
return Promise.all(timeSeriesPromises)
|
||||
|
|
|
@ -30,7 +30,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}
|
||||
|
@ -50,11 +52,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)
|
||||
|
|
|
@ -4,6 +4,7 @@ import PropTypes from 'prop-types'
|
|||
import {connect} from 'react-redux'
|
||||
import _ from 'lodash'
|
||||
import NanoDate from 'nano-date'
|
||||
import ReactResizeDetector from 'react-resize-detector'
|
||||
|
||||
import Dygraphs from 'src/external/dygraph'
|
||||
import DygraphLegend from 'src/shared/components/DygraphLegend'
|
||||
|
@ -353,6 +354,11 @@ class Dygraph extends Component {
|
|||
/>
|
||||
)}
|
||||
{nestedGraph && React.cloneElement(nestedGraph, {staticLegendHeight})}
|
||||
<ReactResizeDetector
|
||||
handleWidth={true}
|
||||
handleHeight={true}
|
||||
onResize={this.resize}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,12 +1,21 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, {PureComponent, MouseEvent} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import _ from 'lodash'
|
||||
import {INFLUXQL_FUNCTIONS} from 'src/data_explorer/constants'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface Props {
|
||||
onApply: (item: string[]) => void
|
||||
selectedItems: string[]
|
||||
singleSelect: boolean
|
||||
}
|
||||
|
||||
interface State {
|
||||
localSelectedItems: string[]
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class FunctionSelector extends Component {
|
||||
class FunctionSelector extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
|
@ -15,60 +24,20 @@ class FunctionSelector extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps) {
|
||||
public componentWillUpdate(nextProps) {
|
||||
if (!_.isEqual(this.props.selectedItems, nextProps.selectedItems)) {
|
||||
this.setState({localSelectedItems: nextProps.selectedItems})
|
||||
}
|
||||
}
|
||||
|
||||
onSelect = (item, e) => {
|
||||
e.stopPropagation()
|
||||
|
||||
const {localSelectedItems} = this.state
|
||||
|
||||
let nextItems
|
||||
if (this.isSelected(item)) {
|
||||
nextItems = localSelectedItems.filter(i => i !== item)
|
||||
} else {
|
||||
nextItems = [...localSelectedItems, item]
|
||||
}
|
||||
|
||||
this.setState({localSelectedItems: nextItems})
|
||||
}
|
||||
|
||||
onSingleSelect = item => {
|
||||
if (item === this.state.localSelectedItems[0]) {
|
||||
this.props.onApply([])
|
||||
this.setState({localSelectedItems: []})
|
||||
} else {
|
||||
this.props.onApply([item])
|
||||
this.setState({localSelectedItems: [item]})
|
||||
}
|
||||
}
|
||||
|
||||
isSelected = item => {
|
||||
return !!this.state.localSelectedItems.find(text => text === item)
|
||||
}
|
||||
|
||||
handleApplyFunctions = e => {
|
||||
e.stopPropagation()
|
||||
|
||||
this.props.onApply(this.state.localSelectedItems)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {localSelectedItems} = this.state
|
||||
public render() {
|
||||
const {singleSelect} = this.props
|
||||
|
||||
return (
|
||||
<div className="function-selector">
|
||||
{singleSelect ? null : (
|
||||
{!singleSelect && (
|
||||
<div className="function-selector--header">
|
||||
<span>
|
||||
{localSelectedItems.length > 0
|
||||
? `${localSelectedItems.length} Selected`
|
||||
: 'Select functions below'}
|
||||
</span>
|
||||
<span>{this.headerText}</span>
|
||||
<div
|
||||
className="btn btn-xs btn-success"
|
||||
onClick={this.handleApplyFunctions}
|
||||
|
@ -100,14 +69,49 @@ class FunctionSelector extends Component {
|
|||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, bool, func, string} = PropTypes
|
||||
private get headerText(): string {
|
||||
const numItems = this.state.localSelectedItems.length
|
||||
if (!numItems) {
|
||||
return 'Select functions below'
|
||||
}
|
||||
|
||||
FunctionSelector.propTypes = {
|
||||
onApply: func.isRequired,
|
||||
selectedItems: arrayOf(string.isRequired).isRequired,
|
||||
singleSelect: bool,
|
||||
return `${numItems} Selected`
|
||||
}
|
||||
|
||||
private onSelect = (item: string, e: MouseEvent<HTMLDivElement>): void => {
|
||||
e.stopPropagation()
|
||||
|
||||
const {localSelectedItems} = this.state
|
||||
|
||||
let nextItems
|
||||
if (this.isSelected(item)) {
|
||||
nextItems = localSelectedItems.filter(i => i !== item)
|
||||
} else {
|
||||
nextItems = [...localSelectedItems, item]
|
||||
}
|
||||
|
||||
this.setState({localSelectedItems: nextItems})
|
||||
}
|
||||
|
||||
private onSingleSelect = (item: string): void => {
|
||||
if (item === this.state.localSelectedItems[0]) {
|
||||
this.props.onApply([])
|
||||
this.setState({localSelectedItems: []})
|
||||
} else {
|
||||
this.props.onApply([item])
|
||||
this.setState({localSelectedItems: [item]})
|
||||
}
|
||||
}
|
||||
|
||||
private isSelected = (item: string): boolean => {
|
||||
return !!this.state.localSelectedItems.find(text => text === item)
|
||||
}
|
||||
|
||||
private handleApplyFunctions = (e: MouseEvent<HTMLDivElement>): void => {
|
||||
e.stopPropagation()
|
||||
this.props.onApply(this.state.localSelectedItems)
|
||||
}
|
||||
}
|
||||
|
||||
export default FunctionSelector
|
|
@ -1,7 +1,8 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import getLastValues, {TimeSeriesResponse} from 'src/shared/parsing/lastValues'
|
||||
import Gauge from 'src/shared/components/Gauge'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {DEFAULT_GAUGE_COLORS} from 'src/shared/constants/thresholds'
|
||||
import {stringifyColorValues} from 'src/shared/constants/colorOperations'
|
||||
|
@ -25,10 +26,6 @@ interface Props {
|
|||
prefix: string
|
||||
suffix: string
|
||||
resizerTopHeight?: number
|
||||
resizeCoords?: {
|
||||
i: string
|
||||
h: number
|
||||
}
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
|
@ -52,11 +49,11 @@ class GaugeChart extends PureComponent<Props> {
|
|||
<div className="single-stat">
|
||||
<Gauge
|
||||
width="900"
|
||||
height={this.height}
|
||||
colors={colors}
|
||||
gaugePosition={this.lastValueForGauge}
|
||||
height={this.height}
|
||||
prefix={prefix}
|
||||
suffix={suffix}
|
||||
gaugePosition={this.lastValueForGauge}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
@ -65,22 +62,7 @@ class GaugeChart extends PureComponent<Props> {
|
|||
private get height(): string {
|
||||
const {resizerTopHeight} = this.props
|
||||
|
||||
return (
|
||||
this.resizeCoordsHeight ||
|
||||
this.initialCellHeight ||
|
||||
resizerTopHeight ||
|
||||
300
|
||||
).toString()
|
||||
}
|
||||
|
||||
private get resizeCoordsHeight(): string {
|
||||
const {resizeCoords} = this.props
|
||||
|
||||
if (resizeCoords && this.isResizing) {
|
||||
return (resizeCoords.h * DASHBOARD_LAYOUT_ROW_HEIGHT).toString()
|
||||
}
|
||||
|
||||
return null
|
||||
return (this.initialCellHeight || resizerTopHeight || 300).toString()
|
||||
}
|
||||
|
||||
private get initialCellHeight(): string {
|
||||
|
@ -93,11 +75,6 @@ class GaugeChart extends PureComponent<Props> {
|
|||
return null
|
||||
}
|
||||
|
||||
private get isResizing(): boolean {
|
||||
const {resizeCoords, cellID} = this.props
|
||||
return resizeCoords ? cellID === resizeCoords.i : false
|
||||
}
|
||||
|
||||
private get lastValueForGauge(): number {
|
||||
const {data} = this.props
|
||||
const {lastValues} = getLastValues(data)
|
||||
|
|
|
@ -69,7 +69,6 @@ const Layout = (
|
|||
autoRefresh,
|
||||
manualRefresh,
|
||||
onDeleteCell,
|
||||
resizeCoords,
|
||||
onCancelEditCell,
|
||||
onStopAddAnnotation,
|
||||
onSummonOverlayTechnologies,
|
||||
|
@ -110,7 +109,6 @@ const Layout = (
|
|||
manualRefresh={manualRefresh}
|
||||
onStopAddAnnotation={onStopAddAnnotation}
|
||||
grabDataForDownload={grabDataForDownload}
|
||||
resizeCoords={resizeCoords}
|
||||
queries={buildQueriesForLayouts(
|
||||
cell,
|
||||
getSource(cell, source, sources, defaultSource),
|
||||
|
@ -191,7 +189,6 @@ const propTypes = {
|
|||
isStatusPage: bool,
|
||||
isEditable: bool,
|
||||
onCancelEditCell: func,
|
||||
resizeCoords: shape(),
|
||||
onZoom: func,
|
||||
sources: arrayOf(shape()),
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ReactGridLayout, {WidthProvider} from 'react-grid-layout'
|
||||
import {ResizableBox} from 'react-resizable'
|
||||
|
||||
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
|
@ -26,7 +25,6 @@ class LayoutRenderer extends Component {
|
|||
|
||||
this.state = {
|
||||
rowHeight: this.calculateRowHeight(),
|
||||
resizeCoords: null,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,8 +34,16 @@ class LayoutRenderer extends Component {
|
|||
}
|
||||
const newCells = this.props.cells.map(cell => {
|
||||
const l = layout.find(ly => ly.i === cell.i)
|
||||
const newLayout = {x: l.x, y: l.y, h: l.h, w: l.w}
|
||||
return {...cell, ...newLayout}
|
||||
const newLayout = {
|
||||
x: l.x,
|
||||
y: l.y,
|
||||
h: l.h,
|
||||
w: l.w,
|
||||
}
|
||||
return {
|
||||
...cell,
|
||||
...newLayout,
|
||||
}
|
||||
})
|
||||
this.props.onPositionChange(newCells)
|
||||
}
|
||||
|
@ -56,10 +62,6 @@ class LayoutRenderer extends Component {
|
|||
: DASHBOARD_LAYOUT_ROW_HEIGHT
|
||||
}
|
||||
|
||||
handleCellResize = () => {
|
||||
this.resizeCoords = this.setState({resizeCoords: new Date()})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
host,
|
||||
|
@ -79,70 +81,63 @@ class LayoutRenderer extends Component {
|
|||
onSummonOverlayTechnologies,
|
||||
} = this.props
|
||||
|
||||
const {rowHeight, resizeCoords} = this.state
|
||||
const {rowHeight} = this.state
|
||||
const isDashboard = !!this.props.onPositionChange
|
||||
|
||||
return (
|
||||
<ResizableBox
|
||||
height={Infinity}
|
||||
width={Infinity}
|
||||
onResize={this.handleCellResize}
|
||||
<Authorized
|
||||
requiredRole={EDITOR_ROLE}
|
||||
propsOverride={{
|
||||
isDraggable: false,
|
||||
isResizable: false,
|
||||
draggableHandle: null,
|
||||
}}
|
||||
>
|
||||
<Authorized
|
||||
requiredRole={EDITOR_ROLE}
|
||||
propsOverride={{
|
||||
isDraggable: false,
|
||||
isResizable: false,
|
||||
draggableHandle: null,
|
||||
}}
|
||||
<GridLayout
|
||||
layout={cells}
|
||||
cols={12}
|
||||
rowHeight={rowHeight}
|
||||
margin={[LAYOUT_MARGIN, LAYOUT_MARGIN]}
|
||||
containerPadding={[0, 0]}
|
||||
useCSSTransforms={false}
|
||||
onResize={this.handleCellResize}
|
||||
onLayoutChange={this.handleLayoutChange}
|
||||
draggableHandle={'.dash-graph--draggable'}
|
||||
isDraggable={isDashboard}
|
||||
isResizable={isDashboard}
|
||||
>
|
||||
<GridLayout
|
||||
layout={cells}
|
||||
cols={12}
|
||||
rowHeight={rowHeight}
|
||||
margin={[LAYOUT_MARGIN, LAYOUT_MARGIN]}
|
||||
containerPadding={[0, 0]}
|
||||
useCSSTransforms={false}
|
||||
onResize={this.handleCellResize}
|
||||
onLayoutChange={this.handleLayoutChange}
|
||||
draggableHandle={'.dash-graph--draggable'}
|
||||
isDraggable={isDashboard}
|
||||
isResizable={isDashboard}
|
||||
>
|
||||
{cells.map(cell => (
|
||||
<div key={cell.i}>
|
||||
<Authorized
|
||||
requiredRole={EDITOR_ROLE}
|
||||
propsOverride={{
|
||||
isEditable: false,
|
||||
}}
|
||||
>
|
||||
<Layout
|
||||
key={cell.i}
|
||||
cell={cell}
|
||||
host={host}
|
||||
source={source}
|
||||
onZoom={onZoom}
|
||||
sources={sources}
|
||||
templates={templates}
|
||||
timeRange={timeRange}
|
||||
isEditable={isEditable}
|
||||
onEditCell={onEditCell}
|
||||
autoRefresh={autoRefresh}
|
||||
resizeCoords={resizeCoords}
|
||||
onDeleteCell={onDeleteCell}
|
||||
onCloneCell={onCloneCell}
|
||||
manualRefresh={manualRefresh}
|
||||
onCancelEditCell={onCancelEditCell}
|
||||
onStopAddAnnotation={this.handleStopAddAnnotation}
|
||||
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
|
||||
/>
|
||||
</Authorized>
|
||||
</div>
|
||||
))}
|
||||
</GridLayout>
|
||||
</Authorized>
|
||||
</ResizableBox>
|
||||
{cells.map(cell => (
|
||||
<div key={cell.i}>
|
||||
<Authorized
|
||||
requiredRole={EDITOR_ROLE}
|
||||
propsOverride={{
|
||||
isEditable: false,
|
||||
}}
|
||||
>
|
||||
<Layout
|
||||
key={cell.i}
|
||||
cell={cell}
|
||||
host={host}
|
||||
source={source}
|
||||
onZoom={onZoom}
|
||||
sources={sources}
|
||||
templates={templates}
|
||||
timeRange={timeRange}
|
||||
isEditable={isEditable}
|
||||
onEditCell={onEditCell}
|
||||
autoRefresh={autoRefresh}
|
||||
onDeleteCell={onDeleteCell}
|
||||
onCloneCell={onCloneCell}
|
||||
manualRefresh={manualRefresh}
|
||||
onCancelEditCell={onCancelEditCell}
|
||||
onStopAddAnnotation={this.handleStopAddAnnotation}
|
||||
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
|
||||
/>
|
||||
</Authorized>
|
||||
</div>
|
||||
))}
|
||||
</GridLayout>
|
||||
</Authorized>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,7 +65,6 @@ class LineGraph extends Component {
|
|||
cellHeight,
|
||||
ruleValues,
|
||||
isBarGraph,
|
||||
resizeCoords,
|
||||
isRefreshing,
|
||||
setResolution,
|
||||
isGraphFilled,
|
||||
|
@ -127,7 +126,6 @@ class LineGraph extends Component {
|
|||
isBarGraph={isBarGraph}
|
||||
timeSeries={timeSeries}
|
||||
ruleValues={ruleValues}
|
||||
resizeCoords={resizeCoords}
|
||||
dygraphSeries={dygraphSeries}
|
||||
setResolution={setResolution}
|
||||
handleSetHoverTime={handleSetHoverTime}
|
||||
|
@ -211,7 +209,6 @@ LineGraph.propTypes = {
|
|||
setResolution: func,
|
||||
cellHeight: number,
|
||||
onZoom: func,
|
||||
resizeCoords: shape(),
|
||||
queries: arrayOf(shape({}).isRequired).isRequired,
|
||||
data: arrayOf(shape({}).isRequired).isRequired,
|
||||
colors: colorsStringSchema,
|
||||
|
|
|
@ -1,69 +1,48 @@
|
|||
import React, {Component, ReactElement} from 'react'
|
||||
import React, {SFC, ReactNode} from 'react'
|
||||
import LoadingDots from 'src/shared/components/LoadingDots'
|
||||
import classnames from 'classnames'
|
||||
|
||||
interface Status {
|
||||
error: string
|
||||
success: string
|
||||
warn: string
|
||||
loading: boolean
|
||||
}
|
||||
import {Status} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
children: ReactElement<any>
|
||||
status: Status
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
class QueryStatus extends Component<Props> {
|
||||
public render() {
|
||||
const {status, children} = this.props
|
||||
|
||||
if (!status) {
|
||||
return <div className="query-editor--status">{children}</div>
|
||||
}
|
||||
|
||||
if (!!status.loading) {
|
||||
return (
|
||||
<div className="query-editor--status">
|
||||
<LoadingDots />
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const QueryStatus: SFC<Props> = ({status, children}) => {
|
||||
if (!status) {
|
||||
return <div className="query-editor--status">{children}</div>
|
||||
}
|
||||
|
||||
if (status.loading) {
|
||||
return (
|
||||
<div className="query-editor--status">
|
||||
<span className={this.className}>
|
||||
{this.icon}
|
||||
{status.error || status.warn || status.success}
|
||||
</span>
|
||||
<LoadingDots className="query-editor--loading" />
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get className(): string {
|
||||
const {status} = this.props
|
||||
|
||||
return classnames('query-status-output', {
|
||||
'query-status-output--error': status.error,
|
||||
'query-status-output--success': status.success,
|
||||
'query-status-output--warning': status.warn,
|
||||
})
|
||||
}
|
||||
|
||||
private get icon(): JSX.Element {
|
||||
const {status} = this.props
|
||||
return (
|
||||
return (
|
||||
<div className="query-editor--status">
|
||||
<span
|
||||
className={classnames('icon', {
|
||||
stop: status.error,
|
||||
checkmark: status.success,
|
||||
'alert-triangle': status.warn,
|
||||
className={classnames('query-status-output', {
|
||||
'query-status-output--error': status.error,
|
||||
'query-status-output--success': status.success,
|
||||
'query-status-output--warning': status.warn,
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
<span
|
||||
className={classnames('icon', {
|
||||
stop: status.error,
|
||||
checkmark: status.success,
|
||||
'alert-triangle': status.warn,
|
||||
})}
|
||||
/>
|
||||
{status.error || status.warn || status.success}
|
||||
</span>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default QueryStatus
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {AuthLinks, Organization, Role, User, Me} from './auth'
|
||||
import {Template, Cell, CellQuery, Legend} from './dashboard'
|
||||
import {Template, Cell, CellQuery, Legend, Axes} from './dashboard'
|
||||
import {
|
||||
GroupBy,
|
||||
QueryConfig,
|
||||
|
@ -52,4 +52,5 @@ export {
|
|||
Task,
|
||||
Notification,
|
||||
NotificationFunc,
|
||||
Axes,
|
||||
}
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import {ReactNode} from 'react'
|
||||
|
||||
export type DropdownItem =
|
||||
| {
|
||||
text: string
|
||||
}
|
||||
| string
|
||||
export interface DropdownItem {
|
||||
text: string
|
||||
}
|
||||
|
||||
export interface DropdownAction {
|
||||
icon: string
|
||||
|
|
|
@ -72,11 +72,11 @@ function generateResponseWithLinks<T extends object>(
|
|||
}
|
||||
|
||||
interface RequestParams {
|
||||
url: string
|
||||
url: string | string[]
|
||||
resource?: string | null
|
||||
id?: string | null
|
||||
method?: string
|
||||
data?: object
|
||||
data?: object | string
|
||||
params?: object
|
||||
headers?: object
|
||||
}
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
import uuid from 'uuid'
|
||||
|
||||
import {NULL_STRING} from 'src/shared/constants/queryFillOptions'
|
||||
import {QueryConfig} from 'src/types'
|
||||
|
||||
interface DefaultQueryArgs {
|
||||
id?: string
|
||||
isKapacitorRule?: boolean
|
||||
}
|
||||
|
||||
const defaultQueryConfig = (
|
||||
{id, isKapacitorRule = false} = {id: uuid.v4()}
|
||||
) => {
|
||||
{id, isKapacitorRule = false}: DefaultQueryArgs = {id: uuid.v4()}
|
||||
): QueryConfig => {
|
||||
const queryConfig = {
|
||||
id,
|
||||
database: null,
|
||||
|
|
|
@ -8,16 +8,23 @@ import {
|
|||
} from 'src/shared/reducers/helpers/fields'
|
||||
|
||||
import {
|
||||
Tag,
|
||||
Field,
|
||||
GroupBy,
|
||||
Namespace,
|
||||
QueryConfig,
|
||||
Tag,
|
||||
TagValues,
|
||||
TimeShift,
|
||||
QueryConfig,
|
||||
ApplyFuncsToFieldArgs,
|
||||
} from 'src/types'
|
||||
|
||||
export const editRawText = (
|
||||
query: QueryConfig,
|
||||
rawText: string
|
||||
): QueryConfig => {
|
||||
return {...query, rawText}
|
||||
}
|
||||
|
||||
export const chooseNamespace = (
|
||||
query: QueryConfig,
|
||||
namespace: Namespace,
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import AJAX from 'src/utils/ajax'
|
||||
|
||||
interface ProxyQuery {
|
||||
source: string
|
||||
source: string | string[]
|
||||
query: string
|
||||
db: string
|
||||
db?: string
|
||||
rp?: string
|
||||
tempVars?: string
|
||||
resolution?: string
|
||||
tempVars?: any[]
|
||||
resolution?: number
|
||||
}
|
||||
|
||||
export async function proxy<T = any>({
|
||||
|
|
|
@ -16,6 +16,14 @@ const queryConfigActions = {
|
|||
editRawTextAsync: () => {},
|
||||
addInitialField: () => {},
|
||||
editQueryStatus: () => {},
|
||||
deleteQuery: () => {},
|
||||
fill: () => {},
|
||||
removeFuncs: () => {},
|
||||
editRawText: () => {},
|
||||
setTimeRange: () => {},
|
||||
updateRawQuery: () => {},
|
||||
updateQueryConfig: () => {},
|
||||
timeShift: () => {},
|
||||
}
|
||||
|
||||
const setup = () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import reducer from 'src/data_explorer/reducers/queryConfigs'
|
||||
|
||||
import defaultQueryConfig from 'utils/defaultQueryConfig'
|
||||
import defaultQueryConfig from 'src/utils/defaultQueryConfig'
|
||||
import {
|
||||
fill,
|
||||
timeShift,
|
||||
|
@ -9,6 +9,7 @@ import {
|
|||
groupByTime,
|
||||
toggleField,
|
||||
removeFuncs,
|
||||
editRawText,
|
||||
updateRawQuery,
|
||||
editQueryStatus,
|
||||
chooseNamespace,
|
||||
|
@ -17,36 +18,46 @@ import {
|
|||
addInitialField,
|
||||
updateQueryConfig,
|
||||
toggleTagAcceptance,
|
||||
ActionAddQuery,
|
||||
} from 'src/data_explorer/actions/view'
|
||||
|
||||
import {LINEAR, NULL_STRING} from 'shared/constants/queryFillOptions'
|
||||
import {LINEAR, NULL_STRING} from 'src/shared/constants/queryFillOptions'
|
||||
|
||||
const fakeAddQueryAction = (panelID, queryID) => {
|
||||
const fakeAddQueryAction = (queryID: string): ActionAddQuery => {
|
||||
return {
|
||||
type: 'DE_ADD_QUERY',
|
||||
payload: {panelID, queryID},
|
||||
payload: {
|
||||
queryID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function buildInitialState(queryID, params) {
|
||||
return Object.assign({}, defaultQueryConfig({id: queryID}), params)
|
||||
function buildInitialState(queryID, params?) {
|
||||
return {
|
||||
...defaultQueryConfig({
|
||||
id: queryID,
|
||||
}),
|
||||
...params,
|
||||
}
|
||||
}
|
||||
|
||||
describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
||||
const queryID = 123
|
||||
const queryID = '123'
|
||||
|
||||
it('can add a query', () => {
|
||||
const state = reducer({}, fakeAddQueryAction('blah', queryID))
|
||||
const state = reducer({}, fakeAddQueryAction(queryID))
|
||||
|
||||
const actual = state[queryID]
|
||||
const expected = defaultQueryConfig({id: queryID})
|
||||
const expected = defaultQueryConfig({
|
||||
id: queryID,
|
||||
})
|
||||
expect(actual).toEqual(expected)
|
||||
})
|
||||
|
||||
describe('choosing db, rp, and measurement', () => {
|
||||
let state
|
||||
beforeEach(() => {
|
||||
state = reducer({}, fakeAddQueryAction('any', queryID))
|
||||
state = reducer({}, fakeAddQueryAction(queryID))
|
||||
})
|
||||
|
||||
it('sets the db and rp', () => {
|
||||
|
@ -72,7 +83,7 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
describe('a query has measurements and fields', () => {
|
||||
let state
|
||||
beforeEach(() => {
|
||||
const one = reducer({}, fakeAddQueryAction('any', queryID))
|
||||
const one = reducer({}, fakeAddQueryAction(queryID))
|
||||
const two = reducer(
|
||||
one,
|
||||
chooseNamespace(queryID, {
|
||||
|
@ -81,14 +92,13 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
})
|
||||
)
|
||||
const three = reducer(two, chooseMeasurement(queryID, 'disk'))
|
||||
const field = {
|
||||
value: 'a great field',
|
||||
type: 'field',
|
||||
}
|
||||
const groupBy = {}
|
||||
|
||||
state = reducer(
|
||||
three,
|
||||
addInitialField(queryID, {
|
||||
value: 'a great field',
|
||||
type: 'field',
|
||||
})
|
||||
)
|
||||
state = reducer(three, addInitialField(queryID, field, groupBy))
|
||||
})
|
||||
|
||||
describe('choosing a new namespace', () => {
|
||||
|
@ -143,7 +153,10 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
expect(newState[queryID].fields.length).toBe(2)
|
||||
expect(newState[queryID].fields[1].alias).toEqual('mean_f2')
|
||||
expect(newState[queryID].fields[1].args).toEqual([
|
||||
{value: 'f2', type: 'field'},
|
||||
{
|
||||
value: 'f2',
|
||||
type: 'field',
|
||||
},
|
||||
])
|
||||
expect(newState[queryID].fields[1].value).toEqual('mean')
|
||||
})
|
||||
|
@ -164,7 +177,10 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
expect(newState[queryID].fields[1].value).toBe('mean')
|
||||
expect(newState[queryID].fields[1].alias).toBe('mean_f2')
|
||||
expect(newState[queryID].fields[1].args).toEqual([
|
||||
{value: 'f2', type: 'field'},
|
||||
{
|
||||
value: 'f2',
|
||||
type: 'field',
|
||||
},
|
||||
])
|
||||
expect(newState[queryID].fields[1].type).toBe('func')
|
||||
})
|
||||
|
@ -175,7 +191,10 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
|
||||
const newState = reducer(
|
||||
state,
|
||||
toggleField(queryID, {value: 'fk1', type: 'field'})
|
||||
toggleField(queryID, {
|
||||
value: 'fk1',
|
||||
type: 'field',
|
||||
})
|
||||
)
|
||||
|
||||
expect(newState[queryID].fields.length).toBe(1)
|
||||
|
@ -185,58 +204,122 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
|
||||
describe('DE_APPLY_FUNCS_TO_FIELD', () => {
|
||||
it('applies new functions to a field', () => {
|
||||
const f1 = {value: 'f1', type: 'field'}
|
||||
const f2 = {value: 'f2', type: 'field'}
|
||||
const f1 = {
|
||||
value: 'f1',
|
||||
type: 'field',
|
||||
}
|
||||
const f2 = {
|
||||
value: 'f2',
|
||||
type: 'field',
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
[queryID]: {
|
||||
id: 123,
|
||||
[queryID]: buildInitialState(queryID, {
|
||||
id: '123',
|
||||
database: 'db1',
|
||||
measurement: 'm1',
|
||||
fields: [
|
||||
{value: 'fn1', type: 'func', args: [f1], alias: `fn1_${f1.value}`},
|
||||
{value: 'fn1', type: 'func', args: [f2], alias: `fn1_${f2.value}`},
|
||||
{value: 'fn2', type: 'func', args: [f1], alias: `fn2_${f1.value}`},
|
||||
{
|
||||
value: 'fn1',
|
||||
type: 'func',
|
||||
args: [f1],
|
||||
alias: `fn1_${f1.value}`,
|
||||
},
|
||||
{
|
||||
value: 'fn1',
|
||||
type: 'func',
|
||||
args: [f2],
|
||||
alias: `fn1_${f2.value}`,
|
||||
},
|
||||
{
|
||||
value: 'fn2',
|
||||
type: 'func',
|
||||
args: [f1],
|
||||
alias: `fn2_${f1.value}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const action = applyFuncsToField(queryID, {
|
||||
field: {value: 'f1', type: 'field'},
|
||||
field: {
|
||||
value: 'f1',
|
||||
type: 'field',
|
||||
},
|
||||
funcs: [
|
||||
{value: 'fn3', type: 'func', args: []},
|
||||
{value: 'fn4', type: 'func', args: []},
|
||||
{
|
||||
value: 'fn3',
|
||||
type: 'func',
|
||||
},
|
||||
{
|
||||
value: 'fn4',
|
||||
type: 'func',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryID].fields).toEqual([
|
||||
{value: 'fn3', type: 'func', args: [f1], alias: `fn3_${f1.value}`},
|
||||
{value: 'fn4', type: 'func', args: [f1], alias: `fn4_${f1.value}`},
|
||||
{value: 'fn1', type: 'func', args: [f2], alias: `fn1_${f2.value}`},
|
||||
{
|
||||
value: 'fn3',
|
||||
type: 'func',
|
||||
args: [f1],
|
||||
alias: `fn3_${f1.value}`,
|
||||
},
|
||||
{
|
||||
value: 'fn4',
|
||||
type: 'func',
|
||||
args: [f1],
|
||||
alias: `fn4_${f1.value}`,
|
||||
},
|
||||
{
|
||||
value: 'fn1',
|
||||
type: 'func',
|
||||
args: [f2],
|
||||
alias: `fn1_${f2.value}`,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('DE_REMOVE_FUNCS', () => {
|
||||
it('removes all functions and group by time when one field has no funcs applied', () => {
|
||||
const f1 = {value: 'f1', type: 'field'}
|
||||
const f2 = {value: 'f2', type: 'field'}
|
||||
const f1 = {
|
||||
value: 'f1',
|
||||
type: 'field',
|
||||
}
|
||||
const f2 = {
|
||||
value: 'f2',
|
||||
type: 'field',
|
||||
}
|
||||
const fields = [
|
||||
{value: 'fn1', type: 'func', args: [f1], alias: `fn1_${f1.value}`},
|
||||
{value: 'fn1', type: 'func', args: [f2], alias: `fn1_${f2.value}`},
|
||||
{
|
||||
value: 'fn1',
|
||||
type: 'func',
|
||||
args: [f1],
|
||||
alias: `fn1_${f1.value}`,
|
||||
},
|
||||
{
|
||||
value: 'fn1',
|
||||
type: 'func',
|
||||
args: [f2],
|
||||
alias: `fn1_${f2.value}`,
|
||||
},
|
||||
]
|
||||
const groupBy = {time: '1m', tags: []}
|
||||
const groupBy = {
|
||||
time: '1m',
|
||||
tags: [],
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
[queryID]: {
|
||||
id: 123,
|
||||
[queryID]: buildInitialState(queryID, {
|
||||
id: '123',
|
||||
database: 'db1',
|
||||
measurement: 'm1',
|
||||
fields,
|
||||
groupBy,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const action = removeFuncs(queryID, fields, groupBy)
|
||||
|
@ -260,6 +343,7 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const action = chooseTag(queryID, {
|
||||
key: 'k1',
|
||||
value: 'v1',
|
||||
|
@ -314,14 +398,17 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
describe('DE_GROUP_BY_TAG', () => {
|
||||
it('adds a tag key/value to the query', () => {
|
||||
const initialState = {
|
||||
[queryID]: {
|
||||
id: 123,
|
||||
[queryID]: buildInitialState(queryID, {
|
||||
id: '123',
|
||||
database: 'db1',
|
||||
measurement: 'm1',
|
||||
fields: [],
|
||||
tags: {},
|
||||
groupBy: {tags: [], time: null},
|
||||
},
|
||||
groupBy: {
|
||||
tags: [],
|
||||
time: null,
|
||||
},
|
||||
}),
|
||||
}
|
||||
const action = groupByTag(queryID, 'k1')
|
||||
|
||||
|
@ -334,16 +421,21 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
})
|
||||
|
||||
it('removes a tag if the given tag key is already in the GROUP BY list', () => {
|
||||
const initialState = {
|
||||
[queryID]: {
|
||||
id: 123,
|
||||
database: 'db1',
|
||||
measurement: 'm1',
|
||||
fields: [],
|
||||
tags: {},
|
||||
groupBy: {tags: ['k1'], time: null},
|
||||
const query = {
|
||||
id: '123',
|
||||
database: 'db1',
|
||||
measurement: 'm1',
|
||||
fields: [],
|
||||
tags: {},
|
||||
groupBy: {
|
||||
tags: ['k1'],
|
||||
time: null,
|
||||
},
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
[queryID]: buildInitialState(queryID, query),
|
||||
}
|
||||
const action = groupByTag(queryID, 'k1')
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
@ -389,7 +481,8 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
const initialState = {
|
||||
[queryID]: buildInitialState(queryID),
|
||||
}
|
||||
const expected = defaultQueryConfig({id: queryID}, {rawText: 'hello'})
|
||||
const id = {id: queryID}
|
||||
const expected = defaultQueryConfig(id)
|
||||
const action = updateQueryConfig(expected)
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
@ -413,12 +506,12 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
const initialState = {
|
||||
[queryID]: buildInitialState(queryID),
|
||||
}
|
||||
const status = 'your query was sweet'
|
||||
const status = {success: 'Your query was very nice'}
|
||||
const action = editQueryStatus(queryID, status)
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryID].status).toBe(status)
|
||||
expect(nextState[queryID].status).toEqual(status)
|
||||
})
|
||||
|
||||
describe('DE_FILL', () => {
|
||||
|
@ -476,11 +569,31 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
[queryID]: buildInitialState(queryID),
|
||||
}
|
||||
|
||||
const shift = {quantity: 1, unit: 'd', duration: '1d'}
|
||||
const shift = {
|
||||
quantity: '1',
|
||||
unit: 'd',
|
||||
duration: '1d',
|
||||
label: 'label',
|
||||
}
|
||||
|
||||
const action = timeShift(queryID, shift)
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryID].shifts).toEqual([shift])
|
||||
})
|
||||
})
|
||||
|
||||
describe('DE_EDIT_RAW_TEXT', () => {
|
||||
it('can edit the raw text', () => {
|
||||
const initialState = {
|
||||
[queryID]: buildInitialState(queryID),
|
||||
}
|
||||
|
||||
const rawText = 'im the raw text'
|
||||
const action = editRawText(queryID, rawText)
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryID].rawText).toEqual(rawText)
|
||||
})
|
||||
})
|
||||
})
|
23
ui/yarn.lock
23
ui/yarn.lock
|
@ -5346,6 +5346,10 @@ lodash.create@3.1.1:
|
|||
lodash._basecreate "^3.0.0"
|
||||
lodash._isiterateecall "^3.0.0"
|
||||
|
||||
lodash.debounce@^4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
|
||||
|
||||
lodash.deburr@^3.0.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.deburr/-/lodash.deburr-3.2.0.tgz#6da8f54334a366a7cf4c4c76ef8d80aa1b365ed5"
|
||||
|
@ -5431,6 +5435,10 @@ lodash.tail@^4.1.1:
|
|||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664"
|
||||
|
||||
lodash.throttle@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
|
||||
|
||||
lodash.uniq@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||
|
@ -7207,13 +7215,22 @@ react-redux@^4.4.0:
|
|||
loose-envify "^1.1.0"
|
||||
prop-types "^15.5.4"
|
||||
|
||||
react-resizable@1.x, react-resizable@^1.7.5:
|
||||
react-resizable@1.x:
|
||||
version "1.7.5"
|
||||
resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.7.5.tgz#83eb75bb3684da6989bbbf4f826e1470f0af902e"
|
||||
dependencies:
|
||||
prop-types "15.x"
|
||||
react-draggable "^2.2.6 || ^3.0.3"
|
||||
|
||||
react-resize-detector@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-2.3.0.tgz#57bad1ae26a28a62a2ddb678ba6ffdf8fa2b599c"
|
||||
dependencies:
|
||||
lodash.debounce "^4.0.8"
|
||||
lodash.throttle "^4.1.1"
|
||||
prop-types "^15.6.0"
|
||||
resize-observer-polyfill "^1.5.0"
|
||||
|
||||
react-router-redux@^4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/react-router-redux/-/react-router-redux-4.0.8.tgz#227403596b5151e182377dab835b5d45f0f8054e"
|
||||
|
@ -7584,6 +7601,10 @@ requires-port@1.0.x, requires-port@^1.0.0:
|
|||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
||||
|
||||
resize-observer-polyfill@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.0.tgz#660ff1d9712a2382baa2cad450a4716209f9ca69"
|
||||
|
||||
resolve-cwd@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
|
||||
|
|
Loading…
Reference in New Issue