diff --git a/lib/NGCP/Panel/Controller/Peering.pm b/lib/NGCP/Panel/Controller/Peering.pm index da2e2e6218..1e4b895fec 100644 --- a/lib/NGCP/Panel/Controller/Peering.pm +++ b/lib/NGCP/Panel/Controller/Peering.pm @@ -363,7 +363,6 @@ sub servers_edit :Chained('servers_base') :PathPart('edit') :Args(0) { } $c->stash( - close_target => $c->uri_for_action('/peering/servers_root', [$c->req->captures->[0]]), servers_form => $form, servers_edit_flag => 1, ); @@ -578,7 +577,7 @@ sub rules_create :Chained('rules_list') :PathPart('create') :Args(0) { my ($self, $c) = @_; my $posted = ($c->request->method eq 'POST'); - my $form = NGCP::Panel::Form::Peering::Rule->new(ctx => $c); + my $form = NGCP::Panel::Form::Peering::Rule->new(ctx => $c, inactive => ['group']); $form->process( posted => $posted, params => $c->request->params, @@ -651,6 +650,7 @@ sub rules_edit :Chained('rules_base') :PathPart('edit') :Args(0) { my $posted = ($c->request->method eq 'POST'); my $form = NGCP::Panel::Form::Peering::Rule->new(ctx => $c); + $c->stash->{rule}{group}{id} = delete $c->stash->{rule}{group_id}; $form->process( posted => $posted, params => $c->request->params, @@ -665,6 +665,7 @@ sub rules_edit :Chained('rules_base') :PathPart('edit') :Args(0) { if($posted && $form->validated) { try { $form->values->{callee_prefix} //= ''; + $form->values->{group_id} = $form->values->{group}{id}; $c->stash->{rule_result}->update($form->values); NGCP::Panel::Utils::Peering::_sip_lcr_reload(c => $c); NGCP::Panel::Utils::Message::info( @@ -682,7 +683,6 @@ sub rules_edit :Chained('rules_base') :PathPart('edit') :Args(0) { } $c->stash( - close_target => $c->uri_for_action('/peering/servers_root', [$c->req->captures->[0]]), rules_form => $form, rules_edit_flag => 1, ); @@ -736,7 +736,7 @@ sub inbound_rules_create :Chained('inbound_rules_list') :PathPart('create') :Arg my ($self, $c) = @_; my $posted = ($c->request->method eq 'POST'); - my $form = NGCP::Panel::Form::Peering::InboundRule->new(ctx => $c); + my $form = NGCP::Panel::Form::Peering::InboundRule->new(ctx => $c, inactive => ['group']); $form->process( posted => $posted, params => $c->request->params, @@ -810,6 +810,7 @@ sub inbound_rules_edit :Chained('inbound_rules_base') :PathPart('edit') :Args(0) my $posted = ($c->request->method eq 'POST'); my $form = NGCP::Panel::Form::Peering::InboundRule->new(ctx => $c); + $c->stash->{rule}{group}{id} = delete $c->stash->{rule}{group_id}; $form->process( posted => $posted, params => $c->request->params, @@ -823,6 +824,7 @@ sub inbound_rules_edit :Chained('inbound_rules_base') :PathPart('edit') :Args(0) ); if($posted && $form->validated) { try { + $form->values->{group_id} = $form->values->{group}{id}; $c->stash->{inbound_rule_result}->update($form->values); NGCP::Panel::Utils::Message::info( c => $c, @@ -839,7 +841,6 @@ sub inbound_rules_edit :Chained('inbound_rules_base') :PathPart('edit') :Args(0) } $c->stash( - close_target => $c->uri_for_action('/peering/servers_root', [$c->req->captures->[0]]), inbound_rules_form => $form, inbound_rules_edit_flag => 1, ); diff --git a/lib/NGCP/Panel/Controller/PeeringOverview.pm b/lib/NGCP/Panel/Controller/PeeringOverview.pm new file mode 100644 index 0000000000..8c41eda04f --- /dev/null +++ b/lib/NGCP/Panel/Controller/PeeringOverview.pm @@ -0,0 +1,212 @@ +package NGCP::Panel::Controller::PeeringOverview; +use NGCP::Panel::Utils::Generic qw(:all); +use Sipwise::Base; + +use parent 'Catalyst::Controller'; + +use NGCP::Panel::Form::PeeringOverview::Columns; +use NGCP::Panel::Utils::Navigation; + +sub auto :Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) { + my ($self, $c) = @_; + $c->log->debug(__PACKAGE__ . '::auto'); + NGCP::Panel::Utils::Navigation::check_redirect_chain(c => $c); + return 1; +} + +sub list :Chained('/') :PathPart('peeringoverview') :CaptureArgs(0) { + my ( $self, $c ) = @_; + + my $stored = $c->session->{created_objects}{peeringoverview} //= {}; + $stored->{rule_direction} //= "outbound"; + + my @default = ( + { name => "callee_prefix", search => 1, title => $c->loc("Callee Prefix") }, + { name => "enabled", search => 1, title => $c->loc("State") }, + { name => "description", search => 1, title => $c->loc("Description") }, + { name => "group.name", search => 1, title => $c->loc("Peer Group") }, + { name => "group.voip_peer_hosts.name", search => 1, title => $c->loc("Peer Name") }, + { name => "group.voip_peer_hosts.host", search => 1, title => $c->loc("Peer Host") }, + { name => "group.voip_peer_hosts.ip", search => 1, title => $c->loc("Peer IP") }, + { name => "group.voip_peer_hosts.enabled", search => 1, title => $c->loc("Peer State") }, + { name => "group.priority", search => 1, title => $c->loc("Priority") }, + { name => "group.voip_peer_hosts.weight", search => 1, title => $c->loc("Weight") }, + ); + + if (defined $stored->{cols}) { + foreach my $col (@{$stored->{cols}}) { + unless ($col->{name} || $col->{title}) { + delete $stored->{$col}; + } + } + } else { + @{$stored->{cols}} = @default; + } + + $c->stash->{po_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [ + { name => "id", search => 1, title => $c->loc("#") }, + @{$stored->{cols}} + ]); + + $c->stash->{template} = 'peeringoverview/list.tt'; + + return; +} + +sub root :Chained('list') :PathPart('') :Args(0) { + my ( $self, $c ) = @_; +} + +sub ajax :Chained('list') :PathPart('ajax') :Args(0) { + my ($self, $c) = @_; + + my $stored = $c->session->{created_objects}{peeringoverview} //= {}; + $stored->{search} = $c->req->params->{sSearch} // ''; + + my $rules_table = "voip_peer_rules"; + if ($stored->{rule_direction} eq "inbound") { + $rules_table = "voip_peer_inbound_rules"; + } + + my $join = { 'join' => 'group' }; + if (grep { /voip_peer_hosts/ } map { $_->{name} } @{$stored->{cols}}) { + $join = { + 'join' => { 'group' => 'voip_peer_hosts' }, + '+select' => [ 'voip_peer_hosts.id' ], + '+as' => [ 'peer_id' ], + }; + } + + $c->stash->{po_rs} = $c->model('DB')->resultset($rules_table)->search( + {}, $join + ); + + NGCP::Panel::Utils::Datatables::process($c, @{$c->stash}{qw(po_rs po_dt_columns)}, sub { + my $item = shift; + my %cols = $item->get_inflated_columns; + return ( peer_group_id => $cols{group_id}, + peer_host_id => $cols{peer_id} ); + },); + + $c->detach( $c->view("JSON") ); +} + + +sub edit :Chained('list') :PathPart('edit') :Args(0) { + my ($self, $c) = @_; + + my $form = NGCP::Panel::Form::PeeringOverview::Columns->new(ctx => $c); + my $params = $c->session->{created_objects}{peeringoverview}{states} // {}; + my $posted = ($c->req->method eq 'POST'); + my $data = $c->req->params; + + $form->process( + posted => $posted, + params => $c->request->params, + item => $params, + ); + + unless ($posted && $form->validated) { + $c->stash( + template => 'peeringoverview/edit.tt', + form => $form, + ); + return; + } + + my $stored = { rule_direction => $data->{rule_direction} // "undef", cols => [] }; + foreach my $field ($form->fields) { + my $prefix = ($stored->{rule_direction} eq "outbound" ? "out" : "in"); + if ($field->name =~ /^${prefix}_(.+)$/ && $data->{$field->name}) { + push @{$stored->{cols}}, { + name => $field->{element_attr}->{field}, + title => $c->loc($field->label), + search => 1, + }; + } + $stored->{states}{$field->name} = $data->{$field->name} // 0; + } + $c->session->{created_objects}{peeringoverview} = $stored; + NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for); +} + +sub csv :Chained('list') :PathPart('csv') :Args(0) { + my ($self, $c) = @_; + + my $stored = $c->session->{created_objects}{peeringoverview} //= {}; + + my $csv = Text::CSV_XS->new({ allow_whitespace => 1, binary => 1, keep_meta_info => 1 }); + my @hdr_cols = map { $_->{title} } @{$stored->{cols}}; + $csv->column_names(@hdr_cols); + my $io = IO::String->new(); + + my $rules_table = "voip_peer_rules"; + if ($stored->{rule_direction} eq "inbound") { + $rules_table = "voip_peer_inbound_rules"; + } + + my @sel_cols = map { scalar split(/\./, $_->{name}) == 3 + ? join('.', (split(/\./, $_->{name}))[1,2]) + : $_->{name} + } @{$stored->{cols}}; + my @as_cols = map { (my $t = $_) =~ s/\./_/g; $t } @sel_cols; + + my $filter = {}; + if ($stored->{search}) { + foreach my $col (@sel_cols) { + my $rel_col = ($col =~ /\./ ? $col : 'me.'.$col); + push @{$filter->{'-or'}}, + { $rel_col => { like => '%'.$stored->{search}.'%' } } + } + } + + my $join = { + 'join' => 'group', + 'select' => [ @sel_cols ], + 'as' => [ @as_cols ], + }; + if (grep { /voip_peer_hosts/ } map { $_->{name} } @{$stored->{cols}}) { + $join = { + 'join' => { 'group' => 'voip_peer_hosts' }, + 'select' => [ @sel_cols ], + 'as' => [ @as_cols ], + }; + } + + my $res = $c->model('DB')->resultset($rules_table)->search( + $filter, $join + ); + + $csv->print($io, [ @hdr_cols ]); + print $io "\n"; + foreach my $row ($res->all) { + my %data = $row->get_inflated_columns; + $csv->print($io, [ @data{@as_cols} ]); + print $io "\n"; + } + + my $date_str = POSIX::strftime('%Y-%m-%d_%H_%M_%S', localtime(time)); + + $c->response->header ('Content-Disposition' => 'attachment; filename="peering_overview_'. $date_str .'.csv"'); + $c->response->content_type('text/csv'); + $c->response->body(${$io->string_ref}); +} + +__PACKAGE__->meta->make_immutable; + +1; + +__END__ + +=head1 AUTHOR + +Kirill Solomko + +=head1 LICENSE + +This library is free software. You can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Field/PeeringGroupSelect.pm b/lib/NGCP/Panel/Field/PeeringGroupSelect.pm new file mode 100644 index 0000000000..e0218b46d9 --- /dev/null +++ b/lib/NGCP/Panel/Field/PeeringGroupSelect.pm @@ -0,0 +1,18 @@ +package NGCP::Panel::Field::PeeringGroupSelect; +use HTML::FormHandler::Moose; +extends 'HTML::FormHandler::Field::Compound'; + +has_field 'id' => ( + type => '+NGCP::Panel::Field::DataTable', + label => 'Peering Group', + do_label => 0, + do_wrapper => 0, + required => 1, + template => 'helpers/datatables_field.tt', + ajax_src => '/peering/ajax', + table_titles => ['#', 'Name', 'Priority', 'Description'], + table_fields => ['id', 'name', 'priority', 'description'], +); + +no Moose; +1; diff --git a/lib/NGCP/Panel/Form/Peering/InboundRule.pm b/lib/NGCP/Panel/Form/Peering/InboundRule.pm index fd0cb7b026..2bdfb4c801 100644 --- a/lib/NGCP/Panel/Form/Peering/InboundRule.pm +++ b/lib/NGCP/Panel/Form/Peering/InboundRule.pm @@ -73,6 +73,17 @@ has_field 'enabled' => ( }, ); +has_field 'group' => ( + type => '+NGCP::Panel::Field::PeeringGroupSelect', + label => 'Peering Group', + not_nullable => 1, + required => 1, + element_attr => { + rel => ['tooltip'], + title => ['A peering group the rule belongs to.'] + }, +); + has_field 'save' => ( type => 'Submit', value => 'Save', @@ -83,7 +94,7 @@ has_field 'save' => ( has_block 'fields' => ( tag => 'div', class => [qw/modal-body/], - render_list => [qw/field pattern reject_code reject_reason enabled/], + render_list => [qw/field pattern reject_code reject_reason enabled group/], ); has_block 'actions' => ( diff --git a/lib/NGCP/Panel/Form/Peering/Rule.pm b/lib/NGCP/Panel/Form/Peering/Rule.pm index 215be7e22d..3094e8f26c 100644 --- a/lib/NGCP/Panel/Form/Peering/Rule.pm +++ b/lib/NGCP/Panel/Form/Peering/Rule.pm @@ -55,6 +55,17 @@ has_field 'enabled' => ( }, ); +has_field 'group' => ( + type => '+NGCP::Panel::Field::PeeringGroupSelect', + label => 'Peering Group', + not_nullable => 1, + required => 1, + element_attr => { + rel => ['tooltip'], + title => ['A peering group the rule belongs to.'] + }, +); + has_field 'save' => ( type => 'Submit', value => 'Save', @@ -65,7 +76,7 @@ has_field 'save' => ( has_block 'fields' => ( tag => 'div', class => [qw/modal-body/], - render_list => [qw/ callee_prefix callee_pattern caller_pattern description enabled /], + render_list => [qw/ callee_prefix callee_pattern caller_pattern description enabled group/], ); has_block 'actions' => ( diff --git a/lib/NGCP/Panel/Form/PeeringOverview/Columns.pm b/lib/NGCP/Panel/Form/PeeringOverview/Columns.pm new file mode 100644 index 0000000000..78d01c4696 --- /dev/null +++ b/lib/NGCP/Panel/Form/PeeringOverview/Columns.pm @@ -0,0 +1,336 @@ +package NGCP::Panel::Form::PeeringOverview::Columns; + +use HTML::FormHandler::Moose; +extends 'HTML::FormHandler'; + +use HTML::FormHandler::Widget::Block::Bootstrap; + +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 'rule_direction' => ( + type => 'Select', + label => 'Direction', + widget => 'RadioGroup', + options => [ { checked => 1, label => 'Outbound', value => 'outbound' }, + { label => 'Inbound', value => 'inbound'} ], + required => 1, + element_attr => { + rel => ['tooltip'], + title => ['Peering rules direction (outbound or inbound).'], + }, +); + +has_field 'out_callee_prefix' => ( + type => 'Boolean', + label => 'Prefix', + default => 1, + element_attr => { + type => 'outbound', + field => 'callee_prefix', + }, +); + +has_field 'out_callee_pattern' => ( + type => 'Boolean', + label => 'Callee Pattern', + default => 0, + element_attr => { + type => 'outbound', + field => 'callee_pattern', + }, +); + +has_field 'out_caller_pattern' => ( + type => 'Boolean', + label => 'Caller Pattern', + default => 0, + element_attr => { + type => 'outbound', + field => 'caller_pattern', + }, +); + +has_field 'out_enabled' => ( + type => 'Boolean', + label => 'State', + default => 1, + element_attr => { + type => 'outbound', + field => 'enabled', + }, +); + +has_field 'out_description' => ( + type => 'Boolean', + label => 'Description', + default => 1, + element_attr => { + type => 'outbound', + field => 'description', + }, +); + +has_field 'out_group_name' => ( + type => 'Boolean', + label => 'Peer Group', + default => 1, + element_attr => { + type => 'outbound', + field => 'group.name', + }, +); + +has_field 'out_peer_name' => ( + type => 'Boolean', + label => 'Peer Name', + default => 1, + element_attr => { + type => 'outbound', + field => 'group.voip_peer_hosts.name', + }, +); + +has_field 'out_peer_host' => ( + type => 'Boolean', + label => 'Peer Host', + default => 1, + element_attr => { + type => 'outbound', + field => 'group.voip_peer_hosts.host', + }, +); + +has_field 'out_peer_ip' => ( + type => 'Boolean', + label => 'Peer IP', + default => 1, + element_attr => { + type => 'outbound', + field => 'group.voip_peer_hosts.ip', + }, +); + +has_field 'out_peer_port' => ( + type => 'Boolean', + label => 'Peer Port', + default => 0, + element_attr => { + type => 'outbound', + field => 'group.voip_peer_hosts.port', + }, +); + +has_field 'out_peer_transport' => ( + type => 'Boolean', + label => 'Peer Proto', + default => 0, + element_attr => { + type => 'outbound', + field => 'group.voip_peer_hosts.transport', + }, +); + +has_field 'out_peer_state' => ( + type => 'Boolean', + label => 'Peer State', + default => 1, + element_attr => { + type => 'outbound', + field => 'group.voip_peer_hosts.enabled', + }, +); + +has_field 'out_group_priority' => ( + type => 'Boolean', + label => 'Priority', + default => 1, + element_attr => { + type => 'outbound', + field => 'group.priority', + }, +); + +has_field 'out_peer_weight' => ( + type => 'Boolean', + label => 'Weight', + default => 1, + element_attr => { + type => 'outbound', + field => 'group.voip_peer_hosts.weight', + }, +); + +has_field 'in_field' => ( + type => 'Boolean', + label => 'Field', + default => 1, + element_attr => { + type => 'inbound', + field => 'field', + }, +); + +has_field 'in_pattern' => ( + type => 'Boolean', + label => 'Pattern', + default => 1, + element_attr => { + type => 'inbound', + field => 'pattern', + }, +); + +has_field 'in_reject_code' => ( + type => 'Boolean', + label => 'Reject Code', + default => 1, + element_attr => { + type => 'inbound', + field => 'reject_code', + }, +); + +has_field 'in_reject_reason' => ( + type => 'Boolean', + label => 'Reject Reason', + default => 1, + element_attr => { + type => 'inbound', + field => 'reject_reason', + }, +); + +has_field 'in_priority' => ( + type => 'Boolean', + label => 'Rule Priority', + default => 1, + element_attr => { + type => 'inbound', + field => 'priority', + }, +); + +has_field 'in_enabled' => ( + type => 'Boolean', + label => 'State', + default => 1, + element_attr => { + type => 'inbound', + field => 'enabled', + }, +); + +has_field 'in_group_name' => ( + type => 'Boolean', + label => 'Peer Group', + default => 1, + element_attr => { + type => 'inbound', + field => 'group.name', + }, +); + +has_field 'in_peer_name' => ( + type => 'Boolean', + label => 'Peer Name', + default => 1, + element_attr => { + type => 'inbound', + field => 'group.voip_peer_hosts.name', + }, +); + +has_field 'in_peer_host' => ( + type => 'Boolean', + label => 'Peer Host', + default => 1, + element_attr => { + type => 'inbound', + field => 'group.voip_peer_hosts.host', + }, +); + +has_field 'in_peer_ip' => ( + type => 'Boolean', + label => 'Peer IP', + default => 1, + element_attr => { + type => 'inbound', + field => 'group.voip_peer_hosts.ip', + }, +); + +has_field 'in_peer_port' => ( + type => 'Boolean', + label => 'Peer Port', + default => 0, + element_attr => { + type => 'inbound', + field => 'group.voip_peer_hosts.port', + }, +); + +has_field 'in_peer_transport' => ( + type => 'Boolean', + label => 'Peer Proto', + default => 0, + element_attr => { + type => 'inbound', + field => 'group.voip_peer_hosts.transport', + }, +); + +has_field 'in_peer_state' => ( + type => 'Boolean', + label => 'Peer State', + default => 1, + element_attr => { + type => 'inbound', + field => 'group.voip_peer_hosts.enabled', + }, +); + +has_field 'in_group_priority' => ( + type => 'Boolean', + label => 'Priority', + default => 1, + element_attr => { + type => 'inbound', + field => 'group.priority', + }, +); + +has_field 'in_peer_weight' => ( + type => 'Boolean', + label => 'Weight', + default => 1, + element_attr => { + type => 'inbound', + field => 'group.voip_peer_hosts.weight', + }, +); + +has_field 'save' => ( + type => 'Submit', + value => 'Save', + element_class => [qw/btn btn-primary/], + label => '', +); + +has_block 'fields' => ( + tag => 'div', + class => [qw/modal-body/], +); + +has_block 'actions' => ( + tag => 'div', + class => [qw/modal-footer/], + render_list => [qw/save/], +); + +1; + +# vim: set tabstop=4 expandtab: diff --git a/share/templates/peeringoverview/edit.tt b/share/templates/peeringoverview/edit.tt new file mode 100644 index 0000000000..1d88888701 --- /dev/null +++ b/share/templates/peeringoverview/edit.tt @@ -0,0 +1,98 @@ + + +[% site_config.title = c.loc('Peering Overview Columns'); + + helper.identifier = 'call_routing_verify'; + helper.close_target = close_target; + + PROCESS "helpers/modal.tt"; + + modal_header(m.create_flag=0, + m.name = c.loc("Peering Overview Columns")); + + helper.form = translate_form(form); +-%] + + +
+ + [% helper.form.render_start %] +
+
+
+
+
+
+ + [% helper.form.field('rule_direction').render %] +
+
+ + [% tmpcol = 0 -%] + [% FOREACH field IN helper.form.fields -%] + [% IF field.element_attr.type == 'outbound' -%] + [% IF tmpcol mod 2 == 0 -%] + + [% ELSE -%] + + [% END -%] + [% tmpcol = tmpcol + 1 -%] + [% END -%] + [% END -%] +
[% field.render -%][% field.render -%]
+
+
+ + [% tmpcol = 0 -%] + [% FOREACH field IN helper.form.fields -%] + [% IF field.element_attr.type == 'inbound' -%] + [% IF tmpcol mod 2 == 0 -%] + + [% ELSE -%] + + [% END -%] + [% tmpcol = tmpcol + 1 -%] + [% END -%] + [% END -%] +
[% field.render -%][% field.render -%]
+
+
+ + [% helper.form.field('save').render %] + + [% helper.form.render_end %] +

+
+ + + +[% + modal_footer(); + modal_script(m.close_target = helper.close_target); +-%] + + +[% # vim: set tabstop=4 syntax=html expandtab: -%] diff --git a/share/templates/peeringoverview/list.tt b/share/templates/peeringoverview/list.tt new file mode 100644 index 0000000000..00bf202765 --- /dev/null +++ b/share/templates/peeringoverview/list.tt @@ -0,0 +1,29 @@ +[% site_config.title = c.loc('Peering Overview') -%] +[% + helper.name = c.loc('Peering Overview'); + helper.identifier = 'PeeringOverview'; + helper.data = po; + helper.messages = messages; + helper.dt_columns = po_dt_columns; + helper.length_change = 1; + + helper.close_target = close_target; + helper.create_flag = create_flag; + helper.edit_flag = edit_flag; + helper.form_object = form; + helper.ajax_uri = c.uri_for_action('/peeringoverview/ajax'); + + helper.dt_buttons = [ + { name = c.loc('Rule'), uri = "/peering/'+full.peer_group_id+'/rules/'+full.id+'/edit", class = 'btn-small btn-primary', icon = 'icon-edit' }, + { name = c.loc('Group'), uri = "/peering/'+full.peer_group_id+'/edit", class = 'btn-small btn-primary', icon = 'icon-edit' }, + { name = c.loc('Host'), uri = "/peering/'+full.peer_group_id+'/servers/'+full.peer_host_id+'/edit", class = 'btn-small btn-primary', icon = 'icon-edit' }, + { name = c.loc('Delete'), uri = "/peering/'+full.peer_group_id+'/rules/'+full.id+'/delete", class = 'btn-small btn-secondary', icon = 'icon-remove' }, + ]; + helper.top_buttons = [ + { name = c.loc('Columns'), uri = c.uri_for_action('/peeringoverview/edit'), icon = 'icon-edit' }, + { name = c.loc('Download as CSV'), uri = c.uri_for_action('/peeringoverview/csv'), icon = 'icon-star' }, + ]; + + PROCESS 'helpers/datatables.tt'; +-%] +[% # vim: set tabstop=4 syntax=html expandtab: -%] diff --git a/share/templates/widgets/admin_topmenu_settings.tt b/share/templates/widgets/admin_topmenu_settings.tt index 7da1a327d2..589837c531 100644 --- a/share/templates/widgets/admin_topmenu_settings.tt +++ b/share/templates/widgets/admin_topmenu_settings.tt @@ -34,6 +34,7 @@