зеркало из https://github.com/mozilla/treeherder.git
Bug 1510280 - Convert Notifications to Redux (#5030)
This introduces Redux back into Treeherder. While redux is often considered unnecessary for simple apps in favor of Context, Treeherder is not simple. Using Redux here allows us to abstract out certain functionality into libraries that can be self-contained. And it does a better job of managing the functions needed to modify the state in these areas. This is the first context I am converting and will likely have minimal (if any) noticeable performance gain. But converting SelectedJob and Pusehs should end up having a larger impact.
This commit is contained in:
Родитель
fd47449017
Коммит
0ddd5a80e1
|
@ -60,7 +60,7 @@
|
|||
"react-hotkeys": "1.1.4",
|
||||
"react-lazylog": "3.2.1",
|
||||
"react-linkify": "0.2.2",
|
||||
"react-redux": "6.0.1",
|
||||
"react-redux": "7.0.3",
|
||||
"react-router-dom": "5.0.0",
|
||||
"react-select": "2.4.3",
|
||||
"react-split-pane": "0.1.87",
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
export const MAX_TRANSIENT_AGE = 4000;
|
||||
|
||||
/*
|
||||
* Clear any expired transient notifications
|
||||
*/
|
||||
export const clearExpiredTransientNotifications = notifications => {
|
||||
const cleanedNotifications = notifications.reduce((acc, note) => {
|
||||
return note.sticky || Date.now() - note.created < MAX_TRANSIENT_AGE
|
||||
? [...acc, note]
|
||||
: acc;
|
||||
}, []);
|
||||
|
||||
return cleanedNotifications.length !== notifications.length
|
||||
? { notifications: cleanedNotifications }
|
||||
: { notifications };
|
||||
};
|
||||
|
||||
export const clearNotificationAtIndex = (notifications, index) => {
|
||||
notifications.splice(index, 1);
|
||||
|
||||
return { notifications: [...notifications] };
|
||||
};
|
|
@ -3,18 +3,19 @@ import { hot } from 'react-hot-loader/root';
|
|||
import SplitPane from 'react-split-pane';
|
||||
import pick from 'lodash/pick';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { thFavicons } from '../helpers/constants';
|
||||
import { Notifications } from '../shared/context/Notifications';
|
||||
import NotificationList from '../shared/NotificationList';
|
||||
import ShortcutTable from '../shared/ShortcutTable';
|
||||
import { allFilterParams, matchesDefaults } from '../helpers/filter';
|
||||
import { getAllUrlParams, getRepo } from '../helpers/location';
|
||||
import { MAX_TRANSIENT_AGE } from '../helpers/notifications';
|
||||
import { deployedRevisionUrl, parseQueryParams } from '../helpers/url';
|
||||
import ClassificationTypeModel from '../models/classificationType';
|
||||
import FilterModel from '../models/filter';
|
||||
import RepositoryModel from '../models/repository';
|
||||
|
||||
import Notifications from './Notifications';
|
||||
import { Pushes } from './context/Pushes';
|
||||
import { SelectedJob } from './context/SelectedJob';
|
||||
import { PinnedJobs } from './context/PinnedJobs';
|
||||
|
@ -25,6 +26,8 @@ import { PUSH_HEALTH_VISIBILITY } from './headerbars/HealthMenu';
|
|||
import DetailsPanel from './details/DetailsPanel';
|
||||
import PushList from './pushes/PushList';
|
||||
import KeyboardShortcuts from './KeyboardShortcuts';
|
||||
import { store } from './redux/store';
|
||||
import { CLEAR_EXPIRED_TRANSIENTS } from './redux/stores/notifications';
|
||||
|
||||
const DEFAULT_DETAILS_PCT = 40;
|
||||
const REVISION_POLL_INTERVAL = 1000 * 60 * 5;
|
||||
|
@ -140,6 +143,11 @@ class App extends React.Component {
|
|||
});
|
||||
}, REVISION_POLL_INTERVAL);
|
||||
});
|
||||
|
||||
// clear expired notifications
|
||||
this.notificationInterval = setInterval(() => {
|
||||
store.dispatch({ type: CLEAR_EXPIRED_TRANSIENTS });
|
||||
}, MAX_TRANSIENT_AGE);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -300,7 +308,7 @@ class App extends React.Component {
|
|||
|
||||
return (
|
||||
<div id="global-container" className="height-minus-navbars">
|
||||
<Notifications>
|
||||
<Provider store={store}>
|
||||
<Pushes filterModel={filterModel}>
|
||||
<PinnedJobs>
|
||||
<SelectedJob>
|
||||
|
@ -369,7 +377,7 @@ class App extends React.Component {
|
|||
classificationMap={classificationMap}
|
||||
/>
|
||||
</SplitPane>
|
||||
<NotificationList />
|
||||
<Notifications />
|
||||
{showShortCuts && (
|
||||
<div
|
||||
id="onscreen-overlay"
|
||||
|
@ -386,7 +394,7 @@ class App extends React.Component {
|
|||
</SelectedJob>
|
||||
</PinnedJobs>
|
||||
</Pushes>
|
||||
</Notifications>
|
||||
</Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import Select from 'react-select';
|
||||
import PropTypes from 'prop-types';
|
||||
import Ajv from 'ajv';
|
||||
|
@ -22,9 +23,9 @@ import { faCheckSquare } from '@fortawesome/free-regular-svg-icons';
|
|||
|
||||
import { formatTaskclusterError } from '../helpers/errorMessage';
|
||||
import TaskclusterModel from '../models/taskcluster';
|
||||
import { withNotifications } from '../shared/context/Notifications';
|
||||
|
||||
import { withPushes } from './context/Pushes';
|
||||
import { notify } from './redux/stores/notifications';
|
||||
|
||||
class CustomJobActions extends React.PureComponent {
|
||||
constructor(props) {
|
||||
|
@ -302,4 +303,7 @@ CustomJobActions.defaultProps = {
|
|||
job: null,
|
||||
};
|
||||
|
||||
export default withNotifications(withPushes(CustomJobActions));
|
||||
export default connect(
|
||||
null,
|
||||
{ notify },
|
||||
)(withPushes(CustomJobActions));
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { thEvents } from '../helpers/constants';
|
||||
import { withNotifications } from '../shared/context/Notifications';
|
||||
|
||||
import { withPinnedJobs } from './context/PinnedJobs';
|
||||
import { withSelectedJob } from './context/SelectedJob';
|
||||
import { clearAllOnScreenNotifications } from './redux/stores/notifications';
|
||||
|
||||
const keyMap = {
|
||||
addRelatedBug: 'b',
|
||||
|
@ -49,11 +50,11 @@ class KeyboardShortcuts extends React.Component {
|
|||
clearSelectedJob,
|
||||
showOnScreenShortcuts,
|
||||
notifications,
|
||||
clearOnScreenNotifications,
|
||||
clearAllOnScreenNotifications,
|
||||
} = this.props;
|
||||
|
||||
if (notifications.length) {
|
||||
clearOnScreenNotifications();
|
||||
clearAllOnScreenNotifications();
|
||||
} else {
|
||||
clearSelectedJob();
|
||||
showOnScreenShortcuts(false);
|
||||
|
@ -243,7 +244,7 @@ KeyboardShortcuts.propTypes = {
|
|||
sticky: PropTypes.bool,
|
||||
}),
|
||||
).isRequired,
|
||||
clearOnScreenNotifications: PropTypes.func.isRequired,
|
||||
clearAllOnScreenNotifications: PropTypes.func.isRequired,
|
||||
selectedJob: PropTypes.object,
|
||||
};
|
||||
|
||||
|
@ -251,6 +252,11 @@ KeyboardShortcuts.defaultProps = {
|
|||
selectedJob: null,
|
||||
};
|
||||
|
||||
export default withPinnedJobs(
|
||||
withSelectedJob(withNotifications(KeyboardShortcuts)),
|
||||
);
|
||||
const mapStateToProps = ({ notifications: { notifications } }) => ({
|
||||
notifications,
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{ clearAllOnScreenNotifications },
|
||||
)(withPinnedJobs(withSelectedJob(KeyboardShortcuts)));
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import NotificationList from '../shared/NotificationList';
|
||||
|
||||
import { clearNotification } from './redux/stores/notifications';
|
||||
|
||||
const Notifications = props => {
|
||||
const { notifications, clearNotification } = props;
|
||||
|
||||
return (
|
||||
<NotificationList
|
||||
notifications={notifications}
|
||||
clearNotification={clearNotification}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Notifications.propTypes = {
|
||||
notifications: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
created: PropTypes.number.isRequired,
|
||||
message: PropTypes.string.isRequired,
|
||||
severity: PropTypes.oneOf(['danger', 'warning', 'info', 'success']),
|
||||
sticky: PropTypes.bool,
|
||||
}),
|
||||
).isRequired,
|
||||
clearNotification: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = ({ notifications: { notifications } }) => ({
|
||||
notifications,
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{ clearNotification },
|
||||
)(Notifications);
|
|
@ -1,7 +1,8 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { withNotifications } from '../../shared/context/Notifications';
|
||||
import { notify } from '../redux/stores/notifications';
|
||||
|
||||
const COUNT_ERROR = 'Max pinboard size of 500 reached.';
|
||||
const MAX_SIZE = 500;
|
||||
|
@ -165,7 +166,10 @@ PinnedJobsClass.propTypes = {
|
|||
children: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export const PinnedJobs = withNotifications(PinnedJobsClass);
|
||||
export const PinnedJobs = connect(
|
||||
null,
|
||||
{ notify },
|
||||
)(PinnedJobsClass);
|
||||
|
||||
export function withPinnedJobs(Component) {
|
||||
return function PinBoardComponent(props) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import pick from 'lodash/pick';
|
||||
import keyBy from 'lodash/keyBy';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
@ -22,7 +23,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';
|
||||
import { notify } from '../redux/stores/notifications';
|
||||
|
||||
const PushesContext = React.createContext({});
|
||||
const defaultPushCount = 10;
|
||||
|
@ -414,7 +415,10 @@ PushesClass.propTypes = {
|
|||
notify: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const Pushes = withNotifications(PushesClass);
|
||||
export const Pushes = connect(
|
||||
null,
|
||||
{ notify },
|
||||
)(PushesClass);
|
||||
|
||||
export function withPushes(Component) {
|
||||
return function PushesComponent(props) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import intersection from 'lodash/intersection';
|
||||
import $ from 'jquery';
|
||||
|
||||
|
@ -15,7 +16,7 @@ import { getUrlParam, setUrlParam } from '../../helpers/location';
|
|||
import { getJobsUrl } from '../../helpers/url';
|
||||
import JobModel from '../../models/job';
|
||||
import PushModel from '../../models/push';
|
||||
import { withNotifications } from '../../shared/context/Notifications';
|
||||
import { notify } from '../redux/stores/notifications';
|
||||
|
||||
import { withPinnedJobs } from './PinnedJobs';
|
||||
import { withPushes } from './Pushes';
|
||||
|
@ -287,9 +288,10 @@ SelectedJobClass.propTypes = {
|
|||
children: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export const SelectedJob = withNotifications(
|
||||
withPushes(withPinnedJobs(SelectedJobClass)),
|
||||
);
|
||||
export const SelectedJob = connect(
|
||||
null,
|
||||
{ notify },
|
||||
)(withPushes(withPinnedJobs(SelectedJobClass)));
|
||||
|
||||
export function withSelectedJob(Component) {
|
||||
return function SelectedJobComponent(props) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
|
@ -27,7 +28,7 @@ import {
|
|||
hgBaseUrl,
|
||||
} from '../../helpers/url';
|
||||
import { create } from '../../helpers/http';
|
||||
import { withNotifications } from '../../shared/context/Notifications';
|
||||
import { notify } from '../redux/stores/notifications';
|
||||
|
||||
const crashRegex = /application crashed \[@ (.+)\]$/g;
|
||||
const omittedLeads = [
|
||||
|
@ -862,4 +863,7 @@ BugFilerClass.propTypes = {
|
|||
notify: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withNotifications(BugFilerClass);
|
||||
export default connect(
|
||||
null,
|
||||
{ notify },
|
||||
)(BugFilerClass);
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button, FormGroup, Input, FormFeedback } from 'reactstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faPlusSquare, faTimes } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { thEvents } from '../../helpers/constants';
|
||||
import { withNotifications } from '../../shared/context/Notifications';
|
||||
import { formatModelError } from '../../helpers/errorMessage';
|
||||
import {
|
||||
getJobBtnClass,
|
||||
|
@ -20,6 +20,7 @@ import JobModel from '../../models/job';
|
|||
import { withPinnedJobs } from '../context/PinnedJobs';
|
||||
import { withSelectedJob } from '../context/SelectedJob';
|
||||
import { withPushes } from '../context/Pushes';
|
||||
import { notify } from '../redux/stores/notifications';
|
||||
|
||||
class PinBoard extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -657,6 +658,7 @@ PinBoard.defaultProps = {
|
|||
revisionTips: [],
|
||||
};
|
||||
|
||||
export default withNotifications(
|
||||
withPushes(withSelectedJob(withPinnedJobs(PinBoard))),
|
||||
);
|
||||
export default connect(
|
||||
null,
|
||||
{ notify },
|
||||
)(withPushes(withSelectedJob(withPinnedJobs(PinBoard))));
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button } from 'reactstrap';
|
||||
import $ from 'jquery';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
@ -21,7 +22,7 @@ import CustomJobActions from '../../CustomJobActions';
|
|||
import { withSelectedJob } from '../../context/SelectedJob';
|
||||
import { withPinnedJobs } from '../../context/PinnedJobs';
|
||||
import { withPushes } from '../../context/Pushes';
|
||||
import { withNotifications } from '../../../shared/context/Notifications';
|
||||
import { notify } from '../../redux/stores/notifications';
|
||||
|
||||
import LogUrls from './LogUrls';
|
||||
|
||||
|
@ -476,6 +477,7 @@ ActionBar.defaultProps = {
|
|||
jobLogUrls: [],
|
||||
};
|
||||
|
||||
export default withNotifications(
|
||||
withPushes(withSelectedJob(withPinnedJobs(ActionBar))),
|
||||
);
|
||||
export default connect(
|
||||
null,
|
||||
{ notify },
|
||||
)(withPushes(withSelectedJob(withPinnedJobs(ActionBar))));
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faStar as faStarRegular } from '@fortawesome/free-regular-svg-icons';
|
||||
import {
|
||||
|
@ -11,8 +12,8 @@ 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';
|
||||
import { longDateFormat } from '../../../helpers/display';
|
||||
import { notify } from '../../redux/stores/notifications';
|
||||
|
||||
function RelatedBugSaved(props) {
|
||||
const { deleteBug, bug } = props;
|
||||
|
@ -254,4 +255,7 @@ AnnotationsTab.defaultProps = {
|
|||
selectedJob: null,
|
||||
};
|
||||
|
||||
export default withNotifications(withPushes(withSelectedJob(AnnotationsTab)));
|
||||
export default connect(
|
||||
null,
|
||||
{ notify },
|
||||
)(withPushes(withSelectedJob(AnnotationsTab)));
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
|
@ -11,7 +12,7 @@ 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';
|
||||
import { notify } from '../../redux/stores/notifications';
|
||||
|
||||
class SimilarJobsTab extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -334,4 +335,7 @@ SimilarJobsTab.defaultProps = {
|
|||
selectedJob: null,
|
||||
};
|
||||
|
||||
export default withNotifications(withSelectedJob(SimilarJobsTab));
|
||||
export default connect(
|
||||
null,
|
||||
{ notify },
|
||||
)(withSelectedJob(SimilarJobsTab));
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import $ from 'jquery';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
|
@ -9,7 +10,7 @@ import { getProjectJobUrl } from '../../../../helpers/location';
|
|||
import TextLogErrorsModel from '../../../../models/textLogErrors';
|
||||
import { withSelectedJob } from '../../../context/SelectedJob';
|
||||
import { withPinnedJobs } from '../../../context/PinnedJobs';
|
||||
import { withNotifications } from '../../../../shared/context/Notifications';
|
||||
import { notify } from '../../../redux/stores/notifications';
|
||||
|
||||
import AutoclassifyToolbar from './AutoclassifyToolbar';
|
||||
import ErrorLine from './ErrorLine';
|
||||
|
@ -371,6 +372,7 @@ AutoclassifyTab.propTypes = {
|
|||
repoName: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default withNotifications(
|
||||
withSelectedJob(withPinnedJobs(AutoclassifyTab)),
|
||||
);
|
||||
export default connect(
|
||||
null,
|
||||
{ notify },
|
||||
)(withSelectedJob(withPinnedJobs(AutoclassifyTab)));
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faBell } from '@fortawesome/free-regular-svg-icons';
|
||||
import {
|
||||
|
@ -10,7 +11,7 @@ import {
|
|||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { shortDateFormat } from '../../helpers/display';
|
||||
import { withNotifications } from '../../shared/context/Notifications';
|
||||
import { clearStoredNotifications } from '../redux/stores/notifications';
|
||||
|
||||
class NotificationsMenu extends React.Component {
|
||||
getIcon(severity) {
|
||||
|
@ -117,4 +118,11 @@ NotificationsMenu.propTypes = {
|
|||
clearStoredNotifications: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withNotifications(NotificationsMenu);
|
||||
const mapStateToProps = ({ notifications: { storedNotifications } }) => ({
|
||||
storedNotifications,
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{ clearStoredNotifications },
|
||||
)(NotificationsMenu);
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
import Logo from '../../img/treeherder-logo.png';
|
||||
import Login from '../../shared/auth/Login';
|
||||
import LogoMenu from '../../shared/LogoMenu';
|
||||
import { notify } from '../redux/stores/notifications';
|
||||
|
||||
import NotificationsMenu from './NotificationsMenu';
|
||||
import InfraMenu from './InfraMenu';
|
||||
|
@ -15,7 +17,7 @@ import HelpMenu from './HelpMenu';
|
|||
import SecondaryNavBar from './SecondaryNavBar';
|
||||
import HealthMenu from './HealthMenu';
|
||||
|
||||
export default class PrimaryNavBar extends React.Component {
|
||||
class PrimaryNavBar extends React.Component {
|
||||
shouldComponentUpdate(prevProps) {
|
||||
const {
|
||||
filterModel,
|
||||
|
@ -52,6 +54,7 @@ export default class PrimaryNavBar extends React.Component {
|
|||
toggleFieldFilterVisible,
|
||||
pushHealthVisibility,
|
||||
setPushHealthVisibility,
|
||||
notify,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
|
@ -71,7 +74,7 @@ export default class PrimaryNavBar extends React.Component {
|
|||
setPushHealthVisibility={setPushHealthVisibility}
|
||||
/>
|
||||
<HelpMenu />
|
||||
<Login user={user} setUser={setUser} />
|
||||
<Login user={user} setUser={setUser} notify={notify} />
|
||||
</span>
|
||||
</div>
|
||||
<SecondaryNavBar
|
||||
|
@ -104,4 +107,10 @@ PrimaryNavBar.propTypes = {
|
|||
groupCountsExpanded: PropTypes.bool.isRequired,
|
||||
pushHealthVisibility: PropTypes.string.isRequired,
|
||||
setPushHealthVisibility: PropTypes.func.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
{ notify },
|
||||
)(PrimaryNavBar);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
|
@ -15,9 +16,9 @@ import {
|
|||
import Fuse from 'fuse.js';
|
||||
|
||||
import PushModel from '../../models/push';
|
||||
import { withNotifications } from '../../shared/context/Notifications';
|
||||
import { formatTaskclusterError } from '../../helpers/errorMessage';
|
||||
import { sortAlphaNum } from '../../helpers/sort';
|
||||
import { notify } from '../redux/stores/notifications';
|
||||
|
||||
class FuzzyJobFinder extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -321,4 +322,7 @@ FuzzyJobFinder.defaultProps = {
|
|||
decisionTaskId: '',
|
||||
};
|
||||
|
||||
export default withNotifications(FuzzyJobFinder);
|
||||
export default connect(
|
||||
null,
|
||||
{ notify },
|
||||
)(FuzzyJobFinder);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
|
||||
import {
|
||||
|
@ -12,9 +13,9 @@ import { getGroupMapKey } from '../../helpers/aggregateId';
|
|||
import { getAllUrlParams, getUrlParam } from '../../helpers/location';
|
||||
import JobModel from '../../models/job';
|
||||
import RunnableJobModel from '../../models/runnableJob';
|
||||
import { withNotifications } from '../../shared/context/Notifications';
|
||||
import { getRevisionTitle } from '../../helpers/revision';
|
||||
import { getPercentComplete } from '../../helpers/display';
|
||||
import { notify } from '../redux/stores/notifications';
|
||||
|
||||
import FuzzyJobFinder from './FuzzyJobFinder';
|
||||
import { Revision } from './Revision';
|
||||
|
@ -549,4 +550,7 @@ Push.propTypes = {
|
|||
pushHealthVisibility: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default withNotifications(withPushes(Push));
|
||||
export default connect(
|
||||
null,
|
||||
{ notify },
|
||||
)(withPushes(Push));
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getUrlParam } from '../../helpers/location';
|
||||
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';
|
||||
import { getPushHealthUrl } from '../../helpers/url';
|
||||
import { notify } from '../redux/stores/notifications';
|
||||
|
||||
// Trigger missing jobs is dangerous on repos other than these (see bug 1335506)
|
||||
const triggerMissingRepos = ['mozilla-inbound', 'autoland'];
|
||||
|
@ -284,4 +285,7 @@ PushActionMenu.propTypes = {
|
|||
notify: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withNotifications(withPushes(PushActionMenu));
|
||||
export default connect(
|
||||
null,
|
||||
{ notify },
|
||||
)(withPushes(PushActionMenu));
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
|
@ -20,9 +21,9 @@ 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';
|
||||
import PushHealthStatus from '../../shared/PushHealthStatus';
|
||||
import { getUrlParam, setUrlParam } from '../../helpers/location';
|
||||
import { notify } from '../redux/stores/notifications';
|
||||
|
||||
import PushActionMenu from './PushActionMenu';
|
||||
|
||||
|
@ -407,6 +408,7 @@ PushHeader.defaultProps = {
|
|||
watchState: 'none',
|
||||
};
|
||||
|
||||
export default withNotifications(
|
||||
withPushes(withSelectedJob(withPinnedJobs(PushHeader))),
|
||||
);
|
||||
export default connect(
|
||||
null,
|
||||
{ notify },
|
||||
)(withPushes(withSelectedJob(withPinnedJobs(PushHeader))));
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import { createStore, combineReducers } from 'redux';
|
||||
|
||||
import * as notificationStore from './stores/notifications';
|
||||
|
||||
export default () => {
|
||||
const reducers = combineReducers({
|
||||
notifications: notificationStore.reducer,
|
||||
});
|
||||
const store = createStore(reducers);
|
||||
|
||||
return { store };
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
import configureStore from './configureStore';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const { store } = configureStore();
|
|
@ -0,0 +1,103 @@
|
|||
import {
|
||||
clearNotificationAtIndex,
|
||||
clearExpiredTransientNotifications,
|
||||
} from '../../../helpers/notifications';
|
||||
|
||||
const MAX_STORED_NOTIFICATIONS = 40;
|
||||
const LOCAL_STORAGE_KEY = 'notifications';
|
||||
|
||||
// *** Event types ***
|
||||
export const NOTIFY = 'NOTIFY';
|
||||
export const CLEAR = 'CLEAR';
|
||||
export const CLEAR_EXPIRED_TRANSIENTS = 'CLEAR_EXPIRED_TRANSIENTS';
|
||||
export const CLEAR_ALL_ON_SCREEN = 'CLEAR_ALL_ON_SCREEN';
|
||||
export const CLEAR_STORED = 'CLEAR_STORED';
|
||||
|
||||
// *** Action creators ***
|
||||
export const clearAllOnScreenNotifications = () => ({
|
||||
type: CLEAR_ALL_ON_SCREEN,
|
||||
});
|
||||
|
||||
export const clearNotification = index => ({
|
||||
type: CLEAR,
|
||||
index,
|
||||
});
|
||||
|
||||
export const clearStoredNotifications = () => ({
|
||||
type: CLEAR_STORED,
|
||||
});
|
||||
|
||||
export const notify = (message, severity, options) => ({
|
||||
type: NOTIFY,
|
||||
message,
|
||||
severity,
|
||||
options,
|
||||
});
|
||||
|
||||
// *** Implementation ***
|
||||
const doNotify = (
|
||||
{ notifications, storedNotifications },
|
||||
message,
|
||||
severity = 'info',
|
||||
options = {},
|
||||
) => {
|
||||
const notification = {
|
||||
...options,
|
||||
message,
|
||||
severity,
|
||||
created: Date.now(),
|
||||
};
|
||||
const newNotifications = [notification, ...notifications];
|
||||
|
||||
storedNotifications.unshift(notification);
|
||||
storedNotifications.splice(MAX_STORED_NOTIFICATIONS);
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(storedNotifications));
|
||||
|
||||
return {
|
||||
notifications: newNotifications,
|
||||
storedNotifications: [...storedNotifications],
|
||||
};
|
||||
};
|
||||
|
||||
const doClearStoredNotifications = () => {
|
||||
const storedNotifications = [];
|
||||
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, storedNotifications);
|
||||
return { storedNotifications };
|
||||
};
|
||||
|
||||
const doClearAllOnScreenNotifications = () => {
|
||||
return { notifications: [] };
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
notifications: [],
|
||||
storedNotifications: JSON.parse(
|
||||
localStorage.getItem(LOCAL_STORAGE_KEY) || '[]',
|
||||
),
|
||||
};
|
||||
|
||||
export const reducer = (state = initialState, action) => {
|
||||
const { message, severity, options, index } = action;
|
||||
|
||||
switch (action.type) {
|
||||
case NOTIFY:
|
||||
return { ...state, ...doNotify(state, message, severity, options) };
|
||||
case CLEAR:
|
||||
return {
|
||||
...state,
|
||||
...clearNotificationAtIndex(state.notifications, index),
|
||||
};
|
||||
case CLEAR_EXPIRED_TRANSIENTS:
|
||||
return {
|
||||
...state,
|
||||
...clearExpiredTransientNotifications(state.notifications),
|
||||
};
|
||||
case CLEAR_ALL_ON_SCREEN:
|
||||
return { ...state, ...doClearAllOnScreenNotifications() };
|
||||
case CLEAR_STORED:
|
||||
return { ...state, ...doClearStoredNotifications() };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
|
@ -17,7 +17,6 @@ import {
|
|||
DropdownItem,
|
||||
} from 'reactstrap';
|
||||
|
||||
import { withNotifications } from '../shared/context/Notifications';
|
||||
import JobModel from '../models/job';
|
||||
|
||||
import TestFailure from './TestFailure';
|
||||
|
@ -68,6 +67,7 @@ class ClassificationGroup extends React.PureComponent {
|
|||
headerColor,
|
||||
user,
|
||||
hasRetriggerAll,
|
||||
notify,
|
||||
} = this.props;
|
||||
const expandIcon = detailsShowing ? faMinusSquare : faPlusSquare;
|
||||
|
||||
|
@ -125,6 +125,7 @@ class ClassificationGroup extends React.PureComponent {
|
|||
repo={repo}
|
||||
revision={revision}
|
||||
user={user}
|
||||
notify={notify}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -154,4 +155,4 @@ ClassificationGroup.defaultProps = {
|
|||
hasRetriggerAll: false,
|
||||
};
|
||||
|
||||
export default withNotifications(ClassificationGroup);
|
||||
export default ClassificationGroup;
|
||||
|
|
|
@ -4,15 +4,18 @@ import { Table, Container, Spinner } from 'reactstrap';
|
|||
|
||||
import ErrorMessages from '../shared/ErrorMessages';
|
||||
import NotificationList from '../shared/NotificationList';
|
||||
import { Notifications } from '../shared/context/Notifications';
|
||||
import { getJobsUrl } from '../helpers/url';
|
||||
import {
|
||||
clearNotificationAtIndex,
|
||||
clearExpiredTransientNotifications,
|
||||
} from '../helpers/notifications';
|
||||
import PushModel from '../models/push';
|
||||
|
||||
import { resultColorMap } from './helpers';
|
||||
import Metric from './Metric';
|
||||
import Navigation from './Navigation';
|
||||
|
||||
export default class Health extends React.Component {
|
||||
export default class Health extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
@ -24,6 +27,7 @@ export default class Health extends React.Component {
|
|||
repo: params.get('repo'),
|
||||
healthData: null,
|
||||
failureMessage: null,
|
||||
notifications: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -33,6 +37,11 @@ export default class Health extends React.Component {
|
|||
|
||||
// Update the tests every two minutes.
|
||||
this.testTimerId = setInterval(() => this.updatePushHealth(), 120000);
|
||||
this.notificationsId = setInterval(() => {
|
||||
const { notifications } = this.state;
|
||||
|
||||
this.setState(clearExpiredTransientNotifications(notifications));
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -53,61 +62,87 @@ export default class Health extends React.Component {
|
|||
this.setState(newState);
|
||||
};
|
||||
|
||||
notify = (message, severity, options = {}) => {
|
||||
const { notifications } = this.state;
|
||||
const notification = {
|
||||
...options,
|
||||
message,
|
||||
severity: severity || 'info',
|
||||
created: Date.now(),
|
||||
};
|
||||
const newNotifications = [notification, ...notifications];
|
||||
|
||||
this.setState({
|
||||
notifications: newNotifications,
|
||||
});
|
||||
};
|
||||
|
||||
clearNotification = index => {
|
||||
const { notifications } = this.state;
|
||||
|
||||
this.setState(clearNotificationAtIndex(notifications, index));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { healthData, user, repo, revision, failureMessage } = this.state;
|
||||
const {
|
||||
healthData,
|
||||
user,
|
||||
repo,
|
||||
revision,
|
||||
failureMessage,
|
||||
notifications,
|
||||
} = this.state;
|
||||
const overallResult = healthData
|
||||
? resultColorMap[healthData.result]
|
||||
: 'none';
|
||||
|
||||
return (
|
||||
<Notifications>
|
||||
<React.Fragment>
|
||||
<Navigation user={user} setUser={this.setUser} />
|
||||
<Container fluid className="mt-2">
|
||||
<NotificationList />
|
||||
{healthData && (
|
||||
<div className="d-flex flex-column">
|
||||
<h3 className="text-center">
|
||||
<span
|
||||
className={`badge badge-xl mb-3 badge-${overallResult}`}
|
||||
<React.Fragment>
|
||||
<Navigation user={user} setUser={this.setUser} notify={this.notify} />
|
||||
<Container fluid className="mt-2">
|
||||
<NotificationList
|
||||
notifications={notifications}
|
||||
clearNotification={this.clearNotification}
|
||||
/>
|
||||
{healthData && (
|
||||
<div className="d-flex flex-column">
|
||||
<h3 className="text-center">
|
||||
<span className={`badge badge-xl mb-3 badge-${overallResult}`}>
|
||||
<a
|
||||
href={getJobsUrl({ repo, revision })}
|
||||
className="text-white"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<a
|
||||
href={getJobsUrl({ repo, revision })}
|
||||
className="text-white"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{repo} - {revision}
|
||||
</a>
|
||||
</span>
|
||||
</h3>
|
||||
<Table size="sm" className="table-fixed">
|
||||
<tbody>
|
||||
{healthData.metrics.map(metric => (
|
||||
<tr key={metric.name}>
|
||||
<Metric
|
||||
name={metric.name}
|
||||
result={metric.result}
|
||||
value={metric.value}
|
||||
details={metric.details}
|
||||
failures={metric.failures}
|
||||
repo={repo}
|
||||
revision={revision}
|
||||
user={user}
|
||||
/>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
{failureMessage && (
|
||||
<ErrorMessages failureMessage={failureMessage} />
|
||||
)}
|
||||
{!failureMessage && !healthData && <Spinner />}
|
||||
</Container>
|
||||
</React.Fragment>
|
||||
</Notifications>
|
||||
{repo} - {revision}
|
||||
</a>
|
||||
</span>
|
||||
</h3>
|
||||
<Table size="sm" className="table-fixed">
|
||||
<tbody>
|
||||
{healthData.metrics.map(metric => (
|
||||
<tr key={metric.name}>
|
||||
<Metric
|
||||
name={metric.name}
|
||||
result={metric.result}
|
||||
value={metric.value}
|
||||
details={metric.details}
|
||||
failures={metric.failures}
|
||||
repo={repo}
|
||||
revision={revision}
|
||||
user={user}
|
||||
notify={this.notify}
|
||||
/>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
{failureMessage && <ErrorMessages failureMessage={failureMessage} />}
|
||||
{!failureMessage && !healthData && <Spinner />}
|
||||
</Container>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ export default class Metric extends React.PureComponent {
|
|||
repo,
|
||||
revision,
|
||||
user,
|
||||
notify,
|
||||
} = this.props;
|
||||
const resultColor = resultColorMap[result];
|
||||
const expandIcon = detailsShowing ? faMinusSquare : faPlusSquare;
|
||||
|
@ -68,6 +69,7 @@ export default class Metric extends React.PureComponent {
|
|||
repo={repo}
|
||||
revision={revision}
|
||||
user={user}
|
||||
notify={notify}
|
||||
/>
|
||||
)}
|
||||
{details &&
|
||||
|
@ -92,6 +94,7 @@ Metric.propTypes = {
|
|||
result: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
details: PropTypes.array,
|
||||
failures: PropTypes.object,
|
||||
};
|
||||
|
|
|
@ -8,7 +8,7 @@ import SimpleTooltip from '../shared/SimpleTooltip';
|
|||
|
||||
export default class Navigation extends React.PureComponent {
|
||||
render() {
|
||||
const { user, setUser } = this.props;
|
||||
const { user, setUser, notify } = this.props;
|
||||
|
||||
return (
|
||||
<Navbar dark color="dark">
|
||||
|
@ -27,7 +27,7 @@ export default class Navigation extends React.PureComponent {
|
|||
</div>
|
||||
}
|
||||
/>
|
||||
<Login user={user} setUser={setUser} />
|
||||
<Login user={user} setUser={setUser} notify={notify} />
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
|
@ -36,4 +36,5 @@ export default class Navigation extends React.PureComponent {
|
|||
Navigation.propTypes = {
|
||||
user: PropTypes.object.isRequired,
|
||||
setUser: PropTypes.func.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
|
@ -5,7 +5,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|||
import { faRedo } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import JobModel from '../models/job';
|
||||
import { withNotifications } from '../shared/context/Notifications';
|
||||
|
||||
import Job from './Job';
|
||||
|
||||
|
@ -32,7 +31,9 @@ class TestFailure extends React.PureComponent {
|
|||
const { user, repo, notify } = this.props;
|
||||
|
||||
if (!user.isLoggedIn) {
|
||||
notify('Must be logged in to retrigger a job', 'danger');
|
||||
notify('Must be logged in to retrigger a job', 'danger', {
|
||||
sticky: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
JobModel.retrigger([job], repo, notify);
|
||||
|
@ -178,4 +179,4 @@ TestFailure.propTypes = {
|
|||
notify: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withNotifications(TestFailure);
|
||||
export default TestFailure;
|
||||
|
|
|
@ -5,7 +5,7 @@ import ClassificationGroup from './ClassificationGroup';
|
|||
|
||||
export default class TestFailures extends React.PureComponent {
|
||||
render() {
|
||||
const { failures, repo, revision, user } = this.props;
|
||||
const { failures, repo, revision, user, notify } = this.props;
|
||||
const { needInvestigation, intermittent } = failures;
|
||||
const needInvestigationLength = Object.keys(needInvestigation).length;
|
||||
|
||||
|
@ -20,6 +20,7 @@ export default class TestFailures extends React.PureComponent {
|
|||
headerColor={needInvestigationLength ? 'danger' : 'secondary'}
|
||||
user={user}
|
||||
hasRetriggerAll
|
||||
notify={notify}
|
||||
/>
|
||||
<ClassificationGroup
|
||||
group={intermittent}
|
||||
|
@ -30,6 +31,7 @@ export default class TestFailures extends React.PureComponent {
|
|||
headerColor="secondary"
|
||||
expanded={false}
|
||||
user={user}
|
||||
notify={notify}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -41,4 +43,5 @@ TestFailures.propTypes = {
|
|||
user: PropTypes.object.isRequired,
|
||||
repo: PropTypes.string.isRequired,
|
||||
revision: PropTypes.string.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
|
@ -8,8 +8,6 @@ import {
|
|||
faExclamationTriangle,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { withNotifications } from './context/Notifications';
|
||||
|
||||
class NotificationList extends React.Component {
|
||||
static getIcon(severity) {
|
||||
// TODO: Move this and the usage in NotificationsMenu to a shared component.
|
||||
|
@ -26,7 +24,7 @@ class NotificationList extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { notifications, removeNotification } = this.props;
|
||||
const { notifications, clearNotification } = this.props;
|
||||
|
||||
return (
|
||||
<ul id="notification-box" className="list-unstyled">
|
||||
|
@ -37,7 +35,7 @@ class NotificationList extends React.Component {
|
|||
icon={NotificationList.getIcon(notification.severity)}
|
||||
title={notification.severity}
|
||||
/>
|
||||
<span>{notification.message}</span>
|
||||
<span className="ml-1">{notification.message}</span>
|
||||
{notification.url && notification.linkText && (
|
||||
<span>
|
||||
<a href={notification.url}>{notification.linkText}</a>
|
||||
|
@ -46,7 +44,7 @@ class NotificationList extends React.Component {
|
|||
{notification.sticky && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeNotification(idx)}
|
||||
onClick={() => clearNotification(idx)}
|
||||
className="close"
|
||||
>
|
||||
x
|
||||
|
@ -65,12 +63,11 @@ NotificationList.propTypes = {
|
|||
PropTypes.shape({
|
||||
created: PropTypes.number.isRequired,
|
||||
message: PropTypes.string.isRequired,
|
||||
// TODO: Enforce specific severity strings
|
||||
severity: PropTypes.string.isRequired,
|
||||
severity: PropTypes.oneOf(['danger', 'warning', 'info', 'success']),
|
||||
sticky: PropTypes.bool,
|
||||
}),
|
||||
).isRequired,
|
||||
removeNotification: PropTypes.func.isRequired,
|
||||
clearNotification: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withNotifications(NotificationList);
|
||||
export default NotificationList;
|
||||
|
|
|
@ -9,7 +9,6 @@ import { loggedOutUser } from '../../helpers/auth';
|
|||
import taskcluster from '../../helpers/taskcluster';
|
||||
import { getApiUrl, loginCallbackUrl } from '../../helpers/url';
|
||||
import UserModel from '../../models/user';
|
||||
import { withNotifications } from '../context/Notifications';
|
||||
|
||||
import AuthService from './AuthService';
|
||||
|
||||
|
@ -167,4 +166,4 @@ Login.defaultProps = {
|
|||
notify: msg => console.error(msg), // eslint-disable-line no-console
|
||||
};
|
||||
|
||||
export default withNotifications(Login);
|
||||
export default Login;
|
||||
|
|
|
@ -1,156 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import findLastIndex from 'lodash/findLastIndex';
|
||||
|
||||
export const NotificationsContext = React.createContext({});
|
||||
const maxTransientNotifications = 5;
|
||||
const maxStoredNotifications = 40;
|
||||
|
||||
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,
|
||||
clearOnScreenNotifications: this.clearOnScreenNotifications,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('storage', this.handleStorageEvent);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('storage', this.handleStorageEvent);
|
||||
}
|
||||
|
||||
setValue = (newState, callback) => {
|
||||
this.value = { ...this.value, ...newState };
|
||||
this.setState(newState, callback);
|
||||
};
|
||||
|
||||
handleStorageEvent = e => {
|
||||
if (e.key === 'notifications') {
|
||||
this.setValue({
|
||||
storedNotifications: JSON.parse(
|
||||
localStorage.getItem('notifications') || '[]',
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
notify = (message, severity, opts) => {
|
||||
opts = opts || {};
|
||||
severity = severity || 'info';
|
||||
const { notifications, storedNotifications } = this.state;
|
||||
const notification = { ...opts, message, severity, created: Date.now() };
|
||||
const trimmedNotifications =
|
||||
notifications >= maxTransientNotifications
|
||||
? this.withoutOldestTransient(notifications)
|
||||
: notifications;
|
||||
|
||||
const newNotifications = [notification, ...trimmedNotifications];
|
||||
|
||||
storedNotifications.unshift(notification);
|
||||
storedNotifications.splice(maxStoredNotifications);
|
||||
localStorage.setItem('notifications', JSON.stringify(storedNotifications));
|
||||
|
||||
this.setValue(
|
||||
{
|
||||
notifications: newNotifications,
|
||||
storedNotifications: [...storedNotifications],
|
||||
},
|
||||
() => {
|
||||
if (!opts.sticky) {
|
||||
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);
|
||||
};
|
||||
|
||||
withoutOldestTransient = notifications => {
|
||||
const last = findLastIndex(notifications, n => !n.sticky);
|
||||
|
||||
if (last) {
|
||||
notifications.splice(last, 1);
|
||||
}
|
||||
return notifications;
|
||||
};
|
||||
|
||||
/*
|
||||
* Clear the list of stored notifications
|
||||
*/
|
||||
clearStoredNotifications = () => {
|
||||
const storedNotifications = [];
|
||||
|
||||
localStorage.setItem('notifications', storedNotifications);
|
||||
this.setValue({ storedNotifications });
|
||||
};
|
||||
|
||||
clearOnScreenNotifications = () => {
|
||||
this.setValue({ notifications: [] });
|
||||
};
|
||||
|
||||
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}
|
||||
clearOnScreenNotifications={context.clearOnScreenNotifications}
|
||||
/>
|
||||
)}
|
||||
</NotificationsContext.Consumer>
|
||||
);
|
||||
};
|
||||
}
|
16
yarn.lock
16
yarn.lock
|
@ -670,7 +670,7 @@
|
|||
"@babel/plugin-transform-react-jsx-self" "^7.0.0"
|
||||
"@babel/plugin-transform-react-jsx-source" "^7.0.0"
|
||||
|
||||
"@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.3":
|
||||
"@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.3":
|
||||
version "7.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.4.tgz#dc2e34982eb236803aa27a07fea6857af1b9171d"
|
||||
integrity sha512-w0+uT71b6Yi7i5SE0co4NioIpSYS6lLiXvCzWzGSKvpK5vdQtCbICHMj+gbAKAOtxiV6HsVh/MBdaF9EQ6faSg==
|
||||
|
@ -7599,7 +7599,7 @@ react-input-autosize@^2.2.1:
|
|||
dependencies:
|
||||
prop-types "^15.5.8"
|
||||
|
||||
react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.2, react-is@^16.8.4, react-is@^16.8.6:
|
||||
react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6:
|
||||
version "16.8.6"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16"
|
||||
integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==
|
||||
|
@ -7640,17 +7640,17 @@ react-popper@^0.10.4:
|
|||
popper.js "^1.14.1"
|
||||
prop-types "^15.6.1"
|
||||
|
||||
react-redux@6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-6.0.1.tgz#0d423e2c1cb10ada87293d47e7de7c329623ba4d"
|
||||
integrity sha512-T52I52Kxhbqy/6TEfBv85rQSDz6+Y28V/pf52vDWs1YRXG19mcFOGfHnY2HsNFHyhP+ST34Aih98fvt6tqwVcQ==
|
||||
react-redux@7.0.3:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.0.3.tgz#983c5a6de81cb1e696bd1c090ba826545f9170f1"
|
||||
integrity sha512-vYZA7ftOYlDk3NetitsI7fLjryt/widNl1SLXYvFenIpm7vjb4ryK0EeFrgn62usg5fYkyIAWNUPKnwWPevKLg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.3.1"
|
||||
"@babel/runtime" "^7.4.3"
|
||||
hoist-non-react-statics "^3.3.0"
|
||||
invariant "^2.2.4"
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.7.2"
|
||||
react-is "^16.8.2"
|
||||
react-is "^16.8.6"
|
||||
|
||||
react-router-dom@5.0.0:
|
||||
version "5.0.0"
|
||||
|
|
Загрузка…
Ссылка в новой задаче