feat(ui): ability to create a bucket from time machine (#17860)

* feat: WIP allow on the fly bucket creation

* refactor: fully implement create bucket in both places

* refactor: use separate popover based component for selector list bucket creation

* chore: prettier

* chore: cleanup buckets tab

Doesn't need org, overlay state, or any overlay components

* chore: prettier

* refactor: convert CreateBucketOverlay to function component

* chore: changelog

* chore: cleanup

* feat: add integration test for creating a bucket from the query builder

* refactor: rebuild selector list bucket creator with useReducer

* refactor: keeping it DRY - both bucket creation components use the same state management
pull/17878/head
alexpaxton 2020-04-27 14:19:12 -07:00 committed by GitHub
parent 5c00430e0b
commit d56966face
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 545 additions and 215 deletions

View File

@ -1,3 +1,13 @@
## v2.0.0-beta.10 [Unreleased]
### Features
### Bug Fixes
### UI Improvements
1. [17860](https://github.com/influxdata/influxdb/pull/17860): Allow bucket creation from the Data Explorer and Cell Editor
## v2.0.0-beta.9 [2020-04-23]
### Features

View File

@ -88,6 +88,26 @@ describe('The Query Builder', () => {
cy.getByTestID('cancel-cell-edit--button').click()
cy.contains('Basic Ole Dashboard').should('exist')
})
it('can create a bucket from the buckets list', () => {
cy.get('@org').then((org: Organization) => {
cy.visit(`orgs/${org.id}/data-explorer`)
})
const newBucketName = '٩(。•́‿•̀。)۶'
cy.getByTestID('selector-list add-bucket').click()
cy.getByTestID('bucket-form').should('exist')
cy.getByTestID('bucket-form-name').type(newBucketName)
cy.getByTestID('bucket-form-submit').click()
cy.getByTestID('buckets-list').within(() => {
cy.contains(newBucketName).should('exist')
})
})
})
describe('the group() function', () => {

View File

@ -16,15 +16,16 @@ import {
ComponentColor,
ComponentStatus,
} from '@influxdata/clockface'
import {RuleType} from 'src/buckets/reducers/createBucket'
interface Props {
name: string
retentionSeconds: number
ruleType: 'expire'
onSubmit: (e: FormEvent<HTMLFormElement>) => void
onCloseModal: () => void
onClose: () => void
onChangeRetentionRule: (seconds: number) => void
onChangeRuleType: (t: 'expire' | null) => void
onChangeRuleType: (t: RuleType) => void
onChangeInput: (e: ChangeEvent<HTMLInputElement>) => void
disableRenaming: boolean
buttonText: string
@ -40,7 +41,7 @@ export default class BucketOverlayForm extends PureComponent<Props> {
buttonText,
retentionSeconds,
disableRenaming,
onCloseModal,
onClose,
onChangeInput,
onChangeRuleType,
onChangeRetentionRule,
@ -50,7 +51,7 @@ export default class BucketOverlayForm extends PureComponent<Props> {
const nameInputStatus = disableRenaming && ComponentStatus.Disabled
return (
<Form onSubmit={onSubmit}>
<Form onSubmit={onSubmit} testID="bucket-form">
<Grid>
<Grid.Row>
<Grid.Column>
@ -69,6 +70,7 @@ export default class BucketOverlayForm extends PureComponent<Props> {
autoFocus={true}
value={name}
onChange={onChangeInput}
testID="bucket-form-name"
/>
)}
</Form.ValidationElement>
@ -90,7 +92,7 @@ export default class BucketOverlayForm extends PureComponent<Props> {
<Form.Footer>
<Button
text="Cancel"
onClick={onCloseModal}
onClick={onClose}
type={ButtonType.Button}
/>
{buttonText === 'Save Changes' && (
@ -102,6 +104,7 @@ export default class BucketOverlayForm extends PureComponent<Props> {
)}
<Button
text={buttonText}
testID="bucket-form-submit"
color={this.submitButtonColor}
status={this.submitButtonStatus}
type={ButtonType.Submit}

View File

@ -7,26 +7,21 @@ import {connect} from 'react-redux'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {
Grid,
IconFont,
ComponentSize,
ComponentColor,
Sort,
Button,
EmptyState,
ComponentStatus,
Columns,
Overlay,
} from '@influxdata/clockface'
import SearchWidget from 'src/shared/components/search_widget/SearchWidget'
import TabbedPageHeader from 'src/shared/components/tabbed_page/TabbedPageHeader'
import FilterList from 'src/shared/components/FilterList'
import BucketList from 'src/buckets/components/BucketList'
import CreateBucketOverlay from 'src/buckets/components/CreateBucketOverlay'
import AssetLimitAlert from 'src/cloud/components/AssetLimitAlert'
import BucketExplainer from 'src/buckets/components/BucketExplainer'
import DemoDataDropdown from 'src/buckets/components/DemoDataDropdown'
import {FeatureFlag} from 'src/shared/utils/featureFlag'
import ResourceSortDropdown from 'src/shared/components/resource_sort_dropdown/ResourceSortDropdown'
import CreateBucketButton from 'src/buckets/components/CreateBucketButton'
// Actions
import {
@ -46,24 +41,15 @@ import {
// Utils
import {getNewDemoBuckets} from 'src/cloud/selectors/demodata'
import {extractBucketLimits} from 'src/cloud/utils/limits'
import {getOrg} from 'src/organizations/selectors'
import {getAll} from 'src/resources/selectors'
import {isFlagEnabled} from 'src/shared/utils/featureFlag'
import {SortTypes} from 'src/shared/utils/sort'
// Types
import {
OverlayState,
AppState,
Bucket,
Organization,
ResourceType,
OwnBucket,
} from 'src/types'
import {AppState, Bucket, ResourceType, OwnBucket} from 'src/types'
import {BucketSortKey} from 'src/shared/components/resource_sort_dropdown/generateSortItems'
interface StateProps {
org: Organization
buckets: Bucket[]
limitStatus: LimitStatus
demoDataBuckets: Bucket[]
@ -80,7 +66,6 @@ interface DispatchProps {
interface State {
searchTerm: string
overlayState: OverlayState
sortKey: BucketSortKey
sortDirection: Sort
sortType: SortTypes
@ -97,7 +82,6 @@ class BucketsTab extends PureComponent<Props, State> {
this.state = {
searchTerm: '',
overlayState: OverlayState.Closed,
sortKey: 'name',
sortDirection: Sort.Ascending,
sortType: SortTypes.String,
@ -113,19 +97,12 @@ class BucketsTab extends PureComponent<Props, State> {
public render() {
const {
org,
buckets,
limitStatus,
demoDataBuckets,
getDemoDataBucketMembership,
} = this.props
const {
searchTerm,
overlayState,
sortKey,
sortDirection,
sortType,
} = this.state
const {searchTerm, sortKey, sortDirection, sortType} = this.state
const leftHeaderItems = (
<>
@ -155,15 +132,7 @@ class BucketsTab extends PureComponent<Props, State> {
/>
)}
</FeatureFlag>
<Button
text="Create Bucket"
icon={IconFont.Plus}
color={ComponentColor.Primary}
onClick={this.handleOpenModal}
testID="Create Bucket"
status={this.createButtonStatus}
titleText={this.createButtonTitleText}
/>
<CreateBucketButton />
</>
)
@ -213,13 +182,6 @@ class BucketsTab extends PureComponent<Props, State> {
</Grid.Column>
</Grid.Row>
</Grid>
<Overlay visible={overlayState === OverlayState.Open}>
<CreateBucketOverlay
org={org}
onCloseModal={this.handleCloseModal}
onCreateBucket={this.handleCreateBucket}
/>
</Overlay>
</>
)
}
@ -236,37 +198,10 @@ class BucketsTab extends PureComponent<Props, State> {
this.props.deleteBucket(id, name)
}
private handleCreateBucket = (bucket: OwnBucket) => {
this.props.createBucket(bucket)
this.handleCloseModal()
}
private handleOpenModal = (): void => {
this.setState({overlayState: OverlayState.Open})
}
private handleCloseModal = (): void => {
this.setState({overlayState: OverlayState.Closed})
}
private handleFilterUpdate = (searchTerm: string): void => {
this.setState({searchTerm})
}
private get createButtonStatus(): ComponentStatus {
if (this.props.limitStatus === LimitStatus.EXCEEDED) {
return ComponentStatus.Disabled
}
return ComponentStatus.Default
}
private get createButtonTitleText(): string {
if (this.props.limitStatus === LimitStatus.EXCEEDED) {
return 'This account has the maximum number of buckets allowed'
}
return 'Create a bucket'
}
private get emptyState(): JSX.Element {
const {searchTerm} = this.state
@ -276,12 +211,7 @@ class BucketsTab extends PureComponent<Props, State> {
<EmptyState.Text>
Looks like there aren't any <b>Buckets</b>, why not create one?
</EmptyState.Text>
<Button
text="Create Bucket"
icon={IconFont.Plus}
color={ComponentColor.Primary}
onClick={this.handleOpenModal}
/>
<CreateBucketButton />
</EmptyState>
)
}
@ -297,7 +227,6 @@ class BucketsTab extends PureComponent<Props, State> {
const mstp = (state: AppState): StateProps => {
const buckets = getAll<Bucket>(state, ResourceType.Buckets)
return {
org: getOrg(state),
buckets,
limitStatus: extractBucketLimits(state.cloud.limits),
demoDataBuckets: getNewDemoBuckets(state, buckets),

View File

@ -0,0 +1,97 @@
// Libraries
import React, {FC, useEffect} from 'react'
import {connect} from 'react-redux'
// Components
import {
Button,
IconFont,
ComponentColor,
ComponentStatus,
} from '@influxdata/clockface'
// Actions
import {
checkBucketLimits as checkBucketLimitsAction,
LimitStatus,
} from 'src/cloud/actions/limits'
import {showOverlay, dismissOverlay} from 'src/overlays/actions/overlays'
// Utils
import {extractBucketLimits} from 'src/cloud/utils/limits'
// Types
import {AppState} from 'src/types'
interface StateProps {
limitStatus: LimitStatus
}
interface DispatchProps {
onShowOverlay: typeof showOverlay
onDismissOverlay: typeof dismissOverlay
checkBucketLimits: typeof checkBucketLimitsAction
}
interface OwnProps {}
type Props = OwnProps & StateProps & DispatchProps
const CreateBucketButton: FC<Props> = ({
limitStatus,
checkBucketLimits,
onShowOverlay,
onDismissOverlay,
}) => {
useEffect(() => {
// Check bucket limits when component mounts
checkBucketLimits()
}, [])
const limitExceeded = limitStatus === LimitStatus.EXCEEDED
const text = 'Create Bucket'
let titleText = 'Click to create a bucket'
let buttonStatus = ComponentStatus.Default
if (limitExceeded) {
titleText = 'This account has the maximum number of buckets allowed'
buttonStatus = ComponentStatus.Disabled
}
const handleItemClick = (): void => {
if (limitExceeded) {
return
}
onShowOverlay('create-bucket', null, onDismissOverlay)
}
return (
<Button
icon={IconFont.Plus}
color={ComponentColor.Primary}
text={text}
titleText={titleText}
onClick={handleItemClick}
testID="Create Bucket"
status={buttonStatus}
/>
)
}
const mstp = (state: AppState): StateProps => {
return {
limitStatus: extractBucketLimits(state.cloud.limits),
}
}
const mdtp: DispatchProps = {
onShowOverlay: showOverlay,
onDismissOverlay: dismissOverlay,
checkBucketLimits: checkBucketLimitsAction,
}
export default connect<StateProps, DispatchProps, OwnProps>(
mstp,
mdtp
)(CreateBucketButton)

View File

@ -1,5 +1,5 @@
// Libraries
import React, {PureComponent, ChangeEvent, FormEvent} from 'react'
import React, {FC, ChangeEvent, FormEvent, useReducer} from 'react'
import {connect} from 'react-redux'
// Components
@ -9,145 +9,123 @@ import BucketOverlayForm from 'src/buckets/components/BucketOverlayForm'
// Utils
import {extractBucketMaxRetentionSeconds} from 'src/cloud/utils/limits'
// Constants
import {
DEFAULT_SECONDS,
READABLE_DEFAULT_SECONDS,
} from 'src/buckets/components/Retention'
// Actions
import {createBucket} from 'src/buckets/actions/thunks'
// Types
import {Organization, Bucket, AppState} from 'src/types'
import {Organization, AppState} from 'src/types'
import {
createBucketReducer,
RuleType,
initialBucketState,
DEFAULT_RULES,
} from 'src/buckets/reducers/createBucket'
const DEFAULT_RULES = [
{type: 'expire' as 'expire', everySeconds: DEFAULT_SECONDS},
]
// Selectors
import {getOrg} from 'src/organizations/selectors'
interface StateProps {
org: Organization
isRetentionLimitEnforced: boolean
}
interface DispatchProps {
createBucket: typeof createBucket
}
interface OwnProps {
org: Organization
onCloseModal: () => void
onCreateBucket: (bucket: Partial<Bucket>) => void
onClose: () => void
}
type Props = StateProps & OwnProps
type Props = OwnProps & StateProps & DispatchProps
interface State {
bucket: Bucket
ruleType: 'expire'
}
const CreateBucketOverlay: FC<Props> = ({
org,
isRetentionLimitEnforced,
createBucket,
onClose,
}) => {
const [state, dispatch] = useReducer(
createBucketReducer,
initialBucketState(isRetentionLimitEnforced, org.id)
)
class CreateBucketOverlay extends PureComponent<Props, State> {
constructor(props: Props) {
super(props)
const retentionRule = state.retentionRules.find(r => r.type === 'expire')
const retentionSeconds = retentionRule ? retentionRule.everySeconds : 3600
this.state = {
bucket: {
name: '',
retentionRules: props.isRetentionLimitEnforced ? DEFAULT_RULES : [],
readableRetention: props.isRetentionLimitEnforced
? READABLE_DEFAULT_SECONDS
: 'forever',
},
ruleType: props.isRetentionLimitEnforced ? 'expire' : null,
}
}
public render() {
const {onCloseModal} = this.props
const {bucket, ruleType} = this.state
return (
<Overlay.Container maxWidth={400}>
<Overlay.Header
title="Create Bucket"
onDismiss={this.props.onCloseModal}
/>
<Overlay.Body>
<BucketOverlayForm
name={bucket.name}
buttonText="Create"
disableRenaming={false}
ruleType={ruleType}
onCloseModal={onCloseModal}
onSubmit={this.handleSubmit}
onChangeInput={this.handleChangeInput}
retentionSeconds={this.retentionSeconds}
onChangeRuleType={this.handleChangeRuleType}
onChangeRetentionRule={this.handleChangeRetentionRule}
/>
</Overlay.Body>
</Overlay.Container>
)
}
private get retentionSeconds(): number {
const rule = this.state.bucket.retentionRules.find(r => r.type === 'expire')
if (!rule) {
return 3600
}
return rule.everySeconds
}
private handleChangeRetentionRule = (everySeconds: number): void => {
const bucket = {
...this.state.bucket,
retentionRules: [{type: 'expire' as 'expire', everySeconds}],
}
this.setState({bucket})
}
private handleChangeRuleType = (ruleType: 'expire' | null) => {
const handleChangeRuleType = (ruleType: RuleType): void => {
if (ruleType === 'expire') {
this.setState({
ruleType,
bucket: {...this.state.bucket, retentionRules: DEFAULT_RULES},
})
dispatch({type: 'updateRetentionRules', payload: DEFAULT_RULES})
} else {
this.setState({
ruleType,
bucket: {...this.state.bucket, retentionRules: []},
})
dispatch({type: 'updateRetentionRules', payload: []})
}
dispatch({type: 'updateRuleType', payload: ruleType})
}
private handleSubmit = (e: FormEvent<HTMLFormElement>): void => {
const handleChangeRetentionRule = (everySeconds: number): void => {
const retentionRules = [
{
type: 'expire',
everySeconds,
},
]
dispatch({type: 'updateRetentionRules', payload: retentionRules})
}
const handleSubmit = (e: FormEvent<HTMLFormElement>): void => {
e.preventDefault()
this.handleCreateBucket()
createBucket(state)
onClose()
}
private handleCreateBucket = (): void => {
const {onCreateBucket, org} = this.props
const orgID = org.id
const organization = org.name
const bucket = {
...this.state.bucket,
orgID,
organization,
}
onCreateBucket(bucket)
}
private handleChangeInput = (e: ChangeEvent<HTMLInputElement>) => {
const handleChangeInput = (e: ChangeEvent<HTMLInputElement>): void => {
const value = e.target.value
const key = e.target.name
const bucket = {...this.state.bucket, [key]: value}
this.setState({bucket})
if (e.target.name === 'name') {
dispatch({type: 'updateName', payload: value})
}
}
return (
<Overlay.Container maxWidth={400}>
<Overlay.Header title="Create Bucket" onDismiss={onClose} />
<Overlay.Body>
<BucketOverlayForm
name={state.name}
buttonText="Create"
disableRenaming={false}
ruleType={state.ruleType}
onClose={onClose}
onSubmit={handleSubmit}
onChangeInput={handleChangeInput}
retentionSeconds={retentionSeconds}
onChangeRuleType={handleChangeRuleType}
onChangeRetentionRule={handleChangeRetentionRule}
/>
</Overlay.Body>
</Overlay.Container>
)
}
const mstp = (state: AppState): StateProps => {
const org = getOrg(state)
const isRetentionLimitEnforced = !!extractBucketMaxRetentionSeconds(
state.cloud.limits
)
return {
org,
isRetentionLimitEnforced,
}
}
const mstp = (state: AppState): StateProps => ({
isRetentionLimitEnforced: !!extractBucketMaxRetentionSeconds(
state.cloud.limits
),
})
const mdtp: DispatchProps = {
createBucket,
}
export default connect<StateProps, {}, OwnProps>(mstp)(CreateBucketOverlay)
export default connect<StateProps, DispatchProps, OwnProps>(
mstp,
mdtp
)(CreateBucketOverlay)

View File

@ -137,7 +137,7 @@ const UpdateBucketOverlay: FunctionComponent<Props> = ({
name={bucketDraft ? bucketDraft.name : ''}
buttonText="Save Changes"
ruleType={ruleType}
onCloseModal={handleClose}
onClose={handleClose}
onSubmit={handleSubmit}
disableRenaming={true}
onChangeInput={handleChangeInput}

View File

@ -0,0 +1,62 @@
import {
DEFAULT_SECONDS,
READABLE_DEFAULT_SECONDS,
} from 'src/buckets/components/Retention'
export type RuleType = 'expire' | null
export interface RetentionRule {
type: RuleType
everySeconds: number
}
export interface ReducerState {
name: string
retentionRules: RetentionRule[]
ruleType: RuleType
readableRetention: string
orgID: string
type: 'user'
}
export type ReducerActionType =
| 'updateName'
| 'updateRuleType'
| 'updateRetentionRules'
| 'updateReadableRetention'
export interface Action {
type: ReducerActionType
payload: any
}
export const DEFAULT_RULES: RetentionRule[] = [
{type: 'expire' as 'expire', everySeconds: DEFAULT_SECONDS},
]
export const initialBucketState = (
isRetentionLimitEnforced: boolean,
orgID: string
) => ({
name: '',
retentionRules: isRetentionLimitEnforced ? DEFAULT_RULES : [],
ruleType: isRetentionLimitEnforced ? ('expire' as 'expire') : null,
readableRetention: isRetentionLimitEnforced
? READABLE_DEFAULT_SECONDS
: 'forever',
orgID,
type: 'user' as 'user',
})
export const createBucketReducer = (state: ReducerState, action: Action) => {
switch (action.type) {
case 'updateName':
return {...state, name: action.payload}
case 'updateRuleType':
return {...state, ruleType: action.payload}
case 'updateRetentionRules':
return {...state, retentionRules: action.payload}
case 'updateReadableRetention':
return {...state, readableRetention: action.payload}
}
}

View File

@ -14,6 +14,7 @@ import BucketsTokenOverlay from 'src/authorizations/components/BucketsTokenOverl
import TelegrafConfigOverlay from 'src/telegrafs/components/TelegrafConfigOverlay'
import TelegrafOutputOverlay from 'src/telegrafs/components/TelegrafOutputOverlay'
import OrgSwitcherOverlay from 'src/pageLayout/components/OrgSwitcherOverlay'
import CreateBucketOverlay from 'src/buckets/components/CreateBucketOverlay'
// Actions
import {dismissOverlay} from 'src/overlays/actions/overlays'
@ -62,6 +63,9 @@ const OverlayController: FunctionComponent<OverlayControllerProps> = props => {
case 'switch-organizations':
activeOverlay = <OrgSwitcherOverlay onClose={closer} />
break
case 'create-bucket':
activeOverlay = <CreateBucketOverlay onClose={closer} />
break
default:
visibility = false
}

View File

@ -12,6 +12,7 @@ export type OverlayID =
| 'telegraf-config'
| 'telegraf-output'
| 'switch-organizations'
| 'create-bucket'
export interface OverlayParams {
[key: string]: string

View File

@ -13,23 +13,35 @@ $selector-list--h-padding: $ix-marg-c;
font-family: $cf-code-font;
font-size: $form-sm-font;
line-height: $form-sm-font;
// white-space: nowrap;
background-color: transparent;
transition: background-color 0.25s ease, color 0.25s ease;
border: 0;
outline: 0;
width: 100%;
text-align: left;
&:hover {
background: $g5-pepper;
background-color: $g5-pepper;
}
&.selected {
background: $c-pool;
background-color: $c-pool;
color: $g20-white;
}
&.selected:hover {
background: $c-pool;
background-color: $c-pool;
}
}
.selector-list--item__disabled,
.selector-list--item__disabled:hover {
cursor: default;
background-color: transparent;
font-style: italic;
color: $g9-mountain;
}
.selector-list--checkbox {
padding-left: $ix-marg-d - $ix-border;
position: relative;

View File

@ -10,13 +10,26 @@ interface Props {
selectedItems: string[]
onSelectItem: (item: string) => void
multiSelect: boolean
children?: JSX.Element | JSX.Element[]
testID?: string
}
const SelectorList: SFC<Props> = props => {
const {items, selectedItems, onSelectItem, multiSelect} = props
const {
items,
selectedItems,
onSelectItem,
multiSelect,
children,
testID,
} = props
return (
<BuilderCard.Body addPadding={false} autoHideScrollbars={true}>
<BuilderCard.Body
addPadding={false}
autoHideScrollbars={true}
testID={testID}
>
{items.map(item => {
const className = classnames('selector-list--item', {
selected: selectedItems.includes(item),
@ -39,6 +52,7 @@ const SelectorList: SFC<Props> = props => {
</div>
)
})}
{children}
</BuilderCard.Body>
)
}

View File

@ -0,0 +1,192 @@
// Libraries
import React, {
FC,
ChangeEvent,
FormEvent,
useEffect,
useRef,
useReducer,
} from 'react'
import {connect} from 'react-redux'
// Components
import {
Popover,
PopoverInteraction,
PopoverPosition,
Appearance,
ComponentColor,
} from '@influxdata/clockface'
import BucketOverlayForm from 'src/buckets/components/BucketOverlayForm'
// Utils
import {
extractBucketMaxRetentionSeconds,
extractBucketLimits,
} from 'src/cloud/utils/limits'
// Actions
import {
checkBucketLimits as checkBucketLimitsAction,
LimitStatus,
} from 'src/cloud/actions/limits'
import {createBucket} from 'src/buckets/actions/thunks'
// Types
import {Organization, AppState} from 'src/types'
import {
createBucketReducer,
RuleType,
initialBucketState,
DEFAULT_RULES,
} from 'src/buckets/reducers/createBucket'
// Selectors
import {getOrg} from 'src/organizations/selectors'
interface StateProps {
org: Organization
isRetentionLimitEnforced: boolean
limitStatus: LimitStatus
}
interface DispatchProps {
createBucket: typeof createBucket
checkBucketLimits: typeof checkBucketLimitsAction
}
interface OwnProps {}
type Props = OwnProps & StateProps & DispatchProps
const SelectorListCreateBucket: FC<Props> = ({
org,
createBucket,
isRetentionLimitEnforced,
limitStatus,
checkBucketLimits,
}) => {
const triggerRef = useRef<HTMLButtonElement>(null)
const [state, dispatch] = useReducer(
createBucketReducer,
initialBucketState(isRetentionLimitEnforced, org.id)
)
useEffect(() => {
// Check bucket limits when component mounts
checkBucketLimits()
}, [])
const limitExceeded = limitStatus === LimitStatus.EXCEEDED
let selectorItemClassName = 'selector-list--item'
let titleText = 'Click to create a bucket'
let buttonDisabled = false
if (limitExceeded) {
selectorItemClassName = 'selector-list--item__disabled'
titleText = 'This account has the maximum number of buckets allowed'
buttonDisabled = true
}
const retentionRule = state.retentionRules.find(r => r.type === 'expire')
const retentionSeconds = retentionRule ? retentionRule.everySeconds : 3600
const handleChangeRuleType = (ruleType: RuleType): void => {
if (ruleType === 'expire') {
dispatch({type: 'updateRetentionRules', payload: DEFAULT_RULES})
} else {
dispatch({type: 'updateRetentionRules', payload: []})
}
dispatch({type: 'updateRuleType', payload: ruleType})
}
const handleChangeRetentionRule = (everySeconds: number): void => {
const retentionRules = [
{
type: 'expire',
everySeconds,
},
]
dispatch({type: 'updateRetentionRules', payload: retentionRules})
}
const handleSubmit = (onHide: () => void) => (
e: FormEvent<HTMLFormElement>
): void => {
e.preventDefault()
createBucket(state)
onHide()
}
const handleChangeInput = (e: ChangeEvent<HTMLInputElement>): void => {
const value = e.target.value
if (e.target.name === 'name') {
dispatch({type: 'updateName', payload: value})
}
}
return (
<>
<button
className={selectorItemClassName}
data-testid="selector-list add-bucket"
disabled={buttonDisabled}
title={titleText}
ref={triggerRef}
>
+ Create Bucket
</button>
<Popover
triggerRef={triggerRef}
appearance={Appearance.Outline}
color={ComponentColor.Primary}
position={PopoverPosition.Above}
showEvent={PopoverInteraction.Click}
hideEvent={PopoverInteraction.Click}
testID="create-bucket-popover"
contents={onHide => (
<BucketOverlayForm
name={state.name}
buttonText="Create"
disableRenaming={false}
ruleType={state.ruleType}
onClose={onHide}
onSubmit={handleSubmit(onHide)}
onChangeInput={handleChangeInput}
retentionSeconds={retentionSeconds}
onChangeRuleType={handleChangeRuleType}
onChangeRetentionRule={handleChangeRetentionRule}
/>
)}
/>
</>
)
}
const mstp = (state: AppState): StateProps => {
const org = getOrg(state)
const isRetentionLimitEnforced = !!extractBucketMaxRetentionSeconds(
state.cloud.limits
)
const limitStatus = extractBucketLimits(state.cloud.limits)
return {
org,
isRetentionLimitEnforced,
limitStatus,
}
}
const mdtp: DispatchProps = {
createBucket,
checkBucketLimits: checkBucketLimitsAction,
}
export default connect<StateProps, DispatchProps, OwnProps>(
mstp,
mdtp
)(SelectorListCreateBucket)

View File

@ -6,21 +6,23 @@ import {connect} from 'react-redux'
import BuilderCard from 'src/timeMachine/components/builderCard/BuilderCard'
import WaitingText from 'src/shared/components/WaitingText'
import SelectorList from 'src/timeMachine/components/SelectorList'
import SelectorListCreateBucket from 'src/timeMachine/components/SelectorListCreateBucket'
import {Input} from '@influxdata/clockface'
// Actions
import {selectBucket} from 'src/timeMachine/actions/queryBuilder'
// Utils
import {getActiveTimeMachine, getActiveQuery} from 'src/timeMachine/selectors'
import {getActiveQuery} from 'src/timeMachine/selectors'
import {getAll, getStatus} from 'src/resources/selectors'
// Types
import {AppState} from 'src/types'
import {AppState, Bucket, ResourceType} from 'src/types'
import {RemoteDataState} from 'src/types'
interface StateProps {
selectedBucket: string
buckets: string[]
bucketNames: string[]
bucketsStatus: RemoteDataState
}
@ -30,16 +32,17 @@ interface DispatchProps {
type Props = StateProps & DispatchProps
const fb = term => b => b.toLocaleLowerCase().includes(term.toLocaleLowerCase())
const fb = term => bucket =>
bucket.toLocaleLowerCase().includes(term.toLocaleLowerCase())
const BucketSelector: FunctionComponent<Props> = ({
selectedBucket,
buckets,
bucketNames,
bucketsStatus,
onSelectBucket,
}) => {
const [searchTerm, setSearchTerm] = useState('')
const list = buckets.filter(fb(searchTerm))
const list = bucketNames.filter(fb(searchTerm))
const onSelect = (bucket: string) => {
onSelectBucket(bucket, true)
@ -57,7 +60,7 @@ const BucketSelector: FunctionComponent<Props> = ({
)
}
if (bucketsStatus === RemoteDataState.Done && !buckets.length) {
if (bucketsStatus === RemoteDataState.Done && !bucketNames.length) {
return <BuilderCard.Empty>No buckets found</BuilderCard.Empty>
}
@ -97,16 +100,21 @@ const Selector: FunctionComponent<SelectorProps> = ({
selectedItems={[selected]}
onSelectItem={onSelect}
multiSelect={false}
/>
testID="buckets-list"
>
<SelectorListCreateBucket />
</SelectorList>
)
}
const mstp = (state: AppState) => {
const {buckets, bucketsStatus} = getActiveTimeMachine(state).queryBuilder
const buckets = getAll<Bucket>(state, ResourceType.Buckets)
const bucketNames = buckets.map(bucket => bucket.name || '')
const bucketsStatus = getStatus(state, ResourceType.Buckets)
const selectedBucket =
getActiveQuery(state).builderConfig.buckets[0] || buckets[0]
getActiveQuery(state).builderConfig.buckets[0] || bucketNames[0]
return {selectedBucket, buckets, bucketsStatus}
return {selectedBucket, bucketNames, bucketsStatus}
}
const mdtp = {