You've already forked oncall
mirror of
https://github.com/linkedin/oncall.git
synced 2025-11-29 23:38:17 +02:00
Merge pull request #20 from dwang159/iris
Add Iris plan support for team escalation
This commit is contained in:
@@ -20,7 +20,7 @@ session:
|
||||
encrypt_key: 'abc'
|
||||
sign_key: '123'
|
||||
auth:
|
||||
debug: True
|
||||
debug: False
|
||||
module: 'oncall.auth.modules.debug'
|
||||
notifier:
|
||||
skipsend: True
|
||||
@@ -65,3 +65,9 @@ user_validator:
|
||||
|
||||
slack_instance: foobar
|
||||
header_color: '#3a3a3a'
|
||||
iris_plan_integration:
|
||||
activated: True
|
||||
app: oncall
|
||||
api_key: magic
|
||||
api_host: http://localhost:16649
|
||||
plan_url: '/v0/applications/oncall/plans'
|
||||
|
||||
@@ -11,6 +11,7 @@ CREATE TABLE IF NOT EXISTS `team` (
|
||||
`email` VARCHAR(255),
|
||||
`scheduling_timezone` VARCHAR(255),
|
||||
`active` BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
`iris_plan` VARCHAR(255),
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE INDEX `name_unique` (`name` ASC));
|
||||
|
||||
|
||||
@@ -71,3 +71,11 @@ def init(application, config):
|
||||
|
||||
from . import upcoming_shifts
|
||||
application.add_route('/api/v0/users/{user_name}/upcoming', upcoming_shifts)
|
||||
|
||||
# Optional Iris integration
|
||||
from . import iris_settings
|
||||
application.add_route('/api/v0/iris_settings', iris_settings)
|
||||
from ... import iris
|
||||
if iris.client and config.get('iris_plan_integration', {}).get('activated'):
|
||||
from . import team_iris_escalate
|
||||
application.add_route('/api/v0/teams/{team}/iris_escalate', team_iris_escalate)
|
||||
|
||||
9
src/oncall/api/v0/iris_settings.py
Normal file
9
src/oncall/api/v0/iris_settings.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from ... import iris
|
||||
from ujson import dumps as json_dumps
|
||||
|
||||
|
||||
def on_get(req, resp):
|
||||
if iris.settings is None:
|
||||
resp.body = json_dumps({'activated': False})
|
||||
else:
|
||||
resp.body = json_dumps(iris.settings)
|
||||
@@ -5,7 +5,7 @@ from urllib import unquote
|
||||
from falcon import HTTPNotFound, HTTPBadRequest, HTTPError
|
||||
from ujson import dumps as json_dumps
|
||||
|
||||
from ... import db
|
||||
from ... import db, iris
|
||||
from .users import get_user_data
|
||||
from .rosters import get_roster_by_team_id
|
||||
from ...auth import login_required, check_team_auth
|
||||
@@ -13,7 +13,7 @@ from ...utils import load_json_body, invalid_char_reg, create_audit
|
||||
from ...constants import TEAM_DELETED, TEAM_EDITED
|
||||
|
||||
|
||||
cols = set(['name', 'slack_channel', 'email', 'scheduling_timezone'])
|
||||
cols = set(['name', 'slack_channel', 'email', 'scheduling_timezone', 'iris_plan'])
|
||||
|
||||
|
||||
def populate_team_users(cursor, team_dict):
|
||||
@@ -60,7 +60,7 @@ def on_get(req, resp, team):
|
||||
|
||||
connection = db.connect()
|
||||
cursor = connection.cursor(db.DictCursor)
|
||||
cursor.execute('SELECT `id`, `name`, `email`, `slack_channel`, `scheduling_timezone` '
|
||||
cursor.execute('SELECT `id`, `name`, `email`, `slack_channel`, `scheduling_timezone`, `iris_plan` '
|
||||
'FROM `team` WHERE `name`=%s AND `active` = %s', (team, active))
|
||||
results = cursor.fetchall()
|
||||
if not results:
|
||||
@@ -96,6 +96,13 @@ def on_put(req, resp, team):
|
||||
if invalid_char:
|
||||
raise HTTPBadRequest('invalid team name',
|
||||
'team name contains invalid character "%s"' % invalid_char.group())
|
||||
|
||||
if 'iris_plan' in data:
|
||||
iris_plan = data['iris_plan']
|
||||
plan_resp = iris.client.get(iris.client.url + 'plans?name=%s&active=1' % iris_plan)
|
||||
if plan_resp.status_code != 200 or plan_resp.json() == []:
|
||||
raise HTTPBadRequest('invalid iris escalation plan', 'no iris plan named %s exists' % iris_plan)
|
||||
|
||||
set_clause = ', '.join(['`{0}`=%s'.format(d) for d in data_cols if d in cols])
|
||||
query_params = tuple(data[d] for d in data_cols) + (team,)
|
||||
try:
|
||||
|
||||
34
src/oncall/api/v0/team_iris_escalate.py
Normal file
34
src/oncall/api/v0/team_iris_escalate.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from ... import db, iris
|
||||
from ...utils import load_json_body
|
||||
from ...auth import login_required
|
||||
from falcon import HTTPBadRequest
|
||||
from requests import ConnectionError
|
||||
|
||||
|
||||
@login_required
|
||||
def on_post(req, resp, team):
|
||||
'''
|
||||
Escalate to a team using the team's configured Iris plan. Configured in the
|
||||
'iris_plan_integration' section of the configuration file.
|
||||
'''
|
||||
data = load_json_body(req)
|
||||
|
||||
connection = db.connect()
|
||||
cursor = connection.cursor()
|
||||
cursor.execute('SELECT iris_plan FROM team WHERE name = %s', team)
|
||||
plan_name = cursor.fetchone()[0]
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
requester = req.context.get('user')
|
||||
if not requester:
|
||||
requester = req.context['app']
|
||||
data['requester'] = requester
|
||||
if 'description' not in data or data['description'] == '':
|
||||
raise HTTPBadRequest('Iris escalation failed', 'Escalation cannot have an empty description')
|
||||
if plan_name is None:
|
||||
raise HTTPBadRequest('Iris escalation failed', 'No escalation plan specified for team: %s' % team)
|
||||
try:
|
||||
iris.client.incident(plan_name, context=data)
|
||||
except (ValueError, ConnectionError) as e:
|
||||
raise HTTPBadRequest('Iris escalation failed', 'Iris client error: %s' % e)
|
||||
@@ -7,7 +7,7 @@ from ujson import dumps as json_dumps
|
||||
from ...utils import load_json_body, invalid_char_reg, subscribe_notifications, create_audit
|
||||
from ...constants import TEAM_CREATED
|
||||
|
||||
from ... import db
|
||||
from ... import db, iris
|
||||
from ...auth import login_required
|
||||
|
||||
constraints = {
|
||||
@@ -23,7 +23,8 @@ constraints = {
|
||||
|
||||
|
||||
def on_get(req, resp):
|
||||
query = 'SELECT `name`, `email`, `slack_channel`, `scheduling_timezone` FROM `team`'
|
||||
|
||||
query = 'SELECT `name`, `email`, `slack_channel`, `scheduling_timezone`, `iris_plan` FROM `team`'
|
||||
if 'active' not in req.params:
|
||||
req.params['active'] = True
|
||||
|
||||
@@ -70,13 +71,21 @@ def on_post(req, resp):
|
||||
raise HTTPBadRequest('invalid slack channel',
|
||||
'slack channel name needs to start with #')
|
||||
email = data.get('email')
|
||||
iris_plan = data.get('iris_plan')
|
||||
|
||||
# validate Iris plan if provided and Iris is configured
|
||||
if iris_plan is not None and iris.client is not None:
|
||||
plan_resp = iris.client.get(iris.client.url + 'plans?name=%s&active=1' % iris_plan)
|
||||
if plan_resp.status_code != 200 or plan_resp.json() == []:
|
||||
raise HTTPBadRequest('invalid iris escalation plan', 'no iris plan named %s exists' % iris_plan)
|
||||
|
||||
connection = db.connect()
|
||||
cursor = connection.cursor()
|
||||
try:
|
||||
cursor.execute('''
|
||||
INSERT INTO `team` (`name`, `slack_channel`, `email`, `scheduling_timezone`)
|
||||
VALUES (%s, %s, %s, %s)''', (team_name, slack, email, scheduling_timezone))
|
||||
INSERT INTO `team` (`name`, `slack_channel`, `email`, `scheduling_timezone`, `iris_plan`)
|
||||
VALUES (%s, %s, %s, %s, %s)''', (team_name, slack, email, scheduling_timezone, iris_plan))
|
||||
|
||||
team_id = cursor.lastrowid
|
||||
query = '''
|
||||
INSERT INTO `team_user` (`team_id`, `user_id`)
|
||||
|
||||
@@ -11,7 +11,7 @@ import re
|
||||
from beaker.middleware import SessionMiddleware
|
||||
from falcon_cors import CORS
|
||||
|
||||
from . import db, constants
|
||||
from . import db, constants, iris
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger('oncall.app')
|
||||
@@ -116,6 +116,8 @@ def init(config):
|
||||
|
||||
db.init(config['db'])
|
||||
constants.init(config)
|
||||
if 'iris_plan_integration' in config:
|
||||
iris.init(config['iris_plan_integration'])
|
||||
init_falcon_api(config)
|
||||
|
||||
global application
|
||||
|
||||
14
src/oncall/iris.py
Normal file
14
src/oncall/iris.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license.
|
||||
# See LICENSE in the project root for license information.
|
||||
|
||||
from irisclient import IrisClient
|
||||
|
||||
client = None
|
||||
settings = None
|
||||
|
||||
|
||||
def init(config):
|
||||
global client
|
||||
global settings
|
||||
settings = {key: config[key] for key in config if key != 'api_key'}
|
||||
client = IrisClient(config['app'], config['api_key'], config['api_host'])
|
||||
@@ -61,6 +61,7 @@ INDEX_CONTENT_SETTING = {
|
||||
|
||||
SLACK_INSTANCE = None
|
||||
HEADER_COLOR = None
|
||||
IRIS_PLAN_SETTINGS = None
|
||||
|
||||
|
||||
def index(req, resp):
|
||||
@@ -71,6 +72,7 @@ def index(req, resp):
|
||||
slack_instance=SLACK_INSTANCE,
|
||||
user_setting_note=INDEX_CONTENT_SETTING['user_setting_note'],
|
||||
header_color=HEADER_COLOR,
|
||||
iris_plan_settings=IRIS_PLAN_SETTINGS,
|
||||
footer=INDEX_CONTENT_SETTING['footer']
|
||||
)
|
||||
|
||||
@@ -112,8 +114,10 @@ def init(application, config):
|
||||
|
||||
global SLACK_INSTANCE
|
||||
global HEADER_COLOR
|
||||
global IRIS_PLAN_SETTINGS
|
||||
SLACK_INSTANCE = config.get('slack_instance')
|
||||
HEADER_COLOR = config.get('header_color', '#3a3a3a')
|
||||
IRIS_PLAN_SETTINGS = config.get('iris_plan_integration')
|
||||
|
||||
application.add_sink(index, '/')
|
||||
application.add_route('/static/bundles/{filename}',
|
||||
|
||||
@@ -818,6 +818,34 @@ nav.subnav li.active {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#oncall-now-heading h4 {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
body[data-authenticated="false"] #escalate-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#escalate-description {
|
||||
height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.text-actions > span {
|
||||
cursor: pointer;
|
||||
color: #000;
|
||||
opacity: .3;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.text-actions > span .grey-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.text-actions > span:hover {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
/* Teams.info */
|
||||
|
||||
.module-card .badge.toggle-rotation {
|
||||
@@ -992,21 +1020,6 @@ div[data-admin="false"] .team-info .toggle-rotation {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.module-schedule .schedule-actions > span {
|
||||
cursor: pointer;
|
||||
color: #000;
|
||||
opacity: .3;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.module-schedule .schedule-actions > span .grey-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.module-schedule .schedule-actions > span:hover {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.module-schedule[data-role="secondary"] .schedule-actions {
|
||||
border-color: #E6E6FF;
|
||||
}
|
||||
|
||||
@@ -13,8 +13,10 @@ var oncall = {
|
||||
logoutUrl: '/logout',
|
||||
user: $('body').attr('data-user'),
|
||||
userUrl: '/api/v0/users/',
|
||||
irisSettingsUrl: '/api/v0/iris_settings',
|
||||
rolesUrl: '/api/v0/roles/',
|
||||
roles: null, // will be fetched from API
|
||||
irisSettings: null,
|
||||
modes: [
|
||||
'email',
|
||||
'sms',
|
||||
@@ -34,6 +36,7 @@ var oncall = {
|
||||
userInfo: null,
|
||||
csrfKey: 'csrf-key',
|
||||
userInfoPromise: $.Deferred(),
|
||||
irisSettingsPromise: $.Deferred(),
|
||||
rolesPromise: $.Deferred()
|
||||
},
|
||||
callbacks: {
|
||||
@@ -48,8 +51,12 @@ var oncall = {
|
||||
var self = this;
|
||||
|
||||
$.ajaxSetup({
|
||||
cache: 'true',
|
||||
headers: {'X-CSRF-TOKEN': localStorage.getItem(this.data.csrfKey)}
|
||||
cache: 'true'
|
||||
});
|
||||
$.ajaxPrefilter(function(options, originalOptions, jqXHR) {
|
||||
if (! options.crossDomain) {
|
||||
jqXHR.setRequestHeader('X-CSRF-TOKEN', localStorage.getItem(oncall.data.csrfKey));
|
||||
}
|
||||
});
|
||||
|
||||
$(document).ajaxError(function(event, jqxhr, settings, thrownError){
|
||||
@@ -63,7 +70,10 @@ var oncall = {
|
||||
this.defineRoutes();
|
||||
this.events.call(this);
|
||||
this.registerHandlebarHelpers();
|
||||
this.modal.init(this);
|
||||
this.getIrisSettings();
|
||||
this.data.irisSettingsPromise.done(function() {
|
||||
self.modal.init(self);
|
||||
});
|
||||
if (this.data.user && this.data.user !== 'None') {
|
||||
this.getUserInfo().done(this.getUpcomingShifts.bind(this));
|
||||
} else {
|
||||
@@ -87,7 +97,6 @@ var oncall = {
|
||||
var data = JSON.parse(data),
|
||||
token = data.csrf_token;
|
||||
|
||||
$.ajaxSetup({headers: {'X-CSRF-TOKEN': token}});
|
||||
localStorage.setItem(self.data.csrfKey, token);
|
||||
|
||||
self.data.userInfo = data;
|
||||
@@ -131,6 +140,13 @@ var oncall = {
|
||||
self.renderUserInfo.call(self, data);
|
||||
});
|
||||
},
|
||||
getIrisSettings: function (){
|
||||
var self = this;
|
||||
return $.get(this.data.irisSettingsUrl).done(function(data){
|
||||
self.data.irisSettings = data;
|
||||
self.data.irisSettingsPromise.resolve();
|
||||
});
|
||||
},
|
||||
renderUserInfo: function(data){
|
||||
var $body = this.data.$body,
|
||||
$nav = $body.find('#navbar'),
|
||||
@@ -733,11 +749,13 @@ var oncall = {
|
||||
$calendar: null,
|
||||
url: '/api/v0/teams/',
|
||||
pageSource: $('#team-calendar-template').html(),
|
||||
escalateModalTemplate: $('#team-escalate-modal'),
|
||||
cardColumnTemplate: $('#card-column-template').html(),
|
||||
cardInnerTemplate: $('#card-inner-slim-template').html(),
|
||||
cardOncallTemplate: $('#card-oncall-template').html(),
|
||||
addCardTemplate: $('#add-card-item-template').html(),
|
||||
calendarTypesTemplate: $('#calendar-types-template').html(),
|
||||
escalateModal: '#team-escalate-modal',
|
||||
cardExtra: '.card-inner[data-collapsed]',
|
||||
cardExtraChevron: '.card-inner[data-collapsed] .svg-icon-chevron',
|
||||
timezoneDisplay: '.timezone-display',
|
||||
@@ -754,7 +772,7 @@ var oncall = {
|
||||
this.getData(name);
|
||||
oncall.callbacks.onLogin = function(){
|
||||
self.init(name);
|
||||
}
|
||||
};
|
||||
oncall.callbacks.onLogout = function(){
|
||||
self.checkIfAdmin();
|
||||
self.data.$calendar.incalendar('updateCalendarOption', 'user', null);
|
||||
@@ -851,7 +869,6 @@ var oncall = {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
},
|
||||
checkIfAdmin: function(){
|
||||
var data = this.data.teamData;
|
||||
@@ -869,8 +886,16 @@ var oncall = {
|
||||
renderTeamSummary: function(data){
|
||||
var template = Handlebars.compile(this.data.cardOncallTemplate),
|
||||
$container = this.data.$page.find('#oncall-now-container');
|
||||
|
||||
$container.html(template(data));
|
||||
self = this;
|
||||
data.showEscalate = oncall.data.user && this.data.teamData.iris_plan;
|
||||
oncall.data.irisSettingsPromise.done(function(){
|
||||
data.showEscalate = data.showEscalate && oncall.data.irisSettings.activated;
|
||||
if (data.showEscalate) {
|
||||
data.iris_plan = self.data.teamData.iris_plan;
|
||||
}
|
||||
$container.html(template(data));
|
||||
self.setupEscalateModal();
|
||||
});
|
||||
},
|
||||
renderCalendarTypes: function(incalendar){
|
||||
var template = Handlebars.compile(this.data.calendarTypesTemplate),
|
||||
@@ -930,6 +955,46 @@ var oncall = {
|
||||
.append('<label class="label-col">Slack</label>')
|
||||
.append('<span class="data-col">' + userData.contacts.im + '</span>')
|
||||
)
|
||||
},
|
||||
setupEscalateModal: function(){
|
||||
var $modal = $(this.data.escalateModal),
|
||||
$modalForm = $modal.find('.modal-form'),
|
||||
$modalInput = $modalForm.find('.create-input'),
|
||||
$modalBtn = $modal.find('#escalate-btn'),
|
||||
$cta = $modal.find('.modal-cta'),
|
||||
self = this;
|
||||
|
||||
$modal.on('shown.bs.modal', function(e){
|
||||
$modalInput.trigger('focus');
|
||||
});
|
||||
|
||||
$modalBtn.on('click', function(e){
|
||||
$cta.addClass('loading disabled').prop('disabled', true);
|
||||
e.preventDefault();
|
||||
$modal.find('.alert').remove();
|
||||
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: self.data.url + self.data.teamName + '/iris_escalate',
|
||||
contentType: 'application/json',
|
||||
dataType: 'html',
|
||||
data: JSON.stringify({description:$modalForm.find('#escalate-description').val()})
|
||||
}).done(function(data){
|
||||
$modal.modal('hide');
|
||||
oncall.alerts.removeAlerts();
|
||||
oncall.alerts.createAlert('Escalated incident to ' + self.data.teamName + ' successfully, using the ' + self.data.teamData.iris_plan + ' Iris plan.', 'success');
|
||||
}).fail(function(data){
|
||||
var error = oncall.isJson(data.responseText) ? JSON.parse(data.responseText).description : data.responseText || 'Escalation failed.';
|
||||
oncall.alerts.createAlert(error, 'danger');
|
||||
}).always(function(){
|
||||
$cta.removeClass('loading disabled').prop('disabled', false);
|
||||
});
|
||||
});
|
||||
|
||||
$modal.on('hide.bs.modal', function(){
|
||||
$modal.find('.alert').remove();
|
||||
$modalForm[0].reset();
|
||||
});
|
||||
}
|
||||
},
|
||||
info: {
|
||||
@@ -2140,6 +2205,7 @@ var oncall = {
|
||||
$teamEmail = $modalForm.find('#team-email'),
|
||||
$teamSlack = $modalForm.find('#team-slack'),
|
||||
$teamTimezone = $modalForm.find('#team-timezone'),
|
||||
$teamIrisPlan = $modalForm.find('#team-irisplan'),
|
||||
self = this,
|
||||
$btn,
|
||||
action;
|
||||
@@ -2151,6 +2217,30 @@ var oncall = {
|
||||
$teamName.val($btn.attr('data-modal-name'));
|
||||
$teamEmail.val($btn.attr('data-modal-email'));
|
||||
$teamSlack.val($btn.attr('data-modal-slack'));
|
||||
$teamIrisPlan.val($btn.attr('data-modal-irisplan'));
|
||||
|
||||
results = new Bloodhound({
|
||||
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
|
||||
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
||||
remote: {
|
||||
url: oncall.data.irisSettings.api_host + oncall.data.irisSettings.plan_url
|
||||
+ '?name__startswith=%QUERY&fields=name',
|
||||
wildcard: '%QUERY'
|
||||
}
|
||||
});
|
||||
$('#team-irisplan').typeahead(null, {
|
||||
hint: true,
|
||||
async: true,
|
||||
highlight: true,
|
||||
source: results,
|
||||
display: 'name',
|
||||
templates: {
|
||||
empty: '<div> No plans found. </div>'
|
||||
}
|
||||
}).on('typeahead:select', function(){
|
||||
$(this).attr('value', $(this).val());
|
||||
});
|
||||
|
||||
if ($btn.attr('data-modal-timezone')) {
|
||||
$teamTimezone.val($btn.attr('data-modal-timezone'));
|
||||
}
|
||||
|
||||
@@ -144,6 +144,12 @@
|
||||
<option value="UTC">UTC</option>
|
||||
</select>
|
||||
</p>
|
||||
{% if iris_plan_settings.activated %}
|
||||
<p>
|
||||
<label for="team-irisplan"> Team Iris Escalation Plan:</label>
|
||||
<input type="text" data-type="plans" name="iris_plan" class="form-control" id="team-irisplan" placeholder="Plan Name" />
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
|
||||
@@ -235,7 +235,7 @@
|
||||
<h3>
|
||||
{{name}}
|
||||
<span class="edit-team-name">
|
||||
<i class="svg-icon svg-icon-pencil" data-toggle="modal" data-target="#team-edit-modal" data-modal-action="oncall.team.updateTeamName" data-modal-title="Update Team Info" data-modal-name="{{#if name}}{{name}}{{else}}{{@key}}{{/if}}" data-modal-email="{{email}}" data-modal-slack="{{slack_channel}}" data-modal-timezone="{{scheduling_timezone}}" data-admin-action="true">
|
||||
<i class="svg-icon svg-icon-pencil" data-toggle="modal" data-target="#team-edit-modal" data-modal-action="oncall.team.updateTeamName" data-modal-title="Update Team Info" data-modal-name="{{#if name}}{{name}}{{else}}{{@key}}{{/if}}" data-modal-email="{{email}}" data-modal-slack="{{slack_channel}}" data-modal-timezone="{{scheduling_timezone}}" data-modal-irisplan="{{iris_plan}}" data-admin-action="true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 8 8" style="fill: currentColor">
|
||||
<path d="M6 0l-1 1 2 2 1-1-2-2zm-2 2l-4 4v2h2l4-4-2-2z" />
|
||||
</svg>
|
||||
@@ -449,8 +449,16 @@
|
||||
<!--// On call now partial -->
|
||||
<script id="card-oncall-template" type="text/x-handlebars-template">
|
||||
<div class="module module-card module-top-edge">
|
||||
<div class="module-heading module-card-heading border-bottom">
|
||||
<h4>On Call Now</h4>
|
||||
<div class="module-heading module-card-heading border-bottom text-actions" id="oncall-now-heading">
|
||||
<h4>On Call Now </h4>
|
||||
{{#if showEscalate}}
|
||||
<span class="pull-right" data-toggle="modal" data-target="#team-escalate-modal" id="escalate-btn">
|
||||
<svg class="svg-icon grey-icon magic" xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 10 10">
|
||||
<path d="M4 0c-1.1 0-2 .9-2 2 0 1.04-.52 1.98-1.34 2.66-.41.34-.66.82-.66 1.34h8c0-.52-.24-1-.66-1.34-.82-.68-1.34-1.62-1.34-2.66 0-1.1-.89-2-2-2zm-1 7c0 .55.45 1 1 1s1-.45 1-1h-2z" />
|
||||
</svg>
|
||||
Escalate
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<ul>
|
||||
{{#ifNotEmpty current.primary}}
|
||||
@@ -518,6 +526,32 @@
|
||||
{{/ifNotEmpty}}
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Team Escalation modal -->
|
||||
<div class="modal fade" id="team-escalate-modal" tabindex="-1" role="dialog" aria-labelledby="team-escalate">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form class="modal-form modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title">Escalate</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
Escalating to the team will trigger an Iris incident with the provided description below, with your name
|
||||
associated as the requester. This incident will trigger an escalation following the Iris plan configured
|
||||
for this team, which is currently: <b>{{ iris_plan }}.</b>
|
||||
</p>
|
||||
<p>
|
||||
<label class="light" for="escalate-description"> Description: </label>
|
||||
<textarea name="escalate_description" id="escalate-description" class="form-control" placeholder="Description"></textarea>
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary modal-cta" id="escalate-btn" data-dismiss="modal"><span class="btn-text">Escalate</span> <i class="loader loader-small"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<!--// **********************
|
||||
@@ -684,7 +718,7 @@
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="schedule-actions">
|
||||
<div class="schedule-actions text-actions">
|
||||
<span class="populate-schedule pull-left" data-toggle="modal" data-target="#populate-schedule-modal">
|
||||
<i class="svg-icon svg-icon-cal grey-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 8 8">
|
||||
|
||||
Reference in New Issue
Block a user