dc54e2ec96
Closes-Bug: #1626059 Change-Id: I442f70e7550be4fdd8549095b01f2762fd8bb841
470 lines
14 KiB
JavaScript
470 lines
14 KiB
JavaScript
/*
|
|
* Copyright 2014 Mirantis, Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
* not use this file except in compliance with the License. You may obtain
|
|
* a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
* License for the specific language governing permissions and limitations
|
|
* under the License.
|
|
**/
|
|
import $ from 'jquery';
|
|
import _ from 'underscore';
|
|
import i18n from 'i18n';
|
|
import React from 'react';
|
|
import utils from 'utils';
|
|
import models from 'models';
|
|
import {Input, ProgressBar, ProgressButton} from 'views/controls';
|
|
import {pollingMixin} from 'component_mixins';
|
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
|
import ReactFragment from 'react-addons-create-fragment';
|
|
|
|
var LogsTab = React.createClass({
|
|
mixins: [
|
|
pollingMixin(5)
|
|
],
|
|
statics: {
|
|
breadcrumbsPath() {
|
|
return [
|
|
[i18n('cluster_page.tabs.logs'), null, {active: true}]
|
|
];
|
|
},
|
|
checkSubroute(tabProps) {
|
|
var {activeTab, tabOptions, defaultLogLevel} = tabProps;
|
|
if (activeTab === 'logs' && tabOptions[0]) {
|
|
var selectedLogs = utils.deserializeTabOptions(_.compact(tabOptions).join('/'));
|
|
selectedLogs.level = selectedLogs.level ?
|
|
selectedLogs.level.toUpperCase()
|
|
:
|
|
defaultLogLevel;
|
|
return {selectedLogs};
|
|
}
|
|
return {
|
|
selectedLogs: {type: 'local', node: null, source: 'app', level: defaultLogLevel}
|
|
};
|
|
}
|
|
},
|
|
shouldDataBeFetched() {
|
|
return this.state.to && this.state.logsEntries;
|
|
},
|
|
fetchData() {
|
|
var request;
|
|
var logsEntries = this.state.logsEntries;
|
|
var from = this.state.from;
|
|
var to = this.state.to;
|
|
request = this.fetchLogs({from: from, to: to})
|
|
.then((data) => {
|
|
this.setState({
|
|
logsEntries: data.entries.concat(logsEntries),
|
|
from: data.from,
|
|
to: data.to
|
|
});
|
|
});
|
|
return request;
|
|
},
|
|
getInitialState() {
|
|
return {
|
|
showMoreLogsLink: false,
|
|
loading: 'loading',
|
|
loadingError: null,
|
|
from: -1,
|
|
to: 0
|
|
};
|
|
},
|
|
fetchLogs(data) {
|
|
return $.ajax({
|
|
url: '/api/logs',
|
|
dataType: 'json',
|
|
data: _.extend(_.omit(this.props.selectedLogs, 'type'), data),
|
|
headers: {
|
|
'X-Auth-Token': app.keystoneClient.token
|
|
}
|
|
});
|
|
},
|
|
showLogs(params) {
|
|
this.stopPolling();
|
|
var logOptions = this.props.selectedLogs.type === 'remote' ?
|
|
_.extend({}, this.props.selectedLogs) : _.omit(this.props.selectedLogs, 'node');
|
|
logOptions.level = logOptions.level.toLowerCase();
|
|
app.navigate('/cluster/' + this.props.cluster.id + '/logs/' +
|
|
utils.serializeTabOptions(logOptions), {replace: true});
|
|
params = params || {};
|
|
this.fetchLogs(params)
|
|
.then((data) => {
|
|
var logsEntries = this.state.logsEntries || [];
|
|
this.setState({
|
|
showMoreLogsLink: data.has_more || false,
|
|
logsEntries: params.fetch_older ? logsEntries.concat(data.entries) : data.entries,
|
|
loading: 'done',
|
|
from: data.from,
|
|
to: data.to
|
|
});
|
|
this.startPolling();
|
|
},
|
|
(response) => {
|
|
this.setState({
|
|
logsEntries: undefined,
|
|
loading: 'fail',
|
|
loadingError: utils.getResponseText(response, i18n('cluster_page.logs_tab.log_alert'))
|
|
});
|
|
}
|
|
);
|
|
},
|
|
onShowButtonClick() {
|
|
this.setState({
|
|
loading: 'loading',
|
|
loadingError: null
|
|
}, this.showLogs);
|
|
},
|
|
onShowMoreClick(value) {
|
|
this.showLogs({max_entries: value, fetch_older: true, from: this.state.from});
|
|
},
|
|
render() {
|
|
return (
|
|
<div className='row'>
|
|
<div className='title'>
|
|
{i18n('cluster_page.logs_tab.title')}
|
|
<div className='help-block'>
|
|
{i18n('cluster_page.logs_tab.deprecation_note')}
|
|
</div>
|
|
</div>
|
|
<div className='col-xs-12 content-elements'>
|
|
<LogFilterBar
|
|
{... _.pick(this.props, 'selectedLogs', 'changeLogSelection')}
|
|
nodes={this.props.cluster.get('nodes')}
|
|
showLogs={this.showLogs}
|
|
onShowButtonClick={this.onShowButtonClick}
|
|
actionInProgress={this.state.loading === 'loading'}
|
|
/>
|
|
{this.state.loading === 'fail' &&
|
|
<div className='logs-fetch-error alert alert-danger'>
|
|
{this.state.loadingError}
|
|
</div>
|
|
}
|
|
{this.state.loading === 'loading' && <ProgressBar />}
|
|
{this.state.logsEntries &&
|
|
<LogsTable
|
|
logsEntries={this.state.logsEntries}
|
|
showMoreLogsLink={this.state.showMoreLogsLink}
|
|
onShowMoreClick={this.onShowMoreClick}
|
|
/>
|
|
}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
|
|
var LogFilterBar = React.createClass({
|
|
// PureRenderMixin added for prevention the rerender LogFilterBar
|
|
// (because of polling) in Mozilla browser
|
|
mixins: [PureRenderMixin],
|
|
getInitialState() {
|
|
return _.extend({}, this.props.selectedLogs, {
|
|
sourcesLoadingState: 'loading',
|
|
sourcesLoadingError: null,
|
|
sources: [],
|
|
locked: true
|
|
});
|
|
},
|
|
fetchSources(type, nodeId) {
|
|
var nodes = this.props.nodes;
|
|
var chosenNodeId = nodeId || (nodes.length ? nodes.first().id : null);
|
|
this.sources = new models.LogSources();
|
|
this.sources.promise = (type === 'remote' && chosenNodeId) ?
|
|
this.sources.fetch({url: '/api/logs/sources/nodes/' + chosenNodeId})
|
|
:
|
|
this.sources.fetch();
|
|
this.sources.promise.then(
|
|
() => {
|
|
var filteredSources = this.sources.filter((source) => {
|
|
return source.get('remote') === (type !== 'local');
|
|
});
|
|
var chosenSource = _.find(filteredSources, {id: this.state.source}) ||
|
|
_.first(filteredSources);
|
|
var chosenLevelId = chosenSource ? _.includes(chosenSource.get('levels'), this.state.level)
|
|
? this.state.level : _.first(chosenSource.get('levels')) : null;
|
|
this.setState({
|
|
type: type,
|
|
sources: this.sources,
|
|
sourcesLoadingState: 'done',
|
|
node: chosenNodeId && type === 'remote' ? chosenNodeId : null,
|
|
source: chosenSource ? chosenSource.id : null,
|
|
level: chosenLevelId,
|
|
locked: false
|
|
});
|
|
},
|
|
(response) => {
|
|
this.setState({
|
|
type: type,
|
|
sources: {},
|
|
sourcesLoadingState: 'fail',
|
|
sourcesLoadingError: utils.getResponseText(response,
|
|
i18n('cluster_page.logs_tab.source_alert')),
|
|
locked: false
|
|
});
|
|
}
|
|
);
|
|
return this.sources.promise;
|
|
},
|
|
componentDidMount() {
|
|
this.fetchSources(this.state.type, this.state.node)
|
|
.then(() => {
|
|
this.setState({locked: true});
|
|
this.props.showLogs();
|
|
});
|
|
},
|
|
onTypeChange(name, value) {
|
|
this.fetchSources(value);
|
|
},
|
|
onNodeChange(name, value) {
|
|
this.fetchSources('remote', value);
|
|
},
|
|
onLevelChange(name, value) {
|
|
this.setState({
|
|
level: value,
|
|
locked: false
|
|
});
|
|
},
|
|
onSourceChange(name, value) {
|
|
var levels = this.state.sources.get(value).get('levels');
|
|
var data = {locked: false, source: value};
|
|
if (!_.includes(levels, this.state.level)) data.level = _.first(levels);
|
|
this.setState(data);
|
|
},
|
|
getLocalSources() {
|
|
return this.state.sources.map((source) => !source.get('remote') &&
|
|
<option value={source.id} key={source.id}>{source.get('name')}</option>
|
|
);
|
|
},
|
|
getRemoteSources() {
|
|
var options = {};
|
|
var groups = [''];
|
|
var sourcesByGroup = {'': []};
|
|
var sources = this.state.sources;
|
|
if (sources.length) {
|
|
sources.each((source) => {
|
|
var group = source.get('group') || '';
|
|
if (!_.has(sourcesByGroup, group)) {
|
|
sourcesByGroup[group] = [];
|
|
groups.push(group);
|
|
}
|
|
sourcesByGroup[group].push(source);
|
|
});
|
|
_.each(groups, (group) => {
|
|
if (sourcesByGroup[group].length) {
|
|
var option = sourcesByGroup[group].map((source) => {
|
|
return <option value={source.id} key={source.id}>{source.get('name')}</option>;
|
|
});
|
|
options[group] = group ? <optgroup label={group}>{option}</optgroup> : option;
|
|
}
|
|
});
|
|
}
|
|
return ReactFragment(options);
|
|
},
|
|
handleShowButtonClick() {
|
|
this.setState({locked: true});
|
|
this.props.changeLogSelection(_.pick(this.state, 'type', 'node', 'source', 'level'));
|
|
this.props.onShowButtonClick();
|
|
},
|
|
render() {
|
|
var isRemote = this.state.type === 'remote';
|
|
return (
|
|
<div className='well well-sm'>
|
|
<div className='sticker row'>
|
|
{this.renderTypeSelect()}
|
|
{isRemote && this.renderNodeSelect()}
|
|
{this.renderSourceSelect()}
|
|
{this.renderLevelSelect()}
|
|
{this.renderFilterButton(isRemote)}
|
|
</div>
|
|
{this.state.sourcesLoadingState === 'fail' &&
|
|
<div className='node-sources-error alert alert-danger'>
|
|
{this.state.sourcesLoadingError}
|
|
</div>
|
|
}
|
|
</div>
|
|
);
|
|
},
|
|
renderFilterButton(isRemote) {
|
|
return <div className={utils.classNames({
|
|
'form-group': true,
|
|
'col-md-4 col-sm-12': isRemote,
|
|
'col-md-6 col-sm-3': !isRemote
|
|
})}>
|
|
<label />
|
|
<ProgressButton
|
|
className='btn btn-default pull-right'
|
|
onClick={this.handleShowButtonClick}
|
|
disabled={!this.state.source || this.state.locked}
|
|
progress={this.props.actionInProgress}
|
|
>
|
|
{i18n('cluster_page.logs_tab.show')}
|
|
</ProgressButton>
|
|
</div>;
|
|
},
|
|
renderTypeSelect() {
|
|
var types = [['local', 'Fuel Master']];
|
|
if (this.props.nodes.length) {
|
|
types.push(['remote', 'Other servers']);
|
|
}
|
|
var typeOptions = types.map((type) => {
|
|
return <option value={type[0]} key={type[0]}>{type[1]}</option>;
|
|
});
|
|
return <div className='col-md-2 col-sm-3'>
|
|
<Input
|
|
type='select'
|
|
label={i18n('cluster_page.logs_tab.logs')}
|
|
value={this.state.type}
|
|
wrapperClassName='filter-bar-item log-type-filter'
|
|
name='type'
|
|
onChange={this.onTypeChange}
|
|
children={typeOptions}
|
|
/>
|
|
</div>;
|
|
},
|
|
renderNodeSelect() {
|
|
var sortedNodes = this.props.nodes.models.sort(_.partialRight(utils.compare, {attr: 'name'}));
|
|
var nodeOptions = sortedNodes.map((node) => {
|
|
return <option value={node.id} key={node.id}>{node.get('name') || node.get('mac')}</option>;
|
|
});
|
|
|
|
return <div className='col-md-2 col-sm-3'>
|
|
<Input
|
|
type='select'
|
|
label={i18n('cluster_page.logs_tab.node')}
|
|
value={this.state.node}
|
|
wrapperClassName='filter-bar-item log-node-filter'
|
|
name='node'
|
|
onChange={this.onNodeChange}
|
|
children={nodeOptions}
|
|
/>
|
|
</div>;
|
|
},
|
|
renderSourceSelect() {
|
|
var sourceOptions = this.state.type === 'local' ? this.getLocalSources() :
|
|
this.getRemoteSources();
|
|
return <div className='col-md-2 col-sm-3'>
|
|
<Input
|
|
type='select'
|
|
label={i18n('cluster_page.logs_tab.source')}
|
|
value={this.state.source}
|
|
wrapperClassName='filter-bar-item log-source-filter'
|
|
name='source'
|
|
onChange={this.onSourceChange}
|
|
disabled={!this.state.source}
|
|
children={sourceOptions}
|
|
/>
|
|
</div>;
|
|
},
|
|
renderLevelSelect() {
|
|
var levelOptions = [];
|
|
if (this.state.source && this.state.sources.length) {
|
|
levelOptions = this.state.sources.get(this.state.source).get('levels').map((level) => {
|
|
return <option value={level} key={level}>{level}</option>;
|
|
});
|
|
}
|
|
return <div className='col-md-2 col-sm-3'>
|
|
<Input
|
|
type='select'
|
|
label={i18n('cluster_page.logs_tab.min_level')}
|
|
value={this.state.level}
|
|
wrapperClassName='filter-bar-item log-level-filter'
|
|
name='level'
|
|
onChange={this.onLevelChange}
|
|
disabled={!this.state.level}
|
|
children={levelOptions}
|
|
/>
|
|
</div>;
|
|
}
|
|
});
|
|
|
|
var LogsTable = React.createClass({
|
|
handleShowMoreClick(value) {
|
|
return this.props.onShowMoreClick(value);
|
|
},
|
|
selectRow(e) {
|
|
// select the entire row on mouse triple click
|
|
if (e.detail === 3 && document.createRange) {
|
|
var range = document.createRange();
|
|
range.selectNodeContents($(e.target).closest('tr')[0]);
|
|
window.getSelection().addRange(range);
|
|
}
|
|
},
|
|
getLevelClass(level) {
|
|
return {
|
|
DEBUG: 'debug',
|
|
INFO: 'info',
|
|
NOTICE: 'notice',
|
|
WARNING: 'warning',
|
|
ERROR: 'error',
|
|
ERR: 'error',
|
|
CRITICAL: 'critical',
|
|
CRIT: 'critical',
|
|
ALERT: 'alert',
|
|
EMERG: 'emerg'
|
|
}[level];
|
|
},
|
|
render() {
|
|
var tabRows = [];
|
|
var logsEntries = this.props.logsEntries;
|
|
if (logsEntries && logsEntries.length) {
|
|
tabRows = _.map(logsEntries, (entry, index) => {
|
|
var key = logsEntries.length - index;
|
|
return <tr
|
|
key={key}
|
|
className={this.getLevelClass(entry[1])}
|
|
onClick={this.selectRow}>
|
|
<td>{entry[0]}</td>
|
|
<td>{entry[1]}</td>
|
|
<td>{entry[2]}</td>
|
|
</tr>;
|
|
});
|
|
}
|
|
return logsEntries.length ?
|
|
<table className='table log-entries'>
|
|
<thead>
|
|
<tr>
|
|
<th className='col-date'>{i18n('cluster_page.logs_tab.date')}</th>
|
|
<th className='col-level'>{i18n('cluster_page.logs_tab.level')}</th>
|
|
<th className='col-message'>{i18n('cluster_page.logs_tab.message')}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{tabRows}
|
|
</tbody>
|
|
{this.props.showMoreLogsLink &&
|
|
<tfoot className='entries-skipped-msg'>
|
|
<tr>
|
|
<td colSpan='3' className='text-center'>
|
|
<div>
|
|
<span>{i18n('cluster_page.logs_tab.bottom_text')}</span>:
|
|
{
|
|
[100, 500, 1000, 5000].map((count) => {
|
|
return <button
|
|
key={count}
|
|
className='btn btn-link show-more-entries'
|
|
onClick={() => this.handleShowMoreClick(count)}
|
|
>
|
|
{count}
|
|
</button>;
|
|
})
|
|
}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tfoot>
|
|
}
|
|
</table>
|
|
:
|
|
<div className='no-logs-msg'>{i18n('cluster_page.logs_tab.no_log_text')}</div>;
|
|
}
|
|
});
|
|
|
|
export default LogsTab;
|