blob: 785c84a9f9a0be4cb955ede1b4adc51e86285692 [file] [log] [blame]
// Copyright 2015 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// Requires:
// NavBar.jsx :: NavBar
// FixtureWidget.jsx :: FixtureWidget
// TerminalWindow.jsx :: TerminalWindow, UploadProgress
// CameraWindow.jsx :: CameraWindow
//
// View for Dashboard App:
// - App
// - NavBar
// - SideBar
// - ClientBox
// - FilterInput
// - ClientList
// - [ClientInfo ...]
// - RecentList
// - ClientInfo
// - Windows
// - [TerminalWindow ...]
// - [CameraWindow ...]
// - UploadProgress
// - FixtureGroup
// - [FixtureWidget ...]
var App = React.createClass({
mixins: [BaseApp],
addTerminal: function (id, term) {
this.setState(function (state, props) {
state.terminals[id] = term;
});
},
addFixture: function (client) {
if (this.isClientInList(this.state.fixtures, client)) {
return;
}
// compute how many fixtures we can put in the screen
var screen = {
width: window.innerWidth,
};
var sidebar = ReactDOM.findDOMNode(this.refs.sidebar).getBoundingClientRect();
screen.width -= sidebar.right;
var nFixturePerRow = Math.floor(
screen.width / (FIXTURE_WINDOW_WIDTH + FIXTURE_WINDOW_MARGIN * 2));
nFixturePerRow = Math.max(1, nFixturePerRow);
var nTotalFixture = Math.min(2 * nFixturePerRow, 8);
// only keep recently opened @nTotalFixture fixtures.
this.setState(function (state, props) {
state.fixtures.push(client);
return {fixtures: state.fixtures.slice(-nTotalFixture)};
});
},
addCamera: function (id, cam) {
this.setState(function (state, props) {
state.cameras[id] = cam;
});
},
toggleFixtureState: function (client) {
if (this.isClientInList(this.state.fixtures, client)) {
this.removeFixture(client.mid);
} else {
this.addFixture(client);
}
},
removeTerminal: function (id) {
this.setState(function (state, props) {
if (typeof(state.terminals[id]) != "undefined") {
delete state.terminals[id];
}
});
},
removeFixture: function (id) {
this.setState(function (state, props) {
this.removeClientFromList(state.fixtures, {mid: id});
});
},
removeCamera: function (id) {
this.setState(function (state, props) {
if (typeof(state.cameras[id]) != "undefined") {
delete state.cameras[id];
}
});
},
getInitialState: function () {
return {cameras: [], fixtures: [], recentclients: [], terminals: {}};
},
componentDidMount: function () {
var socket = io(window.location.protocol + "//" + window.location.host,
{path: "/api/socket.io/"});
this.socket = socket;
socket.on("agent joined", function (msg) {
var client = JSON.parse(msg);
this.addClient(client);
this.state.recentclients.splice(0, 0, client);
this.state.recentclients = this.state.recentclients.slice(0, 5);
}.bind(this));
socket.on("agent left", function (msg) {
var client = JSON.parse(msg);
this.removeClientFromList(this.state.clients, client);
this.removeClientFromList(this.state.recentclients, client);
this.removeFixture(client.mid);
}.bind(this));
// Initiate a file download
socket.on("file download", function (sid) {
var url = window.location.protocol + "//" + window.location.host +
"/api/file/download/" + sid;
$("<iframe id='" + sid + "' src='" + url + "' style='display:none'>" +
"</iframe>").appendTo('body');
});
},
render: function () {
return (
<div id="main">
<NavBar name="Dashboard" url="/api/apps/list" ref="navbar" />
<div id="container">
<SideBar clients={this.getFilteredClientList()} ref="sidebar"
recentclients={this.state.recentclients} app={this} />
<FixtureGroup data={this.state.fixtures} app={this} />
</div>
<div className="windows">
<Windows app={this} terminals={this.state.terminals}
cameras={this.state.cameras} />
</div>
</div>
);
}
});
var SideBar = React.createClass({
render: function () {
return (
<div className="sidebar">
<ClientBox data={this.props.clients} app={this.props.app} />
<RecentList data={this.props.recentclients} app={this.props.app} />
</div>
);
}
});
var ClientBox = React.createClass({
render: function () {
return (
<div className="client-box panel panel-success">
<div className="panel-heading">Clients</div>
<div className="panel-body">
<FilterInput app={this.props.app} />
<ClientList data={this.props.data} app={this.props.app} />
</div>
</div>
);
}
})
var FilterInput = React.createClass({
onKeyUp: function (event) {
this.props.app.setMidFilterPattern(this.refs.filter.value);
},
render: function () {
return (
<div>
<input type="text" className="filter-input form-control" ref="filter"
placeholder="keyword" onKeyUp={this.onKeyUp}></input>
</div>
)
}
});
var ClientList = React.createClass({
render: function () {
return (
<div className="list-box client-list">
{
this.props.data.map(function (item) {
return (
<ClientInfo key={item.mid} data={item} app={this.props.app}>
{abbr(item.mid, 36)}
</ClientInfo>
);
}.bind(this))
}
</div>
);
}
});
var RecentList = React.createClass({
render: function () {
return (
<div className="recent-box panel panel-info">
<div className="panel-heading">Recent Connected Clients</div>
<div className="panel-body">
<div className="list-box recent-list">
{
this.props.data.map(function (item) {
return (
<ClientInfo key={item.mid} data={item} app={this.props.app}>
{abbr(item.mid, 36)}
</ClientInfo>
);
}.bind(this))
}
</div>
</div>
</div>
)
}
});
var ClientInfo = React.createClass({
openTerminal: function (event) {
this.props.app.addTerminal(randomID(), this.props.data);
},
openCamera: function (event) {
this.props.app.addCamera(this.props.data.mid, this.props.data);
},
onUIButtonClick: function (event) {
this.props.app.toggleFixtureState(this.props.data);
},
componentDidMount: function (event) {
// Since the button covers the machine ID text, abbrieviate to match the
// current visible width.
var chPerLine = 50;
var pxPerCh = this.refs.mid.clientWidth / chPerLine;
this.refs.mid.innerText =
abbr(this.refs.mid.innerText,
chPerLine - (this.refs["info-buttons"].clientWidth)/ pxPerCh);
},
render: function () {
var display = "block";
var ui_span = null;
var cam_span = null;
if (typeof(this.props.data.properties) != "undefined" &&
typeof(this.props.data.properties.context) != "undefined" &&
this.props.data.properties.context.indexOf("ui") !== -1) {
var ui_state = this.props.app.isClientInList(
this.props.app.state.fixtures, this.props.data);
var ui_light_css = LIGHT_CSS_MAP[ui_state ? "light-toggle-on"
: "light-toggle-off"];
ui_span = (
<div className={"label " + ui_light_css + " client-info-button"}
data-mid={this.props.data.key} onClick={this.onUIButtonClick}>
UI
</div>
);
}
if (typeof(this.props.data.properties) != "undefined" &&
typeof(this.props.data.properties.context) != "undefined" &&
this.props.data.properties.context.indexOf("cam") !== -1) {
cam_span = (
<div className="label label-success client-info-button"
data-mid={this.props.data.key} onClick={this.openCamera}>
CAM
</div>
);
}
return (
<div className="client-info">
<div className="client-info-mid" ref="mid">
{this.props.children}
</div>
<div className="client-info-buttons" ref="info-buttons">
{cam_span}
{ui_span}
<div className="label label-warning client-info-button"
data-mid={this.props.data.key} onClick={this.openTerminal}>
Terminal
</div>
</div>
</div>
);
}
});
var Windows = React.createClass({
render: function () {
var onTerminalControl = function (control) {
if (control.type == "sid") {
this.terminal_sid = control.data;
this.props.app.socket.emit("subscribe", control.data);
}
};
var onTerminalCloseClicked = function (event) {
this.props.app.removeTerminal(this.props.id);
this.props.app.socket.emit("unsubscribe", this.terminal_sid);
};
var onCameraCloseClicked = function (event) {
this.props.app.removeCamera(this.props.id);
}
// We need to make TerminalWindow and CameraWindow have the same parent
// div so z-index stacking works.
return (
<div>
<div className="windows">
{
Object.keys(this.props.terminals).map(function (id) {
var term = this.props.terminals[id];
var extra = "";
if (typeof(term.path) != "undefined") {
extra = "?tty_device=" + term.path;
}
return (
<TerminalWindow key={id} mid={term.mid} id={id} title={term.mid}
path={"/api/agent/tty/" + term.mid + extra}
uploadPath={"/api/agent/upload/" + term.mid}
enableMaximize={true}
app={this.props.app} progressBars={this.refs.uploadProgress}
onControl={onTerminalControl}
onCloseClicked={onTerminalCloseClicked} />
);
}.bind(this))
}
{
Object.keys(this.props.cameras).map(function (id) {
var cam = this.props.cameras[id];
var cam_prop = cam.properties.camera;
if (typeof(cam_prop) != "undefined") {
var command = cam_prop.command;
var width = cam_prop.width || 640;
var height = cam_prop.height || 640;
return (
<CameraWindow key={id} mid={cam.mid} id={id} title={cam.mid}
path={"/api/agent/shell/" + cam.mid + "?command=" +
encodeURIComponent(command)}
width={width} height={height} app={this.props.app}
onCloseClicked={onCameraCloseClicked} />
);
}
}.bind(this))
}
</div>
<div className="upload-progress">
<UploadProgress ref="uploadProgress" />
</div>
</div>
);
}
});
var FixtureGroup = React.createClass({
render: function () {
return (
<div className="fixture-group">
{
this.props.data.map(function (item) {
return (
<FixtureWidget key={item.mid} client={item}
app={this.props.app} />
);
}.bind(this))
}
</div>
);
}
});
ReactDOM.render(
<App url="/api/agents/list" />,
document.getElementById("body")
);