github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/public/static/js/jsx/waterfall.jsx (about) 1 /* 2 ReactJS code for the Waterfall page. Grid calls the Variant class for each distro, and the Variant class renders each build variant for every version that exists. In each build variant we iterate through all the tasks and render them as well. The row of headers is just a placeholder at the moment. 3 */ 4 5 // Returns string from datetime object in "5/7/96 1:15 AM" format 6 // Used to display version headers 7 function getFormattedTime(input, userTz, fmt) { 8 return moment(input).tz(userTz).format(fmt); 9 } 10 11 function generateURLParameters(params) { 12 var ret = []; 13 for (var p in params) { 14 ret.push(encodeURIComponent(p) + "=" + encodeURIComponent(params[p])); 15 } 16 return ret.join("&"); 17 } 18 19 // getParameterByName returns the value associated with a given query parameter. 20 // Based on: http://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript 21 function getParameterByName(name, url) { 22 name = name.replace(/[\[\]]/g, "\\$&"); 23 var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"); 24 var results = regex.exec(url); 25 if (!results){ 26 return null; 27 } 28 if (!results[2]){ 29 return ''; 30 } 31 return decodeURIComponent(results[2].replace(/\+/g, " ")); 32 } 33 34 function updateURLParams(bvFilter, taskFilter, skip, baseURL) { 35 var params = {}; 36 if (bvFilter && bvFilter != '') 37 params["bv_filter"]= bvFilter; 38 if (taskFilter && taskFilter != '') 39 params["task_filter"]= taskFilter; 40 params["skip"] = skip 41 42 var paramString = generateURLParameters(params); 43 window.history.replaceState({}, '', baseURL + "?" + paramString); 44 } 45 46 var JIRA_REGEX = /[A-Z]{1,10}-\d{1,6}/ig; 47 48 class JiraLink extends React.Component { 49 constructor(props) { 50 super(props); 51 } 52 53 render() { 54 var contents 55 56 if (_.isString(this.props.children)) { 57 let tokens = this.props.children.split(/\s/); 58 let jiraHost = this.props.jiraHost; 59 60 contents = _.map(tokens, function(token, i){ 61 let hasSpace = i !== (tokens.length - 1); 62 let maybeSpace = hasSpace ? ' ': ''; 63 if(token.match(JIRA_REGEX)) { 64 let jiraLink = jiraHost+"/browse/"+token; 65 return ( 66 <a href={jiraLink}>{token+maybeSpace}</a> 67 ); 68 } else { 69 return token + maybeSpace; 70 } 71 }); 72 } else { 73 return null; 74 } 75 return ( 76 <div> 77 {contents} 78 </div> 79 ); 80 } 81 } 82 83 84 // The Root class renders all components on the waterfall page, including the grid view and the filter and new page buttons 85 // The one exception is the header, which is written in Angular and managed by menu.html 86 class Root extends React.Component{ 87 constructor(props){ 88 super(props); 89 // Initialize newer|older buttons 90 var versionsOnPage = _.reduce(_.map(window.serverData.versions, function(version){ 91 return version.authors.length; 92 }), function(memo,num){ 93 return memo + num; 94 }); 95 96 this.baseURL = "/waterfall/" + this.props.project; 97 this.currentSkip = window.serverData.current_skip; 98 this.nextSkip = this.currentSkip + versionsOnPage; 99 this.prevSkip = this.currentSkip - window.serverData.previous_page_count; 100 101 if (this.nextSkip >= window.serverData.total_versions) { 102 this.nextSkip = -1; 103 } 104 if (this.currentSkip <= 0) { 105 this.prevSkip = -1; 106 } 107 108 var buildVariantFilter = getParameterByName('bv_filter',window.location.href); 109 var taskFilter = getParameterByName('task_filter',window.location.href); 110 111 buildVariantFilter = buildVariantFilter || ''; 112 taskFilter = taskFilter || ''; 113 114 var collapsed = localStorage.getItem("collapsed") == "true"; 115 116 this.state = { 117 collapsed: collapsed, 118 shortenCommitMessage: true, 119 buildVariantFilter: buildVariantFilter, 120 taskFilter:taskFilter 121 }; 122 123 // Handle state for a collapsed view, as well as shortened header commit messages 124 this.handleCollapseChange = this.handleCollapseChange.bind(this); 125 this.handleHeaderLinkClick = this.handleHeaderLinkClick.bind(this); 126 this.handleBuildVariantFilter = this.handleBuildVariantFilter.bind(this); 127 this.handleTaskFilter = this.handleTaskFilter.bind(this); 128 } 129 130 handleCollapseChange(collapsed) { 131 localStorage.setItem("collapsed", collapsed); 132 this.setState({collapsed: collapsed}); 133 } 134 handleBuildVariantFilter(filter) { 135 updateURLParams(filter, this.state.taskFilter, this.currentSkip, this.baseURL); 136 this.setState({buildVariantFilter: filter}); 137 } 138 handleTaskFilter(filter) { 139 updateURLParams(this.state.buildVariantFilter, filter, this.currentSkip, this.baseURL); 140 this.setState({taskFilter: filter}); 141 } 142 handleHeaderLinkClick(shortenMessage) { 143 this.setState({shortenCommitMessage: !shortenMessage}); 144 } 145 render() { 146 if (this.props.data.rows.length == 0){ 147 return ( 148 <div> 149 There are no builds for this project. 150 </div> 151 ) 152 } 153 var collapseInfo = { 154 collapsed : this.state.collapsed, 155 activeTaskStatuses : ['failed','system-failed'], 156 }; 157 return ( 158 <div> 159 <Toolbar 160 collapsed={this.state.collapsed} 161 onCheck={this.handleCollapseChange} 162 baseURL={this.baseURL} 163 nextSkip={this.nextSkip} 164 prevSkip={this.prevSkip} 165 buildVariantFilter={this.state.buildVariantFilter} 166 taskFilter={this.state.taskFilter} 167 buildVariantFilterFunc={this.handleBuildVariantFilter} 168 taskFilterFunc={this.handleTaskFilter} 169 /> 170 <Headers 171 shortenCommitMessage={this.state.shortenCommitMessage} 172 versions={this.props.data.versions} 173 onLinkClick={this.handleHeaderLinkClick} 174 userTz={this.props.userTz} 175 jiraHost={this.props.jiraHost} 176 /> 177 <Grid 178 data={this.props.data} 179 collapseInfo={collapseInfo} 180 project={this.props.project} 181 buildVariantFilter={this.state.buildVariantFilter} 182 taskFilter={this.state.taskFilter} 183 /> 184 </div> 185 ) 186 } 187 } 188 189 190 // Toolbar 191 function Toolbar ({collapsed, 192 onCheck, 193 baseURL, 194 nextSkip, 195 prevSkip, 196 buildVariantFilter, 197 taskFilter, 198 buildVariantFilterFunc, 199 taskFilterFunc}) { 200 201 var Form = ReactBootstrap.Form; 202 return ( 203 <div className="row"> 204 <div className="col-xs-12"> 205 <Form inline className="waterfall-toolbar pull-right"> 206 <CollapseButton collapsed={collapsed} onCheck={onCheck} /> 207 <FilterBox 208 filterFunction={buildVariantFilterFunc} 209 placeholder={"Filter variant"} 210 currentFilter={buildVariantFilter} 211 disabled={false} 212 /> 213 <FilterBox 214 filterFunction={taskFilterFunc} 215 placeholder={"Filter task"} 216 currentFilter={taskFilter} 217 disabled={collapsed} 218 /> 219 <PageButtons 220 nextSkip={nextSkip} 221 prevSkip={prevSkip} 222 baseURL={baseURL} 223 buildVariantFilter={buildVariantFilter} 224 taskFilter={taskFilter} 225 /> 226 </Form> 227 </div> 228 </div> 229 ) 230 }; 231 232 function PageButtons ({prevSkip, nextSkip, baseURL, buildVariantFilter, taskFilter}) { 233 var ButtonGroup = ReactBootstrap.ButtonGroup; 234 235 var nextURL= ""; 236 var prevURL= ""; 237 238 prevURLParams = {}; 239 nextURLParams = {}; 240 241 nextURLParams["skip"] = nextSkip; 242 prevURLParams["skip"] = prevSkip; 243 if (buildVariantFilter && buildVariantFilter != '') { 244 nextURLParams["bv_filter"] = buildVariantFilter; 245 prevURLParams["bv_filter"] = buildVariantFilter; 246 } 247 if (taskFilter && taskFilter != '') { 248 nextURLParams["task_filter"] = taskFilter; 249 prevURLParams["task_filter"] = taskFilter; 250 } 251 nextURL = "?" + generateURLParameters(nextURLParams); 252 prevURL = "?" + generateURLParameters(prevURLParams); 253 return ( 254 <span className="waterfall-form-item"> 255 <ButtonGroup> 256 <PageButton pageURL={prevURL} disabled={prevSkip < 0} directionIcon="fa-chevron-left" /> 257 <PageButton pageURL={nextURL} disabled={nextSkip < 0} directionIcon="fa-chevron-right" /> 258 </ButtonGroup> 259 </span> 260 ); 261 } 262 263 function PageButton ({pageURL, directionIcon, disabled}) { 264 var Button = ReactBootstrap.Button; 265 var classes = "fa " + directionIcon; 266 return ( 267 <Button href={pageURL} disabled={disabled}><i className={classes}></i></Button> 268 ); 269 } 270 271 class FilterBox extends React.Component { 272 constructor(props){ 273 super(props); 274 this.applyFilter = this.applyFilter.bind(this); 275 } 276 applyFilter() { 277 this.props.filterFunction(this.refs.searchInput.value) 278 } 279 render() { 280 return <input type="text" ref="searchInput" 281 className="form-control waterfall-form-item" 282 placeholder={this.props.placeholder} 283 value={this.props.currentFilter} onChange={this.applyFilter} 284 disabled={this.props.disabled}/> 285 } 286 } 287 288 class CollapseButton extends React.Component{ 289 constructor(props){ 290 super(props); 291 this.handleChange = this.handleChange.bind(this); 292 } 293 handleChange(event){ 294 this.props.onCheck(this.refs.collapsedBuilds.checked); 295 } 296 render() { 297 return ( 298 <span className="semi-muted waterfall-form-item"> 299 <span id="collapsed-prompt">Show collapsed view</span> 300 <input 301 className="checkbox waterfall-checkbox" 302 type="checkbox" 303 checked={this.props.collapsed} 304 ref="collapsedBuilds" 305 onChange={this.handleChange} 306 /> 307 </span> 308 ) 309 } 310 } 311 312 // Headers 313 314 function Headers ({shortenCommitMessage, versions, onLinkClick, userTz, jiraHost}) { 315 return ( 316 <div className="row version-header"> 317 <div className="variant-col col-xs-2 version-header-rolled"></div> 318 <div className="col-xs-10"> 319 <div className="row"> 320 { 321 versions.map(function(version){ 322 if (version.rolled_up) { 323 return( 324 <RolledUpVersionHeader 325 key={version.ids[0]} 326 version={version} 327 userTz={userTz} 328 jiraHost={jiraHost} 329 /> 330 ); 331 } 332 // Unrolled up version, no popover 333 return ( 334 <ActiveVersionHeader 335 key={version.ids[0]} 336 version={version} 337 userTz = {userTz} 338 shortenCommitMessage={shortenCommitMessage} 339 onLinkClick={onLinkClick} 340 jiraHost={jiraHost} 341 /> 342 ); 343 }) 344 } 345 </div> 346 </div> 347 </div> 348 ) 349 } 350 351 352 function ActiveVersionHeader({shortenCommitMessage, version, onLinkClick, userTz, jiraHost}) { 353 var message = version.messages[0]; 354 var author = version.authors[0]; 355 var id_link = "/version/" + version.ids[0]; 356 var commit = version.revisions[0].substring(0,5); 357 var message = version.messages[0]; 358 var formatted_time = getFormattedTime(version.create_times[0], userTz, 'M/D/YY h:mm A' ); 359 const maxChars = 44 360 var button; 361 if (message.length > maxChars) { 362 // If we shorten the commit message, only display the first maxChars chars 363 if (shortenCommitMessage) { 364 message = message.substring(0, maxChars-3) + "..."; 365 } 366 button = ( 367 <HideHeaderButton onLinkClick={onLinkClick} shortenCommitMessage={shortenCommitMessage} /> 368 ); 369 } 370 371 return ( 372 <div className="header-col"> 373 <div className="version-header-expanded"> 374 <div className="col-xs-12"> 375 <div className="row"> 376 <a className="githash" href={id_link}>{commit}</a> 377 {formatted_time} 378 </div> 379 </div> 380 <div className="col-xs-12"> 381 <div className="row"> 382 <strong>{author}</strong> - <JiraLink jiraHost={jiraHost}>{message}</JiraLink> 383 {button} 384 </div> 385 </div> 386 </div> 387 </div> 388 ) 389 }; 390 391 class HideHeaderButton extends React.Component{ 392 constructor(props){ 393 super(props); 394 this.onLinkClick = this.onLinkClick.bind(this); 395 } 396 onLinkClick(event){ 397 this.props.onLinkClick(this.props.shortenCommitMessage); 398 } 399 render() { 400 var textToShow = this.props.shortenCommitMessage ? "more" : "less"; 401 return ( 402 <span onClick={this.onLinkClick}> <a href="#">{textToShow}</a> </span> 403 ) 404 } 405 } 406 407 function RolledUpVersionHeader({version, userTz, jiraHost}){ 408 var Popover = ReactBootstrap.Popover; 409 var OverlayTrigger = ReactBootstrap.OverlayTrigger; 410 var Button = ReactBootstrap.Button; 411 412 var versionStr = (version.messages.length > 1) ? "versions" : "version"; 413 var rolledHeader = version.messages.length + " inactive " + versionStr; 414 415 var popovers = ( 416 <Popover id="popover-positioned-bottom" title=""> 417 { 418 version.ids.map(function(id,i) { 419 return <RolledUpVersionSummary 420 author={version.authors[i]} 421 commit={version.revisions[i]} 422 message={version.messages[i]} 423 versionId={version.ids[i]} 424 key={id} userTz={userTz} 425 createTime={version.create_times[i]} 426 jiraHost={jiraHost} /> 427 }) 428 } 429 </Popover> 430 ); 431 432 return ( 433 <div className="header-col version-header-rolled"> 434 <OverlayTrigger trigger="click" placement="bottom" overlay={popovers} className="col-xs-2"> 435 <span className="pointer"> {rolledHeader} </span> 436 </OverlayTrigger> 437 </div> 438 ) 439 }; 440 function RolledUpVersionSummary ({author, commit, message, versionId, createTime, userTz, jiraHost}) { 441 var formatted_time = getFormattedTime(new Date(createTime), userTz, 'M/D/YY h:mm A' ); 442 commit = commit.substring(0,10); 443 444 return ( 445 <div className="rolled-up-version-summary"> 446 <span className="version-header-time">{formatted_time}</span> 447 <br /> 448 <a href={"/version/" + versionId}>{commit}</a> - <strong>{author}</strong> 449 <br /> 450 <JiraLink jiraHost={jiraHost}>{message}</JiraLink> 451 <br /> 452 </div> 453 ); 454 } 455 456