Merge branch 'master' into feature/graph-table-time-format
commit
2f985b56dd
|
@ -1,5 +1,5 @@
|
||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 1.4.2.1
|
current_version = 1.4.2.2
|
||||||
files = README.md server/swagger.json
|
files = README.md server/swagger.json
|
||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\.(?P<release>\d+)
|
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\.(?P<release>\d+)
|
||||||
serialize = {major}.{minor}.{patch}.{release}
|
serialize = {major}.{minor}.{patch}.{release}
|
||||||
|
|
|
@ -2,7 +2,12 @@
|
||||||
### Features
|
### Features
|
||||||
### UI Improvements
|
### UI Improvements
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
1. [#2866](https://github.com/influxdata/chronograf/pull/2866): Change hover text on delete mappings confirmation button to 'Delete'
|
||||||
|
1. [#2911](https://github.com/influxdata/chronograf/pull/2911): Fix Heroku OAuth
|
||||||
|
|
||||||
|
## v1.4.2.2 [2018-03-07]
|
||||||
|
### Bug Fixes
|
||||||
|
1. [#2933](https://github.com/influxdata/chronograf/pull/2933): Include url in Kapacitor connection creation requests
|
||||||
|
|
||||||
## v1.4.2.1 [2018-02-28]
|
## v1.4.2.1 [2018-02-28]
|
||||||
### Features
|
### Features
|
||||||
|
|
|
@ -136,7 +136,7 @@ option.
|
||||||
## Versions
|
## Versions
|
||||||
|
|
||||||
The most recent version of Chronograf is
|
The most recent version of Chronograf is
|
||||||
[v1.4.2.1](https://www.influxdata.com/downloads/).
|
[v1.4.2.2](https://www.influxdata.com/downloads/).
|
||||||
|
|
||||||
Spotted a bug or have a feature request? Please open
|
Spotted a bug or have a feature request? Please open
|
||||||
[an issue](https://github.com/influxdata/chronograf/issues/new)!
|
[an issue](https://github.com/influxdata/chronograf/issues/new)!
|
||||||
|
@ -178,7 +178,7 @@ By default, chronograf runs on port `8888`.
|
||||||
To get started right away with Docker, you can pull down our latest release:
|
To get started right away with Docker, you can pull down our latest release:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker pull chronograf:1.4.2.1
|
docker pull chronograf:1.4.2.2
|
||||||
```
|
```
|
||||||
|
|
||||||
### From Source
|
### From Source
|
||||||
|
|
|
@ -2,6 +2,7 @@ package oauth2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/influxdata/chronograf"
|
"github.com/influxdata/chronograf"
|
||||||
|
@ -61,7 +62,19 @@ func (h *Heroku) PrincipalID(provider *http.Client) (string, error) {
|
||||||
DefaultOrganization DefaultOrg `json:"default_organization"`
|
DefaultOrganization DefaultOrg `json:"default_organization"`
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := provider.Get(HerokuAccountRoute)
|
req, err := http.NewRequest("GET", HerokuAccountRoute, nil)
|
||||||
|
// Requests fail to Heroku unless this Accept header is set.
|
||||||
|
req.Header.Set("Accept", "application/vnd.heroku+json; version=3")
|
||||||
|
resp, err := provider.Do(req)
|
||||||
|
if resp.StatusCode/100 != 2 {
|
||||||
|
err := fmt.Errorf(
|
||||||
|
"Unable to GET user data from %s. Status: %s",
|
||||||
|
HerokuAccountRoute,
|
||||||
|
resp.Status,
|
||||||
|
)
|
||||||
|
h.Logger.Error("", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.Logger.Error("Unable to communicate with Heroku. err:", err)
|
h.Logger.Error("Unable to communicate with Heroku. err:", err)
|
||||||
return "", err
|
return "", err
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"info": {
|
"info": {
|
||||||
"title": "Chronograf",
|
"title": "Chronograf",
|
||||||
"description": "API endpoints for Chronograf",
|
"description": "API endpoints for Chronograf",
|
||||||
"version": "1.4.2.1"
|
"version": "1.4.2.2"
|
||||||
},
|
},
|
||||||
"schemes": ["http"],
|
"schemes": ["http"],
|
||||||
"basePath": "/chronograf/v1",
|
"basePath": "/chronograf/v1",
|
||||||
|
|
|
@ -2,10 +2,19 @@ module.exports = {
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
displayName: 'test',
|
displayName: 'test',
|
||||||
testPathIgnorePatterns: ['/build/'],
|
testPathIgnorePatterns: [
|
||||||
|
'build',
|
||||||
|
'<rootDir>/node_modules/(?!(jest-test))',
|
||||||
|
],
|
||||||
modulePaths: ['<rootDir>', '<rootDir>/node_modules/'],
|
modulePaths: ['<rootDir>', '<rootDir>/node_modules/'],
|
||||||
moduleDirectories: ['src'],
|
moduleDirectories: ['src'],
|
||||||
setupFiles: ['<rootDir>/test/setupTests.js'],
|
setupFiles: ['<rootDir>/test/setupTests.js'],
|
||||||
|
transform: {
|
||||||
|
'^.+\\.tsx?$': 'ts-jest',
|
||||||
|
'^.+\\.js$': 'babel-jest',
|
||||||
|
},
|
||||||
|
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
|
||||||
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
runner: 'jest-runner-eslint',
|
runner: 'jest-runner-eslint',
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "chronograf-ui",
|
"name": "chronograf-ui",
|
||||||
"version": "1.4.2-1",
|
"version": "1.4.2-2",
|
||||||
"private": false,
|
"private": false,
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
"start:fast": "webpack --watch --config ./webpack/dev.config.js",
|
"start:fast": "webpack --watch --config ./webpack/dev.config.js",
|
||||||
"start:hmr": "webpack-dev-server --open --config ./webpack/dev.config.js",
|
"start:hmr": "webpack-dev-server --open --config ./webpack/dev.config.js",
|
||||||
"lint": "esw src/",
|
"lint": "esw src/",
|
||||||
"test": "jest --runInBand",
|
"test": "jest",
|
||||||
"test:lint": "yarn run lint; yarn run test",
|
"test:lint": "yarn run lint; yarn run test",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"clean": "rm -rf ./build/*",
|
"clean": "rm -rf ./build/*",
|
||||||
|
@ -31,9 +31,11 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chai": "^4.1.2",
|
"@types/chai": "^4.1.2",
|
||||||
|
"@types/enzyme": "^3.1.9",
|
||||||
|
"@types/jest": "^22.1.4",
|
||||||
"@types/lodash": "^4.14.104",
|
"@types/lodash": "^4.14.104",
|
||||||
"@types/mocha": "^2.2.48",
|
|
||||||
"@types/node": "^9.4.6",
|
"@types/node": "^9.4.6",
|
||||||
|
"@types/prop-types": "^15.5.2",
|
||||||
"@types/react": "^16.0.38",
|
"@types/react": "^16.0.38",
|
||||||
"autoprefixer": "^6.3.1",
|
"autoprefixer": "^6.3.1",
|
||||||
"babel-core": "^6.5.1",
|
"babel-core": "^6.5.1",
|
||||||
|
@ -51,7 +53,6 @@
|
||||||
"babel-preset-react": "^6.5.0",
|
"babel-preset-react": "^6.5.0",
|
||||||
"babel-preset-stage-0": "^6.16.0",
|
"babel-preset-stage-0": "^6.16.0",
|
||||||
"babel-runtime": "^6.5.0",
|
"babel-runtime": "^6.5.0",
|
||||||
"bower": "^1.7.7",
|
|
||||||
"compression-webpack-plugin": "^1.1.8",
|
"compression-webpack-plugin": "^1.1.8",
|
||||||
"concurrently": "^3.5.0",
|
"concurrently": "^3.5.0",
|
||||||
"core-js": "^2.1.3",
|
"core-js": "^2.1.3",
|
||||||
|
@ -71,7 +72,6 @@
|
||||||
"file-loader": "^1.1.7",
|
"file-loader": "^1.1.7",
|
||||||
"fork-ts-checker-webpack-plugin": "^0.3.0",
|
"fork-ts-checker-webpack-plugin": "^0.3.0",
|
||||||
"hanson": "^1.1.1",
|
"hanson": "^1.1.1",
|
||||||
"hson-loader": "^1.0.0",
|
|
||||||
"html-webpack-include-assets-plugin": "^1.0.2",
|
"html-webpack-include-assets-plugin": "^1.0.2",
|
||||||
"html-webpack-plugin": "^2.30.1",
|
"html-webpack-plugin": "^2.30.1",
|
||||||
"imports-loader": "^0.6.5",
|
"imports-loader": "^0.6.5",
|
||||||
|
@ -79,7 +79,6 @@
|
||||||
"jest-runner-eslint": "^0.4.0",
|
"jest-runner-eslint": "^0.4.0",
|
||||||
"jsdom": "^9.0.0",
|
"jsdom": "^9.0.0",
|
||||||
"json-loader": "^0.5.7",
|
"json-loader": "^0.5.7",
|
||||||
"mustache": "^2.2.1",
|
|
||||||
"node-sass": "^4.5.3",
|
"node-sass": "^4.5.3",
|
||||||
"on-build-webpack": "^0.1.0",
|
"on-build-webpack": "^0.1.0",
|
||||||
"postcss-browser-reporter": "^0.4.0",
|
"postcss-browser-reporter": "^0.4.0",
|
||||||
|
@ -93,8 +92,8 @@
|
||||||
"resolve-url-loader": "^2.2.1",
|
"resolve-url-loader": "^2.2.1",
|
||||||
"sass-loader": "^6.0.6",
|
"sass-loader": "^6.0.6",
|
||||||
"style-loader": "^0.13.0",
|
"style-loader": "^0.13.0",
|
||||||
"testem": "^1.2.1",
|
"thread-loader": "^1.1.5",
|
||||||
"thread-loader": "^1.1.4",
|
"ts-jest": "^22.4.1",
|
||||||
"ts-loader": "^3.5.0",
|
"ts-loader": "^3.5.0",
|
||||||
"tslib": "^1.9.0",
|
"tslib": "^1.9.0",
|
||||||
"typescript": "^2.7.2",
|
"typescript": "^2.7.2",
|
||||||
|
|
|
@ -111,6 +111,7 @@ class OrganizationsTableRow extends Component {
|
||||||
onConfirm={this.handleDeleteOrg}
|
onConfirm={this.handleDeleteOrg}
|
||||||
onClickOutside={this.handleDismissDeleteConfirmation}
|
onClickOutside={this.handleDismissDeleteConfirmation}
|
||||||
confirmLeft={true}
|
confirmLeft={true}
|
||||||
|
confirmTitle="Delete"
|
||||||
/>
|
/>
|
||||||
: <OrganizationsTableRowDeleteButton
|
: <OrganizationsTableRowDeleteButton
|
||||||
organization={organization}
|
organization={organization}
|
||||||
|
|
|
@ -109,6 +109,7 @@ class ProvidersTableRow extends Component {
|
||||||
onCancel={this.handleDismissDeleteConfirmation}
|
onCancel={this.handleDismissDeleteConfirmation}
|
||||||
onConfirm={this.handleDeleteMap}
|
onConfirm={this.handleDeleteMap}
|
||||||
onClickOutside={this.handleDismissDeleteConfirmation}
|
onClickOutside={this.handleDismissDeleteConfirmation}
|
||||||
|
confirmTitle="Delete"
|
||||||
/>
|
/>
|
||||||
: <button
|
: <button
|
||||||
className="btn btn-sm btn-default btn-square"
|
className="btn btn-sm btn-default btn-square"
|
||||||
|
|
|
@ -90,7 +90,7 @@ class AxesOptions extends Component {
|
||||||
<h5 className="display-options--header">
|
<h5 className="display-options--header">
|
||||||
{menuOption} Controls
|
{menuOption} Controls
|
||||||
</h5>
|
</h5>
|
||||||
<form autoComplete="off" style={{margin: '0 -6px'}}>
|
<form autoComplete="off" className="form-group-wrapper">
|
||||||
<div className="form-group col-sm-12">
|
<div className="form-group col-sm-12">
|
||||||
<label htmlFor="prefix">Title</label>
|
<label htmlFor="prefix">Title</label>
|
||||||
<OptIn
|
<OptIn
|
||||||
|
|
|
@ -190,7 +190,7 @@ class GaugeOptions extends Component {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="single-stat-controls">
|
<div className="graph-options-group form-group-wrapper">
|
||||||
<div className="form-group col-xs-6">
|
<div className="form-group col-xs-6">
|
||||||
<label>Prefix</label>
|
<label>Prefix</label>
|
||||||
<input
|
<input
|
||||||
|
|
|
@ -8,15 +8,16 @@ const GraphOptionsCustomizableColumn = ({
|
||||||
onColumnRename,
|
onColumnRename,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="gauge-controls--section">
|
<div className="column-controls--section">
|
||||||
<div className="gauge-controls--label">
|
<div className="column-controls--label">
|
||||||
{originalColumnName}
|
{originalColumnName}
|
||||||
</div>
|
</div>
|
||||||
<InputClickToEdit
|
<InputClickToEdit
|
||||||
value={newColumnName}
|
value={newColumnName}
|
||||||
wrapperClass="fancytable--td orgs-table--name"
|
wrapperClass="column-controls-input"
|
||||||
onUpdate={onColumnRename}
|
onUpdate={onColumnRename}
|
||||||
placeholder="Rename..."
|
placeholder="Rename..."
|
||||||
|
appearAsNormalInput={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,8 +5,8 @@ import uuid from 'uuid'
|
||||||
|
|
||||||
const GraphOptionsCustomizeColumns = ({columns, onColumnRename}) => {
|
const GraphOptionsCustomizeColumns = ({columns, onColumnRename}) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="graph-options-group">
|
||||||
<label>Customize Columns</label>
|
<label className="form-label">Customize Columns</label>
|
||||||
{columns.map(col => {
|
{columns.map(col => {
|
||||||
return (
|
return (
|
||||||
<GraphOptionsCustomizableColumn
|
<GraphOptionsCustomizableColumn
|
||||||
|
|
|
@ -7,8 +7,8 @@ const GraphOptionsSortBy = ({sortByOptions, onChooseSortBy}) =>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
items={sortByOptions}
|
items={sortByOptions}
|
||||||
selected={sortByOptions[0].text}
|
selected={sortByOptions[0].text}
|
||||||
buttonColor="btn-primary"
|
buttonColor="btn-default"
|
||||||
buttonSize="btn-xs"
|
buttonSize="btn-sm"
|
||||||
className="dropdown-stretch"
|
className="dropdown-stretch"
|
||||||
onChoose={onChooseSortBy}
|
onChoose={onChooseSortBy}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
// TODO: Needs major refactoring to make thresholds a much more general component to be shared between single stat, gauge, and table.
|
// TODO: Needs major refactoring to make thresholds a much more general component to be shared between single stat, gauge, and table.
|
||||||
const GraphOptionsTextWrapping = ({singleStatType, onToggleTextWrapping}) => {
|
const GraphOptionsTextWrapping = ({singleStatType, onToggleTextWrapping}) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="form-group col-xs-12">
|
||||||
<label>Text Wrapping</label>
|
<label>Text Wrapping</label>
|
||||||
<ul className="nav nav-tablist nav-tablist-sm">
|
<ul className="nav nav-tablist nav-tablist-sm">
|
||||||
<li
|
<li
|
||||||
|
|
|
@ -11,7 +11,7 @@ const GraphOptionsThresholdColoring = ({
|
||||||
singleStatType,
|
singleStatType,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="form-group col-xs-12 col-md-6">
|
||||||
<label>Threshold Coloring</label>
|
<label>Threshold Coloring</label>
|
||||||
<ul className="nav nav-tablist nav-tablist-sm">
|
<ul className="nav nav-tablist nav-tablist-sm">
|
||||||
<li
|
<li
|
||||||
|
|
|
@ -19,8 +19,8 @@ const GraphOptionsThresholds = ({
|
||||||
onDeleteThreshold,
|
onDeleteThreshold,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="gauge-controls graph-options-group">
|
||||||
<label>Thresholds</label>
|
<label className="form-label">Thresholds</label>
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-primary gauge-controls--add-threshold"
|
className="btn btn-sm btn-primary gauge-controls--add-threshold"
|
||||||
onClick={onAddThreshold}
|
onClick={onAddThreshold}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import React, {PropTypes} from 'react'
|
||||||
const VERTICAL = 'VERTICAL'
|
const VERTICAL = 'VERTICAL'
|
||||||
const HORIZONTAL = 'HORIZONTAL'
|
const HORIZONTAL = 'HORIZONTAL'
|
||||||
const GraphOptionsTimeAxis = ({TimeAxis, onToggleTimeAxis}) =>
|
const GraphOptionsTimeAxis = ({TimeAxis, onToggleTimeAxis}) =>
|
||||||
<div className="form-group col-xs-6">
|
<div className="form-group col-xs-12 col-sm-6">
|
||||||
<label>Time Axis</label>
|
<label>Time Axis</label>
|
||||||
<ul className="nav nav-tablist nav-tablist-sm">
|
<ul className="nav nav-tablist nav-tablist-sm">
|
||||||
<li
|
<li
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, {Component} from 'react'
|
import React, {Component} from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
|
||||||
import InputClickToEdit from 'shared/components/InputClickToEdit'
|
import InputClickToEdit from 'shared/components/InputClickToEdit'
|
||||||
import {Dropdown} from 'src/shared/components/Dropdown'
|
import {Dropdown} from 'src/shared/components/Dropdown'
|
||||||
|
@ -43,9 +44,9 @@ class GraphOptionsTimeFormat extends Component {
|
||||||
'For information on formatting, see http://momentjs.com/docs/#/parsing/string-format/'
|
'For information on formatting, see http://momentjs.com/docs/#/parsing/string-format/'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="gauge-controls--section">
|
<div className="form-group col-xs-12">
|
||||||
<label>
|
<label>
|
||||||
Time Format{' '}
|
Time Format
|
||||||
{customFormat &&
|
{customFormat &&
|
||||||
<QuestionMarkTooltip
|
<QuestionMarkTooltip
|
||||||
tipID="Time Axis Format"
|
tipID="Time Axis Format"
|
||||||
|
@ -55,18 +56,21 @@ class GraphOptionsTimeFormat extends Component {
|
||||||
<Dropdown
|
<Dropdown
|
||||||
items={formatOptions}
|
items={formatOptions}
|
||||||
selected={customFormat ? 'Custom' : format}
|
selected={customFormat ? 'Custom' : format}
|
||||||
buttonColor="btn-primary"
|
buttonColor="btn-default"
|
||||||
buttonSize="btn-xs"
|
buttonSize="btn-xs"
|
||||||
className="dropdown-stretch"
|
className="dropdown-stretch"
|
||||||
onChoose={this.handleChooseFormat}
|
onChoose={this.handleChooseFormat}
|
||||||
/>
|
/>
|
||||||
{customFormat &&
|
{customFormat &&
|
||||||
<InputClickToEdit
|
<div className="column-controls--section">
|
||||||
wrapperClass="fancytable--td"
|
<InputClickToEdit
|
||||||
value={format}
|
wrapperClass="column-controls-input "
|
||||||
onUpdate={this.handleInputChange}
|
value={format}
|
||||||
placeholder="Enter custom format..."
|
onUpdate={this.handleInputChange}
|
||||||
/>}
|
placeholder="Enter custom format..."
|
||||||
|
appearAsNormalInput={true}
|
||||||
|
/>
|
||||||
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -186,7 +186,7 @@ class SingleStatOptions extends Component {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="single-stat-controls">
|
<div className="graph-options-group form-group-wrapper">
|
||||||
<div className="form-group col-xs-6">
|
<div className="form-group col-xs-6">
|
||||||
<label>Prefix</label>
|
<label>Prefix</label>
|
||||||
<input
|
<input
|
||||||
|
|
|
@ -66,12 +66,20 @@ class TableOptions extends Component {
|
||||||
|
|
||||||
const sortedColors = _.sortBy(singleStatColors, color => color.value)
|
const sortedColors = _.sortBy(singleStatColors, color => color.value)
|
||||||
|
|
||||||
const columns = ['hey', 'yo', 'what'].map(col => ({
|
const columns = [
|
||||||
|
'cpu.mean_usage_system',
|
||||||
|
'cpu.mean_usage_idle',
|
||||||
|
'cpu.mean_usage_user',
|
||||||
|
].map(col => ({
|
||||||
text: col,
|
text: col,
|
||||||
name: col,
|
name: col,
|
||||||
newName: '',
|
newName: '',
|
||||||
}))
|
}))
|
||||||
const tableSortByOptions = ['hey', 'yo', 'what'].map(col => ({text: col}))
|
const tableSortByOptions = [
|
||||||
|
'cpu.mean_usage_system',
|
||||||
|
'cpu.mean_usage_idle',
|
||||||
|
'cpu.mean_usage_user',
|
||||||
|
].map(col => ({text: col}))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FancyScrollbar
|
<FancyScrollbar
|
||||||
|
@ -80,7 +88,7 @@ class TableOptions extends Component {
|
||||||
>
|
>
|
||||||
<div className="display-options--cell-wrapper">
|
<div className="display-options--cell-wrapper">
|
||||||
<h5 className="display-options--header">Table Controls</h5>
|
<h5 className="display-options--header">Table Controls</h5>
|
||||||
<div className="gauge-controls">
|
<div className="form-group-wrapper">
|
||||||
<GraphOptionsTimeFormat
|
<GraphOptionsTimeFormat
|
||||||
timeFormat={timeFormat}
|
timeFormat={timeFormat}
|
||||||
onTimeFormatChange={this.handleTimeFormatChange}
|
onTimeFormatChange={this.handleTimeFormatChange}
|
||||||
|
@ -97,20 +105,22 @@ class TableOptions extends Component {
|
||||||
singleStatType={singleStatType}
|
singleStatType={singleStatType}
|
||||||
onToggleTextWrapping={this.handleToggleTextWrapping}
|
onToggleTextWrapping={this.handleToggleTextWrapping}
|
||||||
/>
|
/>
|
||||||
<GraphOptionsCustomizeColumns
|
</div>
|
||||||
columns={columns}
|
<GraphOptionsCustomizeColumns
|
||||||
onColumnRename={this.handleColumnRename}
|
columns={columns}
|
||||||
/>
|
onColumnRename={this.handleColumnRename}
|
||||||
<GraphOptionsThresholds
|
/>
|
||||||
onAddThreshold={this.handleAddThreshold}
|
<GraphOptionsThresholds
|
||||||
disableAddThreshold={disableAddThreshold}
|
onAddThreshold={this.handleAddThreshold}
|
||||||
sortedColors={sortedColors}
|
disableAddThreshold={disableAddThreshold}
|
||||||
formatColor={formatColor}
|
sortedColors={sortedColors}
|
||||||
onChooseColor={this.handleChooseColor}
|
formatColor={formatColor}
|
||||||
onValidateColorValue={this.handleValidateColorValue}
|
onChooseColor={this.handleChooseColor}
|
||||||
onUpdateColorValue={this.handleUpdateColorValue}
|
onValidateColorValue={this.handleValidateColorValue}
|
||||||
onDeleteThreshold={this.handleDeleteThreshold}
|
onUpdateColorValue={this.handleUpdateColorValue}
|
||||||
/>
|
onDeleteThreshold={this.handleDeleteThreshold}
|
||||||
|
/>
|
||||||
|
<div className="form-group-wrapper graph-options-group">
|
||||||
<GraphOptionsThresholdColoring
|
<GraphOptionsThresholdColoring
|
||||||
onToggleSingleStatType={this.handleToggleSingleStatType}
|
onToggleSingleStatType={this.handleToggleSingleStatType}
|
||||||
singleStatColors={singleStatType}
|
singleStatColors={singleStatType}
|
||||||
|
|
|
@ -439,29 +439,71 @@ const GRAPH_SVGS = {
|
||||||
y="0px"
|
y="0px"
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
viewBox="0 0 550 550"
|
viewBox="0 0 150 150"
|
||||||
>
|
>
|
||||||
<g>
|
<path
|
||||||
<path
|
className="viz-type-selector--graphic-fill graphic-fill-c"
|
||||||
className="viz-type-selector--graphic-line graphic-line-a"
|
d="M55.5,115H19.7c-1.7,0-3.1-1.4-3.1-3.1V61.7h38.9V115z"
|
||||||
d="M430.274,23.861H16.698C7.48,23.861,0,31.357,0,40.559v365.86c0,5.654,2.834,10.637,7.155,13.663v2.632h5.986 c1.146,0.252,2.332,0.401,3.557,0.401h413.576c1.214,0,2.396-0.149,3.545-0.401h0.821v-0.251 c7.082-1.938,12.336-8.362,12.336-16.044V40.564C446.977,31.357,439.478,23.861,430.274,23.861z M66,408.4H15.458 c-0.676-0.416-1.146-1.132-1.146-1.98v-43.35H66V408.4z M66,348.755H14.312v-47.01H66V348.755z M66,287.436H14.312v-49.632H66 V287.436z M66,223.491H14.312v-53.687H66V223.491z M66,155.49H14.312v-52.493H66V155.49z M186.497,408.4H80.318v-45.33h106.179 V408.4z M186.497,348.755H80.318v-47.01h106.179V348.755z M186.497,287.436H80.318v-49.632h106.179V287.436z M186.497,223.491 H80.318v-53.687h106.179V223.491z M186.497,155.49H80.318v-52.493h106.179V155.49z M186.497,88.68H80.318V38.17h106.179V88.68z M308.195,408.4H200.812v-45.33h107.383V408.4z M308.195,348.755H200.812v-47.01h107.383V348.755z M308.195,287.436H200.812 v-49.632h107.383V287.436z M308.195,223.491H200.812v-53.687h107.383V223.491z M308.195,155.49H200.812v-52.493h107.383V155.49z M308.195,88.68H200.812V38.17h107.383V88.68z M432.66,406.419c0,0.845-0.48,1.56-1.149,1.98h-109v-45.33H432.66V406.419z M432.66,348.755H322.511v-47.01H432.66V348.755z M432.66,287.436H322.511v-49.632H432.66V287.436z M432.66,223.491H322.511 v-53.687H432.66V223.491z M432.66,155.49H322.511v-52.493H432.66V155.49z M432.66,88.68H322.511V38.17h107.764 c1.312,0,2.386,1.073,2.386,2.389V88.68z M175.854,276.251H89.938V246.37h85.915V276.251z M297.261,277.378h-85.915v-29.883h85.915 V277.378z M421.661,276.721h-85.914v-29.883h85.914V276.721z"
|
/>
|
||||||
/>
|
<path
|
||||||
</g>
|
className="viz-type-selector--graphic-fill graphic-fill-b"
|
||||||
<g />
|
d="M133.4,61.7H55.5V35h74.8c1.7,0,3.1,1.4,3.1,3.1V61.7z"
|
||||||
<g />
|
/>
|
||||||
<g />
|
<path
|
||||||
<g />
|
className="viz-type-selector--graphic-fill graphic-fill-a"
|
||||||
<g />
|
d="M55.5,61.7H16.6V38.1c0-1.7,1.4-3.1,3.1-3.1h35.9V61.7z"
|
||||||
<g />
|
/>
|
||||||
<g />
|
<path
|
||||||
<g />
|
className="viz-type-selector--graphic-line graphic-line-c"
|
||||||
<g />
|
d="M16.6,88.3v23.6c0,1.7,1.4,3.1,3.1,3.1h35.9V88.3H16.6z"
|
||||||
<g />
|
/>
|
||||||
<g />
|
<rect
|
||||||
<g />
|
className="viz-type-selector--graphic-line graphic-line-c"
|
||||||
<g />
|
x="16.6"
|
||||||
<g />
|
y="61.7"
|
||||||
<g />
|
width="38.9"
|
||||||
|
height="26.7"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="viz-type-selector--graphic-line graphic-line-b"
|
||||||
|
d="M94.5,35v26.7h38.9V38.1c0-1.7-1.4-3.1-3.1-3.1H94.5z"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
className="viz-type-selector--graphic-line graphic-line-b"
|
||||||
|
x="55.5"
|
||||||
|
y="35"
|
||||||
|
width="38.9"
|
||||||
|
height="26.7"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="viz-type-selector--graphic-line graphic-line-d"
|
||||||
|
d="M94.5,115h35.9c1.7,0,3.1-1.4,3.1-3.1V88.3H94.5V115z"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
className="viz-type-selector--graphic-line graphic-line-d"
|
||||||
|
x="55.5"
|
||||||
|
y="88.3"
|
||||||
|
width="38.9"
|
||||||
|
height="26.7"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
className="viz-type-selector--graphic-line graphic-line-d"
|
||||||
|
x="94.5"
|
||||||
|
y="61.7"
|
||||||
|
width="38.9"
|
||||||
|
height="26.7"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
className="viz-type-selector--graphic-line graphic-line-d"
|
||||||
|
x="55.5"
|
||||||
|
y="61.7"
|
||||||
|
width="38.9"
|
||||||
|
height="26.7"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="viz-type-selector--graphic-line graphic-line-a"
|
||||||
|
d="M55.5,35H19.7c-1.7,0-3.1,1.4-3.1,3.1v23.6h38.9V35z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,139 +0,0 @@
|
||||||
import React, {PropTypes} from 'react'
|
|
||||||
import _ from 'lodash'
|
|
||||||
|
|
||||||
import TagListItem from './TagListItem'
|
|
||||||
|
|
||||||
import {showTagKeys, showTagValues} from 'shared/apis/metaQuery'
|
|
||||||
import showTagKeysParser from 'shared/parsing/showTagKeys'
|
|
||||||
import showTagValuesParser from 'shared/parsing/showTagValues'
|
|
||||||
|
|
||||||
const {string, shape, func, bool} = PropTypes
|
|
||||||
|
|
||||||
const TagList = React.createClass({
|
|
||||||
propTypes: {
|
|
||||||
query: shape({
|
|
||||||
database: string,
|
|
||||||
measurement: string,
|
|
||||||
retentionPolicy: string,
|
|
||||||
areTagsAccepted: bool.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
onChooseTag: func.isRequired,
|
|
||||||
onGroupByTag: func.isRequired,
|
|
||||||
querySource: shape({
|
|
||||||
links: shape({
|
|
||||||
proxy: string.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
|
|
||||||
contextTypes: {
|
|
||||||
source: shape({
|
|
||||||
links: shape({
|
|
||||||
proxy: string.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
},
|
|
||||||
|
|
||||||
getDefaultProps() {
|
|
||||||
return {
|
|
||||||
querySource: null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState() {
|
|
||||||
return {
|
|
||||||
tags: {},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_getTags() {
|
|
||||||
const {database, measurement, retentionPolicy} = this.props.query
|
|
||||||
const {source} = this.context
|
|
||||||
const {querySource} = this.props
|
|
||||||
|
|
||||||
const proxy =
|
|
||||||
_.get(querySource, ['links', 'proxy'], null) || source.links.proxy
|
|
||||||
|
|
||||||
showTagKeys({source: proxy, database, retentionPolicy, measurement})
|
|
||||||
.then(resp => {
|
|
||||||
const {errors, tagKeys} = showTagKeysParser(resp.data)
|
|
||||||
if (errors.length) {
|
|
||||||
// do something
|
|
||||||
}
|
|
||||||
|
|
||||||
return showTagValues({
|
|
||||||
source: proxy,
|
|
||||||
database,
|
|
||||||
retentionPolicy,
|
|
||||||
measurement,
|
|
||||||
tagKeys,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.then(resp => {
|
|
||||||
const {errors: errs, tags} = showTagValuesParser(resp.data)
|
|
||||||
if (errs.length) {
|
|
||||||
// do something
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({tags})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const {database, measurement, retentionPolicy} = this.props.query
|
|
||||||
if (!database || !measurement || !retentionPolicy) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this._getTags()
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {query, querySource} = this.props
|
|
||||||
const {database, measurement, retentionPolicy} = query
|
|
||||||
|
|
||||||
const {
|
|
||||||
database: prevDB,
|
|
||||||
measurement: prevMeas,
|
|
||||||
retentionPolicy: prevRP,
|
|
||||||
} = prevProps.query
|
|
||||||
if (!database || !measurement || !retentionPolicy) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
database === prevDB &&
|
|
||||||
measurement === prevMeas &&
|
|
||||||
retentionPolicy === prevRP &&
|
|
||||||
_.isEqual(prevProps.querySource, querySource)
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this._getTags()
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {query} = this.props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="query-builder--sub-list">
|
|
||||||
{_.map(this.state.tags, (tagValues, tagKey) => {
|
|
||||||
return (
|
|
||||||
<TagListItem
|
|
||||||
key={tagKey}
|
|
||||||
tagKey={tagKey}
|
|
||||||
tagValues={tagValues}
|
|
||||||
selectedTagValues={query.tags[tagKey] || []}
|
|
||||||
isUsingGroupBy={query.groupBy.tags.indexOf(tagKey) > -1}
|
|
||||||
onChooseTag={this.props.onChooseTag}
|
|
||||||
onGroupByTag={this.props.onGroupByTag}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export default TagList
|
|
|
@ -60,8 +60,8 @@ class KapacitorPage extends Component {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChangeUrl = ({value}) => {
|
handleChangeUrl = e => {
|
||||||
this.setState({kapacitor: {...this.state.kapacitor, url: value}})
|
this.setState({kapacitor: {...this.state.kapacitor, url: e.target.value}})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSubmit = e => {
|
handleSubmit = e => {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React, {Component, PropTypes} from 'react'
|
import React, {Component} from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import OnClickOutside from 'shared/components/OnClickOutside'
|
import OnClickOutside from 'shared/components/OnClickOutside'
|
||||||
|
|
||||||
|
|
|
@ -1,86 +0,0 @@
|
||||||
import React, {PropTypes, Component} from 'react'
|
|
||||||
import classnames from 'classnames'
|
|
||||||
|
|
||||||
import OnClickOutside from 'shared/components/OnClickOutside'
|
|
||||||
|
|
||||||
class ConfirmButtons extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleConfirm = item => () => {
|
|
||||||
this.props.onConfirm(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCancel = item => () => {
|
|
||||||
this.props.onCancel(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClickOutside = () => {
|
|
||||||
this.props.onClickOutside(this.props.item)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {item, buttonSize, isDisabled, confirmLeft} = this.props
|
|
||||||
|
|
||||||
return confirmLeft
|
|
||||||
? <div className="confirm-buttons">
|
|
||||||
<button
|
|
||||||
className={classnames('btn btn-success btn-square', {
|
|
||||||
[buttonSize]: buttonSize,
|
|
||||||
})}
|
|
||||||
disabled={isDisabled}
|
|
||||||
title={isDisabled ? 'Cannot Save' : 'Save'}
|
|
||||||
onClick={this.handleConfirm(item)}
|
|
||||||
>
|
|
||||||
<span className="icon checkmark" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={classnames('btn btn-info btn-square', {
|
|
||||||
[buttonSize]: buttonSize,
|
|
||||||
})}
|
|
||||||
onClick={this.handleCancel(item)}
|
|
||||||
>
|
|
||||||
<span className="icon remove" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
: <div className="confirm-buttons">
|
|
||||||
<button
|
|
||||||
className={classnames('btn btn-info btn-square', {
|
|
||||||
[buttonSize]: buttonSize,
|
|
||||||
})}
|
|
||||||
onClick={this.handleCancel(item)}
|
|
||||||
>
|
|
||||||
<span className="icon remove" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={classnames('btn btn-success btn-square', {
|
|
||||||
[buttonSize]: buttonSize,
|
|
||||||
})}
|
|
||||||
disabled={isDisabled}
|
|
||||||
title={isDisabled ? 'Cannot Save' : 'Save'}
|
|
||||||
onClick={this.handleConfirm(item)}
|
|
||||||
>
|
|
||||||
<span className="icon checkmark" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const {func, oneOfType, shape, string, bool} = PropTypes
|
|
||||||
|
|
||||||
ConfirmButtons.propTypes = {
|
|
||||||
onConfirm: func.isRequired,
|
|
||||||
item: oneOfType([shape(), string]),
|
|
||||||
onCancel: func.isRequired,
|
|
||||||
buttonSize: string,
|
|
||||||
isDisabled: bool,
|
|
||||||
onClickOutside: func,
|
|
||||||
confirmLeft: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfirmButtons.defaultProps = {
|
|
||||||
buttonSize: 'btn-sm',
|
|
||||||
onClickOutside: () => {},
|
|
||||||
}
|
|
||||||
export default OnClickOutside(ConfirmButtons)
|
|
|
@ -0,0 +1,120 @@
|
||||||
|
import React, {PureComponent, SFC} from 'react'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
|
||||||
|
import OnClickOutside from 'src/shared/components/OnClickOutside'
|
||||||
|
|
||||||
|
type Item = Object | string
|
||||||
|
|
||||||
|
interface ConfirmProps {
|
||||||
|
buttonSize: string
|
||||||
|
isDisabled: boolean
|
||||||
|
onConfirm: () => void
|
||||||
|
icon: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CancelProps {
|
||||||
|
buttonSize: string
|
||||||
|
onCancel: () => void
|
||||||
|
icon: string
|
||||||
|
}
|
||||||
|
interface ConfirmButtonsProps {
|
||||||
|
onConfirm: (item: Item) => void
|
||||||
|
item: Item
|
||||||
|
onCancel: (item: Item) => void
|
||||||
|
buttonSize?: string
|
||||||
|
isDisabled?: boolean
|
||||||
|
onClickOutside?: (item: Item) => void
|
||||||
|
confirmLeft?: boolean
|
||||||
|
confirmTitle?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Confirm: SFC<ConfirmProps> = ({
|
||||||
|
buttonSize,
|
||||||
|
isDisabled,
|
||||||
|
onConfirm,
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
}) =>
|
||||||
|
<button
|
||||||
|
data-test="confirm"
|
||||||
|
className={classnames('btn btn-success btn-square', {
|
||||||
|
[buttonSize]: buttonSize,
|
||||||
|
})}
|
||||||
|
disabled={isDisabled}
|
||||||
|
title={isDisabled ? `Cannot ${title}` : title}
|
||||||
|
onClick={onConfirm}
|
||||||
|
>
|
||||||
|
<span className={icon} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
export const Cancel: SFC<CancelProps> = ({buttonSize, onCancel, icon}) =>
|
||||||
|
<button
|
||||||
|
data-test="cancel"
|
||||||
|
className={classnames('btn btn-info btn-square', {
|
||||||
|
[buttonSize]: buttonSize,
|
||||||
|
})}
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
<span className={icon} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
class ConfirmButtons extends PureComponent<ConfirmButtonsProps, {}> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static defaultProps: Partial<ConfirmButtonsProps> = {
|
||||||
|
buttonSize: 'btn-sm',
|
||||||
|
onClickOutside: () => {},
|
||||||
|
confirmTitle: 'Save',
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConfirm = item => () => {
|
||||||
|
this.props.onConfirm(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCancel = item => () => {
|
||||||
|
this.props.onCancel(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClickOutside = () => {
|
||||||
|
this.props.onClickOutside(this.props.item)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {item, buttonSize, isDisabled, confirmLeft, confirmTitle} = this.props
|
||||||
|
|
||||||
|
return confirmLeft
|
||||||
|
? <div className="confirm-buttons">
|
||||||
|
<Confirm
|
||||||
|
buttonSize={buttonSize}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
onConfirm={this.handleConfirm(item)}
|
||||||
|
icon="icon checkmark"
|
||||||
|
title={confirmTitle}
|
||||||
|
/>
|
||||||
|
<Cancel
|
||||||
|
buttonSize={buttonSize}
|
||||||
|
onCancel={this.handleCancel(item)}
|
||||||
|
icon="icon remove"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
: <div className="confirm-buttons">
|
||||||
|
<Cancel
|
||||||
|
buttonSize={buttonSize}
|
||||||
|
onCancel={this.handleCancel(item)}
|
||||||
|
icon="icon remove"
|
||||||
|
/>
|
||||||
|
<Confirm
|
||||||
|
buttonSize={buttonSize}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
onConfirm={this.handleConfirm(item)}
|
||||||
|
icon="icon checkmark"
|
||||||
|
title={confirmTitle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OnClickOutside(ConfirmButtons)
|
|
@ -1,137 +0,0 @@
|
||||||
import React, {PropTypes} from 'react'
|
|
||||||
import classnames from 'classnames'
|
|
||||||
import _ from 'lodash'
|
|
||||||
|
|
||||||
import {showDatabases, showRetentionPolicies} from 'shared/apis/metaQuery'
|
|
||||||
import showDatabasesParser from 'shared/parsing/showDatabases'
|
|
||||||
import showRetentionPoliciesParser from 'shared/parsing/showRetentionPolicies'
|
|
||||||
|
|
||||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
|
||||||
|
|
||||||
const {func, shape, string} = PropTypes
|
|
||||||
|
|
||||||
const DatabaseList = React.createClass({
|
|
||||||
propTypes: {
|
|
||||||
query: shape({}).isRequired,
|
|
||||||
onChooseNamespace: func.isRequired,
|
|
||||||
querySource: shape({
|
|
||||||
links: shape({
|
|
||||||
proxy: string.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
|
|
||||||
getDefaultProps() {
|
|
||||||
return {
|
|
||||||
source: null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
contextTypes: {
|
|
||||||
source: shape({
|
|
||||||
links: shape({
|
|
||||||
proxy: string.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState() {
|
|
||||||
return {
|
|
||||||
namespaces: [],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.getDbRp()
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidUpdate({querySource: prevSource, query: prevQuery}) {
|
|
||||||
const {querySource: nextSource, query: nextQuery} = this.props
|
|
||||||
const differentSource = !_.isEqual(prevSource, nextSource)
|
|
||||||
|
|
||||||
if (prevQuery.rawText === nextQuery.rawText) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const newMetaQuery =
|
|
||||||
nextQuery.rawText && nextQuery.rawText.match(/^(create|drop)/i)
|
|
||||||
|
|
||||||
if (differentSource || newMetaQuery) {
|
|
||||||
setTimeout(this.getDbRp, 100)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getDbRp() {
|
|
||||||
const {source} = this.context
|
|
||||||
const {querySource} = this.props
|
|
||||||
const proxy =
|
|
||||||
_.get(querySource, ['links', 'proxy'], null) || source.links.proxy
|
|
||||||
|
|
||||||
showDatabases(proxy).then(resp => {
|
|
||||||
const {errors, databases} = showDatabasesParser(resp.data)
|
|
||||||
if (errors.length) {
|
|
||||||
// do something
|
|
||||||
}
|
|
||||||
|
|
||||||
const namespaces = []
|
|
||||||
showRetentionPolicies(proxy, databases).then(res => {
|
|
||||||
res.data.results.forEach((result, index) => {
|
|
||||||
const {errors: errs, retentionPolicies} = showRetentionPoliciesParser(
|
|
||||||
result
|
|
||||||
)
|
|
||||||
if (errs.length) {
|
|
||||||
// do something
|
|
||||||
}
|
|
||||||
|
|
||||||
retentionPolicies.forEach(rp => {
|
|
||||||
namespaces.push({
|
|
||||||
database: databases[index],
|
|
||||||
retentionPolicy: rp.name,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
this.setState({namespaces})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {query, onChooseNamespace} = this.props
|
|
||||||
const {namespaces} = this.state
|
|
||||||
const sortedNamespaces = namespaces.length
|
|
||||||
? _.sortBy(namespaces, n => n.database.toLowerCase())
|
|
||||||
: namespaces
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="query-builder--column query-builder--column-db">
|
|
||||||
<div className="query-builder--heading">DB.RetentionPolicy</div>
|
|
||||||
<div className="query-builder--list">
|
|
||||||
<FancyScrollbar>
|
|
||||||
{sortedNamespaces.map(namespace => {
|
|
||||||
const {database, retentionPolicy} = namespace
|
|
||||||
const isActive =
|
|
||||||
database === query.database &&
|
|
||||||
retentionPolicy === query.retentionPolicy
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classnames('query-builder--list-item', {
|
|
||||||
active: isActive,
|
|
||||||
})}
|
|
||||||
key={`${database}..${retentionPolicy}`}
|
|
||||||
onClick={_.wrap(namespace, onChooseNamespace)}
|
|
||||||
data-test={`query-builder-list-item-database-${database}`}
|
|
||||||
>
|
|
||||||
{database}.{retentionPolicy}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</FancyScrollbar>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export default DatabaseList
|
|
|
@ -0,0 +1,131 @@
|
||||||
|
import React, {PureComponent} from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
import {Source, Query} from 'src/types'
|
||||||
|
import {Namespace} from 'src/types/query'
|
||||||
|
|
||||||
|
import {showDatabases, showRetentionPolicies} from 'src/shared/apis/metaQuery'
|
||||||
|
import showDatabasesParser from 'src/shared/parsing/showDatabases'
|
||||||
|
import showRetentionPoliciesParser from 'src/shared/parsing/showRetentionPolicies'
|
||||||
|
|
||||||
|
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
||||||
|
import DatabaseListItem from 'src/shared/components/DatabaseListItem'
|
||||||
|
|
||||||
|
interface DatabaseListProps {
|
||||||
|
query: Query
|
||||||
|
querySource: Source
|
||||||
|
onChooseNamespace: (namespace: Namespace) => void
|
||||||
|
source: Source
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DatabaseListState {
|
||||||
|
namespaces: Namespace[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const {shape, string} = PropTypes
|
||||||
|
|
||||||
|
class DatabaseList extends PureComponent<DatabaseListProps, DatabaseListState> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.getDbRp = this.getDbRp.bind(this)
|
||||||
|
this.handleChooseNamespace = this.handleChooseNamespace.bind(this)
|
||||||
|
this.state = {
|
||||||
|
namespaces: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static defaultProps: Partial<DatabaseListProps> = {
|
||||||
|
source: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
public static contextTypes = {
|
||||||
|
source: shape({
|
||||||
|
links: shape({
|
||||||
|
proxy: string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.getDbRp()
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate({querySource: prevSource, query: prevQuery}) {
|
||||||
|
const {querySource: nextSource, query: nextQuery} = this.props
|
||||||
|
const differentSource = !_.isEqual(prevSource, nextSource)
|
||||||
|
|
||||||
|
if (prevQuery.rawText === nextQuery.rawText) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newMetaQuery =
|
||||||
|
nextQuery.rawText && nextQuery.rawText.match(/^(create|drop)/i)
|
||||||
|
|
||||||
|
if (differentSource || newMetaQuery) {
|
||||||
|
setTimeout(this.getDbRp, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDbRp() {
|
||||||
|
const {source} = this.context
|
||||||
|
const {querySource} = this.props
|
||||||
|
const proxy = _.get(querySource, ['links', 'proxy'], source.links.proxy)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {data} = await showDatabases(proxy)
|
||||||
|
const {databases} = showDatabasesParser(data)
|
||||||
|
const rps = await showRetentionPolicies(proxy, databases)
|
||||||
|
const namespaces = rps.data.results.reduce((acc, result, index) => {
|
||||||
|
const {retentionPolicies} = showRetentionPoliciesParser(result)
|
||||||
|
|
||||||
|
const dbrp = retentionPolicies.map(rp => ({
|
||||||
|
database: databases[index],
|
||||||
|
retentionPolicy: rp.name,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return [...acc, ...dbrp]
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const sorted = _.sortBy(namespaces, ({database}: Namespace) =>
|
||||||
|
database.toLowerCase()
|
||||||
|
)
|
||||||
|
|
||||||
|
this.setState({namespaces: sorted})
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChooseNamespace(namespace: Namespace) {
|
||||||
|
return () => this.props.onChooseNamespace(namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive(query: Query, {database, retentionPolicy}: Namespace) {
|
||||||
|
return (
|
||||||
|
database === query.database && retentionPolicy === query.retentionPolicy
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="query-builder--column query-builder--column-db">
|
||||||
|
<div className="query-builder--heading">DB.RetentionPolicy</div>
|
||||||
|
<div className="query-builder--list">
|
||||||
|
<FancyScrollbar>
|
||||||
|
{this.state.namespaces.map(namespace =>
|
||||||
|
<DatabaseListItem
|
||||||
|
isActive={this.isActive(this.props.query, namespace)}
|
||||||
|
namespace={namespace}
|
||||||
|
onChooseNamespace={this.handleChooseNamespace}
|
||||||
|
key={namespace.database + namespace.retentionPolicy}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FancyScrollbar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DatabaseList
|
|
@ -0,0 +1,26 @@
|
||||||
|
import React, {SFC} from 'react'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import {Namespace} from 'src/types/query'
|
||||||
|
|
||||||
|
export interface DatabaseListItemProps {
|
||||||
|
isActive: boolean
|
||||||
|
namespace: Namespace
|
||||||
|
onChooseNamespace: (namespace: Namespace) => () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const DatabaseListItem: SFC<DatabaseListItemProps> = ({
|
||||||
|
isActive,
|
||||||
|
namespace,
|
||||||
|
namespace: {database, retentionPolicy},
|
||||||
|
onChooseNamespace,
|
||||||
|
}) =>
|
||||||
|
<div
|
||||||
|
className={classnames('query-builder--list-item', {
|
||||||
|
active: isActive,
|
||||||
|
})}
|
||||||
|
onClick={onChooseNamespace(namespace)}
|
||||||
|
>
|
||||||
|
{database}.{retentionPolicy}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
export default DatabaseListItem
|
|
@ -46,9 +46,18 @@ class InputClickToEdit extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {isEditing, value} = this.state
|
const {isEditing, value} = this.state
|
||||||
const {wrapperClass, disabled, tabIndex, placeholder} = this.props
|
const {
|
||||||
|
wrapperClass: wrapper,
|
||||||
|
disabled,
|
||||||
|
tabIndex,
|
||||||
|
placeholder,
|
||||||
|
appearAsNormalInput,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
const divStyle = value ? 'input-cte' : 'input-cte__empty'
|
const wrapperClass = `${wrapper}${appearAsNormalInput
|
||||||
|
? ' input-cte__normal'
|
||||||
|
: ''}`
|
||||||
|
const defaultStyle = value ? 'input-cte' : 'input-cte__empty'
|
||||||
|
|
||||||
return disabled
|
return disabled
|
||||||
? <div className={wrapperClass}>
|
? <div className={wrapperClass}>
|
||||||
|
@ -68,16 +77,16 @@ class InputClickToEdit extends Component {
|
||||||
onFocus={this.handleFocus}
|
onFocus={this.handleFocus}
|
||||||
ref={r => (this.inputRef = r)}
|
ref={r => (this.inputRef = r)}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
placeholder={placeholder}
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
: <div
|
: <div
|
||||||
className={divStyle}
|
className={defaultStyle}
|
||||||
onClick={this.handleInputClick}
|
onClick={this.handleInputClick}
|
||||||
onFocus={this.handleInputClick}
|
onFocus={this.handleInputClick}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
>
|
>
|
||||||
{value || placeholder}
|
{value || placeholder}
|
||||||
<span className="icon pencil" />
|
{appearAsNormalInput || <span className="icon pencil" />}
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -92,6 +101,7 @@ InputClickToEdit.propTypes = {
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
tabIndex: number,
|
tabIndex: number,
|
||||||
placeholder: string,
|
placeholder: string,
|
||||||
|
appearAsNormalInput: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default InputClickToEdit
|
export default InputClickToEdit
|
||||||
|
|
|
@ -1,219 +0,0 @@
|
||||||
import React, {PropTypes} from 'react'
|
|
||||||
import classnames from 'classnames'
|
|
||||||
import _ from 'lodash'
|
|
||||||
|
|
||||||
import {showMeasurements} from 'shared/apis/metaQuery'
|
|
||||||
import showMeasurementsParser from 'shared/parsing/showMeasurements'
|
|
||||||
|
|
||||||
import TagList from 'src/data_explorer/components/TagList'
|
|
||||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
|
||||||
|
|
||||||
const {func, shape, string} = PropTypes
|
|
||||||
|
|
||||||
const MeasurementList = React.createClass({
|
|
||||||
propTypes: {
|
|
||||||
query: shape({
|
|
||||||
database: string,
|
|
||||||
measurement: string,
|
|
||||||
}).isRequired,
|
|
||||||
onChooseMeasurement: func.isRequired,
|
|
||||||
onChooseTag: func.isRequired,
|
|
||||||
onToggleTagAcceptance: func.isRequired,
|
|
||||||
onGroupByTag: func.isRequired,
|
|
||||||
querySource: shape({
|
|
||||||
links: shape({
|
|
||||||
proxy: string.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
|
|
||||||
contextTypes: {
|
|
||||||
source: shape({
|
|
||||||
links: shape({
|
|
||||||
proxy: string.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState() {
|
|
||||||
return {
|
|
||||||
measurements: [],
|
|
||||||
filterText: '',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getDefaultProps() {
|
|
||||||
return {
|
|
||||||
querySource: null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
if (!this.props.query.database) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this._getMeasurements()
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {query, querySource} = this.props
|
|
||||||
|
|
||||||
if (!query.database) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
prevProps.query.database === query.database &&
|
|
||||||
_.isEqual(prevProps.querySource, querySource)
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this._getMeasurements()
|
|
||||||
},
|
|
||||||
|
|
||||||
handleFilterText(e) {
|
|
||||||
e.stopPropagation()
|
|
||||||
this.setState({
|
|
||||||
filterText: this.refs.filterText.value,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
handleEscape(e) {
|
|
||||||
if (e.key !== 'Escape') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
e.stopPropagation()
|
|
||||||
this.setState({
|
|
||||||
filterText: '',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
handleAcceptReject(e) {
|
|
||||||
e.stopPropagation()
|
|
||||||
this.props.onToggleTagAcceptance()
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="query-builder--column">
|
|
||||||
<div className="query-builder--heading">
|
|
||||||
<span>Measurements & Tags</span>
|
|
||||||
{this.props.query.database
|
|
||||||
? <div className="query-builder--filter">
|
|
||||||
<input
|
|
||||||
className="form-control input-sm"
|
|
||||||
ref="filterText"
|
|
||||||
placeholder="Filter"
|
|
||||||
type="text"
|
|
||||||
value={this.state.filterText}
|
|
||||||
onChange={this.handleFilterText}
|
|
||||||
onKeyUp={this.handleEscape}
|
|
||||||
spellCheck={false}
|
|
||||||
autoComplete={false}
|
|
||||||
/>
|
|
||||||
<span className="icon search" />
|
|
||||||
</div>
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
{this.renderList()}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
renderList() {
|
|
||||||
if (!this.props.query.database) {
|
|
||||||
return (
|
|
||||||
<div className="query-builder--list-empty">
|
|
||||||
<span>
|
|
||||||
No <strong>Database</strong> selected
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const filterText = this.state.filterText.toLowerCase()
|
|
||||||
const measurements = this.state.measurements.filter(m =>
|
|
||||||
m.toLowerCase().includes(filterText)
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="query-builder--list">
|
|
||||||
<FancyScrollbar>
|
|
||||||
{measurements.map(measurement => {
|
|
||||||
const isActive = measurement === this.props.query.measurement
|
|
||||||
const numTagsActive = Object.keys(this.props.query.tags).length
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={measurement}
|
|
||||||
onClick={
|
|
||||||
isActive
|
|
||||||
? () => {}
|
|
||||||
: () => this.props.onChooseMeasurement(measurement)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={classnames('query-builder--list-item', {
|
|
||||||
active: isActive,
|
|
||||||
})}
|
|
||||||
data-test={`query-builder-list-item-measurement-${measurement}`}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
<div className="query-builder--caret icon caret-right" />
|
|
||||||
{measurement}
|
|
||||||
</span>
|
|
||||||
{isActive && numTagsActive >= 1
|
|
||||||
? <div
|
|
||||||
className={classnames('flip-toggle', {
|
|
||||||
flipped: this.props.query.areTagsAccepted,
|
|
||||||
})}
|
|
||||||
onClick={this.handleAcceptReject}
|
|
||||||
>
|
|
||||||
<div className="flip-toggle--container">
|
|
||||||
<div className="flip-toggle--front">!=</div>
|
|
||||||
<div className="flip-toggle--back">=</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
{isActive
|
|
||||||
? <TagList
|
|
||||||
query={this.props.query}
|
|
||||||
querySource={this.props.querySource}
|
|
||||||
onChooseTag={this.props.onChooseTag}
|
|
||||||
onGroupByTag={this.props.onGroupByTag}
|
|
||||||
/>
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</FancyScrollbar>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
_getMeasurements() {
|
|
||||||
const {source} = this.context
|
|
||||||
const {querySource} = this.props
|
|
||||||
|
|
||||||
const proxy =
|
|
||||||
_.get(querySource, ['links', 'proxy'], null) || source.links.proxy
|
|
||||||
|
|
||||||
showMeasurements(proxy, this.props.query.database).then(resp => {
|
|
||||||
const {errors, measurementSets} = showMeasurementsParser(resp.data)
|
|
||||||
if (errors.length) {
|
|
||||||
// TODO: display errors in the UI.
|
|
||||||
return console.error('InfluxDB returned error(s): ', errors) // eslint-disable-line no-console
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
measurements: measurementSets[0].measurements,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export default MeasurementList
|
|
|
@ -0,0 +1,180 @@
|
||||||
|
import React, {PureComponent} from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
import {showMeasurements} from 'src/shared/apis/metaQuery'
|
||||||
|
import showMeasurementsParser from 'src/shared/parsing/showMeasurements'
|
||||||
|
|
||||||
|
import {Query, Source} from 'src/types'
|
||||||
|
|
||||||
|
import MeasurementListFilter from 'src/shared/components/MeasurementListFilter'
|
||||||
|
import MeasurementListItem from 'src/shared/components/MeasurementListItem'
|
||||||
|
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
query: Query
|
||||||
|
querySource: Source
|
||||||
|
onChooseTag: () => void
|
||||||
|
onGroupByTag: () => void
|
||||||
|
onToggleTagAcceptance: () => void
|
||||||
|
onChooseMeasurement: (measurement: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
measurements: string[]
|
||||||
|
filterText: string
|
||||||
|
filtered: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const {shape, string} = PropTypes
|
||||||
|
|
||||||
|
class MeasurementList extends PureComponent<Props, State> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
filtered: [],
|
||||||
|
measurements: [],
|
||||||
|
filterText: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handleEscape = this.handleEscape.bind(this)
|
||||||
|
this.handleFilterText = this.handleFilterText.bind(this)
|
||||||
|
this.handleAcceptReject = this.handleAcceptReject.bind(this)
|
||||||
|
this.handleFilterMeasuremet = this.handleFilterMeasuremet.bind(this)
|
||||||
|
this.handleChoosemeasurement = this.handleChoosemeasurement.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static defaultProps: Partial<Props> = {
|
||||||
|
querySource: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
public static contextTypes = {
|
||||||
|
source: shape({
|
||||||
|
links: shape({
|
||||||
|
proxy: string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (!this.props.query.database) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getMeasurements()
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {query, querySource} = this.props
|
||||||
|
|
||||||
|
if (!query.database) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
prevProps.query.database === query.database &&
|
||||||
|
_.isEqual(prevProps.querySource, querySource)
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getMeasurements()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFilterText(e) {
|
||||||
|
e.stopPropagation()
|
||||||
|
const filterText = e.target.value
|
||||||
|
this.setState({
|
||||||
|
filterText,
|
||||||
|
filtered: this.handleFilterMeasuremet(filterText),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFilterMeasuremet(filter) {
|
||||||
|
return this.state.measurements.filter(m =>
|
||||||
|
m.toLowerCase().includes(filter.toLowerCase())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEscape(e) {
|
||||||
|
if (e.key !== 'Escape') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.stopPropagation()
|
||||||
|
this.setState({
|
||||||
|
filterText: '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAcceptReject() {
|
||||||
|
this.props.onToggleTagAcceptance()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChoosemeasurement(measurement) {
|
||||||
|
return () => this.props.onChooseMeasurement(measurement)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {query, querySource, onChooseTag, onGroupByTag} = this.props
|
||||||
|
const {database, areTagsAccepted} = query
|
||||||
|
const {filtered} = this.state
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="query-builder--column">
|
||||||
|
<div className="query-builder--heading">
|
||||||
|
<span>Measurements & Tags</span>
|
||||||
|
{database &&
|
||||||
|
<MeasurementListFilter
|
||||||
|
onEscape={this.handleEscape}
|
||||||
|
onFilterText={this.handleFilterText}
|
||||||
|
filterText={this.state.filterText}
|
||||||
|
/>}
|
||||||
|
</div>
|
||||||
|
{database
|
||||||
|
? <div className="query-builder--list">
|
||||||
|
<FancyScrollbar>
|
||||||
|
{filtered.map(measurement =>
|
||||||
|
<MeasurementListItem
|
||||||
|
query={query}
|
||||||
|
key={measurement}
|
||||||
|
measurement={measurement}
|
||||||
|
querySource={querySource}
|
||||||
|
onChooseTag={onChooseTag}
|
||||||
|
onGroupByTag={onGroupByTag}
|
||||||
|
areTagsAccepted={areTagsAccepted}
|
||||||
|
onAcceptReject={this.handleAcceptReject}
|
||||||
|
isActive={measurement === query.measurement}
|
||||||
|
numTagsActive={Object.keys(query.tags).length}
|
||||||
|
onChooseMeasurement={this.handleChoosemeasurement}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FancyScrollbar>
|
||||||
|
</div>
|
||||||
|
: <div className="query-builder--list-empty">
|
||||||
|
<span>
|
||||||
|
No <strong>Database</strong> selected
|
||||||
|
</span>
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMeasurements() {
|
||||||
|
const {source} = this.context
|
||||||
|
const {querySource, query} = this.props
|
||||||
|
|
||||||
|
const proxy = _.get(querySource, ['links', 'proxy'], source.links.proxy)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {data} = await showMeasurements(proxy, query.database)
|
||||||
|
const {measurementSets} = showMeasurementsParser(data)
|
||||||
|
const measurements = measurementSets[0].measurements
|
||||||
|
this.setState({measurements, filtered: measurements})
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MeasurementList
|
|
@ -0,0 +1,28 @@
|
||||||
|
import React, {SFC} from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
filterText: string
|
||||||
|
onEscape: (e: React.KeyboardEvent<HTMLInputElement>) => void
|
||||||
|
onFilterText: (e: React.InputHTMLAttributes<HTMLInputElement>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const MeasurementListFilter: SFC<Props> = ({
|
||||||
|
onEscape,
|
||||||
|
onFilterText,
|
||||||
|
filterText,
|
||||||
|
}) =>
|
||||||
|
<div className="query-builder--filter">
|
||||||
|
<input
|
||||||
|
className="form-control input-sm"
|
||||||
|
placeholder="Filter"
|
||||||
|
type="text"
|
||||||
|
value={filterText}
|
||||||
|
onChange={onFilterText}
|
||||||
|
onKeyUp={onEscape}
|
||||||
|
spellCheck={false}
|
||||||
|
autoComplete="false"
|
||||||
|
/>
|
||||||
|
<span className="icon search" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
export default MeasurementListFilter
|
|
@ -0,0 +1,64 @@
|
||||||
|
import React, {SFC} from 'react'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import TagList from 'src/shared/components/TagList'
|
||||||
|
|
||||||
|
import {Query, Source} from 'src/types'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
query: Query
|
||||||
|
querySource: Source
|
||||||
|
isActive: boolean
|
||||||
|
measurement: string
|
||||||
|
numTagsActive: number
|
||||||
|
areTagsAccepted: boolean
|
||||||
|
onChooseTag: () => void
|
||||||
|
onGroupByTag: () => void
|
||||||
|
onAcceptReject: () => void
|
||||||
|
onChooseMeasurement: (measurement: string) => () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const noop = () => {}
|
||||||
|
|
||||||
|
const MeasurementListItem: SFC<Props> = ({
|
||||||
|
query,
|
||||||
|
isActive,
|
||||||
|
querySource,
|
||||||
|
measurement,
|
||||||
|
onChooseTag,
|
||||||
|
onGroupByTag,
|
||||||
|
numTagsActive,
|
||||||
|
onAcceptReject,
|
||||||
|
areTagsAccepted,
|
||||||
|
onChooseMeasurement,
|
||||||
|
}) =>
|
||||||
|
<div
|
||||||
|
key={measurement}
|
||||||
|
onClick={isActive ? noop : onChooseMeasurement(measurement)}
|
||||||
|
>
|
||||||
|
<div className={classnames('query-builder--list-item', {active: isActive})}>
|
||||||
|
<span>
|
||||||
|
<div className="query-builder--caret icon caret-right" />
|
||||||
|
{measurement}
|
||||||
|
</span>
|
||||||
|
{isActive &&
|
||||||
|
numTagsActive >= 1 &&
|
||||||
|
<div
|
||||||
|
className={classnames('flip-toggle', {flipped: areTagsAccepted})}
|
||||||
|
onClick={onAcceptReject}
|
||||||
|
>
|
||||||
|
<div className="flip-toggle--container">
|
||||||
|
<div className="flip-toggle--front">!=</div>
|
||||||
|
<div className="flip-toggle--back">=</div>
|
||||||
|
</div>
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
|
{isActive &&
|
||||||
|
<TagList
|
||||||
|
query={query}
|
||||||
|
querySource={querySource}
|
||||||
|
onChooseTag={onChooseTag}
|
||||||
|
onGroupByTag={onGroupByTag}
|
||||||
|
/>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
export default MeasurementListItem
|
|
@ -1,6 +1,7 @@
|
||||||
import React, {Component} from 'react'
|
import React, {Component} from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
import classnames from 'classnames'
|
||||||
import {timeSeriesToTable} from 'src/utils/timeSeriesToDygraph'
|
import {timeSeriesToTable} from 'src/utils/timeSeriesToDygraph'
|
||||||
import {MultiGrid} from 'react-virtualized'
|
import {MultiGrid} from 'react-virtualized'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
|
@ -22,11 +23,27 @@ class TableGraph extends Component {
|
||||||
|
|
||||||
cellRenderer = ({columnIndex, key, rowIndex, style}) => {
|
cellRenderer = ({columnIndex, key, rowIndex, style}) => {
|
||||||
const data = this._data
|
const data = this._data
|
||||||
|
const columnCount = _.get(data, ['0', 'length'], 0)
|
||||||
|
const rowCount = data.length
|
||||||
const {timeFormat} = this.state
|
const {timeFormat} = this.state
|
||||||
const isTimeCell = columnIndex === 0 && rowIndex > 0
|
const isTimeCell = columnIndex === 0 && rowIndex > 0
|
||||||
|
|
||||||
|
const isFixedRow = rowIndex === 0 && columnIndex > 0
|
||||||
|
const isFixedColumn = rowIndex > 0 && columnIndex === 0
|
||||||
|
const isFixedCorner = rowIndex === 0 && columnIndex === 0
|
||||||
|
const isLastRow = rowIndex === rowCount - 1
|
||||||
|
const isLastColumn = columnIndex === columnCount - 1
|
||||||
|
|
||||||
|
const cellClass = classnames('table-graph-cell', {
|
||||||
|
'table-graph-cell__fixed-row': isFixedRow,
|
||||||
|
'table-graph-cell__fixed-column': isFixedColumn,
|
||||||
|
'table-graph-cell__fixed-corner': isFixedCorner,
|
||||||
|
'table-graph-cell__last-row': isLastRow,
|
||||||
|
'table-graph-cell__last-column': isLastColumn,
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={key} style={style}>
|
<div key={key} className={cellClass} style={style}>
|
||||||
{isTimeCell
|
{isTimeCell
|
||||||
? moment(data[rowIndex][columnIndex]).format(timeFormat)
|
? moment(data[rowIndex][columnIndex]).format(timeFormat)
|
||||||
: data[rowIndex][columnIndex]}
|
: data[rowIndex][columnIndex]}
|
||||||
|
@ -39,13 +56,13 @@ class TableGraph extends Component {
|
||||||
const columnCount = _.get(data, ['0', 'length'], 0)
|
const columnCount = _.get(data, ['0', 'length'], 0)
|
||||||
const rowCount = data.length
|
const rowCount = data.length
|
||||||
const COLUMN_WIDTH = 300
|
const COLUMN_WIDTH = 300
|
||||||
const ROW_HEIGHT = 50
|
const ROW_HEIGHT = 30
|
||||||
const tableWidth = this.gridContainer ? this.gridContainer.clientWidth : 0
|
const tableWidth = this.gridContainer ? this.gridContainer.clientWidth : 0
|
||||||
const tableHeight = this.gridContainer ? this.gridContainer.clientHeight : 0
|
const tableHeight = this.gridContainer ? this.gridContainer.clientHeight : 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="graph-container"
|
className="table-graph-container"
|
||||||
ref={gridContainer => (this.gridContainer = gridContainer)}
|
ref={gridContainer => (this.gridContainer = gridContainer)}
|
||||||
>
|
>
|
||||||
{data.length > 1 &&
|
{data.length > 1 &&
|
||||||
|
@ -58,8 +75,9 @@ class TableGraph extends Component {
|
||||||
height={tableHeight}
|
height={tableHeight}
|
||||||
rowCount={rowCount}
|
rowCount={rowCount}
|
||||||
rowHeight={ROW_HEIGHT}
|
rowHeight={ROW_HEIGHT}
|
||||||
width={tableWidth - 32}
|
width={tableWidth}
|
||||||
onScroll={this.handleScroll}
|
enableFixedColumnScroll={true}
|
||||||
|
enableFixedRowScroll={true}
|
||||||
/>}
|
/>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
import React, {PureComponent} from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
import TagListItem from 'src/shared/components/TagListItem'
|
||||||
|
|
||||||
|
import {Query, Source} from 'src/types'
|
||||||
|
|
||||||
|
import {showTagKeys, showTagValues} from 'src/shared/apis/metaQuery'
|
||||||
|
import showTagKeysParser from 'src/shared/parsing/showTagKeys'
|
||||||
|
import showTagValuesParser from 'src/shared/parsing/showTagValues'
|
||||||
|
|
||||||
|
const {string, shape} = PropTypes
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
query: Query
|
||||||
|
querySource: Source
|
||||||
|
onChooseTag: () => void
|
||||||
|
onGroupByTag: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
tags: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TagList extends PureComponent<Props, State> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
tags: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static contextTypes = {
|
||||||
|
source: shape({
|
||||||
|
links: shape({
|
||||||
|
proxy: string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
public static defaultProps = {
|
||||||
|
querySource: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const {database, measurement, retentionPolicy} = this.props.query
|
||||||
|
if (!database || !measurement || !retentionPolicy) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getTags()
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {query, querySource} = this.props
|
||||||
|
const {database, measurement, retentionPolicy} = query
|
||||||
|
|
||||||
|
const {
|
||||||
|
database: prevDB,
|
||||||
|
measurement: prevMeas,
|
||||||
|
retentionPolicy: prevRP,
|
||||||
|
} = prevProps.query
|
||||||
|
|
||||||
|
if (!database || !measurement || !retentionPolicy) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
database === prevDB &&
|
||||||
|
measurement === prevMeas &&
|
||||||
|
retentionPolicy === prevRP &&
|
||||||
|
_.isEqual(prevProps.querySource, querySource)
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getTags()
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTags() {
|
||||||
|
const {source} = this.context
|
||||||
|
const {querySource} = this.props
|
||||||
|
const {database, measurement, retentionPolicy} = this.props.query
|
||||||
|
|
||||||
|
const proxy = _.get(querySource, ['links', 'proxy'], source.links.proxy)
|
||||||
|
|
||||||
|
const {data} = await showTagKeys({
|
||||||
|
source: proxy,
|
||||||
|
database,
|
||||||
|
retentionPolicy,
|
||||||
|
measurement,
|
||||||
|
})
|
||||||
|
const {tagKeys} = showTagKeysParser(data)
|
||||||
|
|
||||||
|
const response = await showTagValues({
|
||||||
|
source: proxy,
|
||||||
|
database,
|
||||||
|
retentionPolicy,
|
||||||
|
measurement,
|
||||||
|
tagKeys,
|
||||||
|
})
|
||||||
|
|
||||||
|
const {tags} = showTagValuesParser(response.data)
|
||||||
|
|
||||||
|
this.setState({tags})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {query, onChooseTag, onGroupByTag} = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="query-builder--sub-list">
|
||||||
|
{_.map(this.state.tags, (tagValues: string[], tagKey: string) =>
|
||||||
|
<TagListItem
|
||||||
|
key={tagKey}
|
||||||
|
tagKey={tagKey}
|
||||||
|
tagValues={tagValues}
|
||||||
|
onChooseTag={onChooseTag}
|
||||||
|
onGroupByTag={onGroupByTag}
|
||||||
|
selectedTagValues={query.tags[tagKey] || []}
|
||||||
|
isUsingGroupBy={query.groupBy.tags.indexOf(tagKey) > -1}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TagList
|
|
@ -1,39 +1,50 @@
|
||||||
import React, {PropTypes} from 'react'
|
import React, {PureComponent} from 'react'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
|
|
||||||
const {string, arrayOf, func, bool} = PropTypes
|
interface Props {
|
||||||
const TagListItem = React.createClass({
|
tagKey: string
|
||||||
propTypes: {
|
tagValues: string[]
|
||||||
tagKey: string.isRequired,
|
selectedTagValues: string[]
|
||||||
tagValues: arrayOf(string.isRequired).isRequired,
|
isUsingGroupBy?: boolean
|
||||||
selectedTagValues: arrayOf(string.isRequired).isRequired,
|
onChooseTag: ({key: string, value}) => void
|
||||||
isUsingGroupBy: bool,
|
onGroupByTag: (tagKey: string) => void
|
||||||
onChooseTag: func.isRequired,
|
}
|
||||||
onGroupByTag: func.isRequired,
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState() {
|
interface State {
|
||||||
return {
|
isOpen: boolean
|
||||||
|
filterText: string
|
||||||
|
}
|
||||||
|
|
||||||
|
class TagListItem extends PureComponent<Props, State> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
filterText: '',
|
filterText: '',
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
handleChoose(tagValue) {
|
this.handleEscape = this.handleEscape.bind(this)
|
||||||
|
this.handleChoose = this.handleChoose.bind(this)
|
||||||
|
this.handleGroupBy = this.handleGroupBy.bind(this)
|
||||||
|
this.handleClickKey = this.handleClickKey.bind(this)
|
||||||
|
this.handleFilterText = this.handleFilterText.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChoose(tagValue: string) {
|
||||||
this.props.onChooseTag({key: this.props.tagKey, value: tagValue})
|
this.props.onChooseTag({key: this.props.tagKey, value: tagValue})
|
||||||
},
|
}
|
||||||
|
|
||||||
handleClickKey() {
|
handleClickKey() {
|
||||||
this.setState({isOpen: !this.state.isOpen})
|
this.setState({isOpen: !this.state.isOpen})
|
||||||
},
|
}
|
||||||
|
|
||||||
handleFilterText(e) {
|
handleFilterText(e) {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
this.setState({
|
this.setState({
|
||||||
filterText: this.refs.filterText.value,
|
filterText: e.target.value,
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
|
|
||||||
handleEscape(e) {
|
handleEscape(e) {
|
||||||
if (e.key !== 'Escape') {
|
if (e.key !== 'Escape') {
|
||||||
|
@ -44,7 +55,12 @@ const TagListItem = React.createClass({
|
||||||
this.setState({
|
this.setState({
|
||||||
filterText: '',
|
filterText: '',
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
|
|
||||||
|
handleGroupBy(e) {
|
||||||
|
e.stopPropagation()
|
||||||
|
this.props.onGroupByTag(this.props.tagKey)
|
||||||
|
}
|
||||||
|
|
||||||
renderTagValues() {
|
renderTagValues() {
|
||||||
const {tagValues, selectedTagValues} = this.props
|
const {tagValues, selectedTagValues} = this.props
|
||||||
|
@ -67,7 +83,7 @@ const TagListItem = React.createClass({
|
||||||
onChange={this.handleFilterText}
|
onChange={this.handleFilterText}
|
||||||
onKeyUp={this.handleEscape}
|
onKeyUp={this.handleEscape}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
autoComplete={false}
|
autoComplete="false"
|
||||||
/>
|
/>
|
||||||
<span className="icon search" />
|
<span className="icon search" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -91,12 +107,7 @@ const TagListItem = React.createClass({
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
|
|
||||||
handleGroupBy(e) {
|
|
||||||
e.stopPropagation()
|
|
||||||
this.props.onGroupByTag(this.props.tagKey)
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {tagKey, tagValues, isUsingGroupBy} = this.props
|
const {tagKey, tagValues, isUsingGroupBy} = this.props
|
||||||
|
@ -127,7 +138,7 @@ const TagListItem = React.createClass({
|
||||||
{isOpen ? this.renderTagValues() : null}
|
{isOpen ? this.renderTagValues() : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
export default TagListItem
|
export default TagListItem
|
|
@ -67,6 +67,7 @@
|
||||||
@import 'components/info-indicators';
|
@import 'components/info-indicators';
|
||||||
@import 'components/source-selector';
|
@import 'components/source-selector';
|
||||||
@import 'components/tables';
|
@import 'components/tables';
|
||||||
|
@import 'components/table-graph';
|
||||||
@import 'components/kapacitor-logs-table';
|
@import 'components/kapacitor-logs-table';
|
||||||
|
|
||||||
// Pages
|
// Pages
|
||||||
|
|
|
@ -206,46 +206,50 @@ $graph-type--gutter: 4px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gauge-controls--section {
|
.gauge-controls--section,
|
||||||
|
.column-controls--section {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
margin-left: 4px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
button.btn.btn-primary.btn-sm.gauge-controls--add-threshold {
|
button.btn.btn-primary.btn-sm.gauge-controls--add-threshold {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gauge-controls--label {
|
%gauge-controls-label-styles {
|
||||||
height: 30px;
|
height: 30px;
|
||||||
background-color: $g4-onyx;
|
line-height: 30px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: $g11-sidewalk;
|
font-size: 13px;
|
||||||
padding: 0 11px;
|
padding: 0 11px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
line-height: 30px;
|
|
||||||
@include no-user-select();
|
@include no-user-select();
|
||||||
|
}
|
||||||
|
.gauge-controls--label {
|
||||||
|
@extend %gauge-controls-label-styles;
|
||||||
|
color: $g11-sidewalk;
|
||||||
|
background-color: $g4-onyx;
|
||||||
width: 120px;
|
width: 120px;
|
||||||
}
|
}
|
||||||
.gauge-controls--label-editable {
|
.gauge-controls--label-editable {
|
||||||
height: 30px;
|
@extend %gauge-controls-label-styles;
|
||||||
font-weight: 600;
|
|
||||||
color: $g16-pearl;
|
color: $g16-pearl;
|
||||||
padding: 0 11px;
|
|
||||||
border-radius: 4px;
|
|
||||||
line-height: 30px;
|
|
||||||
@include no-user-select();
|
|
||||||
width: 90px;
|
width: 90px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gauge-controls--input {
|
.gauge-controls--input {
|
||||||
flex: 1 0 0;
|
flex: 1 0 0;
|
||||||
margin: 0 0 0 4px;
|
|
||||||
}
|
|
||||||
.gauge-controls--section .color-dropdown {
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
}
|
||||||
.gauge-controls--section .color-dropdown.color-dropdown--stretch {
|
.gauge-controls--section .color-dropdown.color-dropdown--stretch {
|
||||||
width: auto;
|
width: auto;
|
||||||
|
@ -253,17 +257,27 @@ button.btn.btn-primary.btn-sm.gauge-controls--add-threshold {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.column-controls--label {
|
||||||
|
@extend %gauge-controls-label-styles;
|
||||||
|
color: $g16-pearl;
|
||||||
|
background-color: $g4-onyx;
|
||||||
|
flex: 2 0 0;
|
||||||
|
}
|
||||||
|
.column-controls-input {
|
||||||
|
flex: 1 0 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
/*
|
/*
|
||||||
Cell Editor Overlay - Single-Stat Controls
|
Cell Editor Overlay - Single-Stat Controls
|
||||||
------------------------------------------------------------------------------
|
------------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
.single-stat-controls {
|
.graph-options-group {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
.form-group-wrapper {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: calc(100% + 12px);
|
width: calc(100% + 12px);
|
||||||
margin: 30px -6px 0 -6px;
|
margin-left: -6px;
|
||||||
|
margin-right: -6px;
|
||||||
> div.form-group {
|
|
||||||
padding-left: 6px;
|
|
||||||
padding-right: 6px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-cte {
|
.input-cte {
|
||||||
|
@ -55,10 +56,34 @@
|
||||||
|
|
||||||
.input-cte__empty {
|
.input-cte__empty {
|
||||||
@extend .input-cte;
|
@extend .input-cte;
|
||||||
font-style: italic;
|
|
||||||
color: $g9-mountain;
|
color: $g9-mountain;
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: italic;
|
||||||
|
line-height: 27px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $g9-mountain;
|
color: $g9-mountain;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Appear as Normal Input
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
.input-cte__normal {
|
||||||
|
.input-cte {
|
||||||
|
border-color: $g5-pepper;
|
||||||
|
}
|
||||||
|
.input-cte:hover {
|
||||||
|
border-color: $g6-smoke;
|
||||||
|
background-color: $g2-kevlar;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-cte__empty {
|
||||||
|
background-color: $g2-kevlar;
|
||||||
|
}
|
||||||
|
.input-cte__disabled,
|
||||||
|
.input-cte__disabled:hover {
|
||||||
|
border-color: $g5-pepper;
|
||||||
|
background-color: $g3-castle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
Table Type Graphs in Dashboards
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
.table-graph-container {
|
||||||
|
position: absolute;
|
||||||
|
width: calc(100% - 32px);
|
||||||
|
height: calc(100% - 16px);
|
||||||
|
top: 8px;
|
||||||
|
left: 16px;
|
||||||
|
border: 2px solid $g5-pepper;
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-graph-cell {
|
||||||
|
line-height: 30px;
|
||||||
|
padding: 0 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: $g13-mist;
|
||||||
|
border: 1px solid $g5-pepper;
|
||||||
|
|
||||||
|
&__fixed-row,
|
||||||
|
&__fixed-column {
|
||||||
|
font-weight: 700;
|
||||||
|
color: $g16-pearl;
|
||||||
|
background-color: $g4-onyx;
|
||||||
|
}
|
||||||
|
&__fixed-row {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
&__fixed-column {
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
&__fixed-corner {
|
||||||
|
border-top: 0;
|
||||||
|
border-left: 0;
|
||||||
|
color: $g18-cloud;
|
||||||
|
background-color: $g5-pepper;
|
||||||
|
}
|
||||||
|
&__last-row {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
&__last-column {
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,10 +8,14 @@
|
||||||
border: 2px solid $g5-pepper;
|
border: 2px solid $g5-pepper;
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
color: $g15-platinum;
|
color: $g15-platinum;
|
||||||
|
letter-spacing: 0px;
|
||||||
background-color: $g2-kevlar;
|
background-color: $g2-kevlar;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
transition:
|
transition:
|
||||||
color 0.25s ease,
|
color 0.25s ease,
|
||||||
background-color 0.25s ease,
|
background-color 0.25s ease,
|
||||||
|
@ -188,7 +192,8 @@ textarea.form-control {
|
||||||
.form-group > .btn {
|
.form-group > .btn {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
.form-group > label {
|
.form-group > label,
|
||||||
|
label.form-label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
import {Query} from './query'
|
||||||
|
import {Source} from './sources'
|
||||||
|
|
||||||
|
export {Query, Source}
|
|
@ -0,0 +1,59 @@
|
||||||
|
export interface Query {
|
||||||
|
id: QueryID
|
||||||
|
database: string
|
||||||
|
measurement: string
|
||||||
|
retentionPolicy: string
|
||||||
|
fields: Field[]
|
||||||
|
tags: Tags
|
||||||
|
groupBy: GroupBy
|
||||||
|
areTagsAccepted: boolean
|
||||||
|
rawText: string | null
|
||||||
|
range?: TimeRange | null
|
||||||
|
source?: string
|
||||||
|
fill: string
|
||||||
|
status?: Status
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Field {
|
||||||
|
value: string
|
||||||
|
type: string
|
||||||
|
alias?: string
|
||||||
|
args?: Args[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QueryID = string
|
||||||
|
|
||||||
|
export interface Args {
|
||||||
|
value: string
|
||||||
|
type: string
|
||||||
|
alias?: string
|
||||||
|
args?: Args[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TagValues = string[]
|
||||||
|
|
||||||
|
export interface Tags {
|
||||||
|
[key: string]: TagValues
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GroupBy {
|
||||||
|
time?: string
|
||||||
|
tags?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Namespace {
|
||||||
|
database: string
|
||||||
|
retentionPolicy: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Status {
|
||||||
|
loading?: string
|
||||||
|
error?: string
|
||||||
|
warn?: string
|
||||||
|
success?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimeRange {
|
||||||
|
lower: string
|
||||||
|
upper?: string
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
export interface Source {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
url: string
|
||||||
|
type: string
|
||||||
|
default: boolean
|
||||||
|
organization: string
|
||||||
|
insecureSkipVerify: boolean
|
||||||
|
role: string
|
||||||
|
telegraf: string
|
||||||
|
links: SourceLinks
|
||||||
|
kapacitors?: Kapacitor[]
|
||||||
|
metaUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SourceLinks {
|
||||||
|
self: string
|
||||||
|
kapacitors: string
|
||||||
|
proxy: string
|
||||||
|
queries: string
|
||||||
|
write: string
|
||||||
|
permissions: string
|
||||||
|
users: string
|
||||||
|
databases: string
|
||||||
|
roles?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Kapacitor {
|
||||||
|
id?: string
|
||||||
|
url: string
|
||||||
|
name: string
|
||||||
|
username?: string
|
||||||
|
password?: string
|
||||||
|
active: boolean
|
||||||
|
links: {
|
||||||
|
self: string
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,8 +17,6 @@ const labels = ['time', 'test.label']
|
||||||
const div = document.createElement('div')
|
const div = document.createElement('div')
|
||||||
const graph = new Dygraph(div, timeSeries, {labels})
|
const graph = new Dygraph(div, timeSeries, {labels})
|
||||||
|
|
||||||
const oneHourMs = '3600000'
|
|
||||||
|
|
||||||
const a1 = {
|
const a1 = {
|
||||||
group: '',
|
group: '',
|
||||||
name: 'a1',
|
name: 'a1',
|
||||||
|
@ -43,14 +41,14 @@ describe('Shared.Annotations.Helpers', () => {
|
||||||
const actual = visibleAnnotations(undefined, annotations)
|
const actual = visibleAnnotations(undefined, annotations)
|
||||||
const expected = []
|
const expected = []
|
||||||
|
|
||||||
expect(actual).to.deep.equal(expected)
|
expect(actual).toEqual(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns an annotation if it is in the time range', () => {
|
it('returns an annotation if it is in the time range', () => {
|
||||||
const actual = visibleAnnotations(graph, annotations)
|
const actual = visibleAnnotations(graph, annotations)
|
||||||
const expected = annotations
|
const expected = annotations
|
||||||
|
|
||||||
expect(actual).to.deep.equal(expected)
|
expect(actual).toEqual(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('removes an annotation if it is out of the time range', () => {
|
it('removes an annotation if it is out of the time range', () => {
|
||||||
|
@ -65,7 +63,7 @@ describe('Shared.Annotations.Helpers', () => {
|
||||||
const actual = visibleAnnotations(graph, newAnnos)
|
const actual = visibleAnnotations(graph, newAnnos)
|
||||||
const expected = annotations
|
const expected = annotations
|
||||||
|
|
||||||
expect(actual).to.deep.equal(expected)
|
expect(actual).toEqual(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('with a duration', () => {
|
describe('with a duration', () => {
|
||||||
|
@ -79,7 +77,7 @@ describe('Shared.Annotations.Helpers', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const expected = [...withDurations, expectedAnnotation]
|
const expected = [...withDurations, expectedAnnotation]
|
||||||
expect(actual).to.deep.equal(expected)
|
expect(actual).toEqual(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not add a duration annotation if it is out of bounds', () => {
|
it('does not add a duration annotation if it is out of bounds', () => {
|
||||||
|
@ -96,7 +94,7 @@ describe('Shared.Annotations.Helpers', () => {
|
||||||
const actual = visibleAnnotations(graph, withDurations)
|
const actual = visibleAnnotations(graph, withDurations)
|
||||||
const expected = withDurations
|
const expected = withDurations
|
||||||
|
|
||||||
expect(actual).to.deep.equal(expected)
|
expect(actual).toEqual(expected)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
|
@ -30,12 +30,12 @@ const state = {
|
||||||
annotations: [],
|
annotations: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
describe.only('Shared.Reducers.annotations', () => {
|
describe('Shared.Reducers.annotations', () => {
|
||||||
it('can load the annotations', () => {
|
it('can load the annotations', () => {
|
||||||
const expected = [{time: '0', duration: ''}]
|
const expected = [{time: '0', duration: ''}]
|
||||||
const actual = reducer(state, loadAnnotations(expected))
|
const actual = reducer(state, loadAnnotations(expected))
|
||||||
|
|
||||||
expect(actual.annotations).to.deep.equal(expected)
|
expect(actual.annotations).toEqual(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can update an annotation', () => {
|
it('can update an annotation', () => {
|
||||||
|
@ -45,7 +45,7 @@ describe.only('Shared.Reducers.annotations', () => {
|
||||||
updateAnnotation(expected[0])
|
updateAnnotation(expected[0])
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(actual.annotations).to.deep.equal(expected)
|
expect(actual.annotations).toEqual(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can delete an annotation', () => {
|
it('can delete an annotation', () => {
|
||||||
|
@ -55,13 +55,13 @@ describe.only('Shared.Reducers.annotations', () => {
|
||||||
deleteAnnotation(a1)
|
deleteAnnotation(a1)
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(actual.annotations).to.deep.equal(expected)
|
expect(actual.annotations).toEqual(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can add an annotation', () => {
|
it('can add an annotation', () => {
|
||||||
const expected = [a1]
|
const expected = [a1]
|
||||||
const actual = reducer(state, addAnnotation(a1))
|
const actual = reducer(state, addAnnotation(a1))
|
||||||
|
|
||||||
expect(actual.annotations).to.deep.equal(expected)
|
expect(actual.annotations).toEqual(expected)
|
||||||
})
|
})
|
||||||
})
|
})
|
|
@ -54,31 +54,31 @@ describe('Dashboards.Reducers.cellEditorOverlay', () => {
|
||||||
singleStatType: defaultSingleStatType,
|
singleStatType: defaultSingleStatType,
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(actual.cell).to.equal(expected.cell)
|
expect(actual.cell).toBe(expected.cell)
|
||||||
expect(actual.gaugeColors).to.equal(expected.gaugeColors)
|
expect(actual.gaugeColors).toBe(expected.gaugeColors)
|
||||||
expect(actual.singleStatColors).to.equal(expected.singleStatColors)
|
expect(actual.singleStatColors).toBe(expected.singleStatColors)
|
||||||
expect(actual.singleStatType).to.equal(expected.singleStatType)
|
expect(actual.singleStatType).toBe(expected.singleStatType)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should hide cell editor overlay', () => {
|
it('should hide cell editor overlay', () => {
|
||||||
const actual = reducer(initialState, hideCellEditorOverlay)
|
const actual = reducer(initialState, hideCellEditorOverlay)
|
||||||
const expected = null
|
const expected = null
|
||||||
|
|
||||||
expect(actual.cell).to.equal(expected)
|
expect(actual.cell).toBe(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should change the cell editor visualization type', () => {
|
it('should change the cell editor visualization type', () => {
|
||||||
const actual = reducer(initialState, changeCellType(defaultCellType))
|
const actual = reducer(initialState, changeCellType(defaultCellType))
|
||||||
const expected = defaultCellType
|
const expected = defaultCellType
|
||||||
|
|
||||||
expect(actual.cell.type).to.equal(expected)
|
expect(actual.cell.type).toBe(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should change the name of the cell', () => {
|
it('should change the name of the cell', () => {
|
||||||
const actual = reducer(initialState, renameCell(defaultCellName))
|
const actual = reducer(initialState, renameCell(defaultCellName))
|
||||||
const expected = defaultCellName
|
const expected = defaultCellName
|
||||||
|
|
||||||
expect(actual.cell.name).to.equal(expected)
|
expect(actual.cell.name).toBe(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should update the cell single stat colors', () => {
|
it('should update the cell single stat colors', () => {
|
||||||
|
@ -88,7 +88,7 @@ describe('Dashboards.Reducers.cellEditorOverlay', () => {
|
||||||
)
|
)
|
||||||
const expected = defaultSingleStatColors
|
const expected = defaultSingleStatColors
|
||||||
|
|
||||||
expect(actual.singleStatColors).to.equal(expected)
|
expect(actual.singleStatColors).toBe(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should toggle the single stat type', () => {
|
it('should toggle the single stat type', () => {
|
||||||
|
@ -98,20 +98,20 @@ describe('Dashboards.Reducers.cellEditorOverlay', () => {
|
||||||
)
|
)
|
||||||
const expected = defaultSingleStatType
|
const expected = defaultSingleStatType
|
||||||
|
|
||||||
expect(actual.singleStatType).to.equal(expected)
|
expect(actual.singleStatType).toBe(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should update the cell gauge colors', () => {
|
it('should update the cell gauge colors', () => {
|
||||||
const actual = reducer(initialState, updateGaugeColors(defaultGaugeColors))
|
const actual = reducer(initialState, updateGaugeColors(defaultGaugeColors))
|
||||||
const expected = defaultGaugeColors
|
const expected = defaultGaugeColors
|
||||||
|
|
||||||
expect(actual.gaugeColors).to.equal(expected)
|
expect(actual.gaugeColors).toBe(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should update the cell axes', () => {
|
it('should update the cell axes', () => {
|
||||||
const actual = reducer(initialState, updateAxes(defaultCellAxes))
|
const actual = reducer(initialState, updateAxes(defaultCellAxes))
|
||||||
const expected = defaultCellAxes
|
const expected = defaultCellAxes
|
||||||
|
|
||||||
expect(actual.cell.axes).to.equal(expected)
|
expect(actual.cell.axes).toBe(expected)
|
||||||
})
|
})
|
||||||
})
|
})
|
|
@ -1,3 +0,0 @@
|
||||||
const context = require.context('./', true, /Spec\.js$/)
|
|
||||||
context.keys().forEach(context)
|
|
||||||
module.exports = context
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
export const source = {
|
||||||
|
id: '16',
|
||||||
|
name: 'ssl',
|
||||||
|
type: 'influx',
|
||||||
|
username: 'admin',
|
||||||
|
url: 'https://localhost:9086',
|
||||||
|
insecureSkipVerify: true,
|
||||||
|
default: false,
|
||||||
|
telegraf: 'telegraf',
|
||||||
|
organization: '0',
|
||||||
|
role: 'viewer',
|
||||||
|
links: {
|
||||||
|
self: '/chronograf/v1/sources/16',
|
||||||
|
kapacitors: '/chronograf/v1/sources/16/kapacitors',
|
||||||
|
proxy: '/chronograf/v1/sources/16/proxy',
|
||||||
|
queries: '/chronograf/v1/sources/16/queries',
|
||||||
|
write: '/chronograf/v1/sources/16/write',
|
||||||
|
permissions: '/chronograf/v1/sources/16/permissions',
|
||||||
|
users: '/chronograf/v1/sources/16/users',
|
||||||
|
databases: '/chronograf/v1/sources/16/dbs',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const query = {
|
||||||
|
id: '0',
|
||||||
|
database: 'db1',
|
||||||
|
measurement: 'm1',
|
||||||
|
retentionPolicy: 'r1',
|
||||||
|
fill: 'null',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
value: 'f1',
|
||||||
|
type: 'field',
|
||||||
|
alias: 'foo',
|
||||||
|
args: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tags: {
|
||||||
|
tk1: ['tv1', 'tv2'],
|
||||||
|
},
|
||||||
|
groupBy: {
|
||||||
|
time: null,
|
||||||
|
tags: [],
|
||||||
|
},
|
||||||
|
areTagsAccepted: true,
|
||||||
|
rawText: null,
|
||||||
|
status: null,
|
||||||
|
shifts: [],
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
import React from 'react'
|
||||||
|
import ConfirmButtons, {
|
||||||
|
Confirm,
|
||||||
|
Cancel,
|
||||||
|
} from 'src/shared/components/ConfirmButtons'
|
||||||
|
|
||||||
|
import {shallow} from 'enzyme'
|
||||||
|
|
||||||
|
const setup = (override = {}) => {
|
||||||
|
const props = {
|
||||||
|
item: '',
|
||||||
|
buttonSize: '',
|
||||||
|
isDisabled: false,
|
||||||
|
confirmLeft: false,
|
||||||
|
confirmTitle: '',
|
||||||
|
onConfirm: () => {},
|
||||||
|
onCancel: () => {},
|
||||||
|
onClickOutside: () => {},
|
||||||
|
...override,
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapper = shallow(<ConfirmButtons {...props} />)
|
||||||
|
|
||||||
|
return {
|
||||||
|
props,
|
||||||
|
wrapper,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Componenets.Shared.ConfirmButtons', () => {
|
||||||
|
describe('rendering', () => {
|
||||||
|
it('has a confirm and cancel button', () => {
|
||||||
|
const {wrapper} = setup()
|
||||||
|
const confirm = wrapper.dive().find(Confirm)
|
||||||
|
const cancel = wrapper.dive().find(Cancel)
|
||||||
|
|
||||||
|
expect(confirm.exists()).toBe(true)
|
||||||
|
expect(cancel.exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('confirmLeft is true', () => {
|
||||||
|
it('has a confirm button to the left of the cancel button', () => {
|
||||||
|
const {wrapper} = setup({confirmLeft: true})
|
||||||
|
const buttons = wrapper.dive().children()
|
||||||
|
const firstButton = buttons.first()
|
||||||
|
const lastButton = buttons.last()
|
||||||
|
|
||||||
|
expect(firstButton.find(Confirm).exists()).toBe(true)
|
||||||
|
expect(lastButton.find(Cancel).exists()).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('confirmLeft is false', () => {
|
||||||
|
it('has a confirm button to the right of the cancel button', () => {
|
||||||
|
const {wrapper} = setup({confirmLeft: false})
|
||||||
|
const buttons = wrapper.dive().children()
|
||||||
|
const firstButton = buttons.first()
|
||||||
|
const lastButton = buttons.last()
|
||||||
|
|
||||||
|
expect(firstButton.find(Cancel).exists()).toBe(true)
|
||||||
|
expect(lastButton.find(Confirm).exists()).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('confirmTitle', () => {
|
||||||
|
describe('is not defined', () => {
|
||||||
|
it('has a default title', () => {
|
||||||
|
const {wrapper} = setup({confirmTitle: undefined})
|
||||||
|
const confirm = wrapper.dive().find(Confirm)
|
||||||
|
const title = confirm.prop('title')
|
||||||
|
|
||||||
|
expect(title.length).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('is defined', () => {
|
||||||
|
it('has the title passed in as props', () => {
|
||||||
|
const confirmTitle = 'delete'
|
||||||
|
const {wrapper} = setup({confirmTitle})
|
||||||
|
const confirm = wrapper.dive().find(Confirm)
|
||||||
|
const title = confirm.prop('title')
|
||||||
|
|
||||||
|
expect(title).toContain(confirmTitle)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('user interaction', () => {
|
||||||
|
describe('when clicking confirm', () => {
|
||||||
|
it('should fire onConfirm', () => {
|
||||||
|
const onConfirm = jest.fn()
|
||||||
|
const item = 'test-item'
|
||||||
|
const {wrapper} = setup({onConfirm, item})
|
||||||
|
const confirm = wrapper.dive().find(Confirm)
|
||||||
|
const button = confirm.dive().find({'data-test': 'confirm'})
|
||||||
|
button.simulate('click')
|
||||||
|
|
||||||
|
expect(onConfirm).toHaveBeenCalledTimes(1)
|
||||||
|
expect(onConfirm).toHaveBeenCalledWith(item)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when clicking cancel', () => {
|
||||||
|
it('should fire onCancel', () => {
|
||||||
|
const onCancel = jest.fn()
|
||||||
|
const item = 'test-item'
|
||||||
|
const {wrapper} = setup({onCancel, item})
|
||||||
|
const cancel = wrapper.dive().find(Cancel)
|
||||||
|
const button = cancel.dive().find({'data-test': 'cancel'})
|
||||||
|
button.simulate('click')
|
||||||
|
|
||||||
|
expect(onCancel).toHaveBeenCalledTimes(1)
|
||||||
|
expect(onCancel).toHaveBeenCalledWith(item)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,92 @@
|
||||||
|
import React from 'react'
|
||||||
|
import DatabaseList from 'src/shared/components/DatabaseList'
|
||||||
|
import DatabaseListItem from 'src/shared/components/DatabaseListItem'
|
||||||
|
import {shallow} from 'enzyme'
|
||||||
|
|
||||||
|
import {query, source} from 'test/resources'
|
||||||
|
|
||||||
|
// mock data
|
||||||
|
const dbrp1 = {database: 'db1', retentionPolicy: 'rp1'}
|
||||||
|
const dbrp2 = {database: 'db2', retentionPolicy: 'rp2'}
|
||||||
|
const namespaces = [dbrp1, dbrp2]
|
||||||
|
|
||||||
|
const setup = (override = {}) => {
|
||||||
|
const props = {
|
||||||
|
query,
|
||||||
|
source,
|
||||||
|
querySource: source,
|
||||||
|
onChooseNamespace: () => {},
|
||||||
|
...override,
|
||||||
|
}
|
||||||
|
|
||||||
|
DatabaseList.prototype.getDbRp = jest.fn(() => Promise.resolve())
|
||||||
|
|
||||||
|
const dbList = shallow(<DatabaseList {...props} />, {
|
||||||
|
context: {source},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
dbList,
|
||||||
|
props,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Shared.Components.DatabaseList', () => {
|
||||||
|
describe('rendering', () => {
|
||||||
|
it('can display the <DatabaseList/>', () => {
|
||||||
|
const {dbList} = setup()
|
||||||
|
|
||||||
|
expect(dbList.exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the correct number of <DatabaseListItems', () => {
|
||||||
|
const {dbList} = setup()
|
||||||
|
// replace with mock of getDbRp()?
|
||||||
|
dbList.setState({namespaces})
|
||||||
|
const list = dbList.find(DatabaseListItem)
|
||||||
|
expect(list.length).toBe(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('instance methods', () => {
|
||||||
|
describe('handleChooseNamespace', () => {
|
||||||
|
it('fires onChooseNamespace with a namspace arg', () => {
|
||||||
|
const onChooseNamespace = jest.fn()
|
||||||
|
const {dbList} = setup({onChooseNamespace})
|
||||||
|
|
||||||
|
const instance = dbList.instance() as DatabaseList
|
||||||
|
instance.handleChooseNamespace(dbrp1)()
|
||||||
|
|
||||||
|
expect(onChooseNamespace).toHaveBeenCalledTimes(1)
|
||||||
|
expect(onChooseNamespace).toHaveBeenCalledWith(dbrp1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('isActive', () => {
|
||||||
|
describe('if the query does not match the db and rp', () => {
|
||||||
|
it('returns false', () => {
|
||||||
|
const {dbList} = setup()
|
||||||
|
|
||||||
|
const instance = dbList.instance() as DatabaseList
|
||||||
|
expect(instance.isActive(query, dbrp1)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('if the query matches the db and rp', () => {
|
||||||
|
it('returns true', () => {
|
||||||
|
const {database, retentionPolicy} = dbrp1
|
||||||
|
const matchingQuery = {
|
||||||
|
...query,
|
||||||
|
database,
|
||||||
|
retentionPolicy,
|
||||||
|
}
|
||||||
|
|
||||||
|
const {dbList} = setup()
|
||||||
|
const instance = dbList.instance() as DatabaseList
|
||||||
|
|
||||||
|
expect(instance.isActive(matchingQuery, dbrp1)).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,58 @@
|
||||||
|
import React from 'react'
|
||||||
|
import DatabaseListItem from 'src/shared/components/DatabaseListItem'
|
||||||
|
import {shallow} from 'enzyme'
|
||||||
|
|
||||||
|
const namespace = {database: 'db1', retentionPolicy: 'rp1'}
|
||||||
|
|
||||||
|
const setup = (override = {}) => {
|
||||||
|
const props = {
|
||||||
|
isActive: false,
|
||||||
|
namespace,
|
||||||
|
onChooseNamespace: () => () => {},
|
||||||
|
...override,
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = shallow(<DatabaseListItem {...props} />)
|
||||||
|
return {
|
||||||
|
item,
|
||||||
|
props,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Shared.Components.DatabaseListItem', () => {
|
||||||
|
describe('rendering', () => {
|
||||||
|
it('renders the <DatabaseListItem />', () => {
|
||||||
|
const {item} = setup()
|
||||||
|
|
||||||
|
expect(item.exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('if isActive is false', () => {
|
||||||
|
it('does not have the `.active` class', () => {
|
||||||
|
const {item} = setup()
|
||||||
|
|
||||||
|
expect(item.hasClass('active')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does have the `.active` class', () => {
|
||||||
|
const {item} = setup({isActive: true})
|
||||||
|
|
||||||
|
expect(item.hasClass('active')).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('callbacks', () => {
|
||||||
|
describe('onChooseNamespace', () => {
|
||||||
|
it('calls onChooseNamespace with the items namespace when clicked', () => {
|
||||||
|
const onChooseNamespace = jest.fn()
|
||||||
|
const {item} = setup({onChooseNamespace})
|
||||||
|
|
||||||
|
item.simulate('click')
|
||||||
|
|
||||||
|
expect(onChooseNamespace).toHaveBeenCalledTimes(1)
|
||||||
|
expect(onChooseNamespace).toBeCalledWith(namespace)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {Dropdown} from 'shared/components/Dropdown'
|
import {Dropdown} from 'shared/components/Dropdown'
|
||||||
import DropdownMenu from 'shared/components/DropdownMenu'
|
import DropdownMenu, {DropdownMenuEmpty} from 'shared/components/DropdownMenu'
|
||||||
|
import DropdownHead from 'shared/components/DropdownHead'
|
||||||
import DropdownInput from 'shared/components/DropdownInput'
|
import DropdownInput from 'shared/components/DropdownInput'
|
||||||
|
|
||||||
import {mount} from 'enzyme'
|
import {mount} from 'enzyme'
|
||||||
|
@ -38,13 +39,13 @@ const setup = (override = {}) => {
|
||||||
describe('Components.Shared.Dropdown', () => {
|
describe('Components.Shared.Dropdown', () => {
|
||||||
describe('rendering', () => {
|
describe('rendering', () => {
|
||||||
describe('initial render', () => {
|
describe('initial render', () => {
|
||||||
it('renders the dropdown menu button', () => {
|
it('renders the <Dropdown/> button', () => {
|
||||||
const {dropdown} = setup()
|
const {dropdown} = setup()
|
||||||
|
|
||||||
expect(dropdown.exists()).toBe(true)
|
expect(dropdown.exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not show the list', () => {
|
it('does not show the <DropdownMenu/> list', () => {
|
||||||
const {dropdown} = setup({items})
|
const {dropdown} = setup({items})
|
||||||
|
|
||||||
const menu = dropdown.find(DropdownMenu)
|
const menu = dropdown.find(DropdownMenu)
|
||||||
|
@ -52,6 +53,43 @@ describe('Components.Shared.Dropdown', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('the <DropdownHead />', () => {
|
||||||
|
const {dropdown} = setup()
|
||||||
|
const head = dropdown.find(DropdownHead)
|
||||||
|
|
||||||
|
expect(head.exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when there are no items in the dropdown', () => {
|
||||||
|
it('renders the <DropdownMenuEmpty/> component', () => {
|
||||||
|
const {dropdown} = setup()
|
||||||
|
const empty = dropdown.find(DropdownMenuEmpty)
|
||||||
|
|
||||||
|
expect(empty.exists()).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('the <DropdownInput/>', () => {
|
||||||
|
it('does not display the input by default', () => {
|
||||||
|
const {dropdown} = setup()
|
||||||
|
const input = dropdown.find(DropdownInput)
|
||||||
|
|
||||||
|
expect(input.exists()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays the input when provided useAutoCompelete is true', () => {
|
||||||
|
const {dropdown} = setup({items, useAutoComplete: true})
|
||||||
|
let input = dropdown.find(DropdownInput)
|
||||||
|
|
||||||
|
expect(input.exists()).toBe(false)
|
||||||
|
|
||||||
|
dropdown.simulate('click')
|
||||||
|
input = dropdown.find(DropdownInput)
|
||||||
|
|
||||||
|
expect(input.exists()).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('user interactions', () => {
|
describe('user interactions', () => {
|
||||||
describe('opening the <DropdownMenu/>', () => {
|
describe('opening the <DropdownMenu/>', () => {
|
||||||
it('shows the menu when clicked', () => {
|
it('shows the menu when clicked', () => {
|
||||||
|
@ -77,22 +115,6 @@ describe('Components.Shared.Dropdown', () => {
|
||||||
expect(menu.exists()).toBe(false)
|
expect(menu.exists()).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('the <DropdownInput/>', () => {
|
|
||||||
it('does not display the input by default', () => {
|
|
||||||
const {dropdown} = setup()
|
|
||||||
const input = dropdown.find(DropdownInput)
|
|
||||||
|
|
||||||
expect(input.exists()).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('displays the input when provided useAutoCompelete is true', () => {
|
|
||||||
const {dropdown} = setup({items, useAutoComplete: true})
|
|
||||||
const input = dropdown.find(DropdownInput)
|
|
||||||
|
|
||||||
expect(input.exists()).toBe(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -202,5 +224,93 @@ describe('Components.Shared.Dropdown', () => {
|
||||||
expect(dropdown.state().highlightedItemIndex).toBe(highlightedItemIndex)
|
expect(dropdown.state().highlightedItemIndex).toBe(highlightedItemIndex)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('handleFilterKeyPress', () => {
|
||||||
|
describe('when Enter is pressed and there are items', () => {
|
||||||
|
it('sets state of isOpen to false', () => {
|
||||||
|
const {dropdown} = setup({items})
|
||||||
|
dropdown.setState({isOpen: true})
|
||||||
|
expect(dropdown.state().isOpen).toBe(true)
|
||||||
|
|
||||||
|
dropdown.instance().handleFilterKeyPress({key: 'Enter'})
|
||||||
|
expect(dropdown.state().isOpen).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fires onChoose with the items at the highlighted index', () => {
|
||||||
|
const onChoose = jest.fn(item => item)
|
||||||
|
const highlightedItemIndex = 1
|
||||||
|
const {dropdown} = setup({items, onChoose})
|
||||||
|
dropdown.setState({highlightedItemIndex})
|
||||||
|
|
||||||
|
dropdown.instance().handleFilterKeyPress({key: 'Enter'})
|
||||||
|
expect(onChoose).toHaveBeenCalledTimes(1)
|
||||||
|
expect(onChoose.mock.calls[0][0]).toEqual(items[highlightedItemIndex])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when Escape is pressed', () => {
|
||||||
|
it('sets isOpen state to false', () => {
|
||||||
|
const {dropdown} = setup({items})
|
||||||
|
dropdown.setState({isOpen: true})
|
||||||
|
|
||||||
|
expect(dropdown.state().isOpen).toBe(true)
|
||||||
|
|
||||||
|
dropdown.instance().handleFilterKeyPress({key: 'Escape'})
|
||||||
|
|
||||||
|
expect(dropdown.state().isOpen).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when ArrowUp is pressed', () => {
|
||||||
|
it('decrements the highlightedItemIndex', () => {
|
||||||
|
const {dropdown} = setup({items})
|
||||||
|
dropdown.setState({highlightedItemIndex: 1})
|
||||||
|
|
||||||
|
dropdown.instance().handleFilterKeyPress({key: 'ArrowUp'})
|
||||||
|
expect(dropdown.state().highlightedItemIndex).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not decrement highlightedItemIndex past 0', () => {
|
||||||
|
const {dropdown} = setup({items})
|
||||||
|
dropdown.setState({highlightedItemIndex: 0})
|
||||||
|
|
||||||
|
dropdown.instance().handleFilterKeyPress({key: 'ArrowUp'})
|
||||||
|
expect(dropdown.state().highlightedItemIndex).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when ArrowDown is pressed', () => {
|
||||||
|
describe('if no highlight has been set', () => {
|
||||||
|
it('starts highlighted index at 0', () => {
|
||||||
|
const {dropdown} = setup({items})
|
||||||
|
expect(dropdown.state().highlightedItemIndex).toBe(null)
|
||||||
|
|
||||||
|
dropdown.instance().handleFilterKeyPress({key: 'ArrowDown'})
|
||||||
|
expect(dropdown.state().highlightedItemIndex).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('if highlightedItemIndex has been set', () => {
|
||||||
|
it('it increments the index', () => {
|
||||||
|
const {dropdown} = setup({items})
|
||||||
|
dropdown.setState({highlightedItemIndex: 0})
|
||||||
|
|
||||||
|
dropdown.instance().handleFilterKeyPress({key: 'ArrowDown'})
|
||||||
|
expect(dropdown.state().highlightedItemIndex).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when highilghtedItemIndex is at the end of the list', () => {
|
||||||
|
it('does not exceed the list length', () => {
|
||||||
|
const {dropdown} = setup({items})
|
||||||
|
dropdown.setState({highlightedItemIndex: 1})
|
||||||
|
|
||||||
|
const expectedIndex = items.length - 1
|
||||||
|
dropdown.instance().handleFilterKeyPress({key: 'ArrowDown'})
|
||||||
|
expect(dropdown.state().highlightedItemIndex).toBe(expectedIndex)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,197 @@
|
||||||
|
import React from 'react'
|
||||||
|
import MeasurementList from 'src/shared/components/MeasurementList'
|
||||||
|
import MeasurementListItem from 'src/shared/components/MeasurementListItem'
|
||||||
|
import MeasurementListFilter from 'src/shared/components/MeasurementListFilter'
|
||||||
|
import {shallow} from 'enzyme'
|
||||||
|
import {query, source} from 'test/resources'
|
||||||
|
|
||||||
|
const setup = (override = {}) => {
|
||||||
|
const props = {
|
||||||
|
query,
|
||||||
|
querySource: source,
|
||||||
|
onChooseTag: () => {},
|
||||||
|
onGroupByTag: () => {},
|
||||||
|
onToggleTagAcceptance: () => {},
|
||||||
|
onChooseMeasurement: () => {},
|
||||||
|
...override,
|
||||||
|
}
|
||||||
|
|
||||||
|
MeasurementList.prototype.getMeasurements = jest.fn(() => Promise.resolve())
|
||||||
|
|
||||||
|
const wrapper = shallow(<MeasurementList {...props} />, {
|
||||||
|
context: {source},
|
||||||
|
})
|
||||||
|
|
||||||
|
const instance = wrapper.instance() as MeasurementList
|
||||||
|
|
||||||
|
return {
|
||||||
|
props,
|
||||||
|
wrapper,
|
||||||
|
instance,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Shared.Components.MeasurementList', () => {
|
||||||
|
describe('rendering', () => {
|
||||||
|
it('renders to the page', () => {
|
||||||
|
const {wrapper} = setup()
|
||||||
|
|
||||||
|
expect(wrapper.exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('<MeasurementListItem/>', () => {
|
||||||
|
it('renders <MeasurementListItem/>`s to the page', () => {
|
||||||
|
const {wrapper} = setup()
|
||||||
|
wrapper.setState({filtered: ['foo', 'bar']})
|
||||||
|
const items = wrapper.find(MeasurementListItem)
|
||||||
|
|
||||||
|
expect(items.length).toBe(2)
|
||||||
|
expect(items.first().dive().text()).toContain('foo')
|
||||||
|
expect(items.last().dive().text()).toContain('bar')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders <MeasurementListFilter/> to the page', () => {
|
||||||
|
const {wrapper} = setup()
|
||||||
|
const filter = wrapper.find(MeasurementListFilter)
|
||||||
|
|
||||||
|
expect(filter.exists()).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('user interractions', () => {
|
||||||
|
it('can filter the measurement list', () => {
|
||||||
|
const {wrapper} = setup()
|
||||||
|
const measurements = ['foo', 'bar']
|
||||||
|
const event = {target: {value: 'f'}, stopPropagation: () => {}}
|
||||||
|
wrapper.setState({filtered: measurements, measurements})
|
||||||
|
|
||||||
|
const filter = wrapper.find(MeasurementListFilter)
|
||||||
|
filter.dive().find('input').simulate('change', event)
|
||||||
|
wrapper.update()
|
||||||
|
|
||||||
|
const items = wrapper.find(MeasurementListItem)
|
||||||
|
expect(items.length).toBe(1)
|
||||||
|
expect(items.dive().text()).toBe('foo')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('lifecycle methods', () => {
|
||||||
|
describe('componentDidMount', () => {
|
||||||
|
it('does not fire getMeasurements if there is no database', () => {
|
||||||
|
const getMeasurements = jest.fn()
|
||||||
|
const {instance} = setup({query: {...query, database: ''}})
|
||||||
|
instance.getMeasurements = getMeasurements
|
||||||
|
instance.componentDidMount()
|
||||||
|
expect(getMeasurements).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does fire getMeasurements if there is a database', () => {
|
||||||
|
const getMeasurements = jest.fn()
|
||||||
|
const {instance} = setup()
|
||||||
|
instance.getMeasurements = getMeasurements
|
||||||
|
instance.componentDidMount()
|
||||||
|
expect(getMeasurements).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('componentDidUpdate', () => {
|
||||||
|
it('does not fire getMeasurements if there is no database', () => {
|
||||||
|
const getMeasurements = jest.fn()
|
||||||
|
const {instance, props} = setup({query: {...query, database: ''}})
|
||||||
|
|
||||||
|
instance.getMeasurements = getMeasurements
|
||||||
|
instance.componentDidUpdate(props)
|
||||||
|
|
||||||
|
expect(getMeasurements).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not fire getMeasurements if the database does not change and the sources are equal', () => {
|
||||||
|
const getMeasurements = jest.fn()
|
||||||
|
const {instance, props} = setup()
|
||||||
|
|
||||||
|
instance.getMeasurements = getMeasurements
|
||||||
|
instance.componentDidUpdate(props)
|
||||||
|
|
||||||
|
expect(getMeasurements).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('it fires getMeasurements if there is a change in database', () => {
|
||||||
|
const getMeasurements = jest.fn()
|
||||||
|
const {instance, props} = setup()
|
||||||
|
|
||||||
|
instance.getMeasurements = getMeasurements
|
||||||
|
instance.componentDidUpdate({
|
||||||
|
...props,
|
||||||
|
query: {...query, database: 'diffDb'},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(getMeasurements).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('it calls getMeasurements if there is a change in source', () => {
|
||||||
|
const getMeasurements = jest.fn()
|
||||||
|
const {instance, props} = setup()
|
||||||
|
|
||||||
|
instance.getMeasurements = getMeasurements
|
||||||
|
instance.componentDidUpdate({
|
||||||
|
...props,
|
||||||
|
querySource: {...source, id: 'newSource'},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(getMeasurements).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('instance methods', () => {
|
||||||
|
describe('handleFilterText', () => {
|
||||||
|
it('sets the filterText state to the event targets value', () => {
|
||||||
|
const {instance} = setup()
|
||||||
|
const value = 'spectacs'
|
||||||
|
const stopPropagation = () => {}
|
||||||
|
const event = {target: {value}, stopPropagation}
|
||||||
|
|
||||||
|
instance.handleFilterText(event)
|
||||||
|
|
||||||
|
expect(instance.state.filterText).toBe(value)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('handleEscape', () => {
|
||||||
|
it('resets fiterText when escape is pressed', () => {
|
||||||
|
const {instance} = setup()
|
||||||
|
const key = 'Escape'
|
||||||
|
const stopPropagation = () => {}
|
||||||
|
const event = {key, stopPropagation}
|
||||||
|
|
||||||
|
instance.setState({filterText: 'foo'})
|
||||||
|
expect(instance.state.filterText).toBe('foo')
|
||||||
|
|
||||||
|
instance.handleEscape(event)
|
||||||
|
expect(instance.state.filterText).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not reset fiterText when escape is pressed', () => {
|
||||||
|
const {instance} = setup()
|
||||||
|
const key = 'Enter'
|
||||||
|
const stopPropagation = () => {}
|
||||||
|
const event = {key, stopPropagation}
|
||||||
|
|
||||||
|
instance.setState({filterText: 'foo'})
|
||||||
|
instance.handleEscape(event)
|
||||||
|
expect(instance.state.filterText).toBe('foo')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('handleAcceptReject', () => {
|
||||||
|
it('it calls onToggleTagAcceptance', () => {
|
||||||
|
const onToggleTagAcceptance = jest.fn()
|
||||||
|
const {instance} = setup({onToggleTagAcceptance})
|
||||||
|
|
||||||
|
instance.handleAcceptReject()
|
||||||
|
expect(onToggleTagAcceptance).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,170 @@
|
||||||
|
import React from 'react'
|
||||||
|
import TagList from 'src/shared/components/TagList'
|
||||||
|
import TagListItem from 'src/shared/components/TagListItem'
|
||||||
|
import {shallow} from 'enzyme'
|
||||||
|
|
||||||
|
import {query, source} from 'test/resources'
|
||||||
|
|
||||||
|
const setup = (override = {}) => {
|
||||||
|
const props = {
|
||||||
|
query,
|
||||||
|
querySource: source,
|
||||||
|
onChooseTag: () => {},
|
||||||
|
onGroupByTag: () => {},
|
||||||
|
...override,
|
||||||
|
}
|
||||||
|
|
||||||
|
TagList.prototype.getTags = jest.fn(() => Promise.resolve)
|
||||||
|
const wrapper = shallow(<TagList {...props} />, {context: {source}})
|
||||||
|
const instance = wrapper.instance() as TagList
|
||||||
|
|
||||||
|
return {
|
||||||
|
instance,
|
||||||
|
wrapper,
|
||||||
|
props,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Shared.Components.TagList', () => {
|
||||||
|
describe('lifecycle methods', () => {
|
||||||
|
describe('componentDidMount', () => {
|
||||||
|
it('gets the tags', () => {
|
||||||
|
const getTags = jest.fn()
|
||||||
|
const {instance} = setup()
|
||||||
|
instance.getTags = getTags
|
||||||
|
instance.componentDidMount()
|
||||||
|
|
||||||
|
expect(getTags).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('if there is no database', () => {
|
||||||
|
it('does not get the tags', () => {
|
||||||
|
const getTags = jest.fn()
|
||||||
|
const {instance} = setup({query: {...query, database: ''}})
|
||||||
|
instance.getTags = getTags
|
||||||
|
instance.componentDidMount()
|
||||||
|
|
||||||
|
expect(getTags).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('if there is no measurement', () => {
|
||||||
|
it('does not get the tags', () => {
|
||||||
|
const getTags = jest.fn()
|
||||||
|
const {instance} = setup({query: {...query, measurement: ''}})
|
||||||
|
instance.getTags = getTags
|
||||||
|
instance.componentDidMount()
|
||||||
|
|
||||||
|
expect(getTags).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('if there is no retention policy', () => {
|
||||||
|
it('does not get the tags', () => {
|
||||||
|
const getTags = jest.fn()
|
||||||
|
const {instance} = setup({query: {...query, retentionPolicy: ''}})
|
||||||
|
instance.getTags = getTags
|
||||||
|
instance.componentDidMount()
|
||||||
|
|
||||||
|
expect(getTags).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('componentDidUpdate', () => {
|
||||||
|
describe('if the db, rp, measurement, and source change and exist', () => {
|
||||||
|
it('gets the tags', () => {
|
||||||
|
const getTags = jest.fn()
|
||||||
|
const updates = {
|
||||||
|
database: 'newDb',
|
||||||
|
retentionPolicy: 'newRp',
|
||||||
|
measurement: 'newMeasurement',
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevQuery = {...query, ...updates}
|
||||||
|
const prevSource = {...source, id: 'prevID'}
|
||||||
|
|
||||||
|
const {instance} = setup()
|
||||||
|
instance.getTags = getTags
|
||||||
|
instance.componentDidUpdate({
|
||||||
|
query: prevQuery,
|
||||||
|
querySource: prevSource,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(getTags).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('if there is no database', () => {
|
||||||
|
it('does not get the tags', () => {
|
||||||
|
const getTags = jest.fn()
|
||||||
|
const {instance} = setup({query: {...query, database: ''}})
|
||||||
|
instance.getTags = getTags
|
||||||
|
instance.componentDidMount()
|
||||||
|
|
||||||
|
expect(getTags).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('if there is no retentionPolicy', () => {
|
||||||
|
it('does not get the tags', () => {
|
||||||
|
const getTags = jest.fn()
|
||||||
|
const {instance} = setup({query: {...query, retentionPolicy: ''}})
|
||||||
|
instance.getTags = getTags
|
||||||
|
instance.componentDidMount()
|
||||||
|
|
||||||
|
expect(getTags).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('if there is no measurement', () => {
|
||||||
|
it('does not get the tags', () => {
|
||||||
|
const getTags = jest.fn()
|
||||||
|
const {instance} = setup({query: {...query, measurement: ''}})
|
||||||
|
instance.getTags = getTags
|
||||||
|
instance.componentDidMount()
|
||||||
|
|
||||||
|
expect(getTags).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('rendering', () => {
|
||||||
|
it('renders', () => {
|
||||||
|
const {wrapper} = setup()
|
||||||
|
|
||||||
|
expect(wrapper.exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when there are no tags', () => {
|
||||||
|
it('does not render the <TagListItem/>', () => {
|
||||||
|
const {wrapper} = setup()
|
||||||
|
const tagListItems = wrapper.find(TagListItem)
|
||||||
|
expect(tagListItems.length).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when there are tags', () => {
|
||||||
|
it('does renders the <TagListItem/>', () => {
|
||||||
|
const {wrapper} = setup()
|
||||||
|
const values = ['tv1', 'tv2']
|
||||||
|
const tags = {
|
||||||
|
tk1: values,
|
||||||
|
tk2: values,
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.setState({tags})
|
||||||
|
const tagListItems = wrapper.find(TagListItem)
|
||||||
|
const first = tagListItems.first()
|
||||||
|
const last = tagListItems.last()
|
||||||
|
|
||||||
|
expect(tagListItems.length).toBe(2)
|
||||||
|
expect(first.props().tagKey).toBe('tk1')
|
||||||
|
expect(first.props().tagValues).toBe(values)
|
||||||
|
expect(last.props().tagKey).toBe('tk2')
|
||||||
|
expect(last.props().tagValues).toBe(values)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"types": ["node", "mocha", "chai", "lodash"],
|
"types": ["node", "chai", "lodash", "enzyme", "react", "prop-types", "jest"],
|
||||||
"target": "es6",
|
"target": "es6",
|
||||||
"module": "es2015",
|
"module": "es2015",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
|
|
|
@ -17,15 +17,10 @@ const babelLoader = {
|
||||||
loader: 'babel-loader',
|
loader: 'babel-loader',
|
||||||
options: {
|
options: {
|
||||||
cacheDirectory: true,
|
cacheDirectory: true,
|
||||||
presets: ['env', 'react', 'stage-0'],
|
presets: [['env', {modules: false}], 'react', 'stage-0'],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const log = function(x) {
|
|
||||||
console.log('IM LOGGIN HERE: ', x)
|
|
||||||
return x
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
node: {
|
node: {
|
||||||
fs: 'empty',
|
fs: 'empty',
|
||||||
|
@ -96,6 +91,7 @@ module.exports = {
|
||||||
include: path.resolve(__dirname, '..', 'src'),
|
include: path.resolve(__dirname, '..', 'src'),
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
use: [
|
use: [
|
||||||
|
{loader: 'thread-loader'},
|
||||||
{
|
{
|
||||||
loader: 'babel-loader',
|
loader: 'babel-loader',
|
||||||
options: {
|
options: {
|
||||||
|
|
|
@ -5,7 +5,6 @@ const ExtractTextPlugin = require('extract-text-webpack-plugin')
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||||
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
|
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
|
||||||
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
|
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
|
||||||
const CompressionPlugin = require('compression-webpack-plugin')
|
|
||||||
|
|
||||||
const package = require('../package.json')
|
const package = require('../package.json')
|
||||||
const dependencies = package.dependencies
|
const dependencies = package.dependencies
|
||||||
|
@ -14,7 +13,7 @@ const babelLoader = {
|
||||||
loader: 'babel-loader',
|
loader: 'babel-loader',
|
||||||
options: {
|
options: {
|
||||||
cacheDirectory: false,
|
cacheDirectory: false,
|
||||||
presets: ['env', 'react', 'stage-0'],
|
presets: [['env', {modules: false}], 'react', 'stage-0'],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,13 +148,6 @@ const config = {
|
||||||
new webpack.optimize.CommonsChunkPlugin({
|
new webpack.optimize.CommonsChunkPlugin({
|
||||||
name: 'manifest',
|
name: 'manifest',
|
||||||
}),
|
}),
|
||||||
new CompressionPlugin({
|
|
||||||
asset: '[path].gz[query]',
|
|
||||||
algorithm: 'gzip',
|
|
||||||
test: /\.js$|\.css$/,
|
|
||||||
threshold: 0,
|
|
||||||
minRatio: 0.8,
|
|
||||||
}),
|
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
template: path.resolve(__dirname, '..', 'src', 'index.template.html'),
|
template: path.resolve(__dirname, '..', 'src', 'index.template.html'),
|
||||||
inject: 'body',
|
inject: 'body',
|
||||||
|
|
986
ui/yarn.lock
986
ui/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue