зеркало из https://github.com/mozilla/treeherder.git
Bug 1510280 - Convert Pushes context to Redux
This commit is contained in:
Родитель
9654825fca
Коммит
9181ff4b94
|
@ -71,6 +71,8 @@
|
|||
"reactstrap": "7.1.0",
|
||||
"redux": "4.0.4",
|
||||
"redux-debounce": "1.0.1",
|
||||
"redux-mock-store": "1.5.3",
|
||||
"redux-thunk": "2.3.0",
|
||||
"taskcluster-client-web": "8.1.1",
|
||||
"taskcluster-lib-scopes": "10.0.2",
|
||||
"webpack": "4.36.1",
|
||||
|
|
|
@ -136,11 +136,8 @@ class Treeherder(Base):
|
|||
|
||||
def select_repository(self, name):
|
||||
self.find_element(*self._repo_menu_locator).click()
|
||||
# FIXME workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1411264
|
||||
el = self.find_element(By.CSS_SELECTOR, 'body')
|
||||
locator = (self._repo_locator[0], self._repo_locator[1].format(name))
|
||||
self.find_element(*locator).click()
|
||||
self.wait.until(expected.staleness_of(el))
|
||||
self.wait_for_page_to_load()
|
||||
|
||||
def switch_to_perfherder(self):
|
||||
|
@ -264,26 +261,21 @@ class Treeherder(Base):
|
|||
self.wait.until(lambda _: self.page.pinboard.is_displayed)
|
||||
|
||||
def set_as_bottom_of_range(self):
|
||||
# FIXME workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1411264
|
||||
el = self.page.find_element(By.CSS_SELECTOR, 'body')
|
||||
el = self.page.find_element(By.CSS_SELECTOR, '.push')
|
||||
self.find_element(*self._dropdown_toggle_locator).click()
|
||||
self.find_element(*self._set_bottom_of_range_locator).click()
|
||||
self.wait.until(expected.staleness_of(el))
|
||||
self.page.wait_for_page_to_load()
|
||||
|
||||
def set_as_top_of_range(self):
|
||||
# FIXME workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1411264
|
||||
el = self.page.find_element(By.CSS_SELECTOR, 'body')
|
||||
el = self.page.find_element(By.CSS_SELECTOR, '.push')
|
||||
self.find_element(*self._dropdown_toggle_locator).click()
|
||||
self.find_element(*self._set_top_of_range_locator).click()
|
||||
self.wait.until(expected.staleness_of(el))
|
||||
self.page.wait_for_page_to_load()
|
||||
|
||||
def view(self):
|
||||
# FIXME workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1411264
|
||||
el = self.page.find_element(By.CSS_SELECTOR, 'body')
|
||||
self.find_element(*self._datestamp_locator).click()
|
||||
self.wait.until(expected.staleness_of(el))
|
||||
self.page.wait_for_page_to_load()
|
||||
|
||||
class Job(Region):
|
||||
|
|
|
@ -0,0 +1,188 @@
|
|||
import React from 'react';
|
||||
import { fetchMock } from 'fetch-mock';
|
||||
import { Provider } from 'react-redux';
|
||||
import {
|
||||
render,
|
||||
cleanup,
|
||||
waitForElement,
|
||||
waitForElementToBeRemoved,
|
||||
} from '@testing-library/react';
|
||||
|
||||
import {
|
||||
getProjectUrl,
|
||||
replaceLocation,
|
||||
setUrlParam,
|
||||
} 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 PushList from '../../../ui/job-view/pushes/PushList';
|
||||
import { PinnedJobs } from '../../../ui/job-view/context/PinnedJobs';
|
||||
|
||||
describe('PushList', () => {
|
||||
const repoName = 'autoland';
|
||||
const currentRepo = {
|
||||
id: 4,
|
||||
repository_group: {
|
||||
name: 'development',
|
||||
description: 'meh',
|
||||
},
|
||||
name: repoName,
|
||||
dvcs_type: 'hg',
|
||||
url: 'https://hg.mozilla.org/autoland',
|
||||
branch: null,
|
||||
codebase: 'gecko',
|
||||
description: '',
|
||||
active_status: 'active',
|
||||
performance_alerts_enabled: false,
|
||||
expire_performance_data: true,
|
||||
is_try_repo: false,
|
||||
pushLogUrl: 'https://hg.mozilla.org/autoland/pushloghtml',
|
||||
revisionHrefPrefix: 'https://hg.mozilla.org/autoland/rev/',
|
||||
getRevisionHref: () => 'foo',
|
||||
getPushLogHref: () => 'foo',
|
||||
};
|
||||
const testPushList = (store, filterModel) => (
|
||||
<Provider store={store}>
|
||||
<PinnedJobs>
|
||||
<div id="th-global-content">
|
||||
<PushList
|
||||
user={{ isLoggedIn: false }}
|
||||
repoName={repoName}
|
||||
currentRepo={currentRepo}
|
||||
filterModel={filterModel}
|
||||
duplicateJobsVisible={false}
|
||||
groupCountsExpanded={false}
|
||||
pushHealthVisibility="None"
|
||||
getAllShownJobs={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</PinnedJobs>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
beforeAll(() => {
|
||||
fetchMock.get(getProjectUrl('/push/?full=true&count=10', repoName), {
|
||||
...pushListFixture,
|
||||
results: pushListFixture.results.slice(0, 2),
|
||||
});
|
||||
fetchMock.get(
|
||||
getProjectUrl(
|
||||
'/push/?full=true&count=10&fromchange=ba9c692786e95143b8df3f4b3e9b504dfbc589a0',
|
||||
repoName,
|
||||
),
|
||||
{
|
||||
...pushListFixture,
|
||||
results: pushListFixture.results.slice(0, 1),
|
||||
},
|
||||
);
|
||||
fetchMock.get(
|
||||
getProjectUrl(
|
||||
'/push/?full=true&count=10&tochange=d5b037941b0ebabcc9b843f24d926e9d65961087',
|
||||
repoName,
|
||||
),
|
||||
{
|
||||
...pushListFixture,
|
||||
results: pushListFixture.results.slice(1, 2),
|
||||
},
|
||||
);
|
||||
fetchMock.get(
|
||||
getProjectUrl(
|
||||
'/jobs/?push_id=511138&count=2000&return_type=list',
|
||||
repoName,
|
||||
),
|
||||
jobListFixtureOne,
|
||||
);
|
||||
|
||||
fetchMock.mock(
|
||||
getProjectUrl(
|
||||
'/jobs/?push_id=511137&count=2000&return_type=list',
|
||||
repoName,
|
||||
),
|
||||
jobListFixtureTwo,
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
replaceLocation({});
|
||||
});
|
||||
|
||||
const push1Id = 'push-511138';
|
||||
const push2Id = 'push-511137';
|
||||
const push1Revision = 'ba9c692786e95143b8df3f4b3e9b504dfbc589a0';
|
||||
const push2Revision = 'd5b037941b0ebabcc9b843f24d926e9d65961087';
|
||||
|
||||
test('should have 2 pushes', async () => {
|
||||
const { store } = configureStore();
|
||||
const { getAllByText } = render(testPushList(store, new FilterModel()));
|
||||
const pushes = await waitForElement(() => getAllByText('View Tests'));
|
||||
|
||||
expect(pushes).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('should switch to single loaded revision and back to 2', async () => {
|
||||
const { store } = configureStore();
|
||||
const { getByTestId, getAllByText } = render(
|
||||
testPushList(store, new FilterModel()),
|
||||
);
|
||||
|
||||
expect(await waitForElement(() => getAllByText('View Tests'))).toHaveLength(
|
||||
2,
|
||||
);
|
||||
|
||||
// fireEvent.click(push) not clicking the link, so must set the url param
|
||||
setUrlParam('revision', push2Revision); // click push 2
|
||||
await waitForElementToBeRemoved(() => getByTestId('push-511138'));
|
||||
|
||||
expect(await waitForElement(() => getAllByText('View Tests'))).toHaveLength(
|
||||
1,
|
||||
);
|
||||
|
||||
setUrlParam('revision', null);
|
||||
await waitForElement(() => getByTestId(push1Id));
|
||||
expect(await waitForElement(() => getAllByText('View Tests'))).toHaveLength(
|
||||
2,
|
||||
);
|
||||
});
|
||||
|
||||
test('should reload pushes when setting fromchange', async () => {
|
||||
const { store } = configureStore();
|
||||
const { getByTestId, getAllByText } = render(
|
||||
testPushList(store, new FilterModel()),
|
||||
);
|
||||
|
||||
expect(await waitForElement(() => getAllByText('View Tests'))).toHaveLength(
|
||||
2,
|
||||
);
|
||||
|
||||
setUrlParam('fromchange', push1Revision);
|
||||
await waitForElementToBeRemoved(() => getByTestId(push2Id));
|
||||
expect(await waitForElement(() => getAllByText('View Tests'))).toHaveLength(
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
test('should reload pushes when setting tochange', async () => {
|
||||
const { store } = configureStore();
|
||||
const { getByTestId, getAllByText } = render(
|
||||
testPushList(store, new FilterModel()),
|
||||
);
|
||||
|
||||
expect(await waitForElement(() => getAllByText('View Tests'))).toHaveLength(
|
||||
2,
|
||||
);
|
||||
|
||||
setUrlParam('tochange', push2Revision);
|
||||
await waitForElementToBeRemoved(() => getByTestId(push1Id));
|
||||
expect(await waitForElement(() => getAllByText('View Tests'))).toHaveLength(
|
||||
1,
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,82 @@
|
|||
import React from 'react';
|
||||
import { fetchMock } from 'fetch-mock';
|
||||
import { Provider } from 'react-redux';
|
||||
import { render, cleanup, waitForElement } from '@testing-library/react';
|
||||
import thunk from 'redux-thunk';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
|
||||
import { replaceLocation, setUrlParam } from '../../../ui/helpers/location';
|
||||
import FilterModel from '../../../ui/models/filter';
|
||||
import { PinnedJobs } from '../../../ui/job-view/context/PinnedJobs';
|
||||
import SecondaryNavBar from '../../../ui/job-view/headerbars/SecondaryNavBar';
|
||||
import { initialState } from '../../../ui/job-view/redux/stores/pushes';
|
||||
import repos from '../mock/repositories';
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
const repoName = 'autoland';
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.get('https://treestatus.mozilla-releng.net/trees/autoland', {
|
||||
result: {
|
||||
message_of_the_day: '',
|
||||
reason: '',
|
||||
status: 'open',
|
||||
tree: 'autoland',
|
||||
},
|
||||
});
|
||||
setUrlParam('repo', repoName);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
fetchMock.reset();
|
||||
replaceLocation({});
|
||||
});
|
||||
|
||||
describe('SecondaryNavBar', () => {
|
||||
const testSecondaryNavBar = (store, filterModel) => (
|
||||
<Provider store={store}>
|
||||
<PinnedJobs>
|
||||
<SecondaryNavBar
|
||||
updateButtonClick={() => {}}
|
||||
serverChanged={false}
|
||||
filterModel={filterModel}
|
||||
repos={repos}
|
||||
setCurrentRepoTreeStatus={() => {}}
|
||||
duplicateJobsVisible={false}
|
||||
groupCountsExpanded={false}
|
||||
toggleFieldFilterVisible={() => {}}
|
||||
/>
|
||||
</PinnedJobs>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
test('should 52 unclassified', async () => {
|
||||
const store = mockStore({
|
||||
pushes: {
|
||||
...initialState,
|
||||
allUnclassifiedFailureCount: 52,
|
||||
filteredUnclassifiedFailureCount: 0,
|
||||
},
|
||||
});
|
||||
const { getByText } = render(testSecondaryNavBar(store, new FilterModel()));
|
||||
|
||||
expect(await waitForElement(() => getByText(repoName))).toBeInTheDocument();
|
||||
expect(await waitForElement(() => getByText('52'))).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should 22 unclassified and 10 filtered unclassified', async () => {
|
||||
const store = mockStore({
|
||||
pushes: {
|
||||
...initialState,
|
||||
allUnclassifiedFailureCount: 22,
|
||||
filteredUnclassifiedFailureCount: 10,
|
||||
},
|
||||
});
|
||||
const { getByText } = render(testSecondaryNavBar(store, new FilterModel()));
|
||||
|
||||
expect(await waitForElement(() => getByText(repoName))).toBeInTheDocument();
|
||||
expect(await waitForElement(() => getByText('22'))).toBeInTheDocument();
|
||||
expect(await waitForElement(() => getByText('10'))).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -1,72 +0,0 @@
|
|||
import React from 'react';
|
||||
import { fetchMock } from 'fetch-mock';
|
||||
import { mount } from 'enzyme/build';
|
||||
|
||||
import { getProjectUrl } from '../../../../ui/helpers/location';
|
||||
import { PushesClass } from '../../../../ui/job-view/context/Pushes';
|
||||
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';
|
||||
|
||||
describe('Pushes context', () => {
|
||||
const repoName = 'autoland';
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.mock(
|
||||
getProjectUrl('/push/?full=true&count=10', repoName),
|
||||
pushListFixture,
|
||||
);
|
||||
|
||||
fetchMock.mock(
|
||||
getProjectUrl(
|
||||
'/jobs/?return_type=list&count=2000&result_set_id=1',
|
||||
repoName,
|
||||
),
|
||||
jobListFixtureOne,
|
||||
);
|
||||
|
||||
fetchMock.mock(
|
||||
getProjectUrl(
|
||||
'/jobs/?return_type=list&count=2000&result_set_id=2',
|
||||
repoName,
|
||||
),
|
||||
jobListFixtureTwo,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
/*
|
||||
Tests Pushes context
|
||||
*/
|
||||
test('should have 2 pushes', async () => {
|
||||
const pushes = mount(
|
||||
<PushesClass
|
||||
filterModel={new FilterModel()}
|
||||
notify={() => {}}
|
||||
setSelectedJob={() => {}}
|
||||
>
|
||||
<div />
|
||||
</PushesClass>,
|
||||
);
|
||||
await pushes.instance().fetchPushes(10);
|
||||
expect(pushes.state('pushList')).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('should have id of 1 in current repo', async () => {
|
||||
const pushes = mount(
|
||||
<PushesClass
|
||||
filterModel={new FilterModel()}
|
||||
notify={() => {}}
|
||||
setSelectedJob={() => {}}
|
||||
>
|
||||
<div />
|
||||
</PushesClass>,
|
||||
);
|
||||
await pushes.instance().fetchPushes(10);
|
||||
expect(pushes.state('pushList')[0].id).toBe(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,280 @@
|
|||
import { fetchMock } from 'fetch-mock';
|
||||
import thunk from 'redux-thunk';
|
||||
import { cleanup } from '@testing-library/react';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
|
||||
import {
|
||||
getProjectUrl,
|
||||
getQueryString,
|
||||
replaceLocation,
|
||||
setUrlParam,
|
||||
} from '../../../../ui/helpers/location';
|
||||
import pushListFixture from '../../mock/push_list';
|
||||
import pushListFromChangeFixture from '../../mock/pushListFromchange';
|
||||
import jobMap from '../../mock/job_map';
|
||||
import pollPushListFixture from '../../mock/poll_push_list';
|
||||
import jobListFixtureOne from '../../mock/job_list/job_1';
|
||||
import jobListFixtureTwo from '../../mock/job_list/job_2';
|
||||
import revisionTips from '../../mock/revisionTips';
|
||||
import {
|
||||
LOADING,
|
||||
ADD_PUSHES,
|
||||
CLEAR_PUSHES,
|
||||
SET_PUSHES,
|
||||
RECALCULATE_UNCLASSIFIED_COUNTS,
|
||||
UPDATE_JOB_MAP,
|
||||
initialState,
|
||||
reducer,
|
||||
fetchPushes,
|
||||
pollPushes,
|
||||
fetchNextPushes,
|
||||
updateRange,
|
||||
} from '../../../../ui/job-view/redux/stores/pushes';
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
describe('Pushes Redux store', () => {
|
||||
const repoName = 'autoland';
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
fetchMock.reset();
|
||||
replaceLocation({});
|
||||
});
|
||||
|
||||
test('should get pushes with fetchPushes', async () => {
|
||||
fetchMock.get(
|
||||
getProjectUrl('/push/?full=true&count=10', repoName),
|
||||
pushListFixture,
|
||||
);
|
||||
fetchMock.get(
|
||||
getProjectUrl('/jobs/?push_id=1&count=2000&return_type=list', repoName),
|
||||
jobListFixtureOne,
|
||||
);
|
||||
|
||||
fetchMock.mock(
|
||||
getProjectUrl('/jobs/?push_id=2&count=2000&return_type=list', repoName),
|
||||
jobListFixtureTwo,
|
||||
);
|
||||
const store = mockStore({ pushes: initialState });
|
||||
|
||||
await store.dispatch(fetchPushes());
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions[0]).toEqual({ type: LOADING });
|
||||
expect(actions[1]).toEqual({
|
||||
type: ADD_PUSHES,
|
||||
pushResults: {
|
||||
pushList: pushListFixture.results,
|
||||
oldestPushTimestamp: 1562867109,
|
||||
allUnclassifiedFailureCount: 0,
|
||||
filteredUnclassifiedFailureCount: 0,
|
||||
revisionTips,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should add new push and jobs when polling', async () => {
|
||||
fetchMock.get(
|
||||
getProjectUrl(
|
||||
'/push/?full=true&count=10&fromchange=ba9c692786e95143b8df3f4b3e9b504dfbc589a0',
|
||||
repoName,
|
||||
),
|
||||
pollPushListFixture,
|
||||
);
|
||||
fetchMock.mock(
|
||||
`begin:${getProjectUrl(
|
||||
'/jobs/?push_id__in=511138&last_modified__gt',
|
||||
repoName,
|
||||
)}`,
|
||||
jobListFixtureTwo,
|
||||
);
|
||||
|
||||
const initialPush = pushListFixture.results[0];
|
||||
const store = mockStore({
|
||||
pushes: { ...initialState, pushList: [initialPush] },
|
||||
});
|
||||
|
||||
await store.dispatch(pollPushes());
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual([
|
||||
{
|
||||
type: ADD_PUSHES,
|
||||
pushResults: {
|
||||
pushList: [initialPush, ...pollPushListFixture.results],
|
||||
allUnclassifiedFailureCount: 0,
|
||||
filteredUnclassifiedFailureCount: 0,
|
||||
oldestPushTimestamp: 1562707488,
|
||||
revisionTips: [
|
||||
{
|
||||
author: 'jarilvalenciano@gmail.com',
|
||||
revision: 'ba9c692786e95143b8df3f4b3e9b504dfbc589a0',
|
||||
title:
|
||||
"Fuzzy query='debugger | 'node-devtools&query='mozlint-eslint&query='mochitest-devtools",
|
||||
},
|
||||
{
|
||||
author: 'reviewbot',
|
||||
revision: '750b802afc594b92aba99de82a51772c75526c44',
|
||||
title: 'try_task_config for code-review',
|
||||
},
|
||||
{
|
||||
author: 'reviewbot',
|
||||
revision: '90da061f588d1315ee4087225d041d7474d9dfd8',
|
||||
title: 'try_task_config for code-review',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('fetchNextPushes should update revision param on url', async () => {
|
||||
fetchMock.get(
|
||||
getProjectUrl(
|
||||
'/push/?full=true&count=11&push_timestamp__lte=1562867957',
|
||||
repoName,
|
||||
),
|
||||
pollPushListFixture,
|
||||
);
|
||||
|
||||
const push = pushListFixture.results[0];
|
||||
const store = mockStore({
|
||||
pushes: {
|
||||
...initialState,
|
||||
pushList: [push],
|
||||
oldestPushTimestamp: push.push_timestamp,
|
||||
},
|
||||
});
|
||||
setUrlParam('revision', push.revision);
|
||||
await store.dispatch(fetchNextPushes(10));
|
||||
|
||||
expect(getQueryString()).toEqual(
|
||||
'tochange=ba9c692786e95143b8df3f4b3e9b504dfbc589a0&fromchange=90da061f588d1315ee4087225d041d7474d9dfd8',
|
||||
);
|
||||
});
|
||||
|
||||
test('should pare down to single revision updateRange', async () => {
|
||||
const store = mockStore({
|
||||
pushes: { ...initialState, pushList: pushListFixture.results },
|
||||
});
|
||||
|
||||
await store.dispatch(
|
||||
updateRange({ revision: '9692347caff487cdcd889489b8e89a825fe6bbd1' }),
|
||||
);
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual([
|
||||
{ countPinnedJobs: 0, type: 'CLEAR_JOB' },
|
||||
{
|
||||
type: SET_PUSHES,
|
||||
pushResults: {
|
||||
pushList: [pushListFixture.results[2]],
|
||||
allUnclassifiedFailureCount: 0,
|
||||
filteredUnclassifiedFailureCount: 0,
|
||||
oldestPushTimestamp: 1562867702,
|
||||
revisionTips: [revisionTips[2]],
|
||||
jobMap: {},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should fetch a new set of pushes with updateRange', async () => {
|
||||
fetchMock.get(
|
||||
getProjectUrl(
|
||||
'/push/?full=true&count=10&fromchange=9692347caff487cdcd889489b8e89a825fe6bbd1',
|
||||
repoName,
|
||||
),
|
||||
pushListFromChangeFixture,
|
||||
);
|
||||
|
||||
const store = mockStore({
|
||||
pushes: initialState,
|
||||
});
|
||||
|
||||
setUrlParam('fromchange', '9692347caff487cdcd889489b8e89a825fe6bbd1');
|
||||
await store.dispatch(
|
||||
updateRange({ fromchange: '9692347caff487cdcd889489b8e89a825fe6bbd1' }),
|
||||
);
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual([
|
||||
{
|
||||
type: CLEAR_PUSHES,
|
||||
},
|
||||
{
|
||||
type: LOADING,
|
||||
},
|
||||
{
|
||||
type: ADD_PUSHES,
|
||||
pushResults: {
|
||||
pushList: pushListFromChangeFixture.results,
|
||||
allUnclassifiedFailureCount: 0,
|
||||
filteredUnclassifiedFailureCount: 0,
|
||||
oldestPushTimestamp: 1562867702,
|
||||
revisionTips: revisionTips.slice(0, 3),
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should clear the pushList with clearPushes', async () => {
|
||||
const push = pushListFixture.results[0];
|
||||
const reduced = reducer(
|
||||
{
|
||||
...initialState,
|
||||
pushList: pushListFixture.results,
|
||||
oldestPushTimestamp: push.push_timestamp,
|
||||
},
|
||||
{ type: CLEAR_PUSHES },
|
||||
);
|
||||
|
||||
expect(reduced.pushList).toEqual([]);
|
||||
expect(reduced.allUnclassifiedFailureCount).toEqual(0);
|
||||
expect(reduced.filteredUnclassifiedFailureCount).toEqual(0);
|
||||
});
|
||||
|
||||
test('should replace the pushList with setPushes', async () => {
|
||||
const push = pushListFixture.results[0];
|
||||
const push2 = pushListFixture.results[1];
|
||||
const reduced = reducer(
|
||||
{
|
||||
...initialState,
|
||||
pushList: [push],
|
||||
oldestPushTimestamp: push.push_timestamp,
|
||||
},
|
||||
{ type: SET_PUSHES, pushResults: { pushList: [push2] } },
|
||||
);
|
||||
|
||||
expect(reduced.pushList).toEqual([push2]);
|
||||
expect(reduced.allUnclassifiedFailureCount).toEqual(0);
|
||||
expect(reduced.filteredUnclassifiedFailureCount).toEqual(0);
|
||||
});
|
||||
|
||||
test('should get new unclassified counts with recalculateUnclassifiedCounts', async () => {
|
||||
setUrlParam('job_type_symbol', 'cpp');
|
||||
const reduced = reducer(
|
||||
{
|
||||
...initialState,
|
||||
jobMap,
|
||||
},
|
||||
{ type: RECALCULATE_UNCLASSIFIED_COUNTS },
|
||||
);
|
||||
|
||||
expect(Object.keys(reduced.jobMap)).toHaveLength(26);
|
||||
expect(reduced.allUnclassifiedFailureCount).toEqual(3);
|
||||
expect(reduced.filteredUnclassifiedFailureCount).toEqual(1);
|
||||
});
|
||||
|
||||
test('should add to the jobMap with updateJobMap', async () => {
|
||||
const jobList = [{ id: 5 }, { id: 6 }, { id: 7 }];
|
||||
|
||||
const reduced = reducer(
|
||||
{ ...initialState, jobMap },
|
||||
{ type: UPDATE_JOB_MAP, jobList },
|
||||
);
|
||||
|
||||
expect(Object.keys(reduced.jobMap)).toHaveLength(29);
|
||||
});
|
||||
});
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"meta": {
|
||||
"fromchange": "aeb22500c6c6aa956c4cb0ffa63c94498abe9fba",
|
||||
"count": 10,
|
||||
"repository": "try",
|
||||
"filter_params": {
|
||||
"full": "true",
|
||||
"count": "10",
|
||||
"push_timestamp__gte": 1562707044
|
||||
}
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"id": 509406,
|
||||
"revision": "750b802afc594b92aba99de82a51772c75526c44",
|
||||
"author": "reviewbot",
|
||||
"revisions": [
|
||||
{
|
||||
"result_set_id": 509406,
|
||||
"repository_id": 4,
|
||||
"revision": "750b802afc594b92aba99de82a51772c75526c44",
|
||||
"author": "pulselistener",
|
||||
"comments": "try_task_config for code-review\nDifferential Diff: PHID-DIFF-nchtqrjy3xppmpi24bqm"
|
||||
},
|
||||
{
|
||||
"result_set_id": 509406,
|
||||
"repository_id": 4,
|
||||
"revision": "066e705cb98c4c29f0f38cfc26b52fd55e896041",
|
||||
"author": "pulselistener",
|
||||
"comments": "Bug 1506219 - Use a known remote for applications loaded from file:// URIs\n\nFall back to using Google's DNS server to determine the associated local\naddresses for web applications that are not loaded over the network. This\nincludes the loopback address, which is frequently used in the unit tests.\n\nProvide a separate function for setting the target for the default local\naddress lookup.\n\nDifferential Revision: https://phabricator.services.mozilla.com/D37331\nDifferential Diff: PHID-DIFF-nchtqrjy3xppmpi24bqm"
|
||||
},
|
||||
{
|
||||
"result_set_id": 509406,
|
||||
"repository_id": 4,
|
||||
"revision": "c709e7badbd42e10d370eec7e4af8ed9c082683d",
|
||||
"author": "pulselistener",
|
||||
"comments": "Bug 1506219 - Update default route according to latest IETF draft\n\ndraft-ietf-rtcweb-ip-handling specifies that the default route is the route\nused to reach the origin rather than the one used to reach the internet, so\nupdate the IP routing to reflect this. This addresses issues in which the\nwrong IP address is used on machines with multiple network interfaces.\n\nDifferential Revision: https://phabricator.services.mozilla.com/D36831\nDifferential Diff: PHID-DIFF-mqwgz7sovuhvrqwsem54"
|
||||
}
|
||||
],
|
||||
"revision_count": 3,
|
||||
"push_timestamp": 1562707604,
|
||||
"repository_id": 4
|
||||
},
|
||||
{
|
||||
"id": 509405,
|
||||
"revision": "90da061f588d1315ee4087225d041d7474d9dfd8",
|
||||
"author": "reviewbot",
|
||||
"revisions": [
|
||||
{
|
||||
"result_set_id": 509405,
|
||||
"repository_id": 4,
|
||||
"revision": "90da061f588d1315ee4087225d041d7474d9dfd8",
|
||||
"author": "pulselistener",
|
||||
"comments": "try_task_config for code-review\nDifferential Diff: PHID-DIFF-cvcrzv4vjod6ftfgdfun"
|
||||
},
|
||||
{
|
||||
"result_set_id": 509405,
|
||||
"repository_id": 4,
|
||||
"revision": "328a6b4d0468706ce044a381956e4bba50da23cb",
|
||||
"author": "pulselistener",
|
||||
"comments": "styles for message icon\nDifferential Diff: PHID-DIFF-cvcrzv4vjod6ftfgdfun"
|
||||
}
|
||||
],
|
||||
"revision_count": 2,
|
||||
"push_timestamp": 1562707488,
|
||||
"repository_id": 4
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
{
|
||||
"meta": {
|
||||
"count": 10,
|
||||
"repository": "try",
|
||||
"filter_params": { "full": "true", "count": "10" }
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"id": 511138,
|
||||
"revision": "ba9c692786e95143b8df3f4b3e9b504dfbc589a0",
|
||||
"author": "jarilvalenciano@gmail.com",
|
||||
"revisions": [
|
||||
{
|
||||
"result_set_id": 511138,
|
||||
"repository_id": 4,
|
||||
"revision": "ba9c692786e95143b8df3f4b3e9b504dfbc589a0",
|
||||
"author": "jaril <jarilvalenciano@gmail.com>",
|
||||
"comments": "Fuzzy query='debugger | 'node-devtools&query='mozlint-eslint&query='mochitest-devtools"
|
||||
},
|
||||
{
|
||||
"result_set_id": 511138,
|
||||
"repository_id": 4,
|
||||
"revision": "e7d76c5305da04ecc804394b016fc99f1b23fa15",
|
||||
"author": "jaril <jarilvalenciano@gmail.com>",
|
||||
"comments": "return a packet+sendactorevent and move destroy call"
|
||||
}
|
||||
],
|
||||
"revision_count": 2,
|
||||
"push_timestamp": 1562867957,
|
||||
"repository_id": 4
|
||||
},
|
||||
{
|
||||
"id": 511137,
|
||||
"revision": "d5b037941b0ebabcc9b843f24d926e9d65961087",
|
||||
"author": "bhackett@mozilla.com",
|
||||
"revisions": [
|
||||
{
|
||||
"result_set_id": 511137,
|
||||
"repository_id": 4,
|
||||
"revision": "d5b037941b0ebabcc9b843f24d926e9d65961087",
|
||||
"author": "Brian Hackett <bhackett1024@gmail.com>",
|
||||
"comments": "FUZZY"
|
||||
},
|
||||
{
|
||||
"result_set_id": 511137,
|
||||
"repository_id": 4,
|
||||
"revision": "543107d8404e34a3efca7397a35660c3e619009c",
|
||||
"author": "Brian Hackett <bhackett1024@gmail.com>",
|
||||
"comments": "FUZZY"
|
||||
}
|
||||
],
|
||||
"revision_count": 7,
|
||||
"push_timestamp": 1562867912,
|
||||
"repository_id": 4
|
||||
},
|
||||
{
|
||||
"id": 511136,
|
||||
"revision": "9692347caff487cdcd889489b8e89a825fe6bbd1",
|
||||
"author": "reviewbot",
|
||||
"revisions": [
|
||||
{
|
||||
"result_set_id": 511136,
|
||||
"repository_id": 4,
|
||||
"revision": "9692347caff487cdcd889489b8e89a825fe6bbd1",
|
||||
"author": "pulselistener",
|
||||
"comments": "try_task_config for code-review\nDifferential Diff: PHID-DIFF-5k5lfg4h2file7lxc73j"
|
||||
},
|
||||
{
|
||||
"result_set_id": 511136,
|
||||
"repository_id": 4,
|
||||
"revision": "9e9b39fa7ca9a40ac7ec9361f2b87c5de1680453",
|
||||
"author": "pulselistener",
|
||||
"comments": "Bug 1556854 - Enable ESLint for dom/media/test/ (automatic changes). r?"
|
||||
}
|
||||
],
|
||||
"revision_count": 2,
|
||||
"push_timestamp": 1562867702,
|
||||
"repository_id": 4
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,46 +1,249 @@
|
|||
{
|
||||
"meta": {
|
||||
"count": 10,
|
||||
"filter_params": {
|
||||
"count": "10",
|
||||
"full": "true"
|
||||
},
|
||||
"repository": "mozilla-inbound"
|
||||
"repository": "try",
|
||||
"filter_params": { "full": "true", "count": "10" }
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"repository_id": 1,
|
||||
"revision_count": 1,
|
||||
"author": "john@doe.com",
|
||||
"comments": "Back out bug 1071880 for causing bug 1083952.",
|
||||
"push_timestamp": 1424272815,
|
||||
"id": 511138,
|
||||
"revision": "ba9c692786e95143b8df3f4b3e9b504dfbc589a0",
|
||||
"author": "jarilvalenciano@gmail.com",
|
||||
"revisions": [
|
||||
{
|
||||
"repository_id": 2,
|
||||
"author": "john@doe.com",
|
||||
"comments": "Back out bug 1071880 for causing bug 1083952.",
|
||||
"revision": "acf46fe8b054"
|
||||
"result_set_id": 511138,
|
||||
"repository_id": 4,
|
||||
"revision": "ba9c692786e95143b8df3f4b3e9b504dfbc589a0",
|
||||
"author": "jaril <jarilvalenciano@gmail.com>",
|
||||
"comments": "Fuzzy query='debugger | 'node-devtools&query='mozlint-eslint&query='mochitest-devtools"
|
||||
},
|
||||
{
|
||||
"result_set_id": 511138,
|
||||
"repository_id": 4,
|
||||
"revision": "e7d76c5305da04ecc804394b016fc99f1b23fa15",
|
||||
"author": "jaril <jarilvalenciano@gmail.com>",
|
||||
"comments": "return a packet+sendactorevent and move destroy call"
|
||||
}
|
||||
],
|
||||
"id": 1,
|
||||
"revision": "acf46fe8b054"
|
||||
"revision_count": 2,
|
||||
"push_timestamp": 1562867957,
|
||||
"repository_id": 4
|
||||
},
|
||||
{
|
||||
"repository_id": 1,
|
||||
"revision_count": 1,
|
||||
"author": "john@doe.com",
|
||||
"comments": "Bug 1133254 - Dehandlify shape-updating object methods, allow setting multiple flags on an object at once, r=terrence.",
|
||||
"push_timestamp": 1424272126,
|
||||
"id": 511137,
|
||||
"revision": "d5b037941b0ebabcc9b843f24d926e9d65961087",
|
||||
"author": "bhackett@mozilla.com",
|
||||
"revisions": [
|
||||
{
|
||||
"repository_id": 2,
|
||||
"author": "john@doe.com",
|
||||
"comments": "Bug 1133254 - Dehandlify shape-updating object methods, allow setting multiple flags on an object at once, r=terrence.",
|
||||
"revision": "b1cc2dd3e35c"
|
||||
"result_set_id": 511137,
|
||||
"repository_id": 4,
|
||||
"revision": "d5b037941b0ebabcc9b843f24d926e9d65961087",
|
||||
"author": "Brian Hackett <bhackett1024@gmail.com>",
|
||||
"comments": "FUZZY"
|
||||
},
|
||||
{
|
||||
"result_set_id": 511137,
|
||||
"repository_id": 4,
|
||||
"revision": "543107d8404e34a3efca7397a35660c3e619009c",
|
||||
"author": "Brian Hackett <bhackett1024@gmail.com>",
|
||||
"comments": "FUZZY"
|
||||
}
|
||||
],
|
||||
"id": 2,
|
||||
"revision": "b1cc2dd3e35c"
|
||||
"revision_count": 7,
|
||||
"push_timestamp": 1562867912,
|
||||
"repository_id": 4
|
||||
},
|
||||
{
|
||||
"id": 511136,
|
||||
"revision": "9692347caff487cdcd889489b8e89a825fe6bbd1",
|
||||
"author": "reviewbot",
|
||||
"revisions": [
|
||||
{
|
||||
"result_set_id": 511136,
|
||||
"repository_id": 4,
|
||||
"revision": "9692347caff487cdcd889489b8e89a825fe6bbd1",
|
||||
"author": "pulselistener",
|
||||
"comments": "try_task_config for code-review\nDifferential Diff: PHID-DIFF-5k5lfg4h2file7lxc73j"
|
||||
},
|
||||
{
|
||||
"result_set_id": 511136,
|
||||
"repository_id": 4,
|
||||
"revision": "9e9b39fa7ca9a40ac7ec9361f2b87c5de1680453",
|
||||
"author": "pulselistener",
|
||||
"comments": "Bug 1556854 - Enable ESLint for dom/media/test/ (automatic changes). r?"
|
||||
}
|
||||
],
|
||||
"revision_count": 2,
|
||||
"push_timestamp": 1562867702,
|
||||
"repository_id": 4
|
||||
},
|
||||
{
|
||||
"id": 511135,
|
||||
"revision": "fb941355d1f073f3e9c82773af102fad8fb8cb1d",
|
||||
"author": "reviewbot",
|
||||
"revisions": [
|
||||
{
|
||||
"result_set_id": 511135,
|
||||
"repository_id": 4,
|
||||
"revision": "fb941355d1f073f3e9c82773af102fad8fb8cb1d",
|
||||
"author": "pulselistener",
|
||||
"comments": "try_task_config for code-review\nDifferential Diff: PHID-DIFF-m3q6etamztukb6x5uisj"
|
||||
},
|
||||
{
|
||||
"result_set_id": 511135,
|
||||
"repository_id": 4,
|
||||
"revision": "57886e8e358c94d10b251779a00a869c9d175e84",
|
||||
"author": "pulselistener",
|
||||
"comments": "Bug 1555861 - Make devtools storage.js module use storage principal; r=jdescottes"
|
||||
}
|
||||
],
|
||||
"revision_count": 2,
|
||||
"push_timestamp": 1562867515,
|
||||
"repository_id": 4
|
||||
},
|
||||
{
|
||||
"id": 511134,
|
||||
"revision": "97844f48bb922932e530dfe1cf56de258df9fcf6",
|
||||
"author": "dmalyshau@mozilla.com",
|
||||
"revisions": [
|
||||
{
|
||||
"result_set_id": 511134,
|
||||
"repository_id": 4,
|
||||
"revision": "97844f48bb922932e530dfe1cf56de258df9fcf6",
|
||||
"author": "Dzmitry Malyshau <dmalyshau@mozilla.com>",
|
||||
"comments": "Fuzzy query='-qr&query=^webrender-\n\nPushed via `mach try fuzzy`\n"
|
||||
},
|
||||
{
|
||||
"result_set_id": 511134,
|
||||
"repository_id": 4,
|
||||
"revision": "558569b7be512b153bf46b05cbc4167d5e2aaafd",
|
||||
"author": "Dzmitry Malyshau <dmalyshau@mozilla.com>",
|
||||
"comments": "Use discard in WR primitives\n"
|
||||
}
|
||||
],
|
||||
"revision_count": 2,
|
||||
"push_timestamp": 1562867484,
|
||||
"repository_id": 4
|
||||
},
|
||||
{
|
||||
"id": 511133,
|
||||
"revision": "ed1d0ed9b5754ec5cdd119d9555ad5e3f7fc26ba",
|
||||
"author": "mtigley@mozilla.com",
|
||||
"revisions": [
|
||||
{
|
||||
"result_set_id": 511133,
|
||||
"repository_id": 4,
|
||||
"revision": "ed1d0ed9b5754ec5cdd119d9555ad5e3f7fc26ba",
|
||||
"author": "Micah Tigley <mtigley@mozilla.com>",
|
||||
"comments": "Try Chooser Enhanced (139 tasks selected)\n\nPushed via `mach try chooser`\n"
|
||||
},
|
||||
{
|
||||
"result_set_id": 511133,
|
||||
"repository_id": 4,
|
||||
"revision": "3dcab12ca955f0c170c9f188a20ec7cd28483498",
|
||||
"author": "Micah Tigley <mtigley@mozilla.com>",
|
||||
"comments": "Bug 1559418 - Create a base card for Firefox Lockwise. r=ewright\n"
|
||||
}
|
||||
],
|
||||
"revision_count": 2,
|
||||
"push_timestamp": 1562867441,
|
||||
"repository_id": 4
|
||||
},
|
||||
{
|
||||
"id": 511132,
|
||||
"revision": "ec7bb2bba29ff6b9865bcf7942d50245019642c6",
|
||||
"author": "reviewbot",
|
||||
"revisions": [
|
||||
{
|
||||
"result_set_id": 511132,
|
||||
"repository_id": 4,
|
||||
"revision": "ec7bb2bba29ff6b9865bcf7942d50245019642c6",
|
||||
"author": "pulselistener",
|
||||
"comments": "try_task_config for code-review\nDifferential Diff: PHID-DIFF-apavn5kkofkmv4cc7s5v"
|
||||
},
|
||||
{
|
||||
"result_set_id": 511132,
|
||||
"repository_id": 4,
|
||||
"revision": "12020b041a908cb009ebaea653dfa9ffdb0eb958",
|
||||
"author": "pulselistener",
|
||||
"comments": "Bug 1559418 - Create a base card for Firefox Lockwise. r=ewright\nDifferential Diff: PHID-DIFF-apavn5kkofkmv4cc7s5v"
|
||||
}
|
||||
],
|
||||
"revision_count": 2,
|
||||
"push_timestamp": 1562867409,
|
||||
"repository_id": 4
|
||||
},
|
||||
{
|
||||
"id": 511131,
|
||||
"revision": "ebdd6cf6caba154bc523aa85d311c804b5115231",
|
||||
"author": "mjzffr@gmail.com",
|
||||
"revisions": [
|
||||
{
|
||||
"result_set_id": 511131,
|
||||
"repository_id": 4,
|
||||
"revision": "ebdd6cf6caba154bc523aa85d311c804b5115231",
|
||||
"author": "Maja Frydrychowicz <mjzffr@gmail.com>",
|
||||
"comments": "Fuzzy query=android web-platform\n\nPushed via `mach try fuzzy`"
|
||||
},
|
||||
{
|
||||
"result_set_id": 511131,
|
||||
"repository_id": 4,
|
||||
"revision": "81adb3b8c972102208937422e449ad7eed8b99a3",
|
||||
"author": "Maja Frydrychowicz <mjzffr@gmail.com>",
|
||||
"comments": "Bug 1563766 - Update android x86_64 emulator to r29.0.11, for web-platform tests"
|
||||
}
|
||||
],
|
||||
"revision_count": 2,
|
||||
"push_timestamp": 1562867331,
|
||||
"repository_id": 4
|
||||
},
|
||||
{
|
||||
"id": 511130,
|
||||
"revision": "6babddb082091c1e0d4e1b70aab053e3c73a2e6d",
|
||||
"author": "reviewbot",
|
||||
"revisions": [
|
||||
{
|
||||
"result_set_id": 511130,
|
||||
"repository_id": 4,
|
||||
"revision": "6babddb082091c1e0d4e1b70aab053e3c73a2e6d",
|
||||
"author": "pulselistener",
|
||||
"comments": "try_task_config for code-review\nDifferential Diff: PHID-DIFF-5lhsufjebvqccviuujkr"
|
||||
},
|
||||
{
|
||||
"result_set_id": 511130,
|
||||
"repository_id": 4,
|
||||
"revision": "7992854a66f6a8348a1b7b6552197588824fc2ce",
|
||||
"author": "pulselistener",
|
||||
"comments": "Bug 1561537 - Add badge/feature-callout style that matches the design spec\n\nTags: #secure-revision"
|
||||
}
|
||||
],
|
||||
"revision_count": 2,
|
||||
"push_timestamp": 1562867292,
|
||||
"repository_id": 4
|
||||
},
|
||||
{
|
||||
"id": 511129,
|
||||
"revision": "fb66bad25e85e350c03352f143bc7319afc32548",
|
||||
"author": "reviewbot",
|
||||
"revisions": [
|
||||
{
|
||||
"result_set_id": 511129,
|
||||
"repository_id": 4,
|
||||
"revision": "fb66bad25e85e350c03352f143bc7319afc32548",
|
||||
"author": "pulselistener",
|
||||
"comments": "try_task_config for code-review\nDifferential Diff: PHID-DIFF-gmhgohxmkcqnzkppqipo"
|
||||
},
|
||||
{
|
||||
"result_set_id": 511129,
|
||||
"repository_id": 4,
|
||||
"revision": "6227d7f05cf3130374cd4b19a2f411b50993d2c4",
|
||||
"author": "pulselistener",
|
||||
"comments": "Bug 1563692 - Move all CDP's JSON packet handling to Connection. r=remote-protocol-reviewers"
|
||||
}
|
||||
],
|
||||
"revision_count": 2,
|
||||
"push_timestamp": 1562867109,
|
||||
"repository_id": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,452 +1,59 @@
|
|||
[
|
||||
{
|
||||
"id": 1,
|
||||
"repository_group": 1,
|
||||
"repository_group": {
|
||||
"name": "development",
|
||||
"description": "Collection of repositories where code initially lands in the development process"
|
||||
},
|
||||
"name": "mozilla-central",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/mozilla-central",
|
||||
"branch": null,
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"repository_group": 1,
|
||||
"name": "mozilla-inbound",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/integration/mozilla-inbound",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"repository_group": 1,
|
||||
"name": "b2g-inbound",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/integration/b2g-inbound/",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
"active_status": "active",
|
||||
"performance_alerts_enabled": false,
|
||||
"expire_performance_data": false,
|
||||
"is_try_repo": false,
|
||||
"pushLogUrl": "https://hg.mozilla.org/mozilla-central/pushloghtml",
|
||||
"revisionHrefPrefix": "https://hg.mozilla.org/mozilla-central/rev/"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"repository_group": 3,
|
||||
"repository_group": {
|
||||
"name": "development",
|
||||
"description": "Collection of repositories where code initially lands in the development process"
|
||||
},
|
||||
"name": "try",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/try",
|
||||
"branch": null,
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
"active_status": "active",
|
||||
"performance_alerts_enabled": false,
|
||||
"expire_performance_data": true,
|
||||
"is_try_repo": true,
|
||||
"pushLogUrl": "https://hg.mozilla.org/try/pushloghtml",
|
||||
"revisionHrefPrefix": "https://hg.mozilla.org/try/rev/"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"repository_group": 2,
|
||||
"name": "mozilla-aurora",
|
||||
"id": 77,
|
||||
"repository_group": {
|
||||
"name": "development",
|
||||
"description": "Collection of repositories where code initially lands in the development process"
|
||||
},
|
||||
"name": "autoland",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/releases/mozilla-aurora",
|
||||
"url": "https://hg.mozilla.org/integration/autoland",
|
||||
"branch": null,
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "onhold"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"repository_group": 2,
|
||||
"name": "mozilla-beta",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/releases/mozilla-beta",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"repository_group": 2,
|
||||
"name": "mozilla-release",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/releases/mozilla-release",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"repository_group": 2,
|
||||
"name": "mozilla-esr17",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/releases/mozilla-esr17",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"repository_group": 2,
|
||||
"name": "mozilla-esr24",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/releases/mozilla-esr24",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"repository_group": 2,
|
||||
"name": "mozilla-b2g18",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/releases/mozilla-b2g18",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"repository_group": 2,
|
||||
"name": "mozilla-b2g18_v1_1_0_hd",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/releases/mozilla-b2g18_v1_1_0_hd",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"repository_group": 1,
|
||||
"name": "addon-sdk",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/projects/addon-sdk",
|
||||
"codebase": "jetpack",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"repository_group": 1,
|
||||
"name": "build-system",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/projects/build-system",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"repository_group": 1,
|
||||
"name": "fx-team",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/integration/fx-team",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"repository_group": 1,
|
||||
"name": "graphics",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/projects/graphics",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"repository_group": 1,
|
||||
"name": "ionmonkey",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/projects/ionmonkey",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"repository_group": 1,
|
||||
"name": "profiling",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/projects/profiling",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"repository_group": 1,
|
||||
"name": "services-central",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/services/services-central",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"repository_group": 1,
|
||||
"name": "ux",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/projects/ux",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"repository_group": 1,
|
||||
"name": "alder",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/projects/alder",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"repository_group": 1,
|
||||
"name": "ash",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/projects/ash",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"repository_group": 1,
|
||||
"name": "birch",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/projects/birch",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"repository_group": 1,
|
||||
"name": "cedar",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/projects/cedar",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"repository_group": 1,
|
||||
"name": "cypress",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/projects/cypress",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"repository_group": 1,
|
||||
"name": "date",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/projects/date",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "onhold"
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"repository_group": 1,
|
||||
"name": "elm",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/projects/elm",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"repository_group": 1,
|
||||
"name": "fig",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/projects/fig",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"repository_group": 1,
|
||||
"name": "gum",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/projects/gum",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"repository_group": 1,
|
||||
"name": "holly",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/projects/holly",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"repository_group": 1,
|
||||
"name": "jamun",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/projects/jamun",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"repository_group": 1,
|
||||
"name": "larch",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/projects/larch",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"repository_group": 1,
|
||||
"name": "maple",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/projects/maple",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 33,
|
||||
"repository_group": 1,
|
||||
"name": "oak",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/projects/oak",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 34,
|
||||
"repository_group": 1,
|
||||
"name": "pine",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/projects/pine",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"repository_group": 1,
|
||||
"name": "thunderbird-trunk",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/comm-central",
|
||||
"codebase": "?",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 36,
|
||||
"repository_group": 3,
|
||||
"name": "thunderbird-try",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/try-comm-central",
|
||||
"codebase": "?",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"repository_group": 2,
|
||||
"name": "thunderbird-aurora",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/releases/comm-aurora",
|
||||
"codebase": "?",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 38,
|
||||
"repository_group": 2,
|
||||
"name": "thunderbird-beta",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/releases/comm-beta",
|
||||
"codebase": "?",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 39,
|
||||
"repository_group": 2,
|
||||
"name": "thunderbird-esr24",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/releases/comm-esr24",
|
||||
"codebase": "?",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 40,
|
||||
"repository_group": 2,
|
||||
"name": "thunderbird-esr17",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/releases/comm-esr17",
|
||||
"codebase": "?",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"repository_group": 1,
|
||||
"name": "accessibility",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/projects/accessibility",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 42,
|
||||
"repository_group": 1,
|
||||
"name": "devtools",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/projects/devtools",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 43,
|
||||
"repository_group": 1,
|
||||
"name": "jaegermonkey",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/projects/jaegermonkey",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 44,
|
||||
"repository_group": 1,
|
||||
"name": "unknown",
|
||||
"dvcs_type": "hg",
|
||||
"url": "",
|
||||
"codebase": "",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
},
|
||||
{
|
||||
"id": 45,
|
||||
"repository_group": 2,
|
||||
"name": "mozilla-b2g26_v1_2",
|
||||
"dvcs_type": "hg",
|
||||
"url": "https://hg.mozilla.org/releases/mozilla-b2g26_v1_2",
|
||||
"codebase": "gecko",
|
||||
"description": "",
|
||||
"active_status": "active"
|
||||
"description": "The destination for automatically landed Firefox commits.",
|
||||
"active_status": "active",
|
||||
"performance_alerts_enabled": true,
|
||||
"expire_performance_data": false,
|
||||
"is_try_repo": false,
|
||||
"pushLogUrl": "https://hg.mozilla.org/integration/autoland/pushloghtml",
|
||||
"revisionHrefPrefix": "https://hg.mozilla.org/integration/autoland/rev/"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
[
|
||||
{
|
||||
"author": "jarilvalenciano@gmail.com",
|
||||
"revision": "ba9c692786e95143b8df3f4b3e9b504dfbc589a0",
|
||||
"title": "Fuzzy query='debugger | 'node-devtools&query='mozlint-eslint&query='mochitest-devtools"
|
||||
},
|
||||
{
|
||||
"author": "bhackett@mozilla.com",
|
||||
"revision": "d5b037941b0ebabcc9b843f24d926e9d65961087",
|
||||
"title": "FUZZY"
|
||||
},
|
||||
{
|
||||
"author": "reviewbot",
|
||||
"revision": "9692347caff487cdcd889489b8e89a825fe6bbd1",
|
||||
"title": "try_task_config for code-review"
|
||||
},
|
||||
{
|
||||
"author": "reviewbot",
|
||||
"revision": "fb941355d1f073f3e9c82773af102fad8fb8cb1d",
|
||||
"title": "try_task_config for code-review"
|
||||
},
|
||||
{
|
||||
"author": "dmalyshau@mozilla.com",
|
||||
"revision": "97844f48bb922932e530dfe1cf56de258df9fcf6",
|
||||
"title": "Fuzzy query='-qr&query=^webrender-"
|
||||
},
|
||||
{
|
||||
"author": "mtigley@mozilla.com",
|
||||
"revision": "ed1d0ed9b5754ec5cdd119d9555ad5e3f7fc26ba",
|
||||
"title": "Try Chooser Enhanced (139 tasks selected)"
|
||||
},
|
||||
{
|
||||
"author": "reviewbot",
|
||||
"revision": "ec7bb2bba29ff6b9865bcf7942d50245019642c6",
|
||||
"title": "try_task_config for code-review"
|
||||
},
|
||||
{
|
||||
"author": "mjzffr@gmail.com",
|
||||
"revision": "ebdd6cf6caba154bc523aa85d311c804b5115231",
|
||||
"title": "Fuzzy query=android web-platform"
|
||||
},
|
||||
{
|
||||
"author": "reviewbot",
|
||||
"revision": "6babddb082091c1e0d4e1b70aab053e3c73a2e6d",
|
||||
"title": "try_task_config for code-review"
|
||||
},
|
||||
{
|
||||
"author": "reviewbot",
|
||||
"revision": "fb66bad25e85e350c03352f143bc7319afc32548",
|
||||
"title": "try_task_config for code-review"
|
||||
}
|
||||
]
|
|
@ -35,16 +35,16 @@ describe('JobModel', () => {
|
|||
});
|
||||
|
||||
test('should return a page of results by default', async () => {
|
||||
const jobList = await JobModel.getList({ count: 2 });
|
||||
const { data } = await JobModel.getList({ count: 2 });
|
||||
|
||||
expect(jobList).toHaveLength(2);
|
||||
expect(data).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('should return all the pages when fetch_all==true', async () => {
|
||||
const jobList = await JobModel.getList({ count: 2 }, { fetch_all: true });
|
||||
test('should return all the pages when fetchAll==true', async () => {
|
||||
const { data } = await JobModel.getList({ count: 2 }, { fetchAll: true });
|
||||
|
||||
expect(jobList).toHaveLength(3);
|
||||
expect(jobList[2].id).toBe(3);
|
||||
expect(data).toHaveLength(3);
|
||||
expect(data[2].id).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -213,6 +213,8 @@ export const thEvents = {
|
|||
openLogviewer: 'open-logviewer-EVT',
|
||||
autoclassifyIgnore: 'ac-ignore-EVT',
|
||||
applyNewJobs: 'apply-new-jobs-EVT',
|
||||
filtersUpdated: 'filters-updated-EVT',
|
||||
clearPinboard: 'clear-pinboard-EVT',
|
||||
};
|
||||
|
||||
export const phTimeRanges = [
|
||||
|
|
|
@ -25,6 +25,18 @@ export const setLocation = function setLocation(params, hashPrefix = '/jobs') {
|
|||
window.location.hash = `#${hashPrefix}${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,
|
||||
|
|
|
@ -5,7 +5,7 @@ import pick from 'lodash/pick';
|
|||
import isEqual from 'lodash/isEqual';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { thFavicons } from '../helpers/constants';
|
||||
import { thFavicons, thEvents } from '../helpers/constants';
|
||||
import ShortcutTable from '../shared/ShortcutTable';
|
||||
import { hasUrlFilterChanges, matchesDefaults } from '../helpers/filter';
|
||||
import { getAllUrlParams, getRepo } from '../helpers/location';
|
||||
|
@ -16,7 +16,6 @@ import FilterModel from '../models/filter';
|
|||
import RepositoryModel from '../models/repository';
|
||||
|
||||
import Notifications from './Notifications';
|
||||
import { Pushes } from './context/Pushes';
|
||||
import { PinnedJobs } from './context/PinnedJobs';
|
||||
import PrimaryNavBar from './headerbars/PrimaryNavBar';
|
||||
import ActiveFilters from './headerbars/ActiveFilters';
|
||||
|
@ -106,6 +105,7 @@ 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);
|
||||
|
||||
// Get the current Treeherder revision and poll to notify on updates.
|
||||
this.fetchDeployedRevision().then(revision => {
|
||||
|
@ -197,6 +197,17 @@ class App extends React.Component {
|
|||
link.href = thFavicons[status] || thFavicons.open;
|
||||
};
|
||||
|
||||
getAllShownJobs = pushId => {
|
||||
const {
|
||||
pushes: { jobMap },
|
||||
} = store.getState();
|
||||
const jobList = Object.values(jobMap);
|
||||
|
||||
return pushId
|
||||
? jobList.filter(job => job.push_id === pushId && job.visible)
|
||||
: jobList.filter(job => job.visible);
|
||||
};
|
||||
|
||||
toggleFieldFilterVisible = () => {
|
||||
this.setState(prevState => ({
|
||||
isFieldFilterVisible: !prevState.isFieldFilterVisible,
|
||||
|
@ -229,6 +240,10 @@ class App extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
handleFiltersUpdated = () => {
|
||||
this.setState({ filterModel: new FilterModel() });
|
||||
};
|
||||
|
||||
// If ``show`` is a boolean, then set to that value. If it's not, then toggle
|
||||
showOnScreenShortcuts = show => {
|
||||
const { showShortCuts } = this.state;
|
||||
|
@ -300,87 +315,86 @@ class App extends React.Component {
|
|||
return (
|
||||
<div id="global-container" className="height-minus-navbars">
|
||||
<Provider store={store}>
|
||||
<Pushes filterModel={filterModel}>
|
||||
<PinnedJobs>
|
||||
<KeyboardShortcuts
|
||||
<PinnedJobs>
|
||||
<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}
|
||||
/>
|
||||
<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}
|
||||
<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}
|
||||
/>
|
||||
)}
|
||||
<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}
|
||||
filterBarFilters={filterBarFilters}
|
||||
isFieldFilterVisible={isFieldFilterVisible}
|
||||
toggleFieldFilterVisible={this.toggleFieldFilterVisible}
|
||||
duplicateJobsVisible={duplicateJobsVisible}
|
||||
groupCountsExpanded={groupCountsExpanded}
|
||||
pushHealthVisibility={pushHealthVisibility}
|
||||
getAllShownJobs={this.getAllShownJobs}
|
||||
/>
|
||||
)}
|
||||
{serverChangedDelayed && (
|
||||
<UpdateAvailable
|
||||
updateButtonClick={this.updateButtonClick}
|
||||
/>
|
||||
)}
|
||||
<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}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<DetailsPanel
|
||||
resizedHeight={detailsHeight}
|
||||
currentRepo={currentRepo}
|
||||
repoName={repoName}
|
||||
user={user}
|
||||
classificationTypes={classificationTypes}
|
||||
classificationMap={classificationMap}
|
||||
/>
|
||||
</SplitPane>
|
||||
<Notifications />
|
||||
{showShortCuts && (
|
||||
<div
|
||||
id="onscreen-overlay"
|
||||
onClick={() => this.showOnScreenShortcuts(false)}
|
||||
>
|
||||
<div id="onscreen-shortcuts">
|
||||
<div className="col-8">
|
||||
<ShortcutTable />
|
||||
</div>
|
||||
</div>
|
||||
<DetailsPanel
|
||||
resizedHeight={detailsHeight}
|
||||
currentRepo={currentRepo}
|
||||
repoName={repoName}
|
||||
user={user}
|
||||
classificationTypes={classificationTypes}
|
||||
classificationMap={classificationMap}
|
||||
/>
|
||||
</SplitPane>
|
||||
<Notifications />
|
||||
{showShortCuts && (
|
||||
<div
|
||||
id="onscreen-overlay"
|
||||
onClick={() => this.showOnScreenShortcuts(false)}
|
||||
>
|
||||
<div id="onscreen-shortcuts">
|
||||
<div className="col-8">
|
||||
<ShortcutTable />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</KeyboardShortcuts>
|
||||
</PinnedJobs>
|
||||
</Pushes>
|
||||
</div>
|
||||
)}
|
||||
</KeyboardShortcuts>
|
||||
</PinnedJobs>
|
||||
</Provider>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { notify } from '../redux/stores/notifications';
|
||||
import { thEvents } from '../../helpers/constants';
|
||||
|
||||
const COUNT_ERROR = 'Max pinboard size of 500 reached.';
|
||||
const MAX_SIZE = 500;
|
||||
|
@ -34,6 +35,14 @@ export class PinnedJobsClass extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener(thEvents.clearPinboard, this.unPinAll);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener(thEvents.clearPinboard, this.unPinAll);
|
||||
}
|
||||
|
||||
setValue(newState, callback) {
|
||||
this.value = { ...this.value, ...newState };
|
||||
this.setState(newState, callback);
|
||||
|
|
|
@ -1,427 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import pick from 'lodash/pick';
|
||||
import keyBy from 'lodash/keyBy';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import max from 'lodash/max';
|
||||
|
||||
import {
|
||||
thDefaultRepo,
|
||||
thEvents,
|
||||
thMaxPushFetchSize,
|
||||
} from '../../helpers/constants';
|
||||
import { parseQueryParams } from '../../helpers/url';
|
||||
import {
|
||||
getAllUrlParams,
|
||||
getQueryString,
|
||||
getUrlParam,
|
||||
setLocation,
|
||||
setUrlParam,
|
||||
} from '../../helpers/location';
|
||||
import { isUnclassifiedFailure } from '../../helpers/job';
|
||||
import PushModel from '../../models/push';
|
||||
import JobModel from '../../models/job';
|
||||
import { reloadOnChangeParameters } from '../../helpers/filter';
|
||||
import { notify } from '../redux/stores/notifications';
|
||||
import { setSelectedJob } from '../redux/stores/selectedJob';
|
||||
|
||||
const PushesContext = React.createContext({});
|
||||
const defaultPushCount = 10;
|
||||
// Keys that, if present on the url, must be passed into the push
|
||||
// polling endpoint
|
||||
const pushPollingKeys = ['tochange', 'enddate', 'revision', 'author'];
|
||||
const pushFetchKeys = [...pushPollingKeys, 'fromchange', 'startdate'];
|
||||
const pushPollInterval = 60000;
|
||||
|
||||
export class PushesClass extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.skipNextPageReload = false;
|
||||
this.cachedReloadTriggerParams = this.getNewReloadTriggerParams();
|
||||
|
||||
this.state = {
|
||||
pushList: [],
|
||||
jobMap: {},
|
||||
revisionTips: [],
|
||||
jobsLoaded: false,
|
||||
loadingPushes: true,
|
||||
oldestPushTimestamp: null,
|
||||
latestJobTimestamp: null,
|
||||
allUnclassifiedFailureCount: 0,
|
||||
filteredUnclassifiedFailureCount: 0,
|
||||
};
|
||||
this.value = {
|
||||
...this.state,
|
||||
updateJobMap: this.updateJobMap,
|
||||
getAllShownJobs: this.getAllShownJobs,
|
||||
fetchPushes: this.fetchPushes,
|
||||
recalculateUnclassifiedCounts: this.recalculateUnclassifiedCounts,
|
||||
setRevisionTips: this.setRevisionTips,
|
||||
addPushes: this.addPushes,
|
||||
getPush: this.getPush,
|
||||
updateUrlFromchange: this.updateUrlFromchange,
|
||||
getNextPushes: this.getNextPushes,
|
||||
handleUrlChanges: this.handleUrlChanges,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('hashchange', this.handleUrlChanges, false);
|
||||
|
||||
// get our first set of resultsets
|
||||
const fromchange = getUrlParam('fromchange');
|
||||
// If we have a ``fromchange`` url param. We don't want to limit ourselves
|
||||
// to the default of 10 pushes on the first load.
|
||||
this.fetchPushes(fromchange ? thMaxPushFetchSize : defaultPushCount);
|
||||
this.poll();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.pushIntervalId) {
|
||||
clearInterval(this.pushIntervalId);
|
||||
this.pushIntervalId = null;
|
||||
}
|
||||
window.removeEventListener('hashchange', this.handleUrlChanges, false);
|
||||
}
|
||||
|
||||
setValue = (newState, callback) => {
|
||||
this.value = { ...this.value, ...newState };
|
||||
this.setState(newState, callback);
|
||||
};
|
||||
|
||||
getNewReloadTriggerParams() {
|
||||
const params = parseQueryParams(getQueryString());
|
||||
|
||||
return reloadOnChangeParameters.reduce(
|
||||
(acc, prop) => (params[prop] ? { ...acc, [prop]: params[prop] } : acc),
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
getAllShownJobs = pushId => {
|
||||
const { jobMap } = this.state;
|
||||
const jobList = Object.values(jobMap);
|
||||
|
||||
return pushId
|
||||
? jobList.filter(job => job.push_id === pushId && job.visible)
|
||||
: jobList.filter(job => job.visible);
|
||||
};
|
||||
|
||||
setRevisionTips = () => {
|
||||
const { pushList } = this.state;
|
||||
|
||||
this.setValue({
|
||||
revisionTips: pushList.map(push => ({
|
||||
revision: push.revision,
|
||||
author: push.author,
|
||||
title: push.revisions[0].comments.split('\n')[0],
|
||||
})),
|
||||
});
|
||||
};
|
||||
|
||||
getPush = pushId => {
|
||||
const { pushList } = this.state;
|
||||
|
||||
return pushList.find(push => pushId === push.id);
|
||||
};
|
||||
|
||||
getNextPushes = count => {
|
||||
const params = getAllUrlParams();
|
||||
|
||||
this.setValue({ loadingPushes: true });
|
||||
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.
|
||||
this.skipNextPageReload = true;
|
||||
const revision = params.get('revision');
|
||||
params.delete('revision');
|
||||
params.set('tochange', revision);
|
||||
setLocation(params);
|
||||
} 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``.
|
||||
this.skipNextPageReload = true;
|
||||
setUrlParam('startdate', null);
|
||||
}
|
||||
this.fetchPushes(count).then(this.updateUrlFromchange);
|
||||
};
|
||||
|
||||
getLastModifiedJobTime = () => {
|
||||
const { jobMap } = this.state;
|
||||
const latest =
|
||||
max(
|
||||
Object.values(jobMap).map(job => new Date(`${job.last_modified}Z`)),
|
||||
) || new Date();
|
||||
|
||||
latest.setSeconds(latest.getSeconds() - 3);
|
||||
return latest;
|
||||
};
|
||||
|
||||
poll = () => {
|
||||
this.pushIntervalId = setInterval(async () => {
|
||||
const { notify } = this.props;
|
||||
const { pushList } = this.state;
|
||||
// these params will be passed in each time we poll to remain
|
||||
// within the constraints of the URL params
|
||||
const locationSearch = parseQueryParams(getQueryString());
|
||||
const pushPollingParams = pushPollingKeys.reduce(
|
||||
(acc, prop) =>
|
||||
locationSearch[prop] ? { ...acc, [prop]: locationSearch[prop] } : acc,
|
||||
{},
|
||||
);
|
||||
|
||||
if (pushList.length === 1 && locationSearch.revision) {
|
||||
// If we are on a single revision, no need to poll for more pushes, but
|
||||
// we need to keep polling for jobs.
|
||||
this.fetchNewJobs();
|
||||
} else {
|
||||
if (pushList.length) {
|
||||
// We have a range of pushes, but not bound to a single push,
|
||||
// so get only pushes newer than our latest.
|
||||
pushPollingParams.fromchange = pushList[pushList.length - 1].revision;
|
||||
}
|
||||
// We will either have a ``revision`` param, but no push for it yet,
|
||||
// or a ``fromchange`` param because we have at least 1 push already.
|
||||
const { data, failureStatus } = await PushModel.getList(
|
||||
pushPollingParams,
|
||||
);
|
||||
if (!failureStatus) {
|
||||
this.addPushes(data);
|
||||
this.fetchNewJobs();
|
||||
} else {
|
||||
notify('Error fetching new push data', 'danger', { sticky: true });
|
||||
}
|
||||
}
|
||||
}, pushPollInterval);
|
||||
};
|
||||
|
||||
// reload the page if certain params were changed in the URL. For
|
||||
// others, such as filtering, just re-filter without reload.
|
||||
|
||||
// the param ``skipNextPageReload`` will cause a single run through
|
||||
// this code to skip the page reloading even on a param that would
|
||||
// otherwise trigger a page reload. This is useful for a param that
|
||||
// is being changed by code in a specific situation as opposed to when
|
||||
// the user manually edits the URL location bar.
|
||||
handleUrlChanges = () => {
|
||||
const newReloadTriggerParams = this.getNewReloadTriggerParams();
|
||||
// if we are just setting the repo to the default because none was
|
||||
// set initially, then don't reload the page.
|
||||
const defaulting =
|
||||
newReloadTriggerParams.repo === thDefaultRepo &&
|
||||
!this.cachedReloadTriggerParams.repo;
|
||||
|
||||
if (
|
||||
!defaulting &&
|
||||
this.cachedReloadTriggerParams &&
|
||||
!isEqual(newReloadTriggerParams, this.cachedReloadTriggerParams) &&
|
||||
!this.skipNextPageReload
|
||||
) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
this.cachedReloadTriggerParams = newReloadTriggerParams;
|
||||
}
|
||||
|
||||
this.skipNextPageReload = false;
|
||||
|
||||
this.recalculateUnclassifiedCounts();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the next batch of pushes based on our current offset.
|
||||
* @param count How many to fetch
|
||||
*/
|
||||
fetchPushes = async count => {
|
||||
const { notify } = this.props;
|
||||
const { oldestPushTimestamp } = this.state;
|
||||
// const isAppend = (repoData.pushes.length > 0);
|
||||
// Only pass supported query string params to this endpoint.
|
||||
const options = {
|
||||
...pick(parseQueryParams(getQueryString()), pushFetchKeys),
|
||||
count,
|
||||
};
|
||||
|
||||
if (oldestPushTimestamp) {
|
||||
// If we have an oldestTimestamp, then this isn't our first fetch,
|
||||
// we're fetching more pushes. We don't want to limit this fetch
|
||||
// by the current ``fromchange`` or ``tochange`` value. Deleting
|
||||
// these params here do not affect the params on the location bar.
|
||||
delete options.fromchange;
|
||||
delete options.tochange;
|
||||
options.push_timestamp__lte = oldestPushTimestamp;
|
||||
}
|
||||
const { data, failureStatus } = await PushModel.getList(options);
|
||||
if (!failureStatus) {
|
||||
this.addPushes(data.results.length ? data : { results: [] });
|
||||
} else {
|
||||
notify('Error retrieving push data!', 'danger', { sticky: true });
|
||||
}
|
||||
return this.setValue({ loadingPushes: false });
|
||||
};
|
||||
|
||||
addPushes = data => {
|
||||
const { pushList } = this.state;
|
||||
|
||||
if (data.results.length > 0) {
|
||||
const pushIds = pushList.map(push => push.id);
|
||||
const newPushList = [
|
||||
...pushList,
|
||||
...data.results.filter(push => !pushIds.includes(push.id)),
|
||||
];
|
||||
newPushList.sort((a, b) => b.push_timestamp - a.push_timestamp);
|
||||
const oldestPushTimestamp =
|
||||
newPushList[newPushList.length - 1].push_timestamp;
|
||||
this.recalculateUnclassifiedCounts();
|
||||
this.setValue({ pushList: newPushList, oldestPushTimestamp }, () =>
|
||||
this.setRevisionTips(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
updateUrlFromchange = () => {
|
||||
// since we fetched more pushes, we need to persist the push state in the URL.
|
||||
const { pushList } = this.state;
|
||||
const updatedLastRevision = pushList[pushList.length - 1].revision;
|
||||
|
||||
if (getUrlParam('fromchange') !== updatedLastRevision) {
|
||||
this.skipNextPageReload = true;
|
||||
setUrlParam('fromchange', updatedLastRevision);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Loops through the map of unclassified failures and checks if it is
|
||||
* within the enabled tiers and if the job should be shown. This essentially
|
||||
* gives us the difference in unclassified failures and, of those jobs, the
|
||||
* ones that have been filtered out
|
||||
*/
|
||||
recalculateUnclassifiedCounts = () => {
|
||||
const { jobMap } = this.state;
|
||||
const { filterModel } = this.props;
|
||||
const tiers = filterModel.urlParams.tier || [];
|
||||
let allUnclassifiedFailureCount = 0;
|
||||
let filteredUnclassifiedFailureCount = 0;
|
||||
|
||||
Object.values(jobMap).forEach(job => {
|
||||
if (isUnclassifiedFailure(job)) {
|
||||
if (tiers.includes(String(job.tier))) {
|
||||
if (filterModel.showJob(job)) {
|
||||
filteredUnclassifiedFailureCount++;
|
||||
}
|
||||
allUnclassifiedFailureCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
this.setValue({
|
||||
allUnclassifiedFailureCount,
|
||||
filteredUnclassifiedFailureCount,
|
||||
});
|
||||
};
|
||||
|
||||
updateJobMap = jobList => {
|
||||
const { jobMap, pushList } = this.state;
|
||||
|
||||
if (jobList.length) {
|
||||
// lodash ``keyBy`` is significantly faster than doing a ``reduce``
|
||||
this.setValue({
|
||||
jobMap: { ...jobMap, ...keyBy(jobList, 'id') },
|
||||
jobsLoaded: pushList.every(push => push.jobsLoaded),
|
||||
pushList: [...pushList],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
async fetchNewJobs() {
|
||||
const { setSelectedJob } = this.props;
|
||||
const { pushList } = this.state;
|
||||
if (!pushList.length) {
|
||||
// If we have no pushes, then no need to poll for jobs.
|
||||
return;
|
||||
}
|
||||
const pushIds = pushList.map(push => push.id);
|
||||
const lastModified = this.getLastModifiedJobTime();
|
||||
const jobList = await JobModel.getList(
|
||||
{
|
||||
push_id__in: pushIds.join(','),
|
||||
last_modified__gt: lastModified.toISOString().replace('Z', ''),
|
||||
},
|
||||
{ fetch_all: true },
|
||||
);
|
||||
// break the jobs up per push
|
||||
const jobs = jobList.reduce((acc, job) => {
|
||||
const pushJobs = acc[job.push_id] ? [...acc[job.push_id], job] : [job];
|
||||
return { ...acc, [job.push_id]: pushJobs };
|
||||
}, {});
|
||||
// If a job is selected, and one of the jobs we just fetched is the
|
||||
// updated version of that selected job, then send that with the event.
|
||||
const selectedJobId = getUrlParam('selectedJob');
|
||||
const updatedSelectedJob = selectedJobId
|
||||
? jobList.find(job => job.id === parseInt(selectedJobId, 10))
|
||||
: null;
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(thEvents.applyNewJobs, {
|
||||
detail: { jobs },
|
||||
}),
|
||||
);
|
||||
if (updatedSelectedJob) {
|
||||
setSelectedJob(updatedSelectedJob);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<PushesContext.Provider value={this.value}>
|
||||
{this.props.children}
|
||||
</PushesContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PushesClass.propTypes = {
|
||||
children: PropTypes.object.isRequired,
|
||||
filterModel: PropTypes.object.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
setSelectedJob: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const Pushes = connect(
|
||||
null,
|
||||
{ notify, setSelectedJob },
|
||||
)(PushesClass);
|
||||
|
||||
export function withPushes(Component) {
|
||||
return function PushesComponent(props) {
|
||||
return (
|
||||
<PushesContext.Consumer>
|
||||
{context => (
|
||||
<Component
|
||||
{...props}
|
||||
pushList={context.pushList}
|
||||
revisionTips={context.revisionTips}
|
||||
jobMap={context.jobMap}
|
||||
jobsLoaded={context.jobsLoaded}
|
||||
loadingPushes={context.loadingPushes}
|
||||
allUnclassifiedFailureCount={context.allUnclassifiedFailureCount}
|
||||
filteredUnclassifiedFailureCount={
|
||||
context.filteredUnclassifiedFailureCount
|
||||
}
|
||||
updateJobMap={context.updateJobMap}
|
||||
getAllShownJobs={context.getAllShownJobs}
|
||||
fetchPushes={context.fetchPushes}
|
||||
getPush={context.getPush}
|
||||
recalculateUnclassifiedCounts={
|
||||
context.recalculateUnclassifiedCounts
|
||||
}
|
||||
getNextPushes={context.getNextPushes}
|
||||
/>
|
||||
)}
|
||||
</PushesContext.Consumer>
|
||||
);
|
||||
};
|
||||
}
|
|
@ -5,7 +5,6 @@ import { connect } from 'react-redux';
|
|||
|
||||
import { thEvents, thBugSuggestionLimit } from '../../helpers/constants';
|
||||
import { withPinnedJobs } from '../context/PinnedJobs';
|
||||
import { withPushes } from '../context/Pushes';
|
||||
import { getLogViewerUrl, getReftestUrl } from '../../helpers/url';
|
||||
import BugJobMapModel from '../../models/bugJobMap';
|
||||
import BugSuggestionsModel from '../../models/bugSuggestions';
|
||||
|
@ -152,8 +151,15 @@ class DetailsPanel extends React.Component {
|
|||
this.setState({ classifications, bugs });
|
||||
};
|
||||
|
||||
selectJob() {
|
||||
const { repoName, selectedJob, getPush } = this.props;
|
||||
findPush = pushId => {
|
||||
const { pushList } = this.props;
|
||||
|
||||
return pushList.find(push => pushId === push.id);
|
||||
};
|
||||
|
||||
selectJob = () => {
|
||||
const { repoName, selectedJob } = this.props;
|
||||
const push = this.findPush(selectedJob.push_id);
|
||||
|
||||
this.setState(
|
||||
{ jobDetails: [], suggestions: [], jobDetailLoading: true },
|
||||
|
@ -206,7 +212,7 @@ class DetailsPanel extends React.Component {
|
|||
// is what we'll pass to the rest of the details panel. It has extra fields like
|
||||
// taskcluster_metadata.
|
||||
Object.assign(selectedJob, jobResult);
|
||||
const jobRevision = getPush(selectedJob.push_id).revision;
|
||||
const jobRevision = push.revision;
|
||||
|
||||
jobDetails = jobDetailResult;
|
||||
|
||||
|
@ -308,7 +314,7 @@ class DetailsPanel extends React.Component {
|
|||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
|
@ -411,7 +417,7 @@ DetailsPanel.propTypes = {
|
|||
classificationMap: PropTypes.object.isRequired,
|
||||
setPinBoardVisible: PropTypes.func.isRequired,
|
||||
isPinBoardVisible: PropTypes.bool.isRequired,
|
||||
getPush: PropTypes.func.isRequired,
|
||||
pushList: PropTypes.array.isRequired,
|
||||
selectedJob: PropTypes.object,
|
||||
};
|
||||
|
||||
|
@ -419,8 +425,9 @@ DetailsPanel.defaultProps = {
|
|||
selectedJob: null,
|
||||
};
|
||||
|
||||
const mapStateToProps = ({ selectedJob: { selectedJob } }) => ({ selectedJob });
|
||||
const mapStateToProps = ({
|
||||
selectedJob: { selectedJob },
|
||||
pushes: { pushList },
|
||||
}) => ({ selectedJob, pushList });
|
||||
|
||||
export default connect(mapStateToProps)(
|
||||
withPushes(withPinnedJobs(DetailsPanel)),
|
||||
);
|
||||
export default connect(mapStateToProps)(withPinnedJobs(DetailsPanel));
|
||||
|
|
|
@ -18,9 +18,9 @@ import BugJobMapModel from '../../models/bugJobMap';
|
|||
import JobClassificationModel from '../../models/classification';
|
||||
import JobModel from '../../models/job';
|
||||
import { withPinnedJobs } from '../context/PinnedJobs';
|
||||
import { withPushes } from '../context/Pushes';
|
||||
import { notify } from '../redux/stores/notifications';
|
||||
import { setSelectedJob } from '../redux/stores/selectedJob';
|
||||
import { recalculateUnclassifiedCounts } from '../redux/stores/pushes';
|
||||
|
||||
class PinBoard extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -655,7 +655,9 @@ PinBoard.defaultProps = {
|
|||
revisionTips: [],
|
||||
};
|
||||
|
||||
const mapStateToProps = ({ pushes: { revisionTips } }) => ({ revisionTips });
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
{ notify, setSelectedJob },
|
||||
)(withPushes(withPinnedJobs(PinBoard)));
|
||||
mapStateToProps,
|
||||
{ notify, setSelectedJob, recalculateUnclassifiedCounts },
|
||||
)(withPinnedJobs(PinBoard));
|
||||
|
|
|
@ -21,7 +21,6 @@ import PushModel from '../../../models/push';
|
|||
import TaskclusterModel from '../../../models/taskcluster';
|
||||
import CustomJobActions from '../../CustomJobActions';
|
||||
import { withPinnedJobs } from '../../context/PinnedJobs';
|
||||
import { withPushes } from '../../context/Pushes';
|
||||
import { notify } from '../../redux/stores/notifications';
|
||||
|
||||
import LogUrls from './LogUrls';
|
||||
|
@ -574,9 +573,7 @@ ActionBar.defaultProps = {
|
|||
jobLogUrls: [],
|
||||
};
|
||||
|
||||
const mapStateToProps = ({ selectedJob: { selectedJob } }) => ({ selectedJob });
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
null,
|
||||
{ notify },
|
||||
)(withPushes(withPinnedJobs(ActionBar)));
|
||||
)(withPinnedJobs(ActionBar));
|
||||
|
|
|
@ -10,9 +10,9 @@ import {
|
|||
|
||||
import { thEvents } from '../../../helpers/constants';
|
||||
import { getBugUrl } from '../../../helpers/url';
|
||||
import { withPushes } from '../../context/Pushes';
|
||||
import { longDateFormat } from '../../../helpers/display';
|
||||
import { notify } from '../../redux/stores/notifications';
|
||||
import { recalculateUnclassifiedCounts } from '../../redux/stores/pushes';
|
||||
|
||||
function RelatedBugSaved(props) {
|
||||
const { deleteBug, bug } = props;
|
||||
|
@ -258,5 +258,5 @@ const mapStateToProps = ({ selectedJob: { selectedJob } }) => ({ selectedJob });
|
|||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{ notify },
|
||||
)(withPushes(AnnotationsTab));
|
||||
{ notify, recalculateUnclassifiedCounts },
|
||||
)(AnnotationsTab);
|
||||
|
|
|
@ -6,7 +6,6 @@ import { Label } from 'reactstrap';
|
|||
import { thAllResultStatuses } from '../../helpers/constants';
|
||||
import { getJobsUrl } from '../../helpers/url';
|
||||
import { withPinnedJobs } from '../context/PinnedJobs';
|
||||
import { withPushes } from '../context/Pushes';
|
||||
import { setSelectedJob, clearSelectedJob } from '../redux/stores/selectedJob';
|
||||
|
||||
const resultStatusMenuItems = thAllResultStatuses.filter(
|
||||
|
@ -144,4 +143,4 @@ const mapStateToProps = ({ selectedJob: { selectedJob } }) => ({ selectedJob });
|
|||
export default connect(
|
||||
mapStateToProps,
|
||||
{ setSelectedJob, clearSelectedJob },
|
||||
)(withPushes(withPinnedJobs(FiltersMenu)));
|
||||
)(withPinnedJobs(FiltersMenu));
|
||||
|
|
|
@ -53,6 +53,7 @@ class PrimaryNavBar extends React.Component {
|
|||
groupCountsExpanded,
|
||||
toggleFieldFilterVisible,
|
||||
pushHealthVisibility,
|
||||
getAllShownJobs,
|
||||
setPushHealthVisibility,
|
||||
notify,
|
||||
} = this.props;
|
||||
|
@ -68,7 +69,11 @@ class PrimaryNavBar extends React.Component {
|
|||
<InfraMenu />
|
||||
<ReposMenu repos={repos} />
|
||||
<TiersMenu filterModel={filterModel} />
|
||||
<FiltersMenu filterModel={filterModel} user={user} />
|
||||
<FiltersMenu
|
||||
filterModel={filterModel}
|
||||
user={user}
|
||||
getAllShownJobs={getAllShownJobs}
|
||||
/>
|
||||
<HealthMenu
|
||||
pushHealthVisibility={pushHealthVisibility}
|
||||
setPushHealthVisibility={setPushHealthVisibility}
|
||||
|
@ -108,6 +113,7 @@ PrimaryNavBar.propTypes = {
|
|||
pushHealthVisibility: PropTypes.string.isRequired,
|
||||
setPushHealthVisibility: PropTypes.func.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
getAllShownJobs: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCircle, faDotCircle } from '@fortawesome/free-regular-svg-icons';
|
||||
import {
|
||||
|
@ -9,11 +10,11 @@ import {
|
|||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { getBtnClass } from '../../helpers/job';
|
||||
import { thFilterGroups } from '../../helpers/filter';
|
||||
import { hasUrlFilterChanges, thFilterGroups } from '../../helpers/filter';
|
||||
import { getRepo, getUrlParam, setUrlParam } from '../../helpers/location';
|
||||
import RepositoryModel from '../../models/repository';
|
||||
import ErrorBoundary from '../../shared/ErrorBoundary';
|
||||
import { withPushes } from '../context/Pushes';
|
||||
import { recalculateUnclassifiedCounts } from '../redux/stores/pushes';
|
||||
|
||||
import WatchedRepo from './WatchedRepo';
|
||||
|
||||
|
@ -47,6 +48,14 @@ class SecondaryNavBar extends React.PureComponent {
|
|||
this.loadWatchedRepos();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { repoName } = this.state;
|
||||
|
||||
if (repoName !== prevState.repoName) {
|
||||
this.loadWatchedRepos();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('hashchange', this.handleUrlChanges, false);
|
||||
}
|
||||
|
@ -55,9 +64,22 @@ class SecondaryNavBar extends React.PureComponent {
|
|||
this.setState({ searchQueryStr: ev.target.value });
|
||||
}
|
||||
|
||||
handleUrlChanges = () => {
|
||||
this.setState({
|
||||
handleUrlChanges = evt => {
|
||||
const { oldURL, newURL } = evt;
|
||||
const { repoName } = this.state;
|
||||
const { recalculateUnclassifiedCounts } = this.props;
|
||||
const newState = {
|
||||
searchQueryStr: getSearchStrFromUrl(),
|
||||
repoName: getRepo(),
|
||||
};
|
||||
|
||||
this.setState(newState, () => {
|
||||
if (
|
||||
hasUrlFilterChanges(oldURL, newURL) ||
|
||||
newState.repoName !== repoName
|
||||
) {
|
||||
recalculateUnclassifiedCounts();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -367,10 +389,18 @@ SecondaryNavBar.propTypes = {
|
|||
repos: PropTypes.array.isRequired,
|
||||
setCurrentRepoTreeStatus: PropTypes.func.isRequired,
|
||||
allUnclassifiedFailureCount: PropTypes.number.isRequired,
|
||||
recalculateUnclassifiedCounts: PropTypes.func.isRequired,
|
||||
filteredUnclassifiedFailureCount: PropTypes.number.isRequired,
|
||||
duplicateJobsVisible: PropTypes.bool.isRequired,
|
||||
groupCountsExpanded: PropTypes.bool.isRequired,
|
||||
toggleFieldFilterVisible: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withPushes(SecondaryNavBar);
|
||||
const mapStateToProps = ({
|
||||
pushes: { allUnclassifiedFailureCount, filteredUnclassifiedFailureCount },
|
||||
}) => ({ allUnclassifiedFailureCount, filteredUnclassifiedFailureCount });
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{ recalculateUnclassifiedCounts },
|
||||
)(SecondaryNavBar);
|
||||
|
|
|
@ -8,7 +8,6 @@ import {
|
|||
thOptionOrder,
|
||||
thPlatformMap,
|
||||
} from '../../helpers/constants';
|
||||
import { withPushes } from '../context/Pushes';
|
||||
import { getGroupMapKey } from '../../helpers/aggregateId';
|
||||
import { getAllUrlParams, getUrlParam } from '../../helpers/location';
|
||||
import JobModel from '../../models/job';
|
||||
|
@ -17,6 +16,10 @@ import RunnableJobModel from '../../models/runnableJob';
|
|||
import { getRevisionTitle } from '../../helpers/revision';
|
||||
import { getPercentComplete } from '../../helpers/display';
|
||||
import { notify } from '../redux/stores/notifications';
|
||||
import {
|
||||
updateJobMap,
|
||||
recalculateUnclassifiedCounts,
|
||||
} from '../redux/stores/pushes';
|
||||
|
||||
import FuzzyJobFinder from './FuzzyJobFinder';
|
||||
import { Revision } from './Revision';
|
||||
|
@ -140,17 +143,21 @@ class Push extends React.PureComponent {
|
|||
};
|
||||
|
||||
fetchJobs = async () => {
|
||||
const { push } = this.props;
|
||||
const jobs = await JobModel.getList(
|
||||
const { push, notify } = this.props;
|
||||
const { data, failureStatus } = await JobModel.getList(
|
||||
{
|
||||
push_id: push.id,
|
||||
count: 2000,
|
||||
return_type: 'list',
|
||||
},
|
||||
{ fetch_all: true },
|
||||
{ fetchAll: true },
|
||||
);
|
||||
|
||||
this.mapPushJobs(jobs);
|
||||
if (!failureStatus) {
|
||||
this.mapPushJobs(data);
|
||||
} else {
|
||||
notify(failureStatus, 'danger', { sticky: true });
|
||||
}
|
||||
};
|
||||
|
||||
mapPushJobs = (jobs, skipJobMap) => {
|
||||
|
@ -462,6 +469,7 @@ class Push extends React.PureComponent {
|
|||
return (
|
||||
<div
|
||||
className="push"
|
||||
data-testid={`push-${push.id}`}
|
||||
ref={ref => {
|
||||
this.container = ref;
|
||||
}}
|
||||
|
@ -550,7 +558,11 @@ Push.propTypes = {
|
|||
pushHealthVisibility: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = ({ pushes: { allUnclassifiedFailureCount } }) => ({
|
||||
allUnclassifiedFailureCount,
|
||||
});
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
{ notify },
|
||||
)(withPushes(Push));
|
||||
mapStateToProps,
|
||||
{ notify, updateJobMap, recalculateUnclassifiedCounts },
|
||||
)(Push);
|
||||
|
|
|
@ -99,6 +99,7 @@ class PushHeader extends React.Component {
|
|||
runnableVisible: prevRunnableVisible,
|
||||
collapsed: prevCollapsed,
|
||||
pushHealthVisibility: prevPushHealthVisibility,
|
||||
filterModel: prevFilterModel,
|
||||
} = prevProps;
|
||||
const {
|
||||
jobCounts,
|
||||
|
@ -108,6 +109,7 @@ class PushHeader extends React.Component {
|
|||
runnableVisible,
|
||||
collapsed,
|
||||
pushHealthVisibility,
|
||||
filterModel,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
|
@ -117,7 +119,8 @@ class PushHeader extends React.Component {
|
|||
prevSelectedRunnableJobs !== selectedRunnableJobs ||
|
||||
prevRunnableVisible !== runnableVisible ||
|
||||
prevCollapsed !== collapsed ||
|
||||
prevPushHealthVisibility !== pushHealthVisibility
|
||||
prevPushHealthVisibility !== pushHealthVisibility ||
|
||||
prevFilterModel !== filterModel
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,19 +2,28 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import intersection from 'lodash/intersection';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
import ErrorBoundary from '../../shared/ErrorBoundary';
|
||||
import { withPushes } from '../context/Pushes';
|
||||
import { withPinnedJobs } from '../context/PinnedJobs';
|
||||
import { notify } from '../redux/stores/notifications';
|
||||
import {
|
||||
clearSelectedJob,
|
||||
setSelectedJobFromQueryString,
|
||||
} from '../redux/stores/selectedJob';
|
||||
import {
|
||||
fetchPushes,
|
||||
fetchNextPushes,
|
||||
updateRange,
|
||||
pollPushes,
|
||||
} from '../redux/stores/pushes';
|
||||
import { reloadOnChangeParameters } from '../../helpers/filter';
|
||||
|
||||
import Push from './Push';
|
||||
import PushLoadErrors from './PushLoadErrors';
|
||||
|
||||
const PUSH_POLL_INTERVAL = 60000;
|
||||
|
||||
class PushList extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -24,6 +33,14 @@ class PushList extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { fetchPushes } = this.props;
|
||||
|
||||
window.addEventListener('hashchange', this.handleUrlChanges, false);
|
||||
fetchPushes();
|
||||
this.poll();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
notify,
|
||||
|
@ -32,17 +49,54 @@ class PushList extends React.Component {
|
|||
setSelectedJobFromQueryString,
|
||||
} = this.props;
|
||||
|
||||
if (jobsLoaded !== prevProps.jobsLoaded) {
|
||||
if (jobsLoaded && jobsLoaded !== prevProps.jobsLoaded) {
|
||||
setSelectedJobFromQueryString(notify, jobMap);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.pushIntervalId) {
|
||||
clearInterval(this.pushIntervalId);
|
||||
this.pushIntervalId = null;
|
||||
}
|
||||
window.addEventListener('hashchange', this.handleUrlChanges, false);
|
||||
}
|
||||
|
||||
setWindowTitle() {
|
||||
const { allUnclassifiedFailureCount, repoName } = this.props;
|
||||
|
||||
document.title = `[${allUnclassifiedFailureCount}] ${repoName}`;
|
||||
}
|
||||
|
||||
getUrlRangeValues = url => {
|
||||
const params = [...new URLSearchParams(url.split('?')[1]).entries()];
|
||||
|
||||
return params.reduce((acc, [key, value]) => {
|
||||
return reloadOnChangeParameters.includes(key)
|
||||
? { ...acc, [key]: value }
|
||||
: acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
poll = () => {
|
||||
const { pollPushes } = this.props;
|
||||
|
||||
this.pushIntervalId = setInterval(async () => {
|
||||
pollPushes();
|
||||
}, PUSH_POLL_INTERVAL);
|
||||
};
|
||||
|
||||
handleUrlChanges = evt => {
|
||||
const { updateRange } = this.props;
|
||||
const { oldURL, newURL } = evt;
|
||||
const oldRange = this.getUrlRangeValues(oldURL);
|
||||
const newRange = this.getUrlRangeValues(newURL);
|
||||
|
||||
if (!isEqual(oldRange, newRange)) {
|
||||
updateRange(newRange);
|
||||
}
|
||||
};
|
||||
|
||||
clearIfEligibleTarget(target) {
|
||||
// Target must be within the "push" area, but not be a dropdown-item or
|
||||
// a button/btn.
|
||||
|
@ -69,7 +123,8 @@ class PushList extends React.Component {
|
|||
filterModel,
|
||||
pushList,
|
||||
loadingPushes,
|
||||
getNextPushes,
|
||||
fetchNextPushes,
|
||||
getAllShownJobs,
|
||||
jobsLoaded,
|
||||
duplicateJobsVisible,
|
||||
groupCountsExpanded,
|
||||
|
@ -81,7 +136,6 @@ class PushList extends React.Component {
|
|||
if (!revision) {
|
||||
this.setWindowTitle();
|
||||
}
|
||||
|
||||
return (
|
||||
<div onClick={evt => this.clearIfEligibleTarget(evt.target)}>
|
||||
{jobsLoaded && <span className="hidden ready" />}
|
||||
|
@ -103,6 +157,7 @@ class PushList extends React.Component {
|
|||
groupCountsExpanded={groupCountsExpanded}
|
||||
isOnlyRevision={push.revision === revision}
|
||||
pushHealthVisibility={pushHealthVisibility}
|
||||
getAllShownJobs={getAllShownJobs}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
|
@ -126,7 +181,7 @@ class PushList extends React.Component {
|
|||
{[10, 20, 50].map(count => (
|
||||
<div
|
||||
className="btn btn-light-bordered"
|
||||
onClick={() => getNextPushes(count)}
|
||||
onClick={() => fetchNextPushes(count)}
|
||||
key={count}
|
||||
>
|
||||
{count}
|
||||
|
@ -144,7 +199,10 @@ PushList.propTypes = {
|
|||
user: PropTypes.object.isRequired,
|
||||
filterModel: PropTypes.object.isRequired,
|
||||
pushList: PropTypes.array.isRequired,
|
||||
getNextPushes: PropTypes.func.isRequired,
|
||||
fetchNextPushes: PropTypes.func.isRequired,
|
||||
fetchPushes: PropTypes.func.isRequired,
|
||||
pollPushes: PropTypes.func.isRequired,
|
||||
updateRange: PropTypes.func.isRequired,
|
||||
loadingPushes: PropTypes.bool.isRequired,
|
||||
jobsLoaded: PropTypes.bool.isRequired,
|
||||
duplicateJobsVisible: PropTypes.bool.isRequired,
|
||||
|
@ -154,6 +212,7 @@ PushList.propTypes = {
|
|||
clearSelectedJob: PropTypes.func.isRequired,
|
||||
countPinnedJobs: PropTypes.number.isRequired,
|
||||
setSelectedJobFromQueryString: PropTypes.func.isRequired,
|
||||
getAllShownJobs: PropTypes.func.isRequired,
|
||||
jobMap: PropTypes.object.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
revision: PropTypes.string,
|
||||
|
@ -165,7 +224,31 @@ PushList.defaultProps = {
|
|||
currentRepo: {},
|
||||
};
|
||||
|
||||
const mapStateToProps = ({
|
||||
pushes: {
|
||||
loadingPushes,
|
||||
jobsLoaded,
|
||||
jobMap,
|
||||
pushList,
|
||||
allUnclassifiedFailureCount,
|
||||
},
|
||||
}) => ({
|
||||
loadingPushes,
|
||||
jobsLoaded,
|
||||
jobMap,
|
||||
pushList,
|
||||
allUnclassifiedFailureCount,
|
||||
});
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
{ notify, clearSelectedJob, setSelectedJobFromQueryString },
|
||||
)(withPushes(withPinnedJobs(PushList)));
|
||||
mapStateToProps,
|
||||
{
|
||||
notify,
|
||||
clearSelectedJob,
|
||||
setSelectedJobFromQueryString,
|
||||
fetchNextPushes,
|
||||
fetchPushes,
|
||||
updateRange,
|
||||
pollPushes,
|
||||
},
|
||||
)(withPinnedJobs(PushList));
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { withPushes } from '../context/Pushes';
|
||||
import { getAllUrlParams } from '../../helpers/location';
|
||||
import { uiJobsUrlBase } from '../../helpers/url';
|
||||
|
||||
|
@ -105,4 +105,6 @@ PushLoadErrors.defaultProps = {
|
|||
revision: null,
|
||||
};
|
||||
|
||||
export default withPushes(PushLoadErrors);
|
||||
const mapStateToProps = ({ pushes: { loadingPushes } }) => ({ loadingPushes });
|
||||
|
||||
export default connect(mapStateToProps)(PushLoadErrors);
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import { createStore, combineReducers, applyMiddleware } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import createDebounce from 'redux-debounce';
|
||||
|
||||
import * as selectedJobStore from './stores/selectedJob';
|
||||
import * as notificationStore from './stores/notifications';
|
||||
import * as pushesStore from './stores/pushes';
|
||||
|
||||
export default () => {
|
||||
const debounceConfig = {
|
||||
nextJob: 200,
|
||||
};
|
||||
const debounceConfig = { nextJob: 200 };
|
||||
const debouncer = createDebounce(debounceConfig);
|
||||
const reducers = combineReducers({
|
||||
notifications: notificationStore.reducer,
|
||||
selectedJob: selectedJobStore.reducer,
|
||||
pushes: pushesStore.reducer,
|
||||
});
|
||||
|
||||
const store = createStore(reducers, applyMiddleware(debouncer));
|
||||
const store = createStore(reducers, applyMiddleware(thunk, debouncer));
|
||||
|
||||
return { store };
|
||||
};
|
||||
|
|
|
@ -0,0 +1,386 @@
|
|||
import pick from 'lodash/pick';
|
||||
import keyBy from 'lodash/keyBy';
|
||||
import max from 'lodash/max';
|
||||
|
||||
import { parseQueryParams } from '../../../helpers/url';
|
||||
import {
|
||||
getAllUrlParams,
|
||||
getQueryString,
|
||||
getUrlParam,
|
||||
replaceLocation,
|
||||
} from '../../../helpers/location';
|
||||
import PushModel from '../../../models/push';
|
||||
import { isUnclassifiedFailure } from '../../../helpers/job';
|
||||
import FilterModel from '../../../models/filter';
|
||||
import JobModel from '../../../models/job';
|
||||
import { thEvents } from '../../../helpers/constants';
|
||||
import { processErrors } from '../../../helpers/http';
|
||||
|
||||
import { notify } from './notifications';
|
||||
import { setSelectedJob, clearSelectedJob } from './selectedJob';
|
||||
|
||||
export const LOADING = 'LOADING';
|
||||
export const ADD_PUSHES = 'ADD_PUSHES';
|
||||
export const CLEAR_PUSHES = 'CLEAR_PUSHES';
|
||||
export const SET_PUSHES = 'SET_PUSHES';
|
||||
export const RECALCULATE_UNCLASSIFIED_COUNTS =
|
||||
'RECALCULATE_UNCLASSIFIED_COUNTS';
|
||||
export const UPDATE_JOB_MAP = 'UPDATE_JOB_MAP';
|
||||
|
||||
const DEFAULT_PUSH_COUNT = 10;
|
||||
// Keys that, if present on the url, must be passed into the push
|
||||
// polling endpoint
|
||||
const PUSH_POLLING_KEYS = ['tochange', 'enddate', 'revision', 'author'];
|
||||
const PUSH_FETCH_KEYS = [...PUSH_POLLING_KEYS, 'fromchange', 'startdate'];
|
||||
|
||||
const getRevisionTips = pushList => {
|
||||
return {
|
||||
revisionTips: pushList.map(push => ({
|
||||
revision: push.revision,
|
||||
author: push.author,
|
||||
title: push.revisions[0].comments.split('\n')[0],
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
const getLastModifiedJobTime = jobMap => {
|
||||
const latest =
|
||||
max(Object.values(jobMap).map(job => new Date(`${job.last_modified}Z`))) ||
|
||||
new Date();
|
||||
|
||||
latest.setSeconds(latest.getSeconds() - 3);
|
||||
return latest;
|
||||
};
|
||||
|
||||
/**
|
||||
* Loops through the map of unclassified failures and checks if it is
|
||||
* within the enabled tiers and if the job should be shown. This essentially
|
||||
* 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 tiers = filterModel.urlParams.tier;
|
||||
let allUnclassifiedFailureCount = 0;
|
||||
let filteredUnclassifiedFailureCount = 0;
|
||||
|
||||
Object.values(jobMap).forEach(job => {
|
||||
if (isUnclassifiedFailure(job) && tiers.includes(String(job.tier))) {
|
||||
if (filterModel.showJob(job)) {
|
||||
filteredUnclassifiedFailureCount++;
|
||||
}
|
||||
allUnclassifiedFailureCount++;
|
||||
}
|
||||
});
|
||||
return {
|
||||
allUnclassifiedFailureCount,
|
||||
filteredUnclassifiedFailureCount,
|
||||
};
|
||||
};
|
||||
|
||||
const addPushes = (data, pushList, jobMap, setFromchange) => {
|
||||
if (data.results.length > 0) {
|
||||
const pushIds = pushList.map(push => push.id);
|
||||
const newPushList = [
|
||||
...pushList,
|
||||
...data.results.filter(push => !pushIds.includes(push.id)),
|
||||
];
|
||||
|
||||
newPushList.sort((a, b) => b.push_timestamp - a.push_timestamp);
|
||||
const oldestPushTimestamp =
|
||||
newPushList[newPushList.length - 1].push_timestamp;
|
||||
|
||||
const newStuff = {
|
||||
pushList: newPushList,
|
||||
oldestPushTimestamp,
|
||||
...doRecalculateUnclassifiedCounts(jobMap),
|
||||
...getRevisionTips(newPushList),
|
||||
};
|
||||
|
||||
// since we fetched more pushes, we need to persist the push state in the URL.
|
||||
const updatedLastRevision = newPushList[newPushList.length - 1].revision;
|
||||
|
||||
if (setFromchange && getUrlParam('fromchange') !== updatedLastRevision) {
|
||||
const params = getAllUrlParams();
|
||||
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.
|
||||
window.dispatchEvent(new CustomEvent(thEvents.filtersUpdated));
|
||||
}
|
||||
|
||||
return newStuff;
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
const fetchNewJobs = () => {
|
||||
return async (dispatch, getState) => {
|
||||
const {
|
||||
pushes: { pushList, jobMap },
|
||||
} = getState();
|
||||
|
||||
if (!pushList.length) {
|
||||
// If we have no pushes, then no need to get jobs.
|
||||
return;
|
||||
}
|
||||
|
||||
const pushIds = pushList.map(push => push.id);
|
||||
const lastModified = getLastModifiedJobTime(jobMap);
|
||||
|
||||
const resp = await JobModel.getList(
|
||||
{
|
||||
push_id__in: pushIds.join(','),
|
||||
last_modified__gt: lastModified.toISOString().replace('Z', ''),
|
||||
},
|
||||
{ fetchAll: true },
|
||||
);
|
||||
const errors = processErrors([resp]);
|
||||
|
||||
if (!errors.length) {
|
||||
// break the jobs up per push
|
||||
const { data } = resp;
|
||||
const jobs = data.reduce((acc, job) => {
|
||||
const pushJobs = acc[job.push_id] ? [...acc[job.push_id], job] : [job];
|
||||
return { ...acc, [job.push_id]: pushJobs };
|
||||
}, {});
|
||||
// If a job is selected, and one of the jobs we just fetched is the
|
||||
// updated version of that selected job, then send that with the event.
|
||||
const selectedJobId = getUrlParam('selectedJob');
|
||||
const updatedSelectedJob = selectedJobId
|
||||
? data.find(job => job.id === parseInt(selectedJobId, 10))
|
||||
: null;
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(thEvents.applyNewJobs, {
|
||||
detail: { jobs },
|
||||
}),
|
||||
);
|
||||
if (updatedSelectedJob) {
|
||||
dispatch(setSelectedJob(updatedSelectedJob));
|
||||
}
|
||||
} else {
|
||||
for (const error of errors) {
|
||||
notify(error, 'danger', { sticky: true });
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const doUpdateJobMap = (jobList, jobMap, pushList) => {
|
||||
if (jobList.length) {
|
||||
// lodash ``keyBy`` is significantly faster than doing a ``reduce``
|
||||
return {
|
||||
jobMap: { ...jobMap, ...keyBy(jobList, 'id') },
|
||||
jobsLoaded: pushList.every(push => push.jobsLoaded),
|
||||
};
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
export const fetchPushes = (
|
||||
count = DEFAULT_PUSH_COUNT,
|
||||
setFromchange = false,
|
||||
) => {
|
||||
return async (dispatch, getState) => {
|
||||
const {
|
||||
pushes: { pushList, jobMap, oldestPushTimestamp },
|
||||
} = getState();
|
||||
|
||||
dispatch({ type: LOADING });
|
||||
|
||||
// Only pass supported query string params to this endpoint.
|
||||
const options = {
|
||||
...pick(parseQueryParams(getQueryString()), PUSH_FETCH_KEYS),
|
||||
};
|
||||
|
||||
if (oldestPushTimestamp) {
|
||||
// If we have an oldestTimestamp, then this isn't our first fetch,
|
||||
// we're fetching more pushes. We don't want to limit this fetch
|
||||
// by the current ``fromchange`` or ``tochange`` value. Deleting
|
||||
// these params here do not affect the params on the location bar.
|
||||
delete options.fromchange;
|
||||
delete options.tochange;
|
||||
options.push_timestamp__lte = oldestPushTimestamp;
|
||||
}
|
||||
if (!options.fromchange) {
|
||||
options.count = count;
|
||||
}
|
||||
const { data, failureStatus } = await PushModel.getList(options);
|
||||
|
||||
if (!failureStatus) {
|
||||
return dispatch({
|
||||
type: ADD_PUSHES,
|
||||
pushResults: addPushes(
|
||||
data.results.length ? data : { results: [] },
|
||||
pushList,
|
||||
jobMap,
|
||||
setFromchange,
|
||||
),
|
||||
});
|
||||
}
|
||||
dispatch(notify('Error retrieving push data!', 'danger', { sticky: true }));
|
||||
return {};
|
||||
};
|
||||
};
|
||||
|
||||
export const pollPushes = () => {
|
||||
return async (dispatch, getState) => {
|
||||
const {
|
||||
pushes: { pushList, jobMap },
|
||||
} = 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 pushPollingParams = PUSH_POLLING_KEYS.reduce(
|
||||
(acc, prop) =>
|
||||
locationSearch[prop] ? { ...acc, [prop]: locationSearch[prop] } : acc,
|
||||
{},
|
||||
);
|
||||
|
||||
if (pushList.length === 1 && locationSearch.revision) {
|
||||
// If we are on a single revision, no need to poll for more pushes, but
|
||||
// we need to keep polling for jobs.
|
||||
dispatch(fetchNewJobs());
|
||||
} else {
|
||||
if (pushList.length) {
|
||||
// We have a range of pushes, but not bound to a single push,
|
||||
// so get only pushes newer than our latest.
|
||||
pushPollingParams.fromchange = pushList[0].revision;
|
||||
}
|
||||
// We will either have a ``revision`` param, but no push for it yet,
|
||||
// or a ``fromchange`` param because we have at least 1 push already.
|
||||
const { data, failureStatus } = await PushModel.getList(
|
||||
pushPollingParams,
|
||||
);
|
||||
|
||||
if (!failureStatus) {
|
||||
dispatch({
|
||||
type: ADD_PUSHES,
|
||||
pushResults: addPushes(
|
||||
data.results.length ? data : { results: [] },
|
||||
pushList,
|
||||
jobMap,
|
||||
false,
|
||||
),
|
||||
});
|
||||
dispatch(fetchNewJobs());
|
||||
} else {
|
||||
dispatch(
|
||||
notify('Error fetching new push data', 'danger', { sticky: true }),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 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) => ({
|
||||
type: SET_PUSHES,
|
||||
pushResults: {
|
||||
pushList,
|
||||
jobMap,
|
||||
...getRevisionTips(pushList),
|
||||
...doRecalculateUnclassifiedCounts(jobMap),
|
||||
oldestPushTimestamp: pushList[pushList.length - 1].push_timestamp,
|
||||
},
|
||||
});
|
||||
|
||||
export const recalculateUnclassifiedCounts = filterModel => ({
|
||||
type: RECALCULATE_UNCLASSIFIED_COUNTS,
|
||||
filterModel,
|
||||
});
|
||||
|
||||
export const updateJobMap = jobList => ({
|
||||
type: UPDATE_JOB_MAP,
|
||||
jobList,
|
||||
});
|
||||
|
||||
export const updateRange = range => {
|
||||
return (dispatch, getState) => {
|
||||
const {
|
||||
pushes: { pushList, jobMap },
|
||||
} = getState();
|
||||
const { revision } = range;
|
||||
// change the range of pushes. might already have them.
|
||||
const revisionPushList = revision
|
||||
? pushList.filter(push => push.revision === revision)
|
||||
: [];
|
||||
|
||||
window.dispatchEvent(new CustomEvent(thEvents.clearPinboard));
|
||||
if (revisionPushList.length) {
|
||||
const { id: push_id } = revisionPushList[0];
|
||||
const revisionJobMap = Object.entries(jobMap).reduce(
|
||||
(acc, [id, job]) =>
|
||||
job.push_id === push_id ? { ...acc, [id]: job } : acc,
|
||||
{},
|
||||
);
|
||||
dispatch(clearSelectedJob(0));
|
||||
// We already have the one revision they're looking for,
|
||||
// so we can just erase everything else.
|
||||
dispatch(setPushes(revisionPushList, revisionJobMap));
|
||||
} else {
|
||||
// Clear and refetch everything. We can't be sure if what we
|
||||
// already have is partially correct and just needs fill-in.
|
||||
dispatch(clearPushes());
|
||||
return dispatch(fetchPushes());
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const initialState = {
|
||||
pushList: [],
|
||||
jobMap: {},
|
||||
revisionTips: [],
|
||||
jobsLoaded: false,
|
||||
loadingPushes: true,
|
||||
oldestPushTimestamp: null,
|
||||
allUnclassifiedFailureCount: 0,
|
||||
filteredUnclassifiedFailureCount: 0,
|
||||
};
|
||||
|
||||
export const reducer = (state = initialState, action) => {
|
||||
const { jobList, pushResults, setFromchange } = action;
|
||||
const { pushList, jobMap } = state;
|
||||
switch (action.type) {
|
||||
case LOADING:
|
||||
return { ...state, loadingPushes: true };
|
||||
case ADD_PUSHES:
|
||||
return { ...state, loadingPushes: false, ...pushResults, setFromchange };
|
||||
case CLEAR_PUSHES:
|
||||
return { ...initialState };
|
||||
case SET_PUSHES:
|
||||
return { ...state, loadingPushes: false, ...pushResults };
|
||||
case RECALCULATE_UNCLASSIFIED_COUNTS:
|
||||
return { ...state, ...doRecalculateUnclassifiedCounts(jobMap) };
|
||||
case UPDATE_JOB_MAP:
|
||||
return { ...state, ...doUpdateJobMap(jobList, jobMap, pushList) };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
|
@ -5,6 +5,7 @@ import { thPlatformMap } from '../helpers/constants';
|
|||
import { createQueryParams } from '../helpers/url';
|
||||
import { formatTaskclusterError } from '../helpers/errorMessage';
|
||||
import { getProjectUrl } from '../helpers/location';
|
||||
import { getData } from '../helpers/http';
|
||||
|
||||
import PushModel from './push';
|
||||
import TaskclusterModel from './taskcluster';
|
||||
|
@ -45,51 +46,55 @@ export default class JobModel {
|
|||
.join(' ');
|
||||
}
|
||||
|
||||
static getList(options, config) {
|
||||
// a static method to retrieve a list of JobModel
|
||||
config = config || {};
|
||||
const fetch_all = config.fetch_all || false;
|
||||
static async getList(options, config = {}) {
|
||||
// The `uri` config allows to fetch a list of jobs from an arbitrary
|
||||
// endpoint e.g. the similar jobs endpoint. It defaults to the job
|
||||
// list endpoint.
|
||||
const jobUri = config.uri || getProjectUrl(uri);
|
||||
const { fetchAll, uri: configUri } = config;
|
||||
const jobUri = configUri || getProjectUrl(uri);
|
||||
|
||||
return fetch(`${jobUri}${options ? createQueryParams(options) : ''}`).then(
|
||||
async resp => {
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
let itemList;
|
||||
let nextPagesJobs = [];
|
||||
|
||||
// if the number of elements returned equals the page size, fetch the next pages
|
||||
if (fetch_all && data.results.length === data.meta.count) {
|
||||
const count = parseInt(data.meta.count, 10);
|
||||
const offset = parseInt(data.meta.offset, 10) + count;
|
||||
const newOptions = { ...options, offset, count };
|
||||
|
||||
nextPagesJobs = await JobModel.getList(newOptions, config);
|
||||
}
|
||||
if ('job_property_names' in data) {
|
||||
// the results came as list of fields
|
||||
// we need to convert them to objects
|
||||
itemList = data.results.map(
|
||||
elem =>
|
||||
new JobModel(
|
||||
data.job_property_names.reduce(
|
||||
(prev, prop, i) => ({ ...prev, [prop]: elem[i] }),
|
||||
{},
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
itemList = data.results.map(job_obj => new JobModel(job_obj));
|
||||
}
|
||||
return [...itemList, ...nextPagesJobs];
|
||||
}
|
||||
const text = await resp.text();
|
||||
throw Error(text);
|
||||
},
|
||||
const { data, failureStatus } = await getData(
|
||||
`${jobUri}${options ? createQueryParams(options) : ''}`,
|
||||
);
|
||||
|
||||
if (!failureStatus) {
|
||||
const { results, meta, job_property_names } = data;
|
||||
let itemList;
|
||||
let nextPagesJobs = [];
|
||||
|
||||
// if the number of elements returned equals the page size,
|
||||
// fetch the next pages
|
||||
if (fetchAll && results.length === meta.count) {
|
||||
const count = parseInt(meta.count, 10);
|
||||
const offset = parseInt(meta.offset, 10) + count;
|
||||
const newOptions = { ...options, offset, count };
|
||||
const {
|
||||
data: nextData,
|
||||
failureStatus: nextFailureStatus,
|
||||
} = await JobModel.getList(newOptions, config);
|
||||
|
||||
if (!nextFailureStatus) {
|
||||
nextPagesJobs = nextData;
|
||||
}
|
||||
}
|
||||
if (job_property_names) {
|
||||
// the results came as list of fields
|
||||
// we need to convert them to objects
|
||||
itemList = results.map(
|
||||
elem =>
|
||||
new JobModel(
|
||||
job_property_names.reduce(
|
||||
(prev, prop, i) => ({ ...prev, [prop]: elem[i] }),
|
||||
{},
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
itemList = results.map(job_obj => new JobModel(job_obj));
|
||||
}
|
||||
return { data: [...itemList, ...nextPagesJobs], failureStatus: null };
|
||||
}
|
||||
return { data, failureStatus };
|
||||
}
|
||||
|
||||
static get(repoName, pk, signal) {
|
||||
|
|
17
yarn.lock
17
yarn.lock
|
@ -6068,6 +6068,11 @@ lodash.isplainobject@^3.2.0:
|
|||
lodash.isarguments "^3.0.0"
|
||||
lodash.keysin "^3.0.0"
|
||||
|
||||
lodash.isplainobject@^4.0.6:
|
||||
version "4.0.6"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
|
||||
integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
|
||||
|
||||
lodash.keysin@^3.0.0:
|
||||
version "3.0.8"
|
||||
resolved "https://registry.yarnpkg.com/lodash.keysin/-/lodash.keysin-3.0.8.tgz#22c4493ebbedb1427962a54b445b2c8a767fb47f"
|
||||
|
@ -8009,6 +8014,18 @@ redux-debounce@1.0.1:
|
|||
lodash.debounce "^4.0.6"
|
||||
lodash.mapvalues "^4.3.0"
|
||||
|
||||
redux-mock-store@1.5.3:
|
||||
version "1.5.3"
|
||||
resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.5.3.tgz#1f10528949b7ce8056c2532624f7cafa98576c6d"
|
||||
integrity sha512-ryhkkb/4D4CUGpAV2ln1GOY/uh51aczjcRz9k2L2bPx/Xja3c5pSGJJPyR25GNVRXtKIExScdAgFdiXp68GmJA==
|
||||
dependencies:
|
||||
lodash.isplainobject "^4.0.6"
|
||||
|
||||
redux-thunk@2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
|
||||
integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
|
||||
|
||||
redux@4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.4.tgz#4ee1aeb164b63d6a1bcc57ae4aa0b6e6fa7a3796"
|
||||
|
|
Загрузка…
Ссылка в новой задаче