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