From 4aaa3d97ba712c57de021c2b5cfca975885d07f1 Mon Sep 17 00:00:00 2001 From: Brandon Farmer Date: Mon, 16 Apr 2018 11:50:30 -0700 Subject: [PATCH] Migrate CEO to typescript --- ui/src/dashboards/components/CEOBottom.tsx | 11 + ...EditorOverlay.js => CellEditorOverlay.tsx} | 489 +++++++++--------- ui/src/dashboards/utils/sources.ts | 7 + ui/src/types/colors.ts | 9 + ui/src/types/dashboard.ts | 60 +++ ui/src/types/query.ts | 2 +- ui/src/utils/buildQueriesForGraphs.js | 6 +- 7 files changed, 343 insertions(+), 241 deletions(-) create mode 100644 ui/src/dashboards/components/CEOBottom.tsx rename ui/src/dashboards/components/{CellEditorOverlay.js => CellEditorOverlay.tsx} (50%) create mode 100644 ui/src/dashboards/utils/sources.ts create mode 100644 ui/src/types/colors.ts create mode 100644 ui/src/types/dashboard.ts diff --git a/ui/src/dashboards/components/CEOBottom.tsx b/ui/src/dashboards/components/CEOBottom.tsx new file mode 100644 index 000000000..9dbd22516 --- /dev/null +++ b/ui/src/dashboards/components/CEOBottom.tsx @@ -0,0 +1,11 @@ +import React, {ReactNode, SFC} from 'react' + +interface Props { + children: ReactNode +} + +const CEOBottom: SFC = ({children}) => ( +
{children}
+) + +export default CEOBottom diff --git a/ui/src/dashboards/components/CellEditorOverlay.js b/ui/src/dashboards/components/CellEditorOverlay.tsx similarity index 50% rename from ui/src/dashboards/components/CellEditorOverlay.js rename to ui/src/dashboards/components/CellEditorOverlay.tsx index 3c8eafaa8..a1e240c5a 100644 --- a/ui/src/dashboards/components/CellEditorOverlay.js +++ b/ui/src/dashboards/components/CellEditorOverlay.tsx @@ -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 { + 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 ( +
+ + + + + {isDisplayOptionsTabActive ? ( + + ) : ( + + )} + + +
+ ) + } + + 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 ( -
(this.overlayRef = r)} - > - - - - - {isDisplayOptionsTabActive ? ( - - ) : ( - - )} - - -
- ) + return sources.find(s => s.links.self === query.source) || source } } -const CEOBottom = ({children}) => ( -
{children}
-) - -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 diff --git a/ui/src/dashboards/utils/sources.ts b/ui/src/dashboards/utils/sources.ts new file mode 100644 index 000000000..e76364bfb --- /dev/null +++ b/ui/src/dashboards/utils/sources.ts @@ -0,0 +1,7 @@ +export const nextSource = (prevQuery, nextQuery) => { + if (nextQuery.source) { + return nextQuery.source + } + + return prevQuery.source +} diff --git a/ui/src/types/colors.ts b/ui/src/types/colors.ts new file mode 100644 index 000000000..0d9e92448 --- /dev/null +++ b/ui/src/types/colors.ts @@ -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} diff --git a/ui/src/types/dashboard.ts b/ui/src/types/dashboard.ts new file mode 100644 index 000000000..42ecd5402 --- /dev/null +++ b/ui/src/types/dashboard.ts @@ -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 +} diff --git a/ui/src/types/query.ts b/ui/src/types/query.ts index 3c4971956..d3fee8b31 100644 --- a/ui/src/types/query.ts +++ b/ui/src/types/query.ts @@ -64,7 +64,7 @@ export interface DurationRange { upper?: string } -interface TimeShift { +export interface TimeShift { label: string unit: string quantity: string diff --git a/ui/src/utils/buildQueriesForGraphs.js b/ui/src/utils/buildQueriesForGraphs.js index 3031f9cff..6b183c7a4 100644 --- a/ui/src/utils/buildQueriesForGraphs.js +++ b/ui/src/utils/buildQueriesForGraphs.js @@ -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]