Merge pull request #3431 from influxdata/chore/ts-data-explorer

Chore/ts data explorer
pull/3441/head
Andrew Watkins 2018-05-13 00:35:47 -07:00 committed by GitHub
commit c55276ccb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 2307 additions and 1585 deletions

View File

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

View File

@ -1,167 +0,0 @@
import uuid from 'uuid'
import {getQueryConfigAndStatus} from 'shared/apis'
import {errorThrown} from 'shared/actions/errors'
export const addQuery = (queryID = uuid.v4()) => ({
type: 'DE_ADD_QUERY',
payload: {
queryID,
},
})
export const deleteQuery = queryID => ({
type: 'DE_DELETE_QUERY',
payload: {
queryID,
},
})
export const toggleField = (queryID, fieldFunc) => ({
type: 'DE_TOGGLE_FIELD',
payload: {
queryID,
fieldFunc,
},
})
export const groupByTime = (queryID, time) => ({
type: 'DE_GROUP_BY_TIME',
payload: {
queryID,
time,
},
})
export const fill = (queryID, value) => ({
type: 'DE_FILL',
payload: {
queryID,
value,
},
})
export const removeFuncs = (queryID, fields, groupBy) => ({
type: 'DE_REMOVE_FUNCS',
payload: {
queryID,
fields,
groupBy,
},
})
export const applyFuncsToField = (queryID, fieldFunc, groupBy) => ({
type: 'DE_APPLY_FUNCS_TO_FIELD',
payload: {
queryID,
fieldFunc,
groupBy,
},
})
export const chooseTag = (queryID, tag) => ({
type: 'DE_CHOOSE_TAG',
payload: {
queryID,
tag,
},
})
export const chooseNamespace = (queryID, {database, retentionPolicy}) => ({
type: 'DE_CHOOSE_NAMESPACE',
payload: {
queryID,
database,
retentionPolicy,
},
})
export const chooseMeasurement = (queryID, measurement) => ({
type: 'DE_CHOOSE_MEASUREMENT',
payload: {
queryID,
measurement,
},
})
export const editRawText = (queryID, rawText) => ({
type: 'DE_EDIT_RAW_TEXT',
payload: {
queryID,
rawText,
},
})
export const setTimeRange = bounds => ({
type: 'DE_SET_TIME_RANGE',
payload: {
bounds,
},
})
export const groupByTag = (queryID, tagKey) => ({
type: 'DE_GROUP_BY_TAG',
payload: {
queryID,
tagKey,
},
})
export const toggleTagAcceptance = queryID => ({
type: 'DE_TOGGLE_TAG_ACCEPTANCE',
payload: {
queryID,
},
})
export const updateRawQuery = (queryID, text) => ({
type: 'DE_UPDATE_RAW_QUERY',
payload: {
queryID,
text,
},
})
export const updateQueryConfig = config => ({
type: 'DE_UPDATE_QUERY_CONFIG',
payload: {
config,
},
})
export const addInitialField = (queryID, field, groupBy) => ({
type: 'DE_ADD_INITIAL_FIELD',
payload: {
queryID,
field,
groupBy,
},
})
export const editQueryStatus = (queryID, status) => ({
type: 'DE_EDIT_QUERY_STATUS',
payload: {
queryID,
status,
},
})
export const timeShift = (queryID, shift) => ({
type: 'DE_TIME_SHIFT',
payload: {
queryID,
shift,
},
})
// Async actions
export const editRawTextAsync = (url, id, text) => async dispatch => {
try {
const {data} = await getQueryConfigAndStatus(url, [{query: text, id}])
const config = data.queries.find(q => q.id === id)
dispatch(updateQueryConfig(config.queryConfig))
} catch (error) {
dispatch(errorThrown(error))
}
}

View File

@ -0,0 +1,407 @@
import uuid from 'uuid'
import {getQueryConfigAndStatus} from 'src/shared/apis'
import {errorThrown} from 'src/shared/actions/errors'
import {
QueryConfig,
Status,
Field,
GroupBy,
Tag,
TimeRange,
TimeShift,
ApplyFuncsToFieldArgs,
} from 'src/types'
export type Action =
| ActionAddQuery
| ActionDeleteQuery
| ActionToggleField
| ActionGroupByTime
| ActionFill
| ActionRemoveFuncs
| ActionApplyFuncsToField
| ActionChooseTag
| ActionChooseNamspace
| ActionChooseMeasurement
| ActionEditRawText
| ActionSetTimeRange
| ActionGroupByTime
| ActionToggleField
| ActionUpdateRawQuery
| ActionQueryConfig
| ActionTimeShift
| ActionToggleTagAcceptance
| ActionToggleField
| ActionGroupByTag
| ActionEditQueryStatus
| ActionAddInitialField
export interface ActionAddQuery {
type: 'DE_ADD_QUERY'
payload: {
queryID: string
}
}
export const addQuery = (queryID: string = uuid.v4()): ActionAddQuery => ({
type: 'DE_ADD_QUERY',
payload: {
queryID,
},
})
interface ActionDeleteQuery {
type: 'DE_DELETE_QUERY'
payload: {
queryID: string
}
}
export const deleteQuery = (queryID: string): ActionDeleteQuery => ({
type: 'DE_DELETE_QUERY',
payload: {
queryID,
},
})
interface ActionToggleField {
type: 'DE_TOGGLE_FIELD'
payload: {
queryID: string
fieldFunc: Field
}
}
export const toggleField = (
queryID: string,
fieldFunc: Field
): ActionToggleField => ({
type: 'DE_TOGGLE_FIELD',
payload: {
queryID,
fieldFunc,
},
})
interface ActionGroupByTime {
type: 'DE_GROUP_BY_TIME'
payload: {
queryID: string
time: string
}
}
export const groupByTime = (
queryID: string,
time: string
): ActionGroupByTime => ({
type: 'DE_GROUP_BY_TIME',
payload: {
queryID,
time,
},
})
interface ActionFill {
type: 'DE_FILL'
payload: {
queryID: string
value: string
}
}
export const fill = (queryID: string, value: string): ActionFill => ({
type: 'DE_FILL',
payload: {
queryID,
value,
},
})
interface ActionRemoveFuncs {
type: 'DE_REMOVE_FUNCS'
payload: {
queryID: string
fields: Field[]
groupBy: GroupBy
}
}
export const removeFuncs = (
queryID: string,
fields: Field[],
groupBy: GroupBy
): ActionRemoveFuncs => ({
type: 'DE_REMOVE_FUNCS',
payload: {
queryID,
fields,
groupBy,
},
})
interface ActionApplyFuncsToField {
type: 'DE_APPLY_FUNCS_TO_FIELD'
payload: {
queryID: string
fieldFunc: ApplyFuncsToFieldArgs
groupBy: GroupBy
}
}
export const applyFuncsToField = (
queryID: string,
fieldFunc: ApplyFuncsToFieldArgs,
groupBy?: GroupBy
): ActionApplyFuncsToField => ({
type: 'DE_APPLY_FUNCS_TO_FIELD',
payload: {
queryID,
fieldFunc,
groupBy,
},
})
interface ActionChooseTag {
type: 'DE_CHOOSE_TAG'
payload: {
queryID: string
tag: Tag
}
}
export const chooseTag = (queryID: string, tag: Tag): ActionChooseTag => ({
type: 'DE_CHOOSE_TAG',
payload: {
queryID,
tag,
},
})
interface ActionChooseNamspace {
type: 'DE_CHOOSE_NAMESPACE'
payload: {
queryID: string
database: string
retentionPolicy: string
}
}
interface DBRP {
database: string
retentionPolicy: string
}
export const chooseNamespace = (
queryID: string,
{database, retentionPolicy}: DBRP
): ActionChooseNamspace => ({
type: 'DE_CHOOSE_NAMESPACE',
payload: {
queryID,
database,
retentionPolicy,
},
})
interface ActionChooseMeasurement {
type: 'DE_CHOOSE_MEASUREMENT'
payload: {
queryID: string
measurement: string
}
}
export const chooseMeasurement = (
queryID: string,
measurement: string
): ActionChooseMeasurement => ({
type: 'DE_CHOOSE_MEASUREMENT',
payload: {
queryID,
measurement,
},
})
interface ActionEditRawText {
type: 'DE_EDIT_RAW_TEXT'
payload: {
queryID: string
rawText: string
}
}
export const editRawText = (
queryID: string,
rawText: string
): ActionEditRawText => ({
type: 'DE_EDIT_RAW_TEXT',
payload: {
queryID,
rawText,
},
})
interface ActionSetTimeRange {
type: 'DE_SET_TIME_RANGE'
payload: {
bounds: TimeRange
}
}
export const setTimeRange = (bounds: TimeRange): ActionSetTimeRange => ({
type: 'DE_SET_TIME_RANGE',
payload: {
bounds,
},
})
interface ActionGroupByTag {
type: 'DE_GROUP_BY_TAG'
payload: {
queryID: string
tagKey: string
}
}
export const groupByTag = (
queryID: string,
tagKey: string
): ActionGroupByTag => ({
type: 'DE_GROUP_BY_TAG',
payload: {
queryID,
tagKey,
},
})
interface ActionToggleTagAcceptance {
type: 'DE_TOGGLE_TAG_ACCEPTANCE'
payload: {
queryID: string
}
}
export const toggleTagAcceptance = (
queryID: string
): ActionToggleTagAcceptance => ({
type: 'DE_TOGGLE_TAG_ACCEPTANCE',
payload: {
queryID,
},
})
interface ActionUpdateRawQuery {
type: 'DE_UPDATE_RAW_QUERY'
payload: {
queryID: string
text: string
}
}
export const updateRawQuery = (
queryID: string,
text: string
): ActionUpdateRawQuery => ({
type: 'DE_UPDATE_RAW_QUERY',
payload: {
queryID,
text,
},
})
interface ActionQueryConfig {
type: 'DE_UPDATE_QUERY_CONFIG'
payload: {
config: QueryConfig
}
}
export const updateQueryConfig = (config: QueryConfig): ActionQueryConfig => ({
type: 'DE_UPDATE_QUERY_CONFIG',
payload: {
config,
},
})
interface ActionAddInitialField {
type: 'DE_ADD_INITIAL_FIELD'
payload: {
queryID: string
field: Field
groupBy?: GroupBy
}
}
export const addInitialField = (
queryID: string,
field: Field,
groupBy: GroupBy
): ActionAddInitialField => ({
type: 'DE_ADD_INITIAL_FIELD',
payload: {
queryID,
field,
groupBy,
},
})
interface ActionEditQueryStatus {
type: 'DE_EDIT_QUERY_STATUS'
payload: {
queryID: string
status: Status
}
}
export const editQueryStatus = (
queryID: string,
status: Status
): ActionEditQueryStatus => ({
type: 'DE_EDIT_QUERY_STATUS',
payload: {
queryID,
status,
},
})
interface ActionTimeShift {
type: 'DE_TIME_SHIFT'
payload: {
queryID: string
shift: TimeShift
}
}
export const timeShift = (
queryID: string,
shift: TimeShift
): ActionTimeShift => ({
type: 'DE_TIME_SHIFT',
payload: {
queryID,
shift,
},
})
// Async actions
export const editRawTextAsync = (
url: string,
id: string,
text: string
) => async (dispatch): Promise<void> => {
try {
const {data} = await getQueryConfigAndStatus(url, [
{
query: text,
id,
},
])
const config = data.queries.find(q => q.id === id)
dispatch(updateQueryConfig(config.queryConfig))
} catch (error) {
dispatch(errorThrown(error))
}
}

View File

@ -1,13 +1,18 @@
import {writeLineProtocol as writeLineProtocolAJAX} from 'src/data_explorer/apis'
import {notify} from 'shared/actions/notifications'
import {notify} from 'src/shared/actions/notifications'
import {Source} from 'src/types'
import {
notifyDataWritten,
notifyDataWriteFailed,
} from 'shared/copy/notifications'
} from 'src/shared/copy/notifications'
export const writeLineProtocolAsync = (source, db, data) => async dispatch => {
export const writeLineProtocolAsync = (
source: Source,
db: string,
data: string
) => async (dispatch): Promise<void> => {
try {
await writeLineProtocolAJAX(source, db, data)
dispatch(notify(notifyDataWritten()))

View File

@ -1,8 +0,0 @@
import AJAX from 'src/utils/ajax'
export const writeLineProtocol = async (source, db, data) =>
await AJAX({
url: `${source.links.write}?db=${db}`,
method: 'POST',
data,
})

View File

@ -0,0 +1,67 @@
import AJAX from 'src/utils/ajax'
import _ from 'lodash'
import moment from 'moment'
import download from 'src/external/download'
import {proxy} from 'src/utils/queryUrlGenerator'
import {timeSeriesToTableGraph} from 'src/utils/timeSeriesTransformers'
import {dataToCSV} from 'src/shared/parsing/dataToCSV'
import {TEMPLATES} from 'src/shared/constants'
import {Source, QueryConfig} from 'src/types'
export const writeLineProtocol = async (
source: Source,
db: string,
data: string
): Promise<void> =>
await AJAX({
url: `${source.links.write}?db=${db}`,
method: 'POST',
data,
})
interface DeprecatedQuery {
id: string
host: string
queryConfig: QueryConfig
text: string
}
export const getDataForCSV = (
query: DeprecatedQuery,
errorThrown
) => async () => {
try {
const response = await fetchTimeSeriesForCSV({
source: query.host,
query: query.text,
tempVars: TEMPLATES,
})
const {data} = timeSeriesToTableGraph([{response}])
const name = csvName(query.queryConfig)
download(dataToCSV(data), `${name}.csv`, 'text/plain')
} catch (error) {
errorThrown(error, 'Unable to download .csv file')
console.error(error)
}
}
const fetchTimeSeriesForCSV = async ({source, query, tempVars}) => {
try {
const {data} = await proxy({source, query, tempVars})
return data
} catch (error) {
console.error(error)
throw error
}
}
const csvName = (query: QueryConfig): string => {
const db = _.get(query, 'database', '')
const rp = _.get(query, 'retentionPolicy', '')
const measurement = _.get(query, 'measurement', '')
const timestring = moment().format('YYYY-MM-DD-HH-mm')
return `${db}.${rp}.${measurement}.${timestring}`
}

View File

@ -1,27 +1,80 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import React, {PureComponent, KeyboardEvent} from 'react'
import Dropdown from 'shared/components/Dropdown'
import {QUERY_TEMPLATES} from 'src/data_explorer/constants'
import QueryStatus from 'shared/components/QueryStatus'
import Dropdown from 'src/shared/components/Dropdown'
import {QUERY_TEMPLATES, QueryTemplate} from 'src/data_explorer/constants'
import QueryStatus from 'src/shared/components/QueryStatus'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {QueryConfig} from 'src/types'
interface Props {
query: string
config: QueryConfig
onUpdate: (value: string) => void
}
interface State {
value: string
}
@ErrorHandling
class QueryEditor extends Component {
class QueryEditor extends PureComponent<Props, State> {
private editor: React.RefObject<HTMLTextAreaElement>
constructor(props) {
super(props)
this.state = {
value: this.props.query,
}
this.editor = React.createRef<HTMLTextAreaElement>()
}
componentWillReceiveProps(nextProps) {
public componentWillReceiveProps(nextProps: Props) {
if (this.props.query !== nextProps.query) {
this.setState({value: nextProps.query})
}
}
handleKeyDown = e => {
public render() {
const {
config: {status},
} = this.props
const {value} = this.state
return (
<div className="query-editor">
<textarea
className="query-editor--field"
ref={this.editor}
value={value}
autoComplete="off"
spellCheck={false}
onBlur={this.handleUpdate}
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
data-test="query-editor-field"
placeholder="Enter a query or select database, measurement, and field below and have us build one for you..."
/>
<div className="varmoji">
<div className="varmoji-container">
<div className="varmoji-front">
<QueryStatus status={status}>
<Dropdown
items={QUERY_TEMPLATES}
selected="Query Templates"
onChoose={this.handleChooseMetaQuery}
className="dropdown-140 query-editor--templates"
buttonSize="btn-xs"
/>
</QueryStatus>
</div>
</div>
</div>
</div>
)
}
private handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>): void => {
const {value} = this.state
if (e.key === 'Escape') {
@ -35,64 +88,18 @@ class QueryEditor extends Component {
}
}
handleChange = () => {
this.setState({value: this.editor.value})
private handleChange = (): void => {
const value = this.editor.current.value
this.setState({value})
}
handleUpdate = () => {
private handleUpdate = (): void => {
this.props.onUpdate(this.state.value)
}
handleChooseMetaQuery = template => {
private handleChooseMetaQuery = (template: QueryTemplate): void => {
this.setState({value: template.query})
}
render() {
const {
config: {status},
} = this.props
const {value} = this.state
return (
<div className="query-editor">
<textarea
className="query-editor--field"
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
onBlur={this.handleUpdate}
ref={editor => (this.editor = editor)}
value={value}
placeholder="Enter a query or select database, measurement, and field below and have us build one for you..."
autoComplete="off"
spellCheck="false"
data-test="query-editor-field"
/>
<div className="varmoji">
<div className="varmoji-container">
<div className="varmoji-front">
<QueryStatus status={status}>
<Dropdown
items={QUERY_TEMPLATES}
selected={'Query Templates'}
onChoose={this.handleChooseMetaQuery}
className="dropdown-140 query-editor--templates"
buttonSize="btn-xs"
/>
</QueryStatus>
</div>
</div>
</div>
</div>
)
}
}
const {func, shape, string} = PropTypes
QueryEditor.propTypes = {
query: string,
onUpdate: func.isRequired,
config: shape().isRequired,
}
export default QueryEditor

View File

@ -1,216 +0,0 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import Dimensions from 'react-dimensions'
import _ from 'lodash'
import {Table, Column, Cell} from 'fixed-data-table'
import Dropdown from 'shared/components/Dropdown'
import CustomCell from 'src/data_explorer/components/CustomCell'
import TabItem from 'src/data_explorer/components/TableTabItem'
import {TEMPLATES} from 'src/shared/constants'
import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries'
import {ErrorHandling} from 'src/shared/decorators/errors'
const emptySeries = {columns: [], values: []}
@ErrorHandling
class ChronoTable extends Component {
constructor(props) {
super(props)
this.state = {
series: [emptySeries],
columnWidths: {},
activeSeriesIndex: 0,
}
}
componentDidMount() {
this.fetchCellData(this.props.query)
}
componentWillReceiveProps(nextProps) {
if (this.props.query.text === nextProps.query.text) {
return
}
this.fetchCellData(nextProps.query)
}
fetchCellData = async query => {
if (!query || !query.text) {
return
}
this.setState({isLoading: true})
// second param is db, we want to leave this blank
try {
const {results} = await fetchTimeSeriesAsync({
source: query.host,
query,
tempVars: TEMPLATES,
})
this.setState({isLoading: false})
let series = _.get(results, ['0', 'series'], [])
if (!series.length) {
return this.setState({series: []})
}
series = series.map(s => (s.values ? s : {...s, values: []}))
this.setState({series})
} catch (error) {
this.setState({
isLoading: false,
series: [],
})
throw error
}
}
handleColumnResize = (newColumnWidth, columnKey) => {
const columnWidths = {
...this.state.columnWidths,
[columnKey]: newColumnWidth,
}
this.setState({
columnWidths,
})
}
handleClickTab = activeSeriesIndex => () => {
this.setState({activeSeriesIndex})
}
handleClickDropdown = item => {
this.setState({activeSeriesIndex: item.index})
}
handleCustomCell = (columnName, values, colIndex) => ({rowIndex}) => {
return (
<CustomCell columnName={columnName} data={values[rowIndex][colIndex]} />
)
}
makeTabName = ({name, tags}) => {
if (!tags) {
return name
}
const tagKeys = Object.keys(tags).sort()
const tagValues = tagKeys.map(key => tags[key]).join('.')
return `${name}.${tagValues}`
}
render() {
const {containerWidth, height, query} = this.props
const {series, columnWidths, isLoading, activeSeriesIndex} = this.state
const {columns, values} = _.get(
series,
[`${activeSeriesIndex}`],
emptySeries
)
const maximumTabsCount = 11
// adjust height to proper value by subtracting the heights of the UI around it
// tab height, graph-container vertical padding, graph-heading height, multitable-header height
const minWidth = 70
const rowHeight = 34
const headerHeight = 30
const stylePixelOffset = 130
const defaultColumnWidth = 200
const styleAdjustedHeight = height - stylePixelOffset
const width =
columns && columns.length > 1 ? defaultColumnWidth : containerWidth
if (!query) {
return <div className="generic-empty-state">Please add a query below</div>
}
if (isLoading) {
return <div className="generic-empty-state">Loading...</div>
}
return (
<div style={{width: '100%', height: '100%', position: 'relative'}}>
{series.length < maximumTabsCount ? (
<div className="table--tabs">
{series.map((s, i) => (
<TabItem
isActive={i === activeSeriesIndex}
key={i}
name={this.makeTabName(s)}
index={i}
onClickTab={this.handleClickTab}
/>
))}
</div>
) : (
<Dropdown
className="dropdown-160 table--tabs-dropdown"
items={series.map((s, index) => ({
...s,
text: this.makeTabName(s),
index,
}))}
onChoose={this.handleClickDropdown}
selected={this.makeTabName(series[activeSeriesIndex])}
buttonSize="btn-xs"
/>
)}
<div className="table--tabs-content">
{(columns && !columns.length) || (values && !values.length) ? (
<div className="generic-empty-state">This series is empty</div>
) : (
<Table
onColumnResizeEndCallback={this.handleColumnResize}
isColumnResizing={false}
rowHeight={rowHeight}
rowsCount={values.length}
width={containerWidth}
ownerHeight={styleAdjustedHeight}
height={styleAdjustedHeight}
headerHeight={headerHeight}
>
{columns.map((columnName, colIndex) => {
return (
<Column
isResizable={true}
key={columnName}
columnKey={columnName}
header={<Cell>{columnName}</Cell>}
cell={this.handleCustomCell(columnName, values, colIndex)}
width={columnWidths[columnName] || width}
minWidth={minWidth}
/>
)
})}
</Table>
)}
</div>
</div>
)
}
}
ChronoTable.defaultProps = {
height: 500,
}
const {arrayOf, func, number, shape, string} = PropTypes
ChronoTable.propTypes = {
query: shape({
host: arrayOf(string.isRequired).isRequired,
text: string.isRequired,
id: string.isRequired,
}).isRequired,
containerWidth: number.isRequired,
height: number,
editQueryStatus: func.isRequired,
}
export default Dimensions({elementResize: true})(ChronoTable)

View File

@ -0,0 +1,299 @@
import React, {PureComponent, CSSProperties} from 'react'
import Dimensions from 'react-dimensions'
import _ from 'lodash'
import {Table, Column, Cell} from 'fixed-data-table'
import Dropdown from 'src/shared/components/Dropdown'
import CustomCell from 'src/data_explorer/components/CustomCell'
import TabItem from 'src/data_explorer/components/TableTabItem'
import {TEMPLATES} from 'src/shared/constants'
import {fetchTimeSeriesAsync} from 'src/shared/actions/timeSeries'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {
emptySeries,
maximumTabsCount,
minWidth,
rowHeight,
headerHeight,
stylePixelOffset,
defaultColumnWidth,
} from 'src/data_explorer/constants/table'
interface DataExplorerTableQuery {
host: string[]
text: string
id: string
}
interface Series {
columns: string[]
name: string
values: any[]
}
interface ColumnWidths {
[key: string]: number
}
interface Props {
height: number
query: DataExplorerTableQuery
editQueryStatus: () => void
containerHeight: number
containerWidth: number
}
interface State {
series: Series[]
columnWidths: ColumnWidths
activeSeriesIndex: number
isLoading: boolean
}
@ErrorHandling
class ChronoTable extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
series: [emptySeries],
columnWidths: {},
activeSeriesIndex: 0,
isLoading: false,
}
}
public componentDidMount() {
this.fetchCellData(this.props.query)
}
public componentWillReceiveProps(nextProps) {
if (this.props.query.text === nextProps.query.text) {
return
}
this.fetchCellData(nextProps.query)
}
public render() {
const {query, containerWidth} = this.props
const {series, columnWidths, isLoading, activeSeriesIndex} = this.state
const {columns, values} = _.get(series, `${activeSeriesIndex}`, emptySeries)
if (!query) {
return (
<div className="generic-empty-state"> Please add a query below </div>
)
}
if (isLoading) {
return <div className="generic-empty-state"> Loading... </div>
}
return (
<div style={this.style}>
{this.tableSelector}
<div className="table--tabs-content">
{this.isEmpty ? (
<div className="generic-empty-state"> This series is empty </div>
) : (
<Table
isColumnResizing={false}
width={containerWidth}
rowHeight={rowHeight}
height={this.height}
ownerHeight={this.height}
rowsCount={values.length}
headerHeight={headerHeight}
onColumnResizeEndCallback={this.handleColumnResize}
>
{columns.map((columnName, colIndex) => {
return (
<Column
isResizable={true}
key={columnName}
minWidth={minWidth}
columnKey={columnName}
header={<Cell> {columnName} </Cell>}
width={columnWidths[columnName] || this.columnWidth}
cell={this.handleCustomCell(columnName, values, colIndex)}
/>
)
})}
</Table>
)}
</div>
</div>
)
}
private get isEmpty(): boolean {
const {columns, values} = this.series
return (columns && !columns.length) || (values && !values.length)
}
private get height(): number {
return this.props.containerHeight || 500 - stylePixelOffset
}
private get tableSelector() {
if (this.isTabbed) {
return this.tabs
}
return this.dropdown
}
private get dropdown(): JSX.Element {
const {series, activeSeriesIndex} = this.state
return (
<Dropdown
className="dropdown-160 table--tabs-dropdown"
items={this.dropdownItems}
onChoose={this.handleClickDropdown}
selected={this.makeTabName(series[activeSeriesIndex])}
buttonSize="btn-xs"
/>
)
}
private get dropdownItems(): Series[] {
return this.state.series.map((s, index) => ({
...s,
index,
text: this.makeTabName(s),
}))
}
private get tabs(): JSX.Element {
const {series, activeSeriesIndex} = this.state
return (
<div className="table--tabs">
{series.map((s, i) => (
<TabItem
key={i}
index={i}
name={s.name}
onClickTab={this.handleClickTab}
isActive={i === activeSeriesIndex}
/>
))}
</div>
)
}
private isTabbed(): boolean {
const {series} = this.state
return series.length < maximumTabsCount
}
private get style(): CSSProperties {
return {
width: '100%',
height: '100%',
position: 'relative',
}
}
private get columnWidth(): number {
return defaultColumnWidth
}
private get series(): Series {
const {activeSeriesIndex} = this.state
const {series} = this.state
return _.get(series, `${activeSeriesIndex}`, emptySeries)
}
private get source(): string {
return _.get(this.props.query, 'host.0', '')
}
private fetchCellData = async (query: DataExplorerTableQuery) => {
if (!query || !query.text) {
return
}
this.setState({
isLoading: true,
})
try {
const {results} = await fetchTimeSeriesAsync({
source: this.source,
query: query.text,
tempVars: TEMPLATES,
})
this.setState({
isLoading: false,
})
let series = _.get(results, ['0', 'series'], [])
if (!series.length) {
return this.setState({series: []})
}
series = series.map(s => {
if (s.values) {
return s
}
return {...s, values: []}
})
this.setState({series})
} catch (error) {
this.setState({
isLoading: false,
series: [],
})
throw error
}
}
private handleColumnResize = (
newColumnWidth: number,
columnKey: string
): void => {
const columnWidths = {
...this.state.columnWidths,
[columnKey]: newColumnWidth,
}
this.setState({
columnWidths,
})
}
private handleClickTab = activeSeriesIndex => {
this.setState({
activeSeriesIndex,
})
}
private handleClickDropdown = item => {
this.setState({
activeSeriesIndex: item.index,
})
}
private handleCustomCell = (columnName, values, colIndex) => ({rowIndex}) => {
return (
<CustomCell columnName={columnName} data={values[rowIndex][colIndex]} />
)
}
private makeTabName = ({name}): string => {
return name
}
}
export default Dimensions({
elementResize: true,
})(ChronoTable)

View File

@ -1,23 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
const TableTabItem = ({name, index, onClickTab, isActive}) => (
<div
className={classnames('table--tab', {active: isActive})}
onClick={onClickTab(index)}
>
{name}
</div>
)
const {bool, func, number, string} = PropTypes
TableTabItem.propTypes = {
name: string,
onClickTab: func.isRequired,
index: number.isRequired,
isActive: bool,
}
export default TableTabItem

View File

@ -0,0 +1,31 @@
import React, {PureComponent} from 'react'
import classnames from 'classnames'
interface Props {
name: string
index: number
onClickTab: (index: number) => void
isActive: boolean
}
class TableTabItem extends PureComponent<Props> {
public render() {
return (
<div className={this.className} onClick={this.handleClick}>
{this.props.name}
</div>
)
}
private handleClick = (): void => {
this.props.onClickTab(this.props.index)
}
get className(): string {
return classnames('table--tab', {
active: this.props.isActive,
})
}
}
export default TableTabItem

View File

@ -1,72 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import _ from 'lodash'
import moment from 'moment'
import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries'
import {timeSeriesToTableGraph} from 'src/utils/timeSeriesTransformers'
import {dataToCSV} from 'src/shared/parsing/dataToCSV'
import download from 'src/external/download.js'
import {TEMPLATES} from 'src/shared/constants'
const getDataForCSV = (query, errorThrown) => async () => {
try {
const response = await fetchTimeSeriesAsync({
source: query.host,
query,
tempVars: TEMPLATES,
})
const {data} = timeSeriesToTableGraph([{response}])
const db = _.get(query, ['queryConfig', 'database'], '')
const rp = _.get(query, ['queryConfig', 'retentionPolicy'], '')
const measurement = _.get(query, ['queryConfig', 'measurement'], '')
const timestring = moment().format('YYYY-MM-DD-HH-mm')
const name = `${db}.${rp}.${measurement}.${timestring}`
download(dataToCSV(data), `${name}.csv`, 'text/plain')
} catch (error) {
errorThrown(error, 'Unable to download .csv file')
console.error(error)
}
}
const VisHeader = ({views, view, onToggleView, query, errorThrown}) => (
<div className="graph-heading">
{views.length ? (
<ul className="nav nav-tablist nav-tablist-sm">
{views.map(v => (
<li
key={v}
onClick={onToggleView(v)}
className={classnames({active: view === v})}
data-test={`data-${v}`}
>
{_.upperFirst(v)}
</li>
))}
</ul>
) : null}
{query ? (
<div
className="btn btn-sm btn-default dlcsv"
onClick={getDataForCSV(query, errorThrown)}
>
<span className="icon download dlcsv" />
.csv
</div>
) : null}
</div>
)
const {arrayOf, func, shape, string} = PropTypes
VisHeader.propTypes = {
views: arrayOf(string).isRequired,
view: string.isRequired,
onToggleView: func.isRequired,
query: shape(),
errorThrown: func.isRequired,
}
export default VisHeader

View File

@ -0,0 +1,42 @@
import React, {PureComponent} from 'react'
import {getDataForCSV} from 'src/data_explorer/apis'
import VisHeaderTabs from 'src/data_explorer/components/VisHeaderTabs'
import {OnToggleView} from 'src/data_explorer/components/VisHeaderTab'
interface Props {
views: string[]
view: string
query: any
onToggleView: OnToggleView
errorThrown: () => void
}
class VisHeader extends PureComponent<Props> {
public render() {
const {views, view, onToggleView, query, errorThrown} = this.props
return (
<div className="graph-heading">
{!!views.length && (
<VisHeaderTabs
view={view}
views={views}
currentView={view}
onToggleView={onToggleView}
/>
)}
{query && (
<div
className="btn btn-sm btn-default dlcsv"
onClick={getDataForCSV(query, errorThrown)}
>
<span className="icon download dlcsv" />
.csv
</div>
)}
</div>
)
}
}
export default VisHeader

View File

@ -0,0 +1,36 @@
import React, {PureComponent} from 'react'
import classnames from 'classnames'
import _ from 'lodash'
export type OnToggleView = (view: string) => void
interface TabProps {
view: string
currentView: string
onToggleView: OnToggleView
}
class VisHeaderTab extends PureComponent<TabProps> {
public render() {
return (
<li className={this.className} onClick={this.handleClick}>
{this.text}
</li>
)
}
private get className(): string {
const {view, currentView} = this.props
return classnames({active: view === currentView})
}
private handleClick = () => {
this.props.onToggleView(this.props.view)
}
private get text(): string {
return _.upperFirst(this.props.view)
}
}
export default VisHeaderTab

View File

@ -0,0 +1,28 @@
import React, {SFC} from 'react'
import VisHeaderTab, {
OnToggleView,
} from 'src/data_explorer/components/VisHeaderTab'
interface Props {
views: string[]
view: string
currentView: string
onToggleView: OnToggleView
}
const VisHeaderTabs: SFC<Props> = ({views, currentView, onToggleView}) => {
return (
<ul className="nav nav-tablist nav-tablist-sm">
{views.map(v => (
<VisHeaderTab
key={v}
view={v}
currentView={currentView}
onToggleView={onToggleView}
/>
))}
</ul>
)
}
export default VisHeaderTabs

View File

@ -1,71 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import Table from './Table'
import RefreshingGraph from 'shared/components/RefreshingGraph'
import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes'
const VisView = ({
axes,
view,
query,
queries,
cellType,
templates,
autoRefresh,
heightPixels,
manualRefresh,
editQueryStatus,
resizerBottomHeight,
}) => {
if (view === 'table') {
if (!query) {
return (
<div className="graph-empty">
<p>Build a Query above</p>
</div>
)
}
return (
<Table
query={query}
height={resizerBottomHeight}
editQueryStatus={editQueryStatus}
/>
)
}
return (
<RefreshingGraph
colors={DEFAULT_LINE_COLORS}
axes={axes}
type={cellType}
queries={queries}
templates={templates}
cellHeight={heightPixels}
autoRefresh={autoRefresh}
manualRefresh={manualRefresh}
editQueryStatus={editQueryStatus}
/>
)
}
const {arrayOf, func, number, shape, string} = PropTypes
VisView.propTypes = {
view: string.isRequired,
axes: shape(),
query: shape(),
queries: arrayOf(shape()).isRequired,
cellType: string,
templates: arrayOf(shape()),
autoRefresh: number.isRequired,
heightPixels: number,
editQueryStatus: func.isRequired,
manualRefresh: number,
activeQueryIndex: number,
resizerBottomHeight: number,
}
export default VisView

View File

@ -0,0 +1,53 @@
import React, {SFC} from 'react'
import Table from './Table'
import RefreshingGraph from 'src/shared/components/RefreshingGraph'
import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes'
import {QueryConfig, Template} from 'src/types'
interface Props {
view: string
query?: QueryConfig
queries: QueryConfig[]
templates: Template[]
autoRefresh: number
editQueryStatus: () => void
manualRefresh: number
}
const DataExplorerVisView: SFC<Props> = ({
view,
query,
queries,
templates,
autoRefresh,
manualRefresh,
editQueryStatus,
}) => {
if (view === 'table') {
if (!query) {
return (
<div className="graph-empty">
<p> Build a Query above </p>
</div>
)
}
return <Table query={query} editQueryStatus={editQueryStatus} />
}
return (
<RefreshingGraph
type="line-graph"
queries={queries}
templates={templates}
autoRefresh={autoRefresh}
colors={DEFAULT_LINE_COLORS}
manualRefresh={manualRefresh}
editQueryStatus={editQueryStatus}
/>
)
}
export default DataExplorerVisView

View File

@ -1,156 +0,0 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import VisHeader from 'src/data_explorer/components/VisHeader'
import VisView from 'src/data_explorer/components/VisView'
import {GRAPH, TABLE} from 'shared/constants'
import buildQueries from 'utils/buildQueriesForGraphs'
import _ from 'lodash'
import {ErrorHandling} from 'src/shared/decorators/errors'
const META_QUERY_REGEX = /^(show|create|drop)/i
@ErrorHandling
class Visualization extends Component {
constructor(props) {
super(props)
const {activeQueryIndex, queryConfigs} = this.props
const activeQueryText = this.getQueryText(queryConfigs, activeQueryIndex)
this.state = activeQueryText.match(META_QUERY_REGEX)
? {view: TABLE}
: {view: GRAPH}
}
componentWillReceiveProps(nextProps) {
const {activeQueryIndex, queryConfigs} = nextProps
const nextQueryText = this.getQueryText(queryConfigs, activeQueryIndex)
const queryText = this.getQueryText(
this.props.queryConfigs,
this.props.activeQueryIndex
)
if (queryText === nextQueryText) {
return
}
if (nextQueryText.match(META_QUERY_REGEX)) {
return this.setState({view: TABLE})
}
this.setState({view: GRAPH})
}
handleToggleView = view => () => {
this.setState({view})
}
render() {
const {
axes,
views,
height,
cellType,
timeRange,
templates,
autoRefresh,
heightPixels,
queryConfigs,
manualRefresh,
editQueryStatus,
activeQueryIndex,
resizerBottomHeight,
errorThrown,
} = this.props
const {
source: {
links: {proxy},
},
} = this.context
const {view} = this.state
const queries = buildQueries(proxy, queryConfigs, timeRange)
const activeQuery = queries[activeQueryIndex]
const defaultQuery = queries[0]
const query = activeQuery || defaultQuery
return (
<div className="graph" style={{height}}>
<VisHeader
view={view}
views={views}
query={query}
errorThrown={errorThrown}
onToggleView={this.handleToggleView}
/>
<div
className={classnames({
'graph-container': view === GRAPH,
'table-container': view === TABLE,
})}
>
<VisView
view={view}
axes={axes}
query={query}
queries={queries}
cellType={cellType}
templates={templates}
autoRefresh={autoRefresh}
heightPixels={heightPixels}
manualRefresh={manualRefresh}
editQueryStatus={editQueryStatus}
resizerBottomHeight={resizerBottomHeight}
/>
</div>
</div>
)
}
getQueryText(queryConfigs, index) {
// rawText can be null
return _.get(queryConfigs, [`${index}`, 'rawText'], '') || ''
}
}
Visualization.defaultProps = {
cellType: '',
}
const {arrayOf, func, number, shape, string} = PropTypes
Visualization.contextTypes = {
source: shape({
links: shape({
proxy: string.isRequired,
}).isRequired,
}).isRequired,
}
Visualization.propTypes = {
cellType: string,
autoRefresh: number.isRequired,
templates: arrayOf(shape()),
timeRange: shape({
upper: string,
lower: string,
}).isRequired,
queryConfigs: arrayOf(shape({})).isRequired,
activeQueryIndex: number,
height: string,
heightPixels: number,
editQueryStatus: func.isRequired,
views: arrayOf(string).isRequired,
axes: shape({
y: shape({
bounds: arrayOf(string),
}),
}),
resizerBottomHeight: number,
errorThrown: func.isRequired,
manualRefresh: number,
}
export default Visualization

View File

@ -0,0 +1,137 @@
import React, {PureComponent} from 'react'
import classnames from 'classnames'
import _ from 'lodash'
import VisHeader from 'src/data_explorer/components/VisHeader'
import VisView from 'src/data_explorer/components/VisView'
import {GRAPH, TABLE} from 'src/shared/constants'
import buildQueries from 'src/utils/buildQueriesForGraphs'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {Source, QueryConfig, Template, TimeRange} from 'src/types'
const META_QUERY_REGEX = /^(show|create|drop)/i
interface Props {
source: Source
views: string[]
autoRefresh: number
templates: Template[]
timeRange: TimeRange
queryConfigs: QueryConfig[]
activeQueryIndex: number
manualRefresh: number
editQueryStatus: () => void
errorThrown: () => void
}
interface State {
view: string
}
@ErrorHandling
class DataExplorerVisualization extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = this.initialState
}
public componentWillReceiveProps(nextProps: Props) {
const {activeQueryIndex, queryConfigs} = nextProps
const nextQueryText = this.getQueryText(queryConfigs, activeQueryIndex)
const queryText = this.getQueryText(
this.props.queryConfigs,
this.props.activeQueryIndex
)
if (queryText === nextQueryText) {
return
}
if (nextQueryText.match(META_QUERY_REGEX)) {
return this.setState({view: TABLE})
}
this.setState({view: GRAPH})
}
public render() {
const {
views,
templates,
autoRefresh,
manualRefresh,
editQueryStatus,
errorThrown,
} = this.props
const {view} = this.state
return (
<div className="graph">
<VisHeader
view={view}
views={views}
query={this.query}
errorThrown={errorThrown}
onToggleView={this.handleToggleView}
/>
<div className={this.visualizationClass}>
<VisView
view={view}
query={this.query}
templates={templates}
queries={this.queries}
autoRefresh={autoRefresh}
manualRefresh={manualRefresh}
editQueryStatus={editQueryStatus}
/>
</div>
</div>
)
}
private get visualizationClass(): string {
const {view} = this.state
return classnames({
'graph-container': view === GRAPH,
'table-container': view === TABLE,
})
}
private get queries(): QueryConfig[] {
const {source, queryConfigs, timeRange} = this.props
return buildQueries(source.links.proxy, queryConfigs, timeRange)
}
private get query(): QueryConfig {
const {activeQueryIndex} = this.props
const activeQuery = this.queries[activeQueryIndex]
const defaultQuery = this.queries[0]
return activeQuery || defaultQuery
}
private handleToggleView = (view: string): void => {
this.setState({view})
}
private getQueryText(queryConfigs: QueryConfig[], index: number): string {
// rawText can be null
return _.get(queryConfigs, [`${index}`, 'rawText'], '') || ''
}
private get initialState(): {view: string} {
const {activeQueryIndex, queryConfigs} = this.props
const activeQueryText = this.getQueryText(queryConfigs, activeQueryIndex)
if (activeQueryText.match(META_QUERY_REGEX)) {
return {view: TABLE}
}
return {view: GRAPH}
}
}
export default DataExplorerVisualization

View File

@ -1,105 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import WriteDataFooter from 'src/data_explorer/components/WriteDataFooter'
const WriteDataBody = ({
handleKeyUp,
handleCancelFile,
handleFile,
handleEdit,
handleSubmit,
inputContent,
uploadContent,
fileName,
isManual,
fileInput,
handleFileOpen,
isUploading,
}) => (
<div className="write-data-form--body">
{isManual ? (
<textarea
className="form-control write-data-form--input"
autoComplete="off"
spellCheck="false"
placeholder="<measurement>,<tag_key>=<tag_value> <field_key>=<field_value>"
onKeyUp={handleKeyUp}
onChange={handleEdit}
autoFocus={true}
data-test="manual-entry-field"
/>
) : (
<div
className={
uploadContent
? 'write-data-form--file'
: 'write-data-form--file write-data-form--file_active'
}
onClick={handleFileOpen}
>
{uploadContent ? (
<h3 className="write-data-form--filepath_selected">{fileName}</h3>
) : (
<h3 className="write-data-form--filepath_empty">
Drop a file here or click to upload
</h3>
)}
<div
className={
uploadContent
? 'write-data-form--graphic write-data-form--graphic_success'
: 'write-data-form--graphic'
}
/>
<input
type="file"
onChange={handleFile(false)}
className="write-data-form--upload"
ref={fileInput}
accept="text/*, application/gzip"
/>
{uploadContent && (
<span className="write-data-form--file-submit">
<button className="btn btn-md btn-success" onClick={handleSubmit}>
Write this File
</button>
<button
className="btn btn-md btn-default"
onClick={handleCancelFile}
>
Cancel
</button>
</span>
)}
</div>
)}
{isManual && (
<WriteDataFooter
isUploading={isUploading}
isManual={isManual}
inputContent={inputContent}
handleSubmit={handleSubmit}
uploadContent={uploadContent}
/>
)}
</div>
)
const {func, string, bool} = PropTypes
WriteDataBody.propTypes = {
handleKeyUp: func.isRequired,
handleEdit: func.isRequired,
handleCancelFile: func.isRequired,
handleFile: func.isRequired,
handleSubmit: func.isRequired,
inputContent: string,
uploadContent: string,
fileName: string,
isManual: bool,
fileInput: func.isRequired,
handleFileOpen: func.isRequired,
isUploading: bool.isRequired,
}
export default WriteDataBody

View File

@ -0,0 +1,162 @@
import React, {
PureComponent,
ChangeEvent,
KeyboardEvent,
MouseEvent,
DragEvent,
ReactElement,
} from 'react'
import WriteDataFooter from 'src/data_explorer/components/WriteDataFooter'
interface Props {
handleCancelFile: (e: MouseEvent<HTMLButtonElement>) => void
handleEdit: (e: ChangeEvent<HTMLTextAreaElement>) => void
handleKeyUp: (e: KeyboardEvent<HTMLTextAreaElement>) => void
handleFile: (drop: boolean) => (e: DragEvent<HTMLInputElement>) => void
handleSubmit: (e: MouseEvent<HTMLButtonElement>) => void
inputContent: string
uploadContent: string
fileName: string
isManual: boolean
fileInput: (ref: any) => any
handleFileOpen: () => void
isUploading: boolean
}
class WriteDataBody extends PureComponent<Props> {
public render() {
return (
<div className="write-data-form--body">
{this.input}
{this.footer}
</div>
)
}
private handleFile = (e: any): void => {
this.props.handleFile(false)(e)
}
private get input(): JSX.Element {
const {isManual} = this.props
if (isManual) {
return this.textarea
}
return this.dragArea
}
private get textarea(): ReactElement<HTMLTextAreaElement> {
const {handleKeyUp, handleEdit} = this.props
return (
<textarea
spellCheck={false}
autoFocus={true}
autoComplete="off"
onKeyUp={handleKeyUp}
onChange={handleEdit}
data-test="manual-entry-field"
className="form-control write-data-form--input"
placeholder="<measurement>,<tag_key>=<tag_value> <field_key>=<field_value>"
/>
)
}
private get dragArea(): ReactElement<HTMLDivElement> {
const {fileInput, handleFileOpen} = this.props
return (
<div className={this.dragAreaClass} onClick={handleFileOpen}>
{this.dragAreaHeader}
<div className={this.infoClass} />
<input
type="file"
ref={fileInput}
className="write-data-form--upload"
accept="text/*, application/gzip"
onChange={this.handleFile}
/>
{this.buttons}
</div>
)
}
private get dragAreaHeader(): ReactElement<HTMLHeadElement> {
const {uploadContent, fileName} = this.props
if (uploadContent) {
return <h3 className="write-data-form--filepath_selected">{fileName}</h3>
}
return (
<h3 className="write-data-form--filepath_empty">
Drop a file here or click to upload
</h3>
)
}
private get infoClass(): string {
const {uploadContent} = this.props
if (uploadContent) {
return 'write-data-form--graphic write-data-form--graphic_success'
}
return 'write-data-form--graphic'
}
private get buttons(): ReactElement<HTMLSpanElement> | null {
const {uploadContent, handleSubmit, handleCancelFile} = this.props
if (!uploadContent) {
return null
}
return (
<span className="write-data-form--file-submit">
<button className="btn btn-md btn-success" onClick={handleSubmit}>
Write this File
</button>
<button className="btn btn-md btn-default" onClick={handleCancelFile}>
Cancel
</button>
</span>
)
}
private get dragAreaClass(): string {
const {uploadContent} = this.props
if (uploadContent) {
return 'write-data-form--file'
}
return 'write-data-form--file write-data-form--file_active'
}
private get footer(): JSX.Element | null {
const {
isUploading,
isManual,
inputContent,
handleSubmit,
uploadContent,
} = this.props
if (!isManual) {
return null
}
return (
<WriteDataFooter
isUploading={isUploading}
isManual={isManual}
inputContent={inputContent}
handleSubmit={handleSubmit}
uploadContent={uploadContent}
/>
)
}
}
export default WriteDataBody

View File

@ -1,60 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
const submitButton = 'btn btn-sm btn-success write-data-form--submit'
const spinner = 'btn-spinner'
const WriteDataFooter = ({
isManual,
inputContent,
uploadContent,
handleSubmit,
isUploading,
}) => (
<div className="write-data-form--footer">
{isManual ? (
<span className="write-data-form--helper">
Need help writing InfluxDB Line Protocol? -&nbsp;
<a
href="https://docs.influxdata.com/influxdb/latest/write_protocols/line_protocol_tutorial/"
target="_blank"
>
See Documentation
</a>
</span>
) : (
<span className="write-data-form--helper">
<a
href="https://docs.influxdata.com/influxdb/v1.2//tools/shell/#import-data-from-a-file-with-import"
target="_blank"
>
File Upload Documentation
</a>
</span>
)}
<button
className={isUploading ? `${submitButton} ${spinner}` : submitButton}
onClick={handleSubmit}
disabled={
(!inputContent && isManual) ||
(!uploadContent && !isManual) ||
isUploading
}
data-test="write-data-submit-button"
>
Write
</button>
</div>
)
const {bool, func, string} = PropTypes
WriteDataFooter.propTypes = {
isManual: bool.isRequired,
isUploading: bool.isRequired,
uploadContent: string,
inputContent: string,
handleSubmit: func,
}
export default WriteDataFooter

View File

@ -0,0 +1,70 @@
import React, {PureComponent, MouseEvent} from 'react'
import {
WRITE_DATA_DOCS_LINK,
DATA_IMPORT_DOCS_LINK,
} from 'src/data_explorer/constants'
const submitButton = 'btn btn-sm btn-success write-data-form--submit'
const spinner = 'btn-spinner'
interface Props {
isManual: boolean
isUploading: boolean
uploadContent: string
inputContent: string
handleSubmit: (e: MouseEvent<HTMLButtonElement>) => void
}
class WriteDataFooter extends PureComponent<Props> {
public render() {
const {isManual, handleSubmit} = this.props
return (
<div className="write-data-form--footer">
{isManual ? (
<span className="write-data-form--helper">
Need help writing InfluxDB Line Protocol? -&nbsp;
<a href={WRITE_DATA_DOCS_LINK} target="_blank">
See Documentation
</a>
</span>
) : (
<span className="write-data-form--helper">
<a href={DATA_IMPORT_DOCS_LINK} target="_blank">
File Upload Documentation
</a>
</span>
)}
<button
className={this.buttonClass}
onClick={handleSubmit}
disabled={this.buttonDisabled}
data-test="write-data-submit-button"
>
Write
</button>
</div>
)
}
get buttonDisabled(): boolean {
const {inputContent, isManual, uploadContent, isUploading} = this.props
return (
(!inputContent && isManual) ||
(!uploadContent && !isManual) ||
isUploading
)
}
get buttonClass(): string {
const {isUploading} = this.props
if (isUploading) {
return `${submitButton} ${spinner}`
}
return submitButton
}
}
export default WriteDataFooter

View File

@ -1,17 +1,43 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import React, {
PureComponent,
DragEvent,
ChangeEvent,
KeyboardEvent,
} from 'react'
import classnames from 'classnames'
import OnClickOutside from 'shared/components/OnClickOutside'
import OnClickOutside from 'src/shared/components/OnClickOutside'
import WriteDataBody from 'src/data_explorer/components/WriteDataBody'
import WriteDataHeader from 'src/data_explorer/components/WriteDataHeader'
import {OVERLAY_TECHNOLOGY} from 'shared/constants/classNames'
import {OVERLAY_TECHNOLOGY} from 'src/shared/constants/classNames'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {Source, DropdownItem} from 'src/types'
let dragCounter = 0
interface Props {
source: Source
selectedDatabase: string
onClose: () => void
errorThrown: () => void
writeLineProtocol: (source: Source, database: string, content: string) => void
}
interface State {
selectedDatabase: string
inputContent: string | null
uploadContent: string
fileName: string
progress: string
isManual: boolean
dragClass: string
isUploading: boolean
}
@ErrorHandling
class WriteDataForm extends Component {
class WriteDataForm extends PureComponent<Props, State> {
private fileInput: HTMLInputElement
constructor(props) {
super(props)
this.state = {
@ -26,23 +52,52 @@ class WriteDataForm extends Component {
}
}
toggleWriteView = isManual => () => {
public render() {
const {onClose, errorThrown, source} = this.props
const {dragClass} = this.state
return (
<div
onDrop={this.handleFile(true)}
onDragOver={this.handleDragOver}
onDragEnter={this.handleDragEnter}
onDragExit={this.handleDragLeave}
onDragLeave={this.handleDragLeave}
className={classnames(OVERLAY_TECHNOLOGY, dragClass)}
>
<div className="write-data-form">
<WriteDataHeader
{...this.state}
source={source}
onClose={onClose}
errorThrown={errorThrown}
toggleWriteView={this.toggleWriteView}
handleSelectDatabase={this.handleSelectDatabase}
/>
<WriteDataBody
{...this.state}
fileInput={this.handleFileInputRef}
handleEdit={this.handleEdit}
handleFile={this.handleFile}
handleKeyUp={this.handleKeyUp}
handleSubmit={this.handleSubmit}
handleFileOpen={this.handleFileOpen}
handleCancelFile={this.handleCancelFile}
/>
</div>
</div>
)
}
private toggleWriteView = (isManual: boolean) => {
this.setState({isManual})
}
handleSelectDatabase = item => {
private handleSelectDatabase = (item: DropdownItem): void => {
this.setState({selectedDatabase: item.text})
}
handleClickOutside = e => {
// guard against clicking to close error notification
if (e.target.className === OVERLAY_TECHNOLOGY) {
const {onClose} = this.props
onClose()
}
}
handleKeyUp = e => {
private handleKeyUp = (e: KeyboardEvent<HTMLTextAreaElement>) => {
e.stopPropagation()
if (e.key === 'Escape') {
const {onClose} = this.props
@ -50,7 +105,7 @@ class WriteDataForm extends Component {
}
}
handleSubmit = async () => {
private handleSubmit = async () => {
const {onClose, source, writeLineProtocol} = this.props
const {inputContent, uploadContent, selectedDatabase, isManual} = this.state
const content = isManual ? inputContent : uploadContent
@ -67,11 +122,11 @@ class WriteDataForm extends Component {
}
}
handleEdit = e => {
private handleEdit = (e: ChangeEvent<HTMLTextAreaElement>): void => {
this.setState({inputContent: e.target.value.trim()})
}
handleFile = drop => e => {
private handleFile = (drop: boolean) => (e: any): void => {
let file
if (drop) {
file = e.dataTransfer.files[0]
@ -79,7 +134,7 @@ class WriteDataForm extends Component {
dragClass: 'drag-none',
})
} else {
file = e.target.files[0]
file = e.currentTarget.files[0]
}
if (!file) {
@ -99,23 +154,23 @@ class WriteDataForm extends Component {
}
}
handleCancelFile = () => {
private handleCancelFile = (): void => {
this.setState({uploadContent: ''})
this.fileInput.value = ''
}
handleDragOver = e => {
private handleDragOver = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault()
e.stopPropagation()
}
handleDragEnter = e => {
private handleDragEnter = (e: DragEvent<HTMLDivElement>): void => {
dragCounter += 1
e.preventDefault()
this.setState({dragClass: 'drag-over'})
}
handleDragLeave = e => {
private handleDragLeave = (e: DragEvent<HTMLDivElement>): void => {
dragCounter -= 1
e.preventDefault()
if (dragCounter === 0) {
@ -123,67 +178,14 @@ class WriteDataForm extends Component {
}
}
handleFileOpen = () => {
private handleFileOpen = (): void => {
const {uploadContent} = this.state
if (uploadContent === '') {
this.fileInput.click()
}
}
handleFileInputRef = el => (this.fileInput = el)
render() {
const {onClose, errorThrown, source} = this.props
const {dragClass} = this.state
return (
<div
onDrop={this.handleFile(true)}
onDragOver={this.handleDragOver}
onDragEnter={this.handleDragEnter}
onDragExit={this.handleDragLeave}
onDragLeave={this.handleDragLeave}
className={classnames(OVERLAY_TECHNOLOGY, dragClass)}
>
<div className="write-data-form">
<WriteDataHeader
{...this.state}
source={source}
handleSelectDatabase={this.handleSelectDatabase}
errorThrown={errorThrown}
toggleWriteView={this.toggleWriteView}
onClose={onClose}
/>
<WriteDataBody
{...this.state}
fileInput={this.handleFileInputRef}
handleEdit={this.handleEdit}
handleFile={this.handleFile}
handleKeyUp={this.handleKeyUp}
handleSubmit={this.handleSubmit}
handleFileOpen={this.handleFileOpen}
handleCancelFile={this.handleCancelFile}
/>
</div>
</div>
)
}
}
const {func, shape, string} = PropTypes
WriteDataForm.propTypes = {
source: shape({
links: shape({
proxy: string.isRequired,
self: string.isRequired,
queries: string.isRequired,
}).isRequired,
}).isRequired,
onClose: func.isRequired,
writeLineProtocol: func.isRequired,
errorThrown: func.isRequired,
selectedDatabase: string,
private handleFileInputRef = (r: HTMLInputElement) => (this.fileInput = r)
}
export default OnClickOutside(WriteDataForm)

View File

@ -1,61 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import DatabaseDropdown from 'shared/components/DatabaseDropdown'
const WriteDataHeader = ({
handleSelectDatabase,
selectedDatabase,
errorThrown,
toggleWriteView,
isManual,
onClose,
source,
}) => (
<div className="write-data-form--header">
<div className="page-header__left">
<h1 className="page-header__title">Write Data To</h1>
<DatabaseDropdown
source={source}
onSelectDatabase={handleSelectDatabase}
database={selectedDatabase}
onErrorThrown={errorThrown}
/>
<ul className="nav nav-tablist nav-tablist-sm">
<li
onClick={toggleWriteView(false)}
className={isManual ? '' : 'active'}
>
File Upload
</li>
<li
onClick={toggleWriteView(true)}
className={isManual ? 'active' : ''}
data-test="manual-entry-button"
>
Manual Entry
</li>
</ul>
</div>
<div className="page-header__right">
<span className="page-header__dismiss" onClick={onClose} />
</div>
</div>
)
const {func, shape, string, bool} = PropTypes
WriteDataHeader.propTypes = {
handleSelectDatabase: func.isRequired,
selectedDatabase: string,
toggleWriteView: func.isRequired,
errorThrown: func.isRequired,
onClose: func.isRequired,
isManual: bool,
source: shape({
links: shape({
proxy: string.isRequired,
}).isRequired,
}).isRequired,
}
export default WriteDataHeader

View File

@ -0,0 +1,80 @@
import React, {PureComponent} from 'react'
import DatabaseDropdown from 'src/shared/components/DatabaseDropdown'
import {Source, DropdownItem} from 'src/types'
interface Props {
handleSelectDatabase: (item: DropdownItem) => void
selectedDatabase: string
toggleWriteView: (isWriteViewToggled: boolean) => void
errorThrown: () => void
onClose: () => void
isManual: boolean
source: Source
}
class WriteDataHeader extends PureComponent<Props> {
public render() {
const {
handleSelectDatabase,
selectedDatabase,
errorThrown,
onClose,
source,
} = this.props
return (
<div className="write-data-form--header">
<div className="page-header__left">
<h1 className="page-header__title">Write Data To</h1>
<DatabaseDropdown
source={source}
onSelectDatabase={handleSelectDatabase}
database={selectedDatabase}
onErrorThrown={errorThrown}
/>
<ul className="nav nav-tablist nav-tablist-sm">
<li onClick={this.handleToggleOff} className={this.fileUploadClass}>
File Upload
</li>
<li
onClick={this.handleToggleOn}
className={this.manualEntryClass}
data-test="manual-entry-button"
>
Manual Entry
</li>
</ul>
</div>
<div className="page-header__right">
<span className="page-header__dismiss" onClick={onClose} />
</div>
</div>
)
}
private get fileUploadClass(): string {
if (this.props.isManual) {
return ''
}
return 'active'
}
private get manualEntryClass(): string {
if (this.props.isManual) {
return 'active'
}
return ''
}
private handleToggleOff = (): void => {
this.props.toggleWriteView(false)
}
private handleToggleOn = (): void => {
this.props.toggleWriteView(true)
}
}
export default WriteDataHeader

View File

@ -1,84 +0,0 @@
export const INFLUXQL_FUNCTIONS = [
'mean',
'median',
'count',
'min',
'max',
'sum',
'first',
'last',
'spread',
'stddev',
]
export const MINIMUM_HEIGHTS = {
queryMaker: 350,
visualization: 200,
}
export const INITIAL_HEIGHTS = {
queryMaker: '66.666%',
visualization: '33.334%',
}
const SEPARATOR = 'SEPARATOR'
export const QUERY_TEMPLATES = [
{text: 'Show Databases', query: 'SHOW DATABASES'},
{text: 'Create Database', query: 'CREATE DATABASE "db_name"'},
{text: 'Drop Database', query: 'DROP DATABASE "db_name"'},
{text: `${SEPARATOR}`},
{text: 'Show Measurements', query: 'SHOW MEASUREMENTS ON "db_name"'},
{
text: 'Show Tag Keys',
query: 'SHOW TAG KEYS ON "db_name" FROM "measurement_name"',
},
{
text: 'Show Tag Values',
query:
'SHOW TAG VALUES ON "db_name" FROM "measurement_name" WITH KEY = "tag_key"',
},
{text: `${SEPARATOR}`},
{
text: 'Show Retention Policies',
query: 'SHOW RETENTION POLICIES on "db_name"',
},
{
text: 'Create Retention Policy',
query:
'CREATE RETENTION POLICY "rp_name" ON "db_name" DURATION 30d REPLICATION 1 DEFAULT',
},
{
text: 'Drop Retention Policy',
query: 'DROP RETENTION POLICY "rp_name" ON "db_name"',
},
{text: `${SEPARATOR}`},
{
text: 'Show Continuous Queries',
query: 'SHOW CONTINUOUS QUERIES',
},
{
text: 'Create Continuous Query',
query:
'CREATE CONTINUOUS QUERY "cq_name" ON "db_name" BEGIN SELECT min("field") INTO "target_measurement" FROM "current_measurement" GROUP BY time(30m) END',
},
{
text: 'Drop Continuous Query',
query: 'DROP CONTINUOUS QUERY "cq_name" ON "db_name"',
},
{text: `${SEPARATOR}`},
{text: 'Show Users', query: 'SHOW USERS'},
{
text: 'Create User',
query: 'CREATE USER "username" WITH PASSWORD \'password\'',
},
{
text: 'Create Admin User',
query:
'CREATE USER "username" WITH PASSWORD \'password\' WITH ALL PRIVILEGES',
},
{text: 'Drop User', query: 'DROP USER "username"'},
{text: `${SEPARATOR}`},
{text: 'Show Stats', query: 'SHOW STATS'},
{text: 'Show Diagnostics', query: 'SHOW DIAGNOSTICS'},
]

View File

@ -0,0 +1,144 @@
export const INFLUXQL_FUNCTIONS: string[] = [
'mean',
'median',
'count',
'min',
'max',
'sum',
'first',
'last',
'spread',
'stddev',
]
interface MinHeights {
queryMaker: number
visualization: number
}
export const MINIMUM_HEIGHTS: MinHeights = {
queryMaker: 350,
visualization: 200,
}
interface InitialHeights {
queryMaker: '66.666%'
visualization: '33.334%'
}
export const INITIAL_HEIGHTS: InitialHeights = {
queryMaker: '66.666%',
visualization: '33.334%',
}
const SEPARATOR: string = 'SEPARATOR'
export interface QueryTemplate {
text: string
query: string
}
export interface Separator {
text: string
}
type Template = QueryTemplate | Separator
export const QUERY_TEMPLATES: Template[] = [
{
text: 'Show Databases',
query: 'SHOW DATABASES',
},
{
text: 'Create Database',
query: 'CREATE DATABASE "db_name"',
},
{
text: 'Drop Database',
query: 'DROP DATABASE "db_name"',
},
{
text: `${SEPARATOR}`,
},
{
text: 'Show Measurements',
query: 'SHOW MEASUREMENTS ON "db_name"',
},
{
text: 'Show Tag Keys',
query: 'SHOW TAG KEYS ON "db_name" FROM "measurement_name"',
},
{
text: 'Show Tag Values',
query:
'SHOW TAG VALUES ON "db_name" FROM "measurement_name" WITH KEY = "tag_key"',
},
{
text: `${SEPARATOR}`,
},
{
text: 'Show Retention Policies',
query: 'SHOW RETENTION POLICIES on "db_name"',
},
{
text: 'Create Retention Policy',
query:
'CREATE RETENTION POLICY "rp_name" ON "db_name" DURATION 30d REPLICATION 1 DEFAULT',
},
{
text: 'Drop Retention Policy',
query: 'DROP RETENTION POLICY "rp_name" ON "db_name"',
},
{
text: `${SEPARATOR}`,
},
{
text: 'Show Continuous Queries',
query: 'SHOW CONTINUOUS QUERIES',
},
{
text: 'Create Continuous Query',
query:
'CREATE CONTINUOUS QUERY "cq_name" ON "db_name" BEGIN SELECT min("field") INTO "target_measurement" FROM "current_measurement" GROUP BY time(30m) END',
},
{
text: 'Drop Continuous Query',
query: 'DROP CONTINUOUS QUERY "cq_name" ON "db_name"',
},
{
text: `${SEPARATOR}`,
},
{
text: 'Show Users',
query: 'SHOW USERS',
},
{
text: 'Create User',
query: 'CREATE USER "username" WITH PASSWORD \'password\'',
},
{
text: 'Create Admin User',
query:
'CREATE USER "username" WITH PASSWORD \'password\' WITH ALL PRIVILEGES',
},
{
text: 'Drop User',
query: 'DROP USER "username"',
},
{
text: `${SEPARATOR}`,
},
{
text: 'Show Stats',
query: 'SHOW STATS',
},
{
text: 'Show Diagnostics',
query: 'SHOW DIAGNOSTICS',
},
]
export const WRITE_DATA_DOCS_LINK =
'https://docs.influxdata.com/influxdb/latest/write_protocols/line_protocol_tutorial/'
export const DATA_IMPORT_DOCS_LINK =
'https://docs.influxdata.com/influxdb/v1.2//tools/shell/#import-data-from-a-file-with-import'

View File

@ -0,0 +1,9 @@
export const emptySeries = {columns: [], values: [], name: ''}
export const maximumTabsCount = 11
// adjust height to proper value by subtracting the heights of the UI around it
// tab height, graph-container vertical padding, graph-heading height, multitable-header height
export const minWidth = 70
export const rowHeight = 34
export const headerHeight = 30
export const stylePixelOffset = 130
export const defaultColumnWidth = 200

View File

@ -32,7 +32,7 @@ import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
source: Source
queryConfigs: QueryConfig[]
queryConfigActions: any // TODO: actually type these
queryConfigActions: any
autoRefresh: number
handleChooseAutoRefresh: () => void
router?: InjectedRouter
@ -72,7 +72,7 @@ export class DataExplorer extends PureComponent<Props, State> {
}
}
public componentWillReceiveProps(nextProps) {
public componentWillReceiveProps(nextProps: Props) {
const {router} = this.props
const {queryConfigs, timeRange} = nextProps
@ -138,6 +138,7 @@ export class DataExplorer extends PureComponent<Props, State> {
initialGroupByTime={AUTO_GROUP_BY}
/>
<Visualization
source={source}
views={VIS_VIEWS}
activeQueryIndex={0}
timeRange={timeRange}
@ -161,8 +162,8 @@ export class DataExplorer extends PureComponent<Props, State> {
this.setState({showWriteForm: true})
}
private handleChooseTimeRange = (bounds: TimeRange): void => {
this.props.setTimeRange(bounds)
private handleChooseTimeRange = (timeRange: TimeRange): void => {
this.props.setTimeRange(timeRange)
}
private get selectedDatabase(): string {

View File

@ -1,64 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import {withRouter} from 'react-router'
import AutoRefreshDropdown from 'shared/components/AutoRefreshDropdown'
import TimeRangeDropdown from 'shared/components/TimeRangeDropdown'
import SourceIndicator from 'shared/components/SourceIndicator'
import GraphTips from 'shared/components/GraphTips'
const {func, number, shape, string} = PropTypes
const Header = ({
timeRange,
autoRefresh,
showWriteForm,
onManualRefresh,
onChooseTimeRange,
onChooseAutoRefresh,
}) => (
<div className="page-header full-width">
<div className="page-header__container">
<div className="page-header__left">
<h1 className="page-header__title">Data Explorer</h1>
</div>
<div className="page-header__right">
<GraphTips />
<SourceIndicator />
<div
className="btn btn-sm btn-default"
onClick={showWriteForm}
data-test="write-data-button"
>
<span className="icon pencil" />
Write Data
</div>
<AutoRefreshDropdown
iconName="refresh"
selected={autoRefresh}
onChoose={onChooseAutoRefresh}
onManualRefresh={onManualRefresh}
/>
<TimeRangeDropdown
selected={timeRange}
page="DataExplorer"
onChooseTimeRange={onChooseTimeRange}
/>
</div>
</div>
</div>
)
Header.propTypes = {
onChooseAutoRefresh: func.isRequired,
onChooseTimeRange: func.isRequired,
onManualRefresh: func.isRequired,
autoRefresh: number.isRequired,
showWriteForm: func.isRequired,
timeRange: shape({
lower: string,
upper: string,
}).isRequired,
}
export default withRouter(Header)

View File

@ -0,0 +1,63 @@
import React, {PureComponent} from 'react'
import AutoRefreshDropdown from 'src/shared/components/AutoRefreshDropdown'
import TimeRangeDropdown from 'src/shared/components/TimeRangeDropdown'
import SourceIndicator from 'src/shared/components/SourceIndicator'
import GraphTips from 'src/shared/components/GraphTips'
import {TimeRange} from 'src/types'
interface Props {
onChooseAutoRefresh: () => void
onManualRefresh: () => void
onChooseTimeRange: (timeRange: TimeRange) => void
showWriteForm: () => void
autoRefresh: number
timeRange: TimeRange
}
class Header extends PureComponent<Props> {
public render() {
const {
timeRange,
autoRefresh,
showWriteForm,
onManualRefresh,
onChooseTimeRange,
onChooseAutoRefresh,
} = this.props
return (
<div className="page-header full-width">
<div className="page-header__container">
<div className="page-header__left">
<h1 className="page-header__title">Data Explorer</h1>
</div>
<div className="page-header__right">
<GraphTips />
<SourceIndicator />
<div
className="btn btn-sm btn-default"
onClick={showWriteForm}
data-test="write-data-button"
>
<span className="icon pencil" />
Write Data
</div>
<AutoRefreshDropdown
iconName="refresh"
selected={autoRefresh}
onChoose={onChooseAutoRefresh}
onManualRefresh={onManualRefresh}
/>
<TimeRangeDropdown
selected={timeRange}
page="DataExplorer"
onChooseTimeRange={onChooseTimeRange}
/>
</div>
</div>
</div>
)
}
}
export default Header

View File

@ -1,6 +1,9 @@
import _ from 'lodash'
import defaultQueryConfig from 'src/utils/defaultQueryConfig'
import {QueryConfig} from 'src/types'
import {Action} from 'src/data_explorer/actions/view'
import {
fill,
timeShift,
@ -18,7 +21,11 @@ import {
toggleTagAcceptance,
} from 'src/utils/queryTransitions'
const queryConfigs = (state = {}, action) => {
interface State {
[queryID: string]: Readonly<QueryConfig>
}
const queryConfigs = (state: State = {}, action: Action): State => {
switch (action.type) {
case 'DE_CHOOSE_NAMESPACE': {
const {queryID, database, retentionPolicy} = action.payload
@ -27,9 +34,7 @@ const queryConfigs = (state = {}, action) => {
retentionPolicy,
})
return Object.assign({}, state, {
[queryID]: Object.assign(nextQueryConfig, {rawText: null}),
})
return {...state, [queryID]: {...nextQueryConfig, rawText: null}}
}
case 'DE_CHOOSE_MEASUREMENT': {
@ -71,36 +76,31 @@ const queryConfigs = (state = {}, action) => {
const {queryID, rawText} = action.payload
const nextQueryConfig = editRawText(state[queryID], rawText)
return Object.assign({}, state, {
return {
...state,
[queryID]: nextQueryConfig,
})
}
}
case 'DE_GROUP_BY_TIME': {
const {queryID, time} = action.payload
const nextQueryConfig = groupByTime(state[queryID], time)
return Object.assign({}, state, {
[queryID]: nextQueryConfig,
})
return {...state, [queryID]: nextQueryConfig}
}
case 'DE_TOGGLE_TAG_ACCEPTANCE': {
const {queryID} = action.payload
const nextQueryConfig = toggleTagAcceptance(state[queryID])
return Object.assign({}, state, {
[queryID]: nextQueryConfig,
})
return {...state, [queryID]: nextQueryConfig}
}
case 'DE_TOGGLE_FIELD': {
const {queryID, fieldFunc} = action.payload
const nextQueryConfig = toggleField(state[queryID], fieldFunc)
return Object.assign({}, state, {
[queryID]: {...nextQueryConfig, rawText: null},
})
return {...state, [queryID]: {...nextQueryConfig, rawText: null}}
}
case 'DE_APPLY_FUNCS_TO_FIELD': {
@ -111,26 +111,20 @@ const queryConfigs = (state = {}, action) => {
groupBy
)
return Object.assign({}, state, {
[queryID]: nextQueryConfig,
})
return {...state, [queryID]: nextQueryConfig}
}
case 'DE_CHOOSE_TAG': {
const {queryID, tag} = action.payload
const nextQueryConfig = chooseTag(state[queryID], tag)
return Object.assign({}, state, {
[queryID]: nextQueryConfig,
})
return {...state, [queryID]: nextQueryConfig}
}
case 'DE_GROUP_BY_TAG': {
const {queryID, tagKey} = action.payload
const nextQueryConfig = groupByTag(state[queryID], tagKey)
return Object.assign({}, state, {
[queryID]: nextQueryConfig,
})
return {...state, [queryID]: nextQueryConfig}
}
case 'DE_FILL': {

View File

@ -1,19 +0,0 @@
import {timeRanges} from 'shared/data/timeRanges'
const {lower, upper} = timeRanges.find(tr => tr.lower === 'now() - 1h')
const initialState = {
upper,
lower,
}
export default function timeRange(state = initialState, action) {
switch (action.type) {
case 'DE_SET_TIME_RANGE': {
const {bounds} = action.payload
return {...state, ...bounds}
}
}
return state
}

View File

@ -0,0 +1,33 @@
import {timeRanges} from 'src/shared/data/timeRanges'
import {TimeRange} from 'src/types'
const {lower, upper} = timeRanges.find(tr => tr.lower === 'now() - 1h')
const initialState = {
upper,
lower,
}
type State = Readonly<TimeRange>
interface ActionSetTimeRange {
type: 'DE_SET_TIME_RANGE'
payload: {
bounds: TimeRange
}
}
type Action = ActionSetTimeRange
const timeRange = (state: State = initialState, action: Action): State => {
switch (action.type) {
case 'DE_SET_TIME_RANGE': {
const {bounds} = action.payload
return {...state, ...bounds}
}
}
return state
}
export default timeRange

View File

@ -1,8 +1,31 @@
interface DataExplorerState {
queryIDs: ReadonlyArray<string>
}
interface ActionAddQuery {
type: 'DE_ADD_QUERY'
payload: {
queryID: string
}
}
interface ActionDeleteQuery {
type: 'DE_DELETE_QUERY'
payload: {
queryID: string
}
}
type Action = ActionAddQuery | ActionDeleteQuery
const initialState = {
queryIDs: [],
}
export default function ui(state = initialState, action) {
const ui = (
state: DataExplorerState = initialState,
action: Action
): DataExplorerState => {
switch (action.type) {
// there is an additional reducer for this same action in the queryConfig reducer
case 'DE_ADD_QUERY': {
@ -27,3 +50,5 @@ export default function ui(state = initialState, action) {
return state
}
export default ui

View File

@ -6,7 +6,7 @@ import TimeMachineVis from 'src/ifql/components/TimeMachineVis'
import Threesizer from 'src/shared/components/Threesizer'
import {Suggestion, OnChangeScript, FlatBody} from 'src/types/ifql'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {HANDLE_VERTICAL, HANDLE_HORIZONTAL} from 'src/shared/constants/index'
import {HANDLE_VERTICAL, HANDLE_HORIZONTAL} from 'src/shared/constants'
interface Props {
script: string

View File

@ -1,12 +1,15 @@
import {proxy} from 'utils/queryUrlGenerator'
import {noop} from 'shared/actions/app'
import {proxy} from 'src/utils/queryUrlGenerator'
import {noop} from 'src/shared/actions/app'
import _ from 'lodash'
import {errorThrown} from 'shared/actions/errors'
import {errorThrown} from 'src/shared/actions/errors'
export const handleLoading = (query, editQueryStatus) => {
editQueryStatus(query.id, {loading: true})
editQueryStatus(query.id, {
loading: true,
})
}
// {results: [{}]}
export const handleSuccess = (data, query, editQueryStatus) => {
const {results} = data
@ -22,12 +25,16 @@ export const handleSuccess = (data, query, editQueryStatus) => {
// 200 from chrono server but influx returns an "error" = warning
if (error) {
editQueryStatus(query.id, {warn: error})
editQueryStatus(query.id, {
warn: error,
})
return data
}
// 200 from server and results contains data = success
editQueryStatus(query.id, {success: 'Success!'})
editQueryStatus(query.id, {
success: 'Success!',
})
return data
}
@ -39,11 +46,21 @@ export const handleError = (error, query, editQueryStatus) => {
)
// 400 from chrono server = fail
editQueryStatus(query.id, {error: message})
editQueryStatus(query.id, {
error: message,
})
}
interface Payload {
source: string
query: string
tempVars: any[]
db?: string
rp?: string
resolution?: number
}
export const fetchTimeSeriesAsync = async (
{source, db, rp, query, tempVars, resolution},
{source, db, rp, query, tempVars, resolution}: Payload,
editQueryStatus = noop
) => {
handleLoading(query, editQueryStatus)
@ -52,7 +69,7 @@ export const fetchTimeSeriesAsync = async (
source,
db,
rp,
query: query.text,
query,
tempVars,
resolution,
})

View File

@ -31,14 +31,22 @@ interface Query {
rp: string
}
const parseSource = source => {
if (Array.isArray(source)) {
return _.get(source, '0', '')
}
return source
}
export const fetchTimeSeries = async (
queries: Query[],
resolution: number,
templates: Template[],
editQueryStatus: () => void
editQueryStatus: () => any
) => {
const timeSeriesPromises = queries.map(query => {
const {host, database, rp} = query
const {host, database, rp, text} = query
// the key `database` was used upstream in HostPage.js, and since as of this writing
// the codebase has not been fully converted to TypeScript, it's not clear where else
// it may be used, but this slight modification is intended to allow for the use of
@ -63,11 +71,9 @@ export const fetchTimeSeries = async (
const tempVars = removeUnselectedTemplateValues(templatesWithIntervalVals)
const source = host
return fetchTimeSeriesAsync(
{source, db, rp, query, tempVars, resolution},
editQueryStatus
)
const source = parseSource(host)
const payload = {source, db, rp, query: text, tempVars, resolution}
return fetchTimeSeriesAsync(payload, editQueryStatus)
})
return Promise.all(timeSeriesPromises)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,6 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import ReactGridLayout, {WidthProvider} from 'react-grid-layout'
import {ResizableBox} from 'react-resizable'
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
@ -26,7 +25,6 @@ class LayoutRenderer extends Component {
this.state = {
rowHeight: this.calculateRowHeight(),
resizeCoords: null,
}
}
@ -36,8 +34,16 @@ class LayoutRenderer extends Component {
}
const newCells = this.props.cells.map(cell => {
const l = layout.find(ly => ly.i === cell.i)
const newLayout = {x: l.x, y: l.y, h: l.h, w: l.w}
return {...cell, ...newLayout}
const newLayout = {
x: l.x,
y: l.y,
h: l.h,
w: l.w,
}
return {
...cell,
...newLayout,
}
})
this.props.onPositionChange(newCells)
}
@ -56,10 +62,6 @@ class LayoutRenderer extends Component {
: DASHBOARD_LAYOUT_ROW_HEIGHT
}
handleCellResize = () => {
this.resizeCoords = this.setState({resizeCoords: new Date()})
}
render() {
const {
host,
@ -79,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>
)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,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>({

View File

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

View File

@ -1,6 +1,6 @@
import reducer from 'src/data_explorer/reducers/queryConfigs'
import defaultQueryConfig from 'utils/defaultQueryConfig'
import defaultQueryConfig from 'src/utils/defaultQueryConfig'
import {
fill,
timeShift,
@ -9,6 +9,7 @@ import {
groupByTime,
toggleField,
removeFuncs,
editRawText,
updateRawQuery,
editQueryStatus,
chooseNamespace,
@ -17,36 +18,46 @@ import {
addInitialField,
updateQueryConfig,
toggleTagAcceptance,
ActionAddQuery,
} from 'src/data_explorer/actions/view'
import {LINEAR, NULL_STRING} from 'shared/constants/queryFillOptions'
import {LINEAR, NULL_STRING} from 'src/shared/constants/queryFillOptions'
const fakeAddQueryAction = (panelID, queryID) => {
const fakeAddQueryAction = (queryID: string): ActionAddQuery => {
return {
type: 'DE_ADD_QUERY',
payload: {panelID, queryID},
payload: {
queryID,
},
}
}
function buildInitialState(queryID, params) {
return Object.assign({}, defaultQueryConfig({id: queryID}), params)
function buildInitialState(queryID, params?) {
return {
...defaultQueryConfig({
id: queryID,
}),
...params,
}
}
describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
const queryID = 123
const queryID = '123'
it('can add a query', () => {
const state = reducer({}, fakeAddQueryAction('blah', queryID))
const state = reducer({}, fakeAddQueryAction(queryID))
const actual = state[queryID]
const expected = defaultQueryConfig({id: queryID})
const expected = defaultQueryConfig({
id: queryID,
})
expect(actual).toEqual(expected)
})
describe('choosing db, rp, and measurement', () => {
let state
beforeEach(() => {
state = reducer({}, fakeAddQueryAction('any', queryID))
state = reducer({}, fakeAddQueryAction(queryID))
})
it('sets the db and rp', () => {
@ -72,7 +83,7 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
describe('a query has measurements and fields', () => {
let state
beforeEach(() => {
const one = reducer({}, fakeAddQueryAction('any', queryID))
const one = reducer({}, fakeAddQueryAction(queryID))
const two = reducer(
one,
chooseNamespace(queryID, {
@ -81,14 +92,13 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
})
)
const three = reducer(two, chooseMeasurement(queryID, 'disk'))
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)
})
})
})

View File

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