diff --git a/ui/package.json b/ui/package.json index 9cd976f7d4..b6c0aa7219 100644 --- a/ui/package.json +++ b/ui/package.json @@ -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", diff --git a/ui/src/data_explorer/actions/view/index.js b/ui/src/data_explorer/actions/view/index.js deleted file mode 100644 index a0071e9fe9..0000000000 --- a/ui/src/data_explorer/actions/view/index.js +++ /dev/null @@ -1,167 +0,0 @@ -import uuid from 'uuid' - -import {getQueryConfigAndStatus} from 'shared/apis' - -import {errorThrown} from 'shared/actions/errors' - -export const addQuery = (queryID = uuid.v4()) => ({ - type: 'DE_ADD_QUERY', - payload: { - queryID, - }, -}) - -export const deleteQuery = queryID => ({ - type: 'DE_DELETE_QUERY', - payload: { - queryID, - }, -}) - -export const toggleField = (queryID, fieldFunc) => ({ - type: 'DE_TOGGLE_FIELD', - payload: { - queryID, - fieldFunc, - }, -}) - -export const groupByTime = (queryID, time) => ({ - type: 'DE_GROUP_BY_TIME', - payload: { - queryID, - time, - }, -}) - -export const fill = (queryID, value) => ({ - type: 'DE_FILL', - payload: { - queryID, - value, - }, -}) - -export const removeFuncs = (queryID, fields, groupBy) => ({ - type: 'DE_REMOVE_FUNCS', - payload: { - queryID, - fields, - groupBy, - }, -}) - -export const applyFuncsToField = (queryID, fieldFunc, groupBy) => ({ - type: 'DE_APPLY_FUNCS_TO_FIELD', - payload: { - queryID, - fieldFunc, - groupBy, - }, -}) - -export const chooseTag = (queryID, tag) => ({ - type: 'DE_CHOOSE_TAG', - payload: { - queryID, - tag, - }, -}) - -export const chooseNamespace = (queryID, {database, retentionPolicy}) => ({ - type: 'DE_CHOOSE_NAMESPACE', - payload: { - queryID, - database, - retentionPolicy, - }, -}) - -export const chooseMeasurement = (queryID, measurement) => ({ - type: 'DE_CHOOSE_MEASUREMENT', - payload: { - queryID, - measurement, - }, -}) - -export const editRawText = (queryID, rawText) => ({ - type: 'DE_EDIT_RAW_TEXT', - payload: { - queryID, - rawText, - }, -}) - -export const setTimeRange = bounds => ({ - type: 'DE_SET_TIME_RANGE', - payload: { - bounds, - }, -}) - -export const groupByTag = (queryID, tagKey) => ({ - type: 'DE_GROUP_BY_TAG', - payload: { - queryID, - tagKey, - }, -}) - -export const toggleTagAcceptance = queryID => ({ - type: 'DE_TOGGLE_TAG_ACCEPTANCE', - payload: { - queryID, - }, -}) - -export const updateRawQuery = (queryID, text) => ({ - type: 'DE_UPDATE_RAW_QUERY', - payload: { - queryID, - text, - }, -}) - -export const updateQueryConfig = config => ({ - type: 'DE_UPDATE_QUERY_CONFIG', - payload: { - config, - }, -}) - -export const addInitialField = (queryID, field, groupBy) => ({ - type: 'DE_ADD_INITIAL_FIELD', - payload: { - queryID, - field, - groupBy, - }, -}) - -export const editQueryStatus = (queryID, status) => ({ - type: 'DE_EDIT_QUERY_STATUS', - payload: { - queryID, - status, - }, -}) - -export const timeShift = (queryID, shift) => ({ - type: 'DE_TIME_SHIFT', - payload: { - queryID, - shift, - }, -}) - -// Async actions -export const editRawTextAsync = (url, id, text) => async dispatch => { - try { - const {data} = await getQueryConfigAndStatus(url, [{query: text, id}]) - const config = data.queries.find(q => q.id === id) - dispatch(updateQueryConfig(config.queryConfig)) - } catch (error) { - dispatch(errorThrown(error)) - } -} diff --git a/ui/src/data_explorer/actions/view/index.ts b/ui/src/data_explorer/actions/view/index.ts new file mode 100644 index 0000000000..4b0cfa24bb --- /dev/null +++ b/ui/src/data_explorer/actions/view/index.ts @@ -0,0 +1,407 @@ +import uuid from 'uuid' + +import {getQueryConfigAndStatus} from 'src/shared/apis' + +import {errorThrown} from 'src/shared/actions/errors' +import { + QueryConfig, + Status, + Field, + GroupBy, + Tag, + TimeRange, + TimeShift, + ApplyFuncsToFieldArgs, +} from 'src/types' + +export type Action = + | ActionAddQuery + | ActionDeleteQuery + | ActionToggleField + | ActionGroupByTime + | ActionFill + | ActionRemoveFuncs + | ActionApplyFuncsToField + | ActionChooseTag + | ActionChooseNamspace + | ActionChooseMeasurement + | ActionEditRawText + | ActionSetTimeRange + | ActionGroupByTime + | ActionToggleField + | ActionUpdateRawQuery + | ActionQueryConfig + | ActionTimeShift + | ActionToggleTagAcceptance + | ActionToggleField + | ActionGroupByTag + | ActionEditQueryStatus + | ActionAddInitialField + +export interface ActionAddQuery { + type: 'DE_ADD_QUERY' + payload: { + queryID: string + } +} + +export const addQuery = (queryID: string = uuid.v4()): ActionAddQuery => ({ + type: 'DE_ADD_QUERY', + payload: { + queryID, + }, +}) + +interface ActionDeleteQuery { + type: 'DE_DELETE_QUERY' + payload: { + queryID: string + } +} + +export const deleteQuery = (queryID: string): ActionDeleteQuery => ({ + type: 'DE_DELETE_QUERY', + payload: { + queryID, + }, +}) + +interface ActionToggleField { + type: 'DE_TOGGLE_FIELD' + payload: { + queryID: string + fieldFunc: Field + } +} + +export const toggleField = ( + queryID: string, + fieldFunc: Field +): ActionToggleField => ({ + type: 'DE_TOGGLE_FIELD', + payload: { + queryID, + fieldFunc, + }, +}) + +interface ActionGroupByTime { + type: 'DE_GROUP_BY_TIME' + payload: { + queryID: string + time: string + } +} + +export const groupByTime = ( + queryID: string, + time: string +): ActionGroupByTime => ({ + type: 'DE_GROUP_BY_TIME', + payload: { + queryID, + time, + }, +}) + +interface ActionFill { + type: 'DE_FILL' + payload: { + queryID: string + value: string + } +} + +export const fill = (queryID: string, value: string): ActionFill => ({ + type: 'DE_FILL', + payload: { + queryID, + value, + }, +}) + +interface ActionRemoveFuncs { + type: 'DE_REMOVE_FUNCS' + payload: { + queryID: string + fields: Field[] + groupBy: GroupBy + } +} + +export const removeFuncs = ( + queryID: string, + fields: Field[], + groupBy: GroupBy +): ActionRemoveFuncs => ({ + type: 'DE_REMOVE_FUNCS', + payload: { + queryID, + fields, + groupBy, + }, +}) + +interface ActionApplyFuncsToField { + type: 'DE_APPLY_FUNCS_TO_FIELD' + payload: { + queryID: string + fieldFunc: ApplyFuncsToFieldArgs + groupBy: GroupBy + } +} + +export const applyFuncsToField = ( + queryID: string, + fieldFunc: ApplyFuncsToFieldArgs, + groupBy?: GroupBy +): ActionApplyFuncsToField => ({ + type: 'DE_APPLY_FUNCS_TO_FIELD', + payload: { + queryID, + fieldFunc, + groupBy, + }, +}) + +interface ActionChooseTag { + type: 'DE_CHOOSE_TAG' + payload: { + queryID: string + tag: Tag + } +} + +export const chooseTag = (queryID: string, tag: Tag): ActionChooseTag => ({ + type: 'DE_CHOOSE_TAG', + payload: { + queryID, + tag, + }, +}) + +interface ActionChooseNamspace { + type: 'DE_CHOOSE_NAMESPACE' + payload: { + queryID: string + database: string + retentionPolicy: string + } +} + +interface DBRP { + database: string + retentionPolicy: string +} + +export const chooseNamespace = ( + queryID: string, + {database, retentionPolicy}: DBRP +): ActionChooseNamspace => ({ + type: 'DE_CHOOSE_NAMESPACE', + payload: { + queryID, + database, + retentionPolicy, + }, +}) + +interface ActionChooseMeasurement { + type: 'DE_CHOOSE_MEASUREMENT' + payload: { + queryID: string + measurement: string + } +} + +export const chooseMeasurement = ( + queryID: string, + measurement: string +): ActionChooseMeasurement => ({ + type: 'DE_CHOOSE_MEASUREMENT', + payload: { + queryID, + measurement, + }, +}) + +interface ActionEditRawText { + type: 'DE_EDIT_RAW_TEXT' + payload: { + queryID: string + rawText: string + } +} + +export const editRawText = ( + queryID: string, + rawText: string +): ActionEditRawText => ({ + type: 'DE_EDIT_RAW_TEXT', + payload: { + queryID, + rawText, + }, +}) + +interface ActionSetTimeRange { + type: 'DE_SET_TIME_RANGE' + payload: { + bounds: TimeRange + } +} + +export const setTimeRange = (bounds: TimeRange): ActionSetTimeRange => ({ + type: 'DE_SET_TIME_RANGE', + payload: { + bounds, + }, +}) + +interface ActionGroupByTag { + type: 'DE_GROUP_BY_TAG' + payload: { + queryID: string + tagKey: string + } +} + +export const groupByTag = ( + queryID: string, + tagKey: string +): ActionGroupByTag => ({ + type: 'DE_GROUP_BY_TAG', + payload: { + queryID, + tagKey, + }, +}) + +interface ActionToggleTagAcceptance { + type: 'DE_TOGGLE_TAG_ACCEPTANCE' + payload: { + queryID: string + } +} + +export const toggleTagAcceptance = ( + queryID: string +): ActionToggleTagAcceptance => ({ + type: 'DE_TOGGLE_TAG_ACCEPTANCE', + payload: { + queryID, + }, +}) + +interface ActionUpdateRawQuery { + type: 'DE_UPDATE_RAW_QUERY' + payload: { + queryID: string + text: string + } +} + +export const updateRawQuery = ( + queryID: string, + text: string +): ActionUpdateRawQuery => ({ + type: 'DE_UPDATE_RAW_QUERY', + payload: { + queryID, + text, + }, +}) + +interface ActionQueryConfig { + type: 'DE_UPDATE_QUERY_CONFIG' + payload: { + config: QueryConfig + } +} + +export const updateQueryConfig = (config: QueryConfig): ActionQueryConfig => ({ + type: 'DE_UPDATE_QUERY_CONFIG', + payload: { + config, + }, +}) + +interface ActionAddInitialField { + type: 'DE_ADD_INITIAL_FIELD' + payload: { + queryID: string + field: Field + groupBy?: GroupBy + } +} + +export const addInitialField = ( + queryID: string, + field: Field, + groupBy: GroupBy +): ActionAddInitialField => ({ + type: 'DE_ADD_INITIAL_FIELD', + payload: { + queryID, + field, + groupBy, + }, +}) + +interface ActionEditQueryStatus { + type: 'DE_EDIT_QUERY_STATUS' + payload: { + queryID: string + status: Status + } +} + +export const editQueryStatus = ( + queryID: string, + status: Status +): ActionEditQueryStatus => ({ + type: 'DE_EDIT_QUERY_STATUS', + payload: { + queryID, + status, + }, +}) + +interface ActionTimeShift { + type: 'DE_TIME_SHIFT' + payload: { + queryID: string + shift: TimeShift + } +} + +export const timeShift = ( + queryID: string, + shift: TimeShift +): ActionTimeShift => ({ + type: 'DE_TIME_SHIFT', + payload: { + queryID, + shift, + }, +}) + +// Async actions +export const editRawTextAsync = ( + url: string, + id: string, + text: string +) => async (dispatch): Promise => { + try { + const {data} = await getQueryConfigAndStatus(url, [ + { + query: text, + id, + }, + ]) + const config = data.queries.find(q => q.id === id) + dispatch(updateQueryConfig(config.queryConfig)) + } catch (error) { + dispatch(errorThrown(error)) + } +} diff --git a/ui/src/data_explorer/actions/view/write.js b/ui/src/data_explorer/actions/view/write.ts similarity index 57% rename from ui/src/data_explorer/actions/view/write.js rename to ui/src/data_explorer/actions/view/write.ts index 2dca304ae5..364a9b5747 100644 --- a/ui/src/data_explorer/actions/view/write.js +++ b/ui/src/data_explorer/actions/view/write.ts @@ -1,13 +1,18 @@ import {writeLineProtocol as writeLineProtocolAJAX} from 'src/data_explorer/apis' -import {notify} from 'shared/actions/notifications' +import {notify} from 'src/shared/actions/notifications' +import {Source} from 'src/types' import { notifyDataWritten, notifyDataWriteFailed, -} from 'shared/copy/notifications' +} from 'src/shared/copy/notifications' -export const writeLineProtocolAsync = (source, db, data) => async dispatch => { +export const writeLineProtocolAsync = ( + source: Source, + db: string, + data: string +) => async (dispatch): Promise => { try { await writeLineProtocolAJAX(source, db, data) dispatch(notify(notifyDataWritten())) diff --git a/ui/src/data_explorer/apis/index.js b/ui/src/data_explorer/apis/index.js deleted file mode 100644 index 0abe191a66..0000000000 --- a/ui/src/data_explorer/apis/index.js +++ /dev/null @@ -1,8 +0,0 @@ -import AJAX from 'src/utils/ajax' - -export const writeLineProtocol = async (source, db, data) => - await AJAX({ - url: `${source.links.write}?db=${db}`, - method: 'POST', - data, - }) diff --git a/ui/src/data_explorer/apis/index.ts b/ui/src/data_explorer/apis/index.ts new file mode 100644 index 0000000000..d933354552 --- /dev/null +++ b/ui/src/data_explorer/apis/index.ts @@ -0,0 +1,67 @@ +import AJAX from 'src/utils/ajax' +import _ from 'lodash' +import moment from 'moment' +import download from 'src/external/download' + +import {proxy} from 'src/utils/queryUrlGenerator' +import {timeSeriesToTableGraph} from 'src/utils/timeSeriesTransformers' +import {dataToCSV} from 'src/shared/parsing/dataToCSV' +import {TEMPLATES} from 'src/shared/constants' +import {Source, QueryConfig} from 'src/types' + +export const writeLineProtocol = async ( + source: Source, + db: string, + data: string +): Promise => + await AJAX({ + url: `${source.links.write}?db=${db}`, + method: 'POST', + data, + }) + +interface DeprecatedQuery { + id: string + host: string + queryConfig: QueryConfig + text: string +} + +export const getDataForCSV = ( + query: DeprecatedQuery, + errorThrown +) => async () => { + try { + const response = await fetchTimeSeriesForCSV({ + source: query.host, + query: query.text, + tempVars: TEMPLATES, + }) + + const {data} = timeSeriesToTableGraph([{response}]) + const name = csvName(query.queryConfig) + download(dataToCSV(data), `${name}.csv`, 'text/plain') + } catch (error) { + errorThrown(error, 'Unable to download .csv file') + console.error(error) + } +} + +const fetchTimeSeriesForCSV = async ({source, query, tempVars}) => { + try { + const {data} = await proxy({source, query, tempVars}) + return data + } catch (error) { + console.error(error) + throw error + } +} + +const csvName = (query: QueryConfig): string => { + const db = _.get(query, 'database', '') + const rp = _.get(query, 'retentionPolicy', '') + const measurement = _.get(query, 'measurement', '') + + const timestring = moment().format('YYYY-MM-DD-HH-mm') + return `${db}.${rp}.${measurement}.${timestring}` +} diff --git a/ui/src/data_explorer/components/QueryEditor.js b/ui/src/data_explorer/components/QueryEditor.tsx similarity index 61% rename from ui/src/data_explorer/components/QueryEditor.js rename to ui/src/data_explorer/components/QueryEditor.tsx index 220f5b6090..0f64bdec84 100644 --- a/ui/src/data_explorer/components/QueryEditor.js +++ b/ui/src/data_explorer/components/QueryEditor.tsx @@ -1,27 +1,80 @@ -import React, {Component} from 'react' -import PropTypes from 'prop-types' +import React, {PureComponent, KeyboardEvent} from 'react' -import Dropdown from 'shared/components/Dropdown' -import {QUERY_TEMPLATES} from 'src/data_explorer/constants' -import QueryStatus from 'shared/components/QueryStatus' +import Dropdown from 'src/shared/components/Dropdown' +import {QUERY_TEMPLATES, QueryTemplate} from 'src/data_explorer/constants' +import QueryStatus from 'src/shared/components/QueryStatus' import {ErrorHandling} from 'src/shared/decorators/errors' +import {QueryConfig} from 'src/types' + +interface Props { + query: string + config: QueryConfig + onUpdate: (value: string) => void +} + +interface State { + value: string +} @ErrorHandling -class QueryEditor extends Component { +class QueryEditor extends PureComponent { + private editor: React.RefObject + constructor(props) { super(props) this.state = { value: this.props.query, } + + this.editor = React.createRef() } - componentWillReceiveProps(nextProps) { + public componentWillReceiveProps(nextProps: Props) { if (this.props.query !== nextProps.query) { this.setState({value: nextProps.query}) } } - handleKeyDown = e => { + public render() { + const { + config: {status}, + } = this.props + const {value} = this.state + + return ( +
+