Merge branch 'master' into fun/status-page-to-TS
commit
a06a9d1a9f
|
@ -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/
|
|
@ -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]
|
||||
|
||||
|
|
2
Makefile
2
Makefile
|
@ -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 ./...
|
||||
|
|
91
circle.yml
91
circle.yml
|
@ -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
|
|
@ -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, "") == ""
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -12,7 +12,7 @@ interface State {
|
|||
|
||||
@ErrorHandling
|
||||
class SearchBar extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
applyMasks,
|
||||
insertTempVar,
|
||||
unMask,
|
||||
} from 'src/dashboards/constants'
|
||||
} from 'src/tempVars/constants'
|
||||
|
||||
@ErrorHandling
|
||||
class QueryTextArea extends Component {
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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))
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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: {
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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})
|
||||
}
|
|
@ -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})
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -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,
|
|
@ -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 = {
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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 => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
|
|
|
@ -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 = {
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
|
@ -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)
|
|
@ -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)
|
|
@ -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))
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
------------------------------------------------------
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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,
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
export interface AnnotationInterface {
|
||||
id: string
|
||||
startTime: number
|
||||
endTime: number
|
||||
text: string
|
||||
type: string
|
||||
links: {self: string}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -495,6 +495,7 @@ export declare class DygraphClass {
|
|||
|
||||
// tslint:disable-next-line:variable-name
|
||||
public width_: number
|
||||
public graphDiv: HTMLElement
|
||||
|
||||
constructor(
|
||||
container: HTMLElement | string,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import {Kapacitor, Service} from './'
|
||||
|
||||
export type NewSource = Pick<Source, Exclude<keyof Source, 'id'>>
|
||||
|
||||
export interface Source {
|
||||
id: string
|
||||
name: string
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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])
|
|
@ -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},
|
||||
],
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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},
|
||||
],
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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 = {}) => {
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue