feat(ui/variables/rename): add danger zone to rename (#13555)

pull/13561/head
Delmer 2019-04-22 15:25:08 -04:00 committed by GitHub
parent e5657ca62b
commit cf8785dc76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 394 additions and 189 deletions

View File

@ -66,6 +66,9 @@ import AddMembersOverlay from 'src/members/components/AddMembersOverlay'
import OrgProfilePage from 'src/organizations/containers/OrgProfilePage' import OrgProfilePage from 'src/organizations/containers/OrgProfilePage'
import RenameOrgOverlay from 'src/organizations/components/RenameOrgOverlay' import RenameOrgOverlay from 'src/organizations/components/RenameOrgOverlay'
import UpdateBucketOverlay from 'src/buckets/components/UpdateBucketOverlay' import UpdateBucketOverlay from 'src/buckets/components/UpdateBucketOverlay'
import RenameBucketOverlay from 'src/buckets/components/RenameBucketOverlay'
import RenameVariableOverlay from 'src/variables/components/RenameVariableOverlay'
import UpdateVariableOverlay from 'src/variables/components/UpdateVariableOverlay'
// Actions // Actions
import {disablePresentationMode} from 'src/shared/actions/app' import {disablePresentationMode} from 'src/shared/actions/app'
@ -73,7 +76,6 @@ import {disablePresentationMode} from 'src/shared/actions/app'
// Styles // Styles
import 'src/style/chronograf.scss' import 'src/style/chronograf.scss'
import '@influxdata/clockface/dist/index.css' import '@influxdata/clockface/dist/index.css'
import RenameBucketOverlay from './buckets/components/RenameBucketOverlay'
const rootNode = getRootNode() const rootNode = getRootNode()
const basepath = getBasepath() const basepath = getBasepath()
@ -256,6 +258,14 @@ class Root extends PureComponent {
path="new" path="new"
component={CreateVariableOverlay} component={CreateVariableOverlay}
/> />
<Route
path=":id/rename"
component={RenameVariableOverlay}
/>
<Route
path=":id/edit"
component={UpdateVariableOverlay}
/>
</Route> </Route>
<Route path="labels" component={LabelsIndex} /> <Route path="labels" component={LabelsIndex} />
<Route path="scrapers" component={ScrapersIndex}> <Route path="scrapers" component={ScrapersIndex}>

View File

@ -0,0 +1,159 @@
// Libraries
import React, {PureComponent, ChangeEvent, FormEvent} from 'react'
import _ from 'lodash'
import {connect} from 'react-redux'
import {withRouter, WithRouterProps} from 'react-router'
// Components
import {Form, Input, Button, Grid, Columns} from '@influxdata/clockface'
import {Overlay} from 'src/clockface'
// Utils
import {validateVariableName} from 'src/variables/utils/validation'
import {extractVariablesList} from 'src/variables/selectors'
// Actions
import {updateVariable} from 'src/variables/actions'
// Types
import {AppState} from 'src/types'
import {IVariable as Variable} from '@influxdata/influx'
import {
ButtonType,
ComponentColor,
ComponentStatus,
} from '@influxdata/clockface'
interface OwnProps {
onClose: () => void
}
interface State {
workingVariable: Variable
isNameValid: boolean
}
interface StateProps {
variables: Variable[]
startVariable: Variable
}
interface DispatchProps {
onUpdateVariable: typeof updateVariable
}
type Props = StateProps & OwnProps & DispatchProps & WithRouterProps
class RenameVariableOverlayForm extends PureComponent<Props, State> {
public state: State = {
workingVariable: this.props.startVariable,
isNameValid: true,
}
public render() {
const {onClose} = this.props
const {workingVariable, isNameValid} = this.state
return (
<Overlay.Container maxWidth={1000}>
<Overlay.Heading title="Rename Variable" onDismiss={onClose} />
<Overlay.Body>
<Form onSubmit={this.handleSubmit}>
<Grid>
<Grid.Row>
<Grid.Column widthXS={Columns.Six}>
<div className="overlay-flux-editor--spacing">
<Form.ValidationElement
label="Name"
value={workingVariable.name}
required={true}
validationFunc={this.handleNameValidation}
>
{status => (
<Input
placeholder="Rename your variable"
name="name"
autoFocus={true}
value={workingVariable.name}
onChange={this.handleChangeInput}
status={status}
/>
)}
</Form.ValidationElement>
</div>
</Grid.Column>
</Grid.Row>
<Grid.Row>
<Grid.Column>
<Form.Footer>
<Button
text="Cancel"
color={ComponentColor.Danger}
onClick={onClose}
/>
<Button
text="Submit"
type={ButtonType.Submit}
color={ComponentColor.Primary}
status={
isNameValid
? ComponentStatus.Default
: ComponentStatus.Disabled
}
/>
</Form.Footer>
</Grid.Column>
</Grid.Row>
</Grid>
</Form>
</Overlay.Body>
</Overlay.Container>
)
}
private handleSubmit = (e: FormEvent): void => {
const {workingVariable} = this.state
e.preventDefault()
this.props.onUpdateVariable(workingVariable.id, workingVariable)
this.props.onClose()
}
private handleNameValidation = (name: string) => {
const {variables} = this.props
const {error} = validateVariableName(name, variables)
this.setState({isNameValid: !error})
return error
}
private handleChangeInput = (e: ChangeEvent<HTMLInputElement>) => {
const name = e.target.value
const workingVariable = {...this.state.workingVariable, name}
this.setState({
workingVariable,
})
}
}
const mstp = (state: AppState, {params: {id}}: Props): StateProps => {
const variables = extractVariablesList(state)
const startVariable = variables.find(v => v.id === id)
return {variables, startVariable}
}
const mdtp: DispatchProps = {
onUpdateVariable: updateVariable,
}
export default withRouter<OwnProps>(
connect<StateProps, DispatchProps, OwnProps>(
mstp,
mdtp
)(RenameVariableOverlayForm)
)

View File

@ -0,0 +1,48 @@
// Libraries
import React, {PureComponent} from 'react'
import {withRouter, WithRouterProps} from 'react-router'
import _ from 'lodash'
// Components
import DangerConfirmationOverlay from 'src/shared/components/dangerConfirmation/DangerConfirmationOverlay'
import RenameVariableForm from 'src/variables/components/RenameVariableForm'
// Decorators
import {ErrorHandling} from 'src/shared/decorators/errors'
@ErrorHandling
class RenameVariableOverlay extends PureComponent<WithRouterProps> {
public render() {
return (
<DangerConfirmationOverlay
title="Rename Variable"
message={this.message}
effectedItems={this.effectedItems}
onClose={this.handleClose}
confirmButtonText="I understand, let's rename my Variable"
>
<RenameVariableForm onClose={this.handleClose} />
</DangerConfirmationOverlay>
)
}
private get message(): string {
return 'Updating the name of a Variable can have unintended consequences. Anything that references this Variable by name will stop working including:'
}
private get effectedItems(): string[] {
return ['Queries', 'Dashboards', 'Telegraf Configurations', 'Templates']
}
private handleClose = () => {
const {
router,
params: {orgID},
} = this.props
router.push(`/orgs/${orgID}/variables`)
}
}
export default withRouter(RenameVariableOverlay)

View File

@ -1,6 +1,8 @@
// Libraries // Libraries
import React, {PureComponent, ChangeEvent, FormEvent} from 'react' import React, {PureComponent, FormEvent} from 'react'
import _ from 'lodash' import _ from 'lodash'
import {connect} from 'react-redux'
import {withRouter, WithRouterProps} from 'react-router'
// Components // Components
import { import {
@ -14,8 +16,11 @@ import {
import {Overlay} from 'src/clockface' import {Overlay} from 'src/clockface'
import VariableArgumentsEditor from 'src/variables/components/VariableArgumentsEditor' import VariableArgumentsEditor from 'src/variables/components/VariableArgumentsEditor'
// Actions
import {updateVariable} from 'src/variables/actions'
// Utils // Utils
import {validateVariableName} from 'src/variables/utils/validation' import {extractVariablesList} from 'src/variables/selectors'
// Constants // Constants
import {variableItemTypes} from 'src/variables/constants' import {variableItemTypes} from 'src/variables/constants'
@ -27,14 +32,7 @@ import {
ComponentColor, ComponentColor,
ComponentStatus, ComponentStatus,
} from '@influxdata/clockface' } from '@influxdata/clockface'
import {VariableArguments} from 'src/types' import {VariableArguments, AppState} from 'src/types'
interface Props {
variable: Variable
variables: Variable[]
onCloseOverlay: () => void
onUpdateVariable: (variable: Variable) => Promise<void>
}
interface State { interface State {
workingVariable: Variable workingVariable: Variable
@ -42,107 +40,109 @@ interface State {
hasValidArgs: boolean hasValidArgs: boolean
} }
export default class UpdateVariableOverlay extends PureComponent<Props, State> { interface StateProps {
variables: Variable[]
startVariable: Variable
}
interface DispatchProps {
onUpdateVariable: typeof updateVariable
}
type Props = StateProps & DispatchProps & WithRouterProps
class UpdateVariableOverlay extends PureComponent<Props, State> {
public state: State = { public state: State = {
workingVariable: this.props.variable, workingVariable: this.props.startVariable,
isNameValid: true, isNameValid: true,
hasValidArgs: true, hasValidArgs: true,
} }
public render() { public render() {
const {onCloseOverlay} = this.props const {workingVariable, hasValidArgs} = this.state
const {workingVariable} = this.state
return ( return (
<Overlay.Container maxWidth={1000}> <Overlay visible={true}>
<Overlay.Heading <Overlay.Container maxWidth={1000}>
title="Edit Variable" <Overlay.Heading title="Edit Variable" onDismiss={this.handleClose} />
onDismiss={this.props.onCloseOverlay} <Overlay.Body>
/> <Form onSubmit={this.handleSubmit}>
<Overlay.Body> <Grid>
<Form onSubmit={this.handleSubmit}> <Grid.Row>
<Grid> <Grid.Column widthXS={Columns.Six}>
<Grid.Row> <div className="overlay-flux-editor--spacing">
<Grid.Column widthXS={Columns.Six}> <Form.Element
<div className="overlay-flux-editor--spacing"> label="Name"
<Form.ValidationElement helpText="To rename your variable use the rename button. Renaming is not allowed here."
label="Name" >
value={workingVariable.name}
required={true}
validationFunc={this.handleNameValidation}
>
{status => (
<Input <Input
placeholder="Give your variable a name" placeholder="Give your variable a name"
name="name" name="name"
autoFocus={true} autoFocus={true}
value={workingVariable.name} value={workingVariable.name}
onChange={this.handleChangeInput} status={ComponentStatus.Disabled}
status={status}
/> />
)} </Form.Element>
</Form.ValidationElement> </div>
</div> </Grid.Column>
</Grid.Column> <Grid.Column widthXS={Columns.Six}>
<Grid.Column widthXS={Columns.Six}> <Form.Element label="Type" required={true}>
<Form.Element label="Type" required={true}> <Dropdown
<Dropdown selectedID={workingVariable.arguments.type}
selectedID={workingVariable.arguments.type} onChange={this.handleChangeType}
onChange={this.handleChangeType} >
> {variableItemTypes.map(v => (
{variableItemTypes.map(v => ( <Dropdown.Item
<Dropdown.Item key={v.type} id={v.type} value={v.type}> key={v.type}
{v.label} id={v.type}
</Dropdown.Item> value={v.type}
))} >
</Dropdown> {v.label}
</Form.Element> </Dropdown.Item>
</Grid.Column> ))}
</Grid.Row> </Dropdown>
<Grid.Row> </Form.Element>
<Grid.Column> </Grid.Column>
<VariableArgumentsEditor </Grid.Row>
onChange={this.handleChangeArgs} <Grid.Row>
onSelectMapDefault={this.handleSelectMapDefault} <Grid.Column>
selected={workingVariable.selected} <VariableArgumentsEditor
args={workingVariable.arguments} onChange={this.handleChangeArgs}
/> onSelectMapDefault={this.handleSelectMapDefault}
</Grid.Column> selected={workingVariable.selected}
</Grid.Row> args={workingVariable.arguments}
<Grid.Row>
<Grid.Column>
<Form.Footer>
<Button
text="Cancel"
color={ComponentColor.Danger}
onClick={onCloseOverlay}
/> />
<Button </Grid.Column>
text="Submit" </Grid.Row>
type={ButtonType.Submit} <Grid.Row>
color={ComponentColor.Primary} <Grid.Column>
status={ <Form.Footer>
this.isFormValid <Button
? ComponentStatus.Default text="Cancel"
: ComponentStatus.Disabled color={ComponentColor.Danger}
} onClick={this.handleClose}
/> />
</Form.Footer> <Button
</Grid.Column> text="Submit"
</Grid.Row> type={ButtonType.Submit}
</Grid> color={ComponentColor.Primary}
</Form> status={
</Overlay.Body> hasValidArgs
</Overlay.Container> ? ComponentStatus.Default
: ComponentStatus.Disabled
}
/>
</Form.Footer>
</Grid.Column>
</Grid.Row>
</Grid>
</Form>
</Overlay.Body>
</Overlay.Container>
</Overlay>
) )
} }
private get isFormValid(): boolean {
const {hasValidArgs, isNameValid} = this.state
return hasValidArgs && isNameValid
}
private handleChangeType = (selectedType: string) => { private handleChangeType = (selectedType: string) => {
const {isNameValid, workingVariable} = this.state const {isNameValid, workingVariable} = this.state
const defaults = {hasValidArgs: false, isNameValid} const defaults = {hasValidArgs: false, isNameValid}
@ -221,27 +221,36 @@ export default class UpdateVariableOverlay extends PureComponent<Props, State> {
private handleSubmit = (e: FormEvent): void => { private handleSubmit = (e: FormEvent): void => {
e.preventDefault() e.preventDefault()
const {workingVariable} = this.state
this.props.onUpdateVariable(this.state.workingVariable) this.props.onUpdateVariable(workingVariable.id, workingVariable)
this.props.onCloseOverlay() this.handleClose()
} }
private handleNameValidation = (name: string) => { private handleClose = () => {
const {variables} = this.props const {
const {error} = validateVariableName(name, variables) router,
params: {orgID},
} = this.props
this.setState({isNameValid: !error}) router.push(`/orgs/${orgID}/variables`)
return error
}
private handleChangeInput = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
const key = e.target.name
const workingVariable = {...this.state.workingVariable, [key]: value}
this.setState({
workingVariable,
})
} }
} }
const mstp = (state: AppState, {params: {id}}: Props): StateProps => {
const variables = extractVariablesList(state)
const startVariable = variables.find(v => v.id === id)
return {variables, startVariable}
}
const mdtp: DispatchProps = {
onUpdateVariable: updateVariable,
}
export default withRouter(
connect<StateProps, DispatchProps>(
mstp,
mdtp
)(UpdateVariableOverlay)
)

View File

@ -3,9 +3,8 @@ import React, {PureComponent} from 'react'
import memoizeOne from 'memoize-one' import memoizeOne from 'memoize-one'
// Components // Components
import {IndexList, Overlay} from 'src/clockface' import {IndexList} from 'src/clockface'
import VariableRow from 'src/variables/components/VariableRow' import VariableRow from 'src/variables/components/VariableRow'
import UpdateVariableOverlay from 'src/variables/components/UpdateVariableOverlay'
// Types // Types
import {IVariable as Variable} from '@influxdata/influx' import {IVariable as Variable} from '@influxdata/influx'
@ -52,13 +51,7 @@ export default class VariableList extends PureComponent<Props, State> {
} }
public render() { public render() {
const { const {emptyState, sortKey, sortDirection, onClickColumn} = this.props
emptyState,
variables,
sortKey,
sortDirection,
onClickColumn,
} = this.props
return ( return (
<> <>
@ -77,14 +70,6 @@ export default class VariableList extends PureComponent<Props, State> {
{this.rows} {this.rows}
</IndexList.Body> </IndexList.Body>
</IndexList> </IndexList>
<Overlay visible={this.isVariableOverlayVisible}>
<UpdateVariableOverlay
variable={this.variable}
variables={variables}
onCloseOverlay={this.handleCloseOverlay}
onUpdateVariable={this.handleUpdateVariable}
/>
</Overlay>
</> </>
) )
} }
@ -122,26 +107,10 @@ export default class VariableList extends PureComponent<Props, State> {
)) ))
} }
private get variable(): Variable {
return this.props.variables.find(v => v.id === this.state.variableID)
}
private get isVariableOverlayVisible(): boolean {
return this.state.variableOverlayState === OverlayState.Open
}
private handleCloseOverlay = () => {
this.setState({variableOverlayState: OverlayState.Closed, variableID: null})
}
private handleStartEdit = (variable: Variable) => { private handleStartEdit = (variable: Variable) => {
this.setState({ this.setState({
variableID: variable.id, variableID: variable.id,
variableOverlayState: OverlayState.Open, variableOverlayState: OverlayState.Open,
}) })
} }
private handleUpdateVariable = async (variable: Variable): Promise<void> => {
this.props.onUpdateVariable(variable)
}
} }

View File

@ -1,7 +1,7 @@
// Libraries // Libraries
import React, {PureComponent} from 'react' import React, {PureComponent} from 'react'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import {withRouter, WithRouterProps} from 'react-router' import {withRouter, WithRouterProps, Link} from 'react-router'
// Components // Components
import {IndexList, Alignment, Context, IconFont} from 'src/clockface' import {IndexList, Alignment, Context, IconFont} from 'src/clockface'
@ -11,12 +11,12 @@ import {
FlexDirection, FlexDirection,
AlignItems, AlignItems,
ComponentSize, ComponentSize,
Button,
} from '@influxdata/clockface' } from '@influxdata/clockface'
import InlineLabels from 'src/shared/components/inlineLabels/InlineLabels' import InlineLabels from 'src/shared/components/inlineLabels/InlineLabels'
// Types // Types
import {IVariable as Variable, ILabel} from '@influxdata/influx' import {IVariable as Variable, ILabel} from '@influxdata/influx'
import EditableName from 'src/shared/components/EditableName'
import {AppState} from 'src/types' import {AppState} from 'src/types'
// Selectors // Selectors
@ -62,18 +62,23 @@ class VariableRow extends PureComponent<Props & WithRouterProps> {
alignItems={AlignItems.FlexStart} alignItems={AlignItems.FlexStart}
stretchToFitWidth={true} stretchToFitWidth={true}
> >
<EditableName <div className="editable-name">
onUpdate={this.handleUpdateVariableName} <Link to={this.editVariablePath}>
name={variable.name} <span>{variable.name}</span>
noNameString="NAME THIS VARIABLE" </Link>
onEditName={this.handleEditVariable} </div>
>
{variable.name}
</EditableName>
{this.labels} {this.labels}
</ComponentSpacer> </ComponentSpacer>
</IndexList.Cell> </IndexList.Cell>
<IndexList.Cell alignment={Alignment.Left}>Query</IndexList.Cell> <IndexList.Cell alignment={Alignment.Left}>Query</IndexList.Cell>
<IndexList.Cell revealOnHover={true} alignment={Alignment.Right}>
<Button
text="Rename"
onClick={this.handleRenameVariable}
color={ComponentColor.Danger}
size={ComponentSize.ExtraSmall}
/>
</IndexList.Cell>
<IndexList.Cell revealOnHover={true} alignment={Alignment.Right}> <IndexList.Cell revealOnHover={true} alignment={Alignment.Right}>
<Context> <Context>
<Context.Menu icon={IconFont.CogThick}> <Context.Menu icon={IconFont.CogThick}>
@ -97,6 +102,15 @@ class VariableRow extends PureComponent<Props & WithRouterProps> {
) )
} }
private get editVariablePath(): string {
const {
variable,
params: {orgID},
} = this.props
return `/orgs/${orgID}/variables/${variable.id}/edit`
}
private get labels(): JSX.Element { private get labels(): JSX.Element {
const {variable, labels, onFilterChange} = this.props const {variable, labels, onFilterChange} = this.props
const collectorLabels = viewableLabels(variable.labels) const collectorLabels = viewableLabels(variable.labels)
@ -140,17 +154,17 @@ class VariableRow extends PureComponent<Props & WithRouterProps> {
variable, variable,
params: {orgID}, params: {orgID},
} = this.props } = this.props
router.push(`orgs/${orgID}/variables/${variable.id}/export`) router.push(`/orgs/${orgID}/variables/${variable.id}/export`)
} }
private handleUpdateVariableName = async (name: string) => { private handleRenameVariable = async () => {
const {onUpdateVariableName, variable} = this.props const {
router,
variable,
params: {orgID},
} = this.props
await onUpdateVariableName({id: variable.id, name}) router.push(`/orgs/${orgID}/variables/${variable.id}/rename`)
}
private handleEditVariable = (): void => {
this.props.onEditVariable(this.props.variable)
} }
} }

View File

@ -41,28 +41,5 @@ exports[`VariableList rendering renders 1`] = `
/> />
</IndexListBody> </IndexListBody>
</IndexList> </IndexList>
<Overlay
visible={false}
>
<UpdateVariableOverlay
onCloseOverlay={[Function]}
onUpdateVariable={[Function]}
variables={
Array [
Object {
"arguments": Object {
"type": "query",
"values": Object {
"language": "flux",
"query": "1 + 1 ",
},
},
"name": "a little variable",
"orgID": "0",
},
]
}
/>
</Overlay>
</Fragment> </Fragment>
`; `;

View File

@ -1,4 +1,11 @@
import {Variable} from '@influxdata/influx' import {Variable} from '@influxdata/influx'
import {
TIME_RANGE_START,
TIME_RANGE_STOP,
WINDOW_PERIOD,
} from 'src/variables/constants'
const reservedVarNames = [TIME_RANGE_START, TIME_RANGE_STOP, WINDOW_PERIOD]
export const validateVariableName = ( export const validateVariableName = (
varName: string, varName: string,
@ -8,11 +15,23 @@ export const validateVariableName = (
return {error: 'Variable name cannot be empty'} return {error: 'Variable name cannot be empty'}
} }
const matchingName = variables.find( const lowerName = varName.toLocaleLowerCase()
v => v.name.toLocaleLowerCase() === varName.toLocaleLowerCase()
const reservedMatch = reservedVarNames.find(
r => r.toLocaleLowerCase() === lowerName
) )
if (matchingName) { if (!!reservedMatch) {
return {
error: `Variable name is reserved: ${reservedMatch}`,
}
}
const matchingName = variables.find(
v => v.name.toLocaleLowerCase() === lowerName
)
if (!!matchingName) {
return { return {
error: `Variable name must be unique`, error: `Variable name must be unique`,
} }