Bug 1450024 - Convert Notifications to ReactJS (#4132)

This commit is contained in:
Cameron Dawson 2018-10-12 17:16:20 -07:00 коммит произвёл GitHub
Родитель fd20a61c8e
Коммит 49368ca7ff
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
40 изменённых файлов: 466 добавлений и 489 удалений

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

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