Bug 1539232 - Switch Perfherder to react-router (#5379)

Switch Perfherder to react-router
Use top-level of app as a cache for projects, frameworks, alerts data and compare data
Cleanup files and move constants to dedicated perfherder file
Remove angular-related libraries and bump up the neutrino entry and asset limits
This commit is contained in:
Sarah Clements 2019-09-25 15:15:37 -07:00 коммит произвёл GitHub
Родитель 4a267309d5
Коммит 59737af771
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
51 изменённых файлов: 644 добавлений и 835 удалений

Просмотреть файл

@ -40,8 +40,8 @@ module.exports = {
title: 'Push Health',
},
perf: {
entry: 'entry-perf.js',
template: 'ui/perf.html',
entry: 'perfherder/index.jsx',
title: 'Perfherder',
},
'intermittent-failures': {
entry: 'intermittent-failures/index.jsx',
@ -149,8 +149,8 @@ module.exports = {
// to help prevent unknowingly regressing the bundle size (bug 1384255).
neutrino.config.performance
.hints('error')
.maxAssetSize(2 * 1024 * 1024)
.maxEntrypointSize(2.25 * 1024 * 1024);
.maxAssetSize(1.5 * 1024 * 1024)
.maxEntrypointSize(2 * 1024 * 1024);
}
},
],

Просмотреть файл

@ -22,22 +22,16 @@
"@neutrinojs/copy": "9.0.0-rc.3",
"@neutrinojs/react": "9.0.0-rc.3",
"@testing-library/react": "9.1.4",
"@types/angular": "*",
"@types/prop-types": "*",
"@types/react": "*",
"@types/react-dom": "*",
"@uirouter/angularjs": "0.4.3",
"ajv": "6.10.2",
"angular": "1.7.8",
"angular-clipboard": "1.7.0",
"angular1-ui-bootstrap4": "2.4.22",
"auth0-js": "9.11.3",
"bootstrap": "4.3.1",
"d3": "5.12.0",
"fuse.js": "3.4.5",
"history": "4.10.0",
"jquery": "3.4.1",
"jquery.flot": "0.8.3",
"js-cookie": "2.2.1",
"js-yaml": "3.13.1",
"json-e": "3.0.1",
@ -65,7 +59,6 @@
"react-split-pane": "0.1.87",
"react-table": "6.10.3",
"react-tabs": "3.0.0",
"react2angular": "4.0.6",
"reactstrap": "7.1.0",
"redux": "4.0.4",
"redux-debounce": "1.0.1",

Просмотреть файл

@ -11,6 +11,7 @@ import {
import AlertsViewControls from '../../../ui/perfherder/alerts/AlertsViewControls';
import optionCollectionMap from '../mock/optionCollectionMap';
import { summaryStatusMap } from '../../../ui/perfherder/constants';
import repos from '../mock/repositories';
const testUser = {
username: 'test user',
@ -219,10 +220,6 @@ const alertsViewControls = () =>
hideDwnToInv: undefined,
hideImprovements: undefined,
filter: undefined,
projects: [
{ id: 1, name: 'mozilla-central' },
{ id: 2, name: 'mozilla-inbound' },
],
updateParams: () => {},
}}
dropdownOptions={testAlertDropdowns}
@ -233,6 +230,11 @@ const alertsViewControls = () =>
updateViewState={() => {}}
user={testUser}
modifyAlert={(alert, params) => mockModifyAlert.update(alert, params)}
projects={repos}
location={{
pathname: '/alerts',
search: '',
}}
/>,
);

Просмотреть файл

@ -20,5 +20,5 @@
"prod": "https://treeherder.mozilla.org/",
"stage": "https://treeherder.allizom.org/"
},
"keywords": ["css", "django", "angular", "js", "python"]
"keywords": ["css", "django", "react", "js", "python"]
}

Просмотреть файл

@ -3,8 +3,7 @@
body {
text-align: center;
overflow-x: auto;
overflow-y: auto;
overflow: auto;
}
h1,
@ -17,33 +16,6 @@ h1,
margin-bottom: 7px;
}
.top-navbar {
background-color: #222;
height: 34px;
position: absolute;
}
.btn-navbar {
background-color: transparent;
border-color: #373d40;
border-radius: 0;
border-bottom: 0;
border-top: 0;
border-right: 0;
}
.navbar-link {
color: lightgray;
}
.navbar-link:hover {
color: lightgray;
}
.navbar-link:visited {
color: lightgray;
}
.bug-column-header {
text-align: left;
margin-left: 10px;

Просмотреть файл

@ -1,3 +1,7 @@
body {
overflow: auto;
}
section {
height: calc(100% - 50px);
overflow-y: auto;
@ -75,6 +79,7 @@ h1 {
pointer-events: none;
width: 280px;
z-index: 999;
text-align: left;
}
.graph-tooltip.locked {
pointer-events: auto;

Просмотреть файл

@ -447,3 +447,22 @@ fieldset[disabled] .btn-view-nav-closed.active {
.dropdown-item:hover {
background-color: #d3d3d34d;
}
/* Used by Perfherder and IFV */
.top-navbar {
background-color: #222;
height: 34px;
position: absolute;
}
.navbar-link {
color: lightgray;
}
.navbar-link:hover {
color: lightgray;
}
.navbar-link:visited {
color: lightgray;
}

Просмотреть файл

@ -1,49 +0,0 @@
// Webpack entry point for perf.html
// Vendor Styles
import 'angular/angular-csp.css';
import 'bootstrap/dist/css/bootstrap.min.css';
// Vendor JS
import 'bootstrap';
import { library, dom, config } from '@fortawesome/fontawesome-svg-core';
import { faFileCode, faFileWord } from '@fortawesome/free-regular-svg-icons';
import {
faBug,
faCode,
faQuestionCircle,
} from '@fortawesome/free-solid-svg-icons';
import { faGithub } from '@fortawesome/free-brands-svg-icons';
// The official 'flot' NPM package is out of date, so we're using 'jquery.flot'
// instead, which is identical to https://github.com/flot/flot
import 'jquery.flot';
import 'jquery.flot/jquery.flot.time';
import 'jquery.flot/jquery.flot.selection';
// Perf Styles
import './css/treeherder-global.css';
import './css/treeherder-navbar.css';
import './css/perf.css';
// Bootstrap the Angular modules against which everything will be registered
import './js/perf';
// Perf JS
import './js/perfapp';
import './perfherder/compare/CompareSelectorView';
import './perfherder/compare/CompareView';
import './perfherder/compare/CompareSubtestDistributionView';
import './perfherder/compare/CompareSubtestsView';
import './perfherder/alerts/AlertTable';
import './perfherder/alerts/AlertsView';
import './perfherder/graphs/GraphsView';
config.showMissingIcons = true;
// TODO: Remove these as Perfherder components switch to using react-fontawesome.
library.add(faBug, faCode, faFileCode, faFileWord, faGithub, faQuestionCircle);
// Replace any existing <i> or <span> tags with <svg> and set up a MutationObserver
// to continue doing this as the DOM changes. Remove once using react-fontawesome.
dom.watch();

Просмотреть файл

@ -221,31 +221,6 @@ export const thEvents = {
clearPinboard: 'clear-pinboard-EVT',
};
export const phTimeRanges = [
{ value: 86400, text: 'Last day' },
{ value: 86400 * 2, text: 'Last 2 days' },
{ value: 604800, text: 'Last 7 days' },
{ value: 1209600, text: 'Last 14 days' },
{ value: 2592000, text: 'Last 30 days' },
{ value: 5184000, text: 'Last 60 days' },
{ value: 7776000, text: 'Last 90 days' },
{ value: 31536000, text: 'Last year' },
];
export const phDefaultTimeRangeValue = 1209600;
export const phFrameworksWithRelatedBranches = [
1, // talos
10, // raptor
11, // js-bench
12, // devtools
];
export const compareDefaultTimeRange = {
value: 86400 * 2,
text: 'Last 2 days',
};
export const thBugSuggestionLimit = 20;
export const thMaxPushFetchSize = 100;

Просмотреть файл

@ -122,3 +122,13 @@ export const bugzillaBugsApi = function bugzillaBugsApi(api, params) {
export const getRevisionUrl = (revision, projectName) =>
revision ? getJobsUrl({ repo: projectName, revision }) : '';
export const updateQueryParams = function updateHistoryWithQueryParams(
queryParams,
history,
location,
) {
history.replace({ pathname: location.pathname, search: queryParams });
// we do this so the api's won't be called twice (location/history updates will trigger a lifecycle hook)
location.search = queryParams;
};

Просмотреть файл

@ -238,8 +238,8 @@ BugDetailsView.defaultProps = {
};
const defaultState = {
route: '/bugdetails',
endpoint: bugDetailsEndpoint,
route: '/bugdetails',
};
export default withView(defaultState)(BugDetailsView);

Просмотреть файл

@ -184,8 +184,8 @@ const defaultState = {
.subtract(7, 'days'),
),
endday: ISODate(moment().utc()),
route: '/main',
endpoint: bugsEndpoint,
route: '/main',
};
export default withView(defaultState)(MainView);

Просмотреть файл

@ -7,15 +7,11 @@ import {
createQueryParams,
createApiUrl,
bugzillaBugsApi,
updateQueryParams,
} from '../helpers/url';
import { getData } from '../helpers/http';
import {
updateQueryParams,
validateQueryParams,
mergeData,
formatBugs,
} from './helpers';
import { validateQueryParams, mergeData, formatBugs } from './helpers';
const withView = defaultState => WrappedComponent => {
class View extends React.Component {
@ -72,7 +68,7 @@ const withView = defaultState => WrappedComponent => {
// if the query params are not specified for mainview, set params based on default state
if (location.search === '') {
const queryString = createQueryParams(params);
updateQueryParams(defaultState.route, queryString, history, location);
updateQueryParams(queryString, history, location);
}
this.setState({ initialParamsSet: true });
@ -157,12 +153,7 @@ const withView = defaultState => WrappedComponent => {
// update query params if dates or tree are updated
const queryString = createQueryParams(params);
updateQueryParams(
defaultState.route,
queryString,
this.props.history,
this.props.location,
);
updateQueryParams(queryString, this.props.history, this.props.location);
});
};

Просмотреть файл

@ -63,17 +63,6 @@ export const calculateMetrics = function calculateMetricsForGraphs(data) {
};
};
export const updateQueryParams = function updateHistoryWithQueryParams(
view,
queryParams,
history,
location,
) {
history.replace({ pathname: view, search: queryParams });
// we do this so the api's won't be called twice (location/history updates will trigger a lifecycle hook)
location.search = queryParams;
};
export const sortData = function sortData(data, sortBy, desc) {
data.sort((a, b) => {
const item1 = desc ? b[sortBy] : a[sortBy];

Просмотреть файл

@ -7,13 +7,13 @@ import Logo from '../../img/treeherder-logo.png';
import Login from '../../shared/auth/Login';
import LogoMenu from '../../shared/LogoMenu';
import { notify } from '../redux/stores/notifications';
import HelpMenu from '../../shared/HelpMenu';
import NotificationsMenu from './NotificationsMenu';
import InfraMenu from './InfraMenu';
import ReposMenu from './ReposMenu';
import TiersMenu from './TiersMenu';
import FiltersMenu from './FiltersMenu';
import HelpMenu from './HelpMenu';
import SecondaryNavBar from './SecondaryNavBar';
import HealthMenu from './HealthMenu';

Просмотреть файл

@ -1,24 +0,0 @@
import angular from 'angular';
import angularClipboardModule from 'angular-clipboard';
import uiBootstrap from 'angular1-ui-bootstrap4';
import uiRouter from '@uirouter/angularjs';
import 'ng-text-truncate-2';
import { react2angular } from 'react2angular/index.es2015';
import Login from '../shared/auth/Login';
import LogoMenu from '../shared/LogoMenu';
import treeherderModule from './treeherder';
const perf = angular.module('perf', [
uiRouter,
uiBootstrap,
treeherderModule.name,
angularClipboardModule.name,
'ngTextTruncate',
]);
perf.component('login', react2angular(Login, ['user', 'setUser'], []));
perf.component('logoMenu', react2angular(LogoMenu, ['menuText'], []));
export default perf;

Просмотреть файл

@ -1,79 +0,0 @@
// Remove the eslint-disable when rewriting this file during the React conversion.
/* eslint-disable func-names */
import alertsCtrlTemplate from '../partials/perf/alertsctrl.html';
import graphsCtrlTemplate from '../partials/perf/graphsctrl.html';
import compareCtrlTemplate from '../partials/perf/comparectrl.html';
import compareSubtestCtrlTemplate from '../partials/perf/comparesubtestctrl.html';
import compareChooserCtrlTemplate from '../partials/perf/comparechooserctrl.html';
import compareSubtestDistributionTemplate from '../partials/perf/comparesubtestdistribution.html';
import helpMenuTemplate from '../partials/perf/helpMenu.html';
import perf from './perf';
// configure the router here, after we have defined all the controllers etc
perf.config(['$compileProvider', '$locationProvider', '$httpProvider', '$stateProvider', '$urlRouterProvider',
function ($compileProvider, $locationProvider, $httpProvider, $stateProvider, $urlRouterProvider) {
// Disable debug data & legacy comment/class directive syntax, as recommended by:
// https://docs.angularjs.org/guide/production
$compileProvider.debugInfoEnabled(false);
$compileProvider.commentDirectivesEnabled(false);
$compileProvider.cssClassDirectivesEnabled(false);
// Revert to the legacy Angular <=1.5 URL hash prefix to save breaking existing links:
// https://docs.angularjs.org/guide/migration#commit-aa077e8
$locationProvider.hashPrefix('');
$httpProvider.defaults.xsrfHeaderName = 'X-CSRFToken';
$httpProvider.defaults.xsrfCookieName = 'csrftoken';
$httpProvider.useApplyAsync(true);
$stateProvider
.state('alerts', {
title: 'Alerts',
template: alertsCtrlTemplate,
url: '/alerts?id&status&framework&filter&hideImprovements&hideDwnToInv&page',
})
.state('graphs', {
title: 'Graphs',
template: graphsCtrlTemplate,
url: '/graphs?timerange&series&highlightedRevisions&highlightAlerts&zoom&selected',
})
.state('compare', {
title: 'Compare',
template: compareCtrlTemplate,
url: '/compare?originalProject&originalRevision?&newProject&newRevision&hideMinorChanges&framework&filter&showOnlyComparable&showOnlyImportant&showOnlyConfident&selectedTimeRange&showOnlyNoise?',
})
.state('comparesubtest', {
title: 'Compare - Subtests',
template: compareSubtestCtrlTemplate,
url: '/comparesubtest?originalProject&originalRevision?&newProject&newRevision&originalSignature&newSignature&filter&showOnlyComparable&showOnlyImportant&showOnlyConfident&framework&selectedTimeRange&showOnlyNoise?',
})
.state('comparechooser', {
title: 'Compare Chooser',
template: compareChooserCtrlTemplate,
url: '/comparechooser?originalProject&originalRevision&newProject&newRevision',
})
.state('comparesubtestdistribution', {
title: 'Compare Subtest Distribution',
template: compareSubtestDistributionTemplate,
url: '/comparesubtestdistribution?originalProject&newProject&originalRevision&newRevision&originalSubtestSignature?newSubtestSignature',
});
$urlRouterProvider.otherwise('/graphs');
}]).run(['$rootScope', '$state', '$stateParams', function ($rootScope, $state, $stateParams) {
$rootScope.$state = $state;
$rootScope.$stateParams = $stateParams;
$rootScope.user = { isLoggedIn: false };
$rootScope.setUser = (user) => {
$rootScope.user = user;
$rootScope.$apply();
};
$rootScope.$on('$stateChangeSuccess', function () {
if ($state.current.title) {
window.document.title = $state.current.title;
}
});
// Templates used by ng-include have to be manually put in the template cache.
// Those used by directives should instead be imported at point of use.
}]).run(['$templateCache', ($templateCache) => $templateCache.put('partials/perf/helpMenu.html', helpMenuTemplate)]);

Просмотреть файл

@ -1,3 +0,0 @@
import angular from 'angular';
export default angular.module('treeherder', []);

Просмотреть файл

@ -1 +0,0 @@
<alerts-view user="user"></alerts-view>

Просмотреть файл

@ -1 +0,0 @@
<compare-selector-view />

Просмотреть файл

@ -1 +0,0 @@
<compare-view user="user"></compare-view>

Просмотреть файл

@ -1 +0,0 @@
<compare-subtests-view user="user"/>

Просмотреть файл

@ -1 +0,0 @@
<compare-subtest-distribution-view />

Просмотреть файл

@ -1 +0,0 @@
<graphs-view user="user" />

Просмотреть файл

@ -1,26 +0,0 @@
<!--- TODO: Replace this with the React component used by jobs-view -->
<li>
<a class="dropdown-item" href="https://treeherder.readthedocs.io/" target="_blank" rel="noopener">
<span class="far fa-file-code fa-fw mr-1 midgray"></span>
Development Documentation</a>
</li>
<li>
<a class="dropdown-item" href="/docs/" target="_blank" rel="noopener">
<span class="fas fa-code fa-fw mr-1 midgray"></span>
API Reference</a>
</li>
<li>
<a class="dropdown-item" href="https://wiki.mozilla.org/EngineeringProductivity/Projects/Perfherder" target="_blank" rel="noopener">
<span class="far fa-file-word fa-fw mr-1 midgray"></span>
Project Wiki</a>
</li>
<li>
<a class="dropdown-item" href="https://bugzilla.mozilla.org/enter_bug.cgi?product=Tree%20Management&component=Perfherder" target="_blank" rel="noopener">
<span class="fas fa-bug fa-fw mr-1 midgray"></span>
Report a Bug</a>
</li>
<li>
<a class="dropdown-item" href="https://github.com/mozilla/treeherder" target="_blank" rel="noopener">
<span class="fab fa-github fa-fw mr-1 midgray"></span>
Source</a>
</li>

Просмотреть файл

@ -1,81 +0,0 @@
<!DOCTYPE html>
<html ng-app="perf" ng-strict-di ng-csp>
<head>
<meta charset="utf-8" />
<title>Perfherder</title>
<link
id="favicon"
type="image/png"
rel="shortcut icon"
href="img/line_chart.png"
/>
<style>
[ng\:cloak],
[ng-cloak],
[data-ng-cloak],
[x-ng-cloak],
.ng-cloak,
.x-ng-cloak {
display: none !important;
}
</style>
</head>
<body>
<nav
id="th-global-navbar"
class="navbar navbar-inverse"
role="navigation"
ng-cloak
>
<div id="th-global-navbar-top" class="navbar navbar-collapse">
<span class="navbar-left d-flex">
<logo-menu menu-text="'Perfherder'"></logo-menu>
<a
ng-class="{active: $state.includes('graphs')}"
class="btn btn-view-nav"
ui-sref="graphs"
>Graphs</a
>
<a
ng-class="{active: $state.current.name.startsWith('compare')}"
class="btn btn-view-nav"
ui-sref="comparechooser"
>Compare</a
>
<a
ng-class="{active: $state.current.name.indexOf('alerts') >= 0}"
class="btn btn-view-nav"
ui-sref="alerts({id: null, status: null, framework: null, filter: null, hideImprovements: null, hideDwnToInv: 1})"
>Alerts</a
>
</span>
<div class="nav navbar-right">
<!-- Help Menu -->
<span class="dropdown">
<button
id="helpLabel"
title="Perfherder help"
aria-label="Perfherder help"
role="button"
data-toggle="dropdown"
class="btn btn-view-nav nav-help-btn dropdown-toggle"
>
<span class="fas fa-question-circle lightgray"></span>
</button>
<ul
class="dropdown-menu nav-dropdown-menu-right icon-menu"
role="menu"
aria-labelledby="helpLabel"
ng-include="'partials/perf/helpMenu.html'"
></ul>
</span>
<!-- login -->
<login user="$root.user" set-user="setUser"></login>
</div>
</div>
</nav>
<div style="padding-top: 20px;"></div>
<section ui-view></section>
</body>
</html>

222
ui/perfherder/App.jsx Normal file
Просмотреть файл

@ -0,0 +1,222 @@
import React from 'react';
import { HashRouter, Route, Switch, Redirect } from 'react-router-dom';
import { hot } from 'react-hot-loader/root';
import { Container } from 'reactstrap';
import { getData, processResponse } from '../helpers/http';
import { getApiUrl, repoEndpoint } from '../helpers/url';
import ErrorMessages from '../shared/ErrorMessages';
import { endpoints } from './constants';
import GraphsView from './graphs/GraphsView';
import AlertsView from './alerts/AlertsView';
import CompareView from './compare/CompareView';
import CompareSelectorView from './compare/CompareSelectorView';
import CompareSubtestsView from './compare/CompareSubtestsView';
import CompareSubtestDistributionView from './compare/CompareSubtestDistributionView';
import Navigation from './Navigation';
class App extends React.Component {
constructor(props) {
super(props);
// store alerts and comapre view data so the API's won't be
// called again when navigating back from related views.
this.state = {
projects: [],
frameworks: [],
user: {},
errorMessages: [],
compareData: [],
};
}
async componentDidMount() {
const [projects, frameworks] = await Promise.all([
getData(getApiUrl(repoEndpoint)),
getData(getApiUrl(endpoints.frameworks)),
]);
const errorMessages = [];
const updates = {
...processResponse(projects, 'projects', errorMessages),
...processResponse(frameworks, 'frameworks', errorMessages),
};
this.setState(updates);
}
updateAppState = state => {
this.setState(state);
};
render() {
const {
user,
projects,
frameworks,
errorMessages,
compareData,
} = this.state;
return (
<HashRouter>
<Navigation
user={user}
setUser={user => this.setState({ user })}
notify={message => this.setState({ errorMessages: [message] })}
/>
{projects.length > 0 && frameworks.length > 0 && (
<main className="pt-5">
<Switch>
<Route
exact
path="/alerts"
render={props => (
<AlertsView
{...props}
user={user}
projects={projects}
frameworks={frameworks}
/>
)}
/>
<Route
path="/alerts?id=:id&status=:status&framework=:framework&filter=:filter&hideImprovements=:hideImprovements&hideDwnToInv=:hideDwnToInv&page=:page"
render={props => (
<AlertsView
{...props}
user={user}
projects={projects}
frameworks={frameworks}
/>
)}
/>
<Route
path="/graphs"
render={props => (
<GraphsView
{...props}
user={user}
projects={projects}
frameworks={frameworks}
/>
)}
/>
<Route
path="/graphs?timerange=:timerange&series=:series&highlightedRevisions=:highlightedRevisions&highlightAlerts=:highlightAlerts&zoom=:zoom&selected=:selected"
render={props => (
<GraphsView
{...props}
user={user}
projects={projects}
frameworks={frameworks}
/>
)}
/>
<Route
path="/comparechooser"
render={props => (
<CompareSelectorView
{...props}
user={user}
projects={projects}
frameworks={frameworks}
/>
)}
/>
<Route
path="/comparechooser?originalProject=:originalProject&originalRevision=:originalRevision&newProject=:newProject&newRevision=:newRevision"
render={props => (
<CompareSelectorView
{...props}
user={user}
projects={projects}
frameworks={frameworks}
/>
)}
/>
<Route
path="/compare"
render={props => (
<CompareView
{...props}
user={user}
projects={projects}
frameworks={frameworks}
compareData={compareData}
updateAppState={this.updateAppState}
/>
)}
/>
<Route
path="/compare?originalProject=:originalProject&originalRevision=:originalRevison&newProject=:newProject&newRevision=:newRevision&framework=:framework&showOnlyComparable=:showOnlyComparable&showOnlyImportant=:showOnlyImportant&showOnlyConfident=:showOnlyConfident&selectedTimeRange=:selectedTimeRange&showOnlyNoise=:showOnlyNoise"
render={props => (
<CompareView
{...props}
user={user}
projects={projects}
frameworks={frameworks}
compareData={compareData}
updateAppState={this.updateAppState}
/>
)}
/>
<Route
path="/comparesubtest"
render={props => (
<CompareSubtestsView
{...props}
user={user}
projects={projects}
frameworks={frameworks}
/>
)}
/>
<Route
path="/comparesubtest?originalProject=:originalProject&originalRevision=:originalRevision&newProject=:newProject&newRevision=:newRevision&originalSignature=:originalSignature&newSignature=:newSignature&framework=:framework&showOnlyComparable=:showOnlyComparable&showOnlyImportant=:showOnlyImportant&showOnlyConfident=:showOnlyConfident&selectedTimeRange=:selectedTimeRange&showOnlyNoise=:showOnlyNoise"
render={props => (
<CompareSubtestsView
{...props}
user={user}
projects={projects}
frameworks={frameworks}
/>
)}
/>
<Route
path="/comparesubtestdistribution"
render={props => (
<CompareSubtestDistributionView
{...props}
user={user}
projects={projects}
frameworks={frameworks}
/>
)}
/>
<Route
path="/comparesubtestdistribution?originalProject=:originalProject&newProject=:newProject&originalRevision=:originalRevision&newRevision=:newRevision&originalSubtestSignature=:originalSubtestSignature&newSubtestSignature=:newSubtestSignature"
render={props => (
<CompareSubtestDistributionView
{...props}
user={user}
projects={projects}
frameworks={frameworks}
/>
)}
/>
<Redirect from="/" to="/alerts?hideDwnToInv=1" />
</Switch>
</main>
)}
{errorMessages.length > 0 && (
<Container className="pt-5 max-width-default">
<ErrorMessages errorMessages={errorMessages} />
</Container>
)}
</HashRouter>
);
}
}
export default hot(App);

Просмотреть файл

@ -0,0 +1,42 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Navbar, Nav, NavItem, NavLink } from 'reactstrap';
import LogoMenu from '../shared/LogoMenu';
import Login from '../shared/auth/Login';
import HelpMenu from '../shared/HelpMenu';
const Navigation = ({ user, setUser, notify }) => (
<Navbar expand fixed="top" className="top-navbar">
<LogoMenu menuText="Perfherder" colorClass="text-info" />
<Nav className="navbar navbar-inverse">
<NavItem>
<NavLink href="#/graphs" className="btn-view-nav">
Graphs
</NavLink>
</NavItem>
<NavItem>
<NavLink href="#/comparechooser" className="btn-view-nav">
Compare
</NavLink>
</NavItem>
<NavItem>
<NavLink href="#/alerts?hideDwnToInv=1" className="btn-view-nav">
Alerts
</NavLink>
</NavItem>
</Nav>
<Navbar className="ml-auto">
<HelpMenu />
<Login user={user} setUser={setUser} notify={notify} />
</Navbar>
</Navbar>
);
Navigation.propTypes = {
user: PropTypes.shape({}).isRequired,
setUser: PropTypes.func.isRequired,
notify: PropTypes.func.isRequired,
};
export default Navigation;

Просмотреть файл

@ -2,32 +2,25 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Container } from 'reactstrap';
import { getData, processResponse } from '../helpers/http';
import { getApiUrl, repoEndpoint } from '../helpers/url';
import {
parseQueryParams,
createQueryParams,
updateQueryParams,
} from '../helpers/url';
import PushModel from '../models/push';
import ErrorMessages from '../shared/ErrorMessages';
import LoadingSpinner from '../shared/LoadingSpinner';
import { endpoints, summaryStatusMap } from './constants';
// TODO once we switch to react-router
// 1) use context in this HOC to share state between compare views, by wrapping router component in it;
// advantages include:
// * no need to check historical location.state to determine if user has navigated to compare or comparesubtest
// views from a previous view (thus params have already been validated and resultsets stored in state)
// * if user navigates to compareview from compareChooser and decides to change a project via query params
// to a different project, projects will already be stored in state so no fetching data again to validate
//
import { summaryStatusMap } from './constants';
const withValidation = (
requiredParams,
{ requiredParams },
verifyRevisions = true,
) => WrappedComponent => {
class Validation extends React.Component {
constructor(props) {
super(props);
// TODO change $stateParams to location.state once we switch to react-router
this.state = {
originalProject: null,
newProject: null,
@ -36,38 +29,32 @@ const withValidation = (
originalSignature: null,
newSignature: null,
errorMessages: [],
projects: [],
originalResultSet: null,
newResultSet: null,
selectedTimeRange: null,
framework: null,
frameworks: [],
// TODO reset if validateParams method is called from another component
validationComplete: false,
};
}
async componentDidMount() {
const [projects, frameworks] = await Promise.all([
getData(getApiUrl(repoEndpoint)),
getData(getApiUrl(endpoints.frameworks)),
]);
const updates = {
...processResponse(projects, 'projects'),
...processResponse(frameworks, 'frameworks'),
};
this.setState(updates, () =>
this.validateParams(this.props.$stateParams),
);
this.validateParams(parseQueryParams(this.props.location.search));
}
updateParams = param => {
const { transitionTo, current } = this.props.$state;
transitionTo(current.name, param, {
inherit: true,
notify: false,
});
componentDidUpdate(prevProps) {
const { location } = this.props;
if (location.search !== prevProps.location.search) {
this.validateParams(parseQueryParams(location.search));
}
}
updateParams = params => {
const { location, history } = this.props;
const newParams = { ...parseQueryParams(location.search), ...params };
const queryString = createQueryParams(newParams);
updateQueryParams(queryString, history, location);
};
errorMessage = (param, value) => `${param} ${value} is not valid`;
@ -134,7 +121,7 @@ const withValidation = (
}
validateParams(params) {
const { projects, frameworks } = this.state;
const { projects, frameworks } = this.props;
let errors = [];
// eslint-disable-next-line no-unused-vars
@ -173,10 +160,13 @@ const withValidation = (
if (verifyRevisions) {
return this.checkRevisions(params);
}
this.setState({
...params,
validationComplete: true,
});
this.setState(
{
...params,
validationComplete: true,
},
this.updateParams({ ...params }),
);
}
render() {
@ -208,11 +198,7 @@ const withValidation = (
}
Validation.propTypes = {
$stateParams: PropTypes.shape({}).isRequired,
$state: PropTypes.shape({
transitionTo: PropTypes.func,
current: PropTypes.shape({}),
}).isRequired,
location: PropTypes.shape({}).isRequired,
};
return Validation;

Просмотреть файл

@ -122,7 +122,7 @@ export default class AlertTable extends React.Component {
render() {
const {
user,
validated,
projects,
alertSummaries,
issueTrackers,
fetchAlertSummaries,
@ -140,7 +140,7 @@ export default class AlertTable extends React.Component {
const downstreamIdsLength = downstreamIds.length;
const repo = alertSummary
? validated.projects.find(repo => repo.name === alertSummary.repository)
? projects.find(repo => repo.name === alertSummary.repository)
: null;
const repoModel = new RepositoryModel(repo);
@ -281,9 +281,6 @@ export default class AlertTable extends React.Component {
AlertTable.propTypes = {
alertSummary: PropTypes.shape({}),
user: PropTypes.shape({}),
validated: PropTypes.shape({
projects: PropTypes.arrayOf(PropTypes.shape({})),
}).isRequired,
alertSummaries: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
issueTrackers: PropTypes.arrayOf(PropTypes.shape({})),
optionCollectionMap: PropTypes.shape({}).isRequired,
@ -296,6 +293,7 @@ AlertTable.propTypes = {
updateViewState: PropTypes.func.isRequired,
bugTemplate: PropTypes.shape({}),
modifyAlert: PropTypes.func,
projects: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
};
AlertTable.defaultProps = {

Просмотреть файл

@ -13,10 +13,12 @@ import { createQueryParams } from '../../helpers/url';
import { getStatus, getGraphsURL, modifyAlert } from '../helpers';
import SimpleTooltip from '../../shared/SimpleTooltip';
import ProgressBar from '../ProgressBar';
import { alertStatusMap } from '../constants';
import { phDefaultTimeRangeValue, phTimeRanges } from '../../helpers/constants';
import {
alertStatusMap,
phDefaultTimeRangeValue,
phTimeRanges,
} from '../constants';
// TODO remove $stateParams and $state after switching to react router
export default class AlertTableRow extends React.Component {
constructor(props) {
super(props);
@ -94,7 +96,6 @@ export default class AlertTableRow extends React.Component {
{` ${text} `}
<a
href={`#/alerts?id=${alertId}`}
target="_blank"
rel="noopener noreferrer"
className="text-info"
>{`alert #${alertId}`}</a>

Просмотреть файл

@ -1,6 +1,6 @@
/* eslint-disable react/no-did-update-set-state */
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular/index.es2015';
import {
Alert,
Container,
@ -10,11 +10,14 @@ import {
PaginationLink,
} from 'reactstrap';
import perf from '../../js/perf';
import withValidation from '../Validation';
import { convertParams, getFrameworkData, getStatus } from '../helpers';
import { summaryStatusMap, endpoints } from '../constants';
import { createQueryParams, getApiUrl } from '../../helpers/url';
import {
createQueryParams,
getApiUrl,
parseQueryParams,
} from '../../helpers/url';
import { getData, processResponse } from '../../helpers/http';
import ErrorMessages from '../../shared/ErrorMessages';
import OptionCollectionModel from '../../models/optionCollection';
@ -27,14 +30,13 @@ import LoadingSpinner from '../../shared/LoadingSpinner';
import AlertsViewControls from './AlertsViewControls';
// TODO remove $stateParams and $state after switching to react router
export class AlertsView extends React.Component {
class AlertsView extends React.Component {
constructor(props) {
super(props);
this.validated = this.props.validated;
this.state = {
status: this.getDefaultStatus(),
framework: getFrameworkData(this.validated),
framework: getFrameworkData(this.props),
page: this.validated.page ? parseInt(this.validated.page, 10) : 1,
errorMessages: [],
alertSummaries: [],
@ -54,14 +56,26 @@ export class AlertsView extends React.Component {
componentDidUpdate(prevProps, prevState) {
const { count } = this.state;
const { validated } = this.props;
if (prevState.count !== count) {
// eslint-disable-next-line react/no-did-update-set-state
this.setState({ totalPages: this.generatePages(count) });
}
const params = parseQueryParams(this.props.location.search);
// we're using local state for id instead of validated.id because once
// the user navigates from the id=<alert> view back to the main alerts view
// the Validation component won't reset the id (since the query param doesn't exist
// unless there is a value)
if (this.props.location.search !== prevProps.location.search) {
this.setState({ id: params.id || null }, this.fetchAlertSummaries);
validated.updateParams({ hideDwnToInv: 0 });
}
}
getDefaultStatus = () => {
const { validated } = this.props;
const statusParam = convertParams(validated, 'status');
if (!statusParam) {
return Object.keys(summaryStatusMap)[1];
@ -70,7 +84,8 @@ export class AlertsView extends React.Component {
};
updateFramework = selection => {
const { frameworks, updateParams } = this.props.validated;
const { updateParams } = this.props.validated;
const { frameworks } = this.props;
const framework = frameworks.find(item => item.name === selection);
updateParams({ framework: framework.id });
@ -106,7 +121,6 @@ export class AlertsView extends React.Component {
return pages;
};
// TODO potentially pass as a prop for testing purposes
async fetchAlertSummaries(id = this.state.id, update = false) {
// turn off loading when update is true (used to update alert statuses)
this.setState({ loading: !update, errorMessages: [] });
@ -143,7 +157,6 @@ export class AlertsView extends React.Component {
`${endpoints.alertSummary}${createQueryParams(params)}`,
);
// TODO OptionCollectionModel to use getData wrapper
if (!issueTrackers.length && !optionCollectionMap) {
const [optionCollectionMap, issueTrackers] = await Promise.all([
OptionCollectionModel.getMap(),
@ -162,6 +175,8 @@ export class AlertsView extends React.Component {
if (response.alertSummaries) {
const summary = response.alertSummaries;
// used with the id argument to update one specific alert summary in the array of
// alert summaries that's been updated based on an action taken in the AlertActionPanel
if (update) {
const index = alertSummaries.findIndex(
item => item.id === summary.results[0].id,
@ -184,7 +199,7 @@ export class AlertsView extends React.Component {
}
render() {
const { user, validated } = this.props;
const { user, frameworks } = this.props;
const {
framework,
status,
@ -198,7 +213,6 @@ export class AlertsView extends React.Component {
bugTemplate,
id,
} = this.state;
const { frameworks } = validated;
const frameworkNames =
frameworks && frameworks.length ? frameworks.map(item => item.name) : [];
@ -240,7 +254,6 @@ export class AlertsView extends React.Component {
</Alert>
)}
<AlertsViewControls
validated={validated}
dropdownOptions={id ? [] : alertDropdowns}
alertSummaries={alertSummaries}
issueTrackers={issueTrackers}
@ -249,6 +262,7 @@ export class AlertsView extends React.Component {
updateViewState={state => this.setState(state)}
bugTemplate={bugTemplate}
user={user}
{...this.props}
/>
{pageNums.length > 0 && (
<Row className="justify-content-center pb-5">
@ -301,29 +315,20 @@ export class AlertsView extends React.Component {
}
AlertsView.propTypes = {
$stateParams: PropTypes.shape({}),
$state: PropTypes.shape({
go: PropTypes.func,
}),
location: PropTypes.shape({}),
user: PropTypes.shape({}).isRequired,
validated: PropTypes.shape({
projects: PropTypes.arrayOf(PropTypes.shape({})),
frameworks: PropTypes.arrayOf(PropTypes.shape({})),
updateParams: PropTypes.func.isRequired,
framework: PropTypes.string,
}).isRequired,
projects: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
frameworks: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
};
AlertsView.defaultProps = {
$stateParams: null,
$state: null,
location: null,
};
const alertsView = withValidation(new Set([]), false)(AlertsView);
perf.component(
'alertsView',
react2angular(alertsView, ['user'], ['$stateParams', '$state']),
export default withValidation({ requiredParams: new Set([]) }, false)(
AlertsView,
);
export default alertsView;

Просмотреть файл

@ -17,6 +17,24 @@ export default class AlertsViewControls extends React.Component {
};
}
componentDidUpdate(prevProps) {
const { validated } = this.props;
if (
validated.hideImprovements !== prevProps.validated.hideImprovements ||
validated.hideDwnToInv !== prevProps.validated.hideDwnToInv
) {
// eslint-disable-next-line react/no-did-update-set-state
this.setState({
hideImprovements: convertParams(
this.props.validated,
'hideImprovements',
),
hideDownstream: convertParams(this.props.validated, 'hideDwnToInv'),
});
}
}
updateFilter = filter => {
this.setState(
prevState => ({ [filter]: !prevState[filter] }),

Просмотреть файл

@ -7,7 +7,6 @@ import { getData } from '../../helpers/http';
import { endpoints } from '../constants';
import { getApiUrl } from '../../helpers/url';
// TODO remove $stateParams and $state after switching to react router
export default class DownstreamSummary extends React.Component {
constructor(props) {
super(props);

Просмотреть файл

@ -1,6 +1,4 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular/index.es2015';
import {
Container,
Col,
@ -11,28 +9,23 @@ import {
DropdownToggle,
} from 'reactstrap';
import perf from '../../js/perf';
import { getApiUrl, repoEndpoint } from '../../helpers/url';
import { endpoints } from '../constants';
import { getData, processResponse } from '../../helpers/http';
import { parseQueryParams, createQueryParams } from '../../helpers/url';
import ErrorMessages from '../../shared/ErrorMessages';
import DropdownMenuItems from '../../shared/DropdownMenuItems';
import {
compareDefaultTimeRange,
genericErrorMessage,
errorMessageClass,
} from '../../helpers/constants';
import { compareDefaultTimeRange } from '../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.queryParams = this.props.$stateParams;
this.queryParams = parseQueryParams(this.props.location.search);
this.state = {
projects: [],
originalProject: this.queryParams.originalProject || 'mozilla-central',
newProject: this.queryParams.newProject || 'try',
originalRevision: this.queryParams.originalRevision || '',
@ -42,37 +35,18 @@ export default class CompareSelectorView extends React.Component {
missingRevision: false,
framework: 1,
frameworkName: 'talos',
frameworks: [],
frameworkDropdownIsOpen: false,
};
}
async componentDidMount() {
const { errorMessages } = this.state;
const [projects, frameworks] = await Promise.all([
getData(getApiUrl(repoEndpoint)),
getData(getApiUrl(endpoints.frameworks)),
]);
const updates = {
...processResponse(projects, 'projects', errorMessages),
...processResponse(frameworks, 'frameworks', errorMessages),
};
this.setState(updates);
}
updateFramework = selection => {
this.setState(prevState => {
const selectedFramework = prevState.frameworks.find(
framework => framework.name === selection,
);
const selectedFramework = this.props.frameworks.find(
framework => framework.name === selection,
);
return {
framework: selectedFramework.id,
frameworkName: selectedFramework.name,
};
this.setState({
framework: selectedFramework.id,
frameworkName: selectedFramework.name,
});
};
@ -84,29 +58,31 @@ export default class CompareSelectorView extends React.Component {
newRevision,
framework,
} = this.state;
const { $state } = this.props;
const { history } = this.props;
let params;
if (newRevision === '') {
return this.setState({ missingRevision: 'Revision is required' });
}
if (originalRevision !== '') {
$state.go('compare', {
params = {
originalProject,
originalRevision,
newProject,
newRevision,
framework,
});
};
} else {
$state.go('compare', {
params = {
originalProject,
newProject,
newRevision,
framework,
selectedTimeRange: compareDefaultTimeRange.value,
});
};
}
history.push(`/compare${createQueryParams(params)}`);
};
toggleFrameworkDropdown = () => {
@ -119,9 +95,7 @@ export default class CompareSelectorView extends React.Component {
const {
originalProject,
newProject,
projects,
frameworkName,
frameworks,
originalRevision,
newRevision,
errorMessages,
@ -130,9 +104,11 @@ export default class CompareSelectorView extends React.Component {
frameworkDropdownIsOpen,
} = this.state;
const { projects, frameworks } = this.props;
const frameworkNames = frameworks.length
? frameworks.map(item => item.name)
: [];
return (
<Container fluid className="my-5 pt-5 max-width-default">
<ErrorBoundary
@ -147,35 +123,34 @@ export default class CompareSelectorView extends React.Component {
)}
</Col>
</Row>
{projects.length > 0 && (
<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}
errorMessages={errorMessages}
/>
<SelectorCard
projects={projects}
updateState={updates => this.setState(updates)}
selectedRepo={newProject}
title="New"
projectState="newProject"
revisionState="newRevision"
selectedRevision={newRevision}
errorMessages={errorMessages}
missingRevision={missingRevision}
/>
</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.queryParams.originalRevision}
errorMessages={errorMessages}
/>
<SelectorCard
projects={projects}
updateState={updates => this.setState(updates)}
selectedRepo={newProject}
title="New"
projectState="newProject"
revisionState="newRevision"
selectedRevision={newRevision}
errorMessages={errorMessages}
missingRevision={missingRevision}
/>
</Row>
<Row className="justify-content-center pt-3">
<Col sm="8" className="text-right px-1">
<ButtonGroup>
<ButtonDropdown
@ -208,18 +183,3 @@ export default class CompareSelectorView extends React.Component {
);
}
}
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']),
);

Просмотреть файл

@ -1,19 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular/index.es2015';
import { Container, Row } from 'reactstrap';
import perf from '../../js/perf';
import RepositoryModel from '../../models/repository';
import PushModel from '../../models/push';
import { getData } from '../../helpers/http';
import { createApiUrl, perfSummaryEndpoint } from '../../helpers/url';
import {
createApiUrl,
perfSummaryEndpoint,
parseQueryParams,
} from '../../helpers/url';
import LoadingSpinner from '../../shared/LoadingSpinner';
import RevisionInformation from './RevisionInformation';
import ReplicatesGraph from './ReplicatesGraph';
// TODO remove $stateParams after switching to react router
export default class CompareSubtestDistributionView extends React.Component {
constructor(props) {
super(props);
@ -32,7 +32,8 @@ export default class CompareSubtestDistributionView extends React.Component {
originalRevision,
newRevision,
newProject: newProjectName,
} = this.props.$stateParams;
} = parseQueryParams(this.props.location.search);
const { originalProject, newProject } = await this.fetchProjectsToCompare(
originalProjectName,
newProjectName,
@ -100,20 +101,14 @@ export default class CompareSubtestDistributionView extends React.Component {
return [originalSyncPromise, newSyncPromise];
};
fetchAllRepositories = async () => {
const loadRepositories = RepositoryModel.getList();
const results = await Promise.all([loadRepositories]);
return results[0];
};
fetchProjectsToCompare = async (originalProjectName, newProjectName) => {
const allRepos = await this.fetchAllRepositories();
const { projects } = this.props;
const originalProject = RepositoryModel.getRepo(
originalProjectName,
allRepos,
projects,
);
const newProject = RepositoryModel.getRepo(newProjectName, allRepos);
const newProject = RepositoryModel.getRepo(newProjectName, projects);
return { originalProject, newProject };
};
@ -155,7 +150,7 @@ export default class CompareSubtestDistributionView extends React.Component {
newRevision,
originalSubtestSignature,
newSubtestSignature,
} = this.props.$stateParams;
} = parseQueryParams(this.props.location.search);
return (
originalRevision &&
@ -201,21 +196,3 @@ export default class CompareSubtestDistributionView extends React.Component {
);
}
}
CompareSubtestDistributionView.propTypes = {
$stateParams: PropTypes.shape({
originalProject: PropTypes.string,
newProject: PropTypes.string,
originalRevision: PropTypes.string,
newRevision: PropTypes.string,
originalResultSet: PropTypes.object,
newResultSet: PropTypes.object,
originalSubtestSignature: PropTypes.string,
newSubtestSignature: PropTypes.string,
}).isRequired,
};
perf.component(
'compareSubtestDistributionView',
react2angular(CompareSubtestDistributionView, [], ['$stateParams']),
);

Просмотреть файл

@ -1,22 +1,20 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular/index.es2015';
import difference from 'lodash/difference';
import perf from '../../js/perf';
import { createQueryParams } from '../../helpers/url';
import {
createNoiseMetric,
getCounterMap,
createGraphsLinks,
onPermalinkClick,
} from '../helpers';
import { noiseMetricTitle } from '../constants';
import withValidation from '../Validation';
import CompareTableView from './CompareTableView';
// TODO remove $stateParams and $state after switching to react router
export class CompareSubtestsView extends React.PureComponent {
class CompareSubtestsView extends React.PureComponent {
createQueryParams = (parent_signature, repository, framework) => ({
parent_signature,
framework,
@ -43,7 +41,6 @@ export class CompareSubtestsView extends React.PureComponent {
if (originalRevision) {
originalParams.revision = originalRevision;
} else {
// can create a helper function for both views
const startDateMs =
(newResultSet.push_timestamp - timeRange.value) * 1000;
const endDateMs = newResultSet.push_timestamp * 1000;
@ -211,6 +208,8 @@ export class CompareSubtestsView extends React.PureComponent {
{...this.props}
getQueryParams={this.getQueryParams}
getDisplayResults={this.getDisplayResults}
onPermalinkClick={hashValue => onPermalinkClick(hashValue, this.props)}
hashFragment={this.props.location.hash}
hasSubtests
/>
);
@ -225,20 +224,14 @@ CompareSubtestsView.propTypes = {
originalProject: PropTypes.string,
newProject: PropTypes.string,
originalRevision: PropTypes.string,
projects: PropTypes.arrayOf(PropTypes.shape({})),
updateParams: PropTypes.func.isRequired,
newSignature: PropTypes.string,
originalSignature: PropTypes.string,
}),
$stateParams: PropTypes.shape({}),
$state: PropTypes.shape({}),
user: PropTypes.shape({}).isRequired,
};
CompareSubtestsView.defaultProps = {
validated: PropTypes.shape({}),
$stateParams: null,
$state: null,
};
const requiredParams = new Set([
@ -249,11 +242,4 @@ const requiredParams = new Set([
'newSignature',
]);
const compareSubtestsView = withValidation(requiredParams)(CompareSubtestsView);
perf.component(
'compareSubtestsView',
react2angular(compareSubtestsView, ['user'], ['$stateParams', '$state']),
);
export default compareSubtestsView;
export default withValidation({ requiredParams })(CompareSubtestsView);

Просмотреть файл

@ -9,12 +9,12 @@ import {
faHashtag,
} from '@fortawesome/free-solid-svg-icons';
import JobModel from '../../models/job';
import SimpleTooltip from '../../shared/SimpleTooltip';
import { displayNumber } from '../helpers';
import { compareTableText } from '../constants';
import ProgressBar from '../ProgressBar';
import { hashFunction } from '../../helpers/utils';
import JobModel from '../../models/job';
import TableAverage from './TableAverage';
@ -33,7 +33,7 @@ export default class CompareTable extends React.PureComponent {
displayNumber(percentage),
)}% ${improvement ? 'better' : 'worse'})`;
// humane readable signature name
// human readable signature name
getSignatureName = (testName, platformName) =>
[testName, platformName].filter(item => item !== null).join(' ');
@ -100,14 +100,18 @@ export default class CompareTable extends React.PureComponent {
<tr className="subtest-header bg-lightgray">
<th className="text-left">
<span>{testName}</span>
<Button
className="permalink p-0 ml-1"
color="link"
onClick={() => onPermalinkClick(this.getHashBasedId(testName))}
title="Permalink to this test table"
>
<FontAwesomeIcon icon={faHashtag} />
</Button>
{onPermalinkClick && (
<Button
className="permalink p-0 ml-1"
color="link"
onClick={() =>
onPermalinkClick(this.getHashBasedId(testName))
}
title="Permalink to this test table"
>
<FontAwesomeIcon icon={faHashtag} />
</Button>
)}
</th>
<th className="table-width-lg">Base</th>
{/* empty for less than/greater than data */}
@ -145,20 +149,22 @@ export default class CompareTable extends React.PureComponent {
<th className="text-left font-weight-normal pl-1">
{rowLevelResults.name}
<span className="result-links">
<span>
<Button
className="permalink p-0 ml-1"
color="link"
onClick={() =>
onPermalinkClick(
this.getHashBasedId(testName, rowLevelResults.name),
)
}
title="Permalink to this test"
>
<FontAwesomeIcon icon={faHashtag} />
</Button>
</span>
{onPermalinkClick && (
<span>
<Button
className="permalink p-0 ml-1"
color="link"
onClick={() =>
onPermalinkClick(
this.getHashBasedId(testName, rowLevelResults.name),
)
}
title="Permalink to this test"
>
<FontAwesomeIcon icon={faHashtag} />
</Button>
</span>
)}
{rowLevelResults.links &&
rowLevelResults.links.map(link => (
<span key={link.title}>
@ -289,20 +295,15 @@ CompareTable.propTypes = {
data: PropTypes.arrayOf(PropTypes.shape({})),
testName: PropTypes.string.isRequired,
hashFunction: PropTypes.func,
onPermalinkClick: PropTypes.func.isRequired,
user: PropTypes.shape({}).isRequired,
isBaseAggregate: PropTypes.bool.isRequired,
notify: PropTypes.func,
hasSubtests: PropTypes.bool,
retriggerJob: PropTypes.func,
onPermalinkClick: PropTypes.func,
getJob: PropTypes.func,
retriggerJob: PropTypes.func,
};
CompareTable.defaultProps = {
data: null,
hashFunction,
notify: null,
hasSubtests: false,
retriggerJob: JobModel.retrigger,
onPermalinkClick: undefined,
getJob: JobModel.get,
retriggerJob: JobModel.retrigger,
};

Просмотреть файл

@ -202,7 +202,7 @@ CompareTableControls.propTypes = {
PropTypes.shape({}),
PropTypes.bool,
]),
onPermalinkClick: PropTypes.func.isRequired,
onPermalinkClick: PropTypes.func,
};
CompareTableControls.defaultProps = {
@ -215,4 +215,5 @@ CompareTableControls.defaultProps = {
showOnlyNoise: undefined,
},
showTestsWithNoise: null,
onPermalinkClick: undefined,
};

Просмотреть файл

@ -1,14 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Alert, Col, Row, Container } from 'reactstrap';
import { Link } from 'react-router-dom';
import ErrorMessages from '../../shared/ErrorMessages';
import {
genericErrorMessage,
errorMessageClass,
compareDefaultTimeRange,
phTimeRanges,
} from '../../helpers/constants';
import { compareDefaultTimeRange, phTimeRanges } from '../constants';
import ErrorBoundary from '../../shared/ErrorBoundary';
import { getData } from '../../helpers/http';
import {
@ -25,7 +25,6 @@ import RevisionInformation from './RevisionInformation';
import CompareTableControls from './CompareTableControls';
import NoiseTable from './NoiseTable';
// TODO remove $stateParams and $state after switching to react router
export default class CompareTableView extends React.Component {
constructor(props) {
super(props);
@ -36,25 +35,35 @@ export default class CompareTableView extends React.Component {
failureMessages: [],
loading: false,
timeRange: this.setTimeRange(),
framework: getFrameworkData(this.props.validated),
framework: getFrameworkData(this.props),
title: '',
};
}
componentDidMount() {
this.getPerformanceData();
const { compareData, location } = this.props;
if (
compareData &&
compareData.size > 0 &&
location.pathname === '/compare'
) {
this.setState({ compareResults: compareData });
} else {
this.getPerformanceData();
}
}
componentDidUpdate(prevProps) {
const { loading } = this.state;
const { hashFragment } = this.props;
if (this.props !== prevProps) {
if (this.props.location.search !== prevProps.location.search) {
this.getPerformanceData();
}
if (!loading && hashFragment) {
scrollToLine(`#${hashFragment}`, 100);
scrollToLine(hashFragment, 100);
}
}
@ -146,7 +155,9 @@ export default class CompareTableView extends React.Component {
};
updateFramework = selection => {
const { frameworks, updateParams } = this.props.validated;
const { updateParams } = this.props.validated;
const { frameworks } = this.props;
const framework = frameworks.find(item => item.name === selection);
updateParams({ framework: framework.id });
@ -178,10 +189,14 @@ export default class CompareTableView extends React.Component {
newRevision,
originalResultSet,
newResultSet,
frameworks,
} = this.props.validated;
const { filterByFramework, hasSubtests, onPermalinkClick } = this.props;
const {
filterByFramework,
hasSubtests,
onPermalinkClick,
frameworks,
} = this.props;
const {
compareResults,
loading,
@ -198,7 +213,12 @@ export default class CompareTableView extends React.Component {
const compareDropdowns = [];
const params = { originalProject, newProject, newRevision };
const params = {
originalProject,
newProject,
newRevision,
framework: framework.id,
};
if (originalRevision) {
params.originalRevision = originalRevision;
@ -231,11 +251,14 @@ export default class CompareTableView extends React.Component {
>
<React.Fragment>
{hasSubtests && (
<p>
<a href={`perf.html#/compare${createQueryParams(params)}`}>
Show all tests and platforms
</a>
</p>
<Link
to={{
pathname: '/compare',
search: createQueryParams(params),
}}
>
Back to all tests and platforms
</Link>
)}
<div className="mx-auto">
@ -318,8 +341,6 @@ CompareTableView.propTypes = {
originalProject: PropTypes.string,
newProject: PropTypes.string,
originalRevision: PropTypes.string,
projects: PropTypes.arrayOf(PropTypes.shape({})),
frameworks: PropTypes.arrayOf(PropTypes.shape({})),
selectedTimeRange: PropTypes.string,
updateParams: PropTypes.func.isRequired,
originalSignature: PropTypes.string,
@ -332,9 +353,9 @@ CompareTableView.propTypes = {
getDisplayResults: PropTypes.func.isRequired,
getQueryParams: PropTypes.func.isRequired,
hasSubtests: PropTypes.bool,
onPermalinkClick: PropTypes.func.isRequired,
onPermalinkClick: PropTypes.func,
hashFragment: PropTypes.string,
$stateParams: PropTypes.shape({}).isRequired,
frameworks: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
};
CompareTableView.defaultProps = {
@ -343,4 +364,5 @@ CompareTableView.defaultProps = {
validated: PropTypes.shape({}),
hasSubtests: false,
hashFragment: '',
onPermalinkClick: undefined,
};

Просмотреть файл

@ -1,23 +1,20 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular/index.es2015';
import difference from 'lodash/difference';
import perf from '../../js/perf';
import { createQueryParams } from '../../helpers/url';
import { phTimeRanges } from '../../helpers/constants';
import {
createNoiseMetric,
getCounterMap,
createGraphsLinks,
onPermalinkClick,
} from '../helpers';
import { noiseMetricTitle } from '../constants';
import { noiseMetricTitle, phTimeRanges } from '../constants';
import withValidation from '../Validation';
import CompareTableView from './CompareTableView';
// TODO remove $location, $scope, $stateParams and $state after switching to react router
export class CompareView extends React.PureComponent {
class CompareView extends React.PureComponent {
getInterval = (oldTimestamp, newTimestamp) => {
const now = new Date().getTime() / 1000;
let timeRange = Math.min(oldTimestamp, newTimestamp);
@ -106,6 +103,7 @@ export class CompareView extends React.PureComponent {
} else {
params.selectedTimeRange = timeRange.value;
}
const detailsLink = `perf.html#/comparesubtest${createQueryParams(
params,
)}`;
@ -215,6 +213,7 @@ export class CompareView extends React.PureComponent {
compareResults = new Map([...compareResults.entries()].sort());
const updates = { compareResults, testsWithNoise, loading: false };
this.props.updateAppState({ compareData: compareResults });
const resultsArr = Array.from(compareResults.keys());
const testsNoResults = difference(tableNames, resultsArr)
@ -228,17 +227,7 @@ export class CompareView extends React.PureComponent {
return updates;
};
onPermalinkClick = hashBasedValue => {
const { $location, $scope } = this.props;
$location.hash(hashBasedValue);
$scope.$apply();
};
getHashFragment = () => {
const { $location } = this.props;
return $location.hash();
};
getHashFragment = () => this.props.location.hash;
render() {
return (
@ -246,8 +235,8 @@ export class CompareView extends React.PureComponent {
{...this.props}
getQueryParams={this.getQueryParams}
getDisplayResults={this.getDisplayResults}
onPermalinkClick={this.onPermalinkClick}
hashFragment={this.getHashFragment()}
onPermalinkClick={hashValue => onPermalinkClick(hashValue, this.props)}
hashFragment={this.props.location.hash}
filterByFramework
/>
);
@ -262,20 +251,13 @@ CompareView.propTypes = {
originalProject: PropTypes.string,
newProject: PropTypes.string,
originalRevision: PropTypes.string,
projects: PropTypes.arrayOf(PropTypes.shape({})),
frameworks: PropTypes.arrayOf(PropTypes.shape({})),
framework: PropTypes.string,
updateParams: PropTypes.func.isRequired,
}),
$stateParams: PropTypes.shape({}),
$state: PropTypes.shape({}),
user: PropTypes.shape({}).isRequired,
};
CompareView.defaultProps = {
validated: PropTypes.shape({}),
$stateParams: null,
$state: null,
};
const requiredParams = new Set([
@ -284,15 +266,4 @@ const requiredParams = new Set([
'newRevision',
]);
const compareView = withValidation(requiredParams)(CompareView);
perf.component(
'compareView',
react2angular(
compareView,
['user'],
['$location', '$scope', '$stateParams', '$state'],
),
);
export default compareView;
export default withValidation({ requiredParams })(CompareView);

Просмотреть файл

@ -10,7 +10,6 @@ import { createApiUrl, perfSummaryEndpoint } from '../../helpers/url';
import { noDataFoundMessage } from '../constants';
import LoadingSpinner from '../../shared/LoadingSpinner';
// TODO remove $stateParams after switching to react router
export default class ReplicatesGraph extends React.Component {
// TODO: sync parent with children IRT dataLoading
constructor(props) {

Просмотреть файл

@ -11,7 +11,7 @@ function getRevisionSpecificDetails(
resultSet,
selectedTimeRange = undefined,
) {
const truncatedRevision = revision.substring(0, 12);
const truncatedRevision = revision ? revision.substring(0, 12) : '';
const baselineOrNew = isBaseline || selectedTimeRange ? 'Base' : 'New';
return (

Просмотреть файл

@ -200,7 +200,7 @@ export default class SelectorCard extends React.Component {
missingRevision,
} = this.props;
return (
<Col sm="4" className="p-2">
<Col sm="4" className="p-2 text-left">
<Card className="card-height">
<CardHeader className="bg-lightgray">{title}</CardHeader>
<CardBody>

Просмотреть файл

@ -64,3 +64,28 @@ export const graphColors = [
['darkorchid', '#9932cc'],
['blue', '#1752b8'],
];
export const phFrameworksWithRelatedBranches = [
1, // talos
10, // raptor
11, // js-bench
12, // devtools
];
export const phTimeRanges = [
{ value: 86400, text: 'Last day' },
{ value: 86400 * 2, text: 'Last 2 days' },
{ value: 604800, text: 'Last 7 days' },
{ value: 1209600, text: 'Last 14 days' },
{ value: 2592000, text: 'Last 30 days' },
{ value: 5184000, text: 'Last 60 days' },
{ value: 7776000, text: 'Last 90 days' },
{ value: 31536000, text: 'Last year' },
];
export const phDefaultTimeRangeValue = 1209600;
export const compareDefaultTimeRange = {
value: 86400 * 2,
text: 'Last 2 days',
};

Просмотреть файл

@ -1,26 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular/index.es2015';
import { Container, Col, Row } from 'reactstrap';
import unionBy from 'lodash/unionBy';
import queryString from 'query-string';
import { getData, processResponse, processErrors } from '../../helpers/http';
import {
getApiUrl,
repoEndpoint,
createApiUrl,
perfSummaryEndpoint,
createQueryParams,
parseQueryParams,
updateQueryParams,
} from '../../helpers/url';
import {
phTimeRanges,
phDefaultTimeRangeValue,
genericErrorMessage,
errorMessageClass,
} from '../../helpers/constants';
import perf from '../../js/perf';
import { processSelectedParam } from '../helpers';
import { endpoints, graphColors } from '../constants';
import {
endpoints,
graphColors,
phTimeRanges,
phDefaultTimeRangeValue,
} from '../constants';
import ErrorMessages from '../../shared/ErrorMessages';
import ErrorBoundary from '../../shared/ErrorBoundary';
import LoadingSpinner from '../../shared/LoadingSpinner';
@ -34,8 +37,6 @@ class GraphsView extends React.Component {
super(props);
this.state = {
timeRange: this.getDefaultTimeRange(),
frameworks: [],
projects: [],
zoom: {},
selectedDataPoint: null,
highlightAlerts: true,
@ -51,32 +52,19 @@ class GraphsView extends React.Component {
}
async componentDidMount() {
this.getData();
this.checkQueryParams();
}
getDefaultTimeRange = () => {
const { $stateParams } = this.props;
const { location } = this.props;
const { timerange } = parseQueryParams(location.search);
const defaultValue = $stateParams.timerange
? parseInt($stateParams.timerange, 10)
const defaultValue = timerange
? parseInt(timerange, 10)
: phDefaultTimeRangeValue;
return phTimeRanges.find(time => time.value === defaultValue);
};
async getData() {
const [projects, frameworks] = await Promise.all([
getData(getApiUrl(repoEndpoint)),
getData(getApiUrl(endpoints.frameworks)),
]);
const updates = {
...processResponse(projects, 'projects'),
...processResponse(frameworks, 'frameworks'),
};
this.setState(updates);
}
checkQueryParams = () => {
const {
series,
@ -84,7 +72,7 @@ class GraphsView extends React.Component {
selected,
highlightAlerts,
highlightedRevisions,
} = this.props.$stateParams;
} = queryString.parse(this.props.location.search);
const updates = {};
@ -271,11 +259,7 @@ class GraphsView extends React.Component {
parseSeriesParam = series =>
series.map(encodedSeries => {
const partialSeriesString = decodeURIComponent(encodedSeries).replace(
/[[\]"]/g,
'',
);
const partialSeriesArray = partialSeriesString.split(',');
const partialSeriesArray = encodedSeries.split(',');
const partialSeriesObject = {
repository_name: partialSeriesArray[0],
// TODO deprecate signature_hash
@ -299,14 +283,11 @@ class GraphsView extends React.Component {
};
updateParams = params => {
const { transitionTo, current } = this.props.$state;
const { location, history } = this.props;
let newQueryString = queryString.stringify(params);
newQueryString = newQueryString.replace(/%2C/g, ',');
transitionTo('graphs', params, {
location: true,
inherit: true,
relative: current,
notify: false,
});
updateQueryParams(newQueryString, history, location);
};
changeParams = () => {
@ -325,21 +306,28 @@ class GraphsView extends React.Component {
);
const params = {
series: newSeries,
highlightedRevisions: highlightedRevisions.filter(rev => rev.length),
highlightAlerts: +highlightAlerts,
timerange: timeRange.value,
zoom,
};
const newHighlightedRevisions = highlightedRevisions.filter(
rev => rev.length,
);
if (newHighlightedRevisions.length) {
params.highlightedRevisions = newHighlightedRevisions;
}
if (!selectedDataPoint) {
params.selected = null;
delete params.selected;
} else {
const { signature_id, pushId, x, y } = selectedDataPoint;
params.selected = [signature_id, pushId, x, y].join(',');
}
if (Object.keys(zoom).length === 0) {
params.zoom = null;
delete params.zoom;
} else {
params.zoom = [...zoom.x.map(z => z.getTime()), ...zoom.y].toString();
}
@ -350,8 +338,6 @@ class GraphsView extends React.Component {
render() {
const {
timeRange,
projects,
frameworks,
testData,
highlightAlerts,
highlightedRevisions,
@ -365,6 +351,7 @@ class GraphsView extends React.Component {
visibilityChanged,
} = this.state;
const { projects, frameworks } = this.props;
return (
<ErrorBoundary
errorClasses={errorMessageClass}
@ -464,7 +451,7 @@ class GraphsView extends React.Component {
}
GraphsView.propTypes = {
$stateParams: PropTypes.shape({
location: PropTypes.shape({
zoom: PropTypes.string,
selected: PropTypes.string,
highlightAlerts: PropTypes.string,
@ -477,21 +464,11 @@ GraphsView.propTypes = {
PropTypes.arrayOf(PropTypes.string),
]),
}),
$state: PropTypes.shape({
current: PropTypes.shape({}),
transitionTo: PropTypes.func,
}),
user: PropTypes.shape({}).isRequired,
};
GraphsView.defaultProps = {
$stateParams: undefined,
$state: undefined,
location: undefined,
};
perf.component(
'graphsView',
react2angular(GraphsView, ['user'], ['$stateParams', '$state']),
);
export default GraphsView;

Просмотреть файл

@ -10,7 +10,7 @@ import {
Input,
} from 'reactstrap';
import { phTimeRanges } from '../../helpers/constants';
import { phTimeRanges } from '../constants';
import DropdownMenuItems from '../../shared/DropdownMenuItems';
import TestDataModal from './TestDataModal';
@ -34,21 +34,13 @@ export default class GraphsViewControls extends React.Component {
highlightedRevisions,
updateTimeRange,
hasNoData,
projects,
frameworks,
toggle,
showModal,
} = this.props;
return (
<Container fluid className="justify-content-start">
{projects.length > 0 && frameworks.length > 0 && (
<TestDataModal
showModal={showModal}
toggle={toggle}
{...this.props}
/>
)}
<TestDataModal showModal={showModal} toggle={toggle} {...this.props} />
<Row className="pb-3">
<Col sm="auto" className="pl-0 py-2 pr-2" key={timeRange}>
<UncontrolledDropdown
@ -134,21 +126,17 @@ GraphsViewControls.propTypes = {
]).isRequired,
updateTimeRange: PropTypes.func.isRequired,
hasNoData: PropTypes.bool.isRequired,
projects: PropTypes.arrayOf(PropTypes.shape({})),
getTestData: PropTypes.func.isRequired,
options: PropTypes.shape({
option: PropTypes.string,
relatedSeries: PropTypes.shape({}),
}),
testData: PropTypes.arrayOf(PropTypes.shape({})),
frameworks: PropTypes.arrayOf(PropTypes.shape({})),
showModal: PropTypes.bool,
toggle: PropTypes.func.isRequired,
};
GraphsViewControls.defaultProps = {
frameworks: [],
projects: [],
options: undefined,
testData: [],
showModal: false,

Просмотреть файл

@ -8,11 +8,7 @@ import PerfSeriesModel, {
getSeriesName,
getTestName,
} from '../models/perfSeries';
import {
phFrameworksWithRelatedBranches,
phTimeRanges,
thPerformanceBranches,
} from '../helpers/constants';
import { thPerformanceBranches } from '../helpers/constants';
import {
endpoints,
@ -21,6 +17,8 @@ import {
noiseMetricTitle,
summaryStatusMap,
alertStatusMap,
phFrameworksWithRelatedBranches,
phTimeRanges,
} from './constants';
export const displayNumber = input =>
@ -114,7 +112,7 @@ const analyzeSet = (values, testName) => {
average,
stddev,
stddevPct: Math.round(calcPercentOf(stddev, average) * 100) / 100,
// TODO verify this is needed
// We use slice to keep the original values at their original order
// in case the order is important elsewhere.
runs: values.slice().sort(numericCompare),
@ -171,7 +169,6 @@ export const getCounterMap = function getCounterMap(
originalData,
newData,
) {
// TODO setting this value seems a bit odd, look into how its being used
const cmap = { isEmpty: false };
const hasOrig = originalData && originalData.values.length;
const hasNew = newData && newData.values.length;
@ -278,7 +275,7 @@ export const getCounterMap = function getCounterMap(
return cmap;
};
// TODO look into using signature_id instead of the hash
// TODO change usage of signature_hash to signature.id
export const getGraphsLink = function getGraphsLink(
seriesList,
resultSets,
@ -329,7 +326,6 @@ export const createNoiseMetric = function createNoiseMetric(
return compareResults;
};
// TODO
export const createGraphsLinks = (
validatedProps,
links,
@ -368,7 +364,7 @@ export const createGraphsLinks = (
return links;
};
// TODO change all usage of signature_hash to signature.id
// TODO change usage of signature_hash to signature.id
// for originalSignature and newSignature query params
const Alert = (alertData, optionCollectionMap) => ({
...alertData,
@ -377,7 +373,7 @@ const Alert = (alertData, optionCollectionMap) => ({
}),
});
// TODO look into using signature_id instead of the hash and remove all other params
// TODO change usage of signature_hash to signature.id
export const getGraphsURL = (
alert,
timeRange,
@ -386,11 +382,9 @@ export const getGraphsURL = (
) => {
let url = `#/graphs?timerange=${timeRange}&series=${alertRepository},${alert.series_signature.id},1,${alert.series_signature.framework_id}`;
// TODO deprecate usage of signature hash
// automatically add related branches (we take advantage of
// the otherwise rather useless signature hash to avoid having to fetch this
// information from the server)
if (phFrameworksWithRelatedBranches.includes(performanceFrameworkId)) {
const branches =
alertRepository === 'mozilla-beta'
@ -537,11 +531,11 @@ export const convertParams = (params, value) =>
Boolean(params[value] !== undefined && parseInt(params[value], 10));
export const getFrameworkData = props => {
const { framework, frameworks } = props;
const { validated, frameworks } = props;
if (framework) {
if (validated.framework) {
const frameworkObject = frameworks.find(
item => item.id === parseInt(framework, 10),
item => item.id === parseInt(validated.framework, 10),
);
return frameworkObject;
}
@ -619,3 +613,9 @@ export const getSeriesData = async (
return updates;
};
export const onPermalinkClick = (hashBasedValue, props) => {
const { history, location } = props;
history.replace(`${location.pathname}${location.search}#${hashBasedValue}`);
};

12
ui/perfherder/index.jsx Normal file
Просмотреть файл

@ -0,0 +1,12 @@
import React from 'react';
import { render } from 'react-dom';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'react-table/react-table.css';
import '../css/treeherder-global.css';
import '../css/treeherder-navbar.css';
import '../css/perf.css';
import App from './App';
render(<App />, document.getElementById('root'));

Просмотреть файл

Просмотреть файл

@ -1154,11 +1154,6 @@
"@testing-library/dom" "^6.1.0"
"@types/testing-library__react" "^9.1.0"
"@types/angular@*", "@types/angular@^1.6.39":
version "1.6.55"
resolved "https://registry.yarnpkg.com/@types/angular/-/angular-1.6.55.tgz#1d745af6deadcb5c2c0f6f6e9d85bc18c25a520b"
integrity sha512-jpQ5K6t76thd9Ook5x4Xj9K462hMje6/Q84SFxglBmaW8HOLblVnALpvg6w6aV3Oe6o9jWXnYH8gHHKrfSRxNQ==
"@types/babel__core@^7.1.0":
version "7.1.0"
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.0.tgz#710f2487dda4dcfd010ca6abb2b4dc7394365c51"
@ -1231,18 +1226,6 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636"
integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==
"@types/lodash.frompairs@^4.0.5":
version "4.0.6"
resolved "https://registry.yarnpkg.com/@types/lodash.frompairs/-/lodash.frompairs-4.0.6.tgz#09b082c10fa753dc2001302b75ac79ca1e0a9ea3"
integrity sha512-rwCUf4NMKhXpiVjL/RXP8YOk+rd02/J4tACADEgaMXRVnzDbSSlBMKFZoX/ARmHVLg3Qc98Um4PErGv8FbxU7w==
dependencies:
"@types/lodash" "*"
"@types/lodash@*", "@types/lodash@^4.14.85":
version "4.14.136"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.136.tgz#413e85089046b865d960c9ff1d400e04c31ab60f"
integrity sha512-0GJhzBdvsW2RUccNHOBkabI8HZVdOXmXbXhuKlDEd5Vv12P7oAVGfomGp3Ne21o5D/qu1WmthlNKFaoZJJeErA==
"@types/minimatch@*":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
@ -1339,13 +1322,6 @@
lodash.unescape "4.0.1"
semver "5.5.0"
"@uirouter/angularjs@0.4.3":
version "0.4.3"
resolved "https://registry.yarnpkg.com/@uirouter/angularjs/-/angularjs-0.4.3.tgz#7e2630c59b2bd69ca485ff124f53b0169edddf39"
integrity sha512-jLmZ+VcsvS63E01wJWEqNLND6/6Ju9dZP6t21T+v6q8s9+Xzr8RX6QrrnRt35S0ARugFwJxFlmNFZSIef3jvDw==
dependencies:
angular "^1.0.8"
"@webassemblyjs/ast@1.8.5":
version "1.8.5"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359"
@ -1596,21 +1572,6 @@ ajv@6.10.2, ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.5.5, ajv@^6.9.1:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
angular-clipboard@1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/angular-clipboard/-/angular-clipboard-1.7.0.tgz#9621a6ce66eab1ea9549aa8bfb3b71352307554f"
integrity sha512-4/eg3zZw1MJpIsMc+mWzeVNyWBu8YWpXPTdmbgyPRp/6f0xB6I3XR2iC6Mb4mg/5E9q6exCd0sX2yiIsw+ZLJw==
angular1-ui-bootstrap4@2.4.22:
version "2.4.22"
resolved "https://registry.yarnpkg.com/angular1-ui-bootstrap4/-/angular1-ui-bootstrap4-2.4.22.tgz#378697405c957b96f947f42322f36660cd3fc88d"
integrity sha1-N4aXQFyVe5b5R/QjIvNmYM0/yI0=
angular@1.7.8, angular@>=1.5, angular@>=1.5.0, angular@^1.0.8:
version "1.7.8"
resolved "https://registry.yarnpkg.com/angular/-/angular-1.7.8.tgz#b77ede272ce1b261e3be30c1451a0b346905a3c9"
integrity sha512-wtef/y4COxM7ZVhddd7JtAAhyYObq9YXKar9tsW7558BImeVYteJiTxCKeJOL45lJ/+7B4wrAC49j8gTFYEthg==
ansi-colors@^3.0.0:
version "3.2.4"
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf"
@ -5802,11 +5763,6 @@ jest@24.9.0:
import-local "^2.0.0"
jest-cli "^24.9.0"
jquery.flot@0.8.3:
version "0.8.3"
resolved "https://registry.yarnpkg.com/jquery.flot/-/jquery.flot-0.8.3.tgz#81d2ec4ffdf0dee729c8a442e8faa399e9b48207"
integrity sha512-/tEE8J5NjwvStHDaCHkvTJpD7wDS4hE1OEL8xEmhgQfUe0gLUem923PIceNez1mz4yBNx6Hjv7pJcowLNd+nbg==
jquery@3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.4.1.tgz#714f1f8d9dde4bdfa55764ba37ef214630d80ef2"
@ -6107,11 +6063,6 @@ lodash.flattendeep@^4.4.0:
resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=
lodash.frompairs@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.frompairs/-/lodash.frompairs-4.0.1.tgz#bc4e5207fa2757c136e573614e9664506b2b1bd2"
integrity sha1-vE5SB/onV8E25XNhTpZkUGsrG9I=
lodash.isarguments@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
@ -6637,16 +6588,6 @@ ng-text-truncate-2@1.0.1:
resolved "https://registry.yarnpkg.com/ng-text-truncate-2/-/ng-text-truncate-2-1.0.1.tgz#167b92b04f092e940cc6d60a336fa604570392bd"
integrity sha1-FnuSsE8JLpQMxtYKM2+mBFcDkr0=
ngcomponent@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/ngcomponent/-/ngcomponent-4.1.0.tgz#793e379138f552ea0cd2c767ad0aa7057678e228"
integrity sha512-cGL3iVoqMWTpCfaIwgRKhdaGqiy2Z+CCG0cVfjlBvdqE8saj8xap9B4OTf+qwObxLVZmDTJPDgx3bN6Q/lZ7BQ==
dependencies:
"@types/angular" "^1.6.39"
"@types/lodash" "^4.14.85"
angular ">=1.5.0"
lodash "^4.17.4"
nice-try@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
@ -7935,16 +7876,6 @@ react-virtualized@^9.21.0:
prop-types "^15.6.0"
react-lifecycles-compat "^3.0.4"
react2angular@4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/react2angular/-/react2angular-4.0.6.tgz#ec49ef834d101c9a320e25229fc5afa5b29edc4f"
integrity sha512-MDl2WRoTyu7Gyh4+FAIlmsM2mxIa/DjSz6G/d90L1tK8ZRubqVEayKF6IPyAruC5DMhGDVJ7tlAIcu/gMNDjXg==
dependencies:
"@types/lodash.frompairs" "^4.0.5"
angular ">=1.5"
lodash.frompairs "^4.0.1"
ngcomponent "^4.1.0"
react@16.9.0:
version "16.9.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.9.0.tgz#40ba2f9af13bc1a38d75dbf2f4359a5185c4f7aa"