summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimon Rettberg2020-04-21 18:16:36 +0200
committerSimon Rettberg2020-04-21 18:16:36 +0200
commitd6bbb4d57a086dfaf5f0a1ebf8913577568ae887 (patch)
tree95f7c80a38bfb02b1dd5c1c796e9d333d26d0ff5
parent[statistics] New filter UI (diff)
downloadslx-admin-d6bbb4d57a086dfaf5f0a1ebf8913577568ae887.tar.gz
slx-admin-d6bbb4d57a086dfaf5f0a1ebf8913577568ae887.tar.xz
slx-admin-d6bbb4d57a086dfaf5f0a1ebf8913577568ae887.zip
[statistics] Refactor filter creation (Part 1)
Filter classes are now instances of their respective classes, to move more logic into those classes. A bind method is used for assigning actual operator and argument values. renderFilterBox() is still a little too messy, maybe a clever class for mapping a (bound) filter to data for mustache will come in handy here.
-rw-r--r--modules-available/statistics/inc/statisticsfilter.inc.php729
-rw-r--r--modules-available/statistics/inc/statisticsfilterset.inc.php67
-rw-r--r--modules-available/statistics/pages/list.inc.php5
-rw-r--r--modules-available/statistics/pages/summary.inc.php19
-rw-r--r--modules-available/statistics/templates/filterbox.html2
5 files changed, 401 insertions, 421 deletions
diff --git a/modules-available/statistics/inc/statisticsfilter.inc.php b/modules-available/statistics/inc/statisticsfilter.inc.php
index 6999654d..e554a61c 100644
--- a/modules-available/statistics/inc/statisticsfilter.inc.php
+++ b/modules-available/statistics/inc/statisticsfilter.inc.php
@@ -3,7 +3,7 @@
/* base class with rudimentary SQL generation abilities.
* WARNING: argument is escaped, but $column and $operator are passed unfiltered into SQL */
-class StatisticsFilter
+abstract class StatisticsFilter
{
/**
* Legacy delimiter for js_selectize filters - used to redirect old URLs
@@ -13,11 +13,49 @@ class StatisticsFilter
const SIZE_ID44 = array(0, 8, 16, 24, 30, 40, 50, 60, 80, 100, 120, 150, 180, 250, 300, 400, 500, 1000, 2000, 4000);
const SIZE_RAM = array(1, 2, 3, 4, 6, 8, 10, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, 320, 480, 512, 768, 1024);
+ private static $keyCounter = 0;
+
+
+ /*
+ * Simple filters that map directly to DB columns
+ */
+
+ const OP_ORDINAL = ['=', '!=', '<', '>', '<=', '>='];
+ const OP_STRCMP = ['~', '!~', '=', '!='];
+ const OP_NOMINAL = ['=', '!='];
+
+ /**
+ * @var StatisticsFilter[]
+ */
+ public static $columns;
+
+ /**
+ * @var string|null db-based sort column for this field, null if not sortable
+ */
public $column;
- public $operator;
- public $argument;
- private static $keyCounter = 0;
+ /**
+ * @var string[] valid operators for this filter
+ */
+ public $ops;
+ /**
+ * @var string placeholder for input field
+ */
+ public $placeholder;
+
+ public function __construct($column, array $ops, string $placeholder = '')
+ {
+ $this->column = $column;
+ $this->ops = $ops;
+ $this->placeholder = $placeholder;
+ }
+
+ public function type() { return $this->ops === self::OP_ORDINAL ? 'int' : 'string'; }
+
+ /* returns a where clause and adds needed operators to the passed arrays */
+ public abstract function whereClause(string $operator, $argument, array &$args, array &$joins);
+
+ public function bind(string $op, $argument) { return new DatabaseFilter($this, $op, $argument); }
public static function findBestValue($array, $value, $up)
{
@@ -45,88 +83,12 @@ class StatisticsFilter
return $colname . '_' . (self::$keyCounter++);
}
- public function __construct($column, $operator, $argument = null)
- {
- $this->column = trim($column);
- $this->operator = trim($operator);
- $this->argument = is_array($argument) ? $argument : trim($argument);
- }
-
- /* returns a where clause and adds needed operators to the passed array */
- public function whereClause(&$args, &$joins)
- {
- $key = self::getNewKey($this->column);
- $addendum = '';
-
- /* check if we have to do some parsing*/
- if (self::$columns[$this->column]['type'] === 'date') {
- $args[$key] = strtotime($this->argument);
- if ($this->operator === '=' || $this->operator === '!=') {
- $key2 = self::getNewKey($this->column);
- $args[$key2] = strtotime(' +1 day', $args[$key]);
- return ($this->operator === '=' ? '' : 'NOT ') . 'm.' . $this->column . " BETWEEN :$key AND :$key2";
- }
- if ($this->operator === '>') {
- $args[$key] = strtotime('+1 day', $args[$key]);
- } elseif ($this->operator === '<') {
- $args[$key] = strtotime('-1 day', $args[$key]);
- }
- } else {
- $args[$key] = $this->argument;
- if ($this->operator === '~' || $this->operator === '!~') {
- $args[$key] = str_replace(array('=', '_', '%', '*', '?'), array('==', '=_', '=%', '%', '_'), $args[$key]);
- $addendum = " ESCAPE '='";
- }
- }
-
- $op = $this->operator;
- if ($this->operator == '~') {
- $op = 'LIKE';
- } elseif ($this->operator == '!~') {
- $op = 'NOT LIKE';
- }
-
- return 'm.' . $this->column . ' ' . $op . ' :' . $key . $addendum;
- }
-
- /* parse a query into an array of filters */
+ /**
+ * @return DatabaseFilter[]
+ */
public static function parseQuery()
{
// Get current settings from GET
- $currentValues = self::loadFilterFromGet();
- $filters = [];
- foreach ($currentValues as $filterType => $data) {
- if (!$data['filter'])
- continue;
- $operator = $data['op'];
- $argument = $data['argument'];
-
- if ($filterType === 'gbram') {
- $filters[] = new RamGbStatisticsFilter($operator, $argument);
- } elseif ($filterType === 'runtime') {
- $filters[] = new RuntimeStatisticsFilter($operator, $argument);
- } elseif ($filterType === 'state') {
- $filters[] = new StateStatisticsFilter($operator, $argument);
- } elseif ($filterType === 'hddgb') {
- $filters[] = new Id44StatisticsFilter($operator, $argument);
- } elseif ($filterType === 'location') {
- $filters[] = new LocationStatisticsFilter($operator, $argument);
- } elseif ($filterType === 'subnet') {
- $filters[] = new SubnetStatisticsFilter($operator, $argument);
- } else {
- if (array_key_exists($filterType, self::$columns)) {
- $filters[] = new StatisticsFilter($filterType, $operator, $argument);
- } else {
- Message::addError('invalid-filter-key', $filterType);
- }
- }
- }
-
- return $filters;
- }
-
- private static function loadFilterFromGet()
- {
$ops = Request::get('op', [], 'array');
$currentValues = ArrayUtil::mergeByKey([
'filter' => Request::get('filter', [], 'array'),
@@ -140,7 +102,21 @@ class StatisticsFilter
'argument' => gmdate('Y-m-d', strtotime('-30 day')),
];
}
- return $currentValues;
+ $filters = [];
+ foreach ($currentValues as $filterType => $data) {
+ if (!$data['filter'])
+ continue;
+ $operator = $data['op'];
+ $argument = $data['argument'];
+
+ if (array_key_exists($filterType, self::$columns)) {
+ $filters[$filterType] = self::$columns[$filterType]->bind($operator, $argument);
+ } else {
+ Message::addError('invalid-filter-key', $filterType);
+ }
+ }
+
+ return $filters;
}
/**
@@ -149,414 +125,420 @@ class StatisticsFilter
public static function renderFilterBox($show, $filterSet)
{
// Build location list, with permissions
- $locs = [];
if (Module::isAvailable('locations')) {
- $allowed = $filterSet->getAllowedLocations();
- foreach (Location::getLocations(-1, 0, true) as $loc) {
- $locs[] = [
- 'key' => $loc['locationid'],
- 'value' => $loc['locationpad'] . ' ' . $loc['locationname'],
- 'disabled' => $allowed !== false && !in_array($loc['locationid'], $allowed)
- ? 'disabled' : '',
- ];
- }
+ self::$columns['location']->filterLocations($filterSet->getAllowedLocations());
}
- // Get current settings from GET
- $currentValues = self::loadFilterFromGet();
// Build column array for rendering
$columns = [];
- foreach (self::$columns as $key => $col) {
- if ($key === 'location') {
- $col['values'] = $locs;
- }
- $col['key'] = $key;
- $col['name'] = Dictionary::translateFile('filters', $key, true);
- if ($col['type'] === 'int') {
+ $showCount = 0;
+ foreach (self::$columns as $key => $filter) {
+ $col = [
+ 'key' => $key,
+ 'name' => Dictionary::translateFile('filters', $key, true),
+ 'placeholder' => $filter->placeholder,
+ ];
+ $bind = $filterSet->hasFilterKey($key);
+ if ($filter->type() === 'int') {
$col['input'] = 'number';
- } elseif ($col['type'] === 'string') {
+ } elseif ($filter->type() === 'string') {
$col['input'] = 'text';
- } elseif ($col['type'] === 'date') {
+ } elseif ($filter->type() === 'date') {
$col['input'] = 'text';
$col['inputclass'] = 'is-date';
- } elseif ($col['type'] === 'enum') {
+ } elseif ($filter->type() === 'enum') {
$col['enum'] = true;
- if (isset($col['values'][0])) {
- if (!is_array($col['values'][0])) {
- // Arrayize
- $col['values'] = array_map(function($e) { return [
- 'key' => $e,
- 'value' => $e,
- ]; }, $col['values']);
- }
- } else {
- $col['values'] = array_map(function($v, $k) { return [
- 'key' => $k,
- 'value' => $v,
- ]; }, $col['values'], array_keys($col['values']));
- }
- if (isset($currentValues[$key]['argument'])) {
+ $col['values'] = $filter->values;
+ if ($bind !== false) {
// Current value from GET
foreach ($col['values'] as &$value) {
- if ($value['key'] == $currentValues[$key]['argument']) {
+ if ($value['key'] == $bind->argument) {
$value['selected'] = 'selected';
}
}
}
}
// current value from GET
- if (isset($currentValues[$key])) {
- $col['currentvalue'] = $currentValues[$key]['argument'] ?? '';
- if ($currentValues[$key]['filter']) {
- $col['checked'] = 'checked';
- } elseif (!isset($col['show']) || !$col['show']) {
- $col['collapse'] = 'collapse';
- }
+ if ($bind !== false) {
+ $col['currentvalue'] = $bind->argument;
+ $col['checked'] = 'checked';
+ $showCount++;
} elseif (!isset($col['show']) || !$col['show']) {
$col['collapse'] = 'collapse';
}
- // Current value, arrayize
+ $col['op'] = $filter->ops;
foreach ($col['op'] as &$value) {
$value = ['op' => $value];
- if (($currentValues[$key]['op'] ?? '=') === $value['op']) {
+ if ($bind !== false && $bind->op === $value['op']) {
$value['selected'] = 'selected';
}
}
- $columns[] = $col;
+ $columns[$key] = $col;
+ }
+ if ($showCount < 2) {
+ unset($columns['clientip']['collapse']);
+ }
+ if ($showCount < 1) {
+ unset($columns['machineuuid']['collapse']);
}
$data = array(
'show' => $show,
- 'columns' => $columns,
+ 'columns' => array_values($columns),
+ $show . 'ButtonClass' => 'active',
);
- if ($show === 'list') {
- $data['listButtonClass'] = 'active';
- $data['statButtonClass'] = '';
+ Permission::addGlobalTags($data['perms'], null, ['view.summary', 'view.list']);
+ Render::addTemplate('filterbox', $data);
+ }
+
+ public static function initConstants()
+ {
+ self::$columns = [
+ 'clientip' => new IpStatisticsFilter(),
+ 'hostname' => new SimpleStatisticsFilter('hostname', self::OP_STRCMP, 'pc.fqdn.example.com'),
+ 'machineuuid' => new SimpleStatisticsFilter('machineuuid', self::OP_STRCMP, '88888888-4444-4444-121212121212'),
+ 'macaddr' => new SimpleStatisticsFilter('macaddr', self::OP_STRCMP, '11-22-33-44-55-66'),
+ 'firstseen' => new DateStatisticsFilter('firstseen', '2020-10-15 14:00'),
+ 'lastseen' => new DateStatisticsFilter('lastseen', '2020-10-15 14:00'),
+ 'logintime' => new DateStatisticsFilter('logintime', '2020-10-15 14:00'),
+ 'lastboot' => new RuntimeStatisticsFilter(),
+ 'runtime' => new SimpleStatisticsFilter('runtime', self::OP_ORDINAL, ''),
+ 'realcores' => new SimpleStatisticsFilter('realcores', self::OP_ORDINAL, ''),
+ 'systemmodel' => new SimpleStatisticsFilter('systemmodel', self::OP_STRCMP, 'PC-365 (IBM)'),
+ 'cpumodel' => new SimpleStatisticsFilter('cpumodel', self::OP_STRCMP, 'Pentium Pro 200 MHz'),
+ 'hddgb' => new Id44StatisticsFilter(),
+ 'gbram' => new RamGbStatisticsFilter(),
+ 'kvmstate' => new EnumStatisticsFilter('kvmstate', ['ENABLED', 'DISABLED', 'UNSUPPORTED']),
+ 'badsectors' => new SimpleStatisticsFilter('badsectors', self::OP_ORDINAL, ''),
+ 'currentuser' => new SimpleStatisticsFilter('currentuser', self::OP_STRCMP, 'login'),
+ 'state' => new StateStatisticsFilter(),
+ 'live_swapfree' => new SimpleStatisticsFilter('live_swapfree', self::OP_ORDINAL, 'MiB'),
+ 'live_memfree' => new SimpleStatisticsFilter('live_memfree', self::OP_ORDINAL, 'MiB'),
+ 'live_tmpfree' => new SimpleStatisticsFilter('live_tmpfree', self::OP_ORDINAL, 'MiB'),
+ ];
+ if (Module::isAvailable('locations')) {
+ self::$columns['location'] = new LocationStatisticsFilter();
+ }
+ }
+
+}
+
+class SimpleStatisticsFilter extends StatisticsFilter
+{
+
+ public function whereClause(string $operator, $argument, array &$args, array &$joins)
+ {
+ $addendum = '';
+ $key = self::getNewKey($this->column);
+ $args[$key] = $argument;
+
+ if (is_array($argument)) {
+ if ($operator{0} === '!') {
+ $op = 'NOT IN';
+ } else {
+ $op = 'IN';
+ }
} else {
- $data['listButtonClass'] = '';
- $data['statButtonClass'] = 'active';
+ if ($operator === '~' || $operator === '!~') {
+ $args[$key] = str_replace(array('=', '_', '%', '*', '?'), array('==', '=_', '=%', '%', '_'), $args[$key]);
+ $addendum = " ESCAPE '='";
+ }
+ $op = $operator;
+ if ($operator === '~') {
+ $op = 'LIKE';
+ } elseif ($operator === '!~') {
+ $op = 'NOT LIKE';
+ }
}
+ return 'm.' . $this->column . ' ' . $op . ' (:' . $key . ') ' . $addendum;
+ }
- Permission::addGlobalTags($data['perms'], null, ['view.summary', 'view.list']);
- Render::addTemplate('filterbox', $data);
+}
+
+class EnumStatisticsFilter extends SimpleStatisticsFilter
+{
+
+ public $values;
+
+ public function __construct(string $column, array $values)
+ {
+ parent::__construct($column, self::OP_NOMINAL, '');
+ if (isset($values[0])) {
+ if (!is_array($values[0])) {
+ $values = array_map(function($e) { return [
+ 'key' => $e,
+ 'value' => $e,
+ ]; }, $values);
+ }
+ } else {
+ $values = array_map(function($v, $k) { return [
+ 'key' => $k,
+ 'value' => $v,
+ ]; }, $values, array_keys($values));
+ }
+ $this->values = $values;
}
- /*
- * Simple filters that map directly to DB columns
- */
+ public function type() { return 'enum'; }
- const OP_ORDINAL = ['=', '!=', '<', '>', '<=', '>='];
- const OP_STRCMP = [ '=', '!=', '!~', '~'];
- const OP_NOMINAL = ['!=', '='];
- public static $columns;
+ public function whereClause(string $operator, $argument, array &$args, array &$joins)
+ {
+ $keys = ArrayUtil::flattenByKey($this->values, 'key');
+ if (is_array($argument)) {
+ $ok = true;
+ foreach ($argument as $e) {
+ if (!in_array($e, $keys)) {
+ $ok = false;
+ }
+ }
+ } else {
+ $ok = in_array($argument, $keys);
+ }
+ if (!$ok) {
+ Message::addError('invalid-enum-item', $this->column, $argument);
+ return '0';
+ }
+ return parent::whereClause($operator, $argument, $args, $joins);
+ }
- /**
- * Do this here instead of const since we need to check for available modules while building array.
- */
- public static function initConstants()
+}
+
+class DateStatisticsFilter extends StatisticsFilter
+{
+
+ public function __construct(string $column, string $placeholder)
{
+ parent::__construct($column, self::OP_ORDINAL, $placeholder);
+ }
- self::$columns = [
- 'clientip' => [
- 'op' => self::OP_NOMINAL,
- 'type' => 'string',
- 'placeholder' => '1.2.3.4',
- 'show' => true,
- ],
- 'hostname' => [
- 'op' => self::OP_STRCMP,
- 'type' => 'string',
- 'placeholder' => 'pc.fqdn.example.com',
- ],
- 'machineuuid' => [
- 'op' => self::OP_NOMINAL,
- 'type' => 'string',
- 'placeholder' => '88888888-4444-4444-121212121212',
- 'show' => true,
- ],
- 'macaddr' => [
- 'op' => self::OP_NOMINAL,
- 'type' => 'string',
- 'placeholder' => '11-22-33-44-55-66',
- ],
- 'firstseen' => [
- 'op' => self::OP_ORDINAL,
- 'type' => 'date',
- 'placeholder' => '2020-10-15',
- ],
- 'lastseen' => [
- 'op' => self::OP_ORDINAL,
- 'type' => 'date',
- 'placeholder' => '2020-10-15',
- ],
- 'logintime' => [
- 'op' => self::OP_ORDINAL,
- 'type' => 'date',
- 'placeholder' => '2020-10-15',
- ],
- 'lastboot' => [
- 'op' => self::OP_ORDINAL,
- 'type' => 'date',
- 'placeholder' => '2020-10-15',
- ],
- 'runtime' => [
- 'op' => self::OP_ORDINAL,
- 'type' => 'int',
- ],
- 'realcores' => [
- 'op' => self::OP_ORDINAL,
- 'type' => 'int',
- ],
- 'systemmodel' => [
- 'op' => self::OP_STRCMP,
- 'type' => 'string',
- 'placeholder' => 'PC-365 (IBM)',
- ],
- 'cpumodel' => [
- 'op' => self::OP_STRCMP,
- 'type' => 'string',
- 'placeholder' => 'Pentium Pro 200',
- ],
- 'hddgb' => [
- 'op' => self::OP_ORDINAL,
- 'type' => 'int',
- 'placeholder' => 'GiB',
- 'map_sort' => 'id44mb'
- ],
- 'gbram' => [
- 'op' => self::OP_ORDINAL,
- 'type' => 'int',
- 'map_sort' => 'mbram',
- 'placeholder' => 'GiB',
- ],
- 'kvmstate' => [
- 'op' => self::OP_NOMINAL,
- 'type' => 'enum',
- 'values' => ['ENABLED', 'DISABLED', 'UNSUPPORTED']
- ],
- 'badsectors' => [
- 'op' => self::OP_ORDINAL,
- 'type' => 'int',
- ],
- 'subnet' => [
- 'op' => self::OP_NOMINAL,
- 'type' => 'string',
- ],
- 'currentuser' => [
- 'op' => self::OP_NOMINAL,
- 'type' => 'string',
- 'placeholder' => 'login',
- ],
- 'state' => [
- 'op' => self::OP_NOMINAL,
- 'type' => 'enum',
- 'values' => ['occupied', 'on', 'off', 'idle', 'standby']
- ],
- 'live_swapfree' => [
- 'op' => self::OP_ORDINAL,
- 'type' => 'int',
- 'placeholder' => 'MiB',
- ],
- 'live_memfree' => [
- 'op' => self::OP_ORDINAL,
- 'type' => 'int',
- 'placeholder' => 'MiB',
- ],
- 'live_tmpfree' => [
- 'op' => self::OP_ORDINAL,
- 'type' => 'int',
- 'placeholder' => 'MiB',
- ],
- ];
- if (Module::isAvailable('locations')) {
- self::$columns['location'] = [
- 'op' => self::OP_STRCMP,
- 'type' => 'enum',
- 'show' => true,
- // values filled on render NOCOMMIT
- ];
+ public function type() { return 'date'; }
+
+ public function whereClause(string $operator, $argument, array &$args, array &$joins)
+ {
+ $key = self::getNewKey($this->column);
+ $addendum = '';
+
+ if (!preg_match('/^(?<date>\d{4}-\d{2}-\d{2})(\s+(?<h>\d{1,2})(:(?<m>\d{2})(:\d+)?)?)?$/', $argument, $out)) {
+ Message::addError('invalid-date-format', $argument);
+ return '0';
}
+
+ if (isset($out['m'])) {
+ $span = 'minute';
+ } elseif (isset($out['h'])) {
+ $span = 'hour';
+ $argument .= ':00';
+ } else {
+ $span = 'day';
+ }
+
+ $args[$key] = strtotime($argument);
+ if ($operator === '=' || $operator === '!=') {
+ $key2 = self::getNewKey($this->column);
+ $args[$key2] = strtotime(' +1 ' . $span, $args[$key]);
+ return ($operator === '=' ? '' : 'NOT ') . 'm.' . $this->column . " BETWEEN :$key AND :$key2";
+ }
+ if ($operator === '>' || $operator === '<=') {
+ $args[$key] = strtotime('+1 ' . $span . ' -1 second', $args[$key]);
+ }
+
+ return 'm.' . $this->column . ' ' . $operator . ' :' . $key . $addendum;
}
}
class RamGbStatisticsFilter extends StatisticsFilter
{
- public function __construct($operator, $argument)
+
+ public function __construct()
{
- parent::__construct('mbram', $operator, $argument);
+ parent::__construct('mbram', self::OP_ORDINAL, 'GiB');
}
- public function whereClause(&$args, &$joins)
+ public function whereClause(string $operator, $argument, array &$args, array &$joins)
{
- $lower = floor(StatisticsFilter::findBestValue(StatisticsFilter::SIZE_RAM, (int)$this->argument, false) * 1024 - 100);
- $upper = ceil(StatisticsFilter::findBestValue(StatisticsFilter::SIZE_RAM, (int)$this->argument, true) * 1024 + 100);
- if ($this->operator == '=') {
+ $lower = floor(StatisticsFilter::findBestValue(StatisticsFilter::SIZE_RAM, (int)$argument, false) * 1024 - 100);
+ $upper = ceil(StatisticsFilter::findBestValue(StatisticsFilter::SIZE_RAM, (int)$argument, true) * 1024 + 100);
+ if ($operator == '=') {
return " mbram BETWEEN $lower AND $upper";
- } elseif ($this->operator == '<') {
+ } elseif ($operator == '<') {
return " mbram < $lower";
- } elseif ($this->operator == '<=') {
+ } elseif ($operator == '<=') {
return " mbram <= $upper";
- } elseif ($this->operator == '>') {
+ } elseif ($operator == '>') {
return " mbram > $upper";
- } elseif ($this->operator == '>=') {
+ } elseif ($operator == '>=') {
return " mbram >= $lower";
- } elseif ($this->operator == '!=') {
- return " (mbram < $lower OR mbram > $upper)";
- } else {
- error_log("unimplemented operator in RamGbFilter: $this->operator");
-
- return ' 1';
}
+ // !=
+ return " (mbram < $lower OR mbram > $upper)";
}
+
}
class RuntimeStatisticsFilter extends StatisticsFilter
{
- public function __construct($operator, $argument)
+
+ public function __construct()
{
- parent::__construct('lastboot', $operator, $argument);
+ parent::__construct('lastboot', self::OP_ORDINAL);
}
- public function whereClause(&$args, &$joins)
+ public function whereClause(string $operator, $argument, array &$args, array &$joins)
{
- $upper = time() - (int)$this->argument * 3600;
+ $upper = time() - (int)$argument * 3600;
$lower = $upper - 3600;
$common = "state IN ('OCCUPIED', 'IDLE', 'STANDBY') AND";
- if ($this->operator == '=') {
- return "$common ({$this->column} BETWEEN $lower AND $upper)";
- } elseif ($this->operator == '<') {
- return "$common {$this->column} > $upper";
- } elseif ($this->operator == '<=') {
- return "$common {$this->column} > $lower";
- } elseif ($this->operator == '>') {
- return "$common {$this->column} < $lower";
- } elseif ($this->operator == '>=') {
- return "$common {$this->column} < $upper";
- } elseif ($this->operator == '!=') {
- return "$common ({$this->column} < $lower OR {$this->column} > $upper)";
- } else {
- error_log("unimplemented operator in RuntimeFilter: $this->operator");
- return ' 1';
+ if ($operator == '=') {
+ return "$common (lastboot BETWEEN $lower AND $upper)";
+ } elseif ($operator == '<') {
+ return "$common lastboot > $upper";
+ } elseif ($operator == '<=') {
+ return "$common lastboot > $lower";
+ } elseif ($operator == '>') {
+ return "$common lastboot < $lower";
+ } elseif ($operator == '>=') {
+ return "$common lastboot < $upper";
}
+ // !=
+ return "$common (lastboot < $lower OR lastboot > $upper)";
}
}
class Id44StatisticsFilter extends StatisticsFilter
{
- public function __construct($operator, $argument)
+
+ public function __construct()
{
- parent::__construct('id44mb', $operator, $argument);
+ parent::__construct('id44mb', self::OP_ORDINAL,'GiB');
}
- public function whereClause(&$args, &$joins)
+ public function whereClause(string $operator, $argument, array &$args, array &$joins)
{
- if ($this->operator === '=' || $this->operator === '!=') {
- $lower = floor(StatisticsFilter::findBestValue(StatisticsFilter::SIZE_ID44, $this->argument, false) * 1024 - 100);
- $upper = ceil(StatisticsFilter::findBestValue(StatisticsFilter::SIZE_ID44, $this->argument, true) * 1024 + 100);
+ if ($operator === '=' || $operator === '!=') {
+ $lower = floor(StatisticsFilter::findBestValue(StatisticsFilter::SIZE_ID44, $argument, false) * 1024 - 100);
+ $upper = ceil(StatisticsFilter::findBestValue(StatisticsFilter::SIZE_ID44, $argument, true) * 1024 + 100);
} else {
- $lower = $upper = round($this->argument * 1024);
+ $lower = $upper = round($argument * 1024);
}
- if ($this->operator === '=') {
+ if ($operator === '=') {
return " id44mb BETWEEN $lower AND $upper";
- } elseif ($this->operator === '!=') {
- return " id44mb < $lower OR id44mb > $upper";
- } elseif ($this->operator === '<=') {
+ } elseif ($operator === '<=') {
return " id44mb <= $upper";
- } elseif ($this->operator === '>=') {
+ } elseif ($operator === '>=') {
return " id44mb >= $lower";
- } elseif ($this->operator === '<') {
+ } elseif ($operator === '<') {
return " id44mb < $lower";
- } elseif ($this->operator === '>') {
+ } elseif ($operator === '>') {
return " id44mb > $upper";
- } else {
- error_log("unimplemented operator in Id44Filter: $this->operator");
-
- return ' 1';
}
+ // !=
+ return " id44mb < $lower OR id44mb > $upper";
}
}
class StateStatisticsFilter extends StatisticsFilter
{
- public function __construct($operator, $argument)
+
+ public function __construct()
{
- parent::__construct(null, $operator, $argument);
+ parent::__construct('state', self::OP_NOMINAL);
}
- public function whereClause(&$args, &$joins)
+ public function whereClause(string $operator, $argument, array &$args, array &$joins)
{
$map = [ 'on' => ['IDLE', 'OCCUPIED'], 'off' => ['OFFLINE'], 'idle' => ['IDLE'], 'occupied' => ['OCCUPIED'], 'standby' => ['STANDBY'] ];
- $neg = $this->operator == '!=' ? 'NOT ' : '';
- if (array_key_exists($this->argument, $map)) {
+ $neg = $operator == '!=' ? 'NOT ' : '';
+ if (array_key_exists($argument, $map)) {
$key = StatisticsFilter::getNewKey($this->column);
- $args[$key] = $map[$this->argument];
+ $args[$key] = $map[$argument];
return " m.state $neg IN ( :$key ) ";
} else {
- Message::addError('invalid-filter-argument', 'state', $this->argument);
+ Message::addError('invalid-filter-argument', 'state', $argument);
return ' 1';
}
}
}
-class LocationStatisticsFilter extends StatisticsFilter
+class LocationStatisticsFilter extends EnumStatisticsFilter
{
- public function __construct($operator, $argument)
+
+ public function __construct()
{
- parent::__construct('locationid', $operator, $argument);
+ $locs = [];
+ foreach (Location::getLocations(-1, 0, true) as $loc) {
+ $locs[] = [
+ 'key' => $loc['locationid'],
+ 'value' => $loc['locationpad'] . ' ' . $loc['locationname'],
+ ];
+ }
+ parent::__construct('locationid', $locs);
+
}
- public function whereClause(&$args, &$joins)
+ public function type() { return 'enum'; }
+
+ public function whereClause(string $operator, $argument, array &$args, array &$joins)
{
- $recursive = (substr($this->operator, -1) === '~');
- $this->operator = str_replace('~', '=', $this->operator);
+ $recursive = (substr($operator, -1) === '~');
+ $operator = str_replace('~', '=', $operator);
- if (is_array($this->argument)) {
- if ($recursive)
- Util::traceError('Cannot use ~ operator for location with array');
- } else {
- settype($this->argument, 'int');
+ if ($recursive && is_array($argument)) {
+ Util::traceError('Cannot use ~ operator for location with array');
}
- $neg = $this->operator === '=' ? '' : 'NOT';
- if ($this->argument === 0) {
- return "m.locationid IS $neg NULL";
- } else {
- $key = StatisticsFilter::getNewKey($this->column);
- if ($recursive) {
- $args[$key] = array_keys(Location::getRecursiveFlat($this->argument));
- } else {
- $args[$key] = $this->argument;
+ if ($recursive) {
+ $argument = array_keys(Location::getRecursiveFlat($argument));
+ }
+ return parent::whereClause($operator, $argument, $args, $joins);
+ }
+
+ public function filterLocations($list)
+ {
+ if ($list === false || in_array(0, $list))
+ return;
+ foreach ($this->values as &$loc) {
+ if (!in_array($loc['key'], $list)) {
+ $loc['disabled'] = 'disabled';
}
- return "m.locationid $neg IN (:$key)";
}
}
}
-class SubnetStatisticsFilter extends StatisticsFilter
+class IpStatisticsFilter extends StatisticsFilter
{
- public function __construct($operator, $argument)
+
+ public function __construct()
{
- parent::__construct(null, $operator, $argument);
+ parent::__construct('clientip', self::OP_NOMINAL, '1.2.3.4, 1.2.3.*, 1.2.3/24');
}
- public function whereClause(&$args, &$joins)
+ public function whereClause(string $operator, $argument, array &$args, array &$joins)
{
- $argument = preg_replace('/[^0-9\.:]/', '', $this->argument);
- return " clientip LIKE '$argument%'";
+ $argument = preg_replace('#[^0-9.:/*]#', '', $argument);
+ if (strpos($argument, '/') !== false) {
+ $range = IpUtil::parseCidr($argument);
+ if ($range === false) {
+ Message::addError('invalid-cidr-notion', $argument);
+ return '0';
+ }
+ return 'INET_ATON(clientip) BETWEEN ' . $range['start'] . ' AND ' . $range['end'];
+ }
+ return "clientip LIKE '" . str_replace('*', '%', $argument) . "'";
}
}
class IsClientStatisticsFilter extends StatisticsFilter
{
- public function __construct($argument)
+
+ public function __construct()
{
- parent::__construct(null, null, $argument);
+ parent::__construct(null, []);
}
- public function whereClause(&$args, &$joins)
+ public function whereClause(string $operator, $argument, array &$args, array &$joins)
{
- if ($this->argument) {
+ if ($argument) {
$joins[] = ' LEFT JOIN runmode USING (machineuuid)';
return "(runmode.isclient <> 0 OR runmode.isclient IS NULL)";
}
@@ -566,4 +548,29 @@ class IsClientStatisticsFilter extends StatisticsFilter
}
+class DatabaseFilter
+{
+ /** @var StatisticsFilter
+ */
+ private $inst;
+ public $op;
+ public $argument;
+ public function __construct(StatisticsFilter $inst, string $op, $argument)
+ {
+ $this->inst = $inst;
+ $this->op = $op;
+ $this->argument = $argument;
+ }
+ public function whereClause(array &$args, array &$joins)
+ {
+ return $this->inst->whereClause($this->op, $this->argument, $args, $joins);
+ }
+
+ public function isClass($what)
+ {
+ return get_class($this->inst) === $what;
+ }
+
+}
+
StatisticsFilter::initConstants();
diff --git a/modules-available/statistics/inc/statisticsfilterset.inc.php b/modules-available/statistics/inc/statisticsfilterset.inc.php
index c2642850..a96102dc 100644
--- a/modules-available/statistics/inc/statisticsfilterset.inc.php
+++ b/modules-available/statistics/inc/statisticsfilterset.inc.php
@@ -3,11 +3,9 @@
class StatisticsFilterSet
{
/**
- * @var \StatisticsFilter[]
+ * @var \DatabaseFilter[]
*/
private $filters;
- private $sortDirection;
- private $sortColumn;
private $cache = false;
@@ -16,34 +14,17 @@ class StatisticsFilterSet
$this->filters = $filters;
}
- public function setSort($col, $direction)
- {
- $direction = ($direction === 'DESC' ? 'DESC' : 'ASC');
-
- if (!is_string($col) || !array_key_exists($col, StatisticsFilter::$columns)) {
- /* default sorting column is clientip */
- $col = 'clientip';
- }
- if ($col === $this->sortColumn && $direction === $this->sortDirection)
- return;
- $this->cache = false;
- $this->sortDirection = $direction;
- $this->sortColumn = $col;
- }
-
- public function makeFragments(&$where, &$join, &$sort, &$args)
+ public function makeFragments(&$where, &$join, &$args)
{
if ($this->cache !== false) {
$where = $this->cache['where'];
$join = $this->cache['join'];
- $sort = $this->cache['sort'];
$args = $this->cache['args'];
return;
}
/* generate where clause & arguments */
$where = '';
$joins = [];
- $sort = "";
$args = [];
if (empty($this->filters)) {
$where = ' 1 ';
@@ -54,18 +35,7 @@ class StatisticsFilterSet
}
}
$join = implode(' ', array_unique($joins));
-
- $col = $this->sortColumn;
- $isMapped = array_key_exists('map_sort', StatisticsFilter::$columns[$col]);
- $concreteCol = ($isMapped ? StatisticsFilter::$columns[$col]['map_sort'] : $col) ;
-
- if ($concreteCol === 'clientip') {
- $concreteCol = "INET_ATON(clientip)";
- }
-
- $sort = " ORDER BY " . $concreteCol . " " . $this->sortDirection
- . ", machineuuid ASC";
- $this->cache = compact('where', 'join', 'sort', 'args');
+ $this->cache = compact('where', 'join', 'args');
}
public function isNoId44Filter()
@@ -74,33 +44,23 @@ class StatisticsFilterSet
return $filter !== false && $filter->argument == 0;
}
- public function getSortDirection()
- {
- return $this->sortDirection;
- }
-
- public function getSortColumn()
- {
- return $this->sortColumn;
- }
-
public function filterNonClients()
{
if (Module::get('runmode') === false || $this->hasFilter('IsClientFilter') !== false)
return;
$this->cache = false;
// Runmode module exists, add filter
- $this->filters[] = new IsClientStatisticsFilter(true);
+ $this->filters[] = (new IsClientStatisticsFilter())->bind('=', true);
}
/**
* @param string $type filter type (class name)
- * @return false|StatisticsFilter The filter, false if not found
+ * @return false|DatabaseFilter The filter, false if not found
*/
public function hasFilter($type)
{
foreach ($this->filters as $filter) {
- if (get_class($filter) === $type) {
+ if ($filter->isClass($type)) {
return $filter;
}
}
@@ -108,6 +68,17 @@ class StatisticsFilterSet
}
/**
+ * @param string $type filter type key/id
+ * @return false|DatabaseFilter The filter, false if not found
+ */
+ public function hasFilterKey($type)
+ {
+ if (isset($this->filters[$type]))
+ return $this->filters[$type];
+ return false;
+ }
+
+ /**
* Add a location filter based on the allowed permissions for the given permission.
* Returns false if the user doesn't have the given permission for any location.
*
@@ -116,6 +87,8 @@ class StatisticsFilterSet
*/
public function setAllowedLocationsFromPermission($permission)
{
+ if (!Module::isAvailable('locations'))
+ return true;
$locs = User::getAllowedLocations($permission);
if (empty($locs))
return false;
@@ -124,7 +97,7 @@ class StatisticsFilterSet
return true;
unset($this->filters['permissions']);
} else {
- $this->filters['permissions'] = new LocationStatisticsFilter('=', $locs);
+ $this->filters['permissions'] = StatisticsFilter::$columns['location']->bind('=', $locs);
}
$this->cache = false;
return true;
diff --git a/modules-available/statistics/pages/list.inc.php b/modules-available/statistics/pages/list.inc.php
index a709ab3d..7b8e8f5f 100644
--- a/modules-available/statistics/pages/list.inc.php
+++ b/modules-available/statistics/pages/list.inc.php
@@ -15,7 +15,6 @@ class SubPage
$filters = StatisticsFilter::parseQuery();
$filterSet = new StatisticsFilterSet($filters);
- $filterSet->setSort($sortColumn, $sortDirection);
if (!$filterSet->setAllowedLocationsFromPermission('view.list')) {
Message::addError('main.no-permission');
@@ -32,7 +31,7 @@ class SubPage
private static function showMachineList($filterSet)
{
Module::isAvailable('js_stupidtable');
- $filterSet->makeFragments($where, $join, $sort, $args);
+ $filterSet->makeFragments($where, $join, $args);
$xtra = '';
if ($filterSet->isNoId44Filter()) {
$xtra .= ', data';
@@ -47,7 +46,7 @@ class SubPage
m.logintime, m.state, m.currentuser, m.realcores, m.mbram, m.kvmstate, m.cpumodel, m.id44mb, m.hostname, m.notes IS NOT NULL AS hasnotes,
m.badsectors, Count(s.machineuuid) AS confvars $xtra FROM machine m
LEFT JOIN setting_machine s USING (machineuuid)
- $join WHERE $where GROUP BY m.machineuuid $sort", $args);
+ $join WHERE $where GROUP BY m.machineuuid", $args);
$rows = array();
$singleMachine = 'none';
// TODO: Cannot disable checkbox for those where user has no permission, since we got multiple actions now
diff --git a/modules-available/statistics/pages/summary.inc.php b/modules-available/statistics/pages/summary.inc.php
index c2e3ac80..ead62a26 100644
--- a/modules-available/statistics/pages/summary.inc.php
+++ b/modules-available/statistics/pages/summary.inc.php
@@ -17,7 +17,6 @@ class SubPage
$filters = StatisticsFilter::parseQuery();
$filterSet = new StatisticsFilterSet($filters);
- $filterSet->setSort($sortColumn, $sortDirection);
if (!$filterSet->setAllowedLocationsFromPermission('view.summary')) {
Message::addError('main.no-permission');
@@ -47,7 +46,7 @@ class SubPage
*/
private static function showSummary($filterSet)
{
- $filterSet->makeFragments($where, $join, $sort, $args);
+ $filterSet->makeFragments($where, $join, $args);
$known = Database::queryFirst("SELECT Count(*) AS val FROM machine m $join WHERE $where", $args);
$on = Database::queryFirst("SELECT Count(*) AS val FROM machine m $join WHERE state IN ('IDLE', 'OCCUPIED') AND ($where)", $args);
$used = Database::queryFirst("SELECT Count(*) AS val FROM machine m $join WHERE state = 'OCCUPIED' AND ($where)", $args);
@@ -100,7 +99,7 @@ class SubPage
*/
private static function showSystemModels($filterSet)
{
- $filterSet->makeFragments($where, $join, $sort, $args);
+ $filterSet->makeFragments($where, $join, $args);
$res = Database::simpleQuery('SELECT systemmodel, Round(AVG(realcores)) AS cores, Count(*) AS `count` FROM machine m'
. " $join WHERE $where GROUP BY systemmodel ORDER BY `count` DESC, systemmodel ASC", $args);
$lines = array();
@@ -130,8 +129,9 @@ class SubPage
*/
private static function showMemory($filterSet)
{
- $filterSet->makeFragments($where, $join, $sort, $args);
- $res = Database::simpleQuery("SELECT mbram, Count(*) AS `count` FROM machine m $join WHERE $where GROUP BY mbram", $args);
+ $filterSet->makeFragments($where, $join, $args);
+ $res = Database::simpleQuery("SELECT mbram, Count(*) AS `count` FROM machine m $join
+ WHERE $where GROUP BY mbram", $args);
$lines = array();
while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
$gb = (int)ceil($row['mbram'] / 1024);
@@ -174,9 +174,10 @@ class SubPage
*/
private static function showKvmState($filterSet)
{
- $filterSet->makeFragments($where, $join, $sort, $args);
+ $filterSet->makeFragments($where, $join, $args);
$colors = array('UNKNOWN' => '#666', 'UNSUPPORTED' => '#ea5', 'DISABLED' => '#e55', 'ENABLED' => '#6d6');
- $res = Database::simpleQuery("SELECT kvmstate, Count(*) AS `count` FROM machine m $join WHERE $where GROUP BY kvmstate ORDER BY `count` DESC", $args);
+ $res = Database::simpleQuery("SELECT kvmstate, Count(*) AS `count` FROM machine m $join
+ WHERE $where GROUP BY kvmstate ORDER BY `count` DESC", $args);
$lines = array();
$json = array();
while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
@@ -195,7 +196,7 @@ class SubPage
*/
private static function showId44($filterSet)
{
- $filterSet->makeFragments($where, $join, $sort, $args);
+ $filterSet->makeFragments($where, $join, $args);
$res = Database::simpleQuery("SELECT id44mb, Count(*) AS `count` FROM machine m $join WHERE $where GROUP BY id44mb", $args);
$lines = array();
$total = 0;
@@ -245,7 +246,7 @@ class SubPage
*/
private static function showLatestMachines($filterSet)
{
- $filterSet->makeFragments($where, $join, $sort, $args);
+ $filterSet->makeFragments($where, $join, $args);
$args['cutoff'] = ceil(time() / 3600) * 3600 - 86400 * 10;
$res = Database::simpleQuery("SELECT machineuuid, clientip, hostname, firstseen, mbram, kvmstate, id44mb FROM machine m $join"
diff --git a/modules-available/statistics/templates/filterbox.html b/modules-available/statistics/templates/filterbox.html
index 34f4d3a6..e7c1cd9b 100644
--- a/modules-available/statistics/templates/filterbox.html
+++ b/modules-available/statistics/templates/filterbox.html
@@ -7,7 +7,7 @@
<input type="hidden" name="do" value="statistics">
<div class="btn-group pull-right">
<button type="submit" hidden><!-- first button, so hitting enter in the form fields doesn't jump to summary -->
- <button class="btn btn-default {{statButtonClass}}" type="submit" name="show" value="summary"
+ <button class="btn btn-default {{summaryButtonClass}}" type="submit" name="show" value="summary"
{{perms.view.summary.disabled}}>
<span class="glyphicon glyphicon-stats"></span>
{{lang_showVisualization}}