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 _ from 'lodash'
import {Dispatch} from 'redux'
import {Source, Namespace, TimeRange, QueryConfig} from 'src/types'
import {getSource} from 'src/shared/apis'
import {getDatabasesWithRetentionPolicies} from 'src/shared/apis/databases'
@ -9,16 +11,20 @@ import {
buildLogQuery,
parseHistogramQueryResponse,
} from 'src/logs/utils'
import {logConfigServerToUI, logConfigUIToServer} from 'src/logs/utils/config'
import {getDeep} from 'src/utils/wrappers'
import {executeQueryAsync} from 'src/logs/api'
import {LogsState, Filter, TableData} from 'src/types/logs'
import {
executeQueryAsync,
getLogConfig as getLogConfigAJAX,
updateLogConfig as updateLogConfigAJAX,
} from 'src/logs/api'
import {LogsState, Filter, TableData, LogConfig} from 'src/types/logs'
const defaultTableData: TableData = {
columns: [
'time',
'severity',
'timestamp',
'severity_1',
'facility',
'procid',
'application',
@ -51,6 +57,7 @@ export enum ActionTypes {
IncrementQueryCount = 'LOGS_INCREMENT_QUERY_COUNT',
DecrementQueryCount = 'LOGS_DECREMENT_QUERY_COUNT',
ConcatMoreLogs = 'LOGS_CONCAT_MORE_LOGS',
SetConfig = 'SET_CONFIG',
}
export interface ConcatMoreLogsAction {
@ -160,6 +167,13 @@ interface ChangeZoomAction {
}
}
export interface SetConfigsAction {
type: ActionTypes.SetConfig
payload: {
logConfig: LogConfig
}
}
export type Action =
| SetSourceAction
| SetNamespacesAction
@ -177,6 +191,7 @@ export type Action =
| DecrementQueryCountAction
| IncrementQueryCountAction
| ConcatMoreLogsAction
| SetConfigsAction
const getTimeRange = (state: State): TimeRange | null =>
getDeep<TimeRange | null>(state, 'logs.timeRange', null)
@ -485,3 +500,37 @@ export const changeZoomAsync = (timeRange: TimeRange) => async (
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 AJAX from 'src/utils/ajax'
import {Namespace} from 'src/types'
import {TimeSeriesResponse} from 'src/types/series'
import {ServerLogConfig} from 'src/types/logs'
export const executeQueryAsync = async (
proxyLink: string,
@ -20,3 +22,31 @@ export const executeQueryAsync = async (
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 {
const {timeRange} = this.props
const {timeRange, onShowOptionsOverlay} = this.props
return (
<>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import _ from 'lodash'
import {
ActionTypes,
Action,
@ -8,8 +9,10 @@ import {
DecrementQueryCountAction,
IncrementQueryCountAction,
ConcatMoreLogsAction,
SetConfigsAction,
} from 'src/logs/actions'
import {SeverityFormatOptions} from 'src/logs/constants'
import {LogsState} from 'src/types/logs'
const defaultState: LogsState = {
@ -24,6 +27,10 @@ const defaultState: LogsState = {
searchTerm: '',
filters: [],
queryCount: 0,
logConfig: {
tableColumns: [],
severityFormat: SeverityFormatOptions.dotText,
},
}
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) => {
switch (action.type) {
case ActionTypes.SetSource:
@ -130,6 +146,8 @@ export default (state: LogsState = defaultState, action: Action) => {
return decrementQueryCount(state, action)
case ActionTypes.ConcatMoreLogs:
return concatMoreLogs(state, action)
case ActionTypes.SetConfig:
return setConfigs(state, action)
default:
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',
value: 'message',
},
{
alias: 'severity_text',
type: 'field',
value: 'severity',
},
{
alias: 'facility',
type: 'field',

View File

@ -1,7 +1,8 @@
import _ from 'lodash'
import moment from 'moment'
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
@ -21,7 +22,7 @@ export const getColumnFromData = (data: TableData, index: number): string =>
getDeep(data, `columns.${index}`, '')
export const isClickable = (column: string): boolean =>
_.includes(['appname', 'facility', 'host', 'hostname', 'severity_1'], column)
_.includes(['appname', 'facility', 'host', 'hostname', 'severity'], column)
export const formatColumnValue = (
column: string,
@ -31,6 +32,8 @@ export const formatColumnValue = (
switch (column) {
case 'timestamp':
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':
value = (value || 'No Message Provided').replace('\\n', '')
if (value.indexOf(' ') > charLimit - 5) {
@ -40,19 +43,16 @@ export const formatColumnValue = (
}
return value
}
export const header = (key: string): string => {
return getDeep<string>(
{
timestamp: 'Timestamp',
procid: 'Proc ID',
message: 'Message',
appname: 'Application',
severity: '',
severity_1: 'Severity',
},
key,
_.capitalize(key)
)
export const header = (
key: string,
headerOptions: LogsTableColumn[]
): string => {
if (key === SeverityFormatOptions.dot) {
return ''
}
const headerOption = _.find(headerOptions, h => h.internalName === key)
return _.get(headerOption, 'displayName') || _.capitalize(key)
}
export const getColumnWidth = (column: string): number => {
@ -61,8 +61,9 @@ export const getColumnWidth = (column: string): number => {
timestamp: 160,
procid: 80,
facility: 120,
severity: 22,
severity_1: 120,
severity_dot: 25,
severity_text: 120,
severity_dotText: 120,
host: 300,
},
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 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 + getColumnWidth(col)
let columnName = col
if (col === 'severity') {
columnName = `${col}_${severityFormat}`
}
return acc + getColumnWidth(columnName)
}, 0)
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 {FieldOption} from 'src/types/dashboards'
import {TimeSeriesValue} from 'src/types/series'
export interface Filter {
id: string
@ -9,7 +12,7 @@ export interface Filter {
export interface TableData {
columns: string[]
values: string[]
values: TimeSeriesValue[][]
}
export interface LogsState {
@ -24,6 +27,12 @@ export interface LogsState {
searchTerm: string | null
filters: Filter[]
queryCount: number
logConfig: LogConfig
}
export interface LogConfig {
tableColumns: LogsTableColumn[]
severityFormat: SeverityFormat
}
export interface SeverityLevel {
@ -37,10 +46,21 @@ export interface SeverityColor {
name: string
}
export type SeverityFormat = 'dot' | 'dotText' | 'text'
export type SeverityFormat = SeverityFormatOptions
export interface LogsTableColumn {
internalName: string
displayName: string
visible: boolean
export type LogsTableColumn = FieldOption
export interface ServerLogConfig {
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,
} from 'src/types/dashboards'
import {LineColor, ColorNumber} from 'src/types/colors'
import {ServerLogConfig, ServerColumn} from 'src/types/logs'
export const sourceLinks: SourceLinks = {
services: '/chronograf/v1/sources/4',
@ -381,3 +382,106 @@ export const gaugeColors: ColorNumber[] = [
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)
})
})
})