зеркало из https://github.com/mozilla/treeherder.git
Bug 1506671 - Convert perf compareChooser view to react (#4297)
Remove compareChooser angular partial and controller and create react components
This commit is contained in:
Родитель
b002fdede3
Коммит
a0482c50e7
|
@ -135,8 +135,8 @@ module.exports = {
|
|||
// to help prevent unknowingly regressing the bundle size (bug 1384255).
|
||||
neutrino.config.performance
|
||||
.hints('error')
|
||||
.maxAssetSize(1.3 * 1024 * 1024)
|
||||
.maxEntrypointSize(1.64 * 1024 * 1024);
|
||||
.maxAssetSize(2 * 1024 * 1024)
|
||||
.maxEntrypointSize(1.71 * 1024 * 1024);
|
||||
}
|
||||
},
|
||||
],
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
"ajv": "6.5.5",
|
||||
"angular": "1.7.5",
|
||||
"angular-clipboard": "1.6.2",
|
||||
"angular-local-storage": "0.7.1",
|
||||
"angular1-ui-bootstrap4": "2.4.22",
|
||||
"auth0-js": "9.8.2",
|
||||
"bootstrap": "4.1.3",
|
||||
|
|
|
@ -78,10 +78,6 @@ input {
|
|||
font-size: 14px;
|
||||
}
|
||||
|
||||
.hide {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.tooltip > .tooltip-inner {
|
||||
background-color: lightgray;
|
||||
color: black;
|
||||
|
|
|
@ -46,6 +46,12 @@ a {
|
|||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* this is better for drop down menu items because
|
||||
display: none will change text alignment */
|
||||
.hide {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* Similar Jobs panel */
|
||||
.checkbox {
|
||||
min-height: 20px;
|
||||
|
|
|
@ -31,3 +31,4 @@ import './js/controllers/perf/alerts';
|
|||
import './js/components/perf/compare';
|
||||
import './js/components/loading';
|
||||
import './js/perfapp';
|
||||
import './perfherder/CompareSelectorView';
|
||||
|
|
|
@ -254,10 +254,6 @@ export const thEvents = {
|
|||
applyNewJobs: 'apply-new-jobs-EVT',
|
||||
};
|
||||
|
||||
export const phCompareDefaultOriginalRepo = 'mozilla-central';
|
||||
|
||||
export const phCompareDefaultNewRepo = 'try';
|
||||
|
||||
export const phTimeRanges = [
|
||||
{ value: 86400, text: 'Last day' },
|
||||
{ value: 86400 * 2, text: 'Last 2 days' },
|
||||
|
@ -306,8 +302,12 @@ export const phAlertStatusMap = {
|
|||
CONFIRMING: { id: 5, text: 'confirming' },
|
||||
};
|
||||
|
||||
export const phCompareBaseLineDefaultTimeRange = 86400 * 2;
|
||||
export const compareDefaultTimeRange = 86400 * 2;
|
||||
|
||||
export const thBugSuggestionLimit = 20;
|
||||
|
||||
export const thMaxPushFetchSize = 100;
|
||||
|
||||
export const errorMessageClass = 'text-danger py-4 d-block text-center';
|
||||
|
||||
export const genericErrorMessage = 'Something went wrong';
|
||||
|
|
|
@ -43,3 +43,12 @@ export const formatTaskclusterError = function formatTaskclusterError(e) {
|
|||
|
||||
return `${TC_ERROR_PREFIX}${errorMessage}`;
|
||||
};
|
||||
|
||||
export const processErrorMessage = function processErrorMessage(error, status) {
|
||||
if (status === 503) {
|
||||
return 'There was a problem retrieving the data. Please try again in a minute.';
|
||||
}
|
||||
|
||||
const key = Object.keys(error);
|
||||
return `${key}: ${error[key]}`;
|
||||
};
|
||||
|
|
|
@ -10,6 +10,20 @@ export const dxrBaseUrl = 'https://dxr.mozilla.org/';
|
|||
|
||||
export const tcRootUrl = 'https://taskcluster.net';
|
||||
|
||||
export const bugsEndpoint = 'failures/';
|
||||
|
||||
export const bugDetailsEndpoint = 'failuresbybug/';
|
||||
|
||||
export const graphsEndpoint = 'failurecount/';
|
||||
|
||||
export const deployedRevisionUrl = '/revision.txt';
|
||||
|
||||
export const loginCallbackUrl = '/login.html';
|
||||
|
||||
export const pushEndpoint = '/resultset/';
|
||||
|
||||
export const repoEndpoint = '/repository/';
|
||||
|
||||
export const getUserSessionUrl = function getUserSessionUrl(oidcProvider) {
|
||||
return `https://login.taskcluster.net/v1/oidc-credentials/${oidcProvider}`;
|
||||
};
|
||||
|
@ -88,12 +102,6 @@ export const getCompareChooserUrl = function getCompareChooserUrl(params) {
|
|||
return `perf.html#/comparechooser${createQueryParams(params)}`;
|
||||
};
|
||||
|
||||
export const bugsEndpoint = 'failures/';
|
||||
|
||||
export const bugDetailsEndpoint = 'failuresbybug/';
|
||||
|
||||
export const graphsEndpoint = 'failurecount/';
|
||||
|
||||
export const parseQueryParams = function parseQueryParams(search) {
|
||||
const params = new URLSearchParams(search);
|
||||
|
||||
|
@ -115,10 +123,6 @@ export const bugzillaBugsApi = function bugzillaBugsApi(api, params) {
|
|||
return `${bzBaseUrl}rest/${api}${query}`;
|
||||
};
|
||||
|
||||
export const deployedRevisionUrl = '/revision.txt';
|
||||
|
||||
export const loginCallbackUrl = '/login.html';
|
||||
|
||||
export const getRepoUrl = function getRepoUrl(newRepoName) {
|
||||
const params = getAllUrlParams();
|
||||
|
||||
|
|
|
@ -68,6 +68,7 @@ export default class DateOptions extends React.Component {
|
|||
<DropdownMenuItems
|
||||
options={dateOptions}
|
||||
updateData={this.updateDateRange}
|
||||
selectedItem={dateRange}
|
||||
/>
|
||||
</ButtonDropdown>
|
||||
{dateRange === 'custom range' && (
|
||||
|
|
|
@ -4,11 +4,11 @@ import PropTypes from 'prop-types';
|
|||
import Icon from 'react-fontawesome';
|
||||
|
||||
import ErrorBoundary from '../shared/ErrorBoundary';
|
||||
import ErrorMessages from '../shared/ErrorMessages';
|
||||
import { genericErrorMessage, errorMessageClass } from '../helpers/constants';
|
||||
|
||||
import Navigation from './Navigation';
|
||||
import GraphsContainer from './GraphsContainer';
|
||||
import ErrorMessages from './ErrorMessages';
|
||||
import { prettyErrorMessages, errorMessageClass } from './constants';
|
||||
|
||||
const Layout = props => {
|
||||
const {
|
||||
|
@ -62,7 +62,7 @@ const Layout = props => {
|
|||
{header}
|
||||
<ErrorBoundary
|
||||
errorClasses={errorMessageClass}
|
||||
message={prettyErrorMessages.default}
|
||||
message={genericErrorMessage}
|
||||
>
|
||||
{graphOneData && graphTwoData && (
|
||||
<GraphsContainer
|
||||
|
@ -76,7 +76,7 @@ const Layout = props => {
|
|||
|
||||
<ErrorBoundary
|
||||
errorClasses={errorMessageClass}
|
||||
message={prettyErrorMessages.default}
|
||||
message={genericErrorMessage}
|
||||
>
|
||||
{table}
|
||||
</ErrorBoundary>
|
||||
|
|
|
@ -43,19 +43,3 @@ export const treeOptions = [
|
|||
'comm-esr60',
|
||||
'comm-releases',
|
||||
];
|
||||
|
||||
// we only want bug_ui and tree_ui to be used for UI validation, because
|
||||
// if there is a valid type used but its a non-existent repo or bug_id
|
||||
// we want to see that message from the api response
|
||||
export const prettyErrorMessages = {
|
||||
startday: 'startday is required and must be in YYYY-MM-DD format.',
|
||||
endday: 'endday is required and must be in YYYY-MM-DD format.',
|
||||
bug_ui: 'bug is required and must be a valid integer.',
|
||||
tree_ui:
|
||||
'tree is required and must be a valid repository or repository group.',
|
||||
default: 'Something went wrong.',
|
||||
status503:
|
||||
'There was a problem retrieving the data. Please try again in a minute.',
|
||||
};
|
||||
|
||||
export const errorMessageClass = 'text-danger py-4 d-block';
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import moment from 'moment';
|
||||
|
||||
import { prettyErrorMessages } from './constants';
|
||||
|
||||
// be sure to wrap date arg in a moment()
|
||||
export const ISODate = function formatISODate(date) {
|
||||
return date.format('YYYY-MM-DD');
|
||||
|
@ -92,28 +90,6 @@ export const sortData = function sortData(data, sortBy, desc) {
|
|||
return data;
|
||||
};
|
||||
|
||||
export const processErrorMessage = function processErrorMessage(
|
||||
errorMessage,
|
||||
status,
|
||||
) {
|
||||
const messages = [];
|
||||
|
||||
if (status === 503) {
|
||||
return [prettyErrorMessages.status503];
|
||||
}
|
||||
|
||||
if (Object.keys(errorMessage).length > 0) {
|
||||
for (const [key, value] of Object.entries(errorMessage)) {
|
||||
if (prettyErrorMessages[key]) {
|
||||
messages.push(prettyErrorMessages[key]);
|
||||
} else {
|
||||
messages.push(`${key}: ${value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return messages || [prettyErrorMessages.default];
|
||||
};
|
||||
|
||||
export const validateQueryParams = function validateQueryParams(
|
||||
params,
|
||||
bugRequired = false,
|
||||
|
@ -122,16 +98,18 @@ export const validateQueryParams = function validateQueryParams(
|
|||
const dateFormat = /\d{4}[-]\d{2}[-]\d{2}/;
|
||||
|
||||
if (!params.tree) {
|
||||
messages.push(prettyErrorMessages.tree_ui);
|
||||
messages.push(
|
||||
'tree is required and must be a valid repository or repository group.',
|
||||
);
|
||||
}
|
||||
if (!params.startday || params.startday.search(dateFormat) === -1) {
|
||||
messages.push(prettyErrorMessages.startday);
|
||||
messages.push('startday is required and must be in YYYY-MM-DD format.');
|
||||
}
|
||||
if (!params.endday || params.endday.search(dateFormat) === -1) {
|
||||
messages.push(prettyErrorMessages.endday);
|
||||
messages.push('endday is required and must be in YYYY-MM-DD format.');
|
||||
}
|
||||
if (bugRequired && (!params.bug || Number.isNaN(params.bug))) {
|
||||
messages.push(prettyErrorMessages.bug_ui);
|
||||
messages.push('bug is required and must be a valid integer.');
|
||||
}
|
||||
return messages;
|
||||
};
|
||||
|
|
|
@ -7,10 +7,8 @@ import metricsgraphics from 'metrics-graphics';
|
|||
import perf from '../../perf';
|
||||
import { endpoints } from '../../../perfherder/constants';
|
||||
import {
|
||||
phCompareDefaultOriginalRepo,
|
||||
phCompareDefaultNewRepo,
|
||||
phTimeRanges,
|
||||
phCompareBaseLineDefaultTimeRange,
|
||||
compareDefaultTimeRange,
|
||||
} from '../../../helpers/constants';
|
||||
import PushModel from '../../../models/push';
|
||||
import RepositoryModel from '../../../models/repository';
|
||||
|
@ -20,126 +18,6 @@ import { getCounterMap, getInterval, validateQueryParams, getResultsMap,
|
|||
import { getApiUrl } from '../../../helpers/url';
|
||||
import { getData } from '../../../helpers/http';
|
||||
|
||||
perf.controller('CompareChooserCtrl', [
|
||||
'$state', '$stateParams', '$scope', '$q',
|
||||
'localStorageService',
|
||||
function CompareChooserCtrl($state, $stateParams, $scope, $q,
|
||||
localStorageService) {
|
||||
RepositoryModel.getList().then((projects) => {
|
||||
$scope.projects = projects;
|
||||
$scope.originalTipList = [];
|
||||
$scope.newTipList = [];
|
||||
$scope.revisionComparison = false;
|
||||
|
||||
const getParameter = function (paramName, defaultValue) {
|
||||
if ($stateParams[paramName]) {
|
||||
return $stateParams[paramName];
|
||||
}
|
||||
|
||||
if (localStorageService.get(paramName)) {
|
||||
return localStorageService.get(paramName);
|
||||
}
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
$scope.originalProject = projects.find(project =>
|
||||
project.name === getParameter('originalProject', phCompareDefaultOriginalRepo),
|
||||
) || projects[0];
|
||||
$scope.newProject = projects.find(project =>
|
||||
project.name === getParameter('newProject', phCompareDefaultNewRepo),
|
||||
) || projects[0];
|
||||
|
||||
$scope.originalRevision = getParameter('originalRevision', '');
|
||||
$scope.newRevision = getParameter('newRevision', '');
|
||||
|
||||
const getRevisionTips = function (projectName, list) {
|
||||
// due to we push the revision data into list,
|
||||
// so we need clear the data before we push new data into it.
|
||||
list.splice(0, list.length);
|
||||
PushModel.getList({ repo: projectName }).then(async (response) => {
|
||||
const { results } = await response.json();
|
||||
|
||||
results.forEach(function (revisionSet) {
|
||||
list.push({
|
||||
revision: revisionSet.revision,
|
||||
author: revisionSet.author,
|
||||
});
|
||||
});
|
||||
$scope.$apply();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.updateOriginalRevisionTips = function () {
|
||||
getRevisionTips($scope.originalProject.name, $scope.originalTipList);
|
||||
};
|
||||
$scope.updateNewRevisionTips = function () {
|
||||
getRevisionTips($scope.newProject.name, $scope.newTipList);
|
||||
};
|
||||
$scope.updateOriginalRevisionTips();
|
||||
$scope.updateNewRevisionTips();
|
||||
|
||||
$scope.getOriginalTipRevision = function (tip) {
|
||||
$scope.originalRevision = tip;
|
||||
};
|
||||
|
||||
$scope.getNewTipRevision = function (tip) {
|
||||
$scope.newRevision = tip;
|
||||
};
|
||||
|
||||
$scope.runCompare = function () {
|
||||
const revisionPromises = [];
|
||||
if ($scope.revisionComparison) {
|
||||
revisionPromises.push(PushModel.getList({
|
||||
repo: $scope.originalProject.name,
|
||||
revision: $scope.originalRevision,
|
||||
}).then((resp) => {
|
||||
if (resp.ok) {
|
||||
$scope.originalRevisionError = undefined;
|
||||
} else {
|
||||
$scope.originalRevisionError = resp.statusText;
|
||||
}
|
||||
$scope.$apply();
|
||||
}));
|
||||
}
|
||||
|
||||
revisionPromises.push(PushModel.getList({
|
||||
repo: $scope.newProject.name,
|
||||
revision: $scope.newRevision,
|
||||
}).then((resp) => {
|
||||
if (resp.ok) {
|
||||
$scope.newRevisionError = undefined;
|
||||
} else {
|
||||
$scope.newRevisionError = resp.statusText;
|
||||
}
|
||||
$scope.$apply();
|
||||
}));
|
||||
|
||||
$q.all(revisionPromises).then(function () {
|
||||
localStorageService.set('originalProject', $scope.originalProject.name, 'sessionStorage');
|
||||
localStorageService.set('originalRevision', $scope.originalRevision, 'sessionStorage');
|
||||
localStorageService.set('newProject', $scope.newProject.name, 'sessionStorage');
|
||||
localStorageService.set('newRevision', $scope.newRevision, 'sessionStorage');
|
||||
if ($scope.originalRevisionError === undefined && $scope.newRevisionError === undefined) {
|
||||
if ($scope.revisionComparison) {
|
||||
$state.go('compare', {
|
||||
originalProject: $scope.originalProject.name,
|
||||
originalRevision: $scope.originalRevision,
|
||||
newProject: $scope.newProject.name,
|
||||
newRevision: $scope.newRevision,
|
||||
});
|
||||
} else {
|
||||
$state.go('compare', {
|
||||
originalProject: $scope.originalProject.name,
|
||||
newProject: $scope.newProject.name,
|
||||
newRevision: $scope.newRevision,
|
||||
selectedTimeRange: phCompareBaseLineDefaultTimeRange,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
}]);
|
||||
|
||||
perf.controller('CompareResultsCtrl', [
|
||||
'$state', '$stateParams', '$scope',
|
||||
|
@ -484,7 +362,7 @@ perf.controller('CompareResultsCtrl', [
|
|||
} else {
|
||||
$scope.timeRanges = phTimeRanges;
|
||||
$scope.selectedTimeRange = $scope.timeRanges.find(timeRange =>
|
||||
timeRange.value === ($stateParams.selectedTimeRange ? parseInt($stateParams.selectedTimeRange) : phCompareBaseLineDefaultTimeRange),
|
||||
timeRange.value === ($stateParams.selectedTimeRange ? parseInt($stateParams.selectedTimeRange) : compareDefaultTimeRange),
|
||||
);
|
||||
}
|
||||
$q.all(verifyPromises).then(function () {
|
||||
|
@ -676,7 +554,7 @@ perf.controller('CompareSubtestResultsCtrl', [
|
|||
} else {
|
||||
$scope.timeRanges = phTimeRanges;
|
||||
$scope.selectedTimeRange = $scope.timeRanges.find(timeRange =>
|
||||
timeRange.value === ($stateParams.selectedTimeRange ? parseInt($stateParams.selectedTimeRange) : phCompareBaseLineDefaultTimeRange),
|
||||
timeRange.value === ($stateParams.selectedTimeRange ? parseInt($stateParams.selectedTimeRange) : compareDefaultTimeRange),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ import angularClipboardModule from 'angular-clipboard';
|
|||
import uiBootstrap from 'angular1-ui-bootstrap4';
|
||||
import uiRouter from '@uirouter/angularjs';
|
||||
import 'ng-text-truncate-2';
|
||||
import LocalStorageModule from 'angular-local-storage';
|
||||
import { react2angular } from 'react2angular/index.es2015';
|
||||
|
||||
import Login from '../shared/auth/Login';
|
||||
|
@ -16,7 +15,6 @@ const perf = angular.module('perf', [
|
|||
treeherderModule.name,
|
||||
angularClipboardModule.name,
|
||||
'ngTextTruncate',
|
||||
LocalStorageModule,
|
||||
]);
|
||||
|
||||
perf.component('login', react2angular(Login, ['user', 'setUser'], []));
|
||||
|
|
|
@ -57,7 +57,6 @@ perf.config(['$compileProvider', '$locationProvider', '$httpProvider', '$statePr
|
|||
title: 'Compare Chooser',
|
||||
template: compareChooserCtrlTemplate,
|
||||
url: '/comparechooser?originalProject&originalRevision&newProject&newRevision',
|
||||
controller: 'CompareChooserCtrl',
|
||||
})
|
||||
.state('comparesubtestdistribution', {
|
||||
title: 'Compare Subtest Distribution',
|
||||
|
|
|
@ -3,13 +3,11 @@ import { slugid } from 'taskcluster-client-web';
|
|||
import { thMaxPushFetchSize } from '../helpers/constants';
|
||||
import { getUrlParam } from '../helpers/location';
|
||||
import taskcluster from '../helpers/taskcluster';
|
||||
import { createQueryParams, getProjectUrl } from '../helpers/url';
|
||||
import { createQueryParams, getProjectUrl, pushEndpoint } from '../helpers/url';
|
||||
|
||||
import JobModel from './job';
|
||||
import TaskclusterModel from './taskcluster';
|
||||
|
||||
const uri_base = '/resultset/';
|
||||
|
||||
const convertDates = function convertDates(locationParams) {
|
||||
// support date ranges. we must convert the strings to a timezone
|
||||
// appropriate timestamp
|
||||
|
@ -53,13 +51,13 @@ export default class PushModel {
|
|||
params.count = thMaxPushFetchSize;
|
||||
}
|
||||
return fetch(
|
||||
`${getProjectUrl(uri_base, repoName)}${createQueryParams(params)}`,
|
||||
`${getProjectUrl(pushEndpoint, repoName)}${createQueryParams(params)}`,
|
||||
);
|
||||
}
|
||||
|
||||
static get(pk, options = {}) {
|
||||
const repoName = options.repo || getUrlParam('repo');
|
||||
return fetch(getProjectUrl(`${uri_base}${pk}/`, repoName));
|
||||
return fetch(getProjectUrl(`${pushEndpoint}${pk}/`, repoName));
|
||||
}
|
||||
|
||||
static getJobs(pushIds, options = {}) {
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import { getApiUrl } from '../helpers/url';
|
||||
|
||||
const uri = getApiUrl('/repository/');
|
||||
import { getApiUrl, repoEndpoint } from '../helpers/url';
|
||||
|
||||
export default class RepositoryModel {
|
||||
constructor(props) {
|
||||
|
@ -20,7 +18,7 @@ export default class RepositoryModel {
|
|||
}
|
||||
|
||||
static getList() {
|
||||
return fetch(uri)
|
||||
return fetch(getApiUrl(repoEndpoint))
|
||||
.then(resp => resp.json())
|
||||
.then(repos => repos.map(datum => new RepositoryModel(datum)));
|
||||
}
|
||||
|
|
|
@ -1,71 +1 @@
|
|||
<div class="container-fluid vertical-box">
|
||||
<div class="spacer"></div>
|
||||
<form class="compare-form centered-element">
|
||||
<div class="spacer"></div>
|
||||
<div class="form-group">
|
||||
<div class="spacer"></div>
|
||||
<div class="card centered-element">
|
||||
<div class="card-header">Base</div>
|
||||
<div class="card-body">
|
||||
<label for="original-project-selector">Project</label>
|
||||
<select id="original-project-selector" class="form-control" ng-model="originalProject" ng-options="project.name for project in projects" ng-change="updateOriginalRevisionTips()"/>
|
||||
<div class="checkbox">
|
||||
<label><input type="checkbox" ng-model="revisionComparison">Compare with a specific revision</label>
|
||||
</div>
|
||||
<div ng-show="!revisionComparison">
|
||||
<p class="help-block">By default, Perfherder will compare against performance data gathered over the last 2 days from when new revision was pushed</p>
|
||||
</div>
|
||||
<div ng-show="revisionComparison">
|
||||
<label for="original-revision-input">Revision</label>
|
||||
<div class="input-group" ng-class="{'has-danger': originalRevisionError}">
|
||||
<input id="original-revision-input" maxlength="40" class="form-control" type="text" ng-model="originalRevision" placeholder="Select or enter a revision"/>
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-light-bordered dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Recent</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" ng-model="tipRevision">
|
||||
<li ng-repeat="tip in originalTipList"><a ng-click="getOriginalTipRevision(tip.revision)" class="dropdown-item">{{tip.revision | limitTo: 12}} {{tip.author}}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="form-control-label" style="color:#A94442" ng-show="originalRevisionError">{{originalRevisionError}}"</label>
|
||||
</div>
|
||||
<div class="card centered-element">
|
||||
<div class="card-header">New</div>
|
||||
<div class="card-body">
|
||||
<label for="new-project-selector">Project</label>
|
||||
<select id="new-project-selector" class="form-control" ng-model="newProject" ng-options="project.name for project in projects" ng-change="updateNewRevisionTips()"></select>
|
||||
<label for="new-revision-input">Revision</label>
|
||||
<div class="input-group" ng-class="{'has-danger': newRevisionError}">
|
||||
<input id="new-revision-input" maxlength="40" class="form-control" type="text" ng-model="newRevision" placeholder="Select or enter a revision"/>
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-light-bordered dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Recent</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li ng-repeat="tip in newTipList"><a ng-click="getNewTipRevision(tip.revision);" class="dropdown-item">
|
||||
{{tip.revision | limitTo: 12}} {{tip.author}}
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<label class="form-control-label" style="color:#A94442" ng-show="newRevisionError">{{newRevisionError}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
</div>
|
||||
<div class="horizontal-box">
|
||||
<div class="spacer"></div>
|
||||
<div class="form-group button-container">
|
||||
<button type="submit" class="btn btn-primary-soft btn-lg btn-block"
|
||||
ng-click="runCompare()"
|
||||
ng-disabled="(originalRevision.length > 0 && originalRevision.length < 12) || newRevision.length < 12">
|
||||
Compare
|
||||
</button>
|
||||
<div class="spacer"></div>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="spacer"></div>
|
||||
</div>
|
||||
<compare-selector-view />
|
||||
|
|
|
@ -0,0 +1,244 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { react2angular } from 'react2angular/index.es2015';
|
||||
import { Container, Col, Row, Button } from 'reactstrap';
|
||||
|
||||
import perf from '../js/perf';
|
||||
import {
|
||||
getApiUrl,
|
||||
repoEndpoint,
|
||||
getProjectUrl,
|
||||
createQueryParams,
|
||||
pushEndpoint,
|
||||
} from '../helpers/url';
|
||||
import { getData } from '../helpers/http';
|
||||
import ErrorMessages from '../shared/ErrorMessages';
|
||||
import {
|
||||
compareDefaultTimeRange,
|
||||
genericErrorMessage,
|
||||
errorMessageClass,
|
||||
} from '../helpers/constants';
|
||||
import ErrorBoundary from '../shared/ErrorBoundary';
|
||||
|
||||
import SelectorCard from './SelectorCard';
|
||||
|
||||
// TODO remove $stateParams and $state after switching to react router
|
||||
export default class CompareSelectorView extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
projects: [],
|
||||
failureStatus: null,
|
||||
originalProject: 'mozilla-central',
|
||||
newProject: 'try',
|
||||
originalRevision: '',
|
||||
newRevision: '',
|
||||
errorMessages: [],
|
||||
disableButton: true,
|
||||
};
|
||||
this.submitData = this.submitData.bind(this);
|
||||
this.validateQueryParams = this.validateQueryParams.bind(this);
|
||||
this.validateProject = this.validateProject.bind(this);
|
||||
this.validateRevision = this.validateRevision.bind(this);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const { data, failureStatus } = await getData(getApiUrl(repoEndpoint));
|
||||
this.setState({ projects: data, failureStatus });
|
||||
this.validateQueryParams();
|
||||
}
|
||||
|
||||
validateQueryParams() {
|
||||
const {
|
||||
originalProject,
|
||||
newProject,
|
||||
originalRevision,
|
||||
newRevision,
|
||||
} = this.props.$stateParams;
|
||||
|
||||
if (originalProject) {
|
||||
this.validateProject('originalProject', originalProject);
|
||||
}
|
||||
|
||||
if (newProject) {
|
||||
this.validateProject('newProject', newProject);
|
||||
}
|
||||
|
||||
if (newRevision) {
|
||||
this.validateRevision(
|
||||
'newRevision',
|
||||
newRevision,
|
||||
newProject || this.state.newProject,
|
||||
);
|
||||
}
|
||||
|
||||
if (originalRevision) {
|
||||
this.validateRevision(
|
||||
'originalRevision',
|
||||
originalRevision,
|
||||
originalProject || this.state.originalProject,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
validateProject(projectName, project) {
|
||||
const { projects, errorMessages } = this.state;
|
||||
let updates = {};
|
||||
const validProject = projects.find(item => item.name === project);
|
||||
|
||||
if (validProject) {
|
||||
updates = { [projectName]: project };
|
||||
} else {
|
||||
updates = {
|
||||
errorMessages: [
|
||||
...errorMessages,
|
||||
`${projectName} must be a valid project.`,
|
||||
],
|
||||
};
|
||||
}
|
||||
this.setState(updates);
|
||||
}
|
||||
|
||||
async validateRevision(revisionName, revision, project) {
|
||||
const { errorMessages } = this.state;
|
||||
let updates = {};
|
||||
|
||||
const url = `${getProjectUrl(pushEndpoint, project)}${createQueryParams({
|
||||
revision,
|
||||
})}`;
|
||||
const { data, failureStatus } = await getData(url);
|
||||
|
||||
if (failureStatus || data.meta.count === 0) {
|
||||
updates = {
|
||||
errorMessages: [
|
||||
...errorMessages,
|
||||
`${revisionName} must be a valid revision.`,
|
||||
],
|
||||
};
|
||||
} else {
|
||||
updates = { [revisionName]: revision };
|
||||
}
|
||||
this.setState(updates);
|
||||
}
|
||||
|
||||
submitData() {
|
||||
const {
|
||||
originalProject,
|
||||
newProject,
|
||||
originalRevision,
|
||||
newRevision,
|
||||
} = this.state;
|
||||
const { $state } = this.props;
|
||||
|
||||
if (newRevision === '') {
|
||||
return this.setState({ errorMessages: ['New revision is required'] });
|
||||
}
|
||||
|
||||
if (originalRevision !== '') {
|
||||
$state.go('compare', {
|
||||
originalProject,
|
||||
originalRevision,
|
||||
newProject,
|
||||
newRevision,
|
||||
});
|
||||
} else {
|
||||
$state.go('compare', {
|
||||
originalProject,
|
||||
newProject,
|
||||
newRevision,
|
||||
selectedTimeRange: compareDefaultTimeRange,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
originalProject,
|
||||
newProject,
|
||||
projects,
|
||||
originalRevision,
|
||||
newRevision,
|
||||
data,
|
||||
failureStatus,
|
||||
errorMessages,
|
||||
disableButton,
|
||||
} = this.state;
|
||||
return (
|
||||
<Container
|
||||
fluid
|
||||
style={{ marginBottom: '5rem', marginTop: '5rem', maxWidth: '1200px' }}
|
||||
>
|
||||
<ErrorBoundary
|
||||
errorClasses={errorMessageClass}
|
||||
message={genericErrorMessage}
|
||||
>
|
||||
<div className="mx-auto">
|
||||
<Row className="justify-content-center">
|
||||
<Col sm="8" className="text-center">
|
||||
{(failureStatus || errorMessages.length > 0) && (
|
||||
<ErrorMessages
|
||||
failureMessage={data}
|
||||
failureStatus={failureStatus}
|
||||
errorMessages={errorMessages}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="justify-content-center">
|
||||
<SelectorCard
|
||||
projects={projects}
|
||||
updateState={updates => this.setState(updates)}
|
||||
selectedRepo={originalProject}
|
||||
title="Base"
|
||||
checkbox
|
||||
text="By default, Perfherder will compare against performance data gathered over the last 2 days from when new revision was pushed"
|
||||
projectState="originalProject"
|
||||
revisionState="originalRevision"
|
||||
selectedRevision={originalRevision}
|
||||
queryParam={this.props.$stateParams.originalRevision}
|
||||
/>
|
||||
<SelectorCard
|
||||
projects={projects}
|
||||
updateState={updates => this.setState(updates)}
|
||||
selectedRepo={newProject}
|
||||
title="New"
|
||||
projectState="newProject"
|
||||
revisionState="newRevision"
|
||||
selectedRevision={newRevision}
|
||||
/>
|
||||
</Row>
|
||||
<Row className="justify-content-center">
|
||||
<Col sm="8" className="text-right px-1">
|
||||
<Button
|
||||
color="info"
|
||||
className="mt-2 mx-auto"
|
||||
onClick={
|
||||
newRevision !== '' && disableButton ? '' : this.submitData
|
||||
}
|
||||
>
|
||||
Compare
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CompareSelectorView.propTypes = {
|
||||
$stateParams: PropTypes.shape({
|
||||
newRevision: PropTypes.string,
|
||||
originalRevision: PropTypes.string,
|
||||
newProject: PropTypes.string,
|
||||
originalProject: PropTypes.string,
|
||||
}).isRequired,
|
||||
$state: PropTypes.shape({}).isRequired,
|
||||
};
|
||||
|
||||
perf.component(
|
||||
'compareSelectorView',
|
||||
react2angular(CompareSelectorView, [], ['$stateParams', '$state']),
|
||||
);
|
|
@ -0,0 +1,284 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Icon from 'react-fontawesome';
|
||||
import {
|
||||
Col,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardText,
|
||||
CardBody,
|
||||
DropdownItem,
|
||||
Input,
|
||||
CardSubtitle,
|
||||
Label,
|
||||
FormGroup,
|
||||
ButtonDropdown,
|
||||
DropdownToggle,
|
||||
DropdownMenu,
|
||||
InputGroup,
|
||||
InputGroupButtonDropdown,
|
||||
} from 'reactstrap';
|
||||
|
||||
import { getProjectUrl, createQueryParams, pushEndpoint } from '../helpers/url';
|
||||
import { getData } from '../helpers/http';
|
||||
import { genericErrorMessage } from '../helpers/constants';
|
||||
|
||||
export default class SelectorCard extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
buttonDropdownOpen: false,
|
||||
inputDropdownOpen: false,
|
||||
checkboxSelected: this.props.queryParam,
|
||||
inputValue: '',
|
||||
data: {},
|
||||
failureStatus: null,
|
||||
invalidInput: false,
|
||||
};
|
||||
this.toggle = this.toggle.bind(this);
|
||||
this.fetchRevisions = this.fetchRevisions.bind(this);
|
||||
this.validateInput = this.validateInput.bind(this);
|
||||
this.compareRevisions = this.compareRevisions.bind(this);
|
||||
this.updateRevision = this.updateRevision.bind(this);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
// by default revisions are only needed for the 'New' component dropdown
|
||||
// so we'll fetch revisions for the 'Base' component only as needed
|
||||
if (this.props.revisionState === 'newRevision') {
|
||||
this.fetchRevisions(this.props.selectedRepo);
|
||||
}
|
||||
}
|
||||
|
||||
async fetchRevisions(selectedRepo) {
|
||||
const params = {
|
||||
full: true,
|
||||
count: 10,
|
||||
};
|
||||
const url = `${getProjectUrl(
|
||||
pushEndpoint,
|
||||
selectedRepo,
|
||||
)}${createQueryParams(params)}`;
|
||||
const { data, failureStatus } = await getData(url);
|
||||
|
||||
if (failureStatus) {
|
||||
this.props.updateState({ errorMessages: genericErrorMessage });
|
||||
} else {
|
||||
this.setState({ data, failureStatus });
|
||||
}
|
||||
}
|
||||
|
||||
toggle(dropdown) {
|
||||
this.setState({
|
||||
[dropdown]: !this.state[dropdown],
|
||||
});
|
||||
}
|
||||
|
||||
updateData(selectedRepo) {
|
||||
const { updateState, projectState } = this.props;
|
||||
this.fetchRevisions(selectedRepo);
|
||||
updateState({ [projectState]: selectedRepo });
|
||||
}
|
||||
|
||||
compareRevisions() {
|
||||
this.toggle('checkboxSelected');
|
||||
if (!this.state.data.results) {
|
||||
this.fetchRevisions(this.props.selectedRepo);
|
||||
}
|
||||
}
|
||||
|
||||
async validateInput(value) {
|
||||
const { updateState } = this.props;
|
||||
|
||||
if (value.length < 40 && value !== '') {
|
||||
return this.setState({
|
||||
invalidInput: 'Revision must be at least 40 characters',
|
||||
});
|
||||
}
|
||||
|
||||
if (value.length >= 40) {
|
||||
const url = `${getProjectUrl(
|
||||
pushEndpoint,
|
||||
this.props.selectedRepo,
|
||||
)}${createQueryParams({ revision: value })}`;
|
||||
const { data, failureStatus } = await getData(url);
|
||||
|
||||
if (failureStatus || data.meta.count === 0) {
|
||||
return this.setState({ invalidInput: 'Invalid revision' });
|
||||
}
|
||||
updateState({ disableButton: false });
|
||||
}
|
||||
// reset if value is valid but previous value wasn't
|
||||
if (this.state.invalidInput) {
|
||||
this.setState({ invalidInput: false });
|
||||
}
|
||||
}
|
||||
|
||||
updateRevision(value) {
|
||||
const { updateState, revisionState } = this.props;
|
||||
|
||||
this.setState({ invalidInput: false });
|
||||
updateState({
|
||||
[revisionState]: value,
|
||||
errorMessages: [],
|
||||
disableButton: false,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
buttonDropdownOpen,
|
||||
inputDropdownOpen,
|
||||
checkboxSelected,
|
||||
data,
|
||||
invalidInput,
|
||||
} = this.state;
|
||||
const {
|
||||
selectedRepo,
|
||||
updateState,
|
||||
projects,
|
||||
title,
|
||||
text,
|
||||
checkbox,
|
||||
selectedRevision,
|
||||
revisionState,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Col sm="4" className="p-2">
|
||||
<Card style={{ height: '250px' }}>
|
||||
<CardHeader style={{ backgroundColor: 'lightgrey' }}>
|
||||
{title}
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<CardSubtitle className="pb-2 pt-3">Project</CardSubtitle>
|
||||
<ButtonDropdown
|
||||
className="mr-3 w-25"
|
||||
isOpen={buttonDropdownOpen}
|
||||
toggle={() => this.toggle('buttonDropdownOpen')}
|
||||
>
|
||||
<DropdownToggle caret outline>
|
||||
{selectedRepo}
|
||||
</DropdownToggle>
|
||||
{projects.length > 0 && (
|
||||
<DropdownMenu
|
||||
style={{
|
||||
overflow: 'auto',
|
||||
maxHeight: 300,
|
||||
}}
|
||||
>
|
||||
{projects.map(item => (
|
||||
<DropdownItem
|
||||
key={item.name}
|
||||
onClick={event => this.updateData(event.target.innerText)}
|
||||
>
|
||||
<Icon
|
||||
name="check"
|
||||
className={`pr-1 ${
|
||||
selectedRepo === item.name ? '' : 'hide'
|
||||
}`}
|
||||
/>
|
||||
{item.name}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</ButtonDropdown>
|
||||
|
||||
{checkbox && (
|
||||
<FormGroup check className="pt-1">
|
||||
<Label check className="font-weight-normal">
|
||||
<Input
|
||||
type="checkbox"
|
||||
defaultChecked={checkboxSelected}
|
||||
onClick={this.compareRevisions}
|
||||
/>{' '}
|
||||
Compare with a specific revision
|
||||
</Label>
|
||||
</FormGroup>
|
||||
)}
|
||||
|
||||
{!checkboxSelected && text ? (
|
||||
<CardText className="text-muted py-2">{text}</CardText>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<CardSubtitle className="pt-4 pb-2">Revision</CardSubtitle>
|
||||
<InputGroup>
|
||||
<Input
|
||||
placeholder="select or enter a revision"
|
||||
value={selectedRevision}
|
||||
onChange={event =>
|
||||
updateState({
|
||||
[revisionState]: event.target.value,
|
||||
errorMessages: [],
|
||||
disableButton: true,
|
||||
})
|
||||
}
|
||||
onBlur={event => this.validateInput(event.target.value)}
|
||||
onFocus={() => this.setState({ invalidInput: false })}
|
||||
/>
|
||||
<InputGroupButtonDropdown
|
||||
addonType="append"
|
||||
isOpen={inputDropdownOpen}
|
||||
toggle={() => this.toggle('inputDropdownOpen')}
|
||||
>
|
||||
<DropdownToggle caret outline>
|
||||
Recent
|
||||
</DropdownToggle>
|
||||
{!!data.results && data.results.length > 0 && (
|
||||
<DropdownMenu>
|
||||
{data.results.map(item => (
|
||||
<DropdownItem
|
||||
key={item.id}
|
||||
onClick={event =>
|
||||
this.updateRevision(
|
||||
event.target.innerText.split(' ')[0],
|
||||
)
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
name="check"
|
||||
className={`pr-1 ${
|
||||
selectedRevision === item.revision ? '' : 'hide'
|
||||
}`}
|
||||
/>
|
||||
{`${item.revision} ${item.author}`}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</InputGroupButtonDropdown>
|
||||
</InputGroup>
|
||||
{invalidInput && (
|
||||
<CardText className="text-danger py-2">
|
||||
{invalidInput}
|
||||
</CardText>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectorCard.propTypes = {
|
||||
projects: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
selectedRepo: PropTypes.string.isRequired,
|
||||
selectedRevision: PropTypes.string.isRequired,
|
||||
revisionState: PropTypes.string.isRequired,
|
||||
projectState: PropTypes.string.isRequired,
|
||||
updateState: PropTypes.func.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
text: PropTypes.string,
|
||||
checkbox: PropTypes.bool,
|
||||
queryParam: PropTypes.string,
|
||||
};
|
||||
|
||||
SelectorCard.defaultProps = {
|
||||
projects: [],
|
||||
text: null,
|
||||
checkbox: false,
|
||||
queryParam: undefined,
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import chunk from 'lodash/chunk';
|
||||
|
||||
import { getApiUrl, createQueryParams } from '../helpers/url';
|
||||
import { getApiUrl, createQueryParams, repoEndpoint } from '../helpers/url';
|
||||
import { getData } from '../helpers/http';
|
||||
import PerfSeriesModel from '../models/perfSeries';
|
||||
import { phTimeRanges } from '../helpers/constants';
|
||||
|
@ -278,7 +278,7 @@ export const validateQueryParams = async function validateQueryParams(params) {
|
|||
if (!newSignature) errors.push('Missing input: newSignature');
|
||||
}
|
||||
|
||||
const { data, failureStatus } = await getData(getApiUrl('/repository/'));
|
||||
const { data, failureStatus } = await getData(getApiUrl(repoEndpoint));
|
||||
|
||||
if (
|
||||
!failureStatus &&
|
||||
|
|
|
@ -2,12 +2,12 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { Alert } from 'reactstrap';
|
||||
|
||||
import { processErrorMessage } from './helpers';
|
||||
import { processErrorMessage } from '../helpers/errorMessage';
|
||||
|
||||
const ErrorMessages = ({ failureMessage, failureStatus, errorMessages }) => {
|
||||
const messages = errorMessages.length
|
||||
? errorMessages
|
||||
: processErrorMessage(failureMessage, failureStatus);
|
||||
: [processErrorMessage(failureMessage, failureStatus)];
|
||||
|
||||
return (
|
||||
<div>
|
|
@ -1111,11 +1111,6 @@ angular-clipboard@1.6.2:
|
|||
resolved "https://registry.yarnpkg.com/angular-clipboard/-/angular-clipboard-1.6.2.tgz#4708e5a1dc94f3940ab89861ea1e19b26754154f"
|
||||
integrity sha512-T5kK6X6fFigLPd2szxW8ETEOLNW9z/9RqdlXtLAY7nakrI62BH8FGOxIes36tvAP/9numwokEWKVLy0WpNkB7w==
|
||||
|
||||
angular-local-storage@0.7.1:
|
||||
version "0.7.1"
|
||||
resolved "https://registry.yarnpkg.com/angular-local-storage/-/angular-local-storage-0.7.1.tgz#fbd2730763c29fa9af5725e0186c780621e8cdd2"
|
||||
integrity sha1-+9JzB2PCn6mvVyXgGGx4BiHozdI=
|
||||
|
||||
angular-mocks@1.7.5:
|
||||
version "1.7.5"
|
||||
resolved "https://registry.yarnpkg.com/angular-mocks/-/angular-mocks-1.7.5.tgz#c8baba5a06ed60b934697026b492169626af384b"
|
||||
|
|
Загрузка…
Ссылка в новой задаче