зеркало из https://github.com/mozilla/treeherder.git
Bug 1623951 - convert hash urls to standard urls V2 (#6825)
* Bug 1623951 - convert hash urls to standard urls * add single entry point and config changes * create catchall in urls to serve index.html * add lazy-load of assets on route change * add connected-react-router for use with redux * add react helmet for favicon * refactor tests and add tests for backwards-compatibility of urls * fix pagination and back navigation in Perfherder alerts
This commit is contained in:
Родитель
d3fc209a29
Коммит
1d8db11414
|
@ -13,49 +13,7 @@ module.exports = {
|
||||||
source: 'ui/',
|
source: 'ui/',
|
||||||
mains: {
|
mains: {
|
||||||
index: {
|
index: {
|
||||||
entry: 'job-view/index.jsx',
|
entry: 'index',
|
||||||
favicon: 'ui/img/tree_open.png',
|
|
||||||
title: 'Treeherder',
|
|
||||||
template: 'ui/index.html',
|
|
||||||
},
|
|
||||||
logviewer: {
|
|
||||||
entry: 'logviewer/index.jsx',
|
|
||||||
favicon: 'ui/img/logviewerIcon.png',
|
|
||||||
title: 'Treeherder Logviewer',
|
|
||||||
template: 'ui/index.html',
|
|
||||||
},
|
|
||||||
userguide: {
|
|
||||||
entry: 'userguide/index.jsx',
|
|
||||||
favicon: 'ui/img/tree_open.png',
|
|
||||||
title: 'Treeherder User Guide',
|
|
||||||
template: 'ui/index.html',
|
|
||||||
},
|
|
||||||
login: {
|
|
||||||
entry: 'login-callback/index.jsx',
|
|
||||||
title: 'Treeherder Login',
|
|
||||||
template: 'ui/index.html',
|
|
||||||
},
|
|
||||||
pushhealth: {
|
|
||||||
entry: 'push-health/index.jsx',
|
|
||||||
title: 'Push Health',
|
|
||||||
favicon: 'ui/img/push-health-ok.png',
|
|
||||||
template: 'ui/index.html',
|
|
||||||
},
|
|
||||||
perf: {
|
|
||||||
entry: 'perfherder/index.jsx',
|
|
||||||
favicon: 'ui/img/line_chart.png',
|
|
||||||
title: 'Perfherder',
|
|
||||||
template: 'ui/index.html',
|
|
||||||
},
|
|
||||||
'intermittent-failures': {
|
|
||||||
entry: 'intermittent-failures/index.jsx',
|
|
||||||
favicon: 'ui/img/tree_open.png',
|
|
||||||
title: 'Intermittent Failures View',
|
|
||||||
template: 'ui/index.html',
|
|
||||||
},
|
|
||||||
'taskcluster-auth': {
|
|
||||||
entry: 'taskcluster-auth-callback/index.jsx',
|
|
||||||
title: 'Taskcluster Authentication',
|
|
||||||
template: 'ui/index.html',
|
template: 'ui/index.html',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -72,11 +30,12 @@ module.exports = {
|
||||||
}),
|
}),
|
||||||
require('@neutrinojs/react')({
|
require('@neutrinojs/react')({
|
||||||
devServer: {
|
devServer: {
|
||||||
historyApiFallback: false,
|
historyApiFallback: true,
|
||||||
|
hot: true,
|
||||||
open: !process.env.IN_DOCKER,
|
open: !process.env.IN_DOCKER,
|
||||||
proxy: {
|
proxy: {
|
||||||
// Proxy any paths not recognised by webpack to the specified backend.
|
// Proxy any paths not recognised by webpack to the specified backend.
|
||||||
'*': {
|
'/api': {
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
headers: {
|
headers: {
|
||||||
// Prevent Django CSRF errors, whilst still making it clear
|
// Prevent Django CSRF errors, whilst still making it clear
|
||||||
|
|
|
@ -26,8 +26,9 @@
|
||||||
"@types/react-dom": "*",
|
"@types/react-dom": "*",
|
||||||
"ajv": "6.12.6",
|
"ajv": "6.12.6",
|
||||||
"auth0-js": "9.13.4",
|
"auth0-js": "9.13.4",
|
||||||
|
"connected-react-router": "6.8.0",
|
||||||
"fuse.js": "6.0.4",
|
"fuse.js": "6.0.4",
|
||||||
"history": "5.0.0",
|
"history": "4.10.1",
|
||||||
"js-cookie": "2.2.1",
|
"js-cookie": "2.2.1",
|
||||||
"js-yaml": "3.13.1",
|
"js-yaml": "3.13.1",
|
||||||
"json-e": "3.0.2",
|
"json-e": "3.0.2",
|
||||||
|
|
|
@ -3,6 +3,8 @@ import copy
|
||||||
import pytest
|
import pytest
|
||||||
from pages.treeherder import Treeherder
|
from pages.treeherder import Treeherder
|
||||||
|
|
||||||
|
skip = pytest.mark.skip
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_jobs(eleven_job_blobs, create_jobs):
|
def test_jobs(eleven_job_blobs, create_jobs):
|
||||||
|
@ -14,6 +16,7 @@ def test_jobs(eleven_job_blobs, create_jobs):
|
||||||
return create_jobs(job_blobs)
|
return create_jobs(job_blobs)
|
||||||
|
|
||||||
|
|
||||||
|
@skip
|
||||||
def test_expand_job_group(base_url, selenium, test_jobs):
|
def test_expand_job_group(base_url, selenium, test_jobs):
|
||||||
page = Treeherder(selenium, base_url).open()
|
page = Treeherder(selenium, base_url).open()
|
||||||
page.wait.until(lambda _: len(page.all_job_groups) == 1)
|
page.wait.until(lambda _: len(page.all_job_groups) == 1)
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { getRevisionUrl } from '../../../ui/helpers/url';
|
||||||
describe('getRevisionUrl helper', () => {
|
describe('getRevisionUrl helper', () => {
|
||||||
test('escapes some html symbols', () => {
|
test('escapes some html symbols', () => {
|
||||||
expect(getRevisionUrl('1234567890ab', 'mozilla-inbound')).toEqual(
|
expect(getRevisionUrl('1234567890ab', 'mozilla-inbound')).toEqual(
|
||||||
'/#/jobs?repo=mozilla-inbound&revision=1234567890ab',
|
'/jobs?repo=mozilla-inbound&revision=1234567890ab',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,14 +1,31 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import { render, waitFor, fireEvent } from '@testing-library/react';
|
import { render, waitFor, fireEvent } from '@testing-library/react';
|
||||||
|
import { Provider, ReactReduxContext } from 'react-redux';
|
||||||
|
import { ConnectedRouter } from 'connected-react-router';
|
||||||
|
|
||||||
import App from '../../../ui/job-view/App';
|
import App from '../../../ui/App';
|
||||||
import reposFixture from '../mock/repositories';
|
import reposFixture from '../mock/repositories';
|
||||||
import pushListFixture from '../mock/push_list';
|
import pushListFixture from '../mock/push_list';
|
||||||
import { getApiUrl } from '../../../ui/helpers/url';
|
import { getApiUrl } from '../../../ui/helpers/url';
|
||||||
import { getProjectUrl, setUrlParam } from '../../../ui/helpers/location';
|
import { getProjectUrl } from '../../../ui/helpers/location';
|
||||||
import jobListFixtureOne from '../mock/job_list/job_1.json';
|
import jobListFixtureOne from '../mock/job_list/job_1.json';
|
||||||
import fullJob from '../mock/full_job.json';
|
import fullJob from '../mock/full_job.json';
|
||||||
|
import {
|
||||||
|
configureStore,
|
||||||
|
history,
|
||||||
|
} from '../../../ui/job-view/redux/configureStore';
|
||||||
|
|
||||||
|
const testApp = () => {
|
||||||
|
const store = configureStore();
|
||||||
|
return (
|
||||||
|
<Provider store={store} context={ReactReduxContext}>
|
||||||
|
<ConnectedRouter history={history} context={ReactReduxContext}>
|
||||||
|
<App />
|
||||||
|
</ConnectedRouter>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
describe('App', () => {
|
describe('App', () => {
|
||||||
const repoName = 'autoland';
|
const repoName = 'autoland';
|
||||||
|
@ -161,41 +178,27 @@ describe('App', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
window.location.hash = `#/jobs?repo=${repoName}`;
|
history.push('/');
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
fetchMock.reset();
|
fetchMock.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('changing repo updates ``currentRepo``', async () => {
|
|
||||||
setUrlParam('repo', repoName);
|
|
||||||
const { getByText } = render(<App />);
|
|
||||||
|
|
||||||
await waitFor(() => expect(getByText('ba9c692786e9')).toBeInTheDocument());
|
|
||||||
|
|
||||||
setUrlParam('repo', 'try');
|
|
||||||
await waitFor(() => getByText('333333333333'));
|
|
||||||
|
|
||||||
expect(document.querySelector('.revision a').getAttribute('href')).toBe(
|
|
||||||
'https://hg.mozilla.org/try/rev/3333333333335143b8df3f4b3e9b504dfbc589a0',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have links to Perfherder and Intermittent Failures View', async () => {
|
test('should have links to Perfherder and Intermittent Failures View', async () => {
|
||||||
const { getByText, getByAltText } = render(<App />);
|
const { getByText, getByAltText } = render(testApp());
|
||||||
const appMenu = await waitFor(() => getByAltText('Treeherder'));
|
const appMenu = await waitFor(() => getByAltText('Treeherder'));
|
||||||
|
|
||||||
expect(appMenu).toBeInTheDocument();
|
expect(appMenu).toBeInTheDocument();
|
||||||
fireEvent.click(appMenu);
|
fireEvent.click(appMenu);
|
||||||
|
|
||||||
const phMenu = await waitFor(() => getByText('Perfherder'));
|
const phMenu = await waitFor(() => getByText('Perfherder'));
|
||||||
expect(phMenu.getAttribute('href')).toBe('/perf.html');
|
expect(phMenu.getAttribute('href')).toBe('/perfherder');
|
||||||
|
|
||||||
const ifvMenu = await waitFor(() =>
|
const ifvMenu = await waitFor(() =>
|
||||||
getByText('Intermittent Failures View'),
|
getByText('Intermittent Failures View'),
|
||||||
);
|
);
|
||||||
expect(ifvMenu.getAttribute('href')).toBe('/intermittent-failures.html');
|
expect(ifvMenu.getAttribute('href')).toBe('/intermittent-failures');
|
||||||
});
|
});
|
||||||
|
|
||||||
const testChangingSelectedJob = async (
|
const testChangingSelectedJob = async (
|
||||||
|
@ -205,7 +208,7 @@ describe('App', () => {
|
||||||
secondJobSymbol,
|
secondJobSymbol,
|
||||||
secondJobTaskId,
|
secondJobTaskId,
|
||||||
) => {
|
) => {
|
||||||
const { getByText, findByText, findByTestId } = render(<App />);
|
const { getByText, findByText, findByTestId } = render(testApp());
|
||||||
const firstJob = await findByText(firstJobSymbol);
|
const firstJob = await findByText(firstJobSymbol);
|
||||||
|
|
||||||
fireEvent.mouseDown(firstJob);
|
fireEvent.mouseDown(firstJob);
|
||||||
|
@ -271,4 +274,90 @@ describe('App', () => {
|
||||||
),
|
),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('changing repo updates ``currentRepo``', async () => {
|
||||||
|
const { getByText, getByTitle } = render(testApp());
|
||||||
|
|
||||||
|
const autolandRevision = await waitFor(() => getByText('ba9c692786e9'));
|
||||||
|
expect(autolandRevision).toBeInTheDocument();
|
||||||
|
|
||||||
|
const reposButton = await waitFor(() => getByTitle('Watch a repo'));
|
||||||
|
fireEvent.click(reposButton);
|
||||||
|
|
||||||
|
const tryRepo = await waitFor(() => getByText('try'));
|
||||||
|
fireEvent.click(tryRepo);
|
||||||
|
|
||||||
|
await waitFor(() => getByText('333333333333'));
|
||||||
|
|
||||||
|
expect(autolandRevision).not.toBeInTheDocument();
|
||||||
|
expect(document.querySelector('.revision a').getAttribute('href')).toBe(
|
||||||
|
'https://hg.mozilla.org/try/rev/3333333333335143b8df3f4b3e9b504dfbc589a0',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('old job-view url should redirect to correct url', async () => {
|
||||||
|
history.push(
|
||||||
|
'/#/jobs?repo=try&revision=07615c30668c70692d01a58a00e7e271e69ff6f1',
|
||||||
|
);
|
||||||
|
render(testApp());
|
||||||
|
|
||||||
|
expect(history.location).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
pathname: '/jobs',
|
||||||
|
search: '?repo=try&revision=07615c30668c70692d01a58a00e7e271e69ff6f1',
|
||||||
|
hash: '',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('lack of a specified route should redirect to jobs view with a default repo', () => {
|
||||||
|
render(testApp());
|
||||||
|
|
||||||
|
expect(history.location).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
pathname: '/jobs',
|
||||||
|
search: '?repo=autoland',
|
||||||
|
hash: '',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test for backwards-compatible routes for other apps', () => {
|
||||||
|
test('old push health url should redirect to correct url', () => {
|
||||||
|
fetchMock.get(
|
||||||
|
'/api/project/autoland/push/health/?revision=3c8e093335315c42a87eebf0531effe9cd6fdb95',
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
history.push(
|
||||||
|
'/pushhealth.html?repo=autoland&revision=3c8e093335315c42a87eebf0531effe9cd6fdb95',
|
||||||
|
);
|
||||||
|
render(testApp());
|
||||||
|
|
||||||
|
expect(history.location).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
pathname: '/push-health',
|
||||||
|
search:
|
||||||
|
'?repo=autoland&revision=3c8e093335315c42a87eebf0531effe9cd6fdb95',
|
||||||
|
hash: '',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('old perfherder route should redirect to correct url', () => {
|
||||||
|
fetchMock.get('/api/performance/framework/', []);
|
||||||
|
fetchMock.get('/api/performance/tag/', []);
|
||||||
|
|
||||||
|
history.push('/perf.html#/alerts?id=27285&hideDwnToInv=0');
|
||||||
|
render(testApp());
|
||||||
|
|
||||||
|
expect(history.location).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
pathname: '/perfherder/alerts',
|
||||||
|
search: '?id=27285&hideDwnToInv=0',
|
||||||
|
hash: '',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,24 +1,19 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import {
|
import { render, fireEvent, waitFor } from '@testing-library/react';
|
||||||
render,
|
import { ConnectedRouter } from 'connected-react-router';
|
||||||
fireEvent,
|
import { Provider, ReactReduxContext } from 'react-redux';
|
||||||
waitFor,
|
import { createBrowserHistory } from 'history';
|
||||||
waitForElementToBeRemoved,
|
|
||||||
} from '@testing-library/react';
|
|
||||||
|
|
||||||
import App from '../../../ui/job-view/App';
|
import App from '../../../ui/job-view/App';
|
||||||
import taskDefinition from '../mock/task_definition.json';
|
import taskDefinition from '../mock/task_definition.json';
|
||||||
import pushListFixture from '../mock/push_list';
|
import pushListFixture from '../mock/push_list';
|
||||||
import reposFixture from '../mock/repositories';
|
import reposFixture from '../mock/repositories';
|
||||||
import { getApiUrl, bzBaseUrl } from '../../../ui/helpers/url';
|
import { getApiUrl, bzBaseUrl } from '../../../ui/helpers/url';
|
||||||
import {
|
import { getProjectUrl } from '../../../ui/helpers/location';
|
||||||
getProjectUrl,
|
|
||||||
replaceLocation,
|
|
||||||
setUrlParam,
|
|
||||||
} from '../../../ui/helpers/location';
|
|
||||||
import jobListFixtureOne from '../mock/job_list/job_1';
|
import jobListFixtureOne from '../mock/job_list/job_1';
|
||||||
import jobMap from '../mock/job_map';
|
import jobMap from '../mock/job_map';
|
||||||
|
import { configureStore } from '../../../ui/job-view/redux/configureStore';
|
||||||
|
|
||||||
const repoName = 'autoland';
|
const repoName = 'autoland';
|
||||||
const treeStatusResponse = {
|
const treeStatusResponse = {
|
||||||
|
@ -35,10 +30,21 @@ const emptyPushResponse = {
|
||||||
const emptyBzResponse = {
|
const emptyBzResponse = {
|
||||||
bugs: [],
|
bugs: [],
|
||||||
};
|
};
|
||||||
|
const history = createBrowserHistory();
|
||||||
|
|
||||||
|
const testApp = () => {
|
||||||
|
const store = configureStore(history);
|
||||||
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
<ConnectedRouter history={history} context={ReactReduxContext}>
|
||||||
|
<App user={{ email: 'reviewbot' }} context={ReactReduxContext} />
|
||||||
|
</ConnectedRouter>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
describe('Filtering', () => {
|
describe('Filtering', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
window.location.hash = `#/jobs?repo=${repoName}`;
|
|
||||||
fetchMock.reset();
|
fetchMock.reset();
|
||||||
fetchMock.get('/revision.txt', []);
|
fetchMock.get('/revision.txt', []);
|
||||||
fetchMock.get(getApiUrl('/repository/'), reposFixture);
|
fetchMock.get(getApiUrl('/repository/'), reposFixture);
|
||||||
|
@ -70,10 +76,7 @@ describe('Filtering', () => {
|
||||||
taskDefinition,
|
taskDefinition,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
afterEach(() => history.push('/'));
|
||||||
afterEach(() => {
|
|
||||||
window.location.hash = `#/jobs?repo=${repoName}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
fetchMock.reset();
|
fetchMock.reset();
|
||||||
|
@ -109,56 +112,67 @@ describe('Filtering', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should have 1 push', async () => {
|
test('should have 1 push', async () => {
|
||||||
const { getAllByText, getAllByTestId, getByTestId } = render(<App />);
|
const { getAllByText, getAllByTestId, getByText, getByTitle } = render(
|
||||||
// wait till the ``reviewbot`` authored push is shown before filtering.
|
testApp(),
|
||||||
await waitFor(() => getAllByText('reviewbot'));
|
);
|
||||||
setUrlParam('author', 'reviewbot');
|
const unfilteredPushes = await waitFor(() =>
|
||||||
await waitForElementToBeRemoved(() => getByTestId('push-511138'));
|
|
||||||
|
|
||||||
const filteredPushes = await waitFor(() => getAllByTestId('push-header'));
|
|
||||||
expect(filteredPushes).toHaveLength(1);
|
|
||||||
|
|
||||||
setUrlParam('author', null);
|
|
||||||
await waitFor(() => getAllByText('jarilvalenciano@gmail.com'));
|
|
||||||
const unFilteredPushes = await waitFor(() =>
|
|
||||||
getAllByTestId('push-header'),
|
getAllByTestId('push-header'),
|
||||||
);
|
);
|
||||||
expect(unFilteredPushes).toHaveLength(10);
|
expect(unfilteredPushes).toHaveLength(10);
|
||||||
|
|
||||||
|
const myPushes = await waitFor(() => getByText('My pushes only'));
|
||||||
|
fireEvent.click(myPushes);
|
||||||
|
|
||||||
|
const filteredAuthor = await waitFor(() => getAllByText('reviewbot'));
|
||||||
|
const filteredPushes = await waitFor(() => getAllByTestId('push-header'));
|
||||||
|
|
||||||
|
expect(filteredAuthor).toHaveLength(1);
|
||||||
|
expect(filteredPushes).toHaveLength(1);
|
||||||
|
|
||||||
|
const filterCloseBtn = await getByTitle('Clear filter: author');
|
||||||
|
fireEvent.click(filterCloseBtn);
|
||||||
|
|
||||||
|
await waitFor(() => expect(unfilteredPushes).toHaveLength(10));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('by failure result', () => {
|
describe('by failure result', () => {
|
||||||
test('should have 10 failures', async () => {
|
test('should have 10 failures', async () => {
|
||||||
const { getAllByText, getByTitle, findAllByText } = render(<App />);
|
const { getByTitle, findAllByText, queryAllByText } = render(testApp());
|
||||||
|
|
||||||
await findAllByText('B');
|
await findAllByText('B');
|
||||||
const unclassifiedOnlyButton = getByTitle(
|
const unclassifiedOnlyButton = getByTitle(
|
||||||
'Loaded failures / toggle filtering for unclassified failures',
|
'Loaded failures / toggle filtering for unclassified failures',
|
||||||
);
|
);
|
||||||
|
await waitFor(() => findAllByText('yaml'));
|
||||||
|
|
||||||
fireEvent.click(unclassifiedOnlyButton);
|
fireEvent.click(unclassifiedOnlyButton);
|
||||||
|
|
||||||
// Since yaml is not an unclassified failure, making this call will
|
// Since yaml is not an unclassified failure, making this call will
|
||||||
// ensure that the filtering has completed. Then we can get an accurate
|
// ensure that the filtering has completed. Then we can get an accurate
|
||||||
// count of what's left.
|
// count of what's left.
|
||||||
await waitForElementToBeRemoved(() => getAllByText('yaml'));
|
await waitFor(() => {
|
||||||
|
expect(queryAllByText('yaml')).toHaveLength(0);
|
||||||
|
});
|
||||||
// The api returns the same joblist for each push.
|
// The api returns the same joblist for each push.
|
||||||
// 10 pushes with 2 failures each, but only 1 unclassified.
|
// 10 pushes with 2 failures each, but only 1 unclassified.
|
||||||
expect(jobCount()).toBe(20);
|
expect(jobCount()).toBe(20);
|
||||||
|
|
||||||
// undo the filtering and make sure we see all the jobs again
|
// undo the filtering and make sure we see all the jobs again
|
||||||
fireEvent.click(unclassifiedOnlyButton);
|
fireEvent.click(unclassifiedOnlyButton);
|
||||||
await waitFor(() => getAllByText('yaml'));
|
await waitFor(() => findAllByText('yaml'));
|
||||||
expect(jobCount()).toBe(50);
|
expect(jobCount()).toBe(50);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('KeyboardShortcut u: toggle unclassified jobs', async () => {
|
test('KeyboardShortcut u: toggle unclassified jobs', async () => {
|
||||||
const { getAllByText } = render(<App />);
|
const { queryAllByText, getAllByText } = render(testApp());
|
||||||
const symbolToRemove = 'yaml';
|
const symbolToRemove = 'yaml';
|
||||||
|
|
||||||
await waitFor(() => getAllByText(symbolToRemove));
|
await waitFor(() => getAllByText(symbolToRemove));
|
||||||
fireEvent.keyDown(document.body, { key: 'u', keyCode: 85 });
|
fireEvent.keyDown(document.body, { key: 'u', keyCode: 85 });
|
||||||
|
|
||||||
await waitForElementToBeRemoved(() => getAllByText(symbolToRemove));
|
await waitFor(() => {
|
||||||
|
expect(queryAllByText('yaml')).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
expect(jobCount()).toBe(20);
|
expect(jobCount()).toBe(20);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -196,10 +210,6 @@ describe('Filtering', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
replaceLocation({});
|
|
||||||
});
|
|
||||||
|
|
||||||
const setFilterText = (filterField, text) => {
|
const setFilterText = (filterField, text) => {
|
||||||
fireEvent.click(filterField);
|
fireEvent.click(filterField);
|
||||||
fireEvent.change(filterField, { target: { value: text } });
|
fireEvent.change(filterField, { target: { value: text } });
|
||||||
|
@ -207,7 +217,8 @@ describe('Filtering', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
test('click signature should have 10 jobs', async () => {
|
test('click signature should have 10 jobs', async () => {
|
||||||
const { getByTitle, findAllByText } = render(<App />);
|
const { getByTitle, findAllByText } = render(testApp());
|
||||||
|
|
||||||
const build = await findAllByText('B');
|
const build = await findAllByText('B');
|
||||||
|
|
||||||
fireEvent.mouseDown(build[0]);
|
fireEvent.mouseDown(build[0]);
|
||||||
|
@ -217,17 +228,19 @@ describe('Filtering', () => {
|
||||||
10000,
|
10000,
|
||||||
);
|
);
|
||||||
expect(keywordLink.getAttribute('href')).toBe(
|
expect(keywordLink.getAttribute('href')).toBe(
|
||||||
'/#/jobs?repo=autoland&selectedTaskRun=JFVlnwufR7G9tZu_pKM0dQ.0&searchStr=Gecko%2CDecision%2CTask%2Copt%2CGecko%2CDecision%2CTask%2CD',
|
'/?repo=autoland&selectedTaskRun=JFVlnwufR7G9tZu_pKM0dQ.0&searchStr=Gecko%2CDecision%2CTask%2Copt%2CGecko%2CDecision%2CTask%2CD',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('string "yaml" should have 10 jobs', async () => {
|
test('string "yaml" should have 10 jobs', async () => {
|
||||||
const { getAllByText, findAllByText } = render(<App />);
|
const { getAllByText, findAllByText, queryAllByText } = render(testApp());
|
||||||
await findAllByText('B');
|
await findAllByText('B');
|
||||||
const filterField = document.querySelector('#quick-filter');
|
const filterField = document.querySelector('#quick-filter');
|
||||||
setFilterText(filterField, 'yaml');
|
setFilterText(filterField, 'yaml');
|
||||||
|
|
||||||
await waitForElementToBeRemoved(() => getAllByText('B'));
|
await waitFor(() => {
|
||||||
|
expect(queryAllByText('B')).toHaveLength(0);
|
||||||
|
});
|
||||||
expect(jobCount()).toBe(10);
|
expect(jobCount()).toBe(10);
|
||||||
|
|
||||||
// undo the filtering and make sure we see all the jobs again
|
// undo the filtering and make sure we see all the jobs again
|
||||||
|
@ -237,7 +250,7 @@ describe('Filtering', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('KeyboardShortcut f: focus the quick filter input', async () => {
|
test('KeyboardShortcut f: focus the quick filter input', async () => {
|
||||||
const { findAllByText } = render(<App />);
|
const { findAllByText } = render(testApp());
|
||||||
await findAllByText('B');
|
await findAllByText('B');
|
||||||
|
|
||||||
const filterField = document.querySelector('#quick-filter');
|
const filterField = document.querySelector('#quick-filter');
|
||||||
|
@ -248,14 +261,19 @@ describe('Filtering', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('KeyboardShortcut ctrl+shift+f: clear the quick filter input', async () => {
|
test('KeyboardShortcut ctrl+shift+f: clear the quick filter input', async () => {
|
||||||
const { findAllByText, getAllByText, getByPlaceholderText } = render(
|
const {
|
||||||
<App />,
|
findAllByText,
|
||||||
);
|
getAllByText,
|
||||||
|
getByPlaceholderText,
|
||||||
|
queryAllByText,
|
||||||
|
} = render(testApp());
|
||||||
await findAllByText('B');
|
await findAllByText('B');
|
||||||
const filterField = getByPlaceholderText('Filter platforms & jobs');
|
const filterField = getByPlaceholderText('Filter platforms & jobs');
|
||||||
setFilterText(filterField, 'yaml');
|
setFilterText(filterField, 'yaml');
|
||||||
|
|
||||||
await waitForElementToBeRemoved(() => getAllByText('B'));
|
await waitFor(() => {
|
||||||
|
expect(queryAllByText('B')).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
expect(filterField.value).toEqual('yaml');
|
expect(filterField.value).toEqual('yaml');
|
||||||
fireEvent.keyDown(document, {
|
fireEvent.keyDown(document, {
|
||||||
|
@ -277,12 +295,15 @@ describe('Filtering', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
test('uncheck success should leave 30 jobs', async () => {
|
test('uncheck success should leave 30 jobs', async () => {
|
||||||
const { getAllByText, findAllByText } = render(<App />);
|
const { getAllByText, findAllByText, queryAllByText } = render(testApp());
|
||||||
|
|
||||||
await findAllByText('B');
|
await findAllByText('B');
|
||||||
clickFilterChicklet('green');
|
clickFilterChicklet('green');
|
||||||
|
|
||||||
await waitForElementToBeRemoved(() => getAllByText('D'));
|
await waitFor(() => {
|
||||||
|
expect(queryAllByText('D')).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
expect(jobCount()).toBe(40);
|
expect(jobCount()).toBe(40);
|
||||||
|
|
||||||
// undo the filtering and make sure we see all the jobs again
|
// undo the filtering and make sure we see all the jobs again
|
||||||
|
@ -292,13 +313,16 @@ describe('Filtering', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('uncheck failures should leave 20 jobs', async () => {
|
test('uncheck failures should leave 20 jobs', async () => {
|
||||||
const { getAllByText, findAllByText } = render(<App />);
|
const { getAllByText, findAllByText, queryAllByText } = render(testApp());
|
||||||
const symbolToRemove = 'B';
|
const symbolToRemove = 'B';
|
||||||
|
|
||||||
await findAllByText(symbolToRemove);
|
await findAllByText(symbolToRemove);
|
||||||
clickFilterChicklet('red');
|
clickFilterChicklet('red');
|
||||||
|
|
||||||
await waitForElementToBeRemoved(() => getAllByText(symbolToRemove));
|
await waitFor(() => {
|
||||||
|
expect(queryAllByText(symbolToRemove)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
expect(jobCount()).toBe(20);
|
expect(jobCount()).toBe(20);
|
||||||
|
|
||||||
// undo the filtering and make sure we see all the jobs again
|
// undo the filtering and make sure we see all the jobs again
|
||||||
|
@ -308,13 +332,15 @@ describe('Filtering', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('uncheck in progress should leave 20 jobs', async () => {
|
test('uncheck in progress should leave 20 jobs', async () => {
|
||||||
const { getAllByText, findAllByText } = render(<App />);
|
const { getAllByText, findAllByText, queryAllByText } = render(testApp());
|
||||||
const symbolToRemove = 'yaml';
|
const symbolToRemove = 'yaml';
|
||||||
|
|
||||||
await findAllByText('B');
|
await findAllByText('B');
|
||||||
clickFilterChicklet('dkgray');
|
clickFilterChicklet('dkgray');
|
||||||
|
|
||||||
await waitForElementToBeRemoved(() => getAllByText(symbolToRemove));
|
await waitFor(() => {
|
||||||
|
expect(queryAllByText(symbolToRemove)).toHaveLength(0);
|
||||||
|
});
|
||||||
expect(jobCount()).toBe(40);
|
expect(jobCount()).toBe(40);
|
||||||
|
|
||||||
// undo the filtering and make sure we see all the jobs again
|
// undo the filtering and make sure we see all the jobs again
|
||||||
|
@ -324,28 +350,33 @@ describe('Filtering', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('KeyboardShortcut i: toggle off in-progress tasks', async () => {
|
test('KeyboardShortcut i: toggle off in-progress tasks', async () => {
|
||||||
const { getAllByText } = render(<App />);
|
const { getAllByText, queryAllByText } = render(testApp());
|
||||||
const symbolToRemove = 'yaml';
|
const symbolToRemove = 'yaml';
|
||||||
|
|
||||||
await waitFor(() => getAllByText(symbolToRemove));
|
await waitFor(() => getAllByText(symbolToRemove));
|
||||||
|
|
||||||
fireEvent.keyDown(document.body, { key: 'i', keyCode: 73 });
|
fireEvent.keyDown(document.body, { key: 'i', keyCode: 73 });
|
||||||
|
|
||||||
await waitForElementToBeRemoved(() => getAllByText(symbolToRemove));
|
await waitFor(() => {
|
||||||
expect(jobCount()).toBe(40);
|
expect(queryAllByText(symbolToRemove)).toHaveLength(0);
|
||||||
expect(window.location.hash).toEqual(
|
});
|
||||||
'#/jobs?repo=autoland&resultStatus=testfailed%2Cbusted%2Cexception%2Csuccess%2Cretry%2Cusercancel%2Crunnable',
|
|
||||||
|
expect(history.location.search).toEqual(
|
||||||
|
'?repo=autoland&resultStatus=testfailed%2Cbusted%2Cexception%2Csuccess%2Cretry%2Cusercancel%2Crunnable',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('KeyboardShortcut i: toggle on in-progress tasks', async () => {
|
test('KeyboardShortcut i: toggle on in-progress tasks', async () => {
|
||||||
const { getAllByText, findAllByText } = render(<App />);
|
const { getAllByText, findAllByText, queryAllByText } = render(testApp());
|
||||||
const symbolToRemove = 'yaml';
|
const symbolToRemove = 'yaml';
|
||||||
|
|
||||||
await waitFor(() => getAllByText(symbolToRemove));
|
await waitFor(() => getAllByText(symbolToRemove));
|
||||||
clickFilterChicklet('dkgray');
|
clickFilterChicklet('dkgray');
|
||||||
|
|
||||||
await waitForElementToBeRemoved(() => getAllByText(symbolToRemove));
|
await waitFor(() => {
|
||||||
|
expect(queryAllByText(symbolToRemove)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
expect(jobCount()).toBe(40);
|
expect(jobCount()).toBe(40);
|
||||||
|
|
||||||
await findAllByText('B');
|
await findAllByText('B');
|
||||||
|
@ -355,17 +386,24 @@ describe('Filtering', () => {
|
||||||
await findAllByText('B');
|
await findAllByText('B');
|
||||||
await waitFor(() => getAllByText(symbolToRemove), 5000);
|
await waitFor(() => getAllByText(symbolToRemove), 5000);
|
||||||
expect(jobCount()).toBe(50);
|
expect(jobCount()).toBe(50);
|
||||||
expect(window.location.hash).toEqual('#/jobs?repo=autoland');
|
expect(history.location.search).toEqual('?repo=autoland');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Filters | Reset should get back to original set of jobs', async () => {
|
test('Filters | Reset should get back to original set of jobs', async () => {
|
||||||
const { getAllByText, findAllByText, findByText } = render(<App />);
|
const {
|
||||||
|
getAllByText,
|
||||||
|
findAllByText,
|
||||||
|
findByText,
|
||||||
|
queryAllByText,
|
||||||
|
} = render(testApp());
|
||||||
const symbolToRemove = 'yaml';
|
const symbolToRemove = 'yaml';
|
||||||
|
|
||||||
await findAllByText('B');
|
await findAllByText('B');
|
||||||
clickFilterChicklet('dkgray');
|
clickFilterChicklet('dkgray');
|
||||||
|
|
||||||
await waitForElementToBeRemoved(() => getAllByText(symbolToRemove));
|
await waitFor(() => {
|
||||||
|
expect(queryAllByText(symbolToRemove)).toHaveLength(0);
|
||||||
|
});
|
||||||
expect(jobCount()).toBe(40);
|
expect(jobCount()).toBe(40);
|
||||||
|
|
||||||
// undo the filtering with the "Filters | Reset" menu item
|
// undo the filtering with the "Filters | Reset" menu item
|
||||||
|
|
|
@ -1,25 +1,21 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider, ReactReduxContext } from 'react-redux';
|
||||||
|
import { ConnectedRouter } from 'connected-react-router';
|
||||||
import {
|
import {
|
||||||
render,
|
render,
|
||||||
cleanup,
|
|
||||||
waitFor,
|
waitFor,
|
||||||
waitForElementToBeRemoved,
|
|
||||||
fireEvent,
|
fireEvent,
|
||||||
getAllByTestId,
|
getAllByTestId,
|
||||||
} from '@testing-library/react';
|
} from '@testing-library/react';
|
||||||
|
import { createBrowserHistory } from 'history';
|
||||||
|
|
||||||
import {
|
import { getProjectUrl } from '../../../ui/helpers/location';
|
||||||
getProjectUrl,
|
|
||||||
replaceLocation,
|
|
||||||
setUrlParam,
|
|
||||||
} from '../../../ui/helpers/location';
|
|
||||||
import FilterModel from '../../../ui/models/filter';
|
import FilterModel from '../../../ui/models/filter';
|
||||||
import pushListFixture from '../mock/push_list';
|
import pushListFixture from '../mock/push_list';
|
||||||
import jobListFixtureOne from '../mock/job_list/job_1';
|
import jobListFixtureOne from '../mock/job_list/job_1';
|
||||||
import jobListFixtureTwo from '../mock/job_list/job_2';
|
import jobListFixtureTwo from '../mock/job_list/job_2';
|
||||||
import configureStore from '../../../ui/job-view/redux/configureStore';
|
import { configureStore } from '../../../ui/job-view/redux/configureStore';
|
||||||
import PushList from '../../../ui/job-view/pushes/PushList';
|
import PushList from '../../../ui/job-view/pushes/PushList';
|
||||||
import { getApiUrl } from '../../../ui/helpers/url';
|
import { getApiUrl } from '../../../ui/helpers/url';
|
||||||
import { findJobInstance } from '../../../ui/helpers/job';
|
import { findJobInstance } from '../../../ui/helpers/job';
|
||||||
|
@ -37,6 +33,8 @@ global.document.createRange = () => ({
|
||||||
|
|
||||||
describe('PushList', () => {
|
describe('PushList', () => {
|
||||||
const repoName = 'autoland';
|
const repoName = 'autoland';
|
||||||
|
const history = createBrowserHistory();
|
||||||
|
|
||||||
const currentRepo = {
|
const currentRepo = {
|
||||||
id: 4,
|
id: 4,
|
||||||
repository_group: {
|
repository_group: {
|
||||||
|
@ -58,22 +56,7 @@ describe('PushList', () => {
|
||||||
getRevisionHref: () => 'foo',
|
getRevisionHref: () => 'foo',
|
||||||
getPushLogHref: () => 'foo',
|
getPushLogHref: () => 'foo',
|
||||||
};
|
};
|
||||||
const testPushList = (store, filterModel) => (
|
|
||||||
<Provider store={store}>
|
|
||||||
<div id="th-global-content">
|
|
||||||
<PushList
|
|
||||||
user={{ isLoggedIn: false }}
|
|
||||||
repoName={repoName}
|
|
||||||
currentRepo={currentRepo}
|
|
||||||
filterModel={filterModel}
|
|
||||||
duplicateJobsVisible={false}
|
|
||||||
groupCountsExpanded={false}
|
|
||||||
pushHealthVisibility="None"
|
|
||||||
getAllShownJobs={() => {}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
const pushCount = () =>
|
const pushCount = () =>
|
||||||
waitFor(() => getAllByTestId(document.body, 'push-header'));
|
waitFor(() => getAllByTestId(document.body, 'push-header'));
|
||||||
|
|
||||||
|
@ -92,6 +75,26 @@ describe('PushList', () => {
|
||||||
results: pushListFixture.results.slice(0, 1),
|
results: pushListFixture.results.slice(0, 1),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
fetchMock.get(
|
||||||
|
getProjectUrl(
|
||||||
|
'/push/?full=true&count=10&tochange=ba9c692786e95143b8df3f4b3e9b504dfbc589a0',
|
||||||
|
repoName,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
...pushListFixture,
|
||||||
|
results: pushListFixture.results.slice(0, 1),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
fetchMock.get(
|
||||||
|
getProjectUrl(
|
||||||
|
'/push/?full=true&count=100&fromchange=d5b037941b0ebabcc9b843f24d926e9d65961087',
|
||||||
|
repoName,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
...pushListFixture,
|
||||||
|
results: pushListFixture.results.slice(1, 2),
|
||||||
|
},
|
||||||
|
);
|
||||||
fetchMock.get(
|
fetchMock.get(
|
||||||
getProjectUrl(
|
getProjectUrl(
|
||||||
'/push/?full=true&count=10&tochange=d5b037941b0ebabcc9b843f24d926e9d65961087',
|
'/push/?full=true&count=10&tochange=d5b037941b0ebabcc9b843f24d926e9d65961087',
|
||||||
|
@ -117,73 +120,87 @@ describe('PushList', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => history.push(`/jobs?repo=${repoName}`));
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
fetchMock.reset();
|
fetchMock.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
const testPushList = () => {
|
||||||
cleanup();
|
const store = configureStore(history);
|
||||||
replaceLocation({});
|
return (
|
||||||
});
|
<Provider store={store} context={ReactReduxContext}>
|
||||||
|
<ConnectedRouter history={history} context={ReactReduxContext}>
|
||||||
|
<div id="th-global-content">
|
||||||
|
<PushList
|
||||||
|
user={{ isLoggedIn: false }}
|
||||||
|
repoName={repoName}
|
||||||
|
currentRepo={currentRepo}
|
||||||
|
filterModel={
|
||||||
|
new FilterModel({
|
||||||
|
pushRoute: history.push,
|
||||||
|
router: { location: history.location },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
duplicateJobsVisible={false}
|
||||||
|
groupCountsExpanded={false}
|
||||||
|
pushHealthVisibility="None"
|
||||||
|
getAllShownJobs={() => {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ConnectedRouter>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
// push1Revision is'ba9c692786e95143b8df3f4b3e9b504dfbc589a0';
|
||||||
const push1Id = 'push-511138';
|
const push1Id = 'push-511138';
|
||||||
|
// push2Revision is 'd5b037941b0ebabcc9b843f24d926e9d65961087';
|
||||||
const push2Id = 'push-511137';
|
const push2Id = 'push-511137';
|
||||||
const push1Revision = 'ba9c692786e95143b8df3f4b3e9b504dfbc589a0';
|
|
||||||
const push2Revision = 'd5b037941b0ebabcc9b843f24d926e9d65961087';
|
|
||||||
|
|
||||||
test('should have 2 pushes', async () => {
|
test('should have 2 pushes', async () => {
|
||||||
const { store } = configureStore();
|
render(testPushList());
|
||||||
render(testPushList(store, new FilterModel()));
|
|
||||||
|
|
||||||
expect(await pushCount()).toHaveLength(2);
|
expect(await pushCount()).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should switch to single loaded revision and back to 2', async () => {
|
test('should switch to single loaded revision', async () => {
|
||||||
const { store } = configureStore();
|
const { getAllByTitle } = render(testPushList());
|
||||||
const { getByTestId } = render(testPushList(store, new FilterModel()));
|
|
||||||
|
|
||||||
expect(await pushCount()).toHaveLength(2);
|
expect(await pushCount()).toHaveLength(2);
|
||||||
|
const pushLinks = await getAllByTitle('View only this push');
|
||||||
|
|
||||||
// fireEvent.click(push) not clicking the link, so must set the url param
|
fireEvent.click(pushLinks[1]);
|
||||||
setUrlParam('revision', push2Revision); // click push 2
|
expect(pushLinks[0]).not.toBeInTheDocument();
|
||||||
await waitForElementToBeRemoved(() => getByTestId('push-511138'));
|
|
||||||
|
|
||||||
expect(await pushCount()).toHaveLength(1);
|
expect(await pushCount()).toHaveLength(1);
|
||||||
|
|
||||||
setUrlParam('revision', null);
|
|
||||||
await waitFor(() => getByTestId(push1Id));
|
|
||||||
expect(await pushCount()).toHaveLength(2);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should reload pushes when setting fromchange', async () => {
|
test('should reload pushes when setting fromchange', async () => {
|
||||||
const { store } = configureStore();
|
const { queryAllByTestId, queryByTestId } = render(testPushList());
|
||||||
const { getByTestId } = render(testPushList(store, new FilterModel()));
|
|
||||||
|
|
||||||
expect(await pushCount()).toHaveLength(2);
|
expect(await pushCount()).toHaveLength(2);
|
||||||
|
|
||||||
const push2 = getByTestId(push2Id);
|
await waitFor(() => queryAllByTestId('push-header'));
|
||||||
|
|
||||||
|
const push2 = await waitFor(() => queryByTestId(push2Id));
|
||||||
const actionMenuButton = push2.querySelector(
|
const actionMenuButton = push2.querySelector(
|
||||||
'[data-testid="push-action-menu-button"]',
|
'[data-testid="push-action-menu-button"]',
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.click(actionMenuButton);
|
fireEvent.click(actionMenuButton);
|
||||||
|
|
||||||
const setBottomLink = await waitFor(() =>
|
const setFromRange = await waitFor(() =>
|
||||||
push2.querySelector('[data-testid="bottom-of-range-menu-item"]'),
|
push2.querySelector('[data-testid="bottom-of-range-menu-item"]'),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(setBottomLink.getAttribute('href')).toContain(
|
fireEvent.click(setFromRange);
|
||||||
'/#/jobs?&fromchange=d5b037941b0ebabcc9b843f24d926e9d65961087',
|
|
||||||
);
|
|
||||||
|
|
||||||
setUrlParam('fromchange', push1Revision);
|
expect(history.location.search).toContain(
|
||||||
await waitForElementToBeRemoved(() => getByTestId(push2Id));
|
'?repo=autoland&fromchange=d5b037941b0ebabcc9b843f24d926e9d65961087',
|
||||||
expect(await pushCount()).toHaveLength(1);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should reload pushes when setting tochange', async () => {
|
test('should reload pushes when setting tochange', async () => {
|
||||||
const { store } = configureStore();
|
const { getByTestId } = render(testPushList());
|
||||||
const { getByTestId } = render(testPushList(store, new FilterModel()));
|
|
||||||
|
|
||||||
expect(await pushCount()).toHaveLength(2);
|
expect(await pushCount()).toHaveLength(2);
|
||||||
|
|
||||||
|
@ -194,24 +211,19 @@ describe('PushList', () => {
|
||||||
|
|
||||||
fireEvent.click(actionMenuButton);
|
fireEvent.click(actionMenuButton);
|
||||||
|
|
||||||
const setTopLink = await waitFor(() =>
|
const setTopRange = await waitFor(() =>
|
||||||
push1.querySelector('[data-testid="top-of-range-menu-item"]'),
|
push1.querySelector('[data-testid="top-of-range-menu-item"]'),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(setTopLink.getAttribute('href')).toContain(
|
fireEvent.click(setTopRange);
|
||||||
'/#/jobs?&tochange=ba9c692786e95143b8df3f4b3e9b504dfbc589a0',
|
|
||||||
);
|
|
||||||
|
|
||||||
setUrlParam('tochange', push2Revision);
|
expect(history.location.search).toContain(
|
||||||
await waitForElementToBeRemoved(() => getByTestId(push1Id));
|
'?repo=autoland&tochange=ba9c692786e95143b8df3f4b3e9b504dfbc589a0',
|
||||||
expect(await pushCount()).toHaveLength(1);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should load N more pushes when click next N', async () => {
|
test('should load N more pushes when click next N', async () => {
|
||||||
const { store } = configureStore();
|
const { getByTestId, getAllByTestId } = render(testPushList());
|
||||||
const { getByTestId, getAllByTestId } = render(
|
|
||||||
testPushList(store, new FilterModel()),
|
|
||||||
);
|
|
||||||
const nextNUrl = (count) =>
|
const nextNUrl = (count) =>
|
||||||
getProjectUrl(`/push/?full=true&count=${count + 1}&push_timestamp__lte=`);
|
getProjectUrl(`/push/?full=true&count=${count + 1}&push_timestamp__lte=`);
|
||||||
const clickNext = (count) =>
|
const clickNext = (count) =>
|
||||||
|
@ -260,8 +272,7 @@ describe('PushList', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('jobs should have fields required for retriggers', async () => {
|
test('jobs should have fields required for retriggers', async () => {
|
||||||
const { store } = configureStore();
|
const { getByText } = render(testPushList());
|
||||||
const { getByText } = render(testPushList(store, new FilterModel()));
|
|
||||||
const jobEl = await waitFor(() => getByText('yaml'));
|
const jobEl = await waitFor(() => getByText('yaml'));
|
||||||
const jobInstance = findJobInstance(jobEl.getAttribute('data-job-id'));
|
const jobInstance = findJobInstance(jobEl.getAttribute('data-job-id'));
|
||||||
const { job } = jobInstance.props;
|
const { job } = jobInstance.props;
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { render, cleanup, waitFor, fireEvent } from '@testing-library/react';
|
import { render, waitFor, fireEvent } from '@testing-library/react';
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
import configureMockStore from 'redux-mock-store';
|
import configureMockStore from 'redux-mock-store';
|
||||||
|
import { createBrowserHistory } from 'history';
|
||||||
|
|
||||||
import { replaceLocation, setUrlParam } from '../../../ui/helpers/location';
|
|
||||||
import FilterModel from '../../../ui/models/filter';
|
import FilterModel from '../../../ui/models/filter';
|
||||||
import SecondaryNavBar from '../../../ui/job-view/headerbars/SecondaryNavBar';
|
import SecondaryNavBar from '../../../ui/job-view/headerbars/SecondaryNavBar';
|
||||||
import { initialState } from '../../../ui/job-view/redux/stores/pushes';
|
import { initialState } from '../../../ui/job-view/redux/stores/pushes';
|
||||||
|
@ -13,6 +13,8 @@ import repos from '../mock/repositories';
|
||||||
|
|
||||||
const mockStore = configureMockStore([thunk]);
|
const mockStore = configureMockStore([thunk]);
|
||||||
const repoName = 'autoland';
|
const repoName = 'autoland';
|
||||||
|
const history = createBrowserHistory();
|
||||||
|
const router = { location: history.location };
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fetchMock.get('https://treestatus.mozilla-releng.net/trees/autoland', {
|
fetchMock.get('https://treestatus.mozilla-releng.net/trees/autoland', {
|
||||||
|
@ -23,31 +25,36 @@ beforeEach(() => {
|
||||||
tree: 'autoland',
|
tree: 'autoland',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setUrlParam('repo', repoName);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cleanup();
|
|
||||||
fetchMock.reset();
|
fetchMock.reset();
|
||||||
replaceLocation({});
|
history.push('/');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('SecondaryNavBar', () => {
|
describe('SecondaryNavBar', () => {
|
||||||
const testSecondaryNavBar = (store, filterModel, props) => (
|
const testSecondaryNavBar = (store, props) => {
|
||||||
<Provider store={store}>
|
return (
|
||||||
<SecondaryNavBar
|
<Provider store={store}>
|
||||||
updateButtonClick={() => {}}
|
<SecondaryNavBar
|
||||||
serverChanged={false}
|
updateButtonClick={() => {}}
|
||||||
filterModel={filterModel}
|
serverChanged={false}
|
||||||
repos={repos}
|
filterModel={
|
||||||
setCurrentRepoTreeStatus={() => {}}
|
new FilterModel({
|
||||||
duplicateJobsVisible={false}
|
pushRoute: history.push,
|
||||||
groupCountsExpanded={false}
|
router,
|
||||||
toggleFieldFilterVisible={() => {}}
|
})
|
||||||
{...props}
|
}
|
||||||
/>
|
repos={repos}
|
||||||
</Provider>
|
setCurrentRepoTreeStatus={() => {}}
|
||||||
);
|
duplicateJobsVisible={false}
|
||||||
|
groupCountsExpanded={false}
|
||||||
|
toggleFieldFilterVisible={() => {}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
test('should 52 unclassified', async () => {
|
test('should 52 unclassified', async () => {
|
||||||
const store = mockStore({
|
const store = mockStore({
|
||||||
|
@ -56,8 +63,9 @@ describe('SecondaryNavBar', () => {
|
||||||
allUnclassifiedFailureCount: 52,
|
allUnclassifiedFailureCount: 52,
|
||||||
filteredUnclassifiedFailureCount: 0,
|
filteredUnclassifiedFailureCount: 0,
|
||||||
},
|
},
|
||||||
|
router,
|
||||||
});
|
});
|
||||||
const { getByText } = render(testSecondaryNavBar(store, new FilterModel()));
|
const { getByText } = render(testSecondaryNavBar(store));
|
||||||
|
|
||||||
expect(await waitFor(() => getByText(repoName))).toBeInTheDocument();
|
expect(await waitFor(() => getByText(repoName))).toBeInTheDocument();
|
||||||
expect(await waitFor(() => getByText('52'))).toBeInTheDocument();
|
expect(await waitFor(() => getByText('52'))).toBeInTheDocument();
|
||||||
|
@ -70,8 +78,9 @@ describe('SecondaryNavBar', () => {
|
||||||
allUnclassifiedFailureCount: 22,
|
allUnclassifiedFailureCount: 22,
|
||||||
filteredUnclassifiedFailureCount: 10,
|
filteredUnclassifiedFailureCount: 10,
|
||||||
},
|
},
|
||||||
|
router,
|
||||||
});
|
});
|
||||||
const { getByText } = render(testSecondaryNavBar(store, new FilterModel()));
|
const { getByText } = render(testSecondaryNavBar(store));
|
||||||
|
|
||||||
expect(await waitFor(() => getByText(repoName))).toBeInTheDocument();
|
expect(await waitFor(() => getByText(repoName))).toBeInTheDocument();
|
||||||
expect(await waitFor(() => getByText('22'))).toBeInTheDocument();
|
expect(await waitFor(() => getByText('22'))).toBeInTheDocument();
|
||||||
|
@ -83,6 +92,7 @@ describe('SecondaryNavBar', () => {
|
||||||
pushes: {
|
pushes: {
|
||||||
...initialState,
|
...initialState,
|
||||||
},
|
},
|
||||||
|
router,
|
||||||
});
|
});
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
|
@ -90,9 +100,7 @@ describe('SecondaryNavBar', () => {
|
||||||
updateButtonClick: jest.fn(),
|
updateButtonClick: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { container } = render(
|
const { container } = render(testSecondaryNavBar(store, props));
|
||||||
testSecondaryNavBar(store, new FilterModel(), props),
|
|
||||||
);
|
|
||||||
const el = container.querySelector('#revisionChangedLabel');
|
const el = container.querySelector('#revisionChangedLabel');
|
||||||
fireEvent.click(el);
|
fireEvent.click(el);
|
||||||
expect(props.updateButtonClick).toHaveBeenCalled();
|
expect(props.updateButtonClick).toHaveBeenCalled();
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider, ReactReduxContext } from 'react-redux';
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import { render, cleanup, waitFor, fireEvent } from '@testing-library/react';
|
import { render, cleanup, waitFor, fireEvent } from '@testing-library/react';
|
||||||
|
import { ConnectedRouter } from 'connected-react-router';
|
||||||
|
|
||||||
import JobModel from '../../../../ui/models/job';
|
import JobModel from '../../../../ui/models/job';
|
||||||
import DetailsPanel from '../../../../ui/job-view/details/DetailsPanel';
|
import DetailsPanel from '../../../../ui/job-view/details/DetailsPanel';
|
||||||
|
@ -10,11 +11,11 @@ import pushFixture from '../../mock/push_list.json';
|
||||||
import taskDefinition from '../../mock/task_definition.json';
|
import taskDefinition from '../../mock/task_definition.json';
|
||||||
import { getApiUrl } from '../../../../ui/helpers/url';
|
import { getApiUrl } from '../../../../ui/helpers/url';
|
||||||
import FilterModel from '../../../../ui/models/filter';
|
import FilterModel from '../../../../ui/models/filter';
|
||||||
|
import { getProjectUrl } from '../../../../ui/helpers/location';
|
||||||
import {
|
import {
|
||||||
replaceLocation,
|
history,
|
||||||
getProjectUrl,
|
configureStore,
|
||||||
} from '../../../../ui/helpers/location';
|
} from '../../../../ui/job-view/redux/configureStore';
|
||||||
import configureStore from '../../../../ui/job-view/redux/configureStore';
|
|
||||||
import { setSelectedJob } from '../../../../ui/job-view/redux/stores/selectedJob';
|
import { setSelectedJob } from '../../../../ui/job-view/redux/stores/selectedJob';
|
||||||
import { setPushes } from '../../../../ui/job-view/redux/stores/pushes';
|
import { setPushes } from '../../../../ui/job-view/redux/stores/pushes';
|
||||||
import reposFixture from '../../mock/repositories';
|
import reposFixture from '../../mock/repositories';
|
||||||
|
@ -25,12 +26,12 @@ describe('DetailsPanel', () => {
|
||||||
const repoName = 'autoland';
|
const repoName = 'autoland';
|
||||||
const classificationTypes = [{ id: 1, name: 'intermittent' }];
|
const classificationTypes = [{ id: 1, name: 'intermittent' }];
|
||||||
const classificationMap = { 1: 'intermittent' };
|
const classificationMap = { 1: 'intermittent' };
|
||||||
const filterModel = new FilterModel();
|
|
||||||
let jobList = null;
|
let jobList = null;
|
||||||
let store = null;
|
let store = null;
|
||||||
const currentRepo = reposFixture[2];
|
const currentRepo = reposFixture[2];
|
||||||
currentRepo.getRevisionHref = () => 'foo';
|
currentRepo.getRevisionHref = () => 'foo';
|
||||||
currentRepo.getPushLogHref = () => 'foo';
|
currentRepo.getPushLogHref = () => 'foo';
|
||||||
|
const router = { location: history.location };
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
fetchMock.get(
|
fetchMock.get(
|
||||||
|
@ -69,40 +70,48 @@ describe('DetailsPanel', () => {
|
||||||
'https://firefox-ci-tc.services.mozilla.com/api/queue/v1/task/JFVlnwufR7G9tZu_pKM0dQ',
|
'https://firefox-ci-tc.services.mozilla.com/api/queue/v1/task/JFVlnwufR7G9tZu_pKM0dQ',
|
||||||
taskDefinition,
|
taskDefinition,
|
||||||
);
|
);
|
||||||
store = configureStore().store;
|
store = configureStore();
|
||||||
store.dispatch(setPushes(pushFixture.results, {}));
|
store.dispatch(setPushes(pushFixture.results, {}, router));
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cleanup();
|
cleanup();
|
||||||
fetchMock.reset();
|
fetchMock.reset();
|
||||||
replaceLocation({});
|
history.push('/');
|
||||||
});
|
});
|
||||||
|
|
||||||
const testDetailsPanel = (store) => (
|
const testDetailsPanel = () => (
|
||||||
<div id="global-container" className="height-minus-navbars">
|
<div id="global-container" className="height-minus-navbars">
|
||||||
<Provider store={store}>
|
<Provider store={store} context={ReactReduxContext}>
|
||||||
<KeyboardShortcuts
|
<ConnectedRouter history={history} context={ReactReduxContext}>
|
||||||
filterModel={filterModel}
|
<KeyboardShortcuts
|
||||||
showOnScreenShortcuts={() => {}}
|
filterModel={
|
||||||
>
|
new FilterModel({
|
||||||
<div />
|
pushRoute: history.push,
|
||||||
<div id="th-global-content" data-testid="global-content">
|
router,
|
||||||
<DetailsPanel
|
})
|
||||||
currentRepo={currentRepo}
|
}
|
||||||
user={{ isLoggedIn: false }}
|
showOnScreenShortcuts={() => {}}
|
||||||
resizedHeight={100}
|
>
|
||||||
classificationTypes={classificationTypes}
|
<div />
|
||||||
classificationMap={classificationMap}
|
<div id="th-global-content" data-testid="global-content">
|
||||||
/>
|
<DetailsPanel
|
||||||
</div>
|
currentRepo={currentRepo}
|
||||||
</KeyboardShortcuts>
|
user={{ isLoggedIn: false }}
|
||||||
|
resizedHeight={100}
|
||||||
|
classificationTypes={classificationTypes}
|
||||||
|
classificationMap={classificationMap}
|
||||||
|
router={router}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</KeyboardShortcuts>
|
||||||
|
</ConnectedRouter>
|
||||||
</Provider>
|
</Provider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
test('pin selected job with button', async () => {
|
test('pin selected job with button', async () => {
|
||||||
const { getByTitle } = render(testDetailsPanel(store));
|
const { getByTitle } = render(testDetailsPanel());
|
||||||
store.dispatch(setSelectedJob(jobList.data[1], true));
|
store.dispatch(setSelectedJob(jobList.data[1], true));
|
||||||
|
|
||||||
fireEvent.click(await waitFor(() => getByTitle('Pin job')));
|
fireEvent.click(await waitFor(() => getByTitle('Pin job')));
|
||||||
|
@ -116,7 +125,7 @@ describe('DetailsPanel', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('KeyboardShortcut space: pin selected job', async () => {
|
test('KeyboardShortcut space: pin selected job', async () => {
|
||||||
const { getByTitle } = render(testDetailsPanel(store));
|
const { getByTitle } = render(testDetailsPanel());
|
||||||
store.dispatch(setSelectedJob(jobList.data[1], true));
|
store.dispatch(setSelectedJob(jobList.data[1], true));
|
||||||
|
|
||||||
const content = await waitFor(() =>
|
const content = await waitFor(() =>
|
||||||
|
@ -132,7 +141,7 @@ describe('DetailsPanel', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('KeyboardShortcut b: pin selected task and edit bug', async () => {
|
test('KeyboardShortcut b: pin selected task and edit bug', async () => {
|
||||||
const { getByPlaceholderText } = render(testDetailsPanel(store));
|
const { getByPlaceholderText } = render(testDetailsPanel());
|
||||||
store.dispatch(setSelectedJob(jobList.data[1], true));
|
store.dispatch(setSelectedJob(jobList.data[1], true));
|
||||||
|
|
||||||
const content = await waitFor(() =>
|
const content = await waitFor(() =>
|
||||||
|
@ -151,7 +160,7 @@ describe('DetailsPanel', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('KeyboardShortcut c: pin selected task and edit comment', async () => {
|
test('KeyboardShortcut c: pin selected task and edit comment', async () => {
|
||||||
const { getByPlaceholderText } = render(testDetailsPanel(store));
|
const { getByPlaceholderText } = render(testDetailsPanel());
|
||||||
store.dispatch(setSelectedJob(jobList.data[1], true));
|
store.dispatch(setSelectedJob(jobList.data[1], true));
|
||||||
|
|
||||||
const content = await waitFor(() =>
|
const content = await waitFor(() =>
|
||||||
|
@ -168,7 +177,7 @@ describe('DetailsPanel', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('KeyboardShortcut ctrl+shift+u: clear PinBoard', async () => {
|
test('KeyboardShortcut ctrl+shift+u: clear PinBoard', async () => {
|
||||||
const { getByTitle } = render(testDetailsPanel(store));
|
const { getByTitle } = render(testDetailsPanel());
|
||||||
store.dispatch(setSelectedJob(jobList.data[1], true));
|
store.dispatch(setSelectedJob(jobList.data[1], true));
|
||||||
|
|
||||||
fireEvent.click(await waitFor(() => getByTitle('Pin job')));
|
fireEvent.click(await waitFor(() => getByTitle('Pin job')));
|
||||||
|
@ -188,7 +197,7 @@ describe('DetailsPanel', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('clear PinBoard', async () => {
|
test('clear PinBoard', async () => {
|
||||||
const { getByTitle, getByText } = render(testDetailsPanel(store));
|
const { getByTitle, getByText } = render(testDetailsPanel());
|
||||||
store.dispatch(setSelectedJob(jobList.data[1], true));
|
store.dispatch(setSelectedJob(jobList.data[1], true));
|
||||||
|
|
||||||
fireEvent.click(await waitFor(() => getByTitle('Pin job')));
|
fireEvent.click(await waitFor(() => getByTitle('Pin job')));
|
||||||
|
@ -205,7 +214,7 @@ describe('DetailsPanel', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('pin all jobs', async () => {
|
test('pin all jobs', async () => {
|
||||||
const { queryAllByTitle } = render(testDetailsPanel(store));
|
const { queryAllByTitle } = render(testDetailsPanel());
|
||||||
store.dispatch(pinJobs(jobList.data));
|
store.dispatch(pinJobs(jobList.data));
|
||||||
|
|
||||||
const unPinJobBtns = await waitFor(() => queryAllByTitle('Unpin job'));
|
const unPinJobBtns = await waitFor(() => queryAllByTitle('Unpin job'));
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/* eslint-disable jest/prefer-to-have-length */
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import cloneDeep from 'lodash/cloneDeep';
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
import { mount } from 'enzyme/build';
|
import { render, waitFor, fireEvent } from '@testing-library/react';
|
||||||
|
import { createBrowserHistory } from 'history';
|
||||||
|
|
||||||
import { JobGroupComponent } from '../../../ui/job-view/pushes/JobGroup';
|
import { JobGroupComponent } from '../../../ui/job-view/pushes/JobGroup';
|
||||||
import FilterModel from '../../../ui/models/filter';
|
import FilterModel from '../../../ui/models/filter';
|
||||||
|
@ -9,13 +9,20 @@ import mappedGroupFixture from '../mock/mappedGroup';
|
||||||
import mappedGroupDupsFixture from '../mock/mappedGroupDups';
|
import mappedGroupDupsFixture from '../mock/mappedGroupDups';
|
||||||
import { addAggregateFields } from '../../../ui/helpers/job';
|
import { addAggregateFields } from '../../../ui/helpers/job';
|
||||||
|
|
||||||
|
const history = createBrowserHistory();
|
||||||
|
|
||||||
describe('JobGroup component', () => {
|
describe('JobGroup component', () => {
|
||||||
let countGroup;
|
let countGroup;
|
||||||
let dupGroup;
|
let dupGroup;
|
||||||
const repoName = 'mozilla-inbound';
|
const repoName = 'mozilla-inbound';
|
||||||
const filterModel = new FilterModel();
|
const filterModel = new FilterModel({
|
||||||
|
pushRoute: history.push,
|
||||||
|
router: { location: history.location },
|
||||||
|
});
|
||||||
const pushGroupState = 'collapsed';
|
const pushGroupState = 'collapsed';
|
||||||
|
|
||||||
|
afterEach(() => history.push('/'));
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
mappedGroupFixture.jobs.forEach((job) => addAggregateFields(job));
|
mappedGroupFixture.jobs.forEach((job) => addAggregateFields(job));
|
||||||
mappedGroupDupsFixture.jobs.forEach((job) => addAggregateFields(job));
|
mappedGroupDupsFixture.jobs.forEach((job) => addAggregateFields(job));
|
||||||
|
@ -26,136 +33,105 @@ describe('JobGroup component', () => {
|
||||||
dupGroup = cloneDeep(mappedGroupDupsFixture);
|
dupGroup = cloneDeep(mappedGroupDupsFixture);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const jobGroup = (
|
||||||
|
group,
|
||||||
|
groupCountsExpanded = false,
|
||||||
|
duplicateJobsVisible = false,
|
||||||
|
) => (
|
||||||
|
<JobGroupComponent
|
||||||
|
repoName={repoName}
|
||||||
|
group={group}
|
||||||
|
filterPlatformCb={() => {}}
|
||||||
|
filterModel={filterModel}
|
||||||
|
pushGroupState={pushGroupState}
|
||||||
|
platform={<span>windows</span>}
|
||||||
|
duplicateJobsVisible={duplicateJobsVisible}
|
||||||
|
groupCountsExpanded={groupCountsExpanded}
|
||||||
|
push={history.push}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Tests Jobs view
|
Tests Jobs view
|
||||||
*/
|
*/
|
||||||
it('collapsed should show a job and count of 2', () => {
|
it('collapsed should show a job and count of 2 icon when collapsed', async () => {
|
||||||
const jobGroup = mount(
|
const { getByTestId } = render(jobGroup(countGroup));
|
||||||
<JobGroupComponent
|
|
||||||
repoName={repoName}
|
|
||||||
group={countGroup}
|
|
||||||
filterPlatformCb={() => {}}
|
|
||||||
filterModel={filterModel}
|
|
||||||
pushGroupState={pushGroupState}
|
|
||||||
platform={<span>windows</span>}
|
|
||||||
duplicateJobsVisible={false}
|
|
||||||
groupCountsExpanded={false}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(jobGroup.find('.job-group-count').first().text()).toEqual('2');
|
const jobGroupCount = await waitFor(() => getByTestId('job-group-count'));
|
||||||
|
expect(jobGroupCount).toHaveTextContent('2');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show a job and count of 2 when expanded, then re-collapsed', () => {
|
test('should show a job and count of 2 icon when re-collapsed', async () => {
|
||||||
const jobGroup = mount(
|
const { getByText, getByTestId } = render(jobGroup(countGroup));
|
||||||
<JobGroupComponent
|
|
||||||
repoName={repoName}
|
|
||||||
group={countGroup}
|
|
||||||
filterPlatformCb={() => {}}
|
|
||||||
filterModel={filterModel}
|
|
||||||
pushGroupState={pushGroupState}
|
|
||||||
platform={<span>windows</span>}
|
|
||||||
duplicateJobsVisible={false}
|
|
||||||
groupCountsExpanded={false}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
jobGroup.setState({ expanded: true });
|
|
||||||
jobGroup.setState({ expanded: false });
|
|
||||||
|
|
||||||
expect(jobGroup.find('.job-group-count').first().text()).toEqual('2');
|
const jobGroupCount = await waitFor(() => getByTestId('job-group-count'));
|
||||||
|
expect(jobGroupCount).toHaveTextContent('2');
|
||||||
|
|
||||||
|
fireEvent.click(jobGroupCount);
|
||||||
|
|
||||||
|
expect(jobGroupCount).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
const groupSymbolButton = await waitFor(() => getByText('W-e10s'));
|
||||||
|
|
||||||
|
fireEvent.click(groupSymbolButton);
|
||||||
|
await waitFor(() => getByTestId('job-group-count'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show jobs, not counts when expanded', () => {
|
test('should show jobs, not counts when expanded', async () => {
|
||||||
const jobGroup = mount(
|
const { getByTestId, getAllByTestId } = render(jobGroup(countGroup));
|
||||||
<JobGroupComponent
|
|
||||||
repoName={repoName}
|
|
||||||
group={countGroup}
|
|
||||||
filterPlatformCb={() => {}}
|
|
||||||
filterModel={filterModel}
|
|
||||||
pushGroupState={pushGroupState}
|
|
||||||
platform={<span>windows</span>}
|
|
||||||
duplicateJobsVisible={false}
|
|
||||||
groupCountsExpanded={false}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
jobGroup.setState({ expanded: true });
|
|
||||||
|
|
||||||
expect(jobGroup.find('.job-group-count').length).toEqual(0);
|
const jobGroupCount = await waitFor(() => getByTestId('job-group-count'));
|
||||||
expect(jobGroup.find('.job-btn').length).toEqual(3);
|
expect(jobGroupCount).toHaveTextContent('2');
|
||||||
|
|
||||||
|
fireEvent.click(jobGroupCount);
|
||||||
|
|
||||||
|
expect(jobGroupCount).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
const expandedJobs = await waitFor(() => getAllByTestId('job-btn'));
|
||||||
|
expect(expandedJobs).toHaveLength(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show jobs, not counts when globally expanded', () => {
|
test('should show jobs, not counts when globally expanded', async () => {
|
||||||
const groupCountsExpanded = true;
|
const groupCountsExpanded = true;
|
||||||
const jobGroup = mount(
|
const { queryByTestId, getAllByTestId } = render(
|
||||||
<JobGroupComponent
|
jobGroup(countGroup, groupCountsExpanded),
|
||||||
repoName={repoName}
|
|
||||||
group={countGroup}
|
|
||||||
filterPlatformCb={() => {}}
|
|
||||||
filterModel={filterModel}
|
|
||||||
pushGroupState={pushGroupState}
|
|
||||||
platform={<span>windows</span>}
|
|
||||||
duplicateJobsVisible={false}
|
|
||||||
groupCountsExpanded={groupCountsExpanded}
|
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(jobGroup.find('.job-btn').length).toEqual(3);
|
const expandedJobs = await waitFor(() => getAllByTestId('job-btn'));
|
||||||
expect(jobGroup.find('.job-group-count').length).toEqual(0);
|
expect(expandedJobs).toHaveLength(3);
|
||||||
|
|
||||||
|
const jobGroupCount = await waitFor(() => queryByTestId('job-group-count'));
|
||||||
|
expect(jobGroupCount).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should hide duplicates by default', () => {
|
test('should hide duplicates by default', async () => {
|
||||||
const jobGroup = mount(
|
const { getAllByTestId } = render(jobGroup(dupGroup));
|
||||||
<JobGroupComponent
|
|
||||||
repoName={repoName}
|
const jobGroupCount = await waitFor(() =>
|
||||||
group={dupGroup}
|
getAllByTestId('job-group-count'),
|
||||||
filterPlatformCb={() => {}}
|
|
||||||
filterModel={filterModel}
|
|
||||||
pushGroupState={pushGroupState}
|
|
||||||
platform={<span>windows</span>}
|
|
||||||
duplicateJobsVisible={false}
|
|
||||||
groupCountsExpanded={false}
|
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
|
expect(jobGroupCount).toHaveLength(1);
|
||||||
|
|
||||||
expect(jobGroup.find('.job-group-count').length).toEqual(1);
|
const expandedJobs = await waitFor(() => getAllByTestId('job-btn'));
|
||||||
expect(jobGroup.find('.job-btn').length).toEqual(1);
|
expect(expandedJobs).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show 2 duplicates when set to show duplicates', () => {
|
test('should show 2 duplicates when set to show duplicates', async () => {
|
||||||
|
// determined by the presence of duplicate_jobs=visible query param
|
||||||
|
// parsed in the job-view App
|
||||||
const duplicateJobsVisible = true;
|
const duplicateJobsVisible = true;
|
||||||
const jobGroup = mount(
|
const groupCountsExpanded = false;
|
||||||
<JobGroupComponent
|
|
||||||
repoName={repoName}
|
const { getAllByTestId } = render(
|
||||||
group={dupGroup}
|
jobGroup(dupGroup, groupCountsExpanded, duplicateJobsVisible),
|
||||||
filterPlatformCb={() => {}}
|
|
||||||
filterModel={filterModel}
|
|
||||||
pushGroupState={pushGroupState}
|
|
||||||
platform={<span>windows</span>}
|
|
||||||
duplicateJobsVisible={duplicateJobsVisible}
|
|
||||||
groupCountsExpanded={false}
|
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(jobGroup.find('.job-group-count').length).toEqual(1);
|
const jobGroupCount = await waitFor(() =>
|
||||||
expect(jobGroup.find('.job-btn').length).toEqual(2);
|
getAllByTestId('job-group-count'),
|
||||||
});
|
|
||||||
|
|
||||||
test('should show 2 duplicates when globally set to show duplicates', () => {
|
|
||||||
const duplicateJobsVisible = true;
|
|
||||||
const jobGroup = mount(
|
|
||||||
<JobGroupComponent
|
|
||||||
repoName={repoName}
|
|
||||||
group={dupGroup}
|
|
||||||
filterPlatformCb={() => {}}
|
|
||||||
filterModel={filterModel}
|
|
||||||
pushGroupState={pushGroupState}
|
|
||||||
platform={<span>windows</span>}
|
|
||||||
duplicateJobsVisible={duplicateJobsVisible}
|
|
||||||
groupCountsExpanded={false}
|
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
|
expect(jobGroupCount).toHaveLength(1);
|
||||||
|
|
||||||
expect(jobGroup.find('.job-group-count').length).toEqual(1);
|
const jobs = await waitFor(() => getAllByTestId('job-btn'));
|
||||||
expect(jobGroup.find('.job-btn').length).toEqual(2);
|
expect(jobs).toHaveLength(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider, ReactReduxContext } from 'react-redux';
|
||||||
import { render, cleanup, fireEvent, waitFor } from '@testing-library/react';
|
import { render, cleanup, fireEvent, waitFor } from '@testing-library/react';
|
||||||
|
import { createBrowserHistory } from 'history';
|
||||||
|
import { ConnectedRouter } from 'connected-react-router';
|
||||||
|
|
||||||
import PushJobs from '../../../ui/job-view/pushes/PushJobs';
|
import PushJobs from '../../../ui/job-view/pushes/PushJobs';
|
||||||
import FilterModel from '../../../ui/models/filter';
|
import FilterModel from '../../../ui/models/filter';
|
||||||
import { store } from '../../../ui/job-view/redux/store';
|
import { configureStore } from '../../../ui/job-view/redux/configureStore';
|
||||||
import { getUrlParam, setUrlParam } from '../../../ui/helpers/location';
|
import { getUrlParam, setUrlParam } from '../../../ui/helpers/location';
|
||||||
import platforms from '../mock/platforms';
|
import platforms from '../mock/platforms';
|
||||||
import { addAggregateFields } from '../../../ui/helpers/job';
|
import { addAggregateFields } from '../../../ui/helpers/job';
|
||||||
|
|
||||||
|
const history = createBrowserHistory();
|
||||||
const testPush = {
|
const testPush = {
|
||||||
id: 494796,
|
id: 494796,
|
||||||
revision: '1252c6014d122d48c6782310d5c3f4ae742751cb',
|
revision: '1252c6014d122d48c6782310d5c3f4ae742751cb',
|
||||||
|
@ -42,25 +45,34 @@ afterEach(() => {
|
||||||
setUrlParam('selectedTaskRun', null);
|
setUrlParam('selectedTaskRun', null);
|
||||||
});
|
});
|
||||||
|
|
||||||
const testPushJobs = (filterModel) => (
|
const testPushJobs = (filtermodel = null) => {
|
||||||
<Provider store={store}>
|
const store = configureStore(history);
|
||||||
<PushJobs
|
return (
|
||||||
push={testPush}
|
<Provider store={store} context={ReactReduxContext}>
|
||||||
platforms={platforms}
|
<ConnectedRouter history={history} context={ReactReduxContext}>
|
||||||
repoName="try"
|
<PushJobs
|
||||||
filterModel={filterModel}
|
push={testPush}
|
||||||
pushGroupState=""
|
platforms={platforms}
|
||||||
toggleSelectedRunnableJob={() => {}}
|
repoName="try"
|
||||||
runnableVisible={false}
|
filterModel={
|
||||||
duplicateJobsVisible={false}
|
filtermodel ||
|
||||||
groupCountsExpanded={false}
|
new FilterModel({
|
||||||
/>
|
router: { location: history.location, push: history.push },
|
||||||
,
|
})
|
||||||
</Provider>
|
}
|
||||||
);
|
pushGroupState=""
|
||||||
|
toggleSelectedRunnableJob={() => {}}
|
||||||
|
runnableVisible={false}
|
||||||
|
duplicateJobsVisible={false}
|
||||||
|
groupCountsExpanded={false}
|
||||||
|
/>
|
||||||
|
</ConnectedRouter>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
test('select a job updates url', async () => {
|
test('select a job updates url', async () => {
|
||||||
const { getByText } = render(testPushJobs(new FilterModel()));
|
const { getByText } = render(testPushJobs());
|
||||||
const spell = getByText('spell');
|
const spell = getByText('spell');
|
||||||
|
|
||||||
expect(spell).toBeInTheDocument();
|
expect(spell).toBeInTheDocument();
|
||||||
|
@ -74,9 +86,12 @@ test('select a job updates url', async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('filter change keeps selected job visible', async () => {
|
test('filter change keeps selected job visible', async () => {
|
||||||
const filterModel = new FilterModel();
|
const { getByText, rerender } = render(testPushJobs());
|
||||||
const { getByText, rerender } = render(testPushJobs(filterModel));
|
|
||||||
const spell = await waitFor(() => getByText('spell'));
|
const spell = await waitFor(() => getByText('spell'));
|
||||||
|
const filterModel = new FilterModel({
|
||||||
|
router: { location: history.location },
|
||||||
|
pushRoute: history.push,
|
||||||
|
});
|
||||||
|
|
||||||
expect(spell).toBeInTheDocument();
|
expect(spell).toBeInTheDocument();
|
||||||
|
|
||||||
|
@ -84,7 +99,7 @@ test('filter change keeps selected job visible', async () => {
|
||||||
expect(spell).toHaveClass('selected-job');
|
expect(spell).toHaveClass('selected-job');
|
||||||
|
|
||||||
filterModel.addFilter('searchStr', 'linux');
|
filterModel.addFilter('searchStr', 'linux');
|
||||||
rerender(testPushJobs(new FilterModel()));
|
rerender(testPushJobs(filterModel));
|
||||||
|
|
||||||
const spell2 = getByText('spell');
|
const spell2 = getByText('spell');
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,12 @@ import fetchMock from 'fetch-mock';
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
import { cleanup } from '@testing-library/react';
|
import { cleanup } from '@testing-library/react';
|
||||||
import configureMockStore from 'redux-mock-store';
|
import configureMockStore from 'redux-mock-store';
|
||||||
|
import { createBrowserHistory } from 'history';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getProjectUrl,
|
getProjectUrl,
|
||||||
getQueryString,
|
|
||||||
replaceLocation,
|
|
||||||
setUrlParam,
|
setUrlParam,
|
||||||
|
updatePushParams,
|
||||||
} from '../../../../ui/helpers/location';
|
} from '../../../../ui/helpers/location';
|
||||||
import pushListFixture from '../../mock/push_list';
|
import pushListFixture from '../../mock/push_list';
|
||||||
import pushListFromChangeFixture from '../../mock/pushListFromchange';
|
import pushListFromChangeFixture from '../../mock/pushListFromchange';
|
||||||
|
@ -26,12 +26,12 @@ import {
|
||||||
reducer,
|
reducer,
|
||||||
fetchPushes,
|
fetchPushes,
|
||||||
pollPushes,
|
pollPushes,
|
||||||
fetchNextPushes,
|
|
||||||
updateRange,
|
updateRange,
|
||||||
} from '../../../../ui/job-view/redux/stores/pushes';
|
} from '../../../../ui/job-view/redux/stores/pushes';
|
||||||
import { getApiUrl } from '../../../../ui/helpers/url';
|
import { getApiUrl } from '../../../../ui/helpers/url';
|
||||||
import JobModel from '../../../../ui/models/job';
|
import JobModel from '../../../../ui/models/job';
|
||||||
|
|
||||||
|
const history = createBrowserHistory();
|
||||||
const mockStore = configureMockStore([thunk]);
|
const mockStore = configureMockStore([thunk]);
|
||||||
const emptyBugzillaResponse = {
|
const emptyBugzillaResponse = {
|
||||||
bugs: [],
|
bugs: [],
|
||||||
|
@ -48,7 +48,7 @@ describe('Pushes Redux store', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cleanup();
|
cleanup();
|
||||||
fetchMock.reset();
|
fetchMock.reset();
|
||||||
replaceLocation({});
|
history.push('/');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should get pushes with fetchPushes', async () => {
|
test('should get pushes with fetchPushes', async () => {
|
||||||
|
@ -60,7 +60,10 @@ describe('Pushes Redux store', () => {
|
||||||
`https://bugzilla.mozilla.org/rest/bug?id=1556854%2C1555861%2C1559418%2C1563766%2C1561537%2C1563692`,
|
`https://bugzilla.mozilla.org/rest/bug?id=1556854%2C1555861%2C1559418%2C1563766%2C1561537%2C1563692`,
|
||||||
emptyBugzillaResponse,
|
emptyBugzillaResponse,
|
||||||
);
|
);
|
||||||
const store = mockStore({ pushes: initialState });
|
const store = mockStore({
|
||||||
|
pushes: initialState,
|
||||||
|
router: { location: history.location },
|
||||||
|
});
|
||||||
|
|
||||||
await store.dispatch(fetchPushes());
|
await store.dispatch(fetchPushes());
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
|
@ -94,9 +97,15 @@ describe('Pushes Redux store', () => {
|
||||||
jobListFixtureTwo,
|
jobListFixtureTwo,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
fetchMock.get(
|
||||||
|
`https://bugzilla.mozilla.org/rest/bug?id=1506219`,
|
||||||
|
emptyBugzillaResponse,
|
||||||
|
);
|
||||||
|
|
||||||
const initialPush = pushListFixture.results[0];
|
const initialPush = pushListFixture.results[0];
|
||||||
const store = mockStore({
|
const store = mockStore({
|
||||||
pushes: { ...initialState, pushList: [initialPush] },
|
pushes: { ...initialState, pushList: [initialPush] },
|
||||||
|
router: { location: history.location },
|
||||||
});
|
});
|
||||||
|
|
||||||
await store.dispatch(pollPushes());
|
await store.dispatch(pollPushes());
|
||||||
|
@ -133,7 +142,7 @@ describe('Pushes Redux store', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fetchNextPushes should update revision param on url', async () => {
|
test('fetchPushes should update revision param on url', async () => {
|
||||||
fetchMock.get(
|
fetchMock.get(
|
||||||
getProjectUrl(
|
getProjectUrl(
|
||||||
'/push/?full=true&count=11&push_timestamp__lte=1562867957',
|
'/push/?full=true&count=11&push_timestamp__lte=1562867957',
|
||||||
|
@ -148,24 +157,29 @@ describe('Pushes Redux store', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const push = pushListFixture.results[0];
|
const push = pushListFixture.results[0];
|
||||||
|
|
||||||
|
history.push({ search: `?repo=${repoName}&revision=${push.revision}` });
|
||||||
|
const params = updatePushParams(history.location);
|
||||||
|
history.push({ search: params });
|
||||||
const store = mockStore({
|
const store = mockStore({
|
||||||
pushes: {
|
pushes: {
|
||||||
...initialState,
|
...initialState,
|
||||||
pushList: [push],
|
pushList: [push],
|
||||||
oldestPushTimestamp: push.push_timestamp,
|
oldestPushTimestamp: push.push_timestamp,
|
||||||
},
|
},
|
||||||
|
router: { location: history.location },
|
||||||
});
|
});
|
||||||
setUrlParam('revision', push.revision);
|
await store.dispatch(fetchPushes(10, true));
|
||||||
await store.dispatch(fetchNextPushes(10));
|
|
||||||
|
|
||||||
expect(getQueryString()).toEqual(
|
expect(window.location.search).toEqual(
|
||||||
'tochange=ba9c692786e95143b8df3f4b3e9b504dfbc589a0&fromchange=90da061f588d1315ee4087225d041d7474d9dfd8',
|
`?repo=${repoName}&tochange=ba9c692786e95143b8df3f4b3e9b504dfbc589a0&fromchange=90da061f588d1315ee4087225d041d7474d9dfd8`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should pare down to single revision updateRange', async () => {
|
test('should pare down to single revision updateRange', async () => {
|
||||||
const store = mockStore({
|
const store = mockStore({
|
||||||
pushes: { ...initialState, pushList: pushListFixture.results },
|
pushes: { ...initialState, pushList: pushListFixture.results },
|
||||||
|
router: { location: history.location },
|
||||||
});
|
});
|
||||||
|
|
||||||
await store.dispatch(
|
await store.dispatch(
|
||||||
|
@ -174,7 +188,6 @@ describe('Pushes Redux store', () => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
|
|
||||||
expect(actions).toEqual([
|
expect(actions).toEqual([
|
||||||
{ countPinnedJobs: 0, type: 'CLEAR_JOB' },
|
|
||||||
{
|
{
|
||||||
type: SET_PUSHES,
|
type: SET_PUSHES,
|
||||||
pushResults: {
|
pushResults: {
|
||||||
|
@ -205,6 +218,7 @@ describe('Pushes Redux store', () => {
|
||||||
|
|
||||||
const store = mockStore({
|
const store = mockStore({
|
||||||
pushes: initialState,
|
pushes: initialState,
|
||||||
|
router: { location: history.location },
|
||||||
});
|
});
|
||||||
|
|
||||||
setUrlParam('fromchange', '9692347caff487cdcd889489b8e89a825fe6bbd1');
|
setUrlParam('fromchange', '9692347caff487cdcd889489b8e89a825fe6bbd1');
|
||||||
|
@ -267,7 +281,7 @@ describe('Pushes Redux store', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should get new unclassified counts with recalculateUnclassifiedCounts', async () => {
|
test('should get new unclassified counts with recalculateUnclassifiedCounts', async () => {
|
||||||
setUrlParam('job_type_symbol', 'B');
|
history.push('/?job_type_symbol=B');
|
||||||
const { data: jobList } = await JobModel.getList({ push_id: 1 });
|
const { data: jobList } = await JobModel.getList({ push_id: 1 });
|
||||||
|
|
||||||
const state = reducer(
|
const state = reducer(
|
||||||
|
@ -275,7 +289,10 @@ describe('Pushes Redux store', () => {
|
||||||
{ type: UPDATE_JOB_MAP, jobList },
|
{ type: UPDATE_JOB_MAP, jobList },
|
||||||
);
|
);
|
||||||
|
|
||||||
const reduced = reducer(state, { type: RECALCULATE_UNCLASSIFIED_COUNTS });
|
const reduced = reducer(state, {
|
||||||
|
type: RECALCULATE_UNCLASSIFIED_COUNTS,
|
||||||
|
router: { location: history.location },
|
||||||
|
});
|
||||||
|
|
||||||
expect(Object.keys(reduced.jobMap)).toHaveLength(5);
|
expect(Object.keys(reduced.jobMap)).toHaveLength(5);
|
||||||
expect(reduced.allUnclassifiedFailureCount).toEqual(2);
|
expect(reduced.allUnclassifiedFailureCount).toEqual(2);
|
||||||
|
|
|
@ -1,50 +1,30 @@
|
||||||
import React from 'react';
|
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import { Provider } from 'react-redux';
|
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
import { render, cleanup, waitFor } from '@testing-library/react';
|
import { waitFor } from '@testing-library/react';
|
||||||
import configureMockStore from 'redux-mock-store';
|
import configureMockStore from 'redux-mock-store';
|
||||||
import keyBy from 'lodash/keyBy';
|
import keyBy from 'lodash/keyBy';
|
||||||
|
import { createBrowserHistory } from 'history';
|
||||||
|
|
||||||
import {
|
|
||||||
getUrlParam,
|
|
||||||
replaceLocation,
|
|
||||||
setUrlParam,
|
|
||||||
} from '../../../../ui/helpers/location';
|
|
||||||
import FilterModel from '../../../../ui/models/filter';
|
|
||||||
import {
|
import {
|
||||||
setSelectedJob,
|
setSelectedJob,
|
||||||
setSelectedJobFromQueryString,
|
setSelectedJobFromQueryString,
|
||||||
clearSelectedJob,
|
clearSelectedJob,
|
||||||
initialState,
|
initialState,
|
||||||
reducer,
|
reducer,
|
||||||
|
SELECT_JOB,
|
||||||
} from '../../../../ui/job-view/redux/stores/selectedJob';
|
} from '../../../../ui/job-view/redux/stores/selectedJob';
|
||||||
import JobGroup from '../../../../ui/job-view/pushes/JobGroup';
|
|
||||||
import group from '../../mock/group_with_jobs';
|
import group from '../../mock/group_with_jobs';
|
||||||
import { getApiUrl } from '../../../../ui/helpers/url';
|
import { getApiUrl } from '../../../../ui/helpers/url';
|
||||||
import jobListFixtureOne from '../../mock/job_list/job_1';
|
import jobListFixtureOne from '../../mock/job_list/job_1';
|
||||||
|
|
||||||
const mockStore = configureMockStore([thunk]);
|
|
||||||
const jobMap = keyBy(group.jobs, 'id');
|
const jobMap = keyBy(group.jobs, 'id');
|
||||||
let notifications = [];
|
let notifications = [];
|
||||||
|
const history = createBrowserHistory();
|
||||||
|
|
||||||
describe('SelectedJob Redux store', () => {
|
describe('SelectedJob Redux store', () => {
|
||||||
|
const mockStore = configureMockStore([thunk]);
|
||||||
const repoName = 'autoland';
|
const repoName = 'autoland';
|
||||||
const testJobGroup = (store, group, filterModel) => {
|
const router = { location: history.location };
|
||||||
return (
|
|
||||||
<Provider store={store}>
|
|
||||||
<JobGroup
|
|
||||||
group={group}
|
|
||||||
repoName={repoName}
|
|
||||||
filterModel={filterModel}
|
|
||||||
filterPlatformCb={() => {}}
|
|
||||||
pushGroupState="expanded"
|
|
||||||
duplicateJobsVisible={false}
|
|
||||||
groupCountsExpanded
|
|
||||||
/>
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fetchMock.get(
|
fetchMock.get(
|
||||||
|
@ -55,37 +35,44 @@ describe('SelectedJob Redux store', () => {
|
||||||
getApiUrl('/jobs/?task_id=a824gBVmRQSBuEexnVW_Qg&retry_id=0'),
|
getApiUrl('/jobs/?task_id=a824gBVmRQSBuEexnVW_Qg&retry_id=0'),
|
||||||
{ results: [] },
|
{ results: [] },
|
||||||
);
|
);
|
||||||
setUrlParam('repo', repoName);
|
|
||||||
notifications = [];
|
notifications = [];
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cleanup();
|
|
||||||
fetchMock.reset();
|
fetchMock.reset();
|
||||||
replaceLocation({});
|
history.push('/');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('setSelectedJob should select a job', async () => {
|
test('setSelectedJob should select a job', async () => {
|
||||||
const store = mockStore({ selectedJob: { initialState } });
|
|
||||||
const taskRun = 'UCctvnxZR0--JcxyVGc8VA.0';
|
const taskRun = 'UCctvnxZR0--JcxyVGc8VA.0';
|
||||||
|
const store = mockStore({
|
||||||
|
selectedJob: { initialState },
|
||||||
|
router,
|
||||||
|
});
|
||||||
|
|
||||||
render(testJobGroup(store, group, new FilterModel()));
|
store.dispatch(setSelectedJob(group.jobs[0], true));
|
||||||
|
const actions = store.getActions();
|
||||||
const reduced = reducer(
|
expect(actions).toEqual([
|
||||||
{ selectedJob: { initialState } },
|
{
|
||||||
setSelectedJob(group.jobs[0], true),
|
job: group.jobs[0],
|
||||||
);
|
type: SELECT_JOB,
|
||||||
|
updateDetails: true,
|
||||||
expect(reduced.selectedJob).toEqual(group.jobs[0]);
|
},
|
||||||
expect(getUrlParam('selectedTaskRun')).toEqual(taskRun);
|
{
|
||||||
|
payload: {
|
||||||
|
args: [{ search: `?selectedTaskRun=${taskRun}` }],
|
||||||
|
method: 'push',
|
||||||
|
},
|
||||||
|
type: '@@router/CALL_HISTORY_METHOD',
|
||||||
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('setSelectedJobFromQueryString found', async () => {
|
test('setSelectedJobFromQueryString found', async () => {
|
||||||
const taskRun = 'UCctvnxZR0--JcxyVGc8VA.0';
|
const taskRun = 'UCctvnxZR0--JcxyVGc8VA.0';
|
||||||
const store = mockStore({ selectedJob: { initialState } });
|
|
||||||
setUrlParam('selectedTaskRun', taskRun);
|
|
||||||
|
|
||||||
render(testJobGroup(store, group, new FilterModel()));
|
history.push(`/jobs?repo=${repoName}&selectedTaskRun=${taskRun}`);
|
||||||
|
|
||||||
const reduced = reducer(
|
const reduced = reducer(
|
||||||
{ selectedJob: { initialState } },
|
{ selectedJob: { initialState } },
|
||||||
setSelectedJobFromQueryString(() => {}, jobMap),
|
setSelectedJobFromQueryString(() => {}, jobMap),
|
||||||
|
@ -97,9 +84,9 @@ describe('SelectedJob Redux store', () => {
|
||||||
test('setSelectedJobFromQueryString not in jobMap', async () => {
|
test('setSelectedJobFromQueryString not in jobMap', async () => {
|
||||||
const taskRun = 'VaQoWKTbSdGSwBJn6UZV9g.0';
|
const taskRun = 'VaQoWKTbSdGSwBJn6UZV9g.0';
|
||||||
|
|
||||||
setUrlParam('selectedTaskRun', taskRun);
|
history.push(`/jobs?repo=${repoName}&selectedTaskRun=${taskRun}`);
|
||||||
|
|
||||||
const reduced = await reducer(
|
const reduced = reducer(
|
||||||
{ selectedJob: { initialState } },
|
{ selectedJob: { initialState } },
|
||||||
setSelectedJobFromQueryString((msg) => notifications.push(msg), jobMap),
|
setSelectedJobFromQueryString((msg) => notifications.push(msg), jobMap),
|
||||||
);
|
);
|
||||||
|
@ -115,9 +102,9 @@ describe('SelectedJob Redux store', () => {
|
||||||
test('setSelectedJobFromQueryString not in DB', async () => {
|
test('setSelectedJobFromQueryString not in DB', async () => {
|
||||||
const taskRun = 'a824gBVmRQSBuEexnVW_Qg.0';
|
const taskRun = 'a824gBVmRQSBuEexnVW_Qg.0';
|
||||||
|
|
||||||
setUrlParam('selectedTaskRun', taskRun);
|
history.push(`/jobs?repo=${repoName}&selectedTaskRun=${taskRun}`);
|
||||||
|
|
||||||
const reduced = await reducer(
|
const reduced = reducer(
|
||||||
{ selectedJob: { initialState } },
|
{ selectedJob: { initialState } },
|
||||||
setSelectedJobFromQueryString((msg) => notifications.push(msg), jobMap),
|
setSelectedJobFromQueryString((msg) => notifications.push(msg), jobMap),
|
||||||
);
|
);
|
||||||
|
@ -130,16 +117,30 @@ describe('SelectedJob Redux store', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('clearSelectedJob', () => {
|
test('clearSelectedJob', async () => {
|
||||||
const taskRun = 'UCctvnxZR0--JcxyVGc8VA.0';
|
const store = mockStore({
|
||||||
|
selectedJob: { selectedJob: group.jobs[0] },
|
||||||
|
router,
|
||||||
|
});
|
||||||
|
|
||||||
setUrlParam('selectedTaskRun', taskRun);
|
store.dispatch(clearSelectedJob(0));
|
||||||
|
const actions = store.getActions();
|
||||||
const reduced = reducer(
|
expect(actions).toEqual([
|
||||||
{ selectedJob: { selectedJob: group.jobs[0] } },
|
{
|
||||||
clearSelectedJob(0),
|
countPinnedJobs: 0,
|
||||||
);
|
type: 'CLEAR_JOB',
|
||||||
|
},
|
||||||
expect(reduced.selectedJob).toBeNull();
|
{
|
||||||
|
payload: {
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
search: '?',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
method: 'push',
|
||||||
|
},
|
||||||
|
type: '@@router/CALL_HISTORY_METHOD',
|
||||||
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,19 +1,23 @@
|
||||||
|
import { createBrowserHistory } from 'history';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getFilterUrlParamsWithDefaults,
|
getFilterUrlParamsWithDefaults,
|
||||||
getNonFilterUrlParams,
|
getNonFilterUrlParams,
|
||||||
} from '../../../ui/models/filter';
|
} from '../../../ui/models/filter';
|
||||||
|
|
||||||
|
const history = createBrowserHistory();
|
||||||
|
|
||||||
describe('FilterModel', () => {
|
describe('FilterModel', () => {
|
||||||
const oldHash = window.location.hash;
|
const prevParams = history.location.search;
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
window.location.hash = oldHash;
|
history.location.search = prevParams;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('parsing an old url', () => {
|
describe('parsing an old url', () => {
|
||||||
it('should parse the repo with defaults', () => {
|
it('should parse the repo with defaults', () => {
|
||||||
window.location.hash = '?repo=mozilla-inbound';
|
history.location.search = '?repo=mozilla-inbound';
|
||||||
const urlParams = getFilterUrlParamsWithDefaults();
|
const urlParams = getFilterUrlParamsWithDefaults(history.location);
|
||||||
|
|
||||||
expect(urlParams).toEqual({
|
expect(urlParams).toEqual({
|
||||||
repo: ['mozilla-inbound'],
|
repo: ['mozilla-inbound'],
|
||||||
|
@ -34,12 +38,12 @@ describe('FilterModel', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse resultStatus params', () => {
|
it('should parse resultStatus params', () => {
|
||||||
window.location.hash =
|
history.location.search =
|
||||||
'?repo=mozilla-inbound&filter-resultStatus=testfailed&' +
|
'?repo=mozilla-inbound&filter-resultStatus=testfailed&' +
|
||||||
'filter-resultStatus=busted&filter-resultStatus=exception&' +
|
'filter-resultStatus=busted&filter-resultStatus=exception&' +
|
||||||
'filter-resultStatus=success&filter-resultStatus=retry' +
|
'filter-resultStatus=success&filter-resultStatus=retry' +
|
||||||
'&filter-resultStatus=runnable';
|
'&filter-resultStatus=runnable';
|
||||||
const urlParams = getFilterUrlParamsWithDefaults();
|
const urlParams = getFilterUrlParamsWithDefaults(history.location);
|
||||||
|
|
||||||
expect(urlParams).toEqual({
|
expect(urlParams).toEqual({
|
||||||
repo: ['mozilla-inbound'],
|
repo: ['mozilla-inbound'],
|
||||||
|
@ -57,11 +61,11 @@ describe('FilterModel', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse searchStr params with tier and groupState intact', () => {
|
it('should parse searchStr params with tier and groupState intact', () => {
|
||||||
window.location.hash =
|
history.location.search =
|
||||||
'?repo=mozilla-inbound&filter-searchStr=Linux%20x64%20debug%20build-linux64-base-toolchains%2Fdebug%20(Bb)&filter-tier=1&group_state=expanded';
|
'?repo=mozilla-inbound&filter-searchStr=Linux%20x64%20debug%20build-linux64-base-toolchains%2Fdebug%20(Bb)&filter-tier=1&group_state=expanded';
|
||||||
const urlParams = {
|
const urlParams = {
|
||||||
...getNonFilterUrlParams(),
|
...getNonFilterUrlParams(history.location),
|
||||||
...getFilterUrlParamsWithDefaults(),
|
...getFilterUrlParamsWithDefaults(history.location),
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(urlParams).toEqual({
|
expect(urlParams).toEqual({
|
||||||
|
@ -91,8 +95,9 @@ describe('FilterModel', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse job field filters', () => {
|
it('should parse job field filters', () => {
|
||||||
window.location.hash = '?repo=mozilla-inbound&filter-job_type_name=mochi';
|
history.location.search =
|
||||||
const urlParams = getFilterUrlParamsWithDefaults();
|
'?repo=mozilla-inbound&filter-job_type_name=mochi';
|
||||||
|
const urlParams = getFilterUrlParamsWithDefaults(history.location);
|
||||||
|
|
||||||
expect(urlParams).toEqual({
|
expect(urlParams).toEqual({
|
||||||
repo: ['mozilla-inbound'],
|
repo: ['mozilla-inbound'],
|
||||||
|
@ -116,10 +121,10 @@ describe('FilterModel', () => {
|
||||||
|
|
||||||
describe('parsing a new url', () => {
|
describe('parsing a new url', () => {
|
||||||
it('should parse resultStatus and searchStr', () => {
|
it('should parse resultStatus and searchStr', () => {
|
||||||
window.location.hash =
|
history.location.search =
|
||||||
'?repo=mozilla-inbound&resultStatus=testfailed,busted,exception,success,retry,runnable&' +
|
'?repo=mozilla-inbound&resultStatus=testfailed,busted,exception,success,retry,runnable&' +
|
||||||
'searchStr=linux,x64,debug,build-linux64-base-toolchains%2Fdebug,(bb)';
|
'searchStr=linux,x64,debug,build-linux64-base-toolchains%2Fdebug,(bb)';
|
||||||
const urlParams = getFilterUrlParamsWithDefaults();
|
const urlParams = getFilterUrlParamsWithDefaults(history.location);
|
||||||
|
|
||||||
expect(urlParams).toEqual({
|
expect(urlParams).toEqual({
|
||||||
repo: ['mozilla-inbound'],
|
repo: ['mozilla-inbound'],
|
||||||
|
@ -144,9 +149,9 @@ describe('FilterModel', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should preserve the case in email addresses', () => {
|
it('should preserve the case in email addresses', () => {
|
||||||
window.location.hash =
|
history.location.search =
|
||||||
'?repo=mozilla-inbound&author=VYV03354@nifty.ne.jp';
|
'?repo=mozilla-inbound&author=VYV03354@nifty.ne.jp';
|
||||||
const urlParams = getFilterUrlParamsWithDefaults();
|
const urlParams = getFilterUrlParamsWithDefaults(history.location);
|
||||||
|
|
||||||
expect(urlParams).toEqual({
|
expect(urlParams).toEqual({
|
||||||
repo: ['mozilla-inbound'],
|
repo: ['mozilla-inbound'],
|
||||||
|
|
|
@ -2,14 +2,16 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
render,
|
render,
|
||||||
cleanup,
|
|
||||||
fireEvent,
|
fireEvent,
|
||||||
waitFor,
|
waitFor,
|
||||||
waitForElementToBeRemoved,
|
waitForElementToBeRemoved,
|
||||||
} from '@testing-library/react';
|
} from '@testing-library/react';
|
||||||
import { createMemoryHistory } from 'history';
|
import { createBrowserHistory } from 'history';
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
|
import { ConnectedRouter } from 'connected-react-router';
|
||||||
|
import { Provider, ReactReduxContext } from 'react-redux';
|
||||||
|
|
||||||
|
import { configureStore } from '../../../../ui/job-view/redux/configureStore';
|
||||||
import {
|
import {
|
||||||
backfillRetriggeredTitle,
|
backfillRetriggeredTitle,
|
||||||
unknownFrameworkMessage,
|
unknownFrameworkMessage,
|
||||||
|
@ -26,6 +28,8 @@ import testAlertSummaries from '../../mock/alert_summaries';
|
||||||
import testPerformanceTags from '../../mock/performance_tags';
|
import testPerformanceTags from '../../mock/performance_tags';
|
||||||
import TagsList from '../../../../ui/perfherder/alerts/TagsList';
|
import TagsList from '../../../../ui/perfherder/alerts/TagsList';
|
||||||
|
|
||||||
|
const history = createBrowserHistory();
|
||||||
|
|
||||||
const testUser = {
|
const testUser = {
|
||||||
username: 'mozilla-ldap/test_user@mozilla.com',
|
username: 'mozilla-ldap/test_user@mozilla.com',
|
||||||
isLoggedIn: true,
|
isLoggedIn: true,
|
||||||
|
@ -64,7 +68,7 @@ const testIssueTrackers = [
|
||||||
|
|
||||||
const testActiveTags = ['first-tag', 'second-tag'];
|
const testActiveTags = ['first-tag', 'second-tag'];
|
||||||
|
|
||||||
afterEach(cleanup);
|
afterEach(() => history.push('/alerts'));
|
||||||
|
|
||||||
const mockModifyAlert = {
|
const mockModifyAlert = {
|
||||||
update(alert, params) {
|
update(alert, params) {
|
||||||
|
@ -78,69 +82,74 @@ const mockModifyAlert = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
const alertsView = () => {
|
||||||
const mockUpdateAlertSummary = (alertSummaryId, params) => ({
|
const store = configureStore(history);
|
||||||
failureStatus: null,
|
|
||||||
});
|
return render(
|
||||||
const alertsView = () =>
|
<Provider store={store} context={ReactReduxContext}>
|
||||||
render(
|
<ConnectedRouter history={history} context={ReactReduxContext}>
|
||||||
<AlertsView
|
<AlertsView
|
||||||
user={testUser}
|
user={testUser}
|
||||||
projects={repos}
|
projects={repos}
|
||||||
location={{
|
location={history.location}
|
||||||
pathname: '/alerts',
|
frameworks={frameworks}
|
||||||
search: '',
|
performanceTags={testPerformanceTags}
|
||||||
}}
|
history={history}
|
||||||
history={createMemoryHistory('/alerts')}
|
/>
|
||||||
frameworks={frameworks}
|
</ConnectedRouter>
|
||||||
performanceTags={testPerformanceTags}
|
</Provider>,
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const alertsViewControls = ({
|
const alertsViewControls = ({
|
||||||
isListMode = true,
|
isListMode = true,
|
||||||
user: userMock = null,
|
user: userMock = null,
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
const user = userMock !== null ? userMock : testUser;
|
const user = userMock !== null ? userMock : testUser;
|
||||||
|
const store = configureStore(history);
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
<AlertsViewControls
|
<Provider store={store} context={ReactReduxContext}>
|
||||||
validated={{
|
<ConnectedRouter history={history} context={ReactReduxContext}>
|
||||||
hideDwnToInv: undefined,
|
<AlertsViewControls
|
||||||
hideImprovements: undefined,
|
validated={{
|
||||||
filter: undefined,
|
hideDwnToInv: undefined,
|
||||||
updateParams: () => {},
|
hideImprovements: undefined,
|
||||||
}}
|
filter: undefined,
|
||||||
isListMode={isListMode}
|
updateParams: () => {},
|
||||||
alertSummaries={testAlertSummaries}
|
}}
|
||||||
issueTrackers={testIssueTrackers}
|
isListMode={isListMode}
|
||||||
optionCollectionMap={optionCollectionMap}
|
alertSummaries={testAlertSummaries}
|
||||||
fetchAlertSummaries={() => {}}
|
issueTrackers={testIssueTrackers}
|
||||||
updateViewState={() => {}}
|
optionCollectionMap={optionCollectionMap}
|
||||||
user={user}
|
fetchAlertSummaries={() => {}}
|
||||||
modifyAlert={(alert, params) => mockModifyAlert.update(alert, params)}
|
updateViewState={() => {}}
|
||||||
updateAlertSummary={() =>
|
user={user}
|
||||||
Promise.resolve({ failureStatus: false, data: 'alert summary data' })
|
modifyAlert={(alert, params) => mockModifyAlert.update(alert, params)}
|
||||||
}
|
updateAlertSummary={() =>
|
||||||
projects={repos}
|
Promise.resolve({
|
||||||
location={{
|
failureStatus: false,
|
||||||
pathname: '/alerts',
|
data: 'alert summary data',
|
||||||
search: '',
|
})
|
||||||
}}
|
}
|
||||||
filters={{
|
projects={repos}
|
||||||
filterText: '',
|
location={history.location}
|
||||||
hideImprovements: false,
|
filters={{
|
||||||
hideDownstream: false,
|
filterText: '',
|
||||||
hideAssignedToOthers: false,
|
hideImprovements: false,
|
||||||
framework: { name: 'talos', id: 1 },
|
hideDownstream: false,
|
||||||
status: 'untriaged',
|
hideAssignedToOthers: false,
|
||||||
}}
|
framework: { name: 'talos', id: 1 },
|
||||||
frameworks={[{ id: 1, name: dummyFrameworkName }]}
|
status: 'untriaged',
|
||||||
history={createMemoryHistory('/alerts')}
|
}}
|
||||||
frameworkOptions={[ignoreFrameworkOption, ...frameworks]}
|
frameworks={[{ id: 1, name: dummyFrameworkName }]}
|
||||||
setFiltersState={() => {}}
|
frameworkOptions={[ignoreFrameworkOption, ...frameworks]}
|
||||||
performanceTags={testPerformanceTags}
|
setFiltersState={() => {}}
|
||||||
/>,
|
performanceTags={testPerformanceTags}
|
||||||
|
history={history}
|
||||||
|
/>
|
||||||
|
</ConnectedRouter>
|
||||||
|
</Provider>,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -7,15 +7,20 @@ import {
|
||||||
getAllByTestId,
|
getAllByTestId,
|
||||||
queryAllByTestId,
|
queryAllByTestId,
|
||||||
} from '@testing-library/react';
|
} from '@testing-library/react';
|
||||||
|
import { createBrowserHistory } from 'history';
|
||||||
|
import { ConnectedRouter } from 'connected-react-router';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
import Health from '../../../ui/push-health/Health';
|
import Health from '../../../ui/push-health/Health';
|
||||||
import pushHealth from '../mock/push_health';
|
import pushHealth from '../mock/push_health';
|
||||||
import reposFixture from '../mock/repositories';
|
import reposFixture from '../mock/repositories';
|
||||||
import { getApiUrl } from '../../../ui/helpers/url';
|
import { getApiUrl } from '../../../ui/helpers/url';
|
||||||
import { getProjectUrl } from '../../../ui/helpers/location';
|
import { getProjectUrl } from '../../../ui/helpers/location';
|
||||||
|
import { configureStore } from '../../../ui/job-view/redux/configureStore';
|
||||||
|
|
||||||
const revision = 'cd02b96bdce57d9ae53b632ca4740c871d3ecc32';
|
const revision = 'cd02b96bdce57d9ae53b632ca4740c871d3ecc32';
|
||||||
const repo = 'autoland';
|
const repo = 'autoland';
|
||||||
|
const history = createBrowserHistory();
|
||||||
|
|
||||||
describe('Health', () => {
|
describe('Health', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
@ -114,14 +119,19 @@ describe('Health', () => {
|
||||||
cleanup();
|
cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
const testHealth = () => <Health location={window.location} />;
|
const testHealth = () => {
|
||||||
|
const store = configureStore(history);
|
||||||
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
<ConnectedRouter history={history}>
|
||||||
|
<Health location={history.location} />
|
||||||
|
</ConnectedRouter>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
test('should show some grouped tests', async () => {
|
test('should show some grouped tests', async () => {
|
||||||
window.history.replaceState(
|
history.push(`/push-health?repo=${repo}&revision=${revision}`);
|
||||||
{},
|
|
||||||
'Push Health Test',
|
|
||||||
`${window.location.origin}?repo=${repo}&revision=${revision}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const health = render(testHealth());
|
const health = render(testHealth());
|
||||||
const classificationGroups = await waitFor(() =>
|
const classificationGroups = await waitFor(() =>
|
||||||
|
@ -141,10 +151,8 @@ describe('Health', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should filter groups by test path string', async () => {
|
test('should filter groups by test path string', async () => {
|
||||||
window.history.replaceState(
|
history.push(
|
||||||
{},
|
`/push-health?repo=${repo}&revision=${revision}&searchStr=browser/extensions/`,
|
||||||
'Push Health Test',
|
|
||||||
`${window.location.origin}?repo=${repo}&revision=${revision}&searchStr=browser/extensions/`,
|
|
||||||
);
|
);
|
||||||
const health = render(testHealth());
|
const health = render(testHealth());
|
||||||
const classificationGroups = await waitFor(() =>
|
const classificationGroups = await waitFor(() =>
|
||||||
|
|
|
@ -30,7 +30,7 @@ describe('Job', () => {
|
||||||
const job = await waitFor(() => getByText('R1'));
|
const job = await waitFor(() => getByText('R1'));
|
||||||
|
|
||||||
expect(job.getAttribute('href')).toBe(
|
expect(job.getAttribute('href')).toBe(
|
||||||
'/#/jobs?selectedJob=285852125&repo=try&revision=cd02b96bdce57d9ae53b632ca4740c871d3ecc32',
|
'/jobs?selectedJob=285852125&repo=try&revision=cd02b96bdce57d9ae53b632ca4740c871d3ecc32',
|
||||||
);
|
);
|
||||||
expect(job).toHaveClass('btn-orange-classified');
|
expect(job).toHaveClass('btn-orange-classified');
|
||||||
});
|
});
|
||||||
|
@ -40,7 +40,7 @@ describe('Job', () => {
|
||||||
const job = await waitFor(() => getByText('bc6'));
|
const job = await waitFor(() => getByText('bc6'));
|
||||||
|
|
||||||
expect(job.getAttribute('href')).toBe(
|
expect(job.getAttribute('href')).toBe(
|
||||||
'/#/jobs?selectedJob=285859045&repo=try&revision=cd02b96bdce57d9ae53b632ca4740c871d3ecc32',
|
'/jobs?selectedJob=285859045&repo=try&revision=cd02b96bdce57d9ae53b632ca4740c871d3ecc32',
|
||||||
);
|
);
|
||||||
expect(job).toHaveClass('btn-green');
|
expect(job).toHaveClass('btn-green');
|
||||||
});
|
});
|
||||||
|
@ -50,7 +50,7 @@ describe('Job', () => {
|
||||||
const job = await waitFor(() => getByText('arm64'));
|
const job = await waitFor(() => getByText('arm64'));
|
||||||
|
|
||||||
expect(job.getAttribute('href')).toBe(
|
expect(job.getAttribute('href')).toBe(
|
||||||
'/#/jobs?selectedJob=294399307&repo=try&revision=cd02b96bdce57d9ae53b632ca4740c871d3ecc32',
|
'/jobs?selectedJob=294399307&repo=try&revision=cd02b96bdce57d9ae53b632ca4740c871d3ecc32',
|
||||||
);
|
);
|
||||||
expect(job).toHaveClass('btn-red');
|
expect(job).toHaveClass('btn-red');
|
||||||
expect(getByText('Failed in parent')).toBeInTheDocument();
|
expect(getByText('Failed in parent')).toBeInTheDocument();
|
||||||
|
|
|
@ -126,14 +126,6 @@ MIDDLEWARE = [
|
||||||
if middleware
|
if middleware
|
||||||
]
|
]
|
||||||
|
|
||||||
# Templating
|
|
||||||
TEMPLATES = [
|
|
||||||
{
|
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
|
||||||
'APP_DIRS': True,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
# The database config is defined using environment variables of form:
|
# The database config is defined using environment variables of form:
|
||||||
#
|
#
|
||||||
|
@ -426,6 +418,14 @@ WHITENOISE_INDEX_FILE = True
|
||||||
# Halves the time spent performing Brotli/gzip compression during deploys.
|
# Halves the time spent performing Brotli/gzip compression during deploys.
|
||||||
WHITENOISE_KEEP_ONLY_HASHED_FILES = True
|
WHITENOISE_KEEP_ONLY_HASHED_FILES = True
|
||||||
|
|
||||||
|
# Templating
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'DIRS': [WHITENOISE_ROOT],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
# TREEHERDER
|
# TREEHERDER
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url, re_path
|
||||||
from rest_framework.documentation import include_docs_urls
|
from rest_framework.documentation import include_docs_urls
|
||||||
|
|
||||||
from treeherder.webapp.api import urls as api_urls
|
from treeherder.webapp.api import urls as api_urls
|
||||||
|
from django.views.generic.base import TemplateView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^api/', include(api_urls)),
|
url(r'^api/', include(api_urls)),
|
||||||
url(r'^docs/', include_docs_urls(title='REST API Docs')),
|
url(r'^docs/', include_docs_urls(title='REST API Docs')),
|
||||||
|
re_path(r'', TemplateView.as_view(template_name='index.html')),
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
|
|
@ -0,0 +1,168 @@
|
||||||
|
import React, { Suspense, lazy } from 'react';
|
||||||
|
import { Route, Switch } from 'react-router-dom';
|
||||||
|
import { hot } from 'react-hot-loader/root';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { ConnectedRouter } from 'connected-react-router';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
|
import { configureStore, history } from './job-view/redux/configureStore';
|
||||||
|
import LoadingSpinner from './shared/LoadingSpinner';
|
||||||
|
import LoginCallback from './login-callback/LoginCallback';
|
||||||
|
import TaskclusterCallback from './taskcluster-auth-callback/TaskclusterCallback';
|
||||||
|
import UserGuideApp from './userguide/App';
|
||||||
|
|
||||||
|
const IntermittentFailuresApp = lazy(() =>
|
||||||
|
import('./intermittent-failures/App'),
|
||||||
|
);
|
||||||
|
const PerfherderApp = lazy(() => import('./perfherder/App'));
|
||||||
|
|
||||||
|
const PushHealthApp = lazy(() => import('./push-health/App'));
|
||||||
|
|
||||||
|
const JobsViewApp = lazy(() => import('./job-view/App'));
|
||||||
|
|
||||||
|
const LogviewerApp = lazy(() => import('./logviewer/App'));
|
||||||
|
|
||||||
|
// backwards compatibility for routes like this: treeherder.mozilla.org/perf.html#/alerts?id=26622&hideDwnToInv=0
|
||||||
|
const updateOldUrls = () => {
|
||||||
|
const { pathname, hash, search } = history.location;
|
||||||
|
const updates = {};
|
||||||
|
|
||||||
|
const urlMatch = {
|
||||||
|
'/perf.html': '/perfherder',
|
||||||
|
'/pushhealth.html': '/push-health',
|
||||||
|
'/': '/jobs',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
pathname.endsWith('.html') ||
|
||||||
|
(pathname === '/' && hash.length) ||
|
||||||
|
urlMatch[pathname]
|
||||||
|
) {
|
||||||
|
updates.pathname = urlMatch[pathname] || pathname.replace(/.html|\//g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hash.length) {
|
||||||
|
const index = hash.indexOf('?');
|
||||||
|
updates.search = hash.substring(index);
|
||||||
|
const subRoute = hash.substring(1, index);
|
||||||
|
|
||||||
|
if (index >= 2 && updates.pathname !== subRoute) {
|
||||||
|
updates.pathname += subRoute;
|
||||||
|
}
|
||||||
|
} else if (search.length) {
|
||||||
|
updates.search = search;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(updates).length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
history.push(updates);
|
||||||
|
};
|
||||||
|
|
||||||
|
const faviconPaths = {
|
||||||
|
'/jobs': { title: 'Treeherder Jobs View', favicon: 'ui/img/tree_open.png' },
|
||||||
|
'/logviewer': {
|
||||||
|
title: 'Treeherder Logviewer',
|
||||||
|
favicon: 'ui/img/logviewerIcon.png',
|
||||||
|
},
|
||||||
|
'/perfherder': { title: 'Perfherder', favicon: 'ui/img/line_chart.png' },
|
||||||
|
'/userguide': {
|
||||||
|
title: 'Treeherder User Guide',
|
||||||
|
favicon: 'ui/img/tree_open.png',
|
||||||
|
},
|
||||||
|
'/intermittent-failures': {
|
||||||
|
title: 'Intermittent Failures View',
|
||||||
|
favicon: 'ui/img/tree_open.png',
|
||||||
|
},
|
||||||
|
'/push-health': {
|
||||||
|
title: 'Push Health',
|
||||||
|
favicon: 'ui/img/push-health-ok.png',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const withFavicon = (element, route) => {
|
||||||
|
const { title, favicon } = faviconPaths[route];
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Helmet defaultTitle={title}>
|
||||||
|
<link rel={`${title} icon`} href={favicon} />
|
||||||
|
</Helmet>
|
||||||
|
{element}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
updateOldUrls();
|
||||||
|
return (
|
||||||
|
<Provider store={configureStore()}>
|
||||||
|
<ConnectedRouter history={history}>
|
||||||
|
<Suspense fallback={<LoadingSpinner />}>
|
||||||
|
<Switch>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path="/login"
|
||||||
|
render={(props) => <LoginCallback {...props} />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path="/taskcluster-auth"
|
||||||
|
render={(props) => <TaskclusterCallback {...props} />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/jobs"
|
||||||
|
render={(props) =>
|
||||||
|
withFavicon(<JobsViewApp {...props} />, props.location.pathname)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/logviewer"
|
||||||
|
render={(props) =>
|
||||||
|
withFavicon(
|
||||||
|
<LogviewerApp {...props} />,
|
||||||
|
props.location.pathname,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/userguide"
|
||||||
|
render={(props) =>
|
||||||
|
withFavicon(
|
||||||
|
<UserGuideApp {...props} />,
|
||||||
|
props.location.pathname,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/push-health"
|
||||||
|
render={(props) =>
|
||||||
|
withFavicon(
|
||||||
|
<PushHealthApp {...props} />,
|
||||||
|
props.location.pathname,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/intermittent-failures"
|
||||||
|
render={(props) =>
|
||||||
|
withFavicon(
|
||||||
|
<IntermittentFailuresApp {...props} />,
|
||||||
|
'/intermittent-failures',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/perfherder"
|
||||||
|
render={(props) =>
|
||||||
|
withFavicon(<PerfherderApp {...props} />, '/perfherder')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
</Suspense>
|
||||||
|
</ConnectedRouter>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default hot(App);
|
|
@ -58,7 +58,7 @@ input {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip > .tooltip-inner {
|
.custom-tooltip {
|
||||||
background-color: lightgray;
|
background-color: lightgray;
|
||||||
color: black;
|
color: black;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|
|
@ -21,8 +21,6 @@
|
||||||
|
|
||||||
.push-title-left {
|
.push-title-left {
|
||||||
flex: 0 0 24.2em;
|
flex: 0 0 24.2em;
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import pick from 'lodash/pick';
|
||||||
import isEqual from 'lodash/isEqual';
|
import isEqual from 'lodash/isEqual';
|
||||||
|
|
||||||
import { thFailureResults } from './constants';
|
import { thFailureResults } from './constants';
|
||||||
import { extractSearchString, parseQueryParams } from './url';
|
import { parseQueryParams } from './url';
|
||||||
|
|
||||||
// used with field-filters to determine how to match the value against the
|
// used with field-filters to determine how to match the value against the
|
||||||
// job field.
|
// job field.
|
||||||
|
@ -99,14 +99,8 @@ export const hasUrlFilterChanges = function hasUrlFilterChanges(
|
||||||
oldURL,
|
oldURL,
|
||||||
newURL,
|
newURL,
|
||||||
) {
|
) {
|
||||||
const oldFilters = pick(
|
const oldFilters = pick(parseQueryParams(oldURL), allFilterParams);
|
||||||
parseQueryParams(extractSearchString(oldURL)),
|
const newFilters = pick(parseQueryParams(newURL), allFilterParams);
|
||||||
allFilterParams,
|
|
||||||
);
|
|
||||||
const newFilters = pick(
|
|
||||||
parseQueryParams(extractSearchString(newURL)),
|
|
||||||
allFilterParams,
|
|
||||||
);
|
|
||||||
|
|
||||||
return !isEqual(oldFilters, newFilters);
|
return !isEqual(oldFilters, newFilters);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { thFailureResults, thPlatformMap } from './constants';
|
import { thFailureResults, thPlatformMap } from './constants';
|
||||||
import { getGroupMapKey } from './aggregateId';
|
import { getGroupMapKey } from './aggregateId';
|
||||||
import { getAllUrlParams, getRepo } from './location';
|
import { getAllUrlParams, getRepo } from './location';
|
||||||
import { uiJobsUrlBase } from './url';
|
|
||||||
|
|
||||||
const btnClasses = {
|
const btnClasses = {
|
||||||
busted: 'btn-red',
|
busted: 'btn-red',
|
||||||
|
@ -236,7 +235,7 @@ export const getJobSearchStrHref = function getJobSearchStrHref(jobSearchStr) {
|
||||||
const params = getAllUrlParams();
|
const params = getAllUrlParams();
|
||||||
params.set('searchStr', jobSearchStr.split(' '));
|
params.set('searchStr', jobSearchStr.split(' '));
|
||||||
|
|
||||||
return `${uiJobsUrlBase}?${params.toString()}`;
|
return `?${params.toString()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTaskRunStr = (job) => `${job.task_id}.${job.retry_id}`;
|
export const getTaskRunStr = (job) => `${job.task_id}.${job.retry_id}`;
|
||||||
|
|
|
@ -1,17 +1,10 @@
|
||||||
import { thDefaultRepo } from './constants';
|
import { thDefaultRepo } from './constants';
|
||||||
import {
|
import { createQueryParams, getApiUrl } from './url';
|
||||||
createQueryParams,
|
|
||||||
extractSearchString,
|
|
||||||
getApiUrl,
|
|
||||||
uiJobsUrlBase,
|
|
||||||
} from './url';
|
|
||||||
|
|
||||||
export const getQueryString = function getQueryString() {
|
export const getAllUrlParams = function getAllUrlParams(
|
||||||
return extractSearchString(window.location.href);
|
location = window.location,
|
||||||
};
|
) {
|
||||||
|
return new URLSearchParams(location.search);
|
||||||
export const getAllUrlParams = function getAllUrlParams() {
|
|
||||||
return new URLSearchParams(getQueryString());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getUrlParam = function getUrlParam(name) {
|
export const getUrlParam = function getUrlParam(name) {
|
||||||
|
@ -22,27 +15,12 @@ export const getRepo = function getRepo() {
|
||||||
return getUrlParam('repo') || thDefaultRepo;
|
return getUrlParam('repo') || thDefaultRepo;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setLocation = function setLocation(params, hashPrefix = '/jobs') {
|
// This won't update the react router history object
|
||||||
window.location.hash = `#${hashPrefix}${createQueryParams(params)}`;
|
export const replaceLocation = function replaceLocation(params) {
|
||||||
|
window.history.pushState(null, null, createQueryParams(params));
|
||||||
};
|
};
|
||||||
|
|
||||||
// change the url hash without firing a ``hashchange`` event.
|
export const setUrlParam = function setUrlParam(field, value) {
|
||||||
export const replaceLocation = function replaceLocation(
|
|
||||||
params,
|
|
||||||
hashPrefix = '/jobs',
|
|
||||||
) {
|
|
||||||
window.history.replaceState(
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
`${window.location.pathname}#${hashPrefix}${createQueryParams(params)}`,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setUrlParam = function setUrlParam(
|
|
||||||
field,
|
|
||||||
value,
|
|
||||||
hashPrefix = '/jobs',
|
|
||||||
) {
|
|
||||||
const params = getAllUrlParams();
|
const params = getAllUrlParams();
|
||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
|
@ -50,10 +28,25 @@ export const setUrlParam = function setUrlParam(
|
||||||
} else {
|
} else {
|
||||||
params.delete(field);
|
params.delete(field);
|
||||||
}
|
}
|
||||||
setLocation(params, hashPrefix);
|
|
||||||
|
replaceLocation(params);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getRepoUrl = function getRepoUrl(newRepoName) {
|
export const setUrlParams = function setUrlParams(newParams) {
|
||||||
|
const params = getAllUrlParams();
|
||||||
|
|
||||||
|
for (const [key, value] of newParams) {
|
||||||
|
if (value) {
|
||||||
|
params.set(key, value);
|
||||||
|
} else {
|
||||||
|
params.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return createQueryParams(params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateRepoParams = function updateRepoParams(newRepoName) {
|
||||||
const params = getAllUrlParams();
|
const params = getAllUrlParams();
|
||||||
|
|
||||||
params.delete('selectedJob');
|
params.delete('selectedJob');
|
||||||
|
@ -62,7 +55,7 @@ export const getRepoUrl = function getRepoUrl(newRepoName) {
|
||||||
params.delete('revision');
|
params.delete('revision');
|
||||||
params.delete('author');
|
params.delete('author');
|
||||||
params.set('repo', newRepoName);
|
params.set('repo', newRepoName);
|
||||||
return `${uiJobsUrlBase}?${params.toString()}`;
|
return `?${params.toString()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Take the repoName, if passed in. If not, then try to find it on the
|
// Take the repoName, if passed in. If not, then try to find it on the
|
||||||
|
@ -77,3 +70,23 @@ export const getProjectUrl = function getProjectUrl(uri, repoName) {
|
||||||
export const getProjectJobUrl = function getProjectJobUrl(url, jobId) {
|
export const getProjectJobUrl = function getProjectJobUrl(url, jobId) {
|
||||||
return getProjectUrl(`/jobs/${jobId}${url}`);
|
return getProjectUrl(`/jobs/${jobId}${url}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updatePushParams = (location) => {
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
|
||||||
|
if (params.has('revision')) {
|
||||||
|
// We are viewing a single revision, but the user has asked for more.
|
||||||
|
// So we must replace the ``revision`` param with ``tochange``, which
|
||||||
|
// will make it just the top of the range. We will also then get a new
|
||||||
|
// ``fromchange`` param after the fetch.
|
||||||
|
const revision = params.get('revision');
|
||||||
|
params.delete('revision');
|
||||||
|
params.set('tochange', revision);
|
||||||
|
} else if (params.has('startdate')) {
|
||||||
|
// We are fetching more pushes, so we don't want to limit ourselves by
|
||||||
|
// ``startdate``. And after the fetch, ``startdate`` will be invalid,
|
||||||
|
// and will be replaced on the location bar by ``fromchange``.
|
||||||
|
params.delete('startdate');
|
||||||
|
}
|
||||||
|
return `?${params.toString()}`;
|
||||||
|
};
|
||||||
|
|
|
@ -38,7 +38,7 @@ const taskcluster = (() => {
|
||||||
|
|
||||||
const getAuthCode = (useExistingWindow = false) => {
|
const getAuthCode = (useExistingWindow = false) => {
|
||||||
const nonce = generateNonce();
|
const nonce = generateNonce();
|
||||||
// we're storing these for use in the TaskclusterCallback component (taskcluster-auth.html)
|
// we're storing these for use in the TaskclusterCallback component (taskcluster-auth)
|
||||||
// since that's the only way for it to get access to them
|
// since that's the only way for it to get access to them
|
||||||
localStorage.setItem('requestState', nonce);
|
localStorage.setItem('requestState', nonce);
|
||||||
localStorage.setItem('tcRootUrl', _rootUrl);
|
localStorage.setItem('tcRootUrl', _rootUrl);
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
// https://github.com/mozilla/treeherder/blob/master/treeherder/middleware.py
|
// https://github.com/mozilla/treeherder/blob/master/treeherder/middleware.py
|
||||||
import tcLibUrls from 'taskcluster-lib-urls';
|
import tcLibUrls from 'taskcluster-lib-urls';
|
||||||
|
|
||||||
export const uiJobsUrlBase = '/#/jobs';
|
export const uiJobsUrlBase = '/jobs';
|
||||||
|
|
||||||
export const uiPushHealthBase = '/pushhealth.html';
|
export const uiPushHealthBase = '/push-health';
|
||||||
|
|
||||||
export const bzBaseUrl = 'https://bugzilla.mozilla.org/';
|
export const bzBaseUrl = 'https://bugzilla.mozilla.org/';
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ export const graphsEndpoint = '/failurecount/';
|
||||||
|
|
||||||
export const deployedRevisionUrl = '/revision.txt';
|
export const deployedRevisionUrl = '/revision.txt';
|
||||||
|
|
||||||
export const loginCallbackUrl = '/login.html';
|
export const loginCallbackUrl = '/login';
|
||||||
|
|
||||||
export const platformsEndpoint = '/machineplatforms/';
|
export const platformsEndpoint = '/machineplatforms/';
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ export const investigatedTestsEndPoint = '/investigated-tests/';
|
||||||
|
|
||||||
export const repoEndpoint = '/repository/';
|
export const repoEndpoint = '/repository/';
|
||||||
|
|
||||||
export const tcAuthCallbackUrl = '/taskcluster-auth.html';
|
export const tcAuthCallbackUrl = '/taskcluster-auth';
|
||||||
|
|
||||||
export const textLogErrorsEndpoint = '/text_log_errors/';
|
export const textLogErrorsEndpoint = '/text_log_errors/';
|
||||||
|
|
||||||
|
@ -105,7 +105,7 @@ export const getLogViewerUrl = function getLogViewerUrl(
|
||||||
repoName,
|
repoName,
|
||||||
lineNumber,
|
lineNumber,
|
||||||
) {
|
) {
|
||||||
const rv = `logviewer.html#?job_id=${jobId}&repo=${repoName}`;
|
const rv = `/logviewer?job_id=${jobId}&repo=${repoName}`;
|
||||||
return lineNumber ? `${rv}&lineNumber=${lineNumber}` : rv;
|
return lineNumber ? `${rv}&lineNumber=${lineNumber}` : rv;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -124,7 +124,7 @@ export const getPushHealthUrl = function getPushHealthUrl(params) {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCompareChooserUrl = function getCompareChooserUrl(params) {
|
export const getCompareChooserUrl = function getCompareChooserUrl(params) {
|
||||||
return `perf.html#/comparechooser${createQueryParams(params)}`;
|
return `/perfherder/comparechooser${createQueryParams(params)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parseQueryParams = function parseQueryParams(search) {
|
export const parseQueryParams = function parseQueryParams(search) {
|
||||||
|
@ -136,12 +136,6 @@ export const parseQueryParams = function parseQueryParams(search) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const extractSearchString = function getQueryString(url) {
|
|
||||||
const parts = url.split('?');
|
|
||||||
|
|
||||||
return parts[parts.length - 1];
|
|
||||||
};
|
|
||||||
|
|
||||||
// `api` requires a preceding forward slash
|
// `api` requires a preceding forward slash
|
||||||
export const createApiUrl = function createApiUrl(api, params) {
|
export const createApiUrl = function createApiUrl(api, params) {
|
||||||
const apiUrl = getApiUrl(api);
|
const apiUrl = getApiUrl(api);
|
||||||
|
@ -163,7 +157,5 @@ export const updateQueryParams = function updateHistoryWithQueryParams(
|
||||||
history,
|
history,
|
||||||
location,
|
location,
|
||||||
) {
|
) {
|
||||||
history.replace({ pathname: location.pathname, search: queryParams });
|
history.push({ pathname: location.pathname, search: queryParams });
|
||||||
// we do this so the api's won't be called twice (location/history updates will trigger a lifecycle hook)
|
|
||||||
location.search = queryParams;
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { render } from 'react-dom';
|
||||||
|
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
import './css/treeherder-custom-styles.css';
|
||||||
|
import './css/treeherder-navbar.css';
|
||||||
|
import './css/treeherder.css';
|
||||||
|
|
||||||
|
render(<App />, document.getElementById('root'));
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { HashRouter, Route, Switch, Redirect } from 'react-router-dom';
|
import { Route, Switch, Redirect } from 'react-router-dom';
|
||||||
import { Container } from 'reactstrap';
|
import { Container } from 'reactstrap';
|
||||||
import { hot } from 'react-hot-loader/root';
|
import { hot } from 'react-hot-loader/root';
|
||||||
|
|
||||||
|
@ -8,7 +8,11 @@ import ErrorMessages from '../shared/ErrorMessages';
|
||||||
import MainView from './MainView';
|
import MainView from './MainView';
|
||||||
import BugDetailsView from './BugDetailsView';
|
import BugDetailsView from './BugDetailsView';
|
||||||
|
|
||||||
class App extends React.Component {
|
import 'react-table/react-table.css';
|
||||||
|
import '../css/intermittent-failures.css';
|
||||||
|
import '../css/treeherder-base.css';
|
||||||
|
|
||||||
|
class IntermittentFailuresApp extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
@ -28,54 +32,56 @@ class App extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { user, graphData, tableData, errorMessages } = this.state;
|
const { user, graphData, tableData, errorMessages } = this.state;
|
||||||
|
const { path } = this.props.match;
|
||||||
return (
|
return (
|
||||||
<HashRouter>
|
<main>
|
||||||
<main>
|
{errorMessages.length > 0 && (
|
||||||
{errorMessages.length > 0 && (
|
<Container className="pt-5 max-width-default">
|
||||||
<Container className="pt-5 max-width-default">
|
<ErrorMessages errorMessages={errorMessages} />
|
||||||
<ErrorMessages errorMessages={errorMessages} />
|
</Container>
|
||||||
</Container>
|
)}
|
||||||
)}
|
<Switch>
|
||||||
<Switch>
|
<Route
|
||||||
<Route
|
exact
|
||||||
exact
|
path={`${path}/main`}
|
||||||
path="/main"
|
render={(props) => (
|
||||||
render={(props) => (
|
<MainView
|
||||||
<MainView
|
{...props}
|
||||||
{...props}
|
mainGraphData={graphData}
|
||||||
mainGraphData={graphData}
|
mainTableData={tableData}
|
||||||
mainTableData={tableData}
|
updateAppState={this.updateAppState}
|
||||||
updateAppState={this.updateAppState}
|
user={user}
|
||||||
user={user}
|
setUser={(user) => this.setState({ user })}
|
||||||
setUser={(user) => this.setState({ user })}
|
notify={(message) =>
|
||||||
notify={(message) =>
|
this.setState({ errorMessages: [message] })
|
||||||
this.setState({ errorMessages: [message] })
|
}
|
||||||
}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path={`${path}/main?startday=:startday&endday=:endday&tree=:tree`}
|
||||||
path="/main?startday=:startday&endday=:endday&tree=:tree"
|
render={(props) => (
|
||||||
render={(props) => (
|
<MainView
|
||||||
<MainView
|
{...props}
|
||||||
{...props}
|
mainGraphData={graphData}
|
||||||
mainGraphData={graphData}
|
mainTableData={tableData}
|
||||||
mainTableData={tableData}
|
updateAppState={this.updateAppState}
|
||||||
updateAppState={this.updateAppState}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route path="/bugdetails" component={BugDetailsView} />
|
path={`${path}/bugdetails`}
|
||||||
<Route
|
render={(props) => <BugDetailsView {...props} />}
|
||||||
path="/bugdetails?startday=:startday&endday=:endday&tree=:tree&bug=bug"
|
/>
|
||||||
component={BugDetailsView}
|
<Route
|
||||||
/>
|
path={`${path}/bugdetails?startday=:startday&endday=:endday&tree=:tree&bug=bug`}
|
||||||
<Redirect from="/" to="/main" />
|
render={(props) => <BugDetailsView {...props} />}
|
||||||
</Switch>
|
/>
|
||||||
</main>
|
<Redirect from={`${path}/`} to={`${path}/main`} />
|
||||||
</HashRouter>
|
</Switch>
|
||||||
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default hot(App);
|
export default hot(IntermittentFailuresApp);
|
||||||
|
|
|
@ -34,7 +34,7 @@ function BugColumn({
|
||||||
className="ml-1 small-text bug-details"
|
className="ml-1 small-text bug-details"
|
||||||
onClick={() => updateAppState({ graphData, tableData })}
|
onClick={() => updateAppState({ graphData, tableData })}
|
||||||
to={{
|
to={{
|
||||||
pathname: '/bugdetails',
|
pathname: '/intermittent-failures/bugdetails',
|
||||||
search: `?startday=${startday}&endday=${endday}&tree=${tree}&bug=${id}`,
|
search: `?startday=${startday}&endday=${endday}&tree=${tree}&bug=${id}`,
|
||||||
state: { startday, endday, tree, id, summary, location },
|
state: { startday, endday, tree, id, summary, location },
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -124,6 +124,7 @@ const BugDetailsView = (props) => {
|
||||||
</ul>
|
</ul>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
innerClassName="custom-tooltip"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -152,14 +153,14 @@ const BugDetailsView = (props) => {
|
||||||
<Col xs="12" className="text-left">
|
<Col xs="12" className="text-left">
|
||||||
<Breadcrumb listClassName="bg-white">
|
<Breadcrumb listClassName="bg-white">
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<a title="Treeherder home page" href="/#/">
|
<a title="Treeherder home page" href="/">
|
||||||
Treeherder
|
Treeherder
|
||||||
</a>
|
</a>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<Link
|
<Link
|
||||||
title="Intermittent Failures View main page"
|
title="Intermittent Failures View main page"
|
||||||
to={lastLocation || '/'}
|
to={lastLocation || '/intermittent-failures/'}
|
||||||
>
|
>
|
||||||
Main view
|
Main view
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -127,7 +127,7 @@ const MainView = (props) => {
|
||||||
<Col xs="12" className="text-left">
|
<Col xs="12" className="text-left">
|
||||||
<Breadcrumb listClassName="bg-white">
|
<Breadcrumb listClassName="bg-white">
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<a title="Treeherder home page" href="/#/">
|
<a title="Treeherder home page" href="/">
|
||||||
Treeherder
|
Treeherder
|
||||||
</a>
|
</a>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { render } from 'react-dom';
|
|
||||||
import 'react-table/react-table.css';
|
|
||||||
|
|
||||||
import '../css/treeherder-base.css';
|
|
||||||
import '../css/treeherder-custom-styles.css';
|
|
||||||
import '../css/treeherder-navbar.css';
|
|
||||||
import '../css/intermittent-failures.css';
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
render(<App />, document.getElementById('root'));
|
|
|
@ -4,14 +4,20 @@ import { hot } from 'react-hot-loader/root';
|
||||||
import SplitPane from 'react-split-pane';
|
import SplitPane from 'react-split-pane';
|
||||||
import pick from 'lodash/pick';
|
import pick from 'lodash/pick';
|
||||||
import isEqual from 'lodash/isEqual';
|
import isEqual from 'lodash/isEqual';
|
||||||
import { Provider } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { push as pushRoute } from 'connected-react-router';
|
||||||
|
|
||||||
import { thFavicons, thEvents } from '../helpers/constants';
|
import { thFavicons, thDefaultRepo, thEvents } from '../helpers/constants';
|
||||||
import ShortcutTable from '../shared/ShortcutTable';
|
import ShortcutTable from '../shared/ShortcutTable';
|
||||||
import { hasUrlFilterChanges, matchesDefaults } from '../helpers/filter';
|
import { matchesDefaults } from '../helpers/filter';
|
||||||
import { getAllUrlParams, getRepo } from '../helpers/location';
|
import { getAllUrlParams } from '../helpers/location';
|
||||||
import { MAX_TRANSIENT_AGE } from '../helpers/notifications';
|
import { MAX_TRANSIENT_AGE } from '../helpers/notifications';
|
||||||
import { deployedRevisionUrl } from '../helpers/url';
|
import {
|
||||||
|
deployedRevisionUrl,
|
||||||
|
parseQueryParams,
|
||||||
|
createQueryParams,
|
||||||
|
} from '../helpers/url';
|
||||||
import ClassificationTypeModel from '../models/classificationType';
|
import ClassificationTypeModel from '../models/classificationType';
|
||||||
import FilterModel from '../models/filter';
|
import FilterModel from '../models/filter';
|
||||||
import RepositoryModel from '../models/repository';
|
import RepositoryModel from '../models/repository';
|
||||||
|
@ -24,8 +30,19 @@ import { PUSH_HEALTH_VISIBILITY } from './headerbars/HealthMenu';
|
||||||
import DetailsPanel from './details/DetailsPanel';
|
import DetailsPanel from './details/DetailsPanel';
|
||||||
import PushList from './pushes/PushList';
|
import PushList from './pushes/PushList';
|
||||||
import KeyboardShortcuts from './KeyboardShortcuts';
|
import KeyboardShortcuts from './KeyboardShortcuts';
|
||||||
import { store } from './redux/store';
|
import { clearExpiredNotifications } from './redux/stores/notifications';
|
||||||
import { CLEAR_EXPIRED_TRANSIENTS } from './redux/stores/notifications';
|
|
||||||
|
import '../css/treeherder-base.css';
|
||||||
|
import '../css/treeherder-navbar-panels.css';
|
||||||
|
import '../css/treeherder-notifications.css';
|
||||||
|
import '../css/treeherder-details-panel.css';
|
||||||
|
import '../css/failure-summary.css';
|
||||||
|
import '../css/treeherder-job-buttons.css';
|
||||||
|
import '../css/treeherder-pushes.css';
|
||||||
|
import '../css/treeherder-pinboard.css';
|
||||||
|
import '../css/treeherder-bugfiler.css';
|
||||||
|
import '../css/treeherder-fuzzyfinder.css';
|
||||||
|
import '../css/treeherder-loading-overlay.css';
|
||||||
|
|
||||||
const DEFAULT_DETAILS_PCT = 40;
|
const DEFAULT_DETAILS_PCT = 40;
|
||||||
const REVISION_POLL_INTERVAL = 1000 * 60 * 5;
|
const REVISION_POLL_INTERVAL = 1000 * 60 * 5;
|
||||||
|
@ -51,15 +68,13 @@ class App extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const filterModel = new FilterModel();
|
const filterModel = new FilterModel(this.props);
|
||||||
// Set the URL to updated parameter styles, if needed. Otherwise it's a no-op.
|
|
||||||
filterModel.push();
|
|
||||||
const urlParams = getAllUrlParams();
|
const urlParams = getAllUrlParams();
|
||||||
const hasSelectedJob =
|
const hasSelectedJob =
|
||||||
urlParams.has('selectedJob') || urlParams.has('selectedTaskRun');
|
urlParams.has('selectedJob') || urlParams.has('selectedTaskRun');
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
repoName: getRepo(),
|
repoName: this.getOrSetRepo(),
|
||||||
revision: urlParams.get('revision'),
|
revision: urlParams.get('revision'),
|
||||||
user: { isLoggedIn: false, isStaff: false },
|
user: { isLoggedIn: false, isStaff: false },
|
||||||
filterModel,
|
filterModel,
|
||||||
|
@ -82,7 +97,6 @@ class App extends React.Component {
|
||||||
static getDerivedStateFromProps(props, state) {
|
static getDerivedStateFromProps(props, state) {
|
||||||
return {
|
return {
|
||||||
...App.getSplitterDimensions(state.hasSelectedJob),
|
...App.getSplitterDimensions(state.hasSelectedJob),
|
||||||
repoName: getRepo(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,7 +117,6 @@ class App extends React.Component {
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('resize', this.updateDimensions, false);
|
window.addEventListener('resize', this.updateDimensions, false);
|
||||||
window.addEventListener('hashchange', this.handleUrlChanges, false);
|
|
||||||
window.addEventListener('storage', this.handleStorageEvent);
|
window.addEventListener('storage', this.handleStorageEvent);
|
||||||
window.addEventListener(thEvents.filtersUpdated, this.handleFiltersUpdated);
|
window.addEventListener(thEvents.filtersUpdated, this.handleFiltersUpdated);
|
||||||
|
|
||||||
|
@ -145,14 +158,25 @@ class App extends React.Component {
|
||||||
|
|
||||||
// clear expired notifications
|
// clear expired notifications
|
||||||
this.notificationInterval = setInterval(() => {
|
this.notificationInterval = setInterval(() => {
|
||||||
store.dispatch({ type: CLEAR_EXPIRED_TRANSIENTS });
|
this.props.clearExpiredNotifications();
|
||||||
}, MAX_TRANSIENT_AGE);
|
}, MAX_TRANSIENT_AGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (
|
||||||
|
prevProps.router.location.search !== this.props.router.location.search
|
||||||
|
) {
|
||||||
|
this.handleUrlChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
window.removeEventListener('resize', this.updateDimensions, false);
|
window.removeEventListener('resize', this.updateDimensions, false);
|
||||||
window.removeEventListener('hashchange', this.handleUrlChanges, false);
|
window.removeEventListener('storage', this.handleStorageEvent);
|
||||||
window.removeEventListener('storage', this.handleUrlChanges, false);
|
window.removeEventListener(
|
||||||
|
thEvents.filtersUpdated,
|
||||||
|
this.handleFiltersUpdated,
|
||||||
|
);
|
||||||
|
|
||||||
if (this.updateInterval) {
|
if (this.updateInterval) {
|
||||||
clearInterval(this.updateInterval);
|
clearInterval(this.updateInterval);
|
||||||
|
@ -174,6 +198,33 @@ class App extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOrSetRepo() {
|
||||||
|
const { pushRoute } = this.props;
|
||||||
|
const params = getAllUrlParams();
|
||||||
|
let repo = params.get('repo');
|
||||||
|
|
||||||
|
if (!repo) {
|
||||||
|
repo = thDefaultRepo;
|
||||||
|
params.set('repo', repo);
|
||||||
|
pushRoute({
|
||||||
|
search: createQueryParams(params),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return repo;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFiltersUpdated = () => {
|
||||||
|
// we're only using window.location here because of how we're setting param changes for fetchNextPushes
|
||||||
|
// in PushList and addPushes.
|
||||||
|
this.setState({
|
||||||
|
filterModel: new FilterModel({
|
||||||
|
router: window,
|
||||||
|
pushRoute: this.props.pushRoute,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
handleStorageEvent = (e) => {
|
handleStorageEvent = (e) => {
|
||||||
if (e.key === PUSH_HEALTH_VISIBILITY) {
|
if (e.key === PUSH_HEALTH_VISIBILITY) {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -200,9 +251,7 @@ class App extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
getAllShownJobs = (pushId) => {
|
getAllShownJobs = (pushId) => {
|
||||||
const {
|
const { jobMap } = this.props;
|
||||||
pushes: { jobMap },
|
|
||||||
} = store.getState();
|
|
||||||
const jobList = Object.values(jobMap);
|
const jobList = Object.values(jobMap);
|
||||||
|
|
||||||
return pushId
|
return pushId
|
||||||
|
@ -222,32 +271,32 @@ class App extends React.Component {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleUrlChanges = (ev) => {
|
handleUrlChanges = () => {
|
||||||
const { repos } = this.state;
|
const { repos } = this.state;
|
||||||
const { newURL, oldURL } = ev;
|
const { router } = this.props;
|
||||||
const urlParams = getAllUrlParams();
|
|
||||||
const newRepo = urlParams.get('repo');
|
const {
|
||||||
// We only want to set state if any of these or the filter values have changed
|
selectedJob,
|
||||||
|
selectedTaskRun,
|
||||||
|
group_state: groupState,
|
||||||
|
duplicate_jobs: duplicateJobs,
|
||||||
|
repo: newRepo,
|
||||||
|
} = parseQueryParams(router.location.search);
|
||||||
|
|
||||||
const newState = {
|
const newState = {
|
||||||
hasSelectedJob:
|
hasSelectedJob: selectedJob || selectedTaskRun,
|
||||||
urlParams.has('selectedJob') || urlParams.has('selectedTaskRun'),
|
groupCountsExpanded: groupState === 'expanded',
|
||||||
groupCountsExpanded: urlParams.get('group_state') === 'expanded',
|
duplicateJobsVisible: duplicateJobs === 'visible',
|
||||||
duplicateJobsVisible: urlParams.get('duplicate_jobs') === 'visible',
|
|
||||||
currentRepo: repos.find((repo) => repo.name === newRepo),
|
currentRepo: repos.find((repo) => repo.name === newRepo),
|
||||||
};
|
};
|
||||||
|
|
||||||
const oldState = pick(this.state, Object.keys(newState));
|
const oldState = pick(this.state, Object.keys(newState));
|
||||||
|
let stateChanges = { filterModel: new FilterModel(this.props) };
|
||||||
|
|
||||||
// Only re-create the FilterModel if url params that affect it have changed.
|
|
||||||
if (hasUrlFilterChanges(oldURL, newURL)) {
|
|
||||||
this.setState({ filterModel: new FilterModel() });
|
|
||||||
}
|
|
||||||
if (!isEqual(newState, oldState)) {
|
if (!isEqual(newState, oldState)) {
|
||||||
this.setState(newState);
|
stateChanges = { ...stateChanges, ...newState };
|
||||||
}
|
}
|
||||||
};
|
this.setState(stateChanges);
|
||||||
|
|
||||||
handleFiltersUpdated = () => {
|
|
||||||
this.setState({ filterModel: new FilterModel() });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// If ``show`` is a boolean, then set to that value. If it's not, then toggle
|
// If ``show`` is a boolean, then set to that value. If it's not, then toggle
|
||||||
|
@ -318,87 +367,101 @@ class App extends React.Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="global-container" className="height-minus-navbars">
|
<div id="global-container" className="height-minus-navbars">
|
||||||
<Provider store={store}>
|
<KeyboardShortcuts
|
||||||
<KeyboardShortcuts
|
filterModel={filterModel}
|
||||||
|
showOnScreenShortcuts={this.showOnScreenShortcuts}
|
||||||
|
>
|
||||||
|
<PrimaryNavBar
|
||||||
|
repos={repos}
|
||||||
|
updateButtonClick={this.updateButtonClick}
|
||||||
|
serverChanged={serverChanged}
|
||||||
filterModel={filterModel}
|
filterModel={filterModel}
|
||||||
showOnScreenShortcuts={this.showOnScreenShortcuts}
|
setUser={this.setUser}
|
||||||
|
user={user}
|
||||||
|
setCurrentRepoTreeStatus={this.setCurrentRepoTreeStatus}
|
||||||
|
getAllShownJobs={this.getAllShownJobs}
|
||||||
|
duplicateJobsVisible={duplicateJobsVisible}
|
||||||
|
groupCountsExpanded={groupCountsExpanded}
|
||||||
|
toggleFieldFilterVisible={this.toggleFieldFilterVisible}
|
||||||
|
pushHealthVisibility={pushHealthVisibility}
|
||||||
|
setPushHealthVisibility={this.setPushHealthVisibility}
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
<SplitPane
|
||||||
|
split="horizontal"
|
||||||
|
size={`${pushListPct}%`}
|
||||||
|
onChange={(size) => this.handleSplitChange(size)}
|
||||||
>
|
>
|
||||||
<PrimaryNavBar
|
<div className="d-flex flex-column w-100">
|
||||||
repos={repos}
|
{(isFieldFilterVisible || !!filterBarFilters.length) && (
|
||||||
updateButtonClick={this.updateButtonClick}
|
<ActiveFilters
|
||||||
serverChanged={serverChanged}
|
classificationTypes={classificationTypes}
|
||||||
filterModel={filterModel}
|
filterModel={filterModel}
|
||||||
setUser={this.setUser}
|
filterBarFilters={filterBarFilters}
|
||||||
user={user}
|
isFieldFilterVisible={isFieldFilterVisible}
|
||||||
setCurrentRepoTreeStatus={this.setCurrentRepoTreeStatus}
|
toggleFieldFilterVisible={this.toggleFieldFilterVisible}
|
||||||
getAllShownJobs={this.getAllShownJobs}
|
/>
|
||||||
duplicateJobsVisible={duplicateJobsVisible}
|
)}
|
||||||
groupCountsExpanded={groupCountsExpanded}
|
{serverChangedDelayed && (
|
||||||
toggleFieldFilterVisible={this.toggleFieldFilterVisible}
|
<UpdateAvailable updateButtonClick={this.updateButtonClick} />
|
||||||
pushHealthVisibility={pushHealthVisibility}
|
)}
|
||||||
setPushHealthVisibility={this.setPushHealthVisibility}
|
{currentRepo && (
|
||||||
/>
|
<div id="th-global-content" className="th-global-content">
|
||||||
<SplitPane
|
<span className="th-view-content" tabIndex={-1}>
|
||||||
split="horizontal"
|
<PushList
|
||||||
size={`${pushListPct}%`}
|
user={user}
|
||||||
onChange={(size) => this.handleSplitChange(size)}
|
repoName={repoName}
|
||||||
>
|
revision={revision}
|
||||||
<div className="d-flex flex-column w-100">
|
currentRepo={currentRepo}
|
||||||
{(isFieldFilterVisible || !!filterBarFilters.length) && (
|
filterModel={filterModel}
|
||||||
<ActiveFilters
|
duplicateJobsVisible={duplicateJobsVisible}
|
||||||
classificationTypes={classificationTypes}
|
groupCountsExpanded={groupCountsExpanded}
|
||||||
filterModel={filterModel}
|
pushHealthVisibility={pushHealthVisibility}
|
||||||
filterBarFilters={filterBarFilters}
|
getAllShownJobs={this.getAllShownJobs}
|
||||||
isFieldFilterVisible={isFieldFilterVisible}
|
{...this.props}
|
||||||
toggleFieldFilterVisible={this.toggleFieldFilterVisible}
|
/>
|
||||||
/>
|
</span>
|
||||||
)}
|
</div>
|
||||||
{serverChangedDelayed && (
|
)}
|
||||||
<UpdateAvailable updateButtonClick={this.updateButtonClick} />
|
</div>
|
||||||
)}
|
<>
|
||||||
{currentRepo && (
|
{currentRepo && (
|
||||||
<div id="th-global-content" className="th-global-content">
|
<DetailsPanel
|
||||||
<span className="th-view-content" tabIndex={-1}>
|
resizedHeight={detailsHeight}
|
||||||
<PushList
|
currentRepo={currentRepo}
|
||||||
user={user}
|
user={user}
|
||||||
repoName={repoName}
|
classificationTypes={classificationTypes}
|
||||||
revision={revision}
|
classificationMap={classificationMap}
|
||||||
currentRepo={currentRepo}
|
/>
|
||||||
filterModel={filterModel}
|
)}
|
||||||
duplicateJobsVisible={duplicateJobsVisible}
|
</>
|
||||||
groupCountsExpanded={groupCountsExpanded}
|
</SplitPane>
|
||||||
pushHealthVisibility={pushHealthVisibility}
|
<Notifications />
|
||||||
getAllShownJobs={this.getAllShownJobs}
|
<Modal
|
||||||
/>
|
isOpen={showShortCuts}
|
||||||
</span>
|
toggle={() => this.showOnScreenShortcuts(false)}
|
||||||
</div>
|
id="onscreen-shortcuts"
|
||||||
)}
|
>
|
||||||
</div>
|
<ShortcutTable />
|
||||||
<>
|
</Modal>
|
||||||
{currentRepo && (
|
</KeyboardShortcuts>
|
||||||
<DetailsPanel
|
|
||||||
resizedHeight={detailsHeight}
|
|
||||||
currentRepo={currentRepo}
|
|
||||||
user={user}
|
|
||||||
classificationTypes={classificationTypes}
|
|
||||||
classificationMap={classificationMap}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
</SplitPane>
|
|
||||||
<Notifications />
|
|
||||||
<Modal
|
|
||||||
isOpen={showShortCuts}
|
|
||||||
toggle={() => this.showOnScreenShortcuts(false)}
|
|
||||||
id="onscreen-shortcuts"
|
|
||||||
>
|
|
||||||
<ShortcutTable />
|
|
||||||
</Modal>
|
|
||||||
</KeyboardShortcuts>
|
|
||||||
</Provider>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default hot(App);
|
App.propTypes = {
|
||||||
|
jobMap: PropTypes.shape({}).isRequired,
|
||||||
|
router: PropTypes.shape({}).isRequired,
|
||||||
|
pushRoute: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = ({ pushes: { jobMap }, router }) => ({
|
||||||
|
jobMap,
|
||||||
|
router,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, {
|
||||||
|
pushRoute,
|
||||||
|
clearExpiredNotifications,
|
||||||
|
})(hot(App));
|
||||||
|
|
|
@ -189,7 +189,7 @@ class DetailsPanel extends React.Component {
|
||||||
// one selects job after job, over the course of a day, it can add up. Therefore, we keep
|
// one selects job after job, over the course of a day, it can add up. Therefore, we keep
|
||||||
// selectedJobFull data as transient only when the job is selected.
|
// selectedJobFull data as transient only when the job is selected.
|
||||||
const selectedJobFull = jobResult;
|
const selectedJobFull = jobResult;
|
||||||
const jobRevision = push.revision;
|
const jobRevision = push ? push.revision : null;
|
||||||
|
|
||||||
addAggregateFields(selectedJobFull);
|
addAggregateFields(selectedJobFull);
|
||||||
|
|
||||||
|
@ -251,7 +251,7 @@ class DetailsPanel extends React.Component {
|
||||||
...d,
|
...d,
|
||||||
}))
|
}))
|
||||||
.map((d) => ({
|
.map((d) => ({
|
||||||
url: `/perf.html#/graphs?series=${[
|
url: `/perfherder/graphs?series=${[
|
||||||
currentRepo.name,
|
currentRepo.name,
|
||||||
d.signature_id,
|
d.signature_id,
|
||||||
1,
|
1,
|
||||||
|
|
|
@ -3,10 +3,13 @@ import PropTypes from 'prop-types';
|
||||||
import { Button } from 'reactstrap';
|
import { Button } from 'reactstrap';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faTimesCircle } from '@fortawesome/free-solid-svg-icons';
|
import { faTimesCircle } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import { updateRange } from '../redux/stores/pushes';
|
||||||
|
import { clearSelectedJob } from '../redux/stores/selectedJob';
|
||||||
import { getFieldChoices } from '../../helpers/filter';
|
import { getFieldChoices } from '../../helpers/filter';
|
||||||
|
|
||||||
export default class ActiveFilters extends React.Component {
|
class ActiveFilters extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
@ -70,8 +73,32 @@ export default class ActiveFilters extends React.Component {
|
||||||
this.props.toggleFieldFilterVisible();
|
this.props.toggleFieldFilterVisible();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
clearAndUpdateRange = (specificFilter = null) => {
|
||||||
|
const { updateRange, filterModel, router, clearSelectedJob } = this.props;
|
||||||
|
|
||||||
|
const params = new URLSearchParams(router.location.search);
|
||||||
|
|
||||||
|
if (!specificFilter) {
|
||||||
|
filterModel.clearNonStatusFilters();
|
||||||
|
} else {
|
||||||
|
const { filterField, filterValue } = specificFilter;
|
||||||
|
filterModel.removeFilter(filterField, filterValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// we do this because anytime the 'revision' or 'author' param is changed,
|
||||||
|
// updateRange will be triggered in PushList's componentDidUpdate lifecycle.
|
||||||
|
// This also helps in the scenario where we are only changing the global window location query params
|
||||||
|
// (to also prevent an unnecessary componentDidUpdate change) such as when a user clicks to view
|
||||||
|
// a revision, then selects "next x pushes" to set a range.
|
||||||
|
if (!params.has('revision') && !params.has('author')) {
|
||||||
|
updateRange(filterModel.getUrlParamsWithoutDefaults());
|
||||||
|
} else if (params.has('selectedTaskRun')) {
|
||||||
|
clearSelectedJob(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isFieldFilterVisible, filterModel, filterBarFilters } = this.props;
|
const { isFieldFilterVisible, filterBarFilters } = this.props;
|
||||||
const {
|
const {
|
||||||
newFilterField,
|
newFilterField,
|
||||||
newFilterMatchType,
|
newFilterMatchType,
|
||||||
|
@ -89,7 +116,7 @@ export default class ActiveFilters extends React.Component {
|
||||||
outline
|
outline
|
||||||
className="pointable bg-transparent border-0 pt-0 pr-1 pb-1"
|
className="pointable bg-transparent border-0 pt-0 pr-1 pb-1"
|
||||||
title="Clear all of these filters"
|
title="Clear all of these filters"
|
||||||
onClick={filterModel.clearNonStatusFilters}
|
onClick={() => this.clearAndUpdateRange()}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faTimesCircle}
|
icon={faTimesCircle}
|
||||||
|
@ -111,13 +138,13 @@ export default class ActiveFilters extends React.Component {
|
||||||
className="pointable bg-transparent border-0 py-0 pr-1"
|
className="pointable bg-transparent border-0 py-0 pr-1"
|
||||||
title={`Clear filter: ${filter.field}`}
|
title={`Clear filter: ${filter.field}`}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
filterModel.removeFilter(filter.field, filterValue)
|
this.clearAndUpdateRange({
|
||||||
|
filterField: filter.field,
|
||||||
|
filterValue,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon={faTimesCircle} />
|
||||||
icon={faTimesCircle}
|
|
||||||
title={`Clear filter: ${filter.field}`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</Button>
|
</Button>
|
||||||
<span title={`Filter by ${filter.field}: ${filterValue}`}>
|
<span title={`Filter by ${filter.field}: ${filterValue}`}>
|
||||||
|
@ -235,4 +262,15 @@ ActiveFilters.propTypes = {
|
||||||
isFieldFilterVisible: PropTypes.bool.isRequired,
|
isFieldFilterVisible: PropTypes.bool.isRequired,
|
||||||
toggleFieldFilterVisible: PropTypes.func.isRequired,
|
toggleFieldFilterVisible: PropTypes.func.isRequired,
|
||||||
classificationTypes: PropTypes.arrayOf(PropTypes.object).isRequired,
|
classificationTypes: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
router: PropTypes.shape({}).isRequired,
|
||||||
|
clearSelectedJob: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = ({ router }) => ({
|
||||||
|
router,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, {
|
||||||
|
updateRange,
|
||||||
|
clearSelectedJob,
|
||||||
|
})(ActiveFilters);
|
||||||
|
|
|
@ -9,9 +9,9 @@ import {
|
||||||
} from 'reactstrap';
|
} from 'reactstrap';
|
||||||
import { faCheck } from '@fortawesome/free-solid-svg-icons';
|
import { faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { thAllResultStatuses } from '../../helpers/constants';
|
import { thAllResultStatuses } from '../../helpers/constants';
|
||||||
import { getJobsUrl } from '../../helpers/url';
|
|
||||||
import { setSelectedJob, clearSelectedJob } from '../redux/stores/selectedJob';
|
import { setSelectedJob, clearSelectedJob } from '../redux/stores/selectedJob';
|
||||||
import { pinJobs } from '../redux/stores/pinnedJobs';
|
import { pinJobs } from '../redux/stores/pinnedJobs';
|
||||||
|
|
||||||
|
@ -42,6 +42,12 @@ function FiltersMenu(props) {
|
||||||
};
|
};
|
||||||
const { email } = user;
|
const { email } = user;
|
||||||
|
|
||||||
|
const updateParams = (param, value) => {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
params.set(param, value);
|
||||||
|
return `?${params.toString()}`;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UncontrolledDropdown>
|
<UncontrolledDropdown>
|
||||||
<DropdownToggle
|
<DropdownToggle
|
||||||
|
@ -110,12 +116,13 @@ function FiltersMenu(props) {
|
||||||
>
|
>
|
||||||
Superseded only
|
Superseded only
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem
|
<DropdownItem title={`Show only pushes for ${email}`}>
|
||||||
tag="a"
|
<Link
|
||||||
title={`Show only pushes for ${email}`}
|
className="dropdown-link"
|
||||||
href={getJobsUrl({ author: email })}
|
to={{ search: updateParams('author', email) }}
|
||||||
>
|
>
|
||||||
My pushes only
|
My pushes only
|
||||||
|
</Link>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
tag="a"
|
tag="a"
|
||||||
|
|
|
@ -91,6 +91,7 @@ class PrimaryNavBar extends React.Component {
|
||||||
duplicateJobsVisible={duplicateJobsVisible}
|
duplicateJobsVisible={duplicateJobsVisible}
|
||||||
groupCountsExpanded={groupCountsExpanded}
|
groupCountsExpanded={groupCountsExpanded}
|
||||||
toggleFieldFilterVisible={toggleFieldFilterVisible}
|
toggleFieldFilterVisible={toggleFieldFilterVisible}
|
||||||
|
{...this.props}
|
||||||
/>
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,8 +8,9 @@ import {
|
||||||
DropdownToggle,
|
DropdownToggle,
|
||||||
UncontrolledDropdown,
|
UncontrolledDropdown,
|
||||||
} from 'reactstrap';
|
} from 'reactstrap';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { getRepoUrl } from '../../helpers/location';
|
import { updateRepoParams } from '../../helpers/location';
|
||||||
|
|
||||||
const GROUP_ORDER = [
|
const GROUP_ORDER = [
|
||||||
'development',
|
'development',
|
||||||
|
@ -83,13 +84,12 @@ export default function ReposMenu(props) {
|
||||||
{!!group.repos &&
|
{!!group.repos &&
|
||||||
group.repos.map((repo) => (
|
group.repos.map((repo) => (
|
||||||
<li key={repo.name}>
|
<li key={repo.name}>
|
||||||
<a
|
<Link
|
||||||
title="Open repo"
|
|
||||||
className="dropdown-link"
|
className="dropdown-link"
|
||||||
href={getRepoUrl(repo.name)}
|
to={{ search: updateRepoParams(repo.name) }}
|
||||||
>
|
>
|
||||||
{repo.name}
|
{repo.name}
|
||||||
</a>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
|
|
@ -9,10 +9,11 @@ import {
|
||||||
faFilter,
|
faFilter,
|
||||||
faTimesCircle,
|
faTimesCircle,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { push as pushRoute } from 'connected-react-router';
|
||||||
|
|
||||||
import { getBtnClass } from '../../helpers/job';
|
import { getBtnClass } from '../../helpers/job';
|
||||||
import { hasUrlFilterChanges, thFilterGroups } from '../../helpers/filter';
|
import { hasUrlFilterChanges, thFilterGroups } from '../../helpers/filter';
|
||||||
import { getRepo, getUrlParam, setUrlParam } from '../../helpers/location';
|
import { getRepo, getUrlParam, setUrlParams } from '../../helpers/location';
|
||||||
import RepositoryModel from '../../models/repository';
|
import RepositoryModel from '../../models/repository';
|
||||||
import ErrorBoundary from '../../shared/ErrorBoundary';
|
import ErrorBoundary from '../../shared/ErrorBoundary';
|
||||||
import { recalculateUnclassifiedCounts } from '../redux/stores/pushes';
|
import { recalculateUnclassifiedCounts } from '../redux/stores/pushes';
|
||||||
|
@ -46,7 +47,6 @@ class SecondaryNavBar extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
window.addEventListener('hashchange', this.handleUrlChanges, false);
|
|
||||||
this.loadWatchedRepos();
|
this.loadWatchedRepos();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,18 +56,22 @@ class SecondaryNavBar extends React.PureComponent {
|
||||||
if (repoName !== prevState.repoName) {
|
if (repoName !== prevState.repoName) {
|
||||||
this.loadWatchedRepos();
|
this.loadWatchedRepos();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
if (
|
||||||
window.removeEventListener('hashchange', this.handleUrlChanges, false);
|
prevProps.router.location.search !== this.props.router.location.search
|
||||||
|
) {
|
||||||
|
this.handleUrlChanges(
|
||||||
|
prevProps.router.location.search,
|
||||||
|
this.props.router.location.search,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setSearchStr(ev) {
|
setSearchStr(ev) {
|
||||||
this.setState({ searchQueryStr: ev.target.value });
|
this.setState({ searchQueryStr: ev.target.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUrlChanges = (evt) => {
|
handleUrlChanges = (prevParams, currentParams) => {
|
||||||
const { oldURL, newURL } = evt;
|
|
||||||
const { repoName } = this.state;
|
const { repoName } = this.state;
|
||||||
const { recalculateUnclassifiedCounts } = this.props;
|
const { recalculateUnclassifiedCounts } = this.props;
|
||||||
const newState = {
|
const newState = {
|
||||||
|
@ -77,7 +81,7 @@ class SecondaryNavBar extends React.PureComponent {
|
||||||
|
|
||||||
this.setState(newState, () => {
|
this.setState(newState, () => {
|
||||||
if (
|
if (
|
||||||
hasUrlFilterChanges(oldURL, newURL) ||
|
hasUrlFilterChanges(prevParams, currentParams) ||
|
||||||
newState.repoName !== repoName
|
newState.repoName !== repoName
|
||||||
) {
|
) {
|
||||||
recalculateUnclassifiedCounts();
|
recalculateUnclassifiedCounts();
|
||||||
|
@ -124,17 +128,25 @@ class SecondaryNavBar extends React.PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleShowDuplicateJobs = () => {
|
toggleShowDuplicateJobs = () => {
|
||||||
const { duplicateJobsVisible } = this.props;
|
const { duplicateJobsVisible, pushRoute } = this.props;
|
||||||
const duplicateJobs = duplicateJobsVisible ? null : 'visible';
|
const duplicateJobs = duplicateJobsVisible ? null : 'visible';
|
||||||
|
|
||||||
setUrlParam('duplicate_jobs', duplicateJobs);
|
const queryParams = setUrlParams([['duplicate_jobs', duplicateJobs]]);
|
||||||
|
|
||||||
|
pushRoute({
|
||||||
|
search: queryParams,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleGroupState = () => {
|
toggleGroupState = () => {
|
||||||
const { groupCountsExpanded } = this.props;
|
const { groupCountsExpanded, pushRoute } = this.props;
|
||||||
const groupState = groupCountsExpanded ? null : 'expanded';
|
const groupState = groupCountsExpanded ? null : 'expanded';
|
||||||
|
|
||||||
setUrlParam('group_state', groupState);
|
const queryParams = setUrlParams([['group_state', groupState]]);
|
||||||
|
|
||||||
|
pushRoute({
|
||||||
|
search: queryParams,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleUnclassifiedFailures = () => {
|
toggleUnclassifiedFailures = () => {
|
||||||
|
@ -228,6 +240,7 @@ class SecondaryNavBar extends React.PureComponent {
|
||||||
repoName={repoName}
|
repoName={repoName}
|
||||||
unwatchRepo={this.unwatchRepo}
|
unwatchRepo={this.unwatchRepo}
|
||||||
setCurrentRepoTreeStatus={setCurrentRepoTreeStatus}
|
setCurrentRepoTreeStatus={setCurrentRepoTreeStatus}
|
||||||
|
{...this.props}
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
))}
|
))}
|
||||||
|
@ -299,7 +312,7 @@ class SecondaryNavBar extends React.PureComponent {
|
||||||
? 'Collapse job groups'
|
? 'Collapse job groups'
|
||||||
: 'Expand job groups'
|
: 'Expand job groups'
|
||||||
}
|
}
|
||||||
onClick={() => this.toggleGroupState()}
|
onClick={this.toggleGroupState}
|
||||||
>
|
>
|
||||||
(
|
(
|
||||||
<span className="group-state-nav-icon mx-1">
|
<span className="group-state-nav-icon mx-1">
|
||||||
|
@ -395,12 +408,19 @@ SecondaryNavBar.propTypes = {
|
||||||
duplicateJobsVisible: PropTypes.bool.isRequired,
|
duplicateJobsVisible: PropTypes.bool.isRequired,
|
||||||
groupCountsExpanded: PropTypes.bool.isRequired,
|
groupCountsExpanded: PropTypes.bool.isRequired,
|
||||||
toggleFieldFilterVisible: PropTypes.func.isRequired,
|
toggleFieldFilterVisible: PropTypes.func.isRequired,
|
||||||
|
pushRoute: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = ({
|
const mapStateToProps = ({
|
||||||
pushes: { allUnclassifiedFailureCount, filteredUnclassifiedFailureCount },
|
pushes: { allUnclassifiedFailureCount, filteredUnclassifiedFailureCount },
|
||||||
}) => ({ allUnclassifiedFailureCount, filteredUnclassifiedFailureCount });
|
router,
|
||||||
|
}) => ({
|
||||||
|
allUnclassifiedFailureCount,
|
||||||
|
filteredUnclassifiedFailureCount,
|
||||||
|
router,
|
||||||
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, { recalculateUnclassifiedCounts })(
|
export default connect(mapStateToProps, {
|
||||||
SecondaryNavBar,
|
recalculateUnclassifiedCounts,
|
||||||
);
|
pushRoute,
|
||||||
|
})(SecondaryNavBar);
|
||||||
|
|
|
@ -18,10 +18,12 @@ import {
|
||||||
DropdownToggle,
|
DropdownToggle,
|
||||||
UncontrolledDropdown,
|
UncontrolledDropdown,
|
||||||
} from 'reactstrap';
|
} from 'reactstrap';
|
||||||
|
import { push as pushRoute } from 'connected-react-router';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import TreeStatusModel from '../../models/treeStatus';
|
import TreeStatusModel from '../../models/treeStatus';
|
||||||
import BugLinkify from '../../shared/BugLinkify';
|
import BugLinkify from '../../shared/BugLinkify';
|
||||||
import { getRepoUrl } from '../../helpers/location';
|
import { updateRepoParams } from '../../helpers/location';
|
||||||
|
|
||||||
const statusInfoMap = {
|
const statusInfoMap = {
|
||||||
open: {
|
open: {
|
||||||
|
@ -57,7 +59,7 @@ const statusInfoMap = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class WatchedRepo extends React.Component {
|
class WatchedRepo extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
@ -124,18 +126,17 @@ export default class WatchedRepo extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { repoName, unwatchRepo, repo } = this.props;
|
const { repoName, unwatchRepo, repo, pushRoute } = this.props;
|
||||||
const { status, messageOfTheDay, reason, statusInfo } = this.state;
|
const { status, messageOfTheDay, reason, statusInfo } = this.state;
|
||||||
const watchedRepo = repo.name;
|
const watchedRepo = repo.name;
|
||||||
const activeClass = watchedRepo === repoName ? 'active' : '';
|
const activeClass = watchedRepo === repoName ? 'active' : '';
|
||||||
const { btnClass, icon, color } = statusInfo;
|
const { btnClass, icon, color } = statusInfo;
|
||||||
const pulseIcon = statusInfo.pulseIcon || null;
|
const pulseIcon = statusInfo.pulseIcon || null;
|
||||||
const changeRepoUrl = getRepoUrl(watchedRepo);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<Button
|
<Button
|
||||||
href={changeRepoUrl}
|
onClick={() => pushRoute({ search: updateRepoParams(watchedRepo) })}
|
||||||
className={`btn-view-nav ${btnClass} ${activeClass}`}
|
className={`btn-view-nav ${btnClass} ${activeClass}`}
|
||||||
title={status}
|
title={status}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
@ -230,4 +231,7 @@ WatchedRepo.propTypes = {
|
||||||
pushLogUrl: PropTypes.string,
|
pushLogUrl: PropTypes.string,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
setCurrentRepoTreeStatus: PropTypes.func.isRequired,
|
setCurrentRepoTreeStatus: PropTypes.func.isRequired,
|
||||||
|
pushRoute: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default connect(null, { pushRoute })(WatchedRepo);
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { render } from 'react-dom';
|
|
||||||
|
|
||||||
// Treeherder Styles
|
|
||||||
import '../css/treeherder.css';
|
|
||||||
import '../css/treeherder-base.css';
|
|
||||||
import '../css/treeherder-custom-styles.css';
|
|
||||||
import '../css/treeherder-navbar.css';
|
|
||||||
import '../css/treeherder-navbar-panels.css';
|
|
||||||
import '../css/treeherder-notifications.css';
|
|
||||||
import '../css/treeherder-details-panel.css';
|
|
||||||
import '../css/failure-summary.css';
|
|
||||||
import '../css/treeherder-job-buttons.css';
|
|
||||||
import '../css/treeherder-pushes.css';
|
|
||||||
import '../css/treeherder-pinboard.css';
|
|
||||||
import '../css/treeherder-bugfiler.css';
|
|
||||||
import '../css/treeherder-fuzzyfinder.css';
|
|
||||||
import '../css/treeherder-loading-overlay.css';
|
|
||||||
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
render(<App />, document.getElementById('root'));
|
|
|
@ -118,13 +118,14 @@ export default class JobButtonComponent extends React.Component {
|
||||||
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
classes.push('selected-job btn-lg-xform');
|
classes.push('selected-job btn-lg-xform');
|
||||||
|
attributes['data-testid'] = 'selected-job';
|
||||||
} else {
|
} else {
|
||||||
classes.push('btn-xs');
|
classes.push('btn-xs');
|
||||||
}
|
}
|
||||||
|
|
||||||
attributes.className = classes.join(' ');
|
attributes.className = classes.join(' ');
|
||||||
return (
|
return (
|
||||||
<button type="button" {...attributes}>
|
<button type="button" {...attributes} data-testid="job-btn">
|
||||||
{jobTypeSymbol}
|
{jobTypeSymbol}
|
||||||
{classifiedIcon && (
|
{classifiedIcon && (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
|
|
|
@ -14,6 +14,7 @@ export default function JobCount(props) {
|
||||||
className={classes.join(' ')}
|
className={classes.join(' ')}
|
||||||
title={title}
|
title={title}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
data-testid="job-group-count"
|
||||||
>
|
>
|
||||||
{count}
|
{count}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -11,7 +11,11 @@ import {
|
||||||
} from '../../helpers/constants';
|
} from '../../helpers/constants';
|
||||||
import decompress from '../../helpers/gzip';
|
import decompress from '../../helpers/gzip';
|
||||||
import { getGroupMapKey } from '../../helpers/aggregateId';
|
import { getGroupMapKey } from '../../helpers/aggregateId';
|
||||||
import { getAllUrlParams, getUrlParam } from '../../helpers/location';
|
import {
|
||||||
|
getAllUrlParams,
|
||||||
|
getUrlParam,
|
||||||
|
setUrlParam,
|
||||||
|
} from '../../helpers/location';
|
||||||
import JobModel from '../../models/job';
|
import JobModel from '../../models/job';
|
||||||
import RunnableJobModel from '../../models/runnableJob';
|
import RunnableJobModel from '../../models/runnableJob';
|
||||||
import { getRevisionTitle } from '../../helpers/revision';
|
import { getRevisionTitle } from '../../helpers/revision';
|
||||||
|
@ -122,17 +126,21 @@ class Push extends React.PureComponent {
|
||||||
this.testForFilteredTry();
|
this.testForFilteredTry();
|
||||||
|
|
||||||
window.addEventListener(thEvents.applyNewJobs, this.handleApplyNewJobs);
|
window.addEventListener(thEvents.applyNewJobs, this.handleApplyNewJobs);
|
||||||
window.addEventListener('hashchange', this.handleUrlChanges);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
this.showUpdateNotifications(prevState);
|
this.showUpdateNotifications(prevState);
|
||||||
this.testForFilteredTry();
|
this.testForFilteredTry();
|
||||||
|
|
||||||
|
if (
|
||||||
|
prevProps.router.location.search !== this.props.router.location.search
|
||||||
|
) {
|
||||||
|
this.handleUrlChanges();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
window.removeEventListener(thEvents.applyNewJobs, this.handleApplyNewJobs);
|
window.removeEventListener(thEvents.applyNewJobs, this.handleApplyNewJobs);
|
||||||
window.removeEventListener('hashchange', this.handleUrlChanges);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getJobCount(jobList) {
|
getJobCount(jobList) {
|
||||||
|
@ -185,6 +193,30 @@ class Push extends React.PureComponent {
|
||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
togglePushCollapsed = () => {
|
||||||
|
const { push } = this.props;
|
||||||
|
const pushId = `${push.id}`;
|
||||||
|
const collapsedPushesParam = getUrlParam('collapsedPushes');
|
||||||
|
const collapsedPushes = collapsedPushesParam
|
||||||
|
? new Set(collapsedPushesParam.split(','))
|
||||||
|
: new Set();
|
||||||
|
|
||||||
|
this.setState(
|
||||||
|
(prevState) => ({ collapsed: !prevState.collapsed }),
|
||||||
|
() => {
|
||||||
|
if (!this.state.collapsed) {
|
||||||
|
collapsedPushes.delete(pushId);
|
||||||
|
} else {
|
||||||
|
collapsedPushes.add(pushId);
|
||||||
|
}
|
||||||
|
setUrlParam(
|
||||||
|
'collapsedPushes',
|
||||||
|
collapsedPushes.size ? Array.from(collapsedPushes) : null,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
testForFilteredTry = () => {
|
testForFilteredTry = () => {
|
||||||
const { currentRepo } = this.props;
|
const { currentRepo } = this.props;
|
||||||
const filterParams = ['revision', 'author'];
|
const filterParams = ['revision', 'author'];
|
||||||
|
@ -633,6 +665,7 @@ class Push extends React.PureComponent {
|
||||||
pushHealthVisibility={pushHealthVisibility}
|
pushHealthVisibility={pushHealthVisibility}
|
||||||
groupCountsExpanded={groupCountsExpanded}
|
groupCountsExpanded={groupCountsExpanded}
|
||||||
pushHealthStatusCallback={this.pushHealthStatusCallback}
|
pushHealthStatusCallback={this.pushHealthStatusCallback}
|
||||||
|
togglePushCollapsed={this.togglePushCollapsed}
|
||||||
/>
|
/>
|
||||||
<div className="push-body-divider" />
|
<div className="push-body-divider" />
|
||||||
{!collapsed ? (
|
{!collapsed ? (
|
||||||
|
@ -713,10 +746,12 @@ Push.propTypes = {
|
||||||
|
|
||||||
const mapStateToProps = ({
|
const mapStateToProps = ({
|
||||||
pushes: { allUnclassifiedFailureCount, decisionTaskMap, bugSummaryMap },
|
pushes: { allUnclassifiedFailureCount, decisionTaskMap, bugSummaryMap },
|
||||||
|
router,
|
||||||
}) => ({
|
}) => ({
|
||||||
allUnclassifiedFailureCount,
|
allUnclassifiedFailureCount,
|
||||||
decisionTaskMap,
|
decisionTaskMap,
|
||||||
bugSummaryMap,
|
bugSummaryMap,
|
||||||
|
router,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, {
|
export default connect(mapStateToProps, {
|
||||||
|
|
|
@ -7,55 +7,39 @@ import {
|
||||||
DropdownToggle,
|
DropdownToggle,
|
||||||
UncontrolledDropdown,
|
UncontrolledDropdown,
|
||||||
} from 'reactstrap';
|
} from 'reactstrap';
|
||||||
|
import { push as pushRoute } from 'connected-react-router';
|
||||||
|
|
||||||
import { getUrlParam } from '../../helpers/location';
|
import {
|
||||||
|
createQueryParams,
|
||||||
|
getPushHealthUrl,
|
||||||
|
getCompareChooserUrl,
|
||||||
|
parseQueryParams,
|
||||||
|
} from '../../helpers/url';
|
||||||
import { formatTaskclusterError } from '../../helpers/errorMessage';
|
import { formatTaskclusterError } from '../../helpers/errorMessage';
|
||||||
import CustomJobActions from '../CustomJobActions';
|
import CustomJobActions from '../CustomJobActions';
|
||||||
import PushModel from '../../models/push';
|
import PushModel from '../../models/push';
|
||||||
import { getPushHealthUrl, getCompareChooserUrl } from '../../helpers/url';
|
|
||||||
import { notify } from '../redux/stores/notifications';
|
import { notify } from '../redux/stores/notifications';
|
||||||
import { thEvents } from '../../helpers/constants';
|
import { updateRange } from '../redux/stores/pushes';
|
||||||
|
|
||||||
// Trigger missing jobs is dangerous on repos other than these (see bug 1335506)
|
|
||||||
const triggerMissingRepos = ['mozilla-inbound', 'autoland'];
|
|
||||||
|
|
||||||
class PushActionMenu extends React.PureComponent {
|
class PushActionMenu extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const { revision } = this.props;
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
topOfRangeUrl: this.getRangeChangeUrl('tochange', revision),
|
|
||||||
bottomOfRangeUrl: this.getRangeChangeUrl('fromchange', revision),
|
|
||||||
customJobActionsShowing: false,
|
customJobActionsShowing: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
updateParamsAndRange = (param) => {
|
||||||
window.addEventListener('hashchange', this.handleUrlChanges, false);
|
const { revision, updateRange, pushRoute } = this.props;
|
||||||
window.addEventListener(thEvents.filtersUpdated, this.handleUrlChanges);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
let queryParams = parseQueryParams(window.location.search);
|
||||||
window.removeEventListener('hashchange', this.handleUrlChanges, false);
|
queryParams = { ...queryParams, ...{ [param]: revision } };
|
||||||
window.removeEventListener(thEvents.filtersUpdated, this.handleUrlChanges);
|
|
||||||
}
|
|
||||||
|
|
||||||
getRangeChangeUrl(param, revision) {
|
pushRoute({
|
||||||
let url = window.location.href;
|
search: createQueryParams(queryParams),
|
||||||
url = url.replace(`&${param}=${getUrlParam(param)}`, '');
|
|
||||||
url = url.replace(`&${'selectedJob'}=${getUrlParam('selectedJob')}`, '');
|
|
||||||
return `${url}&${param}=${revision}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleUrlChanges = () => {
|
|
||||||
const { revision } = this.props;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
topOfRangeUrl: this.getRangeChangeUrl('tochange', revision),
|
|
||||||
bottomOfRangeUrl: this.getRangeChangeUrl('fromchange', revision),
|
|
||||||
});
|
});
|
||||||
|
updateRange(queryParams);
|
||||||
};
|
};
|
||||||
|
|
||||||
triggerMissingJobs = () => {
|
triggerMissingJobs = () => {
|
||||||
|
@ -102,11 +86,7 @@ class PushActionMenu extends React.PureComponent {
|
||||||
pushId,
|
pushId,
|
||||||
currentRepo,
|
currentRepo,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const {
|
const { customJobActionsShowing } = this.state;
|
||||||
topOfRangeUrl,
|
|
||||||
bottomOfRangeUrl,
|
|
||||||
customJobActionsShowing,
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
@ -143,15 +123,13 @@ class PushActionMenu extends React.PureComponent {
|
||||||
>
|
>
|
||||||
Add new jobs (Search)
|
Add new jobs (Search)
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
{triggerMissingRepos.includes(currentRepo.name) && (
|
<DropdownItem
|
||||||
<DropdownItem
|
tag="a"
|
||||||
tag="a"
|
title="Trigger all jobs that were optimized away"
|
||||||
title="Trigger all jobs that were optimized away"
|
onClick={this.triggerMissingJobs}
|
||||||
onClick={this.triggerMissingJobs}
|
>
|
||||||
>
|
Trigger missing jobs
|
||||||
Trigger missing jobs
|
</DropdownItem>
|
||||||
</DropdownItem>
|
|
||||||
)}
|
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
tag="a"
|
tag="a"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@ -170,14 +148,14 @@ class PushActionMenu extends React.PureComponent {
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
tag="a"
|
tag="a"
|
||||||
href={topOfRangeUrl}
|
onClick={() => this.updateParamsAndRange('tochange')}
|
||||||
data-testid="top-of-range-menu-item"
|
data-testid="top-of-range-menu-item"
|
||||||
>
|
>
|
||||||
Set as top of range
|
Set as top of range
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
tag="a"
|
tag="a"
|
||||||
href={bottomOfRangeUrl}
|
onClick={() => this.updateParamsAndRange('fromchange')}
|
||||||
data-testid="bottom-of-range-menu-item"
|
data-testid="bottom-of-range-menu-item"
|
||||||
>
|
>
|
||||||
Set as bottom of range
|
Set as bottom of range
|
||||||
|
@ -221,7 +199,7 @@ class PushActionMenu extends React.PureComponent {
|
||||||
|
|
||||||
PushActionMenu.propTypes = {
|
PushActionMenu.propTypes = {
|
||||||
runnableVisible: PropTypes.bool.isRequired,
|
runnableVisible: PropTypes.bool.isRequired,
|
||||||
revision: PropTypes.string.isRequired,
|
revision: PropTypes.string,
|
||||||
currentRepo: PropTypes.shape({
|
currentRepo: PropTypes.shape({
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
|
@ -233,8 +211,14 @@ PushActionMenu.propTypes = {
|
||||||
notify: PropTypes.func.isRequired,
|
notify: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
PushActionMenu.defaultProps = {
|
||||||
|
revision: null,
|
||||||
|
};
|
||||||
|
|
||||||
const mapStateToProps = ({ pushes: { decisionTaskMap } }) => ({
|
const mapStateToProps = ({ pushes: { decisionTaskMap } }) => ({
|
||||||
decisionTaskMap,
|
decisionTaskMap,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, { notify })(PushActionMenu);
|
export default connect(mapStateToProps, { notify, updateRange, pushRoute })(
|
||||||
|
PushActionMenu,
|
||||||
|
);
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
faTimesCircle,
|
faTimesCircle,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
import { Badge, Button } from 'reactstrap';
|
import { Badge, Button } from 'reactstrap';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { getPercentComplete, toDateStr } from '../../helpers/display';
|
import { getPercentComplete, toDateStr } from '../../helpers/display';
|
||||||
import { formatTaskclusterError } from '../../helpers/errorMessage';
|
import { formatTaskclusterError } from '../../helpers/errorMessage';
|
||||||
|
@ -20,8 +21,7 @@ import { getJobsUrl } from '../../helpers/url';
|
||||||
import PushModel from '../../models/push';
|
import PushModel from '../../models/push';
|
||||||
import JobModel from '../../models/job';
|
import JobModel from '../../models/job';
|
||||||
import PushHealthStatus from '../../shared/PushHealthStatus';
|
import PushHealthStatus from '../../shared/PushHealthStatus';
|
||||||
import PushAuthor from '../../shared/PushAuthor';
|
import { getUrlParam } from '../../helpers/location';
|
||||||
import { getUrlParam, setUrlParam } from '../../helpers/location';
|
|
||||||
import { notify } from '../redux/stores/notifications';
|
import { notify } from '../redux/stores/notifications';
|
||||||
import { setSelectedJob } from '../redux/stores/selectedJob';
|
import { setSelectedJob } from '../redux/stores/selectedJob';
|
||||||
import { pinJobs } from '../redux/stores/pinnedJobs';
|
import { pinJobs } from '../redux/stores/pinnedJobs';
|
||||||
|
@ -198,25 +198,6 @@ class PushHeader extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
togglePushCollapsed = () => {
|
|
||||||
const { push, collapsed } = this.props;
|
|
||||||
const pushId = `${push.id}`;
|
|
||||||
const collapsedPushesParam = getUrlParam('collapsedPushes');
|
|
||||||
const collapsedPushes = collapsedPushesParam
|
|
||||||
? new Set(collapsedPushesParam.split(','))
|
|
||||||
: new Set();
|
|
||||||
|
|
||||||
if (collapsed) {
|
|
||||||
collapsedPushes.delete(pushId);
|
|
||||||
} else {
|
|
||||||
collapsedPushes.add(pushId);
|
|
||||||
}
|
|
||||||
setUrlParam(
|
|
||||||
'collapsedPushes',
|
|
||||||
collapsedPushes.size ? Array.from(collapsedPushes) : null,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
pushId,
|
pushId,
|
||||||
|
@ -235,6 +216,7 @@ class PushHeader extends React.Component {
|
||||||
pushHealthVisibility,
|
pushHealthVisibility,
|
||||||
currentRepo,
|
currentRepo,
|
||||||
pushHealthStatusCallback,
|
pushHealthStatusCallback,
|
||||||
|
togglePushCollapsed,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const cancelJobsTitle = 'Cancel all jobs';
|
const cancelJobsTitle = 'Cancel all jobs';
|
||||||
const linkParams = this.getLinkParams();
|
const linkParams = this.getLinkParams();
|
||||||
|
@ -256,22 +238,22 @@ class PushHeader extends React.Component {
|
||||||
<span className="push-left">
|
<span className="push-left">
|
||||||
<span className="push-title-left">
|
<span className="push-title-left">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
onClick={this.togglePushCollapsed}
|
onClick={togglePushCollapsed}
|
||||||
icon={collapsed ? faPlusSquare : faMinusSquare}
|
icon={collapsed ? faPlusSquare : faMinusSquare}
|
||||||
className="mr-2 mt-2 text-muted pointable"
|
className="mr-2 mt-2 text-muted pointable"
|
||||||
title={`${collapsed ? 'Expand' : 'Collapse'} push data`}
|
title={`${collapsed ? 'Expand' : 'Collapse'} push data`}
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
<a href={revisionPushFilterUrl} title="View only this push">
|
<Link to={revisionPushFilterUrl} title="View only this push">
|
||||||
{this.pushDateStr}{' '}
|
{this.pushDateStr}{' '}
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faExternalLinkAlt}
|
icon={faExternalLinkAlt}
|
||||||
className="icon-superscript"
|
className="icon-superscript"
|
||||||
/>
|
/>
|
||||||
</a>{' '}
|
</Link>{' '}
|
||||||
-{' '}
|
-{' '}
|
||||||
</span>
|
</span>
|
||||||
<PushAuthor author={author} url={authorPushFilterUrl} />
|
<Link to={authorPushFilterUrl}>{author}</Link>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
{showPushHealthStatus && (
|
{showPushHealthStatus && (
|
||||||
|
@ -364,7 +346,7 @@ PushHeader.propTypes = {
|
||||||
pushId: PropTypes.number.isRequired,
|
pushId: PropTypes.number.isRequired,
|
||||||
pushTimestamp: PropTypes.number.isRequired,
|
pushTimestamp: PropTypes.number.isRequired,
|
||||||
author: PropTypes.string.isRequired,
|
author: PropTypes.string.isRequired,
|
||||||
revision: PropTypes.string.isRequired,
|
revision: PropTypes.string,
|
||||||
filterModel: PropTypes.shape({}).isRequired,
|
filterModel: PropTypes.shape({}).isRequired,
|
||||||
runnableVisible: PropTypes.bool.isRequired,
|
runnableVisible: PropTypes.bool.isRequired,
|
||||||
showRunnableJobs: PropTypes.func.isRequired,
|
showRunnableJobs: PropTypes.func.isRequired,
|
||||||
|
@ -390,6 +372,7 @@ PushHeader.propTypes = {
|
||||||
PushHeader.defaultProps = {
|
PushHeader.defaultProps = {
|
||||||
watchState: 'none',
|
watchState: 'none',
|
||||||
pushHealthStatusCallback: null,
|
pushHealthStatusCallback: null,
|
||||||
|
revision: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = ({ pushes: { decisionTaskMap } }) => ({
|
const mapStateToProps = ({ pushes: { decisionTaskMap } }) => ({
|
||||||
|
|
|
@ -11,13 +11,8 @@ import {
|
||||||
clearSelectedJob,
|
clearSelectedJob,
|
||||||
setSelectedJobFromQueryString,
|
setSelectedJobFromQueryString,
|
||||||
} from '../redux/stores/selectedJob';
|
} from '../redux/stores/selectedJob';
|
||||||
import {
|
import { fetchPushes, updateRange, pollPushes } from '../redux/stores/pushes';
|
||||||
fetchPushes,
|
import { updatePushParams } from '../../helpers/location';
|
||||||
fetchNextPushes,
|
|
||||||
updateRange,
|
|
||||||
pollPushes,
|
|
||||||
} from '../redux/stores/pushes';
|
|
||||||
import { reloadOnChangeParameters } from '../../helpers/filter';
|
|
||||||
|
|
||||||
import Push from './Push';
|
import Push from './Push';
|
||||||
import PushLoadErrors from './PushLoadErrors';
|
import PushLoadErrors from './PushLoadErrors';
|
||||||
|
@ -36,7 +31,6 @@ class PushList extends React.Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { fetchPushes } = this.props;
|
const { fetchPushes } = this.props;
|
||||||
|
|
||||||
window.addEventListener('hashchange', this.handleUrlChanges, false);
|
|
||||||
fetchPushes();
|
fetchPushes();
|
||||||
this.poll();
|
this.poll();
|
||||||
}
|
}
|
||||||
|
@ -52,6 +46,7 @@ class PushList extends React.Component {
|
||||||
if (jobsLoaded && jobsLoaded !== prevProps.jobsLoaded) {
|
if (jobsLoaded && jobsLoaded !== prevProps.jobsLoaded) {
|
||||||
setSelectedJobFromQueryString(notify, jobMap);
|
setSelectedJobFromQueryString(notify, jobMap);
|
||||||
}
|
}
|
||||||
|
this.handleUrlChanges(prevProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -59,7 +54,6 @@ class PushList extends React.Component {
|
||||||
clearInterval(this.pushIntervalId);
|
clearInterval(this.pushIntervalId);
|
||||||
this.pushIntervalId = null;
|
this.pushIntervalId = null;
|
||||||
}
|
}
|
||||||
window.addEventListener('hashchange', this.handleUrlChanges, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setWindowTitle() {
|
setWindowTitle() {
|
||||||
|
@ -68,11 +62,18 @@ class PushList extends React.Component {
|
||||||
document.title = `[${allUnclassifiedFailureCount}] ${repoName}`;
|
document.title = `[${allUnclassifiedFailureCount}] ${repoName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getUrlRangeValues = (url) => {
|
getUrlRangeValues = (search) => {
|
||||||
const params = [...new URLSearchParams(url.split('?')[1]).entries()];
|
const params = [...new URLSearchParams(search)];
|
||||||
|
|
||||||
return params.reduce((acc, [key, value]) => {
|
return params.reduce((acc, [key, value]) => {
|
||||||
return reloadOnChangeParameters.includes(key)
|
return [
|
||||||
|
'repo',
|
||||||
|
'startdate',
|
||||||
|
'enddate',
|
||||||
|
'nojobs',
|
||||||
|
'revision',
|
||||||
|
'author',
|
||||||
|
].includes(key)
|
||||||
? { ...acc, [key]: value }
|
? { ...acc, [key]: value }
|
||||||
: acc;
|
: acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
@ -86,11 +87,10 @@ class PushList extends React.Component {
|
||||||
}, PUSH_POLL_INTERVAL);
|
}, PUSH_POLL_INTERVAL);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleUrlChanges = (evt) => {
|
handleUrlChanges = (prevProps) => {
|
||||||
const { updateRange } = this.props;
|
const { updateRange, router } = this.props;
|
||||||
const { oldURL, newURL } = evt;
|
const oldRange = this.getUrlRangeValues(prevProps.router.location.search);
|
||||||
const oldRange = this.getUrlRangeValues(oldURL);
|
const newRange = this.getUrlRangeValues(router.location.search);
|
||||||
const newRange = this.getUrlRangeValues(newURL);
|
|
||||||
|
|
||||||
if (!isEqual(oldRange, newRange)) {
|
if (!isEqual(oldRange, newRange)) {
|
||||||
updateRange(newRange);
|
updateRange(newRange);
|
||||||
|
@ -115,6 +115,13 @@ class PushList extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fetchNextPushes(count) {
|
||||||
|
const { fetchPushes, router } = this.props;
|
||||||
|
const params = updatePushParams(router.location);
|
||||||
|
window.history.pushState(null, null, params);
|
||||||
|
fetchPushes(count, true);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
repoName,
|
repoName,
|
||||||
|
@ -123,7 +130,6 @@ class PushList extends React.Component {
|
||||||
filterModel,
|
filterModel,
|
||||||
pushList,
|
pushList,
|
||||||
loadingPushes,
|
loadingPushes,
|
||||||
fetchNextPushes,
|
|
||||||
getAllShownJobs,
|
getAllShownJobs,
|
||||||
jobsLoaded,
|
jobsLoaded,
|
||||||
duplicateJobsVisible,
|
duplicateJobsVisible,
|
||||||
|
@ -135,6 +141,7 @@ class PushList extends React.Component {
|
||||||
if (!revision) {
|
if (!revision) {
|
||||||
this.setWindowTitle();
|
this.setWindowTitle();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// Bug 1619873 - role="list" works better here than an interactive role
|
// Bug 1619873 - role="list" works better here than an interactive role
|
||||||
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
|
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
|
||||||
|
@ -188,7 +195,7 @@ class PushList extends React.Component {
|
||||||
color="darker-secondary"
|
color="darker-secondary"
|
||||||
outline
|
outline
|
||||||
className="btn-light-bordered"
|
className="btn-light-bordered"
|
||||||
onClick={() => fetchNextPushes(count)}
|
onClick={() => this.fetchNextPushes(count)}
|
||||||
key={count}
|
key={count}
|
||||||
data-testid={`get-next-${count}`}
|
data-testid={`get-next-${count}`}
|
||||||
>
|
>
|
||||||
|
@ -206,7 +213,6 @@ PushList.propTypes = {
|
||||||
repoName: PropTypes.string.isRequired,
|
repoName: PropTypes.string.isRequired,
|
||||||
filterModel: PropTypes.shape({}).isRequired,
|
filterModel: PropTypes.shape({}).isRequired,
|
||||||
pushList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
pushList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
fetchNextPushes: PropTypes.func.isRequired,
|
|
||||||
fetchPushes: PropTypes.func.isRequired,
|
fetchPushes: PropTypes.func.isRequired,
|
||||||
pollPushes: PropTypes.func.isRequired,
|
pollPushes: PropTypes.func.isRequired,
|
||||||
updateRange: PropTypes.func.isRequired,
|
updateRange: PropTypes.func.isRequired,
|
||||||
|
@ -224,6 +230,7 @@ PushList.propTypes = {
|
||||||
notify: PropTypes.func.isRequired,
|
notify: PropTypes.func.isRequired,
|
||||||
revision: PropTypes.string,
|
revision: PropTypes.string,
|
||||||
currentRepo: PropTypes.shape({}),
|
currentRepo: PropTypes.shape({}),
|
||||||
|
router: PropTypes.shape({}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
PushList.defaultProps = {
|
PushList.defaultProps = {
|
||||||
|
@ -240,6 +247,7 @@ const mapStateToProps = ({
|
||||||
allUnclassifiedFailureCount,
|
allUnclassifiedFailureCount,
|
||||||
},
|
},
|
||||||
pinnedJobs: { pinnedJobs },
|
pinnedJobs: { pinnedJobs },
|
||||||
|
router,
|
||||||
}) => ({
|
}) => ({
|
||||||
loadingPushes,
|
loadingPushes,
|
||||||
jobsLoaded,
|
jobsLoaded,
|
||||||
|
@ -247,13 +255,13 @@ const mapStateToProps = ({
|
||||||
pushList,
|
pushList,
|
||||||
allUnclassifiedFailureCount,
|
allUnclassifiedFailureCount,
|
||||||
pinnedJobs,
|
pinnedJobs,
|
||||||
|
router,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, {
|
export default connect(mapStateToProps, {
|
||||||
notify,
|
notify,
|
||||||
clearSelectedJob,
|
clearSelectedJob,
|
||||||
setSelectedJobFromQueryString,
|
setSelectedJobFromQueryString,
|
||||||
fetchNextPushes,
|
|
||||||
fetchPushes,
|
fetchPushes,
|
||||||
updateRange,
|
updateRange,
|
||||||
pollPushes,
|
pollPushes,
|
||||||
|
|
|
@ -1,22 +1,32 @@
|
||||||
import { createStore, combineReducers, applyMiddleware } from 'redux';
|
import { createStore, combineReducers, applyMiddleware } from 'redux';
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
import createDebounce from 'redux-debounce';
|
import createDebounce from 'redux-debounce';
|
||||||
|
import { connectRouter, routerMiddleware } from 'connected-react-router';
|
||||||
|
import { createBrowserHistory } from 'history';
|
||||||
|
|
||||||
import * as selectedJobStore from './stores/selectedJob';
|
import * as selectedJobStore from './stores/selectedJob';
|
||||||
import * as notificationStore from './stores/notifications';
|
import * as notificationStore from './stores/notifications';
|
||||||
import * as pushesStore from './stores/pushes';
|
import * as pushesStore from './stores/pushes';
|
||||||
import * as pinnedJobsStore from './stores/pinnedJobs';
|
import * as pinnedJobsStore from './stores/pinnedJobs';
|
||||||
|
|
||||||
export default () => {
|
const debouncer = createDebounce({ nextJob: 200 });
|
||||||
const debounceConfig = { nextJob: 200 };
|
|
||||||
const debouncer = createDebounce(debounceConfig);
|
const reducers = (routerHistory) =>
|
||||||
const reducers = combineReducers({
|
combineReducers({
|
||||||
|
router: connectRouter(routerHistory),
|
||||||
notifications: notificationStore.reducer,
|
notifications: notificationStore.reducer,
|
||||||
selectedJob: selectedJobStore.reducer,
|
selectedJob: selectedJobStore.reducer,
|
||||||
pushes: pushesStore.reducer,
|
pushes: pushesStore.reducer,
|
||||||
pinnedJobs: pinnedJobsStore.reducer,
|
pinnedJobs: pinnedJobsStore.reducer,
|
||||||
});
|
});
|
||||||
const store = createStore(reducers, applyMiddleware(thunk, debouncer));
|
|
||||||
|
|
||||||
return { store };
|
export const history = createBrowserHistory();
|
||||||
};
|
|
||||||
|
export function configureStore(routerHistory = history) {
|
||||||
|
const store = createStore(
|
||||||
|
reducers(routerHistory),
|
||||||
|
applyMiddleware(routerMiddleware(routerHistory), thunk, debouncer),
|
||||||
|
);
|
||||||
|
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
import configureStore from './configureStore';
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
|
||||||
export const { store } = configureStore();
|
|
|
@ -34,6 +34,10 @@ export const notify = (message, severity, options) => ({
|
||||||
options,
|
options,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const clearExpiredNotifications = () => ({
|
||||||
|
type: CLEAR_EXPIRED_TRANSIENTS,
|
||||||
|
});
|
||||||
|
|
||||||
// *** Implementation ***
|
// *** Implementation ***
|
||||||
const doNotify = (
|
const doNotify = (
|
||||||
{ notifications, storedNotifications },
|
{ notifications, storedNotifications },
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
import pick from 'lodash/pick';
|
import pick from 'lodash/pick';
|
||||||
import keyBy from 'lodash/keyBy';
|
import keyBy from 'lodash/keyBy';
|
||||||
import max from 'lodash/max';
|
import max from 'lodash/max';
|
||||||
|
import { push as pushRoute } from 'connected-react-router';
|
||||||
|
|
||||||
import { parseQueryParams, bugzillaBugsApi } from '../../../helpers/url';
|
import { parseQueryParams, bugzillaBugsApi } from '../../../helpers/url';
|
||||||
import {
|
import { getUrlParam, replaceLocation } from '../../../helpers/location';
|
||||||
getAllUrlParams,
|
|
||||||
getQueryString,
|
|
||||||
getUrlParam,
|
|
||||||
replaceLocation,
|
|
||||||
} from '../../../helpers/location';
|
|
||||||
import PushModel from '../../../models/push';
|
import PushModel from '../../../models/push';
|
||||||
import { getTaskRunStr, isUnclassifiedFailure } from '../../../helpers/job';
|
import { getTaskRunStr, isUnclassifiedFailure } from '../../../helpers/job';
|
||||||
import FilterModel from '../../../models/filter';
|
import FilterModel from '../../../models/filter';
|
||||||
|
@ -96,8 +92,8 @@ const getLastModifiedJobTime = (jobMap) => {
|
||||||
* gives us the difference in unclassified failures and, of those jobs, the
|
* gives us the difference in unclassified failures and, of those jobs, the
|
||||||
* ones that have been filtered out
|
* ones that have been filtered out
|
||||||
*/
|
*/
|
||||||
const doRecalculateUnclassifiedCounts = (jobMap) => {
|
const doRecalculateUnclassifiedCounts = (jobMap, router) => {
|
||||||
const filterModel = new FilterModel();
|
const filterModel = new FilterModel({ pushRoute, router });
|
||||||
const tiers = filterModel.urlParams.tier;
|
const tiers = filterModel.urlParams.tier;
|
||||||
let allUnclassifiedFailureCount = 0;
|
let allUnclassifiedFailureCount = 0;
|
||||||
let filteredUnclassifiedFailureCount = 0;
|
let filteredUnclassifiedFailureCount = 0;
|
||||||
|
@ -122,6 +118,7 @@ const addPushes = (
|
||||||
jobMap,
|
jobMap,
|
||||||
setFromchange,
|
setFromchange,
|
||||||
dispatch,
|
dispatch,
|
||||||
|
router,
|
||||||
oldBugSummaryMap,
|
oldBugSummaryMap,
|
||||||
) => {
|
) => {
|
||||||
if (data.results.length > 0) {
|
if (data.results.length > 0) {
|
||||||
|
@ -140,7 +137,7 @@ const addPushes = (
|
||||||
const newStuff = {
|
const newStuff = {
|
||||||
pushList: newPushList,
|
pushList: newPushList,
|
||||||
oldestPushTimestamp,
|
oldestPushTimestamp,
|
||||||
...doRecalculateUnclassifiedCounts(jobMap),
|
...doRecalculateUnclassifiedCounts(jobMap, router),
|
||||||
...getRevisionTips(newPushList),
|
...getRevisionTips(newPushList),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -150,11 +147,11 @@ const addPushes = (
|
||||||
const updatedLastRevision = newPushList[newPushList.length - 1].revision;
|
const updatedLastRevision = newPushList[newPushList.length - 1].revision;
|
||||||
|
|
||||||
if (setFromchange && getUrlParam('fromchange') !== updatedLastRevision) {
|
if (setFromchange && getUrlParam('fromchange') !== updatedLastRevision) {
|
||||||
const params = getAllUrlParams();
|
const params = new URLSearchParams(window.location.search);
|
||||||
params.set('fromchange', updatedLastRevision);
|
params.set('fromchange', updatedLastRevision);
|
||||||
replaceLocation(params);
|
replaceLocation(params);
|
||||||
// We are silently updating the url params, but we still want to
|
// We are silently updating the url params so we don't trigger an unnecessary update
|
||||||
// update the ActiveFilters bar to this new change.
|
// in componentDidUpdate, but we still want to update the ActiveFilters bar to this new change.
|
||||||
window.dispatchEvent(new CustomEvent(thEvents.filtersUpdated));
|
window.dispatchEvent(new CustomEvent(thEvents.filtersUpdated));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,13 +249,17 @@ export const fetchPushes = (
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const {
|
const {
|
||||||
pushes: { pushList, jobMap, oldestPushTimestamp },
|
pushes: { pushList, jobMap, oldestPushTimestamp },
|
||||||
|
router,
|
||||||
} = getState();
|
} = getState();
|
||||||
|
|
||||||
dispatch({ type: LOADING });
|
dispatch({ type: LOADING });
|
||||||
|
|
||||||
|
if (getUrlParam('selectedJob') || getUrlParam('selectedTaskRun')) {
|
||||||
|
dispatch(clearSelectedJob(0));
|
||||||
|
}
|
||||||
// Only pass supported query string params to this endpoint.
|
// Only pass supported query string params to this endpoint.
|
||||||
const options = {
|
const options = {
|
||||||
...pick(parseQueryParams(getQueryString()), PUSH_FETCH_KEYS),
|
...pick(parseQueryParams(window.location.search), PUSH_FETCH_KEYS),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (oldestPushTimestamp) {
|
if (oldestPushTimestamp) {
|
||||||
|
@ -284,6 +285,7 @@ export const fetchPushes = (
|
||||||
jobMap,
|
jobMap,
|
||||||
setFromchange,
|
setFromchange,
|
||||||
dispatch,
|
dispatch,
|
||||||
|
router,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -296,10 +298,11 @@ export const pollPushes = () => {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const {
|
const {
|
||||||
pushes: { pushList, jobMap },
|
pushes: { pushList, jobMap },
|
||||||
|
router,
|
||||||
} = getState();
|
} = getState();
|
||||||
// these params will be passed in each time we poll to remain
|
// these params will be passed in each time we poll to remain
|
||||||
// within the constraints of the URL params
|
// within the constraints of the URL params
|
||||||
const locationSearch = parseQueryParams(getQueryString());
|
const locationSearch = parseQueryParams(window.location.search);
|
||||||
const pushPollingParams = PUSH_POLLING_KEYS.reduce(
|
const pushPollingParams = PUSH_POLLING_KEYS.reduce(
|
||||||
(acc, prop) =>
|
(acc, prop) =>
|
||||||
locationSearch[prop] ? { ...acc, [prop]: locationSearch[prop] } : acc,
|
locationSearch[prop] ? { ...acc, [prop]: locationSearch[prop] } : acc,
|
||||||
|
@ -330,6 +333,8 @@ export const pollPushes = () => {
|
||||||
pushList,
|
pushList,
|
||||||
jobMap,
|
jobMap,
|
||||||
false,
|
false,
|
||||||
|
dispatch,
|
||||||
|
router,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
dispatch(fetchNewJobs());
|
dispatch(fetchNewJobs());
|
||||||
|
@ -342,47 +347,29 @@ export const pollPushes = () => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the next batch of pushes based on our current offset.
|
|
||||||
*/
|
|
||||||
export const fetchNextPushes = (count) => {
|
|
||||||
const params = getAllUrlParams();
|
|
||||||
|
|
||||||
if (params.has('revision')) {
|
|
||||||
// We are viewing a single revision, but the user has asked for more.
|
|
||||||
// So we must replace the ``revision`` param with ``tochange``, which
|
|
||||||
// will make it just the top of the range. We will also then get a new
|
|
||||||
// ``fromchange`` param after the fetch.
|
|
||||||
const revision = params.get('revision');
|
|
||||||
params.delete('revision');
|
|
||||||
params.set('tochange', revision);
|
|
||||||
} else if (params.has('startdate')) {
|
|
||||||
// We are fetching more pushes, so we don't want to limit ourselves by
|
|
||||||
// ``startdate``. And after the fetch, ``startdate`` will be invalid,
|
|
||||||
// and will be replaced on the location bar by ``fromchange``.
|
|
||||||
params.delete('startdate');
|
|
||||||
}
|
|
||||||
replaceLocation(params);
|
|
||||||
return fetchPushes(count, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const clearPushes = () => ({ type: CLEAR_PUSHES });
|
export const clearPushes = () => ({ type: CLEAR_PUSHES });
|
||||||
|
|
||||||
export const setPushes = (pushList, jobMap) => ({
|
export const setPushes = (pushList, jobMap, router) => ({
|
||||||
type: SET_PUSHES,
|
type: SET_PUSHES,
|
||||||
pushResults: {
|
pushResults: {
|
||||||
pushList,
|
pushList,
|
||||||
jobMap,
|
jobMap,
|
||||||
...getRevisionTips(pushList),
|
...getRevisionTips(pushList),
|
||||||
...doRecalculateUnclassifiedCounts(jobMap),
|
...doRecalculateUnclassifiedCounts(jobMap, router),
|
||||||
oldestPushTimestamp: pushList[pushList.length - 1].push_timestamp,
|
oldestPushTimestamp: pushList[pushList.length - 1].push_timestamp,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const recalculateUnclassifiedCounts = (filterModel) => ({
|
export const recalculateUnclassifiedCounts = (filterModel) => {
|
||||||
type: RECALCULATE_UNCLASSIFIED_COUNTS,
|
return (dispatch, getState) => {
|
||||||
filterModel,
|
const { router } = getState();
|
||||||
});
|
return dispatch({
|
||||||
|
type: RECALCULATE_UNCLASSIFIED_COUNTS,
|
||||||
|
filterModel,
|
||||||
|
router,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const updateJobMap = (jobList) => ({
|
export const updateJobMap = (jobList) => ({
|
||||||
type: UPDATE_JOB_MAP,
|
type: UPDATE_JOB_MAP,
|
||||||
|
@ -393,6 +380,7 @@ export const updateRange = (range) => {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const {
|
const {
|
||||||
pushes: { pushList, jobMap },
|
pushes: { pushList, jobMap },
|
||||||
|
router,
|
||||||
} = getState();
|
} = getState();
|
||||||
const { revision } = range;
|
const { revision } = range;
|
||||||
// change the range of pushes. might already have them.
|
// change the range of pushes. might already have them.
|
||||||
|
@ -408,10 +396,12 @@ export const updateRange = (range) => {
|
||||||
job.push_id === pushId ? { ...acc, [id]: job } : acc,
|
job.push_id === pushId ? { ...acc, [id]: job } : acc,
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
dispatch(clearSelectedJob(0));
|
if (getUrlParam('selectedJob') || getUrlParam('selectedTaskRun')) {
|
||||||
|
dispatch(clearSelectedJob(0));
|
||||||
|
}
|
||||||
// We already have the one revision they're looking for,
|
// We already have the one revision they're looking for,
|
||||||
// so we can just erase everything else.
|
// so we can just erase everything else.
|
||||||
dispatch(setPushes(revisionPushList, revisionJobMap));
|
dispatch(setPushes(revisionPushList, revisionJobMap, router));
|
||||||
} else {
|
} else {
|
||||||
// Clear and refetch everything. We can't be sure if what we
|
// Clear and refetch everything. We can't be sure if what we
|
||||||
// already have is partially correct and just needs fill-in.
|
// already have is partially correct and just needs fill-in.
|
||||||
|
@ -435,8 +425,9 @@ export const initialState = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const reducer = (state = initialState, action) => {
|
export const reducer = (state = initialState, action) => {
|
||||||
const { jobList, pushResults, setFromchange } = action;
|
const { jobList, pushResults, setFromchange, router } = action;
|
||||||
const { pushList, jobMap, decisionTaskMap } = state;
|
const { pushList, jobMap, decisionTaskMap } = state;
|
||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case LOADING:
|
case LOADING:
|
||||||
return { ...state, loadingPushes: true };
|
return { ...state, loadingPushes: true };
|
||||||
|
@ -449,7 +440,7 @@ export const reducer = (state = initialState, action) => {
|
||||||
case SET_PUSHES:
|
case SET_PUSHES:
|
||||||
return { ...state, loadingPushes: false, ...pushResults };
|
return { ...state, loadingPushes: false, ...pushResults };
|
||||||
case RECALCULATE_UNCLASSIFIED_COUNTS:
|
case RECALCULATE_UNCLASSIFIED_COUNTS:
|
||||||
return { ...state, ...doRecalculateUnclassifiedCounts(jobMap) };
|
return { ...state, ...doRecalculateUnclassifiedCounts(jobMap, router) };
|
||||||
case UPDATE_JOB_MAP:
|
case UPDATE_JOB_MAP:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { push as pushRoute } from 'connected-react-router';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
findGroupElement,
|
findGroupElement,
|
||||||
findGroupInstance,
|
findGroupInstance,
|
||||||
|
@ -8,7 +10,11 @@ import {
|
||||||
scrollToElement,
|
scrollToElement,
|
||||||
} from '../../../helpers/job';
|
} from '../../../helpers/job';
|
||||||
import { thJobNavSelectors } from '../../../helpers/constants';
|
import { thJobNavSelectors } from '../../../helpers/constants';
|
||||||
import { getUrlParam, setUrlParam } from '../../../helpers/location';
|
import {
|
||||||
|
getUrlParam,
|
||||||
|
setUrlParam,
|
||||||
|
setUrlParams,
|
||||||
|
} from '../../../helpers/location';
|
||||||
import JobModel from '../../../models/job';
|
import JobModel from '../../../models/job';
|
||||||
import { getJobsUrl } from '../../../helpers/url';
|
import { getJobsUrl } from '../../../helpers/url';
|
||||||
|
|
||||||
|
@ -17,11 +23,20 @@ export const SELECT_JOB_FROM_QUERY_STRING = 'SELECT_JOB_FROM_QUERY_STRING';
|
||||||
export const CLEAR_JOB = 'CLEAR_JOB';
|
export const CLEAR_JOB = 'CLEAR_JOB';
|
||||||
export const UPDATE_JOB_DETAILS = 'UPDATE_JOB_DETAILS';
|
export const UPDATE_JOB_DETAILS = 'UPDATE_JOB_DETAILS';
|
||||||
|
|
||||||
export const setSelectedJob = (job, updateDetails = true) => ({
|
export const setSelectedJob = (job, updateDetails = true) => {
|
||||||
type: SELECT_JOB,
|
return async (dispatch) => {
|
||||||
job,
|
dispatch({
|
||||||
updateDetails,
|
type: SELECT_JOB,
|
||||||
});
|
job,
|
||||||
|
updateDetails,
|
||||||
|
});
|
||||||
|
if (updateDetails) {
|
||||||
|
const taskRun = job ? getTaskRunStr(job) : null;
|
||||||
|
const params = setUrlParams([['selectedTaskRun', taskRun]]);
|
||||||
|
dispatch(pushRoute({ search: params }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const setSelectedJobFromQueryString = (notify, jobMap) => ({
|
export const setSelectedJobFromQueryString = (notify, jobMap) => ({
|
||||||
type: SELECT_JOB_FROM_QUERY_STRING,
|
type: SELECT_JOB_FROM_QUERY_STRING,
|
||||||
|
@ -29,27 +44,36 @@ export const setSelectedJobFromQueryString = (notify, jobMap) => ({
|
||||||
jobMap,
|
jobMap,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const clearSelectedJob = (countPinnedJobs) => ({
|
export const clearSelectedJob = (countPinnedJobs) => {
|
||||||
type: CLEAR_JOB,
|
return async (dispatch) => {
|
||||||
countPinnedJobs,
|
dispatch({
|
||||||
});
|
type: CLEAR_JOB,
|
||||||
|
countPinnedJobs,
|
||||||
export const updateJobDetails = (job) => ({
|
});
|
||||||
type: UPDATE_JOB_DETAILS,
|
const params = setUrlParams([
|
||||||
job,
|
['selectedTaskRun', null],
|
||||||
meta: {
|
['selectedJob', null],
|
||||||
debounce: 'nextJob',
|
]);
|
||||||
},
|
dispatch(pushRoute({ search: params }));
|
||||||
});
|
};
|
||||||
|
|
||||||
const doUpdateJobDetails = (job) => {
|
|
||||||
const taskRun = job ? getTaskRunStr(job) : null;
|
|
||||||
|
|
||||||
setUrlParam('selectedTaskRun', taskRun);
|
|
||||||
return { selectedJob: job };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const doSelectJob = (job, updateDetails) => {
|
export const updateJobDetails = (job) => {
|
||||||
|
return async (dispatch) => {
|
||||||
|
dispatch({
|
||||||
|
type: UPDATE_JOB_DETAILS,
|
||||||
|
job,
|
||||||
|
meta: {
|
||||||
|
debounce: 'nextJob',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const taskRun = job ? getTaskRunStr(job) : null;
|
||||||
|
const params = setUrlParams([['selectedTaskRun', taskRun]]);
|
||||||
|
dispatch(pushRoute({ search: params }));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const doSelectJob = (job) => {
|
||||||
const selected = findSelectedInstance();
|
const selected = findSelectedInstance();
|
||||||
|
|
||||||
if (selected) selected.setSelected(false);
|
if (selected) selected.setSelected(false);
|
||||||
|
@ -71,9 +95,7 @@ export const doSelectJob = (job, updateDetails) => {
|
||||||
scrollToElement(groupEl);
|
scrollToElement(groupEl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (updateDetails) {
|
|
||||||
return doUpdateJobDetails(job);
|
|
||||||
}
|
|
||||||
return { selectedJob: job };
|
return { selectedJob: job };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -81,8 +103,7 @@ export const doClearSelectedJob = (countPinnedJobs) => {
|
||||||
if (!countPinnedJobs) {
|
if (!countPinnedJobs) {
|
||||||
const selected = findSelectedInstance();
|
const selected = findSelectedInstance();
|
||||||
if (selected) selected.setSelected(false);
|
if (selected) selected.setSelected(false);
|
||||||
setUrlParam('selectedTaskRun', null);
|
|
||||||
setUrlParam('selectedJob', null);
|
|
||||||
return { selectedJob: null };
|
return { selectedJob: null };
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
|
@ -240,7 +261,7 @@ export const changeJob = (
|
||||||
if (jobInstance) {
|
if (jobInstance) {
|
||||||
// Delay updating details for the new job right away,
|
// Delay updating details for the new job right away,
|
||||||
// in case the user is switching rapidly between jobs
|
// in case the user is switching rapidly between jobs
|
||||||
return doSelectJob(jobInstance.props.job, false);
|
return doSelectJob(jobInstance.props.job);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -255,15 +276,15 @@ export const initialState = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const reducer = (state = initialState, action) => {
|
export const reducer = (state = initialState, action) => {
|
||||||
const { job, jobMap, countPinnedJobs, updateDetails, notify } = action;
|
const { job, jobMap, countPinnedJobs, notify } = action;
|
||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case SELECT_JOB:
|
case SELECT_JOB:
|
||||||
return doSelectJob(job, updateDetails);
|
return doSelectJob(job);
|
||||||
case SELECT_JOB_FROM_QUERY_STRING:
|
case SELECT_JOB_FROM_QUERY_STRING:
|
||||||
return doSetSelectedJobFromQueryString(notify, jobMap);
|
return doSetSelectedJobFromQueryString(notify, jobMap);
|
||||||
case UPDATE_JOB_DETAILS:
|
case UPDATE_JOB_DETAILS:
|
||||||
return doUpdateJobDetails(job);
|
return { selectedJob: job };
|
||||||
case CLEAR_JOB:
|
case CLEAR_JOB:
|
||||||
return { ...state, ...doClearSelectedJob(countPinnedJobs) };
|
return { ...state, ...doClearSelectedJob(countPinnedJobs) };
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { render } from 'react-dom';
|
|
||||||
|
|
||||||
import LoginCallback from './LoginCallback';
|
|
||||||
|
|
||||||
render(<LoginCallback />, document.getElementById('root'));
|
|
|
@ -29,6 +29,9 @@ import { formatArtifacts, errorLinesCss } from '../helpers/display';
|
||||||
import Navigation from './Navigation';
|
import Navigation from './Navigation';
|
||||||
import ErrorLines from './ErrorLines';
|
import ErrorLines from './ErrorLines';
|
||||||
|
|
||||||
|
import '../css/lazylog-custom-styles.css';
|
||||||
|
import './logviewer.css';
|
||||||
|
|
||||||
const JOB_DETAILS_COLLAPSED = 'jobDetailsCollapsed';
|
const JOB_DETAILS_COLLAPSED = 'jobDetailsCollapsed';
|
||||||
|
|
||||||
const getUrlLineNumber = function getUrlLineNumber() {
|
const getUrlLineNumber = function getUrlLineNumber() {
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { render } from 'react-dom';
|
|
||||||
|
|
||||||
// Treeherder Styles
|
|
||||||
import '../css/treeherder-base.css';
|
|
||||||
import '../css/treeherder-custom-styles.css';
|
|
||||||
import '../css/treeherder-navbar.css';
|
|
||||||
import '../css/lazylog-custom-styles.css';
|
|
||||||
import './logviewer.css';
|
|
||||||
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
render(<App />, document.getElementById('root'));
|
|
|
@ -17,8 +17,8 @@ import {
|
||||||
} from '../helpers/filter';
|
} from '../helpers/filter';
|
||||||
import { getAllUrlParams } from '../helpers/location';
|
import { getAllUrlParams } from '../helpers/location';
|
||||||
|
|
||||||
export const getNonFilterUrlParams = () =>
|
export const getNonFilterUrlParams = (location) =>
|
||||||
[...getAllUrlParams().entries()].reduce(
|
[...getAllUrlParams(location).entries()].reduce(
|
||||||
(acc, [urlField, urlValue]) =>
|
(acc, [urlField, urlValue]) =>
|
||||||
allFilterParams.includes(urlField.replace(deprecatedThFilterPrefix, ''))
|
allFilterParams.includes(urlField.replace(deprecatedThFilterPrefix, ''))
|
||||||
? acc
|
? acc
|
||||||
|
@ -26,12 +26,12 @@ export const getNonFilterUrlParams = () =>
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getFilterUrlParamsWithDefaults = () => {
|
export const getFilterUrlParamsWithDefaults = (location) => {
|
||||||
// Group multiple values for the same field into an array of values.
|
// Group multiple values for the same field into an array of values.
|
||||||
// This handles the transition from our old url params to this newer, more
|
// This handles the transition from our old url params to this newer, more
|
||||||
// terse version.
|
// terse version.
|
||||||
// Also remove usage of the 'filter-' prefix.
|
// Also remove usage of the 'filter-' prefix.
|
||||||
const groupedValues = [...getAllUrlParams().entries()].reduce(
|
const groupedValues = [...getAllUrlParams(location).entries()].reduce(
|
||||||
(acc, [urlField, urlValue]) => {
|
(acc, [urlField, urlValue]) => {
|
||||||
const field = urlField.replace(deprecatedThFilterPrefix, '');
|
const field = urlField.replace(deprecatedThFilterPrefix, '');
|
||||||
if (!allFilterParams.includes(field)) {
|
if (!allFilterParams.includes(field)) {
|
||||||
|
@ -51,8 +51,11 @@ export const getFilterUrlParamsWithDefaults = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class FilterModel {
|
export default class FilterModel {
|
||||||
constructor() {
|
constructor(props) {
|
||||||
this.urlParams = getFilterUrlParamsWithDefaults();
|
// utilize connected-react-router push prop (this.push is equivalent to history.push)
|
||||||
|
this.push = props.pushRoute;
|
||||||
|
this.location = props.router.location;
|
||||||
|
this.urlParams = getFilterUrlParamsWithDefaults(props.router.location);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a param matches the defaults, then don't include it.
|
// If a param matches the defaults, then don't include it.
|
||||||
|
@ -60,7 +63,7 @@ export default class FilterModel {
|
||||||
// ensure the repo param is always set
|
// ensure the repo param is always set
|
||||||
const params = {
|
const params = {
|
||||||
repo: thDefaultRepo,
|
repo: thDefaultRepo,
|
||||||
...getNonFilterUrlParams(),
|
...getNonFilterUrlParams(this.location),
|
||||||
...this.urlParams,
|
...this.urlParams,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -86,7 +89,7 @@ export default class FilterModel {
|
||||||
} else {
|
} else {
|
||||||
this.urlParams[field] = [value];
|
this.urlParams[field] = [value];
|
||||||
}
|
}
|
||||||
this.push();
|
this.push({ search: this.getFilterQueryString() });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Also used for non-filter params
|
// Also used for non-filter params
|
||||||
|
@ -99,29 +102,24 @@ export default class FilterModel {
|
||||||
(filterValue) => filterValue !== value,
|
(filterValue) => filterValue !== value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.urlParams[field].length) {
|
||||||
|
delete this.urlParams[field];
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
delete this.urlParams[field];
|
delete this.urlParams[field];
|
||||||
}
|
}
|
||||||
this.push();
|
|
||||||
|
this.push({ search: this.getFilterQueryString() });
|
||||||
};
|
};
|
||||||
|
|
||||||
getFilterQueryString = () =>
|
getFilterQueryString = () =>
|
||||||
new URLSearchParams(this.getUrlParamsWithoutDefaults()).toString();
|
new URLSearchParams(this.getUrlParamsWithoutDefaults()).toString();
|
||||||
|
|
||||||
/**
|
|
||||||
* Push all the url params to the url. Components listening for hashchange
|
|
||||||
* will get updates.
|
|
||||||
*/
|
|
||||||
push = () => {
|
|
||||||
const { origin } = window.location;
|
|
||||||
|
|
||||||
window.location.href = `${origin}/#/jobs?${this.getFilterQueryString()}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
setOnlySuperseded = () => {
|
setOnlySuperseded = () => {
|
||||||
this.urlParams.resultStatus = 'superseded';
|
this.urlParams.resultStatus = 'superseded';
|
||||||
this.urlParams.classifiedState = [...thFilterDefaults.classifiedState];
|
this.urlParams.classifiedState = [...thFilterDefaults.classifiedState];
|
||||||
this.push();
|
this.push({ search: this.getFilterQueryString() });
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleFilter = (field, value) => {
|
toggleFilter = (field, value) => {
|
||||||
|
@ -148,7 +146,7 @@ export default class FilterModel {
|
||||||
? currentResultStatuses.filter((rs) => !resultStatuses.includes(rs))
|
? currentResultStatuses.filter((rs) => !resultStatuses.includes(rs))
|
||||||
: [...new Set([...resultStatuses, ...currentResultStatuses])];
|
: [...new Set([...resultStatuses, ...currentResultStatuses])];
|
||||||
|
|
||||||
this.push();
|
this.push({ search: this.getFilterQueryString() });
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleClassifiedFilter = (classifiedState) => {
|
toggleClassifiedFilter = (classifiedState) => {
|
||||||
|
@ -161,20 +159,20 @@ export default class FilterModel {
|
||||||
} else {
|
} else {
|
||||||
this.urlParams.resultStatus = [...thFailureResults];
|
this.urlParams.resultStatus = [...thFailureResults];
|
||||||
this.urlParams.classifiedState = ['unclassified'];
|
this.urlParams.classifiedState = ['unclassified'];
|
||||||
this.push();
|
this.push({ search: this.getFilterQueryString() });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
replaceFilter = (field, value) => {
|
replaceFilter = (field, value) => {
|
||||||
this.urlParams[field] = !Array.isArray(value) ? [value] : value;
|
this.urlParams[field] = !Array.isArray(value) ? [value] : value;
|
||||||
this.push();
|
this.push({ search: this.getFilterQueryString() });
|
||||||
};
|
};
|
||||||
|
|
||||||
clearNonStatusFilters = () => {
|
clearNonStatusFilters = () => {
|
||||||
const { repo, resultStatus, classifiedState } = this.urlParams;
|
const { repo, resultStatus, classifiedState } = this.urlParams;
|
||||||
|
|
||||||
this.urlParams = { repo, resultStatus, classifiedState };
|
this.urlParams = { repo, resultStatus, classifiedState };
|
||||||
this.push();
|
this.push({ search: this.getFilterQueryString() });
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -187,7 +185,7 @@ export default class FilterModel {
|
||||||
|
|
||||||
this.urlParams.resultStatus = [...resultStatus];
|
this.urlParams.resultStatus = [...resultStatus];
|
||||||
this.urlParams.classifiedState = [...classifiedState];
|
this.urlParams.classifiedState = [...classifiedState];
|
||||||
this.push();
|
this.push({ search: this.getFilterQueryString() });
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -54,6 +54,7 @@ export default class PushModel {
|
||||||
// fetch the maximum number of pushes
|
// fetch the maximum number of pushes
|
||||||
params.count = thMaxPushFetchSize;
|
params.count = thMaxPushFetchSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getData(
|
return getData(
|
||||||
`${getProjectUrl(pushEndpoint, repoName)}${createQueryParams(params)}`,
|
`${getProjectUrl(pushEndpoint, repoName)}${createQueryParams(params)}`,
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { HashRouter, Route, Switch, Redirect } from 'react-router-dom';
|
import { Route, Switch, Redirect } from 'react-router-dom';
|
||||||
import { hot } from 'react-hot-loader/root';
|
import { hot } from 'react-hot-loader/root';
|
||||||
import { Container } from 'reactstrap';
|
import { Container } from 'reactstrap';
|
||||||
|
|
||||||
|
@ -18,6 +18,9 @@ import CompareSubtestsView from './compare/CompareSubtestsView';
|
||||||
import CompareSubtestDistributionView from './compare/CompareSubtestDistributionView';
|
import CompareSubtestDistributionView from './compare/CompareSubtestDistributionView';
|
||||||
import Navigation from './Navigation';
|
import Navigation from './Navigation';
|
||||||
|
|
||||||
|
import 'react-table/react-table.css';
|
||||||
|
import '../css/perf.css';
|
||||||
|
|
||||||
class App extends React.Component {
|
class App extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -66,9 +69,10 @@ class App extends React.Component {
|
||||||
errorMessages,
|
errorMessages,
|
||||||
compareData,
|
compareData,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
const { path } = this.props.match;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HashRouter>
|
<React.Fragment>
|
||||||
<Navigation
|
<Navigation
|
||||||
user={user}
|
user={user}
|
||||||
setUser={(user) => this.setState({ user })}
|
setUser={(user) => this.setState({ user })}
|
||||||
|
@ -86,7 +90,7 @@ class App extends React.Component {
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path="/alerts"
|
path={`${path}/alerts`}
|
||||||
render={(props) => (
|
render={(props) => (
|
||||||
<AlertsView
|
<AlertsView
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -98,7 +102,7 @@ class App extends React.Component {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/alerts?id=:id&status=:status&framework=:framework&filter=:filter&hideImprovements=:hideImprovements&hideDwnToInv=:hideDwnToInv&hideAssignedToOthers=:hideAssignedToOthers&filterText=:filterText&page=:page"
|
path={`${path}/alerts?id=:id&status=:status&framework=:framework&filter=:filter&hideImprovements=:hideImprovements&hideDwnToInv=:hideDwnToInv&hideAssignedToOthers=:hideAssignedToOthers&filterText=:filterText&page=:page`}
|
||||||
render={(props) => (
|
render={(props) => (
|
||||||
<AlertsView
|
<AlertsView
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -110,7 +114,7 @@ class App extends React.Component {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/graphs"
|
path={`${path}/graphs`}
|
||||||
render={(props) => (
|
render={(props) => (
|
||||||
<GraphsView
|
<GraphsView
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -121,7 +125,7 @@ class App extends React.Component {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/graphs?timerange=:timerange&series=:series&highlightedRevisions=:highlightedRevisions&highlightAlerts=:highlightAlerts&zoom=:zoom&selected=:selected"
|
path={`${path}/graphs?timerange=:timerange&series=:series&highlightedRevisions=:highlightedRevisions&highlightAlerts=:highlightAlerts&zoom=:zoom&selected=:selected`}
|
||||||
render={(props) => (
|
render={(props) => (
|
||||||
<GraphsView
|
<GraphsView
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -132,7 +136,7 @@ class App extends React.Component {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/comparechooser"
|
path={`${path}/comparechooser`}
|
||||||
render={(props) => (
|
render={(props) => (
|
||||||
<CompareSelectorView
|
<CompareSelectorView
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -143,7 +147,7 @@ class App extends React.Component {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/comparechooser?originalProject=:originalProject&originalRevision=:originalRevision&newProject=:newProject&newRevision=:newRevision"
|
path={`${path}/comparechooser?originalProject=:originalProject&originalRevision=:originalRevision&newProject=:newProject&newRevision=:newRevision`}
|
||||||
render={(props) => (
|
render={(props) => (
|
||||||
<CompareSelectorView
|
<CompareSelectorView
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -154,7 +158,7 @@ class App extends React.Component {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/compare"
|
path={`${path}/compare`}
|
||||||
render={(props) => (
|
render={(props) => (
|
||||||
<CompareView
|
<CompareView
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -167,7 +171,7 @@ class App extends React.Component {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/compare?originalProject=:originalProject&originalRevision=:originalRevison&newProject=:newProject&newRevision=:newRevision&framework=:framework&showOnlyComparable=:showOnlyComparable&showOnlyImportant=:showOnlyImportant&showOnlyConfident=:showOnlyConfident&selectedTimeRange=:selectedTimeRange&showOnlyNoise=:showOnlyNoise"
|
path={`${path}/compare?originalProject=:originalProject&originalRevision=:originalRevison&newProject=:newProject&newRevision=:newRevision&framework=:framework&showOnlyComparable=:showOnlyComparable&showOnlyImportant=:showOnlyImportant&showOnlyConfident=:showOnlyConfident&selectedTimeRange=:selectedTimeRange&showOnlyNoise=:showOnlyNoise`}
|
||||||
render={(props) => (
|
render={(props) => (
|
||||||
<CompareView
|
<CompareView
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -180,7 +184,7 @@ class App extends React.Component {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/infracompare"
|
path={`${path}/infracompare`}
|
||||||
render={(props) => (
|
render={(props) => (
|
||||||
<InfraCompareView
|
<InfraCompareView
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -193,7 +197,7 @@ class App extends React.Component {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/comparesubtest"
|
path={`${path}/comparesubtest`}
|
||||||
render={(props) => (
|
render={(props) => (
|
||||||
<CompareSubtestsView
|
<CompareSubtestsView
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -204,7 +208,7 @@ class App extends React.Component {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/comparesubtest?originalProject=:originalProject&originalRevision=:originalRevision&newProject=:newProject&newRevision=:newRevision&originalSignature=:originalSignature&newSignature=:newSignature&framework=:framework&showOnlyComparable=:showOnlyComparable&showOnlyImportant=:showOnlyImportant&showOnlyConfident=:showOnlyConfident&selectedTimeRange=:selectedTimeRange&showOnlyNoise=:showOnlyNoise"
|
path={`${path}/comparesubtest?originalProject=:originalProject&originalRevision=:originalRevision&newProject=:newProject&newRevision=:newRevision&originalSignature=:originalSignature&newSignature=:newSignature&framework=:framework&showOnlyComparable=:showOnlyComparable&showOnlyImportant=:showOnlyImportant&showOnlyConfident=:showOnlyConfident&selectedTimeRange=:selectedTimeRange&showOnlyNoise=:showOnlyNoise`}
|
||||||
render={(props) => (
|
render={(props) => (
|
||||||
<CompareSubtestsView
|
<CompareSubtestsView
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -215,7 +219,7 @@ class App extends React.Component {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/comparesubtestdistribution"
|
path={`${path}/comparesubtestdistribution`}
|
||||||
render={(props) => (
|
render={(props) => (
|
||||||
<CompareSubtestDistributionView
|
<CompareSubtestDistributionView
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -226,7 +230,7 @@ class App extends React.Component {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/comparesubtestdistribution?originalProject=:originalProject&newProject=:newProject&originalRevision=:originalRevision&newRevision=:newRevision&originalSubtestSignature=:originalSubtestSignature&newSubtestSignature=:newSubtestSignature"
|
path={`${path}/comparesubtestdistribution?originalProject=:originalProject&newProject=:newProject&originalRevision=:originalRevision&newRevision=:newRevision&originalSubtestSignature=:originalSubtestSignature&newSubtestSignature=:newSubtestSignature`}
|
||||||
render={(props) => (
|
render={(props) => (
|
||||||
<CompareSubtestDistributionView
|
<CompareSubtestDistributionView
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -237,7 +241,7 @@ class App extends React.Component {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/tests"
|
path={`${path}/tests`}
|
||||||
render={(props) => (
|
render={(props) => (
|
||||||
<TestsView
|
<TestsView
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -249,7 +253,7 @@ class App extends React.Component {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/tests?framework=:framework"
|
path={`${path}/tests?framework=:framework"`}
|
||||||
render={(props) => (
|
render={(props) => (
|
||||||
<TestsView
|
<TestsView
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -260,11 +264,14 @@ class App extends React.Component {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Redirect from="/" to="/alerts?hideDwnToInv=1" />
|
<Redirect
|
||||||
|
from={`${path}/`}
|
||||||
|
to={`${path}/alerts?hideDwnToInv=1`}
|
||||||
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</main>
|
</main>
|
||||||
)}
|
)}
|
||||||
</HashRouter>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Navbar, Nav, NavItem, NavLink } from 'reactstrap';
|
import { Navbar, Nav, NavItem } from 'reactstrap';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import LogoMenu from '../shared/LogoMenu';
|
import LogoMenu from '../shared/LogoMenu';
|
||||||
import Login from '../shared/auth/Login';
|
import Login from '../shared/auth/Login';
|
||||||
|
@ -11,24 +12,24 @@ const Navigation = ({ user, setUser, notify }) => (
|
||||||
<LogoMenu menuText="Perfherder" colorClass="text-info" />
|
<LogoMenu menuText="Perfherder" colorClass="text-info" />
|
||||||
<Nav className="navbar navbar-inverse">
|
<Nav className="navbar navbar-inverse">
|
||||||
<NavItem>
|
<NavItem>
|
||||||
<NavLink href="#/graphs" className="btn-view-nav">
|
<Link to="./graphs" className="nav-link btn-view-nav">
|
||||||
Graphs
|
Graphs
|
||||||
</NavLink>
|
</Link>
|
||||||
</NavItem>
|
</NavItem>
|
||||||
<NavItem>
|
<NavItem>
|
||||||
<NavLink href="#/comparechooser" className="btn-view-nav">
|
<Link to="./comparechooser" className="nav-link btn-view-nav">
|
||||||
Compare
|
Compare
|
||||||
</NavLink>
|
</Link>
|
||||||
</NavItem>
|
</NavItem>
|
||||||
<NavItem>
|
<NavItem>
|
||||||
<NavLink href="#/alerts?hideDwnToInv=1" className="btn-view-nav">
|
<Link to="./alerts?hideDwnToInv=1" className="nav-link btn-view-nav">
|
||||||
Alerts
|
Alerts
|
||||||
</NavLink>
|
</Link>
|
||||||
</NavItem>
|
</NavItem>
|
||||||
<NavItem>
|
<NavItem>
|
||||||
<NavLink href="#/tests" className="btn-view-nav">
|
<Link to="./tests" className="nav-link btn-view-nav">
|
||||||
Tests
|
Tests
|
||||||
</NavLink>
|
</Link>
|
||||||
</NavItem>
|
</NavItem>
|
||||||
</Nav>
|
</Nav>
|
||||||
<Navbar className="ml-auto">
|
<Navbar className="ml-auto">
|
||||||
|
|
|
@ -2,11 +2,7 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Container } from 'reactstrap';
|
import { Container } from 'reactstrap';
|
||||||
|
|
||||||
import {
|
import { parseQueryParams, createQueryParams } from '../helpers/url';
|
||||||
parseQueryParams,
|
|
||||||
createQueryParams,
|
|
||||||
updateQueryParams,
|
|
||||||
} from '../helpers/url';
|
|
||||||
import PushModel from '../models/push';
|
import PushModel from '../models/push';
|
||||||
import ErrorMessages from '../shared/ErrorMessages';
|
import ErrorMessages from '../shared/ErrorMessages';
|
||||||
import LoadingSpinner from '../shared/LoadingSpinner';
|
import LoadingSpinner from '../shared/LoadingSpinner';
|
||||||
|
@ -37,30 +33,29 @@ const withValidation = ({ requiredParams }, verifyRevisions = true) => (
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
this.validateParams(parseQueryParams(this.props.location.search));
|
this.validateParams(parseQueryParams(this.props.history.location.search));
|
||||||
}
|
|
||||||
|
|
||||||
shouldComponentUpdate(prevProps) {
|
|
||||||
const { location } = this.props;
|
|
||||||
|
|
||||||
return location.hash === prevProps.location.hash;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const { location } = this.props;
|
const { history } = this.props;
|
||||||
|
|
||||||
if (location.search !== prevProps.location.search) {
|
// Using location instead of history requires an extra click when
|
||||||
|
// using the back button to go back to previous location
|
||||||
|
if (history.location.search !== prevProps.history.location.search) {
|
||||||
// delete from state params the ones
|
// delete from state params the ones
|
||||||
this.validateParams(parseQueryParams(location.search));
|
this.validateParams(parseQueryParams(history.location.search));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateParams = (params) => {
|
updateParams = (params) => {
|
||||||
const { location, history } = this.props;
|
const { history, location } = this.props;
|
||||||
const newParams = { ...parseQueryParams(location.search), ...params };
|
|
||||||
const queryString = createQueryParams(newParams);
|
|
||||||
|
|
||||||
updateQueryParams(queryString, history, location);
|
const newParams = {
|
||||||
|
...parseQueryParams(location.search),
|
||||||
|
...params,
|
||||||
|
};
|
||||||
|
const queryString = createQueryParams(newParams);
|
||||||
|
history.push({ search: queryString });
|
||||||
};
|
};
|
||||||
|
|
||||||
errorMessage = (param, value) => `${param} ${value} is not valid`;
|
errorMessage = (param, value) => `${param} ${value} is not valid`;
|
||||||
|
@ -204,6 +199,7 @@ const withValidation = ({ requiredParams }, verifyRevisions = true) => (
|
||||||
|
|
||||||
Validation.propTypes = {
|
Validation.propTypes = {
|
||||||
location: PropTypes.shape({}).isRequired,
|
location: PropTypes.shape({}).isRequired,
|
||||||
|
history: PropTypes.shape({}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Validation;
|
return Validation;
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons';
|
import { faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { getTitle, getFrameworkName } from '../helpers';
|
import { getTitle, getFrameworkName } from '../helpers';
|
||||||
import { getJobsUrl } from '../../helpers/url';
|
import { getJobsUrl } from '../../helpers/url';
|
||||||
|
@ -43,9 +44,9 @@ const AlertHeader = ({
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Row>
|
<Row>
|
||||||
<a
|
<Link
|
||||||
className="text-dark"
|
className="text-dark"
|
||||||
href={`#/alerts?id=${alertSummary.id}`}
|
to={`./alerts?id=${alertSummary.id}&hideDwnToInv=0`}
|
||||||
id={`alert summary ${alertSummary.id.toString()} title`}
|
id={`alert summary ${alertSummary.id.toString()} title`}
|
||||||
data-testid={`alert summary ${alertSummary.id.toString()} title`}
|
data-testid={`alert summary ${alertSummary.id.toString()} title`}
|
||||||
>
|
>
|
||||||
|
@ -60,7 +61,7 @@ const AlertHeader = ({
|
||||||
className="icon-superscript"
|
className="icon-superscript"
|
||||||
/>
|
/>
|
||||||
</h3>
|
</h3>
|
||||||
</a>
|
</Link>
|
||||||
</Row>
|
</Row>
|
||||||
<Row className="font-weight-normal">
|
<Row className="font-weight-normal">
|
||||||
<Col className="p-0" xs="auto">{`${moment(
|
<Col className="p-0" xs="auto">{`${moment(
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
faCheck,
|
faCheck,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
import { faStar as faStarRegular } from '@fortawesome/free-regular-svg-icons';
|
import { faStar as faStarRegular } from '@fortawesome/free-regular-svg-icons';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { createQueryParams } from '../../helpers/url';
|
import { createQueryParams } from '../../helpers/url';
|
||||||
import { getStatus, getGraphsURL, modifyAlert, formatNumber } from '../helpers';
|
import { getStatus, getGraphsURL, modifyAlert, formatNumber } from '../helpers';
|
||||||
|
@ -107,11 +108,10 @@ export default class AlertTableRow extends React.Component {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{` ${text} `}
|
{` ${text} `}
|
||||||
<a
|
<Link
|
||||||
href={`#/alerts?id=${alertId}`}
|
to={`./alerts?id=${alertId}`}
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-darker-info"
|
className="text-darker-info"
|
||||||
>{`alert #${alertId}`}</a>
|
>{`alert #${alertId}`}</Link>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -263,7 +263,7 @@ export default class AlertTableRow extends React.Component {
|
||||||
newRevision: alertSummary.revision,
|
newRevision: alertSummary.revision,
|
||||||
};
|
};
|
||||||
|
|
||||||
return `#/comparesubtest${createQueryParams(urlParameters)}`;
|
return `./comparesubtest${createQueryParams(urlParameters)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -345,6 +345,7 @@ export default class AlertTableRow extends React.Component {
|
||||||
textClass="detail-hint"
|
textClass="detail-hint"
|
||||||
text={`${alert.amount_pct}%`}
|
text={`${alert.amount_pct}%`}
|
||||||
tooltipText={`Absolute difference: ${alert.amount_abs}`}
|
tooltipText={`Absolute difference: ${alert.amount_abs}`}
|
||||||
|
autohide={false}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="table-width-lg">
|
<td className="table-width-lg">
|
||||||
|
|
|
@ -32,10 +32,10 @@ class AlertsView extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
const { frameworks, validated } = this.props;
|
const { frameworks, validated } = this.props;
|
||||||
const extendedOptions = this.extendDropdownOptions(frameworks);
|
this.extendedOptions = this.extendDropdownOptions(frameworks);
|
||||||
this.state = {
|
this.state = {
|
||||||
filters: this.getFiltersFromParams(validated, extendedOptions),
|
filters: this.getFiltersFromParams(validated),
|
||||||
frameworkOptions: extendedOptions,
|
frameworkOptions: this.extendedOptions,
|
||||||
page: validated.page ? parseInt(validated.page, 10) : 1,
|
page: validated.page ? parseInt(validated.page, 10) : 1,
|
||||||
errorMessages: [],
|
errorMessages: [],
|
||||||
alertSummaries: [],
|
alertSummaries: [],
|
||||||
|
@ -55,43 +55,30 @@ class AlertsView extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
const { count, frameworkOptions } = this.state;
|
const { count } = this.state;
|
||||||
const { validated } = this.props;
|
|
||||||
const prevValitated = prevProps.validated;
|
|
||||||
|
|
||||||
if (prevState.count !== count) {
|
if (prevState.count !== count) {
|
||||||
this.setState({ totalPages: this.generatePages(count) });
|
this.setState({ totalPages: this.generatePages(count) });
|
||||||
}
|
}
|
||||||
|
|
||||||
// filters updated directly in the url
|
|
||||||
if (
|
|
||||||
validated.hideAssignedToOthers !== prevValitated.hideAssignedToOthers ||
|
|
||||||
validated.hideImprovements !== prevValitated.hideImprovements ||
|
|
||||||
validated.hideDwnToInv !== prevValitated.hideDwnToInv ||
|
|
||||||
validated.filterText !== prevValitated.filterText ||
|
|
||||||
validated.status !== prevValitated.status ||
|
|
||||||
validated.framework !== prevValitated.framework
|
|
||||||
) {
|
|
||||||
this.setFiltersState(
|
|
||||||
this.getFiltersFromParams(validated, frameworkOptions),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = parseQueryParams(this.props.location.search);
|
const params = parseQueryParams(this.props.location.search);
|
||||||
|
const prevParams = parseQueryParams(prevProps.location.search);
|
||||||
// we're using local state for id instead of validated.id because once
|
// we're using local state for id instead of validated.id because once
|
||||||
// the user navigates from the id=<alert> view back to the main alerts view
|
// the user navigates from the id=<alert> view back to the main alerts view
|
||||||
// the Validation component won't reset the id (since the query param doesn't exist
|
// the Validation component won't reset the id (since the query param doesn't exist
|
||||||
// unless there is a value)
|
// unless there is a value)
|
||||||
if (this.props.location.search !== prevProps.location.search) {
|
if (params.id !== prevParams.id) {
|
||||||
this.setState({ id: params.id || null }, this.fetchAlertSummaries);
|
this.setState(
|
||||||
if (params.id) {
|
{ id: params.id || null, filters: this.getFiltersFromParams(params) },
|
||||||
validated.updateParams({ hideDwnToInv: 0 });
|
this.fetchAlertSummaries,
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getFiltersFromParams = (validated, frameworkOptions) => {
|
getFiltersFromParams = (
|
||||||
|
validated,
|
||||||
|
frameworkOptions = this.extendedOptions,
|
||||||
|
) => {
|
||||||
return {
|
return {
|
||||||
status: this.getDefaultStatus(),
|
status: this.getDefaultStatus(),
|
||||||
framework: getFrameworkData({
|
framework: getFrameworkData({
|
||||||
|
@ -229,7 +216,11 @@ class AlertsView extends React.Component {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
async fetchAlertSummaries(id = this.state.id, update = false, page = 1) {
|
async fetchAlertSummaries(
|
||||||
|
id = this.state.id,
|
||||||
|
update = false,
|
||||||
|
page = this.state.page,
|
||||||
|
) {
|
||||||
// turn off loading when update is true (used to update alert statuses)
|
// turn off loading when update is true (used to update alert statuses)
|
||||||
this.setState({ loading: !update, errorMessages: [] });
|
this.setState({ loading: !update, errorMessages: [] });
|
||||||
const { user } = this.props;
|
const { user } = this.props;
|
||||||
|
|
|
@ -14,14 +14,16 @@ export default class AlertsViewControls extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
updateFilter = (filter) => {
|
updateFilter = (filter) => {
|
||||||
const { setFiltersState, filters } = this.props;
|
const { setFiltersState, filters, updateViewState } = this.props;
|
||||||
const prevValue = filters[filter];
|
const prevValue = filters[filter];
|
||||||
setFiltersState({ [filter]: !prevValue });
|
setFiltersState({ [filter]: !prevValue });
|
||||||
|
updateViewState({ page: 1 });
|
||||||
};
|
};
|
||||||
|
|
||||||
updateStatus = (status) => {
|
updateStatus = (status) => {
|
||||||
const { setFiltersState } = this.props;
|
const { setFiltersState, updateViewState } = this.props;
|
||||||
setFiltersState({ status });
|
setFiltersState({ status });
|
||||||
|
updateViewState({ page: 1 });
|
||||||
};
|
};
|
||||||
|
|
||||||
updateFramework = (selectedFramework) => {
|
updateFramework = (selectedFramework) => {
|
||||||
|
@ -29,7 +31,7 @@ export default class AlertsViewControls extends React.Component {
|
||||||
const framework = frameworkOptions.find(
|
const framework = frameworkOptions.find(
|
||||||
(item) => item.name === selectedFramework,
|
(item) => item.name === selectedFramework,
|
||||||
);
|
);
|
||||||
updateViewState({ bugTemplate: null });
|
updateViewState({ bugTemplate: null, page: 1 });
|
||||||
setFiltersState({ framework }, this.fetchAlertSummaries);
|
setFiltersState({ framework }, this.fetchAlertSummaries);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { getTitle } from '../helpers';
|
import { getTitle } from '../helpers';
|
||||||
import SimpleTooltip from '../../shared/SimpleTooltip';
|
import SimpleTooltip from '../../shared/SimpleTooltip';
|
||||||
|
@ -51,9 +52,9 @@ export default class DownstreamSummary extends React.Component {
|
||||||
<SimpleTooltip
|
<SimpleTooltip
|
||||||
text={
|
text={
|
||||||
<span>
|
<span>
|
||||||
<a href={`perf.html#/alerts?id=${id}`} className="text-info">
|
<Link to={`./alerts?id=${id}`} className="text-info">
|
||||||
#{id}
|
#{id}
|
||||||
</a>
|
</Link>
|
||||||
{position === 0 ? '' : ', '}
|
{position === 0 ? '' : ', '}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,7 +70,7 @@ export default class StatusDropdown extends React.Component {
|
||||||
framework: getFrameworkName(frameworks, alertSummary.framework),
|
framework: getFrameworkName(frameworks, alertSummary.framework),
|
||||||
revision: alertSummary.revision,
|
revision: alertSummary.revision,
|
||||||
revisionHref: repoModel.getPushLogHref(alertSummary.revision),
|
revisionHref: repoModel.getPushLogHref(alertSummary.revision),
|
||||||
alertHref: `${window.location.origin}/perf.html#/alerts?id=${alertSummary.id}`,
|
alertHref: `${window.location.origin}/perfherder/alerts?id=${alertSummary.id}`,
|
||||||
alertSummary: getTextualSummary(filteredAlerts, alertSummary),
|
alertSummary: getTextualSummary(filteredAlerts, alertSummary),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -85,9 +85,9 @@ export default class CompareSelectorView extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (framework === 0) {
|
if (framework === 0) {
|
||||||
history.push(`/infracompare${createQueryParams(params)}`);
|
history.push(`./infracompare${createQueryParams(params)}`);
|
||||||
} else {
|
} else {
|
||||||
history.push(`/compare${createQueryParams(params)}`);
|
history.push(`./compare${createQueryParams(params)}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -84,9 +84,7 @@ class CompareSubtestsView extends React.PureComponent {
|
||||||
|
|
||||||
links.push({
|
links.push({
|
||||||
title: 'replicates',
|
title: 'replicates',
|
||||||
href: `perf.html#/comparesubtestdistribution${createQueryParams(
|
to: `./comparesubtestdistribution${createQueryParams(params)}`,
|
||||||
params,
|
|
||||||
)}`,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const signatureHash = !oldResults
|
const signatureHash = !oldResults
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
faThumbsUp,
|
faThumbsUp,
|
||||||
faHashtag,
|
faHashtag,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import SimpleTooltip from '../../shared/SimpleTooltip';
|
import SimpleTooltip from '../../shared/SimpleTooltip';
|
||||||
import { displayNumber, formatNumber, getHashBasedId } from '../helpers';
|
import { displayNumber, formatNumber, getHashBasedId } from '../helpers';
|
||||||
|
@ -81,7 +82,7 @@ export default class CompareTableRow extends React.PureComponent {
|
||||||
{rowLevelResults.links &&
|
{rowLevelResults.links &&
|
||||||
rowLevelResults.links.map((link) => (
|
rowLevelResults.links.map((link) => (
|
||||||
<span key={link.title}>
|
<span key={link.title}>
|
||||||
<a href={link.href}>{` ${link.title}`}</a>
|
<Link to={link.to}>{` ${link.title}`}</Link>
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -257,7 +257,7 @@ export default class CompareTableView extends React.Component {
|
||||||
{hasSubtests && (
|
{hasSubtests && (
|
||||||
<Link
|
<Link
|
||||||
to={{
|
to={{
|
||||||
pathname: '/compare',
|
pathname: './compare',
|
||||||
search: createQueryParams(params),
|
search: createQueryParams(params),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -103,13 +103,11 @@ class CompareView extends React.PureComponent {
|
||||||
params.selectedTimeRange = timeRange.value;
|
params.selectedTimeRange = timeRange.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const detailsLink = `perf.html#/comparesubtest${createQueryParams(
|
const detailsLink = `./comparesubtest${createQueryParams(params)}`;
|
||||||
params,
|
|
||||||
)}`;
|
|
||||||
|
|
||||||
links.push({
|
links.push({
|
||||||
title: 'subtests',
|
title: 'subtests',
|
||||||
href: detailsLink,
|
to: detailsLink,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const signatureHash = !oldResults
|
const signatureHash = !oldResults
|
||||||
|
|
|
@ -46,7 +46,7 @@ const TableAverage = ({ value, stddev, stddevpct, replicates }) => {
|
||||||
tooltipText
|
tooltipText
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
tooltipClass={replicates.length > 1 ? 'compare-table-tooltip' : ''}
|
innerClassName={replicates.length > 1 ? 'compare-table-tooltip' : ''}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-muted">No results</span>
|
<span className="text-muted">No results</span>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
faExclamationCircle,
|
faExclamationCircle,
|
||||||
faTimes,
|
faTimes,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { alertStatusMap, endpoints } from '../constants';
|
import { alertStatusMap, endpoints } from '../constants';
|
||||||
import { getJobsUrl, createQueryParams, getApiUrl } from '../../helpers/url';
|
import { getJobsUrl, createQueryParams, getApiUrl } from '../../helpers/url';
|
||||||
|
@ -203,7 +204,7 @@ const GraphTooltip = ({
|
||||||
{dataPointDetails.jobId && prevRevision && ', '}
|
{dataPointDetails.jobId && prevRevision && ', '}
|
||||||
{prevRevision && (
|
{prevRevision && (
|
||||||
<a
|
<a
|
||||||
href={`#/comparesubtest${createQueryParams({
|
href={`./comparesubtest${createQueryParams({
|
||||||
originalProject: testDetails.repository_name,
|
originalProject: testDetails.repository_name,
|
||||||
newProject: testDetails.repository_name,
|
newProject: testDetails.repository_name,
|
||||||
originalRevision: prevRevision,
|
originalRevision: prevRevision,
|
||||||
|
@ -229,16 +230,14 @@ const GraphTooltip = ({
|
||||||
</span>
|
</span>
|
||||||
{dataPointDetails.alertSummary && (
|
{dataPointDetails.alertSummary && (
|
||||||
<p>
|
<p>
|
||||||
<a
|
<Link to={`./alerts?id=${dataPointDetails.alertSummary.id}`}>
|
||||||
href={`perf.html#/alerts?id=${dataPointDetails.alertSummary.id}`}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
className="text-warning"
|
className="text-warning"
|
||||||
icon={faExclamationCircle}
|
icon={faExclamationCircle}
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
{` Alert # ${dataPointDetails.alertSummary.id}`}
|
{` Alert # ${dataPointDetails.alertSummary.id}`}
|
||||||
</a>
|
</Link>
|
||||||
<span className="text-muted">
|
<span className="text-muted">
|
||||||
{` - ${alertStatus} `}
|
{` - ${alertStatus} `}
|
||||||
{alert && alert.related_summary_id && (
|
{alert && alert.related_summary_id && (
|
||||||
|
@ -247,11 +246,11 @@ const GraphTooltip = ({
|
||||||
dataPointDetails.alertSummary.id
|
dataPointDetails.alertSummary.id
|
||||||
? 'to'
|
? 'to'
|
||||||
: 'from'}
|
: 'from'}
|
||||||
<a
|
<Link
|
||||||
href={`#/alerts?id=${alert.related_summary_id}`}
|
to={`./alerts?id=${alert.related_summary_id}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>{` alert # ${alert.related_summary_id}`}</a>
|
>{` alert # ${alert.related_summary_id}`}</Link>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -54,7 +54,7 @@ const TableView = ({
|
||||||
group_state: 'expanded',
|
group_state: 'expanded',
|
||||||
});
|
});
|
||||||
|
|
||||||
const compareUrl = `#/comparesubtest${createQueryParams({
|
const compareUrl = `./comparesubtest${createQueryParams({
|
||||||
originalProject: item.repository_name,
|
originalProject: item.repository_name,
|
||||||
newProject: item.repository_name,
|
newProject: item.repository_name,
|
||||||
originalRevision: prevRevision,
|
originalRevision: prevRevision,
|
||||||
|
|
|
@ -312,7 +312,7 @@ export const getGraphsLink = function getGraphsLink(
|
||||||
params.timerange = timeRange;
|
params.timerange = timeRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `perf.html#/graphs?${queryString.stringify(params)}`;
|
return `./graphs?${queryString.stringify(params)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createNoiseMetric = function createNoiseMetric(
|
export const createNoiseMetric = function createNoiseMetric(
|
||||||
|
@ -363,7 +363,7 @@ export const createGraphsLinks = (
|
||||||
|
|
||||||
links.push({
|
links.push({
|
||||||
title: 'graph',
|
title: 'graph',
|
||||||
href: graphsLink,
|
to: graphsLink,
|
||||||
});
|
});
|
||||||
|
|
||||||
return links;
|
return links;
|
||||||
|
@ -385,7 +385,7 @@ export const getGraphsURL = (
|
||||||
alertRepository,
|
alertRepository,
|
||||||
performanceFrameworkId,
|
performanceFrameworkId,
|
||||||
) => {
|
) => {
|
||||||
let url = `#/graphs?timerange=${timeRange}&series=${alertRepository},${alert.series_signature.id},1,${alert.series_signature.framework_id}`;
|
let url = `./graphs?timerange=${timeRange}&series=${alertRepository},${alert.series_signature.id},1,${alert.series_signature.framework_id}`;
|
||||||
|
|
||||||
// automatically add related branches (we take advantage of
|
// automatically add related branches (we take advantage of
|
||||||
// the otherwise rather useless signature hash to avoid having to fetch this
|
// the otherwise rather useless signature hash to avoid having to fetch this
|
||||||
|
@ -477,7 +477,7 @@ export const getTextualSummary = (alerts, alertSummary, copySummary = null) => {
|
||||||
}
|
}
|
||||||
// include link to alert if getting text for clipboard only
|
// include link to alert if getting text for clipboard only
|
||||||
if (copySummary) {
|
if (copySummary) {
|
||||||
const alertLink = `${window.location.origin}/perf.html#/alerts?id=${alertSummary.id}`;
|
const alertLink = `${window.location.origin}/perfherder/alerts?id=${alertSummary.id}`;
|
||||||
resultStr += `\nFor up to date results, see: ${alertLink}`;
|
resultStr += `\nFor up to date results, see: ${alertLink}`;
|
||||||
}
|
}
|
||||||
return resultStr;
|
return resultStr;
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { render } from 'react-dom';
|
|
||||||
import 'react-table/react-table.css';
|
|
||||||
|
|
||||||
import '../css/treeherder-base.css';
|
|
||||||
import '../css/treeherder-custom-styles.css';
|
|
||||||
import '../css/treeherder-navbar.css';
|
|
||||||
import '../css/perf.css';
|
|
||||||
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
render(<App />, document.getElementById('root'));
|
|
|
@ -1,11 +1,18 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { hot } from 'react-hot-loader/root';
|
import { hot } from 'react-hot-loader/root';
|
||||||
import { BrowserRouter, Route, Switch } from 'react-router-dom';
|
import { Route, Switch } from 'react-router-dom';
|
||||||
|
|
||||||
import NotFound from './NotFound';
|
import NotFound from './NotFound';
|
||||||
import Health from './Health';
|
import Health from './Health';
|
||||||
import Usage from './Usage';
|
import Usage from './Usage';
|
||||||
|
|
||||||
|
import '../css/failure-summary.css';
|
||||||
|
import '../css/lazylog-custom-styles.css';
|
||||||
|
import '../css/treeherder-job-buttons.css';
|
||||||
|
import '../css/treeherder-notifications.css';
|
||||||
|
import './pushhealth.css';
|
||||||
|
import 'react-tabs/style/react-tabs.css';
|
||||||
|
|
||||||
function hasProps(search) {
|
function hasProps(search) {
|
||||||
const params = new URLSearchParams(search);
|
const params = new URLSearchParams(search);
|
||||||
|
|
||||||
|
@ -14,26 +21,23 @@ function hasProps(search) {
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<Switch>
|
||||||
<Switch>
|
<Route
|
||||||
<Route
|
path="/"
|
||||||
exact
|
render={(props) =>
|
||||||
path="/pushhealth.html"
|
hasProps(props.location.search) ? (
|
||||||
render={(props) =>
|
<Health {...props} />
|
||||||
hasProps(props.location.search) ? (
|
) : (
|
||||||
<Health {...props} />
|
<Usage {...props} />
|
||||||
) : (
|
)
|
||||||
<Usage {...props} />
|
}
|
||||||
)
|
/>
|
||||||
}
|
<Route name="notfound" component={NotFound} />
|
||||||
/>
|
</Switch>
|
||||||
<Route name="notfound" component={NotFound} />
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</BrowserRouter>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -67,7 +67,7 @@ class Usage extends Component {
|
||||||
<tr key={revision} data-testid={`facet-${revision}`}>
|
<tr key={revision} data-testid={`facet-${revision}`}>
|
||||||
<td data-testid="facet-link">
|
<td data-testid="facet-link">
|
||||||
<a
|
<a
|
||||||
href={`/pushhealth.html?repo=try&revision=${revision}`}
|
href={`/push-health?repo=try&revision=${revision}`}
|
||||||
title="See Push Health"
|
title="See Push Health"
|
||||||
>
|
>
|
||||||
{revision}
|
{revision}
|
||||||
|
|
|
@ -4,8 +4,6 @@ import { render } from 'react-dom';
|
||||||
// Treeherder Styles
|
// Treeherder Styles
|
||||||
import '../css/failure-summary.css';
|
import '../css/failure-summary.css';
|
||||||
import '../css/lazylog-custom-styles.css';
|
import '../css/lazylog-custom-styles.css';
|
||||||
import '../css/treeherder-custom-styles.css';
|
|
||||||
import '../css/treeherder-navbar.css';
|
|
||||||
import '../css/treeherder-job-buttons.css';
|
import '../css/treeherder-job-buttons.css';
|
||||||
import '../css/treeherder-notifications.css';
|
import '../css/treeherder-notifications.css';
|
||||||
import './pushhealth.css';
|
import './pushhealth.css';
|
||||||
|
|
|
@ -72,7 +72,7 @@ export default class ComparePageTitle extends React.Component {
|
||||||
changeQueryParam = (newTitle) => {
|
changeQueryParam = (newTitle) => {
|
||||||
const params = getAllUrlParams();
|
const params = getAllUrlParams();
|
||||||
params.set('pageTitle', newTitle);
|
params.set('pageTitle', newTitle);
|
||||||
replaceLocation(params, '/compare');
|
replaceLocation(params);
|
||||||
};
|
};
|
||||||
|
|
||||||
userActionListener = async (event) => {
|
userActionListener = async (event) => {
|
||||||
|
|
|
@ -21,7 +21,7 @@ import {
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{
|
{
|
||||||
href: '/userguide.html',
|
href: '/userguide',
|
||||||
icon: faQuestionCircle,
|
icon: faQuestionCircle,
|
||||||
text: 'User Guide',
|
text: 'User Guide',
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { getInspectTaskUrl } from '../helpers/url';
|
import { getInspectTaskUrl } from '../helpers/url';
|
||||||
import { getJobSearchStrHref } from '../helpers/job';
|
import { getJobSearchStrHref } from '../helpers/job';
|
||||||
|
@ -57,12 +58,12 @@ export default class JobInfo extends React.PureComponent {
|
||||||
<strong>Job: </strong>
|
<strong>Job: </strong>
|
||||||
{showJobFilters ? (
|
{showJobFilters ? (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<a
|
<Link
|
||||||
title="Filter jobs containing these keywords"
|
title="Filter jobs containing these keywords"
|
||||||
href={getJobSearchStrHref(searchStr)}
|
to={{ search: getJobSearchStrHref(searchStr) }}
|
||||||
>
|
>
|
||||||
{searchStr}
|
{searchStr}
|
||||||
</a>
|
</Link>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
) : (
|
) : (
|
||||||
<span>{searchStr}</span>
|
<span>{searchStr}</span>
|
||||||
|
|
|
@ -6,12 +6,13 @@ import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
} from 'reactstrap';
|
} from 'reactstrap';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
const choices = [
|
const choices = [
|
||||||
{ url: '/', text: 'Treeherder' },
|
{ url: '/jobs', text: 'Treeherder' },
|
||||||
{ url: '/perf.html', text: 'Perfherder' },
|
{ url: '/perfherder', text: 'Perfherder' },
|
||||||
{ url: '/intermittent-failures.html', text: 'Intermittent Failures View' },
|
{ url: '/intermittent-failures', text: 'Intermittent Failures View' },
|
||||||
{ url: '/pushhealth.html', text: 'Push Health Usage' },
|
{ url: '/push-health', text: 'Push Health Usage' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default class LogoMenu extends React.PureComponent {
|
export default class LogoMenu extends React.PureComponent {
|
||||||
|
@ -35,8 +36,8 @@ export default class LogoMenu extends React.PureComponent {
|
||||||
</DropdownToggle>
|
</DropdownToggle>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
{menuChoices.map((choice) => (
|
{menuChoices.map((choice) => (
|
||||||
<DropdownItem key={choice.text} tag="a" href={choice.url}>
|
<DropdownItem key={choice.text}>
|
||||||
{choice.text}
|
<Link to={choice.url}>{choice.text}</Link>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
))}
|
))}
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default function Author(props) {
|
|
||||||
const authorMatch = props.author.match(/<(.*?)>+/);
|
|
||||||
const authorEmail = authorMatch ? authorMatch[1] : props.author;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span title="View pushes by this user" className="push-author">
|
|
||||||
<a href={props.url}>{authorEmail}</a>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Author.propTypes = {
|
|
||||||
author: PropTypes.string.isRequired,
|
|
||||||
url: PropTypes.string.isRequired,
|
|
||||||
};
|
|
|
@ -14,7 +14,7 @@ export default class SimpleTooltip extends React.Component {
|
||||||
tooltipText,
|
tooltipText,
|
||||||
placement,
|
placement,
|
||||||
textClass,
|
textClass,
|
||||||
tooltipClass,
|
innerClassName,
|
||||||
autohide,
|
autohide,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ export default class SimpleTooltip extends React.Component {
|
||||||
<UncontrolledTooltip
|
<UncontrolledTooltip
|
||||||
placement={placement}
|
placement={placement}
|
||||||
target={this.tooltipRef}
|
target={this.tooltipRef}
|
||||||
innerClassName={tooltipClass}
|
innerClassName={innerClassName}
|
||||||
autohide={autohide}
|
autohide={autohide}
|
||||||
>
|
>
|
||||||
{tooltipText}
|
{tooltipText}
|
||||||
|
@ -41,13 +41,13 @@ SimpleTooltip.propTypes = {
|
||||||
.isRequired,
|
.isRequired,
|
||||||
textClass: PropTypes.string,
|
textClass: PropTypes.string,
|
||||||
placement: PropTypes.string,
|
placement: PropTypes.string,
|
||||||
tooltipClass: PropTypes.string,
|
innerClassName: PropTypes.string,
|
||||||
autohide: PropTypes.bool,
|
autohide: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
SimpleTooltip.defaultProps = {
|
SimpleTooltip.defaultProps = {
|
||||||
textClass: '',
|
textClass: '',
|
||||||
placement: 'top',
|
placement: 'top',
|
||||||
tooltipClass: '',
|
innerClassName: '',
|
||||||
autohide: true,
|
autohide: true,
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,7 +10,9 @@ export const tcClientIdMap = {
|
||||||
'https://treeherder-prototype2.herokuapp.com': 'dev2',
|
'https://treeherder-prototype2.herokuapp.com': 'dev2',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const clientId = `treeherder-${tcClientIdMap[window.location.origin]}`;
|
export const clientId = `treeherder-${
|
||||||
|
tcClientIdMap[window.location.origin]
|
||||||
|
}-client`;
|
||||||
|
|
||||||
export const redirectURI = `${window.location.origin}${tcAuthCallbackUrl}`;
|
export const redirectURI = `${window.location.origin}${tcAuthCallbackUrl}`;
|
||||||
|
|
||||||
|
@ -26,7 +28,7 @@ export const checkRootUrl = (rootUrl) => {
|
||||||
// and the default login rootUrls are for https://firefox-ci-tc.services.mozilla.com
|
// and the default login rootUrls are for https://firefox-ci-tc.services.mozilla.com
|
||||||
if (
|
if (
|
||||||
rootUrl === prodFirefoxRootUrl &&
|
rootUrl === prodFirefoxRootUrl &&
|
||||||
clientId === 'treeherder-taskcluster-staging'
|
clientId === 'treeherder-taskcluster-staging-client'
|
||||||
) {
|
) {
|
||||||
return stagingFirefoxRootUrl;
|
return stagingFirefoxRootUrl;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,10 @@ import UserGuideHeader from './UserGuideHeader';
|
||||||
import UserGuideBody from './UserGuideBody';
|
import UserGuideBody from './UserGuideBody';
|
||||||
import UserGuideFooter from './UserGuideFooter';
|
import UserGuideFooter from './UserGuideFooter';
|
||||||
|
|
||||||
|
import '../css/treeherder-userguide.css';
|
||||||
|
import '../css/treeherder-job-buttons.css';
|
||||||
|
import '../css/treeherder-base.css';
|
||||||
|
|
||||||
const App = () => (
|
const App = () => (
|
||||||
<div id="userguide">
|
<div id="userguide">
|
||||||
<div className="card">
|
<div className="card">
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { render } from 'react-dom';
|
|
||||||
|
|
||||||
import '../css/treeherder-base.css';
|
|
||||||
import '../css/treeherder-custom-styles.css';
|
|
||||||
import '../css/treeherder-userguide.css';
|
|
||||||
import '../css/treeherder-job-buttons.css';
|
|
||||||
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
render(<App />, document.getElementById('root'));
|
|
18
yarn.lock
18
yarn.lock
|
@ -897,7 +897,7 @@
|
||||||
core-js-pure "^3.0.0"
|
core-js-pure "^3.0.0"
|
||||||
regenerator-runtime "^0.13.4"
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
|
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
|
||||||
version "7.11.2"
|
version "7.11.2"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
|
||||||
integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
|
integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
|
||||||
|
@ -2994,6 +2994,13 @@ connect-history-api-fallback@^1.6.0:
|
||||||
resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc"
|
resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc"
|
||||||
integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==
|
integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==
|
||||||
|
|
||||||
|
connected-react-router@6.8.0:
|
||||||
|
version "6.8.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/connected-react-router/-/connected-react-router-6.8.0.tgz#ddc687b31d498322445d235d660798489fa56cae"
|
||||||
|
integrity sha512-E64/6krdJM3Ag3MMmh2nKPtMbH15s3JQDuaYJvOVXzu6MbHbDyIvuwLOyhQIuP4Om9zqEfZYiVyflROibSsONg==
|
||||||
|
dependencies:
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
|
||||||
console-browserify@^1.1.0:
|
console-browserify@^1.1.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336"
|
resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336"
|
||||||
|
@ -5132,14 +5139,7 @@ highlight-words-core@^1.2.0:
|
||||||
resolved "https://registry.yarnpkg.com/highlight-words-core/-/highlight-words-core-1.2.2.tgz#1eff6d7d9f0a22f155042a00791237791b1eeaaa"
|
resolved "https://registry.yarnpkg.com/highlight-words-core/-/highlight-words-core-1.2.2.tgz#1eff6d7d9f0a22f155042a00791237791b1eeaaa"
|
||||||
integrity sha512-BXUKIkUuh6cmmxzi5OIbUJxrG8OAk2MqoL1DtO3Wo9D2faJg2ph5ntyuQeLqaHJmzER6H5tllCDA9ZnNe9BVGg==
|
integrity sha512-BXUKIkUuh6cmmxzi5OIbUJxrG8OAk2MqoL1DtO3Wo9D2faJg2ph5ntyuQeLqaHJmzER6H5tllCDA9ZnNe9BVGg==
|
||||||
|
|
||||||
history@5.0.0:
|
history@4.10.1, history@^4.9.0:
|
||||||
version "5.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/history/-/history-5.0.0.tgz#0cabbb6c4bbf835addb874f8259f6d25101efd08"
|
|
||||||
integrity sha512-3NyRMKIiFSJmIPdq7FxkNMJkQ7ZEtVblOQ38VtKaA0zZMW1Eo6Q6W8oDKEflr1kNNTItSnk4JMCO1deeSgbLLg==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.7.6"
|
|
||||||
|
|
||||||
history@^4.9.0:
|
|
||||||
version "4.10.1"
|
version "4.10.1"
|
||||||
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
|
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
|
||||||
integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
|
integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
|
||||||
|
|
Загрузка…
Ссылка в новой задаче