Bug 1510280 - Convert Pushes context to Redux

This commit is contained in:
Cameron Dawson 2019-06-07 12:08:51 -07:00
Родитель 9654825fca
Коммит 9181ff4b94
35 изменённых файлов: 4854 добавлений и 1321 удалений

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

@ -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);
});
});

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Разница между файлами не показана из-за своего большого размера Загрузить разницу

1016
tests/ui/mock/job_map.json Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@ -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) {

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

@ -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"