mirror of
https://github.com/ansible/awx.git
synced 2026-01-14 11:20:39 -03:30
Merge branch 'devel' of github.com:ansible/ansible-tower into rbac
This commit is contained in:
commit
9d4e6dfc16
31
ISSUE_TEMPLATE.md
Normal file
31
ISSUE_TEMPLATE.md
Normal file
@ -0,0 +1,31 @@
|
||||
### Summary
|
||||
|
||||
<!-- Briefly describe the problem. -->
|
||||
|
||||
### Environment
|
||||
|
||||
<!--
|
||||
* Tower version: X.Y.Z
|
||||
* Ansible version: X.Y.Z
|
||||
* Operating System:
|
||||
* Web Browser:
|
||||
-->
|
||||
|
||||
### Steps To Reproduce:
|
||||
|
||||
<!-- For bugs, please show exactly how to reproduce the problem. For new
|
||||
features, show how the feature would be used. -->
|
||||
|
||||
### Expected Results:
|
||||
|
||||
<!-- For bug reports, what did you expect to happen when running the steps
|
||||
above? -->
|
||||
|
||||
### Actual Results:
|
||||
|
||||
<!-- For bug reports, what actually happened? -->
|
||||
|
||||
### Additional Information:
|
||||
|
||||
<!-- Include any links to sosreport, database dumps, screenshots or other
|
||||
information. -->
|
||||
@ -32,6 +32,7 @@ from django.http import HttpResponse
|
||||
|
||||
# Django REST Framework
|
||||
from rest_framework.exceptions import PermissionDenied, ParseError
|
||||
from rest_framework.parsers import FormParser
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
@ -2064,6 +2065,7 @@ class JobTemplateCallback(GenericAPIView):
|
||||
model = JobTemplate
|
||||
permission_classes = (JobTemplateCallbackPermission,)
|
||||
serializer_class = EmptySerializer
|
||||
parser_classes = api_settings.DEFAULT_PARSER_CLASSES + [FormParser]
|
||||
|
||||
@csrf_exempt
|
||||
@transaction.non_atomic_requests
|
||||
|
||||
@ -67,7 +67,7 @@ class FactCacheReceiver(object):
|
||||
self.timestamp = datetime.fromtimestamp(date_key, None)
|
||||
|
||||
# Update existing Fact entry
|
||||
fact_obj = Fact.objects.filter(host__id=host_obj.id, module=module_name, timestamp=self.timestamp)
|
||||
fact_obj = Fact.objects.filter(host__id=host_obj.id, module=module_name, timestamp=self.timestamp)
|
||||
if fact_obj:
|
||||
fact_obj.facts = facts
|
||||
fact_obj.save()
|
||||
|
||||
115
awx/main/migrations/0005_v300_changes.py
Normal file
115
awx/main/migrations/0005_v300_changes.py
Normal file
@ -0,0 +1,115 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.utils.timezone import now
|
||||
|
||||
from awx.api.license import feature_enabled
|
||||
|
||||
|
||||
def create_system_job_templates(apps, schema_editor):
|
||||
'''
|
||||
Create default system job templates if not present. Create default schedules
|
||||
only if new system job templates were created (i.e. new database).
|
||||
'''
|
||||
|
||||
SystemJobTemplate = apps.get_model('main', 'SystemJobTemplate')
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
sjt_ct = ContentType.objects.get_for_model(SystemJobTemplate)
|
||||
now_dt = now()
|
||||
now_str = now_dt.strftime('%Y%m%dT%H%M%SZ')
|
||||
|
||||
sjt, created = SystemJobTemplate.objects.get_or_create(
|
||||
job_type='cleanup_jobs',
|
||||
defaults=dict(
|
||||
name='Cleanup Job Details',
|
||||
description='Remove job history older than X days',
|
||||
created=now_dt,
|
||||
modified=now_dt,
|
||||
polymorphic_ctype=sjt_ct,
|
||||
),
|
||||
)
|
||||
if created:
|
||||
sjt.schedules.create(
|
||||
name='Cleanup Job Schedule',
|
||||
rrule='DTSTART:%s RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=SU' % now_str,
|
||||
description='Automatically Generated Schedule',
|
||||
enabled=True,
|
||||
extra_data={'days': '120'},
|
||||
created=now_dt,
|
||||
modified=now_dt,
|
||||
)
|
||||
|
||||
sjt, created = SystemJobTemplate.objects.get_or_create(
|
||||
job_type='cleanup_deleted',
|
||||
defaults=dict(
|
||||
name='Cleanup Deleted Data',
|
||||
description='Remove deleted object history older than X days',
|
||||
created=now_dt,
|
||||
modified=now_dt,
|
||||
polymorphic_ctype=sjt_ct,
|
||||
),
|
||||
)
|
||||
if created:
|
||||
sjt.schedules.create(
|
||||
name='Cleanup Deleted Data Schedule',
|
||||
rrule='DTSTART:%s RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO' % now_str,
|
||||
description='Automatically Generated Schedule',
|
||||
enabled=True,
|
||||
extra_data={'days': '30'},
|
||||
created=now_dt,
|
||||
modified=now_dt,
|
||||
)
|
||||
|
||||
sjt, created = SystemJobTemplate.objects.get_or_create(
|
||||
job_type='cleanup_activitystream',
|
||||
defaults=dict(
|
||||
name='Cleanup Activity Stream',
|
||||
description='Remove activity stream history older than X days',
|
||||
created=now_dt,
|
||||
modified=now_dt,
|
||||
polymorphic_ctype=sjt_ct,
|
||||
),
|
||||
)
|
||||
if created:
|
||||
sjt.schedules.create(
|
||||
name='Cleanup Activity Schedule',
|
||||
rrule='DTSTART:%s RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=TU' % now_str,
|
||||
description='Automatically Generated Schedule',
|
||||
enabled=True,
|
||||
extra_data={'days': '355'},
|
||||
created=now_dt,
|
||||
modified=now_dt,
|
||||
)
|
||||
|
||||
sjt, created = SystemJobTemplate.objects.get_or_create(
|
||||
job_type='cleanup_facts',
|
||||
defaults=dict(
|
||||
name='Cleanup Fact Details',
|
||||
description='Remove system tracking history',
|
||||
created=now_dt,
|
||||
modified=now_dt,
|
||||
polymorphic_ctype=sjt_ct,
|
||||
),
|
||||
)
|
||||
if created and feature_enabled('system_tracking', bypass_database=True):
|
||||
sjt.schedules.create(
|
||||
name='Cleanup Fact Schedule',
|
||||
rrule='DTSTART:%s RRULE:FREQ=MONTHLY;INTERVAL=1;BYMONTHDAY=1' % now_str,
|
||||
description='Automatically Generated Schedule',
|
||||
enabled=True,
|
||||
extra_data={'older_than': '120d', 'granularity': '1w'},
|
||||
created=now_dt,
|
||||
modified=now_dt,
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0004_v300_changes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(create_system_job_templates, migrations.RunPython.noop),
|
||||
]
|
||||
@ -233,7 +233,8 @@ def handle_work_success(self, result, task_actual):
|
||||
task_actual['id'],
|
||||
instance_name,
|
||||
notification_body['url'])
|
||||
send_notifications.delay([n.generate_notification(notification_subject, notification_body)
|
||||
notification_body['friendly_name'] = friendly_name
|
||||
send_notifications.delay([n.generate_notification(notification_subject, notification_body).id
|
||||
for n in set(notifiers.get('success', []) + notifiers.get('any', []))],
|
||||
job_id=task_actual['id'])
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import urllib
|
||||
from multiprocessing import Process
|
||||
from subprocess import Popen
|
||||
import re
|
||||
@ -463,6 +464,8 @@ class BaseTestMixin(QueueTestMixin, MockCommonlySlowTestMixin):
|
||||
response = method(url, json.dumps(data), 'application/json')
|
||||
elif data_type == 'yaml':
|
||||
response = method(url, yaml.safe_dump(data), 'application/yaml')
|
||||
elif data_type == 'form':
|
||||
response = method(url, urllib.urlencode(data), 'application/x-www-form-urlencoded')
|
||||
else:
|
||||
self.fail('Unsupported data_type %s' % data_type)
|
||||
else:
|
||||
|
||||
@ -803,6 +803,21 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
|
||||
self.assertEqual(job.hosts.count(), 1)
|
||||
self.assertEqual(job.hosts.all()[0], host)
|
||||
|
||||
# Create the job itself using URL-encoded form data instead of JSON.
|
||||
result = self.post(url, data, expect=202, remote_addr=host_ip, data_type='form')
|
||||
|
||||
# Establish that we got back what we expect, and made the changes
|
||||
# that we expect.
|
||||
self.assertTrue('Location' in result.response, result.response)
|
||||
self.assertEqual(jobs_qs.count(), 2)
|
||||
job = jobs_qs[0]
|
||||
self.assertEqual(urlparse.urlsplit(result.response['Location']).path,
|
||||
job.get_absolute_url())
|
||||
self.assertEqual(job.launch_type, 'callback')
|
||||
self.assertEqual(job.limit, host.name)
|
||||
self.assertEqual(job.hosts.count(), 1)
|
||||
self.assertEqual(job.hosts.all()[0], host)
|
||||
|
||||
# Run the callback job again with extra vars and verify their presence
|
||||
data.update(dict(extra_vars=dict(key="value")))
|
||||
result = self.post(url, data, expect=202, remote_addr=host_ip)
|
||||
@ -853,9 +868,9 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
|
||||
if host_ip:
|
||||
break
|
||||
self.assertTrue(host)
|
||||
self.assertEqual(jobs_qs.count(), 2)
|
||||
self.post(url, data, expect=202, remote_addr=host_ip)
|
||||
self.assertEqual(jobs_qs.count(), 3)
|
||||
self.post(url, data, expect=202, remote_addr=host_ip)
|
||||
self.assertEqual(jobs_qs.count(), 4)
|
||||
job = jobs_qs[0]
|
||||
self.assertEqual(job.launch_type, 'callback')
|
||||
self.assertEqual(job.limit, host.name)
|
||||
@ -878,9 +893,9 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
|
||||
if host_ip:
|
||||
break
|
||||
self.assertTrue(host)
|
||||
self.assertEqual(jobs_qs.count(), 3)
|
||||
self.post(url, data, expect=202, remote_addr=host_ip)
|
||||
self.assertEqual(jobs_qs.count(), 4)
|
||||
self.post(url, data, expect=202, remote_addr=host_ip)
|
||||
self.assertEqual(jobs_qs.count(), 5)
|
||||
job = jobs_qs[0]
|
||||
self.assertEqual(job.launch_type, 'callback')
|
||||
self.assertEqual(job.limit, host.name)
|
||||
@ -892,9 +907,9 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
|
||||
host_qs = host_qs.filter(variables__icontains='ansible_ssh_host')
|
||||
host = host_qs[0]
|
||||
host_ip = host.variables_dict['ansible_ssh_host']
|
||||
self.assertEqual(jobs_qs.count(), 4)
|
||||
self.post(url, data, expect=202, remote_addr=host_ip)
|
||||
self.assertEqual(jobs_qs.count(), 5)
|
||||
self.post(url, data, expect=202, remote_addr=host_ip)
|
||||
self.assertEqual(jobs_qs.count(), 6)
|
||||
job = jobs_qs[0]
|
||||
self.assertEqual(job.launch_type, 'callback')
|
||||
self.assertEqual(job.limit, host.name)
|
||||
@ -926,9 +941,9 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
|
||||
host_ip = list(ips)[0]
|
||||
break
|
||||
self.assertTrue(host)
|
||||
self.assertEqual(jobs_qs.count(), 5)
|
||||
self.post(url, data, expect=202, remote_addr=host_ip)
|
||||
self.assertEqual(jobs_qs.count(), 6)
|
||||
self.post(url, data, expect=202, remote_addr=host_ip)
|
||||
self.assertEqual(jobs_qs.count(), 7)
|
||||
job = jobs_qs[0]
|
||||
self.assertEqual(job.launch_type, 'callback')
|
||||
self.assertEqual(job.limit, ':&'.join([job_template.limit, host.name]))
|
||||
|
||||
@ -44,7 +44,8 @@
|
||||
@import "text-label.less";
|
||||
@import "./bootstrap-datepicker.less";
|
||||
@import "awx/ui/client/src/shared/branding/colors.default.less";
|
||||
|
||||
// Bootstrap default overrides
|
||||
@import "awx/ui/client/src/shared/bootstrap-settings.less";
|
||||
/* Bootstrap fix that's causing a right margin to appear
|
||||
whenver a modal is opened */
|
||||
body.modal-open {
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
export default
|
||||
['$scope', '$state', 'CheckLicense', function($scope, $state, CheckLicense){
|
||||
var processVersion = function(version){
|
||||
// prettify version & calculate padding
|
||||
// e,g 3.0.0-0.git201602191743/ -> 3.0.0
|
||||
var split = version.split('-')[0]
|
||||
var spaces = Math.floor((16-split.length)/2),
|
||||
paddedStr = "";
|
||||
for(var i=0; i<=spaces; i++){
|
||||
paddedStr = paddedStr +" ";
|
||||
}
|
||||
paddedStr = paddedStr + split;
|
||||
for(var j = paddedStr.length; j<16; j++){
|
||||
paddedStr = paddedStr + " ";
|
||||
}
|
||||
return paddedStr
|
||||
}
|
||||
// prettify version & calculate padding
|
||||
// e,g 3.0.0-0.git201602191743/ -> 3.0.0
|
||||
var split = version.split('-')[0]
|
||||
var spaces = Math.floor((16-split.length)/2),
|
||||
paddedStr = "";
|
||||
for(var i=0; i<=spaces; i++){
|
||||
paddedStr = paddedStr +" ";
|
||||
}
|
||||
paddedStr = paddedStr + split;
|
||||
for(var j = paddedStr.length; j<16; j++){
|
||||
paddedStr = paddedStr + " ";
|
||||
}
|
||||
return paddedStr
|
||||
};
|
||||
var init = function(){
|
||||
CheckLicense.get()
|
||||
.then(function(res){
|
||||
@ -23,9 +23,9 @@ export default
|
||||
$('#about-modal').modal('show');
|
||||
});
|
||||
};
|
||||
var back = function(){
|
||||
$state.go('setup');
|
||||
}
|
||||
$('#about-modal').on('hidden.bs.modal', function () {
|
||||
$state.go('setup');
|
||||
});
|
||||
init();
|
||||
}
|
||||
];
|
||||
@ -3,7 +3,7 @@
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<img class="About-brand--ansible img-responsive" src="/static/assets/ansible_tower_logo_minimalc.png" />
|
||||
<button type="button" class="close About-close" ng-click="back()">
|
||||
<button data-dismiss="modal" type="button" class="close About-close">
|
||||
<span class="fa fa-times-circle"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -30,6 +30,7 @@ import inventoryScripts from './inventory-scripts/main';
|
||||
import permissions from './permissions/main';
|
||||
import managementJobs from './management-jobs/main';
|
||||
import jobDetail from './job-detail/main';
|
||||
import notifications from './notifications/main';
|
||||
|
||||
// modules
|
||||
import about from './about/main';
|
||||
@ -76,7 +77,7 @@ __deferLoadIfEnabled();
|
||||
/*#endif#*/
|
||||
|
||||
var tower = angular.module('Tower', [
|
||||
// 'ngAnimate',
|
||||
//'ngAnimate',
|
||||
'ngSanitize',
|
||||
'ngCookies',
|
||||
about.name,
|
||||
@ -98,6 +99,7 @@ var tower = angular.module('Tower', [
|
||||
activityStream.name,
|
||||
footer.name,
|
||||
jobDetail.name,
|
||||
notifications.name,
|
||||
standardOut.name,
|
||||
'templates',
|
||||
'Utilities',
|
||||
@ -882,13 +884,13 @@ var tower = angular.module('Tower', [
|
||||
}]);
|
||||
}])
|
||||
|
||||
.run(['$q', '$compile', '$cookieStore', '$rootScope', '$log', '$state', 'CheckLicense',
|
||||
.run(['$q', '$compile', '$cookieStore', '$rootScope', '$log', '$state', 'CheckLicense',
|
||||
'$location', 'Authorization', 'LoadBasePaths', 'Timer', 'ClearScope', 'Socket',
|
||||
'LoadConfig', 'Store', 'ShowSocketHelp', 'pendoService',
|
||||
function (
|
||||
$q, $compile, $cookieStore, $rootScope, $log, $state, CheckLicense,
|
||||
$q, $compile, $cookieStore, $rootScope, $log, $state, CheckLicense,
|
||||
$location, Authorization, LoadBasePaths, Timer, ClearScope, Socket,
|
||||
LoadConfig, Store, ShowSocketHelp, pendoService)
|
||||
LoadConfig, Store, ShowSocketHelp, pendoService)
|
||||
{
|
||||
var sock;
|
||||
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
/** @define DashboardCounts */
|
||||
|
||||
.Footer {
|
||||
height: 40px;
|
||||
background-color: #f6f6f6;
|
||||
color: #848992;
|
||||
width: 100%;
|
||||
z-index: 1040;
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
|
||||
@ -4,5 +4,5 @@
|
||||
<img id="footer-logo" alt="Red Hat, Inc. | Ansible, Inc." class="Footer-logoImage" src="/static/assets/footer-logo.png">
|
||||
</div>
|
||||
</a>
|
||||
<div class="Footer-copyright" ng-class="{'is-loggedOut' : !$root.current_user.username}">Copyright © 2015 <a class="Footer-link" href="http://www.redhat.com" target="_blank">Red Hat</a>, Inc. All Rights Reserved.</div>
|
||||
<div class="Footer-copyright" ng-class="{'is-loggedOut' : !$root.current_user.username}">Copyright © 2016 <a class="Footer-link" href="http://www.redhat.com" target="_blank">Red Hat</a>, Inc.</div>
|
||||
</footer>
|
||||
|
||||
@ -20,6 +20,16 @@
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.License-submit--success.ng-hide-add, .License-submit--success.ng-hide-remove {
|
||||
transition: all ease-in-out 0.5s;
|
||||
}
|
||||
.License-submit--success{
|
||||
opacity: 1;
|
||||
transition: all ease-in-out 0.5s;
|
||||
}
|
||||
.License-submit--success.ng-hide{
|
||||
opacity: 0;
|
||||
}
|
||||
.License-eula textarea{
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
@ -33,6 +43,9 @@
|
||||
.License-field{
|
||||
.OnePlusTwo-left--detailsRow;
|
||||
}
|
||||
.License-field + .License-field {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.License-greenText{
|
||||
color: @submit-button-bg;
|
||||
}
|
||||
@ -40,16 +53,16 @@
|
||||
color: #d9534f;
|
||||
}
|
||||
.License-fields{
|
||||
.OnePlusTwo-left--details;
|
||||
.OnePlusTwo-left--details;
|
||||
}
|
||||
.License-details {
|
||||
.OnePlusTwo-left--panel(600px);
|
||||
.OnePlusTwo-left--panel(650px);
|
||||
}
|
||||
.License-titleText {
|
||||
.OnePlusTwo-panelHeader;
|
||||
}
|
||||
.License-management{
|
||||
.OnePlusTwo-right--panel(600px);
|
||||
.OnePlusTwo-right--panel(650px);
|
||||
}
|
||||
.License-submit--container{
|
||||
height: 33px;
|
||||
@ -59,8 +72,25 @@
|
||||
margin: 0 10px 0 0;
|
||||
}
|
||||
.License-file--container {
|
||||
margin: 20px 0 20px 0;
|
||||
input[type=file] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.License-upgradeText {
|
||||
margin: 20px 0px;
|
||||
}
|
||||
.License-body {
|
||||
margin-top: 25px;
|
||||
}
|
||||
.License-subTitleText {
|
||||
text-transform: uppercase;
|
||||
margin: 20px 0px 5px 0px;
|
||||
color: @default-interface-txt;
|
||||
}
|
||||
.License-helperText {
|
||||
color: @default-interface-txt;
|
||||
}
|
||||
.License-input--fake{
|
||||
border-top-right-radius: 4px !important;
|
||||
border-bottom-right-radius: 4px !important;
|
||||
}
|
||||
|
||||
@ -5,9 +5,9 @@
|
||||
*************************************************/
|
||||
|
||||
export default
|
||||
[ 'Wait', '$state', '$scope', '$location',
|
||||
[ 'Wait', '$state', '$scope', '$rootScope', '$location',
|
||||
'GetBasePath', 'Rest', 'ProcessErrors', 'CheckLicense', 'moment',
|
||||
function( Wait, $state, $scope, $location,
|
||||
function( Wait, $state, $scope, $rootScope, $location,
|
||||
GetBasePath, Rest, ProcessErrors, CheckLicense, moment){
|
||||
$scope.getKey = function(event){
|
||||
// Mimic HTML5 spec, show filename
|
||||
@ -16,9 +16,19 @@ export default
|
||||
var raw = new FileReader();
|
||||
// readAsFoo runs async
|
||||
raw.onload = function(){
|
||||
$scope.newLicense.file = JSON.parse(raw.result);
|
||||
try {
|
||||
$scope.newLicense.file = JSON.parse(raw.result);
|
||||
}
|
||||
catch(err) {
|
||||
ProcessErrors($rootScope, null, null, null, {msg: 'Invalid file format. Please upload valid JSON.'});
|
||||
}
|
||||
}
|
||||
try {
|
||||
raw.readAsText(event.target.files[0]);
|
||||
}
|
||||
catch(err) {
|
||||
ProcessErrors($rootScope, null, null, null, {msg: 'Invalid file format. Please upload valid JSON.'});
|
||||
}
|
||||
raw.readAsText(event.target.files[0]);
|
||||
};
|
||||
// HTML5 spec doesn't provide a way to customize file input css
|
||||
// So we hide the default input, show our own, and simulate clicks to the hidden input
|
||||
@ -33,6 +43,11 @@ export default
|
||||
reset();
|
||||
init();
|
||||
$scope.success = true;
|
||||
// for animation purposes
|
||||
var successTimeout = setTimeout(function(){
|
||||
$scope.success = false;
|
||||
clearTimeout(successTimeout);
|
||||
}, 4000);
|
||||
});
|
||||
};
|
||||
var calcDaysRemaining = function(ms){
|
||||
@ -51,6 +66,7 @@ export default
|
||||
CheckLicense.get()
|
||||
.then(function(res){
|
||||
$scope.license = res.data;
|
||||
$scope.license.version = res.data.version.split('-')[0];
|
||||
$scope.time = {};
|
||||
$scope.time.remaining = calcDaysRemaining($scope.license.license_info.time_remaining);
|
||||
$scope.time.expiresOn = calcExpiresOn($scope.time.remaining);
|
||||
|
||||
@ -5,95 +5,98 @@
|
||||
<div class="License-fields">
|
||||
<div class="License-field">
|
||||
<div class="License-field--label">License</div>
|
||||
<div class="License-field--content">
|
||||
<div class="License-field--content">
|
||||
<span ng-show='valid'><i class="fa fa-circle License-greenText"></i> Valid</span>
|
||||
<span ng-show='invalid'><i class="fa fa-circle License-redText"></i> Invalid</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="License-field">
|
||||
<div class="License-field--label">Version</div>
|
||||
<div class="License-field--content">
|
||||
{{license.version}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="License-field">
|
||||
<div class="License-field--label">License Type</div>
|
||||
<div class="License-field--label">Version</div>
|
||||
<div class="License-field--content">
|
||||
{{license.license_info.license_type}}
|
||||
</div>
|
||||
{{license.version || "No result found"}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="License-field">
|
||||
<div class="License-field--label">License Type</div>
|
||||
<div class="License-field--content">
|
||||
{{license.license_info.license_type || "No result found"}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="License-field">
|
||||
<div class="License-field--label">Subscription</div>
|
||||
<div class="License-field--content">
|
||||
{{license.license_info.subscription_name}}
|
||||
</div>
|
||||
<div class="License-field--content">
|
||||
{{license.license_info.subscription_name || "No result found"}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="License-field">
|
||||
<div class="License-field--label">License Key</div>
|
||||
<div class="License-field--content">
|
||||
{{license.license_info.license_key}}
|
||||
</div>
|
||||
<div class="License-field--label">License Key</div>
|
||||
<div class="License-field--content">
|
||||
{{license.license_info.license_key || "No result found"}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="License-field">
|
||||
<div class="License-field--label">Expires On</div>
|
||||
<div class="License-field--content">
|
||||
<div class="License-field--content">
|
||||
{{time.expiresOn}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="License-field">
|
||||
<div class="License-field--label">Time Remaining</div>
|
||||
<div class="License-field--content">
|
||||
{{time.remaining}} Day
|
||||
</div>
|
||||
<div class="License-field--content">
|
||||
{{time.remaining}} Days
|
||||
</div>
|
||||
</div>
|
||||
<div class="License-field">
|
||||
<div class="License-field--label">Hosts Available</div>
|
||||
<div class="License-field--label">Hosts Available</div>
|
||||
<div class="License-field--content">
|
||||
{{license.license_info.available_instances}}
|
||||
</div>
|
||||
{{license.license_info.available_instances || "No result found"}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="License-field">
|
||||
<div class="License-field--label">Hosts Used</div>
|
||||
<div class="License-field--content">
|
||||
{{license.license_info.current_instances}}
|
||||
</div>
|
||||
<div class="License-field--content">
|
||||
{{license.license_info.current_instances || "No result found"}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="License-field License-greenText">
|
||||
<div class="License-field--label">Hosts Remaining</div>
|
||||
<div class="License-field--content">
|
||||
{{license.license_info.free_instances}}
|
||||
</div>
|
||||
<div class="License-field--content">
|
||||
{{license.license_info.free_instances || "No result found"}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>If you are ready to upgrade, please contact us by clicking the button below</p>
|
||||
<div class="License-upgradeText">If you are ready to upgrade, please contact us by clicking the button below</div>
|
||||
<a href="https://www.ansible.com/renew" target="_blank"><button class="btn btn-default">Upgrade</button></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="License-management">
|
||||
<div class="Panel">
|
||||
<div class="License-titleText">License Management</div>
|
||||
<p>Choose your license file, agree to the End User License Agreement, and click submit.</p>
|
||||
<form id="License-form" name="license">
|
||||
<div class="input-group License-file--container">
|
||||
<span class="btn btn-default input-group-addon" ng-click="fakeClick()">Browse...</span>
|
||||
<input class="form-control" ng-disabled="true" placeholder="{{fileName}}" />
|
||||
<input id="License-file" class="form-control" type="file" file-on-change="getKey"/>
|
||||
</div>
|
||||
<div class="License-titleText prepend-asterisk"> End User License Agreement</div>
|
||||
<div class="form-group License-eula">
|
||||
<textarea class="form-control">{{license.eula}}
|
||||
</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<div class="License-details--label"><input type="checkbox" ng-model="newLicense.eula" required> I agree to the End User License Agreement</div>
|
||||
<div class="License-submit--container pull-right">
|
||||
<span ng-hide="success == null || false" class="License-greenText License-submit--success pull-left">Save successful!</span>
|
||||
<button ng-click="submit()" class="btn btn-success pull-right" ng-disabled="newLicense.file.license_key == null || newLicense.eula == null">Submit</button>
|
||||
<div class="License-body">
|
||||
<p class="License-helperText">Choose your license file, agree to the End User License Agreement, and click submit.</p>
|
||||
<form id="License-form" name="license">
|
||||
<div class="License-subTitleText prepend-asterisk"> License File</div>
|
||||
<div class="input-group License-file--container">
|
||||
<span class="btn btn-default input-group-addon" ng-click="fakeClick()">Browse...</span>
|
||||
<input class="form-control License-input--fake" ng-disabled="true" placeholder="{{fileName}}" />
|
||||
<input id="License-file" class="form-control" type="file" file-on-change="getKey"/>
|
||||
</div>
|
||||
<div class="License-subTitleText prepend-asterisk"> End User License Agreement</div>
|
||||
<div class="form-group License-eula">
|
||||
<textarea class="form-control">{{license.eula}}
|
||||
</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<div class="License-details--label"><input type="checkbox" ng-model="newLicense.eula" required> I agree to the End User License Agreement</div>
|
||||
<div class="License-submit--container pull-right">
|
||||
<span ng-show="success == true" class="License-greenText License-submit--success pull-left">Save successful!</span>
|
||||
<button ng-click="submit()" class="btn btn-success pull-right" ng-disabled="newLicense.file.license_key == null || newLicense.eula == null">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
67
awx/ui/client/src/notifications/add/add.controller.js
Normal file
67
awx/ui/client/src/notifications/add/add.controller.js
Normal file
@ -0,0 +1,67 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
export default
|
||||
[ '$rootScope', 'pagination', '$compile','SchedulerInit', 'Rest', 'Wait',
|
||||
'notificationsFormObject', 'ProcessErrors', 'GetBasePath', 'Empty',
|
||||
'GenerateForm', 'SearchInit' , 'PaginateInit',
|
||||
'LookUpInit', 'OrganizationList', '$scope', '$state',
|
||||
function(
|
||||
$rootScope, pagination, $compile, SchedulerInit, Rest, Wait,
|
||||
notificationsFormObject, ProcessErrors, GetBasePath, Empty,
|
||||
GenerateForm, SearchInit, PaginateInit,
|
||||
LookUpInit, OrganizationList, $scope, $state
|
||||
) {
|
||||
var scope = $scope,
|
||||
generator = GenerateForm,
|
||||
form = notificationsFormObject,
|
||||
url = GetBasePath('notifications');
|
||||
|
||||
generator.inject(form, {
|
||||
mode: 'add' ,
|
||||
scope:scope,
|
||||
related: false
|
||||
});
|
||||
generator.reset();
|
||||
|
||||
LookUpInit({
|
||||
url: GetBasePath('organization'),
|
||||
scope: scope,
|
||||
form: form,
|
||||
list: OrganizationList,
|
||||
field: 'organization',
|
||||
input_type: 'radio'
|
||||
});
|
||||
|
||||
// Save
|
||||
scope.formSave = function () {
|
||||
|
||||
generator.clearApiErrors();
|
||||
Wait('start');
|
||||
Rest.setUrl(url);
|
||||
Rest.post({
|
||||
name: scope.name,
|
||||
description: scope.description,
|
||||
organization: scope.organization,
|
||||
script: scope.script
|
||||
})
|
||||
.success(function (data) {
|
||||
$rootScope.addedItem = data.id;
|
||||
$state.go('inventoryScripts', {}, {reload: true});
|
||||
Wait('stop');
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors(scope, data, status, form, { hdr: 'Error!',
|
||||
msg: 'Failed to add new inventory script. POST returned status: ' + status });
|
||||
});
|
||||
};
|
||||
|
||||
scope.formCancel = function () {
|
||||
$state.transitionTo('inventoryScripts');
|
||||
};
|
||||
|
||||
}
|
||||
];
|
||||
3
awx/ui/client/src/notifications/add/add.partial.html
Normal file
3
awx/ui/client/src/notifications/add/add.partial.html
Normal file
@ -0,0 +1,3 @@
|
||||
<div class="tab-pane" id="notifications_add">
|
||||
<div ng-cloak id="htmlTemplate" class="Panel"></div>
|
||||
</div>
|
||||
23
awx/ui/client/src/notifications/add/add.route.js
Normal file
23
awx/ui/client/src/notifications/add/add.route.js
Normal file
@ -0,0 +1,23 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import {templateUrl} from '../../shared/template-url/template-url.factory';
|
||||
|
||||
export default {
|
||||
name: 'notifications.add',
|
||||
route: '/add',
|
||||
templateUrl: templateUrl('notifications/add/add'),
|
||||
controller: 'notificationsAddController',
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: 'notifications',
|
||||
label: 'Create Notification'
|
||||
}
|
||||
};
|
||||
15
awx/ui/client/src/notifications/add/main.js
Normal file
15
awx/ui/client/src/notifications/add/main.js
Normal file
@ -0,0 +1,15 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import route from './add.route';
|
||||
import controller from './add.controller';
|
||||
|
||||
export default
|
||||
angular.module('notificationsAdd', [])
|
||||
.controller('notificationsAddController', controller)
|
||||
.run(['$stateExtender', function($stateExtender) {
|
||||
$stateExtender.addState(route);
|
||||
}]);
|
||||
97
awx/ui/client/src/notifications/edit/edit.controller.js
Normal file
97
awx/ui/client/src/notifications/edit/edit.controller.js
Normal file
@ -0,0 +1,97 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
export default
|
||||
[ 'Rest', 'Wait',
|
||||
'notificationsFormObject', 'ProcessErrors', 'GetBasePath',
|
||||
'GenerateForm', 'SearchInit' , 'PaginateInit',
|
||||
'LookUpInit', 'OrganizationList', 'inventory_script',
|
||||
'$scope', '$state',
|
||||
function(
|
||||
Rest, Wait,
|
||||
notificationsFormObject, ProcessErrors, GetBasePath,
|
||||
GenerateForm, SearchInit, PaginateInit,
|
||||
LookUpInit, OrganizationList, inventory_script,
|
||||
$scope, $state
|
||||
) {
|
||||
var generator = GenerateForm,
|
||||
id = inventory_script.id,
|
||||
form = notificationsFormObject,
|
||||
master = {},
|
||||
url = GetBasePath('notifications');
|
||||
|
||||
$scope.inventory_script = inventory_script;
|
||||
generator.inject(form, {
|
||||
mode: 'edit' ,
|
||||
scope:$scope,
|
||||
related: false,
|
||||
activityStream: false
|
||||
});
|
||||
generator.reset();
|
||||
LookUpInit({
|
||||
url: GetBasePath('organization'),
|
||||
scope: $scope,
|
||||
form: form,
|
||||
// hdr: "Select Custom Inventory",
|
||||
list: OrganizationList,
|
||||
field: 'organization',
|
||||
input_type: 'radio'
|
||||
});
|
||||
|
||||
// Retrieve detail record and prepopulate the form
|
||||
Wait('start');
|
||||
Rest.setUrl(url + id+'/');
|
||||
Rest.get()
|
||||
.success(function (data) {
|
||||
var fld;
|
||||
for (fld in form.fields) {
|
||||
if (data[fld]) {
|
||||
$scope[fld] = data[fld];
|
||||
master[fld] = data[fld];
|
||||
}
|
||||
|
||||
if (form.fields[fld].sourceModel && data.summary_fields &&
|
||||
data.summary_fields[form.fields[fld].sourceModel]) {
|
||||
$scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
|
||||
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
|
||||
master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] =
|
||||
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
|
||||
}
|
||||
}
|
||||
Wait('stop');
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors($scope, data, status, form, { hdr: 'Error!',
|
||||
msg: 'Failed to retrieve inventory script: ' + id + '. GET status: ' + status });
|
||||
});
|
||||
|
||||
$scope.formSave = function () {
|
||||
generator.clearApiErrors();
|
||||
Wait('start');
|
||||
Rest.setUrl(url+ id+'/');
|
||||
Rest.put({
|
||||
name: $scope.name,
|
||||
description: $scope.description,
|
||||
organization: $scope.organization,
|
||||
script: $scope.script
|
||||
})
|
||||
.success(function () {
|
||||
$state.transitionTo('inventoryScriptsList');
|
||||
Wait('stop');
|
||||
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors($scope, data, status, form, { hdr: 'Error!',
|
||||
msg: 'Failed to add new inventory script. PUT returned status: ' + status });
|
||||
});
|
||||
};
|
||||
|
||||
$scope.formCancel = function () {
|
||||
$state.transitionTo('inventoryScripts');
|
||||
};
|
||||
|
||||
}
|
||||
];
|
||||
3
awx/ui/client/src/notifications/edit/edit.partial.html
Normal file
3
awx/ui/client/src/notifications/edit/edit.partial.html
Normal file
@ -0,0 +1,3 @@
|
||||
<div class="tab-pane" id="notficiations_edit">
|
||||
<div ng-cloak id="htmlTemplate" class="Panel"></div>
|
||||
</div>
|
||||
23
awx/ui/client/src/notifications/edit/edit.route.js
Normal file
23
awx/ui/client/src/notifications/edit/edit.route.js
Normal file
@ -0,0 +1,23 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import {templateUrl} from '../../shared/template-url/template-url.factory';
|
||||
|
||||
export default {
|
||||
name: 'notifications.edit',
|
||||
route: '/edit',
|
||||
templateUrl: templateUrl('notifications/edit/edit'),
|
||||
controller: 'notificationsEditController',
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: 'notifications',
|
||||
label: 'Edit Notification'
|
||||
}
|
||||
};
|
||||
15
awx/ui/client/src/notifications/edit/main.js
Normal file
15
awx/ui/client/src/notifications/edit/main.js
Normal file
@ -0,0 +1,15 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import route from './edit.route';
|
||||
import controller from './edit.controller';
|
||||
|
||||
export default
|
||||
angular.module('notificationsEdit', [])
|
||||
.controller('notificationsEditController', controller)
|
||||
.run(['$stateExtender', function($stateExtender) {
|
||||
$stateExtender.addState(route);
|
||||
}]);
|
||||
83
awx/ui/client/src/notifications/list/list.controller.js
Normal file
83
awx/ui/client/src/notifications/list/list.controller.js
Normal file
@ -0,0 +1,83 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
export default
|
||||
[ '$rootScope','Wait', 'generateList', 'notificationsListObject',
|
||||
'GetBasePath' , 'SearchInit' , 'PaginateInit',
|
||||
'Rest' , 'ProcessErrors', 'Prompt', '$state',
|
||||
function(
|
||||
$rootScope,Wait, GenerateList, notificationsListObject,
|
||||
GetBasePath, SearchInit, PaginateInit,
|
||||
Rest, ProcessErrors, Prompt, $state
|
||||
) {
|
||||
var scope = $rootScope.$new(),
|
||||
defaultUrl = GetBasePath('notifications'),
|
||||
list = notificationsListObject,
|
||||
view = GenerateList;
|
||||
|
||||
view.inject( list, {
|
||||
mode: 'edit',
|
||||
scope: scope
|
||||
});
|
||||
|
||||
// SearchInit({
|
||||
// scope: scope,
|
||||
// set: 'notifications',
|
||||
// list: list,
|
||||
// url: defaultUrl
|
||||
// });
|
||||
//
|
||||
// if ($rootScope.addedItem) {
|
||||
// scope.addedItem = $rootScope.addedItem;
|
||||
// delete $rootScope.addedItem;
|
||||
// }
|
||||
// PaginateInit({
|
||||
// scope: scope,
|
||||
// list: list,
|
||||
// url: defaultUrl
|
||||
// });
|
||||
//
|
||||
// scope.search(list.iterator);
|
||||
|
||||
scope.editNotification = function(){
|
||||
$state.transitionTo('notifications.edit',{
|
||||
inventory_script_id: this.inventory_script.id,
|
||||
inventory_script: this.inventory_script
|
||||
});
|
||||
};
|
||||
|
||||
scope.deleteNotification = function(id, name){
|
||||
|
||||
var action = function () {
|
||||
$('#prompt-modal').modal('hide');
|
||||
Wait('start');
|
||||
var url = defaultUrl + id + '/';
|
||||
Rest.setUrl(url);
|
||||
Rest.destroy()
|
||||
.success(function () {
|
||||
scope.search(list.iterator);
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status });
|
||||
});
|
||||
};
|
||||
|
||||
var bodyHtml = '<div class="Prompt-bodyQuery">Are you sure you want to delete the inventory script below?</div><div class="Prompt-bodyTarget">' + name + '</div>';
|
||||
Prompt({
|
||||
hdr: 'Delete',
|
||||
body: bodyHtml,
|
||||
action: action,
|
||||
actionText: 'DELETE'
|
||||
});
|
||||
};
|
||||
|
||||
scope.addNotification = function(){
|
||||
$state.transitionTo('notifications.add');
|
||||
};
|
||||
|
||||
}
|
||||
];
|
||||
4
awx/ui/client/src/notifications/list/list.partial.html
Normal file
4
awx/ui/client/src/notifications/list/list.partial.html
Normal file
@ -0,0 +1,4 @@
|
||||
<div class="tab-pane" id="notifications">
|
||||
<div ui-view></div>
|
||||
<div ng-cloak id="htmlTemplate" class="Panel"></div>
|
||||
</div>
|
||||
19
awx/ui/client/src/notifications/list/list.route.js
Normal file
19
awx/ui/client/src/notifications/list/list.route.js
Normal file
@ -0,0 +1,19 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import {templateUrl} from '../../shared/template-url/template-url.factory';
|
||||
|
||||
export default {
|
||||
name: 'notifications',
|
||||
route: '/notifications',
|
||||
templateUrl: templateUrl('notifications/list/list'),
|
||||
controller: 'notificationsListController',
|
||||
resolve: {
|
||||
features: ['FeaturesService', function(FeaturesService) {
|
||||
return FeaturesService.get();
|
||||
}]
|
||||
}
|
||||
};
|
||||
15
awx/ui/client/src/notifications/list/main.js
Normal file
15
awx/ui/client/src/notifications/list/main.js
Normal file
@ -0,0 +1,15 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
import route from './list.route';
|
||||
import controller from './list.controller';
|
||||
|
||||
export default
|
||||
angular.module('notificationsList', [])
|
||||
.controller('notificationsListController', controller)
|
||||
.run(['$stateExtender', function($stateExtender) {
|
||||
$stateExtender.addState(route);
|
||||
}]);
|
||||
22
awx/ui/client/src/notifications/main.js
Normal file
22
awx/ui/client/src/notifications/main.js
Normal file
@ -0,0 +1,22 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2016 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
|
||||
import notificationsList from './list/main';
|
||||
import notificationsAdd from './add/main';
|
||||
import notificationsEdit from './edit/main';
|
||||
|
||||
import list from './notifications.list';
|
||||
import form from './notifications.form';
|
||||
|
||||
export default
|
||||
angular.module('notifications', [
|
||||
notificationsList.name,
|
||||
notificationsAdd.name,
|
||||
notificationsEdit.name
|
||||
])
|
||||
.factory('notificationsListObject', list)
|
||||
.factory('notificationsFormObject', form);
|
||||
47
awx/ui/client/src/notifications/notifications.form.js
Normal file
47
awx/ui/client/src/notifications/notifications.form.js
Normal file
@ -0,0 +1,47 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name forms.function:CustomInventory
|
||||
* @description This form is for adding/editing an organization
|
||||
*/
|
||||
|
||||
export default function() {
|
||||
return {
|
||||
|
||||
addTitle: 'New Notification',
|
||||
editTitle: '{{ name }}',
|
||||
name: 'notification',
|
||||
showActions: true,
|
||||
|
||||
fields: {
|
||||
name: {
|
||||
label: 'Name',
|
||||
type: 'text',
|
||||
addRequired: true,
|
||||
editRequired: true,
|
||||
capitalize: false
|
||||
},
|
||||
description: {
|
||||
label: 'Description',
|
||||
type: 'text',
|
||||
addRequired: false,
|
||||
editRequired: false
|
||||
}
|
||||
},
|
||||
|
||||
buttons: { //for now always generates <button> tags
|
||||
save: {
|
||||
ngClick: 'formSave()', //$scope.function to call on click, optional
|
||||
ngDisabled: true //Disable when $pristine or $invalid, optional
|
||||
},
|
||||
cancel: {
|
||||
ngClick: 'formCancel()',
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
63
awx/ui/client/src/notifications/notifications.list.js
Normal file
63
awx/ui/client/src/notifications/notifications.list.js
Normal file
@ -0,0 +1,63 @@
|
||||
/*************************************************
|
||||
* Copyright (c) 2015 Ansible, Inc.
|
||||
*
|
||||
* All Rights Reserved
|
||||
*************************************************/
|
||||
|
||||
|
||||
|
||||
export default function(){
|
||||
return {
|
||||
name: 'notifications' ,
|
||||
listTitle: 'Notifications',
|
||||
iterator: 'notification',
|
||||
index: false,
|
||||
hover: false,
|
||||
|
||||
fields: {
|
||||
name: {
|
||||
key: true,
|
||||
label: 'Name',
|
||||
columnClass: 'col-md-3 col-sm-9 col-xs-9',
|
||||
modalColumnClass: 'col-md-8'
|
||||
},
|
||||
description: {
|
||||
label: 'Description',
|
||||
excludeModal: true,
|
||||
columnClass: 'col-md-4 hidden-sm hidden-xs'
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
add: {
|
||||
mode: 'all', // One of: edit, select, all
|
||||
ngClick: 'addNotification()',
|
||||
awToolTip: 'Create a new custom inventory',
|
||||
actionClass: 'btn List-buttonSubmit',
|
||||
buttonContent: '+ ADD'
|
||||
}
|
||||
},
|
||||
|
||||
fieldActions: {
|
||||
|
||||
columnClass: 'col-md-2 col-sm-3 col-xs-3',
|
||||
|
||||
edit: {
|
||||
ngClick: "editNotification(inventory_script.id)",
|
||||
icon: 'fa-edit',
|
||||
label: 'Edit',
|
||||
"class": 'btn-sm',
|
||||
awToolTip: 'Edit credential',
|
||||
dataPlacement: 'top'
|
||||
},
|
||||
"delete": {
|
||||
ngClick: "deleteNotification(notification.id, notification.name)",
|
||||
icon: 'fa-trash',
|
||||
label: 'Delete',
|
||||
"class": 'btn-sm',
|
||||
awToolTip: 'Delete credential',
|
||||
dataPlacement: 'top'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -36,6 +36,12 @@
|
||||
Create and edit scripts to dynamically load hosts from any source.
|
||||
</p>
|
||||
</a>
|
||||
<a ui-sref="notifications" class="SetupItem">
|
||||
<h4 class="SetupItem-title">Notifications</h4>
|
||||
<p class="SetupItem-description">
|
||||
Create templates for sending notifications with Email, HipChat, Slack, and SMS.
|
||||
</p>
|
||||
</a>
|
||||
<a ui-sref="license" class="SetupItem">
|
||||
<h4 class="SetupItem-title">View Your License</h4>
|
||||
<p class="SetupItem-description">
|
||||
|
||||
22
awx/ui/client/src/shared/bootstrap-settings.less
vendored
Normal file
22
awx/ui/client/src/shared/bootstrap-settings.less
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
@import "awx/ui/client/src/shared/branding/colors.default.less";
|
||||
.btn-success{
|
||||
background: @default-succ;
|
||||
border-color: transparent;
|
||||
:hover{
|
||||
background: @default-succ-hov;
|
||||
}
|
||||
:disabled{
|
||||
background: @default-succ-disabled;
|
||||
}
|
||||
}
|
||||
.btn-default{
|
||||
background: @btn-bg;
|
||||
border-color: @btn-bord;
|
||||
color: @btn-txt;
|
||||
:hover{
|
||||
background: @btn-bg-hov;
|
||||
}
|
||||
:focus{
|
||||
background: @btn-bg-sel;
|
||||
}
|
||||
}
|
||||
@ -22,25 +22,24 @@
|
||||
flex: 0 0;
|
||||
height: @height;
|
||||
width: 100%;
|
||||
margin-right: 20px;
|
||||
.Panel{
|
||||
height: 100%;
|
||||
}
|
||||
@media screen and (min-width: @breakpoint){
|
||||
max-width: 400px;
|
||||
}
|
||||
@media screen and (max-width: @breakpoint){
|
||||
margin-right: 0px;
|
||||
height: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.OnePlusTwo-right--panel(@height: 100%; @breakpoint: 900px) {
|
||||
height: @height;
|
||||
flex: 1 0;
|
||||
margin-left: 20px;
|
||||
.Panel{
|
||||
height: 100%;
|
||||
}
|
||||
@media screen and (max-width: @breakpoint){
|
||||
flex-direction: column;
|
||||
margin-left: 0px;
|
||||
margin-top: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,6 +49,7 @@
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
text-transform: uppercase;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.OnePlusTwo-left--details {
|
||||
@ -58,9 +58,6 @@
|
||||
|
||||
.OnePlusTwo-left--detailsRow {
|
||||
display: flex;
|
||||
:not(:last-child){
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.OnePlusTwo-left--detailsLabel {
|
||||
@ -73,7 +70,6 @@
|
||||
|
||||
.OnePlusTwo-left--detailsContent {
|
||||
display: inline-block;
|
||||
max-width: 220px;
|
||||
width: 220px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<div class="tab-pane" id="jobs-stdout">
|
||||
<div ng-cloak id="htmlTemplate">
|
||||
<div class="StandardOut">
|
||||
<div class="StandardOut-container">
|
||||
<div class="StandardOut-leftPanel" ng-show="!stdoutFullScreen">
|
||||
<div class="Panel">
|
||||
<div class="StandardOut-panelHeader">
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<div class="tab-pane" id="jobs-stdout">
|
||||
<div ng-cloak id="htmlTemplate">
|
||||
<div class="StandardOut">
|
||||
<div class="StandardOut-container">
|
||||
<div class="StandardOut-leftPanel" ng-show="!stdoutFullScreen">
|
||||
<div class="Panel">
|
||||
<div class="StandardOut-panelHeader">
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<div class="tab-pane" id="jobs-stdout">
|
||||
<div ng-cloak id="htmlTemplate">
|
||||
<div class="StandardOut">
|
||||
<div class="StandardOut-container">
|
||||
<div class="StandardOut-leftPanel" ng-show="!stdoutFullScreen">
|
||||
<div class="Panel">
|
||||
<div class="StandardOut-panelHeader">
|
||||
@ -49,13 +49,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TODO: figure out how to show the extra vars on different rows like the mockup -->
|
||||
|
||||
<div class="StandardOut-detailsRow" ng-show="job.extra_vars">
|
||||
<div class="StandardOut-detailsRow--extraVars" ng-show="job.extra_vars">
|
||||
<div class="StandardOut-detailsLabel">EXTRA VARS</div>
|
||||
<div class="StandardOut-detailsContent">
|
||||
{{ job.extra_vars }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="job.extra_vars">
|
||||
<textarea rows="6" ng-model="variables" name="variables" class="StandardOut-extraVars" id="pre-formatted-variables"></textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<div class="tab-pane" id="jobs-stdout">
|
||||
<div ng-cloak id="htmlTemplate">
|
||||
<div class="StandardOut">
|
||||
<div class="StandardOut-container">
|
||||
<div class="StandardOut-leftPanel" ng-show="!stdoutFullScreen">
|
||||
<div class="Panel">
|
||||
<div class="StandardOut-panelHeader">
|
||||
|
||||
@ -1,28 +1,22 @@
|
||||
@import "../shared/branding/colors.default.less";
|
||||
@import "awx/ui/client/src/shared/layouts/one-plus-two.less";
|
||||
|
||||
/** @define StandardOut */
|
||||
|
||||
.StandardOut {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
.StandardOut-container {
|
||||
.OnePlusTwo-container;
|
||||
}
|
||||
|
||||
.StandardOut-leftPanel {
|
||||
flex: 0 0 400px;
|
||||
margin-right: 20px;
|
||||
.OnePlusTwo-left--panel(590px);
|
||||
}
|
||||
|
||||
.StandardOut-rightPanel {
|
||||
flex: 1 0;
|
||||
.OnePlusTwo-right--panel(590px);
|
||||
}
|
||||
|
||||
.StandardOut-panelHeader {
|
||||
color: @default-interface-txt;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
display: flex;
|
||||
.OnePlusTwo-panelHeader
|
||||
}
|
||||
|
||||
.StandardOut-consoleOutput {
|
||||
@ -30,31 +24,32 @@
|
||||
min-height: 200px;
|
||||
background-color: @default-secondary-bg;
|
||||
border-radius: 5px;
|
||||
height: 300px;
|
||||
height: ~"calc(100% - 74px)";
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.StandardOut-details {
|
||||
margin-top: 25px;
|
||||
.OnePlusTwo-left--details;
|
||||
}
|
||||
|
||||
.StandardOut-detailsRow {
|
||||
display: flex;
|
||||
.OnePlusTwo-left--detailsRow;
|
||||
}
|
||||
|
||||
.StandardOut-detailsRow:not(:last-child) {
|
||||
margin-bottom: 20px;
|
||||
.StandardOut-detailsRow + .StandardOut-detailsRow {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.StandardOut-detailsRow--extraVars {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.StandardOut-detailsLabel {
|
||||
width: 130px;
|
||||
flex: 0 0 130px;
|
||||
color: @default-interface-txt;
|
||||
text-transform: uppercase;
|
||||
.OnePlusTwo-left--detailsLabel;
|
||||
}
|
||||
|
||||
.StandardOut-detailsContent {
|
||||
flex: 1 0;
|
||||
.OnePlusTwo-left--detailsContent;
|
||||
}
|
||||
|
||||
.StandardOut-statusText {
|
||||
@ -66,7 +61,7 @@
|
||||
}
|
||||
|
||||
.StandardOut-preContainer {
|
||||
height: 300px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.StandardOut-panelHeaderText {
|
||||
@ -105,14 +100,3 @@
|
||||
.StandardOut-actionButton + a {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
@standardout-breakpoint: 900px;
|
||||
|
||||
@media screen and (max-width: @standardout-breakpoint) {
|
||||
.StandardOut {
|
||||
flex-direction: column;
|
||||
}
|
||||
.StandardOut-leftPanel {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
*/
|
||||
|
||||
|
||||
export function JobStdoutController ($rootScope, $scope, $state, $stateParams, ClearScope, GetBasePath, Rest, ProcessErrors, Empty, GetChoices, LookUpName) {
|
||||
export function JobStdoutController ($rootScope, $scope, $state, $stateParams, ClearScope, GetBasePath, Rest, ProcessErrors, Empty, GetChoices, LookUpName, ParseTypeChange, ParseVariableString) {
|
||||
|
||||
ClearScope();
|
||||
|
||||
@ -32,118 +32,128 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams, C
|
||||
$scope.job.status = data.status;
|
||||
}
|
||||
|
||||
// TODO: when the job completes we should refresh the job data so that we pull in the finish
|
||||
// timestamp as well as the run time.
|
||||
if (data.status === 'failed' || data.status === 'canceled' || data.status === 'error' || data.status === 'successful') {
|
||||
// Go out and refresh the job details
|
||||
getJobDetails();
|
||||
}
|
||||
});
|
||||
|
||||
// Go out and get the job details based on the job type. jobType gets defined
|
||||
// in the data block of the route declaration for each of the different types
|
||||
// of stdout jobs.
|
||||
Rest.setUrl(GetBasePath('base') + jobType + '/' + job_id + '/');
|
||||
Rest.get()
|
||||
.success(function(data) {
|
||||
$scope.job = data;
|
||||
$scope.job_template_name = data.name;
|
||||
$scope.created_by = data.summary_fields.created_by;
|
||||
$scope.project_name = (data.summary_fields.project) ? data.summary_fields.project.name : '';
|
||||
$scope.inventory_name = (data.summary_fields.inventory) ? data.summary_fields.inventory.name : '';
|
||||
$scope.job_template_url = '/#/job_templates/' + data.unified_job_template;
|
||||
$scope.inventory_url = ($scope.inventory_name && data.inventory) ? '/#/inventories/' + data.inventory : '';
|
||||
$scope.project_url = ($scope.project_name && data.project) ? '/#/projects/' + data.project : '';
|
||||
$scope.credential_name = (data.summary_fields.credential) ? data.summary_fields.credential.name : '';
|
||||
$scope.credential_url = (data.credential) ? '/#/credentials/' + data.credential : '';
|
||||
$scope.cloud_credential_url = (data.cloud_credential) ? '/#/credentials/' + data.cloud_credential : '';
|
||||
$scope.playbook = data.playbook;
|
||||
$scope.credential = data.credential;
|
||||
$scope.cloud_credential = data.cloud_credential;
|
||||
$scope.forks = data.forks;
|
||||
$scope.limit = data.limit;
|
||||
$scope.verbosity = data.verbosity;
|
||||
$scope.job_tags = data.job_tags;
|
||||
// Set the parse type so that CodeMirror knows how to display extra params YAML/JSON
|
||||
$scope.parseType = 'yaml';
|
||||
|
||||
// If we have a source then we have to go get the source choices from the server
|
||||
if (!Empty(data.source)) {
|
||||
if ($scope.removeChoicesReady) {
|
||||
$scope.removeChoicesReady();
|
||||
}
|
||||
$scope.removeChoicesReady = $scope.$on('ChoicesReady', function() {
|
||||
$scope.source_choices.every(function(e) {
|
||||
if (e.value === data.source) {
|
||||
$scope.source = e.label;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
function getJobDetails() {
|
||||
|
||||
// Go out and get the job details based on the job type. jobType gets defined
|
||||
// in the data block of the route declaration for each of the different types
|
||||
// of stdout jobs.
|
||||
Rest.setUrl(GetBasePath('base') + jobType + '/' + job_id + '/');
|
||||
Rest.get()
|
||||
.success(function(data) {
|
||||
$scope.job = data;
|
||||
$scope.job_template_name = data.name;
|
||||
$scope.created_by = data.summary_fields.created_by;
|
||||
$scope.project_name = (data.summary_fields.project) ? data.summary_fields.project.name : '';
|
||||
$scope.inventory_name = (data.summary_fields.inventory) ? data.summary_fields.inventory.name : '';
|
||||
$scope.job_template_url = '/#/job_templates/' + data.unified_job_template;
|
||||
$scope.inventory_url = ($scope.inventory_name && data.inventory) ? '/#/inventories/' + data.inventory : '';
|
||||
$scope.project_url = ($scope.project_name && data.project) ? '/#/projects/' + data.project : '';
|
||||
$scope.credential_name = (data.summary_fields.credential) ? data.summary_fields.credential.name : '';
|
||||
$scope.credential_url = (data.credential) ? '/#/credentials/' + data.credential : '';
|
||||
$scope.cloud_credential_url = (data.cloud_credential) ? '/#/credentials/' + data.cloud_credential : '';
|
||||
$scope.playbook = data.playbook;
|
||||
$scope.credential = data.credential;
|
||||
$scope.cloud_credential = data.cloud_credential;
|
||||
$scope.forks = data.forks;
|
||||
$scope.limit = data.limit;
|
||||
$scope.verbosity = data.verbosity;
|
||||
$scope.job_tags = data.job_tags;
|
||||
|
||||
// If we have a source then we have to go get the source choices from the server
|
||||
if (!Empty(data.source)) {
|
||||
if ($scope.removeChoicesReady) {
|
||||
$scope.removeChoicesReady();
|
||||
}
|
||||
$scope.removeChoicesReady = $scope.$on('ChoicesReady', function() {
|
||||
$scope.source_choices.every(function(e) {
|
||||
if (e.value === data.source) {
|
||||
$scope.source = e.label;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
// GetChoices can be found in the helper: Utilities.js
|
||||
// It attaches the source choices to $scope.source_choices.
|
||||
// Then, when the callback is fired, $scope.source is bound
|
||||
// to the corresponding label.
|
||||
GetChoices({
|
||||
scope: $scope,
|
||||
url: GetBasePath('inventory_sources'),
|
||||
field: 'source',
|
||||
variable: 'source_choices',
|
||||
choice_name: 'choices',
|
||||
callback: 'ChoicesReady'
|
||||
});
|
||||
});
|
||||
// GetChoices can be found in the helper: StandardOut.js
|
||||
// It attaches the source choices to $scope.source_choices.
|
||||
// Then, when the callback is fired, $scope.source is bound
|
||||
// to the corresponding label.
|
||||
GetChoices({
|
||||
scope: $scope,
|
||||
url: GetBasePath('inventory_sources'),
|
||||
field: 'source',
|
||||
variable: 'source_choices',
|
||||
choice_name: 'choices',
|
||||
callback: 'ChoicesReady'
|
||||
});
|
||||
}
|
||||
|
||||
// LookUpName can be found in the helper: StandardOut.js
|
||||
// It attaches the name that it gets (based on the url)
|
||||
// to the $scope variable defined by the attribute scope_var.
|
||||
if (!Empty(data.credential)) {
|
||||
LookUpName({
|
||||
scope: $scope,
|
||||
scope_var: 'credential',
|
||||
url: GetBasePath('credentials') + data.credential + '/'
|
||||
});
|
||||
}
|
||||
|
||||
if (!Empty(data.inventory)) {
|
||||
LookUpName({
|
||||
scope: $scope,
|
||||
scope_var: 'inventory',
|
||||
url: GetBasePath('inventory') + data.inventory + '/'
|
||||
});
|
||||
}
|
||||
|
||||
if (!Empty(data.project)) {
|
||||
LookUpName({
|
||||
scope: $scope,
|
||||
scope_var: 'project',
|
||||
url: GetBasePath('projects') + data.project + '/'
|
||||
});
|
||||
}
|
||||
|
||||
if (!Empty(data.cloud_credential)) {
|
||||
LookUpName({
|
||||
scope: $scope,
|
||||
scope_var: 'cloud_credential',
|
||||
url: GetBasePath('credentials') + data.cloud_credential + '/'
|
||||
});
|
||||
}
|
||||
|
||||
if (!Empty(data.inventory_source)) {
|
||||
LookUpName({
|
||||
scope: $scope,
|
||||
scope_var: 'inventory_source',
|
||||
url: GetBasePath('inventory_sources') + data.inventory_source + '/'
|
||||
});
|
||||
}
|
||||
|
||||
// If the job isn't running we want to clear out the interval that goes out and checks for stdout updates.
|
||||
// This interval is defined in the standard out log directive controller.
|
||||
if (data.status === 'successful' || data.status === 'failed' || data.status === 'error' || data.status === 'canceled') {
|
||||
if ($rootScope.jobStdOutInterval) {
|
||||
window.clearInterval($rootScope.jobStdOutInterval);
|
||||
}
|
||||
}
|
||||
})
|
||||
.error(function(data, status) {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Failed to retrieve job: ' + job_id + '. GET returned: ' + status });
|
||||
});
|
||||
|
||||
// LookUpName can be found in the lookup-name.factory
|
||||
// It attaches the name that it gets (based on the url)
|
||||
// to the $scope variable defined by the attribute scope_var.
|
||||
if (!Empty(data.credential)) {
|
||||
LookUpName({
|
||||
scope: $scope,
|
||||
scope_var: 'credential',
|
||||
url: GetBasePath('credentials') + data.credential + '/'
|
||||
});
|
||||
}
|
||||
|
||||
if (!Empty(data.inventory)) {
|
||||
LookUpName({
|
||||
scope: $scope,
|
||||
scope_var: 'inventory',
|
||||
url: GetBasePath('inventory') + data.inventory + '/'
|
||||
});
|
||||
}
|
||||
|
||||
if (!Empty(data.project)) {
|
||||
LookUpName({
|
||||
scope: $scope,
|
||||
scope_var: 'project',
|
||||
url: GetBasePath('projects') + data.project + '/'
|
||||
});
|
||||
}
|
||||
|
||||
if (!Empty(data.cloud_credential)) {
|
||||
LookUpName({
|
||||
scope: $scope,
|
||||
scope_var: 'cloud_credential',
|
||||
url: GetBasePath('credentials') + data.cloud_credential + '/'
|
||||
});
|
||||
}
|
||||
|
||||
if (!Empty(data.inventory_source)) {
|
||||
LookUpName({
|
||||
scope: $scope,
|
||||
scope_var: 'inventory_source',
|
||||
url: GetBasePath('inventory_sources') + data.inventory_source + '/'
|
||||
});
|
||||
}
|
||||
|
||||
// If the job isn't running we want to clear out the interval that goes out and checks for stdout updates.
|
||||
// This interval is defined in the standard out log directive controller.
|
||||
if (data.status === 'successful' || data.status === 'failed' || data.status === 'error' || data.status === 'canceled') {
|
||||
if ($rootScope.jobStdOutInterval) {
|
||||
window.clearInterval($rootScope.jobStdOutInterval);
|
||||
}
|
||||
}
|
||||
})
|
||||
.error(function(data, status) {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
||||
msg: 'Failed to retrieve job: ' + job_id + '. GET returned: ' + status });
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// TODO: this is currently not used but is necessary for cases where sockets
|
||||
// are not available and a manual refresh trigger is needed.
|
||||
@ -156,6 +166,8 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams, C
|
||||
$scope.stdoutFullScreen = !$scope.stdoutFullScreen;
|
||||
}
|
||||
|
||||
getJobDetails();
|
||||
|
||||
}
|
||||
|
||||
JobStdoutController.$inject = [ '$rootScope', '$scope', '$state', '$stateParams', 'ClearScope', 'GetBasePath', 'Rest', 'ProcessErrors', 'Empty', 'GetChoices', 'LookUpName'];
|
||||
JobStdoutController.$inject = [ '$rootScope', '$scope', '$state', '$stateParams', 'ClearScope', 'GetBasePath', 'Rest', 'ProcessErrors', 'Empty', 'GetChoices', 'LookUpName', 'ParseTypeChange', 'ParseVariableString'];
|
||||
|
||||
@ -154,8 +154,6 @@
|
||||
<div id="login-modal-dialog" style="display: none;"></div>
|
||||
<div id="help-modal-dialog" style="display: none;"></div>
|
||||
|
||||
<div class="About" id="about-modal-dialog" style="display: none;" ng-include=" '{{ STATIC_URL }}assets/cowsay-about.html ' "></div>
|
||||
|
||||
<div id="prompt-for-days" style="display:none">
|
||||
<form name="prompt_for_days_form" id="prompt_for_days_form">
|
||||
Set how many days of data should be retained. <br>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user