From ee925f1b97cfe6a83507ed025e8e7c845da55d91 Mon Sep 17 00:00:00 2001 From: Simpler1 Date: Sat, 3 Jun 2023 19:27:43 -0400 Subject: [PATCH] (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 --- db/zm_update-1.37.44.sql | 47 ++++ docs/userguide/index.rst | 1 + docs/userguide/tags.rst | 38 +++ scripts/ZoneMinder/lib/ZoneMinder/Filter.pm | 21 +- web/ajax/event.php | 60 ++++- web/ajax/events.php | 77 +++++- web/ajax/status.php | 42 ++- web/ajax/tags.php | 24 ++ web/ajax/watch.php | 25 +- web/includes/Event.php | 1 + web/includes/Event_Data.php | 1 + web/includes/Filter.php | 116 ++++++++- web/includes/FilterTerm.php | 12 +- web/includes/database.php | 4 +- web/skins/classic/css/base/skin.css | 82 ++++++ web/skins/classic/css/dark/skin.css | 16 ++ web/skins/classic/js/skin.js | 9 + web/skins/classic/views/event.php | 7 + web/skins/classic/views/events.php | 8 +- web/skins/classic/views/filter.php | 5 + web/skins/classic/views/js/event.js | 272 ++++++++++++++++++-- web/skins/classic/views/js/event.js.php | 3 + web/skins/classic/views/js/events.js | 9 +- web/skins/classic/views/js/filter.js | 15 ++ web/skins/classic/views/js/filter.js.php | 2 + web/skins/classic/views/timeline.php | 60 ++++- web/skins/classic/views/watch.php | 1 + 27 files changed, 900 insertions(+), 58 deletions(-) create mode 100644 db/zm_update-1.37.44.sql create mode 100644 docs/userguide/tags.rst create mode 100644 web/ajax/tags.php diff --git a/db/zm_update-1.37.44.sql b/db/zm_update-1.37.44.sql new file mode 100644 index 00000000000..f7daa193641 --- /dev/null +++ b/db/zm_update-1.37.44.sql @@ -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; diff --git a/docs/userguide/index.rst b/docs/userguide/index.rst index 905a87292cc..c7596e5aaff 100644 --- a/docs/userguide/index.rst +++ b/docs/userguide/index.rst @@ -11,6 +11,7 @@ User Guide viewmonitors filterevents viewevents + tags options cameracontrol mobile diff --git a/docs/userguide/tags.rst b/docs/userguide/tags.rst new file mode 100644 index 00000000000..9d9d91f0579 --- /dev/null +++ b/docs/userguide/tags.rst @@ -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. + + 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 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) diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Filter.pm b/scripts/ZoneMinder/lib/ZoneMinder/Filter.pm index 3498ddae28e..13a891d58cd 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Filter.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Filter.pm @@ -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'; diff --git a/web/ajax/event.php b/web/ajax/event.php index 1c2a003ea30..c24d4d2dd40 100644 --- a/web/ajax/event.php +++ b/web/ajax/event.php @@ -15,7 +15,26 @@ } 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 @@ 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') diff --git a/web/ajax/events.php b/web/ajax/events.php index 532e1221286..853640627b9 100644 --- a/web/ajax/events.php +++ b/web/ajax/events.php @@ -32,7 +32,10 @@ $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['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'; @@ -320,7 +381,7 @@ function queryRequest($filter, $search, $advsearch, $sort, $offset, $order, $lim } else { $data['total'] = $data['totalNotFiltered']; } -ZM\Debug("Done"); + ZM\Debug("Done"); return $data; } ?> diff --git a/web/ajax/status.php b/web/ajax/status.php index 85256070898..5fac2063aef 100644 --- a/web/ajax/status.php +++ b/web/ajax/status.php @@ -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,11 +508,29 @@ 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().')'; } - $sql .=' AND E.Id>'.$event['Id'] . ' ORDER BY '.$sortColumn.' '.($sortOrder=='ASC'?'ASC':'DESC'); + $sql .= ' AND E.Id>'.$event['Id'] . ' ORDER BY '.$sortColumn.' '.($sortOrder=='ASC'?'ASC':'DESC'); if ( $sortColumn != 'E.Id' ) { # When sorting by starttime, if we have two events with the same starttime (diffreent monitors) then we should sort secondly by Id $sql .= ', E.Id ASC'; diff --git a/web/ajax/tags.php b/web/ajax/tags.php new file mode 100644 index 00000000000..11a495d5e21 --- /dev/null +++ b/web/ajax/tags.php @@ -0,0 +1,24 @@ +$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']); +?> diff --git a/web/ajax/watch.php b/web/ajax/watch.php index 2bc6fb1d8b6..8a7a35d29bb 100644 --- a/web/ajax/watch.php +++ b/web/ajax/watch.php @@ -47,8 +47,29 @@ // $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)); diff --git a/web/includes/Event.php b/web/includes/Event.php index e4440e13aaa..55792d6ddb6 100644 --- a/web/includes/Event.php +++ b/web/includes/Event.php @@ -14,6 +14,7 @@ class Event extends ZM_Object { 'StorageId' => null, 'SecondaryStorageId' => null, 'Cause' => '', + 'Tags' => array(), 'StartDateTime' => null, 'EndDateTime' => null, 'Width' => null, diff --git a/web/includes/Event_Data.php b/web/includes/Event_Data.php index 3bd3503f2bb..dfd67a72ff9 100644 --- a/web/includes/Event_Data.php +++ b/web/includes/Event_Data.php @@ -11,6 +11,7 @@ class Event_Data extends ZM_Object { 'EventId' => null, 'FrameId' => null, 'MonitorId' => null, + 'Tags' => array(), 'TimeStamp' => 0, 'Data' => '', ); diff --git a/web/includes/Filter.php b/web/includes/Filter.php index be64b81dbd1..e5491938290 100644 --- a/web/includes/Filter.php +++ b/web/includes/Filter.php @@ -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 @@ public function sql() { } $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; } @@ -117,19 +126,25 @@ public function post_sql_conditions() { return $this->_post_sql_conditions; } - public function FilterTerms() { + public function FilterTerms() { + // echo '
Terms before: '; print_r($this->Terms); echo '
'; if (!isset($this->Terms)) { $this->Terms = array(); $_terms = $this->terms(); + // echo '
$_terms: '; print_r($_terms); echo '
'; 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 '
Terms after: '; print_r($this->Terms); echo '
'; return $this->Terms; } @@ -204,7 +219,7 @@ public static function find_one( $parameters = array(), $options = array() ) { return ZM_Object::_find_one(get_class(), $parameters, $options); } - public function terms( ) { + public function terms() { if ( func_num_args() ) { $Query = $this->Query(); $Query['terms'] = func_get_arg(0); @@ -260,6 +275,7 @@ public function limit( ) { $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 @@ function tree() { } } 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 @@ function tree() { case 'Group': $value = Group::get_group_sql($value); break; + case 'Tags': + // Error($term['attr']); case 'MonitorName': case 'Name': case 'Cause': @@ -719,10 +741,30 @@ function Events() { } $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 @@ public static function attrTypes() { 'StorageId' => translate('AttrStorageArea'), 'StorageServerId' => translate('AttrStorageServer'), 'SystemLoad' => translate('AttrSystemLoad'), + 'Tags' => translate('Tags'), 'TotScore' => translate('AttrTotalScore'), ); } @@ -896,6 +939,10 @@ public function widget() { } } } + // $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 @@ public function widget() { if ( $term['attr'] == 'Archived' ) { $html .= ''.translate('OpEq').''.PHP_EOL; $html .= ''.htmlSelect("filter[Query][terms][$i][val]", $archiveTypes, $term['val']).''.PHP_EOL; + + + + } else if ( $term['attr'] == 'Tags') { + // Error($term['attr']); + $html .= ''.htmlSelect("filter[Query][terms][$i][op]", $opTypes, $term['op']).''.PHP_EOL; + $options = ['class'=>'chosen', 'multiple'=>'multiple']; + $selected = explode(',', $term['val']); + if (count($selected) == 1 and !$selected[0]) { + $selected = null; + } + $html .= ''.htmlSelect("filter[Query][terms][$i][val]", $availableTags, $selected, $options).''.PHP_EOL; + // ZM\Debug('$availableTags: '.$availableTags); + // ZM\Debug('$selected: '.$selected); + + + } else if ( $term['attr'] == 'DateTime' || $term['attr'] == 'StartDateTime' || $term['attr'] == 'EndDateTime') { $html .= ''.htmlSelect("filter[Query][terms][$i][op]", $opTypes, $term['op']).''.PHP_EOL; $html .= ''.PHP_EOL; @@ -983,6 +1047,7 @@ public function widget() { '; } # end foreach term + // Error($html); return $html; } # end function widget() @@ -1006,6 +1071,10 @@ public function simple_widget() { 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 @@ public function simple_widget() { 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 '
selected: '; print_r($selected); echo '
'; + 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 '
availableTags: '; print_r($availableTags); echo '
'; + // echo '
selected: '; print_r($selected); echo '
'; + // echo '
options: '; print_r($options); echo '
'; + + $html .= ''.htmlSelect("filter[Query][terms][$i][val]", $availableTags, $selected, $options).''.PHP_EOL; + // $html .= ''.htmlSelect("filter[Query][terms][$i][val]", array_combine($availableTags,$availableTags), $term['val'], + // $options).''.PHP_EOL; + // $html .= ''.htmlSelect("filter[Query][terms][$i][val]", $availableTags, $term['val'], $options).''.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 .= 'Monitor selected: '; print_r($selected); echo ''; if (count($selected) == 1 and !$selected[0]) { $selected = null; } @@ -1145,8 +1248,9 @@ public function simple_widget() { $html .= ''; } # end foreach term $html .= ''; + // Error($html); return $html; - } # end function widget() + } # end function simple_widget() public function has_term($attr, $op=null) { foreach ($this->terms() as $term) { diff --git a/web/includes/FilterTerm.php b/web/includes/FilterTerm.php index e5b63b79842..6fee3bbb0ab 100644 --- a/web/includes/FilterTerm.php +++ b/web/includes/FilterTerm.php @@ -101,6 +101,7 @@ public function sql_values() { case 'DiskPercent': $value = ''; break; + case 'Tags': case 'MonitorName': case 'Name': case 'Cause': @@ -293,6 +294,8 @@ public function sql_attr() { case 'StateId': case 'Archived': return $this->tablename.'.'.$this->attr; + case 'Tags': + return 'T.Id'; default : return $this->tablename.'.'.$this->attr; } @@ -444,6 +447,11 @@ public function test($event=null) { 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 @@ public function is_pre_sql() { } public function is_post_sql() { - if ( $this->attr == 'ExistsInFileSystem' ) { + if ( $this->attr == 'ExistsInFileSystem' || $this->attr == 'Tags') { return true; } return false; @@ -515,6 +523,7 @@ public static function is_valid_attr($attr) { 'Notes', 'StateId', 'Archived', + 'Tags', # The following are for snapshots 'CreatedOn', 'Description' @@ -536,6 +545,7 @@ public function valid() { return false; break; case 'Archived' : + case 'Tags' : case 'Monitor' : case 'MonitorId' : case 'ServerId' : diff --git a/web/includes/database.php b/web/includes/database.php index e22d14ebf9d..8568ec4c0d4 100644 --- a/web/includes/database.php +++ b/web/includes/database.php @@ -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(); } diff --git a/web/skins/classic/css/base/skin.css b/web/skins/classic/css/base/skin.css index 407c3066ff4..a12a854bee4 100644 --- a/web/skins/classic/css/base/skin.css +++ b/web/skins/classic/css/base/skin.css @@ -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; +} diff --git a/web/skins/classic/css/dark/skin.css b/web/skins/classic/css/dark/skin.css index 63dea4651e5..b041a886acd 100644 --- a/web/skins/classic/css/dark/skin.css +++ b/web/skins/classic/css/dark/skin.css @@ -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; +} diff --git a/web/skins/classic/js/skin.js b/web/skins/classic/js/skin.js index 6f00dfb81e6..61a55c0e64c 100644 --- a/web/skins/classic/js/skin.js +++ b/web/skins/classic/js/skin.js @@ -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'); diff --git a/web/skins/classic/views/event.php b/web/skins/classic/views/event.php index 9d573821536..4541e080452 100644 --- a/web/skins/classic/views/event.php +++ b/web/skins/classic/views/event.php @@ -207,6 +207,13 @@ Id() ) { ?> +
+
+ + +
+
+
diff --git a/web/skins/classic/views/events.php b/web/skins/classic/views/events.php index ff5929ab93f..6c646344e3a 100644 --- a/web/skins/classic/views/events.php +++ b/web/skins/classic/views/events.php @@ -59,7 +59,12 @@ '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 @@ class="table-sm table-borderless table" + diff --git a/web/skins/classic/views/filter.php b/web/skins/classic/views/filter.php index 93e3c5d057b..4d88a233942 100644 --- a/web/skins/classic/views/filter.php +++ b/web/skins/classic/views/filter.php @@ -135,6 +135,10 @@ } } } +$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 @@ 'Id' => translate('AttrId'), 'Name' => translate('AttrName'), 'Cause' => translate('AttrCause'), + 'Tags' => translate('Tags'), 'DiskSpace' => translate('AttrDiskSpace'), 'Notes' => translate('AttrNotes'), 'MonitorName' => translate('AttrMonitorName'), diff --git a/web/skins/classic/views/js/event.js b/web/skins/classic/views/js/event.js index ea193831c3b..18201462d3c 100644 --- a/web/skins/classic/views/js/event.js +++ b/web/skins/classic/views/js/event.js @@ -26,41 +26,52 @@ 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(".modal").is(":visible") ) { - if (e.key === "Enter") { - if ( $j("#deleteConfirm").is(":visible") ) { - $j("#delConfirmBtn").click(); - } else if ( $j("#eventDetailModal").is(":visible") ) { - $j("#eventDetailSaveBtn").click(); - } else if ( $j("#eventRenamelModal").is(":visible") ) { - $j("#eventRenameBtn").click(); + if (!$j(".tag-input").is(":focus")) { + if ( $j(".modal").is(":visible") ) { + if (e.key === "Enter") { + if ( $j("#deleteConfirm").is(":visible") ) { + $j("#delConfirmBtn").click(); + } else if ( $j("#eventDetailModal").is(":visible") ) { + $j("#eventDetailSaveBtn").click(); + } else if ( $j("#eventRenamelModal").is(":visible") ) { + $j("#eventRenameBtn").click(); + } + } else if (e.key === "Escape") { + $j(".modal").modal('hide'); + } else { + console.log('Modal is visible: key not implemented: ', e.key, ' keyCode: ', e.keyCode); } - } else if (e.key === "Escape") { - $j(".modal").modal('hide'); } else { - console.log('Modal is visible: key not implemented: ', e.key, ' keyCode: ', e.keyCode); - } - } else { - if (e.key === "ArrowLeft" && !e.altKey) { - prevEvent(); - } else if (e.key === "ArrowRight" && !e.altKey) { - nextEvent(); - } else if (e.key === "Delete") { - if ( $j("#deleteBtn").is(":disabled") == false ) { - $j("#deleteBtn").click(); - } - } else if (e.keyCode === 32) { - // space bar for Play/Pause - if ( $j("#playBtn").is(":visible") ) { - playClicked(); + if (e.key === "ArrowLeft" && !e.altKey) { + prevEvent(); + } else if (e.key === "ArrowRight" && !e.altKey) { + nextEvent(); + } else if (e.key === "Delete") { + if ( $j("#deleteBtn").is(":disabled") == false ) { + $j("#deleteBtn").click(); + } + } else if (e.keyCode === 32) { + // space bar for Play/Pause + if ( $j("#playBtn").is(":visible") ) { + playClicked(); + } else { + pauseClicked(); + } + } else if (e.key === "ArrowDown") { + if (e.ctrlKey) { + addTag(availableTags[0]); + } else { + $j("#tagInput").focus(); + showDropdown(); + } } else { - pauseClicked(); + console.log('Modal is not visible: key not implemented: ', e.key, ' keyCode: ', e.keyCode); } - } else { - console.log('Modal is not visible: key not implemented: ', e.key, ' keyCode: ', e.keyCode); } } }); @@ -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('
', {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('
', {class: 'tag'}); + tagElement.data('tag', tag); + tagElement.append($j('', {class: 'tag-text', text: tagName})); + tagElement.append($j('', {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); diff --git a/web/skins/classic/views/js/event.js.php b/web/skins/classic/views/js/event.js.php index 26ad9235510..263252a023c 100644 --- a/web/skins/classic/views/js/event.js.php +++ b/web/skins/classic/views/js/event.js.php @@ -25,6 +25,7 @@ MonitorId: 'MonitorId() ?>', MonitorName: 'Name()) ?>', Cause: 'Cause()) ?>', + Notes: `Notes()?>`, Width: 'Width() ?>', Height: 'Height() ?>', @@ -57,6 +58,8 @@ MonitorId: '', MonitorName: '', Cause: '', + + Notes: '', StartDateTimeFormatted: '', EndDateTimeFormatted: '', diff --git a/web/skins/classic/views/js/events.js b/web/skins/classic/views/js/events.js index b778e39e44a..2d788c86c3b 100644 --- a/web/skins/classic/views/js/events.js +++ b/web/skins/classic/views/js/events.js @@ -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); diff --git a/web/skins/classic/views/js/filter.js b/web/skins/classic/views/js/filter.js index bab9956a9de..d33b36f3d22 100644 --- a/web/skins/classic/views/js/filter.js +++ b/web/skins/classic/views/js/filter.js @@ -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('').attr('name', queryPrefix + rowNum + '][val]').attr('id', queryPrefix + rowNum + '][val]'); + for (var key in availableTags) { + tagSelect.append(''); + }; + 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('').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(); diff --git a/web/skins/classic/views/js/filter.js.php b/web/skins/classic/views/js/filter.js.php index 7acf039fd91..7736f3e2c0a 100644 --- a/web/skins/classic/views/js/filter.js.php +++ b/web/skins/classic/views/js/filter.js.php @@ -9,6 +9,7 @@ global $servers; global $storageareas; global $monitors; + global $availableTags; global $zones; global $booleanValues; global $filter; @@ -27,6 +28,7 @@ const servers = ; const storageareas = ; const monitors = ; +const availableTags = ; const sorted_monitor_ids = ; const zones = ; const booleanValues = ; diff --git a/web/skins/classic/views/timeline.php b/web/skins/classic/views/timeline.php index 8e19e462175..09b67cbf7dc 100644 --- a/web/skins/classic/views/timeline.php +++ b/web/skins/classic/views/timeline.php @@ -130,9 +130,63 @@ $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()) ) { diff --git a/web/skins/classic/views/watch.php b/web/skins/classic/views/watch.php index 569d9e19b4e..8528469aae8 100644 --- a/web/skins/classic/views/watch.php +++ b/web/skins/classic/views/watch.php @@ -392,6 +392,7 @@ class="table-sm table-borderless" +