Merge branch 'master' into fun/status-page-to-TS

pull/10616/head
ebb-tide 2018-06-19 08:58:22 -07:00
commit a06a9d1a9f
87 changed files with 3536 additions and 2450 deletions

138
.circleci/config.yml Normal file
View File

@ -0,0 +1,138 @@
workflows:
version: 2
main:
jobs:
- build
- deploy-nightly:
requires:
- build
filters:
branches:
only: master
- deploy-pre-release:
requires:
- build
filters:
branches:
ignore: /.*/
tags:
only: /^[0-9]+(\.[0-9]+)*(\S*)([a|rc|beta]([0-9]+))+$/
- deploy-release:
requires:
- build
filters:
branches:
ignore: /.*/
tags:
only: /^[0-9]+(\.[0-9]+)*$/
version: 2
jobs:
build:
environment:
DOCKER_TAG: chronograf-20180327
machine: true
steps:
- checkout
- run: |
ls -lah
pwd
- run: ./etc/scripts/docker/pull.sh
- run:
name: "Run Tests"
command: >
./etc/scripts/docker/run.sh
--debug
--test
--no-build
- persist_to_workspace:
root: /home/circleci
paths:
- project
deploy-nightly:
environment:
DOCKER_TAG: chronograf-20180327
machine: true
steps:
- attach_workspace:
at: /home/circleci
- run: |
./etc/scripts/docker/run.sh \
--debug \
--clean \
--package \
--platform all \
--arch all \
--upload \
--nightly \
--bucket=dl.influxdata.com/chronograf/releases
cp build/linux/static_amd64/chronograf .
cp build/linux/static_amd64/chronoctl .
docker build -t chronograf .
docker login -u "$QUAY_USER" -p $QUAY_PASS quay.io
docker tag chronograf quay.io/influxdb/chronograf:nightly
docker push quay.io/influxdb/chronograf:nightly
- store_artifacts:
path: ./build/
deploy-pre-release:
environment:
DOCKER_TAG: chronograf-20180327
machine: true
steps:
- attach_workspace:
at: /home/circleci
- run: |
./etc/scripts/docker/run.sh \
--clean \
--debug \
--release \
--package \
--platform all \
--arch all \
--upload-overwrite \
--upload \
--bucket dl.influxdata.com/chronograf/releases
cp build/linux/static_amd64/chronograf .
cp build/linux/static_amd64/chronoctl .
docker build -t chronograf .
docker login -u "$QUAY_USER" -p $QUAY_PASS quay.io
docker tag chronograf quay.io/influxdb/chronograf:${CIRCLE_SHA1:0:7}
docker push quay.io/influxdb/chronograf:${CIRCLE_SHA1:0:7}
docker tag chronograf quay.io/influxdb/chronograf:${CIRCLE_TAG}
docker push quay.io/influxdb/chronograf:${CIRCLE_TAG}
- store_artifacts:
path: ./build/
deploy-release:
environment:
DOCKER_TAG: chronograf-20180327
machine: true
steps:
- attach_workspace:
at: /home/circleci
- run: |
./etc/scripts/docker/run.sh \
--clean \
--debug \
--release \
--package \
--platform all \
--arch all \
--upload-overwrite \
--upload \
--bucket dl.influxdata.com/chronograf/releases
cp build/linux/static_amd64/chronograf .
cp build/linux/static_amd64/chronoctl .
docker build -t chronograf .
docker login -u "$QUAY_USER" -p $QUAY_PASS quay.io
docker tag chronograf quay.io/influxdb/chronograf:${CIRCLE_SHA1:0:7}
docker push quay.io/influxdb/chronograf:${CIRCLE_SHA1:0:7}
docker tag chronograf quay.io/influxdb/chronograf:${CIRCLE_TAG}
docker push quay.io/influxdb/chronograf:${CIRCLE_TAG}
docker tag chronograf quay.io/influxdb/chronograf:latest
docker push quay.io/influxdb/chronograf:latest
- store_artifacts:
path: ./build/

View File

@ -19,6 +19,8 @@
1. [#3527](https://github.com/influxdata/chronograf/pull/3527): Ensure cell queries use constraints from TimeSelector
1. [#3573](https://github.com/influxdata/chronograf/pull/3573): Fix Gauge color selection bug
1. [#3649](https://github.com/influxdata/chronograf/pull/3649): Fix erroneous icons in Date Picker widget
1. [#3697](https://github.com/influxdata/chronograf/pull/3697): Fix allowing hyphens in basepath
1. [#3698](https://github.com/influxdata/chronograf/pull/3698): Fix error in cell when tempVar returns no values
## v1.5.0.0 [2018-05-15-RC]

View File

@ -95,7 +95,7 @@ internal.pb.go: bolt/internal/internal.proto
test: jstest gotest gotestrace
gotest:
go test ./...
go test -timeout 10s ./...
gotestrace:
go test -race ./...

View File

@ -1,91 +0,0 @@
---
machine:
services:
- docker
environment:
DOCKER_TAG: chronograf-20180327
dependencies:
override:
- ./etc/scripts/docker/pull.sh
test:
override:
- >
./etc/scripts/docker/run.sh
--debug
--test
--no-build
deployment:
master:
branch: master
commands:
- >
./etc/scripts/docker/run.sh
--debug
--clean
--package
--platform all
--arch all
--upload
--nightly
--bucket=dl.influxdata.com/chronograf/releases
- sudo chown -R ubuntu:ubuntu /home/ubuntu
- cp build/linux/static_amd64/chronograf .
- cp build/linux/static_amd64/chronoctl .
- docker build -t chronograf .
- docker login -e $QUAY_EMAIL -u "$QUAY_USER" -p $QUAY_PASS quay.io
- docker tag chronograf quay.io/influxdb/chronograf:nightly
- docker push quay.io/influxdb/chronograf:nightly
- mv ./build/* $CIRCLE_ARTIFACTS
pre-release:
tag: /^[0-9]+(\.[0-9]+)*(\S*)([a|rc|beta]([0-9]+))+$/
commands:
- >
./etc/scripts/docker/run.sh
--clean
--debug
--release
--package
--platform all
--arch all
--upload-overwrite
--upload
--bucket dl.influxdata.com/chronograf/releases
- sudo chown -R ubuntu:ubuntu /home/ubuntu
- cp build/linux/static_amd64/chronograf .
- cp build/linux/static_amd64/chronoctl .
- docker build -t chronograf .
- docker login -e $QUAY_EMAIL -u "$QUAY_USER" -p $QUAY_PASS quay.io
- docker tag chronograf quay.io/influxdb/chronograf:${CIRCLE_SHA1:0:7}
- docker push quay.io/influxdb/chronograf:${CIRCLE_SHA1:0:7}
- docker tag chronograf quay.io/influxdb/chronograf:${CIRCLE_TAG}
- docker push quay.io/influxdb/chronograf:${CIRCLE_TAG}
- mv ./build/* $CIRCLE_ARTIFACTS
release:
tag: /^[0-9]+(\.[0-9]+)*$/
commands:
- >
./etc/scripts/docker/run.sh
--clean
--debug
--release
--package
--platform all
--arch all
--upload-overwrite
--upload
--bucket dl.influxdata.com/chronograf/releases
- sudo chown -R ubuntu:ubuntu /home/ubuntu
- cp build/linux/static_amd64/chronograf .
- cp build/linux/static_amd64/chronoctl .
- docker build -t chronograf .
- docker login -e $QUAY_EMAIL -u "$QUAY_USER" -p $QUAY_PASS quay.io
- docker tag chronograf quay.io/influxdb/chronograf:${CIRCLE_SHA1:0:7}
- docker push quay.io/influxdb/chronograf:${CIRCLE_SHA1:0:7}
- docker tag chronograf quay.io/influxdb/chronograf:${CIRCLE_TAG}
- docker push quay.io/influxdb/chronograf:${CIRCLE_TAG}
- docker tag chronograf quay.io/influxdb/chronograf:latest
- docker push quay.io/influxdb/chronograf:latest
- mv ./build/* $CIRCLE_ARTIFACTS

View File

@ -540,6 +540,6 @@ func clientUsage(values client.Values) *client.Usage {
}
func validBasepath(basepath string) bool {
re := regexp.MustCompile(`(\/{1}\w+)+`)
re := regexp.MustCompile(`(\/{1}[\w-]+)+`)
return re.ReplaceAllLiteralString(basepath, "") == ""
}

View File

@ -43,6 +43,13 @@ func Test_validBasepath(t *testing.T) {
},
want: true,
},
{
name: "Basepath can include numbers, hyphens, and underscores",
args: args{
basepath: "/3shishka-bob/-rus4s_rus-1_s-",
},
want: true,
},
{
name: "Basepath is not empty and invalid - no slashes",
args: args{

View File

@ -12,7 +12,7 @@ interface State {
@ErrorHandling
class SearchBar extends PureComponent<Props, State> {
constructor(props) {
constructor(props: Props) {
super(props)
this.state = {

View File

@ -61,6 +61,7 @@ import {
Cell,
Source,
Template,
TemplateType,
URLQueryParams,
} from 'src/types'
import {CellType, DashboardName} from 'src/types/dashboard'
@ -444,7 +445,7 @@ export const getChronografVersion = () => async (): Promise<string | void> => {
const removeUnselectedTemplateValues = (dashboard: Dashboard): Template[] => {
const templates = getDeep<Template[]>(dashboard, 'templates', []).map(
template => {
if (template.type === 'csv') {
if (template.type === TemplateType.CSV) {
return template
}

View File

@ -19,10 +19,8 @@ import {getQueryConfigAndStatus} from 'src/shared/apis'
import {IS_STATIC_LEGEND} from 'src/shared/constants'
import {nextSource} from 'src/dashboards/utils/sources'
import {
removeUnselectedTemplateValues,
TYPE_QUERY_CONFIG,
} from 'src/dashboards/constants'
import {TYPE_QUERY_CONFIG} from 'src/dashboards/constants'
import {removeUnselectedTemplateValues} from 'src/tempVars/constants'
import {OVERLAY_TECHNOLOGY} from 'src/shared/constants/classNames'
import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from 'src/data_explorer/constants'
import {

View File

@ -6,7 +6,8 @@ import QueryTabList from 'src/shared/components/QueryTabList'
import QueryTextArea from 'src/dashboards/components/QueryTextArea'
import SchemaExplorer from 'src/shared/components/SchemaExplorer'
import {buildQuery} from 'src/utils/influxql'
import {TYPE_QUERY_CONFIG, TEMPLATE_RANGE} from 'src/dashboards/constants'
import {TYPE_QUERY_CONFIG} from 'src/dashboards/constants'
import {TEMPLATE_RANGE} from 'src/tempVars/constants'
import {QueryConfig, Source, SourceLinks, TimeRange} from 'src/types'
import {CellEditorOverlayActions} from 'src/dashboards/components/CellEditorOverlay'

View File

@ -12,7 +12,7 @@ import {
applyMasks,
insertTempVar,
unMask,
} from 'src/dashboards/constants'
} from 'src/tempVars/constants'
@ErrorHandling
class QueryTextArea extends Component {

View File

@ -1,71 +0,0 @@
import React, {Component} from 'react'
import _ from 'lodash'
import classnames from 'classnames'
import uuid from 'uuid'
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
import TemplateControlDropdown from 'src/dashboards/components/TemplateControlDropdown'
import {Template} from 'src/types/tempVars'
interface Props {
meRole: string
isUsingAuth: boolean
templates: Template[]
isOpen: boolean
onOpenTemplateManager: () => void
onSelectTemplate: (id: string) => void
}
class TemplateControlBar extends Component<Props> {
public shouldComponentUpdate(nextProps) {
return !_.isEqual(this.props, nextProps)
}
public render() {
const {
isOpen,
templates,
onSelectTemplate,
onOpenTemplateManager,
meRole,
isUsingAuth,
} = this.props
return (
<div className={classnames('template-control-bar', {show: isOpen})}>
<div className="template-control--container">
<div className="template-control--controls">
{templates && templates.length ? (
templates.map(template => (
<TemplateControlDropdown
key={uuid.v4()}
meRole={meRole}
isUsingAuth={isUsingAuth}
template={template}
onSelectTemplate={onSelectTemplate}
/>
))
) : (
<div className="template-control--empty" data-test="empty-state">
This dashboard does not have any{' '}
<strong>Template Variables</strong>
</div>
)}
</div>
<Authorized requiredRole={EDITOR_ROLE}>
<button
className="btn btn-primary btn-sm template-control--manage"
onClick={onOpenTemplateManager}
>
<span className="icon cog-thick" />
Manage
</button>
</Authorized>
</div>
</div>
)
}
}
export default TemplateControlBar

View File

@ -1,53 +0,0 @@
import React, {SFC} from 'react'
import Dropdown from 'src/shared/components/Dropdown'
import {calculateDropdownWidth} from 'src/dashboards/constants/templateControlBar'
import {isUserAuthorized, EDITOR_ROLE} from 'src/auth/Authorized'
import {Template} from 'src/types/tempVars'
interface Props {
template: Template
meRole: string
isUsingAuth: boolean
onSelectTemplate: (id: string) => void
}
// TODO: change Dropdown to a MultiSelectDropdown, `selected` to
// the full array, and [item] to all `selected` values when we update
// this component to support multiple values
const TemplateControlDropdown: SFC<Props> = ({
template,
onSelectTemplate,
isUsingAuth,
meRole,
}) => {
const dropdownItems = template.values.map(value => ({
...value,
text: value.value,
}))
const dropdownStyle = template.values.length
? {minWidth: calculateDropdownWidth(template.values)}
: null
const selectedItem = dropdownItems.find(item => item.selected) ||
dropdownItems[0] || {text: '(No values)'}
return (
<div className="template-control--dropdown" style={dropdownStyle}>
<Dropdown
items={dropdownItems}
buttonSize="btn-xs"
menuClass="dropdown-astronaut"
useAutoComplete={true}
selected={selectedItem.text}
disabled={isUsingAuth && !isUserAuthorized(meRole, EDITOR_ROLE)}
onChoose={onSelectTemplate(template.id)}
/>
<label className="template-control--label">{template.tempVar}</label>
</div>
)
}
export default TemplateControlDropdown

View File

@ -1,383 +0,0 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import uniq from 'lodash/uniq'
import OnClickOutside from 'react-onclickoutside'
import classnames from 'classnames'
import Dropdown from 'shared/components/Dropdown'
import TemplateQueryBuilder from 'src/dashboards/components/template_variables/TemplateQueryBuilder'
import TableInput from 'src/dashboards/components/template_variables/TableInput'
import RowValues from 'src/dashboards/components/template_variables/RowValues'
import ConfirmButton from 'src/shared/components/ConfirmButton'
import {getTempVarValuesBySourceQuery as getTempVarValuesBySourceQueryAJAX} from 'src/dashboards/apis'
import parsers from 'shared/parsing'
import {TEMPLATE_TYPES} from 'src/dashboards/constants'
import generateTemplateVariableQuery from 'src/dashboards/utils/tempVars'
import {errorThrown as errorThrownAction} from 'shared/actions/errors'
import {notify as notifyAction} from 'shared/actions/notifications'
import {notifyTempVarAlreadyExists} from 'shared/copy/notifications'
import {ErrorHandling} from 'src/shared/decorators/errors'
const compact = values => uniq(values).filter(value => /\S/.test(value))
const TemplateVariableRow = ({
template: {id, tempVar, values},
isEditing,
selectedType,
selectedDatabase,
selectedMeasurement,
onSelectType,
onSelectDatabase,
onSelectMeasurement,
selectedTagKey,
onSelectTagKey,
onStartEdit,
onCancelEdit,
autoFocusTarget,
onSubmit,
onErrorThrown,
onDeleteTempVar,
source,
}) => (
<form
className={classnames('template-variable-manager--table-row', {
editing: isEditing,
})}
onSubmit={onSubmit({
selectedType,
selectedDatabase,
selectedMeasurement,
selectedTagKey,
})}
>
<div className="tvm--col-1">
<TableInput
name="tempVar"
defaultValue={tempVar}
isEditing={isEditing}
onStartEdit={onStartEdit}
autoFocusTarget={autoFocusTarget}
/>
</div>
<div className="tvm--col-2">
<Dropdown
items={TEMPLATE_TYPES}
onChoose={onSelectType}
onClick={onStartEdit('tempVar')}
selected={TEMPLATE_TYPES.find(t => t.type === selectedType).text}
className="dropdown-140"
/>
</div>
<div className="tvm--col-3">
<TemplateQueryBuilder
source={source}
onSelectDatabase={onSelectDatabase}
selectedType={selectedType}
selectedDatabase={selectedDatabase}
onSelectMeasurement={onSelectMeasurement}
selectedMeasurement={selectedMeasurement}
selectedTagKey={selectedTagKey}
onSelectTagKey={onSelectTagKey}
onStartEdit={onStartEdit('tempVar')}
onErrorThrown={onErrorThrown}
/>
<RowValues
selectedType={selectedType}
values={values}
isEditing={isEditing}
onStartEdit={onStartEdit}
autoFocusTarget={autoFocusTarget}
/>
</div>
<div className={`tvm--col-4${isEditing ? ' editing' : ''}`}>
{isEditing ? (
<div className="tvm-actions">
<button
className="btn btn-sm btn-info btn-square"
type="button"
onClick={onCancelEdit}
>
<span className="icon remove" />
</button>
<button className="btn btn-sm btn-success btn-square" type="submit">
<span className="icon checkmark" />
</button>
</div>
) : (
<div className="tvm-actions">
<ConfirmButton
type="btn-danger"
confirmText="Delete template variable?"
confirmAction={onDeleteTempVar(id)}
icon="trash"
square={true}
/>
</div>
)}
</div>
</form>
)
@ErrorHandling
class RowWrapper extends Component {
constructor(props) {
super(props)
const {
template: {type, query, isNew},
} = this.props
this.state = {
isEditing: !!isNew,
isNew: !!isNew,
hasBeenSavedToComponentStateOnce: !isNew,
selectedType: type,
selectedDatabase: query && query.db,
selectedMeasurement: query && query.measurement,
selectedTagKey: query && query.tagKey,
autoFocusTarget: 'tempVar',
}
}
handleSubmit = ({
selectedDatabase: database,
selectedMeasurement: measurement,
selectedTagKey: tagKey,
selectedType: type,
}) => async e => {
e.preventDefault()
const {
source,
template,
template: {id},
onRunQuerySuccess,
onRunQueryFailure,
tempVarAlreadyExists,
notify,
} = this.props
const _tempVar = e.target.tempVar.value.replace(/\u003a/g, '')
const tempVar = `\u003a${_tempVar}\u003a` // add ':'s
if (tempVarAlreadyExists(tempVar, id)) {
return notify(notifyTempVarAlreadyExists(_tempVar))
}
this.setState({
isEditing: false,
hasBeenSavedToComponentStateOnce: true,
})
const {query, tempVars} = generateTemplateVariableQuery({
type,
tempVar,
query: {
database,
// rp, TODO
measurement,
tagKey,
},
})
const queryConfig = {
type,
tempVars,
query,
database,
// rp: TODO
measurement,
tagKey,
}
try {
let parsedData
if (type === 'csv') {
parsedData = e.target.values.value.split(',').map(value => value.trim())
} else {
parsedData = await this.getTempVarValuesBySourceQuery(
source,
queryConfig
)
}
onRunQuerySuccess(template, queryConfig, compact(parsedData), tempVar)
} catch (error) {
onRunQueryFailure(error)
}
}
handleClickOutside() {
this.handleCancelEdit()
}
handleStartEdit = name => () => {
this.setState({isEditing: true, autoFocusTarget: name})
}
handleCancelEdit = () => {
const {
template: {
type,
query: {db, measurement, tagKey},
id,
},
onDelete,
} = this.props
const {hasBeenSavedToComponentStateOnce} = this.state
if (!hasBeenSavedToComponentStateOnce) {
return onDelete(id)
}
this.setState({
selectedType: type,
selectedDatabase: db,
selectedMeasurement: measurement,
selectedTagKey: tagKey,
isEditing: false,
})
}
handleSelectType = item => {
this.setState({
selectedType: item.type,
selectedDatabase: null,
selectedMeasurement: null,
selectedTagKey: null,
})
}
handleSelectDatabase = item => {
this.setState({selectedDatabase: item.text})
}
handleSelectMeasurement = item => {
this.setState({selectedMeasurement: item.text})
}
handleSelectTagKey = item => {
this.setState({selectedTagKey: item.text})
}
getTempVarValuesBySourceQuery = async (
source,
{query, database, rp, tempVars, type, measurement, tagKey}
) => {
try {
const {data} = await getTempVarValuesBySourceQueryAJAX(source, {
query,
db: database,
rp,
tempVars,
})
const parsedData = parsers[type](data, tagKey || measurement) // tagKey covers tagKey and fieldKey
if (parsedData.errors.length) {
throw parsedData.errors
}
return parsedData[type]
} catch (error) {
console.error(error)
throw error
}
}
handleDelete = id => () => {
this.props.onDelete(id)
}
render() {
const {
isEditing,
selectedType,
selectedDatabase,
selectedMeasurement,
selectedTagKey,
autoFocusTarget,
} = this.state
return (
<TemplateVariableRow
{...this.props}
isEditing={isEditing}
selectedType={selectedType}
selectedDatabase={selectedDatabase}
selectedMeasurement={selectedMeasurement}
selectedTagKey={selectedTagKey}
onSelectType={this.handleSelectType}
onSelectDatabase={this.handleSelectDatabase}
onSelectMeasurement={this.handleSelectMeasurement}
onSelectTagKey={this.handleSelectTagKey}
onStartEdit={this.handleStartEdit}
onCancelEdit={this.handleCancelEdit}
autoFocusTarget={autoFocusTarget}
onSubmit={this.handleSubmit}
onDeleteTempVar={this.handleDelete}
/>
)
}
}
const {arrayOf, bool, func, shape, string} = PropTypes
RowWrapper.propTypes = {
source: shape({
links: shape({
proxy: string,
}),
}).isRequired,
template: shape({
type: string.isRequired,
tempVar: string.isRequired,
query: shape({
db: string,
influxql: string,
measurement: string,
tagKey: string,
}),
values: arrayOf(
shape({
value: string.isRequired,
type: string.isRequired,
selected: bool.isRequired,
})
).isRequired,
links: shape({
self: string.isRequired,
}),
}),
onRunQuerySuccess: func.isRequired,
onRunQueryFailure: func.isRequired,
onDelete: func.isRequired,
tempVarAlreadyExists: func.isRequired,
notify: func.isRequired,
}
TemplateVariableRow.propTypes = {
...RowWrapper.propTypes,
selectedType: string.isRequired,
selectedDatabase: string,
selectedTagKey: string,
onSelectType: func.isRequired,
onSelectDatabase: func.isRequired,
onSelectTagKey: func.isRequired,
onStartEdit: func.isRequired,
onCancelEdit: func.isRequired,
onSubmit: func.isRequired,
onErrorThrown: func.isRequired,
}
const mapDispatchToProps = dispatch => ({
onErrorThrown: bindActionCreators(errorThrownAction, dispatch),
notify: bindActionCreators(notifyAction, dispatch),
})
export default connect(null, mapDispatchToProps)(OnClickOutside(RowWrapper))

View File

@ -1,44 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import TableInput from 'src/dashboards/components/template_variables/TableInput'
const RowValues = ({
selectedType,
values = [],
isEditing,
onStartEdit,
autoFocusTarget,
}) => {
const _values = values.map(v => v.value).join(', ')
if (selectedType === 'csv') {
return (
<TableInput
name="values"
defaultValue={_values}
isEditing={isEditing}
onStartEdit={onStartEdit}
autoFocusTarget={autoFocusTarget}
spellCheck={false}
autoComplete="false"
/>
)
}
return (
<div className={values.length ? 'tvm-values' : 'tvm-values-empty'}>
{values.length ? _values : 'No values to display'}
</div>
)
}
const {arrayOf, bool, func, shape, string} = PropTypes
RowValues.propTypes = {
selectedType: string.isRequired,
values: arrayOf(shape()),
isEditing: bool.isRequired,
onStartEdit: func.isRequired,
autoFocusTarget: string,
}
export default RowValues

View File

@ -1,80 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import TemplateVariableRow from 'src/dashboards/components/template_variables/Row'
const TemplateVariableTable = ({
source,
templates,
onRunQuerySuccess,
onRunQueryFailure,
onDelete,
tempVarAlreadyExists,
}) => (
<div className="template-variable-manager--table">
{templates.length ? (
<div className="template-variable-manager--table-container">
<div className="template-variable-manager--table-heading">
<div className="tvm--col-1">Variable</div>
<div className="tvm--col-2">Type</div>
<div className="tvm--col-3">Definition / Values</div>
<div className="tvm--col-4" />
</div>
<div className="template-variable-manager--table-rows">
{templates.map(t => (
<TemplateVariableRow
key={t.id}
source={source}
template={t}
onRunQuerySuccess={onRunQuerySuccess}
onRunQueryFailure={onRunQueryFailure}
onDelete={onDelete}
tempVarAlreadyExists={tempVarAlreadyExists}
/>
))}
</div>
</div>
) : (
<div className="generic-empty-state">
<h4 style={{margin: '60px 0'}} className="no-user-select">
You have no Template Variables, why not create one?
</h4>
</div>
)}
</div>
)
const {arrayOf, bool, func, shape, string} = PropTypes
TemplateVariableTable.propTypes = {
source: shape({
links: shape({
proxy: string,
}),
}).isRequired,
templates: arrayOf(
shape({
type: string.isRequired,
tempVar: string.isRequired,
query: shape({
db: string,
influxql: string,
measurement: string,
tagKey: string,
}),
values: arrayOf(
shape({
value: string.isRequired,
type: string.isRequired,
selected: bool.isRequired,
})
).isRequired,
})
),
onRunQuerySuccess: func.isRequired,
onRunQueryFailure: func.isRequired,
onDelete: func.isRequired,
tempVarAlreadyExists: func.isRequired,
}
export default TemplateVariableTable

View File

@ -1,43 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
const TableInput = ({
name,
defaultValue,
isEditing,
onStartEdit,
autoFocusTarget,
}) => {
return isEditing ? (
<div name={name} style={{width: '100%'}}>
<input
required={true}
name={name}
autoFocus={name === autoFocusTarget}
className="form-control input-sm tvm-input-edit"
type="text"
defaultValue={
name === 'tempVar'
? defaultValue.replace(/\u003a/g, '') // remove ':'s
: defaultValue
}
/>
</div>
) : (
<div style={{width: '100%'}} onClick={onStartEdit(name)}>
<div className="tvm-input">{defaultValue}</div>
</div>
)
}
const {bool, func, string} = PropTypes
TableInput.propTypes = {
defaultValue: string,
isEditing: bool.isRequired,
onStartEdit: func.isRequired,
name: string.isRequired,
autoFocusTarget: string,
}
export default TableInput

View File

@ -1,134 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import DatabaseDropdown from 'shared/components/DatabaseDropdown'
import MeasurementDropdown from 'src/dashboards/components/MeasurementDropdown'
import TagKeyDropdown from 'src/dashboards/components/TagKeyDropdown'
const TemplateQueryBuilder = ({
selectedType,
selectedDatabase,
selectedMeasurement,
selectedTagKey,
onSelectDatabase,
onSelectMeasurement,
onSelectTagKey,
onStartEdit,
onErrorThrown,
source,
}) => {
switch (selectedType) {
case 'csv':
return null
case 'databases':
return <div className="tvm-query-builder--text">SHOW DATABASES</div>
case 'measurements':
return (
<div className="tvm-query-builder">
<span className="tvm-query-builder--text">SHOW MEASUREMENTS ON</span>
<DatabaseDropdown
source={source}
onSelectDatabase={onSelectDatabase}
database={selectedDatabase}
onStartEdit={onStartEdit}
onErrorThrown={onErrorThrown}
/>
</div>
)
case 'fieldKeys':
case 'tagKeys':
return (
<div className="tvm-query-builder">
<span className="tvm-query-builder--text">
SHOW {selectedType === 'fieldKeys' ? 'FIELD' : 'TAG'} KEYS ON
</span>
<DatabaseDropdown
source={source}
onSelectDatabase={onSelectDatabase}
database={selectedDatabase}
onStartEdit={onStartEdit}
onErrorThrown={onErrorThrown}
/>
<span className="tvm-query-builder--text">FROM</span>
{selectedDatabase ? (
<MeasurementDropdown
source={source}
database={selectedDatabase}
measurement={selectedMeasurement}
onSelectMeasurement={onSelectMeasurement}
onStartEdit={onStartEdit}
onErrorThrown={onErrorThrown}
/>
) : (
<div>No database selected</div>
)}
</div>
)
case 'tagValues':
return (
<div className="tvm-query-builder">
<span className="tvm-query-builder--text">SHOW TAG VALUES ON</span>
<DatabaseDropdown
source={source}
onSelectDatabase={onSelectDatabase}
database={selectedDatabase}
onStartEdit={onStartEdit}
onErrorThrown={onErrorThrown}
/>
<span className="tvm-query-builder--text">FROM</span>
{selectedDatabase ? (
<MeasurementDropdown
source={source}
database={selectedDatabase}
measurement={selectedMeasurement}
onSelectMeasurement={onSelectMeasurement}
onStartEdit={onStartEdit}
onErrorThrown={onErrorThrown}
/>
) : (
'Pick a DB'
)}
<span className="tvm-query-builder--text">WITH KEY =</span>
{selectedMeasurement ? (
<TagKeyDropdown
source={source}
database={selectedDatabase}
measurement={selectedMeasurement}
tagKey={selectedTagKey}
onSelectTagKey={onSelectTagKey}
onStartEdit={onStartEdit}
onErrorThrown={onErrorThrown}
/>
) : (
'Pick a Tag Key'
)}
</div>
)
default:
return (
<div>
<span className="tvm-query-builder--text">n/a</span>
</div>
)
}
}
const {func, shape, string} = PropTypes
TemplateQueryBuilder.propTypes = {
selectedType: string.isRequired,
onSelectDatabase: func.isRequired,
onSelectMeasurement: func.isRequired,
onSelectTagKey: func.isRequired,
onStartEdit: func.isRequired,
selectedMeasurement: string,
selectedDatabase: string,
selectedTagKey: string,
onErrorThrown: func.isRequired,
source: shape({
links: shape({
proxy: string.isRequired,
}).isRequired,
}).isRequired,
}
export default TemplateQueryBuilder

View File

@ -1,242 +0,0 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import uuid from 'uuid'
import TemplateVariableTable from 'src/dashboards/components/template_variables/Table'
import {TEMPLATE_VARIABLE_TYPES} from 'src/dashboards/constants'
const TemplateVariableManager = ({
source,
onClose,
onDelete,
isEdited,
templates,
onAddVariable,
onRunQuerySuccess,
onRunQueryFailure,
tempVarAlreadyExists,
onSaveTemplatesSuccess,
onEditTemplateVariables,
}) => (
<div className="template-variable-manager">
<div className="template-variable-manager--header">
<div className="page-header__left">
<h1 className="page-header__title">Template Variables</h1>
</div>
<div className="page-header__right">
<button
className="btn btn-primary btn-sm"
type="button"
onClick={onAddVariable}
>
Add Variable
</button>
<button
className={classnames('btn btn-success btn-sm', {
disabled: !isEdited,
})}
type="button"
onClick={onEditTemplateVariables(templates, onSaveTemplatesSuccess)}
>
Save Changes
</button>
<span className="page-header__dismiss" onClick={onClose} />
</div>
</div>
<div className="template-variable-manager--body">
<TemplateVariableTable
source={source}
onDelete={onDelete}
templates={templates}
onRunQuerySuccess={onRunQuerySuccess}
onRunQueryFailure={onRunQueryFailure}
tempVarAlreadyExists={tempVarAlreadyExists}
/>
</div>
</div>
)
class TemplateVariableManagerWrapper extends Component {
constructor(props) {
super(props)
this.state = {
rows: this.props.templates,
isEdited: false,
}
}
onAddVariable = () => {
const {rows} = this.state
const newRow = {
tempVar: '',
values: [],
id: uuid.v4(),
type: 'csv',
query: {
influxql: '',
db: '',
// rp, TODO
measurement: '',
tagKey: '',
},
isNew: true,
}
const newRows = [newRow, ...rows]
this.setState({rows: newRows})
}
onRunQuerySuccess = (template, queryConfig, parsedData, tempVar) => {
const {rows} = this.state
const {id, links} = template
const {
type,
query: influxql,
database: db,
measurement,
tagKey,
} = queryConfig
// Determine which is the selectedValue, if any
const currentRow = rows.find(row => row.id === id)
let selectedValue
if (currentRow && currentRow.values && currentRow.values.length) {
const matchedValue = currentRow.values.find(val => val.selected)
if (matchedValue) {
selectedValue = matchedValue.value
}
}
if (
!selectedValue &&
currentRow &&
currentRow.values &&
currentRow.values.length
) {
selectedValue = currentRow.values[0].value
}
if (!selectedValue) {
selectedValue = parsedData[0]
}
const values = parsedData.map(value => ({
value,
type: TEMPLATE_VARIABLE_TYPES[type],
selected: selectedValue === value,
}))
const templateVariable = {
tempVar,
values,
id,
type,
query: {
influxql,
db,
// rp, TODO
measurement,
tagKey,
},
links,
}
const newRows = rows.map(r => (r.id === template.id ? templateVariable : r))
this.setState({rows: newRows, isEdited: true})
}
onSaveTemplatesSuccess = () => {
const {rows} = this.state
const newRows = rows.map(row => ({...row, isNew: false}))
this.setState({rows: newRows, isEdited: false})
}
onDeleteTemplateVariable = templateID => {
const {rows} = this.state
const newRows = rows.filter(({id}) => id !== templateID)
this.setState({rows: newRows, isEdited: true})
}
tempVarAlreadyExists = (testTempVar, testID) => {
const {rows: tempVars} = this.state
return tempVars.some(
({tempVar, id}) => tempVar === testTempVar && id !== testID
)
}
handleDismissManager = () => {
const {onDismissOverlay} = this.props
const {isEdited} = this.state
if (
!isEdited ||
(isEdited && confirm('Do you want to close without saving?')) // eslint-disable-line no-alert
) {
onDismissOverlay()
}
}
render() {
const {rows, isEdited} = this.state
return (
<TemplateVariableManager
{...this.props}
templates={rows}
isEdited={isEdited}
onClose={this.handleDismissManager}
onRunQuerySuccess={this.onRunQuerySuccess}
onSaveTemplatesSuccess={this.onSaveTemplatesSuccess}
onAddVariable={this.onAddVariable}
onDelete={this.onDeleteTemplateVariable}
tempVarAlreadyExists={this.tempVarAlreadyExists}
/>
)
}
}
const {arrayOf, bool, func, shape, string} = PropTypes
TemplateVariableManager.propTypes = {
...TemplateVariableManagerWrapper.propTypes,
onRunQuerySuccess: func.isRequired,
onSaveTemplatesSuccess: func.isRequired,
onAddVariable: func.isRequired,
isEdited: bool.isRequired,
onDelete: func.isRequired,
}
TemplateVariableManagerWrapper.propTypes = {
onEditTemplateVariables: func.isRequired,
templates: arrayOf(
shape({
type: string.isRequired,
tempVar: string.isRequired,
query: shape({
db: string,
influxql: string,
}),
values: arrayOf(
shape({
value: string.isRequired,
type: string.isRequired,
selected: bool.isRequired,
})
).isRequired,
})
),
onRunQueryFailure: func.isRequired,
onDismissOverlay: func,
}
export default TemplateVariableManagerWrapper

View File

@ -4,8 +4,6 @@ import {
} from 'src/shared/constants/tableGraph'
import {Cell, QueryConfig} from 'src/types'
import {CellType, Dashboard, DecimalPlaces} from 'src/types/dashboard'
import {TimeRange} from 'src/types/query'
import {TEMP_VAR_DASHBOARD_TIME} from 'src/shared/constants'
export const UNTITLED_GRAPH: string = 'Untitled Graph'
@ -106,84 +104,7 @@ export const NEW_DASHBOARD: NewDefaultDashboard = {
cells: [NEW_DEFAULT_DASHBOARD_CELL],
}
export const TEMPLATE_TYPES = [
{
text: 'CSV',
type: 'csv',
},
{
text: 'Databases',
type: 'databases',
},
{
text: 'Measurements',
type: 'measurements',
},
{
text: 'Field Keys',
type: 'fieldKeys',
},
{
text: 'Tag Keys',
type: 'tagKeys',
},
{
text: 'Tag Values',
type: 'tagValues',
},
]
export const TEMPLATE_VARIABLE_TYPES = {
csv: 'csv',
databases: 'database',
measurements: 'measurement',
fieldKeys: 'fieldKey',
tagKeys: 'tagKey',
tagValues: 'tagValue',
}
interface TemplateVariableQueries {
databases: string
measurements: string
fieldKeys: string
tagKeys: string
tagValues: string
}
export const TEMPLATE_VARIABLE_QUERIES: TemplateVariableQueries = {
databases: 'SHOW DATABASES',
measurements: 'SHOW MEASUREMENTS ON :database:',
fieldKeys: 'SHOW FIELD KEYS ON :database: FROM :measurement:',
tagKeys: 'SHOW TAG KEYS ON :database: FROM :measurement:',
tagValues:
'SHOW TAG VALUES ON :database: FROM :measurement: WITH KEY=:tagKey:',
}
export const MATCH_INCOMPLETE_TEMPLATES = /:[\w-]*/g
export const applyMasks = query => {
const matchWholeTemplates = /:([\w-]*):/g
const maskForWholeTemplates = '😸$1😸'
return query.replace(matchWholeTemplates, maskForWholeTemplates)
}
export const insertTempVar = (query, tempVar) => {
return query.replace(MATCH_INCOMPLETE_TEMPLATES, tempVar)
}
export const unMask = query => {
return query.replace(/😸/g, ':')
}
export const removeUnselectedTemplateValues = templates => {
return templates.map(template => {
const selectedValues = template.values.filter(value => value.selected)
return {...template, values: selectedValues}
})
}
export const TYPE_QUERY_CONFIG: string = 'queryConfig'
export const TYPE_SHIFTED: string = 'shifted queryConfig'
export const TYPE_FLUX: string = 'flux'
export const DASHBOARD_NAME_MAX_LENGTH: number = 50
export const TEMPLATE_RANGE: TimeRange = {
upper: null,
lower: TEMP_VAR_DASHBOARD_TIME,
}

View File

@ -11,9 +11,8 @@ import {isUserAuthorized, EDITOR_ROLE} from 'src/auth/Authorized'
import CellEditorOverlay from 'src/dashboards/components/CellEditorOverlay'
import DashboardHeader from 'src/dashboards/components/DashboardHeader'
import Dashboard from 'src/dashboards/components/Dashboard'
import TemplateVariableManager from 'src/dashboards/components/template_variables/TemplateVariableManager'
import ManualRefresh from 'src/shared/components/ManualRefresh'
import TemplateControlBar from 'src/dashboards/components/TemplateControlBar'
import TemplateControlBar from 'src/tempVars/components/TemplateControlBar'
import {errorThrown as errorThrownAction} from 'shared/actions/errors'
import {notify as notifyAction} from 'shared/actions/notifications'
@ -48,7 +47,6 @@ import {FORMAT_INFLUXQL, defaultTimeRange} from 'src/shared/data/timeRanges'
import {colorsStringSchema, colorsNumberSchema} from 'shared/schemas'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {OverlayContext} from 'src/shared/components/OverlayTechnology'
import {getDeep} from 'src/utils/wrappers'
@ -174,31 +172,6 @@ class DashboardPage extends Component {
return topInView && bottomInView
}
handleOpenTemplateManager = () => {
const {handleShowOverlay, dashboard, source} = this.props
const options = {
dismissOnClickOutside: false,
dismissOnEscape: false,
}
handleShowOverlay(
<OverlayContext.Consumer>
{({onDismissOverlay}) => {
return (
<TemplateVariableManager
source={source}
templates={dashboard.templates}
onDismissOverlay={onDismissOverlay}
onRunQueryFailure={this.handleRunQueryFailure}
onEditTemplateVariables={this.handleEditTemplateVariables}
/>
)
}}
</OverlayContext.Consumer>,
options
)
}
handleSaveEditedCell = newCell => {
const {
dashboardActions,
@ -311,10 +284,7 @@ class DashboardPage extends Component {
dashboardActions.putDashboardByID(dashboardID)
}
handleEditTemplateVariables = (
templates,
onSaveTemplatesSuccess
) => async () => {
handleSaveTemplateVariables = async templates => {
const {location, dashboardActions, dashboard} = this.props
try {
@ -322,7 +292,6 @@ class DashboardPage extends Component {
...dashboard,
templates,
})
onSaveTemplatesSuccess()
const deletedTempVars = dashboard.templates.filter(
({tempVar: oldTempVar}) =>
!templates.find(({tempVar: newTempVar}) => oldTempVar === newTempVar)
@ -481,9 +450,10 @@ class DashboardPage extends Component {
templates={dashboard && dashboard.templates}
meRole={meRole}
isUsingAuth={isUsingAuth}
onSaveTemplates={this.handleSaveTemplateVariables}
onSelectTemplate={this.handleSelectTemplate}
onOpenTemplateManager={this.handleOpenTemplateManager}
isOpen={showTemplateControlBar}
source={source}
/>
)}
{dashboard ? (
@ -504,7 +474,6 @@ class DashboardPage extends Component {
onDeleteCell={this.handleDeleteDashboardCell}
onCloneCell={this.handleCloneCell}
showTemplateControlBar={showTemplateControlBar}
onOpenTemplateManager={this.handleOpenTemplateManager}
templatesIncludingDashTime={templatesIncludingDashTime}
onSummonOverlayTechnologies={handleShowCellEditorOverlay}
/>

View File

@ -16,7 +16,7 @@ export const initialState = {
activeCellID: '',
}
import {TEMPLATE_VARIABLE_TYPES} from 'src/dashboards/constants'
import {TEMPLATE_VARIABLE_TYPES} from 'src/tempVars/constants'
const ui = (state = initialState, action) => {
switch (action.type) {

View File

@ -1,6 +1,5 @@
import _ from 'lodash'
import {TEMPLATE_VARIABLE_QUERIES} from 'src/dashboards/constants'
import {
Dashboard,
Template,
@ -8,65 +7,7 @@ import {
TemplateValue,
URLQueryParams,
} from 'src/types'
import {TemplateUpdate} from 'src/types/tempVars'
interface PartialTemplateWithQuery {
query: string
tempVars: Array<Partial<Template>>
}
const generateTemplateVariableQuery = ({
type,
query: {
database,
// rp, TODO
measurement,
tagKey,
},
}: Partial<Template>): PartialTemplateWithQuery => {
const tempVars = []
if (database) {
tempVars.push({
tempVar: ':database:',
values: [
{
type: 'database',
value: database,
},
],
})
}
if (measurement) {
tempVars.push({
tempVar: ':measurement:',
values: [
{
type: 'measurement',
value: measurement,
},
],
})
}
if (tagKey) {
tempVars.push({
tempVar: ':tagKey:',
values: [
{
type: 'tagKey',
value: tagKey,
},
],
})
}
const query: string = TEMPLATE_VARIABLE_QUERIES[type]
return {
query,
tempVars,
}
}
import {TemplateUpdate} from 'src/types'
export const makeQueryForTemplate = ({
influxql,
@ -97,7 +38,7 @@ export const generateURLQueryParamsFromTempVars = (
return urlQueryParams
}
export const isValidTempVarOverride = (
const isValidTempVarOverride = (
values: TemplateValue[],
overrideValue: string
): boolean => !!values.find(({value}) => value === overrideValue)
@ -199,5 +140,3 @@ export const findInvalidTempVarsInURLQuery = (
return urlQueryParamsTempVarsWithInvalidValues
}
export default generateTemplateVariableQuery

View File

@ -23,7 +23,7 @@ interface State {
@ErrorHandling
class QueryEditor extends PureComponent<Props, State> {
constructor(props) {
constructor(props: Props) {
super(props)
this.state = {
value: this.props.query,

View File

@ -450,8 +450,7 @@ export const populateNamespacesAsync = (
export const getSourceAndPopulateNamespacesAsync = (sourceID: string) => async (
dispatch
): Promise<void> => {
const response = await getSource(sourceID)
const source = response.data
const source = await getSource(sourceID)
const proxyLink = getDeep<string | null>(source, 'links.proxy', null)

View File

@ -1,82 +0,0 @@
import * as api from 'shared/apis/annotation'
export const editingAnnotation = () => ({
type: 'EDITING_ANNOTATION',
})
export const dismissEditingAnnotation = () => ({
type: 'DISMISS_EDITING_ANNOTATION',
})
export const addingAnnotation = () => ({
type: 'ADDING_ANNOTATION',
})
export const addingAnnotationSuccess = () => ({
type: 'ADDING_ANNOTATION_SUCCESS',
})
export const dismissAddingAnnotation = () => ({
type: 'DISMISS_ADDING_ANNOTATION',
})
export const mouseEnterTempAnnotation = () => ({
type: 'MOUSEENTER_TEMP_ANNOTATION',
})
export const mouseLeaveTempAnnotation = () => ({
type: 'MOUSELEAVE_TEMP_ANNOTATION',
})
export const loadAnnotations = annotations => ({
type: 'LOAD_ANNOTATIONS',
payload: {
annotations,
},
})
export const updateAnnotation = annotation => ({
type: 'UPDATE_ANNOTATION',
payload: {
annotation,
},
})
export const deleteAnnotation = annotation => ({
type: 'DELETE_ANNOTATION',
payload: {
annotation,
},
})
export const addAnnotation = annotation => ({
type: 'ADD_ANNOTATION',
payload: {
annotation,
},
})
export const addAnnotationAsync = (createUrl, annotation) => async dispatch => {
dispatch(addAnnotation(annotation))
const savedAnnotation = await api.createAnnotation(createUrl, annotation)
dispatch(addAnnotation(savedAnnotation))
dispatch(deleteAnnotation(annotation))
}
export const getAnnotationsAsync = (
indexUrl,
{since, until}
) => async dispatch => {
const annotations = await api.getAnnotations(indexUrl, since, until)
dispatch(loadAnnotations(annotations))
}
export const deleteAnnotationAsync = annotation => async dispatch => {
await api.deleteAnnotation(annotation)
dispatch(deleteAnnotation(annotation))
}
export const updateAnnotationAsync = annotation => async dispatch => {
await api.updateAnnotation(annotation)
dispatch(updateAnnotation(annotation))
}

View File

@ -0,0 +1,161 @@
import * as api from 'src/shared/apis/annotation'
import {AnnotationInterface} from 'src/types'
export type Action =
| EditingAnnotationAction
| DismissEditingAnnotationAction
| AddingAnnotationAction
| AddingAnnotationSuccessAction
| DismissAddingAnnotationAction
| MouseEnterTempAnnotationAction
| MouseLeaveTempAnnotationAction
| LoadAnnotationsAction
| UpdateAnnotationAction
| DeleteAnnotationAction
| AddAnnotationAction
export interface EditingAnnotationAction {
type: 'EDITING_ANNOTATION'
}
export const editingAnnotation = (): EditingAnnotationAction => ({
type: 'EDITING_ANNOTATION',
})
export interface DismissEditingAnnotationAction {
type: 'DISMISS_EDITING_ANNOTATION'
}
export const dismissEditingAnnotation = (): DismissEditingAnnotationAction => ({
type: 'DISMISS_EDITING_ANNOTATION',
})
export interface AddingAnnotationAction {
type: 'ADDING_ANNOTATION'
}
export const addingAnnotation = (): AddingAnnotationAction => ({
type: 'ADDING_ANNOTATION',
})
export interface AddingAnnotationSuccessAction {
type: 'ADDING_ANNOTATION_SUCCESS'
}
export const addingAnnotationSuccess = (): AddingAnnotationSuccessAction => ({
type: 'ADDING_ANNOTATION_SUCCESS',
})
export interface DismissAddingAnnotationAction {
type: 'DISMISS_ADDING_ANNOTATION'
}
export const dismissAddingAnnotation = (): DismissAddingAnnotationAction => ({
type: 'DISMISS_ADDING_ANNOTATION',
})
export interface MouseEnterTempAnnotationAction {
type: 'MOUSEENTER_TEMP_ANNOTATION'
}
export const mouseEnterTempAnnotation = (): MouseEnterTempAnnotationAction => ({
type: 'MOUSEENTER_TEMP_ANNOTATION',
})
export interface MouseLeaveTempAnnotationAction {
type: 'MOUSELEAVE_TEMP_ANNOTATION'
}
export const mouseLeaveTempAnnotation = (): MouseLeaveTempAnnotationAction => ({
type: 'MOUSELEAVE_TEMP_ANNOTATION',
})
export interface LoadAnnotationsAction {
type: 'LOAD_ANNOTATIONS'
payload: {
annotations: AnnotationInterface[]
}
}
export const loadAnnotations = (
annotations: AnnotationInterface[]
): LoadAnnotationsAction => ({
type: 'LOAD_ANNOTATIONS',
payload: {
annotations,
},
})
export interface UpdateAnnotationAction {
type: 'UPDATE_ANNOTATION'
payload: {
annotation: AnnotationInterface
}
}
export const updateAnnotation = (
annotation: AnnotationInterface
): UpdateAnnotationAction => ({
type: 'UPDATE_ANNOTATION',
payload: {
annotation,
},
})
export interface DeleteAnnotationAction {
type: 'DELETE_ANNOTATION'
payload: {
annotation: AnnotationInterface
}
}
export const deleteAnnotation = (
annotation: AnnotationInterface
): DeleteAnnotationAction => ({
type: 'DELETE_ANNOTATION',
payload: {
annotation,
},
})
export interface AddAnnotationAction {
type: 'ADD_ANNOTATION'
payload: {
annotation: AnnotationInterface
}
}
export const addAnnotation = (
annotation: AnnotationInterface
): AddAnnotationAction => ({
type: 'ADD_ANNOTATION',
payload: {
annotation,
},
})
export const addAnnotationAsync = (
createUrl: string,
annotation: AnnotationInterface
) => async dispatch => {
dispatch(addAnnotation(annotation))
const savedAnnotation = await api.createAnnotation(createUrl, annotation)
dispatch(addAnnotation(savedAnnotation))
dispatch(deleteAnnotation(annotation))
}
export interface AnnotationRange {
since: number
until: number
}
export const getAnnotationsAsync = (
indexUrl: string,
{since, until}: AnnotationRange
) => async dispatch => {
const annotations = await api.getAnnotations(indexUrl, since, until)
dispatch(loadAnnotations(annotations))
}
export const deleteAnnotationAsync = (
annotation: AnnotationInterface
) => async dispatch => {
await api.deleteAnnotation(annotation)
dispatch(deleteAnnotation(annotation))
}
export const updateAnnotationAsync = (
annotation: AnnotationInterface
) => async dispatch => {
await api.updateAnnotation(annotation)
dispatch(updateAnnotation(annotation))
}

View File

@ -3,7 +3,7 @@ import {Notification} from 'src/types'
export type Action = ActionPublishNotification | ActionDismissNotification
// Publish notification
export type PubishNotification = (n: Notification) => ActionPublishNotification
export type PublishNotification = (n: Notification) => ActionPublishNotification
export interface ActionPublishNotification {
type: 'PUBLISH_NOTIFICATION'
payload: {

View File

@ -1,28 +0,0 @@
export const ANNOTATION_MIN_DELTA = 0.5
export const ADDING = 'adding'
export const EDITING = 'editing'
export const TEMP_ANNOTATION = {
id: 'tempAnnotation',
text: 'Name Me',
type: '',
startTime: '',
endTime: '',
}
export const visibleAnnotations = (graph, annotations = []) => {
const [xStart, xEnd] = graph.xAxisRange()
if (xStart === 0 && xEnd === 0) {
return []
}
return annotations.filter(a => {
if (a.endTime === a.startTime) {
return xStart <= +a.startTime && +a.startTime <= xEnd
}
return !(+a.endTime < xStart || xEnd < +a.startTime)
})
}

View File

@ -0,0 +1,37 @@
import {AnnotationInterface} from 'src/types'
export const ANNOTATION_MIN_DELTA = 0.5
export const ADDING = 'adding'
export const EDITING = 'editing'
export const TEMP_ANNOTATION: AnnotationInterface = {
id: 'tempAnnotation',
text: 'Name Me',
type: '',
startTime: null,
endTime: null,
links: {self: ''},
}
export const visibleAnnotations = (
xAxisRange: [number, number],
annotations: AnnotationInterface[] = []
): AnnotationInterface[] => {
const [xStart, xEnd] = xAxisRange
if (xStart === 0 && xEnd === 0) {
return []
}
return annotations.filter(a => {
if (a.startTime === null || a.endTime === null) {
return false
}
if (a.endTime === a.startTime) {
return xStart <= a.startTime && a.startTime <= xEnd
}
return !(a.endTime < xStart || xEnd < a.startTime)
})
}

View File

@ -1,40 +0,0 @@
import AJAX from 'src/utils/ajax'
const msToRFC = ms => ms && new Date(parseInt(ms, 10)).toISOString()
const rfcToMS = rfc3339 => rfc3339 && JSON.stringify(Date.parse(rfc3339))
const annoToMillisecond = anno => ({
...anno,
startTime: rfcToMS(anno.startTime),
endTime: rfcToMS(anno.endTime),
})
const annoToRFC = anno => ({
...anno,
startTime: msToRFC(anno.startTime),
endTime: msToRFC(anno.endTime),
})
export const createAnnotation = async (url, annotation) => {
const data = annoToRFC(annotation)
const response = await AJAX({method: 'POST', url, data})
return annoToMillisecond(response.data)
}
export const getAnnotations = async (url, since, until) => {
const {data} = await AJAX({
method: 'GET',
url,
params: {since: msToRFC(since), until: msToRFC(until)},
})
return data.annotations.map(annoToMillisecond)
}
export const deleteAnnotation = async annotation => {
const url = annotation.links.self
await AJAX({method: 'DELETE', url})
}
export const updateAnnotation = async annotation => {
const url = annotation.links.self
const data = annoToRFC(annotation)
await AJAX({method: 'PATCH', url, data})
}

View File

@ -0,0 +1,63 @@
import AJAX from 'src/utils/ajax'
import {AnnotationInterface} from 'src/types'
const msToRFCString = (ms: number) =>
ms && new Date(Math.round(ms)).toISOString()
const rfcStringToMS = (rfc3339: string) => rfc3339 && Date.parse(rfc3339)
interface ServerAnnotation {
id: string
startTime: string
endTime: string
text: string
type: string
links: {self: string}
}
const annoToMillisecond = (
annotation: ServerAnnotation
): AnnotationInterface => ({
...annotation,
startTime: rfcStringToMS(annotation.startTime),
endTime: rfcStringToMS(annotation.endTime),
})
const annoToRFC = (annotation: AnnotationInterface): ServerAnnotation => ({
...annotation,
startTime: msToRFCString(annotation.startTime),
endTime: msToRFCString(annotation.endTime),
})
export const createAnnotation = async (
url: string,
annotation: AnnotationInterface
) => {
const data = annoToRFC(annotation)
const response = await AJAX({method: 'POST', url, data})
return annoToMillisecond(response.data)
}
export const getAnnotations = async (
url: string,
since: number,
until: number
) => {
const {data} = await AJAX({
method: 'GET',
url,
params: {since: msToRFCString(since), until: msToRFCString(until)},
})
return data.annotations.map(annoToMillisecond)
}
export const deleteAnnotation = async (annotation: AnnotationInterface) => {
const url = annotation.links.self
await AJAX({method: 'DELETE', url})
}
export const updateAnnotation = async (annotation: AnnotationInterface) => {
const url = annotation.links.self
const data = annoToRFC(annotation)
await AJAX({method: 'PATCH', url, data})
}

View File

@ -9,29 +9,51 @@ export function getSources() {
})
}
export function getSource(id) {
return AJAX({
url: null,
resource: 'sources',
id,
})
export const getSource = async (id: string): Promise<Source> => {
try {
const {data: source} = await AJAX({
url: null,
resource: 'sources',
id,
})
return source
} catch (error) {
throw error
}
}
export function createSource(attributes) {
return AJAX({
url: null,
resource: 'sources',
method: 'POST',
data: attributes,
})
export const createSource = async (
attributes: Partial<Source>
): Promise<Source> => {
try {
const {data: source} = await AJAX({
url: null,
resource: 'sources',
method: 'POST',
data: attributes,
})
return source
} catch (error) {
throw error
}
}
export function updateSource(newSource) {
return AJAX({
url: newSource.links.self,
method: 'PATCH',
data: newSource,
})
export const updateSource = async (
newSource: Partial<Source>
): Promise<Source> => {
try {
const {data: source} = await AJAX({
url: newSource.links.self,
method: 'PATCH',
data: newSource,
})
return source
} catch (error) {
throw error
}
}
export function deleteSource(source) {

View File

@ -1,6 +1,6 @@
import _ from 'lodash'
import {fetchTimeSeriesAsync} from 'src/shared/actions/timeSeries'
import {removeUnselectedTemplateValues} from 'src/dashboards/constants'
import {removeUnselectedTemplateValues} from 'src/tempVars/constants'
import {intervalValuesPoints} from 'src/shared/constants'

View File

@ -1,15 +1,24 @@
import React from 'react'
import PropTypes from 'prop-types'
import React, {SFC} from 'react'
import AnnotationPoint from 'shared/components/AnnotationPoint'
import AnnotationSpan from 'shared/components/AnnotationSpan'
import AnnotationPoint from 'src/shared/components/AnnotationPoint'
import AnnotationSpan from 'src/shared/components/AnnotationSpan'
import * as schema from 'shared/schemas'
import {AnnotationInterface, DygraphClass} from 'src/types'
const Annotation = ({
interface Props {
mode: string
dWidth: number
xAxisRange: [number, number]
annotation: AnnotationInterface
dygraph: DygraphClass
staticLegendHeight: number
}
const Annotation: SFC<Props> = ({
mode,
dygraph,
dWidth,
xAxisRange,
annotation,
staticLegendHeight,
}) => (
@ -21,6 +30,7 @@ const Annotation = ({
annotation={annotation}
dWidth={dWidth}
staticLegendHeight={staticLegendHeight}
xAxisRange={xAxisRange}
/>
) : (
<AnnotationSpan
@ -29,19 +39,10 @@ const Annotation = ({
annotation={annotation}
dWidth={dWidth}
staticLegendHeight={staticLegendHeight}
xAxisRange={xAxisRange}
/>
)}
</div>
)
const {number, shape, string} = PropTypes
Annotation.propTypes = {
mode: string,
dWidth: number,
annotation: schema.annotation.isRequired,
dygraph: shape({}).isRequired,
staticLegendHeight: number,
}
export default Annotation

View File

@ -1,45 +1,25 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import React, {Component, ChangeEvent, FocusEvent, KeyboardEvent} from 'react'
import onClickOutside from 'react-onclickoutside'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface State {
isEditing: boolean
}
interface Props {
value: string
onChangeInput: (i: string) => void
onConfirmUpdate: () => void
onRejectUpdate: () => void
}
@ErrorHandling
class AnnotationInput extends Component {
state = {
class AnnotationInput extends Component<Props, State> {
public state = {
isEditing: false,
}
handleInputClick = () => {
this.setState({isEditing: true})
}
handleKeyDown = e => {
const {onConfirmUpdate, onRejectUpdate} = this.props
if (e.key === 'Enter') {
onConfirmUpdate()
this.setState({isEditing: false})
}
if (e.key === 'Escape') {
onRejectUpdate()
this.setState({isEditing: false})
}
}
handleFocus = e => {
e.target.select()
}
handleChange = e => {
this.props.onChangeInput(e.target.value)
}
handleClickOutside = () => {
this.props.onConfirmUpdate()
this.setState({isEditing: false})
}
render() {
public render() {
const {isEditing} = this.state
const {value} = this.props
@ -65,15 +45,35 @@ class AnnotationInput extends Component {
</div>
)
}
}
public handleClickOutside = () => {
this.props.onConfirmUpdate()
this.setState({isEditing: false})
}
const {func, string} = PropTypes
private handleInputClick = () => {
this.setState({isEditing: true})
}
AnnotationInput.propTypes = {
value: string,
onChangeInput: func.isRequired,
onConfirmUpdate: func.isRequired,
onRejectUpdate: func.isRequired,
private handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
const {onConfirmUpdate, onRejectUpdate} = this.props
if (e.key === 'Enter') {
onConfirmUpdate()
this.setState({isEditing: false})
}
if (e.key === 'Escape') {
onRejectUpdate()
this.setState({isEditing: false})
}
}
private handleFocus = (e: FocusEvent<HTMLInputElement>) => {
e.target.select()
}
private handleChange = (e: ChangeEvent<HTMLInputElement>) => {
this.props.onChangeInput(e.target.value)
}
}
export default onClickOutside(AnnotationInput)

View File

@ -1,49 +1,116 @@
import React from 'react'
import PropTypes from 'prop-types'
import React, {Component, MouseEvent, DragEvent} from 'react'
import {connect} from 'react-redux'
import {
DYGRAPH_CONTAINER_H_MARGIN,
DYGRAPH_CONTAINER_V_MARGIN,
DYGRAPH_CONTAINER_XLABEL_MARGIN,
} from 'shared/constants'
import {ANNOTATION_MIN_DELTA, EDITING} from 'shared/annotations/helpers'
import * as schema from 'shared/schemas'
import * as actions from 'shared/actions/annotations'
import AnnotationTooltip from 'shared/components/AnnotationTooltip'
} from 'src/shared/constants'
import {ANNOTATION_MIN_DELTA, EDITING} from 'src/shared/annotations/helpers'
import * as actions from 'src/shared/actions/annotations'
import AnnotationTooltip from 'src/shared/components/AnnotationTooltip'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {AnnotationInterface, DygraphClass} from 'src/types'
interface State {
isMouseOver: boolean
isDragging: boolean
}
interface Props {
annotation: AnnotationInterface
mode: string
xAxisRange: [number, number]
dygraph: DygraphClass
updateAnnotation: (a: AnnotationInterface) => void
updateAnnotationAsync: (a: AnnotationInterface) => void
staticLegendHeight: number
}
@ErrorHandling
class AnnotationPoint extends React.Component {
state = {
class AnnotationPoint extends Component<Props, State> {
public static defaultProps: Partial<Props> = {
staticLegendHeight: 0,
}
public state = {
isMouseOver: false,
isDragging: false,
}
handleMouseEnter = () => {
public render() {
const {annotation, mode, dygraph, staticLegendHeight} = this.props
const {isDragging} = this.state
const isEditing = mode === EDITING
const flagClass = isDragging
? 'annotation-point--flag__dragging'
: 'annotation-point--flag'
const markerClass = isDragging ? 'annotation dragging' : 'annotation'
const clickClass = isEditing
? 'annotation--click-area editing'
: 'annotation--click-area'
const markerStyles = {
left: `${dygraph.toDomXCoord(Number(annotation.startTime)) +
DYGRAPH_CONTAINER_H_MARGIN}px`,
height: `calc(100% - ${staticLegendHeight +
DYGRAPH_CONTAINER_XLABEL_MARGIN +
DYGRAPH_CONTAINER_V_MARGIN * 2}px)`,
}
return (
<div className={markerClass} style={markerStyles}>
<div
className={clickClass}
draggable={true}
onDrag={this.handleDrag}
onDragStart={this.handleDragStart}
onDragEnd={this.handleDragEnd}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
/>
<div className={flagClass} />
<AnnotationTooltip
isEditing={isEditing}
timestamp={annotation.startTime}
annotation={annotation}
onMouseLeave={this.handleMouseLeave}
annotationState={this.state}
/>
</div>
)
}
private handleMouseEnter = () => {
this.setState({isMouseOver: true})
}
handleMouseLeave = e => {
private handleMouseLeave = (e: MouseEvent<HTMLDivElement>) => {
const {annotation} = this.props
if (e.relatedTarget.id === `tooltip-${annotation.id}`) {
return this.setState({isDragging: false})
if (e.relatedTarget instanceof Element) {
if (e.relatedTarget.id === `tooltip-${annotation.id}`) {
return this.setState({isDragging: false})
}
}
this.setState({isMouseOver: false})
}
handleDragStart = () => {
private handleDragStart = () => {
this.setState({isDragging: true})
}
handleDragEnd = () => {
private handleDragEnd = () => {
const {annotation, updateAnnotationAsync} = this.props
updateAnnotationAsync(annotation)
this.setState({isDragging: false})
}
handleDrag = e => {
private handleDrag = (e: DragEvent<HTMLDivElement>) => {
if (this.props.mode !== EDITING) {
return
}
@ -83,77 +150,19 @@ class AnnotationPoint extends React.Component {
updateAnnotation({
...annotation,
startTime: `${newTime}`,
endTime: `${newTime}`,
startTime: newTime,
endTime: newTime,
})
e.preventDefault()
e.stopPropagation()
}
render() {
const {annotation, mode, dygraph, staticLegendHeight} = this.props
const {isDragging} = this.state
const isEditing = mode === EDITING
const flagClass = isDragging
? 'annotation-point--flag__dragging'
: 'annotation-point--flag'
const markerClass = isDragging ? 'annotation dragging' : 'annotation'
const clickClass = isEditing
? 'annotation--click-area editing'
: 'annotation--click-area'
const markerStyles = {
left: `${dygraph.toDomXCoord(annotation.startTime) +
DYGRAPH_CONTAINER_H_MARGIN}px`,
height: `calc(100% - ${staticLegendHeight +
DYGRAPH_CONTAINER_XLABEL_MARGIN +
DYGRAPH_CONTAINER_V_MARGIN * 2}px)`,
}
return (
<div className={markerClass} style={markerStyles}>
<div
className={clickClass}
draggable={true}
onDrag={this.handleDrag}
onDragStart={this.handleDragStart}
onDragEnd={this.handleDragEnd}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
/>
<div className={flagClass} />
<AnnotationTooltip
isEditing={isEditing}
timestamp={annotation.startTime}
annotation={annotation}
onMouseLeave={this.handleMouseLeave}
annotationState={this.state}
/>
</div>
)
}
}
const {func, number, shape, string} = PropTypes
AnnotationPoint.defaultProps = {
staticLegendHeight: 0,
}
AnnotationPoint.propTypes = {
annotation: schema.annotation.isRequired,
mode: string.isRequired,
dygraph: shape({}).isRequired,
updateAnnotation: func.isRequired,
updateAnnotationAsync: func.isRequired,
staticLegendHeight: number,
}
const mdtp = {
updateAnnotationAsync: actions.updateAnnotationAsync,
updateAnnotation: actions.updateAnnotation,

View File

@ -1,44 +1,83 @@
import React from 'react'
import PropTypes from 'prop-types'
import React, {Component, MouseEvent, DragEvent} from 'react'
import {connect} from 'react-redux'
import {
DYGRAPH_CONTAINER_H_MARGIN,
DYGRAPH_CONTAINER_V_MARGIN,
DYGRAPH_CONTAINER_XLABEL_MARGIN,
} from 'shared/constants'
import {ANNOTATION_MIN_DELTA, EDITING} from 'shared/annotations/helpers'
import * as schema from 'shared/schemas'
import * as actions from 'shared/actions/annotations'
import AnnotationTooltip from 'shared/components/AnnotationTooltip'
import AnnotationWindow from 'shared/components/AnnotationWindow'
} from 'src/shared/constants'
import * as actions from 'src/shared/actions/annotations'
import {ANNOTATION_MIN_DELTA, EDITING} from 'src/shared/annotations/helpers'
import AnnotationTooltip from 'src/shared/components/AnnotationTooltip'
import AnnotationWindow from 'src/shared/components/AnnotationWindow'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {AnnotationInterface, DygraphClass} from 'src/types'
interface State {
isMouseOver: string
isDragging: string
}
interface Props {
annotation: AnnotationInterface
mode: string
dygraph: DygraphClass
staticLegendHeight: number
updateAnnotation: (a: AnnotationInterface) => void
updateAnnotationAsync: (a: AnnotationInterface) => void
xAxisRange: [number, number]
}
@ErrorHandling
class AnnotationSpan extends React.Component {
state = {
class AnnotationSpan extends Component<Props, State> {
public static defaultProps: Partial<Props> = {
staticLegendHeight: 0,
}
public state: State = {
isDragging: null,
isMouseOver: null,
}
handleMouseEnter = direction => () => {
public render() {
const {annotation, dygraph, staticLegendHeight} = this.props
const {isDragging} = this.state
return (
<div>
<AnnotationWindow
annotation={annotation}
dygraph={dygraph}
active={!!isDragging}
staticLegendHeight={staticLegendHeight}
/>
{this.renderLeftMarker(annotation.startTime, dygraph)}
{this.renderRightMarker(annotation.endTime, dygraph)}
</div>
)
}
private handleMouseEnter = (direction: string) => () => {
this.setState({isMouseOver: direction})
}
handleMouseLeave = e => {
private handleMouseLeave = (e: MouseEvent<HTMLDivElement>) => {
const {annotation} = this.props
if (e.relatedTarget.id === `tooltip-${annotation.id}`) {
return this.setState({isDragging: null})
if (e.relatedTarget instanceof Element) {
if (e.relatedTarget.id === `tooltip-${annotation.id}`) {
return this.setState({isDragging: null})
}
}
this.setState({isMouseOver: null})
}
handleDragStart = direction => () => {
private handleDragStart = (direction: string) => () => {
this.setState({isDragging: direction})
}
handleDragEnd = () => {
private handleDragEnd = () => {
const {annotation, updateAnnotationAsync} = this.props
const [startTime, endTime] = [
annotation.startTime,
@ -54,7 +93,7 @@ class AnnotationSpan extends React.Component {
this.setState({isDragging: null})
}
handleDrag = timeProp => e => {
private handleDrag = (timeProp: string) => (e: DragEvent<HTMLDivElement>) => {
if (this.props.mode !== EDITING) {
return
}
@ -96,7 +135,10 @@ class AnnotationSpan extends React.Component {
e.stopPropagation()
}
renderLeftMarker(startTime, dygraph) {
private renderLeftMarker(
startTime: number,
dygraph: DygraphClass
): JSX.Element {
const isEditing = this.props.mode === EDITING
const {isDragging, isMouseOver} = this.state
const {annotation, staticLegendHeight} = this.props
@ -147,7 +189,10 @@ class AnnotationSpan extends React.Component {
)
}
renderRightMarker(endTime, dygraph) {
private renderRightMarker(
endTime: number,
dygraph: DygraphClass
): JSX.Element {
const isEditing = this.props.mode === EDITING
const {isDragging, isMouseOver} = this.state
const {annotation, staticLegendHeight} = this.props
@ -197,39 +242,6 @@ class AnnotationSpan extends React.Component {
</div>
)
}
render() {
const {annotation, dygraph, staticLegendHeight} = this.props
const {isDragging} = this.state
return (
<div>
<AnnotationWindow
annotation={annotation}
dygraph={dygraph}
active={!!isDragging}
staticLegendHeight={staticLegendHeight}
/>
{this.renderLeftMarker(annotation.startTime, dygraph)}
{this.renderRightMarker(annotation.endTime, dygraph)}
</div>
)
}
}
const {func, number, shape, string} = PropTypes
AnnotationSpan.defaultProps = {
staticLegendHeight: 0,
}
AnnotationSpan.propTypes = {
annotation: schema.annotation.isRequired,
mode: string.isRequired,
dygraph: shape({}).isRequired,
staticLegendHeight: number,
updateAnnotationAsync: func.isRequired,
updateAnnotation: func.isRequired,
}
const mapDispatchToProps = {

View File

@ -1,50 +1,62 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import React, {Component, MouseEvent} from 'react'
import {connect} from 'react-redux'
import moment from 'moment'
import classnames from 'classnames'
import AnnotationInput from 'src/shared/components/AnnotationInput'
import * as schema from 'shared/schemas'
import * as actions from 'shared/actions/annotations'
import * as actions from 'src/shared/actions/annotations'
import {ErrorHandling} from 'src/shared/decorators/errors'
const TimeStamp = ({time}) => (
import {AnnotationInterface} from 'src/types'
interface TimeStampProps {
time: string
}
const TimeStamp = ({time}: TimeStampProps): JSX.Element => (
<div className="annotation-tooltip--timestamp">
{`${moment(+time).format('YYYY/MM/DD HH:mm:ss.SS')}`}
</div>
)
interface AnnotationState {
isDragging: boolean
isMouseOver: boolean
}
interface Span {
spanCenter: number
tooltipLeft: number
spanWidth: number
}
interface State {
annotation: AnnotationInterface
}
interface Props {
isEditing: boolean
annotation: AnnotationInterface
timestamp: string
onMouseLeave: (e: MouseEvent<HTMLDivElement>) => {}
annotationState: AnnotationState
deleteAnnotationAsync: (a: AnnotationInterface) => void
updateAnnotationAsync: (a: AnnotationInterface) => void
span: Span
}
@ErrorHandling
class AnnotationTooltip extends Component {
state = {
class AnnotationTooltip extends Component<Props, State> {
public state = {
annotation: this.props.annotation,
}
componentWillReceiveProps = ({annotation}) => {
public componentWillReceiveProps(nextProps: Props) {
const {annotation} = nextProps
this.setState({annotation})
}
handleChangeInput = key => value => {
const {annotation} = this.state
const newAnnotation = {...annotation, [key]: value}
this.setState({annotation: newAnnotation})
}
handleConfirmUpdate = () => {
this.props.updateAnnotationAsync(this.state.annotation)
}
handleRejectUpdate = () => {
this.setState({annotation: this.props.annotation})
}
handleDelete = () => {
this.props.deleteAnnotationAsync(this.props.annotation)
}
render() {
public render() {
const {annotation} = this.state
const {
onMouseLeave,
@ -99,28 +111,30 @@ class AnnotationTooltip extends Component {
</div>
)
}
private handleChangeInput = (key: string) => (value: string) => {
const {annotation} = this.state
const newAnnotation = {...annotation, [key]: value}
this.setState({annotation: newAnnotation})
}
private handleConfirmUpdate = () => {
this.props.updateAnnotationAsync(this.state.annotation)
}
private handleRejectUpdate = () => {
this.setState({annotation: this.props.annotation})
}
private handleDelete = () => {
this.props.deleteAnnotationAsync(this.props.annotation)
}
}
const {bool, func, number, shape, string} = PropTypes
TimeStamp.propTypes = {
time: string.isRequired,
}
AnnotationTooltip.propTypes = {
isEditing: bool,
annotation: schema.annotation.isRequired,
timestamp: string,
onMouseLeave: func.isRequired,
annotationState: shape({}),
deleteAnnotationAsync: func.isRequired,
updateAnnotationAsync: func.isRequired,
span: shape({
spanCenter: number.isRequired,
spanWidth: number.isRequired,
}),
}
export default connect(null, {
const mdtp = {
deleteAnnotationAsync: actions.deleteAnnotationAsync,
updateAnnotationAsync: actions.updateAnnotationAsync,
})(AnnotationTooltip)
}
export default connect(null, mdtp)(AnnotationTooltip)

View File

@ -1,14 +1,23 @@
import React from 'react'
import PropTypes from 'prop-types'
import {
DYGRAPH_CONTAINER_H_MARGIN,
DYGRAPH_CONTAINER_V_MARGIN,
DYGRAPH_CONTAINER_XLABEL_MARGIN,
} from 'shared/constants'
import * as schema from 'shared/schemas'
} from 'src/shared/constants'
import {AnnotationInterface, DygraphClass} from 'src/types'
const windowDimensions = (anno, dygraph, staticLegendHeight) => {
interface WindowDimensionsReturn {
left: string
width: string
height: string
}
const windowDimensions = (
anno: AnnotationInterface,
dygraph: DygraphClass,
staticLegendHeight: number
): WindowDimensionsReturn => {
// TODO: export and test this function
const [startX, endX] = dygraph.xAxisRange()
const startTime = Math.max(+anno.startTime, startX)
@ -34,25 +43,23 @@ const windowDimensions = (anno, dygraph, staticLegendHeight) => {
}
}
interface AnnotationWindowProps {
annotation: AnnotationInterface
dygraph: DygraphClass
active: boolean
staticLegendHeight: number
}
const AnnotationWindow = ({
annotation,
dygraph,
active,
staticLegendHeight,
}) => (
}: AnnotationWindowProps): JSX.Element => (
<div
className={`annotation-window${active ? ' active' : ''}`}
style={windowDimensions(annotation, dygraph, staticLegendHeight)}
/>
)
const {bool, number, shape} = PropTypes
AnnotationWindow.propTypes = {
annotation: schema.annotation.isRequired,
dygraph: shape({}).isRequired,
staticLegendHeight: number,
active: bool,
}
export default AnnotationWindow

View File

@ -1,124 +0,0 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import Annotation from 'src/shared/components/Annotation'
import NewAnnotation from 'src/shared/components/NewAnnotation'
import * as schema from 'src/shared/schemas'
import {ADDING, TEMP_ANNOTATION} from 'src/shared/annotations/helpers'
import {
updateAnnotation,
addingAnnotationSuccess,
dismissAddingAnnotation,
mouseEnterTempAnnotation,
mouseLeaveTempAnnotation,
} from 'src/shared/actions/annotations'
import {visibleAnnotations} from 'src/shared/annotations/helpers'
import {ErrorHandling} from 'src/shared/decorators/errors'
@ErrorHandling
class Annotations extends Component {
render() {
const {
mode,
dWidth,
dygraph,
isTempHovering,
handleUpdateAnnotation,
handleDismissAddingAnnotation,
handleAddingAnnotationSuccess,
handleMouseEnterTempAnnotation,
handleMouseLeaveTempAnnotation,
staticLegendHeight,
} = this.props
return (
<div className="annotations-container">
{mode === ADDING &&
this.tempAnnotation && (
<NewAnnotation
dygraph={dygraph}
isTempHovering={isTempHovering}
tempAnnotation={this.tempAnnotation}
staticLegendHeight={staticLegendHeight}
onUpdateAnnotation={handleUpdateAnnotation}
onDismissAddingAnnotation={handleDismissAddingAnnotation}
onAddingAnnotationSuccess={handleAddingAnnotationSuccess}
onMouseEnterTempAnnotation={handleMouseEnterTempAnnotation}
onMouseLeaveTempAnnotation={handleMouseLeaveTempAnnotation}
/>
)}
{this.annotations.map(a => (
<Annotation
key={a.id}
mode={mode}
annotation={a}
dygraph={dygraph}
dWidth={dWidth}
staticLegendHeight={staticLegendHeight}
/>
))}
</div>
)
}
get annotations() {
return visibleAnnotations(
this.props.dygraph,
this.props.annotations
).filter(a => a.id !== TEMP_ANNOTATION.id)
}
get tempAnnotation() {
return this.props.annotations.find(a => a.id === TEMP_ANNOTATION.id)
}
}
const {arrayOf, bool, func, number, shape, string} = PropTypes
Annotations.propTypes = {
annotations: arrayOf(schema.annotation),
dygraph: shape({}).isRequired,
dWidth: number.isRequired,
mode: string,
isTempHovering: bool,
handleUpdateAnnotation: func.isRequired,
handleDismissAddingAnnotation: func.isRequired,
handleAddingAnnotationSuccess: func.isRequired,
handleMouseEnterTempAnnotation: func.isRequired,
handleMouseLeaveTempAnnotation: func.isRequired,
staticLegendHeight: number,
}
const mapStateToProps = ({
annotations: {annotations, mode, isTempHovering},
}) => ({
annotations,
mode: mode || 'NORMAL',
isTempHovering,
})
const mapDispatchToProps = dispatch => ({
handleAddingAnnotationSuccess: bindActionCreators(
addingAnnotationSuccess,
dispatch
),
handleDismissAddingAnnotation: bindActionCreators(
dismissAddingAnnotation,
dispatch
),
handleMouseEnterTempAnnotation: bindActionCreators(
mouseEnterTempAnnotation,
dispatch
),
handleMouseLeaveTempAnnotation: bindActionCreators(
mouseLeaveTempAnnotation,
dispatch
),
handleUpdateAnnotation: bindActionCreators(updateAnnotation, dispatch),
})
export default connect(mapStateToProps, mapDispatchToProps)(Annotations)

View File

@ -0,0 +1,118 @@
import React, {Component} from 'react'
import {connect} from 'react-redux'
import Annotation from 'src/shared/components/Annotation'
import NewAnnotation from 'src/shared/components/NewAnnotation'
import {SourceContext} from 'src/CheckSources'
import {ADDING, TEMP_ANNOTATION} from 'src/shared/annotations/helpers'
import {
updateAnnotation,
addingAnnotationSuccess,
dismissAddingAnnotation,
mouseEnterTempAnnotation,
mouseLeaveTempAnnotation,
} from 'src/shared/actions/annotations'
import {visibleAnnotations} from 'src/shared/annotations/helpers'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {AnnotationInterface, DygraphClass, Source} from 'src/types'
import {UpdateAnnotationAction} from 'src/shared/actions/annotations'
interface Props {
dWidth: number
staticLegendHeight: number
annotations: AnnotationInterface[]
mode: string
xAxisRange: [number, number]
dygraph: DygraphClass
isTempHovering: boolean
handleUpdateAnnotation: (
annotation: AnnotationInterface
) => UpdateAnnotationAction
handleDismissAddingAnnotation: () => void
handleAddingAnnotationSuccess: () => void
handleMouseEnterTempAnnotation: () => void
handleMouseLeaveTempAnnotation: () => void
}
@ErrorHandling
class Annotations extends Component<Props> {
public render() {
const {
mode,
dWidth,
dygraph,
xAxisRange,
isTempHovering,
handleUpdateAnnotation,
handleDismissAddingAnnotation,
handleAddingAnnotationSuccess,
handleMouseEnterTempAnnotation,
handleMouseLeaveTempAnnotation,
staticLegendHeight,
} = this.props
return (
<div className="annotations-container">
{mode === ADDING &&
this.tempAnnotation && (
<SourceContext.Consumer>
{(source: Source) => (
<NewAnnotation
dygraph={dygraph}
source={source}
isTempHovering={isTempHovering}
tempAnnotation={this.tempAnnotation}
staticLegendHeight={staticLegendHeight}
onUpdateAnnotation={handleUpdateAnnotation}
onDismissAddingAnnotation={handleDismissAddingAnnotation}
onAddingAnnotationSuccess={handleAddingAnnotationSuccess}
onMouseEnterTempAnnotation={handleMouseEnterTempAnnotation}
onMouseLeaveTempAnnotation={handleMouseLeaveTempAnnotation}
/>
)}
</SourceContext.Consumer>
)}
{this.annotations.map(a => (
<Annotation
key={a.id}
mode={mode}
xAxisRange={xAxisRange}
annotation={a}
dygraph={dygraph}
dWidth={dWidth}
staticLegendHeight={staticLegendHeight}
/>
))}
</div>
)
}
get annotations() {
return visibleAnnotations(
this.props.xAxisRange,
this.props.annotations
).filter(a => a.id !== TEMP_ANNOTATION.id)
}
get tempAnnotation() {
return this.props.annotations.find(a => a.id === TEMP_ANNOTATION.id)
}
}
const mstp = ({annotations: {annotations, mode, isTempHovering}}) => ({
annotations,
mode: mode || 'NORMAL',
isTempHovering,
})
const mdtp = {
handleAddingAnnotationSuccess: addingAnnotationSuccess,
handleDismissAddingAnnotation: dismissAddingAnnotation,
handleMouseEnterTempAnnotation: mouseEnterTempAnnotation,
handleMouseLeaveTempAnnotation: mouseLeaveTempAnnotation,
handleUpdateAnnotation: updateAnnotation,
}
export default connect(mstp, mdtp)(Annotations)

View File

@ -0,0 +1,24 @@
import React, {SFC} from 'react'
import {RemoteDataState} from 'src/types'
import LoadingSpinner from 'src/flux/components/LoadingSpinner'
import Dropdown from 'src/shared/components/Dropdown'
interface Props {
rds: RemoteDataState
children: typeof Dropdown
}
const DropdownLoadingPlaceholder: SFC<Props> = ({children, rds}) => {
if (rds === RemoteDataState.Loading) {
return (
<div className="dropdown-placeholder">
<LoadingSpinner />
</div>
)
}
return children
}
export default DropdownLoadingPlaceholder

View File

@ -75,6 +75,7 @@ interface Props {
interface State {
staticLegendHeight: null | number
isMounted: boolean
xAxisRange: [number, number]
}
@ErrorHandling
@ -110,6 +111,7 @@ class Dygraph extends Component<Props, State> {
this.state = {
staticLegendHeight: null,
isMounted: false,
xAxisRange: [0, 0],
}
this.graphRef = React.createRef<HTMLDivElement>()
@ -153,6 +155,7 @@ class Dygraph extends Component<Props, State> {
},
zoomCallback: (lower: number, upper: number) =>
this.handleZoom(lower, upper),
drawCallback: () => this.handleDraw(),
highlightCircleSize: 0,
}
@ -171,7 +174,7 @@ class Dygraph extends Component<Props, State> {
const {w} = this.dygraph.getArea()
this.props.setResolution(w)
this.setState({isMounted: true})
this.setState({isMounted: true, xAxisRange: this.dygraph.xAxisRange()})
}
public componentWillUnmount() {
@ -249,7 +252,7 @@ class Dygraph extends Component<Props, State> {
}
public render() {
const {staticLegendHeight} = this.state
const {staticLegendHeight, xAxisRange} = this.state
const {staticLegend, cellID} = this.props
return (
@ -261,6 +264,7 @@ class Dygraph extends Component<Props, State> {
dygraph={this.dygraph}
dWidth={this.dygraph.width_}
staticLegendHeight={staticLegendHeight}
xAxisRange={xAxisRange}
/>
)}
<DygraphLegend
@ -370,6 +374,12 @@ class Dygraph extends Component<Props, State> {
onZoom(this.formatTimeRange(lower), this.formatTimeRange(upper))
}
private handleDraw = () => {
if (this.dygraph) {
this.setState({xAxisRange: this.dygraph.xAxisRange()})
}
}
private eventToTimestamp = ({
pageX: pxBetweenMouseAndPage,
}: MouseEvent<HTMLDivElement>): string => {

View File

@ -1,6 +1,5 @@
import React, {PureComponent, ChangeEvent} from 'react'
import {connect} from 'react-redux'
import Dygraph from 'dygraphs'
import _ from 'lodash'
import classnames from 'classnames'
@ -13,22 +12,19 @@ import DygraphLegendSort from 'src/shared/components/DygraphLegendSort'
import {makeLegendStyles} from 'src/shared/graphs/helpers'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {NO_CELL} from 'src/shared/constants'
interface ExtendedDygraph extends Dygraph {
graphDiv: HTMLElement
}
import {DygraphClass} from 'src/types'
interface Props {
dygraph: ExtendedDygraph
dygraph: DygraphClass
cellID: string
onHide: () => void
onShow: (MouseEvent) => void
onShow: (e: MouseEvent) => void
activeCellID: string
setActiveCell: (cellID: string) => void
}
interface LegendData {
x: string | null
x: number
series: SeriesLegendData[]
xHTML: string
}
@ -48,7 +44,7 @@ interface State {
class DygraphLegend extends PureComponent<Props, State> {
private legendRef: HTMLElement | null = null
constructor(props) {
constructor(props: Props) {
super(props)
this.props.dygraph.updateOptions({
@ -175,7 +171,7 @@ class DygraphLegend extends PureComponent<Props, State> {
this.setState({filterText})
}
private handleSortLegend = sortType => () => {
private handleSortLegend = (sortType: string) => () => {
this.setState({sortType, isAscending: !this.state.isAscending})
}
@ -185,7 +181,7 @@ class DygraphLegend extends PureComponent<Props, State> {
this.props.onShow(e)
}
private legendFormatter = legend => {
private legendFormatter = (legend: LegendData) => {
if (!legend.x) {
return ''
}
@ -205,7 +201,7 @@ class DygraphLegend extends PureComponent<Props, State> {
return ''
}
private unhighlightCallback = e => {
private unhighlightCallback = (e: MouseEvent) => {
const {top, bottom, left, right} = this.legendRef.getBoundingClientRect()
const mouseY = e.clientY

View File

@ -78,7 +78,7 @@ export default class LayoutCell extends Component<Props> {
(acc, template) => {
const {tempVar} = template
const templateValue = template.values.find(v => v.selected)
const value = templateValue.value
const value = _.get(templateValue, 'value', str)
const regex = new RegExp(tempVar, 'g')
return acc.replace(regex, value)
},

View File

@ -1,120 +1,47 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import React, {Component, MouseEvent} from 'react'
import classnames from 'classnames'
import {connect} from 'react-redux'
import uuid from 'uuid'
import OnClickOutside from 'shared/components/OnClickOutside'
import AnnotationWindow from 'shared/components/AnnotationWindow'
import * as schema from 'shared/schemas'
import * as actions from 'shared/actions/annotations'
import OnClickOutside from 'src/shared/components/OnClickOutside'
import AnnotationWindow from 'src/shared/components/AnnotationWindow'
import * as actions from 'src/shared/actions/annotations'
import {DYGRAPH_CONTAINER_XLABEL_MARGIN} from 'shared/constants'
import {DYGRAPH_CONTAINER_XLABEL_MARGIN} from 'src/shared/constants'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {AnnotationInterface, DygraphClass, Source} from 'src/types'
interface Props {
dygraph: DygraphClass
source: Source
isTempHovering: boolean
tempAnnotation: AnnotationInterface
addAnnotationAsync: (url: string, a: AnnotationInterface) => void
onDismissAddingAnnotation: () => void
onAddingAnnotationSuccess: () => void
onUpdateAnnotation: (a: AnnotationInterface) => void
onMouseEnterTempAnnotation: () => void
onMouseLeaveTempAnnotation: () => void
staticLegendHeight: number
}
interface State {
isMouseOver: boolean
gatherMode: string
}
@ErrorHandling
class NewAnnotation extends Component {
state = {
isMouseOver: false,
gatherMode: 'startTime',
}
clampWithinGraphTimerange = timestamp => {
const [xRangeStart] = this.props.dygraph.xAxisRange()
return Math.max(xRangeStart, timestamp)
}
eventToTimestamp = ({pageX: pxBetweenMouseAndPage}) => {
const {left: pxBetweenGraphAndPage} = this.wrapper.getBoundingClientRect()
const graphXCoordinate = pxBetweenMouseAndPage - pxBetweenGraphAndPage
const timestamp = this.props.dygraph.toDataXCoord(graphXCoordinate)
const clamped = this.clampWithinGraphTimerange(timestamp)
return `${clamped}`
}
handleMouseDown = e => {
const startTime = this.eventToTimestamp(e)
this.props.onUpdateAnnotation({...this.props.tempAnnotation, startTime})
this.setState({gatherMode: 'endTime'})
}
handleMouseMove = e => {
if (this.props.isTempHovering === false) {
return
}
const {tempAnnotation, onUpdateAnnotation} = this.props
const newTime = this.eventToTimestamp(e)
if (this.state.gatherMode === 'startTime') {
onUpdateAnnotation({
...tempAnnotation,
startTime: newTime,
endTime: newTime,
})
} else {
onUpdateAnnotation({...tempAnnotation, endTime: newTime})
}
}
handleMouseUp = e => {
const {
tempAnnotation,
onUpdateAnnotation,
addAnnotationAsync,
onAddingAnnotationSuccess,
onMouseLeaveTempAnnotation,
} = this.props
const createUrl = this.context.source.links.annotations
const upTime = this.eventToTimestamp(e)
const downTime = tempAnnotation.startTime
const [startTime, endTime] = [downTime, upTime].sort()
const newAnnotation = {...tempAnnotation, startTime, endTime}
onUpdateAnnotation(newAnnotation)
addAnnotationAsync(createUrl, {...newAnnotation, id: uuid.v4()})
onAddingAnnotationSuccess()
onMouseLeaveTempAnnotation()
this.setState({
class NewAnnotation extends Component<Props, State> {
public wrapperRef: React.RefObject<HTMLDivElement>
constructor(props: Props) {
super(props)
this.wrapperRef = React.createRef<HTMLDivElement>()
this.state = {
isMouseOver: false,
gatherMode: 'startTime',
})
}
handleMouseOver = e => {
this.setState({isMouseOver: true})
this.handleMouseMove(e)
this.props.onMouseEnterTempAnnotation()
}
handleMouseLeave = () => {
this.setState({isMouseOver: false})
this.props.onMouseLeaveTempAnnotation()
}
handleClickOutside = () => {
const {onDismissAddingAnnotation, isTempHovering} = this.props
if (!isTempHovering) {
onDismissAddingAnnotation()
}
}
renderTimestamp(time) {
const timestamp = `${new Date(+time)}`
return (
<div className="new-annotation-tooltip">
<span className="new-annotation-helper">Click or Drag to Annotate</span>
<span className="new-annotation-timestamp">{timestamp}</span>
</div>
)
}
render() {
public render() {
const {
dygraph,
isTempHovering,
@ -123,7 +50,6 @@ class NewAnnotation extends Component {
staticLegendHeight,
} = this.props
const {isMouseOver} = this.state
const crosshairOne = Math.max(-1000, dygraph.toDomXCoord(startTime))
const crosshairTwo = dygraph.toDomXCoord(endTime)
const crosshairHeight = `calc(100% - ${staticLegendHeight +
@ -154,7 +80,7 @@ class NewAnnotation extends Component {
className={classnames('new-annotation', {
hover: isTempHovering,
})}
ref={el => (this.wrapper = el)}
ref={this.wrapperRef}
onMouseMove={this.handleMouseMove}
onMouseOver={this.handleMouseOver}
onMouseLeave={this.handleMouseLeave}
@ -185,29 +111,105 @@ class NewAnnotation extends Component {
</div>
)
}
}
const {bool, func, number, shape, string} = PropTypes
public handleClickOutside = () => {
const {onDismissAddingAnnotation, isTempHovering} = this.props
if (!isTempHovering) {
onDismissAddingAnnotation()
}
}
NewAnnotation.contextTypes = {
source: shape({
links: shape({
annotations: string,
}),
}),
}
private clampWithinGraphTimerange = (timestamp: number): number => {
const [xRangeStart] = this.props.dygraph.xAxisRange()
return Math.max(xRangeStart, timestamp)
}
NewAnnotation.propTypes = {
dygraph: shape({}).isRequired,
isTempHovering: bool,
tempAnnotation: schema.annotation.isRequired,
addAnnotationAsync: func.isRequired,
onDismissAddingAnnotation: func.isRequired,
onAddingAnnotationSuccess: func.isRequired,
onUpdateAnnotation: func.isRequired,
onMouseEnterTempAnnotation: func.isRequired,
onMouseLeaveTempAnnotation: func.isRequired,
staticLegendHeight: number,
private eventToTimestamp = ({
pageX: pxBetweenMouseAndPage,
}: MouseEvent<HTMLDivElement>): number => {
const {
left: pxBetweenGraphAndPage,
} = this.wrapperRef.current.getBoundingClientRect()
const graphXCoordinate = pxBetweenMouseAndPage - pxBetweenGraphAndPage
const timestamp = this.props.dygraph.toDataXCoord(graphXCoordinate)
const clamped = this.clampWithinGraphTimerange(timestamp)
return clamped
}
private handleMouseDown = (e: MouseEvent<HTMLDivElement>) => {
const startTime = this.eventToTimestamp(e)
this.props.onUpdateAnnotation({...this.props.tempAnnotation, startTime})
this.setState({gatherMode: 'endTime'})
}
private handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
if (this.props.isTempHovering === false) {
return
}
const {tempAnnotation, onUpdateAnnotation} = this.props
const newTime = this.eventToTimestamp(e)
if (this.state.gatherMode === 'startTime') {
onUpdateAnnotation({
...tempAnnotation,
startTime: newTime,
endTime: newTime,
})
} else {
onUpdateAnnotation({...tempAnnotation, endTime: newTime})
}
}
private handleMouseUp = (e: MouseEvent<HTMLDivElement>) => {
const {
tempAnnotation,
onUpdateAnnotation,
addAnnotationAsync,
onAddingAnnotationSuccess,
onMouseLeaveTempAnnotation,
source,
} = this.props
const createUrl = source.links.annotations
const upTime = this.eventToTimestamp(e)
const downTime = tempAnnotation.startTime
const [startTime, endTime] = [downTime, upTime].sort()
const newAnnotation = {...tempAnnotation, startTime, endTime}
onUpdateAnnotation(newAnnotation)
addAnnotationAsync(createUrl, {...newAnnotation, id: uuid.v4()})
onAddingAnnotationSuccess()
onMouseLeaveTempAnnotation()
this.setState({
isMouseOver: false,
gatherMode: 'startTime',
})
}
private handleMouseOver = (e: MouseEvent<HTMLDivElement>) => {
this.setState({isMouseOver: true})
this.handleMouseMove(e)
this.props.onMouseEnterTempAnnotation()
}
private handleMouseLeave = () => {
this.setState({isMouseOver: false})
this.props.onMouseLeaveTempAnnotation()
}
private renderTimestamp(time: number): JSX.Element {
const timestamp = `${new Date(time)}`
return (
<div className="new-annotation-tooltip">
<span className="new-annotation-helper">Click or Drag to Annotate</span>
<span className="new-annotation-timestamp">{timestamp}</span>
</div>
)
}
}
const mdtp = {

View File

@ -0,0 +1,12 @@
import React, {SFC} from 'react'
const SimpleOverlayTechnology: SFC = ({children}) => {
return (
<div className="overlay-tech show">
<div className="overlay--dialog">{children}</div>
<div className="overlay--mask" />
</div>
)
}
export default SimpleOverlayTechnology

View File

@ -1,5 +1,6 @@
import _ from 'lodash'
import {TemplateValueType, TemplateType} from 'src/types'
import {CellType} from 'src/types/dashboard'
export const NO_CELL = 'none'
@ -461,12 +462,12 @@ export const DEFAULT_SOURCE = {
export const defaultIntervalValue = '333'
export const intervalValuesPoints = [
{value: defaultIntervalValue, type: 'points', selected: true},
{value: defaultIntervalValue, type: TemplateValueType.Points, selected: true},
]
export const interval = {
id: 'interval',
type: 'autoGroupBy',
type: TemplateType.AutoGroupBy,
tempVar: TEMP_VAR_INTERVAL,
label: 'automatically determine the best group by time',
values: intervalValuesPoints,

View File

@ -1,12 +1,24 @@
import {ADDING, EDITING, TEMP_ANNOTATION} from 'src/shared/annotations/helpers'
import {Action} from 'src/shared/actions/annotations'
import {AnnotationInterface} from 'src/types'
export interface AnnotationState {
mode: string
isTempHovering: boolean
annotations: AnnotationInterface[]
}
const initialState = {
mode: null,
isTempHovering: false,
annotations: [],
}
const annotationsReducer = (state = initialState, action) => {
const annotationsReducer = (
state: AnnotationState = initialState,
action: Action
) => {
switch (action.type) {
case 'EDITING_ANNOTATION': {
return {

View File

@ -1,208 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import {connect} from 'react-redux'
import _ from 'lodash'
import {insecureSkipVerifyText} from 'shared/copy/tooltipText'
import {SUPERADMIN_ROLE} from 'src/auth/Authorized'
export const SourceForm = ({
source,
editMode,
onSubmit,
onInputChange,
onBlurSourceURL,
isUsingAuth,
gotoPurgatory,
isInitialSource,
me,
}) => (
<div className="panel-body">
{isUsingAuth && isInitialSource ? (
<div className="text-center">
{me.role === SUPERADMIN_ROLE ? (
<h3>
<strong>{me.currentOrganization.name}</strong> has no connections
</h3>
) : (
<h3>
<strong>{me.currentOrganization.name}</strong> has no connections
available to <em>{me.role}s</em>
</h3>
)}
<h6>Add a Connection below:</h6>
</div>
) : null}
<form onSubmit={onSubmit}>
<div className="form-group col-xs-12 col-sm-6">
<label htmlFor="connect-string">Connection String</label>
<input
type="text"
name="url"
className="form-control"
id="connect-string"
placeholder="Address of InfluxDB"
onChange={onInputChange}
value={source.url}
onBlur={onBlurSourceURL}
required={true}
/>
</div>
<div className="form-group col-xs-12 col-sm-6">
<label htmlFor="name">Name</label>
<input
type="text"
name="name"
className="form-control"
id="name"
placeholder="Name this source"
onChange={onInputChange}
value={source.name}
required={true}
/>
</div>
<div className="form-group col-xs-12 col-sm-6">
<label htmlFor="username">Username</label>
<input
type="text"
name="username"
className="form-control"
id="username"
onChange={onInputChange}
value={source.username}
/>
</div>
<div className="form-group col-xs-12 col-sm-6">
<label htmlFor="password">Password</label>
<input
type="password"
name="password"
className="form-control"
id="password"
onChange={onInputChange}
value={source.password}
/>
</div>
{_.get(source, 'type', '').includes('enterprise') ? (
<div className="form-group col-xs-12">
<label htmlFor="meta-url">Meta Service Connection URL</label>
<input
type="text"
name="metaUrl"
className="form-control"
id="meta-url"
placeholder="http://localhost:8091"
onChange={onInputChange}
value={source.metaUrl}
/>
</div>
) : null}
<div className="form-group col-xs-12 col-sm-6">
<label htmlFor="telegraf">Telegraf Database</label>
<input
type="text"
name="telegraf"
className="form-control"
id="telegraf"
onChange={onInputChange}
value={source.telegraf}
/>
</div>
<div className="form-group col-xs-12 col-sm-6">
<label htmlFor="defaultRP">Default Retention Policy</label>
<input
type="text"
name="defaultRP"
className="form-control"
id="defaultRP"
onChange={onInputChange}
value={source.defaultRP}
/>
</div>
<div className="form-group col-xs-12">
<div className="form-control-static">
<input
type="checkbox"
id="defaultConnectionCheckbox"
name="default"
checked={source.default}
onChange={onInputChange}
/>
<label htmlFor="defaultConnectionCheckbox">
Make this the default connection
</label>
</div>
</div>
{_.get(source, 'url', '').startsWith('https') ? (
<div className="form-group col-xs-12">
<div className="form-control-static">
<input
type="checkbox"
id="insecureSkipVerifyCheckbox"
name="insecureSkipVerify"
checked={source.insecureSkipVerify}
onChange={onInputChange}
/>
<label htmlFor="insecureSkipVerifyCheckbox">Unsafe SSL</label>
</div>
<label className="form-helper">{insecureSkipVerifyText}</label>
</div>
) : null}
<div className="form-group form-group-submit text-center col-xs-12 col-sm-6 col-sm-offset-3">
<button
className={classnames('btn btn-block', {
'btn-primary': editMode,
'btn-success': !editMode,
})}
type="submit"
>
<span className={`icon ${editMode ? 'checkmark' : 'plus'}`} />
{editMode ? 'Save Changes' : 'Add Connection'}
</button>
<br />
{isUsingAuth ? (
<button className="btn btn-link btn-sm" onClick={gotoPurgatory}>
<span className="icon shuffle" /> Switch Orgs
</button>
) : null}
</div>
</form>
</div>
)
const {bool, func, shape, string} = PropTypes
SourceForm.propTypes = {
source: shape({
url: string.isRequired,
name: string.isRequired,
username: string.isRequired,
password: string.isRequired,
telegraf: string.isRequired,
insecureSkipVerify: bool.isRequired,
default: bool.isRequired,
metaUrl: string.isRequired,
}).isRequired,
editMode: bool.isRequired,
onInputChange: func.isRequired,
onSubmit: func.isRequired,
onBlurSourceURL: func.isRequired,
me: shape({
role: string,
currentOrganization: shape({
id: string.isRequired,
name: string.isRequired,
}),
}),
isUsingAuth: bool,
isInitialSource: bool,
gotoPurgatory: func,
}
const mapStateToProps = ({auth: {isUsingAuth, me}}) => ({isUsingAuth, me})
export default connect(mapStateToProps)(SourceForm)

View File

@ -0,0 +1,224 @@
import React, {PureComponent, FocusEvent, MouseEvent, ChangeEvent} from 'react'
import classnames from 'classnames'
import {connect} from 'react-redux'
import _ from 'lodash'
import {insecureSkipVerifyText} from 'src/shared/copy/tooltipText'
import {SUPERADMIN_ROLE} from 'src/auth/Authorized'
import {Source, Me} from 'src/types'
interface Props {
me: Me
source: Partial<Source>
editMode: boolean
isUsingAuth: boolean
gotoPurgatory: () => void
isInitialSource: boolean
onSubmit: (e: MouseEvent<HTMLFormElement>) => void
onInputChange: (e: ChangeEvent<HTMLInputElement>) => void
onBlurSourceURL: (e: FocusEvent<HTMLInputElement>) => void
}
export class SourceForm extends PureComponent<Props> {
public render() {
const {
source,
onSubmit,
isUsingAuth,
onInputChange,
gotoPurgatory,
onBlurSourceURL,
isInitialSource,
} = this.props
return (
<div className="panel-body">
{isUsingAuth && isInitialSource && this.authIndicatior}
<form onSubmit={onSubmit}>
<div className="form-group col-xs-12 col-sm-6">
<label htmlFor="connect-string">Connection String</label>
<input
type="text"
name="url"
className="form-control"
id="connect-string"
placeholder="Address of InfluxDB"
onChange={onInputChange}
value={source.url}
onBlur={onBlurSourceURL}
required={true}
/>
</div>
<div className="form-group col-xs-12 col-sm-6">
<label htmlFor="name">Name</label>
<input
type="text"
name="name"
className="form-control"
id="name"
placeholder="Name this source"
onChange={onInputChange}
value={source.name}
required={true}
/>
</div>
<div className="form-group col-xs-12 col-sm-6">
<label htmlFor="username">Username</label>
<input
type="text"
name="username"
className="form-control"
id="username"
onChange={onInputChange}
value={source.username}
/>
</div>
<div className="form-group col-xs-12 col-sm-6">
<label htmlFor="password">Password</label>
<input
type="password"
name="password"
className="form-control"
id="password"
onChange={onInputChange}
value={source.password}
/>
</div>
{this.isEnterprise && (
<div className="form-group col-xs-12">
<label htmlFor="meta-url">Meta Service Connection URL</label>
<input
type="text"
name="metaUrl"
className="form-control"
id="meta-url"
placeholder="http://localhost:8091"
onChange={onInputChange}
value={source.metaUrl}
/>
</div>
)}
<div className="form-group col-xs-12 col-sm-6">
<label htmlFor="telegraf">Telegraf Database</label>
<input
type="text"
name="telegraf"
className="form-control"
id="telegraf"
onChange={onInputChange}
value={source.telegraf}
/>
</div>
<div className="form-group col-xs-12 col-sm-6">
<label htmlFor="defaultRP">Default Retention Policy</label>
<input
type="text"
name="defaultRP"
className="form-control"
id="defaultRP"
onChange={onInputChange}
value={source.defaultRP}
/>
</div>
<div className="form-group col-xs-12">
<div className="form-control-static">
<input
type="checkbox"
id="defaultConnectionCheckbox"
name="default"
checked={source.default}
onChange={onInputChange}
/>
<label htmlFor="defaultConnectionCheckbox">
Make this the default connection
</label>
</div>
</div>
{this.isHTTPS && (
<div className="form-group col-xs-12">
<div className="form-control-static">
<input
type="checkbox"
id="insecureSkipVerifyCheckbox"
name="insecureSkipVerify"
checked={source.insecureSkipVerify}
onChange={onInputChange}
/>
<label htmlFor="insecureSkipVerifyCheckbox">Unsafe SSL</label>
</div>
<label className="form-helper">{insecureSkipVerifyText}</label>
</div>
)}
<div className="form-group form-group-submit text-center col-xs-12 col-sm-6 col-sm-offset-3">
<button className={this.submitClass} type="submit">
<span className={this.submitIconClass} />
{this.submitText}
</button>
<br />
{isUsingAuth && (
<button className="btn btn-link btn-sm" onClick={gotoPurgatory}>
<span className="icon shuffle" /> Switch Orgs
</button>
)}
</div>
</form>
</div>
)
}
private get authIndicatior(): JSX.Element {
const {me} = this.props
return (
<div className="text-center">
{me.role.name === SUPERADMIN_ROLE ? (
<h3>
<strong>{me.currentOrganization.name}</strong> has no connections
</h3>
) : (
<h3>
<strong>{me.currentOrganization.name}</strong> has no connections
available to <em>{me.role}s</em>
</h3>
)}
<h6>Add a Connection below:</h6>
</div>
)
}
private get submitText(): string {
const {editMode} = this.props
if (editMode) {
return 'Save Changes'
}
return 'Add Connection'
}
private get submitIconClass(): string {
const {editMode} = this.props
return `icon ${editMode ? 'checkmark' : 'plus'}`
}
private get submitClass(): string {
const {editMode} = this.props
return classnames('btn btn-block', {
'btn-primary': editMode,
'btn-success': !editMode,
})
}
private get isEnterprise(): boolean {
const {source} = this.props
return _.get(source, 'type', '').includes('enterprise')
}
private get isHTTPS(): boolean {
const {source} = this.props
return _.get(source, 'url', '').startsWith('https')
}
}
const mapStateToProps = ({auth: {isUsingAuth, me}}) => ({isUsingAuth, me})
export default connect(mapStateToProps)(SourceForm)

View File

@ -1,72 +1,67 @@
import React, {Component, ChangeEvent, FormEvent} from 'react'
import {withRouter, InjectedRouter} from 'react-router'
import {Location} from 'history'
import React, {PureComponent, ChangeEvent, FormEvent} from 'react'
import {withRouter, WithRouterProps} from 'react-router'
import {getSource} from 'src/shared/apis'
import {createSource, updateSource} from 'src/shared/apis'
import {
addSource as addSourceAction,
updateSource as updateSourceAction,
AddSource,
UpdateSource,
} from 'src/shared/actions/sources'
import {notify as notifyAction} from 'src/shared/actions/notifications'
import {
notify as notifyAction,
PublishNotification,
} from 'src/shared/actions/notifications'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import Notifications from 'src/shared/components/Notifications'
import SourceForm from 'src/sources/components/SourceForm'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import SourceIndicator from 'src/shared/components/SourceIndicator'
import {DEFAULT_SOURCE} from 'src/shared/constants'
const initialPath = '/sources/new'
import {
notifyErrorConnectingToSource,
notifySourceCreationSucceeded,
notifySourceCreationFailed,
notifySourceUdpated,
notifySourceUdpateFailed,
notifySourceCreationFailed,
notifyErrorConnectingToSource,
notifySourceCreationSucceeded,
} from 'src/shared/copy/notifications'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {Source, Notification, NotificationFunc} from 'src/types'
import {Source} from 'src/types'
import {getDeep} from 'src/utils/wrappers'
interface Params {
id: string
hash: string
sourceID: string
}
const INITIAL_PATH = '/sources/new'
interface Props {
location: Location
router: InjectedRouter
params: Params
notify: (notification: Notification | NotificationFunc) => void
addSource: (s: Source) => void
updateSource: (s: Source) => void
interface Props extends WithRouterProps {
notify: PublishNotification
addSource: AddSource
updateSource: UpdateSource
}
interface State {
isLoading: boolean
isCreated: boolean
source: Source
source: Partial<Source>
editMode: boolean
isInitialSource: boolean
}
@ErrorHandling
class SourcePage extends Component<Props, State> {
constructor(props: Props) {
class SourcePage extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
isLoading: true,
isCreated: false,
source: DEFAULT_SOURCE,
editMode: props.params.id !== undefined,
isInitialSource: props.location.pathname === initialPath,
editMode: this.props.params.id !== undefined,
isInitialSource: this.props.location.pathname === INITIAL_PATH,
}
}
public componentDidMount() {
public async componentDidMount() {
const {editMode} = this.state
const {params, notify} = this.props
@ -74,17 +69,16 @@ class SourcePage extends Component<Props, State> {
return this.setState({isLoading: false})
}
getSource(params.id)
.then(({data: source}) => {
this.setState({
source: {...DEFAULT_SOURCE, ...source},
isLoading: false,
})
})
.catch(error => {
notify(notifyErrorConnectingToSource(this.parseError(error)))
this.setState({isLoading: false})
try {
const source = await getSource(params.id)
this.setState({
source: {...DEFAULT_SOURCE, ...source},
isLoading: false,
})
} catch (error) {
notify(notifyErrorConnectingToSource(this.parseError(error)))
this.setState({isLoading: false})
}
}
public render() {
@ -182,70 +176,70 @@ class SourcePage extends Component<Props, State> {
this.setState(this.normalizeSource, this.updateSource)
}
private gotoPurgatory = () => {
private gotoPurgatory = (): void => {
const {router} = this.props
router.push('/purgatory')
}
private normalizeSource() {
const {source} = this.state
const url = source.url.trim()
// private normalizeSource({source}) {
// const url = source.url.trim()
if (source.url.startsWith('http')) {
return {source: {...source, url}}
}
return {source: {...source, url: `http://${url}`}}
}
private createSourceOnBlur = () => {
private createSourceOnBlur = async () => {
const {source} = this.state
// if there is a type on source it has already been created
if (source.type) {
return
}
createSource(source)
.then(({data: sourceFromServer}) => {
this.props.addSource(sourceFromServer)
this.setState({
source: {...DEFAULT_SOURCE, ...sourceFromServer},
isCreated: true,
})
})
.catch(err => {
// dont want to flash this until they submit
const error = this.parseError(err)
console.error('Error creating InfluxDB connection: ', error)
try {
const sourceFromServer = await createSource(source)
this.props.addSource(sourceFromServer)
this.setState({
source: {...DEFAULT_SOURCE, ...sourceFromServer},
isCreated: true,
})
} catch (err) {
// dont want to flash this until they submit
const error = this.parseError(err)
console.error('Error creating InfluxDB connection: ', error)
}
}
private createSource = () => {
private createSource = async () => {
const {source} = this.state
const {notify} = this.props
createSource(source)
.then(({data: sourceFromServer}) => {
this.props.addSource(sourceFromServer)
this.redirect(sourceFromServer)
notify(notifySourceCreationSucceeded(source.name))
})
.catch(error => {
notify(notifySourceCreationFailed(source.name, this.parseError(error)))
})
try {
const sourceFromServer = await createSource(source)
this.props.addSource(sourceFromServer)
this.redirect(sourceFromServer)
notify(notifySourceCreationSucceeded(source.name))
} catch (err) {
// dont want to flash this until they submit
notify(notifySourceCreationFailed(source.name, this.parseError(err)))
}
}
private updateSource = () => {
private updateSource = async () => {
const {source} = this.state
const {notify} = this.props
updateSource(source)
.then(({data: sourceFromServer}) => {
this.props.updateSource(sourceFromServer)
this.redirect(sourceFromServer)
notify(notifySourceUdpated(source.name))
})
.catch(error => {
notify(notifySourceUdpateFailed(source.name, this.parseError(error)))
})
try {
const sourceFromServer = await updateSource(source)
this.props.updateSource(sourceFromServer)
this.redirect(sourceFromServer)
notify(notifySourceUdpated(source.name))
} catch (error) {
notify(notifySourceUdpateFailed(source.name, this.parseError(error)))
}
}
private redirect = (source: Source) => {
private redirect = source => {
const {isInitialSource} = this.state
const {params, router} = this.props
@ -276,9 +270,10 @@ class SourcePage extends Component<Props, State> {
}
}
const mapDispatchToProps = dispatch => ({
notify: bindActionCreators(notifyAction, dispatch),
addSource: bindActionCreators(addSourceAction, dispatch),
updateSource: bindActionCreators(updateSourceAction, dispatch),
})
export default withRouter(connect(null, mapDispatchToProps)(SourcePage))
const mdtp = {
notify: notifyAction,
addSource: addSourceAction,
updateSource: updateSourceAction,
}
export default withRouter(connect(null, mdtp)(SourcePage))

View File

@ -72,6 +72,7 @@
@import 'components/threesizer';
@import 'components/threshold-controls';
@import 'components/kapacitor-logs-table';
@import 'components/dropdown-placeholder';
// Pages
@import 'pages/config-endpoints';

View File

@ -0,0 +1,14 @@
.dropdown-placeholder {
background-color: $g5-pepper;
border-radius: 4px;
flex-grow: 1;
flex-shrink: 1;
display: flex;
align-items: center;
justify-content: center;
height: $form-md-height;
.loading-spinner .spinner div {
background-color: $g14-chromium;
}
}

View File

@ -0,0 +1,153 @@
$padding: 20px 30px;
.edit-temp-var {
margin: 0 auto;
width: 650px;
}
.edit-temp-var--header {
background-color: $g0-obsidian;
padding: $padding;
display: flex;
align-items: center;
justify-content: space-between;
}
.edit-temp-var--header-controls > button {
display: inline-block;
margin: 0 5px;
}
.edit-temp-var--body {
@include gradient-v($g3-castle, $g1-raven);
padding: $padding;
.delete {
margin: 20px auto 10px auto;
width: 90px;
}
}
.edit-temp-var--body-row {
display: flex;
.name {
flex: 1 1 50%;
padding-right: 10px;
input {
color: $c-potassium;
font-weight: bold;
}
}
.template-type {
flex: 1 1 50%;
padding-left: 10px;
}
.dropdown {
display: block;
width: 100%;
}
.dropdown-toggle, .dropdown-placeholder {
width: 100%;
}
}
.temp-builder {
width: 100%;
}
.temp-builder--mq-controls {
background: $g3-castle;
border-radius: $radius-small;
display: flex;
padding: 10px 10px 0 10px;
&:last-child {
padding-bottom: 10px;
}
> .temp-builder--mq-text, > .dropdown, .dropdown-placeholder {
margin-right: 5px;
flex-grow: 1;
&:last-child {
margin-right: 0;
}
}
}
.temp-builder--mq-text {
@include no-user-select();
background-color: $g5-pepper;
border-radius: $radius-small;
padding: 8px;
white-space: nowrap;
color: $c-pool;
font-size: 14px;
font-weight: 600;
font-family: $code-font;
}
.temp-builder .temp-builder-results {
margin-top: 30px;
}
@keyframes pulse {
0% {
color: $g19-ghost;
}
50% {
color: $g12-forge;
}
100% {
color: $g19-ghost;
}
}
.temp-builder-results > p {
text-align: center;
font-weight: bold;
color: $g19-ghost;
margin: 15px 0;
&.error {
color: $c-fire;
}
&.warning {
color: $c-pineapple;
}
&.loading {
animation: pulse 1.5s ease infinite;
}
> strong {
color: $c-potassium;
}
}
.temp-builder-results--list {
max-height: 250px;
padding: 0;
margin: 0;
li {
background-color: $g3-castle;
padding: 0 10px;
display: flex;
align-items: center;
border-radius: $radius-small;
margin: 0;
color: $g19-ghost;
font-weight: bold;
list-style: none;
}
}

View File

@ -62,7 +62,7 @@ button.btn.template-control--manage {
font-weight: 500;
@include no-user-select();
}
.template-control--dropdown {
.template-control--controls > .template-control--dropdown {
flex: 0 1 auto;
min-width: $template-control-dropdown-min-width;
max-width: $template-control-dropdown-max-width;
@ -71,29 +71,32 @@ button.btn.template-control--manage {
align-items: stretch;
margin: $template-control--margin;
.dropdown {
> .dropdown {
order: 2;
margin: 0;
flex: 1 0 0%;
.dropdown-toggle {
border-radius: 0 0 $radius-small $radius-small;
width: 100%;
font-size: 12px;
font-family: $code-font;
}
.dropdown-menu .fancy-scroll--view li.dropdown-item a {
white-space: pre-wrap;
word-break: break-all;
overflow: hidden;
font-family: $code-font;
font-size: 12px;
}
}
.dropdown-toggle {
border-radius: 0 0 $radius-small $radius-small;
width: 100%;
font-size: 12px;
font-family: $code-font;
}
.dropdown .dropdown-menu .fancy-scroll--view li.dropdown-item a {
white-space: pre-wrap;
word-break: break-all;
overflow: hidden;
font-family: $code-font;
font-size: 12px;
}
}
.template-control--label {
@include no-user-select();
order: 1;
height: 18px;
height: 22px;
padding: 0 8px;
margin: 0;
font-size: 11px;
@ -102,4 +105,13 @@ button.btn.template-control--manage {
line-height: 18px;
border-radius: $radius-small $radius-small 0 0;
background-color: $g4-onyx;
display: flex;
align-items: center;
justify-content: space-between;
> .icon {
padding-bottom: 1px;
color: $g11-sidewalk;
cursor: pointer;
}
}

View File

@ -495,6 +495,12 @@ $dash-graph-options-arrow: 8px;
*/
@import '../components/template-variables-manager';
/*
Edit Template Variable
------------------------------------------------------
*/
@import '../components/edit-template-variable';
/*
Write Data Form
------------------------------------------------------

View File

@ -0,0 +1,91 @@
import React, {PureComponent, ChangeEvent} from 'react'
import {ErrorHandling} from 'src/shared/decorators/errors'
import TemplatePreviewList from 'src/tempVars/components/TemplatePreviewList'
import {TemplateBuilderProps, TemplateValueType} from 'src/types'
interface State {
templateValues: string[]
templateValuesString: string
}
@ErrorHandling
class CSVTemplateBuilder extends PureComponent<TemplateBuilderProps, State> {
public constructor(props) {
super(props)
const templateValues = props.template.values.map(v => v.value)
this.state = {
templateValues,
templateValuesString: templateValues.join(', '),
}
}
public render() {
const {templateValues, templateValuesString} = this.state
const pluralizer = templateValues.length === 1 ? '' : 's'
return (
<div className="temp-builder csv-temp-builder">
<div className="form-group">
<label>Comma Separated Values</label>
<div className="temp-builder--mq-controls">
<textarea
className="form-control"
value={templateValuesString}
onChange={this.handleChange}
onBlur={this.handleBlur}
/>
</div>
</div>
<div className="temp-builder-results">
<p>
CSV contains <strong>{templateValues.length}</strong> value{
pluralizer
}
</p>
{templateValues.length > 0 && (
<TemplatePreviewList items={templateValues} />
)}
</div>
</div>
)
}
private handleChange = (e: ChangeEvent<HTMLTextAreaElement>): void => {
this.setState({templateValuesString: e.target.value})
}
private handleBlur = (): void => {
const {template, onUpdateTemplate} = this.props
const {templateValuesString} = this.state
let templateValues
if (templateValuesString.trim() === '') {
templateValues = []
} else {
templateValues = templateValuesString.split(',').map(s => s.trim())
}
this.setState({templateValues})
const nextValues = templateValues.map(value => {
return {
type: TemplateValueType.CSV,
value,
selected: false,
}
})
if (nextValues.length > 0) {
nextValues[0].selected = true
}
onUpdateTemplate({...template, values: nextValues})
}
}
export default CSVTemplateBuilder

View File

@ -0,0 +1,94 @@
import React, {PureComponent} from 'react'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {showDatabases} from 'src/shared/apis/metaQuery'
import parseShowDatabases from 'src/shared/parsing/showDatabases'
import TemplateMetaQueryPreview from 'src/tempVars/components/TemplateMetaQueryPreview'
import {
TemplateBuilderProps,
RemoteDataState,
TemplateValueType,
} from 'src/types'
interface State {
databases: string[]
databasesStatus: RemoteDataState
}
@ErrorHandling
class DatabasesTemplateBuilder extends PureComponent<
TemplateBuilderProps,
State
> {
constructor(props) {
super(props)
this.state = {
databases: [],
databasesStatus: RemoteDataState.Loading,
}
}
public async componentDidMount() {
this.loadDatabases()
}
public render() {
const {databases, databasesStatus} = this.state
return (
<div className="temp-builder databases-temp-builder">
<div className="form-group">
<label>Meta Query</label>
<div className="temp-builder--mq-controls">
<div className="temp-builder--mq-text">SHOW DATABASES</div>
</div>
</div>
<TemplateMetaQueryPreview
items={databases}
loadingStatus={databasesStatus}
/>
</div>
)
}
private async loadDatabases(): Promise<void> {
const {template, source, onUpdateTemplate} = this.props
this.setState({databasesStatus: RemoteDataState.Loading})
try {
const {data} = await showDatabases(source.links.proxy)
const {databases} = parseShowDatabases(data)
this.setState({
databases,
databasesStatus: RemoteDataState.Done,
})
const nextValues = databases.map(db => {
return {
type: TemplateValueType.Database,
value: db,
selected: false,
}
})
if (nextValues[0]) {
nextValues[0].selected = true
}
const nextTemplate = {
...template,
values: nextValues,
}
onUpdateTemplate(nextTemplate)
} catch {
this.setState({databasesStatus: RemoteDataState.Error})
}
}
}
export default DatabasesTemplateBuilder

View File

@ -0,0 +1,39 @@
import React, {PureComponent} from 'react'
import _ from 'lodash'
import {TemplateBuilderProps, TemplateValueType} from 'src/types'
import KeysTemplateBuilder from 'src/tempVars/components/KeysTemplateBuilder'
import {proxy} from 'src/utils/queryUrlGenerator'
import parseShowFieldKeys from 'src/shared/parsing/showFieldKeys'
const fetchKeys = async (source, db, measurement): Promise<string[]> => {
const {data} = await proxy({
source: source.links.proxy,
db,
query: `SHOW FIELD KEYS ON "${db}" FROM "${measurement}"`,
})
const {fieldSets} = parseShowFieldKeys(data)
const fieldKeys = _.get(Object.values(fieldSets), '0', [])
return fieldKeys
}
class FieldKeysTemplateBuilder extends PureComponent<TemplateBuilderProps> {
public render() {
const {template, source, onUpdateTemplate} = this.props
return (
<KeysTemplateBuilder
queryPrefix={'SHOW FIELD KEYS ON'}
templateValueType={TemplateValueType.FieldKey}
fetchKeys={fetchKeys}
template={template}
source={source}
onUpdateTemplate={onUpdateTemplate}
/>
)
}
}
export default FieldKeysTemplateBuilder

View File

@ -0,0 +1,244 @@
import React, {PureComponent} from 'react'
import {getDeep} from 'src/utils/wrappers'
import {ErrorHandling} from 'src/shared/decorators/errors'
import Dropdown from 'src/shared/components/Dropdown'
import {showDatabases, showMeasurements} from 'src/shared/apis/metaQuery'
import parseShowDatabases from 'src/shared/parsing/showDatabases'
import parseShowMeasurements from 'src/shared/parsing/showMeasurements'
import TemplateMetaQueryPreview from 'src/tempVars/components/TemplateMetaQueryPreview'
import DropdownLoadingPlaceholder from 'src/shared/components/DropdownLoadingPlaceholder'
import {
TemplateBuilderProps,
TemplateValueType,
RemoteDataState,
Source,
} from 'src/types'
interface Props extends TemplateBuilderProps {
queryPrefix: string
templateValueType: TemplateValueType
fetchKeys: (
source: Source,
db: string,
measurement: string
) => Promise<string[]>
}
interface State {
databases: string[]
databasesStatus: RemoteDataState
selectedDatabase: string
measurements: string[]
measurementsStatus: RemoteDataState
selectedMeasurement: string
keys: string[]
keysStatus: RemoteDataState
}
@ErrorHandling
class KeysTemplateBuilder extends PureComponent<Props, State> {
constructor(props) {
super(props)
const selectedDatabase = getDeep(props, 'template.query.db', '')
const selectedMeasurement = getDeep(props, 'template.query.measurement', '')
this.state = {
databases: [],
databasesStatus: RemoteDataState.Loading,
selectedDatabase,
measurements: [],
measurementsStatus: RemoteDataState.Loading,
selectedMeasurement,
keys: [],
keysStatus: RemoteDataState.Loading,
}
}
public async componentDidMount() {
await this.loadDatabases()
await this.loadMeasurements()
await this.loadKeys()
}
public render() {
const {queryPrefix} = this.props
const {
databases,
databasesStatus,
selectedDatabase,
measurements,
measurementsStatus,
selectedMeasurement,
keys,
keysStatus,
} = this.state
return (
<div className="temp-builder measurements-temp-builder">
<div className="form-group">
<label>Meta Query</label>
<div className="temp-builder--mq-controls">
<div className="temp-builder--mq-text">{queryPrefix}</div>
<DropdownLoadingPlaceholder rds={databasesStatus}>
<Dropdown
items={databases.map(text => ({text}))}
onChoose={this.handleChooseDatabaseDropdown}
selected={selectedDatabase}
buttonSize=""
/>
</DropdownLoadingPlaceholder>
<div className="temp-builder--mq-text">FROM</div>
<DropdownLoadingPlaceholder rds={measurementsStatus}>
<Dropdown
items={measurements.map(text => ({text}))}
onChoose={this.handleChooseMeasurementDropdown}
selected={selectedMeasurement}
buttonSize=""
/>
</DropdownLoadingPlaceholder>
</div>
</div>
<TemplateMetaQueryPreview items={keys} loadingStatus={keysStatus} />
</div>
)
}
private async loadDatabases(): Promise<void> {
const {source} = this.props
this.setState({databasesStatus: RemoteDataState.Loading})
try {
const {data} = await showDatabases(source.links.proxy)
const {databases} = parseShowDatabases(data)
const {selectedDatabase} = this.state
this.setState({
databases,
databasesStatus: RemoteDataState.Done,
})
if (!selectedDatabase) {
this.handleChooseDatabase(getDeep(databases, 0, ''))
}
} catch (error) {
this.setState({databasesStatus: RemoteDataState.Error})
console.error(error)
}
}
private async loadMeasurements(): Promise<void> {
const {source} = this.props
const {selectedDatabase, selectedMeasurement} = this.state
this.setState({measurementsStatus: RemoteDataState.Loading})
try {
const {data} = await showMeasurements(
source.links.proxy,
selectedDatabase
)
const {measurementSets} = parseShowMeasurements(data)
const measurements = getDeep(measurementSets, '0.measurements', [])
this.setState({
measurements,
measurementsStatus: RemoteDataState.Done,
})
if (!selectedMeasurement) {
this.handleChooseMeasurement(getDeep(measurements, 0, ''))
}
} catch (error) {
this.setState({measurementsStatus: RemoteDataState.Error})
console.error(error)
}
}
private async loadKeys(): Promise<void> {
const {
template,
onUpdateTemplate,
templateValueType,
fetchKeys,
source,
} = this.props
const {selectedDatabase, selectedMeasurement} = this.state
this.setState({keysStatus: RemoteDataState.Loading})
try {
const keys = await fetchKeys(
source,
selectedDatabase,
selectedMeasurement
)
this.setState({
keys,
keysStatus: RemoteDataState.Done,
})
const nextValues = keys.map(value => {
return {
type: templateValueType,
value,
selected: false,
}
})
if (nextValues[0]) {
nextValues[0].selected = true
}
onUpdateTemplate({...template, values: nextValues})
} catch (error) {
this.setState({keysStatus: RemoteDataState.Error})
console.error(error)
}
}
private handleChooseDatabaseDropdown = ({text}) => {
this.handleChooseDatabase(text)
}
private handleChooseDatabase = (db: string): void => {
this.setState({selectedDatabase: db, selectedMeasurement: ''}, () =>
this.loadMeasurements()
)
const {template, onUpdateTemplate} = this.props
onUpdateTemplate({
...template,
query: {
...template.query,
db,
},
})
}
private handleChooseMeasurementDropdown = ({text}): void => {
this.handleChooseMeasurement(text)
}
private handleChooseMeasurement = (measurement: string): void => {
this.setState({selectedMeasurement: measurement}, () => this.loadKeys())
const {template, onUpdateTemplate} = this.props
onUpdateTemplate({
...template,
query: {
...template.query,
measurement,
},
})
}
}
export default KeysTemplateBuilder

View File

@ -0,0 +1,165 @@
import React, {PureComponent} from 'react'
import _ from 'lodash'
import {ErrorHandling} from 'src/shared/decorators/errors'
import Dropdown from 'src/shared/components/Dropdown'
import {showDatabases, showMeasurements} from 'src/shared/apis/metaQuery'
import parseShowDatabases from 'src/shared/parsing/showDatabases'
import parseShowMeasurements from 'src/shared/parsing/showMeasurements'
import TemplateMetaQueryPreview from 'src/tempVars/components/TemplateMetaQueryPreview'
import DropdownLoadingPlaceholder from 'src/shared/components/DropdownLoadingPlaceholder'
import {
TemplateBuilderProps,
RemoteDataState,
TemplateValueType,
} from 'src/types'
interface State {
databases: string[]
databasesStatus: RemoteDataState
selectedDatabase: string
measurements: string[]
measurementsStatus: RemoteDataState
}
@ErrorHandling
class MeasurementsTemplateBuilder extends PureComponent<
TemplateBuilderProps,
State
> {
constructor(props) {
super(props)
const selectedDatabase = _.get(props, 'template.query.db', '')
this.state = {
databases: [],
databasesStatus: RemoteDataState.Loading,
selectedDatabase,
measurements: [],
measurementsStatus: RemoteDataState.Loading,
}
}
public async componentDidMount() {
await this.loadDatabases()
await this.loadMeasurements()
}
public render() {
const {
databases,
databasesStatus,
selectedDatabase,
measurements,
measurementsStatus,
} = this.state
return (
<div className="temp-builder measurements-temp-builder">
<div className="form-group">
<label>Meta Query</label>
<div className="temp-builder--mq-controls">
<div className="temp-builder--mq-text">SHOW MEASUREMENTS ON</div>
<DropdownLoadingPlaceholder rds={databasesStatus}>
<Dropdown
items={databases.map(text => ({text}))}
onChoose={this.handleChooseDatabaseDropdown}
selected={selectedDatabase}
buttonSize=""
/>
</DropdownLoadingPlaceholder>
</div>
</div>
<TemplateMetaQueryPreview
items={measurements}
loadingStatus={measurementsStatus}
/>
</div>
)
}
private async loadDatabases(): Promise<void> {
const {source} = this.props
this.setState({databasesStatus: RemoteDataState.Loading})
try {
const {data} = await showDatabases(source.links.proxy)
const {databases} = parseShowDatabases(data)
const {selectedDatabase} = this.state
this.setState({
databases,
databasesStatus: RemoteDataState.Done,
})
if (!selectedDatabase) {
this.handleChooseDatabase(_.get(databases, 0, ''))
}
} catch (error) {
this.setState({databasesStatus: RemoteDataState.Error})
console.error(error)
}
}
private async loadMeasurements(): Promise<void> {
const {template, source, onUpdateTemplate} = this.props
const {selectedDatabase} = this.state
this.setState({measurementsStatus: RemoteDataState.Loading})
try {
const {data} = await showMeasurements(
source.links.proxy,
selectedDatabase
)
const {measurementSets} = parseShowMeasurements(data)
const measurements = _.get(measurementSets, '0.measurements', [])
this.setState({
measurements,
measurementsStatus: RemoteDataState.Done,
})
const nextValues = measurements.map(value => {
return {
type: TemplateValueType.Measurement,
value,
selected: false,
}
})
if (nextValues[0]) {
nextValues[0].selected = true
}
onUpdateTemplate({...template, values: nextValues})
} catch (error) {
this.setState({measurementsStatus: RemoteDataState.Error})
console.error(error)
}
}
private handleChooseDatabaseDropdown = ({text}) => {
this.handleChooseDatabase(text)
}
private handleChooseDatabase = (db: string): void => {
this.setState({selectedDatabase: db}, () => this.loadMeasurements())
const {template, onUpdateTemplate} = this.props
onUpdateTemplate({
...template,
query: {
...template.query,
db,
},
})
}
}
export default MeasurementsTemplateBuilder

View File

@ -0,0 +1,42 @@
import React, {PureComponent} from 'react'
import KeysTemplateBuilder from 'src/tempVars/components/KeysTemplateBuilder'
import {proxy} from 'src/utils/queryUrlGenerator'
import parseShowTagKeys from 'src/shared/parsing/showTagKeys'
import {TemplateBuilderProps, TemplateValueType} from 'src/types'
export const fetchTagKeys = async (
source,
db,
measurement
): Promise<string[]> => {
const {data} = await proxy({
source: source.links.proxy,
db,
query: `SHOW TAG KEYS ON "${db}" FROM "${measurement}"`,
})
const {tagKeys} = parseShowTagKeys(data)
return tagKeys
}
class TagKeysTemplateBuilder extends PureComponent<TemplateBuilderProps> {
public render() {
const {template, source, onUpdateTemplate} = this.props
return (
<KeysTemplateBuilder
queryPrefix={'SHOW TAG KEYS ON'}
templateValueType={TemplateValueType.TagKey}
fetchKeys={fetchTagKeys}
template={template}
source={source}
onUpdateTemplate={onUpdateTemplate}
/>
)
}
}
export default TagKeysTemplateBuilder

View File

@ -0,0 +1,308 @@
import React, {PureComponent} from 'react'
import _ from 'lodash'
import {ErrorHandling} from 'src/shared/decorators/errors'
import Dropdown from 'src/shared/components/Dropdown'
import {showDatabases, showMeasurements} from 'src/shared/apis/metaQuery'
import {proxy} from 'src/utils/queryUrlGenerator'
import parseShowDatabases from 'src/shared/parsing/showDatabases'
import parseShowMeasurements from 'src/shared/parsing/showMeasurements'
import parseShowTagValues from 'src/shared/parsing/showTagValues'
import {fetchTagKeys} from 'src/tempVars/components/TagKeysTemplateBuilder'
import TemplateMetaQueryPreview from 'src/tempVars/components/TemplateMetaQueryPreview'
import DropdownLoadingPlaceholder from 'src/shared/components/DropdownLoadingPlaceholder'
import {
TemplateBuilderProps,
TemplateValueType,
RemoteDataState,
} from 'src/types'
interface State {
databases: string[]
databasesStatus: RemoteDataState
selectedDatabase: string
measurements: string[]
measurementsStatus: RemoteDataState
selectedMeasurement: string
tagKeys: string[]
tagKeysStatus: RemoteDataState
selectedTagKey: string
tagValues: string[]
tagValuesStatus: RemoteDataState
}
@ErrorHandling
class KeysTemplateBuilder extends PureComponent<TemplateBuilderProps, State> {
constructor(props) {
super(props)
const selectedDatabase = _.get(props, 'template.query.db', '')
const selectedMeasurement = _.get(props, 'template.query.measurement', '')
const selectedTagKey = _.get(props, 'template.query.tagKey', '')
this.state = {
databases: [],
databasesStatus: RemoteDataState.Loading,
selectedDatabase,
measurements: [],
measurementsStatus: RemoteDataState.Loading,
selectedMeasurement,
tagKeys: [],
tagKeysStatus: RemoteDataState.Loading,
selectedTagKey,
tagValues: [],
tagValuesStatus: RemoteDataState.Loading,
}
}
public async componentDidMount() {
await this.loadDatabases()
await this.loadMeasurements()
await this.loadTagKeys()
await this.loadTagValues()
}
public render() {
const {
databases,
databasesStatus,
selectedDatabase,
measurements,
measurementsStatus,
selectedMeasurement,
tagKeys,
tagKeysStatus,
selectedTagKey,
tagValues,
tagValuesStatus,
} = this.state
return (
<div className="temp-builder measurements-temp-builder">
<div className="form-group">
<label>Meta Query</label>
<div className="temp-builder--mq-controls">
<div className="temp-builder--mq-text">SHOW TAG VALUES ON</div>
<DropdownLoadingPlaceholder rds={databasesStatus}>
<Dropdown
items={databases.map(text => ({text}))}
onChoose={this.handleChooseDatabaseDropdown}
selected={selectedDatabase}
buttonSize=""
/>
</DropdownLoadingPlaceholder>
</div>
<div className="temp-builder--mq-controls">
<div className="temp-builder--mq-text">FROM</div>
<DropdownLoadingPlaceholder rds={measurementsStatus}>
<Dropdown
items={measurements.map(text => ({text}))}
onChoose={this.handleChooseMeasurementDropdown}
selected={selectedMeasurement}
buttonSize=""
/>
</DropdownLoadingPlaceholder>
<div className="temp-builder--mq-text">WITH KEY</div>
<DropdownLoadingPlaceholder rds={tagKeysStatus}>
<Dropdown
items={tagKeys.map(text => ({text}))}
onChoose={this.handleChooseTagKeyDropdown}
selected={selectedTagKey}
buttonSize=""
/>
</DropdownLoadingPlaceholder>
</div>
</div>
<TemplateMetaQueryPreview
items={tagValues}
loadingStatus={tagValuesStatus}
/>
</div>
)
}
private async loadDatabases(): Promise<void> {
const {source} = this.props
this.setState({databasesStatus: RemoteDataState.Loading})
try {
const {data} = await showDatabases(source.links.proxy)
const {databases} = parseShowDatabases(data)
const {selectedDatabase} = this.state
this.setState({
databases,
databasesStatus: RemoteDataState.Done,
})
if (!selectedDatabase) {
this.handleChooseDatabase(_.get(databases, 0, ''))
}
} catch (error) {
this.setState({databasesStatus: RemoteDataState.Error})
console.error(error)
}
}
private async loadMeasurements(): Promise<void> {
const {source} = this.props
const {selectedDatabase, selectedMeasurement} = this.state
this.setState({measurementsStatus: RemoteDataState.Loading})
try {
const {data} = await showMeasurements(
source.links.proxy,
selectedDatabase
)
const {measurementSets} = parseShowMeasurements(data)
const measurements = _.get(measurementSets, '0.measurements', [])
this.setState({
measurements,
measurementsStatus: RemoteDataState.Done,
})
if (!selectedMeasurement) {
this.handleChooseMeasurement(_.get(measurements, 0, ''))
}
} catch (error) {
this.setState({measurementsStatus: RemoteDataState.Error})
console.error(error)
}
}
private async loadTagKeys(): Promise<void> {
const {source} = this.props
const {selectedTagKey} = this.state
const {selectedDatabase, selectedMeasurement} = this.state
this.setState({tagKeysStatus: RemoteDataState.Loading})
try {
const tagKeys = await fetchTagKeys(
source,
selectedDatabase,
selectedMeasurement
)
this.setState({
tagKeys,
tagKeysStatus: RemoteDataState.Done,
})
if (!selectedTagKey) {
this.handleChooseTagKey(_.get(tagKeys, 0, ''))
}
} catch (error) {
this.setState({tagKeysStatus: RemoteDataState.Error})
console.error(error)
}
}
private loadTagValues = async (): Promise<void> => {
const {source, template, onUpdateTemplate} = this.props
const {selectedDatabase, selectedMeasurement, selectedTagKey} = this.state
this.setState({tagValuesStatus: RemoteDataState.Loading})
try {
const {data} = await proxy({
source: source.links.proxy,
db: selectedDatabase,
query: `SHOW TAG VALUES ON "${selectedDatabase}" FROM "${selectedMeasurement}" WITH KEY = "${selectedTagKey}"`,
})
const {tags} = parseShowTagValues(data)
const tagValues = _.get(Object.values(tags), 0, [])
this.setState({
tagValues,
tagValuesStatus: RemoteDataState.Done,
})
const nextValues = tagValues.map(value => {
return {
type: TemplateValueType.TagValue,
value,
selected: false,
}
})
if (nextValues[0]) {
nextValues[0].selected = true
}
onUpdateTemplate({...template, values: nextValues})
} catch (error) {
this.setState({tagValuesStatus: RemoteDataState.Error})
console.error(error)
}
}
private handleChooseDatabaseDropdown = ({text}) => {
this.handleChooseDatabase(text)
}
private handleChooseDatabase = (db: string): void => {
this.setState({selectedDatabase: db, selectedMeasurement: ''}, () =>
this.loadMeasurements()
)
const {template, onUpdateTemplate} = this.props
onUpdateTemplate({
...template,
query: {
...template.query,
db,
tagKey: '',
measurement: '',
},
})
}
private handleChooseMeasurementDropdown = ({text}): void => {
this.handleChooseMeasurement(text)
}
private handleChooseMeasurement = (measurement: string): void => {
this.setState({selectedMeasurement: measurement, selectedTagKey: ''}, () =>
this.loadTagKeys()
)
const {template, onUpdateTemplate} = this.props
onUpdateTemplate({
...template,
query: {
...template.query,
measurement,
tagKey: '',
},
})
}
private handleChooseTagKeyDropdown = ({text}): void => {
this.handleChooseTagKey(text)
}
private handleChooseTagKey = (tagKey: string): void => {
this.setState({selectedTagKey: tagKey}, () => this.loadTagValues())
const {template, onUpdateTemplate} = this.props
onUpdateTemplate({
...template,
query: {
...template.query,
tagKey,
},
})
}
}
export default KeysTemplateBuilder

View File

@ -0,0 +1,129 @@
import React, {Component} from 'react'
import classnames from 'classnames'
import TemplateControlDropdown from 'src/tempVars/components/TemplateControlDropdown'
import SimpleOverlayTechnology from 'src/shared/components/SimpleOverlayTechnology'
import TemplateVariableEditor from 'src/tempVars/components/TemplateVariableEditor'
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
import {Template, Source} from 'src/types'
interface Props {
meRole: string
isUsingAuth: boolean
templates: Template[]
isOpen: boolean
source: Source
onSelectTemplate: (id: string) => void
onSaveTemplates: (templates: Template[]) => void
onCreateTemplateVariable: () => void
}
interface State {
isAdding: boolean
}
class TemplateControlBar extends Component<Props, State> {
constructor(props) {
super(props)
this.state = {isAdding: false}
}
public render() {
const {
isOpen,
templates,
onSelectTemplate,
meRole,
isUsingAuth,
source,
} = this.props
const {isAdding} = this.state
return (
<div className={classnames('template-control-bar', {show: isOpen})}>
<div className="template-control--container">
<div className="template-control--controls">
{templates && templates.length ? (
templates.map(template => (
<TemplateControlDropdown
key={template.id}
meRole={meRole}
isUsingAuth={isUsingAuth}
template={template}
source={source}
onSelectTemplate={onSelectTemplate}
onCreateTemplate={this.handleCreateTemplate}
onUpdateTemplate={this.handleUpdateTemplate}
onDeleteTemplate={this.handleDeleteTemplate}
/>
))
) : (
<div className="template-control--empty" data-test="empty-state">
This dashboard does not have any{' '}
<strong>Template Variables</strong>
</div>
)}
{isAdding && (
<SimpleOverlayTechnology>
<TemplateVariableEditor
source={source}
onCreate={this.handleCreateTemplate}
onCancel={this.handleCancelAddVariable}
/>
</SimpleOverlayTechnology>
)}
</div>
<Authorized requiredRole={EDITOR_ROLE}>
<button
className="btn btn-primary btn-sm template-control--manage"
onClick={this.handleAddVariable}
>
<span className="icon plus" />
Add Variable
</button>
</Authorized>
</div>
</div>
)
}
private handleAddVariable = (): void => {
this.setState({isAdding: true})
}
private handleCancelAddVariable = (): void => {
this.setState({isAdding: false})
}
private handleCreateTemplate = async (template: Template): Promise<void> => {
const {templates, onSaveTemplates} = this.props
await onSaveTemplates([...templates, template])
this.setState({isAdding: false})
}
private handleUpdateTemplate = async (template: Template): Promise<void> => {
const {templates, onSaveTemplates} = this.props
const newTemplates = templates.reduce((acc, t) => {
if (t.id === template.id) {
return [...acc, template]
}
return [...acc, t]
}, [])
await onSaveTemplates(newTemplates)
}
private handleDeleteTemplate = async (template: Template): Promise<void> => {
const {templates, onSaveTemplates} = this.props
const newTemplates = templates.filter(t => t.id !== template.id)
await onSaveTemplates(newTemplates)
}
}
export default TemplateControlBar

View File

@ -0,0 +1,117 @@
import React, {PureComponent} from 'react'
import Dropdown from 'src/shared/components/Dropdown'
import SimpleOverlayTechnology from 'src/shared/components/SimpleOverlayTechnology'
import TemplateVariableEditor from 'src/tempVars/components/TemplateVariableEditor'
import {calculateDropdownWidth} from 'src/dashboards/constants/templateControlBar'
import Authorized, {isUserAuthorized, EDITOR_ROLE} from 'src/auth/Authorized'
import {Template, Source} from 'src/types'
interface Props {
template: Template
meRole: string
isUsingAuth: boolean
source: Source
onSelectTemplate: (id: string) => void
onCreateTemplate: (template: Template) => Promise<void>
onUpdateTemplate: (template: Template) => Promise<void>
onDeleteTemplate: (template: Template) => Promise<void>
}
interface State {
isEditing: boolean
}
class TemplateControlDropdown extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
isEditing: false,
}
}
public render() {
const {
template,
isUsingAuth,
meRole,
source,
onSelectTemplate,
onCreateTemplate,
} = this.props
const {isEditing} = this.state
const dropdownItems = template.values.map(value => ({
...value,
text: value.value,
}))
const dropdownStyle = template.values.length
? {minWidth: calculateDropdownWidth(template.values)}
: null
const selectedItem = dropdownItems.find(item => item.selected) ||
dropdownItems[0] || {text: '(No values)'}
return (
<div className="template-control--dropdown" style={dropdownStyle}>
<Dropdown
items={dropdownItems}
buttonSize="btn-xs"
menuClass="dropdown-astronaut"
useAutoComplete={true}
selected={selectedItem.text}
disabled={isUsingAuth && !isUserAuthorized(meRole, EDITOR_ROLE)}
onChoose={onSelectTemplate(template.id)}
/>
<Authorized requiredRole={EDITOR_ROLE}>
<label className="template-control--label">
{template.tempVar}
<span
className="icon cog-thick"
onClick={this.handleShowSettings}
/>
</label>
</Authorized>
{isEditing && (
<SimpleOverlayTechnology>
<TemplateVariableEditor
template={template}
source={source}
onCreate={onCreateTemplate}
onUpdate={this.handleUpdateTemplate}
onDelete={this.handleDelete}
onCancel={this.handleHideSettings}
/>
</SimpleOverlayTechnology>
)}
</div>
)
}
private handleShowSettings = (): void => {
this.setState({isEditing: true})
}
private handleHideSettings = (): void => {
this.setState({isEditing: false})
}
private handleUpdateTemplate = async (template: Template): Promise<void> => {
const {onUpdateTemplate} = this.props
await onUpdateTemplate(template)
this.setState({isEditing: false})
}
private handleDelete = (): Promise<any> => {
const {onDeleteTemplate, template} = this.props
return onDeleteTemplate(template)
}
}
export default TemplateControlDropdown

View File

@ -0,0 +1,61 @@
import React, {PureComponent} from 'react'
import {ErrorHandling} from 'src/shared/decorators/errors'
import TemplatePreviewList from 'src/tempVars/components/TemplatePreviewList'
import {RemoteDataState} from 'src/types'
interface Props {
items: string[]
loadingStatus: RemoteDataState
}
@ErrorHandling
class TemplateMetaQueryPreview extends PureComponent<Props> {
public render() {
const {items, loadingStatus} = this.props
if (loadingStatus === RemoteDataState.NotStarted) {
return <div className="temp-builder-results" />
}
if (loadingStatus === RemoteDataState.Loading) {
return (
<div className="temp-builder-results">
<p className="loading">Loading meta query preview...</p>
</div>
)
}
if (loadingStatus === RemoteDataState.Error) {
return (
<div className="temp-builder-results">
<p className="error">Meta Query failed to execute</p>
</div>
)
}
if (items.length === 0) {
return (
<div className="temp-builder-results">
<p className="warning">
Meta Query is syntactically correct but returned no results
</p>
</div>
)
}
const pluralizer = items.length === 1 ? '' : 's'
return (
<div className="temp-builder-results">
<p>
Meta Query returned <strong>{items.length}</strong> value{pluralizer}
</p>
{items.length > 0 && <TemplatePreviewList items={items} />}
</div>
)
}
}
export default TemplateMetaQueryPreview

View File

@ -0,0 +1,52 @@
import React, {PureComponent} from 'react'
import uuid from 'uuid'
import {ErrorHandling} from 'src/shared/decorators/errors'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
const LI_HEIGHT = 35
const LI_MARGIN_BOTTOM = 3
const RESULTS_TO_DISPLAY = 10
interface Props {
items: string[]
}
@ErrorHandling
class TemplatePreviewList extends PureComponent<Props> {
public render() {
const {items} = this.props
return (
<ul
className="temp-builder-results--list"
style={{height: `${this.resultsListHeight}px`}}
>
<FancyScrollbar>
{items.map(db => {
return (
<li
key={uuid.v4()}
style={{
height: `${LI_HEIGHT}px`,
marginBottom: `${LI_MARGIN_BOTTOM}px`,
}}
>
{db}
</li>
)
})}
</FancyScrollbar>
</ul>
)
}
private get resultsListHeight() {
const {items} = this.props
const count = Math.min(items.length, RESULTS_TO_DISPLAY)
return count * (LI_HEIGHT + LI_MARGIN_BOTTOM)
}
}
export default TemplatePreviewList

View File

@ -0,0 +1,288 @@
import React, {
PureComponent,
ComponentClass,
ChangeEvent,
KeyboardEvent,
} from 'react'
import {connect} from 'react-redux'
import {ErrorHandling} from 'src/shared/decorators/errors'
import Dropdown from 'src/shared/components/Dropdown'
import ConfirmButton from 'src/shared/components/ConfirmButton'
import {getDeep} from 'src/utils/wrappers'
import {notify as notifyActionCreator} from 'src/shared/actions/notifications'
import DatabasesTemplateBuilder from 'src/tempVars/components/DatabasesTemplateBuilder'
import CSVTemplateBuilder from 'src/tempVars/components/CSVTemplateBuilder'
import MeasurementsTemplateBuilder from 'src/tempVars/components/MeasurementsTemplateBuilder'
import FieldKeysTemplateBuilder from 'src/tempVars/components/FieldKeysTemplateBuilder'
import TagKeysTemplateBuilder from 'src/tempVars/components/TagKeysTemplateBuilder'
import TagValuesTemplateBuilder from 'src/tempVars/components/TagValuesTemplateBuilder'
import {
Template,
TemplateType,
TemplateBuilderProps,
Source,
RemoteDataState,
Notification,
} from 'src/types'
import {
TEMPLATE_TYPES_LIST,
DEFAULT_TEMPLATES,
RESERVED_TEMPLATE_NAMES,
} from 'src/tempVars/constants'
import {FIVE_SECONDS} from 'src/shared/constants/index'
interface Props {
// We will assume we are creating a new template if none is passed in
template?: Template
source: Source
onCancel: () => void
onCreate?: (template: Template) => Promise<any>
onUpdate?: (template: Template) => Promise<any>
onDelete?: () => Promise<any>
notify: (n: Notification) => void
}
interface State {
nextTemplate: Template
isNew: boolean
savingStatus: RemoteDataState
deletingStatus: RemoteDataState
}
const TEMPLATE_BUILDERS = {
[TemplateType.Databases]: DatabasesTemplateBuilder,
[TemplateType.CSV]: CSVTemplateBuilder,
[TemplateType.Measurements]: MeasurementsTemplateBuilder,
[TemplateType.FieldKeys]: FieldKeysTemplateBuilder,
[TemplateType.TagKeys]: TagKeysTemplateBuilder,
[TemplateType.TagValues]: TagValuesTemplateBuilder,
}
const formatName = name => `:${name.replace(/:/g, '')}:`
const DEFAULT_TEMPLATE = DEFAULT_TEMPLATES[TemplateType.Databases]
@ErrorHandling
class TemplateVariableEditor extends PureComponent<Props, State> {
constructor(props) {
super(props)
const defaultState = {
savingStatus: RemoteDataState.NotStarted,
deletingStatus: RemoteDataState.NotStarted,
}
const {template} = this.props
if (template) {
this.state = {
...defaultState,
nextTemplate: {...template},
isNew: false,
}
} else {
this.state = {
...defaultState,
nextTemplate: DEFAULT_TEMPLATE(),
isNew: true,
}
}
}
public render() {
const {source, onCancel} = this.props
const {nextTemplate, isNew} = this.state
const TemplateBuilder = this.templateBuilder
return (
<div className="edit-temp-var">
<div className="edit-temp-var--header">
<h1 className="page-header__title">Edit Template Variable</h1>
<div className="edit-temp-var--header-controls">
<button
className="btn btn-default"
type="button"
onClick={onCancel}
>
Cancel
</button>
<button
className="btn btn-success"
type="button"
onClick={this.handleSave}
disabled={!this.canSave}
>
{this.isSaving ? 'Saving...' : 'Save'}
</button>
</div>
</div>
<div className="edit-temp-var--body">
<div className="edit-temp-var--body-row">
<div className="form-group name">
<label>Name</label>
<input
type="text"
className="form-control"
value={nextTemplate.tempVar}
onChange={this.handleChangeName}
onKeyPress={this.handleNameKeyPress}
onBlur={this.formatName}
/>
</div>
<div className="form-group template-type">
<label>Type</label>
<Dropdown
items={TEMPLATE_TYPES_LIST}
onChoose={this.handleChooseType}
selected={this.dropdownSelection}
buttonSize=""
/>
</div>
</div>
<div className="edit-temp-var--body-row">
<TemplateBuilder
template={nextTemplate}
source={source}
onUpdateTemplate={this.handleUpdateTemplate}
/>
</div>
<ConfirmButton
text={this.isDeleting ? 'Deleting...' : 'Delete'}
confirmAction={this.handleDelete}
type="btn-danger"
size="btn-xs"
customClass="delete"
disabled={isNew || this.isDeleting}
/>
</div>
</div>
)
}
private get templateBuilder(): ComponentClass<TemplateBuilderProps> {
const {
nextTemplate: {type},
} = this.state
const component = TEMPLATE_BUILDERS[type]
if (!component) {
throw new Error(`Could not find template builder for type "${type}"`)
}
return component
}
private handleUpdateTemplate = (nextTemplate: Template): void => {
this.setState({nextTemplate})
}
private handleChooseType = ({type}) => {
const {
nextTemplate: {id},
} = this.state
const nextNextTemplate = {...DEFAULT_TEMPLATES[type](), id}
this.setState({nextTemplate: nextNextTemplate})
}
private handleChangeName = (e: ChangeEvent<HTMLInputElement>): void => {
const {value} = e.target
const {nextTemplate} = this.state
this.setState({
nextTemplate: {
...nextTemplate,
tempVar: value,
},
})
}
private formatName = (): void => {
const {nextTemplate} = this.state
const tempVar = formatName(nextTemplate.tempVar)
this.setState({nextTemplate: {...nextTemplate, tempVar}})
}
private handleSave = async (): Promise<any> => {
if (!this.canSave) {
return
}
const {onUpdate, onCreate, notify} = this.props
const {nextTemplate, isNew} = this.state
nextTemplate.tempVar = formatName(nextTemplate.tempVar)
this.setState({savingStatus: RemoteDataState.Loading})
try {
if (isNew) {
await onCreate(nextTemplate)
} else {
await onUpdate(nextTemplate)
}
} catch (error) {
notify({
message: `Error saving template: ${error}`,
type: 'error',
icon: 'alert-triangle',
duration: FIVE_SECONDS,
})
console.error(error)
}
}
private handleNameKeyPress = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
this.handleSave()
}
}
private get isSaving(): boolean {
return this.state.savingStatus === RemoteDataState.Loading
}
private get canSave(): boolean {
const {
nextTemplate: {tempVar},
} = this.state
return (
tempVar !== '' &&
!RESERVED_TEMPLATE_NAMES.includes(formatName(tempVar)) &&
!this.isSaving
)
}
private get dropdownSelection(): string {
const {
nextTemplate: {type},
} = this.state
return getDeep(
TEMPLATE_TYPES_LIST.filter(t => t.type === type),
'0.text',
''
)
}
private handleDelete = (): void => {
const {onDelete} = this.props
this.setState({deletingStatus: RemoteDataState.Loading}, onDelete)
}
private get isDeleting(): boolean {
return this.state.deletingStatus === RemoteDataState.Loading
}
}
const mapDispatchToProps = {notify: notifyActionCreator}
export default connect(null, mapDispatchToProps)(TemplateVariableEditor)

View File

@ -0,0 +1,174 @@
import uuid from 'uuid'
import {TimeRange} from 'src/types/query'
import {TEMP_VAR_DASHBOARD_TIME} from 'src/shared/constants'
import {Template, TemplateType, TemplateValueType} from 'src/types'
interface TemplateTypesListItem {
text: string
type: TemplateType
}
export const TEMPLATE_TYPES_LIST: TemplateTypesListItem[] = [
{
text: 'Databases',
type: TemplateType.Databases,
},
{
text: 'Measurements',
type: TemplateType.Measurements,
},
{
text: 'Field Keys',
type: TemplateType.FieldKeys,
},
{
text: 'Tag Keys',
type: TemplateType.TagKeys,
},
{
text: 'Tag Values',
type: TemplateType.TagValues,
},
{
text: 'CSV',
type: TemplateType.CSV,
},
]
export const TEMPLATE_VARIABLE_TYPES = {
[TemplateType.CSV]: TemplateValueType.CSV,
[TemplateType.Databases]: TemplateValueType.Database,
[TemplateType.Measurements]: TemplateValueType.Measurement,
[TemplateType.FieldKeys]: TemplateValueType.FieldKey,
[TemplateType.TagKeys]: TemplateValueType.TagKey,
[TemplateType.TagValues]: TemplateValueType.TagValue,
}
export const TEMPLATE_VARIABLE_QUERIES = {
[TemplateType.Databases]: 'SHOW DATABASES',
[TemplateType.Measurements]: 'SHOW MEASUREMENTS ON :database:',
[TemplateType.FieldKeys]: 'SHOW FIELD KEYS ON :database: FROM :measurement:',
[TemplateType.TagKeys]: 'SHOW TAG KEYS ON :database: FROM :measurement:',
[TemplateType.TagValues]:
'SHOW TAG VALUES ON :database: FROM :measurement: WITH KEY=:tagKey:',
}
interface DefaultTemplates {
[templateType: string]: () => Template
}
export const DEFAULT_TEMPLATES: DefaultTemplates = {
[TemplateType.Databases]: () => {
return {
id: uuid.v4(),
tempVar: ':my-databases:',
values: [
{
value: '_internal',
type: TemplateValueType.Database,
selected: true,
},
],
type: TemplateType.Databases,
label: '',
query: {
influxql: TEMPLATE_VARIABLE_QUERIES[TemplateType.Databases],
},
}
},
[TemplateType.Measurements]: () => {
return {
id: uuid.v4(),
tempVar: ':my-measurements:',
values: [],
type: TemplateType.Measurements,
label: '',
query: {
influxql: TEMPLATE_VARIABLE_QUERIES[TemplateType.Measurements],
db: '',
},
}
},
[TemplateType.CSV]: () => {
return {
id: uuid.v4(),
tempVar: ':my-values:',
values: [],
type: TemplateType.CSV,
label: '',
query: {},
}
},
[TemplateType.TagKeys]: () => {
return {
id: uuid.v4(),
tempVar: ':my-tag-keys:',
values: [],
type: TemplateType.TagKeys,
label: '',
query: {
influxql: TEMPLATE_VARIABLE_QUERIES[TemplateType.TagKeys],
},
}
},
[TemplateType.FieldKeys]: () => {
return {
id: uuid.v4(),
tempVar: ':my-field-keys:',
values: [],
type: TemplateType.FieldKeys,
label: '',
query: {
influxql: TEMPLATE_VARIABLE_QUERIES[TemplateType.FieldKeys],
},
}
},
[TemplateType.TagValues]: () => {
return {
id: uuid.v4(),
tempVar: ':my-tag-values:',
values: [],
type: TemplateType.TagValues,
label: '',
query: {
influxql: TEMPLATE_VARIABLE_QUERIES[TemplateType.TagValues],
},
}
},
}
export const RESERVED_TEMPLATE_NAMES = [
':dashboardTime:',
':upperDashboardTime:',
':interval:',
':lower:',
':upper:',
':zoomedLower:',
':zoomedUpper:',
]
export const MATCH_INCOMPLETE_TEMPLATES = /:[\w-]*/g
export const applyMasks = query => {
const matchWholeTemplates = /:([\w-]*):/g
const maskForWholeTemplates = '😸$1😸'
return query.replace(matchWholeTemplates, maskForWholeTemplates)
}
export const insertTempVar = (query, tempVar) => {
return query.replace(MATCH_INCOMPLETE_TEMPLATES, tempVar)
}
export const unMask = query => {
return query.replace(/😸/g, ':')
}
export const removeUnselectedTemplateValues = templates => {
return templates.map(template => {
const selectedValues = template.values.filter(value => value.selected)
return {...template, values: selectedValues}
})
}
export const TEMPLATE_RANGE: TimeRange = {
upper: null,
lower: TEMP_VAR_DASHBOARD_TIME,
}

View File

@ -0,0 +1,8 @@
export interface AnnotationInterface {
id: string
startTime: number
endTime: number
text: string
type: string
links: {self: string}
}

View File

@ -1,6 +1,6 @@
import {QueryConfig} from 'src/types'
import {ColorString} from 'src/types/colors'
import {Template} from 'src/types/tempVars'
import {Template} from 'src/types'
export interface Axis {
label: string

View File

@ -495,6 +495,7 @@ export declare class DygraphClass {
// tslint:disable-next-line:variable-name
public width_: number
public graphDiv: HTMLElement
constructor(
container: HTMLElement | string,

View File

@ -7,6 +7,10 @@ import {
TemplateQuery,
TemplateValue,
URLQueryParams,
TemplateType,
TemplateValueType,
TemplateUpdate,
TemplateBuilderProps,
} from './tempVars'
import {
GroupBy,
@ -25,7 +29,7 @@ import {
TagValues,
} from './query'
import {AlertRule, Kapacitor, Task, RuleValues} from './kapacitor'
import {Source, SourceLinks} from './sources'
import {NewSource, Source, SourceLinks} from './sources'
import {DropdownAction, DropdownItem, Constructable} from './shared'
import {
Notification,
@ -41,6 +45,7 @@ import {
DygraphData,
} from './dygraphs'
import {JSONFeedData} from './status'
import {AnnotationInterface} from './annotations'
export {
Me,
@ -71,6 +76,7 @@ export {
TagValues,
AlertRule,
Kapacitor,
NewSource,
Source,
SourceLinks,
DropdownAction,
@ -98,4 +104,9 @@ export {
RemoteDataState,
URLQueryParams,
JSONFeedData,
AnnotationInterface,
TemplateType,
TemplateValueType,
TemplateUpdate,
TemplateBuilderProps,
}

View File

@ -1,5 +1,7 @@
import {Kapacitor, Service} from './'
export type NewSource = Pick<Source, Exclude<keyof Source, 'id'>>
export interface Source {
id: string
name: string

View File

@ -1,25 +1,48 @@
import {Source} from 'src/types'
export enum TemplateValueType {
Database = 'database',
TagKey = 'tagKey',
FieldKey = 'fieldKey',
Measurement = 'measurement',
TagValue = 'tagValue',
CSV = 'csv',
Points = 'points',
Constant = 'constant',
}
export interface TemplateValue {
value: string
type: string
type: TemplateValueType
selected: boolean
}
export interface TemplateQuery {
command: string
db: string
database?: string
db?: string
rp?: string
measurement: string
tagKey: string
fieldKey: string
influxql: string
measurement?: string
tagKey?: string
fieldKey?: string
influxql?: string
}
export enum TemplateType {
AutoGroupBy = 'autoGroupBy',
Constant = 'constant',
FieldKeys = 'fieldKeys',
Measurements = 'measurements',
TagKeys = 'tagKeys',
TagValues = 'tagValues',
CSV = 'csv',
Query = 'query',
Databases = 'databases',
}
export interface Template {
id: string
tempVar: string
values: TemplateValue[]
type: string
type: TemplateType
label: string
query?: TemplateQuery
}
@ -32,3 +55,9 @@ export interface TemplateUpdate {
export interface URLQueryParams {
[key: string]: string
}
export interface TemplateBuilderProps {
template: Template
source: Source
onUpdateTemplate: (nextTemplate: Template) => void
}

View File

@ -117,23 +117,6 @@ export const numberValueFormatter = (
return `${prefix}${label}${suffix}`
}
export const formatBytes = (bytes: number) => {
if (bytes === 0) {
return '0 Bytes'
}
if (!bytes) {
return null
}
const k = 1000
const dm = 2
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
}
export const formatRPDuration = (duration: string | null): string => {
if (!duration) {
return

View File

@ -1,45 +1,47 @@
import reducer from 'shared/reducers/annotations'
import reducer from 'src/shared/reducers/annotations'
import {AnnotationInterface} from 'src/types'
import {AnnotationState} from 'src/shared/reducers/annotations'
import {
addAnnotation,
deleteAnnotation,
loadAnnotations,
updateAnnotation,
} from 'shared/actions/annotations'
} from 'src/shared/actions/annotations'
const a1 = {
const a1: AnnotationInterface = {
id: '1',
group: '',
name: 'anno1',
time: '1515716169000',
duration: '',
startTime: 1515716169000,
endTime: 1515716169000,
type: '',
text: 'you have no swoggels',
links: {self: 'to/thine/own/self/be/true'},
}
const a2 = {
const a2: AnnotationInterface = {
id: '2',
group: '',
name: 'anno1',
time: '1515716169000',
duration: '',
text: 'you have no swoggels',
startTime: 1515716169000,
endTime: 1515716169002,
type: '',
text: 'you have so many swoggels',
links: {self: 'self/in/eye/of/beholder'},
}
const state = {
const state: AnnotationState = {
isTempHovering: false,
mode: null,
annotations: [],
}
describe('Shared.Reducers.annotations', () => {
it('can load the annotations', () => {
const expected = [{time: '0', duration: ''}]
const expected = [a1]
const actual = reducer(state, loadAnnotations(expected))
expect(actual.annotations).toEqual(expected)
})
it('can update an annotation', () => {
const expected = [{...a1, time: ''}]
const expected = [{...a1, startTime: 6666666666666}]
const actual = reducer(
{...state, annotations: [a1]},
updateAnnotation(expected[0])

View File

@ -3,6 +3,7 @@ import _ from 'lodash'
import reducer from 'src/dashboards/reducers/ui'
import {template, dashboard, cell} from 'test/resources'
import {initialState} from 'src/dashboards/reducers/ui'
import {TemplateType, TemplateValueType} from 'src/types'
import {
setTimeRange,
@ -21,13 +22,13 @@ let state
const t2 = {
...template,
id: '2',
type: 'csv',
type: TemplateType.CSV,
label: 'test csv',
tempVar: ':temperature:',
values: [
{value: '98.7', type: 'measurement', selected: false},
{value: '99.1', type: 'measurement', selected: false},
{value: '101.3', type: 'measurement', selected: true},
{value: '98.7', type: TemplateValueType.Measurement, selected: false},
{value: '99.1', type: TemplateValueType.Measurement, selected: false},
{value: '101.3', type: TemplateValueType.Measurement, selected: true},
],
}

View File

@ -1,4 +1,4 @@
import {applyMasks, insertTempVar, unMask} from 'src/dashboards/constants'
import {applyMasks, insertTempVar, unMask} from 'src/tempVars/constants'
const masquerade = query => {
const masked = applyMasks(query)

View File

@ -7,6 +7,8 @@ import {
TimeRange,
Template,
QueryConfig,
TemplateType,
TemplateValueType,
} from 'src/types'
import {
Axes,
@ -201,57 +203,57 @@ export const timeRange: TimeRange = {
export const userDefinedTemplateVariables: Template[] = [
{
tempVar: ':fields:',
type: 'fieldKeys',
type: TemplateType.FieldKeys,
label: '',
values: [
{
selected: false,
type: 'fieldKey',
type: TemplateValueType.FieldKey,
value: 'usage_guest',
},
{
selected: false,
type: 'fieldKey',
type: TemplateValueType.FieldKey,
value: 'usage_guest_nice',
},
{
selected: true,
type: 'fieldKey',
type: TemplateValueType.FieldKey,
value: 'usage_idle',
},
{
selected: false,
type: 'fieldKey',
type: TemplateValueType.FieldKey,
value: 'usage_iowait',
},
{
selected: false,
type: 'fieldKey',
type: TemplateValueType.FieldKey,
value: 'usage_irq',
},
{
selected: false,
type: 'fieldKey',
type: TemplateValueType.FieldKey,
value: 'usage_nice',
},
{
selected: false,
type: 'fieldKey',
type: TemplateValueType.FieldKey,
value: 'usage_softirq',
},
{
selected: false,
type: 'fieldKey',
type: TemplateValueType.FieldKey,
value: 'usage_steal',
},
{
selected: false,
type: 'fieldKey',
type: TemplateValueType.FieldKey,
value: 'usage_system',
},
{
selected: false,
type: 'fieldKey',
type: TemplateValueType.FieldKey,
value: 'usage_user',
},
],
@ -259,42 +261,42 @@ export const userDefinedTemplateVariables: Template[] = [
},
{
tempVar: ':measurements:',
type: 'measurements',
type: TemplateType.Measurements,
label: '',
values: [
{
selected: true,
type: 'measurement',
type: TemplateValueType.Measurement,
value: 'cpu',
},
{
selected: false,
type: 'measurement',
type: TemplateValueType.Measurement,
value: 'disk',
},
{
selected: false,
type: 'measurement',
type: TemplateValueType.Measurement,
value: 'diskio',
},
{
selected: false,
type: 'measurement',
type: TemplateValueType.Measurement,
value: 'mem',
},
{
selected: false,
type: 'measurement',
type: TemplateValueType.Measurement,
value: 'processes',
},
{
selected: false,
type: 'measurement',
type: TemplateValueType.Measurement,
value: 'swap',
},
{
selected: false,
type: 'measurement',
type: TemplateValueType.Measurement,
value: 'system',
},
],
@ -305,11 +307,11 @@ export const userDefinedTemplateVariables: Template[] = [
const dashtimeTempVar: Template = {
id: 'dashtime',
tempVar: ':dashboardTime:',
type: 'constant',
type: TemplateType.Constant,
values: [
{
value: 'now() - 5m',
type: 'constant',
type: TemplateValueType.Constant,
selected: true,
},
],
@ -318,11 +320,11 @@ const dashtimeTempVar: Template = {
const upperdashtimeTempVar: Template = {
id: 'upperdashtime',
tempVar: ':upperDashboardTime:',
type: 'constant',
type: TemplateType.Constant,
values: [
{
value: 'now()',
type: 'constant',
type: TemplateValueType.Constant,
selected: true,
},
],

View File

@ -1,5 +1,5 @@
import {Source, Template, Dashboard, Cell, CellType} from 'src/types'
import {SourceLinks} from 'src/types/sources'
import {SourceLinks, TemplateType, TemplateValueType} from 'src/types'
export const role = {
name: '',
@ -590,12 +590,11 @@ export const hosts = {
// Dashboards
export const template: Template = {
id: '1',
type: 'tagKeys',
type: TemplateType.TagKeys,
label: 'test query',
tempVar: ':region:',
query: {
db: 'db1',
command: '',
rp: 'rp1',
tagKey: 'tk1',
fieldKey: 'fk1',
@ -603,9 +602,9 @@ export const template: Template = {
influxql: 'SHOW TAGS WHERE CHRONOGIRAFFE = "friend"',
},
values: [
{value: 'us-west', type: 'tagKey', selected: false},
{value: 'us-east', type: 'tagKey', selected: true},
{value: 'us-mount', type: 'tagKey', selected: false},
{value: 'us-west', type: TemplateValueType.TagKey, selected: false},
{value: 'us-east', type: TemplateValueType.TagKey, selected: true},
{value: 'us-mount', type: TemplateValueType.TagKey, selected: false},
],
}

View File

@ -2,6 +2,7 @@ import React from 'react'
import {shallow} from 'enzyme'
import {SourceForm} from 'src/sources/components/SourceForm'
import {me} from 'test/resources'
const setup = (override = {}) => {
const noop = () => {}
@ -23,7 +24,7 @@ const setup = (override = {}) => {
isUsingAuth: false,
gotoPurgatory: noop,
isInitialSource: false,
me: {},
me,
...override,
}

View File

@ -1,8 +1,10 @@
import React from 'react'
import {shallow} from 'enzyme'
import TemplateControlBar from 'src/dashboards/components/TemplateControlBar'
import TemplateControlDropdown from 'src/dashboards/components/TemplateControlDropdown'
import TemplateControlBar from 'src/tempVars/components/TemplateControlBar'
import TemplateControlDropdown from 'src/tempVars/components/TemplateControlDropdown'
import {TemplateType, TemplateValueType} from 'src/types'
import {source} from 'test/resources'
const defaultProps = {
isOpen: true,
@ -11,16 +13,16 @@ const defaultProps = {
id: '000',
tempVar: ':alpha:',
label: '',
type: 'constant',
type: TemplateType.Constant,
values: [
{
value: 'firstValue',
type: 'constant',
type: TemplateValueType.Constant,
selected: false,
},
{
value: 'secondValue',
type: 'constant',
type: TemplateValueType.Constant,
selected: false,
},
],
@ -30,6 +32,9 @@ const defaultProps = {
isUsingAuth: true,
onOpenTemplateManager: () => {},
onSelectTemplate: () => {},
onSaveTemplates: () => {},
onCreateTemplateVariable: () => {},
source,
}
const setup = (override = {}) => {

View File

@ -1,39 +0,0 @@
import {formatBytes, formatRPDuration} from 'utils/formatting'
describe('Formatting helpers', () => {
describe('formatBytes', () => {
it('returns null when passed a falsey value', () => {
const actual = formatBytes(null)
expect(actual).toBe(null)
})
it('returns the correct value when passed 0', () => {
const actual = formatBytes(0)
expect(actual).toBe('0 Bytes')
})
it("converts a raw byte value into it's most appropriate unit", () => {
expect(formatBytes(1000)).toBe('1 KB')
expect(formatBytes(1000000)).toBe('1 MB')
expect(formatBytes(1000000000)).toBe('1 GB')
})
})
describe('formatRPDuration', () => {
it("returns 'infinite' for a retention policy with a value of '0'", () => {
const actual = formatRPDuration('0')
expect(actual).toBe('∞')
})
it('correctly formats retention policy durations', () => {
expect(formatRPDuration('24h0m0s')).toBe('24h')
expect(formatRPDuration('168h0m0s')).toBe('7d')
expect(formatRPDuration('200h32m3s')).toBe('8d8h32m3s')
})
})
})

View File

@ -0,0 +1,19 @@
import {formatRPDuration} from 'src/utils/formatting'
describe('Formatting helpers', () => {
describe('formatRPDuration', () => {
it("returns 'infinite' for a retention policy with a value of '0'", () => {
const actual = formatRPDuration('0')
expect(actual).toBe('∞')
})
it('correctly formats retention policy durations', () => {
expect(formatRPDuration('24h0m0s')).toBe('24h')
expect(formatRPDuration('168h0m0s')).toBe('7d')
expect(formatRPDuration('200h32m3s')).toBe('8d8h32m3s')
})
})
})