Merge pull request #5836 from influxdata/feat/178/tickscript_rename

feat: allow to rename TICKscript
pull/5839/head
Pavel Závora 2022-01-10 15:07:37 +01:00 committed by GitHub
commit 9b15a496c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 191 additions and 16 deletions

View File

@ -3,6 +3,7 @@
### Features
1. [#5831](https://github.com/influxdata/chronograf/pull/5831): Add download button on query management page.
1. [#5836](https://github.com/influxdata/chronograf/pull/5836): Allow to rename TICKscript.
### Bug Fixes

View File

@ -6,6 +6,7 @@ import (
"fmt"
"net/http"
"regexp"
"strings"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/id"
@ -69,7 +70,7 @@ type Task struct {
TICKScript chronograf.TICKScript // TICKScript is the running script
}
var reTaskName = regexp.MustCompile(`[\r\n]*var[ \t]+name[ \t]+=[ \t]+'([^']+)'`)
var reTaskName = regexp.MustCompile(`[\r\n]*var[ \t]+name[ \t]+=[ \t]+'([^\n]+)'[ \r\t]*\n`)
// NewTask creates a task from a kapacitor client task
func NewTask(task *client.Task) *Task {
@ -90,7 +91,7 @@ func NewTask(task *client.Task) *Task {
if rule.Name == "" {
// try to parse Name from a line such as: `var name = 'Rule Name'
if matches := reTaskName.FindStringSubmatch(task.TICKscript); matches != nil {
rule.Name = matches[1]
rule.Name = strings.ReplaceAll(strings.ReplaceAll(matches[1], "\\'", "'"), "\\\\", "\\")
} else {
rule.Name = task.ID
}

View File

@ -25,6 +25,7 @@ interface Props {
consoleMessage: string
onChangeType: (type: string) => void
onChangeID: (e: ChangeEvent<HTMLInputElement>) => void
onChangeName: (name: string) => void
isNewTickscript: boolean
unsavedChanges: boolean
}
@ -41,6 +42,7 @@ class Tickscript extends PureComponent<Props> {
onChangeScript,
onChangeType,
onChangeID,
onChangeName,
unsavedChanges,
isNewTickscript,
areLogsVisible,
@ -66,6 +68,7 @@ class Tickscript extends PureComponent<Props> {
onSelectDbrps={onSelectDbrps}
onChangeType={onChangeType}
onChangeID={onChangeID}
onChangeName={onChangeName}
task={task}
/>
<TickscriptEditor

View File

@ -2,12 +2,11 @@ import React, {Component, ChangeEvent} from 'react'
import TickscriptType from 'src/kapacitor/components/TickscriptType'
import MultiSelectDBDropdown from 'src/shared/components/MultiSelectDBDropdown'
import TickscriptID, {
TickscriptStaticID,
} from 'src/kapacitor/components/TickscriptID'
import TickscriptID from 'src/kapacitor/components/TickscriptID'
import {Task} from 'src/types'
import {DBRP} from 'src/types/kapacitor'
import TickscriptNameEditor from './TickscriptNameEditor'
interface DBRPDropdownItem extends DBRP {
name: string
@ -18,6 +17,7 @@ interface Props {
onSelectDbrps: (dbrps: DBRP[]) => void
onChangeType: (type: string) => void
onChangeID: (e: ChangeEvent<HTMLInputElement>) => void
onChangeName: (name: string) => void
task: Task
}
@ -27,7 +27,7 @@ class TickscriptEditorControls extends Component<Props> {
return (
<div className="tickscript-controls">
{this.tickscriptID}
{!task.name || task.templateID ? undefined : (
{!task.id || task.templateID ? undefined : (
<div className="tickscript-controls--right">
<TickscriptType type={task.type} onChangeType={onChangeType} />
<MultiSelectDBDropdown
@ -42,13 +42,13 @@ class TickscriptEditorControls extends Component<Props> {
}
private get tickscriptID() {
const {isNewTickscript, onChangeID, task} = this.props
const {isNewTickscript, onChangeID, onChangeName, task} = this.props
if (isNewTickscript) {
return <TickscriptID onChangeID={onChangeID} id={task.id} />
}
return <TickscriptStaticID id={this.taskID} />
return <TickscriptNameEditor name={this.taskID} onRename={onChangeName} />
}
private get taskID() {

View File

@ -1,4 +1,4 @@
import React, {Component, FunctionComponent, ChangeEvent} from 'react'
import React, {Component, ChangeEvent} from 'react'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface TickscriptIDProps {
@ -29,11 +29,4 @@ class TickscriptID extends Component<TickscriptIDProps> {
}
}
interface TickscriptStaticIDProps {
id: string
}
export const TickscriptStaticID: FunctionComponent<TickscriptStaticIDProps> = ({
id,
}) => <h1 className="tickscript-controls--name">{id}</h1>
export default TickscriptID

View File

@ -0,0 +1,70 @@
// Libraries
import React, {
KeyboardEvent,
MutableRefObject,
FocusEvent,
useRef,
useState,
} from 'react'
interface Props {
onRename: (name: string) => void
name: string
}
const TickscriptNameEditor = (props: Props) => {
const [isEditing, setEditing] = useState(false)
const inputRef: MutableRefObject<HTMLInputElement> = useRef(null)
const {name} = props
if (isEditing) {
const handleInputBlur = (e: FocusEvent<HTMLInputElement>): void => {
const {onRename} = props
const newName = e.target.value
if (newName !== name) {
onRename(newName)
}
setEditing(false)
}
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>): void => {
if (e.key === 'Enter') {
inputRef.current.blur()
}
if (e.key === 'Escape') {
inputRef.current.value = name
inputRef.current.blur()
}
}
const handleFocus = (e: FocusEvent<HTMLInputElement>): void =>
e.target.select()
return (
<div className="rename-dashboard">
<input
type="text"
className="rename-dashboard--input form-control input-sm"
defaultValue={name}
autoComplete="off"
autoFocus={true}
spellCheck={false}
onBlur={handleInputBlur}
onKeyDown={handleKeyDown}
onFocus={handleFocus}
placeholder="Name this TICKscript"
ref={inputRef}
/>
</div>
)
}
return (
<h1 className="tickscript-controls--name" onClick={() => setEditing(true)}>
{name}
<span className="icon pencil" />
</h1>
)
}
export default TickscriptNameEditor

View File

@ -25,6 +25,7 @@ import {
notifyKapacitorNotFound,
} from 'src/shared/copy/notifications'
import {ErrorHandling} from 'src/shared/decorators/errors'
import changeTaskName from '../utils/changeTaskName'
interface TaskResponse {
id: number
@ -200,6 +201,7 @@ export class TickscriptPage extends PureComponent<Props, State> {
areLogsEnabled={areLogsEnabled}
consoleMessage={consoleMessage}
onChangeID={this.handleChangeID}
onChangeName={this.handleChangeName}
onChangeType={this.handleChangeType}
isNewTickscript={!this.isEditing}
onSelectDbrps={this.handleSelectDbrps}
@ -302,6 +304,17 @@ export class TickscriptPage extends PureComponent<Props, State> {
})
}
private handleChangeName = (name: string): void => {
this.setState(state => ({
task: {
...state.task,
tickscript: changeTaskName(state.task.tickscript, name),
name,
},
unsavedChanges: true,
}))
}
private handleToggleLogsVisibility = (areLogsVisible: boolean): void => {
this.setState({areLogsVisible})
}

View File

@ -0,0 +1,29 @@
const reNameDeclaration = new RegExp(
// eslint-disable-next-line no-control-regex
"(?:^|\n)var[ \t]+name[ \t]*=[ \t]*'[^\n]*'[ \r\t]*\n"
)
function escapeName(s: string) {
return s.replace(/['\\]/gi, (str: string): string => '\\' + str)
}
/**
* Changes or creates a name variable in the supplied tickscript.
* @param tickscript tickscript
* @param newName new name
* @returns modified tickscript with a new name
*/
export default function changeTaskName(
tickscript: string,
newName: string
): string {
const match = tickscript.match(reNameDeclaration)
if (!match) {
return `var name = '${escapeName(newName)}'\n${tickscript}`
}
let retVal = match.index ? `${tickscript.substring(0, match.index)}\n` : ''
retVal += `var name = '${escapeName(newName)}'\n${tickscript.substring(
match.index + match[0].length
)}`
return retVal
}

View File

@ -35,6 +35,24 @@ $tickscript-controls-height: 60px;
font-size: 17px;
font-weight: 400;
color: $g13-mist;
.icon {
padding-left: 6px;
position: absolute;
font-size: 15px;
opacity: 0;
transition: opacity 0.25s ease;
}
&:hover {
cursor: text;
color: $g20-white;
background-color: $g2-kevlar;
border-color: $g2-kevlar;
}
&:hover .icon {
opacity: 1;
}
}
.tickscript-controls--right {
display: flex;

View File

@ -0,0 +1,47 @@
import changeTaskName from 'src/kapacitor/utils/changeTaskName'
describe('kapacitor.utils.changeTaskName', () => {
;[
{
id: 'inserts into empty tickscript',
existing: '',
name: 'my name',
result: "var name = 'my name'\n",
},
{
id: 'inserts into tickscript without var',
existing: 'var whatever = TRUE\n',
name: 'my name',
result: "var name = 'my name'\nvar whatever = TRUE\n",
},
{
id: 'inserts escaped name into tickscript without var',
existing: 'var whatever = TRUE\n',
name: "my\\'name",
result: "var name = 'my\\\\\\'name'\nvar whatever = TRUE\n",
},
{
id: 'replaces leading variable definition',
existing: "var name='otherName'\r\nWHATEVERHEREIN",
name: 'my name',
result: "var name = 'my name'\nWHATEVERHEREIN",
},
{
id: 'replaces inline variable definition',
existing: "WHATEVERBEFORE\r\nvar name='otherName'\nWHATEVERAFTER",
name: 'my name',
result: "WHATEVERBEFORE\r\nvar name = 'my name'\nWHATEVERAFTER",
},
{
id: 'replaces escaped variable definition',
existing:
"WHATEVERBEFORE\nvar \tname \t= \t'otherName'\t \nWHATEVERAFTER",
name: "my\\'name",
result: "WHATEVERBEFORE\nvar name = 'my\\\\\\'name'\nWHATEVERAFTER",
},
].forEach(test => {
it(test.id, () => {
expect(changeTaskName(test.existing, test.name)).toBe(test.result)
})
})
})