Connect UI and Server for Log Viewer table configs

pull/10616/head
Iris Scholten 2018-06-26 14:48:23 -07:00
parent a974a9ea46
commit 7bc2e9c274
16 changed files with 1014 additions and 153 deletions

View File

@ -1,5 +1,7 @@
import moment from 'moment' import moment from 'moment'
import _ from 'lodash' import _ from 'lodash'
import {Dispatch} from 'redux'
import {Source, Namespace, TimeRange, QueryConfig} from 'src/types' import {Source, Namespace, TimeRange, QueryConfig} from 'src/types'
import {getSource} from 'src/shared/apis' import {getSource} from 'src/shared/apis'
import {getDatabasesWithRetentionPolicies} from 'src/shared/apis/databases' import {getDatabasesWithRetentionPolicies} from 'src/shared/apis/databases'
@ -9,16 +11,20 @@ import {
buildLogQuery, buildLogQuery,
parseHistogramQueryResponse, parseHistogramQueryResponse,
} from 'src/logs/utils' } from 'src/logs/utils'
import {logConfigServerToUI, logConfigUIToServer} from 'src/logs/utils/config'
import {getDeep} from 'src/utils/wrappers' import {getDeep} from 'src/utils/wrappers'
import {executeQueryAsync} from 'src/logs/api' import {
import {LogsState, Filter, TableData} from 'src/types/logs' executeQueryAsync,
getLogConfig as getLogConfigAJAX,
updateLogConfig as updateLogConfigAJAX,
} from 'src/logs/api'
import {LogsState, Filter, TableData, LogConfig} from 'src/types/logs'
const defaultTableData: TableData = { const defaultTableData: TableData = {
columns: [ columns: [
'time', 'time',
'severity', 'severity',
'timestamp', 'timestamp',
'severity_1',
'facility', 'facility',
'procid', 'procid',
'application', 'application',
@ -51,6 +57,7 @@ export enum ActionTypes {
IncrementQueryCount = 'LOGS_INCREMENT_QUERY_COUNT', IncrementQueryCount = 'LOGS_INCREMENT_QUERY_COUNT',
DecrementQueryCount = 'LOGS_DECREMENT_QUERY_COUNT', DecrementQueryCount = 'LOGS_DECREMENT_QUERY_COUNT',
ConcatMoreLogs = 'LOGS_CONCAT_MORE_LOGS', ConcatMoreLogs = 'LOGS_CONCAT_MORE_LOGS',
SetConfig = 'SET_CONFIG',
} }
export interface ConcatMoreLogsAction { export interface ConcatMoreLogsAction {
@ -160,6 +167,13 @@ interface ChangeZoomAction {
} }
} }
export interface SetConfigsAction {
type: ActionTypes.SetConfig
payload: {
logConfig: LogConfig
}
}
export type Action = export type Action =
| SetSourceAction | SetSourceAction
| SetNamespacesAction | SetNamespacesAction
@ -177,6 +191,7 @@ export type Action =
| DecrementQueryCountAction | DecrementQueryCountAction
| IncrementQueryCountAction | IncrementQueryCountAction
| ConcatMoreLogsAction | ConcatMoreLogsAction
| SetConfigsAction
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)
@ -485,3 +500,37 @@ export const changeZoomAsync = (timeRange: TimeRange) => async (
await dispatch(setTimeRangeAsync(timeRange)) await dispatch(setTimeRangeAsync(timeRange))
} }
} }
export const getLogConfigAsync = (url: string) => async (
dispatch: Dispatch<SetConfigsAction>
): Promise<void> => {
url = url
try {
const {data} = await getLogConfigAJAX(url) // TODO: uncomment and replace following line when backend is ready
const logConfig = logConfigServerToUI(data)
dispatch(setConfig(logConfig))
} catch (error) {
console.error(error)
}
}
export const updateLogConfigAsync = (url: string, config: LogConfig) => async (
dispatch: Dispatch<SetConfigsAction>
): Promise<void> => {
try {
const configForServer = logConfigUIToServer(config)
await updateLogConfigAJAX(url, configForServer) // TODO: uncomment when backend is ready
dispatch(setConfig(config))
} catch (error) {
console.error(error)
}
}
export const setConfig = (logConfig: LogConfig): SetConfigsAction => {
return {
type: ActionTypes.SetConfig,
payload: {
logConfig,
},
}
}

View File

@ -1,6 +1,8 @@
import {proxy} from 'src/utils/queryUrlGenerator' import {proxy} from 'src/utils/queryUrlGenerator'
import AJAX from 'src/utils/ajax'
import {Namespace} from 'src/types' import {Namespace} from 'src/types'
import {TimeSeriesResponse} from 'src/types/series' import {TimeSeriesResponse} from 'src/types/series'
import {ServerLogConfig} from 'src/types/logs'
export const executeQueryAsync = async ( export const executeQueryAsync = async (
proxyLink: string, proxyLink: string,
@ -20,3 +22,31 @@ export const executeQueryAsync = async (
throw error throw error
} }
} }
export const getLogConfig = async (url: string) => {
try {
return await AJAX({
method: 'GET',
url,
})
} catch (error) {
console.error(error)
throw error
}
}
export const updateLogConfig = async (
url: string,
logConfig: ServerLogConfig
) => {
try {
return await AJAX({
method: 'PUT',
url,
data: logConfig,
})
} catch (error) {
console.error(error)
throw error
}
}

View File

@ -50,7 +50,7 @@ class LogViewerHeader extends PureComponent<Props> {
} }
private get optionsComponents(): JSX.Element { private get optionsComponents(): JSX.Element {
const {timeRange} = this.props const {timeRange, onShowOptionsOverlay} = this.props
return ( return (
<> <>

View File

@ -66,12 +66,7 @@ class LogsFilter extends PureComponent<Props, State> {
filter: {key, operator, value}, filter: {key, operator, value},
} = this.props } = this.props
let displayKey = key return <span>{`${key} ${operator} ${value}`}</span>
if (key === 'severity_1') {
displayKey = 'severity'
}
return <span>{`${displayKey} ${operator} ${value}`}</span>
} }
private get renderEditor(): JSX.Element { private get renderEditor(): JSX.Element {

View File

@ -22,16 +22,15 @@ import {
} from 'src/logs/utils/table' } from 'src/logs/utils/table'
import timeRanges from 'src/logs/data/timeRanges' import timeRanges from 'src/logs/data/timeRanges'
import {SeverityFormatOptions} from 'src/logs/constants'
import {TimeRange} from 'src/types' import {TimeRange} from 'src/types'
import {TableData, LogsTableColumn, SeverityFormat} from 'src/types/logs'
const ROW_HEIGHT = 26 const ROW_HEIGHT = 26
const CHAR_WIDTH = 9 const CHAR_WIDTH = 9
interface Props { interface Props {
data: { data: TableData
columns: string[]
values: string[]
}
isScrolledToTop: boolean isScrolledToTop: boolean
onScrollVertical: () => void onScrollVertical: () => void
onScrolledToTop: () => void onScrolledToTop: () => void
@ -39,6 +38,8 @@ interface Props {
fetchMore: (queryTimeEnd: string, time: number) => Promise<void> fetchMore: (queryTimeEnd: string, time: number) => Promise<void>
count: number count: number
timeRange: TimeRange timeRange: TimeRange
tableColumns: LogsTableColumn[]
severityFormat: SeverityFormat
} }
interface State { interface State {
@ -69,7 +70,11 @@ class LogsTable extends Component<Props, State> {
scrollTop, scrollTop,
scrollLeft, scrollLeft,
currentRow: -1, currentRow: -1,
currentMessageWidth: getMessageWidth(props.data), currentMessageWidth: getMessageWidth(
props.data,
props.tableColumns,
props.severityFormat
),
} }
} }
@ -121,10 +126,7 @@ class LogsTable extends Component<Props, State> {
} }
public render() { public render() {
const columnCount = Math.max( const columnCount = Math.max(getColumnsFromData(this.props.data).length, 0)
getColumnsFromData(this.props.data).length - 1,
0
)
if (this.isTableEmpty) { if (this.isTableEmpty) {
return this.emptyTable return this.emptyTable
@ -273,21 +275,32 @@ class LogsTable extends Component<Props, State> {
} }
private handleWindowResize = () => { private handleWindowResize = () => {
this.setState({currentMessageWidth: getMessageWidth(this.props.data)}) this.setState({
currentMessageWidth: getMessageWidth(
this.props.data,
this.props.tableColumns,
this.props.severityFormat
),
})
} }
private handleHeaderScroll = ({scrollLeft}): void => private handleHeaderScroll = ({scrollLeft}): void =>
this.setState({scrollLeft}) this.setState({scrollLeft})
private getColumnWidth = ({index}: {index: number}): number => { private getColumnWidth = ({index}: {index: number}): number => {
const column = getColumnFromData(this.props.data, index + 1) const {severityFormat} = this.props
const column = getColumnFromData(this.props.data, index)
const {currentMessageWidth} = this.state const {currentMessageWidth} = this.state
switch (column) { switch (column) {
case 'message': case 'message':
return currentMessageWidth return currentMessageWidth
default: default:
return getColumnWidth(column) let columnKey = column
if (column === 'severity') {
columnKey = `${column}_${severityFormat}`
}
return getColumnWidth(columnKey)
} }
} }
@ -327,33 +340,67 @@ class LogsTable extends Component<Props, State> {
} }
private headerRenderer = ({key, style, columnIndex}) => { private headerRenderer = ({key, style, columnIndex}) => {
const column = getColumnFromData(this.props.data, columnIndex + 1) const column = getColumnFromData(this.props.data, columnIndex)
const classes = 'logs-viewer--cell logs-viewer--cell-header' const classes = 'logs-viewer--cell logs-viewer--cell-header'
let columnKey: string = column
if (column === 'severity') {
columnKey = this.getSeverityColumn(column)
}
return ( return (
<div className={classes} style={style} key={key}> <div className={classes} style={style} key={key}>
{header(column)} {header(columnKey, this.props.tableColumns)}
</div> </div>
) )
} }
private getSeverityColumn(column: string): string {
const {severityFormat} = this.props
if (severityFormat === SeverityFormatOptions.dot) {
return SeverityFormatOptions.dot
}
return column
}
private getSeverityDotText(text: string): JSX.Element {
const {severityFormat} = this.props
if (severityFormat === SeverityFormatOptions.dotText) {
return <span style={{padding: '5px'}}>{text}</span>
}
}
private cellRenderer = ({key, style, rowIndex, columnIndex}) => { private cellRenderer = ({key, style, rowIndex, columnIndex}) => {
const column = getColumnFromData(this.props.data, columnIndex + 1) const {severityFormat} = this.props
const value = getValueFromData(this.props.data, rowIndex, columnIndex + 1)
const column = getColumnFromData(this.props.data, columnIndex)
const value = getValueFromData(this.props.data, rowIndex, columnIndex)
let formattedValue: string | JSX.Element let formattedValue: string | JSX.Element
if (column === 'severity') { const isDotNeeded =
severityFormat === SeverityFormatOptions.dot ||
severityFormat === SeverityFormatOptions.dotText
let title: string
if (column === 'severity' && isDotNeeded) {
title = value
formattedValue = ( formattedValue = (
<div <>
className={`logs-viewer--dot ${value}-severity`} <div
title={value} className={`logs-viewer--dot ${value}-severity`}
onMouseOver={this.handleMouseEnter} title={value}
data-index={rowIndex} onMouseOver={this.handleMouseEnter}
style={this.severityDotStyle(value)} data-index={rowIndex}
/> style={this.severityDotStyle(value)}
/>
{this.getSeverityDotText(value)}
</>
) )
} else { } else {
formattedValue = formatColumnValue(column, value, this.rowCharLimit) formattedValue = formatColumnValue(column, value, this.rowCharLimit)
title = formattedValue
} }
const highlightRow = rowIndex === this.state.currentRow const highlightRow = rowIndex === this.state.currentRow
@ -364,7 +411,7 @@ class LogsTable extends Component<Props, State> {
className={classnames('logs-viewer--cell', { className={classnames('logs-viewer--cell', {
highlight: highlightRow, highlight: highlightRow,
})} })}
title={`Filter by '${formattedValue}'`} title={`Filter by '${title}'`}
style={{...style, padding: '5px'}} style={{...style, padding: '5px'}}
key={key} key={key}
data-index={rowIndex} data-index={rowIndex}
@ -418,9 +465,10 @@ class LogsTable extends Component<Props, State> {
private handleTagClick = (e: MouseEvent<HTMLElement>) => { private handleTagClick = (e: MouseEvent<HTMLElement>) => {
const {onTagSelection} = this.props const {onTagSelection} = this.props
const target = e.target as HTMLElement const target = e.target as HTMLElement
const selection = { const selection = {
tag: target.dataset.tagValue, tag: target.dataset.tagValue || target.parentElement.dataset.tagValue,
key: target.dataset.tagKey, key: target.dataset.tagKey || target.parentElement.dataset.tagKey,
} }
onTagSelection(selection) onTagSelection(selection)

View File

@ -1,9 +1,9 @@
import React, {Component} from 'react' import React, {Component} from 'react'
import _ from 'lodash' import _ from 'lodash'
import Container from 'src/shared/components/overlay/OverlayContainer' import Container from 'src/reusable_ui/components/overlays/OverlayContainer'
import Heading from 'src/shared/components/overlay/OverlayHeading' import Heading from 'src/reusable_ui/components/overlays/OverlayHeading'
import Body from 'src/shared/components/overlay/OverlayBody' import Body from 'src/reusable_ui/components/overlays/OverlayBody'
import SeverityOptions from 'src/logs/components/SeverityOptions' import SeverityOptions from 'src/logs/components/SeverityOptions'
import ColumnsOptions from 'src/logs/components/ColumnsOptions' import ColumnsOptions from 'src/logs/components/ColumnsOptions'
import { import {
@ -109,7 +109,7 @@ class OptionsOverlay extends Component<Props, State> {
return true return true
} }
private handleSave = () => { private handleSave = async () => {
const { const {
onUpdateSeverityLevels, onUpdateSeverityLevels,
onDismissOverlay, onDismissOverlay,
@ -118,9 +118,9 @@ class OptionsOverlay extends Component<Props, State> {
} = this.props } = this.props
const {workingSeverityLevels, workingFormat, workingColumns} = this.state const {workingSeverityLevels, workingFormat, workingColumns} = this.state
onUpdateSeverityFormat(workingFormat) await onUpdateSeverityFormat(workingFormat)
onUpdateSeverityLevels(workingSeverityLevels) await onUpdateSeverityLevels(workingSeverityLevels)
onUpdateColumns(workingColumns) await onUpdateColumns(workingColumns)
onDismissOverlay() onDismissOverlay()
} }

View File

@ -1,4 +1,6 @@
import React, {SFC} from 'react' import React, {SFC} from 'react'
import {SeverityFormatOptions} from 'src/logs/constants'
import {SeverityFormat} from 'src/types/logs' import {SeverityFormat} from 'src/types/logs'
interface Props { interface Props {
@ -18,18 +20,21 @@ const SeverityFormat: SFC<Props> = ({format, onChangeFormat}) => (
<div className="graph-options-group"> <div className="graph-options-group">
<label className="form-label">Severity Format</label> <label className="form-label">Severity Format</label>
<ul className="nav nav-tablist nav-tablist-sm stretch"> <ul className="nav nav-tablist nav-tablist-sm stretch">
<li onClick={onChangeFormat('dot')} className={className('dot', format)}> <li
onClick={onChangeFormat(SeverityFormatOptions.dot)}
className={className(SeverityFormatOptions.dot, format)}
>
Dot Dot
</li> </li>
<li <li
onClick={onChangeFormat('dotText')} onClick={onChangeFormat(SeverityFormatOptions.dotText)}
className={className('dotText', format)} className={className(SeverityFormatOptions.dotText, format)}
> >
Dot + Text Dot + Text
</li> </li>
<li <li
onClick={onChangeFormat('text')} onClick={onChangeFormat(SeverityFormatOptions.text)}
className={className('text', format)} className={className(SeverityFormatOptions.text, format)}
> >
Text Text
</li> </li>

View File

@ -123,3 +123,25 @@ export const DEFAULT_SEVERITY_LEVELS = [
override: null, override: null,
}, },
] ]
export enum SeverityFormatOptions {
dot = 'dot',
dotText = 'dotText',
text = 'text',
}
export enum EncodingTypes {
visibility = 'visibility',
display = 'displayName',
label = 'label',
}
export enum EncodingLabelOptions {
text = 'text',
icon = 'icon',
}
export enum EncodingVisibilityOptions {
visible = 'visible',
hidden = 'hidden',
}

View File

@ -15,24 +15,26 @@ import {
removeFilter, removeFilter,
changeFilter, changeFilter,
fetchMoreAsync, fetchMoreAsync,
getLogConfigAsync,
updateLogConfigAsync,
} from 'src/logs/actions' } from 'src/logs/actions'
import {
showOverlay as showOverlayAction,
ShowOverlay,
} from 'src/shared/actions/overlayTechnology'
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'
import HistogramChart from 'src/shared/components/HistogramChart' import HistogramChart from 'src/shared/components/HistogramChart'
import LogsGraphContainer from 'src/logs/components/LogsGraphContainer' import LogsGraphContainer from 'src/logs/components/LogsGraphContainer'
import OptionsOverlay from 'src/logs/components/OptionsOverlay' import OptionsOverlay from 'src/logs/components/OptionsOverlay'
import Graph from 'src/logs/components/LogsGraph'
import SearchBar from 'src/logs/components/LogsSearchBar' import SearchBar from 'src/logs/components/LogsSearchBar'
import FilterBar from 'src/logs/components/LogsFilterBar' import FilterBar from 'src/logs/components/LogsFilterBar'
import LogsTable from 'src/logs/components/LogsTable' import LogsTable from 'src/logs/components/LogsTable'
import {getDeep} from 'src/utils/wrappers' import {getDeep} from 'src/utils/wrappers'
import {colorForSeverity} from 'src/logs/utils/colors' import {colorForSeverity} from 'src/logs/utils/colors'
import {OverlayContext} from 'src/shared/components/OverlayTechnology' import OverlayTechnology from 'src/reusable_ui/components/overlays/OverlayTechnology'
import {
orderTableColumns,
filterTableColumns,
} from 'src/dashboards/utils/tableGraph'
import {SeverityFormatOptions} from 'src/logs/constants'
import {Source, Namespace, TimeRange} from 'src/types' import {Source, Namespace, TimeRange} from 'src/types'
import {HistogramData, TimePeriod} from 'src/types/histogram' import {HistogramData, TimePeriod} from 'src/types/histogram'
@ -41,6 +43,8 @@ import {
SeverityLevel, SeverityLevel,
SeverityFormat, SeverityFormat,
LogsTableColumn, LogsTableColumn,
LogConfig,
TableData,
} from 'src/types/logs' } from 'src/types/logs'
// Mock // Mock
@ -62,21 +66,22 @@ interface Props {
addFilter: (filter: Filter) => void addFilter: (filter: Filter) => void
removeFilter: (id: string) => void removeFilter: (id: string) => void
changeFilter: (id: string, operator: string, value: string) => void changeFilter: (id: string, operator: string, value: string) => void
getConfig: (url: string) => Promise<void>
updateConfig: (url: string, config: LogConfig) => Promise<void>
timeRange: TimeRange timeRange: TimeRange
histogramData: HistogramData histogramData: HistogramData
tableData: { tableData: TableData
columns: string[]
values: string[]
}
searchTerm: string searchTerm: string
filters: Filter[] filters: Filter[]
queryCount: number queryCount: number
showOverlay: ShowOverlay logConfig: LogConfig
logConfigLink: string
} }
interface State { interface State {
searchString: string searchString: string
liveUpdating: boolean liveUpdating: boolean
isOverlayVisible: boolean
} }
class LogsPage extends PureComponent<Props, State> { class LogsPage extends PureComponent<Props, State> {
@ -88,6 +93,7 @@ class LogsPage extends PureComponent<Props, State> {
this.state = { this.state = {
searchString: '', searchString: '',
liveUpdating: false, liveUpdating: false,
isOverlayVisible: false,
} }
} }
@ -99,6 +105,7 @@ class LogsPage extends PureComponent<Props, State> {
public componentDidMount() { public componentDidMount() {
this.props.getSources() this.props.getSources()
this.props.getConfig(this.logConfigLink)
if (this.props.currentNamespace) { if (this.props.currentNamespace) {
this.fetchNewDataset() this.fetchNewDataset()
@ -116,36 +123,65 @@ class LogsPage extends PureComponent<Props, State> {
const {searchTerm, filters, queryCount, timeRange} = this.props const {searchTerm, filters, queryCount, timeRange} = this.props
return ( return (
<div className="page"> <>
{this.header} <div className="page">
<div className="page-contents logs-viewer"> {this.header}
<LogsGraphContainer>{this.chart}</LogsGraphContainer> <div className="page-contents logs-viewer">
<SearchBar <LogsGraphContainer>{this.chart}</LogsGraphContainer>
searchString={searchTerm} <SearchBar
onSearch={this.handleSubmitSearch} searchString={searchTerm}
/> onSearch={this.handleSubmitSearch}
<FilterBar />
numResults={this.histogramTotal} <FilterBar
filters={filters || []} numResults={this.histogramTotal}
onDelete={this.handleFilterDelete} filters={filters || []}
onFilterChange={this.handleFilterChange} onDelete={this.handleFilterDelete}
queryCount={queryCount} onFilterChange={this.handleFilterChange}
/> queryCount={queryCount}
<LogsTable />
count={this.histogramTotal} <LogsTable
data={this.props.tableData} count={this.histogramTotal}
onScrollVertical={this.handleVerticalScroll} data={this.tableData}
onScrolledToTop={this.handleScrollToTop} onScrollVertical={this.handleVerticalScroll}
isScrolledToTop={liveUpdating} onScrolledToTop={this.handleScrollToTop}
onTagSelection={this.handleTagSelection} isScrolledToTop={liveUpdating}
fetchMore={this.props.fetchMoreAsync} onTagSelection={this.handleTagSelection}
timeRange={timeRange} fetchMore={this.props.fetchMoreAsync}
/> timeRange={timeRange}
tableColumns={this.tableColumns}
severityFormat={this.severityFormat}
/>
</div>
</div> </div>
</div> {this.renderImportOverlay()}
</>
) )
} }
private get tableData(): TableData {
const {logConfig, tableData} = this.props
const tableColumns = _.get(logConfig, 'tableColumns', [])
const columns = _.get(tableData, 'columns', [])
const values = _.get(tableData, 'values', [])
const data = [columns, ...values]
const filteredData = filterTableColumns(data, tableColumns)
const orderedData = orderTableColumns(filteredData, tableColumns)
const updatedColumns: string[] = _.get(orderedData, '0', [])
const updatedValues = _.slice(orderedData, 1)
return {columns: updatedColumns, values: updatedValues}
}
private get logConfigLink(): string {
return this.props.logConfigLink
}
private get tableColumns(): LogsTableColumn[] {
const {logConfig} = this.props
return _.get(logConfig, 'tableColumns', [])
}
private get isSpecificTimeRange(): boolean { private get isSpecificTimeRange(): boolean {
return !!getDeep(this.props, 'timeRange.upper', false) return !!getDeep(this.props, 'timeRange.upper', false)
} }
@ -175,7 +211,6 @@ class LogsPage extends PureComponent<Props, State> {
} }
private handleTagSelection = (selection: {tag: string; key: string}) => { private handleTagSelection = (selection: {tag: string; key: string}) => {
// Do something with the tag
this.props.addFilter({ this.props.addFilter({
id: uuid.v4(), id: uuid.v4(),
key: selection.key, key: selection.key,
@ -236,7 +271,7 @@ class LogsPage extends PureComponent<Props, State> {
currentNamespaces={currentNamespaces} currentNamespaces={currentNamespaces}
currentNamespace={currentNamespace} currentNamespace={currentNamespace}
onChangeLiveUpdatingStatus={this.handleChangeLiveUpdatingStatus} onChangeLiveUpdatingStatus={this.handleChangeLiveUpdatingStatus}
onShowOptionsOverlay={this.handleShowOptionsOverlay} onShowOptionsOverlay={this.handleToggleOverlay}
/> />
) )
} }
@ -301,57 +336,72 @@ class LogsPage extends PureComponent<Props, State> {
this.setState({liveUpdating: true}) this.setState({liveUpdating: true})
} }
private handleShowOptionsOverlay = (): void => { private handleToggleOverlay = (): void => {
const {showOverlay} = this.props this.setState({isOverlayVisible: !this.state.isOverlayVisible})
const options = { }
dismissOnClickOutside: false,
dismissOnEscape: false,
}
showOverlay( private renderImportOverlay = (): JSX.Element => {
<OverlayContext.Consumer> const {isOverlayVisible} = this.state
{({onDismissOverlay}) => (
<OptionsOverlay return (
severityLevels={DEFAULT_SEVERITY_LEVELS} // Todo: replace with real <OverlayTechnology visible={isOverlayVisible}>
onUpdateSeverityLevels={this.handleUpdateSeverityLevels} <OptionsOverlay
onDismissOverlay={onDismissOverlay} severityLevels={DEFAULT_SEVERITY_LEVELS} // Todo: replace with real
columns={this.fakeColumns} onUpdateSeverityLevels={this.handleUpdateSeverityLevels}
onUpdateColumns={this.handleUpdateColumns} onDismissOverlay={this.handleToggleOverlay}
onUpdateSeverityFormat={this.handleUpdateSeverityFormat} columns={this.columns}
severityFormat="dotText" // Todo: repleace with real value onUpdateColumns={this.handleUpdateColumns}
/> onUpdateSeverityFormat={this.handleUpdateSeverityFormat}
)} severityFormat={this.severityFormat}
</OverlayContext.Consumer>, />
options </OverlayTechnology>
) )
} }
private handleUpdateSeverityLevels = (levels: SeverityLevel[]) => { private handleUpdateSeverityLevels = (levels: SeverityLevel[]) => {
console.log(levels) // tslint:disable-line
// Todo: Handle saving of these new severity colors here // Todo: Handle saving of these new severity colors here
levels = levels
} }
private handleUpdateSeverityFormat = (format: SeverityFormat) => { private handleUpdateSeverityFormat = async (format: SeverityFormat) => {
console.log(format) // tslint:disable-line const {logConfig} = this.props
// Todo: Handle saving of the new format here await this.props.updateConfig(this.logConfigLink, {
...logConfig,
severityFormat: format,
})
} }
private get fakeColumns(): LogsTableColumn[] { private get columns(): LogsTableColumn[] {
const { const {logConfig} = this.props
tableData: {columns}, const tableColumns = _.get(logConfig, 'tableColumns', [])
} = this.props
return columns.map(c => ({internalName: c, displayName: '', visible: true})) return tableColumns
} }
private handleUpdateColumns = (columns: LogsTableColumn[]) => { private get severityFormat(): SeverityFormat {
console.log(columns) // tslint:disable-line const {logConfig} = this.props
// Todo: Handle saving of column names, ordering, and visibility const severityFormat = _.get(
logConfig,
'severityFormat',
SeverityFormatOptions.dotText
)
return severityFormat
}
private handleUpdateColumns = async (tableColumns: LogsTableColumn[]) => {
const {logConfig} = this.props
await this.props.updateConfig(this.logConfigLink, {
...logConfig,
tableColumns,
})
} }
} }
const mapStateToProps = ({ const mapStateToProps = ({
sources, sources,
links: {
config: {logViewer},
},
logs: { logs: {
currentSource, currentSource,
currentNamespaces, currentNamespaces,
@ -362,6 +412,7 @@ const mapStateToProps = ({
searchTerm, searchTerm,
filters, filters,
queryCount, queryCount,
logConfig,
}, },
}) => ({ }) => ({
sources, sources,
@ -374,12 +425,13 @@ const mapStateToProps = ({
searchTerm, searchTerm,
filters, filters,
queryCount, queryCount,
logConfig,
logConfigLink: logViewer,
}) })
const mapDispatchToProps = { const mapDispatchToProps = {
getSource: getSourceAndPopulateNamespacesAsync, getSource: getSourceAndPopulateNamespacesAsync,
getSources: getSourcesAsync, getSources: getSourcesAsync,
showOverlay: showOverlayAction,
setTimeRangeAsync, setTimeRangeAsync,
setNamespaceAsync, setNamespaceAsync,
executeQueriesAsync, executeQueriesAsync,
@ -389,6 +441,8 @@ const mapDispatchToProps = {
removeFilter, removeFilter,
changeFilter, changeFilter,
fetchMoreAsync, fetchMoreAsync,
getConfig: getLogConfigAsync,
updateConfig: updateLogConfigAsync,
} }
export default connect(mapStateToProps, mapDispatchToProps)(LogsPage) export default connect(mapStateToProps, mapDispatchToProps)(LogsPage)

View File

@ -1,4 +1,5 @@
import _ from 'lodash' import _ from 'lodash'
import { import {
ActionTypes, ActionTypes,
Action, Action,
@ -8,8 +9,10 @@ import {
DecrementQueryCountAction, DecrementQueryCountAction,
IncrementQueryCountAction, IncrementQueryCountAction,
ConcatMoreLogsAction, ConcatMoreLogsAction,
SetConfigsAction,
} from 'src/logs/actions' } from 'src/logs/actions'
import {SeverityFormatOptions} from 'src/logs/constants'
import {LogsState} from 'src/types/logs' import {LogsState} from 'src/types/logs'
const defaultState: LogsState = { const defaultState: LogsState = {
@ -24,6 +27,10 @@ const defaultState: LogsState = {
searchTerm: '', searchTerm: '',
filters: [], filters: [],
queryCount: 0, queryCount: 0,
logConfig: {
tableColumns: [],
severityFormat: SeverityFormatOptions.dotText,
},
} }
const removeFilter = ( const removeFilter = (
@ -95,6 +102,15 @@ const concatMoreLogs = (
} }
} }
export const setConfigs = (state: LogsState, action: SetConfigsAction) => {
const {logConfig} = state
const {
logConfig: {tableColumns, severityFormat},
} = action.payload
const updatedLogConfig = {...logConfig, tableColumns, severityFormat}
return {...state, logConfig: updatedLogConfig}
}
export default (state: LogsState = defaultState, action: Action) => { export default (state: LogsState = defaultState, action: Action) => {
switch (action.type) { switch (action.type) {
case ActionTypes.SetSource: case ActionTypes.SetSource:
@ -130,6 +146,8 @@ export default (state: LogsState = defaultState, action: Action) => {
return decrementQueryCount(state, action) return decrementQueryCount(state, action)
case ActionTypes.ConcatMoreLogs: case ActionTypes.ConcatMoreLogs:
return concatMoreLogs(state, action) return concatMoreLogs(state, action)
case ActionTypes.SetConfig:
return setConfigs(state, action)
default: default:
return state return state
} }

161
ui/src/logs/utils/config.ts Normal file
View File

@ -0,0 +1,161 @@
import _ from 'lodash'
import {
LogConfig,
ServerLogConfig,
ServerColumn,
LogsTableColumn,
ServerEncoding,
SeverityFormat,
} from 'src/types/logs'
import {
SeverityFormatOptions,
EncodingTypes,
EncodingLabelOptions,
EncodingVisibilityOptions,
} from 'src/logs/constants'
export const logConfigServerToUI = (
serverConfig: ServerLogConfig
): LogConfig => {
const columns = _.get(serverConfig, 'columns', [])
if (_.isEmpty(columns)) {
return
}
const sortedColumns = sortColumns(columns)
let severityFormat: SeverityFormatOptions
const convertedColumns = sortedColumns.map(c => {
if (c.name === 'severity') {
severityFormat = getFormatFromColumn(c)
}
return columnServerToUI(c)
})
return {
tableColumns: convertedColumns,
severityFormat,
}
}
export const sortColumns = (columns: ServerColumn[]): ServerColumn[] => {
return _.sortBy(columns, c => c.position)
}
export const columnServerToUI = (column: ServerColumn): LogsTableColumn => {
const internalName = column.name
const encodings: LogsTableColumn = column.encodings.reduce(
(acc, e) => {
if (e.type === EncodingTypes.visibility) {
if (e.value === 'visible') {
acc.visible = true
}
} else if (e.type === EncodingTypes.display) {
acc.displayName = e.value
}
return acc
},
{visible: false, displayName: '', internalName}
)
return {...encodings, internalName}
}
export const getFormatFromColumn = (
column: ServerColumn
): SeverityFormatOptions => {
let hasText = false
let hasIcon = false
column.encodings.forEach(e => {
if (e.type === EncodingTypes.label) {
if (e.value === EncodingLabelOptions.icon) {
hasIcon = true
}
if (e.value === EncodingLabelOptions.text) {
hasText = true
}
}
})
if (hasText && hasIcon) {
return SeverityFormatOptions.dotText
} else if (hasText) {
return SeverityFormatOptions.text
} else {
return SeverityFormatOptions.dot
}
}
export const logConfigUIToServer = (config: LogConfig): ServerLogConfig => {
const tableColumns = _.get(config, 'tableColumns')
const severityFormat = _.get(config, 'severityFormat')
if (_.isEmpty(tableColumns)) {
return {columns: []}
}
const columns = tableColumns.map((c, i) => {
const encodings = getFullEncodings(c, severityFormat)
const name = c.internalName
const position = i
return {name, position, encodings}
})
return {columns}
}
export const getDisplayAndVisibleEncodings = (
tableColumn: LogsTableColumn
): ServerEncoding[] => {
const encodings: ServerEncoding[] = []
if (tableColumn.visible) {
encodings.push({
type: EncodingTypes.visibility,
value: EncodingVisibilityOptions.visible,
})
} else {
encodings.push({
type: EncodingTypes.visibility,
value: EncodingVisibilityOptions.hidden,
})
}
if (!_.isEmpty(tableColumn.displayName)) {
encodings.push({
type: EncodingTypes.display,
value: tableColumn.displayName,
})
}
return encodings
}
export const getLabelEncodings = (format: SeverityFormat): ServerEncoding[] => {
switch (format) {
case SeverityFormatOptions.dot:
return [{type: 'label', value: EncodingLabelOptions.icon}]
case SeverityFormatOptions.text:
return [{type: 'label', value: EncodingLabelOptions.text}]
case SeverityFormatOptions.dotText:
return [
{type: 'label', value: EncodingLabelOptions.icon},
{type: 'label', value: EncodingLabelOptions.text},
]
}
return null
}
export const getFullEncodings = (
tableColumn: LogsTableColumn,
format: SeverityFormat
) => {
let encodings = getDisplayAndVisibleEncodings(tableColumn)
if (tableColumn.internalName === 'severity') {
encodings = [...encodings, ...getLabelEncodings(format)]
}
return encodings
}

View File

@ -48,11 +48,6 @@ const tableFields = [
type: 'field', type: 'field',
value: 'message', value: 'message',
}, },
{
alias: 'severity_text',
type: 'field',
value: 'severity',
},
{ {
alias: 'facility', alias: 'facility',
type: 'field', type: 'field',

View File

@ -1,7 +1,8 @@
import _ from 'lodash' import _ from 'lodash'
import moment from 'moment' import moment from 'moment'
import {getDeep} from 'src/utils/wrappers' import {getDeep} from 'src/utils/wrappers'
import {TableData} from 'src/types/logs' import {TableData, LogsTableColumn, SeverityFormat} from 'src/types/logs'
import {SeverityFormatOptions} from 'src/logs/constants'
const CHAR_WIDTH = 9 const CHAR_WIDTH = 9
@ -21,7 +22,7 @@ export const getColumnFromData = (data: TableData, index: number): string =>
getDeep(data, `columns.${index}`, '') getDeep(data, `columns.${index}`, '')
export const isClickable = (column: string): boolean => export const isClickable = (column: string): boolean =>
_.includes(['appname', 'facility', 'host', 'hostname', 'severity_1'], column) _.includes(['appname', 'facility', 'host', 'hostname', 'severity'], column)
export const formatColumnValue = ( export const formatColumnValue = (
column: string, column: string,
@ -31,6 +32,8 @@ export const formatColumnValue = (
switch (column) { switch (column) {
case 'timestamp': case 'timestamp':
return moment(+value / 1000000).format('YYYY/MM/DD HH:mm:ss') return moment(+value / 1000000).format('YYYY/MM/DD HH:mm:ss')
case 'time':
return moment(+value / 1000000).format('YYYY/MM/DD HH:mm:ss')
case 'message': case 'message':
value = (value || 'No Message Provided').replace('\\n', '') value = (value || 'No Message Provided').replace('\\n', '')
if (value.indexOf(' ') > charLimit - 5) { if (value.indexOf(' ') > charLimit - 5) {
@ -40,19 +43,16 @@ export const formatColumnValue = (
} }
return value return value
} }
export const header = (key: string): string => { export const header = (
return getDeep<string>( key: string,
{ headerOptions: LogsTableColumn[]
timestamp: 'Timestamp', ): string => {
procid: 'Proc ID', if (key === SeverityFormatOptions.dot) {
message: 'Message', return ''
appname: 'Application', }
severity: '',
severity_1: 'Severity', const headerOption = _.find(headerOptions, h => h.internalName === key)
}, return _.get(headerOption, 'displayName') || _.capitalize(key)
key,
_.capitalize(key)
)
} }
export const getColumnWidth = (column: string): number => { export const getColumnWidth = (column: string): number => {
@ -61,8 +61,9 @@ export const getColumnWidth = (column: string): number => {
timestamp: 160, timestamp: 160,
procid: 80, procid: 80,
facility: 120, facility: 120,
severity: 22, severity_dot: 25,
severity_1: 120, severity_text: 120,
severity_dotText: 120,
host: 300, host: 300,
}, },
column, column,
@ -70,14 +71,25 @@ export const getColumnWidth = (column: string): number => {
) )
} }
export const getMessageWidth = (data: TableData): number => { export const getMessageWidth = (
data: TableData,
tableColumns: LogsTableColumn[],
severityFormat: SeverityFormat
): number => {
const columns = getColumnsFromData(data) const columns = getColumnsFromData(data)
const otherWidth = columns.reduce((acc, col) => { const otherWidth = columns.reduce((acc, col) => {
if (col === 'message' || col === 'time') { const colConfig = tableColumns.find(c => c.internalName === col)
const isColVisible = colConfig && colConfig.visible
if (col === 'message' || !isColVisible) {
return acc return acc
} }
return acc + getColumnWidth(col) let columnName = col
if (col === 'severity') {
columnName = `${col}_${severityFormat}`
}
return acc + getColumnWidth(columnName)
}, 0) }, 0)
const calculatedWidth = Math.max( const calculatedWidth = Math.max(

View File

@ -1,4 +1,7 @@
import {SeverityFormatOptions} from 'src/logs/constants'
import {QueryConfig, TimeRange, Namespace, Source} from 'src/types' import {QueryConfig, TimeRange, Namespace, Source} from 'src/types'
import {FieldOption} from 'src/types/dashboards'
import {TimeSeriesValue} from 'src/types/series'
export interface Filter { export interface Filter {
id: string id: string
@ -9,7 +12,7 @@ export interface Filter {
export interface TableData { export interface TableData {
columns: string[] columns: string[]
values: string[] values: TimeSeriesValue[][]
} }
export interface LogsState { export interface LogsState {
@ -24,6 +27,12 @@ export interface LogsState {
searchTerm: string | null searchTerm: string | null
filters: Filter[] filters: Filter[]
queryCount: number queryCount: number
logConfig: LogConfig
}
export interface LogConfig {
tableColumns: LogsTableColumn[]
severityFormat: SeverityFormat
} }
export interface SeverityLevel { export interface SeverityLevel {
@ -37,10 +46,21 @@ export interface SeverityColor {
name: string name: string
} }
export type SeverityFormat = 'dot' | 'dotText' | 'text' export type SeverityFormat = SeverityFormatOptions
export interface LogsTableColumn { export type LogsTableColumn = FieldOption
internalName: string
displayName: string export interface ServerLogConfig {
visible: boolean columns: ServerColumn[]
}
export interface ServerColumn {
name: string
position: number
encodings: ServerEncoding[]
}
export interface ServerEncoding {
type: string
value: string
} }

View File

@ -18,6 +18,7 @@ import {
CellType, CellType,
} from 'src/types/dashboards' } from 'src/types/dashboards'
import {LineColor, ColorNumber} from 'src/types/colors' import {LineColor, ColorNumber} from 'src/types/colors'
import {ServerLogConfig, ServerColumn} from 'src/types/logs'
export const sourceLinks: SourceLinks = { export const sourceLinks: SourceLinks = {
services: '/chronograf/v1/sources/4', services: '/chronograf/v1/sources/4',
@ -381,3 +382,106 @@ export const gaugeColors: ColorNumber[] = [
value: 100, value: 100,
}, },
] ]
export const serverLogColumns: ServerColumn[] = [
{
name: 'severity',
position: 1,
encodings: [
{
type: 'visibility',
value: 'visible',
},
{
type: 'label',
value: 'icon',
},
{
type: 'label',
value: 'text',
},
],
},
{
name: 'timestamp',
position: 2,
encodings: [
{
type: 'visibility',
value: 'visible',
},
],
},
{
name: 'message',
position: 3,
encodings: [
{
type: 'visibility',
value: 'visible',
},
],
},
{
name: 'facility',
position: 4,
encodings: [
{
type: 'visibility',
value: 'visible',
},
],
},
{
name: 'time',
position: 0,
encodings: [
{
type: 'visibility',
value: 'hidden',
},
],
},
{
name: 'procid',
position: 5,
encodings: [
{
type: 'visibility',
value: 'visible',
},
{
type: 'displayName',
value: 'Proc ID',
},
],
},
{
name: 'host',
position: 7,
encodings: [
{
type: 'visibility',
value: 'visible',
},
],
},
{
name: 'appname',
position: 6,
encodings: [
{
type: 'visibility',
value: 'visible',
},
{
type: 'displayName',
value: 'Application',
},
],
},
]
export const serverLogConfig: ServerLogConfig = {
columns: serverLogColumns,
}

View File

@ -0,0 +1,348 @@
import {
logConfigServerToUI,
logConfigUIToServer,
columnServerToUI,
getFormatFromColumn,
sortColumns,
getDisplayAndVisibleEncodings,
getLabelEncodings,
getFullEncodings,
} from 'src/logs/utils/config'
import {serverLogConfig, serverLogColumns} from 'test/fixtures'
import {SeverityFormatOptions} from 'src/logs/constants'
const sortedServerColumns = () => {
return [
{
name: 'time',
position: 0,
encodings: [
{
type: 'visibility',
value: 'hidden',
},
],
},
{
name: 'severity',
position: 1,
encodings: [
{
type: 'visibility',
value: 'visible',
},
{
type: 'label',
value: 'icon',
},
{
type: 'label',
value: 'text',
},
],
},
{
name: 'timestamp',
position: 2,
encodings: [
{
type: 'visibility',
value: 'visible',
},
],
},
{
name: 'message',
position: 3,
encodings: [
{
type: 'visibility',
value: 'visible',
},
],
},
{
name: 'facility',
position: 4,
encodings: [
{
type: 'visibility',
value: 'visible',
},
],
},
{
name: 'procid',
position: 5,
encodings: [
{
type: 'visibility',
value: 'visible',
},
{
type: 'displayName',
value: 'Proc ID',
},
],
},
{
name: 'appname',
position: 6,
encodings: [
{
type: 'visibility',
value: 'visible',
},
{
type: 'displayName',
value: 'Application',
},
],
},
{
name: 'host',
position: 7,
encodings: [
{
type: 'visibility',
value: 'visible',
},
],
},
]
}
describe('Logs.Config', () => {
describe('logConfigServerToUI', () => {
it('Converts columns to tableColumns', () => {
const serverColumn = {
name: 'appname',
position: 6,
encodings: [
{
type: 'visibility',
value: 'visible',
},
{
type: 'displayName',
value: 'Application',
},
],
}
const serverColumn2 = {
name: 'procid',
position: 0,
encodings: [
{
type: 'visibility',
value: 'hidden',
},
],
}
const uiColumn = columnServerToUI(serverColumn)
const uiColumn2 = columnServerToUI(serverColumn2)
const expectedColumn = {
internalName: 'appname',
displayName: 'Application',
visible: true,
}
const expectedColumn2 = {
internalName: 'procid',
displayName: '',
visible: false,
}
expect(uiColumn).toEqual(expectedColumn)
expect(uiColumn2).toEqual(expectedColumn2)
})
it('Gets severity format from columns', () => {
const serverColumnDotText = {
name: 'severity',
position: 2,
encodings: [
{
type: 'label',
value: 'icon',
},
{
type: 'label',
value: 'text',
},
],
}
const serverColumnDot = {
name: 'severity',
position: 2,
encodings: [
{
type: 'label',
value: 'icon',
},
],
}
const severityFormatDotText = getFormatFromColumn(serverColumnDotText)
const severityFormatDot = getFormatFromColumn(serverColumnDot)
expect(severityFormatDotText).toBe(SeverityFormatOptions.dotText)
expect(severityFormatDot).toBe(SeverityFormatOptions.dot)
})
it('Sorts the columns by position', () => {
const sortedColumns = sortColumns(serverLogColumns)
const expected = sortedServerColumns()
expect(sortedColumns).toEqual(expected)
})
it('Converts the config from server to the format used by UI', () => {
const uiLogConfig = logConfigServerToUI(serverLogConfig)
const expected = {
tableColumns: [
{internalName: 'time', displayName: '', visible: false},
{internalName: 'severity', displayName: '', visible: true},
{internalName: 'timestamp', displayName: '', visible: true},
{internalName: 'message', displayName: '', visible: true},
{internalName: 'facility', displayName: '', visible: true},
{internalName: 'procid', displayName: 'Proc ID', visible: true},
{
internalName: 'appname',
displayName: 'Application',
visible: true,
},
{internalName: 'host', displayName: '', visible: true},
],
severityFormat: SeverityFormatOptions.dotText,
}
expect(uiLogConfig).toEqual(expected)
})
})
describe('logConfigUIToServer', () => {
it('generates visibility and displayName encodings from column', () => {
const tableColumn = {
internalName: 'appname',
displayName: 'Application',
visible: true,
}
const encodings = getDisplayAndVisibleEncodings(tableColumn)
const expected = [
{
type: 'visibility',
value: 'visible',
},
{
type: 'displayName',
value: 'Application',
},
]
expect(encodings).toEqual(expected)
})
it('generates label encodings from serverFormat', () => {
const severityFormatDotText = SeverityFormatOptions.dotText
const severityFromatDot = SeverityFormatOptions.dot
const encodingsDotText = getLabelEncodings(severityFormatDotText)
const encodingsDot = getLabelEncodings(severityFromatDot)
const expectedDotText = [
{
type: 'label',
value: 'icon',
},
{
type: 'label',
value: 'text',
},
]
const expectedDot = [
{
type: 'label',
value: 'icon',
},
]
expect(encodingsDotText).toEqual(expectedDotText)
expect(encodingsDot).toEqual(expectedDot)
})
it('gets all encodings when appropriate', () => {
const displayName = 'SEVERITY'
const tableColumnSeverity = {
internalName: 'severity',
displayName,
visible: true,
}
const tableColumnOther = {
internalName: 'host',
displayName: '',
visible: true,
}
const severityFormat = SeverityFormatOptions.dotText
const encodingsSeverity = getFullEncodings(
tableColumnSeverity,
severityFormat
)
const encodingsOther = getFullEncodings(tableColumnOther, severityFormat)
const expectedSeverity = [
{
type: 'visibility',
value: 'visible',
},
{
type: 'displayName',
value: displayName,
},
{
type: 'label',
value: 'icon',
},
{
type: 'label',
value: 'text',
},
]
const expectedOther = [
{
type: 'visibility',
value: 'visible',
},
]
expect(encodingsSeverity).toEqual(expectedSeverity)
expect(encodingsOther).toEqual(expectedOther)
})
it('Converts the config from what the UI uses to what the server takes', () => {
const uiLogConfig = {
tableColumns: [
{internalName: 'time', displayName: '', visible: false},
{internalName: 'severity', displayName: '', visible: true},
{internalName: 'timestamp', displayName: '', visible: true},
{internalName: 'message', displayName: '', visible: true},
{internalName: 'facility', displayName: '', visible: true},
{internalName: 'procid', displayName: 'Proc ID', visible: true},
{
internalName: 'appname',
displayName: 'Application',
visible: true,
},
{internalName: 'host', displayName: '', visible: true},
],
severityFormat: SeverityFormatOptions.dotText,
}
const convertedServerLogConfig = logConfigUIToServer(uiLogConfig)
const expected = {columns: sortedServerColumns()}
expect(convertedServerLogConfig).toEqual(expected)
})
})
})