github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/service/waterfall.go (about)

     1  package service
     2  
     3  import (
     4  	"net/http"
     5  	"sort"
     6  	"strconv"
     7  	"time"
     8  
     9  	"github.com/evergreen-ci/evergreen"
    10  	"github.com/evergreen-ci/evergreen/apimodels"
    11  	"github.com/evergreen-ci/evergreen/model"
    12  	"github.com/evergreen-ci/evergreen/model/build"
    13  	"github.com/evergreen-ci/evergreen/model/task"
    14  	"github.com/evergreen-ci/evergreen/model/user"
    15  	"github.com/evergreen-ci/evergreen/model/version"
    16  	"github.com/pkg/errors"
    17  )
    18  
    19  const (
    20  	// VersionItemsToCreate is the number of waterfall versions to create,
    21  	// including rolled-up ones.
    22  	VersionItemsToCreate = 5
    23  
    24  	// SkipQueryParam is the string field for the skip value in the URL
    25  	// (how many versions to skip).
    26  	SkipQueryParam = "skip"
    27  
    28  	InactiveStatus = "inactive"
    29  )
    30  
    31  // Pull the skip value out of the http request
    32  func skipValue(r *http.Request) (int, error) {
    33  	// determine how many versions to skip
    34  	toSkipStr := r.FormValue(SkipQueryParam)
    35  	if toSkipStr == "" {
    36  		toSkipStr = "0"
    37  	}
    38  	return strconv.Atoi(toSkipStr)
    39  }
    40  
    41  // uiStatus determines task status label.
    42  func uiStatus(task waterfallTask) string {
    43  	switch task.Status {
    44  	case evergreen.TaskStarted, evergreen.TaskSucceeded,
    45  		evergreen.TaskFailed, evergreen.TaskDispatched:
    46  		return task.Status
    47  	case evergreen.TaskUndispatched:
    48  		if task.Activated {
    49  			return evergreen.TaskUndispatched
    50  		} else {
    51  			return evergreen.TaskInactive
    52  		}
    53  	default:
    54  		return ""
    55  	}
    56  }
    57  
    58  type versionVariantData struct {
    59  	Rows          map[string]waterfallRow `json:"rows"`
    60  	Versions      []waterfallVersion      `json:"versions"`
    61  	BuildVariants waterfallBuildVariants  `json:"build_variants"`
    62  }
    63  
    64  // waterfallData is all of the data that gets sent to the waterfall page on load
    65  type waterfallData struct {
    66  	Rows              []waterfallRow     `json:"rows"`
    67  	Versions          []waterfallVersion `json:"versions"`
    68  	TotalVersions     int                `json:"total_versions"`      // total number of versions (for pagination)
    69  	CurrentSkip       int                `json:"current_skip"`        // number of versions skipped so far
    70  	PreviousPageCount int                `json:"previous_page_count"` // number of versions on previous page
    71  	CurrentTime       int64              `json:"current_time"`        // time used to calculate the eta of started task
    72  }
    73  
    74  // waterfallBuildVariant stores the Id and DisplayName for a given build
    75  // This struct is associated with one waterfallBuild
    76  type waterfallBuildVariant struct {
    77  	Id          string `json:"id"`
    78  	DisplayName string `json:"display_name"`
    79  }
    80  
    81  // waterfallRow represents one row associated with a build variant.
    82  type waterfallRow struct {
    83  	BuildVariant waterfallBuildVariant     `json:"build_variant"`
    84  	Builds       map[string]waterfallBuild `json:"builds"`
    85  }
    86  
    87  // waterfallBuild represents one set of tests for a given build variant and version
    88  type waterfallBuild struct {
    89  	Id              string          `json:"id"`
    90  	Active          bool            `json:"active"`
    91  	Version         string          `json:"version"`
    92  	Tasks           []waterfallTask `json:"tasks"`
    93  	TaskStatusCount taskStatusCount `json:"taskStatusCount"`
    94  }
    95  
    96  // waterfallTask represents one task in the waterfall UI.
    97  type waterfallTask struct {
    98  	Id               string                  `json:"id"`
    99  	Status           string                  `json:"status"`
   100  	StatusDetails    apimodels.TaskEndDetail `json:"task_end_details"`
   101  	DisplayName      string                  `json:"display_name"`
   102  	TimeTaken        time.Duration           `json:"time_taken"`
   103  	Activated        bool                    `json:"activated"`
   104  	FailedTestNames  []string                `json:"failed_test_names,omitempty"`
   105  	ExpectedDuration time.Duration           `json:"expected_duration,omitempty"`
   106  	StartTime        int64                   `json:"start_time"`
   107  }
   108  
   109  // failedTest holds all the information for displaying context about tests that failed in a
   110  // waterfall page tooltip.
   111  
   112  // waterfallVersion holds the waterfall UI representation of a single version (column)
   113  // If the RolledUp field is false, then it contains information about
   114  // a single version and the metadata fields will be of length 1.
   115  // If the RolledUp field is true, this represents multiple inactive versions, with each element
   116  // in the metadata arrays corresponding to one inactive version,
   117  // ordered from most recent inactive version to earliest.
   118  type waterfallVersion struct {
   119  
   120  	// whether or not the version element actually consists of multiple inactive
   121  	// versions rolled up into one
   122  	RolledUp bool `json:"rolled_up"`
   123  
   124  	// metadata about the enclosed versions.  if this version does not consist
   125  	// of multiple rolled-up versions, these will each only have length 1
   126  	Ids                 []string    `json:"ids"`
   127  	Messages            []string    `json:"messages"`
   128  	Authors             []string    `json:"authors"`
   129  	CreateTimes         []time.Time `json:"create_times"`
   130  	Revisions           []string    `json:"revisions"`
   131  	RevisionOrderNumber int         `json:"revision_order"`
   132  
   133  	// used to hold any errors that were found in creating the version
   134  	Errors   []waterfallVersionError `json:"errors"`
   135  	Warnings []waterfallVersionError `json:"warnings"`
   136  	Ignoreds []bool                  `json:"ignoreds"`
   137  }
   138  
   139  type waterfallVersionError struct {
   140  	Messages []string `json:"messages"`
   141  }
   142  
   143  // waterfallBuildVariants implements the sort interface to allow backend sorting.
   144  type waterfallBuildVariants []waterfallBuildVariant
   145  
   146  func (wfbv waterfallBuildVariants) Len() int {
   147  	return len(wfbv)
   148  }
   149  
   150  func (wfbv waterfallBuildVariants) Less(i, j int) bool {
   151  	return wfbv[i].DisplayName < wfbv[j].DisplayName
   152  }
   153  
   154  func (wfbv waterfallBuildVariants) Swap(i, j int) {
   155  	wfbv[i], wfbv[j] = wfbv[j], wfbv[i]
   156  }
   157  
   158  // waterfallVersions implements the sort interface to allow backend sorting.
   159  type waterfallVersions []waterfallVersion
   160  
   161  func (wfv waterfallVersions) Len() int {
   162  	return len(wfv)
   163  }
   164  
   165  func (wfv waterfallVersions) Less(i, j int) bool {
   166  	return wfv[i].RevisionOrderNumber > wfv[j].RevisionOrderNumber
   167  }
   168  
   169  func (wfv waterfallVersions) Swap(i, j int) {
   170  	wfv[i], wfv[j] = wfv[j], wfv[i]
   171  }
   172  
   173  // createWaterfallTasks takes ina  build's task cache returns a list of waterfallTasks.
   174  func createWaterfallTasks(tasks []build.TaskCache) ([]waterfallTask, taskStatusCount) {
   175  	//initialize and set TaskStatusCount fields to zero
   176  	statusCount := taskStatusCount{}
   177  	waterfallTasks := []waterfallTask{}
   178  
   179  	// add the tasks to the build
   180  	for _, t := range tasks {
   181  		taskForWaterfall := waterfallTask{
   182  			Id:            t.Id,
   183  			Status:        t.Status,
   184  			StatusDetails: t.StatusDetails,
   185  			DisplayName:   t.DisplayName,
   186  			Activated:     t.Activated,
   187  			TimeTaken:     t.TimeTaken,
   188  			StartTime:     t.StartTime.UnixNano(),
   189  		}
   190  		taskForWaterfall.Status = uiStatus(taskForWaterfall)
   191  
   192  		statusCount.incrementStatus(taskForWaterfall.Status, taskForWaterfall.StatusDetails)
   193  
   194  		waterfallTasks = append(waterfallTasks, taskForWaterfall)
   195  	}
   196  	return waterfallTasks, statusCount
   197  }
   198  
   199  // Fetch versions until 'numVersionElements' elements are created, including
   200  // elements consisting of multiple versions rolled-up into one.
   201  // The skip value indicates how many versions back in time should be skipped
   202  // before starting to fetch versions, the project indicates which project the
   203  // returned versions should be a part of.
   204  func getVersionsAndVariants(skip, numVersionElements int, project *model.Project) (versionVariantData, error) {
   205  	// the final array of versions to return
   206  	finalVersions := []waterfallVersion{}
   207  
   208  	// keep track of the build variants we see
   209  	bvSet := map[string]bool{}
   210  
   211  	waterfallRows := map[string]waterfallRow{}
   212  
   213  	// build variant mappings - used so we can store the display name as
   214  	// the build variant field of a build
   215  	buildVariantMappings := project.GetVariantMappings()
   216  
   217  	// keep track of the last rolled-up version, so inactive versions can
   218  	// be added
   219  	var lastRolledUpVersion *waterfallVersion
   220  
   221  	// loop until we have enough from the db
   222  	for len(finalVersions) < numVersionElements {
   223  
   224  		// fetch the versions and associated builds
   225  		versionsFromDB, buildsByVersion, err :=
   226  			fetchVersionsAndAssociatedBuilds(project, skip, numVersionElements)
   227  
   228  		if err != nil {
   229  			return versionVariantData{}, errors.Wrap(err,
   230  				"error fetching versions and builds:")
   231  		}
   232  
   233  		// if we've reached the beginning of all versions
   234  		if len(versionsFromDB) == 0 {
   235  			break
   236  		}
   237  
   238  		// to fetch started tasks and failed tests for providing additional context
   239  		// in a tooltip
   240  		failedAndStartedTaskIds := []string{}
   241  
   242  		// update the amount skipped
   243  		skip += len(versionsFromDB)
   244  
   245  		// create the necessary versions, rolling up inactive ones
   246  		for _, versionFromDB := range versionsFromDB {
   247  
   248  			// if we have hit enough versions, break out
   249  			if len(finalVersions) == numVersionElements {
   250  				break
   251  			}
   252  
   253  			// the builds for the version
   254  			buildsInVersion := buildsByVersion[versionFromDB.Id]
   255  
   256  			// see if there are any active tasks in the version
   257  			versionActive := anyActiveTasks(buildsInVersion)
   258  
   259  			// add any represented build variants to the set and initialize rows
   260  			for _, b := range buildsInVersion {
   261  				bvSet[b.BuildVariant] = true
   262  
   263  				buildVariant := waterfallBuildVariant{
   264  					Id:          b.BuildVariant,
   265  					DisplayName: buildVariantMappings[b.BuildVariant],
   266  				}
   267  
   268  				if buildVariant.DisplayName == "" {
   269  					buildVariant.DisplayName = b.BuildVariant +
   270  						" (removed)"
   271  				}
   272  
   273  				if _, ok := waterfallRows[b.BuildVariant]; !ok {
   274  					waterfallRows[b.BuildVariant] = waterfallRow{
   275  						Builds:       map[string]waterfallBuild{},
   276  						BuildVariant: buildVariant,
   277  					}
   278  				}
   279  
   280  			}
   281  
   282  			// if it is inactive, roll up the version and don't create any
   283  			// builds for it
   284  			if !versionActive {
   285  				if lastRolledUpVersion == nil {
   286  					lastRolledUpVersion = &waterfallVersion{RolledUp: true, RevisionOrderNumber: versionFromDB.RevisionOrderNumber}
   287  				}
   288  
   289  				// add the version metadata into the last rolled-up version
   290  				lastRolledUpVersion.Ids = append(lastRolledUpVersion.Ids,
   291  					versionFromDB.Id)
   292  				lastRolledUpVersion.Authors = append(lastRolledUpVersion.Authors,
   293  					versionFromDB.Author)
   294  				lastRolledUpVersion.Errors = append(
   295  					lastRolledUpVersion.Errors, waterfallVersionError{versionFromDB.Errors})
   296  				lastRolledUpVersion.Warnings = append(
   297  					lastRolledUpVersion.Warnings, waterfallVersionError{versionFromDB.Warnings})
   298  				lastRolledUpVersion.Messages = append(
   299  					lastRolledUpVersion.Messages, versionFromDB.Message)
   300  				lastRolledUpVersion.Ignoreds = append(
   301  					lastRolledUpVersion.Ignoreds, versionFromDB.Ignored)
   302  				lastRolledUpVersion.CreateTimes = append(
   303  					lastRolledUpVersion.CreateTimes, versionFromDB.CreateTime)
   304  				lastRolledUpVersion.Revisions = append(
   305  					lastRolledUpVersion.Revisions, versionFromDB.Revision)
   306  
   307  				// move on to the next version
   308  				continue
   309  			}
   310  
   311  			// add a pending rolled-up version, if it exists
   312  			if lastRolledUpVersion != nil {
   313  				finalVersions = append(finalVersions, *lastRolledUpVersion)
   314  				lastRolledUpVersion = nil
   315  			}
   316  
   317  			// if we have hit enough versions, break out
   318  			if len(finalVersions) == numVersionElements {
   319  				break
   320  			}
   321  
   322  			// if the version can not be rolled up, create a fully fledged
   323  			// version for it
   324  			activeVersion := waterfallVersion{
   325  				Ids:                 []string{versionFromDB.Id},
   326  				Messages:            []string{versionFromDB.Message},
   327  				Authors:             []string{versionFromDB.Author},
   328  				CreateTimes:         []time.Time{versionFromDB.CreateTime},
   329  				Revisions:           []string{versionFromDB.Revision},
   330  				Errors:              []waterfallVersionError{{versionFromDB.Errors}},
   331  				Warnings:            []waterfallVersionError{{versionFromDB.Warnings}},
   332  				Ignoreds:            []bool{versionFromDB.Ignored},
   333  				RevisionOrderNumber: versionFromDB.RevisionOrderNumber,
   334  			}
   335  
   336  			// add the builds to the waterfall row
   337  			for _, b := range buildsInVersion {
   338  				currentRow := waterfallRows[b.BuildVariant]
   339  				buildForWaterfall := waterfallBuild{
   340  					Id:      b.Id,
   341  					Version: versionFromDB.Id,
   342  				}
   343  
   344  				tasks, statusCount := createWaterfallTasks(b.Tasks)
   345  				buildForWaterfall.Tasks = tasks
   346  				buildForWaterfall.TaskStatusCount = statusCount
   347  				currentRow.Builds[versionFromDB.Id] = buildForWaterfall
   348  				waterfallRows[b.BuildVariant] = currentRow
   349  				for _, task := range buildForWaterfall.Tasks {
   350  					if task.Status == evergreen.TaskFailed || task.Status == evergreen.TaskStarted {
   351  						failedAndStartedTaskIds = append(failedAndStartedTaskIds, task.Id)
   352  					}
   353  				}
   354  			}
   355  
   356  			// add the version
   357  			finalVersions = append(finalVersions, activeVersion)
   358  
   359  		}
   360  
   361  		failedAndStartedTasks, err := task.Find(task.ByIds(failedAndStartedTaskIds))
   362  		if err != nil {
   363  			return versionVariantData{}, errors.Wrap(err, "error fetching failed tasks")
   364  
   365  		}
   366  		addFailedAndStartedTests(waterfallRows, failedAndStartedTasks)
   367  	}
   368  
   369  	// if the last version was rolled-up, add it
   370  	if lastRolledUpVersion != nil {
   371  		finalVersions = append(finalVersions, *lastRolledUpVersion)
   372  	}
   373  
   374  	// create the list of display names for the build variants represented
   375  	buildVariants := waterfallBuildVariants{}
   376  	for name := range bvSet {
   377  		displayName := buildVariantMappings[name]
   378  		if displayName == "" {
   379  			displayName = name + " (removed)"
   380  		}
   381  		buildVariants = append(buildVariants, waterfallBuildVariant{Id: name, DisplayName: displayName})
   382  	}
   383  
   384  	return versionVariantData{
   385  		Rows:          waterfallRows,
   386  		Versions:      finalVersions,
   387  		BuildVariants: buildVariants,
   388  	}, nil
   389  
   390  }
   391  
   392  // addFailedTests adds all of the failed tests associated with a task to its entry in the waterfallRow.
   393  // addFailedAndStartedTests adds all of the failed tests associated with a task to its entry in the waterfallRow
   394  // and adds the estimated duration to started tasks.
   395  func addFailedAndStartedTests(waterfallRows map[string]waterfallRow, failedAndStartedTasks []task.Task) {
   396  	failedTestsByTaskId := map[string][]string{}
   397  	expectedDurationByTaskId := map[string]time.Duration{}
   398  	for _, t := range failedAndStartedTasks {
   399  		failedTests := []string{}
   400  		for _, r := range t.TestResults {
   401  			if r.Status == evergreen.TestFailedStatus {
   402  				failedTests = append(failedTests, r.TestFile)
   403  			}
   404  		}
   405  		if t.Status == evergreen.TaskStarted {
   406  			expectedDurationByTaskId[t.Id] = t.ExpectedDuration
   407  		}
   408  		failedTestsByTaskId[t.Id] = failedTests
   409  	}
   410  	for buildVariant, row := range waterfallRows {
   411  		for versionId, build := range row.Builds {
   412  			for i, task := range build.Tasks {
   413  				if len(failedTestsByTaskId[task.Id]) != 0 {
   414  					waterfallRows[buildVariant].Builds[versionId].Tasks[i].FailedTestNames = append(
   415  						waterfallRows[buildVariant].Builds[versionId].Tasks[i].FailedTestNames,
   416  						failedTestsByTaskId[task.Id]...)
   417  					sort.Strings(waterfallRows[buildVariant].Builds[versionId].Tasks[i].FailedTestNames)
   418  				}
   419  				if duration, ok := expectedDurationByTaskId[task.Id]; ok {
   420  					waterfallRows[buildVariant].Builds[versionId].Tasks[i].ExpectedDuration = duration
   421  				}
   422  			}
   423  		}
   424  	}
   425  }
   426  
   427  // Helper function to fetch a group of versions and their associated builds.
   428  // Returns the versions themselves, as well as a map of version id -> the
   429  // builds that are a part of the version (unsorted).
   430  func fetchVersionsAndAssociatedBuilds(project *model.Project, skip int, numVersions int) ([]version.Version, map[string][]build.Build, error) {
   431  
   432  	// fetch the versions from the db
   433  	versionsFromDB, err := version.Find(version.ByProjectId(project.Identifier).
   434  		WithFields(
   435  			version.RevisionKey,
   436  			version.ErrorsKey,
   437  			version.WarningsKey,
   438  			version.IgnoredKey,
   439  			version.MessageKey,
   440  			version.AuthorKey,
   441  			version.RevisionOrderNumberKey,
   442  			version.CreateTimeKey,
   443  		).Sort([]string{"-" + version.RevisionOrderNumberKey}).Skip(skip).Limit(numVersions))
   444  
   445  	if err != nil {
   446  		return nil, nil, errors.Wrap(err, "error fetching versions from database")
   447  	}
   448  
   449  	// create a slice of the version ids (used to fetch the builds)
   450  	versionIds := make([]string, 0, len(versionsFromDB))
   451  	for _, v := range versionsFromDB {
   452  		versionIds = append(versionIds, v.Id)
   453  	}
   454  
   455  	// fetch all of the builds (with only relevant fields)
   456  	buildsFromDb, err := build.Find(
   457  		build.ByVersions(versionIds).
   458  			WithFields(build.BuildVariantKey, build.TasksKey, build.VersionKey))
   459  	if err != nil {
   460  		return nil, nil, errors.Wrap(err, "error fetching builds from database")
   461  	}
   462  
   463  	// group the builds by version
   464  	buildsByVersion := map[string][]build.Build{}
   465  	for _, build := range buildsFromDb {
   466  		buildsByVersion[build.Version] = append(buildsByVersion[build.Version], build)
   467  	}
   468  
   469  	return versionsFromDB, buildsByVersion, nil
   470  }
   471  
   472  // Takes in a slice of tasks, and determines whether any of the tasks in
   473  // any of the builds are active.
   474  func anyActiveTasks(builds []build.Build) bool {
   475  	for _, build := range builds {
   476  		for _, task := range build.Tasks {
   477  			if task.Activated {
   478  				return true
   479  			}
   480  		}
   481  	}
   482  	return false
   483  }
   484  
   485  // Calculates how many actual versions would appear on the previous page, given
   486  // the starting skip for the current page as well as the number of version
   487  // elements per page (including elements containing rolled-up versions).
   488  func countOnPreviousPage(skip int, numVersionElements int,
   489  	project *model.Project) (int, error) {
   490  
   491  	// if there is no previous page
   492  	if skip == 0 {
   493  		return 0, nil
   494  	}
   495  
   496  	// the initial number of versions to be fetched per iteration
   497  	toFetch := numVersionElements
   498  
   499  	// the initial number of elements to step back from the current point
   500  	// (capped to 0)
   501  	stepBack := skip - numVersionElements
   502  	if stepBack < 0 {
   503  		toFetch = skip // only fetch up to the current point
   504  		stepBack = 0
   505  	}
   506  
   507  	// bookkeeping: the number of version elements represented so far, as well
   508  	// as the total number of versions fetched
   509  	elementsCreated := 0
   510  	versionsFetched := 0
   511  	// bookkeeping: whether the previous version was active
   512  	prevActive := true
   513  
   514  	for {
   515  
   516  		// fetch the versions and builds
   517  		versionsFromDB, buildsByVersion, err :=
   518  			fetchVersionsAndAssociatedBuilds(project, stepBack, toFetch)
   519  
   520  		if err != nil {
   521  			return 0, errors.Wrap(err, "error fetching versions and builds")
   522  		}
   523  
   524  		// for each of the versions fetched (iterating backwards), calculate
   525  		// how much it contributes to the version elements that would be
   526  		// created
   527  		for i := len(versionsFromDB) - 1; i >= 0; i-- {
   528  
   529  			// increment the versions we've fetched
   530  			versionsFetched += 1
   531  			// if there are any active tasks
   532  			if anyActiveTasks(buildsByVersion[versionsFromDB[i].Id]) {
   533  
   534  				// we may have stepped one over where the versions end, if
   535  				// the last was inactive
   536  				if elementsCreated == numVersionElements {
   537  					return versionsFetched - 1, nil
   538  				}
   539  
   540  				// the active version would get its own element
   541  				elementsCreated += 1
   542  				prevActive = true
   543  
   544  				// see if it's the last
   545  				if elementsCreated == numVersionElements {
   546  					return versionsFetched, nil
   547  				}
   548  			} else if prevActive {
   549  
   550  				// only record a rolled-up version when we hit the first version
   551  				// in it (walking backwards)
   552  				elementsCreated += 1
   553  				prevActive = false
   554  			}
   555  
   556  		}
   557  
   558  		// if we've hit the most recent versions (can't step back farther)
   559  		if stepBack == 0 {
   560  			return versionsFetched, nil
   561  		}
   562  
   563  		// recalculate where to skip to and how many to fetch
   564  		stepBack -= numVersionElements
   565  		if stepBack < 0 {
   566  			toFetch = stepBack + numVersionElements
   567  			stepBack = 0
   568  		}
   569  
   570  	}
   571  }
   572  
   573  // Create and return the waterfall data we need to render the page.
   574  // Http handler for the waterfall page
   575  func (uis *UIServer) waterfallPage(w http.ResponseWriter, r *http.Request) {
   576  	projCtx := MustHaveProjectContext(r)
   577  	if projCtx.Project == nil {
   578  		uis.ProjectNotFound(projCtx, w, r)
   579  		return
   580  	}
   581  
   582  	skip, err := skipValue(r)
   583  	if err != nil {
   584  		skip = 0
   585  	}
   586  
   587  	finalData := waterfallData{}
   588  
   589  	// first, get all of the versions and variants we will need
   590  	vvData, err := getVersionsAndVariants(skip,
   591  		VersionItemsToCreate, projCtx.Project)
   592  
   593  	if err != nil {
   594  		uis.LoggedError(w, r, http.StatusInternalServerError, err)
   595  		return
   596  	}
   597  	var wfv waterfallVersions = vvData.Versions
   598  
   599  	sort.Sort(wfv)
   600  	finalData.Versions = wfv
   601  
   602  	sort.Sort(vvData.BuildVariants)
   603  	rows := []waterfallRow{}
   604  	for _, bv := range vvData.BuildVariants {
   605  		rows = append(rows, vvData.Rows[bv.Id])
   606  	}
   607  	finalData.Rows = rows
   608  
   609  	// compute the total number of versions that exist
   610  	finalData.TotalVersions, err = version.Count(version.ByProjectId(projCtx.Project.Identifier))
   611  	if err != nil {
   612  		uis.LoggedError(w, r, http.StatusInternalServerError, err)
   613  		return
   614  	}
   615  
   616  	// compute the number of versions on the previous page
   617  	finalData.PreviousPageCount, err = countOnPreviousPage(skip, VersionItemsToCreate, projCtx.Project)
   618  	if err != nil {
   619  		uis.LoggedError(w, r, http.StatusInternalServerError, err)
   620  		return
   621  	}
   622  
   623  	// add in the skip value
   624  	finalData.CurrentSkip = skip
   625  
   626  	// pass it the current time
   627  	finalData.CurrentTime = time.Now().UnixNano()
   628  
   629  	uis.WriteHTML(w, http.StatusOK, struct {
   630  		ProjectData projectContext
   631  		User        *user.DBUser
   632  		Data        waterfallData
   633  		JiraHost    string
   634  	}{projCtx, GetUser(r), finalData, uis.Settings.Jira.Host}, "base", "waterfall.html", "base_angular.html", "menu.html")
   635  }