Merge branch 'master' into feature/graph-table-time-format
commit
2f985b56dd
|
@ -1,5 +1,5 @@
|
|||
[bumpversion]
|
||||
current_version = 1.4.2.1
|
||||
current_version = 1.4.2.2
|
||||
files = README.md server/swagger.json
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\.(?P<release>\d+)
|
||||
serialize = {major}.{minor}.{patch}.{release}
|
||||
|
|
|
@ -2,7 +2,12 @@
|
|||
### Features
|
||||
### UI Improvements
|
||||
### 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]
|
||||
### Features
|
||||
|
|
|
@ -136,7 +136,7 @@ option.
|
|||
## Versions
|
||||
|
||||
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
|
||||
[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:
|
||||
|
||||
```sh
|
||||
docker pull chronograf:1.4.2.1
|
||||
docker pull chronograf:1.4.2.2
|
||||
```
|
||||
|
||||
### From Source
|
||||
|
|
|
@ -2,6 +2,7 @@ package oauth2
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
|
@ -61,7 +62,19 @@ func (h *Heroku) PrincipalID(provider *http.Client) (string, error) {
|
|||
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 {
|
||||
h.Logger.Error("Unable to communicate with Heroku. err:", err)
|
||||
return "", err
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"info": {
|
||||
"title": "Chronograf",
|
||||
"description": "API endpoints for Chronograf",
|
||||
"version": "1.4.2.1"
|
||||
"version": "1.4.2.2"
|
||||
},
|
||||
"schemes": ["http"],
|
||||
"basePath": "/chronograf/v1",
|
||||
|
|
|
@ -2,10 +2,19 @@ module.exports = {
|
|||
projects: [
|
||||
{
|
||||
displayName: 'test',
|
||||
testPathIgnorePatterns: ['/build/'],
|
||||
testPathIgnorePatterns: [
|
||||
'build',
|
||||
'<rootDir>/node_modules/(?!(jest-test))',
|
||||
],
|
||||
modulePaths: ['<rootDir>', '<rootDir>/node_modules/'],
|
||||
moduleDirectories: ['src'],
|
||||
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',
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "chronograf-ui",
|
||||
"version": "1.4.2-1",
|
||||
"version": "1.4.2-2",
|
||||
"private": false,
|
||||
"license": "AGPL-3.0",
|
||||
"description": "",
|
||||
|
@ -16,7 +16,7 @@
|
|||
"start:fast": "webpack --watch --config ./webpack/dev.config.js",
|
||||
"start:hmr": "webpack-dev-server --open --config ./webpack/dev.config.js",
|
||||
"lint": "esw src/",
|
||||
"test": "jest --runInBand",
|
||||
"test": "jest",
|
||||
"test:lint": "yarn run lint; yarn run test",
|
||||
"test:watch": "jest --watch",
|
||||
"clean": "rm -rf ./build/*",
|
||||
|
@ -31,9 +31,11 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.1.2",
|
||||
"@types/enzyme": "^3.1.9",
|
||||
"@types/jest": "^22.1.4",
|
||||
"@types/lodash": "^4.14.104",
|
||||
"@types/mocha": "^2.2.48",
|
||||
"@types/node": "^9.4.6",
|
||||
"@types/prop-types": "^15.5.2",
|
||||
"@types/react": "^16.0.38",
|
||||
"autoprefixer": "^6.3.1",
|
||||
"babel-core": "^6.5.1",
|
||||
|
@ -51,7 +53,6 @@
|
|||
"babel-preset-react": "^6.5.0",
|
||||
"babel-preset-stage-0": "^6.16.0",
|
||||
"babel-runtime": "^6.5.0",
|
||||
"bower": "^1.7.7",
|
||||
"compression-webpack-plugin": "^1.1.8",
|
||||
"concurrently": "^3.5.0",
|
||||
"core-js": "^2.1.3",
|
||||
|
@ -71,7 +72,6 @@
|
|||
"file-loader": "^1.1.7",
|
||||
"fork-ts-checker-webpack-plugin": "^0.3.0",
|
||||
"hanson": "^1.1.1",
|
||||
"hson-loader": "^1.0.0",
|
||||
"html-webpack-include-assets-plugin": "^1.0.2",
|
||||
"html-webpack-plugin": "^2.30.1",
|
||||
"imports-loader": "^0.6.5",
|
||||
|
@ -79,7 +79,6 @@
|
|||
"jest-runner-eslint": "^0.4.0",
|
||||
"jsdom": "^9.0.0",
|
||||
"json-loader": "^0.5.7",
|
||||
"mustache": "^2.2.1",
|
||||
"node-sass": "^4.5.3",
|
||||
"on-build-webpack": "^0.1.0",
|
||||
"postcss-browser-reporter": "^0.4.0",
|
||||
|
@ -93,8 +92,8 @@
|
|||
"resolve-url-loader": "^2.2.1",
|
||||
"sass-loader": "^6.0.6",
|
||||
"style-loader": "^0.13.0",
|
||||
"testem": "^1.2.1",
|
||||
"thread-loader": "^1.1.4",
|
||||
"thread-loader": "^1.1.5",
|
||||
"ts-jest": "^22.4.1",
|
||||
"ts-loader": "^3.5.0",
|
||||
"tslib": "^1.9.0",
|
||||
"typescript": "^2.7.2",
|
||||
|
|
|
@ -111,6 +111,7 @@ class OrganizationsTableRow extends Component {
|
|||
onConfirm={this.handleDeleteOrg}
|
||||
onClickOutside={this.handleDismissDeleteConfirmation}
|
||||
confirmLeft={true}
|
||||
confirmTitle="Delete"
|
||||
/>
|
||||
: <OrganizationsTableRowDeleteButton
|
||||
organization={organization}
|
||||
|
|
|
@ -109,6 +109,7 @@ class ProvidersTableRow extends Component {
|
|||
onCancel={this.handleDismissDeleteConfirmation}
|
||||
onConfirm={this.handleDeleteMap}
|
||||
onClickOutside={this.handleDismissDeleteConfirmation}
|
||||
confirmTitle="Delete"
|
||||
/>
|
||||
: <button
|
||||
className="btn btn-sm btn-default btn-square"
|
||||
|
|
|
@ -90,7 +90,7 @@ class AxesOptions extends Component {
|
|||
<h5 className="display-options--header">
|
||||
{menuOption} Controls
|
||||
</h5>
|
||||
<form autoComplete="off" style={{margin: '0 -6px'}}>
|
||||
<form autoComplete="off" className="form-group-wrapper">
|
||||
<div className="form-group col-sm-12">
|
||||
<label htmlFor="prefix">Title</label>
|
||||
<OptIn
|
||||
|
|
|
@ -190,7 +190,7 @@ class GaugeOptions extends Component {
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="single-stat-controls">
|
||||
<div className="graph-options-group form-group-wrapper">
|
||||
<div className="form-group col-xs-6">
|
||||
<label>Prefix</label>
|
||||
<input
|
||||
|
|
|
@ -8,15 +8,16 @@ const GraphOptionsCustomizableColumn = ({
|
|||
onColumnRename,
|
||||
}) => {
|
||||
return (
|
||||
<div className="gauge-controls--section">
|
||||
<div className="gauge-controls--label">
|
||||
<div className="column-controls--section">
|
||||
<div className="column-controls--label">
|
||||
{originalColumnName}
|
||||
</div>
|
||||
<InputClickToEdit
|
||||
value={newColumnName}
|
||||
wrapperClass="fancytable--td orgs-table--name"
|
||||
wrapperClass="column-controls-input"
|
||||
onUpdate={onColumnRename}
|
||||
placeholder="Rename..."
|
||||
appearAsNormalInput={true}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -5,8 +5,8 @@ import uuid from 'uuid'
|
|||
|
||||
const GraphOptionsCustomizeColumns = ({columns, onColumnRename}) => {
|
||||
return (
|
||||
<div>
|
||||
<label>Customize Columns</label>
|
||||
<div className="graph-options-group">
|
||||
<label className="form-label">Customize Columns</label>
|
||||
{columns.map(col => {
|
||||
return (
|
||||
<GraphOptionsCustomizableColumn
|
||||
|
|
|
@ -7,8 +7,8 @@ const GraphOptionsSortBy = ({sortByOptions, onChooseSortBy}) =>
|
|||
<Dropdown
|
||||
items={sortByOptions}
|
||||
selected={sortByOptions[0].text}
|
||||
buttonColor="btn-primary"
|
||||
buttonSize="btn-xs"
|
||||
buttonColor="btn-default"
|
||||
buttonSize="btn-sm"
|
||||
className="dropdown-stretch"
|
||||
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.
|
||||
const GraphOptionsTextWrapping = ({singleStatType, onToggleTextWrapping}) => {
|
||||
return (
|
||||
<div>
|
||||
<div className="form-group col-xs-12">
|
||||
<label>Text Wrapping</label>
|
||||
<ul className="nav nav-tablist nav-tablist-sm">
|
||||
<li
|
||||
|
|
|
@ -11,7 +11,7 @@ const GraphOptionsThresholdColoring = ({
|
|||
singleStatType,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<div className="form-group col-xs-12 col-md-6">
|
||||
<label>Threshold Coloring</label>
|
||||
<ul className="nav nav-tablist nav-tablist-sm">
|
||||
<li
|
||||
|
|
|
@ -19,8 +19,8 @@ const GraphOptionsThresholds = ({
|
|||
onDeleteThreshold,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<label>Thresholds</label>
|
||||
<div className="gauge-controls graph-options-group">
|
||||
<label className="form-label">Thresholds</label>
|
||||
<button
|
||||
className="btn btn-sm btn-primary gauge-controls--add-threshold"
|
||||
onClick={onAddThreshold}
|
||||
|
|
|
@ -3,7 +3,7 @@ import React, {PropTypes} from 'react'
|
|||
const VERTICAL = 'VERTICAL'
|
||||
const HORIZONTAL = 'HORIZONTAL'
|
||||
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>
|
||||
<ul className="nav nav-tablist nav-tablist-sm">
|
||||
<li
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import moment from 'moment'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import InputClickToEdit from 'shared/components/InputClickToEdit'
|
||||
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/'
|
||||
|
||||
return (
|
||||
<div className="gauge-controls--section">
|
||||
<div className="form-group col-xs-12">
|
||||
<label>
|
||||
Time Format{' '}
|
||||
Time Format
|
||||
{customFormat &&
|
||||
<QuestionMarkTooltip
|
||||
tipID="Time Axis Format"
|
||||
|
@ -55,18 +56,21 @@ class GraphOptionsTimeFormat extends Component {
|
|||
<Dropdown
|
||||
items={formatOptions}
|
||||
selected={customFormat ? 'Custom' : format}
|
||||
buttonColor="btn-primary"
|
||||
buttonColor="btn-default"
|
||||
buttonSize="btn-xs"
|
||||
className="dropdown-stretch"
|
||||
onChoose={this.handleChooseFormat}
|
||||
/>
|
||||
{customFormat &&
|
||||
<InputClickToEdit
|
||||
wrapperClass="fancytable--td"
|
||||
value={format}
|
||||
onUpdate={this.handleInputChange}
|
||||
placeholder="Enter custom format..."
|
||||
/>}
|
||||
<div className="column-controls--section">
|
||||
<InputClickToEdit
|
||||
wrapperClass="column-controls-input "
|
||||
value={format}
|
||||
onUpdate={this.handleInputChange}
|
||||
placeholder="Enter custom format..."
|
||||
appearAsNormalInput={true}
|
||||
/>
|
||||
</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -186,7 +186,7 @@ class SingleStatOptions extends Component {
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="single-stat-controls">
|
||||
<div className="graph-options-group form-group-wrapper">
|
||||
<div className="form-group col-xs-6">
|
||||
<label>Prefix</label>
|
||||
<input
|
||||
|
|
|
@ -66,12 +66,20 @@ class TableOptions extends Component {
|
|||
|
||||
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,
|
||||
name: col,
|
||||
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 (
|
||||
<FancyScrollbar
|
||||
|
@ -80,7 +88,7 @@ class TableOptions extends Component {
|
|||
>
|
||||
<div className="display-options--cell-wrapper">
|
||||
<h5 className="display-options--header">Table Controls</h5>
|
||||
<div className="gauge-controls">
|
||||
<div className="form-group-wrapper">
|
||||
<GraphOptionsTimeFormat
|
||||
timeFormat={timeFormat}
|
||||
onTimeFormatChange={this.handleTimeFormatChange}
|
||||
|
@ -97,20 +105,22 @@ class TableOptions extends Component {
|
|||
singleStatType={singleStatType}
|
||||
onToggleTextWrapping={this.handleToggleTextWrapping}
|
||||
/>
|
||||
<GraphOptionsCustomizeColumns
|
||||
columns={columns}
|
||||
onColumnRename={this.handleColumnRename}
|
||||
/>
|
||||
<GraphOptionsThresholds
|
||||
onAddThreshold={this.handleAddThreshold}
|
||||
disableAddThreshold={disableAddThreshold}
|
||||
sortedColors={sortedColors}
|
||||
formatColor={formatColor}
|
||||
onChooseColor={this.handleChooseColor}
|
||||
onValidateColorValue={this.handleValidateColorValue}
|
||||
onUpdateColorValue={this.handleUpdateColorValue}
|
||||
onDeleteThreshold={this.handleDeleteThreshold}
|
||||
/>
|
||||
</div>
|
||||
<GraphOptionsCustomizeColumns
|
||||
columns={columns}
|
||||
onColumnRename={this.handleColumnRename}
|
||||
/>
|
||||
<GraphOptionsThresholds
|
||||
onAddThreshold={this.handleAddThreshold}
|
||||
disableAddThreshold={disableAddThreshold}
|
||||
sortedColors={sortedColors}
|
||||
formatColor={formatColor}
|
||||
onChooseColor={this.handleChooseColor}
|
||||
onValidateColorValue={this.handleValidateColorValue}
|
||||
onUpdateColorValue={this.handleUpdateColorValue}
|
||||
onDeleteThreshold={this.handleDeleteThreshold}
|
||||
/>
|
||||
<div className="form-group-wrapper graph-options-group">
|
||||
<GraphOptionsThresholdColoring
|
||||
onToggleSingleStatType={this.handleToggleSingleStatType}
|
||||
singleStatColors={singleStatType}
|
||||
|
|
|
@ -439,29 +439,71 @@ const GRAPH_SVGS = {
|
|||
y="0px"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 550 550"
|
||||
viewBox="0 0 150 150"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
className="viz-type-selector--graphic-line graphic-line-a"
|
||||
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"
|
||||
/>
|
||||
</g>
|
||||
<g />
|
||||
<g />
|
||||
<g />
|
||||
<g />
|
||||
<g />
|
||||
<g />
|
||||
<g />
|
||||
<g />
|
||||
<g />
|
||||
<g />
|
||||
<g />
|
||||
<g />
|
||||
<g />
|
||||
<g />
|
||||
<g />
|
||||
<path
|
||||
className="viz-type-selector--graphic-fill graphic-fill-c"
|
||||
d="M55.5,115H19.7c-1.7,0-3.1-1.4-3.1-3.1V61.7h38.9V115z"
|
||||
/>
|
||||
<path
|
||||
className="viz-type-selector--graphic-fill graphic-fill-b"
|
||||
d="M133.4,61.7H55.5V35h74.8c1.7,0,3.1,1.4,3.1,3.1V61.7z"
|
||||
/>
|
||||
<path
|
||||
className="viz-type-selector--graphic-fill graphic-fill-a"
|
||||
d="M55.5,61.7H16.6V38.1c0-1.7,1.4-3.1,3.1-3.1h35.9V61.7z"
|
||||
/>
|
||||
<path
|
||||
className="viz-type-selector--graphic-line graphic-line-c"
|
||||
d="M16.6,88.3v23.6c0,1.7,1.4,3.1,3.1,3.1h35.9V88.3H16.6z"
|
||||
/>
|
||||
<rect
|
||||
className="viz-type-selector--graphic-line graphic-line-c"
|
||||
x="16.6"
|
||||
y="61.7"
|
||||
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>
|
||||
</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}) => {
|
||||
this.setState({kapacitor: {...this.state.kapacitor, url: value}})
|
||||
handleChangeUrl = e => {
|
||||
this.setState({kapacitor: {...this.state.kapacitor, url: e.target.value}})
|
||||
}
|
||||
|
||||
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'
|
||||
|
||||
|
|
|
@ -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() {
|
||||
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
|
||||
? <div className={wrapperClass}>
|
||||
|
@ -68,16 +77,16 @@ class InputClickToEdit extends Component {
|
|||
onFocus={this.handleFocus}
|
||||
ref={r => (this.inputRef = r)}
|
||||
tabIndex={tabIndex}
|
||||
placeholder={placeholder}
|
||||
spellCheck={false}
|
||||
/>
|
||||
: <div
|
||||
className={divStyle}
|
||||
className={defaultStyle}
|
||||
onClick={this.handleInputClick}
|
||||
onFocus={this.handleInputClick}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
{value || placeholder}
|
||||
<span className="icon pencil" />
|
||||
{appearAsNormalInput || <span className="icon pencil" />}
|
||||
</div>}
|
||||
</div>
|
||||
}
|
||||
|
@ -92,6 +101,7 @@ InputClickToEdit.propTypes = {
|
|||
disabled: bool,
|
||||
tabIndex: number,
|
||||
placeholder: string,
|
||||
appearAsNormalInput: bool,
|
||||
}
|
||||
|
||||
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 PropTypes from 'prop-types'
|
||||
import _ from 'lodash'
|
||||
import classnames from 'classnames'
|
||||
import {timeSeriesToTable} from 'src/utils/timeSeriesToDygraph'
|
||||
import {MultiGrid} from 'react-virtualized'
|
||||
import moment from 'moment'
|
||||
|
@ -22,11 +23,27 @@ class TableGraph extends Component {
|
|||
|
||||
cellRenderer = ({columnIndex, key, rowIndex, style}) => {
|
||||
const data = this._data
|
||||
const columnCount = _.get(data, ['0', 'length'], 0)
|
||||
const rowCount = data.length
|
||||
const {timeFormat} = this.state
|
||||
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 (
|
||||
<div key={key} style={style}>
|
||||
<div key={key} className={cellClass} style={style}>
|
||||
{isTimeCell
|
||||
? moment(data[rowIndex][columnIndex]).format(timeFormat)
|
||||
: data[rowIndex][columnIndex]}
|
||||
|
@ -39,13 +56,13 @@ class TableGraph extends Component {
|
|||
const columnCount = _.get(data, ['0', 'length'], 0)
|
||||
const rowCount = data.length
|
||||
const COLUMN_WIDTH = 300
|
||||
const ROW_HEIGHT = 50
|
||||
const ROW_HEIGHT = 30
|
||||
const tableWidth = this.gridContainer ? this.gridContainer.clientWidth : 0
|
||||
const tableHeight = this.gridContainer ? this.gridContainer.clientHeight : 0
|
||||
|
||||
return (
|
||||
<div
|
||||
className="graph-container"
|
||||
className="table-graph-container"
|
||||
ref={gridContainer => (this.gridContainer = gridContainer)}
|
||||
>
|
||||
{data.length > 1 &&
|
||||
|
@ -58,8 +75,9 @@ class TableGraph extends Component {
|
|||
height={tableHeight}
|
||||
rowCount={rowCount}
|
||||
rowHeight={ROW_HEIGHT}
|
||||
width={tableWidth - 32}
|
||||
onScroll={this.handleScroll}
|
||||
width={tableWidth}
|
||||
enableFixedColumnScroll={true}
|
||||
enableFixedRowScroll={true}
|
||||
/>}
|
||||
</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 classnames from 'classnames'
|
||||
|
||||
const {string, arrayOf, func, bool} = PropTypes
|
||||
const TagListItem = React.createClass({
|
||||
propTypes: {
|
||||
tagKey: string.isRequired,
|
||||
tagValues: arrayOf(string.isRequired).isRequired,
|
||||
selectedTagValues: arrayOf(string.isRequired).isRequired,
|
||||
isUsingGroupBy: bool,
|
||||
onChooseTag: func.isRequired,
|
||||
onGroupByTag: func.isRequired,
|
||||
},
|
||||
interface Props {
|
||||
tagKey: string
|
||||
tagValues: string[]
|
||||
selectedTagValues: string[]
|
||||
isUsingGroupBy?: boolean
|
||||
onChooseTag: ({key: string, value}) => void
|
||||
onGroupByTag: (tagKey: string) => void
|
||||
}
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
interface State {
|
||||
isOpen: boolean
|
||||
filterText: string
|
||||
}
|
||||
|
||||
class TagListItem extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isOpen: false,
|
||||
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})
|
||||
},
|
||||
}
|
||||
|
||||
handleClickKey() {
|
||||
this.setState({isOpen: !this.state.isOpen})
|
||||
},
|
||||
}
|
||||
|
||||
handleFilterText(e) {
|
||||
e.stopPropagation()
|
||||
this.setState({
|
||||
filterText: this.refs.filterText.value,
|
||||
filterText: e.target.value,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
handleEscape(e) {
|
||||
if (e.key !== 'Escape') {
|
||||
|
@ -44,7 +55,12 @@ const TagListItem = React.createClass({
|
|||
this.setState({
|
||||
filterText: '',
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
handleGroupBy(e) {
|
||||
e.stopPropagation()
|
||||
this.props.onGroupByTag(this.props.tagKey)
|
||||
}
|
||||
|
||||
renderTagValues() {
|
||||
const {tagValues, selectedTagValues} = this.props
|
||||
|
@ -67,7 +83,7 @@ const TagListItem = React.createClass({
|
|||
onChange={this.handleFilterText}
|
||||
onKeyUp={this.handleEscape}
|
||||
spellCheck={false}
|
||||
autoComplete={false}
|
||||
autoComplete="false"
|
||||
/>
|
||||
<span className="icon search" />
|
||||
</div>
|
||||
|
@ -91,12 +107,7 @@ const TagListItem = React.createClass({
|
|||
})}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
handleGroupBy(e) {
|
||||
e.stopPropagation()
|
||||
this.props.onGroupByTag(this.props.tagKey)
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
const {tagKey, tagValues, isUsingGroupBy} = this.props
|
||||
|
@ -127,7 +138,7 @@ const TagListItem = React.createClass({
|
|||
{isOpen ? this.renderTagValues() : null}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default TagListItem
|
|
@ -67,6 +67,7 @@
|
|||
@import 'components/info-indicators';
|
||||
@import 'components/source-selector';
|
||||
@import 'components/tables';
|
||||
@import 'components/table-graph';
|
||||
@import 'components/kapacitor-logs-table';
|
||||
|
||||
// Pages
|
||||
|
|
|
@ -206,46 +206,50 @@ $graph-type--gutter: 4px;
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.gauge-controls--section {
|
||||
.gauge-controls--section,
|
||||
.column-controls--section {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
margin-top: 8px;
|
||||
|
||||
> * {
|
||||
margin-left: 4px;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
button.btn.btn-primary.btn-sm.gauge-controls--add-threshold {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gauge-controls--label {
|
||||
%gauge-controls-label-styles {
|
||||
height: 30px;
|
||||
background-color: $g4-onyx;
|
||||
line-height: 30px;
|
||||
font-weight: 600;
|
||||
color: $g11-sidewalk;
|
||||
font-size: 13px;
|
||||
padding: 0 11px;
|
||||
border-radius: 4px;
|
||||
line-height: 30px;
|
||||
@include no-user-select();
|
||||
}
|
||||
.gauge-controls--label {
|
||||
@extend %gauge-controls-label-styles;
|
||||
color: $g11-sidewalk;
|
||||
background-color: $g4-onyx;
|
||||
width: 120px;
|
||||
}
|
||||
.gauge-controls--label-editable {
|
||||
height: 30px;
|
||||
font-weight: 600;
|
||||
@extend %gauge-controls-label-styles;
|
||||
color: $g16-pearl;
|
||||
padding: 0 11px;
|
||||
border-radius: 4px;
|
||||
line-height: 30px;
|
||||
@include no-user-select();
|
||||
width: 90px;
|
||||
}
|
||||
|
||||
.gauge-controls--input {
|
||||
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 {
|
||||
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
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
.single-stat-controls {
|
||||
.graph-options-group {
|
||||
margin-top: 30px;
|
||||
}
|
||||
.form-group-wrapper {
|
||||
display: inline-block;
|
||||
width: calc(100% + 12px);
|
||||
margin: 30px -6px 0 -6px;
|
||||
|
||||
> div.form-group {
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
margin-left: -6px;
|
||||
margin-right: -6px;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
border-radius: 4px;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.input-cte {
|
||||
|
@ -55,10 +56,34 @@
|
|||
|
||||
.input-cte__empty {
|
||||
@extend .input-cte;
|
||||
font-style: italic;
|
||||
color: $g9-mountain;
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
line-height: 27px;
|
||||
|
||||
&:hover {
|
||||
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-radius: $radius;
|
||||
color: $g15-platinum;
|
||||
letter-spacing: 0px;
|
||||
background-color: $g2-kevlar;
|
||||
font-weight: 600;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
transition:
|
||||
color 0.25s ease,
|
||||
background-color 0.25s ease,
|
||||
|
@ -188,7 +192,8 @@ textarea.form-control {
|
|||
.form-group > .btn {
|
||||
display: inline-block;
|
||||
}
|
||||
.form-group > label {
|
||||
.form-group > label,
|
||||
label.form-label {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
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 graph = new Dygraph(div, timeSeries, {labels})
|
||||
|
||||
const oneHourMs = '3600000'
|
||||
|
||||
const a1 = {
|
||||
group: '',
|
||||
name: 'a1',
|
||||
|
@ -43,14 +41,14 @@ describe('Shared.Annotations.Helpers', () => {
|
|||
const actual = visibleAnnotations(undefined, annotations)
|
||||
const expected = []
|
||||
|
||||
expect(actual).to.deep.equal(expected)
|
||||
expect(actual).toEqual(expected)
|
||||
})
|
||||
|
||||
it('returns an annotation if it is in the time range', () => {
|
||||
const actual = visibleAnnotations(graph, 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', () => {
|
||||
|
@ -65,7 +63,7 @@ describe('Shared.Annotations.Helpers', () => {
|
|||
const actual = visibleAnnotations(graph, newAnnos)
|
||||
const expected = annotations
|
||||
|
||||
expect(actual).to.deep.equal(expected)
|
||||
expect(actual).toEqual(expected)
|
||||
})
|
||||
|
||||
describe('with a duration', () => {
|
||||
|
@ -79,7 +77,7 @@ describe('Shared.Annotations.Helpers', () => {
|
|||
}
|
||||
|
||||
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', () => {
|
||||
|
@ -96,7 +94,7 @@ describe('Shared.Annotations.Helpers', () => {
|
|||
const actual = visibleAnnotations(graph, withDurations)
|
||||
const expected = withDurations
|
||||
|
||||
expect(actual).to.deep.equal(expected)
|
||||
expect(actual).toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -30,12 +30,12 @@ const state = {
|
|||
annotations: [],
|
||||
}
|
||||
|
||||
describe.only('Shared.Reducers.annotations', () => {
|
||||
describe('Shared.Reducers.annotations', () => {
|
||||
it('can load the annotations', () => {
|
||||
const expected = [{time: '0', duration: ''}]
|
||||
const actual = reducer(state, loadAnnotations(expected))
|
||||
|
||||
expect(actual.annotations).to.deep.equal(expected)
|
||||
expect(actual.annotations).toEqual(expected)
|
||||
})
|
||||
|
||||
it('can update an annotation', () => {
|
||||
|
@ -45,7 +45,7 @@ describe.only('Shared.Reducers.annotations', () => {
|
|||
updateAnnotation(expected[0])
|
||||
)
|
||||
|
||||
expect(actual.annotations).to.deep.equal(expected)
|
||||
expect(actual.annotations).toEqual(expected)
|
||||
})
|
||||
|
||||
it('can delete an annotation', () => {
|
||||
|
@ -55,13 +55,13 @@ describe.only('Shared.Reducers.annotations', () => {
|
|||
deleteAnnotation(a1)
|
||||
)
|
||||
|
||||
expect(actual.annotations).to.deep.equal(expected)
|
||||
expect(actual.annotations).toEqual(expected)
|
||||
})
|
||||
|
||||
it('can add an annotation', () => {
|
||||
const expected = [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,
|
||||
}
|
||||
|
||||
expect(actual.cell).to.equal(expected.cell)
|
||||
expect(actual.gaugeColors).to.equal(expected.gaugeColors)
|
||||
expect(actual.singleStatColors).to.equal(expected.singleStatColors)
|
||||
expect(actual.singleStatType).to.equal(expected.singleStatType)
|
||||
expect(actual.cell).toBe(expected.cell)
|
||||
expect(actual.gaugeColors).toBe(expected.gaugeColors)
|
||||
expect(actual.singleStatColors).toBe(expected.singleStatColors)
|
||||
expect(actual.singleStatType).toBe(expected.singleStatType)
|
||||
})
|
||||
|
||||
it('should hide cell editor overlay', () => {
|
||||
const actual = reducer(initialState, hideCellEditorOverlay)
|
||||
const expected = null
|
||||
|
||||
expect(actual.cell).to.equal(expected)
|
||||
expect(actual.cell).toBe(expected)
|
||||
})
|
||||
|
||||
it('should change the cell editor visualization type', () => {
|
||||
const actual = reducer(initialState, changeCellType(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', () => {
|
||||
const actual = reducer(initialState, renameCell(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', () => {
|
||||
|
@ -88,7 +88,7 @@ describe('Dashboards.Reducers.cellEditorOverlay', () => {
|
|||
)
|
||||
const expected = defaultSingleStatColors
|
||||
|
||||
expect(actual.singleStatColors).to.equal(expected)
|
||||
expect(actual.singleStatColors).toBe(expected)
|
||||
})
|
||||
|
||||
it('should toggle the single stat type', () => {
|
||||
|
@ -98,20 +98,20 @@ describe('Dashboards.Reducers.cellEditorOverlay', () => {
|
|||
)
|
||||
const expected = defaultSingleStatType
|
||||
|
||||
expect(actual.singleStatType).to.equal(expected)
|
||||
expect(actual.singleStatType).toBe(expected)
|
||||
})
|
||||
|
||||
it('should update the cell gauge colors', () => {
|
||||
const actual = reducer(initialState, updateGaugeColors(defaultGaugeColors))
|
||||
const expected = defaultGaugeColors
|
||||
|
||||
expect(actual.gaugeColors).to.equal(expected)
|
||||
expect(actual.gaugeColors).toBe(expected)
|
||||
})
|
||||
|
||||
it('should update the cell axes', () => {
|
||||
const actual = reducer(initialState, updateAxes(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 {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 {mount} from 'enzyme'
|
||||
|
@ -38,13 +39,13 @@ const setup = (override = {}) => {
|
|||
describe('Components.Shared.Dropdown', () => {
|
||||
describe('rendering', () => {
|
||||
describe('initial render', () => {
|
||||
it('renders the dropdown menu button', () => {
|
||||
it('renders the <Dropdown/> button', () => {
|
||||
const {dropdown} = setup()
|
||||
|
||||
expect(dropdown.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('does not show the list', () => {
|
||||
it('does not show the <DropdownMenu/> list', () => {
|
||||
const {dropdown} = setup({items})
|
||||
|
||||
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('opening the <DropdownMenu/>', () => {
|
||||
it('shows the menu when clicked', () => {
|
||||
|
@ -77,22 +115,6 @@ describe('Components.Shared.Dropdown', () => {
|
|||
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)
|
||||
})
|
||||
})
|
||||
|
||||
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": {
|
||||
"types": ["node", "mocha", "chai", "lodash"],
|
||||
"types": ["node", "chai", "lodash", "enzyme", "react", "prop-types", "jest"],
|
||||
"target": "es6",
|
||||
"module": "es2015",
|
||||
"moduleResolution": "node",
|
||||
|
|
|
@ -17,15 +17,10 @@ const babelLoader = {
|
|||
loader: 'babel-loader',
|
||||
options: {
|
||||
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 = {
|
||||
node: {
|
||||
fs: 'empty',
|
||||
|
@ -96,6 +91,7 @@ module.exports = {
|
|||
include: path.resolve(__dirname, '..', 'src'),
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
{loader: 'thread-loader'},
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
|
|
|
@ -5,7 +5,6 @@ const ExtractTextPlugin = require('extract-text-webpack-plugin')
|
|||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
|
||||
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
|
||||
const CompressionPlugin = require('compression-webpack-plugin')
|
||||
|
||||
const package = require('../package.json')
|
||||
const dependencies = package.dependencies
|
||||
|
@ -14,7 +13,7 @@ const babelLoader = {
|
|||
loader: 'babel-loader',
|
||||
options: {
|
||||
cacheDirectory: false,
|
||||
presets: ['env', 'react', 'stage-0'],
|
||||
presets: [['env', {modules: false}], 'react', 'stage-0'],
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -149,13 +148,6 @@ const config = {
|
|||
new webpack.optimize.CommonsChunkPlugin({
|
||||
name: 'manifest',
|
||||
}),
|
||||
new CompressionPlugin({
|
||||
asset: '[path].gz[query]',
|
||||
algorithm: 'gzip',
|
||||
test: /\.js$|\.css$/,
|
||||
threshold: 0,
|
||||
minRatio: 0.8,
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.resolve(__dirname, '..', 'src', 'index.template.html'),
|
||||
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