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
- run: make test-js
- run: make chronograf_lint
gotest:
docker:

View File

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

View File

@ -17,6 +17,14 @@ else
yarn run build
endif
lint: node_modules $(UISOURCES)
ifndef YARN
$(error Please install yarn 0.19.1+)
else
yarn run tsc
endif
test:
ifndef YARN
$(error Please install yarn 0.19.1+)
@ -34,4 +42,4 @@ endif
run:
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: [
{
displayName: 'test',
testURL: 'http://localhost',
testPathIgnorePatterns: [
'build',
'<rootDir>/node_modules/(?!(jest-test))',
@ -26,11 +27,5 @@ module.exports = {
displayName: 'eslint',
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-scale": "^2.0.1",
"@types/dygraphs": "^1.1.6",
"@types/enzyme": "^3.1.9",
"@types/jest": "^22.1.4",
"@types/lodash": "^4.14.104",
"@types/enzyme": "^3.1.14",
"@types/jest": "^23.3.2",
"@types/lodash": "^4.14.116",
"@types/node": "^9.4.6",
"@types/papaparse": "^4.1.34",
"@types/prop-types": "^15.5.2",
"@types/qs": "^6.5.1",
"@types/react": "^16.0.38",
"@types/react": "^16.4.14",
"@types/react-dnd": "^2.0.36",
"@types/react-dnd-html5-backend": "^2.1.9",
"@types/react-grid-layout": "^0.16.5",
@ -63,7 +63,7 @@
"babel-preset-react": "^6.5.0",
"babel-preset-stage-0": "^6.16.0",
"babel-runtime": "^6.5.0",
"enzyme": "^3.3.0",
"enzyme": "^3.6.0",
"enzyme-to-json": "^3.3.4",
"eslint": "^3.14.1",
"eslint-config-prettier": "^2.9.0",
@ -74,20 +74,20 @@
"express": "^4.14.0",
"http-proxy-middleware": "^0.18.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^23.1.0",
"jest": "^23.6.0",
"jest-runner-eslint": "^0.6.0",
"jest-runner-tslint": "^1.0.4",
"jsdom": "^9.0.0",
"node-sass": "^4.9.3",
"parcel": "^1.9.7",
"prettier": "1.12.1",
"ts-jest": "^22.4.2",
"ts-jest": "^23.10.2",
"tslib": "^1.9.0",
"tslint": "^5.9.1",
"tslint-config-prettier": "^1.10.0",
"tslint-plugin-prettier": "^1.3.0",
"tslint-react": "^3.5.1",
"typescript": "2.7.2"
"typescript": "^3.0.3"
},
"dependencies": {
"axios": "^0.18.0",

View File

@ -40,7 +40,7 @@ interface Props {
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
// 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.

View File

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

View File

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

View File

@ -3,16 +3,16 @@ import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react'
import ColorDropdown from 'src/shared/components/ColorDropdown'
import {THRESHOLD_COLORS} from 'src/shared/constants/thresholds'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {ColorNumber, ThresholdColor} from 'src/types/colors'
import {Color, ThresholdColor} from 'src/types/colors'
interface Props {
visualizationType: string
threshold: ColorNumber
threshold: Color
disableMaxColor: boolean
onChooseColor: (threshold: ColorNumber) => void
onValidateColorValue: (threshold: ColorNumber, targetValue: number) => boolean
onUpdateColorValue: (threshold: ColorNumber, targetValue: number) => void
onDeleteThreshold: (threshold: ColorNumber) => void
onChooseColor: (threshold: Color) => void
onValidateColorValue: (threshold: Color, targetValue: number) => boolean
onUpdateColorValue: (threshold: Color, targetValue: number) => void
onDeleteThreshold: (threshold: Color) => void
isMin: boolean
isMax: boolean
}
@ -76,7 +76,7 @@ class Threshold extends PureComponent<Props, State> {
onChooseColor({...threshold, hex, name})
}
private get selectedColor(): ColorNumber {
private get selectedColor(): Color {
const {
threshold: {hex, name, type, value, id},
} = 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 {stringifyColorValues} from 'src/shared/constants/colorOperations'
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) => {
switch (cellType) {
@ -32,11 +32,11 @@ export const DEFAULT_AXIS: DefaultAxis = {
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>'
interface Color {
interface ColorArgs {
cellType: CellType
thresholdsListColors: ColorNumber[]
gaugeColors: ColorNumber[]
lineColors: ColorString[]
thresholdsListColors: Color[]
gaugeColors: Color[]
lineColors: Color[]
}
export const getCellTypeColors = ({
@ -44,8 +44,8 @@ export const getCellTypeColors = ({
gaugeColors,
thresholdsListColors,
lineColors,
}: Color): ColorString[] => {
let colors: ColorString[] = []
}: ColorArgs): Color[] => {
let colors: Color[] = []
switch (cellType) {
case CellType.Gauge: {

View File

@ -115,9 +115,9 @@ export enum CEOTabs {
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 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
export const DYNAMIC_SOURCE = 'dynamic'

View File

@ -79,9 +79,9 @@ interface Props extends ManualRefreshProps, WithRouterProps {
notify: NotificationsActions.PublishNotificationActionCreator
selectedCell: Cell
thresholdsListType: string
thresholdsListColors: ColorsModels.ColorNumber[]
gaugeColors: ColorsModels.ColorNumber[]
lineColors: ColorsModels.ColorString[]
thresholdsListColors: ColorsModels.Color[]
gaugeColors: ColorsModels.Color[]
lineColors: ColorsModels.Color[]
addCell: typeof dashboardActions.addCellAsync
deleteCell: typeof dashboardActions.deleteCellAsync
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'
type Graphic = ReactElement<HTMLDivElement>
type Graphic = JSX.Element
interface GraphSVGs {
[CellType.Line]: Graphic
@ -14,6 +14,7 @@ interface GraphSVGs {
[CellType.Gauge]: Graphic
[CellType.Table]: Graphic
}
const GRAPH_SVGS: GraphSVGs = {
line: (
<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 {CellType, Cell} from 'src/types'
import {ThresholdType, TableOptions} from 'src/types/dashboards'
import {ThresholdColor, GaugeColor, LineColor} from 'src/types/colors'
import {Color} from 'src/types/colors'
interface CEOInitialState {
cell: Cell | null
thresholdsListType: ThresholdType
thresholdsListColors: ThresholdColor[]
gaugeColors: GaugeColor[]
lineColors: LineColor[]
thresholdsListColors: Color[]
gaugeColors: Color[]
lineColors: Color[]
}
export const initialState = {

View File

@ -19,11 +19,12 @@ import {
DecimalPlaces,
CellType,
} from 'src/types/dashboards'
import {LineColor, ColorNumber} from 'src/types/colors'
import {Color} from 'src/types/colors'
export const dashboard = {
id: '1',
name: 'd1',
default: false,
cells: [
{
x: 1,
@ -42,6 +43,7 @@ export const dashboard = {
links: {
self: '/v2/dashboards/1',
cells: '/v2/dashboards/cells',
copy: '/v2/dashboards/1/cells/1/copy',
},
}
@ -175,7 +177,7 @@ export const tableOptions: TableOptions = {
wrapping: 'truncate',
fixFirstColumn: true,
}
export const lineColors: LineColor[] = [
export const lineColors: Color[] = [
{
id: '574fb0a3-0a26-44d7-8d71-d4981756acb1',
type: 'scale',
@ -398,29 +400,29 @@ export const predefinedTemplateVariables: Template[] = [
{...interval},
]
export const thresholdsListColors: ColorNumber[] = [
export const thresholdsListColors: Color[] = [
{
type: 'text',
hex: '#00C9FF',
id: 'base',
name: 'laser',
value: -1000000000000000000,
value: '-1000000000000000000',
},
]
export const gaugeColors: ColorNumber[] = [
export const gaugeColors: Color[] = [
{
type: 'min',
hex: '#00C9FF',
id: '0',
name: 'laser',
value: 0,
value: '0',
},
{
type: 'max',
hex: '#9394FF',
id: '1',
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 */
/**
* 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
import {FluxTable} from 'src/types'
import {DygraphSeries} from 'src/types'
import {ViewType} from 'src/types/v2/dashboards'
interface Props {
@ -26,25 +25,18 @@ interface Props {
class FluxGraph extends PureComponent<Props> {
public render() {
const containerStyle = {
width: 'calc(100% - 32px)',
height: 'calc(100% - 16px)',
position: 'absolute',
}
const {dygraphsData, labels} = fluxTablesToDygraph(this.props.data)
return (
<div className="yield-node--graph">
<Dygraph
type={ViewType.Line}
labels={labels}
type={ViewType.Line}
staticLegend={false}
dygraphSeries={{}}
options={this.options}
timeSeries={dygraphsData}
colors={DEFAULT_LINE_COLORS}
dygraphSeries={this.dygraphSeries}
options={this.options}
containerStyle={containerStyle}
handleSetHoverTime={this.props.setHoverTime}
/>
</div>
@ -57,10 +49,6 @@ class FluxGraph extends PureComponent<Props> {
gridLineColor: '#383846',
}
}
private get dygraphSeries(): DygraphSeries {
return {}
}
}
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 {OnChangeArg} from 'src/types/flux'
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
if (func.name === funcNames.FILTER) {
return (

View File

@ -72,7 +72,7 @@ interface State {
type ScriptFunc = (script: string) => void
export const FluxContext = React.createContext()
export const FluxContext = React.createContext({})
@ErrorHandling
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 actual = ast.toString()
// console.log('actual', actual)
// console.log('expected', expected)
expect(actual).toBe(expected)
})

View File

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

View File

@ -4,11 +4,11 @@ import classnames from 'classnames'
import {ClickOutside} from 'src/shared/components/ClickOutside'
import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar'
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'
interface Props {
selected: ColorNumber
selected: Color
disabled?: boolean
stretchToFit?: boolean
colors: ThresholdColor[]

View File

@ -5,16 +5,16 @@ import classnames from 'classnames'
import {ClickOutside} from 'src/shared/components/ClickOutside'
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 {DROPDOWN_MENU_MAX_HEIGHT} from 'src/shared/constants/index'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
onChoose: (colors: ColorNumber[]) => void
onChoose: (colors: Color[]) => void
stretchToFit?: boolean
disabled?: boolean
selected: ColorNumber[]
selected: Color[]
}
interface State {

View File

@ -1,10 +1,10 @@
import React, {PureComponent, ReactElement} from 'react'
import React, {PureComponent} from 'react'
import classnames from 'classnames'
interface Props {
fileTypesToAccept?: string
containerClass?: string
handleSubmit: (uploadContent: string, fileName: string) => void
handleSubmit: (uploadContent: string | ArrayBuffer, fileName: string) => void
submitText?: string
submitOnDrop?: boolean
submitOnUpload?: boolean
@ -13,7 +13,7 @@ interface Props {
interface State {
inputContent: string | null
uploadContent: string
uploadContent: string | ArrayBuffer
fileName: string
dragClass: string
}
@ -106,7 +106,7 @@ class DragAndDrop extends PureComponent<Props, State> {
return classnames('drag-and-drop--form', {active: !uploadContent})
}
private get dragAreaHeader(): ReactElement<HTMLHeadElement> {
private get dragAreaHeader(): JSX.Element {
const {uploadContent, fileName} = this.state
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 {submitText, submitOnDrop} = this.props
@ -175,10 +175,10 @@ class DragAndDrop extends PureComponent<Props, State> {
const reader = new FileReader()
reader.readAsText(file)
reader.onload = loadEvent => {
reader.onload = () => {
this.setState(
{
uploadContent: loadEvent.target.result,
uploadContent: reader.result,
fileName: file.name,
},
this.submitOnUpload
@ -201,10 +201,10 @@ class DragAndDrop extends PureComponent<Props, State> {
const reader = new FileReader()
reader.readAsText(file)
reader.onload = loadEvent => {
reader.onload = () => {
this.setState(
{
uploadContent: loadEvent.target.result,
uploadContent: reader.result,
fileName: file.name,
},
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 * as actions from 'src/dashboards/actions/v2/views'
import {SeriesLegendData} from 'src/types/dygraphs'
import DygraphLegendSort from 'src/shared/components/DygraphLegendSort'
import {makeLegendStyles} from 'src/shared/graphs/helpers'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {NO_CELL} from 'src/shared/constants'
import {DygraphClass} from 'src/types'
// Types
import DygraphClass, {SeriesLegendData} from 'src/external/dygraph'
interface Props {
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 {
COLOR_TYPE_MIN,
COLOR_TYPE_MAX,
DEFAULT_VALUE_MAX,
DEFAULT_VALUE_MIN,
MIN_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 {ColorString} from 'src/types/colors'
import {Color} from 'src/types/colors'
import {DecimalPlaces} from 'src/types/dashboards'
interface Props {
width: string
height: string
gaugePosition: number
colors?: ColorString[]
colors?: Color[]
prefix: string
suffix: string
decimalPlaces: DecimalPlaces
@ -78,12 +80,21 @@ class Gauge extends Component<Props> {
if (!colors || colors.length === 0) {
return
}
// Distill out max and min values
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(
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
@ -206,7 +217,7 @@ class Gauge extends Component<Props> {
// Draw Large ticks
for (let lt = 0; lt <= lineCount; lt++) {
// Rototion before drawing line
// Rotation before drawing line
ctx.rotate(startDegree)
ctx.rotate(lt * arcLargeIncrement)
// Draw line
@ -225,7 +236,7 @@ class Gauge extends Component<Props> {
// Draw Small ticks
for (let lt = 0; lt <= totalSmallLineCount; lt++) {
// Rototion before drawing line
// Rotation before drawing line
ctx.rotate(startDegree)
ctx.rotate(lt * arcSmallIncrement)
// Draw line
@ -322,7 +333,7 @@ class Gauge extends Component<Props> {
let valueString
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, {
minimumFractionDigits: 0,
maximumFractionDigits: digits,
@ -330,7 +341,7 @@ class Gauge extends Component<Props> {
} else {
valueString = value.toLocaleString(undefined, {
minimumFractionDigits: 0,
maximumFractionDigits: MAX_TOLOCALESTRING_VAL,
maximumFractionDigits: MAX_TO_LOCALE_STRING_VAL,
})
}
@ -343,7 +354,7 @@ class Gauge extends Component<Props> {
let valueString
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, {
minimumFractionDigits: digits,
maximumFractionDigits: digits,
@ -351,7 +362,7 @@ class Gauge extends Component<Props> {
} else {
valueString = value.toLocaleString(undefined, {
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 Gauge from 'src/shared/components/Gauge'
import GaugeChart from 'src/shared/components/GaugeChart'
import {ViewType, ViewShape, GaugeView} from 'src/types/v2/dashboards'
const data = [
const tables = [
{
response: {
results: [
{
series: [
{
values: [[1, 2]],
columns: ['time', 'value'],
},
],
},
],
id: '54797afd-734d-4ca3-94b6-3a7870c53b27',
data: [
['', 'result', 'table', '_time', 'mean', '_measurement'],
['', '', '0', '2018-09-27T16:50:10Z', '2', 'cpu'],
],
name: '_measurement=cpu',
groupKey: {
_measurement: 'cpu',
},
dataTypes: {
'': '#datatype',
result: 'string',
table: 'long',
_time: 'dateTime:RFC3339',
mean: 'double',
_measurement: 'string',
},
},
]
const defaultProps = {
data: [],
isFetchingInitially: false,
cellID: '',
const properties: GaugeView = {
queries: [],
colors: [],
shape: ViewShape.ChronografV2,
type: ViewType.Gauge,
prefix: '',
suffix: '',
decimalPlaces: {
@ -32,6 +39,11 @@ const defaultProps = {
},
}
const defaultProps = {
tables: [],
properties,
}
const setup = (overrides = {}) => {
const props = {
...defaultProps,
@ -54,10 +66,10 @@ describe('GaugeChart', () => {
describe('when data has a value', () => {
it('renders the correct number', () => {
const wrapper = setup({data})
const wrapper = setup({tables})
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 _ from 'lodash'
import getLastValues from 'src/shared/parsing/lastValues'
// Components
import Gauge from 'src/shared/components/Gauge'
import {DEFAULT_GAUGE_COLORS} from 'src/shared/constants/thresholds'
import {stringifyColorValues} from 'src/shared/constants/colorOperations'
import {DASHBOARD_LAYOUT_ROW_HEIGHT} from 'src/shared/constants'
// Parsing
import getLastValues from 'src/shared/parsing/flux/fluxToSingleStat'
// Types
import {FluxTable} from 'src/types'
import {GaugeView} from 'src/types/v2/dashboards'
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 {
data: TimeSeriesServerResponse[]
decimalPlaces: DecimalPlaces
cellHeight?: number
colors?: ColorString[]
prefix: string
suffix: string
resizerTopHeight?: number
tables: FluxTable[]
properties: GaugeView
}
@ErrorHandling
class GaugeChart extends PureComponent<Props> {
public static defaultProps: Partial<Props> = {
colors: stringifyColorValues(DEFAULT_GAUGE_COLORS),
}
public render() {
const {colors, prefix, suffix, decimalPlaces} = this.props
const {colors, prefix, suffix, decimalPlaces} = this.props.properties
return (
<div className="single-stat">
<Gauge
width="900"
height="300"
colors={colors}
height={this.height}
prefix={prefix}
suffix={suffix}
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 {
const {data} = this.props
const {lastValues} = getLastValues(data)
const lastValue = _.get(lastValues, 0, 0)
if (!lastValue) {
return 0
}
const {tables} = this.props
const {values} = getLastValues(tables)
const lastValue = _.get(values, 0, 0)
return lastValue
}

View File

@ -1,38 +1,28 @@
// Libraries
import React, {PureComponent, CSSProperties} from 'react'
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'
// Utils
import {
fluxTablesToDygraph,
FluxTablesToDygraphResult,
} from 'src/shared/parsing/flux/dygraph'
// Types
import {ColorString} from 'src/types/colors'
import {DecimalPlaces} from 'src/types/dashboards'
import {Axes, TimeRange, RemoteDataState, FluxTable} from 'src/types'
import {ViewType, CellQuery} from 'src/types/v2'
import {Options} from 'src/external/dygraph'
import {LineView} from 'src/types/v2/dashboards'
import {TimeRange} from 'src/types/v2'
import {FluxTable, RemoteDataState} from 'src/types'
interface Props {
axes: Axes
type: ViewType
queries: CellQuery[]
timeRange: TimeRange
colors: ColorString[]
loading: RemoteDataState
decimalPlaces: DecimalPlaces
data: FluxTable[]
properties: LineView
timeRange: TimeRange
tables: FluxTable[]
viewID: string
cellHeight: number
staticLegend: boolean
onZoom: () => void
handleSetHoverTime: () => void
activeQueryIndex?: number
}
@ErrorHandlingWith(InvalidData)
@ -41,49 +31,46 @@ class LineGraph extends PureComponent<Props> {
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() {
if (!this.isValidData) {
return <InvalidData />
}
const {
axes,
type,
colors,
tables,
viewID,
onZoom,
loading,
queries,
timeRange,
properties,
staticLegend,
handleSetHoverTime,
} = 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,
yRangePad: 10,
labelsKMB: true,
@ -94,68 +81,8 @@ class LineGraph extends PureComponent<Props> {
axisLineColor: '#383846',
gridLineColor: '#383846',
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

View File

@ -5,12 +5,12 @@ import {bindActionCreators} from 'redux'
import ColorScaleDropdown from 'src/shared/components/ColorScaleDropdown'
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'
interface Props {
lineColors: ColorNumber[]
handleUpdateLineColors: (colors: ColorNumber[]) => void
lineColors: Color[]
handleUpdateLineColors: (colors: Color[]) => void
}
@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 classnames from 'classnames'
import getLastValues from 'src/shared/parsing/lastValues'
import _ from 'lodash'
import {SMALL_CELL_HEIGHT} from 'src/shared/graphs/helpers'
import {DYGRAPH_CONTAINER_V_MARGIN} from 'src/shared/constants'
// Constants
import {generateThresholdsListHexs} from 'src/shared/constants/colorOperations'
import {ColorString} from 'src/types/colors'
import {CellType, DecimalPlaces} from 'src/types/dashboards'
import {TimeSeriesServerResponse} from 'src/types/series'
// Types
import {CellType} from 'src/types/dashboards'
import {SingleStatView} from 'src/types/v2/dashboards'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
decimalPlaces: DecimalPlaces
cellHeight: number
colors: ColorString[]
prefix?: string
suffix?: string
lineGraph: boolean
staticLegendHeight?: number
data: TimeSeriesServerResponse[]
properties: SingleStatView
stat: number
}
@ErrorHandling
class SingleStat extends PureComponent<Props> {
public static defaultProps: Partial<Props> = {
prefix: '',
suffix: '',
}
public render() {
return (
<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 {
const {prefix, suffix} = this.props
const {prefix, suffix} = this.props.properties
return `${prefix}${this.roundedLastValue}${suffix}`
}
private get lastValue(): number {
const {data} = this.props
const {lastValues, series} = getLastValues(data)
const firstAlphabeticalSeriesName = _.sortBy(series)[0]
const firstAlphabeticalIndex = _.indexOf(
series,
firstAlphabeticalSeriesName
)
return lastValues[firstAlphabeticalIndex]
return this.props.stat
}
private get roundedLastValue(): string {
const {decimalPlaces} = this.props
const {decimalPlaces} = this.props.properties
if (this.lastValue === null) {
return `${0}`
@ -84,41 +58,21 @@ class SingleStat extends PureComponent<Props> {
}
private get containerStyle(): CSSProperties {
const {staticLegendHeight} = this.props
const height = `calc(100% - ${staticLegendHeight +
DYGRAPH_CONTAINER_V_MARGIN * 2}px)`
const {backgroundColor} = this.coloration
if (staticLegendHeight) {
return {
backgroundColor,
height,
}
}
return {
backgroundColor,
}
}
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({
colors,
lastValue,
cellType: lineGraph ? CellType.LinePlusSingleStat : CellType.SingleStat,
lastValue: this.props.stat,
cellType: CellType.SingleStat,
})
return {
@ -128,22 +82,8 @@ class SingleStat extends PureComponent<Props> {
}
private get resizerBox(): JSX.Element {
const {lineGraph, cellHeight} = this.props
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`
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 {updateThresholdsListColors} from 'src/dashboards/actions/cellEditorOverlay'
import {ColorNumber} from 'src/types/colors'
import {Color} from 'src/types/colors'
import {
THRESHOLD_COLORS,
@ -24,8 +24,8 @@ interface Props {
onResetFocus: () => void
showListHeading: boolean
thresholdsListType: string
thresholdsListColors: ColorNumber[]
handleUpdateThresholdsListColors: (c: ColorNumber[]) => void
thresholdsListColors: Color[]
handleUpdateThresholdsListColors: (c: Color[]) => void
}
@ErrorHandling
@ -99,8 +99,8 @@ class ThresholdsList extends PureComponent<Props> {
const randomColor = _.random(0, THRESHOLD_COLORS.length - 1)
const maxValue = DEFAULT_VALUE_MIN
const minValue = DEFAULT_VALUE_MAX
const maxValue = +DEFAULT_VALUE_MIN
const minValue = +DEFAULT_VALUE_MAX
let randomValue = _.round(_.random(minValue, maxValue, true), 2)
@ -108,7 +108,7 @@ class ThresholdsList extends PureComponent<Props> {
const colorsValues = _.mapValues(thresholdsListColors, 'value')
do {
randomValue = _.round(_.random(minValue, maxValue, true), 2)
} while (_.includes(colorsValues, randomValue))
} while (_.includes(colorsValues, `${randomValue}`))
}
const newThreshold = {
@ -122,7 +122,7 @@ class ThresholdsList extends PureComponent<Props> {
const updatedColors = _.sortBy(
[...thresholdsListColors, newThreshold],
color => color.value
)
) as Color[]
handleUpdateThresholdsListColors(updatedColors)
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
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'
@ -89,10 +89,6 @@ export default class CellComponent extends Component<Props, State> {
return null
}
if (view.properties.shape === ViewShape.Empty) {
return this.emptyGraph
}
return (
<ViewComponent
view={view}
@ -101,23 +97,11 @@ export default class CellComponent extends Component<Props, State> {
timeRange={timeRange}
autoRefresh={autoRefresh}
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 => {
// TODO: add back in once CEO is refactored
}

View File

@ -2,13 +2,15 @@
import React, {Component} from 'react'
// Components
import RefreshingGraph from 'src/shared/components/RefreshingGraph'
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
import {TimeRange, Template} from 'src/types'
import {View, ViewType} from 'src/types/v2'
import {text} from 'src/shared/components/views/gettingsStarted'
import {View, ViewType, ViewShape} from 'src/types/v2'
import {ErrorHandling} from 'src/shared/decorators/errors'
@ -19,6 +21,7 @@ interface Props {
autoRefresh: number
manualRefresh: number
onZoom: (range: TimeRange) => void
onSummonOverlay: () => void
}
@ErrorHandling
@ -37,24 +40,41 @@ class ViewComponent extends Component<Props> {
templates,
} = this.props
if (view.properties.shape === ViewShape.Empty) {
return this.emptyGraph
}
if (view.properties.type === ViewType.Markdown) {
return <Markdown text={text} />
}
return (
<RefreshingGraph
<RefreshingView
viewID={view.id}
onZoom={onZoom}
timeRange={timeRange}
templates={templates}
autoRefresh={autoRefresh}
options={view.properties}
properties={view.properties}
manualRefresh={manualRefresh}
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 => {
this.setState({cellData})
}

View File

@ -5,7 +5,7 @@ import NanoDate from 'nano-date'
import ReactResizeDetector from 'react-resize-detector'
// Components
import D from 'src/external/dygraph'
import Dygraphs from 'src/external/dygraph'
import DygraphLegend from 'src/shared/components/DygraphLegend'
import StaticLegend from 'src/shared/components/StaticLegend'
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'
// Types
import {
Axes,
TimeRange,
DygraphData,
DygraphClass,
DygraphSeries,
Constructable,
} from 'src/types'
import {LineColor} from 'src/types/colors'
import {Color} from 'src/types/colors'
import {Axes, TimeRange} from 'src/types'
import {CellQuery, ViewType} from 'src/types/v2/dashboards'
const Dygraphs = D as Constructable<DygraphClass>
import {DygraphData, DygraphSeries, Options} from 'src/external/dygraph'
interface Props {
type: ViewType
viewID?: string
queries?: CellQuery[]
timeSeries: DygraphData
labels: string[]
options: dygraphs.Options
containerStyle: object // TODO
dygraphSeries: DygraphSeries
timeRange?: TimeRange
colors: LineColor[]
options: Partial<Options>
colors: Color[]
handleSetHoverTime: (t: string) => void
viewID?: string
axes?: Axes
isGraphFilled?: boolean
staticLegend?: boolean
mode?: string
queries?: CellQuery[]
timeRange?: TimeRange
dygraphSeries?: DygraphSeries
underlayCallback?: () => void
setResolution?: (w: number) => void
onZoom?: (timeRange: TimeRange) => void
mode?: string
underlayCallback?: () => void
isGraphFilled?: boolean
staticLegend?: boolean
}
interface State {
@ -92,17 +83,17 @@ class Dygraph extends Component<Props, State> {
...DEFAULT_AXIS,
},
},
containerStyle: {},
isGraphFilled: true,
onZoom: () => {},
staticLegend: false,
setResolution: () => {},
handleSetHoverTime: () => {},
underlayCallback: () => {},
dygraphSeries: {},
}
private graphRef: React.RefObject<HTMLDivElement>
private dygraph: DygraphClass
private dygraph: Dygraphs
constructor(props: Props) {
super(props)
@ -127,7 +118,7 @@ class Dygraph extends Component<Props, State> {
const timeSeries = this.timeSeries
let defaultOptions = {
let defaultOptions: Partial<Options> = {
...options,
labels,
fillGraph,
@ -311,21 +302,30 @@ class Dygraph extends Component<Props, State> {
return null
}
private get containerStyle(): CSSProperties {
return {
width: 'calc(100% - 32px)',
height: 'calc(100% - 16px)',
position: 'absolute',
top: '8px',
}
}
private get dygraphStyle(): CSSProperties {
const {containerStyle, staticLegend} = this.props
const {staticLegend} = this.props
const {staticLegendHeight} = this.state
if (staticLegend) {
const cellVerticalPadding = 16
return {
...containerStyle,
...this.containerStyle,
zIndex: 2,
height: `calc(100% - ${staticLegendHeight + cellVerticalPadding}px)`,
}
}
return {...containerStyle, zIndex: 2}
return {...this.containerStyle, zIndex: 2}
}
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: []}}]
interface RenderProps {
timeSeries: FluxTable[]
tables: FluxTable[]
loading: RemoteDataState
}
@ -29,7 +29,7 @@ interface Props {
interface State {
loading: RemoteDataState
isFirstFetch: boolean
timeSeries: FluxTable[]
tables: FluxTable[]
}
class TimeSeries extends Component<Props, State> {
@ -41,9 +41,9 @@ class TimeSeries extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
timeSeries: [],
loading: RemoteDataState.NotStarted,
isFirstFetch: false,
tables: [],
}
}
@ -73,7 +73,7 @@ class TimeSeries extends Component<Props, State> {
}
if (!queries.length) {
return this.setState({timeSeries: []})
return this.setState({tables: []})
}
this.setState({loading: RemoteDataState.Loading, isFirstFetch})
@ -81,7 +81,7 @@ class TimeSeries extends Component<Props, State> {
const TEMP_RES = 300
try {
const timeSeries = await fetchTimeSeries(
const tables = await fetchTimeSeries(
link,
this.queries,
TEMP_RES,
@ -89,7 +89,7 @@ class TimeSeries extends Component<Props, State> {
)
this.setState({
timeSeries,
tables,
loading: RemoteDataState.Done,
})
} catch (err) {
@ -98,9 +98,9 @@ class TimeSeries extends Component<Props, State> {
}
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', [])
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[] {

View File

@ -1,6 +1,6 @@
import chroma from 'chroma-js'
import uuid from 'uuid'
import {LineColor} from 'src/types/colors'
import {Color} from 'src/types/colors'
const COLOR_TYPE_SCALE = 'scale'
@ -208,10 +208,7 @@ export const LINE_COLOR_SCALES = [
return {name, colors, id}
})
export const validateLineColors = (
colors: LineColor[],
numSeries = 0
): LineColor[] => {
export const validateLineColors = (colors: Color[], numSeries = 0): Color[] => {
const multipleSeriesButOneColor = numSeries > 1 && colors.length < 2
if (!colors || colors.length === 0 || multipleSeriesButOneColor) {
return DEFAULT_LINE_COLORS
@ -225,7 +222,7 @@ export const validateLineColors = (
}
export const getLineColorsHexes = (
colors: LineColor[],
colors: Color[],
numSeries: number
): string[] => {
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 COLOR_TYPE_MIN = 'min'
export const DEFAULT_VALUE_MIN = 0
export const DEFAULT_VALUE_MIN = '0'
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 THRESHOLD_TYPE_TEXT = 'text'
@ -115,7 +115,7 @@ export const DEFAULT_THRESHOLDS_LIST_COLORS = [
hex: THRESHOLD_COLORS[11].hex,
id: THRESHOLD_TYPE_BASE,
name: THRESHOLD_COLORS[11].name,
value: -999999999999999999,
value: '-999999999999999999',
},
]

View File

@ -1,5 +1,9 @@
// Libraries
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([
'_time',
@ -47,7 +51,7 @@ export const fluxTablesToDygraph = (
}
const uniqueColumnName = Object.entries(table.groupKey).reduce(
(acc, [k, v]) => acc + `[${k}=${v}]`,
(acc, [k, v]) => `${acc}[${k}=${v}]`,
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 (
<thead>
<tr>

View File

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

View File

@ -1,15 +1,11 @@
interface ColorBase {
export interface Color {
type: string
hex: string
id: 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 {
hex: string
name: string

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,5 +1,4 @@
import {ReactNode} from 'react'
import {DygraphData, Options} from 'src/types/dygraphs'
export interface DropdownItem {
text: string
@ -17,7 +16,3 @@ export interface PageSection {
component: ReactNode
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 {ColorString} from 'src/types/colors'
import {Color} from 'src/types/colors'
export interface Axis {
label: string
@ -66,15 +66,117 @@ export interface MarkDownProperties {
export interface View {
id: 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 {
type: V1ViewTypes
type: ViewType
queries: CellQuery[]
shape: ViewShape
axes: Axes
colors: ColorString[]
colors: Color[]
tableOptions: TableOptions
fieldOptions: FieldOption[]
timeFormat: string
@ -85,31 +187,27 @@ export interface V1View {
}
export enum ViewShape {
ChronografV2 = 'chronografV2',
ChronografV1 = 'chronografV1',
Empty = 'empty',
}
export enum ViewType {
Markdown = 'markdown',
}
export enum V1ViewTypes {
Line = 'line',
Stacked = 'line-stacked',
StepPlot = 'line-stepplot',
Bar = 'bar',
Line = 'line',
Stacked = 'stacked',
StepPlot = 'step-plot',
LinePlusSingleStat = 'line-plus-single-stat',
SingleStat = 'single-stat',
Gauge = 'gauge',
Table = 'table',
Alerts = 'alerts',
News = 'news',
Guide = 'guide',
Markdown = 'markdown',
}
interface DashboardLinks {
self: string
cells: string
copy: string
}
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,
"checkJs": false,
"sourceMap": true,
"esModuleInterop": true,
"baseUrl": "./"
},
"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 {
Shape string `json:"shape"`
Type string `json:"type"`
}
if err := json.Unmarshal(v.B, &t); err != nil {
@ -99,6 +100,39 @@ func UnmarshalViewPropertiesJSON(b []byte) (ViewProperties, error) {
var vis ViewProperties
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":
var qv V1ViewProperties
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) {
var s interface{}
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:
s = struct {
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 {
Queries []DashboardQuery `json:"queries"`
Axes map[string]Axis `json:"axes"`
@ -208,7 +330,12 @@ type V1ViewProperties struct {
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