Merge remote-tracking branch 'origin/master' into fix/tempvars_url_query
commit
b1d1876621
|
@ -69,12 +69,12 @@ interface LoadDashboardsAction {
|
|||
type: 'LOAD_DASHBOARDS'
|
||||
payload: {
|
||||
dashboards: Dashboard[]
|
||||
dashboardID: string
|
||||
dashboardID: number
|
||||
}
|
||||
}
|
||||
export const loadDashboards = (
|
||||
dashboards: Dashboard[],
|
||||
dashboardID?: string
|
||||
dashboardID?: number
|
||||
): LoadDashboardsAction => ({
|
||||
type: 'LOAD_DASHBOARDS',
|
||||
payload: {
|
||||
|
@ -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 {
|
||||
type: 'SYNC_DASHBOARD_CELL'
|
||||
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 {
|
||||
type: 'DELETE_DASHBOARD_CELL'
|
||||
payload: {
|
||||
|
@ -374,13 +286,13 @@ export const editCellQueryStatus = (
|
|||
interface TemplateVariableSelectedAction {
|
||||
type: 'TEMPLATE_VARIABLE_SELECTED'
|
||||
payload: {
|
||||
dashboardID: string
|
||||
dashboardID: number
|
||||
templateID: string
|
||||
values: any[]
|
||||
}
|
||||
}
|
||||
export const templateVariableSelected = (
|
||||
dashboardID: string,
|
||||
dashboardID: number,
|
||||
templateID: string,
|
||||
values
|
||||
): TemplateVariableSelectedAction => ({
|
||||
|
@ -566,7 +478,7 @@ export const putDashboard = (dashboard: Dashboard) => async (
|
|||
}
|
||||
}
|
||||
|
||||
export const putDashboardByID = (dashboardID: string) => async (
|
||||
export const putDashboardByID = (dashboardID: number) => async (
|
||||
dispatch,
|
||||
getState
|
||||
): Promise<void> => {
|
||||
|
|
|
@ -80,7 +80,7 @@ interface Props {
|
|||
onCancel: () => void
|
||||
onSave: (cell: Cell) => void
|
||||
source: Source
|
||||
dashboardID: string
|
||||
dashboardID: number
|
||||
queryStatus: QueryStatus
|
||||
autoRefresh: number
|
||||
templates: Template[]
|
||||
|
|
|
@ -51,7 +51,7 @@ export const FORMAT_OPTIONS: Array<{text: string}> = [
|
|||
|
||||
export type NewDefaultCell = Pick<
|
||||
Cell,
|
||||
Exclude<keyof Cell, 'id' | 'axes' | 'colors' | 'links' | 'legend'>
|
||||
Exclude<keyof Cell, 'i' | 'axes' | 'colors' | 'links' | 'legend'>
|
||||
>
|
||||
export const NEW_DEFAULT_DASHBOARD_CELL: NewDefaultCell = {
|
||||
x: 0,
|
||||
|
|
|
@ -539,8 +539,6 @@ DashboardPage.propTypes = {
|
|||
getDashboardWithHydratedAndSyncedTempVarsAsync: func.isRequired,
|
||||
setTimeRange: func.isRequired,
|
||||
addDashboardCellAsync: func.isRequired,
|
||||
editDashboardCell: func.isRequired,
|
||||
cancelEditCell: func.isRequired,
|
||||
}).isRequired,
|
||||
dashboards: arrayOf(
|
||||
shape({
|
||||
|
|
|
@ -6,7 +6,7 @@ import {applyDashboardTempVarOverrides} from 'src/dashboards/utils/tempVars'
|
|||
|
||||
const {lower, upper} = timeRanges.find(tr => tr.lower === 'now() - 1h')
|
||||
|
||||
const initialState = {
|
||||
export const initialState = {
|
||||
dashboards: [],
|
||||
timeRange: {lower, upper},
|
||||
zoomedTimeRange: {lower: null, upper: null},
|
||||
|
@ -19,7 +19,7 @@ const initialState = {
|
|||
|
||||
import {TEMPLATE_VARIABLE_TYPES} from 'src/dashboards/constants'
|
||||
|
||||
export default function ui(state = initialState, action) {
|
||||
const ui = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case 'LOAD_DASHBOARDS': {
|
||||
const {dashboards} = action.payload
|
||||
|
@ -84,23 +84,6 @@ export default function ui(state = initialState, action) {
|
|||
return {...state, ...newState}
|
||||
}
|
||||
|
||||
case 'UPDATE_DASHBOARD_CELLS': {
|
||||
const {cells, dashboard} = action.payload
|
||||
|
||||
const newDashboard = {
|
||||
...dashboard,
|
||||
cells,
|
||||
}
|
||||
|
||||
const newState = {
|
||||
dashboards: state.dashboards.map(
|
||||
d => (d.id === dashboard.id ? newDashboard : d)
|
||||
),
|
||||
}
|
||||
|
||||
return {...state, ...newState}
|
||||
}
|
||||
|
||||
case 'ADD_DASHBOARD_CELL': {
|
||||
const {cell, dashboard} = action.payload
|
||||
const {dashboards} = state
|
||||
|
@ -115,30 +98,6 @@ export default function ui(state = initialState, action) {
|
|||
return {...state, ...newState}
|
||||
}
|
||||
|
||||
case 'EDIT_DASHBOARD_CELL': {
|
||||
const {x, y, isEditing, dashboard} = action.payload
|
||||
|
||||
const cell = dashboard.cells.find(c => c.x === x && c.y === y)
|
||||
|
||||
const newCell = {
|
||||
...cell,
|
||||
isEditing,
|
||||
}
|
||||
|
||||
const newDashboard = {
|
||||
...dashboard,
|
||||
cells: dashboard.cells.map(c => (c.x === x && c.y === y ? newCell : c)),
|
||||
}
|
||||
|
||||
const newState = {
|
||||
dashboards: state.dashboards.map(
|
||||
d => (d.id === dashboard.id ? newDashboard : d)
|
||||
),
|
||||
}
|
||||
|
||||
return {...state, ...newState}
|
||||
}
|
||||
|
||||
case 'DELETE_DASHBOARD_CELL': {
|
||||
const {dashboard, cell} = action.payload
|
||||
|
||||
|
@ -158,24 +117,6 @@ export default function ui(state = initialState, action) {
|
|||
return {...state, ...newState}
|
||||
}
|
||||
|
||||
case 'CANCEL_EDIT_CELL': {
|
||||
const {dashboardID, cellID} = action.payload
|
||||
|
||||
const dashboards = state.dashboards.map(
|
||||
d =>
|
||||
d.id === dashboardID
|
||||
? {
|
||||
...d,
|
||||
cells: d.cells.map(
|
||||
c => (c.i === cellID ? {...c, isEditing: false} : c)
|
||||
),
|
||||
}
|
||||
: d
|
||||
)
|
||||
|
||||
return {...state, dashboards}
|
||||
}
|
||||
|
||||
case 'SYNC_DASHBOARD_CELL': {
|
||||
const {cell, dashboard} = action.payload
|
||||
|
||||
|
@ -195,30 +136,6 @@ export default function ui(state = initialState, action) {
|
|||
return {...state, ...newState}
|
||||
}
|
||||
|
||||
case 'RENAME_DASHBOARD_CELL': {
|
||||
const {x, y, name, dashboard} = action.payload
|
||||
|
||||
const cell = dashboard.cells.find(c => c.x === x && c.y === y)
|
||||
|
||||
const newCell = {
|
||||
...cell,
|
||||
name,
|
||||
}
|
||||
|
||||
const newDashboard = {
|
||||
...dashboard,
|
||||
cells: dashboard.cells.map(c => (c.x === x && c.y === y ? newCell : c)),
|
||||
}
|
||||
|
||||
const newState = {
|
||||
dashboards: state.dashboards.map(
|
||||
d => (d.id === dashboard.id ? newDashboard : d)
|
||||
),
|
||||
}
|
||||
|
||||
return {...state, ...newState}
|
||||
}
|
||||
|
||||
case 'EDIT_CELL_QUERY_STATUS': {
|
||||
const {queryID, status} = action.payload
|
||||
|
||||
|
@ -321,3 +238,5 @@ export default function ui(state = initialState, action) {
|
|||
|
||||
return state
|
||||
}
|
||||
|
||||
export default ui
|
||||
|
|
|
@ -4,6 +4,7 @@ import AJAX from 'src/utils/ajax'
|
|||
import {Service, FluxTable} from 'src/types'
|
||||
import {updateService} from 'src/shared/apis'
|
||||
import {parseResponse} from 'src/shared/parsing/flux/response'
|
||||
import {MAX_RESPONSE_BYTES} from 'src/flux/constants'
|
||||
|
||||
export const getSuggestions = async (url: string) => {
|
||||
try {
|
||||
|
@ -39,23 +40,36 @@ export const getAST = async (request: ASTRequest) => {
|
|||
}
|
||||
}
|
||||
|
||||
interface GetTimeSeriesResult {
|
||||
didTruncate: boolean
|
||||
tables: FluxTable[]
|
||||
}
|
||||
|
||||
export const getTimeSeries = async (
|
||||
service: Service,
|
||||
script: string
|
||||
): Promise<FluxTable[]> => {
|
||||
): Promise<GetTimeSeriesResult> => {
|
||||
const and = encodeURIComponent('&')
|
||||
const mark = encodeURIComponent('?')
|
||||
const garbage = script.replace(/\s/g, '') // server cannot handle whitespace
|
||||
const url = `${
|
||||
service.links.proxy
|
||||
}?path=/v1/query${mark}orgName=defaulorgname${and}q=${garbage}`
|
||||
|
||||
try {
|
||||
const {data} = await AJAX({
|
||||
method: 'POST',
|
||||
url: `${
|
||||
service.links.proxy
|
||||
}?path=/v1/query${mark}orgName=defaulorgname${and}q=${garbage}`,
|
||||
})
|
||||
// We are using the `fetch` API here since the `AJAX` utility lacks support
|
||||
// for limiting response size. The `AJAX` utility depends on
|
||||
// `axios.request` which _does_ have a `maxContentLength` option, though it
|
||||
// seems to be broken at the moment. We might use this option instead of
|
||||
// the `fetch` API in the future, if it is ever fixed. See
|
||||
// https://github.com/axios/axios/issues/1491.
|
||||
const resp = await fetch(url, {method: 'POST'})
|
||||
const {body, byteLength} = await decodeFluxRespWithLimit(resp)
|
||||
|
||||
return parseResponse(data)
|
||||
return {
|
||||
tables: parseResponse(body),
|
||||
didTruncate: byteLength >= MAX_RESPONSE_BYTES,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Problem fetching data', error)
|
||||
|
||||
|
@ -114,3 +128,43 @@ export const updateScript = async (service: Service, script: string) => {
|
|||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
interface DecodeFluxRespWithLimitResult {
|
||||
body: string
|
||||
byteLength: number
|
||||
}
|
||||
|
||||
const decodeFluxRespWithLimit = async (
|
||||
resp: Response
|
||||
): Promise<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}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
||||
import ExpressionNode from 'src/flux/components/ExpressionNode'
|
||||
import VariableName from 'src/flux/components/VariableName'
|
||||
import FuncSelector from 'src/flux/components/FuncSelector'
|
||||
|
@ -29,7 +30,7 @@ class BodyBuilder extends PureComponent<Props> {
|
|||
if (d.funcs) {
|
||||
return (
|
||||
<div className="declaration" key={i}>
|
||||
<VariableName name={d.name} />
|
||||
<VariableName name={d.name} assignedToQuery={true} />
|
||||
<ExpressionNode
|
||||
bodyID={b.id}
|
||||
declarationID={d.id}
|
||||
|
@ -43,7 +44,7 @@ class BodyBuilder extends PureComponent<Props> {
|
|||
|
||||
return (
|
||||
<div className="declaration" key={i}>
|
||||
<VariableName name={b.source} />
|
||||
<VariableName name={b.source} assignedToQuery={false} />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
@ -62,18 +63,20 @@ class BodyBuilder extends PureComponent<Props> {
|
|||
})
|
||||
|
||||
return (
|
||||
<div className="body-builder">
|
||||
{_.flatten(bodybuilder)}
|
||||
<div className="declaration">
|
||||
<FuncSelector
|
||||
bodyID="fake-body-id"
|
||||
declarationID="fake-declaration-id"
|
||||
onAddNode={this.createNewBody}
|
||||
funcs={this.newDeclarationFuncs}
|
||||
connectorVisible={false}
|
||||
/>
|
||||
<FancyScrollbar className="body-builder--container" autoHide={true}>
|
||||
<div className="body-builder">
|
||||
{_.flatten(bodybuilder)}
|
||||
<div className="declaration">
|
||||
<FuncSelector
|
||||
bodyID="fake-body-id"
|
||||
declarationID="fake-declaration-id"
|
||||
onAddNode={this.createNewBody}
|
||||
funcs={this.newDeclarationFuncs}
|
||||
connectorVisible={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -85,25 +85,23 @@ class DatabaseListItem extends PureComponent<Props, State> {
|
|||
const {db, service} = this.props
|
||||
const {isOpen, searchTerm} = this.state
|
||||
|
||||
if (isOpen) {
|
||||
return (
|
||||
<>
|
||||
<div className="flux-schema--filter">
|
||||
<input
|
||||
className="form-control input-xs"
|
||||
placeholder={`Filter within ${db}`}
|
||||
type="text"
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
value={searchTerm}
|
||||
onClick={this.handleInputClick}
|
||||
onChange={this.onSearch}
|
||||
/>
|
||||
</div>
|
||||
<TagList db={db} service={service} tags={this.tags} filter={[]} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className={`flux-schema--children ${isOpen ? '' : 'hidden'}`}>
|
||||
<div className="flux-schema--filter">
|
||||
<input
|
||||
className="form-control input-xs"
|
||||
placeholder={`Filter within ${db}`}
|
||||
type="text"
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
value={searchTerm}
|
||||
onClick={this.handleInputClick}
|
||||
onChange={this.onSearch}
|
||||
/>
|
||||
</div>
|
||||
<TagList db={db} service={service} tags={this.tags} filter={[]} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleClickCopy = e => {
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -1,66 +1,58 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import {BinaryExpressionNode, MemberExpressionNode} from 'src/types/flux'
|
||||
|
||||
type FilterNode = BinaryExpressionNode & MemberExpressionNode
|
||||
import {connect} from 'react-redux'
|
||||
import {getAST} from 'src/flux/apis'
|
||||
import {Links, FilterNode} from 'src/types/flux'
|
||||
import Walker from 'src/flux/ast/walker'
|
||||
import FilterConditions from 'src/flux/components/FilterConditions'
|
||||
|
||||
interface Props {
|
||||
filterString?: string
|
||||
links: Links
|
||||
}
|
||||
|
||||
interface State {
|
||||
nodes: FilterNode[]
|
||||
ast: object
|
||||
}
|
||||
|
||||
class FilterPreview extends PureComponent<Props> {
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
{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
|
||||
export class FilterPreview extends PureComponent<Props, State> {
|
||||
public static defaultProps: Partial<Props> = {
|
||||
filterString: '',
|
||||
}
|
||||
|
||||
private get className(): JSX.Element {
|
||||
const {node} = this.props
|
||||
|
||||
switch (node.type) {
|
||||
case 'ObjectExpression': {
|
||||
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 />
|
||||
}
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
nodes: [],
|
||||
ast: {},
|
||||
}
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
this.convertStringToNodes()
|
||||
}
|
||||
|
||||
public async componentDidUpdate(prevProps, __) {
|
||||
if (this.props.filterString !== prevProps.filterString) {
|
||||
this.convertStringToNodes()
|
||||
}
|
||||
}
|
||||
|
||||
public async convertStringToNodes() {
|
||||
const {links, filterString} = this.props
|
||||
|
||||
const ast = await getAST({url: links.ast, body: filterString})
|
||||
const nodes = new Walker(ast).inOrderExpression
|
||||
this.setState({nodes, ast})
|
||||
}
|
||||
|
||||
public render() {
|
||||
return <FilterConditions nodes={this.state.nodes} />
|
||||
}
|
||||
}
|
||||
|
||||
export default FilterPreview
|
||||
const mapStateToProps = ({links}) => {
|
||||
return {links: links.flux}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(FilterPreview)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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}`
|
||||
}
|
||||
}
|
|
@ -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}`
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -24,6 +24,7 @@ class FluxForm extends PureComponent<Props> {
|
|||
value={this.url}
|
||||
placeholder={this.url}
|
||||
onChange={onInputChange}
|
||||
customClass="col-sm-6"
|
||||
/>
|
||||
<Input
|
||||
name="name"
|
||||
|
@ -32,6 +33,7 @@ class FluxForm extends PureComponent<Props> {
|
|||
placeholder={service.name}
|
||||
onChange={onInputChange}
|
||||
maxLength={33}
|
||||
customClass="col-sm-6"
|
||||
/>
|
||||
<div className="form-group form-group-submit col-xs-12 text-center">
|
||||
<button
|
||||
|
|
|
@ -25,7 +25,7 @@ interface DropdownItem {
|
|||
text: string
|
||||
}
|
||||
|
||||
class From extends PureComponent<Props, State> {
|
||||
class FromDatabaseDropdown extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
|
@ -82,4 +82,4 @@ class From extends PureComponent<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
export default From
|
||||
export default FromDatabaseDropdown
|
|
@ -4,7 +4,7 @@ import FuncArgInput from 'src/flux/components/FuncArgInput'
|
|||
import FuncArgTextArea from 'src/flux/components/FuncArgTextArea'
|
||||
import FuncArgBool from 'src/flux/components/FuncArgBool'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import From from 'src/flux/components/From'
|
||||
import FromDatabaseDropdown from 'src/flux/components/FromDatabaseDropdown'
|
||||
|
||||
import {funcNames, argTypes} from 'src/flux/constants'
|
||||
import {OnChangeArg} from 'src/types/flux'
|
||||
|
@ -41,7 +41,7 @@ class FuncArg extends PureComponent<Props> {
|
|||
|
||||
if (funcName === funcNames.FROM) {
|
||||
return (
|
||||
<From
|
||||
<FromDatabaseDropdown
|
||||
service={service}
|
||||
argKey={argKey}
|
||||
funcID={funcID}
|
||||
|
|
|
@ -4,8 +4,10 @@ import {OnChangeArg} from 'src/types/flux'
|
|||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {Func} from 'src/types/flux'
|
||||
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 {getDeep} from 'src/utils/wrappers'
|
||||
|
||||
interface Props {
|
||||
func: Func
|
||||
|
@ -21,55 +23,112 @@ interface Props {
|
|||
@ErrorHandling
|
||||
export default class FuncArgs extends PureComponent<Props> {
|
||||
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 {
|
||||
func,
|
||||
bodyID,
|
||||
service,
|
||||
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,
|
||||
onGenerateScript,
|
||||
declarationsFromBody,
|
||||
} = this.props
|
||||
const {name: funcName, id: funcID} = func
|
||||
|
||||
return (
|
||||
<div className="func-node--tooltip">
|
||||
{funcName === funcNames.JOIN ? (
|
||||
<Join
|
||||
func={func}
|
||||
bodyID={bodyID}
|
||||
declarationID={declarationID}
|
||||
onChangeArg={onChangeArg}
|
||||
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>
|
||||
<JoinArgs
|
||||
func={func}
|
||||
bodyID={bodyID}
|
||||
declarationID={declarationID}
|
||||
onChangeArg={onChangeArg}
|
||||
declarationsFromBody={declarationsFromBody}
|
||||
onGenerateScript={onGenerateScript}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ import _ from 'lodash'
|
|||
|
||||
import {Func} from 'src/types/flux'
|
||||
import {funcNames} from 'src/flux/constants'
|
||||
import Filter from 'src/flux/components/Filter'
|
||||
import FilterPreview from 'src/flux/components/FilterPreview'
|
||||
|
||||
import {getDeep} from 'src/utils/wrappers'
|
||||
|
@ -32,16 +31,12 @@ export default class FuncArgsPreview extends PureComponent<Props> {
|
|||
return this.colorizedArguments
|
||||
}
|
||||
|
||||
return <Filter value={value} render={this.filterPreview} />
|
||||
return <FilterPreview filterString={value} />
|
||||
}
|
||||
|
||||
return this.colorizedArguments
|
||||
}
|
||||
|
||||
private filterPreview = nodes => {
|
||||
return <FilterPreview nodes={nodes} />
|
||||
}
|
||||
|
||||
private get colorizedArguments(): JSX.Element | JSX.Element[] {
|
||||
const {func} = this.props
|
||||
const {args} = func
|
||||
|
@ -76,19 +71,19 @@ export default class FuncArgsPreview extends PureComponent<Props> {
|
|||
case 'period':
|
||||
case 'duration':
|
||||
case 'array': {
|
||||
return <span className="variable-value--number">{argument}</span>
|
||||
return <span className="func-arg--number">{argument}</span>
|
||||
}
|
||||
case 'bool': {
|
||||
return <span className="variable-value--boolean">{argument}</span>
|
||||
return <span className="func-arg--boolean">{argument}</span>
|
||||
}
|
||||
case 'string': {
|
||||
return <span className="variable-value--string">"{argument}"</span>
|
||||
return <span className="func-arg--string">"{argument}"</span>
|
||||
}
|
||||
case 'object': {
|
||||
return <span className="variable-value--object">{argument}</span>
|
||||
return <span className="func-arg--object">{argument}</span>
|
||||
}
|
||||
case 'invalid': {
|
||||
return <span className="variable-value--invalid">{argument}</span>
|
||||
return <span className="func-arg--invalid">{argument}</span>
|
||||
}
|
||||
default: {
|
||||
return <span>{argument}</span>
|
||||
|
|
|
@ -52,6 +52,7 @@ export default class FuncNode extends PureComponent<Props, State> {
|
|||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
>
|
||||
<div className="func-node--connector" />
|
||||
<div className="func-node--name">{func.name}</div>
|
||||
<FuncArgsPreview func={func} />
|
||||
{isExpanded && (
|
||||
|
|
|
@ -22,7 +22,7 @@ interface DropdownItem {
|
|||
text: string
|
||||
}
|
||||
|
||||
class Join extends PureComponent<Props> {
|
||||
class JoinArgs extends PureComponent<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
}
|
||||
|
@ -151,4 +151,4 @@ class Join extends PureComponent<Props> {
|
|||
}
|
||||
}
|
||||
|
||||
export default Join
|
||||
export default JoinArgs
|
|
@ -12,17 +12,7 @@ interface Props {
|
|||
filter: SchemaFilter[]
|
||||
}
|
||||
|
||||
interface State {
|
||||
isOpen: boolean
|
||||
}
|
||||
|
||||
export default class TagList extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {isOpen: false}
|
||||
}
|
||||
|
||||
export default class TagList extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {db, service, tags, filter} = this.props
|
||||
|
||||
|
|
|
@ -66,7 +66,14 @@ export default class TagListItem extends PureComponent<Props, State> {
|
|||
|
||||
public render() {
|
||||
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 (
|
||||
<div className={this.className}>
|
||||
|
@ -83,46 +90,37 @@ export default class TagListItem extends PureComponent<Props, State> {
|
|||
</div>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
{this.state.isOpen && (
|
||||
<>
|
||||
<div
|
||||
className="flux-schema--header"
|
||||
onClick={this.handleInputClick}
|
||||
>
|
||||
<div className="flux-schema--filter">
|
||||
<input
|
||||
className="form-control input-xs"
|
||||
placeholder={`Filter within ${tagKey}`}
|
||||
type="text"
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
value={searchTerm}
|
||||
onChange={this.onSearch}
|
||||
/>
|
||||
{this.isSearching && (
|
||||
<LoadingSpinner style={this.spinnerStyle} />
|
||||
)}
|
||||
</div>
|
||||
{this.count}
|
||||
<div className={`flux-schema--children ${isOpen ? '' : 'hidden'}`}>
|
||||
<div className="flux-schema--header" onClick={this.handleInputClick}>
|
||||
<div className="flux-schema--filter">
|
||||
<input
|
||||
className="form-control input-xs"
|
||||
placeholder={`Filter within ${tagKey}`}
|
||||
type="text"
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
value={searchTerm}
|
||||
onChange={this.onSearch}
|
||||
/>
|
||||
{this.isSearching && <LoadingSpinner style={this.spinnerStyle} />}
|
||||
</div>
|
||||
{this.isLoading && <LoaderSkeleton />}
|
||||
{!this.isLoading && (
|
||||
<>
|
||||
<TagValueList
|
||||
db={db}
|
||||
service={service}
|
||||
values={tagValues}
|
||||
tagKey={tagKey}
|
||||
filter={filter}
|
||||
onLoadMoreValues={this.handleLoadMoreValues}
|
||||
isLoadingMoreValues={loadingMore === RemoteDataState.Loading}
|
||||
shouldShowMoreValues={limit < count}
|
||||
loadMoreCount={this.loadMoreCount}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{this.count}
|
||||
</div>
|
||||
{this.isLoading && <LoaderSkeleton />}
|
||||
{!this.isLoading && (
|
||||
<TagValueList
|
||||
db={db}
|
||||
service={service}
|
||||
values={tagValues}
|
||||
tagKey={tagKey}
|
||||
filter={filter}
|
||||
onLoadMoreValues={this.handleLoadMoreValues}
|
||||
isLoadingMoreValues={loadingMore === RemoteDataState.Loading}
|
||||
shouldShowMoreValues={limit < count}
|
||||
loadMoreCount={this.loadMoreCount}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -315,7 +313,7 @@ export default class TagListItem extends PureComponent<Props, State> {
|
|||
return (
|
||||
!isOpen &&
|
||||
(loadingAll === RemoteDataState.NotStarted ||
|
||||
loadingAll !== RemoteDataState.Error)
|
||||
loadingAll === RemoteDataState.Error)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ class TagValueListItem extends PureComponent<Props, State> {
|
|||
|
||||
public render() {
|
||||
const {db, service, value} = this.props
|
||||
const {searchTerm} = this.state
|
||||
const {searchTerm, isOpen} = this.state
|
||||
|
||||
return (
|
||||
<div className={this.className} onClick={this.handleClick}>
|
||||
|
@ -65,35 +65,33 @@ class TagValueListItem extends PureComponent<Props, State> {
|
|||
</div>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
{this.state.isOpen && (
|
||||
<>
|
||||
{this.isLoading && <LoaderSkeleton />}
|
||||
{!this.isLoading && (
|
||||
<>
|
||||
{!!this.tags.length && (
|
||||
<div className="flux-schema--filter">
|
||||
<input
|
||||
className="form-control input-xs"
|
||||
placeholder={`Filter within ${value}`}
|
||||
type="text"
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
value={searchTerm}
|
||||
onClick={this.handleInputClick}
|
||||
onChange={this.onSearch}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<TagList
|
||||
db={db}
|
||||
service={service}
|
||||
tags={this.tags}
|
||||
filter={this.filter}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div className={`flux-schema--children ${isOpen ? '' : 'hidden'}`}>
|
||||
{this.isLoading && <LoaderSkeleton />}
|
||||
{!this.isLoading && (
|
||||
<>
|
||||
{!!this.tags.length && (
|
||||
<div className="flux-schema--filter">
|
||||
<input
|
||||
className="form-control input-xs"
|
||||
placeholder={`Filter within ${value}`}
|
||||
type="text"
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
value={searchTerm}
|
||||
onClick={this.handleInputClick}
|
||||
onChange={this.onSearch}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<TagList
|
||||
db={db}
|
||||
service={service}
|
||||
tags={this.tags}
|
||||
filter={this.filter}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -177,7 +175,7 @@ class TagValueListItem extends PureComponent<Props, State> {
|
|||
return (
|
||||
!isOpen &&
|
||||
(loading === RemoteDataState.NotStarted ||
|
||||
loading !== RemoteDataState.Error)
|
||||
loading === RemoteDataState.Error)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,25 +8,46 @@ import {vis} from 'src/flux/constants'
|
|||
|
||||
const NUM_FIXED_ROWS = 1
|
||||
|
||||
const filterTable = (table: FluxTable): FluxTable => {
|
||||
const IGNORED_COLUMNS = ['', 'result', 'table', '_start', '_stop']
|
||||
const header = table.data[0]
|
||||
const indices = IGNORED_COLUMNS.map(name => header.indexOf(name))
|
||||
const data = table.data.map(row =>
|
||||
row.filter((__, i) => !indices.includes(i))
|
||||
)
|
||||
|
||||
return {
|
||||
...table,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
interface Props {
|
||||
table: FluxTable
|
||||
}
|
||||
|
||||
interface State {
|
||||
scrollLeft: number
|
||||
filteredTable: FluxTable
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
export default class TimeMachineTable extends PureComponent<Props, State> {
|
||||
public static getDerivedStateFromProps({table}: Props) {
|
||||
return {filteredTable: filterTable(table)}
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
scrollLeft: 0,
|
||||
filteredTable: filterTable(props.table),
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {scrollLeft} = this.state
|
||||
const {scrollLeft, filteredTable} = this.state
|
||||
|
||||
return (
|
||||
<div style={{flex: '1 1 auto'}}>
|
||||
|
@ -73,7 +94,7 @@ export default class TimeMachineTable extends PureComponent<Props, State> {
|
|||
cellRenderer={this.cellRenderer}
|
||||
rowHeight={vis.TABLE_ROW_HEIGHT}
|
||||
height={height - this.headerOffset}
|
||||
rowCount={this.table.data.length - NUM_FIXED_ROWS}
|
||||
rowCount={filteredTable.data.length - NUM_FIXED_ROWS}
|
||||
/>
|
||||
)}
|
||||
</ColumnSizer>
|
||||
|
@ -93,7 +114,9 @@ export default class TimeMachineTable extends PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
private get columnCount(): number {
|
||||
return _.get(this.table, 'data.0', []).length
|
||||
const {filteredTable} = this.state
|
||||
|
||||
return _.get(filteredTable, 'data.0', []).length
|
||||
}
|
||||
|
||||
private get headerOffset(): number {
|
||||
|
@ -109,13 +132,15 @@ export default class TimeMachineTable extends PureComponent<Props, State> {
|
|||
key,
|
||||
style,
|
||||
}: GridCellProps): React.ReactNode => {
|
||||
const {filteredTable} = this.state
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
style={{...style, display: 'flex', alignItems: 'center'}}
|
||||
className="table-graph-cell table-graph-cell__fixed-row"
|
||||
>
|
||||
{this.table.data[0][columnIndex]}
|
||||
{filteredTable.data[0][columnIndex]}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -126,25 +151,12 @@ export default class TimeMachineTable extends PureComponent<Props, State> {
|
|||
rowIndex,
|
||||
style,
|
||||
}: GridCellProps): React.ReactNode => {
|
||||
const {filteredTable} = this.state
|
||||
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
|
||||
interface Props {
|
||||
name?: string
|
||||
name: string
|
||||
assignedToQuery: boolean
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -10,7 +11,7 @@ interface State {
|
|||
|
||||
export default class VariableName extends PureComponent<Props, State> {
|
||||
public static defaultProps: Partial<Props> = {
|
||||
name: '',
|
||||
assignedToQuery: false,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
|
@ -22,7 +23,14 @@ export default class VariableName extends PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
@ -32,7 +40,7 @@ export default class VariableName extends PureComponent<Props, State> {
|
|||
return this.colorizeSyntax
|
||||
}
|
||||
|
||||
return <span className="variable-name">{name}</span>
|
||||
return <span className="variable-node--name">{name}</span>
|
||||
}
|
||||
|
||||
private get colorizeSyntax(): JSX.Element {
|
||||
|
@ -42,14 +50,13 @@ export default class VariableName extends PureComponent<Props, State> {
|
|||
const varValue = this.props.name.replace(/^[^=]+=/, '')
|
||||
|
||||
const valueIsString = varValue.endsWith('"')
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className="variable-name">{varName}</span>
|
||||
<span className="variable-node--name">{varName}</span>
|
||||
{' = '}
|
||||
<span
|
||||
className={
|
||||
valueIsString ? 'variable-value--string' : 'variable-value--number'
|
||||
valueIsString ? 'variable-node--string' : 'variable-node--number'
|
||||
}
|
||||
>
|
||||
{varValue}
|
||||
|
|
|
@ -6,4 +6,15 @@ import * as builder from 'src/flux/constants/builder'
|
|||
import * as vis from 'src/flux/constants/vis'
|
||||
import * as explorer from 'src/flux/constants/explorer'
|
||||
|
||||
export {ast, funcNames, argTypes, editor, builder, vis, explorer}
|
||||
const MAX_RESPONSE_BYTES = 1e7 // 10 MB
|
||||
|
||||
export {
|
||||
ast,
|
||||
funcNames,
|
||||
argTypes,
|
||||
editor,
|
||||
builder,
|
||||
vis,
|
||||
explorer,
|
||||
MAX_RESPONSE_BYTES,
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import KeyboardShortcuts from 'src/shared/components/KeyboardShortcuts'
|
|||
import {
|
||||
analyzeSuccess,
|
||||
fluxTimeSeriesError,
|
||||
fluxResponseTruncatedError,
|
||||
} from 'src/shared/copy/notifications'
|
||||
import {UpdateScript} from 'src/flux/actions'
|
||||
|
||||
|
@ -452,8 +453,13 @@ export class FluxPage extends PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
try {
|
||||
const data = await getTimeSeries(this.service, script)
|
||||
this.setState({data})
|
||||
const {tables, didTruncate} = await getTimeSeries(this.service, script)
|
||||
|
||||
this.setState({data: tables})
|
||||
|
||||
if (didTruncate) {
|
||||
notify(fluxResponseTruncatedError())
|
||||
}
|
||||
} catch (error) {
|
||||
this.setState({data: []})
|
||||
|
||||
|
|
|
@ -39,7 +39,6 @@ const KapacitorFormInput: SFC<Props> = ({
|
|||
|
||||
KapacitorFormInput.defaultProps = {
|
||||
inputType: '',
|
||||
customClass: 'col-sm-6',
|
||||
}
|
||||
|
||||
export default KapacitorFormInput
|
||||
|
|
|
@ -63,7 +63,11 @@ export const saveToLocalStorage = ({
|
|||
const appPersisted = {app: {persisted}}
|
||||
const dashTimeV1 = {ranges: normalizer(ranges)}
|
||||
|
||||
const minimalLogs = _.omit(logs, ['tableData', 'histogramData'])
|
||||
const minimalLogs = _.omit(logs, [
|
||||
'tableData',
|
||||
'histogramData',
|
||||
'queryCount',
|
||||
])
|
||||
|
||||
window.localStorage.setItem(
|
||||
'state',
|
||||
|
@ -75,7 +79,7 @@ export const saveToLocalStorage = ({
|
|||
dataExplorer,
|
||||
dataExplorerQueryConfigs,
|
||||
script,
|
||||
logs: {...minimalLogs, histogramData: [], tableData: {}},
|
||||
logs: {...minimalLogs, histogramData: [], tableData: {}, queryCount: 0},
|
||||
})
|
||||
)
|
||||
} catch (err) {
|
||||
|
|
|
@ -52,6 +52,16 @@ export enum ActionTypes {
|
|||
AddFilter = 'LOGS_ADD_FILTER',
|
||||
RemoveFilter = 'LOGS_REMOVE_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 {
|
||||
|
@ -161,6 +171,8 @@ export type Action =
|
|||
| AddFilterAction
|
||||
| RemoveFilterAction
|
||||
| ChangeFilterAction
|
||||
| DecrementQueryCountAction
|
||||
| IncrementQueryCountAction
|
||||
|
||||
const getTimeRange = (state: State): 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 => {
|
||||
dispatch(executeHistogramQueryAsync())
|
||||
dispatch(executeTableQueryAsync())
|
||||
dispatch(incrementQueryCount())
|
||||
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 => {
|
||||
|
|
|
@ -99,7 +99,16 @@ class LogViewerHeader extends PureComponent<Props> {
|
|||
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 {
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import {Filter} from 'src/types/logs'
|
||||
import FilterBlock from 'src/logs/components/LogsFilter'
|
||||
import QueryResults from 'src/logs/components/QueryResults'
|
||||
|
||||
interface Props {
|
||||
numResults: number
|
||||
filters: Filter[]
|
||||
queryCount: number
|
||||
onDelete: (id: string) => void
|
||||
onFilterChange: (id: string, operator: string, value: string) => void
|
||||
}
|
||||
|
||||
class LogsFilters extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {numResults} = this.props
|
||||
const {numResults, queryCount} = this.props
|
||||
|
||||
return (
|
||||
<div className="logs-viewer--filter-bar">
|
||||
<label className="logs-viewer--results-text">
|
||||
Query returned <strong>{numResults} Events</strong>
|
||||
</label>
|
||||
<QueryResults count={numResults} queryCount={queryCount} />
|
||||
<ul className="logs-viewer--filters">{this.renderFilters}</ul>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -26,7 +26,7 @@ class LogsSearchBar extends PureComponent<Props, State> {
|
|||
<div className="logs-viewer--search-input">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search logs using Keywords or Regular Expressions..."
|
||||
placeholder="Search logs using keywords or regular expressions..."
|
||||
value={searchTerm}
|
||||
onChange={this.handleChange}
|
||||
onKeyDown={this.handleInputKeyDown}
|
||||
|
|
|
@ -7,9 +7,7 @@ import {getDeep} from 'src/utils/wrappers'
|
|||
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
||||
|
||||
const ROW_HEIGHT = 26
|
||||
const ROW_CHAR_LIMIT = 100
|
||||
const CHAR_WIDTH = 7
|
||||
|
||||
const CHAR_WIDTH = 9
|
||||
interface Props {
|
||||
data: {
|
||||
columns: string[]
|
||||
|
@ -46,11 +44,14 @@ class LogsTable extends Component<Props, State> {
|
|||
}
|
||||
|
||||
private grid: React.RefObject<Grid>
|
||||
private headerGrid: React.RefObject<Grid>
|
||||
private currentMessageWidth: number | null
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
this.grid = React.createRef()
|
||||
this.headerGrid = React.createRef()
|
||||
|
||||
this.state = {
|
||||
scrollTop: 0,
|
||||
|
@ -61,6 +62,15 @@ class LogsTable extends Component<Props, State> {
|
|||
|
||||
public componentDidUpdate() {
|
||||
this.grid.current.recomputeGridSize()
|
||||
this.headerGrid.current.recomputeGridSize()
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
window.addEventListener('resize', this.handleWindowResize)
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.handleWindowResize)
|
||||
}
|
||||
|
||||
public render() {
|
||||
|
@ -75,6 +85,7 @@ class LogsTable extends Component<Props, State> {
|
|||
<AutoSizer>
|
||||
{({width}) => (
|
||||
<Grid
|
||||
ref={this.headerGrid}
|
||||
height={ROW_HEIGHT}
|
||||
rowHeight={ROW_HEIGHT}
|
||||
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 handleScrollbarScroll = (e: MouseEvent<JSX.Element>) => {
|
||||
|
@ -127,15 +144,62 @@ class LogsTable extends Component<Props, State> {
|
|||
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 => {
|
||||
const columnIndex = this.props.data.columns.indexOf('message')
|
||||
const height =
|
||||
(Math.floor(
|
||||
this.props.data.values[index][columnIndex].length / ROW_CHAR_LIMIT
|
||||
) +
|
||||
1) *
|
||||
ROW_HEIGHT
|
||||
return height
|
||||
const columnIndex = this.columns.indexOf('message')
|
||||
const value = getDeep(this.props, `data.values.${index}.${columnIndex}`, '')
|
||||
const lines = Math.round(value.length / this.rowCharLimit + 0.25)
|
||||
|
||||
return Math.max(lines, 1) * (ROW_HEIGHT - 14) + 14
|
||||
}
|
||||
|
||||
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 {
|
||||
return getDeep<string>(
|
||||
{
|
||||
|
@ -251,7 +294,9 @@ class LogsTable extends Component<Props, State> {
|
|||
value = moment(+value / 1000000).format('YYYY/MM/DD HH:mm:ss')
|
||||
break
|
||||
case 'message':
|
||||
value = _.replace(value, '\\n', '')
|
||||
if (value.indexOf(' ') > this.rowCharLimit - 5) {
|
||||
value = _.truncate(value, {length: this.rowCharLimit - 5})
|
||||
}
|
||||
break
|
||||
case 'severity':
|
||||
value = (
|
||||
|
@ -283,6 +328,8 @@ class LogsTable extends Component<Props, State> {
|
|||
data-tag-key={column}
|
||||
data-tag-value={value}
|
||||
onClick={this.handleTagClick}
|
||||
data-index={rowIndex}
|
||||
onMouseOver={this.handleMouseEnter}
|
||||
className="logs-viewer--clickable"
|
||||
>
|
||||
{value}
|
||||
|
@ -293,7 +340,9 @@ class LogsTable extends Component<Props, State> {
|
|||
|
||||
return (
|
||||
<div
|
||||
className={classnames('logs-viewer--cell', {highlight: highlightRow})}
|
||||
className={classnames(`logs-viewer--cell ${column}--cell`, {
|
||||
highlight: highlightRow,
|
||||
})}
|
||||
key={key}
|
||||
style={style}
|
||||
onMouseOver={this.handleMouseEnter}
|
||||
|
|
|
@ -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
|
|
@ -47,6 +47,7 @@ interface Props {
|
|||
}
|
||||
searchTerm: string
|
||||
filters: Filter[]
|
||||
queryCount: number
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -88,7 +89,7 @@ class LogsPage extends PureComponent<Props, State> {
|
|||
|
||||
public render() {
|
||||
const {liveUpdating} = this.state
|
||||
const {searchTerm, filters} = this.props
|
||||
const {searchTerm, filters, queryCount} = this.props
|
||||
|
||||
const count = getDeep(this.props, 'tableData.values.length', 0)
|
||||
|
||||
|
@ -106,6 +107,7 @@ class LogsPage extends PureComponent<Props, State> {
|
|||
filters={filters || []}
|
||||
onDelete={this.handleFilterDelete}
|
||||
onFilterChange={this.handleFilterChange}
|
||||
queryCount={queryCount}
|
||||
/>
|
||||
<LogsTable
|
||||
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 = () => {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval)
|
||||
}
|
||||
|
||||
this.interval = setInterval(this.handleInterval, 10000)
|
||||
this.setState({liveUpdating: true})
|
||||
if (!this.isSpecificTimeRange) {
|
||||
this.interval = setInterval(this.handleInterval, 10000)
|
||||
this.setState({liveUpdating: true})
|
||||
}
|
||||
}
|
||||
|
||||
private handleScrollToTop = () => {
|
||||
|
@ -180,7 +188,7 @@ class LogsPage extends PureComponent<Props, State> {
|
|||
|
||||
return (
|
||||
<LogViewerHeader
|
||||
liveUpdating={liveUpdating}
|
||||
liveUpdating={liveUpdating && !this.isSpecificTimeRange}
|
||||
availableSources={sources}
|
||||
timeRange={timeRange}
|
||||
onChooseSource={this.handleChooseSource}
|
||||
|
@ -254,6 +262,7 @@ const mapStateToProps = ({
|
|||
tableData,
|
||||
searchTerm,
|
||||
filters,
|
||||
queryCount,
|
||||
},
|
||||
}) => ({
|
||||
sources,
|
||||
|
@ -265,6 +274,7 @@ const mapStateToProps = ({
|
|||
tableData,
|
||||
searchTerm,
|
||||
filters,
|
||||
queryCount,
|
||||
})
|
||||
|
||||
const mapDispatchToProps = {
|
||||
|
|
|
@ -5,6 +5,8 @@ import {
|
|||
RemoveFilterAction,
|
||||
AddFilterAction,
|
||||
ChangeFilterAction,
|
||||
DecrementQueryCountAction,
|
||||
IncrementQueryCountAction,
|
||||
} from 'src/logs/actions'
|
||||
import {LogsState} from 'src/types/logs'
|
||||
|
||||
|
@ -19,6 +21,7 @@ const defaultState: LogsState = {
|
|||
histogramData: [],
|
||||
searchTerm: null,
|
||||
filters: [],
|
||||
queryCount: 0,
|
||||
}
|
||||
|
||||
const removeFilter = (
|
||||
|
@ -56,6 +59,22 @@ const changeFilter = (
|
|||
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) => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.SetSource:
|
||||
|
@ -86,6 +105,10 @@ export default (state: LogsState = defaultState, action: Action) => {
|
|||
return removeFilter(state, action)
|
||||
case ActionTypes.ChangeFilter:
|
||||
return changeFilter(state, action)
|
||||
case ActionTypes.IncrementQueryCount:
|
||||
return incrementQueryCount(state, action)
|
||||
case ActionTypes.DecrementQueryCount:
|
||||
return decrementQueryCount(state, action)
|
||||
default:
|
||||
return state
|
||||
}
|
||||
|
|
|
@ -155,7 +155,7 @@ const computeSeconds = (range: TimeRange) => {
|
|||
|
||||
const createGroupBy = (range: TimeRange) => {
|
||||
const seconds = computeSeconds(range)
|
||||
const time = `${Math.floor(seconds / BIN_COUNT)}s`
|
||||
const time = `${Math.max(Math.floor(seconds / BIN_COUNT), 1)}s`
|
||||
const tags = []
|
||||
|
||||
return {time, tags}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable no-magic-numbers */
|
||||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {connect} from 'react-redux'
|
||||
|
|
|
@ -102,7 +102,7 @@ export default class LayoutCell extends Component<Props> {
|
|||
|
||||
if (this.queries.length) {
|
||||
const child = React.Children.only(children)
|
||||
return React.cloneElement(child, {cellID: cell.id})
|
||||
return React.cloneElement(child, {cellID: cell.i})
|
||||
}
|
||||
|
||||
return this.emptyGraph
|
||||
|
|
|
@ -537,10 +537,6 @@ class TableGraph extends Component<Props, State> {
|
|||
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 = {
|
||||
...style,
|
||||
backgroundColor: bgColor,
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// and ensuring stylistic consistency
|
||||
|
||||
import {FIVE_SECONDS, TEN_SECONDS, INFINITE} from 'src/shared/constants/index'
|
||||
import {MAX_RESPONSE_BYTES} from 'src/flux/constants'
|
||||
|
||||
const defaultErrorNotification = {
|
||||
type: 'error',
|
||||
|
@ -705,3 +706,13 @@ export const fluxTimeSeriesError = (message: string) => ({
|
|||
...defaultErrorNotification,
|
||||
message: `Could not get data: ${message}`,
|
||||
})
|
||||
|
||||
export const fluxResponseTruncatedError = () => {
|
||||
const BYTES_TO_MB = 1 / 1e6
|
||||
const APPROX_MAX_RESPONSE_MB = +(MAX_RESPONSE_BYTES * BYTES_TO_MB).toFixed(2)
|
||||
|
||||
return {
|
||||
...defaultErrorNotification,
|
||||
message: `Large response truncated to first ${APPROX_MAX_RESPONSE_MB} MB`,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,12 @@ export const parseTables = (responseChunk: string): FluxTable[] => {
|
|||
throw new Error('Unable to extract annotation data')
|
||||
}
|
||||
|
||||
if (_.isEmpty(nonAnnotationLines)) {
|
||||
// A response may be truncated on an arbitrary line. This guards against
|
||||
// the case where a response is truncated on annotation data
|
||||
return []
|
||||
}
|
||||
|
||||
const nonAnnotationData = Papa.parse(nonAnnotationLines).data
|
||||
const annotationData = Papa.parse(annotationLines).data
|
||||
const headerRow = nonAnnotationData[0]
|
||||
|
|
|
@ -84,9 +84,10 @@ class SideNav extends PureComponent<Props> {
|
|||
location={location}
|
||||
>
|
||||
<NavHeader link={dataExplorerLink} title="Data Explorer" />
|
||||
<FeatureFlag name="time-machine">
|
||||
<NavHeader link={`${sourcePrefix}/delorean`} title="Time Machine" />
|
||||
</FeatureFlag>
|
||||
<NavHeader
|
||||
link={`${sourcePrefix}/delorean`}
|
||||
title="Flux Query Interface"
|
||||
/>
|
||||
</NavBlock>
|
||||
<NavBlock
|
||||
highlightWhen={['dashboards']}
|
||||
|
|
|
@ -10,25 +10,28 @@ $flux-func-selector--height: 30px;
|
|||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
|
||||
&.open {
|
||||
z-index: 9999;
|
||||
height: $flux-func-selector--height + $flux-func-selector--gap;
|
||||
}
|
||||
}
|
||||
|
||||
.func-selector--connector {
|
||||
width: $flux-func-selector--gap;
|
||||
height: $flux-func-selector--height;
|
||||
width: $flux-node-gap;
|
||||
height: $flux-func-selector--gap;
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
transform: translateY(-50%);
|
||||
@include gradient-h($g4-onyx, $c-pool);
|
||||
top: -130%;
|
||||
width: $flux-connector-line;
|
||||
left: 50%;
|
||||
height: 230%;
|
||||
transform: translateX(-50%);
|
||||
@include gradient-v($g4-onyx, $c-pool);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,7 +54,7 @@ $flux-func-selector--height: 30px;
|
|||
top: 0;
|
||||
|
||||
.func-selector--connector + & {
|
||||
left: $flux-func-selector--gap;
|
||||
top: $flux-func-selector--gap;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
/*
|
||||
Flux Builder Styles
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
$flux-builder-min-width: 440px;
|
||||
$flux-node-height: 30px;
|
||||
$flux-node-tooltip-gap: 4px;
|
||||
$flux-node-gap: 5px;
|
||||
$flux-connector-line: 2px;
|
||||
$flux-node-gap: 30px;
|
||||
$flux-node-padding: 10px;
|
||||
$flux-arg-min-width: 120px;
|
||||
$flux-number-color: $c-neutrino;
|
||||
|
@ -8,11 +15,7 @@ $flux-object-color: $c-viridian;
|
|||
$flux-string-color: $c-honeydew;
|
||||
$flux-boolean-color: $c-viridian;
|
||||
$flux-invalid-color: $c-viridian;
|
||||
/*
|
||||
Shared Node styles
|
||||
------------------
|
||||
*/
|
||||
|
||||
// Shared Node styles
|
||||
%flux-node {
|
||||
min-height: $flux-node-height;
|
||||
border-radius: $radius;
|
||||
|
@ -22,66 +25,104 @@ $flux-invalid-color: $c-viridian;
|
|||
position: relative;
|
||||
background-color: $g4-onyx;
|
||||
transition: background-color 0.25s ease;
|
||||
|
||||
margin-bottom: $flux-node-tooltip-gap / 2;
|
||||
margin-top: $flux-node-tooltip-gap / 2;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: $g6-smoke;
|
||||
}
|
||||
}
|
||||
|
||||
.body-builder {
|
||||
padding: 30px;
|
||||
min-width: 440px;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
.body-builder--container {
|
||||
background-color: $g1-raven;
|
||||
}
|
||||
|
||||
.body-builder {
|
||||
padding: $flux-node-height;
|
||||
padding-bottom: 0;
|
||||
min-width: $flux-builder-min-width;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.declaration {
|
||||
width: 100%;
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: $flux-node-gap;
|
||||
padding-bottom: $flux-node-gap;
|
||||
border-bottom: 2px solid $g2-kevlar;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.variable-string {
|
||||
.variable-node {
|
||||
@extend %flux-node;
|
||||
color: $g11-sidewalk;
|
||||
line-height: $flux-node-height;
|
||||
white-space: nowrap;
|
||||
@include no-user-select();
|
||||
margin-top: 0;
|
||||
&:hover {
|
||||
background-color: $g4-onyx;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.variable-blank {
|
||||
font-style: italic;
|
||||
.variable-node--connector {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: $c-pool;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 50%;
|
||||
left: $flux-node-gap / 2;
|
||||
transform: translate(-50%, -50%);
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
width: $flux-connector-line;
|
||||
height: $flux-node-gap;
|
||||
@include gradient-v($c-pool, $g4-onyx);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.variable-name {
|
||||
.variable-node--name {
|
||||
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;
|
||||
}
|
||||
|
||||
.variable-value--boolean {
|
||||
.variable-node--boolean,
|
||||
.func-arg--boolean {
|
||||
color: $flux-boolean-color;
|
||||
}
|
||||
|
||||
.variable-value--number {
|
||||
.variable-node--number,
|
||||
.func-arg--number {
|
||||
color: $flux-number-color;
|
||||
}
|
||||
|
||||
.variable-value--object {
|
||||
.variable-node--object,
|
||||
.func-arg--object {
|
||||
color: $flux-object-color;
|
||||
}
|
||||
|
||||
.variable-value--invalid {
|
||||
.variable-node--invalid,
|
||||
.func-arg--invalid {
|
||||
color: $flux-invalid-color;
|
||||
}
|
||||
|
||||
|
@ -89,24 +130,59 @@ $flux-invalid-color: $c-viridian;
|
|||
@extend %flux-node;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
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 {
|
||||
content: '';
|
||||
height: 4px;
|
||||
width: $flux-node-gap;
|
||||
background-color: $g4-onyx;
|
||||
position: absolute;
|
||||
} // Vertical Line
|
||||
&:before {
|
||||
width: $flux-connector-line;
|
||||
height: calc(100% + #{$flux-node-tooltip-gap});
|
||||
top: -$flux-node-tooltip-gap / 2;
|
||||
left: $flux-node-gap / 2;
|
||||
transform: translateX(-50%);
|
||||
} // Horizontal Line
|
||||
&:after {
|
||||
height: $flux-connector-line;
|
||||
width: $flux-node-gap / 2;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
transform: translate(-100%, -50%);
|
||||
left: $flux-node-gap / 2;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child:after {
|
||||
content: none;
|
||||
margin-left: 0;
|
||||
// When a query exists unassigned to a variable
|
||||
.func-node:first-child {
|
||||
margin-left: 0;
|
||||
padding-left: $flux-node-gap;
|
||||
.func-node--connector {
|
||||
transform: translateX(0);
|
||||
z-index: 2; // Vertical Line
|
||||
&:before {
|
||||
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,
|
||||
.variable-name--tooltip {
|
||||
.func-node--tooltip {
|
||||
background-color: $g3-castle;
|
||||
border-radius: $radius;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: calc(100% + #{$flux-node-tooltip-gap});
|
||||
left: 0;
|
||||
top: 0;
|
||||
left: calc(100% + #{$flux-node-tooltip-gap});
|
||||
z-index: 9999;
|
||||
box-shadow: 0 0 10px 2px $g2-kevlar; // Caret
|
||||
&:before {
|
||||
|
@ -157,33 +231,40 @@ $flux-invalid-color: $c-viridian;
|
|||
border-width: 9px;
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
border-bottom-color: $g3-castle;
|
||||
border-right-color: $g3-castle;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: $flux-node-padding + 3px;
|
||||
transform: translate(-50%, -100%);
|
||||
top: $flux-node-height / 2;
|
||||
left: 0;
|
||||
transform: translate(-100%, -50%);
|
||||
} // Invisible block to continue hovering
|
||||
&:after {
|
||||
content: '';
|
||||
width: 80%;
|
||||
height: 7px;
|
||||
height: 50%;
|
||||
width: $flux-node-tooltip-gap * 3;
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
left: 0;
|
||||
top: 0;
|
||||
left: -$flux-node-tooltip-gap * 3;
|
||||
}
|
||||
}
|
||||
|
||||
.func-node--buttons {
|
||||
.func-arg--buttons {
|
||||
display: flex;
|
||||
margin-top: 12px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.func-node--delete,
|
||||
.func-node--build {
|
||||
width: 60px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.func-node--sub .func-arg {
|
||||
.func-args {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.func-arg {
|
||||
min-width: $flux-arg-min-width;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
|
@ -212,26 +293,6 @@ $flux-invalid-color: $c-viridian;
|
|||
width: 300px;
|
||||
}
|
||||
|
||||
.variable-name--tooltip {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.variable-name--input {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.variable-name--operator {
|
||||
width: 20px;
|
||||
height: 30px;
|
||||
text-align: center;
|
||||
line-height: 30px;
|
||||
font-weight: 600;
|
||||
@include no-user-select();
|
||||
}
|
||||
|
||||
/*
|
||||
Filter Preview Styles
|
||||
------------------------------------------------------------------------------
|
||||
|
@ -262,16 +323,16 @@ $flux-filter-parens: $g5-pepper;
|
|||
padding: 0 ($flux-filter-gap / 2);
|
||||
}
|
||||
|
||||
.flux-filter--value + .flux-filter--operator,
|
||||
.flux-filter--paren-close + .flux-filter--operator {
|
||||
.flux-filter--value+.flux-filter--operator,
|
||||
.flux-filter--paren-close+.flux-filter--operator {
|
||||
padding: 0 $flux-filter-gap;
|
||||
}
|
||||
|
||||
.flux-filter--key + .flux-filter--operator {
|
||||
.flux-filter--key+.flux-filter--operator {
|
||||
background-color: $flux-filter-expression;
|
||||
}
|
||||
|
||||
.flux-filter--key + .flux-filter--operator + .flux-filter--value {
|
||||
.flux-filter--key+.flux-filter--operator+.flux-filter--value {
|
||||
background-color: $flux-filter-expression;
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
|
@ -296,8 +357,7 @@ $flux-filter-parens: $g5-pepper;
|
|||
height: $flux-filter-unit-wrapped;
|
||||
width: ($flux-filter-unit-wrapped - $flux-filter-unit) / 2;
|
||||
background-color: $flux-filter-parens;
|
||||
border: (($flux-filter-unit-wrapped - $flux-filter-unit) / 2) solid
|
||||
$flux-filter-expression;
|
||||
border: (($flux-filter-unit-wrapped - $flux-filter-unit) / 2) solid $flux-filter-expression;
|
||||
}
|
||||
|
||||
.flux-filter--paren-open {
|
||||
|
@ -346,4 +406,17 @@ $flux-filter-parens: $g5-pepper;
|
|||
background-color: $flux-filter-expression;
|
||||
height: $flux-filter-unit-wrapped;
|
||||
line-height: $flux-filter-unit-wrapped;
|
||||
}
|
||||
|
||||
.flux-filter--fancyscroll {
|
||||
min-width: 300px;
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.flux-filter--helper-text {
|
||||
@include no-user-select();
|
||||
color: $g13-mist;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
padding-left: 20px;
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
$flux-tree-min-width: 500px;
|
||||
$flux-tree-min-width: 250px;
|
||||
$flux-tree-indent: 26px;
|
||||
$flux-tree-line: 2px;
|
||||
$flux-tree-max-filter: 220px;
|
||||
|
@ -22,11 +22,15 @@ $flux-tree-gutter: 11px;
|
|||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding-left: 0;
|
||||
> .flux-schema-tree {
|
||||
> .flux-schema--children > .flux-schema-tree {
|
||||
padding-left: $flux-tree-indent;
|
||||
}
|
||||
}
|
||||
|
||||
.flux-schema--children.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.flux-schema-tree__empty {
|
||||
height: $flux-tree-indent;
|
||||
display: flex;
|
||||
|
|
|
@ -242,6 +242,10 @@ $logs-viewer-gutter: 60px;
|
|||
}
|
||||
}
|
||||
|
||||
.message--cell {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
// Table Cell Styles
|
||||
.logs-viewer--cell {
|
||||
font-size: 12px;
|
||||
|
|
|
@ -65,7 +65,7 @@ export interface DecimalPlaces {
|
|||
}
|
||||
|
||||
export interface Cell {
|
||||
id: string
|
||||
i: string
|
||||
x: number
|
||||
y: number
|
||||
w: number
|
||||
|
|
|
@ -65,6 +65,26 @@ export interface MemberExpressionNode {
|
|||
property: PropertyNode
|
||||
}
|
||||
|
||||
export type FilterNode = BinaryExpressionNode | MemberExpressionNode
|
||||
|
||||
export interface FilterTagCondition {
|
||||
key: string
|
||||
operator: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface FilterClause {
|
||||
[tagKey: string]: FilterTagCondition[]
|
||||
}
|
||||
|
||||
export type SetFilterTagValue = (
|
||||
key: string,
|
||||
value: string,
|
||||
selected: boolean
|
||||
) => void
|
||||
|
||||
export type SetEquality = (tagKey: string, equal: boolean) => void
|
||||
|
||||
export interface FlatBody {
|
||||
type: string
|
||||
source: string
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {LayoutCell, LayoutQuery} from './layouts'
|
||||
import {Service, NewService} from './services'
|
||||
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 {
|
||||
GroupBy,
|
||||
|
@ -40,6 +40,7 @@ export {
|
|||
TemplateValue,
|
||||
Cell,
|
||||
CellQuery,
|
||||
CellType,
|
||||
Legend,
|
||||
Status,
|
||||
Query,
|
||||
|
|
|
@ -18,4 +18,5 @@ export interface LogsState {
|
|||
tableData: object[]
|
||||
searchTerm: string | null
|
||||
filters: Filter[]
|
||||
queryCount: number
|
||||
}
|
||||
|
|
|
@ -89,6 +89,7 @@ const flattenGroupBySeries = (
|
|||
},
|
||||
],
|
||||
responseIndex,
|
||||
isGroupBy: true,
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -142,10 +143,11 @@ const constructResults = (
|
|||
|
||||
const constructSerieses = (results: Result[]): Series[] => {
|
||||
return _.flatten(
|
||||
fastMap<Result, Series[]>(results, ({series, responseIndex}) =>
|
||||
fastMap<Result, Series[]>(results, ({series, responseIndex, isGroupBy}) =>
|
||||
fastMap<TimeSeriesSeries, Series>(series, (s, index) => ({
|
||||
...s,
|
||||
responseIndex,
|
||||
isGroupBy,
|
||||
seriesIndex: index,
|
||||
}))
|
||||
)
|
||||
|
|
|
@ -24,7 +24,7 @@ const setup = (override = {}) => {
|
|||
cell,
|
||||
timeRange,
|
||||
autoRefresh: 0,
|
||||
dashboardID: '9',
|
||||
dashboardID: 9,
|
||||
queryStatus: {
|
||||
queryID: null,
|
||||
status: null,
|
||||
|
|
|
@ -1,43 +1,25 @@
|
|||
import _ from 'lodash'
|
||||
|
||||
import reducer from 'src/dashboards/reducers/ui'
|
||||
import {template, dashboard, cell} from 'test/resources'
|
||||
import {initialState} from 'src/dashboards/reducers/ui'
|
||||
|
||||
import {
|
||||
setTimeRange,
|
||||
loadDashboards,
|
||||
deleteDashboard,
|
||||
deleteDashboardFailed,
|
||||
setTimeRange,
|
||||
updateDashboardCells,
|
||||
editDashboardCell,
|
||||
renameDashboardCell,
|
||||
syncDashboardCell,
|
||||
deleteDashboardFailed,
|
||||
templateVariableSelected,
|
||||
templateVariablesSelectedByName,
|
||||
cancelEditCell,
|
||||
editTemplateVariableValues,
|
||||
templateVariablesSelectedByName,
|
||||
setActiveCell,
|
||||
} from 'src/dashboards/actions'
|
||||
|
||||
let state
|
||||
|
||||
const t1 = {
|
||||
id: '1',
|
||||
type: 'tagKeys',
|
||||
label: 'test query',
|
||||
tempVar: ':region:',
|
||||
query: {
|
||||
db: 'db1',
|
||||
rp: 'rp1',
|
||||
measurement: 'm1',
|
||||
influxql: 'SHOW TAGS WHERE CHRONOGIRAFFE = "friend"',
|
||||
},
|
||||
values: [
|
||||
{value: 'us-west', type: 'tagKey', selected: false},
|
||||
{value: 'us-east', type: 'tagKey', selected: true},
|
||||
{value: 'us-mount', type: 'tagKey', selected: false},
|
||||
],
|
||||
}
|
||||
|
||||
const t2 = {
|
||||
...template,
|
||||
id: '2',
|
||||
type: 'csv',
|
||||
label: 'test csv',
|
||||
|
@ -49,35 +31,15 @@ const t2 = {
|
|||
],
|
||||
}
|
||||
|
||||
const templates = [t1, t2]
|
||||
const templates = [template, t2]
|
||||
|
||||
const d1 = {
|
||||
id: 1,
|
||||
cells: [],
|
||||
name: 'd1',
|
||||
...dashboard,
|
||||
templates,
|
||||
}
|
||||
|
||||
const d2 = {id: 2, cells: [], name: 'd2', templates: []}
|
||||
const d2 = {...dashboard, id: 2, cells: [], name: 'd2', templates: []}
|
||||
const dashboards = [d1, d2]
|
||||
const c1 = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 4,
|
||||
h: 4,
|
||||
id: 1,
|
||||
i: 'im-a-cell-id-index',
|
||||
isEditing: false,
|
||||
name: 'Gigawatts',
|
||||
}
|
||||
|
||||
const editingCell = {
|
||||
i: 1,
|
||||
isEditing: true,
|
||||
name: 'Edit me',
|
||||
}
|
||||
|
||||
const cells = [c1]
|
||||
|
||||
describe('DataExplorer.Reducers.UI', () => {
|
||||
it('can load the dashboards', () => {
|
||||
|
@ -90,11 +52,8 @@ describe('DataExplorer.Reducers.UI', () => {
|
|||
})
|
||||
|
||||
it('can delete a dashboard', () => {
|
||||
const initialState = {...state, dashboards}
|
||||
const actual = reducer(initialState, deleteDashboard(d1))
|
||||
const expected = initialState.dashboards.filter(
|
||||
dashboard => dashboard.id !== d1.id
|
||||
)
|
||||
const actual = reducer({...initialState, dashboards}, deleteDashboard(d1))
|
||||
const expected = dashboards.filter(dash => dash.id !== d1.id)
|
||||
|
||||
expect(actual.dashboards).toEqual(expected)
|
||||
})
|
||||
|
@ -117,43 +76,14 @@ describe('DataExplorer.Reducers.UI', () => {
|
|||
expect(actual.timeRange).toEqual(expected)
|
||||
})
|
||||
|
||||
it('can update dashboard cells', () => {
|
||||
state = {
|
||||
dashboards,
|
||||
}
|
||||
|
||||
const updatedCells = [{id: 1}, {id: 2}]
|
||||
|
||||
const expected = {
|
||||
id: 1,
|
||||
cells: updatedCells,
|
||||
name: 'd1',
|
||||
templates,
|
||||
}
|
||||
|
||||
const actual = reducer(state, updateDashboardCells(d1, updatedCells))
|
||||
|
||||
expect(actual.dashboards[0]).toEqual(expected)
|
||||
})
|
||||
|
||||
it('can edit a cell', () => {
|
||||
const dash = {...d1, cells}
|
||||
state = {
|
||||
dashboards: [dash],
|
||||
}
|
||||
|
||||
const actual = reducer(state, editDashboardCell(dash, 0, 0, true))
|
||||
expect(actual.dashboards[0].cells[0].isEditing).toBe(true)
|
||||
})
|
||||
|
||||
it('can sync a cell', () => {
|
||||
const newCellName = 'watts is kinda cool'
|
||||
const newCell = {
|
||||
x: c1.x,
|
||||
y: c1.y,
|
||||
...cell,
|
||||
name: newCellName,
|
||||
}
|
||||
const dash = {...d1, cells: [c1]}
|
||||
|
||||
const dash = {...d1, cells: [cell]}
|
||||
state = {
|
||||
dashboards: [dash],
|
||||
}
|
||||
|
@ -162,22 +92,6 @@ describe('DataExplorer.Reducers.UI', () => {
|
|||
expect(actual.dashboards[0].cells[0].name).toBe(newCellName)
|
||||
})
|
||||
|
||||
it('can rename cells', () => {
|
||||
const c2 = {...c1, isEditing: true}
|
||||
const dash = {...d1, cells: [c2]}
|
||||
state = {
|
||||
dashboards: [dash],
|
||||
}
|
||||
|
||||
const actual = reducer(
|
||||
state,
|
||||
renameDashboardCell(dash, 0, 0, 'Plutonium Consumption Rate (ug/sec)')
|
||||
)
|
||||
expect(actual.dashboards[0].cells[0].name).toBe(
|
||||
'Plutonium Consumption Rate (ug/sec)'
|
||||
)
|
||||
})
|
||||
|
||||
it('can select a different template variable', () => {
|
||||
const dash = _.cloneDeep(d1)
|
||||
state = {
|
||||
|
@ -215,24 +129,20 @@ describe('DataExplorer.Reducers.UI', () => {
|
|||
expect(actual.dashboards[0].templates[1].values[2].selected).toBe(false)
|
||||
})
|
||||
|
||||
it('can cancel cell editing', () => {
|
||||
const dash = _.cloneDeep(d1)
|
||||
dash.cells = [editingCell]
|
||||
describe('SET_ACTIVE_CELL', () => {
|
||||
it('can set the active cell', () => {
|
||||
const activeCellID = '1'
|
||||
const actual = reducer(initialState, setActiveCell(activeCellID))
|
||||
|
||||
const actual = reducer(
|
||||
{dashboards: [dash]},
|
||||
cancelEditCell(dash.id, editingCell.i)
|
||||
)
|
||||
|
||||
expect(actual.dashboards[0].cells[0].isEditing).toBe(false)
|
||||
expect(actual.dashboards[0].cells[0].name).toBe(editingCell.name)
|
||||
expect(actual.activeCellID).toEqual(activeCellID)
|
||||
})
|
||||
})
|
||||
|
||||
describe('EDIT_TEMPLATE_VARIABLE_VALUES', () => {
|
||||
it('can edit the tempvar values', () => {
|
||||
const actual = reducer(
|
||||
{dashboards},
|
||||
editTemplateVariableValues(d1.id, t1.id, ['v1', 'v2'])
|
||||
{...initialState, dashboards},
|
||||
editTemplateVariableValues(d1.id, template.id, ['v1', 'v2'])
|
||||
)
|
||||
|
||||
const expected = [
|
||||
|
@ -252,12 +162,12 @@ describe('DataExplorer.Reducers.UI', () => {
|
|||
})
|
||||
|
||||
it('can handle an empty template.values', () => {
|
||||
const ts = [{...t1, values: []}]
|
||||
const ts = [{...template, values: []}]
|
||||
const ds = [{...d1, templates: ts}]
|
||||
|
||||
const actual = reducer(
|
||||
{dashboards: ds},
|
||||
editTemplateVariableValues(d1.id, t1.id, ['v1', 'v2'])
|
||||
{...initialState, dashboards: ds},
|
||||
editTemplateVariableValues(d1.id, template.id, ['v1', 'v2'])
|
||||
)
|
||||
|
||||
const expected = [
|
|
@ -161,7 +161,7 @@ export const decimalPlaces: DecimalPlaces = {
|
|||
}
|
||||
|
||||
export const cell: Cell = {
|
||||
id: '67435af2-17bf-4caa-a5fc-0dd1ffb40dab',
|
||||
i: '67435af2-17bf-4caa-a5fc-0dd1ffb40dab',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 8,
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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")'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react'
|
||||
import {shallow} from 'enzyme'
|
||||
import From from 'src/flux/components/From'
|
||||
import FromDatabaseDropdown from 'src/flux/components/FromDatabaseDropdown'
|
||||
import {service} from 'test/resources'
|
||||
|
||||
jest.mock('src/shared/apis/metaQuery', () => require('mocks/flux/apis'))
|
||||
|
@ -16,14 +16,14 @@ const setup = () => {
|
|||
onChangeArg: () => {},
|
||||
}
|
||||
|
||||
const wrapper = shallow(<From {...props} />)
|
||||
const wrapper = shallow(<FromDatabaseDropdown {...props} />)
|
||||
|
||||
return {
|
||||
wrapper,
|
||||
}
|
||||
}
|
||||
|
||||
describe('Flux.Components.From', () => {
|
||||
describe('Flux.Components.FromDatabaseDropdown', () => {
|
||||
describe('rendering', () => {
|
||||
it('renders without errors', () => {
|
||||
const {wrapper} = setup()
|
|
@ -1,4 +1,4 @@
|
|||
import {Source} from 'src/types'
|
||||
import {Source, Template, Dashboard, Cell, CellType} from 'src/types'
|
||||
import {SourceLinks} from 'src/types/sources'
|
||||
|
||||
export const role = {
|
||||
|
@ -586,3 +586,83 @@ export const hosts = {
|
|||
load: 0,
|
||||
},
|
||||
}
|
||||
|
||||
// Dashboards
|
||||
export const template: Template = {
|
||||
id: '1',
|
||||
type: 'tagKeys',
|
||||
label: 'test query',
|
||||
tempVar: ':region:',
|
||||
query: {
|
||||
db: 'db1',
|
||||
command: '',
|
||||
rp: 'rp1',
|
||||
tagKey: 'tk1',
|
||||
fieldKey: 'fk1',
|
||||
measurement: 'm1',
|
||||
influxql: 'SHOW TAGS WHERE CHRONOGIRAFFE = "friend"',
|
||||
},
|
||||
values: [
|
||||
{value: 'us-west', type: 'tagKey', selected: false},
|
||||
{value: 'us-east', type: 'tagKey', selected: true},
|
||||
{value: 'us-mount', type: 'tagKey', selected: false},
|
||||
],
|
||||
}
|
||||
|
||||
export const dashboard: Dashboard = {
|
||||
id: 1,
|
||||
cells: [],
|
||||
name: 'd1',
|
||||
templates: [],
|
||||
organization: 'thebestorg',
|
||||
}
|
||||
|
||||
export const cell: Cell = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 4,
|
||||
h: 4,
|
||||
i: '0246e457-916b-43e3-be99-211c4cbc03e8',
|
||||
name: 'Apache Bytes/Second',
|
||||
queries: [],
|
||||
axes: {
|
||||
x: {
|
||||
bounds: ['', ''],
|
||||
label: '',
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
base: '',
|
||||
scale: '',
|
||||
},
|
||||
y: {
|
||||
bounds: ['', ''],
|
||||
label: '',
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
base: '',
|
||||
scale: '',
|
||||
},
|
||||
},
|
||||
type: CellType.Line,
|
||||
colors: [],
|
||||
tableOptions: {
|
||||
verticalTimeAxis: true,
|
||||
sortBy: {
|
||||
internalName: '',
|
||||
displayName: '',
|
||||
visible: true,
|
||||
},
|
||||
fixFirstColumn: true,
|
||||
},
|
||||
fieldOptions: [],
|
||||
timeFormat: '',
|
||||
decimalPlaces: {
|
||||
isEnforced: false,
|
||||
digits: 1,
|
||||
},
|
||||
links: {
|
||||
self:
|
||||
'/chronograf/v1/dashboards/10/cells/8b3b7897-49b1-422c-9443-e9b778bcbf12',
|
||||
},
|
||||
legend: {},
|
||||
}
|
||||
|
|
|
@ -134,3 +134,15 @@ export const CSV_TO_DYGRAPH_MISMATCHED = `
|
|||
,,1,2018-06-04T17:12:21.025984999Z,2018-06-04T17:13:00Z,2018-06-05T17:12:25Z,10,available,mem,bertrand.local
|
||||
,,1,2018-06-04T17:12:21.025984999Z,2018-06-04T17:13:00Z,2018-06-05T17:12:35Z,11,available,mem,bertrand.local
|
||||
`
|
||||
|
||||
export const TRUNCATED_RESPONSE = `
|
||||
#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,double,string,string,string,string
|
||||
#partition,false,false,false,false,false,false,true,true,true,true
|
||||
#default,_result,,,,,,,,,
|
||||
,result,table,_start,_stop,_time,_value,_field,_measurement,cpu,host
|
||||
,,0,1677-09-21T00:12:43.145224192Z,2018-05-22T22:39:17.042276772Z,2018-05-22T22:39:12.584Z,0,usage_guest,cpu,cpu-total,WattsInfluxDB
|
||||
,,1,1677-09-21T00:12:43.145224192Z,2018-05-22T22:39:17.042276772Z,2018-05-22T22:39:12.584Z,0,usage_guest_nice,cpu,cpu-total,WattsInfluxDB
|
||||
|
||||
#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,long,string,string,string,string,string,string,string
|
||||
#partition,false,false,false,false,false,false,true,true,true,true,true,true,true
|
||||
#default,_result,,,,,,,,,,,,`
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
RESPONSE_METADATA,
|
||||
MULTI_SCHEMA_RESPONSE,
|
||||
EXPECTED_COLUMNS,
|
||||
TRUNCATED_RESPONSE,
|
||||
} from 'test/shared/parsing/flux/constants'
|
||||
|
||||
describe('Flux results parser', () => {
|
||||
|
@ -37,4 +38,12 @@ describe('Flux results parser', () => {
|
|||
expect(actual).toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('partial responses', () => {
|
||||
it('should discard tables without any non-annotation rows', () => {
|
||||
const actual = parseResponse(TRUNCATED_RESPONSE)
|
||||
|
||||
expect(actual).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
173
ui/yarn.lock
173
ui/yarn.lock
|
@ -169,14 +169,10 @@ acorn@^4.0.3, acorn@^4.0.4:
|
|||
version "4.0.13"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
|
||||
|
||||
acorn@^5.0.0, acorn@^5.3.0:
|
||||
acorn@^5.0.0, acorn@^5.2.1, acorn@^5.3.0, acorn@^5.5.0:
|
||||
version "5.6.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.6.1.tgz#c9e50c3e3717cf897f1b071ceadbb543bbc0a8d4"
|
||||
|
||||
acorn@^5.2.1, acorn@^5.5.0:
|
||||
version "5.5.3"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9"
|
||||
|
||||
add-px-to-style@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/add-px-to-style/-/add-px-to-style-1.0.0.tgz#d0c135441fa8014a8137904531096f67f28f263a"
|
||||
|
@ -458,18 +454,12 @@ async@^1.4.0, async@^1.5.2:
|
|||
version "1.5.2"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
|
||||
|
||||
async@^2.1.2, async@^2.3.0, async@^2.4.1:
|
||||
async@^2.1.2, async@^2.1.4, async@^2.3.0, async@^2.4.1:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4"
|
||||
dependencies:
|
||||
lodash "^4.14.0"
|
||||
|
||||
async@^2.1.4:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610"
|
||||
dependencies:
|
||||
lodash "^4.17.10"
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
|
@ -977,7 +967,7 @@ babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015
|
|||
babel-runtime "^6.22.0"
|
||||
babel-template "^6.24.1"
|
||||
|
||||
babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1:
|
||||
babel-plugin-transform-es2015-modules-commonjs@^6.23.0:
|
||||
version "6.26.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz#0d8394029b7dc6abe1a97ef181e00758dd2e5d8a"
|
||||
dependencies:
|
||||
|
@ -986,7 +976,7 @@ babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-e
|
|||
babel-template "^6.26.0"
|
||||
babel-types "^6.26.0"
|
||||
|
||||
babel-plugin-transform-es2015-modules-commonjs@^6.26.2:
|
||||
babel-plugin-transform-es2015-modules-commonjs@^6.24.1, babel-plugin-transform-es2015-modules-commonjs@^6.26.2:
|
||||
version "6.26.2"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz#58a793863a9e7ca870bdc5a881117ffac27db6f3"
|
||||
dependencies:
|
||||
|
@ -1748,15 +1738,7 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
|
|||
strip-ansi "^3.0.0"
|
||||
supports-color "^2.0.0"
|
||||
|
||||
chalk@^2.0.0, chalk@^2.0.1:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
|
||||
dependencies:
|
||||
ansi-styles "^3.2.1"
|
||||
escape-string-regexp "^1.0.5"
|
||||
supports-color "^5.3.0"
|
||||
|
||||
chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.2:
|
||||
chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.2:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.0.tgz#a060a297a6b57e15b61ca63ce84995daa0fe6e52"
|
||||
dependencies:
|
||||
|
@ -2168,14 +2150,10 @@ core-js@^1.0.0:
|
|||
version "1.2.7"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
|
||||
|
||||
core-js@^2.1.3:
|
||||
core-js@^2.1.3, core-js@^2.4.0, core-js@^2.5.0:
|
||||
version "2.5.5"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.5.tgz#b14dde936c640c0579a6b50cabcc132dd6127e3b"
|
||||
|
||||
core-js@^2.4.0, core-js@^2.5.0:
|
||||
version "2.5.7"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e"
|
||||
|
||||
core-util-is@1.0.2, core-util-is@~1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
||||
|
@ -2280,18 +2258,12 @@ cross-spawn@^5.0.1, cross-spawn@^5.1.0:
|
|||
shebang-command "^1.2.0"
|
||||
which "^1.2.9"
|
||||
|
||||
crossvent@1.5.0:
|
||||
crossvent@1.5.0, crossvent@^1.3.1:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/crossvent/-/crossvent-1.5.0.tgz#3779c1242699e19417f0414e61b144753a52fd6d"
|
||||
dependencies:
|
||||
custom-event "1.0.0"
|
||||
|
||||
crossvent@^1.3.1:
|
||||
version "1.5.5"
|
||||
resolved "https://registry.yarnpkg.com/crossvent/-/crossvent-1.5.5.tgz#ad20878e4921e9be73d9d6976f8b2ecd0f71a0b1"
|
||||
dependencies:
|
||||
custom-event "^1.0.0"
|
||||
|
||||
cryptiles@2.x.x:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
|
||||
|
@ -2464,10 +2436,6 @@ custom-event@1.0.0:
|
|||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.0.tgz#2e4628be19dc4b214b5c02630c5971e811618062"
|
||||
|
||||
custom-event@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
|
||||
|
||||
cyclist@~0.2.2:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
|
||||
|
@ -2773,20 +2741,13 @@ domutils@1.1:
|
|||
dependencies:
|
||||
domelementtype "1"
|
||||
|
||||
domutils@1.5.1:
|
||||
domutils@1.5.1, domutils@^1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
|
||||
dependencies:
|
||||
dom-serializer "0"
|
||||
domelementtype "1"
|
||||
|
||||
domutils@^1.5.1:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
|
||||
dependencies:
|
||||
dom-serializer "0"
|
||||
domelementtype "1"
|
||||
|
||||
duplexer@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
|
||||
|
@ -2931,17 +2892,7 @@ error-ex@^1.2.0, error-ex@^1.3.1:
|
|||
dependencies:
|
||||
is-arrayish "^0.2.1"
|
||||
|
||||
es-abstract@^1.5.1:
|
||||
version "1.12.0"
|
||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165"
|
||||
dependencies:
|
||||
es-to-primitive "^1.1.1"
|
||||
function-bind "^1.1.1"
|
||||
has "^1.0.1"
|
||||
is-callable "^1.1.3"
|
||||
is-regex "^1.0.4"
|
||||
|
||||
es-abstract@^1.6.1, es-abstract@^1.7.0:
|
||||
es-abstract@^1.5.1, es-abstract@^1.6.1, es-abstract@^1.7.0:
|
||||
version "1.11.0"
|
||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.11.0.tgz#cce87d518f0496893b1a30cd8461835535480681"
|
||||
dependencies:
|
||||
|
@ -3424,14 +3375,10 @@ extract-text-webpack-plugin@^3.0.2:
|
|||
schema-utils "^0.3.0"
|
||||
webpack-sources "^1.0.1"
|
||||
|
||||
extsprintf@1.3.0:
|
||||
extsprintf@1.3.0, extsprintf@^1.2.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
|
||||
|
||||
extsprintf@^1.2.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
|
||||
|
||||
fast-deep-equal@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614"
|
||||
|
@ -4300,22 +4247,10 @@ https-browserify@^1.0.0:
|
|||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
|
||||
|
||||
iconv-lite@0.4.19:
|
||||
iconv-lite@0.4.19, iconv-lite@^0.4.17, iconv-lite@^0.4.4, iconv-lite@^0.4.5, iconv-lite@~0.4.13:
|
||||
version "0.4.19"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
|
||||
|
||||
iconv-lite@^0.4.17, iconv-lite@^0.4.5, iconv-lite@~0.4.13:
|
||||
version "0.4.21"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.21.tgz#c47f8733d02171189ebc4a400f3218d348094798"
|
||||
dependencies:
|
||||
safer-buffer "^2.1.0"
|
||||
|
||||
iconv-lite@^0.4.4:
|
||||
version "0.4.23"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3"
|
||||
|
||||
icss-replace-symbols@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
|
||||
|
@ -5732,11 +5667,11 @@ lodash@4.17.4:
|
|||
version "4.17.4"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
|
||||
|
||||
lodash@^4.0.0, lodash@^4.1.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1, lodash@~4.17.4:
|
||||
lodash@^4.0.0, lodash@^4.1.0, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1:
|
||||
version "4.17.5"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
|
||||
|
||||
lodash@^4.13.1, lodash@^4.14.0, lodash@^4.17.10, lodash@^4.17.4:
|
||||
lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.2, lodash@~4.17.4:
|
||||
version "4.17.10"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
|
||||
|
||||
|
@ -5771,14 +5706,7 @@ lower-case@^1.1.1:
|
|||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac"
|
||||
|
||||
lru-cache@^4.0.1:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c"
|
||||
dependencies:
|
||||
pseudomap "^1.0.2"
|
||||
yallist "^2.1.2"
|
||||
|
||||
lru-cache@^4.1.1:
|
||||
lru-cache@^4.0.1, lru-cache@^4.1.1:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.2.tgz#45234b2e6e2f2b33da125624c4664929a0224c3f"
|
||||
dependencies:
|
||||
|
@ -5969,7 +5897,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
|
|||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@0.0.8:
|
||||
minimist@0.0.8, minimist@~0.0.1:
|
||||
version "0.0.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
|
||||
|
||||
|
@ -5977,10 +5905,6 @@ minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0:
|
|||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
|
||||
|
||||
minimist@~0.0.1:
|
||||
version "0.0.10"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
|
||||
|
||||
minipass@^2.2.1, minipass@^2.3.3:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.3.tgz#a7dcc8b7b833f5d368759cce544dccb55f50f233"
|
||||
|
@ -7343,7 +7267,7 @@ q@^1.1.2:
|
|||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
|
||||
|
||||
qs@6.5.1:
|
||||
qs@6.5.1, qs@~6.5.1:
|
||||
version "6.5.1"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
|
||||
|
||||
|
@ -7351,10 +7275,6 @@ qs@~6.3.0:
|
|||
version "6.3.2"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c"
|
||||
|
||||
qs@~6.5.1:
|
||||
version "6.5.2"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
||||
|
||||
query-string@^4.1.0, query-string@^4.2.2:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
|
||||
|
@ -7862,7 +7782,7 @@ request-promise-native@^1.0.5:
|
|||
stealthy-require "^1.1.0"
|
||||
tough-cookie ">=2.3.3"
|
||||
|
||||
request@2, request@^2.79.0:
|
||||
request@2, request@^2.79.0, request@^2.83.0:
|
||||
version "2.85.0"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.85.0.tgz#5a03615a47c61420b3eb99b7dba204f83603e1fa"
|
||||
dependencies:
|
||||
|
@ -7889,31 +7809,6 @@ request@2, request@^2.79.0:
|
|||
tunnel-agent "^0.6.0"
|
||||
uuid "^3.1.0"
|
||||
|
||||
request@^2.83.0:
|
||||
version "2.87.0"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.87.0.tgz#32f00235cd08d482b4d0d68db93a829c0ed5756e"
|
||||
dependencies:
|
||||
aws-sign2 "~0.7.0"
|
||||
aws4 "^1.6.0"
|
||||
caseless "~0.12.0"
|
||||
combined-stream "~1.0.5"
|
||||
extend "~3.0.1"
|
||||
forever-agent "~0.6.1"
|
||||
form-data "~2.3.1"
|
||||
har-validator "~5.0.3"
|
||||
http-signature "~1.2.0"
|
||||
is-typedarray "~1.0.0"
|
||||
isstream "~0.1.2"
|
||||
json-stringify-safe "~5.0.1"
|
||||
mime-types "~2.1.17"
|
||||
oauth-sign "~0.8.2"
|
||||
performance-now "^2.1.0"
|
||||
qs "~6.5.1"
|
||||
safe-buffer "^5.1.1"
|
||||
tough-cookie "~2.3.3"
|
||||
tunnel-agent "^0.6.0"
|
||||
uuid "^3.1.0"
|
||||
|
||||
request@~2.79.0:
|
||||
version "2.79.0"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de"
|
||||
|
@ -8116,11 +8011,11 @@ rx@2.3.24:
|
|||
version "2.3.24"
|
||||
resolved "https://registry.yarnpkg.com/rx/-/rx-2.3.24.tgz#14f950a4217d7e35daa71bbcbe58eff68ea4b2b7"
|
||||
|
||||
safe-buffer@5.1.1, safe-buffer@^5.1.0:
|
||||
safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
|
||||
|
||||
safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
safe-buffer@^5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
|
||||
|
@ -8130,10 +8025,6 @@ safe-regex@^1.1.0:
|
|||
dependencies:
|
||||
ret "~0.1.10"
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
|
||||
sane@^2.0.0:
|
||||
version "2.5.2"
|
||||
resolved "https://registry.yarnpkg.com/sane/-/sane-2.5.2.tgz#b4dc1861c21b427e929507a3e751e2a2cb8ab3fa"
|
||||
|
@ -8590,11 +8481,7 @@ static-extend@^0.1.1:
|
|||
define-property "^0.2.5"
|
||||
object-copy "^0.1.0"
|
||||
|
||||
"statuses@>= 1.3.1 < 2", "statuses@>= 1.4.0 < 2":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
|
||||
|
||||
statuses@~1.4.0:
|
||||
"statuses@>= 1.3.1 < 2", "statuses@>= 1.4.0 < 2", statuses@~1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
|
||||
|
||||
|
@ -8676,11 +8563,7 @@ string_decoder@~0.10.x:
|
|||
version "0.10.31"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
|
||||
|
||||
stringstream@~0.0.4:
|
||||
version "0.0.5"
|
||||
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
|
||||
|
||||
stringstream@~0.0.5:
|
||||
stringstream@~0.0.4, stringstream@~0.0.5:
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.6.tgz#7880225b0d4ad10e30927d167a1d6f2fd3b33a72"
|
||||
|
||||
|
@ -9537,13 +9420,7 @@ which-module@^2.0.0:
|
|||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
|
||||
|
||||
which@1:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
|
||||
dependencies:
|
||||
isexe "^2.0.0"
|
||||
|
||||
which@^1.2.12, which@^1.2.9, which@^1.3.0:
|
||||
which@1, which@^1.2.12, which@^1.2.9, which@^1.3.0:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
|
||||
dependencies:
|
||||
|
@ -9559,14 +9436,10 @@ window-size@0.1.0:
|
|||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
|
||||
|
||||
wordwrap@0.0.2:
|
||||
wordwrap@0.0.2, wordwrap@~0.0.2:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
|
||||
|
||||
wordwrap@~0.0.2:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
|
||||
|
||||
wordwrap@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
|
||||
|
|
Loading…
Reference in New Issue