Skip to content

Commit 466a5d8

Browse files
mareklibrajeff-phillips-18
authored andcommitted
feat(SerialConsole): Introduce SerialConsole component (#160)
The SerialConsole component wraps the Terminal component [1] to access serial console (PTY) of a server or a virtual machine. Part of the code (like styling) was already committed within #4a69ebd806.
1 parent 4a69ebd commit 466a5d8

File tree

10 files changed

+572
-26
lines changed

10 files changed

+572
-26
lines changed

.travis.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ env:
1010
- TRIGGER_REPO_BRANCH: "master"
1111
notifications:
1212
email: false
13+
before_install:
14+
- yarn install
15+
- cd packages/console ; yarn install ; cd -
1316
script:
1417
- yarn test
1518
- yarn coveralls

packages/console/README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,19 @@ npm install @patternfly-react/console --save
1919
### Usage
2020

2121
```javascript
22-
import { VncConsole, SerialConsole } from '@patternfly-react/console
22+
import { VncConsole, SerialConsole } from '@patternfly-react/console'
2323
```
2424

25+
#### Styling:
26+
Example with LESS:
27+
```
28+
@import "~bootstrap/less/variables";
29+
@import "~patternfly/dist/less/variables";
30+
@import "~patternfly-react/dist/less/patternfly-react.less";
31+
@import "~xterm/dist/xterm.css";
32+
@import "~@patternfly-react/console/dist/less/console.less";
33+
```
34+
2535
### Building
2636

2737
```

packages/console/less/console.less

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
2-
+ Styling shared by both VncConsole and SerialConsole.
3-
+*/
2+
Styling shared by both VncConsole and SerialConsole.
3+
*/
44
@import 'serial-console';
55
@import 'vnc-console';
66

Lines changed: 170 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,176 @@
11
import React from 'react';
2+
import PropTypes from 'prop-types';
23

3-
const propTypes = {};
4-
const defaultProps = {};
4+
import { EmptyState } from 'patternfly-react';
5+
import { Button } from 'patternfly-react';
6+
import { noop } from 'patternfly-react';
7+
import { CONNECTED, DISCONNECTED, LOADING } from './constants';
58

6-
const SerialConsole = () => <div>Serial Console</div>;
9+
import XTerm from './XTerm';
10+
import SerialConsoleActions from './SerialConsoleActions';
711

8-
SerialConsole.propTypes = propTypes;
9-
SerialConsole.defaultProps = defaultProps;
12+
class SerialConsole extends React.Component {
13+
componentDidMount() {
14+
this.props.onConnect();
15+
}
16+
17+
componentWillUnmount() {
18+
this.props.onDisconnect();
19+
}
20+
21+
onResetClick = (event) => {
22+
if (event.button !== 0) return;
23+
24+
this.props.onDisconnect();
25+
this.props.onConnect();
26+
event.target.blur();
27+
this.focusTerminal();
28+
};
29+
30+
onDisconnectClick = (event) => {
31+
if (event.button !== 0) return;
32+
33+
this.props.onDisconnect();
34+
event.target.blur();
35+
this.focusTerminal();
36+
};
37+
38+
/**
39+
* Backend sent data.
40+
*/
41+
onDataReceived(data) {
42+
if (this.childTerminal && this.props.status === CONNECTED) {
43+
this.childTerminal.onDataReceived(data);
44+
}
45+
}
46+
47+
/**
48+
* Backend closed connection.
49+
*/
50+
onConnectionClosed(reason) {
51+
if (this.childTerminal) {
52+
this.childTerminal.onConnectionClosed(reason);
53+
}
54+
}
55+
56+
focusTerminal = () => {
57+
this.childTerminal && this.childTerminal.focus();
58+
};
59+
60+
render() {
61+
const { id, status, topClassName } = this.props;
62+
const idPrefix = `${id || 'id'}-serialconsole`;
63+
64+
let terminal;
65+
let isDisconnectEnabled = false;
66+
switch (status) {
67+
case CONNECTED:
68+
terminal = (
69+
<XTerm
70+
ref={c => {
71+
this.childTerminal = c;
72+
}}
73+
cols={this.props.cols}
74+
rows={this.props.rows}
75+
onConnect={this.props.onConnect}
76+
onDisconnect={this.props.onDisconnect}
77+
onData={this.props.onData}
78+
onTitleChanged={this.props.onTitleChanged}
79+
onResize={this.props.onResize}
80+
/>
81+
);
82+
isDisconnectEnabled = true;
83+
break;
84+
case DISCONNECTED:
85+
terminal = (
86+
<EmptyState>
87+
<EmptyState.Title>
88+
{this.props.textDisconnectedTitle}
89+
</EmptyState.Title>
90+
<EmptyState.Info>{this.props.textDisconnected}</EmptyState.Info>
91+
<EmptyState.Action>
92+
<Button
93+
bsStyle="primary"
94+
bsSize="large"
95+
onClick={this.props.onConnect}
96+
>
97+
{this.props.textConnect}
98+
</Button>
99+
</EmptyState.Action>
100+
</EmptyState>
101+
);
102+
break;
103+
case LOADING:
104+
default:
105+
terminal = <span>{this.props.textLoading}</span>;
106+
break;
107+
}
108+
109+
return (
110+
<div className={topClassName} id={id}>
111+
<SerialConsoleActions
112+
idPrefix={idPrefix}
113+
isDisconnectEnabled={isDisconnectEnabled}
114+
onDisconnect={this.onDisconnectClick}
115+
onReset={this.onResetClick}
116+
textDisconnect={this.props.textDisconnect}
117+
textReconnect={this.props.textReconnect}
118+
/>
119+
<div className="panel-body console-terminal-pf">{terminal}</div>
120+
</div>
121+
);
122+
}
123+
}
124+
125+
SerialConsole.propTypes = {
126+
/** Initiate connection to backend. In other words, the calling components manages connection state. */
127+
onConnect: PropTypes.func.isRequired,
128+
/** Close connection to backend */
129+
onDisconnect: PropTypes.func.isRequired,
130+
/** Terminal has been resized, backend shall be informed. (rows, cols) => {} */
131+
onResize: PropTypes.func,
132+
/** Terminal produced data, like key-press */
133+
onData: PropTypes.func,
134+
/** Terminal title has been changed. */
135+
onTitleChanged: PropTypes.func,
136+
137+
/** Connection status, a value from [''connected', 'disconnected', 'loading']. Default is 'loading' for a not matching value. */
138+
status: PropTypes.string.isRequired,
139+
id: PropTypes.string,
140+
141+
/** Size of the terminal component */
142+
rows: PropTypes.number,
143+
cols: PropTypes.number,
144+
145+
/** Enable customization */
146+
topClassName: PropTypes.string,
147+
148+
/** Localization */
149+
textDisconnect: PropTypes.string,
150+
textDisconnectedTitle: PropTypes.string,
151+
textDisconnected: PropTypes.string,
152+
textLoading: PropTypes.string,
153+
textReconnect: PropTypes.string,
154+
textConnect: PropTypes.string
155+
};
156+
157+
SerialConsole.defaultProps = {
158+
topClassName: '',
159+
160+
id: '',
161+
rows: 25,
162+
cols: 80,
163+
164+
onTitleChanged: noop,
165+
onData: noop,
166+
onResize: noop,
167+
168+
textDisconnectedTitle: 'Disconnected from serial console',
169+
textDisconnected: 'Click Connect to open serial console.',
170+
textLoading: 'Loading ...',
171+
textConnect: 'Connect',
172+
textDisconnect: undefined /** Default is set in SerialConsoleActions */,
173+
textReconnect: undefined /** Default is set in SerialConsoleActions */
174+
};
10175

11176
export default SerialConsole;
Lines changed: 144 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,148 @@
1-
/* eslint-disable import/no-extraneous-dependencies */
21
import React from 'react';
2+
33
import { storiesOf } from '@storybook/react';
4-
import { withInfo } from '@storybook/addon-info';
5-
import { inlineTemplate } from '../../../../storybook/decorators/storyTemplates';
6-
import SerialConsole from './SerialConsole';
7-
8-
const stories = storiesOf('@patternfly-react/console', module);
9-
10-
stories.add(
11-
'SerialConsole',
12-
withInfo()(() => {
13-
const story = <SerialConsole />;
14-
return inlineTemplate({
15-
story,
16-
title: 'SerialConsole'
17-
});
4+
import { defaultTemplate } from '../../../../storybook/decorators/storyTemplates';
5+
6+
import { SerialConsole } from './index';
7+
import { CONNECTED, DISCONNECTED, LOADING } from './constants';
8+
9+
const stories = storiesOf('SerialConsole', module);
10+
stories.addDecorator(
11+
defaultTemplate({
12+
title: 'SerialConsole',
13+
description:
14+
'This is an example of the SerialConsole component. For the purpose of this example, there is just a mock backend.'
1815
})
1916
);
17+
18+
/* eslint no-console: ["warn", { allow: ["log"] }] */
19+
const { log } = console; // let's keep these trace messages for tutoring purposes
20+
21+
const timeoutIds = [];
22+
/**
23+
* The SerialConsoleConnector component is consumer-specific and wraps the communication with backend.
24+
* For the purpose of this storybook, the backend is just mimicked.
25+
*/
26+
class SerialConsoleConnector extends React.Component {
27+
state = { status: LOADING, passKeys: false };
28+
29+
onBackendDisconnected = () => {
30+
log('Backend has disconnected, pass the info to the UI component');
31+
if (this.childSerialconsole) {
32+
this.childSerialconsole.onConnectionClosed(
33+
'Reason for disconnect provided by backend.'
34+
);
35+
}
36+
37+
this.setState({
38+
passKeys: false,
39+
status: DISCONNECTED // will close the terminal window
40+
});
41+
};
42+
43+
onConnect = () => {
44+
log('SerialConsoleConnector.onConnect(), ', this.state);
45+
this.setConnected();
46+
this.tellFairyTale();
47+
};
48+
49+
onData = data => {
50+
log(
51+
'UI terminal component produced data, i.e. a key was pressed, pass it to backend. [',
52+
data,
53+
']'
54+
);
55+
56+
// Normally, the "data" shall be passed to the backend which might send them back via onData() call
57+
// Since there is no backend, let;s pass them to UI component immediately.
58+
if (this.state.passKeys) {
59+
this.onDataFromBackend(data);
60+
}
61+
};
62+
63+
onDataFromBackend = data => {
64+
log('Backend sent data, pass them to the UI component. [', data, ']');
65+
if (this.childSerialconsole) {
66+
this.childSerialconsole.onDataReceived(data);
67+
}
68+
};
69+
70+
onDisconnect = () => {
71+
this.setState({
72+
status: DISCONNECTED
73+
});
74+
timeoutIds.forEach(id => clearTimeout(id));
75+
};
76+
77+
onResize = (rows, cols) => {
78+
log(
79+
'UI has been resized, pass this info to backend. [',
80+
rows,
81+
', ',
82+
cols,
83+
']'
84+
);
85+
};
86+
87+
setConnected = () => {
88+
this.setState({
89+
status: CONNECTED,
90+
passKeys: true
91+
});
92+
};
93+
94+
tellFairyTale = () => {
95+
let time = 1000;
96+
timeoutIds.push(
97+
setTimeout(
98+
() => this.onDataFromBackend(' This is a mock terminal. '),
99+
time
100+
)
101+
);
102+
103+
time += 1000;
104+
timeoutIds.push(
105+
setTimeout(
106+
() => this.onDataFromBackend(' Something is happening! '),
107+
time
108+
)
109+
);
110+
111+
time += 1000;
112+
timeoutIds.push(
113+
setTimeout(
114+
() => this.onDataFromBackend(' Something is happening! '),
115+
time
116+
)
117+
);
118+
119+
time += 1000;
120+
timeoutIds.push(
121+
setTimeout(
122+
() => this.onDataFromBackend(' Backend will be disconnected shortly. '),
123+
time
124+
)
125+
);
126+
127+
time += 5000;
128+
timeoutIds.push(setTimeout(this.onBackendDisconnected, time));
129+
};
130+
131+
render() {
132+
return (
133+
<SerialConsole
134+
onConnect={this.onConnect}
135+
onDisconnect={this.onDisconnect}
136+
onResize={this.onResize()}
137+
onData={this.onData}
138+
id="my-serialconsole"
139+
status={this.state.status}
140+
ref={c => {
141+
this.childSerialconsole = c;
142+
}}
143+
/>
144+
);
145+
}
146+
}
147+
148+
stories.addWithInfo('SerialConsole', () => <SerialConsoleConnector />);

packages/console/src/SerialConsole/SerialConsole.test.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
import React from 'react';
33
import { shallow } from 'enzyme';
44
import SerialConsole from './SerialConsole';
5+
import { noop } from 'patternfly-react';
56

67
test('placeholder render test', () => {
7-
const view = shallow(<SerialConsole />);
8+
const view = shallow(<SerialConsole onConnect={noop} onDisconnect={noop} status='loading'/>);
89
expect(view).toMatchSnapshot();
910
});

0 commit comments

Comments
 (0)