зеркало из https://github.com/mozilla/treeherder.git
Bug 1123814 - Get desktop notifications when pushes or jobs complete (#3373)
This commit is contained in:
Родитель
9c017283c7
Коммит
7cf1013f09
|
@ -4,7 +4,7 @@
|
|||
|
||||
.push-bar {
|
||||
border-top: 1px solid black;
|
||||
padding: 2px 0 0 34px;
|
||||
padding: 2px 0 1px 34px;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
|
@ -59,6 +59,11 @@
|
|||
margin-right: 25px;
|
||||
}
|
||||
|
||||
.push-buttons > .btn,
|
||||
.push-buttons > .btn-group{
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.btn-push {
|
||||
color: #666;
|
||||
background-color: transparent;
|
||||
|
@ -93,6 +98,12 @@ th-action-button .btn-push {
|
|||
font-size: 14px;
|
||||
}
|
||||
|
||||
.watch-commit-btn:not([data-watch-state="none"]) {
|
||||
border: 1px solid;
|
||||
border-radius: 3px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* Encompasses unknown push,repo */
|
||||
.unknown-message-body {
|
||||
padding-top: 10px;
|
||||
|
|
|
@ -5,26 +5,90 @@ import PushHeader from './PushHeader';
|
|||
import { RevisionList } from './RevisionList';
|
||||
import * as aggregateIds from './aggregateIds';
|
||||
|
||||
const watchCycleStates = [
|
||||
"none",
|
||||
"push",
|
||||
"job",
|
||||
"none"
|
||||
];
|
||||
|
||||
export default class Push extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { $injector, repoName, push } = props;
|
||||
const { id: pushId, revision, job_counts } = push;
|
||||
|
||||
this.$rootScope = $injector.get('$rootScope');
|
||||
this.thEvents = $injector.get('thEvents');
|
||||
this.ThResultSetStore = $injector.get('ThResultSetStore');
|
||||
|
||||
this.aggregateId = aggregateIds.getPushTableId(
|
||||
repoName, push.id, push.revision
|
||||
);
|
||||
this.aggregateId = aggregateIds.getPushTableId(repoName, pushId, revision);
|
||||
this.showRunnableJobs = this.showRunnableJobs.bind(this);
|
||||
this.hideRunnableJobs = this.hideRunnableJobs.bind(this);
|
||||
|
||||
this.state = {
|
||||
runnableVisible: false
|
||||
runnableVisible: false,
|
||||
watched: "none",
|
||||
|
||||
// props.push isn't actually immutable due to the way it hooks up to angular, therefore we
|
||||
// need to keep the previous value in the state.
|
||||
last_job_counts: job_counts ? { ...job_counts } : null,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.showUpdateNotifications(nextProps);
|
||||
}
|
||||
|
||||
showUpdateNotifications(nextProps) {
|
||||
const { watched, last_job_counts } = this.state;
|
||||
const { repoName, push: { revision, id: pushId } } = this.props;
|
||||
|
||||
if (Notification.permission !== "granted" || watched === "none") {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextCounts = nextProps.push.job_counts;
|
||||
if (last_job_counts) {
|
||||
const nextUncompleted = nextCounts.pending + nextCounts.running;
|
||||
const lastUncompleted = last_job_counts.pending + last_job_counts.running;
|
||||
|
||||
const nextCompleted = nextCounts.completed;
|
||||
const lastCompleted = last_job_counts.completed;
|
||||
|
||||
let message;
|
||||
if (lastUncompleted > 0 && nextUncompleted === 0) {
|
||||
message = "Push completed";
|
||||
this.setState({ watched: "none" });
|
||||
} else if (watched === "job" && lastCompleted < nextCompleted) {
|
||||
const completeCount = nextCompleted - lastCompleted;
|
||||
message = completeCount + " jobs completed";
|
||||
}
|
||||
|
||||
if (message) {
|
||||
const notification = new Notification(message, {
|
||||
body: `${repoName} rev ${revision.substring(0, 12)}`,
|
||||
tag: pushId
|
||||
});
|
||||
|
||||
notification.onerror = (event) => {
|
||||
this.thNotify.send(`${event.target.title}: ${event.target.body}`, "danger");
|
||||
};
|
||||
|
||||
notification.onclick = (event) => {
|
||||
if (this.container) {
|
||||
this.container.scrollIntoView();
|
||||
event.target.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (nextCounts) {
|
||||
this.setState({ last_job_counts: Object.assign({}, nextCounts) });
|
||||
}
|
||||
}
|
||||
|
||||
showRunnableJobs() {
|
||||
this.$rootScope.$emit(this.thEvents.showRunnableJobs, this.props.push.id);
|
||||
this.setState({ runnableVisible: true });
|
||||
|
@ -36,27 +100,45 @@ export default class Push extends React.Component {
|
|||
this.setState({ runnableVisible: false });
|
||||
}
|
||||
|
||||
async cycleWatchState() {
|
||||
let next = watchCycleStates[watchCycleStates.indexOf(this.state.watched) + 1];
|
||||
|
||||
if (next !== "none" && Notification.permission !== "granted") {
|
||||
const result = await Notification.requestPermission();
|
||||
|
||||
if (result === "denied") {
|
||||
this.thNotify.send("Notification permission denied", "danger");
|
||||
|
||||
next = "none";
|
||||
}
|
||||
}
|
||||
this.setState({ watched: next });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { push, loggedIn, isStaff, $injector, repoName } = this.props;
|
||||
const { watched, runnableVisible } = this.state;
|
||||
const { currentRepo, urlBasePath } = this.$rootScope;
|
||||
const { id, push_timestamp, revision, job_counts, author } = push;
|
||||
|
||||
return (
|
||||
<div className="push">
|
||||
<div className="push" ref={(ref) => { this.container = ref; }}>
|
||||
<PushHeader
|
||||
pushId={id}
|
||||
pushTimestamp={push_timestamp}
|
||||
author={author}
|
||||
revision={revision}
|
||||
jobCounts={job_counts}
|
||||
watchState={watched}
|
||||
loggedIn={loggedIn}
|
||||
isStaff={isStaff}
|
||||
repoName={repoName}
|
||||
urlBasePath={urlBasePath}
|
||||
$injector={$injector}
|
||||
runnableVisible={this.state.runnableVisible}
|
||||
runnableVisible={runnableVisible}
|
||||
showRunnableJobsCb={this.showRunnableJobs}
|
||||
hideRunnableJobsCb={this.hideRunnableJobs}
|
||||
cycleWatchState={() => this.cycleWatchState()}
|
||||
/>
|
||||
<div className="push-body-divider" />
|
||||
<div className="row push clearfix">
|
||||
|
|
|
@ -162,14 +162,20 @@ export default class PushHeader extends React.PureComponent {
|
|||
|
||||
render() {
|
||||
const { repoName, loggedIn, pushId, isStaff, jobCounts, author,
|
||||
revision, runnableVisible, $injector,
|
||||
showRunnableJobsCb, hideRunnableJobsCb } = this.props;
|
||||
revision, runnableVisible, $injector, watchState,
|
||||
showRunnableJobsCb, hideRunnableJobsCb, cycleWatchState } = this.props;
|
||||
const { filterParams } = this.state;
|
||||
const cancelJobsTitle = loggedIn ?
|
||||
"Cancel all jobs" :
|
||||
"Must be logged in to cancel jobs";
|
||||
const counts = jobCounts || { pending: 0, running: 0, completed: 0 };
|
||||
|
||||
const watchStateLabel = {
|
||||
none: "Watch",
|
||||
push: "Notifying (per-push)",
|
||||
job: "Notifying (per-job)"
|
||||
}[watchState];
|
||||
|
||||
return (
|
||||
<div className="push-header">
|
||||
<div className="push-bar" key="push-header">
|
||||
|
@ -192,6 +198,13 @@ export default class PushHeader extends React.PureComponent {
|
|||
completed={counts.completed}
|
||||
/>
|
||||
<span className="push-buttons">
|
||||
{counts.pending + counts.running > 0 &&
|
||||
<button
|
||||
className="btn btn-sm btn-push watch-commit-btn"
|
||||
title="Get Desktop Notifications for this Push"
|
||||
data-watch-state={watchState}
|
||||
onClick={() => cycleWatchState()}
|
||||
>{watchStateLabel}</button>}
|
||||
<a
|
||||
className="btn btn-sm btn-push test-view-btn"
|
||||
href={`/testview.html?repo=${repoName}&revision=${revision}`}
|
||||
|
@ -272,6 +285,7 @@ PushHeader.propTypes = {
|
|||
author: PropTypes.string.isRequired,
|
||||
revision: PropTypes.string.isRequired,
|
||||
jobCounts: PropTypes.object,
|
||||
watchState: PropTypes.string,
|
||||
loggedIn: PropTypes.bool,
|
||||
isStaff: PropTypes.bool,
|
||||
repoName: PropTypes.string.isRequired,
|
||||
|
@ -280,4 +294,5 @@ PushHeader.propTypes = {
|
|||
runnableVisible: PropTypes.bool.isRequired,
|
||||
showRunnableJobsCb: PropTypes.func.isRequired,
|
||||
hideRunnableJobsCb: PropTypes.func.isRequired,
|
||||
cycleWatchState: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
Загрузка…
Ссылка в новой задаче