diff --git a/ui/package.json b/ui/package.json index ac7af05da..f4373fc91 100644 --- a/ui/package.json +++ b/ui/package.json @@ -140,6 +140,7 @@ "react": "^16.3.1", "react-addons-shallow-compare": "^15.0.2", "react-codemirror2": "^4.2.1", + "react-copy-to-clipboard": "^5.0.1", "react-custom-scrollbars": "^4.1.1", "react-dimensions": "^1.2.0", "react-dnd": "^2.6.0", diff --git a/ui/src/flux/components/DatabaseList.tsx b/ui/src/flux/components/DatabaseList.tsx index 73a8ef327..b93fafeb7 100644 --- a/ui/src/flux/components/DatabaseList.tsx +++ b/ui/src/flux/components/DatabaseList.tsx @@ -1,5 +1,6 @@ import React, {PureComponent} from 'react' +import {NotificationContext} from 'src/flux/containers/CheckServices' import DatabaseListItem from 'src/flux/components/DatabaseListItem' import {showDatabases} from 'src/shared/apis/metaQuery' @@ -49,7 +50,13 @@ class DatabaseList extends PureComponent { const {service} = this.props return databases.map(db => { - return + return ( + + {({notify}) => ( + + )} + + ) }) } } diff --git a/ui/src/flux/components/DatabaseListItem.tsx b/ui/src/flux/components/DatabaseListItem.tsx index b7d784aa8..1391f8b3c 100644 --- a/ui/src/flux/components/DatabaseListItem.tsx +++ b/ui/src/flux/components/DatabaseListItem.tsx @@ -1,14 +1,21 @@ import React, {PureComponent, ChangeEvent, MouseEvent} from 'react' import classnames from 'classnames' +import {CopyToClipboard} from 'react-copy-to-clipboard' import {tagKeys as fetchTagKeys} from 'src/shared/apis/flux/metaQueries' import parseValuesColumn from 'src/shared/parsing/flux/values' import TagList from 'src/flux/components/TagList' -import {Service} from 'src/types' +import { + notifyCopyToClipboardSuccess, + notifyCopyToClipboardFailed, +} from 'src/shared/copy/notifications' + +import {Service, NotificationAction} from 'src/types' interface Props { db: string service: Service + notify: NotificationAction } interface State { @@ -40,33 +47,24 @@ class DatabaseListItem extends PureComponent { } public render() { - const {db, service} = this.props - const {searchTerm} = this.state + const {db} = this.props return (
-
- {db} - Bucket -
- {this.state.isOpen && ( - <> -
- +
+
+ {db} + Bucket +
+ +
+ + Copy
- - - )} +
+
+ {this.filterAndTagList}
) } @@ -83,6 +81,47 @@ class DatabaseListItem extends PureComponent { }) } + private get filterAndTagList(): JSX.Element { + const {db, service} = this.props + const {isOpen, searchTerm} = this.state + + if (isOpen) { + return ( + <> +
+ +
+ + + ) + } + } + + private handleClickCopy = e => { + e.stopPropagation() + } + + private handleCopyAttempt = ( + copiedText: string, + isSuccessful: boolean + ): void => { + const {notify} = this.props + if (isSuccessful) { + notify(notifyCopyToClipboardSuccess(copiedText)) + } else { + notify(notifyCopyToClipboardFailed(copiedText)) + } + } + private onSearch = (e: ChangeEvent) => { this.setState({ searchTerm: e.target.value, diff --git a/ui/src/flux/components/TagList.tsx b/ui/src/flux/components/TagList.tsx index 389583a03..6290115ef 100644 --- a/ui/src/flux/components/TagList.tsx +++ b/ui/src/flux/components/TagList.tsx @@ -1,7 +1,9 @@ import React, {PureComponent, MouseEvent} from 'react' -import {SchemaFilter, Service} from 'src/types' import TagListItem from 'src/flux/components/TagListItem' +import {NotificationContext} from 'src/flux/containers/CheckServices' + +import {SchemaFilter, Service} from 'src/types' interface Props { db: string @@ -28,13 +30,17 @@ export default class TagList extends PureComponent { return ( <> {tags.map(t => ( - + + {({notify}) => ( + + )} + ))} ) diff --git a/ui/src/flux/components/TagListItem.tsx b/ui/src/flux/components/TagListItem.tsx index c9e32f3c0..1b070544c 100644 --- a/ui/src/flux/components/TagListItem.tsx +++ b/ui/src/flux/components/TagListItem.tsx @@ -6,6 +6,7 @@ import React, { } from 'react' import _ from 'lodash' +import {CopyToClipboard} from 'react-copy-to-clipboard' import {Service, SchemaFilter, RemoteDataState} from 'src/types' import {tagValues as fetchTagValues} from 'src/shared/apis/flux/metaQueries' @@ -14,12 +15,19 @@ import parseValuesColumn from 'src/shared/parsing/flux/values' import TagValueList from 'src/flux/components/TagValueList' import LoaderSkeleton from 'src/flux/components/LoaderSkeleton' import LoadingSpinner from 'src/flux/components/LoadingSpinner' +import { + notifyCopyToClipboardSuccess, + notifyCopyToClipboardFailed, +} from 'src/shared/copy/notifications' + +import {NotificationAction} from 'src/types' interface Props { tagKey: string db: string service: Service filter: SchemaFilter[] + notify: NotificationAction } interface State { @@ -61,9 +69,17 @@ export default class TagListItem extends PureComponent { return (
-
- {tagKey} - Tag Key +
+
+ {tagKey} + Tag Key{' '} +
+ +
+ + Copy +
+
{this.state.isOpen && ( <> @@ -220,6 +236,22 @@ export default class TagListItem extends PureComponent { ) } + private handleClickCopy = e => { + e.stopPropagation() + } + + private handleCopyAttempt = ( + copiedText: string, + isSuccessful: boolean + ): void => { + const {notify} = this.props + if (isSuccessful) { + notify(notifyCopyToClipboardSuccess(copiedText)) + } else { + notify(notifyCopyToClipboardFailed(copiedText)) + } + } + private async getCount() { const {service, db, filter, tagKey} = this.props const {limit, searchTerm} = this.state diff --git a/ui/src/flux/components/TagValueList.tsx b/ui/src/flux/components/TagValueList.tsx index 5fcd65432..a8933a911 100644 --- a/ui/src/flux/components/TagValueList.tsx +++ b/ui/src/flux/components/TagValueList.tsx @@ -2,6 +2,8 @@ import React, {PureComponent, MouseEvent} from 'react' import TagValueListItem from 'src/flux/components/TagValueListItem' import LoadingSpinner from 'src/flux/components/LoadingSpinner' +import {NotificationContext} from 'src/flux/containers/CheckServices' + import {Service, SchemaFilter} from 'src/types' interface Props { @@ -30,14 +32,19 @@ export default class TagValueList extends PureComponent { return ( <> {values.map((v, i) => ( - + + {({notify}) => ( + + )} + ))} {shouldShowMoreValues && (
diff --git a/ui/src/flux/components/TagValueListItem.tsx b/ui/src/flux/components/TagValueListItem.tsx index be7a4c3b3..e883b1197 100644 --- a/ui/src/flux/components/TagValueListItem.tsx +++ b/ui/src/flux/components/TagValueListItem.tsx @@ -1,10 +1,23 @@ import React, {PureComponent, MouseEvent, ChangeEvent} from 'react' +import {CopyToClipboard} from 'react-copy-to-clipboard' + import {tagKeys as fetchTagKeys} from 'src/shared/apis/flux/metaQueries' import parseValuesColumn from 'src/shared/parsing/flux/values' import TagList from 'src/flux/components/TagList' import LoaderSkeleton from 'src/flux/components/LoaderSkeleton' -import {Service, SchemaFilter, RemoteDataState} from 'src/types' + +import { + notifyCopyToClipboardSuccess, + notifyCopyToClipboardFailed, +} from 'src/shared/copy/notifications' + +import { + Service, + SchemaFilter, + RemoteDataState, + NotificationAction, +} from 'src/types' interface Props { db: string @@ -12,6 +25,7 @@ interface Props { tagKey: string value: string filter: SchemaFilter[] + notify: NotificationAction } interface State { @@ -39,9 +53,17 @@ class TagValueListItem extends PureComponent { return (
-
- {value} - Tag Value +
+
+ {value} + Tag Value +
+ +
+ + Copy +
+
{this.state.isOpen && ( <> @@ -127,6 +149,22 @@ class TagValueListItem extends PureComponent { this.setState({isOpen: !this.state.isOpen}) } + private handleClickCopy = e => { + e.stopPropagation() + } + + private handleCopyAttempt = ( + copiedText: string, + isSuccessful: boolean + ): void => { + const {notify} = this.props + if (isSuccessful) { + notify(notifyCopyToClipboardSuccess(copiedText)) + } else { + notify(notifyCopyToClipboardFailed(copiedText)) + } + } + private onSearch = (e: ChangeEvent) => { this.setState({ searchTerm: e.target.value, diff --git a/ui/src/flux/containers/CheckServices.tsx b/ui/src/flux/containers/CheckServices.tsx index 660c5a2aa..adc4def46 100644 --- a/ui/src/flux/containers/CheckServices.tsx +++ b/ui/src/flux/containers/CheckServices.tsx @@ -15,6 +15,8 @@ import { import * as a from 'src/shared/actions/overlayTechnology' import * as b from 'src/shared/actions/services' +export const NotificationContext = React.createContext() + const actions = {...a, ...b} interface Props { @@ -54,14 +56,16 @@ export class CheckServices extends PureComponent { } return ( - + + + ) } diff --git a/ui/src/shared/copy/notifications.ts b/ui/src/shared/copy/notifications.ts index 5dfb9fb6e..8713980b1 100644 --- a/ui/src/shared/copy/notifications.ts +++ b/ui/src/shared/copy/notifications.ts @@ -639,6 +639,17 @@ export const analyzeSuccess = { message: 'No errors found. Happy Happy Joy Joy!', } +export const notifyCopyToClipboardSuccess = text => ({ + ...defaultSuccessNotification, + icon: 'dash-h', + message: `'${text}' has been copied to clipboard.`, +}) + +export const notifyCopyToClipboardFailed = text => ({ + ...defaultErrorNotification, + message: `'${text}' was not copied to clipboard.`, +}) + // Service notifications export const couldNotGetServices = { ...defaultErrorNotification, diff --git a/ui/src/style/components/time-machine/flux-explorer.scss b/ui/src/style/components/time-machine/flux-explorer.scss index dac50389d..0b2e4109f 100644 --- a/ui/src/style/components/time-machine/flux-explorer.scss +++ b/ui/src/style/components/time-machine/flux-explorer.scss @@ -18,7 +18,7 @@ $flux-tree-line: 2px; flex-direction: column; align-items: stretch; padding-left: 0; - >.flux-schema-tree { + > .flux-schema-tree { padding-left: $flux-tree-indent; } } @@ -68,7 +68,7 @@ $flux-tree-line: 2px; color: $g11-sidewalk; white-space: nowrap; transition: color 0.25s ease, background-color 0.25s ease; - >span.icon { + > span.icon { position: absolute; top: 50%; left: $flux-tree-indent / 2; @@ -83,7 +83,7 @@ $flux-tree-line: 2px; background-color: $g17-whisper; } } - .expanded>& { + .expanded > & { color: $c-pool; .flux-schema-item-toggle:before, .flux-schema-item-toggle:after { @@ -120,17 +120,28 @@ $flux-tree-line: 2px; } } +.flex-schema-item-group { + display: flex; + flex: 1 0 0; + align-items: center; +} + @keyframes skeleton-animation { 0% { - background-position: 100% 50% + background-position: 100% 50%; } 100% { - background-position: 0% 50% + background-position: 0% 50%; } } .flux-schema-item-skeleton { - background: linear-gradient(70deg, $g4-onyx 0%, $g5-pepper 50%, $g4-onyx 100%); + background: linear-gradient( + 70deg, + $g4-onyx 0%, + $g5-pepper 50%, + $g4-onyx 100% + ); border-radius: 4px; height: 60%; background-size: 400% 400%; @@ -196,7 +207,24 @@ $flux-tree-line: 2px; } } -.flux-schema-tree>.flux-schema--filter { +.flux-schema-copy { + color: $g11-sidewalk; + display: inline-block; + opacity: 0; + transition: opacity 0.25s ease; + margin-left: 8px; + .flux-schema-item:hover & { + opacity: 1; + } + > span.icon { + margin-right: 3px; + } + &:hover { + color: $g15-platinum; + } +} + +.flux-schema-tree > .flux-schema--filter { margin-left: $flux-tree-indent / 2; margin-right: $flux-tree-indent / 2; margin-top: 1px; @@ -204,12 +232,12 @@ $flux-tree-line: 2px; max-width: 220px; } -.flux-schema--filter>input.input-sm { +.flux-schema--filter > input.input-sm { height: 25px; font-size: 12px; } -.tag-value-list--header>.flux-schema--filter { +.tag-value-list--header > .flux-schema--filter { display: inline-block; margin-right: 15px; } @@ -232,7 +260,7 @@ $flux-tree-line: 2px; margin: 0 auto; } -.loading-spinner .spinner>div { +.loading-spinner .spinner > div { width: 8px; height: 8px; background-color: $g8-storm; @@ -253,10 +281,10 @@ $flux-tree-line: 2px; 0%, 80%, 100% { - -webkit-transform: scale(0) + -webkit-transform: scale(0); } 40% { - -webkit-transform: scale(1.0) + -webkit-transform: scale(1); } } @@ -268,7 +296,7 @@ $flux-tree-line: 2px; transform: scale(0); } 40% { - -webkit-transform: scale(1.0); - transform: scale(1.0); + -webkit-transform: scale(1); + transform: scale(1); } } diff --git a/ui/src/types/index.ts b/ui/src/types/index.ts index 2c7dc702b..043abbcb2 100644 --- a/ui/src/types/index.ts +++ b/ui/src/types/index.ts @@ -21,7 +21,11 @@ import { import {AlertRule, Kapacitor, Task} from './kapacitor' import {Source, SourceLinks} from './sources' import {DropdownAction, DropdownItem} from './shared' -import {Notification, NotificationFunc} from './notifications' +import { + Notification, + NotificationFunc, + NotificationAction, +} from './notifications' import {FluxTable, ScriptStatus, SchemaFilter, RemoteDataState} from './flux' export { @@ -57,6 +61,7 @@ export { Task, Notification, NotificationFunc, + NotificationAction, Axes, Dashboard, Service, diff --git a/ui/src/types/notifications.ts b/ui/src/types/notifications.ts index 6107a58e2..622b1b3b2 100644 --- a/ui/src/types/notifications.ts +++ b/ui/src/types/notifications.ts @@ -7,3 +7,5 @@ export interface Notification { } export type NotificationFunc = (message: any) => Notification + +export type NotificationAction = (message: Notification) => void