Merge pull request #3508 from influxdata/features/log-viewer-scaffold

Add log viewer page with header
pull/10616/head
Brandon Farmer 2018-05-23 12:27:27 -07:00 committed by GitHub
commit f362d2046d
13 changed files with 654 additions and 26 deletions

View File

@ -26,6 +26,7 @@ import {StatusPage} from 'src/status'
import {HostsPage, HostPage} from 'src/hosts'
import DataExplorerPage from 'src/data_explorer'
import {DashboardsPage, DashboardPage} from 'src/dashboards'
import {LogsPage} from 'src/logs'
import AlertsApp from 'src/alerts'
import {
KapacitorPage,
@ -119,6 +120,9 @@ class Root extends PureComponent<{}, State> {
<Route path="/" component={UserIsAuthenticated(CheckSources)} />
<Route path="/login" component={UserIsNotAuthenticated(Login)} />
<Route path="/purgatory" component={UserIsAuthenticated(Purgatory)} />
<Route component={UserIsAuthenticated(App)}>
<Route path="/logs" component={LogsPage} />
</Route>
<Route
path="/sources/new"
component={UserIsAuthenticated(SourcePage)}

View File

@ -0,0 +1,93 @@
import {Source, Namespace, TimeRange} from 'src/types'
import {getSource} from 'src/shared/apis'
import {getDatabasesWithRetentionPolicies} from 'src/shared/apis/databases'
import {get} from 'src/utils/wrappers'
export enum ActionTypes {
SetSource = 'LOGS_SET_SOURCE',
SetNamespaces = 'LOGS_SET_NAMESPACES',
SetTimeRange = 'LOGS_SET_TIMERANGE',
SetNamespace = 'LOGS_SET_NAMESPACE',
}
interface SetSourceAction {
type: ActionTypes.SetSource
payload: {
source: Source
}
}
interface SetNamespacesAction {
type: ActionTypes.SetNamespaces
payload: {
namespaces: Namespace[]
}
}
interface SetNamespaceAction {
type: ActionTypes.SetNamespace
payload: {
namespace: Namespace
}
}
interface SetTimeRangeAction {
type: ActionTypes.SetTimeRange
payload: {
timeRange: TimeRange
}
}
export type Action =
| SetSourceAction
| SetNamespacesAction
| SetTimeRangeAction
| SetNamespaceAction
export const setSource = (source: Source): SetSourceAction => ({
type: ActionTypes.SetSource,
payload: {
source,
},
})
export const setNamespace = (namespace: Namespace): SetNamespaceAction => ({
type: ActionTypes.SetNamespace,
payload: {
namespace,
},
})
export const setNamespaces = (
namespaces: Namespace[]
): SetNamespacesAction => ({
type: ActionTypes.SetNamespaces,
payload: {
namespaces,
},
})
export const setTimeRange = (timeRange: TimeRange): SetTimeRangeAction => ({
type: ActionTypes.SetTimeRange,
payload: {
timeRange,
},
})
export const getSourceAsync = (sourceID: string) => async dispatch => {
const response = await getSource(sourceID)
const source = response.data
if (source) {
const namespaces = await getDatabasesWithRetentionPolicies(
get(source, 'links.proxy', '')
)
if (namespaces && namespaces.length > 0) {
dispatch(setNamespace(namespaces[0]))
}
dispatch(setNamespaces(namespaces))
dispatch(setSource(source))
}
}

View File

@ -0,0 +1,103 @@
import _ from 'lodash'
import React, {PureComponent} from 'react'
import {Source, Namespace} from 'src/types'
import Dropdown from 'src/shared/components/Dropdown'
import TimeRangeDropdown from 'src/logs/components/TimeRangeDropdown'
import {TimeRange} from 'src/types'
interface SourceItem {
id: string
text: string
}
interface Props {
currentNamespace: Namespace
availableSources: Source[]
currentSource: Source | null
currentNamespaces: Namespace[]
timeRange: TimeRange
onChooseSource: (sourceID: string) => void
onChooseNamespace: (namespace: Namespace) => void
onChooseTimerange: (timeRange: TimeRange) => void
}
class LogViewerHeader extends PureComponent<Props> {
public render(): JSX.Element {
const {timeRange} = this.props
return (
<>
<Dropdown
className="dropdown-300"
items={this.sourceDropDownItems}
selected={this.selectedSource}
onChoose={this.handleChooseSource}
/>
<Dropdown
className="dropdown-300"
items={this.namespaceDropDownItems}
selected={this.selectedNamespace}
onChoose={this.handleChooseNamespace}
/>
<TimeRangeDropdown
onChooseTimeRange={this.handleChooseTimeRange}
selected={timeRange}
/>
</>
)
}
private handleChooseTimeRange = (timerange: TimeRange) => {
this.props.onChooseTimerange(timerange)
}
private handleChooseSource = (item: SourceItem) => {
this.props.onChooseSource(item.id)
}
private handleChooseNamespace = (namespace: Namespace) => {
this.props.onChooseNamespace(namespace)
}
private get selectedSource(): string {
if (_.isEmpty(this.sourceDropDownItems)) {
return ''
}
return this.sourceDropDownItems[0].text
}
private get selectedNamespace(): string {
const {currentNamespace} = this.props
if (!currentNamespace) {
return ''
}
return `${currentNamespace.database}.${currentNamespace.retentionPolicy}`
}
private get namespaceDropDownItems() {
const {currentNamespaces} = this.props
return currentNamespaces.map(namespace => {
return {
text: `${namespace.database}.${namespace.retentionPolicy}`,
...namespace,
}
})
}
private get sourceDropDownItems(): SourceItem[] {
const {availableSources} = this.props
return availableSources.map(source => {
return {
text: `${source.name} @ ${source.url}`,
id: source.id,
}
})
}
}
export default LogViewerHeader

View File

@ -0,0 +1,174 @@
import React, {Component} from 'react'
import classnames from 'classnames'
import moment from 'moment'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import timeRanges from 'src/logs/data/timeRanges'
import {DROPDOWN_MENU_MAX_HEIGHT} from 'src/shared/constants/index'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {ClickOutside} from 'src/shared/components/ClickOutside'
import CustomTimeRange from 'src/shared/components/CustomTimeRange'
import {TimeRange} from 'src/types'
const dateFormat = 'YYYY-MM-DD HH:mm'
const emptyTime = {lower: '', upper: ''}
const format = t => moment(t.replace(/\'/g, '')).format(dateFormat)
interface Props {
selected: {
lower: string
upper?: string
}
onChooseTimeRange: (timeRange: TimeRange) => void
preventCustomTimeRange?: boolean
page?: string
}
interface State {
autobind: boolean
isOpen: boolean
isCustomTimeRangeOpen: boolean
customTimeRange: TimeRange
}
@ErrorHandling
class TimeRangeDropdown extends Component<Props, State> {
public static defaultProps = {
page: 'default',
}
constructor(props) {
super(props)
const {lower, upper} = props.selected
const isTimeValid = moment(upper).isValid() && moment(lower).isValid()
const customTimeRange = isTimeValid ? {lower, upper} : emptyTime
this.state = {
autobind: false,
isOpen: false,
isCustomTimeRangeOpen: false,
customTimeRange,
}
}
public render() {
const {selected, preventCustomTimeRange, page} = this.props
const {customTimeRange, isCustomTimeRangeOpen, isOpen} = this.state
return (
<ClickOutside onClickOutside={this.handleClickOutside}>
<div className="time-range-dropdown">
<div
className={classnames('dropdown dropdown-290', {
open: isOpen,
})}
>
<div
className="btn btn-sm btn-default dropdown-toggle"
onClick={this.toggleMenu}
>
<span className="icon clock" />
<span className="dropdown-selected">
{this.findTimeRangeInputValue(selected)}
</span>
<span className="caret" />
</div>
<ul className="dropdown-menu">
<FancyScrollbar
autoHide={false}
autoHeight={true}
maxHeight={DROPDOWN_MENU_MAX_HEIGHT}
>
{preventCustomTimeRange ? null : (
<div>
<li className="dropdown-header">Absolute Time</li>
<li
className={
isCustomTimeRangeOpen
? 'active dropdown-item custom-timerange'
: 'dropdown-item custom-timerange'
}
>
<a href="#" onClick={this.showCustomTimeRange}>
Date Picker
</a>
</li>
</div>
)}
<li className="dropdown-header">
{preventCustomTimeRange ? '' : 'Relative '}Time
</li>
{timeRanges.map(item => {
return (
<li className="dropdown-item" key={item.menuOption}>
<a href="#" onClick={this.handleSelection(item)}>
{item.menuOption}
</a>
</li>
)
})}
</FancyScrollbar>
</ul>
</div>
{isCustomTimeRangeOpen ? (
<ClickOutside onClickOutside={this.handleCloseCustomTimeRange}>
<div className="custom-time--overlay">
<CustomTimeRange
onApplyTimeRange={this.handleApplyCustomTimeRange}
timeRange={customTimeRange}
onClose={this.handleCloseCustomTimeRange}
isVisible={isCustomTimeRangeOpen}
timeInterval={60}
page={page}
/>
</div>
</ClickOutside>
) : null}
</div>
</ClickOutside>
)
}
private findTimeRangeInputValue = ({upper, lower}: TimeRange) => {
if (upper && lower) {
if (upper === 'now()') {
return `${format(lower)} - Now`
}
return `${format(lower)} - ${format(upper)}`
}
const selected = timeRanges.find(range => range.lower === lower)
return selected ? selected.inputValue : 'Custom'
}
private handleClickOutside = () => {
this.setState({isOpen: false})
}
private handleSelection = timeRange => () => {
this.props.onChooseTimeRange(timeRange)
this.setState({customTimeRange: emptyTime, isOpen: false})
}
private toggleMenu = () => {
this.setState({isOpen: !this.state.isOpen})
}
private showCustomTimeRange = () => {
this.setState({isCustomTimeRangeOpen: true})
}
private handleApplyCustomTimeRange = customTimeRange => {
this.props.onChooseTimeRange({...customTimeRange})
this.setState({customTimeRange, isOpen: false})
}
private handleCloseCustomTimeRange = () => {
this.setState({isCustomTimeRangeOpen: false})
}
}
export default TimeRangeDropdown

View File

@ -0,0 +1,101 @@
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
import {getSourceAsync, setTimeRange, setNamespace} from 'src/logs/actions'
import {getSourcesAsync} from 'src/shared/actions/sources'
import {Source, Namespace, TimeRange} from 'src/types'
import LogViewerHeader from 'src/logs/components/LogViewerHeader'
interface Props {
sources: Source[]
currentSource: Source | null
currentNamespaces: Namespace[]
currentNamespace: Namespace
getSource: (sourceID: string) => void
getSources: () => void
setTimeRange: (timeRange: TimeRange) => void
setNamespace: (namespace: Namespace) => void
timeRange: TimeRange
}
class LogsPage extends PureComponent<Props> {
public componentDidUpdate() {
if (!this.props.currentSource) {
this.props.getSource(this.props.sources[0].id)
}
}
public componentDidMount() {
this.props.getSources()
}
public render() {
return (
<div className="page hosts-list-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>
</div>
)
}
private get header(): JSX.Element {
const {
sources,
currentSource,
currentNamespaces,
timeRange,
currentNamespace,
} = this.props
return (
<LogViewerHeader
availableSources={sources}
timeRange={timeRange}
onChooseSource={this.handleChooseSource}
onChooseNamespace={this.handleChooseNamespace}
onChooseTimerange={this.handleChooseTimerange}
currentSource={currentSource}
currentNamespaces={currentNamespaces}
currentNamespace={currentNamespace}
/>
)
}
private handleChooseTimerange = (timeRange: TimeRange) => {
this.props.setTimeRange(timeRange)
}
private handleChooseSource = (sourceID: string) => {
this.props.getSource(sourceID)
}
private handleChooseNamespace = (namespace: Namespace) => {
// Do flip
this.props.setNamespace(namespace)
}
}
const mapStateToProps = ({
sources,
logs: {currentSource, currentNamespaces, timeRange, currentNamespace},
}) => ({
sources,
currentSource,
currentNamespaces,
timeRange,
currentNamespace,
})
const mapDispatchToProps = {
getSource: getSourceAsync,
getSources: getSourcesAsync,
setTimeRange,
setNamespace,
}
export default connect(mapStateToProps, mapDispatchToProps)(LogsPage)

View File

@ -0,0 +1,74 @@
export default [
{
defaultGroupBy: '10s',
seconds: 60,
inputValue: 'Past 1m',
lower: 'now() - 1m',
upper: null,
menuOption: 'Past 1m',
},
{
defaultGroupBy: '10s',
seconds: 300,
inputValue: 'Past 5m',
lower: 'now() - 5m',
upper: null,
menuOption: 'Past 5m',
},
{
defaultGroupBy: '1m',
seconds: 900,
inputValue: 'Past 15m',
lower: 'now() - 15m',
upper: null,
menuOption: 'Past 15m',
},
{
defaultGroupBy: '1m',
seconds: 1800,
inputValue: 'Past 30m',
lower: 'now() - 30m',
upper: null,
menuOption: 'Past 30m',
},
{
defaultGroupBy: '1m',
seconds: 3600,
inputValue: 'Past 1h',
lower: 'now() - 1h',
upper: null,
menuOption: 'Past 1h',
},
{
defaultGroupBy: '1m',
seconds: 5200,
inputValue: 'Past 2h',
lower: 'now() - 2h',
upper: null,
menuOption: 'Past 2h',
},
{
defaultGroupBy: '1m',
seconds: 21600,
inputValue: 'Past 6h',
lower: 'now() - 6h',
upper: null,
menuOption: 'Past 6h',
},
{
defaultGroupBy: '5m',
seconds: 43200,
inputValue: 'Past 12h',
lower: 'now() - 12h',
upper: null,
menuOption: 'Past 12h',
},
{
defaultGroupBy: '10m',
seconds: 86400,
inputValue: 'Past 24h',
lower: 'now() - 24h',
upper: null,
menuOption: 'Past 24h',
},
]

3
ui/src/logs/index.ts Normal file
View File

@ -0,0 +1,3 @@
import LogsPage from 'src/logs/containers/LogsPage'
export {LogsPage}

View File

@ -0,0 +1,31 @@
import {Source, Namespace, TimeRange} from 'src/types'
import {ActionTypes, Action} from 'src/logs/actions'
interface LogsState {
currentSource: Source | null
currentNamespaces: Namespace[]
currentNamespace: Namespace | null
timeRange: TimeRange
}
const defaultState = {
currentSource: null,
currentNamespaces: [],
timeRange: {lower: 'now() - 1m', upper: null},
currentNamespace: null,
}
export default (state: LogsState = defaultState, action: Action) => {
switch (action.type) {
case ActionTypes.SetSource:
return {...state, currentSource: action.payload.source}
case ActionTypes.SetNamespaces:
return {...state, currentNamespaces: action.payload.namespaces}
case ActionTypes.SetTimeRange:
return {...state, timeRange: action.payload.timeRange}
case ActionTypes.SetNamespace:
return {...state, currentNamespace: action.payload.namespace}
default:
return state
}
}

View File

@ -0,0 +1,35 @@
import _ from 'lodash'
import {showDatabases, showRetentionPolicies} from 'src/shared/apis/metaQuery'
import showDatabasesParser from 'src/shared/parsing/showDatabases'
import showRetentionPoliciesParser from 'src/shared/parsing/showRetentionPolicies'
import {Namespace} from 'src/types/query'
export const getDatabasesWithRetentionPolicies = async (
proxy: string
): Promise<Namespace[]> => {
try {
const {data} = await showDatabases(proxy)
const {databases} = showDatabasesParser(data)
const rps = await showRetentionPolicies(proxy, databases)
const namespaces = rps.data.results.reduce((acc, result, index) => {
const {retentionPolicies} = showRetentionPoliciesParser(result)
const dbrp = retentionPolicies.map(rp => ({
database: databases[index],
retentionPolicy: rp.name,
}))
return [...acc, ...dbrp]
}, [])
const sorted = _.sortBy(namespaces, ({database}: Namespace) =>
database.toLowerCase()
)
return sorted
} catch (err) {
console.error(err)
}
}

View File

@ -17,7 +17,7 @@ class CustomTimeRange extends Component {
}
componentDidMount() {
const {timeRange} = this.props
const {timeRange, timeInterval} = this.props
const lower = rome(this.lower, {
dateValidator: rome.val.beforeEq(this.upper),
@ -26,6 +26,7 @@ class CustomTimeRange extends Component {
autoClose: false,
autoHideOnBlur: false,
autoHideOnClick: false,
timeInterval,
})
const upper = rome(this.upper, {
@ -35,6 +36,7 @@ class CustomTimeRange extends Component {
initialValue: this.getInitialDate(timeRange.upper),
autoHideOnBlur: false,
autoHideOnClick: false,
timeInterval,
})
this.lowerCal = lower
@ -239,7 +241,11 @@ class CustomTimeRange extends Component {
}
}
const {func, shape, string} = PropTypes
CustomTimeRange.defaultProps = {
timeInterval: 1800,
}
const {func, shape, string, number} = PropTypes
CustomTimeRange.propTypes = {
onApplyTimeRange: func.isRequired,
@ -247,6 +253,7 @@ CustomTimeRange.propTypes = {
lower: string.isRequired,
upper: string,
}).isRequired,
timeInterval: number,
onClose: func,
page: string,
}

View File

@ -6,14 +6,12 @@ import _ from 'lodash'
import {QueryConfig, Source} from 'src/types'
import {Namespace} from 'src/types/query'
import {showDatabases, showRetentionPolicies} from 'src/shared/apis/metaQuery'
import showDatabasesParser from 'src/shared/parsing/showDatabases'
import showRetentionPoliciesParser from 'src/shared/parsing/showRetentionPolicies'
import DatabaseListItem from 'src/shared/components/DatabaseListItem'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {getDatabasesWithRetentionPolicies} from 'src/shared/apis/databases'
interface DatabaseListProps {
query: QueryConfig
querySource?: Source
@ -80,24 +78,7 @@ class DatabaseList extends Component<DatabaseListProps, DatabaseListState> {
const proxy = _.get(querySource, ['links', 'proxy'], source.links.proxy)
try {
const {data} = await showDatabases(proxy)
const {databases} = showDatabasesParser(data)
const rps = await showRetentionPolicies(proxy, databases)
const namespaces = rps.data.results.reduce((acc, result, index) => {
const {retentionPolicies} = showRetentionPoliciesParser(result)
const dbrp = retentionPolicies.map(rp => ({
database: databases[index],
retentionPolicy: rp.name,
}))
return [...acc, ...dbrp]
}, [])
const sorted = _.sortBy(namespaces, ({database}: Namespace) =>
database.toLowerCase()
)
const sorted = await getDatabasesWithRetentionPolicies(proxy)
this.setState({namespaces: sorted})
} catch (err) {
console.error(err)

View File

@ -1,3 +1,4 @@
import _ from 'lodash'
import React, {PureComponent} from 'react'
import {withRouter, Link} from 'react-router'
import {connect} from 'react-redux'
@ -14,10 +15,13 @@ import {
} from 'src/side_nav/components/NavItems'
import {DEFAULT_HOME_PAGE} from 'src/shared/constants'
import {Params, Location, Links, Me} from 'src/types/sideNav'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {Params, Location, Links, Me} from 'src/types/sideNav'
import {Source} from 'src/types'
interface Props {
sources: Source[]
params: Params
location: Location
isHidden: boolean
@ -42,9 +46,13 @@ class SideNav extends PureComponent<Props> {
logoutLink,
links,
me,
sources = [],
} = this.props
const sourcePrefix = `/sources/${sourceID}`
const defaultSource = sources.find(s => s.default)
const id = sourceID || _.get(defaultSource, 'id', 0)
const sourcePrefix = `/sources/${id}`
const dataExplorerLink = `${sourcePrefix}/chronograf/data-explorer`
const isDefaultPage = location.split('/').includes(DEFAULT_HOME_PAGE)
@ -69,6 +77,16 @@ class SideNav extends PureComponent<Props> {
>
<NavHeader link={`${sourcePrefix}/hosts`} title="Host List" />
</NavBlock>
<FeatureFlag name="log-viewer">
<NavBlock
highlightWhen={['logs']}
icon="cubo-node"
link={'/logs'}
location={location}
>
<NavHeader link={'/logs'} title="Log Viewer" />
</NavBlock>
</FeatureFlag>
<NavBlock
highlightWhen={['data-explorer', 'delorean']}
icon="graphline"
@ -164,12 +182,14 @@ class SideNav extends PureComponent<Props> {
}
const mapStateToProps = ({
sources,
auth: {isUsingAuth, logoutLink, me},
app: {
ephemeral: {inPresentationMode},
},
links,
}) => ({
sources,
isHidden: inPresentationMode,
isUsingAuth,
logoutLink,

View File

@ -7,6 +7,7 @@ import errorsMiddleware from 'shared/middleware/errors'
import {resizeLayout} from 'shared/middleware/resizeLayout'
import {queryStringConfig} from 'shared/middleware/queryStringConfig'
import statusReducers from 'src/status/reducers'
import logsReducer from 'src/logs/reducers'
import sharedReducers from 'shared/reducers'
import dataExplorerReducers from 'src/data_explorer/reducers'
import adminReducers from 'src/admin/reducers'
@ -29,6 +30,7 @@ const rootReducer = combineReducers({
cellEditorOverlay,
overlayTechnology,
dashTimeV1,
logs: logsReducer,
routing: routerReducer,
services: servicesReducer,
script: scriptReducer,