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 testpull/15998/head
parent
afd124f19f
commit
684139d3af
|
@ -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
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)(
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)(
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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: () => {},
|
||||
})
|
||||
|
|
|
@ -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', [])
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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[]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue