Improve Log Messages UX (#4363)
* Refactor ExpandableMessage to use React Portals * Display "Clear Filters" button when more than 3 filters are present * Move logs truncation toggle from options overlay to filter bar * Simplify styles and markup Ensure that the ... truncation happens Showing a "zoom in" cursor when log messages can be clicked * Update changelog * Remove rogue file * Type props in constructor and remove unecessary condition * Remove spacespull/4374/head
parent
7201521b35
commit
5f6cb054ae
|
@ -19,6 +19,7 @@
|
|||
1. [#4227](https://github.com/influxdata/chronograf/pull/4227): Redesign Cell Editor Overlay for reuse in other parts of application
|
||||
1. [#4268](https://github.com/influxdata/chronograf/pull/4268): Clear logs after searching
|
||||
1. [#4253](https://github.com/influxdata/chronograf/pull/4253): Add search expression highlighting to log lines
|
||||
1. [#4363](https://github.com/influxdata/chronograf/pull/4363): Move log message truncation controls into logs filter bar
|
||||
|
||||
|
||||
### UI Improvements
|
||||
|
@ -28,6 +29,8 @@
|
|||
### Bug Fixes
|
||||
|
||||
1. [#4272](https://github.com/influxdata/chronograf/pull/4272): Fix logs loading description not displaying
|
||||
1. [#4363](https://github.com/influxdata/chronograf/pull/4363): Position expanded log messages above logs table
|
||||
|
||||
|
||||
## v1.6.2 [unreleased]
|
||||
|
||||
|
|
|
@ -69,6 +69,7 @@ export enum ActionTypes {
|
|||
AddFilter = 'LOGS_ADD_FILTER',
|
||||
RemoveFilter = 'LOGS_REMOVE_FILTER',
|
||||
ChangeFilter = 'LOGS_CHANGE_FILTER',
|
||||
ClearFilters = 'LOGS_CLEAR_FILTERS',
|
||||
IncrementQueryCount = 'LOGS_INCREMENT_QUERY_COUNT',
|
||||
DecrementQueryCount = 'LOGS_DECREMENT_QUERY_COUNT',
|
||||
ConcatMoreLogs = 'LOGS_CONCAT_MORE_LOGS',
|
||||
|
@ -151,6 +152,10 @@ export interface ChangeFilterAction {
|
|||
}
|
||||
}
|
||||
|
||||
export interface ClearFiltersAction {
|
||||
type: ActionTypes.ClearFilters
|
||||
}
|
||||
|
||||
export interface RemoveFilterAction {
|
||||
type: ActionTypes.RemoveFilter
|
||||
payload: {
|
||||
|
@ -248,6 +253,7 @@ export type Action =
|
|||
| AddFilterAction
|
||||
| RemoveFilterAction
|
||||
| ChangeFilterAction
|
||||
| ClearFiltersAction
|
||||
| DecrementQueryCountAction
|
||||
| IncrementQueryCountAction
|
||||
| ConcatMoreLogsAction
|
||||
|
@ -471,6 +477,10 @@ export const addFilter = (filter: Filter): AddFilterAction => ({
|
|||
payload: {filter},
|
||||
})
|
||||
|
||||
export const clearFilters = (): ClearFiltersAction => ({
|
||||
type: ActionTypes.ClearFilters,
|
||||
})
|
||||
|
||||
export const removeFilter = (id: string): RemoveFilterAction => ({
|
||||
type: ActionTypes.RemoveFilter,
|
||||
payload: {id},
|
||||
|
|
|
@ -1,16 +1,69 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import {Filter} from 'src/types/logs'
|
||||
import FilterBlock from 'src/logs/components/LogsFilter'
|
||||
import {Button, ComponentSize, Radio} from 'src/reusable_ui'
|
||||
|
||||
interface Props {
|
||||
filters: Filter[]
|
||||
onDelete: (id: string) => void
|
||||
onClearFilters: () => void
|
||||
onFilterChange: (id: string, operator: string, value: string) => void
|
||||
onUpdateTruncation: (isTruncated: boolean) => Promise<void>
|
||||
isTruncated: boolean
|
||||
}
|
||||
|
||||
class LogsFilters extends PureComponent<Props> {
|
||||
public render() {
|
||||
return <div className="logs-viewer--filter-bar">{this.renderFilters}</div>
|
||||
return (
|
||||
<div className="logs-viewer--filter-bar">
|
||||
<div className="logs-viewer--filters">{this.renderFilters}</div>
|
||||
{this.clearFiltersButton}
|
||||
{this.truncationToggle}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get clearFiltersButton(): JSX.Element {
|
||||
const {filters, onClearFilters} = this.props
|
||||
|
||||
if (filters.length >= 3) {
|
||||
return (
|
||||
<div className="logs-viewer--clear-filters">
|
||||
<Button
|
||||
size={ComponentSize.Small}
|
||||
text="Clear Filters"
|
||||
onClick={onClearFilters}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private get truncationToggle(): JSX.Element {
|
||||
const {isTruncated, onUpdateTruncation} = this.props
|
||||
|
||||
return (
|
||||
<Radio>
|
||||
<Radio.Button
|
||||
id="logs-truncation--truncate"
|
||||
active={isTruncated === true}
|
||||
value={true}
|
||||
titleText="Truncate log messages when they exceed 1 line"
|
||||
onClick={onUpdateTruncation}
|
||||
>
|
||||
Truncate
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
id="logs-truncation--multi"
|
||||
active={isTruncated === false}
|
||||
value={false}
|
||||
titleText="Allow log messages to wrap text"
|
||||
onClick={onUpdateTruncation}
|
||||
>
|
||||
Wrap
|
||||
</Radio.Button>
|
||||
</Radio>
|
||||
)
|
||||
}
|
||||
|
||||
private get renderFilters(): JSX.Element[] {
|
||||
|
|
|
@ -12,7 +12,6 @@ import {
|
|||
SeverityFormat,
|
||||
LogsTableColumn,
|
||||
} from 'src/types/logs'
|
||||
import SlideToggle from 'src/reusable_ui/components/slide_toggle/SlideToggle'
|
||||
import {DEFAULT_SEVERITY_LEVELS, SeverityLevelOptions} from 'src/logs/constants'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
|
@ -26,15 +25,12 @@ interface Props {
|
|||
onUpdateColumns: (columns: LogsTableColumn[]) => Promise<void>
|
||||
severityFormat: SeverityFormat
|
||||
onUpdateSeverityFormat: (format: SeverityFormat) => Promise<void>
|
||||
onUpdateTruncation: (isTruncated: boolean) => Promise<void>
|
||||
isTruncated: boolean
|
||||
}
|
||||
|
||||
interface State {
|
||||
workingLevelColumns: SeverityLevelColor[]
|
||||
workingColumns: LogsTableColumn[]
|
||||
workingFormat: SeverityFormat
|
||||
isWorkingTruncated: boolean
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
|
@ -46,14 +42,10 @@ class OptionsOverlay extends Component<Props, State> {
|
|||
workingLevelColumns: this.props.severityLevelColors,
|
||||
workingColumns: this.props.columns,
|
||||
workingFormat: this.props.severityFormat,
|
||||
isWorkingTruncated: this.props.isTruncated,
|
||||
}
|
||||
}
|
||||
|
||||
public shouldComponentUpdate(__, nextState: State) {
|
||||
const isTruncatedDifferent =
|
||||
nextState.isWorkingTruncated !== this.state.isWorkingTruncated
|
||||
|
||||
const isColorsDifferent = !_.isEqual(
|
||||
nextState.workingLevelColumns,
|
||||
this.state.workingLevelColumns
|
||||
|
@ -67,12 +59,7 @@ class OptionsOverlay extends Component<Props, State> {
|
|||
this.state.workingColumns
|
||||
)
|
||||
|
||||
if (
|
||||
isTruncatedDifferent ||
|
||||
isColorsDifferent ||
|
||||
isFormatDifferent ||
|
||||
isColumnsDifferent
|
||||
) {
|
||||
if (isColorsDifferent || isFormatDifferent || isColumnsDifferent) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
@ -97,7 +84,6 @@ class OptionsOverlay extends Component<Props, State> {
|
|||
onChangeSeverityFormat={this.handleChangeSeverityFormat}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-sm-7">{this.renderTruncateOption}</div>
|
||||
<div className="col-sm-7">
|
||||
<ColumnsOptions
|
||||
columns={workingColumns}
|
||||
|
@ -130,57 +116,15 @@ class OptionsOverlay extends Component<Props, State> {
|
|||
)
|
||||
}
|
||||
|
||||
private get renderTruncateOption() {
|
||||
const {isWorkingTruncated} = this.state
|
||||
|
||||
let labelMessage = 'Truncate Log Lines'
|
||||
|
||||
if (isWorkingTruncated) {
|
||||
labelMessage = 'Show Log Lines'
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<label className="form-label">{labelMessage}</label>
|
||||
<SlideToggle
|
||||
onChange={this.handleTruncationToggle}
|
||||
active={isWorkingTruncated}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
private handleTruncationToggle = (): void => {
|
||||
const {isWorkingTruncated} = this.state
|
||||
|
||||
this.setState({isWorkingTruncated: !isWorkingTruncated})
|
||||
}
|
||||
|
||||
private get isSaveDisabled(): boolean {
|
||||
const {
|
||||
workingLevelColumns,
|
||||
workingColumns,
|
||||
workingFormat,
|
||||
isWorkingTruncated,
|
||||
} = this.state
|
||||
const {
|
||||
severityLevelColors,
|
||||
columns,
|
||||
severityFormat,
|
||||
isTruncated,
|
||||
} = this.props
|
||||
const {workingLevelColumns, workingColumns, workingFormat} = this.state
|
||||
const {severityLevelColors, columns, severityFormat} = this.props
|
||||
|
||||
const severityChanged = !_.isEqual(workingLevelColumns, severityLevelColors)
|
||||
const columnsChanged = !_.isEqual(workingColumns, columns)
|
||||
const formatChanged = !_.isEqual(workingFormat, severityFormat)
|
||||
const isTruncatedChanged = isWorkingTruncated !== isTruncated
|
||||
|
||||
if (
|
||||
severityChanged ||
|
||||
columnsChanged ||
|
||||
formatChanged ||
|
||||
isTruncatedChanged
|
||||
) {
|
||||
if (severityChanged || columnsChanged || formatChanged) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -193,19 +137,12 @@ class OptionsOverlay extends Component<Props, State> {
|
|||
onDismissOverlay,
|
||||
onUpdateSeverityFormat,
|
||||
onUpdateColumns,
|
||||
onUpdateTruncation,
|
||||
} = this.props
|
||||
const {
|
||||
workingLevelColumns,
|
||||
workingFormat,
|
||||
workingColumns,
|
||||
isWorkingTruncated,
|
||||
} = this.state
|
||||
const {workingLevelColumns, workingFormat, workingColumns} = this.state
|
||||
|
||||
await onUpdateSeverityFormat(workingFormat)
|
||||
await onUpdateSeverityLevels(workingLevelColumns)
|
||||
await onUpdateColumns(workingColumns)
|
||||
await onUpdateTruncation(isWorkingTruncated)
|
||||
onDismissOverlay()
|
||||
}
|
||||
|
||||
|
|
|
@ -4,51 +4,44 @@
|
|||
*/
|
||||
|
||||
.expandable--message {
|
||||
display: flex;
|
||||
min-height: 100%;
|
||||
min-width: 100%;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
cursor: zoom-in;
|
||||
display: block;
|
||||
|
||||
.expandable--text {
|
||||
> .logs-message {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.collapsed--message {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.expanded--message {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
top: -($ix-marg-b - 1px);
|
||||
left: -($ix-marg-b - 4px);
|
||||
position: fixed;
|
||||
white-space: pre-wrap;
|
||||
padding: $ix-marg-b;
|
||||
width: calc(100% + #{$ix-marg-b});
|
||||
max-height: 300px;
|
||||
background-color: $g5-pepper;
|
||||
z-index: 100;
|
||||
border: solid 2px $c-pool;
|
||||
border: solid $ix-border $c-pool;
|
||||
color: $g19-ghost;
|
||||
border-radius: $radius;
|
||||
cursor: text;
|
||||
transform: translate(-$ix-border, -$ix-border);
|
||||
font-size: 12px;
|
||||
font-family: $code-font;
|
||||
}
|
||||
|
||||
|
||||
.expanded--dismiss {
|
||||
position: absolute;
|
||||
z-index: 5000;
|
||||
top: -15px;
|
||||
left: -10px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform: translate(-50%,-50%);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
outline: none;
|
||||
border-radius: 50%;
|
||||
background-color: $c-pool;
|
||||
|
@ -59,7 +52,7 @@
|
|||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 14px;
|
||||
width: 13px;
|
||||
height: 3px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
// Libraries
|
||||
import React, {Component, MouseEvent} from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
|
||||
// Components
|
||||
import {ClickOutside} from 'src/shared/components/ClickOutside'
|
||||
import LogsMessage from 'src/logs/components/logs_message/LogsMessage'
|
||||
|
||||
// Types
|
||||
import {NotificationAction} from 'src/types'
|
||||
|
||||
// Decorators
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface State {
|
||||
expanded: boolean
|
||||
}
|
||||
|
@ -16,49 +23,73 @@ interface Props {
|
|||
searchPattern: string
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
export class ExpandableMessage extends Component<Props, State> {
|
||||
constructor(props) {
|
||||
private containerRef: React.RefObject<HTMLDivElement>
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
this.containerRef = React.createRef()
|
||||
this.state = {
|
||||
expanded: false,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {notify, searchPattern} = this.props
|
||||
const formattedValue = `${this.props.formattedValue}`
|
||||
const trimmedValue = formattedValue.trimLeft()
|
||||
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.handleClickOutside}>
|
||||
<div onClick={this.handleClick} className="expandable--message">
|
||||
<div className="expandable--text">
|
||||
<LogsMessage
|
||||
formattedValue={trimmedValue}
|
||||
notify={notify}
|
||||
searchPattern={searchPattern}
|
||||
/>
|
||||
</div>
|
||||
<div className={this.isExpanded}>
|
||||
{this.closeExpansionButton}
|
||||
<LogsMessage
|
||||
formattedValue={formattedValue}
|
||||
notify={notify}
|
||||
searchPattern={searchPattern}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ClickOutside>
|
||||
<div
|
||||
onClick={this.handleClick}
|
||||
className="expandable--message"
|
||||
ref={this.containerRef}
|
||||
>
|
||||
{this.message}
|
||||
{this.expandedMessage}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get isExpanded() {
|
||||
private get message(): JSX.Element {
|
||||
const {notify, searchPattern, formattedValue} = this.props
|
||||
const valueString = `${formattedValue}`
|
||||
const trimmedValue = valueString.trimLeft()
|
||||
|
||||
return (
|
||||
<LogsMessage
|
||||
formattedValue={trimmedValue}
|
||||
notify={notify}
|
||||
searchPattern={searchPattern}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private get expandedMessage() {
|
||||
const {expanded} = this.state
|
||||
if (expanded) {
|
||||
return 'expanded--message'
|
||||
} else {
|
||||
return 'collapsed--message'
|
||||
|
||||
if (!expanded || !this.containerRef.current) {
|
||||
return null
|
||||
}
|
||||
|
||||
const portalElement = document.getElementById('expanded-message-container')
|
||||
const containerRect = this.containerRef.current.getBoundingClientRect()
|
||||
const padding = 8
|
||||
|
||||
const style = {
|
||||
top: containerRect.top - padding,
|
||||
left: containerRect.left - padding,
|
||||
width: containerRect.width + padding + padding,
|
||||
padding,
|
||||
}
|
||||
|
||||
const message = (
|
||||
<ClickOutside onClickOutside={this.handleClickOutside}>
|
||||
<div className="expanded--message" style={style}>
|
||||
{this.closeExpansionButton}
|
||||
{this.message}
|
||||
</div>
|
||||
</ClickOutside>
|
||||
)
|
||||
|
||||
return ReactDOM.createPortal(message, portalElement)
|
||||
}
|
||||
|
||||
private get closeExpansionButton(): JSX.Element {
|
||||
|
|
|
@ -13,8 +13,10 @@
|
|||
margin-left: $ix-marg-a;
|
||||
|
||||
> .icon {
|
||||
margin-right: $ix-marg-a;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-right: $ix-marg-a;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.logs-message:hover & {
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
addFilter,
|
||||
removeFilter,
|
||||
changeFilter,
|
||||
clearFilters,
|
||||
fetchOlderLogsAsync,
|
||||
fetchNewerLogsAsync,
|
||||
getLogConfigAsync,
|
||||
|
@ -93,6 +94,7 @@ interface Props {
|
|||
addFilter: (filter: Filter) => void
|
||||
removeFilter: (id: string) => void
|
||||
changeFilter: (id: string, operator: string, value: string) => void
|
||||
clearFilters: () => void
|
||||
getConfig: (url: string) => Promise<void>
|
||||
updateConfig: (url: string, config: LogConfig) => Promise<void>
|
||||
notify: NotificationAction
|
||||
|
@ -231,6 +233,9 @@ class LogsPage extends Component<Props, State> {
|
|||
filters={filters || []}
|
||||
onDelete={this.handleFilterDelete}
|
||||
onFilterChange={this.handleFilterChange}
|
||||
onClearFilters={this.handleClearFilters}
|
||||
onUpdateTruncation={this.handleUpdateTruncation}
|
||||
isTruncated={this.isTruncated}
|
||||
/>
|
||||
<LogsTable
|
||||
count={this.histogramTotal}
|
||||
|
@ -259,6 +264,7 @@ class LogsPage extends Component<Props, State> {
|
|||
</div>
|
||||
</div>
|
||||
{this.renderImportOverlay()}
|
||||
{this.expandedMessageContainer}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -603,6 +609,11 @@ class LogsPage extends Component<Props, State> {
|
|||
this.fetchSearchDataset(SearchStatus.UpdatingFilters)
|
||||
}
|
||||
|
||||
private handleClearFilters = async (): Promise<void> => {
|
||||
this.props.clearFilters()
|
||||
this.fetchSearchDataset(SearchStatus.UpdatingFilters)
|
||||
}
|
||||
|
||||
private handleBarClick = (time: string): void => {
|
||||
const formattedTime = moment(time).toISOString()
|
||||
|
||||
|
@ -681,8 +692,6 @@ class LogsPage extends Component<Props, State> {
|
|||
onUpdateColumns={this.handleUpdateColumns}
|
||||
onUpdateSeverityFormat={this.handleUpdateSeverityFormat}
|
||||
severityFormat={this.severityFormat}
|
||||
onUpdateTruncation={this.handleUpdateTruncation}
|
||||
isTruncated={this.isTruncated}
|
||||
/>
|
||||
</OverlayTechnology>
|
||||
)
|
||||
|
@ -742,6 +751,15 @@ class LogsPage extends Component<Props, State> {
|
|||
private get isTruncated(): boolean {
|
||||
return this.props.logConfig.isTruncated
|
||||
}
|
||||
|
||||
private get expandedMessageContainer(): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
className="logs-viewer--expanded-message-container"
|
||||
id="expanded-message-container"
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = ({
|
||||
|
@ -792,6 +810,7 @@ const mapDispatchToProps = {
|
|||
addFilter,
|
||||
removeFilter,
|
||||
changeFilter,
|
||||
clearFilters,
|
||||
fetchOlderLogsAsync,
|
||||
fetchNewerLogsAsync,
|
||||
setTableCustomTime: setTableCustomTimeAsync,
|
||||
|
|
|
@ -80,6 +80,10 @@ const addFilter = (state: LogsState, action: AddFilterAction): LogsState => {
|
|||
return {...state, filters: [..._.get(state, 'filters', []), filter]}
|
||||
}
|
||||
|
||||
const clearFilters = (state: LogsState): LogsState => {
|
||||
return {...state, filters: []}
|
||||
}
|
||||
|
||||
const changeFilter = (
|
||||
state: LogsState,
|
||||
action: ChangeFilterAction
|
||||
|
@ -230,6 +234,8 @@ export default (state: LogsState = defaultState, action: Action) => {
|
|||
return removeFilter(state, action)
|
||||
case ActionTypes.ChangeFilter:
|
||||
return changeFilter(state, action)
|
||||
case ActionTypes.ClearFilters:
|
||||
return clearFilters(state)
|
||||
case ActionTypes.IncrementQueryCount:
|
||||
return incrementQueryCount(state, action)
|
||||
case ActionTypes.DecrementQueryCount:
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
$logs-viewer-graph-height: 170px;
|
||||
$logs-viewer-search-height: 46px;
|
||||
$logs-viewer-filter-height: 42px;
|
||||
$logs-viewer-filter-height: 46px;
|
||||
$logs-viewer-results-text-indent: 33px;
|
||||
$logs-viewer-gutter: 60px;
|
||||
|
||||
|
@ -75,6 +75,26 @@ $logs-viewer-gutter: 60px;
|
|||
background-color: $g3-castle;
|
||||
}
|
||||
|
||||
.logs-viewer--filters {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
width: 44px;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
@include gradient-h(rgba($g3-castle, 0), $g3-castle);
|
||||
}
|
||||
}
|
||||
|
||||
.logs-viewer--clear-filters {
|
||||
padding: 0 $ix-marg-b;
|
||||
}
|
||||
|
||||
.logs-viewer--results-text {
|
||||
margin: 0;
|
||||
margin-right: $logs-viewer-results-text-indent;
|
||||
|
@ -137,6 +157,7 @@ $logs-viewer-gutter: 60px;
|
|||
font-weight: 500;
|
||||
font-family: $code-font;
|
||||
margin: 1px;
|
||||
max-width: 289px;
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
|
@ -146,6 +167,13 @@ $logs-viewer-gutter: 60px;
|
|||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
> span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
.logs-viewer--filter-remove {
|
||||
|
|
Loading…
Reference in New Issue