Merge pull request #3575 from influxdata/ifql/copy-paste-schema

flux/Copy to clipboard in schema explorer
pull/3590/head
Iris Scholten 2018-06-06 14:14:43 -07:00 committed by GitHub
commit 4cc0c93fd2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 250 additions and 70 deletions

View File

@ -140,6 +140,7 @@
"react": "^16.3.1", "react": "^16.3.1",
"react-addons-shallow-compare": "^15.0.2", "react-addons-shallow-compare": "^15.0.2",
"react-codemirror2": "^4.2.1", "react-codemirror2": "^4.2.1",
"react-copy-to-clipboard": "^5.0.1",
"react-custom-scrollbars": "^4.1.1", "react-custom-scrollbars": "^4.1.1",
"react-dimensions": "^1.2.0", "react-dimensions": "^1.2.0",
"react-dnd": "^2.6.0", "react-dnd": "^2.6.0",

View File

@ -1,5 +1,6 @@
import React, {PureComponent} from 'react' import React, {PureComponent} from 'react'
import {NotificationContext} from 'src/flux/containers/CheckServices'
import DatabaseListItem from 'src/flux/components/DatabaseListItem' import DatabaseListItem from 'src/flux/components/DatabaseListItem'
import {showDatabases} from 'src/shared/apis/metaQuery' import {showDatabases} from 'src/shared/apis/metaQuery'
@ -49,7 +50,13 @@ class DatabaseList extends PureComponent<Props, State> {
const {service} = this.props const {service} = this.props
return databases.map(db => { return databases.map(db => {
return <DatabaseListItem db={db} key={db} service={service} /> return (
<NotificationContext.Consumer key={db}>
{({notify}) => (
<DatabaseListItem db={db} service={service} notify={notify} />
)}
</NotificationContext.Consumer>
)
}) })
} }
} }

View File

@ -1,14 +1,21 @@
import React, {PureComponent, ChangeEvent, MouseEvent} from 'react' import React, {PureComponent, ChangeEvent, MouseEvent} from 'react'
import classnames from 'classnames' import classnames from 'classnames'
import {CopyToClipboard} from 'react-copy-to-clipboard'
import {tagKeys as fetchTagKeys} from 'src/shared/apis/flux/metaQueries' import {tagKeys as fetchTagKeys} from 'src/shared/apis/flux/metaQueries'
import parseValuesColumn from 'src/shared/parsing/flux/values' import parseValuesColumn from 'src/shared/parsing/flux/values'
import TagList from 'src/flux/components/TagList' 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 { interface Props {
db: string db: string
service: Service service: Service
notify: NotificationAction
} }
interface State { interface State {
@ -40,17 +47,46 @@ class DatabaseListItem extends PureComponent<Props, State> {
} }
public render() { public render() {
const {db, service} = this.props const {db} = this.props
const {searchTerm} = this.state
return ( return (
<div className={this.className} onClick={this.handleClick}> <div className={this.className} onClick={this.handleClick}>
<div className="flux-schema-item"> <div className="flux-schema-item">
<div className="flex-schema-item-group">
<div className="flux-schema-item-toggle" /> <div className="flux-schema-item-toggle" />
{db} {db}
<span className="flux-schema-type">Bucket</span> <span className="flux-schema-type">Bucket</span>
</div> </div>
{this.state.isOpen && ( <CopyToClipboard text={db} onCopy={this.handleCopyAttempt}>
<div className="flux-schema-copy" onClick={this.handleClickCopy}>
<span className="icon duplicate" title="copy to clipboard" />
Copy
</div>
</CopyToClipboard>
</div>
{this.filterAndTagList}
</div>
)
}
private get tags(): string[] {
const {tags, searchTerm} = this.state
const term = searchTerm.toLocaleLowerCase()
return tags.filter(t => t.toLocaleLowerCase().includes(term))
}
private get className(): string {
return classnames('flux-schema-tree', {
expanded: this.state.isOpen,
})
}
private get filterAndTagList(): JSX.Element {
const {db, service} = this.props
const {isOpen, searchTerm} = this.state
if (isOpen) {
return (
<> <>
<div className="flux-schema--filter"> <div className="flux-schema--filter">
<input <input
@ -66,21 +102,24 @@ class DatabaseListItem extends PureComponent<Props, State> {
</div> </div>
<TagList db={db} service={service} tags={this.tags} filter={[]} /> <TagList db={db} service={service} tags={this.tags} filter={[]} />
</> </>
)}
</div>
) )
} }
private get tags(): string[] {
const {tags, searchTerm} = this.state
const term = searchTerm.toLocaleLowerCase()
return tags.filter(t => t.toLocaleLowerCase().includes(term))
} }
private get className(): string { private handleClickCopy = e => {
return classnames('flux-schema-tree', { e.stopPropagation()
expanded: this.state.isOpen, }
})
private handleCopyAttempt = (
copiedText: string,
isSuccessful: boolean
): void => {
const {notify} = this.props
if (isSuccessful) {
notify(notifyCopyToClipboardSuccess(copiedText))
} else {
notify(notifyCopyToClipboardFailed(copiedText))
}
} }
private onSearch = (e: ChangeEvent<HTMLInputElement>) => { private onSearch = (e: ChangeEvent<HTMLInputElement>) => {

View File

@ -1,7 +1,9 @@
import React, {PureComponent, MouseEvent} from 'react' import React, {PureComponent, MouseEvent} from 'react'
import {SchemaFilter, Service} from 'src/types'
import TagListItem from 'src/flux/components/TagListItem' import TagListItem from 'src/flux/components/TagListItem'
import {NotificationContext} from 'src/flux/containers/CheckServices'
import {SchemaFilter, Service} from 'src/types'
interface Props { interface Props {
db: string db: string
@ -28,13 +30,17 @@ export default class TagList extends PureComponent<Props, State> {
return ( return (
<> <>
{tags.map(t => ( {tags.map(t => (
<NotificationContext.Consumer key={t}>
{({notify}) => (
<TagListItem <TagListItem
key={t}
db={db} db={db}
tagKey={t} tagKey={t}
service={service} service={service}
filter={filter} filter={filter}
notify={notify}
/> />
)}
</NotificationContext.Consumer>
))} ))}
</> </>
) )

View File

@ -6,6 +6,7 @@ import React, {
} from 'react' } from 'react'
import _ from 'lodash' import _ from 'lodash'
import {CopyToClipboard} from 'react-copy-to-clipboard'
import {Service, SchemaFilter, RemoteDataState} from 'src/types' import {Service, SchemaFilter, RemoteDataState} from 'src/types'
import {tagValues as fetchTagValues} from 'src/shared/apis/flux/metaQueries' 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 TagValueList from 'src/flux/components/TagValueList'
import LoaderSkeleton from 'src/flux/components/LoaderSkeleton' import LoaderSkeleton from 'src/flux/components/LoaderSkeleton'
import LoadingSpinner from 'src/flux/components/LoadingSpinner' import LoadingSpinner from 'src/flux/components/LoadingSpinner'
import {
notifyCopyToClipboardSuccess,
notifyCopyToClipboardFailed,
} from 'src/shared/copy/notifications'
import {NotificationAction} from 'src/types'
interface Props { interface Props {
tagKey: string tagKey: string
db: string db: string
service: Service service: Service
filter: SchemaFilter[] filter: SchemaFilter[]
notify: NotificationAction
} }
interface State { interface State {
@ -61,9 +69,17 @@ export default class TagListItem extends PureComponent<Props, State> {
return ( return (
<div className={this.className}> <div className={this.className}>
<div className="flux-schema-item" onClick={this.handleClick}> <div className="flux-schema-item" onClick={this.handleClick}>
<div className="flex-schema-item-group">
<div className="flux-schema-item-toggle" /> <div className="flux-schema-item-toggle" />
{tagKey} {tagKey}
<span className="flux-schema-type">Tag Key</span> <span className="flux-schema-type">Tag Key</span>{' '}
</div>
<CopyToClipboard text={tagKey} onCopy={this.handleCopyAttempt}>
<div className="flux-schema-copy" onClick={this.handleClickCopy}>
<span className="icon duplicate" title="copy to clipboard" />
Copy
</div>
</CopyToClipboard>
</div> </div>
{this.state.isOpen && ( {this.state.isOpen && (
<> <>
@ -220,6 +236,22 @@ export default class TagListItem extends PureComponent<Props, State> {
) )
} }
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() { private async getCount() {
const {service, db, filter, tagKey} = this.props const {service, db, filter, tagKey} = this.props
const {limit, searchTerm} = this.state const {limit, searchTerm} = this.state

View File

@ -2,6 +2,8 @@ import React, {PureComponent, MouseEvent} from 'react'
import TagValueListItem from 'src/flux/components/TagValueListItem' import TagValueListItem from 'src/flux/components/TagValueListItem'
import LoadingSpinner from 'src/flux/components/LoadingSpinner' import LoadingSpinner from 'src/flux/components/LoadingSpinner'
import {NotificationContext} from 'src/flux/containers/CheckServices'
import {Service, SchemaFilter} from 'src/types' import {Service, SchemaFilter} from 'src/types'
interface Props { interface Props {
@ -30,6 +32,8 @@ export default class TagValueList extends PureComponent<Props> {
return ( return (
<> <>
{values.map((v, i) => ( {values.map((v, i) => (
<NotificationContext.Consumer key={v}>
{({notify}) => (
<TagValueListItem <TagValueListItem
key={i} key={i}
db={db} db={db}
@ -37,7 +41,10 @@ export default class TagValueList extends PureComponent<Props> {
tagKey={tagKey} tagKey={tagKey}
service={service} service={service}
filter={filter} filter={filter}
notify={notify}
/> />
)}
</NotificationContext.Consumer>
))} ))}
{shouldShowMoreValues && ( {shouldShowMoreValues && (
<div className="flux-schema-tree flux-tree-node"> <div className="flux-schema-tree flux-tree-node">

View File

@ -1,10 +1,23 @@
import React, {PureComponent, MouseEvent, ChangeEvent} from 'react' 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 {tagKeys as fetchTagKeys} from 'src/shared/apis/flux/metaQueries'
import parseValuesColumn from 'src/shared/parsing/flux/values' import parseValuesColumn from 'src/shared/parsing/flux/values'
import TagList from 'src/flux/components/TagList' import TagList from 'src/flux/components/TagList'
import LoaderSkeleton from 'src/flux/components/LoaderSkeleton' 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 { interface Props {
db: string db: string
@ -12,6 +25,7 @@ interface Props {
tagKey: string tagKey: string
value: string value: string
filter: SchemaFilter[] filter: SchemaFilter[]
notify: NotificationAction
} }
interface State { interface State {
@ -39,10 +53,18 @@ class TagValueListItem extends PureComponent<Props, State> {
return ( return (
<div className={this.className} onClick={this.handleClick}> <div className={this.className} onClick={this.handleClick}>
<div className="flux-schema-item"> <div className="flux-schema-item">
<div className="flex-schema-item-group">
<div className="flux-schema-item-toggle" /> <div className="flux-schema-item-toggle" />
{value} {value}
<span className="flux-schema-type">Tag Value</span> <span className="flux-schema-type">Tag Value</span>
</div> </div>
<CopyToClipboard text={value} onCopy={this.handleCopyAttempt}>
<div className="flux-schema-copy" onClick={this.handleClickCopy}>
<span className="icon duplicate" title="copy to clipboard" />
Copy
</div>
</CopyToClipboard>
</div>
{this.state.isOpen && ( {this.state.isOpen && (
<> <>
{this.isLoading && <LoaderSkeleton />} {this.isLoading && <LoaderSkeleton />}
@ -127,6 +149,22 @@ class TagValueListItem extends PureComponent<Props, State> {
this.setState({isOpen: !this.state.isOpen}) 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<HTMLInputElement>) => { private onSearch = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({ this.setState({
searchTerm: e.target.value, searchTerm: e.target.value,

View File

@ -15,6 +15,8 @@ import {
import * as a from 'src/shared/actions/overlayTechnology' import * as a from 'src/shared/actions/overlayTechnology'
import * as b from 'src/shared/actions/services' import * as b from 'src/shared/actions/services'
export const NotificationContext = React.createContext()
const actions = {...a, ...b} const actions = {...a, ...b}
interface Props { interface Props {
@ -54,6 +56,7 @@ export class CheckServices extends PureComponent<Props & WithRouterProps> {
} }
return ( return (
<NotificationContext.Provider value={{notify}}>
<FluxPage <FluxPage
source={this.source} source={this.source}
services={services} services={services}
@ -62,6 +65,7 @@ export class CheckServices extends PureComponent<Props & WithRouterProps> {
notify={notify} notify={notify}
updateScript={updateScript} updateScript={updateScript}
/> />
</NotificationContext.Provider>
) )
} }

View File

@ -639,6 +639,17 @@ export const analyzeSuccess = {
message: 'No errors found. Happy Happy Joy Joy!', 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 // Service notifications
export const couldNotGetServices = { export const couldNotGetServices = {
...defaultErrorNotification, ...defaultErrorNotification,

View File

@ -120,17 +120,28 @@ $flux-tree-line: 2px;
} }
} }
.flex-schema-item-group {
display: flex;
flex: 1 0 0;
align-items: center;
}
@keyframes skeleton-animation { @keyframes skeleton-animation {
0% { 0% {
background-position: 100% 50% background-position: 100% 50%;
} }
100% { 100% {
background-position: 0% 50% background-position: 0% 50%;
} }
} }
.flux-schema-item-skeleton { .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; border-radius: 4px;
height: 60%; height: 60%;
background-size: 400% 400%; background-size: 400% 400%;
@ -196,6 +207,23 @@ $flux-tree-line: 2px;
} }
} }
.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 { .flux-schema-tree > .flux-schema--filter {
margin-left: $flux-tree-indent / 2; margin-left: $flux-tree-indent / 2;
margin-right: $flux-tree-indent / 2; margin-right: $flux-tree-indent / 2;
@ -253,10 +281,10 @@ $flux-tree-line: 2px;
0%, 0%,
80%, 80%,
100% { 100% {
-webkit-transform: scale(0) -webkit-transform: scale(0);
} }
40% { 40% {
-webkit-transform: scale(1.0) -webkit-transform: scale(1);
} }
} }
@ -268,7 +296,7 @@ $flux-tree-line: 2px;
transform: scale(0); transform: scale(0);
} }
40% { 40% {
-webkit-transform: scale(1.0); -webkit-transform: scale(1);
transform: scale(1.0); transform: scale(1);
} }
} }

View File

@ -21,7 +21,11 @@ import {
import {AlertRule, Kapacitor, Task} from './kapacitor' import {AlertRule, Kapacitor, Task} from './kapacitor'
import {Source, SourceLinks} from './sources' import {Source, SourceLinks} from './sources'
import {DropdownAction, DropdownItem} from './shared' import {DropdownAction, DropdownItem} from './shared'
import {Notification, NotificationFunc} from './notifications' import {
Notification,
NotificationFunc,
NotificationAction,
} from './notifications'
import {FluxTable, ScriptStatus, SchemaFilter, RemoteDataState} from './flux' import {FluxTable, ScriptStatus, SchemaFilter, RemoteDataState} from './flux'
export { export {
@ -57,6 +61,7 @@ export {
Task, Task,
Notification, Notification,
NotificationFunc, NotificationFunc,
NotificationAction,
Axes, Axes,
Dashboard, Dashboard,
Service, Service,

View File

@ -7,3 +7,5 @@ export interface Notification {
} }
export type NotificationFunc = (message: any) => Notification export type NotificationFunc = (message: any) => Notification
export type NotificationAction = (message: Notification) => void