зеркало из https://github.com/mozilla/treeherder.git
Bug 1507172 - Use Prettier for formatting JS/JSX (#4276)
Since it's more reliable (and strict) at code formatting than ESLint. We use it via an ESLint plugin, and so disable the style-related AirBnB preset rules, leaving the AirBnB guide to handle only correctness and best practices rules. It's highly encouraged to use an IDE integration or Git commit hook to run Prettier (or `yarn lint --fix`) automatically. See: * https://prettier.io/docs/en/editors.html * https://prettier.io/docs/en/precommit.html We may consider enabling a git commit hook out of the box (using eg Husky) in the future, however they have previously been known to interfere with partial-staging workflows, so would need to test the fixes they have made for them thoroughly first. In future PRs we may also want to start formatting JSON/CSS/Markdown using Prettier too.
This commit is contained in:
Родитель
4db0cfa973
Коммит
65b7f4ab45
35
.eslintrc.js
35
.eslintrc.js
|
@ -1,6 +1,17 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
extends: 'eslint-config-airbnb',
|
||||
extends: [
|
||||
'eslint-config-airbnb',
|
||||
// We use Prettier instead of AirBnb for style-related rules (see .prettierrc.js).
|
||||
process.env.NODE_ENV === 'development'
|
||||
? // Disables the AirBnB style rules but does not enable Prettier
|
||||
// (to reduce the amount of console noise when using `yarn start`).
|
||||
'prettier'
|
||||
: // The above plus enables the prettier rule.
|
||||
'plugin:prettier/recommended',
|
||||
// Disable React-related AirBnB style rules.
|
||||
'prettier/react',
|
||||
],
|
||||
parser: 'babel-eslint',
|
||||
settings: {
|
||||
react: {
|
||||
|
@ -17,17 +28,12 @@ module.exports = {
|
|||
'class-methods-use-this': 'off',
|
||||
'consistent-return': 'off',
|
||||
'default-case': 'off',
|
||||
'function-paren-newline': 'off',
|
||||
'implicit-arrow-linebreak': 'off',
|
||||
// Indentation is disabled pending a switch from 4 to 2 space for JS.
|
||||
indent: 'off',
|
||||
'jsx-a11y/anchor-is-valid': 'off',
|
||||
'jsx-a11y/click-events-have-key-events': 'off',
|
||||
'jsx-a11y/label-has-associated-control': 'off',
|
||||
'jsx-a11y/label-has-for': 'off',
|
||||
'jsx-a11y/no-noninteractive-element-interactions': 'off',
|
||||
'jsx-a11y/no-static-element-interactions': 'off',
|
||||
'max-len': 'off',
|
||||
'no-alert': 'off',
|
||||
'no-continue': 'off',
|
||||
'no-param-reassign': 'off',
|
||||
|
@ -35,17 +41,11 @@ module.exports = {
|
|||
'no-restricted-syntax': 'off',
|
||||
'no-shadow': 'off',
|
||||
'no-underscore-dangle': 'off',
|
||||
'object-curly-newline': 'off',
|
||||
'operator-linebreak': 'off',
|
||||
'padded-blocks': 'off',
|
||||
'prefer-promise-reject-errors': 'off',
|
||||
'react/button-has-type': 'off',
|
||||
'react/default-props-match-prop-types': 'off',
|
||||
'react/destructuring-assignment': 'off',
|
||||
'react/forbid-prop-types': 'off',
|
||||
'react/jsx-closing-tag-location': 'off',
|
||||
'react/jsx-one-expression-per-line': 'off',
|
||||
'react/jsx-wrap-multilines': 'off',
|
||||
'react/no-access-state-in-setstate': 'off',
|
||||
// Override AirBnB's config for this rule to make it more strict.
|
||||
// https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/order.md
|
||||
|
@ -56,4 +56,15 @@ module.exports = {
|
|||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
// Exclude our legacy JS from prettier since it will be rewritten when converted to React.
|
||||
// This directory is already ignored in .prettierignore but have to repeat here due to:
|
||||
// https://github.com/prettier/eslint-plugin-prettier/issues/126
|
||||
files: ['ui/js/**'],
|
||||
rules: {
|
||||
'prettier/prettier': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
159
.neutrinorc.js
159
.neutrinorc.js
|
@ -42,97 +42,104 @@ module.exports = {
|
|||
tests: 'tests/ui/',
|
||||
},
|
||||
use: [
|
||||
process.env.NODE_ENV === 'development' && ['@neutrinojs/eslint', {
|
||||
eslint: {
|
||||
// Treat ESLint errors as warnings so they don't block the webpack build.
|
||||
// Remove if/when changed in @neutrinojs/eslint.
|
||||
emitWarning: true,
|
||||
// We manage our lint config in .eslintrc.js instead of here.
|
||||
useEslintrc: true,
|
||||
process.env.NODE_ENV === 'development' && [
|
||||
'@neutrinojs/eslint',
|
||||
{
|
||||
eslint: {
|
||||
// Treat ESLint errors as warnings so they don't block the webpack build.
|
||||
// Remove if/when changed in @neutrinojs/eslint.
|
||||
emitWarning: true,
|
||||
// We manage our lint config in .eslintrc.js instead of here.
|
||||
useEslintrc: true,
|
||||
},
|
||||
},
|
||||
}],
|
||||
['@neutrinojs/react', {
|
||||
devServer: {
|
||||
historyApiFallback: false,
|
||||
open: !process.env.MOZ_HEADLESS,
|
||||
// Remove when enabled by default (https://github.com/neutrinojs/neutrino/issues/1131).
|
||||
overlay: true,
|
||||
proxy: {
|
||||
// Proxy any paths not recognised by webpack to the specified backend.
|
||||
'*': {
|
||||
changeOrigin: true,
|
||||
headers: {
|
||||
// Prevent Django CSRF errors, whilst still making it clear
|
||||
// that the requests were from local development.
|
||||
referer: `${BACKEND}/webpack-dev-server`,
|
||||
},
|
||||
target: BACKEND,
|
||||
onProxyRes: (proxyRes) => {
|
||||
// Strip the cookie `secure` attribute, otherwise production's cookies
|
||||
// will be rejected by the browser when using non-HTTPS localhost:
|
||||
// https://github.com/nodejitsu/node-http-proxy/pull/1166
|
||||
const removeSecure = str => str.replace(/; secure/i, '');
|
||||
const cookieHeader = proxyRes.headers['set-cookie'];
|
||||
if (cookieHeader) {
|
||||
proxyRes.headers['set-cookie'] = Array.isArray(cookieHeader)
|
||||
? cookieHeader.map(removeSecure)
|
||||
: removeSecure(cookieHeader);
|
||||
}
|
||||
],
|
||||
[
|
||||
'@neutrinojs/react',
|
||||
{
|
||||
devServer: {
|
||||
historyApiFallback: false,
|
||||
open: !process.env.MOZ_HEADLESS,
|
||||
// Remove when enabled by default (https://github.com/neutrinojs/neutrino/issues/1131).
|
||||
overlay: true,
|
||||
proxy: {
|
||||
// Proxy any paths not recognised by webpack to the specified backend.
|
||||
'*': {
|
||||
changeOrigin: true,
|
||||
headers: {
|
||||
// Prevent Django CSRF errors, whilst still making it clear
|
||||
// that the requests were from local development.
|
||||
referer: `${BACKEND}/webpack-dev-server`,
|
||||
},
|
||||
target: BACKEND,
|
||||
onProxyRes: proxyRes => {
|
||||
// Strip the cookie `secure` attribute, otherwise production's cookies
|
||||
// will be rejected by the browser when using non-HTTPS localhost:
|
||||
// https://github.com/nodejitsu/node-http-proxy/pull/1166
|
||||
const removeSecure = str => str.replace(/; secure/i, '');
|
||||
const cookieHeader = proxyRes.headers['set-cookie'];
|
||||
if (cookieHeader) {
|
||||
proxyRes.headers['set-cookie'] = Array.isArray(cookieHeader)
|
||||
? cookieHeader.map(removeSecure)
|
||||
: removeSecure(cookieHeader);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
// Inside Vagrant filesystem watching has to be performed using polling mode,
|
||||
// since inotify doesn't work with Virtualbox shared folders.
|
||||
watchOptions: process.env.USE_WATCH_POLLING && {
|
||||
// Poll only once a second and ignore the node_modules folder to keep CPU usage down.
|
||||
poll: 1000,
|
||||
ignored: /node_modules/,
|
||||
},
|
||||
},
|
||||
// Inside Vagrant filesystem watching has to be performed using polling mode,
|
||||
// since inotify doesn't work with Virtualbox shared folders.
|
||||
watchOptions: process.env.USE_WATCH_POLLING && {
|
||||
// Poll only once a second and ignore the node_modules folder to keep CPU usage down.
|
||||
poll: 1000,
|
||||
ignored: /node_modules/,
|
||||
devtool: {
|
||||
// Enable source maps for `yarn build` too (but not on CI, since it doubles build times).
|
||||
production: process.env.CI ? false : 'source-map',
|
||||
},
|
||||
html: {
|
||||
// Disable the default viewport meta tag, since Treeherder doesn't work well at
|
||||
// small viewport sizes, so shouldn't use `width=device-width` (see bug 1505417).
|
||||
meta: false,
|
||||
},
|
||||
style: {
|
||||
// Disable Neutrino's CSS modules support, since we don't use it.
|
||||
modules: false,
|
||||
},
|
||||
targets: {
|
||||
browsers: [
|
||||
'last 1 Chrome versions',
|
||||
'last 1 Edge versions',
|
||||
'last 1 Firefox versions',
|
||||
'last 1 Safari versions',
|
||||
],
|
||||
},
|
||||
},
|
||||
devtool: {
|
||||
// Enable source maps for `yarn build` too (but not on CI, since it doubles build times).
|
||||
production: process.env.CI ? false : 'source-map',
|
||||
],
|
||||
[
|
||||
'@neutrinojs/copy',
|
||||
{
|
||||
patterns: ['ui/contribute.json', 'ui/revision.txt', 'ui/robots.txt'],
|
||||
},
|
||||
html: {
|
||||
// Disable the default viewport meta tag, since Treeherder doesn't work well at
|
||||
// small viewport sizes, so shouldn't use `width=device-width` (see bug 1505417).
|
||||
meta: false,
|
||||
},
|
||||
style: {
|
||||
// Disable Neutrino's CSS modules support, since we don't use it.
|
||||
modules: false,
|
||||
},
|
||||
targets: {
|
||||
browsers: [
|
||||
'last 1 Chrome versions',
|
||||
'last 1 Edge versions',
|
||||
'last 1 Firefox versions',
|
||||
'last 1 Safari versions',
|
||||
],
|
||||
},
|
||||
}],
|
||||
['@neutrinojs/copy', {
|
||||
patterns: [
|
||||
'ui/contribute.json',
|
||||
'ui/revision.txt',
|
||||
'ui/robots.txt',
|
||||
],
|
||||
}],
|
||||
(neutrino) => {
|
||||
],
|
||||
neutrino => {
|
||||
neutrino.config
|
||||
.plugin('provide')
|
||||
.use(require.resolve('webpack/lib/ProvidePlugin'), [{
|
||||
// Required since AngularJS and jquery.flot don't import jQuery themselves.
|
||||
jQuery: 'jquery',
|
||||
'window.jQuery': 'jquery',
|
||||
}]);
|
||||
.use(require.resolve('webpack/lib/ProvidePlugin'), [
|
||||
{
|
||||
// Required since AngularJS and jquery.flot don't import jQuery themselves.
|
||||
jQuery: 'jquery',
|
||||
'window.jQuery': 'jquery',
|
||||
},
|
||||
]);
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
// Fail the build if these file size thresholds (in bytes) are exceeded,
|
||||
// to help prevent unknowingly regressing the bundle size (bug 1384255).
|
||||
neutrino.config.performance
|
||||
.hints('error')
|
||||
.maxAssetSize(1.30 * 1024 * 1024)
|
||||
.maxAssetSize(1.3 * 1024 * 1024)
|
||||
.maxEntrypointSize(1.64 * 1024 * 1024);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
# Ignore our legacy JS since it will be rewritten when converted to React.
|
||||
ui/js/
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
endOfLine: 'lf',
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
};
|
|
@ -34,9 +34,18 @@ to reduce the line length guess-work when adding imports, even though it's not t
|
|||
UI
|
||||
--
|
||||
|
||||
We use the [Airbnb](https://github.com/airbnb/javascript) style guide for Javascript and validate it with ESlint (see Validating Javascript in the [Installation section](installation.html#validating-javascript)). For CSS, we use [reactstrap](https://reactstrap.github.io/) and Bootstrap's utility classes as much as possible before adding custom CSS to a style sheet. Any custom style that can be made reusable should be named generically and stored in the ``ui/css/treeherder-global.css`` file.
|
||||
We use Prettier for JS/JSX formatting and the [Airbnb](https://github.com/airbnb/javascript)
|
||||
guide for non-style related best practices. Both are validated using ESlint (see Validating
|
||||
Javascript in the [Installation section](installation.html#validating-javascript)).
|
||||
|
||||
We recommend that you [add Prettier to your editor/IDE](https://prettier.io/docs/en/editors.html)
|
||||
and enable "format on save" for the most seamless development workflow.
|
||||
|
||||
Imports in JS/JSX must be ordered like so (with newlines between each group):
|
||||
1. external modules (eg `'react'`)
|
||||
2. modules from a parent directory (eg `'../foo'`)
|
||||
3. "sibling" modules from the same or a sibling's directory (eg `'./bar'` or './bar/baz')
|
||||
|
||||
For CSS, we use [reactstrap](https://reactstrap.github.io/) and Bootstrap's utility classes as
|
||||
much as possible before adding custom CSS to a style sheet. Any custom style that can be made
|
||||
reusable should be named generically and stored in the ``ui/css/treeherder-global.css`` file.
|
||||
|
|
|
@ -55,18 +55,26 @@ production site. You do not need to set up the Vagrant VM unless making backend
|
|||
Validating JavaScript
|
||||
---------------------
|
||||
|
||||
We run our JavaScript code in the frontend through [eslint] to ensure
|
||||
We run our JavaScript code in the frontend through [ESLint] to ensure
|
||||
that new code has a consistent style and doesn't suffer from common
|
||||
errors. Eslint will run automatically when you build the JavaScript code
|
||||
or run the development server. A production build will fail if your code
|
||||
errors. ESLint will run automatically when you build the JavaScript code
|
||||
or run the development server. A production build will fail if your code
|
||||
does not match the style requirements.
|
||||
|
||||
To run eslint by itself, you may run the lint task:
|
||||
To run ESLint by itself, you may run the lint task:
|
||||
|
||||
```bash
|
||||
$ yarn lint
|
||||
```
|
||||
|
||||
Or to automatically fix issues found (where possible):
|
||||
|
||||
```bash
|
||||
$ yarn lint --fix
|
||||
```
|
||||
|
||||
See the [code style](code_style.html#ui) section for more details.
|
||||
|
||||
Running the unit tests
|
||||
----------------------
|
||||
|
||||
|
@ -86,7 +94,6 @@ $ yarn test:watch
|
|||
|
||||
The tests will perform an initial run and then re-execute each time a project file is changed.
|
||||
|
||||
Continue to the [Code Style](code_style.md) doc.
|
||||
|
||||
Server and Full-stack Development
|
||||
=================================
|
||||
|
@ -251,6 +258,6 @@ Continue to **Working with the Server** section after looking at the [Code Style
|
|||
[Node.js]: https://nodejs.org/en/download/current/
|
||||
[Yarn]: https://yarnpkg.com/en/docs/install
|
||||
[package.json]: https://github.com/mozilla/treeherder/blob/master/package.json
|
||||
[eslint]: https://eslint.org
|
||||
[ESLint]: https://eslint.org
|
||||
[Jasmine]: https://jasmine.github.io/
|
||||
[enzyme]: http://airbnb.io/enzyme/
|
||||
|
|
|
@ -18,13 +18,9 @@ webpackConfig.node.Buffer = true;
|
|||
webpackConfig.optimization.splitChunks = false;
|
||||
webpackConfig.optimization.runtimeChunk = false;
|
||||
|
||||
module.exports = (config) => {
|
||||
module.exports = config => {
|
||||
config.set({
|
||||
plugins: [
|
||||
'karma-webpack',
|
||||
'karma-firefox-launcher',
|
||||
'karma-jasmine',
|
||||
],
|
||||
plugins: ['karma-webpack', 'karma-firefox-launcher', 'karma-jasmine'],
|
||||
browsers: ['FirefoxHeadless'],
|
||||
frameworks: ['jasmine'],
|
||||
files: [
|
||||
|
|
|
@ -80,8 +80,10 @@
|
|||
"enzyme-adapter-react-16": "1.7.0",
|
||||
"eslint": "5.9.0",
|
||||
"eslint-config-airbnb": "17.1.0",
|
||||
"eslint-config-prettier": "3.3.0",
|
||||
"eslint-plugin-import": "2.14.0",
|
||||
"eslint-plugin-jsx-a11y": "6.1.2",
|
||||
"eslint-plugin-prettier": "3.0.0",
|
||||
"eslint-plugin-react": "7.11.1",
|
||||
"fetch-mock": "7.2.5",
|
||||
"jasmine-core": "3.3.0",
|
||||
|
@ -90,6 +92,7 @@
|
|||
"karma-firefox-launcher": "1.1.0",
|
||||
"karma-jasmine": "2.0.0",
|
||||
"karma-webpack": "4.0.0-beta.0",
|
||||
"prettier": "1.15.2",
|
||||
"webpack-dev-server": "3.1.10"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
|
@ -9,52 +9,57 @@ import FilterModel from '../../../../ui/models/filter';
|
|||
const { getJSONFixture } = window;
|
||||
|
||||
describe('Pushes context', () => {
|
||||
const repoName = 'mozilla-inbound';
|
||||
const repoName = 'mozilla-inbound';
|
||||
|
||||
beforeEach(() => {
|
||||
jasmine.getJSONFixtures().fixturesPath = 'base/tests/ui/mock';
|
||||
jasmine.getJSONFixtures().fixturesPath = 'base/tests/ui/mock';
|
||||
|
||||
fetchMock.get(getProjectUrl('/resultset/?full=true&count=10', repoName),
|
||||
getJSONFixture('push_list.json'),
|
||||
);
|
||||
fetchMock.get(
|
||||
getProjectUrl('/resultset/?full=true&count=10', repoName),
|
||||
getJSONFixture('push_list.json'),
|
||||
);
|
||||
|
||||
fetchMock.get(
|
||||
getProjectUrl('/jobs/?return_type=list&count=2000&result_set_id=1', repoName),
|
||||
getJSONFixture('job_list/job_1.json'),
|
||||
);
|
||||
fetchMock.get(
|
||||
getProjectUrl(
|
||||
'/jobs/?return_type=list&count=2000&result_set_id=1',
|
||||
repoName,
|
||||
),
|
||||
getJSONFixture('job_list/job_1.json'),
|
||||
);
|
||||
|
||||
fetchMock.get(
|
||||
getProjectUrl('/jobs/?return_type=list&count=2000&result_set_id=2', repoName),
|
||||
getJSONFixture('job_list/job_2.json'),
|
||||
);
|
||||
});
|
||||
fetchMock.get(
|
||||
getProjectUrl(
|
||||
'/jobs/?return_type=list&count=2000&result_set_id=2',
|
||||
repoName,
|
||||
),
|
||||
getJSONFixture('job_list/job_2.json'),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
});
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
/*
|
||||
/*
|
||||
Tests Pushes context
|
||||
*/
|
||||
it('should have 2 pushes', async () => {
|
||||
const pushes = mount(
|
||||
<PushesClass
|
||||
filterModel={new FilterModel()}
|
||||
notify={() => {}}
|
||||
><div /></PushesClass>,
|
||||
);
|
||||
await pushes.instance().fetchPushes(10);
|
||||
expect(pushes.state('pushList').length).toBe(2);
|
||||
});
|
||||
it('should have 2 pushes', async () => {
|
||||
const pushes = mount(
|
||||
<PushesClass filterModel={new FilterModel()} notify={() => {}}>
|
||||
<div />
|
||||
</PushesClass>,
|
||||
);
|
||||
await pushes.instance().fetchPushes(10);
|
||||
expect(pushes.state('pushList').length).toBe(2);
|
||||
});
|
||||
|
||||
it('should have id of 1 in current repo', async () => {
|
||||
const pushes = mount(
|
||||
<PushesClass
|
||||
filterModel={new FilterModel()}
|
||||
notify={() => {}}
|
||||
><div /></PushesClass>,
|
||||
);
|
||||
await pushes.instance().fetchPushes(10);
|
||||
expect(pushes.state('pushList')[0].id).toBe(1);
|
||||
});
|
||||
it('should have id of 1 in current repo', async () => {
|
||||
const pushes = mount(
|
||||
<PushesClass filterModel={new FilterModel()} notify={() => {}}>
|
||||
<div />
|
||||
</PushesClass>,
|
||||
);
|
||||
await pushes.instance().fetchPushes(10);
|
||||
expect(pushes.state('pushList')[0].id).toBe(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,32 +3,33 @@ import angular from 'angular';
|
|||
const { inject } = window;
|
||||
|
||||
describe('getRevisionUrl filter', () => {
|
||||
let $filter;
|
||||
beforeEach(angular.mock.module('treeherder'));
|
||||
beforeEach(inject((_$filter_) => {
|
||||
$filter = _$filter_;
|
||||
}));
|
||||
let $filter;
|
||||
beforeEach(angular.mock.module('treeherder'));
|
||||
beforeEach(inject(_$filter_ => {
|
||||
$filter = _$filter_;
|
||||
}));
|
||||
|
||||
it('escapes some html symbols', () => {
|
||||
const getRevisionUrl = $filter('getRevisionUrl');
|
||||
expect(getRevisionUrl('1234567890ab', 'mozilla-inbound'))
|
||||
.toEqual('/#/jobs?repo=mozilla-inbound&revision=1234567890ab');
|
||||
});
|
||||
it('escapes some html symbols', () => {
|
||||
const getRevisionUrl = $filter('getRevisionUrl');
|
||||
expect(getRevisionUrl('1234567890ab', 'mozilla-inbound')).toEqual(
|
||||
'/#/jobs?repo=mozilla-inbound&revision=1234567890ab',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('displayNumber filter', () => {
|
||||
let $filter;
|
||||
beforeEach(angular.mock.module('treeherder'));
|
||||
beforeEach(inject((_$filter_) => {
|
||||
$filter = _$filter_;
|
||||
}));
|
||||
let $filter;
|
||||
beforeEach(angular.mock.module('treeherder'));
|
||||
beforeEach(inject(_$filter_ => {
|
||||
$filter = _$filter_;
|
||||
}));
|
||||
|
||||
it('returns expected values', () => {
|
||||
const displayPrecision = $filter('displayNumber');
|
||||
const infinitySymbol = '\u221e';
|
||||
expect(displayPrecision('123.53222')).toEqual('123.53');
|
||||
expect(displayPrecision('123123123.53222')).toEqual('123,123,123.53');
|
||||
expect(displayPrecision(1 / 0)).toEqual(infinitySymbol);
|
||||
expect(displayPrecision(Number.NaN)).toEqual('N/A');
|
||||
});
|
||||
it('returns expected values', () => {
|
||||
const displayPrecision = $filter('displayNumber');
|
||||
const infinitySymbol = '\u221e';
|
||||
expect(displayPrecision('123.53222')).toEqual('123.53');
|
||||
expect(displayPrecision('123123123.53222')).toEqual('123,123,123.53');
|
||||
expect(displayPrecision(1 / 0)).toEqual(infinitySymbol);
|
||||
expect(displayPrecision(Number.NaN)).toEqual('N/A');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,21 +8,31 @@ describe('FilterModel', () => {
|
|||
});
|
||||
|
||||
describe('parsing an old url', () => {
|
||||
|
||||
it('should parse the repo with defaults', () => {
|
||||
window.location.hash = '?repo=mozilla-inbound';
|
||||
const urlParams = FilterModel.getUrlParamsWithDefaults();
|
||||
|
||||
expect(urlParams).toEqual({
|
||||
repo: ['mozilla-inbound'],
|
||||
resultStatus: ['testfailed', 'busted', 'exception', 'success', 'retry', 'usercancel', 'running', 'pending', 'runnable'],
|
||||
resultStatus: [
|
||||
'testfailed',
|
||||
'busted',
|
||||
'exception',
|
||||
'success',
|
||||
'retry',
|
||||
'usercancel',
|
||||
'running',
|
||||
'pending',
|
||||
'runnable',
|
||||
],
|
||||
classifiedState: ['classified', 'unclassified'],
|
||||
tier: ['1', '2'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse resultStatus params', () => {
|
||||
window.location.hash = '?repo=mozilla-inbound&filter-resultStatus=testfailed&' +
|
||||
window.location.hash =
|
||||
'?repo=mozilla-inbound&filter-resultStatus=testfailed&' +
|
||||
'filter-resultStatus=busted&filter-resultStatus=exception&' +
|
||||
'filter-resultStatus=success&filter-resultStatus=retry' +
|
||||
'&filter-resultStatus=runnable';
|
||||
|
@ -30,22 +40,46 @@ describe('FilterModel', () => {
|
|||
|
||||
expect(urlParams).toEqual({
|
||||
repo: ['mozilla-inbound'],
|
||||
resultStatus: ['testfailed', 'busted', 'exception', 'success', 'retry', 'runnable'],
|
||||
resultStatus: [
|
||||
'testfailed',
|
||||
'busted',
|
||||
'exception',
|
||||
'success',
|
||||
'retry',
|
||||
'runnable',
|
||||
],
|
||||
classifiedState: ['classified', 'unclassified'],
|
||||
tier: ['1', '2'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse searchStr params with tier and groupState intact', () => {
|
||||
window.location.hash = '?repo=mozilla-inbound&filter-searchStr=Linux%20x64%20debug%20build-linux64-base-toolchains%2Fdebug%20(Bb)&filter-tier=1&group_state=expanded';
|
||||
window.location.hash =
|
||||
'?repo=mozilla-inbound&filter-searchStr=Linux%20x64%20debug%20build-linux64-base-toolchains%2Fdebug%20(Bb)&filter-tier=1&group_state=expanded';
|
||||
const urlParams = FilterModel.getUrlParamsWithDefaults();
|
||||
|
||||
expect(urlParams).toEqual({
|
||||
repo: ['mozilla-inbound'],
|
||||
resultStatus: ['testfailed', 'busted', 'exception', 'success', 'retry', 'usercancel', 'running', 'pending', 'runnable'],
|
||||
resultStatus: [
|
||||
'testfailed',
|
||||
'busted',
|
||||
'exception',
|
||||
'success',
|
||||
'retry',
|
||||
'usercancel',
|
||||
'running',
|
||||
'pending',
|
||||
'runnable',
|
||||
],
|
||||
classifiedState: ['classified', 'unclassified'],
|
||||
tier: ['1'],
|
||||
searchStr: ['linux', 'x64', 'debug', 'build-linux64-base-toolchains/debug', '(bb)'],
|
||||
searchStr: [
|
||||
'linux',
|
||||
'x64',
|
||||
'debug',
|
||||
'build-linux64-base-toolchains/debug',
|
||||
'(bb)',
|
||||
],
|
||||
group_state: ['expanded'],
|
||||
});
|
||||
});
|
||||
|
@ -56,7 +90,17 @@ describe('FilterModel', () => {
|
|||
|
||||
expect(urlParams).toEqual({
|
||||
repo: ['mozilla-inbound'],
|
||||
resultStatus: ['testfailed', 'busted', 'exception', 'success', 'retry', 'usercancel', 'running', 'pending', 'runnable'],
|
||||
resultStatus: [
|
||||
'testfailed',
|
||||
'busted',
|
||||
'exception',
|
||||
'success',
|
||||
'retry',
|
||||
'usercancel',
|
||||
'running',
|
||||
'pending',
|
||||
'runnable',
|
||||
],
|
||||
classifiedState: ['classified', 'unclassified'],
|
||||
tier: ['1', '2'],
|
||||
job_type_name: ['mochi'],
|
||||
|
@ -66,26 +110,51 @@ describe('FilterModel', () => {
|
|||
|
||||
describe('parsing a new url', () => {
|
||||
it('should parse resultStatus and searchStr', () => {
|
||||
window.location.hash = '?repo=mozilla-inbound&resultStatus=testfailed,busted,exception,success,retry,runnable&' +
|
||||
'searchStr=linux,x64,debug,build-linux64-base-toolchains%2Fdebug,(bb)';
|
||||
window.location.hash =
|
||||
'?repo=mozilla-inbound&resultStatus=testfailed,busted,exception,success,retry,runnable&' +
|
||||
'searchStr=linux,x64,debug,build-linux64-base-toolchains%2Fdebug,(bb)';
|
||||
const urlParams = FilterModel.getUrlParamsWithDefaults();
|
||||
|
||||
expect(urlParams).toEqual({
|
||||
repo: ['mozilla-inbound'],
|
||||
resultStatus: ['testfailed', 'busted', 'exception', 'success', 'retry', 'runnable'],
|
||||
resultStatus: [
|
||||
'testfailed',
|
||||
'busted',
|
||||
'exception',
|
||||
'success',
|
||||
'retry',
|
||||
'runnable',
|
||||
],
|
||||
classifiedState: ['classified', 'unclassified'],
|
||||
tier: ['1', '2'],
|
||||
searchStr: ['linux', 'x64', 'debug', 'build-linux64-base-toolchains/debug', '(bb)'],
|
||||
searchStr: [
|
||||
'linux',
|
||||
'x64',
|
||||
'debug',
|
||||
'build-linux64-base-toolchains/debug',
|
||||
'(bb)',
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve the case in email addresses', () => {
|
||||
window.location.hash = '?repo=mozilla-inbound&author=VYV03354@nifty.ne.jp';
|
||||
window.location.hash =
|
||||
'?repo=mozilla-inbound&author=VYV03354@nifty.ne.jp';
|
||||
const urlParams = FilterModel.getUrlParamsWithDefaults();
|
||||
|
||||
expect(urlParams).toEqual({
|
||||
repo: ['mozilla-inbound'],
|
||||
resultStatus: ['testfailed', 'busted', 'exception', 'success', 'retry', 'usercancel', 'running', 'pending', 'runnable'],
|
||||
resultStatus: [
|
||||
'testfailed',
|
||||
'busted',
|
||||
'exception',
|
||||
'success',
|
||||
'retry',
|
||||
'usercancel',
|
||||
'running',
|
||||
'pending',
|
||||
'runnable',
|
||||
],
|
||||
classifiedState: ['classified', 'unclassified'],
|
||||
tier: ['1', '2'],
|
||||
author: ['VYV03354@nifty.ne.jp'],
|
||||
|
|
|
@ -18,7 +18,10 @@ describe('JobModel', () => {
|
|||
|
||||
describe('getList', () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.get(getProjectUrl('/jobs/'), getJSONFixture('job_list/job_1.json'));
|
||||
fetchMock.get(
|
||||
getProjectUrl('/jobs/'),
|
||||
getJSONFixture('job_list/job_1.json'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return a promise', () => {
|
||||
|
@ -29,8 +32,14 @@ describe('JobModel', () => {
|
|||
|
||||
describe('pagination', () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.get(getProjectUrl('/jobs/?count=2'), getJSONFixture('job_list/pagination/page_1.json'));
|
||||
fetchMock.get(getProjectUrl('/jobs/?count=2&offset=2'), getJSONFixture('job_list/pagination/page_2.json'));
|
||||
fetchMock.get(
|
||||
getProjectUrl('/jobs/?count=2'),
|
||||
getJSONFixture('job_list/pagination/page_1.json'),
|
||||
);
|
||||
fetchMock.get(
|
||||
getProjectUrl('/jobs/?count=2&offset=2'),
|
||||
getJSONFixture('job_list/pagination/page_2.json'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return a page of results by default', async () => {
|
||||
|
@ -40,7 +49,11 @@ describe('JobModel', () => {
|
|||
});
|
||||
|
||||
it('should return all the pages when fetch_all==true', async () => {
|
||||
const jobList = await JobModel.getList(repoName, { count: 2 }, { fetch_all: true });
|
||||
const jobList = await JobModel.getList(
|
||||
repoName,
|
||||
{ count: 2 },
|
||||
{ fetch_all: true },
|
||||
);
|
||||
|
||||
expect(jobList.length).toBe(3);
|
||||
expect(jobList[2].id).toBe(3);
|
||||
|
|
|
@ -7,8 +7,10 @@ import { isReftest } from '../../../../ui/helpers/job';
|
|||
import { BugFilerClass } from '../../../../ui/job-view/details/BugFiler';
|
||||
|
||||
describe('BugFiler', () => {
|
||||
const fullLog = 'https://queue.taskcluster.net/v1/task/AGs4CgN_RnCTb943uQn8NQ/runs/0/artifacts/public/logs/live_backing.log';
|
||||
const parsedLog = 'http://localhost:5000/logviewer.html#?job_id=89017089&repo=mozilla-inbound';
|
||||
const fullLog =
|
||||
'https://queue.taskcluster.net/v1/task/AGs4CgN_RnCTb943uQn8NQ/runs/0/artifacts/public/logs/live_backing.log';
|
||||
const parsedLog =
|
||||
'http://localhost:5000/logviewer.html#?job_id=89017089&repo=mozilla-inbound';
|
||||
const reftest = '';
|
||||
const selectedJob = {
|
||||
job_group_name: 'Mochitests executed by TaskCluster',
|
||||
|
@ -16,9 +18,18 @@ describe('BugFiler', () => {
|
|||
};
|
||||
const suggestions = [
|
||||
{ search: 'ShutdownLeaks | process() called before end of test suite' },
|
||||
{ search: 'browser/components/search/test/browser_searchbar_smallpanel_keyboard_navigation.js | application terminated with exit code 11' },
|
||||
{ search: 'browser/components/search/test/browser_searchbar_smallpanel_keyboard_navigation.js | application crashed [@ js::GCMarker::eagerlyMarkChildren]' },
|
||||
{ search: 'leakcheck | default process: missing output line for total leaks!' },
|
||||
{
|
||||
search:
|
||||
'browser/components/search/test/browser_searchbar_smallpanel_keyboard_navigation.js | application terminated with exit code 11',
|
||||
},
|
||||
{
|
||||
search:
|
||||
'browser/components/search/test/browser_searchbar_smallpanel_keyboard_navigation.js | application crashed [@ js::GCMarker::eagerlyMarkChildren]',
|
||||
},
|
||||
{
|
||||
search:
|
||||
'leakcheck | default process: missing output line for total leaks!',
|
||||
},
|
||||
{ search: '# TBPL FAILURE #' },
|
||||
];
|
||||
const successCallback = () => {};
|
||||
|
@ -43,18 +54,20 @@ describe('BugFiler', () => {
|
|||
|
||||
fetchMock.get(
|
||||
`${bzBaseUrl}rest/prod_comp_search/firefox%20::%20search?limit=5`,
|
||||
{ products: [
|
||||
{ product: 'Firefox' },
|
||||
{ component: 'Search', product: 'Firefox' },
|
||||
{ product: 'Marketplace' },
|
||||
{ component: 'Search', product: 'Marketplace' },
|
||||
{ product: 'Firefox for Android' },
|
||||
{ component: 'Search Activity', product: 'Firefox for Android' },
|
||||
{ product: 'Firefox OS' },
|
||||
{ component: 'Gaia::Search', product: 'Firefox OS' },
|
||||
{ product: 'Cloud Services' },
|
||||
{ component: 'Operations: Storage', product: 'Cloud Services' },
|
||||
] },
|
||||
{
|
||||
products: [
|
||||
{ product: 'Firefox' },
|
||||
{ component: 'Search', product: 'Firefox' },
|
||||
{ product: 'Marketplace' },
|
||||
{ component: 'Search', product: 'Marketplace' },
|
||||
{ product: 'Firefox for Android' },
|
||||
{ component: 'Search Activity', product: 'Firefox for Android' },
|
||||
{ product: 'Firefox OS' },
|
||||
{ component: 'Gaia::Search', product: 'Firefox OS' },
|
||||
{ product: 'Cloud Services' },
|
||||
{ component: 'Operations: Storage', product: 'Cloud Services' },
|
||||
],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -62,10 +75,12 @@ describe('BugFiler', () => {
|
|||
fetchMock.reset();
|
||||
});
|
||||
|
||||
const getBugFilerForSummary = (summary) => {
|
||||
const getBugFilerForSummary = summary => {
|
||||
const suggestion = {
|
||||
summary,
|
||||
search_terms: ['browser_searchbar_smallpanel_keyboard_navigation.js", "[@ js::GCMarker::eagerlyMarkChildren]'],
|
||||
search_terms: [
|
||||
'browser_searchbar_smallpanel_keyboard_navigation.js", "[@ js::GCMarker::eagerlyMarkChildren]',
|
||||
],
|
||||
search: summary,
|
||||
};
|
||||
|
||||
|
@ -86,129 +101,191 @@ describe('BugFiler', () => {
|
|||
};
|
||||
|
||||
it('parses a crash suggestion', () => {
|
||||
const summary = 'PROCESS-CRASH | browser/components/search/test/browser_searchbar_smallpanel_keyboard_navigation.js | application crashed [@ js::GCMarker::eagerlyMarkChildren]';
|
||||
const summary =
|
||||
'PROCESS-CRASH | browser/components/search/test/browser_searchbar_smallpanel_keyboard_navigation.js | application crashed [@ js::GCMarker::eagerlyMarkChildren]';
|
||||
const bugFiler = getBugFilerForSummary(summary);
|
||||
const { parsedSummary } = bugFiler.state();
|
||||
expect(parsedSummary[0][0]).toEqual('browser/components/search/test/browser_searchbar_smallpanel_keyboard_navigation.js');
|
||||
expect(parsedSummary[0][0]).toEqual(
|
||||
'browser/components/search/test/browser_searchbar_smallpanel_keyboard_navigation.js',
|
||||
);
|
||||
});
|
||||
|
||||
it('should parse mochitest-bc summaries', () => {
|
||||
const rawSummary = 'browser/components/sessionstore/test/browser_625016.js | observe1: 1 window in data written to disk - Got 0, expected 1';
|
||||
const rawSummary =
|
||||
'browser/components/sessionstore/test/browser_625016.js | observe1: 1 window in data written to disk - Got 0, expected 1';
|
||||
const bugFiler = getBugFilerForSummary(rawSummary);
|
||||
const summary = bugFiler.state().parsedSummary;
|
||||
expect(summary[0][0]).toBe('browser/components/sessionstore/test/browser_625016.js');
|
||||
expect(summary[0][1]).toBe('observe1: 1 window in data written to disk - Got 0, expected 1');
|
||||
expect(summary[0][0]).toBe(
|
||||
'browser/components/sessionstore/test/browser_625016.js',
|
||||
);
|
||||
expect(summary[0][1]).toBe(
|
||||
'observe1: 1 window in data written to disk - Got 0, expected 1',
|
||||
);
|
||||
expect(summary[1]).toBe('browser_625016.js');
|
||||
});
|
||||
|
||||
it('should parse accessibility summaries', () => {
|
||||
const rawSummary = 'chrome://mochitests/content/a11y/accessible/tests/mochitest/states/test_expandable.xul' +
|
||||
const rawSummary =
|
||||
'chrome://mochitests/content/a11y/accessible/tests/mochitest/states/test_expandable.xul' +
|
||||
' | uncaught exception - TypeError: this.textbox.popup.oneOffButtons is undefined at ' +
|
||||
'searchbar_XBL_Constructor@chrome://browser/content/search/search.xml:95:9';
|
||||
const bugFiler = getBugFilerForSummary(rawSummary);
|
||||
const summary = bugFiler.state().parsedSummary;
|
||||
expect(summary[0][0]).toBe('accessible/tests/mochitest/states/test_expandable.xul');
|
||||
expect(summary[0][1]).toBe('uncaught exception - TypeError: this.textbox.popup.oneOffButtons is undefined at ' +
|
||||
'searchbar_XBL_Constructor@chrome://browser/content/search/search.xml:95:9');
|
||||
expect(summary[0][0]).toBe(
|
||||
'accessible/tests/mochitest/states/test_expandable.xul',
|
||||
);
|
||||
expect(summary[0][1]).toBe(
|
||||
'uncaught exception - TypeError: this.textbox.popup.oneOffButtons is undefined at ' +
|
||||
'searchbar_XBL_Constructor@chrome://browser/content/search/search.xml:95:9',
|
||||
);
|
||||
expect(summary[1]).toBe('test_expandable.xul');
|
||||
});
|
||||
|
||||
it('should parse xpcshell summaries', () => {
|
||||
const rawSummary = 'xpcshell-child-process.ini:dom/indexedDB/test/unit/test_rename_objectStore_errors.js | application crashed [@ mozalloc_abort(char const*)]';
|
||||
const rawSummary =
|
||||
'xpcshell-child-process.ini:dom/indexedDB/test/unit/test_rename_objectStore_errors.js | application crashed [@ mozalloc_abort(char const*)]';
|
||||
const bugFiler = getBugFilerForSummary(rawSummary);
|
||||
const summary = bugFiler.state().parsedSummary;
|
||||
expect(summary[0][0]).toBe('dom/indexedDB/test/unit/test_rename_objectStore_errors.js');
|
||||
expect(summary[0][1]).toBe('application crashed [@ mozalloc_abort(char const*)]');
|
||||
expect(summary[0][0]).toBe(
|
||||
'dom/indexedDB/test/unit/test_rename_objectStore_errors.js',
|
||||
);
|
||||
expect(summary[0][1]).toBe(
|
||||
'application crashed [@ mozalloc_abort(char const*)]',
|
||||
);
|
||||
expect(summary[1]).toBe('test_rename_objectStore_errors.js');
|
||||
});
|
||||
|
||||
it('should parse xpcshell unpack summaries', () => {
|
||||
const rawSummary = 'xpcshell-unpack.ini:dom/indexedDB/test/unit/test_rename_objectStore_errors.js | application crashed [@ mozalloc_abort(char const*)]';
|
||||
const rawSummary =
|
||||
'xpcshell-unpack.ini:dom/indexedDB/test/unit/test_rename_objectStore_errors.js | application crashed [@ mozalloc_abort(char const*)]';
|
||||
const bugFiler = getBugFilerForSummary(rawSummary);
|
||||
const summary = bugFiler.state().parsedSummary;
|
||||
expect(summary[0][0]).toBe('dom/indexedDB/test/unit/test_rename_objectStore_errors.js');
|
||||
expect(summary[0][1]).toBe('application crashed [@ mozalloc_abort(char const*)]');
|
||||
expect(summary[0][0]).toBe(
|
||||
'dom/indexedDB/test/unit/test_rename_objectStore_errors.js',
|
||||
);
|
||||
expect(summary[0][1]).toBe(
|
||||
'application crashed [@ mozalloc_abort(char const*)]',
|
||||
);
|
||||
expect(summary[1]).toBe('test_rename_objectStore_errors.js');
|
||||
});
|
||||
|
||||
it('should parse xpcshell dom summaries', () => {
|
||||
const rawSummary = 'xpcshell.ini:dom/indexedDB/test/unit/test_rename_objectStore_errors.js | application crashed [@ mozalloc_abort(char const*)]';
|
||||
const rawSummary =
|
||||
'xpcshell.ini:dom/indexedDB/test/unit/test_rename_objectStore_errors.js | application crashed [@ mozalloc_abort(char const*)]';
|
||||
const bugFiler = getBugFilerForSummary(rawSummary);
|
||||
const summary = bugFiler.state().parsedSummary;
|
||||
expect(summary[0][0]).toBe('dom/indexedDB/test/unit/test_rename_objectStore_errors.js');
|
||||
expect(summary[0][1]).toBe('application crashed [@ mozalloc_abort(char const*)]');
|
||||
expect(summary[0][0]).toBe(
|
||||
'dom/indexedDB/test/unit/test_rename_objectStore_errors.js',
|
||||
);
|
||||
expect(summary[0][1]).toBe(
|
||||
'application crashed [@ mozalloc_abort(char const*)]',
|
||||
);
|
||||
expect(summary[1]).toBe('test_rename_objectStore_errors.js');
|
||||
});
|
||||
|
||||
it('should parse Windows reftests on C drive summaries', () => {
|
||||
const rawSummary = 'file:///C:/slave/test/build/tests/reftest/tests/layout/reftests/w3c-css/submitted/variables/variable-supports-12.html | application timed out after 330 seconds with no output';
|
||||
const rawSummary =
|
||||
'file:///C:/slave/test/build/tests/reftest/tests/layout/reftests/w3c-css/submitted/variables/variable-supports-12.html | application timed out after 330 seconds with no output';
|
||||
const bugFiler = getBugFilerForSummary(rawSummary);
|
||||
const summary = bugFiler.state().parsedSummary;
|
||||
expect(summary[0][0]).toBe('layout/reftests/w3c-css/submitted/variables/variable-supports-12.html');
|
||||
expect(summary[0][1]).toBe('application timed out after 330 seconds with no output');
|
||||
expect(summary[0][0]).toBe(
|
||||
'layout/reftests/w3c-css/submitted/variables/variable-supports-12.html',
|
||||
);
|
||||
expect(summary[0][1]).toBe(
|
||||
'application timed out after 330 seconds with no output',
|
||||
);
|
||||
expect(summary[1]).toBe('variable-supports-12.html');
|
||||
});
|
||||
|
||||
it('should parse Linux reftest summaries', () => {
|
||||
const rawSummary = 'file:///home/worker/workspace/build/tests/reftest/tests/image/test/reftest/encoders-lossless/size-7x7.png | application timed out after 330 seconds with no output';
|
||||
const rawSummary =
|
||||
'file:///home/worker/workspace/build/tests/reftest/tests/image/test/reftest/encoders-lossless/size-7x7.png | application timed out after 330 seconds with no output';
|
||||
const bugFiler = getBugFilerForSummary(rawSummary);
|
||||
const summary = bugFiler.state().parsedSummary;
|
||||
expect(summary[0][0]).toBe('image/test/reftest/encoders-lossless/size-7x7.png');
|
||||
expect(summary[0][1]).toBe('application timed out after 330 seconds with no output');
|
||||
expect(summary[0][0]).toBe(
|
||||
'image/test/reftest/encoders-lossless/size-7x7.png',
|
||||
);
|
||||
expect(summary[0][1]).toBe(
|
||||
'application timed out after 330 seconds with no output',
|
||||
);
|
||||
expect(summary[1]).toBe('size-7x7.png');
|
||||
});
|
||||
|
||||
it('should parse Windows reftests on Z drive summaries', () => {
|
||||
const rawSummary = 'file:///Z:/task_1491428153/build/tests/reftest/tests/layout/reftests/font-face/src-list-local-full.html == file:///Z:/task_1491428153/build/tests/reftest/tests/layout/reftests/font-face/src-list-local-full-ref.html | image comparison, max difference: 255, number of differing pixels: 5184';
|
||||
const rawSummary =
|
||||
'file:///Z:/task_1491428153/build/tests/reftest/tests/layout/reftests/font-face/src-list-local-full.html == file:///Z:/task_1491428153/build/tests/reftest/tests/layout/reftests/font-face/src-list-local-full-ref.html | image comparison, max difference: 255, number of differing pixels: 5184';
|
||||
const bugFiler = getBugFilerForSummary(rawSummary);
|
||||
const summary = bugFiler.state().parsedSummary;
|
||||
expect(summary[0][0]).toBe('layout/reftests/font-face/src-list-local-full.html == layout/reftests/font-face/src-list-local-full-ref.html');
|
||||
expect(summary[0][1]).toBe('image comparison, max difference: 255, number of differing pixels: 5184');
|
||||
expect(summary[0][0]).toBe(
|
||||
'layout/reftests/font-face/src-list-local-full.html == layout/reftests/font-face/src-list-local-full-ref.html',
|
||||
);
|
||||
expect(summary[0][1]).toBe(
|
||||
'image comparison, max difference: 255, number of differing pixels: 5184',
|
||||
);
|
||||
expect(summary[1]).toBe('src-list-local-full.html');
|
||||
});
|
||||
|
||||
it('should parse android reftests summaries', () => {
|
||||
const rawSummary = 'http://10.0.2.2:8854/tests/layout/reftests/css-display/display-contents-style-inheritance-1.html == http://10.0.2.2:8854/tests/layout/reftests/css-display/display-contents-style-inheritance-1-ref.html | image comparison, max difference: 255, number of differing pixels: 699';
|
||||
const rawSummary =
|
||||
'http://10.0.2.2:8854/tests/layout/reftests/css-display/display-contents-style-inheritance-1.html == http://10.0.2.2:8854/tests/layout/reftests/css-display/display-contents-style-inheritance-1-ref.html | image comparison, max difference: 255, number of differing pixels: 699';
|
||||
const bugFiler = getBugFilerForSummary(rawSummary);
|
||||
const summary = bugFiler.state().parsedSummary;
|
||||
expect(summary[0][0]).toBe('layout/reftests/css-display/display-contents-style-inheritance-1.html == layout/reftests/css-display/display-contents-style-inheritance-1-ref.html');
|
||||
expect(summary[0][1]).toBe('image comparison, max difference: 255, number of differing pixels: 699');
|
||||
expect(summary[0][0]).toBe(
|
||||
'layout/reftests/css-display/display-contents-style-inheritance-1.html == layout/reftests/css-display/display-contents-style-inheritance-1-ref.html',
|
||||
);
|
||||
expect(summary[0][1]).toBe(
|
||||
'image comparison, max difference: 255, number of differing pixels: 699',
|
||||
);
|
||||
expect(summary[1]).toBe('display-contents-style-inheritance-1.html');
|
||||
});
|
||||
|
||||
it('should parse reftest unexpected pass summaries', () => {
|
||||
const rawSummary = 'REFTEST TEST-UNEXPECTED-PASS | file:///home/worker/workspace/build/tests/reftest/tests/layout/' +
|
||||
const rawSummary =
|
||||
'REFTEST TEST-UNEXPECTED-PASS | file:///home/worker/workspace/build/tests/reftest/tests/layout/' +
|
||||
'reftests/backgrounds/vector/empty/wide--cover--width.html == file:///home/worker/workspace/' +
|
||||
'build/tests/reftest/tests/layout/reftests/backgrounds/vector/empty/ref-wide-lime.html | image comparison';
|
||||
const bugFiler = getBugFilerForSummary(rawSummary);
|
||||
const summary = bugFiler.state().parsedSummary;
|
||||
expect(summary[0][0]).toBe('TEST-UNEXPECTED-PASS');
|
||||
expect(summary[0][1]).toBe('layout/reftests/backgrounds/vector/empty/wide--cover--width.html == layout/reftests/backgrounds/vector/empty/ref-wide-lime.html');
|
||||
expect(summary[0][1]).toBe(
|
||||
'layout/reftests/backgrounds/vector/empty/wide--cover--width.html == layout/reftests/backgrounds/vector/empty/ref-wide-lime.html',
|
||||
);
|
||||
expect(summary[0][2]).toBe('image comparison');
|
||||
expect(summary[1]).toBe('wide--cover--width.html');
|
||||
});
|
||||
|
||||
it('should parse finding the filename when the `TEST-FOO` is not omitted', () => {
|
||||
const rawSummary = 'TEST-UNEXPECTED-CRASH | /service-workers/service-worker/xhr.https.html | expected OK';
|
||||
const rawSummary =
|
||||
'TEST-UNEXPECTED-CRASH | /service-workers/service-worker/xhr.https.html | expected OK';
|
||||
const bugFiler = getBugFilerForSummary(rawSummary);
|
||||
const summary = bugFiler.state().parsedSummary;
|
||||
expect(summary[0][0]).toBe('TEST-UNEXPECTED-CRASH');
|
||||
expect(summary[0][1]).toBe('/service-workers/service-worker/xhr.https.html');
|
||||
expect(summary[0][1]).toBe(
|
||||
'/service-workers/service-worker/xhr.https.html',
|
||||
);
|
||||
expect(summary[0][2]).toBe('expected OK');
|
||||
expect(summary[1]).toBe('xhr.https.html');
|
||||
});
|
||||
|
||||
it('should strip omitted leads from thisFailure', () => {
|
||||
const suggestions = [
|
||||
{ bugs: {},
|
||||
{
|
||||
bugs: {},
|
||||
search_terms: [],
|
||||
search: 'TEST-UNEXPECTED-FAIL | browser/extensions/pdfjs/test/browser_pdfjs_views.js | Test timed out -' },
|
||||
{ bugs: {},
|
||||
search:
|
||||
'TEST-UNEXPECTED-FAIL | browser/extensions/pdfjs/test/browser_pdfjs_views.js | Test timed out -',
|
||||
},
|
||||
{
|
||||
bugs: {},
|
||||
search_terms: [],
|
||||
search: 'TEST-UNEXPECTED-FAIL | browser/extensions/pdfjs/test/browser_pdfjs_views.js | Found a tab after previous test timed out: about:blank -' },
|
||||
{ bugs: {},
|
||||
search:
|
||||
'TEST-UNEXPECTED-FAIL | browser/extensions/pdfjs/test/browser_pdfjs_views.js | Found a tab after previous test timed out: about:blank -',
|
||||
},
|
||||
{
|
||||
bugs: {},
|
||||
search_terms: [],
|
||||
search: 'REFTEST TEST-UNEXPECTED-PASS | flee | floo' },
|
||||
search: 'REFTEST TEST-UNEXPECTED-PASS | flee | floo',
|
||||
},
|
||||
];
|
||||
const bugFiler = mount(
|
||||
<BugFilerClass
|
||||
|
@ -226,8 +303,10 @@ describe('BugFiler', () => {
|
|||
);
|
||||
|
||||
const { thisFailure } = bugFiler.state();
|
||||
expect(thisFailure).toBe('browser/extensions/pdfjs/test/browser_pdfjs_views.js | Test timed out -\n' +
|
||||
'browser/extensions/pdfjs/test/browser_pdfjs_views.js | Found a tab after previous test timed out: about:blank -\n' +
|
||||
'TEST-UNEXPECTED-PASS | flee | floo');
|
||||
expect(thisFailure).toBe(
|
||||
'browser/extensions/pdfjs/test/browser_pdfjs_views.js | Test timed out -\n' +
|
||||
'browser/extensions/pdfjs/test/browser_pdfjs_views.js | Found a tab after previous test timed out: about:blank -\n' +
|
||||
'TEST-UNEXPECTED-PASS | flee | floo',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,7 +13,6 @@ describe('JobGroup component', () => {
|
|||
const filterModel = new FilterModel();
|
||||
const pushGroupState = 'collapsed';
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
jasmine.getJSONFixtures().fixturesPath = 'base/tests/ui/mock';
|
||||
countGroup = getJSONFixture('mappedGroup.json');
|
||||
|
@ -40,9 +39,9 @@ describe('JobGroup component', () => {
|
|||
'<span class="platform-group" data-group-key="313281W-e10s1linux64debug"><span class="disabled job-group" title="Web platform tests with e10s">' +
|
||||
'<button class="btn group-symbol">W-e10s</button>' +
|
||||
'<span class="group-content">' +
|
||||
'<span class="group-job-list"><button data-job-id="166315800" title="success | test-linux64/debug-web-platform-tests-reftests-e10s-1 - (18 mins)" class="btn btn-green filter-shown job-btn btn-xs">Wr1</button></span>' +
|
||||
'<span class="group-count-list"><button class="btn-dkgray-count btn group-btn btn-xs job-group-count filter-shown" title="2 running jobs in group">2</button>' +
|
||||
'</span></span></span></span>',
|
||||
'<span class="group-job-list"><button data-job-id="166315800" title="success | test-linux64/debug-web-platform-tests-reftests-e10s-1 - (18 mins)" class="btn btn-green filter-shown job-btn btn-xs">Wr1</button></span>' +
|
||||
'<span class="group-count-list"><button class="btn-dkgray-count btn group-btn btn-xs job-group-count filter-shown" title="2 running jobs in group">2</button>' +
|
||||
'</span></span></span></span>',
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -65,9 +64,9 @@ describe('JobGroup component', () => {
|
|||
'<span class="platform-group" data-group-key="313281W-e10s1linux64debug"><span class="disabled job-group" title="Web platform tests with e10s">' +
|
||||
'<button class="btn group-symbol">W-e10s</button>' +
|
||||
'<span class="group-content">' +
|
||||
'<span class="group-job-list"><button data-job-id="166315800" title="success | test-linux64/debug-web-platform-tests-reftests-e10s-1 - (18 mins)" class="btn btn-green filter-shown job-btn btn-xs">Wr1</button></span>' +
|
||||
'<span class="group-count-list"><button class="btn-dkgray-count btn group-btn btn-xs job-group-count filter-shown" title="2 running jobs in group">2</button>' +
|
||||
'</span></span></span></span>',
|
||||
'<span class="group-job-list"><button data-job-id="166315800" title="success | test-linux64/debug-web-platform-tests-reftests-e10s-1 - (18 mins)" class="btn btn-green filter-shown job-btn btn-xs">Wr1</button></span>' +
|
||||
'<span class="group-count-list"><button class="btn-dkgray-count btn group-btn btn-xs job-group-count filter-shown" title="2 running jobs in group">2</button>' +
|
||||
'</span></span></span></span>',
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -88,13 +87,13 @@ describe('JobGroup component', () => {
|
|||
expect(jobGroup.html()).toEqual(
|
||||
'<span class="platform-group" data-group-key="313281W-e10s1linux64debug"><span class="disabled job-group" title="Web platform tests with e10s">' +
|
||||
'<button class="btn group-symbol">W-e10s</button>' +
|
||||
'<span class="group-content">' +
|
||||
'<span class="group-job-list">' +
|
||||
'<button data-job-id="166315799" title="running | test-linux64/debug-web-platform-tests-wdspec-e10s - " class="btn btn-dkgray filter-shown job-btn btn-xs">Wd</button>' +
|
||||
'<button data-job-id="166315800" title="success | test-linux64/debug-web-platform-tests-reftests-e10s-1 - (18 mins)" class="btn btn-green filter-shown job-btn btn-xs">Wr1</button>' +
|
||||
'<button data-job-id="166315797" title="running | test-linux64/debug-web-platform-tests-e10s-1 - " class="btn btn-dkgray filter-shown job-btn btn-xs">wpt1</button>' +
|
||||
'</span>' +
|
||||
'<span class="group-count-list"></span></span></span></span>',
|
||||
'<span class="group-content">' +
|
||||
'<span class="group-job-list">' +
|
||||
'<button data-job-id="166315799" title="running | test-linux64/debug-web-platform-tests-wdspec-e10s - " class="btn btn-dkgray filter-shown job-btn btn-xs">Wd</button>' +
|
||||
'<button data-job-id="166315800" title="success | test-linux64/debug-web-platform-tests-reftests-e10s-1 - (18 mins)" class="btn btn-green filter-shown job-btn btn-xs">Wr1</button>' +
|
||||
'<button data-job-id="166315797" title="running | test-linux64/debug-web-platform-tests-e10s-1 - " class="btn btn-dkgray filter-shown job-btn btn-xs">wpt1</button>' +
|
||||
'</span>' +
|
||||
'<span class="group-count-list"></span></span></span></span>',
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -116,13 +115,13 @@ describe('JobGroup component', () => {
|
|||
expect(jobGroup.html()).toEqual(
|
||||
'<span class="platform-group" data-group-key="313281W-e10s1linux64debug"><span class="disabled job-group" title="Web platform tests with e10s">' +
|
||||
'<button class="btn group-symbol">W-e10s</button>' +
|
||||
'<span class="group-content">' +
|
||||
'<span class="group-job-list">' +
|
||||
'<button data-job-id="166315799" title="running | test-linux64/debug-web-platform-tests-wdspec-e10s - " class="btn btn-dkgray filter-shown job-btn btn-xs">Wd</button>' +
|
||||
'<button data-job-id="166315800" title="success | test-linux64/debug-web-platform-tests-reftests-e10s-1 - (18 mins)" class="btn btn-green filter-shown job-btn btn-xs">Wr1</button>' +
|
||||
'<button data-job-id="166315797" title="running | test-linux64/debug-web-platform-tests-e10s-1 - " class="btn btn-dkgray filter-shown job-btn btn-xs">wpt1</button>' +
|
||||
'</span>' +
|
||||
'<span class="group-count-list"></span></span></span></span>',
|
||||
'<span class="group-content">' +
|
||||
'<span class="group-job-list">' +
|
||||
'<button data-job-id="166315799" title="running | test-linux64/debug-web-platform-tests-wdspec-e10s - " class="btn btn-dkgray filter-shown job-btn btn-xs">Wd</button>' +
|
||||
'<button data-job-id="166315800" title="success | test-linux64/debug-web-platform-tests-reftests-e10s-1 - (18 mins)" class="btn btn-green filter-shown job-btn btn-xs">Wr1</button>' +
|
||||
'<button data-job-id="166315797" title="running | test-linux64/debug-web-platform-tests-e10s-1 - " class="btn btn-dkgray filter-shown job-btn btn-xs">wpt1</button>' +
|
||||
'</span>' +
|
||||
'<span class="group-count-list"></span></span></span></span>',
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -144,11 +143,11 @@ describe('JobGroup component', () => {
|
|||
'<span class="platform-group" data-group-key="313293SM1linux64opt"><span class="disabled job-group" title="Spidermonkey builds">' +
|
||||
'<button class="btn group-symbol">SM</button>' +
|
||||
'<span class="group-content"><span class="group-job-list">' +
|
||||
'<button data-job-id="166316707" title="retry | spidermonkey-sm-msan-linux64/opt - (0 mins)" class="btn btn-dkblue filter-shown job-btn btn-xs">msan</button>' +
|
||||
'<button data-job-id="166316707" title="retry | spidermonkey-sm-msan-linux64/opt - (0 mins)" class="btn btn-dkblue filter-shown job-btn btn-xs">msan</button>' +
|
||||
'</span>' +
|
||||
'<span class="group-count-list">' +
|
||||
'<button class="btn-green-count btn group-btn btn-xs job-group-count filter-shown" title="6 success jobs in group">6</button>' +
|
||||
'</span></span></span></span>',
|
||||
'<button class="btn-green-count btn group-btn btn-xs job-group-count filter-shown" title="6 success jobs in group">6</button>' +
|
||||
'</span></span></span></span>',
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -171,12 +170,12 @@ describe('JobGroup component', () => {
|
|||
'<span class="platform-group" data-group-key="313293SM1linux64opt"><span class="disabled job-group" title="Spidermonkey builds">' +
|
||||
'<button class="btn group-symbol">SM</button>' +
|
||||
'<span class="group-content"><span class="group-job-list">' +
|
||||
'<button data-job-id="166316707" title="retry | spidermonkey-sm-msan-linux64/opt - (0 mins)" class="btn btn-dkblue filter-shown job-btn btn-xs">msan</button>' +
|
||||
'<button data-job-id="166321182" title="success | spidermonkey-sm-msan-linux64/opt - (10 mins)" class="btn btn-green filter-shown job-btn btn-xs">msan</button>' +
|
||||
'<button data-job-id="166316707" title="retry | spidermonkey-sm-msan-linux64/opt - (0 mins)" class="btn btn-dkblue filter-shown job-btn btn-xs">msan</button>' +
|
||||
'<button data-job-id="166321182" title="success | spidermonkey-sm-msan-linux64/opt - (10 mins)" class="btn btn-green filter-shown job-btn btn-xs">msan</button>' +
|
||||
'</span>' +
|
||||
'<span class="group-count-list">' +
|
||||
'<button class="btn-green-count btn group-btn btn-xs job-group-count filter-shown" title="5 success jobs in group">5</button>' +
|
||||
'</span></span></span></span>',
|
||||
'<button class="btn-green-count btn group-btn btn-xs job-group-count filter-shown" title="5 success jobs in group">5</button>' +
|
||||
'</span></span></span></span>',
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -199,12 +198,12 @@ describe('JobGroup component', () => {
|
|||
'<span class="platform-group" data-group-key="313293SM1linux64opt"><span class="disabled job-group" title="Spidermonkey builds">' +
|
||||
'<button class="btn group-symbol">SM</button>' +
|
||||
'<span class="group-content"><span class="group-job-list">' +
|
||||
'<button data-job-id="166316707" title="retry | spidermonkey-sm-msan-linux64/opt - (0 mins)" class="btn btn-dkblue filter-shown job-btn btn-xs">msan</button>' +
|
||||
'<button data-job-id="166321182" title="success | spidermonkey-sm-msan-linux64/opt - (10 mins)" class="btn btn-green filter-shown job-btn btn-xs">msan</button>' +
|
||||
'<button data-job-id="166316707" title="retry | spidermonkey-sm-msan-linux64/opt - (0 mins)" class="btn btn-dkblue filter-shown job-btn btn-xs">msan</button>' +
|
||||
'<button data-job-id="166321182" title="success | spidermonkey-sm-msan-linux64/opt - (10 mins)" class="btn btn-green filter-shown job-btn btn-xs">msan</button>' +
|
||||
'</span>' +
|
||||
'<span class="group-count-list">' +
|
||||
'<button class="btn-green-count btn group-btn btn-xs job-group-count filter-shown" title="5 success jobs in group">5</button>' +
|
||||
'</span></span></span></span>',
|
||||
'<button class="btn-green-count btn group-btn btn-xs job-group-count filter-shown" title="5 success jobs in group">5</button>' +
|
||||
'</span></span></span></span>',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,7 +12,6 @@ describe('Revision list component', () => {
|
|||
let mockData;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
const repo = new RepositoryModel({
|
||||
id: 2,
|
||||
repository_group: {
|
||||
|
@ -27,48 +26,53 @@ describe('Revision list component', () => {
|
|||
description: '',
|
||||
active_status: 'active',
|
||||
performance_alerts_enabled: true,
|
||||
pushlogURL: 'https://hg.mozilla.org/integration/mozilla-inbound/pushloghtml',
|
||||
pushlogURL:
|
||||
'https://hg.mozilla.org/integration/mozilla-inbound/pushloghtml',
|
||||
});
|
||||
|
||||
const push = {
|
||||
id: 151371,
|
||||
revision: '5a110ad242ead60e71d2186bae78b1fb766ad5ff',
|
||||
revision_count: 3,
|
||||
author: 'ryanvm@gmail.com',
|
||||
push_timestamp: 1481326280,
|
||||
repository_id: 2,
|
||||
revisions: [{
|
||||
result_set_id: 151371,
|
||||
repository_id: 2,
|
||||
revision: '5a110ad242ead60e71d2186bae78b1fb766ad5ff',
|
||||
author: 'André Bargull <andre.bargull@gmail.com>',
|
||||
comments: 'Bug 1319926 - Part 2: Collect telemetry about deprecated String generics methods. r=jandem',
|
||||
}, {
|
||||
result_set_id: 151371,
|
||||
repository_id: 2,
|
||||
revision: '07d6bf74b7a2552da91b5e2fce0fa0bc3b457394',
|
||||
author: 'André Bargull <andre.bargull@gmail.com>',
|
||||
comments: 'Bug 1319926 - Part 1: Warn when deprecated String generics methods are used. r=jandem',
|
||||
}, {
|
||||
result_set_id: 151371,
|
||||
repository_id: 2,
|
||||
revision: 'e83eaf2380c65400dc03c6f3615d4b2cef669af3',
|
||||
author: 'Frédéric Wang <fred.wang@free.fr>',
|
||||
comments: 'Bug 1322743 - Add STIX Two Math to the list of math fonts. r=karlt',
|
||||
}],
|
||||
};
|
||||
mockData = {
|
||||
push,
|
||||
repo,
|
||||
};
|
||||
const push = {
|
||||
id: 151371,
|
||||
revision: '5a110ad242ead60e71d2186bae78b1fb766ad5ff',
|
||||
revision_count: 3,
|
||||
author: 'ryanvm@gmail.com',
|
||||
push_timestamp: 1481326280,
|
||||
repository_id: 2,
|
||||
revisions: [
|
||||
{
|
||||
result_set_id: 151371,
|
||||
repository_id: 2,
|
||||
revision: '5a110ad242ead60e71d2186bae78b1fb766ad5ff',
|
||||
author: 'André Bargull <andre.bargull@gmail.com>',
|
||||
comments:
|
||||
'Bug 1319926 - Part 2: Collect telemetry about deprecated String generics methods. r=jandem',
|
||||
},
|
||||
{
|
||||
result_set_id: 151371,
|
||||
repository_id: 2,
|
||||
revision: '07d6bf74b7a2552da91b5e2fce0fa0bc3b457394',
|
||||
author: 'André Bargull <andre.bargull@gmail.com>',
|
||||
comments:
|
||||
'Bug 1319926 - Part 1: Warn when deprecated String generics methods are used. r=jandem',
|
||||
},
|
||||
{
|
||||
result_set_id: 151371,
|
||||
repository_id: 2,
|
||||
revision: 'e83eaf2380c65400dc03c6f3615d4b2cef669af3',
|
||||
author: 'Frédéric Wang <fred.wang@free.fr>',
|
||||
comments:
|
||||
'Bug 1322743 - Add STIX Two Math to the list of math fonts. r=karlt',
|
||||
},
|
||||
],
|
||||
};
|
||||
mockData = {
|
||||
push,
|
||||
repo,
|
||||
};
|
||||
});
|
||||
|
||||
it('renders the correct number of revisions in a list', () => {
|
||||
const wrapper = mount(
|
||||
<RevisionList
|
||||
repo={mockData.repo}
|
||||
push={mockData.push}
|
||||
/>,
|
||||
<RevisionList repo={mockData.repo} push={mockData.push} />,
|
||||
);
|
||||
expect(wrapper.find(Revision).length).toEqual(mockData.push.revision_count);
|
||||
});
|
||||
|
@ -77,14 +81,10 @@ describe('Revision list component', () => {
|
|||
mockData.push.revision_count = 21;
|
||||
|
||||
const wrapper = mount(
|
||||
<RevisionList
|
||||
repo={mockData.repo}
|
||||
push={mockData.push}
|
||||
/>,
|
||||
<RevisionList repo={mockData.repo} push={mockData.push} />,
|
||||
);
|
||||
expect(wrapper.find(MoreRevisionsLink).length).toEqual(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Revision item component', () => {
|
||||
|
@ -105,14 +105,16 @@ describe('Revision item component', () => {
|
|||
description: '',
|
||||
active_status: 'active',
|
||||
performance_alerts_enabled: true,
|
||||
pushlogURL: 'https://hg.mozilla.org/integration/mozilla-inbound/pushloghtml',
|
||||
pushlogURL:
|
||||
'https://hg.mozilla.org/integration/mozilla-inbound/pushloghtml',
|
||||
});
|
||||
const revision = {
|
||||
result_set_id: 151371,
|
||||
repository_id: 2,
|
||||
revision: '5a110ad242ead60e71d2186bae78b1fb766ad5ff',
|
||||
author: 'André Bargull <andre.bargull@gmail.com>',
|
||||
comments: 'Bug 1319926 - Part 2: Collect telemetry about deprecated String generics methods. r=jandem',
|
||||
comments:
|
||||
'Bug 1319926 - Part 2: Collect telemetry about deprecated String generics methods. r=jandem',
|
||||
};
|
||||
|
||||
mockData = {
|
||||
|
@ -123,21 +125,21 @@ describe('Revision item component', () => {
|
|||
|
||||
it('renders a linked revision', () => {
|
||||
const wrapper = mount(
|
||||
<Revision
|
||||
repo={mockData.repo}
|
||||
revision={mockData.revision}
|
||||
/>);
|
||||
<Revision repo={mockData.repo} revision={mockData.revision} />,
|
||||
);
|
||||
const link = wrapper.find('a').first();
|
||||
expect(link.props().href).toEqual(mockData.repo.getRevisionHref(mockData.revision.revision));
|
||||
expect(link.props().title).toEqual(`Open revision ${mockData.revision.revision} on ${mockData.repo.url}`);
|
||||
expect(link.props().href).toEqual(
|
||||
mockData.repo.getRevisionHref(mockData.revision.revision),
|
||||
);
|
||||
expect(link.props().title).toEqual(
|
||||
`Open revision ${mockData.revision.revision} on ${mockData.repo.url}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the contributors\' initials', () => {
|
||||
it("renders the contributors' initials", () => {
|
||||
const wrapper = mount(
|
||||
<Revision
|
||||
repo={mockData.repo}
|
||||
revision={mockData.revision}
|
||||
/>);
|
||||
<Revision repo={mockData.repo} revision={mockData.revision} />,
|
||||
);
|
||||
const initials = wrapper.find('.user-push-initials');
|
||||
expect(initials.length).toEqual(1);
|
||||
expect(initials.text()).toEqual('AB');
|
||||
|
@ -145,30 +147,28 @@ describe('Revision item component', () => {
|
|||
|
||||
it('linkifies bug IDs in the comments', () => {
|
||||
const wrapper = mount(
|
||||
<Revision
|
||||
repo={mockData.repo}
|
||||
revision={mockData.revision}
|
||||
/>);
|
||||
<Revision repo={mockData.repo} revision={mockData.revision} />,
|
||||
);
|
||||
|
||||
const comment = wrapper.find('.revision-comment em');
|
||||
expect(comment.html()).toEqual('<em><span class="Linkify"><a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1319926" target="_blank" rel="noopener noreferrer">Bug 1319926</a> - Part 2: Collect telemetry about deprecated String generics methods. r=jandem</span></em>');
|
||||
expect(comment.html()).toEqual(
|
||||
'<em><span class="Linkify"><a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1319926" target="_blank" rel="noopener noreferrer">Bug 1319926</a> - Part 2: Collect telemetry about deprecated String generics methods. r=jandem</span></em>',
|
||||
);
|
||||
});
|
||||
|
||||
it('marks the revision as backed out if the words "Back/Backed out" appear in the comments', () => {
|
||||
mockData.revision.comments = 'Backed out changeset a6e2d96c1274 (bug 1322565) for eslint failure';
|
||||
mockData.revision.comments =
|
||||
'Backed out changeset a6e2d96c1274 (bug 1322565) for eslint failure';
|
||||
let wrapper = mount(
|
||||
<Revision
|
||||
repo={mockData.repo}
|
||||
revision={mockData.revision}
|
||||
/>);
|
||||
<Revision repo={mockData.repo} revision={mockData.revision} />,
|
||||
);
|
||||
expect(wrapper.find({ 'data-tags': 'backout' }).length).toEqual(1);
|
||||
|
||||
mockData.revision.comments = 'Back out changeset a6e2d96c1274 (bug 1322565) for eslint failure';
|
||||
mockData.revision.comments =
|
||||
'Back out changeset a6e2d96c1274 (bug 1322565) for eslint failure';
|
||||
wrapper = mount(
|
||||
<Revision
|
||||
repo={mockData.repo}
|
||||
revision={mockData.revision}
|
||||
/>);
|
||||
<Revision repo={mockData.repo} revision={mockData.revision} />,
|
||||
);
|
||||
expect(wrapper.find({ 'data-tags': 'backout' }).length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
@ -192,32 +192,32 @@ describe('initials filter', () => {
|
|||
it('initializes a one-word name', () => {
|
||||
const name = 'Starscream';
|
||||
const initials = mount(
|
||||
<Initials
|
||||
title={`${name}: ${email}`}
|
||||
author={name}
|
||||
/>);
|
||||
expect(initials.html()).toEqual('<span title="Starscream: foo@bar.baz"><span class="user-push-icon"><i class="fa fa-user-o" aria-hidden="true"></i></span><div class="icon-superscript user-push-initials">S</div></span>');
|
||||
<Initials title={`${name}: ${email}`} author={name} />,
|
||||
);
|
||||
expect(initials.html()).toEqual(
|
||||
'<span title="Starscream: foo@bar.baz"><span class="user-push-icon"><i class="fa fa-user-o" aria-hidden="true"></i></span><div class="icon-superscript user-push-initials">S</div></span>',
|
||||
);
|
||||
});
|
||||
|
||||
it('initializes a two-word name', () => {
|
||||
const name = 'Optimus Prime';
|
||||
const initials = mount(
|
||||
<Initials
|
||||
title={`${name}: ${email}`}
|
||||
author={name}
|
||||
/>);
|
||||
<Initials title={`${name}: ${email}`} author={name} />,
|
||||
);
|
||||
const userPushInitials = initials.find('.user-push-initials');
|
||||
expect(userPushInitials.html()).toEqual('<div class="icon-superscript user-push-initials">OP</div>');
|
||||
expect(userPushInitials.html()).toEqual(
|
||||
'<div class="icon-superscript user-push-initials">OP</div>',
|
||||
);
|
||||
});
|
||||
|
||||
it('initializes a three-word name', () => {
|
||||
const name = 'Some Other Transformer';
|
||||
const initials = mount(
|
||||
<Initials
|
||||
title={`${name}: ${email}`}
|
||||
author={name}
|
||||
/>);
|
||||
<Initials title={`${name}: ${email}`} author={name} />,
|
||||
);
|
||||
const userPushInitials = initials.find('.user-push-initials');
|
||||
expect(userPushInitials.html()).toEqual('<div class="icon-superscript user-push-initials">ST</div>');
|
||||
expect(userPushInitials.html()).toEqual(
|
||||
'<div class="icon-superscript user-push-initials">ST</div>',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
export const escapeId = id => (
|
||||
id.replace(/(:|\[|\]|\?|,|\.|\s+)/g, '-')
|
||||
);
|
||||
export const escapeId = id => id.replace(/(:|\[|\]|\?|,|\.|\s+)/g, '-');
|
||||
|
||||
export const getPlatformRowId = (repoName, pushId, platformName, platformOptions) => (
|
||||
export const getPlatformRowId = (
|
||||
repoName,
|
||||
pushId,
|
||||
platformName,
|
||||
platformOptions,
|
||||
) =>
|
||||
// ensure there are no invalid characters in the id (like spaces, etc)
|
||||
escapeId(`${repoName}${pushId}${platformName}${platformOptions}`)
|
||||
);
|
||||
escapeId(`${repoName}${pushId}${platformName}${platformOptions}`);
|
||||
|
||||
export const getPushTableId = (repoName, pushId, revision) => (
|
||||
escapeId(`${repoName}${pushId}${revision}`)
|
||||
);
|
||||
export const getPushTableId = (repoName, pushId, revision) =>
|
||||
escapeId(`${repoName}${pushId}${revision}`);
|
||||
|
||||
export const getGroupMapKey = (grSymbol, grTier, plName, plOpt) => (
|
||||
export const getGroupMapKey = (grSymbol, grTier, plName, plOpt) =>
|
||||
// Build string key for groupMap entries
|
||||
escapeId(`${grSymbol}${grTier}${plName}${plOpt}`)
|
||||
);
|
||||
escapeId(`${grSymbol}${grTier}${plName}${plOpt}`);
|
||||
|
|
|
@ -8,12 +8,14 @@ export const webAuth = new WebAuth({
|
|||
domain: 'auth.mozilla.auth0.com',
|
||||
responseType: 'id_token token',
|
||||
audience: 'login.taskcluster.net',
|
||||
redirectUri: `${window.location.protocol}//${window.location.host}${loginCallbackUrl}`,
|
||||
redirectUri: `${window.location.protocol}//${
|
||||
window.location.host
|
||||
}${loginCallbackUrl}`,
|
||||
scope: 'taskcluster-credentials openid profile email',
|
||||
});
|
||||
|
||||
export const userSessionFromAuthResult = (authResult) => {
|
||||
const expiresAt = JSON.stringify((authResult.expiresIn * 1000) + Date.now());
|
||||
export const userSessionFromAuthResult = authResult => {
|
||||
const expiresAt = JSON.stringify(authResult.expiresIn * 1000 + Date.now());
|
||||
const userSession = {
|
||||
idToken: authResult.idToken,
|
||||
accessToken: authResult.accessToken,
|
||||
|
@ -31,7 +33,7 @@ export const userSessionFromAuthResult = (authResult) => {
|
|||
};
|
||||
|
||||
// Wrapper around webAuth's renewAuth
|
||||
export const renew = () => (
|
||||
export const renew = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
webAuth.renewAuth({}, (error, authResult) => {
|
||||
if (error) {
|
||||
|
@ -40,11 +42,10 @@ export const renew = () => (
|
|||
|
||||
return resolve(authResult);
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Wrapper around webAuth's parseHash
|
||||
export const parseHash = options => (
|
||||
export const parseHash = options =>
|
||||
new Promise((resolve, reject) => {
|
||||
webAuth.parseHash(options, (error, authResult) => {
|
||||
if (error) {
|
||||
|
@ -53,7 +54,11 @@ export const parseHash = options => (
|
|||
|
||||
return resolve(authResult);
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
export const loggedOutUser = { isStaff: false, username: '', email: '', isLoggedIn: false };
|
||||
export const loggedOutUser = {
|
||||
isStaff: false,
|
||||
username: '',
|
||||
email: '',
|
||||
isLoggedIn: false,
|
||||
};
|
||||
|
|
|
@ -22,13 +22,12 @@ export const stringOverlap = function stringOverlap(str1, str2) {
|
|||
|
||||
const tokenCounts = tokens.map(tokens => countBy(tokens, x => x));
|
||||
|
||||
const overlap = Object.keys(tokenCounts[0])
|
||||
.reduce((overlap, x) => {
|
||||
if (Object.prototype.hasOwnProperty.call(tokenCounts[1], x)) {
|
||||
overlap += 2 * Math.min(tokenCounts[0][x], tokenCounts[1][x]);
|
||||
}
|
||||
return overlap;
|
||||
}, 0);
|
||||
const overlap = Object.keys(tokenCounts[0]).reduce((overlap, x) => {
|
||||
if (Object.prototype.hasOwnProperty.call(tokenCounts[1], x)) {
|
||||
overlap += 2 * Math.min(tokenCounts[0][x], tokenCounts[1][x]);
|
||||
}
|
||||
return overlap;
|
||||
}, 0);
|
||||
|
||||
return overlap / (tokens[0].length + tokens[1].length);
|
||||
};
|
||||
|
@ -37,10 +36,12 @@ export const highlightLogLine = function highlightLogLine(logLine) {
|
|||
const parts = logLine.split(' | ', 3);
|
||||
return (
|
||||
<span>
|
||||
{parts[0].startsWith('TEST-UNEXPECTED') && <span>
|
||||
<strong className="failure-line-status">{parts[0]}</strong>
|
||||
<strong>{parts[1]}</strong>
|
||||
</span>}
|
||||
{parts[0].startsWith('TEST-UNEXPECTED') && (
|
||||
<span>
|
||||
<strong className="failure-line-status">{parts[0]}</strong>
|
||||
<strong>{parts[1]}</strong>
|
||||
</span>
|
||||
)}
|
||||
{!parts[0].startsWith('TEST-UNEXPECTED') && logLine}
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -5,181 +5,181 @@ import closedTreeFavicon from '../img/tree_closed.png';
|
|||
// to a specific helper or into the classes that use them.
|
||||
|
||||
export const thPlatformMap = {
|
||||
linux32: 'Linux',
|
||||
'linux32-devedition': 'Linux DevEdition',
|
||||
'linux32-qr': 'Linux QuantumRender',
|
||||
'linux32-nightly': 'Linux Nightly',
|
||||
'linux32-stylo': 'Linux Stylo',
|
||||
'linux32-stylo-disabled': 'Linux Stylo Disabled',
|
||||
linux64: 'Linux x64',
|
||||
'linux64-asan': 'Linux x64 asan',
|
||||
'linux64-add-on-devel': 'Linux x64 addon',
|
||||
'linux64-devedition': 'Linux x64 DevEdition',
|
||||
'linux64-qr': 'Linux x64 QuantumRender',
|
||||
'linux64-nightly': 'Linux x64 Nightly',
|
||||
'linux64-stylo': 'Linux x64 Stylo',
|
||||
'linux64-stylo-disabled': 'Linux x64 Stylo Disabled',
|
||||
'linux64-stylo-sequential': 'Linux x64 Stylo-Seq',
|
||||
'linux64-ccov': 'Linux x64 CCov',
|
||||
'linux64-jsdcov': 'Linux x64 JSDCov',
|
||||
'linux64-noopt': 'Linux x64 NoOpt',
|
||||
'linux64-dmd': 'Linux x64 DMD',
|
||||
'osx-10-6': 'OS X 10.6',
|
||||
'osx-10-7': 'OS X 10.7',
|
||||
'osx-10-7-add-on-devel': 'OS X 10.7 addon',
|
||||
'osx-10-7-devedition': 'OS X 10.7 DevEdition',
|
||||
'osx-10-8': 'OS X 10.8',
|
||||
'osx-10-9': 'OS X 10.9',
|
||||
'osx-10-10': 'OS X 10.10',
|
||||
'osx-10-10-devedition': 'OS X 10.10 DevEdition',
|
||||
'osx-10-10-dmd': 'OS X 10.10 DMD',
|
||||
'osx-10-11': 'OS X 10.11',
|
||||
'osx-10-7-noopt': 'OS X 10.7 NoOpt',
|
||||
'osx-cross': 'OS X Cross Compiled',
|
||||
'osx-cross-noopt': 'OS X Cross Compiled NoOpt',
|
||||
'osx-cross-add-on-devel': 'OS X Cross Compiled addon',
|
||||
'osx-cross-devedition': 'OS X Cross Compiled DevEdition',
|
||||
'macosx64-qr': 'OS X 10.10 QuantumRender',
|
||||
'macosx64-stylo': 'OS X 10.10 Stylo',
|
||||
'macosx64-stylo-disabled': 'OS X 10.10 Stylo Disabled',
|
||||
'macosx64-devedition': 'OS X 10.10 DevEdition',
|
||||
'macosx64-nightly': 'OS X 10.10 Nightly',
|
||||
windowsxp: 'Windows XP',
|
||||
'windowsxp-devedition': 'Windows XP DevEdition',
|
||||
'windows7-32': 'Windows 7',
|
||||
'windows7-32-vm': 'Windows 7 VM',
|
||||
'windows7-32-devedition': 'Windows 7 DevEdition',
|
||||
'windows7-32-stylo-disabled': 'Windows 7 Stylo Disabled',
|
||||
'windows7-32-vm-devedition': 'Windows 7 VM DevEdition',
|
||||
'windows7-32-nightly': 'Windows 7 VM Nightly',
|
||||
'windows7-32-stylo': 'Windows 7 VM Stylo',
|
||||
'windows7-64': 'Windows 7 x64',
|
||||
'windows8-32': 'Windows 8',
|
||||
'windows8-64': 'Windows 8 x64',
|
||||
'windows8-64-devedition': 'Windows 8 x64 DevEdition',
|
||||
'windows10-32': 'Windows 10',
|
||||
'windows10-64': 'Windows 10 x64',
|
||||
'windows10-64-vm': 'Windows 10 x64 VM',
|
||||
'windows10-64-devedition': 'Windows 10 x64 DevEdition',
|
||||
'windows10-64-nightly': 'Windows 10 x64 Nightly',
|
||||
'windows10-64-stylo': 'Windows 10 x64 Stylo',
|
||||
'windows10-64-stylo-disabled': 'Windows 10 x64 Stylo Disabled',
|
||||
'windows10-64-qr': 'Windows 10 x64 QuantumRender',
|
||||
'windows2012-32': 'Windows 2012',
|
||||
'windows2012-32-add-on-devel': 'Windows 2012 addon',
|
||||
'windows2012-32-noopt': 'Windows 2012 NoOpt',
|
||||
'windows2012-32-devedition': 'Windows 2012 DevEdition',
|
||||
'windows2012-32-dmd': 'Windows 2012 DMD',
|
||||
'windows2012-64': 'Windows 2012 x64',
|
||||
'windows2012-64-add-on-devel': 'Windows 2012 x64 addon',
|
||||
'windows2012-64-noopt': 'Windows 2012 x64 NoOpt',
|
||||
'windows2012-64-devedition': 'Windows 2012 x64 DevEdition',
|
||||
'windows2012-64-dmd': 'Windows 2012 x64 DMD',
|
||||
'windows-mingw32': 'Windows MinGW',
|
||||
linux32: 'Linux',
|
||||
'linux32-devedition': 'Linux DevEdition',
|
||||
'linux32-qr': 'Linux QuantumRender',
|
||||
'linux32-nightly': 'Linux Nightly',
|
||||
'linux32-stylo': 'Linux Stylo',
|
||||
'linux32-stylo-disabled': 'Linux Stylo Disabled',
|
||||
linux64: 'Linux x64',
|
||||
'linux64-asan': 'Linux x64 asan',
|
||||
'linux64-add-on-devel': 'Linux x64 addon',
|
||||
'linux64-devedition': 'Linux x64 DevEdition',
|
||||
'linux64-qr': 'Linux x64 QuantumRender',
|
||||
'linux64-nightly': 'Linux x64 Nightly',
|
||||
'linux64-stylo': 'Linux x64 Stylo',
|
||||
'linux64-stylo-disabled': 'Linux x64 Stylo Disabled',
|
||||
'linux64-stylo-sequential': 'Linux x64 Stylo-Seq',
|
||||
'linux64-ccov': 'Linux x64 CCov',
|
||||
'linux64-jsdcov': 'Linux x64 JSDCov',
|
||||
'linux64-noopt': 'Linux x64 NoOpt',
|
||||
'linux64-dmd': 'Linux x64 DMD',
|
||||
'osx-10-6': 'OS X 10.6',
|
||||
'osx-10-7': 'OS X 10.7',
|
||||
'osx-10-7-add-on-devel': 'OS X 10.7 addon',
|
||||
'osx-10-7-devedition': 'OS X 10.7 DevEdition',
|
||||
'osx-10-8': 'OS X 10.8',
|
||||
'osx-10-9': 'OS X 10.9',
|
||||
'osx-10-10': 'OS X 10.10',
|
||||
'osx-10-10-devedition': 'OS X 10.10 DevEdition',
|
||||
'osx-10-10-dmd': 'OS X 10.10 DMD',
|
||||
'osx-10-11': 'OS X 10.11',
|
||||
'osx-10-7-noopt': 'OS X 10.7 NoOpt',
|
||||
'osx-cross': 'OS X Cross Compiled',
|
||||
'osx-cross-noopt': 'OS X Cross Compiled NoOpt',
|
||||
'osx-cross-add-on-devel': 'OS X Cross Compiled addon',
|
||||
'osx-cross-devedition': 'OS X Cross Compiled DevEdition',
|
||||
'macosx64-qr': 'OS X 10.10 QuantumRender',
|
||||
'macosx64-stylo': 'OS X 10.10 Stylo',
|
||||
'macosx64-stylo-disabled': 'OS X 10.10 Stylo Disabled',
|
||||
'macosx64-devedition': 'OS X 10.10 DevEdition',
|
||||
'macosx64-nightly': 'OS X 10.10 Nightly',
|
||||
windowsxp: 'Windows XP',
|
||||
'windowsxp-devedition': 'Windows XP DevEdition',
|
||||
'windows7-32': 'Windows 7',
|
||||
'windows7-32-vm': 'Windows 7 VM',
|
||||
'windows7-32-devedition': 'Windows 7 DevEdition',
|
||||
'windows7-32-stylo-disabled': 'Windows 7 Stylo Disabled',
|
||||
'windows7-32-vm-devedition': 'Windows 7 VM DevEdition',
|
||||
'windows7-32-nightly': 'Windows 7 VM Nightly',
|
||||
'windows7-32-stylo': 'Windows 7 VM Stylo',
|
||||
'windows7-64': 'Windows 7 x64',
|
||||
'windows8-32': 'Windows 8',
|
||||
'windows8-64': 'Windows 8 x64',
|
||||
'windows8-64-devedition': 'Windows 8 x64 DevEdition',
|
||||
'windows10-32': 'Windows 10',
|
||||
'windows10-64': 'Windows 10 x64',
|
||||
'windows10-64-vm': 'Windows 10 x64 VM',
|
||||
'windows10-64-devedition': 'Windows 10 x64 DevEdition',
|
||||
'windows10-64-nightly': 'Windows 10 x64 Nightly',
|
||||
'windows10-64-stylo': 'Windows 10 x64 Stylo',
|
||||
'windows10-64-stylo-disabled': 'Windows 10 x64 Stylo Disabled',
|
||||
'windows10-64-qr': 'Windows 10 x64 QuantumRender',
|
||||
'windows2012-32': 'Windows 2012',
|
||||
'windows2012-32-add-on-devel': 'Windows 2012 addon',
|
||||
'windows2012-32-noopt': 'Windows 2012 NoOpt',
|
||||
'windows2012-32-devedition': 'Windows 2012 DevEdition',
|
||||
'windows2012-32-dmd': 'Windows 2012 DMD',
|
||||
'windows2012-64': 'Windows 2012 x64',
|
||||
'windows2012-64-add-on-devel': 'Windows 2012 x64 addon',
|
||||
'windows2012-64-noopt': 'Windows 2012 x64 NoOpt',
|
||||
'windows2012-64-devedition': 'Windows 2012 x64 DevEdition',
|
||||
'windows2012-64-dmd': 'Windows 2012 x64 DMD',
|
||||
'windows-mingw32': 'Windows MinGW',
|
||||
|
||||
'android-2-2-armv6': 'Android 2.2 Armv6',
|
||||
'android-2-2': 'Android 2.2',
|
||||
'android-2-3-armv6': 'Android 2.3 Armv6',
|
||||
'android-2-3': 'Android 2.3',
|
||||
'android-2-3-armv7-api9': 'Android 2.3 API9',
|
||||
'android-4-0': 'Android 4.0',
|
||||
'android-4-0-armv7-api10': 'Android 4.0 API10+',
|
||||
'android-4-0-armv7-api11': 'Android 4.0 API11+',
|
||||
'android-4-0-armv7-api15': 'Android 4.0 API15+',
|
||||
'android-4-0-armv7-api15-old-id': 'Android 4.0 API15+ OldId',
|
||||
'android-4-0-armv7-api16': 'Android 4.0 API16+',
|
||||
'android-em-4-0-armv7-api16': 'Android 4.0 API16+',
|
||||
'android-4-0-armv7-api16-old-id': 'Android 4.0 API16+ OldId',
|
||||
'android-4-2-x86': 'Android 4.2 x86',
|
||||
'android-em-4-2-x86': 'Android 4.2 x86',
|
||||
'android-4-2-x86-old-id': 'Android 4.2 x86 OldId',
|
||||
'android-4-2': 'Android 4.2',
|
||||
'android-4-2-armv7-api11': 'Android 4.2 API11+',
|
||||
'android-4-2-armv7-api15': 'Android 4.2 API15+',
|
||||
'android-4-2-armv7-api16': 'Android 4.2 API16+',
|
||||
'android-em-4-2-armv7-api16': 'Android 4.2 API16+',
|
||||
'android-4-3': 'Android 4.3',
|
||||
'android-4-3-armv7-api11': 'Android 4.3 API11+',
|
||||
'android-4-3-armv7-api15': 'Android 4.3 API15+',
|
||||
'android-4-3-armv7-api16': 'Android 4.3 API16+',
|
||||
'android-em-4-3-armv7-api16': 'Android 4.3 API16+',
|
||||
'android-em-4-3-armv7-api16-ccov': 'Android 4.3 API16+ CCov',
|
||||
'android-4-4': 'Android 4.4',
|
||||
'android-4-4-armv7-api11': 'Android 4.4 API11+',
|
||||
'android-4-4-armv7-api15': 'Android 4.4 API15+',
|
||||
'android-4-4-armv7-api16': 'Android 4.4 API16+',
|
||||
'android-5-0-aarch64': 'Android 5.0 AArch64',
|
||||
'android-5-0-armv7-api11': 'Android 5.0 API11+',
|
||||
'android-5-0-armv7-api15': 'Android 5.0 API15+',
|
||||
'android-5-0-armv8-api15': 'Android 5.0 API15+',
|
||||
'android-5-0-armv8-api16': 'Android 5.0 API16+',
|
||||
'android-5-0-x86_64': 'Android 5.0 x86-64',
|
||||
'android-5-1-armv7-api15': 'Android 5.1 API15+',
|
||||
'android-6-0-armv8-api15': 'Android 6.0 API15+',
|
||||
'android-6-0-armv8-api16': 'Android 6.0 API16+',
|
||||
'android-em-7-0-x86': 'Android 7.0 x86',
|
||||
'android-7-1-armv8-api15': 'Android 7.1 API15+',
|
||||
'android-7-1-armv8-api16': 'Android 7.1 API16+',
|
||||
'b2gdroid-4-0-armv7-api11': 'B2GDroid 4.0 API11+',
|
||||
'b2gdroid-4-0-armv7-api15': 'B2GDroid 4.0 API15+',
|
||||
'android-4-0-armv7-api11-partner1': 'Android API11+ partner1',
|
||||
'android-4-0-armv7-api15-partner1': 'Android API15+ partner1',
|
||||
'android-api-15-gradle': 'Android API15+ Gradle',
|
||||
'android-api-16-gradle': 'Android API16+ Gradle',
|
||||
'android-hw-g5-7-0-arm7-api-16': 'Android 7.0 MotoG5',
|
||||
'android-hw-p2-8-0-arm7-api-16': 'Android 8.0 Pixel2',
|
||||
'android-hw-p2-8-0-android-aarch64': 'Android 8.0 Pixel2 AArch64',
|
||||
Android: 'Android',
|
||||
'android-2-2-armv6': 'Android 2.2 Armv6',
|
||||
'android-2-2': 'Android 2.2',
|
||||
'android-2-3-armv6': 'Android 2.3 Armv6',
|
||||
'android-2-3': 'Android 2.3',
|
||||
'android-2-3-armv7-api9': 'Android 2.3 API9',
|
||||
'android-4-0': 'Android 4.0',
|
||||
'android-4-0-armv7-api10': 'Android 4.0 API10+',
|
||||
'android-4-0-armv7-api11': 'Android 4.0 API11+',
|
||||
'android-4-0-armv7-api15': 'Android 4.0 API15+',
|
||||
'android-4-0-armv7-api15-old-id': 'Android 4.0 API15+ OldId',
|
||||
'android-4-0-armv7-api16': 'Android 4.0 API16+',
|
||||
'android-em-4-0-armv7-api16': 'Android 4.0 API16+',
|
||||
'android-4-0-armv7-api16-old-id': 'Android 4.0 API16+ OldId',
|
||||
'android-4-2-x86': 'Android 4.2 x86',
|
||||
'android-em-4-2-x86': 'Android 4.2 x86',
|
||||
'android-4-2-x86-old-id': 'Android 4.2 x86 OldId',
|
||||
'android-4-2': 'Android 4.2',
|
||||
'android-4-2-armv7-api11': 'Android 4.2 API11+',
|
||||
'android-4-2-armv7-api15': 'Android 4.2 API15+',
|
||||
'android-4-2-armv7-api16': 'Android 4.2 API16+',
|
||||
'android-em-4-2-armv7-api16': 'Android 4.2 API16+',
|
||||
'android-4-3': 'Android 4.3',
|
||||
'android-4-3-armv7-api11': 'Android 4.3 API11+',
|
||||
'android-4-3-armv7-api15': 'Android 4.3 API15+',
|
||||
'android-4-3-armv7-api16': 'Android 4.3 API16+',
|
||||
'android-em-4-3-armv7-api16': 'Android 4.3 API16+',
|
||||
'android-em-4-3-armv7-api16-ccov': 'Android 4.3 API16+ CCov',
|
||||
'android-4-4': 'Android 4.4',
|
||||
'android-4-4-armv7-api11': 'Android 4.4 API11+',
|
||||
'android-4-4-armv7-api15': 'Android 4.4 API15+',
|
||||
'android-4-4-armv7-api16': 'Android 4.4 API16+',
|
||||
'android-5-0-aarch64': 'Android 5.0 AArch64',
|
||||
'android-5-0-armv7-api11': 'Android 5.0 API11+',
|
||||
'android-5-0-armv7-api15': 'Android 5.0 API15+',
|
||||
'android-5-0-armv8-api15': 'Android 5.0 API15+',
|
||||
'android-5-0-armv8-api16': 'Android 5.0 API16+',
|
||||
'android-5-0-x86_64': 'Android 5.0 x86-64',
|
||||
'android-5-1-armv7-api15': 'Android 5.1 API15+',
|
||||
'android-6-0-armv8-api15': 'Android 6.0 API15+',
|
||||
'android-6-0-armv8-api16': 'Android 6.0 API16+',
|
||||
'android-em-7-0-x86': 'Android 7.0 x86',
|
||||
'android-7-1-armv8-api15': 'Android 7.1 API15+',
|
||||
'android-7-1-armv8-api16': 'Android 7.1 API16+',
|
||||
'b2gdroid-4-0-armv7-api11': 'B2GDroid 4.0 API11+',
|
||||
'b2gdroid-4-0-armv7-api15': 'B2GDroid 4.0 API15+',
|
||||
'android-4-0-armv7-api11-partner1': 'Android API11+ partner1',
|
||||
'android-4-0-armv7-api15-partner1': 'Android API15+ partner1',
|
||||
'android-api-15-gradle': 'Android API15+ Gradle',
|
||||
'android-api-16-gradle': 'Android API16+ Gradle',
|
||||
'android-hw-g5-7-0-arm7-api-16': 'Android 7.0 MotoG5',
|
||||
'android-hw-p2-8-0-arm7-api-16': 'Android 8.0 Pixel2',
|
||||
'android-hw-p2-8-0-android-aarch64': 'Android 8.0 Pixel2 AArch64',
|
||||
Android: 'Android',
|
||||
|
||||
'b2g-linux32': 'B2G Desktop Linux',
|
||||
'b2g-linux64': 'B2G Desktop Linux x64',
|
||||
'b2g-osx': 'B2G Desktop OS X',
|
||||
'b2g-win32': 'B2G Desktop Windows',
|
||||
'b2g-emu-ics': 'B2G ICS Emulator',
|
||||
'b2g-emu-jb': 'B2G JB Emulator',
|
||||
'b2g-emu-kk': 'B2G KK Emulator',
|
||||
'b2g-emu-x86-kk': 'B2G KK Emulator x86',
|
||||
'b2g-emu-l': 'B2G L Emulator',
|
||||
'b2g-device-image': 'B2G Device Image',
|
||||
'mulet-linux32': 'Mulet Linux',
|
||||
'mulet-linux64': 'Mulet Linux x64',
|
||||
'mulet-osx': 'Mulet OS X',
|
||||
'mulet-win32': 'Mulet Windows',
|
||||
'b2g-linux32': 'B2G Desktop Linux',
|
||||
'b2g-linux64': 'B2G Desktop Linux x64',
|
||||
'b2g-osx': 'B2G Desktop OS X',
|
||||
'b2g-win32': 'B2G Desktop Windows',
|
||||
'b2g-emu-ics': 'B2G ICS Emulator',
|
||||
'b2g-emu-jb': 'B2G JB Emulator',
|
||||
'b2g-emu-kk': 'B2G KK Emulator',
|
||||
'b2g-emu-x86-kk': 'B2G KK Emulator x86',
|
||||
'b2g-emu-l': 'B2G L Emulator',
|
||||
'b2g-device-image': 'B2G Device Image',
|
||||
'mulet-linux32': 'Mulet Linux',
|
||||
'mulet-linux64': 'Mulet Linux x64',
|
||||
'mulet-osx': 'Mulet OS X',
|
||||
'mulet-win32': 'Mulet Windows',
|
||||
|
||||
'graphene-linux64': 'Graphene Linux x64',
|
||||
'graphene-osx': 'Graphene OS X',
|
||||
'graphene-win64': 'Graphene Windows x64',
|
||||
'horizon-linux64': 'Horizon Linux x64',
|
||||
'horizon-osx': 'Horizon OS X',
|
||||
'horizon-win64': 'Horizon Windows x64',
|
||||
'graphene-linux64': 'Graphene Linux x64',
|
||||
'graphene-osx': 'Graphene OS X',
|
||||
'graphene-win64': 'Graphene Windows x64',
|
||||
'horizon-linux64': 'Horizon Linux x64',
|
||||
'horizon-osx': 'Horizon OS X',
|
||||
'horizon-win64': 'Horizon Windows x64',
|
||||
|
||||
'gecko-decision': 'Gecko Decision Task',
|
||||
'firefox-release': 'Firefox Release Tasks',
|
||||
'devedition-release': 'Devedition Release Tasks',
|
||||
'fennec-release': 'Fennec Release Tasks',
|
||||
'thunderbird-release': 'Thunderbird Release Tasks',
|
||||
lint: 'Linting',
|
||||
'release-mozilla-release-': 'Balrog Publishing',
|
||||
'taskcluster-images': 'Docker Images',
|
||||
packages: 'Packages',
|
||||
toolchains: 'Toolchains',
|
||||
diff: 'Diffoscope',
|
||||
other: 'Other',
|
||||
'gecko-decision': 'Gecko Decision Task',
|
||||
'firefox-release': 'Firefox Release Tasks',
|
||||
'devedition-release': 'Devedition Release Tasks',
|
||||
'fennec-release': 'Fennec Release Tasks',
|
||||
'thunderbird-release': 'Thunderbird Release Tasks',
|
||||
lint: 'Linting',
|
||||
'release-mozilla-release-': 'Balrog Publishing',
|
||||
'taskcluster-images': 'Docker Images',
|
||||
packages: 'Packages',
|
||||
toolchains: 'Toolchains',
|
||||
diff: 'Diffoscope',
|
||||
other: 'Other',
|
||||
};
|
||||
|
||||
// Platforms where the `opt` should be dropped from
|
||||
export const thSimplePlatforms = [
|
||||
'gecko-decision',
|
||||
'firefox-release',
|
||||
'devedition-release',
|
||||
'fennec-release',
|
||||
'thunderbird-release',
|
||||
'lint',
|
||||
'release-mozilla-release-',
|
||||
'taskcluster-images',
|
||||
'packages',
|
||||
'toolchains',
|
||||
'diff',
|
||||
'gecko-decision',
|
||||
'firefox-release',
|
||||
'devedition-release',
|
||||
'fennec-release',
|
||||
'thunderbird-release',
|
||||
'lint',
|
||||
'release-mozilla-release-',
|
||||
'taskcluster-images',
|
||||
'packages',
|
||||
'toolchains',
|
||||
'diff',
|
||||
];
|
||||
|
||||
export const thFailureResults = ['testfailed', 'busted', 'exception'];
|
||||
|
@ -226,7 +226,8 @@ export const thJobNavSelectors = {
|
|||
},
|
||||
UNCLASSIFIED_FAILURES: {
|
||||
name: 'unclassified failures',
|
||||
selector: '.selected-job, .job-btn.btn-red, .job-btn.btn-orange, .job-btn.btn-purple, .job-btn.autoclassified',
|
||||
selector:
|
||||
'.selected-job, .job-btn.btn-red, .job-btn.btn-orange, .job-btn.btn-purple, .job-btn.autoclassified',
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -261,7 +262,8 @@ export const phTimeRanges = [
|
|||
{ value: 2592000, text: 'Last 30 days' },
|
||||
{ value: 5184000, text: 'Last 60 days' },
|
||||
{ value: 7776000, text: 'Last 90 days' },
|
||||
{ value: 31536000, text: 'Last year' }];
|
||||
{ value: 31536000, text: 'Last year' },
|
||||
];
|
||||
|
||||
export const phDefaultTimeRangeValue = 1209600;
|
||||
|
||||
|
@ -272,10 +274,10 @@ export const phTimeRangeValues = {
|
|||
export const phDefaultFramework = 'talos';
|
||||
|
||||
export const phFrameworksWithRelatedBranches = [
|
||||
1, // talos
|
||||
10, // raptor
|
||||
11, // js-bench
|
||||
12, // devtools
|
||||
1, // talos
|
||||
10, // raptor
|
||||
11, // js-bench
|
||||
12, // devtools
|
||||
];
|
||||
|
||||
export const phAlertSummaryStatusMap = {
|
||||
|
|
|
@ -28,9 +28,10 @@ export const toShortDateStr = function toDateStr(timestamp) {
|
|||
export const getSearchWords = function getHighlighterArray(text) {
|
||||
const tokens = text.split(/[^a-zA-Z0-9_-]+/);
|
||||
|
||||
return tokens.reduce((acc, token) => (
|
||||
token.length > 1 ? [...acc, token] : acc
|
||||
), []);
|
||||
return tokens.reduce(
|
||||
(acc, token) => (token.length > 1 ? [...acc, token] : acc),
|
||||
[],
|
||||
);
|
||||
};
|
||||
|
||||
export const getPercentComplete = function getPercentComplete(counts) {
|
||||
|
@ -38,5 +39,5 @@ export const getPercentComplete = function getPercentComplete(counts) {
|
|||
const inProgress = pending + running;
|
||||
const total = completed + inProgress;
|
||||
|
||||
return total > 0 ? Math.floor(((completed / total) * 100)) : 0;
|
||||
return total > 0 ? Math.floor((completed / total) * 100) : 0;
|
||||
};
|
||||
|
|
|
@ -11,19 +11,20 @@ Helper method for constructing an error message from the server side.
|
|||
found in the error object.
|
||||
*/
|
||||
export const formatModelError = function formatModelError(e, message) {
|
||||
// Generic error message when we encounter 401 status codes from the
|
||||
// server.
|
||||
const AUTH_ERROR_MSG = 'Please login to Treeherder to complete this action';
|
||||
// Generic error message when we encounter 401 status codes from the
|
||||
// server.
|
||||
const AUTH_ERROR_MSG = 'Please login to Treeherder to complete this action';
|
||||
|
||||
// If we failed to authenticate for some reason return a nicer error message.
|
||||
if (e.status === 401 || e.status === 403) {
|
||||
return AUTH_ERROR_MSG;
|
||||
}
|
||||
|
||||
// If there is nothing in the server message use the HTTP response status.
|
||||
const errorMessage = `${(e.data && e.data.detail) || e.status} ${e.statusText}`;
|
||||
return `${message}: ${errorMessage}`;
|
||||
// If we failed to authenticate for some reason return a nicer error message.
|
||||
if (e.status === 401 || e.status === 403) {
|
||||
return AUTH_ERROR_MSG;
|
||||
}
|
||||
|
||||
// If there is nothing in the server message use the HTTP response status.
|
||||
const errorMessage = `${(e.data && e.data.detail) || e.status} ${
|
||||
e.statusText
|
||||
}`;
|
||||
return `${message}: ${errorMessage}`;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -37,7 +38,7 @@ export const formatTaskclusterError = function formatTaskclusterError(e) {
|
|||
const errorMessage = err.message || err.toString();
|
||||
|
||||
if (errorMessage.indexOf('----') !== -1) {
|
||||
return `${TC_ERROR_PREFIX}${errorMessage.split('----')[0]}`;
|
||||
return `${TC_ERROR_PREFIX}${errorMessage.split('----')[0]}`;
|
||||
}
|
||||
|
||||
return `${TC_ERROR_PREFIX}${errorMessage}`;
|
||||
|
|
|
@ -20,7 +20,10 @@ export const thFieldChoices = {
|
|||
machine_name: { name: 'machine name', matchType: thMatchType.substr },
|
||||
platform: { name: 'platform', matchType: thMatchType.substr },
|
||||
tier: { name: 'tier', matchType: thMatchType.exactstr },
|
||||
failure_classification_id: { name: 'failure classification', matchType: thMatchType.choice },
|
||||
failure_classification_id: {
|
||||
name: 'failure classification',
|
||||
matchType: thMatchType.choice,
|
||||
},
|
||||
// text search across multiple fields
|
||||
searchStr: { name: 'search string', matchType: thMatchType.searchStr },
|
||||
};
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import $ from 'jquery';
|
||||
|
||||
import {
|
||||
thFailureResults,
|
||||
thPlatformMap,
|
||||
} from './constants';
|
||||
import { thFailureResults, thPlatformMap } from './constants';
|
||||
import { getGroupMapKey } from './aggregateId';
|
||||
|
||||
const btnClasses = {
|
||||
|
@ -34,7 +31,10 @@ export const getStatus = function getStatus(job) {
|
|||
// Get the CSS class for job buttons as well as jobs that show in the pinboard.
|
||||
// These also apply to result "groupings" like ``failures`` and ``in progress``
|
||||
// for the colored filter chicklets on the nav bar.
|
||||
export const getBtnClass = function getBtnClass(resultStatus, failureClassificationId) {
|
||||
export const getBtnClass = function getBtnClass(
|
||||
resultStatus,
|
||||
failureClassificationId,
|
||||
) {
|
||||
let btnClass = btnClasses[resultStatus] || 'btn-default';
|
||||
|
||||
// handle if a job is classified
|
||||
|
@ -54,14 +54,17 @@ export const getJobBtnClass = function getJobBtnClass(job) {
|
|||
};
|
||||
|
||||
export const isReftest = function isReftest(job) {
|
||||
return [job.job_group_name, job.job_type_name]
|
||||
.some(name => name.toLowerCase().includes('reftest'));
|
||||
return [job.job_group_name, job.job_type_name].some(name =>
|
||||
name.toLowerCase().includes('reftest'),
|
||||
);
|
||||
};
|
||||
|
||||
export const isPerfTest = function isPerfTest(job) {
|
||||
return [job.job_group_name, job.job_type_name]
|
||||
.some(name => name.toLowerCase().includes('talos') ||
|
||||
name.toLowerCase().includes('raptor'));
|
||||
return [job.job_group_name, job.job_type_name].some(
|
||||
name =>
|
||||
name.toLowerCase().includes('talos') ||
|
||||
name.toLowerCase().includes('raptor'),
|
||||
);
|
||||
};
|
||||
|
||||
export const isClassified = function isClassified(job) {
|
||||
|
@ -69,14 +72,15 @@ export const isClassified = function isClassified(job) {
|
|||
};
|
||||
|
||||
export const isUnclassifiedFailure = function isUnclassifiedFailure(job) {
|
||||
return (thFailureResults.includes(job.result) &&
|
||||
!isClassified(job));
|
||||
return thFailureResults.includes(job.result) && !isClassified(job);
|
||||
};
|
||||
|
||||
// Fetch the React instance of an object from a DOM element.
|
||||
// Credit for this approach goes to SO: https://stackoverflow.com/a/48335220/333614
|
||||
export const findInstance = function findInstance(el) {
|
||||
const key = Object.keys(el).find(key => key.startsWith('__reactInternalInstance$'));
|
||||
const key = Object.keys(el).find(key =>
|
||||
key.startsWith('__reactInternalInstance$'),
|
||||
);
|
||||
if (key) {
|
||||
const fiberNode = el[key];
|
||||
return fiberNode && fiberNode.return && fiberNode.return.stateNode;
|
||||
|
@ -86,7 +90,9 @@ export const findInstance = function findInstance(el) {
|
|||
|
||||
// Fetch the React instance of the currently selected job.
|
||||
export const findSelectedInstance = function findSelectedInstance() {
|
||||
const selectedEl = $('.th-view-content').find('.job-btn.selected-job').first();
|
||||
const selectedEl = $('.th-view-content')
|
||||
.find('.job-btn.selected-job')
|
||||
.first();
|
||||
if (selectedEl.length) {
|
||||
return findInstance(selectedEl[0]);
|
||||
}
|
||||
|
@ -95,16 +101,19 @@ export const findSelectedInstance = function findSelectedInstance() {
|
|||
// Check if the element is visible on screen or not.
|
||||
const isOnScreen = function isOnScreen(el) {
|
||||
const viewport = {};
|
||||
viewport.top = $(window).scrollTop() + $('#global-navbar-container').height() + 30;
|
||||
viewport.top =
|
||||
$(window).scrollTop() + $('#global-navbar-container').height() + 30;
|
||||
const filterbarheight = $('.active-filters-bar').height();
|
||||
viewport.top = filterbarheight > 0 ? viewport.top + filterbarheight : viewport.top;
|
||||
viewport.top =
|
||||
filterbarheight > 0 ? viewport.top + filterbarheight : viewport.top;
|
||||
const updatebarheight = $('.update-alert-panel').height();
|
||||
viewport.top = updatebarheight > 0 ? viewport.top + updatebarheight : viewport.top;
|
||||
viewport.top =
|
||||
updatebarheight > 0 ? viewport.top + updatebarheight : viewport.top;
|
||||
viewport.bottom = $(window).height() - $('#details-panel').height() - 20;
|
||||
const bounds = {};
|
||||
bounds.top = el.offset().top;
|
||||
bounds.bottom = bounds.top + el.outerHeight();
|
||||
return ((bounds.top <= viewport.bottom) && (bounds.bottom >= viewport.top));
|
||||
return bounds.top <= viewport.bottom && bounds.bottom >= viewport.top;
|
||||
};
|
||||
|
||||
// Scroll the element into view.
|
||||
|
@ -128,11 +137,16 @@ export const scrollToElement = function scrollToElement(el, duration) {
|
|||
|
||||
export const findGroupElement = function findGroupElement(job) {
|
||||
const { push_id, job_group_symbol, tier, platform, platform_option } = job;
|
||||
const groupMapKey = getGroupMapKey(push_id, job_group_symbol, tier, platform, platform_option);
|
||||
const groupMapKey = getGroupMapKey(
|
||||
push_id,
|
||||
job_group_symbol,
|
||||
tier,
|
||||
platform,
|
||||
platform_option,
|
||||
);
|
||||
const viewContent = $('.th-view-content');
|
||||
|
||||
return viewContent.find(
|
||||
`span[data-group-key='${groupMapKey}']`).first();
|
||||
return viewContent.find(`span[data-group-key='${groupMapKey}']`).first();
|
||||
};
|
||||
|
||||
export const findGroupInstance = function findGroupInstance(job) {
|
||||
|
@ -146,8 +160,9 @@ export const findGroupInstance = function findGroupInstance(job) {
|
|||
// Fetch the React instance based on the jobId, and if scrollTo
|
||||
// is true, then scroll it into view.
|
||||
export const findJobInstance = function findJobInstance(jobId, scrollTo) {
|
||||
const jobEl = $('.th-view-content').find(
|
||||
`button[data-job-id='${jobId}']`).first();
|
||||
const jobEl = $('.th-view-content')
|
||||
.find(`button[data-job-id='${jobId}']`)
|
||||
.first();
|
||||
|
||||
if (jobEl.length) {
|
||||
if (scrollTo) {
|
||||
|
@ -161,15 +176,18 @@ export const getSearchStr = function getSearchStr(job) {
|
|||
// we want to join the group and type information together
|
||||
// so we can search for it as one token (useful when
|
||||
// we want to do a search on something like `fxup-esr(`)
|
||||
const symbolInfo = (job.job_group_symbol === '?') ? '' : job.job_group_symbol;
|
||||
const symbolInfo = job.job_group_symbol === '?' ? '' : job.job_group_symbol;
|
||||
|
||||
return [
|
||||
thPlatformMap[job.platform] || job.platform,
|
||||
job.platform_option,
|
||||
(job.job_group_name === 'unknown') ? undefined : job.job_group_name,
|
||||
job.job_group_name === 'unknown' ? undefined : job.job_group_name,
|
||||
job.job_type_name,
|
||||
`${symbolInfo}(${job.job_type_symbol})`,
|
||||
].filter(item => typeof item !== 'undefined').join(' ').toLowerCase();
|
||||
]
|
||||
.filter(item => typeof item !== 'undefined')
|
||||
.join(' ')
|
||||
.toLowerCase();
|
||||
};
|
||||
|
||||
export const getHoverText = function getHoverText(job) {
|
||||
|
|
|
@ -21,7 +21,11 @@ export const setLocation = function setLocation(params, hashPrefix = '/jobs') {
|
|||
window.location.hash = `#${hashPrefix}${createQueryParams(params)}`;
|
||||
};
|
||||
|
||||
export const setUrlParam = function setUrlParam(field, value, hashPrefix = '/jobs') {
|
||||
export const setUrlParam = function setUrlParam(
|
||||
field,
|
||||
value,
|
||||
hashPrefix = '/jobs',
|
||||
) {
|
||||
const params = getAllUrlParams();
|
||||
|
||||
if (value) {
|
||||
|
|
|
@ -2,7 +2,9 @@ export const thTitleSuffixLimit = 70;
|
|||
|
||||
export const parseAuthor = function parseAuthor(author) {
|
||||
const userTokens = author.split(/[<>]+/);
|
||||
const name = userTokens[0].trim().replace(/\w\S*/g, txt => txt.charAt(0).toUpperCase() + txt.substr(1));
|
||||
const name = userTokens[0]
|
||||
.trim()
|
||||
.replace(/\w\S*/g, txt => txt.charAt(0).toUpperCase() + txt.substr(1));
|
||||
const email = userTokens.length > 1 ? userTokens[1] : '';
|
||||
return { name, email };
|
||||
};
|
||||
|
|
|
@ -29,12 +29,11 @@ const taskcluster = (() => {
|
|||
return {
|
||||
getAgent: tcAgent,
|
||||
// When the access token is refreshed, simply update it on the credential agent
|
||||
getQueue: () => (
|
||||
getQueue: () =>
|
||||
new Queue({
|
||||
credentialAgent: tcAgent(),
|
||||
rootUrl: tcRootUrl,
|
||||
})
|
||||
),
|
||||
}),
|
||||
updateAgent: () => {
|
||||
const userSession = localStorage.getItem('userSession');
|
||||
|
||||
|
|
|
@ -15,7 +15,8 @@ export const getUserSessionUrl = function getUserSessionUrl(oidcProvider) {
|
|||
};
|
||||
|
||||
export const createQueryParams = function createQueryParams(params) {
|
||||
const query = params instanceof URLSearchParams ? params : new URLSearchParams(params);
|
||||
const query =
|
||||
params instanceof URLSearchParams ? params : new URLSearchParams(params);
|
||||
return `?${query.toString()}`;
|
||||
};
|
||||
|
||||
|
@ -45,7 +46,11 @@ export const getReftestUrl = function getReftestUrl(logUrl) {
|
|||
// which is a "project" endpoint that requires the project name. We shouldn't
|
||||
// need that since the ids are unique across projects.
|
||||
// Bug 1441938 - The project_bound_router is not needed and cumbersome in some cases
|
||||
export const getLogViewerUrl = function getLogViewerUrl(job_id, repoName, line_number) {
|
||||
export const getLogViewerUrl = function getLogViewerUrl(
|
||||
job_id,
|
||||
repoName,
|
||||
line_number,
|
||||
) {
|
||||
const rv = `logviewer.html#?job_id=${job_id}&repo=${repoName}`;
|
||||
return line_number ? `${rv}&lineNumber=${line_number}` : rv;
|
||||
};
|
||||
|
@ -92,9 +97,10 @@ export const graphsEndpoint = 'failurecount/';
|
|||
export const parseQueryParams = function parseQueryParams(search) {
|
||||
const params = new URLSearchParams(search);
|
||||
|
||||
return [...params.entries()].reduce((acc, [key, value]) => (
|
||||
{ ...acc, [key]: value }
|
||||
), {});
|
||||
return [...params.entries()].reduce(
|
||||
(acc, [key, value]) => ({ ...acc, [key]: value }),
|
||||
{},
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: Combine this with getApiUrl().
|
||||
|
|
|
@ -6,16 +6,16 @@ import MainView from './MainView';
|
|||
import BugDetailsView from './BugDetailsView';
|
||||
|
||||
class App extends React.Component {
|
||||
constructor(props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.updateAppState = this.updateAppState.bind(this);
|
||||
|
||||
// keep track of the mainviews graph and table data so the API won't be
|
||||
// called again when navigating back from bugdetailsview.
|
||||
this.state = {
|
||||
graphData: null,
|
||||
tableData: null,
|
||||
};
|
||||
graphData: null,
|
||||
tableData: null,
|
||||
};
|
||||
}
|
||||
|
||||
updateAppState(state) {
|
||||
|
@ -27,29 +27,34 @@ class App extends React.Component {
|
|||
<HashRouter>
|
||||
<main>
|
||||
<Switch>
|
||||
(<Route
|
||||
<Route
|
||||
exact
|
||||
path="/main"
|
||||
render={props =>
|
||||
(<MainView
|
||||
{...props}
|
||||
mainGraphData={this.state.graphData}
|
||||
mainTableData={this.state.tableData}
|
||||
updateAppState={this.updateAppState}
|
||||
/>)}
|
||||
/>)
|
||||
(<Route
|
||||
path="/main?startday=:startday&endday=:endday&tree=:tree"
|
||||
render={props =>
|
||||
(<MainView
|
||||
render={props => (
|
||||
<MainView
|
||||
{...props}
|
||||
mainGraphData={this.state.graphData}
|
||||
mainTableData={this.state.tableData}
|
||||
updateAppState={this.updateAppState}
|
||||
/>)}
|
||||
/>)
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path="/main?startday=:startday&endday=:endday&tree=:tree"
|
||||
render={props => (
|
||||
<MainView
|
||||
{...props}
|
||||
mainGraphData={this.state.graphData}
|
||||
mainTableData={this.state.tableData}
|
||||
updateAppState={this.updateAppState}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route path="/bugdetails" component={BugDetailsView} />
|
||||
<Route path="/bugdetails?startday=:startday&endday=:endday&tree=:tree&bug=bug" component={BugDetailsView} />
|
||||
<Route
|
||||
path="/bugdetails?startday=:startday&endday=:endday&tree=:tree&bug=bug"
|
||||
component={BugDetailsView}
|
||||
/>
|
||||
<Redirect from="/" to="/main" />
|
||||
</Switch>
|
||||
</main>
|
||||
|
|
|
@ -8,18 +8,38 @@ import { getBugUrl } from '../helpers/url';
|
|||
// in bugdetailsview to navigate back to mainview displays this console warning:
|
||||
// "Hash history go(n) causes a full page reload in this browser"
|
||||
|
||||
function BugColumn({ tree, startday, endday, data, location, graphData, tableData, updateAppState }) {
|
||||
function BugColumn({
|
||||
tree,
|
||||
startday,
|
||||
endday,
|
||||
data,
|
||||
location,
|
||||
graphData,
|
||||
tableData,
|
||||
updateAppState,
|
||||
}) {
|
||||
const { id, summary } = data;
|
||||
return (
|
||||
<div>
|
||||
<a className="ml-1" target="_blank" rel="noopener noreferrer" href={getBugUrl(id)}>{id}</a>
|
||||
<a
|
||||
className="ml-1"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={getBugUrl(id)}
|
||||
>
|
||||
{id}
|
||||
</a>
|
||||
|
||||
<span className="ml-1 small-text bug-details" onClick={() => updateAppState({ graphData, tableData })}>
|
||||
<span
|
||||
className="ml-1 small-text bug-details"
|
||||
onClick={() => updateAppState({ graphData, tableData })}
|
||||
>
|
||||
<Link
|
||||
to={{ pathname: '/bugdetails',
|
||||
search: `?startday=${startday}&endday=${endday}&tree=${tree}&bug=${id}`,
|
||||
state: { startday, endday, tree, id, summary, location },
|
||||
}}
|
||||
to={{
|
||||
pathname: '/bugdetails',
|
||||
search: `?startday=${startday}&endday=${endday}&tree=${tree}&bug=${id}`,
|
||||
state: { startday, endday, tree, id, summary, location },
|
||||
}}
|
||||
>
|
||||
details
|
||||
</Link>
|
||||
|
@ -38,14 +58,10 @@ BugColumn.propTypes = {
|
|||
tree: PropTypes.string.isRequired,
|
||||
location: PropTypes.shape({}),
|
||||
graphData: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(
|
||||
PropTypes.shape({}),
|
||||
),
|
||||
PropTypes.arrayOf(PropTypes.shape({})),
|
||||
PropTypes.shape({}),
|
||||
]),
|
||||
tableData: PropTypes.arrayOf(
|
||||
PropTypes.shape({}),
|
||||
),
|
||||
tableData: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
updateAppState: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
|
|
@ -12,11 +12,23 @@ import Layout from './Layout';
|
|||
import withView from './View';
|
||||
import DateOptions from './DateOptions';
|
||||
|
||||
const BugDetailsView = (props) => {
|
||||
const { graphData, tableData, initialParamsSet, startday, endday, updateState, bug,
|
||||
summary, errorMessages, lastLocation, tableFailureStatus, graphFailureStatus } = props;
|
||||
const BugDetailsView = props => {
|
||||
const {
|
||||
graphData,
|
||||
tableData,
|
||||
initialParamsSet,
|
||||
startday,
|
||||
endday,
|
||||
updateState,
|
||||
bug,
|
||||
summary,
|
||||
errorMessages,
|
||||
lastLocation,
|
||||
tableFailureStatus,
|
||||
graphFailureStatus,
|
||||
} = props;
|
||||
|
||||
const columns = [
|
||||
const columns = [
|
||||
{
|
||||
Header: 'Push Time',
|
||||
accessor: 'push_time',
|
||||
|
@ -28,14 +40,19 @@ const BugDetailsView = (props) => {
|
|||
{
|
||||
Header: 'Revision',
|
||||
accessor: 'revision',
|
||||
Cell: _props =>
|
||||
(<a
|
||||
href={getJobsUrl({ repo: _props.original.tree, revision: _props.value, selectedJob: _props.original.job_id })}
|
||||
Cell: _props => (
|
||||
<a
|
||||
href={getJobsUrl({
|
||||
repo: _props.original.tree,
|
||||
revision: _props.value,
|
||||
selectedJob: _props.original.job_id,
|
||||
})}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{_props.value}
|
||||
</a>),
|
||||
</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
Header: 'Platform',
|
||||
|
@ -79,47 +96,64 @@ const BugDetailsView = (props) => {
|
|||
header={
|
||||
<React.Fragment>
|
||||
<Row>
|
||||
<Col xs="12"><span className="pull-left"><Link to={(lastLocation || '/')}><Icon name="arrow-left" className="pr-1" />
|
||||
back</Link></span>
|
||||
<Col xs="12">
|
||||
<span className="pull-left">
|
||||
<Link to={lastLocation || '/'}>
|
||||
<Icon name="arrow-left" className="pr-1" />
|
||||
back
|
||||
</Link>
|
||||
</span>
|
||||
</Col>
|
||||
</Row>
|
||||
{!errorMessages.length && !tableFailureStatus && !graphFailureStatus &&
|
||||
<React.Fragment>
|
||||
<Row>
|
||||
<Col xs="12" className="mx-auto"><h1>Details for Bug {!bug ? '' : bug}</h1></Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs="12" className="mx-auto"><p className="subheader">{`${prettyDate(startday)} to ${prettyDate(endday)} UTC`}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
{summary &&
|
||||
<Row>
|
||||
<Col xs="4" className="mx-auto"><p className="text-secondary text-center">{summary}</p></Col>
|
||||
</Row>}
|
||||
{tableData.length > 0 &&
|
||||
<Row>
|
||||
<Col xs="12" className="mx-auto"><p className="text-secondary">{tableData.length} total failures</p></Col>
|
||||
</Row>}
|
||||
</React.Fragment>}
|
||||
{!errorMessages.length && !tableFailureStatus && !graphFailureStatus && (
|
||||
<React.Fragment>
|
||||
<Row>
|
||||
<Col xs="12" className="mx-auto">
|
||||
<h1>Details for Bug {!bug ? '' : bug}</h1>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs="12" className="mx-auto">
|
||||
<p className="subheader">{`${prettyDate(
|
||||
startday,
|
||||
)} to ${prettyDate(endday)} UTC`}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
{summary && (
|
||||
<Row>
|
||||
<Col xs="4" className="mx-auto">
|
||||
<p className="text-secondary text-center">{summary}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
{tableData.length > 0 && (
|
||||
<Row>
|
||||
<Col xs="12" className="mx-auto">
|
||||
<p className="text-secondary">
|
||||
{tableData.length} total failures
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
}
|
||||
table={
|
||||
bug && initialParamsSet &&
|
||||
<ReactTable
|
||||
data={tableData}
|
||||
showPageSizeOptions
|
||||
columns={columns}
|
||||
className="-striped"
|
||||
getTrProps={tableRowStyling}
|
||||
showPaginationTop
|
||||
defaultPageSize={50}
|
||||
/>
|
||||
}
|
||||
datePicker={
|
||||
<DateOptions
|
||||
updateState={updateState}
|
||||
/>
|
||||
bug &&
|
||||
initialParamsSet && (
|
||||
<ReactTable
|
||||
data={tableData}
|
||||
showPageSizeOptions
|
||||
columns={columns}
|
||||
className="-striped"
|
||||
getTrProps={tableRowStyling}
|
||||
showPaginationTop
|
||||
defaultPageSize={50}
|
||||
/>
|
||||
)
|
||||
}
|
||||
datePicker={<DateOptions updateState={updateState} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -4,7 +4,6 @@ import { Tooltip } from 'reactstrap';
|
|||
|
||||
import { getLogViewerUrl } from '../helpers/url';
|
||||
|
||||
|
||||
export default class BugLogColumn extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -36,7 +35,9 @@ export default class BugLogColumn extends React.Component {
|
|||
return (
|
||||
<div>
|
||||
<span ref={this.updateTarget}>
|
||||
{`${original.lines.length} unexpected-fail${original.lines.length > 1 ? 's' : ''}`}
|
||||
{`${original.lines.length} unexpected-fail${
|
||||
original.lines.length > 1 ? 's' : ''
|
||||
}`}
|
||||
<br />
|
||||
<a
|
||||
className="small-text"
|
||||
|
@ -48,20 +49,23 @@ export default class BugLogColumn extends React.Component {
|
|||
</a>
|
||||
</span>
|
||||
|
||||
{target && original.lines.length > 0 &&
|
||||
<Tooltip
|
||||
placement="left"
|
||||
isOpen={tooltipOpen}
|
||||
target={target}
|
||||
toggle={this.toggle}
|
||||
className="tooltip"
|
||||
>
|
||||
<ul>
|
||||
{original.lines.map(line => (
|
||||
<li key={line} className="failure_li text-truncate">{line}</li>
|
||||
))}
|
||||
</ul>
|
||||
</Tooltip>}
|
||||
{target && original.lines.length > 0 && (
|
||||
<Tooltip
|
||||
placement="left"
|
||||
isOpen={tooltipOpen}
|
||||
target={target}
|
||||
toggle={this.toggle}
|
||||
className="tooltip"
|
||||
>
|
||||
<ul>
|
||||
{original.lines.map(line => (
|
||||
<li key={line} className="failure_li text-truncate">
|
||||
{line}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -38,7 +38,11 @@ export default class DateOptions extends React.Component {
|
|||
// bug history is max 4 months
|
||||
from = 120;
|
||||
}
|
||||
const startday = ISODate(moment().utc().subtract(from, 'days'));
|
||||
const startday = ISODate(
|
||||
moment()
|
||||
.utc()
|
||||
.subtract(from, 'days'),
|
||||
);
|
||||
const endday = ISODate(moment().utc());
|
||||
this.props.updateState({ startday, endday });
|
||||
}
|
||||
|
@ -46,23 +50,29 @@ export default class DateOptions extends React.Component {
|
|||
render() {
|
||||
const { updateState } = this.props;
|
||||
const { dropdownOpen, dateRange } = this.state;
|
||||
const dateOptions = ['last 7 days', 'last 30 days', 'custom range', 'entire history'];
|
||||
const dateOptions = [
|
||||
'last 7 days',
|
||||
'last 30 days',
|
||||
'custom range',
|
||||
'entire history',
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="d-inline-block">
|
||||
<ButtonDropdown className="mr-3" isOpen={dropdownOpen} toggle={this.toggle}>
|
||||
<DropdownToggle caret>
|
||||
date range
|
||||
</DropdownToggle>
|
||||
<ButtonDropdown
|
||||
className="mr-3"
|
||||
isOpen={dropdownOpen}
|
||||
toggle={this.toggle}
|
||||
>
|
||||
<DropdownToggle caret>date range</DropdownToggle>
|
||||
<DropdownMenuItems
|
||||
options={dateOptions}
|
||||
updateData={this.updateDateRange}
|
||||
/>
|
||||
</ButtonDropdown>
|
||||
{dateRange === 'custom range' &&
|
||||
<DateRangePicker
|
||||
updateState={updateState}
|
||||
/>}
|
||||
{dateRange === 'custom range' && (
|
||||
<DateRangePicker updateState={updateState} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ export default class DateRangePicker extends React.Component {
|
|||
<span className="ml-1 mr-1">-</span>
|
||||
<span className="InputFromTo-to">
|
||||
<DayPickerInput
|
||||
ref={(element) => {
|
||||
ref={element => {
|
||||
this.to = element;
|
||||
}}
|
||||
value={to}
|
||||
|
@ -98,7 +98,9 @@ export default class DateRangePicker extends React.Component {
|
|||
onDayChange={this.toChange}
|
||||
/>
|
||||
</span>
|
||||
<Button color="secondary" className="ml-2" onClick={this.updateData}>update</Button>
|
||||
<Button color="secondary" className="ml-2" onClick={this.updateData}>
|
||||
update
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,17 +3,20 @@ import Icon from 'react-fontawesome';
|
|||
import PropTypes from 'prop-types';
|
||||
import { DropdownMenu, DropdownItem } from 'reactstrap';
|
||||
|
||||
const DropdownMenuItems = ({ selectedItem, updateData, options }) =>
|
||||
(
|
||||
const DropdownMenuItems = ({ selectedItem, updateData, options }) => (
|
||||
<DropdownMenu>
|
||||
{options.map(item =>
|
||||
(<DropdownItem key={item} onClick={event => updateData(event.target.innerText)}>
|
||||
{options.map(item => (
|
||||
<DropdownItem
|
||||
key={item}
|
||||
onClick={event => updateData(event.target.innerText)}
|
||||
>
|
||||
<Icon
|
||||
name="check"
|
||||
className={`pr-1 ${selectedItem === item ? '' : 'hide'}`}
|
||||
/>
|
||||
{item}
|
||||
</DropdownItem>))}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
||||
|
|
|
@ -5,13 +5,17 @@ import { Alert } from 'reactstrap';
|
|||
import { processErrorMessage } from './helpers';
|
||||
|
||||
const ErrorMessages = ({ failureMessage, failureStatus, errorMessages }) => {
|
||||
const messages = errorMessages.length ? errorMessages : processErrorMessage(failureMessage, failureStatus);
|
||||
const messages = errorMessages.length
|
||||
? errorMessages
|
||||
: processErrorMessage(failureMessage, failureStatus);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{messages.map(message =>
|
||||
<Alert color="danger" key={message}>{message}</Alert>,
|
||||
)}
|
||||
{messages.map(message => (
|
||||
<Alert color="danger" key={message}>
|
||||
{message}
|
||||
</Alert>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -19,9 +23,7 @@ const ErrorMessages = ({ failureMessage, failureStatus, errorMessages }) => {
|
|||
ErrorMessages.propTypes = {
|
||||
failureMessage: PropTypes.oneOfType([
|
||||
PropTypes.object,
|
||||
PropTypes.arrayOf(
|
||||
PropTypes.string,
|
||||
),
|
||||
PropTypes.arrayOf(PropTypes.string),
|
||||
]),
|
||||
failureStatus: PropTypes.number,
|
||||
errorMessages: PropTypes.array,
|
||||
|
@ -30,9 +32,7 @@ ErrorMessages.propTypes = {
|
|||
ErrorMessages.defaultProps = {
|
||||
failureMessage: null,
|
||||
failureStatus: null,
|
||||
errorMessages: PropTypes.arrayOf(
|
||||
PropTypes.string,
|
||||
),
|
||||
errorMessages: PropTypes.arrayOf(PropTypes.string),
|
||||
};
|
||||
|
||||
export default ErrorMessages;
|
||||
|
|
|
@ -17,7 +17,6 @@ import PropTypes from 'prop-types';
|
|||
// };
|
||||
|
||||
export default class Graph extends React.Component {
|
||||
|
||||
componentDidUpdate() {
|
||||
const { specs, data } = this.props;
|
||||
if (specs.data !== data) {
|
||||
|
@ -48,7 +47,8 @@ export default class Graph extends React.Component {
|
|||
Graph.propTypes = {
|
||||
specs: PropTypes.shape({
|
||||
legend: PropTypes.oneOfType([
|
||||
PropTypes.string, PropTypes.arrayOf(PropTypes.string),
|
||||
PropTypes.string,
|
||||
PropTypes.arrayOf(PropTypes.string),
|
||||
]),
|
||||
}).isRequired,
|
||||
data: PropTypes.oneOfType([
|
||||
|
@ -65,7 +65,8 @@ Graph.propTypes = {
|
|||
date: PropTypes.shape({ Date: PropTypes.string }),
|
||||
value: PropTypes.number,
|
||||
}),
|
||||
), PropTypes.arrayOf(
|
||||
),
|
||||
PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
date: PropTypes.shape({ Date: PropTypes.string }),
|
||||
value: PropTypes.number,
|
||||
|
|
|
@ -25,26 +25,25 @@ export default class GraphsContainer extends React.Component {
|
|||
return (
|
||||
<React.Fragment>
|
||||
<Row className="pt-5">
|
||||
<Graph
|
||||
specs={graphOneSpecs}
|
||||
data={graphOneData}
|
||||
/>
|
||||
<Graph specs={graphOneSpecs} data={graphOneData} />
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs="12" className="mx-auto pb-5">
|
||||
<Button color="secondary" onClick={this.toggleGraph} className="d-inline-block mr-3">
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={this.toggleGraph}
|
||||
className="d-inline-block mr-3"
|
||||
>
|
||||
{`${showGraphTwo ? 'less' : 'more'} graphs`}
|
||||
</Button>
|
||||
{children}
|
||||
</Col>
|
||||
</Row>
|
||||
{showGraphTwo &&
|
||||
<Row className="pt-5">
|
||||
<Graph
|
||||
specs={graphTwoSpecs}
|
||||
data={graphTwoData}
|
||||
/>
|
||||
</Row>}
|
||||
{showGraphTwo && (
|
||||
<Row className="pt-5">
|
||||
<Graph specs={graphTwoSpecs} data={graphTwoData} />
|
||||
</Row>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -63,7 +62,8 @@ GraphsContainer.propTypes = {
|
|||
date: PropTypes.shape({ Date: PropTypes.string }),
|
||||
value: PropTypes.number,
|
||||
}),
|
||||
), PropTypes.arrayOf(
|
||||
),
|
||||
PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
date: PropTypes.shape({ Date: PropTypes.string }),
|
||||
value: PropTypes.number,
|
||||
|
|
|
@ -10,47 +10,68 @@ import GraphsContainer from './GraphsContainer';
|
|||
import ErrorMessages from './ErrorMessages';
|
||||
import { prettyErrorMessages, errorMessageClass } from './constants';
|
||||
|
||||
const Layout = (props) => {
|
||||
const Layout = props => {
|
||||
const {
|
||||
graphData,
|
||||
tableData,
|
||||
errorMessages,
|
||||
tree,
|
||||
isFetchingTable,
|
||||
isFetchingGraphs,
|
||||
tableFailureStatus,
|
||||
graphFailureStatus,
|
||||
updateState,
|
||||
graphOneData,
|
||||
graphTwoData,
|
||||
table,
|
||||
datePicker,
|
||||
header,
|
||||
} = props;
|
||||
|
||||
const { graphData, tableData, errorMessages, tree, isFetchingTable,
|
||||
isFetchingGraphs, tableFailureStatus, graphFailureStatus, updateState,
|
||||
graphOneData, graphTwoData, table, datePicker, header } = props;
|
||||
|
||||
let failureMessage = null;
|
||||
if (tableFailureStatus) {
|
||||
failureMessage = tableData;
|
||||
} else if (graphFailureStatus) {
|
||||
failureMessage = graphData;
|
||||
}
|
||||
let failureMessage = null;
|
||||
if (tableFailureStatus) {
|
||||
failureMessage = tableData;
|
||||
} else if (graphFailureStatus) {
|
||||
failureMessage = graphData;
|
||||
}
|
||||
return (
|
||||
<Container fluid style={{ marginBottom: '5rem', marginTop: '5rem', maxWidth: '1200px' }}>
|
||||
<Navigation
|
||||
updateState={updateState}
|
||||
tree={tree}
|
||||
/>
|
||||
<Container
|
||||
fluid
|
||||
style={{ marginBottom: '5rem', marginTop: '5rem', maxWidth: '1200px' }}
|
||||
>
|
||||
<Navigation updateState={updateState} tree={tree} />
|
||||
{(isFetchingGraphs || isFetchingTable) &&
|
||||
!(tableFailureStatus || graphFailureStatus || errorMessages.length > 0) &&
|
||||
<div className="loading">
|
||||
<Icon spin name="cog" size="4x" />
|
||||
</div>}
|
||||
{(tableFailureStatus || graphFailureStatus || errorMessages.length > 0) &&
|
||||
!(
|
||||
tableFailureStatus ||
|
||||
graphFailureStatus ||
|
||||
errorMessages.length > 0
|
||||
) && (
|
||||
<div className="loading">
|
||||
<Icon spin name="cog" size="4x" />
|
||||
</div>
|
||||
)}
|
||||
{(tableFailureStatus ||
|
||||
graphFailureStatus ||
|
||||
errorMessages.length > 0) && (
|
||||
<ErrorMessages
|
||||
failureMessage={failureMessage}
|
||||
failureStatus={tableFailureStatus || graphFailureStatus}
|
||||
errorMessages={errorMessages}
|
||||
/>}
|
||||
/>
|
||||
)}
|
||||
{header}
|
||||
<ErrorBoundary
|
||||
errorClasses={errorMessageClass}
|
||||
message={prettyErrorMessages.default}
|
||||
>
|
||||
{graphOneData && graphTwoData &&
|
||||
<GraphsContainer
|
||||
graphOneData={graphOneData}
|
||||
graphTwoData={graphTwoData}
|
||||
>
|
||||
{datePicker}
|
||||
</GraphsContainer>}
|
||||
{graphOneData && graphTwoData && (
|
||||
<GraphsContainer
|
||||
graphOneData={graphOneData}
|
||||
graphTwoData={graphTwoData}
|
||||
>
|
||||
{datePicker}
|
||||
</GraphsContainer>
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
|
||||
<ErrorBoundary
|
||||
|
@ -59,7 +80,8 @@ const Layout = (props) => {
|
|||
>
|
||||
{table}
|
||||
</ErrorBoundary>
|
||||
</Container>);
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
Container.propTypes = {
|
||||
|
@ -71,32 +93,17 @@ Layout.propTypes = {
|
|||
location: PropTypes.shape({
|
||||
search: PropTypes.string,
|
||||
}).isRequired,
|
||||
datePicker: PropTypes.oneOfType([
|
||||
PropTypes.shape({}), PropTypes.bool,
|
||||
]),
|
||||
header: PropTypes.oneOfType([
|
||||
PropTypes.shape({}), PropTypes.bool,
|
||||
]),
|
||||
table: PropTypes.oneOfType([
|
||||
PropTypes.shape({}), PropTypes.bool,
|
||||
]),
|
||||
graphOneData: PropTypes.arrayOf(
|
||||
PropTypes.shape({}),
|
||||
),
|
||||
datePicker: PropTypes.oneOfType([PropTypes.shape({}), PropTypes.bool]),
|
||||
header: PropTypes.oneOfType([PropTypes.shape({}), PropTypes.bool]),
|
||||
table: PropTypes.oneOfType([PropTypes.shape({}), PropTypes.bool]),
|
||||
graphOneData: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
graphTwoData: PropTypes.arrayOf(
|
||||
PropTypes.arrayOf(
|
||||
PropTypes.shape({}),
|
||||
), PropTypes.arrayOf(
|
||||
PropTypes.shape({}),
|
||||
),
|
||||
),
|
||||
tableData: PropTypes.arrayOf(
|
||||
PropTypes.shape({}),
|
||||
PropTypes.arrayOf(PropTypes.shape({})),
|
||||
PropTypes.arrayOf(PropTypes.shape({})),
|
||||
),
|
||||
tableData: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
graphData: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(
|
||||
PropTypes.shape({}),
|
||||
),
|
||||
PropTypes.arrayOf(PropTypes.shape({})),
|
||||
PropTypes.shape({}),
|
||||
]),
|
||||
tree: PropTypes.string,
|
||||
|
|
|
@ -7,14 +7,28 @@ import ReactTable from 'react-table';
|
|||
import { bugsEndpoint } from '../helpers/url';
|
||||
|
||||
import BugColumn from './BugColumn';
|
||||
import { calculateMetrics, prettyDate, ISODate, tableRowStyling } from './helpers';
|
||||
import {
|
||||
calculateMetrics,
|
||||
prettyDate,
|
||||
ISODate,
|
||||
tableRowStyling,
|
||||
} from './helpers';
|
||||
import withView from './View';
|
||||
import Layout from './Layout';
|
||||
import DateRangePicker from './DateRangePicker';
|
||||
|
||||
const MainView = (props) => {
|
||||
const { graphData, tableData, initialParamsSet, startday, endday, updateState,
|
||||
tree, location, updateAppState } = props;
|
||||
const MainView = props => {
|
||||
const {
|
||||
graphData,
|
||||
tableData,
|
||||
initialParamsSet,
|
||||
startday,
|
||||
endday,
|
||||
updateState,
|
||||
tree,
|
||||
location,
|
||||
updateAppState,
|
||||
} = props;
|
||||
|
||||
const textFilter = (filter, row) => {
|
||||
const text = row[filter.id];
|
||||
|
@ -31,17 +45,18 @@ const MainView = (props) => {
|
|||
headerClassName: 'bug-column-header',
|
||||
className: 'bug-column',
|
||||
maxWidth: 150,
|
||||
Cell: _props =>
|
||||
(<BugColumn
|
||||
data={_props.original}
|
||||
tree={tree}
|
||||
startday={startday}
|
||||
endday={endday}
|
||||
location={location}
|
||||
graphData={graphData}
|
||||
tableData={tableData}
|
||||
updateAppState={updateAppState}
|
||||
/>),
|
||||
Cell: _props => (
|
||||
<BugColumn
|
||||
data={_props.original}
|
||||
tree={tree}
|
||||
startday={startday}
|
||||
endday={endday}
|
||||
location={location}
|
||||
graphData={graphData}
|
||||
tableData={tableData}
|
||||
updateAppState={updateAppState}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
Header: 'Count',
|
||||
|
@ -69,7 +84,12 @@ const MainView = (props) => {
|
|||
let totalRuns = 0;
|
||||
|
||||
if (graphData.length) {
|
||||
({ graphOneData, graphTwoData, totalFailures, totalRuns } = calculateMetrics(graphData));
|
||||
({
|
||||
graphOneData,
|
||||
graphTwoData,
|
||||
totalFailures,
|
||||
totalRuns,
|
||||
} = calculateMetrics(graphData));
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -78,39 +98,45 @@ const MainView = (props) => {
|
|||
graphOneData={graphOneData}
|
||||
graphTwoData={graphTwoData}
|
||||
header={
|
||||
initialParamsSet &&
|
||||
<React.Fragment>
|
||||
<Row>
|
||||
<Col xs="12" className="mx-auto pt-3"><h1>Intermittent Test Failures</h1></Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs="12" className="mx-auto"><p className="subheader">{`${prettyDate(startday)} to ${prettyDate(endday)} UTC`}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs="12" className="mx-auto"><p className="text-secondary">{totalFailures} bugs in {totalRuns} pushes</p>
|
||||
</Col>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
initialParamsSet && (
|
||||
<React.Fragment>
|
||||
<Row>
|
||||
<Col xs="12" className="mx-auto pt-3">
|
||||
<h1>Intermittent Test Failures</h1>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs="12" className="mx-auto">
|
||||
<p className="subheader">{`${prettyDate(
|
||||
startday,
|
||||
)} to ${prettyDate(endday)} UTC`}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs="12" className="mx-auto">
|
||||
<p className="text-secondary">
|
||||
{totalFailures} bugs in {totalRuns} pushes
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
table={
|
||||
initialParamsSet &&
|
||||
<ReactTable
|
||||
data={tableData}
|
||||
showPageSizeOptions
|
||||
columns={columns}
|
||||
className="-striped"
|
||||
getTrProps={tableRowStyling}
|
||||
showPaginationTop
|
||||
defaultPageSize={50}
|
||||
filterable
|
||||
/>
|
||||
}
|
||||
datePicker={
|
||||
<DateRangePicker
|
||||
updateState={updateState}
|
||||
/>
|
||||
initialParamsSet && (
|
||||
<ReactTable
|
||||
data={tableData}
|
||||
showPageSizeOptions
|
||||
columns={columns}
|
||||
className="-striped"
|
||||
getTrProps={tableRowStyling}
|
||||
showPaginationTop
|
||||
defaultPageSize={50}
|
||||
filterable
|
||||
/>
|
||||
)
|
||||
}
|
||||
datePicker={<DateRangePicker updateState={updateState} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -121,7 +147,11 @@ MainView.propTypes = {
|
|||
|
||||
const defaultState = {
|
||||
tree: 'trunk',
|
||||
startday: ISODate(moment().utc().subtract(7, 'days')),
|
||||
startday: ISODate(
|
||||
moment()
|
||||
.utc()
|
||||
.subtract(7, 'days'),
|
||||
),
|
||||
endday: ISODate(moment().utc()),
|
||||
route: '/main',
|
||||
endpoint: bugsEndpoint,
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Collapse, Navbar, Nav, UncontrolledDropdown, DropdownToggle } from 'reactstrap';
|
||||
import {
|
||||
Collapse,
|
||||
Navbar,
|
||||
Nav,
|
||||
UncontrolledDropdown,
|
||||
DropdownToggle,
|
||||
} from 'reactstrap';
|
||||
|
||||
import DropdownMenuItems from './DropdownMenuItems';
|
||||
import { treeOptions } from './constants';
|
||||
|
|
|
@ -1,136 +1,71 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { graphsEndpoint, parseQueryParams, createQueryParams, createApiUrl,
|
||||
bugzillaBugsApi } from '../helpers/url';
|
||||
import {
|
||||
graphsEndpoint,
|
||||
parseQueryParams,
|
||||
createQueryParams,
|
||||
createApiUrl,
|
||||
bugzillaBugsApi,
|
||||
} from '../helpers/url';
|
||||
import { getData } from '../helpers/http';
|
||||
|
||||
import { updateQueryParams, validateQueryParams, mergeData, formatBugs } from './helpers';
|
||||
import {
|
||||
updateQueryParams,
|
||||
validateQueryParams,
|
||||
mergeData,
|
||||
formatBugs,
|
||||
} from './helpers';
|
||||
|
||||
const withView = defaultState => WrappedComponent =>
|
||||
class View extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
super(props);
|
||||
|
||||
this.updateData = this.updateData.bind(this);
|
||||
this.setQueryParams = this.setQueryParams.bind(this);
|
||||
this.checkQueryValidation = this.checkQueryValidation.bind(this);
|
||||
this.getTableData = this.getTableData.bind(this);
|
||||
this.getGraphData = this.getGraphData.bind(this);
|
||||
this.updateState = this.updateState.bind(this);
|
||||
this.getBugDetails = this.getBugDetails.bind(this);
|
||||
this.updateData = this.updateData.bind(this);
|
||||
this.setQueryParams = this.setQueryParams.bind(this);
|
||||
this.checkQueryValidation = this.checkQueryValidation.bind(this);
|
||||
this.getTableData = this.getTableData.bind(this);
|
||||
this.getGraphData = this.getGraphData.bind(this);
|
||||
this.updateState = this.updateState.bind(this);
|
||||
this.getBugDetails = this.getBugDetails.bind(this);
|
||||
|
||||
this.default = (this.props.location.state || defaultState);
|
||||
this.state = {
|
||||
errorMessages: [],
|
||||
initialParamsSet: false,
|
||||
tree: (this.default.tree || null),
|
||||
startday: (this.default.startday || null),
|
||||
endday: (this.default.endday || null),
|
||||
bug: (this.default.id || null),
|
||||
summary: (this.default.summary || null),
|
||||
tableData: [],
|
||||
tableFailureStatus: null,
|
||||
isFetchingTable: false,
|
||||
graphData: [],
|
||||
graphFailureStatus: null,
|
||||
isFetchingGraphs: false,
|
||||
lastLocation: (this.default.location || null),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setQueryParams();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { location } = this.props;
|
||||
// update all data if the user edits dates, tree or bug via the query params
|
||||
if (prevProps.location.search !== location.search) {
|
||||
this.checkQueryValidation(parseQueryParams(location.search), this.state.initialParamsSet);
|
||||
}
|
||||
}
|
||||
|
||||
setQueryParams() {
|
||||
const { location, history } = this.props;
|
||||
const { startday, endday, tree, bug } = this.state;
|
||||
const params = { startday, endday, tree };
|
||||
|
||||
if (bug) {
|
||||
params.bug = bug;
|
||||
this.default = this.props.location.state || defaultState;
|
||||
this.state = {
|
||||
errorMessages: [],
|
||||
initialParamsSet: false,
|
||||
tree: this.default.tree || null,
|
||||
startday: this.default.startday || null,
|
||||
endday: this.default.endday || null,
|
||||
bug: this.default.id || null,
|
||||
summary: this.default.summary || null,
|
||||
tableData: [],
|
||||
tableFailureStatus: null,
|
||||
isFetchingTable: false,
|
||||
graphData: [],
|
||||
graphFailureStatus: null,
|
||||
isFetchingGraphs: false,
|
||||
lastLocation: this.default.location || null,
|
||||
};
|
||||
}
|
||||
|
||||
if (location.search !== '' && !location.state) {
|
||||
// update data based on the params or show error if params are missing
|
||||
this.checkQueryValidation(parseQueryParams(location.search));
|
||||
} else {
|
||||
// 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);
|
||||
componentDidMount() {
|
||||
this.setQueryParams();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { location } = this.props;
|
||||
// update all data if the user edits dates, tree or bug via the query params
|
||||
if (prevProps.location.search !== location.search) {
|
||||
this.checkQueryValidation(
|
||||
parseQueryParams(location.search),
|
||||
this.state.initialParamsSet,
|
||||
);
|
||||
}
|
||||
|
||||
this.setState({ initialParamsSet: true });
|
||||
this.getGraphData(createApiUrl(graphsEndpoint, params));
|
||||
this.getTableData(createApiUrl(defaultState.endpoint, params));
|
||||
}
|
||||
}
|
||||
|
||||
async getBugDetails(url) {
|
||||
const { data, failureStatus } = await getData(url);
|
||||
if (!failureStatus && data.bugs.length === 1) {
|
||||
this.setState({ summary: data.bugs[0].summary });
|
||||
}
|
||||
}
|
||||
|
||||
async getTableData(url) {
|
||||
this.setState({ tableFailureStatus: null, isFetchingTable: true });
|
||||
const { data, failureStatus } = await getData(url);
|
||||
let mergedData = null;
|
||||
|
||||
if (defaultState.route === '/main' && !failureStatus && data.length) {
|
||||
const bugIds = formatBugs(data);
|
||||
const bugzillaData = await this.batchBugRequests(bugIds);
|
||||
mergedData = mergeData(data, bugzillaData);
|
||||
}
|
||||
|
||||
this.setState({ tableData: mergedData || data, tableFailureStatus: failureStatus, isFetchingTable: false });
|
||||
}
|
||||
|
||||
async getGraphData(url) {
|
||||
this.setState({ graphFailureStatus: null, isFetchingGraphs: true });
|
||||
const { data, failureStatus } = await getData(url);
|
||||
this.setState({ graphData: data, graphFailureStatus: failureStatus, isFetchingGraphs: false });
|
||||
}
|
||||
|
||||
async batchBugRequests(bugIds) {
|
||||
const urlParams = {
|
||||
include_fields: 'id,status,summary,whiteboard',
|
||||
};
|
||||
// TODO: bump up the max to ~1200 when this bug is fixed:
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1497721
|
||||
let min = 0;
|
||||
let max = 800;
|
||||
let bugsList = [];
|
||||
const results = [];
|
||||
|
||||
while (bugIds.length >= min) {
|
||||
const batch = bugIds.slice(min, max + 1);
|
||||
urlParams.id = batch.join();
|
||||
results.push(getData(bugzillaBugsApi('bug', urlParams)));
|
||||
|
||||
min = max;
|
||||
max += 800;
|
||||
}
|
||||
|
||||
for (const result of await Promise.all(results)) {
|
||||
bugsList = [...bugsList, ...result.data.bugs];
|
||||
}
|
||||
return bugsList;
|
||||
}
|
||||
|
||||
updateState(updatedObj) {
|
||||
this.setState(updatedObj, () => {
|
||||
setQueryParams() {
|
||||
const { location, history } = this.props;
|
||||
const { startday, endday, tree, bug } = this.state;
|
||||
const params = { startday, endday, tree };
|
||||
|
||||
|
@ -138,62 +73,156 @@ const withView = defaultState => WrappedComponent =>
|
|||
params.bug = bug;
|
||||
}
|
||||
|
||||
this.getGraphData(createApiUrl(graphsEndpoint, params));
|
||||
this.getTableData(createApiUrl(defaultState.endpoint, params));
|
||||
if (location.search !== '' && !location.state) {
|
||||
// update data based on the params or show error if params are missing
|
||||
this.checkQueryValidation(parseQueryParams(location.search));
|
||||
} else {
|
||||
// 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);
|
||||
}
|
||||
|
||||
// update query params if dates or tree are updated
|
||||
const queryString = createQueryParams(params);
|
||||
updateQueryParams(defaultState.route, queryString, this.props.history, this.props.location);
|
||||
});
|
||||
}
|
||||
|
||||
updateData(params, urlChanged = false) {
|
||||
const { mainGraphData, mainTableData } = this.props;
|
||||
|
||||
if (mainGraphData && mainTableData && !urlChanged) {
|
||||
this.setState({ graphData: mainGraphData, tableData: mainTableData });
|
||||
} else {
|
||||
this.getGraphData(createApiUrl(graphsEndpoint, params));
|
||||
this.getTableData(createApiUrl(defaultState.endpoint, params));
|
||||
this.setState({ initialParamsSet: true });
|
||||
this.getGraphData(createApiUrl(graphsEndpoint, params));
|
||||
this.getTableData(createApiUrl(defaultState.endpoint, params));
|
||||
}
|
||||
}
|
||||
|
||||
if (params.bug && this.state.tableData.length) {
|
||||
this.getBugDetails(bugzillaBugsApi('bug', { include_fields: 'summary', id: params.bug }));
|
||||
async getBugDetails(url) {
|
||||
const { data, failureStatus } = await getData(url);
|
||||
if (!failureStatus && data.bugs.length === 1) {
|
||||
this.setState({ summary: data.bugs[0].summary });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkQueryValidation(params, urlChanged = false) {
|
||||
const { errorMessages, initialParamsSet, summary } = this.state;
|
||||
const messages = validateQueryParams(params, defaultState.route === '/bugdetails');
|
||||
const updates = {};
|
||||
async getTableData(url) {
|
||||
this.setState({ tableFailureStatus: null, isFetchingTable: true });
|
||||
const { data, failureStatus } = await getData(url);
|
||||
let mergedData = null;
|
||||
|
||||
if (messages.length > 0) {
|
||||
this.setState({ errorMessages: messages });
|
||||
} else {
|
||||
if (errorMessages.length) {
|
||||
updates.errorMessages = [];
|
||||
}
|
||||
if (!initialParamsSet) {
|
||||
updates.initialParamsSet = true;
|
||||
}
|
||||
if (summary) {
|
||||
// reset summary
|
||||
updates.summary = null;
|
||||
if (defaultState.route === '/main' && !failureStatus && data.length) {
|
||||
const bugIds = formatBugs(data);
|
||||
const bugzillaData = await this.batchBugRequests(bugIds);
|
||||
mergedData = mergeData(data, bugzillaData);
|
||||
}
|
||||
|
||||
this.setState({ ...updates, ...params });
|
||||
this.updateData(params, urlChanged);
|
||||
this.setState({
|
||||
tableData: mergedData || data,
|
||||
tableFailureStatus: failureStatus,
|
||||
isFetchingTable: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const updateState = { updateState: this.updateState };
|
||||
const newProps = { ...this.props, ...this.state, ...updateState };
|
||||
return (
|
||||
<WrappedComponent {...newProps} />
|
||||
);
|
||||
}
|
||||
};
|
||||
async getGraphData(url) {
|
||||
this.setState({ graphFailureStatus: null, isFetchingGraphs: true });
|
||||
const { data, failureStatus } = await getData(url);
|
||||
this.setState({
|
||||
graphData: data,
|
||||
graphFailureStatus: failureStatus,
|
||||
isFetchingGraphs: false,
|
||||
});
|
||||
}
|
||||
|
||||
async batchBugRequests(bugIds) {
|
||||
const urlParams = {
|
||||
include_fields: 'id,status,summary,whiteboard',
|
||||
};
|
||||
// TODO: bump up the max to ~1200 when this bug is fixed:
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1497721
|
||||
let min = 0;
|
||||
let max = 800;
|
||||
let bugsList = [];
|
||||
const results = [];
|
||||
|
||||
while (bugIds.length >= min) {
|
||||
const batch = bugIds.slice(min, max + 1);
|
||||
urlParams.id = batch.join();
|
||||
results.push(getData(bugzillaBugsApi('bug', urlParams)));
|
||||
|
||||
min = max;
|
||||
max += 800;
|
||||
}
|
||||
|
||||
for (const result of await Promise.all(results)) {
|
||||
bugsList = [...bugsList, ...result.data.bugs];
|
||||
}
|
||||
return bugsList;
|
||||
}
|
||||
|
||||
updateState(updatedObj) {
|
||||
this.setState(updatedObj, () => {
|
||||
const { startday, endday, tree, bug } = this.state;
|
||||
const params = { startday, endday, tree };
|
||||
|
||||
if (bug) {
|
||||
params.bug = bug;
|
||||
}
|
||||
|
||||
this.getGraphData(createApiUrl(graphsEndpoint, params));
|
||||
this.getTableData(createApiUrl(defaultState.endpoint, params));
|
||||
|
||||
// update query params if dates or tree are updated
|
||||
const queryString = createQueryParams(params);
|
||||
updateQueryParams(
|
||||
defaultState.route,
|
||||
queryString,
|
||||
this.props.history,
|
||||
this.props.location,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
updateData(params, urlChanged = false) {
|
||||
const { mainGraphData, mainTableData } = this.props;
|
||||
|
||||
if (mainGraphData && mainTableData && !urlChanged) {
|
||||
this.setState({ graphData: mainGraphData, tableData: mainTableData });
|
||||
} else {
|
||||
this.getGraphData(createApiUrl(graphsEndpoint, params));
|
||||
this.getTableData(createApiUrl(defaultState.endpoint, params));
|
||||
}
|
||||
|
||||
if (params.bug && this.state.tableData.length) {
|
||||
this.getBugDetails(
|
||||
bugzillaBugsApi('bug', { include_fields: 'summary', id: params.bug }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
checkQueryValidation(params, urlChanged = false) {
|
||||
const { errorMessages, initialParamsSet, summary } = this.state;
|
||||
const messages = validateQueryParams(
|
||||
params,
|
||||
defaultState.route === '/bugdetails',
|
||||
);
|
||||
const updates = {};
|
||||
|
||||
if (messages.length > 0) {
|
||||
this.setState({ errorMessages: messages });
|
||||
} else {
|
||||
if (errorMessages.length) {
|
||||
updates.errorMessages = [];
|
||||
}
|
||||
if (!initialParamsSet) {
|
||||
updates.initialParamsSet = true;
|
||||
}
|
||||
if (summary) {
|
||||
// reset summary
|
||||
updates.summary = null;
|
||||
}
|
||||
|
||||
this.setState({ ...updates, ...params });
|
||||
this.updateData(params, urlChanged);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const updateState = { updateState: this.updateState };
|
||||
const newProps = { ...this.props, ...this.state, ...updateState };
|
||||
return <WrappedComponent {...newProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
withView.propTypes = {
|
||||
history: PropTypes.shape({}).isRequired,
|
||||
|
|
|
@ -51,9 +51,11 @@ 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.',
|
||||
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.',
|
||||
status503:
|
||||
'There was a problem retrieving the data. Please try again in a minute.',
|
||||
};
|
||||
|
||||
export const errorMessageClass = 'text-danger py-4 d-block';
|
||||
|
|
|
@ -47,7 +47,7 @@ export const calculateMetrics = function calculateMetricsForGraphs(data) {
|
|||
for (let i = 0; i < data.length; i++) {
|
||||
const failures = data[i].failure_count;
|
||||
const testRuns = data[i].test_runs;
|
||||
const freq = (testRuns < 1 || failures < 1) ? 0 : failures / testRuns;
|
||||
const freq = testRuns < 1 || failures < 1 ? 0 : failures / testRuns;
|
||||
// metrics graphics only accepts JS Date objects
|
||||
const date = moment(data[i].date).toDate();
|
||||
|
||||
|
@ -57,19 +57,29 @@ export const calculateMetrics = function calculateMetricsForGraphs(data) {
|
|||
dateTestRunCounts.push({ date, value: testRuns });
|
||||
dateFreqs.push({ date, value: freq });
|
||||
}
|
||||
return { graphOneData: dateFreqs, graphTwoData: [dateCounts, dateTestRunCounts], totalFailures, totalRuns };
|
||||
return {
|
||||
graphOneData: dateFreqs,
|
||||
graphTwoData: [dateCounts, dateTestRunCounts],
|
||||
totalFailures,
|
||||
totalRuns,
|
||||
};
|
||||
};
|
||||
|
||||
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 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]);
|
||||
const item2 = (desc ? a[sortBy] : b[sortBy]);
|
||||
const item1 = desc ? b[sortBy] : a[sortBy];
|
||||
const item2 = desc ? a[sortBy] : b[sortBy];
|
||||
|
||||
if (item1 < item2) {
|
||||
return -1;
|
||||
|
@ -82,7 +92,10 @@ export const sortData = function sortData(data, sortBy, desc) {
|
|||
return data;
|
||||
};
|
||||
|
||||
export const processErrorMessage = function processErrorMessage(errorMessage, status) {
|
||||
export const processErrorMessage = function processErrorMessage(
|
||||
errorMessage,
|
||||
status,
|
||||
) {
|
||||
const messages = [];
|
||||
|
||||
if (status === 503) {
|
||||
|
@ -101,7 +114,10 @@ export const processErrorMessage = function processErrorMessage(errorMessage, st
|
|||
return messages || [prettyErrorMessages.default];
|
||||
};
|
||||
|
||||
export const validateQueryParams = function validateQueryParams(params, bugRequired = false) {
|
||||
export const validateQueryParams = function validateQueryParams(
|
||||
params,
|
||||
bugRequired = false,
|
||||
) {
|
||||
const messages = [];
|
||||
const dateFormat = /\d{4}[-]\d{2}[-]\d{2}/;
|
||||
|
||||
|
|
|
@ -27,7 +27,11 @@ const DEFAULT_DETAILS_PCT = 40;
|
|||
const REVISION_POLL_INTERVAL = 1000 * 60 * 5;
|
||||
const REVISION_POLL_DELAYED_INTERVAL = 1000 * 60 * 60;
|
||||
const HIDDEN_URL_PARAMS = [
|
||||
'repo', 'classifiedState', 'resultStatus', 'selectedJob', 'searchStr',
|
||||
'repo',
|
||||
'classifiedState',
|
||||
'resultStatus',
|
||||
'selectedJob',
|
||||
'searchStr',
|
||||
];
|
||||
|
||||
const getWindowHeight = function getWindowHeight() {
|
||||
|
@ -83,13 +87,14 @@ class App extends React.Component {
|
|||
this.handleUrlChanges = this.handleUrlChanges.bind(this);
|
||||
this.showOnScreenShortcuts = this.showOnScreenShortcuts.bind(this);
|
||||
|
||||
RepositoryModel.getList().then((repos) => {
|
||||
const currentRepo = repos.find(repo => repo.name === repoName) || this.state.currentRepo;
|
||||
RepositoryModel.getList().then(repos => {
|
||||
const currentRepo =
|
||||
repos.find(repo => repo.name === repoName) || this.state.currentRepo;
|
||||
|
||||
this.setState({ currentRepo, repos });
|
||||
});
|
||||
|
||||
ClassificationTypeModel.getList().then((classificationTypes) => {
|
||||
ClassificationTypeModel.getList().then(classificationTypes => {
|
||||
this.setState({
|
||||
classificationTypes,
|
||||
classificationMap: ClassificationTypeModel.getMap(classificationTypes),
|
||||
|
@ -100,29 +105,38 @@ class App extends React.Component {
|
|||
window.addEventListener('hashchange', this.handleUrlChanges, false);
|
||||
|
||||
// Get the current Treeherder revision and poll to notify on updates.
|
||||
this.fetchDeployedRevision().then((revision) => {
|
||||
this.fetchDeployedRevision().then(revision => {
|
||||
this.setState({ serverRev: revision });
|
||||
this.updateInterval = setInterval(() => {
|
||||
this.fetchDeployedRevision()
|
||||
.then((revision) => {
|
||||
const { serverChangedTimestamp, serverRev, serverChanged } = this.state;
|
||||
this.fetchDeployedRevision().then(revision => {
|
||||
const {
|
||||
serverChangedTimestamp,
|
||||
serverRev,
|
||||
serverChanged,
|
||||
} = this.state;
|
||||
|
||||
if (serverChanged) {
|
||||
if (Date.now() - serverChangedTimestamp > REVISION_POLL_DELAYED_INTERVAL) {
|
||||
this.setState({ serverChangedDelayed: true });
|
||||
// Now that we know there's an update, stop polling.
|
||||
clearInterval(this.updateInterval);
|
||||
}
|
||||
if (serverChanged) {
|
||||
if (
|
||||
Date.now() - serverChangedTimestamp >
|
||||
REVISION_POLL_DELAYED_INTERVAL
|
||||
) {
|
||||
this.setState({ serverChangedDelayed: true });
|
||||
// Now that we know there's an update, stop polling.
|
||||
clearInterval(this.updateInterval);
|
||||
}
|
||||
// This request returns the treeherder git revision running on the server
|
||||
// If this differs from the version chosen during the UI page load, show a warning
|
||||
if (serverRev && serverRev !== revision) {
|
||||
this.setState({ serverRev: revision });
|
||||
if (serverChanged === false) {
|
||||
this.setState({ serverChangedTimestamp: Date.now(), serverChanged: true });
|
||||
}
|
||||
}
|
||||
// This request returns the treeherder git revision running on the server
|
||||
// If this differs from the version chosen during the UI page load, show a warning
|
||||
if (serverRev && serverRev !== revision) {
|
||||
this.setState({ serverRev: revision });
|
||||
if (serverChanged === false) {
|
||||
this.setState({
|
||||
serverChangedTimestamp: Date.now(),
|
||||
serverChanged: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}, REVISION_POLL_INTERVAL);
|
||||
});
|
||||
}
|
||||
|
@ -136,8 +150,10 @@ class App extends React.Component {
|
|||
const defaultPushListPct = hasSelectedJob ? 100 - DEFAULT_DETAILS_PCT : 100;
|
||||
// calculate the height of the details panel to use if it has not been
|
||||
// resized by the user.
|
||||
const defaultDetailsHeight = defaultPushListPct < 100 ?
|
||||
DEFAULT_DETAILS_PCT / 100 * getWindowHeight() : 0;
|
||||
const defaultDetailsHeight =
|
||||
defaultPushListPct < 100
|
||||
? (DEFAULT_DETAILS_PCT / 100) * getWindowHeight()
|
||||
: 0;
|
||||
|
||||
return {
|
||||
defaultPushListPct,
|
||||
|
@ -196,16 +212,29 @@ class App extends React.Component {
|
|||
|
||||
handleSplitChange(latestSplitSize) {
|
||||
this.setState({
|
||||
latestSplitPct: latestSplitSize / getWindowHeight() * 100,
|
||||
latestSplitPct: (latestSplitSize / getWindowHeight()) * 100,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
user, isFieldFilterVisible, serverChangedDelayed,
|
||||
defaultPushListPct, defaultDetailsHeight, latestSplitPct, serverChanged,
|
||||
currentRepo, repoName, repos, classificationTypes, classificationMap,
|
||||
filterModel, hasSelectedJob, revision, duplicateJobsVisible, groupCountsExpanded,
|
||||
user,
|
||||
isFieldFilterVisible,
|
||||
serverChangedDelayed,
|
||||
defaultPushListPct,
|
||||
defaultDetailsHeight,
|
||||
latestSplitPct,
|
||||
serverChanged,
|
||||
currentRepo,
|
||||
repoName,
|
||||
repos,
|
||||
classificationTypes,
|
||||
classificationMap,
|
||||
filterModel,
|
||||
hasSelectedJob,
|
||||
revision,
|
||||
duplicateJobsVisible,
|
||||
groupCountsExpanded,
|
||||
showShortCuts,
|
||||
} = this.state;
|
||||
|
||||
|
@ -220,16 +249,21 @@ class App extends React.Component {
|
|||
// we resize. Therefore, we must calculate the new
|
||||
// height of the DetailsPanel based on the current height of the PushList.
|
||||
// Reported this upstream: https://github.com/tomkp/react-split-pane/issues/282
|
||||
const pushListPct = latestSplitPct === undefined || !hasSelectedJob ?
|
||||
defaultPushListPct :
|
||||
latestSplitPct;
|
||||
const detailsHeight = latestSplitPct === undefined || !hasSelectedJob ?
|
||||
defaultDetailsHeight :
|
||||
getWindowHeight() * (1 - latestSplitPct / 100);
|
||||
const filterBarFilters = Object.entries(filterModel.urlParams).reduce((acc, [field, value]) => (
|
||||
HIDDEN_URL_PARAMS.includes(field) || matchesDefaults(field, value) ?
|
||||
acc : [...acc, { field, value }]
|
||||
), []);
|
||||
const pushListPct =
|
||||
latestSplitPct === undefined || !hasSelectedJob
|
||||
? defaultPushListPct
|
||||
: latestSplitPct;
|
||||
const detailsHeight =
|
||||
latestSplitPct === undefined || !hasSelectedJob
|
||||
? defaultDetailsHeight
|
||||
: getWindowHeight() * (1 - latestSplitPct / 100);
|
||||
const filterBarFilters = Object.entries(filterModel.urlParams).reduce(
|
||||
(acc, [field, value]) =>
|
||||
HIDDEN_URL_PARAMS.includes(field) || matchesDefaults(field, value)
|
||||
? acc
|
||||
: [...acc, { field, value }],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div id="global-container" className="height-minus-navbars">
|
||||
|
@ -260,16 +294,22 @@ class App extends React.Component {
|
|||
onChange={size => this.handleSplitChange(size)}
|
||||
>
|
||||
<div className="d-flex flex-column w-100">
|
||||
{(isFieldFilterVisible || !!filterBarFilters.length) && <ActiveFilters
|
||||
classificationTypes={classificationTypes}
|
||||
filterModel={filterModel}
|
||||
filterBarFilters={filterBarFilters}
|
||||
isFieldFilterVisible={isFieldFilterVisible}
|
||||
toggleFieldFilterVisible={this.toggleFieldFilterVisible}
|
||||
/>}
|
||||
{serverChangedDelayed && <UpdateAvailable
|
||||
updateButtonClick={this.updateButtonClick}
|
||||
/>}
|
||||
{(isFieldFilterVisible || !!filterBarFilters.length) && (
|
||||
<ActiveFilters
|
||||
classificationTypes={classificationTypes}
|
||||
filterModel={filterModel}
|
||||
filterBarFilters={filterBarFilters}
|
||||
isFieldFilterVisible={isFieldFilterVisible}
|
||||
toggleFieldFilterVisible={
|
||||
this.toggleFieldFilterVisible
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{serverChangedDelayed && (
|
||||
<UpdateAvailable
|
||||
updateButtonClick={this.updateButtonClick}
|
||||
/>
|
||||
)}
|
||||
<div id="th-global-content" className="th-global-content">
|
||||
<span className="th-view-content" tabIndex={-1}>
|
||||
<PushList
|
||||
|
@ -294,16 +334,18 @@ class App extends React.Component {
|
|||
/>
|
||||
</SplitPane>
|
||||
<NotificationList />
|
||||
{showShortCuts && <div
|
||||
id="onscreen-overlay"
|
||||
onClick={() => this.showOnScreenShortcuts(false)}
|
||||
>
|
||||
<div id="onscreen-shortcuts">
|
||||
<div className="col-8">
|
||||
<ShortcutTable />
|
||||
{showShortCuts && (
|
||||
<div
|
||||
id="onscreen-overlay"
|
||||
onClick={() => this.showOnScreenShortcuts(false)}
|
||||
>
|
||||
<div id="onscreen-shortcuts">
|
||||
<div className="col-8">
|
||||
<ShortcutTable />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
)}
|
||||
</KeyboardShortcuts>
|
||||
</SelectedJob>
|
||||
</PinnedJobs>
|
||||
|
|
|
@ -5,9 +5,7 @@ import Ajv from 'ajv';
|
|||
import jsonSchemaDefaults from 'json-schema-defaults';
|
||||
import jsyaml from 'js-yaml';
|
||||
import { slugid } from 'taskcluster-client-web';
|
||||
import {
|
||||
Button, Modal, ModalHeader, ModalBody, ModalFooter,
|
||||
} from 'reactstrap';
|
||||
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
|
||||
|
||||
import { formatTaskclusterError } from '../helpers/errorMessage';
|
||||
import TaskclusterModel from '../models/taskcluster';
|
||||
|
@ -41,19 +39,30 @@ class CustomJobActions extends React.PureComponent {
|
|||
this.close = this.close.bind(this);
|
||||
this.triggerAction = this.triggerAction.bind(this);
|
||||
|
||||
getGeckoDecisionTaskId(pushId).then((decisionTaskId) => {
|
||||
TaskclusterModel.load(decisionTaskId, job).then((results) => {
|
||||
const { originalTask, originalTaskId, staticActionVariables, actions } = results;
|
||||
const actionOptions = actions.map(action => ({ value: action, label: action.title }));
|
||||
|
||||
this.setState({
|
||||
getGeckoDecisionTaskId(pushId).then(decisionTaskId => {
|
||||
TaskclusterModel.load(decisionTaskId, job).then(results => {
|
||||
const {
|
||||
originalTask,
|
||||
originalTaskId,
|
||||
actions,
|
||||
staticActionVariables,
|
||||
actionOptions,
|
||||
selectedActionOption: actionOptions[0],
|
||||
}, () => this.updateSelectedAction(actions[0]));
|
||||
actions,
|
||||
} = results;
|
||||
const actionOptions = actions.map(action => ({
|
||||
value: action,
|
||||
label: action.title,
|
||||
}));
|
||||
|
||||
this.setState(
|
||||
{
|
||||
originalTask,
|
||||
originalTaskId,
|
||||
actions,
|
||||
staticActionVariables,
|
||||
actionOptions,
|
||||
selectedActionOption: actionOptions[0],
|
||||
},
|
||||
() => this.updateSelectedAction(actions[0]),
|
||||
);
|
||||
});
|
||||
this.setState({ decisionTaskId });
|
||||
});
|
||||
|
@ -87,8 +96,14 @@ class CustomJobActions extends React.PureComponent {
|
|||
triggerAction() {
|
||||
this.setState({ triggering: true });
|
||||
const {
|
||||
ajv, validate, payload, decisionTaskId, originalTaskId, originalTask,
|
||||
selectedActionOption, staticActionVariables,
|
||||
ajv,
|
||||
validate,
|
||||
payload,
|
||||
decisionTaskId,
|
||||
originalTaskId,
|
||||
originalTask,
|
||||
selectedActionOption,
|
||||
staticActionVariables,
|
||||
} = this.state;
|
||||
const { notify } = this.props;
|
||||
const action = selectedActionOption.value;
|
||||
|
@ -111,34 +126,40 @@ class CustomJobActions extends React.PureComponent {
|
|||
}
|
||||
|
||||
TaskclusterModel.submit({
|
||||
action,
|
||||
actionTaskId: slugid(),
|
||||
decisionTaskId,
|
||||
taskId: originalTaskId,
|
||||
task: originalTask,
|
||||
input,
|
||||
staticActionVariables,
|
||||
}).then((taskId) => {
|
||||
this.setState({ triggering: false });
|
||||
let message = 'Custom action request sent successfully:';
|
||||
let url = `https://tools.taskcluster.net/tasks/${taskId}`;
|
||||
action,
|
||||
actionTaskId: slugid(),
|
||||
decisionTaskId,
|
||||
taskId: originalTaskId,
|
||||
task: originalTask,
|
||||
input,
|
||||
staticActionVariables,
|
||||
}).then(
|
||||
taskId => {
|
||||
this.setState({ triggering: false });
|
||||
let message = 'Custom action request sent successfully:';
|
||||
let url = `https://tools.taskcluster.net/tasks/${taskId}`;
|
||||
|
||||
// For the time being, we are redirecting specific actions to
|
||||
// specific urls that are different than usual. At this time, we are
|
||||
// only directing loaner tasks to the loaner UI in the tools site.
|
||||
// It is possible that we may make this a part of the spec later.
|
||||
const loaners = ['docker-worker-linux-loaner', 'generic-worker-windows-loaner'];
|
||||
if (loaners.includes(action.name)) {
|
||||
message = 'Visit Taskcluster Tools site to access loaner:';
|
||||
url = `${url}/connect`;
|
||||
}
|
||||
notify(message, 'success', { linkText: 'Open in Taskcluster', url });
|
||||
this.close();
|
||||
}, (e) => {
|
||||
notify(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
this.setState({ triggering: false });
|
||||
this.close();
|
||||
});
|
||||
// For the time being, we are redirecting specific actions to
|
||||
// specific urls that are different than usual. At this time, we are
|
||||
// only directing loaner tasks to the loaner UI in the tools site.
|
||||
// It is possible that we may make this a part of the spec later.
|
||||
const loaners = [
|
||||
'docker-worker-linux-loaner',
|
||||
'generic-worker-windows-loaner',
|
||||
];
|
||||
if (loaners.includes(action.name)) {
|
||||
message = 'Visit Taskcluster Tools site to access loaner:';
|
||||
url = `${url}/connect`;
|
||||
}
|
||||
notify(message, 'success', { linkText: 'Open in Taskcluster', url });
|
||||
this.close();
|
||||
},
|
||||
e => {
|
||||
notify(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
this.setState({ triggering: false });
|
||||
this.close();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
close() {
|
||||
|
@ -154,62 +175,79 @@ class CustomJobActions extends React.PureComponent {
|
|||
render() {
|
||||
const { isLoggedIn, toggle } = this.props;
|
||||
const {
|
||||
triggering, selectedActionOption, schema, actions, actionOptions, payload,
|
||||
triggering,
|
||||
selectedActionOption,
|
||||
schema,
|
||||
actions,
|
||||
actionOptions,
|
||||
payload,
|
||||
} = this.state;
|
||||
const isOpen = true;
|
||||
const selectedAction = selectedActionOption.value;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} toggle={this.close} size="lg">
|
||||
<ModalHeader toggle={this.close}>Custom Taskcluster Job Actions</ModalHeader>
|
||||
<ModalHeader toggle={this.close}>
|
||||
Custom Taskcluster Job Actions
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
{!actions && <div>
|
||||
<p className="blink"> Getting available actions...</p>
|
||||
</div>}
|
||||
{!!actions && <div>
|
||||
<div className="form-group">
|
||||
<label>Action</label>
|
||||
<Select
|
||||
aria-describedby="selectedActionHelp"
|
||||
value={selectedActionOption}
|
||||
onChange={this.onChangeAction}
|
||||
options={actionOptions}
|
||||
/>
|
||||
<p
|
||||
id="selectedActionHelp"
|
||||
className="help-block"
|
||||
>{selectedAction.description}</p>
|
||||
{selectedAction.kind === 'hook' && <p>This action triggers hook
|
||||
<code>{selectedAction.hookGroupId}/{selectedAction.hookId}</code>
|
||||
</p>}
|
||||
{!actions && (
|
||||
<div>
|
||||
<p className="blink"> Getting available actions...</p>
|
||||
</div>
|
||||
<div className="row">
|
||||
{!!selectedAction.schema && <React.Fragment>
|
||||
<div className="col-s-12 col-md-6 form-group">
|
||||
<label>Payload</label>
|
||||
<textarea
|
||||
value={payload}
|
||||
className="form-control pre"
|
||||
rows="10"
|
||||
onChange={evt => this.onChangePayload(evt.target.value)}
|
||||
spellCheck="false"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-s-12 col-md-6 form-group">
|
||||
<label>Schema</label>
|
||||
<textarea
|
||||
className="form-control pre"
|
||||
rows="10"
|
||||
readOnly
|
||||
value={schema}
|
||||
/>
|
||||
</div>
|
||||
</React.Fragment>}
|
||||
)}
|
||||
{!!actions && (
|
||||
<div>
|
||||
<div className="form-group">
|
||||
<label>Action</label>
|
||||
<Select
|
||||
aria-describedby="selectedActionHelp"
|
||||
value={selectedActionOption}
|
||||
onChange={this.onChangeAction}
|
||||
options={actionOptions}
|
||||
/>
|
||||
<p id="selectedActionHelp" className="help-block">
|
||||
{selectedAction.description}
|
||||
</p>
|
||||
{selectedAction.kind === 'hook' && (
|
||||
<p>
|
||||
This action triggers hook
|
||||
<code>
|
||||
{selectedAction.hookGroupId}/{selectedAction.hookId}
|
||||
</code>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="row">
|
||||
{!!selectedAction.schema && (
|
||||
<React.Fragment>
|
||||
<div className="col-s-12 col-md-6 form-group">
|
||||
<label>Payload</label>
|
||||
<textarea
|
||||
value={payload}
|
||||
className="form-control pre"
|
||||
rows="10"
|
||||
onChange={evt => this.onChangePayload(evt.target.value)}
|
||||
spellCheck="false"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-s-12 col-md-6 form-group">
|
||||
<label>Schema</label>
|
||||
<textarea
|
||||
className="form-control pre"
|
||||
rows="10"
|
||||
readOnly
|
||||
value={schema}
|
||||
/>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{isLoggedIn ?
|
||||
{isLoggedIn ? (
|
||||
<Button
|
||||
color="secondary"
|
||||
className={`btn btn-primary-soft ${triggering ? 'disabled' : ''}`}
|
||||
|
@ -218,10 +256,13 @@ class CustomJobActions extends React.PureComponent {
|
|||
>
|
||||
<span className="fa fa-check-square-o" aria-hidden="true" />
|
||||
<span>{triggering ? 'Triggering' : 'Trigger'}</span>
|
||||
</Button> :
|
||||
</Button>
|
||||
) : (
|
||||
<p className="help-block"> Custom actions require login </p>
|
||||
}
|
||||
<Button color="secondary" onClick={toggle}>Cancel</Button>
|
||||
)}
|
||||
<Button color="secondary" onClick={toggle}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
@ -59,7 +59,9 @@ class KeyboardShortcuts extends React.Component {
|
|||
// open panels and selected job
|
||||
clearScreen() {
|
||||
const {
|
||||
clearSelectedJob, showOnScreenShortcuts, notifications,
|
||||
clearSelectedJob,
|
||||
showOnScreenShortcuts,
|
||||
notifications,
|
||||
clearOnScreenNotifications,
|
||||
} = this.props;
|
||||
|
||||
|
@ -134,7 +136,9 @@ class KeyboardShortcuts extends React.Component {
|
|||
|
||||
if (selectedJob) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(thEvents.jobRetrigger, { detail: { job: selectedJob } }),
|
||||
new CustomEvent(thEvents.jobRetrigger, {
|
||||
detail: { job: selectedJob },
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -172,11 +176,15 @@ class KeyboardShortcuts extends React.Component {
|
|||
return;
|
||||
}
|
||||
|
||||
if ((element.tagName === 'INPUT' &&
|
||||
element.type !== 'radio' && element.type !== 'checkbox') ||
|
||||
if (
|
||||
(element.tagName === 'INPUT' &&
|
||||
element.type !== 'radio' &&
|
||||
element.type !== 'checkbox') ||
|
||||
element.tagName === 'SELECT' ||
|
||||
element.tagName === 'TEXTAREA' ||
|
||||
element.isContentEditable || ev.key === 'shift') {
|
||||
element.isContentEditable ||
|
||||
ev.key === 'shift'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -186,19 +194,26 @@ class KeyboardShortcuts extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { filterModel, changeSelectedJob, showOnScreenShortcuts } = this.props;
|
||||
const {
|
||||
filterModel,
|
||||
changeSelectedJob,
|
||||
showOnScreenShortcuts,
|
||||
} = this.props;
|
||||
const handlers = {
|
||||
addRelatedBug: ev => this.doKey(ev, this.addRelatedBug),
|
||||
pinEditComment: ev => this.doKey(ev, this.pinEditComment),
|
||||
quickFilter: ev => this.doKey(ev, this.quickFilter),
|
||||
clearFilter: ev => this.doKey(ev, this.clearFilter),
|
||||
toggleInProgress: ev => this.doKey(ev, filterModel.toggleInProgress),
|
||||
nextUnclassified: ev => this.doKey(ev, () => changeSelectedJob('next', true)),
|
||||
previousUnclassified: ev => this.doKey(ev, () => changeSelectedJob('previous', true)),
|
||||
nextUnclassified: ev =>
|
||||
this.doKey(ev, () => changeSelectedJob('next', true)),
|
||||
previousUnclassified: ev =>
|
||||
this.doKey(ev, () => changeSelectedJob('previous', true)),
|
||||
openLogviewer: ev => this.doKey(ev, this.openLogviewer),
|
||||
jobRetrigger: ev => this.doKey(ev, this.jobRetrigger),
|
||||
selectNextTab: ev => this.doKey(ev, this.selectNextTab),
|
||||
toggleUnclassifiedFailures: ev => this.doKey(ev, filterModel.toggleUnclassifiedFailures),
|
||||
toggleUnclassifiedFailures: ev =>
|
||||
this.doKey(ev, filterModel.toggleUnclassifiedFailures),
|
||||
clearPinboard: ev => this.doKey(ev, this.clearPinboard),
|
||||
previousJob: ev => this.doKey(ev, () => changeSelectedJob('previous')),
|
||||
nextJob: ev => this.doKey(ev, () => changeSelectedJob('next')),
|
||||
|
@ -249,4 +264,6 @@ KeyboardShortcuts.defaultProps = {
|
|||
selectedJob: null,
|
||||
};
|
||||
|
||||
export default withPinnedJobs(withSelectedJob(withNotifications(KeyboardShortcuts)));
|
||||
export default withPinnedJobs(
|
||||
withSelectedJob(withNotifications(KeyboardShortcuts)),
|
||||
);
|
||||
|
|
|
@ -69,10 +69,15 @@ export class PinnedJobsClass extends React.Component {
|
|||
const { notify } = this.props;
|
||||
|
||||
if (MAX_SIZE - Object.keys(pinnedJobs).length > 0) {
|
||||
this.setValue({
|
||||
pinnedJobs: { ...pinnedJobs, [job.id]: job },
|
||||
isPinBoardVisible: true,
|
||||
}, () => { if (callback) callback(); });
|
||||
this.setValue(
|
||||
{
|
||||
pinnedJobs: { ...pinnedJobs, [job.id]: job },
|
||||
isPinBoardVisible: true,
|
||||
},
|
||||
() => {
|
||||
if (callback) callback();
|
||||
},
|
||||
);
|
||||
this.pulsePinCount();
|
||||
} else {
|
||||
notify(COUNT_ERROR, 'danger');
|
||||
|
@ -91,21 +96,26 @@ export class PinnedJobsClass extends React.Component {
|
|||
const { notify } = this.props;
|
||||
const spaceRemaining = MAX_SIZE - Object.keys(pinnedJobs).length;
|
||||
const showError = jobsToPin.length > spaceRemaining;
|
||||
const newPinnedJobs = jobsToPin.slice(0, spaceRemaining).reduce((acc, job) => ({ ...acc, [job.id]: job }), {});
|
||||
const newPinnedJobs = jobsToPin
|
||||
.slice(0, spaceRemaining)
|
||||
.reduce((acc, job) => ({ ...acc, [job.id]: job }), {});
|
||||
|
||||
if (!spaceRemaining) {
|
||||
notify(COUNT_ERROR, 'danger', { sticky: true });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setValue({
|
||||
pinnedJobs: { ...pinnedJobs, ...newPinnedJobs },
|
||||
isPinBoardVisible: true,
|
||||
}, () => {
|
||||
if (showError) {
|
||||
notify(COUNT_ERROR, 'danger', { sticky: true });
|
||||
}
|
||||
});
|
||||
this.setValue(
|
||||
{
|
||||
pinnedJobs: { ...pinnedJobs, ...newPinnedJobs },
|
||||
isPinBoardVisible: true,
|
||||
},
|
||||
() => {
|
||||
if (showError) {
|
||||
notify(COUNT_ERROR, 'danger', { sticky: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
addBug(bug, job) {
|
||||
|
@ -114,7 +124,7 @@ export class PinnedJobsClass extends React.Component {
|
|||
pinnedJobBugs[bug.id] = bug;
|
||||
this.setValue({ pinnedJobBugs: { ...pinnedJobBugs } });
|
||||
if (job) {
|
||||
this.pinJob(job);
|
||||
this.pinJob(job);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,11 @@ import keyBy from 'lodash/keyBy';
|
|||
import isEqual from 'lodash/isEqual';
|
||||
import max from 'lodash/max';
|
||||
|
||||
import { thDefaultRepo, thEvents, thMaxPushFetchSize } from '../../helpers/constants';
|
||||
import {
|
||||
thDefaultRepo,
|
||||
thEvents,
|
||||
thMaxPushFetchSize,
|
||||
} from '../../helpers/constants';
|
||||
import { parseQueryParams } from '../../helpers/url';
|
||||
import {
|
||||
getAllUrlParams,
|
||||
|
@ -68,7 +72,9 @@ export class PushesClass extends React.Component {
|
|||
this.updateJobMap = this.updateJobMap.bind(this);
|
||||
this.getAllShownJobs = this.getAllShownJobs.bind(this);
|
||||
this.fetchPushes = this.fetchPushes.bind(this);
|
||||
this.recalculateUnclassifiedCounts = this.recalculateUnclassifiedCounts.bind(this);
|
||||
this.recalculateUnclassifiedCounts = this.recalculateUnclassifiedCounts.bind(
|
||||
this,
|
||||
);
|
||||
this.setRevisionTips = this.setRevisionTips.bind(this);
|
||||
this.addPushes = this.addPushes.bind(this);
|
||||
this.getPush = this.getPush.bind(this);
|
||||
|
@ -125,26 +131,30 @@ export class PushesClass extends React.Component {
|
|||
const params = parseQueryParams(getQueryString());
|
||||
|
||||
return reloadOnChangeParameters.reduce(
|
||||
(acc, prop) => (params[prop] ? { ...acc, [prop]: params[prop] } : acc), {});
|
||||
(acc, prop) => (params[prop] ? { ...acc, [prop]: params[prop] } : acc),
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
getAllShownJobs(pushId) {
|
||||
const { jobMap } = this.state;
|
||||
const jobList = Object.values(jobMap);
|
||||
|
||||
return pushId ?
|
||||
jobList.filter(job => job.push_id === pushId && job.visible) :
|
||||
jobList.filter(job => job.visible);
|
||||
return pushId
|
||||
? jobList.filter(job => job.push_id === pushId && job.visible)
|
||||
: jobList.filter(job => job.visible);
|
||||
}
|
||||
|
||||
setRevisionTips() {
|
||||
const { pushList } = this.state;
|
||||
|
||||
this.setValue({ revisionTips: pushList.map(push => ({
|
||||
revision: push.revision,
|
||||
author: push.author,
|
||||
title: push.revisions[0].comments.split('\n')[0],
|
||||
})) });
|
||||
this.setValue({
|
||||
revisionTips: pushList.map(push => ({
|
||||
revision: push.revision,
|
||||
author: push.author,
|
||||
title: push.revisions[0].comments.split('\n')[0],
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
getPush(pushId) {
|
||||
|
@ -180,25 +190,26 @@ export class PushesClass extends React.Component {
|
|||
getGeckoDecisionJob(pushId) {
|
||||
const { jobMap } = this.state;
|
||||
|
||||
return Object.values(jobMap).find(job => (
|
||||
job.push_id === pushId &&
|
||||
job.platform === 'gecko-decision' &&
|
||||
job.state === 'completed' &&
|
||||
job.job_type_symbol === 'D'));
|
||||
return Object.values(jobMap).find(
|
||||
job =>
|
||||
job.push_id === pushId &&
|
||||
job.platform === 'gecko-decision' &&
|
||||
job.state === 'completed' &&
|
||||
job.job_type_symbol === 'D',
|
||||
);
|
||||
}
|
||||
|
||||
getGeckoDecisionTaskId(pushId, repoName) {
|
||||
const decisionTask = this.getGeckoDecisionJob(pushId);
|
||||
if (decisionTask) {
|
||||
return JobModel.get(repoName, decisionTask.id).then(
|
||||
(job) => {
|
||||
// this failure case is unlikely, but I guess you
|
||||
// never know
|
||||
if (!job.taskcluster_metadata) {
|
||||
return Promise.reject('Decision task missing taskcluster metadata');
|
||||
}
|
||||
return job.taskcluster_metadata.task_id;
|
||||
});
|
||||
return JobModel.get(repoName, decisionTask.id).then(job => {
|
||||
// this failure case is unlikely, but I guess you
|
||||
// never know
|
||||
if (!job.taskcluster_metadata) {
|
||||
return Promise.reject('Decision task missing taskcluster metadata');
|
||||
}
|
||||
return job.taskcluster_metadata.task_id;
|
||||
});
|
||||
}
|
||||
|
||||
// no decision task, we fail
|
||||
|
@ -207,7 +218,10 @@ export class PushesClass extends React.Component {
|
|||
|
||||
getLastModifiedJobTime() {
|
||||
const { jobMap } = this.state;
|
||||
const latest = max(Object.values(jobMap).map(job => new Date(`${job.last_modified}Z`))) || new Date();
|
||||
const latest =
|
||||
max(
|
||||
Object.values(jobMap).map(job => new Date(`${job.last_modified}Z`)),
|
||||
) || new Date();
|
||||
|
||||
latest.setSeconds(latest.getSeconds() - 3);
|
||||
return latest;
|
||||
|
@ -221,7 +235,10 @@ export class PushesClass extends React.Component {
|
|||
// within the constraints of the URL params
|
||||
const locationSearch = parseQueryParams(getQueryString());
|
||||
const pushPollingParams = pushPollingKeys.reduce(
|
||||
(acc, prop) => (locationSearch[prop] ? { ...acc, [prop]: locationSearch[prop] } : acc), {});
|
||||
(acc, prop) =>
|
||||
locationSearch[prop] ? { ...acc, [prop]: locationSearch[prop] } : acc,
|
||||
{},
|
||||
);
|
||||
|
||||
if (pushList.length === 1 && locationSearch.revision) {
|
||||
// If we are on a single revision, no need to poll for more pushes, but
|
||||
|
@ -235,16 +252,15 @@ export class PushesClass extends React.Component {
|
|||
}
|
||||
// We will either have a ``revision`` param, but no push for it yet,
|
||||
// or a ``fromchange`` param because we have at least 1 push already.
|
||||
PushModel.getList(pushPollingParams)
|
||||
.then(async (resp) => {
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
this.addPushes(data);
|
||||
this.fetchNewJobs();
|
||||
} else {
|
||||
notify('Error fetching new push data', 'danger', { sticky: true });
|
||||
}
|
||||
});
|
||||
PushModel.getList(pushPollingParams).then(async resp => {
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
this.addPushes(data);
|
||||
this.fetchNewJobs();
|
||||
} else {
|
||||
notify('Error fetching new push data', 'danger', { sticky: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
}, pushPollInterval);
|
||||
}
|
||||
|
@ -262,12 +278,16 @@ export class PushesClass extends React.Component {
|
|||
const newReloadTriggerParams = this.getNewReloadTriggerParams();
|
||||
// if we are just setting the repo to the default because none was
|
||||
// set initially, then don't reload the page.
|
||||
const defaulting = newReloadTriggerParams.repo === thDefaultRepo &&
|
||||
const defaulting =
|
||||
newReloadTriggerParams.repo === thDefaultRepo &&
|
||||
!cachedReloadTriggerParams.repo;
|
||||
|
||||
if (!defaulting && cachedReloadTriggerParams &&
|
||||
if (
|
||||
!defaulting &&
|
||||
cachedReloadTriggerParams &&
|
||||
!isEqual(newReloadTriggerParams, cachedReloadTriggerParams) &&
|
||||
!this.skipNextPageReload) {
|
||||
!this.skipNextPageReload
|
||||
) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
this.setState({ cachedReloadTriggerParams: newReloadTriggerParams });
|
||||
|
@ -301,15 +321,17 @@ export class PushesClass extends React.Component {
|
|||
delete options.tochange;
|
||||
options.push_timestamp__lte = oldestPushTimestamp;
|
||||
}
|
||||
return PushModel.getList(options).then(async (resp) => {
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
return PushModel.getList(options)
|
||||
.then(async resp => {
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
|
||||
this.addPushes(data.results.length ? data : { results: [] });
|
||||
} else {
|
||||
notify('Error retrieving push data!', 'danger', { sticky: true });
|
||||
}
|
||||
}).then(() => this.setValue({ loadingPushes: false }));
|
||||
this.addPushes(data.results.length ? data : { results: [] });
|
||||
} else {
|
||||
notify('Error retrieving push data!', 'danger', { sticky: true });
|
||||
}
|
||||
})
|
||||
.then(() => this.setValue({ loadingPushes: false }));
|
||||
}
|
||||
|
||||
addPushes(data) {
|
||||
|
@ -317,13 +339,16 @@ export class PushesClass extends React.Component {
|
|||
|
||||
if (data.results.length > 0) {
|
||||
const pushIds = pushList.map(push => push.id);
|
||||
const newPushList = [...pushList, ...data.results.filter(push => !pushIds.includes(push.id))];
|
||||
const newPushList = [
|
||||
...pushList,
|
||||
...data.results.filter(push => !pushIds.includes(push.id)),
|
||||
];
|
||||
newPushList.sort((a, b) => b.push_timestamp - a.push_timestamp);
|
||||
const oldestPushTimestamp = newPushList[newPushList.length - 1].push_timestamp;
|
||||
const oldestPushTimestamp =
|
||||
newPushList[newPushList.length - 1].push_timestamp;
|
||||
this.recalculateUnclassifiedCounts();
|
||||
this.setValue(
|
||||
{ pushList: newPushList, oldestPushTimestamp },
|
||||
() => this.setRevisionTips(),
|
||||
this.setValue({ pushList: newPushList, oldestPushTimestamp }, () =>
|
||||
this.setRevisionTips(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -345,13 +370,15 @@ export class PushesClass extends React.Component {
|
|||
// If a job is selected, and one of the jobs we just fetched is the
|
||||
// updated version of that selected job, then send that with the event.
|
||||
const selectedJobId = getUrlParam('selectedJob');
|
||||
const updatedSelectedJob = selectedJobId ?
|
||||
jobList.find(job => job.id === parseInt(selectedJobId, 10)) : null;
|
||||
const updatedSelectedJob = selectedJobId
|
||||
? jobList.find(job => job.id === parseInt(selectedJobId, 10))
|
||||
: null;
|
||||
|
||||
window.dispatchEvent(new CustomEvent(
|
||||
thEvents.applyNewJobs,
|
||||
{ detail: { jobs, updatedSelectedJob } },
|
||||
));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(thEvents.applyNewJobs, {
|
||||
detail: { jobs, updatedSelectedJob },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
updateUrlFromchange() {
|
||||
|
@ -378,7 +405,7 @@ export class PushesClass extends React.Component {
|
|||
let allUnclassifiedFailureCount = 0;
|
||||
let filteredUnclassifiedFailureCount = 0;
|
||||
|
||||
Object.values(jobMap).forEach((job) => {
|
||||
Object.values(jobMap).forEach(job => {
|
||||
if (isUnclassifiedFailure(job)) {
|
||||
if (tiers.includes(String(job.tier))) {
|
||||
if (filterModel.showJob(job)) {
|
||||
|
@ -388,7 +415,10 @@ export class PushesClass extends React.Component {
|
|||
}
|
||||
}
|
||||
});
|
||||
this.setValue({ allUnclassifiedFailureCount, filteredUnclassifiedFailureCount });
|
||||
this.setValue({
|
||||
allUnclassifiedFailureCount,
|
||||
filteredUnclassifiedFailureCount,
|
||||
});
|
||||
}
|
||||
|
||||
updateJobMap(jobList) {
|
||||
|
@ -411,7 +441,6 @@ export class PushesClass extends React.Component {
|
|||
</PushesContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PushesClass.propTypes = {
|
||||
|
@ -435,12 +464,16 @@ export function withPushes(Component) {
|
|||
jobsLoaded={context.jobsLoaded}
|
||||
loadingPushes={context.loadingPushes}
|
||||
allUnclassifiedFailureCount={context.allUnclassifiedFailureCount}
|
||||
filteredUnclassifiedFailureCount={context.filteredUnclassifiedFailureCount}
|
||||
filteredUnclassifiedFailureCount={
|
||||
context.filteredUnclassifiedFailureCount
|
||||
}
|
||||
updateJobMap={context.updateJobMap}
|
||||
getAllShownJobs={context.getAllShownJobs}
|
||||
fetchPushes={context.fetchPushes}
|
||||
getPush={context.getPush}
|
||||
recalculateUnclassifiedCounts={context.recalculateUnclassifiedCounts}
|
||||
recalculateUnclassifiedCounts={
|
||||
context.recalculateUnclassifiedCounts
|
||||
}
|
||||
getNextPushes={context.getNextPushes}
|
||||
getGeckoDecisionJob={context.getGeckoDecisionJob}
|
||||
getGeckoDecisionTaskId={context.getGeckoDecisionTaskId}
|
||||
|
|
|
@ -42,7 +42,9 @@ class SelectedJobClass extends React.Component {
|
|||
this.setSelectedJob = this.setSelectedJob.bind(this);
|
||||
this.clearSelectedJob = this.clearSelectedJob.bind(this);
|
||||
this.changeSelectedJob = this.changeSelectedJob.bind(this);
|
||||
this.noMoreUnclassifiedFailures = this.noMoreUnclassifiedFailures.bind(this);
|
||||
this.noMoreUnclassifiedFailures = this.noMoreUnclassifiedFailures.bind(
|
||||
this,
|
||||
);
|
||||
this.handleApplyNewJobs = this.handleApplyNewJobs.bind(this);
|
||||
|
||||
// TODO: this.value needs to now get the bound versions of the functions.
|
||||
|
@ -88,7 +90,10 @@ class SelectedJobClass extends React.Component {
|
|||
const selectedJobIdStr = getUrlParam('selectedJob');
|
||||
const selectedJobId = parseInt(selectedJobIdStr, 10);
|
||||
|
||||
if (selectedJobIdStr && (!selectedJob || selectedJob.id !== selectedJobId)) {
|
||||
if (
|
||||
selectedJobIdStr &&
|
||||
(!selectedJob || selectedJob.id !== selectedJobId)
|
||||
) {
|
||||
const selectedJob = jobMap[selectedJobIdStr];
|
||||
|
||||
// select the job in question
|
||||
|
@ -98,30 +103,37 @@ class SelectedJobClass extends React.Component {
|
|||
setUrlParam('selectedJob');
|
||||
// If the ``selectedJob`` was not mapped, then we need to notify
|
||||
// the user it's not in the range of the current result set list.
|
||||
JobModel.get(repoName, selectedJobId).then((job) => {
|
||||
PushModel.get(job.push_id).then(async (resp) => {
|
||||
if (resp.ok) {
|
||||
const push = await resp.json();
|
||||
const newPushUrl = getJobsUrl({ repo: repoName, revision: push.revision, selectedJob: selectedJobId });
|
||||
JobModel.get(repoName, selectedJobId)
|
||||
.then(job => {
|
||||
PushModel.get(job.push_id).then(async resp => {
|
||||
if (resp.ok) {
|
||||
const push = await resp.json();
|
||||
const newPushUrl = getJobsUrl({
|
||||
repo: repoName,
|
||||
revision: push.revision,
|
||||
selectedJob: selectedJobId,
|
||||
});
|
||||
|
||||
// the job exists, but isn't in any loaded push.
|
||||
// provide a message and link to load the right push
|
||||
notify(
|
||||
`Selected job id: ${selectedJobId} not within current push range.`,
|
||||
'danger',
|
||||
{ sticky: true, linkText: 'Load push', url: newPushUrl });
|
||||
} else {
|
||||
throw Error(`Unable to find push with id ${job.push_id} for selected job`);
|
||||
}
|
||||
// the job exists, but isn't in any loaded push.
|
||||
// provide a message and link to load the right push
|
||||
notify(
|
||||
`Selected job id: ${selectedJobId} not within current push range.`,
|
||||
'danger',
|
||||
{ sticky: true, linkText: 'Load push', url: newPushUrl },
|
||||
);
|
||||
} else {
|
||||
throw Error(
|
||||
`Unable to find push with id ${job.push_id} for selected job`,
|
||||
);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
// the job wasn't found in the db. Either never existed,
|
||||
// or was expired and deleted.
|
||||
this.clearSelectedJob();
|
||||
notify(`Selected Job - ${error}`, 'danger', { sticky: true });
|
||||
});
|
||||
}).catch((error) => {
|
||||
// the job wasn't found in the db. Either never existed,
|
||||
// or was expired and deleted.
|
||||
this.clearSelectedJob();
|
||||
notify(`Selected Job - ${error}`,
|
||||
'danger',
|
||||
{ sticky: true });
|
||||
});
|
||||
}
|
||||
} else if (!selectedJobIdStr && selectedJob) {
|
||||
this.setValue({ selectedJob: null });
|
||||
|
@ -193,13 +205,15 @@ class SelectedJobClass extends React.Component {
|
|||
|
||||
changeSelectedJob(direction, unclassifiedOnly) {
|
||||
const { pinnedJobs } = this.props;
|
||||
const jobNavSelector = unclassifiedOnly ?
|
||||
thJobNavSelectors.UNCLASSIFIED_FAILURES : thJobNavSelectors.ALL_JOBS;
|
||||
const jobNavSelector = unclassifiedOnly
|
||||
? thJobNavSelectors.UNCLASSIFIED_FAILURES
|
||||
: thJobNavSelectors.ALL_JOBS;
|
||||
// Get the appropriate next index based on the direction and current job
|
||||
// selection (if any). Must wrap end to end.
|
||||
const getIndex = direction === 'next' ?
|
||||
(idx, jobs) => (idx + 1 > jobs.length - 1 ? 0 : idx + 1) :
|
||||
(idx, jobs) => (idx - 1 < 0 ? jobs.length - 1 : idx - 1);
|
||||
const getIndex =
|
||||
direction === 'next'
|
||||
? (idx, jobs) => (idx + 1 > jobs.length - 1 ? 0 : idx + 1)
|
||||
: (idx, jobs) => (idx - 1 < 0 ? jobs.length - 1 : idx - 1);
|
||||
|
||||
// TODO: (bug 1434679) Move from using jquery here to find the next/prev
|
||||
// component. This could perhaps be done either with:
|
||||
|
@ -257,7 +271,8 @@ class SelectedJobClass extends React.Component {
|
|||
// a btn.
|
||||
// This will exclude the JobDetails and navbars.
|
||||
const globalContent = document.getElementById('th-global-content');
|
||||
const isEligible = globalContent.contains(target) &&
|
||||
const isEligible =
|
||||
globalContent.contains(target) &&
|
||||
target.tagName !== 'A' &&
|
||||
!intersection(target.classList, ['btn', 'dropdown-item']).length;
|
||||
|
||||
|
@ -278,7 +293,6 @@ class SelectedJobClass extends React.Component {
|
|||
</SelectedJobContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SelectedJobClass.propTypes = {
|
||||
|
@ -289,7 +303,9 @@ SelectedJobClass.propTypes = {
|
|||
children: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export const SelectedJob = withNotifications(withPushes(withPinnedJobs(SelectedJobClass)));
|
||||
export const SelectedJob = withNotifications(
|
||||
withPushes(withPinnedJobs(SelectedJobClass)),
|
||||
);
|
||||
|
||||
export function withSelectedJob(Component) {
|
||||
return function SelectedJobComponent(props) {
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Button, Modal, ModalHeader, ModalBody, ModalFooter, Tooltip, FormGroup, Input,
|
||||
Button,
|
||||
Modal,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Tooltip,
|
||||
FormGroup,
|
||||
Input,
|
||||
Label,
|
||||
} from 'reactstrap';
|
||||
|
||||
|
@ -16,11 +23,16 @@ import { create } from '../../helpers/http';
|
|||
import { withNotifications } from '../../shared/context/Notifications';
|
||||
|
||||
const crashRegex = /application crashed \[@ (.+)\]$/g;
|
||||
const omittedLeads = ['TEST-UNEXPECTED-FAIL', 'PROCESS-CRASH', 'TEST-UNEXPECTED-ERROR', 'REFTEST ERROR'];
|
||||
const omittedLeads = [
|
||||
'TEST-UNEXPECTED-FAIL',
|
||||
'PROCESS-CRASH',
|
||||
'TEST-UNEXPECTED-ERROR',
|
||||
'REFTEST ERROR',
|
||||
];
|
||||
/*
|
||||
* Find the first thing in the summary line that looks like a filename.
|
||||
*/
|
||||
const findFilename = (summary) => {
|
||||
const findFilename = summary => {
|
||||
// Take left side of any reftest comparisons, as the right side is the reference file
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
summary = summary.split('==')[0];
|
||||
|
@ -39,7 +51,7 @@ const findFilename = (summary) => {
|
|||
* Remove extraneous junk from the start of the summary line
|
||||
* and try to find the failing test name from what's left
|
||||
*/
|
||||
const parseSummary = (suggestion) => {
|
||||
const parseSummary = suggestion => {
|
||||
let summary = suggestion.search;
|
||||
const searchTerms = suggestion.search_terms;
|
||||
// Strip out some extra stuff at the start of some failure paths
|
||||
|
@ -59,7 +71,10 @@ const parseSummary = (suggestion) => {
|
|||
summary = summary.replace(re, '');
|
||||
summary = summary.replace('/_mozilla/', 'mozilla/tests/');
|
||||
// We don't want to include "REFTEST" when it's an unexpected pass
|
||||
summary = summary.replace('REFTEST TEST-UNEXPECTED-PASS', 'TEST-UNEXPECTED-PASS');
|
||||
summary = summary.replace(
|
||||
'REFTEST TEST-UNEXPECTED-PASS',
|
||||
'TEST-UNEXPECTED-PASS',
|
||||
);
|
||||
const summaryParts = summary.split(' | ');
|
||||
|
||||
// If the search_terms used for finding bug suggestions
|
||||
|
@ -67,7 +82,7 @@ const parseSummary = (suggestion) => {
|
|||
// for the full string match, so don't omit it in this case.
|
||||
// If it's not needed, go ahead and omit it.
|
||||
if (searchTerms.length && summaryParts.length > 1) {
|
||||
omittedLeads.forEach((lead) => {
|
||||
omittedLeads.forEach(lead => {
|
||||
if (!searchTerms[0].includes(lead) && summaryParts[0].includes(lead)) {
|
||||
summaryParts.shift();
|
||||
}
|
||||
|
@ -76,7 +91,10 @@ const parseSummary = (suggestion) => {
|
|||
|
||||
// Some of the TEST-FOO bits aren't removed from the summary,
|
||||
// so we sometimes end up with them instead of the test path here.
|
||||
const summaryName = summaryParts[0].startsWith('TEST-') && summaryParts.length > 1 ? summaryParts[1] : summaryParts[0];
|
||||
const summaryName =
|
||||
summaryParts[0].startsWith('TEST-') && summaryParts.length > 1
|
||||
? summaryParts[1]
|
||||
: summaryParts[0];
|
||||
const possibleFilename = findFilename(summaryName);
|
||||
|
||||
return [summaryParts, possibleFilename];
|
||||
|
@ -86,16 +104,30 @@ export class BugFilerClass extends React.Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const { suggestions, suggestion, fullLog, parsedLog, reftestUrl, jobGroupName } = props;
|
||||
const {
|
||||
suggestions,
|
||||
suggestion,
|
||||
fullLog,
|
||||
parsedLog,
|
||||
reftestUrl,
|
||||
jobGroupName,
|
||||
} = props;
|
||||
|
||||
const allFailures = suggestions.map(sugg => sugg.search
|
||||
.split(' | ')
|
||||
.filter(part => !omittedLeads.includes(part))
|
||||
.map(item => (item === 'REFTEST TEST-UNEXPECTED-PASS' ? 'TEST-UNEXPECTED-PASS' : item)),
|
||||
const allFailures = suggestions.map(sugg =>
|
||||
sugg.search
|
||||
.split(' | ')
|
||||
.filter(part => !omittedLeads.includes(part))
|
||||
.map(item =>
|
||||
item === 'REFTEST TEST-UNEXPECTED-PASS'
|
||||
? 'TEST-UNEXPECTED-PASS'
|
||||
: item,
|
||||
),
|
||||
);
|
||||
const thisFailure = allFailures.map(f => f.join(' | ')).join('\n');
|
||||
const crash = suggestion.search.match(crashRegex);
|
||||
const crashSignatures = crash ? [crash[0].split('application crashed ')[1]] : [];
|
||||
const crashSignatures = crash
|
||||
? [crash[0].split('application crashed ')[1]]
|
||||
: [];
|
||||
const parsedSummary = parseSummary(suggestion);
|
||||
|
||||
let summaryString = parsedSummary[0].join(' | ');
|
||||
|
@ -140,7 +172,7 @@ export class BugFilerClass extends React.Component {
|
|||
return 'Selected failure does not contain any searchable terms.';
|
||||
}
|
||||
if (searchTerms.every(term => !summary.includes(term))) {
|
||||
return 'Summary does not include the full text of any of the selected failure\'s search terms:';
|
||||
return "Summary does not include the full text of any of the selected failure's search terms:";
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
@ -157,7 +189,10 @@ export class BugFilerClass extends React.Component {
|
|||
if (jg.includes('talos')) {
|
||||
newProducts.push('Testing :: Talos');
|
||||
}
|
||||
if (jg.includes('mochitest') && (fp.includes('webextensions/') || fp.includes('components/extensions'))) {
|
||||
if (
|
||||
jg.includes('mochitest') &&
|
||||
(fp.includes('webextensions/') || fp.includes('components/extensions'))
|
||||
) {
|
||||
newProducts.push('WebExtensions :: General');
|
||||
}
|
||||
if (jg.includes('mochitest') && fp.includes('webrtc/')) {
|
||||
|
@ -193,12 +228,20 @@ export class BugFilerClass extends React.Component {
|
|||
this.setState({ searching: true });
|
||||
|
||||
if (productSearch) {
|
||||
const resp = await fetch(`${bzBaseUrl}rest/prod_comp_search/${productSearch}?limit=5`);
|
||||
const resp = await fetch(
|
||||
`${bzBaseUrl}rest/prod_comp_search/${productSearch}?limit=5`,
|
||||
);
|
||||
const data = await resp.json();
|
||||
const products = data.products.filter(item => !!item.product && !!item.component);
|
||||
suggestedProductsSet = new Set([...suggestedProductsSet, ...products.map(prod => (
|
||||
prod.product + (prod.component ? ` :: ${prod.component}` : '')
|
||||
))]);
|
||||
const products = data.products.filter(
|
||||
item => !!item.product && !!item.component,
|
||||
);
|
||||
suggestedProductsSet = new Set([
|
||||
...suggestedProductsSet,
|
||||
...products.map(
|
||||
prod =>
|
||||
prod.product + (prod.component ? ` :: ${prod.component}` : ''),
|
||||
),
|
||||
]);
|
||||
} else {
|
||||
let failurePath = parsedSummary[0][0];
|
||||
|
||||
|
@ -219,17 +262,22 @@ export class BugFilerClass extends React.Component {
|
|||
failurePath = `dom/media/test/external/external_media_tests/${failurePath}`;
|
||||
}
|
||||
if (lowerJobGroupName.includes('web platform')) {
|
||||
failurePath = failurePath.startsWith('mozilla/tests') ?
|
||||
`testing/web-platform/${failurePath}` :
|
||||
`testing/web-platform/tests/${failurePath}`;
|
||||
failurePath = failurePath.startsWith('mozilla/tests')
|
||||
? `testing/web-platform/${failurePath}`
|
||||
: `testing/web-platform/tests/${failurePath}`;
|
||||
}
|
||||
|
||||
// Search mercurial's moz.build metadata to find products/components
|
||||
fetch(`${hgBaseUrl}mozilla-central/json-mozbuildinfo?p=${failurePath}`)
|
||||
.then(resp => resp.json().then((firstRequest) => {
|
||||
|
||||
if (firstRequest.data.aggregate && firstRequest.data.aggregate.recommended_bug_component) {
|
||||
const suggested = firstRequest.data.aggregate.recommended_bug_component;
|
||||
fetch(
|
||||
`${hgBaseUrl}mozilla-central/json-mozbuildinfo?p=${failurePath}`,
|
||||
).then(resp =>
|
||||
resp.json().then(firstRequest => {
|
||||
if (
|
||||
firstRequest.data.aggregate &&
|
||||
firstRequest.data.aggregate.recommended_bug_component
|
||||
) {
|
||||
const suggested =
|
||||
firstRequest.data.aggregate.recommended_bug_component;
|
||||
suggestedProductsSet.add(`${suggested[0]} :: ${suggested[1]}`);
|
||||
}
|
||||
|
||||
|
@ -237,34 +285,53 @@ export class BugFilerClass extends React.Component {
|
|||
if (suggestedProductsSet.size === 0 && possibleFilename.length > 4) {
|
||||
const dxrlink = `${dxrBaseUrl}mozilla-central/search?q=file:${possibleFilename}&redirect=false&limit=5`;
|
||||
// Bug 1358328 - We need to override headers here until DXR returns JSON with the default Accept header
|
||||
fetch(dxrlink, { headers: { Accept: 'application/json' } })
|
||||
.then((secondRequest) => {
|
||||
fetch(dxrlink, { headers: { Accept: 'application/json' } }).then(
|
||||
secondRequest => {
|
||||
const { results } = secondRequest.data;
|
||||
let resultsCount = results.length;
|
||||
// If the search returns too many results, this probably isn't a good search term, so bail
|
||||
if (resultsCount === 0) {
|
||||
suggestedProductsSet = new Set([...suggestedProductsSet, this.getSpecialProducts(failurePath)]);
|
||||
suggestedProductsSet = new Set([
|
||||
...suggestedProductsSet,
|
||||
this.getSpecialProducts(failurePath),
|
||||
]);
|
||||
}
|
||||
results.forEach((result) => {
|
||||
fetch(`${hgBaseUrl}mozilla-central/json-mozbuildinfo?p=${result.path}`)
|
||||
.then((thirdRequest) => {
|
||||
if (thirdRequest.data.aggregate && thirdRequest.data.aggregate.recommended_bug_component) {
|
||||
const suggested = thirdRequest.data.aggregate.recommended_bug_component;
|
||||
suggestedProductsSet.add(`${suggested[0]} :: ${suggested[1]}`);
|
||||
}
|
||||
// Only get rid of the throbber when all of these searches have completed
|
||||
resultsCount -= 1;
|
||||
if (resultsCount === 0) {
|
||||
suggestedProductsSet = new Set([...suggestedProductsSet, this.getSpecialProducts(failurePath)]);
|
||||
}
|
||||
});
|
||||
results.forEach(result => {
|
||||
fetch(
|
||||
`${hgBaseUrl}mozilla-central/json-mozbuildinfo?p=${
|
||||
result.path
|
||||
}`,
|
||||
).then(thirdRequest => {
|
||||
if (
|
||||
thirdRequest.data.aggregate &&
|
||||
thirdRequest.data.aggregate.recommended_bug_component
|
||||
) {
|
||||
const suggested =
|
||||
thirdRequest.data.aggregate.recommended_bug_component;
|
||||
suggestedProductsSet.add(
|
||||
`${suggested[0]} :: ${suggested[1]}`,
|
||||
);
|
||||
}
|
||||
// Only get rid of the throbber when all of these searches have completed
|
||||
resultsCount -= 1;
|
||||
if (resultsCount === 0) {
|
||||
suggestedProductsSet = new Set([
|
||||
...suggestedProductsSet,
|
||||
this.getSpecialProducts(failurePath),
|
||||
]);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
} else {
|
||||
suggestedProductsSet = new Set([...suggestedProductsSet, this.getSpecialProducts(failurePath)]);
|
||||
suggestedProductsSet = new Set([
|
||||
...suggestedProductsSet,
|
||||
this.getSpecialProducts(failurePath),
|
||||
]);
|
||||
}
|
||||
|
||||
}));
|
||||
}),
|
||||
);
|
||||
}
|
||||
const newSuggestedProducts = [...suggestedProductsSet];
|
||||
|
||||
|
@ -277,9 +344,9 @@ export class BugFilerClass extends React.Component {
|
|||
|
||||
toggleCheckedLogLink(link) {
|
||||
const { checkedLogLinks } = this.state;
|
||||
const newCheckedLogLinks = checkedLogLinks.includes(link) ?
|
||||
checkedLogLinks.filter(item => item !== link) :
|
||||
[...checkedLogLinks, link];
|
||||
const newCheckedLogLinks = checkedLogLinks.includes(link)
|
||||
? checkedLogLinks.filter(item => item !== link)
|
||||
: [...checkedLogLinks, link];
|
||||
|
||||
this.setState({ checkedLogLinks: newCheckedLogLinks });
|
||||
}
|
||||
|
@ -289,19 +356,32 @@ export class BugFilerClass extends React.Component {
|
|||
*/
|
||||
async submitFiler() {
|
||||
const {
|
||||
summary, selectedProduct, comment, isIntermittent, checkedLogLinks,
|
||||
blocks, dependsOn, seeAlso, crashSignatures,
|
||||
summary,
|
||||
selectedProduct,
|
||||
comment,
|
||||
isIntermittent,
|
||||
checkedLogLinks,
|
||||
blocks,
|
||||
dependsOn,
|
||||
seeAlso,
|
||||
crashSignatures,
|
||||
} = this.state;
|
||||
const { toggle, successCallback, notify } = this.props;
|
||||
const [product, component] = selectedProduct.split(' :: ');
|
||||
|
||||
if (!selectedProduct) {
|
||||
notify('Please select (or search and select) a product/component pair to continue', 'danger');
|
||||
notify(
|
||||
'Please select (or search and select) a product/component pair to continue',
|
||||
'danger',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (summary.length > 255) {
|
||||
notify('Please ensure the summary is no more than 255 characters', 'danger');
|
||||
notify(
|
||||
'Please ensure the summary is no more than 255 characters',
|
||||
'danger',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -320,12 +400,16 @@ export class BugFilerClass extends React.Component {
|
|||
// submit the new bug. Only request the versions because some products
|
||||
// take quite a long time to fetch the full object
|
||||
try {
|
||||
const productResp = await fetch(bugzillaBugsApi(`product/${product}`, { include_fields: 'versions' }));
|
||||
const productResp = await fetch(
|
||||
bugzillaBugsApi(`product/${product}`, { include_fields: 'versions' }),
|
||||
);
|
||||
const productData = await productResp.json();
|
||||
if (productResp.ok) {
|
||||
const productObject = productData.products[0];
|
||||
// Find the newest version for the product that is_active
|
||||
const version = productObject.versions.filter(prodVer => prodVer.is_active).slice(-1)[0];
|
||||
const version = productObject.versions
|
||||
.filter(prodVer => prodVer.is_active)
|
||||
.slice(-1)[0];
|
||||
const payload = {
|
||||
product,
|
||||
component,
|
||||
|
@ -342,17 +426,30 @@ export class BugFilerClass extends React.Component {
|
|||
comment_tags: 'treeherder',
|
||||
};
|
||||
|
||||
const bugResp = await create(getApiUrl('/bugzilla/create_bug/'), payload);
|
||||
const bugResp = await create(
|
||||
getApiUrl('/bugzilla/create_bug/'),
|
||||
payload,
|
||||
);
|
||||
// const bugResp = await create('http://httpstat.us/404', payload);
|
||||
const data = await bugResp.json();
|
||||
if (bugResp.ok) {
|
||||
successCallback(data);
|
||||
toggle();
|
||||
} else {
|
||||
this.submitFailure('Treeherder Bug Filer API', bugResp.status, bugResp.statusText, data);
|
||||
this.submitFailure(
|
||||
'Treeherder Bug Filer API',
|
||||
bugResp.status,
|
||||
bugResp.statusText,
|
||||
data,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.submitFailure('Bugzilla', productResp.status, productResp.statusText, productData);
|
||||
this.submitFailure(
|
||||
'Bugzilla',
|
||||
productResp.status,
|
||||
productResp.statusText,
|
||||
productData,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
notify(`Error filing bug: ${e.toString()}`, 'danger', { sticky: true });
|
||||
|
@ -367,28 +464,45 @@ export class BugFilerClass extends React.Component {
|
|||
failureString += `\n\n${data.failure}`;
|
||||
}
|
||||
if (status === 403) {
|
||||
failureString += '\n\nAuthentication failed. Has your Treeherder session expired?';
|
||||
failureString +=
|
||||
'\n\nAuthentication failed. Has your Treeherder session expired?';
|
||||
}
|
||||
notify(failureString, 'danger', { sticky: true });
|
||||
}
|
||||
|
||||
toggleTooltip(key) {
|
||||
const { tooltipOpen } = this.state;
|
||||
this.setState({ tooltipOpen: { ...tooltipOpen, [key]: !tooltipOpen[key] } });
|
||||
this.setState({
|
||||
tooltipOpen: { ...tooltipOpen, [key]: !tooltipOpen[key] },
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen, toggle, suggestion, parsedLog, fullLog, reftestUrl,
|
||||
isOpen,
|
||||
toggle,
|
||||
suggestion,
|
||||
parsedLog,
|
||||
fullLog,
|
||||
reftestUrl,
|
||||
} = this.props;
|
||||
const {
|
||||
productSearch, suggestedProducts, thisFailure, isFilerSummaryVisible,
|
||||
isIntermittent, summary, searching, checkedLogLinks, tooltipOpen,
|
||||
productSearch,
|
||||
suggestedProducts,
|
||||
thisFailure,
|
||||
isFilerSummaryVisible,
|
||||
isIntermittent,
|
||||
summary,
|
||||
searching,
|
||||
checkedLogLinks,
|
||||
tooltipOpen,
|
||||
selectedProduct,
|
||||
} = this.state;
|
||||
const searchTerms = suggestion.search_terms;
|
||||
const crash = summary.match(crashRegex);
|
||||
const crashSignatures = crash ? [crash[0].split('application crashed ')[1]] : [];
|
||||
const crashSignatures = crash
|
||||
? [crash[0].split('application crashed ')[1]]
|
||||
: [];
|
||||
const unhelpfulSummaryReason = this.getUnhelpfulSummaryReason(summary);
|
||||
|
||||
return (
|
||||
|
@ -402,7 +516,9 @@ export class BugFilerClass extends React.Component {
|
|||
name="modalProductFinderSearch"
|
||||
id="modalProductFinderSearch"
|
||||
onKeyDown={this.productSearchEnter}
|
||||
onChange={evt => this.setState({ productSearch: evt.target.value })}
|
||||
onChange={evt =>
|
||||
this.setState({ productSearch: evt.target.value })
|
||||
}
|
||||
type="text"
|
||||
placeholder="Firefox"
|
||||
className="flex-fill flex-grow-1"
|
||||
|
@ -411,29 +527,42 @@ export class BugFilerClass extends React.Component {
|
|||
target="modalProductFinderSearch"
|
||||
isOpen={tooltipOpen.modalProductFinderSearch}
|
||||
toggle={() => this.toggleTooltip('modalProductFinderSearch')}
|
||||
>Manually search for a product</Tooltip>
|
||||
>
|
||||
Manually search for a product
|
||||
</Tooltip>
|
||||
<Button
|
||||
color="secondary"
|
||||
className="ml-1 btn-sm"
|
||||
type="button"
|
||||
onClick={this.findProduct}
|
||||
>Find Product</Button>
|
||||
>
|
||||
Find Product
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
{!!productSearch && searching && <div>
|
||||
<span className="fa fa-spinner fa-pulse th-spinner-lg" />Searching {productSearch}
|
||||
</div>}
|
||||
{!!productSearch && searching && (
|
||||
<div>
|
||||
<span className="fa fa-spinner fa-pulse th-spinner-lg" />
|
||||
Searching {productSearch}
|
||||
</div>
|
||||
)}
|
||||
<FormGroup tag="fieldset" className="mt-1">
|
||||
{suggestedProducts.map(product => (
|
||||
<div className="ml-4" key={`modalProductSuggestion${product}`}>
|
||||
<div
|
||||
className="ml-4"
|
||||
key={`modalProductSuggestion${product}`}
|
||||
>
|
||||
<Label check>
|
||||
<Input
|
||||
type="radio"
|
||||
value={product}
|
||||
checked={product === selectedProduct}
|
||||
onChange={evt => this.setState({ selectedProduct: evt.target.value })}
|
||||
onChange={evt =>
|
||||
this.setState({ selectedProduct: evt.target.value })
|
||||
}
|
||||
name="productGroup"
|
||||
/>{product}
|
||||
/>
|
||||
{product}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
|
@ -441,20 +570,31 @@ export class BugFilerClass extends React.Component {
|
|||
</div>
|
||||
<label>Summary:</label>
|
||||
<div className="d-flex">
|
||||
{!!unhelpfulSummaryReason && <div>
|
||||
<div className="text-danger">
|
||||
<span
|
||||
className="fa fa-warning"
|
||||
id="unhelpful-summary-reason"
|
||||
/>Warning: {unhelpfulSummaryReason}
|
||||
<Tooltip
|
||||
target="unhelpful-summary-reason"
|
||||
isOpen={tooltipOpen.unhelpfulSummaryReason}
|
||||
toggle={() => this.toggleTooltip('unhelpfulSummaryReason')}
|
||||
>This can cause poor bug suggestions to be generated</Tooltip>
|
||||
{!!unhelpfulSummaryReason && (
|
||||
<div>
|
||||
<div className="text-danger">
|
||||
<span
|
||||
className="fa fa-warning"
|
||||
id="unhelpful-summary-reason"
|
||||
/>
|
||||
Warning: {unhelpfulSummaryReason}
|
||||
<Tooltip
|
||||
target="unhelpful-summary-reason"
|
||||
isOpen={tooltipOpen.unhelpfulSummaryReason}
|
||||
toggle={() =>
|
||||
this.toggleTooltip('unhelpfulSummaryReason')
|
||||
}
|
||||
>
|
||||
This can cause poor bug suggestions to be generated
|
||||
</Tooltip>
|
||||
</div>
|
||||
{searchTerms.map(term => (
|
||||
<div className="text-monospace pl-3" key={term}>
|
||||
{term}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{searchTerms.map(term => <div className="text-monospace pl-3" key={term}>{term}</div>)}
|
||||
</div>}
|
||||
)}
|
||||
<Input
|
||||
id="summary"
|
||||
className="flex-grow-1"
|
||||
|
@ -469,27 +609,45 @@ export class BugFilerClass extends React.Component {
|
|||
isOpen={tooltipOpen.toggleFailureLines}
|
||||
toggle={() => this.toggleTooltip('toggleFailureLines')}
|
||||
>
|
||||
{isFilerSummaryVisible ? 'Hide all failure lines for this job' : 'Show all failure lines for this job'}
|
||||
{isFilerSummaryVisible
|
||||
? 'Hide all failure lines for this job'
|
||||
: 'Show all failure lines for this job'}
|
||||
</Tooltip>
|
||||
<i
|
||||
onClick={() => this.setState({ isFilerSummaryVisible: !isFilerSummaryVisible })}
|
||||
className={`fa fa-lg pointable align-bottom pt-2 ml-1 ${isFilerSummaryVisible ? 'fa-chevron-circle-up' : 'fa-chevron-circle-down'}`}
|
||||
onClick={() =>
|
||||
this.setState({
|
||||
isFilerSummaryVisible: !isFilerSummaryVisible,
|
||||
})
|
||||
}
|
||||
className={`fa fa-lg pointable align-bottom pt-2 ml-1 ${
|
||||
isFilerSummaryVisible
|
||||
? 'fa-chevron-circle-up'
|
||||
: 'fa-chevron-circle-down'
|
||||
}`}
|
||||
id="toggle-failure-lines"
|
||||
/>
|
||||
<span
|
||||
id="summaryLength"
|
||||
className={`ml-1 font-weight-bold lg ${summary.length > 255 ? 'text-danger' : 'text-success'}`}
|
||||
>{summary.length}</span>
|
||||
className={`ml-1 font-weight-bold lg ${
|
||||
summary.length > 255 ? 'text-danger' : 'text-success'
|
||||
}`}
|
||||
>
|
||||
{summary.length}
|
||||
</span>
|
||||
</div>
|
||||
{isFilerSummaryVisible && <span>
|
||||
<Input
|
||||
className="w-100"
|
||||
type="textarea"
|
||||
value={thisFailure}
|
||||
readOnly
|
||||
onChange={evt => this.setState({ thisFailure: evt.target.value })}
|
||||
/>
|
||||
</span>}
|
||||
{isFilerSummaryVisible && (
|
||||
<span>
|
||||
<Input
|
||||
className="w-100"
|
||||
type="textarea"
|
||||
value={thisFailure}
|
||||
readOnly
|
||||
onChange={evt =>
|
||||
this.setState({ thisFailure: evt.target.value })
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
<div className="ml-5 mt-2">
|
||||
<div>
|
||||
<label>
|
||||
|
@ -498,7 +656,13 @@ export class BugFilerClass extends React.Component {
|
|||
checked={checkedLogLinks.includes(parsedLog)}
|
||||
onChange={() => this.toggleCheckedLogLink(parsedLog)}
|
||||
/>
|
||||
<a target="_blank" rel="noopener noreferrer" href={parsedLog}>Include Parsed Log Link</a>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={parsedLog}
|
||||
>
|
||||
Include Parsed Log Link
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
|
@ -508,17 +672,29 @@ export class BugFilerClass extends React.Component {
|
|||
checked={checkedLogLinks.includes(fullLog)}
|
||||
onChange={() => this.toggleCheckedLogLink(fullLog)}
|
||||
/>
|
||||
<a target="_blank" rel="noopener noreferrer" href={fullLog}>Include Full Log Link</a>
|
||||
<a target="_blank" rel="noopener noreferrer" href={fullLog}>
|
||||
Include Full Log Link
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
{!!reftestUrl && <div><label>
|
||||
<Input
|
||||
type="checkbox"
|
||||
checked={checkedLogLinks.includes(reftestUrl)}
|
||||
onChange={() => this.toggleCheckedLogLink(reftestUrl)}
|
||||
/>
|
||||
<a target="_blank" rel="noopener noreferrer" href={reftestUrl}>Include Reftest Viewer Link</a>
|
||||
</label></div>}
|
||||
{!!reftestUrl && (
|
||||
<div>
|
||||
<label>
|
||||
<Input
|
||||
type="checkbox"
|
||||
checked={checkedLogLinks.includes(reftestUrl)}
|
||||
onChange={() => this.toggleCheckedLogLink(reftestUrl)}
|
||||
/>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={reftestUrl}
|
||||
>
|
||||
Include Reftest Viewer Link
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="d-flex flex-column">
|
||||
<label>Comment:</label>
|
||||
|
@ -533,17 +709,22 @@ export class BugFilerClass extends React.Component {
|
|||
<div className="mt-2">
|
||||
<label>
|
||||
<Input
|
||||
onChange={() => this.setState({ isIntermittent: !isIntermittent })}
|
||||
onChange={() =>
|
||||
this.setState({ isIntermittent: !isIntermittent })
|
||||
}
|
||||
type="checkbox"
|
||||
checked={isIntermittent}
|
||||
/>This is an intermittent failure
|
||||
/>
|
||||
This is an intermittent failure
|
||||
</label>
|
||||
</div>
|
||||
<div className="d-inline-flex ml-2">
|
||||
<Input
|
||||
id="blocksInput"
|
||||
type="text"
|
||||
onChange={evt => this.setState({ blocks: evt.target.value })}
|
||||
onChange={evt =>
|
||||
this.setState({ blocks: evt.target.value })
|
||||
}
|
||||
placeholder="Blocks"
|
||||
/>
|
||||
<Tooltip
|
||||
|
@ -551,12 +732,16 @@ export class BugFilerClass extends React.Component {
|
|||
placement="bottom"
|
||||
isOpen={tooltipOpen.blocksInput}
|
||||
toggle={() => this.toggleTooltip('blocksInput')}
|
||||
>Comma-separated list of bugs</Tooltip>
|
||||
>
|
||||
Comma-separated list of bugs
|
||||
</Tooltip>
|
||||
<Input
|
||||
id="dependsOn"
|
||||
type="text"
|
||||
className="ml-1"
|
||||
onChange={evt => this.setState({ dependsOn: evt.target.value })}
|
||||
onChange={evt =>
|
||||
this.setState({ dependsOn: evt.target.value })
|
||||
}
|
||||
placeholder="Depends on"
|
||||
/>
|
||||
<Tooltip
|
||||
|
@ -564,12 +749,16 @@ export class BugFilerClass extends React.Component {
|
|||
placement="bottom"
|
||||
isOpen={tooltipOpen.dependsOn}
|
||||
toggle={() => this.toggleTooltip('dependsOn')}
|
||||
>Comma-separated list of bugs</Tooltip>
|
||||
>
|
||||
Comma-separated list of bugs
|
||||
</Tooltip>
|
||||
<Input
|
||||
id="seeAlso"
|
||||
className="ml-1"
|
||||
type="text"
|
||||
onChange={evt => this.setState({ seeAlso: evt.target.value })}
|
||||
onChange={evt =>
|
||||
this.setState({ seeAlso: evt.target.value })
|
||||
}
|
||||
placeholder="See also"
|
||||
/>
|
||||
<Tooltip
|
||||
|
@ -577,25 +766,34 @@ export class BugFilerClass extends React.Component {
|
|||
placement="bottom"
|
||||
isOpen={tooltipOpen.seeAlso}
|
||||
toggle={() => this.toggleTooltip('seeAlso')}
|
||||
>Comma-separated list of bugs</Tooltip>
|
||||
>
|
||||
Comma-separated list of bugs
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{!!crashSignatures.length && <div>
|
||||
<label>Signature:</label>
|
||||
<Input
|
||||
type="textarea"
|
||||
onChange={evt => this.setState({ crashSignatures: evt.target.value })}
|
||||
maxLength="2048"
|
||||
readOnly
|
||||
value={crashSignatures.join('\n')}
|
||||
/>
|
||||
</div>}
|
||||
{!!crashSignatures.length && (
|
||||
<div>
|
||||
<label>Signature:</label>
|
||||
<Input
|
||||
type="textarea"
|
||||
onChange={evt =>
|
||||
this.setState({ crashSignatures: evt.target.value })
|
||||
}
|
||||
maxLength="2048"
|
||||
readOnly
|
||||
value={crashSignatures.join('\n')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={this.submitFiler}>Submit Bug</Button>{' '}
|
||||
<Button color="secondary" onClick={toggle}>Cancel</Button>
|
||||
<Button color="secondary" onClick={this.submitFiler}>
|
||||
Submit Bug
|
||||
</Button>{' '}
|
||||
<Button color="secondary" onClick={toggle}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</div>
|
||||
|
|
|
@ -50,19 +50,28 @@ class DetailsPanel extends React.Component {
|
|||
componentDidMount() {
|
||||
this.updateClassifications = this.updateClassifications.bind(this);
|
||||
|
||||
window.addEventListener(thEvents.classificationChanged, this.updateClassifications);
|
||||
window.addEventListener(
|
||||
thEvents.classificationChanged,
|
||||
this.updateClassifications,
|
||||
);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { selectedJob } = this.props;
|
||||
|
||||
if (selectedJob && (!prevProps.selectedJob || prevProps.selectedJob !== selectedJob)) {
|
||||
if (
|
||||
selectedJob &&
|
||||
(!prevProps.selectedJob || prevProps.selectedJob !== selectedJob)
|
||||
) {
|
||||
this.selectJob();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener(thEvents.classificationChanged, this.updateClassifications);
|
||||
window.removeEventListener(
|
||||
thEvents.classificationChanged,
|
||||
this.updateClassifications,
|
||||
);
|
||||
}
|
||||
|
||||
togglePinBoardVisibility() {
|
||||
|
@ -72,55 +81,57 @@ class DetailsPanel extends React.Component {
|
|||
}
|
||||
|
||||
loadBugSuggestions() {
|
||||
const { repoName, selectedJob } = this.props;
|
||||
const { repoName, selectedJob } = this.props;
|
||||
|
||||
if (!selectedJob) {
|
||||
return;
|
||||
}
|
||||
BugSuggestionsModel.get(selectedJob.id).then((suggestions) => {
|
||||
suggestions.forEach((suggestion) => {
|
||||
suggestion.bugs.too_many_open_recent = (
|
||||
suggestion.bugs.open_recent.length > thBugSuggestionLimit
|
||||
);
|
||||
suggestion.bugs.too_many_all_others = (
|
||||
suggestion.bugs.all_others.length > thBugSuggestionLimit
|
||||
);
|
||||
suggestion.valid_open_recent = (
|
||||
suggestion.bugs.open_recent.length > 0 &&
|
||||
!suggestion.bugs.too_many_open_recent
|
||||
);
|
||||
suggestion.valid_all_others = (
|
||||
suggestion.bugs.all_others.length > 0 &&
|
||||
!suggestion.bugs.too_many_all_others &&
|
||||
// If we have too many open_recent bugs, we're unlikely to have
|
||||
// relevant all_others bugs, so don't show them either.
|
||||
!suggestion.bugs.too_many_open_recent
|
||||
);
|
||||
});
|
||||
|
||||
// if we have no bug suggestions, populate with the raw errors from
|
||||
// the log (we can do this asynchronously, it should normally be
|
||||
// fast)
|
||||
if (!suggestions.length) {
|
||||
TextLogStepModel.get(selectedJob.id).then((textLogSteps) => {
|
||||
const errors = textLogSteps
|
||||
.filter(step => step.result !== 'success')
|
||||
.map(step => ({
|
||||
name: step.name,
|
||||
result: step.result,
|
||||
logViewerUrl: getLogViewerUrl(selectedJob.id, repoName, step.finished_line_number),
|
||||
}));
|
||||
this.setState({ errors });
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({ bugSuggestionsLoading: false, suggestions });
|
||||
if (!selectedJob) {
|
||||
return;
|
||||
}
|
||||
BugSuggestionsModel.get(selectedJob.id).then(suggestions => {
|
||||
suggestions.forEach(suggestion => {
|
||||
suggestion.bugs.too_many_open_recent =
|
||||
suggestion.bugs.open_recent.length > thBugSuggestionLimit;
|
||||
suggestion.bugs.too_many_all_others =
|
||||
suggestion.bugs.all_others.length > thBugSuggestionLimit;
|
||||
suggestion.valid_open_recent =
|
||||
suggestion.bugs.open_recent.length > 0 &&
|
||||
!suggestion.bugs.too_many_open_recent;
|
||||
suggestion.valid_all_others =
|
||||
suggestion.bugs.all_others.length > 0 &&
|
||||
!suggestion.bugs.too_many_all_others &&
|
||||
// If we have too many open_recent bugs, we're unlikely to have
|
||||
// relevant all_others bugs, so don't show them either.
|
||||
!suggestion.bugs.too_many_open_recent;
|
||||
});
|
||||
|
||||
// if we have no bug suggestions, populate with the raw errors from
|
||||
// the log (we can do this asynchronously, it should normally be
|
||||
// fast)
|
||||
if (!suggestions.length) {
|
||||
TextLogStepModel.get(selectedJob.id).then(textLogSteps => {
|
||||
const errors = textLogSteps
|
||||
.filter(step => step.result !== 'success')
|
||||
.map(step => ({
|
||||
name: step.name,
|
||||
result: step.result,
|
||||
logViewerUrl: getLogViewerUrl(
|
||||
selectedJob.id,
|
||||
repoName,
|
||||
step.finished_line_number,
|
||||
),
|
||||
}));
|
||||
this.setState({ errors });
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({ bugSuggestionsLoading: false, suggestions });
|
||||
});
|
||||
}
|
||||
|
||||
async updateClassifications() {
|
||||
const { selectedJob } = this.props;
|
||||
const classifications = await JobClassificationModel.getList({ job_id: selectedJob.id });
|
||||
const classifications = await JobClassificationModel.getList({
|
||||
job_id: selectedJob.id,
|
||||
});
|
||||
const bugs = await BugJobMapModel.getList({ job_id: selectedJob.id });
|
||||
|
||||
this.setState({ classifications, bugs });
|
||||
|
@ -129,115 +140,193 @@ class DetailsPanel extends React.Component {
|
|||
selectJob() {
|
||||
const { repoName, selectedJob, getPush } = this.props;
|
||||
|
||||
this.setState({ jobDetails: [], suggestions: [], jobDetailLoading: true }, () => {
|
||||
if (this.selectJobController !== null) {
|
||||
// Cancel the in-progress fetch requests.
|
||||
this.selectJobController.abort();
|
||||
}
|
||||
|
||||
this.selectJobController = new AbortController();
|
||||
|
||||
let jobDetails = [];
|
||||
const jobPromise = 'logs' in selectedJob ? Promise.resolve(selectedJob) : JobModel.get(
|
||||
repoName, selectedJob.id,
|
||||
this.selectJobController.signal);
|
||||
|
||||
const jobDetailPromise = JobDetailModel.getJobDetails(
|
||||
{ job_guid: selectedJob.job_guid },
|
||||
this.selectJobController.signal);
|
||||
|
||||
const jobLogUrlPromise = JobLogUrlModel.getList(
|
||||
{ job_id: selectedJob.id },
|
||||
this.selectJobController.signal);
|
||||
|
||||
const phSeriesPromise = PerfSeriesModel.getSeriesData(
|
||||
repoName, { job_id: selectedJob.id });
|
||||
|
||||
Promise.all([
|
||||
jobPromise,
|
||||
jobDetailPromise,
|
||||
jobLogUrlPromise,
|
||||
phSeriesPromise,
|
||||
]).then(async ([jobResult, jobDetailResult, jobLogUrlResult, phSeriesResult]) => {
|
||||
// This version of the job has more information than what we get in the main job list. This
|
||||
// is what we'll pass to the rest of the details panel. It has extra fields like
|
||||
// taskcluster_metadata.
|
||||
Object.assign(selectedJob, jobResult);
|
||||
const jobRevision = getPush(selectedJob.push_id).revision;
|
||||
|
||||
jobDetails = jobDetailResult;
|
||||
|
||||
// incorporate the buildername into the job details if this is a buildbot job
|
||||
// (i.e. it has a buildbot request id)
|
||||
const buildbotRequestIdDetail = jobDetails.find(detail => detail.title === 'buildbot_request_id');
|
||||
if (buildbotRequestIdDetail) {
|
||||
jobDetails = [...jobDetails, { title: 'Buildername', value: selectedJob.ref_data_name }];
|
||||
this.setState(
|
||||
{ jobDetails: [], suggestions: [], jobDetailLoading: true },
|
||||
() => {
|
||||
if (this.selectJobController !== null) {
|
||||
// Cancel the in-progress fetch requests.
|
||||
this.selectJobController.abort();
|
||||
}
|
||||
|
||||
// the third result comes from the jobLogUrl promise
|
||||
// exclude the json log URLs
|
||||
const jobLogUrls = jobLogUrlResult.filter(log => !log.name.endsWith('_json'));
|
||||
this.selectJobController = new AbortController();
|
||||
|
||||
let logParseStatus = 'unavailable';
|
||||
// Provide a parse status as a scope variable for logviewer shortcut
|
||||
if (jobLogUrls.length && jobLogUrls[0].parse_status) {
|
||||
logParseStatus = jobLogUrls[0].parse_status;
|
||||
}
|
||||
let jobDetails = [];
|
||||
const jobPromise =
|
||||
'logs' in selectedJob
|
||||
? Promise.resolve(selectedJob)
|
||||
: JobModel.get(
|
||||
repoName,
|
||||
selectedJob.id,
|
||||
this.selectJobController.signal,
|
||||
);
|
||||
|
||||
const logViewerUrl = getLogViewerUrl(selectedJob.id, repoName);
|
||||
const logViewerFullUrl = `${window.location.origin}/${logViewerUrl}`;
|
||||
const reftestUrl = jobLogUrls.length ? getReftestUrl(jobLogUrls[0].url) : '';
|
||||
const performanceData = Object.values(phSeriesResult).reduce((a, b) => [...a, ...b], []);
|
||||
let perfJobDetail = [];
|
||||
const jobDetailPromise = JobDetailModel.getJobDetails(
|
||||
{ job_guid: selectedJob.job_guid },
|
||||
this.selectJobController.signal,
|
||||
);
|
||||
|
||||
if (performanceData.length) {
|
||||
const signatureIds = [...new Set(performanceData.map(perf => perf.signature_id))];
|
||||
const seriesListList = await Promise.all(chunk(signatureIds, 20).map(
|
||||
signatureIdChunk => PerfSeriesModel.getSeriesList(repoName, { id: signatureIdChunk }),
|
||||
));
|
||||
const seriesList = seriesListList.reduce((a, b) => [...a, ...b], []);
|
||||
const jobLogUrlPromise = JobLogUrlModel.getList(
|
||||
{ job_id: selectedJob.id },
|
||||
this.selectJobController.signal,
|
||||
);
|
||||
|
||||
perfJobDetail = performanceData.map(d => ({
|
||||
series: seriesList.find(s => d.signature_id === s.id),
|
||||
...d,
|
||||
})).filter(d => !d.series.parentSignature).map(d => ({
|
||||
url: `/perf.html#/graphs?series=${[repoName, d.signature_id, 1, d.series.frameworkId]}&selected=${[repoName, d.signature_id, selectedJob.push_id, d.id]}`,
|
||||
value: d.value,
|
||||
title: d.series.name,
|
||||
}));
|
||||
}
|
||||
|
||||
this.setState({
|
||||
jobLogUrls,
|
||||
jobDetails,
|
||||
logParseStatus,
|
||||
logViewerUrl,
|
||||
logViewerFullUrl,
|
||||
reftestUrl,
|
||||
perfJobDetail,
|
||||
jobRevision,
|
||||
}, async () => {
|
||||
await this.updateClassifications();
|
||||
await this.loadBugSuggestions();
|
||||
this.setState({ jobDetailLoading: false });
|
||||
const phSeriesPromise = PerfSeriesModel.getSeriesData(repoName, {
|
||||
job_id: selectedJob.id,
|
||||
});
|
||||
}).finally(() => {
|
||||
this.selectJobController = null;
|
||||
});
|
||||
});
|
||||
|
||||
Promise.all([
|
||||
jobPromise,
|
||||
jobDetailPromise,
|
||||
jobLogUrlPromise,
|
||||
phSeriesPromise,
|
||||
])
|
||||
.then(
|
||||
async ([
|
||||
jobResult,
|
||||
jobDetailResult,
|
||||
jobLogUrlResult,
|
||||
phSeriesResult,
|
||||
]) => {
|
||||
// This version of the job has more information than what we get in the main job list. This
|
||||
// is what we'll pass to the rest of the details panel. It has extra fields like
|
||||
// taskcluster_metadata.
|
||||
Object.assign(selectedJob, jobResult);
|
||||
const jobRevision = getPush(selectedJob.push_id).revision;
|
||||
|
||||
jobDetails = jobDetailResult;
|
||||
|
||||
// incorporate the buildername into the job details if this is a buildbot job
|
||||
// (i.e. it has a buildbot request id)
|
||||
const buildbotRequestIdDetail = jobDetails.find(
|
||||
detail => detail.title === 'buildbot_request_id',
|
||||
);
|
||||
if (buildbotRequestIdDetail) {
|
||||
jobDetails = [
|
||||
...jobDetails,
|
||||
{ title: 'Buildername', value: selectedJob.ref_data_name },
|
||||
];
|
||||
}
|
||||
|
||||
// the third result comes from the jobLogUrl promise
|
||||
// exclude the json log URLs
|
||||
const jobLogUrls = jobLogUrlResult.filter(
|
||||
log => !log.name.endsWith('_json'),
|
||||
);
|
||||
|
||||
let logParseStatus = 'unavailable';
|
||||
// Provide a parse status as a scope variable for logviewer shortcut
|
||||
if (jobLogUrls.length && jobLogUrls[0].parse_status) {
|
||||
logParseStatus = jobLogUrls[0].parse_status;
|
||||
}
|
||||
|
||||
const logViewerUrl = getLogViewerUrl(selectedJob.id, repoName);
|
||||
const logViewerFullUrl = `${
|
||||
window.location.origin
|
||||
}/${logViewerUrl}`;
|
||||
const reftestUrl = jobLogUrls.length
|
||||
? getReftestUrl(jobLogUrls[0].url)
|
||||
: '';
|
||||
const performanceData = Object.values(phSeriesResult).reduce(
|
||||
(a, b) => [...a, ...b],
|
||||
[],
|
||||
);
|
||||
let perfJobDetail = [];
|
||||
|
||||
if (performanceData.length) {
|
||||
const signatureIds = [
|
||||
...new Set(performanceData.map(perf => perf.signature_id)),
|
||||
];
|
||||
const seriesListList = await Promise.all(
|
||||
chunk(signatureIds, 20).map(signatureIdChunk =>
|
||||
PerfSeriesModel.getSeriesList(repoName, {
|
||||
id: signatureIdChunk,
|
||||
}),
|
||||
),
|
||||
);
|
||||
const seriesList = seriesListList.reduce(
|
||||
(a, b) => [...a, ...b],
|
||||
[],
|
||||
);
|
||||
|
||||
perfJobDetail = performanceData
|
||||
.map(d => ({
|
||||
series: seriesList.find(s => d.signature_id === s.id),
|
||||
...d,
|
||||
}))
|
||||
.filter(d => !d.series.parentSignature)
|
||||
.map(d => ({
|
||||
url: `/perf.html#/graphs?series=${[
|
||||
repoName,
|
||||
d.signature_id,
|
||||
1,
|
||||
d.series.frameworkId,
|
||||
]}&selected=${[
|
||||
repoName,
|
||||
d.signature_id,
|
||||
selectedJob.push_id,
|
||||
d.id,
|
||||
]}`,
|
||||
value: d.value,
|
||||
title: d.series.name,
|
||||
}));
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
jobLogUrls,
|
||||
jobDetails,
|
||||
logParseStatus,
|
||||
logViewerUrl,
|
||||
logViewerFullUrl,
|
||||
reftestUrl,
|
||||
perfJobDetail,
|
||||
jobRevision,
|
||||
},
|
||||
async () => {
|
||||
await this.updateClassifications();
|
||||
await this.loadBugSuggestions();
|
||||
this.setState({ jobDetailLoading: false });
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
.finally(() => {
|
||||
this.selectJobController = null;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
repoName, user, currentRepo, resizedHeight, classificationMap,
|
||||
classificationTypes, isPinBoardVisible, selectedJob,
|
||||
repoName,
|
||||
user,
|
||||
currentRepo,
|
||||
resizedHeight,
|
||||
classificationMap,
|
||||
classificationTypes,
|
||||
isPinBoardVisible,
|
||||
selectedJob,
|
||||
} = this.props;
|
||||
const {
|
||||
jobDetails, jobRevision, jobLogUrls, jobDetailLoading,
|
||||
perfJobDetail, suggestions, errors, bugSuggestionsLoading, logParseStatus,
|
||||
classifications, logViewerUrl, logViewerFullUrl, bugs, reftestUrl,
|
||||
jobDetails,
|
||||
jobRevision,
|
||||
jobLogUrls,
|
||||
jobDetailLoading,
|
||||
perfJobDetail,
|
||||
suggestions,
|
||||
errors,
|
||||
bugSuggestionsLoading,
|
||||
logParseStatus,
|
||||
classifications,
|
||||
logViewerUrl,
|
||||
logViewerFullUrl,
|
||||
bugs,
|
||||
reftestUrl,
|
||||
} = this.state;
|
||||
const detailsPanelHeight = isPinBoardVisible ? resizedHeight - pinboardHeight : resizedHeight;
|
||||
const detailsPanelHeight = isPinBoardVisible
|
||||
? resizedHeight - pinboardHeight
|
||||
: resizedHeight;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -251,41 +340,47 @@ class DetailsPanel extends React.Component {
|
|||
isLoggedIn={user.isLoggedIn || false}
|
||||
classificationTypes={classificationTypes}
|
||||
/>
|
||||
{!!selectedJob && <div id="details-panel-content">
|
||||
<SummaryPanel
|
||||
repoName={repoName}
|
||||
currentRepo={currentRepo}
|
||||
classificationMap={classificationMap}
|
||||
jobLogUrls={jobLogUrls}
|
||||
logParseStatus={logParseStatus}
|
||||
jobDetailLoading={jobDetailLoading}
|
||||
latestClassification={classifications.length ? classifications[0] : null}
|
||||
logViewerUrl={logViewerUrl}
|
||||
logViewerFullUrl={logViewerFullUrl}
|
||||
bugs={bugs}
|
||||
user={user}
|
||||
/>
|
||||
<span className="job-tabs-divider" />
|
||||
<TabsPanel
|
||||
jobDetails={jobDetails}
|
||||
perfJobDetail={perfJobDetail}
|
||||
repoName={repoName}
|
||||
jobRevision={jobRevision}
|
||||
suggestions={suggestions}
|
||||
errors={errors}
|
||||
bugSuggestionsLoading={bugSuggestionsLoading}
|
||||
logParseStatus={logParseStatus}
|
||||
classifications={classifications}
|
||||
classificationMap={classificationMap}
|
||||
jobLogUrls={jobLogUrls}
|
||||
bugs={bugs}
|
||||
togglePinBoardVisibility={() => this.togglePinBoardVisibility()}
|
||||
logViewerFullUrl={logViewerFullUrl}
|
||||
reftestUrl={reftestUrl}
|
||||
user={user}
|
||||
/>
|
||||
</div>}
|
||||
<div id="clipboard-container"><textarea id="clipboard" /></div>
|
||||
{!!selectedJob && (
|
||||
<div id="details-panel-content">
|
||||
<SummaryPanel
|
||||
repoName={repoName}
|
||||
currentRepo={currentRepo}
|
||||
classificationMap={classificationMap}
|
||||
jobLogUrls={jobLogUrls}
|
||||
logParseStatus={logParseStatus}
|
||||
jobDetailLoading={jobDetailLoading}
|
||||
latestClassification={
|
||||
classifications.length ? classifications[0] : null
|
||||
}
|
||||
logViewerUrl={logViewerUrl}
|
||||
logViewerFullUrl={logViewerFullUrl}
|
||||
bugs={bugs}
|
||||
user={user}
|
||||
/>
|
||||
<span className="job-tabs-divider" />
|
||||
<TabsPanel
|
||||
jobDetails={jobDetails}
|
||||
perfJobDetail={perfJobDetail}
|
||||
repoName={repoName}
|
||||
jobRevision={jobRevision}
|
||||
suggestions={suggestions}
|
||||
errors={errors}
|
||||
bugSuggestionsLoading={bugSuggestionsLoading}
|
||||
logParseStatus={logParseStatus}
|
||||
classifications={classifications}
|
||||
classificationMap={classificationMap}
|
||||
jobLogUrls={jobLogUrls}
|
||||
bugs={bugs}
|
||||
togglePinBoardVisibility={() => this.togglePinBoardVisibility()}
|
||||
logViewerFullUrl={logViewerFullUrl}
|
||||
reftestUrl={reftestUrl}
|
||||
user={user}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div id="clipboard-container">
|
||||
<textarea id="clipboard" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -30,7 +30,9 @@ class PinBoard extends React.Component {
|
|||
componentDidMount() {
|
||||
this.bugNumberKeyPress = this.bugNumberKeyPress.bind(this);
|
||||
this.save = this.save.bind(this);
|
||||
this.handleRelatedBugDocumentClick = this.handleRelatedBugDocumentClick.bind(this);
|
||||
this.handleRelatedBugDocumentClick = this.handleRelatedBugDocumentClick.bind(
|
||||
this,
|
||||
);
|
||||
this.handleRelatedBugEscape = this.handleRelatedBugEscape.bind(this);
|
||||
this.unPinAll = this.unPinAll.bind(this);
|
||||
this.retriggerAllPinnedJobs = this.retriggerAllPinnedJobs.bind(this);
|
||||
|
@ -62,7 +64,12 @@ class PinBoard extends React.Component {
|
|||
}
|
||||
|
||||
save() {
|
||||
const { isLoggedIn, pinnedJobs, recalculateUnclassifiedCounts, notify } = this.props;
|
||||
const {
|
||||
isLoggedIn,
|
||||
pinnedJobs,
|
||||
recalculateUnclassifiedCounts,
|
||||
notify,
|
||||
} = this.props;
|
||||
|
||||
let errorFree = true;
|
||||
if (this.state.enteringBugNumber) {
|
||||
|
@ -107,7 +114,10 @@ class PinBoard extends React.Component {
|
|||
|
||||
createNewClassification() {
|
||||
const { email } = this.props;
|
||||
const { failureClassificationId, failureClassificationComment } = this.state;
|
||||
const {
|
||||
failureClassificationId,
|
||||
failureClassificationComment,
|
||||
} = this.state;
|
||||
|
||||
return new JobClassificationModel({
|
||||
text: failureClassificationComment,
|
||||
|
@ -127,10 +137,18 @@ class PinBoard extends React.Component {
|
|||
recalculateUnclassifiedCounts();
|
||||
|
||||
classification.job_id = job.id;
|
||||
return classification.create().then(() => {
|
||||
notify(`Classification saved for ${job.platform} ${job.job_type_name}`, 'success');
|
||||
}).catch((response) => {
|
||||
const message = `Error saving classification for ${job.platform} ${job.job_type_name}`;
|
||||
return classification
|
||||
.create()
|
||||
.then(() => {
|
||||
notify(
|
||||
`Classification saved for ${job.platform} ${job.job_type_name}`,
|
||||
'success',
|
||||
);
|
||||
})
|
||||
.catch(response => {
|
||||
const message = `Error saving classification for ${job.platform} ${
|
||||
job.job_type_name
|
||||
}`;
|
||||
notify(formatModelError(response, message), 'danger');
|
||||
});
|
||||
}
|
||||
|
@ -139,21 +157,27 @@ class PinBoard extends React.Component {
|
|||
saveBugs(job) {
|
||||
const { pinnedJobBugs, notify } = this.props;
|
||||
|
||||
Object.values(pinnedJobBugs).forEach((bug) => {
|
||||
Object.values(pinnedJobBugs).forEach(bug => {
|
||||
const bjm = new BugJobMapModel({
|
||||
bug_id: bug.id,
|
||||
job_id: job.id,
|
||||
type: 'annotation',
|
||||
});
|
||||
|
||||
bjm.create()
|
||||
bjm
|
||||
.create()
|
||||
.then(() => {
|
||||
notify(`Bug association saved for ${job.platform} ${job.job_type_name}`, 'success');
|
||||
notify(
|
||||
`Bug association saved for ${job.platform} ${job.job_type_name}`,
|
||||
'success',
|
||||
);
|
||||
})
|
||||
.catch((response) => {
|
||||
const message = `Error saving bug association for ${job.platform} ${job.job_type_name}`;
|
||||
.catch(response => {
|
||||
const message = `Error saving bug association for ${job.platform} ${
|
||||
job.job_type_name
|
||||
}`;
|
||||
notify(formatModelError(response, message), 'danger');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -182,7 +206,8 @@ class PinBoard extends React.Component {
|
|||
|
||||
canCancelAllPinnedJobs() {
|
||||
const cancellableJobs = Object.values(this.props.pinnedJobs).filter(
|
||||
job => (job.state === 'pending' || job.state === 'running'));
|
||||
job => job.state === 'pending' || job.state === 'running',
|
||||
);
|
||||
|
||||
return this.props.isLoggedIn && cancellableJobs.length > 0;
|
||||
}
|
||||
|
@ -190,7 +215,9 @@ class PinBoard extends React.Component {
|
|||
cancelAllPinnedJobs() {
|
||||
const { getGeckoDecisionTaskId, notify, repoName } = this.props;
|
||||
|
||||
if (window.confirm('This will cancel all the selected jobs. Are you sure?')) {
|
||||
if (
|
||||
window.confirm('This will cancel all the selected jobs. Are you sure?')
|
||||
) {
|
||||
const jobIds = Object.keys(this.props.pinnedJobs);
|
||||
|
||||
JobModel.cancel(jobIds, repoName, getGeckoDecisionTaskId, notify);
|
||||
|
@ -200,24 +227,37 @@ class PinBoard extends React.Component {
|
|||
|
||||
canSaveClassifications() {
|
||||
const { pinnedJobBugs, isLoggedIn, currentRepo } = this.props;
|
||||
const { failureClassificationId, failureClassificationComment } = this.state;
|
||||
const {
|
||||
failureClassificationId,
|
||||
failureClassificationComment,
|
||||
} = this.state;
|
||||
|
||||
return this.hasPinnedJobs() && isLoggedIn &&
|
||||
return (
|
||||
this.hasPinnedJobs() &&
|
||||
isLoggedIn &&
|
||||
(!!Object.keys(pinnedJobBugs).length ||
|
||||
(failureClassificationId !== 4 && failureClassificationId !== 2) ||
|
||||
currentRepo.is_try_repo ||
|
||||
currentRepo.repository_group.name === 'project repositories' ||
|
||||
(failureClassificationId === 4 && failureClassificationComment.length > 0) ||
|
||||
(failureClassificationId === 2 && failureClassificationComment.length > 7));
|
||||
(failureClassificationId === 4 &&
|
||||
failureClassificationComment.length > 0) ||
|
||||
(failureClassificationId === 2 &&
|
||||
failureClassificationComment.length > 7))
|
||||
);
|
||||
}
|
||||
|
||||
// Facilitates Clear all if no jobs pinned to reset pinBoard UI
|
||||
pinboardIsDirty() {
|
||||
const { failureClassificationId, failureClassificationComment } = this.state;
|
||||
const {
|
||||
failureClassificationId,
|
||||
failureClassificationComment,
|
||||
} = this.state;
|
||||
|
||||
return failureClassificationComment !== '' ||
|
||||
return (
|
||||
failureClassificationComment !== '' ||
|
||||
!!Object.keys(this.props.pinnedJobBugs).length ||
|
||||
failureClassificationId !== 4;
|
||||
failureClassificationId !== 4
|
||||
);
|
||||
}
|
||||
|
||||
// Dynamic btn/anchor title for classification save
|
||||
|
@ -274,23 +314,32 @@ class PinBoard extends React.Component {
|
|||
}
|
||||
|
||||
toggleEnterBugNumber(tf) {
|
||||
this.setState({
|
||||
enteringBugNumber: tf,
|
||||
}, () => {
|
||||
if (tf) {
|
||||
document.getElementById('related-bug-input').focus();
|
||||
// Bind escape to canceling the bug entry.
|
||||
document.addEventListener('keydown', this.handleRelatedBugEscape);
|
||||
// Install a click handler on the document so that clicking
|
||||
// outside of the input field will close it. A blur handler
|
||||
// can't be used because it would have timing issues with the
|
||||
// click handler on the + icon.
|
||||
document.addEventListener('click', this.handleRelatedBugDocumentClick);
|
||||
} else {
|
||||
document.removeEventListener('keydown', this.handleRelatedBugEscape);
|
||||
document.removeEventListener('click', this.handleRelatedBugDocumentClick);
|
||||
}
|
||||
});
|
||||
this.setState(
|
||||
{
|
||||
enteringBugNumber: tf,
|
||||
},
|
||||
() => {
|
||||
if (tf) {
|
||||
document.getElementById('related-bug-input').focus();
|
||||
// Bind escape to canceling the bug entry.
|
||||
document.addEventListener('keydown', this.handleRelatedBugEscape);
|
||||
// Install a click handler on the document so that clicking
|
||||
// outside of the input field will close it. A blur handler
|
||||
// can't be used because it would have timing issues with the
|
||||
// click handler on the + icon.
|
||||
document.addEventListener(
|
||||
'click',
|
||||
this.handleRelatedBugDocumentClick,
|
||||
);
|
||||
} else {
|
||||
document.removeEventListener('keydown', this.handleRelatedBugEscape);
|
||||
document.removeEventListener(
|
||||
'click',
|
||||
this.handleRelatedBugDocumentClick,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
isNumber(text) {
|
||||
|
@ -335,39 +384,60 @@ class PinBoard extends React.Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
selectedJob, revisionTips, isLoggedIn, isPinBoardVisible, classificationTypes,
|
||||
pinnedJobs, pinnedJobBugs, removeBug, unPinJob, setSelectedJob,
|
||||
selectedJob,
|
||||
revisionTips,
|
||||
isLoggedIn,
|
||||
isPinBoardVisible,
|
||||
classificationTypes,
|
||||
pinnedJobs,
|
||||
pinnedJobBugs,
|
||||
removeBug,
|
||||
unPinJob,
|
||||
setSelectedJob,
|
||||
} = this.props;
|
||||
const {
|
||||
failureClassificationId, failureClassificationComment,
|
||||
enteringBugNumber, newBugNumber,
|
||||
failureClassificationId,
|
||||
failureClassificationComment,
|
||||
enteringBugNumber,
|
||||
newBugNumber,
|
||||
} = this.state;
|
||||
const selectedJobId = selectedJob ? selectedJob.id : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
id="pinboard-panel"
|
||||
className={isPinBoardVisible ? '' : 'hidden'}
|
||||
>
|
||||
<div id="pinboard-panel" className={isPinBoardVisible ? '' : 'hidden'}>
|
||||
<div id="pinboard-contents">
|
||||
<div id="pinned-job-list">
|
||||
<div className="content">
|
||||
{!this.hasPinnedJobs() && <span
|
||||
className="pinboard-preload-txt"
|
||||
>press spacebar to pin a selected job</span>}
|
||||
{!this.hasPinnedJobs() && (
|
||||
<span className="pinboard-preload-txt">
|
||||
press spacebar to pin a selected job
|
||||
</span>
|
||||
)}
|
||||
{Object.values(pinnedJobs).map(job => (
|
||||
<span className="btn-group" key={job.id}>
|
||||
<span
|
||||
className={`btn pinned-job ${getJobBtnClass(job)} ${selectedJobId === job.id ? 'btn-lg selected-job' : 'btn-xs'}`}
|
||||
className={`btn pinned-job ${getJobBtnClass(job)} ${
|
||||
selectedJobId === job.id
|
||||
? 'btn-lg selected-job'
|
||||
: 'btn-xs'
|
||||
}`}
|
||||
title={getHoverText(job)}
|
||||
onClick={() => setSelectedJob(job)}
|
||||
data-job-id={job.job_id}
|
||||
>{job.job_type_symbol}</span>
|
||||
>
|
||||
{job.job_type_symbol}
|
||||
</span>
|
||||
<span
|
||||
className={`btn btn-ltgray pinned-job-close-btn ${selectedJobId === job.id ? 'btn-lg selected-job' : 'btn-xs'}`}
|
||||
className={`btn btn-ltgray pinned-job-close-btn ${
|
||||
selectedJobId === job.id
|
||||
? 'btn-lg selected-job'
|
||||
: 'btn-xs'
|
||||
}`}
|
||||
onClick={() => unPinJob(job)}
|
||||
title="un-pin this job"
|
||||
><i className="fa fa-times" /></span>
|
||||
>
|
||||
<i className="fa fa-times" />
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
@ -381,45 +451,59 @@ class PinBoard extends React.Component {
|
|||
onClick={() => this.toggleEnterBugNumber(!enteringBugNumber)}
|
||||
className="pointable"
|
||||
title="Add a related bug"
|
||||
><i className="fa fa-plus-square add-related-bugs-icon" /></span>
|
||||
{!this.hasPinnedJobBugs() && <span
|
||||
className="pinboard-preload-txt pinboard-related-bug-preload-txt"
|
||||
onClick={() => {
|
||||
this.toggleEnterBugNumber(!enteringBugNumber);
|
||||
}}
|
||||
>click to add a related bug</span>}
|
||||
{enteringBugNumber && <span
|
||||
className="add-related-bugs-form"
|
||||
>
|
||||
<Input
|
||||
id="related-bug-input"
|
||||
data-bug-input
|
||||
type="text"
|
||||
pattern="[0-9]*"
|
||||
className="add-related-bugs-input"
|
||||
placeholder="enter bug number"
|
||||
invalid={!this.isNumber(newBugNumber)}
|
||||
onKeyPress={this.bugNumberKeyPress}
|
||||
onChange={ev => this.setState({ newBugNumber: ev.target.value })}
|
||||
/>
|
||||
<FormFeedback>Please enter only numbers</FormFeedback>
|
||||
</span>}
|
||||
{Object.values(pinnedJobBugs).map(bug => (<span key={bug.id}>
|
||||
<span className="btn-group pinboard-related-bugs-btn">
|
||||
<a
|
||||
className="btn btn-xs related-bugs-link"
|
||||
title={bug.summary}
|
||||
href={getBugUrl(bug.id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
><em>{bug.id}</em></a>
|
||||
<span
|
||||
className="btn btn-ltgray btn-xs pinned-job-close-btn"
|
||||
onClick={() => removeBug(bug.id)}
|
||||
title="remove this bug"
|
||||
><i className="fa fa-times" /></span>
|
||||
<i className="fa fa-plus-square add-related-bugs-icon" />
|
||||
</span>
|
||||
{!this.hasPinnedJobBugs() && (
|
||||
<span
|
||||
className="pinboard-preload-txt pinboard-related-bug-preload-txt"
|
||||
onClick={() => {
|
||||
this.toggleEnterBugNumber(!enteringBugNumber);
|
||||
}}
|
||||
>
|
||||
click to add a related bug
|
||||
</span>
|
||||
</span>))}
|
||||
)}
|
||||
{enteringBugNumber && (
|
||||
<span className="add-related-bugs-form">
|
||||
<Input
|
||||
id="related-bug-input"
|
||||
data-bug-input
|
||||
type="text"
|
||||
pattern="[0-9]*"
|
||||
className="add-related-bugs-input"
|
||||
placeholder="enter bug number"
|
||||
invalid={!this.isNumber(newBugNumber)}
|
||||
onKeyPress={this.bugNumberKeyPress}
|
||||
onChange={ev =>
|
||||
this.setState({ newBugNumber: ev.target.value })
|
||||
}
|
||||
/>
|
||||
<FormFeedback>Please enter only numbers</FormFeedback>
|
||||
</span>
|
||||
)}
|
||||
{Object.values(pinnedJobBugs).map(bug => (
|
||||
<span key={bug.id}>
|
||||
<span className="btn-group pinboard-related-bugs-btn">
|
||||
<a
|
||||
className="btn btn-xs related-bugs-link"
|
||||
title={bug.summary}
|
||||
href={getBugUrl(bug.id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<em>{bug.id}</em>
|
||||
</a>
|
||||
<span
|
||||
className="btn btn-ltgray btn-xs pinned-job-close-btn"
|
||||
onClick={() => removeBug(bug.id)}
|
||||
title="remove this bug"
|
||||
>
|
||||
<i className="fa fa-times" />
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -437,7 +521,9 @@ class PinBoard extends React.Component {
|
|||
onChange={evt => this.setClassificationId(evt)}
|
||||
>
|
||||
{classificationTypes.map(opt => (
|
||||
<option value={opt.id} key={opt.id}>{opt.name}</option>
|
||||
<option value={opt.id} key={opt.id}>
|
||||
{opt.name}
|
||||
</option>
|
||||
))}
|
||||
</Input>
|
||||
</FormGroup>
|
||||
|
@ -452,26 +538,32 @@ class PinBoard extends React.Component {
|
|||
placeholder="click to add comment"
|
||||
value={failureClassificationComment}
|
||||
/>
|
||||
{failureClassificationId === 2 && <div>
|
||||
<FormGroup>
|
||||
<Input
|
||||
id="pinboard-revision-select"
|
||||
className="classification-select"
|
||||
type="select"
|
||||
defaultValue={0}
|
||||
onChange={evt => this.setClassificationText(evt)}
|
||||
>
|
||||
<option value="0" disabled>Choose a recent
|
||||
commit
|
||||
</option>
|
||||
{revisionTips.slice(0, 20).map(tip => (<option
|
||||
title={tip.title}
|
||||
value={tip.revision}
|
||||
key={tip.revision}
|
||||
>{tip.revision.slice(0, 12)} {tip.author}</option>))}
|
||||
</Input>
|
||||
</FormGroup>
|
||||
</div>}
|
||||
{failureClassificationId === 2 && (
|
||||
<div>
|
||||
<FormGroup>
|
||||
<Input
|
||||
id="pinboard-revision-select"
|
||||
className="classification-select"
|
||||
type="select"
|
||||
defaultValue={0}
|
||||
onChange={evt => this.setClassificationText(evt)}
|
||||
>
|
||||
<option value="0" disabled>
|
||||
Choose a recent commit
|
||||
</option>
|
||||
{revisionTips.slice(0, 20).map(tip => (
|
||||
<option
|
||||
title={tip.title}
|
||||
value={tip.revision}
|
||||
key={tip.revision}
|
||||
>
|
||||
{tip.revision.slice(0, 12)} {tip.author}
|
||||
</option>
|
||||
))}
|
||||
</Input>
|
||||
</FormGroup>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -484,14 +576,27 @@ class PinBoard extends React.Component {
|
|||
>
|
||||
<div className="btn-group save-btn-group dropdown">
|
||||
<button
|
||||
className={`btn btn-light-bordered btn-xs save-btn ${!isLoggedIn || !this.canSaveClassifications() ? 'disabled' : ''}`}
|
||||
className={`btn btn-light-bordered btn-xs save-btn ${
|
||||
!isLoggedIn || !this.canSaveClassifications()
|
||||
? 'disabled'
|
||||
: ''
|
||||
}`}
|
||||
title={this.saveUITitle('classification')}
|
||||
onClick={this.save}
|
||||
>save
|
||||
>
|
||||
save
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-light-bordered btn-xs dropdown-toggle save-btn-dropdown ${!this.hasPinnedJobs() && !this.pinboardIsDirty() ? 'disabled' : ''}`}
|
||||
title={!this.hasPinnedJobs() && !this.pinboardIsDirty() ? 'No pinned jobs' : 'Additional pinboard functions'}
|
||||
className={`btn btn-light-bordered btn-xs dropdown-toggle save-btn-dropdown ${
|
||||
!this.hasPinnedJobs() && !this.pinboardIsDirty()
|
||||
? 'disabled'
|
||||
: ''
|
||||
}`}
|
||||
title={
|
||||
!this.hasPinnedJobs() && !this.pinboardIsDirty()
|
||||
? 'No pinned jobs'
|
||||
: 'Additional pinboard functions'
|
||||
}
|
||||
type="button"
|
||||
data-toggle="dropdown"
|
||||
>
|
||||
|
@ -500,23 +605,36 @@ class PinBoard extends React.Component {
|
|||
<ul className="dropdown-menu save-btn-dropdown-menu">
|
||||
<li
|
||||
className={!isLoggedIn ? 'disabled' : ''}
|
||||
title={!isLoggedIn ? 'Not logged in' : 'Repeat the pinned jobs'}
|
||||
title={
|
||||
!isLoggedIn ? 'Not logged in' : 'Repeat the pinned jobs'
|
||||
}
|
||||
>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
onClick={() => !isLoggedIn || this.retriggerAllPinnedJobs()}
|
||||
>Retrigger all</a></li>
|
||||
>
|
||||
Retrigger all
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className={this.canCancelAllPinnedJobs() ? '' : 'disabled'}
|
||||
title={this.cancelAllPinnedJobsTitle()}
|
||||
>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
onClick={() => this.canCancelAllPinnedJobs() && this.cancelAllPinnedJobs()}
|
||||
>Cancel all</a>
|
||||
onClick={() =>
|
||||
this.canCancelAllPinnedJobs() &&
|
||||
this.cancelAllPinnedJobs()
|
||||
}
|
||||
>
|
||||
Cancel all
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a className="dropdown-item" onClick={() => this.unPinAll()}>
|
||||
Clear all
|
||||
</a>
|
||||
</li>
|
||||
<li><a className="dropdown-item" onClick={() => this.unPinAll()}>Clear
|
||||
all</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -553,4 +671,6 @@ PinBoard.defaultProps = {
|
|||
revisionTips: [],
|
||||
};
|
||||
|
||||
export default withNotifications(withPushes(withSelectedJob(withPinnedJobs(PinBoard))));
|
||||
export default withNotifications(
|
||||
withPushes(withSelectedJob(withPinnedJobs(PinBoard))),
|
||||
);
|
||||
|
|
|
@ -52,11 +52,14 @@ class ActionBar extends React.PureComponent {
|
|||
|
||||
switch (logParseStatus) {
|
||||
case 'pending':
|
||||
notify('Log parsing in progress, log viewer not yet available', 'info'); break;
|
||||
notify('Log parsing in progress, log viewer not yet available', 'info');
|
||||
break;
|
||||
case 'failed':
|
||||
notify('Log parsing has failed, log viewer is unavailable', 'warning'); break;
|
||||
notify('Log parsing has failed, log viewer is unavailable', 'warning');
|
||||
break;
|
||||
case 'unavailable':
|
||||
notify('No logs available for this job', 'info'); break;
|
||||
notify('No logs available for this job', 'info');
|
||||
break;
|
||||
case 'parsed':
|
||||
$('.logviewer-btn')[0].click();
|
||||
}
|
||||
|
@ -73,13 +76,19 @@ class ActionBar extends React.PureComponent {
|
|||
return notify('Must be logged in to create a gecko profile', 'danger');
|
||||
}
|
||||
|
||||
getGeckoDecisionTaskId(
|
||||
selectedJob.push_id).then(decisionTaskId => (
|
||||
TaskclusterModel.load(decisionTaskId, selectedJob).then((results) => {
|
||||
const geckoprofile = results.actions.find(result => result.name === 'geckoprofile');
|
||||
getGeckoDecisionTaskId(selectedJob.push_id).then(decisionTaskId =>
|
||||
TaskclusterModel.load(decisionTaskId, selectedJob).then(results => {
|
||||
const geckoprofile = results.actions.find(
|
||||
result => result.name === 'geckoprofile',
|
||||
);
|
||||
|
||||
if (geckoprofile === undefined || !Object.prototype.hasOwnProperty.call(geckoprofile, 'kind')) {
|
||||
return notify('Job was scheduled without taskcluster support for GeckoProfiles');
|
||||
if (
|
||||
geckoprofile === undefined ||
|
||||
!Object.prototype.hasOwnProperty.call(geckoprofile, 'kind')
|
||||
) {
|
||||
return notify(
|
||||
'Job was scheduled without taskcluster support for GeckoProfiles',
|
||||
);
|
||||
}
|
||||
|
||||
TaskclusterModel.submit({
|
||||
|
@ -89,20 +98,21 @@ class ActionBar extends React.PureComponent {
|
|||
task: results.originalTask,
|
||||
input: {},
|
||||
staticActionVariables: results.staticActionVariables,
|
||||
}).then(() => {
|
||||
notify(
|
||||
'Request sent to collect gecko profile job via actions.json',
|
||||
'success');
|
||||
}, (e) => {
|
||||
// The full message is too large to fit in a Treeherder
|
||||
// notification box.
|
||||
notify(
|
||||
formatTaskclusterError(e),
|
||||
'danger',
|
||||
{ sticky: true });
|
||||
});
|
||||
})
|
||||
));
|
||||
}).then(
|
||||
() => {
|
||||
notify(
|
||||
'Request sent to collect gecko profile job via actions.json',
|
||||
'success',
|
||||
);
|
||||
},
|
||||
e => {
|
||||
// The full message is too large to fit in a Treeherder
|
||||
// notification box.
|
||||
notify(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
retriggerJob(jobs) {
|
||||
|
@ -143,11 +153,15 @@ class ActionBar extends React.PureComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
if (selectedJob.build_system_type === 'taskcluster' || selectedJob.reason.startsWith('Created by BBB for task')) {
|
||||
getGeckoDecisionTaskId(
|
||||
selectedJob.push_id).then(decisionTaskId => (
|
||||
TaskclusterModel.load(decisionTaskId, selectedJob).then((results) => {
|
||||
const backfilltask = results.actions.find(result => result.name === 'backfill');
|
||||
if (
|
||||
selectedJob.build_system_type === 'taskcluster' ||
|
||||
selectedJob.reason.startsWith('Created by BBB for task')
|
||||
) {
|
||||
getGeckoDecisionTaskId(selectedJob.push_id).then(decisionTaskId =>
|
||||
TaskclusterModel.load(decisionTaskId, selectedJob).then(results => {
|
||||
const backfilltask = results.actions.find(
|
||||
result => result.name === 'backfill',
|
||||
);
|
||||
|
||||
return TaskclusterModel.submit({
|
||||
action: backfilltask,
|
||||
|
@ -155,15 +169,21 @@ class ActionBar extends React.PureComponent {
|
|||
taskId: results.originalTaskId,
|
||||
input: {},
|
||||
staticActionVariables: results.staticActionVariables,
|
||||
}).then(() => {
|
||||
notify('Request sent to backfill job via actions.json', 'success');
|
||||
}, (e) => {
|
||||
// The full message is too large to fit in a Treeherder
|
||||
// notification box.
|
||||
notify(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
});
|
||||
})
|
||||
));
|
||||
}).then(
|
||||
() => {
|
||||
notify(
|
||||
'Request sent to backfill job via actions.json',
|
||||
'success',
|
||||
);
|
||||
},
|
||||
e => {
|
||||
// The full message is too large to fit in a Treeherder
|
||||
// notification box.
|
||||
notify(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
notify('Unable to backfill this job type!', 'danger', { sticky: true });
|
||||
}
|
||||
|
@ -189,7 +209,8 @@ class ActionBar extends React.PureComponent {
|
|||
}
|
||||
|
||||
if (title === '') {
|
||||
title = 'Trigger jobs of ths type on prior pushes ' +
|
||||
title =
|
||||
'Trigger jobs of ths type on prior pushes ' +
|
||||
'to fill in gaps where the job was not run';
|
||||
} else {
|
||||
// Cut off trailing '/ ' if one exists, capitalize first letter
|
||||
|
@ -200,17 +221,28 @@ class ActionBar extends React.PureComponent {
|
|||
}
|
||||
|
||||
async createInteractiveTask() {
|
||||
const { user, selectedJob, repoName, getGeckoDecisionTaskId, notify } = this.props;
|
||||
const {
|
||||
user,
|
||||
selectedJob,
|
||||
repoName,
|
||||
getGeckoDecisionTaskId,
|
||||
notify,
|
||||
} = this.props;
|
||||
const jobId = selectedJob.id;
|
||||
|
||||
if (!user.isLoggedIn) {
|
||||
return notify('Must be logged in to create an interactive task', 'danger');
|
||||
return notify(
|
||||
'Must be logged in to create an interactive task',
|
||||
'danger',
|
||||
);
|
||||
}
|
||||
|
||||
const job = await JobModel.get(repoName, jobId);
|
||||
const decisionTaskId = await getGeckoDecisionTaskId(job.push_id);
|
||||
const results = await TaskclusterModel.load(decisionTaskId, job);
|
||||
const interactiveTask = results.actions.find(result => result.name === 'create-interactive');
|
||||
const interactiveTask = results.actions.find(
|
||||
result => result.name === 'create-interactive',
|
||||
);
|
||||
|
||||
try {
|
||||
await TaskclusterModel.submit({
|
||||
|
@ -226,7 +258,8 @@ class ActionBar extends React.PureComponent {
|
|||
notify(
|
||||
`Request sent to create an interactive job via actions.json.
|
||||
You will soon receive an email containing a link to interact with the task.`,
|
||||
'success');
|
||||
'success',
|
||||
);
|
||||
} catch (e) {
|
||||
// The full message is too large to fit in a Treeherder
|
||||
// notification box.
|
||||
|
@ -236,7 +269,9 @@ class ActionBar extends React.PureComponent {
|
|||
|
||||
cancelJobs(jobs) {
|
||||
const { user, repoName, getGeckoDecisionTaskId, notify } = this.props;
|
||||
const jobIds = jobs.filter(({ state }) => state === 'pending' || state === 'running').map(({ id }) => id);
|
||||
const jobIds = jobs
|
||||
.filter(({ state }) => state === 'pending' || state === 'running')
|
||||
.map(({ id }) => id);
|
||||
|
||||
if (!user.isLoggedIn) {
|
||||
return notify('Must be logged in to cancel a job', 'danger');
|
||||
|
@ -256,14 +291,20 @@ class ActionBar extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { selectedJob, logViewerUrl, logViewerFullUrl, jobLogUrls, user, pinJob } = this.props;
|
||||
const {
|
||||
selectedJob,
|
||||
logViewerUrl,
|
||||
logViewerFullUrl,
|
||||
jobLogUrls,
|
||||
user,
|
||||
pinJob,
|
||||
} = this.props;
|
||||
const { customJobActionsShowing } = this.state;
|
||||
|
||||
return (
|
||||
<div id="job-details-actionbar">
|
||||
<nav className="navbar navbar-dark details-panel-navbar">
|
||||
<ul className="nav navbar-nav actionbar-nav">
|
||||
|
||||
<LogUrls
|
||||
logUrls={jobLogUrls}
|
||||
logViewerUrl={logViewerUrl}
|
||||
|
@ -275,32 +316,53 @@ class ActionBar extends React.PureComponent {
|
|||
title="Add this job to the pinboard"
|
||||
className="btn icon-blue"
|
||||
onClick={() => pinJob(selectedJob)}
|
||||
><span className="fa fa-thumb-tack" /></span>
|
||||
>
|
||||
<span className="fa fa-thumb-tack" />
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
id="retrigger-btn"
|
||||
title={user.isLoggedIn ? 'Repeat the selected job' : 'Must be logged in to retrigger a job'}
|
||||
title={
|
||||
user.isLoggedIn
|
||||
? 'Repeat the selected job'
|
||||
: 'Must be logged in to retrigger a job'
|
||||
}
|
||||
className={`btn ${user.isLoggedIn ? 'icon-green' : 'disabled'}`}
|
||||
disabled={!user.isLoggedIn}
|
||||
onClick={() => this.retriggerJob([selectedJob])}
|
||||
><span className="fa fa-repeat" /></span>
|
||||
>
|
||||
<span className="fa fa-repeat" />
|
||||
</span>
|
||||
</li>
|
||||
{isReftest(selectedJob) && jobLogUrls.map(jobLogUrl => (<li key={`reftest-${jobLogUrl.id}`}>
|
||||
<a
|
||||
title="Launch the Reftest Analyser in a new window"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={getReftestUrl(jobLogUrl.url)}
|
||||
><span className="fa fa-bar-chart-o" /></a>
|
||||
</li>))}
|
||||
{this.canCancel() && <li>
|
||||
<a
|
||||
title={user.isLoggedIn ? 'Cancel this job' : 'Must be logged in to cancel a job'}
|
||||
className={user.isLoggedIn ? 'hover-warning' : 'disabled'}
|
||||
onClick={() => this.cancelJob()}
|
||||
><span className="fa fa-times-circle cancel-job-icon" /></a>
|
||||
</li>}
|
||||
{isReftest(selectedJob) &&
|
||||
jobLogUrls.map(jobLogUrl => (
|
||||
<li key={`reftest-${jobLogUrl.id}`}>
|
||||
<a
|
||||
title="Launch the Reftest Analyser in a new window"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={getReftestUrl(jobLogUrl.url)}
|
||||
>
|
||||
<span className="fa fa-bar-chart-o" />
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
{this.canCancel() && (
|
||||
<li>
|
||||
<a
|
||||
title={
|
||||
user.isLoggedIn
|
||||
? 'Cancel this job'
|
||||
: 'Must be logged in to cancel a job'
|
||||
}
|
||||
className={user.isLoggedIn ? 'hover-warning' : 'disabled'}
|
||||
onClick={() => this.cancelJob()}
|
||||
>
|
||||
<span className="fa fa-times-circle cancel-job-icon" />
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
<ul className="nav navbar-right">
|
||||
<li className="dropdown">
|
||||
|
@ -311,54 +373,76 @@ class ActionBar extends React.PureComponent {
|
|||
aria-expanded="false"
|
||||
className="dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
><span className="fa fa-ellipsis-h" aria-hidden="true" /></span>
|
||||
>
|
||||
<span className="fa fa-ellipsis-h" aria-hidden="true" />
|
||||
</span>
|
||||
<ul className="dropdown-menu actionbar-menu" role="menu">
|
||||
<li>
|
||||
<span
|
||||
id="backfill-btn"
|
||||
className={`btn dropdown-item ${!user.isLoggedIn || !this.canBackfill() ? 'disabled' : ''}`}
|
||||
className={`btn dropdown-item ${
|
||||
!user.isLoggedIn || !this.canBackfill() ? 'disabled' : ''
|
||||
}`}
|
||||
title={this.backfillButtonTitle()}
|
||||
onClick={() => !this.canBackfill() || this.backfillJob()}
|
||||
>Backfill</span>
|
||||
>
|
||||
Backfill
|
||||
</span>
|
||||
</li>
|
||||
{selectedJob.taskcluster_metadata && <React.Fragment>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="dropdown-item"
|
||||
href={getInspectTaskUrl(selectedJob.taskcluster_metadata.task_id)}
|
||||
>Inspect Task</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
onClick={this.createInteractiveTask}
|
||||
>Create Interactive Task</a>
|
||||
</li>
|
||||
{isPerfTest(selectedJob) && <li>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
onClick={this.createGeckoProfile}
|
||||
>Create Gecko Profile</a>
|
||||
</li>}
|
||||
<li>
|
||||
<a
|
||||
onClick={this.toggleCustomJobActions}
|
||||
className="dropdown-item"
|
||||
>Custom Action...</a>
|
||||
</li>
|
||||
</React.Fragment>}
|
||||
{selectedJob.taskcluster_metadata && (
|
||||
<React.Fragment>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="dropdown-item"
|
||||
href={getInspectTaskUrl(
|
||||
selectedJob.taskcluster_metadata.task_id,
|
||||
)}
|
||||
>
|
||||
Inspect Task
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
onClick={this.createInteractiveTask}
|
||||
>
|
||||
Create Interactive Task
|
||||
</a>
|
||||
</li>
|
||||
{isPerfTest(selectedJob) && (
|
||||
<li>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
onClick={this.createGeckoProfile}
|
||||
>
|
||||
Create Gecko Profile
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
<a
|
||||
onClick={this.toggleCustomJobActions}
|
||||
className="dropdown-item"
|
||||
>
|
||||
Custom Action...
|
||||
</a>
|
||||
</li>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{customJobActionsShowing && <CustomJobActions
|
||||
job={selectedJob}
|
||||
pushId={selectedJob.push_id}
|
||||
isLoggedIn={user.isLoggedIn}
|
||||
toggle={this.toggleCustomJobActions}
|
||||
/>}
|
||||
{customJobActionsShowing && (
|
||||
<CustomJobActions
|
||||
job={selectedJob}
|
||||
pushId={selectedJob.push_id}
|
||||
isLoggedIn={user.isLoggedIn}
|
||||
toggle={this.toggleCustomJobActions}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -385,4 +469,6 @@ ActionBar.defaultProps = {
|
|||
jobLogUrls: [],
|
||||
};
|
||||
|
||||
export default withNotifications(withPushes(withSelectedJob(withPinnedJobs(ActionBar))));
|
||||
export default withNotifications(
|
||||
withPushes(withSelectedJob(withPinnedJobs(ActionBar))),
|
||||
);
|
||||
|
|
|
@ -6,12 +6,12 @@ import RevisionLinkify from '../../../shared/RevisionLinkify';
|
|||
import { longDateFormat } from '../../../helpers/display';
|
||||
|
||||
export default function ClassificationsPanel(props) {
|
||||
const {
|
||||
classification, job, bugs, currentRepo, classificationMap,
|
||||
} = props;
|
||||
const { classification, job, bugs, currentRepo, classificationMap } = props;
|
||||
|
||||
const failureId = classification.failure_classification_id;
|
||||
const iconClass = `${(failureId === 7 ? 'fa-star-o' : 'fa fa-star')} star-${job.result}`;
|
||||
const iconClass = `${failureId === 7 ? 'fa-star-o' : 'fa fa-star'} star-${
|
||||
job.result
|
||||
}`;
|
||||
const classificationName = classificationMap[failureId];
|
||||
|
||||
return (
|
||||
|
@ -21,23 +21,33 @@ export default function ClassificationsPanel(props) {
|
|||
<i className={`fa ${iconClass}`} />
|
||||
<span className="ml-1">{classificationName.name}</span>
|
||||
</span>
|
||||
{!!bugs.length &&
|
||||
{!!bugs.length && (
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={getBugUrl(bugs[0].bug_id)}
|
||||
title={`View bug ${bugs[0].bug_id}`}
|
||||
><em> {bugs[0].bug_id}</em></a>}
|
||||
>
|
||||
<em> {bugs[0].bug_id}</em>
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
{classification.text.length > 0 &&
|
||||
<li><em><RevisionLinkify repo={currentRepo}>{classification.text}</RevisionLinkify></em></li>
|
||||
}
|
||||
{classification.text.length > 0 && (
|
||||
<li>
|
||||
<em>
|
||||
<RevisionLinkify repo={currentRepo}>
|
||||
{classification.text}
|
||||
</RevisionLinkify>
|
||||
</em>
|
||||
</li>
|
||||
)}
|
||||
<li className="revision-comment">
|
||||
{new Date(classification.created).toLocaleString('en-US', longDateFormat)}
|
||||
</li>
|
||||
<li className="revision-comment">
|
||||
{classification.who}
|
||||
{new Date(classification.created).toLocaleString(
|
||||
'en-US',
|
||||
longDateFormat,
|
||||
)}
|
||||
</li>
|
||||
<li className="revision-comment">{classification.who}</li>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -31,48 +31,60 @@ export default function LogUrls(props) {
|
|||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{logUrls.map(jobLogUrl => (<li key={`logview-${jobLogUrl.id}`}>
|
||||
<a
|
||||
className="logviewer-btn"
|
||||
{...getLogUrlProps(jobLogUrl, logViewerUrl, logViewerFullUrl)}
|
||||
>
|
||||
<img
|
||||
alt="Logviewer"
|
||||
src={logviewerIcon}
|
||||
className="logviewer-icon"
|
||||
/>
|
||||
</a>
|
||||
</li>))}
|
||||
{logUrls.map(jobLogUrl => (
|
||||
<li key={`logview-${jobLogUrl.id}`}>
|
||||
<a
|
||||
className="logviewer-btn"
|
||||
{...getLogUrlProps(jobLogUrl, logViewerUrl, logViewerFullUrl)}
|
||||
>
|
||||
<img
|
||||
alt="Logviewer"
|
||||
src={logviewerIcon}
|
||||
className="logviewer-icon"
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
<li>
|
||||
{!logUrls.length && <a
|
||||
className="logviewer-btn disabled"
|
||||
title="No logs available for this job"
|
||||
>
|
||||
<img
|
||||
alt="Logviewer"
|
||||
src={logviewerIcon}
|
||||
className="logviewer-icon"
|
||||
/>
|
||||
</a>}
|
||||
{!logUrls.length && (
|
||||
<a
|
||||
className="logviewer-btn disabled"
|
||||
title="No logs available for this job"
|
||||
>
|
||||
<img
|
||||
alt="Logviewer"
|
||||
src={logviewerIcon}
|
||||
className="logviewer-icon"
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
|
||||
{logUrls.map(jobLogUrl => (<li key={`raw-${jobLogUrl.id}`}>
|
||||
<a
|
||||
id="raw-log-btn"
|
||||
className="raw-log-icon"
|
||||
title="Open the raw log in a new window"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={jobLogUrl.url}
|
||||
copy-value={jobLogUrl.url}
|
||||
><span className="fa fa-file-text-o" /></a>
|
||||
</li>))}
|
||||
{!logUrls.length && <li>
|
||||
<a
|
||||
className="disabled raw-log-icon"
|
||||
title="No logs available for this job"
|
||||
><span className="fa fa-file-text-o" /></a>
|
||||
</li>}
|
||||
{logUrls.map(jobLogUrl => (
|
||||
<li key={`raw-${jobLogUrl.id}`}>
|
||||
<a
|
||||
id="raw-log-btn"
|
||||
className="raw-log-icon"
|
||||
title="Open the raw log in a new window"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={jobLogUrl.url}
|
||||
copy-value={jobLogUrl.url}
|
||||
>
|
||||
<span className="fa fa-file-text-o" />
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
{!logUrls.length && (
|
||||
<li>
|
||||
<a
|
||||
className="disabled raw-log-icon"
|
||||
title="No logs available for this job"
|
||||
>
|
||||
<span className="fa fa-file-text-o" />
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,10 +9,7 @@ function StatusPanel(props) {
|
|||
const shadingClass = `result-status-shading-${getStatus(selectedJob)}`;
|
||||
|
||||
return (
|
||||
<li
|
||||
id="result-status-pane"
|
||||
className={`small ${shadingClass}`}
|
||||
>
|
||||
<li id="result-status-pane" className={`small ${shadingClass}`}>
|
||||
<div>
|
||||
<label>Result:</label>
|
||||
<span> {selectedJob.result}</span>
|
||||
|
|
|
@ -11,15 +11,28 @@ import StatusPanel from './StatusPanel';
|
|||
class SummaryPanel extends React.PureComponent {
|
||||
render() {
|
||||
const {
|
||||
repoName, selectedJob, latestClassification, bugs, jobLogUrls,
|
||||
jobDetailLoading, logViewerUrl, logViewerFullUrl,
|
||||
logParseStatus, user, currentRepo, classificationMap,
|
||||
repoName,
|
||||
selectedJob,
|
||||
latestClassification,
|
||||
bugs,
|
||||
jobLogUrls,
|
||||
jobDetailLoading,
|
||||
logViewerUrl,
|
||||
logViewerFullUrl,
|
||||
logParseStatus,
|
||||
user,
|
||||
currentRepo,
|
||||
classificationMap,
|
||||
} = this.props;
|
||||
|
||||
const logStatus = [{
|
||||
title: 'Log parsing status',
|
||||
value: !jobLogUrls.length ? 'No logs' : jobLogUrls.map(log => log.parse_status).join(', '),
|
||||
}];
|
||||
const logStatus = [
|
||||
{
|
||||
title: 'Log parsing status',
|
||||
value: !jobLogUrls.length
|
||||
? 'No logs'
|
||||
: jobLogUrls.map(log => log.parse_status).join(', '),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div id="summary-panel">
|
||||
|
@ -34,28 +47,26 @@ class SummaryPanel extends React.PureComponent {
|
|||
/>
|
||||
<div id="summary-panel-content">
|
||||
<div>
|
||||
{jobDetailLoading &&
|
||||
{jobDetailLoading && (
|
||||
<div className="overlay">
|
||||
<div>
|
||||
<span className="fa fa-spinner fa-pulse th-spinner-lg" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
|
||||
<ul className="list-unstyled">
|
||||
{latestClassification &&
|
||||
{latestClassification && (
|
||||
<ClassificationsPanel
|
||||
job={selectedJob}
|
||||
classification={latestClassification}
|
||||
classificationMap={classificationMap}
|
||||
bugs={bugs}
|
||||
currentRepo={currentRepo}
|
||||
/>}
|
||||
/>
|
||||
)}
|
||||
<StatusPanel />
|
||||
<JobInfo
|
||||
job={selectedJob}
|
||||
extraFields={logStatus}
|
||||
/>
|
||||
<JobInfo job={selectedJob} extraFields={logStatus} />
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -48,11 +48,9 @@ function RelatedBug(props) {
|
|||
<ul className="annotations-bug-list">
|
||||
{bugs.map(bug => (
|
||||
<li key={bug.bug_id}>
|
||||
<RelatedBugSaved
|
||||
bug={bug}
|
||||
deleteBug={() => deleteBug(bug)}
|
||||
/>
|
||||
</li>))}
|
||||
<RelatedBugSaved bug={bug} deleteBug={() => deleteBug(bug)} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</span>
|
||||
);
|
||||
|
@ -66,7 +64,9 @@ RelatedBug.propTypes = {
|
|||
function TableRow(props) {
|
||||
const { deleteClassification, classification, classificationMap } = props;
|
||||
const { created, who, name, text } = classification;
|
||||
const deleteEvent = () => { deleteClassification(classification); };
|
||||
const deleteEvent = () => {
|
||||
deleteClassification(classification);
|
||||
};
|
||||
const failureId = classification.failure_classification_id;
|
||||
const iconClass = failureId === 7 ? 'fa-star-o' : 'fa fa-star';
|
||||
const classificationName = classificationMap[failureId];
|
||||
|
@ -104,9 +104,7 @@ TableRow.propTypes = {
|
|||
};
|
||||
|
||||
function AnnotationsTable(props) {
|
||||
const {
|
||||
classifications, deleteClassification, classificationMap,
|
||||
} = props;
|
||||
const { classifications, deleteClassification, classificationMap } = props;
|
||||
|
||||
return (
|
||||
<table className="table-super-condensed table-hover">
|
||||
|
@ -125,8 +123,8 @@ function AnnotationsTable(props) {
|
|||
classification={classification}
|
||||
deleteClassification={deleteClassification}
|
||||
classificationMap={classificationMap}
|
||||
/>))
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
@ -148,11 +146,17 @@ class AnnotationsTab extends React.Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener(thEvents.deleteClassification, this.onDeleteClassification);
|
||||
window.addEventListener(
|
||||
thEvents.deleteClassification,
|
||||
this.onDeleteClassification,
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener(thEvents.deleteClassification, this.onDeleteClassification);
|
||||
window.removeEventListener(
|
||||
thEvents.deleteClassification,
|
||||
this.onDeleteClassification,
|
||||
);
|
||||
}
|
||||
|
||||
onDeleteClassification() {
|
||||
|
@ -161,7 +165,9 @@ class AnnotationsTab extends React.Component {
|
|||
if (classifications.length) {
|
||||
this.deleteClassification(classifications[0]);
|
||||
// Delete any number of bugs if they exist
|
||||
bugs.forEach((bug) => { this.deleteBug(bug); });
|
||||
bugs.forEach(bug => {
|
||||
this.deleteBug(bug);
|
||||
});
|
||||
} else {
|
||||
notify('No classification on this job to delete', 'warning');
|
||||
}
|
||||
|
@ -182,19 +188,27 @@ class AnnotationsTab extends React.Component {
|
|||
},
|
||||
() => {
|
||||
notify('Classification deletion failed', 'danger', { sticky: true });
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
deleteBug(bug) {
|
||||
const { notify } = this.props;
|
||||
|
||||
bug.destroy()
|
||||
.then(() => {
|
||||
notify(`Association to bug ${bug.bug_id} successfully deleted`, 'success');
|
||||
bug.destroy().then(
|
||||
() => {
|
||||
notify(
|
||||
`Association to bug ${bug.bug_id} successfully deleted`,
|
||||
'success',
|
||||
);
|
||||
window.dispatchEvent(new CustomEvent(thEvents.classificationChanged));
|
||||
}, () => {
|
||||
notify(`Association to bug ${bug.bug_id} deletion failed`, 'danger', { sticky: true });
|
||||
});
|
||||
},
|
||||
() => {
|
||||
notify(`Association to bug ${bug.bug_id} deletion failed`, 'danger', {
|
||||
sticky: true,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -204,23 +218,22 @@ class AnnotationsTab extends React.Component {
|
|||
<div className="container-fluid">
|
||||
<div className="row h-100">
|
||||
<div className="col-sm-10 classifications-pane">
|
||||
{classifications.length ?
|
||||
{classifications.length ? (
|
||||
<AnnotationsTable
|
||||
classifications={classifications}
|
||||
deleteClassification={this.deleteClassification}
|
||||
classificationMap={classificationMap}
|
||||
/> :
|
||||
/>
|
||||
) : (
|
||||
<p>This job has not been classified</p>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!!classifications.length && !!bugs.length &&
|
||||
<div className="col-sm-2 bug-list-pane">
|
||||
<RelatedBug
|
||||
bugs={bugs}
|
||||
deleteBug={this.deleteBug}
|
||||
/>
|
||||
</div>}
|
||||
{!!classifications.length && !!bugs.length && (
|
||||
<div className="col-sm-2 bug-list-pane">
|
||||
<RelatedBug bugs={bugs} deleteBug={this.deleteBug} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
|
|||
import { getCompareChooserUrl } from '../../../helpers/url';
|
||||
|
||||
export default class PerformanceTab extends React.PureComponent {
|
||||
|
||||
render() {
|
||||
const { repoName, revision, perfJobDetail } = this.props;
|
||||
const sortedDetails = perfJobDetail ? perfJobDetail.slice() : [];
|
||||
|
@ -13,26 +12,34 @@ export default class PerformanceTab extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<div className="performance-panel">
|
||||
{!!sortedDetails.length && <ul>
|
||||
<li>Perfherder:
|
||||
{sortedDetails.map((detail, idx) => (
|
||||
<ul
|
||||
key={idx} // eslint-disable-line react/no-array-index-key
|
||||
>
|
||||
<li>{detail.title}:
|
||||
<a href={detail.url}> {detail.value}</a>
|
||||
</li>
|
||||
</ul>
|
||||
))}
|
||||
</li>
|
||||
</ul>}
|
||||
{!!sortedDetails.length && (
|
||||
<ul>
|
||||
<li>
|
||||
Perfherder:
|
||||
{sortedDetails.map((detail, idx) => (
|
||||
<ul
|
||||
key={idx} // eslint-disable-line react/no-array-index-key
|
||||
>
|
||||
<li>
|
||||
{detail.title}:<a href={detail.url}> {detail.value}</a>
|
||||
</li>
|
||||
</ul>
|
||||
))}
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
href={getCompareChooserUrl({ newProject: repoName, newRevision: revision })}
|
||||
href={getCompareChooserUrl({
|
||||
newProject: repoName,
|
||||
newRevision: revision,
|
||||
})}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Compare result against another revision</a>
|
||||
>
|
||||
Compare result against another revision
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -51,15 +51,18 @@ class SimilarJobsTab extends React.Component {
|
|||
offset: (page - 1) * this.pageSize,
|
||||
};
|
||||
|
||||
['filterBuildPlatformId', 'filterOptionCollectionHash']
|
||||
.forEach((key) => {
|
||||
if (this.state[key]) {
|
||||
const field = this.filterMap[key];
|
||||
options[field] = selectedJob[field];
|
||||
}
|
||||
['filterBuildPlatformId', 'filterOptionCollectionHash'].forEach(key => {
|
||||
if (this.state[key]) {
|
||||
const field = this.filterMap[key];
|
||||
options[field] = selectedJob[field];
|
||||
}
|
||||
});
|
||||
|
||||
const newSimilarJobs = await JobModel.getSimilarJobs(repoName, selectedJob.id, options);
|
||||
const newSimilarJobs = await JobModel.getSimilarJobs(
|
||||
repoName,
|
||||
selectedJob.id,
|
||||
options,
|
||||
);
|
||||
|
||||
if (newSimilarJobs.length > 0) {
|
||||
this.setState({ hasNextPage: newSimilarJobs.length > this.pageSize });
|
||||
|
@ -68,18 +71,28 @@ class SimilarJobsTab extends React.Component {
|
|||
const pushIds = [...new Set(newSimilarJobs.map(job => job.push_id))];
|
||||
// get pushes and revisions for the given ids
|
||||
let pushList = { results: [] };
|
||||
const resp = await PushModel.getList({ id__in: pushIds.join(','), count: thMaxPushFetchSize });
|
||||
const resp = await PushModel.getList({
|
||||
id__in: pushIds.join(','),
|
||||
count: thMaxPushFetchSize,
|
||||
});
|
||||
|
||||
if (resp.ok) {
|
||||
pushList = await resp.json();
|
||||
// decorate the list of jobs with their result sets
|
||||
const pushes = pushList.results.reduce((acc, push) => (
|
||||
{ ...acc, [push.id]: push }
|
||||
), {});
|
||||
newSimilarJobs.forEach((simJob) => {
|
||||
const pushes = pushList.results.reduce(
|
||||
(acc, push) => ({ ...acc, [push.id]: push }),
|
||||
{},
|
||||
);
|
||||
newSimilarJobs.forEach(simJob => {
|
||||
simJob.result_set = pushes[simJob.push_id];
|
||||
simJob.revisionResultsetFilterUrl = getJobsUrl({ repo: repoName, revision: simJob.result_set.revisions[0].revision });
|
||||
simJob.authorResultsetFilterUrl = getJobsUrl({ repo: repoName, author: simJob.result_set.author });
|
||||
simJob.revisionResultsetFilterUrl = getJobsUrl({
|
||||
repo: repoName,
|
||||
revision: simJob.result_set.revisions[0].revision,
|
||||
});
|
||||
simJob.authorResultsetFilterUrl = getJobsUrl({
|
||||
repo: repoName,
|
||||
author: simJob.result_set.author,
|
||||
});
|
||||
});
|
||||
this.setState({ similarJobs: [...similarJobs, ...newSimilarJobs] });
|
||||
// on the first page show the first element info by default
|
||||
|
@ -87,7 +100,11 @@ class SimilarJobsTab extends React.Component {
|
|||
this.showJobInfo(newSimilarJobs[0]);
|
||||
}
|
||||
} else {
|
||||
notify(`Error fetching similar jobs push data: ${resp.message}`, 'danger', { sticky: true });
|
||||
notify(
|
||||
`Error fetching similar jobs push data: ${resp.message}`,
|
||||
'danger',
|
||||
{ sticky: true },
|
||||
);
|
||||
}
|
||||
}
|
||||
this.setState({ isLoading: false });
|
||||
|
@ -102,25 +119,30 @@ class SimilarJobsTab extends React.Component {
|
|||
showJobInfo(job) {
|
||||
const { repoName, classificationMap } = this.props;
|
||||
|
||||
JobModel.get(repoName, job.id)
|
||||
.then((nextJob) => {
|
||||
nextJob.result_status = getStatus(nextJob);
|
||||
nextJob.duration = (nextJob.end_timestamp - nextJob.start_timestamp) / 60;
|
||||
nextJob.failure_classification = classificationMap[
|
||||
nextJob.failure_classification_id];
|
||||
JobModel.get(repoName, job.id).then(nextJob => {
|
||||
nextJob.result_status = getStatus(nextJob);
|
||||
nextJob.duration = (nextJob.end_timestamp - nextJob.start_timestamp) / 60;
|
||||
nextJob.failure_classification =
|
||||
classificationMap[nextJob.failure_classification_id];
|
||||
|
||||
// retrieve the list of error lines
|
||||
TextLogStepModel.get(nextJob.id).then((textLogSteps) => {
|
||||
nextJob.error_lines = textLogSteps.reduce((acc, step) => (
|
||||
[...acc, ...step.errors]), []);
|
||||
this.setState({ selectedSimilarJob: nextJob });
|
||||
});
|
||||
// retrieve the list of error lines
|
||||
TextLogStepModel.get(nextJob.id).then(textLogSteps => {
|
||||
nextJob.error_lines = textLogSteps.reduce(
|
||||
(acc, step) => [...acc, ...step.errors],
|
||||
[],
|
||||
);
|
||||
this.setState({ selectedSimilarJob: nextJob });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
toggleFilter(filterField) {
|
||||
this.setState(
|
||||
{ [filterField]: !this.state[filterField], similarJobs: [], isLoading: true },
|
||||
{
|
||||
[filterField]: !this.state[filterField],
|
||||
similarJobs: [],
|
||||
isLoading: true,
|
||||
},
|
||||
this.getSimilarJobs,
|
||||
);
|
||||
}
|
||||
|
@ -135,7 +157,9 @@ class SimilarJobsTab extends React.Component {
|
|||
isLoading,
|
||||
} = this.state;
|
||||
const button_class = job => getBtnClass(getStatus(job));
|
||||
const selectedSimilarJobId = selectedSimilarJob ? selectedSimilarJob.id : null;
|
||||
const selectedSimilarJobId = selectedSimilarJob
|
||||
? selectedSimilarJob.id
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="similar-jobs w-100">
|
||||
|
@ -154,19 +178,25 @@ class SimilarJobsTab extends React.Component {
|
|||
<tr
|
||||
key={similarJob.id}
|
||||
onClick={() => this.showJobInfo(similarJob)}
|
||||
className={selectedSimilarJobId === similarJob.id ? 'table-active' : ''}
|
||||
className={
|
||||
selectedSimilarJobId === similarJob.id ? 'table-active' : ''
|
||||
}
|
||||
>
|
||||
<td>
|
||||
<button
|
||||
className={`btn btn-similar-jobs btn-xs ${button_class(similarJob)}`}
|
||||
>{similarJob.job_type_symbol}
|
||||
{similarJob.failure_classification_id > 1 &&
|
||||
<span>*</span>}
|
||||
className={`btn btn-similar-jobs btn-xs ${button_class(
|
||||
similarJob,
|
||||
)}`}
|
||||
>
|
||||
{similarJob.job_type_symbol}
|
||||
{similarJob.failure_classification_id > 1 && (
|
||||
<span>*</span>
|
||||
)}
|
||||
</button>
|
||||
</td>
|
||||
<td
|
||||
title={toDateStr(similarJob.result_set.push_timestamp)}
|
||||
>{toShortDateStr(similarJob.result_set.push_timestamp)}</td>
|
||||
<td title={toDateStr(similarJob.result_set.push_timestamp)}>
|
||||
{toShortDateStr(similarJob.result_set.push_timestamp)}
|
||||
</td>
|
||||
<td>
|
||||
<a href={similarJob.authorResultsetFilterUrl}>
|
||||
{similarJob.result_set.author}
|
||||
|
@ -177,14 +207,18 @@ class SimilarJobsTab extends React.Component {
|
|||
{similarJob.result_set.revisions[0].revision}
|
||||
</a>
|
||||
</td>
|
||||
</tr>))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{hasNextPage &&
|
||||
<button
|
||||
className="btn btn-light-bordered btn-sm link-style"
|
||||
onClick={this.showNext}
|
||||
>Show previous jobs</button>}
|
||||
{hasNextPage && (
|
||||
<button
|
||||
className="btn btn-light-bordered btn-sm link-style"
|
||||
onClick={this.showNext}
|
||||
>
|
||||
Show previous jobs
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="similar-job-detail-panel">
|
||||
<form className="form form-inline">
|
||||
|
@ -195,7 +229,6 @@ class SimilarJobsTab extends React.Component {
|
|||
checked={filterBuildPlatformId}
|
||||
/>
|
||||
<small>Same platform</small>
|
||||
|
||||
</div>
|
||||
<div className="checkbox">
|
||||
<input
|
||||
|
@ -204,70 +237,83 @@ class SimilarJobsTab extends React.Component {
|
|||
checked={filterOptionCollectionHash}
|
||||
/>
|
||||
<small>Same options</small>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
<div className="similar_job_detail">
|
||||
{selectedSimilarJob && <table className="table table-super-condensed">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Result</th>
|
||||
<td>{selectedSimilarJob.result_status}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Build</th>
|
||||
<td>
|
||||
{selectedSimilarJob.build_architecture} {selectedSimilarJob.build_platform} {selectedSimilarJob.build_os}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Build option</th>
|
||||
<td>
|
||||
{selectedSimilarJob.platform_option}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Job name</th>
|
||||
<td>{selectedSimilarJob.job_type_name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Started</th>
|
||||
<td>{toDateStr(selectedSimilarJob.start_timestamp)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Duration</th>
|
||||
<td>
|
||||
{selectedSimilarJob.duration >= 0 ? `${selectedSimilarJob.duration.toFixed(0)} minute(s)` : 'unknown'}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Classification</th>
|
||||
<td>
|
||||
<label
|
||||
className={`badge ${selectedSimilarJob.failure_classification.star}`}
|
||||
>{selectedSimilarJob.failure_classification.name}</label>
|
||||
</td>
|
||||
</tr>
|
||||
{!!selectedSimilarJob.error_lines && <tr>
|
||||
<td colSpan={2}>
|
||||
<ul className="list-unstyled error_list">
|
||||
{selectedSimilarJob.error_lines.map(error => (<li key={error.id}>
|
||||
<small title={error.line}>{error.line}</small>
|
||||
</li>))}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>}
|
||||
</tbody>
|
||||
</table>}
|
||||
{selectedSimilarJob && (
|
||||
<table className="table table-super-condensed">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Result</th>
|
||||
<td>{selectedSimilarJob.result_status}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Build</th>
|
||||
<td>
|
||||
{selectedSimilarJob.build_architecture}{' '}
|
||||
{selectedSimilarJob.build_platform}{' '}
|
||||
{selectedSimilarJob.build_os}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Build option</th>
|
||||
<td>{selectedSimilarJob.platform_option}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Job name</th>
|
||||
<td>{selectedSimilarJob.job_type_name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Started</th>
|
||||
<td>{toDateStr(selectedSimilarJob.start_timestamp)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Duration</th>
|
||||
<td>
|
||||
{selectedSimilarJob.duration >= 0
|
||||
? `${selectedSimilarJob.duration.toFixed(0)} minute(s)`
|
||||
: 'unknown'}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Classification</th>
|
||||
<td>
|
||||
<label
|
||||
className={`badge ${
|
||||
selectedSimilarJob.failure_classification.star
|
||||
}`}
|
||||
>
|
||||
{selectedSimilarJob.failure_classification.name}
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
{!!selectedSimilarJob.error_lines && (
|
||||
<tr>
|
||||
<td colSpan={2}>
|
||||
<ul className="list-unstyled error_list">
|
||||
{selectedSimilarJob.error_lines.map(error => (
|
||||
<li key={error.id}>
|
||||
<small title={error.line}>{error.line}</small>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isLoading && <div className="overlay">
|
||||
<div>
|
||||
<span className="fa fa-spinner fa-pulse th-spinner-lg" />
|
||||
{isLoading && (
|
||||
<div className="overlay">
|
||||
<div>
|
||||
<span className="fa fa-spinner fa-pulse th-spinner-lg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -32,10 +32,14 @@ class TabsPanel extends React.Component {
|
|||
// This fires every time the props change. But we only want to figure out the new default
|
||||
// tab when we get a new job. However, the job could change, then later, the perf details fetch
|
||||
// returns. So we need to check for a change in the size of the perfJobDetail too.
|
||||
if (state.jobId !== selectedJob.id || state.perfJobDetailSize !== perfJobDetail.length) {
|
||||
if (
|
||||
state.jobId !== selectedJob.id ||
|
||||
state.perfJobDetailSize !== perfJobDetail.length
|
||||
) {
|
||||
const tabIndex = TabsPanel.getDefaultTabIndex(
|
||||
getStatus(selectedJob),
|
||||
!!perfJobDetail.length, showAutoclassifyTab,
|
||||
!!perfJobDetail.length,
|
||||
showAutoclassifyTab,
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -62,28 +66,45 @@ class TabsPanel extends React.Component {
|
|||
const { tabIndex, showAutoclassifyTab } = this.state;
|
||||
const { perfJobDetail } = this.props;
|
||||
const nextIndex = tabIndex + 1;
|
||||
const tabCount = TabsPanel.getTabNames(!!perfJobDetail.length, showAutoclassifyTab).length;
|
||||
const tabCount = TabsPanel.getTabNames(
|
||||
!!perfJobDetail.length,
|
||||
showAutoclassifyTab,
|
||||
).length;
|
||||
this.setState({ tabIndex: nextIndex < tabCount ? nextIndex : 0 });
|
||||
}
|
||||
|
||||
static getDefaultTabIndex(status, showPerf, showAutoclassify) {
|
||||
let idx = 0;
|
||||
const tabNames = TabsPanel.getTabNames(showPerf, showAutoclassify);
|
||||
const tabIndexes = tabNames.reduce((acc, name) => ({ ...acc, [name]: idx++ }), {});
|
||||
const tabIndexes = tabNames.reduce(
|
||||
(acc, name) => ({ ...acc, [name]: idx++ }),
|
||||
{},
|
||||
);
|
||||
|
||||
let tabIndex = showPerf ? tabIndexes.perf : tabIndexes.details;
|
||||
if (['busted', 'testfailed', 'exception'].includes(status)) {
|
||||
tabIndex = showAutoclassify ? tabIndexes.autoclassify : tabIndexes.failure;
|
||||
tabIndex = showAutoclassify
|
||||
? tabIndexes.autoclassify
|
||||
: tabIndexes.failure;
|
||||
}
|
||||
return tabIndex;
|
||||
}
|
||||
|
||||
static getTabNames(showPerf, showAutoclassify) {
|
||||
return [
|
||||
'details', 'failure', 'autoclassify', 'annotations', 'similar', 'perf',
|
||||
].filter(name => (
|
||||
!((name === 'autoclassify' && !showAutoclassify) || (name === 'perf' && !showPerf))
|
||||
));
|
||||
'details',
|
||||
'failure',
|
||||
'autoclassify',
|
||||
'annotations',
|
||||
'similar',
|
||||
'perf',
|
||||
].filter(
|
||||
name =>
|
||||
!(
|
||||
(name === 'autoclassify' && !showAutoclassify) ||
|
||||
(name === 'perf' && !showPerf)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
setTabIndex(tabIndex) {
|
||||
|
@ -92,10 +113,25 @@ class TabsPanel extends React.Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
jobDetails, jobLogUrls, logParseStatus, suggestions, errors, user, bugs,
|
||||
bugSuggestionsLoading, perfJobDetail, repoName, jobRevision,
|
||||
classifications, togglePinBoardVisibility, isPinBoardVisible, pinnedJobs,
|
||||
classificationMap, logViewerFullUrl, reftestUrl, clearSelectedJob,
|
||||
jobDetails,
|
||||
jobLogUrls,
|
||||
logParseStatus,
|
||||
suggestions,
|
||||
errors,
|
||||
user,
|
||||
bugs,
|
||||
bugSuggestionsLoading,
|
||||
perfJobDetail,
|
||||
repoName,
|
||||
jobRevision,
|
||||
classifications,
|
||||
togglePinBoardVisibility,
|
||||
isPinBoardVisible,
|
||||
pinnedJobs,
|
||||
classificationMap,
|
||||
logViewerFullUrl,
|
||||
reftestUrl,
|
||||
clearSelectedJob,
|
||||
} = this.props;
|
||||
const { showAutoclassifyTab, tabIndex } = this.state;
|
||||
const countPinnedJobs = Object.keys(pinnedJobs).length;
|
||||
|
@ -116,30 +152,50 @@ class TabsPanel extends React.Component {
|
|||
<Tab>Similar Jobs</Tab>
|
||||
{!!perfJobDetail.length && <Tab>Performance</Tab>}
|
||||
</span>
|
||||
<span id="tab-header-buttons" className="details-panel-controls pull-right">
|
||||
<span
|
||||
id="tab-header-buttons"
|
||||
className="details-panel-controls pull-right"
|
||||
>
|
||||
<span
|
||||
id="pinboard-btn"
|
||||
className="btn pinboard-btn-text"
|
||||
onClick={togglePinBoardVisibility}
|
||||
title={isPinBoardVisible ? 'Close the pinboard' : 'Open the pinboard'}
|
||||
>PinBoard
|
||||
{!!countPinnedJobs && <div
|
||||
id="pin-count-group"
|
||||
title={`You have ${countPinnedJobs} job${countPinnedJobs > 1 ? 's' : ''} pinned`}
|
||||
className={`${countPinnedJobs > 99 ? 'pin-count-group-3-digit' : ''}`}
|
||||
>
|
||||
title={
|
||||
isPinBoardVisible ? 'Close the pinboard' : 'Open the pinboard'
|
||||
}
|
||||
>
|
||||
PinBoard
|
||||
{!!countPinnedJobs && (
|
||||
<div
|
||||
className={`pin-count-text ${countPinnedJobs > 99 ? 'pin-count-group-3-digit' : ''}`}
|
||||
>{countPinnedJobs}</div>
|
||||
</div>}
|
||||
id="pin-count-group"
|
||||
title={`You have ${countPinnedJobs} job${
|
||||
countPinnedJobs > 1 ? 's' : ''
|
||||
} pinned`}
|
||||
className={`${
|
||||
countPinnedJobs > 99 ? 'pin-count-group-3-digit' : ''
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`pin-count-text ${
|
||||
countPinnedJobs > 99 ? 'pin-count-group-3-digit' : ''
|
||||
}`}
|
||||
>
|
||||
{countPinnedJobs}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<span
|
||||
className={`fa ${isPinBoardVisible ? 'fa-angle-down' : 'fa-angle-up'}`}
|
||||
className={`fa ${
|
||||
isPinBoardVisible ? 'fa-angle-down' : 'fa-angle-up'
|
||||
}`}
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
onClick={() => clearSelectedJob(countPinnedJobs)}
|
||||
className="btn details-panel-close-btn"
|
||||
><span className="fa fa-times" /></span>
|
||||
>
|
||||
<span className="fa fa-times" />
|
||||
</span>
|
||||
</span>
|
||||
</TabList>
|
||||
<TabPanel>
|
||||
|
@ -156,15 +212,17 @@ class TabsPanel extends React.Component {
|
|||
reftestUrl={reftestUrl}
|
||||
/>
|
||||
</TabPanel>
|
||||
{showAutoclassifyTab && <TabPanel>
|
||||
<AutoclassifyTab
|
||||
hasLogs={!!jobLogUrls.length}
|
||||
logsParsed={logParseStatus !== 'pending'}
|
||||
logParseStatus={logParseStatus}
|
||||
user={user}
|
||||
repoName={repoName}
|
||||
/>
|
||||
</TabPanel>}
|
||||
{showAutoclassifyTab && (
|
||||
<TabPanel>
|
||||
<AutoclassifyTab
|
||||
hasLogs={!!jobLogUrls.length}
|
||||
logsParsed={logParseStatus !== 'pending'}
|
||||
logParseStatus={logParseStatus}
|
||||
user={user}
|
||||
repoName={repoName}
|
||||
/>
|
||||
</TabPanel>
|
||||
)}
|
||||
<TabPanel>
|
||||
<AnnotationsTab
|
||||
classificationMap={classificationMap}
|
||||
|
@ -178,13 +236,15 @@ class TabsPanel extends React.Component {
|
|||
classificationMap={classificationMap}
|
||||
/>
|
||||
</TabPanel>
|
||||
{!!perfJobDetail.length && <TabPanel>
|
||||
<PerformanceTab
|
||||
repoName={repoName}
|
||||
perfJobDetail={perfJobDetail}
|
||||
revision={jobRevision}
|
||||
/>
|
||||
</TabPanel>}
|
||||
{!!perfJobDetail.length && (
|
||||
<TabPanel>
|
||||
<PerformanceTab
|
||||
repoName={repoName}
|
||||
perfJobDetail={perfJobDetail}
|
||||
revision={jobRevision}
|
||||
/>
|
||||
</TabPanel>
|
||||
)}
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -68,10 +68,9 @@ class AutoclassifyTab extends React.Component {
|
|||
*/
|
||||
onSaveAll(pendingLines) {
|
||||
const pending = pendingLines || Array.from(this.state.inputByLine.values());
|
||||
this.save(pending)
|
||||
.then(() => {
|
||||
this.setState({ selectedLineIds: new Set() });
|
||||
});
|
||||
this.save(pending).then(() => {
|
||||
this.setState({ selectedLineIds: new Set() });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -121,21 +120,32 @@ class AutoclassifyTab extends React.Component {
|
|||
|
||||
getLoadStatusText() {
|
||||
switch (this.state.loadStatus) {
|
||||
case 'job_pending': return 'Job not complete, please wait';
|
||||
case 'pending': return 'Logs not fully parsed, please wait';
|
||||
case 'failed': return 'Log parsing failed';
|
||||
case 'no_logs': return 'No errors logged';
|
||||
case 'error': return 'Error showing autoclassification data';
|
||||
case 'loading': return null;
|
||||
case 'ready': return (!this.state.errorLines || this.state.errorLines.length === 0) ? 'No error lines reported' : null;
|
||||
default: return `Unexpected status: ${this.state.loadStatus}`;
|
||||
case 'job_pending':
|
||||
return 'Job not complete, please wait';
|
||||
case 'pending':
|
||||
return 'Logs not fully parsed, please wait';
|
||||
case 'failed':
|
||||
return 'Log parsing failed';
|
||||
case 'no_logs':
|
||||
return 'No errors logged';
|
||||
case 'error':
|
||||
return 'Error showing autoclassification data';
|
||||
case 'loading':
|
||||
return null;
|
||||
case 'ready':
|
||||
return !this.state.errorLines || this.state.errorLines.length === 0
|
||||
? 'No error lines reported'
|
||||
: null;
|
||||
default:
|
||||
return `Unexpected status: ${this.state.loadStatus}`;
|
||||
}
|
||||
}
|
||||
|
||||
setEditable(lineIds, editable) {
|
||||
const { editableLineIds } = this.state;
|
||||
const f = editable ? lineId => editableLineIds.add(lineId) :
|
||||
lineId => editableLineIds.delete(lineId);
|
||||
const f = editable
|
||||
? lineId => editableLineIds.add(lineId)
|
||||
: lineId => editableLineIds.delete(lineId);
|
||||
|
||||
lineIds.forEach(f);
|
||||
this.setState({ editableLineIds });
|
||||
|
@ -154,18 +164,23 @@ class AutoclassifyTab extends React.Component {
|
|||
async fetchErrorData() {
|
||||
const { selectedJob } = this.props;
|
||||
|
||||
this.setState({
|
||||
this.setState(
|
||||
{
|
||||
loadStatus: 'loading',
|
||||
errorLines: [],
|
||||
selectedLineIds: new Set(),
|
||||
editableLineIds: new Set(),
|
||||
inputByLine: new Map(),
|
||||
autoclassifyStatusOnLoad: null,
|
||||
}, async () => {
|
||||
},
|
||||
async () => {
|
||||
if (selectedJob.id) {
|
||||
const errorLineResp = await fetch(getProjectJobUrl('/text_log_errors/', selectedJob.id));
|
||||
const errorLineResp = await fetch(
|
||||
getProjectJobUrl('/text_log_errors/', selectedJob.id),
|
||||
);
|
||||
const errorLineData = await errorLineResp.json();
|
||||
const errorLines = errorLineData.map(line => new ErrorLineData(line))
|
||||
const errorLines = errorLineData
|
||||
.map(line => new ErrorLineData(line))
|
||||
.sort((a, b) => a.data.id - b.data.id);
|
||||
|
||||
if (errorLines.length) {
|
||||
|
@ -180,10 +195,10 @@ class AutoclassifyTab extends React.Component {
|
|||
loadStatus: 'ready',
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test if it is possible to save a specific line.
|
||||
* @param {number} lineId - Line id to test.
|
||||
|
@ -214,8 +229,11 @@ class AutoclassifyTab extends React.Component {
|
|||
canSaveAll() {
|
||||
const pendingLines = this.getPendingLines();
|
||||
|
||||
return (this.state.canClassify && !!pendingLines.length &&
|
||||
pendingLines.every(line => this.canSave(line.id)));
|
||||
return (
|
||||
this.state.canClassify &&
|
||||
!!pendingLines.length &&
|
||||
pendingLines.every(line => this.canSave(line.id))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -236,19 +254,23 @@ class AutoclassifyTab extends React.Component {
|
|||
}));
|
||||
|
||||
this.setState({ loadStatus: 'loading' });
|
||||
return TextLogErrorsModel
|
||||
.verifyMany(data)
|
||||
.then((data) => {
|
||||
const newErrorLines = data.reduce((newLines, updatedLine) => {
|
||||
const idx = newLines.findIndex(line => line.id === updatedLine.id);
|
||||
newLines[idx] = new ErrorLineData(updatedLine);
|
||||
return newLines;
|
||||
}, [...errorLines]);
|
||||
return TextLogErrorsModel.verifyMany(data)
|
||||
.then(data => {
|
||||
const newErrorLines = data.reduce(
|
||||
(newLines, updatedLine) => {
|
||||
const idx = newLines.findIndex(line => line.id === updatedLine.id);
|
||||
newLines[idx] = new ErrorLineData(updatedLine);
|
||||
return newLines;
|
||||
},
|
||||
[...errorLines],
|
||||
);
|
||||
this.setState({ errorLines: newErrorLines, loadStatus: 'ready' });
|
||||
})
|
||||
.catch((err) => {
|
||||
.catch(err => {
|
||||
const prefix = 'Error saving classifications: ';
|
||||
const msg = err.stack ? `${prefix}${err}${err.stack}` : `${prefix}${err.statusText} - ${err.data.detail}`;
|
||||
const msg = err.stack
|
||||
? `${prefix}${err}${err.stack}`
|
||||
: `${prefix}${err.statusText} - ${err.data.detail}`;
|
||||
notify(msg, 'danger', { sticky: true });
|
||||
});
|
||||
}
|
||||
|
@ -257,7 +279,13 @@ class AutoclassifyTab extends React.Component {
|
|||
* Update the panel for a new job selection
|
||||
*/
|
||||
jobChanged() {
|
||||
const { autoclassifyStatus, hasLogs, logsParsed, logParseStatus, selectedJob } = this.props;
|
||||
const {
|
||||
autoclassifyStatus,
|
||||
hasLogs,
|
||||
logsParsed,
|
||||
logParseStatus,
|
||||
selectedJob,
|
||||
} = this.props;
|
||||
const { loadStatus, autoclassifyStatusOnLoad } = this.state;
|
||||
|
||||
let newLoadStatus = 'loading';
|
||||
|
@ -269,7 +297,10 @@ class AutoclassifyTab extends React.Component {
|
|||
newLoadStatus = 'failed';
|
||||
} else if (!hasLogs) {
|
||||
newLoadStatus = 'no_logs';
|
||||
} else if (autoclassifyStatusOnLoad === null || autoclassifyStatusOnLoad === 'cross_referenced') {
|
||||
} else if (
|
||||
autoclassifyStatusOnLoad === null ||
|
||||
autoclassifyStatusOnLoad === 'cross_referenced'
|
||||
) {
|
||||
if (loadStatus !== 'ready') {
|
||||
newLoadStatus = 'loading';
|
||||
}
|
||||
|
@ -331,46 +362,52 @@ class AutoclassifyTab extends React.Component {
|
|||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{canClassify && <AutoclassifyToolbar
|
||||
loadStatus={loadStatus}
|
||||
autoclassifyStatus={autoclassifyStatus}
|
||||
user={user}
|
||||
hasSelection={!!selectedLineIds.size}
|
||||
canSave={canSave}
|
||||
canSaveAll={canSaveAll}
|
||||
canClassify={canClassify}
|
||||
onPin={this.onPin}
|
||||
onIgnore={this.onIgnore}
|
||||
onEdit={this.onToggleEditable}
|
||||
onSave={this.onSave}
|
||||
onSaveAll={() => this.onSaveAll()}
|
||||
/>}
|
||||
{canClassify && (
|
||||
<AutoclassifyToolbar
|
||||
loadStatus={loadStatus}
|
||||
autoclassifyStatus={autoclassifyStatus}
|
||||
user={user}
|
||||
hasSelection={!!selectedLineIds.size}
|
||||
canSave={canSave}
|
||||
canSaveAll={canSaveAll}
|
||||
canClassify={canClassify}
|
||||
onPin={this.onPin}
|
||||
onIgnore={this.onIgnore}
|
||||
onEdit={this.onToggleEditable}
|
||||
onSave={this.onSave}
|
||||
onSaveAll={() => this.onSaveAll()}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div>
|
||||
{loadStatusText && <span>{loadStatusText}</span>}
|
||||
{loadStatus === 'loading' && <div className="overlay">
|
||||
<div>
|
||||
<span className="fa fa-spinner fa-pulse th-spinner-lg" />
|
||||
{loadStatus === 'loading' && (
|
||||
<div className="overlay">
|
||||
<div>
|
||||
<span className="fa fa-spinner fa-pulse th-spinner-lg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
)}
|
||||
</div>
|
||||
|
||||
<span className="autoclassify-error-lines">
|
||||
<ul className="list-unstyled">
|
||||
{errorLines.map((errorLine, idx) => (<li key={errorLine.id}>
|
||||
<ErrorLine
|
||||
errorMatchers={errorMatchers}
|
||||
errorLine={errorLine}
|
||||
prevErrorLine={errorLines[idx - 1]}
|
||||
canClassify={canClassify}
|
||||
isSelected={selectedLineIds.has(errorLine.id)}
|
||||
isEditable={editableLineIds.has(errorLine.id)}
|
||||
setEditable={() => this.setEditable([errorLine.id], true)}
|
||||
setErrorLineInput={this.setErrorLineInput}
|
||||
toggleSelect={this.toggleSelect}
|
||||
repoName={repoName}
|
||||
/>
|
||||
</li>))}
|
||||
{errorLines.map((errorLine, idx) => (
|
||||
<li key={errorLine.id}>
|
||||
<ErrorLine
|
||||
errorMatchers={errorMatchers}
|
||||
errorLine={errorLine}
|
||||
prevErrorLine={errorLines[idx - 1]}
|
||||
canClassify={canClassify}
|
||||
isSelected={selectedLineIds.has(errorLine.id)}
|
||||
isEditable={editableLineIds.has(errorLine.id)}
|
||||
setEditable={() => this.setEditable([errorLine.id], true)}
|
||||
setErrorLineInput={this.setErrorLineInput}
|
||||
toggleSelect={this.toggleSelect}
|
||||
repoName={repoName}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</span>
|
||||
</React.Fragment>
|
||||
|
@ -396,4 +433,6 @@ AutoclassifyTab.defaultProps = {
|
|||
logParseStatus: 'pending',
|
||||
};
|
||||
|
||||
export default withNotifications(withSelectedJob(withPinnedJobs(AutoclassifyTab)));
|
||||
export default withNotifications(
|
||||
withSelectedJob(withPinnedJobs(AutoclassifyTab)),
|
||||
);
|
||||
|
|
|
@ -2,7 +2,6 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class AutoclassifyToolbar extends React.Component {
|
||||
|
||||
getButtonTitle(condition, activeTitle, inactiveTitle) {
|
||||
const { user } = this.props;
|
||||
|
||||
|
@ -20,60 +19,88 @@ export default class AutoclassifyToolbar extends React.Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
hasSelection, canSave, canSaveAll, canClassify, onPin, onIgnore, onSave,
|
||||
onSaveAll, onEdit, autoclassifyStatus,
|
||||
hasSelection,
|
||||
canSave,
|
||||
canSaveAll,
|
||||
canClassify,
|
||||
onPin,
|
||||
onIgnore,
|
||||
onSave,
|
||||
onSaveAll,
|
||||
onEdit,
|
||||
autoclassifyStatus,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className="autoclassify-toolbar th-context-navbar navbar-right">
|
||||
{
|
||||
// TODO: This is broken (bug 1504711)
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
status === 'ready' && (
|
||||
<div>
|
||||
{autoclassifyStatus === 'cross_referenced' && (
|
||||
<span>Autoclassification pending</span>
|
||||
)}
|
||||
{autoclassifyStatus === 'failed' && (
|
||||
<span>Autoclassification failed</span>
|
||||
)}
|
||||
</div>
|
||||
{// TODO: This is broken (bug 1504711)
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
status === 'ready' && (
|
||||
<div>
|
||||
{autoclassifyStatus === 'cross_referenced' && (
|
||||
<span>Autoclassification pending</span>
|
||||
)}
|
||||
{autoclassifyStatus === 'failed' && (
|
||||
<span>Autoclassification failed</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="btn btn-view-nav btn-sm nav-menu-btn"
|
||||
title="Pin job for bustage"
|
||||
onClick={onPin}
|
||||
>Bustage
|
||||
>
|
||||
Bustage
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btn btn-view-nav btn-sm nav-menu-btn"
|
||||
title={this.getButtonTitle(hasSelection, 'Edit selected lines', 'Nothing selected')}
|
||||
title={this.getButtonTitle(
|
||||
hasSelection,
|
||||
'Edit selected lines',
|
||||
'Nothing selected',
|
||||
)}
|
||||
onClick={onEdit}
|
||||
disabled={hasSelection && !canClassify}
|
||||
>Edit</button>
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btn btn-view-nav btn-sm nav-menu-btn"
|
||||
title={this.getButtonTitle(hasSelection, 'Ignore selected lines', 'Nothing selected')}
|
||||
title={this.getButtonTitle(
|
||||
hasSelection,
|
||||
'Ignore selected lines',
|
||||
'Nothing selected',
|
||||
)}
|
||||
onClick={onIgnore}
|
||||
disabled={hasSelection && !canClassify}
|
||||
>Ignore</button>
|
||||
>
|
||||
Ignore
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btn btn-view-nav btn-sm nav-menu-btn"
|
||||
title={this.getButtonTitle(canSave, 'Save', 'Nothing selected')}
|
||||
onClick={onSave}
|
||||
disabled={!canSave}
|
||||
>Save</button>
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btn btn-view-nav btn-sm nav-menu-btn"
|
||||
title={this.getButtonTitle(canSaveAll, 'Save All', 'Lines not classified')}
|
||||
title={this.getButtonTitle(
|
||||
canSaveAll,
|
||||
'Save All',
|
||||
'Lines not classified',
|
||||
)}
|
||||
onClick={onSaveAll}
|
||||
disabled={!canSaveAll}
|
||||
>Save All</button>
|
||||
>
|
||||
Save All
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,10 @@ import PropTypes from 'prop-types';
|
|||
import { FormGroup } from 'reactstrap';
|
||||
|
||||
import { thEvents } from '../../../../helpers/constants';
|
||||
import { stringOverlap, highlightLogLine } from '../../../../helpers/autoclassify';
|
||||
import {
|
||||
stringOverlap,
|
||||
highlightLogLine,
|
||||
} from '../../../../helpers/autoclassify';
|
||||
import { getBugUrl, getLogViewerUrl } from '../../../../helpers/url';
|
||||
import { withSelectedJob } from '../../../context/SelectedJob';
|
||||
|
||||
|
@ -127,8 +130,7 @@ class ErrorLine extends React.Component {
|
|||
return 'unverified-ignore';
|
||||
}
|
||||
|
||||
if (selectedOption.type === 'manual' &&
|
||||
!selectedOption.manualBugNumber) {
|
||||
if (selectedOption.type === 'manual' && !selectedOption.manualBugNumber) {
|
||||
return 'unverified-no-bug';
|
||||
}
|
||||
|
||||
|
@ -141,40 +143,53 @@ class ErrorLine extends React.Component {
|
|||
getOptions() {
|
||||
const bugSuggestions = [].concat(
|
||||
this.props.errorLine.data.bug_suggestions.bugs.open_recent,
|
||||
this.props.errorLine.data.bug_suggestions.bugs.all_others);
|
||||
this.props.errorLine.data.bug_suggestions.bugs.all_others,
|
||||
);
|
||||
const classificationMatches = this.getClassifiedFailureMatcher();
|
||||
const autoclassifyOptions = this.props.errorLine.data.classified_failures
|
||||
.filter(cf => cf.bug_number !== 0)
|
||||
.map(cf => new LineOptionModel(
|
||||
'classifiedFailure',
|
||||
`${this.props.errorLine.id}-${cf.id}`,
|
||||
cf.id,
|
||||
cf.bug_number,
|
||||
cf.bug ? cf.bug.summary : '',
|
||||
cf.bug ? cf.bug.resolution : '',
|
||||
classificationMatches(cf.id),
|
||||
));
|
||||
const autoclassifiedBugs = autoclassifyOptions
|
||||
.reduce((classifiedBugs, option) => classifiedBugs.add(option.bugNumber),
|
||||
new Set());
|
||||
.map(
|
||||
cf =>
|
||||
new LineOptionModel(
|
||||
'classifiedFailure',
|
||||
`${this.props.errorLine.id}-${cf.id}`,
|
||||
cf.id,
|
||||
cf.bug_number,
|
||||
cf.bug ? cf.bug.summary : '',
|
||||
cf.bug ? cf.bug.resolution : '',
|
||||
classificationMatches(cf.id),
|
||||
),
|
||||
);
|
||||
const autoclassifiedBugs = autoclassifyOptions.reduce(
|
||||
(classifiedBugs, option) => classifiedBugs.add(option.bugNumber),
|
||||
new Set(),
|
||||
);
|
||||
const bugSuggestionOptions = bugSuggestions
|
||||
.filter(bug => !autoclassifiedBugs.has(bug.id))
|
||||
.map(bugSuggestion => new LineOptionModel(
|
||||
'unstructuredBug',
|
||||
`${this.props.errorLine.id}-ub-${bugSuggestion.id}`,
|
||||
null,
|
||||
bugSuggestion.id,
|
||||
bugSuggestion.summary,
|
||||
bugSuggestion.resolution));
|
||||
.map(
|
||||
bugSuggestion =>
|
||||
new LineOptionModel(
|
||||
'unstructuredBug',
|
||||
`${this.props.errorLine.id}-ub-${bugSuggestion.id}`,
|
||||
null,
|
||||
bugSuggestion.id,
|
||||
bugSuggestion.summary,
|
||||
bugSuggestion.resolution,
|
||||
),
|
||||
);
|
||||
|
||||
this.bestOption = null;
|
||||
|
||||
// Look for an option that has been marked as the best classification.
|
||||
// This is always sorted first and never hidden, so we remove it and readd it.
|
||||
if (!this.bestIsIgnore()) {
|
||||
const bestIndex = this.props.errorLine.bestClassification ?
|
||||
autoclassifyOptions
|
||||
.findIndex(option => option.classifiedFailureId === this.props.errorLine.bestClassification.id) : -1;
|
||||
const bestIndex = this.props.errorLine.bestClassification
|
||||
? autoclassifyOptions.findIndex(
|
||||
option =>
|
||||
option.classifiedFailureId ===
|
||||
this.props.errorLine.bestClassification.id,
|
||||
)
|
||||
: -1;
|
||||
|
||||
if (bestIndex > -1) {
|
||||
this.bestOption = autoclassifyOptions[bestIndex];
|
||||
|
@ -199,8 +214,14 @@ class ErrorLine extends React.Component {
|
|||
* Build a list of the default options that apply to all lines.
|
||||
*/
|
||||
getExtraOptions() {
|
||||
const extraOptions = [new LineOptionModel('manual', `${this.props.errorLine.id}-manual`)];
|
||||
const ignoreOption = new LineOptionModel('ignore', `${this.props.errorLine.id}-ignore`, 0);
|
||||
const extraOptions = [
|
||||
new LineOptionModel('manual', `${this.props.errorLine.id}-manual`),
|
||||
];
|
||||
const ignoreOption = new LineOptionModel(
|
||||
'ignore',
|
||||
`${this.props.errorLine.id}-ignore`,
|
||||
0,
|
||||
);
|
||||
|
||||
extraOptions.push(ignoreOption);
|
||||
if (this.bestIsIgnore()) {
|
||||
|
@ -250,8 +271,9 @@ class ErrorLine extends React.Component {
|
|||
*/
|
||||
|
||||
// Get the test id for this line and the last line, if any
|
||||
const thisTest = failureLine ? failureLine.test :
|
||||
parseTest(errorLine.data.bug_suggestions.search);
|
||||
const thisTest = failureLine
|
||||
? failureLine.test
|
||||
: parseTest(errorLine.data.bug_suggestions.search);
|
||||
let prevTest;
|
||||
if (prevErrorLine) {
|
||||
prevTest = prevErrorLine.data.failure_line
|
||||
|
@ -274,16 +296,19 @@ class ErrorLine extends React.Component {
|
|||
// suggestions, we assume that is the signature line
|
||||
// and this is ignorable
|
||||
ignore = true;
|
||||
} else if (failureLine &&
|
||||
(failureLine.action === 'crash' ||
|
||||
failureLine.action === 'test_result')) {
|
||||
} else if (
|
||||
failureLine &&
|
||||
(failureLine.action === 'crash' || failureLine.action === 'test_result')
|
||||
) {
|
||||
// Don't ignore crashes or test results
|
||||
ignore = false;
|
||||
} else {
|
||||
// Don't ignore lines containing a well-known string
|
||||
let message;
|
||||
if (failureLine) {
|
||||
message = failureLine.signature ? failureLine.signature : failureLine.message;
|
||||
message = failureLine.signature
|
||||
? failureLine.signature
|
||||
: failureLine.message;
|
||||
} else {
|
||||
message = this.props.errorLine.data.bug_suggestions.search;
|
||||
}
|
||||
|
@ -312,13 +337,15 @@ class ErrorLine extends React.Component {
|
|||
}
|
||||
matchesByCF.get(match.classified_failure).push(match);
|
||||
return matchesByCF;
|
||||
}, new Map());
|
||||
},
|
||||
new Map(),
|
||||
);
|
||||
|
||||
const matchFunc = cf_id => matchesByCF.get(cf_id).map(
|
||||
match => ({
|
||||
matcher: match.matcher_name,
|
||||
score: match.score,
|
||||
}));
|
||||
const matchFunc = cf_id =>
|
||||
matchesByCF.get(cf_id).map(match => ({
|
||||
matcher: match.matcher_name,
|
||||
score: match.score,
|
||||
}));
|
||||
|
||||
return matchFunc.bind(this);
|
||||
}
|
||||
|
@ -329,11 +356,12 @@ class ErrorLine extends React.Component {
|
|||
// TODO: consider adding the update/create options back here, although it's
|
||||
// not clear anyone ever understood how they were supposed to work
|
||||
const { errorLine, setErrorLineInput } = this.props;
|
||||
const classifiedFailureId = ((this.bestOption &&
|
||||
const classifiedFailureId =
|
||||
this.bestOption &&
|
||||
this.bestOption.classifiedFailureId &&
|
||||
this.bestOption.bugNumber === null) ?
|
||||
this.bestOption.classifiedFailureId :
|
||||
option.classifiedFailureId);
|
||||
this.bestOption.bugNumber === null
|
||||
? this.bestOption.classifiedFailureId
|
||||
: option.classifiedFailureId;
|
||||
|
||||
let bug;
|
||||
if (option.type === 'manual') {
|
||||
|
@ -386,10 +414,11 @@ class ErrorLine extends React.Component {
|
|||
const bestScore = this.bestOption.score;
|
||||
|
||||
options.forEach((option, idx) => {
|
||||
option.hidden = idx > (minOptions - 1) &&
|
||||
option.hidden =
|
||||
idx > minOptions - 1 &&
|
||||
(option.score < lowerCutoff ||
|
||||
option.score < bestRatio * bestScore ||
|
||||
idx > (maxOptions - 1));
|
||||
idx > maxOptions - 1);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -399,22 +428,25 @@ class ErrorLine extends React.Component {
|
|||
* @param {Object[]} options - List of options to score
|
||||
*/
|
||||
scoreOptions(options) {
|
||||
options
|
||||
.forEach((option) => {
|
||||
let score;
|
||||
const { data } = this.props.errorLine;
|
||||
if (option.type === 'classifiedFailure') {
|
||||
score = parseFloat(
|
||||
data.matches.find(
|
||||
x => x.classified_failure === option.classifiedFailureId).score);
|
||||
} else {
|
||||
score = stringOverlap(data.bug_suggestions.search,
|
||||
option.bugSummary.replace(/^\s*Intermittent\s+/, ''));
|
||||
// Artificially reduce the score of resolved bugs
|
||||
score *= option.bugResolution ? 0.8 : 1;
|
||||
}
|
||||
option.score = score;
|
||||
});
|
||||
options.forEach(option => {
|
||||
let score;
|
||||
const { data } = this.props.errorLine;
|
||||
if (option.type === 'classifiedFailure') {
|
||||
score = parseFloat(
|
||||
data.matches.find(
|
||||
x => x.classified_failure === option.classifiedFailureId,
|
||||
).score,
|
||||
);
|
||||
} else {
|
||||
score = stringOverlap(
|
||||
data.bug_suggestions.search,
|
||||
option.bugSummary.replace(/^\s*Intermittent\s+/, ''),
|
||||
);
|
||||
// Artificially reduce the score of resolved bugs
|
||||
score *= option.bugResolution ? 0.8 : 1;
|
||||
}
|
||||
option.score = score;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -429,11 +461,15 @@ class ErrorLine extends React.Component {
|
|||
* Test if the initial best option is to ignore the line
|
||||
*/
|
||||
bestIsIgnore() {
|
||||
const { errorLine: { data: errorData } } = this.props;
|
||||
const {
|
||||
errorLine: { data: errorData },
|
||||
} = this.props;
|
||||
|
||||
if (errorData.metaData) {
|
||||
return (errorData.metadata.best_classification &&
|
||||
errorData.metadata.best_classification.bugNumber === 0);
|
||||
return (
|
||||
errorData.metadata.best_classification &&
|
||||
errorData.metadata.best_classification.bugNumber === 0
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -442,21 +478,37 @@ class ErrorLine extends React.Component {
|
|||
* Determine whether the line should be open for editing by default
|
||||
*/
|
||||
defaultEditable(option) {
|
||||
return option ? !(option.score >= GOOD_MATCH_SCORE || option.type === 'ignore') : false;
|
||||
return option
|
||||
? !(option.score >= GOOD_MATCH_SCORE || option.type === 'ignore')
|
||||
: false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
errorLine, selectedJob, canClassify, isSelected, isEditable, setEditable,
|
||||
toggleSelect, repoName,
|
||||
errorLine,
|
||||
selectedJob,
|
||||
canClassify,
|
||||
isSelected,
|
||||
isEditable,
|
||||
setEditable,
|
||||
toggleSelect,
|
||||
repoName,
|
||||
} = this.props;
|
||||
const {
|
||||
messageExpanded, showHidden, selectedOption, options, extraOptions,
|
||||
messageExpanded,
|
||||
showHidden,
|
||||
selectedOption,
|
||||
options,
|
||||
extraOptions,
|
||||
} = this.state;
|
||||
|
||||
const failureLine = errorLine.data.metadata.failure_line;
|
||||
const searchLine = errorLine.data.bug_suggestions.search;
|
||||
const logUrl = getLogViewerUrl(selectedJob.id, repoName, errorLine.data.line_number + 1);
|
||||
const logUrl = getLogViewerUrl(
|
||||
selectedJob.id,
|
||||
repoName,
|
||||
errorLine.data.line_number + 1,
|
||||
);
|
||||
const status = this.getStatus();
|
||||
|
||||
return (
|
||||
|
@ -465,131 +517,215 @@ class ErrorLine extends React.Component {
|
|||
onClick={evt => toggleSelect(evt, errorLine)}
|
||||
>
|
||||
<div className={status}> </div>
|
||||
{errorLine.verified && <div>
|
||||
{!errorLine.verifiedIgnore && <span
|
||||
className="badge badge-xs badge-primary"
|
||||
title="This line is verified"
|
||||
>Verified</span>}
|
||||
{errorLine.verifiedIgnore && <span
|
||||
className="badge badge-xs badge-ignored"
|
||||
title="This line is ignored"
|
||||
>Ignored</span>}
|
||||
</div>}
|
||||
{errorLine.verified && (
|
||||
<div>
|
||||
{!errorLine.verifiedIgnore && (
|
||||
<span
|
||||
className="badge badge-xs badge-primary"
|
||||
title="This line is verified"
|
||||
>
|
||||
Verified
|
||||
</span>
|
||||
)}
|
||||
{errorLine.verifiedIgnore && (
|
||||
<span
|
||||
className="badge badge-xs badge-ignored"
|
||||
title="This line is ignored"
|
||||
>
|
||||
Ignored
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="classification-line-detail">
|
||||
{failureLine && <div>
|
||||
{failureLine.action === 'test_result' && <span>
|
||||
<span className={errorLine.verifiedIgnore ? 'ignored-line' : ''}>
|
||||
<strong className="failure-line-status">{failureLine.status}</strong>
|
||||
{failureLine.expected !== 'PASS' && failureLine.expected !== 'OK' && <span>
|
||||
(expected <strong>{failureLine.expected}</strong>)
|
||||
</span>} | <strong>{failureLine.test}</strong>
|
||||
{failureLine.subtest && <span>| {failureLine.subtest}</span>}
|
||||
</span>
|
||||
{failureLine.message && !errorLine.verifiedIgnore &&
|
||||
<div className="failure-line-message">
|
||||
{failureLine && (
|
||||
<div>
|
||||
{failureLine.action === 'test_result' && (
|
||||
<span>
|
||||
<span
|
||||
className={`failure-line-message-toggle fa fa-fw fa-lg${messageExpanded ? 'fa-caret-down' : 'fa-carat-right'}`}
|
||||
onClick={() => this.setState({ messageExpanded: !messageExpanded })}
|
||||
/>
|
||||
{messageExpanded ?
|
||||
<span className="failure-line-message-expanded">{failureLine.message}</span> :
|
||||
<span className="failure-line-message-collapsed">{failureLine.message}</span>}
|
||||
</div>}
|
||||
</span>}
|
||||
{failureLine.action === 'log' && <span>
|
||||
className={errorLine.verifiedIgnore ? 'ignored-line' : ''}
|
||||
>
|
||||
<strong className="failure-line-status">
|
||||
{failureLine.status}
|
||||
</strong>
|
||||
{failureLine.expected !== 'PASS' &&
|
||||
failureLine.expected !== 'OK' && (
|
||||
<span>
|
||||
(expected <strong>{failureLine.expected}</strong>)
|
||||
</span>
|
||||
)}{' '}
|
||||
| <strong>{failureLine.test}</strong>
|
||||
{failureLine.subtest && (
|
||||
<span>| {failureLine.subtest}</span>
|
||||
)}
|
||||
</span>
|
||||
{failureLine.message && !errorLine.verifiedIgnore && (
|
||||
<div className="failure-line-message">
|
||||
<span
|
||||
className={`failure-line-message-toggle fa fa-fw fa-lg${
|
||||
messageExpanded ? 'fa-caret-down' : 'fa-carat-right'
|
||||
}`}
|
||||
onClick={() =>
|
||||
this.setState({ messageExpanded: !messageExpanded })
|
||||
}
|
||||
/>
|
||||
{messageExpanded ? (
|
||||
<span className="failure-line-message-expanded">
|
||||
{failureLine.message}
|
||||
</span>
|
||||
) : (
|
||||
<span className="failure-line-message-collapsed">
|
||||
{failureLine.message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{failureLine.action === 'log' && (
|
||||
<span>
|
||||
LOG {failureLine.level} | {failureLine.message}
|
||||
</span>}
|
||||
{failureLine.action === 'crash' && <span>
|
||||
</span>
|
||||
)}
|
||||
{failureLine.action === 'crash' && (
|
||||
<span>
|
||||
CRASH |
|
||||
{failureLine.test && <span><strong>{failureLine.test}</strong> |
|
||||
</span>}
|
||||
{failureLine.test && (
|
||||
<span>
|
||||
<strong>{failureLine.test}</strong> |
|
||||
</span>
|
||||
)}
|
||||
application crashed [{failureLine.signature}]
|
||||
</span>}
|
||||
<span> [<a
|
||||
title="Open the log viewer in a new window"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={logUrl}
|
||||
className=""
|
||||
>log…</a>]</span>
|
||||
</div>}
|
||||
{!failureLine && <div>
|
||||
{highlightLogLine(searchLine)}
|
||||
<span> [<a
|
||||
title="Open the log viewer in a new window"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={logUrl}
|
||||
>log…</a>]</span>
|
||||
</div>}
|
||||
|
||||
{errorLine.verified && !errorLine.verifiedIgnore && <div>
|
||||
<span className="fa fa-star best-classification-star" />
|
||||
{errorLine.bugNumber && <span className="line-option-text">
|
||||
<a
|
||||
href={getBugUrl(errorLine.bugNumber)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Bug {errorLine.bugNumber} - {errorLine.bugSummary && <span>{errorLine.bugSummary}</span>}</a>
|
||||
{!errorLine.bugNumber && <span className="line-option-text">
|
||||
Classifed failure with no bug number
|
||||
</span>}
|
||||
</span>}
|
||||
</div>}
|
||||
|
||||
{((!errorLine.verified && isEditable) || !canClassify) && <div>
|
||||
<FormGroup>
|
||||
<ul className="list-unstyled">
|
||||
{options.map(option => (
|
||||
(showHidden || !option.hidden) && <li key={option.id}>
|
||||
<LineOption
|
||||
errorLine={errorLine}
|
||||
optionModel={option}
|
||||
selectedOption={selectedOption}
|
||||
canClassify={canClassify}
|
||||
onOptionChange={this.onOptionChange}
|
||||
ignoreAlways={option.ignoreAlways}
|
||||
/>
|
||||
</li>))}
|
||||
</ul>
|
||||
{this.hasHidden(options) &&
|
||||
</span>
|
||||
)}
|
||||
<span>
|
||||
{' '}
|
||||
[
|
||||
<a
|
||||
onClick={() => this.setState({ showHidden: !this.state.showHidden })}
|
||||
className="link-style has-hidden"
|
||||
>{!showHidden ? <span>More…</span> : <span>Fewer</span>}</a>
|
||||
}
|
||||
{canClassify && <ul className="list-unstyled extra-options">
|
||||
{/* classification options for line */}
|
||||
{extraOptions.map(option => (
|
||||
<li key={option.id}>
|
||||
<LineOption
|
||||
errorLine={errorLine}
|
||||
optionModel={option}
|
||||
selectedOption={selectedOption}
|
||||
canClassify={canClassify}
|
||||
onOptionChange={this.onOptionChange}
|
||||
onIgnoreAlwaysChange={this.onIgnoreAlwaysChange}
|
||||
onManualBugNumberChange={this.onManualBugNumberChange}
|
||||
manualBugNumber={option.manualBugNumber}
|
||||
ignoreAlways={option.ignoreAlways}
|
||||
/>
|
||||
</li>))}
|
||||
</ul>}
|
||||
</FormGroup>
|
||||
</div>}
|
||||
title="Open the log viewer in a new window"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={logUrl}
|
||||
className=""
|
||||
>
|
||||
log…
|
||||
</a>
|
||||
]
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{!failureLine && (
|
||||
<div>
|
||||
{highlightLogLine(searchLine)}
|
||||
<span>
|
||||
{' '}
|
||||
[
|
||||
<a
|
||||
title="Open the log viewer in a new window"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={logUrl}
|
||||
>
|
||||
log…
|
||||
</a>
|
||||
]
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!errorLine.verified && !isEditable && canClassify && <div>
|
||||
<StaticLineOption
|
||||
errorLine={errorLine}
|
||||
option={selectedOption}
|
||||
numOptions={options.length}
|
||||
canClassify={canClassify}
|
||||
setEditable={setEditable}
|
||||
ignoreAlways={selectedOption.ignoreAlways}
|
||||
manualBugNumber={selectedOption.manualBugNumber}
|
||||
/>
|
||||
</div>}
|
||||
{errorLine.verified && !errorLine.verifiedIgnore && (
|
||||
<div>
|
||||
<span className="fa fa-star best-classification-star" />
|
||||
{errorLine.bugNumber && (
|
||||
<span className="line-option-text">
|
||||
<a
|
||||
href={getBugUrl(errorLine.bugNumber)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Bug {errorLine.bugNumber} -{' '}
|
||||
{errorLine.bugSummary && (
|
||||
<span>{errorLine.bugSummary}</span>
|
||||
)}
|
||||
</a>
|
||||
{!errorLine.bugNumber && (
|
||||
<span className="line-option-text">
|
||||
Classifed failure with no bug number
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{((!errorLine.verified && isEditable) || !canClassify) && (
|
||||
<div>
|
||||
<FormGroup>
|
||||
<ul className="list-unstyled">
|
||||
{options.map(
|
||||
option =>
|
||||
(showHidden || !option.hidden) && (
|
||||
<li key={option.id}>
|
||||
<LineOption
|
||||
errorLine={errorLine}
|
||||
optionModel={option}
|
||||
selectedOption={selectedOption}
|
||||
canClassify={canClassify}
|
||||
onOptionChange={this.onOptionChange}
|
||||
ignoreAlways={option.ignoreAlways}
|
||||
/>
|
||||
</li>
|
||||
),
|
||||
)}
|
||||
</ul>
|
||||
{this.hasHidden(options) && (
|
||||
<a
|
||||
onClick={() =>
|
||||
this.setState({ showHidden: !this.state.showHidden })
|
||||
}
|
||||
className="link-style has-hidden"
|
||||
>
|
||||
{!showHidden ? <span>More…</span> : <span>Fewer</span>}
|
||||
</a>
|
||||
)}
|
||||
{canClassify && (
|
||||
<ul className="list-unstyled extra-options">
|
||||
{/* classification options for line */}
|
||||
{extraOptions.map(option => (
|
||||
<li key={option.id}>
|
||||
<LineOption
|
||||
errorLine={errorLine}
|
||||
optionModel={option}
|
||||
selectedOption={selectedOption}
|
||||
canClassify={canClassify}
|
||||
onOptionChange={this.onOptionChange}
|
||||
onIgnoreAlwaysChange={this.onIgnoreAlwaysChange}
|
||||
onManualBugNumberChange={this.onManualBugNumberChange}
|
||||
manualBugNumber={option.manualBugNumber}
|
||||
ignoreAlways={option.ignoreAlways}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</FormGroup>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!errorLine.verified && !isEditable && canClassify && (
|
||||
<div>
|
||||
<StaticLineOption
|
||||
errorLine={errorLine}
|
||||
option={selectedOption}
|
||||
numOptions={options.length}
|
||||
canClassify={canClassify}
|
||||
setEditable={setEditable}
|
||||
ignoreAlways={selectedOption.ignoreAlways}
|
||||
manualBugNumber={selectedOption.manualBugNumber}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -9,19 +9,25 @@ export default class ErrorLineModel {
|
|||
// an actual boolean later
|
||||
if (line.metadata) {
|
||||
this.verified = line.metadata.best_is_verified;
|
||||
this.bestClassification = line.metadata.best_classification ?
|
||||
line.classified_failures
|
||||
.find(cf => cf.id === line.metadata.best_classification) : null;
|
||||
this.bestClassification = line.metadata.best_classification
|
||||
? line.classified_failures.find(
|
||||
cf => cf.id === line.metadata.best_classification,
|
||||
)
|
||||
: null;
|
||||
} else {
|
||||
this.verified = false;
|
||||
this.bestClassification = null;
|
||||
line.metadata = {};
|
||||
}
|
||||
this.bugNumber = this.bestClassification ?
|
||||
this.bestClassification.bug_number : null;
|
||||
this.verifiedIgnore = this.verified && (this.bugNumber === 0 ||
|
||||
this.bestClassification === null);
|
||||
this.bugSummary = (this.bestClassification && this.bestClassification.bug) ?
|
||||
this.bestClassification.bug.summary : null;
|
||||
this.bugNumber = this.bestClassification
|
||||
? this.bestClassification.bug_number
|
||||
: null;
|
||||
this.verifiedIgnore =
|
||||
this.verified &&
|
||||
(this.bugNumber === 0 || this.bestClassification === null);
|
||||
this.bugSummary =
|
||||
this.bestClassification && this.bestClassification.bug
|
||||
? this.bestClassification.bug.summary
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,11 @@ import Highlighter from 'react-highlight-words';
|
|||
|
||||
import { getSearchWords } from '../../../../helpers/display';
|
||||
import { isReftest } from '../../../../helpers/job';
|
||||
import { getBugUrl, getLogViewerUrl, getReftestUrl } from '../../../../helpers/url';
|
||||
import {
|
||||
getBugUrl,
|
||||
getLogViewerUrl,
|
||||
getReftestUrl,
|
||||
} from '../../../../helpers/url';
|
||||
import BugFiler from '../../BugFiler';
|
||||
import { thEvents } from '../../../../helpers/constants';
|
||||
import { getAllUrlParams } from '../../../../helpers/location';
|
||||
|
@ -81,52 +85,77 @@ class LineOption extends React.Component {
|
|||
return (
|
||||
<div className="classification-option">
|
||||
<span className="classification-icon">
|
||||
{option.isBest ?
|
||||
<span className="fa fa-star-o" title="Autoclassifier best match" /> :
|
||||
<span className="classification-no-icon"> </span>}
|
||||
{option.isBest ? (
|
||||
<span className="fa fa-star-o" title="Autoclassifier best match" />
|
||||
) : (
|
||||
<span className="classification-no-icon"> </span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
<FormGroup check>
|
||||
<Label check>
|
||||
{!(option.type === 'classifiedFailure' && !option.bugNumber) && <Input
|
||||
type="radio"
|
||||
value={option}
|
||||
id={option.id}
|
||||
checked={selectedOption.id === option.id}
|
||||
name={errorLine.id}
|
||||
onChange={() => onOptionChange(option)}
|
||||
className={canClassify ? '' : 'hidden'}
|
||||
/>}
|
||||
{!!option.bugNumber && <span className="line-option-text">
|
||||
{(!canClassify || selectedJob.id in pinnedJobs) &&
|
||||
<button
|
||||
className="btn btn-xs btn-light-bordered"
|
||||
onClick={() => addBug({ id: option.bugNumber }, selectedJob)}
|
||||
title="add to list of bugs to associate with all pinned jobs"
|
||||
><i className="fa fa-thumb-tack" /></button>}
|
||||
{!!option.bugResolution &&
|
||||
<span className="classification-bug-resolution"> [{option.bugResolution}] </span>}
|
||||
<a
|
||||
href={getBugUrl(option.bugNumber)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{option.bugNumber} -
|
||||
<Highlighter
|
||||
searchWords={getSearchWords(errorLine.data.bug_suggestions.search)}
|
||||
textToHighlight={option.bugSummary}
|
||||
caseSensitive
|
||||
highlightTag="strong"
|
||||
/>
|
||||
</a>
|
||||
<span> [ {Number.parseFloat(option.score).toPrecision(2)} ]</span>
|
||||
</span>}
|
||||
{!(option.type === 'classifiedFailure' && !option.bugNumber) && (
|
||||
<Input
|
||||
type="radio"
|
||||
value={option}
|
||||
id={option.id}
|
||||
checked={selectedOption.id === option.id}
|
||||
name={errorLine.id}
|
||||
onChange={() => onOptionChange(option)}
|
||||
className={canClassify ? '' : 'hidden'}
|
||||
/>
|
||||
)}
|
||||
{!!option.bugNumber && (
|
||||
<span className="line-option-text">
|
||||
{(!canClassify || selectedJob.id in pinnedJobs) && (
|
||||
<button
|
||||
className="btn btn-xs btn-light-bordered"
|
||||
onClick={() =>
|
||||
addBug({ id: option.bugNumber }, selectedJob)
|
||||
}
|
||||
title="add to list of bugs to associate with all pinned jobs"
|
||||
>
|
||||
<i className="fa fa-thumb-tack" />
|
||||
</button>
|
||||
)}
|
||||
{!!option.bugResolution && (
|
||||
<span className="classification-bug-resolution">
|
||||
{' '}
|
||||
[{option.bugResolution}]{' '}
|
||||
</span>
|
||||
)}
|
||||
<a
|
||||
href={getBugUrl(option.bugNumber)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{option.bugNumber} -
|
||||
<Highlighter
|
||||
searchWords={getSearchWords(
|
||||
errorLine.data.bug_suggestions.search,
|
||||
)}
|
||||
textToHighlight={option.bugSummary}
|
||||
caseSensitive
|
||||
highlightTag="strong"
|
||||
/>
|
||||
</a>
|
||||
<span>
|
||||
{' '}
|
||||
[ {Number.parseFloat(option.score).toPrecision(2)} ]
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{option.type === 'classifiedFailure' && !option.bugNumber && <span>
|
||||
Autoclassified failure with no associated bug number
|
||||
</span>}
|
||||
{option.type === 'classifiedFailure' && !option.bugNumber && (
|
||||
<span>Autoclassified failure with no associated bug number</span>
|
||||
)}
|
||||
|
||||
{option.type === 'manual' &&
|
||||
<div className={`line-option-text manual-bug ${!canClassify ? 'hidden' : ''}`}>
|
||||
{option.type === 'manual' && (
|
||||
<div
|
||||
className={`line-option-text manual-bug ${
|
||||
!canClassify ? 'hidden' : ''
|
||||
}`}
|
||||
>
|
||||
Other bug:
|
||||
<Input
|
||||
className="manual-bug-input"
|
||||
|
@ -135,63 +164,84 @@ class LineOption extends React.Component {
|
|||
size="7"
|
||||
placeholder="Number"
|
||||
value={manualBugNumber}
|
||||
onChange={evt => onManualBugNumberChange(option, evt.target.value)}
|
||||
onChange={evt =>
|
||||
onManualBugNumberChange(option, evt.target.value)
|
||||
}
|
||||
/>
|
||||
<a
|
||||
className="btn btn-xs btn-light-bordered btn-file-bug"
|
||||
onClick={() => this.fileBug()}
|
||||
title="File a bug for this failure"
|
||||
><i className="fa fa-bug" /></a>
|
||||
>
|
||||
<i className="fa fa-bug" />
|
||||
</a>
|
||||
{option.id === 'manual' && !!option.manualBugNumber && (
|
||||
<a
|
||||
href={getBugUrl(option.manualBugNumber)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
[view]
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{option.id === 'manual' && !!option.manualBugNumber &&
|
||||
<a
|
||||
href={getBugUrl(option.manualBugNumber)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>[view]</a>}
|
||||
</div>}
|
||||
|
||||
{option.type === 'ignore' && <span
|
||||
className={`line-option-text ignore ${canClassify ? '' : 'hidden'}`}
|
||||
>Ignore line
|
||||
<Select
|
||||
value={ignoreAlways}
|
||||
clearable={false}
|
||||
classNamePrefix="ignore-option"
|
||||
onChange={onIgnoreAlwaysChange}
|
||||
bsSize="small"
|
||||
options={[
|
||||
{ value: false, label: 'Here only' },
|
||||
{ value: true, label: 'For future classifications' },
|
||||
]}
|
||||
/>
|
||||
</span>}
|
||||
{option.type === 'ignore' && (
|
||||
<span
|
||||
className={`line-option-text ignore ${
|
||||
canClassify ? '' : 'hidden'
|
||||
}`}
|
||||
>
|
||||
Ignore line
|
||||
<Select
|
||||
value={ignoreAlways}
|
||||
clearable={false}
|
||||
classNamePrefix="ignore-option"
|
||||
onChange={onIgnoreAlwaysChange}
|
||||
bsSize="small"
|
||||
options={[
|
||||
{ value: false, label: 'Here only' },
|
||||
{ value: true, label: 'For future classifications' },
|
||||
]}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
</FormGroup>
|
||||
|
||||
{option.type === 'classifiedFailure' && <div className="classification-matchers">
|
||||
Matched by:
|
||||
{option.matches && option.matches.map(match => (<span key={match.matcher_name}>
|
||||
{match.matcher_name} ({match.score})
|
||||
</span>))}
|
||||
</div>}
|
||||
{isBugFilerOpen && <BugFiler
|
||||
isOpen={isBugFilerOpen}
|
||||
toggle={this.toggleBugFiler}
|
||||
suggestion={errorLine.data.bug_suggestions}
|
||||
suggestions={[errorLine.data.bug_suggestions]}
|
||||
fullLog={logUrl}
|
||||
parsedLog={`${window.location.origin}/${getLogViewerUrl(selectedJob.id, repoName)}`}
|
||||
reftestUrl={isReftest(selectedJob) ? getReftestUrl(logUrl) : ''}
|
||||
successCallback={this.bugFilerCallback}
|
||||
jobGroupName={selectedJob.job_group_name}
|
||||
/>}
|
||||
{option.type === 'classifiedFailure' && (
|
||||
<div className="classification-matchers">
|
||||
Matched by:
|
||||
{option.matches &&
|
||||
option.matches.map(match => (
|
||||
<span key={match.matcher_name}>
|
||||
{match.matcher_name} ({match.score})
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{isBugFilerOpen && (
|
||||
<BugFiler
|
||||
isOpen={isBugFilerOpen}
|
||||
toggle={this.toggleBugFiler}
|
||||
suggestion={errorLine.data.bug_suggestions}
|
||||
suggestions={[errorLine.data.bug_suggestions]}
|
||||
fullLog={logUrl}
|
||||
parsedLog={`${window.location.origin}/${getLogViewerUrl(
|
||||
selectedJob.id,
|
||||
repoName,
|
||||
)}`}
|
||||
reftestUrl={isReftest(selectedJob) ? getReftestUrl(logUrl) : ''}
|
||||
successCallback={this.bugFilerCallback}
|
||||
jobGroupName={selectedJob.job_group_name}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
LineOption.propTypes = {
|
||||
selectedJob: PropTypes.object.isRequired,
|
||||
errorLine: PropTypes.object.isRequired,
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
import { extendProperties } from '../../../../helpers/object';
|
||||
|
||||
export default class LineOptionModel {
|
||||
constructor(type, id, classifiedFailureId, bugNumber,
|
||||
bugSummary, bugResolution, matches) {
|
||||
constructor(
|
||||
type,
|
||||
id,
|
||||
classifiedFailureId,
|
||||
bugNumber,
|
||||
bugSummary,
|
||||
bugResolution,
|
||||
matches,
|
||||
) {
|
||||
extendProperties(this, {
|
||||
type,
|
||||
id,
|
||||
|
|
|
@ -12,66 +12,100 @@ import { withPinnedJobs } from '../../../context/PinnedJobs';
|
|||
*/
|
||||
function StaticLineOption(props) {
|
||||
const {
|
||||
selectedJob, canClassify, errorLine, option, numOptions, setEditable, ignoreAlways,
|
||||
manualBugNumber, pinnedJobs, addBug,
|
||||
selectedJob,
|
||||
canClassify,
|
||||
errorLine,
|
||||
option,
|
||||
numOptions,
|
||||
setEditable,
|
||||
ignoreAlways,
|
||||
manualBugNumber,
|
||||
pinnedJobs,
|
||||
addBug,
|
||||
} = props;
|
||||
|
||||
const optionCount = numOptions - 1;
|
||||
const ignoreAlwaysText = ignoreAlways ? 'for future classifications' : 'here only';
|
||||
const ignoreAlwaysText = ignoreAlways
|
||||
? 'for future classifications'
|
||||
: 'here only';
|
||||
|
||||
return (
|
||||
<div className="static-classification-option">
|
||||
<div className="classification-icon">
|
||||
{option.isBest ?
|
||||
<span className="fa fa-star-o" title="Autoclassifier best match" /> :
|
||||
<span className="classification-no-icon"> </span>}
|
||||
{option.isBest ? (
|
||||
<span className="fa fa-star-o" title="Autoclassifier best match" />
|
||||
) : (
|
||||
<span className="classification-no-icon"> </span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!!option.bugNumber && <span className="line-option-text">
|
||||
{(!canClassify || selectedJob.id in pinnedJobs) &&
|
||||
<button
|
||||
className="btn btn-xs btn-light-bordered"
|
||||
onClick={() => addBug({ id: option.bugNumber }, selectedJob)}
|
||||
title="add to list of bugs to associate with all pinned jobs"
|
||||
><i className="fa fa-thumb-tack" /></button>}
|
||||
{!!option.bugResolution &&
|
||||
<span className="classification-bug-resolution">[{option.bugResolution}]</span>}
|
||||
<a
|
||||
href={getBugUrl(option.bugNumber)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{option.bugNumber} -
|
||||
<Highlighter
|
||||
searchWords={getSearchWords(errorLine.data.bug_suggestions.search)}
|
||||
textToHighlight={option.bugSummary}
|
||||
caseSensitive
|
||||
highlightTag="strong"
|
||||
/>
|
||||
</a>
|
||||
<span>[ {Number.parseFloat(option.score).toPrecision(2)} ]</span>
|
||||
</span>}
|
||||
{!!option.bugNumber && (
|
||||
<span className="line-option-text">
|
||||
{(!canClassify || selectedJob.id in pinnedJobs) && (
|
||||
<button
|
||||
className="btn btn-xs btn-light-bordered"
|
||||
onClick={() => addBug({ id: option.bugNumber }, selectedJob)}
|
||||
title="add to list of bugs to associate with all pinned jobs"
|
||||
>
|
||||
<i className="fa fa-thumb-tack" />
|
||||
</button>
|
||||
)}
|
||||
{!!option.bugResolution && (
|
||||
<span className="classification-bug-resolution">
|
||||
[{option.bugResolution}]
|
||||
</span>
|
||||
)}
|
||||
<a
|
||||
href={getBugUrl(option.bugNumber)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{option.bugNumber} -
|
||||
<Highlighter
|
||||
searchWords={getSearchWords(
|
||||
errorLine.data.bug_suggestions.search,
|
||||
)}
|
||||
textToHighlight={option.bugSummary}
|
||||
caseSensitive
|
||||
highlightTag="strong"
|
||||
/>
|
||||
</a>
|
||||
<span>[ {Number.parseFloat(option.score).toPrecision(2)} ]</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{option.type === 'classifiedFailure' && !option.bugNumber && <span>
|
||||
Autoclassified failure with no associated bug number
|
||||
</span>}
|
||||
{option.type === 'classifiedFailure' && !option.bugNumber && (
|
||||
<span>Autoclassified failure with no associated bug number</span>
|
||||
)}
|
||||
|
||||
{option.type === 'manual' && <span className="line-option-text">
|
||||
Bug
|
||||
{!!manualBugNumber && <a
|
||||
href={getBugUrl(option.manualBugNumber)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{manualBugNumber}</a>}
|
||||
{!!manualBugNumber && <span>No bug number specified</span>}
|
||||
</span>}
|
||||
{option.type === 'manual' && (
|
||||
<span className="line-option-text">
|
||||
Bug
|
||||
{!!manualBugNumber && (
|
||||
<a
|
||||
href={getBugUrl(option.manualBugNumber)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{manualBugNumber}
|
||||
</a>
|
||||
)}
|
||||
{!!manualBugNumber && <span>No bug number specified</span>}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{option.type === 'ignore' &&
|
||||
<span className="line-option-text">Ignore {ignoreAlwaysText}</span>}
|
||||
{optionCount > 0 && <span>, {optionCount} other {optionCount === 1 ? 'option' : 'options'}
|
||||
|
||||
</span>}
|
||||
{option.type === 'ignore' && (
|
||||
<span className="line-option-text">Ignore {ignoreAlwaysText}</span>
|
||||
)}
|
||||
{optionCount > 0 && (
|
||||
<span>
|
||||
, {optionCount} other {optionCount === 1 ? 'option' : 'options'}
|
||||
</span>
|
||||
)}
|
||||
<div>
|
||||
<a onClick={setEditable} className="link-style">Edit…</a>
|
||||
<a onClick={setEditable} className="link-style">
|
||||
Edit…
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -7,11 +7,8 @@ import { getBugUrl } from '../../../../helpers/url';
|
|||
import { withSelectedJob } from '../../../context/SelectedJob';
|
||||
import { withPinnedJobs } from '../../../context/PinnedJobs';
|
||||
|
||||
|
||||
function BugListItem(props) {
|
||||
const {
|
||||
bug, suggestion, bugClassName, title, selectedJob, addBug,
|
||||
} = props;
|
||||
const { bug, suggestion, bugClassName, title, selectedJob, addBug } = props;
|
||||
const bugUrl = getBugUrl(bug.id);
|
||||
|
||||
return (
|
||||
|
@ -29,7 +26,8 @@ function BugListItem(props) {
|
|||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title={title}
|
||||
>{bug.id}
|
||||
>
|
||||
{bug.id}
|
||||
<Highlighter
|
||||
className={`${bugClassName} ml-1`}
|
||||
searchWords={getSearchWords(suggestion.search)}
|
||||
|
|
|
@ -5,19 +5,23 @@ export default function ErrorsList(props) {
|
|||
const errorListItem = props.errors.map((error, key) => (
|
||||
<li
|
||||
key={key} // eslint-disable-line react/no-array-index-key
|
||||
>{error.name} : {error.result}.
|
||||
>
|
||||
{error.name} : {error.result}.
|
||||
<a
|
||||
title="Open in Log Viewer"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={error.logViewerUrl}
|
||||
><span className="ml-1">View log</span></a>
|
||||
>
|
||||
<span className="ml-1">View log</span>
|
||||
</a>
|
||||
</li>
|
||||
));
|
||||
|
||||
return (
|
||||
<li>
|
||||
No Bug Suggestions Available.<br />
|
||||
No Bug Suggestions Available.
|
||||
<br />
|
||||
<span className="font-weight-bold">Unsuccessful Execution Steps</span>
|
||||
<ul>{errorListItem}</ul>
|
||||
</li>
|
||||
|
|
|
@ -51,74 +51,104 @@ class FailureSummaryTab extends React.Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
jobLogUrls, logParseStatus, suggestions, errors, logViewerFullUrl,
|
||||
bugSuggestionsLoading, selectedJob, reftestUrl,
|
||||
jobLogUrls,
|
||||
logParseStatus,
|
||||
suggestions,
|
||||
errors,
|
||||
logViewerFullUrl,
|
||||
bugSuggestionsLoading,
|
||||
selectedJob,
|
||||
reftestUrl,
|
||||
} = this.props;
|
||||
const { isBugFilerOpen, suggestion } = this.state;
|
||||
const logs = jobLogUrls;
|
||||
const jobLogsAllParsed = logs.every(jlu => (jlu.parse_status !== 'pending'));
|
||||
const jobLogsAllParsed = logs.every(jlu => jlu.parse_status !== 'pending');
|
||||
|
||||
return (
|
||||
<div className="w-100 h-100">
|
||||
<ul className="list-unstyled failure-summary-list" ref={this.fsMount}>
|
||||
{suggestions.map((suggestion, index) =>
|
||||
(<SuggestionsListItem
|
||||
{suggestions.map((suggestion, index) => (
|
||||
<SuggestionsListItem
|
||||
key={index} // eslint-disable-line react/no-array-index-key
|
||||
index={index}
|
||||
suggestion={suggestion}
|
||||
toggleBugFiler={() => this.fileBug(suggestion)}
|
||||
/>))}
|
||||
/>
|
||||
))}
|
||||
|
||||
{!!errors.length &&
|
||||
<ErrorsList errors={errors} />}
|
||||
{!!errors.length && <ErrorsList errors={errors} />}
|
||||
|
||||
{!bugSuggestionsLoading && jobLogsAllParsed &&
|
||||
!logs.length && !suggestions.length && !errors.length &&
|
||||
<ListItem text="Failure summary is empty" />}
|
||||
{!bugSuggestionsLoading &&
|
||||
jobLogsAllParsed &&
|
||||
!logs.length &&
|
||||
!suggestions.length &&
|
||||
!errors.length && <ListItem text="Failure summary is empty" />}
|
||||
|
||||
{!bugSuggestionsLoading && jobLogsAllParsed && !!logs.length &&
|
||||
logParseStatus === 'success' &&
|
||||
<li>
|
||||
<p className="failure-summary-line-empty mb-0">Log parsing complete. Generating bug suggestions.<br />
|
||||
<span>The content of this panel will refresh in 5 seconds.</span></p>
|
||||
</li>}
|
||||
{!bugSuggestionsLoading &&
|
||||
jobLogsAllParsed &&
|
||||
!!logs.length &&
|
||||
logParseStatus === 'success' && (
|
||||
<li>
|
||||
<p className="failure-summary-line-empty mb-0">
|
||||
Log parsing complete. Generating bug suggestions.
|
||||
<br />
|
||||
<span>
|
||||
The content of this panel will refresh in 5 seconds.
|
||||
</span>
|
||||
</p>
|
||||
</li>
|
||||
)}
|
||||
|
||||
{!bugSuggestionsLoading && !jobLogsAllParsed &&
|
||||
logs.map(jobLog =>
|
||||
(<li key={jobLog.id}>
|
||||
<p className="failure-summary-line-empty mb-0">Log parsing in progress.<br />
|
||||
<a
|
||||
title="Open the raw log in a new window"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={jobLog.url}
|
||||
>The raw log</a> is available. This panel will automatically recheck every 5 seconds.</p>
|
||||
</li>))}
|
||||
{!bugSuggestionsLoading &&
|
||||
!jobLogsAllParsed &&
|
||||
logs.map(jobLog => (
|
||||
<li key={jobLog.id}>
|
||||
<p className="failure-summary-line-empty mb-0">
|
||||
Log parsing in progress.
|
||||
<br />
|
||||
<a
|
||||
title="Open the raw log in a new window"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={jobLog.url}
|
||||
>
|
||||
The raw log
|
||||
</a>{' '}
|
||||
is available. This panel will automatically recheck every 5
|
||||
seconds.
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
|
||||
{!bugSuggestionsLoading && logParseStatus === 'failed' &&
|
||||
<ListItem text="Log parsing failed. Unable to generate failure summary." />}
|
||||
{!bugSuggestionsLoading && logParseStatus === 'failed' && (
|
||||
<ListItem text="Log parsing failed. Unable to generate failure summary." />
|
||||
)}
|
||||
|
||||
{!bugSuggestionsLoading && !logs.length &&
|
||||
<ListItem text="No logs available for this job." />}
|
||||
{!bugSuggestionsLoading && !logs.length && (
|
||||
<ListItem text="No logs available for this job." />
|
||||
)}
|
||||
|
||||
{bugSuggestionsLoading &&
|
||||
{bugSuggestionsLoading && (
|
||||
<div className="overlay">
|
||||
<div>
|
||||
<span className="fa fa-spinner fa-pulse th-spinner-lg" />
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
)}
|
||||
</ul>
|
||||
{isBugFilerOpen && <BugFiler
|
||||
isOpen={isBugFilerOpen}
|
||||
toggle={this.toggleBugFiler}
|
||||
suggestion={suggestion}
|
||||
suggestions={suggestions}
|
||||
fullLog={jobLogUrls[0].url}
|
||||
parsedLog={logViewerFullUrl}
|
||||
reftestUrl={isReftest(selectedJob) ? reftestUrl : ''}
|
||||
successCallback={this.bugFilerCallback}
|
||||
jobGroupName={selectedJob.job_group_name}
|
||||
/>}
|
||||
{isBugFilerOpen && (
|
||||
<BugFiler
|
||||
isOpen={isBugFilerOpen}
|
||||
toggle={this.toggleBugFiler}
|
||||
suggestion={suggestion}
|
||||
suggestions={suggestions}
|
||||
fullLog={jobLogUrls[0].url}
|
||||
parsedLog={logViewerFullUrl}
|
||||
reftestUrl={isReftest(selectedJob) ? reftestUrl : ''}
|
||||
successCallback={this.bugFilerCallback}
|
||||
jobGroupName={selectedJob.job_group_name}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -20,9 +20,7 @@ export default class SuggestionsListItem extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
suggestion, toggleBugFiler,
|
||||
} = this.props;
|
||||
const { suggestion, toggleBugFiler } = this.props;
|
||||
const { suggestionShowMore } = this.state;
|
||||
|
||||
return (
|
||||
|
@ -39,41 +37,48 @@ export default class SuggestionsListItem extends React.Component {
|
|||
</div>
|
||||
|
||||
{/* <!--Open recent bugs--> */}
|
||||
{suggestion.valid_open_recent &&
|
||||
<ul className="list-unstyled failure-summary-bugs">
|
||||
{suggestion.bugs.open_recent.map(bug =>
|
||||
(<BugListItem
|
||||
key={bug.id}
|
||||
bug={bug}
|
||||
suggestion={suggestion}
|
||||
/>))}
|
||||
|
||||
</ul>}
|
||||
{suggestion.valid_open_recent && (
|
||||
<ul className="list-unstyled failure-summary-bugs">
|
||||
{suggestion.bugs.open_recent.map(bug => (
|
||||
<BugListItem key={bug.id} bug={bug} suggestion={suggestion} />
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{/* <!--All other bugs--> */}
|
||||
{suggestion.valid_all_others && suggestion.valid_open_recent &&
|
||||
<span
|
||||
rel="noopener"
|
||||
onClick={this.clickShowMore}
|
||||
className="show-hide-more"
|
||||
>Show / Hide more</span>}
|
||||
{suggestion.valid_all_others && suggestion.valid_open_recent && (
|
||||
<span
|
||||
rel="noopener"
|
||||
onClick={this.clickShowMore}
|
||||
className="show-hide-more"
|
||||
>
|
||||
Show / Hide more
|
||||
</span>
|
||||
)}
|
||||
|
||||
{suggestion.valid_all_others && (suggestionShowMore
|
||||
|| !suggestion.valid_open_recent) &&
|
||||
<ul className="list-unstyled failure-summary-bugs">
|
||||
{suggestion.bugs.all_others.map(bug =>
|
||||
(<BugListItem
|
||||
key={bug.id}
|
||||
bug={bug}
|
||||
suggestion={suggestion}
|
||||
bugClassName={bug.resolution !== '' ? 'deleted' : ''}
|
||||
title={bug.resolution !== '' ? bug.resolution : ''}
|
||||
/>))}
|
||||
</ul>}
|
||||
{suggestion.valid_all_others &&
|
||||
(suggestionShowMore || !suggestion.valid_open_recent) && (
|
||||
<ul className="list-unstyled failure-summary-bugs">
|
||||
{suggestion.bugs.all_others.map(bug => (
|
||||
<BugListItem
|
||||
key={bug.id}
|
||||
bug={bug}
|
||||
suggestion={suggestion}
|
||||
bugClassName={bug.resolution !== '' ? 'deleted' : ''}
|
||||
title={bug.resolution !== '' ? bug.resolution : ''}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{(suggestion.bugs.too_many_open_recent || (suggestion.bugs.too_many_all_others
|
||||
&& !suggestion.valid_open_recent)) &&
|
||||
<mark>Exceeded max {thBugSuggestionLimit} bug suggestions, most of which are likely false positives.</mark>}
|
||||
{(suggestion.bugs.too_many_open_recent ||
|
||||
(suggestion.bugs.too_many_all_others &&
|
||||
!suggestion.valid_open_recent)) && (
|
||||
<mark>
|
||||
Exceeded max {thBugSuggestionLimit} bug suggestions, most of which
|
||||
are likely false positives.
|
||||
</mark>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -48,7 +48,9 @@ export default class ActiveFilters extends React.Component {
|
|||
const choice = fieldChoices[field];
|
||||
const choiceValue = choice.choices.find(c => String(c.id) === value);
|
||||
|
||||
return choice.matchType === 'choice' && choiceValue ? choiceValue.name : value;
|
||||
return choice.matchType === 'choice' && choiceValue
|
||||
? choiceValue.name
|
||||
: value;
|
||||
}
|
||||
|
||||
addNewFieldFilter() {
|
||||
|
@ -75,95 +77,142 @@ export default class ActiveFilters extends React.Component {
|
|||
render() {
|
||||
const { isFieldFilterVisible, filterModel, filterBarFilters } = this.props;
|
||||
const {
|
||||
newFilterField, newFilterMatchType, newFilterValue, newFilterChoices,
|
||||
newFilterField,
|
||||
newFilterMatchType,
|
||||
newFilterValue,
|
||||
newFilterChoices,
|
||||
fieldChoices,
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<div className="alert-info active-filters-bar">
|
||||
{!!filterBarFilters.length && <div>
|
||||
<span
|
||||
className="pointable"
|
||||
title="Clear all of these filters"
|
||||
onClick={filterModel.clearNonStatusFilters}
|
||||
><i className="fa fa-times-circle" /> </span>
|
||||
<span className="active-filters-title">
|
||||
<b>Active Filters</b>
|
||||
</span>
|
||||
{filterBarFilters.map(filter => (
|
||||
filter.value.map(filterValue => (
|
||||
<span className="filtersbar-filter" key={`${filter.field}${filterValue}`}>
|
||||
{!!filterBarFilters.length && (
|
||||
<div>
|
||||
<span
|
||||
className="pointable"
|
||||
title="Clear all of these filters"
|
||||
onClick={filterModel.clearNonStatusFilters}
|
||||
>
|
||||
<i className="fa fa-times-circle" />{' '}
|
||||
</span>
|
||||
<span className="active-filters-title">
|
||||
<b>Active Filters</b>
|
||||
</span>
|
||||
{filterBarFilters.map(filter =>
|
||||
filter.value.map(filterValue => (
|
||||
<span
|
||||
className="pointable"
|
||||
title={`Clear filter: ${filter.field}`}
|
||||
onClick={() => filterModel.removeFilter(filter.field, filterValue)}
|
||||
className="filtersbar-filter"
|
||||
key={`${filter.field}${filterValue}`}
|
||||
>
|
||||
<i className="fa fa-times-circle" />
|
||||
<span
|
||||
className="pointable"
|
||||
title={`Clear filter: ${filter.field}`}
|
||||
onClick={() =>
|
||||
filterModel.removeFilter(filter.field, filterValue)
|
||||
}
|
||||
>
|
||||
<i className="fa fa-times-circle" />
|
||||
|
||||
</span>
|
||||
<span title={`Filter by ${filter.field}: ${filterValue}`}>
|
||||
<b>{filter.field}:</b>
|
||||
{filter.field === 'failure_classification_id' && (
|
||||
<span>
|
||||
{' '}
|
||||
{this.getFilterValue(filter.field, filterValue)}
|
||||
</span>
|
||||
)}
|
||||
{filter.field === 'author' && (
|
||||
<span> {filterValue.split('@')[0].substr(0, 20)}</span>
|
||||
)}
|
||||
{filter.field !== 'author' &&
|
||||
filter.field !== 'failure_classification_id' && (
|
||||
<span> {filterValue.substr(0, 12)}</span>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
<span title={`Filter by ${filter.field}: ${filterValue}`}>
|
||||
<b>{filter.field}:</b>
|
||||
{filter.field === 'failure_classification_id' && (
|
||||
<span> {this.getFilterValue(filter.field, filterValue)}</span>
|
||||
)),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isFieldFilterVisible && (
|
||||
<div>
|
||||
<form className="form-inline">
|
||||
<div className="form-group input-group-sm new-filter-input">
|
||||
<label className="sr-only" htmlFor="job-filter-field">
|
||||
Field
|
||||
</label>
|
||||
<select
|
||||
id="job-filter-field"
|
||||
className="form-control"
|
||||
value={newFilterField}
|
||||
onChange={evt => this.setNewFilterField(evt.target.value)}
|
||||
placeholder="filter field"
|
||||
required
|
||||
>
|
||||
<option value="" disabled>
|
||||
select filter field
|
||||
</option>
|
||||
{Object.entries(fieldChoices).map(([field, obj]) =>
|
||||
obj.name !== 'tier' ? (
|
||||
<option value={field} key={field}>
|
||||
{obj.name}
|
||||
</option>
|
||||
) : null,
|
||||
)}
|
||||
{filter.field === 'author' && <span> {filterValue.split('@')[0].substr(0, 20)}</span>}
|
||||
{filter.field !== 'author' && filter.field !== 'failure_classification_id' && <span> {filterValue.substr(0, 12)}</span>}
|
||||
</span>
|
||||
</span>
|
||||
))
|
||||
))}
|
||||
</div>}
|
||||
{isFieldFilterVisible && <div>
|
||||
<form className="form-inline">
|
||||
<div className="form-group input-group-sm new-filter-input">
|
||||
<label className="sr-only" htmlFor="job-filter-field">Field</label>
|
||||
<select
|
||||
id="job-filter-field"
|
||||
className="form-control"
|
||||
value={newFilterField}
|
||||
onChange={evt => this.setNewFilterField(evt.target.value)}
|
||||
placeholder="filter field"
|
||||
required
|
||||
>
|
||||
<option value="" disabled>select filter field</option>
|
||||
{Object.entries(fieldChoices).map(([field, obj]) => (
|
||||
obj.name !== 'tier' ? <option value={field} key={field}>{obj.name}</option> : null
|
||||
))}
|
||||
</select>
|
||||
<label className="sr-only" htmlFor="job-filter-value">Value</label>
|
||||
{newFilterMatchType !== 'choice' && <input
|
||||
className="form-control"
|
||||
value={newFilterValue}
|
||||
onChange={evt => this.setNewFilterValue(evt.target.value)}
|
||||
id="job-filter-value"
|
||||
type="text"
|
||||
required
|
||||
placeholder="enter filter value"
|
||||
/>}
|
||||
<label className="sr-only" htmlFor="job-filter-choice-value">Value</label>
|
||||
{newFilterMatchType === 'choice' && <select
|
||||
className="form-control"
|
||||
value={newFilterValue}
|
||||
onChange={evt => this.setNewFilterValue(evt.target.value)}
|
||||
id="job-filter-choice-value"
|
||||
>
|
||||
<option value="" disabled>select value</option>
|
||||
{Object.entries(newFilterChoices).map(([fci, fci_obj]) => (
|
||||
<option value={fci_obj.id} key={fci}>{fci_obj.name}</option>
|
||||
)) }
|
||||
</select>}
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-light-bordered btn-sm"
|
||||
onClick={this.addNewFieldFilter}
|
||||
>add</button>
|
||||
<button
|
||||
type="reset"
|
||||
className="btn btn-light-bordered btn-sm"
|
||||
onClick={this.clearNewFieldFilter}
|
||||
>cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>}
|
||||
</select>
|
||||
<label className="sr-only" htmlFor="job-filter-value">
|
||||
Value
|
||||
</label>
|
||||
{newFilterMatchType !== 'choice' && (
|
||||
<input
|
||||
className="form-control"
|
||||
value={newFilterValue}
|
||||
onChange={evt => this.setNewFilterValue(evt.target.value)}
|
||||
id="job-filter-value"
|
||||
type="text"
|
||||
required
|
||||
placeholder="enter filter value"
|
||||
/>
|
||||
)}
|
||||
<label className="sr-only" htmlFor="job-filter-choice-value">
|
||||
Value
|
||||
</label>
|
||||
{newFilterMatchType === 'choice' && (
|
||||
<select
|
||||
className="form-control"
|
||||
value={newFilterValue}
|
||||
onChange={evt => this.setNewFilterValue(evt.target.value)}
|
||||
id="job-filter-choice-value"
|
||||
>
|
||||
<option value="" disabled>
|
||||
select value
|
||||
</option>
|
||||
{Object.entries(newFilterChoices).map(([fci, fci_obj]) => (
|
||||
<option value={fci_obj.id} key={fci}>
|
||||
{fci_obj.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-light-bordered btn-sm"
|
||||
onClick={this.addNewFieldFilter}
|
||||
>
|
||||
add
|
||||
</button>
|
||||
<button
|
||||
type="reset"
|
||||
className="btn btn-light-bordered btn-sm"
|
||||
onClick={this.clearNewFieldFilter}
|
||||
>
|
||||
cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,11 +6,21 @@ import { withPinnedJobs } from '../context/PinnedJobs';
|
|||
import { withSelectedJob } from '../context/SelectedJob';
|
||||
import { withPushes } from '../context/Pushes';
|
||||
|
||||
const resultStatusMenuItems = thAllResultStatuses.filter(rs => rs !== 'runnable');
|
||||
const resultStatusMenuItems = thAllResultStatuses.filter(
|
||||
rs => rs !== 'runnable',
|
||||
);
|
||||
|
||||
function FiltersMenu(props) {
|
||||
const { filterModel, pinJobs, getAllShownJobs, selectedJob, setSelectedJob } = props;
|
||||
const { urlParams: { resultStatus, classifiedState } } = filterModel;
|
||||
const {
|
||||
filterModel,
|
||||
pinJobs,
|
||||
getAllShownJobs,
|
||||
selectedJob,
|
||||
setSelectedJob,
|
||||
} = props;
|
||||
const {
|
||||
urlParams: { resultStatus, classifiedState },
|
||||
} = filterModel;
|
||||
|
||||
const pinAllShownJobs = () => {
|
||||
const shownJobs = getAllShownJobs();
|
||||
|
@ -29,7 +39,9 @@ function FiltersMenu(props) {
|
|||
title="Set filters"
|
||||
data-toggle="dropdown"
|
||||
className="btn btn-view-nav nav-menu-btn dropdown-toggle"
|
||||
>Filters</button>
|
||||
>
|
||||
Filters
|
||||
</button>
|
||||
<ul
|
||||
id="filter-dropdown"
|
||||
className="dropdown-menu nav-dropdown-menu-right checkbox-dropdown-menu"
|
||||
|
@ -46,8 +58,11 @@ function FiltersMenu(props) {
|
|||
className="mousetrap"
|
||||
id={filterName}
|
||||
checked={resultStatus.includes(filterName)}
|
||||
onChange={() => filterModel.toggleResultStatuses([filterName])}
|
||||
/>{filterName}
|
||||
onChange={() =>
|
||||
filterModel.toggleResultStatuses([filterName])
|
||||
}
|
||||
/>
|
||||
{filterName}
|
||||
</label>
|
||||
</span>
|
||||
</span>
|
||||
|
@ -60,32 +75,42 @@ function FiltersMenu(props) {
|
|||
id="classified"
|
||||
checked={classifiedState.includes('classified')}
|
||||
onChange={() => filterModel.toggleClassifiedFilter('classified')}
|
||||
/>classified
|
||||
/>
|
||||
classified
|
||||
</label>
|
||||
<label className="dropdown-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="unclassified"
|
||||
checked={classifiedState.includes('unclassified')}
|
||||
onChange={() => filterModel.toggleClassifiedFilter('unclassified')}
|
||||
/>unclassified
|
||||
onChange={() =>
|
||||
filterModel.toggleClassifiedFilter('unclassified')
|
||||
}
|
||||
/>
|
||||
unclassified
|
||||
</label>
|
||||
<li className="dropdown-divider separator" />
|
||||
<li
|
||||
title="Pin all jobs that pass the global filters"
|
||||
className="dropdown-item"
|
||||
onClick={pinAllShownJobs}
|
||||
>Pin all showing</li>
|
||||
>
|
||||
Pin all showing
|
||||
</li>
|
||||
<li
|
||||
title="Show only superseded jobs"
|
||||
className="dropdown-item"
|
||||
onClick={filterModel.setOnlySuperseded}
|
||||
>Superseded only</li>
|
||||
>
|
||||
Superseded only
|
||||
</li>
|
||||
<li
|
||||
title="Reset to default status filters"
|
||||
className="dropdown-item"
|
||||
onClick={filterModel.resetNonFieldFilters}
|
||||
>Reset</li>
|
||||
>
|
||||
Reset
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</span>
|
||||
|
|
|
@ -17,7 +17,8 @@ const menuItems = [
|
|||
text: 'API Reference',
|
||||
},
|
||||
{
|
||||
href: 'https://wiki.mozilla.org/EngineeringProductivity/Projects/Treeherder',
|
||||
href:
|
||||
'https://wiki.mozilla.org/EngineeringProductivity/Projects/Treeherder',
|
||||
icon: 'fa-file-word-o',
|
||||
text: 'Project Wiki',
|
||||
},
|
||||
|
@ -27,7 +28,8 @@ const menuItems = [
|
|||
text: 'Mailing List',
|
||||
},
|
||||
{
|
||||
href: 'https://bugzilla.mozilla.org/enter_bug.cgi?product=Tree+Management&component=Treeherder',
|
||||
href:
|
||||
'https://bugzilla.mozilla.org/enter_bug.cgi?product=Tree+Management&component=Treeherder',
|
||||
icon: 'fa-bug',
|
||||
text: 'Report a Bug',
|
||||
},
|
||||
|
@ -37,9 +39,10 @@ const menuItems = [
|
|||
text: 'Source',
|
||||
},
|
||||
{
|
||||
href: 'https://whatsdeployed.io/?owner=mozilla&repo=treeherder&name[]=Stage&url[]=https://treeherder.allizom.org/revision.txt&name[]=Prod&url[]=https://treeherder.mozilla.org/revision.txt',
|
||||
href:
|
||||
'https://whatsdeployed.io/?owner=mozilla&repo=treeherder&name[]=Stage&url[]=https://treeherder.allizom.org/revision.txt&name[]=Prod&url[]=https://treeherder.mozilla.org/revision.txt',
|
||||
icon: 'fa-question',
|
||||
text: 'What\'s Deployed?',
|
||||
text: "What's Deployed?",
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -60,11 +63,19 @@ export default function HelpMenu() {
|
|||
role="menu"
|
||||
aria-labelledby="helpLabel"
|
||||
>
|
||||
{menuItems.map(item => (<li key={item.text}>
|
||||
<a href={item.href} target="_blank" rel="noopener noreferrer" className="dropdown-item">
|
||||
<span className={`fa ${item.icon} midgray`} />{item.text}
|
||||
</a>
|
||||
</li>))}
|
||||
{menuItems.map(item => (
|
||||
<li key={item.text}>
|
||||
<a
|
||||
href={item.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="dropdown-item"
|
||||
>
|
||||
<span className={`fa ${item.icon} midgray`} />
|
||||
{item.text}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -8,21 +8,27 @@ export default function InfraMenu() {
|
|||
title="Infrastructure status"
|
||||
data-toggle="dropdown"
|
||||
className="btn btn-view-nav nav-menu-btn dropdown-toggle"
|
||||
>Infra</button>
|
||||
>
|
||||
Infra
|
||||
</button>
|
||||
<ul
|
||||
id="infra-dropdown"
|
||||
className="dropdown-menu nav-dropdown-menu-right container"
|
||||
role="menu"
|
||||
aria-labelledby="infraLabel"
|
||||
>
|
||||
<li role="presentation" className="dropdown-header">Buildbot</li>
|
||||
<li role="presentation" className="dropdown-header">
|
||||
Buildbot
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="https://secure.pub.build.mozilla.org/buildapi/pending"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>BuildAPI: Pending</a>
|
||||
>
|
||||
BuildAPI: Pending
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
|
@ -30,7 +36,9 @@ export default function InfraMenu() {
|
|||
href="https://secure.pub.build.mozilla.org/buildapi/running"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>BuildAPI: Running</a>
|
||||
>
|
||||
BuildAPI: Running
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
|
@ -38,7 +46,9 @@ export default function InfraMenu() {
|
|||
href="https://www.hostedgraphite.com/da5c920d/86a8384e-d9cf-4208-989b-9538a1a53e4b/grafana2/#/dashboard/db/ec2-dashboard"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>EC2 Dashboard</a>
|
||||
>
|
||||
EC2 Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
|
@ -46,17 +56,23 @@ export default function InfraMenu() {
|
|||
href="https://secure.pub.build.mozilla.org/builddata/reports/slave_health/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Slave Health</a>
|
||||
>
|
||||
Slave Health
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation" className="dropdown-divider" />
|
||||
<li role="presentation" className="dropdown-header">Other</li>
|
||||
<li role="presentation" className="dropdown-header">
|
||||
Other
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="https://mozilla-releng.net/treestatus"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>TreeStatus</a>
|
||||
>
|
||||
TreeStatus
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
|
@ -64,7 +80,9 @@ export default function InfraMenu() {
|
|||
href="https://tools.taskcluster.net/diagnostics"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Taskcluster</a>
|
||||
>
|
||||
Taskcluster
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
|
|
|
@ -7,9 +7,12 @@ import { withNotifications } from '../../shared/context/Notifications';
|
|||
class NotificationsMenu extends React.Component {
|
||||
getSeverityClass(severity) {
|
||||
switch (severity) {
|
||||
case 'danger': return 'fa fa-ban text-danger';
|
||||
case 'warning': return 'fa fa-warning text-warning';
|
||||
case 'success': return 'fa fa-check text-success';
|
||||
case 'danger':
|
||||
return 'fa fa-ban text-danger';
|
||||
case 'warning':
|
||||
return 'fa fa-warning text-warning';
|
||||
case 'success':
|
||||
return 'fa fa-check text-success';
|
||||
}
|
||||
return 'fa fa-info-circle text-info';
|
||||
}
|
||||
|
@ -25,7 +28,9 @@ class NotificationsMenu extends React.Component {
|
|||
aria-label="Recent notifications"
|
||||
data-toggle="dropdown"
|
||||
className="btn btn-view-nav nav-menu-btn"
|
||||
><span className="fa fa-bell-o lightgray" /></button>
|
||||
>
|
||||
<span className="fa fa-bell-o lightgray" />
|
||||
</button>
|
||||
<ul
|
||||
id="notification-dropdown"
|
||||
className="dropdown-menu nav-dropdown-menu-right"
|
||||
|
@ -36,35 +41,53 @@ class NotificationsMenu extends React.Component {
|
|||
role="presentation"
|
||||
className="dropdown-header"
|
||||
title="Notifications"
|
||||
>Recent notifications
|
||||
{!!storedNotifications.length && <button
|
||||
className="btn btn-xs btn-light-bordered notification-dropdown-btn"
|
||||
title="Clear all notifications"
|
||||
onClick={clearStoredNotifications}
|
||||
>Clear all</button>}
|
||||
>
|
||||
Recent notifications
|
||||
{!!storedNotifications.length && (
|
||||
<button
|
||||
className="btn btn-xs btn-light-bordered notification-dropdown-btn"
|
||||
title="Clear all notifications"
|
||||
onClick={clearStoredNotifications}
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
{storedNotifications.length ?
|
||||
{storedNotifications.length ? (
|
||||
storedNotifications.map(notification => (
|
||||
<li
|
||||
className="notification-dropdown-line"
|
||||
key={`${notification.created}${notification.message}`}
|
||||
>
|
||||
<span title={`${notification.message} ${notification.linkText}`}>
|
||||
<span className={this.getSeverityClass(notification.severity)} />
|
||||
<span
|
||||
title={`${notification.message} ${notification.linkText}`}
|
||||
>
|
||||
<span
|
||||
className={this.getSeverityClass(notification.severity)}
|
||||
/>
|
||||
|
||||
<small className="text-muted">
|
||||
{new Date(notification.created).toLocaleString('en-US', shortDateFormat)}
|
||||
{new Date(notification.created).toLocaleString(
|
||||
'en-US',
|
||||
shortDateFormat,
|
||||
)}
|
||||
</small>
|
||||
{notification.message}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={notification.url}
|
||||
>{notification.linkText}</a>
|
||||
>
|
||||
{notification.linkText}
|
||||
</a>
|
||||
</span>
|
||||
</li>
|
||||
)) :
|
||||
<li><span>No recent notifications</span></li>
|
||||
}
|
||||
))
|
||||
) : (
|
||||
<li>
|
||||
<span>No recent notifications</span>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -15,9 +15,16 @@ import SecondaryNavBar from './SecondaryNavBar';
|
|||
|
||||
export default function PrimaryNavBar(props) {
|
||||
const {
|
||||
user, setUser, repos, updateButtonClick, serverChanged,
|
||||
filterModel, setCurrentRepoTreeStatus, duplicateJobsVisible,
|
||||
groupCountsExpanded, toggleFieldFilterVisible,
|
||||
user,
|
||||
setUser,
|
||||
repos,
|
||||
updateButtonClick,
|
||||
serverChanged,
|
||||
filterModel,
|
||||
setCurrentRepoTreeStatus,
|
||||
duplicateJobsVisible,
|
||||
groupCountsExpanded,
|
||||
toggleFieldFilterVisible,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
|
@ -25,10 +32,7 @@ export default function PrimaryNavBar(props) {
|
|||
<div id="th-global-top-nav-panel">
|
||||
<nav id="th-global-navbar" className="navbar navbar-dark">
|
||||
<div id="th-global-navbar-top">
|
||||
<LogoMenu
|
||||
menuText="Treeherder"
|
||||
menuImage={Logo}
|
||||
/>
|
||||
<LogoMenu menuText="Treeherder" menuImage={Logo} />
|
||||
<span className="navbar-right">
|
||||
<NotificationsMenu />
|
||||
<InfraMenu />
|
||||
|
|
|
@ -16,9 +16,13 @@ const GROUP_ORDER = [
|
|||
|
||||
export default function ReposMenu(props) {
|
||||
const { repos } = props;
|
||||
const groups = repos.reduce((acc, repo, idx, arr, group = repo => repo.repository_group.name) => (
|
||||
{ ...acc, [group(repo)]: [...acc[group(repo)] || [], repo] }
|
||||
), {});
|
||||
const groups = repos.reduce(
|
||||
(acc, repo, idx, arr, group = repo => repo.repository_group.name) => ({
|
||||
...acc,
|
||||
[group(repo)]: [...(acc[group(repo)] || []), repo],
|
||||
}),
|
||||
{},
|
||||
);
|
||||
const groupedRepos = GROUP_ORDER.map(name => ({ name, repos: groups[name] }));
|
||||
|
||||
return (
|
||||
|
@ -29,7 +33,9 @@ export default function ReposMenu(props) {
|
|||
title="Watch a repo"
|
||||
data-toggle="dropdown"
|
||||
className="btn btn-view-nav nav-menu-btn dropdown-toggle"
|
||||
>Repos</button>
|
||||
>
|
||||
Repos
|
||||
</button>
|
||||
<span
|
||||
id="repo-dropdown"
|
||||
className="dropdown-menu nav-dropdown-menu-right container"
|
||||
|
@ -47,16 +53,21 @@ export default function ReposMenu(props) {
|
|||
role="presentation"
|
||||
className="dropdown-header"
|
||||
title={group.name}
|
||||
>{group.name} <span className="fa fa-info-circle" /></li>
|
||||
{!!group.repos && group.repos.map(repo => (
|
||||
<li key={repo.name}>
|
||||
<a
|
||||
title="Open repo"
|
||||
className="dropdown-link"
|
||||
href={getRepoUrl(repo.name)}
|
||||
>{repo.name}</a>
|
||||
</li>
|
||||
))}
|
||||
>
|
||||
{group.name} <span className="fa fa-info-circle" />
|
||||
</li>
|
||||
{!!group.repos &&
|
||||
group.repos.map(repo => (
|
||||
<li key={repo.name}>
|
||||
<a
|
||||
title="Open repo"
|
||||
className="dropdown-link"
|
||||
href={getRepoUrl(repo.name)}
|
||||
>
|
||||
{repo.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</span>
|
||||
))}
|
||||
</ul>
|
||||
|
@ -66,7 +77,6 @@ export default function ReposMenu(props) {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
ReposMenu.propTypes = {
|
||||
repos: PropTypes.array.isRequired,
|
||||
};
|
||||
|
|
|
@ -3,11 +3,7 @@ import PropTypes from 'prop-types';
|
|||
|
||||
import { getBtnClass } from '../../helpers/job';
|
||||
import { thFilterGroups } from '../../helpers/filter';
|
||||
import {
|
||||
getRepo,
|
||||
getUrlParam,
|
||||
setUrlParam,
|
||||
} from '../../helpers/location';
|
||||
import { getRepo, getUrlParam, setUrlParam } from '../../helpers/location';
|
||||
import RepositoryModel from '../../models/repository';
|
||||
import ErrorBoundary from '../../shared/ErrorBoundary';
|
||||
import { withPushes } from '../context/Pushes';
|
||||
|
@ -29,7 +25,8 @@ class SecondaryNavBar extends React.Component {
|
|||
this.filterChicklets = [
|
||||
'failures',
|
||||
thFilterGroups.nonfailures,
|
||||
'in progress'].reduce((acc, val) => acc.concat(val), []);
|
||||
'in progress',
|
||||
].reduce((acc, val) => acc.concat(val), []);
|
||||
|
||||
this.state = {
|
||||
searchQueryStr: getSearchStrFromUrl(),
|
||||
|
@ -40,7 +37,9 @@ class SecondaryNavBar extends React.Component {
|
|||
|
||||
componentDidMount() {
|
||||
this.toggleGroupState = this.toggleGroupState.bind(this);
|
||||
this.toggleUnclassifiedFailures = this.toggleUnclassifiedFailures.bind(this);
|
||||
this.toggleUnclassifiedFailures = this.toggleUnclassifiedFailures.bind(
|
||||
this,
|
||||
);
|
||||
this.clearFilterBox = this.clearFilterBox.bind(this);
|
||||
this.unwatchRepo = this.unwatchRepo.bind(this);
|
||||
this.handleUrlChanges = this.handleUrlChanges.bind(this);
|
||||
|
@ -93,9 +92,10 @@ class SecondaryNavBar extends React.Component {
|
|||
*/
|
||||
toggleResultStatusFilterChicklet(filter) {
|
||||
const { filterModel } = this.props;
|
||||
const filterValues = filter in thFilterGroups ?
|
||||
thFilterGroups[filter] : // this is a filter grouping, so toggle all on/off
|
||||
[filter];
|
||||
const filterValues =
|
||||
filter in thFilterGroups
|
||||
? thFilterGroups[filter] // this is a filter grouping, so toggle all on/off
|
||||
: [filter];
|
||||
|
||||
filterModel.toggleResultStatuses(filterValues);
|
||||
}
|
||||
|
@ -136,11 +136,12 @@ class SecondaryNavBar extends React.Component {
|
|||
const { repoName } = this.state;
|
||||
|
||||
try {
|
||||
const storedWatched = JSON.parse(localStorage.getItem(WATCHED_REPOS_STORAGE_KEY)) || [];
|
||||
const storedWatched =
|
||||
JSON.parse(localStorage.getItem(WATCHED_REPOS_STORAGE_KEY)) || [];
|
||||
// Ensure the current repo is first in the list
|
||||
const watchedRepoNames = [
|
||||
repoName,
|
||||
...storedWatched.filter(value => (value !== repoName)),
|
||||
...storedWatched.filter(value => value !== repoName),
|
||||
].slice(0, MAX_WATCHED_REPOS);
|
||||
|
||||
// Re-save the list, in case it has now changed
|
||||
|
@ -162,19 +163,27 @@ class SecondaryNavBar extends React.Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
updateButtonClick, serverChanged, setCurrentRepoTreeStatus, repos,
|
||||
allUnclassifiedFailureCount, filteredUnclassifiedFailureCount,
|
||||
groupCountsExpanded, duplicateJobsVisible, toggleFieldFilterVisible,
|
||||
updateButtonClick,
|
||||
serverChanged,
|
||||
setCurrentRepoTreeStatus,
|
||||
repos,
|
||||
allUnclassifiedFailureCount,
|
||||
filteredUnclassifiedFailureCount,
|
||||
groupCountsExpanded,
|
||||
duplicateJobsVisible,
|
||||
toggleFieldFilterVisible,
|
||||
} = this.props;
|
||||
const {
|
||||
watchedRepoNames, searchQueryStr, repoName,
|
||||
} = this.state;
|
||||
const { watchedRepoNames, searchQueryStr, repoName } = this.state;
|
||||
// This array needs to be RepositoryModel objects, not strings.
|
||||
// If ``repos`` is not yet populated, then leave as empty array.
|
||||
// We need to filter just in case some of these repo names do not exist.
|
||||
// This could happen if the user typed an invalid ``repo`` param on the URL
|
||||
const watchedRepos = (repos.length && watchedRepoNames.map(
|
||||
name => RepositoryModel.getRepo(name, repos)).filter(name => name)) || [];
|
||||
const watchedRepos =
|
||||
(repos.length &&
|
||||
watchedRepoNames
|
||||
.map(name => RepositoryModel.getRepo(name, repos))
|
||||
.filter(name => name)) ||
|
||||
[];
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -200,42 +209,64 @@ class SecondaryNavBar extends React.Component {
|
|||
))}
|
||||
</span>
|
||||
<form role="search" className="form-inline flex-row">
|
||||
{serverChanged && <span
|
||||
className="btn btn-sm btn-view-nav nav-menu-btn"
|
||||
onClick={updateButtonClick}
|
||||
id="revisionChangedLabel"
|
||||
title="New version of Treeherder has been deployed. Reload to pick up changes."
|
||||
>
|
||||
<span className="fa fa-exclamation-circle" /> Treeherder update available
|
||||
</span>}
|
||||
{serverChanged && (
|
||||
<span
|
||||
className="btn btn-sm btn-view-nav nav-menu-btn"
|
||||
onClick={updateButtonClick}
|
||||
id="revisionChangedLabel"
|
||||
title="New version of Treeherder has been deployed. Reload to pick up changes."
|
||||
>
|
||||
<span className="fa fa-exclamation-circle" />
|
||||
Treeherder update available
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Unclassified Failures Button */}
|
||||
<span
|
||||
className={`btn btn-sm ${allUnclassifiedFailureCount ? 'btn-unclassified-failures' : 'btn-view-nav'}`}
|
||||
className={`btn btn-sm ${
|
||||
allUnclassifiedFailureCount
|
||||
? 'btn-unclassified-failures'
|
||||
: 'btn-view-nav'
|
||||
}`}
|
||||
title="Loaded failures / toggle filtering for unclassified failures"
|
||||
tabIndex="-1"
|
||||
role="button"
|
||||
onClick={this.toggleUnclassifiedFailures}
|
||||
>
|
||||
<span id="unclassified-failure-count">{allUnclassifiedFailureCount}</span> unclassified
|
||||
<span id="unclassified-failure-count">
|
||||
{allUnclassifiedFailureCount}
|
||||
</span>{' '}
|
||||
unclassified
|
||||
</span>
|
||||
|
||||
{/* Filtered Unclassified Failures Button */}
|
||||
{filteredUnclassifiedFailureCount !== allUnclassifiedFailureCount &&
|
||||
<span
|
||||
className="navbar-badge badge badge-secondary badge-pill"
|
||||
title="Reflects the unclassified failures which pass the current filters"
|
||||
>
|
||||
<span id="filtered-unclassified-failure-count">{filteredUnclassifiedFailureCount}</span>
|
||||
</span>}
|
||||
{filteredUnclassifiedFailureCount !==
|
||||
allUnclassifiedFailureCount && (
|
||||
<span
|
||||
className="navbar-badge badge badge-secondary badge-pill"
|
||||
title="Reflects the unclassified failures which pass the current filters"
|
||||
>
|
||||
<span id="filtered-unclassified-failure-count">
|
||||
{filteredUnclassifiedFailureCount}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Toggle Duplicate Jobs */}
|
||||
<span
|
||||
className={`btn btn-view-nav btn-sm btn-toggle-duplicate-jobs ${groupCountsExpanded ? 'disabled' : ''} ${!duplicateJobsVisible ? 'strikethrough' : ''}`}
|
||||
className={`btn btn-view-nav btn-sm btn-toggle-duplicate-jobs ${
|
||||
groupCountsExpanded ? 'disabled' : ''
|
||||
} ${!duplicateJobsVisible ? 'strikethrough' : ''}`}
|
||||
tabIndex="0"
|
||||
role="button"
|
||||
title={duplicateJobsVisible ? 'Hide duplicate jobs' : 'Show duplicate jobs'}
|
||||
onClick={() => !groupCountsExpanded && this.toggleShowDuplicateJobs()}
|
||||
title={
|
||||
duplicateJobsVisible
|
||||
? 'Hide duplicate jobs'
|
||||
: 'Show duplicate jobs'
|
||||
}
|
||||
onClick={() =>
|
||||
!groupCountsExpanded && this.toggleShowDuplicateJobs()
|
||||
}
|
||||
/>
|
||||
<span className="btn-group">
|
||||
{/* Toggle Group State Button */}
|
||||
|
@ -243,28 +274,45 @@ class SecondaryNavBar extends React.Component {
|
|||
className="btn btn-view-nav btn-sm btn-toggle-group-state"
|
||||
tabIndex="-1"
|
||||
role="button"
|
||||
title={groupCountsExpanded ? 'Collapse job groups' : 'Expand job groups'}
|
||||
title={
|
||||
groupCountsExpanded
|
||||
? 'Collapse job groups'
|
||||
: 'Expand job groups'
|
||||
}
|
||||
onClick={() => this.toggleGroupState()}
|
||||
>( <span className="group-state-nav-icon">{groupCountsExpanded ? '-' : '+'}</span> )
|
||||
>
|
||||
({' '}
|
||||
<span className="group-state-nav-icon">
|
||||
{groupCountsExpanded ? '-' : '+'}
|
||||
</span>{' '}
|
||||
)
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{/* Result Status Filter Chicklets */}
|
||||
<span className="resultStatusChicklets">
|
||||
<span id="filter-chicklets">
|
||||
{this.filterChicklets.map((filterName) => {
|
||||
{this.filterChicklets.map(filterName => {
|
||||
const isOn = this.isFilterOn(filterName);
|
||||
return (<span key={filterName}>
|
||||
<span
|
||||
className={`btn btn-view-nav btn-sm btn-nav-filter ${getBtnClass(filterName)}-filter-chicklet fa ${isOn ? 'fa-dot-circle-o' : 'fa-circle-thin'}`}
|
||||
onClick={() => this.toggleResultStatusFilterChicklet(filterName)}
|
||||
title={filterName}
|
||||
aria-label={filterName}
|
||||
role="checkbox"
|
||||
aria-checked={isOn}
|
||||
tabIndex={0}
|
||||
/>
|
||||
</span>);
|
||||
return (
|
||||
<span key={filterName}>
|
||||
<span
|
||||
className={`btn btn-view-nav btn-sm btn-nav-filter ${getBtnClass(
|
||||
filterName,
|
||||
)}-filter-chicklet fa ${
|
||||
isOn ? 'fa-dot-circle-o' : 'fa-circle-thin'
|
||||
}`}
|
||||
onClick={() =>
|
||||
this.toggleResultStatusFilterChicklet(filterName)
|
||||
}
|
||||
title={filterName}
|
||||
aria-label={filterName}
|
||||
role="checkbox"
|
||||
aria-checked={isOn}
|
||||
tabIndex={0}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
</span>
|
||||
|
@ -274,7 +322,9 @@ class SecondaryNavBar extends React.Component {
|
|||
className="btn btn-view-nav btn-sm"
|
||||
onClick={toggleFieldFilterVisible}
|
||||
title="Filter by a job field"
|
||||
><i className="fa fa-filter" /></span>
|
||||
>
|
||||
<i className="fa fa-filter" />
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{/* Quick Filter Field */}
|
||||
|
|
|
@ -15,30 +15,36 @@ export default function TiersMenu(props) {
|
|||
title="Show/hide job tiers"
|
||||
data-toggle="dropdown"
|
||||
className="btn btn-view-nav btn-sm nav-menu-btn dropdown-toggle"
|
||||
>Tiers</span>
|
||||
<ul
|
||||
className="dropdown-menu checkbox-dropdown-menu"
|
||||
role="menu"
|
||||
>
|
||||
{TIERS.map((tier) => {
|
||||
Tiers
|
||||
</span>
|
||||
<ul className="dropdown-menu checkbox-dropdown-menu" role="menu">
|
||||
{TIERS.map(tier => {
|
||||
const isOnlyTier = shownTiers.length === 1 && tier === shownTiers[0];
|
||||
return (<li key={tier}>
|
||||
<div>
|
||||
<label
|
||||
title={isOnlyTier ? 'Must have at least one tier selected at all times' : ''}
|
||||
className={`dropdown-item ${isOnlyTier ? 'disabled' : ''}`}
|
||||
>
|
||||
<input
|
||||
id="tier-checkbox"
|
||||
type="checkbox"
|
||||
className="mousetrap"
|
||||
disabled={isOnlyTier}
|
||||
checked={shownTiers.includes(tier)}
|
||||
onChange={() => filterModel.toggleFilter('tier', tier)}
|
||||
/>tier {tier}
|
||||
</label>
|
||||
</div>
|
||||
</li>);
|
||||
return (
|
||||
<li key={tier}>
|
||||
<div>
|
||||
<label
|
||||
title={
|
||||
isOnlyTier
|
||||
? 'Must have at least one tier selected at all times'
|
||||
: ''
|
||||
}
|
||||
className={`dropdown-item ${isOnlyTier ? 'disabled' : ''}`}
|
||||
>
|
||||
<input
|
||||
id="tier-checkbox"
|
||||
type="checkbox"
|
||||
className="mousetrap"
|
||||
disabled={isOnlyTier}
|
||||
checked={shownTiers.includes(tier)}
|
||||
onChange={() => filterModel.toggleFilter('tier', tier)}
|
||||
/>
|
||||
tier {tier}
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</span>
|
||||
|
|
|
@ -8,12 +8,15 @@ export default function UpdateAvailable(props) {
|
|||
return (
|
||||
<div className="alert alert-info update-alert-panel">
|
||||
<i className="fa fa-info-circle" aria-hidden="true" />
|
||||
Treeherder has updated. To pick up the changes, you can reload the page
|
||||
Treeherder has updated. To pick up the changes, you can reload the page
|
||||
|
||||
<button
|
||||
onClick={updateButtonClick}
|
||||
className="btn btn-xs btn-danger"
|
||||
type="button"
|
||||
>Reload</button>
|
||||
>
|
||||
Reload
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,37 +6,37 @@ import BugLinkify from '../../shared/BugLinkify';
|
|||
import { getRepoUrl } from '../../helpers/url';
|
||||
|
||||
const statusInfoMap = {
|
||||
open: {
|
||||
icon: 'fa-circle-o',
|
||||
color: 'tree-open',
|
||||
btnClass: 'btn-view-nav',
|
||||
},
|
||||
'approval required': {
|
||||
icon: 'fa-lock',
|
||||
color: 'tree-approval',
|
||||
btnClass: 'btn-view-nav',
|
||||
},
|
||||
closed: {
|
||||
icon: 'fa-times-circle',
|
||||
color: 'tree-closed',
|
||||
btnClass: 'btn-view-nav-closed',
|
||||
},
|
||||
unsupported: {
|
||||
icon: 'fa-question',
|
||||
color: 'tree-unavailable',
|
||||
btnClass: 'btn-view-nav',
|
||||
},
|
||||
'not retrieved yet': {
|
||||
icon: 'fa-spinner',
|
||||
pulseIcon: 'fa-pulse',
|
||||
color: 'tree-unavailable',
|
||||
btnClass: 'btn-view-nav',
|
||||
},
|
||||
error: {
|
||||
icon: 'fa-question',
|
||||
color: 'tree-unavailable',
|
||||
btnClass: 'btn-view-nav',
|
||||
},
|
||||
open: {
|
||||
icon: 'fa-circle-o',
|
||||
color: 'tree-open',
|
||||
btnClass: 'btn-view-nav',
|
||||
},
|
||||
'approval required': {
|
||||
icon: 'fa-lock',
|
||||
color: 'tree-approval',
|
||||
btnClass: 'btn-view-nav',
|
||||
},
|
||||
closed: {
|
||||
icon: 'fa-times-circle',
|
||||
color: 'tree-closed',
|
||||
btnClass: 'btn-view-nav-closed',
|
||||
},
|
||||
unsupported: {
|
||||
icon: 'fa-question',
|
||||
color: 'tree-unavailable',
|
||||
btnClass: 'btn-view-nav',
|
||||
},
|
||||
'not retrieved yet': {
|
||||
icon: 'fa-spinner',
|
||||
pulseIcon: 'fa-pulse',
|
||||
color: 'tree-unavailable',
|
||||
btnClass: 'btn-view-nav',
|
||||
},
|
||||
error: {
|
||||
icon: 'fa-question',
|
||||
color: 'tree-unavailable',
|
||||
btnClass: 'btn-view-nav',
|
||||
},
|
||||
};
|
||||
|
||||
export default class WatchedRepo extends React.Component {
|
||||
|
@ -63,7 +63,10 @@ export default class WatchedRepo extends React.Component {
|
|||
|
||||
this.updateTreeStatus();
|
||||
// update the TreeStatus every 2 minutes
|
||||
this.treeStatusIntervalId = setInterval(this.updateTreeStatus, 2 * 60 * 1000);
|
||||
this.treeStatusIntervalId = setInterval(
|
||||
this.updateTreeStatus,
|
||||
2 * 60 * 1000,
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -81,7 +84,7 @@ export default class WatchedRepo extends React.Component {
|
|||
const { repo, repoName, setCurrentRepoTreeStatus } = this.props;
|
||||
const watchedRepoName = repo.name;
|
||||
|
||||
TreeStatusModel.get(watchedRepoName).then((data) => {
|
||||
TreeStatusModel.get(watchedRepoName).then(data => {
|
||||
const treeStatus = data.result;
|
||||
|
||||
if (watchedRepoName === repoName) {
|
||||
|
@ -100,7 +103,11 @@ export default class WatchedRepo extends React.Component {
|
|||
render() {
|
||||
const { repoName, unwatchRepo, repo } = this.props;
|
||||
const {
|
||||
status, messageOfTheDay, reason, statusInfo, hasBoundaryError,
|
||||
status,
|
||||
messageOfTheDay,
|
||||
reason,
|
||||
statusInfo,
|
||||
hasBoundaryError,
|
||||
boundaryError,
|
||||
} = this.state;
|
||||
const watchedRepo = repo.name;
|
||||
|
@ -115,7 +122,9 @@ export default class WatchedRepo extends React.Component {
|
|||
<span
|
||||
className="btn-view-nav pl-1 pr-1 border-right"
|
||||
title={boundaryError.toString()}
|
||||
>Error getting {watchedRepo} info</span>
|
||||
>
|
||||
Error getting {watchedRepo} info
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
|
@ -134,39 +143,64 @@ export default class WatchedRepo extends React.Component {
|
|||
title={`${watchedRepo} info`}
|
||||
aria-label={`${watchedRepo} info`}
|
||||
data-toggle="dropdown"
|
||||
><span className="fa fa-info-circle" /></button>
|
||||
{watchedRepo !== repoName && <button
|
||||
className={`watched-repo-unwatch-btn btn btn-sm btn-view-nav ${activeClass}`}
|
||||
onClick={() => unwatchRepo(watchedRepo)}
|
||||
title={`Unwatch ${watchedRepo}`}
|
||||
><span className="fa fa-times" /></button>}
|
||||
>
|
||||
<span className="fa fa-info-circle" />
|
||||
</button>
|
||||
{watchedRepo !== repoName && (
|
||||
<button
|
||||
className={`watched-repo-unwatch-btn btn btn-sm btn-view-nav ${activeClass}`}
|
||||
onClick={() => unwatchRepo(watchedRepo)}
|
||||
title={`Unwatch ${watchedRepo}`}
|
||||
>
|
||||
<span className="fa fa-times" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<ul className="dropdown-menu" role="menu">
|
||||
{status === 'unsupported' && <React.Fragment>
|
||||
{status === 'unsupported' && (
|
||||
<React.Fragment>
|
||||
<li className="watched-repo-dropdown-item">
|
||||
<span>
|
||||
{watchedRepo} is not listed on{' '}
|
||||
<a
|
||||
href="https://mozilla-releng.net/treestatus"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Tree Status
|
||||
</a>
|
||||
</span>
|
||||
</li>
|
||||
<li className="dropdown-divider" />
|
||||
</React.Fragment>
|
||||
)}
|
||||
{!!reason && (
|
||||
<li className="watched-repo-dropdown-item">
|
||||
<span>{watchedRepo} is not listed on <a
|
||||
href="https://mozilla-releng.net/treestatus"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Tree Status</a></span>
|
||||
<span>
|
||||
<BugLinkify>{reason}</BugLinkify>
|
||||
</span>
|
||||
</li>
|
||||
<li className="dropdown-divider" />
|
||||
</React.Fragment>}
|
||||
{!!reason && <li className="watched-repo-dropdown-item">
|
||||
<span><BugLinkify>{reason}</BugLinkify></span>
|
||||
</li>}
|
||||
)}
|
||||
{!!reason && !!messageOfTheDay && <li className="dropdown-divider" />}
|
||||
{!!messageOfTheDay && <li className="watched-repo-dropdown-item">
|
||||
<span><BugLinkify>{messageOfTheDay}</BugLinkify></span>
|
||||
</li>}
|
||||
{(!!reason || !!messageOfTheDay) && <li className="dropdown-divider" />}
|
||||
{!!messageOfTheDay && (
|
||||
<li className="watched-repo-dropdown-item">
|
||||
<span>
|
||||
<BugLinkify>{messageOfTheDay}</BugLinkify>
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
{(!!reason || !!messageOfTheDay) && (
|
||||
<li className="dropdown-divider" />
|
||||
)}
|
||||
<li className="watched-repo-dropdown-item">
|
||||
<a
|
||||
href={`https://mozilla-releng.net/treestatus/show/${treeStatusName}`}
|
||||
className="dropdown-item"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Tree Status</a>
|
||||
>
|
||||
Tree Status
|
||||
</a>
|
||||
</li>
|
||||
<li className="watched-repo-dropdown-item">
|
||||
<a
|
||||
|
@ -174,7 +208,9 @@ export default class WatchedRepo extends React.Component {
|
|||
className="dropdown-item"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Pushlog</a>
|
||||
>
|
||||
Pushlog
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
|
|
|
@ -71,9 +71,18 @@ export default class JobButtonComponent extends React.Component {
|
|||
render() {
|
||||
const { job } = this.props;
|
||||
const { isSelected, isRunnableSelected } = this.state;
|
||||
const { state, job_type_name, failure_classification_id, end_timestamp,
|
||||
start_timestamp, ref_data_name, visible, id,
|
||||
job_type_symbol, result } = job;
|
||||
const {
|
||||
state,
|
||||
job_type_name,
|
||||
failure_classification_id,
|
||||
end_timestamp,
|
||||
start_timestamp,
|
||||
ref_data_name,
|
||||
visible,
|
||||
id,
|
||||
job_type_symbol,
|
||||
result,
|
||||
} = job;
|
||||
|
||||
if (!visible) return null;
|
||||
const resultStatus = state === 'completed' ? result : state;
|
||||
|
@ -111,9 +120,7 @@ export default class JobButtonComponent extends React.Component {
|
|||
}
|
||||
|
||||
attributes.className = classes.join(' ');
|
||||
return (
|
||||
<button {...attributes}>{job_type_symbol}</button>
|
||||
);
|
||||
return <button {...attributes}>{job_type_symbol}</button>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,14 +3,15 @@ import PropTypes from 'prop-types';
|
|||
|
||||
export default function JobCount(props) {
|
||||
const { className, title, onClick, count } = props;
|
||||
const classes = [className, 'btn group-btn btn-xs job-group-count filter-shown'];
|
||||
const classes = [
|
||||
className,
|
||||
'btn group-btn btn-xs job-group-count filter-shown',
|
||||
];
|
||||
|
||||
return (
|
||||
<button
|
||||
className={classes.join(' ')}
|
||||
title={title}
|
||||
onClick={onClick}
|
||||
>{count}</button>
|
||||
<button className={classes.join(' ')} title={title} onClick={onClick}>
|
||||
{count}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -14,10 +14,9 @@ const GroupSymbol = function GroupSymbol(props) {
|
|||
const { symbol, tier, toggleExpanded } = props;
|
||||
|
||||
return (
|
||||
<button
|
||||
className="btn group-symbol"
|
||||
onClick={toggleExpanded}
|
||||
>{symbol}{tier !== 1 && <span className="small text-muted">[tier {tier}]</span>}
|
||||
<button className="btn group-symbol" onClick={toggleExpanded}>
|
||||
{symbol}
|
||||
{tier !== 1 && <span className="small text-muted">[tier {tier}]</span>}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
@ -32,7 +31,6 @@ GroupSymbol.defaultProps = {
|
|||
tier: 1,
|
||||
};
|
||||
|
||||
|
||||
export class JobGroupComponent extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -49,7 +47,9 @@ export class JobGroupComponent extends React.Component {
|
|||
static getDerivedStateFromProps(nextProps, state) {
|
||||
// We should expand this group if it's own state is set to be expanded,
|
||||
// or if the push was set to have all groups expanded.
|
||||
return { expanded: state.expanded || nextProps.pushGroupState === 'expanded' };
|
||||
return {
|
||||
expanded: state.expanded || nextProps.pushGroupState === 'expanded',
|
||||
};
|
||||
}
|
||||
|
||||
setExpanded(isExpanded) {
|
||||
|
@ -61,7 +61,11 @@ export class JobGroupComponent extends React.Component {
|
|||
}
|
||||
|
||||
groupButtonsAndCounts(jobs, expanded) {
|
||||
const { selectedJob, duplicateJobsVisible, groupCountsExpanded } = this.props;
|
||||
const {
|
||||
selectedJob,
|
||||
duplicateJobsVisible,
|
||||
groupCountsExpanded,
|
||||
} = this.props;
|
||||
let buttons = [];
|
||||
const counts = [];
|
||||
|
||||
|
@ -71,20 +75,25 @@ export class JobGroupComponent extends React.Component {
|
|||
} else {
|
||||
const stateCounts = {};
|
||||
const typeSymbolCounts = countBy(jobs, 'job_type_symbol');
|
||||
jobs.forEach((job) => {
|
||||
jobs.forEach(job => {
|
||||
if (!job.visible) return;
|
||||
const status = getStatus(job);
|
||||
let countInfo = {
|
||||
btnClass: getBtnClass(status, job.failure_classification_id),
|
||||
countText: status,
|
||||
};
|
||||
if (thFailureResults.includes(status) ||
|
||||
(typeSymbolCounts[job.job_type_symbol] > 1 && duplicateJobsVisible)) {
|
||||
if (
|
||||
thFailureResults.includes(status) ||
|
||||
(typeSymbolCounts[job.job_type_symbol] > 1 && duplicateJobsVisible)
|
||||
) {
|
||||
// render the job itself, not a count
|
||||
buttons.push(job);
|
||||
} else {
|
||||
countInfo = { ...countInfo, ...stateCounts[countInfo.btnClass] };
|
||||
if ((selectedJob && selectedJob.id === job.id) || countInfo.selectedClasses) {
|
||||
if (
|
||||
(selectedJob && selectedJob.id === job.id) ||
|
||||
countInfo.selectedClasses
|
||||
) {
|
||||
countInfo.selectedClasses = ' selected-count btn-lg-xform';
|
||||
} else {
|
||||
countInfo.selectedClasses = '';
|
||||
|
@ -111,8 +120,17 @@ export class JobGroupComponent extends React.Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
repoName, filterPlatformCb, platform, filterModel,
|
||||
group: { name: groupName, symbol: groupSymbol, tier: groupTier, jobs: groupJobs, mapKey: groupMapKey },
|
||||
repoName,
|
||||
filterPlatformCb,
|
||||
platform,
|
||||
filterModel,
|
||||
group: {
|
||||
name: groupName,
|
||||
symbol: groupSymbol,
|
||||
tier: groupTier,
|
||||
jobs: groupJobs,
|
||||
mapKey: groupMapKey,
|
||||
},
|
||||
} = this.props;
|
||||
const { expanded } = this.state;
|
||||
const { buttons, counts } = this.groupButtonsAndCounts(groupJobs, expanded);
|
||||
|
@ -120,14 +138,8 @@ export class JobGroupComponent extends React.Component {
|
|||
this.toggleExpanded = this.toggleExpanded.bind(this);
|
||||
|
||||
return (
|
||||
<span
|
||||
className="platform-group"
|
||||
data-group-key={groupMapKey}
|
||||
>
|
||||
<span
|
||||
className="disabled job-group"
|
||||
title={groupName}
|
||||
>
|
||||
<span className="platform-group" data-group-key={groupMapKey}>
|
||||
<span className="disabled job-group" title={groupName}>
|
||||
<GroupSymbol
|
||||
symbol={groupSymbol}
|
||||
tier={groupTier}
|
||||
|
@ -154,8 +166,12 @@ export class JobGroupComponent extends React.Component {
|
|||
<JobCount
|
||||
count={countInfo.count}
|
||||
onClick={this.toggleExpanded}
|
||||
className={`${countInfo.btnClass}-count${countInfo.selectedClasses}`}
|
||||
title={`${countInfo.count} ${countInfo.countText} jobs in group`}
|
||||
className={`${countInfo.btnClass}-count${
|
||||
countInfo.selectedClasses
|
||||
}`}
|
||||
title={`${countInfo.count} ${
|
||||
countInfo.countText
|
||||
} jobs in group`}
|
||||
key={countInfo.lastJob.id}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -9,43 +9,49 @@ import JobGroup from './JobGroup';
|
|||
export default class JobsAndGroups extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
groups, repoName, platform, filterPlatformCb, filterModel,
|
||||
pushGroupState, duplicateJobsVisible, groupCountsExpanded,
|
||||
groups,
|
||||
repoName,
|
||||
platform,
|
||||
filterPlatformCb,
|
||||
filterModel,
|
||||
pushGroupState,
|
||||
duplicateJobsVisible,
|
||||
groupCountsExpanded,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<td className="job-row">
|
||||
{groups.map((group) => {
|
||||
{groups.map(group => {
|
||||
if (group.tier !== 1 || group.symbol !== '') {
|
||||
return (
|
||||
group.visible && <JobGroup
|
||||
group={group}
|
||||
repoName={repoName}
|
||||
filterModel={filterModel}
|
||||
filterPlatformCb={filterPlatformCb}
|
||||
platform={platform}
|
||||
key={group.mapKey}
|
||||
pushGroupState={pushGroupState}
|
||||
duplicateJobsVisible={duplicateJobsVisible}
|
||||
groupCountsExpanded={groupCountsExpanded}
|
||||
/>
|
||||
group.visible && (
|
||||
<JobGroup
|
||||
group={group}
|
||||
repoName={repoName}
|
||||
filterModel={filterModel}
|
||||
filterPlatformCb={filterPlatformCb}
|
||||
platform={platform}
|
||||
key={group.mapKey}
|
||||
pushGroupState={pushGroupState}
|
||||
duplicateJobsVisible={duplicateJobsVisible}
|
||||
groupCountsExpanded={groupCountsExpanded}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
return (
|
||||
group.jobs.map(job => (
|
||||
<JobButton
|
||||
job={job}
|
||||
filterModel={filterModel}
|
||||
repoName={repoName}
|
||||
visible={job.visible}
|
||||
status={getStatus(job)}
|
||||
failureClassificationId={job.failure_classification_id}
|
||||
filterPlatformCb={filterPlatformCb}
|
||||
platform={platform}
|
||||
key={job.id}
|
||||
/>
|
||||
))
|
||||
);
|
||||
return group.jobs.map(job => (
|
||||
<JobButton
|
||||
job={job}
|
||||
filterModel={filterModel}
|
||||
repoName={repoName}
|
||||
visible={job.visible}
|
||||
status={getStatus(job)}
|
||||
failureClassificationId={job.failure_classification_id}
|
||||
filterPlatformCb={filterPlatformCb}
|
||||
platform={platform}
|
||||
key={job.id}
|
||||
/>
|
||||
));
|
||||
})}
|
||||
</td>
|
||||
);
|
||||
|
|
|
@ -18,8 +18,13 @@ PlatformName.propTypes = {
|
|||
|
||||
export default function Platform(props) {
|
||||
const {
|
||||
platform, repoName, filterPlatformCb, filterModel, pushGroupState,
|
||||
duplicateJobsVisible, groupCountsExpanded,
|
||||
platform,
|
||||
repoName,
|
||||
filterPlatformCb,
|
||||
filterModel,
|
||||
pushGroupState,
|
||||
duplicateJobsVisible,
|
||||
groupCountsExpanded,
|
||||
} = props;
|
||||
const { title, groups, id } = platform;
|
||||
|
||||
|
|
|
@ -2,7 +2,11 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
|
||||
import { thEvents, thOptionOrder, thPlatformMap } from '../../helpers/constants';
|
||||
import {
|
||||
thEvents,
|
||||
thOptionOrder,
|
||||
thPlatformMap,
|
||||
} from '../../helpers/constants';
|
||||
import { withPushes } from '../context/Pushes';
|
||||
import { escapeId, getGroupMapKey } from '../../helpers/aggregateId';
|
||||
import { getAllUrlParams } from '../../helpers/location';
|
||||
|
@ -64,20 +68,32 @@ class Push extends React.Component {
|
|||
}
|
||||
|
||||
getJobCount(jobList) {
|
||||
return jobList.reduce((memo, job) => (
|
||||
job.result !== 'superseded' ? { ...memo, [job.state]: memo[job.state] + 1 } : memo
|
||||
), { running: 0, pending: 0, completed: 0 },
|
||||
return jobList.reduce(
|
||||
(memo, job) =>
|
||||
job.result !== 'superseded'
|
||||
? { ...memo, [job.state]: memo[job.state] + 1 }
|
||||
: memo,
|
||||
{ running: 0, pending: 0, completed: 0 },
|
||||
);
|
||||
}
|
||||
|
||||
getJobGroupInfo(job) {
|
||||
const {
|
||||
job_group_name: name, job_group_symbol,
|
||||
platform, platform_option, tier, push_id,
|
||||
job_group_name: name,
|
||||
job_group_symbol,
|
||||
platform,
|
||||
platform_option,
|
||||
tier,
|
||||
push_id,
|
||||
} = job;
|
||||
const symbol = job_group_symbol === '?' ? '' : job_group_symbol;
|
||||
const mapKey = getGroupMapKey(
|
||||
push_id, symbol, tier, platform, platform_option);
|
||||
push_id,
|
||||
symbol,
|
||||
tier,
|
||||
platform,
|
||||
platform_option,
|
||||
);
|
||||
|
||||
return { name, tier, symbol, mapKey };
|
||||
}
|
||||
|
@ -87,7 +103,9 @@ class Push extends React.Component {
|
|||
const percentComplete = getPercentComplete(this.state.jobCounts);
|
||||
const title = `[${allUnclassifiedFailureCount}] ${repoName}`;
|
||||
|
||||
document.title = `${percentComplete}% - ${title}: ${getRevisionTitle(push.revisions)}`;
|
||||
document.title = `${percentComplete}% - ${title}: ${getRevisionTitle(
|
||||
push.revisions,
|
||||
)}`;
|
||||
}
|
||||
|
||||
handleApplyNewJobs(event) {
|
||||
|
@ -132,7 +150,9 @@ class Push extends React.Component {
|
|||
// remove old versions of jobs we just fetched.
|
||||
const existingJobs = jobList.filter(job => !newIds.includes(job.id));
|
||||
const newJobList = [...existingJobs, ...jobs];
|
||||
const platforms = this.sortGroupedJobs(this.groupJobByPlatform(newJobList));
|
||||
const platforms = this.sortGroupedJobs(
|
||||
this.groupJobByPlatform(newJobList),
|
||||
);
|
||||
const jobCounts = this.getJobCount(newJobList);
|
||||
|
||||
this.setState({
|
||||
|
@ -156,12 +176,13 @@ class Push extends React.Component {
|
|||
if (jobList.length === 0) {
|
||||
return platforms;
|
||||
}
|
||||
jobList.forEach((job) => {
|
||||
jobList.forEach(job => {
|
||||
// search for the right platform
|
||||
const platformName = thPlatformMap[job.platform] || job.platform;
|
||||
let platform = platforms.find(platform =>
|
||||
platformName === platform.name &&
|
||||
job.platform_option === platform.option,
|
||||
let platform = platforms.find(
|
||||
platform =>
|
||||
platformName === platform.name &&
|
||||
job.platform_option === platform.option,
|
||||
);
|
||||
if (platform === undefined) {
|
||||
platform = {
|
||||
|
@ -174,9 +195,9 @@ class Push extends React.Component {
|
|||
|
||||
const groupInfo = this.getJobGroupInfo(job);
|
||||
// search for the right group
|
||||
let group = platform.groups.find(group =>
|
||||
groupInfo.symbol === group.symbol &&
|
||||
groupInfo.tier === group.tier,
|
||||
let group = platform.groups.find(
|
||||
group =>
|
||||
groupInfo.symbol === group.symbol && groupInfo.tier === group.tier,
|
||||
);
|
||||
if (group === undefined) {
|
||||
group = { ...groupInfo, jobs: [] };
|
||||
|
@ -188,21 +209,26 @@ class Push extends React.Component {
|
|||
}
|
||||
|
||||
sortGroupedJobs(platforms) {
|
||||
platforms.forEach((platform) => {
|
||||
platform.groups.forEach((group) => {
|
||||
group.jobs = sortBy(group.jobs, job => (
|
||||
platforms.forEach(platform => {
|
||||
platform.groups.forEach(group => {
|
||||
group.jobs = sortBy(group.jobs, job =>
|
||||
// Symbol could be something like 1, 2 or 3. Or A, B, C or R1, R2, R10.
|
||||
// So this will pad the numeric portion with 0s like R001, R010, etc.
|
||||
job.job_type_symbol.replace(/([\D]*)([\d]*)/g,
|
||||
(matcher, s1, s2) => (s2 !== '' ? s1 + `00${s2}`.slice(-3) : matcher))
|
||||
));
|
||||
job.job_type_symbol.replace(/([\D]*)([\d]*)/g, (matcher, s1, s2) =>
|
||||
s2 !== '' ? s1 + `00${s2}`.slice(-3) : matcher,
|
||||
),
|
||||
);
|
||||
});
|
||||
platform.groups.sort((a, b) => a.symbol.length + a.tier - b.symbol.length - b.tier);
|
||||
platform.groups.sort(
|
||||
(a, b) => a.symbol.length + a.tier - b.symbol.length - b.tier,
|
||||
);
|
||||
});
|
||||
platforms.sort((a, b) => (
|
||||
(platformArray.indexOf(a.name) * 100 + (thOptionOrder[a.option] || 10)) -
|
||||
(platformArray.indexOf(b.name) * 100 + (thOptionOrder[b.option] || 10))
|
||||
));
|
||||
platforms.sort(
|
||||
(a, b) =>
|
||||
platformArray.indexOf(a.name) * 100 +
|
||||
(thOptionOrder[a.option] || 10) -
|
||||
(platformArray.indexOf(b.name) * 100 + (thOptionOrder[b.option] || 10)),
|
||||
);
|
||||
return platforms;
|
||||
}
|
||||
|
||||
|
@ -219,10 +245,17 @@ class Push extends React.Component {
|
|||
showUpdateNotifications(prevState) {
|
||||
const { watched, jobCounts } = this.state;
|
||||
const {
|
||||
repoName, notificationSupported, push: { revision, id: pushId }, notify,
|
||||
repoName,
|
||||
notificationSupported,
|
||||
push: { revision, id: pushId },
|
||||
notify,
|
||||
} = this.props;
|
||||
|
||||
if (!notificationSupported || Notification.permission !== 'granted' || watched === 'none') {
|
||||
if (
|
||||
!notificationSupported ||
|
||||
Notification.permission !== 'granted' ||
|
||||
watched === 'none'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -249,11 +282,11 @@ class Push extends React.Component {
|
|||
tag: pushId,
|
||||
});
|
||||
|
||||
notification.onerror = (event) => {
|
||||
notification.onerror = event => {
|
||||
notify(`${event.target.title}: ${event.target.body}`, 'danger');
|
||||
};
|
||||
|
||||
notification.onclick = (event) => {
|
||||
notification.onclick = event => {
|
||||
if (this.container) {
|
||||
this.container.scrollIntoView();
|
||||
event.target.close();
|
||||
|
@ -268,10 +301,12 @@ class Push extends React.Component {
|
|||
|
||||
try {
|
||||
const decisionTaskId = await getGeckoDecisionTaskId(push.id, repoName);
|
||||
const jobList = await RunnableJobModel.getList(repoName, { decision_task_id: decisionTaskId });
|
||||
const jobList = await RunnableJobModel.getList(repoName, {
|
||||
decision_task_id: decisionTaskId,
|
||||
});
|
||||
const { id } = push;
|
||||
|
||||
jobList.forEach((job) => {
|
||||
jobList.forEach(job => {
|
||||
job.push_id = id;
|
||||
job.id = escapeId(job.push_id + job.ref_data_name);
|
||||
});
|
||||
|
@ -281,7 +316,10 @@ class Push extends React.Component {
|
|||
this.mapPushJobs(jobList, true);
|
||||
this.setState({ runnableVisible: jobList.length > 0 });
|
||||
} catch (error) {
|
||||
notify(`Error fetching runnable jobs: Failed to fetch task ID (${error})`, 'danger');
|
||||
notify(
|
||||
`Error fetching runnable jobs: Failed to fetch task ID (${error})`,
|
||||
'danger',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -289,11 +327,14 @@ class Push extends React.Component {
|
|||
const { jobList } = this.state;
|
||||
const newJobList = jobList.filter(job => job.state !== 'runnable');
|
||||
|
||||
this.setState({
|
||||
runnableVisible: false,
|
||||
selectedRunnableJobs: [],
|
||||
jobList: newJobList,
|
||||
}, () => this.mapPushJobs(newJobList));
|
||||
this.setState(
|
||||
{
|
||||
runnableVisible: false,
|
||||
selectedRunnableJobs: [],
|
||||
jobList: newJobList,
|
||||
},
|
||||
() => this.mapPushJobs(newJobList),
|
||||
);
|
||||
}
|
||||
|
||||
async cycleWatchState() {
|
||||
|
@ -303,7 +344,8 @@ class Push extends React.Component {
|
|||
return;
|
||||
}
|
||||
|
||||
let next = watchCycleStates[watchCycleStates.indexOf(this.state.watched) + 1];
|
||||
let next =
|
||||
watchCycleStates[watchCycleStates.indexOf(this.state.watched) + 1];
|
||||
|
||||
if (next !== 'none' && Notification.permission !== 'granted') {
|
||||
const result = await Notification.requestPermission();
|
||||
|
@ -319,13 +361,24 @@ class Push extends React.Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
push, isLoggedIn, repoName, currentRepo, duplicateJobsVisible,
|
||||
filterModel, notificationSupported, getAllShownJobs, groupCountsExpanded,
|
||||
push,
|
||||
isLoggedIn,
|
||||
repoName,
|
||||
currentRepo,
|
||||
duplicateJobsVisible,
|
||||
filterModel,
|
||||
notificationSupported,
|
||||
getAllShownJobs,
|
||||
groupCountsExpanded,
|
||||
isOnlyRevision,
|
||||
} = this.props;
|
||||
const {
|
||||
watched, runnableVisible, pushGroupState,
|
||||
platforms, jobCounts, selectedRunnableJobs,
|
||||
watched,
|
||||
runnableVisible,
|
||||
pushGroupState,
|
||||
platforms,
|
||||
jobCounts,
|
||||
selectedRunnableJobs,
|
||||
} = this.state;
|
||||
const { id, push_timestamp, revision, author } = push;
|
||||
|
||||
|
@ -334,7 +387,12 @@ class Push extends React.Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="push" ref={(ref) => { this.container = ref; }}>
|
||||
<div
|
||||
className="push"
|
||||
ref={ref => {
|
||||
this.container = ref;
|
||||
}}
|
||||
>
|
||||
<PushHeader
|
||||
push={push}
|
||||
pushId={id}
|
||||
|
@ -357,12 +415,7 @@ class Push extends React.Component {
|
|||
/>
|
||||
<div className="push-body-divider" />
|
||||
<div className="row push clearfix">
|
||||
{currentRepo &&
|
||||
<RevisionList
|
||||
push={push}
|
||||
repo={currentRepo}
|
||||
/>
|
||||
}
|
||||
{currentRepo && <RevisionList push={push} repo={currentRepo} />}
|
||||
<span className="job-list job-list-pad col-7">
|
||||
<PushJobs
|
||||
push={push}
|
||||
|
|
|
@ -56,19 +56,27 @@ class PushActionMenu extends React.PureComponent {
|
|||
triggerMissingJobs() {
|
||||
const { getGeckoDecisionTaskId, notify } = this.props;
|
||||
|
||||
if (!window.confirm(`This will trigger all missing jobs for revision ${this.revision}!\n\nClick "OK" if you want to proceed.`)) {
|
||||
if (
|
||||
!window.confirm(
|
||||
`This will trigger all missing jobs for revision ${
|
||||
this.revision
|
||||
}!\n\nClick "OK" if you want to proceed.`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
getGeckoDecisionTaskId(this.pushId)
|
||||
.then((decisionTaskID) => {
|
||||
.then(decisionTaskID => {
|
||||
PushModel.triggerMissingJobs(decisionTaskID)
|
||||
.then((msg) => {
|
||||
.then(msg => {
|
||||
notify(msg, 'success');
|
||||
}).catch((e) => {
|
||||
})
|
||||
.catch(e => {
|
||||
notify(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
});
|
||||
}).catch((e) => {
|
||||
})
|
||||
.catch(e => {
|
||||
notify(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
});
|
||||
}
|
||||
|
@ -76,24 +84,38 @@ class PushActionMenu extends React.PureComponent {
|
|||
triggerAllTalosJobs() {
|
||||
const { getGeckoDecisionTaskId, notify } = this.props;
|
||||
|
||||
if (!window.confirm(`This will trigger all Talos jobs for revision ${this.revision}!\n\nClick "OK" if you want to proceed.`)) {
|
||||
if (
|
||||
!window.confirm(
|
||||
`This will trigger all Talos jobs for revision ${
|
||||
this.revision
|
||||
}!\n\nClick "OK" if you want to proceed.`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let times = parseInt(window.prompt('Enter number of instances to have for each talos job', 6), 10);
|
||||
let times = parseInt(
|
||||
window.prompt('Enter number of instances to have for each talos job', 6),
|
||||
10,
|
||||
);
|
||||
while (times < 1 || times > 6 || Number.isNaN(times)) {
|
||||
times = window.prompt('We only allow instances of each talos job to be between 1 to 6 times. Enter again', 6);
|
||||
times = window.prompt(
|
||||
'We only allow instances of each talos job to be between 1 to 6 times. Enter again',
|
||||
6,
|
||||
);
|
||||
}
|
||||
|
||||
getGeckoDecisionTaskId(this.pushId)
|
||||
.then((decisionTaskID) => {
|
||||
.then(decisionTaskID => {
|
||||
PushModel.triggerAllTalosJobs(times, decisionTaskID)
|
||||
.then((msg) => {
|
||||
.then(msg => {
|
||||
notify(msg, 'success');
|
||||
}).catch((e) => {
|
||||
})
|
||||
.catch(e => {
|
||||
notify(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
});
|
||||
}).catch((e) => {
|
||||
})
|
||||
.catch(e => {
|
||||
notify(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
});
|
||||
}
|
||||
|
@ -105,9 +127,20 @@ class PushActionMenu extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { isLoggedIn, repoName, revision, runnableVisible,
|
||||
hideRunnableJobs, showRunnableJobs, pushId } = this.props;
|
||||
const { topOfRangeUrl, bottomOfRangeUrl, customJobActionsShowing } = this.state;
|
||||
const {
|
||||
isLoggedIn,
|
||||
repoName,
|
||||
revision,
|
||||
runnableVisible,
|
||||
hideRunnableJobs,
|
||||
showRunnableJobs,
|
||||
pushId,
|
||||
} = this.props;
|
||||
const {
|
||||
topOfRangeUrl,
|
||||
bottomOfRangeUrl,
|
||||
customJobActionsShowing,
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<span className="btn-group dropdown" dropdown="true">
|
||||
|
@ -124,57 +157,96 @@ class PushActionMenu extends React.PureComponent {
|
|||
</button>
|
||||
|
||||
<ul className="dropdown-menu pull-right">
|
||||
{runnableVisible ?
|
||||
{runnableVisible ? (
|
||||
<li
|
||||
title="Hide Runnable Jobs"
|
||||
className="dropdown-item"
|
||||
onClick={hideRunnableJobs}
|
||||
>Hide Runnable Jobs</li> :
|
||||
>
|
||||
Hide Runnable Jobs
|
||||
</li>
|
||||
) : (
|
||||
<li
|
||||
title={isLoggedIn ? 'Add new jobs to this push' : 'Must be logged in'}
|
||||
className={isLoggedIn ? 'dropdown-item' : 'dropdown-item disabled'}
|
||||
title={
|
||||
isLoggedIn ? 'Add new jobs to this push' : 'Must be logged in'
|
||||
}
|
||||
className={
|
||||
isLoggedIn ? 'dropdown-item' : 'dropdown-item disabled'
|
||||
}
|
||||
onClick={showRunnableJobs}
|
||||
>Add new jobs</li>
|
||||
}
|
||||
{this.triggerMissingRepos.includes(repoName) &&
|
||||
>
|
||||
Add new jobs
|
||||
</li>
|
||||
)}
|
||||
{this.triggerMissingRepos.includes(repoName) && (
|
||||
<li
|
||||
title={isLoggedIn ? 'Trigger all jobs that were optimized away' : 'Must be logged in'}
|
||||
className={isLoggedIn ? 'dropdown-item' : 'dropdown-item disabled'}
|
||||
title={
|
||||
isLoggedIn
|
||||
? 'Trigger all jobs that were optimized away'
|
||||
: 'Must be logged in'
|
||||
}
|
||||
className={
|
||||
isLoggedIn ? 'dropdown-item' : 'dropdown-item disabled'
|
||||
}
|
||||
onClick={() => this.triggerMissingJobs(revision)}
|
||||
>Trigger missing jobs</li>
|
||||
}
|
||||
>
|
||||
Trigger missing jobs
|
||||
</li>
|
||||
)}
|
||||
<li
|
||||
title={isLoggedIn ? 'Trigger all talos performance tests' : 'Must be logged in'}
|
||||
title={
|
||||
isLoggedIn
|
||||
? 'Trigger all talos performance tests'
|
||||
: 'Must be logged in'
|
||||
}
|
||||
className={isLoggedIn ? 'dropdown-item' : 'dropdown-item disabled'}
|
||||
onClick={() => this.triggerAllTalosJobs(revision)}
|
||||
>Trigger all Talos jobs</li>
|
||||
<li><a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="dropdown-item"
|
||||
href={`https://bugherder.mozilla.org/?cset=${revision}&tree=${repoName}`}
|
||||
title="Use Bugherder to mark the bugs in this push"
|
||||
>Mark with Bugherder</a></li>
|
||||
>
|
||||
Trigger all Talos jobs
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="dropdown-item"
|
||||
href={`https://bugherder.mozilla.org/?cset=${revision}&tree=${repoName}`}
|
||||
title="Use Bugherder to mark the bugs in this push"
|
||||
>
|
||||
Mark with Bugherder
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className="dropdown-item"
|
||||
onClick={this.toggleCustomJobActions}
|
||||
title="View/Edit/Submit Action tasks for this push"
|
||||
>Custom Push Action...</li>
|
||||
<li><a
|
||||
className="dropdown-item top-of-range-menu-item"
|
||||
href={topOfRangeUrl}
|
||||
>Set as top of range</a></li>
|
||||
<li><a
|
||||
className="dropdown-item bottom-of-range-menu-item"
|
||||
href={bottomOfRangeUrl}
|
||||
>Set as bottom of range</a></li>
|
||||
>
|
||||
Custom Push Action...
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="dropdown-item top-of-range-menu-item"
|
||||
href={topOfRangeUrl}
|
||||
>
|
||||
Set as top of range
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="dropdown-item bottom-of-range-menu-item"
|
||||
href={bottomOfRangeUrl}
|
||||
>
|
||||
Set as bottom of range
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{customJobActionsShowing && <CustomJobActions
|
||||
job={null}
|
||||
pushId={pushId}
|
||||
isLoggedIn={isLoggedIn}
|
||||
toggle={this.toggleCustomJobActions}
|
||||
/>}
|
||||
{customJobActionsShowing && (
|
||||
<CustomJobActions
|
||||
job={null}
|
||||
pushId={pushId}
|
||||
isLoggedIn={isLoggedIn}
|
||||
toggle={this.toggleCustomJobActions}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,7 +16,13 @@ import PushActionMenu from './PushActionMenu';
|
|||
// url params we don't want added from the current querystring to the revision
|
||||
// and author links.
|
||||
const SKIPPED_LINK_PARAMS = [
|
||||
'revision', 'fromchange', 'tochange', 'nojobs', 'startdate', 'enddate', 'author',
|
||||
'revision',
|
||||
'fromchange',
|
||||
'tochange',
|
||||
'nojobs',
|
||||
'startdate',
|
||||
'enddate',
|
||||
'author',
|
||||
];
|
||||
|
||||
function Author(props) {
|
||||
|
@ -43,14 +49,12 @@ function PushCounts(props) {
|
|||
|
||||
return (
|
||||
<span className="push-progress">
|
||||
{percentComplete === 100 &&
|
||||
<span>- Complete -</span>
|
||||
}
|
||||
{percentComplete < 100 && total > 0 &&
|
||||
<span
|
||||
title="Proportion of jobs that are complete"
|
||||
>{percentComplete}% - {inProgress} in progress</span>
|
||||
}
|
||||
{percentComplete === 100 && <span>- Complete -</span>}
|
||||
{percentComplete < 100 && total > 0 && (
|
||||
<span title="Proportion of jobs that are complete">
|
||||
{percentComplete}% - {inProgress} in progress
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -78,34 +82,45 @@ class PushHeader extends React.PureComponent {
|
|||
getLinkParams() {
|
||||
const { filterModel } = this.props;
|
||||
|
||||
return Object.entries(filterModel.getUrlParamsWithoutDefaults())
|
||||
.reduce((acc, [field, values]) => (
|
||||
SKIPPED_LINK_PARAMS.includes(field) ? acc : { ...acc, [field]: values }
|
||||
), {});
|
||||
return Object.entries(filterModel.getUrlParamsWithoutDefaults()).reduce(
|
||||
(acc, [field, values]) =>
|
||||
SKIPPED_LINK_PARAMS.includes(field) ? acc : { ...acc, [field]: values },
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
triggerNewJobs() {
|
||||
const {
|
||||
isLoggedIn, pushId, getGeckoDecisionTaskId, selectedRunnableJobs,
|
||||
hideRunnableJobs, notify,
|
||||
isLoggedIn,
|
||||
pushId,
|
||||
getGeckoDecisionTaskId,
|
||||
selectedRunnableJobs,
|
||||
hideRunnableJobs,
|
||||
notify,
|
||||
} = this.props;
|
||||
|
||||
if (!window.confirm(
|
||||
'This will trigger all selected jobs. Click "OK" if you want to proceed.')) {
|
||||
if (
|
||||
!window.confirm(
|
||||
'This will trigger all selected jobs. Click "OK" if you want to proceed.',
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (isLoggedIn) {
|
||||
const builderNames = selectedRunnableJobs;
|
||||
getGeckoDecisionTaskId(pushId)
|
||||
.then((decisionTaskID) => {
|
||||
PushModel.triggerNewJobs(builderNames, decisionTaskID).then((result) => {
|
||||
notify(result, 'success');
|
||||
hideRunnableJobs(pushId);
|
||||
this.props.hideRunnableJobs();
|
||||
}).catch((e) => {
|
||||
notify(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
});
|
||||
}).catch((e) => {
|
||||
.then(decisionTaskID => {
|
||||
PushModel.triggerNewJobs(builderNames, decisionTaskID)
|
||||
.then(result => {
|
||||
notify(result, 'success');
|
||||
hideRunnableJobs(pushId);
|
||||
this.props.hideRunnableJobs();
|
||||
})
|
||||
.catch(e => {
|
||||
notify(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
notify(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
});
|
||||
} else {
|
||||
|
@ -116,7 +131,11 @@ class PushHeader extends React.PureComponent {
|
|||
cancelAllJobs() {
|
||||
const { notify, repoName } = this.props;
|
||||
|
||||
if (window.confirm('This will cancel all pending and running jobs for this push. It cannot be undone! Are you sure?')) {
|
||||
if (
|
||||
window.confirm(
|
||||
'This will cancel all pending and running jobs for this push. It cannot be undone! Are you sure?',
|
||||
)
|
||||
) {
|
||||
const { push, isLoggedIn, getGeckoDecisionTaskId } = this.props;
|
||||
|
||||
if (!isLoggedIn) return;
|
||||
|
@ -127,8 +146,13 @@ class PushHeader extends React.PureComponent {
|
|||
|
||||
pinAllShownJobs() {
|
||||
const {
|
||||
selectedJob, setSelectedJob, pinJobs, expandAllPushGroups, getAllShownJobs,
|
||||
notify, pushId,
|
||||
selectedJob,
|
||||
setSelectedJob,
|
||||
pinJobs,
|
||||
expandAllPushGroups,
|
||||
getAllShownJobs,
|
||||
notify,
|
||||
pushId,
|
||||
} = this.props;
|
||||
const shownJobs = getAllShownJobs(pushId);
|
||||
|
||||
|
@ -145,13 +169,24 @@ class PushHeader extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { repoName, isLoggedIn, pushId, jobCounts, author,
|
||||
revision, runnableVisible, watchState,
|
||||
showRunnableJobs, hideRunnableJobs, cycleWatchState,
|
||||
notificationSupported, selectedRunnableJobs } = this.props;
|
||||
const cancelJobsTitle = isLoggedIn ?
|
||||
'Cancel all jobs' :
|
||||
'Must be logged in to cancel jobs';
|
||||
const {
|
||||
repoName,
|
||||
isLoggedIn,
|
||||
pushId,
|
||||
jobCounts,
|
||||
author,
|
||||
revision,
|
||||
runnableVisible,
|
||||
watchState,
|
||||
showRunnableJobs,
|
||||
hideRunnableJobs,
|
||||
cycleWatchState,
|
||||
notificationSupported,
|
||||
selectedRunnableJobs,
|
||||
} = this.props;
|
||||
const cancelJobsTitle = isLoggedIn
|
||||
? 'Cancel all jobs'
|
||||
: 'Must be logged in to cancel jobs';
|
||||
const linkParams = this.getLinkParams();
|
||||
const revisionPushFilterUrl = getJobsUrl({ ...linkParams, revision });
|
||||
const authorPushFilterUrl = getJobsUrl({ ...linkParams, author });
|
||||
|
@ -168,11 +203,12 @@ class PushHeader extends React.PureComponent {
|
|||
<span className="push-left">
|
||||
<span className="push-title-left">
|
||||
<span>
|
||||
<a
|
||||
href={revisionPushFilterUrl}
|
||||
title="View only this push"
|
||||
>{this.pushDateStr} <span className="fa fa-external-link icon-superscript" />
|
||||
</a> - </span>
|
||||
<a href={revisionPushFilterUrl} title="View only this push">
|
||||
{this.pushDateStr}{' '}
|
||||
<span className="fa fa-external-link icon-superscript" />
|
||||
</a>{' '}
|
||||
-{' '}
|
||||
</span>
|
||||
<Author author={author} url={authorPushFilterUrl} />
|
||||
</span>
|
||||
</span>
|
||||
|
@ -183,49 +219,56 @@ class PushHeader extends React.PureComponent {
|
|||
completed={jobCounts.completed}
|
||||
/>
|
||||
<span className="push-buttons">
|
||||
{jobCounts.pending + jobCounts.running > 0 &&
|
||||
{jobCounts.pending + jobCounts.running > 0 && (
|
||||
<button
|
||||
className="btn btn-sm btn-push watch-commit-btn"
|
||||
disabled={!notificationSupported}
|
||||
title={notificationSupported ? 'Get Desktop Notifications for this Push' : 'Desktop notifications not supported in this browser'}
|
||||
title={
|
||||
notificationSupported
|
||||
? 'Get Desktop Notifications for this Push'
|
||||
: 'Desktop notifications not supported in this browser'
|
||||
}
|
||||
data-watch-state={watchState}
|
||||
onClick={() => cycleWatchState()}
|
||||
>{watchStateLabel}</button>}
|
||||
>
|
||||
{watchStateLabel}
|
||||
</button>
|
||||
)}
|
||||
<a
|
||||
className="btn btn-sm btn-push test-view-btn"
|
||||
href={`/testview.html?repo=${repoName}&revision=${revision}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="View details on failed test results for this push"
|
||||
>View Tests</a>
|
||||
{isLoggedIn &&
|
||||
>
|
||||
View Tests
|
||||
</a>
|
||||
{isLoggedIn && (
|
||||
<button
|
||||
className="btn btn-sm btn-push cancel-all-jobs-btn"
|
||||
title={cancelJobsTitle}
|
||||
onClick={this.cancelAllJobs}
|
||||
>
|
||||
<span
|
||||
className="fa fa-times-circle cancel-job-icon dim-quarter"
|
||||
/>
|
||||
<span className="fa fa-times-circle cancel-job-icon dim-quarter" />
|
||||
</button>
|
||||
}
|
||||
)}
|
||||
<button
|
||||
className="btn btn-sm btn-push pin-all-jobs-btn"
|
||||
title="Pin all available jobs in this push"
|
||||
aria-label="Pin all available jobs in this push"
|
||||
onClick={this.pinAllShownJobs}
|
||||
>
|
||||
<span
|
||||
className="fa fa-thumb-tack"
|
||||
/>
|
||||
<span className="fa fa-thumb-tack" />
|
||||
</button>
|
||||
{!!selectedRunnableJobs.length && runnableVisible &&
|
||||
{!!selectedRunnableJobs.length && runnableVisible && (
|
||||
<button
|
||||
className="btn btn-sm btn-push trigger-new-jobs-btn"
|
||||
title="Trigger new jobs"
|
||||
onClick={this.triggerNewJobs}
|
||||
>Trigger New Jobs</button>
|
||||
}
|
||||
>
|
||||
Trigger New Jobs
|
||||
</button>
|
||||
)}
|
||||
<PushActionMenu
|
||||
isLoggedIn={isLoggedIn}
|
||||
runnableVisible={runnableVisible}
|
||||
|
@ -273,4 +316,6 @@ PushHeader.defaultProps = {
|
|||
watchState: 'none',
|
||||
};
|
||||
|
||||
export default withNotifications(withPushes(withSelectedJob(withPinnedJobs(PushHeader))));
|
||||
export default withNotifications(
|
||||
withPushes(withSelectedJob(withPinnedJobs(PushHeader))),
|
||||
);
|
||||
|
|
|
@ -19,20 +19,38 @@ class PushJobs extends React.Component {
|
|||
const selectedJobId = parseInt(getUrlParam('selectedJob'), 10);
|
||||
const filteredPlatforms = platforms.reduce((acc, platform) => {
|
||||
const thisPlatform = { ...platform };
|
||||
const suffix = (thSimplePlatforms.includes(platform.name) && platform.option === 'opt') ? '' : ` ${platform.option}`;
|
||||
const suffix =
|
||||
thSimplePlatforms.includes(platform.name) && platform.option === 'opt'
|
||||
? ''
|
||||
: ` ${platform.option}`;
|
||||
thisPlatform.title = `${thisPlatform.name}${suffix}`;
|
||||
thisPlatform.visible = true;
|
||||
return [...acc, PushJobs.filterPlatform(thisPlatform, selectedJobId, push, filterModel, runnableVisible)];
|
||||
return [
|
||||
...acc,
|
||||
PushJobs.filterPlatform(
|
||||
thisPlatform,
|
||||
selectedJobId,
|
||||
push,
|
||||
filterModel,
|
||||
runnableVisible,
|
||||
),
|
||||
];
|
||||
}, []);
|
||||
|
||||
return { filteredPlatforms };
|
||||
}
|
||||
|
||||
static filterPlatform(platform, selectedJobId, push, filterModel, runnableVisible) {
|
||||
static filterPlatform(
|
||||
platform,
|
||||
selectedJobId,
|
||||
push,
|
||||
filterModel,
|
||||
runnableVisible,
|
||||
) {
|
||||
platform.visible = false;
|
||||
platform.groups.forEach((group) => {
|
||||
platform.groups.forEach(group => {
|
||||
group.visible = false;
|
||||
group.jobs.forEach((job) => {
|
||||
group.jobs.forEach(job => {
|
||||
job.visible = filterModel.showJob(job) || job.id === selectedJobId;
|
||||
if (job.state === 'runnable') {
|
||||
job.visible = job.visible && runnableVisible;
|
||||
|
@ -52,11 +70,7 @@ class PushJobs extends React.Component {
|
|||
const { push, repoName } = this.props;
|
||||
|
||||
this.pushId = push.id;
|
||||
this.aggregateId = getPushTableId(
|
||||
repoName,
|
||||
this.pushId,
|
||||
push.revision,
|
||||
);
|
||||
this.aggregateId = getPushTableId(repoName, this.pushId, push.revision);
|
||||
|
||||
this.state = {
|
||||
filteredPlatforms: [],
|
||||
|
@ -75,14 +89,17 @@ class PushJobs extends React.Component {
|
|||
|
||||
if (jobInstance && jobInstance.props.job) {
|
||||
const { job } = jobInstance.props;
|
||||
if (ev.button === 1) { // Middle click
|
||||
if (ev.button === 1) {
|
||||
// Middle click
|
||||
this.handleLogViewerClick(job.id);
|
||||
} else if (ev.metaKey || ev.ctrlKey) { // Pin job
|
||||
} else if (ev.metaKey || ev.ctrlKey) {
|
||||
// Pin job
|
||||
if (!selectedJob) {
|
||||
this.selectJob(job, ev.target);
|
||||
}
|
||||
togglePinJob(job);
|
||||
} else if (job && job.state === 'runnable') { // Toggle runnable
|
||||
} else if (job && job.state === 'runnable') {
|
||||
// Toggle runnable
|
||||
this.handleRunnableClick(jobInstance);
|
||||
} else {
|
||||
this.selectJob(job, ev.target); // Left click
|
||||
|
@ -106,13 +123,11 @@ class PushJobs extends React.Component {
|
|||
handleLogViewerClick(jobId) {
|
||||
// Open logviewer in a new window
|
||||
const { repoName } = this.props;
|
||||
JobModel.get(
|
||||
repoName,
|
||||
jobId,
|
||||
).then((data) => {
|
||||
JobModel.get(repoName, jobId).then(data => {
|
||||
if (data.logs.length > 0) {
|
||||
window.open(`${window.location.origin}/${
|
||||
getLogViewerUrl(jobId, repoName)}`);
|
||||
window.open(
|
||||
`${window.location.origin}/${getLogViewerUrl(jobId, repoName)}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -130,7 +145,13 @@ class PushJobs extends React.Component {
|
|||
|
||||
// This actually filters the platform in-place. So we just need to
|
||||
// trigger a re-render by giving it a new ``filteredPlatforms`` object instance.
|
||||
PushJobs.filterPlatform(platform, selectedJobId, push, filterModel, runnableVisible);
|
||||
PushJobs.filterPlatform(
|
||||
platform,
|
||||
selectedJobId,
|
||||
push,
|
||||
filterModel,
|
||||
runnableVisible,
|
||||
);
|
||||
if (filteredPlatforms.length) {
|
||||
this.setState({ filteredPlatforms: [...filteredPlatforms] });
|
||||
}
|
||||
|
@ -139,28 +160,39 @@ class PushJobs extends React.Component {
|
|||
render() {
|
||||
const filteredPlatforms = this.state.filteredPlatforms || [];
|
||||
const {
|
||||
repoName, filterModel, pushGroupState, duplicateJobsVisible,
|
||||
repoName,
|
||||
filterModel,
|
||||
pushGroupState,
|
||||
duplicateJobsVisible,
|
||||
groupCountsExpanded,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<table id={this.aggregateId} className="table-hover">
|
||||
<tbody onMouseDown={this.onMouseDown}>
|
||||
{filteredPlatforms ? filteredPlatforms.map(platform => (
|
||||
platform.visible &&
|
||||
<Platform
|
||||
platform={platform}
|
||||
repoName={repoName}
|
||||
key={platform.title}
|
||||
filterModel={filterModel}
|
||||
pushGroupState={pushGroupState}
|
||||
filterPlatformCb={this.filterPlatformCallback}
|
||||
duplicateJobsVisible={duplicateJobsVisible}
|
||||
groupCountsExpanded={groupCountsExpanded}
|
||||
/>
|
||||
)) : <tr>
|
||||
<td><span className="fa fa-spinner fa-pulse th-spinner" /></td>
|
||||
</tr>}
|
||||
{filteredPlatforms ? (
|
||||
filteredPlatforms.map(
|
||||
platform =>
|
||||
platform.visible && (
|
||||
<Platform
|
||||
platform={platform}
|
||||
repoName={repoName}
|
||||
key={platform.title}
|
||||
filterModel={filterModel}
|
||||
pushGroupState={pushGroupState}
|
||||
filterPlatformCb={this.filterPlatformCallback}
|
||||
duplicateJobsVisible={duplicateJobsVisible}
|
||||
groupCountsExpanded={groupCountsExpanded}
|
||||
/>
|
||||
),
|
||||
)
|
||||
) : (
|
||||
<tr>
|
||||
<td>
|
||||
<span className="fa fa-spinner fa-pulse th-spinner" />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
|
|
@ -24,8 +24,16 @@ class PushList extends React.Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
user, repoName, revision, currentRepo, filterModel, pushList,
|
||||
loadingPushes, getNextPushes, jobsLoaded, duplicateJobsVisible,
|
||||
user,
|
||||
repoName,
|
||||
revision,
|
||||
currentRepo,
|
||||
filterModel,
|
||||
pushList,
|
||||
loadingPushes,
|
||||
getNextPushes,
|
||||
jobsLoaded,
|
||||
duplicateJobsVisible,
|
||||
groupCountsExpanded,
|
||||
} = this.props;
|
||||
const { notificationSupported } = this.state;
|
||||
|
@ -38,48 +46,51 @@ class PushList extends React.Component {
|
|||
return (
|
||||
<div>
|
||||
{jobsLoaded && <span className="hidden ready" />}
|
||||
{repoName && pushList.map(push => (
|
||||
<ErrorBoundary
|
||||
errorClasses="pl-2 border-top border-bottom border-dark d-block"
|
||||
message={`Error on push with revision ${push.revision}: `}
|
||||
key={push.id}
|
||||
>
|
||||
<Push
|
||||
push={push}
|
||||
isLoggedIn={isLoggedIn || false}
|
||||
currentRepo={currentRepo}
|
||||
repoName={repoName}
|
||||
filterModel={filterModel}
|
||||
notificationSupported={notificationSupported}
|
||||
duplicateJobsVisible={duplicateJobsVisible}
|
||||
groupCountsExpanded={groupCountsExpanded}
|
||||
isOnlyRevision={push.revision === revision}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
{loadingPushes &&
|
||||
{repoName &&
|
||||
pushList.map(push => (
|
||||
<ErrorBoundary
|
||||
errorClasses="pl-2 border-top border-bottom border-dark d-block"
|
||||
message={`Error on push with revision ${push.revision}: `}
|
||||
key={push.id}
|
||||
>
|
||||
<Push
|
||||
push={push}
|
||||
isLoggedIn={isLoggedIn || false}
|
||||
currentRepo={currentRepo}
|
||||
repoName={repoName}
|
||||
filterModel={filterModel}
|
||||
notificationSupported={notificationSupported}
|
||||
duplicateJobsVisible={duplicateJobsVisible}
|
||||
groupCountsExpanded={groupCountsExpanded}
|
||||
isOnlyRevision={push.revision === revision}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
{loadingPushes && (
|
||||
<div
|
||||
className="progress active progress-bar progress-bar-striped"
|
||||
role="progressbar"
|
||||
/>
|
||||
}
|
||||
{pushList.length === 0 && !loadingPushes &&
|
||||
)}
|
||||
{pushList.length === 0 && !loadingPushes && (
|
||||
<PushLoadErrors
|
||||
loadingPushes={loadingPushes}
|
||||
currentRepo={currentRepo}
|
||||
repoName={repoName}
|
||||
revision={revision}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
<div className="card card-body get-next">
|
||||
<span>get next:</span>
|
||||
<div className="btn-group">
|
||||
{[10, 20, 50].map(count => (
|
||||
<div
|
||||
className="btn btn-light-bordered"
|
||||
onClick={() => (getNextPushes(count))}
|
||||
onClick={() => getNextPushes(count)}
|
||||
key={count}
|
||||
>{count}</div>
|
||||
>
|
||||
{count}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -10,60 +10,79 @@ function PushLoadErrors(props) {
|
|||
const urlParams = getAllUrlParams();
|
||||
urlParams.delete('revision');
|
||||
|
||||
const isRevision = revision => (
|
||||
revision && (revision.length === 12 || revision.length === 40)
|
||||
);
|
||||
const isRevision = revision =>
|
||||
revision && (revision.length === 12 || revision.length === 40);
|
||||
|
||||
return (
|
||||
<div className="push-load-errors">
|
||||
{!loadingPushes && isRevision(revision) && currentRepo && currentRepo.url &&
|
||||
<div className="push-body unknown-message-body">
|
||||
<span>
|
||||
{revision &&
|
||||
<div>
|
||||
<p>
|
||||
Waiting for push with revision
|
||||
<a
|
||||
href={currentRepo.getPushLogHref(revision)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title={`Open revision ${revision} on ${currentRepo.url}`}
|
||||
>{revision}</a>
|
||||
|
||||
<span className="fa fa-spinner fa-pulse th-spinner" />
|
||||
</p>
|
||||
<p>If the push exists, it will appear in a few minutes once it has been processed.</p>
|
||||
</div>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
{!loadingPushes && !isRevision(revision) && currentRepo.url &&
|
||||
{!loadingPushes &&
|
||||
isRevision(revision) &&
|
||||
currentRepo &&
|
||||
currentRepo.url && (
|
||||
<div className="push-body unknown-message-body">
|
||||
<span>
|
||||
{revision && (
|
||||
<div>
|
||||
<p>
|
||||
Waiting for push with revision
|
||||
<a
|
||||
href={currentRepo.getPushLogHref(revision)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title={`Open revision ${revision} on ${currentRepo.url}`}
|
||||
>
|
||||
{revision}
|
||||
</a>
|
||||
|
||||
<span className="fa fa-spinner fa-pulse th-spinner" />
|
||||
</p>
|
||||
<p>
|
||||
If the push exists, it will appear in a few minutes once it
|
||||
has been processed.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{!loadingPushes && !isRevision(revision) && currentRepo.url && (
|
||||
<div className="push-body unknown-message-body">
|
||||
This is an invalid or unknown revision. Please change it, or click
|
||||
<a href={`${uiJobsUrlBase}?${urlParams.toString()}`}> here</a> to reload the latest revisions from {repoName}.
|
||||
<a href={`${uiJobsUrlBase}?${urlParams.toString()}`}> here</a> to
|
||||
reload the latest revisions from {repoName}.
|
||||
</div>
|
||||
}
|
||||
{!loadingPushes && !revision && currentRepo && currentRepo.url &&
|
||||
)}
|
||||
{!loadingPushes && !revision && currentRepo && currentRepo.url && (
|
||||
<div className="push-body unknown-message-body">
|
||||
<span>
|
||||
<div><b>No pushes found.</b></div>
|
||||
<span>No commit information could be loaded for this repository.
|
||||
More information about this repository can be found <a href={currentRepo.url}>here</a>.</span>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
{!loadingPushes && !currentRepo.url &&
|
||||
<div className="push-body unknown-message-body">
|
||||
<span>
|
||||
<div><b>Unknown repository.</b></div>
|
||||
<span>This repository is either unknown to Treeherder or it does not exist.
|
||||
If this repository does exist, please <a href="https://bugzilla.mozilla.org/enter_bug.cgi?product=Tree%20Management&component=Treeherder">
|
||||
file a bug against the Treeherder product in Bugzilla</a> to get it added to the system.
|
||||
<div>
|
||||
<b>No pushes found.</b>
|
||||
</div>
|
||||
<span>
|
||||
No commit information could be loaded for this repository. More
|
||||
information about this repository can be found{' '}
|
||||
<a href={currentRepo.url}>here</a>.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
{!loadingPushes && !currentRepo.url && (
|
||||
<div className="push-body unknown-message-body">
|
||||
<span>
|
||||
<div>
|
||||
<b>Unknown repository.</b>
|
||||
</div>
|
||||
<span>
|
||||
This repository is either unknown to Treeherder or it does not
|
||||
exist. If this repository does exist, please{' '}
|
||||
<a href="https://bugzilla.mozilla.org/enter_bug.cgi?product=Tree%20Management&component=Treeherder">
|
||||
file a bug against the Treeherder product in Bugzilla
|
||||
</a>{' '}
|
||||
to get it added to the system.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,9 +7,9 @@ import BugLinkify from '../../shared/BugLinkify';
|
|||
export function Initials(props) {
|
||||
const str = props.author || '';
|
||||
const words = str.split(' ');
|
||||
const firstLetters = words.map(
|
||||
word => word.replace(/[^A-Z]/gi, '')[0],
|
||||
).filter(firstLetter => typeof firstLetter !== 'undefined');
|
||||
const firstLetters = words
|
||||
.map(word => word.replace(/[^A-Z]/gi, '')[0])
|
||||
.filter(firstLetter => typeof firstLetter !== 'undefined');
|
||||
let initials = '';
|
||||
|
||||
if (firstLetters.length === 1) {
|
||||
|
@ -41,8 +41,11 @@ export class Revision extends React.PureComponent {
|
|||
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
this.comment = revision.comments.split('\n')[0];
|
||||
this.tags = this.comment.search('Backed out') >= 0 || this.comment.search('Back out') >= 0 ?
|
||||
'backout' : '';
|
||||
this.tags =
|
||||
this.comment.search('Backed out') >= 0 ||
|
||||
this.comment.search('Back out') >= 0
|
||||
? 'backout'
|
||||
: '';
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -50,28 +53,28 @@ export class Revision extends React.PureComponent {
|
|||
const { name, email } = parseAuthor(revision.author);
|
||||
const commitRevision = revision.revision;
|
||||
|
||||
return (<li className="clearfix">
|
||||
<span className="revision" data-tags={this.tags}>
|
||||
<span className="revision-holder">
|
||||
<a
|
||||
title={`Open revision ${commitRevision} on ${repo.url}`}
|
||||
href={repo.getRevisionHref(commitRevision)}
|
||||
>{commitRevision.substring(0, 12)}
|
||||
</a>
|
||||
</span>
|
||||
<Initials
|
||||
title={`${name}: ${email}`}
|
||||
author={name}
|
||||
/>
|
||||
<span title={this.comment}>
|
||||
<span className="revision-comment">
|
||||
<em>
|
||||
<BugLinkify>{this.comment}</BugLinkify>
|
||||
</em>
|
||||
return (
|
||||
<li className="clearfix">
|
||||
<span className="revision" data-tags={this.tags}>
|
||||
<span className="revision-holder">
|
||||
<a
|
||||
title={`Open revision ${commitRevision} on ${repo.url}`}
|
||||
href={repo.getRevisionHref(commitRevision)}
|
||||
>
|
||||
{commitRevision.substring(0, 12)}
|
||||
</a>
|
||||
</span>
|
||||
<Initials title={`${name}: ${email}`} author={name} />
|
||||
<span title={this.comment}>
|
||||
<span className="revision-comment">
|
||||
<em>
|
||||
<BugLinkify>{this.comment}</BugLinkify>
|
||||
</em>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</li>);
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,19 +15,15 @@ export class RevisionList extends React.PureComponent {
|
|||
return (
|
||||
<span className="revision-list col-5">
|
||||
<ul className="list-unstyled">
|
||||
{push.revisions.map(revision =>
|
||||
(<Revision
|
||||
revision={revision}
|
||||
repo={repo}
|
||||
key={revision.revision}
|
||||
/>),
|
||||
{push.revisions.map(revision => (
|
||||
<Revision revision={revision} repo={repo} key={revision.revision} />
|
||||
))}
|
||||
{this.hasMore && (
|
||||
<MoreRevisionsLink
|
||||
key="more"
|
||||
href={repo.getPushLogHref(push.revision)}
|
||||
/>
|
||||
)}
|
||||
{this.hasMore &&
|
||||
<MoreRevisionsLink
|
||||
key="more"
|
||||
href={repo.getPushLogHref(push.revision)}
|
||||
/>
|
||||
}
|
||||
</ul>
|
||||
</span>
|
||||
);
|
||||
|
@ -42,11 +38,10 @@ RevisionList.propTypes = {
|
|||
export function MoreRevisionsLink(props) {
|
||||
return (
|
||||
<li>
|
||||
<a
|
||||
href={props.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{'\u2026and more'}<i className="fa fa-external-link-square" /></a>
|
||||
<a href={props.href} target="_blank" rel="noopener noreferrer">
|
||||
{'\u2026and more'}
|
||||
<i className="fa fa-external-link-square" />
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -49,7 +49,9 @@ class LoginCallback extends React.PureComponent {
|
|||
}
|
||||
|
||||
setError(err) {
|
||||
this.setState({ loginError: err.message ? err.message : err.errorDescription });
|
||||
this.setState({
|
||||
loginError: err.message ? err.message : err.errorDescription,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -37,7 +37,6 @@ const errorLinesCss = function errorLinesCss(errors) {
|
|||
style.sheet.insertRule(rule);
|
||||
};
|
||||
|
||||
|
||||
class App extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -64,48 +63,64 @@ class App extends React.PureComponent {
|
|||
componentDidMount() {
|
||||
const { repoName, jobId } = this.state;
|
||||
|
||||
JobModel.get(repoName, jobId).then(async (job) => {
|
||||
// set the title of the browser window/tab
|
||||
document.title = job.getTitle();
|
||||
const rawLogUrl = job.logs && job.logs.length ? job.logs[0].url : null;
|
||||
// other properties, in order of appearance
|
||||
// Test to disable successful steps checkbox on taskcluster jobs
|
||||
// Test to expose the reftest button in the logviewer actionbar
|
||||
const reftestUrl = rawLogUrl && job.job_group_name && isReftest(job)
|
||||
? getReftestUrl(rawLogUrl) : null;
|
||||
const jobDetails = await JobDetailModel.getJobDetails({ job_id: jobId });
|
||||
JobModel.get(repoName, jobId)
|
||||
.then(async job => {
|
||||
// set the title of the browser window/tab
|
||||
document.title = job.getTitle();
|
||||
const rawLogUrl = job.logs && job.logs.length ? job.logs[0].url : null;
|
||||
// other properties, in order of appearance
|
||||
// Test to disable successful steps checkbox on taskcluster jobs
|
||||
// Test to expose the reftest button in the logviewer actionbar
|
||||
const reftestUrl =
|
||||
rawLogUrl && job.job_group_name && isReftest(job)
|
||||
? getReftestUrl(rawLogUrl)
|
||||
: null;
|
||||
const jobDetails = await JobDetailModel.getJobDetails({
|
||||
job_id: jobId,
|
||||
});
|
||||
|
||||
this.setState({
|
||||
job,
|
||||
rawLogUrl,
|
||||
reftestUrl,
|
||||
jobDetails,
|
||||
jobExists: true,
|
||||
}, async () => {
|
||||
// get the revision and linkify it
|
||||
PushModel.get(job.push_id).then(async (resp) => {
|
||||
const push = await resp.json();
|
||||
const { revision } = push;
|
||||
this.setState(
|
||||
{
|
||||
job,
|
||||
rawLogUrl,
|
||||
reftestUrl,
|
||||
jobDetails,
|
||||
jobExists: true,
|
||||
},
|
||||
async () => {
|
||||
// get the revision and linkify it
|
||||
PushModel.get(job.push_id).then(async resp => {
|
||||
const push = await resp.json();
|
||||
const { revision } = push;
|
||||
|
||||
this.setState({
|
||||
revision,
|
||||
jobUrl: getJobsUrl({ repo: repoName, revision, selectedJob: jobId }),
|
||||
});
|
||||
this.setState({
|
||||
revision,
|
||||
jobUrl: getJobsUrl({
|
||||
repo: repoName,
|
||||
revision,
|
||||
selectedJob: jobId,
|
||||
}),
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState({
|
||||
jobExists: false,
|
||||
jobError: error.toString(),
|
||||
});
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.setState({
|
||||
jobExists: false,
|
||||
jobError: error.toString(),
|
||||
});
|
||||
});
|
||||
|
||||
TextLogStepModel.get(jobId).then((textLogSteps) => {
|
||||
TextLogStepModel.get(jobId).then(textLogSteps => {
|
||||
const stepErrors = textLogSteps.length ? textLogSteps[0].errors : [];
|
||||
const errors = stepErrors.map(error => (
|
||||
{ line: error.line, lineNumber: error.line_number + 1 }
|
||||
));
|
||||
const firstErrorLineNumber = errors.length ? [errors[0].lineNumber] : null;
|
||||
const errors = stepErrors.map(error => ({
|
||||
line: error.line,
|
||||
lineNumber: error.line_number + 1,
|
||||
}));
|
||||
const firstErrorLineNumber = errors.length
|
||||
? [errors[0].lineNumber]
|
||||
: null;
|
||||
const urlLN = getUrlLineNumber();
|
||||
const highlight = urlLN || firstErrorLineNumber;
|
||||
|
||||
|
@ -173,14 +188,24 @@ class App extends React.PureComponent {
|
|||
|
||||
render() {
|
||||
const {
|
||||
job, rawLogUrl, reftestUrl, jobDetails, jobError, jobExists,
|
||||
revision, errors, highlight, jobUrl,
|
||||
job,
|
||||
rawLogUrl,
|
||||
reftestUrl,
|
||||
jobDetails,
|
||||
jobError,
|
||||
jobExists,
|
||||
revision,
|
||||
errors,
|
||||
highlight,
|
||||
jobUrl,
|
||||
} = this.state;
|
||||
const extraFields = [{
|
||||
title: 'Revision',
|
||||
url: jobUrl,
|
||||
value: revision,
|
||||
}];
|
||||
const extraFields = [
|
||||
{
|
||||
title: 'Revision',
|
||||
url: jobUrl,
|
||||
value: revision,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-column body-logviewer h-100">
|
||||
|
@ -205,10 +230,7 @@ class App extends React.PureComponent {
|
|||
/>
|
||||
<JobDetails jobDetails={jobDetails} />
|
||||
</div>
|
||||
<ErrorLines
|
||||
errors={errors}
|
||||
onClickLine={this.setSelectedLine}
|
||||
/>
|
||||
<ErrorLines errors={errors} onClickLine={this.setSelectedLine} />
|
||||
</div>
|
||||
<div className="log-contents flex-fill">
|
||||
<LazyLog
|
||||
|
|
|
@ -9,7 +9,12 @@ const getShadingClass = result => `result-status-shading-${result}`;
|
|||
export default class Navigation extends React.PureComponent {
|
||||
render() {
|
||||
const {
|
||||
jobExists, result, jobError, jobUrl, rawLogUrl, reftestUrl,
|
||||
jobExists,
|
||||
result,
|
||||
jobError,
|
||||
jobUrl,
|
||||
rawLogUrl,
|
||||
reftestUrl,
|
||||
} = this.props;
|
||||
const resultStatusShading = getShadingClass(result);
|
||||
|
||||
|
@ -19,21 +24,18 @@ export default class Navigation extends React.PureComponent {
|
|||
<span id="lv-logo">
|
||||
<LogoMenu menuText="Logviewer" />
|
||||
</span>
|
||||
{jobExists
|
||||
? (
|
||||
<span className={`lightgray ${resultStatusShading} pt-2 pl-2 pr-2`}>
|
||||
<strong>Result: </strong>
|
||||
{result}
|
||||
{jobExists ? (
|
||||
<span className={`lightgray ${resultStatusShading} pt-2 pl-2 pr-2`}>
|
||||
<strong>Result: </strong>
|
||||
{result}
|
||||
</span>
|
||||
) : (
|
||||
<span className="alert-danger">
|
||||
<span title="The job does not exist or has expired">
|
||||
{`Unavailable: ${jobError}`}
|
||||
</span>
|
||||
) : (
|
||||
<span className="alert-danger">
|
||||
<span
|
||||
title="The job does not exist or has expired"
|
||||
>
|
||||
{`Unavailable: ${jobError}`}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{!!jobUrl && (
|
||||
<span>
|
||||
<a
|
||||
|
|
|
@ -10,10 +10,9 @@ export default class BugJobMapModel {
|
|||
|
||||
// the options parameter is used to filter/limit the list of objects
|
||||
static getList(options) {
|
||||
return fetch(`${uri}${createQueryParams(options)}`)
|
||||
.then(resp => resp.json().then(data => (
|
||||
data.map(elem => new BugJobMapModel(elem))
|
||||
)));
|
||||
return fetch(`${uri}${createQueryParams(options)}`).then(resp =>
|
||||
resp.json().then(data => data.map(elem => new BugJobMapModel(elem))),
|
||||
);
|
||||
}
|
||||
|
||||
create() {
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче