Merge pull request #12843 from influxdata/feat/export-copy-button

Add copy button to export overlays
pull/12850/head
Iris Scholten 2019-03-22 14:26:36 -07:00 committed by GitHub
commit 9277a76ca8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 162 additions and 562 deletions

View File

@ -8,6 +8,7 @@
1. [12706](https://github.com/influxdata/influxdb/pull/12706): Add ability to add variable to script from the side menu.
1. [12791](https://github.com/influxdata/influxdb/pull/12791): Use time range for metaqueries in Data Explorer and Cell Editor Overlay
1. [12827](https://github.com/influxdata/influxdb/pull/12827): Fix screen tearing bug in Raw Data View
1. [12843](https://github.com/influxdata/influxdb/pull/12843): Add copy to clipboard button to export overlays
### Bug Fixes

View File

@ -32,7 +32,6 @@ export class VerifyCollectorStep extends PureComponent<Props> {
const {
telegrafConfigID,
bucket,
notify,
org,
onDecrementCurrentStepIndex,
onExit,
@ -53,7 +52,6 @@ export class VerifyCollectorStep extends PureComponent<Props> {
</div>
<DataStreaming
org={org}
notify={notify}
bucket={bucket}
token={token}
configID={telegrafConfigID}

View File

@ -9,11 +9,7 @@ import DataListening from 'src/dataLoaders/components/verifyStep/DataListening'
// Decorator
import {ErrorHandling} from 'src/shared/decorators/errors'
// Types
import {NotificationAction} from 'src/types'
interface Props {
notify: NotificationAction
bucket: string
org: string
configID: string
@ -23,15 +19,11 @@ interface Props {
@ErrorHandling
class DataStreaming extends PureComponent<Props> {
public render() {
const {token, configID, bucket, notify} = this.props
const {token, configID, bucket} = this.props
return (
<div className="streaming">
<TelegrafInstructions
notify={notify}
token={token}
configID={configID}
/>
<TelegrafInstructions token={token} configID={configID} />
<DataListening bucket={bucket} />
</div>

View File

@ -7,7 +7,6 @@ let wrapper
const setup = (override = {}) => {
const props = {
notify: jest.fn(),
token: '',
configID: '',
...override,

View File

@ -8,11 +8,7 @@ import {ErrorHandling} from 'src/shared/decorators/errors'
// Components
import CodeSnippet from 'src/shared/components/CodeSnippet'
// Types
import {NotificationAction} from 'src/types'
export interface Props {
notify: NotificationAction
token: string
configID: string
}
@ -20,7 +16,7 @@ export interface Props {
@ErrorHandling
class TelegrafInstructions extends PureComponent<Props> {
public render() {
const {notify, token, configID} = this.props
const {token, configID} = this.props
const exportToken = `export INFLUX_TOKEN=${token || ''}`
const configScript = `telegraf --config ${
this.origin
@ -46,13 +42,13 @@ class TelegrafInstructions extends PureComponent<Props> {
copy the following command to your terminal window to set an
environment variable with your token.
</p>
<CodeSnippet copyText={exportToken} notify={notify} label="CLI" />
<CodeSnippet copyText={exportToken} label="CLI" />
<h6>3. Start Telegraf</h6>
<p>
Finally, you can run the following command to start the Telegraf agent
running on your machine.
</p>
<CodeSnippet copyText={configScript} notify={notify} label="CLI" />
<CodeSnippet copyText={configScript} label="CLI" />
</div>
)
}

View File

@ -28,7 +28,6 @@ exports[`TelegrafInstructions matches snapshot 1`] = `
<CodeSnippet
copyText="export INFLUX_TOKEN="
label="CLI"
notify={[MockFunction]}
/>
<h6>
3. Start Telegraf
@ -39,7 +38,6 @@ exports[`TelegrafInstructions matches snapshot 1`] = `
<CodeSnippet
copyText="telegraf --config http://localhost/api/v2/telegrafs/"
label="CLI"
notify={[MockFunction]}
/>
</div>
`;

View File

@ -1,6 +1,6 @@
// Libraries
import React from 'react'
import {mount} from 'enzyme'
import {shallow} from 'enzyme'
// Components
import ViewTokenOverlay from 'src/me/components/account/ViewTokenOverlay'
@ -14,7 +14,7 @@ const setup = (override?) => {
...override,
}
const wrapper = mount(<ViewTokenOverlay {...props} />)
const wrapper = shallow(<ViewTokenOverlay {...props} />)
return {wrapper}
}

View File

@ -25,7 +25,6 @@ interface Props {
export default class ViewTokenOverlay extends PureComponent<Props> {
public render() {
const {description} = this.props.auth
const {onNotify} = this.props
const permissions = this.permissions
@ -33,7 +32,7 @@ export default class ViewTokenOverlay extends PureComponent<Props> {
<Overlay.Container>
<Overlay.Heading title={description} onDismiss={this.handleDismiss} />
<Overlay.Body>
<CodeSnippet copyText={this.props.auth.token} notify={onNotify} />
<CodeSnippet copyText={this.props.auth.token} />
<PermissionsWidget
mode={PermissionsWidgetMode.Read}
heightPixels={500}

View File

@ -1,483 +1,49 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Account rendering renders! 1`] = `
<ViewTokenOverlay
auth={
Object {
"description": "im a token",
"id": "030444b11fb10000",
"links": Object {
"self": "/api/v2/authorizations/030444b11fb10000",
"user": "/api/v2/users/030444b10a710000",
},
"orgID": "030444b10a713000",
"permissions": Array [
Object {
"action": "write",
"resource": Object {
"type": "orgs",
},
},
Object {
"action": "write",
"resource": Object {
"type": "buckets",
},
},
],
"status": "active",
"token": "ohEmfY80A9UsW_cicNXgOMIPIsUvU6K9YcpTfCPQE3NV8Y6nTsCwVghczATBPyQh96CoZkOW5DIKldya6Y84KA==",
"user": "watts",
"userID": "030444b10a710000",
}
}
<OverlayContainer
maxWidth={800}
>
<OverlayContainer
maxWidth={800}
>
<div
className="overlay--container"
data-testid="overlay--container"
style={
Object {
"maxWidth": "800px",
}
}
<OverlayHeading
onDismiss={[Function]}
title="im a token"
/>
<OverlayBody>
<CodeSnippet
copyText="ohEmfY80A9UsW_cicNXgOMIPIsUvU6K9YcpTfCPQE3NV8Y6nTsCwVghczATBPyQh96CoZkOW5DIKldya6Y84KA=="
label="Code Snippet"
/>
<PermissionsWidget
heightPixels={500}
mode="read"
>
<OverlayHeading
onDismiss={[Function]}
title="im a token"
<PermissionsWidgetSection
id="orgs"
key="orgs"
mode="read"
title="orgs"
>
<div
className="overlay--heading"
>
<div
className="overlay--title"
>
im a token
</div>
<button
className="overlay--dismiss"
onClick={[Function]}
type="button"
/>
</div>
</OverlayHeading>
<OverlayBody>
<div
className="overlay--body"
>
<CodeSnippet
copyText="ohEmfY80A9UsW_cicNXgOMIPIsUvU6K9YcpTfCPQE3NV8Y6nTsCwVghczATBPyQh96CoZkOW5DIKldya6Y84KA=="
label="Code Snippet"
>
<div
className="code-snippet"
>
<FancyScrollbar
autoHeight={true}
autoHide={false}
className="code-snippet--scroll"
hideTracksWhenNotNeeded={true}
maxHeight={400}
setScrollTop={[Function]}
style={Object {}}
>
<Scrollbars
autoHeight={true}
autoHeightMax={400}
autoHeightMin={0}
autoHide={false}
autoHideDuration={250}
autoHideTimeout={1000}
className="fancy-scroll--container code-snippet--scroll"
hideTracksWhenNotNeeded={true}
onScroll={[Function]}
style={Object {}}
tagName="div"
thumbMinSize={30}
thumbStartColor="#22ADF6"
thumbStopColor="#9394FF"
universal={false}
>
<div
className="fancy-scroll--container code-snippet--scroll"
style={
Object {
"height": "auto",
"maxHeight": 400,
"minHeight": 0,
"overflow": "hidden",
"position": "relative",
"width": "100%",
}
}
>
<div
className="fancy-scroll--view"
key="view"
style={
Object {
"WebkitOverflowScrolling": "touch",
"bottom": undefined,
"left": undefined,
"marginBottom": 0,
"marginRight": 0,
"maxHeight": 415,
"minHeight": 15,
"overflow": "scroll",
"position": "relative",
"right": undefined,
"top": undefined,
}
}
>
<div
className="code-snippet--text"
>
<pre>
<code>
ohEmfY80A9UsW_cicNXgOMIPIsUvU6K9YcpTfCPQE3NV8Y6nTsCwVghczATBPyQh96CoZkOW5DIKldya6Y84KA==
</code>
</pre>
</div>
</div>
<div
className="fancy-scroll--track-h"
key="trackHorizontal"
style={
Object {
"bottom": "0",
"cursor": "pointer",
"height": "12px",
"left": "0",
"padding": "3px",
"position": "absolute",
"width": "100%",
}
}
>
<div
className="fancy-scroll--thumb-h"
style={
Object {
"background": "linear-gradient(to right, #22ADF6 0%, #9394FF 100%)",
"borderRadius": "3px",
"display": "block",
"height": "6px",
"position": "relative",
}
}
/>
</div>
<div
className="fancy-scroll--track-v"
key="trackVertical"
style={
Object {
"cursor": "pointer",
"height": "100%",
"padding": "3px",
"position": "absolute",
"right": "0",
"top": "0",
"width": "12px",
}
}
>
<div
className="fancy-scroll--thumb-v"
style={
Object {
"background": "linear-gradient(to bottom, #22ADF6 0%, #9394FF 100%)",
"borderRadius": "3px",
"display": "block",
"position": "relative",
"width": "6px",
}
}
/>
</div>
</div>
</Scrollbars>
</FancyScrollbar>
<div
className="code-snippet--footer"
>
<CopyToClipboard
onCopy={[Function]}
text="ohEmfY80A9UsW_cicNXgOMIPIsUvU6K9YcpTfCPQE3NV8Y6nTsCwVghczATBPyQh96CoZkOW5DIKldya6Y84KA=="
>
<t
active={false}
color="secondary"
onClick={[Function]}
shape="none"
size="xs"
status="default"
testID="button"
text="Copy to Clipboard"
titleText="Copy to Clipboard"
type="button"
>
<button
className="button button-xs button-secondary"
data-testid="button"
disabled={false}
onClick={[Function]}
tabIndex={0}
title="Copy to Clipboard"
type="button"
>
Copy to Clipboard
</button>
</t>
</CopyToClipboard>
<label
className="code-snippet--label"
>
Code Snippet
</label>
</div>
</div>
</CodeSnippet>
<PermissionsWidget
heightPixels={500}
mode="read"
>
<div
className="permissions-widget"
style={
Object {
"height": "500px",
}
}
>
<div
className="permissions-widget--header"
>
Summary of access permissions
</div>
<div
className="permissions-widget--body"
>
<FancyScrollbar
autoHeight={false}
autoHide={false}
hideTracksWhenNotNeeded={true}
maxHeight={null}
setScrollTop={[Function]}
style={Object {}}
>
<Scrollbars
autoHeight={false}
autoHeightMax={null}
autoHeightMin={0}
autoHide={false}
autoHideDuration={250}
autoHideTimeout={1000}
className="fancy-scroll--container"
hideTracksWhenNotNeeded={true}
onScroll={[Function]}
style={Object {}}
tagName="div"
thumbMinSize={30}
thumbStartColor="#22ADF6"
thumbStopColor="#9394FF"
universal={false}
>
<div
className="fancy-scroll--container"
style={
Object {
"height": "100%",
"overflow": "hidden",
"position": "relative",
"width": "100%",
}
}
>
<div
className="fancy-scroll--view"
key="view"
style={
Object {
"WebkitOverflowScrolling": "touch",
"bottom": 0,
"left": 0,
"marginBottom": 0,
"marginRight": 0,
"overflow": "scroll",
"position": "absolute",
"right": 0,
"top": 0,
}
}
>
<PermissionsWidgetSection
id="orgs"
key=".$orgs"
mode="read"
title="orgs"
>
<section
className="permissions-widget--section"
>
<header
className="permissions-widget--section-heading"
>
<h3
className="permissions-widget--section-title"
>
orgs
</h3>
</header>
<ul
className="permissions-widget--section-list"
>
<PermissionsWidgetItem
id="orgs-write-orgs-orgs"
key=".$0"
label="write"
mode="read"
selected="selected"
>
<li
className="permissions-widget--item selected"
onClick={[Function]}
>
<div
className="permissions-widget--icon"
>
<span
className="icon checkmark"
/>
</div>
<label
className="permissions-widget--item-label"
>
write
</label>
</li>
</PermissionsWidgetItem>
</ul>
</section>
</PermissionsWidgetSection>
<PermissionsWidgetSection
id="buckets"
key=".$buckets"
mode="read"
title="buckets"
>
<section
className="permissions-widget--section"
>
<header
className="permissions-widget--section-heading"
>
<h3
className="permissions-widget--section-title"
>
buckets
</h3>
</header>
<ul
className="permissions-widget--section-list"
>
<PermissionsWidgetItem
id="buckets-write-buckets-buckets"
key=".$0"
label="write"
mode="read"
selected="selected"
>
<li
className="permissions-widget--item selected"
onClick={[Function]}
>
<div
className="permissions-widget--icon"
>
<span
className="icon checkmark"
/>
</div>
<label
className="permissions-widget--item-label"
>
write
</label>
</li>
</PermissionsWidgetItem>
</ul>
</section>
</PermissionsWidgetSection>
</div>
<div
className="fancy-scroll--track-h"
key="trackHorizontal"
style={
Object {
"bottom": "0",
"cursor": "pointer",
"height": "12px",
"left": "0",
"padding": "3px",
"position": "absolute",
"width": "100%",
}
}
>
<div
className="fancy-scroll--thumb-h"
style={
Object {
"background": "linear-gradient(to right, #22ADF6 0%, #9394FF 100%)",
"borderRadius": "3px",
"display": "block",
"height": "6px",
"position": "relative",
}
}
/>
</div>
<div
className="fancy-scroll--track-v"
key="trackVertical"
style={
Object {
"cursor": "pointer",
"height": "100%",
"padding": "3px",
"position": "absolute",
"right": "0",
"top": "0",
"width": "12px",
}
}
>
<div
className="fancy-scroll--thumb-v"
style={
Object {
"background": "linear-gradient(to bottom, #22ADF6 0%, #9394FF 100%)",
"borderRadius": "3px",
"display": "block",
"position": "relative",
"width": "6px",
}
}
/>
</div>
</div>
</Scrollbars>
</FancyScrollbar>
</div>
</div>
</PermissionsWidget>
</div>
</OverlayBody>
</div>
</OverlayContainer>
</ViewTokenOverlay>
<PermissionsWidgetItem
id="orgs-write-orgs-orgs"
key="0"
label="write"
selected="selected"
/>
</PermissionsWidgetSection>
<PermissionsWidgetSection
id="buckets"
key="buckets"
mode="read"
title="buckets"
>
<PermissionsWidgetItem
id="buckets-write-buckets-buckets"
key="0"
label="write"
selected="selected"
/>
</PermissionsWidgetSection>
</PermissionsWidget>
</OverlayBody>
</OverlayContainer>
`;

View File

@ -8,9 +8,6 @@ import {ErrorHandling} from 'src/shared/decorators/errors'
import WizardOverlay from 'src/clockface/components/wizard/WizardOverlay'
import TelegrafInstructions from 'src/dataLoaders/components/verifyStep/TelegrafInstructions'
// Actions
import {notify as notifyAction} from 'src/shared/actions/notifications'
// Constants
import {TOKEN_LABEL} from 'src/labels/constants'
@ -24,22 +21,18 @@ interface OwnProps {
collector?: Telegraf
}
interface DispatchProps {
notify: typeof notifyAction
}
interface StateProps {
username: string
telegrafs: AppState['telegrafs']['list']
tokens: AppState['tokens']['list']
}
type Props = StateProps & DispatchProps & OwnProps
type Props = StateProps & OwnProps
@ErrorHandling
export class TelegrafInstructionsOverlay extends PureComponent<Props> {
public render() {
const {notify, collector, visible, onDismiss} = this.props
const {collector, visible, onDismiss} = this.props
return (
<WizardOverlay
@ -48,7 +41,6 @@ export class TelegrafInstructionsOverlay extends PureComponent<Props> {
onDismiss={onDismiss}
>
<TelegrafInstructions
notify={notify}
token={this.token}
configID={get(collector, 'id', '')}
/>
@ -84,11 +76,7 @@ const mstp = ({me: {name}, telegrafs, tokens}: AppState): StateProps => ({
tokens: tokens.list,
})
const mdtp: DispatchProps = {
notify: notifyAction,
}
export default connect<StateProps, DispatchProps, OwnProps>(
export default connect<StateProps, {}, OwnProps>(
mstp,
mdtp
null
)(TelegrafInstructionsOverlay)

View File

@ -1,28 +1,19 @@
// Libraries
import React, {PureComponent, MouseEvent} from 'react'
import React, {PureComponent} from 'react'
import _ from 'lodash'
import CopyToClipboard from 'react-copy-to-clipboard'
// Decorator
import {ErrorHandling} from 'src/shared/decorators/errors'
// Components
import {Button, ComponentSize, ComponentColor} from '@influxdata/clockface'
import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar'
// Actions
import {NotificationAction} from 'src/types'
import {
copyToClipboardSuccess,
copyToClipboardFailed,
} from 'src/shared/copy/notifications'
import CopyButton from 'src/shared/components/CopyButton'
// Styles
import 'src/shared/components/CodeSnippet.scss'
export interface PassedProps {
copyText: string
notify: NotificationAction
}
interface DefaultProps {
@ -54,41 +45,12 @@ class CodeSnippet extends PureComponent<Props> {
</div>
</FancyScrollbar>
<div className="code-snippet--footer">
<CopyToClipboard text={copyText} onCopy={this.handleCopyAttempt}>
<Button
size={ComponentSize.ExtraSmall}
color={ComponentColor.Secondary}
titleText="Copy to Clipboard"
text="Copy to Clipboard"
onClick={this.handleClickCopy}
/>
</CopyToClipboard>
<CopyButton textToCopy={copyText} contentName={'Script'} />
<label className="code-snippet--label">{label}</label>
</div>
</div>
)
}
private handleClickCopy = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
e.preventDefault()
}
private handleCopyAttempt = (
copiedText: string,
isSuccessful: boolean
): void => {
const {notify} = this.props
const text = copiedText.slice(0, 30).trimRight()
const truncatedText = `${text}...`
const title = 'Script '
if (isSuccessful) {
notify(copyToClipboardSuccess(truncatedText, title))
} else {
notify(copyToClipboardFailed(truncatedText, title))
}
}
}
export default CodeSnippet

View File

@ -0,0 +1,84 @@
// Libraries
import React, {PureComponent, MouseEvent} from 'react'
import CopyToClipboard from 'react-copy-to-clipboard'
import {connect} from 'react-redux'
// Components
import {Button, ComponentColor, ComponentSize} from '@influxdata/clockface'
// Constants
import {
copyToClipboardSuccess,
copyToClipboardFailed,
} from 'src/shared/copy/notifications'
// Actions
import {notify as notifyAction} from 'src/shared/actions/notifications'
interface OwnProps {
textToCopy: string
contentName: string // if copying a script, its "script"
size?: ComponentSize
color?: ComponentColor
}
interface DefaultProps {
size: ComponentSize
color: ComponentColor
}
interface DispatchProps {
notify: typeof notifyAction
}
type Props = OwnProps & DispatchProps
class CopyButton extends PureComponent<Props> {
public static defaultProps: DefaultProps = {
size: ComponentSize.ExtraSmall,
color: ComponentColor.Secondary,
}
public render() {
const {textToCopy, color, size} = this.props
return (
<CopyToClipboard text={textToCopy} onCopy={this.handleCopyAttempt}>
<Button
size={size}
color={color}
titleText="Copy to Clipboard"
text="Copy to Clipboard"
onClick={this.handleClickCopy}
/>
</CopyToClipboard>
)
}
private handleClickCopy = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
e.preventDefault()
}
private handleCopyAttempt = (
copiedText: string,
isSuccessful: boolean
): void => {
const {contentName, notify} = this.props
const text = copiedText.slice(0, 30).trimRight()
const truncatedText = `${text}...`
if (isSuccessful) {
notify(copyToClipboardSuccess(truncatedText, contentName))
} else {
notify(copyToClipboardFailed(truncatedText, contentName))
}
}
}
const mdtp: DispatchProps = {
notify: notifyAction,
}
export default connect<{}, DispatchProps, OwnProps>(
null,
mdtp
)(CopyButton)

View File

@ -9,10 +9,12 @@ import {Form} from 'src/clockface'
import {
Button,
ComponentColor,
ComponentSize,
SpinnerContainer,
TechnoSpinner,
} from '@influxdata/clockface'
import {Controlled as ReactCodeMirror} from 'react-codemirror2'
import CopyButton from 'src/shared/components/CopyButton'
// Actions
import {notify as notifyAction} from 'src/shared/actions/notifications'
@ -76,6 +78,7 @@ class ExportOverlay extends PureComponent<Props> {
<Overlay.Footer>
{this.downloadButton}
{this.toTemplateButton}
{this.copyButton}
</Overlay.Footer>
</Form>
</Overlay.Container>
@ -86,7 +89,6 @@ class ExportOverlay extends PureComponent<Props> {
private doNothing = () => {}
private get overlayBody(): JSX.Element {
const {resource} = this.props
const options = {
tabIndex: 1,
mode: 'json',
@ -101,7 +103,7 @@ class ExportOverlay extends PureComponent<Props> {
<ReactCodeMirror
autoFocus={false}
autoCursor={true}
value={JSON.stringify(resource, null, 1)}
value={this.resourceText}
options={options}
onBeforeChange={this.doNothing}
onTouchStart={this.doNothing}
@ -110,6 +112,21 @@ class ExportOverlay extends PureComponent<Props> {
)
}
private get resourceText(): string {
return JSON.stringify(this.props.resource, null, 1)
}
private get copyButton(): JSX.Element {
return (
<CopyButton
textToCopy={this.resourceText}
contentName={this.props.resourceName}
size={ComponentSize.Small}
color={ComponentColor.Secondary}
/>
)
}
private get downloadButton(): JSX.Element {
return (
<Button

View File

@ -733,7 +733,7 @@ export const copyToClipboardSuccess = (
): Notification => ({
...defaultSuccessNotification,
icon: 'dash-h',
message: `${title}'${text}' has been copied to clipboard.`,
message: `${title} '${text}' has been copied to clipboard.`,
})
export const copyToClipboardFailed = (