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-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",

View File

@ -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<Props, State> {
const {service} = this.props
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 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,17 +47,46 @@ class DatabaseListItem extends PureComponent<Props, State> {
}
public render() {
const {db, service} = this.props
const {searchTerm} = this.state
const {db} = this.props
return (
<div className={this.className} onClick={this.handleClick}>
<div className="flux-schema-item">
<div className="flex-schema-item-group">
<div className="flux-schema-item-toggle" />
{db}
<span className="flux-schema-type">Bucket</span>
</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">
<input
@ -66,21 +102,24 @@ class DatabaseListItem extends PureComponent<Props, State> {
</div>
<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 {
return classnames('flux-schema-tree', {
expanded: 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>) => {

View File

@ -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<Props, State> {
return (
<>
{tags.map(t => (
<NotificationContext.Consumer key={t}>
{({notify}) => (
<TagListItem
key={t}
db={db}
tagKey={t}
service={service}
filter={filter}
notify={notify}
/>
)}
</NotificationContext.Consumer>
))}
</>
)

View File

@ -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<Props, State> {
return (
<div className={this.className}>
<div className="flux-schema-item" onClick={this.handleClick}>
<div className="flex-schema-item-group">
<div className="flux-schema-item-toggle" />
{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>
{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() {
const {service, db, filter, tagKey} = this.props
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 LoadingSpinner from 'src/flux/components/LoadingSpinner'
import {NotificationContext} from 'src/flux/containers/CheckServices'
import {Service, SchemaFilter} from 'src/types'
interface Props {
@ -30,6 +32,8 @@ export default class TagValueList extends PureComponent<Props> {
return (
<>
{values.map((v, i) => (
<NotificationContext.Consumer key={v}>
{({notify}) => (
<TagValueListItem
key={i}
db={db}
@ -37,7 +41,10 @@ export default class TagValueList extends PureComponent<Props> {
tagKey={tagKey}
service={service}
filter={filter}
notify={notify}
/>
)}
</NotificationContext.Consumer>
))}
{shouldShowMoreValues && (
<div className="flux-schema-tree flux-tree-node">

View File

@ -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,10 +53,18 @@ class TagValueListItem extends PureComponent<Props, State> {
return (
<div className={this.className} onClick={this.handleClick}>
<div className="flux-schema-item">
<div className="flex-schema-item-group">
<div className="flux-schema-item-toggle" />
{value}
<span className="flux-schema-type">Tag Value</span>
</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.isLoading && <LoaderSkeleton />}
@ -127,6 +149,22 @@ class TagValueListItem extends PureComponent<Props, State> {
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>) => {
this.setState({
searchTerm: e.target.value,

View File

@ -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,6 +56,7 @@ export class CheckServices extends PureComponent<Props & WithRouterProps> {
}
return (
<NotificationContext.Provider value={{notify}}>
<FluxPage
source={this.source}
services={services}
@ -62,6 +65,7 @@ export class CheckServices extends PureComponent<Props & WithRouterProps> {
notify={notify}
updateScript={updateScript}
/>
</NotificationContext.Provider>
)
}

View File

@ -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,

View File

@ -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);
}
}

View File

@ -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,

View File

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