TT#80605 prov templates: create/update/delete (panel UI)

- persist prov tmeplates in the database: create, update
  and permanently remove them again.

- prov templates from config.yml are still supported,
  but cannot be edited though. the templates from
  config.yml are merged with those from the db.

- each reseller can have their own prov templates,
  while the prov templates from config.yml are visible
  to all.

- YAML syntax highlighting and parse check when saving.
  Scripting language (perl/javascript) is currently parsed
  when executing a provisioning templates only. It is
  possible to further extend the parsing checks.

- the prov template "name" + reseller is the unique
  identifier. relevant also for the command line tool.

Change-Id: I58d7c54fa82fe512b263b3219bfc84d7e49c56a8
changes/91/39491/27
Rene Krenn 5 years ago
parent 8c04b91143
commit 36db8161ec

@ -8,8 +8,12 @@ use NGCP::Panel::Form;
use NGCP::Panel::Utils::Message;
use NGCP::Panel::Utils::Navigation;
use NGCP::Panel::Utils::Datatables;
use NGCP::Panel::Utils::DateTime qw();
use NGCP::Panel::Utils::ProvisioningTemplates qw();
use NGCP::Panel::Form::ProvisioningTemplate::Admin qw();
use NGCP::Panel::Form::ProvisioningTemplate::Reseller qw();
use URI::Encode qw();
use YAML::XS qw();
sub auto :Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) :AllowedRole(reseller) :AllowedRole(ccareadmin) :AllowedRole(ccare) {
my ($self, $c) = @_;
@ -24,13 +28,54 @@ sub template_list :Chained('/') :PathPart('batchprovisioning') :CaptureArgs(0) {
my ( $self, $c ) = @_;
my $templates = { %{$c->config->{provisioning_templates} // {}} };
map { $templates->{$_}->{name} = $_; } keys %$templates;
map {
$templates->{$_}->{name} = $_;
$templates->{$_}->{static} = 1;
$templates->{$_}->{id} = undef;
$templates->{$_}->{reseller} = undef;
} keys %$templates;
my $rs = $c->model('DB')->resultset('provisioning_templates')->search_rs();
if($c->user->roles eq "admin" || $c->user->roles eq "ccareadmin") {
} elsif($c->user->roles eq "reseller" || $c->user->roles eq "ccare") {
$rs = $rs->search_rs({ -or => [
reseller_id => $c->user->reseller_id,
reseller_id => undef
], },);
} else {
$rs = $rs->search_rs({ -or => [
reseller_id => $c->user->contract->contact->reseller_id,
reseller_id => undef
], },);
}
$c->stash->{template_rs} = $rs;
foreach my $db_template ($rs->all) {
my $template = { $db_template->get_inflated_columns };
eval {
%$template = ( %{YAML::XS::Load($template->{yaml})}, %$template );
#use Data::Dumper;
#$c->log->error(Dumper($template));
delete $template->{yaml};
};
if ($@) {
$c->log->error("error parsing provisioning_template id $template->{id} '$template->{name}': " . $@);
next;
}
$template->{static} = 0;
if ($db_template->reseller) {
$template->{reseller} = $db_template->reseller->name;
}
$templates->{$template->{name}} = $template;
}
$c->stash->{provisioning_templates} = $templates;
$c->stash->{template_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [
{ name => "id", search => 1, title => $c->loc('#') },
{ name => "reseller", search => 1, title => $c->loc("Reseller") },
{ name => "name", search => 1, title => $c->loc('Name') },
{ name => "description", search => 1, title => $c->loc('Description') },
{ name => "static", search => 0, field => 1 },
]);
$c->stash(template => 'batchprovisioning/list.tt');
@ -49,16 +94,42 @@ sub ajax :Chained('template_list') :PathPart('ajax') :Args(0) {
return;
}
sub template_base :Chained('template_list') :PathPart('') :CaptureArgs(1) {
sub template_base :Chained('template_list') :PathPart('templates') :CaptureArgs(1) {
my ( $self, $c, $template ) = @_;
my $decoder = URI::Encode->new;
$c->stash->{provisioning_template_name} = $decoder->decode($template);
$template = $decoder->decode($template);
$c->stash->{provisioning_template_name} = $template;
if (exists $c->stash->{provisioning_templates}->{$template}) {
if ($c->stash->{provisioning_templates}->{$template}->{id}) {
$c->stash->{template_rs} = $c->stash->{template_rs}->search_rs(
id => $c->stash->{provisioning_templates}->{$template}->{id},
);
unless ($c->stash->{template_rs}->first) {
NGCP::Panel::Utils::Message::error(
c => $c,
data => { id => $c->stash->{provisioning_templates}->{$template}->{id} },
desc => $c->loc('Provisioning templates does not exist!'),
);
$c->response->redirect($c->uri_for_action('/batchprovisioning/root'));
return;
}
}
} else {
NGCP::Panel::Utils::Message::error(
c => $c,
data => { name => $template },
desc => $c->loc('Provisioning templates does not exist!'),
);
$c->response->redirect($c->uri_for_action('/batchprovisioning/root'));
return;
}
}
sub do_template_form :Chained('template_base') :PathPart('form') :Args(0) {
my ($self, $c) = @_;
$c->stash(create_flag => 1);
$c->stash(modal_title => $c->loc("Subscriber with Provisioning Template '[_1]'", $c->stash->{provisioning_template_name}));
$c->log->debug($c->stash->{provisioning_template_name});
$c->log->debug($c->uri_for_action('/batchprovisioning/root'));
@ -74,7 +145,7 @@ sub do_template_upload :Chained('template_base') :PathPart('upload') :Args(0) {
$c->log->debug($c->uri_for_action('/batchprovisioning/do_template_upload', $c->req->captures));
my $form = NGCP::Panel::Form::get("NGCP::Panel::Form::ProvisioningTemplateUpload", $c);
my $form = NGCP::Panel::Form::get("NGCP::Panel::Form::ProvisioningTemplate::ProvisioningTemplateUpload", $c);
my $upload = $c->req->upload('csv');
my $posted = $c->req->method eq 'POST';
my @params = ( csv => ($posted ? $upload : undef), );
@ -86,7 +157,6 @@ sub do_template_upload :Chained('template_base') :PathPart('upload') :Args(0) {
if($form->validated) {
# TODO: check by formhandler?
unless($upload) {
NGCP::Panel::Utils::Message::error(
c => $c,
@ -129,7 +199,180 @@ sub do_template_upload :Chained('template_base') :PathPart('upload') :Args(0) {
}
$c->stash(create_flag => 1);
$c->stash(modal_title => $c->loc("Subscribers with Provisioning Template '[_1]' from CSV", $c->stash->{provisioning_template_name}));
$c->stash(form => $form);
}
sub create :Chained('template_list') :PathPart('create') :Args(0) {
my ($self, $c) = @_;
my $posted = ($c->request->method eq 'POST');
my $form;
my $params = {};
$params->{reseller}{id} = delete $params->{reseller_id};
$params = merge($params, $c->session->{created_objects});
$c->stash->{old_name} = undef;
if($c->user->is_superuser) {
$form = NGCP::Panel::Form::get("NGCP::Panel::Form::ProvisioningTemplate::Admin", $c);
} else {
$form = NGCP::Panel::Form::get("NGCP::Panel::Form::ProvisioningTemplate::Reseller", $c);
}
$form->process(
posted => $posted,
params => $c->request->params,
item => $params
);
NGCP::Panel::Utils::Navigation::check_form_buttons(
c => $c,
form => $form,
fields => {
'reseller.create' => $c->uri_for('/reseller/create'),
},
back_uri => $c->req->uri,
);
if($posted && $form->validated) {
try {
if (exists $form->values->{reseller}) {
if($c->user->is_superuser) {
$form->values->{reseller_id} = $form->values->{reseller}{id};
} else {
$form->values->{reseller_id} = $c->user->reseller_id;
}
delete $form->values->{reseller};
}
$form->values->{create_timestamp} = $form->values->{modify_timestamp} = NGCP::Panel::Utils::DateTime::current_local;
my $template = $c->model('DB')->resultset('provisioning_templates')->create($form->values);
$c->session->{created_objects}->{provisioning_template} = { id => $template->id };
delete $c->session->{created_objects}->{reseller};
NGCP::Panel::Utils::Message::info(
c => $c,
desc => $c->loc('Provisioning template successfully created'),
);
} catch($e) {
NGCP::Panel::Utils::Message::error(
c => $c,
error => $e,
desc => $c->loc('Failed to create provisioning template'),
);
}
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/batchprovisioning'));
}
$c->stash(create_flag => 1);
$c->stash(modal_title => $c->loc("Provisioning Template"));
$c->stash(form => $form);
}
sub edit :Chained('template_base') :PathPart('edit') :Args(0) {
my ($self, $c) = @_;
my $posted = ($c->request->method eq 'POST');
my $form;
my $params = {};
my $template = $c->stash->{provisioning_template_name};
if ($c->stash->{provisioning_templates}->{$template}->{static}) {
NGCP::Panel::Utils::Message::error(
c => $c,
data => { name => $template },
desc => $c->loc('Provisioning template cannot be edited.'),
);
$c->response->redirect($c->uri_for_action('/batchprovisioning/root'));
return;
} else {
$params = { $c->stash->{template_rs}->first->get_inflated_columns };
}
$params->{reseller}{id} = delete $params->{reseller_id};
$params = merge($params, $c->session->{created_objects});
$c->stash->{old_name} = $template;
if($c->user->is_superuser) {
$form = NGCP::Panel::Form::get("NGCP::Panel::Form::ProvisioningTemplate::Admin", $c);
} else {
$form = NGCP::Panel::Form::get("NGCP::Panel::Form::ProvisioningTemplate::Reseller", $c);
}
$form->process(
posted => $posted,
params => $c->request->params,
item => $params,
);
NGCP::Panel::Utils::Navigation::check_form_buttons(
c => $c,
form => $form,
fields => {
'reseller.create' => $c->uri_for('/reseller/create'),
},
back_uri => $c->req->uri,
);
if($posted && $form->validated) {
try {
if (exists $form->values->{reseller}) {
if($c->user->is_superuser) {
$form->values->{reseller_id} = $form->values->{reseller}{id};
} else {
$form->values->{reseller_id} = $c->user->reseller_id;
}
delete $form->values->{reseller};
}
$form->values->{modify_timestamp} = NGCP::Panel::Utils::DateTime::current_local;
$c->stash->{template_rs}->update($form->values);
delete $c->session->{created_objects}->{reseller};
NGCP::Panel::Utils::Message::info(
c => $c,
desc => $c->loc('Provisioning template successfully updated'),
);
} catch($e) {
NGCP::Panel::Utils::Message::error(
c => $c,
error => $e,
desc => $c->loc('Failed to update provisioning template'),
);
}
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/batchprovisioning'));
}
$c->stash(edit_flag => 1 );
$c->stash(modal_title => $c->loc("Provisioning Template"));
$c->stash(form => $form );
}
sub remove :Chained('template_base') :PathPart('remove') :Args(0) {
my ($self, $c) = @_;
my $template = $c->stash->{provisioning_template_name};
if ($c->stash->{provisioning_templates}->{$template}->{static}) {
NGCP::Panel::Utils::Message::error(
c => $c,
data => { name => $template },
desc => $c->loc('Provisioning template cannot be removed.'),
);
$c->response->redirect($c->uri_for_action('/batchprovisioning/root'));
return;
} else {
$template = $c->stash->{template_rs}->first;
}
try {
$template->delete;
NGCP::Panel::Utils::Message::info(
c => $c,
data => $template,
desc => $c->loc('Provisioning template successfully removed'),
);
} catch ($e) {
NGCP::Panel::Utils::Message::error(
c => $c,
error => $e,
data => $template,
desc => $c->loc('Failed to remove provisioning template'),
);
};
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/batchprovisioning'));
}
1;

@ -0,0 +1,21 @@
package NGCP::Panel::Form::ProvisioningTemplate::Admin;
use HTML::FormHandler::Moose;
extends 'NGCP::Panel::Form::ProvisioningTemplate::Reseller';
has_field 'reseller' => (
type => '+NGCP::Panel::Field::Reseller',
validate_when_empty => 1,
element_attr => {
rel => ['tooltip'],
title => ['The reseller id this template belongs to.']
},
);
has_block 'fields' => (
tag => 'div',
class => [qw/modal-body/],
render_list => [qw/reseller name description lang yaml/],
);
1;

@ -1,4 +1,4 @@
package NGCP::Panel::Form::ProvisioningTemplate;
package NGCP::Panel::Form::ProvisioningTemplate::ProvisioningTemplate;
use Sipwise::Base;
use HTML::FormHandler::Moose;

@ -1,4 +1,4 @@
package NGCP::Panel::Form::ProvisioningTemplateUpload;
package NGCP::Panel::Form::ProvisioningTemplate::ProvisioningTemplateUpload;
use Sipwise::Base;
use HTML::FormHandler::Moose;
extends 'HTML::FormHandler';
@ -40,4 +40,3 @@ has_block 'actions' => (
);
1;

@ -0,0 +1,164 @@
package NGCP::Panel::Form::ProvisioningTemplate::Reseller;
use HTML::FormHandler::Moose;
extends 'HTML::FormHandler';
use HTML::FormHandler::Widget::Block::Bootstrap;
use Storable qw();
has '+widget_wrapper' => ( default => 'Bootstrap' );
has_field 'submitid' => ( type => 'Hidden' );
sub build_render_list {[qw/submitid fields actions/]}
sub build_form_element_class { [qw/form-horizontal/] }
has_field 'id' => (
type => 'Hidden'
);
has_field 'name' => (
type => 'Text',
required => 1,
maxlength => 255,
element_attr => {
rel => ['tooltip'],
title => ['A unique template name.']
},
);
has_field 'description' => (
type => 'Text',
label => 'Description',
required => 1,
maxlength => 255,
element_attr => {
rel => ['tooltip'],
title => ['Arbitrary text.'],
},
);
has_field 'lang' => (
type => 'Select',
label => 'Calculated fields',
options => [
{ value => 'js', label => 'JavaScript' },
{ value => 'perl', label => 'Perl' },
],
element_attr => {
rel => ['tooltip'],
title => ['Which scripting language used in the template.']
},
);
has_field 'yaml' => (
type => 'TextArea',
required => 1,
label => 'Template',
cols => 200,
rows => 100,
maxlength => '67108864',
#form_element_class => [],
element_class => [qw/ngcp-provtemplate-area/],
default => <<'EOS_DEFAULT_YAML',
fields:
- name: fields
label: "First Name:"
type: Text
required: 1
- name: last_name
label: "Last Name:"
type: Text
required: 1
- name: cc
label: "Country Code:"
type: Text
required: 1
- name: ac
label: "Area Code:"
type: Text
required: 1
- name: sn
label: "Subscriber Number:"
type: Text
required: 1
- name: sip_username
type: calculated
value_code: "function() { return row.cc.concat(row.ac).concat(row.sn); }"
- name: purge
label: "Terminate subscriber, if exists:"
type: Boolean
contract_contact:
identifier: "firstname, lastname"
reseller: default
firstname_code: "function() { return row.first_name; }"
lastname_code: "function() { return row.last_name; }"
contract:
product: "Basic SIP Account"
billing_profile: "Default Billing Profile"
identifier: contact_id
contact_id_code: "function() { return contract_contact.id; }"
subscriber:
domain: "example.org"
primary_number:
cc_code: "function() { return row.cc; }"
ac_code: "function() { return row.ac; }"
sn_code: "function() { return row.sn; }"
username_code: "function() { return row.sip_username; }"
password_code: "function() { return row.sip_password; }"
subscriber_preferences:
gpp0: "provisioning templates test"
EOS_DEFAULT_YAML
);
has_field 'save' => (
type => 'Submit',
value => 'Save',
element_class => [qw/btn btn-primary/],
label => '',
);
has_block 'fields' => (
tag => 'div',
class => [qw/modal-body/],
render_list => [qw/name description lang yaml/],
);
has_block 'actions' => (
tag => 'div',
class => [qw/modal-footer/],
render_list => [qw/save/],
);
sub validate_yaml {
my ($self, $field) = @_;
eval {
my $data = YAML::XS::Load($field->value);
die('not a hash') unless 'HASH' eq ref $data;
foreach my $section (qw/contract subscriber/) {
die("section '$section' required") unless exists $data->{$section};
die("section '$section' is not a hash") unless 'HASH' eq ref $data->{$section};
}
};
if ($@) {
$field->add_error($@);
}
}
sub validate_name {
my ($self, $field) = @_;
my $c = $self->ctx;
return unless $c;
if (not defined $c->stash->{old_name}
or $c->stash->{old_name} ne $field->value) {
$field->add_error("a provisioning template with name '" . $field->value . "' already exists")
if exists $c->stash->{provisioning_templates}->{$field->value};
}
}
1;

@ -459,7 +459,7 @@ sub set_columns {
sub _prune_row {
my ($user_tz, $columns, %row) = @_;
while (my ($k,$v) = each %row) {
unless (first { $_->{accessor} eq $k && $_->{title} } @{ $columns }) {
unless (first { $_->{accessor} eq $k && ($_->{title} || $_->{field}) } @{ $columns }) {
delete $row{$k};
next;
}

@ -2,7 +2,7 @@ package NGCP::Panel::Utils::ProvisioningTemplates;
use Sipwise::Base;
use NGCP::Panel::Form::ProvisioningTemplate qw();
use NGCP::Panel::Form::ProvisioningTemplate::ProvisioningTemplate qw();
use DateTime::TimeZone qw();
use String::MkPasswd qw();
use Eval::Closure qw(eval_closure);
@ -96,7 +96,7 @@ sub create_provisioning_template_form {
my $form;
try {
$form = NGCP::Panel::Form::ProvisioningTemplate->new({
$form = NGCP::Panel::Form::ProvisioningTemplate::ProvisioningTemplate->new({
ctx => $c,
fields_config => [ values %$fields ],
});

@ -0,0 +1,349 @@
/* BASICS */
.CodeMirror {
/* Set height, width, borders, and global font properties here */
font-family: monospace;
height: 300px;
color: black;
direction: ltr;
}
/* PADDING */
.CodeMirror-lines {
padding: 4px 0; /* Vertical padding around content */
}
.CodeMirror pre.CodeMirror-line,
.CodeMirror pre.CodeMirror-line-like {
padding: 0 4px; /* Horizontal padding of content */
}
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
background-color: white; /* The little square between H and V scrollbars */
}
/* GUTTER */
.CodeMirror-gutters {
border-right: 1px solid #ddd;
background-color: #f7f7f7;
white-space: nowrap;
}
.CodeMirror-linenumbers {}
.CodeMirror-linenumber {
padding: 0 3px 0 5px;
min-width: 20px;
text-align: right;
color: #999;
white-space: nowrap;
}
.CodeMirror-guttermarker { color: black; }
.CodeMirror-guttermarker-subtle { color: #999; }
/* CURSOR */
.CodeMirror-cursor {
border-left: 1px solid black;
border-right: none;
width: 0;
}
/* Shown when moving in bi-directional text */
.CodeMirror div.CodeMirror-secondarycursor {
border-left: 1px solid silver;
}
.cm-fat-cursor .CodeMirror-cursor {
width: auto;
border: 0 !important;
background: #7e7;
}
.cm-fat-cursor div.CodeMirror-cursors {
z-index: 1;
}
.cm-fat-cursor-mark {
background-color: rgba(20, 255, 20, 0.5);
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
}
.cm-animate-fat-cursor {
width: auto;
border: 0;
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
background-color: #7e7;
}
@-moz-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@-webkit-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
/* Can style cursor different in overwrite (non-insert) mode */
.CodeMirror-overwrite .CodeMirror-cursor {}
.cm-tab { display: inline-block; text-decoration: inherit; }
.CodeMirror-rulers {
position: absolute;
left: 0; right: 0; top: -50px; bottom: 0;
overflow: hidden;
}
.CodeMirror-ruler {
border-left: 1px solid #ccc;
top: 0; bottom: 0;
position: absolute;
}
/* DEFAULT THEME */
.cm-s-default .cm-header {color: blue;}
.cm-s-default .cm-quote {color: #090;}
.cm-negative {color: #d44;}
.cm-positive {color: #292;}
.cm-header, .cm-strong {font-weight: bold;}
.cm-em {font-style: italic;}
.cm-link {text-decoration: underline;}
.cm-strikethrough {text-decoration: line-through;}
.cm-s-default .cm-keyword {color: #708;}
.cm-s-default .cm-atom {color: #219;}
.cm-s-default .cm-number {color: #164;}
.cm-s-default .cm-def {color: #00f;}
.cm-s-default .cm-variable,
.cm-s-default .cm-punctuation,
.cm-s-default .cm-property,
.cm-s-default .cm-operator {}
.cm-s-default .cm-variable-2 {color: #05a;}
.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;}
.cm-s-default .cm-comment {color: #a50;}
.cm-s-default .cm-string {color: #a11;}
.cm-s-default .cm-string-2 {color: #f50;}
.cm-s-default .cm-meta {color: #555;}
.cm-s-default .cm-qualifier {color: #555;}
.cm-s-default .cm-builtin {color: #30a;}
.cm-s-default .cm-bracket {color: #997;}
.cm-s-default .cm-tag {color: #170;}
.cm-s-default .cm-attribute {color: #00c;}
.cm-s-default .cm-hr {color: #999;}
.cm-s-default .cm-link {color: #00c;}
.cm-s-default .cm-error {color: #f00;}
.cm-invalidchar {color: #f00;}
.CodeMirror-composing { border-bottom: 2px solid; }
/* Default styles for common addons */
div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;}
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;}
.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
.CodeMirror-activeline-background {background: #e8f2ff;}
/* STOP */
/* The rest of this file contains styles related to the mechanics of
the editor. You probably shouldn't touch them. */
.CodeMirror {
position: relative;
overflow: hidden;
background: white;
}
.CodeMirror-scroll {
overflow: scroll !important; /* Things will break if this is overridden */
/* 30px is the magic margin used to hide the element's real scrollbars */
/* See overflow: hidden in .CodeMirror */
margin-bottom: -30px; margin-right: -30px;
padding-bottom: 30px;
height: 100%;
outline: none; /* Prevent dragging from highlighting the element */
position: relative;
}
.CodeMirror-sizer {
position: relative;
border-right: 30px solid transparent;
}
/* The fake, visible scrollbars. Used to force redraw during scrolling
before actual scrolling happens, thus preventing shaking and
flickering artifacts. */
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
position: absolute;
z-index: 6;
display: none;
}
.CodeMirror-vscrollbar {
right: 0; top: 0;
overflow-x: hidden;
overflow-y: scroll;
}
.CodeMirror-hscrollbar {
bottom: 0; left: 0;
overflow-y: hidden;
overflow-x: scroll;
}
.CodeMirror-scrollbar-filler {
right: 0; bottom: 0;
}
.CodeMirror-gutter-filler {
left: 0; bottom: 0;
}
.CodeMirror-gutters {
position: absolute; left: 0; top: 0;
min-height: 100%;
z-index: 3;
}
.CodeMirror-gutter {
white-space: normal;
height: 100%;
display: inline-block;
vertical-align: top;
margin-bottom: -30px;
}
.CodeMirror-gutter-wrapper {
position: absolute;
z-index: 4;
background: none !important;
border: none !important;
}
.CodeMirror-gutter-background {
position: absolute;
top: 0; bottom: 0;
z-index: 4;
}
.CodeMirror-gutter-elt {
position: absolute;
cursor: default;
z-index: 4;
}
.CodeMirror-gutter-wrapper ::selection { background-color: transparent }
.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent }
.CodeMirror-lines {
cursor: text;
min-height: 1px; /* prevents collapsing before first draw */
}
.CodeMirror pre.CodeMirror-line,
.CodeMirror pre.CodeMirror-line-like {
/* Reset some styles that the rest of the page might have set */
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
border-width: 0;
background: transparent;
font-family: inherit;
font-size: inherit;
margin: 0;
white-space: pre;
word-wrap: normal;
line-height: inherit;
color: inherit;
z-index: 2;
position: relative;
overflow: visible;
-webkit-tap-highlight-color: transparent;
-webkit-font-variant-ligatures: contextual;
font-variant-ligatures: contextual;
}
.CodeMirror-wrap pre.CodeMirror-line,
.CodeMirror-wrap pre.CodeMirror-line-like {
word-wrap: break-word;
white-space: pre-wrap;
word-break: normal;
}
.CodeMirror-linebackground {
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
z-index: 0;
}
.CodeMirror-linewidget {
position: relative;
z-index: 2;
padding: 0.1px; /* Force widget margins to stay inside of the container */
}
.CodeMirror-widget {}
.CodeMirror-rtl pre { direction: rtl; }
.CodeMirror-code {
outline: none;
}
/* Force content-box sizing for the elements where we expect it */
.CodeMirror-scroll,
.CodeMirror-sizer,
.CodeMirror-gutter,
.CodeMirror-gutters,
.CodeMirror-linenumber {
-moz-box-sizing: content-box;
box-sizing: content-box;
}
.CodeMirror-measure {
position: absolute;
width: 100%;
height: 0;
overflow: hidden;
visibility: hidden;
}
.CodeMirror-cursor {
position: absolute;
pointer-events: none;
}
.CodeMirror-measure pre { position: static; }
div.CodeMirror-cursors {
visibility: hidden;
position: relative;
z-index: 3;
}
div.CodeMirror-dragcursors {
visibility: visible;
}
.CodeMirror-focused div.CodeMirror-cursors {
visibility: visible;
}
.CodeMirror-selected { background: #d9d9d9; }
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
.CodeMirror-crosshair { cursor: crosshair; }
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
.cm-searching {
background-color: #ffa;
background-color: rgba(255, 255, 0, .4);
}
/* Used to force a border model for a node */
.cm-force-border { padding-right: .1px; }
@media print {
/* Hide the cursor when printing */
.CodeMirror div.CodeMirror-cursors {
visibility: hidden;
}
}
/* See issue #2901 */
.cm-tab-wrap-hack:after { content: ''; }
/* Help users use markselection to safely style text background */
span.CodeMirror-selectedtext { background: none; }

File diff suppressed because one or more lines are too long

@ -571,6 +571,10 @@ textarea.ngcp-autoconf-area {
width: 580px;
}
textarea.ngcp-provtemplate-area {
width: 580px;
}
/* -------------
Login Page
----------------*/

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

@ -0,0 +1 @@
(function(a){if(typeof exports=="object"&&typeof module=="object"){a(require("../../lib/codemirror"))}else{if(typeof define=="function"&&define.amd){define(["../../lib/codemirror"],a)}else{a(CodeMirror)}}})(function(a){a.defineMode("yaml",function(){var b=["true","false","on","off","yes","no"];var c=new RegExp("\\b(("+b.join(")|(")+"))$","i");return{token:function(g,f){var e=g.peek();var d=f.escaped;f.escaped=false;if(e=="#"&&(g.pos==0||/\s/.test(g.string.charAt(g.pos-1)))){g.skipToEnd();return"comment"}if(g.match(/^('([^']|\\.)*'?|"([^"]|\\.)*"?)/)){return"string"}if(f.literal&&g.indentation()>f.keyCol){g.skipToEnd();return"string"}else{if(f.literal){f.literal=false}}if(g.sol()){f.keyCol=0;f.pair=false;f.pairStart=false;if(g.match(/---/)){return"def"}if(g.match(/\.\.\./)){return"def"}if(g.match(/\s*-\s+/)){return"meta"}}if(g.match(/^(\{|\}|\[|\])/)){if(e=="{"){f.inlinePairs++}else{if(e=="}"){f.inlinePairs--}else{if(e=="["){f.inlineList++}else{f.inlineList--}}}return"meta"}if(f.inlineList>0&&!d&&e==","){g.next();return"meta"}if(f.inlinePairs>0&&!d&&e==","){f.keyCol=0;f.pair=false;f.pairStart=false;g.next();return"meta"}if(f.pairStart){if(g.match(/^\s*(\||\>)\s*/)){f.literal=true;return"meta"}if(g.match(/^\s*(\&|\*)[a-z0-9\._-]+\b/i)){return"variable-2"}if(f.inlinePairs==0&&g.match(/^\s*-?[0-9\.\,]+\s?$/)){return"number"}if(f.inlinePairs>0&&g.match(/^\s*-?[0-9\.\,]+\s?(?=(,|}))/)){return"number"}if(g.match(c)){return"keyword"}}if(!f.pair&&g.match(/^\s*(?:[,\[\]{}&*!|>'"%@`][^\s'":]|[^,\[\]{}#&*!|>'"%@`])[^#]*?(?=\s*:($|\s))/)){f.pair=true;f.keyCol=g.indentation();return"atom"}if(f.pair&&g.match(/^:\s*/)){f.pairStart=true;return"meta"}f.pairStart=false;f.escaped=(e=="\\");g.next();return null},startState:function(){return{pair:false,pairStart:false,keyCol:0,inlinePairs:0,inlineList:0,literal:false,escaped:false}},lineComment:"#",fold:"indent"}});a.defineMIME("text/x-yaml","yaml");a.defineMIME("text/yaml","yaml")});

@ -0,0 +1,120 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
CodeMirror.defineMode("yaml", function() {
var cons = ['true', 'false', 'on', 'off', 'yes', 'no'];
var keywordRegex = new RegExp("\\b(("+cons.join(")|(")+"))$", 'i');
return {
token: function(stream, state) {
var ch = stream.peek();
var esc = state.escaped;
state.escaped = false;
/* comments */
if (ch == "#" && (stream.pos == 0 || /\s/.test(stream.string.charAt(stream.pos - 1)))) {
stream.skipToEnd();
return "comment";
}
if (stream.match(/^('([^']|\\.)*'?|"([^"]|\\.)*"?)/))
return "string";
if (state.literal && stream.indentation() > state.keyCol) {
stream.skipToEnd(); return "string";
} else if (state.literal) { state.literal = false; }
if (stream.sol()) {
state.keyCol = 0;
state.pair = false;
state.pairStart = false;
/* document start */
if(stream.match(/---/)) { return "def"; }
/* document end */
if (stream.match(/\.\.\./)) { return "def"; }
/* array list item */
if (stream.match(/\s*-\s+/)) { return 'meta'; }
}
/* inline pairs/lists */
if (stream.match(/^(\{|\}|\[|\])/)) {
if (ch == '{')
state.inlinePairs++;
else if (ch == '}')
state.inlinePairs--;
else if (ch == '[')
state.inlineList++;
else
state.inlineList--;
return 'meta';
}
/* list seperator */
if (state.inlineList > 0 && !esc && ch == ',') {
stream.next();
return 'meta';
}
/* pairs seperator */
if (state.inlinePairs > 0 && !esc && ch == ',') {
state.keyCol = 0;
state.pair = false;
state.pairStart = false;
stream.next();
return 'meta';
}
/* start of value of a pair */
if (state.pairStart) {
/* block literals */
if (stream.match(/^\s*(\||\>)\s*/)) { state.literal = true; return 'meta'; };
/* references */
if (stream.match(/^\s*(\&|\*)[a-z0-9\._-]+\b/i)) { return 'variable-2'; }
/* numbers */
if (state.inlinePairs == 0 && stream.match(/^\s*-?[0-9\.\,]+\s?$/)) { return 'number'; }
if (state.inlinePairs > 0 && stream.match(/^\s*-?[0-9\.\,]+\s?(?=(,|}))/)) { return 'number'; }
/* keywords */
if (stream.match(keywordRegex)) { return 'keyword'; }
}
/* pairs (associative arrays) -> key */
if (!state.pair && stream.match(/^\s*(?:[,\[\]{}&*!|>'"%@`][^\s'":]|[^,\[\]{}#&*!|>'"%@`])[^#]*?(?=\s*:($|\s))/)) {
state.pair = true;
state.keyCol = stream.indentation();
return "atom";
}
if (state.pair && stream.match(/^:\s*/)) { state.pairStart = true; return 'meta'; }
/* nothing found, continue */
state.pairStart = false;
state.escaped = (ch == '\\');
stream.next();
return null;
},
startState: function() {
return {
pair: false,
pairStart: false,
keyCol: 0,
inlinePairs: 0,
inlineList: 0,
literal: false,
escaped: false
};
},
lineComment: "#",
fold: "indent"
};
});
CodeMirror.defineMIME("text/x-yaml", "yaml");
CodeMirror.defineMIME("text/yaml", "yaml");
});

@ -1,7 +1,23 @@
<script src="/js/libs/codemirror/codemirror-min.js"></script>
<link rel="stylesheet" href="/css/codemirror/codemirror.min.css">
<script src="/js/libs/codemirror/mode/yaml-min.js"></script>
<script>
$( document ).ready(function() {
var area = document.getElementById('yaml');
if (area) {
var editor = CodeMirror.fromTextArea(area, {
lineNumbers: true,
mode: "yaml"
});
}
});
</script>
[% site_config.title = c.loc('Batch Provisioning') -%]
[%
helper.name = c.loc("Subscriber from Provisioning Template '[_1]'", provisioning_template_name);
helper.name = modal_title;
helper.identifier = 'provisioning_templates';
helper.length_change = 1;
helper.dt_columns = template_dt_columns;
@ -14,9 +30,17 @@
helper.edit_flag = edit_flag;
helper.form_object = form;
UNLESS c.user.read_only;
helper.dt_buttons = [
{ name = c.loc('Open Form'), uri = "/batchprovisioning/'+encodeURI(full.name)+'/form", class = 'btn-small btn-primary', icon = 'icon-edit' },
{ name = c.loc('Upload CSV'), uri = "/batchprovisioning/'+encodeURI(full.name)+'/upload", class = 'btn-small btn-primary', icon = 'icon-star' },
{ name = c.loc('Delete'), condition = "full.static == 0", uri = "/batchprovisioning/templates/'+encodeURI(full.name)+'/remove", class = 'btn-small btn-secondary', icon = 'icon-remove' },
{ name = c.loc('Edit'), condition = "full.static == 0", uri = "/batchprovisioning/templates/'+encodeURI(full.name)+'/edit", class = 'btn-small btn-primary', icon = 'icon-edit' },
{ name = c.loc('Open Form'), uri = "/batchprovisioning/templates/'+encodeURI(full.name)+'/form", class = 'btn-small btn-tertiary', icon = 'icon-list' },
{ name = c.loc('Upload CSV'), uri = "/batchprovisioning/templates/'+encodeURI(full.name)+'/upload", class = 'btn-small btn-tertiary', icon = 'icon-star' },
];
helper.top_buttons = [
{ name = c.loc('Create Provisioning Template'), uri = c.uri_for_action('/batchprovisioning/create'), icon = 'icon-star' },
];
END;
PROCESS 'helpers/datatables.tt';
%]
Loading…
Cancel
Save