chronograf(v2-views): Introduce new view patterns, linting, and TypeScript 3.x (#901)

* Simplifiy color type

* Fix type

* Introduce V2 data shape for views

* WIP Split style and parsing to separate Dygraph components

* Add basic dygraph view types

* Add Gauge to v2 view shapes

* Upgrade TypeScript to ^3.0

* Add tsc to Circle build

* Fix Dygraph component paths

* Add testURL to jest config

* Upgrade lodash types

* Remove redundant test linter stetp

* Upgrade to TypeScript ^3

* Remove TableGraph (temporarily)
pull/10616/head
Andrew Watkins 2018-09-27 10:46:48 -07:00 committed by GitHub
parent 6a86f8ee14
commit e34d2e76ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 3798 additions and 4701 deletions

View File

@ -22,6 +22,7 @@ jobs:
- ~/.cache/yarn - ~/.cache/yarn
- run: make test-js - run: make test-js
- run: make chronograf_lint
gotest: gotest:
docker: docker:

View File

@ -91,6 +91,9 @@ bin/$(GOOS)/go-bindata: go.mod go.sum
node_modules: chronograf/ui/node_modules node_modules: chronograf/ui/node_modules
chronograf_lint:
make -C chronograf/ui lint
chronograf/ui/node_modules: chronograf/ui/node_modules:
make -C chronograf/ui node_modules make -C chronograf/ui node_modules

View File

@ -17,6 +17,14 @@ else
yarn run build yarn run build
endif endif
lint: node_modules $(UISOURCES)
ifndef YARN
$(error Please install yarn 0.19.1+)
else
yarn run tsc
endif
test: test:
ifndef YARN ifndef YARN
$(error Please install yarn 0.19.1+) $(error Please install yarn 0.19.1+)
@ -34,4 +42,4 @@ endif
run: run:
yarn run start yarn run start
.PHONY: all clean test run .PHONY: all clean test run lint

1420
chronograf/ui/index.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@ module.exports = {
projects: [ projects: [
{ {
displayName: 'test', displayName: 'test',
testURL: 'http://localhost',
testPathIgnorePatterns: [ testPathIgnorePatterns: [
'build', 'build',
'<rootDir>/node_modules/(?!(jest-test))', '<rootDir>/node_modules/(?!(jest-test))',
@ -26,11 +27,5 @@ module.exports = {
displayName: 'eslint', displayName: 'eslint',
testMatch: ['<rootDir>/**/*.test.js'], testMatch: ['<rootDir>/**/*.test.js'],
}, },
{
runner: 'jest-runner-tslint',
displayName: 'tslint',
moduleFileExtensions: ['ts', 'tsx'],
testMatch: ['<rootDir>/**/*.test.ts', '<rootDir>/**/*.test.tsx'],
},
], ],
} }

View File

@ -31,14 +31,14 @@
"@types/d3-color": "^1.2.1", "@types/d3-color": "^1.2.1",
"@types/d3-scale": "^2.0.1", "@types/d3-scale": "^2.0.1",
"@types/dygraphs": "^1.1.6", "@types/dygraphs": "^1.1.6",
"@types/enzyme": "^3.1.9", "@types/enzyme": "^3.1.14",
"@types/jest": "^22.1.4", "@types/jest": "^23.3.2",
"@types/lodash": "^4.14.104", "@types/lodash": "^4.14.116",
"@types/node": "^9.4.6", "@types/node": "^9.4.6",
"@types/papaparse": "^4.1.34", "@types/papaparse": "^4.1.34",
"@types/prop-types": "^15.5.2", "@types/prop-types": "^15.5.2",
"@types/qs": "^6.5.1", "@types/qs": "^6.5.1",
"@types/react": "^16.0.38", "@types/react": "^16.4.14",
"@types/react-dnd": "^2.0.36", "@types/react-dnd": "^2.0.36",
"@types/react-dnd-html5-backend": "^2.1.9", "@types/react-dnd-html5-backend": "^2.1.9",
"@types/react-grid-layout": "^0.16.5", "@types/react-grid-layout": "^0.16.5",
@ -63,7 +63,7 @@
"babel-preset-react": "^6.5.0", "babel-preset-react": "^6.5.0",
"babel-preset-stage-0": "^6.16.0", "babel-preset-stage-0": "^6.16.0",
"babel-runtime": "^6.5.0", "babel-runtime": "^6.5.0",
"enzyme": "^3.3.0", "enzyme": "^3.6.0",
"enzyme-to-json": "^3.3.4", "enzyme-to-json": "^3.3.4",
"eslint": "^3.14.1", "eslint": "^3.14.1",
"eslint-config-prettier": "^2.9.0", "eslint-config-prettier": "^2.9.0",
@ -74,20 +74,20 @@
"express": "^4.14.0", "express": "^4.14.0",
"http-proxy-middleware": "^0.18.0", "http-proxy-middleware": "^0.18.0",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^23.1.0", "jest": "^23.6.0",
"jest-runner-eslint": "^0.6.0", "jest-runner-eslint": "^0.6.0",
"jest-runner-tslint": "^1.0.4", "jest-runner-tslint": "^1.0.4",
"jsdom": "^9.0.0", "jsdom": "^9.0.0",
"node-sass": "^4.9.3", "node-sass": "^4.9.3",
"parcel": "^1.9.7", "parcel": "^1.9.7",
"prettier": "1.12.1", "prettier": "1.12.1",
"ts-jest": "^22.4.2", "ts-jest": "^23.10.2",
"tslib": "^1.9.0", "tslib": "^1.9.0",
"tslint": "^5.9.1", "tslint": "^5.9.1",
"tslint-config-prettier": "^1.10.0", "tslint-config-prettier": "^1.10.0",
"tslint-plugin-prettier": "^1.3.0", "tslint-plugin-prettier": "^1.3.0",
"tslint-react": "^3.5.1", "tslint-react": "^3.5.1",
"typescript": "2.7.2" "typescript": "^3.0.3"
}, },
"dependencies": { "dependencies": {
"axios": "^0.18.0", "axios": "^0.18.0",

View File

@ -40,7 +40,7 @@ interface Props {
notify: (message: Notification | NotificationFunc) => void notify: (message: Notification | NotificationFunc) => void
} }
export const SourceContext = React.createContext() 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.

View File

@ -1,4 +1,4 @@
import {ColorNumber, ColorString} from 'src/types/colors' import {Color} from 'src/types/colors'
import { import {
DecimalPlaces, DecimalPlaces,
FieldOption, FieldOption,
@ -68,7 +68,7 @@ export interface RenameCellAction {
export interface UpdateThresholdsListColorsAction { export interface UpdateThresholdsListColorsAction {
type: ActionType.UpdateThresholdsListColors type: ActionType.UpdateThresholdsListColors
payload: { payload: {
thresholdsListColors: ColorNumber[] thresholdsListColors: Color[]
} }
} }
@ -82,7 +82,7 @@ export interface UpdateThresholdsListTypeAction {
export interface UpdateGaugeColorsAction { export interface UpdateGaugeColorsAction {
type: ActionType.UpdateGaugeColors type: ActionType.UpdateGaugeColors
payload: { payload: {
gaugeColors: ColorNumber[] gaugeColors: Color[]
} }
} }
@ -103,7 +103,7 @@ export interface UpdateTableOptionsAction {
export interface UpdateLineColorsAction { export interface UpdateLineColorsAction {
type: ActionType.UpdateLineColors type: ActionType.UpdateLineColors
payload: { payload: {
lineColors: ColorString[] lineColors: Color[]
} }
} }
@ -156,7 +156,7 @@ export const renameCell = (cellName: string): RenameCellAction => ({
}) })
export const updateThresholdsListColors = ( export const updateThresholdsListColors = (
thresholdsListColors: ColorNumber[] thresholdsListColors: Color[]
): UpdateThresholdsListColorsAction => ({ ): UpdateThresholdsListColorsAction => ({
type: ActionType.UpdateThresholdsListColors, type: ActionType.UpdateThresholdsListColors,
payload: { payload: {
@ -174,7 +174,7 @@ export const updateThresholdsListType = (
}) })
export const updateGaugeColors = ( export const updateGaugeColors = (
gaugeColors: ColorNumber[] gaugeColors: Color[]
): UpdateGaugeColorsAction => ({ ): UpdateGaugeColorsAction => ({
type: ActionType.UpdateGaugeColors, type: ActionType.UpdateGaugeColors,
payload: { payload: {
@ -199,7 +199,7 @@ export const updateTableOptions = (
}) })
export const updateLineColors = ( export const updateLineColors = (
lineColors: ColorString[] lineColors: Color[]
): UpdateLineColorsAction => ({ ): UpdateLineColorsAction => ({
type: ActionType.UpdateLineColors, type: ActionType.UpdateLineColors,
payload: { payload: {

View File

@ -23,16 +23,16 @@ import {
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
import {Axes} from 'src/types' import {Axes} from 'src/types'
import {DecimalPlaces} from 'src/types/dashboards' import {DecimalPlaces} from 'src/types/dashboards'
import {ColorNumber} from 'src/types/colors' import {Color} from 'src/types/colors'
interface Props { interface Props {
axes: Axes axes: Axes
gaugeColors: ColorNumber[] gaugeColors: Color[]
decimalPlaces: DecimalPlaces decimalPlaces: DecimalPlaces
onResetFocus: () => void onResetFocus: () => void
handleUpdateAxes: (a: Axes) => void handleUpdateAxes: (a: Axes) => void
onUpdateDecimalPlaces: (d: DecimalPlaces) => void onUpdateDecimalPlaces: (d: DecimalPlaces) => void
handleUpdateGaugeColors: (d: ColorNumber[]) => void handleUpdateGaugeColors: (d: Color[]) => void
} }
@ErrorHandling @ErrorHandling
@ -125,8 +125,8 @@ class GaugeOptions extends PureComponent<Props> {
if (sortedColors.length <= MAX_THRESHOLDS) { if (sortedColors.length <= MAX_THRESHOLDS) {
const randomColor = _.random(0, THRESHOLD_COLORS.length - 1) const randomColor = _.random(0, THRESHOLD_COLORS.length - 1)
const maxValue = sortedColors[sortedColors.length - 1].value const maxValue = +sortedColors[sortedColors.length - 1].value
const minValue = sortedColors[0].value const minValue = +sortedColors[0].value
const colorsValues = _.mapValues(gaugeColors, 'value') const colorsValues = _.mapValues(gaugeColors, 'value')
let randomValue let randomValue
@ -143,7 +143,7 @@ class GaugeOptions extends PureComponent<Props> {
name: THRESHOLD_COLORS[randomColor].name, name: THRESHOLD_COLORS[randomColor].name,
} }
const updatedColors: ColorNumber[] = _.sortBy<ColorNumber>( const updatedColors: Color[] = _.sortBy<Color>(
[...gaugeColors, newThreshold], [...gaugeColors, newThreshold],
color => color.value color => color.value
) )

View File

@ -1,61 +0,0 @@
import React, {SFC} from 'react'
import ConfirmOrCancel from 'src/shared/components/ConfirmOrCancel'
import SourceSelector from 'src/dashboards/components/SourceSelector'
import RadioButtons from 'src/clockface/components/radio_buttons/RadioButtons'
import {ButtonShape} from 'src/clockface/types'
import {CEOTabs} from 'src/dashboards/constants'
import * as QueriesModels from 'src/types/queries'
import * as SourcesModels from 'src/types/sources'
interface Props {
onCancel: () => void
onSave: () => void
activeEditorTab: CEOTabs
onSetActiveEditorTab: (tabName: CEOTabs) => void
isSavable: boolean
sources: SourcesModels.SourceOption[]
onSetQuerySource: (source: SourcesModels.Source) => void
selected: string
queries: QueriesModels.QueryConfig[]
}
const OverlayControls: SFC<Props> = ({
onSave,
sources,
queries,
selected,
onCancel,
isSavable,
onSetQuerySource,
activeEditorTab,
onSetActiveEditorTab,
}) => (
<div className="overlay-controls">
<SourceSelector
sources={sources}
selected={selected}
onSetQuerySource={onSetQuerySource}
queries={queries}
/>
<div className="overlay-controls--tabs">
<RadioButtons
activeButton={activeEditorTab}
buttons={[CEOTabs.Queries, CEOTabs.Vis]}
onChange={onSetActiveEditorTab}
shape={ButtonShape.StretchToFit}
/>
</div>
<div className="overlay-controls--right">
<ConfirmOrCancel
onCancel={onCancel}
onConfirm={onSave}
isDisabled={!isSavable}
/>
</div>
</div>
)
export default OverlayControls

View File

@ -12,7 +12,7 @@ describe('Threshold', () => {
hex: '#444444', hex: '#444444',
id: 'id', id: 'id',
name: 'name', name: 'name',
value: 2, value: '2',
}, },
disableMaxColor: true, disableMaxColor: true,
onChooseColor: () => {}, onChooseColor: () => {},

View File

@ -3,16 +3,16 @@ import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react'
import ColorDropdown from 'src/shared/components/ColorDropdown' import ColorDropdown from 'src/shared/components/ColorDropdown'
import {THRESHOLD_COLORS} from 'src/shared/constants/thresholds' import {THRESHOLD_COLORS} from 'src/shared/constants/thresholds'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
import {ColorNumber, ThresholdColor} from 'src/types/colors' import {Color, ThresholdColor} from 'src/types/colors'
interface Props { interface Props {
visualizationType: string visualizationType: string
threshold: ColorNumber threshold: Color
disableMaxColor: boolean disableMaxColor: boolean
onChooseColor: (threshold: ColorNumber) => void onChooseColor: (threshold: Color) => void
onValidateColorValue: (threshold: ColorNumber, targetValue: number) => boolean onValidateColorValue: (threshold: Color, targetValue: number) => boolean
onUpdateColorValue: (threshold: ColorNumber, targetValue: number) => void onUpdateColorValue: (threshold: Color, targetValue: number) => void
onDeleteThreshold: (threshold: ColorNumber) => void onDeleteThreshold: (threshold: Color) => void
isMin: boolean isMin: boolean
isMax: boolean isMax: boolean
} }
@ -76,7 +76,7 @@ class Threshold extends PureComponent<Props, State> {
onChooseColor({...threshold, hex, name}) onChooseColor({...threshold, hex, name})
} }
private get selectedColor(): ColorNumber { private get selectedColor(): Color {
const { const {
threshold: {hex, name, type, value, id}, threshold: {hex, name, type, value, id},
} = this.props } = this.props

View File

@ -1,112 +0,0 @@
import React, {SFC} from 'react'
import {connect} from 'react-redux'
import RefreshingGraph from 'src/shared/components/RefreshingGraph'
import buildQueries from 'src/utils/buildQueriesForGraphs'
import VisualizationName from 'src/dashboards/components/VisualizationName'
import {getCellTypeColors} from 'src/dashboards/constants/cellEditor'
import {TimeRange, QueryConfig, Axes, Template, Source} from 'src/types'
import {
TableOptions,
DecimalPlaces,
FieldOption,
CellType,
} from 'src/types/dashboards'
import {ColorString, ColorNumber} from 'src/types/colors'
interface Props {
axes: Axes
type: CellType
source: Source
autoRefresh: number
templates: Template[]
timeRange: TimeRange
queryConfigs: QueryConfig[]
editQueryStatus: () => void
tableOptions: TableOptions
timeFormat: string
decimalPlaces: DecimalPlaces
fieldOptions: FieldOption[]
resizerTopHeight: number
thresholdsListColors: ColorNumber[]
gaugeColors: ColorNumber[]
lineColors: ColorString[]
staticLegend: boolean
isInCEO: boolean
}
const DashVisualization: SFC<Props> = ({
axes,
type,
source,
isInCEO,
templates,
timeRange,
lineColors,
timeFormat,
autoRefresh,
gaugeColors,
fieldOptions,
queryConfigs,
staticLegend,
tableOptions,
decimalPlaces,
editQueryStatus,
resizerTopHeight,
thresholdsListColors,
}) => {
const colors: ColorString[] = getCellTypeColors({
cellType: type,
gaugeColors,
thresholdsListColors,
lineColors,
})
return (
<div className="graph">
<VisualizationName />
<div className="graph-container">
<RefreshingGraph
source={source}
colors={colors}
axes={axes}
type={type}
tableOptions={tableOptions}
queries={buildQueries(queryConfigs, timeRange)}
templates={templates}
autoRefresh={autoRefresh}
editQueryStatus={editQueryStatus}
resizerTopHeight={resizerTopHeight}
staticLegend={staticLegend}
timeFormat={timeFormat}
decimalPlaces={decimalPlaces}
fieldOptions={fieldOptions}
isInCEO={isInCEO}
/>
</div>
</div>
)
}
const mapStateToProps = ({
cellEditorOverlay: {
thresholdsListColors,
gaugeColors,
lineColors,
cell: {type, axes, tableOptions, fieldOptions, timeFormat, decimalPlaces},
},
}) => ({
gaugeColors,
thresholdsListColors,
lineColors,
type,
axes,
tableOptions,
fieldOptions,
timeFormat,
decimalPlaces,
})
export default connect(mapStateToProps, null)(DashVisualization)

View File

@ -1,79 +0,0 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import {renameCell} from 'src/dashboards/actions/cellEditorOverlay'
import {ErrorHandling} from 'src/shared/decorators/errors'
@ErrorHandling
class VisualizationName extends Component {
constructor(props) {
super(props)
this.state = {
workingName: props.name,
}
}
handleChange = e => {
this.setState({workingName: e.target.value})
}
handleBlur = () => {
const {handleRenameCell} = this.props
const {workingName} = this.state
handleRenameCell(workingName)
}
handleKeyDown = e => {
if (e.key === 'Enter' || e.key === 'Escape') {
e.target.blur()
}
}
handleFocus = e => {
e.target.select()
}
render() {
const {workingName} = this.state
return (
<div className="graph-heading">
<input
type="text"
className="form-control input-sm"
value={workingName}
onChange={this.handleChange}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
onKeyDown={this.handleKeyDown}
placeholder="Name this Cell..."
spellCheck={false}
/>
</div>
)
}
}
const {string, func} = PropTypes
VisualizationName.propTypes = {
name: string.isRequired,
handleRenameCell: func,
}
const mapStateToProps = ({
cellEditorOverlay: {
cell: {name},
},
}) => ({
name,
})
const mapDispatchToProps = dispatch => ({
handleRenameCell: bindActionCreators(renameCell, dispatch),
})
export default connect(mapStateToProps, mapDispatchToProps)(VisualizationName)

View File

@ -1,7 +1,7 @@
import {DEFAULT_TABLE_OPTIONS} from 'src/dashboards/constants' import {DEFAULT_TABLE_OPTIONS} from 'src/dashboards/constants'
import {stringifyColorValues} from 'src/shared/constants/colorOperations' import {stringifyColorValues} from 'src/shared/constants/colorOperations'
import {CellType, Axis} from 'src/types/dashboards' import {CellType, Axis} from 'src/types/dashboards'
import {ColorString, ColorNumber} from 'src/types/colors' import {Color} from 'src/types/colors'
export const initializeOptions = (cellType: CellType) => { export const initializeOptions = (cellType: CellType) => {
switch (cellType) { switch (cellType) {
@ -32,11 +32,11 @@ export const DEFAULT_AXIS: DefaultAxis = {
export const TOOLTIP_Y_VALUE_FORMAT = export const TOOLTIP_Y_VALUE_FORMAT =
'<p><strong>K/M/B</strong> = Thousand / Million / Billion<br/><strong>K/M/G</strong> = Kilo / Mega / Giga </p>' '<p><strong>K/M/B</strong> = Thousand / Million / Billion<br/><strong>K/M/G</strong> = Kilo / Mega / Giga </p>'
interface Color { interface ColorArgs {
cellType: CellType cellType: CellType
thresholdsListColors: ColorNumber[] thresholdsListColors: Color[]
gaugeColors: ColorNumber[] gaugeColors: Color[]
lineColors: ColorString[] lineColors: Color[]
} }
export const getCellTypeColors = ({ export const getCellTypeColors = ({
@ -44,8 +44,8 @@ export const getCellTypeColors = ({
gaugeColors, gaugeColors,
thresholdsListColors, thresholdsListColors,
lineColors, lineColors,
}: Color): ColorString[] => { }: ColorArgs): Color[] => {
let colors: ColorString[] = [] let colors: Color[] = []
switch (cellType) { switch (cellType) {
case CellType.Gauge: { case CellType.Gauge: {

View File

@ -115,9 +115,9 @@ export enum CEOTabs {
Vis = 'Visualization', Vis = 'Visualization',
} }
export const MAX_TOLOCALESTRING_VAL = 20 // 20 is the max input to maximumFractionDigits in spec for tolocalestring export const MAX_TO_LOCALE_STRING_VAL = 20 // 20 is the max input to maximumFractionDigits in spec for "to locale string"
export const MIN_DECIMAL_PLACES = '0' export const MIN_DECIMAL_PLACES = '0'
export const MAX_DECIMAL_PLACES = MAX_TOLOCALESTRING_VAL.toString() export const MAX_DECIMAL_PLACES = MAX_TO_LOCALE_STRING_VAL.toString()
// used in importing dashboards and mapping sources // used in importing dashboards and mapping sources
export const DYNAMIC_SOURCE = 'dynamic' export const DYNAMIC_SOURCE = 'dynamic'

View File

@ -79,9 +79,9 @@ interface Props extends ManualRefreshProps, WithRouterProps {
notify: NotificationsActions.PublishNotificationActionCreator notify: NotificationsActions.PublishNotificationActionCreator
selectedCell: Cell selectedCell: Cell
thresholdsListType: string thresholdsListType: string
thresholdsListColors: ColorsModels.ColorNumber[] thresholdsListColors: ColorsModels.Color[]
gaugeColors: ColorsModels.ColorNumber[] gaugeColors: ColorsModels.Color[]
lineColors: ColorsModels.ColorString[] lineColors: ColorsModels.Color[]
addCell: typeof dashboardActions.addCellAsync addCell: typeof dashboardActions.addCellAsync
deleteCell: typeof dashboardActions.deleteCellAsync deleteCell: typeof dashboardActions.deleteCellAsync
copyCell: typeof dashboardActions.copyDashboardCellAsync copyCell: typeof dashboardActions.copyDashboardCellAsync

View File

@ -1,8 +1,8 @@
import React, {ReactElement} from 'react' import React from 'react'
import {CellType} from 'src/types/dashboards' import {CellType} from 'src/types/dashboards'
type Graphic = ReactElement<HTMLDivElement> type Graphic = JSX.Element
interface GraphSVGs { interface GraphSVGs {
[CellType.Line]: Graphic [CellType.Line]: Graphic
@ -14,6 +14,7 @@ interface GraphSVGs {
[CellType.Gauge]: Graphic [CellType.Gauge]: Graphic
[CellType.Table]: Graphic [CellType.Table]: Graphic
} }
const GRAPH_SVGS: GraphSVGs = { const GRAPH_SVGS: GraphSVGs = {
line: ( line: (
<div className="viz-type-selector--graphic"> <div className="viz-type-selector--graphic">

View File

@ -17,14 +17,14 @@ import {initializeOptions} from 'src/dashboards/constants/cellEditor'
import {Action, ActionType} from 'src/dashboards/actions/cellEditorOverlay' import {Action, ActionType} from 'src/dashboards/actions/cellEditorOverlay'
import {CellType, Cell} from 'src/types' import {CellType, Cell} from 'src/types'
import {ThresholdType, TableOptions} from 'src/types/dashboards' import {ThresholdType, TableOptions} from 'src/types/dashboards'
import {ThresholdColor, GaugeColor, LineColor} from 'src/types/colors' import {Color} from 'src/types/colors'
interface CEOInitialState { interface CEOInitialState {
cell: Cell | null cell: Cell | null
thresholdsListType: ThresholdType thresholdsListType: ThresholdType
thresholdsListColors: ThresholdColor[] thresholdsListColors: Color[]
gaugeColors: GaugeColor[] gaugeColors: Color[]
lineColors: LineColor[] lineColors: Color[]
} }
export const initialState = { export const initialState = {

View File

@ -19,11 +19,12 @@ import {
DecimalPlaces, DecimalPlaces,
CellType, CellType,
} from 'src/types/dashboards' } from 'src/types/dashboards'
import {LineColor, ColorNumber} from 'src/types/colors' import {Color} from 'src/types/colors'
export const dashboard = { export const dashboard = {
id: '1', id: '1',
name: 'd1', name: 'd1',
default: false,
cells: [ cells: [
{ {
x: 1, x: 1,
@ -42,6 +43,7 @@ export const dashboard = {
links: { links: {
self: '/v2/dashboards/1', self: '/v2/dashboards/1',
cells: '/v2/dashboards/cells', cells: '/v2/dashboards/cells',
copy: '/v2/dashboards/1/cells/1/copy',
}, },
} }
@ -175,7 +177,7 @@ export const tableOptions: TableOptions = {
wrapping: 'truncate', wrapping: 'truncate',
fixFirstColumn: true, fixFirstColumn: true,
} }
export const lineColors: LineColor[] = [ export const lineColors: Color[] = [
{ {
id: '574fb0a3-0a26-44d7-8d71-d4981756acb1', id: '574fb0a3-0a26-44d7-8d71-d4981756acb1',
type: 'scale', type: 'scale',
@ -398,29 +400,29 @@ export const predefinedTemplateVariables: Template[] = [
{...interval}, {...interval},
] ]
export const thresholdsListColors: ColorNumber[] = [ export const thresholdsListColors: Color[] = [
{ {
type: 'text', type: 'text',
hex: '#00C9FF', hex: '#00C9FF',
id: 'base', id: 'base',
name: 'laser', name: 'laser',
value: -1000000000000000000, value: '-1000000000000000000',
}, },
] ]
export const gaugeColors: ColorNumber[] = [ export const gaugeColors: Color[] = [
{ {
type: 'min', type: 'min',
hex: '#00C9FF', hex: '#00C9FF',
id: '0', id: '0',
name: 'laser', name: 'laser',
value: 0, value: '0',
}, },
{ {
type: 'max', type: 'max',
hex: '#9394FF', hex: '#9394FF',
id: '1', id: '1',
name: 'comet', name: 'comet',
value: 100, value: '100',
}, },
] ]

View File

@ -1,4 +1,4 @@
import Dygraph from 'dygraphs/src-es5/dygraph' import Dygraph from 'dygraphs/src/dygraph'
/* eslint-disable */ /* eslint-disable */
/** /**
* Synchronize zooming and/or selections between a set of dygraphs. * Synchronize zooming and/or selections between a set of dygraphs.

View File

@ -1,23 +0,0 @@
import React, {SFC} from 'react'
import PageHeader from 'src/clockface/components/page_layout/PageHeader'
interface Props {
onGoToNewService: () => void
}
const EmptyFluxPage: SFC<Props> = ({onGoToNewService}) => (
<div className="page">
<PageHeader titleText="Flux Editor" fullWidth={true} />
<div className="page-contents">
<div className="flux-empty">
<p>You do not have a configured Flux source</p>
<button className="btn btn-primary btn-md" onClick={onGoToNewService}>
Connect to Flux
</button>
</div>
</div>
</div>
)
export default EmptyFluxPage

View File

@ -16,7 +16,6 @@ import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes'
// Types // Types
import {FluxTable} from 'src/types' import {FluxTable} from 'src/types'
import {DygraphSeries} from 'src/types'
import {ViewType} from 'src/types/v2/dashboards' import {ViewType} from 'src/types/v2/dashboards'
interface Props { interface Props {
@ -26,25 +25,18 @@ interface Props {
class FluxGraph extends PureComponent<Props> { class FluxGraph extends PureComponent<Props> {
public render() { public render() {
const containerStyle = {
width: 'calc(100% - 32px)',
height: 'calc(100% - 16px)',
position: 'absolute',
}
const {dygraphsData, labels} = fluxTablesToDygraph(this.props.data) const {dygraphsData, labels} = fluxTablesToDygraph(this.props.data)
return ( return (
<div className="yield-node--graph"> <div className="yield-node--graph">
<Dygraph <Dygraph
type={ViewType.Line}
labels={labels} labels={labels}
type={ViewType.Line}
staticLegend={false} staticLegend={false}
dygraphSeries={{}}
options={this.options}
timeSeries={dygraphsData} timeSeries={dygraphsData}
colors={DEFAULT_LINE_COLORS} colors={DEFAULT_LINE_COLORS}
dygraphSeries={this.dygraphSeries}
options={this.options}
containerStyle={containerStyle}
handleSetHoverTime={this.props.setHoverTime} handleSetHoverTime={this.props.setHoverTime}
/> />
</div> </div>
@ -57,10 +49,6 @@ class FluxGraph extends PureComponent<Props> {
gridLineColor: '#383846', gridLineColor: '#383846',
} }
} }
private get dygraphSeries(): DygraphSeries {
return {}
}
} }
const mdtp = { const mdtp = {

View File

@ -1,4 +1,4 @@
import React, {PureComponent, ReactElement, MouseEvent} from 'react' import React, {PureComponent, MouseEvent} from 'react'
import FuncArg from 'src/flux/components/FuncArg' import FuncArg from 'src/flux/components/FuncArg'
import {OnChangeArg} from 'src/types/flux' import {OnChangeArg} from 'src/types/flux'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
@ -126,7 +126,7 @@ export default class FuncArgs extends PureComponent<Props> {
) )
} }
get build(): ReactElement<HTMLDivElement> { get build(): JSX.Element {
const {func, onGenerateScript} = this.props const {func, onGenerateScript} = this.props
if (func.name === funcNames.FILTER) { if (func.name === funcNames.FILTER) {
return ( return (

View File

@ -72,7 +72,7 @@ interface State {
type ScriptFunc = (script: string) => void type ScriptFunc = (script: string) => void
export const FluxContext = React.createContext() export const FluxContext = React.createContext({})
@ErrorHandling @ErrorHandling
export class FluxPage extends PureComponent<Props, State> { export class FluxPage extends PureComponent<Props, State> {

View File

@ -173,9 +173,6 @@ describe('influxql astToString', () => {
const expected = `SELECT derivative("field1", 1h) / derivative("field2", 1h) FROM "myseries"` const expected = `SELECT derivative("field1", 1h) / derivative("field2", 1h) FROM "myseries"`
const actual = ast.toString() const actual = ast.toString()
// console.log('actual', actual)
// console.log('expected', expected)
expect(actual).toBe(expected) expect(actual).toBe(expected)
}) })

View File

@ -2,7 +2,6 @@ import {toString} from './ast'
const InfluxQL = ast => { const InfluxQL = ast => {
return { return {
// select: () =>
toString: () => toString(ast), toString: () => toString(ast),
} }
} }

View File

@ -4,11 +4,11 @@ import classnames from 'classnames'
import {ClickOutside} from 'src/shared/components/ClickOutside' import {ClickOutside} from 'src/shared/components/ClickOutside'
import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar' import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
import {ColorNumber, ThresholdColor} from 'src/types/colors' import {Color, ThresholdColor} from 'src/types/colors'
import {DROPDOWN_MENU_MAX_HEIGHT} from 'src/shared/constants/index' import {DROPDOWN_MENU_MAX_HEIGHT} from 'src/shared/constants/index'
interface Props { interface Props {
selected: ColorNumber selected: Color
disabled?: boolean disabled?: boolean
stretchToFit?: boolean stretchToFit?: boolean
colors: ThresholdColor[] colors: ThresholdColor[]

View File

@ -5,16 +5,16 @@ import classnames from 'classnames'
import {ClickOutside} from 'src/shared/components/ClickOutside' import {ClickOutside} from 'src/shared/components/ClickOutside'
import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar' import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar'
import {ColorNumber} from 'src/types/colors' import {Color} from 'src/types/colors'
import {LINE_COLOR_SCALES} from 'src/shared/constants/graphColorPalettes' import {LINE_COLOR_SCALES} from 'src/shared/constants/graphColorPalettes'
import {DROPDOWN_MENU_MAX_HEIGHT} from 'src/shared/constants/index' import {DROPDOWN_MENU_MAX_HEIGHT} from 'src/shared/constants/index'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props { interface Props {
onChoose: (colors: ColorNumber[]) => void onChoose: (colors: Color[]) => void
stretchToFit?: boolean stretchToFit?: boolean
disabled?: boolean disabled?: boolean
selected: ColorNumber[] selected: Color[]
} }
interface State { interface State {

View File

@ -1,10 +1,10 @@
import React, {PureComponent, ReactElement} from 'react' import React, {PureComponent} from 'react'
import classnames from 'classnames' import classnames from 'classnames'
interface Props { interface Props {
fileTypesToAccept?: string fileTypesToAccept?: string
containerClass?: string containerClass?: string
handleSubmit: (uploadContent: string, fileName: string) => void handleSubmit: (uploadContent: string | ArrayBuffer, fileName: string) => void
submitText?: string submitText?: string
submitOnDrop?: boolean submitOnDrop?: boolean
submitOnUpload?: boolean submitOnUpload?: boolean
@ -13,7 +13,7 @@ interface Props {
interface State { interface State {
inputContent: string | null inputContent: string | null
uploadContent: string uploadContent: string | ArrayBuffer
fileName: string fileName: string
dragClass: string dragClass: string
} }
@ -106,7 +106,7 @@ class DragAndDrop extends PureComponent<Props, State> {
return classnames('drag-and-drop--form', {active: !uploadContent}) return classnames('drag-and-drop--form', {active: !uploadContent})
} }
private get dragAreaHeader(): ReactElement<HTMLHeadElement> { private get dragAreaHeader(): JSX.Element {
const {uploadContent, fileName} = this.state const {uploadContent, fileName} = this.state
if (uploadContent) { if (uploadContent) {
@ -120,7 +120,7 @@ class DragAndDrop extends PureComponent<Props, State> {
) )
} }
private get buttons(): ReactElement<HTMLSpanElement> | null { private get buttons(): JSX.Element | null {
const {uploadContent} = this.state const {uploadContent} = this.state
const {submitText, submitOnDrop} = this.props const {submitText, submitOnDrop} = this.props
@ -175,10 +175,10 @@ class DragAndDrop extends PureComponent<Props, State> {
const reader = new FileReader() const reader = new FileReader()
reader.readAsText(file) reader.readAsText(file)
reader.onload = loadEvent => { reader.onload = () => {
this.setState( this.setState(
{ {
uploadContent: loadEvent.target.result, uploadContent: reader.result,
fileName: file.name, fileName: file.name,
}, },
this.submitOnUpload this.submitOnUpload
@ -201,10 +201,10 @@ class DragAndDrop extends PureComponent<Props, State> {
const reader = new FileReader() const reader = new FileReader()
reader.readAsText(file) reader.readAsText(file)
reader.onload = loadEvent => { reader.onload = () => {
this.setState( this.setState(
{ {
uploadContent: loadEvent.target.result, uploadContent: reader.result,
fileName: file.name, fileName: file.name,
}, },
this.submitOnDrop this.submitOnDrop

View File

@ -0,0 +1,36 @@
// Libraries
import React, {PureComponent, CSSProperties} from 'react'
// Types
import {RemoteDataState} from 'src/types'
interface Props {
loading: RemoteDataState
children: JSX.Element
}
const GraphLoadingDots = () => (
<div className="graph-panel__refreshing">
<div />
<div />
<div />
</div>
)
class DygraphCell extends PureComponent<Props> {
public render() {
const {loading} = this.props
return (
<div className="dygraph graph--hasYLabel" style={this.style}>
{loading === RemoteDataState.Loading && <GraphLoadingDots />}
{this.props.children}
</div>
)
}
private get style(): CSSProperties {
return {height: '100%'}
}
}
export default DygraphCell

View File

@ -6,13 +6,14 @@ import classnames from 'classnames'
import uuid from 'uuid' import uuid from 'uuid'
import * as actions from 'src/dashboards/actions/v2/views' import * as actions from 'src/dashboards/actions/v2/views'
import {SeriesLegendData} from 'src/types/dygraphs'
import DygraphLegendSort from 'src/shared/components/DygraphLegendSort' import DygraphLegendSort from 'src/shared/components/DygraphLegendSort'
import {makeLegendStyles} from 'src/shared/graphs/helpers' import {makeLegendStyles} from 'src/shared/graphs/helpers'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
import {NO_CELL} from 'src/shared/constants' import {NO_CELL} from 'src/shared/constants'
import {DygraphClass} from 'src/types'
// Types
import DygraphClass, {SeriesLegendData} from 'src/external/dygraph'
interface Props { interface Props {
hoverTime: number hoverTime: number

View File

@ -0,0 +1,38 @@
import {PureComponent} from 'react'
import {FluxTable} from 'src/types'
import {
fluxTablesToDygraph,
FluxTablesToDygraphResult,
} from 'src/shared/parsing/flux/dygraph'
interface Props {
tables: FluxTable[]
children: (result: FluxTablesToDygraphResult) => JSX.Element
}
class DygraphTransformation extends PureComponent<
Props,
FluxTablesToDygraphResult
> {
public static getDerivedStateFromProps(props) {
return {...fluxTablesToDygraph(props.tables)}
}
constructor(props) {
super(props)
this.state = {
labels: [],
dygraphsData: [],
nonNumericColumns: [],
}
}
public render() {
const {children} = this.props
return children(this.state)
}
}
export default DygraphTransformation

View File

@ -6,20 +6,22 @@ import {GAUGE_SPECS} from 'src/shared/constants/gaugeSpecs'
import { import {
COLOR_TYPE_MIN, COLOR_TYPE_MIN,
COLOR_TYPE_MAX, COLOR_TYPE_MAX,
DEFAULT_VALUE_MAX,
DEFAULT_VALUE_MIN,
MIN_THRESHOLDS, MIN_THRESHOLDS,
} from 'src/shared/constants/thresholds' } from 'src/shared/constants/thresholds'
import {MAX_TOLOCALESTRING_VAL} from 'src/dashboards/constants' import {MAX_TO_LOCALE_STRING_VAL} from 'src/dashboards/constants'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
import {ColorString} from 'src/types/colors' import {Color} from 'src/types/colors'
import {DecimalPlaces} from 'src/types/dashboards' import {DecimalPlaces} from 'src/types/dashboards'
interface Props { interface Props {
width: string width: string
height: string height: string
gaugePosition: number gaugePosition: number
colors?: ColorString[] colors?: Color[]
prefix: string prefix: string
suffix: string suffix: string
decimalPlaces: DecimalPlaces decimalPlaces: DecimalPlaces
@ -78,12 +80,21 @@ class Gauge extends Component<Props> {
if (!colors || colors.length === 0) { if (!colors || colors.length === 0) {
return return
} }
// Distill out max and min values // Distill out max and min values
const minValue = Number( const minValue = Number(
colors.find(color => color.type === COLOR_TYPE_MIN).value _.get(
colors.find(color => color.type === COLOR_TYPE_MIN),
'value',
DEFAULT_VALUE_MIN
)
) )
const maxValue = Number( const maxValue = Number(
colors.find(color => color.type === COLOR_TYPE_MAX).value _.get(
colors.find(color => color.type === COLOR_TYPE_MAX),
'value',
DEFAULT_VALUE_MAX
)
) )
// The following functions must be called in the specified order // The following functions must be called in the specified order
@ -206,7 +217,7 @@ class Gauge extends Component<Props> {
// Draw Large ticks // Draw Large ticks
for (let lt = 0; lt <= lineCount; lt++) { for (let lt = 0; lt <= lineCount; lt++) {
// Rototion before drawing line // Rotation before drawing line
ctx.rotate(startDegree) ctx.rotate(startDegree)
ctx.rotate(lt * arcLargeIncrement) ctx.rotate(lt * arcLargeIncrement)
// Draw line // Draw line
@ -225,7 +236,7 @@ class Gauge extends Component<Props> {
// Draw Small ticks // Draw Small ticks
for (let lt = 0; lt <= totalSmallLineCount; lt++) { for (let lt = 0; lt <= totalSmallLineCount; lt++) {
// Rototion before drawing line // Rotation before drawing line
ctx.rotate(startDegree) ctx.rotate(startDegree)
ctx.rotate(lt * arcSmallIncrement) ctx.rotate(lt * arcSmallIncrement)
// Draw line // Draw line
@ -322,7 +333,7 @@ class Gauge extends Component<Props> {
let valueString let valueString
if (decimalPlaces.isEnforced) { if (decimalPlaces.isEnforced) {
const digits = Math.min(decimalPlaces.digits, MAX_TOLOCALESTRING_VAL) const digits = Math.min(decimalPlaces.digits, MAX_TO_LOCALE_STRING_VAL)
valueString = value.toLocaleString(undefined, { valueString = value.toLocaleString(undefined, {
minimumFractionDigits: 0, minimumFractionDigits: 0,
maximumFractionDigits: digits, maximumFractionDigits: digits,
@ -330,7 +341,7 @@ class Gauge extends Component<Props> {
} else { } else {
valueString = value.toLocaleString(undefined, { valueString = value.toLocaleString(undefined, {
minimumFractionDigits: 0, minimumFractionDigits: 0,
maximumFractionDigits: MAX_TOLOCALESTRING_VAL, maximumFractionDigits: MAX_TO_LOCALE_STRING_VAL,
}) })
} }
@ -343,7 +354,7 @@ class Gauge extends Component<Props> {
let valueString let valueString
if (decimalPlaces.isEnforced) { if (decimalPlaces.isEnforced) {
const digits = Math.min(decimalPlaces.digits, MAX_TOLOCALESTRING_VAL) const digits = Math.min(decimalPlaces.digits, MAX_TO_LOCALE_STRING_VAL)
valueString = value.toLocaleString(undefined, { valueString = value.toLocaleString(undefined, {
minimumFractionDigits: digits, minimumFractionDigits: digits,
maximumFractionDigits: digits, maximumFractionDigits: digits,
@ -351,7 +362,7 @@ class Gauge extends Component<Props> {
} else { } else {
valueString = value.toLocaleString(undefined, { valueString = value.toLocaleString(undefined, {
minimumFractionDigits: 0, minimumFractionDigits: 0,
maximumFractionDigits: MAX_TOLOCALESTRING_VAL, maximumFractionDigits: MAX_TO_LOCALE_STRING_VAL,
}) })
} }

View File

@ -2,28 +2,35 @@ import {shallow} from 'enzyme'
import React from 'react' import React from 'react'
import Gauge from 'src/shared/components/Gauge' import Gauge from 'src/shared/components/Gauge'
import GaugeChart from 'src/shared/components/GaugeChart' import GaugeChart from 'src/shared/components/GaugeChart'
import {ViewType, ViewShape, GaugeView} from 'src/types/v2/dashboards'
const data = [ const tables = [
{ {
response: { id: '54797afd-734d-4ca3-94b6-3a7870c53b27',
results: [ data: [
{ ['', 'result', 'table', '_time', 'mean', '_measurement'],
series: [ ['', '', '0', '2018-09-27T16:50:10Z', '2', 'cpu'],
{ ],
values: [[1, 2]], name: '_measurement=cpu',
columns: ['time', 'value'], groupKey: {
}, _measurement: 'cpu',
], },
}, dataTypes: {
], '': '#datatype',
result: 'string',
table: 'long',
_time: 'dateTime:RFC3339',
mean: 'double',
_measurement: 'string',
}, },
}, },
] ]
const defaultProps = { const properties: GaugeView = {
data: [], queries: [],
isFetchingInitially: false, colors: [],
cellID: '', shape: ViewShape.ChronografV2,
type: ViewType.Gauge,
prefix: '', prefix: '',
suffix: '', suffix: '',
decimalPlaces: { decimalPlaces: {
@ -32,6 +39,11 @@ const defaultProps = {
}, },
} }
const defaultProps = {
tables: [],
properties,
}
const setup = (overrides = {}) => { const setup = (overrides = {}) => {
const props = { const props = {
...defaultProps, ...defaultProps,
@ -54,10 +66,10 @@ describe('GaugeChart', () => {
describe('when data has a value', () => { describe('when data has a value', () => {
it('renders the correct number', () => { it('renders the correct number', () => {
const wrapper = setup({data}) const wrapper = setup({tables})
expect(wrapper.find(Gauge).exists()).toBe(true) expect(wrapper.find(Gauge).exists()).toBe(true)
expect(wrapper.find(Gauge).props().gaugePosition).toBe(2) expect(wrapper.find(Gauge).props().gaugePosition).toBe('2')
}) })
}) })
}) })

View File

@ -1,41 +1,35 @@
// Libraries
import React, {PureComponent} from 'react' import React, {PureComponent} from 'react'
import _ from 'lodash' import _ from 'lodash'
import getLastValues from 'src/shared/parsing/lastValues' // Components
import Gauge from 'src/shared/components/Gauge' import Gauge from 'src/shared/components/Gauge'
import {DEFAULT_GAUGE_COLORS} from 'src/shared/constants/thresholds' // Parsing
import {stringifyColorValues} from 'src/shared/constants/colorOperations' import getLastValues from 'src/shared/parsing/flux/fluxToSingleStat'
import {DASHBOARD_LAYOUT_ROW_HEIGHT} from 'src/shared/constants'
// Types
import {FluxTable} from 'src/types'
import {GaugeView} from 'src/types/v2/dashboards'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
import {DecimalPlaces} from 'src/types/dashboards'
import {ColorString} from 'src/types/colors'
import {TimeSeriesServerResponse} from 'src/types/series'
interface Props { interface Props {
data: TimeSeriesServerResponse[] tables: FluxTable[]
decimalPlaces: DecimalPlaces properties: GaugeView
cellHeight?: number
colors?: ColorString[]
prefix: string
suffix: string
resizerTopHeight?: number
} }
@ErrorHandling @ErrorHandling
class GaugeChart extends PureComponent<Props> { class GaugeChart extends PureComponent<Props> {
public static defaultProps: Partial<Props> = {
colors: stringifyColorValues(DEFAULT_GAUGE_COLORS),
}
public render() { public render() {
const {colors, prefix, suffix, decimalPlaces} = this.props const {colors, prefix, suffix, decimalPlaces} = this.props.properties
return ( return (
<div className="single-stat"> <div className="single-stat">
<Gauge <Gauge
width="900" width="900"
height="300"
colors={colors} colors={colors}
height={this.height}
prefix={prefix} prefix={prefix}
suffix={suffix} suffix={suffix}
gaugePosition={this.lastValueForGauge} gaugePosition={this.lastValueForGauge}
@ -45,30 +39,10 @@ class GaugeChart extends PureComponent<Props> {
) )
} }
private get height(): string {
const {resizerTopHeight} = this.props
return (this.initialCellHeight || resizerTopHeight || 300).toString()
}
private get initialCellHeight(): string {
const {cellHeight} = this.props
if (cellHeight) {
return (cellHeight * DASHBOARD_LAYOUT_ROW_HEIGHT).toString()
}
return null
}
private get lastValueForGauge(): number { private get lastValueForGauge(): number {
const {data} = this.props const {tables} = this.props
const {lastValues} = getLastValues(data) const {values} = getLastValues(tables)
const lastValue = _.get(lastValues, 0, 0) const lastValue = _.get(values, 0, 0)
if (!lastValue) {
return 0
}
return lastValue return lastValue
} }

View File

@ -1,38 +1,28 @@
// Libraries // Libraries
import React, {PureComponent, CSSProperties} from 'react' import React, {PureComponent} from 'react'
import Dygraph from 'src/shared/components/dygraph/Dygraph' import Dygraph from 'src/shared/components/dygraph/Dygraph'
import DygraphCell from 'src/shared/components/DygraphCell'
import DygraphTransformation from 'src/shared/components/DygraphTransformation'
// Components // Components
import {ErrorHandlingWith} from 'src/shared/decorators/errors' import {ErrorHandlingWith} from 'src/shared/decorators/errors'
import InvalidData from 'src/shared/components/InvalidData' import InvalidData from 'src/shared/components/InvalidData'
// Utils
import {
fluxTablesToDygraph,
FluxTablesToDygraphResult,
} from 'src/shared/parsing/flux/dygraph'
// Types // Types
import {ColorString} from 'src/types/colors' import {Options} from 'src/external/dygraph'
import {DecimalPlaces} from 'src/types/dashboards' import {LineView} from 'src/types/v2/dashboards'
import {Axes, TimeRange, RemoteDataState, FluxTable} from 'src/types' import {TimeRange} from 'src/types/v2'
import {ViewType, CellQuery} from 'src/types/v2' import {FluxTable, RemoteDataState} from 'src/types'
interface Props { interface Props {
axes: Axes
type: ViewType
queries: CellQuery[]
timeRange: TimeRange
colors: ColorString[]
loading: RemoteDataState loading: RemoteDataState
decimalPlaces: DecimalPlaces properties: LineView
data: FluxTable[] timeRange: TimeRange
tables: FluxTable[]
viewID: string viewID: string
cellHeight: number
staticLegend: boolean staticLegend: boolean
onZoom: () => void onZoom: () => void
handleSetHoverTime: () => void handleSetHoverTime: () => void
activeQueryIndex?: number
} }
@ErrorHandlingWith(InvalidData) @ErrorHandlingWith(InvalidData)
@ -41,49 +31,46 @@ class LineGraph extends PureComponent<Props> {
staticLegend: false, staticLegend: false,
} }
private isValidData: boolean = true
private timeSeries: FluxTablesToDygraphResult
public componentWillMount() {
const {data} = this.props
this.parseTimeSeries(data)
}
public parseTimeSeries(data) {
this.timeSeries = fluxTablesToDygraph(data)
}
public componentWillUpdate(nextProps) {
const {data, activeQueryIndex} = this.props
if (
data !== nextProps.data ||
activeQueryIndex !== nextProps.activeQueryIndex
) {
this.parseTimeSeries(nextProps.data)
}
}
public render() { public render() {
if (!this.isValidData) {
return <InvalidData />
}
const { const {
axes, tables,
type,
colors,
viewID, viewID,
onZoom, onZoom,
loading, loading,
queries,
timeRange, timeRange,
properties,
staticLegend, staticLegend,
handleSetHoverTime, handleSetHoverTime,
} = this.props } = this.props
const {labels, dygraphsData} = this.timeSeries const {axes, type, colors, queries} = properties
const options = { return (
<DygraphTransformation tables={tables}>
{({labels, dygraphsData}) => (
<DygraphCell loading={loading}>
<Dygraph
type={type}
axes={axes}
viewID={viewID}
colors={colors}
onZoom={onZoom}
labels={labels}
queries={queries}
options={this.options}
timeRange={timeRange}
timeSeries={dygraphsData}
staticLegend={staticLegend}
handleSetHoverTime={handleSetHoverTime}
/>
</DygraphCell>
)}
</DygraphTransformation>
)
}
private get options(): Partial<Options> {
return {
rightGap: 0, rightGap: 0,
yRangePad: 10, yRangePad: 10,
labelsKMB: true, labelsKMB: true,
@ -94,68 +81,8 @@ class LineGraph extends PureComponent<Props> {
axisLineColor: '#383846', axisLineColor: '#383846',
gridLineColor: '#383846', gridLineColor: '#383846',
connectSeparatedPoints: true, connectSeparatedPoints: true,
stepPlot: type === 'line-stepplot',
stackedGraph: type === 'line-stacked',
}
return (
<div className="dygraph graph--hasYLabel" style={this.style}>
{loading === RemoteDataState.Loading && <GraphLoadingDots />}
<Dygraph
type={type}
axes={axes}
viewID={viewID}
colors={colors}
onZoom={onZoom}
labels={labels}
queries={queries}
options={options}
timeRange={timeRange}
timeSeries={dygraphsData}
staticLegend={staticLegend}
dygraphSeries={{}}
isGraphFilled={this.isGraphFilled}
containerStyle={this.containerStyle}
handleSetHoverTime={handleSetHoverTime}
>
{type === ViewType.LinePlusSingleStat && (
<div>Single Stat Goes Here</div>
)}
</Dygraph>
</div>
)
}
private get isGraphFilled(): boolean {
const {type} = this.props
if (type === ViewType.LinePlusSingleStat) {
return false
}
return true
}
private get style(): CSSProperties {
return {height: '100%'}
}
private get containerStyle(): CSSProperties {
return {
width: 'calc(100% - 32px)',
height: 'calc(100% - 16px)',
position: 'absolute',
top: '8px',
} }
} }
} }
const GraphLoadingDots = () => (
<div className="graph-panel__refreshing">
<div />
<div />
<div />
</div>
)
export default LineGraph export default LineGraph

View File

@ -5,12 +5,12 @@ import {bindActionCreators} from 'redux'
import ColorScaleDropdown from 'src/shared/components/ColorScaleDropdown' import ColorScaleDropdown from 'src/shared/components/ColorScaleDropdown'
import {updateLineColors} from 'src/dashboards/actions/cellEditorOverlay' import {updateLineColors} from 'src/dashboards/actions/cellEditorOverlay'
import {ColorNumber} from 'src/types/colors' import {Color} from 'src/types/colors'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props { interface Props {
lineColors: ColorNumber[] lineColors: Color[]
handleUpdateLineColors: (colors: ColorNumber[]) => void handleUpdateLineColors: (colors: Color[]) => void
} }
@ErrorHandling @ErrorHandling

View File

@ -1,16 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import {OVERLAY_TECHNOLOGY} from 'shared/constants/classNames'
const OverlayTechnologies = ({children}) => (
<div className={OVERLAY_TECHNOLOGY}>{children}</div>
)
const {node} = PropTypes
OverlayTechnologies.propTypes = {
children: node.isRequired,
}
export default OverlayTechnologies

View File

@ -1,226 +0,0 @@
// Libraries
import React, {PureComponent} from 'react'
import {withRouter, WithRouterProps} from 'react-router'
import {connect} from 'react-redux'
import _ from 'lodash'
// Components
import LineGraph from 'src/shared/components/LineGraph'
import GaugeChart from 'src/shared/components/GaugeChart'
import TableGraph from 'src/shared/components/TableGraph'
import SingleStat from 'src/shared/components/SingleStat'
import TimeSeries from 'src/shared/components/time_series/TimeSeries'
// Constants
import {emptyGraphCopy} from 'src/shared/copy/cell'
import {DEFAULT_TIME_FORMAT} from 'src/dashboards/constants'
// Utils
import {buildQueries} from 'src/utils/buildQueriesForLayouts'
// Actions
import {setHoverTime} from 'src/dashboards/actions/v2/hoverTime'
// Types
import {TimeRange, Template, CellQuery} from 'src/types'
import {V1View, V1ViewTypes} from 'src/types/v2/dashboards'
interface Props {
link: string
timeRange: TimeRange
templates: Template[]
viewID: string
inView: boolean
isInCEO: boolean
timeFormat: string
cellHeight: number
autoRefresh: number
manualRefresh: number
options: V1View
staticLegend: boolean
onZoom: () => void
editQueryStatus: () => void
onSetResolution: () => void
grabDataForDownload: () => void
handleSetHoverTime: () => void
}
class RefreshingGraph extends PureComponent<Props & WithRouterProps> {
public static defaultProps: Partial<Props> = {
inView: true,
manualRefresh: 0,
staticLegend: false,
}
public render() {
const {link, inView, templates} = this.props
const {queries, type} = this.props.options
if (!queries.length) {
return (
<div className="graph-empty">
<p data-test="data-explorer-no-results">{emptyGraphCopy}</p>
</div>
)
}
return (
<TimeSeries
link={link}
inView={inView}
queries={this.queries}
templates={templates}
>
{({timeSeries, loading}) => {
switch (type) {
case V1ViewTypes.SingleStat:
return this.singleStat(timeSeries)
case V1ViewTypes.Table:
return this.table(timeSeries)
case V1ViewTypes.Gauge:
return this.gauge(timeSeries)
default:
return this.lineGraph(timeSeries, loading)
}
}}
</TimeSeries>
)
}
private singleStat = (data): JSX.Element => {
const {cellHeight, manualRefresh} = this.props
const {colors, decimalPlaces} = this.props.options
return (
<SingleStat
data={data}
colors={colors}
prefix={this.prefix}
suffix={this.suffix}
lineGraph={false}
key={manualRefresh}
cellHeight={cellHeight}
decimalPlaces={decimalPlaces}
/>
)
}
private table = (data): JSX.Element => {
const {manualRefresh, handleSetHoverTime, grabDataForDownload} = this.props
const {
colors,
fieldOptions,
tableOptions,
decimalPlaces,
} = this.props.options
return (
<TableGraph
data={data}
colors={colors}
key={manualRefresh}
tableOptions={tableOptions}
fieldOptions={fieldOptions}
decimalPlaces={decimalPlaces}
timeFormat={DEFAULT_TIME_FORMAT}
grabDataForDownload={grabDataForDownload}
handleSetHoverTime={handleSetHoverTime}
/>
)
}
private gauge = (data): JSX.Element => {
const {cellHeight, manualRefresh} = this.props
const {colors, decimalPlaces} = this.props.options
return (
<GaugeChart
data={data}
colors={colors}
prefix={this.prefix}
suffix={this.suffix}
key={manualRefresh}
cellHeight={cellHeight}
decimalPlaces={decimalPlaces}
resizerTopHeight={100}
/>
)
}
private lineGraph = (data, loading): JSX.Element => {
const {
onZoom,
viewID,
timeRange,
cellHeight,
staticLegend,
manualRefresh,
handleSetHoverTime,
} = this.props
const {decimalPlaces, axes, type, colors, queries} = this.props.options
return (
<LineGraph
data={data}
type={type}
axes={axes}
viewID={viewID}
colors={colors}
onZoom={onZoom}
queries={queries}
loading={loading}
key={manualRefresh}
timeRange={timeRange}
cellHeight={cellHeight}
staticLegend={staticLegend}
decimalPlaces={decimalPlaces}
handleSetHoverTime={handleSetHoverTime}
/>
)
}
private get queries(): CellQuery[] {
const {timeRange, options} = this.props
const {type} = options
const queries = buildQueries(options.queries, timeRange)
if (type === V1ViewTypes.SingleStat) {
return [queries[0]]
}
if (type === V1ViewTypes.Gauge) {
return [queries[0]]
}
return queries
}
private get prefix(): string {
const {axes} = this.props.options
return _.get(axes, 'y.prefix', '')
}
private get suffix(): string {
const {axes} = this.props.options
return _.get(axes, 'y.suffix', '')
}
}
const mstp = ({sources, routing}): Partial<Props> => {
const sourceID = routing.locationBeforeTransitions.query.sourceID
const source = sources.find(s => s.id === sourceID)
const link = source.links.query
return {
link,
}
}
const mdtp = {
handleSetHoverTime: setHoverTime,
}
export default connect(mstp, mdtp)(withRouter(RefreshingGraph))

View File

@ -0,0 +1,184 @@
// Libraries
import React, {PureComponent} from 'react'
import {withRouter, WithRouterProps} from 'react-router'
import {connect} from 'react-redux'
import _ from 'lodash'
// Components
import LineGraph from 'src/shared/components/LineGraph'
import StepPlot from 'src/shared/components/StepPlot'
import Stacked from 'src/shared/components/Stacked'
import GaugeChart from 'src/shared/components/GaugeChart'
// import TableGraph from 'src/shared/components/TableGraph'
import SingleStat from 'src/shared/components/SingleStat'
import TimeSeries from 'src/shared/components/time_series/TimeSeries'
import SingleStatTransform from 'src/shared/components/SingleStatTransform'
// Constants
import {emptyGraphCopy} from 'src/shared/copy/cell'
// import {DEFAULT_TIME_FORMAT} from 'src/dashboards/constants'
// Utils
import {buildQueries} from 'src/utils/buildQueriesForLayouts'
// Actions
import {setHoverTime} from 'src/dashboards/actions/v2/hoverTime'
// Types
import {TimeRange, Template, CellQuery} from 'src/types'
import {RefreshingViewProperties, ViewType} from 'src/types/v2/dashboards'
interface Props {
link: string
timeRange: TimeRange
templates: Template[]
viewID: string
inView: boolean
timeFormat: string
autoRefresh: number
manualRefresh: number
staticLegend: boolean
onZoom: () => void
editQueryStatus: () => void
onSetResolution: () => void
grabDataForDownload: () => void
handleSetHoverTime: () => void
properties: RefreshingViewProperties
}
class RefreshingView extends PureComponent<Props & WithRouterProps> {
public static defaultProps: Partial<Props> = {
inView: true,
manualRefresh: 0,
staticLegend: false,
}
public render() {
const {
link,
inView,
onZoom,
viewID,
timeRange,
templates,
properties,
staticLegend,
manualRefresh,
handleSetHoverTime,
} = this.props
if (!properties.queries.length) {
return (
<div className="graph-empty">
<p data-test="data-explorer-no-results">{emptyGraphCopy}</p>
</div>
)
}
return (
<TimeSeries
link={link}
inView={inView}
queries={this.queries}
templates={templates}
>
{({tables, loading}) => {
switch (properties.type) {
case ViewType.SingleStat:
return (
<SingleStatTransform tables={tables}>
{stat => <SingleStat stat={stat} properties={properties} />}
</SingleStatTransform>
)
case ViewType.Table:
return <div>YO! Imma table</div>
case ViewType.Gauge:
return (
<GaugeChart
tables={tables}
key={manualRefresh}
properties={properties}
/>
)
case ViewType.Line:
return (
<LineGraph
tables={tables}
viewID={viewID}
onZoom={onZoom}
loading={loading}
key={manualRefresh}
timeRange={timeRange}
properties={properties}
staticLegend={staticLegend}
handleSetHoverTime={handleSetHoverTime}
/>
)
case ViewType.StepPlot:
return (
<StepPlot
tables={tables}
viewID={viewID}
onZoom={onZoom}
loading={loading}
key={manualRefresh}
timeRange={timeRange}
properties={properties}
staticLegend={staticLegend}
handleSetHoverTime={handleSetHoverTime}
/>
)
case ViewType.Stacked:
return (
<Stacked
tables={tables}
viewID={viewID}
onZoom={onZoom}
loading={loading}
key={manualRefresh}
timeRange={timeRange}
properties={properties}
staticLegend={staticLegend}
handleSetHoverTime={handleSetHoverTime}
/>
)
default:
return <div>YO!</div>
}
}}
</TimeSeries>
)
}
private get queries(): CellQuery[] {
const {timeRange, properties} = this.props
const {type} = properties
const queries = buildQueries(properties.queries, timeRange)
if (type === ViewType.SingleStat) {
return [queries[0]]
}
if (type === ViewType.Gauge) {
return [queries[0]]
}
return queries
}
}
const mstp = ({sources, routing}): Partial<Props> => {
const sourceID = routing.locationBeforeTransitions.query.sourceID
const source = sources.find(s => s.id === sourceID)
const link = source.links.query
return {
link,
}
}
const mdtp = {
handleSetHoverTime: setHoverTime,
}
export default connect(mstp, mdtp)(withRouter(RefreshingView))

View File

@ -1,168 +0,0 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import ResizeHandle from 'shared/components/ResizeHandle'
import {ErrorHandling} from 'src/shared/decorators/errors'
const maximumNumChildren = 2
const defaultMinTopHeight = 200
const defaultMinBottomHeight = 200
const defaultInitialTopHeight = '50%'
const defaultInitialBottomHeight = '50%'
@ErrorHandling
class ResizeContainer extends Component {
constructor(props) {
super(props)
this.state = {
isDragging: false,
topHeight: props.initialTopHeight,
bottomHeight: props.initialBottomHeight,
}
}
static defaultProps = {
minTopHeight: defaultMinTopHeight,
minBottomHeight: defaultMinBottomHeight,
initialTopHeight: defaultInitialTopHeight,
initialBottomHeight: defaultInitialBottomHeight,
}
componentDidMount() {
this.setState({
bottomHeightPixels: this.bottom.getBoundingClientRect().height,
topHeightPixels: this.top.getBoundingClientRect().height,
})
}
handleStartDrag = () => {
this.setState({isDragging: true})
}
handleStopDrag = () => {
this.setState({isDragging: false})
}
handleMouseLeave = () => {
this.setState({isDragging: false})
}
handleDrag = e => {
if (!this.state.isDragging) {
return
}
const {minTopHeight, minBottomHeight} = this.props
const oneHundred = 100
const containerHeight = parseInt(
getComputedStyle(this.resizeContainer).height,
10
)
// verticalOffset moves the resize handle as many pixels as the page-heading is taking up.
const verticalOffset = window.innerHeight - containerHeight
const newTopPanelPercent = Math.ceil(
(e.pageY - verticalOffset) / containerHeight * oneHundred
)
const newBottomPanelPercent = oneHundred - newTopPanelPercent
// Don't trigger a resize unless the change in size is greater than minResizePercentage
const minResizePercentage = 0.5
if (
Math.abs(newTopPanelPercent - parseFloat(this.state.topHeight)) <
minResizePercentage
) {
return
}
const topHeightPixels = newTopPanelPercent / oneHundred * containerHeight
const bottomHeightPixels =
newBottomPanelPercent / oneHundred * containerHeight
// Don't trigger a resize if the new sizes are too small
if (
topHeightPixels < minTopHeight ||
bottomHeightPixels < minBottomHeight
) {
return
}
this.setState({
topHeight: `${newTopPanelPercent}%`,
bottomHeight: `${newBottomPanelPercent}%`,
bottomHeightPixels,
topHeightPixels,
})
}
render() {
const {
topHeightPixels,
bottomHeightPixels,
topHeight,
bottomHeight,
isDragging,
} = this.state
const {containerClass, children, theme} = this.props
if (React.Children.count(children) > maximumNumChildren) {
console.error(
`There cannot be more than ${maximumNumChildren}' children in ResizeContainer`
)
return
}
return (
<div
className={classnames(`resize--container ${containerClass}`, {
'resize--dragging': isDragging,
})}
onMouseLeave={this.handleMouseLeave}
onMouseUp={this.handleStopDrag}
onMouseMove={this.handleDrag}
ref={r => (this.resizeContainer = r)}
>
<div
className="resize--top"
style={{height: topHeight}}
ref={r => (this.top = r)}
>
{React.cloneElement(children[0], {
resizerBottomHeight: bottomHeightPixels,
resizerTopHeight: topHeightPixels,
})}
</div>
<ResizeHandle
theme={theme}
isDragging={isDragging}
onHandleStartDrag={this.handleStartDrag}
top={topHeight}
/>
<div
className="resize--bottom"
style={{height: bottomHeight, top: topHeight}}
ref={r => (this.bottom = r)}
>
{React.cloneElement(children[1], {
resizerBottomHeight: bottomHeightPixels,
resizerTopHeight: topHeightPixels,
})}
</div>
</div>
)
}
}
const {node, number, string} = PropTypes
ResizeContainer.propTypes = {
children: node.isRequired,
containerClass: string.isRequired,
minTopHeight: number,
minBottomHeight: number,
initialTopHeight: string,
initialBottomHeight: string,
theme: string,
}
export default ResizeContainer

View File

@ -1,34 +1,23 @@
// Libraries
import React, {PureComponent, CSSProperties} from 'react' import React, {PureComponent, CSSProperties} from 'react'
import classnames from 'classnames'
import getLastValues from 'src/shared/parsing/lastValues'
import _ from 'lodash' import _ from 'lodash'
import {SMALL_CELL_HEIGHT} from 'src/shared/graphs/helpers' // Constants
import {DYGRAPH_CONTAINER_V_MARGIN} from 'src/shared/constants'
import {generateThresholdsListHexs} from 'src/shared/constants/colorOperations' import {generateThresholdsListHexs} from 'src/shared/constants/colorOperations'
import {ColorString} from 'src/types/colors'
import {CellType, DecimalPlaces} from 'src/types/dashboards' // Types
import {TimeSeriesServerResponse} from 'src/types/series' import {CellType} from 'src/types/dashboards'
import {SingleStatView} from 'src/types/v2/dashboards'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props { interface Props {
decimalPlaces: DecimalPlaces properties: SingleStatView
cellHeight: number stat: number
colors: ColorString[]
prefix?: string
suffix?: string
lineGraph: boolean
staticLegendHeight?: number
data: TimeSeriesServerResponse[]
} }
@ErrorHandling @ErrorHandling
class SingleStat extends PureComponent<Props> { class SingleStat extends PureComponent<Props> {
public static defaultProps: Partial<Props> = {
prefix: '',
suffix: '',
}
public render() { public render() {
return ( return (
<div className="single-stat" style={this.containerStyle}> <div className="single-stat" style={this.containerStyle}>
@ -37,33 +26,18 @@ class SingleStat extends PureComponent<Props> {
) )
} }
private get renderShadow(): JSX.Element {
const {lineGraph} = this.props
return lineGraph && <div className="single-stat--shadow" />
}
private get prefixSuffixValue(): string { private get prefixSuffixValue(): string {
const {prefix, suffix} = this.props const {prefix, suffix} = this.props.properties
return `${prefix}${this.roundedLastValue}${suffix}` return `${prefix}${this.roundedLastValue}${suffix}`
} }
private get lastValue(): number { private get lastValue(): number {
const {data} = this.props return this.props.stat
const {lastValues, series} = getLastValues(data)
const firstAlphabeticalSeriesName = _.sortBy(series)[0]
const firstAlphabeticalIndex = _.indexOf(
series,
firstAlphabeticalSeriesName
)
return lastValues[firstAlphabeticalIndex]
} }
private get roundedLastValue(): string { private get roundedLastValue(): string {
const {decimalPlaces} = this.props const {decimalPlaces} = this.props.properties
if (this.lastValue === null) { if (this.lastValue === null) {
return `${0}` return `${0}`
@ -84,41 +58,21 @@ class SingleStat extends PureComponent<Props> {
} }
private get containerStyle(): CSSProperties { private get containerStyle(): CSSProperties {
const {staticLegendHeight} = this.props
const height = `calc(100% - ${staticLegendHeight +
DYGRAPH_CONTAINER_V_MARGIN * 2}px)`
const {backgroundColor} = this.coloration const {backgroundColor} = this.coloration
if (staticLegendHeight) {
return {
backgroundColor,
height,
}
}
return { return {
backgroundColor, backgroundColor,
} }
} }
private get coloration(): CSSProperties { private get coloration(): CSSProperties {
const {data, colors, lineGraph} = this.props const {colors} = this.props.properties
const {lastValues, series} = getLastValues(data)
const firstAlphabeticalSeriesName = _.sortBy(series)[0]
const firstAlphabeticalIndex = _.indexOf(
series,
firstAlphabeticalSeriesName
)
const lastValue = lastValues[firstAlphabeticalIndex]
const {bgColor, textColor} = generateThresholdsListHexs({ const {bgColor, textColor} = generateThresholdsListHexs({
colors, colors,
lastValue, lastValue: this.props.stat,
cellType: lineGraph ? CellType.LinePlusSingleStat : CellType.SingleStat, cellType: CellType.SingleStat,
}) })
return { return {
@ -128,22 +82,8 @@ class SingleStat extends PureComponent<Props> {
} }
private get resizerBox(): JSX.Element { private get resizerBox(): JSX.Element {
const {lineGraph, cellHeight} = this.props
const {color} = this.coloration const {color} = this.coloration
if (lineGraph) {
const className = classnames('single-stat--value', {
small: cellHeight <= SMALL_CELL_HEIGHT,
})
return (
<span className={className} style={{color}}>
{this.prefixSuffixValue}
{this.renderShadow}
</span>
)
}
const viewBox = `0 0 ${this.prefixSuffixValue.length * 55} 100` const viewBox = `0 0 ${this.prefixSuffixValue.length * 55} 100`
return ( return (

View File

@ -0,0 +1,42 @@
// Libraries
import React, {PureComponent} from 'react'
import _ from 'lodash'
// Parsing
import getLastValues from 'src/shared/parsing/flux/fluxToSingleStat'
// Types
import {FluxTable} from 'src/types'
interface Props {
tables: FluxTable[]
children: (stat: number) => JSX.Element
}
export default class SingleStatTransform extends PureComponent<Props> {
public render() {
const lastValue = +this.lastValue
if (!_.isNumber(lastValue)) {
return (
<div>
Could not display single stat because your values are non-numeric
</div>
)
}
return this.props.children(lastValue)
}
private get lastValue(): number {
const {tables} = this.props
const {series, values} = getLastValues(tables)
const firstAlphabeticalSeriesName = _.sortBy(series)[0]
const firstAlphabeticalIndex = _.indexOf(
series,
firstAlphabeticalSeriesName
)
return values[firstAlphabeticalIndex]
}
}

View File

@ -0,0 +1,89 @@
// Libraries
import React, {PureComponent} from 'react'
import Dygraph from 'src/shared/components/dygraph/Dygraph'
import DygraphCell from 'src/shared/components/DygraphCell'
import DygraphTransformation from 'src/shared/components/DygraphTransformation'
// Components
import {ErrorHandlingWith} from 'src/shared/decorators/errors'
import InvalidData from 'src/shared/components/InvalidData'
// Types
import {Options} from 'src/external/dygraph'
import {StackedView} from 'src/types/v2/dashboards'
import {TimeRange} from 'src/types/v2'
import {FluxTable, RemoteDataState} from 'src/types'
interface Props {
loading: RemoteDataState
properties: StackedView
timeRange: TimeRange
tables: FluxTable[]
viewID: string
staticLegend: boolean
onZoom: () => void
handleSetHoverTime: () => void
}
@ErrorHandlingWith(InvalidData)
class Stacked extends PureComponent<Props> {
public static defaultProps: Partial<Props> = {
staticLegend: false,
}
public render() {
const {
tables,
viewID,
onZoom,
loading,
timeRange,
properties,
staticLegend,
handleSetHoverTime,
} = this.props
const {axes, type, colors, queries} = properties
return (
<DygraphTransformation tables={tables}>
{({labels, dygraphsData}) => (
<DygraphCell loading={loading}>
<Dygraph
type={type}
axes={axes}
viewID={viewID}
colors={colors}
onZoom={onZoom}
labels={labels}
queries={queries}
options={this.options}
timeRange={timeRange}
timeSeries={dygraphsData}
staticLegend={staticLegend}
handleSetHoverTime={handleSetHoverTime}
/>
</DygraphCell>
)}
</DygraphTransformation>
)
}
private get options(): Partial<Options> {
return {
rightGap: 0,
yRangePad: 10,
labelsKMB: true,
fillGraph: true,
axisLabelWidth: 60,
animatedZooms: true,
drawAxesAtZero: true,
axisLineColor: '#383846',
gridLineColor: '#383846',
connectSeparatedPoints: true,
stackedGraph: true,
}
}
}
export default Stacked

View File

@ -0,0 +1,89 @@
// Libraries
import React, {PureComponent} from 'react'
import Dygraph from 'src/shared/components/dygraph/Dygraph'
import DygraphCell from 'src/shared/components/DygraphCell'
import DygraphTransformation from 'src/shared/components/DygraphTransformation'
// Components
import {ErrorHandlingWith} from 'src/shared/decorators/errors'
import InvalidData from 'src/shared/components/InvalidData'
// Types
import {Options} from 'src/external/dygraph'
import {StepPlotView} from 'src/types/v2/dashboards'
import {TimeRange} from 'src/types/v2'
import {FluxTable, RemoteDataState} from 'src/types'
interface Props {
loading: RemoteDataState
properties: StepPlotView
timeRange: TimeRange
tables: FluxTable[]
viewID: string
staticLegend: boolean
onZoom: () => void
handleSetHoverTime: () => void
}
@ErrorHandlingWith(InvalidData)
class StepPlot extends PureComponent<Props> {
public static defaultProps: Partial<Props> = {
staticLegend: false,
}
public render() {
const {
tables,
viewID,
onZoom,
loading,
timeRange,
properties,
staticLegend,
handleSetHoverTime,
} = this.props
const {axes, type, colors, queries} = properties
return (
<DygraphTransformation tables={tables}>
{({labels, dygraphsData}) => (
<DygraphCell loading={loading}>
<Dygraph
type={type}
axes={axes}
viewID={viewID}
colors={colors}
onZoom={onZoom}
labels={labels}
queries={queries}
options={this.options}
timeRange={timeRange}
timeSeries={dygraphsData}
staticLegend={staticLegend}
handleSetHoverTime={handleSetHoverTime}
/>
</DygraphCell>
)}
</DygraphTransformation>
)
}
private get options(): Partial<Options> {
return {
rightGap: 0,
yRangePad: 10,
labelsKMB: true,
fillGraph: true,
axisLabelWidth: 60,
animatedZooms: true,
drawAxesAtZero: true,
axisLineColor: '#383846',
gridLineColor: '#383846',
connectSeparatedPoints: true,
stepPlot: true,
}
}
}
export default StepPlot

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@ import Threshold from 'src/dashboards/components/Threshold'
import ColorDropdown from 'src/shared/components/ColorDropdown' import ColorDropdown from 'src/shared/components/ColorDropdown'
import {updateThresholdsListColors} from 'src/dashboards/actions/cellEditorOverlay' import {updateThresholdsListColors} from 'src/dashboards/actions/cellEditorOverlay'
import {ColorNumber} from 'src/types/colors' import {Color} from 'src/types/colors'
import { import {
THRESHOLD_COLORS, THRESHOLD_COLORS,
@ -24,8 +24,8 @@ interface Props {
onResetFocus: () => void onResetFocus: () => void
showListHeading: boolean showListHeading: boolean
thresholdsListType: string thresholdsListType: string
thresholdsListColors: ColorNumber[] thresholdsListColors: Color[]
handleUpdateThresholdsListColors: (c: ColorNumber[]) => void handleUpdateThresholdsListColors: (c: Color[]) => void
} }
@ErrorHandling @ErrorHandling
@ -99,8 +99,8 @@ class ThresholdsList extends PureComponent<Props> {
const randomColor = _.random(0, THRESHOLD_COLORS.length - 1) const randomColor = _.random(0, THRESHOLD_COLORS.length - 1)
const maxValue = DEFAULT_VALUE_MIN const maxValue = +DEFAULT_VALUE_MIN
const minValue = DEFAULT_VALUE_MAX const minValue = +DEFAULT_VALUE_MAX
let randomValue = _.round(_.random(minValue, maxValue, true), 2) let randomValue = _.round(_.random(minValue, maxValue, true), 2)
@ -108,7 +108,7 @@ class ThresholdsList extends PureComponent<Props> {
const colorsValues = _.mapValues(thresholdsListColors, 'value') const colorsValues = _.mapValues(thresholdsListColors, 'value')
do { do {
randomValue = _.round(_.random(minValue, maxValue, true), 2) randomValue = _.round(_.random(minValue, maxValue, true), 2)
} while (_.includes(colorsValues, randomValue)) } while (_.includes(colorsValues, `${randomValue}`))
} }
const newThreshold = { const newThreshold = {
@ -122,7 +122,7 @@ class ThresholdsList extends PureComponent<Props> {
const updatedColors = _.sortBy( const updatedColors = _.sortBy(
[...thresholdsListColors, newThreshold], [...thresholdsListColors, newThreshold],
color => color.value color => color.value
) ) as Color[]
handleUpdateThresholdsListColors(updatedColors) handleUpdateThresholdsListColors(updatedColors)
onResetFocus() onResetFocus()

View File

@ -1,34 +0,0 @@
import React, {SFC} from 'react'
import NewsFeed from 'src/status/components/NewsFeed'
import GettingStarted from 'src/status/components/GettingStarted'
import {Cell} from 'src/types/dashboards'
import {Source} from 'src/types/sources'
import {TimeRange} from 'src/types/queries'
interface Props {
timeRange: TimeRange
cell: Cell
source: Source
}
const WidgetCell: SFC<Props> = ({cell, source}) => {
switch (cell.type) {
case 'news': {
return <NewsFeed source={source} />
}
case 'guide': {
return <GettingStarted />
}
default: {
return (
<div className="graph-empty">
<p data-test="data-explorer-no-results">Nothing to show</p>
</div>
)
}
}
}
export default WidgetCell

View File

@ -12,7 +12,7 @@ import {getView} from 'src/dashboards/apis/v2/view'
// Types // Types
import {CellQuery, RemoteDataState, Template, TimeRange} from 'src/types' import {CellQuery, RemoteDataState, Template, TimeRange} from 'src/types'
import {Cell, View, ViewShape} from 'src/types/v2' import {Cell, View} from 'src/types/v2'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
@ -89,10 +89,6 @@ export default class CellComponent extends Component<Props, State> {
return null return null
} }
if (view.properties.shape === ViewShape.Empty) {
return this.emptyGraph
}
return ( return (
<ViewComponent <ViewComponent
view={view} view={view}
@ -101,23 +97,11 @@ export default class CellComponent extends Component<Props, State> {
timeRange={timeRange} timeRange={timeRange}
autoRefresh={autoRefresh} autoRefresh={autoRefresh}
manualRefresh={manualRefresh} manualRefresh={manualRefresh}
onSummonOverlay={this.handleSummonOverlay}
/> />
) )
} }
private get emptyGraph(): JSX.Element {
return (
<div className="graph-empty">
<button
className="no-query--button btn btn-md btn-primary"
onClick={this.handleSummonOverlay}
>
<span className="icon plus" /> Add Data
</button>
</div>
)
}
private handleSummonOverlay = (): void => { private handleSummonOverlay = (): void => {
// TODO: add back in once CEO is refactored // TODO: add back in once CEO is refactored
} }

View File

@ -2,13 +2,15 @@
import React, {Component} from 'react' import React, {Component} from 'react'
// Components // Components
import RefreshingGraph from 'src/shared/components/RefreshingGraph'
import Markdown from 'src/shared/components/views/Markdown' import Markdown from 'src/shared/components/views/Markdown'
import RefreshingView from 'src/shared/components/RefreshingView'
// Constants
import {text} from 'src/shared/components/views/gettingsStarted'
// Types // Types
import {TimeRange, Template} from 'src/types' import {TimeRange, Template} from 'src/types'
import {View, ViewType} from 'src/types/v2' import {View, ViewType, ViewShape} from 'src/types/v2'
import {text} from 'src/shared/components/views/gettingsStarted'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
@ -19,6 +21,7 @@ interface Props {
autoRefresh: number autoRefresh: number
manualRefresh: number manualRefresh: number
onZoom: (range: TimeRange) => void onZoom: (range: TimeRange) => void
onSummonOverlay: () => void
} }
@ErrorHandling @ErrorHandling
@ -37,24 +40,41 @@ class ViewComponent extends Component<Props> {
templates, templates,
} = this.props } = this.props
if (view.properties.shape === ViewShape.Empty) {
return this.emptyGraph
}
if (view.properties.type === ViewType.Markdown) { if (view.properties.type === ViewType.Markdown) {
return <Markdown text={text} /> return <Markdown text={text} />
} }
return ( return (
<RefreshingGraph <RefreshingView
viewID={view.id} viewID={view.id}
onZoom={onZoom} onZoom={onZoom}
timeRange={timeRange} timeRange={timeRange}
templates={templates} templates={templates}
autoRefresh={autoRefresh} autoRefresh={autoRefresh}
options={view.properties} properties={view.properties}
manualRefresh={manualRefresh} manualRefresh={manualRefresh}
grabDataForDownload={this.grabDataForDownload} grabDataForDownload={this.grabDataForDownload}
/> />
) )
} }
private get emptyGraph(): JSX.Element {
return (
<div className="graph-empty">
<button
className="no-query--button btn btn-md btn-primary"
onClick={this.props.onSummonOverlay}
>
<span className="icon plus" /> Add Data
</button>
</div>
)
}
private grabDataForDownload = cellData => { private grabDataForDownload = cellData => {
this.setState({cellData}) this.setState({cellData})
} }

View File

@ -5,7 +5,7 @@ import NanoDate from 'nano-date'
import ReactResizeDetector from 'react-resize-detector' import ReactResizeDetector from 'react-resize-detector'
// Components // Components
import D from 'src/external/dygraph' import Dygraphs from 'src/external/dygraph'
import DygraphLegend from 'src/shared/components/DygraphLegend' import DygraphLegend from 'src/shared/components/DygraphLegend'
import StaticLegend from 'src/shared/components/StaticLegend' import StaticLegend from 'src/shared/components/StaticLegend'
import Crosshair from 'src/shared/components/crosshair/Crosshair' import Crosshair from 'src/shared/components/crosshair/Crosshair'
@ -35,38 +35,29 @@ const {LOG, BASE_10, BASE_2} = AXES_SCALE_OPTIONS
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
// Types // Types
import { import {Color} from 'src/types/colors'
Axes, import {Axes, TimeRange} from 'src/types'
TimeRange,
DygraphData,
DygraphClass,
DygraphSeries,
Constructable,
} from 'src/types'
import {LineColor} from 'src/types/colors'
import {CellQuery, ViewType} from 'src/types/v2/dashboards' import {CellQuery, ViewType} from 'src/types/v2/dashboards'
import {DygraphData, DygraphSeries, Options} from 'src/external/dygraph'
const Dygraphs = D as Constructable<DygraphClass>
interface Props { interface Props {
type: ViewType type: ViewType
viewID?: string
queries?: CellQuery[]
timeSeries: DygraphData timeSeries: DygraphData
labels: string[] labels: string[]
options: dygraphs.Options options: Partial<Options>
containerStyle: object // TODO colors: Color[]
dygraphSeries: DygraphSeries
timeRange?: TimeRange
colors: LineColor[]
handleSetHoverTime: (t: string) => void handleSetHoverTime: (t: string) => void
viewID?: string
axes?: Axes axes?: Axes
isGraphFilled?: boolean mode?: string
staticLegend?: boolean queries?: CellQuery[]
timeRange?: TimeRange
dygraphSeries?: DygraphSeries
underlayCallback?: () => void
setResolution?: (w: number) => void setResolution?: (w: number) => void
onZoom?: (timeRange: TimeRange) => void onZoom?: (timeRange: TimeRange) => void
mode?: string isGraphFilled?: boolean
underlayCallback?: () => void staticLegend?: boolean
} }
interface State { interface State {
@ -92,17 +83,17 @@ class Dygraph extends Component<Props, State> {
...DEFAULT_AXIS, ...DEFAULT_AXIS,
}, },
}, },
containerStyle: {},
isGraphFilled: true, isGraphFilled: true,
onZoom: () => {}, onZoom: () => {},
staticLegend: false, staticLegend: false,
setResolution: () => {}, setResolution: () => {},
handleSetHoverTime: () => {}, handleSetHoverTime: () => {},
underlayCallback: () => {}, underlayCallback: () => {},
dygraphSeries: {},
} }
private graphRef: React.RefObject<HTMLDivElement> private graphRef: React.RefObject<HTMLDivElement>
private dygraph: DygraphClass private dygraph: Dygraphs
constructor(props: Props) { constructor(props: Props) {
super(props) super(props)
@ -127,7 +118,7 @@ class Dygraph extends Component<Props, State> {
const timeSeries = this.timeSeries const timeSeries = this.timeSeries
let defaultOptions = { let defaultOptions: Partial<Options> = {
...options, ...options,
labels, labels,
fillGraph, fillGraph,
@ -311,21 +302,30 @@ class Dygraph extends Component<Props, State> {
return null return null
} }
private get containerStyle(): CSSProperties {
return {
width: 'calc(100% - 32px)',
height: 'calc(100% - 16px)',
position: 'absolute',
top: '8px',
}
}
private get dygraphStyle(): CSSProperties { private get dygraphStyle(): CSSProperties {
const {containerStyle, staticLegend} = this.props const {staticLegend} = this.props
const {staticLegendHeight} = this.state const {staticLegendHeight} = this.state
if (staticLegend) { if (staticLegend) {
const cellVerticalPadding = 16 const cellVerticalPadding = 16
return { return {
...containerStyle, ...this.containerStyle,
zIndex: 2, zIndex: 2,
height: `calc(100% - ${staticLegendHeight + cellVerticalPadding}px)`, height: `calc(100% - ${staticLegendHeight + cellVerticalPadding}px)`,
} }
} }
return {...containerStyle, zIndex: 2} return {...this.containerStyle, zIndex: 2}
} }
private getYRange = (timeSeries: DygraphData): [number, number] => { private getYRange = (timeSeries: DygraphData): [number, number] => {

View File

@ -14,7 +14,7 @@ import AutoRefresh from 'src/utils/AutoRefresh'
export const DEFAULT_TIME_SERIES = [{response: {results: []}}] export const DEFAULT_TIME_SERIES = [{response: {results: []}}]
interface RenderProps { interface RenderProps {
timeSeries: FluxTable[] tables: FluxTable[]
loading: RemoteDataState loading: RemoteDataState
} }
@ -29,7 +29,7 @@ interface Props {
interface State { interface State {
loading: RemoteDataState loading: RemoteDataState
isFirstFetch: boolean isFirstFetch: boolean
timeSeries: FluxTable[] tables: FluxTable[]
} }
class TimeSeries extends Component<Props, State> { class TimeSeries extends Component<Props, State> {
@ -41,9 +41,9 @@ class TimeSeries extends Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props) super(props)
this.state = { this.state = {
timeSeries: [],
loading: RemoteDataState.NotStarted, loading: RemoteDataState.NotStarted,
isFirstFetch: false, isFirstFetch: false,
tables: [],
} }
} }
@ -73,7 +73,7 @@ class TimeSeries extends Component<Props, State> {
} }
if (!queries.length) { if (!queries.length) {
return this.setState({timeSeries: []}) return this.setState({tables: []})
} }
this.setState({loading: RemoteDataState.Loading, isFirstFetch}) this.setState({loading: RemoteDataState.Loading, isFirstFetch})
@ -81,7 +81,7 @@ class TimeSeries extends Component<Props, State> {
const TEMP_RES = 300 const TEMP_RES = 300
try { try {
const timeSeries = await fetchTimeSeries( const tables = await fetchTimeSeries(
link, link,
this.queries, this.queries,
TEMP_RES, TEMP_RES,
@ -89,7 +89,7 @@ class TimeSeries extends Component<Props, State> {
) )
this.setState({ this.setState({
timeSeries, tables,
loading: RemoteDataState.Done, loading: RemoteDataState.Done,
}) })
} catch (err) { } catch (err) {
@ -98,9 +98,9 @@ class TimeSeries extends Component<Props, State> {
} }
public render() { public render() {
const {timeSeries, loading, isFirstFetch} = this.state const {tables, loading, isFirstFetch} = this.state
const hasValues = _.some(timeSeries, s => { const hasValues = _.some(tables, s => {
const data = _.get(s, 'data', []) const data = _.get(s, 'data', [])
return !!data.length return !!data.length
}) })
@ -121,7 +121,7 @@ class TimeSeries extends Component<Props, State> {
) )
} }
return this.props.children({timeSeries, loading}) return this.props.children({tables, loading})
} }
private get queries(): string[] { private get queries(): string[] {

View File

@ -1,6 +1,6 @@
import chroma from 'chroma-js' import chroma from 'chroma-js'
import uuid from 'uuid' import uuid from 'uuid'
import {LineColor} from 'src/types/colors' import {Color} from 'src/types/colors'
const COLOR_TYPE_SCALE = 'scale' const COLOR_TYPE_SCALE = 'scale'
@ -208,10 +208,7 @@ export const LINE_COLOR_SCALES = [
return {name, colors, id} return {name, colors, id}
}) })
export const validateLineColors = ( export const validateLineColors = (colors: Color[], numSeries = 0): Color[] => {
colors: LineColor[],
numSeries = 0
): LineColor[] => {
const multipleSeriesButOneColor = numSeries > 1 && colors.length < 2 const multipleSeriesButOneColor = numSeries > 1 && colors.length < 2
if (!colors || colors.length === 0 || multipleSeriesButOneColor) { if (!colors || colors.length === 0 || multipleSeriesButOneColor) {
return DEFAULT_LINE_COLORS return DEFAULT_LINE_COLORS
@ -225,7 +222,7 @@ export const validateLineColors = (
} }
export const getLineColorsHexes = ( export const getLineColorsHexes = (
colors: LineColor[], colors: Color[],
numSeries: number numSeries: number
): string[] => { ): string[] => {
const validatedColors = validateLineColors(colors, numSeries) // ensures safe defaults const validatedColors = validateLineColors(colors, numSeries) // ensures safe defaults

View File

@ -4,9 +4,9 @@ export const MAX_THRESHOLDS = 5
export const MIN_THRESHOLDS = 2 export const MIN_THRESHOLDS = 2
export const COLOR_TYPE_MIN = 'min' export const COLOR_TYPE_MIN = 'min'
export const DEFAULT_VALUE_MIN = 0 export const DEFAULT_VALUE_MIN = '0'
export const COLOR_TYPE_MAX = 'max' export const COLOR_TYPE_MAX = 'max'
export const DEFAULT_VALUE_MAX = 100 export const DEFAULT_VALUE_MAX = '100'
export const COLOR_TYPE_THRESHOLD = 'threshold' export const COLOR_TYPE_THRESHOLD = 'threshold'
export const THRESHOLD_TYPE_TEXT = 'text' export const THRESHOLD_TYPE_TEXT = 'text'
@ -115,7 +115,7 @@ export const DEFAULT_THRESHOLDS_LIST_COLORS = [
hex: THRESHOLD_COLORS[11].hex, hex: THRESHOLD_COLORS[11].hex,
id: THRESHOLD_TYPE_BASE, id: THRESHOLD_TYPE_BASE,
name: THRESHOLD_COLORS[11].name, name: THRESHOLD_COLORS[11].name,
value: -999999999999999999, value: '-999999999999999999',
}, },
] ]

View File

@ -1,5 +1,9 @@
// Libraries
import _ from 'lodash' import _ from 'lodash'
import {FluxTable, DygraphValue} from 'src/types'
// Types
import {FluxTable} from 'src/types'
import {DygraphValue} from 'src/external/dygraph'
const COLUMN_BLACKLIST = new Set([ const COLUMN_BLACKLIST = new Set([
'_time', '_time',
@ -47,7 +51,7 @@ export const fluxTablesToDygraph = (
} }
const uniqueColumnName = Object.entries(table.groupKey).reduce( const uniqueColumnName = Object.entries(table.groupKey).reduce(
(acc, [k, v]) => acc + `[${k}=${v}]`, (acc, [k, v]) => `${acc}[${k}=${v}]`,
columnName columnName
) )

View File

@ -0,0 +1,28 @@
import _ from 'lodash'
import {FluxTable} from 'src/types'
import {parseTablesByTime} from 'src/shared/parsing/flux/parseTablesByTime'
export interface LastValues {
values: number[]
series: string[]
}
export default (tables: FluxTable[]): LastValues => {
const {tablesByTime} = parseTablesByTime(tables)
const lastValues = _.reduce(
tablesByTime,
(acc, table) => {
const lastTime = _.last(Object.keys(table))
const values = table[lastTime]
_.forEach(values, (value, series) => {
acc.series.push(series)
acc.values.push(value)
})
return acc
},
{values: [], series: []}
)
return lastValues
}

View File

@ -0,0 +1,83 @@
import {FluxTable} from 'src/types'
const COLUMN_BLACKLIST = new Set([
'_time',
'result',
'table',
'_start',
'_stop',
'',
])
const NUMERIC_DATATYPES = ['double', 'long', 'int', 'float']
interface TableByTime {
[time: string]: {[columnName: string]: string}
}
interface ParseTablesByTimeResult {
tablesByTime: TableByTime[]
allColumnNames: string[]
nonNumericColumns: string[]
}
export const parseTablesByTime = (
tables: FluxTable[]
): ParseTablesByTimeResult => {
const allColumnNames = []
const nonNumericColumns = []
const tablesByTime = tables.map(table => {
const header = table.data[0] as string[]
const columnNames: {[k: number]: string} = {}
for (let i = 0; i < header.length; i++) {
const columnName = header[i]
const dataType = table.dataTypes[columnName]
if (COLUMN_BLACKLIST.has(columnName)) {
continue
}
if (table.groupKey[columnName]) {
continue
}
if (!NUMERIC_DATATYPES.includes(dataType)) {
nonNumericColumns.push(columnName)
continue
}
const uniqueColumnName = Object.entries(table.groupKey).reduce(
(acc, [k, v]) => acc + `[${k}=${v}]`,
columnName
)
columnNames[i] = uniqueColumnName
allColumnNames.push(uniqueColumnName)
}
const timeIndex = header.indexOf('_time')
if (timeIndex < 0) {
throw new Error('Could not find time index in FluxTable')
}
const result = {}
for (let i = 1; i < table.data.length; i++) {
const row = table.data[i]
const time = row[timeIndex].toString()
result[time] = Object.entries(columnNames).reduce(
(acc, [valueIndex, columnName]) => ({
...acc,
[columnName]: row[valueIndex],
}),
{}
)
}
return result
})
return {nonNumericColumns, tablesByTime, allColumnNames}
}

View File

@ -1,10 +0,0 @@
import lastValues from 'src/shared/parsing/lastValues'
describe('lastValues', () => {
it('returns the correct value when response is empty', () => {
expect(lastValues([])).toEqual({
lastValues: [''],
series: [''],
})
})
})

View File

@ -1,28 +0,0 @@
import _ from 'lodash'
import {Data} from 'src/types/dygraphs'
import {TimeSeriesServerResponse} from 'src/types/series'
interface Result {
lastValues: number[]
series: string[]
}
export default function(
timeSeriesResponse: TimeSeriesServerResponse[] | Data | null
): Result {
const values = _.get(
timeSeriesResponse,
['0', 'response', 'results', '0', 'series', '0', 'values'],
[['', '']]
)
const series = _.get(
timeSeriesResponse,
['0', 'response', 'results', '0', 'series', '0', 'columns'],
['', '']
).slice(1)
const lastValues = values[values.length - 1].slice(1) // remove time with slice 1
return {lastValues, series}
}

View File

@ -1,6 +1,6 @@
import React, {SFC, ReactElement} from 'react' import React, {SFC} from 'react'
const InfluxTableHead: SFC = (): ReactElement<HTMLTableHeaderCellElement> => { const InfluxTableHead: SFC = (): JSX.Element => {
return ( return (
<thead> <thead>
<tr> <tr>

View File

@ -1,4 +1,4 @@
import React, {PureComponent, ReactElement} from 'react' import React, {PureComponent} from 'react'
import {Link} from 'react-router' import {Link} from 'react-router'
import ConfirmButton from 'src/shared/components/ConfirmButton' import ConfirmButton from 'src/shared/components/ConfirmButton'
@ -42,7 +42,7 @@ class InfluxTableRow extends PureComponent<Props> {
this.props.onDeleteSource(this.props.source) this.props.onDeleteSource(this.props.source)
} }
private get connectButton(): ReactElement<HTMLDivElement> { private get connectButton(): JSX.Element {
const {source} = this.props const {source} = this.props
if (this.isCurrentSource) { if (this.isCurrentSource) {
return ( return (

View File

@ -1,15 +1,11 @@
interface ColorBase { export interface Color {
type: string type: string
hex: string hex: string
id: string id: string
name: string name: string
value: string
} }
export type ColorString = ColorBase & {value: string}
export type ColorNumber = ColorBase & {value: number}
export type LineColor = ColorString
export type GaugeColor = ColorString | ColorNumber
export interface ThresholdColor { export interface ThresholdColor {
hex: string hex: string
name: string name: string

View File

@ -1,5 +1,5 @@
import {Template, TimeRange, QueryConfig} from 'src/types' import {Template, TimeRange, QueryConfig} from 'src/types'
import {ColorString} from 'src/types/colors' import {Color} from 'src/types/colors'
export interface Axis { export interface Axis {
label: string label: string
@ -68,7 +68,7 @@ export interface Cell {
queries: CellQuery[] queries: CellQuery[]
type: CellType type: CellType
axes: Axes axes: Axes
colors: ColorString[] colors: Color[]
tableOptions: TableOptions tableOptions: TableOptions
fieldOptions: FieldOption[] fieldOptions: FieldOption[]
timeFormat: string timeFormat: string

File diff suppressed because it is too large Load Diff

View File

@ -32,26 +32,17 @@ import {
SourceLinks, SourceLinks,
SourceAuthenticationMethod, SourceAuthenticationMethod,
} from './sources' } from './sources'
import {DropdownAction, DropdownItem, Constructable} from './shared' import {DropdownAction, DropdownItem} from './shared'
import { import {
Notification, Notification,
NotificationFunc, NotificationFunc,
NotificationAction, NotificationAction,
} from './notifications' } from './notifications'
import {FluxTable, ScriptStatus, SchemaFilter, RemoteDataState} from './flux' import {FluxTable, ScriptStatus, SchemaFilter, RemoteDataState} from './flux'
import {
DygraphSeries,
DygraphValue,
DygraphAxis,
DygraphClass,
DygraphData,
} from './dygraphs'
import {JSONFeedData} from './status'
import {AnnotationInterface} from './annotations' import {AnnotationInterface} from './annotations'
import {WriteDataMode} from './dataExplorer' import {WriteDataMode} from './dataExplorer'
export { export {
Constructable,
Template, Template,
TemplateQuery, TemplateQuery,
TemplateValue, TemplateValue,
@ -79,11 +70,6 @@ export {
DropdownAction, DropdownAction,
DropdownItem, DropdownItem,
TimeRange, TimeRange,
DygraphData,
DygraphSeries,
DygraphValue,
DygraphAxis,
DygraphClass,
Notification, Notification,
NotificationFunc, NotificationFunc,
NotificationAction, NotificationAction,
@ -97,7 +83,6 @@ export {
ScriptStatus, ScriptStatus,
SchemaFilter, SchemaFilter,
RemoteDataState, RemoteDataState,
JSONFeedData,
AnnotationInterface, AnnotationInterface,
TemplateType, TemplateType,
TemplateValueType, TemplateValueType,

View File

@ -1,5 +1,4 @@
import {ReactNode} from 'react' import {ReactNode} from 'react'
import {DygraphData, Options} from 'src/types/dygraphs'
export interface DropdownItem { export interface DropdownItem {
text: string text: string
@ -17,7 +16,3 @@ export interface PageSection {
component: ReactNode component: ReactNode
enabled: boolean enabled: boolean
} }
export interface Constructable<T> {
new (container: HTMLElement | string, data: DygraphData, options?: Options): T
}

View File

@ -1,5 +1,5 @@
import {QueryConfig} from 'src/types' import {QueryConfig} from 'src/types'
import {ColorString} from 'src/types/colors' import {Color} from 'src/types/colors'
export interface Axis { export interface Axis {
label: string label: string
@ -66,15 +66,117 @@ export interface MarkDownProperties {
export interface View { export interface View {
id: string id: string
name: string name: string
properties: MarkDownProperties | V1View properties: ViewProperties
}
export type RefreshingViewProperties =
| LineView
| StepPlotView
| StackedView
| BarChartView
| LinePlusSingleStatView
| SingleStatView
| TableView
| GaugeView
export type ViewProperties = RefreshingViewProperties | MarkdownView | EmptyView
export interface EmptyView {
type: ViewShape.Empty
shape: ViewShape.Empty
}
export interface LineView {
type: ViewType.Line
queries: CellQuery[]
shape: ViewShape.ChronografV2
axes: Axes
colors: Color[]
legend: Legend
}
export interface StackedView {
type: ViewType.Stacked
queries: CellQuery[]
shape: ViewShape.ChronografV2
axes: Axes
colors: Color[]
legend: Legend
}
export interface StepPlotView {
type: ViewType.StepPlot
queries: CellQuery[]
shape: ViewShape.ChronografV2
axes: Axes
colors: Color[]
legend: Legend
}
export interface BarChartView {
type: ViewType.Bar
queries: CellQuery[]
shape: ViewShape.ChronografV2
axes: Axes
colors: Color[]
legend: Legend
}
export interface LinePlusSingleStatView {
type: ViewType.LinePlusSingleStat
queries: CellQuery[]
shape: ViewShape.ChronografV2
axes: Axes
colors: Color[]
legend: Legend
prefix: string
suffix: string
decimalPlaces: DecimalPlaces
}
export interface SingleStatView {
type: ViewType.SingleStat
queries: CellQuery[]
shape: ViewShape.ChronografV2
colors: Color[]
prefix: string
suffix: string
decimalPlaces: DecimalPlaces
}
export interface GaugeView {
type: ViewType.Gauge
queries: CellQuery[]
shape: ViewShape.ChronografV2
colors: Color[]
prefix: string
suffix: string
decimalPlaces: DecimalPlaces
}
export interface TableView {
type: ViewType.Table
queries: CellQuery[]
shape: ViewShape.ChronografV2
colors: Color[]
tableOptions: TableOptions
fieldOptions: FieldOption[]
decimalPlaces: DecimalPlaces
timeFormat: string
}
export interface MarkdownView {
type: ViewType.Markdown
shape: ViewShape.ChronografV2
text: string
} }
export interface V1View { export interface V1View {
type: V1ViewTypes type: ViewType
queries: CellQuery[] queries: CellQuery[]
shape: ViewShape shape: ViewShape
axes: Axes axes: Axes
colors: ColorString[] colors: Color[]
tableOptions: TableOptions tableOptions: TableOptions
fieldOptions: FieldOption[] fieldOptions: FieldOption[]
timeFormat: string timeFormat: string
@ -85,31 +187,27 @@ export interface V1View {
} }
export enum ViewShape { export enum ViewShape {
ChronografV2 = 'chronografV2',
ChronografV1 = 'chronografV1', ChronografV1 = 'chronografV1',
Empty = 'empty', Empty = 'empty',
} }
export enum ViewType { export enum ViewType {
Markdown = 'markdown',
}
export enum V1ViewTypes {
Line = 'line',
Stacked = 'line-stacked',
StepPlot = 'line-stepplot',
Bar = 'bar', Bar = 'bar',
Line = 'line',
Stacked = 'stacked',
StepPlot = 'step-plot',
LinePlusSingleStat = 'line-plus-single-stat', LinePlusSingleStat = 'line-plus-single-stat',
SingleStat = 'single-stat', SingleStat = 'single-stat',
Gauge = 'gauge', Gauge = 'gauge',
Table = 'table', Table = 'table',
Alerts = 'alerts', Markdown = 'markdown',
News = 'news',
Guide = 'guide',
} }
interface DashboardLinks { interface DashboardLinks {
self: string self: string
cells: string cells: string
copy: string
} }
export interface Dashboard { export interface Dashboard {

View File

@ -1,679 +0,0 @@
import {
timeSeriesToDygraph,
timeSeriesToTableGraph,
} from 'src/utils/timeSeriesTransformers'
import {
filterTableColumns,
transformTableData,
} from 'src/dashboards/utils/tableGraph'
import {DEFAULT_SORT_DIRECTION} from 'src/shared/constants/tableGraph'
import {
DEFAULT_TIME_FORMAT,
DEFAULT_DECIMAL_PLACES,
} from 'src/dashboards/constants'
describe('timeSeriesToDygraph', () => {
it('parses a raw InfluxDB response into a dygraph friendly data format', () => {
const influxResponse = [
{
response: {
results: [
{
statement_id: 0,
series: [
{
name: 'm1',
columns: ['time', 'f1'],
values: [[1000, 1], [2000, 2]],
},
],
},
{
statement_id: 0,
series: [
{
name: 'm1',
columns: ['time', 'f2'],
values: [[2000, 3], [4000, 4]],
},
],
},
],
},
},
]
const actual = timeSeriesToDygraph(influxResponse)
const expected = {
labels: ['time', `m1.f1`, `m1.f2`],
timeSeries: [
[new Date(1000), 1, null],
[new Date(2000), 2, 3],
[new Date(4000), null, 4],
],
dygraphSeries: {
'm1.f1': {
axis: 'y',
},
'm1.f2': {
axis: 'y',
},
},
}
expect(actual).toEqual(expected)
})
it('can sort numerical timestamps correctly', () => {
const influxResponse = [
{
response: {
results: [
{
statement_id: 0,
series: [
{
name: 'm1',
columns: ['time', 'f1'],
values: [[100, 1], [3000, 3], [200, 2]],
},
],
},
],
},
},
]
const actual = timeSeriesToDygraph(influxResponse)
const expected = {
labels: ['time', 'm1.f1'],
timeSeries: [[new Date(100), 1], [new Date(200), 2], [new Date(3000), 3]],
}
expect(actual.timeSeries).toEqual(expected.timeSeries)
})
it('can parse multiple responses into two axes', () => {
const influxResponse = [
{
response: {
results: [
{
statement_id: 0,
series: [
{
name: 'm1',
columns: ['time', 'f1'],
values: [[1000, 1], [2000, 2]],
},
],
},
{
statement_id: 0,
series: [
{
name: 'm1',
columns: ['time', 'f2'],
values: [[2000, 3], [4000, 4]],
},
],
},
],
},
},
{
response: {
results: [
{
statement_id: 0,
series: [
{
name: 'm3',
columns: ['time', 'f3'],
values: [[1000, 1], [2000, 2]],
},
],
},
],
},
},
]
const actual = timeSeriesToDygraph(influxResponse)
const expected = {
'm1.f1': {
axis: 'y',
},
'm1.f2': {
axis: 'y',
},
'm3.f3': {
axis: 'y2',
},
}
expect(actual.dygraphSeries).toEqual(expected)
})
it('can parse multiple responses with the same field and measurement', () => {
const influxResponse = [
{
response: {
results: [
{
statement_id: 0,
series: [
{
name: 'm1',
columns: ['time', 'f1'],
values: [[1000, 1], [2000, 2]],
},
],
},
],
},
},
{
response: {
results: [
{
statement_id: 0,
series: [
{
name: 'm1',
columns: ['time', 'f1'],
values: [[2000, 3], [4000, 4]],
},
],
},
],
},
},
]
const actual = timeSeriesToDygraph(influxResponse)
const expected = {
labels: ['time', `m1.f1`, `m1.f1`],
timeSeries: [
[new Date(1000), 1, null],
[new Date(2000), 2, 3],
[new Date(4000), null, 4],
],
dygraphSeries: {
'm1.f1': {
axis: 'y2',
},
},
}
expect(actual).toEqual(expected)
})
it('it does not use multiple axes if being used for the DataExplorer', () => {
const influxResponse = [
{
response: {
results: [
{
statement_id: 0,
series: [
{
name: 'm1',
columns: ['time', 'f1'],
values: [[1000, 1], [2000, 2]],
},
],
},
],
},
},
{
response: {
results: [
{
statement_id: 0,
series: [
{
name: 'm1',
columns: ['time', 'f2'],
values: [[2000, 3], [4000, 4]],
},
],
},
],
},
},
]
const actual = timeSeriesToDygraph(influxResponse, 'data-explorer')
const expected = {}
expect(actual.dygraphSeries).toEqual(expected)
})
it('parses a raw InfluxDB response into a dygraph friendly data format', () => {
const influxResponse = [
{
response: {
results: [
{
statement_id: 0,
series: [
{
name: 'mb',
columns: ['time', 'f1'],
values: [[1000, 1], [2000, 2]],
},
],
},
{
statement_id: 0,
series: [
{
name: 'ma',
columns: ['time', 'f1'],
values: [[1000, 1], [2000, 2]],
},
],
},
{
statement_id: 0,
series: [
{
name: 'mc',
columns: ['time', 'f2'],
values: [[2000, 3], [4000, 4]],
},
],
},
{
statement_id: 0,
series: [
{
name: 'mc',
columns: ['time', 'f1'],
values: [[2000, 3], [4000, 4]],
},
],
},
],
},
},
]
const actual = timeSeriesToDygraph(influxResponse)
const expected = ['time', `ma.f1`, `mb.f1`, `mc.f1`, `mc.f2`]
expect(actual.labels).toEqual(expected)
})
})
describe('timeSeriesToTableGraph', () => {
it('parses raw data into a nested array of data', () => {
const influxResponse = [
{
response: {
results: [
{
statement_id: 0,
series: [
{
name: 'mb',
columns: ['time', 'f1'],
values: [[1000, 1], [2000, 2]],
},
],
},
{
statement_id: 0,
series: [
{
name: 'ma',
columns: ['time', 'f1'],
values: [[1000, 1], [2000, 2]],
},
],
},
{
statement_id: 0,
series: [
{
name: 'mc',
columns: ['time', 'f2'],
values: [[2000, 3], [4000, 4]],
},
],
},
{
statement_id: 0,
series: [
{
name: 'mc',
columns: ['time', 'f1'],
values: [[2000, 3], [4000, 4]],
},
],
},
],
},
},
]
const actual = timeSeriesToTableGraph(influxResponse)
const expected = [
['time', 'ma.f1', 'mb.f1', 'mc.f1', 'mc.f2'],
[1000, 1, 1, null, null],
[2000, 2, 2, 3, 3],
[4000, null, null, 4, 4],
]
expect(actual.data).toEqual(expected)
})
it('parses raw data into a table-readable format with the first row being labels', () => {
const influxResponse = [
{
response: {
results: [
{
statement_id: 0,
series: [
{
name: 'mb',
columns: ['time', 'f1'],
values: [[1000, 1], [2000, 2]],
},
],
},
{
statement_id: 0,
series: [
{
name: 'ma',
columns: ['time', 'f1'],
values: [[1000, 1], [2000, 2]],
},
],
},
{
statement_id: 0,
series: [
{
name: 'mc',
columns: ['time', 'f2'],
values: [[2000, 3], [4000, 4]],
},
],
},
{
statement_id: 0,
series: [
{
name: 'mc',
columns: ['time', 'f1'],
values: [[2000, 3], [4000, 4]],
},
],
},
],
},
},
]
const actual = timeSeriesToTableGraph(influxResponse)
const expected = ['time', 'ma.f1', 'mb.f1', 'mc.f1', 'mc.f2']
expect(actual.data[0]).toEqual(expected)
})
it('returns an array of an empty array if there is an empty response', () => {
const influxResponse = []
const actual = timeSeriesToTableGraph(influxResponse)
const expected = [[]]
expect(actual.data).toEqual(expected)
})
})
describe('filterTableColumns', () => {
it("returns a nested array of fieldnamesthat only include columns whose corresponding fieldName's visibility is true", () => {
const data = [
['time', 'f1', 'f2'],
[1000, 3000, 2000],
[2000, 1000, 3000],
[3000, 2000, 1000],
]
const fieldOptions = [
{internalName: 'time', displayName: 'Time', visible: true},
{internalName: 'f1', displayName: '', visible: false},
{internalName: 'f2', displayName: 'F2', visible: false},
]
const actual = filterTableColumns(data, fieldOptions)
const expected = [['time'], [1000], [2000], [3000]]
expect(actual).toEqual(expected)
})
it('returns an array of an empty array if all fieldOptions are not visible', () => {
const data = [
['time', 'f1', 'f2'],
[1000, 3000, 2000],
[2000, 1000, 3000],
[3000, 2000, 1000],
]
const fieldOptions = [
{internalName: 'time', displayName: 'Time', visible: false},
{internalName: 'f1', displayName: '', visible: false},
{internalName: 'f2', displayName: 'F2', visible: false},
]
const actual = filterTableColumns(data, fieldOptions)
const expected = [[]]
expect(actual).toEqual(expected)
})
})
describe('transformTableData', () => {
it('sorts the data based on the provided sortFieldName', () => {
const data = [
['time', 'f1', 'f2'],
[1000, 3000, 2000],
[2000, 1000, 3000],
[3000, 2000, 1000],
]
const sort = {field: 'f1', direction: DEFAULT_SORT_DIRECTION}
const sortBy = {internalName: 'time', displayName: 'Time', visible: true}
const tableOptions = {
verticalTimeAxis: true,
sortBy,
fixFirstColumn: true,
}
const timeFormat = DEFAULT_TIME_FORMAT
const decimalPlaces = DEFAULT_DECIMAL_PLACES
const fieldOptions = [
{internalName: 'time', displayName: 'Time', visible: true},
{internalName: 'f1', displayName: '', visible: true},
{internalName: 'f2', displayName: 'F2', visible: true},
]
const actual = transformTableData(
data,
sort,
fieldOptions,
tableOptions,
timeFormat,
decimalPlaces
)
const expected = [
['time', 'f1', 'f2'],
[2000, 1000, 3000],
[3000, 2000, 1000],
[1000, 3000, 2000],
]
expect(actual.transformedData).toEqual(expected)
})
it('filters out columns that should not be visible', () => {
const data = [
['time', 'f1', 'f2'],
[1000, 3000, 2000],
[2000, 1000, 3000],
[3000, 2000, 1000],
]
const sort = {field: 'time', direction: DEFAULT_SORT_DIRECTION}
const sortBy = {internalName: 'time', displayName: 'Time', visible: true}
const tableOptions = {
verticalTimeAxis: true,
sortBy,
fixFirstColumn: true,
}
const timeFormat = DEFAULT_TIME_FORMAT
const decimalPlaces = DEFAULT_DECIMAL_PLACES
const fieldOptions = [
{internalName: 'time', displayName: 'Time', visible: true},
{internalName: 'f1', displayName: '', visible: false},
{internalName: 'f2', displayName: 'F2', visible: true},
]
const actual = transformTableData(
data,
sort,
fieldOptions,
tableOptions,
timeFormat,
decimalPlaces
)
const expected = [['time', 'f2'], [1000, 2000], [2000, 3000], [3000, 1000]]
expect(actual.transformedData).toEqual(expected)
})
it('filters out invisible columns after sorting', () => {
const data = [
['time', 'f1', 'f2'],
[1000, 3000, 2000],
[2000, 1000, 3000],
[3000, 2000, 1000],
]
const sort = {field: 'f1', direction: DEFAULT_SORT_DIRECTION}
const sortBy = {internalName: 'time', displayName: 'Time', visible: true}
const tableOptions = {
verticalTimeAxis: true,
sortBy,
fixFirstColumn: true,
}
const timeFormat = DEFAULT_TIME_FORMAT
const decimalPlaces = DEFAULT_DECIMAL_PLACES
const fieldOptions = [
{internalName: 'time', displayName: 'Time', visible: true},
{internalName: 'f1', displayName: '', visible: false},
{internalName: 'f2', displayName: 'F2', visible: true},
]
const actual = transformTableData(
data,
sort,
fieldOptions,
tableOptions,
timeFormat,
decimalPlaces
)
const expected = [['time', 'f2'], [2000, 3000], [3000, 1000], [1000, 2000]]
expect(actual.transformedData).toEqual(expected)
})
})
describe('if verticalTimeAxis is false', () => {
it('transforms data', () => {
const data = [
['time', 'f1', 'f2'],
[1000, 3000, 2000],
[2000, 1000, 3000],
[3000, 2000, 1000],
]
const sort = {field: 'time', direction: DEFAULT_SORT_DIRECTION}
const sortBy = {internalName: 'time', displayName: 'Time', visible: true}
const tableOptions = {
sortBy,
fixFirstColumn: true,
verticalTimeAxis: false,
}
const timeFormat = DEFAULT_TIME_FORMAT
const decimalPlaces = DEFAULT_DECIMAL_PLACES
const fieldOptions = [
{internalName: 'time', displayName: 'Time', visible: true},
{internalName: 'f1', displayName: '', visible: true},
{internalName: 'f2', displayName: 'F2', visible: true},
]
const actual = transformTableData(
data,
sort,
fieldOptions,
tableOptions,
timeFormat,
decimalPlaces
)
const expected = [
['time', 1000, 2000, 3000],
['f1', 3000, 1000, 2000],
['f2', 2000, 3000, 1000],
]
expect(actual.transformedData).toEqual(expected)
})
it('transforms data after filtering out invisible columns', () => {
const data = [
['time', 'f1', 'f2'],
[1000, 3000, 2000],
[2000, 1000, 3000],
[3000, 2000, 1000],
]
const sort = {field: 'f1', direction: DEFAULT_SORT_DIRECTION}
const sortBy = {internalName: 'time', displayName: 'Time', visible: true}
const tableOptions = {
sortBy,
fixFirstColumn: true,
verticalTimeAxis: false,
}
const timeFormat = DEFAULT_TIME_FORMAT
const decimalPlaces = DEFAULT_DECIMAL_PLACES
const fieldOptions = [
{internalName: 'time', displayName: 'Time', visible: true},
{internalName: 'f1', displayName: '', visible: false},
{internalName: 'f2', displayName: 'F2', visible: true},
]
const actual = transformTableData(
data,
sort,
fieldOptions,
tableOptions,
timeFormat,
decimalPlaces
)
const expected = [['time', 2000, 3000, 1000], ['f2', 3000, 1000, 2000]]
expect(actual.transformedData).toEqual(expected)
})
})

View File

@ -1,88 +0,0 @@
import {fastMap, fastReduce} from 'src/utils/fast'
import {groupByTimeSeriesTransform} from 'src/utils/groupByTimeSeriesTransform'
import {
TimeSeriesServerResponse,
TimeSeries,
TimeSeriesValue,
} from 'src/types/series'
import {DygraphSeries, DygraphValue} from 'src/types'
interface Label {
label: string
seriesIndex: number
responseIndex: number
}
export interface TimeSeriesToDygraphReturnType {
labels: string[]
timeSeries: DygraphValue[][]
dygraphSeries: DygraphSeries
}
interface TimeSeriesToTableGraphReturnType {
data: TimeSeriesValue[][]
sortedLabels: Label[]
}
export const timeSeriesToDygraph = (
raw: TimeSeriesServerResponse[],
pathname: string = ''
): TimeSeriesToDygraphReturnType => {
const isTable = false
const isInDataExplorer = pathname.includes('data-explorer')
const {sortedLabels, sortedTimeSeries} = groupByTimeSeriesTransform(
raw,
isTable
)
const labels = [
'time',
...fastMap<Label, string>(sortedLabels, ({label}) => label),
]
const timeSeries = fastMap<TimeSeries, DygraphValue[]>(
sortedTimeSeries,
({time, values}) => [new Date(time as string), ...values]
)
const dygraphSeries = fastReduce<Label, DygraphSeries>(
sortedLabels,
(acc, {label, responseIndex}) => {
if (!isInDataExplorer) {
acc[label] = {
axis: responseIndex === 0 ? 'y' : 'y2',
}
}
return acc
},
{}
)
return {labels, timeSeries, dygraphSeries}
}
export const timeSeriesToTableGraph = (
raw: TimeSeriesServerResponse[]
): TimeSeriesToTableGraphReturnType => {
const isTable = true
const {sortedLabels, sortedTimeSeries} = groupByTimeSeriesTransform(
raw,
isTable
)
const labels = [
'time',
...fastMap<Label, string>(sortedLabels, ({label}) => label),
]
const tableData = fastMap<TimeSeries, TimeSeriesValue[]>(
sortedTimeSeries,
({time, values}) => [time, ...values]
)
const data = tableData.length ? [labels, ...tableData] : [[]]
return {
data,
sortedLabels,
}
}

View File

@ -23,6 +23,7 @@
"allowJs": true, "allowJs": true,
"checkJs": false, "checkJs": false,
"sourceMap": true, "sourceMap": true,
"esModuleInterop": true,
"baseUrl": "./" "baseUrl": "./"
}, },
"exclude": ["assets", "build", "node_modules"] "exclude": ["assets", "build", "node_modules"]

File diff suppressed because it is too large Load Diff

129
view.go
View File

@ -91,6 +91,7 @@ func UnmarshalViewPropertiesJSON(b []byte) (ViewProperties, error) {
var t struct { var t struct {
Shape string `json:"shape"` Shape string `json:"shape"`
Type string `json:"type"`
} }
if err := json.Unmarshal(v.B, &t); err != nil { if err := json.Unmarshal(v.B, &t); err != nil {
@ -99,6 +100,39 @@ func UnmarshalViewPropertiesJSON(b []byte) (ViewProperties, error) {
var vis ViewProperties var vis ViewProperties
switch t.Shape { switch t.Shape {
case "chronograf-v2":
switch t.Type {
case "line":
var lv LineViewProperties
if err := json.Unmarshal(v.B, &lv); err != nil {
return nil, err
}
vis = lv
case "single-stat":
var ssv SingleStatViewProperties
if err := json.Unmarshal(v.B, &ssv); err != nil {
return nil, err
}
vis = ssv
case "gauge":
var gv GaugeViewProperties
if err := json.Unmarshal(v.B, &gv); err != nil {
return nil, err
}
vis = gv
case "step-plot":
var spv StepPlotViewProperties
if err := json.Unmarshal(v.B, &spv); err != nil {
return nil, err
}
vis = spv
case "stacked":
var sv StackedViewProperties
if err := json.Unmarshal(v.B, &sv); err != nil {
return nil, err
}
vis = sv
}
case "chronograf-v1": case "chronograf-v1":
var qv V1ViewProperties var qv V1ViewProperties
if err := json.Unmarshal(v.B, &qv); err != nil { if err := json.Unmarshal(v.B, &qv); err != nil {
@ -121,6 +155,46 @@ func UnmarshalViewPropertiesJSON(b []byte) (ViewProperties, error) {
func MarshalViewPropertiesJSON(v ViewProperties) ([]byte, error) { func MarshalViewPropertiesJSON(v ViewProperties) ([]byte, error) {
var s interface{} var s interface{}
switch vis := v.(type) { switch vis := v.(type) {
case SingleStatViewProperties:
s = struct {
Shape string `json:"shape"`
SingleStatViewProperties
}{
Shape: "chronograf-v2",
SingleStatViewProperties: vis,
}
case GaugeViewProperties:
s = struct {
Shape string `json:"shape"`
GaugeViewProperties
}{
Shape: "chronograf-v2",
GaugeViewProperties: vis,
}
case LineViewProperties:
s = struct {
Shape string `json:"shape"`
LineViewProperties
}{
Shape: "chronograf-v2",
LineViewProperties: vis,
}
case StepPlotViewProperties:
s = struct {
Shape string `json:"shape"`
StepPlotViewProperties
}{
Shape: "chronograf-v2",
StepPlotViewProperties: vis,
}
case StackedViewProperties:
s = struct {
Shape string `json:"shape"`
StackedViewProperties
}{
Shape: "chronograf-v2",
StackedViewProperties: vis,
}
case V1ViewProperties: case V1ViewProperties:
s = struct { s = struct {
Shape string `json:"shape"` Shape string `json:"shape"`
@ -196,6 +270,54 @@ func (u ViewUpdate) MarshalJSON() ([]byte, error) {
}) })
} }
// LineViewProperties represents options for line view in Chronograf
type LineViewProperties struct {
Queries []DashboardQuery `json:"queries"`
Axes map[string]Axis `json:"axes"`
Type string `json:"type"`
Legend Legend `json:"legend"`
ViewColors []ViewColor `json:"colors"`
}
// StepPlotViewProperties represents options for step plot view in Chronograf
type StepPlotViewProperties struct {
Queries []DashboardQuery `json:"queries"`
Axes map[string]Axis `json:"axes"`
Type string `json:"type"`
Legend Legend `json:"legend"`
ViewColors []ViewColor `json:"colors"`
}
// StackedViewProperties represents options for stacked view in Chronograf
type StackedViewProperties struct {
Queries []DashboardQuery `json:"queries"`
Axes map[string]Axis `json:"axes"`
Type string `json:"type"`
Legend Legend `json:"legend"`
ViewColors []ViewColor `json:"colors"`
}
// SingleStatViewProperties represents options for single stat view in Chronograf
type SingleStatViewProperties struct {
Type string `json:"type"`
Queries []DashboardQuery `json:"queries"`
Prefix string `json:"prefix"`
Suffix string `json:"suffix"`
ViewColors []ViewColor `json:"colors"`
DecimalPlaces DecimalPlaces `json:"decimalPlaces"`
}
// GaugeViewProperties represents options for gauge view in Chronograf
type GaugeViewProperties struct {
Type string `json:"type"`
Queries []DashboardQuery `json:"queries"`
Prefix string `json:"prefix"`
Suffix string `json:"suffix"`
ViewColors []ViewColor `json:"colors"`
DecimalPlaces DecimalPlaces `json:"decimalPlaces"`
}
// V1ViewProperties represents V1 Chronograf view shapes
type V1ViewProperties struct { type V1ViewProperties struct {
Queries []DashboardQuery `json:"queries"` Queries []DashboardQuery `json:"queries"`
Axes map[string]Axis `json:"axes"` Axes map[string]Axis `json:"axes"`
@ -208,7 +330,12 @@ type V1ViewProperties struct {
DecimalPlaces DecimalPlaces `json:"decimalPlaces"` DecimalPlaces DecimalPlaces `json:"decimalPlaces"`
} }
func (V1ViewProperties) ViewProperties() {} func (V1ViewProperties) ViewProperties() {}
func (LineViewProperties) ViewProperties() {}
func (StepPlotViewProperties) ViewProperties() {}
func (SingleStatViewProperties) ViewProperties() {}
func (StackedViewProperties) ViewProperties() {}
func (GaugeViewProperties) ViewProperties() {}
///////////////////////////// /////////////////////////////
// Old Chronograf Types // Old Chronograf Types