http://git-wip-us.apache.org/repos/asf/ambari/blob/cf5c068c/contrib/views/storm/src/main/resources/scripts/views/ComponentDetailView.jsx ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/scripts/views/ComponentDetailView.jsx b/contrib/views/storm/src/main/resources/scripts/views/ComponentDetailView.jsx new file mode 100644 index 0000000..5847ef9 --- /dev/null +++ b/contrib/views/storm/src/main/resources/scripts/views/ComponentDetailView.jsx @@ -0,0 +1,534 @@ +/** + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at +* + http://www.apache.org/licenses/LICENSE-2.0 +* + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +define([ + 'jsx!components/Table', + 'jsx!modules/Table/Pagination', + 'react', + 'react-dom', + 'collections/BaseCollection', + 'models/VTopology', + 'jsx!components/Breadcrumbs', + 'jsx!components/SearchLogs', + 'jsx!views/ProfilingView', + 'utils/Utils', + 'bootbox', + 'bootstrap', + 'bootstrap-switch' + ],function(Table, Pagination, React, ReactDOM, BaseCollection, VTopology, Breadcrumbs, SearchLogs, ProfilingView, Utils, bootbox){ + 'use strict'; + + return React.createClass({ + displayName: 'ComponentDetailView', + propTypes: { + id: React.PropTypes.string.isRequired, + name: React.PropTypes.string.isRequired + }, + getInitialState: function(){ + this.model = new VTopology({'id': this.props.id}); + this.systemFlag = (this.props.name.startsWith('__')) ? true : false; + this.windowSize = ':all-time'; + this.initializeData(); + return { + componentObj: {}, + profilingModalOpen: false + }; + }, + componentWillMount: function(){ + $('.loader').show(); + }, + componentWillUpdate: function(){ + $('.loader').show(); + $('#collapse-input').off('hidden.bs.collapse').off('shown.bs.collapse'); + $('#collapse-output').off('hidden.bs.collapse').off('shown.bs.collapse'); + $('#collapse-executor').off('hidden.bs.collapse').off('shown.bs.collapse'); + $('#collapse-error').off('hidden.bs.collapse').off('shown.bs.collapse'); + }, + componentDidMount: function(){ + $(".boot-switch.systemSum").bootstrapSwitch({ + size: 'small', + onSwitchChange: function(event, state){ + this.systemFlag = state; + this.initializeData(); + }.bind(this) + }); + + $(".boot-switch.debug").bootstrapSwitch({ + size: 'small', + onSwitchChange: function(event, state){ + this.debugAction(state); + }.bind(this) + }); + $('.loader').hide(); + }, + componentDidUpdate: function(){ + $('#collapse-input').on('hidden.bs.collapse', function () { + $("#input-box").toggleClass("fa-compress fa-expand"); + }).on('shown.bs.collapse', function() { + $("#input-box").toggleClass("fa-compress fa-expand"); + }); + + $('#collapse-output').on('hidden.bs.collapse', function () { + $("#output-box").toggleClass("fa-compress fa-expand"); + }).on('shown.bs.collapse', function() { + $("#output-box").toggleClass("fa-compress fa-expand"); + }); + + $('#collapse-executor').on('hidden.bs.collapse', function () { + $("#executor-box").toggleClass("fa-compress fa-expand"); + }).on('shown.bs.collapse', function() { + $("#executor-box").toggleClass("fa-compress fa-expand"); + }); + + $('#collapse-error').on('hidden.bs.collapse', function () { + $("#error-box").toggleClass("fa-compress fa-expand"); + }).on('shown.bs.collapse', function() { + $("#error-box").toggleClass("fa-compress fa-expand"); + }); + $('#modal-profiling').on('hidden.bs.modal', function (e) { + this.initializeData(); + this.setState({"profilingModalOpen":false}); + }.bind(this)); + if(this.state.profilingModalOpen){ + $('#modal-profiling').modal("show"); + } + $('.loader').hide(); + }, + initializeData: function(){ + this.model.getComponent({ + id: this.props.id, + name: this.props.name, + window: this.windowSize, + sys: this.systemFlag, + success: function(model, response){ + if(response.error || model.error){ + Utils.notifyError(response.error || model.error+'('+model.errorMessage.split('(')[0]+')'); + } else { + this.setState({"componentObj": model}); + } + }.bind(this), + error: function(model, response, options){ + Utils.notifyError("Error occured in fetching topology component data."); + } + }); + }, + renderWindowOptions: function(){ + var arr = this.state.componentObj.spoutSummary || this.state.componentObj.boltStats; + if(arr){ + return arr.map(function(object, i){ + return ( <option key={i} value={object.window}>{object.windowPretty}</option> ); + }); + } else { + return null; + } + }, + handleWindowChange: function(e){ + this.windowSize = e.currentTarget.value; + this.initializeData(); + }, + getLinks: function() { + var links = [ + {link: '#!/dashboard', title: 'Dashboard'}, + {link: '#!/topology', title: 'Topology Listing'}, + {link: '#!/topology/'+this.state.componentObj.topologyId, title: this.state.componentObj.name || ""}, + {link: 'javascript:void(0);', title: this.state.componentObj.id || ""} + ]; + return links; + }, + renderStatsRow: function(){ + var spoutFlag = (this.state.componentObj.componentType === 'spout' ? true: false); + var statsArr = this.state.componentObj.spoutSummary || this.state.componentObj.boltStats; + if(statsArr){ + return statsArr.map(function(stats, i){ + return ( + <tr key={i}> + <td>{stats.windowPretty}</td> + <td>{stats.emitted}</td> + <td>{stats.transferred}</td> + {spoutFlag ? <td>{stats.completeLatency}</td> : null} + {!spoutFlag ? <td>{stats.executeLatency}</td> : null} + {!spoutFlag ? <td>{stats.executed}</td> : null} + {!spoutFlag ? <td>{stats.processLatency}</td> : null} + <td>{stats.acked}</td> + <td>{stats.failed}</td> + </tr> + ); + }); + } + }, + renderAccordion: function(type, header, searchField, searchCb, collection, emptyText, columns, toggleCb){ + return ( + <div className="box"> + <div className="box-header" data-toggle="collapse" data-target={"#collapse-"+type} aria-expanded="false" aria-controls={"collapse-"+type}> + <h4>{header} ( {this.state.componentObj.windowHint} )</h4> + <h4 className="box-control"> + <a href="javascript:void(0);" className="primary"> + <i className="fa fa-compress" id={type+"-box"} onClick={toggleCb}></i> + </a> + </h4> + </div> + <div className="box-body collapse in" id={"collapse-"+type}> + <div className="input-group col-sm-4"> + <input type="text" onKeyUp={searchCb} className="form-control" placeholder={"Search by "+searchField} /> + <span className="input-group-btn"> + <button className="btn btn-primary" type="button"><i className="fa fa-search"></i></button> + </span> + </div> + <Table className="table table-striped" collection={collection} emptyText={emptyText} columns={columns()} /> + {type === 'error' ? <Pagination collection={collection} /> : null} + </div> + </div> + ); + }, + renderInputStats: function(){ + var inputCollection = Utils.ArrayToCollection(this.state.componentObj.inputStats, new BaseCollection()); + inputCollection.searchFields = ['component']; + var searchCb = function(e){ + var value = e.currentTarget.value; + inputCollection.search(value); + }; + var toggleCb = function(e){ + $("#collapse-input").collapse('toggle'); + } + return this.renderAccordion('input', 'Input Stats', 'component', searchCb, inputCollection, 'No input stats found !', this.getInputColumns, toggleCb); + }, + getInputColumns: function(){ + return [ + {name: 'component', title: 'Component', tooltip: 'The ID assigned to a the Component by the Topology.'}, + {name: 'stream', title: 'Stream', tooltip: 'The name of the Tuple stream given in the Topolgy, or "default" if none was given.'}, + {name: 'executeLatency', title: 'Execute Latency (ms)', tooltip: 'The average time a Tuple spends in the execute method. The execute method may complete without sending an Ack for the tuple.'}, + {name: 'executed', title: 'Executed', tooltip: 'The number of incoming Tuples processed.'}, + {name: 'processLatency', title: 'Process Latency (ms)', tooltip: 'The average time it takes to Ack a Tuple after it is first received. Bolts that join, aggregate or batch may not Ack a tuple until a number of other Tuples have been received.'}, + {name: 'acked', title: 'Acked', tooltip: 'The number of Tuples acknowledged by this Bolt.'}, + {name: 'failed', title: 'Failed', tooltip: 'The number of tuples Failed by this Bolt.'} + ]; + }, + renderOutputStats: function(){ + var outputCollection = Utils.ArrayToCollection(this.state.componentObj.outputStats, new BaseCollection()); + outputCollection.searchFields = ['stream']; + var searchCb = function(e){ + var value = e.currentTarget.value; + outputCollection.search(value); + }; + var toggleCb = function(e){ + $("#collapse-output").collapse('toggle'); + } + return this.renderAccordion('output', 'Output Stats', 'stream', searchCb, outputCollection, 'No output stats found !', this.getOutputColumns, toggleCb); + }, + getOutputColumns: function(){ + if(this.state.componentObj.componentType === 'spout'){ + return [ + {name: 'stream', title: 'Stream', tooltip: 'The name of the Tuple stream given in the Topolgy, or "default" if none was given.'}, + {name: 'emitted', title: 'Emitted', tooltip: 'The number of Tuples emitted.'}, + {name: 'transferred', title: 'Transferred', tooltip: 'The number of Tuples emitted that sent to one or more bolts.'}, + {name: 'completeLatency', title: 'Complete Latency (ms)', tooltip: 'The average time a Tuple "tree" takes to be completely processed by the Topology. A value of 0 is expected if no acking is done.'}, + {name: 'acked', title: 'Acked', tooltip: 'The number of Tuple "trees" successfully processed. A value of 0 is expected if no acking is done.'}, + {name: 'failed', title: 'Failed', tooltip: 'The number of Tuple "trees" that were explicitly failed or timed out before acking was completed. A value of 0 is expected if no acking is done.'} + ]; + } else { + return [ + {name: 'stream', title: 'Stream', tooltip: 'The name of the Tuple stream given in the Topolgy, or "default" if none was given.'}, + {name: 'emitted', title: 'Emitted', tooltip: 'The number of Tuples emitted.'}, + {name: 'transferred', title: 'Transferred', tooltip: 'The number of Tuples emitted that sent to one or more bolts.'} + ]; + } + }, + renderExecutorStats: function(){ + var executorCollection = Utils.ArrayToCollection(this.state.componentObj.executorStats, new BaseCollection()); + executorCollection.searchFields = ['id']; + var searchCb = function(e){ + var value = e.currentTarget.value; + executorCollection.search(value); + }; + var toggleCb = function(e){ + $("#collapse-executor").collapse('toggle'); + } + return this.renderAccordion('executor', 'Executor Stats', 'id', searchCb, executorCollection, 'No executor stats found !', this.getExecutorColumns, toggleCb); + }, + getExecutorColumns: function(){ + var self = this; + if(this.state.componentObj.componentType === 'spout'){ + return [ + {name: 'id', title: 'Id', tooltip: 'The unique executor ID.'}, + {name: 'uptime', title: 'Uptime', tooltip: 'The length of time an Executor (thread) has been alive.'}, + {name: 'port', title: 'Host:Port', tooltip: 'The hostname reported by the remote host. (Note that this hostname is not the result of a reverse lookup at the Nimbus node.) Click on it to open the logviewer page for this Worker.', component: React.createClass({ + propTypes: { + model: React.PropTypes.object.isRequired + }, + render: function(){ + return ( <a href={this.props.model.get('workerLogLink')} target="_blank"> {this.props.model.get('host')}:{this.props.model.get('port')} </a>); + } + })}, + {name: 'emitted', title: 'Emitted', tooltip: 'The number of Tuples emitted.'}, + {name: 'transferred', title: 'Transferred', tooltip: 'The number of Tuples emitted that sent to one or more bolts.'}, + {name: 'completeLatency', title: 'Complete Latency (ms)', tooltip: 'The average time a Tuple "tree" takes to be completely processed by the Topology. A value of 0 is expected if no acking is done.'}, + {name: 'acked', title: 'Acked', tooltip: 'The number of Tuple "trees" successfully processed. A value of 0 is expected if no acking is done.'}, + {name: 'failed', title: 'Failed', tooltip: 'The number of Tuple "trees" that were explicitly failed or timed out before acking was completed. A value of 0 is expected if no acking is done.'}, + {name: 'workerLogLink', title: 'Dumps', component: React.createClass({ + propTypes: { + model: React.PropTypes.object.isRequired + }, + render: function(){ + var link = this.props.model.get('workerLogLink'); + link = ""+link.split('/log')[0]+"/dumps/"+self.props.id+"/"+this.props.model.get('host')+":"+this.props.model.get('port'); + return (<a href={link} className="btn btn-primary btn-xs" target="_blank"><i className="fa fa-file-text"></i></a>); + } + })} + ]; + } else { + return [ + {name: 'id', title: 'Id', tooltip: 'The unique executor ID.'}, + {name: 'uptime', title: 'Uptime', tooltip: 'The length of time an Executor (thread) has been alive.'}, + {name: 'port', title: 'Host:Port', tooltip: 'The hostname reported by the remote host. (Note that this hostname is not the result of a reverse lookup at the Nimbus node.) Click on it to open the logviewer page for this Worker.', component: React.createClass({ + propTypes: { + model: React.PropTypes.object.isRequired + }, + render: function(){ + return ( <a href={this.props.model.get('workerLogLink')} target="_blank"> {this.props.model.get('host')}:{this.props.model.get('port')} </a>); + } + })}, + {name: 'emitted', title: 'Emitted', tooltip: 'The number of Tuples emitted.'}, + {name: 'transferred', title: 'Transferred', tooltip: 'The number of Tuples emitted that sent to one or more bolts.'}, + {name: 'capacity', title: 'Capacity (last 10m)', tooltip: "If this is around 1.0, the corresponding Bolt is running as fast as it can, so you may want to increase the Bolt's parallelism. This is (number executed * average execute latency) / measurement time."}, + {name: 'executeLatency', title: 'Execute Latency (ms)', tooltip: 'The average time a Tuple spends in the execute method. The execute method may complete without sending an Ack for the tuple.'}, + {name: 'executed', title: 'Executed', tooltip: 'The number of incoming Tuples processed.'}, + {name: 'processLatency', title: 'Process Latency (ms)', tooltip: 'The average time it takes to Ack a Tuple after it is first received. Bolts that join, aggregate or batch may not Ack a tuple until a number of other Tuples have been received.'}, + {name: 'acked', title: 'Acked', tooltip: 'The number of Tuples acknowledged by this Bolt.'}, + {name: 'failed', title: 'Failed', tooltip: 'The number of tuples Failed by this Bolt.'}, + {name: 'workerLogLink', title: 'Dumps', component: React.createClass({ + propTypes: { + model: React.PropTypes.object.isRequired + }, + render: function(){ + var link = this.props.model.get('workerLogLink'); + link = ""+link.split('/log')[0]+"/dumps/"+self.props.id+"/"+this.props.model.get('host')+":"+this.props.model.get('port'); + return (<a href={link} className="btn btn-primary btn-xs" target="_blank"><i className="fa fa-file-text"></i></a>); + } + })} + ]; + } + }, + renderErrorStats: function(){ + var errorCollection = Utils.ArrayToCollection(this.state.componentObj.componentErrors, new BaseCollection()); + errorCollection.searchFields = ['error']; + var searchCb = function(e){ + var value = e.currentTarget.value; + errorCollection.search(value); + }; + var toggleCb = function(e){ + $("#collapse-error").collapse('toggle'); + } + return this.renderAccordion('error', 'Error Stats', 'error', searchCb, errorCollection, 'No errors found !', this.getErrorColumns, toggleCb); + }, + getErrorColumns: function(){ + return [ + {name: 'errorTime', title: 'Time', component: React.createClass({ + propTypes: { + model: React.PropTypes.object.isRequired + }, + render: function(){ + if(this.props.model.get('errorTime') && this.props.model.get('errorTime') != 0) { + var d = new Date(this.props.model.get('errorTime') * 1000), + date = d.toLocaleDateString() + ' ' + d.toLocaleTimeString(); + return (<span>{date}</span>); + } else return (<span></span>); + } + })}, + {name: 'errorPort', title: 'Host:Port', component: React.createClass({ + propTypes: { + model: React.PropTypes.object.isRequired + }, + render: function(){ + return ( <a href={this.props.model.get('errorWorkerLogLink')} target="_blank"> {this.props.model.get('errorHost')}:{this.props.model.get('errorPort')} </a>); + } + })}, + {name: 'error', title: 'Error'} + ]; + }, + render: function() { + if(this.state.componentObj.debug){ + $(".boot-switch.debug").bootstrapSwitch('state', true, true); + } else { + $(".boot-switch.debug").bootstrapSwitch('state', false, true); + } + if(this.systemFlag){ + $(".boot-switch.systemSum").bootstrapSwitch('state', true, true); + } else { + $(".boot-switch.systemSum").bootstrapSwitch('state', false, true); + } + var spoutFlag = (this.state.componentObj.componentType === 'spout' ? true: false); + return ( + <div> + <Breadcrumbs links={this.getLinks()} /> + <SearchLogs id={this.state.componentObj.topologyId}/> + <div className="row"> + <div className="col-sm-12"> + <div className="box filter"> + <div className="box-body form-horizontal"> + <div className="form-group no-margin"> + <label className="col-sm-1 control-label">Window</label> + <div className="col-sm-2"> + <select className="form-control" onChange={this.handleWindowChange} value={this.windowSize}> + {this.renderWindowOptions()} + </select> + </div> + <label className="col-sm-2 control-label">System Summary</label> + <div className="col-sm-2"> + <input className="boot-switch systemSum" type="checkbox" /> + </div> + <label className="col-sm-1 control-label">Debug</label> + <div className="col-sm-1"> + <input className="boot-switch debug" type="checkbox"/> + </div> + <div className="col-sm-3 text-right"> + <div className="btn-group" role="group"> + <button type="button" className="btn btn-primary" onClick={this.handleProfiling} title="Profiling & Debugging" data-rel="tooltip"> + <i className="fa fa-cogs"></i> + </button> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + <div className="row"> + <div className="col-sm-4"> + <div className="summary-tile"> + <div className="summary-title">Component Summary</div> + <div className="summary-body"> + <p><strong>ID: </strong>{this.state.componentObj.id}</p> + <p><strong>Topology: </strong>{this.state.componentObj.name}</p> + <p><strong>Executors: </strong>{this.state.componentObj.executors}</p> + <p><strong>Tasks: </strong>{this.state.componentObj.tasks}</p> + <p><strong>Debug: </strong><a href={this.state.componentObj.eventLogLink} target="_blank">events</a></p> + </div> + </div> + </div> + <div className="col-sm-8"> + <div className="stats-tile"> + <div className="stats-title">{spoutFlag ? "Spout Stats" : "Bolt Stats"}</div> + <div className="stats-body"> + <table className="table table-condensed no-margin"> + <thead> + <tr> + <th><span data-rel="tooltip" title="The past period of time for which the statistics apply.">Window</span></th> + <th><span data-rel="tooltip" title="The number of Tuples emitted.">Emitted</span></th> + <th><span data-rel="tooltip" title="The number of Tuples emitted that sent to one or more bolts.">Transferred</span></th> + {spoutFlag ? <th><span data-rel="tooltip" title='The average time a Tuple "tree" takes to be completely processed by the Topology. A value of 0 is expected if no acking is done.'>Complete Latency (ms)</span></th> : null} + {!spoutFlag ? <th><span data-rel="tooltip" title="The average time a Tuple spends in the execute method. The execute method may complete without sending an Ack for the tuple.">Execute Latency (ms)</span></th> : null} + {!spoutFlag ? <th><span data-rel="tooltip" title="The number of incoming Tuples processed.">Executed</span></th> : null} + {!spoutFlag ? <th><span data-rel="tooltip" title="The average time it takes to Ack a Tuple after it is first received. Bolts that join, aggregate or batch may not Ack a tuple until a number of other Tuples have been received.">Process Latency (ms)</span></th> : null} + <th><span data-rel="tooltip" title={spoutFlag ? 'The number of Tuple "trees" successfully processed. A value of 0 is expected if no acking is done.' : "The number of Tuples acknowledged by this Bolt."}>Acked</span></th> + <th><span data-rel="tooltip" title={spoutFlag ? 'The number of Tuple "trees" that were explicitly failed or timed out before acking was completed. A value of 0 is expected if no acking is done.' : "The number of tuples Failed by this Bolt."}>Failed</span></th> + </tr> + </thead> + <tbody> + {this.renderStatsRow()} + </tbody> + </table> + </div> + </div> + </div> + </div> + <div className="row"> + <div className="col-sm-12"> + {this.state.componentObj.inputStats ? this.renderInputStats() : null} + {this.state.componentObj.outputStats ? this.renderOutputStats() : null} + {this.state.componentObj.executorStats ? this.renderExecutorStats() : null} + {this.state.componentObj.componentErrors ? this.renderErrorStats() : null} + </div> + </div> + {this.state.profilingModalOpen ? <ProfilingView modalId="modal-profiling" topologyId={this.props.id} executorStats={this.state.componentObj.executorStats} /> : null} + </div> + ); + }, + handleProfiling: function(){ + this.setState({"profilingModalOpen":true}); + }, + debugAction: function(toEnableFlag){ + if(toEnableFlag){ + bootbox.prompt({ + title: 'Do you really want to debug this component ? If yes, please, specify sampling percentage.', + value: this.state.componentObj.samplingPct ? this.state.componentObj.samplingPct : "10", + buttons: { + confirm: { + label: 'Yes', + className: "btn-success", + }, + cancel: { + label: 'No', + className: "btn-default", + } + }, + callback: function(result) { + if(result == null) { + $(".boot-switch.debug").bootstrapSwitch('toggleState', true); + } else if(result == "" || isNaN(result) || result < 0) { + Utils.notifyError("Enter valid sampling percentage"); + $(".boot-switch.debug").bootstrapSwitch('toggleState', true) + } else { + this.model.debugComponent({ + id: this.state.componentObj.topologyId, + name: this.state.componentObj.id, + debugType: 'enable', + percent: result, + success: function(model, response){ + if(response.error || model.error){ + Utils.notifyError(response.error || model.error+'('+model.errorMessage.split('(')[0]+')'); + } else { + this.initializeData(); + Utils.notifySuccess("Debugging enabled successfully."); + } + }.bind(this), + error: function(model, response, options){ + Utils.notifyError("Error occured in enabling debugging."); + } + }); + } + }.bind(this) + }); + } else { + var title = "Do you really want to stop debugging this component ?"; + var successCb = function(){ + this.model.debugComponent({ + id: this.state.componentObj.topologyId, + name: this.state.componentObj.id, + debugType: 'disable', + percent: '0', + success: function(model, response){ + if(response.error || model.error){ + Utils.notifyError(response.error || model.error+'('+model.errorMessage.split('(')[0]+')'); + } else { + this.initializeData(); + Utils.notifySuccess("Debugging disabled successfully."); + } + }.bind(this), + error: function(model, response, options){ + Utils.notifyError("Error occured in disabling debugging."); + } + }); + }.bind(this); + var cancelCb = function(){ + $(".boot-switch.debug").bootstrapSwitch('toggleState', true) + }.bind(this); + Utils.ConfirmDialog(' ', title, successCb, cancelCb); + } + }, + }); +}); \ No newline at end of file
http://git-wip-us.apache.org/repos/asf/ambari/blob/cf5c068c/contrib/views/storm/src/main/resources/scripts/views/Dashboard.jsx ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/scripts/views/Dashboard.jsx b/contrib/views/storm/src/main/resources/scripts/views/Dashboard.jsx new file mode 100644 index 0000000..3f4f682 --- /dev/null +++ b/contrib/views/storm/src/main/resources/scripts/views/Dashboard.jsx @@ -0,0 +1,65 @@ +/** + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at +* + http://www.apache.org/licenses/LICENSE-2.0 +* + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +define([ + 'jsx!components/Table', + 'jsx!components/RadialChart', + 'react', + 'react-dom', + 'jsx!containers/ClusterSummary', + 'jsx!containers/NimbusSummary', + 'jsx!containers/SupervisorSummary', + 'jsx!containers/TopologyListing', + 'jsx!containers/NimbusConfigSummary' + ],function(Table,RadialChart, React, ReactDOM, ClusterSummary, NimbusSummary, SupervisorSummary, TopologyListing, NimbusConfigSummary){ + 'use strict'; + + return React.createClass({ + displayName: 'Dashboard', + getInitialState: function(){ + return null; + }, + componentWillMount: function(){ + $('.loader').show(); + }, + componentDidMount: function(){ + $('.loader').hide(); + }, + componentWillUpdate: function(){ + $('.loader').show(); + }, + componentDidUpdate: function(){ + $('.loader').hide(); + }, + render: function() { + return ( + <div> + <div className="row" style={{marginTop: '20px'}}> + <ClusterSummary /> + <TopologyListing fromDashboard={true} /> + </div> + <div className="row"> + <div className="col-sm-12"> + <NimbusConfigSummary /> + </div> + </div> + </div> + ); + } + }); +}); \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ambari/blob/cf5c068c/contrib/views/storm/src/main/resources/scripts/views/Footer.jsx ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/scripts/views/Footer.jsx b/contrib/views/storm/src/main/resources/scripts/views/Footer.jsx new file mode 100644 index 0000000..98e63e9 --- /dev/null +++ b/contrib/views/storm/src/main/resources/scripts/views/Footer.jsx @@ -0,0 +1,48 @@ +/** + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at +* + http://www.apache.org/licenses/LICENSE-2.0 +* + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +define(['react', 'react-dom', 'models/VCluster', 'utils/Utils'], function(React, ReactDOM, VCluster, Utils) { + 'use strict'; + return React.createClass({ + displayName: 'Footer', + getInitialState: function(){ + this.initializeData(); + return { + version: 0 + }; + }, + initializeData: function(){ + this.model = new VCluster(); + this.model.fetch({ + success: function(model, response){ + if(response.error || model.error){ + Utils.notifyError(response.error || model.error+'('+model.errorMessage.split('(')[0]+')'); + } else { + this.setState({version: model.get('stormVersion')}); + } + }.bind(this), + error: function(model, response, options){ + Utils.notifyError("Error occured in fetching cluster summary data."); + } + }); + }, + render: function() { + return (<p className="text-center">Apache Storm - v{this.state.version}</p>); + } + }); +}); \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ambari/blob/cf5c068c/contrib/views/storm/src/main/resources/scripts/views/NimbusSummaryView.jsx ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/scripts/views/NimbusSummaryView.jsx b/contrib/views/storm/src/main/resources/scripts/views/NimbusSummaryView.jsx new file mode 100644 index 0000000..6221d3f --- /dev/null +++ b/contrib/views/storm/src/main/resources/scripts/views/NimbusSummaryView.jsx @@ -0,0 +1,65 @@ +/** + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at +* + http://www.apache.org/licenses/LICENSE-2.0 +* + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +define([ + 'jsx!components/Table', + 'react', + 'react-dom', + 'jsx!containers/NimbusSummary', + 'jsx!components/Breadcrumbs' + ],function(Table, React, ReactDOM, NimbusSummary, Breadcrumbs){ + 'use strict'; + + return React.createClass({ + displayName: 'NimbusSummaryView', + getInitialState: function(){ + return null; + }, + componentWillMount: function(){ + $('.loader').show(); + }, + componentDidMount: function(){ + $('.loader').hide(); + }, + componentWillUpdate: function(){ + $('.loader').show(); + }, + componentDidUpdate: function(){ + $('.loader').hide(); + }, + render: function() { + return ( + <div> + <Breadcrumbs links={this.getLinks()} /> + <div className="row"> + <div className="col-sm-12"> + <NimbusSummary/> + </div> + </div> + </div> + ); + }, + getLinks: function() { + var links = [ + {link: '#!/dashboard', title: 'Dashboard'}, + {link: 'javascript:void(0);', title: 'Nimbus Summary'} + ]; + return links; + } + }); +}); \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ambari/blob/cf5c068c/contrib/views/storm/src/main/resources/scripts/views/ProfilingView.jsx ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/scripts/views/ProfilingView.jsx b/contrib/views/storm/src/main/resources/scripts/views/ProfilingView.jsx new file mode 100644 index 0000000..f5ffefe --- /dev/null +++ b/contrib/views/storm/src/main/resources/scripts/views/ProfilingView.jsx @@ -0,0 +1,214 @@ +/** + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at +* + http://www.apache.org/licenses/LICENSE-2.0 +* + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +define(['react', + 'react-dom', + 'collections/BaseCollection', + 'models/VTopology', + 'utils/Utils', + 'utils/Globals', + 'jsx!components/Table', + 'bootstrap' + ], function(React, ReactDOM, BaseCollection, VTopology, Utils, Globals, Table) { + 'use strict'; + return React.createClass({ + displayName: 'Profiling', + propTypes: { + modalId: React.PropTypes.string.isRequired, + topologyId: React.PropTypes.string.isRequired, + executorStats: React.PropTypes.array.isRequired + }, + getInitialState: function(){ + this.model = new VTopology(); + this.selectedWorker = []; + return null; + }, + componentWillMount: function(){ + this.syncData(); + }, + componentDidMount: function(){ + $('.error-msg').hide(); + $('.warning-msg').hide(); + $('.success-msg').hide(); + }, + componentDidUpdate: function(){ + + }, + syncData: function(){ + this.collection = new BaseCollection(); + if(this.props.executorStats.length){ + var data = {}; + this.props.executorStats.map(function(obj){ + var hostPort = obj.host + ":" + obj.port; + if(!data[hostPort]){ + data[hostPort] = {}; + } + if(!data[hostPort].idArr){ + data[hostPort].idArr = []; + } + data[hostPort].idArr.push(obj.id); + }); + var keys = this.hostPortArr = _.keys(data); + keys.map(function(key){ + this.collection.add(new Backbone.Model({ + hostPort: key, + executorId: data[key].idArr + })); + }.bind(this)); + } + }, + handleJStackOp: function(){ + this.performOp('JStack'); + }, + handleRestartWorker: function(){ + this.performOp('RestartWorker'); + }, + handleHeapOp: function(){ + this.performOp('Heap'); + }, + performOp: function(opType){ + if(!this.selectedWorker.length){ + $('.warning-msg').show(); + $('.success-msg').hide(); + $('.error-msg').hide(); + } else { + $('.warning-msg').hide(); + $('.success-msg').hide(); + $('.error-msg').hide(); + var promiseArr = []; + this.selectedWorker.map(function(worker){ + var obj = { + id: this.props.topologyId, + hostPort: worker + }; + if(opType === 'JStack'){ + promiseArr.push(this.model.profileJStack(obj)); + } else if(opType === 'RestartWorker'){ + promiseArr.push(this.model.profileRestartWorker(obj)); + } else if(opType === 'Heap'){ + promiseArr.push(this.model.profileHeap(obj)); + } + }.bind(this)); + Promise.all(promiseArr) + .then(function(resultsArr){ + $('.success-msg').show(); + }) + .catch(function(){ + $('.error-msg').show(); + }); + } + }, + getColumns: function(){ + var self = this; + return [ + { + name: 'hostPort', + title: React.createClass({ + handleChange: function(e){ + if($(e.currentTarget).prop('checked')){ + self.selectedWorker = self.hostPortArr; + $('[name="single"]').prop("checked", true) + } else { + self.selectedWorker = []; + $('[name="single"]').prop("checked", false) + } + }, + render: function(){ + return ( + <input type="checkbox" name="selectAll" onChange={this.handleChange}/> + ); + } + }), + component: React.createClass({ + propTypes: { + model: React.PropTypes.object.isRequired + }, + handleChange: function(e){ + var hostPort = this.props.model.get('hostPort') + if($(e.currentTarget).prop('checked')){ + self.selectedWorker.push(hostPort); + } else { + var index = _.indexOf(self.selectedWorker, hostPort); + if(index > -1){ + self.selectedWorker.splice(index, 1); + } + } + }, + render: function(){ + return ( + <input type="checkbox" name="single" onChange={this.handleChange}/> + ); + } + }) + }, + {name: 'hostPort', title:'Host:Port'}, + {name: 'executorId', title:'Executor Id', component: React.createClass({ + propTypes: { + model: React.PropTypes.object.isRequired + }, + render: function(){ + var executors = this.props.model.get('executorId').join(', '); + return ( + <span>{executors}</span> + ); + } + })} + ]; + }, + closeModal: function(){ + $('#'+this.props.modalId).modal("hide"); + }, + render: function() { + return ( + <div className="modal fade" id={this.props.modalId} role="dialog"> + <div className="modal-dialog"> + <div className="modal-content"> + <div className="modal-header"> + <button type="button" className="close" data-dismiss="modal">×</button> + <h4 className="modal-title">Profiling & Debugging</h4> + </div> + <div className="modal-body"> + <div className="alert alert-warning alert-dismissible warning-msg" role="alert"> + <strong>Warning!</strong> Please select atleast one worker to perform operation. + </div> + <div className="alert alert-success alert-dismissible success-msg" role="alert"> + <strong>Success!</strong> Action performed successfully. + </div> + <div className="alert alert-danger alert-dismissible error-msg" role="alert"> + <strong>Error!</strong> Error occured while performing the action. + </div> + <div className="clearfix"> + <div className="btn-group btn-group-sm pull-right"> + <button type="button" className="btn btn-primary" onClick={this.handleJStackOp}>JStack</button> + <button type="button" className="btn btn-primary" onClick={this.handleRestartWorker}>Restart Worker</button> + <button type="button" className="btn btn-primary" onClick={this.handleHeapOp}>Heap</button> + </div> + </div> + <hr /> + <Table className="table table-bordered" collection={this.collection} columns={this.getColumns()} emptyText="No workers found !" /> + </div> + <div className="modal-footer"> + <button type="button" className="btn btn-default" onClick={this.closeModal}>Close</button> + </div> + </div> + </div> + </div> + ); + }, + }); +}); \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ambari/blob/cf5c068c/contrib/views/storm/src/main/resources/scripts/views/RebalanceView.jsx ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/scripts/views/RebalanceView.jsx b/contrib/views/storm/src/main/resources/scripts/views/RebalanceView.jsx new file mode 100644 index 0000000..33f5963 --- /dev/null +++ b/contrib/views/storm/src/main/resources/scripts/views/RebalanceView.jsx @@ -0,0 +1,223 @@ +/** + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at +* + http://www.apache.org/licenses/LICENSE-2.0 +* + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +define(['react', + 'react-dom', + 'utils/Utils', + 'models/VCluster', + 'utils/Globals', + 'bootstrap', + 'bootstrap-slider'], function(React, ReactDOM, Utils, VCluster, Globals) { + 'use strict'; + return React.createClass({ + displayName: 'Rebalance', + propTypes: { + modalId: React.PropTypes.string.isRequired, + topologyId: React.PropTypes.string.isRequired, + topologyExecutors: React.PropTypes.string.isRequired, + spouts: React.PropTypes.array.isRequired, + bolts: React.PropTypes.array.isRequired + }, + getInitialState: function(){ + var spoutArr = []; + var boltArr = []; + this.getClusterDetails(); + return { + spout: spoutArr, + bolt: boltArr, + workers: parseInt(this.props.topologyExecutors,10), + waitTime: 30, + freeSlots: 0 + }; + }, + componentWillMount: function(){ + this.syncData(); + }, + componentDidMount: function(){ + $('.error-msg').hide(); + }, + componentDidUpdate: function(){ + $('#ex1').slider({ + value: this.state.workers, + min: 0, + step: 1, + max: this.state.workers + this.state.freeSlots, + tooltip_position: 'bottom', + formatter: function(value) { + return 'Current value: ' + value; + } + }); + }, + syncData: function(){ + var spoutArr, boltArr; + if(this.props.spouts){ + spoutArr = this.props.spouts.map(function(spout){ + var obj = { + key: spout.spoutId, + value: spout.executors + }; + return obj; + }); + this.setState({'spout': spoutArr}); + } + if(this.props.bolts){ + boltArr = this.props.bolts.map(function(bolt){ + var obj = { + key: bolt.boltId, + value: bolt.executors + }; + return obj; + }); + this.setState({'bolt': boltArr}); + } + }, + getClusterDetails: function(){ + var model = new VCluster(); + model.fetch({ + success: function(model){ + this.setState({"freeSlots": model.get('slotsFree')}); + }.bind(this) + }); + }, + rebalanceTopologyAction: function(e){ + var arr = $('form').serializeArray(); + var errorFlag = false; + var finalData = { + "rebalanceOptions": { + "executors": {} + }, + }; + var waitTime; + var result = arr.map(function(obj){ + if(!errorFlag){ + if(obj.value === ''){ + errorFlag = true; + } else { + if(obj.name === 'workers'){ + finalData.rebalanceOptions.numWorkers = obj.value; + } else if(obj.name === 'waitTime'){ + waitTime = obj.value; + } else { + finalData.rebalanceOptions.executors[obj.name] = obj.value; + } + } + } + }); + if(errorFlag){ + $('.error-msg').show(); + } else { + $('.error-msg').hide(); + $.ajax({ + url: Globals.baseURL + '/api/v1/topology/' + this.props.topologyId + '/rebalance/' + waitTime, + data: (_.keys(finalData.rebalanceOptions).length) ? JSON.stringify(finalData) : null, + cache: false, + contentType: 'application/json', + type: 'POST', + success: function(model, response, options){ + if(!_.isUndefined(model.error)){ + if(model.errorMessage.search("msg:") != -1){ + var startIndex = model.errorMessage.search("msg:") + 4; + var endIndex = model.errorMessage.split("\n")[0].search("\\)"); + Utils.notifyError(model.error+":<br/>"+model.errorMessage.substring(startIndex, endIndex)); + } else { + Utils.notifyError(model.error); + } + } else { + Utils.notifySuccess("Topology rebalanced successfully."); + } + this.closeModal(); + }.bind(this), + error: function(model, response, options){ + Utils.notifyError("Error occured in rebalancing topology."); + } + }); + } + }, + renderSpoutInput: function(){ + if(this.state.spout){ + return this.state.spout.map(function(spout, i){ + return ( + <div key={i} className="form-group"> + <label className="control-label col-sm-3">{spout.key}*:</label> + <div className="col-sm-9"> + <input type="number" min="0" name={spout.key} className="form-control" defaultValue={spout.value} required="required"/> + </div> + </div> + ); + }); + } + }, + renderBoltInput: function(){ + if(this.state.bolt){ + return this.state.bolt.map(function(bolt, i){ + return ( + <div key={i} className="form-group"> + <label className="control-label col-sm-3">{bolt.key}*:</label> + <div className="col-sm-9"> + <input type="number" min="0" name={bolt.key} className="form-control" defaultValue={bolt.value} /> + </div> + </div> + ); + }); + } + }, + closeModal: function(){ + $('#'+this.props.modalId).modal("hide"); + }, + render: function() { + var totalExecutor = this.state.workers + this.state.freeSlots; + return ( + <div className="modal fade" id={this.props.modalId} role="dialog" data-backdrop="static"> + <div className="modal-dialog"> + <div className="modal-content"> + <div className="modal-header"> + <button type="button" className="close" data-dismiss="modal">×</button> + <h4 className="modal-title">Rebalance Topology</h4> + </div> + <div className="modal-body"> + <div className="alert alert-danger alert-dismissible error-msg" role="alert"> + <strong>Warning!</strong> Please fill out all the required (*) fields. + </div> + <form className="form-horizontal" role="form"> + <div className="form-group"> + <label className="control-label col-sm-3">Workers*:</label> + <div className="col-sm-9"> + <b>0</b><input id="ex1" name="workers" data-slider-id='ex1Slider' type="text" /><b>{totalExecutor}</b> + </div> + </div> + {this.renderSpoutInput()} + {this.renderBoltInput()} + <div className="form-group"> + <label className="control-label col-sm-3">Wait Time*:</label> + <div className="col-sm-9"> + <input type="number" min="0" name="waitTime" className="form-control" defaultValue={this.state.waitTime}/> + </div> + </div> + </form> + </div> + <div className="modal-footer"> + <button type="button" className="btn btn-default" onClick={this.closeModal}>Close</button> + <button type="button" className="btn btn-success" onClick={this.rebalanceTopologyAction}>Save</button> + </div> + </div> + </div> + </div> + ); + }, + }); +}); \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ambari/blob/cf5c068c/contrib/views/storm/src/main/resources/scripts/views/SupervisorSummaryView.jsx ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/scripts/views/SupervisorSummaryView.jsx b/contrib/views/storm/src/main/resources/scripts/views/SupervisorSummaryView.jsx new file mode 100644 index 0000000..5827147 --- /dev/null +++ b/contrib/views/storm/src/main/resources/scripts/views/SupervisorSummaryView.jsx @@ -0,0 +1,65 @@ +/** + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at +* + http://www.apache.org/licenses/LICENSE-2.0 +* + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +define([ + 'jsx!components/Table', + 'react', + 'react-dom', + 'jsx!containers/SupervisorSummary', + 'jsx!components/Breadcrumbs' + ],function(Table, React, ReactDOM, SupervisorSummary, Breadcrumbs){ + 'use strict'; + + return React.createClass({ + displayName: 'SupervisorSummaryView', + getInitialState: function(){ + return null; + }, + componentWillMount: function(){ + $('.loader').show(); + }, + componentDidMount: function(){ + $('.loader').hide(); + }, + componentWillUpdate: function(){ + $('.loader').show(); + }, + componentDidUpdate: function(){ + $('.loader').hide(); + }, + render: function() { + return ( + <div> + <Breadcrumbs links={this.getLinks()} /> + <div className="row"> + <div className="col-sm-12"> + <SupervisorSummary/> + </div> + </div> + </div> + ); + }, + getLinks: function() { + var links = [ + {link: '#!/dashboard', title: 'Dashboard'}, + {link: 'javascript:void(0);', title: 'Supervisor Summary'} + ]; + return links; + } + }); +}); \ No newline at end of file
