custom fields on devices and ports in the web from config (#945)

* custom device field web display and edit

* make display work; relies on T::T calling dict slot or method with same syntax

* add storing port custom fields

* use resultset method instead, use cf_ prefix

* update Pg min ver for jsonb

* allow override of position and default for port custom fields

* support hidden for custom fields

* update description of Objects API class

* allow left and mid position for custom fields

* add custom fields in csv

* change port control sidebar label

* fix default missing bug on backend jobs
This commit is contained in:
Oliver Gorwits
2022-12-09 10:20:26 +00:00
committed by GitHub
parent d03eab02db
commit 1c7c749f0e
15 changed files with 225 additions and 44 deletions

View File

@@ -11,7 +11,7 @@ __PACKAGE__->load_namespaces(
);
our # try to hide from kwalitee
$VERSION = 76; # schema version used for upgrades, keep as integer
$VERSION = 77; # schema version used for upgrades, keep as integer
use Path::Class;
use File::ShareDir 'dist_dir';

View File

@@ -85,6 +85,8 @@ __PACKAGE__->add_columns(
{ data_type => "boolean", is_nullable => 0, default_value => \"false" },
"pae_is_enabled",
{ data_type => "boolean", is_nullable => 1 },
"custom_fields",
{ data_type => "jsonb", is_nullable => 0, default_value => \"{}" },
);
__PACKAGE__->set_primary_key("ip");

View File

@@ -71,6 +71,8 @@ __PACKAGE__->add_columns(
{ data_type => "integer", is_nullable => 1 },
"lastchange",
{ data_type => "bigint", is_nullable => 1 },
"custom_fields",
{ data_type => "jsonb", is_nullable => 0, default_value => \"{}" },
);
__PACKAGE__->set_primary_key("port", "ip");

View File

@@ -332,18 +332,44 @@ sub jq_insert {
my $happy = false;
try {
schema(vars->{'tenant'})->txn_do(sub {
schema(vars->{'tenant'})->resultset('Admin')->populate([
map {{
device => $_->{device},
device_key => $_->{device_key},
port => $_->{port},
action => $_->{action},
subaction => ($_->{extra} || $_->{subaction}),
username => $_->{username},
userip => $_->{userip},
status => 'queued',
}} @$jobs
]);
if (scalar @$jobs == 1 and defined $jobs->[0]->{device} and
scalar grep {$_ eq $jobs->[0]->{action}} @{ setting('_inline_actions') || [] }) {
my $spec = $jobs->[0];
my $row = undef;
if ($spec->{port}) {
$row = schema(vars->{'tenant'})->resultset('DevicePort')
->find($spec->{port}, $spec->{device});
}
else {
$row = schema(vars->{'tenant'})->resultset('Device')
->find($spec->{device});
}
die 'failed to find row for custom field update' unless $row;
$spec->{action} =~ s/^cf_//;
$spec->{subaction} = to_json( $spec->{subaction} );
$row->make_column_dirty('custom_fields');
$row->update({
custom_fields => \['jsonb_set(custom_fields, ?, ?)'
=> (qq{{$spec->{action}}}, $spec->{subaction}) ]
})->discard_changes();
}
else {
schema(vars->{'tenant'})->resultset('Admin')->populate([
map {{
device => $_->{device},
device_key => $_->{device_key},
port => $_->{port},
action => $_->{action},
subaction => ($_->{extra} || $_->{subaction}),
username => $_->{username},
userip => $_->{userip},
status => 'queued',
}} @$jobs
]);
}
});
$happy = true;
}

View File

@@ -111,6 +111,7 @@ use App::Netdisco::Web::TypeAhead;
use App::Netdisco::Web::PortControl;
use App::Netdisco::Web::Statistics;
use App::Netdisco::Web::Password;
use App::Netdisco::Web::CustomFields;
use App::Netdisco::Web::GenericReport;
sub _load_web_plugins {
@@ -391,7 +392,7 @@ $swagger_doc->{tags} = [
{name => 'Search',
description => 'Search Operations'},
{name => 'Objects',
description => 'Retrieve Device, Port, and associated Node Data'},
description => 'Device, Port, and associated Node Data'},
{name => 'Reports',
description => 'Canned and Custom Reports'},
];

View File

@@ -0,0 +1,86 @@
package App::Netdisco::Web::CustomFields;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use App::Netdisco::DB::ResultSet::Device;
use App::Netdisco::DB::ResultSet::DevicePort;
use App::Netdisco::Web::Plugin;
my @inline_device_actions = ();
my @inline_device_port_actions = ();
foreach my $config (@{ setting('custom_fields')->{'device'} || [] }) {
if (! $config->{'name'}) {
error 'custom_field missing name';
next;
}
register_device_details({
%{ $config },
field => ('cf_' . $config->{'name'}),
label => ($config->{'label'} || ucfirst $config->{'name'}),
}) unless $config->{'hidden'};
push @inline_device_actions, $config->{'name'};
}
foreach my $config (@{ setting('custom_fields')->{'device_port'} || [] }) {
if (! $config->{'name'}) {
error 'custom_field missing name';
next;
}
register_device_port_column({
position => 'right', # or "mid" or "right"
default => undef, # or undef
%{ $config },
field => ('cf_' . $config->{'name'}),
label => ($config->{'label'} || ucfirst $config->{'name'}),
}) unless $config->{'hidden'};
push @inline_device_port_actions, $config->{'name'};
}
{
package App::Netdisco::DB::ResultSet::Device;
sub with_custom_fields {
my ($rs, $cond, $attrs) = @_;
return $rs
->search_rs($cond, $attrs)
->search({},
{ '+columns' => {
map {( ('cf_'. $_) => \[ 'me.custom_fields ->> ?' => $_ ] )}
@inline_device_actions
}});
}
}
{
package App::Netdisco::DB::ResultSet::DevicePort;
sub with_custom_fields {
my ($rs, $cond, $attrs) = @_;
return $rs
->search_rs($cond, $attrs)
->search({},
{ '+columns' => {
map {( ('cf_'. $_) => \[ 'me.custom_fields ->> ?' => $_ ] )}
@inline_device_port_actions
}});
}
}
set('_inline_actions' => [
map {'cf_' . $_} (@inline_device_actions, @inline_device_port_actions)
]);
true;

View File

@@ -23,8 +23,7 @@ ajax '/ajax/content/device/details' => require_login sub {
'+as' => ['has_snapshot'],
join => 'snapshot',
},
)->with_times()
->hri->all;
)->with_times->with_custom_fields->hri->all;
my @power
= schema(vars->{'tenant'})->resultset('DevicePower')

View File

@@ -20,7 +20,7 @@ get '/ajax/content/device/ports' => require_login sub {
my $device = schema(vars->{'tenant'})->resultset('Device')
->search_for_device($q) or send_error('Bad device', 400);
my $set = $device->ports->with_properties;
my $set = $device->ports->with_properties->with_custom_fields;
# refine by ports if requested
my $f = param('f');

View File

@@ -24,7 +24,7 @@ ajax '/ajax/portcontrol' => require_any_role [qw(admin port_control)] => sub {
'c_power' => 'power',
);
my $action = $action_map{ param('field') };
my $action = ($action_map{ param('field') } || param('field') || '');
my $subaction = ($action =~ m/^(?:power|portcontrol)/
? (param('action') ."-other")
: param('value'));
@@ -34,6 +34,7 @@ ajax '/ajax/portcontrol' => require_any_role [qw(admin port_control)] => sub {
my $act = "$action $subaction";
$act =~ s/-other$//;
$act =~ s/^portcontrol/port/;
$act =~ s/^device_port_custom_field_/custom_field: /;
schema(vars->{'tenant'})->resultset('DevicePortLog')->create({
ip => param('device'),