Merge pull request #3815 from influxdata/logs-viewer/reorder-columns
Log Viewer Configs - reorder columns and format severitypull/10616/head
commit
7b2e16fc76
|
@ -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)
|
||||
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)
|
||||
dispatch(setConfig(config))
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
export const setConfig = (logConfig: LogConfig): SetConfigsAction => {
|
||||
return {
|
||||
type: ActionTypes.SetConfig,
|
||||
payload: {
|
||||
logConfig,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
import React, {Component, MouseEvent} from 'react'
|
||||
|
||||
import classnames from 'classnames'
|
||||
import {ClickOutside} from 'src/shared/components/ClickOutside'
|
||||
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {DROPDOWN_MENU_MAX_HEIGHT} from 'src/shared/constants/index'
|
||||
import {SEVERITY_COLORS} from 'src/logs/constants'
|
||||
|
||||
import {SeverityColor} from 'src/types/logs'
|
||||
|
||||
interface Props {
|
||||
selected: SeverityColor
|
||||
disabled?: boolean
|
||||
stretchToFit?: boolean
|
||||
onChoose: (severityLevel: string, colors: SeverityColor) => void
|
||||
severityLevel: string
|
||||
}
|
||||
|
||||
interface State {
|
||||
expanded: boolean
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
export default class ColorDropdown extends Component<Props, State> {
|
||||
public static defaultProps: Partial<Props> = {
|
||||
stretchToFit: false,
|
||||
disabled: false,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
expanded: false,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {expanded} = this.state
|
||||
const {selected} = this.props
|
||||
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.handleClickOutside}>
|
||||
<div className={this.dropdownClassNames}>
|
||||
<div
|
||||
className={this.buttonClassNames}
|
||||
onClick={this.handleToggleMenu}
|
||||
>
|
||||
<div
|
||||
className="color-dropdown--swatch"
|
||||
style={{backgroundColor: selected.hex}}
|
||||
/>
|
||||
<div className="color-dropdown--name">{selected.name}</div>
|
||||
<span className="caret" />
|
||||
</div>
|
||||
{expanded && this.renderMenu}
|
||||
</div>
|
||||
</ClickOutside>
|
||||
)
|
||||
}
|
||||
|
||||
private get dropdownClassNames(): string {
|
||||
const {stretchToFit} = this.props
|
||||
const {expanded} = this.state
|
||||
|
||||
return classnames('color-dropdown', {
|
||||
open: expanded,
|
||||
'color-dropdown--stretch': stretchToFit,
|
||||
})
|
||||
}
|
||||
|
||||
private get buttonClassNames(): string {
|
||||
const {disabled} = this.props
|
||||
const {expanded} = this.state
|
||||
|
||||
return classnames('btn btn-sm btn-default color-dropdown--toggle', {
|
||||
active: expanded,
|
||||
'color-dropdown__disabled': disabled,
|
||||
})
|
||||
}
|
||||
|
||||
private get renderMenu(): JSX.Element {
|
||||
const {selected} = this.props
|
||||
|
||||
return (
|
||||
<div className="color-dropdown--menu">
|
||||
<FancyScrollbar
|
||||
autoHide={false}
|
||||
autoHeight={true}
|
||||
maxHeight={DROPDOWN_MENU_MAX_HEIGHT}
|
||||
>
|
||||
{SEVERITY_COLORS.map((color, i) => (
|
||||
<div
|
||||
className={classnames('color-dropdown--item', {
|
||||
active: color.name === selected.name,
|
||||
})}
|
||||
data-tag-key={color.name}
|
||||
data-tag-value={color.hex}
|
||||
key={i}
|
||||
onClick={this.handleColorClick}
|
||||
title={color.name}
|
||||
>
|
||||
<span
|
||||
className="color-dropdown--swatch"
|
||||
style={{backgroundColor: color.hex}}
|
||||
/>
|
||||
<span className="color-dropdown--name">{color.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</FancyScrollbar>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleToggleMenu = (): void => {
|
||||
const {disabled} = this.props
|
||||
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
this.setState({expanded: !this.state.expanded})
|
||||
}
|
||||
|
||||
private handleClickOutside = (): void => {
|
||||
this.setState({expanded: false})
|
||||
}
|
||||
|
||||
private handleColorClick = (e: MouseEvent<HTMLElement>): void => {
|
||||
const target = e.target as HTMLElement
|
||||
const hex = target.dataset.tagValue || target.parentElement.dataset.tagValue
|
||||
const name = target.dataset.tagKey || target.parentElement.dataset.tagKey
|
||||
|
||||
const color: SeverityColor = {name, hex}
|
||||
this.props.onChoose(this.props.severityLevel, color)
|
||||
this.setState({expanded: false})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import React, {Component} from 'react'
|
||||
import {DragDropContext} from 'react-dnd'
|
||||
import HTML5Backend from 'react-dnd-html5-backend'
|
||||
import DraggableColumn from 'src/logs/components/DraggableColumn'
|
||||
import {LogsTableColumn} from 'src/types/logs'
|
||||
|
||||
interface Props {
|
||||
columns: LogsTableColumn[]
|
||||
onMoveColumn: (dragIndex: number, hoverIndex: number) => void
|
||||
onUpdateColumn: (column: LogsTableColumn) => void
|
||||
}
|
||||
|
||||
class ColumnsOptions extends Component<Props> {
|
||||
public render() {
|
||||
const {columns} = this.props
|
||||
|
||||
return (
|
||||
<>
|
||||
<label className="form-label">Table Columns</label>
|
||||
<div className="logs-options--columns">
|
||||
{columns.map((c, i) => this.getDraggableColumn(c, i))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
private getDraggableColumn(column: LogsTableColumn, i: number): JSX.Element {
|
||||
const {onMoveColumn, onUpdateColumn} = this.props
|
||||
if (column.internalName !== 'time') {
|
||||
return (
|
||||
<DraggableColumn
|
||||
key={column.internalName}
|
||||
index={i}
|
||||
id={column.internalName}
|
||||
internalName={column.internalName}
|
||||
displayName={column.displayName}
|
||||
visible={column.visible}
|
||||
onUpdateColumn={onUpdateColumn}
|
||||
onMoveColumn={onMoveColumn}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default DragDropContext(HTML5Backend)(ColumnsOptions)
|
|
@ -0,0 +1,216 @@
|
|||
import React, {Component, ChangeEvent} from 'react'
|
||||
import {findDOMNode} from 'react-dom'
|
||||
import {
|
||||
DragSourceSpec,
|
||||
DropTargetConnector,
|
||||
DragSourceMonitor,
|
||||
DragSource,
|
||||
DropTarget,
|
||||
DragSourceConnector,
|
||||
ConnectDragSource,
|
||||
ConnectDropTarget,
|
||||
ConnectDragPreview,
|
||||
} from 'react-dnd'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {LogsTableColumn} from 'src/types/logs'
|
||||
|
||||
const columnType = 'column'
|
||||
|
||||
interface Props {
|
||||
internalName: string
|
||||
displayName: string
|
||||
visible: boolean
|
||||
index: number
|
||||
id: string
|
||||
key: string
|
||||
onUpdateColumn: (column: LogsTableColumn) => void
|
||||
isDragging?: boolean
|
||||
connectDragSource?: ConnectDragSource
|
||||
connectDropTarget?: ConnectDropTarget
|
||||
connectDragPreview?: ConnectDragPreview
|
||||
onMoveColumn: (dragIndex: number, hoverIndex: number) => void
|
||||
}
|
||||
|
||||
const columnSource: DragSourceSpec<Props> = {
|
||||
beginDrag(props) {
|
||||
return {
|
||||
id: props.id,
|
||||
index: props.index,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const columnTarget = {
|
||||
hover(props, monitor, component) {
|
||||
const dragIndex = monitor.getItem().index
|
||||
const hoverIndex = props.index
|
||||
|
||||
// Don't replace items with themselves
|
||||
if (dragIndex === hoverIndex) {
|
||||
return
|
||||
}
|
||||
|
||||
// Determine rectangle on screen
|
||||
const hoverBoundingRect = findDOMNode(component).getBoundingClientRect()
|
||||
|
||||
// Get vertical middle
|
||||
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2
|
||||
|
||||
// Determine mouse position
|
||||
const clientOffset = monitor.getClientOffset()
|
||||
|
||||
// Get pixels to the top
|
||||
const hoverClientY = clientOffset.y - hoverBoundingRect.top
|
||||
|
||||
// Only perform the move when the mouse has crossed half of the items height
|
||||
// When dragging downwards, only move when the cursor is below 50%
|
||||
// When dragging upwards, only move when the cursor is above 50%
|
||||
|
||||
// Dragging downwards
|
||||
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
|
||||
return
|
||||
}
|
||||
|
||||
// Dragging upwards
|
||||
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
|
||||
return
|
||||
}
|
||||
// Time to actually perform the action
|
||||
props.onMoveColumn(dragIndex, hoverIndex)
|
||||
|
||||
// Note: we're mutating the monitor item here!
|
||||
// Generally it's better to avoid mutations,
|
||||
// but it's good here for the sake of performance
|
||||
// to avoid expensive index searches.
|
||||
monitor.getItem().index = hoverIndex
|
||||
},
|
||||
}
|
||||
|
||||
function ColumnDropTarget(dropColumnType, dropColumnTarget, dropHandler) {
|
||||
return target =>
|
||||
DropTarget(dropColumnType, dropColumnTarget, dropHandler)(target) as any
|
||||
}
|
||||
|
||||
function ColumnDragSource(dragColumnType, dragColumnSource, dragHandler) {
|
||||
return target =>
|
||||
DragSource(dragColumnType, dragColumnSource, dragHandler)(target) as any
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
@ColumnDropTarget(columnType, columnTarget, (connect: DropTargetConnector) => ({
|
||||
connectDropTarget: connect.dropTarget(),
|
||||
}))
|
||||
@ColumnDragSource(
|
||||
columnType,
|
||||
columnSource,
|
||||
(connect: DragSourceConnector, monitor: DragSourceMonitor) => ({
|
||||
connectDragSource: connect.dragSource(),
|
||||
connectDragPreview: connect.dragPreview(),
|
||||
isDragging: monitor.isDragging(),
|
||||
})
|
||||
)
|
||||
export default class DraggableColumn extends Component<Props> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.handleColumnRename = this.handleColumnRename.bind(this)
|
||||
this.handleToggleVisible = this.handleToggleVisible.bind(this)
|
||||
}
|
||||
public render(): JSX.Element | null {
|
||||
const {
|
||||
internalName,
|
||||
displayName,
|
||||
connectDragPreview,
|
||||
connectDropTarget,
|
||||
visible,
|
||||
} = this.props
|
||||
|
||||
return connectDragPreview(
|
||||
connectDropTarget(
|
||||
<div className={this.columnClassName}>
|
||||
<div className={this.labelClassName}>
|
||||
{this.dragHandle}
|
||||
{this.visibilityToggle}
|
||||
<div className="customizable-field--name">{internalName}</div>
|
||||
</div>
|
||||
<input
|
||||
className="form-control input-sm customizable-field--input"
|
||||
type="text"
|
||||
spellCheck={false}
|
||||
id="internalName"
|
||||
value={displayName}
|
||||
onChange={this.handleColumnRename}
|
||||
placeholder={`Rename ${internalName}`}
|
||||
disabled={!visible}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private get dragHandle(): JSX.Element {
|
||||
const {connectDragSource} = this.props
|
||||
|
||||
return connectDragSource(
|
||||
<div className="customizable-field--drag">
|
||||
<span className="hamburger" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get visibilityToggle(): JSX.Element {
|
||||
const {visible, internalName} = this.props
|
||||
|
||||
if (visible) {
|
||||
return (
|
||||
<div
|
||||
className="customizable-field--visibility"
|
||||
onClick={this.handleToggleVisible}
|
||||
title={`Click to HIDE ${internalName}`}
|
||||
>
|
||||
<span className="icon eye-open" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="customizable-field--visibility"
|
||||
onClick={this.handleToggleVisible}
|
||||
title={`Click to SHOW ${internalName}`}
|
||||
>
|
||||
<span className="icon eye-closed" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get labelClassName(): string {
|
||||
const {visible} = this.props
|
||||
|
||||
if (visible) {
|
||||
return 'customizable-field--label'
|
||||
}
|
||||
|
||||
return 'customizable-field--label__hidden'
|
||||
}
|
||||
|
||||
private get columnClassName(): string {
|
||||
const {isDragging} = this.props
|
||||
|
||||
if (isDragging) {
|
||||
return 'customizable-field dragging'
|
||||
}
|
||||
|
||||
return 'customizable-field'
|
||||
}
|
||||
|
||||
private handleColumnRename = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||
const {onUpdateColumn, internalName, visible} = this.props
|
||||
onUpdateColumn({internalName, displayName: e.target.value, visible})
|
||||
}
|
||||
|
||||
private handleToggleVisible = (): void => {
|
||||
const {onUpdateColumn, internalName, displayName, visible} = this.props
|
||||
onUpdateColumn({internalName, displayName, visible: !visible})
|
||||
}
|
||||
}
|
|
@ -2,11 +2,12 @@ import _ from 'lodash'
|
|||
import React, {PureComponent} from 'react'
|
||||
import {Source, Namespace} from 'src/types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import Dropdown from 'src/shared/components/Dropdown'
|
||||
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
|
||||
import PageHeaderTitle from 'src/reusable_ui/components/page_layout/PageHeaderTitle'
|
||||
import TimeRangeDropdown from 'src/logs/components/TimeRangeDropdown'
|
||||
|
||||
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
|
||||
import {TimeRange} from 'src/types'
|
||||
|
||||
interface SourceItem {
|
||||
|
@ -25,6 +26,7 @@ interface Props {
|
|||
onChooseNamespace: (namespace: Namespace) => void
|
||||
onChooseTimerange: (timeRange: TimeRange) => void
|
||||
onChangeLiveUpdatingStatus: () => void
|
||||
onShowOptionsOverlay: () => void
|
||||
}
|
||||
|
||||
class LogViewerHeader extends PureComponent<Props> {
|
||||
|
@ -48,7 +50,7 @@ class LogViewerHeader extends PureComponent<Props> {
|
|||
}
|
||||
|
||||
private get optionsComponents(): JSX.Element {
|
||||
const {timeRange} = this.props
|
||||
const {timeRange, onShowOptionsOverlay} = this.props
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -69,6 +71,14 @@ class LogViewerHeader extends PureComponent<Props> {
|
|||
onChooseTimeRange={this.handleChooseTimeRange}
|
||||
selected={timeRange}
|
||||
/>
|
||||
<Authorized requiredRole={EDITOR_ROLE}>
|
||||
<button
|
||||
className="btn btn-sm btn-square btn-default"
|
||||
onClick={onShowOptionsOverlay}
|
||||
>
|
||||
<span className="icon cog-thick" />
|
||||
</button>
|
||||
</Authorized>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
@ -46,7 +47,9 @@ interface State {
|
|||
scrollTop: number
|
||||
currentRow: number
|
||||
currentMessageWidth: number
|
||||
isMessageVisible: boolean
|
||||
lastQueryTime: number
|
||||
visibleColumnsCount: number
|
||||
}
|
||||
|
||||
class LogsTable extends Component<Props, State> {
|
||||
|
@ -62,6 +65,14 @@ class LogsTable extends Component<Props, State> {
|
|||
|
||||
const scrollLeft = _.get(state, 'scrollLeft', 0)
|
||||
|
||||
let isMessageVisible: boolean = false
|
||||
const visibleColumnsCount = props.tableColumns.filter(c => {
|
||||
if (c.internalName === 'message') {
|
||||
isMessageVisible = c.visible
|
||||
}
|
||||
return c.visible
|
||||
}).length
|
||||
|
||||
return {
|
||||
...state,
|
||||
isQuerying: false,
|
||||
|
@ -69,7 +80,13 @@ class LogsTable extends Component<Props, State> {
|
|||
scrollTop,
|
||||
scrollLeft,
|
||||
currentRow: -1,
|
||||
currentMessageWidth: getMessageWidth(props.data),
|
||||
currentMessageWidth: getMessageWidth(
|
||||
props.data,
|
||||
props.tableColumns,
|
||||
props.severityFormat
|
||||
),
|
||||
isMessageVisible,
|
||||
visibleColumnsCount,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,12 +99,22 @@ class LogsTable extends Component<Props, State> {
|
|||
this.grid = null
|
||||
this.headerGrid = React.createRef()
|
||||
|
||||
let isMessageVisible: boolean = false
|
||||
const visibleColumnsCount = props.tableColumns.filter(c => {
|
||||
if (c.internalName === 'message') {
|
||||
isMessageVisible = c.visible
|
||||
}
|
||||
return c.visible
|
||||
}).length
|
||||
|
||||
this.state = {
|
||||
scrollTop: 0,
|
||||
scrollLeft: 0,
|
||||
currentRow: -1,
|
||||
currentMessageWidth: 0,
|
||||
lastQueryTime: null,
|
||||
isMessageVisible,
|
||||
visibleColumnsCount,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -121,10 +148,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 +297,41 @@ 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 {currentMessageWidth} = this.state
|
||||
const {severityFormat} = this.props
|
||||
const column = getColumnFromData(this.props.data, index)
|
||||
const {
|
||||
currentMessageWidth,
|
||||
isMessageVisible,
|
||||
visibleColumnsCount,
|
||||
} = this.state
|
||||
|
||||
switch (column) {
|
||||
case 'message':
|
||||
return currentMessageWidth
|
||||
default:
|
||||
return getColumnWidth(column)
|
||||
let columnKey = column
|
||||
if (column === 'severity') {
|
||||
columnKey = `${column}_${severityFormat}`
|
||||
}
|
||||
const width = getColumnWidth(columnKey)
|
||||
if (!isMessageVisible) {
|
||||
const inc = currentMessageWidth / visibleColumnsCount
|
||||
return width + inc
|
||||
}
|
||||
return width
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -327,33 +371,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 +442,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 +496,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)
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
import React, {Component} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
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 {
|
||||
SeverityLevel,
|
||||
SeverityColor,
|
||||
SeverityFormat,
|
||||
LogsTableColumn,
|
||||
} from 'src/types/logs'
|
||||
import {DEFAULT_SEVERITY_LEVELS} from 'src/logs/constants'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface Props {
|
||||
severityLevels: SeverityLevel[]
|
||||
onUpdateSeverityLevels: (severityLevels: SeverityLevel[]) => void
|
||||
onDismissOverlay: () => void
|
||||
columns: LogsTableColumn[]
|
||||
onUpdateColumns: (columns: LogsTableColumn[]) => void
|
||||
severityFormat: SeverityFormat
|
||||
onUpdateSeverityFormat: (format: SeverityFormat) => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
workingSeverityLevels: SeverityLevel[]
|
||||
workingColumns: LogsTableColumn[]
|
||||
workingFormat: SeverityFormat
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class OptionsOverlay extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
workingSeverityLevels: this.props.severityLevels,
|
||||
workingColumns: this.props.columns,
|
||||
workingFormat: this.props.severityFormat,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {workingSeverityLevels, workingColumns, workingFormat} = this.state
|
||||
|
||||
return (
|
||||
<Container maxWidth={800}>
|
||||
<Heading title="Configure Log Viewer">
|
||||
{this.overlayActionButtons}
|
||||
</Heading>
|
||||
<Body>
|
||||
<div className="row">
|
||||
<div className="col-sm-5">
|
||||
<SeverityOptions
|
||||
severityLevels={workingSeverityLevels}
|
||||
onReset={this.handleResetSeverityLevels}
|
||||
onChangeSeverityLevel={this.handleChangeSeverityLevel}
|
||||
severityFormat={workingFormat}
|
||||
onChangeSeverityFormat={this.handleChangeSeverityFormat}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-sm-7">
|
||||
<ColumnsOptions
|
||||
columns={workingColumns}
|
||||
onMoveColumn={this.handleMoveColumn}
|
||||
onUpdateColumn={this.handleUpdateColumn}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Body>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
private get overlayActionButtons(): JSX.Element {
|
||||
const {onDismissOverlay} = this.props
|
||||
|
||||
return (
|
||||
<div className="btn-group--right">
|
||||
<button className="btn btn-sm btn-default" onClick={onDismissOverlay}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-success"
|
||||
onClick={this.handleSave}
|
||||
disabled={this.isSaveDisabled}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get isSaveDisabled(): boolean {
|
||||
const {workingSeverityLevels, workingColumns, workingFormat} = this.state
|
||||
const {severityLevels, columns, severityFormat} = this.props
|
||||
|
||||
const severityChanged = !_.isEqual(workingSeverityLevels, severityLevels)
|
||||
const columnsChanged = !_.isEqual(workingColumns, columns)
|
||||
const formatChanged = !_.isEqual(workingFormat, severityFormat)
|
||||
|
||||
if (severityChanged || columnsChanged || formatChanged) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private handleSave = async () => {
|
||||
const {
|
||||
onUpdateSeverityLevels,
|
||||
onDismissOverlay,
|
||||
onUpdateSeverityFormat,
|
||||
onUpdateColumns,
|
||||
} = this.props
|
||||
const {workingSeverityLevels, workingFormat, workingColumns} = this.state
|
||||
|
||||
await onUpdateSeverityFormat(workingFormat)
|
||||
await onUpdateSeverityLevels(workingSeverityLevels)
|
||||
await onUpdateColumns(workingColumns)
|
||||
onDismissOverlay()
|
||||
}
|
||||
|
||||
private handleResetSeverityLevels = (): void => {
|
||||
this.setState({workingSeverityLevels: DEFAULT_SEVERITY_LEVELS})
|
||||
}
|
||||
|
||||
private handleChangeSeverityLevel = (
|
||||
severity: string,
|
||||
override: SeverityColor
|
||||
): void => {
|
||||
const workingSeverityLevels = this.state.workingSeverityLevels.map(
|
||||
config => {
|
||||
if (config.severity === severity) {
|
||||
return {...config, override}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
)
|
||||
|
||||
this.setState({workingSeverityLevels})
|
||||
}
|
||||
|
||||
private handleChangeSeverityFormat = (format: SeverityFormat) => {
|
||||
this.setState({workingFormat: format})
|
||||
}
|
||||
|
||||
private handleMoveColumn = (dragIndex, hoverIndex) => {
|
||||
const {workingColumns} = this.state
|
||||
|
||||
const draggedField = workingColumns[dragIndex]
|
||||
|
||||
const columnsRemoved = _.concat(
|
||||
_.slice(workingColumns, 0, dragIndex),
|
||||
_.slice(workingColumns, dragIndex + 1)
|
||||
)
|
||||
|
||||
const columnsAdded = _.concat(
|
||||
_.slice(columnsRemoved, 0, hoverIndex),
|
||||
[draggedField],
|
||||
_.slice(columnsRemoved, hoverIndex)
|
||||
)
|
||||
|
||||
this.setState({workingColumns: columnsAdded})
|
||||
}
|
||||
|
||||
private handleUpdateColumn = (column: LogsTableColumn) => {
|
||||
const workingColumns = this.state.workingColumns.map(wc => {
|
||||
if (wc.internalName === column.internalName) {
|
||||
return column
|
||||
}
|
||||
|
||||
return wc
|
||||
})
|
||||
|
||||
this.setState({workingColumns})
|
||||
}
|
||||
}
|
||||
|
||||
export default OptionsOverlay
|
|
@ -0,0 +1,68 @@
|
|||
import React, {PureComponent, MouseEvent} from 'react'
|
||||
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {SeverityFormatOptions} from 'src/logs/constants'
|
||||
import {SeverityFormat} from 'src/types/logs'
|
||||
|
||||
interface Props {
|
||||
format: SeverityFormat
|
||||
onChangeFormat: (format: SeverityFormat) => void
|
||||
}
|
||||
|
||||
const className = (name: SeverityFormat, format: SeverityFormat): string => {
|
||||
if (name === format) {
|
||||
return 'active'
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class SeverityColumnFormat extends PureComponent<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {format} = this.props
|
||||
|
||||
return (
|
||||
<div className="graph-options-group">
|
||||
<label className="form-label">Severity Format</label>
|
||||
<ul className="nav nav-tablist nav-tablist-sm stretch">
|
||||
<li
|
||||
data-tag-value={SeverityFormatOptions.dot}
|
||||
onClick={this.handleClick}
|
||||
className={className(SeverityFormatOptions.dot, format)}
|
||||
>
|
||||
Dot
|
||||
</li>
|
||||
<li
|
||||
data-tag-value={SeverityFormatOptions.dotText}
|
||||
onClick={this.handleClick}
|
||||
className={className(SeverityFormatOptions.dotText, format)}
|
||||
>
|
||||
Dot + Text
|
||||
</li>
|
||||
<li
|
||||
data-tag-value={SeverityFormatOptions.text}
|
||||
onClick={this.handleClick}
|
||||
className={className(SeverityFormatOptions.text, format)}
|
||||
>
|
||||
Text
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleClick = (e: MouseEvent<HTMLElement>) => {
|
||||
const {onChangeFormat} = this.props
|
||||
const target = e.target as HTMLElement
|
||||
const value = target.dataset.tagValue
|
||||
|
||||
onChangeFormat(SeverityFormatOptions[value])
|
||||
}
|
||||
}
|
||||
|
||||
export default SeverityColumnFormat
|
|
@ -0,0 +1,52 @@
|
|||
import React, {SFC} from 'react'
|
||||
import uuid from 'uuid'
|
||||
import ColorDropdown from 'src/logs/components/ColorDropdown'
|
||||
import SeverityColumnFormat from 'src/logs/components/SeverityColumnFormat'
|
||||
import {SeverityLevel, SeverityColor, SeverityFormat} from 'src/types/logs'
|
||||
|
||||
interface Props {
|
||||
severityLevels: SeverityLevel[]
|
||||
onReset: () => void
|
||||
onChangeSeverityLevel: (severity: string, override: SeverityColor) => void
|
||||
severityFormat: SeverityFormat
|
||||
onChangeSeverityFormat: (format: SeverityFormat) => void
|
||||
}
|
||||
|
||||
const SeverityConfig: SFC<Props> = ({
|
||||
severityLevels,
|
||||
onReset,
|
||||
onChangeSeverityLevel,
|
||||
severityFormat,
|
||||
onChangeSeverityFormat,
|
||||
}) => (
|
||||
<>
|
||||
<label className="form-label">Severity Colors</label>
|
||||
<div className="logs-options--color-list">
|
||||
{severityLevels.map(config => (
|
||||
<div key={uuid.v4()} className="logs-options--color-row">
|
||||
<div className="logs-options--color-column">
|
||||
<div className="logs-options--color-label">{config.severity}</div>
|
||||
</div>
|
||||
<div className="logs-options--color-column">
|
||||
<ColorDropdown
|
||||
selected={config.override || config.default}
|
||||
onChoose={onChangeSeverityLevel}
|
||||
stretchToFit={true}
|
||||
severityLevel={config.severity}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button className="btn btn-sm btn-default btn-block" onClick={onReset}>
|
||||
<span className="icon refresh" />
|
||||
Reset to Defaults
|
||||
</button>
|
||||
<SeverityColumnFormat
|
||||
format={severityFormat}
|
||||
onChangeFormat={onChangeSeverityFormat}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
export default SeverityConfig
|
|
@ -0,0 +1,147 @@
|
|||
export const SEVERITY_COLORS = [
|
||||
{
|
||||
hex: '#BF3D5E',
|
||||
name: 'ruby',
|
||||
},
|
||||
{
|
||||
hex: '#DC4E58',
|
||||
name: 'fire',
|
||||
},
|
||||
{
|
||||
hex: '#F95F53',
|
||||
name: 'curacao',
|
||||
},
|
||||
{
|
||||
hex: '#F48D38',
|
||||
name: 'tiger',
|
||||
},
|
||||
{
|
||||
hex: '#FFB94A',
|
||||
name: 'pineapple',
|
||||
},
|
||||
{
|
||||
hex: '#FFD255',
|
||||
name: 'thunder',
|
||||
},
|
||||
{
|
||||
hex: '#FFE480',
|
||||
name: 'sulfur',
|
||||
},
|
||||
{
|
||||
hex: '#32B08C',
|
||||
name: 'viridian',
|
||||
},
|
||||
{
|
||||
hex: '#4ED8A0',
|
||||
name: 'rainforest',
|
||||
},
|
||||
{
|
||||
hex: '#7CE490',
|
||||
name: 'honeydew',
|
||||
},
|
||||
{
|
||||
hex: '#4591ED',
|
||||
name: 'ocean',
|
||||
},
|
||||
{
|
||||
hex: '#22ADF6',
|
||||
name: 'pool',
|
||||
},
|
||||
{
|
||||
hex: '#00C9FF',
|
||||
name: 'laser',
|
||||
},
|
||||
{
|
||||
hex: '#513CC6',
|
||||
name: 'planet',
|
||||
},
|
||||
{
|
||||
hex: '#7A65F2',
|
||||
name: 'star',
|
||||
},
|
||||
{
|
||||
hex: '#9394FF',
|
||||
name: 'comet',
|
||||
},
|
||||
{
|
||||
hex: '#545667',
|
||||
name: 'graphite',
|
||||
},
|
||||
{
|
||||
hex: '#8E91A1',
|
||||
name: 'wolf',
|
||||
},
|
||||
{
|
||||
hex: '#BEC2CC',
|
||||
name: 'mist',
|
||||
},
|
||||
{
|
||||
hex: '#E7E8EB',
|
||||
name: 'pearl',
|
||||
},
|
||||
]
|
||||
|
||||
export const DEFAULT_SEVERITY_LEVELS = [
|
||||
{
|
||||
severity: 'emergency',
|
||||
default: SEVERITY_COLORS.find(c => c.name === 'ruby'),
|
||||
override: null,
|
||||
},
|
||||
{
|
||||
severity: 'alert',
|
||||
default: SEVERITY_COLORS.find(c => c.name === 'fire'),
|
||||
override: null,
|
||||
},
|
||||
{
|
||||
severity: 'critical',
|
||||
default: SEVERITY_COLORS.find(c => c.name === 'curacao'),
|
||||
override: null,
|
||||
},
|
||||
{
|
||||
severity: 'error',
|
||||
default: SEVERITY_COLORS.find(c => c.name === 'tiger'),
|
||||
override: null,
|
||||
},
|
||||
{
|
||||
severity: 'warning',
|
||||
default: SEVERITY_COLORS.find(c => c.name === 'pineapple'),
|
||||
override: null,
|
||||
},
|
||||
{
|
||||
severity: 'notice',
|
||||
default: SEVERITY_COLORS.find(c => c.name === 'rainforest'),
|
||||
override: null,
|
||||
},
|
||||
{
|
||||
severity: 'info',
|
||||
default: SEVERITY_COLORS.find(c => c.name === 'star'),
|
||||
override: null,
|
||||
},
|
||||
{
|
||||
severity: 'debug',
|
||||
default: SEVERITY_COLORS.find(c => c.name === 'wolf'),
|
||||
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',
|
||||
}
|
|
@ -15,20 +15,40 @@ import {
|
|||
removeFilter,
|
||||
changeFilter,
|
||||
fetchMoreAsync,
|
||||
getLogConfigAsync,
|
||||
updateLogConfigAsync,
|
||||
} from 'src/logs/actions'
|
||||
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 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 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 {Filter} from 'src/types/logs'
|
||||
|
||||
import {HistogramData, TimePeriod} from 'src/types/histogram'
|
||||
import {
|
||||
Filter,
|
||||
SeverityLevel,
|
||||
SeverityFormat,
|
||||
LogsTableColumn,
|
||||
LogConfig,
|
||||
TableData,
|
||||
} from 'src/types/logs'
|
||||
|
||||
// Mock
|
||||
import {DEFAULT_SEVERITY_LEVELS} from 'src/logs/constants'
|
||||
|
||||
interface Props {
|
||||
sources: Source[]
|
||||
|
@ -46,20 +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
|
||||
logConfig: LogConfig
|
||||
logConfigLink: string
|
||||
}
|
||||
|
||||
interface State {
|
||||
searchString: string
|
||||
liveUpdating: boolean
|
||||
isOverlayVisible: boolean
|
||||
}
|
||||
|
||||
class LogsPage extends PureComponent<Props, State> {
|
||||
|
@ -71,6 +93,7 @@ class LogsPage extends PureComponent<Props, State> {
|
|||
this.state = {
|
||||
searchString: '',
|
||||
liveUpdating: false,
|
||||
isOverlayVisible: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,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()
|
||||
|
@ -99,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 {tableData} = this.props
|
||||
const tableColumns = this.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)
|
||||
}
|
||||
|
@ -158,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,
|
||||
|
@ -219,6 +271,7 @@ class LogsPage extends PureComponent<Props, State> {
|
|||
currentNamespaces={currentNamespaces}
|
||||
currentNamespace={currentNamespace}
|
||||
onChangeLiveUpdatingStatus={this.handleChangeLiveUpdatingStatus}
|
||||
onShowOptionsOverlay={this.handleToggleOverlay}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -282,10 +335,66 @@ class LogsPage extends PureComponent<Props, State> {
|
|||
this.props.executeQueriesAsync()
|
||||
this.setState({liveUpdating: true})
|
||||
}
|
||||
|
||||
private handleToggleOverlay = (): void => {
|
||||
this.setState({isOverlayVisible: !this.state.isOverlayVisible})
|
||||
}
|
||||
|
||||
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.tableColumns}
|
||||
onUpdateColumns={this.handleUpdateColumns}
|
||||
onUpdateSeverityFormat={this.handleUpdateSeverityFormat}
|
||||
severityFormat={this.severityFormat}
|
||||
/>
|
||||
</OverlayTechnology>
|
||||
)
|
||||
}
|
||||
|
||||
private handleUpdateSeverityLevels = (levels: SeverityLevel[]) => {
|
||||
// Todo: Handle saving of these new severity colors here
|
||||
levels = levels
|
||||
}
|
||||
|
||||
private handleUpdateSeverityFormat = async (format: SeverityFormat) => {
|
||||
const {logConfig} = this.props
|
||||
await this.props.updateConfig(this.logConfigLink, {
|
||||
...logConfig,
|
||||
severityFormat: format,
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
|
@ -296,6 +405,7 @@ const mapStateToProps = ({
|
|||
searchTerm,
|
||||
filters,
|
||||
queryCount,
|
||||
logConfig,
|
||||
},
|
||||
}) => ({
|
||||
sources,
|
||||
|
@ -308,6 +418,8 @@ const mapStateToProps = ({
|
|||
searchTerm,
|
||||
filters,
|
||||
queryCount,
|
||||
logConfig,
|
||||
logConfigLink: logViewer,
|
||||
})
|
||||
|
||||
const mapDispatchToProps = {
|
||||
|
@ -322,6 +434,8 @@ const mapDispatchToProps = {
|
|||
removeFilter,
|
||||
changeFilter,
|
||||
fetchMoreAsync,
|
||||
getConfig: getLogConfigAsync,
|
||||
updateConfig: updateLogConfigAsync,
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LogsPage)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -48,11 +48,6 @@ const tableFields = [
|
|||
type: 'field',
|
||||
value: 'message',
|
||||
},
|
||||
{
|
||||
alias: 'severity_text',
|
||||
type: 'field',
|
||||
value: 'severity',
|
||||
},
|
||||
{
|
||||
alias: 'facility',
|
||||
type: 'field',
|
||||
|
|
|
@ -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,
|
||||
|
@ -40,19 +41,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 +59,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 +69,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' || col === 'time' || !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(
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
@import 'components/graph';
|
||||
@import 'components/input-click-to-edit';
|
||||
@import 'components/input-tag-list';
|
||||
@import 'components/logs-viewer-options';
|
||||
@import 'components/newsfeed';
|
||||
@import 'components/opt-in';
|
||||
@import 'components/organizations-table';
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
margin-top: 8px;
|
||||
margin-top: 4px;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 0;
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
Styles for Logs Viewer Options Overlay
|
||||
----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
.logs-options--color-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.logs-options--color-row {
|
||||
display: flex;
|
||||
height: 30px;
|
||||
align-items: stretch;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.logs-options--color-column {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.logs-options--color-label {
|
||||
height: 30px;
|
||||
border-radius: $radius;
|
||||
background-color: $g3-castle;
|
||||
padding: 0 11px;
|
||||
line-height: 30px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
color: $g13-mist;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
margin-right: 4px;
|
||||
@include no-user-select();
|
||||
}
|
||||
|
||||
// Not very clean way of slightly darkening the disabled state
|
||||
// of draggable columns in the overlay
|
||||
.logs-options--columns .customizable-field--label__hidden {
|
||||
background-color: $g3-castle;
|
||||
}
|
|
@ -461,4 +461,4 @@ button.btn-link-alert {
|
|||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -194,6 +194,7 @@ textarea.form-control {
|
|||
}
|
||||
.form-group > label,
|
||||
label.form-label {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
|
|
|
@ -25,6 +25,8 @@ $grid--col-11: 91.66666667%;
|
|||
$grid--col-12: 100%;
|
||||
|
||||
.row {
|
||||
display: inline-block;
|
||||
width: calc(100% + #{$grid--gutter * 2});
|
||||
margin-left: -$grid--gutter;
|
||||
margin-right: -$grid--gutter;
|
||||
|
||||
|
|
|
@ -113,3 +113,14 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stretch to Fit
|
||||
// ----------------------------------------------------------------------------
|
||||
.nav-tablist.stretch {
|
||||
width: 100%;
|
||||
|
||||
> li {
|
||||
flex: 1 0 0;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,4 +27,40 @@ export interface LogsState {
|
|||
searchTerm: string | null
|
||||
filters: Filter[]
|
||||
queryCount: number
|
||||
logConfig: LogConfig
|
||||
}
|
||||
|
||||
export interface LogConfig {
|
||||
tableColumns: LogsTableColumn[]
|
||||
severityFormat: SeverityFormat
|
||||
}
|
||||
|
||||
export interface SeverityLevel {
|
||||
severity: string
|
||||
default: SeverityColor
|
||||
override?: SeverityColor
|
||||
}
|
||||
|
||||
export interface SeverityColor {
|
||||
hex: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export type SeverityFormat = SeverityFormatOptions
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue