parent
3d9b9fe36f
commit
c85a0f8b68
|
@ -38,6 +38,7 @@ const ColorDropdown: SFC<Props> = props => {
|
|||
|
||||
return (
|
||||
<Dropdown
|
||||
customClass="color-dropdown"
|
||||
selectedID={selected.name}
|
||||
onChange={onChoose}
|
||||
status={status}
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
// Libraries
|
||||
import React, {FunctionComponent} from 'react'
|
||||
|
||||
// Components
|
||||
import {
|
||||
Input,
|
||||
Button,
|
||||
ButtonShape,
|
||||
IconFont,
|
||||
ComponentStatus,
|
||||
} from '@influxdata/clockface'
|
||||
import ColorDropdown from 'src/shared/components/ColorDropdown'
|
||||
|
||||
// Constants
|
||||
import {
|
||||
THRESHOLD_COLORS,
|
||||
BASE_THRESHOLD_ID,
|
||||
COLOR_TYPE_MIN,
|
||||
COLOR_TYPE_MAX,
|
||||
} from 'src/shared/constants/thresholds'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
type: string
|
||||
name: string
|
||||
value: string
|
||||
error?: string
|
||||
onChangeValue: (value: string) => void
|
||||
onChangeColor: (name: string, hex: string) => void
|
||||
onRemove: () => void
|
||||
onBlur: () => void
|
||||
}
|
||||
|
||||
const ThresholdSetting: FunctionComponent<Props> = ({
|
||||
id,
|
||||
type,
|
||||
name,
|
||||
value,
|
||||
error,
|
||||
onChangeValue,
|
||||
onChangeColor,
|
||||
onRemove,
|
||||
onBlur,
|
||||
}) => {
|
||||
const isBaseThreshold = id === BASE_THRESHOLD_ID
|
||||
|
||||
let label: string = ''
|
||||
|
||||
if (isBaseThreshold) {
|
||||
label = 'Base'
|
||||
} else if (type === COLOR_TYPE_MIN) {
|
||||
label = 'Minimum'
|
||||
} else if (type === COLOR_TYPE_MAX) {
|
||||
label = 'Maximum'
|
||||
} else {
|
||||
label = 'Value is <='
|
||||
}
|
||||
|
||||
const isRemoveable =
|
||||
!isBaseThreshold && type !== COLOR_TYPE_MIN && type !== COLOR_TYPE_MAX
|
||||
|
||||
const inputStatus = error ? ComponentStatus.Error : ComponentStatus.Default
|
||||
|
||||
return (
|
||||
<div className="threshold-setting" data-test-id={id}>
|
||||
<div className="threshold-setting--controls">
|
||||
<div className="threshold-setting--label">{label}</div>
|
||||
{!isBaseThreshold && (
|
||||
<Input
|
||||
className="threshold-setting--value"
|
||||
value={value}
|
||||
status={inputStatus}
|
||||
onChange={e => onChangeValue(e.target.value)}
|
||||
onBlur={onBlur}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') {
|
||||
onBlur()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ColorDropdown
|
||||
colors={THRESHOLD_COLORS}
|
||||
selected={THRESHOLD_COLORS.find(d => d.name === name)}
|
||||
onChoose={({name, hex}) => onChangeColor(name, hex)}
|
||||
stretchToFit={true}
|
||||
/>
|
||||
{isRemoveable && (
|
||||
<Button
|
||||
className="threshold-setting--remove"
|
||||
icon={IconFont.Remove}
|
||||
shape={ButtonShape.Square}
|
||||
onClick={onRemove}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{error && <div className="threshold-setting--error">{error}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ThresholdSetting
|
|
@ -0,0 +1,50 @@
|
|||
.threshold-setting {
|
||||
margin: $ix-marg-b 0;
|
||||
}
|
||||
|
||||
.threshold-setting--controls {
|
||||
display: flex;
|
||||
justify-content: stretch;
|
||||
|
||||
> * {
|
||||
margin-right: $ix-marg-a;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.threshold-setting--label,
|
||||
.threshold-setting--error {
|
||||
font-weight: 600;
|
||||
font-size: $form-xs-font;
|
||||
}
|
||||
|
||||
.threshold-setting--label {
|
||||
flex: 0 0 70px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: $form-sm-height;
|
||||
justify-content: flex-end;
|
||||
padding: 0 $ix-marg-b;
|
||||
border-radius: $radius;
|
||||
@include no-user-select();
|
||||
background-color: $g4-onyx;
|
||||
color: $g13-mist;
|
||||
}
|
||||
|
||||
.threshold-setting--error {
|
||||
grid-column: 1 / 4;
|
||||
align-self: center;
|
||||
color: $c-curacao;
|
||||
margin: $ix-marg-b 0;
|
||||
}
|
||||
|
||||
.threshold-setting--value {
|
||||
flex: 1 0 110px;
|
||||
}
|
||||
|
||||
.threshold-setting--remove {
|
||||
flex: 0 0 auto;
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
import * as React from 'react'
|
||||
import {useState} from 'react'
|
||||
import {render, fireEvent, wait} from 'react-testing-library'
|
||||
import ThresholdsSettings from 'src/shared/components/ThresholdsSettings'
|
||||
import {BASE_THRESHOLD_ID} from 'src/shared/constants/thresholds'
|
||||
|
||||
describe('ThresholdSettings', () => {
|
||||
const getErrorMessage = (container, thresholdID) => {
|
||||
const node = container.querySelector(
|
||||
`.threshold-setting[data-test-id='${thresholdID}'] .threshold-setting--error`
|
||||
)
|
||||
|
||||
return node ? node.textContent.trim() : null
|
||||
}
|
||||
|
||||
const getInput = (container, thresholdID) =>
|
||||
container.querySelector(
|
||||
`.threshold-setting[data-test-id='${thresholdID}'] input`
|
||||
)
|
||||
|
||||
test('making then correcting an error', () => {
|
||||
const thresholds = [
|
||||
{
|
||||
id: BASE_THRESHOLD_ID,
|
||||
type: 'threshold',
|
||||
name: 'thunder',
|
||||
hex: '',
|
||||
value: null,
|
||||
},
|
||||
{id: '0', type: 'threshold', name: 'fire', hex: '', value: 30},
|
||||
]
|
||||
|
||||
const {container} = render(
|
||||
<ThresholdsSettings thresholds={thresholds} onSetThresholds={jest.fn()} />
|
||||
)
|
||||
|
||||
// Enter an invalid value in the input
|
||||
fireEvent.change(getInput(container, '0'), {
|
||||
target: {value: 'baloney'},
|
||||
})
|
||||
|
||||
// Blur the input
|
||||
fireEvent.blur(getInput(container, '0'))
|
||||
|
||||
// Expect an error message to exist
|
||||
expect(getErrorMessage(container, '0')).toEqual(
|
||||
'Please enter a valid number'
|
||||
)
|
||||
|
||||
// Enter a valid value in the input
|
||||
fireEvent.change(getInput(container, '0'), {
|
||||
target: {value: '9000'},
|
||||
})
|
||||
|
||||
// Blur the input
|
||||
fireEvent.blur(getInput(container, '0'))
|
||||
|
||||
// Expect there to be no error
|
||||
expect(getErrorMessage(container, '0')).toBeNull()
|
||||
})
|
||||
|
||||
test('entering value less than min threshold shows error', () => {
|
||||
const thresholds = [
|
||||
{id: '0', type: 'min', name: 'thunder', hex: '', value: 20},
|
||||
{id: '1', type: 'threshold', name: 'fire', hex: '', value: 30},
|
||||
{id: '2', type: 'max', name: 'ruby', hex: '', value: 60},
|
||||
]
|
||||
|
||||
const {container} = render(
|
||||
<ThresholdsSettings thresholds={thresholds} onSetThresholds={jest.fn()} />
|
||||
)
|
||||
|
||||
// Enter a value in the input
|
||||
fireEvent.change(getInput(container, '1'), {
|
||||
target: {value: '10'},
|
||||
})
|
||||
|
||||
// Blur the input
|
||||
fireEvent.blur(getInput(container, '1'))
|
||||
|
||||
// Expect an error message to exist
|
||||
expect(getErrorMessage(container, '1')).toEqual(
|
||||
'Please enter a value greater than the minimum threshold'
|
||||
)
|
||||
})
|
||||
|
||||
test('entering value greater than max threshold shows error', () => {
|
||||
const thresholds = [
|
||||
{id: '0', type: 'min', name: 'thunder', hex: '', value: 20},
|
||||
{id: '1', type: 'threshold', name: 'fire', hex: '', value: 30},
|
||||
{id: '2', type: 'max', name: 'ruby', hex: '', value: 60},
|
||||
]
|
||||
|
||||
const {container} = render(
|
||||
<ThresholdsSettings thresholds={thresholds} onSetThresholds={jest.fn()} />
|
||||
)
|
||||
|
||||
// Enter a value in the input
|
||||
fireEvent.change(getInput(container, '1'), {
|
||||
target: {value: '80'},
|
||||
})
|
||||
|
||||
// Blur the input
|
||||
fireEvent.blur(getInput(container, '1'))
|
||||
|
||||
// Expect an error message to be called
|
||||
expect(getErrorMessage(container, '1')).toEqual(
|
||||
'Please enter a value less than the maximum threshold'
|
||||
)
|
||||
})
|
||||
|
||||
test('broadcasts edited thresholds only when changes are valid', async () => {
|
||||
const handleSetThresholdsSpy = jest.fn()
|
||||
|
||||
const TestWrapper = () => {
|
||||
const [thresholds, setThresholds] = useState([
|
||||
{id: '0', type: 'min', name: 'thunder', hex: '', value: 20},
|
||||
{id: '1', type: 'threshold', name: 'fire', hex: '', value: 30},
|
||||
{id: '2', type: 'max', name: 'ruby', hex: '', value: 60},
|
||||
])
|
||||
|
||||
const [didRerender, setDidRerender] = useState(false)
|
||||
|
||||
const handleSetThresholds = newThresholds => {
|
||||
setThresholds(newThresholds)
|
||||
setDidRerender(true)
|
||||
handleSetThresholdsSpy(newThresholds)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{didRerender && <div data-testid="did-rerender" />}
|
||||
<ThresholdsSettings
|
||||
thresholds={thresholds}
|
||||
onSetThresholds={handleSetThresholds}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const {container, getByTestId} = render(<TestWrapper />)
|
||||
|
||||
// Enter an invalid value in the input
|
||||
fireEvent.change(getInput(container, '1'), {
|
||||
target: {value: 'baloney'},
|
||||
})
|
||||
|
||||
// Blur the input
|
||||
fireEvent.blur(getInput(container, '1'))
|
||||
|
||||
// Now enter a valid value
|
||||
fireEvent.change(getInput(container, '1'), {
|
||||
target: {value: '40'},
|
||||
})
|
||||
|
||||
// Blur the input again
|
||||
fireEvent.blur(getInput(container, '1'))
|
||||
|
||||
// Wait for the changes to propogate to the test component
|
||||
await wait(() => {
|
||||
getByTestId('did-rerender')
|
||||
})
|
||||
|
||||
// Changed thresholds should only have emitted once
|
||||
expect(handleSetThresholdsSpy).toHaveBeenCalledTimes(1)
|
||||
|
||||
// ...with the expected values
|
||||
expect(handleSetThresholdsSpy.mock.calls[0][0]).toEqual([
|
||||
{id: '0', type: 'min', name: 'thunder', hex: '', value: 20},
|
||||
{id: '1', type: 'threshold', name: 'fire', hex: '', value: 40},
|
||||
{id: '2', type: 'max', name: 'ruby', hex: '', value: 60},
|
||||
])
|
||||
})
|
||||
})
|
|
@ -0,0 +1,189 @@
|
|||
// Libraries
|
||||
import React, {useMemo, useEffect, FunctionComponent} from 'react'
|
||||
import {cloneDeep} from 'lodash'
|
||||
|
||||
// Components
|
||||
import ThresholdSetting from 'src/shared/components/ThresholdSetting'
|
||||
import {Button, ButtonShape, IconFont} from '@influxdata/clockface'
|
||||
|
||||
// Utils
|
||||
import {useOneWayReducer} from 'src/shared/utils/useOneWayReducer'
|
||||
import {
|
||||
sortThresholds,
|
||||
validateThresholds,
|
||||
addThreshold,
|
||||
} from 'src/shared/utils/thresholds'
|
||||
|
||||
// Types
|
||||
import {Color} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
thresholds: Color[]
|
||||
onSetThresholds: (thresholds: Color[]) => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
thresholds: Color[]
|
||||
inputs: {[thresholdID: string]: string}
|
||||
errors: {[thresholdID: string]: string}
|
||||
isValid: boolean
|
||||
isDirty: boolean
|
||||
}
|
||||
|
||||
type Action =
|
||||
| {type: 'COLOR_CHANGED'; id: string; name: string; hex: string}
|
||||
| {type: 'VALUE_CHANGED'; id: string; value: string}
|
||||
| {type: 'VALUE_BLURRED'; id: string}
|
||||
| {type: 'THRESHOLD_REMOVED'; id: string}
|
||||
| {type: 'THRESHOLD_ADDED'}
|
||||
|
||||
const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case 'COLOR_CHANGED': {
|
||||
const {id, name, hex} = action
|
||||
|
||||
const thresholds = state.thresholds.map(threshold =>
|
||||
threshold.id === id ? {...threshold, name, hex} : threshold
|
||||
)
|
||||
|
||||
return {...state, thresholds, isDirty: true}
|
||||
}
|
||||
|
||||
case 'VALUE_CHANGED': {
|
||||
const {id, value} = action
|
||||
|
||||
const inputs = {...state.inputs, [id]: value}
|
||||
|
||||
return {...state, inputs, isDirty: true, isValid: false}
|
||||
}
|
||||
|
||||
case 'VALUE_BLURRED': {
|
||||
const thresholds = state.thresholds.map(threshold =>
|
||||
threshold.id === action.id
|
||||
? {...threshold, value: parseFloat(state.inputs[action.id])}
|
||||
: threshold
|
||||
)
|
||||
|
||||
const errors = validateThresholds(thresholds)
|
||||
|
||||
const isValid = Object.values(errors).length === 0
|
||||
|
||||
return {...state, thresholds, errors, isValid}
|
||||
}
|
||||
|
||||
case 'THRESHOLD_ADDED': {
|
||||
const newThreshold = addThreshold(state.thresholds)
|
||||
|
||||
const thresholds = sortThresholds([...state.thresholds, newThreshold])
|
||||
|
||||
const inputs = {
|
||||
...state.inputs,
|
||||
[newThreshold.id]: String(newThreshold.value),
|
||||
}
|
||||
|
||||
return {...state, thresholds, inputs, isDirty: true}
|
||||
}
|
||||
|
||||
case 'THRESHOLD_REMOVED': {
|
||||
const thresholds = state.thresholds.filter(
|
||||
threshold => threshold.id !== action.id
|
||||
)
|
||||
|
||||
return {...state, thresholds, isDirty: true}
|
||||
}
|
||||
|
||||
default:
|
||||
const unknownAction: never = action
|
||||
const unknownActionType = (unknownAction as any).type
|
||||
|
||||
throw new Error(
|
||||
`unhandled action of type "${unknownActionType}" in ThresholdsSettings`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const ThresholdsSettings: FunctionComponent<Props> = ({
|
||||
thresholds,
|
||||
onSetThresholds,
|
||||
}) => {
|
||||
const initialState: State = useMemo(
|
||||
() => ({
|
||||
thresholds: sortThresholds(
|
||||
cloneDeep(thresholds.filter(({type}) => type !== 'scale'))
|
||||
),
|
||||
inputs: thresholds.reduce(
|
||||
(acc, {id, value}) => ({...acc, [id]: String(value)}),
|
||||
{}
|
||||
),
|
||||
errors: {},
|
||||
isDirty: false,
|
||||
isValid: true,
|
||||
}),
|
||||
[thresholds]
|
||||
)
|
||||
|
||||
const [state, dispatch] = useOneWayReducer(reducer, initialState)
|
||||
|
||||
useEffect(() => {
|
||||
if (state.isDirty && state.isValid) {
|
||||
onSetThresholds(state.thresholds)
|
||||
}
|
||||
}, [state])
|
||||
|
||||
return (
|
||||
<div className="thresholds-settings">
|
||||
<Button
|
||||
className="thresholds-settings--add"
|
||||
shape={ButtonShape.StretchToFit}
|
||||
icon={IconFont.Plus}
|
||||
text="Add a Threshold"
|
||||
onClick={() => dispatch({type: 'THRESHOLD_ADDED'})}
|
||||
/>
|
||||
{state.thresholds.map(threshold => {
|
||||
const onChangeValue = value =>
|
||||
dispatch({
|
||||
type: 'VALUE_CHANGED',
|
||||
id: threshold.id,
|
||||
value,
|
||||
})
|
||||
|
||||
const onChangeColor = (name, hex) =>
|
||||
dispatch({
|
||||
type: 'COLOR_CHANGED',
|
||||
id: threshold.id,
|
||||
name,
|
||||
hex,
|
||||
})
|
||||
|
||||
const onRemove = () =>
|
||||
dispatch({
|
||||
type: 'THRESHOLD_REMOVED',
|
||||
id: threshold.id,
|
||||
})
|
||||
|
||||
const onBlur = () =>
|
||||
dispatch({
|
||||
type: 'VALUE_BLURRED',
|
||||
id: threshold.id,
|
||||
})
|
||||
|
||||
return (
|
||||
<ThresholdSetting
|
||||
key={threshold.id}
|
||||
id={threshold.id}
|
||||
name={threshold.name}
|
||||
type={threshold.type}
|
||||
value={state.inputs[threshold.id]}
|
||||
error={state.errors[threshold.id]}
|
||||
onBlur={onBlur}
|
||||
onRemove={onRemove}
|
||||
onChangeValue={onChangeValue}
|
||||
onChangeColor={onChangeColor}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ThresholdsSettings
|
|
@ -21,7 +21,7 @@ import {ErrorHandling} from 'src/shared/decorators/errors'
|
|||
|
||||
// Types
|
||||
import {ComponentStatus} from '@influxdata/clockface'
|
||||
import {LogsTableColumn} from 'src/types/logs'
|
||||
import {FieldOption} from 'src/types'
|
||||
|
||||
// Constants
|
||||
const columnType = 'column'
|
||||
|
@ -33,7 +33,7 @@ interface Props {
|
|||
index: number
|
||||
id: string
|
||||
key: string
|
||||
onUpdateColumn: (column: LogsTableColumn) => void
|
||||
onUpdateColumn: (column: FieldOption) => void
|
||||
isDragging?: boolean
|
||||
connectDragSource?: ConnectDragSource
|
||||
connectDropTarget?: ConnectDropTarget
|
||||
|
|
|
@ -3,7 +3,7 @@ import chroma from 'chroma-js'
|
|||
|
||||
import {
|
||||
THRESHOLD_COLORS,
|
||||
THRESHOLD_TYPE_BASE,
|
||||
BASE_THRESHOLD_ID,
|
||||
THRESHOLD_TYPE_TEXT,
|
||||
} from 'src/shared/constants/thresholds'
|
||||
|
||||
|
@ -52,7 +52,7 @@ export const generateThresholdsListHexs = ({
|
|||
}
|
||||
|
||||
// baseColor is expected in all cases
|
||||
const baseColor = colors.find(color => color.id === THRESHOLD_TYPE_BASE) || {
|
||||
const baseColor = colors.find(color => color.id === BASE_THRESHOLD_ID) || {
|
||||
hex: defaultColoring.textColor,
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,8 @@ export const COLOR_TYPE_THRESHOLD = 'threshold'
|
|||
|
||||
export const THRESHOLD_TYPE_TEXT = 'text'
|
||||
export const THRESHOLD_TYPE_BG = 'background'
|
||||
export const THRESHOLD_TYPE_BASE = 'base'
|
||||
|
||||
export const BASE_THRESHOLD_ID = 'base'
|
||||
|
||||
export const THRESHOLD_COLORS = [
|
||||
{
|
||||
|
@ -113,7 +114,7 @@ export const DEFAULT_THRESHOLDS_LIST_COLORS = [
|
|||
{
|
||||
type: THRESHOLD_TYPE_TEXT,
|
||||
hex: THRESHOLD_COLORS[11].hex,
|
||||
id: THRESHOLD_TYPE_BASE,
|
||||
id: BASE_THRESHOLD_ID,
|
||||
name: THRESHOLD_COLORS[11].name,
|
||||
value: 0,
|
||||
},
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
// Libraries
|
||||
import uuid from 'uuid'
|
||||
|
||||
// Constants
|
||||
import {
|
||||
THRESHOLD_COLORS,
|
||||
DEFAULT_VALUE_MIN,
|
||||
DEFAULT_VALUE_MAX,
|
||||
COLOR_TYPE_THRESHOLD,
|
||||
COLOR_TYPE_MIN,
|
||||
COLOR_TYPE_MAX,
|
||||
BASE_THRESHOLD_ID,
|
||||
} from 'src/shared/constants/thresholds'
|
||||
|
||||
// Types
|
||||
import {Color} from 'src/types'
|
||||
|
||||
/*
|
||||
Sort a list of thresholds for rendering.
|
||||
|
||||
- Base or minimum thresholds come first
|
||||
- Max thresholds come last
|
||||
- All other thresholds are sorted by value
|
||||
|
||||
*/
|
||||
export const sortThresholds = (thresholds: Color[]): Color[] => {
|
||||
const result = [...thresholds]
|
||||
|
||||
result.sort((a, b) =>
|
||||
a.id === BASE_THRESHOLD_ID ? -Infinity : a.value - b.value
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/*
|
||||
Given a list of thresholds, return an object of error messages for any
|
||||
invalid threshold in the list.
|
||||
|
||||
A threshold is invalid if:
|
||||
|
||||
- Its value is NaN
|
||||
- Its value is less than the min threshold in the list
|
||||
- Its value is more than the max threshold in the list
|
||||
|
||||
*/
|
||||
export const validateThresholds = (
|
||||
thresholds: Color[]
|
||||
): {[thresholdID: string]: string} => {
|
||||
const minThreshold = thresholds.find(({type}) => type === COLOR_TYPE_MIN)
|
||||
const maxThreshold = thresholds.find(({type}) => type === COLOR_TYPE_MAX)
|
||||
const errors = {}
|
||||
|
||||
for (const {id, value, type} of thresholds) {
|
||||
if (isNaN(value)) {
|
||||
errors[id] = 'Please enter a valid number'
|
||||
} else if (
|
||||
minThreshold &&
|
||||
type !== COLOR_TYPE_MIN &&
|
||||
value < minThreshold.value
|
||||
) {
|
||||
errors[id] = 'Please enter a value greater than the minimum threshold'
|
||||
} else if (
|
||||
maxThreshold &&
|
||||
type !== COLOR_TYPE_MAX &&
|
||||
value > maxThreshold.value
|
||||
) {
|
||||
errors[id] = 'Please enter a value less than the maximum threshold'
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
/*
|
||||
Given a list of thresholds, produce a new threshold that is suitable for
|
||||
adding to the list.
|
||||
*/
|
||||
export const addThreshold = (thresholds: Color[]): Color => {
|
||||
const values = thresholds.map(threshold => threshold.value)
|
||||
|
||||
let minValue = Math.min(...values)
|
||||
let maxValue = Math.max(...values)
|
||||
|
||||
if (minValue === Infinity || isNaN(minValue) || minValue === maxValue) {
|
||||
minValue = DEFAULT_VALUE_MIN
|
||||
maxValue = DEFAULT_VALUE_MAX
|
||||
}
|
||||
|
||||
const value = randomTick(minValue, maxValue)
|
||||
|
||||
const colorChoice =
|
||||
THRESHOLD_COLORS[Math.floor(Math.random() * THRESHOLD_COLORS.length)]
|
||||
|
||||
const firstThresholdType = thresholds[0].type
|
||||
|
||||
const thresholdType =
|
||||
firstThresholdType === COLOR_TYPE_MIN ||
|
||||
firstThresholdType === COLOR_TYPE_MAX
|
||||
? COLOR_TYPE_THRESHOLD
|
||||
: firstThresholdType
|
||||
|
||||
const threshold = {
|
||||
...colorChoice,
|
||||
id: uuid.v4(),
|
||||
type: thresholdType,
|
||||
value,
|
||||
}
|
||||
|
||||
return threshold
|
||||
}
|
||||
|
||||
/*
|
||||
Generate a nice random number between `min` and `max` inclusive.
|
||||
*/
|
||||
const randomTick = (min: number, max: number): number => {
|
||||
const domainWidth = max - min
|
||||
|
||||
let roundTo
|
||||
|
||||
if (domainWidth > 1000) {
|
||||
roundTo = 100
|
||||
} else if (domainWidth > 100) {
|
||||
roundTo = 10
|
||||
} else if (domainWidth > 50) {
|
||||
roundTo = 5
|
||||
} else if (domainWidth > 10) {
|
||||
roundTo = 1
|
||||
} else {
|
||||
roundTo = null
|
||||
}
|
||||
|
||||
let value: number
|
||||
|
||||
if (roundTo) {
|
||||
value = Math.round((Math.random() * (max - min)) / roundTo) * roundTo
|
||||
} else {
|
||||
value = Number((Math.random() * (max - min)).toFixed(2))
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
import {
|
||||
useRef,
|
||||
useReducer,
|
||||
useCallback,
|
||||
Reducer,
|
||||
ReducerState,
|
||||
ReducerAction,
|
||||
Dispatch,
|
||||
} from 'react'
|
||||
|
||||
/*
|
||||
Works like `useReducer`, except if the `defaultState` argument changes then
|
||||
the reducer state will reset to `defaultState`.
|
||||
|
||||
It is assumed that the passed `reducer` function has a stable identity over
|
||||
the lifetime of the component.
|
||||
*/
|
||||
export const useOneWayReducer = <R extends Reducer<any, any>>(
|
||||
reducer: R,
|
||||
defaultState: ReducerState<R>
|
||||
): [ReducerState<R>, Dispatch<ReducerAction<R>>] => {
|
||||
// The value of `defaultState` the last time the hook was called
|
||||
const prevDefaultState = useRef(defaultState)
|
||||
|
||||
// Whether or not the next run of the reducer should be against its internal
|
||||
// state, or against the defaultState
|
||||
const reduceDefaultState = useRef(false)
|
||||
|
||||
const wrappedReducer = useCallback(
|
||||
(state: ReducerState<R>, action: ReducerAction<R>) => {
|
||||
if (reduceDefaultState.current) {
|
||||
reduceDefaultState.current = false
|
||||
|
||||
return reducer(prevDefaultState.current, action)
|
||||
}
|
||||
|
||||
return reducer(state, action)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const [reducerState, dispatch] = useReducer(wrappedReducer, defaultState)
|
||||
|
||||
if (defaultState !== prevDefaultState.current) {
|
||||
reduceDefaultState.current = true
|
||||
prevDefaultState.current = defaultState
|
||||
|
||||
return [defaultState, dispatch]
|
||||
}
|
||||
|
||||
return [reducerState, dispatch]
|
||||
}
|
|
@ -95,7 +95,6 @@
|
|||
@import 'src/timeMachine/components/variableToolbar/VariableToolbar.scss';
|
||||
@import 'src/timeMachine/components/fluxFunctionsToolbar/FluxFunctionsToolbar.scss';
|
||||
@import 'src/timeMachine/components/view_options/TimeFormat.scss';
|
||||
@import 'src/timeMachine/components/view_options/ThresholdList.scss';
|
||||
@import 'src/timeMachine/components/view_options/HistogramOptions.scss';
|
||||
@import 'src/timeMachine/components/view_options/ViewOptions.scss';
|
||||
@import 'src/timeMachine/components/view_options/ViewTypeDropdown.scss';
|
||||
|
@ -109,6 +108,8 @@
|
|||
@import 'src/shared/components/dapperScrollbars/DapperScrollbars.scss';
|
||||
@import 'src/templates/components/createFromTemplateOverlay/CreateFromTemplateOverlay.scss';
|
||||
@import 'src/onboarding/components/SigninForm.scss';
|
||||
@import 'src/shared/components/ThresholdsSettings.scss';
|
||||
|
||||
|
||||
// External
|
||||
@import '../../node_modules/@influxdata/react-custom-scrollbars/dist/styles.css';
|
||||
|
|
|
@ -10,7 +10,6 @@ import {MIN_DECIMAL_PLACES, MAX_DECIMAL_PLACES} from 'src/dashboards/constants'
|
|||
|
||||
// Types
|
||||
import {DecimalPlaces} from 'src/types'
|
||||
import {Columns} from '@influxdata/clockface'
|
||||
|
||||
// Decorators
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
@ -27,7 +26,7 @@ class DecimalPlacesOption extends PureComponent<Props> {
|
|||
|
||||
public render() {
|
||||
return (
|
||||
<Grid.Column widthXS={Columns.Six}>
|
||||
<Grid.Column>
|
||||
<Form.Element label="Decimal Places">
|
||||
<AutoInput
|
||||
name="decimal-places"
|
||||
|
|
|
@ -6,7 +6,7 @@ import {connect} from 'react-redux'
|
|||
import {Grid} from '@influxdata/clockface'
|
||||
import Affixes from 'src/timeMachine/components/view_options/Affixes'
|
||||
import DecimalPlacesOption from 'src/timeMachine/components/view_options/DecimalPlaces'
|
||||
import ThresholdList from 'src/timeMachine/components/view_options/ThresholdList'
|
||||
import ThresholdsSettings from 'src/shared/components/ThresholdsSettings'
|
||||
|
||||
// Actions
|
||||
import {
|
||||
|
@ -19,7 +19,7 @@ import {
|
|||
// Types
|
||||
import {ViewType} from 'src/types'
|
||||
import {DecimalPlaces} from 'src/types/dashboards'
|
||||
import {Color, ThresholdConfig} from 'src/types/colors'
|
||||
import {Color} from 'src/types/colors'
|
||||
|
||||
interface OwnProps {
|
||||
type: ViewType
|
||||
|
@ -63,11 +63,12 @@ class GaugeOptions extends PureComponent<Props> {
|
|||
<Grid.Column>
|
||||
<h4 className="view-options--header">Colorized Thresholds</h4>
|
||||
</Grid.Column>
|
||||
<ThresholdList
|
||||
colorConfigs={this.colorConfigs}
|
||||
onUpdateColors={onUpdateColors}
|
||||
onValidateNewColor={this.handleValidateNewColor}
|
||||
/>
|
||||
<Grid.Column>
|
||||
<ThresholdsSettings
|
||||
thresholds={this.props.colors}
|
||||
onSetThresholds={onUpdateColors}
|
||||
/>
|
||||
</Grid.Column>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -87,80 +88,6 @@ class GaugeOptions extends PureComponent<Props> {
|
|||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private get colorConfigs(): ThresholdConfig[] {
|
||||
const {maxColor, minColor} = this.extents
|
||||
const {colors} = this.props
|
||||
|
||||
return colors.map(color => {
|
||||
switch (color.id) {
|
||||
case minColor.id:
|
||||
return {
|
||||
color,
|
||||
isDeletable: false,
|
||||
disableColor: false,
|
||||
label: 'Minimum',
|
||||
}
|
||||
case maxColor.id:
|
||||
return {
|
||||
color,
|
||||
isDeletable: false,
|
||||
disableColor: colors.length > 2,
|
||||
label: 'Maximum',
|
||||
}
|
||||
default:
|
||||
return {color}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private get extents(): {minColor: Color; maxColor: Color} {
|
||||
const first = this.props.colors[0]
|
||||
|
||||
const defaults = {minColor: first, maxColor: first}
|
||||
|
||||
return this.props.colors.reduce((extents, color) => {
|
||||
if (!extents.minColor || extents.minColor.value > color.value) {
|
||||
return {...extents, minColor: color}
|
||||
} else if (!extents.minColor || extents.maxColor.value < color.value) {
|
||||
return {...extents, maxColor: color}
|
||||
}
|
||||
|
||||
return extents
|
||||
}, defaults)
|
||||
}
|
||||
|
||||
private handleValidateNewColor = (sortedColors: Color[], newColor: Color) => {
|
||||
const newColorValue = newColor.value
|
||||
let allowedToUpdate = false
|
||||
|
||||
const minValue = sortedColors[0].value
|
||||
const maxValue = sortedColors[sortedColors.length - 1].value
|
||||
|
||||
if (newColorValue === minValue) {
|
||||
const nextValue = sortedColors[1].value
|
||||
allowedToUpdate = newColorValue < nextValue
|
||||
} else if (newColorValue === maxValue) {
|
||||
const previousValue = sortedColors[sortedColors.length - 2].value
|
||||
allowedToUpdate = previousValue < newColorValue
|
||||
} else {
|
||||
const greaterThanMin = newColorValue > minValue
|
||||
const lessThanMax = newColorValue < maxValue
|
||||
|
||||
const colorsWithoutMinOrMax = sortedColors.slice(
|
||||
1,
|
||||
sortedColors.length - 1
|
||||
)
|
||||
|
||||
const isUnique = !colorsWithoutMinOrMax.some(
|
||||
color => color.value === newColorValue && color.id !== newColor.id
|
||||
)
|
||||
|
||||
allowedToUpdate = greaterThanMin && lessThanMax && isUnique
|
||||
}
|
||||
|
||||
return allowedToUpdate
|
||||
}
|
||||
}
|
||||
|
||||
const mdtp: DispatchProps = {
|
||||
|
|
|
@ -6,8 +6,8 @@ import {connect} from 'react-redux'
|
|||
import {Grid} from '@influxdata/clockface'
|
||||
import Affixes from 'src/timeMachine/components/view_options/Affixes'
|
||||
import DecimalPlacesOption from 'src/timeMachine/components/view_options/DecimalPlaces'
|
||||
import ThresholdList from 'src/timeMachine/components/view_options/ThresholdList'
|
||||
import ThresholdColoring from 'src/timeMachine/components/view_options/ThresholdColoring'
|
||||
import ThresholdsSettings from 'src/shared/components/ThresholdsSettings'
|
||||
|
||||
// Actions
|
||||
import {
|
||||
|
@ -20,14 +20,14 @@ import {
|
|||
// Utils
|
||||
import {getActiveTimeMachine} from 'src/timeMachine/selectors'
|
||||
|
||||
// Constants
|
||||
import {THRESHOLD_TYPE_BASE} from 'src/shared/constants/thresholds'
|
||||
|
||||
// Types
|
||||
import {AppState, NewView} from 'src/types'
|
||||
import {SingleStatView} from 'src/types/dashboards'
|
||||
import {DecimalPlaces} from 'src/types/dashboards'
|
||||
import {Color, ThresholdConfig} from 'src/types/colors'
|
||||
import {
|
||||
AppState,
|
||||
NewView,
|
||||
Color,
|
||||
SingleStatView,
|
||||
DecimalPlaces,
|
||||
} from 'src/types'
|
||||
|
||||
interface StateProps {
|
||||
colors: Color[]
|
||||
|
@ -57,23 +57,6 @@ const SingleStatOptions: SFC<Props> = props => {
|
|||
onSetColors,
|
||||
} = props
|
||||
|
||||
const colorConfigs = colors
|
||||
.filter(c => c.type !== 'scale')
|
||||
.map(color => {
|
||||
const isBase = color.id === THRESHOLD_TYPE_BASE
|
||||
|
||||
const config: ThresholdConfig = {
|
||||
color,
|
||||
isBase,
|
||||
}
|
||||
|
||||
if (isBase) {
|
||||
config.label = 'Base'
|
||||
}
|
||||
|
||||
return config
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid.Column>
|
||||
|
@ -95,11 +78,7 @@ const SingleStatOptions: SFC<Props> = props => {
|
|||
<Grid.Column>
|
||||
<h4 className="view-options--header">Colorized Thresholds</h4>
|
||||
</Grid.Column>
|
||||
<ThresholdList
|
||||
colorConfigs={colorConfigs}
|
||||
onUpdateColors={onSetColors}
|
||||
onValidateNewColor={() => true}
|
||||
/>
|
||||
<ThresholdsSettings thresholds={colors} onSetThresholds={onSetColors} />
|
||||
<Grid.Column>
|
||||
<ThresholdColoring />
|
||||
</Grid.Column>
|
||||
|
|
|
@ -4,15 +4,14 @@ import {connect} from 'react-redux'
|
|||
|
||||
// Components
|
||||
import DecimalPlacesOption from 'src/timeMachine/components/view_options/DecimalPlaces'
|
||||
import ThresholdList from 'src/timeMachine/components/view_options/ThresholdList'
|
||||
import ColumnOptions from 'src/shared/components/columns_options/ColumnsOptions'
|
||||
import FixFirstColumn from 'src/timeMachine/components/view_options/FixFirstColumn'
|
||||
import TimeFormat from 'src/timeMachine/components/view_options/TimeFormat'
|
||||
import SortBy from 'src/timeMachine/components/view_options/SortBy'
|
||||
import {Grid} from '@influxdata/clockface'
|
||||
import ThresholdsSettings from 'src/shared/components/ThresholdsSettings'
|
||||
|
||||
// Constants
|
||||
import {THRESHOLD_TYPE_BASE} from 'src/shared/constants/thresholds'
|
||||
|
||||
// Actions
|
||||
import {
|
||||
|
@ -38,7 +37,6 @@ import {
|
|||
FieldOption,
|
||||
TableOptions as ViewTableOptions,
|
||||
Color,
|
||||
ThresholdConfig,
|
||||
} from 'src/types'
|
||||
import {move} from 'src/shared/utils/move'
|
||||
|
||||
|
@ -68,6 +66,7 @@ export class TableOptions extends Component<Props, {}> {
|
|||
onSetColors,
|
||||
fieldOptions,
|
||||
tableOptions,
|
||||
colors,
|
||||
decimalPlaces,
|
||||
onSetTimeFormat,
|
||||
onSetDecimalPlaces,
|
||||
|
@ -126,11 +125,12 @@ export class TableOptions extends Component<Props, {}> {
|
|||
<Grid.Column>
|
||||
<h4 className="view-options--header">Colorized Thresholds</h4>
|
||||
</Grid.Column>
|
||||
<ThresholdList
|
||||
colorConfigs={this.colorConfigs}
|
||||
onUpdateColors={onSetColors}
|
||||
onValidateNewColor={() => true}
|
||||
/>
|
||||
<Grid.Column>
|
||||
<ThresholdsSettings
|
||||
thresholds={colors}
|
||||
onSetThresholds={onSetColors}
|
||||
/>
|
||||
</Grid.Column>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -159,23 +159,6 @@ export class TableOptions extends Component<Props, {}> {
|
|||
const fixFirstColumn = !tableOptions.fixFirstColumn
|
||||
onSetTableOptions({...tableOptions, fixFirstColumn})
|
||||
}
|
||||
|
||||
private get colorConfigs(): ThresholdConfig[] {
|
||||
return this.props.colors.map(color => {
|
||||
const isBase = color.id === THRESHOLD_TYPE_BASE
|
||||
|
||||
const config: ThresholdConfig = {
|
||||
color,
|
||||
isBase,
|
||||
}
|
||||
|
||||
if (isBase) {
|
||||
config.label = 'Base'
|
||||
}
|
||||
|
||||
return config
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const mstp = (state: AppState) => {
|
||||
|
|
|
@ -1,161 +0,0 @@
|
|||
// Libraries
|
||||
import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react'
|
||||
|
||||
// Components
|
||||
import {Input, SquareButton} from '@influxdata/clockface'
|
||||
|
||||
// Types
|
||||
import {
|
||||
IconFont,
|
||||
InputType,
|
||||
ButtonType,
|
||||
ComponentSize,
|
||||
ComponentStatus,
|
||||
} from '@influxdata/clockface'
|
||||
import ColorDropdown from 'src/shared/components/ColorDropdown'
|
||||
import {THRESHOLD_COLORS} from 'src/shared/constants/thresholds'
|
||||
import {Color, ColorLabel} from 'src/types/colors'
|
||||
import {SeverityColor, SeverityColorOptions} from 'src/types/logs'
|
||||
|
||||
// Decorators
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface Props {
|
||||
threshold: Color
|
||||
label: string
|
||||
isBase: boolean
|
||||
isDeletable: boolean
|
||||
disableColor: boolean
|
||||
onChooseColor: (threshold: Color) => void
|
||||
onValidateColorValue: (threshold: Color, targetValue: number) => boolean
|
||||
onUpdateColorValue: (threshold: Color, targetValue: number) => void
|
||||
onDeleteThreshold: (threshold: Color) => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
workingValue: number | string
|
||||
valid: boolean
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class Threshold extends PureComponent<Props, State> {
|
||||
public static defaultProps = {
|
||||
label: 'Value is <=',
|
||||
disableColor: false,
|
||||
isDeletable: true,
|
||||
isBase: false,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
workingValue: this.props.threshold.value,
|
||||
valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {isDeletable, disableColor, isBase} = this.props
|
||||
const {workingValue} = this.state
|
||||
|
||||
return (
|
||||
<div className="threshold-item">
|
||||
<div className="threshold-item--label">{this.props.label}</div>
|
||||
{!isBase && (
|
||||
<Input
|
||||
value={workingValue.toString()}
|
||||
className="threshold-item--input"
|
||||
type={InputType.Number}
|
||||
onChange={this.handleChangeWorkingValue}
|
||||
onBlur={this.handleBlur}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
status={this.inputStatus}
|
||||
/>
|
||||
)}
|
||||
<ColorDropdown
|
||||
colors={THRESHOLD_COLORS}
|
||||
selected={this.selectedColor}
|
||||
onChoose={this.handleChooseColor}
|
||||
disabled={disableColor}
|
||||
stretchToFit={isBase}
|
||||
widthPixels={this.dropdownWidthPixels}
|
||||
/>
|
||||
{isDeletable && !isBase && (
|
||||
<SquareButton
|
||||
size={ComponentSize.Small}
|
||||
onClick={this.handleDelete}
|
||||
icon={IconFont.Remove}
|
||||
type={ButtonType.Button}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleChooseColor = (color: ColorLabel): void => {
|
||||
const {onChooseColor, threshold} = this.props
|
||||
const {hex, name} = color
|
||||
|
||||
onChooseColor({...threshold, hex, name})
|
||||
}
|
||||
|
||||
private get dropdownWidthPixels(): number {
|
||||
const {isDeletable} = this.props
|
||||
|
||||
return isDeletable ? 124 : 124 + 34
|
||||
}
|
||||
|
||||
private get selectedColor(): SeverityColor {
|
||||
const {
|
||||
threshold: {hex, name},
|
||||
} = this.props
|
||||
|
||||
const colorName = name as SeverityColorOptions
|
||||
|
||||
return {hex, name: colorName}
|
||||
}
|
||||
|
||||
private get inputStatus(): ComponentStatus {
|
||||
const {valid} = this.state
|
||||
|
||||
if (!valid) {
|
||||
return ComponentStatus.Error
|
||||
}
|
||||
|
||||
return ComponentStatus.Valid
|
||||
}
|
||||
|
||||
private handleChangeWorkingValue = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const {threshold, onValidateColorValue} = this.props
|
||||
const targetValue = e.target.value
|
||||
|
||||
const valid = onValidateColorValue(threshold, Number(targetValue))
|
||||
|
||||
this.setState({valid, workingValue: targetValue})
|
||||
}
|
||||
|
||||
private handleBlur = () => {
|
||||
const {valid, workingValue} = this.state
|
||||
const {threshold, onUpdateColorValue} = this.props
|
||||
|
||||
if (valid) {
|
||||
onUpdateColorValue(threshold, Number(workingValue))
|
||||
} else {
|
||||
this.setState({workingValue: threshold.value, valid: true})
|
||||
}
|
||||
}
|
||||
|
||||
private handleKeyUp = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.currentTarget.blur()
|
||||
}
|
||||
}
|
||||
|
||||
private handleDelete = () => {
|
||||
const {threshold, onDeleteThreshold} = this.props
|
||||
onDeleteThreshold(threshold)
|
||||
}
|
||||
}
|
||||
|
||||
export default Threshold
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
Threshold Controls
|
||||
------------------------------------------------------------------------------
|
||||
Used primarily within the View Editor Overlay for Single Stat, Gauge,
|
||||
and Table type cells
|
||||
*/
|
||||
|
||||
.threshold-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.threshold-item {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
height: $form-sm-height;
|
||||
margin-top: $ix-marg-b;
|
||||
|
||||
> * {
|
||||
margin-left: $ix-marg-a;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.threshold-item--label {
|
||||
flex: 0 0 76px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: $form-sm-height;
|
||||
font-weight: 600;
|
||||
font-size: $form-xs-font;
|
||||
padding-left: $ix-marg-b;
|
||||
padding-right: $ix-marg-a;
|
||||
border-radius: $radius;
|
||||
@include no-user-select();
|
||||
background-color: $g4-onyx;
|
||||
color: $g13-mist;
|
||||
}
|
||||
|
||||
.threshold-item--input {
|
||||
flex: 1 1 0;
|
||||
}
|
|
@ -1,160 +0,0 @@
|
|||
// Libraries
|
||||
import React, {PureComponent} from 'react'
|
||||
import _ from 'lodash'
|
||||
import uuid from 'uuid'
|
||||
|
||||
// Components
|
||||
import ThresholdItem from 'src/timeMachine/components/view_options/ThresholdItem'
|
||||
import {Form, Button, Grid} from '@influxdata/clockface'
|
||||
|
||||
// Constants
|
||||
import {
|
||||
COLOR_TYPE_THRESHOLD,
|
||||
THRESHOLD_COLORS,
|
||||
MAX_THRESHOLDS,
|
||||
DEFAULT_VALUE_MAX,
|
||||
} from 'src/shared/constants/thresholds'
|
||||
|
||||
// Types
|
||||
import {
|
||||
IconFont,
|
||||
ButtonType,
|
||||
ComponentSize,
|
||||
ComponentStatus,
|
||||
} from '@influxdata/clockface'
|
||||
import {Color, ThresholdConfig} from 'src/types/colors'
|
||||
|
||||
interface Props {
|
||||
colorConfigs: ThresholdConfig[]
|
||||
onUpdateColors: (colors: Color[]) => void
|
||||
onValidateNewColor: (colors: Color[], newColor: Color) => boolean
|
||||
}
|
||||
|
||||
class ThresholdList extends PureComponent<Props> {
|
||||
public render() {
|
||||
return (
|
||||
<Grid.Column>
|
||||
<Form.Element label="Thresholds">
|
||||
<div className="threshold-list">
|
||||
<Button
|
||||
size={ComponentSize.Small}
|
||||
onClick={this.handleAddThreshold}
|
||||
status={this.disableAddThreshold}
|
||||
icon={IconFont.Plus}
|
||||
type={ButtonType.Button}
|
||||
text="Add a Threshold"
|
||||
/>
|
||||
{this.sortedColorConfigs.map<JSX.Element>(colorConfig => {
|
||||
const {
|
||||
color: threshold,
|
||||
isDeletable,
|
||||
isBase,
|
||||
disableColor,
|
||||
label,
|
||||
} = colorConfig
|
||||
|
||||
return (
|
||||
<ThresholdItem
|
||||
label={label}
|
||||
key={uuid.v4()}
|
||||
threshold={threshold}
|
||||
isBase={isBase}
|
||||
isDeletable={isDeletable}
|
||||
disableColor={disableColor}
|
||||
onChooseColor={this.handleChooseColor}
|
||||
onDeleteThreshold={this.handleDeleteThreshold}
|
||||
onUpdateColorValue={this.handleUpdateColorValue}
|
||||
onValidateColorValue={this.handleValidateColorValue}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Form.Element>
|
||||
</Grid.Column>
|
||||
)
|
||||
}
|
||||
|
||||
private handleAddThreshold = () => {
|
||||
const sortedColors = this.sortedColorConfigs.map(config => config.color)
|
||||
|
||||
if (sortedColors.length <= MAX_THRESHOLDS) {
|
||||
const randomColor = _.random(0, THRESHOLD_COLORS.length - 1)
|
||||
|
||||
const maxColor = sortedColors[sortedColors.length - 1]
|
||||
|
||||
let maxValue = DEFAULT_VALUE_MAX
|
||||
|
||||
if (sortedColors.length > 1) {
|
||||
maxValue = maxColor.value
|
||||
}
|
||||
|
||||
const minValue = sortedColors[0].value
|
||||
|
||||
const randomValue = _.round(_.random(minValue, maxValue, true), 2)
|
||||
|
||||
const color: Color = {
|
||||
type: COLOR_TYPE_THRESHOLD,
|
||||
id: uuid.v4(),
|
||||
value: randomValue,
|
||||
hex: THRESHOLD_COLORS[randomColor].hex,
|
||||
name: THRESHOLD_COLORS[randomColor].name,
|
||||
}
|
||||
|
||||
const updatedColors = _.sortBy<Color>(
|
||||
[...sortedColors, color],
|
||||
color => color.value
|
||||
)
|
||||
|
||||
this.props.onUpdateColors(updatedColors)
|
||||
}
|
||||
}
|
||||
|
||||
private handleChooseColor = (threshold: Color) => {
|
||||
const colors = this.props.colorConfigs.map(({color}) =>
|
||||
color.id === threshold.id
|
||||
? {...color, hex: threshold.hex, name: threshold.name}
|
||||
: color
|
||||
)
|
||||
|
||||
this.props.onUpdateColors(colors)
|
||||
}
|
||||
|
||||
private handleUpdateColorValue = (threshold: Color, value: number) => {
|
||||
const colors = this.props.colorConfigs.map(({color}) =>
|
||||
color.id === threshold.id ? {...color, value} : color
|
||||
)
|
||||
|
||||
this.props.onUpdateColors(colors)
|
||||
}
|
||||
|
||||
private handleDeleteThreshold = (threshold: Color) => {
|
||||
const updatedColors = this.sortedColorConfigs.reduce(
|
||||
(colors, {color}) =>
|
||||
color.id === threshold.id ? colors : [...colors, color],
|
||||
[]
|
||||
)
|
||||
|
||||
this.props.onUpdateColors(updatedColors)
|
||||
}
|
||||
|
||||
private handleValidateColorValue = (newColor: Color) => {
|
||||
const {sortedColorConfigs} = this
|
||||
const sortedColors = sortedColorConfigs.map(config => config.color)
|
||||
|
||||
return this.props.onValidateNewColor(sortedColors, newColor)
|
||||
}
|
||||
|
||||
private get disableAddThreshold(): ComponentStatus {
|
||||
if (this.props.colorConfigs.length > MAX_THRESHOLDS) {
|
||||
return ComponentStatus.Disabled
|
||||
} else {
|
||||
return ComponentStatus.Valid
|
||||
}
|
||||
}
|
||||
|
||||
private get sortedColorConfigs() {
|
||||
return _.sortBy(this.props.colorConfigs, config => config.color.value)
|
||||
}
|
||||
}
|
||||
|
||||
export default ThresholdList
|
|
@ -13,6 +13,10 @@ import {
|
|||
DE_TIME_MACHINE_ID,
|
||||
} from 'src/timeMachine/constants'
|
||||
import {AUTOREFRESH_DEFAULT} from 'src/shared/constants'
|
||||
import {
|
||||
THRESHOLD_TYPE_TEXT,
|
||||
THRESHOLD_TYPE_BG,
|
||||
} from 'src/shared/constants/thresholds'
|
||||
|
||||
// Types
|
||||
import {TimeRange, View, AutoRefresh} from 'src/types'
|
||||
|
@ -451,7 +455,7 @@ export const timeMachineReducer = (
|
|||
if (color.type !== 'scale') {
|
||||
return {
|
||||
...color,
|
||||
type: 'background',
|
||||
type: THRESHOLD_TYPE_BG,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -468,7 +472,7 @@ export const timeMachineReducer = (
|
|||
if (color.type !== 'scale') {
|
||||
return {
|
||||
...color,
|
||||
type: 'text',
|
||||
type: THRESHOLD_TYPE_TEXT,
|
||||
}
|
||||
}
|
||||
return color
|
||||
|
|
|
@ -1,8 +1,47 @@
|
|||
export interface Color {
|
||||
// The following values are used in the `type` field:
|
||||
//
|
||||
// - 'threshold'
|
||||
// - 'max'
|
||||
// - 'min'
|
||||
// - 'text'
|
||||
// - 'background'
|
||||
// - 'scale'
|
||||
//
|
||||
// This field drastically changes how a `Color` is used in the UI.
|
||||
type: string
|
||||
hex: string
|
||||
|
||||
// The `id` field is one of the following:
|
||||
//
|
||||
// - '0'
|
||||
// - '1'
|
||||
// - 'base'
|
||||
// - A client-generated UUID
|
||||
//
|
||||
// When the `id` is 'base', the `Color` is treated specially in certain UI
|
||||
// features. The `id` is unique within the array of colors for a particular
|
||||
// `View`.
|
||||
id: string
|
||||
|
||||
// A hex code for this color
|
||||
hex: string
|
||||
|
||||
// A name for the hex code for this color; used as a stable identifier
|
||||
name: string
|
||||
|
||||
// When a `Color` is being used as a threshold for coloring parts of a
|
||||
// visualization, then the `type` field is one of the following:
|
||||
//
|
||||
// - 'threshold'
|
||||
// - 'max'
|
||||
// - 'min'
|
||||
// - 'text'
|
||||
// - 'background'
|
||||
//
|
||||
// In this case, the `value` is used to determine when the `hex` for this
|
||||
// color is applied to parts of a visualization.
|
||||
//
|
||||
// If the `type` field is 'scale', then this field is unused.
|
||||
value: number
|
||||
}
|
||||
|
||||
|
@ -11,14 +50,6 @@ export interface ColorLabel {
|
|||
name: string
|
||||
}
|
||||
|
||||
export interface ThresholdConfig {
|
||||
color: Color
|
||||
label?: string
|
||||
isDeletable?: boolean
|
||||
isBase?: boolean
|
||||
disableColor?: boolean
|
||||
}
|
||||
|
||||
export enum LabelColorType {
|
||||
Preset = 'preset',
|
||||
Custom = 'custom',
|
||||
|
|
|
@ -1,268 +0,0 @@
|
|||
import {Index} from 'react-virtualized'
|
||||
|
||||
import {QueryConfig} from 'src/types'
|
||||
import {Bucket, Source} from '@influxdata/influx'
|
||||
|
||||
import {FieldOption, TimeSeriesValue} from 'src/types/dashboards'
|
||||
|
||||
export interface LogSearchParams {
|
||||
lower: string
|
||||
upper: string
|
||||
config: QueryConfig
|
||||
filters: Filter[]
|
||||
}
|
||||
|
||||
export interface LogQuery extends LogSearchParams {
|
||||
source: Source
|
||||
}
|
||||
|
||||
export enum SearchStatus {
|
||||
None = 'None',
|
||||
Loading = 'Loading',
|
||||
NoResults = 'NoResults',
|
||||
UpdatingTimeBounds = 'UpdatingTimeBounds',
|
||||
UpdatingFilters = 'UpdatingFilters',
|
||||
UpdatingSource = 'UpdatingSource',
|
||||
UpdatingBucket = 'UpdatingBucket',
|
||||
SourceError = 'SourceError',
|
||||
Loaded = 'Loaded',
|
||||
Clearing = 'Clearing',
|
||||
Cleared = 'Cleared',
|
||||
}
|
||||
|
||||
export interface Filter {
|
||||
id: string
|
||||
key: string
|
||||
value: string
|
||||
operator: string
|
||||
}
|
||||
|
||||
export interface LogsConfigState {
|
||||
currentSource: Source | null
|
||||
currentBuckets: Bucket[]
|
||||
currentBucket: Bucket | null
|
||||
tableQueryConfig: QueryConfig | null
|
||||
filters: Filter[]
|
||||
queryCount: number
|
||||
logConfig: LogConfig
|
||||
searchStatus: SearchStatus
|
||||
}
|
||||
|
||||
export interface LogsTableDataState {
|
||||
currentTailID: number | undefined
|
||||
currentOlderBatchID: string | undefined
|
||||
tableTime: TableTime
|
||||
currentTailUpperBound: number | undefined
|
||||
nextTailLowerBound: number | undefined
|
||||
nextOlderUpperBound: number | undefined
|
||||
nextOlderLowerBound: number | undefined
|
||||
olderChunkDurationMs: number
|
||||
tailChunkDurationMs: number
|
||||
tableQueryConfig: QueryConfig | null
|
||||
tableInfiniteData: {
|
||||
forward: TableData
|
||||
backward: TableData
|
||||
}
|
||||
}
|
||||
|
||||
export type LogsState = LogsConfigState & LogsTableDataState
|
||||
|
||||
// Log Config
|
||||
export interface LogConfig {
|
||||
id?: string
|
||||
link?: string
|
||||
tableColumns: LogsTableColumn[]
|
||||
severityFormat: SeverityFormat
|
||||
severityLevelColors: SeverityLevelColor[]
|
||||
isTruncated: boolean
|
||||
}
|
||||
|
||||
// Severity Colors
|
||||
export interface SeverityLevelColor {
|
||||
level: SeverityLevelOptions
|
||||
color: SeverityColorOptions
|
||||
}
|
||||
|
||||
export interface SeverityColor {
|
||||
hex: string
|
||||
name: SeverityColorOptions
|
||||
}
|
||||
|
||||
export type SeverityFormat = SeverityFormatOptions
|
||||
|
||||
export type LogsTableColumn = FieldOption
|
||||
|
||||
// Log Severity
|
||||
export enum SeverityLevelOptions {
|
||||
Emerg = 'emerg',
|
||||
Alert = 'alert',
|
||||
Crit = 'crit',
|
||||
Err = 'err',
|
||||
Warning = 'warning',
|
||||
Notice = 'notice',
|
||||
Info = 'info',
|
||||
Debug = 'debug',
|
||||
}
|
||||
|
||||
export enum SeverityFormatOptions {
|
||||
Dot = 'dot',
|
||||
DotText = 'dotText',
|
||||
Text = 'text',
|
||||
}
|
||||
|
||||
export enum SeverityColorOptions {
|
||||
Ruby = 'ruby',
|
||||
Fire = 'fire',
|
||||
Curacao = 'curacao',
|
||||
Tiger = 'tiger',
|
||||
Pineapple = 'pineapple',
|
||||
Thunder = 'thunder',
|
||||
Sulfur = 'sulfur',
|
||||
Viridian = 'viridian',
|
||||
Rainforest = 'rainforest',
|
||||
Honeydew = 'honeydew',
|
||||
Ocean = 'ocean',
|
||||
Pool = 'pool',
|
||||
Laser = 'laser',
|
||||
Planet = 'planet',
|
||||
Star = 'star',
|
||||
Comet = 'comet',
|
||||
Graphite = 'graphite',
|
||||
Wolf = 'wolf',
|
||||
Mist = 'mist',
|
||||
Pearl = 'pearl',
|
||||
}
|
||||
|
||||
export const SeverityColorValues = {
|
||||
[SeverityColorOptions.Ruby]: '#BF3D5E',
|
||||
[SeverityColorOptions.Fire]: '#DC4E58',
|
||||
[SeverityColorOptions.Curacao]: '#F95F53',
|
||||
[SeverityColorOptions.Tiger]: '#F48D38',
|
||||
[SeverityColorOptions.Pineapple]: '#FFB94A',
|
||||
[SeverityColorOptions.Thunder]: '#FFD255',
|
||||
[SeverityColorOptions.Sulfur]: '#FFE480',
|
||||
[SeverityColorOptions.Viridian]: '#32B08C',
|
||||
[SeverityColorOptions.Rainforest]: '#4ED8A0',
|
||||
[SeverityColorOptions.Honeydew]: '#7CE490',
|
||||
[SeverityColorOptions.Ocean]: '#4591ED',
|
||||
[SeverityColorOptions.Pool]: '#22ADF6',
|
||||
[SeverityColorOptions.Laser]: '#00C9FF',
|
||||
[SeverityColorOptions.Planet]: '#513CC6',
|
||||
[SeverityColorOptions.Star]: '#7A65F2',
|
||||
[SeverityColorOptions.Comet]: '#9394FF',
|
||||
[SeverityColorOptions.Graphite]: '#545667',
|
||||
[SeverityColorOptions.Wolf]: '#8E91A1',
|
||||
[SeverityColorOptions.Mist]: '#BEC2CC',
|
||||
[SeverityColorOptions.Pearl]: '#E7E8EB',
|
||||
}
|
||||
|
||||
// Log Column Settings
|
||||
export enum ColumnSettingTypes {
|
||||
Visibility = 'visibility',
|
||||
Display = 'displayName',
|
||||
Label = 'label',
|
||||
Color = 'color',
|
||||
}
|
||||
|
||||
export enum ColumnSettingLabelOptions {
|
||||
Text = 'text',
|
||||
Icon = 'icon',
|
||||
}
|
||||
|
||||
export enum ColumnSettingVisibilityOptions {
|
||||
Visible = 'visible',
|
||||
Hidden = 'hidden',
|
||||
}
|
||||
|
||||
// Time
|
||||
export interface TimeWindow {
|
||||
seconds: number
|
||||
windowOption: string
|
||||
}
|
||||
|
||||
export interface TimeRange {
|
||||
upper?: string
|
||||
lower: string
|
||||
seconds?: number
|
||||
windowOption: string
|
||||
timeOption: string
|
||||
}
|
||||
|
||||
// Log Search
|
||||
export interface Term {
|
||||
type: TermType
|
||||
term: string
|
||||
attribute: string
|
||||
}
|
||||
|
||||
export interface TokenLiteralMatch {
|
||||
literal: string
|
||||
nextText: string
|
||||
rule: TermRule
|
||||
attribute: string
|
||||
}
|
||||
|
||||
export interface TermRule {
|
||||
type: TermType
|
||||
pattern: RegExp
|
||||
}
|
||||
|
||||
export enum TermType {
|
||||
Exclude,
|
||||
Include,
|
||||
}
|
||||
|
||||
export enum TermPart {
|
||||
Exclusion = '-',
|
||||
SingleQuoted = "'([^']+)'",
|
||||
DoubleQuoted = '"([^"]+)"',
|
||||
Attribute = '(\\w+(?=\\:))',
|
||||
Colon = '(?::)',
|
||||
UnquotedWord = '([\\S]+)',
|
||||
}
|
||||
|
||||
export enum Operator {
|
||||
NotLike = '!~',
|
||||
Like = '=~',
|
||||
Equal = '==',
|
||||
NotEqual = '!=',
|
||||
}
|
||||
|
||||
export enum MatchType {
|
||||
None = 'no-match',
|
||||
Match = 'match',
|
||||
}
|
||||
|
||||
export interface MatchSection {
|
||||
id: string
|
||||
type: MatchType
|
||||
text: string
|
||||
}
|
||||
|
||||
// Table Data
|
||||
export interface TableData {
|
||||
columns: string[]
|
||||
values: TimeSeriesValue[][]
|
||||
}
|
||||
|
||||
export type RowHeightHandler = (index: Index) => number
|
||||
|
||||
export enum ScrollMode {
|
||||
None = 'None',
|
||||
TailScrolling = 'TailScrolling',
|
||||
TailTop = 'TailTop',
|
||||
TimeSelected = 'TimeSelected',
|
||||
TimeSelectedScrolling = 'TimeSelectedScroll',
|
||||
}
|
||||
|
||||
// Logs State Getter
|
||||
export interface State {
|
||||
logs: LogsState
|
||||
}
|
||||
|
||||
export type GetState = () => State
|
||||
|
||||
export interface TableTime {
|
||||
custom?: string
|
||||
relative?: number
|
||||
}
|
Loading…
Reference in New Issue