fix(ui): constrain query builder in check overlay

Closes #14785
pull/14906/head
Christopher Henn 2019-08-28 17:04:48 -07:00
parent 9d6d1d1418
commit 626286863a
28 changed files with 432 additions and 226 deletions

View File

@ -19,8 +19,10 @@ import {
import {
Action as TimeMachineAction,
setActiveTimeMachine,
updateTimeMachineCheck,
setCheckStatus,
} from 'src/timeMachine/actions'
import {setCheckStatus} from 'src/timeMachine/actions'
import {executeQueries} from 'src/timeMachine/actions/queries'
// Types
import {
@ -246,3 +248,8 @@ export const cloneCheck = (check: Check) => async (
dispatch(notify(copy.createCheckFailed(error.message)))
}
}
export const selectCheckEvery = (every: string) => async dispatch => {
dispatch(updateTimeMachineCheck({every}))
dispatch(executeQueries())
}

View File

@ -36,12 +36,9 @@ const CheckEOHeader: FC<Props> = ({name, onSetName, onCancel, onSave}) => {
return
}
try {
setSaveStatus(RemoteDataState.Loading)
await onSave()
} catch {
setSaveStatus(RemoteDataState.NotStarted)
}
setSaveStatus(RemoteDataState.Loading)
await onSave()
setSaveStatus(RemoteDataState.NotStarted)
}
const handleClickOutsideTitle = (e: MouseEvent<HTMLElement>) => {
@ -82,7 +79,7 @@ const CheckEOHeader: FC<Props> = ({name, onSetName, onCancel, onSave}) => {
color={ComponentColor.Success}
size={ComponentSize.Small}
status={saveButtonStatus}
onClick={onSave}
onClick={handleSave}
testID="save-cell--button"
/>
</Page.Header.Right>

View File

@ -31,4 +31,8 @@
.alert-builder--check-type-selector {
margin: $ix-marg-c;
}
}
.alert-builder--tag-row {
margin-bottom: $ix-marg-b;
}

View File

@ -30,7 +30,7 @@ const AlertBuilder: FC = () => {
className="alert-builder--card"
>
<BuilderCard.Header title="Conditions" />
<BuilderCard.Body addPadding={true}>
<BuilderCard.Body addPadding={true} autoHideScrollbars={true}>
<CheckConditionsCard />
</BuilderCard.Body>
</BuilderCard>
@ -40,7 +40,7 @@ const AlertBuilder: FC = () => {
className="alert-builder--card"
>
<BuilderCard.Header title="Matching Notification Rules" />
<BuilderCard.Body addPadding={true}>
<BuilderCard.Body addPadding={true} autoHideScrollbars={true}>
<CheckMatchingRulesCard />
</BuilderCard.Body>
</BuilderCard>

View File

@ -11,21 +11,26 @@ import {
Wrap,
ComponentColor,
Grid,
InfluxColors,
} from '@influxdata/clockface'
import {Input} from '@influxdata/clockface'
import DashedButton from 'src/shared/components/dashed_button/DashedButton'
import CheckTagRow from 'src/alerting/components/builder/CheckTagRow'
import DurationSelector from 'src/timeMachine/components/DurationSelector'
// Actions & Selectors
import {updateTimeMachineCheck} from 'src/timeMachine/actions'
import {getActiveTimeMachine} from 'src/timeMachine/selectors'
import {selectCheckEvery} from 'src/alerting/actions/checks'
// Constants
import {CHECK_EVERY_OPTIONS, CHECK_OFFSET_OPTIONS} from 'src/alerting/constants'
// Types
import {Check, AppState, CheckTagSet} from 'src/types'
interface DispatchProps {
updateTimeMachineCheck: typeof updateTimeMachineCheck
onUpdateTimeMachineCheck: typeof updateTimeMachineCheck
onSelectCheckEvery: typeof selectCheckEvery
}
interface StateProps {
@ -34,28 +39,32 @@ interface StateProps {
type Props = DispatchProps & StateProps
const CheckMetaCard: FC<Props> = ({updateTimeMachineCheck, check}) => {
const CheckMetaCard: FC<Props> = ({
check,
onUpdateTimeMachineCheck,
onSelectCheckEvery,
}) => {
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
updateTimeMachineCheck({[e.target.name]: e.target.value})
onUpdateTimeMachineCheck({[e.target.name]: e.target.value})
}
const addTagsRow = () => {
const tags = check.tags || []
updateTimeMachineCheck({tags: [...tags, {key: '', value: ''}]})
onUpdateTimeMachineCheck({tags: [...tags, {key: '', value: ''}]})
}
const handleChangeTagRow = (index: number, tagSet: CheckTagSet) => {
const tags = [...check.tags]
tags[index] = tagSet
updateTimeMachineCheck({tags})
onUpdateTimeMachineCheck({tags})
}
const handleRemoveTagRow = (index: number) => {
let tags = [...check.tags]
tags = tags.filter((_, i) => i !== index)
updateTimeMachineCheck({tags})
onUpdateTimeMachineCheck({tags})
}
return (
@ -94,29 +103,26 @@ const CheckMetaCard: FC<Props> = ({updateTimeMachineCheck, check}) => {
<Grid>
<Grid.Row>
<Grid.Column widthSM={6}>
<Form.Element label="Schedule every">
<Input
name="every"
onChange={handleChange}
titleText="Check run interval"
value={check.every}
<Form.Element label="Schedule Every">
<DurationSelector
selectedDuration={check.every}
durations={CHECK_EVERY_OPTIONS}
onSelectDuration={onSelectCheckEvery}
/>
</Form.Element>
</Grid.Column>
<Grid.Column widthSM={6}>
<Form.Element label="Offset">
<Input
name="offset"
onChange={handleChange}
titleText="Check offset interval"
value={check.offset}
<DurationSelector
selectedDuration={check.offset}
durations={CHECK_OFFSET_OPTIONS}
onSelectDuration={offset => onUpdateTimeMachineCheck({offset})}
/>
</Form.Element>
</Grid.Column>
</Grid.Row>
</Grid>
<Form.Label label="Tags :" />
<Form.Divider lineColor={InfluxColors.Smoke} />
<Form.Label label="Tags" />
{check.tags &&
check.tags.map((t, i) => (
<CheckTagRow
@ -128,7 +134,7 @@ const CheckMetaCard: FC<Props> = ({updateTimeMachineCheck, check}) => {
/>
))}
<DashedButton
text="+ Tags"
text="+ Tag"
onClick={addTagsRow}
color={ComponentColor.Primary}
size={ComponentSize.Small}
@ -146,7 +152,8 @@ const mstp = (state: AppState): StateProps => {
}
const mdtp: DispatchProps = {
updateTimeMachineCheck: updateTimeMachineCheck,
onUpdateTimeMachineCheck: updateTimeMachineCheck,
onSelectCheckEvery: selectCheckEvery,
}
export default connect<StateProps, DispatchProps, {}>(

View File

@ -38,7 +38,11 @@ const CheckTagRow: FC<Props> = ({
}
return (
<Panel testID="tag-rule" size={ComponentSize.ExtraSmall}>
<Panel
testID="tag-rule"
size={ComponentSize.ExtraSmall}
className="alert-builder--tag-row"
>
<DismissButton onClick={handleRemove} color={ComponentColor.Default} />
<Panel.Body>
<FlexBox direction={FlexDirection.Row} margin={ComponentSize.Small}>

View File

@ -1,3 +1,5 @@
import {DURATIONS} from 'src/timeMachine/constants/queryBuilder'
import {
Check,
DashboardQuery,
@ -12,12 +14,29 @@ export const DEFAULT_CHECK_NAME = 'Name this check'
export const DEFAULT_NOTIFICATION_RULE_NAME = 'Name this notification rule'
export const CHECK_NAME_MAX_LENGTH = 68
export const DEFAULT_CHECK_CRON = '1h'
export const DEFAULT_CHECK_EVERY = '1h'
export const DEFAULT_CHECK_EVERY = '5m'
export const DEFAULT_CHECK_OFFSET = '0s'
export const DEFAULT_CHECK_REPORT_ZERO = false
export const DEFAULT_DEADMAN_LEVEL: CheckStatusLevel = 'CRIT'
export const CHECK_EVERY_OPTIONS = DURATIONS
export const CHECK_OFFSET_OPTIONS = [
{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 LEVEL_COLORS = {
OK: '#32B08C',
INFO: '#4591ED',

View File

@ -0,0 +1,11 @@
import {getCheckVisTimeRange} from 'src/alerting/utils/vis'
const TESTS = [
['5s', {lower: 'now() - 1500s'}],
['1m', {lower: 'now() - 300m'}],
['1m5s', {lower: 'now() - 300m1500s'}],
]
test.each(TESTS)('getCheckVisTimeRange(%s)', (input, expected) => {
expect(getCheckVisTimeRange(input)).toEqual(expected)
})

View File

@ -0,0 +1,26 @@
// Utils
import {parseDuration} from 'src/variables/utils/parseDuration'
// Types
import {TimeRange} from 'src/types'
const POINTS_PER_CHECK_PLOT = 300
/*
Given the duration in a check's `every` field, return a `TimeRange` suitable
for visualizing the input data to the check.
For example, suppose a check has an `every` value of `1m`. Then the check is
run once per minute, and the input data to the `check` for each run is a
single value aggregated from the last `1m`. To display a visualization of the
check data over time, we want to show a series of the values aggregated for
each minute. So to display a plot with say, 300 points, we need to query a
time range of the last 300 minutes.
*/
export const getCheckVisTimeRange = (durationStr: string): TimeRange => {
const durationMultiple = parseDuration(durationStr)
.map(({magnitude, unit}) => `${magnitude * POINTS_PER_CHECK_PLOT}${unit}`)
.join('')
return {lower: `now() - ${durationMultiple}`}
}

View File

@ -66,7 +66,7 @@ const CheckPlot: FunctionComponent<Props> = ({
}
const [yDomain, onSetYDomain, onResetYDomain] = useVisDomainSettings(
[0, 100],
null,
table.getColumn(Y_COLUMN, 'number')
)
@ -95,12 +95,14 @@ const CheckPlot: FunctionComponent<Props> = ({
table
)
const yTicks = flatMap(thresholds, (t: any) => [
const thresholdValues = flatMap(thresholds, (t: any) => [
t.value,
t.minValue,
t.maxValue,
]).filter(t => t !== undefined)
const yTicks = thresholdValues.length ? thresholdValues : null
const config: Config = {
...VIS_THEME,
table,

View File

@ -6,6 +6,7 @@ import {
DEFAULT_GAUGE_COLORS,
DEFAULT_THRESHOLDS_LIST_COLORS,
} from 'src/shared/constants/thresholds'
import {DEFAULT_CHECK_EVERY} from 'src/alerting/constants'
// Types
import {
@ -251,7 +252,19 @@ const NEW_VIEW_CREATORS = {
type: 'check',
shape: 'chronograf-v2',
checkID: '',
queries: [defaultViewQuery()],
queries: [
{
name: '',
text: '',
editMode: 'builder',
builderConfig: {
buckets: [],
tags: [{key: '_measurement', values: []}],
functions: [{name: 'mean'}],
aggregateWindow: {period: DEFAULT_CHECK_EVERY},
},
},
],
colors: NINETEEN_EIGHTY_FOUR,
},
}),

View File

@ -100,7 +100,6 @@
@import 'src/timeMachine/components/view_options/ViewOptions.scss';
@import 'src/timeMachine/components/view_options/ViewTypeDropdown.scss';
@import 'src/timeMachine/components/builderCard/BuilderCard.scss';
@import 'src/timeMachine/components/WindowSelector.scss';
@import 'src/dataLoaders/components/side_bar/SideBar.scss';
@import 'src/dataLoaders/components/DataLoadersOverlay.scss';
@import 'src/shared/components/EmptyGraphError.scss';

View File

@ -13,7 +13,7 @@ import {notify} from 'src/shared/actions/notifications'
import {rateLimitReached, resultTooLarge} from 'src/shared/copy/notifications'
// Utils
import {getActiveTimeMachine} from 'src/timeMachine/selectors'
import {getActiveTimeMachine, getTimeRange} from 'src/timeMachine/selectors'
import {getVariableAssignments} from 'src/variables/selectors'
import {getTimeRangeVars} from 'src/variables/utils/getTimeRangeVars'
import {filterUnusedVars} from 'src/shared/utils/filterUnusedVars'
@ -88,7 +88,8 @@ export const refreshTimeMachineVariableValues = () => async (
let pendingResults: Array<CancelBox<RunQueryResult>> = []
export const executeQueries = () => async (dispatch, getState: GetState) => {
const {view, timeRange} = getActiveTimeMachine(getState())
const {view} = getActiveTimeMachine(getState())
const timeRange = getTimeRange(getState())
const queries = view.properties.queries.filter(({text}) => !!text.trim())
if (!queries.length) {

View File

@ -2,7 +2,11 @@
import {queryBuilderFetcher} from 'src/timeMachine/apis/QueryBuilderFetcher'
// Utils
import {getActiveQuery, getActiveTimeMachine} from 'src/timeMachine/selectors'
import {
getActiveQuery,
getActiveTimeMachine,
getTimeRange,
} from 'src/timeMachine/selectors'
// Types
import {Dispatch} from 'redux-thunk'
@ -282,7 +286,7 @@ export const loadTagSelector = (index: number) => async (
dispatch(setBuilderTagKeysStatus(index, RemoteDataState.Loading))
try {
const timeRange = getActiveTimeMachine(getState()).timeRange
const timeRange = getTimeRange(getState())
const searchTerm = getActiveTimeMachine(getState()).queryBuilder.tags[index]
.keysSearchTerm
@ -338,7 +342,7 @@ const loadTagSelectorValues = (index: number) => async (
dispatch(setBuilderTagValuesStatus(index, RemoteDataState.Loading))
try {
const timeRange = getActiveTimeMachine(getState()).timeRange
const timeRange = getTimeRange(getState())
const key = getActiveQuery(getState()).builderConfig.tags[index].key
const searchTerm = getActiveTimeMachine(getState()).queryBuilder.tags[index]
.valuesSearchTerm

View File

@ -0,0 +1,69 @@
// Libraries
import React, {FunctionComponent} from 'react'
// Components
import {Dropdown, ComponentStatus} from '@influxdata/clockface'
interface Props {
selectedDuration: string
onSelectDuration: (duration: string) => any
durations: Array<{duration: string; displayText: string}>
disabled?: boolean
}
const DurationSelector: FunctionComponent<Props> = ({
selectedDuration,
onSelectDuration,
durations,
disabled = false,
}) => {
let resolvedDurations = durations
let selected = durations.find(d => d.duration === selectedDuration)
if (!selected) {
selected = {duration: selectedDuration, displayText: selectedDuration}
resolvedDurations = [selected, ...resolvedDurations]
}
return (
<Dropdown
testID="duration-selector"
button={(active, onClick) => (
<Dropdown.Button
testID="duration-selector--button"
active={active}
onClick={onClick}
status={getStatus(disabled)}
>
{selected.displayText}
</Dropdown.Button>
)}
menu={onCollapse => (
<Dropdown.Menu onCollapse={onCollapse} testID="duration-selector--menu">
{resolvedDurations.map(({duration, displayText}) => (
<Dropdown.Item
id={duration}
key={duration}
value={duration}
testID={`duration-selector--${duration}`}
selected={duration === selectedDuration}
onClick={onSelectDuration}
>
{displayText}
</Dropdown.Item>
))}
</Dropdown.Menu>
)}
/>
)
}
const getStatus = (disabled: boolean): ComponentStatus => {
if (disabled) {
return ComponentStatus.Disabled
}
return ComponentStatus.Default
}
export default DurationSelector

View File

@ -6,7 +6,7 @@ import {connect} from 'react-redux'
import {Input} from '@influxdata/clockface'
import SelectorList from 'src/timeMachine/components/SelectorList'
import BuilderCard from 'src/timeMachine/components/builderCard/BuilderCard'
import WindowSelector from 'src/timeMachine/components/WindowSelector'
import DurationSelector from 'src/timeMachine/components/DurationSelector'
// Actions
import {
@ -15,12 +15,14 @@ import {
} from 'src/timeMachine/actions/queryBuilder'
// Utils
import {getActiveQuery} from 'src/timeMachine/selectors'
import {getActiveQuery, getIsInCheckOverlay} from 'src/timeMachine/selectors'
// Constants
import {
FUNCTIONS,
AGG_WINDOW_AUTO,
DURATIONS,
AUTO_NONE_DURATIONS,
} from 'src/timeMachine/constants/queryBuilder'
// Types
@ -31,6 +33,7 @@ const FUNCTION_NAMES = FUNCTIONS.map(f => f.name)
interface StateProps {
aggregateWindow: BuilderConfig['aggregateWindow']
selectedFunctions: BuilderConfig['functions']
isInCheckOverlay: boolean
}
interface DispatchProps {
@ -49,9 +52,9 @@ class FunctionSelector extends PureComponent<Props, State> {
public render() {
const {
onSelectFunction,
selectedFunctions,
onSelectAggregateWindow,
isInCheckOverlay,
} = this.props
const {searchTerm} = this.state
@ -60,9 +63,10 @@ class FunctionSelector extends PureComponent<Props, State> {
<BuilderCard className="function-selector" testID="function-selector">
<BuilderCard.Header title="Aggregate Functions" />
<BuilderCard.Menu>
<WindowSelector
onSelect={onSelectAggregateWindow}
period={this.period}
<DurationSelector
onSelectDuration={onSelectAggregateWindow}
selectedDuration={this.duration}
durations={this.durations}
disabled={!selectedFunctions.length}
/>
<Input
@ -75,18 +79,25 @@ class FunctionSelector extends PureComponent<Props, State> {
<SelectorList
items={this.functions}
selectedItems={this.selectedFunctions}
onSelectItem={onSelectFunction}
multiSelect={true}
onSelectItem={this.handleSelectFunction}
multiSelect={!isInCheckOverlay}
/>
</BuilderCard>
)
}
private get period(): string {
private get duration(): string {
const {aggregateWindow} = this.props
return aggregateWindow.period || AGG_WINDOW_AUTO
}
private get durations(): Array<{duration: string; displayText: string}> {
return this.props.isInCheckOverlay
? DURATIONS
: [...DURATIONS, ...AUTO_NONE_DURATIONS]
}
private get functions(): string[] {
return FUNCTION_NAMES.filter(f => f.includes(this.state.searchTerm))
}
@ -98,6 +109,17 @@ class FunctionSelector extends PureComponent<Props, State> {
private handleSetSearchTerm = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({searchTerm: e.target.value})
}
private handleSelectFunction = (functionName: string) => {
const {isInCheckOverlay, selectedFunctions, onSelectFunction} = this.props
if (isInCheckOverlay && selectedFunctions[0].name === functionName) {
// Disallow empty aggreegate selections in check overlay
return
}
onSelectFunction(functionName)
}
}
const mstp = (state: AppState) => {
@ -105,7 +127,9 @@ const mstp = (state: AppState) => {
state
).builderConfig
return {selectedFunctions, aggregateWindow}
const isInCheckOverlay = getIsInCheckOverlay(state)
return {selectedFunctions, aggregateWindow, isInCheckOverlay}
}
const mdtp = {

View File

@ -15,6 +15,14 @@
background: $g2-kevlar;
padding: 8px 10px 0 10px;
width: 100%;
.view-raw-data-toggle {
display: flex;
align-items: center;
justify-content: space-between;
width: 115px;
margin-right: $ix-marg-b;
}
}
.time-machine-queries--tabs {

View File

@ -10,26 +10,27 @@ import TimeMachineRefreshDropdown from 'src/timeMachine/components/RefreshDropdo
import TimeRangeDropdown, {
RangeType,
} from 'src/shared/components/TimeRangeDropdown'
import TimeMachineQueryTab from 'src/timeMachine/components/QueryTab'
import TimeMachineQueryBuilder from 'src/timeMachine/components/QueryBuilder'
import SubmitQueryButton from 'src/timeMachine/components/SubmitQueryButton'
import RawDataToggle from 'src/timeMachine/components/RawDataToggle'
import QueryTabs from 'src/timeMachine/components/QueryTabs'
import {
SquareButton,
IconFont,
ComponentSize,
ComponentColor,
FlexBox,
FlexDirection,
JustifyContent,
} from '@influxdata/clockface'
// Actions
import {addQuery, setAutoRefresh} from 'src/timeMachine/actions'
import {setAutoRefresh} from 'src/timeMachine/actions'
import {setTimeRange} from 'src/timeMachine/actions'
// Utils
import {getActiveTimeMachine, getActiveQuery} from 'src/timeMachine/selectors'
import {
getActiveTimeMachine,
getIsInCheckOverlay,
getActiveQuery,
} from 'src/timeMachine/selectors'
// Types
import {
@ -39,18 +40,15 @@ import {
AutoRefresh,
AutoRefreshStatus,
} from 'src/types'
import {DashboardDraftQuery} from 'src/types/dashboards'
interface StateProps {
activeQuery: DashboardQuery
draftQueries: DashboardDraftQuery[]
timeRange: TimeRange
autoRefresh: AutoRefresh
activeTimeMachineID: string
isInCheckOverlay: boolean
}
interface DispatchProps {
onAddQuery: typeof addQuery
onSetTimeRange: typeof setTimeRange
onSetAutoRefresh: typeof setAutoRefresh
}
@ -59,34 +57,12 @@ type Props = StateProps & DispatchProps
class TimeMachineQueries extends PureComponent<Props> {
public render() {
const {
draftQueries,
onAddQuery,
timeRange,
activeTimeMachineID,
} = this.props
const {timeRange, isInCheckOverlay} = this.props
return (
<div className="time-machine-queries">
<div className="time-machine-queries--controls">
<div className="time-machine-queries--tabs">
{draftQueries.map((query, queryIndex) => (
<TimeMachineQueryTab
key={queryIndex}
queryIndex={queryIndex}
query={query}
/>
))}
{activeTimeMachineID !== 'alerting' && (
<SquareButton
className="time-machine-queries--new"
icon={IconFont.PlusSkinny}
size={ComponentSize.ExtraSmall}
color={ComponentColor.Default}
onClick={onAddQuery}
/>
)}
</div>
<QueryTabs />
<div className="time-machine-queries--buttons">
<FlexBox
direction={FlexDirection.Row}
@ -94,15 +70,17 @@ class TimeMachineQueries extends PureComponent<Props> {
margin={ComponentSize.Small}
>
<RawDataToggle />
{activeTimeMachineID !== 'alerting' && <CSVExportButton />}
<TimeMachineRefreshDropdown />
<TimeRangeDropdown
timeRange={timeRange}
onSetTimeRange={this.handleSetTimeRange}
centerPicker={true}
/>
{activeTimeMachineID !== 'alerting' && (
<TimeMachineQueriesSwitcher />
{!isInCheckOverlay && (
<>
<CSVExportButton />
<TimeMachineRefreshDropdown />
<TimeRangeDropdown
timeRange={timeRange}
onSetTimeRange={this.handleSetTimeRange}
centerPicker={true}
/>
<TimeMachineQueriesSwitcher />
</>
)}
<SubmitQueryButton />
</FlexBox>
@ -150,25 +128,19 @@ class TimeMachineQueries extends PureComponent<Props> {
}
const mstp = (state: AppState) => {
const {
timeMachines: {activeTimeMachineID},
} = state
const {draftQueries, timeRange, autoRefresh} = getActiveTimeMachine(state)
const {timeRange, autoRefresh} = getActiveTimeMachine(state)
const activeQuery = getActiveQuery(state)
return {
timeRange,
activeQuery,
draftQueries,
autoRefresh,
activeTimeMachineID,
isInCheckOverlay: getIsInCheckOverlay(state),
}
}
const mdtp = {
onAddQuery: addQuery,
onSetTimeRange: setTimeRange,
onSetAutoRefresh: setAutoRefresh,
}

View File

@ -2,7 +2,6 @@ import React from 'react'
import {renderWithRedux} from 'src/mockState'
import {waitForElement, fireEvent} from 'react-testing-library'
import {windows} from 'src/timeMachine/components/WindowSelector'
import QueryBuilder from 'src/timeMachine/components/QueryBuilder'
@ -80,20 +79,20 @@ describe('QueryBuilder', () => {
const mean = getByTestId('selector-list mean')
fireEvent.click(mean)
let windowSelectorButton = getByTestId('window-selector--button')
let windowSelectorButton = getByTestId('duration-selector--button')
fireEvent.click(windowSelectorButton)
const windowSelector = getByTestId('window-selector--menu')
const windowSelector = getByTestId('duration-selector--menu')
expect(windowSelector.childElementCount).toBe(windows.length)
expect(windowSelector.childElementCount).toBe(14)
const fiveMins = getByTestId('window-selector--5m')
const fiveMins = getByTestId('duration-selector--5m')
fireEvent.click(fiveMins)
windowSelectorButton = getByTestId('window-selector--button')
windowSelectorButton = getByTestId('duration-selector--button')
expect(windowSelectorButton.textContent).toContain('5m')
expect(windowSelectorButton.textContent).toContain('5 minutes')
})
})

View File

@ -0,0 +1,74 @@
// Libraries
import React, {FC} from 'react'
import {connect} from 'react-redux'
// Components
import TimeMachineQueryTab from 'src/timeMachine/components/QueryTab'
import {
SquareButton,
IconFont,
ComponentSize,
ComponentColor,
} from '@influxdata/clockface'
// Actions
import {addQuery} from 'src/timeMachine/actions'
// Utils
import {
getActiveTimeMachine,
getIsInCheckOverlay,
} from 'src/timeMachine/selectors'
// Types
import {AppState, DashboardDraftQuery} from 'src/types'
interface StateProps {
draftQueries: DashboardDraftQuery[]
isInCheckOverlay: boolean
}
interface DispatchProps {
onAddQuery: () => any
}
type Props = StateProps & DispatchProps
const QueryTabs: FC<Props> = ({draftQueries, isInCheckOverlay, onAddQuery}) => {
return (
<div className="time-machine-queries--tabs">
{draftQueries.map((query, queryIndex) => (
<TimeMachineQueryTab
key={queryIndex}
queryIndex={queryIndex}
query={query}
/>
))}
{!isInCheckOverlay && (
<SquareButton
className="time-machine-queries--new"
icon={IconFont.PlusSkinny}
size={ComponentSize.ExtraSmall}
color={ComponentColor.Default}
onClick={onAddQuery}
/>
)}
</div>
)
}
const mstp = (state: AppState): StateProps => {
const {draftQueries} = getActiveTimeMachine(state)
const isInCheckOverlay = getIsInCheckOverlay(state)
return {draftQueries, isInCheckOverlay}
}
const mdtp = {
onAddQuery: addQuery,
}
export default connect<StateProps, DispatchProps>(
mstp,
mdtp
)(QueryTabs)

View File

@ -29,14 +29,14 @@ class TimeMachineQueries extends PureComponent<Props> {
const {isViewingRawData} = this.props
return (
<>
<div className="view-raw-data-toggle">
<SlideToggle.Label text="View Raw Data" />
<SlideToggle
active={isViewingRawData}
onChange={this.handleToggleIsViewingRawData}
size={ComponentSize.ExtraSmall}
/>
</>
</div>
)
}

View File

@ -16,7 +16,7 @@ const SelectorList: SFC<Props> = props => {
const {items, selectedItems, onSelectItem, multiSelect} = props
return (
<BuilderCard.Body addPadding={false}>
<BuilderCard.Body addPadding={false} autoHideScrollbars={true}>
{items.map(item => {
const className = classnames('selector-list--item', {
selected: selectedItems.includes(item),

View File

@ -32,7 +32,11 @@ import {
// Utils
import {toComponentStatus} from 'src/shared/utils/toComponentStatus'
import DefaultDebouncer from 'src/shared/utils/debouncer'
import {getActiveQuery, getActiveTimeMachine} from 'src/timeMachine/selectors'
import {
getActiveQuery,
getActiveTimeMachine,
getIsInCheckOverlay,
} from 'src/timeMachine/selectors'
// Types
import {AppState, RemoteDataState} from 'src/types'
@ -49,6 +53,7 @@ interface StateProps {
selectedValues: string[]
valuesSearchTerm: string
keysSearchTerm: string
isInCheckOverlay: boolean
}
interface DispatchProps {
@ -179,7 +184,7 @@ class TagSelector extends PureComponent<Props> {
items={values}
selectedItems={selectedValues}
onSelectItem={this.handleSelectValue}
multiSelect={true}
multiSelect={!this.props.isInCheckOverlay}
/>
)
}
@ -268,6 +273,8 @@ const mstp = (state: AppState, ownProps: OwnProps): StateProps => {
emptyText = `Select a ${tags[ownProps.index - 1].key} value first`
}
const isInCheckOverlay = getIsInCheckOverlay(state)
return {
emptyText,
keys,
@ -278,6 +285,7 @@ const mstp = (state: AppState, ownProps: OwnProps): StateProps => {
selectedValues,
valuesSearchTerm,
keysSearchTerm,
isInCheckOverlay,
}
}

View File

@ -1,8 +0,0 @@
.window-selector--label {
display: none;
margin-right: $ix-marg-a;
}
.cf-dropdown--button .window-selector--label {
display: inline-block;
}

View File

@ -1,93 +0,0 @@
// Libraries
import React, {FunctionComponent} from 'react'
// Components
import {Dropdown, ComponentStatus} from '@influxdata/clockface'
// Constants
import {
AGG_WINDOW_AUTO,
AGG_WINDOW_NONE,
} from 'src/timeMachine/constants/queryBuilder'
interface Window {
period: string
}
export const windows: Window[] = [
{period: AGG_WINDOW_NONE},
{period: AGG_WINDOW_AUTO},
{period: '5m'},
{period: '15m'},
{period: '1h'},
{period: '6h'},
{period: '12h'},
{period: '24h'},
{period: '2d'},
{period: '7d'},
{period: '30d'},
]
interface Props {
onSelect: (period: string) => void
period: string
disabled: boolean
}
const WindowSelector: FunctionComponent<Props> = ({
onSelect,
period,
disabled,
}) => {
return (
<Dropdown
testID="window-selector"
button={(active, onClick) => (
<Dropdown.Button
testID="window-selector--button"
active={active}
onClick={onClick}
status={getStatus(disabled)}
>
{showPrefix(period) && (
<span className="window-selector--label">Every</span>
)}
{period}
</Dropdown.Button>
)}
menu={onCollapse => (
<Dropdown.Menu onCollapse={onCollapse} testID="window-selector--menu">
{windows.map(window => (
<Dropdown.Item
id={window.period}
key={window.period}
value={window.period}
testID={`window-selector--${window.period}`}
selected={window.period === period}
onClick={onSelect}
>
{showPrefix(window.period) && (
<span className="window-selector--label">Every</span>
)}
{window.period}
</Dropdown.Item>
))}
</Dropdown.Menu>
)}
/>
)
}
const showPrefix = (id: string): boolean => {
return id !== AGG_WINDOW_AUTO && id !== AGG_WINDOW_NONE
}
const getStatus = (disabled: boolean): ComponentStatus => {
if (disabled) {
return ComponentStatus.Disabled
}
return ComponentStatus.Default
}
export default WindowSelector

View File

@ -1,6 +1,26 @@
export const AGG_WINDOW_AUTO = 'auto'
export const AGG_WINDOW_NONE = 'none'
export const DURATIONS = [
{duration: '5s', displayText: 'Every 5 seconds'},
{duration: '15s', displayText: 'Every 15 seconds'},
{duration: '1m', displayText: 'Every minute'},
{duration: '5m', displayText: 'Every 5 minutes'},
{duration: '15m', displayText: 'Every 15 minutes'},
{duration: '1h', displayText: 'Every hour'},
{duration: '6h', displayText: 'Every 6 hours'},
{duration: '12h', displayText: 'Every 12 hours'},
{duration: '24h', displayText: 'Every 24 hours'},
{duration: '2d', displayText: 'Every 2 days'},
{duration: '7d', displayText: 'Every 7 days'},
{duration: '30d', displayText: 'Every 30 days'},
]
export const AUTO_NONE_DURATIONS = [
{duration: AGG_WINDOW_AUTO, displayText: 'Auto'},
{duration: AGG_WINDOW_NONE, displayText: 'None'},
]
export interface QueryFn {
name: string
flux: (period?: string) => string

View File

@ -146,6 +146,8 @@ export const timeMachinesReducer = (
}))
const queryBuilder = initialQueryBuilderState(draftQueries[0].builderConfig)
const queryResults = initialQueryResultsState()
const timeRange =
activeTimeMachineID === 'alerting' ? null : activeTimeMachine.timeRange
return {
...state,
@ -155,6 +157,7 @@ export const timeMachinesReducer = (
[activeTimeMachineID]: {
...activeTimeMachine,
activeTab: 'queries',
timeRange,
...initialState,
isViewingRawData: false,
activeQueryIndex: 0,
@ -604,7 +607,6 @@ export const timeMachineReducer = (
if (action.payload.resetSelections) {
builderConfig.tags = [{key: '', values: []}]
builderConfig.functions = []
buildActiveQuery(draftState)
}
})
@ -765,6 +767,10 @@ export const timeMachineReducer = (
draftQueries[activeQueryIndex].builderConfig.aggregateWindow = {period}
buildActiveQuery(draftState)
if (state.alerting.check) {
state.alerting.check.every = period
}
})
}
@ -837,12 +843,24 @@ export const timeMachineReducer = (
return {...state, alerting}
case 'UPDATE_TIMEMACHINE_CHECK':
const updatedCheck = {
...state.alerting.check,
...action.payload.checkUpdate,
} as Partial<Check>
return produce(state, draftState => {
draftState.alerting.check = {
...draftState.alerting.check,
...action.payload.checkUpdate,
} as Check
return {...state, alerting: {...state.alerting, check: updatedCheck}}
const every = action.payload.checkUpdate.every
if (every) {
const {activeQueryIndex, draftQueries} = draftState
draftQueries[activeQueryIndex].builderConfig.aggregateWindow = {
period: every,
}
buildActiveQuery(draftState)
}
})
case 'CHANGE_TIMEMACHINE_CHECK_TYPE':
return produce(state, draftState => {

View File

@ -5,6 +5,7 @@ import {fromFlux, Table} from '@influxdata/giraffe'
// Utils
import {parseResponse} from 'src/shared/parsing/flux/response'
import {getCheckVisTimeRange} from 'src/alerting/utils/vis'
import {
defaultXColumn,
defaultYColumn,
@ -13,7 +14,13 @@ import {
} from 'src/shared/utils/vis'
// Types
import {FluxTable, QueryView, AppState, DashboardDraftQuery} from 'src/types'
import {
FluxTable,
QueryView,
AppState,
DashboardDraftQuery,
TimeRange,
} from 'src/types'
export const getActiveTimeMachine = (state: AppState) => {
const {activeTimeMachineID, timeMachines} = state.timeMachines
@ -22,6 +29,20 @@ export const getActiveTimeMachine = (state: AppState) => {
return timeMachine
}
export const getIsInCheckOverlay = (state: AppState): boolean => {
return state.timeMachines.activeTimeMachineID === 'alerting'
}
export const getTimeRange = (state: AppState): TimeRange => {
const {alerting, timeRange} = getActiveTimeMachine(state)
if (!getIsInCheckOverlay(state)) {
return timeRange
}
return getCheckVisTimeRange(alerting.check.every)
}
export const getActiveQuery = (state: AppState): DashboardDraftQuery => {
const {draftQueries, activeQueryIndex} = getActiveTimeMachine(state)