Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE ui-oncalls] On-call active incident view: show incident info and make red #74

Merged
merged 8 commits into from
Jan 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ This project is under active development.
- [x] Automated Testing
- [x] Automated Builds
- [x] No limit on the total number of schedules supported
- [ ] Change color to red when an incident is triggered
- [x] Make on-call view red during an active incident

#### Version 2.0

Expand Down
12 changes: 7 additions & 5 deletions src/app/PagerBeautyWorker.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ export class PagerBeautyWorker {
);
this.onCallsService = new OnCallsService(this.pagerDutyClient);
this.schedulesService = new SchedulesService(this.pagerDutyClient);
// Optional
this.incidentsService = false;
this.incidentsService = new IncidentsService(this.pagerDutyClient);

// Timers
this.onCallsTimer = false;
Expand All @@ -62,7 +61,6 @@ export class PagerBeautyWorker {
this.incidentsRefreshMS = PagerBeautyWorker.refreshRateToMs(
pagerDutyConfig.incidents.refreshRate,
);
this.incidentsService = new IncidentsService(this.pagerDutyClient);
}
}

Expand All @@ -71,8 +69,9 @@ export class PagerBeautyWorker {
async start() {
const { db, incidentsEnabled } = this;
logger.debug('Initializing database.');
db.set('oncalls', new Map());
db.set('schedules', new Map());
db.set('oncalls', this.onCallsService);
db.set('schedules', this.schedulesService);
db.set('incidents', this.incidentsService);

// Load schedules first.
await this.startSchedulesWorker();
Expand All @@ -95,6 +94,9 @@ export class PagerBeautyWorker {
if (this.schedulesTimer) {
await this.schedulesTimer.stop();
}
if (this.incidentsTimer) {
await this.incidentsTimer.stop();
}

const { db } = this;
db.clear();
Expand Down
120 changes: 98 additions & 22 deletions src/assets/javascripts/views/OnCallViews.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import React from 'react';
// ------- Internal imports ----------------------------------------------------

import { OnCall } from '../../../models/OnCall.mjs';
import { Incident } from '../../../models/Incident.mjs';
import { PagerBeautyFetchNotFoundUiError } from '../ui-errors';
import { StatusIndicatorView } from './StatusIndicatorView';

Expand Down Expand Up @@ -35,25 +36,62 @@ export class OnCallView extends React.Component {

let onCall;
let userInfo;

let state;
if (!is404) {
onCall = new OnCall(data);
userInfo = {
name: onCall.userName,
url: onCall.userURL,
avatar: onCall.userAvatarSized(),
};
state = onCall.incident ? 'active_incident' : 'normal';
} else {
state = 'not_found';
}

// Resolve what to show on status row.
let statusRow;
switch (state) {
case 'not_found':
statusRow = <OnCallStatusRowView />;
break;
case 'active_incident':
statusRow = (
<OnCallStatusRowView>
<OnCallIncidentRowView incident={onCall.incident} />
</OnCallStatusRowView>
);
break;
default:
statusRow = (
<OnCallStatusRowView>
<OnCallDateRowView
className="date_start"
label="From"
date={onCall.dateStart}
timezone={onCall.schedule.timezone}
/>
<OnCallDateRowView
className="date_end"
label="To"
date={onCall.dateEnd}
timezone={onCall.schedule.timezone}
/>
</OnCallStatusRowView>
);
}

return (
<div className={`schedule ${is404 ? 'not_found' : ''}`}>
<div className={`schedule state_${state}`}>
{ /* Header */ }
<OnCallScheduleRowView filled>
<span>ON CALL</span>
<OnCallStatusIndicatorView error={error} isFetching={isFetching} />
</OnCallScheduleRowView>

{ /* Schedule name */ }
{onCall && (
{state !== 'not_found' && (
<OnCallScheduleRowView>
<a href={onCall.schedule.url} className="schedule_name">{onCall.schedule.name}</a>
</OnCallScheduleRowView>
Expand All @@ -64,25 +102,8 @@ export class OnCallView extends React.Component {
<OnCallUserInfoView userInfo={userInfo} />
</OnCallScheduleRowView>

{ /* Dates */ }
<OnCallScheduleRowView filled equalSpacing>
{onCall && (
<React.Fragment>
<OnCallDateRowView
className="date_start"
label="From"
date={onCall.dateStart}
timezone={onCall.schedule.timezone}
/>
<OnCallDateRowView
className="date_end"
label="To"
date={onCall.dateEnd}
timezone={onCall.schedule.timezone}
/>
</React.Fragment>
)}
</OnCallScheduleRowView>
{ /* Status row */ }
{statusRow}

{ /* End */ }
</div>
Expand All @@ -108,27 +129,53 @@ OnCallView.defaultProps = {

export class OnCallScheduleRowView extends React.Component {
render() {
const { equalSpacing, filled, children } = this.props;
const { equalSpacing, filled, statusRow, children } = this.props;
const classes = ['schedule_row'];
if (equalSpacing) {
classes.push('equal_spacing');
}
if (filled) {
classes.push('filled_row');
}
if (statusRow) {
classes.push('status_row');
}
return <div className={classes.join(' ')}>{children}</div>;
}
}

OnCallScheduleRowView.propTypes = {
equalSpacing: PropTypes.bool,
filled: PropTypes.bool,
statusRow: PropTypes.bool,
children: PropTypes.node,
};

OnCallScheduleRowView.defaultProps = {
equalSpacing: false,
filled: false,
statusRow: false,
children: null,
};

// ------- OnCallStatusRowView -------------------------------------------------

export class OnCallStatusRowView extends React.Component {
render() {
const { children } = this.props;
return (
<OnCallScheduleRowView filled equalSpacing statusRow>
{children}
</OnCallScheduleRowView>
);
}
}

OnCallStatusRowView.propTypes = {
children: PropTypes.node,
};

OnCallStatusRowView.defaultProps = {
children: null,
};

Expand Down Expand Up @@ -277,6 +324,35 @@ OnCallDateRowView.defaultProps = {
label: '',
};

// ------- OnCallIncidentRowView -----------------------------------------------

export class OnCallIncidentRowView extends React.Component {
render() {
const { incident, className } = this.props;
return (
<div className={`incident ${className}`}>
<div className="incident_summary">
<span>{`Incident ${incident.status}: `}</span>
<a href={incident.url}>{incident.title}</a>
</div>
<div className="incident_service">
<span>Service: </span>
<a href={incident.serviceUrl}>{incident.serviceName}</a>
</div>
</div>
);
}
}

OnCallIncidentRowView.propTypes = {
incident: PropTypes.instanceOf(Incident).isRequired,
className: PropTypes.string,
};

OnCallIncidentRowView.defaultProps = {
className: '',
};

// ------- OnCallDateTimeView --------------------------------------------------

export class OnCallDateTimeView extends React.Component {
Expand Down
23 changes: 13 additions & 10 deletions src/assets/stylesheets/application.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Variables
$yellow: #fecb2e;
$red: #eb364b;
$green: #25c151;
$textBlack: #454545;
$textWhite: white;
Expand Down Expand Up @@ -76,6 +77,10 @@ a {
font-size: $row_size;
min-height: $row_size;

a {
color: $textWhite;
}

@media (max-width: 1000px) {
font-size: $row_size + 1;
min-height: $row_size + 1;
Expand Down Expand Up @@ -150,23 +155,21 @@ a {
margin-right: $dateMargin;
}
}

}

// Not found
.schedule.not_found {
border-color: $yellow;

// Schedule states
@mixin schedule_state($color) {
border-color: $color;
.schedule_row {
border-color: $yellow;
&.filled_row { background-color: $yellow; }
border-color: $color;
&.filled_row { background-color: $color; }
}

.user_name.error {
padding: $scheduleRowDefaultPadding;
}

.user_avatar img {
border-color: $yellow;
border-color: $color;
}
}
.schedule.state_not_found { @include schedule_state($yellow); }
.schedule.state_active_incident { @include schedule_state($red); }
6 changes: 5 additions & 1 deletion src/models/Incident.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export class Incident {
summary,
url,
serviceName,
serviceUrl,
}) {
this.id = id;
this.scheduleId = scheduleId;
Expand All @@ -21,6 +22,7 @@ export class Incident {
this.summary = summary;
this.url = url;
this.serviceName = serviceName;
this.serviceUrl = serviceUrl;
}

serialize() {
Expand All @@ -30,8 +32,9 @@ export class Incident {
status: this.status,
title: this.title,
summary: this.summary,
serviceName: this.serviceName,
url: this.url,
serviceName: this.serviceName,
serviceUrl: this.serviceUrl,
};
}

Expand All @@ -48,6 +51,7 @@ export class Incident {
summary: record.summary,
url: record.html_url,
serviceName: record.service ? record.service.summary : 'Unknown',
serviceUrl: record.service ? record.service.html_url : null,
scheduleId,
};
return new Incident(attributes);
Expand Down
10 changes: 9 additions & 1 deletion src/models/OnCall.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import moment from 'moment-timezone';

// ------- Internal imports ----------------------------------------------------

import { Incident } from './Incident';
import { Schedule } from './Schedule';

// ------- OnCall --------------------------------------------------------------
Expand All @@ -22,6 +23,7 @@ export class OnCall {
dateStart,
dateEnd,
schedule,
incident = null,
}) {
this.userId = userId;
this.userName = userName;
Expand All @@ -34,7 +36,13 @@ export class OnCall {
} else {
this.schedule = new Schedule(schedule);
}
this.incident = false;
if (incident instanceof Incident) {
this.incident = incident;
} else if (incident) {
this.incident = new Incident(incident);
} else {
this.incident = false;
}
}

serialize() {
Expand Down
16 changes: 12 additions & 4 deletions src/services/OnCallsService.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ export class OnCallsService {
this.onCallRepo = new Map();
}

async load(schedulesService) {
async load(schedulesService, incidentsService) {
const schedules = schedulesService.schedulesRepo;
const incidents = incidentsService.incidentsRepo;
if (!schedules.size) {
logger.verbose('Skipping on-calls load: Schedules not loaded yet');
return false;
Expand All @@ -34,10 +35,17 @@ export class OnCallsService {
includeFlags,
);

const oncall = OnCall.fromApiRecord(record, schedule);
const onCall = OnCall.fromApiRecord(record, schedule);

// Needed because of full override.
if (incidents.has(schedule.id)) {
onCall.setIncident(incidents.get(schedule.id));
}

logger.verbose(`On-call for schedule ${schedule.id} is loaded`);
logger.silly(`On-call loaded ${oncall.toString()}`);
this.onCallRepo.set(schedule.id, oncall);
logger.silly(`On-call loaded ${onCall.toString()}`);

this.onCallRepo.set(schedule.id, onCall);
} catch (e) {
logger.warn(`Error loading on-call for ${schedule.id}: ${e}`);
}
Expand Down
4 changes: 0 additions & 4 deletions src/tasks/IncidentsTimerTask.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@ export class IncidentsTimerTask {
logger.verbose(`Incidents refresh run #${runNumber}, every ${intervalMs}ms`);
const oncalls = this.db.get('oncalls');
const result = await this.incidentsService.load(oncalls);
if (result) {
// @todo: refresh without full override.
this.db.set('incidents', this.incidentsService);
}
return result;
}

Expand Down
Loading