Merge remote-tracking branch 'origin/master' into fix/tempvars_url_query

pull/10616/head
Jared Scheib 2018-06-11 14:08:57 -07:00
commit b1d1876621
67 changed files with 2121 additions and 953 deletions

View File

@ -69,12 +69,12 @@ interface LoadDashboardsAction {
type: 'LOAD_DASHBOARDS' type: 'LOAD_DASHBOARDS'
payload: { payload: {
dashboards: Dashboard[] dashboards: Dashboard[]
dashboardID: string dashboardID: number
} }
} }
export const loadDashboards = ( export const loadDashboards = (
dashboards: Dashboard[], dashboards: Dashboard[],
dashboardID?: string dashboardID?: number
): LoadDashboardsAction => ({ ): LoadDashboardsAction => ({
type: 'LOAD_DASHBOARDS', type: 'LOAD_DASHBOARDS',
payload: { payload: {
@ -211,24 +211,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 { interface SyncDashboardCellAction {
type: 'SYNC_DASHBOARD_CELL' type: 'SYNC_DASHBOARD_CELL'
payload: { payload: {
@ -265,76 +247,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 { interface DeleteDashboardCellAction {
type: 'DELETE_DASHBOARD_CELL' type: 'DELETE_DASHBOARD_CELL'
payload: { payload: {
@ -374,13 +286,13 @@ export const editCellQueryStatus = (
interface TemplateVariableSelectedAction { interface TemplateVariableSelectedAction {
type: 'TEMPLATE_VARIABLE_SELECTED' type: 'TEMPLATE_VARIABLE_SELECTED'
payload: { payload: {
dashboardID: string dashboardID: number
templateID: string templateID: string
values: any[] values: any[]
} }
} }
export const templateVariableSelected = ( export const templateVariableSelected = (
dashboardID: string, dashboardID: number,
templateID: string, templateID: string,
values values
): TemplateVariableSelectedAction => ({ ): TemplateVariableSelectedAction => ({
@ -566,7 +478,7 @@ export const putDashboard = (dashboard: Dashboard) => async (
} }
} }
export const putDashboardByID = (dashboardID: string) => async ( export const putDashboardByID = (dashboardID: number) => async (
dispatch, dispatch,
getState getState
): Promise<void> => { ): Promise<void> => {

View File

@ -80,7 +80,7 @@ interface Props {
onCancel: () => void onCancel: () => void
onSave: (cell: Cell) => void onSave: (cell: Cell) => void
source: Source source: Source
dashboardID: string dashboardID: number
queryStatus: QueryStatus queryStatus: QueryStatus
autoRefresh: number autoRefresh: number
templates: Template[] templates: Template[]

View File

@ -51,7 +51,7 @@ export const FORMAT_OPTIONS: Array<{text: string}> = [
export type NewDefaultCell = Pick< export type NewDefaultCell = Pick<
Cell, Cell,
Exclude<keyof Cell, 'id' | 'axes' | 'colors' | 'links' | 'legend'> Exclude<keyof Cell, 'i' | 'axes' | 'colors' | 'links' | 'legend'>
> >
export const NEW_DEFAULT_DASHBOARD_CELL: NewDefaultCell = { export const NEW_DEFAULT_DASHBOARD_CELL: NewDefaultCell = {
x: 0, x: 0,

View File

@ -539,8 +539,6 @@ DashboardPage.propTypes = {
getDashboardWithHydratedAndSyncedTempVarsAsync: func.isRequired, getDashboardWithHydratedAndSyncedTempVarsAsync: func.isRequired,
setTimeRange: func.isRequired, setTimeRange: func.isRequired,
addDashboardCellAsync: func.isRequired, addDashboardCellAsync: func.isRequired,
editDashboardCell: func.isRequired,
cancelEditCell: func.isRequired,
}).isRequired, }).isRequired,
dashboards: arrayOf( dashboards: arrayOf(
shape({ shape({

View File

@ -6,7 +6,7 @@ import {applyDashboardTempVarOverrides} from 'src/dashboards/utils/tempVars'
const {lower, upper} = timeRanges.find(tr => tr.lower === 'now() - 1h') const {lower, upper} = timeRanges.find(tr => tr.lower === 'now() - 1h')
const initialState = { export const initialState = {
dashboards: [], dashboards: [],
timeRange: {lower, upper}, timeRange: {lower, upper},
zoomedTimeRange: {lower: null, upper: null}, zoomedTimeRange: {lower: null, upper: null},
@ -19,7 +19,7 @@ const initialState = {
import {TEMPLATE_VARIABLE_TYPES} from 'src/dashboards/constants' import {TEMPLATE_VARIABLE_TYPES} from 'src/dashboards/constants'
export default function ui(state = initialState, action) { const ui = (state = initialState, action) => {
switch (action.type) { switch (action.type) {
case 'LOAD_DASHBOARDS': { case 'LOAD_DASHBOARDS': {
const {dashboards} = action.payload const {dashboards} = action.payload
@ -84,23 +84,6 @@ export default function ui(state = initialState, action) {
return {...state, ...newState} 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': { case 'ADD_DASHBOARD_CELL': {
const {cell, dashboard} = action.payload const {cell, dashboard} = action.payload
const {dashboards} = state const {dashboards} = state
@ -115,30 +98,6 @@ export default function ui(state = initialState, action) {
return {...state, ...newState} 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': { case 'DELETE_DASHBOARD_CELL': {
const {dashboard, cell} = action.payload const {dashboard, cell} = action.payload
@ -158,24 +117,6 @@ export default function ui(state = initialState, action) {
return {...state, ...newState} 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': { case 'SYNC_DASHBOARD_CELL': {
const {cell, dashboard} = action.payload const {cell, dashboard} = action.payload
@ -195,30 +136,6 @@ export default function ui(state = initialState, action) {
return {...state, ...newState} 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': { case 'EDIT_CELL_QUERY_STATUS': {
const {queryID, status} = action.payload const {queryID, status} = action.payload
@ -321,3 +238,5 @@ export default function ui(state = initialState, action) {
return state return state
} }
export default ui

View File

@ -4,6 +4,7 @@ import AJAX from 'src/utils/ajax'
import {Service, FluxTable} from 'src/types' import {Service, FluxTable} from 'src/types'
import {updateService} from 'src/shared/apis' import {updateService} from 'src/shared/apis'
import {parseResponse} from 'src/shared/parsing/flux/response' import {parseResponse} from 'src/shared/parsing/flux/response'
import {MAX_RESPONSE_BYTES} from 'src/flux/constants'
export const getSuggestions = async (url: string) => { export const getSuggestions = async (url: string) => {
try { try {
@ -39,23 +40,36 @@ export const getAST = async (request: ASTRequest) => {
} }
} }
interface GetTimeSeriesResult {
didTruncate: boolean
tables: FluxTable[]
}
export const getTimeSeries = async ( export const getTimeSeries = async (
service: Service, service: Service,
script: string script: string
): Promise<FluxTable[]> => { ): Promise<GetTimeSeriesResult> => {
const and = encodeURIComponent('&') const and = encodeURIComponent('&')
const mark = encodeURIComponent('?') const mark = encodeURIComponent('?')
const garbage = script.replace(/\s/g, '') // server cannot handle whitespace 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 { try {
const {data} = await AJAX({ // We are using the `fetch` API here since the `AJAX` utility lacks support
method: 'POST', // for limiting response size. The `AJAX` utility depends on
url: `${ // `axios.request` which _does_ have a `maxContentLength` option, though it
service.links.proxy // seems to be broken at the moment. We might use this option instead of
}?path=/v1/query${mark}orgName=defaulorgname${and}q=${garbage}`, // 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) { } catch (error) {
console.error('Problem fetching data', error) console.error('Problem fetching data', error)
@ -114,3 +128,43 @@ export const updateScript = async (service: Service, script: string) => {
throw error throw error
} }
} }
interface DecodeFluxRespWithLimitResult {
body: string
byteLength: number
}
const decodeFluxRespWithLimit = async (
resp: Response
): Promise<DecodeFluxRespWithLimitResult> => {
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}
}

View File

@ -1,6 +1,7 @@
import React, {PureComponent} from 'react' import React, {PureComponent} from 'react'
import _ from 'lodash' import _ from 'lodash'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import ExpressionNode from 'src/flux/components/ExpressionNode' import ExpressionNode from 'src/flux/components/ExpressionNode'
import VariableName from 'src/flux/components/VariableName' import VariableName from 'src/flux/components/VariableName'
import FuncSelector from 'src/flux/components/FuncSelector' import FuncSelector from 'src/flux/components/FuncSelector'
@ -29,7 +30,7 @@ class BodyBuilder extends PureComponent<Props> {
if (d.funcs) { if (d.funcs) {
return ( return (
<div className="declaration" key={i}> <div className="declaration" key={i}>
<VariableName name={d.name} /> <VariableName name={d.name} assignedToQuery={true} />
<ExpressionNode <ExpressionNode
bodyID={b.id} bodyID={b.id}
declarationID={d.id} declarationID={d.id}
@ -43,7 +44,7 @@ class BodyBuilder extends PureComponent<Props> {
return ( return (
<div className="declaration" key={i}> <div className="declaration" key={i}>
<VariableName name={b.source} /> <VariableName name={b.source} assignedToQuery={false} />
</div> </div>
) )
}) })
@ -62,18 +63,20 @@ class BodyBuilder extends PureComponent<Props> {
}) })
return ( return (
<div className="body-builder"> <FancyScrollbar className="body-builder--container" autoHide={true}>
{_.flatten(bodybuilder)} <div className="body-builder">
<div className="declaration"> {_.flatten(bodybuilder)}
<FuncSelector <div className="declaration">
bodyID="fake-body-id" <FuncSelector
declarationID="fake-declaration-id" bodyID="fake-body-id"
onAddNode={this.createNewBody} declarationID="fake-declaration-id"
funcs={this.newDeclarationFuncs} onAddNode={this.createNewBody}
connectorVisible={false} funcs={this.newDeclarationFuncs}
/> connectorVisible={false}
/>
</div>
</div> </div>
</div> </FancyScrollbar>
) )
} }

View File

@ -85,25 +85,23 @@ class DatabaseListItem extends PureComponent<Props, State> {
const {db, service} = this.props const {db, service} = this.props
const {isOpen, searchTerm} = this.state const {isOpen, searchTerm} = this.state
if (isOpen) { return (
return ( <div className={`flux-schema--children ${isOpen ? '' : 'hidden'}`}>
<> <div className="flux-schema--filter">
<div className="flux-schema--filter"> <input
<input className="form-control input-xs"
className="form-control input-xs" placeholder={`Filter within ${db}`}
placeholder={`Filter within ${db}`} type="text"
type="text" spellCheck={false}
spellCheck={false} autoComplete="off"
autoComplete="off" value={searchTerm}
value={searchTerm} onClick={this.handleInputClick}
onClick={this.handleInputClick} onChange={this.onSearch}
onChange={this.onSearch} />
/> </div>
</div> <TagList db={db} service={service} tags={this.tags} filter={[]} />
<TagList db={db} service={service} tags={this.tags} filter={[]} /> </div>
</> )
)
}
} }
private handleClickCopy = e => { private handleClickCopy = e => {

View File

@ -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<Props, State> {
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)

View File

@ -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<Props, State> {
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 (
<FilterTagList
db={db}
service={service}
tags={this.state.tagKeys}
filter={[]}
onChangeArg={onChangeArg}
func={func}
nodes={nodes}
bodyID={bodyID}
declarationID={declarationID}
onGenerateScript={onGenerateScript}
/>
)
}
}
const mapStateToProps = ({links}) => {
return {links: links.flux}
}
export default connect(mapStateToProps, null)(FilterArgs)

View File

@ -0,0 +1,48 @@
import React, {PureComponent} from 'react'
import {FilterNode, MemberExpressionNode} from 'src/types/flux'
interface Props {
node: FilterNode
}
class FilterConditionNode extends PureComponent<Props> {
public render() {
const {node} = this.props
switch (node.type) {
case 'ObjectExpression': {
return <div className="flux-filter--key">{node.source}</div>
}
case 'MemberExpression': {
const memberNode = node as MemberExpressionNode
return (
<div className="flux-filter--key">{memberNode.property.name}</div>
)
}
case 'OpenParen': {
return <div className="flux-filter--paren-open" />
}
case 'CloseParen': {
return <div className="flux-filter--paren-close" />
}
case 'NumberLiteral':
case 'IntegerLiteral': {
return <div className="flux-filter--value number">{node.source}</div>
}
case 'BooleanLiteral': {
return <div className="flux-filter--value boolean">{node.source}</div>
}
case 'StringLiteral': {
return <div className="flux-filter--value string">{node.source}</div>
}
case 'Operator': {
return <div className="flux-filter--operator">{node.source}</div>
}
default: {
return <div />
}
}
}
}
export default FilterConditionNode

View File

@ -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<Props> {
public render() {
return (
<>
{this.props.nodes.map((n, i) => (
<FilterConditionNode node={n} key={i} />
))}
</>
)
}
}
export default FilterConditions

View File

@ -1,66 +1,58 @@
import React, {PureComponent} from 'react' import React, {PureComponent} from 'react'
import {BinaryExpressionNode, MemberExpressionNode} from 'src/types/flux' import {connect} from 'react-redux'
import {getAST} from 'src/flux/apis'
type FilterNode = BinaryExpressionNode & MemberExpressionNode import {Links, FilterNode} from 'src/types/flux'
import Walker from 'src/flux/ast/walker'
import FilterConditions from 'src/flux/components/FilterConditions'
interface Props { interface Props {
filterString?: string
links: Links
}
interface State {
nodes: FilterNode[] nodes: FilterNode[]
ast: object
} }
class FilterPreview extends PureComponent<Props> { export class FilterPreview extends PureComponent<Props, State> {
public render() { public static defaultProps: Partial<Props> = {
return ( filterString: '',
<>
{this.props.nodes.map((n, i) => <FilterPreviewNode node={n} key={i} />)}
</>
)
}
}
interface FilterPreviewNodeProps {
node: FilterNode
}
/* tslint:disable */
class FilterPreviewNode extends PureComponent<FilterPreviewNodeProps> {
public render() {
return this.className
} }
private get className(): JSX.Element { constructor(props) {
const {node} = this.props super(props)
this.state = {
switch (node.type) { nodes: [],
case 'ObjectExpression': { ast: {},
return <div className="flux-filter--key">{node.source}</div>
}
case 'MemberExpression': {
return <div className="flux-filter--key">{node.property.name}</div>
}
case 'OpenParen': {
return <div className="flux-filter--paren-open" />
}
case 'CloseParen': {
return <div className="flux-filter--paren-close" />
}
case 'NumberLiteral':
case 'IntegerLiteral': {
return <div className="flux-filter--value number">{node.source}</div>
}
case 'BooleanLiteral': {
return <div className="flux-filter--value boolean">{node.source}</div>
}
case 'StringLiteral': {
return <div className="flux-filter--value string">{node.source}</div>
}
case 'Operator': {
return <div className="flux-filter--operator">{node.source}</div>
}
default: {
return <div />
}
} }
} }
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 <FilterConditions nodes={this.state.nodes} />
}
} }
export default FilterPreview const mapStateToProps = ({links}) => {
return {links: links.flux}
}
export default connect(mapStateToProps, null)(FilterPreview)

View File

@ -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<Props> {
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<string>(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 (
<>
<p className="flux-filter--helper-text">
Unable to render expression as a Builder
</p>
<FuncArgTextArea
type={argTypes.STRING}
value={value}
argKey={argKey}
funcID={funcID}
bodyID={bodyID}
onChangeArg={onChangeArg}
declarationID={declarationID}
onGenerateScript={onGenerateScript}
/>
</>
)
}
if (tags.length) {
return (
<FancyScrollbar className="flux-filter--fancyscroll" maxHeight={600}>
{tags.map(t => (
<FilterTagListItem
key={t}
db={db}
tagKey={t}
conditions={this.conditions(t)}
operator={this.operator(t)}
onChangeValue={this.handleChangeValue}
onSetEquality={this.handleSetEquality}
service={service}
filter={filter}
/>
))}
</FancyScrollbar>
)
}
return (
<div className="flux-schema-tree">
<div className="flux-schema--item no-hover" onClick={this.handleClick}>
<div className="no-results">No tag keys found.</div>
</div>
</div>
)
}
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<HTMLDivElement>) {
e.stopPropagation()
}
}

View File

@ -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<Props, State> {
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 (
<ul className="nav nav-tablist nav-tablist-xs">
<li
className={operator === '==' ? 'active' : ''}
onClick={this.setEquality(true)}
>
=
</li>
<li
className={operator === '!=' ? 'active' : ''}
onClick={this.setEquality(false)}
>
!=
</li>
</ul>
)
}
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 (
<div className={this.className}>
<div className="flux-schema--item" onClick={this.handleClick}>
<div className="flex-schema-item-group">
<div className="flux-schema--expander" />
{tagKey}
<span className="flux-schema--type">Tag Key</span>
</div>
{this.renderEqualitySwitcher()}
</div>
{this.state.isOpen && (
<>
<div
className="flux-schema--header"
onClick={this.handleInputClick}
>
<div className="flux-schema--filter">
<input
className="form-control input-sm"
placeholder={`Filter within ${tagKey}`}
type="text"
spellCheck={false}
autoComplete="off"
value={searchTerm}
onChange={this.onSearch}
/>
{this.isSearching && (
<LoadingSpinner style={this.spinnerStyle} />
)}
</div>
{!!count && (
<div className="flux-schema--count">{`${count} Tag Values`}</div>
)}
</div>
{this.isLoading && <LoaderSkeleton />}
{!this.isLoading && (
<>
<FilterTagValueList
db={db}
service={service}
values={tagValues}
selectedValues={selectedValues}
tagKey={tagKey}
onChangeValue={this.props.onChangeValue}
filter={filter}
onLoadMoreValues={this.handleLoadMoreValues}
isLoadingMoreValues={loadingMore === RemoteDataState.Loading}
shouldShowMoreValues={limit < count}
loadMoreCount={this.loadMoreCount}
/>
</>
)}
</>
)}
</div>
)
}
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<HTMLInputElement>): void => {
const searchTerm = e.target.value
this.setState({searchTerm, loadingSearch: RemoteDataState.Loading}, () =>
this.debouncedOnSearch()
)
}
private debouncedOnSearch() {} // See constructor
private handleInputClick = (e: MouseEvent<HTMLDivElement>): 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<HTMLDivElement>) => {
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}`
}
}

View File

@ -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<Props> {
public render() {
const {values, tagKey, shouldShowMoreValues} = this.props
return (
<>
{values.map((v, i) => (
<FilterTagValueListItem
key={i}
value={v}
selected={_.includes(this.props.selectedValues, v)}
tagKey={tagKey}
onChangeValue={this.props.onChangeValue}
/>
))}
{shouldShowMoreValues && (
<div className="flux-schema-tree flux-schema--child">
<div className="flux-schema--item no-hover">
<button
className="btn btn-xs btn-default increase-values-limit"
onClick={this.handleClick}
>
{this.buttonValue}
</button>
</div>
</div>
)}
</>
)
}
private handleClick = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
this.props.onLoadMoreValues()
}
private get buttonValue(): string | JSX.Element {
const {isLoadingMoreValues, loadMoreCount, tagKey} = this.props
if (isLoadingMoreValues) {
return <LoadingSpinner />
}
return `Load next ${loadMoreCount} values for ${tagKey}`
}
}

View File

@ -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<Props> {
constructor(props) {
super(props)
}
public render() {
const {value} = this.props
return (
<div
className="flux-schema-tree flux-schema--child"
onClick={this.handleClick}
>
<div className={this.listItemClasses}>
<div className="flex-schema-item-group">
<div className="query-builder--checkbox" />
{value}
<span className="flux-schema--type">Tag Value</span>
</div>
</div>
</div>
)
}
private handleClick = (e: MouseEvent<HTMLDivElement>) => {
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

View File

@ -24,6 +24,7 @@ class FluxForm extends PureComponent<Props> {
value={this.url} value={this.url}
placeholder={this.url} placeholder={this.url}
onChange={onInputChange} onChange={onInputChange}
customClass="col-sm-6"
/> />
<Input <Input
name="name" name="name"
@ -32,6 +33,7 @@ class FluxForm extends PureComponent<Props> {
placeholder={service.name} placeholder={service.name}
onChange={onInputChange} onChange={onInputChange}
maxLength={33} maxLength={33}
customClass="col-sm-6"
/> />
<div className="form-group form-group-submit col-xs-12 text-center"> <div className="form-group form-group-submit col-xs-12 text-center">
<button <button

View File

@ -25,7 +25,7 @@ interface DropdownItem {
text: string text: string
} }
class From extends PureComponent<Props, State> { class FromDatabaseDropdown extends PureComponent<Props, State> {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
@ -82,4 +82,4 @@ class From extends PureComponent<Props, State> {
} }
} }
export default From export default FromDatabaseDropdown

View File

@ -4,7 +4,7 @@ import FuncArgInput from 'src/flux/components/FuncArgInput'
import FuncArgTextArea from 'src/flux/components/FuncArgTextArea' import FuncArgTextArea from 'src/flux/components/FuncArgTextArea'
import FuncArgBool from 'src/flux/components/FuncArgBool' import FuncArgBool from 'src/flux/components/FuncArgBool'
import {ErrorHandling} from 'src/shared/decorators/errors' 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 {funcNames, argTypes} from 'src/flux/constants'
import {OnChangeArg} from 'src/types/flux' import {OnChangeArg} from 'src/types/flux'
@ -41,7 +41,7 @@ class FuncArg extends PureComponent<Props> {
if (funcName === funcNames.FROM) { if (funcName === funcNames.FROM) {
return ( return (
<From <FromDatabaseDropdown
service={service} service={service}
argKey={argKey} argKey={argKey}
funcID={funcID} funcID={funcID}

View File

@ -4,8 +4,10 @@ import {OnChangeArg} from 'src/types/flux'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
import {Func} from 'src/types/flux' import {Func} from 'src/types/flux'
import {funcNames} from 'src/flux/constants' import {funcNames} from 'src/flux/constants'
import Join from 'src/flux/components/Join' import JoinArgs from 'src/flux/components/JoinArgs'
import FilterArgs from 'src/flux/components/FilterArgs'
import {Service} from 'src/types' import {Service} from 'src/types'
import {getDeep} from 'src/utils/wrappers'
interface Props { interface Props {
func: Func func: Func
@ -21,55 +23,112 @@ interface Props {
@ErrorHandling @ErrorHandling
export default class FuncArgs extends PureComponent<Props> { export default class FuncArgs extends PureComponent<Props> {
public render() { public render() {
const {onDeleteFunc} = this.props
return (
<div className="func-node--tooltip">
<div className="func-args">{this.renderArguments}</div>
<div className="func-arg--buttons">
<div
className="btn btn-sm btn-danger btn-square"
onClick={onDeleteFunc}
>
<span className="icon trash" />
</div>
{this.build}
</div>
</div>
)
}
get renderArguments(): JSX.Element | JSX.Element[] {
const {func} = this.props
const {name: funcName} = func
if (funcName === funcNames.JOIN) {
return this.renderJoin
}
if (funcName === funcNames.FILTER) {
return this.renderFilter
}
return this.renderGeneralArguments
}
get renderGeneralArguments(): JSX.Element | JSX.Element[] {
const { const {
func, func,
bodyID, bodyID,
service, service,
onChangeArg, onChangeArg,
onDeleteFunc, declarationID,
onGenerateScript,
} = this.props
const {name: funcName, id: funcID} = func
return func.args.map(({key, value, type}) => (
<FuncArg
key={key}
type={type}
argKey={key}
value={value}
bodyID={bodyID}
funcID={funcID}
funcName={funcName}
service={service}
onChangeArg={onChangeArg}
declarationID={declarationID}
onGenerateScript={onGenerateScript}
/>
))
}
get renderFilter(): JSX.Element {
const {
func,
bodyID,
service,
onChangeArg,
declarationID,
onGenerateScript,
} = this.props
const value = getDeep<string>(func.args, '0.value', '')
return (
<FilterArgs
value={value}
func={func}
bodyID={bodyID}
declarationID={declarationID}
onChangeArg={onChangeArg}
onGenerateScript={onGenerateScript}
service={service}
db={'telegraf'}
/>
)
}
get renderJoin(): JSX.Element {
const {
func,
bodyID,
onChangeArg,
declarationID, declarationID,
onGenerateScript, onGenerateScript,
declarationsFromBody, declarationsFromBody,
} = this.props } = this.props
const {name: funcName, id: funcID} = func
return ( return (
<div className="func-node--tooltip"> <JoinArgs
{funcName === funcNames.JOIN ? ( func={func}
<Join bodyID={bodyID}
func={func} declarationID={declarationID}
bodyID={bodyID} onChangeArg={onChangeArg}
declarationID={declarationID} declarationsFromBody={declarationsFromBody}
onChangeArg={onChangeArg} onGenerateScript={onGenerateScript}
declarationsFromBody={declarationsFromBody} />
onGenerateScript={onGenerateScript}
/>
) : (
func.args.map(({key, value, type}) => (
<FuncArg
key={key}
type={type}
argKey={key}
value={value}
bodyID={bodyID}
funcID={funcID}
funcName={funcName}
service={service}
onChangeArg={onChangeArg}
declarationID={declarationID}
onGenerateScript={onGenerateScript}
/>
))
)}
<div className="func-node--buttons">
<div
className="btn btn-sm btn-danger func-node--delete"
onClick={onDeleteFunc}
>
Delete
</div>
{this.build}
</div>
</div>
) )
} }

View File

@ -4,7 +4,6 @@ import _ from 'lodash'
import {Func} from 'src/types/flux' import {Func} from 'src/types/flux'
import {funcNames} from 'src/flux/constants' import {funcNames} from 'src/flux/constants'
import Filter from 'src/flux/components/Filter'
import FilterPreview from 'src/flux/components/FilterPreview' import FilterPreview from 'src/flux/components/FilterPreview'
import {getDeep} from 'src/utils/wrappers' import {getDeep} from 'src/utils/wrappers'
@ -32,16 +31,12 @@ export default class FuncArgsPreview extends PureComponent<Props> {
return this.colorizedArguments return this.colorizedArguments
} }
return <Filter value={value} render={this.filterPreview} /> return <FilterPreview filterString={value} />
} }
return this.colorizedArguments return this.colorizedArguments
} }
private filterPreview = nodes => {
return <FilterPreview nodes={nodes} />
}
private get colorizedArguments(): JSX.Element | JSX.Element[] { private get colorizedArguments(): JSX.Element | JSX.Element[] {
const {func} = this.props const {func} = this.props
const {args} = func const {args} = func
@ -76,19 +71,19 @@ export default class FuncArgsPreview extends PureComponent<Props> {
case 'period': case 'period':
case 'duration': case 'duration':
case 'array': { case 'array': {
return <span className="variable-value--number">{argument}</span> return <span className="func-arg--number">{argument}</span>
} }
case 'bool': { case 'bool': {
return <span className="variable-value--boolean">{argument}</span> return <span className="func-arg--boolean">{argument}</span>
} }
case 'string': { case 'string': {
return <span className="variable-value--string">"{argument}"</span> return <span className="func-arg--string">"{argument}"</span>
} }
case 'object': { case 'object': {
return <span className="variable-value--object">{argument}</span> return <span className="func-arg--object">{argument}</span>
} }
case 'invalid': { case 'invalid': {
return <span className="variable-value--invalid">{argument}</span> return <span className="func-arg--invalid">{argument}</span>
} }
default: { default: {
return <span>{argument}</span> return <span>{argument}</span>

View File

@ -52,6 +52,7 @@ export default class FuncNode extends PureComponent<Props, State> {
onMouseEnter={this.handleMouseEnter} onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave} onMouseLeave={this.handleMouseLeave}
> >
<div className="func-node--connector" />
<div className="func-node--name">{func.name}</div> <div className="func-node--name">{func.name}</div>
<FuncArgsPreview func={func} /> <FuncArgsPreview func={func} />
{isExpanded && ( {isExpanded && (

View File

@ -22,7 +22,7 @@ interface DropdownItem {
text: string text: string
} }
class Join extends PureComponent<Props> { class JoinArgs extends PureComponent<Props> {
constructor(props: Props) { constructor(props: Props) {
super(props) super(props)
} }
@ -151,4 +151,4 @@ class Join extends PureComponent<Props> {
} }
} }
export default Join export default JoinArgs

View File

@ -12,17 +12,7 @@ interface Props {
filter: SchemaFilter[] filter: SchemaFilter[]
} }
interface State { export default class TagList extends PureComponent<Props> {
isOpen: boolean
}
export default class TagList extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {isOpen: false}
}
public render() { public render() {
const {db, service, tags, filter} = this.props const {db, service, tags, filter} = this.props

View File

@ -66,7 +66,14 @@ export default class TagListItem extends PureComponent<Props, State> {
public render() { public render() {
const {tagKey, db, service, filter} = this.props const {tagKey, db, service, filter} = this.props
const {tagValues, searchTerm, loadingMore, count, limit} = this.state const {
tagValues,
searchTerm,
loadingMore,
count,
limit,
isOpen,
} = this.state
return ( return (
<div className={this.className}> <div className={this.className}>
@ -83,46 +90,37 @@ export default class TagListItem extends PureComponent<Props, State> {
</div> </div>
</CopyToClipboard> </CopyToClipboard>
</div> </div>
{this.state.isOpen && ( <div className={`flux-schema--children ${isOpen ? '' : 'hidden'}`}>
<> <div className="flux-schema--header" onClick={this.handleInputClick}>
<div <div className="flux-schema--filter">
className="flux-schema--header" <input
onClick={this.handleInputClick} className="form-control input-xs"
> placeholder={`Filter within ${tagKey}`}
<div className="flux-schema--filter"> type="text"
<input spellCheck={false}
className="form-control input-xs" autoComplete="off"
placeholder={`Filter within ${tagKey}`} value={searchTerm}
type="text" onChange={this.onSearch}
spellCheck={false} />
autoComplete="off" {this.isSearching && <LoadingSpinner style={this.spinnerStyle} />}
value={searchTerm}
onChange={this.onSearch}
/>
{this.isSearching && (
<LoadingSpinner style={this.spinnerStyle} />
)}
</div>
{this.count}
</div> </div>
{this.isLoading && <LoaderSkeleton />} {this.count}
{!this.isLoading && ( </div>
<> {this.isLoading && <LoaderSkeleton />}
<TagValueList {!this.isLoading && (
db={db} <TagValueList
service={service} db={db}
values={tagValues} service={service}
tagKey={tagKey} values={tagValues}
filter={filter} tagKey={tagKey}
onLoadMoreValues={this.handleLoadMoreValues} filter={filter}
isLoadingMoreValues={loadingMore === RemoteDataState.Loading} onLoadMoreValues={this.handleLoadMoreValues}
shouldShowMoreValues={limit < count} isLoadingMoreValues={loadingMore === RemoteDataState.Loading}
loadMoreCount={this.loadMoreCount} shouldShowMoreValues={limit < count}
/> loadMoreCount={this.loadMoreCount}
</> />
)} )}
</> </div>
)}
</div> </div>
) )
} }
@ -315,7 +313,7 @@ export default class TagListItem extends PureComponent<Props, State> {
return ( return (
!isOpen && !isOpen &&
(loadingAll === RemoteDataState.NotStarted || (loadingAll === RemoteDataState.NotStarted ||
loadingAll !== RemoteDataState.Error) loadingAll === RemoteDataState.Error)
) )
} }

View File

@ -48,7 +48,7 @@ class TagValueListItem extends PureComponent<Props, State> {
public render() { public render() {
const {db, service, value} = this.props const {db, service, value} = this.props
const {searchTerm} = this.state const {searchTerm, isOpen} = this.state
return ( return (
<div className={this.className} onClick={this.handleClick}> <div className={this.className} onClick={this.handleClick}>
@ -65,35 +65,33 @@ class TagValueListItem extends PureComponent<Props, State> {
</div> </div>
</CopyToClipboard> </CopyToClipboard>
</div> </div>
{this.state.isOpen && ( <div className={`flux-schema--children ${isOpen ? '' : 'hidden'}`}>
<> {this.isLoading && <LoaderSkeleton />}
{this.isLoading && <LoaderSkeleton />} {!this.isLoading && (
{!this.isLoading && ( <>
<> {!!this.tags.length && (
{!!this.tags.length && ( <div className="flux-schema--filter">
<div className="flux-schema--filter"> <input
<input className="form-control input-xs"
className="form-control input-xs" placeholder={`Filter within ${value}`}
placeholder={`Filter within ${value}`} type="text"
type="text" spellCheck={false}
spellCheck={false} autoComplete="off"
autoComplete="off" value={searchTerm}
value={searchTerm} onClick={this.handleInputClick}
onClick={this.handleInputClick} onChange={this.onSearch}
onChange={this.onSearch} />
/> </div>
</div> )}
)} <TagList
<TagList db={db}
db={db} service={service}
service={service} tags={this.tags}
tags={this.tags} filter={this.filter}
filter={this.filter} />
/> </>
</> )}
)} </div>
</>
)}
</div> </div>
) )
} }
@ -177,7 +175,7 @@ class TagValueListItem extends PureComponent<Props, State> {
return ( return (
!isOpen && !isOpen &&
(loading === RemoteDataState.NotStarted || (loading === RemoteDataState.NotStarted ||
loading !== RemoteDataState.Error) loading === RemoteDataState.Error)
) )
} }
} }

View File

@ -8,25 +8,46 @@ import {vis} from 'src/flux/constants'
const NUM_FIXED_ROWS = 1 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 { interface Props {
table: FluxTable table: FluxTable
} }
interface State { interface State {
scrollLeft: number scrollLeft: number
filteredTable: FluxTable
} }
@ErrorHandling @ErrorHandling
export default class TimeMachineTable extends PureComponent<Props, State> { export default class TimeMachineTable extends PureComponent<Props, State> {
public static getDerivedStateFromProps({table}: Props) {
return {filteredTable: filterTable(table)}
}
constructor(props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
scrollLeft: 0, scrollLeft: 0,
filteredTable: filterTable(props.table),
} }
} }
public render() { public render() {
const {scrollLeft} = this.state const {scrollLeft, filteredTable} = this.state
return ( return (
<div style={{flex: '1 1 auto'}}> <div style={{flex: '1 1 auto'}}>
@ -73,7 +94,7 @@ export default class TimeMachineTable extends PureComponent<Props, State> {
cellRenderer={this.cellRenderer} cellRenderer={this.cellRenderer}
rowHeight={vis.TABLE_ROW_HEIGHT} rowHeight={vis.TABLE_ROW_HEIGHT}
height={height - this.headerOffset} height={height - this.headerOffset}
rowCount={this.table.data.length - NUM_FIXED_ROWS} rowCount={filteredTable.data.length - NUM_FIXED_ROWS}
/> />
)} )}
</ColumnSizer> </ColumnSizer>
@ -93,7 +114,9 @@ export default class TimeMachineTable extends PureComponent<Props, State> {
} }
private get columnCount(): number { 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 { private get headerOffset(): number {
@ -109,13 +132,15 @@ export default class TimeMachineTable extends PureComponent<Props, State> {
key, key,
style, style,
}: GridCellProps): React.ReactNode => { }: GridCellProps): React.ReactNode => {
const {filteredTable} = this.state
return ( return (
<div <div
key={key} key={key}
style={{...style, display: 'flex', alignItems: 'center'}} style={{...style, display: 'flex', alignItems: 'center'}}
className="table-graph-cell table-graph-cell__fixed-row" className="table-graph-cell table-graph-cell__fixed-row"
> >
{this.table.data[0][columnIndex]} {filteredTable.data[0][columnIndex]}
</div> </div>
) )
} }
@ -126,25 +151,12 @@ export default class TimeMachineTable extends PureComponent<Props, State> {
rowIndex, rowIndex,
style, style,
}: GridCellProps): React.ReactNode => { }: GridCellProps): React.ReactNode => {
const {filteredTable} = this.state
return ( return (
<div key={key} style={style} className="table-graph-cell"> <div key={key} style={style} className="table-graph-cell">
{this.table.data[rowIndex + NUM_FIXED_ROWS][columnIndex]} {filteredTable.data[rowIndex + NUM_FIXED_ROWS][columnIndex]}
</div> </div>
) )
} }
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,
}
}
} }

View File

@ -1,7 +1,8 @@
import React, {PureComponent} from 'react' import React, {PureComponent} from 'react'
interface Props { interface Props {
name?: string name: string
assignedToQuery: boolean
} }
interface State { interface State {
@ -10,7 +11,7 @@ interface State {
export default class VariableName extends PureComponent<Props, State> { export default class VariableName extends PureComponent<Props, State> {
public static defaultProps: Partial<Props> = { public static defaultProps: Partial<Props> = {
name: '', assignedToQuery: false,
} }
constructor(props) { constructor(props) {
@ -22,7 +23,14 @@ export default class VariableName extends PureComponent<Props, State> {
} }
public render() { public render() {
return <div className="variable-string">{this.nameElement}</div> const {assignedToQuery} = this.props
return (
<div className="variable-node">
{assignedToQuery && <div className="variable-node--connector" />}
{this.nameElement}
</div>
)
} }
private get nameElement(): JSX.Element { private get nameElement(): JSX.Element {
@ -32,7 +40,7 @@ export default class VariableName extends PureComponent<Props, State> {
return this.colorizeSyntax return this.colorizeSyntax
} }
return <span className="variable-name">{name}</span> return <span className="variable-node--name">{name}</span>
} }
private get colorizeSyntax(): JSX.Element { private get colorizeSyntax(): JSX.Element {
@ -42,14 +50,13 @@ export default class VariableName extends PureComponent<Props, State> {
const varValue = this.props.name.replace(/^[^=]+=/, '') const varValue = this.props.name.replace(/^[^=]+=/, '')
const valueIsString = varValue.endsWith('"') const valueIsString = varValue.endsWith('"')
return ( return (
<> <>
<span className="variable-name">{varName}</span> <span className="variable-node--name">{varName}</span>
{' = '} {' = '}
<span <span
className={ className={
valueIsString ? 'variable-value--string' : 'variable-value--number' valueIsString ? 'variable-node--string' : 'variable-node--number'
} }
> >
{varValue} {varValue}

View File

@ -6,4 +6,15 @@ import * as builder from 'src/flux/constants/builder'
import * as vis from 'src/flux/constants/vis' import * as vis from 'src/flux/constants/vis'
import * as explorer from 'src/flux/constants/explorer' import * as explorer from 'src/flux/constants/explorer'
export {ast, funcNames, argTypes, editor, builder, vis, explorer} const MAX_RESPONSE_BYTES = 1e7 // 10 MB
export {
ast,
funcNames,
argTypes,
editor,
builder,
vis,
explorer,
MAX_RESPONSE_BYTES,
}

View File

@ -9,6 +9,7 @@ import KeyboardShortcuts from 'src/shared/components/KeyboardShortcuts'
import { import {
analyzeSuccess, analyzeSuccess,
fluxTimeSeriesError, fluxTimeSeriesError,
fluxResponseTruncatedError,
} from 'src/shared/copy/notifications' } from 'src/shared/copy/notifications'
import {UpdateScript} from 'src/flux/actions' import {UpdateScript} from 'src/flux/actions'
@ -452,8 +453,13 @@ export class FluxPage extends PureComponent<Props, State> {
} }
try { try {
const data = await getTimeSeries(this.service, script) const {tables, didTruncate} = await getTimeSeries(this.service, script)
this.setState({data})
this.setState({data: tables})
if (didTruncate) {
notify(fluxResponseTruncatedError())
}
} catch (error) { } catch (error) {
this.setState({data: []}) this.setState({data: []})

View File

@ -39,7 +39,6 @@ const KapacitorFormInput: SFC<Props> = ({
KapacitorFormInput.defaultProps = { KapacitorFormInput.defaultProps = {
inputType: '', inputType: '',
customClass: 'col-sm-6',
} }
export default KapacitorFormInput export default KapacitorFormInput

View File

@ -63,7 +63,11 @@ export const saveToLocalStorage = ({
const appPersisted = {app: {persisted}} const appPersisted = {app: {persisted}}
const dashTimeV1 = {ranges: normalizer(ranges)} const dashTimeV1 = {ranges: normalizer(ranges)}
const minimalLogs = _.omit(logs, ['tableData', 'histogramData']) const minimalLogs = _.omit(logs, [
'tableData',
'histogramData',
'queryCount',
])
window.localStorage.setItem( window.localStorage.setItem(
'state', 'state',
@ -75,7 +79,7 @@ export const saveToLocalStorage = ({
dataExplorer, dataExplorer,
dataExplorerQueryConfigs, dataExplorerQueryConfigs,
script, script,
logs: {...minimalLogs, histogramData: [], tableData: {}}, logs: {...minimalLogs, histogramData: [], tableData: {}, queryCount: 0},
}) })
) )
} catch (err) { } catch (err) {

View File

@ -52,6 +52,16 @@ export enum ActionTypes {
AddFilter = 'LOGS_ADD_FILTER', AddFilter = 'LOGS_ADD_FILTER',
RemoveFilter = 'LOGS_REMOVE_FILTER', RemoveFilter = 'LOGS_REMOVE_FILTER',
ChangeFilter = 'LOGS_CHANGE_FILTER', ChangeFilter = 'LOGS_CHANGE_FILTER',
IncrementQueryCount = 'LOGS_INCREMENT_QUERY_COUNT',
DecrementQueryCount = 'LOGS_DECREMENT_QUERY_COUNT',
}
export interface IncrementQueryCountAction {
type: ActionTypes.IncrementQueryCount
}
export interface DecrementQueryCountAction {
type: ActionTypes.DecrementQueryCount
} }
export interface AddFilterAction { export interface AddFilterAction {
@ -161,6 +171,8 @@ export type Action =
| AddFilterAction | AddFilterAction
| RemoveFilterAction | RemoveFilterAction
| ChangeFilterAction | ChangeFilterAction
| DecrementQueryCountAction
| IncrementQueryCountAction
const getTimeRange = (state: State): TimeRange | null => const getTimeRange = (state: State): TimeRange | null =>
getDeep<TimeRange | null>(state, 'logs.timeRange', null) getDeep<TimeRange | null>(state, 'logs.timeRange', null)
@ -257,9 +269,26 @@ export const executeTableQueryAsync = () => async (
} }
} }
export const decrementQueryCount = () => ({
type: ActionTypes.DecrementQueryCount,
})
export const incrementQueryCount = () => ({
type: ActionTypes.IncrementQueryCount,
})
export const executeQueriesAsync = () => async dispatch => { export const executeQueriesAsync = () => async dispatch => {
dispatch(executeHistogramQueryAsync()) dispatch(incrementQueryCount())
dispatch(executeTableQueryAsync()) try {
await Promise.all([
dispatch(executeHistogramQueryAsync()),
dispatch(executeTableQueryAsync()),
])
} catch (ex) {
console.error('Could not make query requests')
} finally {
dispatch(decrementQueryCount())
}
} }
export const setSearchTermAsync = (searchTerm: string) => async dispatch => { export const setSearchTermAsync = (searchTerm: string) => async dispatch => {

View File

@ -99,7 +99,16 @@ class LogViewerHeader extends PureComponent<Props> {
return '' 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 { private get selectedNamespace(): string {

View File

@ -1,23 +1,23 @@
import React, {PureComponent} from 'react' import React, {PureComponent} from 'react'
import {Filter} from 'src/types/logs' import {Filter} from 'src/types/logs'
import FilterBlock from 'src/logs/components/LogsFilter' import FilterBlock from 'src/logs/components/LogsFilter'
import QueryResults from 'src/logs/components/QueryResults'
interface Props { interface Props {
numResults: number numResults: number
filters: Filter[] filters: Filter[]
queryCount: number
onDelete: (id: string) => void onDelete: (id: string) => void
onFilterChange: (id: string, operator: string, value: string) => void onFilterChange: (id: string, operator: string, value: string) => void
} }
class LogsFilters extends PureComponent<Props> { class LogsFilters extends PureComponent<Props> {
public render() { public render() {
const {numResults} = this.props const {numResults, queryCount} = this.props
return ( return (
<div className="logs-viewer--filter-bar"> <div className="logs-viewer--filter-bar">
<label className="logs-viewer--results-text"> <QueryResults count={numResults} queryCount={queryCount} />
Query returned <strong>{numResults} Events</strong>
</label>
<ul className="logs-viewer--filters">{this.renderFilters}</ul> <ul className="logs-viewer--filters">{this.renderFilters}</ul>
</div> </div>
) )

View File

@ -26,7 +26,7 @@ class LogsSearchBar extends PureComponent<Props, State> {
<div className="logs-viewer--search-input"> <div className="logs-viewer--search-input">
<input <input
type="text" type="text"
placeholder="Search logs using Keywords or Regular Expressions..." placeholder="Search logs using keywords or regular expressions..."
value={searchTerm} value={searchTerm}
onChange={this.handleChange} onChange={this.handleChange}
onKeyDown={this.handleInputKeyDown} onKeyDown={this.handleInputKeyDown}

View File

@ -7,9 +7,7 @@ import {getDeep} from 'src/utils/wrappers'
import FancyScrollbar from 'src/shared/components/FancyScrollbar' import FancyScrollbar from 'src/shared/components/FancyScrollbar'
const ROW_HEIGHT = 26 const ROW_HEIGHT = 26
const ROW_CHAR_LIMIT = 100 const CHAR_WIDTH = 9
const CHAR_WIDTH = 7
interface Props { interface Props {
data: { data: {
columns: string[] columns: string[]
@ -46,11 +44,14 @@ class LogsTable extends Component<Props, State> {
} }
private grid: React.RefObject<Grid> private grid: React.RefObject<Grid>
private headerGrid: React.RefObject<Grid>
private currentMessageWidth: number | null
constructor(props: Props) { constructor(props: Props) {
super(props) super(props)
this.grid = React.createRef() this.grid = React.createRef()
this.headerGrid = React.createRef()
this.state = { this.state = {
scrollTop: 0, scrollTop: 0,
@ -61,6 +62,15 @@ class LogsTable extends Component<Props, State> {
public componentDidUpdate() { public componentDidUpdate() {
this.grid.current.recomputeGridSize() this.grid.current.recomputeGridSize()
this.headerGrid.current.recomputeGridSize()
}
public componentDidMount() {
window.addEventListener('resize', this.handleWindowResize)
}
public componentWillUnmount() {
window.removeEventListener('resize', this.handleWindowResize)
} }
public render() { public render() {
@ -75,6 +85,7 @@ class LogsTable extends Component<Props, State> {
<AutoSizer> <AutoSizer>
{({width}) => ( {({width}) => (
<Grid <Grid
ref={this.headerGrid}
height={ROW_HEIGHT} height={ROW_HEIGHT}
rowHeight={ROW_HEIGHT} rowHeight={ROW_HEIGHT}
rowCount={1} rowCount={1}
@ -120,6 +131,12 @@ class LogsTable extends Component<Props, State> {
) )
} }
private handleWindowResize = () => {
this.currentMessageWidth = null
this.grid.current.recomputeGridSize()
this.headerGrid.current.recomputeGridSize()
}
private handleHeaderScroll = ({scrollLeft}) => this.setState({scrollLeft}) private handleHeaderScroll = ({scrollLeft}) => this.setState({scrollLeft})
private handleScrollbarScroll = (e: MouseEvent<JSX.Element>) => { private handleScrollbarScroll = (e: MouseEvent<JSX.Element>) => {
@ -127,15 +144,62 @@ class LogsTable extends Component<Props, State> {
this.handleScroll(target) this.handleScroll(target)
} }
private get widthMapping() {
return {
timestamp: 160,
procid: 80,
facility: 120,
severity: 22,
severity_1: 120,
host: 300,
}
}
private get messageWidth() {
if (this.currentMessageWidth) {
return this.currentMessageWidth
}
const columns = getDeep<string[]>(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<string>(this.props, `data.columns.${index + 1}`, '')
switch (column) {
case 'message':
return this.messageWidth
default:
return _.get(this.widthMapping, column, 200)
}
}
private get rowCharLimit(): number {
return Math.floor(this.messageWidth / CHAR_WIDTH)
}
private get columns(): string[] {
return getDeep<string[]>(this.props, 'data.columns', [])
}
private calculateMessageHeight = (index: number): number => { private calculateMessageHeight = (index: number): number => {
const columnIndex = this.props.data.columns.indexOf('message') const columnIndex = this.columns.indexOf('message')
const height = const value = getDeep(this.props, `data.values.${index}.${columnIndex}`, '')
(Math.floor( const lines = Math.round(value.length / this.rowCharLimit + 0.25)
this.props.data.values[index][columnIndex].length / ROW_CHAR_LIMIT
) + return Math.max(lines, 1) * (ROW_HEIGHT - 14) + 14
1) *
ROW_HEIGHT
return height
} }
private calculateTotalHeight = (): number => { private calculateTotalHeight = (): number => {
@ -181,27 +245,6 @@ class LogsTable extends Component<Props, State> {
} }
} }
private getColumnWidth = ({index}: {index: number}) => {
const column = getDeep<string>(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 { private header(key: string): string {
return getDeep<string>( return getDeep<string>(
{ {
@ -251,7 +294,9 @@ class LogsTable extends Component<Props, State> {
value = moment(+value / 1000000).format('YYYY/MM/DD HH:mm:ss') value = moment(+value / 1000000).format('YYYY/MM/DD HH:mm:ss')
break break
case 'message': case 'message':
value = _.replace(value, '\\n', '') if (value.indexOf(' ') > this.rowCharLimit - 5) {
value = _.truncate(value, {length: this.rowCharLimit - 5})
}
break break
case 'severity': case 'severity':
value = ( value = (
@ -283,6 +328,8 @@ class LogsTable extends Component<Props, State> {
data-tag-key={column} data-tag-key={column}
data-tag-value={value} data-tag-value={value}
onClick={this.handleTagClick} onClick={this.handleTagClick}
data-index={rowIndex}
onMouseOver={this.handleMouseEnter}
className="logs-viewer--clickable" className="logs-viewer--clickable"
> >
{value} {value}
@ -293,7 +340,9 @@ class LogsTable extends Component<Props, State> {
return ( return (
<div <div
className={classnames('logs-viewer--cell', {highlight: highlightRow})} className={classnames(`logs-viewer--cell ${column}--cell`, {
highlight: highlightRow,
})}
key={key} key={key}
style={style} style={style}
onMouseOver={this.handleMouseEnter} onMouseOver={this.handleMouseEnter}

View File

@ -0,0 +1,28 @@
import React, {PureComponent} from 'react'
interface Props {
count: number
queryCount: number
}
class QueryResults extends PureComponent<Props> {
public render() {
const {count} = this.props
if (this.isPending) {
return <label className="logs-viewer--results-text">Querying ...</label>
}
return (
<label className="logs-viewer--results-text">
Query returned <strong>{count} Events</strong>
</label>
)
}
private get isPending(): boolean {
const {queryCount} = this.props
return queryCount > 0
}
}
export default QueryResults

View File

@ -47,6 +47,7 @@ interface Props {
} }
searchTerm: string searchTerm: string
filters: Filter[] filters: Filter[]
queryCount: number
} }
interface State { interface State {
@ -88,7 +89,7 @@ class LogsPage extends PureComponent<Props, State> {
public render() { public render() {
const {liveUpdating} = this.state const {liveUpdating} = this.state
const {searchTerm, filters} = this.props const {searchTerm, filters, queryCount} = this.props
const count = getDeep(this.props, 'tableData.values.length', 0) const count = getDeep(this.props, 'tableData.values.length', 0)
@ -106,6 +107,7 @@ class LogsPage extends PureComponent<Props, State> {
filters={filters || []} filters={filters || []}
onDelete={this.handleFilterDelete} onDelete={this.handleFilterDelete}
onFilterChange={this.handleFilterChange} onFilterChange={this.handleFilterChange}
queryCount={queryCount}
/> />
<LogsTable <LogsTable
data={this.props.tableData} data={this.props.tableData}
@ -119,13 +121,19 @@ class LogsPage extends PureComponent<Props, State> {
) )
} }
private get isSpecificTimeRange(): boolean {
return !!getDeep(this.props, 'timeRange.upper', false)
}
private startUpdating = () => { private startUpdating = () => {
if (this.interval) { if (this.interval) {
clearInterval(this.interval) clearInterval(this.interval)
} }
this.interval = setInterval(this.handleInterval, 10000) if (!this.isSpecificTimeRange) {
this.setState({liveUpdating: true}) this.interval = setInterval(this.handleInterval, 10000)
this.setState({liveUpdating: true})
}
} }
private handleScrollToTop = () => { private handleScrollToTop = () => {
@ -180,7 +188,7 @@ class LogsPage extends PureComponent<Props, State> {
return ( return (
<LogViewerHeader <LogViewerHeader
liveUpdating={liveUpdating} liveUpdating={liveUpdating && !this.isSpecificTimeRange}
availableSources={sources} availableSources={sources}
timeRange={timeRange} timeRange={timeRange}
onChooseSource={this.handleChooseSource} onChooseSource={this.handleChooseSource}
@ -254,6 +262,7 @@ const mapStateToProps = ({
tableData, tableData,
searchTerm, searchTerm,
filters, filters,
queryCount,
}, },
}) => ({ }) => ({
sources, sources,
@ -265,6 +274,7 @@ const mapStateToProps = ({
tableData, tableData,
searchTerm, searchTerm,
filters, filters,
queryCount,
}) })
const mapDispatchToProps = { const mapDispatchToProps = {

View File

@ -5,6 +5,8 @@ import {
RemoveFilterAction, RemoveFilterAction,
AddFilterAction, AddFilterAction,
ChangeFilterAction, ChangeFilterAction,
DecrementQueryCountAction,
IncrementQueryCountAction,
} from 'src/logs/actions' } from 'src/logs/actions'
import {LogsState} from 'src/types/logs' import {LogsState} from 'src/types/logs'
@ -19,6 +21,7 @@ const defaultState: LogsState = {
histogramData: [], histogramData: [],
searchTerm: null, searchTerm: null,
filters: [], filters: [],
queryCount: 0,
} }
const removeFilter = ( const removeFilter = (
@ -56,6 +59,22 @@ const changeFilter = (
return {...state, filters: mappedFilters} return {...state, filters: mappedFilters}
} }
const decrementQueryCount = (
state: LogsState,
__: DecrementQueryCountAction
) => {
const {queryCount} = state
return {...state, queryCount: Math.max(queryCount - 1, 0)}
}
const incrementQueryCount = (
state: LogsState,
__: IncrementQueryCountAction
) => {
const {queryCount} = state
return {...state, queryCount: queryCount + 1}
}
export default (state: LogsState = defaultState, action: Action) => { export default (state: LogsState = defaultState, action: Action) => {
switch (action.type) { switch (action.type) {
case ActionTypes.SetSource: case ActionTypes.SetSource:
@ -86,6 +105,10 @@ export default (state: LogsState = defaultState, action: Action) => {
return removeFilter(state, action) return removeFilter(state, action)
case ActionTypes.ChangeFilter: case ActionTypes.ChangeFilter:
return changeFilter(state, action) return changeFilter(state, action)
case ActionTypes.IncrementQueryCount:
return incrementQueryCount(state, action)
case ActionTypes.DecrementQueryCount:
return decrementQueryCount(state, action)
default: default:
return state return state
} }

View File

@ -155,7 +155,7 @@ const computeSeconds = (range: TimeRange) => {
const createGroupBy = (range: TimeRange) => { const createGroupBy = (range: TimeRange) => {
const seconds = computeSeconds(range) 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 = [] const tags = []
return {time, tags} return {time, tags}

View File

@ -1,4 +1,3 @@
/* eslint-disable no-magic-numbers */
import React, {Component} from 'react' import React, {Component} from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import {connect} from 'react-redux' import {connect} from 'react-redux'

View File

@ -102,7 +102,7 @@ export default class LayoutCell extends Component<Props> {
if (this.queries.length) { if (this.queries.length) {
const child = React.Children.only(children) const child = React.Children.only(children)
return React.cloneElement(child, {cellID: cell.id}) return React.cloneElement(child, {cellID: cell.i})
} }
return this.emptyGraph return this.emptyGraph

View File

@ -537,10 +537,6 @@ class TableGraph extends Component<Props, State> {
cellType: 'table', cellType: 'table',
}) })
// Argument of type '{ colors: ColorString; lastValue: ReactText; cellType: "table"; }' is not assignable to parameter of type '{ colors: any; lastValue: any; cellType?: CellType; }'.
// Types of property 'cellType' are incompatible.
// Type '"table"' is not assignable to type 'CellType'.
cellStyle = { cellStyle = {
...style, ...style,
backgroundColor: bgColor, backgroundColor: bgColor,

View File

@ -2,6 +2,7 @@
// and ensuring stylistic consistency // and ensuring stylistic consistency
import {FIVE_SECONDS, TEN_SECONDS, INFINITE} from 'src/shared/constants/index' import {FIVE_SECONDS, TEN_SECONDS, INFINITE} from 'src/shared/constants/index'
import {MAX_RESPONSE_BYTES} from 'src/flux/constants'
const defaultErrorNotification = { const defaultErrorNotification = {
type: 'error', type: 'error',
@ -705,3 +706,13 @@ export const fluxTimeSeriesError = (message: string) => ({
...defaultErrorNotification, ...defaultErrorNotification,
message: `Could not get data: ${message}`, 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`,
}
}

View File

@ -29,6 +29,12 @@ export const parseTables = (responseChunk: string): FluxTable[] => {
throw new Error('Unable to extract annotation data') 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 nonAnnotationData = Papa.parse(nonAnnotationLines).data
const annotationData = Papa.parse(annotationLines).data const annotationData = Papa.parse(annotationLines).data
const headerRow = nonAnnotationData[0] const headerRow = nonAnnotationData[0]

View File

@ -84,9 +84,10 @@ class SideNav extends PureComponent<Props> {
location={location} location={location}
> >
<NavHeader link={dataExplorerLink} title="Data Explorer" /> <NavHeader link={dataExplorerLink} title="Data Explorer" />
<FeatureFlag name="time-machine"> <NavHeader
<NavHeader link={`${sourcePrefix}/delorean`} title="Time Machine" /> link={`${sourcePrefix}/delorean`}
</FeatureFlag> title="Flux Query Interface"
/>
</NavBlock> </NavBlock>
<NavBlock <NavBlock
highlightWhen={['dashboards']} highlightWhen={['dashboards']}

View File

@ -10,25 +10,28 @@ $flux-func-selector--height: 30px;
display: flex; display: flex;
align-items: center; align-items: center;
position: relative; position: relative;
flex-direction: column;
&.open { &.open {
z-index: 9999; z-index: 9999;
height: $flux-func-selector--height + $flux-func-selector--gap;
} }
} }
.func-selector--connector { .func-selector--connector {
width: $flux-func-selector--gap; width: $flux-node-gap;
height: $flux-func-selector--height; height: $flux-func-selector--gap;
position: relative; position: relative;
&:after { &:after {
content: ''; content: '';
position: absolute; position: absolute;
top: 50%; top: -130%;
width: 100%; width: $flux-connector-line;
height: 4px; left: 50%;
transform: translateY(-50%); height: 230%;
@include gradient-h($g4-onyx, $c-pool); transform: translateX(-50%);
@include gradient-v($g4-onyx, $c-pool);
} }
} }
@ -51,7 +54,7 @@ $flux-func-selector--height: 30px;
top: 0; top: 0;
.func-selector--connector + & { .func-selector--connector + & {
left: $flux-func-selector--gap; top: $flux-func-selector--gap;
} }
} }

View File

@ -1,6 +1,13 @@
/*
Flux Builder Styles
------------------------------------------------------------------------------
*/
$flux-builder-min-width: 440px;
$flux-node-height: 30px; $flux-node-height: 30px;
$flux-node-tooltip-gap: 4px; $flux-node-tooltip-gap: 4px;
$flux-node-gap: 5px; $flux-connector-line: 2px;
$flux-node-gap: 30px;
$flux-node-padding: 10px; $flux-node-padding: 10px;
$flux-arg-min-width: 120px; $flux-arg-min-width: 120px;
$flux-number-color: $c-neutrino; $flux-number-color: $c-neutrino;
@ -8,11 +15,7 @@ $flux-object-color: $c-viridian;
$flux-string-color: $c-honeydew; $flux-string-color: $c-honeydew;
$flux-boolean-color: $c-viridian; $flux-boolean-color: $c-viridian;
$flux-invalid-color: $c-viridian; $flux-invalid-color: $c-viridian;
/* // Shared Node styles
Shared Node styles
------------------
*/
%flux-node { %flux-node {
min-height: $flux-node-height; min-height: $flux-node-height;
border-radius: $radius; border-radius: $radius;
@ -22,66 +25,104 @@ $flux-invalid-color: $c-viridian;
position: relative; position: relative;
background-color: $g4-onyx; background-color: $g4-onyx;
transition: background-color 0.25s ease; transition: background-color 0.25s ease;
margin-bottom: $flux-node-tooltip-gap / 2;
margin-top: $flux-node-tooltip-gap / 2;
&:hover { &:hover {
cursor: pointer;
background-color: $g6-smoke; background-color: $g6-smoke;
} }
} }
.body-builder { .body-builder--container {
padding: 30px;
min-width: 440px;
overflow: hidden;
height: 100%;
width: 100%;
background-color: $g1-raven; background-color: $g1-raven;
} }
.body-builder {
padding: $flux-node-height;
padding-bottom: 0;
min-width: $flux-builder-min-width;
width: 100%;
}
.declaration { .declaration {
width: 100%; width: 100%;
margin-bottom: 24px; margin-bottom: $flux-node-gap;
padding-bottom: $flux-node-gap;
border-bottom: 2px solid $g2-kevlar;
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
align-items: center; flex-direction: column;
align-items: flex-start;
&:last-of-type { &:last-of-type {
margin-bottom: 0; margin-bottom: 0;
border: 0;
} }
} }
.variable-string { .variable-node {
@extend %flux-node; @extend %flux-node;
color: $g11-sidewalk; color: $g11-sidewalk;
line-height: $flux-node-height; line-height: $flux-node-height;
white-space: nowrap; white-space: nowrap;
@include no-user-select(); @include no-user-select();
margin-top: 0;
&:hover {
background-color: $g4-onyx;
cursor: default;
}
} }
.variable-blank { .variable-node--connector {
font-style: italic; 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-name { .variable-node--name {
color: $c-pool; color: $c-pool;
.variable-node--connector+& {
margin-left: $flux-node-gap - $flux-node-padding;
}
} }
.variable-value--string { .variable-node--string,
.func-arg--string {
color: $flux-string-color; color: $flux-string-color;
} }
.variable-value--boolean { .variable-node--boolean,
.func-arg--boolean {
color: $flux-boolean-color; color: $flux-boolean-color;
} }
.variable-value--number { .variable-node--number,
.func-arg--number {
color: $flux-number-color; color: $flux-number-color;
} }
.variable-value--object { .variable-node--object,
.func-arg--object {
color: $flux-object-color; color: $flux-object-color;
} }
.variable-value--invalid { .variable-node--invalid,
.func-arg--invalid {
color: $flux-invalid-color; color: $flux-invalid-color;
} }
@ -89,24 +130,59 @@ $flux-invalid-color: $c-viridian;
@extend %flux-node; @extend %flux-node;
display: flex; display: flex;
align-items: center; align-items: center;
margin-left: $flux-node-gap; margin-left: $flux-node-gap;
}
// Connection Line .func-node--connector {
width: $flux-node-gap;
height: 100%;
position: absolute;
top: 0;
left: 0;
transform: translateX(-100%);
z-index: 0; // Connection Lines
&:before,
&:after { &:after {
content: ''; content: '';
height: 4px;
width: $flux-node-gap;
background-color: $g4-onyx; background-color: $g4-onyx;
position: absolute; 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%; top: 50%;
left: 0; left: $flux-node-gap / 2;
transform: translate(-100%, -50%); transform: translateY(-50%);
} }
}
&:first-child:after { // When a query exists unassigned to a variable
content: none; .func-node:first-child {
margin-left: 0; margin-left: 0;
padding-left: $flux-node-gap;
.func-node--connector {
transform: translateX(0);
z-index: 2; // Vertical Line
&:before {
height: $flux-node-gap;
top: $flux-node-gap / 2;
@include gradient-v($c-comet, $g4-onyx);
} // Dot
&:after {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: $c-comet;
top: $flux-node-gap / 2;
transform: translate(-50%, -50%);
}
} }
} }
@ -139,17 +215,15 @@ $flux-invalid-color: $c-viridian;
} }
} }
.func-node--tooltip, .func-node--tooltip {
.variable-name--tooltip {
background-color: $g3-castle; background-color: $g3-castle;
border-radius: $radius; border-radius: $radius;
padding: 10px; padding: 10px;
display: flex; display: flex;
align-items: stretch; align-items: stretch;
flex-direction: column;
position: absolute; position: absolute;
top: calc(100% + #{$flux-node-tooltip-gap}); top: 0;
left: 0; left: calc(100% + #{$flux-node-tooltip-gap});
z-index: 9999; z-index: 9999;
box-shadow: 0 0 10px 2px $g2-kevlar; // Caret box-shadow: 0 0 10px 2px $g2-kevlar; // Caret
&:before { &:before {
@ -157,33 +231,40 @@ $flux-invalid-color: $c-viridian;
border-width: 9px; border-width: 9px;
border-style: solid; border-style: solid;
border-color: transparent; border-color: transparent;
border-bottom-color: $g3-castle; border-right-color: $g3-castle;
position: absolute; position: absolute;
top: 0; top: $flux-node-height / 2;
left: $flux-node-padding + 3px; left: 0;
transform: translate(-50%, -100%); transform: translate(-100%, -50%);
} // Invisible block to continue hovering } // Invisible block to continue hovering
&:after { &:after {
content: ''; content: '';
width: 80%; height: 50%;
height: 7px; width: $flux-node-tooltip-gap * 3;
position: absolute; position: absolute;
top: -7px; top: 0;
left: 0; left: -$flux-node-tooltip-gap * 3;
} }
} }
.func-node--buttons { .func-arg--buttons {
display: flex; display: flex;
margin-top: 12px; flex-direction: column;
justify-content: center;
margin-left: 8px;
} }
.func-node--delete,
.func-node--build { .func-node--build {
width: 60px; 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; min-width: $flux-arg-min-width;
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
@ -212,26 +293,6 @@ $flux-invalid-color: $c-viridian;
width: 300px; 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 Filter Preview Styles
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
@ -262,16 +323,16 @@ $flux-filter-parens: $g5-pepper;
padding: 0 ($flux-filter-gap / 2); padding: 0 ($flux-filter-gap / 2);
} }
.flux-filter--value + .flux-filter--operator, .flux-filter--value+.flux-filter--operator,
.flux-filter--paren-close + .flux-filter--operator { .flux-filter--paren-close+.flux-filter--operator {
padding: 0 $flux-filter-gap; padding: 0 $flux-filter-gap;
} }
.flux-filter--key + .flux-filter--operator { .flux-filter--key+.flux-filter--operator {
background-color: $flux-filter-expression; 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; background-color: $flux-filter-expression;
border-radius: 0 3px 3px 0; border-radius: 0 3px 3px 0;
} }
@ -296,8 +357,7 @@ $flux-filter-parens: $g5-pepper;
height: $flux-filter-unit-wrapped; height: $flux-filter-unit-wrapped;
width: ($flux-filter-unit-wrapped - $flux-filter-unit) / 2; width: ($flux-filter-unit-wrapped - $flux-filter-unit) / 2;
background-color: $flux-filter-parens; background-color: $flux-filter-parens;
border: (($flux-filter-unit-wrapped - $flux-filter-unit) / 2) solid border: (($flux-filter-unit-wrapped - $flux-filter-unit) / 2) solid $flux-filter-expression;
$flux-filter-expression;
} }
.flux-filter--paren-open { .flux-filter--paren-open {
@ -347,3 +407,16 @@ $flux-filter-parens: $g5-pepper;
height: $flux-filter-unit-wrapped; height: $flux-filter-unit-wrapped;
line-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;
}

View File

@ -3,7 +3,7 @@
---------------------------------------------------------------------------- ----------------------------------------------------------------------------
*/ */
$flux-tree-min-width: 500px; $flux-tree-min-width: 250px;
$flux-tree-indent: 26px; $flux-tree-indent: 26px;
$flux-tree-line: 2px; $flux-tree-line: 2px;
$flux-tree-max-filter: 220px; $flux-tree-max-filter: 220px;
@ -22,11 +22,15 @@ $flux-tree-gutter: 11px;
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
padding-left: 0; padding-left: 0;
> .flux-schema-tree { > .flux-schema--children > .flux-schema-tree {
padding-left: $flux-tree-indent; padding-left: $flux-tree-indent;
} }
} }
.flux-schema--children.hidden {
display: none;
}
.flux-schema-tree__empty { .flux-schema-tree__empty {
height: $flux-tree-indent; height: $flux-tree-indent;
display: flex; display: flex;

View File

@ -242,6 +242,10 @@ $logs-viewer-gutter: 60px;
} }
} }
.message--cell {
word-break: break-all;
}
// Table Cell Styles // Table Cell Styles
.logs-viewer--cell { .logs-viewer--cell {
font-size: 12px; font-size: 12px;

View File

@ -65,7 +65,7 @@ export interface DecimalPlaces {
} }
export interface Cell { export interface Cell {
id: string i: string
x: number x: number
y: number y: number
w: number w: number

View File

@ -65,6 +65,26 @@ export interface MemberExpressionNode {
property: PropertyNode 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 { export interface FlatBody {
type: string type: string
source: string source: string

View File

@ -1,7 +1,7 @@
import {LayoutCell, LayoutQuery} from './layouts' import {LayoutCell, LayoutQuery} from './layouts'
import {Service, NewService} from './services' import {Service, NewService} from './services'
import {AuthLinks, Organization, Role, User, Me} from './auth' import {AuthLinks, Organization, Role, User, Me} from './auth'
import {Cell, CellQuery, Legend, Axes, Dashboard} from './dashboard' import {Cell, CellQuery, Legend, Axes, Dashboard, CellType} from './dashboard'
import {Template, TemplateQuery, TemplateValue, URLQueries} from './tempVars' import {Template, TemplateQuery, TemplateValue, URLQueries} from './tempVars'
import { import {
GroupBy, GroupBy,
@ -40,6 +40,7 @@ export {
TemplateValue, TemplateValue,
Cell, Cell,
CellQuery, CellQuery,
CellType,
Legend, Legend,
Status, Status,
Query, Query,

View File

@ -18,4 +18,5 @@ export interface LogsState {
tableData: object[] tableData: object[]
searchTerm: string | null searchTerm: string | null
filters: Filter[] filters: Filter[]
queryCount: number
} }

View File

@ -89,6 +89,7 @@ const flattenGroupBySeries = (
}, },
], ],
responseIndex, responseIndex,
isGroupBy: true,
}, },
] ]
@ -142,10 +143,11 @@ const constructResults = (
const constructSerieses = (results: Result[]): Series[] => { const constructSerieses = (results: Result[]): Series[] => {
return _.flatten( return _.flatten(
fastMap<Result, Series[]>(results, ({series, responseIndex}) => fastMap<Result, Series[]>(results, ({series, responseIndex, isGroupBy}) =>
fastMap<TimeSeriesSeries, Series>(series, (s, index) => ({ fastMap<TimeSeriesSeries, Series>(series, (s, index) => ({
...s, ...s,
responseIndex, responseIndex,
isGroupBy,
seriesIndex: index, seriesIndex: index,
})) }))
) )

View File

@ -24,7 +24,7 @@ const setup = (override = {}) => {
cell, cell,
timeRange, timeRange,
autoRefresh: 0, autoRefresh: 0,
dashboardID: '9', dashboardID: 9,
queryStatus: { queryStatus: {
queryID: null, queryID: null,
status: null, status: null,

View File

@ -1,43 +1,25 @@
import _ from 'lodash' import _ from 'lodash'
import reducer from 'src/dashboards/reducers/ui' import reducer from 'src/dashboards/reducers/ui'
import {template, dashboard, cell} from 'test/resources'
import {initialState} from 'src/dashboards/reducers/ui'
import { import {
setTimeRange,
loadDashboards, loadDashboards,
deleteDashboard, deleteDashboard,
deleteDashboardFailed,
setTimeRange,
updateDashboardCells,
editDashboardCell,
renameDashboardCell,
syncDashboardCell, syncDashboardCell,
deleteDashboardFailed,
templateVariableSelected, templateVariableSelected,
templateVariablesSelectedByName,
cancelEditCell,
editTemplateVariableValues, editTemplateVariableValues,
templateVariablesSelectedByName,
setActiveCell,
} from 'src/dashboards/actions' } from 'src/dashboards/actions'
let state 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 = { const t2 = {
...template,
id: '2', id: '2',
type: 'csv', type: 'csv',
label: 'test csv', label: 'test csv',
@ -49,35 +31,15 @@ const t2 = {
], ],
} }
const templates = [t1, t2] const templates = [template, t2]
const d1 = { const d1 = {
id: 1, ...dashboard,
cells: [],
name: 'd1',
templates, templates,
} }
const d2 = {id: 2, cells: [], name: 'd2', templates: []} const d2 = {...dashboard, id: 2, cells: [], name: 'd2', templates: []}
const dashboards = [d1, d2] 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', () => { describe('DataExplorer.Reducers.UI', () => {
it('can load the dashboards', () => { it('can load the dashboards', () => {
@ -90,11 +52,8 @@ describe('DataExplorer.Reducers.UI', () => {
}) })
it('can delete a dashboard', () => { it('can delete a dashboard', () => {
const initialState = {...state, dashboards} const actual = reducer({...initialState, dashboards}, deleteDashboard(d1))
const actual = reducer(initialState, deleteDashboard(d1)) const expected = dashboards.filter(dash => dash.id !== d1.id)
const expected = initialState.dashboards.filter(
dashboard => dashboard.id !== d1.id
)
expect(actual.dashboards).toEqual(expected) expect(actual.dashboards).toEqual(expected)
}) })
@ -117,43 +76,14 @@ describe('DataExplorer.Reducers.UI', () => {
expect(actual.timeRange).toEqual(expected) 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', () => { it('can sync a cell', () => {
const newCellName = 'watts is kinda cool' const newCellName = 'watts is kinda cool'
const newCell = { const newCell = {
x: c1.x, ...cell,
y: c1.y,
name: newCellName, name: newCellName,
} }
const dash = {...d1, cells: [c1]}
const dash = {...d1, cells: [cell]}
state = { state = {
dashboards: [dash], dashboards: [dash],
} }
@ -162,22 +92,6 @@ describe('DataExplorer.Reducers.UI', () => {
expect(actual.dashboards[0].cells[0].name).toBe(newCellName) 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', () => { it('can select a different template variable', () => {
const dash = _.cloneDeep(d1) const dash = _.cloneDeep(d1)
state = { state = {
@ -215,24 +129,20 @@ describe('DataExplorer.Reducers.UI', () => {
expect(actual.dashboards[0].templates[1].values[2].selected).toBe(false) expect(actual.dashboards[0].templates[1].values[2].selected).toBe(false)
}) })
it('can cancel cell editing', () => { describe('SET_ACTIVE_CELL', () => {
const dash = _.cloneDeep(d1) it('can set the active cell', () => {
dash.cells = [editingCell] const activeCellID = '1'
const actual = reducer(initialState, setActiveCell(activeCellID))
const actual = reducer( expect(actual.activeCellID).toEqual(activeCellID)
{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', () => { describe('EDIT_TEMPLATE_VARIABLE_VALUES', () => {
it('can edit the tempvar values', () => { it('can edit the tempvar values', () => {
const actual = reducer( const actual = reducer(
{dashboards}, {...initialState, dashboards},
editTemplateVariableValues(d1.id, t1.id, ['v1', 'v2']) editTemplateVariableValues(d1.id, template.id, ['v1', 'v2'])
) )
const expected = [ const expected = [
@ -252,12 +162,12 @@ describe('DataExplorer.Reducers.UI', () => {
}) })
it('can handle an empty template.values', () => { it('can handle an empty template.values', () => {
const ts = [{...t1, values: []}] const ts = [{...template, values: []}]
const ds = [{...d1, templates: ts}] const ds = [{...d1, templates: ts}]
const actual = reducer( const actual = reducer(
{dashboards: ds}, {...initialState, dashboards: ds},
editTemplateVariableValues(d1.id, t1.id, ['v1', 'v2']) editTemplateVariableValues(d1.id, template.id, ['v1', 'v2'])
) )
const expected = [ const expected = [

View File

@ -161,7 +161,7 @@ export const decimalPlaces: DecimalPlaces = {
} }
export const cell: Cell = { export const cell: Cell = {
id: '67435af2-17bf-4caa-a5fc-0dd1ffb40dab', i: '67435af2-17bf-4caa-a5fc-0dd1ffb40dab',
x: 0, x: 0,
y: 0, y: 0,
w: 8, w: 8,

View File

@ -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: () => <div className="test-element" />,
links: {
self: '',
ast: '',
suggestions: '',
},
...override,
}
const wrapper = shallow(<Filter {...props} />)
return {
wrapper,
props,
}
}
describe('Flux.Components.Filter', () => {
describe('rendering', () => {
it('renders without errors', () => {
const {wrapper} = setup()
expect(wrapper.exists()).toBe(true)
})
})
})

View File

@ -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(<FilterTagList {...props} />)
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")'
)
})
})
})

View File

@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import {shallow} from 'enzyme' import {shallow} from 'enzyme'
import From from 'src/flux/components/From' import FromDatabaseDropdown from 'src/flux/components/FromDatabaseDropdown'
import {service} from 'test/resources' import {service} from 'test/resources'
jest.mock('src/shared/apis/metaQuery', () => require('mocks/flux/apis')) jest.mock('src/shared/apis/metaQuery', () => require('mocks/flux/apis'))
@ -16,14 +16,14 @@ const setup = () => {
onChangeArg: () => {}, onChangeArg: () => {},
} }
const wrapper = shallow(<From {...props} />) const wrapper = shallow(<FromDatabaseDropdown {...props} />)
return { return {
wrapper, wrapper,
} }
} }
describe('Flux.Components.From', () => { describe('Flux.Components.FromDatabaseDropdown', () => {
describe('rendering', () => { describe('rendering', () => {
it('renders without errors', () => { it('renders without errors', () => {
const {wrapper} = setup() const {wrapper} = setup()

View File

@ -1,4 +1,4 @@
import {Source} from 'src/types' import {Source, Template, Dashboard, Cell, CellType} from 'src/types'
import {SourceLinks} from 'src/types/sources' import {SourceLinks} from 'src/types/sources'
export const role = { export const role = {
@ -586,3 +586,83 @@ export const hosts = {
load: 0, 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: {},
}

View File

@ -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: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 ,,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,,,,,,,,,,,,`

View File

@ -4,6 +4,7 @@ import {
RESPONSE_METADATA, RESPONSE_METADATA,
MULTI_SCHEMA_RESPONSE, MULTI_SCHEMA_RESPONSE,
EXPECTED_COLUMNS, EXPECTED_COLUMNS,
TRUNCATED_RESPONSE,
} from 'test/shared/parsing/flux/constants' } from 'test/shared/parsing/flux/constants'
describe('Flux results parser', () => { describe('Flux results parser', () => {
@ -37,4 +38,12 @@ describe('Flux results parser', () => {
expect(actual).toEqual(expected) 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)
})
})
}) })

View File

@ -169,14 +169,10 @@ acorn@^4.0.3, acorn@^4.0.4:
version "4.0.13" version "4.0.13"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" 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" version "5.6.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.6.1.tgz#c9e50c3e3717cf897f1b071ceadbb543bbc0a8d4" 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: add-px-to-style@1.0.0:
version "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" resolved "https://registry.yarnpkg.com/add-px-to-style/-/add-px-to-style-1.0.0.tgz#d0c135441fa8014a8137904531096f67f28f263a"
@ -458,18 +454,12 @@ async@^1.4.0, async@^1.5.2:
version "1.5.2" version "1.5.2"
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" 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" version "2.6.0"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4"
dependencies: dependencies:
lodash "^4.14.0" 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: asynckit@^0.4.0:
version "0.4.0" version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
@ -977,7 +967,7 @@ babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015
babel-runtime "^6.22.0" babel-runtime "^6.22.0"
babel-template "^6.24.1" 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" 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" resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz#0d8394029b7dc6abe1a97ef181e00758dd2e5d8a"
dependencies: dependencies:
@ -986,7 +976,7 @@ babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-e
babel-template "^6.26.0" babel-template "^6.26.0"
babel-types "^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" 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" resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz#58a793863a9e7ca870bdc5a881117ffac27db6f3"
dependencies: dependencies:
@ -1748,15 +1738,7 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
strip-ansi "^3.0.0" strip-ansi "^3.0.0"
supports-color "^2.0.0" supports-color "^2.0.0"
chalk@^2.0.0, chalk@^2.0.1: chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.2:
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:
version "2.4.0" version "2.4.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.0.tgz#a060a297a6b57e15b61ca63ce84995daa0fe6e52" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.0.tgz#a060a297a6b57e15b61ca63ce84995daa0fe6e52"
dependencies: dependencies:
@ -2168,14 +2150,10 @@ core-js@^1.0.0:
version "1.2.7" version "1.2.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" 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" version "2.5.5"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.5.tgz#b14dde936c640c0579a6b50cabcc132dd6127e3b" 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: core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@ -2280,18 +2258,12 @@ cross-spawn@^5.0.1, cross-spawn@^5.1.0:
shebang-command "^1.2.0" shebang-command "^1.2.0"
which "^1.2.9" which "^1.2.9"
crossvent@1.5.0: crossvent@1.5.0, crossvent@^1.3.1:
version "1.5.0" version "1.5.0"
resolved "https://registry.yarnpkg.com/crossvent/-/crossvent-1.5.0.tgz#3779c1242699e19417f0414e61b144753a52fd6d" resolved "https://registry.yarnpkg.com/crossvent/-/crossvent-1.5.0.tgz#3779c1242699e19417f0414e61b144753a52fd6d"
dependencies: dependencies:
custom-event "1.0.0" 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: cryptiles@2.x.x:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
@ -2464,10 +2436,6 @@ custom-event@1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.0.tgz#2e4628be19dc4b214b5c02630c5971e811618062" 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: cyclist@~0.2.2:
version "0.2.2" version "0.2.2"
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
@ -2773,20 +2741,13 @@ domutils@1.1:
dependencies: dependencies:
domelementtype "1" domelementtype "1"
domutils@1.5.1: domutils@1.5.1, domutils@^1.5.1:
version "1.5.1" version "1.5.1"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
dependencies: dependencies:
dom-serializer "0" dom-serializer "0"
domelementtype "1" 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: duplexer@^0.1.1:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
@ -2931,17 +2892,7 @@ error-ex@^1.2.0, error-ex@^1.3.1:
dependencies: dependencies:
is-arrayish "^0.2.1" is-arrayish "^0.2.1"
es-abstract@^1.5.1: es-abstract@^1.5.1, es-abstract@^1.6.1, es-abstract@^1.7.0:
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:
version "1.11.0" version "1.11.0"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.11.0.tgz#cce87d518f0496893b1a30cd8461835535480681" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.11.0.tgz#cce87d518f0496893b1a30cd8461835535480681"
dependencies: dependencies:
@ -3424,14 +3375,10 @@ extract-text-webpack-plugin@^3.0.2:
schema-utils "^0.3.0" schema-utils "^0.3.0"
webpack-sources "^1.0.1" webpack-sources "^1.0.1"
extsprintf@1.3.0: extsprintf@1.3.0, extsprintf@^1.2.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" 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: fast-deep-equal@^1.0.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614"
@ -4300,22 +4247,10 @@ https-browserify@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" 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" version "0.4.19"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" 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: icss-replace-symbols@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
@ -5732,11 +5667,11 @@ lodash@4.17.4:
version "4.17.4" version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" 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" version "4.17.5"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" 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" version "4.17.10"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
@ -5771,14 +5706,7 @@ lower-case@^1.1.1:
version "1.1.4" version "1.1.4"
resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac"
lru-cache@^4.0.1: lru-cache@^4.0.1, lru-cache@^4.1.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:
version "4.1.2" version "4.1.2"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.2.tgz#45234b2e6e2f2b33da125624c4664929a0224c3f" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.2.tgz#45234b2e6e2f2b33da125624c4664929a0224c3f"
dependencies: dependencies:
@ -5969,7 +5897,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
dependencies: dependencies:
brace-expansion "^1.1.7" brace-expansion "^1.1.7"
minimist@0.0.8: minimist@0.0.8, minimist@~0.0.1:
version "0.0.8" version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
@ -5977,10 +5905,6 @@ minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" 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: minipass@^2.2.1, minipass@^2.3.3:
version "2.3.3" version "2.3.3"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.3.tgz#a7dcc8b7b833f5d368759cce544dccb55f50f233" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.3.tgz#a7dcc8b7b833f5d368759cce544dccb55f50f233"
@ -7343,7 +7267,7 @@ q@^1.1.2:
version "1.5.1" version "1.5.1"
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" 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" version "6.5.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
@ -7351,10 +7275,6 @@ qs@~6.3.0:
version "6.3.2" version "6.3.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c" 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: query-string@^4.1.0, query-string@^4.2.2:
version "4.3.4" version "4.3.4"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
@ -7862,7 +7782,7 @@ request-promise-native@^1.0.5:
stealthy-require "^1.1.0" stealthy-require "^1.1.0"
tough-cookie ">=2.3.3" 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" version "2.85.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.85.0.tgz#5a03615a47c61420b3eb99b7dba204f83603e1fa" resolved "https://registry.yarnpkg.com/request/-/request-2.85.0.tgz#5a03615a47c61420b3eb99b7dba204f83603e1fa"
dependencies: dependencies:
@ -7889,31 +7809,6 @@ request@2, request@^2.79.0:
tunnel-agent "^0.6.0" tunnel-agent "^0.6.0"
uuid "^3.1.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: request@~2.79.0:
version "2.79.0" version "2.79.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de"
@ -8116,11 +8011,11 @@ rx@2.3.24:
version "2.3.24" version "2.3.24"
resolved "https://registry.yarnpkg.com/rx/-/rx-2.3.24.tgz#14f950a4217d7e35daa71bbcbe58eff68ea4b2b7" 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" version "5.1.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" 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" version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
@ -8130,10 +8025,6 @@ safe-regex@^1.1.0:
dependencies: dependencies:
ret "~0.1.10" 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: sane@^2.0.0:
version "2.5.2" version "2.5.2"
resolved "https://registry.yarnpkg.com/sane/-/sane-2.5.2.tgz#b4dc1861c21b427e929507a3e751e2a2cb8ab3fa" resolved "https://registry.yarnpkg.com/sane/-/sane-2.5.2.tgz#b4dc1861c21b427e929507a3e751e2a2cb8ab3fa"
@ -8590,11 +8481,7 @@ static-extend@^0.1.1:
define-property "^0.2.5" define-property "^0.2.5"
object-copy "^0.1.0" object-copy "^0.1.0"
"statuses@>= 1.3.1 < 2", "statuses@>= 1.4.0 < 2": "statuses@>= 1.3.1 < 2", "statuses@>= 1.4.0 < 2", statuses@~1.4.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
statuses@~1.4.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
@ -8676,11 +8563,7 @@ string_decoder@~0.10.x:
version "0.10.31" version "0.10.31"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
stringstream@~0.0.4: stringstream@~0.0.4, stringstream@~0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
stringstream@~0.0.5:
version "0.0.6" version "0.0.6"
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.6.tgz#7880225b0d4ad10e30927d167a1d6f2fd3b33a72" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.6.tgz#7880225b0d4ad10e30927d167a1d6f2fd3b33a72"
@ -9537,13 +9420,7 @@ which-module@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
which@1: which@1, which@^1.2.12, which@^1.2.9, which@^1.3.0:
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:
version "1.3.1" version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
dependencies: dependencies:
@ -9559,14 +9436,10 @@ window-size@0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" 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" version "0.0.2"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" 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: wordwrap@~1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"