diff --git a/lib/App/Netdisco/DB.pm b/lib/App/Netdisco/DB.pm index 7e864c36..abb2af1a 100644 --- a/lib/App/Netdisco/DB.pm +++ b/lib/App/Netdisco/DB.pm @@ -11,7 +11,7 @@ __PACKAGE__->load_namespaces( ); our # try to hide from kwalitee - $VERSION = 45; # schema version used for upgrades, keep as integer + $VERSION = 46; # schema version used for upgrades, keep as integer use Path::Class; use File::ShareDir 'dist_dir'; diff --git a/lib/App/Netdisco/DB/Result/Device.pm b/lib/App/Netdisco/DB/Result/Device.pm index e3a794d8..7a6e37e0 100644 --- a/lib/App/Netdisco/DB/Result/Device.pm +++ b/lib/App/Netdisco/DB/Result/Device.pm @@ -191,6 +191,15 @@ Returns the row from the community string table, if one exists. __PACKAGE__->might_have( community => 'App::Netdisco::DB::Result::Community', 'ip'); +=head2 throughput + +Returns a sum of speeds on all ports on the device. + +=cut + +__PACKAGE__->has_one( + throughput => 'App::Netdisco::DB::Result::Virtual::DevicePortSpeed', 'ip'); + =head1 ADDITIONAL METHODS =head2 is_pseudo diff --git a/lib/App/Netdisco/DB/Result/NetmapPositions.pm b/lib/App/Netdisco/DB/Result/NetmapPositions.pm new file mode 100644 index 00000000..d80c7e8a --- /dev/null +++ b/lib/App/Netdisco/DB/Result/NetmapPositions.pm @@ -0,0 +1,25 @@ +use utf8; +package App::Netdisco::DB::Result::NetmapPositions; + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; +__PACKAGE__->table("netmap_positions"); +__PACKAGE__->add_columns( + "id", + { data_type => "integer", is_nullable => 0, is_auto_increment => 1 }, + "device_groups", + { data_type => "text[]", is_nullable => 0 }, + "vlan", + { data_type => "integer", is_nullable => 0, default => 0 }, + "positions", + { data_type => "text", is_nullable => 0 }, +); + +__PACKAGE__->set_primary_key("id"); + +__PACKAGE__->add_unique_constraint( + "netmap_positions_device_groups_vlan_key" => [qw/device_groups vlan/]); + +1; diff --git a/lib/App/Netdisco/DB/Result/Virtual/DeviceLinks.pm b/lib/App/Netdisco/DB/Result/Virtual/DeviceLinks.pm index 545e9ce2..cd6e0255 100644 --- a/lib/App/Netdisco/DB/Result/Virtual/DeviceLinks.pm +++ b/lib/App/Netdisco/DB/Result/Virtual/DeviceLinks.pm @@ -7,18 +7,37 @@ use base 'DBIx::Class::Core'; __PACKAGE__->table_class('DBIx::Class::ResultSource::View'); +# note to future devs: +# this query does not use the slave_of field in device_port table to group +# ports because what we actually want is total b/w between devices on all +# links, regardless of whether those links are in an aggregate. + __PACKAGE__->table('device_links'); __PACKAGE__->result_source_instance->is_virtual(1); __PACKAGE__->result_source_instance->view_definition(<add_columns( 'left_ip' => { data_type => 'inet', }, - 'left_port' => { + 'left_dns' => { data_type => 'text', }, + 'left_name' => { + data_type => 'text', + }, + 'left_port' => { + data_type => '[text]', + }, + 'left_descr' => { + data_type => '[text]', + }, + 'aggspeed' => { + data_type => 'integer', + }, + 'aggports' => { + data_type => 'integer', + }, 'right_ip' => { data_type => 'inet', }, - 'right_port' => { + 'right_dns' => { data_type => 'text', }, + 'right_name' => { + data_type => 'text', + }, + 'right_port' => { + data_type => '[text]', + }, + 'right_descr' => { + data_type => '[text]', + }, ); __PACKAGE__->has_many('left_vlans', 'App::Netdisco::DB::Result::DevicePortVlan', - { 'foreign.ip' => 'self.left_ip', 'foreign.port' => 'self.left_port' }, - { join_type => 'INNER' } ); + sub { + my $args = shift; + return { + "$args->{foreign_alias}.ip" => { -ident => "$args->{self_alias}.left_ip" }, + "$args->{self_alias}.left_port" => { '@>' => \"ARRAY[$args->{foreign_alias}.port]" }, + }; + } +); __PACKAGE__->has_many('right_vlans', 'App::Netdisco::DB::Result::DevicePortVlan', - { 'foreign.ip' => 'self.right_ip', 'foreign.port' => 'self.right_port' }, - { join_type => 'INNER' } ); + sub { + my $args = shift; + return { + "$args->{foreign_alias}.ip" => { -ident => "$args->{self_alias}.right_ip" }, + "$args->{self_alias}.right_port" => { '@>' => \"ARRAY[$args->{foreign_alias}.port]" }, + }; + } +); 1; diff --git a/lib/App/Netdisco/DB/Result/Virtual/DevicePortSpeed.pm b/lib/App/Netdisco/DB/Result/Virtual/DevicePortSpeed.pm new file mode 100644 index 00000000..5996c828 --- /dev/null +++ b/lib/App/Netdisco/DB/Result/Virtual/DevicePortSpeed.pm @@ -0,0 +1,36 @@ +package App::Netdisco::DB::Result::Virtual::DevicePortSpeed; + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +__PACKAGE__->table_class('DBIx::Class::ResultSource::View'); + +__PACKAGE__->table('device_port_speed'); +__PACKAGE__->result_source_instance->is_virtual(1); +__PACKAGE__->result_source_instance->view_definition(<add_columns( + 'total' => { + data_type => 'integer', + }, +); + +__PACKAGE__->belongs_to('device', 'App::Netdisco::DB::Result::Device', + { 'foreign.ip' => 'self.ip' }); + +1; diff --git a/lib/App/Netdisco/Util/Statistics.pm b/lib/App/Netdisco/Util/Statistics.pm index 1375f1f9..fe14f507 100644 --- a/lib/App/Netdisco/Util/Statistics.pm +++ b/lib/App/Netdisco/Util/Statistics.pm @@ -47,8 +47,7 @@ sub update_stats { device_ip_count => $schema->resultset('DeviceIp')->count_rs->as_query, device_link_count => - $schema->resultset('Virtual::DeviceLinks') - ->count_rs({'me.left_ip' => {'>', \'me.right_ip'}})->as_query, + $schema->resultset('Virtual::DeviceLinks')->count_rs->as_query, device_port_count => $schema->resultset('DevicePort')->count_rs->as_query, device_port_up_count => diff --git a/lib/App/Netdisco/Web.pm b/lib/App/Netdisco/Web.pm index 3cae4070..6702c416 100644 --- a/lib/App/Netdisco/Web.pm +++ b/lib/App/Netdisco/Web.pm @@ -6,6 +6,7 @@ use Dancer::Plugin::Ajax; use Dancer::Plugin::DBIC; use Dancer::Plugin::Auth::Extensible; +use URI (); use Socket6 (); # to ensure dependency is met use HTML::Entities (); # to ensure dependency is met use URI::QueryParam (); # part of URI, to add helper methods @@ -129,6 +130,12 @@ hook 'before_template' => sub { # allow portable dynamic content $tokens->{uri_for} = sub { uri_for(@_)->path_query }; + # current query string to all resubmit from within ajax template + my $queryuri = URI->new(); + $queryuri->query_param($_ => param($_)) + for grep {$_ ne 'return_url'} keys %{params()}; + $tokens->{my_query} = $queryuri->query(); + # access to logged in user's roles $tokens->{user_has_role} = sub { user_has_role(@_) }; diff --git a/lib/App/Netdisco/Web/Device.pm b/lib/App/Netdisco/Web/Device.pm index bf411c0b..143389b8 100644 --- a/lib/App/Netdisco/Web/Device.pm +++ b/lib/App/Netdisco/Web/Device.pm @@ -16,6 +16,7 @@ set('connected_properties' => [ ]); hook 'before_template' => sub { + my $tokens = shift; my $defaults = var('sidebar_defaults')->{'device_ports'} or return; @@ -34,6 +35,13 @@ hook 'before_template' => sub { } } + # used in the device search sidebar template to set selected items + foreach my $opt (qw/devgrp/) { + my $p = (ref [] eq ref param($opt) ? param($opt) + : (param($opt) ? [param($opt)] : [])); + $tokens->{"${opt}_lkp"} = { map { $_ => 1 } @$p }; + } + return if param('reset') or not var('sidebar_key') or (var('sidebar_key') ne 'device_ports'); @@ -69,6 +77,7 @@ get '/device' => require_login sub { params->{'tab'} ||= 'details'; template 'device', { display_name => ($others ? $first->ip : ($first->dns || $first->ip)), + devgrp_list => setting('host_group_displaynames'), device => params->{'tab'}, }; }; diff --git a/lib/App/Netdisco/Web/Plugin/AdminTask/Topology.pm b/lib/App/Netdisco/Web/Plugin/AdminTask/Topology.pm index 52a23390..afe06c04 100644 --- a/lib/App/Netdisco/Web/Plugin/AdminTask/Topology.pm +++ b/lib/App/Netdisco/Web/Plugin/AdminTask/Topology.pm @@ -29,7 +29,7 @@ sub _sanity_ok { return 1; } -ajax '/ajax/control/admin/topology/add' => require_role admin => sub { +ajax '/ajax/control/admin/topology/add' => require_role port_control => sub { send_error('Bad Request', 400) unless _sanity_ok(); my $device = schema('netdisco')->resultset('Topology') @@ -78,7 +78,7 @@ ajax '/ajax/control/admin/topology/add' => require_role admin => sub { }; }; -ajax '/ajax/control/admin/topology/del' => require_role admin => sub { +ajax '/ajax/control/admin/topology/del' => require_role port_control => sub { send_error('Bad Request', 400) unless _sanity_ok(); schema('netdisco')->txn_do(sub { @@ -129,7 +129,7 @@ ajax '/ajax/control/admin/topology/del' => require_role admin => sub { }; }; -ajax '/ajax/content/admin/topology' => require_role admin => sub { +ajax '/ajax/content/admin/topology' => require_role port_control => sub { my $set = schema('netdisco')->resultset('Topology') ->search({},{order_by => [qw/dev1 dev2 port1/]}); diff --git a/lib/App/Netdisco/Web/Plugin/Device/Neighbors.pm b/lib/App/Netdisco/Web/Plugin/Device/Neighbors.pm index 1711b2b7..5330ab3e 100644 --- a/lib/App/Netdisco/Web/Plugin/Device/Neighbors.pm +++ b/lib/App/Netdisco/Web/Plugin/Device/Neighbors.pm @@ -5,6 +5,10 @@ use Dancer::Plugin::Ajax; use Dancer::Plugin::DBIC; use Dancer::Plugin::Auth::Extensible; +use SNMP::Info (); +use List::Util 'first'; +use List::MoreUtils (); +use App::Netdisco::Util::Permission 'check_acl_only'; use App::Netdisco::Web::Plugin; register_device_tab({ tag => 'netmap', label => 'Neighbors' }); @@ -14,108 +18,211 @@ ajax '/ajax/content/device/netmap' => require_login sub { template 'ajax/device/netmap.tt', {}, { layout => undef }; }; -sub _get_name { - my $ip = shift; - my $domain = quotemeta( setting('domain_suffix') || '' ); - - (my $dns = (var('devices')->{$ip} || '')) =~ s/$domain$//; - return ($dns || $ip); -} - -sub _add_children { - my ($ptr, $childs, $step, $limit) = @_; - - return $step if $limit and $step > $limit; - my @legit = (); - my $max = $step; - - foreach my $c (@$childs) { - next if exists var('seen')->{$c}; - var('seen')->{$c}++; - push @legit, $c; - push @{$ptr}, { - ip => $c, - name => _get_name($c), - }; - } - - for (my $i = 0; $i < @legit; $i++) { - $ptr->[$i]->{children} = []; - my $nm = _add_children($ptr->[$i]->{children}, var('links')->{$legit[$i]}, - ($step + 1), $limit); - $max = $nm if $nm > $max; - } - - return $max; -} - -# d3 seems not to use proper ajax semantics, so get instead of ajax -get '/ajax/data/device/netmap' => require_login sub { - my $q = param('q'); +ajax '/ajax/data/device/netmappositions' => require_login sub { + my $p = param('positions') or send_error('Missing positions', 400); + my $positions = from_json($p) or send_error('Bad positions', 400); + send_error('Bad positions', 400) unless ref [] eq ref $positions; my $vlan = param('vlan'); undef $vlan if (defined $vlan and $vlan !~ m/^\d+$/); - my $depth = (param('depth') || 8); - undef $depth if (defined $depth and $depth !~ m/^\d+$/); + my $mapshow = param('mapshow'); + return if !defined $mapshow or $mapshow !~ m/^(?:all|only)$/; - my $device = schema('netdisco')->resultset('Device') + # list of groups selected by user and passed in param + my $devgrp = (ref [] eq ref param('devgrp') ? param('devgrp') : [param('devgrp')]); + # list of groups validated as real host groups and named host groups + my @hgrplist = List::MoreUtils::uniq + grep { exists setting('host_group_displaynames')->{$_} } + grep { exists setting('host_groups')->{$_} } + grep { defined } @{ $devgrp }; + return if $mapshow eq 'only' and 0 == scalar @hgrplist; + push(@hgrplist, '__ANY__') if 0 == scalar @hgrplist; + + my %clean = (); + POSITION: foreach my $pos (@$positions) { + next unless ref {} eq ref $pos; + foreach my $k (qw/ID x y/) { + next POSITION unless exists $pos->{$k}; + next POSITION unless $pos->{$k} =~ m/^[[:word:]\.-]+$/; + } + $clean{$pos->{ID}} = { x => $pos->{x}, y => $pos->{y} }; + } + return unless scalar keys %clean; + + my $posrow = schema('netdisco')->resultset('NetmapPositions')->find({ + device_groups => \[ '= ?', [device_groups => [sort @hgrplist]] ], + vlan => ($vlan || 0)}); + if ($posrow) { + $posrow->update({ positions => to_json(\%clean) }); + } + else { + schema('netdisco')->resultset('NetmapPositions')->create({ + device_groups => [sort @hgrplist], + vlan => ($vlan || 0), + positions => to_json(\%clean), + }); + } +}; + +sub to_speed { + my $speed = shift or return ''; + $speed = SNMP::Info::munge_highspeed($speed); + $speed =~ s/(?:\s|bps)//g; + return $speed; +} + +sub make_node_infostring { + my $node = shift or return ''; + my $fmt = ('Serial: %s
Vendor/Model: %s / %s
' + .'OS/Version: %s / %s
Uptime: %s
' + .'Location: %s
Contact: %s'); + return sprintf $fmt, + map {defined $_ ? $_ : ''} + map {$node->$_} + (qw/serial vendor model os os_ver uptime_age location contact/); +} + +sub make_link_infostring { + my $link = shift or return ''; + + my $domain = quotemeta( setting('domain_suffix') || '' ); + (my $left_name = lc($link->{left_dns} || $link->{left_name} || $link->{left_ip})) =~ s/$domain$//; + (my $right_name = lc($link->{right_dns} || $link->{right_name} || $link->{right_ip})) =~ s/$domain$//; + + if ($link->{aggports} == 1) { + return sprintf '%s:%s (%s)
%s:%s (%s)', + $left_name, $link->{left_port}->[0], $link->{left_descr}->[0], + $right_name, $link->{right_port}->[0], $link->{right_descr}->[0]; + } + else { + return sprintf '%s:(%s)
%s:(%s)', + $left_name, join(',', @{$link->{left_port}}), + $right_name, join(',', @{$link->{right_port}}); + } +} + +ajax '/ajax/data/device/netmap' => require_login sub { + my $q = param('q'); + my $qdev = schema('netdisco')->resultset('Device') ->search_for_device($q) or send_error('Bad device', 400); - my $start = $device->ip; - my @devices = schema('netdisco')->resultset('Device')->search({}, { - result_class => 'DBIx::Class::ResultClass::HashRefInflator', - columns => ['ip', 'dns', 'name'], - })->all; - var(devices => { map { $_->{ip} => lc($_->{dns} || $_->{name} || '') } - @devices }); + my $vlan = param('vlan'); + undef $vlan if (defined $vlan and $vlan !~ m/^\d+$/); - var(links => {}); - my $rs = schema('netdisco')->resultset('Virtual::DeviceLinks')->search({}, { - columns => [qw/left_ip right_ip/], - result_class => 'DBIx::Class::ResultClass::HashRefInflator', - }); + my $mapshow = (param('mapshow') || 'neighbors'); + $mapshow = 'neighbors' if $mapshow !~ m/^(?:all|neighbors|only)$/; + $mapshow = 'all' unless $qdev->in_storage; + + # list of groups selected by user and passed in param + my $devgrp = (ref [] eq ref param('devgrp') ? param('devgrp') : [param('devgrp')]); + # list of groups validated as real host groups and named host groups + my @hgrplist = List::MoreUtils::uniq + grep { exists setting('host_group_displaynames')->{$_} } + grep { exists setting('host_groups')->{$_} } + grep { defined } @{ $devgrp }; + + my %ok_dev = (); + my %logvals = (); + my %metadata = (); + my %data = ( nodes => [], links => [] ); + my $domain = quotemeta( setting('domain_suffix') || '' ); + + # LINKS + + my $links = schema('netdisco')->resultset('Virtual::DeviceLinks')->search({ + ($mapshow eq 'neighbors' ? ( -or => [ + { left_ip => $qdev->ip }, + { right_ip => $qdev->ip }, + ]) : ()) + }, { result_class => 'DBIx::Class::ResultClass::HashRefInflator' }); if ($vlan) { - $rs = $rs->search({ - 'left_vlans.vlan' => $vlan, - 'right_vlans.vlan' => $vlan, + $links = $links->search({ + -or => [ + { 'left_vlans.vlan' => $vlan }, + { 'right_vlans.vlan' => $vlan }, + ], }, { join => [qw/left_vlans right_vlans/], }); } - while (my $l = $rs->next) { - var('links')->{ $l->{left_ip} } ||= []; - push @{ var('links')->{ $l->{left_ip} } }, $l->{right_ip}; + while (my $link = $links->next) { + push @{$data{'links'}}, { + FROMID => $link->{left_ip}, + TOID => $link->{right_ip}, + INFOSTRING => make_link_infostring($link), + SPEED => to_speed($link->{aggspeed}), + }; + + ++$ok_dev{$link->{left_ip}}; + ++$ok_dev{$link->{right_ip}}; } - my %tree = ( - ip => $start, - name => _get_name($start), # dns or sysname or ip - children => [], - ); + # DEVICES (NODES) - var(seen => {$start => 1}); - my $max = _add_children($tree{children}, var('links')->{$start}, 1, $depth); - $tree{scale} = $max; + my $posrow = schema('netdisco')->resultset('NetmapPositions')->find({ + device_groups => \[ '= ?', + [device_groups => [$mapshow eq 'all' ? '__ANY__' : (sort @hgrplist)]] ], + vlan => ($vlan || 0)}); + my $pos_for = from_json( $posrow ? $posrow->positions : '{}' ); - content_type('application/json'); - to_json(\%tree); -}; + my $devices = schema('netdisco')->resultset('Device')->search({}, { + '+select' => [\'floor(log(throughput.total))'], '+as' => ['log'], + join => 'throughput', + })->with_times; -ajax '/ajax/data/device/alldevicelinks' => require_login sub { - my $rs = schema('netdisco')->resultset('Virtual::DeviceLinks')->search({}, { - result_class => 'DBIx::Class::ResultClass::HashRefInflator', - }); + DEVICE: while (my $device = $devices->next) { + # if in neighbors or vlan mode then use %ok_dev to filter + next DEVICE if (($mapshow eq 'neighbors') or $vlan) + and (not $ok_dev{$device->ip}); - my %tree = (); - while (my $l = $rs->next) { - push @{ $tree{ $l->{left_ip} } }, $l->{right_ip}; + # if in only mode then use ACLs to filter + my $first_hgrp = + first { check_acl_only($device, setting('host_groups')->{$_}) } @hgrplist; + next DEVICE if $mapshow eq 'only' and not $first_hgrp; + + ++$logvals{ $device->get_column('log') || 1 }; + (my $name = lc($device->dns || $device->name || $device->ip)) =~ s/$domain$//; + + my $node = { + ID => $device->ip, + SIZEVALUE => (param('dynamicsize') ? + (($device->get_column('log') || 1) * 1000) : 3000), + (param('colorgroups') ? + (COLORVALUE => ($first_hgrp ? setting('host_group_displaynames')->{$first_hgrp} + : 'Other')) : ()), + LABEL => (param('showips') + ? (($name eq $device->ip) ? $name : ($name .' '. $device->ip)) : $name), + ORIG_LABEL => $name, + INFOSTRING => make_node_infostring($device), + LINK => uri_for('/device', { + tab => 'netmap', + q => $device->ip, + firstsearch => 'on', + })->path_query, + }; + + if ($mapshow ne 'neighbors' and exists $pos_for->{$device->ip}) { + $node->{'fixed'} = 1; + $node->{'x'} = $pos_for->{$device->ip}->{'x'}; + $node->{'y'} = $pos_for->{$device->ip}->{'y'}; + } + else { + ++$metadata{'newnodes'}; + } + + push @{$data{'nodes'}}, $node; + $metadata{'centernode'} = $device->ip + if $qdev and $qdev->in_storage and $device->ip eq $qdev->ip; } + # to help get a sensible range of node sizes + $metadata{'numsizes'} = scalar keys %logvals; + content_type('application/json'); - to_json(\%tree); + to_json({ data => \%data, %metadata }); }; true; diff --git a/share/config.yml b/share/config.yml index cc0c9bde..e5ffbc0e 100644 --- a/share/config.yml +++ b/share/config.yml @@ -87,20 +87,20 @@ web_plugins: extra_web_plugins: [] sidebar_defaults: search_node: - stamps: {default: checked } - deviceports: {default: checked } - show_vendor: {default: null } - archived: {default: null } - partial: {default: null } - age_invert: {default: null } - daterange: {default: null } - mac_format: {default: IEEE } + stamps: { default: checked } + deviceports: { default: checked } + show_vendor: { default: null } + archived: { default: null } + partial: { default: null } + age_invert: { default: null } + daterange: { default: null } + mac_format: { default: IEEE } search_port: - partial: {default: null } - uplink: {default: null } - ethernet: {default: checked } + partial: { default: null } + uplink: { default: null } + ethernet: { default: checked } search_device: - matchall: {default: checked } + matchall: { default: checked } device_ports: c_admin: { label: 'Port Controls', default: null, idx: 0 } c_port: { label: 'Port', default: checked, idx: 1 } @@ -132,9 +132,15 @@ sidebar_defaults: age_unit: { default: months } mac_format: { default: IEEE } neigh_id: { default: null } + device_netmap: + showips: { default: null } + showspeed: { default: null } + mapshow: { default: neighbors } + colorgroups: { default: checked } + dynamicsize: { default: null } report_moduleinventory: - fruonly: {default: checked } - matchall: {default: checked } + fruonly: { default: checked } + matchall: { default: checked } device_port_col_idx_left: 0 device_port_col_idx_mid: 2 device_port_col_idx_right: -1 @@ -172,10 +178,13 @@ login_logo: "" # mibhome is discovered from environment # mibdirs defaults to contents of mibhome host_groups: + __ANY__: + - 'any' __LOCAL_ADDRESSES__: - '::1' - 'fe80::/10' - '127.0.0.0/8' +host_group_displaynames: {} device_identity: [] community: [] community_rw: [] diff --git a/share/public/css/awesome-bootstrap-checkbox.css b/share/public/css/awesome-bootstrap-checkbox.css new file mode 100644 index 00000000..ece828e1 --- /dev/null +++ b/share/public/css/awesome-bootstrap-checkbox.css @@ -0,0 +1,329 @@ +/* +.checkbox { + padding-left: 20px; +} +.checkbox label { + display: inline-block; + vertical-align: middle; + position: relative; + padding-left: 5px; +} +.checkbox label::before { + content: ""; + display: inline-block; + position: absolute; + width: 17px; + height: 17px; + left: 0; + margin-left: -20px; + border: 1px solid #cccccc; + border-radius: 3px; + background-color: #fff; + -webkit-transition: border 0.15s ease-in-out, color 0.15s ease-in-out; + -o-transition: border 0.15s ease-in-out, color 0.15s ease-in-out; + transition: border 0.15s ease-in-out, color 0.15s ease-in-out; +} +.checkbox label::after { + display: inline-block; + position: absolute; + width: 16px; + height: 16px; + left: 0; + top: 0; + margin-left: -20px; + padding-left: 3px; + padding-top: 1px; + font-size: 11px; + color: #555555; + line-height: 1.4; +} +.checkbox input[type="checkbox"], +.checkbox input[type="radio"] { + opacity: 0; + z-index: 1; + cursor: pointer; +} +.checkbox input[type="checkbox"]:focus + label::before, +.checkbox input[type="radio"]:focus + label::before { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +.checkbox input[type="checkbox"]:checked + label::after, +.checkbox input[type="radio"]:checked + label::after { + font-family: "FontAwesome"; + content: "\f00c"; +} +.checkbox input[type="checkbox"]:indeterminate + label::after, +.checkbox input[type="radio"]:indeterminate + label::after { + display: block; + content: ""; + width: 10px; + height: 3px; + background-color: #555555; + border-radius: 2px; + margin-left: -16.5px; + margin-top: 7px; +} +.checkbox input[type="checkbox"]:disabled, +.checkbox input[type="radio"]:disabled { + cursor: not-allowed; +} +.checkbox input[type="checkbox"]:disabled + label, +.checkbox input[type="radio"]:disabled + label { + opacity: 0.65; +} +.checkbox input[type="checkbox"]:disabled + label::before, +.checkbox input[type="radio"]:disabled + label::before { + background-color: #eeeeee; + cursor: not-allowed; +} +.checkbox.checkbox-circle label::before { + border-radius: 50%; +} +.checkbox.checkbox-inline { + margin-top: 0; +} + +.checkbox-primary input[type="checkbox"]:checked + label::before, +.checkbox-primary input[type="radio"]:checked + label::before { + background-color: #337ab7; + border-color: #337ab7; +} +.checkbox-primary input[type="checkbox"]:checked + label::after, +.checkbox-primary input[type="radio"]:checked + label::after { + color: #fff; +} + +.checkbox-danger input[type="checkbox"]:checked + label::before, +.checkbox-danger input[type="radio"]:checked + label::before { + background-color: #d9534f; + border-color: #d9534f; +} +.checkbox-danger input[type="checkbox"]:checked + label::after, +.checkbox-danger input[type="radio"]:checked + label::after { + color: #fff; +} + +.checkbox-info input[type="checkbox"]:checked + label::before, +.checkbox-info input[type="radio"]:checked + label::before { + background-color: #5bc0de; + border-color: #5bc0de; +} +.checkbox-info input[type="checkbox"]:checked + label::after, +.checkbox-info input[type="radio"]:checked + label::after { + color: #fff; +} + +.checkbox-warning input[type="checkbox"]:checked + label::before, +.checkbox-warning input[type="radio"]:checked + label::before { + background-color: #f0ad4e; + border-color: #f0ad4e; +} +.checkbox-warning input[type="checkbox"]:checked + label::after, +.checkbox-warning input[type="radio"]:checked + label::after { + color: #fff; +} + +.checkbox-success input[type="checkbox"]:checked + label::before, +.checkbox-success input[type="radio"]:checked + label::before { + background-color: #5cb85c; + border-color: #5cb85c; +} +.checkbox-success input[type="checkbox"]:checked + label::after, +.checkbox-success input[type="radio"]:checked + label::after { + color: #fff; +} + +.checkbox-primary input[type="checkbox"]:indeterminate + label::before, +.checkbox-primary input[type="radio"]:indeterminate + label::before { + background-color: #337ab7; + border-color: #337ab7; +} + +.checkbox-primary input[type="checkbox"]:indeterminate + label::after, +.checkbox-primary input[type="radio"]:indeterminate + label::after { + background-color: #fff; +} + +.checkbox-danger input[type="checkbox"]:indeterminate + label::before, +.checkbox-danger input[type="radio"]:indeterminate + label::before { + background-color: #d9534f; + border-color: #d9534f; +} + +.checkbox-danger input[type="checkbox"]:indeterminate + label::after, +.checkbox-danger input[type="radio"]:indeterminate + label::after { + background-color: #fff; +} + +.checkbox-info input[type="checkbox"]:indeterminate + label::before, +.checkbox-info input[type="radio"]:indeterminate + label::before { + background-color: #5bc0de; + border-color: #5bc0de; +} + +.checkbox-info input[type="checkbox"]:indeterminate + label::after, +.checkbox-info input[type="radio"]:indeterminate + label::after { + background-color: #fff; +} + +.checkbox-warning input[type="checkbox"]:indeterminate + label::before, +.checkbox-warning input[type="radio"]:indeterminate + label::before { + background-color: #f0ad4e; + border-color: #f0ad4e; +} + +.checkbox-warning input[type="checkbox"]:indeterminate + label::after, +.checkbox-warning input[type="radio"]:indeterminate + label::after { + background-color: #fff; +} + +.checkbox-success input[type="checkbox"]:indeterminate + label::before, +.checkbox-success input[type="radio"]:indeterminate + label::before { + background-color: #5cb85c; + border-color: #5cb85c; +} + +.checkbox-success input[type="checkbox"]:indeterminate + label::after, +.checkbox-success input[type="radio"]:indeterminate + label::after { + background-color: #fff; +} +*/ + +.radio { + padding-left: 20px; +} +.radio label { + display: inline-block; + vertical-align: middle; + position: relative; + padding-left: 5px; +} +.radio label::before { + content: ""; + display: inline-block; + position: absolute; + width: 17px; + height: 17px; + left: 0; + margin-left: -20px; + border: 1px solid #cccccc; + border-radius: 50%; + background-color: #fff; + -webkit-transition: border 0.15s ease-in-out; + -o-transition: border 0.15s ease-in-out; + transition: border 0.15s ease-in-out; +} +.radio label::after { + display: inline-block; + position: absolute; + content: " "; + width: 11px; + height: 11px; + left: 4px; + top: 4px; + margin-left: -20px; + border-radius: 50%; + background-color: #555555; + -webkit-transform: scale(0, 0); + -ms-transform: scale(0, 0); + -o-transform: scale(0, 0); + transform: scale(0, 0); + -webkit-transition: -webkit-transform 0.1s cubic-bezier(0.8, -0.33, 0.2, 1.33); + -moz-transition: -moz-transform 0.1s cubic-bezier(0.8, -0.33, 0.2, 1.33); + -o-transition: -o-transform 0.1s cubic-bezier(0.8, -0.33, 0.2, 1.33); + transition: transform 0.1s cubic-bezier(0.8, -0.33, 0.2, 1.33); +} +.radio input[type="radio"] { + opacity: 0; + z-index: 1; + cursor: pointer; +} +.radio input[type="radio"]:focus + label::before { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +.radio input[type="radio"]:checked + label::after { + -webkit-transform: scale(1, 1); + -ms-transform: scale(1, 1); + -o-transform: scale(1, 1); + transform: scale(1, 1); +} +.radio input[type="radio"]:disabled { + cursor: not-allowed; +} +.radio input[type="radio"]:disabled + label { + opacity: 0.65; +} +.radio input[type="radio"]:disabled + label::before { + cursor: not-allowed; +} +.radio.radio-inline { + margin-top: 0; +} + +.radio-primary input[type="radio"] + label::after { + background-color: #337ab7; +} +.radio-primary input[type="radio"]:checked + label::before { + border-color: #337ab7; +} +.radio-primary input[type="radio"]:checked + label::after { + background-color: #337ab7; +} + +.radio-danger input[type="radio"] + label::after { + background-color: #d9534f; +} +.radio-danger input[type="radio"]:checked + label::before { + border-color: #d9534f; +} +.radio-danger input[type="radio"]:checked + label::after { + background-color: #d9534f; +} + +.radio-info input[type="radio"] + label::after { + background-color: #5bc0de; +} +.radio-info input[type="radio"]:checked + label::before { + border-color: #5bc0de; +} +.radio-info input[type="radio"]:checked + label::after { + background-color: #5bc0de; +} + +.radio-warning input[type="radio"] + label::after { + background-color: #f0ad4e; +} +.radio-warning input[type="radio"]:checked + label::before { + border-color: #f0ad4e; +} +.radio-warning input[type="radio"]:checked + label::after { + background-color: #f0ad4e; +} + +.radio-success input[type="radio"] + label::after { + background-color: #5cb85c; +} +.radio-success input[type="radio"]:checked + label::before { + border-color: #5cb85c; +} +.radio-success input[type="radio"]:checked + label::after { + background-color: #5cb85c; +} + +/* input[type="checkbox"].styled:checked + label:after, */ +input[type="radio"].styled:checked + label:after { + font-family: 'FontAwesome'; + content: "\f00c"; +} +/* input[type="checkbox"] .styled:checked + label::before, */ +input[type="radio"] .styled:checked + label::before { + color: #fff; +} +/* input[type="checkbox"] .styled:checked + label::after, */ +input[type="radio"] .styled:checked + label::after { + color: #fff; +} diff --git a/share/public/css/bootstrap2-toggle.min.css b/share/public/css/bootstrap2-toggle.min.css new file mode 100644 index 00000000..411c2387 --- /dev/null +++ b/share/public/css/bootstrap2-toggle.min.css @@ -0,0 +1,32 @@ +/*! ======================================================================== + * Bootstrap Toggle: bootstrap2-toggle.css v2.2.0 + * http://www.bootstraptoggle.com + * ======================================================================== + * Copyright 2014 Min Hur, The New York Times Company + * Licensed under MIT + * ======================================================================== */ +label.checkbox .toggle,label.checkbox.inline .toggle{margin-left:-20px;margin-right:5px} +.toggle{min-width:40px;height:20px;position:relative;overflow:hidden} +.toggle input[type=checkbox]{display:none} +.toggle-group{position:absolute;width:200%;top:0;bottom:0;left:0;transition:left .35s;-webkit-transition:left .35s;-moz-user-select:none;-webkit-user-select:none} +.toggle.off .toggle-group{left:-100%} +.toggle-on{position:absolute;top:0;bottom:0;left:0;right:50%;margin:0;border:0;border-radius:0} +.toggle-off{position:absolute;top:0;bottom:0;left:50%;right:0;margin:0;border:0;border-radius:0} +.toggle-handle{position:relative;margin:0 auto;padding-top:0;padding-bottom:0;height:100%;width:0;border-width:0 1px} +.toggle-handle.btn-mini{top:-1px} +.toggle.btn{min-width:30px} +.toggle-on.btn{padding-right:24px} +.toggle-off.btn{padding-left:24px} +.toggle.btn-large{min-width:40px} +.toggle-on.btn-large{padding-right:35px} +.toggle-off.btn-large{padding-left:35px} +.toggle.btn-small{min-width:25px} +.toggle-on.btn-small{padding-right:20px} +.toggle-off.btn-small{padding-left:20px} +.toggle.btn-mini{min-width:20px} +.toggle-on.btn-mini{padding-right:12px} +.toggle-off.btn-mini{padding-left:12px} +/* added for netdisco */ +div.checkbox.pull-left {padding-left:5px} +label.btn.btn-success.btn-small.toggle-on {left:-4px} + diff --git a/share/public/css/d3-force-network-chart.css b/share/public/css/d3-force-network-chart.css new file mode 100644 index 00000000..ce01b203 --- /dev/null +++ b/share/public/css/d3-force-network-chart.css @@ -0,0 +1,197 @@ +.net_gobrechts_d3_force, +.net_gobrechts_d3_force_customize, +.net_gobrechts_d3_force_customize td, +.net_gobrechts_d3_force_tooltip { + box-sizing: content-box; + font-family: Arial, Helvetica, Sans Serif; + font-size: 10px; + background-color: #fff +} +.net_gobrechts_d3_force.border { + border: 1px solid silver; + border-radius: 5px; +} +.net_gobrechts_d3_force circle.highlighted { + stroke: #555; + stroke-width: 2px; + stroke-opacity: 1.0; +} +.net_gobrechts_d3_force circle.selected { + stroke: #555; + stroke-width: 4px; + stroke-dasharray: 4 2; + stroke-opacity: 1.0; +} +.net_gobrechts_d3_force text.label, +.net_gobrechts_d3_force text.labelCircular { + fill: black; + font-size: 10px; + letter-spacing: 0; + pointer-events: none; +} +.net_gobrechts_d3_force text.label{ + text-anchor: middle; +} +.net_gobrechts_d3_force text.highlighted { + font-size: 12px; + font-weight: bold; +} +.net_gobrechts_d3_force text.link { + font-size: 12px; + fill: #2a7ae2; + cursor: pointer; +} +.net_gobrechts_d3_force line.link, +.net_gobrechts_d3_force path.link { + fill: none; + stroke: #bbb; + stroke-width: 1.5px; + stroke-opacity: 0.8; +} +.net_gobrechts_d3_force line.dotted, +.net_gobrechts_d3_force path.dotted { + stroke-dasharray: .01 3; + stroke-linecap: round; +} +.net_gobrechts_d3_force line.dashed, +.net_gobrechts_d3_force path.dashed { + stroke-dasharray: 4 2; +} +.net_gobrechts_d3_force line.highlighted, +.net_gobrechts_d3_force path.highlighted { + stroke: #555 !important; + stroke-opacity: 1.0; +} +.net_gobrechts_d3_force marker.normal { + stroke: none; + fill: #bbb; +} +.net_gobrechts_d3_force marker.highlighted { + stroke: none; + fill: #555; +} +.net_gobrechts_d3_force .graphOverlay, +.net_gobrechts_d3_force .graphOverlaySizeHelper { + fill: none; + pointer-events: all; +} +.net_gobrechts_d3_force .lasso path { + stroke: #505050; + stroke-width: 2px; +} +.net_gobrechts_d3_force .lasso .drawn { + fill-opacity: 0.05 ; +} +.net_gobrechts_d3_force .lasso .loop_close { + fill: none; + stroke-dasharray: 4,4; +} +.net_gobrechts_d3_force .lasso .origin { + fill: #3399FF; + fill-opacity: 0.5; +} +.net_gobrechts_d3_force .loading rect { + fill: black; + fill-opacity: 0.2; +} +.net_gobrechts_d3_force .loading text { + fill: white; + font-size: 36px; + text-anchor: middle; +} +.net_gobrechts_d3_force_tooltip { + position: absolute; + border-radius: 5px; + padding: 5px; + background-color: silver; + opacity: 0.9; + width: 150px; + overflow: auto; + font-size: 12px; + z-index: 100000; + pointer-events: none; + display: none; +} +.net_gobrechts_d3_force_customize { + border: 1px solid silver; + border-radius: 5px; + font-size: 12px; + position: absolute; + padding: 5px; + background-color:white; + box-shadow: 1px 1px 6px #666; + z-index: 200000; +} +.net_gobrechts_d3_force_customize .drag { + border: 1px dashed silver; + border-radius: 3px; + display: block; + cursor: move; + font-weight: bold; + height: 24px; + margin-bottom: 5px; +} +.net_gobrechts_d3_force_customize .title { + position: absolute; + top: 10px; + left: 10px; +} +.net_gobrechts_d3_force_customize .close { + position: absolute; + top: 10px; + right: 10px; +} +.net_gobrechts_d3_force_customize table { + border-collapse: collapse; + border-spacing: 0; + border: none; + margin:0; + padding:0; +} +.net_gobrechts_d3_force_customize tr.hidden { + display: none; +} +.net_gobrechts_d3_force_customize td { + padding: 1px; + font-size: 12px; + vertical-align: middle; + border: none; +} +.net_gobrechts_d3_force_customize .label { + text-align: right; +} +.net_gobrechts_d3_force_customize .warning { + background-color: orange; +} +.net_gobrechts_d3_force_customize input, +.net_gobrechts_d3_force_customize select, +.net_gobrechts_d3_force_customize textarea, +.net_gobrechts_d3_force_customize a { + border: 1px solid silver; + margin: 0; + padding: 0; + height: auto; +} +.net_gobrechts_d3_force_customize a { + border: 1px solid transparent; + color: #2a7ae2; + text-decoration: none; + cursor: pointer; +} +.net_gobrechts_d3_force_customize input:focus, +.net_gobrechts_d3_force_customize select:focus, +.net_gobrechts_d3_force_customize textarea:focus, +.net_gobrechts_d3_force_customize a:focus { + outline: none !important; + border: 1px solid #2a7ae2 !important; + background-color: #ffff99 !important; + box-shadow: none !important; +} +.net_gobrechts_d3_force_customize textarea { + font-size: 10px !important; + padding: 2px; + width: 160px; + height: 85px; + background-color: white; + color: black; +} diff --git a/share/public/css/netdisco.css b/share/public/css/netdisco.css index 85c1befd..5cd4a325 100644 --- a/share/public/css/netdisco.css +++ b/share/public/css/netdisco.css @@ -425,6 +425,14 @@ td > form.nd_inline-form { /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ /* style customization for many items which appear in the sidebar */ +/* horizontal rule */ +.nd_sidebar-hr { + color: black; + background-color: black; + height: 2px; + margin: 12px 0px 12px 0px; +} + /* text in the sidebar */ .nd_sidebar-title { margin-left: 10px; @@ -432,11 +440,49 @@ td > form.nd_inline-form { margin-bottom: 12px; } -/* Flabels in netmap sidebar (not in a collapser) */ +/* labels in netmap sidebar (not in a collapser) */ .nd_sidebar-label { margin-left: 7px; } +/* to allow display of tooltip on a disabled control + http://jsfiddle.net/cSSUA/209/ */ +.tooltip-wrapper { + display: inline-block; +} +.tooltip-wrapper .input[disabled] { + pointer-events: none; +} + +/* vlan entry box for netmap */ +#nd_vlan-label { + margin-left: 5px; + margin-bottom: -7px; +} +#nd_vlan-label-text { + vertical-align: text-bottom; +} +#nd_vlan-entry { + width: 56px; +} + +/* netmap maximise icon */ +#nd2_fullscreen-netmap { + fill: black; + font-size: 15px; +} + +/* netmap link labels */ +.nd_netmap-linklabel { + pointer-events: none; + font-weight: bold; +} + +/* netmap tooltip box */ +#netmap_pane_tooltip { + width: unset; +} + /* fixup for prepended checkbox in sidebar */ .nd_searchcheckbox { width: 121px; @@ -614,8 +660,12 @@ form .clearfix.success input { } .nd_netmap-sidebar { - margin-top: 7px; - margin-left: -9px; + margin-top: 0px; + margin-left: -8px; +} + +.nd_netmap-sidebar > .input-prepend { + margin-left: 5px; } .nd_netmap-sidebar-help { @@ -629,6 +679,7 @@ form .clearfix.success input { /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ /* D3 SVG */ +/* .node circle { fill: #fff; stroke: steelblue; @@ -651,6 +702,7 @@ form .clearfix.success input { stroke-width: 2px; display: none; } +*/ /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ /* dataTables */ diff --git a/share/public/javascripts/bootstrap2-toggle.min.js b/share/public/javascripts/bootstrap2-toggle.min.js new file mode 100644 index 00000000..2aa60aab --- /dev/null +++ b/share/public/javascripts/bootstrap2-toggle.min.js @@ -0,0 +1,9 @@ +/*! ======================================================================== + * Bootstrap Toggle: bootstrap2-toggle.js v2.2.0 + * http://www.bootstraptoggle.com + * ======================================================================== + * Copyright 2014 Min Hur, The New York Times Company + * Licensed under MIT + * ======================================================================== */ ++function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.toggle"),f="object"==typeof b&&b;e||d.data("bs.toggle",e=new c(this,f)),"string"==typeof b&&e[b]&&e[b]()})}var c=function(b,c){this.$element=a(b),this.options=a.extend({},this.defaults(),c),this.render()};c.VERSION="2.2.0",c.DEFAULTS={on:"On",off:"Off",onstyle:"primary",offstyle:"default",size:"normal",style:"",width:null,height:null},c.prototype.defaults=function(){return{on:this.$element.attr("data-on")||c.DEFAULTS.on,off:this.$element.attr("data-off")||c.DEFAULTS.off,onstyle:this.$element.attr("data-onstyle")||c.DEFAULTS.onstyle,offstyle:this.$element.attr("data-offstyle")||c.DEFAULTS.offstyle,size:this.$element.attr("data-size")||c.DEFAULTS.size,style:this.$element.attr("data-style")||c.DEFAULTS.style,width:this.$element.attr("data-width")||c.DEFAULTS.width,height:this.$element.attr("data-height")||c.DEFAULTS.height}},c.prototype.render=function(){this._onstyle="btn-"+this.options.onstyle,this._offstyle="btn-"+this.options.offstyle;var b="large"===this.options.size?"btn-large":"small"===this.options.size?"btn-small":"mini"===this.options.size?"btn-mini":"",c=a('