From ccf60a3c7a958bbd0c68d9af5069014593276967 Mon Sep 17 00:00:00 2001 From: Deniz Kusefoglu Date: Sun, 5 Jan 2020 16:23:38 +0100 Subject: [PATCH 01/12] Use duration input for check.every field --- .../components/builder/CheckMetaCard.tsx | 13 ++- ui/src/alerting/constants/index.ts | 4 - ui/src/dashboards/utils/time.ts | 4 +- ui/src/shared/components/DurationInput.tsx | 94 +++++++++++++++++++ ui/src/shared/utils/duration.test.tsx | 26 ++++- ui/src/shared/utils/duration.ts | 10 +- ui/src/timeMachine/constants/queryBuilder.ts | 15 +++ 7 files changed, 149 insertions(+), 17 deletions(-) create mode 100644 ui/src/shared/components/DurationInput.tsx diff --git a/ui/src/alerting/components/builder/CheckMetaCard.tsx b/ui/src/alerting/components/builder/CheckMetaCard.tsx index ccb26feb080..743f23d71fb 100644 --- a/ui/src/alerting/components/builder/CheckMetaCard.tsx +++ b/ui/src/alerting/components/builder/CheckMetaCard.tsx @@ -8,6 +8,7 @@ import DashedButton from 'src/shared/components/dashed_button/DashedButton' import CheckTagRow from 'src/alerting/components/builder/CheckTagRow' import DurationSelector from 'src/shared/components/DurationSelector' import BuilderCard from 'src/timeMachine/components/builderCard/BuilderCard' +import DurationInput from 'src/shared/components/DurationInput' // Actions import { @@ -18,7 +19,8 @@ import { } from 'src/alerting/actions/alertBuilder' // Constants -import {CHECK_EVERY_OPTIONS, CHECK_OFFSET_OPTIONS} from 'src/alerting/constants' +import {CHECK_OFFSET_OPTIONS} from 'src/alerting/constants' +import {DURATION_STRINGS} from 'src/timeMachine/constants/queryBuilder' // Types import {AppState, CheckTagSet} from 'src/types' @@ -63,10 +65,11 @@ const CheckMetaCard: FC = ({ - diff --git a/ui/src/alerting/constants/index.ts b/ui/src/alerting/constants/index.ts index e23e793b746..7b3c9f9bf74 100644 --- a/ui/src/alerting/constants/index.ts +++ b/ui/src/alerting/constants/index.ts @@ -1,5 +1,3 @@ -import {DURATIONS} from 'src/timeMachine/constants/queryBuilder' - import {ThresholdCheck, TagRuleDraft} from 'src/types' import {NotificationEndpoint, CheckStatusLevel} from 'src/client' import {ComponentColor, InfluxColors} from '@influxdata/clockface' @@ -17,8 +15,6 @@ export const DEFAULT_DEADMAN_LEVEL: CheckStatusLevel = 'CRIT' export const DEFAULT_STATUS_MESSAGE = 'Check: ${ r._check_name } is: ${ r._level }' -export const CHECK_EVERY_OPTIONS: DurationOption[] = DURATIONS - export const CHECK_OFFSET_OPTIONS: DurationOption[] = [ {duration: '0s', displayText: 'None'}, {duration: '5s', displayText: '5 seconds'}, diff --git a/ui/src/dashboards/utils/time.ts b/ui/src/dashboards/utils/time.ts index aff028a5acc..9bc83c0fcd0 100644 --- a/ui/src/dashboards/utils/time.ts +++ b/ui/src/dashboards/utils/time.ts @@ -2,7 +2,7 @@ import {CustomTimeRange, TimeRange, DurationTimeRange} from 'src/types/queries' import {SELECTABLE_TIME_RANGES} from 'src/shared/constants/timeRanges' import {isDateParseable} from 'src/variables/utils/getTimeRangeVars' -import {isDurationParseable} from 'src/shared/utils/duration' +import {isDurationWithNowParseable} from 'src/shared/utils/duration' interface InputTimeRange { seconds?: number @@ -47,7 +47,7 @@ export const validateAndTypeRange = (timeRange: { } as CustomTimeRange } - if (isDurationParseable(lower)) { + if (isDurationWithNowParseable(lower)) { const selectableTimeRange = SELECTABLE_TIME_RANGES.find( r => r.lower === lower ) diff --git a/ui/src/shared/components/DurationInput.tsx b/ui/src/shared/components/DurationInput.tsx new file mode 100644 index 00000000000..905fddb7300 --- /dev/null +++ b/ui/src/shared/components/DurationInput.tsx @@ -0,0 +1,94 @@ +// Libraries +import React, {useState, FC} from 'react' +import { + Input, + ComponentStatus, + DropdownMenu, + DropdownDivider, + DropdownItem, + ClickOutside, +} from '@influxdata/clockface' +import {isDurationParseable} from 'src/shared/utils/duration' + +const SUGGESTION_CLASS = 'duration-input--suggestion' + +type Props = { + placeholder?: string + exampleSearches: string[] + onSubmit: (input: string) => void + value: string +} + +const DurationInput: FC = ({ + placeholder, + exampleSearches, + onSubmit, + value, +}) => { + const [isFocused, setIsFocused] = useState(false) + + const [input, setInput] = useState(value) + + const inputStatus = isDurationParseable(input) + ? ComponentStatus.Default + : ComponentStatus.Error + + const handleClickSuggestion = (suggestion: string) => { + setInput(suggestion) + + onSubmit(suggestion) + setIsFocused(false) + } + + const handleClickOutside = e => { + const didClickSuggestion = + e.target.classList.contains(SUGGESTION_CLASS) || + e.target.parentNode.classList.contains(SUGGESTION_CLASS) + + if (!didClickSuggestion) { + setIsFocused(false) + } + } + + const onChange = (i: string) => { + setInput(i) + if (isDurationParseable(i)) { + onSubmit(i) + } + } + + return ( +
+ + onChange(e.target.value)} + onFocus={() => setIsFocused(true)} + /> + + {isFocused && ( + + + {exampleSearches.map(s => ( + + {s} + + ))} + + )} +
+ ) +} + +export default DurationInput diff --git a/ui/src/shared/utils/duration.test.tsx b/ui/src/shared/utils/duration.test.tsx index b8470f7d551..e617207dd69 100644 --- a/ui/src/shared/utils/duration.test.tsx +++ b/ui/src/shared/utils/duration.test.tsx @@ -3,6 +3,7 @@ import { durationToMilliseconds, areDurationsEqual, millisecondsToDuration, + isDurationWithNowParseable, isDurationParseable, } from 'src/shared/utils/duration' import {SELECTABLE_TIME_RANGES} from 'src/shared/constants/timeRanges' @@ -69,18 +70,33 @@ describe('millisecondsToDuration', () => { }) }) +describe('isDurationWithNowParseable', () => { + test('returns false when passed invalid durations', () => { + expect(isDurationWithNowParseable('1h')).toBe(false) + expect(isDurationWithNowParseable('moo')).toBe(false) + expect(isDurationWithNowParseable('123')).toBe(false) + expect(isDurationWithNowParseable('now()')).toBe(false) + }) + + test.each(SELECTABLE_TIME_RANGES)( + 'returns true when passed valid duration', + ({lower}) => { + expect(isDurationWithNowParseable(lower)).toEqual(true) + } + ) +}) + describe('isDurationParseable', () => { test('returns false when passed invalid durations', () => { - expect(isDurationParseable('1h')).toBe(false) expect(isDurationParseable('moo')).toBe(false) expect(isDurationParseable('123')).toBe(false) - expect(isDurationParseable('now()')).toBe(false) + expect(isDurationParseable('now()-1h')).toBe(false) }) - test.each(SELECTABLE_TIME_RANGES)( + test.each(TEST_CASES)( 'returns true when passed valid duration', - ({lower}) => { - expect(isDurationParseable(lower)).toEqual(true) + (input, _) => { + expect(isDurationParseable(input)).toEqual(true) } ) }) diff --git a/ui/src/shared/utils/duration.ts b/ui/src/shared/utils/duration.ts index 78d8e04681b..98ad5a4dfd6 100644 --- a/ui/src/shared/utils/duration.ts +++ b/ui/src/shared/utils/duration.ts @@ -7,7 +7,7 @@ import {TIME_RANGE_FORMAT} from 'src/shared/constants/timeRanges' export const removeSpacesAndNow = (input: string): string => input.replace(/\s/g, '').replace(/now\(\)-/, '') -export const isDurationParseable = (lower: string): boolean => { +export const isDurationWithNowParseable = (lower: string): boolean => { const durationRegExp = /([0-9]+)(y|mo|w|d|h|ms|s|m|us|µs|ns)/g if (!lower || !lower.includes('now()')) { return false @@ -18,6 +18,14 @@ export const isDurationParseable = (lower: string): boolean => { return !!removedLower.match(durationRegExp) } +export const isDurationParseable = (duration: string): boolean => { + const durationRegExp = /^(([0-9]+)(y|mo|w|d|h|ms|s|m|us|µs|ns))+$/g + + // warning! Using string.match(regex) here instead of regex.test(string) because regex.test() modifies the regex object, and can lead to unexpected behavior + + return !!duration.match(durationRegExp) +} + export const parseDuration = (input: string): Duration[] => { const result = [] const durationRegExp = /([0-9]+)(y|mo|w|d|h|ms|s|m|us|µs|ns)/g diff --git a/ui/src/timeMachine/constants/queryBuilder.ts b/ui/src/timeMachine/constants/queryBuilder.ts index 4127ecf528a..4ef5028ebcf 100644 --- a/ui/src/timeMachine/constants/queryBuilder.ts +++ b/ui/src/timeMachine/constants/queryBuilder.ts @@ -16,6 +16,21 @@ export const DURATIONS = [ {duration: '30d', displayText: 'Every 30 days'}, ] +export const DURATION_STRINGS = [ + '5s', + '15s', + '1m', + '5m', + '15m', + '1h', + '6h', + '12h', + '24h', + '2d', + '7d', + '30d', +] + export interface QueryFn { name: string flux: (period?: string) => string From 4218743e97c22e13800937ae43fed42eaf7d79fd Mon Sep 17 00:00:00 2001 From: Deniz Kusefoglu Date: Sun, 5 Jan 2020 18:03:33 +0100 Subject: [PATCH 02/12] Convert check.offset to Duration Input --- .../components/builder/CheckMetaCard.tsx | 10 +++---- ui/src/alerting/constants/index.ts | 29 +++++++++---------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/ui/src/alerting/components/builder/CheckMetaCard.tsx b/ui/src/alerting/components/builder/CheckMetaCard.tsx index 743f23d71fb..e9f0d596be3 100644 --- a/ui/src/alerting/components/builder/CheckMetaCard.tsx +++ b/ui/src/alerting/components/builder/CheckMetaCard.tsx @@ -6,7 +6,6 @@ import {connect} from 'react-redux' import {Form, ComponentSize, ComponentColor, Grid} from '@influxdata/clockface' import DashedButton from 'src/shared/components/dashed_button/DashedButton' import CheckTagRow from 'src/alerting/components/builder/CheckTagRow' -import DurationSelector from 'src/shared/components/DurationSelector' import BuilderCard from 'src/timeMachine/components/builderCard/BuilderCard' import DurationInput from 'src/shared/components/DurationInput' @@ -75,10 +74,11 @@ const CheckMetaCard: FC = ({ - diff --git a/ui/src/alerting/constants/index.ts b/ui/src/alerting/constants/index.ts index 7b3c9f9bf74..f0c4b875202 100644 --- a/ui/src/alerting/constants/index.ts +++ b/ui/src/alerting/constants/index.ts @@ -1,7 +1,6 @@ import {ThresholdCheck, TagRuleDraft} from 'src/types' import {NotificationEndpoint, CheckStatusLevel} from 'src/client' import {ComponentColor, InfluxColors} from '@influxdata/clockface' -import {DurationOption} from 'src/shared/components/DurationSelector' export const DEFAULT_CHECK_NAME = 'Name this Check' export const DEFAULT_NOTIFICATION_RULE_NAME = 'Name this Notification Rule' @@ -15,20 +14,20 @@ export const DEFAULT_DEADMAN_LEVEL: CheckStatusLevel = 'CRIT' export const DEFAULT_STATUS_MESSAGE = 'Check: ${ r._check_name } is: ${ r._level }' -export const CHECK_OFFSET_OPTIONS: DurationOption[] = [ - {duration: '0s', displayText: 'None'}, - {duration: '5s', displayText: '5 seconds'}, - {duration: '15s', displayText: '15 seconds'}, - {duration: '1m', displayText: '1 minute'}, - {duration: '5m', displayText: '5 minutes'}, - {duration: '15m', displayText: '15 minutes'}, - {duration: '1h', displayText: '1 hour'}, - {duration: '6h', displayText: '6 hours'}, - {duration: '12h', displayText: '12 hours'}, - {duration: '24h', displayText: '24 hours'}, - {duration: '2d', displayText: '2 days'}, - {duration: '7d', displayText: '7 days'}, - {duration: '30d', displayText: '30 days'}, +export const CHECK_OFFSET_OPTIONS = [ + '0s', + '5s', + '15s', + '1m', + '5m', + '15m', + '1h', + '6h', + '12h', + '24h', + '2d', + '7d', + '30d', ] export const MONITORING_BUCKET = '_monitoring' From 426609c8abc547dd67d5d35743a426f5ad5008fc Mon Sep 17 00:00:00 2001 From: Deniz Kusefoglu Date: Mon, 6 Jan 2020 01:59:53 +0300 Subject: [PATCH 03/12] Check is string for duration --- ui/src/shared/utils/duration.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui/src/shared/utils/duration.ts b/ui/src/shared/utils/duration.ts index 98ad5a4dfd6..7d2b4baf5d6 100644 --- a/ui/src/shared/utils/duration.ts +++ b/ui/src/shared/utils/duration.ts @@ -19,6 +19,10 @@ export const isDurationWithNowParseable = (lower: string): boolean => { } export const isDurationParseable = (duration: string): boolean => { + if (typeof duration !== 'string') { + return false + } + const durationRegExp = /^(([0-9]+)(y|mo|w|d|h|ms|s|m|us|µs|ns))+$/g // warning! Using string.match(regex) here instead of regex.test(string) because regex.test() modifies the regex object, and can lead to unexpected behavior From 51a3afa3c1e59067a5e01d108d5fffde02c5742d Mon Sep 17 00:00:00 2001 From: Deniz Kusefoglu Date: Wed, 8 Jan 2020 23:45:14 +0300 Subject: [PATCH 04/12] feat(duration-inputs): Add submitInvalid ld to duration input --- .../components/builder/CheckMetaCard.tsx | 6 ++--- ui/src/shared/components/DurationInput.tsx | 24 ++++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/ui/src/alerting/components/builder/CheckMetaCard.tsx b/ui/src/alerting/components/builder/CheckMetaCard.tsx index e9f0d596be3..af6e54a7f55 100644 --- a/ui/src/alerting/components/builder/CheckMetaCard.tsx +++ b/ui/src/alerting/components/builder/CheckMetaCard.tsx @@ -66,9 +66,8 @@ const CheckMetaCard: FC = ({ @@ -76,9 +75,8 @@ const CheckMetaCard: FC = ({ diff --git a/ui/src/shared/components/DurationInput.tsx b/ui/src/shared/components/DurationInput.tsx index 905fddb7300..ea710dafec5 100644 --- a/ui/src/shared/components/DurationInput.tsx +++ b/ui/src/shared/components/DurationInput.tsx @@ -13,28 +13,30 @@ import {isDurationParseable} from 'src/shared/utils/duration' const SUGGESTION_CLASS = 'duration-input--suggestion' type Props = { - placeholder?: string - exampleSearches: string[] + suggestions: string[] onSubmit: (input: string) => void value: string + placeholder?: string + submitInvalid?: boolean } const DurationInput: FC = ({ - placeholder, - exampleSearches, + suggestions, onSubmit, value, + placeholder, + submitInvalid = true, }) => { const [isFocused, setIsFocused] = useState(false) - const [input, setInput] = useState(value) + const [inputValue, setInputValue] = useState(value) - const inputStatus = isDurationParseable(input) + const inputStatus = isDurationParseable(inputValue) ? ComponentStatus.Default : ComponentStatus.Error const handleClickSuggestion = (suggestion: string) => { - setInput(suggestion) + setInputValue(suggestion) onSubmit(suggestion) setIsFocused(false) @@ -51,8 +53,8 @@ const DurationInput: FC = ({ } const onChange = (i: string) => { - setInput(i) - if (isDurationParseable(i)) { + setInputValue(i) + if (submitInvalid || (!submitInvalid && isDurationParseable(i))) { onSubmit(i) } } @@ -62,7 +64,7 @@ const DurationInput: FC = ({ onChange(e.target.value)} onFocus={() => setIsFocused(true)} @@ -75,7 +77,7 @@ const DurationInput: FC = ({ noScrollY={true} > - {exampleSearches.map(s => ( + {suggestions.map(s => ( Date: Fri, 10 Jan 2020 15:16:17 +0000 Subject: [PATCH 05/12] feat(duration-inputs) Add duration inputs to Deadman Conditions --- .../components/builder/DeadmanConditions.tsx | 26 +++++++------------ ui/src/shared/components/DurationInput.tsx | 4 ++- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/ui/src/alerting/components/builder/DeadmanConditions.tsx b/ui/src/alerting/components/builder/DeadmanConditions.tsx index be896e4fc3c..2205fbcf33d 100644 --- a/ui/src/alerting/components/builder/DeadmanConditions.tsx +++ b/ui/src/alerting/components/builder/DeadmanConditions.tsx @@ -9,8 +9,6 @@ import { ComponentSize, PanelBody, TextBlock, - InputType, - Input, FlexDirection, InfluxColors, } from '@influxdata/clockface' @@ -25,6 +23,8 @@ import { // Types import {CheckStatusLevel, AppState} from 'src/types' +import DurationInput from 'src/shared/components/DurationInput' +import {CHECK_OFFSET_OPTIONS} from 'src/alerting/constants' interface DispatchProps { onSetStaleTime: typeof setStaleTime @@ -77,14 +77,11 @@ const DeadmanConditions: FC = ({ > - ) => - onSetTimeSince(e.target.value) - } - name="timeSince" - testID="input-field" - type={InputType.Text} + @@ -106,14 +103,11 @@ const DeadmanConditions: FC = ({ text="And stop checking after" /> - ) => - onSetStaleTime(e.target.value) - } - name="staleTime" - testID="input-field" - type={InputType.Text} + diff --git a/ui/src/shared/components/DurationInput.tsx b/ui/src/shared/components/DurationInput.tsx index ea710dafec5..7ba6935c865 100644 --- a/ui/src/shared/components/DurationInput.tsx +++ b/ui/src/shared/components/DurationInput.tsx @@ -18,6 +18,7 @@ type Props = { value: string placeholder?: string submitInvalid?: boolean + showDivider?: boolean } const DurationInput: FC = ({ @@ -26,6 +27,7 @@ const DurationInput: FC = ({ value, placeholder, submitInvalid = true, + showDivider = true, }) => { const [isFocused, setIsFocused] = useState(false) @@ -76,7 +78,7 @@ const DurationInput: FC = ({ noScrollX={true} noScrollY={true} > - + {showDivider && } {suggestions.map(s => ( Date: Fri, 10 Jan 2020 15:20:56 +0000 Subject: [PATCH 06/12] feat(duration-inputs) Add test-id to duration inputs --- ui/src/shared/components/DurationInput.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui/src/shared/components/DurationInput.tsx b/ui/src/shared/components/DurationInput.tsx index 7ba6935c865..c80fc90c878 100644 --- a/ui/src/shared/components/DurationInput.tsx +++ b/ui/src/shared/components/DurationInput.tsx @@ -19,6 +19,7 @@ type Props = { placeholder?: string submitInvalid?: boolean showDivider?: boolean + testID: string } const DurationInput: FC = ({ @@ -28,6 +29,7 @@ const DurationInput: FC = ({ placeholder, submitInvalid = true, showDivider = true, + testID = 'duration-input', }) => { const [isFocused, setIsFocused] = useState(false) @@ -70,6 +72,7 @@ const DurationInput: FC = ({ status={inputStatus} onChange={e => onChange(e.target.value)} onFocus={() => setIsFocused(true)} + testID={testID} /> {isFocused && ( From d5dc552e41dbe98d1d9f83f3c6cce6439452ae40 Mon Sep 17 00:00:00 2001 From: Deniz Kusefoglu Date: Fri, 10 Jan 2020 15:21:22 +0000 Subject: [PATCH 07/12] feat(duration-inputs) Add offset defaults to notification rules --- ui/src/alerting/components/notifications/utils/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/alerting/components/notifications/utils/index.ts b/ui/src/alerting/components/notifications/utils/index.ts index ffa60ed2027..e2db16bf01a 100644 --- a/ui/src/alerting/components/notifications/utils/index.ts +++ b/ui/src/alerting/components/notifications/utils/index.ts @@ -81,6 +81,7 @@ export const changeStatusRule = ( export const initRuleDraft = (orgID: string): NotificationRuleDraft => ({ type: 'http', every: '10m', + offset: '0s', url: '', orgID, name: '', From 29a2806fb2125ed183f5c057d9fa7ea859da42c6 Mon Sep 17 00:00:00 2001 From: Deniz Kusefoglu Date: Sat, 11 Jan 2020 16:43:26 +0000 Subject: [PATCH 08/12] feat(duration-inputs): Add duration inputs to notification rules --- .../alerting/actions/notifications/rules.ts | 6 ++++ .../notifications/EditRuleOverlay.tsx | 4 +-- .../notifications/RuleOverlayContents.tsx | 11 ++++++- .../components/notifications/RuleSchedule.tsx | 30 ++++++++++--------- .../components/notifications/utils/index.ts | 1 + 5 files changed, 35 insertions(+), 17 deletions(-) diff --git a/ui/src/alerting/actions/notifications/rules.ts b/ui/src/alerting/actions/notifications/rules.ts index c227f748f2b..1ce7087d605 100644 --- a/ui/src/alerting/actions/notifications/rules.ts +++ b/ui/src/alerting/actions/notifications/rules.ts @@ -139,6 +139,12 @@ export const createRule = (rule: NotificationRuleDraft) => async ( export const updateRule = (rule: NotificationRuleDraft) => async ( dispatch: Dispatch ) => { + if (rule.offset == '') { + throw new Error('Notification Rule offset field can not be empty') + } + if (rule.every == '') { + throw new Error('Notification Rule every field can not be empty') + } const resp = await api.putNotificationRule({ ruleID: rule.id, data: draftRuleToPostRule(rule), diff --git a/ui/src/alerting/components/notifications/EditRuleOverlay.tsx b/ui/src/alerting/components/notifications/EditRuleOverlay.tsx index bfd2f2c0824..bb4dbcafc35 100644 --- a/ui/src/alerting/components/notifications/EditRuleOverlay.tsx +++ b/ui/src/alerting/components/notifications/EditRuleOverlay.tsx @@ -26,7 +26,7 @@ interface StateProps { interface DispatchProps { onNotify: typeof notify - onUpdateRule: (rule: NotificationRuleDraft) => Promise + onUpdateRule: typeof updateRule } type Props = WithRouterProps & StateProps & DispatchProps @@ -84,7 +84,7 @@ const mstp = ({rules}: AppState, {params}: Props): StateProps => { const mdtp = { onNotify: notify, - onUpdateRule: updateRule as any, + onUpdateRule: updateRule, } export default connect( diff --git a/ui/src/alerting/components/notifications/RuleOverlayContents.tsx b/ui/src/alerting/components/notifications/RuleOverlayContents.tsx index 09a60179dc7..a0fd008466d 100644 --- a/ui/src/alerting/components/notifications/RuleOverlayContents.tsx +++ b/ui/src/alerting/components/notifications/RuleOverlayContents.tsx @@ -40,6 +40,15 @@ const RuleOverlayContents: FC = ({saveButtonText, onSave}) => { }) } + const handleChangeParameter = (key: keyof NotificationRuleDraft) => ( + value: string + ) => { + dispatch({ + type: 'UPDATE_RULE', + rule: {...rule, [key]: value} as NotificationRuleDraft, + }) + } + return (
@@ -57,7 +66,7 @@ const RuleOverlayContents: FC = ({saveButtonText, onSave}) => { onChange={handleChange} /> - + diff --git a/ui/src/alerting/components/notifications/RuleSchedule.tsx b/ui/src/alerting/components/notifications/RuleSchedule.tsx index 351db13b972..c84c298d9ff 100644 --- a/ui/src/alerting/components/notifications/RuleSchedule.tsx +++ b/ui/src/alerting/components/notifications/RuleSchedule.tsx @@ -1,15 +1,19 @@ // Libraries -import React, {FC, ChangeEvent} from 'react' +import React, {FC} from 'react' // Components -import {Form, Input, InputType, Grid, Columns} from '@influxdata/clockface' +import {Form, Grid, Columns} from '@influxdata/clockface' +import DurationInput from 'src/shared/components/DurationInput' // Types import {RuleState} from './RuleOverlay.reducer' +import {DURATION_STRINGS} from 'src/timeMachine/constants/queryBuilder' +import {NotificationRuleDraft} from 'src/types' +import {CHECK_OFFSET_OPTIONS} from 'src/alerting/constants' interface Props { rule: RuleState - onChange: (e: ChangeEvent) => void + onChange: (key: keyof NotificationRuleDraft) => (value: string) => void } const RuleSchedule: FC = ({rule, onChange}) => { @@ -19,12 +23,11 @@ const RuleSchedule: FC = ({rule, onChange}) => { - @@ -32,12 +35,11 @@ const RuleSchedule: FC = ({rule, onChange}) => { - diff --git a/ui/src/alerting/components/notifications/utils/index.ts b/ui/src/alerting/components/notifications/utils/index.ts index e2db16bf01a..f43f9083bf4 100644 --- a/ui/src/alerting/components/notifications/utils/index.ts +++ b/ui/src/alerting/components/notifications/utils/index.ts @@ -131,6 +131,7 @@ export const ruleToDraftRule = ( const tagRules = rule.tagRules || [] return { ...rule, + offset: rule.offset || '', statusRules: statusRules.map(value => ({cid: uuid.v4(), value})), tagRules: tagRules.map(value => ({cid: uuid.v4(), value})), } From a580c9ccc72c6485568032580b8997364dff93d6 Mon Sep 17 00:00:00 2001 From: Deniz Kusefoglu Date: Sat, 11 Jan 2020 17:52:37 +0000 Subject: [PATCH 09/12] feat(duration-inputs): Check validity of check durations and thresholds --- ui/src/alerting/actions/checks.ts | 19 +++++++++++++++---- ui/src/alerting/utils/checkValidate.ts | 16 ++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 ui/src/alerting/utils/checkValidate.ts diff --git a/ui/src/alerting/actions/checks.ts b/ui/src/alerting/actions/checks.ts index 6d7d0c1ee53..5c142f335ed 100644 --- a/ui/src/alerting/actions/checks.ts +++ b/ui/src/alerting/actions/checks.ts @@ -13,6 +13,10 @@ import * as api from 'src/client' import {getActiveTimeMachine} from 'src/timeMachine/selectors' import {incrementCloneName} from 'src/utils/naming' import {reportError} from 'src/shared/utils/errors' +import {isDurationParseable} from 'src/shared/utils/duration' +import {checkThresholdsValid} from '../utils/checkValidate' +import {createView} from 'src/shared/utils/view' +import {getOrg} from 'src/organizations/selectors' // Actions import { @@ -44,10 +48,6 @@ import { DeadmanCheck, } from 'src/types' -// Utils -import {createView} from 'src/shared/utils/view' -import {getOrg} from 'src/organizations/selectors' - export type Action = | ReturnType | ReturnType @@ -187,6 +187,7 @@ export const saveCheckFromTimeMachine = () => async ( tags, thresholds, } as ThresholdCheck + checkThresholdsValid(thresholds) } else if (check.type === 'deadman') { check = { ...check, @@ -199,6 +200,16 @@ export const saveCheckFromTimeMachine = () => async ( tags, timeSince, } as DeadmanCheck + if (!isDurationParseable(timeSince) || !isDurationParseable(staleTime)) { + throw new Error('Duration fields must contain valid duration') + } + } + + if (!isDurationParseable(offset)) { + throw new Error('Check offset must be a valid duration') + } + if (!isDurationParseable(every)) { + throw new Error('Check every must be a valid duration') } const resp = check.id diff --git a/ui/src/alerting/utils/checkValidate.ts b/ui/src/alerting/utils/checkValidate.ts new file mode 100644 index 00000000000..445e5050b3e --- /dev/null +++ b/ui/src/alerting/utils/checkValidate.ts @@ -0,0 +1,16 @@ +import {Threshold} from 'src/types' +import {isNaN} from 'lodash' + +export const checkThresholdsValid = (thresholds: Threshold[]) => { + thresholds.forEach(t => { + if (t.type === 'greater' && isNaN(t.value)) { + throw new Error('Threshold must have defined value') + } + if (t.type === 'lesser' && isNaN(t.value)) { + throw new Error('Threshold must have defined value') + } + if (t.type === 'range' && (isNaN(t.min) || isNaN(t.min))) { + throw new Error('Threshold must have defined values') + } + }) +} From ea5c85210180a5c9fa564b71de807f7b3e85bb80 Mon Sep 17 00:00:00 2001 From: Deniz Kusefoglu Date: Mon, 13 Jan 2020 10:27:08 -0500 Subject: [PATCH 10/12] feat(duration-inputs): Add vertical scroll bar to duration input --- ui/src/shared/components/DurationInput.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/src/shared/components/DurationInput.tsx b/ui/src/shared/components/DurationInput.tsx index c80fc90c878..2d654689b35 100644 --- a/ui/src/shared/components/DurationInput.tsx +++ b/ui/src/shared/components/DurationInput.tsx @@ -79,7 +79,6 @@ const DurationInput: FC = ({ {showDivider && } {suggestions.map(s => ( From 98a019114c99d19ea4f356f972b82a1f193d7763 Mon Sep 17 00:00:00 2001 From: Deniz Kusefoglu Date: Mon, 13 Jan 2020 10:44:46 -0500 Subject: [PATCH 11/12] feat(duration-inputs): Make testID an optional parameter --- ui/src/shared/components/DurationInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/shared/components/DurationInput.tsx b/ui/src/shared/components/DurationInput.tsx index 2d654689b35..46e1db4a4da 100644 --- a/ui/src/shared/components/DurationInput.tsx +++ b/ui/src/shared/components/DurationInput.tsx @@ -19,7 +19,7 @@ type Props = { placeholder?: string submitInvalid?: boolean showDivider?: boolean - testID: string + testID?: string } const DurationInput: FC = ({ From a95ca316134f8534416073386718ed608801bb76 Mon Sep 17 00:00:00 2001 From: Deniz Kusefoglu Date: Mon, 13 Jan 2020 15:53:40 -0500 Subject: [PATCH 12/12] feat(duration-inputs) : use window isNan instead of lodash --- ui/src/alerting/utils/checkValidate.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ui/src/alerting/utils/checkValidate.ts b/ui/src/alerting/utils/checkValidate.ts index 445e5050b3e..485aae034f1 100644 --- a/ui/src/alerting/utils/checkValidate.ts +++ b/ui/src/alerting/utils/checkValidate.ts @@ -1,5 +1,4 @@ import {Threshold} from 'src/types' -import {isNaN} from 'lodash' export const checkThresholdsValid = (thresholds: Threshold[]) => { thresholds.forEach(t => { @@ -10,7 +9,7 @@ export const checkThresholdsValid = (thresholds: Threshold[]) => { throw new Error('Threshold must have defined value') } if (t.type === 'range' && (isNaN(t.min) || isNaN(t.min))) { - throw new Error('Threshold must have defined values') + throw new Error('Threshold must have defined min and max values') } }) }