Device groups rewrite (#10346)

* Device Groups rewrite
Updated web ui
Static or dynamic groups allowed
Alert rule query builder
Translation support
Permissions support

* cleanup, make relationship save, and validate it

* builder WIP

* rules builder and rules saving/loading

* Parse query builder to Laravel Fluent query

* Upgrade existing groups when editing.
Properly update only dynamic groups when polling.

* remove unused old code
Update API and other places to use Eloquent

* debug output in poller restored

* Fix up some things
creating static
improved validation
fix js error on creation
Fix static groups in polling

* hide pattern for static group

* Implement authorization
Use in the menu too

* update schema

* fix rollback

* Don't abort on invalid queries

* fixes to query builder

* add test data, looks like macros aren't handled (omitted them because groups don't use them generally)

* Add macro support for QueryBuilderFluentParser

* add test for macro that accepts value

* More space in forms
Retain rules when converted to static
no duplicate names allowed

* Better error feedback
Update related devices on save

* Add button icon

* format

* update docs

* fix tests

* Fix some QueryBuilderFluentParser issues with OR
updated/more test data

* Show device groups runtime
fix querybuilder.json format

* Store table joins in the rules to minimize polling time
Update group joins in daily.sh (and when they are saved)

* Update daily.php

* Add units to time
This commit is contained in:
Tony Murray
2019-06-19 16:01:53 -05:00
committed by GitHub
parent d8931e1946
commit 1a60c44eb0
38 changed files with 1065 additions and 1360 deletions

View File

@@ -0,0 +1,29 @@
@extends('layouts.librenmsv1')
@section('title', __('Create Device Group'))
@section('content')
<div class="container">
<div class="row">
<form action="{{ route('device-groups.store') }}" method="POST" role="form"
class="form-horizontal device-group-form col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2 col-sm-12">
<legend>@lang('Create Device Group')</legend>
@include('device-group.form')
<div class="form-group">
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-sm-offset-2">
<button type="submit" class="btn btn-primary">@lang('Save')</button>
<a type="button" class="btn btn-danger"
href="{{ route('device-groups.index') }}">@lang('Cancel')</a>
</div>
</div>
</form>
</div>
</div>
@endsection
@section('javascript')
<script src="{{ asset('js/sql-parser.min.js') }}"></script>
<script src="{{ asset('js/query-builder.standalone.min.js') }}"></script>
@endsection

View File

@@ -0,0 +1,30 @@
@extends('layouts.librenmsv1')
@section('title', __('Edit Device Group'))
@section('content')
<div class="container">
<div class="row">
<form action="{{ route('device-groups.update', $device_group->id) }}" method="POST" role="form"
class="form-horizontal device-group-form col-md-10 col-md-offset-1 col-sm-12">
<legend>@lang('Edit Device Group'): {{ $device_group->name }}</legend>
{{ method_field('PUT') }}
@include('device-group.form')
<div class="form-group">
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-sm-offset-2">
<button type="submit" class="btn btn-primary">@lang('Save')</button>
<a type="button" class="btn btn-danger"
href="{{ route('device-groups.index') }}">@lang('Cancel')</a>
</div>
</div>
</form>
</div>
</div>
@endsection
@section('javascript')
<script src="{{ asset('js/sql-parser.min.js') }}"></script>
<script src="{{ asset('js/query-builder.standalone.min.js') }}"></script>
@endsection

View File

@@ -0,0 +1,128 @@
<div class="form-group @if($errors->has('name')) has-error @endif">
<label for="name" class="control-label col-sm-3 col-md-2 text-nowrap">@lang('Name')</label>
<div class="col-sm-9 col-md-10">
<input type="text" class="form-control" id="name" name="name" value="{{ old('name', $device_group->name) }}">
<span class="help-block">{{ $errors->first('name') }}</span>
</div>
</div>
<div class="form-group @if($errors->has('desc')) has-error @endif">
<label for="desc" class="control-label col-sm-3 col-md-2 text-nowrap">@lang('Description')</label>
<div class="col-sm-9 col-md-10">
<input type="text" class="form-control" id="desc" name="desc" value="{{ old('desc', $device_group->desc) }}">
<span class="help-block">{{ $errors->first('desc') }}</span>
</div>
</div>
<div class="form-group @if($errors->has('type')) has-error @endif">
<label for="type" class="control-label col-sm-3 col-md-2">@lang('Type')</label>
<div class="col-sm-9 col-md-10">
<select class="form-control" id="type" name="type" onchange="change_dg_type(this)">
<option value="dynamic"
@if(old('type', $device_group->type) == 'dynamic') selected @endif>@lang('Dynamic')</option>
<option value="static"
@if(old('type', $device_group->type) == 'static') selected @endif>@lang('Static')</option>
</select>
<span class="help-block">{{ $errors->first('type') }}</span>
</div>
</div>
<div id="dynamic-dg-form" class="form-group @if($errors->has('rules')) has-error @endif">
<label for="pattern" class="control-label col-sm-3 col-md-2 text-nowrap">@lang('Define Rules')</label>
<div class="col-sm-9 col-md-10">
<div id="builder"></div>
<span class="help-block">{{ $errors->first('rules') }}</span>
</div>
</div>
<div id="static-dg-form" class="form-group @if($errors->has('devices')) has-error @endif" style="display: none">
<label for="devices" class="control-label col-sm-3 col-md-2 text-nowrap">@lang('Select Devices')</label>
<div class="col-sm-9 col-md-10">
<select class="form-control" id="devices" name="devices[]" multiple>
@foreach($device_group->devices as $device)
<option value="{{ $device->device_id }}" selected>{{ $device->displayName() }}</option>
@endforeach
</select>
<span class="help-block">{{ $errors->first('devices') }}</span>
</div>
</div>
<script>
function change_dg_type(select) {
var type = select.options[select.selectedIndex].value;
document.getElementById("dynamic-dg-form").style.display = (type === 'dynamic' ? 'block' : 'none');
document.getElementById("static-dg-form").style.display = (type === 'dynamic' ? 'none' : 'block');
}
change_dg_type(document.getElementById('type'));
init_select2('#devices', 'device', {multiple: true});
var builder = $('#builder').on('afterApplyRuleFlags.queryBuilder afterCreateRuleFilters.queryBuilder', function () {
$("[name$='_filter']").each(function () {
$(this).select2({
dropdownAutoWidth: true,
width: 'auto'
});
});
}).on('ruleToSQL.queryBuilder.filter', function (e, rule) {
if (rule.operator === 'regexp') {
e.value += ' \'' + rule.value + '\'';
}
}).queryBuilder({
plugins: [
'bt-tooltip-errors'
// 'not-group'
],
filters: {!! $filters !!},
operators: [
'equal', 'not_equal', 'between', 'not_between', 'begins_with', 'not_begins_with', 'contains', 'not_contains', 'ends_with', 'not_ends_with', 'is_empty', 'is_not_empty', 'is_null', 'is_not_null', 'in', 'not_in',
{type: 'less', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime']},
{type: 'less_or_equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime']},
{type: 'greater', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime']},
{type: 'greater_or_equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime']},
{type: 'regex', nb_inputs: 1, multiple: false, apply_to: ['string', 'number']},
{type: 'not_regex', nb_inputs: 1, multiple: false, apply_to: ['string', 'number']}
],
lang: {
operators: {
regexp: 'regex',
not_regex: 'not regex'
}
},
sqlOperators: {
regexp: {op: 'REGEXP'},
not_regexp: {op: 'NOT REGEXP'}
},
sqlRuleOperator: {
'REGEXP': function (v) {
return {val: v, op: 'regexp'};
},
'NOT REGEXP': function (v) {
return {val: v, op: 'not_regexp'};
}
}
});
$('.device-group-form').submit(function (eventObj) {
if ($('#type').val() === 'static') {
return true;
}
if (!builder.queryBuilder('validate')) {
return false;
}
$('<input type="hidden" name="rules" />')
.attr('value', JSON.stringify(builder.queryBuilder('getRules')))
.appendTo(this);
return true;
});
</script>
<script>
var rules = {!! json_encode(old('rules') ? json_decode(old('rules')) : $device_group->rules) !!};
if (rules) {
builder.queryBuilder('setRules', rules);
}
</script>

View File

@@ -0,0 +1,92 @@
@extends('layouts.librenmsv1')
@section('title', __('Device Groups'))
@section('content')
<div class="container-fluid">
<div id="manage-device-groups-panel" class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fa fa-th fa-fw fa-lg" aria-hidden="true"></i> @lang('Device Groups')
</h4>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-12">
<a type="button" class="btn btn-primary" href="{{ route('device-groups.create') }}">
<i class="fa fa-plus"></i> @lang('New Device Group')
</a>
</div>
</div>
<div class="table-responsive">
<table id="manage-device-groups-table" class="table table-condensed table-hover">
<thead>
<tr>
<th>@lang('Name')</th>
<th>@lang('Description')</th>
<th>@lang('Type')</th>
<th>@lang('Devices')</th>
<th>@lang('Pattern')</th>
<th>@lang('Actions')</th>
</tr>
</thead>
<tbody>
@foreach($device_groups as $device_group)
<tr id="row_{{ $device_group->id }}">
<td>{{ $device_group->name }}</td>
<td>{{ $device_group->desc }}</td>
<td>{{ __(ucfirst($device_group->type)) }}</td>
<td>
<a href="{{ url("/devices/group=$device_group->id") }}">{{ $device_group->devices_count }}</a>
</td>
<td>{{ $device_group->type == 'dynamic' ? $device_group->getParser()->toSql(false) : '' }}</td>
<td>
<a type="button" class="btn btn-primary btn-sm" aria-label="@lang('Edit')"
href="{{ route('device-groups.edit', $device_group->id) }}">
<i class="fa fa-pencil" aria-hidden="true"></i></a>
<button type="button" class="btn btn-danger btn-sm" aria-label="@lang('Delete')"
onclick="delete_dg(this, '{{ $device_group->name }}', '{{ route('device-groups.destroy', $device_group->id) }}')">
<i
class="fa fa-trash" aria-hidden="true"></i></button>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
@endsection
@section('scripts')
<script>
function delete_dg(button, name, url) {
var index = button.parentNode.parentNode.rowIndex;
if (confirm('@lang('Are you sure you want to delete ')' + name + '?')) {
$.ajax({
url: url,
type: 'DELETE',
success: function (msg) {
document.getElementById("manage-device-groups-table").deleteRow(index);
toastr.success(msg);
},
error: function () {
toastr.error('@lang('The device group could not be deleted')');
}
});
}
return false;
}
</script>
@endsection
@section('css')
<style>
.table-responsive {
padding-top: 16px
}
</style>
@endsection

View File

@@ -178,12 +178,11 @@
</li>
<li role="presentation" class="divider"></li>
@endconfig
@notconfig('navbar.manage_groups.hide')
<li><a href="{{ url('device-groups') }}"><i class="fa fa-th fa-fw fa-lg"
aria-hidden="true"></i> @lang('Manage Groups')</a>
</li>
@endconfig
@can('manage', \App\Models\DeviceGroup::class)
<li><a href="{{ url('device-groups') }}"><i class="fa fa-th fa-fw fa-lg"
aria-hidden="true"></i> @lang('Manage Groups')
</a></li>
@endcan
<li><a href="{{ url('device-dependencies') }}"><i class="fa fa-group fa-fw fa-lg"></i> @lang('Device Dependencies')</a></li>
@if($show_vmwinfo)
<li><a href="{{ url('vminfo') }}"><i