From d03cee05354ac8b102ee97645e2501a929395f53 Mon Sep 17 00:00:00 2001 From: Brandon Farmer Date: Thu, 7 Jun 2018 11:05:45 -0700 Subject: [PATCH 01/28] Message column width changes with window size --- ui/src/logs/components/LogsTable.tsx | 100 ++++++++++++++++++--------- 1 file changed, 68 insertions(+), 32 deletions(-) diff --git a/ui/src/logs/components/LogsTable.tsx b/ui/src/logs/components/LogsTable.tsx index d761a3c205..c2f6964a00 100644 --- a/ui/src/logs/components/LogsTable.tsx +++ b/ui/src/logs/components/LogsTable.tsx @@ -7,9 +7,7 @@ import {getDeep} from 'src/utils/wrappers' import FancyScrollbar from 'src/shared/components/FancyScrollbar' const ROW_HEIGHT = 26 -const ROW_CHAR_LIMIT = 100 -const CHAR_WIDTH = 7 - +const CHAR_WIDTH = 8 interface Props { data: { columns: string[] @@ -46,11 +44,14 @@ class LogsTable extends Component { } private grid: React.RefObject + private headerGrid: React.RefObject + private currentMessageWidth: number | null constructor(props: Props) { super(props) this.grid = React.createRef() + this.headerGrid = React.createRef() this.state = { scrollTop: 0, @@ -61,6 +62,15 @@ class LogsTable extends Component { public componentDidUpdate() { this.grid.current.recomputeGridSize() + this.headerGrid.current.recomputeGridSize() + } + + public componentDidMount() { + window.addEventListener('resize', this.handleWindowResize) + } + + public componentWillUnmount() { + window.removeEventListener('resize', this.handleWindowResize) } public render() { @@ -75,6 +85,7 @@ class LogsTable extends Component { {({width}) => ( { ) } + private handleWindowResize = () => { + this.currentMessageWidth = null + this.grid.current.recomputeGridSize() + this.headerGrid.current.recomputeGridSize() + } + private handleHeaderScroll = ({scrollLeft}) => this.setState({scrollLeft}) private handleScrollbarScroll = (e: MouseEvent) => { @@ -127,15 +144,55 @@ class LogsTable extends Component { this.handleScroll(target) } + private get widthMapping() { + return { + timestamp: 160, + procid: 80, + facility: 120, + severity: 22, + severity_1: 120, + } + } + + private get messageWidth() { + if (this.currentMessageWidth) { + return this.currentMessageWidth + } + + const columns = getDeep(this.props, 'data.columns', []) + const otherWidth = columns.reduce((acc, col) => { + if (col === 'message' || col === 'time') { + return acc + } + + return acc + _.get(this.widthMapping, col, 200) + }, 0) + + const calculatedWidth = window.innerWidth - (otherWidth + 180) + this.currentMessageWidth = Math.max(100 * CHAR_WIDTH, calculatedWidth) + + return this.currentMessageWidth - CHAR_WIDTH + } + + private getColumnWidth = ({index}: {index: number}) => { + const column = getDeep(this.props, `data.columns.${index + 1}`, '') + + switch (column) { + case 'message': + return this.messageWidth + default: + return _.get(this.widthMapping, column, 200) + } + } + private calculateMessageHeight = (index: number): number => { - const columnIndex = this.props.data.columns.indexOf('message') - const height = - (Math.floor( - this.props.data.values[index][columnIndex].length / ROW_CHAR_LIMIT - ) + - 1) * - ROW_HEIGHT - return height + const columns = getDeep(this.props, 'data.columns', []) + const columnIndex = columns.indexOf('message') + const value = getDeep(this.props, `data.values.${index}.${columnIndex}`, '') + const ROW_CHAR_LIMIT = Math.floor(this.messageWidth / CHAR_WIDTH) + const lines = Math.ceil(value.length / ROW_CHAR_LIMIT) + + return Math.max(lines, 1) * (ROW_HEIGHT - 14) + 14 } private calculateTotalHeight = (): number => { @@ -181,27 +238,6 @@ class LogsTable extends Component { } } - private getColumnWidth = ({index}: {index: number}) => { - const column = getDeep(this.props, `data.columns.${index + 1}`, '') - - switch (column) { - case 'message': - return ROW_CHAR_LIMIT * CHAR_WIDTH - case 'timestamp': - return 160 - case 'procid': - return 80 - case 'facility': - return 120 - case 'severity_1': - return 80 - case 'severity': - return 22 - default: - return 200 - } - } - private header(key: string): string { return getDeep( { From 4b92b84b584d7cc4ad2b2e048fd02e3cadc335a2 Mon Sep 17 00:00:00 2001 From: Alex P Date: Thu, 7 Jun 2018 11:57:25 -0700 Subject: [PATCH 02/28] Wrap body builder in fancy scrollbars --- ui/src/flux/components/BodyBuilder.tsx | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/ui/src/flux/components/BodyBuilder.tsx b/ui/src/flux/components/BodyBuilder.tsx index e06965628b..bc2c8f9c95 100644 --- a/ui/src/flux/components/BodyBuilder.tsx +++ b/ui/src/flux/components/BodyBuilder.tsx @@ -1,6 +1,7 @@ import React, {PureComponent} from 'react' import _ from 'lodash' +import FancyScrollbar from 'src/shared/components/FancyScrollbar' import ExpressionNode from 'src/flux/components/ExpressionNode' import VariableName from 'src/flux/components/VariableName' import FuncSelector from 'src/flux/components/FuncSelector' @@ -62,18 +63,20 @@ class BodyBuilder extends PureComponent { }) return ( -
- {_.flatten(bodybuilder)} -
- + +
+ {_.flatten(bodybuilder)} +
+ +
-
+ ) } From 09b44edfe1599b2d8a5252090b58e1d87942da59 Mon Sep 17 00:00:00 2001 From: Alex P Date: Thu, 7 Jun 2018 11:57:48 -0700 Subject: [PATCH 03/28] Redesign flux builder to stack nodes vertically --- ui/src/flux/components/FuncArgsPreview.tsx | 10 +- ui/src/flux/components/VariableName.tsx | 8 +- .../time-machine/add-func-button.scss | 19 ++-- .../components/time-machine/flux-builder.scss | 101 ++++++++++++------ 4 files changed, 91 insertions(+), 47 deletions(-) diff --git a/ui/src/flux/components/FuncArgsPreview.tsx b/ui/src/flux/components/FuncArgsPreview.tsx index ac8c5a56cc..07b928230b 100644 --- a/ui/src/flux/components/FuncArgsPreview.tsx +++ b/ui/src/flux/components/FuncArgsPreview.tsx @@ -76,19 +76,19 @@ export default class FuncArgsPreview extends PureComponent { case 'period': case 'duration': case 'array': { - return {argument} + return {argument} } case 'bool': { - return {argument} + return {argument} } case 'string': { - return "{argument}" + return "{argument}" } case 'object': { - return {argument} + return {argument} } case 'invalid': { - return {argument} + return {argument} } default: { return {argument} diff --git a/ui/src/flux/components/VariableName.tsx b/ui/src/flux/components/VariableName.tsx index d07f4b3ab9..ca92a73070 100644 --- a/ui/src/flux/components/VariableName.tsx +++ b/ui/src/flux/components/VariableName.tsx @@ -22,7 +22,7 @@ export default class VariableName extends PureComponent { } public render() { - return
{this.nameElement}
+ return
{this.nameElement}
} private get nameElement(): JSX.Element { @@ -32,7 +32,7 @@ export default class VariableName extends PureComponent { return this.colorizeSyntax } - return {name} + return {name} } private get colorizeSyntax(): JSX.Element { @@ -45,11 +45,11 @@ export default class VariableName extends PureComponent { return ( <> - {varName} + {varName} {' = '} {varValue} diff --git a/ui/src/style/components/time-machine/add-func-button.scss b/ui/src/style/components/time-machine/add-func-button.scss index 0be530b189..e0dfaec063 100644 --- a/ui/src/style/components/time-machine/add-func-button.scss +++ b/ui/src/style/components/time-machine/add-func-button.scss @@ -10,25 +10,28 @@ $flux-func-selector--height: 30px; display: flex; align-items: center; position: relative; + flex-direction: column; &.open { z-index: 9999; + height: $flux-func-selector--height + $flux-func-selector--gap; } } .func-selector--connector { - width: $flux-func-selector--gap; - height: $flux-func-selector--height; + width: $flux-node-gap; + height: $flux-func-selector--gap; position: relative; &:after { content: ''; position: absolute; - top: 50%; - width: 100%; - height: 4px; - transform: translateY(-50%); - @include gradient-h($g4-onyx, $c-pool); + top: -130%; + width: $flux-connector-line; + left: 50%; + height: 230%; + transform: translateX(-50%); + @include gradient-v($g4-onyx, $c-pool); } } @@ -51,7 +54,7 @@ $flux-func-selector--height: 30px; top: 0; .func-selector--connector + & { - left: $flux-func-selector--gap; + top: $flux-func-selector--gap; } } diff --git a/ui/src/style/components/time-machine/flux-builder.scss b/ui/src/style/components/time-machine/flux-builder.scss index fa5bade6cf..b5a0c69ec7 100644 --- a/ui/src/style/components/time-machine/flux-builder.scss +++ b/ui/src/style/components/time-machine/flux-builder.scss @@ -1,18 +1,23 @@ +/* + Flux Builder Styles + ------------------------------------------------------------------------------ +*/ + +$flux-builder-min-width: 440px; $flux-node-height: 30px; $flux-node-tooltip-gap: 4px; -$flux-node-gap: 5px; +$flux-connector-line: 2px; +$flux-node-gap: 30px; $flux-node-padding: 10px; $flux-arg-min-width: 120px; + $flux-number-color: $c-neutrino; $flux-object-color: $c-viridian; $flux-string-color: $c-honeydew; $flux-boolean-color: $c-viridian; $flux-invalid-color: $c-viridian; -/* - Shared Node styles - ------------------ -*/ +// Shared Node styles %flux-node { min-height: $flux-node-height; border-radius: $radius; @@ -22,66 +27,81 @@ $flux-invalid-color: $c-viridian; position: relative; background-color: $g4-onyx; transition: background-color 0.25s ease; + margin-bottom: $flux-node-tooltip-gap / 2; + margin-top: $flux-node-tooltip-gap / 2; &:hover { + cursor: pointer; background-color: $g6-smoke; } } -.body-builder { - padding: 30px; - min-width: 440px; - overflow: hidden; - height: 100%; - width: 100%; +.body-builder--container { background-color: $g1-raven; } +.body-builder { + padding: $flux-node-height; + padding-bottom: 0; + min-width: $flux-builder-min-width; + width: 100%; +} .declaration { width: 100%; - margin-bottom: 24px; + margin-bottom: $flux-node-gap; + padding-bottom: $flux-node-gap; + border-bottom: 2px solid $g2-kevlar; display: flex; flex-wrap: nowrap; - align-items: center; + flex-direction: column; + align-items: flex-start; &:last-of-type { margin-bottom: 0; + border: 0; } } -.variable-string { +.variable-node { @extend %flux-node; color: $g11-sidewalk; line-height: $flux-node-height; white-space: nowrap; @include no-user-select(); + margin-top: 0; + + &:hover { + background-color: $g4-onyx; + cursor: default; + } } -.variable-blank { - font-style: italic; -} - -.variable-name { +.variable-node--name { color: $c-pool; } -.variable-value--string { +.variable-node--string, +.func-arg--string { color: $flux-string-color; } -.variable-value--boolean { +.variable-node--boolean, +.func-arg--boolean { color: $flux-boolean-color; } -.variable-value--number { +.variable-node--number, +.func-arg--number { color: $flux-number-color; } -.variable-value--object { +.variable-node--object, +.func-arg--object { color: $flux-object-color; } -.variable-value--invalid { +.variable-node--invalid, +.func-arg--invalid { color: $flux-invalid-color; } @@ -92,21 +112,42 @@ $flux-invalid-color: $c-viridian; margin-left: $flux-node-gap; - // Connection Line + // Connection Lines + &:before, &:after { content: ''; - height: 4px; - width: $flux-node-gap; background-color: $g4-onyx; position: absolute; + } + // Vertical Line + &:before { + width: $flux-connector-line; + height: calc(100% + #{$flux-node-tooltip-gap}); + top: -$flux-node-tooltip-gap / 2; + left: -$flux-node-gap / 2; + transform: translateX(-50%); + } + // Horizontal Line + &:after { + height: $flux-connector-line; + width: $flux-node-gap / 2; top: 50%; left: 0; transform: translate(-100%, -50%); } - - &:first-child:after { - content: none; + + // When there is no variable name for the declaration + &:first-child { margin-left: 0; + + &:before { + left: $flux-node-gap / 2; + top: 100%; + height: $flux-node-tooltip-gap / 2; + } + &:after { + content: none; + } } } From a754311b0592f49497b681e08e51f76fc7ff66c4 Mon Sep 17 00:00:00 2001 From: Christopher Henn Date: Thu, 7 Jun 2018 14:13:12 -0700 Subject: [PATCH 04/28] Only parse first 2MB of Flux query responses --- ui/src/flux/apis/index.ts | 70 ++++++++++++++++++--- ui/src/flux/constants/index.ts | 13 +++- ui/src/flux/containers/FluxPage.tsx | 10 ++- ui/src/shared/copy/notifications.ts | 11 ++++ ui/src/shared/parsing/flux/response.ts | 6 ++ ui/test/shared/parsing/flux/constants.ts | 12 ++++ ui/test/shared/parsing/flux/results.test.ts | 9 +++ 7 files changed, 120 insertions(+), 11 deletions(-) diff --git a/ui/src/flux/apis/index.ts b/ui/src/flux/apis/index.ts index 2a0c3114ea..4487294d7d 100644 --- a/ui/src/flux/apis/index.ts +++ b/ui/src/flux/apis/index.ts @@ -4,6 +4,7 @@ import AJAX from 'src/utils/ajax' import {Service, FluxTable} from 'src/types' import {updateService} from 'src/shared/apis' import {parseResponse} from 'src/shared/parsing/flux/response' +import {MAX_RESPONSE_BYTES} from 'src/flux/constants' export const getSuggestions = async (url: string) => { try { @@ -39,23 +40,36 @@ export const getAST = async (request: ASTRequest) => { } } +interface GetTimeSeriesResult { + didTruncate: boolean + tables: FluxTable[] +} + export const getTimeSeries = async ( service: Service, script: string -): Promise => { +): Promise => { const and = encodeURIComponent('&') const mark = encodeURIComponent('?') const garbage = script.replace(/\s/g, '') // server cannot handle whitespace + const url = `${ + service.links.proxy + }?path=/v1/query${mark}orgName=defaulorgname${and}q=${garbage}` try { - const {data} = await AJAX({ - method: 'POST', - url: `${ - service.links.proxy - }?path=/v1/query${mark}orgName=defaulorgname${and}q=${garbage}`, - }) + // We are using the `fetch` API here since the `AJAX` utility lacks support + // for limiting response size. The `AJAX` utility depends on + // `axios.request` which _does_ have a `maxContentLength` option, though it + // seems to be broken at the moment. We might use this option instead of + // the `fetch` API in the future, if it is ever fixed. See + // https://github.com/axios/axios/issues/1491. + const resp = await fetch(url, {method: 'POST'}) + const {body, byteLength} = await decodeFluxRespWithLimit(resp) - return parseResponse(data) + return { + tables: parseResponse(body), + didTruncate: byteLength >= MAX_RESPONSE_BYTES, + } } catch (error) { console.error('Problem fetching data', error) @@ -114,3 +128,43 @@ export const updateScript = async (service: Service, script: string) => { throw error } } + +interface DecodeFluxRespWithLimitResult { + body: string + byteLength: number +} + +const decodeFluxRespWithLimit = async ( + resp: Response +): Promise => { + const reader = resp.body.getReader() + const decoder = new TextDecoder() + + let bytesRead = 0 + let body = '' + let currentRead = await reader.read() + + while (!currentRead.done) { + const currentText = decoder.decode(currentRead.value) + + bytesRead += currentRead.value.byteLength + + if (bytesRead >= MAX_RESPONSE_BYTES) { + // Discard last line since it may be partially read + const lines = currentText.split('\n') + body += lines.slice(0, lines.length - 1).join('\n') + + reader.cancel() + + return {body, byteLength: bytesRead} + } else { + body += currentText + } + + currentRead = await reader.read() + } + + reader.cancel() + + return {body, byteLength: bytesRead} +} diff --git a/ui/src/flux/constants/index.ts b/ui/src/flux/constants/index.ts index b6e09e948f..dd7f0fad1e 100644 --- a/ui/src/flux/constants/index.ts +++ b/ui/src/flux/constants/index.ts @@ -6,4 +6,15 @@ import * as builder from 'src/flux/constants/builder' import * as vis from 'src/flux/constants/vis' import * as explorer from 'src/flux/constants/explorer' -export {ast, funcNames, argTypes, editor, builder, vis, explorer} +const MAX_RESPONSE_BYTES = 2e6 // 2 MB + +export { + ast, + funcNames, + argTypes, + editor, + builder, + vis, + explorer, + MAX_RESPONSE_BYTES, +} diff --git a/ui/src/flux/containers/FluxPage.tsx b/ui/src/flux/containers/FluxPage.tsx index 62656d41ac..6ca4d87f3f 100644 --- a/ui/src/flux/containers/FluxPage.tsx +++ b/ui/src/flux/containers/FluxPage.tsx @@ -9,6 +9,7 @@ import KeyboardShortcuts from 'src/shared/components/KeyboardShortcuts' import { analyzeSuccess, fluxTimeSeriesError, + fluxResponseTruncatedError, } from 'src/shared/copy/notifications' import {UpdateScript} from 'src/flux/actions' @@ -439,8 +440,13 @@ export class FluxPage extends PureComponent { } try { - const data = await getTimeSeries(this.service, script) - this.setState({data}) + const {tables, didTruncate} = await getTimeSeries(this.service, script) + + this.setState({data: tables}) + + if (didTruncate) { + notify(fluxResponseTruncatedError()) + } } catch (error) { this.setState({data: []}) diff --git a/ui/src/shared/copy/notifications.ts b/ui/src/shared/copy/notifications.ts index 8713980b1f..bbdbb765ff 100644 --- a/ui/src/shared/copy/notifications.ts +++ b/ui/src/shared/copy/notifications.ts @@ -2,6 +2,7 @@ // and ensuring stylistic consistency import {FIVE_SECONDS, TEN_SECONDS, INFINITE} from 'src/shared/constants/index' +import {MAX_RESPONSE_BYTES} from 'src/flux/constants' const defaultErrorNotification = { type: 'error', @@ -680,3 +681,13 @@ export const fluxTimeSeriesError = (message: string) => ({ ...defaultErrorNotification, message: `Could not get data: ${message}`, }) + +export const fluxResponseTruncatedError = () => { + const BYTES_TO_MB = 1 / 1e6 + const APPROX_MAX_RESPONSE_MB = +(MAX_RESPONSE_BYTES * BYTES_TO_MB).toFixed(2) + + return { + ...defaultErrorNotification, + message: `Large response truncated to first ${APPROX_MAX_RESPONSE_MB} MB`, + } +} diff --git a/ui/src/shared/parsing/flux/response.ts b/ui/src/shared/parsing/flux/response.ts index 28f59e6915..5f9f369a67 100644 --- a/ui/src/shared/parsing/flux/response.ts +++ b/ui/src/shared/parsing/flux/response.ts @@ -29,6 +29,12 @@ export const parseTables = (responseChunk: string): FluxTable[] => { throw new Error('Unable to extract annotation data') } + if (_.isEmpty(nonAnnotationLines)) { + // A response may be truncated on an arbitrary line. This guards against + // the case where a response is truncated on annotation data + return [] + } + const nonAnnotationData = Papa.parse(nonAnnotationLines).data const annotationData = Papa.parse(annotationLines).data const headerRow = nonAnnotationData[0] diff --git a/ui/test/shared/parsing/flux/constants.ts b/ui/test/shared/parsing/flux/constants.ts index 6d121636cd..451e5f38ce 100644 --- a/ui/test/shared/parsing/flux/constants.ts +++ b/ui/test/shared/parsing/flux/constants.ts @@ -134,3 +134,15 @@ export const CSV_TO_DYGRAPH_MISMATCHED = ` ,,1,2018-06-04T17:12:21.025984999Z,2018-06-04T17:13:00Z,2018-06-05T17:12:25Z,10,available,mem,bertrand.local ,,1,2018-06-04T17:12:21.025984999Z,2018-06-04T17:13:00Z,2018-06-05T17:12:35Z,11,available,mem,bertrand.local ` + +export const TRUNCATED_RESPONSE = ` +#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,double,string,string,string,string +#partition,false,false,false,false,false,false,true,true,true,true +#default,_result,,,,,,,,, +,result,table,_start,_stop,_time,_value,_field,_measurement,cpu,host +,,0,1677-09-21T00:12:43.145224192Z,2018-05-22T22:39:17.042276772Z,2018-05-22T22:39:12.584Z,0,usage_guest,cpu,cpu-total,WattsInfluxDB +,,1,1677-09-21T00:12:43.145224192Z,2018-05-22T22:39:17.042276772Z,2018-05-22T22:39:12.584Z,0,usage_guest_nice,cpu,cpu-total,WattsInfluxDB + +#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,long,string,string,string,string,string,string,string +#partition,false,false,false,false,false,false,true,true,true,true,true,true,true +#default,_result,,,,,,,,,,,,` diff --git a/ui/test/shared/parsing/flux/results.test.ts b/ui/test/shared/parsing/flux/results.test.ts index 34c514e1d3..7e0758aef8 100644 --- a/ui/test/shared/parsing/flux/results.test.ts +++ b/ui/test/shared/parsing/flux/results.test.ts @@ -4,6 +4,7 @@ import { RESPONSE_METADATA, MULTI_SCHEMA_RESPONSE, EXPECTED_COLUMNS, + TRUNCATED_RESPONSE, } from 'test/shared/parsing/flux/constants' describe('Flux results parser', () => { @@ -37,4 +38,12 @@ describe('Flux results parser', () => { expect(actual).toEqual(expected) }) }) + + describe('partial responses', () => { + it('should discard tables without any non-annotation rows', () => { + const actual = parseResponse(TRUNCATED_RESPONSE) + + expect(actual).toHaveLength(2) + }) + }) }) From 5645764478d0504320a03809644c02bcdef862d9 Mon Sep 17 00:00:00 2001 From: Christopher Henn Date: Thu, 7 Jun 2018 14:29:02 -0700 Subject: [PATCH 05/28] Improve scrolling performance in TimeMachineTable --- ui/src/flux/components/TimeMachineTable.tsx | 52 +++++++++++++-------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/ui/src/flux/components/TimeMachineTable.tsx b/ui/src/flux/components/TimeMachineTable.tsx index 344804ec68..54964086d5 100644 --- a/ui/src/flux/components/TimeMachineTable.tsx +++ b/ui/src/flux/components/TimeMachineTable.tsx @@ -8,25 +8,46 @@ import {vis} from 'src/flux/constants' const NUM_FIXED_ROWS = 1 +const filterTable = (table: FluxTable): FluxTable => { + const IGNORED_COLUMNS = ['', 'result', 'table', '_start', '_stop'] + const header = table.data[0] + const indices = IGNORED_COLUMNS.map(name => header.indexOf(name)) + const data = table.data.map(row => + row.filter((__, i) => !indices.includes(i)) + ) + + return { + ...table, + data, + } +} + interface Props { table: FluxTable } interface State { scrollLeft: number + filteredTable: FluxTable } @ErrorHandling export default class TimeMachineTable extends PureComponent { + public static getDerivedStateFromProps({table}: Props) { + return {filteredTable: filterTable(table)} + } + constructor(props) { super(props) + this.state = { scrollLeft: 0, + filteredTable: filterTable(props.table), } } public render() { - const {scrollLeft} = this.state + const {scrollLeft, filteredTable} = this.state return (
@@ -73,7 +94,7 @@ export default class TimeMachineTable extends PureComponent { cellRenderer={this.cellRenderer} rowHeight={vis.TABLE_ROW_HEIGHT} height={height - this.headerOffset} - rowCount={this.table.data.length - NUM_FIXED_ROWS} + rowCount={filteredTable.data.length - NUM_FIXED_ROWS} /> )} @@ -93,7 +114,9 @@ export default class TimeMachineTable extends PureComponent { } private get columnCount(): number { - return _.get(this.table, 'data.0', []).length + const {filteredTable} = this.state + + return _.get(filteredTable, 'data.0', []).length } private get headerOffset(): number { @@ -109,13 +132,15 @@ export default class TimeMachineTable extends PureComponent { key, style, }: GridCellProps): React.ReactNode => { + const {filteredTable} = this.state + return (
- {this.table.data[0][columnIndex]} + {filteredTable.data[0][columnIndex]}
) } @@ -126,25 +151,12 @@ export default class TimeMachineTable extends PureComponent { rowIndex, style, }: GridCellProps): React.ReactNode => { + const {filteredTable} = this.state + return (
- {this.table.data[rowIndex + NUM_FIXED_ROWS][columnIndex]} + {filteredTable.data[rowIndex + NUM_FIXED_ROWS][columnIndex]}
) } - - private get table(): FluxTable { - const IGNORED_COLUMNS = ['', 'result', 'table', '_start', '_stop'] - const {table} = this.props - const header = table.data[0] - const indices = IGNORED_COLUMNS.map(name => header.indexOf(name)) - const data = table.data.map(row => - row.filter((__, i) => !indices.includes(i)) - ) - - return { - ...table, - data, - } - } } From fd2a489a918be00d243a89d78ee177c5c7b70266 Mon Sep 17 00:00:00 2001 From: Christopher Henn Date: Thu, 7 Jun 2018 14:31:51 -0700 Subject: [PATCH 06/28] Increase max response limit --- ui/src/flux/constants/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/flux/constants/index.ts b/ui/src/flux/constants/index.ts index dd7f0fad1e..81695e650d 100644 --- a/ui/src/flux/constants/index.ts +++ b/ui/src/flux/constants/index.ts @@ -6,7 +6,7 @@ import * as builder from 'src/flux/constants/builder' import * as vis from 'src/flux/constants/vis' import * as explorer from 'src/flux/constants/explorer' -const MAX_RESPONSE_BYTES = 2e6 // 2 MB +const MAX_RESPONSE_BYTES = 1e7 // 10 MB export { ast, From 944c4ed80318d1f693cedda1a4aff14abf35c2f1 Mon Sep 17 00:00:00 2001 From: Alex P Date: Thu, 7 Jun 2018 14:37:42 -0700 Subject: [PATCH 07/28] Add connector dot to variables when they are assigned to queries --- ui/src/flux/components/BodyBuilder.tsx | 4 +-- ui/src/flux/components/VariableName.tsx | 15 ++++++++--- .../components/time-machine/flux-builder.scss | 27 +++++++++++++++++++ 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/ui/src/flux/components/BodyBuilder.tsx b/ui/src/flux/components/BodyBuilder.tsx index bc2c8f9c95..707656f517 100644 --- a/ui/src/flux/components/BodyBuilder.tsx +++ b/ui/src/flux/components/BodyBuilder.tsx @@ -30,7 +30,7 @@ class BodyBuilder extends PureComponent { if (d.funcs) { return (
- + { return (
- +
) }) diff --git a/ui/src/flux/components/VariableName.tsx b/ui/src/flux/components/VariableName.tsx index ca92a73070..f8c150720e 100644 --- a/ui/src/flux/components/VariableName.tsx +++ b/ui/src/flux/components/VariableName.tsx @@ -1,7 +1,8 @@ import React, {PureComponent} from 'react' interface Props { - name?: string + name: string + assignedToQuery: boolean } interface State { @@ -10,7 +11,7 @@ interface State { export default class VariableName extends PureComponent { public static defaultProps: Partial = { - name: '', + assignedToQuery: false, } constructor(props) { @@ -22,7 +23,14 @@ export default class VariableName extends PureComponent { } public render() { - return
{this.nameElement}
+ const {assignedToQuery} = this.props + + return ( +
+ {assignedToQuery &&
} + {this.nameElement} +
+ ) } private get nameElement(): JSX.Element { @@ -42,7 +50,6 @@ export default class VariableName extends PureComponent { const varValue = this.props.name.replace(/^[^=]+=/, '') const valueIsString = varValue.endsWith('"') - return ( <> {varName} diff --git a/ui/src/style/components/time-machine/flux-builder.scss b/ui/src/style/components/time-machine/flux-builder.scss index b5a0c69ec7..aa657ca628 100644 --- a/ui/src/style/components/time-machine/flux-builder.scss +++ b/ui/src/style/components/time-machine/flux-builder.scss @@ -76,8 +76,35 @@ $flux-invalid-color: $c-viridian; } } +.variable-node--connector { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: $c-pool; + position: absolute; + z-index: 2; + top: 50%; + left: $flux-node-gap / 2; + transform: translate(-50%, -50%); + + &:before { + content: ''; + position: absolute; + top: 100%; + left: 50%; + width: $flux-connector-line; + height: $flux-node-gap; + @include gradient-v($c-pool, $g4-onyx); + transform: translateX(-50%); + } +} + .variable-node--name { color: $c-pool; + + .variable-node--connector + & { + margin-left: $flux-node-gap - $flux-node-padding; + } } .variable-node--string, From 278c147089c778d76419c1faa696b4701f3a9cec Mon Sep 17 00:00:00 2001 From: Alex P Date: Thu, 7 Jun 2018 14:54:37 -0700 Subject: [PATCH 08/28] Add connector line from unassigned FROM to following node --- ui/src/flux/components/FuncNode.tsx | 1 + .../components/time-machine/flux-builder.scss | 45 ++++++++++++++----- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/ui/src/flux/components/FuncNode.tsx b/ui/src/flux/components/FuncNode.tsx index 8648425083..7e2fbe5bdf 100644 --- a/ui/src/flux/components/FuncNode.tsx +++ b/ui/src/flux/components/FuncNode.tsx @@ -52,6 +52,7 @@ export default class FuncNode extends PureComponent { onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} > +
{func.name}
{isExpanded && ( diff --git a/ui/src/style/components/time-machine/flux-builder.scss b/ui/src/style/components/time-machine/flux-builder.scss index aa657ca628..df77998c14 100644 --- a/ui/src/style/components/time-machine/flux-builder.scss +++ b/ui/src/style/components/time-machine/flux-builder.scss @@ -136,8 +136,17 @@ $flux-invalid-color: $c-viridian; @extend %flux-node; display: flex; align-items: center; - margin-left: $flux-node-gap; +} + +.func-node--connector { + width: $flux-node-gap; + height: 100%; + position: absolute; + top: 0; + left: 0; + transform: translateX(-100%); + z-index: 0; // Connection Lines &:before, @@ -151,7 +160,7 @@ $flux-invalid-color: $c-viridian; width: $flux-connector-line; height: calc(100% + #{$flux-node-tooltip-gap}); top: -$flux-node-tooltip-gap / 2; - left: -$flux-node-gap / 2; + left: $flux-node-gap / 2; transform: translateX(-50%); } // Horizontal Line @@ -159,21 +168,33 @@ $flux-invalid-color: $c-viridian; height: $flux-connector-line; width: $flux-node-gap / 2; top: 50%; - left: 0; - transform: translate(-100%, -50%); + left: $flux-node-gap / 2; + transform: translateY(-50%); } - - // When there is no variable name for the declaration - &:first-child { - margin-left: 0; +} +// When a query exists unassigned to a variable +.func-node:first-child { + margin-left: 0; + padding-left: $flux-node-gap; + .func-node--connector { + transform: translateX(0); + z-index: 2; + + // Vertical Line &:before { - left: $flux-node-gap / 2; - top: 100%; - height: $flux-node-tooltip-gap / 2; + height: $flux-node-gap; + top: $flux-node-gap / 2; + @include gradient-v($c-comet, $g4-onyx); } + // Dot &:after { - content: none; + width: 8px; + height: 8px; + border-radius: 50%; + background-color: $c-comet; + top: $flux-node-gap / 2; + transform: translate(-50%, -50%); } } } From 5e86a7b5e73cf110c22af1f2d1ee36ba29cacdfe Mon Sep 17 00:00:00 2001 From: Alex P Date: Thu, 7 Jun 2018 15:08:39 -0700 Subject: [PATCH 09/28] Position node tooltips to the right of the node --- .../components/time-machine/flux-builder.scss | 50 ++++++------------- 1 file changed, 16 insertions(+), 34 deletions(-) diff --git a/ui/src/style/components/time-machine/flux-builder.scss b/ui/src/style/components/time-machine/flux-builder.scss index df77998c14..9ded7300e5 100644 --- a/ui/src/style/components/time-machine/flux-builder.scss +++ b/ui/src/style/components/time-machine/flux-builder.scss @@ -228,8 +228,7 @@ $flux-invalid-color: $c-viridian; } } -.func-node--tooltip, -.variable-name--tooltip { +.func-node--tooltip { background-color: $g3-castle; border-radius: $radius; padding: 10px; @@ -237,28 +236,31 @@ $flux-invalid-color: $c-viridian; align-items: stretch; flex-direction: column; position: absolute; - top: calc(100% + #{$flux-node-tooltip-gap}); - left: 0; + top: 0; + left: calc(100% + #{$flux-node-tooltip-gap}); z-index: 9999; - box-shadow: 0 0 10px 2px $g2-kevlar; // Caret + box-shadow: 0 0 10px 2px $g2-kevlar; + + // Caret &:before { content: ''; border-width: 9px; border-style: solid; border-color: transparent; - border-bottom-color: $g3-castle; + border-right-color: $g3-castle; position: absolute; - top: 0; - left: $flux-node-padding + 3px; - transform: translate(-50%, -100%); - } // Invisible block to continue hovering + top: $flux-node-height / 2; + left: 0; + transform: translate(-100%, -50%); + } + // Invisible block to continue hovering &:after { content: ''; - width: 80%; - height: 7px; + height: 50%; + width: $flux-node-tooltip-gap * 3; position: absolute; - top: -7px; - left: 0; + top: 0; + left: -$flux-node-tooltip-gap * 3; } } @@ -301,26 +303,6 @@ $flux-invalid-color: $c-viridian; width: 300px; } -.variable-name--tooltip { - flex-direction: row; - align-items: center; - justify-content: space-between; - flex-wrap: nowrap; -} - -.variable-name--input { - width: 140px; -} - -.variable-name--operator { - width: 20px; - height: 30px; - text-align: center; - line-height: 30px; - font-weight: 600; - @include no-user-select(); -} - /* Filter Preview Styles ------------------------------------------------------------------------------ From 27d21914aefa9215e032a2a5b7f4da3e245b3411 Mon Sep 17 00:00:00 2001 From: Brandon Farmer Date: Thu, 7 Jun 2018 15:43:41 -0700 Subject: [PATCH 10/28] Selecting source is reflected in the dropdown --- ui/src/logs/components/LogViewerHeader.tsx | 11 +++++++- ui/src/logs/components/LogsSearchBar.tsx | 2 +- ui/src/logs/components/LogsTable.tsx | 29 ++++++++++++++++------ ui/src/logs/containers/LogsPage.tsx | 12 ++++++--- ui/src/logs/utils/index.ts | 2 +- ui/src/style/pages/logs-viewer.scss | 4 +++ 6 files changed, 46 insertions(+), 14 deletions(-) diff --git a/ui/src/logs/components/LogViewerHeader.tsx b/ui/src/logs/components/LogViewerHeader.tsx index c61068cba2..2b463746be 100644 --- a/ui/src/logs/components/LogViewerHeader.tsx +++ b/ui/src/logs/components/LogViewerHeader.tsx @@ -99,7 +99,16 @@ class LogViewerHeader extends PureComponent { return '' } - return this.sourceDropDownItems[0].text + const id = _.get(this.props, 'currentSource.id', '') + const currentItem = _.find(this.sourceDropDownItems, item => { + return item.id === id + }) + + if (currentItem) { + return currentItem.text + } + + return '' } private get selectedNamespace(): string { diff --git a/ui/src/logs/components/LogsSearchBar.tsx b/ui/src/logs/components/LogsSearchBar.tsx index a563ab28d3..cb1049b43d 100644 --- a/ui/src/logs/components/LogsSearchBar.tsx +++ b/ui/src/logs/components/LogsSearchBar.tsx @@ -26,7 +26,7 @@ class LogsSearchBar extends PureComponent {
{ facility: 120, severity: 22, severity_1: 120, + host: 300, } } @@ -185,12 +186,18 @@ class LogsTable extends Component { } } + private get rowCharLimit(): number { + return Math.floor(this.messageWidth / CHAR_WIDTH) + } + + private get columns(): string[] { + return getDeep(this.props, 'data.columns', []) + } + private calculateMessageHeight = (index: number): number => { - const columns = getDeep(this.props, 'data.columns', []) - const columnIndex = columns.indexOf('message') + const columnIndex = this.columns.indexOf('message') const value = getDeep(this.props, `data.values.${index}.${columnIndex}`, '') - const ROW_CHAR_LIMIT = Math.floor(this.messageWidth / CHAR_WIDTH) - const lines = Math.ceil(value.length / ROW_CHAR_LIMIT) + const lines = Math.round(value.length / this.rowCharLimit + 0.25) return Math.max(lines, 1) * (ROW_HEIGHT - 14) + 14 } @@ -287,7 +294,9 @@ class LogsTable extends Component { value = moment(+value / 1000000).format('YYYY/MM/DD HH:mm:ss') break case 'message': - value = _.replace(value, '\\n', '') + if (value.indexOf(' ') > this.rowCharLimit - 5) { + value = _.truncate(value, {length: this.rowCharLimit - 5}) + } break case 'severity': value = ( @@ -306,7 +315,7 @@ class LogsTable extends Component { if (this.isClickable(column)) { return (
{ data-tag-key={column} data-tag-value={value} onClick={this.handleTagClick} + data-index={rowIndex} + onMouseOver={this.handleMouseEnter} className="logs-viewer--clickable" > {value} @@ -329,7 +340,9 @@ class LogsTable extends Component { return (
{ ) } + private get isSpecificTimeRange(): boolean { + return !!this.props.timeRange.upper + } + private startUpdating = () => { if (this.interval) { clearInterval(this.interval) } - this.interval = setInterval(this.handleInterval, 10000) - this.setState({liveUpdating: true}) + if (!this.isSpecificTimeRange) { + this.interval = setInterval(this.handleInterval, 10000) + this.setState({liveUpdating: true}) + } } private handleScrollToTop = () => { @@ -180,7 +186,7 @@ class LogsPage extends PureComponent { return ( { const createGroupBy = (range: TimeRange) => { const seconds = computeSeconds(range) - const time = `${Math.floor(seconds / BIN_COUNT)}s` + const time = `${Math.max(Math.floor(seconds / BIN_COUNT), 1)}s` const tags = [] return {time, tags} diff --git a/ui/src/style/pages/logs-viewer.scss b/ui/src/style/pages/logs-viewer.scss index 2d771d4656..4790be2453 100644 --- a/ui/src/style/pages/logs-viewer.scss +++ b/ui/src/style/pages/logs-viewer.scss @@ -242,6 +242,10 @@ $logs-viewer-gutter: 60px; } } +.message--cell { + word-break: break-all; +} + // Table Cell Styles .logs-viewer--cell { font-size: 12px; From a9ab9e0c418c4ef5055ed683b16019f5c6c08696 Mon Sep 17 00:00:00 2001 From: Alex P Date: Thu, 7 Jun 2018 16:22:12 -0700 Subject: [PATCH 11/28] Align func node tooltip contents horizontally --- ui/src/flux/components/FuncArgs.tsx | 52 ++++++++++--------- .../components/time-machine/flux-builder.scss | 17 ++++-- 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/ui/src/flux/components/FuncArgs.tsx b/ui/src/flux/components/FuncArgs.tsx index f7bda83c7c..eedc342883 100644 --- a/ui/src/flux/components/FuncArgs.tsx +++ b/ui/src/flux/components/FuncArgs.tsx @@ -34,38 +34,40 @@ export default class FuncArgs extends PureComponent { const {name: funcName, id: funcID} = func return (
- {funcName === funcNames.JOIN ? ( - - ) : ( - func.args.map(({key, value, type}) => ( - + {funcName === funcNames.JOIN ? ( + - )) - )} -
+ ) : ( + func.args.map(({key, value, type}) => ( + + )) + )} +
+
- Delete +
{this.build}
diff --git a/ui/src/style/components/time-machine/flux-builder.scss b/ui/src/style/components/time-machine/flux-builder.scss index 9ded7300e5..5efb3bde62 100644 --- a/ui/src/style/components/time-machine/flux-builder.scss +++ b/ui/src/style/components/time-machine/flux-builder.scss @@ -234,7 +234,6 @@ $flux-invalid-color: $c-viridian; padding: 10px; display: flex; align-items: stretch; - flex-direction: column; position: absolute; top: 0; left: calc(100% + #{$flux-node-tooltip-gap}); @@ -264,22 +263,30 @@ $flux-invalid-color: $c-viridian; } } -.func-node--buttons { +.func-arg--buttons { display: flex; - margin-top: 12px; + flex-direction: column; + justify-content: center; + margin-left: 8px; } -.func-node--delete, .func-node--build { width: 60px; + margin-top: 4px; } -.func-node--sub .func-arg { +.func-args { + display: flex; + flex-direction: column; +} + +.func-arg { min-width: $flux-arg-min-width; display: flex; flex-wrap: nowrap; align-items: center; margin-bottom: 4px; + &:last-of-type { margin-bottom: 0; } From c0bbdee93efc1ffc441ca5195db65522f9098803 Mon Sep 17 00:00:00 2001 From: Christopher Henn Date: Thu, 7 Jun 2018 16:36:48 -0700 Subject: [PATCH 12/28] Fix misaligned kapacitor form --- ui/src/flux/components/FluxForm.tsx | 2 ++ ui/src/kapacitor/components/KapacitorFormInput.tsx | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/src/flux/components/FluxForm.tsx b/ui/src/flux/components/FluxForm.tsx index 056514b58f..a483d0a6df 100644 --- a/ui/src/flux/components/FluxForm.tsx +++ b/ui/src/flux/components/FluxForm.tsx @@ -24,6 +24,7 @@ class FluxForm extends PureComponent { value={this.url} placeholder={this.url} onChange={onInputChange} + customClass="col-sm-6" /> { placeholder={service.name} onChange={onInputChange} maxLength={33} + customClass="col-sm-6" />
) } @@ -313,7 +313,7 @@ export default class TagListItem extends PureComponent { return ( !isOpen && (loadingAll === RemoteDataState.NotStarted || - loadingAll !== RemoteDataState.Error) + loadingAll === RemoteDataState.Error) ) } diff --git a/ui/src/flux/components/TagValueListItem.tsx b/ui/src/flux/components/TagValueListItem.tsx index ffc88b61ac..19b992658d 100644 --- a/ui/src/flux/components/TagValueListItem.tsx +++ b/ui/src/flux/components/TagValueListItem.tsx @@ -48,7 +48,7 @@ class TagValueListItem extends PureComponent { public render() { const {db, service, value} = this.props - const {searchTerm} = this.state + const {searchTerm, isOpen} = this.state return (
@@ -65,35 +65,33 @@ class TagValueListItem extends PureComponent {
- {this.state.isOpen && ( -
- {this.isLoading && } - {!this.isLoading && ( - <> - {!!this.tags.length && ( -
- -
- )} - - - )} -
- )} +
+ {this.isLoading && } + {!this.isLoading && ( + <> + {!!this.tags.length && ( +
+ +
+ )} + + + )} +
) } @@ -177,7 +175,7 @@ class TagValueListItem extends PureComponent { return ( !isOpen && (loading === RemoteDataState.NotStarted || - loading !== RemoteDataState.Error) + loading === RemoteDataState.Error) ) } } diff --git a/ui/src/style/components/time-machine/flux-explorer.scss b/ui/src/style/components/time-machine/flux-explorer.scss index 635ff1dcce..36a9831102 100644 --- a/ui/src/style/components/time-machine/flux-explorer.scss +++ b/ui/src/style/components/time-machine/flux-explorer.scss @@ -27,6 +27,10 @@ $flux-tree-gutter: 11px; } } +.flux-schema--children.hidden { + display: none; +} + .flux-schema-tree__empty { height: $flux-tree-indent; display: flex; From ba62ec82b5d275aeffa590d27732ab478d1fc7a6 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Fri, 8 Jun 2018 15:04:24 -0700 Subject: [PATCH 23/28] Type dashboard test --- ui/src/dashboards/actions/index.ts | 131 ++--------------- .../components/CellEditorOverlay.tsx | 2 +- ui/src/dashboards/constants/index.ts | 2 +- ui/src/dashboards/containers/DashboardPage.js | 2 - ui/src/dashboards/reducers/ui.js | 85 +---------- ui/src/shared/components/LayoutCell.tsx | 2 +- ui/src/types/dashboard.ts | 2 +- ui/src/types/index.ts | 11 +- .../components/CellEditorOverlay.test.tsx | 2 +- ui/test/dashboards/reducers/ui.test.ts | 138 +++--------------- ui/test/fixtures/index.ts | 2 +- ui/test/resources.ts | 82 ++++++++++- 12 files changed, 133 insertions(+), 328 deletions(-) diff --git a/ui/src/dashboards/actions/index.ts b/ui/src/dashboards/actions/index.ts index cc4f99b525..511d29c51e 100644 --- a/ui/src/dashboards/actions/index.ts +++ b/ui/src/dashboards/actions/index.ts @@ -37,19 +37,19 @@ import {makeQueryForTemplate} from 'src/dashboards/utils/templateVariableQueryGe import parsers from 'src/shared/parsing' import {getDeep} from 'src/utils/wrappers' -import {Dashboard, TimeRange, Cell, Query, Source, Template} from 'src/types' +import {Dashboard, TimeRange, Cell, Source, Template} from 'src/types' interface LoadDashboardsAction { type: 'LOAD_DASHBOARDS' payload: { dashboards: Dashboard[] - dashboardID: string + dashboardID: number } } export const loadDashboards = ( dashboards: Dashboard[], - dashboardID?: string + dashboardID?: number ): LoadDashboardsAction => ({ type: 'LOAD_DASHBOARDS', payload: { @@ -61,12 +61,12 @@ export const loadDashboards = ( interface LoadDeafaultDashTimeV1Action { type: 'ADD_DASHBOARD_TIME_V1' payload: { - dashboardID: string + dashboardID: number } } export const loadDeafaultDashTimeV1 = ( - dashboardID: string + dashboardID: number ): LoadDeafaultDashTimeV1Action => ({ type: 'ADD_DASHBOARD_TIME_V1', payload: { @@ -77,13 +77,13 @@ export const loadDeafaultDashTimeV1 = ( interface AddDashTimeV1Action { type: 'ADD_DASHBOARD_TIME_V1' payload: { - dashboardID: string + dashboardID: number timeRange: TimeRange } } export const addDashTimeV1 = ( - dashboardID: string, + dashboardID: number, timeRange: TimeRange ): AddDashTimeV1Action => ({ type: 'ADD_DASHBOARD_TIME_V1', @@ -96,13 +96,13 @@ export const addDashTimeV1 = ( interface SetDashTimeV1Action { type: 'SET_DASHBOARD_TIME_V1' payload: { - dashboardID: string + dashboardID: number timeRange: TimeRange } } export const setDashTimeV1 = ( - dashboardID: string, + dashboardID: number, timeRange: TimeRange ): SetDashTimeV1Action => ({ type: 'SET_DASHBOARD_TIME_V1', @@ -192,25 +192,6 @@ export const deleteDashboardFailed = ( }, }) -interface UpdateDashboardCellsAction { - type: 'UPDATE_DASHBOARD_CELLS' - payload: { - dashboard: Dashboard - cells: Cell[] - } -} - -export const updateDashboardCells = ( - dashboard: Dashboard, - cells: Cell[] -): UpdateDashboardCellsAction => ({ - type: 'UPDATE_DASHBOARD_CELLS', - payload: { - dashboard, - cells, - }, -}) - interface SyncDashboardCellAction { type: 'SYNC_DASHBOARD_CELL' payload: { @@ -249,79 +230,6 @@ export const addDashboardCell = ( }, }) -interface EditDashboardCellAction { - type: 'EDIT_DASHBOARD_CELL' - payload: { - dashboard: Dashboard - x: number - y: number - isEditing: boolean - } -} - -export const editDashboardCell = ( - dashboard: Dashboard, - x: number, - y: number, - isEditing: boolean -): EditDashboardCellAction => ({ - type: 'EDIT_DASHBOARD_CELL', - // x and y coords are used as a alternative to cell ids, which are not - // universally unique, and cannot be because React depends on a - // quasi-predictable ID for keys. Since cells cannot overlap, coordinates act - // as a suitable id - payload: { - dashboard, - x, // x-coord of the cell to be edited - y, // y-coord of the cell to be edited - isEditing, - }, -}) - -interface CancelEditCellAction { - type: 'CANCEL_EDIT_CELL' - payload: { - dashboardID: string - cellID: string - } -} - -export const cancelEditCell = ( - dashboardID: string, - cellID: string -): CancelEditCellAction => ({ - type: 'CANCEL_EDIT_CELL', - payload: { - dashboardID, - cellID, - }, -}) - -interface RenameDashboardCellAction { - type: 'RENAME_DASHBOARD_CELL' - payload: { - dashboard: Dashboard - x: number - y: number - name: string - } -} - -export const renameDashboardCell = ( - dashboard: Dashboard, - x: number, - y: number, - name: string -): RenameDashboardCellAction => ({ - type: 'RENAME_DASHBOARD_CELL', - payload: { - dashboard, - x, // x-coord of the cell to be renamed - y, // y-coord of the cell to be renamed - name, - }, -}) - interface DeleteDashboardCellAction { type: 'DELETE_DASHBOARD_CELL' payload: { @@ -363,14 +271,14 @@ export const editCellQueryStatus = ( interface TemplateVariableSelectedAction { type: 'TEMPLATE_VARIABLE_SELECTED' payload: { - dashboardID: string + dashboardID: number templateID: string values: any[] } } export const templateVariableSelected = ( - dashboardID: string, + dashboardID: number, templateID: string, values ): TemplateVariableSelectedAction => ({ @@ -382,18 +290,11 @@ export const templateVariableSelected = ( }, }) -interface TemplateVariablesSelectedByNameAction { - type: 'TEMPLATE_VARIABLES_SELECTED_BY_NAME' - payload: { - dashboardID: string - query: Query - } -} - +// This is limited in typing as it will be changed soon export const templateVariablesSelectedByName = ( - dashboardID: string, - query: Query -): TemplateVariablesSelectedByNameAction => ({ + dashboardID: number, + query: any +) => ({ type: TEMPLATE_VARIABLES_SELECTED_BY_NAME, payload: { dashboardID, @@ -521,7 +422,7 @@ export const putDashboard = (dashboard: Dashboard) => async ( } } -export const putDashboardByID = (dashboardID: string) => async ( +export const putDashboardByID = (dashboardID: number) => async ( dispatch, getState ): Promise => { diff --git a/ui/src/dashboards/components/CellEditorOverlay.tsx b/ui/src/dashboards/components/CellEditorOverlay.tsx index 3aed37a61e..31baee8985 100644 --- a/ui/src/dashboards/components/CellEditorOverlay.tsx +++ b/ui/src/dashboards/components/CellEditorOverlay.tsx @@ -80,7 +80,7 @@ interface Props { onCancel: () => void onSave: (cell: Cell) => void source: Source - dashboardID: string + dashboardID: number queryStatus: QueryStatus autoRefresh: number templates: Template[] diff --git a/ui/src/dashboards/constants/index.ts b/ui/src/dashboards/constants/index.ts index 747e9dc288..6416693020 100644 --- a/ui/src/dashboards/constants/index.ts +++ b/ui/src/dashboards/constants/index.ts @@ -51,7 +51,7 @@ export const FORMAT_OPTIONS: Array<{text: string}> = [ export type NewDefaultCell = Pick< Cell, - Exclude + Exclude > export const NEW_DEFAULT_DASHBOARD_CELL: NewDefaultCell = { x: 0, diff --git a/ui/src/dashboards/containers/DashboardPage.js b/ui/src/dashboards/containers/DashboardPage.js index 6190b43687..5cfe8a965c 100644 --- a/ui/src/dashboards/containers/DashboardPage.js +++ b/ui/src/dashboards/containers/DashboardPage.js @@ -487,8 +487,6 @@ DashboardPage.propTypes = { getDashboardsAsync: func.isRequired, setTimeRange: func.isRequired, addDashboardCellAsync: func.isRequired, - editDashboardCell: func.isRequired, - cancelEditCell: func.isRequired, }).isRequired, dashboards: arrayOf( shape({ diff --git a/ui/src/dashboards/reducers/ui.js b/ui/src/dashboards/reducers/ui.js index 6ca5cbe1d0..45574e4edc 100644 --- a/ui/src/dashboards/reducers/ui.js +++ b/ui/src/dashboards/reducers/ui.js @@ -4,7 +4,7 @@ import {NULL_HOVER_TIME} from 'src/shared/constants/tableGraph' const {lower, upper} = timeRanges.find(tr => tr.lower === 'now() - 1h') -const initialState = { +export const initialState = { dashboards: [], timeRange: {lower, upper}, isEditMode: false, @@ -71,23 +71,6 @@ export default function ui(state = initialState, action) { return {...state, ...newState} } - case 'UPDATE_DASHBOARD_CELLS': { - const {cells, dashboard} = action.payload - - const newDashboard = { - ...dashboard, - cells, - } - - const newState = { - dashboards: state.dashboards.map( - d => (d.id === dashboard.id ? newDashboard : d) - ), - } - - return {...state, ...newState} - } - case 'ADD_DASHBOARD_CELL': { const {cell, dashboard} = action.payload const {dashboards} = state @@ -102,30 +85,6 @@ export default function ui(state = initialState, action) { return {...state, ...newState} } - case 'EDIT_DASHBOARD_CELL': { - const {x, y, isEditing, dashboard} = action.payload - - const cell = dashboard.cells.find(c => c.x === x && c.y === y) - - const newCell = { - ...cell, - isEditing, - } - - const newDashboard = { - ...dashboard, - cells: dashboard.cells.map(c => (c.x === x && c.y === y ? newCell : c)), - } - - const newState = { - dashboards: state.dashboards.map( - d => (d.id === dashboard.id ? newDashboard : d) - ), - } - - return {...state, ...newState} - } - case 'DELETE_DASHBOARD_CELL': { const {dashboard, cell} = action.payload @@ -145,24 +104,6 @@ export default function ui(state = initialState, action) { return {...state, ...newState} } - case 'CANCEL_EDIT_CELL': { - const {dashboardID, cellID} = action.payload - - const dashboards = state.dashboards.map( - d => - d.id === dashboardID - ? { - ...d, - cells: d.cells.map( - c => (c.i === cellID ? {...c, isEditing: false} : c) - ), - } - : d - ) - - return {...state, dashboards} - } - case 'SYNC_DASHBOARD_CELL': { const {cell, dashboard} = action.payload @@ -182,30 +123,6 @@ export default function ui(state = initialState, action) { return {...state, ...newState} } - case 'RENAME_DASHBOARD_CELL': { - const {x, y, name, dashboard} = action.payload - - const cell = dashboard.cells.find(c => c.x === x && c.y === y) - - const newCell = { - ...cell, - name, - } - - const newDashboard = { - ...dashboard, - cells: dashboard.cells.map(c => (c.x === x && c.y === y ? newCell : c)), - } - - const newState = { - dashboards: state.dashboards.map( - d => (d.id === dashboard.id ? newDashboard : d) - ), - } - - return {...state, ...newState} - } - case 'EDIT_CELL_QUERY_STATUS': { const {queryID, status} = action.payload diff --git a/ui/src/shared/components/LayoutCell.tsx b/ui/src/shared/components/LayoutCell.tsx index 4ef76af620..fa4b0fca35 100644 --- a/ui/src/shared/components/LayoutCell.tsx +++ b/ui/src/shared/components/LayoutCell.tsx @@ -102,7 +102,7 @@ export default class LayoutCell extends Component { if (this.queries.length) { const child = React.Children.only(children) - return React.cloneElement(child, {cellID: cell.id}) + return React.cloneElement(child, {cellID: cell.i}) } return this.emptyGraph diff --git a/ui/src/types/dashboard.ts b/ui/src/types/dashboard.ts index 0472b6d71a..9f9f2c54c2 100644 --- a/ui/src/types/dashboard.ts +++ b/ui/src/types/dashboard.ts @@ -64,7 +64,7 @@ export interface DecimalPlaces { } export interface Cell { - id: string + i: string x: number y: number w: number diff --git a/ui/src/types/index.ts b/ui/src/types/index.ts index 043abbcb29..520a872d01 100644 --- a/ui/src/types/index.ts +++ b/ui/src/types/index.ts @@ -1,7 +1,15 @@ import {LayoutCell, LayoutQuery} from './layouts' import {Service, NewService} from './services' import {AuthLinks, Organization, Role, User, Me} from './auth' -import {Template, Cell, CellQuery, Legend, Axes, Dashboard} from './dashboard' +import { + Template, + Cell, + CellQuery, + Legend, + Axes, + Dashboard, + CellType, +} from './dashboard' import { GroupBy, Query, @@ -37,6 +45,7 @@ export { Template, Cell, CellQuery, + CellType, Legend, Status, Query, diff --git a/ui/test/dashboards/components/CellEditorOverlay.test.tsx b/ui/test/dashboards/components/CellEditorOverlay.test.tsx index 0c30c8caa0..698042f49d 100644 --- a/ui/test/dashboards/components/CellEditorOverlay.test.tsx +++ b/ui/test/dashboards/components/CellEditorOverlay.test.tsx @@ -24,7 +24,7 @@ const setup = (override = {}) => { cell, timeRange, autoRefresh: 0, - dashboardID: '9', + dashboardID: 9, queryStatus: { queryID: null, status: null, diff --git a/ui/test/dashboards/reducers/ui.test.ts b/ui/test/dashboards/reducers/ui.test.ts index c454dd9f04..7ff29b345d 100644 --- a/ui/test/dashboards/reducers/ui.test.ts +++ b/ui/test/dashboards/reducers/ui.test.ts @@ -1,43 +1,24 @@ import _ from 'lodash' import reducer from 'src/dashboards/reducers/ui' +import {template, dashboard, cell} from 'test/resources' +import {initialState} from 'src/dashboards/reducers/ui' import { + setTimeRange, loadDashboards, deleteDashboard, - deleteDashboardFailed, - setTimeRange, - updateDashboardCells, - editDashboardCell, - renameDashboardCell, syncDashboardCell, + deleteDashboardFailed, templateVariableSelected, - templateVariablesSelectedByName, - cancelEditCell, editTemplateVariableValues, + templateVariablesSelectedByName, } from 'src/dashboards/actions' let state -const t1 = { - id: '1', - type: 'tagKeys', - label: 'test query', - tempVar: ':region:', - query: { - db: 'db1', - rp: 'rp1', - measurement: 'm1', - influxql: 'SHOW TAGS WHERE CHRONOGIRAFFE = "friend"', - }, - values: [ - {value: 'us-west', type: 'tagKey', selected: false}, - {value: 'us-east', type: 'tagKey', selected: true}, - {value: 'us-mount', type: 'tagKey', selected: false}, - ], -} - const t2 = { + ...template, id: '2', type: 'csv', label: 'test csv', @@ -49,35 +30,15 @@ const t2 = { ], } -const templates = [t1, t2] +const templates = [template, t2] const d1 = { - id: 1, - cells: [], - name: 'd1', + ...dashboard, templates, } -const d2 = {id: 2, cells: [], name: 'd2', templates: []} +const d2 = {...dashboard, id: 2, cells: [], name: 'd2', templates: []} const dashboards = [d1, d2] -const c1 = { - x: 0, - y: 0, - w: 4, - h: 4, - id: 1, - i: 'im-a-cell-id-index', - isEditing: false, - name: 'Gigawatts', -} - -const editingCell = { - i: 1, - isEditing: true, - name: 'Edit me', -} - -const cells = [c1] describe('DataExplorer.Reducers.UI', () => { it('can load the dashboards', () => { @@ -90,11 +51,8 @@ describe('DataExplorer.Reducers.UI', () => { }) it('can delete a dashboard', () => { - const initialState = {...state, dashboards} - const actual = reducer(initialState, deleteDashboard(d1)) - const expected = initialState.dashboards.filter( - dashboard => dashboard.id !== d1.id - ) + const actual = reducer({...initialState, dashboards}, deleteDashboard(d1)) + const expected = dashboards.filter(dash => dash.id !== d1.id) expect(actual.dashboards).toEqual(expected) }) @@ -117,43 +75,14 @@ describe('DataExplorer.Reducers.UI', () => { expect(actual.timeRange).toEqual(expected) }) - it('can update dashboard cells', () => { - state = { - dashboards, - } - - const updatedCells = [{id: 1}, {id: 2}] - - const expected = { - id: 1, - cells: updatedCells, - name: 'd1', - templates, - } - - const actual = reducer(state, updateDashboardCells(d1, updatedCells)) - - expect(actual.dashboards[0]).toEqual(expected) - }) - - it('can edit a cell', () => { - const dash = {...d1, cells} - state = { - dashboards: [dash], - } - - const actual = reducer(state, editDashboardCell(dash, 0, 0, true)) - expect(actual.dashboards[0].cells[0].isEditing).toBe(true) - }) - it('can sync a cell', () => { const newCellName = 'watts is kinda cool' const newCell = { - x: c1.x, - y: c1.y, + ...cell, name: newCellName, } - const dash = {...d1, cells: [c1]} + + const dash = {...d1, cells: [cell]} state = { dashboards: [dash], } @@ -162,22 +91,6 @@ describe('DataExplorer.Reducers.UI', () => { expect(actual.dashboards[0].cells[0].name).toBe(newCellName) }) - it('can rename cells', () => { - const c2 = {...c1, isEditing: true} - const dash = {...d1, cells: [c2]} - state = { - dashboards: [dash], - } - - const actual = reducer( - state, - renameDashboardCell(dash, 0, 0, 'Plutonium Consumption Rate (ug/sec)') - ) - expect(actual.dashboards[0].cells[0].name).toBe( - 'Plutonium Consumption Rate (ug/sec)' - ) - }) - it('can select a different template variable', () => { const dash = _.cloneDeep(d1) state = { @@ -215,24 +128,11 @@ describe('DataExplorer.Reducers.UI', () => { expect(actual.dashboards[0].templates[1].values[2].selected).toBe(false) }) - it('can cancel cell editing', () => { - const dash = _.cloneDeep(d1) - dash.cells = [editingCell] - - const actual = reducer( - {dashboards: [dash]}, - cancelEditCell(dash.id, editingCell.i) - ) - - expect(actual.dashboards[0].cells[0].isEditing).toBe(false) - expect(actual.dashboards[0].cells[0].name).toBe(editingCell.name) - }) - describe('EDIT_TEMPLATE_VARIABLE_VALUES', () => { it('can edit the tempvar values', () => { const actual = reducer( - {dashboards}, - editTemplateVariableValues(d1.id, t1.id, ['v1', 'v2']) + {...initialState, dashboards}, + editTemplateVariableValues(d1.id, template.id, ['v1', 'v2']) ) const expected = [ @@ -252,12 +152,12 @@ describe('DataExplorer.Reducers.UI', () => { }) it('can handle an empty template.values', () => { - const ts = [{...t1, values: []}] + const ts = [{...template, values: []}] const ds = [{...d1, templates: ts}] const actual = reducer( - {dashboards: ds}, - editTemplateVariableValues(d1.id, t1.id, ['v1', 'v2']) + {...initialState, dashboards: ds}, + editTemplateVariableValues(d1.id, template.id, ['v1', 'v2']) ) const expected = [ diff --git a/ui/test/fixtures/index.ts b/ui/test/fixtures/index.ts index c0fbcef6f0..071ef3b777 100644 --- a/ui/test/fixtures/index.ts +++ b/ui/test/fixtures/index.ts @@ -161,7 +161,7 @@ export const decimalPlaces: DecimalPlaces = { } export const cell: Cell = { - id: '67435af2-17bf-4caa-a5fc-0dd1ffb40dab', + i: '67435af2-17bf-4caa-a5fc-0dd1ffb40dab', x: 0, y: 0, w: 8, diff --git a/ui/test/resources.ts b/ui/test/resources.ts index 04ea7b367a..e7a9ea9cba 100644 --- a/ui/test/resources.ts +++ b/ui/test/resources.ts @@ -1,4 +1,4 @@ -import {Source} from 'src/types' +import {Source, Template, Dashboard, Cell, CellType} from 'src/types' import {SourceLinks} from 'src/types/sources' export const role = { @@ -586,3 +586,83 @@ export const hosts = { load: 0, }, } + +// Dashboards +export const template: Template = { + id: '1', + type: 'tagKeys', + label: 'test query', + tempVar: ':region:', + query: { + db: 'db1', + command: '', + rp: 'rp1', + tagKey: 'tk1', + fieldKey: 'fk1', + measurement: 'm1', + influxql: 'SHOW TAGS WHERE CHRONOGIRAFFE = "friend"', + }, + values: [ + {value: 'us-west', type: 'tagKey', selected: false}, + {value: 'us-east', type: 'tagKey', selected: true}, + {value: 'us-mount', type: 'tagKey', selected: false}, + ], +} + +export const dashboard: Dashboard = { + id: 1, + cells: [], + name: 'd1', + templates: [], + organization: 'thebestorg', +} + +export const cell: Cell = { + x: 0, + y: 0, + w: 4, + h: 4, + i: '0246e457-916b-43e3-be99-211c4cbc03e8', + name: 'Apache Bytes/Second', + queries: [], + axes: { + x: { + bounds: ['', ''], + label: '', + prefix: '', + suffix: '', + base: '', + scale: '', + }, + y: { + bounds: ['', ''], + label: '', + prefix: '', + suffix: '', + base: '', + scale: '', + }, + }, + type: CellType.Line, + colors: [], + tableOptions: { + verticalTimeAxis: true, + sortBy: { + internalName: '', + displayName: '', + visible: true, + }, + fixFirstColumn: true, + }, + fieldOptions: [], + timeFormat: '', + decimalPlaces: { + isEnforced: false, + digits: 1, + }, + links: { + self: + '/chronograf/v1/dashboards/10/cells/8b3b7897-49b1-422c-9443-e9b778bcbf12', + }, + legend: {}, +} From 4285abf5d4e269f902c53a01d699ed8cc6593621 Mon Sep 17 00:00:00 2001 From: Christopher Henn Date: Fri, 8 Jun 2018 15:22:54 -0700 Subject: [PATCH 24/28] Ensure schema explorer copy button visible --- ui/src/style/components/time-machine/flux-explorer.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/style/components/time-machine/flux-explorer.scss b/ui/src/style/components/time-machine/flux-explorer.scss index 36a9831102..3bad300872 100644 --- a/ui/src/style/components/time-machine/flux-explorer.scss +++ b/ui/src/style/components/time-machine/flux-explorer.scss @@ -3,7 +3,7 @@ ---------------------------------------------------------------------------- */ -$flux-tree-min-width: 500px; +$flux-tree-min-width: 250px; $flux-tree-indent: 26px; $flux-tree-line: 2px; $flux-tree-max-filter: 220px; From 957880bea250aec21440cf229d39cc761fdd0355 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Fri, 8 Jun 2018 15:34:59 -0700 Subject: [PATCH 25/28] Add test for SET_ACTIVE_CELL --- ui/src/dashboards/reducers/ui.js | 4 +++- ui/test/dashboards/reducers/ui.test.ts | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/ui/src/dashboards/reducers/ui.js b/ui/src/dashboards/reducers/ui.js index 45574e4edc..92a5ef0e3f 100644 --- a/ui/src/dashboards/reducers/ui.js +++ b/ui/src/dashboards/reducers/ui.js @@ -19,7 +19,7 @@ import { } from 'shared/constants/actionTypes' import {TEMPLATE_VARIABLE_TYPES} from 'src/dashboards/constants' -export default function ui(state = initialState, action) { +const ui = (state = initialState, action) => { switch (action.type) { case 'LOAD_DASHBOARDS': { const {dashboards} = action.payload @@ -255,3 +255,5 @@ export default function ui(state = initialState, action) { return state } + +export default ui diff --git a/ui/test/dashboards/reducers/ui.test.ts b/ui/test/dashboards/reducers/ui.test.ts index 7ff29b345d..50132320f8 100644 --- a/ui/test/dashboards/reducers/ui.test.ts +++ b/ui/test/dashboards/reducers/ui.test.ts @@ -13,6 +13,7 @@ import { templateVariableSelected, editTemplateVariableValues, templateVariablesSelectedByName, + setActiveCell, } from 'src/dashboards/actions' let state @@ -128,6 +129,15 @@ describe('DataExplorer.Reducers.UI', () => { expect(actual.dashboards[0].templates[1].values[2].selected).toBe(false) }) + describe('SET_ACTIVE_CELL', () => { + it('can set the active cell', () => { + const activeCellID = '1' + const actual = reducer(initialState, setActiveCell(activeCellID)) + + expect(actual.activeCellID).toEqual(activeCellID) + }) + }) + describe('EDIT_TEMPLATE_VARIABLE_VALUES', () => { it('can edit the tempvar values', () => { const actual = reducer( From 042b168c5a7feba437ab53aa728e933a96cabd5f Mon Sep 17 00:00:00 2001 From: nathan haugo Date: Fri, 8 Jun 2018 17:37:22 -0700 Subject: [PATCH 26/28] Update the name to fqi --- ui/src/side_nav/containers/SideNav.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/side_nav/containers/SideNav.tsx b/ui/src/side_nav/containers/SideNav.tsx index 2ba5f851a4..b84ccadf25 100644 --- a/ui/src/side_nav/containers/SideNav.tsx +++ b/ui/src/side_nav/containers/SideNav.tsx @@ -84,7 +84,7 @@ class SideNav extends PureComponent { location={location} > - + Date: Fri, 8 Jun 2018 17:52:47 -0700 Subject: [PATCH 27/28] Fix prettier error --- ui/src/side_nav/containers/SideNav.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/src/side_nav/containers/SideNav.tsx b/ui/src/side_nav/containers/SideNav.tsx index b84ccadf25..f79bc6fd70 100644 --- a/ui/src/side_nav/containers/SideNav.tsx +++ b/ui/src/side_nav/containers/SideNav.tsx @@ -84,7 +84,10 @@ class SideNav extends PureComponent { location={location} > - + Date: Fri, 8 Jun 2018 18:36:50 -0700 Subject: [PATCH 28/28] Provide a builder UI for a Flux Filter Node (#3615) * Introduce FilterArgs as a seam * Selecting a value adds to Script and Preview * Hydrate FilterArgs dropdown from function body * Functional operator selector * Style polish * Rename Filter/FilterPreview components * Pull types into flux type file * Derive Flux Filter UI state directly from AST * Default func body * Remove flicker when updating conditions * Render a div when filter body isn't supported * Base condition (true) is parseable * Remove a test file if it doesn't have any tests * Enable tslint and resolve errors * Address other PR feedback * Tests and improved parsing logic * Add helper text when filter ui falls back to a textarea --- ui/src/flux/components/Filter.tsx | 47 --- ui/src/flux/components/FilterArgs.tsx | 100 ++++++ .../flux/components/FilterConditionNode.tsx | 48 +++ ui/src/flux/components/FilterConditions.tsx | 21 ++ ui/src/flux/components/FilterPreview.tsx | 100 +++--- ui/src/flux/components/FilterTagList.tsx | 270 ++++++++++++++ ui/src/flux/components/FilterTagListItem.tsx | 330 ++++++++++++++++++ ui/src/flux/components/FilterTagValueList.tsx | 68 ++++ .../components/FilterTagValueListItem.tsx | 49 +++ .../{From.tsx => FromDatabaseDropdown.tsx} | 4 +- ui/src/flux/components/FuncArg.tsx | 4 +- ui/src/flux/components/FuncArgs.tsx | 44 ++- ui/src/flux/components/FuncArgsPreview.tsx | 7 +- .../components/{Join.tsx => JoinArgs.tsx} | 4 +- .../components/time-machine/flux-builder.scss | 61 ++-- ui/src/types/flux.ts | 20 ++ ui/test/flux/components/Filter.test.tsx | 39 --- .../flux/components/FilterTagList.test.tsx | 257 ++++++++++++++ ...test.tsx => FromDatabaseDropdown.test.tsx} | 6 +- ui/yarn.lock | 173 ++------- 20 files changed, 1308 insertions(+), 344 deletions(-) delete mode 100644 ui/src/flux/components/Filter.tsx create mode 100644 ui/src/flux/components/FilterArgs.tsx create mode 100644 ui/src/flux/components/FilterConditionNode.tsx create mode 100644 ui/src/flux/components/FilterConditions.tsx create mode 100644 ui/src/flux/components/FilterTagList.tsx create mode 100644 ui/src/flux/components/FilterTagListItem.tsx create mode 100644 ui/src/flux/components/FilterTagValueList.tsx create mode 100644 ui/src/flux/components/FilterTagValueListItem.tsx rename ui/src/flux/components/{From.tsx => FromDatabaseDropdown.tsx} (94%) rename ui/src/flux/components/{Join.tsx => JoinArgs.tsx} (98%) delete mode 100644 ui/test/flux/components/Filter.test.tsx create mode 100644 ui/test/flux/components/FilterTagList.test.tsx rename ui/test/flux/components/{From.test.tsx => FromDatabaseDropdown.test.tsx} (73%) diff --git a/ui/src/flux/components/Filter.tsx b/ui/src/flux/components/Filter.tsx deleted file mode 100644 index 967e0383ad..0000000000 --- a/ui/src/flux/components/Filter.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import {PureComponent, ReactNode} from 'react' -import {connect} from 'react-redux' -import {getAST} from 'src/flux/apis' -import {Links, BinaryExpressionNode, MemberExpressionNode} from 'src/types/flux' -import Walker from 'src/flux/ast/walker' - -interface Props { - value: string - links: Links - render: (nodes: FilterNode[]) => ReactNode -} - -type FilterNode = BinaryExpressionNode | MemberExpressionNode - -interface State { - nodes: FilterNode[] -} - -export class Filter extends PureComponent { - constructor(props) { - super(props) - this.state = { - nodes: [], - } - } - - public async componentDidMount() { - const {links, value} = this.props - try { - const ast = await getAST({url: links.ast, body: value}) - const nodes = new Walker(ast).inOrderExpression - this.setState({nodes}) - } catch (error) { - console.error('Could not parse AST', error) - } - } - - public render() { - return this.props.render(this.state.nodes) - } -} - -const mapStateToProps = ({links}) => { - return {links: links.flux} -} - -export default connect(mapStateToProps, null)(Filter) diff --git a/ui/src/flux/components/FilterArgs.tsx b/ui/src/flux/components/FilterArgs.tsx new file mode 100644 index 0000000000..65b281c35b --- /dev/null +++ b/ui/src/flux/components/FilterArgs.tsx @@ -0,0 +1,100 @@ +import React, {PureComponent} from 'react' +import {connect} from 'react-redux' +import {getAST} from 'src/flux/apis' +import {tagKeys as fetchTagKeys} from 'src/shared/apis/flux/metaQueries' +import parseValuesColumn from 'src/shared/parsing/flux/values' +import FilterTagList from 'src/flux/components/FilterTagList' +import Walker from 'src/flux/ast/walker' + +import {Service} from 'src/types' +import {Links, OnChangeArg, Func, FilterNode} from 'src/types/flux' + +interface Props { + links: Links + value: string + func: Func + bodyID: string + declarationID: string + onChangeArg: OnChangeArg + db: string + service: Service + onGenerateScript: () => void +} + +interface State { + tagKeys: string[] + nodes: FilterNode[] + ast: object +} + +class FilterArgs extends PureComponent { + constructor(props) { + super(props) + this.state = { + tagKeys: [], + nodes: [], + ast: {}, + } + } + + public async convertStringToNodes() { + const {links, value} = this.props + + const ast = await getAST({url: links.ast, body: value}) + const nodes = new Walker(ast).inOrderExpression + this.setState({nodes, ast}) + } + + public componentDidUpdate(prevProps) { + if (prevProps.value !== this.props.value) { + this.convertStringToNodes() + } + } + + public async componentDidMount() { + const {db, service} = this.props + + try { + this.convertStringToNodes() + const response = await fetchTagKeys(service, db, []) + const tagKeys = parseValuesColumn(response) + this.setState({tagKeys}) + } catch (error) { + console.error(error) + } + } + + public render() { + const { + db, + service, + onChangeArg, + func, + bodyID, + declarationID, + onGenerateScript, + } = this.props + const {nodes} = this.state + + return ( + + ) + } +} + +const mapStateToProps = ({links}) => { + return {links: links.flux} +} + +export default connect(mapStateToProps, null)(FilterArgs) diff --git a/ui/src/flux/components/FilterConditionNode.tsx b/ui/src/flux/components/FilterConditionNode.tsx new file mode 100644 index 0000000000..a41b0c9221 --- /dev/null +++ b/ui/src/flux/components/FilterConditionNode.tsx @@ -0,0 +1,48 @@ +import React, {PureComponent} from 'react' +import {FilterNode, MemberExpressionNode} from 'src/types/flux' + +interface Props { + node: FilterNode +} + +class FilterConditionNode extends PureComponent { + public render() { + const {node} = this.props + + switch (node.type) { + case 'ObjectExpression': { + return
{node.source}
+ } + case 'MemberExpression': { + const memberNode = node as MemberExpressionNode + return ( +
{memberNode.property.name}
+ ) + } + case 'OpenParen': { + return
+ } + case 'CloseParen': { + return
+ } + case 'NumberLiteral': + case 'IntegerLiteral': { + return
{node.source}
+ } + case 'BooleanLiteral': { + return
{node.source}
+ } + case 'StringLiteral': { + return
{node.source}
+ } + case 'Operator': { + return
{node.source}
+ } + default: { + return
+ } + } + } +} + +export default FilterConditionNode diff --git a/ui/src/flux/components/FilterConditions.tsx b/ui/src/flux/components/FilterConditions.tsx new file mode 100644 index 0000000000..7877a0f643 --- /dev/null +++ b/ui/src/flux/components/FilterConditions.tsx @@ -0,0 +1,21 @@ +import React, {PureComponent} from 'react' +import {FilterNode} from 'src/types/flux' +import FilterConditionNode from 'src/flux/components/FilterConditionNode' + +interface Props { + nodes: FilterNode[] +} + +class FilterConditions extends PureComponent { + public render() { + return ( + <> + {this.props.nodes.map((n, i) => ( + + ))} + + ) + } +} + +export default FilterConditions diff --git a/ui/src/flux/components/FilterPreview.tsx b/ui/src/flux/components/FilterPreview.tsx index 28e58b33df..eb47c884f8 100644 --- a/ui/src/flux/components/FilterPreview.tsx +++ b/ui/src/flux/components/FilterPreview.tsx @@ -1,66 +1,58 @@ import React, {PureComponent} from 'react' -import {BinaryExpressionNode, MemberExpressionNode} from 'src/types/flux' - -type FilterNode = BinaryExpressionNode & MemberExpressionNode +import {connect} from 'react-redux' +import {getAST} from 'src/flux/apis' +import {Links, FilterNode} from 'src/types/flux' +import Walker from 'src/flux/ast/walker' +import FilterConditions from 'src/flux/components/FilterConditions' interface Props { + filterString?: string + links: Links +} + +interface State { nodes: FilterNode[] + ast: object } -class FilterPreview extends PureComponent { - public render() { - return ( - <> - {this.props.nodes.map((n, i) => )} - - ) - } -} - -interface FilterPreviewNodeProps { - node: FilterNode -} - -/* tslint:disable */ -class FilterPreviewNode extends PureComponent { - public render() { - return this.className +export class FilterPreview extends PureComponent { + public static defaultProps: Partial = { + filterString: '', } - private get className(): JSX.Element { - const {node} = this.props - - switch (node.type) { - case 'ObjectExpression': { - return
{node.source}
- } - case 'MemberExpression': { - return
{node.property.name}
- } - case 'OpenParen': { - return
- } - case 'CloseParen': { - return
- } - case 'NumberLiteral': - case 'IntegerLiteral': { - return
{node.source}
- } - case 'BooleanLiteral': { - return
{node.source}
- } - case 'StringLiteral': { - return
{node.source}
- } - case 'Operator': { - return
{node.source}
- } - default: { - return
- } + constructor(props) { + super(props) + this.state = { + nodes: [], + ast: {}, } } + + public async componentDidMount() { + this.convertStringToNodes() + } + + public async componentDidUpdate(prevProps, __) { + if (this.props.filterString !== prevProps.filterString) { + this.convertStringToNodes() + } + } + + public async convertStringToNodes() { + const {links, filterString} = this.props + + const ast = await getAST({url: links.ast, body: filterString}) + const nodes = new Walker(ast).inOrderExpression + this.setState({nodes, ast}) + } + + public render() { + return + } } -export default FilterPreview +const mapStateToProps = ({links}) => { + return {links: links.flux} +} + +export default connect(mapStateToProps, null)(FilterPreview) diff --git a/ui/src/flux/components/FilterTagList.tsx b/ui/src/flux/components/FilterTagList.tsx new file mode 100644 index 0000000000..f0de27a083 --- /dev/null +++ b/ui/src/flux/components/FilterTagList.tsx @@ -0,0 +1,270 @@ +import React, {PureComponent, MouseEvent} from 'react' +import _ from 'lodash' + +import {SchemaFilter, Service} from 'src/types' +import { + OnChangeArg, + Func, + FilterClause, + FilterTagCondition, + FilterNode, +} from 'src/types/flux' +import {argTypes} from 'src/flux/constants' + +import FuncArgTextArea from 'src/flux/components/FuncArgTextArea' +import FilterTagListItem from 'src/flux/components/FilterTagListItem' +import FancyScrollbar from '../../shared/components/FancyScrollbar' +import {getDeep} from 'src/utils/wrappers' + +interface Props { + db: string + service: Service + tags: string[] + filter: SchemaFilter[] + onChangeArg: OnChangeArg + func: Func + nodes: FilterNode[] + bodyID: string + declarationID: string + onGenerateScript: () => void +} + +type ParsedClause = [FilterClause, boolean] + +export default class FilterTagList extends PureComponent { + public get clauseIsParseable(): boolean { + const [, parseable] = this.reduceNodesToClause(this.props.nodes, []) + return parseable + } + + public get clause(): FilterClause { + const [clause] = this.reduceNodesToClause(this.props.nodes, []) + return clause + } + + public conditions(key: string, clause?): FilterTagCondition[] { + clause = clause || this.clause + return clause[key] || [] + } + + public operator(key: string, clause?): string { + const conditions = this.conditions(key, clause) + return getDeep(conditions, '0.operator', '==') + } + + public addCondition(condition: FilterTagCondition): FilterClause { + const conditions = this.conditions(condition.key) + return { + ...this.clause, + [condition.key]: [...conditions, condition], + } + } + + public removeCondition(condition: FilterTagCondition): FilterClause { + const conditions = this.conditions(condition.key) + const newConditions = _.reject(conditions, c => _.isEqual(c, condition)) + return { + ...this.clause, + [condition.key]: newConditions, + } + } + + public buildFilterString(clause: FilterClause): string { + const funcBody = Object.entries(clause) + .filter(([__, conditions]) => conditions.length) + .map(([key, conditions]) => { + const joiner = this.operator(key, clause) === '==' ? ' OR ' : ' AND ' + const subClause = conditions + .map(c => `r.${key} ${c.operator} "${c.value}"`) + .join(joiner) + return '(' + subClause + ')' + }) + .join(' AND ') + return funcBody ? `(r) => ${funcBody}` : `() => true` + } + + public handleChangeValue = ( + key: string, + value: string, + selected: boolean + ): void => { + const condition: FilterTagCondition = { + key, + operator: this.operator(key), + value, + } + const clause: FilterClause = selected + ? this.addCondition(condition) + : this.removeCondition(condition) + const filterString: string = this.buildFilterString(clause) + this.updateFilterString(filterString) + } + + public handleSetEquality = (key: string, equal: boolean): void => { + const operator = equal ? '==' : '!=' + const clause: FilterClause = { + ...this.clause, + [key]: this.conditions(key).map(c => ({...c, operator})), + } + const filterString: string = this.buildFilterString(clause) + this.updateFilterString(filterString) + } + + public updateFilterString = (newFilterString: string): void => { + const { + func: {id}, + bodyID, + declarationID, + } = this.props + + this.props.onChangeArg({ + funcID: id, + key: 'fn', + value: newFilterString, + declarationID, + bodyID, + generate: true, + }) + } + + public render() { + const { + db, + service, + tags, + filter, + bodyID, + declarationID, + onChangeArg, + onGenerateScript, + func: {id: funcID, args}, + } = this.props + const {value, key: argKey} = args[0] + + if (!this.clauseIsParseable) { + return ( + <> +

+ Unable to render expression as a Builder +

+ + + ) + } + + if (tags.length) { + return ( + + {tags.map(t => ( + + ))} + + ) + } + + return ( +
+
+
No tag keys found.
+
+
+ ) + } + + public reduceNodesToClause( + nodes, + conditions: FilterTagCondition[] + ): ParsedClause { + if (!nodes.length) { + return this.constructClause(conditions) + } else if (this.noConditions(nodes, conditions)) { + return [{}, true] + } else if ( + ['OpenParen', 'CloseParen', 'Operator'].includes(nodes[0].type) + ) { + return this.skipNode(nodes, conditions) + } else if (this.conditionExtractable(nodes)) { + return this.extractCondition(nodes, conditions) + } else { + // Unparseable + return [{}, false] + } + } + + private constructClause(conditions: FilterTagCondition[]): ParsedClause { + const clause = _.groupBy(conditions, condition => condition.key) + if (this.validateClause(clause)) { + return [clause, true] + } else { + return [{}, false] + } + } + + private validateClause(clause) { + return Object.values(clause).every((conditions: FilterTagCondition[]) => + conditions.every(c => conditions[0].operator === c.operator) + ) + } + + private noConditions(nodes, conditions) { + return ( + !conditions.length && + nodes.length === 1 && + nodes[0].type === 'BooleanLiteral' && + nodes[0].source === 'true' + ) + } + + private skipNode([, ...nodes], conditions) { + return this.reduceNodesToClause(nodes, conditions) + } + + private conditionExtractable(nodes): boolean { + return ( + nodes.length >= 3 && + nodes[0].type === 'MemberExpression' && + nodes[1].type === 'Operator' && + this.supportedOperator(nodes[1].source) && + nodes[2].type === 'StringLiteral' + ) + } + + private supportedOperator(operator): boolean { + return operator === '==' || operator === '!=' + } + + private extractCondition( + [keyNode, operatorNode, valueNode, ...nodes], + conditions + ) { + const condition: FilterTagCondition = { + key: keyNode.property.name, + operator: operatorNode.source, + value: valueNode.source.replace(/"/g, ''), + } + return this.reduceNodesToClause(nodes, [...conditions, condition]) + } + + private handleClick(e: MouseEvent) { + e.stopPropagation() + } +} diff --git a/ui/src/flux/components/FilterTagListItem.tsx b/ui/src/flux/components/FilterTagListItem.tsx new file mode 100644 index 0000000000..207738b1fc --- /dev/null +++ b/ui/src/flux/components/FilterTagListItem.tsx @@ -0,0 +1,330 @@ +import React, { + PureComponent, + CSSProperties, + ChangeEvent, + MouseEvent, +} from 'react' + +import _ from 'lodash' + +import {Service, SchemaFilter, RemoteDataState} from 'src/types' +import {tagValues as fetchTagValues} from 'src/shared/apis/flux/metaQueries' +import {explorer} from 'src/flux/constants' +import { + SetFilterTagValue, + SetEquality, + FilterTagCondition, +} from 'src/types/flux' +import parseValuesColumn from 'src/shared/parsing/flux/values' +import FilterTagValueList from 'src/flux/components/FilterTagValueList' +import LoaderSkeleton from 'src/flux/components/LoaderSkeleton' +import LoadingSpinner from 'src/flux/components/LoadingSpinner' + +interface Props { + tagKey: string + onSetEquality: SetEquality + onChangeValue: SetFilterTagValue + conditions: FilterTagCondition[] + operator: string + db: string + service: Service + filter: SchemaFilter[] +} + +interface State { + isOpen: boolean + loadingAll: RemoteDataState + loadingSearch: RemoteDataState + loadingMore: RemoteDataState + tagValues: string[] + searchTerm: string + limit: number + count: number | null +} + +export default class FilterTagListItem extends PureComponent { + constructor(props) { + super(props) + + this.state = { + isOpen: false, + loadingAll: RemoteDataState.NotStarted, + loadingSearch: RemoteDataState.NotStarted, + loadingMore: RemoteDataState.NotStarted, + tagValues: [], + count: null, + searchTerm: '', + limit: explorer.TAG_VALUES_LIMIT, + } + + this.debouncedOnSearch = _.debounce(() => { + this.searchTagValues() + this.getCount() + }, 250) + } + + public renderEqualitySwitcher() { + const {operator} = this.props + + if (!this.state.isOpen) { + return null + } + + return ( +
    +
  • + = +
  • +
  • + != +
  • +
+ ) + } + + public render() { + const {tagKey, db, service, filter} = this.props + const {tagValues, searchTerm, loadingMore, count, limit} = this.state + const selectedValues = this.props.conditions.map(c => c.value) + + return ( +
+
+
+
+ {tagKey} + Tag Key +
+ {this.renderEqualitySwitcher()} +
+ {this.state.isOpen && ( + <> +
+
+ + {this.isSearching && ( + + )} +
+ + {!!count && ( +
{`${count} Tag Values`}
+ )} +
+ {this.isLoading && } + {!this.isLoading && ( + <> + + + )} + + )} +
+ ) + } + + private setEquality(equal: boolean) { + return (e): void => { + e.stopPropagation() + + const {tagKey} = this.props + this.props.onSetEquality(tagKey, equal) + } + } + + private get spinnerStyle(): CSSProperties { + return { + position: 'absolute', + right: '15px', + top: '6px', + } + } + + private get isSearching(): boolean { + return this.state.loadingSearch === RemoteDataState.Loading + } + + private get isLoading(): boolean { + return this.state.loadingAll === RemoteDataState.Loading + } + + private onSearch = (e: ChangeEvent): void => { + const searchTerm = e.target.value + + this.setState({searchTerm, loadingSearch: RemoteDataState.Loading}, () => + this.debouncedOnSearch() + ) + } + + private debouncedOnSearch() {} // See constructor + + private handleInputClick = (e: MouseEvent): void => { + e.stopPropagation() + } + + private searchTagValues = async () => { + try { + const tagValues = await this.getTagValues() + + this.setState({ + tagValues, + loadingSearch: RemoteDataState.Done, + }) + } catch (error) { + console.error(error) + this.setState({loadingSearch: RemoteDataState.Error}) + } + } + + private getAllTagValues = async () => { + this.setState({loadingAll: RemoteDataState.Loading}) + + try { + const tagValues = await this.getTagValues() + + this.setState({ + tagValues, + loadingAll: RemoteDataState.Done, + }) + } catch (error) { + console.error(error) + this.setState({loadingAll: RemoteDataState.Error}) + } + } + + private getMoreTagValues = async () => { + this.setState({loadingMore: RemoteDataState.Loading}) + + try { + const tagValues = await this.getTagValues() + + this.setState({ + tagValues, + loadingMore: RemoteDataState.Done, + }) + } catch (error) { + console.error(error) + this.setState({loadingMore: RemoteDataState.Error}) + } + } + + private getTagValues = async () => { + const {db, service, tagKey, filter} = this.props + const {searchTerm, limit} = this.state + const response = await fetchTagValues({ + service, + db, + filter, + tagKey, + limit, + searchTerm, + }) + + return parseValuesColumn(response) + } + + private handleClick = (e: MouseEvent) => { + e.stopPropagation() + + if (this.isFetchable) { + this.getCount() + this.getAllTagValues() + } + + this.setState({isOpen: !this.state.isOpen}) + } + + private handleLoadMoreValues = (): void => { + const {limit} = this.state + + this.setState( + {limit: limit + explorer.TAG_VALUES_LIMIT}, + this.getMoreTagValues + ) + } + + private async getCount() { + const {service, db, filter, tagKey} = this.props + const {limit, searchTerm} = this.state + try { + const response = await fetchTagValues({ + service, + db, + filter, + tagKey, + limit, + searchTerm, + count: true, + }) + + const parsed = parseValuesColumn(response) + + if (parsed.length !== 1) { + // We expect to never reach this state; instead, the Flux server should + // return a non-200 status code is handled earlier (after fetching). + // This return guards against some unexpected behavior---the Flux server + // returning a 200 status code but ALSO having an error in the CSV + // response body + return + } + + const count = Number(parsed[0]) + + this.setState({count}) + } catch (error) { + console.error(error) + } + } + + private get loadMoreCount(): number { + const {count, limit} = this.state + + return Math.min(Math.abs(count - limit), explorer.TAG_VALUES_LIMIT) + } + + private get isFetchable(): boolean { + const {isOpen, loadingAll} = this.state + + return ( + !isOpen && + (loadingAll === RemoteDataState.NotStarted || + loadingAll !== RemoteDataState.Error) + ) + } + + private get className(): string { + const {isOpen} = this.state + const openClass = isOpen ? 'expanded' : '' + + return `flux-schema-tree ${openClass}` + } +} diff --git a/ui/src/flux/components/FilterTagValueList.tsx b/ui/src/flux/components/FilterTagValueList.tsx new file mode 100644 index 0000000000..3ad139893d --- /dev/null +++ b/ui/src/flux/components/FilterTagValueList.tsx @@ -0,0 +1,68 @@ +import React, {PureComponent, MouseEvent} from 'react' +import _ from 'lodash' + +import FilterTagValueListItem from 'src/flux/components/FilterTagValueListItem' +import LoadingSpinner from 'src/flux/components/LoadingSpinner' +import {Service, SchemaFilter} from 'src/types' +import {SetFilterTagValue} from 'src/types/flux' + +interface Props { + service: Service + db: string + tagKey: string + values: string[] + selectedValues: string[] + onChangeValue: SetFilterTagValue + filter: SchemaFilter[] + isLoadingMoreValues: boolean + onLoadMoreValues: () => void + shouldShowMoreValues: boolean + loadMoreCount: number +} + +export default class FilterTagValueList extends PureComponent { + public render() { + const {values, tagKey, shouldShowMoreValues} = this.props + + return ( + <> + {values.map((v, i) => ( + + ))} + {shouldShowMoreValues && ( +
+
+ +
+
+ )} + + ) + } + + private handleClick = (e: MouseEvent) => { + e.stopPropagation() + this.props.onLoadMoreValues() + } + + private get buttonValue(): string | JSX.Element { + const {isLoadingMoreValues, loadMoreCount, tagKey} = this.props + + if (isLoadingMoreValues) { + return + } + + return `Load next ${loadMoreCount} values for ${tagKey}` + } +} diff --git a/ui/src/flux/components/FilterTagValueListItem.tsx b/ui/src/flux/components/FilterTagValueListItem.tsx new file mode 100644 index 0000000000..3d0cb4c8ec --- /dev/null +++ b/ui/src/flux/components/FilterTagValueListItem.tsx @@ -0,0 +1,49 @@ +import React, {PureComponent, MouseEvent} from 'react' + +import {SetFilterTagValue} from 'src/types/flux' + +interface Props { + tagKey: string + value: string + onChangeValue: SetFilterTagValue + selected: boolean +} + +class FilterTagValueListItem extends PureComponent { + constructor(props) { + super(props) + } + + public render() { + const {value} = this.props + + return ( +
+
+
+
+ {value} + Tag Value +
+
+
+ ) + } + + private handleClick = (e: MouseEvent) => { + const {tagKey, value, selected} = this.props + + e.stopPropagation() + this.props.onChangeValue(tagKey, value, !selected) + } + + private get listItemClasses() { + const baseClasses = 'flux-schema--item query-builder--list-item' + return this.props.selected ? baseClasses + ' active' : baseClasses + } +} + +export default FilterTagValueListItem diff --git a/ui/src/flux/components/From.tsx b/ui/src/flux/components/FromDatabaseDropdown.tsx similarity index 94% rename from ui/src/flux/components/From.tsx rename to ui/src/flux/components/FromDatabaseDropdown.tsx index 2f56c9508b..99b1f51b31 100644 --- a/ui/src/flux/components/From.tsx +++ b/ui/src/flux/components/FromDatabaseDropdown.tsx @@ -25,7 +25,7 @@ interface DropdownItem { text: string } -class From extends PureComponent { +class FromDatabaseDropdown extends PureComponent { constructor(props) { super(props) this.state = { @@ -82,4 +82,4 @@ class From extends PureComponent { } } -export default From +export default FromDatabaseDropdown diff --git a/ui/src/flux/components/FuncArg.tsx b/ui/src/flux/components/FuncArg.tsx index ab04c51b3c..0f418e9d5c 100644 --- a/ui/src/flux/components/FuncArg.tsx +++ b/ui/src/flux/components/FuncArg.tsx @@ -4,7 +4,7 @@ import FuncArgInput from 'src/flux/components/FuncArgInput' import FuncArgTextArea from 'src/flux/components/FuncArgTextArea' import FuncArgBool from 'src/flux/components/FuncArgBool' import {ErrorHandling} from 'src/shared/decorators/errors' -import From from 'src/flux/components/From' +import FromDatabaseDropdown from 'src/flux/components/FromDatabaseDropdown' import {funcNames, argTypes} from 'src/flux/constants' import {OnChangeArg} from 'src/types/flux' @@ -41,7 +41,7 @@ class FuncArg extends PureComponent { if (funcName === funcNames.FROM) { return ( - { return (
-
{this.renderJoinOrArgs}
+
{this.renderArguments}
{ ) } - get renderJoinOrArgs(): JSX.Element | JSX.Element[] { + get renderArguments(): JSX.Element | JSX.Element[] { const {func} = this.props const {name: funcName} = func @@ -47,10 +49,14 @@ export default class FuncArgs extends PureComponent { return this.renderJoin } - return this.renderArguments + if (funcName === funcNames.FILTER) { + return this.renderFilter + } + + return this.renderGeneralArguments } - get renderArguments(): JSX.Element | JSX.Element[] { + get renderGeneralArguments(): JSX.Element | JSX.Element[] { const { func, bodyID, @@ -59,6 +65,7 @@ export default class FuncArgs extends PureComponent { declarationID, onGenerateScript, } = this.props + const {name: funcName, id: funcID} = func return func.args.map(({key, value, type}) => ( @@ -78,6 +85,31 @@ export default class FuncArgs extends PureComponent { )) } + get renderFilter(): JSX.Element { + const { + func, + bodyID, + service, + onChangeArg, + declarationID, + onGenerateScript, + } = this.props + const value = getDeep(func.args, '0.value', '') + + return ( + + ) + } + get renderJoin(): JSX.Element { const { func, @@ -89,7 +121,7 @@ export default class FuncArgs extends PureComponent { } = this.props return ( - { return this.colorizedArguments } - return + return } return this.colorizedArguments } - private filterPreview = nodes => { - return - } - private get colorizedArguments(): JSX.Element | JSX.Element[] { const {func} = this.props const {args} = func diff --git a/ui/src/flux/components/Join.tsx b/ui/src/flux/components/JoinArgs.tsx similarity index 98% rename from ui/src/flux/components/Join.tsx rename to ui/src/flux/components/JoinArgs.tsx index d0f994800f..973be692d6 100644 --- a/ui/src/flux/components/Join.tsx +++ b/ui/src/flux/components/JoinArgs.tsx @@ -22,7 +22,7 @@ interface DropdownItem { text: string } -class Join extends PureComponent { +class JoinArgs extends PureComponent { constructor(props: Props) { super(props) } @@ -151,4 +151,4 @@ class Join extends PureComponent { } } -export default Join +export default JoinArgs diff --git a/ui/src/style/components/time-machine/flux-builder.scss b/ui/src/style/components/time-machine/flux-builder.scss index 5efb3bde62..f41fcf0635 100644 --- a/ui/src/style/components/time-machine/flux-builder.scss +++ b/ui/src/style/components/time-machine/flux-builder.scss @@ -10,13 +10,11 @@ $flux-connector-line: 2px; $flux-node-gap: 30px; $flux-node-padding: 10px; $flux-arg-min-width: 120px; - $flux-number-color: $c-neutrino; $flux-object-color: $c-viridian; $flux-string-color: $c-honeydew; $flux-boolean-color: $c-viridian; $flux-invalid-color: $c-viridian; - // Shared Node styles %flux-node { min-height: $flux-node-height; @@ -29,7 +27,6 @@ $flux-invalid-color: $c-viridian; transition: background-color 0.25s ease; margin-bottom: $flux-node-tooltip-gap / 2; margin-top: $flux-node-tooltip-gap / 2; - &:hover { cursor: pointer; background-color: $g6-smoke; @@ -39,6 +36,7 @@ $flux-invalid-color: $c-viridian; .body-builder--container { background-color: $g1-raven; } + .body-builder { padding: $flux-node-height; padding-bottom: 0; @@ -55,7 +53,6 @@ $flux-invalid-color: $c-viridian; flex-wrap: nowrap; flex-direction: column; align-items: flex-start; - &:last-of-type { margin-bottom: 0; border: 0; @@ -69,7 +66,6 @@ $flux-invalid-color: $c-viridian; white-space: nowrap; @include no-user-select(); margin-top: 0; - &:hover { background-color: $g4-onyx; cursor: default; @@ -86,7 +82,6 @@ $flux-invalid-color: $c-viridian; top: 50%; left: $flux-node-gap / 2; transform: translate(-50%, -50%); - &:before { content: ''; position: absolute; @@ -101,8 +96,7 @@ $flux-invalid-color: $c-viridian; .variable-node--name { color: $c-pool; - - .variable-node--connector + & { + .variable-node--connector+& { margin-left: $flux-node-gap - $flux-node-padding; } } @@ -146,24 +140,20 @@ $flux-invalid-color: $c-viridian; top: 0; left: 0; transform: translateX(-100%); - z-index: 0; - - // Connection Lines + z-index: 0; // Connection Lines &:before, &:after { content: ''; background-color: $g4-onyx; position: absolute; - } - // Vertical Line + } // Vertical Line &:before { width: $flux-connector-line; height: calc(100% + #{$flux-node-tooltip-gap}); top: -$flux-node-tooltip-gap / 2; left: $flux-node-gap / 2; transform: translateX(-50%); - } - // Horizontal Line + } // Horizontal Line &:after { height: $flux-connector-line; width: $flux-node-gap / 2; @@ -172,22 +162,19 @@ $flux-invalid-color: $c-viridian; transform: translateY(-50%); } } + // When a query exists unassigned to a variable .func-node:first-child { margin-left: 0; padding-left: $flux-node-gap; - .func-node--connector { transform: translateX(0); - z-index: 2; - - // Vertical Line + z-index: 2; // Vertical Line &:before { height: $flux-node-gap; top: $flux-node-gap / 2; @include gradient-v($c-comet, $g4-onyx); - } - // Dot + } // Dot &:after { width: 8px; height: 8px; @@ -238,9 +225,7 @@ $flux-invalid-color: $c-viridian; top: 0; left: calc(100% + #{$flux-node-tooltip-gap}); z-index: 9999; - box-shadow: 0 0 10px 2px $g2-kevlar; - - // Caret + box-shadow: 0 0 10px 2px $g2-kevlar; // Caret &:before { content: ''; border-width: 9px; @@ -251,8 +236,7 @@ $flux-invalid-color: $c-viridian; top: $flux-node-height / 2; left: 0; transform: translate(-100%, -50%); - } - // Invisible block to continue hovering + } // Invisible block to continue hovering &:after { content: ''; height: 50%; @@ -286,7 +270,6 @@ $flux-invalid-color: $c-viridian; flex-wrap: nowrap; align-items: center; margin-bottom: 4px; - &:last-of-type { margin-bottom: 0; } @@ -340,16 +323,16 @@ $flux-filter-parens: $g5-pepper; padding: 0 ($flux-filter-gap / 2); } -.flux-filter--value + .flux-filter--operator, -.flux-filter--paren-close + .flux-filter--operator { +.flux-filter--value+.flux-filter--operator, +.flux-filter--paren-close+.flux-filter--operator { padding: 0 $flux-filter-gap; } -.flux-filter--key + .flux-filter--operator { +.flux-filter--key+.flux-filter--operator { background-color: $flux-filter-expression; } -.flux-filter--key + .flux-filter--operator + .flux-filter--value { +.flux-filter--key+.flux-filter--operator+.flux-filter--value { background-color: $flux-filter-expression; border-radius: 0 3px 3px 0; } @@ -374,8 +357,7 @@ $flux-filter-parens: $g5-pepper; height: $flux-filter-unit-wrapped; width: ($flux-filter-unit-wrapped - $flux-filter-unit) / 2; background-color: $flux-filter-parens; - border: (($flux-filter-unit-wrapped - $flux-filter-unit) / 2) solid - $flux-filter-expression; + border: (($flux-filter-unit-wrapped - $flux-filter-unit) / 2) solid $flux-filter-expression; } .flux-filter--paren-open { @@ -424,4 +406,17 @@ $flux-filter-parens: $g5-pepper; background-color: $flux-filter-expression; height: $flux-filter-unit-wrapped; line-height: $flux-filter-unit-wrapped; +} + +.flux-filter--fancyscroll { + min-width: 300px; + min-height: 250px; +} + +.flux-filter--helper-text { + @include no-user-select(); + color: $g13-mist; + font-size: 12px; + font-weight: 500; + padding-left: 20px; } \ No newline at end of file diff --git a/ui/src/types/flux.ts b/ui/src/types/flux.ts index f5fc7c50d6..7daed20e2f 100644 --- a/ui/src/types/flux.ts +++ b/ui/src/types/flux.ts @@ -65,6 +65,26 @@ export interface MemberExpressionNode { property: PropertyNode } +export type FilterNode = BinaryExpressionNode | MemberExpressionNode + +export interface FilterTagCondition { + key: string + operator: string + value: string +} + +export interface FilterClause { + [tagKey: string]: FilterTagCondition[] +} + +export type SetFilterTagValue = ( + key: string, + value: string, + selected: boolean +) => void + +export type SetEquality = (tagKey: string, equal: boolean) => void + export interface FlatBody { type: string source: string diff --git a/ui/test/flux/components/Filter.test.tsx b/ui/test/flux/components/Filter.test.tsx deleted file mode 100644 index 78e2ce81d6..0000000000 --- a/ui/test/flux/components/Filter.test.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react' -import {shallow} from 'enzyme' -import {Filter} from 'src/flux/components/Filter' - -jest.mock('src/flux/apis', () => require('mocks/flux/apis')) - -const setup = (override = {}) => { - const props = { - argKey: 'fn', - funcID: 'f1', - bodyID: 'b1', - declarationID: 'd1', - value: '(r) => r["measurement"] === "m1"', - onChangeArg: () => {}, - render: () =>
, - links: { - self: '', - ast: '', - suggestions: '', - }, - ...override, - } - - const wrapper = shallow() - - return { - wrapper, - props, - } -} - -describe('Flux.Components.Filter', () => { - describe('rendering', () => { - it('renders without errors', () => { - const {wrapper} = setup() - expect(wrapper.exists()).toBe(true) - }) - }) -}) diff --git a/ui/test/flux/components/FilterTagList.test.tsx b/ui/test/flux/components/FilterTagList.test.tsx new file mode 100644 index 0000000000..3cfb622447 --- /dev/null +++ b/ui/test/flux/components/FilterTagList.test.tsx @@ -0,0 +1,257 @@ +import React from 'react' +import {shallow} from 'enzyme' +import FilterTagList from 'src/flux/components/FilterTagList' +import FilterTagListItem from 'src/flux/components/FilterTagListItem' + +const setup = (override?) => { + const props = { + db: 'telegraf', + tags: ['cpu', '_measurement'], + filter: [], + func: { + id: 'f1', + args: [{key: 'fn', value: '(r) => true'}], + }, + nodes: [], + bodyID: 'b1', + declarationID: 'd1', + onChangeArg: () => {}, + onGenerateScript: () => {}, + ...override, + } + + const wrapper = shallow() + + return { + wrapper, + props, + } +} + +describe('Flux.Components.FilterTagList', () => { + describe('rendering', () => { + it('renders without errors', () => { + const {wrapper} = setup() + + expect(wrapper.exists()).toBe(true) + }) + }) + + it('renders a builder when the clause is parseable', () => { + const override = { + nodes: [{type: 'BooleanLiteral', source: 'true'}], + } + const {wrapper} = setup(override) + + const builderContents = wrapper.find(FilterTagListItem) + expect(builderContents).not.toHaveLength(0) + }) + + it('renders a builder when the clause cannot be parsed', () => { + const override = { + nodes: [{type: 'Unparseable', source: 'baconcannon'}], + } + const {wrapper} = setup(override) + + const builderContents = wrapper.find(FilterTagListItem) + expect(builderContents).toHaveLength(0) + }) + + describe('clause parseability', () => { + const parser = setup().wrapper.instance() as FilterTagList + + it('recognizes a simple `true` body', () => { + const nodes = [{type: 'BooleanLiteral', source: 'true'}] + const [clause, parseable] = parser.reduceNodesToClause(nodes, []) + + expect(parseable).toBe(true) + expect(clause).toEqual({}) + }) + + it('allows for an empty node list', () => { + const nodes = [] + const [clause, parseable] = parser.reduceNodesToClause(nodes, []) + + expect(parseable).toBe(true) + expect(clause).toEqual({}) + }) + + it('extracts a tag condition equality', () => { + const nodes = [ + {type: 'MemberExpression', property: {name: 'tagKey'}}, + {type: 'Operator', source: '=='}, + {type: 'StringLiteral', source: 'tagValue'}, + ] + const [clause, parseable] = parser.reduceNodesToClause(nodes, []) + + expect(parseable).toBe(true) + expect(clause).toEqual({ + tagKey: [{key: 'tagKey', operator: '==', value: 'tagValue'}], + }) + }) + + it('extracts a tag condition inequality', () => { + const nodes = [ + {type: 'MemberExpression', property: {name: 'tagKey'}}, + {type: 'Operator', source: '!='}, + {type: 'StringLiteral', source: 'tagValue'}, + ] + const [clause, parseable] = parser.reduceNodesToClause(nodes, []) + + expect(parseable).toBe(true) + expect(clause).toEqual({ + tagKey: [{key: 'tagKey', operator: '!=', value: 'tagValue'}], + }) + }) + + it('groups like keys together', () => { + const nodes = [ + {type: 'MemberExpression', property: {name: 'tagKey'}}, + {type: 'Operator', source: '!='}, + {type: 'StringLiteral', source: 'value1'}, + {type: 'MemberExpression', property: {name: 'tagKey'}}, + {type: 'Operator', source: '!='}, + {type: 'StringLiteral', source: 'value2'}, + ] + const [clause, parseable] = parser.reduceNodesToClause(nodes, []) + + expect(parseable).toBe(true) + expect(clause).toEqual({ + tagKey: [ + {key: 'tagKey', operator: '!=', value: 'value1'}, + {key: 'tagKey', operator: '!=', value: 'value2'}, + ], + }) + }) + + it('separates conditions with different keys', () => { + const nodes = [ + {type: 'MemberExpression', property: {name: 'key1'}}, + {type: 'Operator', source: '!='}, + {type: 'StringLiteral', source: 'value1'}, + {type: 'MemberExpression', property: {name: 'key2'}}, + {type: 'Operator', source: '!='}, + {type: 'StringLiteral', source: 'value2'}, + ] + const [clause, parseable] = parser.reduceNodesToClause(nodes, []) + + expect(parseable).toBe(true) + expect(clause).toEqual({ + key1: [{key: 'key1', operator: '!=', value: 'value1'}], + key2: [{key: 'key2', operator: '!=', value: 'value2'}], + }) + }) + + it('cannot recognize other operators', () => { + const nodes = [ + {type: 'MemberExpression', property: {name: 'tagKey'}}, + {type: 'Operator', source: '=~'}, + {type: 'StringLiteral', source: 'tagValue'}, + ] + const [clause, parseable] = parser.reduceNodesToClause(nodes, []) + + expect(parseable).toBe(false) + expect(clause).toEqual({}) + }) + + it('requires that operators be consistent within a key group', () => { + const nodes = [ + {type: 'MemberExpression', property: {name: 'tagKey'}}, + {type: 'Operator', source: '=='}, + {type: 'StringLiteral', source: 'tagValue'}, + {type: 'MemberExpression', property: {name: 'tagKey'}}, + {type: 'Operator', source: '!='}, + {type: 'StringLiteral', source: 'tagValue'}, + ] + const [clause, parseable] = parser.reduceNodesToClause(nodes, []) + + expect(parseable).toBe(false) + expect(clause).toEqual({}) + }) + + it('conditions must come in order to be recognizeable', () => { + const nodes = [ + {type: 'MemberExpression', property: {name: 'tagKey'}}, + {type: 'StringLiteral', source: 'tagValue'}, + {type: 'Operator', source: '=~'}, + ] + const [clause, parseable] = parser.reduceNodesToClause(nodes, []) + + expect(parseable).toBe(false) + expect(clause).toEqual({}) + }) + + it('does not recognize more esoteric types', () => { + const nodes = [ + {type: 'ArrayExpression', property: {name: 'tagKey'}}, + {type: 'MemberExpression', property: {name: 'tagKey'}}, + {type: 'StringLiteral', source: 'tagValue'}, + {type: 'Operator', source: '=~'}, + ] + const [clause, parseable] = parser.reduceNodesToClause(nodes, []) + + expect(parseable).toBe(false) + expect(clause).toEqual({}) + }) + }) + + describe('building a filter string', () => { + const builder = setup().wrapper.instance() as FilterTagList + + it('returns a simple filter with no conditions', () => { + const filterString = builder.buildFilterString({}) + expect(filterString).toEqual('() => true') + }) + + it('renders a single condition', () => { + const clause = { + myKey: [{key: 'myKey', operator: '==', value: 'val1'}], + } + const filterString = builder.buildFilterString(clause) + expect(filterString).toEqual('(r) => (r.myKey == "val1")') + }) + + it('groups like keys together', () => { + const clause = { + myKey: [ + {key: 'myKey', operator: '==', value: 'val1'}, + {key: 'myKey', operator: '==', value: 'val2'}, + ], + } + const filterString = builder.buildFilterString(clause) + expect(filterString).toEqual( + '(r) => (r.myKey == "val1" OR r.myKey == "val2")' + ) + }) + + it('joins conditions together with AND when operator is !=', () => { + const clause = { + myKey: [ + {key: 'myKey', operator: '!=', value: 'val1'}, + {key: 'myKey', operator: '!=', value: 'val2'}, + ], + } + const filterString = builder.buildFilterString(clause) + expect(filterString).toEqual( + '(r) => (r.myKey != "val1" AND r.myKey != "val2")' + ) + }) + + it('always uses AND to join conditions across keys', () => { + const clause = { + key1: [ + {key: 'key1', operator: '!=', value: 'val1'}, + {key: 'key1', operator: '!=', value: 'val2'}, + ], + key2: [ + {key: 'key2', operator: '==', value: 'val3'}, + {key: 'key2', operator: '==', value: 'val4'}, + ], + } + const filterString = builder.buildFilterString(clause) + expect(filterString).toEqual( + '(r) => (r.key1 != "val1" AND r.key1 != "val2") AND (r.key2 == "val3" OR r.key2 == "val4")' + ) + }) + }) +}) diff --git a/ui/test/flux/components/From.test.tsx b/ui/test/flux/components/FromDatabaseDropdown.test.tsx similarity index 73% rename from ui/test/flux/components/From.test.tsx rename to ui/test/flux/components/FromDatabaseDropdown.test.tsx index 0121027472..67c2f75d8d 100644 --- a/ui/test/flux/components/From.test.tsx +++ b/ui/test/flux/components/FromDatabaseDropdown.test.tsx @@ -1,6 +1,6 @@ import React from 'react' import {shallow} from 'enzyme' -import From from 'src/flux/components/From' +import FromDatabaseDropdown from 'src/flux/components/FromDatabaseDropdown' import {service} from 'test/resources' jest.mock('src/shared/apis/metaQuery', () => require('mocks/flux/apis')) @@ -16,14 +16,14 @@ const setup = () => { onChangeArg: () => {}, } - const wrapper = shallow() + const wrapper = shallow() return { wrapper, } } -describe('Flux.Components.From', () => { +describe('Flux.Components.FromDatabaseDropdown', () => { describe('rendering', () => { it('renders without errors', () => { const {wrapper} = setup() diff --git a/ui/yarn.lock b/ui/yarn.lock index 8ee7a3638d..4a9890070d 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -162,14 +162,10 @@ acorn@^4.0.3, acorn@^4.0.4: version "4.0.13" resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" -acorn@^5.0.0, acorn@^5.3.0: +acorn@^5.0.0, acorn@^5.2.1, acorn@^5.3.0, acorn@^5.5.0: version "5.6.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.6.1.tgz#c9e50c3e3717cf897f1b071ceadbb543bbc0a8d4" -acorn@^5.2.1, acorn@^5.5.0: - version "5.5.3" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9" - add-px-to-style@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/add-px-to-style/-/add-px-to-style-1.0.0.tgz#d0c135441fa8014a8137904531096f67f28f263a" @@ -451,18 +447,12 @@ async@^1.4.0, async@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" -async@^2.1.2, async@^2.3.0, async@^2.4.1: +async@^2.1.2, async@^2.1.4, async@^2.3.0, async@^2.4.1: version "2.6.0" resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" dependencies: lodash "^4.14.0" -async@^2.1.4: - version "2.6.1" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" - dependencies: - lodash "^4.17.10" - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -970,7 +960,7 @@ babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015 babel-runtime "^6.22.0" babel-template "^6.24.1" -babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1: +babel-plugin-transform-es2015-modules-commonjs@^6.23.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz#0d8394029b7dc6abe1a97ef181e00758dd2e5d8a" dependencies: @@ -979,7 +969,7 @@ babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-e babel-template "^6.26.0" babel-types "^6.26.0" -babel-plugin-transform-es2015-modules-commonjs@^6.26.2: +babel-plugin-transform-es2015-modules-commonjs@^6.24.1, babel-plugin-transform-es2015-modules-commonjs@^6.26.2: version "6.26.2" resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz#58a793863a9e7ca870bdc5a881117ffac27db6f3" dependencies: @@ -1741,15 +1731,7 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0, chalk@^2.0.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.2: +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.2: version "2.4.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.0.tgz#a060a297a6b57e15b61ca63ce84995daa0fe6e52" dependencies: @@ -2161,14 +2143,10 @@ core-js@^1.0.0: version "1.2.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" -core-js@^2.1.3: +core-js@^2.1.3, core-js@^2.4.0, core-js@^2.5.0: version "2.5.5" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.5.tgz#b14dde936c640c0579a6b50cabcc132dd6127e3b" -core-js@^2.4.0, core-js@^2.5.0: - version "2.5.7" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" - core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -2273,18 +2251,12 @@ cross-spawn@^5.0.1, cross-spawn@^5.1.0: shebang-command "^1.2.0" which "^1.2.9" -crossvent@1.5.0: +crossvent@1.5.0, crossvent@^1.3.1: version "1.5.0" resolved "https://registry.yarnpkg.com/crossvent/-/crossvent-1.5.0.tgz#3779c1242699e19417f0414e61b144753a52fd6d" dependencies: custom-event "1.0.0" -crossvent@^1.3.1: - version "1.5.5" - resolved "https://registry.yarnpkg.com/crossvent/-/crossvent-1.5.5.tgz#ad20878e4921e9be73d9d6976f8b2ecd0f71a0b1" - dependencies: - custom-event "^1.0.0" - cryptiles@2.x.x: version "2.0.5" resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" @@ -2457,10 +2429,6 @@ custom-event@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.0.tgz#2e4628be19dc4b214b5c02630c5971e811618062" -custom-event@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" - cyclist@~0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" @@ -2766,20 +2734,13 @@ domutils@1.1: dependencies: domelementtype "1" -domutils@1.5.1: +domutils@1.5.1, domutils@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" dependencies: dom-serializer "0" domelementtype "1" -domutils@^1.5.1: - version "1.7.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" - dependencies: - dom-serializer "0" - domelementtype "1" - duplexer@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" @@ -2924,17 +2885,7 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.5.1: - version "1.12.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165" - dependencies: - es-to-primitive "^1.1.1" - function-bind "^1.1.1" - has "^1.0.1" - is-callable "^1.1.3" - is-regex "^1.0.4" - -es-abstract@^1.6.1, es-abstract@^1.7.0: +es-abstract@^1.5.1, es-abstract@^1.6.1, es-abstract@^1.7.0: version "1.11.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.11.0.tgz#cce87d518f0496893b1a30cd8461835535480681" dependencies: @@ -3417,14 +3368,10 @@ extract-text-webpack-plugin@^3.0.2: schema-utils "^0.3.0" webpack-sources "^1.0.1" -extsprintf@1.3.0: +extsprintf@1.3.0, extsprintf@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" -extsprintf@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - fast-deep-equal@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" @@ -4293,22 +4240,10 @@ https-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" -iconv-lite@0.4.19: +iconv-lite@0.4.19, iconv-lite@^0.4.17, iconv-lite@^0.4.4, iconv-lite@^0.4.5, iconv-lite@~0.4.13: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" -iconv-lite@^0.4.17, iconv-lite@^0.4.5, iconv-lite@~0.4.13: - version "0.4.21" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.21.tgz#c47f8733d02171189ebc4a400f3218d348094798" - dependencies: - safer-buffer "^2.1.0" - -iconv-lite@^0.4.4: - version "0.4.23" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" - dependencies: - safer-buffer ">= 2.1.2 < 3" - icss-replace-symbols@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" @@ -5725,11 +5660,11 @@ lodash@4.17.4: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" -lodash@^4.0.0, lodash@^4.1.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1, lodash@~4.17.4: +lodash@^4.0.0, lodash@^4.1.0, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1: version "4.17.5" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" -lodash@^4.13.1, lodash@^4.14.0, lodash@^4.17.10, lodash@^4.17.4: +lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.2, lodash@~4.17.4: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" @@ -5764,14 +5699,7 @@ lower-case@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" -lru-cache@^4.0.1: - version "4.1.3" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c" - dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" - -lru-cache@^4.1.1: +lru-cache@^4.0.1, lru-cache@^4.1.1: version "4.1.2" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.2.tgz#45234b2e6e2f2b33da125624c4664929a0224c3f" dependencies: @@ -5962,7 +5890,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: dependencies: brace-expansion "^1.1.7" -minimist@0.0.8: +minimist@0.0.8, minimist@~0.0.1: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" @@ -5970,10 +5898,6 @@ minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" -minimist@~0.0.1: - version "0.0.10" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" - minipass@^2.2.1, minipass@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.3.tgz#a7dcc8b7b833f5d368759cce544dccb55f50f233" @@ -7336,7 +7260,7 @@ q@^1.1.2: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" -qs@6.5.1: +qs@6.5.1, qs@~6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" @@ -7344,10 +7268,6 @@ qs@~6.3.0: version "6.3.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c" -qs@~6.5.1: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - query-string@^4.1.0, query-string@^4.2.2: version "4.3.4" resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" @@ -7855,7 +7775,7 @@ request-promise-native@^1.0.5: stealthy-require "^1.1.0" tough-cookie ">=2.3.3" -request@2, request@^2.79.0: +request@2, request@^2.79.0, request@^2.83.0: version "2.85.0" resolved "https://registry.yarnpkg.com/request/-/request-2.85.0.tgz#5a03615a47c61420b3eb99b7dba204f83603e1fa" dependencies: @@ -7882,31 +7802,6 @@ request@2, request@^2.79.0: tunnel-agent "^0.6.0" uuid "^3.1.0" -request@^2.83.0: - version "2.87.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.87.0.tgz#32f00235cd08d482b4d0d68db93a829c0ed5756e" - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.6.0" - caseless "~0.12.0" - combined-stream "~1.0.5" - extend "~3.0.1" - forever-agent "~0.6.1" - form-data "~2.3.1" - har-validator "~5.0.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.17" - oauth-sign "~0.8.2" - performance-now "^2.1.0" - qs "~6.5.1" - safe-buffer "^5.1.1" - tough-cookie "~2.3.3" - tunnel-agent "^0.6.0" - uuid "^3.1.0" - request@~2.79.0: version "2.79.0" resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" @@ -8109,11 +8004,11 @@ rx@2.3.24: version "2.3.24" resolved "https://registry.yarnpkg.com/rx/-/rx-2.3.24.tgz#14f950a4217d7e35daa71bbcbe58eff68ea4b2b7" -safe-buffer@5.1.1, safe-buffer@^5.1.0: +safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" -safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -8123,10 +8018,6 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - sane@^2.0.0: version "2.5.2" resolved "https://registry.yarnpkg.com/sane/-/sane-2.5.2.tgz#b4dc1861c21b427e929507a3e751e2a2cb8ab3fa" @@ -8583,11 +8474,7 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" -"statuses@>= 1.3.1 < 2", "statuses@>= 1.4.0 < 2": - version "1.5.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - -statuses@~1.4.0: +"statuses@>= 1.3.1 < 2", "statuses@>= 1.4.0 < 2", statuses@~1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" @@ -8669,11 +8556,7 @@ string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" -stringstream@~0.0.4: - version "0.0.5" - resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" - -stringstream@~0.0.5: +stringstream@~0.0.4, stringstream@~0.0.5: version "0.0.6" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.6.tgz#7880225b0d4ad10e30927d167a1d6f2fd3b33a72" @@ -9530,13 +9413,7 @@ which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" -which@1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" - dependencies: - isexe "^2.0.0" - -which@^1.2.12, which@^1.2.9, which@^1.3.0: +which@1, which@^1.2.12, which@^1.2.9, which@^1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" dependencies: @@ -9552,14 +9429,10 @@ window-size@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" -wordwrap@0.0.2: +wordwrap@0.0.2, wordwrap@~0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" -wordwrap@~0.0.2: - version "0.0.3" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" - wordwrap@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"