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', 'defbuck',
'Funky Town', 'Funky Town',
'Jimmy Mack', 'Jimmy Mack',
'_tasks',
'_monitoring', '_monitoring',
'_tasks',
] ]
// check the order // check the order
expect(results).to.deep.equal(expectedOrder) 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 // 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('delete-checkbox').check({force: true})
cy.getByTestID('confirm-delete-btn').click() cy.getByTestID('confirm-delete-btn').click()
cy.getByTestID('overlay--container').should('not.exist') cy.getByTestID('overlay--container').should('not.exist')
cy.getByTestID('notification-success').should('have.length', 1) cy.getByTestID('notification-success').should('have.length', 1)
}) })
// needs relevant data in order to test functionality
it('should require key-value pairs when deleting predicate with filters', () => { it.skip('should require key-value pairs when deleting predicate with filters', () => {
// confirm delete is disabled // confirm delete is disabled
cy.getByTestID('add-filter-btn').click() cy.getByTestID('add-filter-btn').click()
// checks the consent input // checks the consent input
@ -192,12 +192,7 @@ describe('Buckets', () => {
// should display warnings // should display warnings
cy.getByTestID('form--element-error').should('have.length', 2) cy.getByTestID('form--element-error').should('have.length', 2)
cy.getByTestID('key-input').type('mean') // TODO: add filter values based on dropdown selection in key / value
cy.getByTestID('value-input').type(100)
cy.getByTestID('confirm-delete-btn')
.should('not.be.disabled')
.click()
}) })
}) })

View File

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

View File

@ -10,7 +10,7 @@ import DeleteDataForm from 'src/shared/components/DeleteDataForm/DeleteDataForm'
import GetResources, {ResourceType} from 'src/shared/components/GetResources' import GetResources, {ResourceType} from 'src/shared/components/GetResources'
// Utils // Utils
import {getActiveTimeMachine, getActiveQuery} from 'src/timeMachine/selectors' import {getActiveQuery, getActiveTimeMachine} from 'src/timeMachine/selectors'
// Types // Types
import {AppState, TimeRange} from 'src/types' import {AppState, TimeRange} from 'src/types'
@ -34,10 +34,10 @@ interface StateProps {
} }
const DeleteDataOverlay: FunctionComponent<StateProps & WithRouterProps> = ({ const DeleteDataOverlay: FunctionComponent<StateProps & WithRouterProps> = ({
selectedBucketName,
selectedTimeRange,
router, router,
params: {orgID}, params: {orgID},
selectedBucketName,
selectedTimeRange,
}) => { }) => {
const handleDismiss = () => router.push(`/orgs/${orgID}/data-explorer`) const handleDismiss = () => router.push(`/orgs/${orgID}/data-explorer`)
@ -67,7 +67,10 @@ const mstp = (state: AppState): StateProps => {
const {timeRange} = getActiveTimeMachine(state) const {timeRange} = getActiveTimeMachine(state)
const selectedTimeRange = resolveTimeRange(timeRange) const selectedTimeRange = resolveTimeRange(timeRange)
return {selectedBucketName, selectedTimeRange} return {
selectedBucketName,
selectedTimeRange,
}
} }
export default connect<StateProps>(mstp)( 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 {Dispatch} from 'redux-thunk'
import {extractBoxedCol} from 'src/timeMachine/apis/queryBuilder'
// API // API
import * as api from 'src/client' import {postDelete} from 'src/client'
import {runQuery} from 'src/shared/apis/query'
// Actions // Actions
import {notify} from 'src/shared/actions/notifications' import {notify} from 'src/shared/actions/notifications'
@ -11,82 +13,41 @@ import {notify} from 'src/shared/actions/notifications'
import { import {
predicateDeleteFailed, predicateDeleteFailed,
predicateDeleteSucceeded, predicateDeleteSucceeded,
setFilterKeyFailed,
setFilterValueFailed,
} from 'src/shared/copy/notifications' } from 'src/shared/copy/notifications'
// Types // Types
import {RemoteDataState, Filter} from 'src/types' import {RemoteDataState, Filter} from 'src/types'
export type Action = export type Action =
| SetIsSerious
| SetBucketName
| SetTimeRange
| SetFilter
| DeleteFilter | DeleteFilter
| ResetFilters
| SetBucketName
| SetDeletionStatus | SetDeletionStatus
| SetFilter
| SetIsSerious
| SetKeysByBucket
| SetPredicateToDefault | SetPredicateToDefault
| SetTimeRange
interface SetIsSerious { | SetValuesByKey
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,
})
interface DeleteFilter { interface DeleteFilter {
type: 'DELETE_FILTER' type: 'DELETE_FILTER'
index: number payload: {index: number}
} }
export const deleteFilter = (index: number): DeleteFilter => ({ export const deleteFilter = (index: number): DeleteFilter => ({
type: 'DELETE_FILTER', type: 'DELETE_FILTER',
index, payload: {index},
}) })
interface SetDeletionStatus { interface ResetFilters {
type: 'SET_DELETION_STATUS' type: 'RESET_FILTERS'
deletionStatus: RemoteDataState
} }
export const setDeletionStatus = ( export const resetFilters = (): ResetFilters => ({
status: RemoteDataState type: 'RESET_FILTERS',
): SetDeletionStatus => ({
type: 'SET_DELETION_STATUS',
deletionStatus: status,
}) })
interface SetPredicateToDefault { interface SetPredicateToDefault {
@ -97,11 +58,87 @@ export const resetPredicateState = (): SetPredicateToDefault => ({
type: 'SET_PREDICATE_DEFAULT', 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 ( export const deleteWithPredicate = params => async (
dispatch: Dispatch<Action> dispatch: Dispatch<Action>
) => { ) => {
try { try {
const resp = await api.postDelete(params) const resp = await postDelete(params)
if (resp.status !== 204) { if (resp.status !== 204) {
throw new Error(resp.data.message) throw new Error(resp.data.message)
} }
@ -115,3 +152,35 @@ export const deleteWithPredicate = params => async (
dispatch(resetPredicateState()) 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--filters,
.delete-data-filters--no-filters .delete-data-filters--no-filters {
{
margin: $ix-marg-c 0; margin: $ix-marg-c 0;
} }
@ -10,7 +9,7 @@
} }
.delete-data-filter { .delete-data-filter {
display: flex; display: flex;
justify-content: stretch; justify-content: stretch;
} }
@ -27,14 +26,12 @@
} }
.delete-data-filter--remove, .delete-data-filter--remove,
.delete-data-filter--equals, .delete-data-filter--equals {
{
margin-top: 18px; margin-top: 18px;
} }
.delete-data-filter--remove { .delete-data-filter--remove {
flex: 0 0 auto; flex: 0 0 auto;
margin-left: $ix-marg-a;
} }
.delete-data-form--danger-zone { .delete-data-form--danger-zone {
@ -65,3 +62,7 @@
color: $c-fire; color: $c-fire;
} }
} }
.dwp-filter-dropdown {
max-width: 95%;
}

View File

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

View File

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

View File

@ -5,25 +5,44 @@ import {
ButtonShape, ButtonShape,
Form, Form,
IconFont, IconFont,
Input,
SelectDropdown, SelectDropdown,
} from '@influxdata/clockface' } from '@influxdata/clockface'
import {connect} from 'react-redux'
// Components
import SearchableDropdown from 'src/shared/components/SearchableDropdown'
// Types // Types
import {Filter} from 'src/types' import {Filter} from 'src/types'
// Actions
import {setValuesByKey} from 'src/shared/actions/predicates'
interface Props { interface Props {
bucket: string
filter: Filter filter: Filter
keys: string[]
onChange: (filter: Filter) => any onChange: (filter: Filter) => any
onDelete: () => any onDelete: () => any
orgID: string
shouldValidate: boolean 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}, filter: {key, equality, value},
keys,
onChange, onChange,
onDelete, onDelete,
orgID,
setValuesByKey,
shouldValidate, shouldValidate,
values,
}) => { }) => {
const keyErrorMessage = const keyErrorMessage =
shouldValidate && key.trim() === '' ? 'Key cannot be empty' : null shouldValidate && key.trim() === '' ? 'Key cannot be empty' : null
@ -32,8 +51,12 @@ const FilterRow: FC<Props> = ({
const valueErrorMessage = const valueErrorMessage =
shouldValidate && value.trim() === '' ? 'Value cannot be empty' : null shouldValidate && value.trim() === '' ? 'Value cannot be empty' : null
const onChangeKey = e => onChange({key: e.target.value, equality, value}) const onChangeKey = input => onChange({key: input, equality, value})
const onChangeValue = e => onChange({key, equality, value: e.target.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}) const onChangeEquality = e => onChange({key, equality: e, value})
return ( return (
@ -43,7 +66,19 @@ const FilterRow: FC<Props> = ({
required={true} required={true}
errorMessage={keyErrorMessage} 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>
<Form.Element <Form.Element
label="Equality Filter" label="Equality Filter"
@ -51,6 +86,7 @@ const FilterRow: FC<Props> = ({
errorMessage={equalityErrorMessage} errorMessage={equalityErrorMessage}
> >
<SelectDropdown <SelectDropdown
className="dwp-filter-dropdown"
options={['=', '!=']} options={['=', '!=']}
selectedOption={equality} selectedOption={equality}
onSelect={onChangeEquality} onSelect={onChangeEquality}
@ -61,7 +97,19 @@ const FilterRow: FC<Props> = ({
required={true} required={true}
errorMessage={valueErrorMessage} 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> </Form.Element>
<Button <Button
className="delete-data-filter--remove" 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 {connect} from 'react-redux'
import {withRouter, WithRouterProps} from 'react-router' import {withRouter, WithRouterProps} from 'react-router'
import {Overlay} from '@influxdata/clockface' import {Overlay} from '@influxdata/clockface'
import {get} from 'lodash'
// Components // Components
import DeleteDataForm from 'src/shared/components/DeleteDataForm/DeleteDataForm' import DeleteDataForm from 'src/shared/components/DeleteDataForm/DeleteDataForm'
@ -10,28 +11,36 @@ import DeleteDataForm from 'src/shared/components/DeleteDataForm/DeleteDataForm'
// Types // Types
import {Bucket, AppState} from 'src/types' import {Bucket, AppState} from 'src/types'
// Utils
import {getActiveQuery} from 'src/timeMachine/selectors'
interface StateProps { interface StateProps {
buckets: Bucket[] buckets: Bucket[]
selectedBucketName?: string
} }
const DeleteDataOverlay: FunctionComponent<StateProps & WithRouterProps> = ({ const DeleteDataOverlay: FunctionComponent<StateProps & WithRouterProps> = ({
buckets,
router, router,
params: {orgID, bucketID}, params: {orgID, bucketID},
buckets, selectedBucketName,
}) => { }) => {
const handleDismiss = () => const handleDismiss = () =>
router.push(`/orgs/${orgID}/load-data/buckets/${bucketID}`) 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 ( return (
<Overlay visible={true}> <Overlay visible={true}>
<Overlay.Container maxWidth={600}> <Overlay.Container maxWidth={600}>
<Overlay.Header title="Delete Data" onDismiss={handleDismiss} /> <Overlay.Header title="Delete Data" onDismiss={handleDismiss} />
<Overlay.Body> <Overlay.Body>
<DeleteDataForm <DeleteDataForm
initialBucketName={bucketName}
orgID={orgID}
handleDismiss={handleDismiss} handleDismiss={handleDismiss}
initialBucketName={initialBucketName}
orgID={orgID}
/> />
</Overlay.Body> </Overlay.Body>
</Overlay.Container> </Overlay.Container>
@ -40,7 +49,12 @@ const DeleteDataOverlay: FunctionComponent<StateProps & WithRouterProps> = ({
} }
const mstp = (state: AppState): StateProps => { 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)( export default connect<StateProps>(mstp)(

View File

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

View File

@ -568,6 +568,16 @@ export const predicateDeleteFailed = (): Notification => ({
message: 'Failed to delete data with predicate', 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 => ({ export const bucketCreateSuccess = (): Notification => ({
...defaultSuccessNotification, ...defaultSuccessNotification,
message: 'Bucket was successfully created', message: 'Bucket was successfully created',

View File

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

View File

@ -12,3 +12,8 @@ export const findValues = (_: string) => ({
promise: Promise.resolve(['tv1', 'tv2']), promise: Promise.resolve(['tv1', 'tv2']),
cancel: () => {}, 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') return extractBoxedCol(runQuery(orgID, query), '_value')
} }
function extractBoxedCol( export function extractBoxedCol(
resp: CancelBox<RunQueryResult>, resp: CancelBox<RunQueryResult>,
colName: string colName: string
): CancelBox<string[]> { ): CancelBox<string[]> {
@ -120,7 +120,7 @@ function extractBoxedCol(
return {promise, cancel: resp.cancel} 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 tables = parseResponse(csv)
const data = get(tables, '0.data', []) const data = get(tables, '0.data', [])

View File

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

View File

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