How to create in browser terminal using react and nodejs

Saurabh Mhatre
CodeClassifiers
Published in
6 min readMar 24, 2017
Image source:Pixabay

In the last tutorial we had set up a docker instance to store files in cloud using openstack swift. Today we are going to create a in browser terminal in reactjs and run commands on remote server similar to ones used by cloud ide’s like c9,koding among others.

Set up a new project if you haven’t already using create react app:

sudo npm install -g create-react-appcreate-react-app terminal

We are first going to create in browser terminal to run commands from frontend using react-bash,use material ui for creating ui and superagent module to send requests to backend.Here is the complete package.json for reference:

{
"name": "terminal",
"version": "0.1.0",
"private": true,
"devDependencies": {
"react-scripts": "0.9.5"
},
"dependencies": {
"material-ui": "^0.17.1",
"react": "^15.4.2",
"react-bash": "^1.5.3",
"react-dom": "^15.4.2",
"react-router": "^4.0.0",
"react-router-dom": "^4.0.0",
"react-tap-event-plugin": "^2.0.1",
"superagent": "^3.5.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}

Copy the dependencies into your project’s package.json and run npm install to install all the modules.

Next create a new component called TerminalAccess.js and start writing the following code:

import React, { Component } from 'react';
import {Tabs, Tab} from 'material-ui/Tabs';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
export default class TerminalAccess extends Component {
constructor(props){
super(props);
this.state={
commandData:''
}
}
render(){
<MuiThemeProvider>
<Tabs>
<Tab label="Docker Administration" >
<center>
<div className="parentContainer">
<h4>Run docker administration commands here</h4>
<div>
{this.state.commandData}
</div>
</div>
</center>
</Tab>
</Tabs>
</MuiThemeProvider>
}
}

This should bring up basic page with a single tabbar page.

Next initialize terminal in the component by making following modifications:

...
import Terminal from 'react-bash';
const history = [
{ value: 'Type `help` to begin' },
];
const structure = {
public: {
file1: { content: 'The is the content for file1 in the <public> directory.' },
file2: { content: 'The is the content for file2 in the <public> directory.' },
file3: { content: 'The is the content for file3 in the <public> directory.' },
},
'README.md': { content: 'Some readme' },
};
export default class TerminalAccess extends Component {
...
render(){
const extensions = {
sudo: {
exec: ({ structure, history, cwd }) => {
return { structure, cwd,
history: history.concat({ value: 'Nice try...' }),
};
},
},
}
return(
<MuiThemeProvider>
<Tabs>
<Tab label="Docker Administration" >
<center>
<div className="parentContainer">
<h4>Run docker administration commands here</h4>
<div style={{flex:1}}>
<Terminal history={history} structure={structure} extensions={extensions} prefix={"user@HOMEPC"}/>
</div>
<div>
{this.state.commandData}
</div>
</div>
</center>
</Tab>
</Tabs>
</MuiThemeProvider>
)
}
}

This should bring up a basic terminal in the browser with basic terminal commands working.

Taking this a step further we will now send commands entered by the user to a remote server(localhost in this example) and run them directly in localhost terminal.

We first need to create backend apis to handle input commands from frontend in nodejs as follows:

I am going to follow same directory structure we kept in last tutorial for creating mysql dashboard which is as follows:

├── package.json
├── routes
│ ├── commandroutes.js
│ └── dbroutes.js
└── server.js

We need node-cmd module which allows us to run commands from nodejs server to system shell.

We will first create commandroutes.js as follows:

let cmd=require('node-cmd');exports.commandrun = function(req,res){
console.log("command req",req.body.command);
cmd.get(
req.body.command,
function(data){
res.send({
"code":"200",
"result":data
})
}
);
}

Next modify server.js to handle new request handler:

var express    = require("express");
var bodyParser = require('body-parser');
let commandroutes = require('./routes/commandroutes');
let dbroutes = require('./routes/dbroutes');
var app = express();
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
var router = express.Router();
// test route
router.get('/', function(req, res) {
res.json({ message: 'welcome to our dockerdashboard module apis' });
});
router.post('/commandrun',commandroutes.commandrun);
router.get('/getAllRecords',dbroutes.getAllRecords);
app.use('/api', router);
app.listen(5000);

Make sure that you run node server with sudo prefix to allow node-cmd to run commands with superuser privileges:

sudo node server.js

Now let us get back to development in frontend. We need to extend react-bash modules capabilities to handle commands which don’t exist in its commands file found here.The command example I am taking is docker ps command which is used to get the list of currently running docker instances.The reason for choosing this specific command is that it requires superuser privileges to run properly and we started our node server for that purpose itself. If you have not set up docker on your machine then you can go through the setup as explained in my previous article here.

We will first define a function to send data to node server from react-bash:

import request from 'superagent';
var apiBaseUrl = "http://localhost:5000/api/";
let getData=function(input,callback){
request
.post(apiBaseUrl+'commandrun')
.send({ command: input})
.set('Accept', 'application/json')
.end(function(err, res){
// Calling the end function will send the request
if(err){
callback(err)
}
callback(res);
});
}
export default class TerminalAccess extends Component {
...
}

Next we need to modify extensions in the render function as follows:

import {Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn} from 'material-ui/Table';
export default class TerminalAccess extends Component {
...
render(){
const extensions = {
sudo: {
exec: ({ structure, history, cwd }) => {
return { structure, cwd,
history: history.concat({ value: 'Nice try...' }),
};
},
},
docker:{
exec: ({ structure, history, cwd },command) => {
let fetchData = getData(command.input,(res) =>{
var response = JSON.parse(res.text).result.split("\n");
var tablerowData = response[1].split(" ");
tablerowData = tablerowData.filter(function(item){
return item.length>3;
})
var tableheaderData = response[0].split(" ");
tableheaderData = tableheaderData.filter(function(item){
return item.length>3;
})
var table=[],tableHeaderColumn=[],tableHeader=[],tableRowColumn=[],tableBody=[];
tableheaderData.map(function(item){
tableHeaderColumn.push(
<TableHeaderColumn>
{item}
</TableHeaderColumn>
)
return item;
})
tableHeader.push(
<TableHeader>
<TableRow>
{tableHeaderColumn}
</TableRow>
</TableHeader>
)
tablerowData.map(function(item){
tableRowColumn.push(
<TableRowColumn>
{item}
</TableRowColumn>
)
return item;
})
tableBody.push(
<TableBody>
<TableRow>
{tableRowColumn}
</TableRow>
</TableBody>
)
table.push(
<Table>
{tableHeader}
{tableBody}
</Table>
)
this.setState({commandData:table})
})
return { structure, cwd, history: history.concat({ value: 'Nice try...' }), };;
},
}
};
}
}

Whenever user types any docker related command, the docker extension is executed which sends data to node server and receives data back from node server in res callback.

Note that code in res callback is to simply format the data into proper table instead of simply displaying the data as it is:

Terminal emulator

You can find the source code for this project on github here

Bonus Tips:

The goal of this project was not just to create terminal in browser but to add additional capabilities to existing terminals.For example in our case instead of simply displaying list of running docker instances we created a check list which can be further customized to allow removal,stopping and monitoring docker instances by adding features in the check list.

Terminals have existed from quite a few decades, but additional capabilities to terminals are limited to customizing them with external plugins or modifying bash_profile.

Browsers provides us a perfect platform to extend terminal capabilities and create new shells which are close to specialized software themselves thereby boosting developer workflows by leaps and bounds.

If you like the idea behind the project then feel free to fork it and contribute towards creating smarter terminals by creating your own custom commands.

Ex. A simple ls command can be extended to display files as folders found in windows/linux desktop environment and allow editing, adding and removing files right in the terminal itself instead of running vim,mkdir and the dreaded rm commands.

Connect Deeper:

In the next tutorial I will be covering how to create an in browser nodejs debugger for smarter json parsing,error & syntax highlighting similar to current chrome console, so follow our facebook page here: Technoetics

--

--

Saurabh Mhatre
CodeClassifiers

Senior Frontend Developer with 9+ years industry experience. Content creator on Youtube and Medium. LinkedIn/Twitter/Instagram: @SaurabhNative