Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ env:
- TRIGGER_REPO_BRANCH: "master"
notifications:
email: false
before_install:
- yarn install
- cd packages/console ; yarn install ; cd -
script:
- yarn test
- yarn coveralls
Expand Down
12 changes: 11 additions & 1 deletion packages/console/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,19 @@ npm install @patternfly-react/console --save
### Usage

```javascript
import { VncConsole, SerialConsole } from '@patternfly-react/console
import { VncConsole, SerialConsole } from '@patternfly-react/console'
```

#### Styling:
Example with LESS:
```
@import "~bootstrap/less/variables";
@import "~patternfly/dist/less/variables";
@import "~patternfly-react/dist/less/patternfly-react.less";
@import "~xterm/dist/xterm.css";
@import "~@patternfly-react/console/dist/less/console.less";
```

### Building

```
Expand Down
4 changes: 2 additions & 2 deletions packages/console/less/console.less
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
+ Styling shared by both VncConsole and SerialConsole.
+*/
Styling shared by both VncConsole and SerialConsole.
*/
@import 'serial-console';
@import 'vnc-console';

Expand Down
175 changes: 170 additions & 5 deletions packages/console/src/SerialConsole/SerialConsole.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,176 @@
import React from 'react';
import PropTypes from 'prop-types';

const propTypes = {};
const defaultProps = {};
import { EmptyState } from 'patternfly-react';
import { Button } from 'patternfly-react';
import { noop } from 'patternfly-react';
import { CONNECTED, DISCONNECTED, LOADING } from './constants';

const SerialConsole = () => <div>Serial Console</div>;
import XTerm from './XTerm';
import SerialConsoleActions from './SerialConsoleActions';

SerialConsole.propTypes = propTypes;
SerialConsole.defaultProps = defaultProps;
class SerialConsole extends React.Component {
componentDidMount() {
this.props.onConnect();
}

componentWillUnmount() {
this.props.onDisconnect();
}

onResetClick = (event) => {
if (event.button !== 0) return;

this.props.onDisconnect();
this.props.onConnect();
event.target.blur();
this.focusTerminal();
};

onDisconnectClick = (event) => {
if (event.button !== 0) return;

this.props.onDisconnect();
event.target.blur();
this.focusTerminal();
};

/**
* Backend sent data.
*/
onDataReceived(data) {
if (this.childTerminal && this.props.status === CONNECTED) {
this.childTerminal.onDataReceived(data);
}
}

/**
* Backend closed connection.
*/
onConnectionClosed(reason) {
if (this.childTerminal) {
this.childTerminal.onConnectionClosed(reason);
}
}

focusTerminal = () => {
this.childTerminal && this.childTerminal.focus();
};

render() {
const { id, status, topClassName } = this.props;
const idPrefix = `${id || 'id'}-serialconsole`;

let terminal;
let isDisconnectEnabled = false;
switch (status) {
case CONNECTED:
terminal = (
<XTerm
ref={c => {
this.childTerminal = c;
}}
cols={this.props.cols}
rows={this.props.rows}
onConnect={this.props.onConnect}
onDisconnect={this.props.onDisconnect}
onData={this.props.onData}
onTitleChanged={this.props.onTitleChanged}
onResize={this.props.onResize}
/>
);
isDisconnectEnabled = true;
break;
case DISCONNECTED:
terminal = (
<EmptyState>
<EmptyState.Title>
{this.props.textDisconnectedTitle}
</EmptyState.Title>
<EmptyState.Info>{this.props.textDisconnected}</EmptyState.Info>
<EmptyState.Action>
<Button
bsStyle="primary"
bsSize="large"
onClick={this.props.onConnect}
>
{this.props.textConnect}
</Button>
</EmptyState.Action>
</EmptyState>
);
break;
case LOADING:
default:
terminal = <span>{this.props.textLoading}</span>;
break;
}

return (
<div className={topClassName} id={id}>
<SerialConsoleActions
idPrefix={idPrefix}
isDisconnectEnabled={isDisconnectEnabled}
onDisconnect={this.onDisconnectClick}
onReset={this.onResetClick}
textDisconnect={this.props.textDisconnect}
textReconnect={this.props.textReconnect}
/>
<div className="panel-body console-terminal-pf">{terminal}</div>
</div>
);
}
}

SerialConsole.propTypes = {
/** Initiate connection to backend. In other words, the calling components manages connection state. */
onConnect: PropTypes.func.isRequired,
/** Close connection to backend */
onDisconnect: PropTypes.func.isRequired,
/** Terminal has been resized, backend shall be informed. (rows, cols) => {} */
onResize: PropTypes.func,
/** Terminal produced data, like key-press */
onData: PropTypes.func,
/** Terminal title has been changed. */
onTitleChanged: PropTypes.func,

/** Connection status, a value from [''connected', 'disconnected', 'loading']. Default is 'loading' for a not matching value. */
status: PropTypes.string.isRequired,
id: PropTypes.string,

/** Size of the terminal component */
rows: PropTypes.number,
cols: PropTypes.number,

/** Enable customization */
topClassName: PropTypes.string,

/** Localization */
textDisconnect: PropTypes.string,
textDisconnectedTitle: PropTypes.string,
textDisconnected: PropTypes.string,
textLoading: PropTypes.string,
textReconnect: PropTypes.string,
textConnect: PropTypes.string
};

SerialConsole.defaultProps = {
topClassName: '',

id: '',
rows: 25,
cols: 80,

onTitleChanged: noop,
onData: noop,
onResize: noop,

textDisconnectedTitle: 'Disconnected from serial console',
textDisconnected: 'Click Connect to open serial console.',
textLoading: 'Loading ...',
textConnect: 'Connect',
textDisconnect: undefined /** Default is set in SerialConsoleActions */,
textReconnect: undefined /** Default is set in SerialConsoleActions */
};

export default SerialConsole;
159 changes: 144 additions & 15 deletions packages/console/src/SerialConsole/SerialConsole.stories.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,148 @@
/* eslint-disable import/no-extraneous-dependencies */
import React from 'react';

import { storiesOf } from '@storybook/react';
import { withInfo } from '@storybook/addon-info';
import { inlineTemplate } from '../../../../storybook/decorators/storyTemplates';
import SerialConsole from './SerialConsole';

const stories = storiesOf('@patternfly-react/console', module);

stories.add(
'SerialConsole',
withInfo()(() => {
const story = <SerialConsole />;
return inlineTemplate({
story,
title: 'SerialConsole'
});
import { defaultTemplate } from '../../../../storybook/decorators/storyTemplates';

import { SerialConsole } from './index';
import { CONNECTED, DISCONNECTED, LOADING } from './constants';

const stories = storiesOf('SerialConsole', module);
stories.addDecorator(
defaultTemplate({
title: 'SerialConsole',
description:
'This is an example of the SerialConsole component. For the purpose of this example, there is just a mock backend.'
})
);

/* eslint no-console: ["warn", { allow: ["log"] }] */
const { log } = console; // let's keep these trace messages for tutoring purposes

const timeoutIds = [];
/**
* The SerialConsoleConnector component is consumer-specific and wraps the communication with backend.
* For the purpose of this storybook, the backend is just mimicked.
*/
class SerialConsoleConnector extends React.Component {
state = { status: LOADING, passKeys: false };

onBackendDisconnected = () => {
log('Backend has disconnected, pass the info to the UI component');
if (this.childSerialconsole) {
this.childSerialconsole.onConnectionClosed(
'Reason for disconnect provided by backend.'
);
}

this.setState({
passKeys: false,
status: DISCONNECTED // will close the terminal window
});
};

onConnect = () => {
log('SerialConsoleConnector.onConnect(), ', this.state);
this.setConnected();
this.tellFairyTale();
};

onData = data => {
log(
'UI terminal component produced data, i.e. a key was pressed, pass it to backend. [',
data,
']'
);

// Normally, the "data" shall be passed to the backend which might send them back via onData() call
// Since there is no backend, let;s pass them to UI component immediately.
if (this.state.passKeys) {
this.onDataFromBackend(data);
}
};

onDataFromBackend = data => {
log('Backend sent data, pass them to the UI component. [', data, ']');
if (this.childSerialconsole) {
this.childSerialconsole.onDataReceived(data);
}
};

onDisconnect = () => {
this.setState({
status: DISCONNECTED
});
timeoutIds.forEach(id => clearTimeout(id));
};

onResize = (rows, cols) => {
log(
'UI has been resized, pass this info to backend. [',
rows,
', ',
cols,
']'
);
};

setConnected = () => {
this.setState({
status: CONNECTED,
passKeys: true
});
};

tellFairyTale = () => {
let time = 1000;
timeoutIds.push(
setTimeout(
() => this.onDataFromBackend(' This is a mock terminal. '),
time
)
);

time += 1000;
timeoutIds.push(
setTimeout(
() => this.onDataFromBackend(' Something is happening! '),
time
)
);

time += 1000;
timeoutIds.push(
setTimeout(
() => this.onDataFromBackend(' Something is happening! '),
time
)
);

time += 1000;
timeoutIds.push(
setTimeout(
() => this.onDataFromBackend(' Backend will be disconnected shortly. '),
time
)
);

time += 5000;
timeoutIds.push(setTimeout(this.onBackendDisconnected, time));
};

render() {
return (
<SerialConsole
onConnect={this.onConnect}
onDisconnect={this.onDisconnect}
onResize={this.onResize()}
onData={this.onData}
id="my-serialconsole"
status={this.state.status}
ref={c => {
this.childSerialconsole = c;
}}
/>
);
}
}

stories.addWithInfo('SerialConsole', () => <SerialConsoleConnector />);
3 changes: 2 additions & 1 deletion packages/console/src/SerialConsole/SerialConsole.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
import React from 'react';
import { shallow } from 'enzyme';
import SerialConsole from './SerialConsole';
import { noop } from 'patternfly-react';

test('placeholder render test', () => {
const view = shallow(<SerialConsole />);
const view = shallow(<SerialConsole onConnect={noop} onDisconnect={noop} status='loading'/>);
expect(view).toMatchSnapshot();
});
Loading