Merge branch 'master' into bugfix/groupbys-in-alert-rule

pull/10616/head
ebb-tide 2018-05-01 18:21:45 -07:00
commit e044f5b76a
67 changed files with 1642 additions and 628 deletions

View File

@ -11,6 +11,7 @@
1. [#3215](https://github.com/influxdata/chronograf/pull/3215): Fix Template Variables Control Bar to top of dashboard page 1. [#3215](https://github.com/influxdata/chronograf/pull/3215): Fix Template Variables Control Bar to top of dashboard page
1. [#3214](https://github.com/influxdata/chronograf/pull/3214): Remove extra click when creating dashboard cell 1. [#3214](https://github.com/influxdata/chronograf/pull/3214): Remove extra click when creating dashboard cell
1. [#3256](https://github.com/influxdata/chronograf/pull/3256): Reduce font sizes in dashboards for increased space efficiency 1. [#3256](https://github.com/influxdata/chronograf/pull/3256): Reduce font sizes in dashboards for increased space efficiency
1. [#3320](https://github.com/influxdata/chronograf/pull/3320): Add overlay animation to Template Variables Manager
1. [#3245](https://github.com/influxdata/chronograf/pull/3245): Display 'no results' on cells without results 1. [#3245](https://github.com/influxdata/chronograf/pull/3245): Display 'no results' on cells without results
### Bug Fixes ### Bug Fixes
@ -20,6 +21,7 @@
1. [#3281](https://github.com/influxdata/chronograf/pull/3281): Fix base path for kapacitor logs 1. [#3281](https://github.com/influxdata/chronograf/pull/3281): Fix base path for kapacitor logs
1. [#3284](https://github.com/influxdata/chronograf/pull/3284): Fix logout when using basepath & simplify basepath usage (deprecates `PREFIX_ROUTES`) 1. [#3284](https://github.com/influxdata/chronograf/pull/3284): Fix logout when using basepath & simplify basepath usage (deprecates `PREFIX_ROUTES`)
1. [#3349](https://github.com/influxdata/chronograf/pull/3349): Fix graphs in alert rule builder for queries that include groupby 1. [#3349](https://github.com/influxdata/chronograf/pull/3349): Fix graphs in alert rule builder for queries that include groupby
1. [#3345](https://github.com/influxdata/chronograf/pull/3345): Fix auto not showing in the group by dropdown and explorer getting disconnected
## v1.4.4.1 [2018-04-16] ## v1.4.4.1 [2018-04-16]

View File

@ -63,3 +63,49 @@ export const updateKapacitorBody = {
proxy: '/chronograf/v1/sources/47/kapacitors/1/proxy', proxy: '/chronograf/v1/sources/47/kapacitors/1/proxy',
}, },
} }
export const queryConfig = {
queries: [
{
id: '60842c85-8bc7-4180-a844-b974e47a98cd',
query:
'SELECT mean(:fields:), mean("usage_user") AS "mean_usage_user" FROM "telegraf"."autogen"."cpu" WHERE time > :dashboardTime: GROUP BY time(:interval:) FILL(null)',
queryConfig: {
id: '60842c85-8bc7-4180-a844-b974e47a98cd',
database: 'telegraf',
measurement: 'cpu',
retentionPolicy: 'autogen',
fields: [
{
value: 'mean',
type: 'func',
alias: '',
args: [{value: 'usage_idle', type: 'field', alias: ''}],
},
{
value: 'mean',
type: 'func',
alias: 'mean_usage_user',
args: [{value: 'usage_user', type: 'field', alias: ''}],
},
],
tags: {},
groupBy: {time: 'auto', tags: []},
areTagsAccepted: false,
fill: 'null',
rawText:
'SELECT mean(:fields:), mean("usage_user") AS "mean_usage_user" FROM "telegraf"."autogen"."cpu" WHERE time > :dashboardTime: GROUP BY time(:interval:) FILL(null)',
range: null,
shifts: [],
},
queryTemplated:
'SELECT mean("usage_idle"), mean("usage_user") AS "mean_usage_user" FROM "telegraf"."autogen"."cpu" WHERE time > :dashboardTime: GROUP BY time(:interval:) FILL(null)',
tempVars: [
{
tempVar: ':fields:',
values: [{value: 'usage_idle', type: 'fieldKey', selected: true}],
},
],
},
],
}

View File

@ -1,7 +1,10 @@
import {kapacitor} from 'mocks/dummy' import {kapacitor, queryConfig} from 'mocks/dummy'
export const getKapacitor = jest.fn(() => Promise.resolve(kapacitor)) export const getKapacitor = jest.fn(() => Promise.resolve(kapacitor))
export const getActiveKapacitor = jest.fn(() => Promise.resolve(kapacitor)) export const getActiveKapacitor = jest.fn(() => Promise.resolve(kapacitor))
export const createKapacitor = jest.fn(() => Promise.resolve({data: kapacitor})) export const createKapacitor = jest.fn(() => Promise.resolve({data: kapacitor}))
export const updateKapacitor = jest.fn(() => Promise.resolve({data: kapacitor})) export const updateKapacitor = jest.fn(() => Promise.resolve({data: kapacitor}))
export const pingKapacitor = jest.fn(() => Promise.resolve()) export const pingKapacitor = jest.fn(() => Promise.resolve())
export const getQueryConfigAndStatus = jest.fn(() =>
Promise.resolve({data: queryConfig})
)

View File

@ -3,9 +3,11 @@ import PropTypes from 'prop-types'
import SideNav from 'src/side_nav' import SideNav from 'src/side_nav'
import Notifications from 'shared/components/Notifications' import Notifications from 'shared/components/Notifications'
import Overlay from 'shared/components/OverlayTechnology'
const App = ({children}) => ( const App = ({children}) => (
<div className="chronograf-root"> <div className="chronograf-root">
<Overlay />
<Notifications /> <Notifications />
<SideNav /> <SideNav />
{children} {children}

View File

@ -51,6 +51,8 @@ interface Props {
errorThrown: () => void errorThrown: () => void
} }
export const SourceContext = React.createContext()
// Acts as a 'router middleware'. The main `App` component is responsible for // Acts as a 'router middleware'. The main `App` component is responsible for
// getting the list of data sources, but not every page requires them to function. // getting the list of data sources, but not every page requires them to function.
// Routes that do require data sources can be nested under this component. // Routes that do require data sources can be nested under this component.
@ -205,8 +207,10 @@ export class CheckSources extends Component<Props, State> {
// TODO: guard against invalid resource access // TODO: guard against invalid resource access
return ( return (
this.props.children && <SourceContext.Provider value={source}>
React.cloneElement(this.props.children, {...this.props, source}) {this.props.children &&
React.cloneElement(this.props.children, {...this.props, source})}
</SourceContext.Provider>
) )
} }
} }

View File

@ -5,7 +5,7 @@ interface Props {
} }
const CEOBottom: SFC<Props> = ({children}) => ( const CEOBottom: SFC<Props> = ({children}) => (
<div className="overlay-technology--editor">{children}</div> <div className="ceo--editor">{children}</div>
) )
export default CEOBottom export default CEOBottom

View File

@ -3,6 +3,11 @@ import React, {Component} from 'react'
import _ from 'lodash' import _ from 'lodash'
import uuid from 'uuid' import uuid from 'uuid'
import {
CellEditorOverlayActions,
CellEditorOverlayActionsFunc,
} from 'src/types/dashboard'
import ResizeContainer from 'src/shared/components/ResizeContainer' import ResizeContainer from 'src/shared/components/ResizeContainer'
import QueryMaker from 'src/dashboards/components/QueryMaker' import QueryMaker from 'src/dashboards/components/QueryMaker'
import Visualization from 'src/dashboards/components/Visualization' import Visualization from 'src/dashboards/components/Visualization'
@ -14,7 +19,7 @@ import * as queryModifiers from 'src/utils/queryTransitions'
import defaultQueryConfig from 'src/utils/defaultQueryConfig' import defaultQueryConfig from 'src/utils/defaultQueryConfig'
import {buildQuery} from 'src/utils/influxql' import {buildQuery} from 'src/utils/influxql'
import {getQueryConfig} from 'src/shared/apis' import {getQueryConfigAndStatus} from 'src/shared/apis'
import {IS_STATIC_LEGEND} from 'src/shared/constants' import {IS_STATIC_LEGEND} from 'src/shared/constants'
import {ColorString, ColorNumber} from 'src/types/colors' import {ColorString, ColorNumber} from 'src/types/colors'
import {nextSource} from 'src/dashboards/utils/sources' import {nextSource} from 'src/dashboards/utils/sources'
@ -25,11 +30,21 @@ import {
} from 'src/dashboards/constants' } from 'src/dashboards/constants'
import {OVERLAY_TECHNOLOGY} from 'src/shared/constants/classNames' import {OVERLAY_TECHNOLOGY} from 'src/shared/constants/classNames'
import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from 'src/data_explorer/constants' import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from 'src/data_explorer/constants'
import {AUTO_GROUP_BY} from 'src/shared/constants' import {
AUTO_GROUP_BY,
PREDEFINED_TEMP_VARS,
TEMP_VAR_DASHBOARD_TIME,
} from 'src/shared/constants'
import {getCellTypeColors} from 'src/dashboards/constants/cellEditor' import {getCellTypeColors} from 'src/dashboards/constants/cellEditor'
import {TimeRange, Source, Query} from 'src/types' import {
import {Status} from 'src/types/query' TimeRange,
import {Cell, CellQuery, Legend} from 'src/types/dashboard' Source,
QueryConfig,
Cell,
CellQuery,
Legend,
Status,
} from 'src/types'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
const staticLegend: Legend = { const staticLegend: Legend = {
@ -65,15 +80,15 @@ interface Props {
} }
interface State { interface State {
queriesWorkingDraft: Query[] queriesWorkingDraft: QueryConfig[]
activeQueryIndex: number activeQueryIndex: number
isDisplayOptionsTabActive: boolean isDisplayOptionsTabActive: boolean
isStaticLegend: boolean isStaticLegend: boolean
} }
const createWorkingDraft = (source: string, query: CellQuery): Query => { const createWorkingDraft = (source: string, query: CellQuery): QueryConfig => {
const {queryConfig} = query const {queryConfig} = query
const draft: Query = { const draft: QueryConfig = {
...queryConfig, ...queryConfig,
id: uuid.v4(), id: uuid.v4(),
source, source,
@ -84,8 +99,8 @@ const createWorkingDraft = (source: string, query: CellQuery): Query => {
const createWorkingDrafts = ( const createWorkingDrafts = (
source: string, source: string,
queries: CellQuery[] = [] queries: CellQuery[]
): Query[] => ): QueryConfig[] =>
_.cloneDeep( _.cloneDeep(
queries.map((query: CellQuery) => createWorkingDraft(source, query)) queries.map((query: CellQuery) => createWorkingDraft(source, query))
) )
@ -142,7 +157,9 @@ class CellEditorOverlay extends Component<Props, State> {
} }
public componentDidMount() { public componentDidMount() {
this.overlayRef.focus() if (this.overlayRef) {
this.overlayRef.focus()
}
} }
public render() { public render() {
@ -228,7 +245,10 @@ class CellEditorOverlay extends Component<Props, State> {
this.overlayRef = r this.overlayRef = r
} }
private queryStateReducer = queryModifier => (queryID, ...payload) => { private queryStateReducer = (queryModifier): CellEditorOverlayActionsFunc => (
queryID: string,
...payload: any[]
) => {
const {queriesWorkingDraft} = this.state const {queriesWorkingDraft} = this.state
const query = queriesWorkingDraft.find(q => q.id === queryID) const query = queriesWorkingDraft.find(q => q.id === queryID)
@ -270,7 +290,7 @@ class CellEditorOverlay extends Component<Props, State> {
const {cell, thresholdsListColors, gaugeColors, lineColors} = this.props const {cell, thresholdsListColors, gaugeColors, lineColors} = this.props
const queries = queriesWorkingDraft.map(q => { const queries = queriesWorkingDraft.map(q => {
const timeRange = q.range || {upper: null, lower: ':dashboardTime:'} const timeRange = q.range || {upper: null, lower: TEMP_VAR_DASHBOARD_TIME}
return { return {
queryConfig: q, queryConfig: q,
@ -317,20 +337,91 @@ class CellEditorOverlay extends Component<Props, State> {
private getActiveQuery = () => { private getActiveQuery = () => {
const {queriesWorkingDraft, activeQueryIndex} = this.state const {queriesWorkingDraft, activeQueryIndex} = this.state
const activeQuery = _.get(
queriesWorkingDraft,
activeQueryIndex,
queriesWorkingDraft[0]
)
return _.get(queriesWorkingDraft, activeQueryIndex, queriesWorkingDraft[0]) const queryText = _.get(activeQuery, 'rawText', '')
const userDefinedTempVarsInQuery = this.findUserDefinedTempVarsInQuery(
queryText,
this.props.templates
)
if (!!userDefinedTempVarsInQuery.length) {
activeQuery.isQuerySupportedByExplorer = false
}
return activeQuery
} }
private handleEditRawText = async (url, id, text) => { private findUserDefinedTempVarsInQuery = (
const templates = removeUnselectedTemplateValues(this.props.templates) query: string,
templates: Template[]
// use this as the handler passed into fetchTimeSeries to update a query status ): Template[] => {
try { return templates.filter((temp: Template) => {
const {data} = await getQueryConfig(url, [{query: text, id}], templates) if (!query) {
const config = data.queries.find(q => q.id === id) return false
const nextQueries = this.state.queriesWorkingDraft.map( }
q => (q.id === id ? {...config.queryConfig, source: q.source} : q) const isPredefinedTempVar: boolean = !!PREDEFINED_TEMP_VARS.find(
t => t === temp.tempVar
) )
if (!isPredefinedTempVar) {
return query.includes(temp.tempVar)
}
return false
})
}
// The schema explorer is not built to handle user defined template variables
// in the query in a clear manner. If they are being used, we indicate that in
// the query config in order to disable the fields column down stream because
// at this point the query string is disconnected from the schema explorer.
private handleEditRawText = async (
url: string,
id: string,
text: string
): Promise<void> => {
const userDefinedTempVarsInQuery = this.findUserDefinedTempVarsInQuery(
text,
this.props.templates
)
const isUsingUserDefinedTempVars: boolean = !!userDefinedTempVarsInQuery.length
try {
const selectedTempVars: Template[] = isUsingUserDefinedTempVars
? removeUnselectedTemplateValues(userDefinedTempVarsInQuery)
: []
const {data} = await getQueryConfigAndStatus(
url,
[{query: text, id}],
selectedTempVars
)
const config = data.queries.find(q => q.id === id)
const nextQueries: QueryConfig[] = this.state.queriesWorkingDraft.map(
(q: QueryConfig) => {
if (q.id === id) {
const isQuerySupportedByExplorer = !isUsingUserDefinedTempVars
if (isUsingUserDefinedTempVars) {
return {...q, rawText: text, isQuerySupportedByExplorer}
}
return {
...config.queryConfig,
source: q.source,
isQuerySupportedByExplorer,
}
}
return q
}
)
this.setState({queriesWorkingDraft: nextQueries}) this.setState({queriesWorkingDraft: nextQueries})
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@ -389,17 +480,32 @@ class CellEditorOverlay extends Component<Props, State> {
const {queriesWorkingDraft} = this.state const {queriesWorkingDraft} = this.state
return queriesWorkingDraft.every( return queriesWorkingDraft.every(
(query: Query) => (query: QueryConfig) =>
(!!query.measurement && !!query.database && !!query.fields.length) || (!!query.measurement && !!query.database && !!query.fields.length) ||
!!query.rawText !!query.rawText
) )
} }
private get queryActions() { private get queryActions(): CellEditorOverlayActions {
return { const original = {
editRawTextAsync: this.handleEditRawText, editRawTextAsync: () => Promise.resolve(),
..._.mapValues(queryModifiers, this.queryStateReducer), ...queryModifiers,
} }
const mapped = _.reduce<CellEditorOverlayActions, CellEditorOverlayActions>(
original,
(acc, v, k) => {
acc[k] = this.queryStateReducer(v)
return acc
},
original
)
const result = {
...mapped,
editRawTextAsync: this.handleEditRawText,
}
return result
} }
private get sourceLink(): string { private get sourceLink(): string {

View File

@ -42,16 +42,14 @@ class MeasurementDropdown extends Component {
} }
_getMeasurements = async () => { _getMeasurements = async () => {
const {
source: {
links: {proxy},
},
} = this.context
const { const {
measurement, measurement,
database, database,
onSelectMeasurement, onSelectMeasurement,
onErrorThrown, onErrorThrown,
source: {
links: {proxy},
},
} = this.props } = this.props
try { try {
@ -72,7 +70,12 @@ class MeasurementDropdown extends Component {
const {func, shape, string} = PropTypes const {func, shape, string} = PropTypes
MeasurementDropdown.contextTypes = { MeasurementDropdown.propTypes = {
database: string.isRequired,
measurement: string,
onSelectMeasurement: func.isRequired,
onStartEdit: func.isRequired,
onErrorThrown: func.isRequired,
source: shape({ source: shape({
links: shape({ links: shape({
proxy: string.isRequired, proxy: string.isRequired,
@ -80,12 +83,4 @@ MeasurementDropdown.contextTypes = {
}).isRequired, }).isRequired,
} }
MeasurementDropdown.propTypes = {
database: string.isRequired,
measurement: string,
onSelectMeasurement: func.isRequired,
onStartEdit: func.isRequired,
onErrorThrown: func.isRequired,
}
export default MeasurementDropdown export default MeasurementDropdown

View File

@ -1,20 +1,44 @@
import React from 'react' import React, {SFC} from 'react'
import PropTypes from 'prop-types' import _ from 'lodash'
import {QueryConfig, Source, SourceLinks, TimeRange} from 'src/types'
import {CellEditorOverlayActions} from 'src/types/dashboard'
import EmptyQuery from 'src/shared/components/EmptyQuery' import EmptyQuery from 'src/shared/components/EmptyQuery'
import QueryTabList from 'src/shared/components/QueryTabList' import QueryTabList from 'src/shared/components/QueryTabList'
import QueryTextArea from 'src/dashboards/components/QueryTextArea' import QueryTextArea from 'src/dashboards/components/QueryTextArea'
import SchemaExplorer from 'src/shared/components/SchemaExplorer' import SchemaExplorer from 'src/shared/components/SchemaExplorer'
import {buildQuery} from 'utils/influxql' import {buildQuery} from 'src/utils/influxql'
import {TYPE_QUERY_CONFIG} from 'src/dashboards/constants' import {TYPE_QUERY_CONFIG, TEMPLATE_RANGE} from 'src/dashboards/constants'
const TEMPLATE_RANGE = {upper: null, lower: ':dashboardTime:'} const rawTextBinder = (
const rawTextBinder = (links, id, action) => text => links: SourceLinks,
action(links.queries, id, text) id: string,
const buildText = q => action: (linksQueries: string, id: string, text: string) => void
) => (text: string) => action(links.queries, id, text)
const buildText = (q: QueryConfig): string =>
q.rawText || buildQuery(TYPE_QUERY_CONFIG, q.range || TEMPLATE_RANGE, q) || '' q.rawText || buildQuery(TYPE_QUERY_CONFIG, q.range || TEMPLATE_RANGE, q) || ''
const QueryMaker = ({ interface Template {
tempVar: string
}
interface Props {
source: Source
queries: QueryConfig[]
timeRange: TimeRange
actions: CellEditorOverlayActions
setActiveQueryIndex: (index: number) => void
onDeleteQuery: (index: number) => void
activeQueryIndex: number
activeQuery: QueryConfig
onAddQuery: () => void
templates: Template[]
initialGroupByTime: string
}
const QueryMaker: SFC<Props> = ({
source, source,
actions, actions,
queries, queries,
@ -52,8 +76,12 @@ const QueryMaker = ({
source={source} source={source}
actions={actions} actions={actions}
query={activeQuery} query={activeQuery}
onAddQuery={onAddQuery}
initialGroupByTime={initialGroupByTime} initialGroupByTime={initialGroupByTime}
isQuerySupportedByExplorer={_.get(
activeQuery,
'isQuerySupportedByExplorer',
true
)}
/> />
</div> </div>
) : ( ) : (
@ -62,43 +90,4 @@ const QueryMaker = ({
</div> </div>
) )
const {arrayOf, func, number, shape, string} = PropTypes
QueryMaker.propTypes = {
source: shape({
links: shape({
queries: string.isRequired,
}).isRequired,
}).isRequired,
queries: arrayOf(shape({})).isRequired,
timeRange: shape({
upper: string,
lower: string,
}).isRequired,
actions: shape({
chooseNamespace: func.isRequired,
chooseMeasurement: func.isRequired,
chooseTag: func.isRequired,
groupByTag: func.isRequired,
toggleField: func.isRequired,
groupByTime: func.isRequired,
toggleTagAcceptance: func.isRequired,
fill: func,
applyFuncsToField: func.isRequired,
editRawTextAsync: func.isRequired,
addInitialField: func.isRequired,
}).isRequired,
setActiveQueryIndex: func.isRequired,
onDeleteQuery: func.isRequired,
activeQueryIndex: number,
activeQuery: shape({}),
onAddQuery: func.isRequired,
templates: arrayOf(
shape({
tempVar: string.isRequired,
})
).isRequired,
initialGroupByTime: string.isRequired,
}
export default QueryMaker export default QueryMaker

View File

@ -42,7 +42,7 @@ const TemplateVariableManager = ({
> >
Save Changes Save Changes
</button> </button>
<span className="page-header__dismiss" onClick={onClose(isEdited)} /> <span className="page-header__dismiss" onClick={onClose} />
</div> </div>
</div> </div>
<div className="template-variable-manager--body"> <div className="template-variable-manager--body">
@ -175,16 +175,29 @@ class TemplateVariableManagerWrapper extends Component {
) )
} }
handleDismissManager = () => {
const {onDismissOverlay} = this.props
const {isEdited} = this.state
if (
!isEdited ||
(isEdited && confirm('Do you want to close without saving?')) // eslint-disable-line no-alert
) {
onDismissOverlay()
}
}
render() { render() {
const {rows, isEdited} = this.state const {rows, isEdited} = this.state
return ( return (
<TemplateVariableManager <TemplateVariableManager
{...this.props} {...this.props}
templates={rows}
isEdited={isEdited}
onClose={this.handleDismissManager}
onRunQuerySuccess={this.onRunQuerySuccess} onRunQuerySuccess={this.onRunQuerySuccess}
onSaveTemplatesSuccess={this.onSaveTemplatesSuccess} onSaveTemplatesSuccess={this.onSaveTemplatesSuccess}
onAddVariable={this.onAddVariable} onAddVariable={this.onAddVariable}
templates={rows}
isEdited={isEdited}
onDelete={this.onDeleteTemplateVariable} onDelete={this.onDeleteTemplateVariable}
tempVarAlreadyExists={this.tempVarAlreadyExists} tempVarAlreadyExists={this.tempVarAlreadyExists}
/> />
@ -204,13 +217,7 @@ TemplateVariableManager.propTypes = {
} }
TemplateVariableManagerWrapper.propTypes = { TemplateVariableManagerWrapper.propTypes = {
onClose: func.isRequired,
onEditTemplateVariables: func.isRequired, onEditTemplateVariables: func.isRequired,
source: shape({
links: shape({
proxy: string,
}),
}).isRequired,
templates: arrayOf( templates: arrayOf(
shape({ shape({
type: string.isRequired, type: string.isRequired,
@ -229,6 +236,7 @@ TemplateVariableManagerWrapper.propTypes = {
}) })
), ),
onRunQueryFailure: func.isRequired, onRunQueryFailure: func.isRequired,
onDismissOverlay: func,
} }
export default TemplateVariableManagerWrapper export default TemplateVariableManagerWrapper

View File

@ -46,6 +46,7 @@ const TemplateVariableRow = ({
onSubmit, onSubmit,
onErrorThrown, onErrorThrown,
onDeleteTempVar, onDeleteTempVar,
source,
}) => ( }) => (
<form <form
className={classnames('template-variable-manager--table-row', { className={classnames('template-variable-manager--table-row', {
@ -78,6 +79,7 @@ const TemplateVariableRow = ({
</div> </div>
<div className="tvm--col-3"> <div className="tvm--col-3">
<TemplateQueryBuilder <TemplateQueryBuilder
source={source}
onSelectDatabase={onSelectDatabase} onSelectDatabase={onSelectDatabase}
selectedType={selectedType} selectedType={selectedType}
selectedDatabase={selectedDatabase} selectedDatabase={selectedDatabase}

View File

@ -14,6 +14,7 @@ const TemplateQueryBuilder = ({
onSelectTagKey, onSelectTagKey,
onStartEdit, onStartEdit,
onErrorThrown, onErrorThrown,
source,
}) => { }) => {
switch (selectedType) { switch (selectedType) {
case 'csv': case 'csv':
@ -25,6 +26,7 @@ const TemplateQueryBuilder = ({
<div className="tvm-query-builder"> <div className="tvm-query-builder">
<span className="tvm-query-builder--text">SHOW MEASUREMENTS ON</span> <span className="tvm-query-builder--text">SHOW MEASUREMENTS ON</span>
<DatabaseDropdown <DatabaseDropdown
source={source}
onSelectDatabase={onSelectDatabase} onSelectDatabase={onSelectDatabase}
database={selectedDatabase} database={selectedDatabase}
onStartEdit={onStartEdit} onStartEdit={onStartEdit}
@ -40,6 +42,7 @@ const TemplateQueryBuilder = ({
SHOW {selectedType === 'fieldKeys' ? 'FIELD' : 'TAG'} KEYS ON SHOW {selectedType === 'fieldKeys' ? 'FIELD' : 'TAG'} KEYS ON
</span> </span>
<DatabaseDropdown <DatabaseDropdown
source={source}
onSelectDatabase={onSelectDatabase} onSelectDatabase={onSelectDatabase}
database={selectedDatabase} database={selectedDatabase}
onStartEdit={onStartEdit} onStartEdit={onStartEdit}
@ -48,6 +51,7 @@ const TemplateQueryBuilder = ({
<span className="tvm-query-builder--text">FROM</span> <span className="tvm-query-builder--text">FROM</span>
{selectedDatabase ? ( {selectedDatabase ? (
<MeasurementDropdown <MeasurementDropdown
source={source}
database={selectedDatabase} database={selectedDatabase}
measurement={selectedMeasurement} measurement={selectedMeasurement}
onSelectMeasurement={onSelectMeasurement} onSelectMeasurement={onSelectMeasurement}
@ -64,6 +68,7 @@ const TemplateQueryBuilder = ({
<div className="tvm-query-builder"> <div className="tvm-query-builder">
<span className="tvm-query-builder--text">SHOW TAG VALUES ON</span> <span className="tvm-query-builder--text">SHOW TAG VALUES ON</span>
<DatabaseDropdown <DatabaseDropdown
source={source}
onSelectDatabase={onSelectDatabase} onSelectDatabase={onSelectDatabase}
database={selectedDatabase} database={selectedDatabase}
onStartEdit={onStartEdit} onStartEdit={onStartEdit}
@ -72,6 +77,7 @@ const TemplateQueryBuilder = ({
<span className="tvm-query-builder--text">FROM</span> <span className="tvm-query-builder--text">FROM</span>
{selectedDatabase ? ( {selectedDatabase ? (
<MeasurementDropdown <MeasurementDropdown
source={source}
database={selectedDatabase} database={selectedDatabase}
measurement={selectedMeasurement} measurement={selectedMeasurement}
onSelectMeasurement={onSelectMeasurement} onSelectMeasurement={onSelectMeasurement}
@ -105,7 +111,7 @@ const TemplateQueryBuilder = ({
} }
} }
const {func, string} = PropTypes const {func, shape, string} = PropTypes
TemplateQueryBuilder.propTypes = { TemplateQueryBuilder.propTypes = {
selectedType: string.isRequired, selectedType: string.isRequired,
@ -117,6 +123,11 @@ TemplateQueryBuilder.propTypes = {
selectedDatabase: string, selectedDatabase: string,
selectedTagKey: string, selectedTagKey: string,
onErrorThrown: func.isRequired, onErrorThrown: func.isRequired,
source: shape({
links: shape({
proxy: string.isRequired,
}).isRequired,
}).isRequired,
} }
export default TemplateQueryBuilder export default TemplateQueryBuilder

View File

@ -3,6 +3,7 @@ import {
DEFAULT_FIX_FIRST_COLUMN, DEFAULT_FIX_FIRST_COLUMN,
} from 'src/shared/constants/tableGraph' } from 'src/shared/constants/tableGraph'
import {CELL_TYPE_LINE} from 'src/dashboards/graphics/graph' import {CELL_TYPE_LINE} from 'src/dashboards/graphics/graph'
import {TEMP_VAR_DASHBOARD_TIME} from 'src/shared/constants'
export const UNTITLED_CELL_LINE = 'Untitled Line Graph' export const UNTITLED_CELL_LINE = 'Untitled Line Graph'
export const UNTITLED_CELL_STACKED = 'Untitled Stacked Gracph' export const UNTITLED_CELL_STACKED = 'Untitled Stacked Gracph'
@ -151,3 +152,4 @@ export const TYPE_QUERY_CONFIG = 'queryConfig'
export const TYPE_SHIFTED = 'shifted queryConfig' export const TYPE_SHIFTED = 'shifted queryConfig'
export const TYPE_IFQL = 'ifql' export const TYPE_IFQL = 'ifql'
export const DASHBOARD_NAME_MAX_LENGTH = 50 export const DASHBOARD_NAME_MAX_LENGTH = 50
export const TEMPLATE_RANGE = {upper: null, lower: TEMP_VAR_DASHBOARD_TIME}

View File

@ -8,7 +8,6 @@ import _ from 'lodash'
import {isUserAuthorized, EDITOR_ROLE} from 'src/auth/Authorized' import {isUserAuthorized, EDITOR_ROLE} from 'src/auth/Authorized'
import OverlayTechnologies from 'shared/components/OverlayTechnologies'
import CellEditorOverlay from 'src/dashboards/components/CellEditorOverlay' import CellEditorOverlay from 'src/dashboards/components/CellEditorOverlay'
import DashboardHeader from 'src/dashboards/components/DashboardHeader' import DashboardHeader from 'src/dashboards/components/DashboardHeader'
import Dashboard from 'src/dashboards/components/Dashboard' import Dashboard from 'src/dashboards/components/Dashboard'
@ -28,6 +27,7 @@ import {
showCellEditorOverlay, showCellEditorOverlay,
hideCellEditorOverlay, hideCellEditorOverlay,
} from 'src/dashboards/actions/cellEditorOverlay' } from 'src/dashboards/actions/cellEditorOverlay'
import {showOverlay} from 'src/shared/actions/overlayTechnology'
import {dismissEditingAnnotation} from 'src/shared/actions/annotations' import {dismissEditingAnnotation} from 'src/shared/actions/annotations'
@ -36,10 +36,16 @@ import {
templateControlBarVisibilityToggled as templateControlBarVisibilityToggledAction, templateControlBarVisibilityToggled as templateControlBarVisibilityToggledAction,
} from 'shared/actions/app' } from 'shared/actions/app'
import {presentationButtonDispatcher} from 'shared/dispatchers' import {presentationButtonDispatcher} from 'shared/dispatchers'
import {interval, DASHBOARD_LAYOUT_ROW_HEIGHT} from 'shared/constants' import {
interval,
DASHBOARD_LAYOUT_ROW_HEIGHT,
TEMP_VAR_DASHBOARD_TIME,
TEMP_VAR_UPPER_DASHBOARD_TIME,
} from 'shared/constants'
import {notifyDashboardNotFound} from 'shared/copy/notifications' import {notifyDashboardNotFound} from 'shared/copy/notifications'
import {colorsStringSchema, colorsNumberSchema} from 'shared/schemas' import {colorsStringSchema, colorsNumberSchema} from 'shared/schemas'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
import {OverlayContext} from 'src/shared/components/OverlayTechnology'
const FORMAT_INFLUXQL = 'influxql' const FORMAT_INFLUXQL = 'influxql'
const defaultTimeRange = { const defaultTimeRange = {
@ -57,7 +63,6 @@ class DashboardPage extends Component {
this.state = { this.state = {
isEditMode: false, isEditMode: false,
selectedCell: null, selectedCell: null,
isTemplating: false,
zoomedTimeRange: {zoomedLower: null, zoomedUpper: null}, zoomedTimeRange: {zoomedLower: null, zoomedUpper: null},
scrollTop: 0, scrollTop: 0,
windowHeight: window.innerHeight, windowHeight: window.innerHeight,
@ -149,16 +154,28 @@ class DashboardPage extends Component {
} }
handleOpenTemplateManager = () => { handleOpenTemplateManager = () => {
this.setState({isTemplating: true}) const {handleShowOverlay, dashboard, source} = this.props
} const options = {
dismissOnClickOutside: false,
handleCloseTemplateManager = isEdited => () => { dismissOnEscape: false,
if (
!isEdited ||
(isEdited && confirm('Do you want to close without saving?')) // eslint-disable-line no-alert
) {
this.setState({isTemplating: false})
} }
handleShowOverlay(
<OverlayContext.Consumer>
{({onDismissOverlay}) => {
return (
<TemplateVariableManager
source={source}
templates={dashboard.templates}
onDismissOverlay={onDismissOverlay}
onRunQueryFailure={this.handleRunQueryFailure}
onEditTemplateVariables={this.handleEditTemplateVariables}
/>
)
}}
</OverlayContext.Consumer>,
options
)
} }
handleSaveEditedCell = newCell => { handleSaveEditedCell = newCell => {
@ -321,7 +338,7 @@ class DashboardPage extends Component {
const dashboardTime = { const dashboardTime = {
id: 'dashtime', id: 'dashtime',
tempVar: ':dashboardTime:', tempVar: TEMP_VAR_DASHBOARD_TIME,
type: lowerType, type: lowerType,
values: [ values: [
{ {
@ -334,7 +351,7 @@ class DashboardPage extends Component {
const upperDashboardTime = { const upperDashboardTime = {
id: 'upperdashtime', id: 'upperdashtime',
tempVar: ':upperDashboardTime:', tempVar: TEMP_VAR_UPPER_DASHBOARD_TIME,
type: upperType, type: upperType,
values: [ values: [
{ {
@ -357,7 +374,7 @@ class DashboardPage extends Component {
templatesIncludingDashTime = [] templatesIncludingDashTime = []
} }
const {isEditMode, isTemplating} = this.state const {isEditMode} = this.state
const names = dashboards.map(d => ({ const names = dashboards.map(d => ({
name: d.name, name: d.name,
@ -365,17 +382,6 @@ class DashboardPage extends Component {
})) }))
return ( return (
<div className="page dashboard-page"> <div className="page dashboard-page">
{isTemplating ? (
<OverlayTechnologies>
<TemplateVariableManager
source={source}
templates={dashboard.templates}
onClose={this.handleCloseTemplateManager}
onRunQueryFailure={this.handleRunQueryFailure}
onEditTemplateVariables={this.handleEditTemplateVariables}
/>
</OverlayTechnologies>
) : null}
{selectedCell ? ( {selectedCell ? (
<CellEditorOverlay <CellEditorOverlay
source={source} source={source}
@ -534,6 +540,7 @@ DashboardPage.propTypes = {
thresholdsListColors: colorsNumberSchema.isRequired, thresholdsListColors: colorsNumberSchema.isRequired,
gaugeColors: colorsNumberSchema.isRequired, gaugeColors: colorsNumberSchema.isRequired,
lineColors: colorsStringSchema.isRequired, lineColors: colorsStringSchema.isRequired,
handleShowOverlay: func.isRequired,
} }
const mapStateToProps = (state, {params: {dashboardID}}) => { const mapStateToProps = (state, {params: {dashboardID}}) => {
@ -613,6 +620,7 @@ const mapDispatchToProps = dispatch => ({
dismissEditingAnnotation, dismissEditingAnnotation,
dispatch dispatch
), ),
handleShowOverlay: bindActionCreators(showOverlay, dispatch),
}) })
export default connect(mapStateToProps, mapDispatchToProps)( export default connect(mapStateToProps, mapDispatchToProps)(

View File

@ -1,6 +1,9 @@
import {Query} from 'src/types' import {QueryConfig} from 'src/types'
export const nextSource = (prevQuery: Query, nextQuery: Query): string => { export const nextSource = (
prevQuery: QueryConfig,
nextQuery: QueryConfig
): string => {
if (nextQuery.source) { if (nextQuery.source) {
return nextQuery.source return nextQuery.source
} }

View File

@ -1,6 +1,6 @@
import uuid from 'uuid' import uuid from 'uuid'
import {getQueryConfig} from 'shared/apis' import {getQueryConfigAndStatus} from 'shared/apis'
import {errorThrown} from 'shared/actions/errors' import {errorThrown} from 'shared/actions/errors'
@ -158,7 +158,7 @@ export const timeShift = (queryID, shift) => ({
// Async actions // Async actions
export const editRawTextAsync = (url, id, text) => async dispatch => { export const editRawTextAsync = (url, id, text) => async dispatch => {
try { try {
const {data} = await getQueryConfig(url, [{query: text, id}]) const {data} = await getQueryConfigAndStatus(url, [{query: text, id}])
const config = data.queries.find(q => q.id === id) const config = data.queries.find(q => q.id === id)
dispatch(updateQueryConfig(config.queryConfig)) dispatch(updateQueryConfig(config.queryConfig))
} catch (error) { } catch (error) {

View File

@ -1,14 +1,45 @@
import React, {Component} from 'react' import React, {PureComponent, MouseEvent} from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames' import classnames from 'classnames'
import _ from 'lodash' import _ from 'lodash'
import FunctionSelector from 'shared/components/FunctionSelector' import FunctionSelector from 'src/shared/components/FunctionSelector'
import {firstFieldName} from 'shared/reducers/helpers/fields' import {firstFieldName} from 'src/shared/reducers/helpers/fields'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
interface Field {
type: string
value: string
}
interface FuncArg {
value: string
type: string
}
interface FieldFunc extends Field {
args: FuncArg[]
}
interface ApplyFuncsToFieldArgs {
field: Field
funcs: FuncArg[]
}
interface Props {
fieldFuncs: FieldFunc[]
isSelected: boolean
onToggleField: (field: Field) => void
onApplyFuncsToField: (args: ApplyFuncsToFieldArgs) => void
isKapacitorRule: boolean
funcs: string[]
isDisabled: boolean
}
interface State {
isOpen: boolean
}
@ErrorHandling @ErrorHandling
class FieldListItem extends Component { class FieldListItem extends PureComponent<Props, State> {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
@ -16,55 +47,10 @@ class FieldListItem extends Component {
} }
} }
toggleFunctionsMenu = e => { public render() {
if (e) { const {isKapacitorRule, isSelected, funcs, isDisabled} = this.props
e.stopPropagation()
}
this.setState({isOpen: !this.state.isOpen})
}
close = () => {
this.setState({isOpen: false})
}
handleToggleField = () => {
const {onToggleField} = this.props
const value = this._getFieldName()
onToggleField({value, type: 'field'})
this.close()
}
handleApplyFunctions = selectedFuncs => {
const {onApplyFuncsToField} = this.props
const fieldName = this._getFieldName()
const field = {value: fieldName, type: 'field'}
onApplyFuncsToField({
field,
funcs: selectedFuncs.map(this._makeFunc),
})
this.close()
}
_makeFunc = value => ({
value,
type: 'func',
})
_getFieldName = () => {
const {fieldFuncs} = this.props
const fieldFunc = _.head(fieldFuncs)
return _.get(fieldFunc, 'type') === 'field'
? _.get(fieldFunc, 'value')
: firstFieldName(_.get(fieldFunc, 'args'))
}
render() {
const {isKapacitorRule, isSelected, funcs} = this.props
const {isOpen} = this.state const {isOpen} = this.state
const fieldName = this._getFieldName() const fieldName = this.getFieldName()
let fieldFuncsLabel let fieldFuncsLabel
const num = funcs.length const num = funcs.length
@ -84,6 +70,7 @@ class FieldListItem extends Component {
<div <div
className={classnames('query-builder--list-item', { className={classnames('query-builder--list-item', {
active: isSelected, active: isSelected,
disabled: isDisabled,
})} })}
onClick={this.handleToggleField} onClick={this.handleToggleField}
data-test={`query-builder-list-item-field-${fieldName}`} data-test={`query-builder-list-item-field-${fieldName}`}
@ -98,6 +85,7 @@ class FieldListItem extends Component {
active: isOpen, active: isOpen,
'btn-default': !num, 'btn-default': !num,
'btn-primary': num, 'btn-primary': num,
disabled: isDisabled,
})} })}
onClick={this.toggleFunctionsMenu} onClick={this.toggleFunctionsMenu}
data-test={`query-builder-list-item-function-${fieldName}`} data-test={`query-builder-list-item-function-${fieldName}`}
@ -116,28 +104,54 @@ class FieldListItem extends Component {
</div> </div>
) )
} }
}
const {string, shape, func, arrayOf, bool} = PropTypes private toggleFunctionsMenu = (e: MouseEvent<HTMLElement>) => {
e.stopPropagation()
const {isDisabled} = this.props
if (isDisabled) {
return
}
FieldListItem.propTypes = { this.setState({isOpen: !this.state.isOpen})
fieldFuncs: arrayOf( }
shape({
type: string.isRequired, private close = (): void => {
value: string.isRequired, this.setState({isOpen: false})
alias: string, }
args: arrayOf(
shape({ private handleToggleField = (): void => {
type: string.isRequired, const {onToggleField} = this.props
value: string.isRequired, const value = this.getFieldName()
})
), onToggleField({value, type: 'field'})
this.close()
}
private handleApplyFunctions = (selectedFuncs: string[]) => {
const {onApplyFuncsToField} = this.props
const fieldName = this.getFieldName()
const field: Field = {value: fieldName, type: 'field'}
onApplyFuncsToField({
field,
funcs: selectedFuncs.map(val => this.makeFuncArg(val)),
}) })
).isRequired, this.close()
isSelected: bool.isRequired, }
onToggleField: func.isRequired,
onApplyFuncsToField: func.isRequired, private makeFuncArg = (value: string): FuncArg => ({
isKapacitorRule: bool.isRequired, value,
funcs: arrayOf(string.isRequired).isRequired, type: 'func',
})
private getFieldName = (): string => {
const {fieldFuncs} = this.props
const fieldFunc = _.head(fieldFuncs)
return _.get(fieldFunc, 'type') === 'field'
? _.get(fieldFunc, 'value')
: firstFieldName(_.get(fieldFunc, 'args'))
}
} }
export default FieldListItem export default FieldListItem

View File

@ -1,24 +1,40 @@
import React from 'react' import React, {SFC} from 'react'
import PropTypes from 'prop-types'
import {withRouter} from 'react-router' import {withRouter} from 'react-router'
import {Location} from 'history'
import groupByTimeOptions from 'src/data_explorer/data/groupByTimes' import groupByTimeOptions from 'src/data_explorer/data/groupByTimes'
import Dropdown from 'shared/components/Dropdown' import Dropdown from 'src/shared/components/Dropdown'
import {AUTO_GROUP_BY} from 'shared/constants' import {AUTO_GROUP_BY} from 'src/shared/constants'
import {GroupBy} from 'src/types'
const isInRuleBuilder = pathname => pathname.includes('alert-rules') interface GroupByTimeOption {
defaultTimeBound: string
seconds: number
menuOption: string
}
const getOptions = pathname => interface Props {
location?: Location
selected: string
onChooseGroupByTime: (groupBy: GroupBy) => void
isDisabled: boolean
}
const isInRuleBuilder = (pathname: string): boolean =>
pathname.includes('alert-rules')
const getOptions = (pathname: string): GroupByTimeOption[] =>
isInRuleBuilder(pathname) isInRuleBuilder(pathname)
? groupByTimeOptions.filter(({menuOption}) => menuOption !== AUTO_GROUP_BY) ? groupByTimeOptions.filter(({menuOption}) => menuOption !== AUTO_GROUP_BY)
: groupByTimeOptions : groupByTimeOptions
const GroupByTimeDropdown = ({ const GroupByTimeDropdown: SFC<Props> = ({
selected, selected,
onChooseGroupByTime, onChooseGroupByTime,
location: {pathname}, location: {pathname},
isDisabled,
}) => ( }) => (
<div className="group-by-time"> <div className="group-by-time">
<label className="group-by-time--label">Group by:</label> <label className="group-by-time--label">Group by:</label>
@ -32,18 +48,9 @@ const GroupByTimeDropdown = ({
}))} }))}
onChoose={onChooseGroupByTime} onChoose={onChooseGroupByTime}
selected={selected || 'Time'} selected={selected || 'Time'}
disabled={isDisabled}
/> />
</div> </div>
) )
const {func, string, shape} = PropTypes
GroupByTimeDropdown.propTypes = {
location: shape({
pathname: string.isRequired,
}).isRequired,
selected: string,
onChooseGroupByTime: func.isRequired,
}
export default withRouter(GroupByTimeDropdown) export default withRouter(GroupByTimeDropdown)

View File

@ -2,7 +2,7 @@ import React, {PureComponent} from 'react'
import QueryEditor from './QueryEditor' import QueryEditor from './QueryEditor'
import SchemaExplorer from 'src/shared/components/SchemaExplorer' import SchemaExplorer from 'src/shared/components/SchemaExplorer'
import {Source, Query} from 'src/types' import {Source, QueryConfig} from 'src/types'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
const rawTextBinder = (links, id, action) => text => const rawTextBinder = (links, id, action) => text =>
@ -12,7 +12,7 @@ interface Props {
source: Source source: Source
rawText: string rawText: string
actions: any actions: any
activeQuery: Query activeQuery: QueryConfig
initialGroupByTime: string initialGroupByTime: string
} }

View File

@ -133,7 +133,7 @@ class WriteDataForm extends Component {
handleFileInputRef = el => (this.fileInput = el) handleFileInputRef = el => (this.fileInput = el)
render() { render() {
const {onClose, errorThrown} = this.props const {onClose, errorThrown, source} = this.props
const {dragClass} = this.state const {dragClass} = this.state
return ( return (
@ -148,6 +148,7 @@ class WriteDataForm extends Component {
<div className="write-data-form"> <div className="write-data-form">
<WriteDataHeader <WriteDataHeader
{...this.state} {...this.state}
source={source}
handleSelectDatabase={this.handleSelectDatabase} handleSelectDatabase={this.handleSelectDatabase}
errorThrown={errorThrown} errorThrown={errorThrown}
toggleWriteView={this.toggleWriteView} toggleWriteView={this.toggleWriteView}

View File

@ -9,11 +9,13 @@ const WriteDataHeader = ({
toggleWriteView, toggleWriteView,
isManual, isManual,
onClose, onClose,
source,
}) => ( }) => (
<div className="write-data-form--header"> <div className="write-data-form--header">
<div className="page-header__left"> <div className="page-header__left">
<h1 className="page-header__title">Write Data To</h1> <h1 className="page-header__title">Write Data To</h1>
<DatabaseDropdown <DatabaseDropdown
source={source}
onSelectDatabase={handleSelectDatabase} onSelectDatabase={handleSelectDatabase}
database={selectedDatabase} database={selectedDatabase}
onErrorThrown={errorThrown} onErrorThrown={errorThrown}
@ -40,7 +42,7 @@ const WriteDataHeader = ({
</div> </div>
) )
const {func, string, bool} = PropTypes const {func, shape, string, bool} = PropTypes
WriteDataHeader.propTypes = { WriteDataHeader.propTypes = {
handleSelectDatabase: func.isRequired, handleSelectDatabase: func.isRequired,
@ -49,6 +51,11 @@ WriteDataHeader.propTypes = {
errorThrown: func.isRequired, errorThrown: func.isRequired,
onClose: func.isRequired, onClose: func.isRequired,
isManual: bool, isManual: bool,
source: shape({
links: shape({
proxy: string.isRequired,
}).isRequired,
}).isRequired,
} }
export default WriteDataHeader export default WriteDataHeader

View File

@ -26,12 +26,12 @@ import {writeLineProtocolAsync} from 'src/data_explorer/actions/view/write'
import {buildRawText} from 'src/utils/influxql' import {buildRawText} from 'src/utils/influxql'
import defaultQueryConfig from 'src/utils/defaultQueryConfig' import defaultQueryConfig from 'src/utils/defaultQueryConfig'
import {Source, Query, TimeRange} from 'src/types' import {Source, QueryConfig, TimeRange} from 'src/types'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props { interface Props {
source: Source source: Source
queryConfigs: Query[] queryConfigs: QueryConfig[]
queryConfigActions: any // TODO: actually type these queryConfigActions: any // TODO: actually type these
autoRefresh: number autoRefresh: number
handleChooseAutoRefresh: () => void handleChooseAutoRefresh: () => void
@ -169,7 +169,7 @@ export class DataExplorer extends PureComponent<Props, State> {
return _.get(this.props.queryConfigs, ['0', 'database'], null) return _.get(this.props.queryConfigs, ['0', 'database'], null)
} }
private get activeQuery(): Query { private get activeQuery(): QueryConfig {
const {queryConfigs} = this.props const {queryConfigs} = this.props
if (queryConfigs.length === 0) { if (queryConfigs.length === 0) {

View File

@ -1,5 +1,7 @@
import {TEMP_VAR_INTERVAL} from 'src/shared/constants'
const groupByTimes = [ const groupByTimes = [
{defaultTimeBound: ':interval:', seconds: 604800, menuOption: 'auto'}, {defaultTimeBound: TEMP_VAR_INTERVAL, seconds: 604800, menuOption: 'auto'},
{defaultTimeBound: 'now() - 5m', seconds: 10, menuOption: '10s'}, {defaultTimeBound: 'now() - 5m', seconds: 10, menuOption: '10s'},
{defaultTimeBound: 'now() - 15m', seconds: 60, menuOption: '1m'}, {defaultTimeBound: 'now() - 15m', seconds: 60, menuOption: '1m'},
{defaultTimeBound: 'now() - 1h', seconds: 300, menuOption: '5m'}, {defaultTimeBound: 'now() - 1h', seconds: 300, menuOption: '5m'},

View File

@ -0,0 +1,21 @@
import {ReactElement} from 'react'
type OverlayNodeType = ReactElement<any>
interface Options {
dismissOnClickOutside?: boolean
dismissOnEscape?: boolean
transitionTime?: number
}
export const showOverlay = (
OverlayNode: OverlayNodeType,
options: Options
) => ({
type: 'SHOW_OVERLAY',
payload: {OverlayNode, options},
})
export const dismissOverlay = () => ({
type: 'DISMISS_OVERLAY',
})

View File

@ -231,7 +231,7 @@ export function kapacitorProxy(kapacitor, method, path, body) {
}) })
} }
export const getQueryConfig = (url, queries, tempVars) => export const getQueryConfigAndStatus = (url, queries, tempVars) =>
AJAX({ AJAX({
url, url,
method: 'POST', method: 'POST',

View File

@ -1,7 +1,7 @@
import React, {Component, ComponentClass} from 'react' import React, {Component, ComponentClass} from 'react'
import _ from 'lodash' import _ from 'lodash'
import {getQueryConfig} from 'src/shared/apis' import {getQueryConfigAndStatus} from 'src/shared/apis'
import {fetchTimeSeries} from 'src/shared/apis/query' import {fetchTimeSeries} from 'src/shared/apis/query'
import {DEFAULT_TIME_SERIES} from 'src/shared/constants/series' import {DEFAULT_TIME_SERIES} from 'src/shared/constants/series'
import {TimeSeriesServerResponse, TimeSeriesResponse} from 'src/types/series' import {TimeSeriesServerResponse, TimeSeriesResponse} from 'src/types/series'
@ -265,7 +265,11 @@ const AutoRefresh = (
const host = _.isArray(q.host) ? q.host[0] : q.host const host = _.isArray(q.host) ? q.host[0] : q.host
const url = host.replace('proxy', 'queries') const url = host.replace('proxy', 'queries')
const text = q.text const text = q.text
const {data} = await getQueryConfig(url, [{query: text}], templates) const {data} = await getQueryConfigAndStatus(
url,
[{query: text}],
templates
)
return data.queries[0].queryAST return data.queries[0].queryAST
}) })
) )

View File

@ -2,8 +2,10 @@ import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import _ from 'lodash' import _ from 'lodash'
import {TEMP_VAR_DASHBOARD_TIME} from 'src/shared/constants'
const CustomTimeIndicator = ({queries}) => { const CustomTimeIndicator = ({queries}) => {
const q = queries.find(({query}) => !query.includes(':dashboardTime:')) const q = queries.find(({query}) => !query.includes(TEMP_VAR_DASHBOARD_TIME))
const customLower = _.get(q, ['queryConfig', 'range', 'lower'], null) const customLower = _.get(q, ['queryConfig', 'range', 'lower'], null)
const customUpper = _.get(q, ['queryConfig', 'range', 'upper'], null) const customUpper = _.get(q, ['queryConfig', 'range', 'upper'], null)

View File

@ -39,8 +39,7 @@ class DatabaseDropdown extends Component {
} }
_getDatabases = async () => { _getDatabases = async () => {
const {source} = this.context const {source, database, onSelectDatabase, onErrorThrown} = this.props
const {database, onSelectDatabase, onErrorThrown} = this.props
const proxy = source.links.proxy const proxy = source.links.proxy
try { try {
const {data} = await showDatabases(proxy) const {data} = await showDatabases(proxy)
@ -65,7 +64,11 @@ class DatabaseDropdown extends Component {
const {func, shape, string} = PropTypes const {func, shape, string} = PropTypes
DatabaseDropdown.contextTypes = { DatabaseDropdown.propTypes = {
database: string,
onSelectDatabase: func.isRequired,
onStartEdit: func,
onErrorThrown: func.isRequired,
source: shape({ source: shape({
links: shape({ links: shape({
proxy: string.isRequired, proxy: string.isRequired,
@ -73,11 +76,4 @@ DatabaseDropdown.contextTypes = {
}).isRequired, }).isRequired,
} }
DatabaseDropdown.propTypes = {
database: string,
onSelectDatabase: func.isRequired,
onStartEdit: func,
onErrorThrown: func.isRequired,
}
export default DatabaseDropdown export default DatabaseDropdown

View File

@ -3,7 +3,7 @@ import React, {PureComponent} from 'react'
import _ from 'lodash' import _ from 'lodash'
import {Query, Source} from 'src/types' import {QueryConfig, Source} from 'src/types'
import {Namespace} from 'src/types/query' import {Namespace} from 'src/types/query'
import {showDatabases, showRetentionPolicies} from 'src/shared/apis/metaQuery' import {showDatabases, showRetentionPolicies} from 'src/shared/apis/metaQuery'
@ -15,7 +15,7 @@ import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
interface DatabaseListProps { interface DatabaseListProps {
query: Query query: QueryConfig
querySource: Source querySource: Source
onChooseNamespace: (namespace: Namespace) => void onChooseNamespace: (namespace: Namespace) => void
source: Source source: Source
@ -102,7 +102,7 @@ class DatabaseList extends PureComponent<DatabaseListProps, DatabaseListState> {
return () => this.props.onChooseNamespace(namespace) return () => this.props.onChooseNamespace(namespace)
} }
public isActive(query: Query, {database, retentionPolicy}: Namespace) { public isActive(query: QueryConfig, {database, retentionPolicy}: Namespace) {
return ( return (
database === query.database && retentionPolicy === query.retentionPolicy database === query.database && retentionPolicy === query.retentionPolicy
) )

View File

@ -1,23 +1,83 @@
import React, {Component} from 'react' import React, {PureComponent} from 'react'
import PropTypes from 'prop-types'
import _ from 'lodash' import _ from 'lodash'
import QueryOptions from 'shared/components/QueryOptions' import {QueryConfig, GroupBy, Source, TimeShift} from 'src/types'
import FieldListItem from 'src/data_explorer/components/FieldListItem'
import FancyScrollbar from 'shared/components/FancyScrollbar'
import {showFieldKeys} from 'shared/apis/metaQuery' import QueryOptions from 'src/shared/components/QueryOptions'
import showFieldKeysParser from 'shared/parsing/showFieldKeys' import FieldListItem from 'src/data_explorer/components/FieldListItem'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import {showFieldKeys} from 'src/shared/apis/metaQuery'
import showFieldKeysParser from 'src/shared/parsing/showFieldKeys'
import { import {
functionNames, functionNames,
numFunctions, numFunctions,
getFieldsWithName, getFieldsWithName,
getFuncsByFieldName, getFuncsByFieldName,
} from 'shared/reducers/helpers/fields' } from 'src/shared/reducers/helpers/fields'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
interface GroupByOption extends GroupBy {
menuOption: string
}
interface TimeShiftOption extends TimeShift {
text: string
}
interface Links {
proxy: string
}
interface Field {
type: string
value: string
}
interface FieldFunc extends Field {
args: FuncArg[]
}
interface FuncArg {
value: string
type: string
}
interface ApplyFuncsToFieldArgs {
field: Field
funcs: FuncArg[]
}
interface Props {
query: QueryConfig
onTimeShift: (shift: TimeShiftOption) => void
onToggleField: (field: Field) => void
onGroupByTime: (groupByOption: string) => void
onFill: (fill: string) => void
applyFuncsToField: (field: ApplyFuncsToFieldArgs, groupBy: GroupBy) => void
isKapacitorRule: boolean
querySource: {
links: Links
}
removeFuncs: (fields: Field[]) => void
addInitialField: (field: Field, groupBy: GroupBy) => void
initialGroupByTime: string | null
isQuerySupportedByExplorer: boolean
}
interface State {
fields: Field[]
}
interface Context {
source: Source
}
@ErrorHandling @ErrorHandling
class FieldList extends Component { class FieldList extends PureComponent<Props, State> {
public static context: Context
public static defaultProps: Partial<Props> = {
isKapacitorRule: false,
initialGroupByTime: null,
}
constructor(props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
@ -25,16 +85,16 @@ class FieldList extends Component {
} }
} }
componentDidMount() { public componentDidMount() {
const {database, measurement} = this.props.query const {database, measurement} = this.props.query
if (!database || !measurement) { if (!database || !measurement) {
return return
} }
this._getFields() this.getFields()
} }
componentDidUpdate(prevProps) { public componentDidUpdate(prevProps) {
const {querySource, query} = this.props const {querySource, query} = this.props
const {database, measurement, retentionPolicy} = query const {database, measurement, retentionPolicy} = query
const { const {
@ -55,26 +115,100 @@ class FieldList extends Component {
return return
} }
this._getFields() this.getFields()
} }
handleGroupByTime = groupBy => { public render() {
const {
query: {database, measurement, fields = [], groupBy, fill, shifts},
isQuerySupportedByExplorer,
isKapacitorRule,
} = this.props
const hasAggregates = numFunctions(fields) > 0
const noDBorMeas = !database || !measurement
return (
<div className="query-builder--column">
<div className="query-builder--heading">
<span>Fields</span>
{hasAggregates ? (
<QueryOptions
fill={fill}
shift={_.first(shifts)}
groupBy={groupBy}
onFill={this.handleFill}
isKapacitorRule={isKapacitorRule}
onTimeShift={this.handleTimeShift}
onGroupByTime={this.handleGroupByTime}
isDisabled={!isQuerySupportedByExplorer}
/>
) : null}
</div>
{noDBorMeas ? (
<div className="query-builder--list-empty">
<span>
No <strong>Measurement</strong> selected
</span>
</div>
) : (
<div className="query-builder--list">
<FancyScrollbar>
{this.state.fields.map((fieldFunc, i) => {
const selectedFields = getFieldsWithName(
fieldFunc.value,
fields
)
const funcs: FieldFunc[] = getFuncsByFieldName(
fieldFunc.value,
fields
)
const fieldFuncs = selectedFields.length
? selectedFields
: [fieldFunc]
return (
<FieldListItem
key={i}
onToggleField={this.handleToggleField}
onApplyFuncsToField={this.handleApplyFuncs}
isSelected={!!selectedFields.length}
fieldFuncs={fieldFuncs}
funcs={functionNames(funcs)}
isKapacitorRule={isKapacitorRule}
isDisabled={!isQuerySupportedByExplorer}
/>
)
})}
</FancyScrollbar>
</div>
)}
</div>
)
}
private handleGroupByTime = (groupBy: GroupByOption): void => {
this.props.onGroupByTime(groupBy.menuOption) this.props.onGroupByTime(groupBy.menuOption)
} }
handleFill = fill => { private handleFill = (fill: string): void => {
this.props.onFill(fill) this.props.onFill(fill)
} }
handleToggleField = field => { private handleToggleField = (field: Field) => {
const { const {
query, query,
onToggleField, onToggleField,
addInitialField, addInitialField,
initialGroupByTime: time, initialGroupByTime: time,
isKapacitorRule, isKapacitorRule,
isQuerySupportedByExplorer,
} = this.props } = this.props
const {fields, groupBy} = query const {fields, groupBy} = query
if (!isQuerySupportedByExplorer) {
return
}
const initialGroupBy = {...groupBy, time} const initialGroupBy = {...groupBy, time}
if (!_.size(fields)) { if (!_.size(fields)) {
@ -86,7 +220,7 @@ class FieldList extends Component {
onToggleField(field) onToggleField(field)
} }
handleApplyFuncs = fieldFunc => { private handleApplyFuncs = (fieldFunc: ApplyFuncsToFieldArgs): void => {
const { const {
query, query,
removeFuncs, removeFuncs,
@ -109,11 +243,11 @@ class FieldList extends Component {
applyFuncsToField(fieldFunc, groupBy) applyFuncsToField(fieldFunc, groupBy)
} }
handleTimeShift = shift => { private handleTimeShift = (shift: TimeShiftOption): void => {
this.props.onTimeShift(shift) this.props.onTimeShift(shift)
} }
_getFields = () => { private getFields = (): void => {
const {database, measurement, retentionPolicy} = this.props.query const {database, measurement, retentionPolicy} = this.props.query
const {source} = this.context const {source} = this.context
const {querySource} = this.props const {querySource} = this.props
@ -137,114 +271,6 @@ class FieldList extends Component {
}) })
}) })
} }
render() {
const {
query: {database, measurement, fields = [], groupBy, fill, shifts},
isKapacitorRule,
} = this.props
const hasAggregates = numFunctions(fields) > 0
const noDBorMeas = !database || !measurement
return (
<div className="query-builder--column">
<div className="query-builder--heading">
<span>Fields</span>
{hasAggregates ? (
<QueryOptions
fill={fill}
shift={_.first(shifts)}
groupBy={groupBy}
onFill={this.handleFill}
isKapacitorRule={isKapacitorRule}
onTimeShift={this.handleTimeShift}
onGroupByTime={this.handleGroupByTime}
/>
) : null}
</div>
{noDBorMeas ? (
<div className="query-builder--list-empty">
<span>
No <strong>Measurement</strong> selected
</span>
</div>
) : (
<div className="query-builder--list">
<FancyScrollbar>
{this.state.fields.map((fieldFunc, i) => {
const selectedFields = getFieldsWithName(
fieldFunc.value,
fields
)
const funcs = getFuncsByFieldName(fieldFunc.value, fields)
const fieldFuncs = selectedFields.length
? selectedFields
: [fieldFunc]
return (
<FieldListItem
key={i}
onToggleField={this.handleToggleField}
onApplyFuncsToField={this.handleApplyFuncs}
isSelected={!!selectedFields.length}
fieldFuncs={fieldFuncs}
funcs={functionNames(funcs)}
isKapacitorRule={isKapacitorRule}
/>
)
})}
</FancyScrollbar>
</div>
)}
</div>
)
}
}
const {arrayOf, bool, func, shape, string} = PropTypes
FieldList.defaultProps = {
isKapacitorRule: false,
initialGroupByTime: null,
}
FieldList.contextTypes = {
source: shape({
links: shape({
proxy: string.isRequired,
}).isRequired,
}).isRequired,
}
FieldList.propTypes = {
query: shape({
database: string,
retentionPolicy: string,
measurement: string,
shifts: arrayOf(
shape({
label: string,
unit: string,
quantity: string,
})
),
}).isRequired,
onTimeShift: func,
onToggleField: func.isRequired,
onGroupByTime: func.isRequired,
onFill: func,
applyFuncsToField: func.isRequired,
isKapacitorRule: bool,
querySource: shape({
links: shape({
proxy: string.isRequired,
}).isRequired,
}),
removeFuncs: func.isRequired,
addInitialField: func,
initialGroupByTime: string,
} }
export default FieldList export default FieldList

View File

@ -1,18 +1,48 @@
import React, {Component} from 'react' import React, {
import PropTypes from 'prop-types' PureComponent,
import Dropdown from 'shared/components/Dropdown' FocusEvent,
ChangeEvent,
KeyboardEvent,
} from 'react'
import Dropdown from 'src/shared/components/Dropdown'
import {NULL_STRING, NUMBER} from 'shared/constants/queryFillOptions' import {NULL_STRING, NUMBER} from 'src/shared/constants/queryFillOptions'
import queryFills from 'shared/data/queryFills' import queryFills from 'src/shared/data/queryFills'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
onChooseFill: (text: string) => void
value: string
size?: string
theme?: string
isDisabled?: boolean
}
interface Item {
type: string
text: string
}
interface State {
selected: Item
currentNumberValue: string
resetNumberValue: string
}
@ErrorHandling @ErrorHandling
class FillQuery extends Component { class FillQuery extends PureComponent<Props, State> {
public static defaultProps: Partial<Props> = {
size: 'sm',
theme: 'blue',
value: NULL_STRING,
}
private numberInput: HTMLElement
constructor(props) { constructor(props) {
super(props) super(props)
const isNumberValue = !isNaN(Number(props.value)) const isNumberValue: boolean = !isNaN(Number(props.value))
this.state = isNumberValue this.state = isNumberValue
? { ? {
@ -27,66 +57,8 @@ class FillQuery extends Component {
} }
} }
handleDropdown = item => { public render() {
if (item.text === NUMBER) { const {size, theme, isDisabled} = this.props
this.setState({selected: item}, () => {
this.numberInput.focus()
})
} else {
this.setState({selected: item}, () => {
this.props.onChooseFill(item.text)
})
}
}
handleInputBlur = e => {
const nextNumberValue = e.target.value
? e.target.value
: this.state.resetNumberValue || '0'
this.setState({
currentNumberValue: nextNumberValue,
resetNumberValue: nextNumberValue,
})
this.props.onChooseFill(nextNumberValue)
}
handleInputChange = e => {
const currentNumberValue = e.target.value
this.setState({currentNumberValue})
}
handleKeyDown = e => {
if (e.key === 'Enter') {
this.numberInput.blur()
}
}
handleKeyUp = e => {
if (e.key === 'Escape') {
this.setState({currentNumberValue: this.state.resetNumberValue}, () => {
this.numberInput.blur()
})
}
}
getColor = theme => {
switch (theme) {
case 'BLUE':
return 'plutonium'
case 'GREEN':
return 'malachite'
case 'PURPLE':
return 'astronaut'
default:
return 'plutonium'
}
}
render() {
const {size, theme} = this.props
const {selected, currentNumberValue} = this.state const {selected, currentNumberValue} = this.state
return ( return (
@ -114,26 +86,70 @@ class FillQuery extends Component {
buttonColor="btn-info" buttonColor="btn-info"
menuClass={`dropdown-${this.getColor(theme)}`} menuClass={`dropdown-${this.getColor(theme)}`}
onChoose={this.handleDropdown} onChoose={this.handleDropdown}
disabled={isDisabled}
/> />
<label className="fill-query--label">Fill:</label> <label className="fill-query--label">Fill:</label>
</div> </div>
) )
} }
}
const {func, string} = PropTypes private handleDropdown = (item: Item): void => {
if (item.text === NUMBER) {
this.setState({selected: item}, () => {
this.numberInput.focus()
})
} else {
this.setState({selected: item}, () => {
this.props.onChooseFill(item.text)
})
}
}
FillQuery.defaultProps = { private handleInputBlur = (e: FocusEvent<HTMLInputElement>): void => {
size: 'sm', const nextNumberValue = e.target.value
theme: 'blue', ? e.target.value
value: NULL_STRING, : this.state.resetNumberValue || '0'
}
FillQuery.propTypes = { this.setState({
onChooseFill: func.isRequired, currentNumberValue: nextNumberValue,
value: string, resetNumberValue: nextNumberValue,
size: string, })
theme: string,
this.props.onChooseFill(nextNumberValue)
}
private handleInputChange = (e: ChangeEvent<HTMLInputElement>): void => {
const currentNumberValue = e.target.value
this.setState({currentNumberValue})
}
private handleKeyDown = (e: KeyboardEvent<HTMLInputElement>): void => {
if (e.key === 'Enter') {
this.numberInput.blur()
}
}
private handleKeyUp = (e: KeyboardEvent<HTMLInputElement>): void => {
if (e.key === 'Escape') {
this.setState({currentNumberValue: this.state.resetNumberValue}, () => {
this.numberInput.blur()
})
}
}
private getColor = (theme: string): string => {
switch (theme) {
case 'BLUE':
return 'plutonium'
case 'GREEN':
return 'malachite'
case 'PURPLE':
return 'astronaut'
default:
return 'plutonium'
}
}
} }
export default FillQuery export default FillQuery

View File

@ -6,7 +6,7 @@ import _ from 'lodash'
import {showMeasurements} from 'src/shared/apis/metaQuery' import {showMeasurements} from 'src/shared/apis/metaQuery'
import showMeasurementsParser from 'src/shared/parsing/showMeasurements' import showMeasurementsParser from 'src/shared/parsing/showMeasurements'
import {Query, Source} from 'src/types' import {QueryConfig, Source} from 'src/types'
import FancyScrollbar from 'src/shared/components/FancyScrollbar' import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import MeasurementListFilter from 'src/shared/components/MeasurementListFilter' import MeasurementListFilter from 'src/shared/components/MeasurementListFilter'
@ -14,11 +14,12 @@ import MeasurementListItem from 'src/shared/components/MeasurementListItem'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props { interface Props {
query: Query query: QueryConfig
querySource: Source querySource: Source
onChooseTag: () => void onChooseTag: () => void
onGroupByTag: () => void onGroupByTag: () => void
onToggleTagAcceptance: () => void onToggleTagAcceptance: () => void
isQuerySupportedByExplorer: boolean
onChooseMeasurement: (measurement: string) => void onChooseMeasurement: (measurement: string) => void
} }
@ -117,7 +118,13 @@ class MeasurementList extends PureComponent<Props, State> {
} }
public render() { public render() {
const {query, querySource, onChooseTag, onGroupByTag} = this.props const {
query,
querySource,
onChooseTag,
onGroupByTag,
isQuerySupportedByExplorer,
} = this.props
const {database, areTagsAccepted} = query const {database, areTagsAccepted} = query
const {filtered} = this.state const {filtered} = this.state
@ -147,6 +154,7 @@ class MeasurementList extends PureComponent<Props, State> {
areTagsAccepted={areTagsAccepted} areTagsAccepted={areTagsAccepted}
onAcceptReject={this.handleAcceptReject} onAcceptReject={this.handleAcceptReject}
isActive={measurement === query.measurement} isActive={measurement === query.measurement}
isQuerySupportedByExplorer={isQuerySupportedByExplorer}
numTagsActive={Object.keys(query.tags).length} numTagsActive={Object.keys(query.tags).length}
onChooseMeasurement={this.handleChoosemeasurement} onChooseMeasurement={this.handleChoosemeasurement}
/> />

View File

@ -38,6 +38,7 @@ interface Props {
onChooseTag: () => void onChooseTag: () => void
onGroupByTag: () => void onGroupByTag: () => void
onAcceptReject: () => void onAcceptReject: () => void
isQuerySupportedByExplorer: boolean
onChooseMeasurement: (measurement: string) => () => void onChooseMeasurement: (measurement: string) => () => void
} }
@ -62,6 +63,7 @@ class MeasurementListItem extends PureComponent<Props, State> {
onGroupByTag, onGroupByTag,
numTagsActive, numTagsActive,
areTagsAccepted, areTagsAccepted,
isQuerySupportedByExplorer,
} = this.props } = this.props
return ( return (
@ -80,6 +82,7 @@ class MeasurementListItem extends PureComponent<Props, State> {
<div <div
className={classnames('flip-toggle', { className={classnames('flip-toggle', {
flipped: areTagsAccepted, flipped: areTagsAccepted,
disabled: !isQuerySupportedByExplorer,
})} })}
onClick={this.handleAcceptReject} onClick={this.handleAcceptReject}
> >
@ -96,6 +99,7 @@ class MeasurementListItem extends PureComponent<Props, State> {
querySource={querySource} querySource={querySource}
onChooseTag={onChooseTag} onChooseTag={onChooseTag}
onGroupByTag={onGroupByTag} onGroupByTag={onGroupByTag}
isQuerySupportedByExplorer={isQuerySupportedByExplorer}
/> />
)} )}
</div> </div>
@ -105,6 +109,11 @@ class MeasurementListItem extends PureComponent<Props, State> {
private handleAcceptReject = (e: MouseEvent<HTMLElement>) => { private handleAcceptReject = (e: MouseEvent<HTMLElement>) => {
e.stopPropagation() e.stopPropagation()
const {isQuerySupportedByExplorer} = this.props
if (!isQuerySupportedByExplorer) {
return
}
const {onAcceptReject} = this.props const {onAcceptReject} = this.props
onAcceptReject() onAcceptReject()
} }

View File

@ -0,0 +1,105 @@
import React, {PureComponent, ComponentClass} from 'react'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {dismissOverlay} from 'src/shared/actions/overlayTechnology'
interface Props {
OverlayNode?: ComponentClass<any>
dismissOnClickOutside?: boolean
dismissOnEscape?: boolean
transitionTime?: number
handleDismissOverlay: () => void
}
interface State {
visible: boolean
}
export const OverlayContext = React.createContext()
@ErrorHandling
class Overlay extends PureComponent<Props, State> {
public static defaultProps: Partial<Props> = {
dismissOnClickOutside: false,
dismissOnEscape: false,
transitionTime: 300,
}
private animationTimer: number
constructor(props) {
super(props)
this.state = {
visible: false,
}
}
public componentDidUpdate(prevProps) {
if (prevProps.OverlayNode === null && this.props.OverlayNode) {
return this.setState({visible: true})
}
}
public render() {
const {OverlayNode} = this.props
return (
<OverlayContext.Provider
value={{
onDismissOverlay: this.handleAnimateDismiss,
}}
>
<div className={this.overlayClass}>
<div className="overlay--dialog">{OverlayNode}</div>
<div className="overlay--mask" onClick={this.handleClickOutside} />
</div>
</OverlayContext.Provider>
)
}
private get overlayClass(): string {
const {visible} = this.state
return `overlay-tech ${visible ? 'show' : ''}`
}
public handleClickOutside = () => {
const {handleDismissOverlay, dismissOnClickOutside} = this.props
if (dismissOnClickOutside) {
handleDismissOverlay()
}
}
public handleAnimateDismiss = () => {
const {transitionTime} = this.props
this.setState({visible: false})
this.animationTimer = window.setTimeout(this.handleDismiss, transitionTime)
}
public handleDismiss = () => {
const {handleDismissOverlay} = this.props
handleDismissOverlay()
clearTimeout(this.animationTimer)
}
}
const mapStateToProps = ({
overlayTechnology: {
OverlayNode,
options: {dismissOnClickOutside, dismissOnEscape, transitionTime},
},
}) => ({
OverlayNode,
dismissOnClickOutside,
dismissOnEscape,
transitionTime,
})
const mapDispatchToProps = dispatch => ({
handleDismissOverlay: bindActionCreators(dismissOverlay, dispatch),
})
export default connect(mapStateToProps, mapDispatchToProps)(Overlay)

View File

@ -1,10 +1,23 @@
import React from 'react' import React, {SFC} from 'react'
import PropTypes from 'prop-types'
import {GroupBy, TimeShift} from 'src/types'
import GroupByTimeDropdown from 'src/data_explorer/components/GroupByTimeDropdown' import GroupByTimeDropdown from 'src/data_explorer/components/GroupByTimeDropdown'
import TimeShiftDropdown from 'src/shared/components/TimeShiftDropdown' import TimeShiftDropdown from 'src/shared/components/TimeShiftDropdown'
import FillQuery from 'shared/components/FillQuery' import FillQuery from 'src/shared/components/FillQuery'
const QueryOptions = ({ interface Props {
fill: string
onFill: (fill: string) => void
groupBy: GroupBy
shift: TimeShift
onGroupByTime: (groupBy: GroupBy) => void
isKapacitorRule: boolean
onTimeShift: (shift: TimeShift) => void
isDisabled: boolean
}
const QueryOptions: SFC<Props> = ({
fill, fill,
shift, shift,
onFill, onFill,
@ -12,36 +25,25 @@ const QueryOptions = ({
onTimeShift, onTimeShift,
onGroupByTime, onGroupByTime,
isKapacitorRule, isKapacitorRule,
isDisabled,
}) => ( }) => (
<div className="query-builder--groupby-fill-container"> <div className="query-builder--groupby-fill-container">
<GroupByTimeDropdown <GroupByTimeDropdown
selected={groupBy.time} selected={groupBy.time}
onChooseGroupByTime={onGroupByTime} onChooseGroupByTime={onGroupByTime}
isDisabled={isDisabled}
/> />
{isKapacitorRule ? null : ( {isKapacitorRule ? null : (
<TimeShiftDropdown <TimeShiftDropdown
selected={shift && shift.label} selected={shift && shift.label}
onChooseTimeShift={onTimeShift} onChooseTimeShift={onTimeShift}
isDisabled={isDisabled}
/> />
)} )}
{isKapacitorRule ? null : <FillQuery value={fill} onChooseFill={onFill} />} {isKapacitorRule ? null : (
<FillQuery value={fill} onChooseFill={onFill} isDisabled={isDisabled} />
)}
</div> </div>
) )
const {bool, func, shape, string} = PropTypes
QueryOptions.propTypes = {
fill: string,
onFill: func.isRequired,
groupBy: shape({
time: string,
}).isRequired,
shift: shape({
label: string,
}),
onGroupByTime: func.isRequired,
isKapacitorRule: bool.isRequired,
onTimeShift: func.isRequired,
}
export default QueryOptions export default QueryOptions

View File

@ -9,7 +9,6 @@ const actionBinder = (id, action) => (...args) => action(id, ...args)
const SchemaExplorer = ({ const SchemaExplorer = ({
query, query,
query: {id},
source, source,
initialGroupByTime, initialGroupByTime,
actions: { actions: {
@ -26,39 +25,50 @@ const SchemaExplorer = ({
applyFuncsToField, applyFuncsToField,
toggleTagAcceptance, toggleTagAcceptance,
}, },
}) => ( isQuerySupportedByExplorer = true,
<div className="query-builder"> }) => {
<DatabaseList const {id} = query
query={query}
querySource={source}
onChooseNamespace={actionBinder(id, chooseNamespace)}
/>
<MeasurementList
source={source}
query={query}
querySource={source}
onChooseTag={actionBinder(id, chooseTag)}
onGroupByTag={actionBinder(id, groupByTag)}
onChooseMeasurement={actionBinder(id, chooseMeasurement)}
onToggleTagAcceptance={actionBinder(id, toggleTagAcceptance)}
/>
<FieldList
source={source}
query={query}
querySource={source}
onFill={actionBinder(id, fill)}
initialGroupByTime={initialGroupByTime}
onTimeShift={actionBinder(id, timeShift)}
removeFuncs={actionBinder(id, removeFuncs)}
onToggleField={actionBinder(id, toggleField)}
onGroupByTime={actionBinder(id, groupByTime)}
addInitialField={actionBinder(id, addInitialField)}
applyFuncsToField={actionBinder(id, applyFuncsToField)}
/>
</div>
)
const {func, shape, string} = PropTypes return (
<div className="query-builder">
<DatabaseList
query={query}
querySource={source}
onChooseNamespace={actionBinder(id, chooseNamespace)}
/>
<MeasurementList
source={source}
query={query}
querySource={source}
onChooseTag={actionBinder(id, chooseTag)}
onGroupByTag={actionBinder(id, groupByTag)}
onChooseMeasurement={actionBinder(id, chooseMeasurement)}
onToggleTagAcceptance={actionBinder(id, toggleTagAcceptance)}
isQuerySupportedByExplorer={isQuerySupportedByExplorer}
/>
<FieldList
source={source}
query={query}
querySource={source}
onFill={actionBinder(id, fill)}
initialGroupByTime={initialGroupByTime}
onTimeShift={actionBinder(id, timeShift)}
removeFuncs={actionBinder(id, removeFuncs)}
onToggleField={actionBinder(id, toggleField)}
onGroupByTime={actionBinder(id, groupByTime)}
addInitialField={actionBinder(id, addInitialField)}
applyFuncsToField={actionBinder(id, applyFuncsToField)}
isQuerySupportedByExplorer={isQuerySupportedByExplorer}
/>
</div>
)
}
const {bool, func, shape, string} = PropTypes
SchemaExplorer.defaultProps = {
isQuerySupportedByExplorer: true,
}
SchemaExplorer.propTypes = { SchemaExplorer.propTypes = {
query: shape({ query: shape({
@ -80,6 +90,7 @@ SchemaExplorer.propTypes = {
}).isRequired, }).isRequired,
source: shape({}), source: shape({}),
initialGroupByTime: string.isRequired, initialGroupByTime: string.isRequired,
isQuerySupportedByExplorer: bool,
} }
export default SchemaExplorer export default SchemaExplorer

View File

@ -40,6 +40,7 @@ interface Props {
querySource: Source querySource: Source
onChooseTag: () => void onChooseTag: () => void
onGroupByTag: () => void onGroupByTag: () => void
isQuerySupportedByExplorer: boolean
} }
interface State { interface State {
@ -129,7 +130,12 @@ class TagList extends PureComponent<Props, State> {
} }
public render() { public render() {
const {query, onChooseTag, onGroupByTag} = this.props const {
query,
onChooseTag,
onGroupByTag,
isQuerySupportedByExplorer,
} = this.props
return ( return (
<div className="query-builder--sub-list"> <div className="query-builder--sub-list">
@ -142,6 +148,7 @@ class TagList extends PureComponent<Props, State> {
onGroupByTag={onGroupByTag} onGroupByTag={onGroupByTag}
selectedTagValues={query.tags[tagKey] || []} selectedTagValues={query.tags[tagKey] || []}
isUsingGroupBy={query.groupBy.tags.indexOf(tagKey) > -1} isUsingGroupBy={query.groupBy.tags.indexOf(tagKey) > -1}
isQuerySupportedByExplorer={isQuerySupportedByExplorer}
/> />
))} ))}
</div> </div>

View File

@ -14,6 +14,7 @@ interface Props {
selectedTagValues: string[] selectedTagValues: string[]
isUsingGroupBy?: boolean isUsingGroupBy?: boolean
onChooseTag: (tag: Tag) => void onChooseTag: (tag: Tag) => void
isQuerySupportedByExplorer: boolean
onGroupByTag: (tagKey: string) => void onGroupByTag: (tagKey: string) => void
} }
@ -36,10 +37,16 @@ class TagListItem extends PureComponent<Props, State> {
this.handleGroupBy = this.handleGroupBy.bind(this) this.handleGroupBy = this.handleGroupBy.bind(this)
this.handleClickKey = this.handleClickKey.bind(this) this.handleClickKey = this.handleClickKey.bind(this)
this.handleFilterText = this.handleFilterText.bind(this) this.handleFilterText = this.handleFilterText.bind(this)
this.handleInputClick = this.handleInputClick.bind(this)
} }
public handleChoose(tagValue: string, e: MouseEvent<HTMLElement>) { public handleChoose(tagValue: string, e: MouseEvent<HTMLElement>) {
e.stopPropagation() e.stopPropagation()
const {isQuerySupportedByExplorer} = this.props
if (!isQuerySupportedByExplorer) {
return
}
this.props.onChooseTag({key: this.props.tagKey, value: tagValue}) this.props.onChooseTag({key: this.props.tagKey, value: tagValue})
} }
@ -67,7 +74,11 @@ class TagListItem extends PureComponent<Props, State> {
} }
public handleGroupBy(e) { public handleGroupBy(e) {
const {isQuerySupportedByExplorer} = this.props
e.stopPropagation() e.stopPropagation()
if (!isQuerySupportedByExplorer) {
return
}
this.props.onGroupByTag(this.props.tagKey) this.props.onGroupByTag(this.props.tagKey)
} }
@ -76,7 +87,11 @@ class TagListItem extends PureComponent<Props, State> {
} }
public renderTagValues() { public renderTagValues() {
const {tagValues, selectedTagValues} = this.props const {
tagValues,
selectedTagValues,
isQuerySupportedByExplorer,
} = this.props
if (!tagValues || !tagValues.length) { if (!tagValues || !tagValues.length) {
return <div>no tag values</div> return <div>no tag values</div>
} }
@ -103,6 +118,7 @@ class TagListItem extends PureComponent<Props, State> {
{filtered.map(v => { {filtered.map(v => {
const cx = classnames('query-builder--list-item', { const cx = classnames('query-builder--list-item', {
active: selectedTagValues.indexOf(v) > -1, active: selectedTagValues.indexOf(v) > -1,
disabled: !isQuerySupportedByExplorer,
}) })
return ( return (
<div <div
@ -123,7 +139,12 @@ class TagListItem extends PureComponent<Props, State> {
} }
public render() { public render() {
const {tagKey, tagValues, isUsingGroupBy} = this.props const {
tagKey,
tagValues,
isUsingGroupBy,
isQuerySupportedByExplorer,
} = this.props
const {isOpen} = this.state const {isOpen} = this.state
const tagItemLabel = `${tagKey}${tagValues.length}` const tagItemLabel = `${tagKey}${tagValues.length}`
@ -142,6 +163,7 @@ class TagListItem extends PureComponent<Props, State> {
className={classnames('btn btn-xs group-by-tag', { className={classnames('btn btn-xs group-by-tag', {
'btn-default': !isUsingGroupBy, 'btn-default': !isUsingGroupBy,
'btn-primary': isUsingGroupBy, 'btn-primary': isUsingGroupBy,
disabled: !isQuerySupportedByExplorer,
})} })}
onClick={this.handleGroupBy} onClick={this.handleGroupBy}
> >

View File

@ -1,26 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import Dropdown from 'shared/components/Dropdown'
import {TIME_SHIFTS} from 'shared/constants/timeShift'
const TimeShiftDropdown = ({selected, onChooseTimeShift}) => (
<div className="group-by-time">
<label className="group-by-time--label">Compare:</label>
<Dropdown
className="group-by-time--dropdown"
buttonColor="btn-info"
items={TIME_SHIFTS}
onChoose={onChooseTimeShift}
selected={selected || 'none'}
/>
</div>
)
const {func, string} = PropTypes
TimeShiftDropdown.propTypes = {
selected: string,
onChooseTimeShift: func.isRequired,
}
export default TimeShiftDropdown

View File

@ -0,0 +1,31 @@
import React, {SFC} from 'react'
import Dropdown from 'src/shared/components/Dropdown'
import {TIME_SHIFTS} from 'src/shared/constants/timeShift'
import {TimeShift} from 'src/types'
interface Props {
selected: string
onChooseTimeShift: (timeShift: TimeShift) => void
isDisabled: boolean
}
const TimeShiftDropdown: SFC<Props> = ({
selected,
onChooseTimeShift,
isDisabled,
}) => (
<div className="group-by-time">
<label className="group-by-time--label">Compare:</label>
<Dropdown
className="group-by-time--dropdown"
buttonColor="btn-info"
items={TIME_SHIFTS}
onChoose={onChooseTimeShift}
selected={selected || 'none'}
disabled={isDisabled}
/>
</div>
)
export default TimeShiftDropdown

View File

@ -1 +1 @@
export const OVERLAY_TECHNOLOGY = 'overlay-technology' export const OVERLAY_TECHNOLOGY = 'ceo'

View File

@ -59,7 +59,7 @@ export const generateThresholdsListHexs = ({
} }
if (!lastValue) { if (!lastValue) {
return {...defaultColoring, textColor: baseColor} return {...defaultColoring, textColor: baseColor.hex}
} }
// If the single stat is above a line graph never have a background color // If the single stat is above a line graph never have a background color

View File

@ -410,6 +410,13 @@ export const VIS_VIEWS = [GRAPH, TABLE]
// InfluxQL Macros // InfluxQL Macros
export const TEMP_VAR_INTERVAL = ':interval:' export const TEMP_VAR_INTERVAL = ':interval:'
export const TEMP_VAR_DASHBOARD_TIME = ':dashboardTime:'
export const TEMP_VAR_UPPER_DASHBOARD_TIME = ':upperDashboardTime:'
export const PREDEFINED_TEMP_VARS = [
TEMP_VAR_INTERVAL,
TEMP_VAR_DASHBOARD_TIME,
TEMP_VAR_UPPER_DASHBOARD_TIME,
]
export const INITIAL_GROUP_BY_TIME = '10s' export const INITIAL_GROUP_BY_TIME = '10s'
export const AUTO_GROUP_BY = 'auto' export const AUTO_GROUP_BY = 'auto'
@ -443,7 +450,7 @@ export const intervalValuesPoints = [
export const interval = { export const interval = {
id: 'interval', id: 'interval',
type: 'autoGroupBy', type: 'autoGroupBy',
tempVar: ':interval:', tempVar: TEMP_VAR_INTERVAL,
label: 'automatically determine the best group by time', label: 'automatically determine the best group by time',
values: intervalValuesPoints, values: intervalValuesPoints,
} }

View File

@ -438,6 +438,13 @@ export const notifyCellDeleted = name => ({
message: `Deleted "${name}" from dashboard.`, message: `Deleted "${name}" from dashboard.`,
}) })
export const notifyBuilderDisabled = () => ({
type: 'info',
icon: 'graphline',
duration: 7500,
message: `Your query contains a user-defined Template Variable. The Schema Explorer cannot render the query and is disabled.`,
})
// Rule Builder Notifications // Rule Builder Notifications
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
export const notifyAlertRuleCreated = () => ({ export const notifyAlertRuleCreated = () => ({

View File

@ -42,6 +42,12 @@ export const firstFieldName = fields => _.head(fieldNamesDeep(fields))
export const hasField = (fieldName, fields) => export const hasField = (fieldName, fields) =>
fieldNamesDeep(fields).some(f => f === fieldName) fieldNamesDeep(fields).some(f => f === fieldName)
/**
* getAllFields and funcs with fieldName
* @param {string} fieldName
* @param {FieldFunc[]} fields
* @returns {FieldFunc[]}
*/
// getAllFields and funcs with fieldName // getAllFields and funcs with fieldName
export const getFieldsWithName = (fieldName, fields) => export const getFieldsWithName = (fieldName, fields) =>
getFieldsDeep(fields).filter(f => f.value === fieldName) getFieldsDeep(fields).filter(f => f.value === fieldName)

View File

@ -0,0 +1,29 @@
const initialState = {
options: {
dismissOnClickOutside: false,
dismissOnEscape: false,
transitionTime: 300,
},
OverlayNode: null,
}
export default function overlayTechnology(state = initialState, action) {
switch (action.type) {
case 'SHOW_OVERLAY': {
const {OverlayNode, options} = action.payload
return {...state, OverlayNode, options}
}
case 'DISMISS_OVERLAY': {
const {options} = initialState
return {
...state,
OverlayNode: null,
options,
}
}
}
return state
}

View File

@ -8,6 +8,10 @@ import LayoutRenderer from 'shared/components/LayoutRenderer'
import {fixtureStatusPageCells} from 'src/status/fixtures' import {fixtureStatusPageCells} from 'src/status/fixtures'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
import {
TEMP_VAR_DASHBOARD_TIME,
TEMP_VAR_UPPER_DASHBOARD_TIME,
} from 'src/shared/constants'
@ErrorHandling @ErrorHandling
class StatusPage extends Component { class StatusPage extends Component {
@ -25,7 +29,7 @@ class StatusPage extends Component {
const dashboardTime = { const dashboardTime = {
id: 'dashtime', id: 'dashtime',
tempVar: ':dashboardTime:', tempVar: TEMP_VAR_DASHBOARD_TIME,
type: 'constant', type: 'constant',
values: [ values: [
{ {
@ -38,7 +42,7 @@ class StatusPage extends Component {
const upperDashboardTime = { const upperDashboardTime = {
id: 'upperdashtime', id: 'upperdashtime',
tempVar: ':upperDashboardTime:', tempVar: TEMP_VAR_UPPER_DASHBOARD_TIME,
type: 'constant', type: 'constant',
values: [ values: [
{ {

View File

@ -1,4 +1,5 @@
import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes' import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes'
import {TEMP_VAR_DASHBOARD_TIME} from 'src/shared/constants'
export const fixtureStatusPageCells = [ export const fixtureStatusPageCells = [
{ {
@ -13,8 +14,7 @@ export const fixtureStatusPageCells = [
colors: DEFAULT_LINE_COLORS, colors: DEFAULT_LINE_COLORS,
queries: [ queries: [
{ {
query: query: `SELECT count("value") AS "count_value" FROM "chronograf"."autogen"."alerts" WHERE time > ${TEMP_VAR_DASHBOARD_TIME} GROUP BY time(1d)`,
'SELECT count("value") AS "count_value" FROM "chronograf"."autogen"."alerts" WHERE time > :dashboardTime: GROUP BY time(1d)',
label: 'Events', label: 'Events',
queryConfig: { queryConfig: {
database: 'chronograf', database: 'chronograf',

View File

@ -13,6 +13,7 @@ import adminReducers from 'src/admin/reducers'
import kapacitorReducers from 'src/kapacitor/reducers' import kapacitorReducers from 'src/kapacitor/reducers'
import dashboardUI from 'src/dashboards/reducers/ui' import dashboardUI from 'src/dashboards/reducers/ui'
import cellEditorOverlay from 'src/dashboards/reducers/cellEditorOverlay' import cellEditorOverlay from 'src/dashboards/reducers/cellEditorOverlay'
import overlayTechnology from 'src/shared/reducers/overlayTechnology'
import dashTimeV1 from 'src/dashboards/reducers/dashTimeV1' import dashTimeV1 from 'src/dashboards/reducers/dashTimeV1'
import persistStateEnhancer from './persistStateEnhancer' import persistStateEnhancer from './persistStateEnhancer'
@ -24,6 +25,7 @@ const rootReducer = combineReducers({
...adminReducers, ...adminReducers,
dashboardUI, dashboardUI,
cellEditorOverlay, cellEditorOverlay,
overlayTechnology,
dashTimeV1, dashTimeV1,
routing: routerReducer, routing: routerReducer,
}) })

View File

@ -29,7 +29,7 @@
@import 'layout/page-header'; @import 'layout/page-header';
@import 'layout/page-subsections'; @import 'layout/page-subsections';
@import 'layout/sidebar'; @import 'layout/sidebar';
@import 'layout/overlay'; @import 'layout/overlay-technology';
// Components // Components
@import 'components/annotations'; @import 'components/annotations';

View File

@ -1,8 +1,8 @@
/* /*
Flip Toggle Flip Toggle
------------------------------------------------------------- ------------------------------------------------------------------------------
Toggles between 2 options using a 3D transition Toggles between 2 options using a 3D transition
Aesthetic and space conservative Aesthetic and space conservative
*/ */
$flip-toggle-text: $g11-sidewalk; $flip-toggle-text: $g11-sidewalk;
@ -63,3 +63,19 @@ $flip-toggle-size: 28px;
.flip-toggle.flipped .flip-toggle--container { .flip-toggle.flipped .flip-toggle--container {
transform: rotateY(180deg); transform: rotateY(180deg);
} }
/*
Disabled State
------------------------------------------------------------------------------
*/
.flip-toggle.disabled,
.flip-toggle.disabled:hover {
.flip-toggle--front,
.flip-toggle--back {
cursor: not-allowed;
background-color: $g3-castle;
color: $g9-mountain;
border-color: $g5-pepper;
}
}

View File

@ -98,6 +98,20 @@
white-space: nowrap; white-space: nowrap;
margin-right: 8px; margin-right: 8px;
} }
/* Disabled State */
&.disabled {
font-style: italic;
color: $query-builder--list-item-text-disabled;
&:hover {
cursor: default;
background-color: $query-builder--list-item-bg;
}
&.active {
background-color: $query-builder--list-item-bg-active;
}
}
} }
/* Filter Element */ /* Filter Element */
.query-builder--filter { .query-builder--filter {
@ -156,6 +170,10 @@
border-radius: 50%; border-radius: 50%;
transition: transform 0.25s ease, opacity 0.25s ease; transition: transform 0.25s ease, opacity 0.25s ease;
} }
.disabled &:after {
background-color: $g5-pepper;
}
} }
.query-builder--list-item.active .query-builder--checkbox:after { .query-builder--list-item.active .query-builder--checkbox:after {
opacity: 1; opacity: 1;

View File

@ -63,6 +63,7 @@ $query-builder--list-item-bg-hover: $g4-onyx;
$query-builder--list-item-text-hover: $g15-platinum; $query-builder--list-item-text-hover: $g15-platinum;
$query-builder--list-item-bg-active: $g4-onyx; $query-builder--list-item-bg-active: $g4-onyx;
$query-builder--list-item-text-active: $g18-cloud; $query-builder--list-item-text-active: $g18-cloud;
$query-builder--list-item-text-disabled: $g9-mountain;
$query-builder--sub-list-gutter: 24px; $query-builder--sub-list-gutter: 24px;
$query-builder--sub-list-bg: $query-builder--list-item-bg-active; $query-builder--sub-list-bg: $query-builder--list-item-bg-active;

View File

@ -0,0 +1,50 @@
/*
Overlay Technology Styles
----------------------------------------------------------------------------
*/
%overlay-styles {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
.overlay--mask {
@extend %overlay-styles;
z-index: 1;
opacity: 0;
transition: opacity 0.25s ease;
@include gradient-diag-down($c-pool,$c-comet);
}
.overlay--dialog {
position: relative;
z-index: 2;
transform: translateY(72px);
opacity: 0;
transition: transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.25s ease;
}
.overlay-tech {
@extend %overlay-styles;
visibility: hidden;
transition: all 0.25s ease;
z-index: 9999;;
&.show {
visibility: visible;
}
}
// Open State
.overlay-tech.show {
.overlay--mask {
opacity: 0.7;
}
.overlay--dialog {
opacity: 1;
transform: translateY(0);
}
}

View File

@ -1,31 +0,0 @@
/*
Manages Overlays
----------------------------------------------
*/
.page, .sidebar {
position: relative;
}
.page {
z-index: 2;
}
.sidebar {
z-index: 1;
// Ensures that sidebar menus appear above the rest of the app on hover
&:hover {z-index: 2;}
&:hover + .page {z-index: 1;}
}
// Make Overlay Technology full screen
.overlay-technology {
left: -($sidebar--width) !important;
// Hacky way to ensure proper appearance of file upload modal
// Needed to have a this div nested inside of itself for the
// Drag & drop feature to work correctly
.overlay-technology {
left: 0 !important;
&:before {display: none;}
}
}

View File

@ -342,3 +342,17 @@ span.icon.sidebar--icon.sidebar--icon__superadmin {
} }
} }
} }
// Ensures that sidebar menus appear above the rest of the app on hover
.page, .sidebar {
position: relative;
}
.page {
z-index: 2;
}
.sidebar {
z-index: 1;
&:hover {z-index: 2;}
&:hover + .page {z-index: 1;}
}

View File

@ -1,13 +1,27 @@
/* /*
Styles for Overlay Technology (aka Cell Edit Mode) Styles for Cell Editor Overlay
------------------------------------------------------ ------------------------------------------------------------------------------
*/ */
$overlay-controls-height: 60px; $overlay-controls-height: 60px;
$overlay-controls-bg: $g2-kevlar; $overlay-controls-bg: $g2-kevlar;
$overlay-z: 100; $overlay-z: 100;
.overlay-technology {
// Make Overlay Technology full screen
.ceo {
left: -($sidebar--width) !important;
// Hacky way to ensure proper appearance of file upload modal
// Needed to have a this div nested inside of itself for the
// Drag & drop feature to work correctly
.ceo {
left: 0 !important;
&:before {display: none;}
}
}
.ceo {
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0; bottom: 0;
@ -79,13 +93,13 @@ $overlay-z: 100;
@include no-user-select; @include no-user-select;
} }
} }
.overlay-technology--editor { .ceo--editor {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
height: 100%; height: 100%;
} }
.overlay-technology--editor .query-maker--empty { .ceo--editor .query-maker--empty {
margin-bottom: 8px; margin-bottom: 8px;
} }
.overlay-controls .confirm-or-cancel { .overlay-controls .confirm-or-cancel {
@ -93,18 +107,18 @@ $overlay-z: 100;
} }
/* Graph editing in Dashboards is a little smaller so the dash can be seen in the background */ /* Graph editing in Dashboards is a little smaller so the dash can be seen in the background */
.overlay-technology .graph { .ceo .graph {
margin: 0 15%; margin: 0 15%;
} }
.overlay-technology .query-maker { .ceo .query-maker {
flex: 1 0 0%; flex: 1 0 0%;
padding: 0 18px; padding: 0 18px;
margin: 0; margin: 0;
background-color: $g2-kevlar; background-color: $g2-kevlar;
} }
.overlay-technology .query-maker--tabs { .ceo .query-maker--tabs {
margin-top: 0; margin-top: 0;
} }
.overlay-technology .query-maker--tab-contents { .ceo .query-maker--tab-contents {
margin-bottom: 8px; margin-bottom: 8px;
} }

View File

@ -486,10 +486,10 @@ $dash-graph-options-arrow: 8px;
@import '../components/template-control-bar'; @import '../components/template-control-bar';
/* /*
Overylay Technology (Cell Edit Mode) Cell Editor Overlay
------------------------------------------------------ ------------------------------------------------------
*/ */
@import 'overlay-technology'; @import 'cell-editor-overlay';
/* /*
Template Variables Manager Template Variables Manager

View File

@ -1,5 +1,6 @@
import {Query} from 'src/types' import {QueryConfig} from 'src/types'
import {ColorString} from 'src/types/colors' import {ColorString} from 'src/types/colors'
interface Axis { interface Axis {
bounds: [string, string] bounds: [string, string]
label: string label: string
@ -9,18 +10,18 @@ interface Axis {
scale: string scale: string
} }
interface Axes { export interface Axes {
x: Axis x: Axis
y: Axis y: Axis
} }
interface FieldName { export interface FieldName {
internalName: string internalName: string
displayName: string displayName: string
visible: boolean visible: boolean
} }
interface TableOptions { export interface TableOptions {
verticalTimeAxis: boolean verticalTimeAxis: boolean
sortBy: FieldName sortBy: FieldName
wrapping: string wrapping: string
@ -33,7 +34,7 @@ interface CellLinks {
export interface CellQuery { export interface CellQuery {
query: string query: string
queryConfig: Query queryConfig: QueryConfig
} }
export interface Legend { export interface Legend {
@ -41,7 +42,7 @@ export interface Legend {
orientation?: string orientation?: string
} }
interface DecimalPlaces { export interface DecimalPlaces {
isEnforced: boolean isEnforced: boolean
digits: number digits: number
} }
@ -75,3 +76,21 @@ export interface Template {
tempVar: string tempVar: string
values: TemplateValue[] values: TemplateValue[]
} }
export type CellEditorOverlayActionsFunc = (id: string, ...args: any[]) => any
export interface CellEditorOverlayActions {
chooseNamespace: (id: string) => void
chooseMeasurement: (id: string) => void
applyFuncsToField: (id: string) => void
chooseTag: (id: string) => void
groupByTag: (id: string) => void
toggleField: (id: string) => void
groupByTime: (id: string) => void
toggleTagAcceptance: (id: string) => void
fill: (id: string) => void
editRawTextAsync: (url: string, id: string, text: string) => Promise<void>
addInitialField: (id: string) => void
removeFuncs: (id: string) => void
timeShift: (id: string) => void
}

View File

@ -1,8 +1,17 @@
import {AuthLinks, Organization, Role, User, Me} from './auth' import {AuthLinks, Organization, Role, User, Me} from './auth'
import {Query, QueryConfig, TimeRange} from './query' import {Template, Cell, CellQuery, Legend} from './dashboard'
import {
GroupBy,
QueryConfig,
Status,
TimeRange,
TimeShift,
Field,
} from './query'
import {AlertRule, Kapacitor, Task} from './kapacitor' import {AlertRule, Kapacitor, Task} from './kapacitor'
import {Source} from './sources' import {Source, SourceLinks} from './sources'
import {DropdownAction, DropdownItem} from './shared' import {DropdownAction, DropdownItem} from './shared'
import {Notification} from 'src/kapacitor/components/AlertOutputs'
export { export {
Me, Me,
@ -10,13 +19,22 @@ export {
Role, Role,
User, User,
Organization, Organization,
Template,
Cell,
CellQuery,
Legend,
Status,
QueryConfig,
TimeShift,
Field,
GroupBy,
AlertRule, AlertRule,
Kapacitor, Kapacitor,
Query,
QueryConfig,
Source, Source,
SourceLinks,
DropdownAction, DropdownAction,
DropdownItem, DropdownItem,
TimeRange, TimeRange,
Task, Task,
Notification,
} }

View File

@ -1,5 +1,5 @@
export interface Query { export interface QueryConfig {
id: QueryID id?: string
database: string database: string
measurement: string measurement: string
retentionPolicy: string retentionPolicy: string
@ -7,12 +7,13 @@ export interface Query {
tags: Tags tags: Tags
groupBy: GroupBy groupBy: GroupBy
areTagsAccepted: boolean areTagsAccepted: boolean
rawText: string | null rawText: string
range?: DurationRange | null range?: DurationRange | null
source?: string source?: string
fill?: string fill?: string
status?: Status status?: Status
shifts: TimeShift[] shifts: TimeShift[]
isQuerySupportedByExplorer?: boolean // doesn't come from server -- is set in CellEditorOverlay
} }
export interface Field { export interface Field {
@ -22,8 +23,6 @@ export interface Field {
args?: Args[] args?: Args[]
} }
export type QueryID = string
export interface Args { export interface Args {
value: string value: string
type: string type: string
@ -69,18 +68,3 @@ export interface TimeShift {
unit: string unit: string
quantity: string quantity: string
} }
export interface QueryConfig {
id?: string
database: string
measurement: string
retentionPolicy: string
fields: Field[]
tags: Tags
groupBy: GroupBy
areTagsAccepted: boolean
fill?: string
rawText: string
range: DurationRange
shifts: TimeShift[]
}

View File

@ -1,5 +1,9 @@
import {buildQuery} from 'utils/influxql' import {buildQuery} from 'utils/influxql'
import {TYPE_SHIFTED, TYPE_QUERY_CONFIG} from 'src/dashboards/constants' import {TYPE_SHIFTED, TYPE_QUERY_CONFIG} from 'src/dashboards/constants'
import {
TEMP_VAR_DASHBOARD_TIME,
TEMP_VAR_UPPER_DASHBOARD_TIME,
} from 'src/shared/constants'
import {timeRanges} from 'shared/data/timeRanges' import {timeRanges} from 'shared/data/timeRanges'
const buildCannedDashboardQuery = (query, {lower, upper}, host) => { const buildCannedDashboardQuery = (query, {lower, upper}, host) => {
@ -48,8 +52,8 @@ export const buildQueriesForLayouts = (cell, source, timeRange, host) => {
queryConfig: {database, measurement, fields, shifts, rawText, range}, queryConfig: {database, measurement, fields, shifts, rawText, range},
} = query } = query
const tR = range || { const tR = range || {
upper: ':upperDashboardTime:', upper: TEMP_VAR_UPPER_DASHBOARD_TIME,
lower: ':dashboardTime:', lower: TEMP_VAR_DASHBOARD_TIME,
} }
queryText = queryText =

View File

@ -0,0 +1,73 @@
import React from 'react'
import {shallow} from 'enzyme'
import CellEditorOverlay from 'src/dashboards/components/CellEditorOverlay'
import QueryMaker from 'src/dashboards/components/QueryMaker'
import {
source,
cell,
timeRange,
userDefinedTemplateVariables,
predefinedTemplateVariables,
thresholdsListColors,
gaugeColors,
lineColors,
query,
} from 'test/fixtures'
jest.mock('src/shared/apis', () => require('mocks/shared/apis'))
const setup = (override = {}) => {
const props = {
source,
sources: [source],
cell,
timeRange,
autoRefresh: 0,
dashboardID: '9',
queryStatus: {
queryID: null,
status: null,
},
templates: [
...userDefinedTemplateVariables,
...predefinedTemplateVariables,
],
thresholdsListType: 'text',
thresholdsListColors,
gaugeColors,
lineColors,
editQueryStatus: () => {},
onCancel: () => {},
onSave: () => {},
notify: () => {},
...override,
}
const wrapper = shallow(<CellEditorOverlay {...props} />)
return {props, wrapper}
}
describe('Dashboards.Components.CellEditorOverlay', () => {
describe('rendering', () => {
describe('if a predefined template variable is used in the query', () => {
it('should render the query maker with isQuerySupportedByExplorer as false', () => {
const queryText =
'SELECT mean(:fields:), mean("usage_user") AS "mean_usage_user" FROM "telegraf"."autogen"."cpu" WHERE time > :dashboardTime: GROUP BY time(:interval:) FILL(null)'
const {queryConfig} = query
const updatedQueryConfig = {...queryConfig, rawText: queryText}
const updatedQueries = [
{...query, query: queryText, queryConfig: updatedQueryConfig},
]
const updatedCell = {...cell, queries: updatedQueries}
const {wrapper} = setup({cell: updatedCell})
const queryMaker = wrapper.find(QueryMaker)
const activeQuery = queryMaker.prop('activeQuery')
expect(activeQuery.isQuerySupportedByExplorer).toBe(false)
})
})
})
})

330
ui/test/fixtures/index.ts vendored Normal file
View File

@ -0,0 +1,330 @@
import {
Source,
CellQuery,
SourceLinks,
Cell,
TimeRange,
Template,
} from 'src/types'
import {Axes, TableOptions, FieldName, DecimalPlaces} from 'src/types/dashboard'
import {ColorString, ColorNumber} from 'src/types/colors'
export const sourceLinks: SourceLinks = {
self: '/chronograf/v1/sources/4',
kapacitors: '/chronograf/v1/sources/4/kapacitors',
proxy: '/chronograf/v1/sources/4/proxy',
queries: '/chronograf/v1/sources/4/queries',
write: '/chronograf/v1/sources/4/write',
permissions: '/chronograf/v1/sources/4/permissions',
users: '/chronograf/v1/sources/4/users',
databases: '/chronograf/v1/sources/4/dbs',
annotations: '/chronograf/v1/sources/4/annotations',
health: '/chronograf/v1/sources/4/health',
}
export const source: Source = {
id: '4',
name: 'Influx 1',
type: 'influx',
url: 'http://localhost:8086',
default: false,
telegraf: 'telegraf',
organization: 'default',
role: 'viewer',
defaultRP: '',
links: sourceLinks,
insecureSkipVerify: false,
}
export const query: CellQuery = {
query:
'SELECT mean("usage_idle") AS "mean_usage_idle", mean("usage_user") AS "mean_usage_user" FROM "telegraf"."autogen"."cpu" WHERE time > :dashboardTime: GROUP BY time(:interval:) FILL(null)',
queryConfig: {
database: 'telegraf',
measurement: 'cpu',
retentionPolicy: 'autogen',
fields: [
{
value: 'mean',
type: 'func',
alias: 'mean_usage_idle',
args: [
{
value: 'usage_idle',
type: 'field',
alias: '',
},
],
},
{
value: 'mean',
type: 'func',
alias: 'mean_usage_user',
args: [
{
value: 'usage_user',
type: 'field',
alias: '',
},
],
},
],
tags: {},
groupBy: {
time: 'auto',
tags: [],
},
areTagsAccepted: false,
fill: 'null',
rawText: null,
range: null,
shifts: null,
},
}
export const axes: Axes = {
x: {
bounds: ['', ''],
label: '',
prefix: '',
suffix: '',
base: '10',
scale: 'linear',
},
y: {
bounds: ['', ''],
label: '',
prefix: '',
suffix: '',
base: '10',
scale: 'linear',
},
}
export const fieldOptions: FieldName[] = [
{
internalName: 'time',
displayName: '',
visible: true,
},
]
export const tableOptions: TableOptions = {
verticalTimeAxis: true,
sortBy: {
internalName: 'time',
displayName: '',
visible: true,
},
wrapping: 'truncate',
fixFirstColumn: true,
}
export const lineColors: ColorString[] = [
{
id: '574fb0a3-0a26-44d7-8d71-d4981756acb1',
type: 'scale',
hex: '#31C0F6',
name: 'Nineteen Eighty Four',
value: '0',
},
{
id: '3b9750f9-d41d-4100-8ee6-bd2785237f35',
type: 'scale',
hex: '#A500A5',
name: 'Nineteen Eighty Four',
value: '0',
},
{
id: '8d39064f-8124-4967-ae22-ffe14e425781',
type: 'scale',
hex: '#FF7E27',
name: 'Nineteen Eighty Four',
value: '0',
},
]
export const decimalPlaces: DecimalPlaces = {
isEnforced: true,
digits: 4,
}
export const cell: Cell = {
id: '67435af2-17bf-4caa-a5fc-0dd1ffb40dab',
x: 0,
y: 0,
w: 8,
h: 4,
name: 'Untitled Line Graph',
queries: [query],
axes: axes,
type: 'line',
colors: lineColors,
legend: {},
tableOptions: tableOptions,
fieldOptions: fieldOptions,
timeFormat: 'MM/DD/YYYY HH:mm:ss',
decimalPlaces: decimalPlaces,
links: {
self:
'/chronograf/v1/dashboards/9/cells/67435af2-17bf-4caa-a5fc-0dd1ffb40dab',
},
}
export const fullTimeRange = {
dashboardID: 9,
defaultGroupBy: '10s',
seconds: 300,
inputValue: 'Past 5 minutes',
lower: 'now() - 5m',
upper: null,
menuOption: 'Past 5 minutes',
format: 'influxql',
}
export const timeRange: TimeRange = {
lower: 'now() - 5m',
upper: null,
}
export const userDefinedTemplateVariables: Template[] = [
{
tempVar: ':fields:',
values: [
{
selected: false,
value: 'usage_guest',
},
{
selected: false,
value: 'usage_guest_nice',
},
{
selected: true,
value: 'usage_idle',
},
{
selected: false,
value: 'usage_iowait',
},
{
selected: false,
value: 'usage_irq',
},
{
selected: false,
value: 'usage_nice',
},
{
selected: false,
value: 'usage_softirq',
},
{
selected: false,
value: 'usage_steal',
},
{
selected: false,
value: 'usage_system',
},
{
selected: false,
value: 'usage_user',
},
],
id: '2b8dca84-879c-4555-a7cf-97f2951f8643',
},
{
tempVar: ':measurements:',
values: [
{
selected: true,
value: 'cpu',
},
{
selected: false,
value: 'disk',
},
{
selected: false,
value: 'diskio',
},
{
selected: false,
value: 'mem',
},
{
selected: false,
value: 'processes',
},
{
selected: false,
value: 'swap',
},
{
selected: false,
value: 'system',
},
],
id: '18855209-12db-4619-9834-1d7eb643ae6e',
},
]
export const predefinedTemplateVariables: Template[] = [
{
id: 'dashtime',
tempVar: ':dashboardTime:',
values: [
{
value: 'now() - 5m',
selected: true,
},
],
},
{
id: 'upperdashtime',
tempVar: ':upperDashboardTime:',
values: [
{
value: 'now()',
selected: true,
},
],
},
{
id: 'interval',
tempVar: ':interval:',
values: [
{
value: '333',
selected: true,
},
],
},
]
export const thresholdsListColors: ColorNumber[] = [
{
type: 'text',
hex: '#00C9FF',
id: 'base',
name: 'laser',
value: -1000000000000000000,
},
]
export const gaugeColors: ColorNumber[] = [
{
type: 'min',
hex: '#00C9FF',
id: '0',
name: 'laser',
value: 0,
},
{
type: 'max',
hex: '#9394FF',
id: '1',
name: 'comet',
value: 100,
},
]

View File

@ -13,6 +13,7 @@ const setup = (override = {}) => {
onToggleTagAcceptance: () => {}, onToggleTagAcceptance: () => {},
query, query,
querySource: source, querySource: source,
isQuerySupportedByExplorer: true,
...override, ...override,
} }

View File

@ -23,6 +23,7 @@ const setup = (overrides = {}) => {
measurement: 'test', measurement: 'test',
numTagsActive: 3, numTagsActive: 3,
areTagsAccepted: true, areTagsAccepted: true,
isQuerySupportedByExplorer: true,
onChooseTag: () => {}, onChooseTag: () => {},
onGroupByTag: () => {}, onGroupByTag: () => {},
onAcceptReject: () => {}, onAcceptReject: () => {},

View File

@ -13,6 +13,7 @@ const setup = (override = {}) => {
onGroupByTag: () => {}, onGroupByTag: () => {},
query, query,
querySource: source, querySource: source,
isQuerySupportedByExplorer: true,
...override, ...override,
} }