diff --git a/public/controllers/agent/agents-preview.js b/public/controllers/agent/agents-preview.js index a50b511b7e..681d427d88 100644 --- a/public/controllers/agent/agents-preview.js +++ b/public/controllers/agent/agents-preview.js @@ -72,12 +72,8 @@ export class AgentsPreviewController { this.load(); } - search(term) { - this.$scope.$broadcast('wazuhSearch', { term }); - } - - filter(filter) { - this.$scope.$broadcast('wazuhFilter', { filter }); + query(query, search) { + this.$scope.$broadcast('wazuhQuery', { query, search }); } showAgent(agent) { @@ -128,6 +124,19 @@ export class AgentsPreviewController { const [agentsUnique, agentsTop] = data; const unique = agentsUnique.data.result; + this.searchBarModel = { + 'status': ['Active', 'Disconnected', 'Never connected'], + 'group': unique.groups, + 'node_name': unique.nodes, + 'version': unique.versions, + 'os.platform': unique.osPlatforms.map(x => x.platform), + 'os.version': unique.osPlatforms.map(x => x.version), + 'os.name': unique.osPlatforms.map(x => x.name), + }; + this.searchBarModel['os.name'] = Array.from(new Set(this.searchBarModel['os.name'])); + this.searchBarModel['os.version'] = Array.from(new Set(this.searchBarModel['os.version'])); + this.searchBarModel['os.platform'] = Array.from(new Set(this.searchBarModel['os.platform'])); + this.groups = unique.groups; this.nodes = unique.nodes.map(item => ({ id: item })); this.versions = unique.versions.map(item => ({ id: item })); diff --git a/public/directives/index.js b/public/directives/index.js index 91d9f92c5c..9127222ba9 100644 --- a/public/directives/index.js +++ b/public/directives/index.js @@ -18,4 +18,6 @@ import './wz-welcome-card/wz-welcome-card'; import './wz-no-config/wz-no-config'; import './wz-config-item/wz-config-item'; import './wz-config-item/wz-config-item.less'; -import './wz-config-viewer/wz-config-viewer'; \ No newline at end of file +import './wz-tag-filter/wz-tag-filter'; +import './wz-tag-filter/wz-tag-filter.less'; +import './wz-config-viewer/wz-config-viewer'; diff --git a/public/directives/wz-table/lib/data.js b/public/directives/wz-table/lib/data.js index 30624ecf79..4a2b16e92c 100644 --- a/public/directives/wz-table/lib/data.js +++ b/public/directives/wz-table/lib/data.js @@ -64,10 +64,48 @@ export async function filterData( $scope.wazuh_table_loading = false; $scope.error = `Error filtering by ${ filter ? filter.value : 'undefined' - } - ${error.message || error}.`; + } - ${error.message || error}.`; errorHandler.handle( `Error filtering by ${ - filter ? filter.value : 'undefined' + filter ? filter.value : 'undefined' + }. ${error.message || error}`, + 'Data factory' + ); + } + if (!$scope.$$phase) $scope.$digest(); + return; +} + +export async function queryData( + query, + term, + instance, + wzTableFilter, + $scope, + fetch, + errorHandler +) { + try { + $scope.error = false; + $scope.wazuh_table_loading = true; + instance.removeFilters(); + if (term) { + instance.addFilter('search', term); + } + if (query) { + instance.addFilter('q', query); + } + wzTableFilter.set(instance.filters); + await fetch(); + $scope.wazuh_table_loading = false; + } catch (error) { + $scope.wazuh_table_loading = false; + $scope.error = `Query error ${ + query ? query.value : 'undefined' + } - ${error.message || error}.`; + errorHandler.handle( + `Query error ${ + query ? query.value : 'undefined' }. ${error.message || error}`, 'Data factory' ); diff --git a/public/directives/wz-table/lib/listeners.js b/public/directives/wz-table/lib/listeners.js index 658afa2017..1428895cce 100644 --- a/public/directives/wz-table/lib/listeners.js +++ b/public/directives/wz-table/lib/listeners.js @@ -20,6 +20,10 @@ export function wazuhFilter(parameters, filter) { return filter(parameters.filter); } +export function wazuhQuery(parameters, query) { + return query(parameters.query, parameters.search); +} + export function wazuhSearch(parameters, instance, search) { try { const matchesSpecificPath = diff --git a/public/directives/wz-table/wz-table-directive.js b/public/directives/wz-table/wz-table-directive.js index ff01e79943..2c62d7b4b3 100644 --- a/public/directives/wz-table/wz-table-directive.js +++ b/public/directives/wz-table/wz-table-directive.js @@ -19,14 +19,14 @@ import { parseValue } from './lib/parse-value'; import * as pagination from './lib/pagination'; import { sort } from './lib/sort'; import * as listeners from './lib/listeners'; -import { searchData, filterData } from './lib/data'; +import { searchData, filterData, queryData } from './lib/data'; import { clickAction } from './lib/click-action'; import { initTable } from './lib/init'; import { checkGap } from './lib/check-gap'; const app = uiModules.get('app/wazuh', []); -app.directive('wzTable', function() { +app.directive('wzTable', function () { return { restrict: 'E', scope: { @@ -135,6 +135,17 @@ app.directive('wzTable', function() { errorHandler ); + const query = async (query, search) => + queryData( + query, + search, + instance, + wzTableFilter, + $scope, + fetch, + errorHandler + ); + const realTimeFunction = async () => { try { $scope.error = false; @@ -184,7 +195,7 @@ app.directive('wzTable', function() { $scope.prevPage = () => pagination.prevPage($scope); $scope.nextPage = async currentPage => pagination.nextPage(currentPage, $scope, errorHandler, fetch); - $scope.setPage = function() { + $scope.setPage = function () { $scope.currentPage = this.n; $scope.nextPage(this.n); }; @@ -204,6 +215,10 @@ app.directive('wzTable', function() { listeners.wazuhSearch(parameters, instance, search) ); + $scope.$on('wazuhQuery', (event, parameters) => + listeners.wazuhQuery(parameters, query) + ); + $scope.$on('wazuhRemoveFilter', (event, parameters) => listeners.wazuhRemoveFilter(parameters, instance, wzTableFilter, init) ); diff --git a/public/directives/wz-tag-filter/wz-tag-filter.html b/public/directives/wz-tag-filter/wz-tag-filter.html new file mode 100644 index 0000000000..75e440ea64 --- /dev/null +++ b/public/directives/wz-tag-filter/wz-tag-filter.html @@ -0,0 +1,33 @@ +
+ +
+ + +
+

{{autocompleteContent.title}}

+
    +
  • + {{element}} +
  • +
+
+
+
\ No newline at end of file diff --git a/public/directives/wz-tag-filter/wz-tag-filter.js b/public/directives/wz-tag-filter/wz-tag-filter.js new file mode 100644 index 0000000000..4963826bd5 --- /dev/null +++ b/public/directives/wz-tag-filter/wz-tag-filter.js @@ -0,0 +1,257 @@ +/* + * Wazuh app - Wazuh search and filter by tags bar + * Copyright (C) 2018 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ + +import template from './wz-tag-filter.html'; +import { DataFactory } from '../../services/data-factory'; +import { uiModules } from 'ui/modules'; + +const app = uiModules.get('app/wazuh', []); + +app.directive('wzTagFilter', function () { + return { + restrict: 'E', + scope: { + path: '=path', + queryFn: '&', + fieldsModel: '=' + }, + controller( + $scope, + $timeout, + apiReq, + $document, + errorHandler + ) { + const instance = new DataFactory( + apiReq, + $scope.path, + {} + ); + + $scope.tagList = []; + $scope.groupedTagList = []; + $scope.newTag = ''; + $scope.isAutocomplete = false; + $scope.dataModel = []; + + $scope.addTag = (flag = false) => { + try { + const input = $document[0].getElementById('wz-search-filter-bar-input'); + input.blur(); + const term = $scope.newTag.split(':'); + const obj = { name: term[0], value: term[1] }; + const isFilter = obj.value; + if ((isFilter && Object.keys($scope.fieldsModel).indexOf(obj.name) === -1) || + (!isFilter && (!obj.name || /^\s*$/.test(obj.name)))) { + $scope.showAutocomplete(flag); + $scope.newTag = ''; + } else { + const tag = { + 'id': generateUID(), + 'key': obj.name, + 'value': obj, + 'type': isFilter ? 'filter' : 'search' + }; + const idxSearch = $scope.tagList.find(function (x) { return x.type === 'search' }); + if (!isFilter && idxSearch) { $scope.removeTag(idxSearch.id, false) }; + if (!$scope.tagList.find(function (x) { return x.type === 'filter' && x.key === tag.key && x.value.value === tag.value.value })) { + $scope.tagList.push(tag); + $scope.groupedTagList = groupBy($scope.tagList, 'key'); + buildQuery($scope.groupedTagList); + } + $scope.showAutocomplete(flag); + $scope.newTag = ''; + } + } catch (error) { + errorHandler.handle(error, 'Error adding filter'); + } + }; + + const buildQuery = groups => { + try { + let queryObj = { + 'query': '', + 'search': '' + }; + let first = true; + groups.forEach(function (group, idx1) { + const search = group.find(function (x) { return x.type === 'search' }); + if (search) { + queryObj.search = search.value.name; + } + else { + if (!first) { + queryObj.query += ';'; + } + const twoOrMoreElements = group.length > 1; + if (twoOrMoreElements) { + queryObj.query += '(' + } + group.filter(function (x) { return x.type === 'filter' }).forEach(function (tag, idx2) { + queryObj.query += tag.key + '=' + tag.value.value; + if (idx2 != group.length - 1) { + queryObj.query += ','; + } + }); + if (twoOrMoreElements) { + queryObj.query += ')' + } + first = false; + } + }); + $scope.queryFn({ 'q': queryObj.query, 'search': queryObj.search }); + } catch (error) { + errorHandler.handle(error, 'Error in query request'); + } + } + + const groupBy = (collection, property) => { + let i = 0, val, index, + values = [], result = []; + for (; i < collection.length; i++) { + val = collection[i][property]; + index = values.indexOf(val); + if (index > -1 && collection[i].type === 'filter') + result[index].push(collection[i]); + else { + values.push(val); + result.push([collection[i]]); + } + } + return result; + } + + $scope.addTagKey = (key) => { + $scope.newTag = key + ':'; + $scope.showAutocomplete(true); + }; + + $scope.addTagValue = (value) => { + $scope.newTag = $scope.newTag.substring(0, $scope.newTag.indexOf(':') + 1); + $scope.newTag += value; + $scope.addTag(); + }; + + $scope.removeTag = (id, deleteGroup) => { + if (deleteGroup) { + $scope.tagList = $scope.tagList.filter(function (x) { return x.key !== id }); + } else { + $scope.tagList.splice($scope.tagList.findIndex(function (x) { return x.id === id }), 1); + } + $scope.groupedTagList = groupBy($scope.tagList, 'key'); + buildQuery($scope.groupedTagList); + $scope.showAutocomplete(false); + }; + + $scope.showAutocomplete = (flag) => { + if (flag) { + $scope.getAutocompleteContent(); + } + $scope.isAutocomplete = flag; + indexAutocomplete(flag); + }; + + $scope.getAutocompleteContent = () => { + const term = $scope.newTag.split(':'); + const isKey = !term[1] && $scope.newTag.indexOf(':') === -1; + $scope.autocompleteContent = { 'title': '', 'isKey': isKey, 'list': [] }; + $scope.autocompleteContent.title = isKey ? 'Filter keys' : 'Values'; + if (isKey) { + for (let key in $scope.fieldsModel) { + if (key.toUpperCase().includes(term[0].toUpperCase())) { + $scope.autocompleteContent.list.push(key); + } + } + } else { + const model = $scope.dataModel.find(function (x) { return x.key === $scope.newTag.split(':')[0] }) + if (model) { + $scope.autocompleteContent.list = [...new Set(model.list.filter(function (x) { return x.toUpperCase().includes(term[1].toUpperCase()); }))]; + } + } + }; + + $scope.addSearchKey = (e) => { + if ($scope.autocompleteEnter) { + $scope.autocompleteEnter = false; + } + $scope.getAutocompleteContent(); + }; + + const indexAutocomplete = (flag = true) => { + $timeout(function () { + const bar = $document[0].getElementById('wz-search-filter-bar'); + const autocomplete = $document[0].getElementById('wz-search-filter-bar-autocomplete'); + const input = $document[0].getElementById('wz-search-filter-bar-input'); + autocomplete.style.left = input.offsetLeft - bar.scrollLeft + 'px'; + if (flag) { + input.focus(); + } + $('#wz-search-filter-bar-autocomplete-list li').removeClass('selected'); + }, 100); + } + + const load = async () => { + try { + const result = await instance.fetch(); + Object.keys($scope.fieldsModel).forEach(function (key) { + $scope.dataModel.push({ 'key': key, 'list': $scope.fieldsModel[key] }); + }); + return; + } catch (error) { + return Promise.reject(error); + } + }; + + const generateUID = () => { + // I generate the UID from two parts here + // to ensure the random number provide enough bits. + let firstPart = (Math.random() * 46656) | 0; + let secondPart = (Math.random() * 46656) | 0; + firstPart = ('000' + firstPart.toString(36)).slice(-3); + secondPart = ('000' + secondPart.toString(36)).slice(-3); + return firstPart + secondPart; + } + + $('#wz-search-filter-bar-input').bind('keydown', function (e) { + let $current = $('#wz-search-filter-bar-autocomplete-list li.selected'); + if ($current.length === 0 && (e.keyCode === 38 || e.keyCode === 40)) { + $('#wz-search-filter-bar-autocomplete-list li').first().addClass('selected'); + $current = $('#wz-search-filter-bar-autocomplete-list li.selected'); + } else { + let $next; + switch (e.keyCode) { + case 13: // enter + if ($current.text().trim() && !/^\s*$/.test($current.text().trim())) { + $scope.autocompleteEnter = true; + $scope.autocompleteContent.isKey ? $scope.addTagKey($current.text().trim()) : $scope.addTagValue($current.text().trim()); + } + break; + case 38: // if the UP key is pressed + $next = $current.prev(); + break; + case 40: // if the DOWN key is pressed + $next = $current.next(); + break; + } + if ($next && $next.length > 0 && (e.keyCode === 38 || e.keyCode === 40)) { + const input = $document[0].getElementById('wz-search-filter-bar-input'); + input.focus(); + $('#wz-search-filter-bar-autocomplete-list li').removeClass('selected'); + $next.addClass('selected'); + } + } + }); + load(); + }, + template + }; +}); diff --git a/public/directives/wz-tag-filter/wz-tag-filter.less b/public/directives/wz-tag-filter/wz-tag-filter.less new file mode 100644 index 0000000000..634f9ceea6 --- /dev/null +++ b/public/directives/wz-tag-filter/wz-tag-filter.less @@ -0,0 +1,119 @@ +/* + * Wazuh app - Search and filter by tags bar stylesheet + * Copyright (C) 2018 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ + +/* -------------------------------------------------------------------------- */ +/* ----------- Wazuh search and filter by tags bar stylesheet --------------- */ +/* -------------------------------------------------------------------------- */ + +#wz-search-filter-bar { + padding: 8px; + margin-top: 15px; + overflow: auto; +} + +#wz-search-filter-bar > .fa-search{ + padding: 8px 3px; + float: left; + opacity: .5; + width: calc(~'0% + 25px'); +} + +.wz-tags { + height: 30px; + width: calc(~'100% - 25px'); + display: flex; +} + +.wz-tags > ul{ + flex: 0 1 auto; + display: flex; +} + +.wz-tags > ul li{ + white-space: nowrap; + display: flex; +} + +.wz-tags > ul li .grouped{ + background: #d9d9d9; + border-radius: 3px; + padding: 3px; + height: 36px; + margin-top: -3px; + display: flex; +} + +.wz-tag-item { + color: white; + border-radius: 3px; + display: inline-block; + padding: 3px 5px; + background: #0079a5; + margin: 1px 3px; +} + +.wz-tag-item-connector { + font-size: 10px; + line-height: 30px; + color: #005571; + font-weight: bold; + padding: 0px 5px; +} + +.wz-tag-remove-button{ + color: #005571; +} + +.wz-tag-remove-button-group{ + padding: 4px 2px; + color: gray; +} + +.wz-search-filter-bar-input{ + border: none; + padding: 5px; + width: auto; + margin-left: 5px; + min-width: 225px; + flex: 1; +} + +.wz-search-filter-bar-autocomplete{ + min-width: 200px; + background: #000000d9; + position: absolute; + border: 1px solid black; + border-radius: 5px; + padding: 10px 0; + margin-top: 30px; +} + +.wz-search-filter-bar-autocomplete p b{ + color: white; + font-weight: 700; + padding: 10px; +} + +.wz-search-filter-bar-autocomplete ul li{ + color: white; + padding: 0 10px; +} + +.wz-search-filter-bar-autocomplete ul li span{ + padding-left: 5px; +} + +.wz-search-filter-bar-autocomplete ul li:hover, .wz-search-filter-bar-autocomplete ul li.selected{ + background: black; + color:#0079a5; + cursor: pointer; +} diff --git a/public/templates/agents-prev/agents-prev.html b/public/templates/agents-prev/agents-prev.html index 8a1d03fc79..07ddc836d3 100644 --- a/public/templates/agents-prev/agents-prev.html +++ b/public/templates/agents-prev/agents-prev.html @@ -34,85 +34,26 @@

Last registered agent

- -

{{ctrl.lastAgent.name}} (manager)

+ +

{{ctrl.lastAgent.name}} + (manager)

Higher activity

- +

{{ctrl.mostActiveAgent.name}}

-
-
- -
- -
- -
- -
- -
-
- -
-
- -
-
- -
- - -
+
- +
@@ -122,4 +63,4 @@ - + \ No newline at end of file