Merge pull request #3529 from influxdata/log-viewer/placeholder-markup
Log Viewer Search & Filtering UIpull/10616/head
commit
955761ec3b
|
@ -26,7 +26,12 @@ class LogViewerHeader extends PureComponent<Props> {
|
|||
public render(): JSX.Element {
|
||||
const {timeRange} = this.props
|
||||
return (
|
||||
<>
|
||||
<div className="page-header full-width">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">Log Viewer</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<Dropdown
|
||||
className="dropdown-300"
|
||||
items={this.sourceDropDownItems}
|
||||
|
@ -34,7 +39,8 @@ class LogViewerHeader extends PureComponent<Props> {
|
|||
onChoose={this.handleChooseSource}
|
||||
/>
|
||||
<Dropdown
|
||||
className="dropdown-300"
|
||||
className="dropdown-180"
|
||||
iconName="disks"
|
||||
items={this.namespaceDropDownItems}
|
||||
selected={this.selectedNamespace}
|
||||
onChoose={this.handleChooseNamespace}
|
||||
|
@ -43,7 +49,9 @@ class LogViewerHeader extends PureComponent<Props> {
|
|||
onChooseTimeRange={this.handleChooseTimeRange}
|
||||
selected={timeRange}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import {Filter} from 'src/logs/containers/LogsPage'
|
||||
|
||||
interface Props {
|
||||
filter: Filter
|
||||
onDelete: (id: string) => () => void
|
||||
onToggleStatus: (id: string) => () => void
|
||||
onToggleOperator: (id: string) => () => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
expanded: boolean
|
||||
}
|
||||
|
||||
class LogsFilter extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
expanded: false,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
filter: {id},
|
||||
onDelete,
|
||||
} = this.props
|
||||
const {expanded} = this.state
|
||||
|
||||
return (
|
||||
<li className={this.className} onMouseLeave={this.handleMouseLeave}>
|
||||
{this.label}
|
||||
<div className="logs-viewer--filter-remove" onClick={onDelete(id)} />
|
||||
{expanded && this.renderTooltip}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
private get label(): JSX.Element {
|
||||
const {
|
||||
filter: {key, operator, value},
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<span
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
>{`${key} ${operator} ${value}`}</span>
|
||||
)
|
||||
}
|
||||
|
||||
private get className(): string {
|
||||
const {expanded} = this.state
|
||||
const {
|
||||
filter: {enabled},
|
||||
} = this.props
|
||||
|
||||
return classnames('logs-viewer--filter', {
|
||||
active: expanded,
|
||||
disabled: !enabled,
|
||||
})
|
||||
}
|
||||
|
||||
private handleMouseEnter = (): void => {
|
||||
this.setState({expanded: true})
|
||||
}
|
||||
|
||||
private handleMouseLeave = (): void => {
|
||||
this.setState({expanded: false})
|
||||
}
|
||||
|
||||
private get renderTooltip(): JSX.Element {
|
||||
const {
|
||||
filter: {id, enabled, operator},
|
||||
onDelete,
|
||||
onToggleStatus,
|
||||
onToggleOperator,
|
||||
} = this.props
|
||||
|
||||
const toggleStatusText = enabled ? 'Disable' : 'Enable'
|
||||
const toggleOperatorText = operator === '==' ? '!=' : '=='
|
||||
|
||||
return (
|
||||
<ul className="logs-viewer--filter-tooltip">
|
||||
<li onClick={onToggleStatus(id)}>{toggleStatusText}</li>
|
||||
<li onClick={onToggleOperator(id)}>{toggleOperatorText}</li>
|
||||
<li onClick={onDelete(id)}>Delete</li>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default LogsFilter
|
|
@ -0,0 +1,84 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import {Filter} from 'src/logs/containers/LogsPage'
|
||||
import FilterBlock from 'src/logs/components/LogsFilter'
|
||||
|
||||
interface Props {
|
||||
numResults: number
|
||||
filters: Filter[]
|
||||
onUpdateFilters: (fitlers: Filter[]) => void
|
||||
}
|
||||
|
||||
class LogsFilters extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {numResults} = this.props
|
||||
|
||||
return (
|
||||
<div className="logs-viewer--filter-bar">
|
||||
<label className="logs-viewer--results-text">
|
||||
Query returned <strong>{numResults} Events</strong>
|
||||
</label>
|
||||
<ul className="logs-viewer--filters">{this.renderFilters}</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get renderFilters(): JSX.Element[] {
|
||||
const {filters} = this.props
|
||||
|
||||
return filters.map(filter => (
|
||||
<FilterBlock
|
||||
key={filter.id}
|
||||
filter={filter}
|
||||
onDelete={this.handleDeleteFilter}
|
||||
onToggleStatus={this.handleToggleFilterStatus}
|
||||
onToggleOperator={this.handleToggleFilterOperator}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
private handleDeleteFilter = (id: string) => (): void => {
|
||||
const {filters, onUpdateFilters} = this.props
|
||||
|
||||
const filteredFilters = filters.filter(filter => filter.id !== id)
|
||||
|
||||
onUpdateFilters(filteredFilters)
|
||||
}
|
||||
|
||||
private handleToggleFilterStatus = (id: string) => (): void => {
|
||||
const {filters, onUpdateFilters} = this.props
|
||||
|
||||
const filteredFilters = filters.map(filter => {
|
||||
if (filter.id === id) {
|
||||
return {...filter, enabled: !filter.enabled}
|
||||
}
|
||||
|
||||
return filter
|
||||
})
|
||||
|
||||
onUpdateFilters(filteredFilters)
|
||||
}
|
||||
|
||||
private handleToggleFilterOperator = (id: string) => (): void => {
|
||||
const {filters, onUpdateFilters} = this.props
|
||||
|
||||
const filteredFilters = filters.map(filter => {
|
||||
if (filter.id === id) {
|
||||
return {...filter, operator: this.toggleOperator(filter.operator)}
|
||||
}
|
||||
|
||||
return filter
|
||||
})
|
||||
|
||||
onUpdateFilters(filteredFilters)
|
||||
}
|
||||
|
||||
private toggleOperator = (op: string): string => {
|
||||
if (op === '==') {
|
||||
return '!='
|
||||
}
|
||||
|
||||
return '=='
|
||||
}
|
||||
}
|
||||
|
||||
export default LogsFilters
|
|
@ -0,0 +1,43 @@
|
|||
import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react'
|
||||
|
||||
interface Props {
|
||||
searchString: string
|
||||
onChange: (e: ChangeEvent<HTMLInputElement>) => void
|
||||
onSearch: () => void
|
||||
}
|
||||
|
||||
class LogsSearchBar extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {searchString, onSearch, onChange} = this.props
|
||||
|
||||
return (
|
||||
<div className="logs-viewer--search-bar">
|
||||
<div className="logs-viewer--search-input">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search logs using Keywords or Regular Expressions..."
|
||||
value={searchString}
|
||||
onChange={onChange}
|
||||
onKeyDown={this.handleInputKeyDown}
|
||||
className="form-control input-sm"
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<span className="icon search" />
|
||||
</div>
|
||||
<button className="btn btn-sm btn-primary" onClick={onSearch}>
|
||||
<span className="icon search" />
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleInputKeyDown = (e: KeyboardEvent<HTMLInputElement>): void => {
|
||||
if (e.key === 'Enter') {
|
||||
return this.props.onSearch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default LogsSearchBar
|
|
@ -0,0 +1,17 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
|
||||
interface Props {
|
||||
thing: string
|
||||
}
|
||||
|
||||
class LogsTableContainer extends PureComponent<Props> {
|
||||
public render() {
|
||||
return (
|
||||
<div className="logs-viewer--table-container">
|
||||
<p>{this.props.thing}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default LogsTableContainer
|
|
@ -1,22 +0,0 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
|
||||
interface Props {
|
||||
thing: string
|
||||
}
|
||||
|
||||
class LogsTableContainer extends PureComponent<Props> {
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<div className="logs-viewer--search-container">
|
||||
<p>search</p>
|
||||
</div>
|
||||
<div className="logs-viewer--table-container">
|
||||
<p>{this.props.thing}</p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default LogsTableContainer
|
|
@ -1,6 +1,7 @@
|
|||
import React, {Component} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import moment from 'moment'
|
||||
import _ from 'lodash'
|
||||
|
||||
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
||||
import timeRanges from 'src/logs/data/timeRanges'
|
||||
|
@ -56,16 +57,12 @@ class TimeRangeDropdown extends Component<Props, State> {
|
|||
|
||||
public render() {
|
||||
const {selected, preventCustomTimeRange, page} = this.props
|
||||
const {customTimeRange, isCustomTimeRangeOpen, isOpen} = this.state
|
||||
const {customTimeRange, isCustomTimeRangeOpen} = this.state
|
||||
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.handleClickOutside}>
|
||||
<div className="time-range-dropdown">
|
||||
<div
|
||||
className={classnames('dropdown dropdown-290', {
|
||||
open: isOpen,
|
||||
})}
|
||||
>
|
||||
<div className={this.dropdownClassName}>
|
||||
<div
|
||||
className="btn btn-sm btn-default dropdown-toggle"
|
||||
onClick={this.toggleMenu}
|
||||
|
@ -132,6 +129,21 @@ class TimeRangeDropdown extends Component<Props, State> {
|
|||
)
|
||||
}
|
||||
|
||||
private get dropdownClassName(): string {
|
||||
const {
|
||||
isOpen,
|
||||
customTimeRange: {lower, upper},
|
||||
} = this.state
|
||||
|
||||
const absoluteTimeRange = !_.isEmpty(lower) && !_.isEmpty(upper)
|
||||
|
||||
return classnames('dropdown', {
|
||||
'dropdown-290': absoluteTimeRange,
|
||||
'dropdown-120': !absoluteTimeRange,
|
||||
open: isOpen,
|
||||
})
|
||||
}
|
||||
|
||||
private findTimeRangeInputValue = ({upper, lower}: TimeRange) => {
|
||||
if (upper && lower) {
|
||||
if (upper === 'now()') {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import React, {PureComponent, ChangeEvent} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import {
|
||||
getSourceAndPopulateNamespacesAsync,
|
||||
|
@ -9,12 +9,22 @@ import {
|
|||
} from 'src/logs/actions'
|
||||
import {getSourcesAsync} from 'src/shared/actions/sources'
|
||||
import LogViewerHeader from 'src/logs/components/LogViewerHeader'
|
||||
import Graph from 'src/logs/components/LogsGraph'
|
||||
import Table from 'src/logs/components/LogsTable'
|
||||
import SearchBar from 'src/logs/components/LogsSearchBar'
|
||||
import FilterBar from 'src/logs/components/LogsFilterBar'
|
||||
import LogViewerChart from 'src/logs/components/LogViewerChart'
|
||||
import GraphContainer from 'src/logs/components/LogsGraphContainer'
|
||||
import TableContainer from 'src/logs/components/LogsTableContainer'
|
||||
|
||||
import {Source, Namespace, TimeRange} from 'src/types'
|
||||
|
||||
export interface Filter {
|
||||
id: string
|
||||
key: string
|
||||
value: string
|
||||
operator: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
interface Props {
|
||||
sources: Source[]
|
||||
currentSource: Source | null
|
||||
|
@ -30,7 +40,31 @@ interface Props {
|
|||
histogramData: object[]
|
||||
}
|
||||
|
||||
class LogsPage extends PureComponent<Props> {
|
||||
interface State {
|
||||
searchString: string
|
||||
filters: Filter[]
|
||||
}
|
||||
|
||||
const DUMMY_FILTERS = [
|
||||
{
|
||||
id: '0',
|
||||
key: 'host',
|
||||
value: 'prod1-rsavage.local',
|
||||
operator: '==',
|
||||
enabled: true,
|
||||
},
|
||||
]
|
||||
|
||||
class LogsPage extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
searchString: '',
|
||||
filters: DUMMY_FILTERS,
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
if (!this.props.currentSource) {
|
||||
this.props.getSource(this.props.sources[0].id)
|
||||
|
@ -42,19 +76,24 @@ class LogsPage extends PureComponent<Props> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const {searchString, filters} = this.state
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<div className="page-header full-width">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">Log Viewer</h1>
|
||||
</div>
|
||||
<div className="page-header__right">{this.header}</div>
|
||||
</div>
|
||||
</div>
|
||||
{this.header}
|
||||
<div className="page-contents logs-viewer">
|
||||
<GraphContainer>{this.chart}</GraphContainer>
|
||||
<TableContainer thing="snooo" />
|
||||
<Graph>{this.chart}</Graph>
|
||||
<SearchBar
|
||||
searchString={searchString}
|
||||
onChange={this.handleSearchInputChange}
|
||||
onSearch={this.handleSubmitSearch}
|
||||
/>
|
||||
<FilterBar
|
||||
numResults={300}
|
||||
filters={filters}
|
||||
onUpdateFilters={this.handleUpdateFilters}
|
||||
/>
|
||||
<Table thing="snooo" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -94,6 +133,20 @@ class LogsPage extends PureComponent<Props> {
|
|||
)
|
||||
}
|
||||
|
||||
private handleSearchInputChange = (
|
||||
e: ChangeEvent<HTMLInputElement>
|
||||
): void => {
|
||||
this.setState({searchString: e.target.value})
|
||||
}
|
||||
|
||||
private handleSubmitSearch = (): void => {
|
||||
// do the thing
|
||||
}
|
||||
|
||||
private handleUpdateFilters = (filters: Filter[]): void => {
|
||||
this.setState({filters})
|
||||
}
|
||||
|
||||
private handleChooseTimerange = (timeRange: TimeRange) => {
|
||||
this.props.setTimeRangeAsync(timeRange)
|
||||
this.props.executeHistogramQueryAsync()
|
||||
|
|
|
@ -174,7 +174,7 @@
|
|||
min-width: 350px;
|
||||
user-select: text;
|
||||
transform: translateX(-50%);
|
||||
box-shadow: 0 0 10px 2px $g2-kevlar;
|
||||
@extend %drop-shadow;
|
||||
|
||||
&.hidden {
|
||||
display: none !important;
|
||||
|
|
|
@ -125,3 +125,8 @@ $scrollbar-offset: 3px;
|
|||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
// Shadows
|
||||
%drop-shadow {
|
||||
box-shadow: 0 0 10px 2px $g2-kevlar;
|
||||
}
|
||||
|
|
|
@ -4,14 +4,15 @@
|
|||
*/
|
||||
|
||||
$logs-viewer-graph-height: 240px;
|
||||
$logs-viewer-search-height: 108px;
|
||||
$logs-viewer-search-height: 46px;
|
||||
$logs-viewer-filter-height: 42px;
|
||||
$logs-viewer-gutter: 60px;
|
||||
|
||||
.logs-viewer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
flex-wrap: none;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.logs-viewer--graph-container {
|
||||
|
@ -21,18 +22,180 @@ $logs-viewer-gutter: 60px;
|
|||
display: flex;
|
||||
}
|
||||
|
||||
.logs-viewer--search-container {
|
||||
padding: 20px $logs-viewer-gutter;
|
||||
.logs-viewer--search-bar {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
flex-wrap: nowrap;
|
||||
padding: 0 $logs-viewer-gutter;
|
||||
height: $logs-viewer-search-height;
|
||||
background-color: $g3-castle;
|
||||
}
|
||||
|
||||
.logs-viewer--table-container {
|
||||
padding: 12px $logs-viewer-gutter 30px $logs-viewer-gutter;
|
||||
height: calc(100% - #{$logs-viewer-graph-height + $logs-viewer-search-height});
|
||||
height: calc(100% - #{$logs-viewer-graph-height + $logs-viewer-search-height + $logs-viewer-filter-height});
|
||||
background-color: $g3-castle;
|
||||
}
|
||||
|
||||
// Search Bar
|
||||
.logs-viewer--search-input {
|
||||
flex: 1 0 0;
|
||||
margin-right: 8px;
|
||||
position: relative;
|
||||
|
||||
> span.icon.search {
|
||||
font-size: 14px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 12px;
|
||||
transform: translateY(-50%);
|
||||
color: $g8-storm;
|
||||
transition: color 0.25s ease;
|
||||
}
|
||||
|
||||
> input.form-control.input-sm {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
> input.form-control.input-sm:focus + span.icon.search {
|
||||
color: $c-pool;
|
||||
}
|
||||
}
|
||||
|
||||
// Filters Bar
|
||||
.logs-viewer--filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@include no-user-select();
|
||||
padding: 0 $logs-viewer-gutter;
|
||||
height: $logs-viewer-filter-height;
|
||||
background-color: $g3-castle;
|
||||
}
|
||||
|
||||
.logs-viewer--results-text {
|
||||
margin: 0 12px 0 33px;
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
line-height: 13px;
|
||||
font-weight: 500;
|
||||
color: $g9-mountain;
|
||||
|
||||
strong {
|
||||
color: $g15-platinum;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.logs-viewer--filters {
|
||||
flex: 1 0 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logs-viewer--filter {
|
||||
position: relative;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
list-style: none;
|
||||
padding: 0 2px 0 8px;
|
||||
height: 26px;
|
||||
border-radius: 4px;
|
||||
background-color: $g5-pepper;
|
||||
color: $g13-mist;
|
||||
font-weight: 600;
|
||||
margin: 2px;
|
||||
|
||||
&.disabled {
|
||||
background-color: $g4-onyx;
|
||||
color: $g9-mountain;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $g6-smoke;
|
||||
color: $g15-platinum;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.logs-viewer--filter-remove {
|
||||
outline: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
position: relative;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 12px;
|
||||
height: 2px;
|
||||
border-radius: 1px;
|
||||
background-color: $g8-storm;
|
||||
transition: background-color 0.25s ease;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&:before {
|
||||
transform: translate(-50%, -50%) rotate(-45deg);
|
||||
}
|
||||
|
||||
&:after {
|
||||
transform: translate(-50%, -50%) rotate(45deg);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
background-color: $c-dreamsicle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logs-viewer--filter-tooltip {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
border-radius: $radius;
|
||||
z-index: 9999;
|
||||
@extend %drop-shadow;
|
||||
background-color: $g4-onyx;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
overflow: hidden;
|
||||
|
||||
> li {
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
padding: 0 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: $g11-sidewalk;
|
||||
transition: background-color 0.25s ease, color 0.25s ease;
|
||||
margin: 0;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: $g5-pepper;
|
||||
color: $g18-cloud;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Graph
|
||||
.logs-viewer--graph {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
|
Loading…
Reference in New Issue