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 managementpull/17878/head
parent
5c00430e0b
commit
d56966face
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export type OverlayID =
|
|||
| 'telegraf-config'
|
||||
| 'telegraf-output'
|
||||
| 'switch-organizations'
|
||||
| 'create-bucket'
|
||||
|
||||
export interface OverlayParams {
|
||||
[key: string]: string
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue