зеркало из https://github.com/mozilla/treeherder.git
Bug 1450024 - Convert Notifications to ReactJS (#4132)
This commit is contained in:
Родитель
fd20a61c8e
Коммит
49368ca7ff
|
@ -4,7 +4,7 @@ import * as fetchMock from 'fetch-mock';
|
|||
import { mount } from 'enzyme';
|
||||
|
||||
import { getProjectUrl } from '../../../../ui/helpers/url';
|
||||
import { Pushes } from '../../../../ui/job-view/context/Pushes';
|
||||
import { PushesClass } from '../../../../ui/job-view/context/Pushes';
|
||||
import FilterModel from '../../../../ui/models/filter';
|
||||
|
||||
const { getJSONFixture, inject } = window;
|
||||
|
@ -42,10 +42,11 @@ describe('Pushes context', () => {
|
|||
*/
|
||||
it('should have 2 pushes', async () => {
|
||||
const pushes = mount(
|
||||
<Pushes
|
||||
<PushesClass
|
||||
filterModel={new FilterModel()}
|
||||
$injector={$injector}
|
||||
><div /></Pushes>,
|
||||
notify={() => {}}
|
||||
><div /></PushesClass>,
|
||||
);
|
||||
await pushes.instance().fetchPushes(10);
|
||||
expect(pushes.state('pushList').length).toBe(2);
|
||||
|
@ -53,10 +54,11 @@ describe('Pushes context', () => {
|
|||
|
||||
it('should have id of 1 in current repo', async () => {
|
||||
const pushes = mount(
|
||||
<Pushes
|
||||
<PushesClass
|
||||
filterModel={new FilterModel()}
|
||||
$injector={$injector}
|
||||
><div /></Pushes>,
|
||||
notify={() => {}}
|
||||
><div /></PushesClass>,
|
||||
);
|
||||
await pushes.instance().fetchPushes(10);
|
||||
expect(pushes.state('pushList')[0].id).toBe(1);
|
||||
|
|
|
@ -18,14 +18,5 @@ configure({ adapter: new Adapter() });
|
|||
const jsContext = require.context('../../../ui/js', true, /^\.\/.*\.jsx?$/);
|
||||
jsContext('./filters.js');
|
||||
|
||||
const controllerContext = require.context('../../../ui/js/controllers', true, /^\.\/.*\.jsx?$/);
|
||||
controllerContext.keys().forEach(controllerContext);
|
||||
const directiveContext = require.context('../../../ui/js/directives', true, /^\.\/.*\.jsx?$/);
|
||||
directiveContext.keys().forEach(directiveContext);
|
||||
const serviceContext = require.context('../../../ui/js/services', true, /^\.\/.*\.jsx?$/);
|
||||
serviceContext.keys().forEach(serviceContext);
|
||||
const componentContext = require.context('../../../ui/js/components', true, /^\.\/.*\.jsx?$/);
|
||||
componentContext.keys().forEach(componentContext);
|
||||
|
||||
const testContext = require.context('./', true, /^\.\/.*\.tests\.jsx?$/);
|
||||
testContext.keys().forEach(testContext);
|
||||
|
|
|
@ -4,7 +4,7 @@ import * as fetchMock from 'fetch-mock';
|
|||
|
||||
import { hgBaseUrl, bzBaseUrl } from '../../../../ui/helpers/url';
|
||||
import { isReftest } from '../../../../ui/helpers/job';
|
||||
import BugFiler from '../../../../ui/job-view/details/BugFiler';
|
||||
import { BugFilerClass } from '../../../../ui/job-view/details/BugFiler';
|
||||
|
||||
describe('BugFiler', () => {
|
||||
const fullLog = 'https://queue.taskcluster.net/v1/task/AGs4CgN_RnCTb943uQn8NQ/runs/0/artifacts/public/logs/live_backing.log';
|
||||
|
@ -70,7 +70,7 @@ describe('BugFiler', () => {
|
|||
};
|
||||
|
||||
return mount(
|
||||
<BugFiler
|
||||
<BugFilerClass
|
||||
isOpen={isOpen}
|
||||
toggle={toggle}
|
||||
suggestion={suggestion}
|
||||
|
@ -80,7 +80,7 @@ describe('BugFiler', () => {
|
|||
reftestUrl={isReftest(selectedJob) ? reftest : ''}
|
||||
successCallback={successCallback}
|
||||
jobGroupName={selectedJob.job_group_name}
|
||||
notify={{}}
|
||||
notify={() => {}}
|
||||
/>,
|
||||
);
|
||||
};
|
||||
|
@ -211,7 +211,7 @@ describe('BugFiler', () => {
|
|||
search: 'REFTEST TEST-UNEXPECTED-PASS | flee | floo' },
|
||||
];
|
||||
const bugFiler = mount(
|
||||
<BugFiler
|
||||
<BugFilerClass
|
||||
isOpen={isOpen}
|
||||
toggle={toggle}
|
||||
suggestion={suggestions[0]}
|
||||
|
@ -221,7 +221,7 @@ describe('BugFiler', () => {
|
|||
reftestUrl={isReftest(selectedJob) ? reftest : ''}
|
||||
successCallback={successCallback}
|
||||
jobGroupName={selectedJob.job_group_name}
|
||||
notify={{}}
|
||||
notify={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
|
|
|
@ -30,7 +30,5 @@ import './job-view/JobView';
|
|||
import './shared/ShortcutTable';
|
||||
|
||||
// Treeherder JS
|
||||
import './js/directives/treeherder/main';
|
||||
import './js/services/main';
|
||||
import './js/models/perf/series';
|
||||
import './js/controllers/main';
|
||||
|
|
|
@ -16,8 +16,6 @@ import './js/logviewer';
|
|||
|
||||
// Logviewer JS
|
||||
import './js/directives/treeherder/log_viewer_steps';
|
||||
import './js/directives/treeherder/main';
|
||||
import './js/components/logviewer/logviewer';
|
||||
import './js/services/main';
|
||||
import './js/filters';
|
||||
import './js/controllers/logviewer';
|
||||
|
|
|
@ -27,7 +27,6 @@ import './shared/Login';
|
|||
|
||||
// Perf JS
|
||||
import './js/filters';
|
||||
import './js/services/main';
|
||||
import './js/models/perf/series';
|
||||
import './js/models/perf/issue_tracker';
|
||||
import './js/models/perf/performance_framework';
|
||||
|
|
|
@ -22,7 +22,5 @@
|
|||
|
||||
<job-view />
|
||||
</div>
|
||||
|
||||
<th-notification-box></th-notification-box>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
import { formatTaskclusterError } from '../helpers/errorMessage';
|
||||
import TaskclusterModel from '../models/taskcluster';
|
||||
import { withPushes } from './context/Pushes';
|
||||
import { withNotifications } from '../shared/context/Notifications';
|
||||
|
||||
class CustomJobActions extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -97,13 +98,13 @@ class CustomJobActions extends React.Component {
|
|||
input = jsyaml.safeLoad(payload);
|
||||
} catch (e) {
|
||||
this.setState({ triggering: false });
|
||||
notify.send(`YAML Error: ${e.message}`, 'danger');
|
||||
notify(`YAML Error: ${e.message}`, 'danger');
|
||||
return;
|
||||
}
|
||||
const valid = validate(input);
|
||||
if (!valid) {
|
||||
this.setState({ triggering: false });
|
||||
notify.send(ajv.errorsText(validate.errors), 'danger');
|
||||
notify(ajv.errorsText(validate.errors), 'danger');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -130,10 +131,10 @@ class CustomJobActions extends React.Component {
|
|||
message = 'Visit Taskcluster Tools site to access loaner:';
|
||||
url = `${url}/connect`;
|
||||
}
|
||||
notify.send(message, 'success', { linkText: 'Open in Taskcluster', url });
|
||||
notify(message, 'success', { linkText: 'Open in Taskcluster', url });
|
||||
this.close();
|
||||
}, (e) => {
|
||||
notify.send(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
notify(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
this.setState({ triggering: false });
|
||||
this.close();
|
||||
});
|
||||
|
@ -229,7 +230,7 @@ class CustomJobActions extends React.Component {
|
|||
CustomJobActions.propTypes = {
|
||||
pushId: PropTypes.number.isRequired,
|
||||
isLoggedIn: PropTypes.bool.isRequired,
|
||||
notify: PropTypes.object.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
toggle: PropTypes.func.isRequired,
|
||||
getGeckoDecisionTaskId: PropTypes.func.isRequired,
|
||||
job: PropTypes.object,
|
||||
|
@ -239,4 +240,4 @@ CustomJobActions.defaultProps = {
|
|||
job: null,
|
||||
};
|
||||
|
||||
export default withPushes(CustomJobActions);
|
||||
export default withNotifications(withPushes(CustomJobActions));
|
||||
|
|
|
@ -8,6 +8,7 @@ import { thEvents, thFavicons } from '../helpers/constants';
|
|||
import { Pushes } from './context/Pushes';
|
||||
import { SelectedJob } from './context/SelectedJob';
|
||||
import { PinnedJobs } from './context/PinnedJobs';
|
||||
import { Notifications } from '../shared/context/Notifications';
|
||||
import { matchesDefaults } from '../helpers/filter';
|
||||
import { getAllUrlParams, getRepo } from '../helpers/location';
|
||||
import { deployedRevisionUrl } from '../helpers/url';
|
||||
|
@ -20,6 +21,7 @@ import UpdateAvailable from './headerbars/UpdateAvailable';
|
|||
import DetailsPanel from './details/DetailsPanel';
|
||||
import PushList from './pushes/PushList';
|
||||
import KeyboardShortcuts from './KeyboardShortcuts';
|
||||
import NotificationList from '../shared/NotificationList';
|
||||
|
||||
const DEFAULT_DETAILS_PCT = 40;
|
||||
const REVISION_POLL_INTERVAL = 1000 * 60 * 5;
|
||||
|
@ -42,7 +44,6 @@ class JobView extends React.Component {
|
|||
|
||||
const { $injector } = this.props;
|
||||
this.$rootScope = $injector.get('$rootScope');
|
||||
this.thNotify = $injector.get('thNotify');
|
||||
|
||||
const filterModel = new FilterModel();
|
||||
this.$rootScope.filterModel = filterModel;
|
||||
|
@ -231,75 +232,74 @@ class JobView extends React.Component {
|
|||
), []);
|
||||
|
||||
return (
|
||||
<Pushes filterModel={filterModel} $injector={$injector}>
|
||||
<PinnedJobs notify={this.thNotify}>
|
||||
<SelectedJob
|
||||
notify={this.thNotify}
|
||||
$injector={$injector}
|
||||
>
|
||||
<KeyboardShortcuts
|
||||
filterModel={filterModel}
|
||||
$injector={$injector}
|
||||
>
|
||||
<PrimaryNavBar
|
||||
repos={repos}
|
||||
updateButtonClick={this.updateButtonClick}
|
||||
serverChanged={serverChanged}
|
||||
<Notifications>
|
||||
<Pushes filterModel={filterModel} $injector={$injector}>
|
||||
<PinnedJobs>
|
||||
<SelectedJob>
|
||||
<KeyboardShortcuts
|
||||
filterModel={filterModel}
|
||||
setUser={this.setUser}
|
||||
user={user}
|
||||
setCurrentRepoTreeStatus={this.setCurrentRepoTreeStatus}
|
||||
getAllShownJobs={this.getAllShownJobs}
|
||||
duplicateJobsVisible={duplicateJobsVisible}
|
||||
groupCountsExpanded={groupCountsExpanded}
|
||||
$injector={$injector}
|
||||
/>
|
||||
<SplitPane
|
||||
split="horizontal"
|
||||
size={`${pushListPct}%`}
|
||||
onChange={size => this.handleSplitChange(size)}
|
||||
>
|
||||
<div className="d-flex flex-column w-100">
|
||||
{(isFieldFilterVisible || !!filterBarFilters.length) && <ActiveFilters
|
||||
$injector={$injector}
|
||||
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" data-job-clear-on-click>
|
||||
<span className="th-view-content" tabIndex={-1}>
|
||||
<PushList
|
||||
user={user}
|
||||
repoName={repoName}
|
||||
revision={revision}
|
||||
currentRepo={currentRepo}
|
||||
filterModel={filterModel}
|
||||
$injector={$injector}
|
||||
duplicateJobsVisible={duplicateJobsVisible}
|
||||
groupCountsExpanded={groupCountsExpanded}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<DetailsPanel
|
||||
resizedHeight={detailsHeight}
|
||||
currentRepo={currentRepo}
|
||||
repoName={repoName}
|
||||
<PrimaryNavBar
|
||||
repos={repos}
|
||||
updateButtonClick={this.updateButtonClick}
|
||||
serverChanged={serverChanged}
|
||||
filterModel={filterModel}
|
||||
setUser={this.setUser}
|
||||
user={user}
|
||||
classificationTypes={classificationTypes}
|
||||
classificationMap={classificationMap}
|
||||
setCurrentRepoTreeStatus={this.setCurrentRepoTreeStatus}
|
||||
getAllShownJobs={this.getAllShownJobs}
|
||||
duplicateJobsVisible={duplicateJobsVisible}
|
||||
groupCountsExpanded={groupCountsExpanded}
|
||||
$injector={$injector}
|
||||
/>
|
||||
</SplitPane>
|
||||
</KeyboardShortcuts>
|
||||
</SelectedJob>
|
||||
</PinnedJobs>
|
||||
</Pushes>
|
||||
<SplitPane
|
||||
split="horizontal"
|
||||
size={`${pushListPct}%`}
|
||||
onChange={size => this.handleSplitChange(size)}
|
||||
>
|
||||
<div className="d-flex flex-column w-100">
|
||||
{(isFieldFilterVisible || !!filterBarFilters.length) && <ActiveFilters
|
||||
$injector={$injector}
|
||||
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" data-job-clear-on-click>
|
||||
<span className="th-view-content" tabIndex={-1}>
|
||||
<PushList
|
||||
user={user}
|
||||
repoName={repoName}
|
||||
revision={revision}
|
||||
currentRepo={currentRepo}
|
||||
filterModel={filterModel}
|
||||
duplicateJobsVisible={duplicateJobsVisible}
|
||||
groupCountsExpanded={groupCountsExpanded}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<DetailsPanel
|
||||
resizedHeight={detailsHeight}
|
||||
currentRepo={currentRepo}
|
||||
repoName={repoName}
|
||||
user={user}
|
||||
classificationTypes={classificationTypes}
|
||||
classificationMap={classificationMap}
|
||||
$injector={$injector}
|
||||
/>
|
||||
</SplitPane>
|
||||
<NotificationList />
|
||||
</KeyboardShortcuts>
|
||||
</SelectedJob>
|
||||
</PinnedJobs>
|
||||
</Pushes>
|
||||
</Notifications>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withNotifications } from '../../shared/context/Notifications';
|
||||
|
||||
const COUNT_ERROR = 'Max pinboard size of 500 reached.';
|
||||
const MAX_SIZE = 500;
|
||||
const PinnedJobsContext = React.createContext({});
|
||||
|
||||
export class PinnedJobs extends React.Component {
|
||||
export class PinnedJobsClass extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
@ -73,7 +74,7 @@ export class PinnedJobs extends React.Component {
|
|||
}, () => { if (callback) callback(); });
|
||||
this.pulsePinCount();
|
||||
} else {
|
||||
notify.send(COUNT_ERROR, 'danger');
|
||||
notify(COUNT_ERROR, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -92,7 +93,7 @@ export class PinnedJobs extends React.Component {
|
|||
const newPinnedJobs = jobsToPin.slice(0, spaceRemaining).reduce((acc, job) => ({ ...acc, [job.id]: job }), {});
|
||||
|
||||
if (!spaceRemaining) {
|
||||
notify.send(COUNT_ERROR, 'danger', { sticky: true });
|
||||
notify(COUNT_ERROR, 'danger', { sticky: true });
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -101,7 +102,7 @@ export class PinnedJobs extends React.Component {
|
|||
isPinBoardVisible: true,
|
||||
}, () => {
|
||||
if (showError) {
|
||||
notify.send(COUNT_ERROR, 'danger', { sticky: true });
|
||||
notify(COUNT_ERROR, 'danger', { sticky: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -160,6 +161,13 @@ export class PinnedJobs extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
PinnedJobsClass.propTypes = {
|
||||
notify: PropTypes.func.isRequired,
|
||||
children: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export const PinnedJobs = withNotifications(PinnedJobsClass);
|
||||
|
||||
export function withPinnedJobs(Component) {
|
||||
return function PinBoardComponent(props) {
|
||||
return (
|
||||
|
@ -184,8 +192,3 @@ export function withPinnedJobs(Component) {
|
|||
);
|
||||
};
|
||||
}
|
||||
|
||||
PinnedJobs.propTypes = {
|
||||
notify: PropTypes.object.isRequired,
|
||||
children: PropTypes.object.isRequired,
|
||||
};
|
||||
|
|
|
@ -17,6 +17,7 @@ import { isUnclassifiedFailure } from '../../helpers/job';
|
|||
import PushModel from '../../models/push';
|
||||
import JobModel from '../../models/job';
|
||||
import { reloadOnChangeParameters } from '../../helpers/filter';
|
||||
import { withNotifications } from '../../shared/context/Notifications';
|
||||
|
||||
const PushesContext = React.createContext({});
|
||||
const defaultPushCount = 10;
|
||||
|
@ -26,13 +27,12 @@ const pushPollingKeys = ['tochange', 'enddate', 'revision', 'author'];
|
|||
const pushFetchKeys = [...pushPollingKeys, 'fromchange', 'startdate'];
|
||||
const pushPollInterval = 60000;
|
||||
|
||||
export class Pushes extends React.Component {
|
||||
export class PushesClass extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const { $injector } = this.props;
|
||||
this.$rootScope = $injector.get('$rootScope');
|
||||
this.thNotify = $injector.get('thNotify');
|
||||
|
||||
this.skipNextPageReload = false;
|
||||
|
||||
|
@ -208,6 +208,7 @@ export class Pushes extends React.Component {
|
|||
|
||||
poll() {
|
||||
this.pushIntervalId = setInterval(() => {
|
||||
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
|
||||
|
@ -221,7 +222,7 @@ export class Pushes extends React.Component {
|
|||
const data = await resp.json();
|
||||
this.addPushes(data);
|
||||
} else {
|
||||
this.thNotify.send('Error fetching new push data', 'danger', { sticky: true });
|
||||
notify('Error fetching new push data', 'danger', { sticky: true });
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -262,6 +263,7 @@ export class Pushes extends React.Component {
|
|||
* @param count How many to fetch
|
||||
*/
|
||||
fetchPushes(count) {
|
||||
const { notify } = this.props;
|
||||
const { oldestPushTimestamp } = this.state;
|
||||
// const isAppend = (repoData.pushes.length > 0);
|
||||
// Only pass supported query string params to this endpoint.
|
||||
|
@ -285,7 +287,7 @@ export class Pushes extends React.Component {
|
|||
|
||||
this.addPushes(data.results.length ? data : { results: [] });
|
||||
} else {
|
||||
this.thNotify.send('Error retrieving push data!', 'danger', { sticky: true });
|
||||
notify('Error retrieving push data!', 'danger', { sticky: true });
|
||||
}
|
||||
}).then(() => this.setValue({ loadingPushes: false }));
|
||||
}
|
||||
|
@ -370,12 +372,15 @@ export class Pushes extends React.Component {
|
|||
|
||||
}
|
||||
|
||||
Pushes.propTypes = {
|
||||
PushesClass.propTypes = {
|
||||
children: PropTypes.object.isRequired,
|
||||
filterModel: PropTypes.object.isRequired,
|
||||
$injector: PropTypes.object.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const Pushes = withNotifications(PushesClass);
|
||||
|
||||
export function withPushes(Component) {
|
||||
return function PushesComponent(props) {
|
||||
return (
|
||||
|
|
|
@ -16,6 +16,7 @@ import JobModel from '../../models/job';
|
|||
import PushModel from '../../models/push';
|
||||
import { withPinnedJobs } from './PinnedJobs';
|
||||
import { withPushes } from './Pushes';
|
||||
import { withNotifications } from '../../shared/context/Notifications';
|
||||
|
||||
const SelectedJobContext = React.createContext({});
|
||||
|
||||
|
@ -96,7 +97,7 @@ class SelectedJobClass extends React.Component {
|
|||
|
||||
// the job exists, but isn't in any loaded push.
|
||||
// provide a message and link to load the right push
|
||||
notify.send(
|
||||
notify(
|
||||
`Selected job id: ${selectedJobId} not within current push range.`,
|
||||
'danger',
|
||||
{ sticky: true, linkText: 'Load push', url: newPushUrl });
|
||||
|
@ -108,7 +109,7 @@ class SelectedJobClass extends React.Component {
|
|||
// the job wasn't found in the db. Either never existed,
|
||||
// or was expired and deleted.
|
||||
this.clearSelectedJob();
|
||||
notify.send(`Selected Job - ${error}`,
|
||||
notify(`Selected Job - ${error}`,
|
||||
'danger',
|
||||
{ sticky: true });
|
||||
});
|
||||
|
@ -169,7 +170,7 @@ class SelectedJobClass extends React.Component {
|
|||
noMoreUnclassifiedFailures() {
|
||||
const { pinnedJobs, notify } = this.props;
|
||||
|
||||
notify.send('No unclassified failures to select.');
|
||||
notify('No unclassified failures to select.');
|
||||
this.clearSelectedJob(Object.keys(pinnedJobs).length);
|
||||
}
|
||||
|
||||
|
@ -257,13 +258,13 @@ class SelectedJobClass extends React.Component {
|
|||
|
||||
SelectedJobClass.propTypes = {
|
||||
jobsLoaded: PropTypes.bool.isRequired,
|
||||
notify: PropTypes.object.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
pinnedJobs: PropTypes.object.isRequired,
|
||||
jobMap: PropTypes.object.isRequired,
|
||||
children: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export const SelectedJob = withPushes(withPinnedJobs(SelectedJobClass));
|
||||
export const SelectedJob = withNotifications(withPushes(withPinnedJobs(SelectedJobClass)));
|
||||
|
||||
export function withSelectedJob(Component) {
|
||||
return function SelectedJobComponent(props) {
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
hgBaseUrl,
|
||||
} from '../../helpers/url';
|
||||
import { create } from '../../helpers/http';
|
||||
import { withNotifications } from '../../shared/context/Notifications';
|
||||
|
||||
const crashRegex = /application crashed \[@ (.+)\]$/g;
|
||||
const omittedLeads = ['TEST-UNEXPECTED-FAIL', 'PROCESS-CRASH', 'TEST-UNEXPECTED-ERROR', 'REFTEST ERROR'];
|
||||
|
@ -79,7 +80,7 @@ const parseSummary = (suggestion) => {
|
|||
return [summaryParts, possibleFilename];
|
||||
};
|
||||
|
||||
export default class BugFiler extends React.Component {
|
||||
export class BugFilerClass extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
@ -294,12 +295,12 @@ export default class BugFiler extends React.Component {
|
|||
const [product, component] = selectedProduct.split(' :: ');
|
||||
|
||||
if (!selectedProduct) {
|
||||
notify.send('Please select (or search and select) a product/component pair to continue', 'danger');
|
||||
notify('Please select (or search and select) a product/component pair to continue', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
if (summary.length > 255) {
|
||||
notify.send('Please ensure the summary is no more than 255 characters', 'danger');
|
||||
notify('Please ensure the summary is no more than 255 characters', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -353,7 +354,7 @@ export default class BugFiler extends React.Component {
|
|||
this.submitFailure('Bugzilla', productResp.status, productResp.statusText, productData);
|
||||
}
|
||||
} catch (e) {
|
||||
notify.send(`Error filing bug: ${e.toString()}`, 'danger', { sticky: true });
|
||||
notify(`Error filing bug: ${e.toString()}`, 'danger', { sticky: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -367,7 +368,7 @@ export default class BugFiler extends React.Component {
|
|||
if (status === 403) {
|
||||
failureString += '\n\nAuthentication failed. Has your Treeherder session expired?';
|
||||
}
|
||||
notify.send(failureString, 'danger', { sticky: true });
|
||||
notify(failureString, 'danger', { sticky: true });
|
||||
}
|
||||
|
||||
toggleTooltip(key) {
|
||||
|
@ -601,7 +602,7 @@ export default class BugFiler extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
BugFiler.propTypes = {
|
||||
BugFilerClass.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
toggle: PropTypes.func.isRequired,
|
||||
suggestion: PropTypes.object.isRequired,
|
||||
|
@ -611,5 +612,7 @@ BugFiler.propTypes = {
|
|||
reftestUrl: PropTypes.string.isRequired,
|
||||
successCallback: PropTypes.func.isRequired,
|
||||
jobGroupName: PropTypes.string.isRequired,
|
||||
notify: PropTypes.object.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withNotifications(BugFilerClass);
|
||||
|
|
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
|||
import { FormGroup, Input, FormFeedback } from 'reactstrap';
|
||||
|
||||
import { thEvents } from '../../helpers/constants';
|
||||
import { withNotifications } from '../../shared/context/Notifications';
|
||||
import { formatModelError } from '../../helpers/errorMessage';
|
||||
import { getJobBtnClass, getHoverText } from '../../helpers/job';
|
||||
import { isSHAorCommit } from '../../helpers/revision';
|
||||
|
@ -19,7 +20,6 @@ class PinBoard extends React.Component {
|
|||
super(props);
|
||||
|
||||
const { $injector } = this.props;
|
||||
this.thNotify = $injector.get('thNotify');
|
||||
this.$rootScope = $injector.get('$rootScope');
|
||||
|
||||
this.state = {
|
||||
|
@ -67,7 +67,7 @@ class PinBoard extends React.Component {
|
|||
}
|
||||
|
||||
save() {
|
||||
const { isLoggedIn, pinnedJobs, recalculateUnclassifiedCounts } = this.props;
|
||||
const { isLoggedIn, pinnedJobs, recalculateUnclassifiedCounts, notify } = this.props;
|
||||
|
||||
let errorFree = true;
|
||||
if (this.state.enteringBugNumber) {
|
||||
|
@ -75,15 +75,15 @@ class PinBoard extends React.Component {
|
|||
// just forgot to hit enter. Returns false if invalid
|
||||
errorFree = this.saveEnteredBugNumber();
|
||||
if (!errorFree) {
|
||||
this.thNotify.send('Please enter a valid bug number', 'danger');
|
||||
notify('Please enter a valid bug number', 'danger');
|
||||
}
|
||||
}
|
||||
if (!this.canSaveClassifications() && isLoggedIn) {
|
||||
this.thNotify.send('Please classify this failure before saving', 'danger');
|
||||
notify('Please classify this failure before saving', 'danger');
|
||||
errorFree = false;
|
||||
}
|
||||
if (!isLoggedIn) {
|
||||
this.thNotify.send('Must be logged in to save job classifications', 'danger');
|
||||
notify('Must be logged in to save job classifications', 'danger');
|
||||
errorFree = false;
|
||||
}
|
||||
if (errorFree) {
|
||||
|
@ -122,7 +122,7 @@ class PinBoard extends React.Component {
|
|||
}
|
||||
|
||||
saveClassification(job) {
|
||||
const { recalculateUnclassifiedCounts } = this.props;
|
||||
const { recalculateUnclassifiedCounts, notify } = this.props;
|
||||
const classification = this.createNewClassification();
|
||||
|
||||
// classification can be left unset making this a no-op
|
||||
|
@ -133,16 +133,16 @@ class PinBoard extends React.Component {
|
|||
|
||||
classification.job_id = job.id;
|
||||
return classification.create().then(() => {
|
||||
this.thNotify.send(`Classification saved for ${job.platform} ${job.job_type_name}`, 'success');
|
||||
notify(`Classification saved for ${job.platform} ${job.job_type_name}`, 'success');
|
||||
}).catch((response) => {
|
||||
const message = `Error saving classification for ${job.platform} ${job.job_type_name}`;
|
||||
this.thNotify.send(formatModelError(response, message), 'danger');
|
||||
notify(formatModelError(response, message), 'danger');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
saveBugs(job) {
|
||||
const { pinnedJobBugs } = this.props;
|
||||
const { pinnedJobBugs, notify } = this.props;
|
||||
|
||||
Object.values(pinnedJobBugs).forEach((bug) => {
|
||||
const bjm = new BugJobMapModel({
|
||||
|
@ -153,11 +153,11 @@ class PinBoard extends React.Component {
|
|||
|
||||
bjm.create()
|
||||
.then(() => {
|
||||
this.thNotify.send(`Bug association saved for ${job.platform} ${job.job_type_name}`, 'success');
|
||||
notify(`Bug association saved for ${job.platform} ${job.job_type_name}`, 'success');
|
||||
})
|
||||
.catch((response) => {
|
||||
const message = `Error saving bug association for ${job.platform} ${job.job_type_name}`;
|
||||
this.thNotify.send(formatModelError(response, message), 'danger');
|
||||
notify(formatModelError(response, message), 'danger');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -191,7 +191,7 @@ class PinBoard extends React.Component {
|
|||
}
|
||||
|
||||
cancelAllPinnedJobs() {
|
||||
const { getGeckoDecisionTaskId } = this.props;
|
||||
const { getGeckoDecisionTaskId, notify } = this.props;
|
||||
|
||||
if (window.confirm('This will cancel all the selected jobs. Are you sure?')) {
|
||||
const jobIds = Object.keys(this.props.pinnedJobs);
|
||||
|
@ -200,7 +200,7 @@ class PinBoard extends React.Component {
|
|||
jobIds,
|
||||
this.$rootScope.repoName,
|
||||
getGeckoDecisionTaskId,
|
||||
this.thNotify,
|
||||
notify,
|
||||
);
|
||||
|
||||
this.unPinAll();
|
||||
|
@ -332,12 +332,12 @@ class PinBoard extends React.Component {
|
|||
}
|
||||
|
||||
retriggerAllPinnedJobs() {
|
||||
const { getGeckoDecisionTaskId } = this.props;
|
||||
const { getGeckoDecisionTaskId, notify } = this.props;
|
||||
JobModel.retrigger(
|
||||
Object.keys(this.props.pinnedJobs),
|
||||
this.$rootScope.repoName,
|
||||
getGeckoDecisionTaskId,
|
||||
this.thNotify,
|
||||
notify,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -548,6 +548,7 @@ PinBoard.propTypes = {
|
|||
unPinAll: PropTypes.func.isRequired,
|
||||
getGeckoDecisionTaskId: PropTypes.func.isRequired,
|
||||
setSelectedJob: PropTypes.func.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
selectedJob: PropTypes.object,
|
||||
email: PropTypes.string,
|
||||
revisionTips: PropTypes.array,
|
||||
|
@ -559,4 +560,4 @@ PinBoard.defaultProps = {
|
|||
revisionTips: [],
|
||||
};
|
||||
|
||||
export default withPushes(withSelectedJob(withPinnedJobs(PinBoard)));
|
||||
export default withNotifications(withPushes(withSelectedJob(withPinnedJobs(PinBoard))));
|
||||
|
|
|
@ -13,15 +13,13 @@ import LogUrls from './LogUrls';
|
|||
import { withSelectedJob } from '../../context/SelectedJob';
|
||||
import { withPinnedJobs } from '../../context/PinnedJobs';
|
||||
import { withPushes } from '../../context/Pushes';
|
||||
import { withNotifications } from '../../../shared/context/Notifications';
|
||||
|
||||
class ActionBar extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const { $injector } = this.props;
|
||||
|
||||
this.thNotify = $injector.get('thNotify');
|
||||
this.$interpolate = $injector.get('$interpolate');
|
||||
this.$rootScope = $injector.get('$rootScope');
|
||||
|
||||
this.state = {
|
||||
|
@ -32,15 +30,15 @@ class ActionBar extends React.Component {
|
|||
componentDidMount() {
|
||||
// Open the logviewer and provide notifications if it isn't available
|
||||
this.openLogViewerUnlisten = this.$rootScope.$on(thEvents.openLogviewer, () => {
|
||||
const { logParseStatus } = this.props;
|
||||
const { logParseStatus, notify } = this.props;
|
||||
|
||||
switch (logParseStatus) {
|
||||
case 'pending':
|
||||
this.thNotify.send('Log parsing in progress, log viewer not yet available', 'info'); break;
|
||||
notify('Log parsing in progress, log viewer not yet available', 'info'); break;
|
||||
case 'failed':
|
||||
this.thNotify.send('Log parsing has failed, log viewer is unavailable', 'warning'); break;
|
||||
notify('Log parsing has failed, log viewer is unavailable', 'warning'); break;
|
||||
case 'unavailable':
|
||||
this.thNotify.send('No logs available for this job', 'info'); break;
|
||||
notify('No logs available for this job', 'info'); break;
|
||||
case 'parsed':
|
||||
$('.logviewer-btn')[0].click();
|
||||
}
|
||||
|
@ -66,9 +64,9 @@ class ActionBar extends React.Component {
|
|||
}
|
||||
|
||||
createGeckoProfile() {
|
||||
const { user, selectedJob, getGeckoDecisionTaskId } = this.props;
|
||||
const { user, selectedJob, getGeckoDecisionTaskId, notify } = this.props;
|
||||
if (!user.isLoggedIn) {
|
||||
return this.thNotify.send('Must be logged in to create a gecko profile', 'danger');
|
||||
return notify('Must be logged in to create a gecko profile', 'danger');
|
||||
}
|
||||
|
||||
getGeckoDecisionTaskId(
|
||||
|
@ -77,7 +75,7 @@ class ActionBar extends React.Component {
|
|||
const geckoprofile = results.actions.find(result => result.name === 'geckoprofile');
|
||||
|
||||
if (geckoprofile === undefined || !geckoprofile.hasOwnProperty('kind')) {
|
||||
return this.thNotify.send('Job was scheduled without taskcluster support for GeckoProfiles');
|
||||
return notify('Job was scheduled without taskcluster support for GeckoProfiles');
|
||||
}
|
||||
|
||||
TaskclusterModel.submit({
|
||||
|
@ -88,13 +86,13 @@ class ActionBar extends React.Component {
|
|||
input: {},
|
||||
staticActionVariables: results.staticActionVariables,
|
||||
}).then(() => {
|
||||
this.thNotify.send(
|
||||
notify(
|
||||
'Request sent to collect gecko profile job via actions.json',
|
||||
'success');
|
||||
}, (e) => {
|
||||
// The full message is too large to fit in a Treeherder
|
||||
// notification box.
|
||||
this.thNotify.send(
|
||||
notify(
|
||||
formatTaskclusterError(e),
|
||||
'danger',
|
||||
{ sticky: true });
|
||||
|
@ -104,11 +102,11 @@ class ActionBar extends React.Component {
|
|||
}
|
||||
|
||||
retriggerJob(jobs) {
|
||||
const { user, repoName, getGeckoDecisionTaskId } = this.props;
|
||||
const { user, repoName, getGeckoDecisionTaskId, notify } = this.props;
|
||||
const jobIds = jobs.map(({ id }) => id);
|
||||
|
||||
if (!user.isLoggedIn) {
|
||||
return this.thNotify.send('Must be logged in to retrigger a job', 'danger');
|
||||
return notify('Must be logged in to retrigger a job', 'danger');
|
||||
}
|
||||
|
||||
// Spin the retrigger button when retriggers happen
|
||||
|
@ -119,24 +117,24 @@ class ActionBar extends React.Component {
|
|||
});
|
||||
});
|
||||
|
||||
JobModel.retrigger(jobIds, repoName, getGeckoDecisionTaskId, this.thNotify);
|
||||
JobModel.retrigger(jobIds, repoName, getGeckoDecisionTaskId, notify);
|
||||
}
|
||||
|
||||
backfillJob() {
|
||||
const { user, selectedJob, getGeckoDecisionTaskId } = this.props;
|
||||
const { user, selectedJob, getGeckoDecisionTaskId, notify } = this.props;
|
||||
|
||||
if (!this.canBackfill()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user.isLoggedIn) {
|
||||
this.thNotify.send('Must be logged in to backfill a job', 'danger');
|
||||
notify('Must be logged in to backfill a job', 'danger');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedJob.id) {
|
||||
this.thNotify.send('Job not yet loaded for backfill', 'warning');
|
||||
notify('Job not yet loaded for backfill', 'warning');
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -154,21 +152,16 @@ class ActionBar extends React.Component {
|
|||
input: {},
|
||||
staticActionVariables: results.staticActionVariables,
|
||||
}).then(() => {
|
||||
this.thNotify.send(
|
||||
'Request sent to backfill job via actions.json',
|
||||
'success');
|
||||
notify('Request sent to backfill job via actions.json', 'success');
|
||||
}, (e) => {
|
||||
// The full message is too large to fit in a Treeherder
|
||||
// notification box.
|
||||
this.thNotify.send(
|
||||
formatTaskclusterError(e),
|
||||
'danger',
|
||||
{ sticky: true });
|
||||
notify(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
});
|
||||
})
|
||||
));
|
||||
} else {
|
||||
this.thNotify.send('Unable to backfill this job type!', 'danger', { sticky: true });
|
||||
notify('Unable to backfill this job type!', 'danger', { sticky: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -203,11 +196,11 @@ class ActionBar extends React.Component {
|
|||
}
|
||||
|
||||
async createInteractiveTask() {
|
||||
const { user, selectedJob, repoName, getGeckoDecisionTaskId } = this.props;
|
||||
const { user, selectedJob, repoName, getGeckoDecisionTaskId, notify } = this.props;
|
||||
const jobId = selectedJob.id;
|
||||
|
||||
if (!user.isLoggedIn) {
|
||||
return this.thNotify.send('Must be logged in to create an interactive task', 'danger');
|
||||
return notify('Must be logged in to create an interactive task', 'danger');
|
||||
}
|
||||
|
||||
const job = await JobModel.get(repoName, jobId);
|
||||
|
@ -226,29 +219,26 @@ class ActionBar extends React.Component {
|
|||
staticActionVariables: results.staticActionVariables,
|
||||
});
|
||||
|
||||
this.thNotify.send(
|
||||
notify(
|
||||
`Request sent to create an interactive job via actions.json.
|
||||
You will soon receive an email containing a link to interact with the task.`,
|
||||
'success');
|
||||
} catch (e) {
|
||||
// The full message is too large to fit in a Treeherder
|
||||
// notification box.
|
||||
this.thNotify.send(
|
||||
formatTaskclusterError(e),
|
||||
'danger',
|
||||
{ sticky: true });
|
||||
notify(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
}
|
||||
}
|
||||
|
||||
cancelJobs(jobs) {
|
||||
const { user, repoName, getGeckoDecisionTaskId } = this.props;
|
||||
const { user, repoName, getGeckoDecisionTaskId, notify } = this.props;
|
||||
const jobIds = jobs.filter(({ state }) => state === 'pending' || state === 'running').map(({ id }) => id);
|
||||
|
||||
if (!user.isLoggedIn) {
|
||||
return this.thNotify.send('Must be logged in to cancel a job', 'danger');
|
||||
return notify('Must be logged in to cancel a job', 'danger');
|
||||
}
|
||||
|
||||
JobModel.cancel(jobIds, repoName, getGeckoDecisionTaskId, this.thNotify);
|
||||
JobModel.cancel(jobIds, repoName, getGeckoDecisionTaskId, notify);
|
||||
}
|
||||
|
||||
cancelJob() {
|
||||
|
@ -363,7 +353,6 @@ class ActionBar extends React.Component {
|
|||
job={selectedJob}
|
||||
pushId={selectedJob.push_id}
|
||||
isLoggedIn={user.isLoggedIn}
|
||||
notify={this.thNotify}
|
||||
toggle={this.toggleCustomJobActions}
|
||||
/>}
|
||||
</div>
|
||||
|
@ -379,6 +368,7 @@ ActionBar.propTypes = {
|
|||
selectedJob: PropTypes.object.isRequired,
|
||||
logParseStatus: PropTypes.string.isRequired,
|
||||
getGeckoDecisionTaskId: PropTypes.func.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
jobLogUrls: PropTypes.array,
|
||||
isTryRepo: PropTypes.bool,
|
||||
logViewerUrl: PropTypes.string,
|
||||
|
@ -392,4 +382,4 @@ ActionBar.defaultProps = {
|
|||
jobLogUrls: [],
|
||||
};
|
||||
|
||||
export default withPushes(withSelectedJob(withPinnedJobs(ActionBar)));
|
||||
export default withNotifications(withPushes(withSelectedJob(withPinnedJobs(ActionBar))));
|
||||
|
|
|
@ -5,6 +5,7 @@ import { thEvents } from '../../../helpers/constants';
|
|||
import { getBugUrl } from '../../../helpers/url';
|
||||
import { withSelectedJob } from '../../context/SelectedJob';
|
||||
import { withPushes } from '../../context/Pushes';
|
||||
import { withNotifications } from '../../../shared/context/Notifications';
|
||||
|
||||
function RelatedBugSaved(props) {
|
||||
const { deleteBug, bug } = props;
|
||||
|
@ -144,7 +145,6 @@ class AnnotationsTab extends React.Component {
|
|||
super(props);
|
||||
|
||||
const { $injector } = props;
|
||||
this.thNotify = $injector.get('thNotify');
|
||||
this.$rootScope = $injector.get('$rootScope');
|
||||
|
||||
this.deleteBug = this.deleteBug.bind(this);
|
||||
|
@ -152,7 +152,7 @@ class AnnotationsTab extends React.Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { classifications, bugs } = this.props;
|
||||
const { classifications, bugs, notify } = this.props;
|
||||
|
||||
this.deleteClassificationUnlisten = this.$rootScope.$on(thEvents.deleteClassification, () => {
|
||||
if (classifications.length) {
|
||||
|
@ -160,7 +160,7 @@ class AnnotationsTab extends React.Component {
|
|||
// Delete any number of bugs if they exist
|
||||
bugs.forEach((bug) => { this.deleteBug(bug); });
|
||||
} else {
|
||||
this.thNotify.send('No classification on this job to delete', 'warning');
|
||||
notify('No classification on this job to delete', 'warning');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -170,14 +170,14 @@ class AnnotationsTab extends React.Component {
|
|||
}
|
||||
|
||||
deleteClassification(classification) {
|
||||
const { selectedJob, recalculateUnclassifiedCounts } = this.props;
|
||||
const { selectedJob, recalculateUnclassifiedCounts, notify } = this.props;
|
||||
|
||||
selectedJob.failure_classification_id = 1;
|
||||
recalculateUnclassifiedCounts();
|
||||
|
||||
classification.destroy().then(
|
||||
() => {
|
||||
this.thNotify.send('Classification successfully deleted', 'success');
|
||||
notify('Classification successfully deleted', 'success');
|
||||
// also be sure the job object in question gets updated to the latest
|
||||
// classification state (in case one was added or removed).
|
||||
this.$rootScope.$emit(
|
||||
|
@ -186,23 +186,19 @@ class AnnotationsTab extends React.Component {
|
|||
);
|
||||
},
|
||||
() => {
|
||||
this.thNotify.send(
|
||||
'Classification deletion failed',
|
||||
'danger',
|
||||
{ sticky: true },
|
||||
);
|
||||
notify('Classification deletion failed', 'danger', { sticky: true });
|
||||
});
|
||||
}
|
||||
|
||||
deleteBug(bug) {
|
||||
const { selectedJob } = this.props;
|
||||
const { selectedJob, notify } = this.props;
|
||||
|
||||
bug.destroy()
|
||||
.then(() => {
|
||||
this.thNotify.send(`Association to bug ${bug.bug_id} successfully deleted`, 'success');
|
||||
notify(`Association to bug ${bug.bug_id} successfully deleted`, 'success');
|
||||
this.$rootScope.$emit(thEvents.jobsClassified, { jobs: { [selectedJob.id]: selectedJob } });
|
||||
}, () => {
|
||||
this.thNotify.send(`Association to bug ${bug.bug_id} deletion failed`, 'danger', { sticky: true });
|
||||
notify(`Association to bug ${bug.bug_id} deletion failed`, 'danger', { sticky: true });
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -247,6 +243,7 @@ AnnotationsTab.propTypes = {
|
|||
bugs: PropTypes.array.isRequired,
|
||||
classifications: PropTypes.array.isRequired,
|
||||
recalculateUnclassifiedCounts: PropTypes.func.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
selectedJob: PropTypes.object,
|
||||
};
|
||||
|
||||
|
@ -254,4 +251,4 @@ AnnotationsTab.defaultProps = {
|
|||
selectedJob: null,
|
||||
};
|
||||
|
||||
export default withPushes(withSelectedJob(AnnotationsTab));
|
||||
export default withNotifications(withPushes(withSelectedJob(AnnotationsTab)));
|
||||
|
|
|
@ -9,14 +9,12 @@ import JobModel from '../../../models/job';
|
|||
import PushModel from '../../../models/push';
|
||||
import TextLogStepModel from '../../../models/textLogStep';
|
||||
import { withSelectedJob } from '../../context/SelectedJob';
|
||||
import { withNotifications } from '../../../shared/context/Notifications';
|
||||
|
||||
class SimilarJobsTab extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const { $injector } = this.props;
|
||||
this.thNotify = $injector.get('thNotify');
|
||||
|
||||
this.pageSize = 20;
|
||||
|
||||
this.state = {
|
||||
|
@ -46,7 +44,7 @@ class SimilarJobsTab extends React.Component {
|
|||
|
||||
async getSimilarJobs() {
|
||||
const { page, similarJobs, selectedSimilarJob } = this.state;
|
||||
const { repoName, selectedJob } = this.props;
|
||||
const { repoName, selectedJob, notify } = this.props;
|
||||
const options = {
|
||||
// get one extra to detect if there are more jobs that can be loaded (hasNextPage)
|
||||
count: this.pageSize + 1,
|
||||
|
@ -89,7 +87,7 @@ class SimilarJobsTab extends React.Component {
|
|||
this.showJobInfo(newSimilarJobs[0]);
|
||||
}
|
||||
} else {
|
||||
this.thNotify.send(`Error fetching similar jobs push data: ${resp.message}`, 'danger', { sticky: true });
|
||||
notify(`Error fetching similar jobs push data: ${resp.message}`, 'danger', { sticky: true });
|
||||
}
|
||||
}
|
||||
this.setState({ isLoading: false });
|
||||
|
@ -284,9 +282,9 @@ class SimilarJobsTab extends React.Component {
|
|||
}
|
||||
|
||||
SimilarJobsTab.propTypes = {
|
||||
$injector: PropTypes.object.isRequired,
|
||||
repoName: PropTypes.string.isRequired,
|
||||
classificationMap: PropTypes.object.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
selectedJob: PropTypes.object,
|
||||
};
|
||||
|
||||
|
@ -294,4 +292,4 @@ SimilarJobsTab.defaultProps = {
|
|||
selectedJob: null,
|
||||
};
|
||||
|
||||
export default withSelectedJob(SimilarJobsTab);
|
||||
export default withNotifications(withSelectedJob(SimilarJobsTab));
|
||||
|
|
|
@ -180,7 +180,6 @@ class TabsPanel extends React.Component {
|
|||
<SimilarJobsTab
|
||||
repoName={repoName}
|
||||
classificationMap={classificationMap}
|
||||
$injector={$injector}
|
||||
/>
|
||||
</TabPanel>
|
||||
{!!perfJobDetail.length && <TabPanel>
|
||||
|
|
|
@ -11,6 +11,7 @@ import ErrorLine from './ErrorLine';
|
|||
import ErrorLineData from './ErrorLineModel';
|
||||
import { withSelectedJob } from '../../../context/SelectedJob';
|
||||
import { withPinnedJobs } from '../../../context/PinnedJobs';
|
||||
import { withNotifications } from '../../../../shared/context/Notifications';
|
||||
|
||||
class AutoclassifyTab extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -18,7 +19,6 @@ class AutoclassifyTab extends React.Component {
|
|||
|
||||
const { $injector } = this.props;
|
||||
|
||||
this.thNotify = $injector.get('thNotify');
|
||||
this.$rootScope = $injector.get('$rootScope');
|
||||
|
||||
this.state = {
|
||||
|
@ -235,6 +235,7 @@ class AutoclassifyTab extends React.Component {
|
|||
return Promise.reject('No lines to save');
|
||||
}
|
||||
const { errorLines } = this.state;
|
||||
const { notify } = this.props;
|
||||
const data = Object.values(lines).map(input => ({
|
||||
id: input.id,
|
||||
best_classification: input.classifiedFailureId || null,
|
||||
|
@ -255,7 +256,7 @@ class AutoclassifyTab extends React.Component {
|
|||
.catch((err) => {
|
||||
const prefix = 'Error saving classifications: ';
|
||||
const msg = err.stack ? `${prefix}${err}${err.stack}` : `${prefix}${err.statusText} - ${err.data.detail}`;
|
||||
this.thNotify.send(msg, 'danger', { sticky: true });
|
||||
notify(msg, 'danger', { sticky: true });
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -391,6 +392,7 @@ AutoclassifyTab.propTypes = {
|
|||
selectedJob: PropTypes.object.isRequired,
|
||||
hasLogs: PropTypes.bool.isRequired,
|
||||
pinJob: PropTypes.func.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
autoclassifyStatus: PropTypes.string,
|
||||
logsParsed: PropTypes.bool,
|
||||
logParseStatus: PropTypes.string,
|
||||
|
@ -402,4 +404,4 @@ AutoclassifyTab.defaultProps = {
|
|||
logParseStatus: 'pending',
|
||||
};
|
||||
|
||||
export default withSelectedJob(withPinnedJobs(AutoclassifyTab));
|
||||
export default withNotifications(withSelectedJob(withPinnedJobs(AutoclassifyTab)));
|
||||
|
|
|
@ -22,7 +22,6 @@ class LineOption extends React.Component {
|
|||
const { $injector } = props;
|
||||
|
||||
this.$rootScope = $injector.get('$rootScope');
|
||||
this.thNotify = $injector.get('thNotify');
|
||||
|
||||
this.state = {
|
||||
isBugFilerOpen: false,
|
||||
|
@ -189,7 +188,6 @@ class LineOption extends React.Component {
|
|||
reftestUrl={isReftest(selectedJob) ? getReftestUrl(logUrl) : ''}
|
||||
successCallback={this.bugFilerCallback}
|
||||
jobGroupName={selectedJob.job_group_name}
|
||||
notify={this.thNotify}
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -18,7 +18,6 @@ class FailureSummaryTab extends React.Component {
|
|||
|
||||
const { $injector } = this.props;
|
||||
this.$rootScope = $injector.get('$rootScope');
|
||||
this.thNotify = $injector.get('thNotify');
|
||||
|
||||
this.state = {
|
||||
isBugFilerOpen: false,
|
||||
|
@ -122,7 +121,6 @@ class FailureSummaryTab extends React.Component {
|
|||
reftestUrl={isReftest(selectedJob) ? reftestUrl : ''}
|
||||
successCallback={this.bugFilerCallback}
|
||||
jobGroupName={selectedJob.job_group_name}
|
||||
notify={this.thNotify}
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -2,35 +2,9 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { toShortDateStr } from '../../helpers/display';
|
||||
import { withNotifications } from '../../shared/context/Notifications';
|
||||
|
||||
export default class NotificationsMenu extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const { $injector } = this.props;
|
||||
this.thNotify = $injector.get('thNotify');
|
||||
|
||||
this.state = {
|
||||
notifications: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unlistenStorage = window.addEventListener('storage', (e) => {
|
||||
if (e.key === 'notifications') {
|
||||
this.changeCallback(JSON.parse(localStorage.getItem('notifications') || '[]'));
|
||||
}
|
||||
});
|
||||
|
||||
this.changeCallback = this.changeCallback.bind(this);
|
||||
this.thNotify.setChangeCallback(this.changeCallback);
|
||||
this.changeCallback(this.thNotify.storedNotifications);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unlistenStorage();
|
||||
}
|
||||
|
||||
class NotificationsMenu extends React.Component {
|
||||
getSeverityClass(severity) {
|
||||
switch (severity) {
|
||||
case 'danger': return 'fa fa-ban text-danger';
|
||||
|
@ -40,12 +14,8 @@ export default class NotificationsMenu extends React.Component {
|
|||
return 'fa fa-info-circle text-info';
|
||||
}
|
||||
|
||||
changeCallback(notifications) {
|
||||
this.setState({ notifications });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { notifications } = this.state;
|
||||
const { storedNotifications, clearStoredNotifications } = this.props;
|
||||
|
||||
return (
|
||||
<span className="dropdown">
|
||||
|
@ -67,14 +37,14 @@ export default class NotificationsMenu extends React.Component {
|
|||
className="dropdown-header"
|
||||
title="Notifications"
|
||||
>Recent notifications
|
||||
{!!notifications.length && <button
|
||||
{!!storedNotifications.length && <button
|
||||
className="btn btn-xs btn-light-bordered notification-dropdown-btn"
|
||||
title="Clear all notifications"
|
||||
onClick={this.thNotify.clear}
|
||||
onClick={clearStoredNotifications}
|
||||
>Clear all</button>}
|
||||
</li>
|
||||
{notifications.length ?
|
||||
notifications.map(notification => (
|
||||
{storedNotifications.length ?
|
||||
storedNotifications.map(notification => (
|
||||
<li
|
||||
className="notification-dropdown-line"
|
||||
key={notification.created}
|
||||
|
@ -100,5 +70,8 @@ export default class NotificationsMenu extends React.Component {
|
|||
}
|
||||
|
||||
NotificationsMenu.propTypes = {
|
||||
$injector: PropTypes.object.isRequired,
|
||||
storedNotifications: PropTypes.array.isRequired,
|
||||
clearStoredNotifications: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withNotifications(NotificationsMenu);
|
||||
|
|
|
@ -25,21 +25,13 @@ export default function PrimaryNavBar(props) {
|
|||
<div id="th-global-navbar-top">
|
||||
<LogoMenu />
|
||||
<span className="navbar-right">
|
||||
<NotificationsMenu $injector={$injector} />
|
||||
<NotificationsMenu />
|
||||
<InfraMenu />
|
||||
<ReposMenu repos={repos} />
|
||||
<TiersMenu
|
||||
filterModel={filterModel}
|
||||
/>
|
||||
<FiltersMenu
|
||||
filterModel={filterModel}
|
||||
/>
|
||||
<TiersMenu filterModel={filterModel} />
|
||||
<FiltersMenu filterModel={filterModel} />
|
||||
<HelpMenu />
|
||||
<Login
|
||||
user={user}
|
||||
setUser={setUser}
|
||||
$injector={$injector}
|
||||
/>
|
||||
<Login user={user} setUser={setUser} />
|
||||
</span>
|
||||
</div>
|
||||
<SecondaryNavBar
|
||||
|
|
|
@ -7,7 +7,7 @@ import { getStatus } from '../../helpers/job';
|
|||
export default class JobsAndGroups extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
$injector, groups, repoName, platform, filterPlatformCb, filterModel,
|
||||
groups, repoName, platform, filterPlatformCb, filterModel,
|
||||
pushGroupState, duplicateJobsVisible, groupCountsExpanded,
|
||||
} = this.props;
|
||||
|
||||
|
@ -19,7 +19,6 @@ export default class JobsAndGroups extends React.Component {
|
|||
group.visible && <JobGroup
|
||||
group={group}
|
||||
repoName={repoName}
|
||||
$injector={$injector}
|
||||
filterModel={filterModel}
|
||||
filterPlatformCb={filterPlatformCb}
|
||||
platform={platform}
|
||||
|
@ -34,7 +33,6 @@ export default class JobsAndGroups extends React.Component {
|
|||
group.jobs.map(job => (
|
||||
<JobButton
|
||||
job={job}
|
||||
$injector={$injector}
|
||||
filterModel={filterModel}
|
||||
repoName={repoName}
|
||||
visible={job.visible}
|
||||
|
@ -55,7 +53,6 @@ export default class JobsAndGroups extends React.Component {
|
|||
JobsAndGroups.propTypes = {
|
||||
groups: PropTypes.array.isRequired,
|
||||
repoName: PropTypes.string.isRequired,
|
||||
$injector: PropTypes.object.isRequired,
|
||||
filterModel: PropTypes.object.isRequired,
|
||||
filterPlatformCb: PropTypes.func.isRequired,
|
||||
platform: PropTypes.object.isRequired,
|
||||
|
|
|
@ -17,7 +17,7 @@ PlatformName.propTypes = {
|
|||
|
||||
export default function Platform(props) {
|
||||
const {
|
||||
platform, $injector, repoName, filterPlatformCb, filterModel, pushGroupState,
|
||||
platform, repoName, filterPlatformCb, filterModel, pushGroupState,
|
||||
duplicateJobsVisible, groupCountsExpanded,
|
||||
} = props;
|
||||
const { title, groups, id } = platform;
|
||||
|
@ -28,7 +28,6 @@ export default function Platform(props) {
|
|||
<JobsAndGroups
|
||||
groups={groups}
|
||||
repoName={repoName}
|
||||
$injector={$injector}
|
||||
filterPlatformCb={filterPlatformCb}
|
||||
platform={platform}
|
||||
filterModel={filterModel}
|
||||
|
@ -43,7 +42,6 @@ export default function Platform(props) {
|
|||
Platform.propTypes = {
|
||||
platform: PropTypes.object.isRequired,
|
||||
repoName: PropTypes.string.isRequired,
|
||||
$injector: PropTypes.object.isRequired,
|
||||
filterModel: PropTypes.object.isRequired,
|
||||
filterPlatformCb: PropTypes.func.isRequired,
|
||||
pushGroupState: PropTypes.string.isRequired,
|
||||
|
|
|
@ -13,6 +13,7 @@ import { getAllUrlParams } from '../../helpers/location';
|
|||
import PushModel from '../../models/push';
|
||||
import RunnableJobModel from '../../models/runnableJob';
|
||||
import { withSelectedJob } from '../context/SelectedJob';
|
||||
import { withNotifications } from '../../shared/context/Notifications';
|
||||
|
||||
const watchCycleStates = ['none', 'push', 'job', 'none'];
|
||||
const platformArray = Object.values(thPlatformMap);
|
||||
|
@ -21,9 +22,6 @@ const jobPollInterval = 60000;
|
|||
class Push extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { $injector } = props;
|
||||
|
||||
this.thNotify = $injector.get('thNotify');
|
||||
|
||||
this.state = {
|
||||
platforms: [],
|
||||
|
@ -229,7 +227,7 @@ class Push extends React.Component {
|
|||
showUpdateNotifications(prevState) {
|
||||
const { watched, jobCounts } = this.state;
|
||||
const {
|
||||
repoName, notificationSupported, push: { revision, id: pushId },
|
||||
repoName, notificationSupported, push: { revision, id: pushId }, notify,
|
||||
} = this.props;
|
||||
|
||||
if (!notificationSupported || Notification.permission !== 'granted' || watched === 'none') {
|
||||
|
@ -260,7 +258,7 @@ class Push extends React.Component {
|
|||
});
|
||||
|
||||
notification.onerror = (event) => {
|
||||
this.thNotify.send(`${event.target.title}: ${event.target.body}`, 'danger');
|
||||
notify(`${event.target.title}: ${event.target.body}`, 'danger');
|
||||
};
|
||||
|
||||
notification.onclick = (event) => {
|
||||
|
@ -274,7 +272,7 @@ class Push extends React.Component {
|
|||
}
|
||||
|
||||
async showRunnableJobs() {
|
||||
const { push, repoName, getGeckoDecisionTaskId } = this.props;
|
||||
const { push, repoName, getGeckoDecisionTaskId, notify } = this.props;
|
||||
|
||||
try {
|
||||
const decisionTaskId = await getGeckoDecisionTaskId(push.id, repoName);
|
||||
|
@ -286,12 +284,12 @@ class Push extends React.Component {
|
|||
job.id = escapeId(job.push_id + job.ref_data_name);
|
||||
});
|
||||
if (jobList.length === 0) {
|
||||
this.thNotify.send('No new jobs available');
|
||||
notify('No new jobs available');
|
||||
}
|
||||
this.mapPushJobs(jobList, true);
|
||||
this.setState({ runnableVisible: jobList.length > 0 });
|
||||
} catch (error) {
|
||||
this.thNotify.send(`Error fetching runnable jobs: Failed to fetch task ID (${error})`, 'danger');
|
||||
notify(`Error fetching runnable jobs: Failed to fetch task ID (${error})`, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -307,6 +305,8 @@ class Push extends React.Component {
|
|||
}
|
||||
|
||||
async cycleWatchState() {
|
||||
const { notify } = this.props;
|
||||
|
||||
if (!this.props.notificationSupported) {
|
||||
return;
|
||||
}
|
||||
|
@ -317,7 +317,7 @@ class Push extends React.Component {
|
|||
const result = await Notification.requestPermission();
|
||||
|
||||
if (result === 'denied') {
|
||||
this.thNotify.send('Notification permission denied', 'danger');
|
||||
notify('Notification permission denied', 'danger');
|
||||
|
||||
next = 'none';
|
||||
}
|
||||
|
@ -327,7 +327,7 @@ class Push extends React.Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
push, isLoggedIn, $injector, repoName, currentRepo, duplicateJobsVisible,
|
||||
push, isLoggedIn, repoName, currentRepo, duplicateJobsVisible,
|
||||
filterModel, notificationSupported, getAllShownJobs, groupCountsExpanded,
|
||||
} = this.props;
|
||||
const {
|
||||
|
@ -349,7 +349,6 @@ class Push extends React.Component {
|
|||
isLoggedIn={isLoggedIn}
|
||||
repoName={repoName}
|
||||
filterModel={filterModel}
|
||||
$injector={$injector}
|
||||
runnableVisible={runnableVisible}
|
||||
showRunnableJobs={this.showRunnableJobs}
|
||||
hideRunnableJobs={this.hideRunnableJobs}
|
||||
|
@ -364,7 +363,6 @@ class Push extends React.Component {
|
|||
{currentRepo &&
|
||||
<RevisionList
|
||||
push={push}
|
||||
$injector={$injector}
|
||||
repo={currentRepo}
|
||||
/>
|
||||
}
|
||||
|
@ -379,7 +377,6 @@ class Push extends React.Component {
|
|||
runnableVisible={runnableVisible}
|
||||
duplicateJobsVisible={duplicateJobsVisible}
|
||||
groupCountsExpanded={groupCountsExpanded}
|
||||
$injector={$injector}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -391,7 +388,6 @@ class Push extends React.Component {
|
|||
Push.propTypes = {
|
||||
push: PropTypes.object.isRequired,
|
||||
currentRepo: PropTypes.object.isRequired,
|
||||
$injector: PropTypes.object.isRequired,
|
||||
filterModel: PropTypes.object.isRequired,
|
||||
repoName: PropTypes.string.isRequired,
|
||||
isLoggedIn: PropTypes.bool.isRequired,
|
||||
|
@ -402,6 +398,7 @@ Push.propTypes = {
|
|||
getGeckoDecisionTaskId: PropTypes.func.isRequired,
|
||||
duplicateJobsVisible: PropTypes.bool.isRequired,
|
||||
groupCountsExpanded: PropTypes.bool.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withPushes(withSelectedJob(Push));
|
||||
export default withNotifications(withPushes(withSelectedJob(Push)));
|
||||
|
|
|
@ -5,14 +5,11 @@ import { formatTaskclusterError } from '../../helpers/errorMessage';
|
|||
import CustomJobActions from '../CustomJobActions';
|
||||
import PushModel from '../../models/push';
|
||||
import { withPushes } from '../context/Pushes';
|
||||
import { withNotifications } from '../../shared/context/Notifications';
|
||||
|
||||
class PushActionMenu extends React.PureComponent {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { $injector } = this.props;
|
||||
|
||||
this.thNotify = $injector.get('thNotify');
|
||||
|
||||
this.revision = this.props.revision;
|
||||
this.pushId = this.props.pushId;
|
||||
|
@ -56,7 +53,7 @@ class PushActionMenu extends React.PureComponent {
|
|||
}
|
||||
|
||||
triggerMissingJobs() {
|
||||
const { getGeckoDecisionTaskId } = this.props;
|
||||
const { getGeckoDecisionTaskId, notify } = this.props;
|
||||
|
||||
if (!window.confirm(`This will trigger all missing jobs for revision ${this.revision}!\n\nClick "OK" if you want to proceed.`)) {
|
||||
return;
|
||||
|
@ -66,17 +63,17 @@ class PushActionMenu extends React.PureComponent {
|
|||
.then((decisionTaskID) => {
|
||||
PushModel.triggerMissingJobs(decisionTaskID)
|
||||
.then((msg) => {
|
||||
this.thNotify.send(msg, 'success');
|
||||
notify(msg, 'success');
|
||||
}).catch((e) => {
|
||||
this.thNotify.send(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
notify(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
});
|
||||
}).catch((e) => {
|
||||
this.thNotify.send(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
notify(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
});
|
||||
}
|
||||
|
||||
triggerAllTalosJobs() {
|
||||
const { getGeckoDecisionTaskId } = this.props;
|
||||
const { getGeckoDecisionTaskId, notify } = this.props;
|
||||
|
||||
if (!window.confirm(`This will trigger all Talos jobs for revision ${this.revision}!\n\nClick "OK" if you want to proceed.`)) {
|
||||
return;
|
||||
|
@ -91,12 +88,12 @@ class PushActionMenu extends React.PureComponent {
|
|||
.then((decisionTaskID) => {
|
||||
PushModel.triggerAllTalosJobs(times, decisionTaskID)
|
||||
.then((msg) => {
|
||||
this.thNotify.send(msg, 'success');
|
||||
notify(msg, 'success');
|
||||
}).catch((e) => {
|
||||
this.thNotify.send(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
notify(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
});
|
||||
}).catch((e) => {
|
||||
this.thNotify.send(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
notify(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -175,7 +172,6 @@ class PushActionMenu extends React.PureComponent {
|
|||
job={null}
|
||||
pushId={pushId}
|
||||
isLoggedIn={isLoggedIn}
|
||||
notify={this.thNotify}
|
||||
toggle={this.toggleCustomJobActions}
|
||||
/>}
|
||||
</span>
|
||||
|
@ -192,7 +188,7 @@ PushActionMenu.propTypes = {
|
|||
hideRunnableJobs: PropTypes.func.isRequired,
|
||||
showRunnableJobs: PropTypes.func.isRequired,
|
||||
getGeckoDecisionTaskId: PropTypes.func.isRequired,
|
||||
$injector: PropTypes.object.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withPushes(PushActionMenu);
|
||||
export default withNotifications(withPushes(PushActionMenu));
|
||||
|
|
|
@ -9,6 +9,7 @@ import JobModel from '../../models/job';
|
|||
import { withPinnedJobs } from '../context/PinnedJobs';
|
||||
import { withSelectedJob } from '../context/SelectedJob';
|
||||
import { withPushes } from '../context/Pushes';
|
||||
import { withNotifications } from '../../shared/context/Notifications';
|
||||
|
||||
// url params we don't want added from the current querystring to the revision
|
||||
// and author links.
|
||||
|
@ -63,10 +64,7 @@ PushCounts.propTypes = {
|
|||
class PushHeader extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { $injector, pushTimestamp } = this.props;
|
||||
|
||||
this.$rootScope = $injector.get('$rootScope');
|
||||
this.thNotify = $injector.get('thNotify');
|
||||
const { pushTimestamp } = this.props;
|
||||
|
||||
this.pushDateStr = toDateStr(pushTimestamp);
|
||||
}
|
||||
|
@ -90,7 +88,10 @@ class PushHeader extends React.PureComponent {
|
|||
}
|
||||
|
||||
triggerNewJobs() {
|
||||
const { isLoggedIn, pushId, getGeckoDecisionTaskId, selectedRunnableJobs, hideRunnableJobs } = this.props;
|
||||
const {
|
||||
isLoggedIn, pushId, getGeckoDecisionTaskId, selectedRunnableJobs,
|
||||
hideRunnableJobs, notify,
|
||||
} = this.props;
|
||||
|
||||
if (!window.confirm(
|
||||
'This will trigger all selected jobs. Click "OK" if you want to proceed.')) {
|
||||
|
@ -101,21 +102,23 @@ class PushHeader extends React.PureComponent {
|
|||
getGeckoDecisionTaskId(pushId)
|
||||
.then((decisionTaskID) => {
|
||||
PushModel.triggerNewJobs(builderNames, decisionTaskID).then((result) => {
|
||||
this.thNotify.send(result, 'success');
|
||||
notify(result, 'success');
|
||||
hideRunnableJobs(pushId);
|
||||
this.props.hideRunnableJobs();
|
||||
}).catch((e) => {
|
||||
this.thNotify.send(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
notify(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
});
|
||||
}).catch((e) => {
|
||||
this.thNotify.send(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
notify(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
});
|
||||
} else {
|
||||
this.thNotify.send('Must be logged in to trigger a job', 'danger');
|
||||
notify('Must be logged in to trigger a job', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
cancelAllJobs() {
|
||||
const { notify, repoName } = this.props;
|
||||
|
||||
if (window.confirm('This will cancel all pending and running jobs for this push. It cannot be undone! Are you sure?')) {
|
||||
const { push, isLoggedIn, getGeckoDecisionTaskId } = this.props;
|
||||
// Any job Id inside the push will do
|
||||
|
@ -123,12 +126,7 @@ class PushHeader extends React.PureComponent {
|
|||
|
||||
if (!isLoggedIn) return;
|
||||
|
||||
JobModel.cancelAll(
|
||||
jobId,
|
||||
this.$rootScope.repoName,
|
||||
getGeckoDecisionTaskId,
|
||||
this.thNotify,
|
||||
);
|
||||
JobModel.cancelAll(jobId, repoName, getGeckoDecisionTaskId, notify);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -148,7 +146,7 @@ class PushHeader extends React.PureComponent {
|
|||
|
||||
render() {
|
||||
const { repoName, isLoggedIn, pushId, jobCounts, author,
|
||||
revision, runnableVisible, $injector, watchState,
|
||||
revision, runnableVisible, watchState,
|
||||
showRunnableJobs, hideRunnableJobs, cycleWatchState,
|
||||
notificationSupported, selectedRunnableJobs } = this.props;
|
||||
const cancelJobsTitle = isLoggedIn ?
|
||||
|
@ -235,7 +233,6 @@ class PushHeader extends React.PureComponent {
|
|||
revision={revision}
|
||||
repoName={repoName}
|
||||
pushId={pushId}
|
||||
$injector={$injector}
|
||||
showRunnableJobs={showRunnableJobs}
|
||||
hideRunnableJobs={hideRunnableJobs}
|
||||
/>
|
||||
|
@ -253,7 +250,6 @@ PushHeader.propTypes = {
|
|||
author: PropTypes.string.isRequired,
|
||||
revision: PropTypes.string.isRequired,
|
||||
repoName: PropTypes.string.isRequired,
|
||||
$injector: PropTypes.object.isRequired,
|
||||
filterModel: PropTypes.object.isRequired,
|
||||
runnableVisible: PropTypes.bool.isRequired,
|
||||
showRunnableJobs: PropTypes.func.isRequired,
|
||||
|
@ -267,6 +263,7 @@ PushHeader.propTypes = {
|
|||
getAllShownJobs: PropTypes.func.isRequired,
|
||||
getGeckoDecisionTaskId: PropTypes.func.isRequired,
|
||||
selectedRunnableJobs: PropTypes.array.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
jobCounts: PropTypes.object,
|
||||
watchState: PropTypes.string,
|
||||
selectedJob: PropTypes.object,
|
||||
|
@ -278,4 +275,4 @@ PushHeader.defaultProps = {
|
|||
watchState: 'none',
|
||||
};
|
||||
|
||||
export default withPushes(withSelectedJob(withPinnedJobs(PushHeader)));
|
||||
export default withNotifications(withPushes(withSelectedJob(withPinnedJobs(PushHeader))));
|
||||
|
|
|
@ -138,7 +138,7 @@ class PushJobs extends React.Component {
|
|||
render() {
|
||||
const filteredPlatforms = this.state.filteredPlatforms || [];
|
||||
const {
|
||||
$injector, repoName, filterModel, pushGroupState, duplicateJobsVisible,
|
||||
repoName, filterModel, pushGroupState, duplicateJobsVisible,
|
||||
groupCountsExpanded,
|
||||
} = this.props;
|
||||
|
||||
|
@ -150,7 +150,6 @@ class PushJobs extends React.Component {
|
|||
<Platform
|
||||
platform={platform}
|
||||
repoName={repoName}
|
||||
$injector={$injector}
|
||||
key={platform.title}
|
||||
filterModel={filterModel}
|
||||
pushGroupState={pushGroupState}
|
||||
|
@ -173,7 +172,6 @@ PushJobs.propTypes = {
|
|||
repoName: PropTypes.string.isRequired,
|
||||
filterModel: PropTypes.object.isRequired,
|
||||
togglePinJob: PropTypes.func.isRequired,
|
||||
$injector: PropTypes.object.isRequired,
|
||||
setSelectedJob: PropTypes.func.isRequired,
|
||||
pushGroupState: PropTypes.string.isRequired,
|
||||
toggleSelectedRunnableJob: PropTypes.func.isRequired,
|
||||
|
|
|
@ -17,7 +17,7 @@ class PushList extends React.Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
$injector, user, repoName, revision, currentRepo, filterModel, pushList,
|
||||
user, repoName, revision, currentRepo, filterModel, pushList,
|
||||
loadingPushes, getNextPushes, jobsLoaded, duplicateJobsVisible,
|
||||
groupCountsExpanded,
|
||||
} = this.props;
|
||||
|
@ -39,7 +39,6 @@ class PushList extends React.Component {
|
|||
currentRepo={currentRepo}
|
||||
repoName={repoName}
|
||||
filterModel={filterModel}
|
||||
$injector={$injector}
|
||||
notificationSupported={notificationSupported}
|
||||
duplicateJobsVisible={duplicateJobsVisible}
|
||||
groupCountsExpanded={groupCountsExpanded}
|
||||
|
@ -78,7 +77,6 @@ class PushList extends React.Component {
|
|||
}
|
||||
|
||||
PushList.propTypes = {
|
||||
$injector: PropTypes.object.isRequired,
|
||||
repoName: PropTypes.string.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
filterModel: PropTypes.object.isRequired,
|
||||
|
|
|
@ -14,10 +14,10 @@ import {
|
|||
|
||||
logViewerApp.controller('LogviewerCtrl', [
|
||||
'$window', '$document', '$rootScope', '$scope',
|
||||
'$timeout', 'thNotify', 'dateFilter',
|
||||
'$timeout', 'dateFilter',
|
||||
function Logviewer(
|
||||
$window, $document, $rootScope, $scope,
|
||||
$timeout, thNotify, dateFilter) {
|
||||
$timeout, dateFilter) {
|
||||
|
||||
const query_string = getAllUrlParams();
|
||||
$scope.css = '';
|
||||
|
@ -209,7 +209,8 @@ logViewerApp.controller('LogviewerCtrl', [
|
|||
}).catch((error) => {
|
||||
$scope.loading = false;
|
||||
$scope.jobExists = false;
|
||||
thNotify.send(`${error}`, 'danger', { sticky: true });
|
||||
$scope.jobError = error.toString();
|
||||
$scope.$apply();
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -5,14 +5,9 @@ import { thTitleSuffixLimit, thDefaultRepo } from '../../helpers/constants';
|
|||
import { getUrlParam } from '../../helpers/location';
|
||||
|
||||
treeherderApp.controller('MainCtrl', [
|
||||
'$scope', '$rootScope', 'thNotify',
|
||||
'$scope', '$rootScope',
|
||||
function MainController(
|
||||
$scope, $rootScope, thNotify) {
|
||||
|
||||
if (window.navigator.userAgent.indexOf('Firefox/52') !== -1) {
|
||||
thNotify.send('Firefox ESR52 is not supported. Please update to ESR60 or ideally release/beta/nightly.',
|
||||
'danger', { sticky: true });
|
||||
}
|
||||
$scope, $rootScope) {
|
||||
|
||||
// set to the default repo if one not specified
|
||||
const repoName = getUrlParam('repo');
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
import treeherder from '../../treeherder';
|
||||
import thNotificationsBoxTemplate from '../../../partials/main/thNotificationsBox.html';
|
||||
|
||||
treeherder.directive('thNotificationBox', [
|
||||
'thNotify',
|
||||
function (thNotify) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: thNotificationsBoxTemplate,
|
||||
link: function (scope) {
|
||||
scope.notifier = thNotify;
|
||||
scope.alert_class_prefix = 'alert-';
|
||||
},
|
||||
};
|
||||
}]);
|
|
@ -1,95 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
import treeherder from '../treeherder';
|
||||
|
||||
/* Services */
|
||||
treeherder.factory('thNotify', [
|
||||
'$timeout',
|
||||
function ($timeout) {
|
||||
// a growl-like notification system
|
||||
|
||||
const thNotify = {
|
||||
// message queue
|
||||
notifications: [],
|
||||
|
||||
// Long-term storage for notifications
|
||||
storedNotifications: JSON.parse(localStorage.getItem('notifications') || '[]'),
|
||||
|
||||
// Callback for any updates. Listening to window for 'storage'
|
||||
// events won't work for this because those events are only fired
|
||||
// for storage events made in OTHER windows/tabs. Not the current
|
||||
// one. Default to dummy function.
|
||||
// TODO: We should be able to remove this once this service is
|
||||
// converted to a class for direct usage in ReactJS.
|
||||
changeCallback: () => {},
|
||||
|
||||
/*
|
||||
* send a message to the notification queue
|
||||
* @severity can be one of success|info|warning|danger
|
||||
* @opts is an object with up to three entries:
|
||||
* sticky -- Keeps notification visible until cleared if true
|
||||
* linkText -- Text to display as a link if exists
|
||||
* url -- Location the link should point to if exists
|
||||
*/
|
||||
send: function (message, severity, opts) {
|
||||
if (opts !== undefined && !_.isPlainObject(opts)) {
|
||||
throw new Error('Must pass an object as last argument to thNotify.send!');
|
||||
}
|
||||
opts = opts || {};
|
||||
severity = severity || 'info';
|
||||
|
||||
const maxNsNotifications = 5;
|
||||
const notification = {
|
||||
...opts,
|
||||
message,
|
||||
severity,
|
||||
created: Date.now(),
|
||||
};
|
||||
$timeout(thNotify.notifications.unshift(notification));
|
||||
thNotify.storedNotifications.unshift(notification);
|
||||
thNotify.storedNotifications.splice(40);
|
||||
localStorage.setItem('notifications', JSON.stringify(thNotify.storedNotifications));
|
||||
thNotify.changeCallback(thNotify.storedNotifications);
|
||||
|
||||
if (!opts.sticky) {
|
||||
if (thNotify.notifications.length > maxNsNotifications) {
|
||||
$timeout(thNotify.shift);
|
||||
return;
|
||||
}
|
||||
$timeout(thNotify.shift, 4000, true);
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Delete the first non-sticky element from the notifications queue
|
||||
*/
|
||||
shift: function () {
|
||||
for (let i = 0; i < thNotify.notifications.length; i++) {
|
||||
if (!thNotify.notifications[i].sticky) {
|
||||
thNotify.remove(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
/*
|
||||
* remove an arbitrary element from the notifications queue
|
||||
*/
|
||||
remove: function (index) {
|
||||
thNotify.notifications.splice(index, 1);
|
||||
},
|
||||
|
||||
/*
|
||||
* Clear the list of stored notifications
|
||||
*/
|
||||
clear: function () {
|
||||
thNotify.storedNotifications = [];
|
||||
localStorage.setItem('notifications', thNotify.storedNotifications);
|
||||
thNotify.changeCallback(thNotify.storedNotifications);
|
||||
},
|
||||
setChangeCallback: function (cb) {
|
||||
thNotify.changeCallback = cb;
|
||||
},
|
||||
};
|
||||
return thNotify;
|
||||
|
||||
}]);
|
|
@ -40,7 +40,7 @@
|
|||
</li>
|
||||
<li ng-if="!jobExists" class="alert-danger">
|
||||
<div>
|
||||
<span title="The job does not exist or has expired">Unavailable</span>
|
||||
<span title="The job does not exist or has expired">Unavailable: {{jobError}}</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
|
@ -128,7 +128,6 @@
|
|||
</div>
|
||||
|
||||
<th-log-viewer class="logview-container"></th-log-viewer>
|
||||
<th-notification-box></th-notification-box>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -97,13 +97,11 @@ export default class JobModel {
|
|||
return JobModel.getList(repoName, options, config);
|
||||
}
|
||||
|
||||
static async retrigger(jobIds, repoName, getGeckoDecisionTaskId, thNotify) {
|
||||
static async retrigger(jobIds, repoName, getGeckoDecisionTaskId, notify) {
|
||||
const jobTerm = jobIds.length > 1 ? 'jobs' : 'job';
|
||||
|
||||
try {
|
||||
thNotify.send(
|
||||
`Attempting to retrigger ${jobTerm} via actions.json`,
|
||||
'info');
|
||||
notify(`Attempting to retrigger ${jobTerm} via actions.json`, 'info');
|
||||
|
||||
/* eslint-disable no-await-in-loop */
|
||||
for (const id of jobIds) {
|
||||
|
@ -123,22 +121,19 @@ export default class JobModel {
|
|||
} catch (e) {
|
||||
// The full message is too large to fit in a Treeherder
|
||||
// notification box.
|
||||
thNotify.send(
|
||||
formatTaskclusterError(e),
|
||||
'danger',
|
||||
{ sticky: true });
|
||||
notify(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
}
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
|
||||
thNotify.send(`Request sent to retrigger ${jobTerm} via action.json`, 'success');
|
||||
notify(`Request sent to retrigger ${jobTerm} via action.json`, 'success');
|
||||
} catch (e) {
|
||||
thNotify.send(`Unable to retrigger ${jobTerm}`, 'danger', { sticky: true });
|
||||
notify(`Unable to retrigger ${jobTerm}`, 'danger', { sticky: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Any jobId inside the push will do
|
||||
static async cancelAll(jobId, repoName, getGeckoDecisionTaskId, thNotify) {
|
||||
static async cancelAll(jobId, repoName, getGeckoDecisionTaskId, notify) {
|
||||
const job = await JobModel.get(repoName, jobId);
|
||||
const decisionTaskId = await getGeckoDecisionTaskId(job.push_id);
|
||||
const results = await TaskclusterModel.load(decisionTaskId);
|
||||
|
@ -154,22 +149,17 @@ export default class JobModel {
|
|||
} catch (e) {
|
||||
// The full message is too large to fit in a Treeherder
|
||||
// notification box.
|
||||
thNotify.send(
|
||||
formatTaskclusterError(e),
|
||||
'danger',
|
||||
{ sticky: true });
|
||||
notify(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
}
|
||||
|
||||
thNotify.send('Request sent to cancel all jobs via action.json', 'success');
|
||||
notify('Request sent to cancel all jobs via action.json', 'success');
|
||||
}
|
||||
|
||||
static async cancel(jobIds, repoName, getGeckoDecisionTaskId, thNotify) {
|
||||
static async cancel(jobIds, repoName, getGeckoDecisionTaskId, notify) {
|
||||
const jobTerm = jobIds.length > 1 ? 'jobs' : 'job';
|
||||
|
||||
try {
|
||||
thNotify.send(
|
||||
`Attempting to cancel selected ${jobTerm} via actions.json`,
|
||||
'info');
|
||||
notify(`Attempting to cancel selected ${jobTerm} via actions.json`, 'info');
|
||||
|
||||
/* eslint-disable no-await-in-loop */
|
||||
for (const id of jobIds) {
|
||||
|
@ -189,17 +179,14 @@ export default class JobModel {
|
|||
} catch (e) {
|
||||
// The full message is too large to fit in a Treeherder
|
||||
// notification box.
|
||||
thNotify.send(
|
||||
formatTaskclusterError(e),
|
||||
'danger',
|
||||
{ sticky: true });
|
||||
notify(formatTaskclusterError(e), 'danger', { sticky: true });
|
||||
}
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
|
||||
thNotify.send(`Request sent to cancel ${jobTerm} via action.json`, 'success');
|
||||
notify(`Request sent to cancel ${jobTerm} via action.json`, 'success');
|
||||
} catch (e) {
|
||||
thNotify.send(`Unable to cancel ${jobTerm}`, 'danger', { sticky: true });
|
||||
notify(`Unable to cancel ${jobTerm}`, 'danger', { sticky: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
<ul id="notification-box" class="list-unstyled">
|
||||
<li ng-repeat="notification in notifier.notifications">
|
||||
<div ng-switch on="notification.severity" class="alert alert-{{::notification.severity}}">
|
||||
<span ng-switch-when="danger" class="fa fa-ban"></span>
|
||||
<span ng-switch-when="warning" class="fa fa-warning"></span>
|
||||
<span ng-switch-when="info" class="fa fa-info-circle"></span>
|
||||
<span ng-switch-when="success" class="fa fa-check"></span>
|
||||
<span ng-switch-default></span>
|
||||
{{::notification.message}}
|
||||
<span ng-if="notification.url && notification.linkText">
|
||||
<a href="{{::notification.url}}">{{::notification.linkText}}</a>
|
||||
</span>
|
||||
<button ng-click="notifier.remove($index)" ng-if="notification.sticky" class="close">x</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
|
@ -9,19 +9,17 @@ import { loggedOutUser } from '../js/auth/auth-utils';
|
|||
import taskcluster from '../helpers/taskcluster';
|
||||
import { getApiUrl, loginCallbackUrl } from '../helpers/url';
|
||||
import UserModel from '../models/user';
|
||||
import { withNotifications } from './context/Notifications';
|
||||
|
||||
/**
|
||||
* This component handles logging in to Taskcluster Authentication
|
||||
*
|
||||
* See: https://docs.taskcluster.net/manual/3rdparty
|
||||
*/
|
||||
export default class Login extends React.Component {
|
||||
class Login extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const { $injector } = props;
|
||||
this.thNotify = $injector.get('thNotify');
|
||||
|
||||
this.authService = new AuthService();
|
||||
}
|
||||
|
||||
|
@ -89,13 +87,15 @@ export default class Login extends React.Component {
|
|||
}
|
||||
|
||||
logout() {
|
||||
const { notify } = this.props;
|
||||
|
||||
fetch(getApiUrl('/auth/logout/'))
|
||||
.then(async (resp) => {
|
||||
if (resp.ok) {
|
||||
this.setLoggedOut();
|
||||
} else {
|
||||
const msg = await resp.json();
|
||||
this.thNotify.send(`Logout failed: ${msg}`, 'danger', { sticky: true });
|
||||
const msg = await resp.text();
|
||||
notify(`Logout failed: ${msg}`, 'danger', { sticky: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -140,12 +140,16 @@ export default class Login extends React.Component {
|
|||
|
||||
Login.propTypes = {
|
||||
setUser: PropTypes.func.isRequired,
|
||||
$injector: PropTypes.object.isRequired,
|
||||
user: PropTypes.object,
|
||||
notify: PropTypes.func,
|
||||
};
|
||||
|
||||
Login.defaultProps = {
|
||||
user: { isLoggedIn: false },
|
||||
notify: msg => console.error(msg), // eslint-disable-line no-console
|
||||
};
|
||||
|
||||
treeherder.component('login', react2angular(Login, ['user', 'setUser'], ['$injector']));
|
||||
export default withNotifications(Login);
|
||||
|
||||
treeherder.component('login', react2angular(
|
||||
withNotifications(Login), ['user', 'setUser'], []));
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { withNotifications } from './context/Notifications';
|
||||
|
||||
class NotificationList extends React.Component {
|
||||
static getSeverityClass(severity) {
|
||||
switch (severity) {
|
||||
case 'danger':
|
||||
return 'fa fa-ban';
|
||||
case 'warning':
|
||||
return 'fa fa-warning';
|
||||
case 'info':
|
||||
return 'fa fa-circle';
|
||||
case 'success':
|
||||
return 'fa fa-check';
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { notifications, removeNotification } = this.props;
|
||||
|
||||
return (
|
||||
<ul id="notification-box" className="list-unstyled">
|
||||
{notifications.map((notification, idx) => (
|
||||
<li key={notification.created}>
|
||||
<div className={`alert alert-${notification.severity}`}>
|
||||
<span
|
||||
className={NotificationList.getSeverityClass(notification.severity)}
|
||||
/>
|
||||
<span>{notification.message}</span>
|
||||
{notification.url && notification.linkText && <span>
|
||||
<a href={notification.url}>{notification.linkText}</a>
|
||||
</span>}
|
||||
{notification.sticky && <button
|
||||
onClick={() => removeNotification(idx)}
|
||||
className="close"
|
||||
>x</button>}
|
||||
</div>
|
||||
</li>))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NotificationList.propTypes = {
|
||||
notifications: PropTypes.array.isRequired,
|
||||
removeNotification: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withNotifications(NotificationList);
|
|
@ -0,0 +1,138 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const NotificationsContext = React.createContext({});
|
||||
|
||||
export class Notifications extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
notifications: [],
|
||||
storedNotifications: JSON.parse(localStorage.getItem('notifications') || '[]'),
|
||||
};
|
||||
this.value = {
|
||||
...this.state,
|
||||
notify: this.notify,
|
||||
removeNotification: this.removeNotification,
|
||||
clearStoredNotifications: this.clearStoredNotifications,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.notify = this.notify.bind(this);
|
||||
this.removeNotification = this.removeNotification.bind(this);
|
||||
this.shift = this.shift.bind(this);
|
||||
this.clearStoredNotifications = this.clearStoredNotifications.bind(this);
|
||||
|
||||
this.unlistenStorage = window.addEventListener('storage', (e) => {
|
||||
if (e.key === 'notifications') {
|
||||
this.setValue({
|
||||
storedNotifications: JSON.parse(localStorage.getItem('notifications') || '[]'),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.value = {
|
||||
...this.state,
|
||||
notify: this.notify,
|
||||
removeNotification: this.removeNotification,
|
||||
clearStoredNotifications: this.clearStoredNotifications,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unlistenStorage();
|
||||
}
|
||||
|
||||
setValue(newState, callback) {
|
||||
this.value = { ...this.value, ...newState };
|
||||
this.setState(newState, callback);
|
||||
}
|
||||
|
||||
notify(message, severity, opts) {
|
||||
opts = opts || {};
|
||||
severity = severity || 'info';
|
||||
const { notifications, storedNotifications } = this.state;
|
||||
const maxNsNotifications = 5;
|
||||
const notification = { ...opts, message, severity, created: Date.now() };
|
||||
const newNotifications = [notification, ...notifications];
|
||||
|
||||
storedNotifications.unshift(notification);
|
||||
storedNotifications.splice(40);
|
||||
localStorage.setItem('notifications', JSON.stringify(storedNotifications));
|
||||
|
||||
this.setValue(
|
||||
{ notifications: newNotifications, storedNotifications: [...storedNotifications] },
|
||||
() => {
|
||||
if (!opts.sticky) {
|
||||
if (notifications.length > maxNsNotifications) {
|
||||
this.shift();
|
||||
return;
|
||||
}
|
||||
setTimeout(this.shift, 4000, true);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* remove an arbitrary element from the notifications queue
|
||||
*/
|
||||
removeNotification(index, delay = 0) {
|
||||
const { notifications } = this.state;
|
||||
|
||||
notifications.splice(index, 1);
|
||||
setTimeout(() => this.setValue({ notifications: [...notifications] }), delay);
|
||||
}
|
||||
|
||||
/*
|
||||
* Delete the first non-sticky element from the notifications queue
|
||||
*/
|
||||
shift(delay) {
|
||||
const { notifications } = this.state;
|
||||
|
||||
this.removeNotification(notifications.findIndex(n => !n.sticky), delay);
|
||||
}
|
||||
|
||||
/*
|
||||
* Clear the list of stored notifications
|
||||
*/
|
||||
clearStoredNotifications() {
|
||||
const storedNotifications = [];
|
||||
|
||||
localStorage.setItem('notifications', storedNotifications);
|
||||
this.setValue({ storedNotifications });
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<NotificationsContext.Provider value={this.value}>
|
||||
{this.props.children}
|
||||
</NotificationsContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Notifications.propTypes = {
|
||||
children: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export function withNotifications(Component) {
|
||||
return function NotificationComponent(props) {
|
||||
return (
|
||||
<NotificationsContext.Consumer>
|
||||
{context => (
|
||||
<Component
|
||||
{...props}
|
||||
notifications={context.notifications}
|
||||
storedNotifications={context.storedNotifications}
|
||||
notify={context.notify}
|
||||
removeNotification={context.removeNotification}
|
||||
clearStoredNotifications={context.clearStoredNotifications}
|
||||
/>
|
||||
)}
|
||||
</NotificationsContext.Consumer>
|
||||
);
|
||||
};
|
||||
}
|
Загрузка…
Ссылка в новой задаче