Migrate CEO to typescript

pull/10616/head
Brandon Farmer 2018-04-16 11:50:30 -07:00
parent ada654cbe6
commit d34ea10d98
7 changed files with 343 additions and 241 deletions

View File

@ -0,0 +1,11 @@
import React, {ReactNode, SFC} from 'react'
interface Props {
children: ReactNode
}
const CEOBottom: SFC<Props> = ({children}) => (
<div className="overlay-technology--editor">{children}</div>
)
export default CEOBottom

View File

@ -1,5 +1,4 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import _ from 'lodash'
import uuid from 'uuid'
@ -9,13 +8,16 @@ import QueryMaker from 'src/dashboards/components/QueryMaker'
import Visualization from 'src/dashboards/components/Visualization'
import OverlayControls from 'src/dashboards/components/OverlayControls'
import DisplayOptions from 'src/dashboards/components/DisplayOptions'
import CEOBottom from 'src/dashboards/components/CEOBottom'
import * as queryModifiers from 'src/utils/queryTransitions'
import defaultQueryConfig from 'src/utils/defaultQueryConfig'
import {buildQuery} from 'utils/influxql'
import {getQueryConfig} from 'shared/apis'
import {buildQuery} from 'src/utils/influxql'
import {getQueryConfig} from 'src/shared/apis'
import {IS_STATIC_LEGEND} from 'src/shared/constants'
import {ColorString, ColorNumber} from 'src/types/colors'
import {nextSource} from 'src/dashboards/utils/sources'
import {
removeUnselectedTemplateValues,
@ -25,98 +27,237 @@ import {OVERLAY_TECHNOLOGY} from 'src/shared/constants/classNames'
import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from 'src/data_explorer/constants'
import {AUTO_GROUP_BY} from 'src/shared/constants'
import {getCellTypeColors} from 'src/dashboards/constants/cellEditor'
import {colorsStringSchema, colorsNumberSchema} from 'shared/schemas'
import {TimeRange, Source, Query} from 'src/types'
import {Status} from 'src/types/query'
import {Cell, CellQuery, Legend} from 'src/types/dashboard'
const staticLegend: Legend = {
type: 'static',
orientation: 'bottom',
}
interface Template {
tempVar: string
}
interface QueryStatus {
queryID: string
status: Status
}
interface Props {
sources: Source[]
editQueryStatus: () => void
onCancel: () => void
onSave: (cell: Cell) => void
source: Source
dashboardID: string
queryStatus: QueryStatus
autoRefresh: number
templates: Template[]
timeRange: TimeRange
thresholdsListType: string
thresholdsListColors: ColorNumber[]
gaugeColors: ColorNumber[]
lineColors: ColorString[]
cell: Cell
}
interface State {
queriesWorkingDraft: Query[]
activeQueryIndex: number
isDisplayOptionsTabActive: boolean
isStaticLegend: boolean
}
const createWorkingDraft = (source: string, query: CellQuery): Query => {
const {queryConfig} = query
const draft: Query = {
...queryConfig,
id: uuid.v4(),
source,
}
return draft
}
const createWorkingDrafts = (source: string, queries: CellQuery[]): Query[] =>
_.cloneDeep(
queries.map((query: CellQuery) => createWorkingDraft(source, query))
)
class CellEditorOverlay extends Component<Props, State> {
private overlayRef: HTMLDivElement
private formattedSources = this.props.sources.map(s => ({
...s,
text: `${s.name} @ ${s.url}`,
}))
class CellEditorOverlay extends Component {
constructor(props) {
super(props)
const {cell: {queries, legend}, sources} = props
let source = _.get(queries, ['0', 'source'], null)
source = sources.find(s => s.links.self === source) || props.source
const queriesWorkingDraft = _.cloneDeep(
queries.map(({queryConfig}) => ({
...queryConfig,
id: uuid.v4(),
source,
}))
)
const {cell: {queries, legend}} = props
const queriesWorkingDraft = createWorkingDrafts(this.sourceLink, queries)
this.state = {
queriesWorkingDraft,
activeQueryIndex: 0,
isDisplayOptionsTabActive: false,
staticLegend: IS_STATIC_LEGEND(legend),
isStaticLegend: IS_STATIC_LEGEND(legend),
}
}
componentWillReceiveProps(nextProps) {
public componentWillReceiveProps(nextProps: Props) {
const {status, queryID} = this.props.queryStatus
const nextStatus = nextProps.queryStatus
if (nextStatus.status && nextStatus.queryID) {
if (nextStatus.queryID !== queryID || nextStatus.status !== status) {
const nextQueries = this.state.queriesWorkingDraft.map(
q => (q.id === queryID ? {...q, status: nextStatus.status} : q)
)
this.setState({queriesWorkingDraft: nextQueries})
}
const {queriesWorkingDraft} = this.state
const {queryStatus} = nextProps
if (
queryStatus.status &&
queryStatus.queryID &&
(queryStatus.queryID !== queryID || queryStatus.status !== status)
) {
const nextQueries = queriesWorkingDraft.map(
q => (q.id === queryID ? {...q, status: queryStatus.status} : q)
)
this.setState({queriesWorkingDraft: nextQueries})
}
}
componentDidMount = () => {
public componentDidMount() {
this.overlayRef.focus()
}
queryStateReducer = queryModifier => (queryID, ...payload) => {
public render() {
const {
onCancel,
templates,
timeRange,
autoRefresh,
editQueryStatus,
} = this.props
const {
activeQueryIndex,
isDisplayOptionsTabActive,
queriesWorkingDraft,
isStaticLegend,
} = this.state
return (
<div
className={OVERLAY_TECHNOLOGY}
onKeyDown={this.handleKeyDown}
tabIndex={0}
ref={this.onRef}
>
<ResizeContainer
containerClass="resizer--full-size"
minTopHeight={MINIMUM_HEIGHTS.visualization}
minBottomHeight={MINIMUM_HEIGHTS.queryMaker}
initialTopHeight={INITIAL_HEIGHTS.visualization}
initialBottomHeight={INITIAL_HEIGHTS.queryMaker}
>
<Visualization
timeRange={timeRange}
templates={templates}
autoRefresh={autoRefresh}
queryConfigs={queriesWorkingDraft}
editQueryStatus={editQueryStatus}
staticLegend={isStaticLegend}
/>
<CEOBottom>
<OverlayControls
onCancel={onCancel}
queries={queriesWorkingDraft}
sources={this.formattedSources}
onSave={this.handleSaveCell}
selected={this.findSelectedSource()}
onSetQuerySource={this.handleSetQuerySource}
isSavable={this.isSaveable}
isDisplayOptionsTabActive={isDisplayOptionsTabActive}
onClickDisplayOptions={this.handleClickDisplayOptionsTab}
/>
{isDisplayOptionsTabActive ? (
<DisplayOptions
queryConfigs={queriesWorkingDraft}
onToggleStaticLegend={this.handleToggleStaticLegend}
staticLegend={isStaticLegend}
onResetFocus={this.handleResetFocus}
/>
) : (
<QueryMaker
source={this.source}
templates={templates}
queries={queriesWorkingDraft}
actions={this.queryActions}
timeRange={timeRange}
onDeleteQuery={this.handleDeleteQuery}
onAddQuery={this.handleAddQuery}
activeQueryIndex={activeQueryIndex}
activeQuery={this.getActiveQuery()}
setActiveQueryIndex={this.handleSetActiveQueryIndex}
initialGroupByTime={AUTO_GROUP_BY}
/>
)}
</CEOBottom>
</ResizeContainer>
</div>
)
}
private onRef = (r: HTMLDivElement) => {
this.overlayRef = r
}
private queryStateReducer = queryModifier => (queryID, ...payload) => {
const {queriesWorkingDraft} = this.state
const query = queriesWorkingDraft.find(q => q.id === queryID)
const nextQuery = queryModifier(query, ...payload)
const nextQueries = queriesWorkingDraft.map(
q =>
q.id === query.id
? {...nextQuery, source: this.nextSource(q, nextQuery)}
: q
)
const nextQueries = queriesWorkingDraft.map(q => {
if (q.id === query.id) {
return {...nextQuery, source: nextSource(q, nextQuery)}
}
return q
})
this.setState({queriesWorkingDraft: nextQueries})
}
handleAddQuery = () => {
private handleAddQuery = () => {
const {queriesWorkingDraft} = this.state
const newIndex = queriesWorkingDraft.length
this.setState({
queriesWorkingDraft: [
...queriesWorkingDraft,
defaultQueryConfig({id: uuid.v4()}),
{...defaultQueryConfig({id: uuid.v4()}), source: null},
],
})
this.handleSetActiveQueryIndex(newIndex)
}
handleDeleteQuery = index => {
const nextQueries = this.state.queriesWorkingDraft.filter(
(__, i) => i !== index
)
private handleDeleteQuery = index => {
const {queriesWorkingDraft} = this.state
const nextQueries = queriesWorkingDraft.filter((__, i) => i !== index)
this.setState({queriesWorkingDraft: nextQueries})
}
handleSaveCell = () => {
const {queriesWorkingDraft, staticLegend} = this.state
private handleSaveCell = () => {
const {queriesWorkingDraft, isStaticLegend} = this.state
const {cell, thresholdsListColors, gaugeColors, lineColors} = this.props
const queries = queriesWorkingDraft.map(q => {
const timeRange = q.range || {upper: null, lower: ':dashboardTime:'}
const query = q.rawText || buildQuery(TYPE_QUERY_CONFIG, timeRange, q)
return {
queryConfig: q,
query,
source: _.get(q, ['source', 'links', 'self'], null),
query: q.rawText || buildQuery(TYPE_QUERY_CONFIG, timeRange, q),
source: q.source,
}
})
@ -131,28 +272,23 @@ class CellEditorOverlay extends Component {
...cell,
queries,
colors,
legend: staticLegend
? {
type: 'static',
orientation: 'bottom',
}
: {},
legend: isStaticLegend ? staticLegend : {},
})
}
handleClickDisplayOptionsTab = isDisplayOptionsTabActive => () => {
private handleClickDisplayOptionsTab = isDisplayOptionsTabActive => () => {
this.setState({isDisplayOptionsTabActive})
}
handleSetActiveQueryIndex = activeQueryIndex => {
private handleSetActiveQueryIndex = activeQueryIndex => {
this.setState({activeQueryIndex})
}
handleToggleStaticLegend = staticLegend => () => {
this.setState({staticLegend})
private handleToggleStaticLegend = isStaticLegend => () => {
this.setState({isStaticLegend})
}
handleSetQuerySource = source => {
private handleSetQuerySource = source => {
const queriesWorkingDraft = this.state.queriesWorkingDraft.map(q => ({
..._.cloneDeep(q),
source,
@ -161,15 +297,13 @@ class CellEditorOverlay extends Component {
this.setState({queriesWorkingDraft})
}
getActiveQuery = () => {
private getActiveQuery = () => {
const {queriesWorkingDraft, activeQueryIndex} = this.state
const activeQuery = queriesWorkingDraft[activeQueryIndex]
const defaultQuery = queriesWorkingDraft[0]
return activeQuery || defaultQuery
return _.get(queriesWorkingDraft, activeQueryIndex, queriesWorkingDraft[0])
}
handleEditRawText = async (url, id, text) => {
private handleEditRawText = async (url, id, text) => {
const templates = removeUnselectedTemplateValues(this.props.templates)
// use this as the handler passed into fetchTimeSeries to update a query status
@ -185,203 +319,86 @@ class CellEditorOverlay extends Component {
}
}
formatSources = this.props.sources.map(s => ({
...s,
text: `${s.name} @ ${s.url}`,
}))
findSelectedSource = () => {
private findSelectedSource = () => {
const {source} = this.props
const sources = this.formatSources
const query = _.get(this.state.queriesWorkingDraft, 0, false)
const sources = this.formattedSources
const currentSource = _.get(this.state.queriesWorkingDraft, '0.source')
if (!query || !query.source) {
if (!currentSource) {
const defaultSource = sources.find(s => s.id === source.id)
return (defaultSource && defaultSource.text) || 'No sources'
}
const selected = sources.find(s => s.id === query.source.id)
const selected = sources.find(s => s.links.self === currentSource)
return (selected && selected.text) || 'No sources'
}
getSource = () => {
const {source, sources} = this.props
const query = _.get(this.state.queriesWorkingDraft, 0, false)
private handleKeyDown = e => {
switch (e.key) {
case 'Enter':
if (!e.metaKey) {
return
} else if (e.target === this.overlayRef) {
this.handleSaveCell()
} else {
e.target.blur()
setTimeout(this.handleSaveCell, 50)
}
break
case 'Escape':
if (e.target === this.overlayRef) {
this.props.onCancel()
} else {
const targetIsDropdown = e.target.classList[0] === 'dropdown'
const targetIsButton = e.target.tagName === 'BUTTON'
if (!query || !query.source) {
return source
}
if (targetIsDropdown || targetIsButton) {
return this.props.onCancel()
}
const querySource = sources.find(s => s.id === query.source.id)
return querySource || source
}
nextSource = (prevQuery, nextQuery) => {
if (nextQuery.source) {
return nextQuery.source
}
return prevQuery.source
}
handleKeyDown = e => {
if (e.key === 'Enter' && e.metaKey && e.target === this.overlayRef) {
this.handleSaveCell()
}
if (e.key === 'Enter' && e.metaKey && e.target !== this.overlayRef) {
e.target.blur()
setTimeout(this.handleSaveCell, 50)
}
if (e.key === 'Escape' && e.target === this.overlayRef) {
this.props.onCancel()
}
if (e.key === 'Escape' && e.target !== this.overlayRef) {
const targetIsDropdown = e.target.classList[0] === 'dropdown'
const targetIsButton = e.target.tagName === 'BUTTON'
if (targetIsDropdown || targetIsButton) {
return this.props.onCancel()
}
e.target.blur()
this.overlayRef.focus()
e.target.blur()
this.overlayRef.focus()
}
break
}
}
handleResetFocus = () => {
private handleResetFocus = () => {
this.overlayRef.focus()
}
render() {
const {
onCancel,
templates,
timeRange,
autoRefresh,
editQueryStatus,
} = this.props
private get isSaveable(): boolean {
const {queriesWorkingDraft} = this.state
const {
activeQueryIndex,
isDisplayOptionsTabActive,
queriesWorkingDraft,
staticLegend,
} = this.state
return queriesWorkingDraft.every(
(query: Query) =>
(!!query.measurement && !!query.database && !!query.fields.length) ||
!!query.rawText
)
}
const queryActions = {
private get queryActions() {
return {
editRawTextAsync: this.handleEditRawText,
..._.mapValues(queryModifiers, qm => this.queryStateReducer(qm)),
..._.mapValues(queryModifiers, this.queryStateReducer),
}
}
private get sourceLink(): string {
const {cell: {queries}, source: {links}} = this.props
return _.get(queries, '0.source.links.self', links.self)
}
private get source() {
const {source, sources} = this.props
const query = _.get(this.state.queriesWorkingDraft, 0, {source: null})
if (!query.source) {
return source
}
const isQuerySavable = query =>
(!!query.measurement && !!query.database && !!query.fields.length) ||
!!query.rawText
return (
<div
className={OVERLAY_TECHNOLOGY}
onKeyDown={this.handleKeyDown}
tabIndex="0"
ref={r => (this.overlayRef = r)}
>
<ResizeContainer
containerClass="resizer--full-size"
minTopHeight={MINIMUM_HEIGHTS.visualization}
minBottomHeight={MINIMUM_HEIGHTS.queryMaker}
initialTopHeight={INITIAL_HEIGHTS.visualization}
initialBottomHeight={INITIAL_HEIGHTS.queryMaker}
>
<Visualization
timeRange={timeRange}
templates={templates}
autoRefresh={autoRefresh}
queryConfigs={queriesWorkingDraft}
editQueryStatus={editQueryStatus}
staticLegend={staticLegend}
/>
<CEOBottom>
<OverlayControls
onCancel={onCancel}
queries={queriesWorkingDraft}
sources={this.formatSources}
onSave={this.handleSaveCell}
selected={this.findSelectedSource()}
onSetQuerySource={this.handleSetQuerySource}
isSavable={queriesWorkingDraft.every(isQuerySavable)}
isDisplayOptionsTabActive={isDisplayOptionsTabActive}
onClickDisplayOptions={this.handleClickDisplayOptionsTab}
/>
{isDisplayOptionsTabActive ? (
<DisplayOptions
queryConfigs={queriesWorkingDraft}
onToggleStaticLegend={this.handleToggleStaticLegend}
staticLegend={staticLegend}
onResetFocus={this.handleResetFocus}
/>
) : (
<QueryMaker
source={this.getSource()}
templates={templates}
queries={queriesWorkingDraft}
actions={queryActions}
autoRefresh={autoRefresh}
timeRange={timeRange}
onDeleteQuery={this.handleDeleteQuery}
onAddQuery={this.handleAddQuery}
activeQueryIndex={activeQueryIndex}
activeQuery={this.getActiveQuery()}
setActiveQueryIndex={this.handleSetActiveQueryIndex}
initialGroupByTime={AUTO_GROUP_BY}
/>
)}
</CEOBottom>
</ResizeContainer>
</div>
)
return sources.find(s => s.links.self === query.source) || source
}
}
const CEOBottom = ({children}) => (
<div className="overlay-technology--editor">{children}</div>
)
const {arrayOf, func, node, number, shape, string} = PropTypes
CellEditorOverlay.propTypes = {
onCancel: func.isRequired,
onSave: func.isRequired,
cell: shape({}).isRequired,
templates: arrayOf(
shape({
tempVar: string.isRequired,
})
).isRequired,
timeRange: shape({
upper: string,
lower: string,
}).isRequired,
autoRefresh: number.isRequired,
source: shape({
links: shape({
proxy: string.isRequired,
queries: string.isRequired,
}).isRequired,
}).isRequired,
editQueryStatus: func.isRequired,
queryStatus: shape({
queryID: string,
status: shape({}),
}).isRequired,
dashboardID: string.isRequired,
sources: arrayOf(shape()),
thresholdsListType: string.isRequired,
thresholdsListColors: colorsNumberSchema.isRequired,
gaugeColors: colorsNumberSchema.isRequired,
lineColors: colorsStringSchema.isRequired,
}
CEOBottom.propTypes = {
children: node,
}
export default CellEditorOverlay

View File

@ -0,0 +1,7 @@
export const nextSource = (prevQuery, nextQuery) => {
if (nextQuery.source) {
return nextQuery.source
}
return prevQuery.source
}

9
ui/src/types/colors.ts Normal file
View File

@ -0,0 +1,9 @@
interface ColorBase {
type: string
hex: string
id: string
name: string
}
export type ColorString = ColorBase & {value: string}
export type ColorNumber = ColorBase & {value: number}

60
ui/src/types/dashboard.ts Normal file
View File

@ -0,0 +1,60 @@
import {Query} from 'src/types'
import {ColorString} from 'src/types/colors'
interface Axis {
bounds: [string, string]
label: string
prefix: string
suffix: string
base: string
scale: string
}
interface Axes {
x: Axis
y: Axis
}
interface FieldName {
internalName: string
displayName: string
visible: boolean
}
interface TableOptions {
timeFormat: string
verticalTimeAxis: boolean
sortBy: FieldName
wrapping: string
fixFirstColumn: boolean
fieldNames: FieldName[]
}
interface CellLinks {
self: string
}
export interface CellQuery {
query: string
queryConfig: Query
}
export interface Legend {
type?: string
orientation?: string
}
export interface Cell {
id: string
x: number
y: number
w: number
h: number
name: string
queries: CellQuery[]
type: string
axes: Axes
colors: ColorString[]
tableOptions: TableOptions
links: CellLinks
legend: Legend
}

View File

@ -64,7 +64,7 @@ export interface DurationRange {
upper?: string
}
interface TimeShift {
export interface TimeShift {
label: string
unit: string
quantity: string

View File

@ -1,3 +1,4 @@
import _ from 'lodash'
import {buildQuery} from 'utils/influxql'
import {TYPE_QUERY_CONFIG, TYPE_SHIFTED} from 'src/dashboards/constants'
@ -26,10 +27,7 @@ const buildQueries = (proxy, queryConfigs, tR) => {
const queries = statements
.filter(s => s.text !== null)
.map(({queryConfig, text, id}) => {
let queryProxy = ''
if (queryConfig.source) {
queryProxy = `${queryConfig.source.links.proxy}`
}
const queryProxy = _.get(queryConfig, 'source.links.proxy', '')
const host = [queryProxy || proxy]