Skip to content

Commit

Permalink
Merge pull request openshift#96 from TheRealJon/log-websockets
Browse files Browse the repository at this point in the history
Use WebSockets for logs
  • Loading branch information
spadgett authored Jun 8, 2018
2 parents e39cd7f + 79c0893 commit d4bad50
Show file tree
Hide file tree
Showing 10 changed files with 225 additions and 360 deletions.
61 changes: 0 additions & 61 deletions frontend/__tests__/line-buffer.js

This file was deleted.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"fuzzysearch": "1.0.x",
"history": "4.x",
"immutable": "3.x",
"js-base64": "^2.4.5",
"js-yaml": "3.x",
"lodash-es": "4.x",
"patternfly": "^3.45.0",
Expand Down
10 changes: 5 additions & 5 deletions frontend/public/components/build-logs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ export class BuildLogs extends React.Component {
constructor(props) {
super(props);
this.state = {
eof: false
alive: true
};
}

static getDerivedStateFromProps(nextProps, prevState) {
const eof = ['Complete', 'Failed', 'Error', 'Cancelled'].includes(_.get(nextProps.obj, 'status.phase'));
if (prevState.eof !== eof){
return {eof};
const alive = _.get(nextProps.obj, 'status.phase') === 'Running';
if (prevState.alive !== alive){
return {alive};
}
return null;
}
Expand All @@ -24,7 +24,7 @@ export class BuildLogs extends React.Component {
const buildName = _.get(this.props.obj, 'metadata.name');
return <div className="co-m-pane__body">
<ResourceLog
eof={this.state.eof}
alive={this.state.alive}
kind="Build"
namespace={namespace}
resourceName={buildName} />
Expand Down
46 changes: 27 additions & 19 deletions frontend/public/components/pod-logs.jsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,61 @@
import * as _ from 'lodash-es';
import * as React from 'react';

import { Dropdown, LoadingInline, ResourceLog, ResourceName } from './utils';
import { Dropdown, ResourceLog, ResourceName } from './utils';

// Component to container dropdown or conatiner name if only one container in pod.
const ContainerDropdown = ({currentContainer, containers, kind, onChange}) => {
const resourceName = (container) => <ResourceName name={container.name || <LoadingInline />} kind={kind} />;
const resourceName = (container) => {
return <ResourceName name={container.name} kind={kind} />;
};
const dropdownItems = _.mapValues(containers, resourceName);
return <Dropdown className="btn-group" items={dropdownItems} title={resourceName(currentContainer)} onChange={onChange} />;
return <Dropdown
className="btn-group"
items={dropdownItems}
title={resourceName(currentContainer)}
onChange={onChange} />;
};

export class PodLogs extends React.Component {
constructor(props) {
super(props);
this._selectContainer = this._selectContainer.bind(this);

this.state = {
containers: [],
currentContainer: null,
currentKey: ''
};
}

static getDerivedStateFromProps(nextProps, prevState) {
const newState = {};
const containers = _.get(nextProps.obj, 'status.containerStatuses', []);
newState.containers = _.map(containers, (container) => {
newState.containers = _.reduce(containers, (accumulator, {name, state}, index) => {
return {
name: container.name,
eof: !_.isEmpty(container.state.terminated)
...accumulator,
[name]: {
alive: _.has(state, 'running'),
name,
order: index
}
};
});
}, {});

newState.currentContainer = prevState.currentContainer || newState.containers[0];
if ( !_.isEqual(prevState.currentContainer, newState.currentContainer)
|| !_.isEqual(prevState.containers, newState.containers)) {
return newState;
if (!prevState.currentKey) {
const firstContainer = _.find(newState.containers, { order: 0 });
newState.currentKey = firstContainer.name;
}
return null;
return newState;
}

_selectContainer(index) {
const currentContainer = this.state.containers[index];
this.setState({currentContainer});
_selectContainer(name) {
this.setState({currentKey: name});
}

render() {
const {currentContainer, containers} = this.state;
const {containers, currentKey} = this.state;
const namespace = _.get(this.props.obj, 'metadata.namespace');
const podName = _.get(this.props.obj, 'metadata.name');
const currentContainer = _.get(containers, currentKey);
const containerDropdown = <ContainerDropdown
currentContainer={currentContainer}
containers={containers}
Expand All @@ -56,8 +64,8 @@ export class PodLogs extends React.Component {

return <div className="co-m-pane__body">
<ResourceLog
alive={currentContainer.alive}
containerName={currentContainer.name}
eof={currentContainer.eof}
kind="Pod"
dropdown={containerDropdown}
namespace={namespace}
Expand Down
2 changes: 0 additions & 2 deletions frontend/public/components/utils/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ export * from './cog';
export * from './selector';
export * from './selector-input';
export * from './label-list';
export * from './line-buffer';
export * from './log-window';
export * from './resource-icon';
export * from './resource-link';
Expand All @@ -23,7 +22,6 @@ export * from './select-text';
export * from './toggle-play';
export * from './button-bar';
export * from './number-spinner';
export * from './stream';
export * from './cloud-provider';
export * from './documentation';
export * from './router';
Expand Down
46 changes: 0 additions & 46 deletions frontend/public/components/utils/line-buffer.js

This file was deleted.

76 changes: 34 additions & 42 deletions frontend/public/components/utils/log-window.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,24 @@ const FUDGE_FACTOR = 105;
export class LogWindow extends React.PureComponent {
constructor(props) {
super(props);

this.state = {
pausedAt: 0,
lineCount: 0,
content: '',
height: ''
};

this._unpause = this._unpause.bind(this);
this._handleScroll = _.throttle(this._handleScroll.bind(this), 100);
this._handleResize = _.debounce(this._handleResize.bind(this), 50);
this._setScrollPane = (element) => this.scrollPane = element;
this._setLogContents = (element) => this.logContents = element;
this.state = {
content: '',
height: ''
};
}

static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.status === 'paused') {
// If paused, make sure pausedAt state is accurate. This makes the "Resume stream and show X lines"
// footer consistent whether the log is paused via scrolling or a parent control (like the pause button in
// pod logs).
const pausedAt = prevState.pausedAt > 0 ? prevState.pausedAt : nextProps.buffer.totalLineCount;
return {pausedAt};
static getDerivedStateFromProps(nextProps) {
if (nextProps.status !== 'paused') {
return {
content: nextProps.lines.join(''),
};
}

const lines = nextProps.buffer.lines();
return {
pausedAt: 0, // Streaming, so reset pausedAt
lineCount: lines.length,
content: lines.join('')
};
return null;
}

componentDidMount() {
Expand All @@ -48,7 +36,7 @@ export class LogWindow extends React.PureComponent {
}

componentDidUpdate(prevProps) {
if (prevProps.status !== this.props.status || prevProps.buffer.totalLineCount || this.props.buffer.totalLineCount) {
if (prevProps.status !== this.props.status || prevProps.lines.length || this.props.lines.length) {
this._scrollToBottom();
}
}
Expand All @@ -59,7 +47,6 @@ export class LogWindow extends React.PureComponent {
}

_handleScroll() {

// Stream is finished, take no action on scroll
if (this.props.status === 'eof') {
return;
Expand All @@ -70,11 +57,9 @@ export class LogWindow extends React.PureComponent {
if (this.scrollPane.scrollTop < scrollTarget) {
if (this.props.status !== 'paused') {
this.props.updateStatus('paused');
this.setState({ pausedAt: this.props.buffer.totalLineCount });
}
} else {
this.props.updateStatus('streaming');
this.setState({ pausedAt: 0 });
}
}

Expand All @@ -90,10 +75,10 @@ export class LogWindow extends React.PureComponent {
}

_scrollToBottom() {
if (['streaming', 'eof'].includes(this.props.status)) {
if (this.props.status === 'streaming') {
// Async because scrollHeight depends on the size of the rendered pane
setTimeout(() => {
if (this.scrollPane && ['streaming', 'eof'].includes(this.props.status)) {
if (this.scrollPane && this.props.status === 'streaming') {
this.scrollPane.scrollTop = this.scrollPane.scrollHeight;
}
}, 0);
Expand All @@ -105,36 +90,43 @@ export class LogWindow extends React.PureComponent {
}

render() {
let linesBehind = 0;
if (this.props.status === 'paused') {
linesBehind = this.props.buffer.totalLineCount - this.state.pausedAt;
const {bufferFull, lines, linesBehind, status } = this.props;
const {content, height} = this.state;

// TODO maybe move these variables into state so they are only updated on changes
const totalLineCount = pluralize(lines.length, 'line');
const linesBehindCount = pluralize(linesBehind, 'line');
const headerText = bufferFull ? `last ${totalLineCount}` : totalLineCount;
let footerText = ' Resume stream';
if (linesBehind > 0) {
footerText += bufferFull ? ` and show last ${totalLineCount}` : ` and show ${linesBehindCount}`;
}
const hasLinesBehind = linesBehind > 0;

return <div className="log-window">
<div className="log-window__header">
{ this.state.lineCount < this.props.buffer.maxSize ? pluralize(this.state.lineCount, 'line') : `last ${pluralize(this.props.buffer.maxSize, 'line')}` }
{headerText}
</div>
<div className="log-window__body">
<div className="log-window__scroll-pane" ref={this._setScrollPane}>
<div className="log-window__contents" ref={this._setLogContents} style={{ height: this.state.height }}>
<div className="log-window__contents" ref={this._setLogContents} style={{ height: height }}>
<div className="log-window__contents__text">
{this.state.content}
{content}
</div>
</div>
</div>
</div>
{ !['streaming', 'loading', 'eof'].includes(this.props.status) && <div onClick={this._unpause} className="log-window__footer">
{ !hasLinesBehind && <div><span className="fa fa-play-circle-o"></span> Resume stream</div> }
{ hasLinesBehind && linesBehind < this.props.buffer.maxSize && <div><span className="fa fa-play-circle-o"></span> Resume stream and show {pluralize(linesBehind, 'line')}</div> }
{ hasLinesBehind && linesBehind > this.props.buffer.maxSize && <div><span className="fa fa-play-circle-o"></span> Resume stream and show last {pluralize(this.props.buffer.maxSize, 'line')}</div> }
{ status === 'paused' && <div onClick={this._unpause} className="log-window__footer">
<span className="fa fa-play-circle-o" aria-hidden="true"></span>
{footerText}
</div> }
</div>;
}
}

LogWindow.propTypes = {
buffer: PropTypes.object.isRequired,
bufferFull: PropTypes.bool.isRequired,
lines: PropTypes.array.isRequired,
linesBehind: PropTypes.number.isRequired,
status: PropTypes.string.isRequired,
updateStatus: PropTypes.func.isRequired,
touched: PropTypes.number.isRequired // touched is used as a signal that props.buffer has changed
updateStatus: PropTypes.func.isRequired
};
Loading

0 comments on commit d4bad50

Please sign in to comment.