Merge pull request #3904 from influxdata/refactor/auto-refresh

Refactor AutoRefresh as singleton
pull/10616/head
Andrew Watkins 2018-07-16 12:33:50 -07:00 committed by GitHub
commit 102b0f2a15
37 changed files with 1106 additions and 1007 deletions

View File

@ -17,6 +17,7 @@ import {
import {ColorString, ColorNumber} from 'src/types/colors'
interface Props {
axes: Axes
type: CellType
source: Source
autoRefresh: number
@ -24,7 +25,6 @@ interface Props {
timeRange: TimeRange
queryConfigs: QueryConfig[]
editQueryStatus: () => void
axes: Axes
tableOptions: TableOptions
timeFormat: string
decimalPlaces: DecimalPlaces

View File

@ -63,6 +63,7 @@ export const NEW_DEFAULT_DASHBOARD_CELL: NewDefaultCell = {
timeFormat: DEFAULT_TIME_FORMAT,
decimalPlaces: DEFAULT_DECIMAL_PLACES,
fieldOptions: [DEFAULT_TIME_FIELD],
inView: true,
}
interface EmptyDefaultDashboardCell {

View File

@ -25,6 +25,7 @@ import idNormalizer, {TYPE_ID} from 'src/normalizers/id'
import {millisecondTimeRange} from 'src/dashboards/utils/time'
import {getDeep} from 'src/utils/wrappers'
import {updateDashboardLinks} from 'src/dashboards/utils/dashboardSwitcherLinks'
import AutoRefresh from 'src/utils/AutoRefresh'
// APIs
import {loadDashboardLinks} from 'src/dashboards/apis'
@ -110,39 +111,33 @@ interface Props extends ManualRefreshProps, WithRouterProps {
}
interface State {
isEditMode: boolean
selectedCell: DashboardsModels.Cell | null
scrollTop: number
isEditMode: boolean
windowHeight: number
selectedCell: DashboardsModels.Cell | null
dashboardLinks: DashboardsModels.DashboardSwitcherLinks
}
@ErrorHandling
class DashboardPage extends Component<Props, State> {
private intervalID: number
public constructor(props: Props) {
super(props)
this.state = {
scrollTop: 0,
isEditMode: false,
selectedCell: null,
scrollTop: 0,
windowHeight: window.innerHeight,
dashboardLinks: EMPTY_LINKS,
}
}
public async componentDidMount() {
const {source, getAnnotationsAsync, timeRange, autoRefresh} = this.props
const annotationRange = millisecondTimeRange(timeRange)
getAnnotationsAsync(source.links.annotations, annotationRange)
const {autoRefresh} = this.props
if (autoRefresh) {
this.intervalID = window.setInterval(() => {
getAnnotationsAsync(source.links.annotations, annotationRange)
}, autoRefresh)
AutoRefresh.poll(autoRefresh)
AutoRefresh.subscribe(this.fetchAnnotations)
}
window.addEventListener('resize', this.handleWindowResize, true)
@ -152,45 +147,37 @@ class DashboardPage extends Component<Props, State> {
this.getDashboardLinks()
}
public componentWillReceiveProps(nextProps: Props) {
const {source, getAnnotationsAsync, timeRange} = this.props
if (this.props.autoRefresh !== nextProps.autoRefresh) {
clearInterval(this.intervalID)
this.intervalID = null
const annotationRange = millisecondTimeRange(timeRange)
if (nextProps.autoRefresh) {
this.intervalID = window.setInterval(() => {
getAnnotationsAsync(source.links.annotations, annotationRange)
}, nextProps.autoRefresh)
}
}
public fetchAnnotations = () => {
const {source, timeRange, getAnnotationsAsync} = this.props
const rangeMs = millisecondTimeRange(timeRange)
getAnnotationsAsync(source.links.annotations, rangeMs)
}
public componentDidUpdate(prevProps: Props) {
const {dashboard, autoRefresh} = this.props
const prevPath = getDeep(prevProps.location, 'pathname', null)
const thisPath = getDeep(this.props.location, 'pathname', null)
const templates = getDeep<TempVarsModels.Template[]>(
this.props.dashboard,
'templates',
[]
).map(t => t.tempVar)
const prevTemplates = getDeep<TempVarsModels.Template[]>(
prevProps.dashboard,
'templates',
[]
).map(t => t.tempVar)
const isTemplateDeleted: boolean =
_.intersection(templates, prevTemplates).length !== prevTemplates.length
const templates = this.parseTempVar(dashboard)
const prevTemplates = this.parseTempVar(prevProps.dashboard)
const intersection = _.intersection(templates, prevTemplates)
const isTemplateDeleted = intersection.length !== prevTemplates.length
if ((prevPath && thisPath && prevPath !== thisPath) || isTemplateDeleted) {
this.getDashboard()
}
if (autoRefresh !== prevProps.autoRefresh) {
AutoRefresh.poll(autoRefresh)
}
}
public componentWillUnmount() {
clearInterval(this.intervalID)
this.intervalID = null
AutoRefresh.stopPolling()
AutoRefresh.unsubscribe(this.fetchAnnotations)
window.removeEventListener('resize', this.handleWindowResize, true)
this.props.handleDismissEditingAnnotation()
}
@ -217,7 +204,6 @@ class DashboardPage extends Component<Props, State> {
cellQueryStatus,
thresholdsListType,
thresholdsListColors,
inPresentationMode,
handleChooseAutoRefresh,
handleShowCellEditorOverlay,
@ -346,6 +332,12 @@ class DashboardPage extends Component<Props, State> {
)
}
public parseTempVar(
dashboard: DashboardsModels.Dashboard
): TempVarsModels.Template[] {
return getDeep(dashboard, 'templates', []).map(t => t.tempVar)
}
private handleWindowResize = (): void => {
this.setState({windowHeight: window.innerHeight})
}

View File

@ -4,7 +4,7 @@ import Table from './Table'
import RefreshingGraph from 'src/shared/components/RefreshingGraph'
import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes'
import {Source, Query, Template} from 'src/types'
import {Source, Query, Template, CellType} from 'src/types'
interface Props {
view: string
@ -39,18 +39,18 @@ const DataExplorerVisView: SFC<Props> = ({
return (
<Table
query={query}
editQueryStatus={editQueryStatus}
source={source}
templates={templates}
editQueryStatus={editQueryStatus}
/>
)
}
return (
<RefreshingGraph
type="line-graph"
source={source}
queries={queries}
type={CellType.Line}
templates={templates}
autoRefresh={autoRefresh}
colors={DEFAULT_LINE_COLORS}

View File

@ -19,6 +19,7 @@ import AutoRefreshDropdown from 'src/shared/components/AutoRefreshDropdown'
import TimeRangeDropdown from 'src/shared/components/TimeRangeDropdown'
import GraphTips from 'src/shared/components/GraphTips'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
import AutoRefresh from 'src/utils/AutoRefresh'
import {VIS_VIEWS, AUTO_GROUP_BY, TEMPLATES} from 'src/shared/constants'
import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from 'src/data_explorer/constants'
@ -63,8 +64,11 @@ export class DataExplorer extends PureComponent<Props, State> {
}
public componentDidMount() {
const {source} = this.props
const {source, autoRefresh} = this.props
const {query} = qs.parse(location.search, {ignoreQueryPrefix: true})
AutoRefresh.poll(autoRefresh)
if (query && query.length) {
const qc = this.props.queryConfigs[0]
this.props.queryConfigActions.editRawTextAsync(
@ -75,6 +79,13 @@ export class DataExplorer extends PureComponent<Props, State> {
}
}
public componentDidUpdate(prevProps: Props) {
const {autoRefresh} = this.props
if (autoRefresh !== prevProps.autoRefresh) {
AutoRefresh.poll(autoRefresh)
}
}
public componentWillReceiveProps(nextProps: Props) {
const {router} = this.props
const {queryConfigs, timeRange} = nextProps
@ -89,6 +100,10 @@ export class DataExplorer extends PureComponent<Props, State> {
}
}
public componentWillUnmount() {
AutoRefresh.stopPolling()
}
public render() {
const {
source,

View File

@ -1,19 +1,16 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import React, {PureComponent} from 'react'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {AlertRule} from 'src/types'
interface Props {
rule: AlertRule
updateDetails: (id: string, value: string) => void
}
@ErrorHandling
class RuleDetailsText extends Component {
constructor(props) {
super(props)
}
handleUpdateDetails = e => {
const {rule, updateDetails} = this.props
updateDetails(rule.id, e.target.value)
}
render() {
class RuleDetailsText extends PureComponent<Props> {
public render() {
const {rule} = this.props
return (
<div className="rule-builder--details">
@ -27,13 +24,10 @@ class RuleDetailsText extends Component {
</div>
)
}
private handleUpdateDetails = e => {
const {rule, updateDetails} = this.props
updateDetails(rule.id, e.target.value)
}
}
const {shape, func} = PropTypes
RuleDetailsText.propTypes = {
rule: shape().isRequired,
updateDetails: func.isRequired,
}
export default RuleDetailsText

View File

@ -1,71 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import buildInfluxQLQuery from 'utils/influxql'
import AutoRefresh from 'shared/components/AutoRefresh'
import LineGraph from 'shared/components/LineGraph'
import TimeRangeDropdown from 'shared/components/TimeRangeDropdown'
import underlayCallback from 'src/kapacitor/helpers/ruleGraphUnderlay'
const RefreshingLineGraph = AutoRefresh(LineGraph)
import {LINE_COLORS_RULE_GRAPH} from 'src/shared/constants/graphColorPalettes'
const {shape, string, func} = PropTypes
const RuleGraph = ({
query,
source,
timeRange: {lower},
timeRange,
rule,
onChooseTimeRange,
}) => {
const autoRefreshMs = 30000
const queryText = buildInfluxQLQuery({lower}, query)
const queries = [{host: source.links.proxy, text: queryText}]
if (!queryText) {
return (
<div className="rule-builder--graph-empty">
<p>
Select a <strong>Time-Series</strong> to preview on a graph
</p>
</div>
)
}
return (
<div className="rule-builder--graph">
<div className="rule-builder--graph-options">
<p>Preview Data from</p>
<TimeRangeDropdown
onChooseTimeRange={onChooseTimeRange}
selected={timeRange}
preventCustomTimeRange={true}
/>
</div>
<RefreshingLineGraph
source={source}
queries={queries}
isGraphFilled={false}
ruleValues={rule.values}
autoRefresh={autoRefreshMs}
colors={LINE_COLORS_RULE_GRAPH}
underlayCallback={underlayCallback(rule)}
/>
</div>
)
}
RuleGraph.propTypes = {
source: shape({
links: shape({
proxy: string.isRequired,
}).isRequired,
}).isRequired,
query: shape({}).isRequired,
rule: shape({}).isRequired,
timeRange: shape({}).isRequired,
onChooseTimeRange: func.isRequired,
}
export default RuleGraph

View File

@ -0,0 +1,127 @@
// Libraries
import React, {PureComponent, CSSProperties} from 'react'
import TimeSeries from 'src/shared/components/time_series/TimeSeries'
// Components
import Dygraph from 'src/shared/components/Dygraph'
import TimeRangeDropdown from 'src/shared/components/TimeRangeDropdown'
// Utils
import buildInfluxQLQuery from 'src/utils/influxql'
import buildQueries from 'src/utils/buildQueriesForGraphs'
import underlayCallback from 'src/kapacitor/helpers/ruleGraphUnderlay'
import {timeSeriesToDygraph} from 'src/utils/timeSeriesTransformers'
// Constants
import {LINE_COLORS_RULE_GRAPH} from 'src/shared/constants/graphColorPalettes'
// Types
import {Source, AlertRule, QueryConfig, Query, TimeRange} from 'src/types'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
source: Source
query: QueryConfig
rule: AlertRule
timeRange: TimeRange
onChooseTimeRange: (tR: TimeRange) => void
}
@ErrorHandling
class RuleGraph extends PureComponent<Props> {
public render() {
const {source, onChooseTimeRange, timeRange, rule} = this.props
if (!this.queryText) {
return (
<div className="rule-builder--graph-empty">
<p>
Select a <strong>Time-Series</strong> to preview on a graph
</p>
</div>
)
}
return (
<div className="rule-builder--graph">
<div className="rule-builder--graph-options">
<p>Preview Data from</p>
<TimeRangeDropdown
onChooseTimeRange={onChooseTimeRange}
selected={timeRange}
preventCustomTimeRange={true}
/>
</div>
<div className="dygraph graph--hasYLabel" style={this.style}>
<TimeSeries source={source} queries={this.queries}>
{data => {
const {labels, timeSeries, dygraphSeries} = timeSeriesToDygraph(
data.timeSeries,
'rule-builder'
)
return (
<Dygraph
labels={labels}
staticLegend={false}
isGraphFilled={false}
ruleValues={rule.values}
options={this.options}
timeRange={timeRange}
queries={this.queries}
timeSeries={timeSeries}
dygraphSeries={dygraphSeries}
colors={LINE_COLORS_RULE_GRAPH}
containerStyle={this.containerStyle}
underlayCallback={underlayCallback(rule)}
/>
)
}}
</TimeSeries>
</div>
</div>
)
}
private get options() {
return {
rightGap: 0,
yRangePad: 10,
labelsKMB: true,
fillGraph: true,
axisLabelWidth: 60,
animatedZooms: true,
drawAxesAtZero: true,
axisLineColor: '#383846',
gridLineColor: '#383846',
connectSeparatedPoints: true,
}
}
private get containerStyle(): CSSProperties {
return {
width: 'calc(100% - 32px)',
height: 'calc(100% - 16px)',
position: 'absolute',
top: '8px',
}
}
private get style(): CSSProperties {
return {height: '100%'}
}
private get queryText(): string {
const {timeRange, query} = this.props
const lower = timeRange.lower
return buildInfluxQLQuery({lower}, query)
}
private get queries(): Query[] {
const {query, timeRange} = this.props
return buildQueries([query], timeRange)
}
}
export default RuleGraph

View File

@ -98,6 +98,17 @@ export const OUTSIDE_RANGE: string = 'outside range'
export const EQUAL_TO_OR_GREATER_THAN: string = 'equal to or greater'
export const EQUAL_TO_OR_LESS_THAN: string = 'equal to or less than'
export enum ThresholdOperators {
EqualTo = 'equal to',
LessThan = 'less than',
GreaterThan = 'greater than',
NotEqualTo = 'not equal to',
InsideRange = 'inside range',
OutsideRange = 'outside range',
EqualToOrGreaterThan = 'equal to or greater',
EqualToOrLessThan = 'equal to or less than',
}
export const THRESHOLD_OPERATORS: string[] = [
GREATER_THAN,
EQUAL_TO_OR_GREATER_THAN,

View File

@ -1,33 +1,35 @@
import {
EQUAL_TO,
LESS_THAN,
NOT_EQUAL_TO,
GREATER_THAN,
INSIDE_RANGE,
OUTSIDE_RANGE,
EQUAL_TO_OR_LESS_THAN,
EQUAL_TO_OR_GREATER_THAN,
} from 'src/kapacitor/constants'
import {ThresholdOperators} from 'src/kapacitor/constants'
const HIGHLIGHT = 'rgba(78, 216, 160, 0.3)'
const BACKGROUND = 'rgba(41, 41, 51, 1)'
const getFillColor = operator => {
const getFillColor = (operator: ThresholdOperators) => {
const backgroundColor = BACKGROUND
const highlightColor = HIGHLIGHT
if (operator === OUTSIDE_RANGE) {
if (operator === ThresholdOperators.OutsideRange) {
return backgroundColor
}
if (operator === NOT_EQUAL_TO) {
if (operator === ThresholdOperators.NotEqualTo) {
return backgroundColor
}
return highlightColor
}
const underlayCallback = rule => (canvas, area, dygraph) => {
interface Area {
x: number
y: number
w: number
h: number
}
const underlayCallback = rule => (
canvas: CanvasRenderingContext2D,
area: Area,
dygraph: Dygraph
) => {
const {values} = rule
const {operator, value} = values
@ -40,21 +42,21 @@ const underlayCallback = rule => (canvas, area, dygraph) => {
let highlightEnd = 0
switch (operator) {
case `${EQUAL_TO_OR_GREATER_THAN}`:
case `${GREATER_THAN}`: {
case ThresholdOperators.EqualToOrGreaterThan:
case ThresholdOperators.GreaterThan: {
highlightStart = value
highlightEnd = dygraph.yAxisRange()[1]
break
}
case `${EQUAL_TO_OR_LESS_THAN}`:
case `${LESS_THAN}`: {
case ThresholdOperators.EqualToOrLessThan:
case ThresholdOperators.LessThan: {
highlightStart = dygraph.yAxisRange()[0]
highlightEnd = value
break
}
case `${EQUAL_TO}`: {
case ThresholdOperators.LessThan: {
const width =
theOnePercent * (dygraph.yAxisRange()[1] - dygraph.yAxisRange()[0])
highlightStart = +value - width
@ -62,7 +64,7 @@ const underlayCallback = rule => (canvas, area, dygraph) => {
break
}
case `${NOT_EQUAL_TO}`: {
case ThresholdOperators.NotEqualTo: {
const width =
theOnePercent * (dygraph.yAxisRange()[1] - dygraph.yAxisRange()[0])
highlightStart = +value - width
@ -73,7 +75,7 @@ const underlayCallback = rule => (canvas, area, dygraph) => {
break
}
case `${OUTSIDE_RANGE}`: {
case ThresholdOperators.OutsideRange: {
highlightStart = Math.min(+value, +values.rangeValue)
highlightEnd = Math.max(+value, +values.rangeValue)
@ -82,7 +84,7 @@ const underlayCallback = rule => (canvas, area, dygraph) => {
break
}
case `${INSIDE_RANGE}`: {
case ThresholdOperators.InsideRange: {
highlightStart = Math.min(+value, +values.rangeValue)
highlightEnd = Math.max(+value, +values.rangeValue)
break

View File

@ -1,23 +1,28 @@
// Libraries
import React, {Component, CSSProperties, MouseEvent} from 'react'
import {connect} from 'react-redux'
import _ from 'lodash'
import NanoDate from 'nano-date'
import ReactResizeDetector from 'react-resize-detector'
import {getDeep} from 'src/utils/wrappers'
// Components
import D from 'src/external/dygraph'
import DygraphLegend from 'src/shared/components/DygraphLegend'
import StaticLegend from 'src/shared/components/StaticLegend'
import Annotations from 'src/shared/components/Annotations'
import Crosshair from 'src/shared/components/Crosshair'
// Utils
import getRange, {getStackedRange} from 'src/shared/parsing/getRangeForDygraph'
import {getDeep} from 'src/utils/wrappers'
import {numberValueFormatter} from 'src/utils/formatting'
// Constants
import {
AXES_SCALE_OPTIONS,
DEFAULT_AXIS,
} from 'src/dashboards/constants/cellEditor'
import {buildDefaultYLabel} from 'src/shared/presenters'
import {numberValueFormatter} from 'src/utils/formatting'
import {NULL_HOVER_TIME} from 'src/shared/constants/tableGraph'
import {
OPTIONS,
@ -26,14 +31,16 @@ import {
CHAR_PIXELS,
barPlotter,
} from 'src/shared/graphs/helpers'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {getLineColorsHexes} from 'src/shared/constants/graphColorPalettes'
const {LOG, BASE_10, BASE_2} = AXES_SCALE_OPTIONS
import {ErrorHandling} from 'src/shared/decorators/errors'
// Types
import {
Axes,
Query,
CellType,
RuleValues,
TimeRange,
DygraphData,
@ -46,6 +53,7 @@ import {LineColor} from 'src/types/colors'
const Dygraphs = D as Constructable<DygraphClass>
interface Props {
type: CellType
cellID: string
queries: Query[]
timeSeries: DygraphData
@ -59,11 +67,11 @@ interface Props {
ruleValues?: RuleValues
axes?: Axes
isGraphFilled?: boolean
isBarGraph?: boolean
staticLegend?: boolean
setResolution?: (w: number) => void
onZoom?: (timeRange: TimeRange) => void
mode?: string
underlayCallback?: () => void
}
interface State {
@ -95,6 +103,7 @@ class Dygraph extends Component<Props, State> {
staticLegend: false,
setResolution: () => {},
handleSetHoverTime: () => {},
underlayCallback: () => {},
}
private graphRef: React.RefObject<HTMLDivElement>
@ -113,11 +122,12 @@ class Dygraph extends Component<Props, State> {
public componentDidMount() {
const {
axes: {y, y2},
isGraphFilled: fillGraph,
isBarGraph,
type,
options,
labels,
axes: {y, y2},
isGraphFilled: fillGraph,
underlayCallback,
} = this.props
const timeSeries = this.timeSeries
@ -150,10 +160,11 @@ class Dygraph extends Component<Props, State> {
zoomCallback: (lower: number, upper: number) =>
this.handleZoom(lower, upper),
drawCallback: () => this.handleDraw(),
underlayCallback,
highlightCircleSize: 3,
}
if (isBarGraph) {
if (type === CellType.Bar) {
defaultOptions = {
...defaultOptions,
plotter: barPlotter,
@ -183,7 +194,8 @@ class Dygraph extends Component<Props, State> {
labels,
axes: {y, y2},
options,
isBarGraph,
type,
underlayCallback,
} = this.props
const dygraph = this.dygraph
@ -229,7 +241,8 @@ class Dygraph extends Component<Props, State> {
},
colors: LINE_COLORS,
series: this.colorDygraphSeries,
plotter: isBarGraph ? barPlotter : null,
plotter: type === CellType.Bar ? barPlotter : null,
underlayCallback,
}
dygraph.updateOptions(updateOptions)

View File

@ -1,7 +1,7 @@
import React, {PureComponent} from 'react'
import _ from 'lodash'
import getLastValues, {TimeSeriesResponse} from 'src/shared/parsing/lastValues'
import getLastValues from 'src/shared/parsing/lastValues'
import Gauge from 'src/shared/components/Gauge'
import {DEFAULT_GAUGE_COLORS} from 'src/shared/constants/thresholds'
@ -9,6 +9,7 @@ import {stringifyColorValues} from 'src/shared/constants/colorOperations'
import {DASHBOARD_LAYOUT_ROW_HEIGHT} from 'src/shared/constants'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {DecimalPlaces} from 'src/types/dashboards'
import {TimeSeriesServerResponse} from 'src/types/series'
interface Color {
type: string
@ -19,9 +20,8 @@ interface Color {
}
interface Props {
data: TimeSeriesResponse[]
data: TimeSeriesServerResponse[]
decimalPlaces: DecimalPlaces
isFetchingInitially: boolean
cellID: string
cellHeight?: number
colors?: Color[]
@ -37,15 +37,7 @@ class GaugeChart extends PureComponent<Props> {
}
public render() {
const {isFetchingInitially, colors, prefix, suffix} = this.props
if (isFetchingInitially) {
return (
<div className="graph-empty">
<h3 className="graph-spinner" />
</div>
)
}
const {colors, prefix, suffix} = this.props
return (
<div className="single-stat">

View File

@ -1,203 +0,0 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import WidgetCell from 'shared/components/WidgetCell'
import LayoutCell from 'shared/components/LayoutCell'
import RefreshingGraph from 'shared/components/RefreshingGraph'
import {buildQueriesForLayouts} from 'utils/buildQueriesForLayouts'
import {IS_STATIC_LEGEND, defaultIntervalValue} from 'src/shared/constants'
import _ from 'lodash'
import {colorsStringSchema} from 'shared/schemas'
import {ErrorHandling} from 'src/shared/decorators/errors'
const getSource = (cell, source, sources, defaultSource) => {
const s = _.get(cell, ['queries', '0', 'source'], null)
if (!s) {
return source
}
return sources.find(src => src.links.self === s) || defaultSource
}
@ErrorHandling
class LayoutState extends Component {
state = {
cellData: [],
resolution: defaultIntervalValue,
}
grabDataForDownload = cellData => {
this.setState({cellData})
}
render() {
return (
<Layout
{...this.props}
{...this.state}
grabDataForDownload={this.grabDataForDownload}
onSetResolution={this.handleSetResolution}
/>
)
}
handleSetResolution = resolution => {
this.setState({resolution})
}
}
const Layout = (
{
host,
cell,
cell: {
h: cellHeight,
axes,
type,
colors,
legend,
timeFormat,
fieldOptions,
tableOptions,
decimalPlaces,
},
source,
sources,
onZoom,
cellData,
resolution,
templates,
timeRange,
isEditable,
isDragging,
onEditCell,
onCloneCell,
autoRefresh,
manualRefresh,
onDeleteCell,
onSetResolution,
onStopAddAnnotation,
onSummonOverlayTechnologies,
grabDataForDownload,
},
{source: defaultSource}
) => (
<LayoutCell
cell={cell}
cellData={cellData}
templates={templates}
isEditable={isEditable}
resolution={resolution}
onEditCell={onEditCell}
onCloneCell={onCloneCell}
onDeleteCell={onDeleteCell}
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
>
{cell.isWidget ? (
<WidgetCell cell={cell} timeRange={timeRange} source={source} />
) : (
<RefreshingGraph
axes={axes}
type={type}
cellHeight={cellHeight}
onZoom={onZoom}
colors={colors}
sources={sources}
inView={cell.inView}
timeRange={timeRange}
templates={templates}
isDragging={isDragging}
timeFormat={timeFormat}
autoRefresh={autoRefresh}
tableOptions={tableOptions}
fieldOptions={fieldOptions}
decimalPlaces={decimalPlaces}
manualRefresh={manualRefresh}
onSetResolution={onSetResolution}
staticLegend={IS_STATIC_LEGEND(legend)}
onStopAddAnnotation={onStopAddAnnotation}
grabDataForDownload={grabDataForDownload}
queries={buildQueriesForLayouts(cell, timeRange, host)}
source={getSource(cell, source, sources, defaultSource)}
/>
)}
</LayoutCell>
)
const {arrayOf, bool, func, number, shape, string} = PropTypes
const propTypes = {
isDragging: bool,
autoRefresh: number.isRequired,
manualRefresh: number,
timeRange: shape({
lower: string.isRequired,
}),
cell: shape({
// isWidget cells will not have queries
isWidget: bool,
queries: arrayOf(
shape({
label: string,
text: string,
query: string,
}).isRequired
),
x: number.isRequired,
y: number.isRequired,
w: number.isRequired,
h: number.isRequired,
i: string.isRequired,
name: string.isRequired,
type: string.isRequired,
colors: colorsStringSchema,
tableOptions: shape({
verticalTimeAxis: bool.isRequired,
sortBy: shape({
internalName: string.isRequired,
displayName: string.isRequired,
visible: bool.isRequired,
}).isRequired,
wrapping: string.isRequired,
fixFirstColumn: bool.isRequired,
}),
timeFormat: string,
decimalPlaces: shape({
isEnforced: bool.isRequired,
digits: number.isRequired,
}),
fieldOptions: arrayOf(
shape({
internalName: string.isRequired,
displayName: string.isRequired,
visible: bool.isRequired,
}).isRequired
),
}).isRequired,
templates: arrayOf(shape()),
host: string,
source: shape({
links: shape({
proxy: string.isRequired,
}).isRequired,
}).isRequired,
onPositionChange: func,
onEditCell: func,
onDeleteCell: func,
onCloneCell: func,
onSummonOverlayTechnologies: func,
isStatusPage: bool,
isEditable: bool,
onZoom: func,
sources: arrayOf(shape()),
}
LayoutState.propTypes = {...propTypes}
Layout.propTypes = {
...propTypes,
grabDataForDownload: func,
cellData: arrayOf(shape({})),
}
export default LayoutState

View File

@ -0,0 +1,112 @@
// Libraries
import React, {Component} from 'react'
import _ from 'lodash'
// Components
import WidgetCell from 'src/shared/components/WidgetCell'
import LayoutCell from 'src/shared/components/LayoutCell'
import RefreshingGraph from 'src/shared/components/RefreshingGraph'
// Utils
import {buildQueriesForLayouts} from 'src/utils/buildQueriesForLayouts'
// Constants
import {IS_STATIC_LEGEND} from 'src/shared/constants'
// Types
import {TimeRange, Cell, Template, Source} from 'src/types'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
cell: Cell
timeRange: TimeRange
templates: Template[]
source: Source
sources: Source[]
host: string
autoRefresh: number
isEditable: boolean
manualRefresh: number
onZoom: () => void
onDeleteCell: () => void
onCloneCell: () => void
onSummonOverlayTechnologies: () => void
}
@ErrorHandling
class Layout extends Component<Props> {
public state = {
cellData: [],
}
public render() {
const {
cell,
host,
source,
sources,
onZoom,
timeRange,
autoRefresh,
manualRefresh,
templates,
isEditable,
onCloneCell,
onDeleteCell,
onSummonOverlayTechnologies,
} = this.props
const {cellData} = this.state
return (
<LayoutCell
cell={cell}
cellData={cellData}
templates={templates}
isEditable={isEditable}
onCloneCell={onCloneCell}
onDeleteCell={onDeleteCell}
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
>
{cell.isWidget ? (
<WidgetCell cell={cell} timeRange={timeRange} source={source} />
) : (
<RefreshingGraph
onZoom={onZoom}
axes={cell.axes}
type={cell.type}
inView={cell.inView}
colors={cell.colors}
tableOptions={cell.tableOptions}
fieldOptions={cell.fieldOptions}
decimalPlaces={cell.decimalPlaces}
timeRange={timeRange}
templates={templates}
autoRefresh={autoRefresh}
manualRefresh={manualRefresh}
staticLegend={IS_STATIC_LEGEND(cell.legend)}
grabDataForDownload={this.grabDataForDownload}
queries={buildQueriesForLayouts(cell, timeRange, host)}
source={this.getSource(cell, source, sources, source)}
/>
)}
</LayoutCell>
)
}
private grabDataForDownload = cellData => {
this.setState({cellData})
}
private getSource = (cell, source, sources, defaultSource) => {
const s = _.get(cell, ['queries', '0', 'source'], null)
if (!s) {
return source
}
return sources.find(src => src.links.self === s) || defaultSource
}
}
export default Layout

View File

@ -24,7 +24,6 @@ interface Props {
isEditable: boolean
cellData: TimeSeriesServerResponse[]
templates: Template[]
resolution: number
}
@ErrorHandling

View File

@ -1,8 +1,12 @@
import React from 'react'
import PropTypes from 'prop-types'
import React, {SFC} from 'react'
import {isCellUntitled} from 'src/dashboards/utils/cellGetters'
const LayoutCellHeader = ({isEditable, cellName}) => {
interface Props {
isEditable: boolean
cellName: string
}
const LayoutCellHeader: SFC<Props> = ({isEditable, cellName}) => {
const headingClass = `dash-graph--heading ${
isEditable ? 'dash-graph--draggable dash-graph--heading-draggable' : ''
}`
@ -22,11 +26,4 @@ const LayoutCellHeader = ({isEditable, cellName}) => {
)
}
const {bool, string} = PropTypes
LayoutCellHeader.propTypes = {
isEditable: bool,
cellName: string,
}
export default LayoutCellHeader

View File

@ -1,12 +1,16 @@
// Libraries
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import ReactGridLayout, {WidthProvider} from 'react-grid-layout'
// Components
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
import Layout from 'src/shared/components/Layout'
const GridLayout = WidthProvider(ReactGridLayout)
// Utils
import {fastMap} from 'src/utils/fast'
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
import Layout from 'src/shared/components/Layout'
// Constants
import {
// TODO: get these const values dynamically
STATUS_PAGE_ROW_COUNT,
@ -14,13 +18,37 @@ import {
PAGE_CONTAINER_MARGIN,
LAYOUT_MARGIN,
DASHBOARD_LAYOUT_ROW_HEIGHT,
} from 'shared/constants'
} from 'src/shared/constants'
// Types
import {TimeRange, Cell, Template, Source} from 'src/types'
import {ErrorHandling} from 'src/shared/decorators/errors'
const GridLayout = WidthProvider(ReactGridLayout)
interface Props {
source: Source
cells: Cell[]
timeRange: TimeRange
templates: Template[]
sources: Source[]
host: string
autoRefresh: number
manualRefresh: number
isStatusPage: boolean
isEditable: boolean
onZoom?: () => void
onCloneCell?: () => void
onDeleteCell?: () => void
onSummonOverlayTechnologies?: () => void
onPositionChange?: (cells: Cell[]) => void
}
interface State {
rowHeight: number
}
@ErrorHandling
class LayoutRenderer extends Component {
class LayoutRenderer extends Component<Props, State> {
constructor(props) {
super(props)
@ -29,7 +57,80 @@ class LayoutRenderer extends Component {
}
}
handleLayoutChange = layout => {
public render() {
const {
host,
cells,
source,
sources,
onZoom,
templates,
timeRange,
isEditable,
autoRefresh,
manualRefresh,
onDeleteCell,
onCloneCell,
onSummonOverlayTechnologies,
} = this.props
const {rowHeight} = this.state
const isDashboard = !!this.props.onPositionChange
return (
<Authorized
requiredRole={EDITOR_ROLE}
propsOverride={{
isDraggable: false,
isResizable: false,
draggableHandle: null,
}}
>
<GridLayout
layout={cells}
cols={12}
rowHeight={rowHeight}
margin={[LAYOUT_MARGIN, LAYOUT_MARGIN]}
containerPadding={[0, 0]}
useCSSTransforms={false}
onLayoutChange={this.handleLayoutChange}
draggableHandle={'.dash-graph--draggable'}
isDraggable={isDashboard}
isResizable={isDashboard}
>
{fastMap(cells, cell => (
<div key={cell.i}>
<Authorized
requiredRole={EDITOR_ROLE}
propsOverride={{
isEditable: false,
}}
>
<Layout
key={cell.i}
cell={cell}
host={host}
source={source}
onZoom={onZoom}
sources={sources}
templates={templates}
timeRange={timeRange}
isEditable={isEditable}
autoRefresh={autoRefresh}
onDeleteCell={onDeleteCell}
onCloneCell={onCloneCell}
manualRefresh={manualRefresh}
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
/>
</Authorized>
</div>
))}
</GridLayout>
</Authorized>
)
}
private handleLayoutChange = layout => {
if (!this.props.onPositionChange) {
return
}
@ -67,7 +168,7 @@ class LayoutRenderer extends Component {
}
// ensures that Status Page height fits the window
calculateRowHeight = () => {
private calculateRowHeight = () => {
const {isStatusPage} = this.props
return isStatusPage
@ -79,147 +180,6 @@ class LayoutRenderer extends Component {
STATUS_PAGE_ROW_COUNT
: DASHBOARD_LAYOUT_ROW_HEIGHT
}
render() {
const {
host,
cells,
source,
sources,
onZoom,
templates,
timeRange,
isEditable,
onEditCell,
autoRefresh,
manualRefresh,
onDeleteCell,
onCloneCell,
onSummonOverlayTechnologies,
} = this.props
const {rowHeight} = this.state
const isDashboard = !!this.props.onPositionChange
return (
<Authorized
requiredRole={EDITOR_ROLE}
propsOverride={{
isDraggable: false,
isResizable: false,
draggableHandle: null,
}}
>
<GridLayout
layout={cells}
cols={12}
rowHeight={rowHeight}
margin={[LAYOUT_MARGIN, LAYOUT_MARGIN]}
containerPadding={[0, 0]}
useCSSTransforms={false}
onResize={this.handleCellResize}
onLayoutChange={this.handleLayoutChange}
draggableHandle={'.dash-graph--draggable'}
isDraggable={isDashboard}
isResizable={isDashboard}
>
{fastMap(cells, cell => (
<div key={cell.i}>
<Authorized
requiredRole={EDITOR_ROLE}
propsOverride={{
isEditable: false,
}}
>
<Layout
key={cell.i}
cell={cell}
host={host}
source={source}
onZoom={onZoom}
sources={sources}
templates={templates}
timeRange={timeRange}
isEditable={isEditable}
onEditCell={onEditCell}
autoRefresh={autoRefresh}
onDeleteCell={onDeleteCell}
onCloneCell={onCloneCell}
manualRefresh={manualRefresh}
onStopAddAnnotation={this.handleStopAddAnnotation}
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
/>
</Authorized>
</div>
))}
</GridLayout>
</Authorized>
)
}
}
const {arrayOf, bool, func, number, shape, string} = PropTypes
LayoutRenderer.propTypes = {
autoRefresh: number.isRequired,
manualRefresh: number,
timeRange: shape({
lower: string.isRequired,
}),
cells: arrayOf(
shape({
// isWidget cells will not have queries
isWidget: bool,
queries: arrayOf(
shape({
label: string,
text: string,
query: string,
}).isRequired
),
x: number.isRequired,
y: number.isRequired,
w: number.isRequired,
h: number.isRequired,
i: string.isRequired,
name: string.isRequired,
type: string.isRequired,
timeFormat: string,
tableOptions: shape({
verticalTimeAxis: bool.isRequired,
sortBy: shape({
internalName: string.isRequired,
displayName: string.isRequired,
visible: bool.isRequired,
}).isRequired,
wrapping: string.isRequired,
fixFirstColumn: bool.isRequired,
}),
fieldOptions: arrayOf(
shape({
internalName: string.isRequired,
displayName: string.isRequired,
visible: bool.isRequired,
}).isRequired
),
}).isRequired
),
templates: arrayOf(shape()),
host: string,
source: shape({
links: shape({
proxy: string.isRequired,
}).isRequired,
}).isRequired,
onPositionChange: func,
onEditCell: func,
onDeleteCell: func,
onCloneCell: func,
onSummonOverlayTechnologies: func,
isStatusPage: bool,
isEditable: bool,
onZoom: func,
sources: arrayOf(shape({})),
}
export default LayoutRenderer

View File

@ -1,65 +1,48 @@
// Libraries
import React, {PureComponent, CSSProperties} from 'react'
import Dygraph from 'src/shared/components/Dygraph'
import {withRouter, RouteComponentProps} from 'react-router'
import _ from 'lodash'
// Components
import SingleStat from 'src/shared/components/SingleStat'
import {ErrorHandlingWith} from 'src/shared/decorators/errors'
import InvalidData from 'src/shared/components/InvalidData'
// Utils
import {
timeSeriesToDygraph,
TimeSeriesToDyGraphReturnType,
} from 'src/utils/timeSeriesTransformers'
import {ErrorHandlingWith} from 'src/shared/decorators/errors'
import InvalidData from 'src/shared/components/InvalidData'
import {Query, Axes, RuleValues, TimeRange} from 'src/types'
import {DecimalPlaces} from 'src/types/dashboards'
// Types
import {ColorString} from 'src/types/colors'
import {Data} from 'src/types/dygraphs'
const validateTimeSeries = ts => {
return _.every(ts, r =>
_.every(
r,
(v, i: number) =>
(i === 0 && Date.parse(v)) || _.isNumber(v) || _.isNull(v)
)
)
}
import {DecimalPlaces} from 'src/types/dashboards'
import {TimeSeriesServerResponse} from 'src/types/series'
import {Query, Axes, TimeRange, RemoteDataState, CellType} from 'src/types'
interface Props {
axes: Axes
title: string
type: CellType
queries: Query[]
timeRange: TimeRange
colors: ColorString[]
loading: RemoteDataState
decimalPlaces: DecimalPlaces
data: TimeSeriesServerResponse[]
cellID: string
cellHeight: number
isFetchingInitially: boolean
isRefreshing: boolean
isGraphFilled: boolean
isBarGraph: boolean
staticLegend: boolean
showSingleStat: boolean
displayOptions: {
stepPlot: boolean
stackedGraph: boolean
animatedZooms: boolean
}
activeQueryIndex: number
ruleValues: RuleValues
timeRange: TimeRange
isInDataExplorer: boolean
onZoom: () => void
data: Data
queries: Query[]
colors: ColorString[]
decimalPlaces: DecimalPlaces
underlayCallback?: () => void
setResolution: () => void
handleSetHoverTime: () => void
activeQueryIndex?: number
}
type LineGraphProps = Props & RouteComponentProps<any, any>
@ErrorHandlingWith(InvalidData)
class LineGraph extends PureComponent<Props> {
public static defaultProps: Partial<Props> = {
underlayCallback: () => {},
isGraphFilled: true,
class LineGraph extends PureComponent<LineGraphProps> {
public static defaultProps: Partial<LineGraphProps> = {
staticLegend: false,
}
@ -67,15 +50,16 @@ class LineGraph extends PureComponent<Props> {
private timeSeries: TimeSeriesToDyGraphReturnType
public componentWillMount() {
const {data, isInDataExplorer} = this.props
this.parseTimeSeries(data, isInDataExplorer)
const {data} = this.props
this.parseTimeSeries(data)
}
public parseTimeSeries(data, isInDataExplorer) {
this.timeSeries = timeSeriesToDygraph(data, isInDataExplorer)
this.isValidData = validateTimeSeries(
_.get(this.timeSeries, 'timeSeries', [])
)
public parseTimeSeries(data) {
const {location} = this.props
this.timeSeries = timeSeriesToDygraph(data, location.pathname)
const timeSeries = _.get(this.timeSeries, 'timeSeries', [])
this.isValidData = this.validateTimeSeries(timeSeries)
}
public componentWillUpdate(nextProps) {
@ -84,7 +68,7 @@ class LineGraph extends PureComponent<Props> {
data !== nextProps.data ||
activeQueryIndex !== nextProps.activeQueryIndex
) {
this.parseTimeSeries(nextProps.data, nextProps.isInDataExplorer)
this.parseTimeSeries(nextProps.data)
}
}
@ -96,54 +80,41 @@ class LineGraph extends PureComponent<Props> {
const {
data,
axes,
title,
type,
colors,
cellID,
onZoom,
loading,
queries,
timeRange,
cellHeight,
ruleValues,
isBarGraph,
isRefreshing,
setResolution,
isGraphFilled,
showSingleStat,
displayOptions,
staticLegend,
decimalPlaces,
underlayCallback,
isFetchingInitially,
handleSetHoverTime,
} = this.props
const {labels, timeSeries, dygraphSeries} = this.timeSeries
// If data for this graph is being fetched for the first time, show a graph-wide spinner.
if (isFetchingInitially) {
return <GraphSpinner />
}
const options = {
...displayOptions,
title,
labels,
rightGap: 0,
yRangePad: 10,
labelsKMB: true,
fillGraph: true,
underlayCallback,
axisLabelWidth: 60,
animatedZooms: true,
drawAxesAtZero: true,
axisLineColor: '#383846',
gridLineColor: '#383846',
connectSeparatedPoints: true,
stepPlot: type === 'line-stepplot',
stackedGraph: type === 'line-stacked',
}
return (
<div className="dygraph graph--hasYLabel" style={this.style}>
{isRefreshing && <GraphLoadingDots />}
{loading === RemoteDataState.Loading && <GraphLoadingDots />}
<Dygraph
type={type}
axes={axes}
cellID={cellID}
colors={colors}
@ -152,17 +123,14 @@ class LineGraph extends PureComponent<Props> {
queries={queries}
options={options}
timeRange={timeRange}
isBarGraph={isBarGraph}
timeSeries={timeSeries}
ruleValues={ruleValues}
staticLegend={staticLegend}
dygraphSeries={dygraphSeries}
setResolution={setResolution}
isGraphFilled={this.isGraphFilled}
containerStyle={this.containerStyle}
handleSetHoverTime={handleSetHoverTime}
isGraphFilled={showSingleStat ? false : isGraphFilled}
>
{showSingleStat && (
{type === CellType.LinePlusSingleStat && (
<SingleStat
data={data}
lineGraph={true}
@ -171,7 +139,6 @@ class LineGraph extends PureComponent<Props> {
suffix={this.suffix}
cellHeight={cellHeight}
decimalPlaces={decimalPlaces}
isFetchingInitially={isFetchingInitially}
/>
)}
</Dygraph>
@ -179,6 +146,26 @@ class LineGraph extends PureComponent<Props> {
)
}
private validateTimeSeries = ts => {
return _.every(ts, r =>
_.every(
r,
(v, i: number) =>
(i === 0 && Date.parse(v)) || _.isNumber(v) || _.isNull(v)
)
)
}
private get isGraphFilled(): boolean {
const {type} = this.props
if (type === CellType.LinePlusSingleStat) {
return false
}
return true
}
private get style(): CSSProperties {
return {height: '100%'}
}
@ -221,10 +208,4 @@ const GraphLoadingDots = () => (
</div>
)
const GraphSpinner = () => (
<div className="graph-fetching">
<div className="graph-spinner" />
</div>
)
export default LineGraph
export default withRouter<Props>(LineGraph)

View File

@ -1,231 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import {emptyGraphCopy} from 'src/shared/copy/cell'
import {bindActionCreators} from 'redux'
import AutoRefresh from 'shared/components/AutoRefresh'
import LineGraph from 'shared/components/LineGraph'
import SingleStat from 'shared/components/SingleStat'
import GaugeChart from 'shared/components/GaugeChart'
import TableGraph from 'shared/components/TableGraph'
import {colorsStringSchema} from 'shared/schemas'
import {setHoverTime} from 'src/dashboards/actions'
import {
DEFAULT_TIME_FORMAT,
DEFAULT_DECIMAL_PLACES,
} from 'src/dashboards/constants'
const RefreshingLineGraph = AutoRefresh(LineGraph)
const RefreshingSingleStat = AutoRefresh(SingleStat)
const RefreshingGaugeChart = AutoRefresh(GaugeChart)
const RefreshingTableGraph = AutoRefresh(TableGraph)
const RefreshingGraph = ({
axes,
inView,
type,
colors,
onZoom,
cellID,
queries,
source,
templates,
timeRange,
cellHeight,
autoRefresh,
fieldOptions,
timeFormat,
tableOptions,
decimalPlaces,
onSetResolution,
resizerTopHeight,
staticLegend,
manualRefresh, // when changed, re-mounts the component
editQueryStatus,
handleSetHoverTime,
grabDataForDownload,
isInCEO,
}) => {
const prefix = (axes && axes.y.prefix) || ''
const suffix = (axes && axes.y.suffix) || ''
if (!queries.length) {
return (
<div className="graph-empty">
<p data-test="data-explorer-no-results">{emptyGraphCopy}</p>
</div>
)
}
if (type === 'single-stat') {
return (
<RefreshingSingleStat
type={type}
source={source}
colors={colors}
prefix={prefix}
suffix={suffix}
inView={inView}
key={manualRefresh}
templates={templates}
queries={[queries[0]]}
cellHeight={cellHeight}
autoRefresh={autoRefresh}
decimalPlaces={decimalPlaces}
editQueryStatus={editQueryStatus}
onSetResolution={onSetResolution}
/>
)
}
if (type === 'gauge') {
return (
<RefreshingGaugeChart
type={type}
source={source}
cellID={cellID}
prefix={prefix}
suffix={suffix}
inView={inView}
colors={colors}
key={manualRefresh}
queries={[queries[0]]}
templates={templates}
autoRefresh={autoRefresh}
cellHeight={cellHeight}
decimalPlaces={decimalPlaces}
resizerTopHeight={resizerTopHeight}
editQueryStatus={editQueryStatus}
onSetResolution={onSetResolution}
/>
)
}
if (type === 'table') {
return (
<RefreshingTableGraph
type={type}
source={source}
cellID={cellID}
colors={colors}
inView={inView}
isInCEO={isInCEO}
key={manualRefresh}
queries={queries}
templates={templates}
autoRefresh={autoRefresh}
cellHeight={cellHeight}
tableOptions={tableOptions}
fieldOptions={fieldOptions}
timeFormat={timeFormat}
decimalPlaces={decimalPlaces}
editQueryStatus={editQueryStatus}
resizerTopHeight={resizerTopHeight}
grabDataForDownload={grabDataForDownload}
handleSetHoverTime={handleSetHoverTime}
onSetResolution={onSetResolution}
/>
)
}
const displayOptions = {
stepPlot: type === 'line-stepplot',
stackedGraph: type === 'line-stacked',
}
return (
<RefreshingLineGraph
type={type}
axes={axes}
source={source}
cellID={cellID}
colors={colors}
onZoom={onZoom}
queries={queries}
inView={inView}
key={manualRefresh}
templates={templates}
timeRange={timeRange}
cellHeight={cellHeight}
autoRefresh={autoRefresh}
isBarGraph={type === 'bar'}
decimalPlaces={decimalPlaces}
staticLegend={staticLegend}
displayOptions={displayOptions}
editQueryStatus={editQueryStatus}
grabDataForDownload={grabDataForDownload}
handleSetHoverTime={handleSetHoverTime}
showSingleStat={type === 'line-plus-single-stat'}
onSetResolution={onSetResolution}
/>
)
}
const {arrayOf, bool, func, number, shape, string} = PropTypes
RefreshingGraph.propTypes = {
timeRange: shape({
lower: string.isRequired,
}),
autoRefresh: number.isRequired,
manualRefresh: number,
templates: arrayOf(shape()),
type: string.isRequired,
cellHeight: number,
resizerTopHeight: number,
axes: shape(),
queries: arrayOf(shape()).isRequired,
editQueryStatus: func,
staticLegend: bool,
onZoom: func,
grabDataForDownload: func,
colors: colorsStringSchema,
cellID: string,
inView: bool,
tableOptions: shape({
verticalTimeAxis: bool.isRequired,
sortBy: shape({
internalName: string.isRequired,
displayName: string.isRequired,
visible: bool.isRequired,
}).isRequired,
wrapping: string.isRequired,
fixFirstColumn: bool.isRequired,
}),
fieldOptions: arrayOf(
shape({
internalName: string.isRequired,
displayName: string.isRequired,
visible: bool.isRequired,
}).isRequired
),
timeFormat: string.isRequired,
decimalPlaces: shape({
isEnforced: bool.isRequired,
digits: number.isRequired,
}).isRequired,
handleSetHoverTime: func.isRequired,
isInCEO: bool,
onSetResolution: func,
source: shape().isRequired,
}
RefreshingGraph.defaultProps = {
manualRefresh: 0,
staticLegend: false,
inView: true,
timeFormat: DEFAULT_TIME_FORMAT,
decimalPlaces: DEFAULT_DECIMAL_PLACES,
}
const mapStateToProps = ({annotations: {mode}}) => ({
mode,
})
const mapDispatchToProps = dispatch => ({
handleSetHoverTime: bindActionCreators(setHoverTime, dispatch),
})
export default connect(mapStateToProps, mapDispatchToProps)(RefreshingGraph)

View File

@ -0,0 +1,238 @@
// Libraries
import React, {PureComponent} from 'react'
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,
DEFAULT_DECIMAL_PLACES,
} from 'src/dashboards/constants'
// Actions
import {setHoverTime} from 'src/dashboards/actions'
// Types
import {ColorString} from 'src/types/colors'
import {Source, Axes, TimeRange, Template, Query, CellType} from 'src/types'
import {TableOptions, FieldOption, DecimalPlaces} from 'src/types/dashboards'
interface Props {
axes: Axes
source: Source
queries: Query[]
timeRange: TimeRange
colors: ColorString[]
templates: Template[]
tableOptions: TableOptions
fieldOptions: FieldOption[]
decimalPlaces: DecimalPlaces
type: CellType
cellID: string
inView: boolean
isInCEO: boolean
timeFormat: string
cellHeight: number
autoRefresh: number
staticLegend: boolean
manualRefresh: number
resizerTopHeight: number
onZoom: () => void
editQueryStatus: () => void
onSetResolution: () => void
grabDataForDownload: () => void
handleSetHoverTime: () => void
}
class RefreshingGraph extends PureComponent<Props> {
public static defaultProps: Partial<Props> = {
inView: true,
manualRefresh: 0,
staticLegend: false,
timeFormat: DEFAULT_TIME_FORMAT,
decimalPlaces: DEFAULT_DECIMAL_PLACES,
}
public render() {
const {inView, type, queries, source, templates} = this.props
if (!queries.length) {
return (
<div className="graph-empty">
<p data-test="data-explorer-no-results">{emptyGraphCopy}</p>
</div>
)
}
return (
<TimeSeries
source={source}
inView={inView}
queries={this.queries}
templates={templates}
>
{({timeSeries, loading}) => {
switch (type) {
case CellType.SingleStat:
return this.singleStat(timeSeries)
case CellType.Table:
return this.table(timeSeries)
case CellType.Gauge:
return this.gauge(timeSeries)
default:
return this.lineGraph(timeSeries, loading)
}
}}
</TimeSeries>
)
}
private singleStat = (data): JSX.Element => {
const {colors, cellHeight, decimalPlaces, manualRefresh} = this.props
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 {
colors,
fieldOptions,
timeFormat,
tableOptions,
decimalPlaces,
manualRefresh,
handleSetHoverTime,
grabDataForDownload,
isInCEO,
} = this.props
return (
<TableGraph
data={data}
colors={colors}
isInCEO={isInCEO}
key={manualRefresh}
tableOptions={tableOptions}
fieldOptions={fieldOptions}
timeFormat={timeFormat}
decimalPlaces={decimalPlaces}
grabDataForDownload={grabDataForDownload}
handleSetHoverTime={handleSetHoverTime}
/>
)
}
private gauge = (data): JSX.Element => {
const {
colors,
cellID,
cellHeight,
decimalPlaces,
manualRefresh,
resizerTopHeight,
} = this.props
return (
<GaugeChart
data={data}
cellID={cellID}
colors={colors}
prefix={this.prefix}
suffix={this.suffix}
key={manualRefresh}
cellHeight={cellHeight}
decimalPlaces={decimalPlaces}
resizerTopHeight={resizerTopHeight}
/>
)
}
private lineGraph = (data, loading): JSX.Element => {
const {
axes,
type,
colors,
onZoom,
cellID,
queries,
timeRange,
cellHeight,
decimalPlaces,
staticLegend,
manualRefresh,
handleSetHoverTime,
} = this.props
return (
<LineGraph
data={data}
type={type}
axes={axes}
cellID={cellID}
colors={colors}
onZoom={onZoom}
queries={queries}
loading={loading}
key={manualRefresh}
timeRange={timeRange}
cellHeight={cellHeight}
staticLegend={staticLegend}
decimalPlaces={decimalPlaces}
handleSetHoverTime={handleSetHoverTime}
/>
)
}
private get queries(): Query[] {
const {queries, type} = this.props
if (type === CellType.SingleStat) {
return [queries[0]]
}
if (type === CellType.Gauge) {
return [queries[0]]
}
return queries
}
private get prefix(): string {
const {axes} = this.props
return _.get(axes, 'y.prefix', '')
}
private get suffix(): string {
const {axes} = this.props
return _.get(axes, 'y.suffix', '')
}
}
const mapStateToProps = ({annotations: {mode}}) => ({
mode,
})
const mdtp = {
handleSetHoverTime: setHoverTime,
}
export default connect(mapStateToProps, mdtp)(RefreshingGraph)

View File

@ -8,11 +8,10 @@ import {DYGRAPH_CONTAINER_V_MARGIN} from 'src/shared/constants'
import {generateThresholdsListHexs} from 'src/shared/constants/colorOperations'
import {ColorString} from 'src/types/colors'
import {CellType, DecimalPlaces} from 'src/types/dashboards'
import {Data} from 'src/types/dygraphs'
import {TimeSeriesServerResponse} from 'src/types/series'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
isFetchingInitially: boolean
decimalPlaces: DecimalPlaces
cellHeight: number
colors: ColorString[]
@ -20,7 +19,7 @@ interface Props {
suffix?: string
lineGraph: boolean
staticLegendHeight?: number
data: Data
data: TimeSeriesServerResponse[]
}
@ErrorHandling
@ -31,16 +30,6 @@ class SingleStat extends PureComponent<Props> {
}
public render() {
const {isFetchingInitially} = this.props
if (isFetchingInitially) {
return (
<div className="graph-empty">
<h3 className="graph-spinner" />
</div>
)
}
return (
<div className="single-stat" style={this.containerStyle}>
{this.resizerBox}

View File

@ -0,0 +1,152 @@
// Library
import React, {Component} from 'react'
import _ from 'lodash'
// API
import {fetchTimeSeries} from 'src/shared/apis/query'
// Types
import {Template, Source, Query, RemoteDataState} from 'src/types'
import {TimeSeriesServerResponse, TimeSeriesResponse} from 'src/types/series'
// Utils
import AutoRefresh from 'src/utils/AutoRefresh'
export const DEFAULT_TIME_SERIES = [{response: {results: []}}]
interface RenderProps {
timeSeries: TimeSeriesServerResponse[]
loading: RemoteDataState
}
interface Props {
source: Source
queries: Query[]
children: (r: RenderProps) => JSX.Element
inView?: boolean
templates?: Template[]
}
interface State {
loading: RemoteDataState
isFirstFetch: boolean
timeSeries: TimeSeriesServerResponse[]
}
class TimeSeries extends Component<Props, State> {
public static defaultProps = {
inView: true,
templates: [],
}
constructor(props: Props) {
super(props)
this.state = {
timeSeries: DEFAULT_TIME_SERIES,
loading: RemoteDataState.NotStarted,
isFirstFetch: false,
}
}
public async componentDidMount() {
const isFirstFetch = true
this.executeQueries(isFirstFetch)
AutoRefresh.subscribe(this.executeQueries)
}
public componentWillUnmount() {
AutoRefresh.unsubscribe(this.executeQueries)
}
public async componentDidUpdate(prevProps: Props) {
if (!this.isPropsDifferent(prevProps)) {
return
}
this.executeQueries()
}
public executeQueries = async (isFirstFetch: boolean = false) => {
const {source, inView, queries, templates} = this.props
if (!inView) {
return
}
if (!queries.length) {
return this.setState({timeSeries: DEFAULT_TIME_SERIES})
}
this.setState({loading: RemoteDataState.Loading, isFirstFetch})
const TEMP_RES = 300
try {
const timeSeries = await fetchTimeSeries(
source,
queries,
TEMP_RES,
templates
)
const newSeries = timeSeries.map((response: TimeSeriesResponse) => ({
response,
}))
this.setState({
timeSeries: newSeries,
loading: RemoteDataState.Done,
})
} catch (err) {
console.error(err)
}
}
public render() {
const {timeSeries, loading, isFirstFetch} = this.state
const hasValues = _.some(timeSeries, s => {
const results = _.get(s, 'response.results', [])
const v = _.some(results, r => r.series)
return v
})
if (isFirstFetch && loading === RemoteDataState.Loading) {
return (
<div className="graph-empty">
<h3 className="graph-spinner" />
</div>
)
}
if (!hasValues) {
return (
<div className="graph-empty">
<p>No Results</p>
</div>
)
}
return this.props.children({timeSeries, loading})
}
private isPropsDifferent(nextProps: Props) {
const isSourceDifferent = !_.isEqual(this.props.source, nextProps.source)
return (
this.props.inView !== nextProps.inView ||
!!this.queryDifference(this.props.queries, nextProps.queries).length ||
!_.isEqual(this.props.templates, nextProps.templates) ||
isSourceDifferent
)
}
private queryDifference = (left, right) => {
const mapper = q => `${q.text}`
const l = left.map(mapper)
const r = right.map(mapper)
return _.difference(_.union(l, r), _.intersection(l, r))
}
}
export default TimeSeries

View File

@ -1,3 +1,4 @@
// TODO: Delete me!
export const DEFAULT_TIME_SERIES = [
{
response: {

View File

@ -1,31 +1,14 @@
import _ from 'lodash'
import {Data} from 'src/types/dygraphs'
import {TimeSeriesServerResponse} from 'src/types/series'
interface Result {
lastValues: number[]
series: string[]
}
type SeriesValue = number | string
interface Series {
name: string
values: SeriesValue[][] | null
columns: string[] | null
}
interface TimeSeriesResult {
series: Series[]
}
export interface TimeSeriesResponse {
response: {
results: TimeSeriesResult[]
}
}
export default function(
timeSeriesResponse: TimeSeriesResponse[] | Data | null
timeSeriesResponse: TimeSeriesServerResponse[] | Data | null
): Result {
const values = _.get(
timeSeriesResponse,

View File

@ -1,18 +1,30 @@
// Libraries
import React, {Component} from 'react'
// Components
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import LayoutRenderer from 'src/shared/components/LayoutRenderer'
import {STATUS_PAGE_TIME_RANGE} from 'src/shared/data/timeRanges'
import {AUTOREFRESH_DEFAULT} from 'src/shared/constants'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
// Constants
import {AUTOREFRESH_DEFAULT} from 'src/shared/constants'
import {STATUS_PAGE_TIME_RANGE} from 'src/shared/data/timeRanges'
import {fixtureStatusPageCells} from 'src/status/fixtures'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {
TEMP_VAR_DASHBOARD_TIME,
TEMP_VAR_UPPER_DASHBOARD_TIME,
} from 'src/shared/constants'
import {Source, Cell} from 'src/types'
// Types
import {
Source,
Template,
Cell,
TemplateType,
TemplateValueType,
} from 'src/types'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface State {
cells: Cell[]
@ -39,36 +51,6 @@ class StatusPage extends Component<Props, State> {
const {source} = this.props
const {cells} = this.state
const dashboardTime = {
id: 'dashtime',
tempVar: TEMP_VAR_DASHBOARD_TIME,
type: 'constant',
values: [
{
value: timeRange.lower,
type: 'constant',
selected: true,
localSelected: true,
},
],
}
const upperDashboardTime = {
id: 'upperdashtime',
tempVar: TEMP_VAR_UPPER_DASHBOARD_TIME,
type: 'constant',
values: [
{
value: 'now()',
type: 'constant',
selected: true,
localSelected: true,
},
],
}
const templates = [dashboardTime, upperDashboardTime]
return (
<div className="page">
<PageHeader
@ -80,14 +62,16 @@ class StatusPage extends Component<Props, State> {
<div className="dashboard container-fluid full-width">
{cells.length ? (
<LayoutRenderer
autoRefresh={autoRefresh}
timeRange={timeRange}
host=""
sources={[]}
cells={cells}
templates={templates}
source={source}
shouldNotBeEditable={true}
isStatusPage={true}
manualRefresh={0}
isEditable={false}
isStatusPage={true}
timeRange={timeRange}
templates={this.templates}
autoRefresh={autoRefresh}
/>
) : (
<span>Loading Status Page...</span>
@ -97,6 +81,40 @@ class StatusPage extends Component<Props, State> {
</div>
)
}
private get templates(): Template[] {
const dashboardTime = {
id: 'dashtime',
tempVar: TEMP_VAR_DASHBOARD_TIME,
type: TemplateType.Constant,
label: '',
values: [
{
value: timeRange.lower,
type: TemplateValueType.Constant,
selected: true,
localSelected: true,
},
],
}
const upperDashboardTime = {
id: 'upperdashtime',
tempVar: TEMP_VAR_UPPER_DASHBOARD_TIME,
type: TemplateType.Constant,
label: '',
values: [
{
value: 'now()',
type: TemplateValueType.Constant,
selected: true,
localSelected: true,
},
],
}
return [dashboardTime, upperDashboardTime]
}
}
export default StatusPage

View File

@ -2,8 +2,7 @@ import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes'
import {TEMP_VAR_DASHBOARD_TIME} from 'src/shared/constants'
import {NEW_DEFAULT_DASHBOARD_CELL} from 'src/dashboards/constants/index'
import {DEFAULT_AXIS} from 'src/dashboards/constants/cellEditor'
import {Cell, CellQuery, Axes} from 'src/types'
import {CellType} from 'src/types/dashboards'
import {Cell, CellQuery, Axes, CellType} from 'src/types'
const emptyQuery: CellQuery = {
query: '',

View File

@ -76,6 +76,7 @@ export interface Cell {
links: CellLinks
legend: Legend
isWidget?: boolean
inView: boolean
}
export enum CellType {

View File

@ -0,0 +1,42 @@
type func = (...args: any[]) => any
class AutoRefresh {
public subscribers: func[] = []
private intervalID: NodeJS.Timer
public subscribe(fn: func) {
this.subscribers = [...this.subscribers, fn]
}
public unsubscribe(fn: func) {
this.subscribers = this.subscribers.filter(f => f !== fn)
}
public poll(refreshMs: number) {
this.clearInterval()
if (refreshMs) {
this.intervalID = setInterval(this.refresh, refreshMs)
}
}
public stopPolling() {
this.clearInterval()
}
private clearInterval() {
if (!this.intervalID) {
return
}
clearInterval(this.intervalID)
this.intervalID = null
}
private refresh = () => {
this.subscribers.forEach(fn => fn())
}
}
export default new AutoRefresh()

View File

@ -27,9 +27,10 @@ interface TimeSeriesToTableGraphReturnType {
export const timeSeriesToDygraph = (
raw: TimeSeriesServerResponse[],
isInDataExplorer: boolean
pathname: string = ''
): TimeSeriesToDyGraphReturnType => {
const isTable = false
const isInDataExplorer = pathname.includes('data-explorer')
const {sortedLabels, sortedTimeSeries} = groupByTimeSeriesTransform(
raw,
isTable

View File

@ -185,6 +185,7 @@ export const cell: Cell = {
self:
'/chronograf/v1/dashboards/9/cells/67435af2-17bf-4caa-a5fc-0dd1ffb40dab',
},
inView: true,
}
export const fullTimeRange = {

View File

@ -698,4 +698,5 @@ export const cell: Cell = {
'/chronograf/v1/dashboards/10/cells/8b3b7897-49b1-422c-9443-e9b778bcbf12',
},
legend: {},
inView: true,
}

View File

@ -21,6 +21,7 @@ describe('timeSeriesToDygraph', () => {
response: {
results: [
{
statement_id: 0,
series: [
{
name: 'm1',
@ -30,6 +31,7 @@ describe('timeSeriesToDygraph', () => {
],
},
{
statement_id: 0,
series: [
{
name: 'm1',
@ -71,6 +73,7 @@ describe('timeSeriesToDygraph', () => {
response: {
results: [
{
statement_id: 0,
series: [
{
name: 'm1',
@ -100,6 +103,7 @@ describe('timeSeriesToDygraph', () => {
response: {
results: [
{
statement_id: 0,
series: [
{
name: 'm1',
@ -109,6 +113,7 @@ describe('timeSeriesToDygraph', () => {
],
},
{
statement_id: 0,
series: [
{
name: 'm1',
@ -124,6 +129,7 @@ describe('timeSeriesToDygraph', () => {
response: {
results: [
{
statement_id: 0,
series: [
{
name: 'm3',
@ -160,6 +166,7 @@ describe('timeSeriesToDygraph', () => {
response: {
results: [
{
statement_id: 0,
series: [
{
name: 'm1',
@ -175,6 +182,7 @@ describe('timeSeriesToDygraph', () => {
response: {
results: [
{
statement_id: 0,
series: [
{
name: 'm1',
@ -212,6 +220,7 @@ describe('timeSeriesToDygraph', () => {
response: {
results: [
{
statement_id: 0,
series: [
{
name: 'm1',
@ -227,6 +236,7 @@ describe('timeSeriesToDygraph', () => {
response: {
results: [
{
statement_id: 0,
series: [
{
name: 'm1',
@ -240,8 +250,7 @@ describe('timeSeriesToDygraph', () => {
},
]
const isInDataExplorer = true
const actual = timeSeriesToDygraph(influxResponse, isInDataExplorer)
const actual = timeSeriesToDygraph(influxResponse, 'data-explorer')
const expected = {}
@ -254,6 +263,7 @@ describe('timeSeriesToDygraph', () => {
response: {
results: [
{
statement_id: 0,
series: [
{
name: 'mb',
@ -263,6 +273,7 @@ describe('timeSeriesToDygraph', () => {
],
},
{
statement_id: 0,
series: [
{
name: 'ma',
@ -272,6 +283,7 @@ describe('timeSeriesToDygraph', () => {
],
},
{
statement_id: 0,
series: [
{
name: 'mc',
@ -281,6 +293,7 @@ describe('timeSeriesToDygraph', () => {
],
},
{
statement_id: 0,
series: [
{
name: 'mc',
@ -309,6 +322,7 @@ describe('timeSeriesToTableGraph', () => {
response: {
results: [
{
statement_id: 0,
series: [
{
name: 'mb',
@ -318,6 +332,7 @@ describe('timeSeriesToTableGraph', () => {
],
},
{
statement_id: 0,
series: [
{
name: 'ma',
@ -327,6 +342,7 @@ describe('timeSeriesToTableGraph', () => {
],
},
{
statement_id: 0,
series: [
{
name: 'mc',
@ -336,6 +352,7 @@ describe('timeSeriesToTableGraph', () => {
],
},
{
statement_id: 0,
series: [
{
name: 'mc',
@ -349,38 +366,7 @@ describe('timeSeriesToTableGraph', () => {
},
]
const qASTs = [
{
groupBy: {
time: {
interval: '2s',
},
},
},
{
groupBy: {
time: {
interval: '2s',
},
},
},
{
groupBy: {
time: {
interval: '2s',
},
},
},
{
groupBy: {
time: {
interval: '2s',
},
},
},
]
const actual = timeSeriesToTableGraph(influxResponse, qASTs)
const actual = timeSeriesToTableGraph(influxResponse)
const expected = [
['time', 'ma.f1', 'mb.f1', 'mc.f1', 'mc.f2'],
[1000, 1, 1, null, null],
@ -397,6 +383,7 @@ describe('timeSeriesToTableGraph', () => {
response: {
results: [
{
statement_id: 0,
series: [
{
name: 'mb',
@ -406,6 +393,7 @@ describe('timeSeriesToTableGraph', () => {
],
},
{
statement_id: 0,
series: [
{
name: 'ma',
@ -415,6 +403,7 @@ describe('timeSeriesToTableGraph', () => {
],
},
{
statement_id: 0,
series: [
{
name: 'mc',
@ -424,6 +413,7 @@ describe('timeSeriesToTableGraph', () => {
],
},
{
statement_id: 0,
series: [
{
name: 'mc',
@ -437,38 +427,7 @@ describe('timeSeriesToTableGraph', () => {
},
]
const qASTs = [
{
groupBy: {
time: {
interval: '2s',
},
},
},
{
groupBy: {
time: {
interval: '2s',
},
},
},
{
groupBy: {
time: {
interval: '2s',
},
},
},
{
groupBy: {
time: {
interval: '2s',
},
},
},
]
const actual = timeSeriesToTableGraph(influxResponse, qASTs)
const actual = timeSeriesToTableGraph(influxResponse)
const expected = ['time', 'ma.f1', 'mb.f1', 'mc.f1', 'mc.f2']
expect(actual.data[0]).toEqual(expected)
@ -476,10 +435,7 @@ describe('timeSeriesToTableGraph', () => {
it('returns an array of an empty array if there is an empty response', () => {
const influxResponse = []
const qASTs = []
const actual = timeSeriesToTableGraph(influxResponse, qASTs)
const actual = timeSeriesToTableGraph(influxResponse)
const expected = [[]]
expect(actual.data).toEqual(expected)
@ -535,7 +491,12 @@ describe('transformTableData', () => {
[3000, 2000, 1000],
]
const sort = {field: 'f1', direction: DEFAULT_SORT_DIRECTION}
const tableOptions = {verticalTimeAxis: true}
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 = [
@ -552,6 +513,7 @@ describe('transformTableData', () => {
timeFormat,
decimalPlaces
)
const expected = [
['time', 'f1', 'f2'],
[2000, 1000, 3000],
@ -570,7 +532,12 @@ describe('transformTableData', () => {
[3000, 2000, 1000],
]
const sort = {field: 'time', direction: DEFAULT_SORT_DIRECTION}
const tableOptions = {verticalTimeAxis: true}
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 = [
@ -602,7 +569,12 @@ describe('transformTableData', () => {
]
const sort = {field: 'f1', direction: DEFAULT_SORT_DIRECTION}
const tableOptions = {verticalTimeAxis: true}
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 = [
@ -636,7 +608,12 @@ describe('if verticalTimeAxis is false', () => {
]
const sort = {field: 'time', direction: DEFAULT_SORT_DIRECTION}
const tableOptions = {verticalTimeAxis: false}
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 = [
@ -672,7 +649,12 @@ describe('if verticalTimeAxis is false', () => {
]
const sort = {field: 'f1', direction: DEFAULT_SORT_DIRECTION}
const tableOptions = {verticalTimeAxis: false}
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 = [