feat(ui/DWP): added searchable dropdown for tag key and values for selected bucket (#15879)

* feat(ui/DWP): added searchable dropdown for tag key and values for selected bucket

* feat(DWP-targets): key-value tags are associated with selected bucket

* fix(predicates/action): reordered actions so that actions alphabetical and thunks are at the bottom of the page

* feat(DWP-dropdown): made suggested PR changes

* fix(DWP-dropdown): added filter predicate to tagKeys query

* feat(ui/DWP): added searchable dropdown for tag key and values for selected bucket

* feat(DWP-targets): key-value tags are associated with selected bucket

* fix(predicates/action): reordered actions so that actions alphabetical and thunks are at the bottom of the page

* feat(DWP-dropdown): made suggested PR changes

* fix(DWP-dropdown): added filter predicate to tagKeys query

* first steps to predicate action tests

* fix(dwp-dropdown): added action tests for thunk actions

* fix(predicates.test): removed unnecessary store and redux logic from test
pull/15998/head
Ariel Salem 2019-11-20 15:23:35 -08:00 committed by GitHub
parent afd124f19f
commit 684139d3af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 481 additions and 166 deletions

View File

@ -144,8 +144,8 @@ describe('Buckets', () => {
'defbuck',
'Funky Town',
'Jimmy Mack',
'_tasks',
'_monitoring',
'_tasks',
]
// check the order
expect(results).to.deep.equal(expectedOrder)
@ -174,14 +174,14 @@ describe('Buckets', () => {
})
// this is currently not producing success, its actually failing, im going to write a separate issue for this
it.skip('closes the overlay upon a successful delete with predicate submission', () => {
it('closes the overlay upon a successful delete with predicate submission', () => {
cy.getByTestID('delete-checkbox').check({force: true})
cy.getByTestID('confirm-delete-btn').click()
cy.getByTestID('overlay--container').should('not.exist')
cy.getByTestID('notification-success').should('have.length', 1)
})
it('should require key-value pairs when deleting predicate with filters', () => {
// needs relevant data in order to test functionality
it.skip('should require key-value pairs when deleting predicate with filters', () => {
// confirm delete is disabled
cy.getByTestID('add-filter-btn').click()
// checks the consent input
@ -192,12 +192,7 @@ describe('Buckets', () => {
// should display warnings
cy.getByTestID('form--element-error').should('have.length', 2)
cy.getByTestID('key-input').type('mean')
cy.getByTestID('value-input').type(100)
cy.getByTestID('confirm-delete-btn')
.should('not.be.disabled')
.click()
// TODO: add filter values based on dropdown selection in key / value
})
})

View File

@ -621,8 +621,8 @@ describe('DataExplorer', () => {
cy.getByTestID('overlay--container').should('not.exist')
cy.getByTestID('notification-success').should('have.length', 1)
})
it('should require key-value pairs when deleting predicate with filters', () => {
// needs relevant data in order to test functionality
it.skip('should require key-value pairs when deleting predicate with filters', () => {
// confirm delete is disabled
cy.getByTestID('add-filter-btn').click()
// checks the consent input
@ -633,12 +633,7 @@ describe('DataExplorer', () => {
// should display warnings
cy.getByTestID('form--element-error').should('have.length', 2)
cy.getByTestID('key-input').type('mean')
cy.getByTestID('value-input').type(100)
cy.getByTestID('confirm-delete-btn')
.should('not.be.disabled')
.click()
// TODO: add filter values based on dropdown selection in key / value
})
})
})

View File

@ -10,7 +10,7 @@ import DeleteDataForm from 'src/shared/components/DeleteDataForm/DeleteDataForm'
import GetResources, {ResourceType} from 'src/shared/components/GetResources'
// Utils
import {getActiveTimeMachine, getActiveQuery} from 'src/timeMachine/selectors'
import {getActiveQuery, getActiveTimeMachine} from 'src/timeMachine/selectors'
// Types
import {AppState, TimeRange} from 'src/types'
@ -34,10 +34,10 @@ interface StateProps {
}
const DeleteDataOverlay: FunctionComponent<StateProps & WithRouterProps> = ({
selectedBucketName,
selectedTimeRange,
router,
params: {orgID},
selectedBucketName,
selectedTimeRange,
}) => {
const handleDismiss = () => router.push(`/orgs/${orgID}/data-explorer`)
@ -67,7 +67,10 @@ const mstp = (state: AppState): StateProps => {
const {timeRange} = getActiveTimeMachine(state)
const selectedTimeRange = resolveTimeRange(timeRange)
return {selectedBucketName, selectedTimeRange}
return {
selectedBucketName,
selectedTimeRange,
}
}
export default connect<StateProps>(mstp)(

View File

@ -0,0 +1,103 @@
import {mocked} from 'ts-jest/utils'
// Mocks
import {postDelete} from 'src/client'
jest.mock('src/client')
jest.mock('src/timeMachine/apis/queryBuilder')
jest.mock('src/shared/apis/query')
// Types
// Actions
import {
deleteWithPredicate,
setBucketAndKeys,
setValuesByKey,
} from 'src/shared/actions/predicates'
describe('Shared.Actions.Predicates', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('deletes then dispatches success messages', async () => {
const mockDispatch = jest.fn()
const params = {}
mocked(postDelete).mockImplementation(() => ({status: 204}))
await deleteWithPredicate(params)(mockDispatch)
expect(postDelete).toHaveBeenCalledTimes(1)
const [
setDeletionStatusDispatch,
notifySuccessCall,
resetPredicateStateCall,
] = mockDispatch.mock.calls
expect(setDeletionStatusDispatch).toEqual([
{
type: 'SET_DELETION_STATUS',
payload: {deletionStatus: 'Done'},
},
])
expect(notifySuccessCall).toEqual([
{
type: 'PUBLISH_NOTIFICATION',
payload: {
notification: {
duration: 5000,
icon: 'checkmark',
message: 'Successfully deleted data with predicate!',
style: 'success',
},
},
},
])
expect(resetPredicateStateCall).toEqual([{type: 'SET_PREDICATE_DEFAULT'}])
})
it('sets the keys based on the bucket name', async () => {
const mockDispatch = jest.fn()
const orgID = '1'
const bucketName = 'Foxygen'
await setBucketAndKeys(orgID, bucketName)(mockDispatch)
const [setBucketNameDispatch, setKeysDispatch] = mockDispatch.mock.calls
expect(setBucketNameDispatch).toEqual([
{type: 'SET_BUCKET_NAME', payload: {bucketName: 'Foxygen'}},
])
expect(setKeysDispatch).toEqual([
{
type: 'SET_KEYS_BY_BUCKET',
payload: {
keys: ['Talking Heads', 'This must be the place'],
},
},
])
})
it('sets the values based on the bucket and key name', async () => {
const mockDispatch = jest.fn()
const orgID = '1'
const bucketName = 'Simon & Garfunkel'
const keyName = 'America'
await setValuesByKey(orgID, bucketName, keyName)(mockDispatch)
const [setValuesDispatch] = mockDispatch.mock.calls
expect(setValuesDispatch).toEqual([
{
type: 'SET_VALUES_BY_KEY',
payload: {
values: ['Talking Heads', 'This must be the place'],
},
},
])
})
})

View File

@ -1,8 +1,10 @@
// Redux
// Libraries
import {Dispatch} from 'redux-thunk'
import {extractBoxedCol} from 'src/timeMachine/apis/queryBuilder'
// API
import * as api from 'src/client'
import {postDelete} from 'src/client'
import {runQuery} from 'src/shared/apis/query'
// Actions
import {notify} from 'src/shared/actions/notifications'
@ -11,82 +13,41 @@ import {notify} from 'src/shared/actions/notifications'
import {
predicateDeleteFailed,
predicateDeleteSucceeded,
setFilterKeyFailed,
setFilterValueFailed,
} from 'src/shared/copy/notifications'
// Types
import {RemoteDataState, Filter} from 'src/types'
export type Action =
| SetIsSerious
| SetBucketName
| SetTimeRange
| SetFilter
| DeleteFilter
| ResetFilters
| SetBucketName
| SetDeletionStatus
| SetFilter
| SetIsSerious
| SetKeysByBucket
| SetPredicateToDefault
interface SetIsSerious {
type: 'SET_IS_SERIOUS'
isSerious: boolean
}
export const setIsSerious = (isSerious: boolean): SetIsSerious => ({
type: 'SET_IS_SERIOUS',
isSerious,
})
interface SetBucketName {
type: 'SET_BUCKET_NAME'
bucketName: string
}
export const setBucketName = (bucketName: string): SetBucketName => ({
type: 'SET_BUCKET_NAME',
bucketName,
})
interface SetTimeRange {
type: 'SET_DELETE_TIME_RANGE'
timeRange: [number, number]
}
export const setTimeRange = (timeRange: [number, number]): SetTimeRange => ({
type: 'SET_DELETE_TIME_RANGE',
timeRange,
})
interface SetFilter {
type: 'SET_FILTER'
filter: Filter
index: number
}
export const setFilter = (filter: Filter, index: number): SetFilter => ({
type: 'SET_FILTER',
filter,
index,
})
| SetTimeRange
| SetValuesByKey
interface DeleteFilter {
type: 'DELETE_FILTER'
index: number
payload: {index: number}
}
export const deleteFilter = (index: number): DeleteFilter => ({
type: 'DELETE_FILTER',
index,
payload: {index},
})
interface SetDeletionStatus {
type: 'SET_DELETION_STATUS'
deletionStatus: RemoteDataState
interface ResetFilters {
type: 'RESET_FILTERS'
}
export const setDeletionStatus = (
status: RemoteDataState
): SetDeletionStatus => ({
type: 'SET_DELETION_STATUS',
deletionStatus: status,
export const resetFilters = (): ResetFilters => ({
type: 'RESET_FILTERS',
})
interface SetPredicateToDefault {
@ -97,11 +58,87 @@ export const resetPredicateState = (): SetPredicateToDefault => ({
type: 'SET_PREDICATE_DEFAULT',
})
interface SetBucketName {
type: 'SET_BUCKET_NAME'
payload: {bucketName: string}
}
export const setBucketName = (bucketName: string): SetBucketName => ({
type: 'SET_BUCKET_NAME',
payload: {bucketName},
})
interface SetDeletionStatus {
type: 'SET_DELETION_STATUS'
payload: {deletionStatus: RemoteDataState}
}
export const setDeletionStatus = (
status: RemoteDataState
): SetDeletionStatus => ({
type: 'SET_DELETION_STATUS',
payload: {deletionStatus: status},
})
interface SetFilter {
type: 'SET_FILTER'
payload: {
filter: Filter
index: number
}
}
export const setFilter = (filter: Filter, index: number): SetFilter => ({
type: 'SET_FILTER',
payload: {filter, index},
})
interface SetIsSerious {
type: 'SET_IS_SERIOUS'
payload: {isSerious: boolean}
}
export const setIsSerious = (isSerious: boolean): SetIsSerious => ({
type: 'SET_IS_SERIOUS',
payload: {isSerious},
})
interface SetKeysByBucket {
type: 'SET_KEYS_BY_BUCKET'
payload: {keys: string[]}
}
const setKeys = (keys: string[]): SetKeysByBucket => ({
type: 'SET_KEYS_BY_BUCKET',
payload: {keys},
})
interface SetTimeRange {
type: 'SET_DELETE_TIME_RANGE'
payload: {timeRange: [number, number]}
}
export const setTimeRange = (timeRange: [number, number]): SetTimeRange => ({
type: 'SET_DELETE_TIME_RANGE',
payload: {timeRange},
})
interface SetValuesByKey {
type: 'SET_VALUES_BY_KEY'
payload: {values: string[]}
}
const setValues = (values: string[]): SetValuesByKey => ({
type: 'SET_VALUES_BY_KEY',
payload: {values},
})
export const deleteWithPredicate = params => async (
dispatch: Dispatch<Action>
) => {
try {
const resp = await api.postDelete(params)
const resp = await postDelete(params)
if (resp.status !== 204) {
throw new Error(resp.data.message)
}
@ -115,3 +152,35 @@ export const deleteWithPredicate = params => async (
dispatch(resetPredicateState())
}
}
export const setBucketAndKeys = (orgID: string, bucketName: string) => async (
dispatch: Dispatch<Action>
) => {
try {
const query = `import "influxdata/influxdb/v1"
v1.tagKeys(bucket: "${bucketName}")
|> filter(fn: (r) => r._value != "_stop" and r._value != "_start")`
const keys = await extractBoxedCol(runQuery(orgID, query), '_value').promise
dispatch(setBucketName(bucketName))
dispatch(setKeys(keys))
} catch {
dispatch(notify(setFilterKeyFailed()))
dispatch(setDeletionStatus(RemoteDataState.Error))
}
}
export const setValuesByKey = (
orgID: string,
bucketName: string,
keyName: string
) => async (dispatch: Dispatch<Action>) => {
try {
const query = `import "influxdata/influxdb/v1" v1.tagValues(bucket: "${bucketName}", tag: "${keyName}")`
const values = await extractBoxedCol(runQuery(orgID, query), '_value')
.promise
dispatch(setValues(values))
} catch {
dispatch(notify(setFilterValueFailed()))
dispatch(setDeletionStatus(RemoteDataState.Error))
}
}

View File

@ -1,6 +1,5 @@
.delete-data-filters--filters,
.delete-data-filters--no-filters
{
.delete-data-filters--no-filters {
margin: $ix-marg-c 0;
}
@ -10,7 +9,7 @@
}
.delete-data-filter {
display: flex;
display: flex;
justify-content: stretch;
}
@ -27,14 +26,12 @@
}
.delete-data-filter--remove,
.delete-data-filter--equals,
{
.delete-data-filter--equals {
margin-top: 18px;
}
.delete-data-filter--remove {
flex: 0 0 auto;
margin-left: $ix-marg-a;
}
.delete-data-form--danger-zone {
@ -65,3 +62,7 @@
color: $c-fire;
}
}
.dwp-filter-dropdown {
max-width: 95%;
}

View File

@ -1,5 +1,5 @@
// Libraries
import React, {FunctionComponent} from 'react'
import React, {FC, useEffect} from 'react'
import moment from 'moment'
import {connect} from 'react-redux'
import {Form, Grid, Columns, Panel} from '@influxdata/clockface'
@ -17,14 +17,15 @@ import {Filter, RemoteDataState} from 'src/types'
// Selectors
import {setCanDelete} from 'src/shared/selectors/canDelete'
// action
// Actions
import {
deleteFilter,
deleteWithPredicate,
setBucketName,
resetFilters,
setDeletionStatus,
setFilter,
setIsSerious,
setBucketAndKeys,
setTimeRange,
} from 'src/shared/actions/predicates'
@ -33,30 +34,35 @@ interface OwnProps {
handleDismiss: () => void
initialBucketName?: string
initialTimeRange?: [number, number]
keys: string[]
values: (string | number)[]
}
interface StateProps {
bucketName: string
canDelete: boolean
filters: Filter[]
timeRange: [number, number]
isSerious: boolean
deletionStatus: RemoteDataState
filters: Filter[]
isSerious: boolean
keys: string[]
timeRange: [number, number]
values: (string | number)[]
}
interface DispatchProps {
deleteFilter: typeof deleteFilter
deleteFilter: (index: number) => void
deleteWithPredicate: typeof deleteWithPredicate
setBucketName: typeof setBucketName
setDeletionStatus: typeof setDeletionStatus
resetFilters: () => void
setDeletionStatus: (status: RemoteDataState) => void
setFilter: typeof setFilter
setIsSerious: typeof setIsSerious
setTimeRange: typeof setTimeRange
setIsSerious: (isSerious: boolean) => void
setBucketAndKeys: (orgID: string, bucketName: string) => void
setTimeRange: (timeRange: [number, number]) => void
}
export type Props = StateProps & DispatchProps & OwnProps
const DeleteDataForm: FunctionComponent<Props> = ({
const DeleteDataForm: FC<Props> = ({
bucketName,
canDelete,
deleteFilter,
@ -67,15 +73,24 @@ const DeleteDataForm: FunctionComponent<Props> = ({
initialBucketName,
initialTimeRange,
isSerious,
keys,
orgID,
setBucketName,
resetFilters,
setDeletionStatus,
setFilter,
setIsSerious,
setBucketAndKeys,
setTimeRange,
timeRange,
values,
}) => {
const name = bucketName || initialBucketName
// trigger the setBucketAndKeys if the bucketName hasn't been set
if (bucketName === '' && name !== undefined) {
useEffect(() => {
setBucketAndKeys(orgID, name)
})
}
const realTimeRange = initialTimeRange || timeRange
@ -114,6 +129,11 @@ const DeleteDataForm: FunctionComponent<Props> = ({
handleDismiss()
}
const handleBucketClick = selectedBucket => {
setBucketAndKeys(orgID, selectedBucket)
resetFilters()
}
return (
<Form className="delete-data-form">
<Grid>
@ -122,7 +142,7 @@ const DeleteDataForm: FunctionComponent<Props> = ({
<Form.Element label="Target Bucket">
<BucketsDropdown
bucketName={name}
onSetBucketName={bucketName => setBucketName(bucketName)}
onSetBucketName={bucketName => handleBucketClick(bucketName)}
/>
</Form.Element>
</Grid.Column>
@ -138,10 +158,14 @@ const DeleteDataForm: FunctionComponent<Props> = ({
<Grid.Row>
<Grid.Column widthXS={Columns.Twelve}>
<FilterEditor
bucket={name}
filters={filters}
onSetFilter={(filter, index) => setFilter(filter, index)}
keys={keys}
onDeleteFilter={index => deleteFilter(index)}
onSetFilter={(filter, index) => setFilter(filter, index)}
orgID={orgID}
shouldValidate={isSerious}
values={values}
/>
</Grid.Column>
</Grid.Row>
@ -173,25 +197,35 @@ const DeleteDataForm: FunctionComponent<Props> = ({
}
const mstp = ({predicates}) => {
const {bucketName, deletionStatus, filters, isSerious, timeRange} = predicates
const {
bucketName,
deletionStatus,
filters,
isSerious,
keys,
timeRange,
values,
} = predicates
return {
bucketName,
canDelete: setCanDelete(predicates),
deletionStatus,
filters,
isSerious,
keys,
timeRange,
values,
}
}
const mdtp = {
deleteFilter,
deleteWithPredicate,
setBucketName,
resetFilters,
setDeletionStatus,
setFilter,
setIsSerious,
setBucketAndKeys,
setTimeRange,
}

View File

@ -9,17 +9,25 @@ import FilterRow from 'src/shared/components/DeleteDataForm/FilterRow'
import {Filter} from 'src/types'
interface Props {
bucket: string
filters: Filter[]
onSetFilter: (filter: Filter, index: number) => any
keys: string[]
onDeleteFilter: (index: number) => any
onSetFilter: (filter: Filter, index: number) => any
orgID: string
shouldValidate: boolean
values: (string | number)[]
}
const FilterEditor: FunctionComponent<Props> = ({
bucket,
filters,
onSetFilter,
keys,
onDeleteFilter,
onSetFilter,
orgID,
shouldValidate,
values,
}) => {
return (
<div className="delete-data-filters">
@ -37,11 +45,15 @@ const FilterEditor: FunctionComponent<Props> = ({
<div className="delete-data-filters--filters">
{filters.map((filter, i) => (
<FilterRow
bucket={bucket}
key={i}
keys={keys}
filter={filter}
onChange={filter => onSetFilter(filter, i)}
onDelete={() => onDeleteFilter(i)}
orgID={orgID}
shouldValidate={shouldValidate}
values={values}
/>
))}
</div>

View File

@ -5,25 +5,44 @@ import {
ButtonShape,
Form,
IconFont,
Input,
SelectDropdown,
} from '@influxdata/clockface'
import {connect} from 'react-redux'
// Components
import SearchableDropdown from 'src/shared/components/SearchableDropdown'
// Types
import {Filter} from 'src/types'
// Actions
import {setValuesByKey} from 'src/shared/actions/predicates'
interface Props {
bucket: string
filter: Filter
keys: string[]
onChange: (filter: Filter) => any
onDelete: () => any
orgID: string
shouldValidate: boolean
values: (string | number)[]
}
const FilterRow: FC<Props> = ({
interface DispatchProps {
setValuesByKey: (orgID: string, bucketName: string, keyName: string) => void
}
const FilterRow: FC<Props & DispatchProps> = ({
bucket,
filter: {key, equality, value},
keys,
onChange,
onDelete,
orgID,
setValuesByKey,
shouldValidate,
values,
}) => {
const keyErrorMessage =
shouldValidate && key.trim() === '' ? 'Key cannot be empty' : null
@ -32,8 +51,12 @@ const FilterRow: FC<Props> = ({
const valueErrorMessage =
shouldValidate && value.trim() === '' ? 'Value cannot be empty' : null
const onChangeKey = e => onChange({key: e.target.value, equality, value})
const onChangeValue = e => onChange({key, equality, value: e.target.value})
const onChangeKey = input => onChange({key: input, equality, value})
const onKeySelect = input => {
setValuesByKey(orgID, bucket, input)
onChange({key: input, equality, value})
}
const onChangeValue = input => onChange({key, equality, value: input})
const onChangeEquality = e => onChange({key, equality: e, value})
return (
@ -43,7 +66,19 @@ const FilterRow: FC<Props> = ({
required={true}
errorMessage={keyErrorMessage}
>
<Input onChange={onChangeKey} value={key} testID="key-input" />
<SearchableDropdown
className="dwp-filter-dropdown"
searchTerm={key}
emptyText="No Tags Found"
searchPlaceholder="Search keys..."
selectedOption={key}
onSelect={onKeySelect}
onChangeSearchTerm={onChangeKey}
testID="dwp-filter-key-input"
buttonTestID="tag-selector--dropdown-button"
menuTestID="tag-selector--dropdown-menu"
options={keys}
/>
</Form.Element>
<Form.Element
label="Equality Filter"
@ -51,6 +86,7 @@ const FilterRow: FC<Props> = ({
errorMessage={equalityErrorMessage}
>
<SelectDropdown
className="dwp-filter-dropdown"
options={['=', '!=']}
selectedOption={equality}
onSelect={onChangeEquality}
@ -61,7 +97,19 @@ const FilterRow: FC<Props> = ({
required={true}
errorMessage={valueErrorMessage}
>
<Input onChange={onChangeValue} value={value} testID="value-input" />
<SearchableDropdown
className="dwp-filter-dropdown"
searchTerm={value}
emptyText="No Tags Found"
searchPlaceholder="Search values..."
selectedOption={value}
onSelect={onChangeValue}
onChangeSearchTerm={onChangeValue}
testID="dwp-filter-value-input"
buttonTestID="tag-selector--dropdown-button"
menuTestID="tag-selector--dropdown-menu"
options={values}
/>
</Form.Element>
<Button
className="delete-data-filter--remove"
@ -73,4 +121,9 @@ const FilterRow: FC<Props> = ({
)
}
export default FilterRow
const mdtp = {setValuesByKey}
export default connect<{}, DispatchProps>(
null,
mdtp
)(FilterRow)

View File

@ -3,6 +3,7 @@ import React, {FunctionComponent} from 'react'
import {connect} from 'react-redux'
import {withRouter, WithRouterProps} from 'react-router'
import {Overlay} from '@influxdata/clockface'
import {get} from 'lodash'
// Components
import DeleteDataForm from 'src/shared/components/DeleteDataForm/DeleteDataForm'
@ -10,28 +11,36 @@ import DeleteDataForm from 'src/shared/components/DeleteDataForm/DeleteDataForm'
// Types
import {Bucket, AppState} from 'src/types'
// Utils
import {getActiveQuery} from 'src/timeMachine/selectors'
interface StateProps {
buckets: Bucket[]
selectedBucketName?: string
}
const DeleteDataOverlay: FunctionComponent<StateProps & WithRouterProps> = ({
buckets,
router,
params: {orgID, bucketID},
buckets,
selectedBucketName,
}) => {
const handleDismiss = () =>
router.push(`/orgs/${orgID}/load-data/buckets/${bucketID}`)
const bucketName = buckets.find(bucket => bucket.id === bucketID).name
// separated find logic and name logic since directly routing the a delete-data
// endpoint was crashing the app because the bucket is undefined until the component mounts
const bucket = buckets.find(bucket => bucket.id === bucketID)
const bucketName = bucket && bucket.name ? bucket.name : ''
const initialBucketName = selectedBucketName || bucketName
return (
<Overlay visible={true}>
<Overlay.Container maxWidth={600}>
<Overlay.Header title="Delete Data" onDismiss={handleDismiss} />
<Overlay.Body>
<DeleteDataForm
initialBucketName={bucketName}
orgID={orgID}
handleDismiss={handleDismiss}
initialBucketName={initialBucketName}
orgID={orgID}
/>
</Overlay.Body>
</Overlay.Container>
@ -40,7 +49,12 @@ const DeleteDataOverlay: FunctionComponent<StateProps & WithRouterProps> = ({
}
const mstp = (state: AppState): StateProps => {
return {buckets: state.buckets.list}
const activeQuery = getActiveQuery(state)
const selectedBucketName = get(activeQuery, 'builderConfig.buckets.0')
return {
buckets: state.buckets.list,
selectedBucketName,
}
}
export default connect<StateProps>(mstp)(

View File

@ -28,7 +28,7 @@ interface Props {
buttonTestID: string
menuTheme: DropdownMenuTheme
menuTestID: string
options: string[]
options: (string | number)[]
emptyText: string
style?: CSSProperties
}
@ -110,7 +110,7 @@ export default class SearchableDropdown extends Component<Props> {
} = this.props
const filteredOptions = options.filter(option =>
option.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase())
`${option}`.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase())
)
if (!filteredOptions.length) {

View File

@ -568,6 +568,16 @@ export const predicateDeleteFailed = (): Notification => ({
message: 'Failed to delete data with predicate',
})
export const setFilterKeyFailed = (): Notification => ({
...defaultErrorNotification,
message: 'Failed to set the filter key tag',
})
export const setFilterValueFailed = (): Notification => ({
...defaultErrorNotification,
message: 'Failed to set the filter value tag',
})
export const bucketCreateSuccess = (): Notification => ({
...defaultSuccessNotification,
message: 'Bucket was successfully created',

View File

@ -12,10 +12,12 @@ export const HOUR_MS = 1000 * 60 * 60
export const initialState: PredicatesState = {
bucketName: '',
timeRange: [recently - HOUR_MS, recently],
deletionStatus: RemoteDataState.NotStarted,
filters: [],
isSerious: false,
deletionStatus: RemoteDataState.NotStarted,
keys: [],
timeRange: [recently - HOUR_MS, recently],
values: [],
}
export const predicatesReducer = (
@ -23,43 +25,54 @@ export const predicatesReducer = (
action: Action
): PredicatesState => {
switch (action.type) {
case 'RESET_FILTERS':
return {...state, filters: []}
case 'SET_IS_SERIOUS':
return {...state, isSerious: action.isSerious}
return {...state, isSerious: action.payload.isSerious}
case 'SET_BUCKET_NAME':
return {...state, bucketName: action.bucketName}
return {...state, bucketName: action.payload.bucketName}
case 'SET_DELETE_TIME_RANGE':
return {...state, timeRange: action.timeRange}
return {...state, timeRange: action.payload.timeRange}
case 'SET_FILTER':
if (action.index >= state.filters.length) {
return {...state, filters: [...state.filters, action.filter]}
if (action.payload.index >= state.filters.length) {
return {...state, filters: [...state.filters, action.payload.filter]}
}
return {
...state,
filters: state.filters.map((filter, i) =>
i === action.index ? action.filter : filter
i === action.payload.index ? action.payload.filter : filter
),
}
case 'DELETE_FILTER':
return {
...state,
filters: state.filters.filter((_, i) => i !== action.index),
filters: state.filters.filter((_, i) => i !== action.payload.index),
}
case 'SET_DELETION_STATUS':
return {...state, deletionStatus: action.deletionStatus}
return {...state, deletionStatus: action.payload.deletionStatus}
case 'SET_KEYS_BY_BUCKET':
return {...state, keys: action.payload.keys}
case 'SET_VALUES_BY_KEY':
return {...state, values: action.payload.values}
case 'SET_PREDICATE_DEFAULT':
return {
bucketName: '',
timeRange: [recently - HOUR_MS, recently],
deletionStatus: RemoteDataState.NotStarted,
filters: [],
isSerious: false,
deletionStatus: RemoteDataState.NotStarted,
keys: [],
timeRange: [recently - HOUR_MS, recently],
values: [],
}
default:

View File

@ -12,3 +12,8 @@ export const findValues = (_: string) => ({
promise: Promise.resolve(['tv1', 'tv2']),
cancel: () => {},
})
export const extractBoxedCol = (_: string) => ({
promise: Promise.resolve(['Talking Heads', 'This must be the place']),
cancel: () => {},
})

View File

@ -105,7 +105,7 @@ export function findValues({
return extractBoxedCol(runQuery(orgID, query), '_value')
}
function extractBoxedCol(
export function extractBoxedCol(
resp: CancelBox<RunQueryResult>,
colName: string
): CancelBox<string[]> {
@ -120,7 +120,7 @@ function extractBoxedCol(
return {promise, cancel: resp.cancel}
}
function extractCol(csv: string, colName: string): string[] {
export function extractCol(csv: string, colName: string): string[] {
const tables = parseResponse(csv)
const data = get(tables, '0.data', [])

View File

@ -4,11 +4,11 @@ import {connect} from 'react-redux'
// Components
import {
Input,
FlexBox,
ComponentSize,
FlexDirection,
AlignItems,
ComponentSize,
FlexBox,
FlexDirection,
Input,
} from '@influxdata/clockface'
import SearchableDropdown from 'src/shared/components/SearchableDropdown'
import WaitingText from 'src/shared/components/WaitingText'

View File

@ -2,8 +2,10 @@ import {Filter, RemoteDataState} from 'src/types'
export interface PredicatesState {
bucketName: string
timeRange: [number, number]
deletionStatus: RemoteDataState
filters: Filter[]
isSerious: boolean
deletionStatus: RemoteDataState
keys: string[]
timeRange: [number, number]
values: string[]
}

View File

@ -9,6 +9,7 @@ import {MeState} from 'src/shared/reducers/me'
import {NoteEditorState} from 'src/dashboards/reducers/notes'
import {DataLoadingState} from 'src/dataLoaders/reducers'
import {OnboardingState} from 'src/onboarding/reducers'
import {PredicatesState} from 'src/types'
import {VariablesState, VariableEditorState} from 'src/variables/reducers'
import {LabelsState} from 'src/labels/reducers'
import {BucketsState} from 'src/buckets/reducers'
@ -30,38 +31,39 @@ import {NotificationRulesState} from 'src/alerting/reducers/notifications/rules'
import {NotificationEndpointsState} from 'src/alerting/reducers/notifications/endpoints'
export interface AppState {
VERSION: string
labels: LabelsState
buckets: BucketsState
telegrafs: TelegrafsState
links: Links
app: AppPresentationState
ranges: RangeState
autoRefresh: AutoRefreshState
views: ViewsState
buckets: BucketsState
checks: ChecksState
cloud: {limits: LimitsState}
dashboards: DashboardsState
dataLoading: DataLoadingState
endpoints: NotificationEndpointsState
labels: LabelsState
links: Links
me: MeState
members: MembersState
noteEditor: NoteEditorState
notifications: Notification[]
timeMachines: TimeMachinesState
routing: RouterState
tasks: TasksState
timeRange: TimeRange
onboarding: OnboardingState
orgs: OrgsState
overlays: OverlayState
me: MeState
onboarding: OnboardingState
noteEditor: NoteEditorState
dataLoading: DataLoadingState
predicates: PredicatesState
ranges: RangeState
routing: RouterState
rules: NotificationRulesState
scrapers: ScrapersState
tasks: TasksState
telegrafs: TelegrafsState
templates: TemplatesState
timeMachines: TimeMachinesState
timeRange: TimeRange
tokens: AuthorizationsState
userSettings: UserSettingsState
variables: VariablesState
variableEditor: VariableEditorState
tokens: AuthorizationsState
templates: TemplatesState
scrapers: ScrapersState
userSettings: UserSettingsState
members: MembersState
cloud: {limits: LimitsState}
checks: ChecksState
rules: NotificationRulesState
endpoints: NotificationEndpointsState
VERSION: string
views: ViewsState
}
export type GetState = () => AppState

4
yarn.lock Normal file
View File

@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1