зеркало из 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/',
|
||||
mains: {
|
||||
index: {
|
||||
entry: 'job-view/index.jsx',
|
||||
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',
|
||||
entry: 'index',
|
||||
template: 'ui/index.html',
|
||||
},
|
||||
},
|
||||
|
@ -72,11 +30,12 @@ module.exports = {
|
|||
}),
|
||||
require('@neutrinojs/react')({
|
||||
devServer: {
|
||||
historyApiFallback: false,
|
||||
historyApiFallback: true,
|
||||
hot: true,
|
||||
open: !process.env.IN_DOCKER,
|
||||
proxy: {
|
||||
// Proxy any paths not recognised by webpack to the specified backend.
|
||||
'*': {
|
||||
'/api': {
|
||||
changeOrigin: true,
|
||||
headers: {
|
||||
// Prevent Django CSRF errors, whilst still making it clear
|
||||
|
|
|
@ -26,8 +26,9 @@
|
|||
"@types/react-dom": "*",
|
||||
"ajv": "6.12.6",
|
||||
"auth0-js": "9.13.4",
|
||||
"connected-react-router": "6.8.0",
|
||||
"fuse.js": "6.0.4",
|
||||
"history": "5.0.0",
|
||||
"history": "4.10.1",
|
||||
"js-cookie": "2.2.1",
|
||||
"js-yaml": "3.13.1",
|
||||
"json-e": "3.0.2",
|
||||
|
|
|
@ -3,6 +3,8 @@ import copy
|
|||
import pytest
|
||||
from pages.treeherder import Treeherder
|
||||
|
||||
skip = pytest.mark.skip
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
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)
|
||||
|
||||
|
||||
@skip
|
||||
def test_expand_job_group(base_url, selenium, test_jobs):
|
||||
page = Treeherder(selenium, base_url).open()
|
||||
page.wait.until(lambda _: len(page.all_job_groups) == 1)
|
||||
|
|
|
@ -4,7 +4,7 @@ import { getRevisionUrl } from '../../../ui/helpers/url';
|
|||
describe('getRevisionUrl helper', () => {
|
||||
test('escapes some html symbols', () => {
|
||||
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 fetchMock from 'fetch-mock';
|
||||
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 pushListFixture from '../mock/push_list';
|
||||
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 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', () => {
|
||||
const repoName = 'autoland';
|
||||
|
@ -161,41 +178,27 @@ describe('App', () => {
|
|||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.location.hash = `#/jobs?repo=${repoName}`;
|
||||
history.push('/');
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
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 () => {
|
||||
const { getByText, getByAltText } = render(<App />);
|
||||
const { getByText, getByAltText } = render(testApp());
|
||||
const appMenu = await waitFor(() => getByAltText('Treeherder'));
|
||||
|
||||
expect(appMenu).toBeInTheDocument();
|
||||
fireEvent.click(appMenu);
|
||||
|
||||
const phMenu = await waitFor(() => getByText('Perfherder'));
|
||||
expect(phMenu.getAttribute('href')).toBe('/perf.html');
|
||||
expect(phMenu.getAttribute('href')).toBe('/perfherder');
|
||||
|
||||
const ifvMenu = await waitFor(() =>
|
||||
getByText('Intermittent Failures View'),
|
||||
);
|
||||
expect(ifvMenu.getAttribute('href')).toBe('/intermittent-failures.html');
|
||||
expect(ifvMenu.getAttribute('href')).toBe('/intermittent-failures');
|
||||
});
|
||||
|
||||
const testChangingSelectedJob = async (
|
||||
|
@ -205,7 +208,7 @@ describe('App', () => {
|
|||
secondJobSymbol,
|
||||
secondJobTaskId,
|
||||
) => {
|
||||
const { getByText, findByText, findByTestId } = render(<App />);
|
||||
const { getByText, findByText, findByTestId } = render(testApp());
|
||||
const firstJob = await findByText(firstJobSymbol);
|
||||
|
||||
fireEvent.mouseDown(firstJob);
|
||||
|
@ -271,4 +274,90 @@ describe('App', () => {
|
|||
),
|
||||
).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 fetchMock from 'fetch-mock';
|
||||
import {
|
||||
render,
|
||||
fireEvent,
|
||||
waitFor,
|
||||
waitForElementToBeRemoved,
|
||||
} from '@testing-library/react';
|
||||
import { render, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { ConnectedRouter } from 'connected-react-router';
|
||||
import { Provider, ReactReduxContext } from 'react-redux';
|
||||
import { createBrowserHistory } from 'history';
|
||||
|
||||
import App from '../../../ui/job-view/App';
|
||||
import taskDefinition from '../mock/task_definition.json';
|
||||
import pushListFixture from '../mock/push_list';
|
||||
import reposFixture from '../mock/repositories';
|
||||
import { getApiUrl, bzBaseUrl } from '../../../ui/helpers/url';
|
||||
import {
|
||||
getProjectUrl,
|
||||
replaceLocation,
|
||||
setUrlParam,
|
||||
} from '../../../ui/helpers/location';
|
||||
import { getProjectUrl } from '../../../ui/helpers/location';
|
||||
import jobListFixtureOne from '../mock/job_list/job_1';
|
||||
import jobMap from '../mock/job_map';
|
||||
import { configureStore } from '../../../ui/job-view/redux/configureStore';
|
||||
|
||||
const repoName = 'autoland';
|
||||
const treeStatusResponse = {
|
||||
|
@ -35,10 +30,21 @@ const emptyPushResponse = {
|
|||
const emptyBzResponse = {
|
||||
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', () => {
|
||||
beforeAll(() => {
|
||||
window.location.hash = `#/jobs?repo=${repoName}`;
|
||||
fetchMock.reset();
|
||||
fetchMock.get('/revision.txt', []);
|
||||
fetchMock.get(getApiUrl('/repository/'), reposFixture);
|
||||
|
@ -70,10 +76,7 @@ describe('Filtering', () => {
|
|||
taskDefinition,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.location.hash = `#/jobs?repo=${repoName}`;
|
||||
});
|
||||
afterEach(() => history.push('/'));
|
||||
|
||||
afterAll(() => {
|
||||
fetchMock.reset();
|
||||
|
@ -109,56 +112,67 @@ describe('Filtering', () => {
|
|||
});
|
||||
|
||||
test('should have 1 push', async () => {
|
||||
const { getAllByText, getAllByTestId, getByTestId } = render(<App />);
|
||||
// wait till the ``reviewbot`` authored push is shown before filtering.
|
||||
await waitFor(() => getAllByText('reviewbot'));
|
||||
setUrlParam('author', 'reviewbot');
|
||||
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(() =>
|
||||
const { getAllByText, getAllByTestId, getByText, getByTitle } = render(
|
||||
testApp(),
|
||||
);
|
||||
const unfilteredPushes = await waitFor(() =>
|
||||
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', () => {
|
||||
test('should have 10 failures', async () => {
|
||||
const { getAllByText, getByTitle, findAllByText } = render(<App />);
|
||||
const { getByTitle, findAllByText, queryAllByText } = render(testApp());
|
||||
|
||||
await findAllByText('B');
|
||||
const unclassifiedOnlyButton = getByTitle(
|
||||
'Loaded failures / toggle filtering for unclassified failures',
|
||||
);
|
||||
await waitFor(() => findAllByText('yaml'));
|
||||
|
||||
fireEvent.click(unclassifiedOnlyButton);
|
||||
|
||||
// Since yaml is not an unclassified failure, making this call will
|
||||
// ensure that the filtering has completed. Then we can get an accurate
|
||||
// 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.
|
||||
// 10 pushes with 2 failures each, but only 1 unclassified.
|
||||
expect(jobCount()).toBe(20);
|
||||
|
||||
// undo the filtering and make sure we see all the jobs again
|
||||
fireEvent.click(unclassifiedOnlyButton);
|
||||
await waitFor(() => getAllByText('yaml'));
|
||||
await waitFor(() => findAllByText('yaml'));
|
||||
expect(jobCount()).toBe(50);
|
||||
});
|
||||
|
||||
test('KeyboardShortcut u: toggle unclassified jobs', async () => {
|
||||
const { getAllByText } = render(<App />);
|
||||
const { queryAllByText, getAllByText } = render(testApp());
|
||||
const symbolToRemove = 'yaml';
|
||||
|
||||
await waitFor(() => getAllByText(symbolToRemove));
|
||||
fireEvent.keyDown(document.body, { key: 'u', keyCode: 85 });
|
||||
|
||||
await waitForElementToBeRemoved(() => getAllByText(symbolToRemove));
|
||||
await waitFor(() => {
|
||||
expect(queryAllByText('yaml')).toHaveLength(0);
|
||||
});
|
||||
|
||||
expect(jobCount()).toBe(20);
|
||||
});
|
||||
});
|
||||
|
@ -196,10 +210,6 @@ describe('Filtering', () => {
|
|||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
replaceLocation({});
|
||||
});
|
||||
|
||||
const setFilterText = (filterField, text) => {
|
||||
fireEvent.click(filterField);
|
||||
fireEvent.change(filterField, { target: { value: text } });
|
||||
|
@ -207,7 +217,8 @@ describe('Filtering', () => {
|
|||
};
|
||||
|
||||
test('click signature should have 10 jobs', async () => {
|
||||
const { getByTitle, findAllByText } = render(<App />);
|
||||
const { getByTitle, findAllByText } = render(testApp());
|
||||
|
||||
const build = await findAllByText('B');
|
||||
|
||||
fireEvent.mouseDown(build[0]);
|
||||
|
@ -217,17 +228,19 @@ describe('Filtering', () => {
|
|||
10000,
|
||||
);
|
||||
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 () => {
|
||||
const { getAllByText, findAllByText } = render(<App />);
|
||||
const { getAllByText, findAllByText, queryAllByText } = render(testApp());
|
||||
await findAllByText('B');
|
||||
const filterField = document.querySelector('#quick-filter');
|
||||
setFilterText(filterField, 'yaml');
|
||||
|
||||
await waitForElementToBeRemoved(() => getAllByText('B'));
|
||||
await waitFor(() => {
|
||||
expect(queryAllByText('B')).toHaveLength(0);
|
||||
});
|
||||
expect(jobCount()).toBe(10);
|
||||
|
||||
// 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 () => {
|
||||
const { findAllByText } = render(<App />);
|
||||
const { findAllByText } = render(testApp());
|
||||
await findAllByText('B');
|
||||
|
||||
const filterField = document.querySelector('#quick-filter');
|
||||
|
@ -248,14 +261,19 @@ describe('Filtering', () => {
|
|||
});
|
||||
|
||||
test('KeyboardShortcut ctrl+shift+f: clear the quick filter input', async () => {
|
||||
const { findAllByText, getAllByText, getByPlaceholderText } = render(
|
||||
<App />,
|
||||
);
|
||||
const {
|
||||
findAllByText,
|
||||
getAllByText,
|
||||
getByPlaceholderText,
|
||||
queryAllByText,
|
||||
} = render(testApp());
|
||||
await findAllByText('B');
|
||||
const filterField = getByPlaceholderText('Filter platforms & jobs');
|
||||
setFilterText(filterField, 'yaml');
|
||||
|
||||
await waitForElementToBeRemoved(() => getAllByText('B'));
|
||||
await waitFor(() => {
|
||||
expect(queryAllByText('B')).toHaveLength(0);
|
||||
});
|
||||
|
||||
expect(filterField.value).toEqual('yaml');
|
||||
fireEvent.keyDown(document, {
|
||||
|
@ -277,12 +295,15 @@ describe('Filtering', () => {
|
|||
};
|
||||
|
||||
test('uncheck success should leave 30 jobs', async () => {
|
||||
const { getAllByText, findAllByText } = render(<App />);
|
||||
const { getAllByText, findAllByText, queryAllByText } = render(testApp());
|
||||
|
||||
await findAllByText('B');
|
||||
clickFilterChicklet('green');
|
||||
|
||||
await waitForElementToBeRemoved(() => getAllByText('D'));
|
||||
await waitFor(() => {
|
||||
expect(queryAllByText('D')).toHaveLength(0);
|
||||
});
|
||||
|
||||
expect(jobCount()).toBe(40);
|
||||
|
||||
// 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 () => {
|
||||
const { getAllByText, findAllByText } = render(<App />);
|
||||
const { getAllByText, findAllByText, queryAllByText } = render(testApp());
|
||||
const symbolToRemove = 'B';
|
||||
|
||||
await findAllByText(symbolToRemove);
|
||||
clickFilterChicklet('red');
|
||||
|
||||
await waitForElementToBeRemoved(() => getAllByText(symbolToRemove));
|
||||
await waitFor(() => {
|
||||
expect(queryAllByText(symbolToRemove)).toHaveLength(0);
|
||||
});
|
||||
|
||||
expect(jobCount()).toBe(20);
|
||||
|
||||
// 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 () => {
|
||||
const { getAllByText, findAllByText } = render(<App />);
|
||||
const { getAllByText, findAllByText, queryAllByText } = render(testApp());
|
||||
const symbolToRemove = 'yaml';
|
||||
|
||||
await findAllByText('B');
|
||||
clickFilterChicklet('dkgray');
|
||||
|
||||
await waitForElementToBeRemoved(() => getAllByText(symbolToRemove));
|
||||
await waitFor(() => {
|
||||
expect(queryAllByText(symbolToRemove)).toHaveLength(0);
|
||||
});
|
||||
expect(jobCount()).toBe(40);
|
||||
|
||||
// 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 () => {
|
||||
const { getAllByText } = render(<App />);
|
||||
const { getAllByText, queryAllByText } = render(testApp());
|
||||
const symbolToRemove = 'yaml';
|
||||
|
||||
await waitFor(() => getAllByText(symbolToRemove));
|
||||
|
||||
fireEvent.keyDown(document.body, { key: 'i', keyCode: 73 });
|
||||
|
||||
await waitForElementToBeRemoved(() => getAllByText(symbolToRemove));
|
||||
expect(jobCount()).toBe(40);
|
||||
expect(window.location.hash).toEqual(
|
||||
'#/jobs?repo=autoland&resultStatus=testfailed%2Cbusted%2Cexception%2Csuccess%2Cretry%2Cusercancel%2Crunnable',
|
||||
await waitFor(() => {
|
||||
expect(queryAllByText(symbolToRemove)).toHaveLength(0);
|
||||
});
|
||||
|
||||
expect(history.location.search).toEqual(
|
||||
'?repo=autoland&resultStatus=testfailed%2Cbusted%2Cexception%2Csuccess%2Cretry%2Cusercancel%2Crunnable',
|
||||
);
|
||||
});
|
||||
|
||||
test('KeyboardShortcut i: toggle on in-progress tasks', async () => {
|
||||
const { getAllByText, findAllByText } = render(<App />);
|
||||
const { getAllByText, findAllByText, queryAllByText } = render(testApp());
|
||||
const symbolToRemove = 'yaml';
|
||||
|
||||
await waitFor(() => getAllByText(symbolToRemove));
|
||||
clickFilterChicklet('dkgray');
|
||||
|
||||
await waitForElementToBeRemoved(() => getAllByText(symbolToRemove));
|
||||
await waitFor(() => {
|
||||
expect(queryAllByText(symbolToRemove)).toHaveLength(0);
|
||||
});
|
||||
|
||||
expect(jobCount()).toBe(40);
|
||||
|
||||
await findAllByText('B');
|
||||
|
@ -355,17 +386,24 @@ describe('Filtering', () => {
|
|||
await findAllByText('B');
|
||||
await waitFor(() => getAllByText(symbolToRemove), 5000);
|
||||
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 () => {
|
||||
const { getAllByText, findAllByText, findByText } = render(<App />);
|
||||
const {
|
||||
getAllByText,
|
||||
findAllByText,
|
||||
findByText,
|
||||
queryAllByText,
|
||||
} = render(testApp());
|
||||
const symbolToRemove = 'yaml';
|
||||
|
||||
await findAllByText('B');
|
||||
clickFilterChicklet('dkgray');
|
||||
|
||||
await waitForElementToBeRemoved(() => getAllByText(symbolToRemove));
|
||||
await waitFor(() => {
|
||||
expect(queryAllByText(symbolToRemove)).toHaveLength(0);
|
||||
});
|
||||
expect(jobCount()).toBe(40);
|
||||
|
||||
// undo the filtering with the "Filters | Reset" menu item
|
||||
|
|
|
@ -1,25 +1,21 @@
|
|||
import React from 'react';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Provider, ReactReduxContext } from 'react-redux';
|
||||
import { ConnectedRouter } from 'connected-react-router';
|
||||
import {
|
||||
render,
|
||||
cleanup,
|
||||
waitFor,
|
||||
waitForElementToBeRemoved,
|
||||
fireEvent,
|
||||
getAllByTestId,
|
||||
} from '@testing-library/react';
|
||||
import { createBrowserHistory } from 'history';
|
||||
|
||||
import {
|
||||
getProjectUrl,
|
||||
replaceLocation,
|
||||
setUrlParam,
|
||||
} from '../../../ui/helpers/location';
|
||||
import { getProjectUrl } from '../../../ui/helpers/location';
|
||||
import FilterModel from '../../../ui/models/filter';
|
||||
import pushListFixture from '../mock/push_list';
|
||||
import jobListFixtureOne from '../mock/job_list/job_1';
|
||||
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 { getApiUrl } from '../../../ui/helpers/url';
|
||||
import { findJobInstance } from '../../../ui/helpers/job';
|
||||
|
@ -37,6 +33,8 @@ global.document.createRange = () => ({
|
|||
|
||||
describe('PushList', () => {
|
||||
const repoName = 'autoland';
|
||||
const history = createBrowserHistory();
|
||||
|
||||
const currentRepo = {
|
||||
id: 4,
|
||||
repository_group: {
|
||||
|
@ -58,22 +56,7 @@ describe('PushList', () => {
|
|||
getRevisionHref: () => '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 = () =>
|
||||
waitFor(() => getAllByTestId(document.body, 'push-header'));
|
||||
|
||||
|
@ -92,6 +75,26 @@ describe('PushList', () => {
|
|||
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(
|
||||
getProjectUrl(
|
||||
'/push/?full=true&count=10&tochange=d5b037941b0ebabcc9b843f24d926e9d65961087',
|
||||
|
@ -117,73 +120,87 @@ describe('PushList', () => {
|
|||
);
|
||||
});
|
||||
|
||||
afterEach(() => history.push(`/jobs?repo=${repoName}`));
|
||||
|
||||
afterAll(() => {
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
replaceLocation({});
|
||||
});
|
||||
|
||||
const testPushList = () => {
|
||||
const store = configureStore(history);
|
||||
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';
|
||||
// push2Revision is 'd5b037941b0ebabcc9b843f24d926e9d65961087';
|
||||
const push2Id = 'push-511137';
|
||||
const push1Revision = 'ba9c692786e95143b8df3f4b3e9b504dfbc589a0';
|
||||
const push2Revision = 'd5b037941b0ebabcc9b843f24d926e9d65961087';
|
||||
|
||||
test('should have 2 pushes', async () => {
|
||||
const { store } = configureStore();
|
||||
render(testPushList(store, new FilterModel()));
|
||||
render(testPushList());
|
||||
|
||||
expect(await pushCount()).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('should switch to single loaded revision and back to 2', async () => {
|
||||
const { store } = configureStore();
|
||||
const { getByTestId } = render(testPushList(store, new FilterModel()));
|
||||
test('should switch to single loaded revision', async () => {
|
||||
const { getAllByTitle } = render(testPushList());
|
||||
|
||||
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
|
||||
setUrlParam('revision', push2Revision); // click push 2
|
||||
await waitForElementToBeRemoved(() => getByTestId('push-511138'));
|
||||
|
||||
fireEvent.click(pushLinks[1]);
|
||||
expect(pushLinks[0]).not.toBeInTheDocument();
|
||||
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 () => {
|
||||
const { store } = configureStore();
|
||||
const { getByTestId } = render(testPushList(store, new FilterModel()));
|
||||
const { queryAllByTestId, queryByTestId } = render(testPushList());
|
||||
|
||||
expect(await pushCount()).toHaveLength(2);
|
||||
|
||||
const push2 = getByTestId(push2Id);
|
||||
await waitFor(() => queryAllByTestId('push-header'));
|
||||
|
||||
const push2 = await waitFor(() => queryByTestId(push2Id));
|
||||
const actionMenuButton = push2.querySelector(
|
||||
'[data-testid="push-action-menu-button"]',
|
||||
);
|
||||
|
||||
fireEvent.click(actionMenuButton);
|
||||
|
||||
const setBottomLink = await waitFor(() =>
|
||||
const setFromRange = await waitFor(() =>
|
||||
push2.querySelector('[data-testid="bottom-of-range-menu-item"]'),
|
||||
);
|
||||
|
||||
expect(setBottomLink.getAttribute('href')).toContain(
|
||||
'/#/jobs?&fromchange=d5b037941b0ebabcc9b843f24d926e9d65961087',
|
||||
);
|
||||
fireEvent.click(setFromRange);
|
||||
|
||||
setUrlParam('fromchange', push1Revision);
|
||||
await waitForElementToBeRemoved(() => getByTestId(push2Id));
|
||||
expect(await pushCount()).toHaveLength(1);
|
||||
expect(history.location.search).toContain(
|
||||
'?repo=autoland&fromchange=d5b037941b0ebabcc9b843f24d926e9d65961087',
|
||||
);
|
||||
});
|
||||
|
||||
test('should reload pushes when setting tochange', async () => {
|
||||
const { store } = configureStore();
|
||||
const { getByTestId } = render(testPushList(store, new FilterModel()));
|
||||
const { getByTestId } = render(testPushList());
|
||||
|
||||
expect(await pushCount()).toHaveLength(2);
|
||||
|
||||
|
@ -194,24 +211,19 @@ describe('PushList', () => {
|
|||
|
||||
fireEvent.click(actionMenuButton);
|
||||
|
||||
const setTopLink = await waitFor(() =>
|
||||
const setTopRange = await waitFor(() =>
|
||||
push1.querySelector('[data-testid="top-of-range-menu-item"]'),
|
||||
);
|
||||
|
||||
expect(setTopLink.getAttribute('href')).toContain(
|
||||
'/#/jobs?&tochange=ba9c692786e95143b8df3f4b3e9b504dfbc589a0',
|
||||
);
|
||||
fireEvent.click(setTopRange);
|
||||
|
||||
setUrlParam('tochange', push2Revision);
|
||||
await waitForElementToBeRemoved(() => getByTestId(push1Id));
|
||||
expect(await pushCount()).toHaveLength(1);
|
||||
expect(history.location.search).toContain(
|
||||
'?repo=autoland&tochange=ba9c692786e95143b8df3f4b3e9b504dfbc589a0',
|
||||
);
|
||||
});
|
||||
|
||||
test('should load N more pushes when click next N', async () => {
|
||||
const { store } = configureStore();
|
||||
const { getByTestId, getAllByTestId } = render(
|
||||
testPushList(store, new FilterModel()),
|
||||
);
|
||||
const { getByTestId, getAllByTestId } = render(testPushList());
|
||||
const nextNUrl = (count) =>
|
||||
getProjectUrl(`/push/?full=true&count=${count + 1}&push_timestamp__lte=`);
|
||||
const clickNext = (count) =>
|
||||
|
@ -260,8 +272,7 @@ describe('PushList', () => {
|
|||
});
|
||||
|
||||
test('jobs should have fields required for retriggers', async () => {
|
||||
const { store } = configureStore();
|
||||
const { getByText } = render(testPushList(store, new FilterModel()));
|
||||
const { getByText } = render(testPushList());
|
||||
const jobEl = await waitFor(() => getByText('yaml'));
|
||||
const jobInstance = findJobInstance(jobEl.getAttribute('data-job-id'));
|
||||
const { job } = jobInstance.props;
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import React from 'react';
|
||||
import fetchMock from 'fetch-mock';
|
||||
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 configureMockStore from 'redux-mock-store';
|
||||
import { createBrowserHistory } from 'history';
|
||||
|
||||
import { replaceLocation, setUrlParam } from '../../../ui/helpers/location';
|
||||
import FilterModel from '../../../ui/models/filter';
|
||||
import SecondaryNavBar from '../../../ui/job-view/headerbars/SecondaryNavBar';
|
||||
import { initialState } from '../../../ui/job-view/redux/stores/pushes';
|
||||
|
@ -13,6 +13,8 @@ import repos from '../mock/repositories';
|
|||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
const repoName = 'autoland';
|
||||
const history = createBrowserHistory();
|
||||
const router = { location: history.location };
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.get('https://treestatus.mozilla-releng.net/trees/autoland', {
|
||||
|
@ -23,31 +25,36 @@ beforeEach(() => {
|
|||
tree: 'autoland',
|
||||
},
|
||||
});
|
||||
setUrlParam('repo', repoName);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
fetchMock.reset();
|
||||
replaceLocation({});
|
||||
history.push('/');
|
||||
});
|
||||
|
||||
describe('SecondaryNavBar', () => {
|
||||
const testSecondaryNavBar = (store, filterModel, props) => (
|
||||
<Provider store={store}>
|
||||
<SecondaryNavBar
|
||||
updateButtonClick={() => {}}
|
||||
serverChanged={false}
|
||||
filterModel={filterModel}
|
||||
repos={repos}
|
||||
setCurrentRepoTreeStatus={() => {}}
|
||||
duplicateJobsVisible={false}
|
||||
groupCountsExpanded={false}
|
||||
toggleFieldFilterVisible={() => {}}
|
||||
{...props}
|
||||
/>
|
||||
</Provider>
|
||||
);
|
||||
const testSecondaryNavBar = (store, props) => {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<SecondaryNavBar
|
||||
updateButtonClick={() => {}}
|
||||
serverChanged={false}
|
||||
filterModel={
|
||||
new FilterModel({
|
||||
pushRoute: history.push,
|
||||
router,
|
||||
})
|
||||
}
|
||||
repos={repos}
|
||||
setCurrentRepoTreeStatus={() => {}}
|
||||
duplicateJobsVisible={false}
|
||||
groupCountsExpanded={false}
|
||||
toggleFieldFilterVisible={() => {}}
|
||||
{...props}
|
||||
/>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
test('should 52 unclassified', async () => {
|
||||
const store = mockStore({
|
||||
|
@ -56,8 +63,9 @@ describe('SecondaryNavBar', () => {
|
|||
allUnclassifiedFailureCount: 52,
|
||||
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('52'))).toBeInTheDocument();
|
||||
|
@ -70,8 +78,9 @@ describe('SecondaryNavBar', () => {
|
|||
allUnclassifiedFailureCount: 22,
|
||||
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('22'))).toBeInTheDocument();
|
||||
|
@ -83,6 +92,7 @@ describe('SecondaryNavBar', () => {
|
|||
pushes: {
|
||||
...initialState,
|
||||
},
|
||||
router,
|
||||
});
|
||||
|
||||
const props = {
|
||||
|
@ -90,9 +100,7 @@ describe('SecondaryNavBar', () => {
|
|||
updateButtonClick: jest.fn(),
|
||||
};
|
||||
|
||||
const { container } = render(
|
||||
testSecondaryNavBar(store, new FilterModel(), props),
|
||||
);
|
||||
const { container } = render(testSecondaryNavBar(store, props));
|
||||
const el = container.querySelector('#revisionChangedLabel');
|
||||
fireEvent.click(el);
|
||||
expect(props.updateButtonClick).toHaveBeenCalled();
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Provider, ReactReduxContext } from 'react-redux';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { render, cleanup, waitFor, fireEvent } from '@testing-library/react';
|
||||
import { ConnectedRouter } from 'connected-react-router';
|
||||
|
||||
import JobModel from '../../../../ui/models/job';
|
||||
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 { getApiUrl } from '../../../../ui/helpers/url';
|
||||
import FilterModel from '../../../../ui/models/filter';
|
||||
import { getProjectUrl } from '../../../../ui/helpers/location';
|
||||
import {
|
||||
replaceLocation,
|
||||
getProjectUrl,
|
||||
} from '../../../../ui/helpers/location';
|
||||
import configureStore from '../../../../ui/job-view/redux/configureStore';
|
||||
history,
|
||||
configureStore,
|
||||
} from '../../../../ui/job-view/redux/configureStore';
|
||||
import { setSelectedJob } from '../../../../ui/job-view/redux/stores/selectedJob';
|
||||
import { setPushes } from '../../../../ui/job-view/redux/stores/pushes';
|
||||
import reposFixture from '../../mock/repositories';
|
||||
|
@ -25,12 +26,12 @@ describe('DetailsPanel', () => {
|
|||
const repoName = 'autoland';
|
||||
const classificationTypes = [{ id: 1, name: 'intermittent' }];
|
||||
const classificationMap = { 1: 'intermittent' };
|
||||
const filterModel = new FilterModel();
|
||||
let jobList = null;
|
||||
let store = null;
|
||||
const currentRepo = reposFixture[2];
|
||||
currentRepo.getRevisionHref = () => 'foo';
|
||||
currentRepo.getPushLogHref = () => 'foo';
|
||||
const router = { location: history.location };
|
||||
|
||||
beforeEach(async () => {
|
||||
fetchMock.get(
|
||||
|
@ -69,40 +70,48 @@ describe('DetailsPanel', () => {
|
|||
'https://firefox-ci-tc.services.mozilla.com/api/queue/v1/task/JFVlnwufR7G9tZu_pKM0dQ',
|
||||
taskDefinition,
|
||||
);
|
||||
store = configureStore().store;
|
||||
store.dispatch(setPushes(pushFixture.results, {}));
|
||||
store = configureStore();
|
||||
store.dispatch(setPushes(pushFixture.results, {}, router));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
fetchMock.reset();
|
||||
replaceLocation({});
|
||||
history.push('/');
|
||||
});
|
||||
|
||||
const testDetailsPanel = (store) => (
|
||||
const testDetailsPanel = () => (
|
||||
<div id="global-container" className="height-minus-navbars">
|
||||
<Provider store={store}>
|
||||
<KeyboardShortcuts
|
||||
filterModel={filterModel}
|
||||
showOnScreenShortcuts={() => {}}
|
||||
>
|
||||
<div />
|
||||
<div id="th-global-content" data-testid="global-content">
|
||||
<DetailsPanel
|
||||
currentRepo={currentRepo}
|
||||
user={{ isLoggedIn: false }}
|
||||
resizedHeight={100}
|
||||
classificationTypes={classificationTypes}
|
||||
classificationMap={classificationMap}
|
||||
/>
|
||||
</div>
|
||||
</KeyboardShortcuts>
|
||||
<Provider store={store} context={ReactReduxContext}>
|
||||
<ConnectedRouter history={history} context={ReactReduxContext}>
|
||||
<KeyboardShortcuts
|
||||
filterModel={
|
||||
new FilterModel({
|
||||
pushRoute: history.push,
|
||||
router,
|
||||
})
|
||||
}
|
||||
showOnScreenShortcuts={() => {}}
|
||||
>
|
||||
<div />
|
||||
<div id="th-global-content" data-testid="global-content">
|
||||
<DetailsPanel
|
||||
currentRepo={currentRepo}
|
||||
user={{ isLoggedIn: false }}
|
||||
resizedHeight={100}
|
||||
classificationTypes={classificationTypes}
|
||||
classificationMap={classificationMap}
|
||||
router={router}
|
||||
/>
|
||||
</div>
|
||||
</KeyboardShortcuts>
|
||||
</ConnectedRouter>
|
||||
</Provider>
|
||||
</div>
|
||||
);
|
||||
|
||||
test('pin selected job with button', async () => {
|
||||
const { getByTitle } = render(testDetailsPanel(store));
|
||||
const { getByTitle } = render(testDetailsPanel());
|
||||
store.dispatch(setSelectedJob(jobList.data[1], true));
|
||||
|
||||
fireEvent.click(await waitFor(() => getByTitle('Pin job')));
|
||||
|
@ -116,7 +125,7 @@ describe('DetailsPanel', () => {
|
|||
});
|
||||
|
||||
test('KeyboardShortcut space: pin selected job', async () => {
|
||||
const { getByTitle } = render(testDetailsPanel(store));
|
||||
const { getByTitle } = render(testDetailsPanel());
|
||||
store.dispatch(setSelectedJob(jobList.data[1], true));
|
||||
|
||||
const content = await waitFor(() =>
|
||||
|
@ -132,7 +141,7 @@ describe('DetailsPanel', () => {
|
|||
});
|
||||
|
||||
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));
|
||||
|
||||
const content = await waitFor(() =>
|
||||
|
@ -151,7 +160,7 @@ describe('DetailsPanel', () => {
|
|||
});
|
||||
|
||||
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));
|
||||
|
||||
const content = await waitFor(() =>
|
||||
|
@ -168,7 +177,7 @@ describe('DetailsPanel', () => {
|
|||
});
|
||||
|
||||
test('KeyboardShortcut ctrl+shift+u: clear PinBoard', async () => {
|
||||
const { getByTitle } = render(testDetailsPanel(store));
|
||||
const { getByTitle } = render(testDetailsPanel());
|
||||
store.dispatch(setSelectedJob(jobList.data[1], true));
|
||||
|
||||
fireEvent.click(await waitFor(() => getByTitle('Pin job')));
|
||||
|
@ -188,7 +197,7 @@ describe('DetailsPanel', () => {
|
|||
});
|
||||
|
||||
test('clear PinBoard', async () => {
|
||||
const { getByTitle, getByText } = render(testDetailsPanel(store));
|
||||
const { getByTitle, getByText } = render(testDetailsPanel());
|
||||
store.dispatch(setSelectedJob(jobList.data[1], true));
|
||||
|
||||
fireEvent.click(await waitFor(() => getByTitle('Pin job')));
|
||||
|
@ -205,7 +214,7 @@ describe('DetailsPanel', () => {
|
|||
});
|
||||
|
||||
test('pin all jobs', async () => {
|
||||
const { queryAllByTitle } = render(testDetailsPanel(store));
|
||||
const { queryAllByTitle } = render(testDetailsPanel());
|
||||
store.dispatch(pinJobs(jobList.data));
|
||||
|
||||
const unPinJobBtns = await waitFor(() => queryAllByTitle('Unpin job'));
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint-disable jest/prefer-to-have-length */
|
||||
import React from 'react';
|
||||
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 FilterModel from '../../../ui/models/filter';
|
||||
|
@ -9,13 +9,20 @@ import mappedGroupFixture from '../mock/mappedGroup';
|
|||
import mappedGroupDupsFixture from '../mock/mappedGroupDups';
|
||||
import { addAggregateFields } from '../../../ui/helpers/job';
|
||||
|
||||
const history = createBrowserHistory();
|
||||
|
||||
describe('JobGroup component', () => {
|
||||
let countGroup;
|
||||
let dupGroup;
|
||||
const repoName = 'mozilla-inbound';
|
||||
const filterModel = new FilterModel();
|
||||
const filterModel = new FilterModel({
|
||||
pushRoute: history.push,
|
||||
router: { location: history.location },
|
||||
});
|
||||
const pushGroupState = 'collapsed';
|
||||
|
||||
afterEach(() => history.push('/'));
|
||||
|
||||
beforeAll(() => {
|
||||
mappedGroupFixture.jobs.forEach((job) => addAggregateFields(job));
|
||||
mappedGroupDupsFixture.jobs.forEach((job) => addAggregateFields(job));
|
||||
|
@ -26,136 +33,105 @@ describe('JobGroup component', () => {
|
|||
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
|
||||
*/
|
||||
it('collapsed should show a job and count of 2', () => {
|
||||
const jobGroup = mount(
|
||||
<JobGroupComponent
|
||||
repoName={repoName}
|
||||
group={countGroup}
|
||||
filterPlatformCb={() => {}}
|
||||
filterModel={filterModel}
|
||||
pushGroupState={pushGroupState}
|
||||
platform={<span>windows</span>}
|
||||
duplicateJobsVisible={false}
|
||||
groupCountsExpanded={false}
|
||||
/>,
|
||||
);
|
||||
it('collapsed should show a job and count of 2 icon when collapsed', async () => {
|
||||
const { getByTestId } = render(jobGroup(countGroup));
|
||||
|
||||
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', () => {
|
||||
const jobGroup = mount(
|
||||
<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 });
|
||||
test('should show a job and count of 2 icon when re-collapsed', async () => {
|
||||
const { getByText, getByTestId } = render(jobGroup(countGroup));
|
||||
|
||||
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', () => {
|
||||
const jobGroup = mount(
|
||||
<JobGroupComponent
|
||||
repoName={repoName}
|
||||
group={countGroup}
|
||||
filterPlatformCb={() => {}}
|
||||
filterModel={filterModel}
|
||||
pushGroupState={pushGroupState}
|
||||
platform={<span>windows</span>}
|
||||
duplicateJobsVisible={false}
|
||||
groupCountsExpanded={false}
|
||||
/>,
|
||||
);
|
||||
jobGroup.setState({ expanded: true });
|
||||
test('should show jobs, not counts when expanded', async () => {
|
||||
const { getByTestId, getAllByTestId } = render(jobGroup(countGroup));
|
||||
|
||||
expect(jobGroup.find('.job-group-count').length).toEqual(0);
|
||||
expect(jobGroup.find('.job-btn').length).toEqual(3);
|
||||
const jobGroupCount = await waitFor(() => getByTestId('job-group-count'));
|
||||
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 jobGroup = mount(
|
||||
<JobGroupComponent
|
||||
repoName={repoName}
|
||||
group={countGroup}
|
||||
filterPlatformCb={() => {}}
|
||||
filterModel={filterModel}
|
||||
pushGroupState={pushGroupState}
|
||||
platform={<span>windows</span>}
|
||||
duplicateJobsVisible={false}
|
||||
groupCountsExpanded={groupCountsExpanded}
|
||||
/>,
|
||||
const { queryByTestId, getAllByTestId } = render(
|
||||
jobGroup(countGroup, groupCountsExpanded),
|
||||
);
|
||||
|
||||
expect(jobGroup.find('.job-btn').length).toEqual(3);
|
||||
expect(jobGroup.find('.job-group-count').length).toEqual(0);
|
||||
const expandedJobs = await waitFor(() => getAllByTestId('job-btn'));
|
||||
expect(expandedJobs).toHaveLength(3);
|
||||
|
||||
const jobGroupCount = await waitFor(() => queryByTestId('job-group-count'));
|
||||
expect(jobGroupCount).toBeNull();
|
||||
});
|
||||
|
||||
test('should hide duplicates by default', () => {
|
||||
const jobGroup = mount(
|
||||
<JobGroupComponent
|
||||
repoName={repoName}
|
||||
group={dupGroup}
|
||||
filterPlatformCb={() => {}}
|
||||
filterModel={filterModel}
|
||||
pushGroupState={pushGroupState}
|
||||
platform={<span>windows</span>}
|
||||
duplicateJobsVisible={false}
|
||||
groupCountsExpanded={false}
|
||||
/>,
|
||||
test('should hide duplicates by default', async () => {
|
||||
const { getAllByTestId } = render(jobGroup(dupGroup));
|
||||
|
||||
const jobGroupCount = await waitFor(() =>
|
||||
getAllByTestId('job-group-count'),
|
||||
);
|
||||
expect(jobGroupCount).toHaveLength(1);
|
||||
|
||||
expect(jobGroup.find('.job-group-count').length).toEqual(1);
|
||||
expect(jobGroup.find('.job-btn').length).toEqual(1);
|
||||
const expandedJobs = await waitFor(() => getAllByTestId('job-btn'));
|
||||
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 jobGroup = mount(
|
||||
<JobGroupComponent
|
||||
repoName={repoName}
|
||||
group={dupGroup}
|
||||
filterPlatformCb={() => {}}
|
||||
filterModel={filterModel}
|
||||
pushGroupState={pushGroupState}
|
||||
platform={<span>windows</span>}
|
||||
duplicateJobsVisible={duplicateJobsVisible}
|
||||
groupCountsExpanded={false}
|
||||
/>,
|
||||
const groupCountsExpanded = false;
|
||||
|
||||
const { getAllByTestId } = render(
|
||||
jobGroup(dupGroup, groupCountsExpanded, duplicateJobsVisible),
|
||||
);
|
||||
|
||||
expect(jobGroup.find('.job-group-count').length).toEqual(1);
|
||||
expect(jobGroup.find('.job-btn').length).toEqual(2);
|
||||
});
|
||||
|
||||
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}
|
||||
/>,
|
||||
const jobGroupCount = await waitFor(() =>
|
||||
getAllByTestId('job-group-count'),
|
||||
);
|
||||
expect(jobGroupCount).toHaveLength(1);
|
||||
|
||||
expect(jobGroup.find('.job-group-count').length).toEqual(1);
|
||||
expect(jobGroup.find('.job-btn').length).toEqual(2);
|
||||
const jobs = await waitFor(() => getAllByTestId('job-btn'));
|
||||
expect(jobs).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
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 { createBrowserHistory } from 'history';
|
||||
import { ConnectedRouter } from 'connected-react-router';
|
||||
|
||||
import PushJobs from '../../../ui/job-view/pushes/PushJobs';
|
||||
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 platforms from '../mock/platforms';
|
||||
import { addAggregateFields } from '../../../ui/helpers/job';
|
||||
|
||||
const history = createBrowserHistory();
|
||||
const testPush = {
|
||||
id: 494796,
|
||||
revision: '1252c6014d122d48c6782310d5c3f4ae742751cb',
|
||||
|
@ -42,25 +45,34 @@ afterEach(() => {
|
|||
setUrlParam('selectedTaskRun', null);
|
||||
});
|
||||
|
||||
const testPushJobs = (filterModel) => (
|
||||
<Provider store={store}>
|
||||
<PushJobs
|
||||
push={testPush}
|
||||
platforms={platforms}
|
||||
repoName="try"
|
||||
filterModel={filterModel}
|
||||
pushGroupState=""
|
||||
toggleSelectedRunnableJob={() => {}}
|
||||
runnableVisible={false}
|
||||
duplicateJobsVisible={false}
|
||||
groupCountsExpanded={false}
|
||||
/>
|
||||
,
|
||||
</Provider>
|
||||
);
|
||||
const testPushJobs = (filtermodel = null) => {
|
||||
const store = configureStore(history);
|
||||
return (
|
||||
<Provider store={store} context={ReactReduxContext}>
|
||||
<ConnectedRouter history={history} context={ReactReduxContext}>
|
||||
<PushJobs
|
||||
push={testPush}
|
||||
platforms={platforms}
|
||||
repoName="try"
|
||||
filterModel={
|
||||
filtermodel ||
|
||||
new FilterModel({
|
||||
router: { location: history.location, push: history.push },
|
||||
})
|
||||
}
|
||||
pushGroupState=""
|
||||
toggleSelectedRunnableJob={() => {}}
|
||||
runnableVisible={false}
|
||||
duplicateJobsVisible={false}
|
||||
groupCountsExpanded={false}
|
||||
/>
|
||||
</ConnectedRouter>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
test('select a job updates url', async () => {
|
||||
const { getByText } = render(testPushJobs(new FilterModel()));
|
||||
const { getByText } = render(testPushJobs());
|
||||
const spell = getByText('spell');
|
||||
|
||||
expect(spell).toBeInTheDocument();
|
||||
|
@ -74,9 +86,12 @@ test('select a job updates url', async () => {
|
|||
});
|
||||
|
||||
test('filter change keeps selected job visible', async () => {
|
||||
const filterModel = new FilterModel();
|
||||
const { getByText, rerender } = render(testPushJobs(filterModel));
|
||||
const { getByText, rerender } = render(testPushJobs());
|
||||
const spell = await waitFor(() => getByText('spell'));
|
||||
const filterModel = new FilterModel({
|
||||
router: { location: history.location },
|
||||
pushRoute: history.push,
|
||||
});
|
||||
|
||||
expect(spell).toBeInTheDocument();
|
||||
|
||||
|
@ -84,7 +99,7 @@ test('filter change keeps selected job visible', async () => {
|
|||
expect(spell).toHaveClass('selected-job');
|
||||
|
||||
filterModel.addFilter('searchStr', 'linux');
|
||||
rerender(testPushJobs(new FilterModel()));
|
||||
rerender(testPushJobs(filterModel));
|
||||
|
||||
const spell2 = getByText('spell');
|
||||
|
||||
|
|
|
@ -2,12 +2,12 @@ import fetchMock from 'fetch-mock';
|
|||
import thunk from 'redux-thunk';
|
||||
import { cleanup } from '@testing-library/react';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import { createBrowserHistory } from 'history';
|
||||
|
||||
import {
|
||||
getProjectUrl,
|
||||
getQueryString,
|
||||
replaceLocation,
|
||||
setUrlParam,
|
||||
updatePushParams,
|
||||
} from '../../../../ui/helpers/location';
|
||||
import pushListFixture from '../../mock/push_list';
|
||||
import pushListFromChangeFixture from '../../mock/pushListFromchange';
|
||||
|
@ -26,12 +26,12 @@ import {
|
|||
reducer,
|
||||
fetchPushes,
|
||||
pollPushes,
|
||||
fetchNextPushes,
|
||||
updateRange,
|
||||
} from '../../../../ui/job-view/redux/stores/pushes';
|
||||
import { getApiUrl } from '../../../../ui/helpers/url';
|
||||
import JobModel from '../../../../ui/models/job';
|
||||
|
||||
const history = createBrowserHistory();
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
const emptyBugzillaResponse = {
|
||||
bugs: [],
|
||||
|
@ -48,7 +48,7 @@ describe('Pushes Redux store', () => {
|
|||
afterEach(() => {
|
||||
cleanup();
|
||||
fetchMock.reset();
|
||||
replaceLocation({});
|
||||
history.push('/');
|
||||
});
|
||||
|
||||
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`,
|
||||
emptyBugzillaResponse,
|
||||
);
|
||||
const store = mockStore({ pushes: initialState });
|
||||
const store = mockStore({
|
||||
pushes: initialState,
|
||||
router: { location: history.location },
|
||||
});
|
||||
|
||||
await store.dispatch(fetchPushes());
|
||||
const actions = store.getActions();
|
||||
|
@ -94,9 +97,15 @@ describe('Pushes Redux store', () => {
|
|||
jobListFixtureTwo,
|
||||
);
|
||||
|
||||
fetchMock.get(
|
||||
`https://bugzilla.mozilla.org/rest/bug?id=1506219`,
|
||||
emptyBugzillaResponse,
|
||||
);
|
||||
|
||||
const initialPush = pushListFixture.results[0];
|
||||
const store = mockStore({
|
||||
pushes: { ...initialState, pushList: [initialPush] },
|
||||
router: { location: history.location },
|
||||
});
|
||||
|
||||
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(
|
||||
getProjectUrl(
|
||||
'/push/?full=true&count=11&push_timestamp__lte=1562867957',
|
||||
|
@ -148,24 +157,29 @@ describe('Pushes Redux store', () => {
|
|||
);
|
||||
|
||||
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({
|
||||
pushes: {
|
||||
...initialState,
|
||||
pushList: [push],
|
||||
oldestPushTimestamp: push.push_timestamp,
|
||||
},
|
||||
router: { location: history.location },
|
||||
});
|
||||
setUrlParam('revision', push.revision);
|
||||
await store.dispatch(fetchNextPushes(10));
|
||||
await store.dispatch(fetchPushes(10, true));
|
||||
|
||||
expect(getQueryString()).toEqual(
|
||||
'tochange=ba9c692786e95143b8df3f4b3e9b504dfbc589a0&fromchange=90da061f588d1315ee4087225d041d7474d9dfd8',
|
||||
expect(window.location.search).toEqual(
|
||||
`?repo=${repoName}&tochange=ba9c692786e95143b8df3f4b3e9b504dfbc589a0&fromchange=90da061f588d1315ee4087225d041d7474d9dfd8`,
|
||||
);
|
||||
});
|
||||
|
||||
test('should pare down to single revision updateRange', async () => {
|
||||
const store = mockStore({
|
||||
pushes: { ...initialState, pushList: pushListFixture.results },
|
||||
router: { location: history.location },
|
||||
});
|
||||
|
||||
await store.dispatch(
|
||||
|
@ -174,7 +188,6 @@ describe('Pushes Redux store', () => {
|
|||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual([
|
||||
{ countPinnedJobs: 0, type: 'CLEAR_JOB' },
|
||||
{
|
||||
type: SET_PUSHES,
|
||||
pushResults: {
|
||||
|
@ -205,6 +218,7 @@ describe('Pushes Redux store', () => {
|
|||
|
||||
const store = mockStore({
|
||||
pushes: initialState,
|
||||
router: { location: history.location },
|
||||
});
|
||||
|
||||
setUrlParam('fromchange', '9692347caff487cdcd889489b8e89a825fe6bbd1');
|
||||
|
@ -267,7 +281,7 @@ describe('Pushes Redux store', () => {
|
|||
});
|
||||
|
||||
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 state = reducer(
|
||||
|
@ -275,7 +289,10 @@ describe('Pushes Redux store', () => {
|
|||
{ 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(reduced.allUnclassifiedFailureCount).toEqual(2);
|
||||
|
|
|
@ -1,50 +1,30 @@
|
|||
import React from 'react';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { Provider } from 'react-redux';
|
||||
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 keyBy from 'lodash/keyBy';
|
||||
import { createBrowserHistory } from 'history';
|
||||
|
||||
import {
|
||||
getUrlParam,
|
||||
replaceLocation,
|
||||
setUrlParam,
|
||||
} from '../../../../ui/helpers/location';
|
||||
import FilterModel from '../../../../ui/models/filter';
|
||||
import {
|
||||
setSelectedJob,
|
||||
setSelectedJobFromQueryString,
|
||||
clearSelectedJob,
|
||||
initialState,
|
||||
reducer,
|
||||
SELECT_JOB,
|
||||
} from '../../../../ui/job-view/redux/stores/selectedJob';
|
||||
import JobGroup from '../../../../ui/job-view/pushes/JobGroup';
|
||||
import group from '../../mock/group_with_jobs';
|
||||
import { getApiUrl } from '../../../../ui/helpers/url';
|
||||
import jobListFixtureOne from '../../mock/job_list/job_1';
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
const jobMap = keyBy(group.jobs, 'id');
|
||||
let notifications = [];
|
||||
const history = createBrowserHistory();
|
||||
|
||||
describe('SelectedJob Redux store', () => {
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
const repoName = 'autoland';
|
||||
const testJobGroup = (store, group, filterModel) => {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<JobGroup
|
||||
group={group}
|
||||
repoName={repoName}
|
||||
filterModel={filterModel}
|
||||
filterPlatformCb={() => {}}
|
||||
pushGroupState="expanded"
|
||||
duplicateJobsVisible={false}
|
||||
groupCountsExpanded
|
||||
/>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
const router = { location: history.location };
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.get(
|
||||
|
@ -55,37 +35,44 @@ describe('SelectedJob Redux store', () => {
|
|||
getApiUrl('/jobs/?task_id=a824gBVmRQSBuEexnVW_Qg&retry_id=0'),
|
||||
{ results: [] },
|
||||
);
|
||||
setUrlParam('repo', repoName);
|
||||
notifications = [];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
fetchMock.reset();
|
||||
replaceLocation({});
|
||||
history.push('/');
|
||||
});
|
||||
|
||||
test('setSelectedJob should select a job', async () => {
|
||||
const store = mockStore({ selectedJob: { initialState } });
|
||||
const taskRun = 'UCctvnxZR0--JcxyVGc8VA.0';
|
||||
const store = mockStore({
|
||||
selectedJob: { initialState },
|
||||
router,
|
||||
});
|
||||
|
||||
render(testJobGroup(store, group, new FilterModel()));
|
||||
|
||||
const reduced = reducer(
|
||||
{ selectedJob: { initialState } },
|
||||
setSelectedJob(group.jobs[0], true),
|
||||
);
|
||||
|
||||
expect(reduced.selectedJob).toEqual(group.jobs[0]);
|
||||
expect(getUrlParam('selectedTaskRun')).toEqual(taskRun);
|
||||
store.dispatch(setSelectedJob(group.jobs[0], true));
|
||||
const actions = store.getActions();
|
||||
expect(actions).toEqual([
|
||||
{
|
||||
job: group.jobs[0],
|
||||
type: SELECT_JOB,
|
||||
updateDetails: true,
|
||||
},
|
||||
{
|
||||
payload: {
|
||||
args: [{ search: `?selectedTaskRun=${taskRun}` }],
|
||||
method: 'push',
|
||||
},
|
||||
type: '@@router/CALL_HISTORY_METHOD',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('setSelectedJobFromQueryString found', async () => {
|
||||
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(
|
||||
{ selectedJob: { initialState } },
|
||||
setSelectedJobFromQueryString(() => {}, jobMap),
|
||||
|
@ -97,9 +84,9 @@ describe('SelectedJob Redux store', () => {
|
|||
test('setSelectedJobFromQueryString not in jobMap', async () => {
|
||||
const taskRun = 'VaQoWKTbSdGSwBJn6UZV9g.0';
|
||||
|
||||
setUrlParam('selectedTaskRun', taskRun);
|
||||
history.push(`/jobs?repo=${repoName}&selectedTaskRun=${taskRun}`);
|
||||
|
||||
const reduced = await reducer(
|
||||
const reduced = reducer(
|
||||
{ selectedJob: { initialState } },
|
||||
setSelectedJobFromQueryString((msg) => notifications.push(msg), jobMap),
|
||||
);
|
||||
|
@ -115,9 +102,9 @@ describe('SelectedJob Redux store', () => {
|
|||
test('setSelectedJobFromQueryString not in DB', async () => {
|
||||
const taskRun = 'a824gBVmRQSBuEexnVW_Qg.0';
|
||||
|
||||
setUrlParam('selectedTaskRun', taskRun);
|
||||
history.push(`/jobs?repo=${repoName}&selectedTaskRun=${taskRun}`);
|
||||
|
||||
const reduced = await reducer(
|
||||
const reduced = reducer(
|
||||
{ selectedJob: { initialState } },
|
||||
setSelectedJobFromQueryString((msg) => notifications.push(msg), jobMap),
|
||||
);
|
||||
|
@ -130,16 +117,30 @@ describe('SelectedJob Redux store', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('clearSelectedJob', () => {
|
||||
const taskRun = 'UCctvnxZR0--JcxyVGc8VA.0';
|
||||
test('clearSelectedJob', async () => {
|
||||
const store = mockStore({
|
||||
selectedJob: { selectedJob: group.jobs[0] },
|
||||
router,
|
||||
});
|
||||
|
||||
setUrlParam('selectedTaskRun', taskRun);
|
||||
|
||||
const reduced = reducer(
|
||||
{ selectedJob: { selectedJob: group.jobs[0] } },
|
||||
clearSelectedJob(0),
|
||||
);
|
||||
|
||||
expect(reduced.selectedJob).toBeNull();
|
||||
store.dispatch(clearSelectedJob(0));
|
||||
const actions = store.getActions();
|
||||
expect(actions).toEqual([
|
||||
{
|
||||
countPinnedJobs: 0,
|
||||
type: 'CLEAR_JOB',
|
||||
},
|
||||
{
|
||||
payload: {
|
||||
args: [
|
||||
{
|
||||
search: '?',
|
||||
},
|
||||
],
|
||||
method: 'push',
|
||||
},
|
||||
type: '@@router/CALL_HISTORY_METHOD',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,19 +1,23 @@
|
|||
import { createBrowserHistory } from 'history';
|
||||
|
||||
import {
|
||||
getFilterUrlParamsWithDefaults,
|
||||
getNonFilterUrlParams,
|
||||
} from '../../../ui/models/filter';
|
||||
|
||||
const history = createBrowserHistory();
|
||||
|
||||
describe('FilterModel', () => {
|
||||
const oldHash = window.location.hash;
|
||||
const prevParams = history.location.search;
|
||||
|
||||
afterEach(() => {
|
||||
window.location.hash = oldHash;
|
||||
history.location.search = prevParams;
|
||||
});
|
||||
|
||||
describe('parsing an old url', () => {
|
||||
it('should parse the repo with defaults', () => {
|
||||
window.location.hash = '?repo=mozilla-inbound';
|
||||
const urlParams = getFilterUrlParamsWithDefaults();
|
||||
history.location.search = '?repo=mozilla-inbound';
|
||||
const urlParams = getFilterUrlParamsWithDefaults(history.location);
|
||||
|
||||
expect(urlParams).toEqual({
|
||||
repo: ['mozilla-inbound'],
|
||||
|
@ -34,12 +38,12 @@ describe('FilterModel', () => {
|
|||
});
|
||||
|
||||
it('should parse resultStatus params', () => {
|
||||
window.location.hash =
|
||||
history.location.search =
|
||||
'?repo=mozilla-inbound&filter-resultStatus=testfailed&' +
|
||||
'filter-resultStatus=busted&filter-resultStatus=exception&' +
|
||||
'filter-resultStatus=success&filter-resultStatus=retry' +
|
||||
'&filter-resultStatus=runnable';
|
||||
const urlParams = getFilterUrlParamsWithDefaults();
|
||||
const urlParams = getFilterUrlParamsWithDefaults(history.location);
|
||||
|
||||
expect(urlParams).toEqual({
|
||||
repo: ['mozilla-inbound'],
|
||||
|
@ -57,11 +61,11 @@ describe('FilterModel', () => {
|
|||
});
|
||||
|
||||
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';
|
||||
const urlParams = {
|
||||
...getNonFilterUrlParams(),
|
||||
...getFilterUrlParamsWithDefaults(),
|
||||
...getNonFilterUrlParams(history.location),
|
||||
...getFilterUrlParamsWithDefaults(history.location),
|
||||
};
|
||||
|
||||
expect(urlParams).toEqual({
|
||||
|
@ -91,8 +95,9 @@ describe('FilterModel', () => {
|
|||
});
|
||||
|
||||
it('should parse job field filters', () => {
|
||||
window.location.hash = '?repo=mozilla-inbound&filter-job_type_name=mochi';
|
||||
const urlParams = getFilterUrlParamsWithDefaults();
|
||||
history.location.search =
|
||||
'?repo=mozilla-inbound&filter-job_type_name=mochi';
|
||||
const urlParams = getFilterUrlParamsWithDefaults(history.location);
|
||||
|
||||
expect(urlParams).toEqual({
|
||||
repo: ['mozilla-inbound'],
|
||||
|
@ -116,10 +121,10 @@ describe('FilterModel', () => {
|
|||
|
||||
describe('parsing a new url', () => {
|
||||
it('should parse resultStatus and searchStr', () => {
|
||||
window.location.hash =
|
||||
history.location.search =
|
||||
'?repo=mozilla-inbound&resultStatus=testfailed,busted,exception,success,retry,runnable&' +
|
||||
'searchStr=linux,x64,debug,build-linux64-base-toolchains%2Fdebug,(bb)';
|
||||
const urlParams = getFilterUrlParamsWithDefaults();
|
||||
const urlParams = getFilterUrlParamsWithDefaults(history.location);
|
||||
|
||||
expect(urlParams).toEqual({
|
||||
repo: ['mozilla-inbound'],
|
||||
|
@ -144,9 +149,9 @@ describe('FilterModel', () => {
|
|||
});
|
||||
|
||||
it('should preserve the case in email addresses', () => {
|
||||
window.location.hash =
|
||||
history.location.search =
|
||||
'?repo=mozilla-inbound&author=VYV03354@nifty.ne.jp';
|
||||
const urlParams = getFilterUrlParamsWithDefaults();
|
||||
const urlParams = getFilterUrlParamsWithDefaults(history.location);
|
||||
|
||||
expect(urlParams).toEqual({
|
||||
repo: ['mozilla-inbound'],
|
||||
|
|
|
@ -2,14 +2,16 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
render,
|
||||
cleanup,
|
||||
fireEvent,
|
||||
waitFor,
|
||||
waitForElementToBeRemoved,
|
||||
} from '@testing-library/react';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { createBrowserHistory } from 'history';
|
||||
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 {
|
||||
backfillRetriggeredTitle,
|
||||
unknownFrameworkMessage,
|
||||
|
@ -26,6 +28,8 @@ import testAlertSummaries from '../../mock/alert_summaries';
|
|||
import testPerformanceTags from '../../mock/performance_tags';
|
||||
import TagsList from '../../../../ui/perfherder/alerts/TagsList';
|
||||
|
||||
const history = createBrowserHistory();
|
||||
|
||||
const testUser = {
|
||||
username: 'mozilla-ldap/test_user@mozilla.com',
|
||||
isLoggedIn: true,
|
||||
|
@ -64,7 +68,7 @@ const testIssueTrackers = [
|
|||
|
||||
const testActiveTags = ['first-tag', 'second-tag'];
|
||||
|
||||
afterEach(cleanup);
|
||||
afterEach(() => history.push('/alerts'));
|
||||
|
||||
const mockModifyAlert = {
|
||||
update(alert, params) {
|
||||
|
@ -78,69 +82,74 @@ const mockModifyAlert = {
|
|||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const mockUpdateAlertSummary = (alertSummaryId, params) => ({
|
||||
failureStatus: null,
|
||||
});
|
||||
const alertsView = () =>
|
||||
render(
|
||||
<AlertsView
|
||||
user={testUser}
|
||||
projects={repos}
|
||||
location={{
|
||||
pathname: '/alerts',
|
||||
search: '',
|
||||
}}
|
||||
history={createMemoryHistory('/alerts')}
|
||||
frameworks={frameworks}
|
||||
performanceTags={testPerformanceTags}
|
||||
/>,
|
||||
const alertsView = () => {
|
||||
const store = configureStore(history);
|
||||
|
||||
return render(
|
||||
<Provider store={store} context={ReactReduxContext}>
|
||||
<ConnectedRouter history={history} context={ReactReduxContext}>
|
||||
<AlertsView
|
||||
user={testUser}
|
||||
projects={repos}
|
||||
location={history.location}
|
||||
frameworks={frameworks}
|
||||
performanceTags={testPerformanceTags}
|
||||
history={history}
|
||||
/>
|
||||
</ConnectedRouter>
|
||||
</Provider>,
|
||||
);
|
||||
};
|
||||
|
||||
const alertsViewControls = ({
|
||||
isListMode = true,
|
||||
user: userMock = null,
|
||||
} = {}) => {
|
||||
const user = userMock !== null ? userMock : testUser;
|
||||
const store = configureStore(history);
|
||||
|
||||
return render(
|
||||
<AlertsViewControls
|
||||
validated={{
|
||||
hideDwnToInv: undefined,
|
||||
hideImprovements: undefined,
|
||||
filter: undefined,
|
||||
updateParams: () => {},
|
||||
}}
|
||||
isListMode={isListMode}
|
||||
alertSummaries={testAlertSummaries}
|
||||
issueTrackers={testIssueTrackers}
|
||||
optionCollectionMap={optionCollectionMap}
|
||||
fetchAlertSummaries={() => {}}
|
||||
updateViewState={() => {}}
|
||||
user={user}
|
||||
modifyAlert={(alert, params) => mockModifyAlert.update(alert, params)}
|
||||
updateAlertSummary={() =>
|
||||
Promise.resolve({ failureStatus: false, data: 'alert summary data' })
|
||||
}
|
||||
projects={repos}
|
||||
location={{
|
||||
pathname: '/alerts',
|
||||
search: '',
|
||||
}}
|
||||
filters={{
|
||||
filterText: '',
|
||||
hideImprovements: false,
|
||||
hideDownstream: false,
|
||||
hideAssignedToOthers: false,
|
||||
framework: { name: 'talos', id: 1 },
|
||||
status: 'untriaged',
|
||||
}}
|
||||
frameworks={[{ id: 1, name: dummyFrameworkName }]}
|
||||
history={createMemoryHistory('/alerts')}
|
||||
frameworkOptions={[ignoreFrameworkOption, ...frameworks]}
|
||||
setFiltersState={() => {}}
|
||||
performanceTags={testPerformanceTags}
|
||||
/>,
|
||||
<Provider store={store} context={ReactReduxContext}>
|
||||
<ConnectedRouter history={history} context={ReactReduxContext}>
|
||||
<AlertsViewControls
|
||||
validated={{
|
||||
hideDwnToInv: undefined,
|
||||
hideImprovements: undefined,
|
||||
filter: undefined,
|
||||
updateParams: () => {},
|
||||
}}
|
||||
isListMode={isListMode}
|
||||
alertSummaries={testAlertSummaries}
|
||||
issueTrackers={testIssueTrackers}
|
||||
optionCollectionMap={optionCollectionMap}
|
||||
fetchAlertSummaries={() => {}}
|
||||
updateViewState={() => {}}
|
||||
user={user}
|
||||
modifyAlert={(alert, params) => mockModifyAlert.update(alert, params)}
|
||||
updateAlertSummary={() =>
|
||||
Promise.resolve({
|
||||
failureStatus: false,
|
||||
data: 'alert summary data',
|
||||
})
|
||||
}
|
||||
projects={repos}
|
||||
location={history.location}
|
||||
filters={{
|
||||
filterText: '',
|
||||
hideImprovements: false,
|
||||
hideDownstream: false,
|
||||
hideAssignedToOthers: false,
|
||||
framework: { name: 'talos', id: 1 },
|
||||
status: 'untriaged',
|
||||
}}
|
||||
frameworks={[{ id: 1, name: dummyFrameworkName }]}
|
||||
frameworkOptions={[ignoreFrameworkOption, ...frameworks]}
|
||||
setFiltersState={() => {}}
|
||||
performanceTags={testPerformanceTags}
|
||||
history={history}
|
||||
/>
|
||||
</ConnectedRouter>
|
||||
</Provider>,
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -7,15 +7,20 @@ import {
|
|||
getAllByTestId,
|
||||
queryAllByTestId,
|
||||
} 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 pushHealth from '../mock/push_health';
|
||||
import reposFixture from '../mock/repositories';
|
||||
import { getApiUrl } from '../../../ui/helpers/url';
|
||||
import { getProjectUrl } from '../../../ui/helpers/location';
|
||||
import { configureStore } from '../../../ui/job-view/redux/configureStore';
|
||||
|
||||
const revision = 'cd02b96bdce57d9ae53b632ca4740c871d3ecc32';
|
||||
const repo = 'autoland';
|
||||
const history = createBrowserHistory();
|
||||
|
||||
describe('Health', () => {
|
||||
beforeAll(() => {
|
||||
|
@ -114,14 +119,19 @@ describe('Health', () => {
|
|||
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 () => {
|
||||
window.history.replaceState(
|
||||
{},
|
||||
'Push Health Test',
|
||||
`${window.location.origin}?repo=${repo}&revision=${revision}`,
|
||||
);
|
||||
history.push(`/push-health?repo=${repo}&revision=${revision}`);
|
||||
|
||||
const health = render(testHealth());
|
||||
const classificationGroups = await waitFor(() =>
|
||||
|
@ -141,10 +151,8 @@ describe('Health', () => {
|
|||
});
|
||||
|
||||
test('should filter groups by test path string', async () => {
|
||||
window.history.replaceState(
|
||||
{},
|
||||
'Push Health Test',
|
||||
`${window.location.origin}?repo=${repo}&revision=${revision}&searchStr=browser/extensions/`,
|
||||
history.push(
|
||||
`/push-health?repo=${repo}&revision=${revision}&searchStr=browser/extensions/`,
|
||||
);
|
||||
const health = render(testHealth());
|
||||
const classificationGroups = await waitFor(() =>
|
||||
|
|
|
@ -30,7 +30,7 @@ describe('Job', () => {
|
|||
const job = await waitFor(() => getByText('R1'));
|
||||
|
||||
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');
|
||||
});
|
||||
|
@ -40,7 +40,7 @@ describe('Job', () => {
|
|||
const job = await waitFor(() => getByText('bc6'));
|
||||
|
||||
expect(job.getAttribute('href')).toBe(
|
||||
'/#/jobs?selectedJob=285859045&repo=try&revision=cd02b96bdce57d9ae53b632ca4740c871d3ecc32',
|
||||
'/jobs?selectedJob=285859045&repo=try&revision=cd02b96bdce57d9ae53b632ca4740c871d3ecc32',
|
||||
);
|
||||
expect(job).toHaveClass('btn-green');
|
||||
});
|
||||
|
@ -50,7 +50,7 @@ describe('Job', () => {
|
|||
const job = await waitFor(() => getByText('arm64'));
|
||||
|
||||
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(getByText('Failed in parent')).toBeInTheDocument();
|
||||
|
|
|
@ -126,14 +126,6 @@ MIDDLEWARE = [
|
|||
if middleware
|
||||
]
|
||||
|
||||
# Templating
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'APP_DIRS': True,
|
||||
}
|
||||
]
|
||||
|
||||
# Database
|
||||
# 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.
|
||||
WHITENOISE_KEEP_ONLY_HASHED_FILES = True
|
||||
|
||||
# Templating
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'APP_DIRS': True,
|
||||
'DIRS': [WHITENOISE_ROOT],
|
||||
}
|
||||
]
|
||||
|
||||
# TREEHERDER
|
||||
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
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 treeherder.webapp.api import urls as api_urls
|
||||
from django.views.generic.base import TemplateView
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^api/', include(api_urls)),
|
||||
url(r'^docs/', include_docs_urls(title='REST API Docs')),
|
||||
re_path(r'', TemplateView.as_view(template_name='index.html')),
|
||||
]
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
.tooltip > .tooltip-inner {
|
||||
.custom-tooltip {
|
||||
background-color: lightgray;
|
||||
color: black;
|
||||
font-size: 14px;
|
||||
|
|
|
@ -21,8 +21,6 @@
|
|||
|
||||
.push-title-left {
|
||||
flex: 0 0 24.2em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import pick from 'lodash/pick';
|
|||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
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
|
||||
// job field.
|
||||
|
@ -99,14 +99,8 @@ export const hasUrlFilterChanges = function hasUrlFilterChanges(
|
|||
oldURL,
|
||||
newURL,
|
||||
) {
|
||||
const oldFilters = pick(
|
||||
parseQueryParams(extractSearchString(oldURL)),
|
||||
allFilterParams,
|
||||
);
|
||||
const newFilters = pick(
|
||||
parseQueryParams(extractSearchString(newURL)),
|
||||
allFilterParams,
|
||||
);
|
||||
const oldFilters = pick(parseQueryParams(oldURL), allFilterParams);
|
||||
const newFilters = pick(parseQueryParams(newURL), allFilterParams);
|
||||
|
||||
return !isEqual(oldFilters, newFilters);
|
||||
};
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { thFailureResults, thPlatformMap } from './constants';
|
||||
import { getGroupMapKey } from './aggregateId';
|
||||
import { getAllUrlParams, getRepo } from './location';
|
||||
import { uiJobsUrlBase } from './url';
|
||||
|
||||
const btnClasses = {
|
||||
busted: 'btn-red',
|
||||
|
@ -236,7 +235,7 @@ export const getJobSearchStrHref = function getJobSearchStrHref(jobSearchStr) {
|
|||
const params = getAllUrlParams();
|
||||
params.set('searchStr', jobSearchStr.split(' '));
|
||||
|
||||
return `${uiJobsUrlBase}?${params.toString()}`;
|
||||
return `?${params.toString()}`;
|
||||
};
|
||||
|
||||
export const getTaskRunStr = (job) => `${job.task_id}.${job.retry_id}`;
|
||||
|
|
|
@ -1,17 +1,10 @@
|
|||
import { thDefaultRepo } from './constants';
|
||||
import {
|
||||
createQueryParams,
|
||||
extractSearchString,
|
||||
getApiUrl,
|
||||
uiJobsUrlBase,
|
||||
} from './url';
|
||||
import { createQueryParams, getApiUrl } from './url';
|
||||
|
||||
export const getQueryString = function getQueryString() {
|
||||
return extractSearchString(window.location.href);
|
||||
};
|
||||
|
||||
export const getAllUrlParams = function getAllUrlParams() {
|
||||
return new URLSearchParams(getQueryString());
|
||||
export const getAllUrlParams = function getAllUrlParams(
|
||||
location = window.location,
|
||||
) {
|
||||
return new URLSearchParams(location.search);
|
||||
};
|
||||
|
||||
export const getUrlParam = function getUrlParam(name) {
|
||||
|
@ -22,27 +15,12 @@ export const getRepo = function getRepo() {
|
|||
return getUrlParam('repo') || thDefaultRepo;
|
||||
};
|
||||
|
||||
export const setLocation = function setLocation(params, hashPrefix = '/jobs') {
|
||||
window.location.hash = `#${hashPrefix}${createQueryParams(params)}`;
|
||||
// This won't update the react router history object
|
||||
export const replaceLocation = function replaceLocation(params) {
|
||||
window.history.pushState(null, null, createQueryParams(params));
|
||||
};
|
||||
|
||||
// change the url hash without firing a ``hashchange`` event.
|
||||
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',
|
||||
) {
|
||||
export const setUrlParam = function setUrlParam(field, value) {
|
||||
const params = getAllUrlParams();
|
||||
|
||||
if (value) {
|
||||
|
@ -50,10 +28,25 @@ export const setUrlParam = function setUrlParam(
|
|||
} else {
|
||||
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();
|
||||
|
||||
params.delete('selectedJob');
|
||||
|
@ -62,7 +55,7 @@ export const getRepoUrl = function getRepoUrl(newRepoName) {
|
|||
params.delete('revision');
|
||||
params.delete('author');
|
||||
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
|
||||
|
@ -77,3 +70,23 @@ export const getProjectUrl = function getProjectUrl(uri, repoName) {
|
|||
export const getProjectJobUrl = function getProjectJobUrl(url, jobId) {
|
||||
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 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
|
||||
localStorage.setItem('requestState', nonce);
|
||||
localStorage.setItem('tcRootUrl', _rootUrl);
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
// https://github.com/mozilla/treeherder/blob/master/treeherder/middleware.py
|
||||
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/';
|
||||
|
||||
|
@ -21,7 +21,7 @@ export const graphsEndpoint = '/failurecount/';
|
|||
|
||||
export const deployedRevisionUrl = '/revision.txt';
|
||||
|
||||
export const loginCallbackUrl = '/login.html';
|
||||
export const loginCallbackUrl = '/login';
|
||||
|
||||
export const platformsEndpoint = '/machineplatforms/';
|
||||
|
||||
|
@ -31,7 +31,7 @@ export const investigatedTestsEndPoint = '/investigated-tests/';
|
|||
|
||||
export const repoEndpoint = '/repository/';
|
||||
|
||||
export const tcAuthCallbackUrl = '/taskcluster-auth.html';
|
||||
export const tcAuthCallbackUrl = '/taskcluster-auth';
|
||||
|
||||
export const textLogErrorsEndpoint = '/text_log_errors/';
|
||||
|
||||
|
@ -105,7 +105,7 @@ export const getLogViewerUrl = function getLogViewerUrl(
|
|||
repoName,
|
||||
lineNumber,
|
||||
) {
|
||||
const rv = `logviewer.html#?job_id=${jobId}&repo=${repoName}`;
|
||||
const rv = `/logviewer?job_id=${jobId}&repo=${repoName}`;
|
||||
return lineNumber ? `${rv}&lineNumber=${lineNumber}` : rv;
|
||||
};
|
||||
|
||||
|
@ -124,7 +124,7 @@ export const getPushHealthUrl = function getPushHealthUrl(params) {
|
|||
};
|
||||
|
||||
export const getCompareChooserUrl = function getCompareChooserUrl(params) {
|
||||
return `perf.html#/comparechooser${createQueryParams(params)}`;
|
||||
return `/perfherder/comparechooser${createQueryParams(params)}`;
|
||||
};
|
||||
|
||||
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
|
||||
export const createApiUrl = function createApiUrl(api, params) {
|
||||
const apiUrl = getApiUrl(api);
|
||||
|
@ -163,7 +157,5 @@ export const updateQueryParams = function updateHistoryWithQueryParams(
|
|||
history,
|
||||
location,
|
||||
) {
|
||||
history.replace({ pathname: location.pathname, search: queryParams });
|
||||
// we do this so the api's won't be called twice (location/history updates will trigger a lifecycle hook)
|
||||
location.search = queryParams;
|
||||
history.push({ pathname: location.pathname, 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 { HashRouter, Route, Switch, Redirect } from 'react-router-dom';
|
||||
import { Route, Switch, Redirect } from 'react-router-dom';
|
||||
import { Container } from 'reactstrap';
|
||||
import { hot } from 'react-hot-loader/root';
|
||||
|
||||
|
@ -8,7 +8,11 @@ import ErrorMessages from '../shared/ErrorMessages';
|
|||
import MainView from './MainView';
|
||||
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) {
|
||||
super(props);
|
||||
|
||||
|
@ -28,54 +32,56 @@ class App extends React.Component {
|
|||
|
||||
render() {
|
||||
const { user, graphData, tableData, errorMessages } = this.state;
|
||||
const { path } = this.props.match;
|
||||
return (
|
||||
<HashRouter>
|
||||
<main>
|
||||
{errorMessages.length > 0 && (
|
||||
<Container className="pt-5 max-width-default">
|
||||
<ErrorMessages errorMessages={errorMessages} />
|
||||
</Container>
|
||||
)}
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path="/main"
|
||||
render={(props) => (
|
||||
<MainView
|
||||
{...props}
|
||||
mainGraphData={graphData}
|
||||
mainTableData={tableData}
|
||||
updateAppState={this.updateAppState}
|
||||
user={user}
|
||||
setUser={(user) => this.setState({ user })}
|
||||
notify={(message) =>
|
||||
this.setState({ errorMessages: [message] })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path="/main?startday=:startday&endday=:endday&tree=:tree"
|
||||
render={(props) => (
|
||||
<MainView
|
||||
{...props}
|
||||
mainGraphData={graphData}
|
||||
mainTableData={tableData}
|
||||
updateAppState={this.updateAppState}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route path="/bugdetails" component={BugDetailsView} />
|
||||
<Route
|
||||
path="/bugdetails?startday=:startday&endday=:endday&tree=:tree&bug=bug"
|
||||
component={BugDetailsView}
|
||||
/>
|
||||
<Redirect from="/" to="/main" />
|
||||
</Switch>
|
||||
</main>
|
||||
</HashRouter>
|
||||
<main>
|
||||
{errorMessages.length > 0 && (
|
||||
<Container className="pt-5 max-width-default">
|
||||
<ErrorMessages errorMessages={errorMessages} />
|
||||
</Container>
|
||||
)}
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={`${path}/main`}
|
||||
render={(props) => (
|
||||
<MainView
|
||||
{...props}
|
||||
mainGraphData={graphData}
|
||||
mainTableData={tableData}
|
||||
updateAppState={this.updateAppState}
|
||||
user={user}
|
||||
setUser={(user) => this.setState({ user })}
|
||||
notify={(message) =>
|
||||
this.setState({ errorMessages: [message] })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`${path}/main?startday=:startday&endday=:endday&tree=:tree`}
|
||||
render={(props) => (
|
||||
<MainView
|
||||
{...props}
|
||||
mainGraphData={graphData}
|
||||
mainTableData={tableData}
|
||||
updateAppState={this.updateAppState}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`${path}/bugdetails`}
|
||||
render={(props) => <BugDetailsView {...props} />}
|
||||
/>
|
||||
<Route
|
||||
path={`${path}/bugdetails?startday=:startday&endday=:endday&tree=:tree&bug=bug`}
|
||||
render={(props) => <BugDetailsView {...props} />}
|
||||
/>
|
||||
<Redirect from={`${path}/`} to={`${path}/main`} />
|
||||
</Switch>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default hot(App);
|
||||
export default hot(IntermittentFailuresApp);
|
||||
|
|
|
@ -34,7 +34,7 @@ function BugColumn({
|
|||
className="ml-1 small-text bug-details"
|
||||
onClick={() => updateAppState({ graphData, tableData })}
|
||||
to={{
|
||||
pathname: '/bugdetails',
|
||||
pathname: '/intermittent-failures/bugdetails',
|
||||
search: `?startday=${startday}&endday=${endday}&tree=${tree}&bug=${id}`,
|
||||
state: { startday, endday, tree, id, summary, location },
|
||||
}}
|
||||
|
|
|
@ -124,6 +124,7 @@ const BugDetailsView = (props) => {
|
|||
</ul>
|
||||
)
|
||||
}
|
||||
innerClassName="custom-tooltip"
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
@ -152,14 +153,14 @@ const BugDetailsView = (props) => {
|
|||
<Col xs="12" className="text-left">
|
||||
<Breadcrumb listClassName="bg-white">
|
||||
<BreadcrumbItem>
|
||||
<a title="Treeherder home page" href="/#/">
|
||||
<a title="Treeherder home page" href="/">
|
||||
Treeherder
|
||||
</a>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbItem>
|
||||
<Link
|
||||
title="Intermittent Failures View main page"
|
||||
to={lastLocation || '/'}
|
||||
to={lastLocation || '/intermittent-failures/'}
|
||||
>
|
||||
Main view
|
||||
</Link>
|
||||
|
|
|
@ -127,7 +127,7 @@ const MainView = (props) => {
|
|||
<Col xs="12" className="text-left">
|
||||
<Breadcrumb listClassName="bg-white">
|
||||
<BreadcrumbItem>
|
||||
<a title="Treeherder home page" href="/#/">
|
||||
<a title="Treeherder home page" href="/">
|
||||
Treeherder
|
||||
</a>
|
||||
</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 pick from 'lodash/pick';
|
||||
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 { hasUrlFilterChanges, matchesDefaults } from '../helpers/filter';
|
||||
import { getAllUrlParams, getRepo } from '../helpers/location';
|
||||
import { matchesDefaults } from '../helpers/filter';
|
||||
import { getAllUrlParams } from '../helpers/location';
|
||||
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 FilterModel from '../models/filter';
|
||||
import RepositoryModel from '../models/repository';
|
||||
|
@ -24,8 +30,19 @@ import { PUSH_HEALTH_VISIBILITY } from './headerbars/HealthMenu';
|
|||
import DetailsPanel from './details/DetailsPanel';
|
||||
import PushList from './pushes/PushList';
|
||||
import KeyboardShortcuts from './KeyboardShortcuts';
|
||||
import { store } from './redux/store';
|
||||
import { CLEAR_EXPIRED_TRANSIENTS } from './redux/stores/notifications';
|
||||
import { clearExpiredNotifications } 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 REVISION_POLL_INTERVAL = 1000 * 60 * 5;
|
||||
|
@ -51,15 +68,13 @@ class App extends React.Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const filterModel = new FilterModel();
|
||||
// Set the URL to updated parameter styles, if needed. Otherwise it's a no-op.
|
||||
filterModel.push();
|
||||
const filterModel = new FilterModel(this.props);
|
||||
const urlParams = getAllUrlParams();
|
||||
const hasSelectedJob =
|
||||
urlParams.has('selectedJob') || urlParams.has('selectedTaskRun');
|
||||
|
||||
this.state = {
|
||||
repoName: getRepo(),
|
||||
repoName: this.getOrSetRepo(),
|
||||
revision: urlParams.get('revision'),
|
||||
user: { isLoggedIn: false, isStaff: false },
|
||||
filterModel,
|
||||
|
@ -82,7 +97,6 @@ class App extends React.Component {
|
|||
static getDerivedStateFromProps(props, state) {
|
||||
return {
|
||||
...App.getSplitterDimensions(state.hasSelectedJob),
|
||||
repoName: getRepo(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -103,7 +117,6 @@ class App extends React.Component {
|
|||
});
|
||||
|
||||
window.addEventListener('resize', this.updateDimensions, false);
|
||||
window.addEventListener('hashchange', this.handleUrlChanges, false);
|
||||
window.addEventListener('storage', this.handleStorageEvent);
|
||||
window.addEventListener(thEvents.filtersUpdated, this.handleFiltersUpdated);
|
||||
|
||||
|
@ -145,14 +158,25 @@ class App extends React.Component {
|
|||
|
||||
// clear expired notifications
|
||||
this.notificationInterval = setInterval(() => {
|
||||
store.dispatch({ type: CLEAR_EXPIRED_TRANSIENTS });
|
||||
this.props.clearExpiredNotifications();
|
||||
}, MAX_TRANSIENT_AGE);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
prevProps.router.location.search !== this.props.router.location.search
|
||||
) {
|
||||
this.handleUrlChanges();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.updateDimensions, false);
|
||||
window.removeEventListener('hashchange', this.handleUrlChanges, false);
|
||||
window.removeEventListener('storage', this.handleUrlChanges, false);
|
||||
window.removeEventListener('storage', this.handleStorageEvent);
|
||||
window.removeEventListener(
|
||||
thEvents.filtersUpdated,
|
||||
this.handleFiltersUpdated,
|
||||
);
|
||||
|
||||
if (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) => {
|
||||
if (e.key === PUSH_HEALTH_VISIBILITY) {
|
||||
this.setState({
|
||||
|
@ -200,9 +251,7 @@ class App extends React.Component {
|
|||
};
|
||||
|
||||
getAllShownJobs = (pushId) => {
|
||||
const {
|
||||
pushes: { jobMap },
|
||||
} = store.getState();
|
||||
const { jobMap } = this.props;
|
||||
const jobList = Object.values(jobMap);
|
||||
|
||||
return pushId
|
||||
|
@ -222,32 +271,32 @@ class App extends React.Component {
|
|||
);
|
||||
};
|
||||
|
||||
handleUrlChanges = (ev) => {
|
||||
handleUrlChanges = () => {
|
||||
const { repos } = this.state;
|
||||
const { newURL, oldURL } = ev;
|
||||
const urlParams = getAllUrlParams();
|
||||
const newRepo = urlParams.get('repo');
|
||||
// We only want to set state if any of these or the filter values have changed
|
||||
const { router } = this.props;
|
||||
|
||||
const {
|
||||
selectedJob,
|
||||
selectedTaskRun,
|
||||
group_state: groupState,
|
||||
duplicate_jobs: duplicateJobs,
|
||||
repo: newRepo,
|
||||
} = parseQueryParams(router.location.search);
|
||||
|
||||
const newState = {
|
||||
hasSelectedJob:
|
||||
urlParams.has('selectedJob') || urlParams.has('selectedTaskRun'),
|
||||
groupCountsExpanded: urlParams.get('group_state') === 'expanded',
|
||||
duplicateJobsVisible: urlParams.get('duplicate_jobs') === 'visible',
|
||||
hasSelectedJob: selectedJob || selectedTaskRun,
|
||||
groupCountsExpanded: groupState === 'expanded',
|
||||
duplicateJobsVisible: duplicateJobs === 'visible',
|
||||
currentRepo: repos.find((repo) => repo.name === newRepo),
|
||||
};
|
||||
|
||||
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)) {
|
||||
this.setState(newState);
|
||||
stateChanges = { ...stateChanges, ...newState };
|
||||
}
|
||||
};
|
||||
|
||||
handleFiltersUpdated = () => {
|
||||
this.setState({ filterModel: new FilterModel() });
|
||||
this.setState(stateChanges);
|
||||
};
|
||||
|
||||
// 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 (
|
||||
<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}
|
||||
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
|
||||
repos={repos}
|
||||
updateButtonClick={this.updateButtonClick}
|
||||
serverChanged={serverChanged}
|
||||
filterModel={filterModel}
|
||||
setUser={this.setUser}
|
||||
user={user}
|
||||
setCurrentRepoTreeStatus={this.setCurrentRepoTreeStatus}
|
||||
getAllShownJobs={this.getAllShownJobs}
|
||||
duplicateJobsVisible={duplicateJobsVisible}
|
||||
groupCountsExpanded={groupCountsExpanded}
|
||||
toggleFieldFilterVisible={this.toggleFieldFilterVisible}
|
||||
pushHealthVisibility={pushHealthVisibility}
|
||||
setPushHealthVisibility={this.setPushHealthVisibility}
|
||||
/>
|
||||
<SplitPane
|
||||
split="horizontal"
|
||||
size={`${pushListPct}%`}
|
||||
onChange={(size) => this.handleSplitChange(size)}
|
||||
>
|
||||
<div className="d-flex flex-column w-100">
|
||||
{(isFieldFilterVisible || !!filterBarFilters.length) && (
|
||||
<ActiveFilters
|
||||
classificationTypes={classificationTypes}
|
||||
filterModel={filterModel}
|
||||
filterBarFilters={filterBarFilters}
|
||||
isFieldFilterVisible={isFieldFilterVisible}
|
||||
toggleFieldFilterVisible={this.toggleFieldFilterVisible}
|
||||
/>
|
||||
)}
|
||||
{serverChangedDelayed && (
|
||||
<UpdateAvailable updateButtonClick={this.updateButtonClick} />
|
||||
)}
|
||||
{currentRepo && (
|
||||
<div id="th-global-content" className="th-global-content">
|
||||
<span className="th-view-content" tabIndex={-1}>
|
||||
<PushList
|
||||
user={user}
|
||||
repoName={repoName}
|
||||
revision={revision}
|
||||
currentRepo={currentRepo}
|
||||
filterModel={filterModel}
|
||||
duplicateJobsVisible={duplicateJobsVisible}
|
||||
groupCountsExpanded={groupCountsExpanded}
|
||||
pushHealthVisibility={pushHealthVisibility}
|
||||
getAllShownJobs={this.getAllShownJobs}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<>
|
||||
{currentRepo && (
|
||||
<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 className="d-flex flex-column w-100">
|
||||
{(isFieldFilterVisible || !!filterBarFilters.length) && (
|
||||
<ActiveFilters
|
||||
classificationTypes={classificationTypes}
|
||||
filterModel={filterModel}
|
||||
filterBarFilters={filterBarFilters}
|
||||
isFieldFilterVisible={isFieldFilterVisible}
|
||||
toggleFieldFilterVisible={this.toggleFieldFilterVisible}
|
||||
/>
|
||||
)}
|
||||
{serverChangedDelayed && (
|
||||
<UpdateAvailable updateButtonClick={this.updateButtonClick} />
|
||||
)}
|
||||
{currentRepo && (
|
||||
<div id="th-global-content" className="th-global-content">
|
||||
<span className="th-view-content" tabIndex={-1}>
|
||||
<PushList
|
||||
user={user}
|
||||
repoName={repoName}
|
||||
revision={revision}
|
||||
currentRepo={currentRepo}
|
||||
filterModel={filterModel}
|
||||
duplicateJobsVisible={duplicateJobsVisible}
|
||||
groupCountsExpanded={groupCountsExpanded}
|
||||
pushHealthVisibility={pushHealthVisibility}
|
||||
getAllShownJobs={this.getAllShownJobs}
|
||||
{...this.props}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<>
|
||||
{currentRepo && (
|
||||
<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>
|
||||
</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
|
||||
// selectedJobFull data as transient only when the job is selected.
|
||||
const selectedJobFull = jobResult;
|
||||
const jobRevision = push.revision;
|
||||
const jobRevision = push ? push.revision : null;
|
||||
|
||||
addAggregateFields(selectedJobFull);
|
||||
|
||||
|
@ -251,7 +251,7 @@ class DetailsPanel extends React.Component {
|
|||
...d,
|
||||
}))
|
||||
.map((d) => ({
|
||||
url: `/perf.html#/graphs?series=${[
|
||||
url: `/perfherder/graphs?series=${[
|
||||
currentRepo.name,
|
||||
d.signature_id,
|
||||
1,
|
||||
|
|
|
@ -3,10 +3,13 @@ import PropTypes from 'prop-types';
|
|||
import { Button } from 'reactstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
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';
|
||||
|
||||
export default class ActiveFilters extends React.Component {
|
||||
class ActiveFilters extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
@ -70,8 +73,32 @@ export default class ActiveFilters extends React.Component {
|
|||
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() {
|
||||
const { isFieldFilterVisible, filterModel, filterBarFilters } = this.props;
|
||||
const { isFieldFilterVisible, filterBarFilters } = this.props;
|
||||
const {
|
||||
newFilterField,
|
||||
newFilterMatchType,
|
||||
|
@ -89,7 +116,7 @@ export default class ActiveFilters extends React.Component {
|
|||
outline
|
||||
className="pointable bg-transparent border-0 pt-0 pr-1 pb-1"
|
||||
title="Clear all of these filters"
|
||||
onClick={filterModel.clearNonStatusFilters}
|
||||
onClick={() => this.clearAndUpdateRange()}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faTimesCircle}
|
||||
|
@ -111,13 +138,13 @@ export default class ActiveFilters extends React.Component {
|
|||
className="pointable bg-transparent border-0 py-0 pr-1"
|
||||
title={`Clear filter: ${filter.field}`}
|
||||
onClick={() =>
|
||||
filterModel.removeFilter(filter.field, filterValue)
|
||||
this.clearAndUpdateRange({
|
||||
filterField: filter.field,
|
||||
filterValue,
|
||||
})
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faTimesCircle}
|
||||
title={`Clear filter: ${filter.field}`}
|
||||
/>
|
||||
<FontAwesomeIcon icon={faTimesCircle} />
|
||||
|
||||
</Button>
|
||||
<span title={`Filter by ${filter.field}: ${filterValue}`}>
|
||||
|
@ -235,4 +262,15 @@ ActiveFilters.propTypes = {
|
|||
isFieldFilterVisible: PropTypes.bool.isRequired,
|
||||
toggleFieldFilterVisible: PropTypes.func.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';
|
||||
import { faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { thAllResultStatuses } from '../../helpers/constants';
|
||||
import { getJobsUrl } from '../../helpers/url';
|
||||
import { setSelectedJob, clearSelectedJob } from '../redux/stores/selectedJob';
|
||||
import { pinJobs } from '../redux/stores/pinnedJobs';
|
||||
|
||||
|
@ -42,6 +42,12 @@ function FiltersMenu(props) {
|
|||
};
|
||||
const { email } = user;
|
||||
|
||||
const updateParams = (param, value) => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.set(param, value);
|
||||
return `?${params.toString()}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<UncontrolledDropdown>
|
||||
<DropdownToggle
|
||||
|
@ -110,12 +116,13 @@ function FiltersMenu(props) {
|
|||
>
|
||||
Superseded only
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
tag="a"
|
||||
title={`Show only pushes for ${email}`}
|
||||
href={getJobsUrl({ author: email })}
|
||||
>
|
||||
My pushes only
|
||||
<DropdownItem title={`Show only pushes for ${email}`}>
|
||||
<Link
|
||||
className="dropdown-link"
|
||||
to={{ search: updateParams('author', email) }}
|
||||
>
|
||||
My pushes only
|
||||
</Link>
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
tag="a"
|
||||
|
|
|
@ -91,6 +91,7 @@ class PrimaryNavBar extends React.Component {
|
|||
duplicateJobsVisible={duplicateJobsVisible}
|
||||
groupCountsExpanded={groupCountsExpanded}
|
||||
toggleFieldFilterVisible={toggleFieldFilterVisible}
|
||||
{...this.props}
|
||||
/>
|
||||
</nav>
|
||||
</div>
|
||||
|
|
|
@ -8,8 +8,9 @@ import {
|
|||
DropdownToggle,
|
||||
UncontrolledDropdown,
|
||||
} from 'reactstrap';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { getRepoUrl } from '../../helpers/location';
|
||||
import { updateRepoParams } from '../../helpers/location';
|
||||
|
||||
const GROUP_ORDER = [
|
||||
'development',
|
||||
|
@ -83,13 +84,12 @@ export default function ReposMenu(props) {
|
|||
{!!group.repos &&
|
||||
group.repos.map((repo) => (
|
||||
<li key={repo.name}>
|
||||
<a
|
||||
title="Open repo"
|
||||
<Link
|
||||
className="dropdown-link"
|
||||
href={getRepoUrl(repo.name)}
|
||||
to={{ search: updateRepoParams(repo.name) }}
|
||||
>
|
||||
{repo.name}
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</DropdownItem>
|
||||
|
|
|
@ -9,10 +9,11 @@ import {
|
|||
faFilter,
|
||||
faTimesCircle,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { push as pushRoute } from 'connected-react-router';
|
||||
|
||||
import { getBtnClass } from '../../helpers/job';
|
||||
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 ErrorBoundary from '../../shared/ErrorBoundary';
|
||||
import { recalculateUnclassifiedCounts } from '../redux/stores/pushes';
|
||||
|
@ -46,7 +47,6 @@ class SecondaryNavBar extends React.PureComponent {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('hashchange', this.handleUrlChanges, false);
|
||||
this.loadWatchedRepos();
|
||||
}
|
||||
|
||||
|
@ -56,18 +56,22 @@ class SecondaryNavBar extends React.PureComponent {
|
|||
if (repoName !== prevState.repoName) {
|
||||
this.loadWatchedRepos();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('hashchange', this.handleUrlChanges, false);
|
||||
if (
|
||||
prevProps.router.location.search !== this.props.router.location.search
|
||||
) {
|
||||
this.handleUrlChanges(
|
||||
prevProps.router.location.search,
|
||||
this.props.router.location.search,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setSearchStr(ev) {
|
||||
this.setState({ searchQueryStr: ev.target.value });
|
||||
}
|
||||
|
||||
handleUrlChanges = (evt) => {
|
||||
const { oldURL, newURL } = evt;
|
||||
handleUrlChanges = (prevParams, currentParams) => {
|
||||
const { repoName } = this.state;
|
||||
const { recalculateUnclassifiedCounts } = this.props;
|
||||
const newState = {
|
||||
|
@ -77,7 +81,7 @@ class SecondaryNavBar extends React.PureComponent {
|
|||
|
||||
this.setState(newState, () => {
|
||||
if (
|
||||
hasUrlFilterChanges(oldURL, newURL) ||
|
||||
hasUrlFilterChanges(prevParams, currentParams) ||
|
||||
newState.repoName !== repoName
|
||||
) {
|
||||
recalculateUnclassifiedCounts();
|
||||
|
@ -124,17 +128,25 @@ class SecondaryNavBar extends React.PureComponent {
|
|||
};
|
||||
|
||||
toggleShowDuplicateJobs = () => {
|
||||
const { duplicateJobsVisible } = this.props;
|
||||
const { duplicateJobsVisible, pushRoute } = this.props;
|
||||
const duplicateJobs = duplicateJobsVisible ? null : 'visible';
|
||||
|
||||
setUrlParam('duplicate_jobs', duplicateJobs);
|
||||
const queryParams = setUrlParams([['duplicate_jobs', duplicateJobs]]);
|
||||
|
||||
pushRoute({
|
||||
search: queryParams,
|
||||
});
|
||||
};
|
||||
|
||||
toggleGroupState = () => {
|
||||
const { groupCountsExpanded } = this.props;
|
||||
const { groupCountsExpanded, pushRoute } = this.props;
|
||||
const groupState = groupCountsExpanded ? null : 'expanded';
|
||||
|
||||
setUrlParam('group_state', groupState);
|
||||
const queryParams = setUrlParams([['group_state', groupState]]);
|
||||
|
||||
pushRoute({
|
||||
search: queryParams,
|
||||
});
|
||||
};
|
||||
|
||||
toggleUnclassifiedFailures = () => {
|
||||
|
@ -228,6 +240,7 @@ class SecondaryNavBar extends React.PureComponent {
|
|||
repoName={repoName}
|
||||
unwatchRepo={this.unwatchRepo}
|
||||
setCurrentRepoTreeStatus={setCurrentRepoTreeStatus}
|
||||
{...this.props}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
|
@ -299,7 +312,7 @@ class SecondaryNavBar extends React.PureComponent {
|
|||
? 'Collapse job groups'
|
||||
: 'Expand job groups'
|
||||
}
|
||||
onClick={() => this.toggleGroupState()}
|
||||
onClick={this.toggleGroupState}
|
||||
>
|
||||
(
|
||||
<span className="group-state-nav-icon mx-1">
|
||||
|
@ -395,12 +408,19 @@ SecondaryNavBar.propTypes = {
|
|||
duplicateJobsVisible: PropTypes.bool.isRequired,
|
||||
groupCountsExpanded: PropTypes.bool.isRequired,
|
||||
toggleFieldFilterVisible: PropTypes.func.isRequired,
|
||||
pushRoute: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = ({
|
||||
pushes: { allUnclassifiedFailureCount, filteredUnclassifiedFailureCount },
|
||||
}) => ({ allUnclassifiedFailureCount, filteredUnclassifiedFailureCount });
|
||||
router,
|
||||
}) => ({
|
||||
allUnclassifiedFailureCount,
|
||||
filteredUnclassifiedFailureCount,
|
||||
router,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, { recalculateUnclassifiedCounts })(
|
||||
SecondaryNavBar,
|
||||
);
|
||||
export default connect(mapStateToProps, {
|
||||
recalculateUnclassifiedCounts,
|
||||
pushRoute,
|
||||
})(SecondaryNavBar);
|
||||
|
|
|
@ -18,10 +18,12 @@ import {
|
|||
DropdownToggle,
|
||||
UncontrolledDropdown,
|
||||
} from 'reactstrap';
|
||||
import { push as pushRoute } from 'connected-react-router';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import TreeStatusModel from '../../models/treeStatus';
|
||||
import BugLinkify from '../../shared/BugLinkify';
|
||||
import { getRepoUrl } from '../../helpers/location';
|
||||
import { updateRepoParams } from '../../helpers/location';
|
||||
|
||||
const statusInfoMap = {
|
||||
open: {
|
||||
|
@ -57,7 +59,7 @@ const statusInfoMap = {
|
|||
},
|
||||
};
|
||||
|
||||
export default class WatchedRepo extends React.Component {
|
||||
class WatchedRepo extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
@ -124,18 +126,17 @@ export default class WatchedRepo extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const { repoName, unwatchRepo, repo } = this.props;
|
||||
const { repoName, unwatchRepo, repo, pushRoute } = this.props;
|
||||
const { status, messageOfTheDay, reason, statusInfo } = this.state;
|
||||
const watchedRepo = repo.name;
|
||||
const activeClass = watchedRepo === repoName ? 'active' : '';
|
||||
const { btnClass, icon, color } = statusInfo;
|
||||
const pulseIcon = statusInfo.pulseIcon || null;
|
||||
const changeRepoUrl = getRepoUrl(watchedRepo);
|
||||
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
href={changeRepoUrl}
|
||||
onClick={() => pushRoute({ search: updateRepoParams(watchedRepo) })}
|
||||
className={`btn-view-nav ${btnClass} ${activeClass}`}
|
||||
title={status}
|
||||
size="sm"
|
||||
|
@ -230,4 +231,7 @@ WatchedRepo.propTypes = {
|
|||
pushLogUrl: PropTypes.string,
|
||||
}).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) {
|
||||
classes.push('selected-job btn-lg-xform');
|
||||
attributes['data-testid'] = 'selected-job';
|
||||
} else {
|
||||
classes.push('btn-xs');
|
||||
}
|
||||
|
||||
attributes.className = classes.join(' ');
|
||||
return (
|
||||
<button type="button" {...attributes}>
|
||||
<button type="button" {...attributes} data-testid="job-btn">
|
||||
{jobTypeSymbol}
|
||||
{classifiedIcon && (
|
||||
<FontAwesomeIcon
|
||||
|
|
|
@ -14,6 +14,7 @@ export default function JobCount(props) {
|
|||
className={classes.join(' ')}
|
||||
title={title}
|
||||
onClick={onClick}
|
||||
data-testid="job-group-count"
|
||||
>
|
||||
{count}
|
||||
</button>
|
||||
|
|
|
@ -11,7 +11,11 @@ import {
|
|||
} from '../../helpers/constants';
|
||||
import decompress from '../../helpers/gzip';
|
||||
import { getGroupMapKey } from '../../helpers/aggregateId';
|
||||
import { getAllUrlParams, getUrlParam } from '../../helpers/location';
|
||||
import {
|
||||
getAllUrlParams,
|
||||
getUrlParam,
|
||||
setUrlParam,
|
||||
} from '../../helpers/location';
|
||||
import JobModel from '../../models/job';
|
||||
import RunnableJobModel from '../../models/runnableJob';
|
||||
import { getRevisionTitle } from '../../helpers/revision';
|
||||
|
@ -122,17 +126,21 @@ class Push extends React.PureComponent {
|
|||
this.testForFilteredTry();
|
||||
|
||||
window.addEventListener(thEvents.applyNewJobs, this.handleApplyNewJobs);
|
||||
window.addEventListener('hashchange', this.handleUrlChanges);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
this.showUpdateNotifications(prevState);
|
||||
this.testForFilteredTry();
|
||||
|
||||
if (
|
||||
prevProps.router.location.search !== this.props.router.location.search
|
||||
) {
|
||||
this.handleUrlChanges();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener(thEvents.applyNewJobs, this.handleApplyNewJobs);
|
||||
window.removeEventListener('hashchange', this.handleUrlChanges);
|
||||
}
|
||||
|
||||
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 = () => {
|
||||
const { currentRepo } = this.props;
|
||||
const filterParams = ['revision', 'author'];
|
||||
|
@ -633,6 +665,7 @@ class Push extends React.PureComponent {
|
|||
pushHealthVisibility={pushHealthVisibility}
|
||||
groupCountsExpanded={groupCountsExpanded}
|
||||
pushHealthStatusCallback={this.pushHealthStatusCallback}
|
||||
togglePushCollapsed={this.togglePushCollapsed}
|
||||
/>
|
||||
<div className="push-body-divider" />
|
||||
{!collapsed ? (
|
||||
|
@ -713,10 +746,12 @@ Push.propTypes = {
|
|||
|
||||
const mapStateToProps = ({
|
||||
pushes: { allUnclassifiedFailureCount, decisionTaskMap, bugSummaryMap },
|
||||
router,
|
||||
}) => ({
|
||||
allUnclassifiedFailureCount,
|
||||
decisionTaskMap,
|
||||
bugSummaryMap,
|
||||
router,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
|
|
|
@ -7,55 +7,39 @@ import {
|
|||
DropdownToggle,
|
||||
UncontrolledDropdown,
|
||||
} 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 CustomJobActions from '../CustomJobActions';
|
||||
import PushModel from '../../models/push';
|
||||
import { getPushHealthUrl, getCompareChooserUrl } from '../../helpers/url';
|
||||
import { notify } from '../redux/stores/notifications';
|
||||
import { thEvents } from '../../helpers/constants';
|
||||
|
||||
// Trigger missing jobs is dangerous on repos other than these (see bug 1335506)
|
||||
const triggerMissingRepos = ['mozilla-inbound', 'autoland'];
|
||||
import { updateRange } from '../redux/stores/pushes';
|
||||
|
||||
class PushActionMenu extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const { revision } = this.props;
|
||||
|
||||
this.state = {
|
||||
topOfRangeUrl: this.getRangeChangeUrl('tochange', revision),
|
||||
bottomOfRangeUrl: this.getRangeChangeUrl('fromchange', revision),
|
||||
customJobActionsShowing: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('hashchange', this.handleUrlChanges, false);
|
||||
window.addEventListener(thEvents.filtersUpdated, this.handleUrlChanges);
|
||||
}
|
||||
updateParamsAndRange = (param) => {
|
||||
const { revision, updateRange, pushRoute } = this.props;
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('hashchange', this.handleUrlChanges, false);
|
||||
window.removeEventListener(thEvents.filtersUpdated, this.handleUrlChanges);
|
||||
}
|
||||
let queryParams = parseQueryParams(window.location.search);
|
||||
queryParams = { ...queryParams, ...{ [param]: revision } };
|
||||
|
||||
getRangeChangeUrl(param, revision) {
|
||||
let url = window.location.href;
|
||||
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),
|
||||
pushRoute({
|
||||
search: createQueryParams(queryParams),
|
||||
});
|
||||
updateRange(queryParams);
|
||||
};
|
||||
|
||||
triggerMissingJobs = () => {
|
||||
|
@ -102,11 +86,7 @@ class PushActionMenu extends React.PureComponent {
|
|||
pushId,
|
||||
currentRepo,
|
||||
} = this.props;
|
||||
const {
|
||||
topOfRangeUrl,
|
||||
bottomOfRangeUrl,
|
||||
customJobActionsShowing,
|
||||
} = this.state;
|
||||
const { customJobActionsShowing } = this.state;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
@ -143,15 +123,13 @@ class PushActionMenu extends React.PureComponent {
|
|||
>
|
||||
Add new jobs (Search)
|
||||
</DropdownItem>
|
||||
{triggerMissingRepos.includes(currentRepo.name) && (
|
||||
<DropdownItem
|
||||
tag="a"
|
||||
title="Trigger all jobs that were optimized away"
|
||||
onClick={this.triggerMissingJobs}
|
||||
>
|
||||
Trigger missing jobs
|
||||
</DropdownItem>
|
||||
)}
|
||||
<DropdownItem
|
||||
tag="a"
|
||||
title="Trigger all jobs that were optimized away"
|
||||
onClick={this.triggerMissingJobs}
|
||||
>
|
||||
Trigger missing jobs
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
tag="a"
|
||||
target="_blank"
|
||||
|
@ -170,14 +148,14 @@ class PushActionMenu extends React.PureComponent {
|
|||
</DropdownItem>
|
||||
<DropdownItem
|
||||
tag="a"
|
||||
href={topOfRangeUrl}
|
||||
onClick={() => this.updateParamsAndRange('tochange')}
|
||||
data-testid="top-of-range-menu-item"
|
||||
>
|
||||
Set as top of range
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
tag="a"
|
||||
href={bottomOfRangeUrl}
|
||||
onClick={() => this.updateParamsAndRange('fromchange')}
|
||||
data-testid="bottom-of-range-menu-item"
|
||||
>
|
||||
Set as bottom of range
|
||||
|
@ -221,7 +199,7 @@ class PushActionMenu extends React.PureComponent {
|
|||
|
||||
PushActionMenu.propTypes = {
|
||||
runnableVisible: PropTypes.bool.isRequired,
|
||||
revision: PropTypes.string.isRequired,
|
||||
revision: PropTypes.string,
|
||||
currentRepo: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
}).isRequired,
|
||||
|
@ -233,8 +211,14 @@ PushActionMenu.propTypes = {
|
|||
notify: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
PushActionMenu.defaultProps = {
|
||||
revision: null,
|
||||
};
|
||||
|
||||
const mapStateToProps = ({ pushes: { decisionTaskMap } }) => ({
|
||||
decisionTaskMap,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, { notify })(PushActionMenu);
|
||||
export default connect(mapStateToProps, { notify, updateRange, pushRoute })(
|
||||
PushActionMenu,
|
||||
);
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
faTimesCircle,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { Badge, Button } from 'reactstrap';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { getPercentComplete, toDateStr } from '../../helpers/display';
|
||||
import { formatTaskclusterError } from '../../helpers/errorMessage';
|
||||
|
@ -20,8 +21,7 @@ import { getJobsUrl } from '../../helpers/url';
|
|||
import PushModel from '../../models/push';
|
||||
import JobModel from '../../models/job';
|
||||
import PushHealthStatus from '../../shared/PushHealthStatus';
|
||||
import PushAuthor from '../../shared/PushAuthor';
|
||||
import { getUrlParam, setUrlParam } from '../../helpers/location';
|
||||
import { getUrlParam } from '../../helpers/location';
|
||||
import { notify } from '../redux/stores/notifications';
|
||||
import { setSelectedJob } from '../redux/stores/selectedJob';
|
||||
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() {
|
||||
const {
|
||||
pushId,
|
||||
|
@ -235,6 +216,7 @@ class PushHeader extends React.Component {
|
|||
pushHealthVisibility,
|
||||
currentRepo,
|
||||
pushHealthStatusCallback,
|
||||
togglePushCollapsed,
|
||||
} = this.props;
|
||||
const cancelJobsTitle = 'Cancel all jobs';
|
||||
const linkParams = this.getLinkParams();
|
||||
|
@ -256,22 +238,22 @@ class PushHeader extends React.Component {
|
|||
<span className="push-left">
|
||||
<span className="push-title-left">
|
||||
<FontAwesomeIcon
|
||||
onClick={this.togglePushCollapsed}
|
||||
onClick={togglePushCollapsed}
|
||||
icon={collapsed ? faPlusSquare : faMinusSquare}
|
||||
className="mr-2 mt-2 text-muted pointable"
|
||||
title={`${collapsed ? 'Expand' : 'Collapse'} push data`}
|
||||
/>
|
||||
<span>
|
||||
<a href={revisionPushFilterUrl} title="View only this push">
|
||||
<Link to={revisionPushFilterUrl} title="View only this push">
|
||||
{this.pushDateStr}{' '}
|
||||
<FontAwesomeIcon
|
||||
icon={faExternalLinkAlt}
|
||||
className="icon-superscript"
|
||||
/>
|
||||
</a>{' '}
|
||||
</Link>{' '}
|
||||
-{' '}
|
||||
</span>
|
||||
<PushAuthor author={author} url={authorPushFilterUrl} />
|
||||
<Link to={authorPushFilterUrl}>{author}</Link>
|
||||
</span>
|
||||
</span>
|
||||
{showPushHealthStatus && (
|
||||
|
@ -364,7 +346,7 @@ PushHeader.propTypes = {
|
|||
pushId: PropTypes.number.isRequired,
|
||||
pushTimestamp: PropTypes.number.isRequired,
|
||||
author: PropTypes.string.isRequired,
|
||||
revision: PropTypes.string.isRequired,
|
||||
revision: PropTypes.string,
|
||||
filterModel: PropTypes.shape({}).isRequired,
|
||||
runnableVisible: PropTypes.bool.isRequired,
|
||||
showRunnableJobs: PropTypes.func.isRequired,
|
||||
|
@ -390,6 +372,7 @@ PushHeader.propTypes = {
|
|||
PushHeader.defaultProps = {
|
||||
watchState: 'none',
|
||||
pushHealthStatusCallback: null,
|
||||
revision: null,
|
||||
};
|
||||
|
||||
const mapStateToProps = ({ pushes: { decisionTaskMap } }) => ({
|
||||
|
|
|
@ -11,13 +11,8 @@ import {
|
|||
clearSelectedJob,
|
||||
setSelectedJobFromQueryString,
|
||||
} from '../redux/stores/selectedJob';
|
||||
import {
|
||||
fetchPushes,
|
||||
fetchNextPushes,
|
||||
updateRange,
|
||||
pollPushes,
|
||||
} from '../redux/stores/pushes';
|
||||
import { reloadOnChangeParameters } from '../../helpers/filter';
|
||||
import { fetchPushes, updateRange, pollPushes } from '../redux/stores/pushes';
|
||||
import { updatePushParams } from '../../helpers/location';
|
||||
|
||||
import Push from './Push';
|
||||
import PushLoadErrors from './PushLoadErrors';
|
||||
|
@ -36,7 +31,6 @@ class PushList extends React.Component {
|
|||
componentDidMount() {
|
||||
const { fetchPushes } = this.props;
|
||||
|
||||
window.addEventListener('hashchange', this.handleUrlChanges, false);
|
||||
fetchPushes();
|
||||
this.poll();
|
||||
}
|
||||
|
@ -52,6 +46,7 @@ class PushList extends React.Component {
|
|||
if (jobsLoaded && jobsLoaded !== prevProps.jobsLoaded) {
|
||||
setSelectedJobFromQueryString(notify, jobMap);
|
||||
}
|
||||
this.handleUrlChanges(prevProps);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -59,7 +54,6 @@ class PushList extends React.Component {
|
|||
clearInterval(this.pushIntervalId);
|
||||
this.pushIntervalId = null;
|
||||
}
|
||||
window.addEventListener('hashchange', this.handleUrlChanges, false);
|
||||
}
|
||||
|
||||
setWindowTitle() {
|
||||
|
@ -68,11 +62,18 @@ class PushList extends React.Component {
|
|||
document.title = `[${allUnclassifiedFailureCount}] ${repoName}`;
|
||||
}
|
||||
|
||||
getUrlRangeValues = (url) => {
|
||||
const params = [...new URLSearchParams(url.split('?')[1]).entries()];
|
||||
getUrlRangeValues = (search) => {
|
||||
const params = [...new URLSearchParams(search)];
|
||||
|
||||
return params.reduce((acc, [key, value]) => {
|
||||
return reloadOnChangeParameters.includes(key)
|
||||
return [
|
||||
'repo',
|
||||
'startdate',
|
||||
'enddate',
|
||||
'nojobs',
|
||||
'revision',
|
||||
'author',
|
||||
].includes(key)
|
||||
? { ...acc, [key]: value }
|
||||
: acc;
|
||||
}, {});
|
||||
|
@ -86,11 +87,10 @@ class PushList extends React.Component {
|
|||
}, PUSH_POLL_INTERVAL);
|
||||
};
|
||||
|
||||
handleUrlChanges = (evt) => {
|
||||
const { updateRange } = this.props;
|
||||
const { oldURL, newURL } = evt;
|
||||
const oldRange = this.getUrlRangeValues(oldURL);
|
||||
const newRange = this.getUrlRangeValues(newURL);
|
||||
handleUrlChanges = (prevProps) => {
|
||||
const { updateRange, router } = this.props;
|
||||
const oldRange = this.getUrlRangeValues(prevProps.router.location.search);
|
||||
const newRange = this.getUrlRangeValues(router.location.search);
|
||||
|
||||
if (!isEqual(oldRange, 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() {
|
||||
const {
|
||||
repoName,
|
||||
|
@ -123,7 +130,6 @@ class PushList extends React.Component {
|
|||
filterModel,
|
||||
pushList,
|
||||
loadingPushes,
|
||||
fetchNextPushes,
|
||||
getAllShownJobs,
|
||||
jobsLoaded,
|
||||
duplicateJobsVisible,
|
||||
|
@ -135,6 +141,7 @@ class PushList extends React.Component {
|
|||
if (!revision) {
|
||||
this.setWindowTitle();
|
||||
}
|
||||
|
||||
return (
|
||||
// Bug 1619873 - role="list" works better here than an interactive role
|
||||
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
|
||||
|
@ -188,7 +195,7 @@ class PushList extends React.Component {
|
|||
color="darker-secondary"
|
||||
outline
|
||||
className="btn-light-bordered"
|
||||
onClick={() => fetchNextPushes(count)}
|
||||
onClick={() => this.fetchNextPushes(count)}
|
||||
key={count}
|
||||
data-testid={`get-next-${count}`}
|
||||
>
|
||||
|
@ -206,7 +213,6 @@ PushList.propTypes = {
|
|||
repoName: PropTypes.string.isRequired,
|
||||
filterModel: PropTypes.shape({}).isRequired,
|
||||
pushList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchNextPushes: PropTypes.func.isRequired,
|
||||
fetchPushes: PropTypes.func.isRequired,
|
||||
pollPushes: PropTypes.func.isRequired,
|
||||
updateRange: PropTypes.func.isRequired,
|
||||
|
@ -224,6 +230,7 @@ PushList.propTypes = {
|
|||
notify: PropTypes.func.isRequired,
|
||||
revision: PropTypes.string,
|
||||
currentRepo: PropTypes.shape({}),
|
||||
router: PropTypes.shape({}).isRequired,
|
||||
};
|
||||
|
||||
PushList.defaultProps = {
|
||||
|
@ -240,6 +247,7 @@ const mapStateToProps = ({
|
|||
allUnclassifiedFailureCount,
|
||||
},
|
||||
pinnedJobs: { pinnedJobs },
|
||||
router,
|
||||
}) => ({
|
||||
loadingPushes,
|
||||
jobsLoaded,
|
||||
|
@ -247,13 +255,13 @@ const mapStateToProps = ({
|
|||
pushList,
|
||||
allUnclassifiedFailureCount,
|
||||
pinnedJobs,
|
||||
router,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
notify,
|
||||
clearSelectedJob,
|
||||
setSelectedJobFromQueryString,
|
||||
fetchNextPushes,
|
||||
fetchPushes,
|
||||
updateRange,
|
||||
pollPushes,
|
||||
|
|
|
@ -1,22 +1,32 @@
|
|||
import { createStore, combineReducers, applyMiddleware } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import createDebounce from 'redux-debounce';
|
||||
import { connectRouter, routerMiddleware } from 'connected-react-router';
|
||||
import { createBrowserHistory } from 'history';
|
||||
|
||||
import * as selectedJobStore from './stores/selectedJob';
|
||||
import * as notificationStore from './stores/notifications';
|
||||
import * as pushesStore from './stores/pushes';
|
||||
import * as pinnedJobsStore from './stores/pinnedJobs';
|
||||
|
||||
export default () => {
|
||||
const debounceConfig = { nextJob: 200 };
|
||||
const debouncer = createDebounce(debounceConfig);
|
||||
const reducers = combineReducers({
|
||||
const debouncer = createDebounce({ nextJob: 200 });
|
||||
|
||||
const reducers = (routerHistory) =>
|
||||
combineReducers({
|
||||
router: connectRouter(routerHistory),
|
||||
notifications: notificationStore.reducer,
|
||||
selectedJob: selectedJobStore.reducer,
|
||||
pushes: pushesStore.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,
|
||||
});
|
||||
|
||||
export const clearExpiredNotifications = () => ({
|
||||
type: CLEAR_EXPIRED_TRANSIENTS,
|
||||
});
|
||||
|
||||
// *** Implementation ***
|
||||
const doNotify = (
|
||||
{ notifications, storedNotifications },
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
import pick from 'lodash/pick';
|
||||
import keyBy from 'lodash/keyBy';
|
||||
import max from 'lodash/max';
|
||||
import { push as pushRoute } from 'connected-react-router';
|
||||
|
||||
import { parseQueryParams, bugzillaBugsApi } from '../../../helpers/url';
|
||||
import {
|
||||
getAllUrlParams,
|
||||
getQueryString,
|
||||
getUrlParam,
|
||||
replaceLocation,
|
||||
} from '../../../helpers/location';
|
||||
import { getUrlParam, replaceLocation } from '../../../helpers/location';
|
||||
import PushModel from '../../../models/push';
|
||||
import { getTaskRunStr, isUnclassifiedFailure } from '../../../helpers/job';
|
||||
import FilterModel from '../../../models/filter';
|
||||
|
@ -96,8 +92,8 @@ const getLastModifiedJobTime = (jobMap) => {
|
|||
* gives us the difference in unclassified failures and, of those jobs, the
|
||||
* ones that have been filtered out
|
||||
*/
|
||||
const doRecalculateUnclassifiedCounts = (jobMap) => {
|
||||
const filterModel = new FilterModel();
|
||||
const doRecalculateUnclassifiedCounts = (jobMap, router) => {
|
||||
const filterModel = new FilterModel({ pushRoute, router });
|
||||
const tiers = filterModel.urlParams.tier;
|
||||
let allUnclassifiedFailureCount = 0;
|
||||
let filteredUnclassifiedFailureCount = 0;
|
||||
|
@ -122,6 +118,7 @@ const addPushes = (
|
|||
jobMap,
|
||||
setFromchange,
|
||||
dispatch,
|
||||
router,
|
||||
oldBugSummaryMap,
|
||||
) => {
|
||||
if (data.results.length > 0) {
|
||||
|
@ -140,7 +137,7 @@ const addPushes = (
|
|||
const newStuff = {
|
||||
pushList: newPushList,
|
||||
oldestPushTimestamp,
|
||||
...doRecalculateUnclassifiedCounts(jobMap),
|
||||
...doRecalculateUnclassifiedCounts(jobMap, router),
|
||||
...getRevisionTips(newPushList),
|
||||
};
|
||||
|
||||
|
@ -150,11 +147,11 @@ const addPushes = (
|
|||
const updatedLastRevision = newPushList[newPushList.length - 1].revision;
|
||||
|
||||
if (setFromchange && getUrlParam('fromchange') !== updatedLastRevision) {
|
||||
const params = getAllUrlParams();
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.set('fromchange', updatedLastRevision);
|
||||
replaceLocation(params);
|
||||
// We are silently updating the url params, but we still want to
|
||||
// update the ActiveFilters bar to this new change.
|
||||
// We are silently updating the url params so we don't trigger an unnecessary update
|
||||
// in componentDidUpdate, but we still want to update the ActiveFilters bar to this new change.
|
||||
window.dispatchEvent(new CustomEvent(thEvents.filtersUpdated));
|
||||
}
|
||||
|
||||
|
@ -252,13 +249,17 @@ export const fetchPushes = (
|
|||
return async (dispatch, getState) => {
|
||||
const {
|
||||
pushes: { pushList, jobMap, oldestPushTimestamp },
|
||||
router,
|
||||
} = getState();
|
||||
|
||||
dispatch({ type: LOADING });
|
||||
|
||||
if (getUrlParam('selectedJob') || getUrlParam('selectedTaskRun')) {
|
||||
dispatch(clearSelectedJob(0));
|
||||
}
|
||||
// Only pass supported query string params to this endpoint.
|
||||
const options = {
|
||||
...pick(parseQueryParams(getQueryString()), PUSH_FETCH_KEYS),
|
||||
...pick(parseQueryParams(window.location.search), PUSH_FETCH_KEYS),
|
||||
};
|
||||
|
||||
if (oldestPushTimestamp) {
|
||||
|
@ -284,6 +285,7 @@ export const fetchPushes = (
|
|||
jobMap,
|
||||
setFromchange,
|
||||
dispatch,
|
||||
router,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
@ -296,10 +298,11 @@ export const pollPushes = () => {
|
|||
return async (dispatch, getState) => {
|
||||
const {
|
||||
pushes: { pushList, jobMap },
|
||||
router,
|
||||
} = getState();
|
||||
// these params will be passed in each time we poll to remain
|
||||
// within the constraints of the URL params
|
||||
const locationSearch = parseQueryParams(getQueryString());
|
||||
const locationSearch = parseQueryParams(window.location.search);
|
||||
const pushPollingParams = PUSH_POLLING_KEYS.reduce(
|
||||
(acc, prop) =>
|
||||
locationSearch[prop] ? { ...acc, [prop]: locationSearch[prop] } : acc,
|
||||
|
@ -330,6 +333,8 @@ export const pollPushes = () => {
|
|||
pushList,
|
||||
jobMap,
|
||||
false,
|
||||
dispatch,
|
||||
router,
|
||||
),
|
||||
});
|
||||
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 setPushes = (pushList, jobMap) => ({
|
||||
export const setPushes = (pushList, jobMap, router) => ({
|
||||
type: SET_PUSHES,
|
||||
pushResults: {
|
||||
pushList,
|
||||
jobMap,
|
||||
...getRevisionTips(pushList),
|
||||
...doRecalculateUnclassifiedCounts(jobMap),
|
||||
...doRecalculateUnclassifiedCounts(jobMap, router),
|
||||
oldestPushTimestamp: pushList[pushList.length - 1].push_timestamp,
|
||||
},
|
||||
});
|
||||
|
||||
export const recalculateUnclassifiedCounts = (filterModel) => ({
|
||||
type: RECALCULATE_UNCLASSIFIED_COUNTS,
|
||||
filterModel,
|
||||
});
|
||||
export const recalculateUnclassifiedCounts = (filterModel) => {
|
||||
return (dispatch, getState) => {
|
||||
const { router } = getState();
|
||||
return dispatch({
|
||||
type: RECALCULATE_UNCLASSIFIED_COUNTS,
|
||||
filterModel,
|
||||
router,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const updateJobMap = (jobList) => ({
|
||||
type: UPDATE_JOB_MAP,
|
||||
|
@ -393,6 +380,7 @@ export const updateRange = (range) => {
|
|||
return (dispatch, getState) => {
|
||||
const {
|
||||
pushes: { pushList, jobMap },
|
||||
router,
|
||||
} = getState();
|
||||
const { revision } = range;
|
||||
// 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,
|
||||
{},
|
||||
);
|
||||
dispatch(clearSelectedJob(0));
|
||||
if (getUrlParam('selectedJob') || getUrlParam('selectedTaskRun')) {
|
||||
dispatch(clearSelectedJob(0));
|
||||
}
|
||||
// We already have the one revision they're looking for,
|
||||
// so we can just erase everything else.
|
||||
dispatch(setPushes(revisionPushList, revisionJobMap));
|
||||
dispatch(setPushes(revisionPushList, revisionJobMap, router));
|
||||
} else {
|
||||
// Clear and refetch everything. We can't be sure if what we
|
||||
// already have is partially correct and just needs fill-in.
|
||||
|
@ -435,8 +425,9 @@ export const initialState = {
|
|||
};
|
||||
|
||||
export const reducer = (state = initialState, action) => {
|
||||
const { jobList, pushResults, setFromchange } = action;
|
||||
const { jobList, pushResults, setFromchange, router } = action;
|
||||
const { pushList, jobMap, decisionTaskMap } = state;
|
||||
|
||||
switch (action.type) {
|
||||
case LOADING:
|
||||
return { ...state, loadingPushes: true };
|
||||
|
@ -449,7 +440,7 @@ export const reducer = (state = initialState, action) => {
|
|||
case SET_PUSHES:
|
||||
return { ...state, loadingPushes: false, ...pushResults };
|
||||
case RECALCULATE_UNCLASSIFIED_COUNTS:
|
||||
return { ...state, ...doRecalculateUnclassifiedCounts(jobMap) };
|
||||
return { ...state, ...doRecalculateUnclassifiedCounts(jobMap, router) };
|
||||
case UPDATE_JOB_MAP:
|
||||
return {
|
||||
...state,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { push as pushRoute } from 'connected-react-router';
|
||||
|
||||
import {
|
||||
findGroupElement,
|
||||
findGroupInstance,
|
||||
|
@ -8,7 +10,11 @@ import {
|
|||
scrollToElement,
|
||||
} from '../../../helpers/job';
|
||||
import { thJobNavSelectors } from '../../../helpers/constants';
|
||||
import { getUrlParam, setUrlParam } from '../../../helpers/location';
|
||||
import {
|
||||
getUrlParam,
|
||||
setUrlParam,
|
||||
setUrlParams,
|
||||
} from '../../../helpers/location';
|
||||
import JobModel from '../../../models/job';
|
||||
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 UPDATE_JOB_DETAILS = 'UPDATE_JOB_DETAILS';
|
||||
|
||||
export const setSelectedJob = (job, updateDetails = true) => ({
|
||||
type: SELECT_JOB,
|
||||
job,
|
||||
updateDetails,
|
||||
});
|
||||
export const setSelectedJob = (job, updateDetails = true) => {
|
||||
return async (dispatch) => {
|
||||
dispatch({
|
||||
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) => ({
|
||||
type: SELECT_JOB_FROM_QUERY_STRING,
|
||||
|
@ -29,27 +44,36 @@ export const setSelectedJobFromQueryString = (notify, jobMap) => ({
|
|||
jobMap,
|
||||
});
|
||||
|
||||
export const clearSelectedJob = (countPinnedJobs) => ({
|
||||
type: CLEAR_JOB,
|
||||
countPinnedJobs,
|
||||
});
|
||||
|
||||
export const updateJobDetails = (job) => ({
|
||||
type: UPDATE_JOB_DETAILS,
|
||||
job,
|
||||
meta: {
|
||||
debounce: 'nextJob',
|
||||
},
|
||||
});
|
||||
|
||||
const doUpdateJobDetails = (job) => {
|
||||
const taskRun = job ? getTaskRunStr(job) : null;
|
||||
|
||||
setUrlParam('selectedTaskRun', taskRun);
|
||||
return { selectedJob: job };
|
||||
export const clearSelectedJob = (countPinnedJobs) => {
|
||||
return async (dispatch) => {
|
||||
dispatch({
|
||||
type: CLEAR_JOB,
|
||||
countPinnedJobs,
|
||||
});
|
||||
const params = setUrlParams([
|
||||
['selectedTaskRun', null],
|
||||
['selectedJob', null],
|
||||
]);
|
||||
dispatch(pushRoute({ search: params }));
|
||||
};
|
||||
};
|
||||
|
||||
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();
|
||||
|
||||
if (selected) selected.setSelected(false);
|
||||
|
@ -71,9 +95,7 @@ export const doSelectJob = (job, updateDetails) => {
|
|||
scrollToElement(groupEl);
|
||||
}
|
||||
}
|
||||
if (updateDetails) {
|
||||
return doUpdateJobDetails(job);
|
||||
}
|
||||
|
||||
return { selectedJob: job };
|
||||
};
|
||||
|
||||
|
@ -81,8 +103,7 @@ export const doClearSelectedJob = (countPinnedJobs) => {
|
|||
if (!countPinnedJobs) {
|
||||
const selected = findSelectedInstance();
|
||||
if (selected) selected.setSelected(false);
|
||||
setUrlParam('selectedTaskRun', null);
|
||||
setUrlParam('selectedJob', null);
|
||||
|
||||
return { selectedJob: null };
|
||||
}
|
||||
return {};
|
||||
|
@ -240,7 +261,7 @@ export const changeJob = (
|
|||
if (jobInstance) {
|
||||
// Delay updating details for the new job right away,
|
||||
// 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) => {
|
||||
const { job, jobMap, countPinnedJobs, updateDetails, notify } = action;
|
||||
const { job, jobMap, countPinnedJobs, notify } = action;
|
||||
|
||||
switch (action.type) {
|
||||
case SELECT_JOB:
|
||||
return doSelectJob(job, updateDetails);
|
||||
return doSelectJob(job);
|
||||
case SELECT_JOB_FROM_QUERY_STRING:
|
||||
return doSetSelectedJobFromQueryString(notify, jobMap);
|
||||
case UPDATE_JOB_DETAILS:
|
||||
return doUpdateJobDetails(job);
|
||||
return { selectedJob: job };
|
||||
case CLEAR_JOB:
|
||||
return { ...state, ...doClearSelectedJob(countPinnedJobs) };
|
||||
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 ErrorLines from './ErrorLines';
|
||||
|
||||
import '../css/lazylog-custom-styles.css';
|
||||
import './logviewer.css';
|
||||
|
||||
const JOB_DETAILS_COLLAPSED = 'jobDetailsCollapsed';
|
||||
|
||||
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';
|
||||
import { getAllUrlParams } from '../helpers/location';
|
||||
|
||||
export const getNonFilterUrlParams = () =>
|
||||
[...getAllUrlParams().entries()].reduce(
|
||||
export const getNonFilterUrlParams = (location) =>
|
||||
[...getAllUrlParams(location).entries()].reduce(
|
||||
(acc, [urlField, urlValue]) =>
|
||||
allFilterParams.includes(urlField.replace(deprecatedThFilterPrefix, ''))
|
||||
? 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.
|
||||
// This handles the transition from our old url params to this newer, more
|
||||
// terse version.
|
||||
// Also remove usage of the 'filter-' prefix.
|
||||
const groupedValues = [...getAllUrlParams().entries()].reduce(
|
||||
const groupedValues = [...getAllUrlParams(location).entries()].reduce(
|
||||
(acc, [urlField, urlValue]) => {
|
||||
const field = urlField.replace(deprecatedThFilterPrefix, '');
|
||||
if (!allFilterParams.includes(field)) {
|
||||
|
@ -51,8 +51,11 @@ export const getFilterUrlParamsWithDefaults = () => {
|
|||
};
|
||||
|
||||
export default class FilterModel {
|
||||
constructor() {
|
||||
this.urlParams = getFilterUrlParamsWithDefaults();
|
||||
constructor(props) {
|
||||
// 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.
|
||||
|
@ -60,7 +63,7 @@ export default class FilterModel {
|
|||
// ensure the repo param is always set
|
||||
const params = {
|
||||
repo: thDefaultRepo,
|
||||
...getNonFilterUrlParams(),
|
||||
...getNonFilterUrlParams(this.location),
|
||||
...this.urlParams,
|
||||
};
|
||||
|
||||
|
@ -86,7 +89,7 @@ export default class FilterModel {
|
|||
} else {
|
||||
this.urlParams[field] = [value];
|
||||
}
|
||||
this.push();
|
||||
this.push({ search: this.getFilterQueryString() });
|
||||
};
|
||||
|
||||
// Also used for non-filter params
|
||||
|
@ -99,29 +102,24 @@ export default class FilterModel {
|
|||
(filterValue) => filterValue !== value,
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.urlParams[field].length) {
|
||||
delete this.urlParams[field];
|
||||
}
|
||||
} else {
|
||||
delete this.urlParams[field];
|
||||
}
|
||||
this.push();
|
||||
|
||||
this.push({ search: this.getFilterQueryString() });
|
||||
};
|
||||
|
||||
getFilterQueryString = () =>
|
||||
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 = () => {
|
||||
this.urlParams.resultStatus = 'superseded';
|
||||
this.urlParams.classifiedState = [...thFilterDefaults.classifiedState];
|
||||
this.push();
|
||||
this.push({ search: this.getFilterQueryString() });
|
||||
};
|
||||
|
||||
toggleFilter = (field, value) => {
|
||||
|
@ -148,7 +146,7 @@ export default class FilterModel {
|
|||
? currentResultStatuses.filter((rs) => !resultStatuses.includes(rs))
|
||||
: [...new Set([...resultStatuses, ...currentResultStatuses])];
|
||||
|
||||
this.push();
|
||||
this.push({ search: this.getFilterQueryString() });
|
||||
};
|
||||
|
||||
toggleClassifiedFilter = (classifiedState) => {
|
||||
|
@ -161,20 +159,20 @@ export default class FilterModel {
|
|||
} else {
|
||||
this.urlParams.resultStatus = [...thFailureResults];
|
||||
this.urlParams.classifiedState = ['unclassified'];
|
||||
this.push();
|
||||
this.push({ search: this.getFilterQueryString() });
|
||||
}
|
||||
};
|
||||
|
||||
replaceFilter = (field, value) => {
|
||||
this.urlParams[field] = !Array.isArray(value) ? [value] : value;
|
||||
this.push();
|
||||
this.push({ search: this.getFilterQueryString() });
|
||||
};
|
||||
|
||||
clearNonStatusFilters = () => {
|
||||
const { repo, resultStatus, classifiedState } = this.urlParams;
|
||||
|
||||
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.classifiedState = [...classifiedState];
|
||||
this.push();
|
||||
this.push({ search: this.getFilterQueryString() });
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -54,6 +54,7 @@ export default class PushModel {
|
|||
// fetch the maximum number of pushes
|
||||
params.count = thMaxPushFetchSize;
|
||||
}
|
||||
|
||||
return getData(
|
||||
`${getProjectUrl(pushEndpoint, repoName)}${createQueryParams(params)}`,
|
||||
);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { Container } from 'reactstrap';
|
||||
|
||||
|
@ -18,6 +18,9 @@ import CompareSubtestsView from './compare/CompareSubtestsView';
|
|||
import CompareSubtestDistributionView from './compare/CompareSubtestDistributionView';
|
||||
import Navigation from './Navigation';
|
||||
|
||||
import 'react-table/react-table.css';
|
||||
import '../css/perf.css';
|
||||
|
||||
class App extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -66,9 +69,10 @@ class App extends React.Component {
|
|||
errorMessages,
|
||||
compareData,
|
||||
} = this.state;
|
||||
const { path } = this.props.match;
|
||||
|
||||
return (
|
||||
<HashRouter>
|
||||
<React.Fragment>
|
||||
<Navigation
|
||||
user={user}
|
||||
setUser={(user) => this.setState({ user })}
|
||||
|
@ -86,7 +90,7 @@ class App extends React.Component {
|
|||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path="/alerts"
|
||||
path={`${path}/alerts`}
|
||||
render={(props) => (
|
||||
<AlertsView
|
||||
{...props}
|
||||
|
@ -98,7 +102,7 @@ class App extends React.Component {
|
|||
)}
|
||||
/>
|
||||
<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) => (
|
||||
<AlertsView
|
||||
{...props}
|
||||
|
@ -110,7 +114,7 @@ class App extends React.Component {
|
|||
)}
|
||||
/>
|
||||
<Route
|
||||
path="/graphs"
|
||||
path={`${path}/graphs`}
|
||||
render={(props) => (
|
||||
<GraphsView
|
||||
{...props}
|
||||
|
@ -121,7 +125,7 @@ class App extends React.Component {
|
|||
)}
|
||||
/>
|
||||
<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) => (
|
||||
<GraphsView
|
||||
{...props}
|
||||
|
@ -132,7 +136,7 @@ class App extends React.Component {
|
|||
)}
|
||||
/>
|
||||
<Route
|
||||
path="/comparechooser"
|
||||
path={`${path}/comparechooser`}
|
||||
render={(props) => (
|
||||
<CompareSelectorView
|
||||
{...props}
|
||||
|
@ -143,7 +147,7 @@ class App extends React.Component {
|
|||
)}
|
||||
/>
|
||||
<Route
|
||||
path="/comparechooser?originalProject=:originalProject&originalRevision=:originalRevision&newProject=:newProject&newRevision=:newRevision"
|
||||
path={`${path}/comparechooser?originalProject=:originalProject&originalRevision=:originalRevision&newProject=:newProject&newRevision=:newRevision`}
|
||||
render={(props) => (
|
||||
<CompareSelectorView
|
||||
{...props}
|
||||
|
@ -154,7 +158,7 @@ class App extends React.Component {
|
|||
)}
|
||||
/>
|
||||
<Route
|
||||
path="/compare"
|
||||
path={`${path}/compare`}
|
||||
render={(props) => (
|
||||
<CompareView
|
||||
{...props}
|
||||
|
@ -167,7 +171,7 @@ class App extends React.Component {
|
|||
)}
|
||||
/>
|
||||
<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) => (
|
||||
<CompareView
|
||||
{...props}
|
||||
|
@ -180,7 +184,7 @@ class App extends React.Component {
|
|||
)}
|
||||
/>
|
||||
<Route
|
||||
path="/infracompare"
|
||||
path={`${path}/infracompare`}
|
||||
render={(props) => (
|
||||
<InfraCompareView
|
||||
{...props}
|
||||
|
@ -193,7 +197,7 @@ class App extends React.Component {
|
|||
)}
|
||||
/>
|
||||
<Route
|
||||
path="/comparesubtest"
|
||||
path={`${path}/comparesubtest`}
|
||||
render={(props) => (
|
||||
<CompareSubtestsView
|
||||
{...props}
|
||||
|
@ -204,7 +208,7 @@ class App extends React.Component {
|
|||
)}
|
||||
/>
|
||||
<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) => (
|
||||
<CompareSubtestsView
|
||||
{...props}
|
||||
|
@ -215,7 +219,7 @@ class App extends React.Component {
|
|||
)}
|
||||
/>
|
||||
<Route
|
||||
path="/comparesubtestdistribution"
|
||||
path={`${path}/comparesubtestdistribution`}
|
||||
render={(props) => (
|
||||
<CompareSubtestDistributionView
|
||||
{...props}
|
||||
|
@ -226,7 +230,7 @@ class App extends React.Component {
|
|||
)}
|
||||
/>
|
||||
<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) => (
|
||||
<CompareSubtestDistributionView
|
||||
{...props}
|
||||
|
@ -237,7 +241,7 @@ class App extends React.Component {
|
|||
)}
|
||||
/>
|
||||
<Route
|
||||
path="/tests"
|
||||
path={`${path}/tests`}
|
||||
render={(props) => (
|
||||
<TestsView
|
||||
{...props}
|
||||
|
@ -249,7 +253,7 @@ class App extends React.Component {
|
|||
)}
|
||||
/>
|
||||
<Route
|
||||
path="/tests?framework=:framework"
|
||||
path={`${path}/tests?framework=:framework"`}
|
||||
render={(props) => (
|
||||
<TestsView
|
||||
{...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>
|
||||
</main>
|
||||
)}
|
||||
</HashRouter>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
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 Login from '../shared/auth/Login';
|
||||
|
@ -11,24 +12,24 @@ const Navigation = ({ user, setUser, notify }) => (
|
|||
<LogoMenu menuText="Perfherder" colorClass="text-info" />
|
||||
<Nav className="navbar navbar-inverse">
|
||||
<NavItem>
|
||||
<NavLink href="#/graphs" className="btn-view-nav">
|
||||
<Link to="./graphs" className="nav-link btn-view-nav">
|
||||
Graphs
|
||||
</NavLink>
|
||||
</Link>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink href="#/comparechooser" className="btn-view-nav">
|
||||
<Link to="./comparechooser" className="nav-link btn-view-nav">
|
||||
Compare
|
||||
</NavLink>
|
||||
</Link>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink href="#/alerts?hideDwnToInv=1" className="btn-view-nav">
|
||||
<Link to="./alerts?hideDwnToInv=1" className="nav-link btn-view-nav">
|
||||
Alerts
|
||||
</NavLink>
|
||||
</Link>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink href="#/tests" className="btn-view-nav">
|
||||
<Link to="./tests" className="nav-link btn-view-nav">
|
||||
Tests
|
||||
</NavLink>
|
||||
</Link>
|
||||
</NavItem>
|
||||
</Nav>
|
||||
<Navbar className="ml-auto">
|
||||
|
|
|
@ -2,11 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { Container } from 'reactstrap';
|
||||
|
||||
import {
|
||||
parseQueryParams,
|
||||
createQueryParams,
|
||||
updateQueryParams,
|
||||
} from '../helpers/url';
|
||||
import { parseQueryParams, createQueryParams } from '../helpers/url';
|
||||
import PushModel from '../models/push';
|
||||
import ErrorMessages from '../shared/ErrorMessages';
|
||||
import LoadingSpinner from '../shared/LoadingSpinner';
|
||||
|
@ -37,30 +33,29 @@ const withValidation = ({ requiredParams }, verifyRevisions = true) => (
|
|||
}
|
||||
|
||||
async componentDidMount() {
|
||||
this.validateParams(parseQueryParams(this.props.location.search));
|
||||
}
|
||||
|
||||
shouldComponentUpdate(prevProps) {
|
||||
const { location } = this.props;
|
||||
|
||||
return location.hash === prevProps.location.hash;
|
||||
this.validateParams(parseQueryParams(this.props.history.location.search));
|
||||
}
|
||||
|
||||
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
|
||||
this.validateParams(parseQueryParams(location.search));
|
||||
this.validateParams(parseQueryParams(history.location.search));
|
||||
}
|
||||
}
|
||||
|
||||
updateParams = (params) => {
|
||||
const { location, history } = this.props;
|
||||
const newParams = { ...parseQueryParams(location.search), ...params };
|
||||
const queryString = createQueryParams(newParams);
|
||||
const { history, location } = this.props;
|
||||
|
||||
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`;
|
||||
|
@ -204,6 +199,7 @@ const withValidation = ({ requiredParams }, verifyRevisions = true) => (
|
|||
|
||||
Validation.propTypes = {
|
||||
location: PropTypes.shape({}).isRequired,
|
||||
history: PropTypes.shape({}).isRequired,
|
||||
};
|
||||
|
||||
return Validation;
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import moment from 'moment';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { getTitle, getFrameworkName } from '../helpers';
|
||||
import { getJobsUrl } from '../../helpers/url';
|
||||
|
@ -43,9 +44,9 @@ const AlertHeader = ({
|
|||
return (
|
||||
<Container>
|
||||
<Row>
|
||||
<a
|
||||
<Link
|
||||
className="text-dark"
|
||||
href={`#/alerts?id=${alertSummary.id}`}
|
||||
to={`./alerts?id=${alertSummary.id}&hideDwnToInv=0`}
|
||||
id={`alert summary ${alertSummary.id.toString()} title`}
|
||||
data-testid={`alert summary ${alertSummary.id.toString()} title`}
|
||||
>
|
||||
|
@ -60,7 +61,7 @@ const AlertHeader = ({
|
|||
className="icon-superscript"
|
||||
/>
|
||||
</h3>
|
||||
</a>
|
||||
</Link>
|
||||
</Row>
|
||||
<Row className="font-weight-normal">
|
||||
<Col className="p-0" xs="auto">{`${moment(
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
faCheck,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { faStar as faStarRegular } from '@fortawesome/free-regular-svg-icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { createQueryParams } from '../../helpers/url';
|
||||
import { getStatus, getGraphsURL, modifyAlert, formatNumber } from '../helpers';
|
||||
|
@ -107,11 +108,10 @@ export default class AlertTableRow extends React.Component {
|
|||
return (
|
||||
<span>
|
||||
{` ${text} `}
|
||||
<a
|
||||
href={`#/alerts?id=${alertId}`}
|
||||
rel="noopener noreferrer"
|
||||
<Link
|
||||
to={`./alerts?id=${alertId}`}
|
||||
className="text-darker-info"
|
||||
>{`alert #${alertId}`}</a>
|
||||
>{`alert #${alertId}`}</Link>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
@ -263,7 +263,7 @@ export default class AlertTableRow extends React.Component {
|
|||
newRevision: alertSummary.revision,
|
||||
};
|
||||
|
||||
return `#/comparesubtest${createQueryParams(urlParameters)}`;
|
||||
return `./comparesubtest${createQueryParams(urlParameters)}`;
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -345,6 +345,7 @@ export default class AlertTableRow extends React.Component {
|
|||
textClass="detail-hint"
|
||||
text={`${alert.amount_pct}%`}
|
||||
tooltipText={`Absolute difference: ${alert.amount_abs}`}
|
||||
autohide={false}
|
||||
/>
|
||||
</td>
|
||||
<td className="table-width-lg">
|
||||
|
|
|
@ -32,10 +32,10 @@ class AlertsView extends React.Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
const { frameworks, validated } = this.props;
|
||||
const extendedOptions = this.extendDropdownOptions(frameworks);
|
||||
this.extendedOptions = this.extendDropdownOptions(frameworks);
|
||||
this.state = {
|
||||
filters: this.getFiltersFromParams(validated, extendedOptions),
|
||||
frameworkOptions: extendedOptions,
|
||||
filters: this.getFiltersFromParams(validated),
|
||||
frameworkOptions: this.extendedOptions,
|
||||
page: validated.page ? parseInt(validated.page, 10) : 1,
|
||||
errorMessages: [],
|
||||
alertSummaries: [],
|
||||
|
@ -55,43 +55,30 @@ class AlertsView extends React.Component {
|
|||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { count, frameworkOptions } = this.state;
|
||||
const { validated } = this.props;
|
||||
const prevValitated = prevProps.validated;
|
||||
const { count } = this.state;
|
||||
|
||||
if (prevState.count !== 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 prevParams = parseQueryParams(prevProps.location.search);
|
||||
// we're using local state for id instead of validated.id because once
|
||||
// the user navigates from the id=<alert> view back to the main alerts view
|
||||
// the Validation component won't reset the id (since the query param doesn't exist
|
||||
// unless there is a value)
|
||||
if (this.props.location.search !== prevProps.location.search) {
|
||||
this.setState({ id: params.id || null }, this.fetchAlertSummaries);
|
||||
if (params.id) {
|
||||
validated.updateParams({ hideDwnToInv: 0 });
|
||||
}
|
||||
if (params.id !== prevParams.id) {
|
||||
this.setState(
|
||||
{ id: params.id || null, filters: this.getFiltersFromParams(params) },
|
||||
this.fetchAlertSummaries,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getFiltersFromParams = (validated, frameworkOptions) => {
|
||||
getFiltersFromParams = (
|
||||
validated,
|
||||
frameworkOptions = this.extendedOptions,
|
||||
) => {
|
||||
return {
|
||||
status: this.getDefaultStatus(),
|
||||
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)
|
||||
this.setState({ loading: !update, errorMessages: [] });
|
||||
const { user } = this.props;
|
||||
|
|
|
@ -14,14 +14,16 @@ export default class AlertsViewControls extends React.Component {
|
|||
};
|
||||
|
||||
updateFilter = (filter) => {
|
||||
const { setFiltersState, filters } = this.props;
|
||||
const { setFiltersState, filters, updateViewState } = this.props;
|
||||
const prevValue = filters[filter];
|
||||
setFiltersState({ [filter]: !prevValue });
|
||||
updateViewState({ page: 1 });
|
||||
};
|
||||
|
||||
updateStatus = (status) => {
|
||||
const { setFiltersState } = this.props;
|
||||
const { setFiltersState, updateViewState } = this.props;
|
||||
setFiltersState({ status });
|
||||
updateViewState({ page: 1 });
|
||||
};
|
||||
|
||||
updateFramework = (selectedFramework) => {
|
||||
|
@ -29,7 +31,7 @@ export default class AlertsViewControls extends React.Component {
|
|||
const framework = frameworkOptions.find(
|
||||
(item) => item.name === selectedFramework,
|
||||
);
|
||||
updateViewState({ bugTemplate: null });
|
||||
updateViewState({ bugTemplate: null, page: 1 });
|
||||
setFiltersState({ framework }, this.fetchAlertSummaries);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { getTitle } from '../helpers';
|
||||
import SimpleTooltip from '../../shared/SimpleTooltip';
|
||||
|
@ -51,9 +52,9 @@ export default class DownstreamSummary extends React.Component {
|
|||
<SimpleTooltip
|
||||
text={
|
||||
<span>
|
||||
<a href={`perf.html#/alerts?id=${id}`} className="text-info">
|
||||
<Link to={`./alerts?id=${id}`} className="text-info">
|
||||
#{id}
|
||||
</a>
|
||||
</Link>
|
||||
{position === 0 ? '' : ', '}
|
||||
</span>
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ export default class StatusDropdown extends React.Component {
|
|||
framework: getFrameworkName(frameworks, alertSummary.framework),
|
||||
revision: 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),
|
||||
};
|
||||
|
||||
|
|
|
@ -85,9 +85,9 @@ export default class CompareSelectorView extends React.Component {
|
|||
};
|
||||
}
|
||||
if (framework === 0) {
|
||||
history.push(`/infracompare${createQueryParams(params)}`);
|
||||
history.push(`./infracompare${createQueryParams(params)}`);
|
||||
} else {
|
||||
history.push(`/compare${createQueryParams(params)}`);
|
||||
history.push(`./compare${createQueryParams(params)}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -84,9 +84,7 @@ class CompareSubtestsView extends React.PureComponent {
|
|||
|
||||
links.push({
|
||||
title: 'replicates',
|
||||
href: `perf.html#/comparesubtestdistribution${createQueryParams(
|
||||
params,
|
||||
)}`,
|
||||
to: `./comparesubtestdistribution${createQueryParams(params)}`,
|
||||
});
|
||||
}
|
||||
const signatureHash = !oldResults
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
faThumbsUp,
|
||||
faHashtag,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import SimpleTooltip from '../../shared/SimpleTooltip';
|
||||
import { displayNumber, formatNumber, getHashBasedId } from '../helpers';
|
||||
|
@ -81,7 +82,7 @@ export default class CompareTableRow extends React.PureComponent {
|
|||
{rowLevelResults.links &&
|
||||
rowLevelResults.links.map((link) => (
|
||||
<span key={link.title}>
|
||||
<a href={link.href}>{` ${link.title}`}</a>
|
||||
<Link to={link.to}>{` ${link.title}`}</Link>
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
|
|
|
@ -257,7 +257,7 @@ export default class CompareTableView extends React.Component {
|
|||
{hasSubtests && (
|
||||
<Link
|
||||
to={{
|
||||
pathname: '/compare',
|
||||
pathname: './compare',
|
||||
search: createQueryParams(params),
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -103,13 +103,11 @@ class CompareView extends React.PureComponent {
|
|||
params.selectedTimeRange = timeRange.value;
|
||||
}
|
||||
|
||||
const detailsLink = `perf.html#/comparesubtest${createQueryParams(
|
||||
params,
|
||||
)}`;
|
||||
const detailsLink = `./comparesubtest${createQueryParams(params)}`;
|
||||
|
||||
links.push({
|
||||
title: 'subtests',
|
||||
href: detailsLink,
|
||||
to: detailsLink,
|
||||
});
|
||||
}
|
||||
const signatureHash = !oldResults
|
||||
|
|
|
@ -46,7 +46,7 @@ const TableAverage = ({ value, stddev, stddevpct, replicates }) => {
|
|||
tooltipText
|
||||
)
|
||||
}
|
||||
tooltipClass={replicates.length > 1 ? 'compare-table-tooltip' : ''}
|
||||
innerClassName={replicates.length > 1 ? 'compare-table-tooltip' : ''}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-muted">No results</span>
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
faExclamationCircle,
|
||||
faTimes,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { alertStatusMap, endpoints } from '../constants';
|
||||
import { getJobsUrl, createQueryParams, getApiUrl } from '../../helpers/url';
|
||||
|
@ -203,7 +204,7 @@ const GraphTooltip = ({
|
|||
{dataPointDetails.jobId && prevRevision && ', '}
|
||||
{prevRevision && (
|
||||
<a
|
||||
href={`#/comparesubtest${createQueryParams({
|
||||
href={`./comparesubtest${createQueryParams({
|
||||
originalProject: testDetails.repository_name,
|
||||
newProject: testDetails.repository_name,
|
||||
originalRevision: prevRevision,
|
||||
|
@ -229,16 +230,14 @@ const GraphTooltip = ({
|
|||
</span>
|
||||
{dataPointDetails.alertSummary && (
|
||||
<p>
|
||||
<a
|
||||
href={`perf.html#/alerts?id=${dataPointDetails.alertSummary.id}`}
|
||||
>
|
||||
<Link to={`./alerts?id=${dataPointDetails.alertSummary.id}`}>
|
||||
<FontAwesomeIcon
|
||||
className="text-warning"
|
||||
icon={faExclamationCircle}
|
||||
size="sm"
|
||||
/>
|
||||
{` Alert # ${dataPointDetails.alertSummary.id}`}
|
||||
</a>
|
||||
</Link>
|
||||
<span className="text-muted">
|
||||
{` - ${alertStatus} `}
|
||||
{alert && alert.related_summary_id && (
|
||||
|
@ -247,11 +246,11 @@ const GraphTooltip = ({
|
|||
dataPointDetails.alertSummary.id
|
||||
? 'to'
|
||||
: 'from'}
|
||||
<a
|
||||
href={`#/alerts?id=${alert.related_summary_id}`}
|
||||
<Link
|
||||
to={`./alerts?id=${alert.related_summary_id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{` alert # ${alert.related_summary_id}`}</a>
|
||||
>{` alert # ${alert.related_summary_id}`}</Link>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
|
|
@ -54,7 +54,7 @@ const TableView = ({
|
|||
group_state: 'expanded',
|
||||
});
|
||||
|
||||
const compareUrl = `#/comparesubtest${createQueryParams({
|
||||
const compareUrl = `./comparesubtest${createQueryParams({
|
||||
originalProject: item.repository_name,
|
||||
newProject: item.repository_name,
|
||||
originalRevision: prevRevision,
|
||||
|
|
|
@ -312,7 +312,7 @@ export const getGraphsLink = function getGraphsLink(
|
|||
params.timerange = timeRange;
|
||||
}
|
||||
|
||||
return `perf.html#/graphs?${queryString.stringify(params)}`;
|
||||
return `./graphs?${queryString.stringify(params)}`;
|
||||
};
|
||||
|
||||
export const createNoiseMetric = function createNoiseMetric(
|
||||
|
@ -363,7 +363,7 @@ export const createGraphsLinks = (
|
|||
|
||||
links.push({
|
||||
title: 'graph',
|
||||
href: graphsLink,
|
||||
to: graphsLink,
|
||||
});
|
||||
|
||||
return links;
|
||||
|
@ -385,7 +385,7 @@ export const getGraphsURL = (
|
|||
alertRepository,
|
||||
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
|
||||
// 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
|
||||
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}`;
|
||||
}
|
||||
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 { 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 Health from './Health';
|
||||
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) {
|
||||
const params = new URLSearchParams(search);
|
||||
|
||||
|
@ -14,26 +21,23 @@ function hasProps(search) {
|
|||
|
||||
const App = () => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path="/pushhealth.html"
|
||||
render={(props) =>
|
||||
hasProps(props.location.search) ? (
|
||||
<Health {...props} />
|
||||
) : (
|
||||
<Usage {...props} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route name="notfound" component={NotFound} />
|
||||
</Switch>
|
||||
</div>
|
||||
<Switch>
|
||||
<Route
|
||||
path="/"
|
||||
render={(props) =>
|
||||
hasProps(props.location.search) ? (
|
||||
<Health {...props} />
|
||||
) : (
|
||||
<Usage {...props} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route name="notfound" component={NotFound} />
|
||||
</Switch>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -67,7 +67,7 @@ class Usage extends Component {
|
|||
<tr key={revision} data-testid={`facet-${revision}`}>
|
||||
<td data-testid="facet-link">
|
||||
<a
|
||||
href={`/pushhealth.html?repo=try&revision=${revision}`}
|
||||
href={`/push-health?repo=try&revision=${revision}`}
|
||||
title="See Push Health"
|
||||
>
|
||||
{revision}
|
||||
|
|
|
@ -4,8 +4,6 @@ import { render } from 'react-dom';
|
|||
// Treeherder Styles
|
||||
import '../css/failure-summary.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-notifications.css';
|
||||
import './pushhealth.css';
|
||||
|
|
|
@ -72,7 +72,7 @@ export default class ComparePageTitle extends React.Component {
|
|||
changeQueryParam = (newTitle) => {
|
||||
const params = getAllUrlParams();
|
||||
params.set('pageTitle', newTitle);
|
||||
replaceLocation(params, '/compare');
|
||||
replaceLocation(params);
|
||||
};
|
||||
|
||||
userActionListener = async (event) => {
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
|
||||
const menuItems = [
|
||||
{
|
||||
href: '/userguide.html',
|
||||
href: '/userguide',
|
||||
icon: faQuestionCircle,
|
||||
text: 'User Guide',
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { getInspectTaskUrl } from '../helpers/url';
|
||||
import { getJobSearchStrHref } from '../helpers/job';
|
||||
|
@ -57,12 +58,12 @@ export default class JobInfo extends React.PureComponent {
|
|||
<strong>Job: </strong>
|
||||
{showJobFilters ? (
|
||||
<React.Fragment>
|
||||
<a
|
||||
<Link
|
||||
title="Filter jobs containing these keywords"
|
||||
href={getJobSearchStrHref(searchStr)}
|
||||
to={{ search: getJobSearchStrHref(searchStr) }}
|
||||
>
|
||||
{searchStr}
|
||||
</a>
|
||||
</Link>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<span>{searchStr}</span>
|
||||
|
|
|
@ -6,12 +6,13 @@ import {
|
|||
DropdownMenu,
|
||||
DropdownItem,
|
||||
} from 'reactstrap';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const choices = [
|
||||
{ url: '/', text: 'Treeherder' },
|
||||
{ url: '/perf.html', text: 'Perfherder' },
|
||||
{ url: '/intermittent-failures.html', text: 'Intermittent Failures View' },
|
||||
{ url: '/pushhealth.html', text: 'Push Health Usage' },
|
||||
{ url: '/jobs', text: 'Treeherder' },
|
||||
{ url: '/perfherder', text: 'Perfherder' },
|
||||
{ url: '/intermittent-failures', text: 'Intermittent Failures View' },
|
||||
{ url: '/push-health', text: 'Push Health Usage' },
|
||||
];
|
||||
|
||||
export default class LogoMenu extends React.PureComponent {
|
||||
|
@ -35,8 +36,8 @@ export default class LogoMenu extends React.PureComponent {
|
|||
</DropdownToggle>
|
||||
<DropdownMenu>
|
||||
{menuChoices.map((choice) => (
|
||||
<DropdownItem key={choice.text} tag="a" href={choice.url}>
|
||||
{choice.text}
|
||||
<DropdownItem key={choice.text}>
|
||||
<Link to={choice.url}>{choice.text}</Link>
|
||||
</DropdownItem>
|
||||
))}
|
||||
</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,
|
||||
placement,
|
||||
textClass,
|
||||
tooltipClass,
|
||||
innerClassName,
|
||||
autohide,
|
||||
} = this.props;
|
||||
|
||||
|
@ -26,7 +26,7 @@ export default class SimpleTooltip extends React.Component {
|
|||
<UncontrolledTooltip
|
||||
placement={placement}
|
||||
target={this.tooltipRef}
|
||||
innerClassName={tooltipClass}
|
||||
innerClassName={innerClassName}
|
||||
autohide={autohide}
|
||||
>
|
||||
{tooltipText}
|
||||
|
@ -41,13 +41,13 @@ SimpleTooltip.propTypes = {
|
|||
.isRequired,
|
||||
textClass: PropTypes.string,
|
||||
placement: PropTypes.string,
|
||||
tooltipClass: PropTypes.string,
|
||||
innerClassName: PropTypes.string,
|
||||
autohide: PropTypes.bool,
|
||||
};
|
||||
|
||||
SimpleTooltip.defaultProps = {
|
||||
textClass: '',
|
||||
placement: 'top',
|
||||
tooltipClass: '',
|
||||
innerClassName: '',
|
||||
autohide: true,
|
||||
};
|
||||
|
|
|
@ -10,7 +10,9 @@ export const tcClientIdMap = {
|
|||
'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}`;
|
||||
|
||||
|
@ -26,7 +28,7 @@ export const checkRootUrl = (rootUrl) => {
|
|||
// and the default login rootUrls are for https://firefox-ci-tc.services.mozilla.com
|
||||
if (
|
||||
rootUrl === prodFirefoxRootUrl &&
|
||||
clientId === 'treeherder-taskcluster-staging'
|
||||
clientId === 'treeherder-taskcluster-staging-client'
|
||||
) {
|
||||
return stagingFirefoxRootUrl;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,10 @@ import UserGuideHeader from './UserGuideHeader';
|
|||
import UserGuideBody from './UserGuideBody';
|
||||
import UserGuideFooter from './UserGuideFooter';
|
||||
|
||||
import '../css/treeherder-userguide.css';
|
||||
import '../css/treeherder-job-buttons.css';
|
||||
import '../css/treeherder-base.css';
|
||||
|
||||
const App = () => (
|
||||
<div id="userguide">
|
||||
<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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
|
||||
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"
|
||||
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:
|
||||
version "1.2.0"
|
||||
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"
|
||||
integrity sha512-BXUKIkUuh6cmmxzi5OIbUJxrG8OAk2MqoL1DtO3Wo9D2faJg2ph5ntyuQeLqaHJmzER6H5tllCDA9ZnNe9BVGg==
|
||||
|
||||
history@5.0.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:
|
||||
history@4.10.1, history@^4.9.0:
|
||||
version "4.10.1"
|
||||
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
|
||||
integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
|
||||
|
|
Загрузка…
Ссылка в новой задаче