| // 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") |
| ); |