Merge pull request #3815 from influxdata/logs-viewer/reorder-columns

Log Viewer Configs - reorder columns and format severity
pull/10616/head
Iris Scholten 2018-07-03 14:42:44 -07:00 committed by GitHub
commit 7b2e16fc76
27 changed files with 1960 additions and 99 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)
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,
},
}
}

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

@ -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})
}
}

View File

@ -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)

View File

@ -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})
}
}

View File

@ -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>
</>
)
}

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 {
@ -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,23 +371,54 @@ 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}
@ -351,9 +426,12 @@ class LogsTable extends Component<Props, State> {
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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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',
}

View File

@ -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,6 +123,7 @@ 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">
@ -116,19 +141,47 @@ class LogsPage extends PureComponent<Props, State> {
/>
<LogsTable
count={this.histogramTotal}
data={this.props.tableData}
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>
{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)

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,
@ -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(

View File

@ -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';

View File

@ -10,7 +10,7 @@
flex-wrap: nowrap;
align-items: center;
height: 30px;
margin-top: 8px;
margin-top: 4px;
&:first-of-type {
margin-top: 0;

View File

@ -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;
}

View File

@ -194,6 +194,7 @@ textarea.form-control {
}
.form-group > label,
label.form-label {
width: 100%;
display: inline-block;
font-size: 12px;
font-weight: 600;

View File

@ -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;

View File

@ -113,3 +113,14 @@
}
}
}
// Stretch to Fit
// ----------------------------------------------------------------------------
.nav-tablist.stretch {
width: 100%;
> li {
flex: 1 0 0;
justify-content: center;
}
}

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,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
}

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)
})
})
})