(feat): Tags

fix(tag): Create tags on mobile

chore(tags): Change TagName to Name

chore(tags): eslint

chore(tags): dbFetchAll to dbQuery for removetag

chore(events): eslint (attempt 2)

feat(tags): Better handling of keyboard

fix(tags): Enter key for creating new tag

fix(tags): Don't allow space as a tag name

feat(tags): Delete tag if last assignment removed

fix(tags): Increase height of dropdown

in progress

fix(Tags): Use T.Id on the events page dropdown

fix(Tags): Remove $availableTags from events.php

chore(sql): Formatting sql statements

feat(Tags): Working OR on filters and events pages

fix(filter): Populate availableTags

chore(Tags): code formatting

fix(tag): Add tag on create tag

Fix(tags): Remove tag from available if last

feat(tags): Add zm_update.sql

fix(chosen): Undo css width

fix(chosen): tags dropdown width

fix(tags): dropdown over timeline

fix(tags): Full width input

fix(events): Refresh table on page show

chore(filter): Clean up availableTags

chore(event): Clean up available & selected Tags

fix(event): Update available tags on remove

fix(event): Remove hack for selected tags

feat(tags): Blur input after adding tag

doc(tags): Initial tags documentation

fix(tags): Dark theme dropdown

fix(tags): Dark theme for tags on input

fix(tags): Dark theme for highlight in dropdown

fix(tags): Populate filter tags droplist

chore(): Bump zm_update to 1.37.42

chore(tags): Move mobile check to skin.js

chore(tags): Comment debug statements

fix(tags): Enter key to create tag on mobile Chome

chore(tags): Space in 'All Tags' for translation

Temporary commit to handle cookie expiration times

chore(tags): Remove unnecessary Tag(s) from en_gb

chore(): Cleanup unnecessary Error and Debug

chore(): Resolve merge conflicts

chore(): Address merge conflicts with master
pull/3758/head
Simpler1 2023-06-03 19:27:43 -04:00
parent df411c3fe3
commit 18d74ed7ac
27 changed files with 901 additions and 59 deletions

47
db/zm_update-1.37.44.sql Normal file
View File

@ -0,0 +1,47 @@
--
-- This adds Tags
--
SELECT 'Checking For Tags Table';
SET @s = (SELECT IF(
(SELECT COUNT(*)
FROM INFORMATION_SCHEMA.TABLES
WHERE table_name = 'Tags'
AND table_schema = DATABASE()
) > 0,
"SELECT 'Tags table exists'",
"CREATE TABLE `Tags` (
`Id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`Name` varchar(64) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL DEFAULT '',
`CreateDate` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`CreatedBy` int(10) unsigned,
`LastAssignedDate` dateTime,
PRIMARY KEY (`Id`),
UNIQUE(`Name`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"
));
PREPARE stmt FROM @s;
EXECUTE stmt;
SELECT 'Checking For Events_Tags Table';
SET @s = (SELECT IF(
(SELECT COUNT(*)
FROM INFORMATION_SCHEMA.TABLES
WHERE table_name = 'Events_Tags'
AND table_schema = DATABASE()
) > 0,
"SELECT 'Events_Tags table exists'",
"CREATE TABLE `Events_Tags` (
`TagId` bigint(20) unsigned NOT NULL,
`EventId` bigint(20) unsigned NOT NULL,
`AssignedDate` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`AssignedBy` int(10) unsigned,
PRIMARY KEY (`TagId`, `EventId`),
CONSTRAINT `Events_Tags_ibfk_1` FOREIGN KEY (`TagId`) REFERENCES `Tags` (`Id`) ON DELETE CASCADE,
CONSTRAINT `Events_Tags_ibfk_2` FOREIGN KEY (`EventId`) REFERENCES `Events` (`Id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"
));
PREPARE stmt FROM @s;
EXECUTE stmt;

View File

@ -11,6 +11,7 @@ User Guide
viewmonitors
filterevents
viewevents
tags
options
cameracontrol
mobile

38
docs/userguide/tags.rst Normal file
View File

@ -0,0 +1,38 @@
Tags
====
Tags are a simple quick way to categorize events so that you can identify them easier.
Creating New Tags
-----------------
Creating new tags is as easy as typing a word in the tags field (located just above the video). Pressing the space bar, comma, or Enter will create the new tag and add it to the event.
Adding Existing Tags to an Event
--------------------------------
Clicking in the tags field will show a dropdown list of all of the available tags in descending order of when they were last added to an event.
An existing tag can be added to the event by clicking it from the dropdown or by using the down/up arrow keys to highlight the desired tag and pressing Enter.
<Ctrl-Down Arrow> will add the tag most recently added to any event to the current event.
Typing in the tags field will filter the available tags to the ones that contain the text typed.
.. note::
Since you can use the right/left arrows to move between events when the tags field doesn't have focus, you can quickly add the most recent tag with <Ctrl-Down Arrow> and then move to the next event with Right Arrow. You can also use the Down Arrow to bring up the available tags to add a different tag before pressing the Right Arrow to move to the next event.
Removing Tags from an Event
---------------------------
Pressing the "x" to the right of a tag will remove it from the event. When the tag is removed from the last event, the tag will be deleted from the available tags.
Filtering with Tags
===================
Current Limitations
-------------------
1. Filtering for multiple tags is an OR search (Goal is to make this an AND search)
2. Resulting events only display the tags that were searched (Goal is to display all of the tags on the resulting events)
3. There is no way to search for events that don't have any tag (Goal is to provide search criteria for events with no tag)
4. There is no way to search for events with ONLY the specified tag or tags (Goal is to provide search criteria to search for events with ONLY the specified tag or tags)

View File

@ -143,8 +143,21 @@ sub Sql {
}
my $filter_expr = ZoneMinder::General::jsonDecode($self->{Query_json});
my $sql = 'SELECT E.*, unix_timestamp(E.StartDateTime) as Time
FROM Events as E';
my $sql = '
SELECT
E.*,
unix_timestamp(E.StartDateTime)
AS Time,
GROUP_CONCAT(T.Name SEPARATOR ", ")
FROM Events
AS E
LEFT JOIN Events_Tags
AS ET
ON E.Id = ET.EventId
LEFT JOIN Tags
AS T
ON T.Id = ET.TagId
';
if ( $filter_expr->{terms} ) {
foreach my $term ( @{$filter_expr->{terms}} ) {
@ -164,6 +177,8 @@ sub Sql {
if ( $term->{attr} eq 'AlarmedZoneId' ) {
$term->{op} = 'EXISTS';
} elsif ( $term->{attr} eq 'Tags' ) {
$self->{Sql} .= 'T.Name';
} elsif ( $term->{attr} =~ /^Monitor/ ) {
$sql = 'SELECT E.*, unix_timestamp(E.StartDateTime) as Time, M.Name as MonitorName
FROM Events as E INNER JOIN Monitors as M on M.Id = E.MonitorId';
@ -368,6 +383,8 @@ sub Sql {
my $sort_column = '';
if ( $filter_expr->{sort_field} eq 'Id' ) {
$sort_column = 'E.Id';
} elsif ( $filter_expr->{sort_field} eq 'Tag' ) {
$sort_column = 'T.Name';
} elsif ( $filter_expr->{sort_field} eq 'MonitorName' ) {
$sql = 'SELECT E.*, unix_timestamp(E.StartDateTime) as Time, M.Name as MonitorName
FROM Events as E INNER JOIN Monitors as M on M.Id = E.MonitorId';

View File

@ -15,7 +15,26 @@ if ( canView('Events') or canView('Snapshots') ) {
} elseif ( empty($_REQUEST['scale']) ) {
ajaxError('Video Generation Failure, no scale given');
} else {
$sql = 'SELECT E.*,M.Name AS MonitorName,M.DefaultRate,M.DefaultScale FROM Events AS E INNER JOIN Monitors AS M ON E.MonitorId = M.Id WHERE E.Id = ?'.monitorLimitSql();
$sql = '
SELECT
E.*,
M.Name
AS MonitorName,M.DefaultRate,M.DefaultScale,
GROUP_CONCAT(T.Name SEPARATOR ", ")
AS Tags
FROM Events
AS E
INNER JOIN Monitors
AS M
ON E.MonitorId = M.Id
LEFT JOIN Events_Tags
AS ET
ON E.Id = ET.EventId
LEFT JOIN Tags
AS T
ON T.Id = ET.TagId
WHERE
E.Id = ?'.monitorLimitSql();
if ( !($event = dbFetchOne($sql, NULL, array( $_REQUEST['id']))) ) {
ajaxError('Video Generation Failure, Unable to load event');
} else {
@ -167,6 +186,45 @@ if ( canEdit('Events') ) {
ajaxResponse(array('refreshEvent'=>false, 'refreshParent'=>true));
}
break;
case 'getselectedtags' :
$sql = '
SELECT
T.*
FROM Tags
AS T
INNER JOIN Events_Tags
AS ET
ON ET.TagId = T.Id
WHERE ET.EventId = ?
';
$values = array($_REQUEST['id']);
$response = dbFetchAll($sql, NULL, $values);
ajaxResponse(array('response'=>$response));
break;
case 'addtag' :
$sql = 'INSERT INTO Events_Tags (TagId, EventId, AssignedBy) VALUES (?, ?, ?)';
$values = array($_REQUEST['tid'], $_REQUEST['id'], $user->Id());
$response = dbFetchAll($sql, NULL, $values);
$sql = 'UPDATE Tags SET LastAssignedDate = NOW() WHERE Id = ?';
$values = array($_REQUEST['tid']);
dbFetchAll($sql, NULL, $values);
ajaxResponse(array('response'=>$response));
break;
case 'removetag' :
$tagId = $_REQUEST['tid'];
dbQuery('DELETE FROM Events_Tags WHERE TagId = ? AND EventId = ?', array($tagId, $_REQUEST['id']));
$sql = "SELECT * FROM Events_Tags WHERE TagId = $tagId";
$rowCount = dbNumRows($sql);
if ($rowCount < 1) {
$sql = 'DELETE FROM Tags WHERE Id = ?';
$values = array($_REQUEST['tid']);
$response = dbNumRows($sql, $values);
ajaxResponse(array('response'=>$response));
}
ajaxResponse();
break;
} // end switch action
} // end if canEdit('Events')

View File

@ -32,7 +32,10 @@ require_once('includes/Filter.php');
$filter = isset($_REQUEST['filter']) ? ZM\Filter::parse($_REQUEST['filter']) : new ZM\Filter();
if (count( $user->unviewableMonitorIds())) {
$filter = $filter->addTerm(array('cnj'=>'and', 'attr'=>'MonitorId', 'op'=>'IN', 'val'=>$user->viewableMonitorIds()));
// $filter = $filter->addTerm(array('cnj'=>'and', 'attr'=>'MonitorId', 'op'=>'IN', 'val'=>'5'));
}
// TODO: Why is $user->viewableMonitorIds() returning $user->unviewableMonitorIds()
// Error('$user->viewableMonitorIds(): '.print_r($user->viewableMonitorIds()));
if (!empty($_REQUEST['StartDateTime'])) {
$filter->addTerm(array('cnj'=>'and', 'attr'=>'StartDateTime', 'op'=> '>=', 'val'=>$_REQUEST['StartDateTime']));
}
@ -42,6 +45,9 @@ if (!empty($_REQUEST['EndDateTime'])) {
if (!empty($_REQUEST['MonitorId'])) {
$filter->addTerm(array('cnj'=>'and', 'attr'=>'MonitorId', 'op'=> '=', 'val'=>$_REQUEST['MonitorId']));
}
if (!empty($_REQUEST['Tag'])) {
$filter->addTerm(array('cnj'=>'and', 'attr'=>'Tag', 'op'=>'=', 'val'=>''));
}
// Search contains a user entered string to search on
$search = isset($_REQUEST['search']) ? $_REQUEST['search'] : '';
@ -176,12 +182,14 @@ function queryRequest($filter, $search, $advsearch, $sort, $offset, $order, $lim
$columns = array('Id', 'MonitorId', 'StorageId', 'Name', 'Cause', 'StartDateTime', 'EndDateTime', 'Length', 'Frames', 'AlarmFrames', 'TotScore', 'AvgScore', 'MaxScore', 'Archived', 'Emailed', 'Notes', 'DiskSpace');
// The names of columns shown in the event view that are NOT dB columns in the database
$col_alt = array('Monitor', 'Storage');
$col_alt = array('Monitor', 'Tags', 'Storage');
if ( $sort != '' ) {
if (!in_array($sort, array_merge($columns, $col_alt))) {
ZM\Error('Invalid sort field: ' . $sort);
$sort = '';
} else if ( $sort == 'Tags' ) {
$sort = 'T.Name';
} else if ( $sort == 'Monitor' ) {
$sort = 'M.Name';
} else if ($sort == 'EndDateTime') {
@ -197,13 +205,46 @@ function queryRequest($filter, $search, $advsearch, $sort, $offset, $order, $lim
$values = array();
$likes = array();
// Error($filter->sql());
$where = $filter->sql()?' WHERE ('.$filter->sql().')' : '';
$col_str = 'E.*, UNIX_TIMESTAMP(E.StartDateTime) AS StartTimeSecs,
CASE WHEN E.EndDateTime IS NULL THEN (SELECT NOW()) ELSE E.EndDateTime END AS EndDateTime,
CASE WHEN E.EndDateTime IS NULL THEN (SELECT UNIX_TIMESTAMP(NOW())) ELSE UNIX_TIMESTAMP(EndDateTime) END AS EndTimeSecs,
M.Name AS Monitor';
$sql = 'SELECT ' .$col_str. ' FROM `Events` AS E INNER JOIN Monitors AS M ON E.MonitorId = M.Id'.$where.($sort?' ORDER BY '.$sort.' '.$order:'');
$col_str = '
E.*,
UNIX_TIMESTAMP(E.StartDateTime)
AS StartTimeSecs,
CASE WHEN E.EndDateTime
IS NULL
THEN (SELECT NOW())
ELSE E.EndDateTime END
AS EndDateTime,
CASE WHEN E.EndDateTime
IS NULL
THEN (SELECT UNIX_TIMESTAMP(NOW()))
ELSE UNIX_TIMESTAMP(EndDateTime) END
AS EndTimeSecs,
M.Name
AS Monitor,
GROUP_CONCAT(T.Name SEPARATOR ", ")
AS Tags';
$sql = '
SELECT
' .$col_str. '
FROM `Events`
AS E
INNER JOIN Monitors
AS M
ON E.MonitorId = M.Id
LEFT JOIN Events_Tags
AS ET
ON E.Id = ET.EventId
LEFT JOIN Tags
AS T
ON T.Id = ET.TagId
'.$where.'
GROUP BY E.Id
'.($sort?' ORDER BY '.$sort.' '.$order:'');
if ($filter->limit() and !count($filter->post_sql_conditions())) {
$sql .= ' LIMIT '.$filter->limit();
}
@ -243,6 +284,8 @@ function queryRequest($filter, $search, $advsearch, $sort, $offset, $order, $lim
$filtered_rows = null;
ZM\Debug('$advsearch: ' . $advsearch );
ZM\Debug('$search: ' . $search);
if (count($advsearch) or $search != '') {
$search_filter = new ZM\Filter();
$search_filter = $search_filter->addTerm(array('cnj'=>'and', 'attr'=>'Id', 'op'=>'IN', 'val'=>$event_ids));
@ -270,7 +313,24 @@ function queryRequest($filter, $search, $advsearch, $sort, $offset, $order, $lim
$search_filter = $search_filter->addTerms($terms, array('obr'=>1, 'cbr'=>1, 'op'=>'OR'));
} # end if search
$sql = 'SELECT ' .$col_str. ' FROM `Events` AS E INNER JOIN Monitors AS M ON E.MonitorId = M.Id WHERE '.$search_filter->sql().' ORDER BY ' .$sort. ' ' .$order;
$sql = 'SELECT ' .$col_str. '
FROM `Events`
AS E
INNER JOIN Monitors
AS M
ON E.MonitorId = M.Id
LEFT JOIN Events_Tags
AS ET
ON E.Id = ET.EventId
LEFT JOIN Tags
AS T
ON T.Id = ET.TagId
WHERE
'.$search_filter->sql().'
ORDER BY
' .$sort. '
' .$order;
$filtered_rows = dbFetchAll($sql);
ZM\Debug('Have ' . count($filtered_rows) . ' events matching search filter: '.$sql);
} else {
@ -303,6 +363,7 @@ function queryRequest($filter, $search, $advsearch, $sort, $offset, $order, $lim
$row['Archived'] = $row['Archived'] ? translate('Yes') : translate('No');
$row['Emailed'] = $row['Emailed'] ? translate('Yes') : translate('No');
$row['Cause'] = validHtmlStr($row['Cause']);
$row['Tags'] = validHtmlStr($row['Tags']);
$row['StartDateTime'] = $dateTimeFormatter->format(strtotime($row['StartDateTime']));
$row['EndDateTime'] = $row['EndDateTime'] ? $dateTimeFormatter->format(strtotime($row['EndDateTime'])) : null;
$row['Storage'] = ( $row['StorageId'] and isset($StorageById[$row['StorageId']]) ) ? $StorageById[$row['StorageId']]->Name() : 'Default';

View File

@ -472,7 +472,25 @@ function getNearEvents() {
$sortOrder = 'ASC';
}
$sql = 'SELECT E.Id AS Id, E.StartDateTime AS StartDateTime FROM Events AS E INNER JOIN Monitors AS M ON E.MonitorId = M.Id WHERE '.$sortColumn.' '.($sortOrder=='ASC'?'<=':'>=').' \''.$event[$_REQUEST['sort_field']].'\'';
$sql = '
SELECT
E.Id
AS Id,
E.StartDateTime
AS StartDateTime
FROM Events
AS E
INNER JOIN Monitors
AS M
ON E.MonitorId = M.Id
LEFT JOIN Events_Tags
AS ET
ON E.Id = ET.EventId
LEFT JOIN Tags
AS T
ON T.Id = ET.TagId
WHERE '.$sortColumn.'
'.($sortOrder=='ASC'?'<=':'>=').' \''.$event[$_REQUEST['sort_field']].'\'';
if ($filter->sql()) {
$sql .= ' AND ('.$filter->sql().')';
}
@ -490,7 +508,25 @@ function getNearEvents() {
$prevEvent = dbFetchNext($result);
$sql = 'SELECT E.Id AS Id, E.StartDateTime AS StartDateTime FROM Events AS E INNER JOIN Monitors AS M ON E.MonitorId = M.Id WHERE '.$sortColumn .' '.($sortOrder=='ASC'?'>=':'<=').' \''.$event[$_REQUEST['sort_field']].'\'';
$sql = '
SELECT
E.Id
AS Id,
E.StartDateTime
AS StartDateTime
FROM Events
AS E
INNER JOIN Monitors
AS M
ON E.MonitorId = M.Id
LEFT JOIN Events_Tags
AS ET
ON E.Id = ET.EventId
LEFT JOIN Tags
AS T
ON T.Id = ET.TagId
WHERE '.$sortColumn.'
'.($sortOrder=='ASC'?'>=':'<=').' \''.$event[$_REQUEST['sort_field']].'\'';
if ($filter->sql()) {
$sql .= ' AND ('.$filter->sql().')';
}

24
web/ajax/tags.php Normal file
View File

@ -0,0 +1,24 @@
<?php
switch ( $_REQUEST['action'] ) {
case 'getavailabletags' :
$sql = 'SELECT * FROM Tags ORDER BY LastAssignedDate DESC';
$dbFetchResult = dbFetchAll($sql);
ajaxResponse(array('response'=>$dbFetchResult));
break;
case 'createtag' :
$sql = 'INSERT INTO Tags (Name, CreatedBy) VALUES (?, ?) RETURNING Id';
$values = array($_REQUEST['tname'], $user->Id());
$result = dbFetchAll($sql, NULL, $values);
$r = $result[0];
$sql = 'SELECT * FROM Tags WHERE Id = ?';
$values = array($r['Id']);
$dbFetchResult = dbFetchAll($sql, NULL, $values);
ajaxResponse(array('response'=>$dbFetchResult));
break;
} // end switch action
ajaxError('Unrecognised action '.$_REQUEST['action']);
?>

View File

@ -47,8 +47,29 @@ $order = (isset($_REQUEST['order']) and (strtolower($_REQUEST['order']) == 'asc'
//
$where = 'WHERE MonitorId = '.$mid;
$col_str = 'E.*';
$sql = 'SELECT ' .$col_str. ' FROM `Events` AS E '.$where.' ORDER BY '.$sort.' '.$order. ' LIMIT ?';
$col_str = '
E.*,
T.Name
AS Tags ';
$sql = '
SELECT
' .$col_str. '
FROM `Events`
AS E
LEFT JOIN Events_Tags
AS ET
ON E.Id = ET.EventId
LEFT JOIN Tags
AS T
ON T.Id = ET.TagId
'.$where.'
ORDER BY
'.$sort.'
'.$order.'
LIMIT ?';
ZM\Debug('Calling the following sql query: ' .$sql);
$rows = dbQuery($sql, array($limit));

View File

@ -14,6 +14,7 @@ class Event extends ZM_Object {
'StorageId' => null,
'SecondaryStorageId' => null,
'Cause' => '',
'Tags' => array(),
'StartDateTime' => null,
'EndDateTime' => null,
'Width' => null,

View File

@ -11,6 +11,7 @@ class Event_Data extends ZM_Object {
'EventId' => null,
'FrameId' => null,
'MonitorId' => null,
'Tags' => array(),
'TimeStamp' => 0,
'Data' => '',
);

View File

@ -47,12 +47,19 @@ class Filter extends ZM_Object {
public $_pre_sql_conditions;
public $_post_sql_conditions;
protected $_Terms;
public $availableTags = array();
public function sql() {
// Debug('$_Terms: '. $_Terms);
// Debug('$_sql: ' . $_sql);
// Debug('$this->_sql: ' . $this->_sql);
if (!isset($this->_sql)) {
$this->_sql = '';
foreach ( $this->FilterTerms() as $term ) {
// Error($term->valid());
if ($term->valid()) {
// Debug('$this->_sql: ' . $this->_sql);
// Error($this-cnj);
if (!$this->_sql) {
if ($term->cnj) unset($term->cnj);
} else {
@ -60,10 +67,12 @@ class Filter extends ZM_Object {
}
$this->_sql .= $term->sql();
} else {
Debug('Term is not valid '.$term->to_string());
// Debug('Term is not valid '.$term->to_string());
}
// Debug('$term->_sql: ' . $term->_sql);
} # end foreach term
}
// Debug('$this->_sql: ' . $this->_sql);
return $this->_sql;
}
@ -118,18 +127,24 @@ class Filter extends ZM_Object {
}
public function FilterTerms() {
// echo '<pre style="text-align: start;">Terms before: '; print_r($this->Terms); echo '</pre>';
if (!isset($this->Terms)) {
$this->Terms = array();
$_terms = $this->terms();
// echo '<pre style="text-align: start;">$_terms: '; print_r($_terms); echo '</pre>';
if ($_terms) {
for ($i=0; $i < count($_terms); $i++) {
// Error($i);
// Error($_terms[$i]);
if (isset($_terms[$i])) {
$term = new FilterTerm($this, $_terms[$i], $i);
$this->Terms[] = $term;
// Error($this->Terms[]);
}
} # end foreach term
}
}
// echo '<pre style="text-align: start;">Terms after: '; print_r($this->Terms); echo '</pre>';
return $this->Terms;
}
@ -260,6 +275,7 @@ class Filter extends ZM_Object {
$Query['limit'] = func_get_arg(0);
$this->Query($Query);
}
// Error($this->Query()['limit']);
if ( isset( $this->Query()['limit'] ) )
return $this->{'Query'}['limit'];
return 0;
@ -414,8 +430,12 @@ class Filter extends ZM_Object {
}
}
if ( !empty($term['attr']) ) {
// Error($term['attr']);
$dtAttr = false;
switch ( $term['attr']) {
case 'Tags':
$sqlValue = 'T.Name';
break;
case 'Group':
$sqlValue = 'M.Id';
case 'Monitor':
@ -586,6 +606,8 @@ class Filter extends ZM_Object {
case 'Group':
$value = Group::get_group_sql($value);
break;
case 'Tags':
// Error($term['attr']);
case 'MonitorName':
case 'Name':
case 'Cause':
@ -719,10 +741,30 @@ class Filter extends ZM_Object {
}
$where = $this->sql() ? ' WHERE ('.$this->sql().')' : '';
// $where = ' WHERE ( T.Name = "Bird" )';
$sort = $this->sort_field() ? $this->sort_field() .' '.($this->sort_asc() ? 'ASC' : 'DESC') : '';
$col_str = 'E.*, M.Name AS Monitor';
$sql = 'SELECT ' .$col_str. ' FROM `Events` AS E INNER JOIN Monitors AS M ON E.MonitorId = M.Id'.$where.($sort?' ORDER BY '.$sort:'');
$col_str = '
E.*,
M.Name
AS Monitor';
$sql = '
SELECT
' .$col_str. '
FROM `Events`
AS E
INNER JOIN Monitors
AS M
ON E.MonitorId = M.Id
LEFT JOIN Events_Tags
AS ET
ON E.Id = ET.EventId
LEFT JOIN Tags
AS T
ON T.Id = ET.TagId
'.$where.($sort?' ORDER BY '.$sort:'');
if ($this->limit() and !count($this->pre_sql_conditions()) and !count($this->post_sql_conditions())) {
$sql .= ' LIMIT '.$this->limit();
}
@ -785,6 +827,7 @@ class Filter extends ZM_Object {
'StorageId' => translate('AttrStorageArea'),
'StorageServerId' => translate('AttrStorageServer'),
'SystemLoad' => translate('AttrSystemLoad'),
'Tags' => translate('Tags'),
'TotScore' => translate('AttrTotalScore'),
);
}
@ -896,6 +939,10 @@ class Filter extends ZM_Object {
}
}
}
// $availableTags = array();
foreach ( dbFetchAll('SELECT Id, Name FROM Tags ORDER BY LastAssignedDate DESC') AS $tag ) {
$availableTags[$tag['Id']] = validHtmlStr($tag['Name']);
}
for ($i=0; $i < count($terms); $i++) {
$term = $terms[$i];
@ -919,6 +966,23 @@ class Filter extends ZM_Object {
if ( $term['attr'] == 'Archived' ) {
$html .= '<td>'.translate('OpEq').'<input type="hidden" name="filter[Query][terms]['.$i.'][op]" value="="/></td>'.PHP_EOL;
$html .= '<td>'.htmlSelect("filter[Query][terms][$i][val]", $archiveTypes, $term['val']).'</td>'.PHP_EOL;
} else if ( $term['attr'] == 'Tags') {
// Error($term['attr']);
$html .= '<td>'.htmlSelect("filter[Query][terms][$i][op]", $opTypes, $term['op']).'</td>'.PHP_EOL;
$options = ['class'=>'chosen', 'multiple'=>'multiple'];
$selected = explode(',', $term['val']);
if (count($selected) == 1 and !$selected[0]) {
$selected = null;
}
$html .= '<td>'.htmlSelect("filter[Query][terms][$i][val]", $availableTags, $selected, $options).'</td>'.PHP_EOL;
// ZM\Debug('$availableTags: '.$availableTags);
// ZM\Debug('$selected: '.$selected);
} else if ( $term['attr'] == 'DateTime' || $term['attr'] == 'StartDateTime' || $term['attr'] == 'EndDateTime') {
$html .= '<td>'.htmlSelect("filter[Query][terms][$i][op]", $opTypes, $term['op']).'</td>'.PHP_EOL;
$html .= '<td><input type="text" name="filter[Query][terms]['.$i.'][val]" id="filter[Query][terms]['.$i.'][val]" value="'.(isset($term['val'])?validHtmlStr(str_replace('T', ' ', $term['val'])):'').'"/></td>'.PHP_EOL;
@ -983,6 +1047,7 @@ class Filter extends ZM_Object {
</tr>
';
} # end foreach term
// Error($html);
return $html;
} # end function widget()
@ -1006,6 +1071,10 @@ class Filter extends ZM_Object {
foreach ( $Servers as $server ) {
$servers[$server->Id()] = validHtmlStr($server->Name());
}
// $availableTags = array();
foreach ( dbFetchAll('SELECT Id, Name FROM Tags ORDER BY LastAssignedDate DESC') AS $tag ) {
$availableTags[$tag['Id']] = validHtmlStr($tag['Name']);
}
for ($i=0; $i < count($terms); $i++) {
$term = $terms[$i];
@ -1034,6 +1103,39 @@ class Filter extends ZM_Object {
if ( $term['attr'] == 'Archived' ) {
$html .= htmlSelect("filter[Query][terms][$i][val]", $archiveTypes, $term['val']).PHP_EOL;
} else if ( $term['attr'] == 'Tags' ) {
$selected = explode(',', $term['val']);
// echo '<pre>selected: '; print_r($selected); echo '</pre>';
if (count($selected) == 1 and !$selected[0]) {
$selected = null;
}
$options = ['class'=>'chosen', 'multiple'=>'multiple', 'data-placeholder'=>translate('All Tags')];
if (isset($term['cookie'])) {
$options['data-cookie'] = $term['cookie'];
if (!$selected and isset($_COOKIE[$term['cookie']]) and $_COOKIE[$term['cookie']])
$selected = explode(',', $_COOKIE[$term['cookie']]);
}
// These echo statements print these variables at the top of the view.
// echo '<pre>availableTags: '; print_r($availableTags); echo '</pre>';
// echo '<pre>selected: '; print_r($selected); echo '</pre>';
// echo '<pre>options: '; print_r($options); echo '</pre>';
$html .= '<span>'.htmlSelect("filter[Query][terms][$i][val]", $availableTags, $selected, $options).'</span>'.PHP_EOL;
// $html .= '<span>'.htmlSelect("filter[Query][terms][$i][val]", array_combine($availableTags,$availableTags), $term['val'],
// $options).'</span>'.PHP_EOL;
// $html .= '<span>'.htmlSelect("filter[Query][terms][$i][val]", $availableTags, $term['val'], $options).'</span>'.PHP_EOL;
// Debug doesn't work here.
// Debug('$availableTags: '.$availableTags);
// Debug('$selected: '.$selected);
// Debug('$options: '.$options);
} else if ( $term['attr'] == 'DateTime' || $term['attr'] == 'StartDateTime' || $term['attr'] == 'EndDateTime') {
$html .= '<span><input type="text" class="datetimepicker" name="filter[Query][terms]['.$i.'][val]"';
if (isset($term['id'])) {
@ -1079,6 +1181,7 @@ class Filter extends ZM_Object {
}
}
$selected = explode(',', $term['val']);
// echo '<pre>Monitor selected: '; print_r($selected); echo '</pre>';
if (count($selected) == 1 and !$selected[0]) {
$selected = null;
}
@ -1145,8 +1248,9 @@ class Filter extends ZM_Object {
$html .= '</span>';
} # end foreach term
$html .= '</div>';
// Error($html);
return $html;
} # end function widget()
} # end function simple_widget()
public function has_term($attr, $op=null) {
foreach ($this->terms() as $term) {

View File

@ -101,6 +101,7 @@ class FilterTerm {
case 'DiskPercent':
$value = '';
break;
case 'Tags':
case 'MonitorName':
case 'Name':
case 'Cause':
@ -293,6 +294,8 @@ class FilterTerm {
case 'StateId':
case 'Archived':
return $this->tablename.'.'.$this->attr;
case 'Tags':
return 'T.Id';
default :
return $this->tablename.'.'.$this->attr;
}
@ -444,6 +447,11 @@ class FilterTerm {
Error('Failed evaluating '.$string_to_eval);
return false;
}
} else if ( $this->attr == 'Tags' ) {
// Debug('TODO: Complete this post_sql_condition for Tags val: ' . $this->val . ' op: ' . $this->op . ' id: ' . $this->id);
// Debug(print_r($this, true));
// Debug(print_r($event, true));
return true;
} else {
Error('testing unsupported post term ' . $this->attr);
}
@ -460,7 +468,7 @@ class FilterTerm {
}
public function is_post_sql() {
if ( $this->attr == 'ExistsInFileSystem' ) {
if ( $this->attr == 'ExistsInFileSystem' || $this->attr == 'Tags') {
return true;
}
return false;
@ -515,6 +523,7 @@ class FilterTerm {
'Notes',
'StateId',
'Archived',
'Tags',
# The following are for snapshots
'CreatedOn',
'Description'
@ -536,6 +545,7 @@ class FilterTerm {
return false;
break;
case 'Archived' :
case 'Tags' :
case 'Monitor' :
case 'MonitorId' :
case 'ServerId' :

View File

@ -224,8 +224,8 @@ function dbFetchNext($result, $col=false) {
return false;
}
function dbNumRows( $sql ) {
$result = dbQuery($sql);
function dbNumRows($sql, $params=NULL) {
$result = dbQuery($sql, $params);
return $result->rowCount();
}

View File

@ -709,6 +709,7 @@ ul.nav.nav-pills.flex-column {
.chosen-container {
text-align: left;
min-width: 11em;
}
.chosen-single,
@ -884,3 +885,84 @@ a.flip {
button .material-icons {
font-size: 18px;
}
/* input[type="search"]::-webkit-search-cancel-button {
display: none;
} */
.tags-container {
display: flex;
flex-wrap: wrap;
border: 1px solid;
border-color: #ccc;
border-radius: 4px;
min-height: 35px;
margin: 0.25rem 1rem 0.25rem 1rem;
}
.tag {
background-color: #F0F0F0;
border-radius: 12px;
padding: 4px 8px;
margin: 4px;
display: flex;
align-items: center;
}
.tag-text {
margin-right: 4px;
}
.tag-remove {
cursor: pointer;
color: red;
}
.tag-input {
height: 30px;
margin-left: 8px;
border: none;
width: 100%;
}
.tag-input:focus {
outline: none;
}
.tag-dropdown {
vertical-align: center;
margin-right: 8px;
display: inline-block;
flex-grow: 2;
}
.tag-dropdown-content {
display: none;
position: absolute;
background-color: #f9f9f9;
min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 15;
padding: 4px 0;
overflow-y: auto;
max-height: 800px;
}
.tag-dropdown-item {
cursor: pointer;
padding: 4px 8px;
margin-bottom: 4px;
}
.tag-dropdown-item:hover {
background-color: #dfdfdf;
}
.hlight{
background:#dfdfdf;
}
.tag-input:focus + .tag-dropdown-content,
.tag-input + .tag-dropdown-content:active {
display: block;
}

View File

@ -230,3 +230,19 @@ ul.nav.nav-pills.flex-column {
.thead-highlight {
background-color:#485460;
}
.tag {
background-color: #444444;
}
.tag-dropdown-content {
background-color: #333333;
}
.tag-dropdown-item:hover {
background-color: #222222;
}
.hlight{
background:#444444;
}

View File

@ -1040,6 +1040,15 @@ function post(path, params, method='post') {
form.submit();
}
function isMobile() {
var result = false;
// device detection
if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substring(0, 4))) {
result = true;
}
return result;
}
const font = new FontFaceObserver('Material Icons', {weight: 400});
font.load().then(function() {
$j('.material-icons').css('display', 'inline-block');

View File

@ -207,6 +207,13 @@ if ( $Event->Id() and !file_exists($Event->Path()) )
</div>
</div>
<?php if ( $Event->Id() ) { ?>
<div class="tags-container">
<div class="tag-dropdown">
<!-- input type has to be "search" (not "text") so that the Enter button (not Next) works on mobile Chrome browser. -->
<input type="search" id="tagInput" class="tag-input" placeholder="Add tag" data-role="tagsinput">
<div class="tag-dropdown-content"></div>
</div>
</div>
<!-- BEGIN VIDEO CONTENT ROW -->
<div id="inner-content">
<div class="d-flex flex-row">

View File

@ -59,7 +59,12 @@ if (!$filter->Id()) {
'val' => $num_terms ? '' : (isset($_COOKIE['eventsEndDateTimeEnd']) ? $_COOKIE['eventsEndDateTimeEnd'] : ''),
'cnj' => 'and', 'cookie'=>'eventsEndDateTimeEnd'));
}
$filter->sort_terms(['Group','Monitor','StartDateTime','EndDateTime']);
if (!$filter->has_term('Tags')) {
$filter->addTerm(array('attr' => 'Tags', 'op' => '=',
'val' => $num_terms ? '' : (isset($_COOKIE['eventsTags']) ? $_COOKIE['eventsTags'] : ''),
'cnj' => 'and', 'cookie'=>'eventsTags'));
}
$filter->sort_terms(['Group','Monitor','StartDateTime','EndDateTime','Tags']);
#$filter->addTerm(array('cnj'=>'and', 'attr'=>'AlarmFrames', 'op'=> '>', 'val'=>'10'));
#$filter->addTerm(array('cnj'=>'and', 'attr'=>'StartDateTime', 'op'=> '<=', 'val'=>''));
}
@ -149,6 +154,7 @@ data-min-width="562"
<th data-sortable="true" data-field="Emailed" class="Emailed"><?php echo translate('Emailed') ?></th>
<th data-sortable="true" data-field="Monitor" class="Monitor"><?php echo translate('Monitor') ?></th>
<th data-sortable="true" data-field="Cause" class="Cause" data-click-to-select="false"><?php echo translate('Cause') ?></th>
<th data-sortable="true" data-field="Tags" class="Tags"><?php echo translate('Tags') ?></th>
<th data-sortable="true" data-field="StartDateTime" class="StartDateTime"><?php echo translate('AttrStartTime') ?></th>
<th data-sortable="true" data-field="EndDateTime" class="EndDateTime"><?php echo translate('AttrEndTime') ?></th>
<th data-sortable="true" data-field="Length" class="Length"><?php echo translate('Duration') ?></th>

View File

@ -135,6 +135,10 @@ foreach ( dbFetchAll('SELECT Id, Name, MonitorId FROM Zones ORDER BY lower(`Name
}
}
}
$availableTags = array();
foreach ( dbFetchAll('SELECT Id, Name FROM Tags ORDER BY LastAssignedDate DESC') AS $tag ) {
$availableTags[$tag['Id']] = validHtmlStr($tag['Name']);
}
xhtmlHeaders(__FILE__, translate('EventFilter'));
echo getBodyTopHTML();
@ -194,6 +198,7 @@ $sort_fields = array(
'Id' => translate('AttrId'),
'Name' => translate('AttrName'),
'Cause' => translate('AttrCause'),
'Tags' => translate('Tags'),
'DiskSpace' => translate('AttrDiskSpace'),
'Notes' => translate('AttrNotes'),
'MonitorName' => translate('AttrMonitorName'),

View File

@ -26,9 +26,12 @@ var streamStatus = null;
var lastEventId = 0;
var zmsBroke = false; //Use alternate navigation if zms has crashed
var wasHidden = false;
var availableTags = [];
var selectedTags = [];
$j(document).on("keydown", "", function(e) {
e = e || window.event;
if (!$j(".tag-input").is(":focus")) {
if ( $j(".modal").is(":visible") ) {
if (e.key === "Enter") {
if ( $j("#deleteConfirm").is(":visible") ) {
@ -59,10 +62,18 @@ $j(document).on("keydown", "", function(e) {
} else {
pauseClicked();
}
} else if (e.key === "ArrowDown") {
if (e.ctrlKey) {
addTag(availableTags[0]);
} else {
$j("#tagInput").focus();
showDropdown();
}
} else {
console.log('Modal is not visible: key not implemented: ', e.key, ' keyCode: ', e.keyCode);
}
}
}
});
function streamReq(data) {
@ -1046,6 +1057,9 @@ function onStatsResize(vidWidth) {
}
function initPage() {
getAvailableTags();
getSelectedTags();
// Load the event stats
getStat();
@ -1259,6 +1273,85 @@ function initPage() {
}
});
document.addEventListener('fullscreenchange', fullscreenChangeEvent);
if (isMobile()) { // Mobile
// Event listener for adding tags when Space or Comma key is pressed on mobile devices
// Mobile Firefox is consistent with Desktop Firefox and Desktop Chrome supporting event.key for space and comma.
// Mobile Chrome always returns Unidentified for event.key for space and comma.
$j('#tagInput').on('input', function(event) {
var key = this.value.substr(-1).charCodeAt(0);
if (key === 32 || key === 44) { // Space or Comma
const tagInput = $j(this);
const tagValue = tagInput.val().slice(0, -1).trim();
addOrCreateTag(tagValue);
event.preventDefault(); // Prevent the key from being entered in the input field
}
});
// Event listener for adding tags when Enter key is pressed on mobile devices
// All mobile and desktop browsers don't pick up on Enter as 'input'.
// Mobile Chrome 'input' doesn't pick up "Next" button as Enter.
$j('#tagInput').on('keydown', function(event) {
var key = event.key;
if (key === "Enter") { // Enter
const tagInput = $j(this);
const tagValue = tagInput.val().trim();
addOrCreateTag(tagValue);
event.preventDefault(); // Prevent the key from being entered in the input field
}
});
} else { // Desktop
// Event listener for adding tags when Enter key is pressed or highlighting available tag when up/down arrows are pressed
$j('#tagInput').on('keydown', function(event) {
event = event || window.event;
var $hlight = $j('div.tag-dropdown-item.hlight');
var $div = $j('div.tag-dropdown-item');
if (event.key === "ArrowDown") {
if (event.ctrlKey) {
addTag(availableTags[0]);
} else if ($div.is(":visible")) {
$hlight.removeClass('hlight').next().addClass('hlight');
if ($hlight.next().length == 0) {
$div.eq(0).addClass('hlight');
}
} else {
showDropdown();
}
} else if (event.key === "ArrowUp") {
$hlight.removeClass('hlight').prev().addClass('hlight');
if ($hlight.prev().length == 0) {
$div.eq(-1).addClass('hlight');
}
} else if (event.key === "Enter") {
var tagValue = $hlight.text();
if (!tagValue) {
const tagInput = $j(this);
tagValue = tagInput.val().trim();
}
addOrCreateTag(tagValue);
} else if (event.key === " " || event.key === ",") {
const tagInput = $j(this);
const tagValue = tagInput.val().trim();
addOrCreateTag(tagValue);
event.preventDefault(); // Prevent the key from being entered in the input field
} else if (event.key === "Escape") {
$j("#tagInput").blur();
}
});
}
// Event listener for typing in the tag input
$j('#tagInput').on('input', showDropdown);
// Event listener for clicking in the tag input
$j('#tagInput').on('focus', showDropdown);
// Event listener for removing tags
$j('.tags-container').on('click', '.tag-remove', function() {
const tagElement = $j(this).closest('.tag');
const tag = tagElement.data('tag');
removeTag(tag);
});
streamPlay();
if ( parseInt(ZM_OPT_USE_GEOLOCATION) && parseFloat(eventData.Latitude) && parseFloat(eventData.Longitude)) {
@ -1287,6 +1380,129 @@ function initPage() {
} // end if ZM_OPT_USE_GEOLOCATION
} // end initPage
function addOrCreateTag(tagValue) {
const tagNames = availableTags.map((t) => t.Name.toLowerCase());
const index = tagNames.indexOf(tagValue.toLowerCase());
if (index > -1) {
addTag(availableTags[index]);
$j('.tag-dropdown-content').hide();
} else if (tagValue.trim().length > 0) {
createTag(tagValue);
}
}
function clickTag() {
const tagName = $j(this).text();
const selectedTag = availableTags.find((tag) => tag.Name === tagName);
addTag(selectedTag);
}
function showDropdown() {
const dropdownContent = $j('.tag-dropdown-content');
dropdownContent.empty();
const input = $j('#tagInput').val().trim();
var matchingTags = [];
if (availableTags) {
matchingTags = availableTags.filter(function(tag) {
var isMatch = tag.Name.toLowerCase().includes(input.toLowerCase());
return isMatch && !isDup(tag.Name);
});
}
matchingTags.forEach(function(tag) {
const dropdownItem = $j('<div>', {class: 'tag-dropdown-item', text: tag.Name});
dropdownItem.appendTo(dropdownContent); // Append the element to the dropdown content
});
if (matchingTags.length > 0) {
$j('.tag-dropdown-content').off('click');
$j('.tag-dropdown-content').on('click', '.tag-dropdown-item', clickTag);
$j('.tag-dropdown-content').show();
} else {
$j('.tag-dropdown-content').hide();
}
}
function isDup(tagName) {
return $j('.tag-text').filter(function() {
var elemText = $j(this).text();
return elemText === tagName;
}).length != 0;
}
function formatTag(tag) {
const tagName = tag.Name;
const tagElement = $j('<div>', {class: 'tag'});
tagElement.data('tag', tag);
tagElement.append($j('<span>', {class: 'tag-text', text: tagName}));
tagElement.append($j('<span>', {class: 'tag-remove', text: '\u00D7'}));
$j('.tag-dropdown').before(tagElement);
}
function addTag(tag) {
if (tag.Name.trim() !== '' && !isDup(tag.Name)) {
$j.getJSON(thisUrl + '?request=event&action=addtag&tid=' + tag.Id + '&id=' + eventData.Id)
.done(function(data) {
formatTag(tag);
selectedTags.push(tag);
// Move the added tag to the front(top) of the availableTags array
const index = availableTags.map((t) => t.Id).indexOf(tag.Id);
availableTags.splice(0, 0, availableTags.splice(index, 1)[0]);
})
.fail(logAjaxFail);
} else {
$j('.tag-dropdown-content').hide();
}
$j('#tagInput').val('');
$j('#tagInput').blur();
}
function removeTag(tag) {
$j.getJSON(thisUrl + '?request=event&action=removetag&tid=' + tag.Id + '&id=' + eventData.Id)
.done(function(data) {
$j('.tag-text').filter(function() {
return $j(this).text() === tag.Name;
}).parent().remove();
if (data.response > 0) {
getAvailableTags();
}
})
.fail(logAjaxFail);
}
function createTag(tagName) {
$j.getJSON(thisUrl + '?request=tags&action=createtag&tname=' + tagName)
.done(function(data) {
if (data.response.length > 0) {
var tag = data.response[0];
if (availableTags) {
availableTags.splice(0, 0, tag);
}
addTag(tag);
}
})
.fail(logAjaxFail);
}
function getAvailableTags() {
$j.getJSON(thisUrl + '?request=tags&action=getavailabletags')
.done(function(data) {
availableTags = data.response;
})
.fail(logAjaxFail);
}
function getSelectedTags() {
$j.getJSON(thisUrl + '?request=event&action=getselectedtags&id=' + eventData.Id)
.done(function(data) {
selectedTags = data.response;
selectedTags.forEach((tag) => formatTag(tag));
})
.fail(logAjaxFail);
}
var toggleZonesButton = document.getElementById('toggleZonesButton');
if (toggleZonesButton) toggleZonesButton.addEventListener('click', toggleZones);

View File

@ -25,6 +25,7 @@ var eventData = {
MonitorId: '<?php echo $Event->MonitorId() ?>',
MonitorName: '<?php echo validJsStr($monitor->Name()) ?>',
Cause: '<?php echo validHtmlStr($Event->Cause()) ?>',
<!-- Tags: '<?php echo validHtmlStr($Event->Tags()) ?>', -->
Notes: `<?php echo $Event->Notes()?>`,
Width: '<?php echo $Event->Width() ?>',
Height: '<?php echo $Event->Height() ?>',
@ -57,6 +58,8 @@ var eventDataStrings = {
MonitorId: '<?php echo translate('AttrMonitorId') ?>',
MonitorName: '<?php echo translate('AttrMonitorName') ?>',
Cause: '<?php echo translate('Cause') ?>',
<!-- Tags is not necessary since tags are displayed above -->
<!-- Tags: '<?php echo translate('Tags') ?>', -->
Notes: '<?php echo translate('Notes') ?>',
StartDateTimeFormatted: '<?php echo translate('AttrStartTime') ?>',
EndDateTimeFormatted: '<?php echo translate('AttrEndTime') ?>',

View File

@ -220,7 +220,7 @@ function initPage() {
// Hide these columns on first run when no cookie is saved
if (!getCookie('zmEventsTable.bs.table.columns')) {
table.bootstrapTable('hideColumn', 'Archived');
// table.bootstrapTable('hideColumn', 'Archived');
table.bootstrapTable('hideColumn', 'Emailed');
}
@ -420,6 +420,11 @@ function initPage() {
}
});
window.onpageshow = function(evt) {
console.log('Refreshing table');
table.bootstrapTable('refresh');
};
table.bootstrapTable('resetSearch');
// The table is initially given a hidden style, so now that we are done rendering, show it
table.show();
@ -429,10 +434,12 @@ function filterEvents() {
filterQuery = '';
$j('#fieldsTable input').each(function(index) {
const el = $j(this);
console.log('input index: '+index+' this: '+encodeURIComponent(el.val()));
filterQuery += '&'+encodeURIComponent(el.attr('name'))+'='+encodeURIComponent(el.val());
});
$j('#fieldsTable select').each(function(index) {
const el = $j(this);
console.log('select index: '+index+' this: '+encodeURIComponent(el.val()));
filterQuery += '&'+encodeURIComponent(el.attr('name'))+'='+encodeURIComponent(el.val());
});
console.log(filterQuery);

View File

@ -291,6 +291,13 @@ function parseRows(rows) {
});
var monitorVal = inputTds.eq(4).children().val();
inputTds.eq(4).html(monitorSelect).children().val(monitorVal).chosen({width: '101%'});
} else if ( attr == 'Tags' ) { // Tags
var tagSelect = $j('<select></select>').attr('name', queryPrefix + rowNum + '][val]').attr('id', queryPrefix + rowNum + '][val]');
for (var key in availableTags) {
tagSelect.append('<option value="' + key + '">' + escapeHTML(availableTags[key]) + '</option>');
};
var tagVal = inputTds.eq(4).children().val();
inputTds.eq(4).html(tagSelect).children().val(tagVal).chosen({width: '101%'});
} else if ( attr == 'ExistsInFileSystem' ) {
var select = $j('<select></select>').attr('name', queryPrefix + rowNum + '][val]').attr('id', queryPrefix + rowNum + '][val]');
for ( var booleanVal in booleanValues ) {
@ -400,6 +407,14 @@ function manageModalBtns(id) {
}
}
// function getAvailableTags() {
// $j.getJSON(thisUrl + '?request=tags&action=getavailabletags')
// .done(function(data) {
// return data.response;
// })
// .fail(logAjaxFail);
// }
function initPage() {
updateButtons($j('#executeButton')[0]);
$j('#Id').chosen();

View File

@ -9,6 +9,7 @@
global $servers;
global $storageareas;
global $monitors;
global $availableTags;
global $zones;
global $booleanValues;
global $filter;
@ -27,6 +28,7 @@ const states = <?php echo isset($states) ? json_encode($states) : '{}' ?>;
const servers = <?php echo isset($servers) ? json_encode($servers) : '{}' ?>;
const storageareas = <?php echo isset($storageareas) ? json_encode($storageareas) : '{}' ?>;
const monitors = <?php echo isset($monitors) ? json_encode($monitors) : '{}' ?>;
const availableTags = <?php echo isset($availableTags) ? json_encode($availableTags) : '[]' ?>;
const sorted_monitor_ids = <?php echo isset($monitors) ? json_encode(array_keys($monitors)) : '[]' ?>;
const zones = <?php echo isset($zones) ? json_encode($zones) : '{}' ?>;
const booleanValues = <?php echo json_encode($booleanValues) ?>;

View File

@ -130,9 +130,63 @@ $chart = array(
$monitors = array();
# The as E, and joining with Monitors is required for the filterSQL filters.
$rangeSql = 'SELECT min(E.StartDateTime) AS MinTime, max(E.EndDateTime) AS MaxTime FROM Events AS E INNER JOIN Monitors AS M ON (E.MonitorId = M.Id) WHERE NOT isnull(E.StartDateTime) AND NOT isnull(E.EndDateTime)';
$eventsSql = 'SELECT E.* FROM Events AS E INNER JOIN Monitors AS M ON (E.MonitorId = M.Id) WHERE NOT isnull(StartDateTime)';
$eventIdsSql = 'SELECT E.Id FROM Events AS E INNER JOIN Monitors AS M ON (E.MonitorId = M.Id) WHERE NOT isnull(StartDateTime)';
$rangeSql = '
SELECT
min(E.StartDateTime)
AS MinTime,
max(E.EndDateTime)
AS MaxTime,
GROUP_CONCAT(T.Name SEPARATOR ", ")
AS Tags
FROM Events
AS E
INNER JOIN Monitors
AS M
ON (E.MonitorId = M.Id)
LEFT JOIN Events_Tags
AS ET
ON E.Id = ET.EventId
LEFT JOIN Tags
AS T
ON T.Id = ET.TagId
WHERE NOT isnull(E.StartDateTime)
AND NOT isnull(E.EndDateTime)';
$eventsSql = '
SELECT
E.*,
GROUP_CONCAT(T.Name SEPARATOR ", ")
AS Tags
FROM Events
AS E
INNER JOIN Monitors
AS M
ON (E.MonitorId = M.Id)
LEFT JOIN Events_Tags
AS ET
ON E.Id = ET.EventId
LEFT JOIN Tags
AS T
ON T.Id = ET.TagId
WHERE NOT isnull(StartDateTime)';
$eventIdsSql = '
SELECT
E.Id,
GROUP_CONCAT(T.Name SEPARATOR ", ")
AS TagsFROM Events
AS E
INNER JOIN Monitors
AS M
ON (E.MonitorId = M.Id)
LEFT JOIN Events_Tags
AS ET
ON E.Id = ET.EventId
LEFT JOIN Tags
AS T
ON T.Id = ET.TagId
WHERE NOT isnull(StartDateTime)';
$eventsValues = array();
if ( count($user->unviewableMonitorIds()) ) {

View File

@ -392,6 +392,7 @@ if ( canView('Events') && ($monitor->Type() != 'WebSite') ) {
<th data-sortable="false" data-field="Id"><?php echo translate('Id') ?></th>
<th data-sortable="false" data-field="Name"><?php echo translate('Name') ?></th>
<th data-sortable="false" data-field="Cause"><?php echo translate('Cause') ?></th>
<th data-sortable="false" data-field="Tags"><?php echo translate('Tags') ?></th>
<th data-sortable="false" data-field="Notes"><?php echo translate('Notes') ?></th>
<th data-sortable="false" data-field="StartDateTime"><?php echo translate('AttrStartTime') ?></th>
<th data-sortable="false" data-field="EndDateTime"><?php echo translate('AttrEndTime') ?></th>