feat(ui): expose bucketid (#18319)

* refactor: extract bucket card actions and meta into separate components

* feat: allow bucket ID to be copied

* chore: cleanup

* chore: update changelog

* feat: add copiable ID to demo data bucket cards
pull/18377/head
alexpaxton 2020-06-04 15:12:14 -07:00 committed by GitHub
parent 17791301ab
commit 5dfeb0ad82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 303 additions and 144 deletions

View File

@ -14,6 +14,10 @@
1. [18335](https://github.com/influxdata/influxdb/pull/18335): Disable failing when providing an unexpected error to influx CLI
1. [18345](https://github.com/influxdata/influxdb/pull/18345): Have influx delete cmd respect the config
### UI Improvements
1. [18319](https://github.com/influxdata/influxdb/pull/18319): Display bucket ID in bucket list and enable 1 click copying
## v2.0.0-beta.11 [2020-05-26]
### Features

View File

@ -1,40 +1,19 @@
// Libraries
import React, {FC} from 'react'
import {withRouter, WithRouterProps} from 'react-router'
import {connect} from 'react-redux'
import _ from 'lodash'
// Components
import {
Button,
ResourceCard,
FlexBox,
FlexDirection,
ComponentSize,
} from '@influxdata/clockface'
import {ResourceCard} from '@influxdata/clockface'
import BucketContextMenu from 'src/buckets/components/BucketContextMenu'
import BucketAddDataButton from 'src/buckets/components/BucketAddDataButton'
import InlineLabels from 'src/shared/components/inlineLabels/InlineLabels'
import {FeatureFlag} from 'src/shared/utils/featureFlag'
import BucketCardMeta from 'src/buckets/components/BucketCardMeta'
import BucketCardActions from 'src/buckets/components/BucketCardActions'
// Constants
import {isSystemBucket} from 'src/buckets/constants/index'
// Actions
import {addBucketLabel, deleteBucketLabel} from 'src/buckets/actions/thunks'
import {setBucketInfo} from 'src/dataLoaders/actions/steps'
import {setDataLoadersType} from 'src/dataLoaders/actions/dataLoaders'
// Types
import {Label, OwnBucket} from 'src/types'
import {DataLoaderType} from 'src/types/dataLoaders'
interface DispatchProps {
onAddBucketLabel: typeof addBucketLabel
onDeleteBucketLabel: typeof deleteBucketLabel
onSetDataLoadersBucket: typeof setBucketInfo
onSetDataLoadersType: typeof setDataLoadersType
}
import {OwnBucket} from 'src/types'
interface Props {
bucket: OwnBucket
@ -44,123 +23,18 @@ interface Props {
onFilterChange: (searchTerm: string) => void
}
const BucketCard: FC<Props & WithRouterProps & DispatchProps> = ({
const BucketCard: FC<Props & WithRouterProps> = ({
bucket,
onDeleteBucket,
onFilterChange,
onAddBucketLabel,
onDeleteBucketLabel,
onDeleteData,
router,
params: {orgID},
onSetDataLoadersBucket,
onSetDataLoadersType,
}) => {
const handleAddLabel = (label: Label) => {
onAddBucketLabel(bucket.id, label)
}
const handleRemoveLabel = (label: Label) => {
onDeleteBucketLabel(bucket.id, label)
}
const handleClickSettings = () => {
router.push(`/orgs/${orgID}/load-data/buckets/${bucket.id}/edit`)
}
const handleNameClick = () => {
router.push(`/orgs/${orgID}/data-explorer?bucket=${bucket.name}`)
}
const handleAddCollector = () => {
onSetDataLoadersBucket(orgID, bucket.name, bucket.id)
onSetDataLoadersType(DataLoaderType.Streaming)
router.push(`/orgs/${orgID}/load-data/buckets/${bucket.id}/telegrafs/new`)
}
const handleAddLineProtocol = () => {
onSetDataLoadersBucket(orgID, bucket.name, bucket.id)
onSetDataLoadersType(DataLoaderType.LineProtocol)
router.push(
`/orgs/${orgID}/load-data/buckets/${bucket.id}/line-protocols/new`
)
}
const handleAddClientLibrary = (): void => {
onSetDataLoadersBucket(orgID, bucket.name, bucket.id)
onSetDataLoadersType(DataLoaderType.ClientLibrary)
router.push(`/orgs/${orgID}/load-data/client-libraries`)
}
const handleAddScraper = () => {
onSetDataLoadersBucket(orgID, bucket.name, bucket.id)
onSetDataLoadersType(DataLoaderType.Scraping)
router.push(`/orgs/${orgID}/load-data/buckets/${bucket.id}/scrapers/new`)
}
const actionButtons = (
<FlexBox
direction={FlexDirection.Row}
margin={ComponentSize.Small}
style={{marginTop: '4px'}}
>
<InlineLabels
selectedLabelIDs={bucket.labels}
onFilterChange={onFilterChange}
onAddLabel={handleAddLabel}
onRemoveLabel={handleRemoveLabel}
/>
<BucketAddDataButton
onAddCollector={handleAddCollector}
onAddLineProtocol={handleAddLineProtocol}
onAddClientLibrary={handleAddClientLibrary}
onAddScraper={handleAddScraper}
/>
<Button
text="Settings"
testID="bucket-settings"
size={ComponentSize.ExtraSmall}
onClick={handleClickSettings}
/>
<FeatureFlag name="deleteWithPredicate">
<Button
text="Delete Data By Filter"
testID="bucket-delete-bucket"
size={ComponentSize.ExtraSmall}
onClick={() => onDeleteData(bucket)}
/>
</FeatureFlag>
</FlexBox>
)
let cardMeta = (
<ResourceCard.Meta>
<span data-testid="bucket-retention">
Retention: {_.capitalize(bucket.readableRetention)}
</span>
</ResourceCard.Meta>
)
if (bucket.type !== 'user') {
cardMeta = (
<ResourceCard.Meta>
<span
className="system-bucket"
key={`system-bucket-indicator-${bucket.id}`}
>
System Bucket
</span>
<span data-testid="bucket-retention">
Retention: {_.capitalize(bucket.readableRetention)}
</span>
</ResourceCard.Meta>
)
}
return (
<ResourceCard
testID={`bucket-card ${bucket.name}`}
@ -175,20 +49,16 @@ const BucketCard: FC<Props & WithRouterProps & DispatchProps> = ({
onClick={handleNameClick}
name={bucket.name}
/>
{cardMeta}
{bucket.type === 'user' && actionButtons}
<BucketCardMeta bucket={bucket} />
<BucketCardActions
bucket={bucket}
orgID={orgID}
bucketType={bucket.type}
onDeleteData={onDeleteData}
onFilterChange={onFilterChange}
/>
</ResourceCard>
)
}
const mdtp: DispatchProps = {
onAddBucketLabel: addBucketLabel,
onDeleteBucketLabel: deleteBucketLabel,
onSetDataLoadersBucket: setBucketInfo,
onSetDataLoadersType: setDataLoadersType,
}
export default connect<{}, DispatchProps>(
null,
mdtp
)(withRouter<Props>(BucketCard))
export default withRouter<Props>(BucketCard)

View File

@ -0,0 +1,146 @@
// Libraries
import React, {FC} from 'react'
import {withRouter, WithRouterProps} from 'react-router'
import {connect} from 'react-redux'
import _ from 'lodash'
// Components
import {
Button,
FlexBox,
FlexDirection,
ComponentSize,
} from '@influxdata/clockface'
import BucketAddDataButton from 'src/buckets/components/BucketAddDataButton'
import InlineLabels from 'src/shared/components/inlineLabels/InlineLabels'
import {FeatureFlag} from 'src/shared/utils/featureFlag'
// Actions
import {addBucketLabel, deleteBucketLabel} from 'src/buckets/actions/thunks'
import {setBucketInfo} from 'src/dataLoaders/actions/steps'
import {setDataLoadersType} from 'src/dataLoaders/actions/dataLoaders'
// Types
import {Label, OwnBucket} from 'src/types'
import {DataLoaderType} from 'src/types/dataLoaders'
interface DispatchProps {
onAddBucketLabel: typeof addBucketLabel
onDeleteBucketLabel: typeof deleteBucketLabel
onSetDataLoadersBucket: typeof setBucketInfo
onSetDataLoadersType: typeof setDataLoadersType
}
interface Props {
bucket: OwnBucket
bucketType: 'user' | 'system'
orgID: string
onDeleteData: (b: OwnBucket) => void
onFilterChange: (searchTerm: string) => void
}
const BucketCardActions: FC<Props & WithRouterProps & DispatchProps> = ({
bucket,
bucketType,
orgID,
onFilterChange,
onAddBucketLabel,
onDeleteBucketLabel,
onDeleteData,
router,
onSetDataLoadersBucket,
onSetDataLoadersType,
}) => {
if (bucketType === 'system') {
return null
}
const handleAddLabel = (label: Label) => {
onAddBucketLabel(bucket.id, label)
}
const handleRemoveLabel = (label: Label) => {
onDeleteBucketLabel(bucket.id, label)
}
const handleClickSettings = () => {
router.push(`/orgs/${orgID}/load-data/buckets/${bucket.id}/edit`)
}
const handleAddCollector = () => {
onSetDataLoadersBucket(orgID, bucket.name, bucket.id)
onSetDataLoadersType(DataLoaderType.Streaming)
router.push(`/orgs/${orgID}/load-data/buckets/${bucket.id}/telegrafs/new`)
}
const handleAddLineProtocol = () => {
onSetDataLoadersBucket(orgID, bucket.name, bucket.id)
onSetDataLoadersType(DataLoaderType.LineProtocol)
router.push(
`/orgs/${orgID}/load-data/buckets/${bucket.id}/line-protocols/new`
)
}
const handleAddClientLibrary = (): void => {
onSetDataLoadersBucket(orgID, bucket.name, bucket.id)
onSetDataLoadersType(DataLoaderType.ClientLibrary)
router.push(`/orgs/${orgID}/load-data/client-libraries`)
}
const handleAddScraper = () => {
onSetDataLoadersBucket(orgID, bucket.name, bucket.id)
onSetDataLoadersType(DataLoaderType.Scraping)
router.push(`/orgs/${orgID}/load-data/buckets/${bucket.id}/scrapers/new`)
}
return (
<FlexBox
direction={FlexDirection.Row}
margin={ComponentSize.Small}
style={{marginTop: '4px'}}
>
<InlineLabels
selectedLabelIDs={bucket.labels}
onFilterChange={onFilterChange}
onAddLabel={handleAddLabel}
onRemoveLabel={handleRemoveLabel}
/>
<BucketAddDataButton
onAddCollector={handleAddCollector}
onAddLineProtocol={handleAddLineProtocol}
onAddClientLibrary={handleAddClientLibrary}
onAddScraper={handleAddScraper}
/>
<Button
text="Settings"
testID="bucket-settings"
size={ComponentSize.ExtraSmall}
onClick={handleClickSettings}
/>
<FeatureFlag name="deleteWithPredicate">
<Button
text="Delete Data By Filter"
testID="bucket-delete-bucket"
size={ComponentSize.ExtraSmall}
onClick={() => onDeleteData(bucket)}
/>
</FeatureFlag>
</FlexBox>
)
}
const mdtp: DispatchProps = {
onAddBucketLabel: addBucketLabel,
onDeleteBucketLabel: deleteBucketLabel,
onSetDataLoadersBucket: setBucketInfo,
onSetDataLoadersType: setDataLoadersType,
}
export default connect<{}, DispatchProps>(
null,
mdtp
)(withRouter<Props>(BucketCardActions))

View File

@ -0,0 +1,20 @@
.copy-bucket-id {
transition: color 0.25s ease;
&:hover {
cursor: pointer;
color: $c-pool;
}
}
.copy-bucket-id--helper {
color: $g13-mist;
transition: opacity 0.25s ease;
opacity: 0;
display: inline-block;
margin-left: $cf-marg-b;
}
.copy-bucket-id:hover .copy-bucket-id--helper {
opacity: 1;
}

View File

@ -0,0 +1,87 @@
// Libraries
import React, {FC} from 'react'
import CopyToClipboard from 'react-copy-to-clipboard'
import {capitalize} from 'lodash'
import {connect} from 'react-redux'
// Constants
import {
copyToClipboardSuccess,
copyToClipboardFailed,
} from 'src/shared/copy/notifications'
// Actions
import {notify as notifyAction} from 'src/shared/actions/notifications'
// Components
import {ResourceCard} from '@influxdata/clockface'
// Types
import {OwnBucket} from 'src/types'
interface DispatchProps {
notify: typeof notifyAction
}
interface OwnProps {
bucket: OwnBucket
}
type Props = OwnProps & DispatchProps
const BucketCardMeta: FC<Props> = ({bucket, notify}) => {
const handleCopyAttempt = (
copiedText: string,
isSuccessful: boolean
): void => {
const text = copiedText.slice(0, 30).trimRight()
const truncatedText = `${text}...`
if (isSuccessful) {
notify(copyToClipboardSuccess(truncatedText, 'Bucket ID'))
} else {
notify(copyToClipboardFailed(truncatedText, 'Bucket ID'))
}
}
const persistentBucketMeta = (
<span data-testid="bucket-retention">
Retention: {capitalize(bucket.readableRetention)}
</span>
)
if (bucket.type === 'system') {
return (
<ResourceCard.Meta>
<span
className="system-bucket"
key={`system-bucket-indicator-${bucket.id}`}
>
System Bucket
</span>
{persistentBucketMeta}
</ResourceCard.Meta>
)
}
return (
<ResourceCard.Meta>
{persistentBucketMeta}
<CopyToClipboard text={bucket.id} onCopy={handleCopyAttempt}>
<span className="copy-bucket-id" title="Click to Copy to Clipboard">
ID: {bucket.id}
<span className="copy-bucket-id--helper">Copy to Clipboard</span>
</span>
</CopyToClipboard>
</ResourceCard.Meta>
)
}
const mdtp: DispatchProps = {
notify: notifyAction,
}
export default connect<{}, DispatchProps, OwnProps>(
null,
mdtp
)(BucketCardMeta)

View File

@ -1,8 +1,15 @@
// Libraries
import React, {FC} from 'react'
import {withRouter, WithRouterProps} from 'react-router'
import CopyToClipboard from 'react-copy-to-clipboard'
import {connect} from 'react-redux'
// Constants
import {
copyToClipboardSuccess,
copyToClipboardFailed,
} from 'src/shared/copy/notifications'
// Components
import {
ResourceCard,
@ -20,12 +27,14 @@ import {Context} from 'src/clockface'
// Actions
import {deleteDemoDataBucketMembership} from 'src/cloud/actions/demodata'
import {notify as notifyAction} from 'src/shared/actions/notifications'
// Types
import {DemoBucket} from 'src/types'
interface DispatchProps {
removeBucket: typeof deleteDemoDataBucketMembership
notify: typeof notifyAction
}
interface Props {
@ -37,11 +46,26 @@ const DemoDataBucketCard: FC<Props & WithRouterProps & DispatchProps> = ({
router,
params: {orgID},
removeBucket,
notify,
}) => {
const handleNameClick = () => {
router.push(`/orgs/${orgID}/data-explorer?bucket=${bucket.name}`)
}
const handleCopyAttempt = (
copiedText: string,
isSuccessful: boolean
): void => {
const text = copiedText.slice(0, 30).trimRight()
const truncatedText = `${text}...`
if (isSuccessful) {
notify(copyToClipboardSuccess(truncatedText, 'Bucket ID'))
} else {
notify(copyToClipboardFailed(truncatedText, 'Bucket ID'))
}
}
return (
<ResourceCard
testID={`bucket-card ${bucket.name}`}
@ -83,6 +107,12 @@ const DemoDataBucketCard: FC<Props & WithRouterProps & DispatchProps> = ({
Demo Data Bucket
</span>
<>Retention: {bucket.readableRetention}</>
<CopyToClipboard text={bucket.id} onCopy={handleCopyAttempt}>
<span className="copy-bucket-id" title="Click to Copy to Clipboard">
ID: {bucket.id}
<span className="copy-bucket-id--helper">Copy to Clipboard</span>
</span>
</CopyToClipboard>
</ResourceCard.Meta>
<FlexBox
direction={FlexDirection.Row}
@ -105,6 +135,7 @@ const DemoDataBucketCard: FC<Props & WithRouterProps & DispatchProps> = ({
const mdtp: DispatchProps = {
removeBucket: deleteDemoDataBucketMembership,
notify: notifyAction,
}
export default connect<{}, DispatchProps, {}>(

View File

@ -123,6 +123,7 @@
@import 'src/dashboards/components/DashboardsCardGrid.scss';
@import 'src/dashboards/components/DashboardLightMode.scss';
@import 'src/buckets/components/DemoDataDropdown.scss';
@import 'src/buckets/components/BucketCardMeta.scss';
@import 'src/shared/components/notifications/Notification.scss';
// External