blob: bd50697bc272c1e55614193b8f549c22d6543eb0 [file] [log] [blame]
class TestFreshnessPage extends PageWithHeading {
constructor(summaryPageConfiguration, testAgeToleranceInHours)
{
super('test-freshness', null);
this._testAgeTolerance = (testAgeToleranceInHours || 24) * 3600 * 1000;
this._timeDuration = this._testAgeTolerance * 2;
this._excludedConfigurations = {};
this._lastDataPointByConfiguration = null;
this._indicatorByConfiguration = null;
this._renderTableLazily = new LazilyEvaluatedFunction(this._renderTable.bind(this));
this._loadConfig(summaryPageConfiguration);
}
name() { return 'Test-Freshness'; }
_loadConfig(summaryPageConfiguration)
{
const platformIdSet = new Set;
const metricIdSet = new Set;
for (const config of summaryPageConfiguration) {
for (const platformGroup of config.platformGroups) {
for (const platformId of platformGroup.platforms)
platformIdSet.add(platformId);
}
for (const metricGroup of config.metricGroups) {
for (const subgroup of metricGroup.subgroups) {
for (const metricId of subgroup.metrics)
metricIdSet.add(metricId);
}
}
const excludedConfigs = config.excludedConfigurations;
for (const platform in excludedConfigs) {
if (platform in this._excludedConfigurations)
this._excludedConfigurations[platform] = this._excludedConfigurations[platform].concat(excludedConfigs[platform]);
else
this._excludedConfigurations[platform] = excludedConfigs[platform];
}
}
this._platforms = [...platformIdSet].map((platformId) => Platform.findById(platformId));
this._metrics = [...metricIdSet].map((metricId) => Metric.findById(metricId));
}
open(state)
{
this._fetchTestResults();
super.open(state);
}
_fetchTestResults()
{
this._measurementSetFetchTime = Date.now();
this._lastDataPointByConfiguration = new Map;
const startTime = this._measurementSetFetchTime - this._timeDuration;
for (const platform of this._platforms) {
const lastDataPointByMetric = new Map;
this._lastDataPointByConfiguration.set(platform, lastDataPointByMetric);
for (const metric of this._metrics) {
if (!this._isValidPlatformMetricCombination(platform, metric, this._excludedConfigurations))
continue;
const measurementSet = MeasurementSet.findSet(platform.id(), metric.id(), platform.lastModified(metric));
measurementSet.fetchBetween(startTime, this._measurementSetFetchTime).then(() => {
const currentTimeSeries = measurementSet.fetchedTimeSeries('current', false, false);
let timeForLastDataPoint = startTime;
if (currentTimeSeries.lastPoint())
timeForLastDataPoint = currentTimeSeries.lastPoint().build().buildTime();
lastDataPointByMetric.set(metric, {time: timeForLastDataPoint, hasCurrentDataPoint: !!currentTimeSeries.lastPoint()});
this.enqueueToRender();
});
}
}
}
render()
{
super.render();
this._renderTableLazily.evaluate(this._platforms, this._metrics);
for (const [platform, lastDataPointByMetric] of this._lastDataPointByConfiguration.entries()) {
for (const [metric, lastDataPoint] of lastDataPointByMetric.entries()) {
const timeDuration = this._measurementSetFetchTime - lastDataPoint.time;
const timeDurationSummaryPrefix = lastDataPoint.hasCurrentDataPoint ? '' : 'More than ';
const timeDurationSummary = BuildRequest.formatTimeInterval(timeDuration);
const testLabel = `"${metric.test().fullName()}" for "${platform.name()}"`;
const summary = `${timeDurationSummaryPrefix}${timeDurationSummary} since last data point on ${testLabel}`;
const url = this._router.url('charts', ChartsPage.createStateForDashboardItem(platform.id(), metric.id(),
this._measurementSetFetchTime - this._timeDuration));
const indicator = this._indicatorByConfiguration.get(platform).get(metric);
indicator.update(timeDuration, this._testAgeTolerance, summary, url);
}
}
}
_renderTable(platforms, metrics)
{
const element = ComponentBase.createElement;
const tableBodyElement = [];
const tableHeadElements = [element('th', {class: 'table-corner'}, 'Platform \\ Test')];
for (const metric of metrics)
tableHeadElements.push(element('th', {class: 'diagonal-header'}, element('div', metric.test().fullName())));
this._indicatorByConfiguration = new Map;
for (const platform of platforms) {
const indicatorByMetric = new Map;
this._indicatorByConfiguration.set(platform, indicatorByMetric);
tableBodyElement.push(element('tr',
[element('th', platform.label()), ...metrics.map((metric) => this._constructTableCell(platform, metric, indicatorByMetric))]));
}
this.renderReplace(this.content('test-health'), [element('thead', tableHeadElements), element('tbody', tableBodyElement)]);
}
_isValidPlatformMetricCombination(platform, metric)
{
return !(this._excludedConfigurations && this._excludedConfigurations[platform.id()]
&& this._excludedConfigurations[platform.id()].some((metricId) => metricId == metric.id()))
&& platform.hasMetric(metric);
}
_constructTableCell(platform, metric, indicatorByMetric)
{
const element = ComponentBase.createElement;
if (!this._isValidPlatformMetricCombination(platform, metric))
return element('td', {class: 'blank-cell'}, element('div'));
const indicator = new FreshnessIndicator;
indicatorByMetric.set(metric, indicator);
return element('td', {class: 'status-cell'}, indicator);
}
static htmlTemplate()
{
return `<section class="page-with-heading"><table id="test-health"></table></section>`;
}
static cssTemplate()
{
return `
.page-with-heading {
display: flex;
justify-content: center;
}
#test-health {
font-size: 1rem;
}
#test-health th.table-corner {
text-align: right;
vertical-align: bottom;
}
#test-health th {
text-align: left;
border-bottom: 0.1rem solid #ccc;
font-weight: normal;
}
#test-health th.diagonal-header {
white-space: nowrap;
height: 16rem;
border-bottom: 0rem;
}
#test-health th.diagonal-header > div {
transform: translate(1rem, 7rem) rotate(315deg);
width: 2rem;
border: 0rem;
}
#test-health td.status-cell {
margin: 0;
padding: 0;
max-width: 2.2rem;
max-height: 2.2rem;
min-width: 2.2rem;
min-height: 2.2rem;
}
#test-health td.blank-cell {
margin: 0;
padding: 0;
max-width: 2.2rem;
max-height: 2.2rem;
min-width: 2.2rem;
min-height: 2.2rem;
}
#test-health td.blank-cell > div {
background-color: #F9F9F9;
height: 1.6rem;
width: 1.6rem;
margin: 0.1rem;
padding: 0;
position: relative;
overflow: hidden;
}
#test-health td.blank-cell > div::before {
content: "";
position: absolute;
top: -1px;
left: -1px;
display: block;
width: 0px;
height: 0px;
border-right: calc(1.6rem + 1px) solid #ddd;
border-top: calc(1.6rem + 1px) solid transparent;
}
#test-health td.blank-cell > div::after {
content: "";
display: block;
position: absolute;
top: 1px;
left: 1px;
width: 0px;
height: 0px;
border-right: calc(1.6rem - 1px) solid #F9F9F9;
border-top: calc(1.6rem - 1px) solid transparent;
}
`;
}
routeName() { return 'test-freshness'; }
}