Merge pull request #3588 from influxdata/log-viewer/filtering

Log viewer/filtering
pull/3590/head
Brandon Farmer 2018-06-06 15:33:11 -07:00 committed by GitHub
commit 8ca0a6f1e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 358 additions and 141 deletions

View File

@ -10,7 +10,7 @@ import {
import {getDeep} from 'src/utils/wrappers' import {getDeep} from 'src/utils/wrappers'
import buildQuery from 'src/utils/influxql' import buildQuery from 'src/utils/influxql'
import {executeQueryAsync} from 'src/logs/api' import {executeQueryAsync} from 'src/logs/api'
import {LogsState} from 'src/types/localStorage' import {LogsState, Filter} from 'src/types/logs'
interface TableData { interface TableData {
columns: string[] columns: string[]
@ -49,8 +49,33 @@ export enum ActionTypes {
SetTableData = 'LOGS_SET_TABLE_DATA', SetTableData = 'LOGS_SET_TABLE_DATA',
ChangeZoom = 'LOGS_CHANGE_ZOOM', ChangeZoom = 'LOGS_CHANGE_ZOOM',
SetSearchTerm = 'LOGS_SET_SEARCH_TERM', SetSearchTerm = 'LOGS_SET_SEARCH_TERM',
AddFilter = 'LOGS_ADD_FILTER',
RemoveFilter = 'LOGS_REMOVE_FILTER',
ChangeFilter = 'LOGS_CHANGE_FILTER',
} }
export interface AddFilterAction {
type: ActionTypes.AddFilter
payload: {
filter: Filter
}
}
export interface ChangeFilterAction {
type: ActionTypes.ChangeFilter
payload: {
id: string
operator: string
value: string
}
}
export interface RemoveFilterAction {
type: ActionTypes.RemoveFilter
payload: {
id: string
}
}
interface SetSourceAction { interface SetSourceAction {
type: ActionTypes.SetSource type: ActionTypes.SetSource
payload: { payload: {
@ -133,6 +158,9 @@ export type Action =
| SetTableData | SetTableData
| SetTableQueryConfig | SetTableQueryConfig
| SetSearchTerm | SetSearchTerm
| AddFilterAction
| RemoveFilterAction
| ChangeFilterAction
const getTimeRange = (state: State): TimeRange | null => const getTimeRange = (state: State): TimeRange | null =>
getDeep<TimeRange | null>(state, 'logs.timeRange', null) getDeep<TimeRange | null>(state, 'logs.timeRange', null)
@ -152,11 +180,29 @@ const getTableQueryConfig = (state: State): QueryConfig | null =>
const getSearchTerm = (state: State): string | null => const getSearchTerm = (state: State): string | null =>
getDeep<string | null>(state, 'logs.searchTerm', null) getDeep<string | null>(state, 'logs.searchTerm', null)
const getFilters = (state: State): Filter[] =>
getDeep<Filter[]>(state, 'logs.filters', [])
export const changeFilter = (id: string, operator: string, value: string) => ({
type: ActionTypes.ChangeFilter,
payload: {id, operator, value},
})
export const setSource = (source: Source): SetSourceAction => ({ export const setSource = (source: Source): SetSourceAction => ({
type: ActionTypes.SetSource, type: ActionTypes.SetSource,
payload: {source}, payload: {source},
}) })
export const addFilter = (filter: Filter): AddFilterAction => ({
type: ActionTypes.AddFilter,
payload: {filter},
})
export const removeFilter = (id: string): RemoveFilterAction => ({
type: ActionTypes.RemoveFilter,
payload: {id},
})
const setHistogramData = (response): SetHistogramData => ({ const setHistogramData = (response): SetHistogramData => ({
type: ActionTypes.SetHistogramData, type: ActionTypes.SetHistogramData,
payload: {data: [{response}]}, payload: {data: [{response}]},
@ -173,9 +219,10 @@ export const executeHistogramQueryAsync = () => async (
const namespace = getNamespace(state) const namespace = getNamespace(state)
const proxyLink = getProxyLink(state) const proxyLink = getProxyLink(state)
const searchTerm = getSearchTerm(state) const searchTerm = getSearchTerm(state)
const filters = getFilters(state)
if (_.every([queryConfig, timeRange, namespace, proxyLink])) { if (_.every([queryConfig, timeRange, namespace, proxyLink])) {
const query = buildLogQuery(timeRange, queryConfig, searchTerm) const query = buildLogQuery(timeRange, queryConfig, filters, searchTerm)
const response = await executeQueryAsync(proxyLink, namespace, query) const response = await executeQueryAsync(proxyLink, namespace, query)
dispatch(setHistogramData(response)) dispatch(setHistogramData(response))
@ -198,9 +245,10 @@ export const executeTableQueryAsync = () => async (
const namespace = getNamespace(state) const namespace = getNamespace(state)
const proxyLink = getProxyLink(state) const proxyLink = getProxyLink(state)
const searchTerm = getSearchTerm(state) const searchTerm = getSearchTerm(state)
const filters = getFilters(state)
if (_.every([queryConfig, timeRange, namespace, proxyLink])) { if (_.every([queryConfig, timeRange, namespace, proxyLink])) {
const query = buildLogQuery(timeRange, queryConfig, searchTerm) const query = buildLogQuery(timeRange, queryConfig, filters, searchTerm)
const response = await executeQueryAsync(proxyLink, namespace, query) const response = await executeQueryAsync(proxyLink, namespace, query)
const series = getDeep(response, 'results.0.series.0', defaultTableData) const series = getDeep(response, 'results.0.series.0', defaultTableData)

View File

@ -1,17 +1,19 @@
import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react' import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react'
import classnames from 'classnames' import classnames from 'classnames'
import {Filter} from 'src/logs/containers/LogsPage' import {Filter} from 'src/types/logs'
import {getDeep} from 'src/utils/wrappers'
import {ClickOutside} from 'src/shared/components/ClickOutside' import {ClickOutside} from 'src/shared/components/ClickOutside'
interface Props { interface Props {
filter: Filter filter: Filter
onDelete: (id: string) => () => void onDelete: (id: string) => void
onChangeOperator: (id: string, newOperator: string) => void onChangeFilter: (id: string, newOperator: string, newValue: string) => void
onChangeValue: (id: string, newValue: string) => void
} }
interface State { interface State {
editing: boolean editing: boolean
value: string
operator: string
} }
class LogsFilter extends PureComponent<Props, State> { class LogsFilter extends PureComponent<Props, State> {
@ -20,28 +22,29 @@ class LogsFilter extends PureComponent<Props, State> {
this.state = { this.state = {
editing: false, editing: false,
value: this.props.filter.value,
operator: this.props.filter.operator,
} }
} }
public render() { public render() {
const {
filter: {id},
onDelete,
} = this.props
const {editing} = this.state const {editing} = this.state
return ( return (
<ClickOutside onClickOutside={this.handleClickOutside}> <ClickOutside onClickOutside={this.handleClickOutside}>
<li className={this.className} onClick={this.handleStartEdit}> <li className={this.className} onClick={this.handleStartEdit}>
{editing ? this.renderEditor : this.label} {editing ? this.renderEditor : this.label}
<div className="logs-viewer--filter-remove" onClick={onDelete(id)} /> <div
className="logs-viewer--filter-remove"
onClick={this.handleDelete}
/>
</li> </li>
</ClickOutside> </ClickOutside>
) )
} }
private handleClickOutside = (): void => { private handleClickOutside = (): void => {
this.setState({editing: false}) this.stopEditing()
} }
private handleStartEdit = (): void => { private handleStartEdit = (): void => {
@ -53,17 +56,28 @@ class LogsFilter extends PureComponent<Props, State> {
return classnames('logs-viewer--filter', {active: editing}) return classnames('logs-viewer--filter', {active: editing})
} }
private handleDelete = () => {
const id = getDeep(this.props, 'filter.id', '')
this.props.onDelete(id)
}
private get label(): JSX.Element { private get label(): JSX.Element {
const { const {
filter: {key, operator, value}, filter: {key, operator, value},
} = this.props } = this.props
return <span>{`${key} ${operator} ${value}`}</span> let displayKey = key
if (key === 'severity_1') {
displayKey = 'severity'
}
return <span>{`${displayKey} ${operator} ${value}`}</span>
} }
private get renderEditor(): JSX.Element { private get renderEditor(): JSX.Element {
const {operator, value} = this.state
const { const {
filter: {key, operator, value}, filter: {key},
} = this.props } = this.props
return ( return (
@ -92,38 +106,37 @@ class LogsFilter extends PureComponent<Props, State> {
} }
private handleOperatorInput = (e: ChangeEvent<HTMLInputElement>): void => { private handleOperatorInput = (e: ChangeEvent<HTMLInputElement>): void => {
const { const operator = getDeep(e, 'target.value', '').trim()
filter: {id},
onChangeOperator,
} = this.props
const cleanValue = this.enforceOperatorChars(e.target.value) this.setState({operator})
onChangeOperator(id, cleanValue)
} }
private handleValueInput = (e: ChangeEvent<HTMLInputElement>): void => { private handleValueInput = (e: ChangeEvent<HTMLInputElement>): void => {
const { const value = getDeep(e, 'target.value', '').trim()
filter: {id}, this.setState({value})
onChangeValue,
} = this.props
onChangeValue(id, e.target.value)
}
private enforceOperatorChars = text => {
return text
.split('')
.filter(t => ['!', '~', `=`].includes(t))
.join('')
} }
private handleKeyDown = (e: KeyboardEvent<HTMLInputElement>): void => { private handleKeyDown = (e: KeyboardEvent<HTMLInputElement>): void => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
e.preventDefault() e.preventDefault()
this.setState({editing: false}) this.stopEditing()
} }
} }
private stopEditing(): void {
const id = getDeep(this.props, 'filter.id', '')
const {operator, value} = this.state
let state = {}
if (['!=', '==', '=~'].includes(operator) && value !== '') {
this.props.onChangeFilter(id, operator, value)
} else {
const {filter} = this.props
state = {operator: filter.operator, value: filter.value}
}
this.setState({...state, editing: false})
}
} }
export default LogsFilter export default LogsFilter

View File

@ -1,11 +1,12 @@
import React, {PureComponent} from 'react' import React, {PureComponent} from 'react'
import {Filter} from 'src/logs/containers/LogsPage' import {Filter} from 'src/types/logs'
import FilterBlock from 'src/logs/components/LogsFilter' import FilterBlock from 'src/logs/components/LogsFilter'
interface Props { interface Props {
numResults: number numResults: number
filters: Filter[] filters: Filter[]
onUpdateFilters: (fitlers: Filter[]) => void onDelete: (id: string) => void
onFilterChange: (id: string, operator: string, value: string) => void
} }
class LogsFilters extends PureComponent<Props> { class LogsFilters extends PureComponent<Props> {
@ -29,48 +30,11 @@ class LogsFilters extends PureComponent<Props> {
<FilterBlock <FilterBlock
key={filter.id} key={filter.id}
filter={filter} filter={filter}
onDelete={this.handleDeleteFilter} onDelete={this.props.onDelete}
onChangeOperator={this.handleChangeFilterOperator} onChangeFilter={this.props.onFilterChange}
onChangeValue={this.handleChangeFilterValue}
/> />
)) ))
} }
private handleDeleteFilter = (id: string) => (): void => {
const {filters, onUpdateFilters} = this.props
const filteredFilters = filters.filter(filter => filter.id !== id)
onUpdateFilters(filteredFilters)
}
private handleChangeFilterOperator = (id: string, operator: string): void => {
const {filters, onUpdateFilters} = this.props
const filteredFilters = filters.map(filter => {
if (filter.id === id) {
return {...filter, operator}
}
return filter
})
onUpdateFilters(filteredFilters)
}
private handleChangeFilterValue = (id: string, value: string): void => {
const {filters, onUpdateFilters} = this.props
const filteredFilters = filters.map(filter => {
if (filter.id === id) {
return {...filter, value}
}
return filter
})
onUpdateFilters(filteredFilters)
}
} }
export default LogsFilters export default LogsFilters

View File

@ -7,6 +7,8 @@ import {getDeep} from 'src/utils/wrappers'
import FancyScrollbar from 'src/shared/components/FancyScrollbar' import FancyScrollbar from 'src/shared/components/FancyScrollbar'
const ROW_HEIGHT = 26 const ROW_HEIGHT = 26
const ROW_CHAR_LIMIT = 100
const CHAR_WIDTH = 7
interface Props { interface Props {
data: { data: {
@ -16,6 +18,7 @@ interface Props {
isScrolledToTop: boolean isScrolledToTop: boolean
onScrollVertical: () => void onScrollVertical: () => void
onScrolledToTop: () => void onScrolledToTop: () => void
onTagSelection: (selection: {tag: string; key: string}) => void
} }
interface State { interface State {
@ -26,23 +29,29 @@ interface State {
class LogsTable extends Component<Props, State> { class LogsTable extends Component<Props, State> {
public static getDerivedStateFromProps(props, state) { public static getDerivedStateFromProps(props, state) {
const {scrolledToTop} = props const {isScrolledToTop} = props
let scrollTop = _.get(state, 'scrollTop', 0) let scrollTop = _.get(state, 'scrollTop', 0)
if (scrolledToTop) { if (isScrolledToTop) {
scrollTop = 0 scrollTop = 0
} }
const scrollLeft = _.get(state, 'scrollLeft', 0)
return { return {
scrollTop, scrollTop,
scrollLeft: 0, scrollLeft,
currentRow: -1, currentRow: -1,
} }
} }
private grid: React.RefObject<Grid>
constructor(props: Props) { constructor(props: Props) {
super(props) super(props)
this.grid = React.createRef()
this.state = { this.state = {
scrollTop: 0, scrollTop: 0,
scrollLeft: 0, scrollLeft: 0,
@ -50,6 +59,10 @@ class LogsTable extends Component<Props, State> {
} }
} }
public componentDidUpdate() {
this.grid.current.recomputeGridSize()
}
public render() { public render() {
const rowCount = getDeep(this.props, 'data.values.length', 0) const rowCount = getDeep(this.props, 'data.values.length', 0)
const columnCount = getDeep(this.props, 'data.columns.length', 1) - 1 const columnCount = getDeep(this.props, 'data.columns.length', 1) - 1
@ -57,7 +70,7 @@ class LogsTable extends Component<Props, State> {
return ( return (
<div <div
className="logs-viewer--table-container" className="logs-viewer--table-container"
onMouseOut={this.handleMouseLeave} onMouseOut={this.handleMouseOut}
> >
<AutoSizer> <AutoSizer>
{({width}) => ( {({width}) => (
@ -84,11 +97,11 @@ class LogsTable extends Component<Props, State> {
}} }}
setScrollTop={this.handleScrollbarScroll} setScrollTop={this.handleScrollbarScroll}
scrollTop={this.state.scrollTop} scrollTop={this.state.scrollTop}
autoHide={true} autoHide={false}
> >
<Grid <Grid
height={height} height={height}
rowHeight={ROW_HEIGHT} rowHeight={this.calculateRowHeight}
rowCount={rowCount} rowCount={rowCount}
width={width} width={width}
scrollLeft={this.state.scrollLeft} scrollLeft={this.state.scrollLeft}
@ -97,7 +110,8 @@ class LogsTable extends Component<Props, State> {
cellRenderer={this.cellRenderer} cellRenderer={this.cellRenderer}
columnCount={columnCount} columnCount={columnCount}
columnWidth={this.getColumnWidth} columnWidth={this.getColumnWidth}
style={{height: ROW_HEIGHT * rowCount}} ref={this.grid}
style={{height: this.calculateTotalHeight()}}
/> />
</FancyScrollbar> </FancyScrollbar>
)} )}
@ -113,6 +127,31 @@ class LogsTable extends Component<Props, State> {
this.handleScroll(target) this.handleScroll(target)
} }
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
}
private calculateTotalHeight = (): number => {
return _.reduce(
this.props.data.values,
(acc, __, index) => {
return acc + this.calculateMessageHeight(index)
},
0
)
}
private calculateRowHeight = (d: {index: number}): number => {
return this.calculateMessageHeight(d.index)
}
private handleScroll = scrollInfo => { private handleScroll = scrollInfo => {
const {scrollLeft, scrollTop} = scrollInfo const {scrollLeft, scrollTop} = scrollInfo
@ -147,7 +186,7 @@ class LogsTable extends Component<Props, State> {
switch (column) { switch (column) {
case 'message': case 'message':
return 1200 return ROW_CHAR_LIMIT * CHAR_WIDTH
case 'timestamp': case 'timestamp':
return 160 return 160
case 'procid': case 'procid':
@ -219,24 +258,39 @@ class LogsTable extends Component<Props, State> {
<div <div
className={`logs-viewer--dot ${value}-severity`} className={`logs-viewer--dot ${value}-severity`}
title={this.severityLevel(value)} title={this.severityLevel(value)}
onMouseOver={this.handleMouseEnter}
data-index={rowIndex}
/> />
) )
break break
default:
value = (
<div
className="logs-viewer--clickable"
title={`Filter by "${value}"`}
onMouseOver={this.handleMouseEnter}
data-index={rowIndex}
>
{value}
</div>
)
} }
const highlightRow = rowIndex === this.state.currentRow && columnIndex >= 0 const highlightRow = rowIndex === this.state.currentRow && columnIndex >= 0
if (this.isClickable(column)) {
return (
<div
className={classnames('logs-viewer--cell', {
highlight: highlightRow,
})}
title={`Filter by "${value}"`}
style={{...style, padding: '5px'}}
key={key}
data-index={rowIndex}
onMouseOver={this.handleMouseEnter}
>
<div
data-tag-key={column}
data-tag-value={value}
onClick={this.handleTagClick}
className="logs-viewer--clickable"
>
{value}
</div>
</div>
)
}
return ( return (
<div <div
className={classnames('logs-viewer--cell', {highlight: highlightRow})} className={classnames('logs-viewer--cell', {highlight: highlightRow})}
@ -255,9 +309,27 @@ class LogsTable extends Component<Props, State> {
this.setState({currentRow: +target.dataset.index}) this.setState({currentRow: +target.dataset.index})
} }
private handleMouseLeave = (): void => { private handleTagClick = (e: MouseEvent<HTMLElement>) => {
const {onTagSelection} = this.props
const target = e.target as HTMLElement
const selection = {
tag: target.dataset.tagValue,
key: target.dataset.tagKey,
}
onTagSelection(selection)
}
private handleMouseOut = () => {
this.setState({currentRow: -1}) this.setState({currentRow: -1})
} }
private isClickable(key): boolean {
return _.includes(
['appname', 'facility', 'host', 'hostname', 'severity_1'],
key
)
}
} }
export default LogsTable export default LogsTable

View File

@ -1,4 +1,5 @@
import React, {PureComponent} from 'react' import React, {PureComponent} from 'react'
import uuid from 'uuid'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import { import {
getSourceAndPopulateNamespacesAsync, getSourceAndPopulateNamespacesAsync,
@ -7,6 +8,9 @@ import {
executeQueriesAsync, executeQueriesAsync,
changeZoomAsync, changeZoomAsync,
setSearchTermAsync, setSearchTermAsync,
addFilter,
removeFilter,
changeFilter,
} from 'src/logs/actions' } from 'src/logs/actions'
import {getSourcesAsync} from 'src/shared/actions/sources' import {getSourcesAsync} from 'src/shared/actions/sources'
import LogViewerHeader from 'src/logs/components/LogViewerHeader' import LogViewerHeader from 'src/logs/components/LogViewerHeader'
@ -18,13 +22,7 @@ import LogsTable from 'src/logs/components/LogsTable'
import {getDeep} from 'src/utils/wrappers' import {getDeep} from 'src/utils/wrappers'
import {Source, Namespace, TimeRange} from 'src/types' import {Source, Namespace, TimeRange} from 'src/types'
import {Filter} from 'src/types/logs'
export interface Filter {
id: string
key: string
value: string
operator: string
}
interface Props { interface Props {
sources: Source[] sources: Source[]
@ -38,6 +36,9 @@ interface Props {
changeZoomAsync: (timeRange: TimeRange) => void changeZoomAsync: (timeRange: TimeRange) => void
executeQueriesAsync: () => void executeQueriesAsync: () => void
setSearchTermAsync: (searchTerm: string) => void setSearchTermAsync: (searchTerm: string) => void
addFilter: (filter: Filter) => void
removeFilter: (id: string) => void
changeFilter: (id: string, operator: string, value: string) => void
timeRange: TimeRange timeRange: TimeRange
histogramData: object[] histogramData: object[]
tableData: { tableData: {
@ -45,23 +46,14 @@ interface Props {
values: string[] values: string[]
} }
searchTerm: string searchTerm: string
filters: Filter[]
} }
interface State { interface State {
searchString: string searchString: string
filters: Filter[]
liveUpdating: boolean liveUpdating: boolean
} }
const DUMMY_FILTERS = [
{
id: '0',
key: 'host',
value: 'prod1-rsavage.local',
operator: '==',
},
]
class LogsPage extends PureComponent<Props, State> { class LogsPage extends PureComponent<Props, State> {
private interval: NodeJS.Timer private interval: NodeJS.Timer
@ -70,7 +62,6 @@ class LogsPage extends PureComponent<Props, State> {
this.state = { this.state = {
searchString: '', searchString: '',
filters: DUMMY_FILTERS,
liveUpdating: false, liveUpdating: false,
} }
} }
@ -96,8 +87,8 @@ class LogsPage extends PureComponent<Props, State> {
} }
public render() { public render() {
const {filters, liveUpdating} = this.state const {liveUpdating} = this.state
const {searchTerm} = this.props const {searchTerm, filters} = this.props
const count = getDeep(this.props, 'tableData.values.length', 0) const count = getDeep(this.props, 'tableData.values.length', 0)
@ -112,14 +103,16 @@ class LogsPage extends PureComponent<Props, State> {
/> />
<FilterBar <FilterBar
numResults={count} numResults={count}
filters={filters} filters={filters || []}
onUpdateFilters={this.handleUpdateFilters} onDelete={this.handleFilterDelete}
onFilterChange={this.handleFilterChange}
/> />
<LogsTable <LogsTable
data={this.props.tableData} data={this.props.tableData}
onScrollVertical={this.handleVerticalScroll} onScrollVertical={this.handleVerticalScroll}
onScrolledToTop={this.handleScrollToTop} onScrolledToTop={this.handleScrollToTop}
isScrolledToTop={liveUpdating} isScrolledToTop={liveUpdating}
onTagSelection={this.handleTagSelection}
/> />
</div> </div>
</div> </div>
@ -148,6 +141,17 @@ class LogsPage extends PureComponent<Props, State> {
} }
} }
private handleTagSelection = (selection: {tag: string; key: string}) => {
// Do something with the tag
this.props.addFilter({
id: uuid.v4(),
key: selection.key,
value: selection.tag,
operator: '==',
})
this.props.executeQueriesAsync()
}
private handleInterval = () => { private handleInterval = () => {
this.props.executeQueriesAsync() this.props.executeQueriesAsync()
} }
@ -205,8 +209,18 @@ class LogsPage extends PureComponent<Props, State> {
this.props.setSearchTermAsync(value) this.props.setSearchTermAsync(value)
} }
private handleUpdateFilters = (filters: Filter[]): void => { private handleFilterDelete = (id: string): void => {
this.setState({filters}) this.props.removeFilter(id)
this.props.executeQueriesAsync()
}
private handleFilterChange = (
id: string,
operator: string,
value: string
) => {
this.props.changeFilter(id, operator, value)
this.props.executeQueriesAsync()
} }
private handleChooseTimerange = (timeRange: TimeRange) => { private handleChooseTimerange = (timeRange: TimeRange) => {
@ -239,6 +253,7 @@ const mapStateToProps = ({
histogramData, histogramData,
tableData, tableData,
searchTerm, searchTerm,
filters,
}, },
}) => ({ }) => ({
sources, sources,
@ -249,6 +264,7 @@ const mapStateToProps = ({
histogramData, histogramData,
tableData, tableData,
searchTerm, searchTerm,
filters,
}) })
const mapDispatchToProps = { const mapDispatchToProps = {
@ -259,6 +275,9 @@ const mapDispatchToProps = {
executeQueriesAsync, executeQueriesAsync,
changeZoomAsync, changeZoomAsync,
setSearchTermAsync, setSearchTermAsync,
addFilter,
removeFilter,
changeFilter,
} }
export default connect(mapStateToProps, mapDispatchToProps)(LogsPage) export default connect(mapStateToProps, mapDispatchToProps)(LogsPage)

View File

@ -1,5 +1,12 @@
import {ActionTypes, Action} from 'src/logs/actions' import _ from 'lodash'
import {LogsState} from 'src/types/localStorage' import {
ActionTypes,
Action,
RemoveFilterAction,
AddFilterAction,
ChangeFilterAction,
} from 'src/logs/actions'
import {LogsState} from 'src/types/logs'
const defaultState: LogsState = { const defaultState: LogsState = {
currentSource: null, currentSource: null,
@ -11,6 +18,42 @@ const defaultState: LogsState = {
tableData: [], tableData: [],
histogramData: [], histogramData: [],
searchTerm: null, searchTerm: null,
filters: [],
}
const removeFilter = (
state: LogsState,
action: RemoveFilterAction
): LogsState => {
const {id} = action.payload
const filters = _.filter(
_.get(state, 'filters', []),
filter => filter.id !== id
)
return {...state, filters}
}
const addFilter = (state: LogsState, action: AddFilterAction): LogsState => {
const {filter} = action.payload
return {...state, filters: [..._.get(state, 'filters', []), filter]}
}
const changeFilter = (
state: LogsState,
action: ChangeFilterAction
): LogsState => {
const {id, operator, value} = action.payload
const mappedFilters = _.map(_.get(state, 'filters', []), f => {
if (f.id === id) {
return {...f, operator, value}
}
return f
})
return {...state, filters: mappedFilters}
} }
export default (state: LogsState = defaultState, action: Action) => { export default (state: LogsState = defaultState, action: Action) => {
@ -37,6 +80,12 @@ export default (state: LogsState = defaultState, action: Action) => {
case ActionTypes.SetSearchTerm: case ActionTypes.SetSearchTerm:
const {searchTerm} = action.payload const {searchTerm} = action.payload
return {...state, searchTerm} return {...state, searchTerm}
case ActionTypes.AddFilter:
return addFilter(state, action)
case ActionTypes.RemoveFilter:
return removeFilter(state, action)
case ActionTypes.ChangeFilter:
return changeFilter(state, action)
default: default:
return state return state
} }

View File

@ -1,6 +1,7 @@
import _ from 'lodash' import _ from 'lodash'
import moment from 'moment' import moment from 'moment'
import uuid from 'uuid' import uuid from 'uuid'
import {Filter} from 'src/types/logs'
import {TimeRange, Namespace, QueryConfig} from 'src/types' import {TimeRange, Namespace, QueryConfig} from 'src/types'
import {NULL_STRING} from 'src/shared/constants/queryFillOptions' import {NULL_STRING} from 'src/shared/constants/queryFillOptions'
import { import {
@ -39,6 +40,11 @@ const tableFields = [
type: 'field', type: 'field',
value: 'timestamp', value: 'timestamp',
}, },
{
alias: 'message',
type: 'field',
value: 'message',
},
{ {
alias: 'severity_text', alias: 'severity_text',
type: 'field', type: 'field',
@ -64,11 +70,6 @@ const tableFields = [
type: 'field', type: 'field',
value: 'host', value: 'host',
}, },
{
alias: 'message',
type: 'field',
value: 'message',
},
] ]
const defaultQueryConfig = { const defaultQueryConfig = {
@ -80,9 +81,46 @@ const defaultQueryConfig = {
tags: {}, tags: {},
} }
const keyMapping = (key: string): string => {
switch (key) {
case 'severity_1':
return 'severity'
default:
return key
}
}
const operatorMapping = (operator: string): string => {
switch (operator) {
case '==':
return '='
default:
return operator
}
}
const valueMapping = (operator: string, value): string => {
if (operator === '=~') {
return `${new RegExp(value)}`
} else {
return `'${value}'`
}
}
export const filtersClause = (filters: Filter[]): string => {
return _.map(
filters,
(filter: Filter) =>
`"${keyMapping(filter.key)}" ${operatorMapping(
filter.operator
)} ${valueMapping(filter.operator, filter.value)}`
).join(' AND ')
}
export function buildLogQuery( export function buildLogQuery(
timeRange: TimeRange, timeRange: TimeRange,
config: QueryConfig, config: QueryConfig,
filters: Filter[],
searchTerm: string | null = null searchTerm: string | null = null
): string { ): string {
const {groupBy, fill = NULL_STRING, tags, areTagsAccepted} = config const {groupBy, fill = NULL_STRING, tags, areTagsAccepted} = config
@ -96,6 +134,10 @@ export function buildLogQuery(
condition = `${condition} AND message =~ ${new RegExp(searchTerm)}` condition = `${condition} AND message =~ ${new RegExp(searchTerm)}`
} }
if (!_.isEmpty(filters)) {
condition = `${condition} AND ${filtersClause(filters)}`
}
return `${select}${condition}${dimensions}${fillClause}` return `${select}${condition}${dimensions}${fillClause}`
} }

View File

@ -1,16 +1,5 @@
import {QueryConfig, TimeRange, Namespace, Source} from 'src/types' import {QueryConfig, TimeRange} from 'src/types'
import {LogsState} from 'src/types/logs'
export interface LogsState {
currentSource: Source | null
currentNamespaces: Namespace[]
currentNamespace: Namespace | null
timeRange: TimeRange
histogramQueryConfig: QueryConfig | null
histogramData: object[]
tableQueryConfig: QueryConfig | null
tableData: object[]
searchTerm: string | null
}
export interface LocalStorage { export interface LocalStorage {
VERSION: VERSION VERSION: VERSION

21
ui/src/types/logs.ts Normal file
View File

@ -0,0 +1,21 @@
import {QueryConfig, TimeRange, Namespace, Source} from 'src/types'
export interface Filter {
id: string
key: string
value: string
operator: string
}
export interface LogsState {
currentSource: Source | null
currentNamespaces: Namespace[]
currentNamespace: Namespace | null
timeRange: TimeRange
histogramQueryConfig: QueryConfig | null
histogramData: object[]
tableQueryConfig: QueryConfig | null
tableData: object[]
searchTerm: string | null
filters: Filter[]
}