commit
8ca0a6f1e4
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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[]
|
||||||
|
}
|
Loading…
Reference in New Issue