http://git-wip-us.apache.org/repos/asf/ambari/blob/e3931cc2/contrib/views/storm/src/main/resources/ui/app/scripts/components/CommonWindowPanel.jsx ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/ui/app/scripts/components/CommonWindowPanel.jsx b/contrib/views/storm/src/main/resources/ui/app/scripts/components/CommonWindowPanel.jsx new file mode 100644 index 0000000..0f8130f --- /dev/null +++ b/contrib/views/storm/src/main/resources/ui/app/scripts/components/CommonWindowPanel.jsx @@ -0,0 +1,99 @@ +/** + 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. +**/ + +import React, {Component} from 'react'; +import Select from 'react-select'; +import CommonSwitchComponent from './CommonSwitchComponent'; +import {OverlayTrigger, Tooltip} from 'react-bootstrap'; + +export default class CommonWindowPanel extends Component{ + constructor(props){ + super(props); + } + + windowChange = (obj) => { + this.props.handleWindowChange(obj); + } + + commonToggleChange = (params) => { + this.props.toggleSystem(params); + } + + commonTopologyActionHandler = (action) => { + this.props.handleTopologyAction(action); + } + + render(){ + const {selectedWindowKey,windowOptions,systemFlag,debugFlag,handleLogLevel,topologyStatus,KYC,handleProfiling} = this.props; + return( + <div className="form-group no-margin"> + <label className="col-sm-1 control-label">Window</label> + <div className="col-sm-2"> + <Select value={selectedWindowKey} options={windowOptions} onChange={this.windowChange.bind(this)} valueKey="label" labelKey="label" clearable={false}/> + </div> + <label className="col-sm-2 control-label">System Summary</label> + <div className="col-sm-2"> + <CommonSwitchComponent checked={systemFlag} switchCallBack={this.commonToggleChange.bind(this,'systemFlag')}/> + </div> + <label className="col-sm-1 control-label">Debug</label> + <div className="col-sm-1"> + <CommonSwitchComponent checked={debugFlag} switchCallBack={this.commonToggleChange.bind(this,'debugFlag')}/> + </div> + <div className="col-sm-3 text-right"> + <div className="btn-group" role="group"> + { + KYC === 'detailView' + ? [ <OverlayTrigger key={1} placement="top" overlay={<Tooltip id="tooltip1">Activate</Tooltip>}> + <button type="button" className="btn btn-primary" onClick={this.commonTopologyActionHandler.bind(this,'activate')} disabled={topologyStatus === 'ACTIVE' ? "disabled" : null}> + <i className="fa fa-play"></i> + </button> + </OverlayTrigger>, + <OverlayTrigger key={2} placement="top" overlay={<Tooltip id="tooltip1">Deactivate</Tooltip>}> + <button type="button" className="btn btn-primary" onClick={this.commonTopologyActionHandler.bind(this,'deactivate')} disabled={topologyStatus === 'INACTIVE' ? "disabled" : null}> + <i className="fa fa-stop"></i> + </button> + </OverlayTrigger>, + <OverlayTrigger key={3} placement="top" overlay={<Tooltip id="tooltip1">Rebalance</Tooltip>}> + <button type="button" className="btn btn-primary" onClick={this.commonTopologyActionHandler.bind(this,'rebalance')} disabled={topologyStatus === 'REBALANCING' ? "disabled" : null}> + <i className="fa fa-balance-scale"></i> + </button> + </OverlayTrigger>, + <OverlayTrigger key={4} placement="top" overlay={<Tooltip id="tooltip1">Kill</Tooltip>}> + <button type="button" className="btn btn-primary" onClick={this.commonTopologyActionHandler.bind(this,'kill')} disabled={topologyStatus === 'KILLED' ? "disabled" : null}> + <i className="fa fa-ban"></i> + </button> + </OverlayTrigger>, + <OverlayTrigger key={5} placement="top" overlay={<Tooltip id="tooltip1">Change Log Level</Tooltip>}> + <button type="button" className="btn btn-primary" onClick={handleLogLevel}> + <i className="fa fa-file-o"></i> + </button> + </OverlayTrigger> + ] + : <OverlayTrigger placement="top" overlay={<Tooltip id="tooltip1">Profiling & Debugging</Tooltip>}> + <button type="button" className="btn btn-primary" onClick={handleProfiling}> + <i className="fa fa-cogs"></i> + </button> + </OverlayTrigger> + + } + </div> + </div> + </div> + ); + } +}
http://git-wip-us.apache.org/repos/asf/ambari/blob/e3931cc2/contrib/views/storm/src/main/resources/ui/app/scripts/components/CustomToastContainer.jsx ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/ui/app/scripts/components/CustomToastContainer.jsx b/contrib/views/storm/src/main/resources/ui/app/scripts/components/CustomToastContainer.jsx new file mode 100644 index 0000000..04456cb --- /dev/null +++ b/contrib/views/storm/src/main/resources/ui/app/scripts/components/CustomToastContainer.jsx @@ -0,0 +1,41 @@ +/** + 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. +**/ + +import React, {Component}from 'react'; +import { render } from 'react-dom'; +import ReactToastr, {ToastMessage, ToastContainer} from "react-toastr"; + +class CustomToastContainer extends ToastContainer{ + success(msg, title, opts){ + super.success(msg.props.children, msg, opts); + } + + error(msg, title, opts){ + super.error(msg.props.children, msg, opts); + } + + info(msg, title, opts){ + super.info(msg.props.children, msg, opts); + } + + warning(msg, title, opts){ + super.warning(msg.props.children, msg, opts); + } +} + +export default CustomToastContainer; http://git-wip-us.apache.org/repos/asf/ambari/blob/e3931cc2/contrib/views/storm/src/main/resources/ui/app/scripts/components/Editable.jsx ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/ui/app/scripts/components/Editable.jsx b/contrib/views/storm/src/main/resources/ui/app/scripts/components/Editable.jsx new file mode 100644 index 0000000..03005ae --- /dev/null +++ b/contrib/views/storm/src/main/resources/ui/app/scripts/components/Editable.jsx @@ -0,0 +1,127 @@ +/** + 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. +**/ + +import React, {Component} from 'react'; +import ReactDOM, {findDOMNode} from 'react-dom'; +import {Overlay, Popover, Button} from 'react-bootstrap'; + +export default class Editable extends Component { + state = { + edit: false, + errorMsg: '' + }; + + handleClick = () => { + let state = this.state; + state.edit = true; + this.setState(state); + } + + handleResolve = () => { + const {resolve} = this.props; + if (resolve) { + resolve(this); + } + } + + handleReject = () => { + const {reject} = this.props; + if (reject) { + reject(this); + } else { + this.hideEditor(); + } + } + + hideEditor = () => { + let state = this.state; + state.edit = false; + this.setState(state); + } + + getValueString() { + const {children} = this.props; + + if (children.type == 'input' || children.type == 'textarea') { + return children.props.value || children.props.defaultValue; + } else if (children.type == 'select') {} else { + var fn = children.getStringValue; + if (fn) { + return fn(); + } else { + console.error('Custom component must have getValueString() function.'); + } + } + } + + anchorStyle = { + textDecoration: 'none', + borderBottom: 'dashed 1px #0088cc', + cursor: 'pointer', + color: '#323133' + }; + + render() { + const {children, showButtons, inline, placement, title} = this.props; + const {edit, errorMsg} = this.state; + + const buttons = showButtons + ? ([<Button className="btn-primary btn-sm" onClick={this.handleResolve} key="resolve" style={{margin : "0 0 3px 5px"}}> + <i className="fa fa-check"></i></Button>, + <Button className="btn-default btn-sm" onClick={this.handleReject} key="reject" style={{margin : "0 3px"}}> + <i className="fa fa-times"></i> + </Button> + ]) + : null; + + const error = errorMsg + ? ( + <div className="editable-error">{errorMsg}</div> + ) + : null; + + const popover = ( + <Popover id="popover-positioned-left" title={title || ''}> + {children} + {buttons} + {error} + </Popover> + ); + + return ( + <div className="editable-container" style={{display: 'inline'}} id={this.props.id || ''}> + {edit && inline + ? null + : <a ref="target" onClick={this.handleClick} style={this.anchorStyle}>{this.getValueString()}</a> +} + {edit && inline + ? [children, buttons, error] + : <Overlay show={edit} target={() => ReactDOM.findDOMNode(this.refs.target)} {...this.props}> + {popover} + </Overlay> +} + </div> + ); + } +} + +Editable.defaultProps = { + showButtons: true, + inline: false, + placement: "top" +}; http://git-wip-us.apache.org/repos/asf/ambari/blob/e3931cc2/contrib/views/storm/src/main/resources/ui/app/scripts/components/FSModel.jsx ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/ui/app/scripts/components/FSModel.jsx b/contrib/views/storm/src/main/resources/ui/app/scripts/components/FSModel.jsx new file mode 100644 index 0000000..14cb17d --- /dev/null +++ b/contrib/views/storm/src/main/resources/ui/app/scripts/components/FSModel.jsx @@ -0,0 +1,149 @@ +/** + 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. +**/ + +import React, {Component} from 'react'; +import {Modal, Button} from 'react-bootstrap'; + +const defaultState = { + show: false, + title: '', + btnOkText: 'Ok', + btnCancelText: 'Cancel' +}; + +export default class FSModal extends Component { + state = defaultState; + show() { + var state = state || {}; + state.show = true; + this.setState(state); + } + sure() { + let resolve = this.props["data-resolve"]; + if (resolve) { + resolve(); + } + } + cancel() { + let reject = this.props["data-reject"]; + if (reject) { + reject(); + } else { + this.hide(); + } + } + hide() { + this.setState({show: false}); + } + header() { + return ( + <Modal.Header closeButton> + <Modal.Title> + {this.props["data-title"]} + </Modal.Title> + </Modal.Header> + ); + } + body() { + return ( + <Modal.Body> + {this.props.children} + </Modal.Body> + ); + } + footer() { + return ( + <Modal.Footer> + { + this.props.hideCloseBtn + ? null + : <Button bsStyle='default' onClick={this.cancel.bind(this)} data-stest="cancelbtn"> + {this.props.closeLabel || this.state.btnCancelText} + </Button> + } + { + this.props.hideOkBtn + ? null + : <Button bsStyle='success' onClick={this.sure.bind(this)} data-stest="okbtn" disabled={this.props.btnOkDisabled}> + {this.props.okLabel || this.state.btnOkText} + </Button> + } + </Modal.Footer> + ); + } + render() { + return ( + <Modal aria-labelledby='contained-modal-title' backdrop="static" keyboard={true} onHide={this.cancel.bind(this)} show={this.state.show} {...this.props}> + {this.props.hideHeader + ? '' + : this.header()} + {this.body()} + {this.props.hideFooter + ? '' + : this.footer()} + </Modal> + ); + } +} + +var _resolve; +var _reject; + +export class Confirm extends FSModal { + show(state) { + var state = state || {}; + state.show = true; + this.setState(state); + let promise = new Promise(function(resolve, reject) { + _resolve = resolve; + _reject = reject; + }); + return promise; + } + sure() { + _resolve(this); + } + cancel() { + _reject(this); + this.setState(defaultState); + } + header() { + return ( + <Modal.Header closeButton> + <Modal.Title> + {this.state.title} + </Modal.Title> + </Modal.Header> + ); + } + body() { + return ''; + } + footer() { + return ( + <Modal.Footer> + <Button bsStyle='danger' onClick={this.cancel.bind(this)} data-stest="confirmBoxCancelBtn"> + {this.state.btnCancelText || 'No'} + </Button> + <Button bsStyle='success' onClick={this.sure.bind(this)} data-stest="confirmBoxOkBtn"> + {this.state.btnOkText || 'Yes'} + </Button> + </Modal.Footer> + ); + } +} http://git-wip-us.apache.org/repos/asf/ambari/blob/e3931cc2/contrib/views/storm/src/main/resources/ui/app/scripts/components/FSReactToastr.jsx ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/ui/app/scripts/components/FSReactToastr.jsx b/contrib/views/storm/src/main/resources/ui/app/scripts/components/FSReactToastr.jsx new file mode 100644 index 0000000..432ca5b --- /dev/null +++ b/contrib/views/storm/src/main/resources/ui/app/scripts/components/FSReactToastr.jsx @@ -0,0 +1,37 @@ +/** + 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. +**/ + +import React, {Component}from 'react'; +import { render } from 'react-dom'; +import ReactToastr, {ToastMessage, ToastContainer} from "react-toastr"; +import CustomToastContainer from './CustomToastContainer'; +var {animation} = ToastMessage; + +var ToastMessageFactory = React.createFactory(animation); + +var container = document.createElement('div'); +var body = document.getElementsByTagName('body').item(0); +body.appendChild(container); + +const FSReactToastr = render( + <CustomToastContainer + toastMessageFactory={ToastMessageFactory} + className="toast-top-right" />, container +); + +export default FSReactToastr; http://git-wip-us.apache.org/repos/asf/ambari/blob/e3931cc2/contrib/views/storm/src/main/resources/ui/app/scripts/components/Footer.jsx ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/ui/app/scripts/components/Footer.jsx b/contrib/views/storm/src/main/resources/ui/app/scripts/components/Footer.jsx new file mode 100644 index 0000000..59064d2 --- /dev/null +++ b/contrib/views/storm/src/main/resources/ui/app/scripts/components/Footer.jsx @@ -0,0 +1,28 @@ +/** + 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. +**/ + +import React, {Component} from 'react'; +import {stormVersion} from '../utils/Constants'; + +const Footer = () =>{ + return( + <p className="text-center">Apache Storm - {stormVersion}</p> + ); +}; + +export default Footer; http://git-wip-us.apache.org/repos/asf/ambari/blob/e3931cc2/contrib/views/storm/src/main/resources/ui/app/scripts/components/LogLevelComponent.jsx ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/ui/app/scripts/components/LogLevelComponent.jsx b/contrib/views/storm/src/main/resources/ui/app/scripts/components/LogLevelComponent.jsx new file mode 100644 index 0000000..fb8e10b --- /dev/null +++ b/contrib/views/storm/src/main/resources/ui/app/scripts/components/LogLevelComponent.jsx @@ -0,0 +1,236 @@ +/** + 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. +**/ + +import React, {Component} from 'react'; +import TopologyREST from '../rest/TopologyREST'; +import { + Table, + Thead, + Th, + Tr, + Td, + unsafe +} from 'reactable'; +import CommonPagination from './CommonPagination'; +import {toastOpt,pageSize} from '../utils/Constants'; +import Select from 'react-select'; +import FSReactToastr from './FSReactToastr'; +import CommonNotification from './CommonNotification'; +import Editable from './Editable'; + +export default class LogLevelComponent extends Component{ + constructor(props){ + super(props); + this.state = { + logLevelObj : {}, + traceOption : this.populateTraceOptions(), + selectedKeyName : 'com.your.organization.LoggerName', + selectedTrace : 'ALL', + selectedTimeOut : 30 + }; + this.fetchData(); + this.keyName = ''; + this.timeChange=''; + } + + fetchData = () => { + const {topologyId} = this.props; + TopologyREST.getLogConfig(topologyId).then((result) => { + if(result.errorMessage !== undefined){ + FSReactToastr.error( + <CommonNotification flag="error" content={result.errorMessage}/>, '', toastOpt); + } else { + let stateObj={}; + stateObj.selectedKeyName = 'com.your.organization.LoggerName'; + stateObj.selectedTrace = 'ALL'; + stateObj.selectedTimeOut = 30; + stateObj.logLevelObj = result.namedLoggerLevels; + this.setState(stateObj); + } + }); + } + + populateTraceOptions = () => { + let temp=[]; + const arr = ['ALL','TRACE','DEBUG','INFO','WARN','ERROR','FATAL','OFF']; + _.map(arr, (a) => { + temp.push({label : a, value : a}); + }); + return temp; + } + + handleNameChange = (e) => { + this.keyName = e.target.value.trim(); + } + + handleTimeChange = (e) => { + this.timeChange = e.target.value.trim(); + } + + traceLavelChange = (type,key,addRow,obj) => { + let tempObj = _.cloneDeep(this.state.logLevelObj); + let tempKeyName = 'ALL'; + if(!!addRow){ + tempKeyName = obj.value; + } else{ + tempObj[type][key] = obj.value; + } + this.setState({logLevelObj : tempObj,selectedTrace : tempKeyName}); + } + + modifyCommonObjValue = (refType,type,key,action,addRow) => { + let logObj = _.cloneDeep(this.state.logLevelObj); + let tempTimeOut = _.cloneDeep(this.state.selectedTimeOut); + const timeValue = (this.timeChange === '' || this.timeChange === undefined) ? parseInt(this.refs[refType].defaultValue || 0,10) : parseInt(this.timeChange,10); + if(action === 'save' && addRow === null){ + logObj[type][key] = timeValue; + } else if(action === 'save' && !!addRow){ + tempTimeOut = timeValue; + this.timeChange = ''; + }else if(action === 'reject'){ + this.timeChange = parseInt(this.refs[refType].defaultValue || 0,10); + } + this.refs[refType].hideEditor(); + this.setState({logLevelObj : logObj ,selectedTimeOut :tempTimeOut }); + } + + getDateFormat = (str) => { + const d = new Date(str); + return d.toLocaleDateString() + ' ' + d.toLocaleTimeString(); + } + + saveAndClearLogConfig = (type,action) => { + let tempObj = _.cloneDeep(this.state.logLevelObj); + let obj={},namedLoggerLevels={}; + obj.namedLoggerLevels={}; + if(action === 'clear'){ + obj.namedLoggerLevels[type] = {}; + obj.namedLoggerLevels[type].timeout = 0; + obj.namedLoggerLevels[type].target_level = null; + } else { + obj.namedLoggerLevels[type] = tempObj[type]; + } + obj.namedLoggerLevels[type].reset_level = 'INFO'; + delete obj.namedLoggerLevels[type].timeout_epoch; + + this.callLogConfigAPI(obj,null,action); + } + + callLogConfigAPI = (obj,addRow,action) => { + const {topologyId,logConfig} = this.props; + const {logLevelObj} = this.state; + TopologyREST.postLogConfig(topologyId, {body : JSON.stringify(obj)}).then((result) => { + if(result.errorMessage !== undefined){ + this.setState({logLevelObj : logConfig}); + FSReactToastr.error( + <CommonNotification flag="error" content={result.errorMessage}/>, '', toastOpt); + } else { + let msg = !!addRow ? "Log configuration added successfully" : (action === 'save' ? "Log configuration applied successfully." : "Log configuration cleared successfully."); + FSReactToastr.success(<strong>{msg}</strong>); + this.fetchData(); + } + }); + } + + addLoggerName = (refType,action) => { + let tempName = _.cloneDeep(this.state.selectedKeyName); + if(action === 'save'){ + tempName = !!this.keyName ? this.keyName : tempName; + }else if(action === 'reject'){ + this.keyName = this.refs[refType].defaultValue || tempName; + } + this.refs[refType].hideEditor(); + this.setState({selectedKeyName : tempName}); + } + + addLogRow = () => { + const {selectedKeyName,selectedTrace,selectedTimeOut} = this.state; + let obj={}; + obj.namedLoggerLevels = {}; + obj.namedLoggerLevels[selectedKeyName] = {}; + obj.namedLoggerLevels[selectedKeyName].target_level = selectedTrace; + obj.namedLoggerLevels[selectedKeyName].reset_level = 'INFO'; + obj.namedLoggerLevels[selectedKeyName].timeout = selectedTimeOut; + this.callLogConfigAPI(obj,'addRow'); + } + + render(){ + const {logLevelObj,traceOption,selectedKeyName,selectedTrace,selectedTimeOut} = this.state; + return( + <div className={`boxAnimated`}> + <hr/> + <h4 className="col-sm-offset-5">Change Log Level</h4> + <p>Modify the logger levels for topology. Note that applying a setting restarts the timer in the workers. To configure the root logger, use the name ROOT.</p> + <Table className="table no-margin"> + <Thead> + <Th column="logger" title="Logger">Logger</Th> + <Th column="target_level" title="Level">Level</Th> + <Th column="timeout" title="Timeout">Timeout</Th> + <Th column="timeout_epoch" title="Expires At">Expires At</Th> + <Th column="action" title="Action">Action</Th> + </Thead> + { + _.map(_.keys(logLevelObj), (logKey, i) => { + return <Tr key={i}> + <Td column="logger"> + <a href="javascript:void(0)">{logKey}</a> + </Td> + <Td column="target_level">{} + <Select value={logLevelObj[logKey].target_level} options={traceOption} onChange={this.traceLavelChange.bind(this,logKey,'target_level',null)} required={true} clearable={false} /> + </Td> + <Td column="timeout"> + <Editable ref={`logKey${i}`} inline={true} resolve={this.modifyCommonObjValue.bind(this,`logKey${i}`,logKey,'timeout','save',null)} reject={this.modifyCommonObjValue.bind(this,`logKey${i}`,logKey,'timeout','reject',null)}> + <input className="form-control input-sm editInput" ref={this.focusInput} defaultValue={logLevelObj[logKey].timeout} onChange={this.handleTimeChange.bind(this)}/> + </Editable> + </Td> + <Td column="timeout_epoch">{this.getDateFormat(logLevelObj[logKey].timeout_epoch)}</Td> + <Td column="action"> + <span> + <a href="javascript:void(0)" className="btn btn-success btn-xs" onClick={this.saveAndClearLogConfig.bind(this,logKey,'save')}><i className="fa fa-check"></i></a> + <a href="javascript:void(0)" className="btn btn-danger btn-xs" onClick={this.saveAndClearLogConfig.bind(this,logKey,'clear')}><i className="fa fa-times"></i></a> + </span> + </Td> + </Tr>; + }) + } + <Tr key={Math.random()}> + <Td column="logger"> + <Editable ref="addRowRef" inline={true} resolve={this.addLoggerName.bind(this,'addRowRef','save')} reject={this.addLoggerName.bind(this,"addRowRef",'reject')}> + <input className="form-control input-sm editInput" ref={this.focusInput} defaultValue={selectedKeyName} onChange={this.handleNameChange.bind(this)}/> + </Editable> + </Td> + <Td column="target_level"> + <Select value={selectedTrace} options={traceOption} onChange={this.traceLavelChange.bind(this,null,'target_level','ADD')} required={true} clearable={false} /> + </Td> + <Td column="timeout"> + <Editable ref={"timeoutRef"} inline={true} resolve={this.modifyCommonObjValue.bind(this,"timeoutRef",null,'timeout','save','ADD')} reject={this.modifyCommonObjValue.bind(this,"timeoutRef",null,'timeout','reject','ADD')}> + <input className="form-control input-sm editInput" ref={this.focusInput} defaultValue={selectedTimeOut} onChange={this.handleTimeChange.bind(this)}/> + </Editable> + </Td> + <Td column="timeout_epoch"> </Td> + <Td column="action"> + <span> + <a href="javascript:void(0)" className="btn btn-primary btn-xs" onClick={this.addLogRow.bind(this,'save')}><i className="fa fa-check"></i></a> + </span> + </Td> + </Tr> + </Table> + </div> + ); + } +} http://git-wip-us.apache.org/repos/asf/ambari/blob/e3931cc2/contrib/views/storm/src/main/resources/ui/app/scripts/components/ProfilingView.jsx ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/ui/app/scripts/components/ProfilingView.jsx b/contrib/views/storm/src/main/resources/ui/app/scripts/components/ProfilingView.jsx new file mode 100644 index 0000000..eedf0dd --- /dev/null +++ b/contrib/views/storm/src/main/resources/ui/app/scripts/components/ProfilingView.jsx @@ -0,0 +1,168 @@ +/** + 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. +**/ + +import React, {Component} from 'react'; +import TopologyREST from '../rest/TopologyREST'; +import { + Table, + Thead, + Th, + Tr, + Td, + unsafe +} from 'reactable'; +import {toastOpt,pageSize} from '../utils/Constants'; +import Utils from '../utils/Utils'; +import FSReactToastr from '../components/FSReactToastr'; +import CommonNotification from '../components/CommonNotification'; +import _ from 'lodash'; + +export default class ProfilingView extends Component{ + constructor(props){ + super(props); + this.state = { + currentPage : 1, + executorArr : this.props.executorStats ? this.fetchData() : [], + selectedWorker : [], + selectAll : false, + warnMsg : false, + successMsg : false, + errorMsg : false + }; + } + + fetchData = () => { + const {executorStats} = this.props; + let data = {},executorArr=[]; + _.map(executorStats, (o) => { + const hostPort = o.host + ":" + o.port; + if(!data[hostPort]){ + data[hostPort] = {}; + } + if(!data[hostPort].idArr){ + data[hostPort].idArr = []; + } + data[hostPort].idArr.push(o.id); + }); + let keys = this.hostPortArr = _.keys(data); + _.map(keys, (k) => { + executorArr.push({ + hostPort: k, + executorId: data[k].idArr, + checked : false + }); + }); + return executorArr; + } + + commonBtnAction = (actionType) => { + const {selectedWorker} = this.state; + selectedWorker.length ? this.apiCallback(actionType) : this.setState({warnMsg : true,successMsg : false,errorMsg: false}); + } + + apiCallback = (actionType) => { + const {topologyId} = this.props; + const {selectedWorker} = this.state; + let promiseArr=[]; + _.map(selectedWorker, (w) => { + promiseArr.push(TopologyREST.getProfiling(topologyId,actionType,w.hostPort)); + }); + + Promise.all(promiseArr).then((results) => { + _.map(results, (r) => { + let tempErrorMsg= false,tempSuccessMsg=false; + if(r.errorMessage !== undefined){ + tempErrorMsg = true; + tempSuccessMsg: false; + } else { + tempErrorMsg = false; + tempSuccessMsg: true; + } + this.setState({successMsg : tempSuccessMsg,errorMsg: tempErrorMsg,warnMsg : false}); + }); + }); + } + + handleChange = (hostPort) => { + let tempSelect = _.cloneDeep(this.state.selectAll); + let tempExecutor=_.cloneDeep(this.state.executorArr); + let tempWorker = _.cloneDeep(this.state.selectedWorker); + if(!!hostPort){ + const ind = _.findIndex(tempExecutor, (e) => {return e.hostPort === hostPort; }); + const index = _.findIndex(tempWorker,(t) => {return t.hostPort === hostPort;}); + if(index === -1 && ind !== -1){ + tempWorker.push(tempExecutor[ind]); + } else { + tempWorker.splice(index,1); + } + tempExecutor[ind].checked = !tempExecutor[ind].checked; + } else { + tempSelect = !this.state.selectAll; + _.map(tempExecutor,(t) => { + t.checked = tempSelect; + }); + tempWorker = tempExecutor; + } + this.setState({selectedWorker : tempWorker,selectAll : tempSelect,executorArr :tempExecutor }); + } + + render(){ + const {currentPage,executorArr,selectAll,warnMsg,successMsg,errorMsg} = this.state; + return( + <div> + <div className={`alert alert-warning alert-dismissible warning-msg ${warnMsg ? '' : 'hidden'}`} role="alert"> + <strong>Warning!</strong> Please select atleast one worker to perform operation. + </div> + <div className={`alert alert-success alert-dismissible success-msg ${successMsg ? '' : 'hidden'}`} role="alert"> + <strong>Success!</strong> Action performed successfully. + </div> + <div className={`alert alert-danger alert-dismissible error-msg ${errorMsg ? '' : 'hidden'}`} 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.commonBtnAction.bind(this,'dumpjstack')}>JStack</button> + <button type="button" className="btn btn-primary" onClick={this.commonBtnAction.bind(this,'restartworker')}>Restart Worker</button> + <button type="button" className="btn btn-primary" onClick={this.commonBtnAction.bind(this,'dumpheap')}>Heap</button> + </div> + </div> + <hr /> + <Table className="table table-bordered" columns={currentPage-1} noDataText="No workers found !"> + <Thead> + <Th column="checkbox"> + <input type="checkbox" name="single" onChange={this.handleChange.bind(this,null)}/> + </Th> + <Th column="hostPort" >Host:Port</Th> + <Th column="executorId" >Executor Id</Th> + </Thead> + { + _.map(executorArr , (e,i) => { + return <Tr key={i}> + <Td column="checkbox"> + <input type="checkbox" checked={e.checked} name="single" onChange={this.handleChange.bind(this,e.hostPort)}/> + </Td> + <Td column="hostPort">{e.hostPort}</Td> + <Td column="executorId">{e.executorId.join(',')}</Td> + </Tr>; + }) + } + </Table> + </div> + ); + } +} http://git-wip-us.apache.org/repos/asf/ambari/blob/e3931cc2/contrib/views/storm/src/main/resources/ui/app/scripts/components/RadialChart.jsx ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/ui/app/scripts/components/RadialChart.jsx b/contrib/views/storm/src/main/resources/ui/app/scripts/components/RadialChart.jsx new file mode 100644 index 0000000..4c4e8fc --- /dev/null +++ b/contrib/views/storm/src/main/resources/ui/app/scripts/components/RadialChart.jsx @@ -0,0 +1,134 @@ +/** + 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. +**/ + +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; +import ReactDOM from 'react-dom'; +import d3 from 'd3'; +import d3Tip from 'd3-tip'; + + +export default class RadialChart extends Component { + static propTypes = { + data: PropTypes.array.isRequired, + labels: PropTypes.array.isRequired, + width: PropTypes.number, + height: PropTypes.number, + innerRadius: PropTypes.number.isRequired, + outerRadius: PropTypes.number.isRequired, + color: PropTypes.array + } + constructor(props) { + super(props); + this.const = { + tau: 2 * Math.PI, + width: props.width || "44", + height: props.height || "52", + innerRadius: parseInt(props.innerRadius, 10) || 20, + outerRadius: parseInt(props.outerRadius, 10) || 25, + color: props.color || d3.scale.category20() + }; + this.arc = d3.svg.arc() + .innerRadius(this.const.innerRadius) + .outerRadius(this.const.outerRadius) + .startAngle(0); + } + componentDidUpdate() { + this.animateGraph(); + } + componentDidMount() { + const self = this; + this.tip = d3Tip() + .attr('class', 'd3-tip') + .offset([-10, 0]) + .html(function() { + var text = "<div class='summary'>" + this.props.labels[0] + ": " + this.props.data[0] + "</div>"; + text += "<div class='summary'>Free: " + (parseInt(this.props.data[1], 10) - parseInt(this.props.data[0], 10)) + "</div>"; + text += "<div class='summary'>" + this.props.labels[1] + ": " + this.props.data[1] + "</div>"; + return text; + }.bind(this)); + var svg = this.svg = d3.select(ReactDOM.findDOMNode(this)) + .attr('width', this.const.width + "px") + .attr('height', this.const.height + "px") + .append('g').attr('transform', 'translate(' + (this.const.width / 2) + ', ' + (this.const.height / 2) + ')'); + + this.text = svg.append("text") + .attr("y", "0.3em") + .attr("class", "graphVal") + .attr("text-anchor", "middle") + .attr("font-size", this.const.fontSize) + .on("mouseover", function(d){ + self.tip.show(d, this); + }) + .on("mouseout", function(d){ + self.tip.hide(d, this); + }) + .text("0"); + + var background = svg.append("path") + .datum({ + endAngle: this.const.tau + }) + .style("fill", this.const.color[0]) + .attr("d", this.arc); + + this.foreground = svg.append("path") + .datum({ + endAngle: 0 + }) + .style("fill", function(d, i) { + return this.const.color[1]; + }.bind(this)) + .attr("d", this.arc); + this.svg.call(this.tip); + // $('#container').append($('body > .d3-tip')); + this.animateGraph(); + } + animateGraph() { + var percent = (parseInt(this.props.data[0], 10) / parseInt(this.props.data[1], 10) * 100); + if (percent) { + percent = percent.toFixed(0) + ' %'; + } else { + percent = '0 %'; + } + + d3.select(ReactDOM.findDOMNode(this)).select('.graphVal').text(percent); + + var newValue = this.props.data[0] / this.props.data[1] * 100; + this.foreground.transition() + .duration(750) + .call(this._arcTween.bind(this), this.const.tau * (newValue / 100)); + } + _arcTween(transition, newAngle) { + var arc = this.arc; + transition.attrTween("d", function(d) { + var interpolate = d3.interpolate(d.endAngle, newAngle); + return function(t) { + d.endAngle = interpolate(t); + if (!d.endAngle) { + d.endAngle = 0; + } + return arc(d); + }; + + }); + } + render() { + return ( < svg className = "radial-chart" > < /svg>); + } +} http://git-wip-us.apache.org/repos/asf/ambari/blob/e3931cc2/contrib/views/storm/src/main/resources/ui/app/scripts/components/RebalanceTopology.jsx ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/ui/app/scripts/components/RebalanceTopology.jsx b/contrib/views/storm/src/main/resources/ui/app/scripts/components/RebalanceTopology.jsx new file mode 100644 index 0000000..43c7f78 --- /dev/null +++ b/contrib/views/storm/src/main/resources/ui/app/scripts/components/RebalanceTopology.jsx @@ -0,0 +1,152 @@ +/** + 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. +**/ + +import React, {Component} from 'react'; +import TopologyREST from '../rest/TopologyREST'; +import {toastOpt,pageSize} from '../utils/Constants'; +import Utils from '../utils/Utils'; +import FSReactToastr from './FSReactToastr'; +import CommonNotification from './CommonNotification'; +import _ from 'lodash'; + +export default class RebalanceTopology extends Component{ + constructor(props){ + super(props); + this.state = { + freeSlot : 0, + waitTime : 30, + rebalanceData : {} + }; + this.fetchData(); + } + + fetchData = () => { + const {topologyExecutors,spoutArr,boltArr} = this.props; + TopologyREST.getSummary('cluster').then((result) => { + if(result.errorMessage !== undefined){ + FSReactToastr.error( + <CommonNotification flag="error" content={result.errorMessage}/>, '', toastOpt); + } else { + let stateObj = {}; + stateObj.freeSlot = result.slotsFree; + stateObj.rebalanceData={}; + stateObj.rebalanceData.workers = topologyExecutors + stateObj.freeSlot; + this.totalWorker = stateObj.rebalanceData.workers; + _.map(spoutArr, (s) => { + stateObj.rebalanceData[s.spoutId] = s.executors; + }); + _.map(boltArr, (b) => { + stateObj.rebalanceData[b.boltId] = b.executors; + }); + this.setState(stateObj); + } + }); + } + + rebalanceValueChange = (type,e) => { + let data = _.cloneDeep(this.state.rebalanceData); + data[type] = +e.target.value; + this.setState({rebalanceData : data}); + } + + waitTimeChange = (e) => { + this.setState({waitTime : +e.target.value}); + } + + validateData = () => { + const {rebalanceData,waitTime} = this.state; + let errorFlag = []; + _.map(_.keys(rebalanceData), (key) => { + if(rebalanceData[key] === ''){ + errorFlag.push(false); + } + }); + if(waitTime === ''){ + errorFlag.push(false); + } + return errorFlag.length ? false : true; + } + + handleSave = () => { + const {rebalanceData,waitTime} = this.state; + const {topologyId} = this.props; + let finalData = { + "rebalanceOptions": { + "executors": {} + } + }; + _.map(_.keys(rebalanceData), (key) => { + if(key === "workers"){ + finalData.rebalanceOptions.numWorkers = rebalanceData[key]; + } else { + finalData.rebalanceOptions.executors[key] = rebalanceData[key]; + } + }); + + return TopologyREST.postActionOnTopology(topologyId,'rebalance',waitTime,{body : JSON.stringify(finalData)}); + } + + render(){ + const {freeSlot,waitTime,rebalanceData} = this.state; + const {spoutArr,boltArr}= this.props; + return( + <div> + <div className="form-group row"> + <div className="col-sm-3"> + <label>Workers:<span className="text-danger">*</span></label> + </div> + <div className="col-sm-7"> + <span style={{float : 'left'}}>0</span><input type="range" title={rebalanceData.workers +' workers selected.'} min={0} style={{width : '90%', float : 'left',margin : '6px 7px' }} max={this.totalWorker} onChange={this.rebalanceValueChange.bind(this,'workers')} /><span style={{float : 'left', clear : 'right'}}>{this.totalWorker}</span> + </div> + </div> + { + _.map(spoutArr, (s , i) => { + return <div className="form-group row" key={i}> + <div className="col-sm-3"> + <label>{s.spoutId}:<span className="text-danger">*</span></label> + </div> + <div className="col-sm-7"> + <input type="number" className="form-control" min={0} defaultValue={s.executors} onChange={this.rebalanceValueChange.bind(this,s.spoutId)} /> + </div> + </div>; + }) + } + { + _.map(boltArr, (b , n) => { + return <div className="form-group row" key={n}> + <div className="col-sm-3"> + <label>{b.boltId}:<span className="text-danger">*</span></label> + </div> + <div className="col-sm-7"> + <input type="number" className="form-control" min={0} defaultValue={b.executors} onChange={this.rebalanceValueChange.bind(this,b.boltId)} /> + </div> + </div>; + }) + } + <div className="form-group row"> + <div className="col-sm-3"> + <label>Wait Time:<span className="text-danger">*</span></label> + </div> + <div className="col-sm-7"> + <input type="number" className="form-control" min={0} value={waitTime} onChange={this.waitTimeChange.bind(this)} /> + </div> + </div> + </div> + ); + } +} http://git-wip-us.apache.org/repos/asf/ambari/blob/e3931cc2/contrib/views/storm/src/main/resources/ui/app/scripts/components/SearchLogs.jsx ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/ui/app/scripts/components/SearchLogs.jsx b/contrib/views/storm/src/main/resources/ui/app/scripts/components/SearchLogs.jsx new file mode 100644 index 0000000..ebf1615 --- /dev/null +++ b/contrib/views/storm/src/main/resources/ui/app/scripts/components/SearchLogs.jsx @@ -0,0 +1,84 @@ +/** + 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. +**/ + +import React, {Component} from 'react'; +import ReactDOM from 'react-dom'; +import {baseUrl} from '../utils/Constants'; +import {DropdownButton, FormGroup, Checkbox} from 'react-bootstrap'; +import fetch from 'isomorphic-fetch'; + +export default class SearchLogs extends Component{ + render() { + return ( + <div className="col-md-3 pull-right searchbar"> + <div className="input-group"> + <input type="text" id="searchBox" className="form-control" placeholder="Search in Logs"/> + <div className="input-group-btn"> + <div className="btn-group" role="group"> + <div className="dropdown dropdown-lg"> + <DropdownButton title="" pullRight id="bg-nested-dropdown"> + <FormGroup> + <Checkbox id="searchArchivedLogs">Search archived logs</Checkbox> + </FormGroup> + <FormGroup> + <Checkbox id="deepSearch">Deep search</Checkbox> + </FormGroup> + </DropdownButton> + </div> + <button type="button" className="btn btn-default" onClick={this.handleSearch.bind(this)}> + <i className="fa fa-search"></i> + </button> + </div> + </div> + </div> + </div> + ); + } + handleSearch(){ + var searchBoxEl = document.getElementById('searchBox'); + var searchArchivedLogsEl = document.getElementById('searchArchivedLogs'); + var deepSearchEl = document.getElementById('deepSearch'); + var topologyId = this.props.id; + + fetch(baseUrl.replace('proxy?url=/api/v1/', 'storm_details'), {"credentials": "same-origin"}) + .then((response) => { + return response.json(); + }) + .then((response) => { + var url = response.hostdata+'/'; + if(deepSearchEl.checked == true){ + url += "deep_search_result.html"; + }else{ + url += "search_result.html"; + } + url += '?search='+searchBoxEl.value+'&id='+ topologyId +'&count=1'; + if(searchArchivedLogsEl.checked == true){ + if(deepSearchEl.checked == true){ + url += "&search-archived=on"; + }else{ + url += "&searchArchived=checked"; + } + } + window.open(url, '_blank'); + + searchBoxEl.value = ''; + searchArchivedLogsEl.checked = false; + deepSearchEl.checked = false; + }); + } +} http://git-wip-us.apache.org/repos/asf/ambari/blob/e3931cc2/contrib/views/storm/src/main/resources/ui/app/scripts/components/TopologyGraph.jsx ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/ui/app/scripts/components/TopologyGraph.jsx b/contrib/views/storm/src/main/resources/ui/app/scripts/components/TopologyGraph.jsx new file mode 100644 index 0000000..4cfc6bb --- /dev/null +++ b/contrib/views/storm/src/main/resources/ui/app/scripts/components/TopologyGraph.jsx @@ -0,0 +1,208 @@ +/** + 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. +**/ + +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; +import ReactDOM from 'react-dom'; +import d3 from 'd3'; +import d3Tip from 'd3-tip'; +import dagreD3 from 'dagre-d3/dist/dagre-d3'; + +export default class TopologyGraph extends Component{ + static propTypes = { + data: PropTypes.object.isRequired, + width: PropTypes.string, + height: PropTypes.string + } + constructor(props) { + super(props); + this.syncData(this.props.data); + this.updateFlag = true; + } + componentDidUpdate() { + if(!!this.updateFlag){ + this.syncData(this.props.data); + this.updateGraph(); + } + } + componentWillReceiveProps(nextProps){ + _.isEqual(nextProps.data,this.props.data) ? this.updateFlag = false : this.updateFlag = true; + } + componentDidMount(){ + var that = this; + this.svg = d3.select(ReactDOM.findDOMNode(this)); + //Set up tooltip + this.tooltip = d3Tip() + .attr('class', function() { + return 'd3-tip testing'; + }) + .offset([-10, 0]) + .html(function(data) { + var d = that.g.node(data); + var tip = "<ul>"; + if (d[":capacity"] !== null){ tip += "<li>Capacity: " + d[":capacity"].toFixed(2) + "</li>";} + if (d[":latency"] !== null){ tip += "<li>Latency: " + d[":latency"].toFixed(2) + "</li>";} + if (d[":transferred"] !== null){ tip += "<li>Transferred: " + d[":transferred"].toFixed(2) + "</li>";} + tip += "</ul>"; + return tip; + }); + //Set up zoom + this.zoom = d3.behavior.zoom() + .scaleExtent([0, 8]) + .on("zoom", this.zoomed.bind(this)); + } + zoomed(){ + this.inner.attr("transform", + "translate(" + this.zoom.translate() + ")" + + "scale(" + this.zoom.scale() + ")" + ); + } + // update graph (called when needed) + updateGraph(){ + var that = this; + var g = ReactDOM.findDOMNode(this).children[0]; + if(g){ + g.remove(); + } + var inner = this.inner = this.svg.append("g"); + // Create the renderer + var render = new dagreD3.render(); + render.arrows().arrowPoint = (parent, id, edge, type) => { + var marker = parent.append("marker") + .attr("id", id) + .attr("viewBox", "0 0 10 10") + .attr("refX", 5) + .attr("refY", 5) + .attr("markerUnits", "strokeWidth") + .attr("markerWidth", 6) + .attr("markerHeight", 6.5) + .attr("orient", "auto"); + var path = marker.append("path") + .attr("d", "M 0 0 L 10 5 L 0 10 z") + .style("stroke-width", 1) + .style("stroke-dasharray", "1,0") + .style("fill", "grey") + .style("stroke", "grey"); + dagreD3.util.applyStyle(path, edge[type + "Style"]); + }; + + render.shapes().img = (parent, bbox, node) => { + var shapeSvg; + if(parent){ + shapeSvg = parent.insert("image") + .attr("class", "nodeImage") + .attr("xlink:href", function(d) { + if (node) { + if(node.type === 'spout'){ + return "styles/img/icon-spout.png"; + } else if(node.type === 'bolt'){ + return "styles/img/icon-bolt.png"; + } + } + }).attr("x", "-12px") + .attr("y", "-12px") + .attr("width", "30px") + .attr("height", "30px"); + } + node.intersect = function(point) { + return dagreD3.intersect.circle(node, 20, point); + }; + return shapeSvg; + }; + this.svg.call(this.zoom).call(this.tooltip); + // Run the renderer. This is what draws the final graph. + render(inner, this.g); + + inner.selectAll("g.nodes image") + .on('mouseover', function(d) { + that.tooltip.show(d, this); + }) + .on('mouseout', function(d) { + that.tooltip.hide(this); + }); + inner.selectAll("g.nodes g.label") + .attr("transform", "translate(2,-30)"); + // Center the graph + var initialScale = 1; + var svgWidth = this.svg[0][0].parentNode.clientWidth; + var svgHeight = this.svg[0][0].parentNode.clientHeight; + if(this.linkArray.length > 0){ + this.zoom.translate([(svgWidth - this.g.graph().width * initialScale) / 2, (svgHeight - this.g.graph().height * initialScale) / 2]) + .scale(initialScale) + .event(this.svg); + } + } + syncData(data){ + this.nodeArray = []; + this.linkArray = []; + this.g = new dagreD3.graphlib.Graph().setGraph({ + nodesep: 50, + ranksep: 190, + rankdir: "LR", + marginx: 20, + marginy: 20 + // transition: function transition(selection) { + // return selection.transition().duration(500); + // } + }); + if(data){ + var keys = _.keys(data); + keys.map(function(key){ + if(!key.startsWith('__')){ + data[key].id = key; + data[key].type = data[key][":type"]; + this.nodeArray.push(data[key]); + } + }.bind(this)); + + var spoutObjArr = _.filter(this.nodeArray, { "type": "spout" }); + if (spoutObjArr.length > 1) { + for(var i = 0; i < spoutObjArr.length; i++){ + spoutObjArr[i].x = 50; + spoutObjArr[i].y = parseInt(i+'10', 10); + spoutObjArr[i].fixed = true; + } + } else if (spoutObjArr.length == 1) { + spoutObjArr[0].x = 50; + spoutObjArr[0].y = 100; + spoutObjArr[0].fixed = true; + } + + this.nodeArray.map(function(node){ + var inputArr = node[":inputs"] || []; + inputArr.map(function(input){ + if(!input[":component"].startsWith("__")){ + var sourceNode = _.find(this.nodeArray, {id: input[":component"]}); + this.linkArray.push({ + source: sourceNode, + target: node + }); + this.g.setNode(sourceNode.id, _.extend(sourceNode, {label: sourceNode.id, shape: 'img'})); + this.g.setNode(node.id, _.extend(node, {label: node.id, shape: 'img'})); + this.g.setEdge(sourceNode.id, node.id, {"arrowhead": 'arrowPoint'}); + } + }.bind(this)); + }.bind(this)); + } + } + render() { + return ( + <svg className="topology-graph" width="100%" height="300"></svg> + ); + } +} http://git-wip-us.apache.org/repos/asf/ambari/blob/e3931cc2/contrib/views/storm/src/main/resources/ui/app/scripts/containers/BaseContainer.jsx ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/ui/app/scripts/containers/BaseContainer.jsx b/contrib/views/storm/src/main/resources/ui/app/scripts/containers/BaseContainer.jsx new file mode 100644 index 0000000..62846b7 --- /dev/null +++ b/contrib/views/storm/src/main/resources/ui/app/scripts/containers/BaseContainer.jsx @@ -0,0 +1,50 @@ +/** + 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. +**/ + +import React, {Component} from 'react'; +import Footer from '../components/Footer'; +import {Confirm} from '../components/FSModel'; + +export default class BaseContainer extends Component { + + constructor(props) { + super(props); + } + + handleKeyPress = (event) => { + event.key === "Enter" + ? this.refs.Confirm.state.show + ? this.refs.Confirm.sure() + : '' + :event.key === "Escape" + ? this.refs.Confirm.state.show + ? this.refs.Confirm.cancel() + : '' + :''; + } + + render() { + return ( + <div className="container-fluid"> + {this.props.children} + <Confirm ref="Confirm" onKeyUp={this.handleKeyPress}/> + <Footer /> + </div> + ); + } +} http://git-wip-us.apache.org/repos/asf/ambari/blob/e3931cc2/contrib/views/storm/src/main/resources/ui/app/scripts/containers/ClusterSummary.jsx ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/ui/app/scripts/containers/ClusterSummary.jsx b/contrib/views/storm/src/main/resources/ui/app/scripts/containers/ClusterSummary.jsx new file mode 100644 index 0000000..904ed68 --- /dev/null +++ b/contrib/views/storm/src/main/resources/ui/app/scripts/containers/ClusterSummary.jsx @@ -0,0 +1,125 @@ +/** + 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. +**/ + +import React, {Component} from 'react'; +import RadialChart from '../components/RadialChart'; +import FSReactToastr from '../components/FSReactToastr'; +import {toastOpt} from '../utils/Constants'; +import TopologyREST from '../rest/TopologyREST'; +import NimbusSummary from './NimbusSummary'; +import CommonNotification from '../components/CommonNotification'; +import {OverlayTrigger, Tooltip} from 'react-bootstrap'; + +export default class ClusterSummary extends Component{ + constructor(props){ + super(props); + this.fetchData(); + this.state = { + entity :{} + }; + } + + fetchData = () => { + TopologyREST.getSummary('cluster').then((result) => { + if(result.errorMessage !== undefined){ + FSReactToastr.error( + <CommonNotification flag="error" content={result.errorMessage}/>, '', toastOpt); + } else { + this.setState({entity : result}); + } + }); + } + render(){ + const {entity} = this.state; + return( + <div> + <div className="row"> + <div className="col-sm-6"> + <OverlayTrigger placement="bottom" overlay={<Tooltip id="tooltip1">Executors are threads in a Worker process.</Tooltip>}> + <div className="tile primary"> + <div className="tile-header">Executor</div> + <div className="tile-body"> + <i className="fa fa-play-circle-o"></i> + <span className="count">{entity.executorsTotal}</span> + </div> + </div> + </OverlayTrigger> + </div> + <div className="col-sm-6"> + <OverlayTrigger placement="bottom" overlay={<Tooltip id="tooltip1">A Task is an instance of a Bolt or Spout. The number of Tasks is almost always equal to the number of Executors.</Tooltip>}> + <div className="tile warning"> + <div className="tile-header">Tasks</div> + <div className="tile-body"> + <i className="fa fa-tasks"></i> + <span className="count">{entity.tasksTotal}</span> + </div> + </div> + </OverlayTrigger> + </div> + </div> + <div className="row"> + <div className="col-sm-6"> + <OverlayTrigger placement="bottom" overlay={<Tooltip id="tooltip1">The number of nodes in the cluster currently.</Tooltip>}> + <div className="tile success"> + <div className="tile-header" style={{textAlign:"center"}}>Supervisor</div> + <div className="tile-body" style={{textAlign:"center"}}> + <div id="supervisorCount"> + <RadialChart + data={[entity.supervisors,entity.supervisors]} + labels={['Used','Total']} + width={100} + height={100} + innerRadius={46} + outerRadius={50} + color={["rgba(255,255,255,0.6)", "rgba(255,255,255,1)"]} + /> + </div> + </div> + </div> + </OverlayTrigger> + </div> + <div className="col-sm-6"> + <OverlayTrigger placement="bottom" overlay={<Tooltip id="tooltip1">Slots are Workers (processes).</Tooltip>}> + <div className="tile danger"> + <div className="tile-header" style={{textAlign:"center"}}>Slots</div> + <div className="tile-body" style={{textAlign:"center"}}> + <div id="slotsCount"> + <RadialChart + data={[entity.slotsUsed,entity.slotsTotal]} + labels={['Used','Total']} + width={100} + height={100} + innerRadius={46} + outerRadius={50} + color={["rgba(255,255,255,0.6)", "rgba(255,255,255,1)"]} + /> + </div> + </div> + </div> + </OverlayTrigger> + </div> + </div> + <div className="row"> + <div className="col-sm-12"> + <NimbusSummary fromDashboard={true}/> + </div> + </div> + </div> + ); + } +}