METRON-1123 Add group by option using faceted search capabilities of metron-rest-api (iraghumitra via james-sirota) closes apache/metron#768
Project: http://git-wip-us.apache.org/repos/asf/metron/repo Commit: http://git-wip-us.apache.org/repos/asf/metron/commit/942aaaf2 Tree: http://git-wip-us.apache.org/repos/asf/metron/tree/942aaaf2 Diff: http://git-wip-us.apache.org/repos/asf/metron/diff/942aaaf2 Branch: refs/heads/master Commit: 942aaaf2219d58af4d08744b49a90e7cf34b751c Parents: 39bb856 Author: iraghumitra <[email protected]> Authored: Sat Oct 14 07:44:27 2017 -0700 Committer: jsirota <[email protected]> Committed: Sat Oct 14 07:44:27 2017 -0700 ---------------------------------------------------------------------- metron-interface/metron-alerts/angular-cli.json | 1 + .../e2e/alert-details/alert-details.po.ts | 4 + .../alert-details-status.e2e-spec.ts | 47 ++- .../alert-status/alerts-list-status.e2e-spec.ts | 42 +++ .../e2e/alerts-list/alerts-list.e2e-spec.ts | 2 +- .../e2e/alerts-list/alerts-list.po.ts | 11 +- .../alerts-list/tree-view/tree-view.e2e-spec.ts | 236 +++++++++++++ .../e2e/alerts-list/tree-view/tree-view.po.ts | 162 +++++++++ .../metron-alerts/e2e/login/login.po.ts | 1 + .../metron-alerts/e2e/utils/e2e_util.ts | 7 +- metron-interface/metron-alerts/package.json | 1 + .../metron-alerts/protractor.conf.js | 1 + .../metron-alerts/src/_hexagon.scss | 91 +++++ .../metron-alerts/src/_variables.scss | 16 + .../alerts-list/alerts-list.component.html | 32 +- .../alerts-list/alerts-list.component.scss | 2 +- .../alerts/alerts-list/alerts-list.component.ts | 44 ++- .../alerts/alerts-list/alerts-list.module.ts | 9 +- .../src/app/alerts/alerts-list/query-builder.ts | 12 + .../table-view/table-view.component.html | 2 - .../table-view/table-view.component.scss | 2 +- .../alerts-list/tree-view/tree-group-data.ts | 65 ++++ .../tree-view/tree-view.component.html | 114 ++++++ .../tree-view/tree-view.component.scss | 153 ++++++++ .../tree-view/tree-view.component.spec.ts | 25 ++ .../tree-view/tree-view.component.ts | 352 +++++++++++++++++++ .../configure-rows.component.scss | 4 +- .../metron-alerts/src/app/app.component.ts | 21 +- .../metron-alerts/src/app/model/group-order.ts | 22 ++ .../src/app/model/group-request.ts | 26 ++ .../src/app/model/group-response.ts | 23 ++ .../metron-alerts/src/app/model/group-result.ts | 25 ++ .../metron-alerts/src/app/model/group.ts | 29 ++ .../src/app/model/search-request.ts | 8 +- .../src/app/model/search-response.ts | 4 + .../src/app/model/search-result-group.ts | 26 ++ .../src/app/service/search.service.ts | 13 +- .../alert-severity-hexagon.directive.spec.ts | 8 + .../alert-severity-hexagon.directive.ts | 45 +++ .../directives/alert-severity.directive.ts | 12 +- .../shared/group-by/group-by-component-data.ts | 28 ++ .../app/shared/group-by/group-by.component.html | 28 ++ .../app/shared/group-by/group-by.component.scss | 132 +++++++ .../app/shared/group-by/group-by.component.ts | 92 +++++ .../src/app/shared/group-by/group-by.module.ts | 35 ++ .../metron-sorter/metron-sorter.component.ts | 16 +- .../metron-table/metron-table.directive.ts | 4 +- .../src/app/shared/shared.module.ts | 7 +- .../metron-alerts/src/app/utils/constants.ts | 8 +- .../src/app/utils/elasticsearch-utils.ts | 8 +- metron-interface/metron-alerts/src/styles.scss | 12 +- 51 files changed, 2002 insertions(+), 68 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/metron/blob/942aaaf2/metron-interface/metron-alerts/angular-cli.json ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/angular-cli.json b/metron-interface/metron-alerts/angular-cli.json index 141f29b..833a778 100644 --- a/metron-interface/metron-alerts/angular-cli.json +++ b/metron-interface/metron-alerts/angular-cli.json @@ -20,6 +20,7 @@ "prefix": "app", "styles": [ "../node_modules/font-awesome/css/font-awesome.css", + "../node_modules/dragula/dist/dragula.css", "vendor.scss", "styles.scss" ], http://git-wip-us.apache.org/repos/asf/metron/blob/942aaaf2/metron-interface/metron-alerts/e2e/alert-details/alert-details.po.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/e2e/alert-details/alert-details.po.ts b/metron-interface/metron-alerts/e2e/alert-details/alert-details.po.ts index 39aea0b..ed71627 100644 --- a/metron-interface/metron-alerts/e2e/alert-details/alert-details.po.ts +++ b/metron-interface/metron-alerts/e2e/alert-details/alert-details.po.ts @@ -109,6 +109,10 @@ export class MetronAlertDetailsPage { return element.all(by.css('app-table-view .fa.fa-comments-o')).count(); } + getCommentIconCountInTreeView() { + return element.all(by.css('app-tree-view .fa.fa-comments-o')).count(); + } + waitForTextChange(element, previousText) { let EC = protractor.ExpectedConditions; return browser.wait(EC.not(EC.textToBePresentInElement(element, previousText))); http://git-wip-us.apache.org/repos/asf/metron/blob/942aaaf2/metron-interface/metron-alerts/e2e/alert-details/alert-status/alert-details-status.e2e-spec.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/e2e/alert-details/alert-status/alert-details-status.e2e-spec.ts b/metron-interface/metron-alerts/e2e/alert-details/alert-status/alert-details-status.e2e-spec.ts index 58d4892..4e25f82 100644 --- a/metron-interface/metron-alerts/e2e/alert-details/alert-status/alert-details-status.e2e-spec.ts +++ b/metron-interface/metron-alerts/e2e/alert-details/alert-status/alert-details-status.e2e-spec.ts @@ -21,16 +21,19 @@ import {customMatchers} from '../../matchers/custom-matchers'; import {LoginPage} from '../../login/login.po'; import {loadTestData, deleteTestData} from '../../utils/e2e_util'; import { MetronAlertsPage } from '../../alerts-list/alerts-list.po'; +import {TreeViewPage} from '../../alerts-list/tree-view/tree-view.po'; describe('metron-alerts alert status', function() { let page: MetronAlertDetailsPage; let listPage: MetronAlertsPage; + let treePage: TreeViewPage; let loginPage: LoginPage; beforeAll(() => { loadTestData(); loginPage = new LoginPage(); listPage = new MetronAlertsPage(); + treePage = new TreeViewPage(); loginPage.login(); }); @@ -65,7 +68,7 @@ describe('metron-alerts alert status', function() { page.clickNew(); }); - it('should add comments', () => { + it('should add comments for table view', () => { let comment1 = 'This is a sample comment'; let comment2 = 'This is a sample comment again'; let userNameAndTimestamp = '- admin - a few seconds ago'; @@ -97,4 +100,46 @@ describe('metron-alerts alert status', function() { page.closeDetailPane(); }); + it('should add comments for tree view', () => { + let comment1 = 'This is a sample comment'; + let comment2 = 'This is a sample comment again'; + let userNameAndTimestamp = '- admin - a few seconds ago'; + + treePage.selectGroup('source:type'); + treePage.expandDashGroup('alerts_ui_e2e'); + + treePage.clickOnRow('acf5a641-9cdb-d7ec-c309-6ea316e14fbe'); + page.clickCommentsInSideNav(); + page.addCommentAndSave(comment1, 0); + + expect(page.getCommentsText()).toEqual([comment1]); + expect(page.getCommentsUserNameAndTimeStamp()).toEqual([userNameAndTimestamp]); + expect(page.getCommentIconCountInTreeView()).toEqual(1); + + page.deleteComment(); + page.clickYesForConfirmation(); + expect(page.getCommentsText()).toEqual([]); + page.closeDetailPane(); + + treePage.unGroup(); + + treePage.selectGroup('source:type'); + treePage.selectGroup('enrichments:geo:ip_dst_addr:country'); + treePage.expandDashGroup('alerts_ui_e2e'); + treePage.expandSubGroup('alerts_ui_e2e', 'FR'); + + treePage.clickOnRow('7cd91565-132f-3340-db76-3ade5be54a6e'); + page.clickCommentsInSideNav(); + page.addCommentAndSave(comment2, 0); + + expect(page.getCommentsText()).toEqual([comment2]); + expect(page.getCommentsUserNameAndTimeStamp()).toEqual([userNameAndTimestamp]); + expect(page.getCommentIconCountInTreeView()).toEqual(1); + + page.deleteComment(); + page.clickYesForConfirmation(); + expect(page.getCommentsText()).toEqual([]); + page.closeDetailPane(); + }); + }); http://git-wip-us.apache.org/repos/asf/metron/blob/942aaaf2/metron-interface/metron-alerts/e2e/alerts-list/alert-status/alerts-list-status.e2e-spec.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/e2e/alerts-list/alert-status/alerts-list-status.e2e-spec.ts b/metron-interface/metron-alerts/e2e/alerts-list/alert-status/alerts-list-status.e2e-spec.ts index e793d6f..828f290 100644 --- a/metron-interface/metron-alerts/e2e/alerts-list/alert-status/alerts-list-status.e2e-spec.ts +++ b/metron-interface/metron-alerts/e2e/alerts-list/alert-status/alerts-list-status.e2e-spec.ts @@ -20,9 +20,11 @@ import { MetronAlertsPage } from '../alerts-list.po'; import {customMatchers} from '../../matchers/custom-matchers'; import {LoginPage} from '../../login/login.po'; import {loadTestData, deleteTestData} from '../../utils/e2e_util'; +import {TreeViewPage} from '../tree-view/tree-view.po'; describe('metron-alerts alert status', function() { let page: MetronAlertsPage; + let treePage: TreeViewPage; let loginPage: LoginPage; beforeAll(() => { @@ -38,6 +40,7 @@ describe('metron-alerts alert status', function() { beforeEach(() => { page = new MetronAlertsPage(); + treePage = new TreeViewPage(); jasmine.addMatchers(customMatchers); }); @@ -82,4 +85,43 @@ describe('metron-alerts alert status', function() { expect(page.getAlertStatus(11, 'NEW')).toEqual('RESOLVE'); }); + + it('should change alert status for multiple alerts to OPEN in tree view', () => { + treePage.selectGroup('source:type'); + treePage.selectGroup('enrichments:geo:ip_dst_addr:country'); + + treePage.expandDashGroup('alerts_ui_e2e'); + treePage.expandSubGroup('alerts_ui_e2e', 'US'); + treePage.expandSubGroup('alerts_ui_e2e', 'RU'); + treePage.expandSubGroup('alerts_ui_e2e', 'FR'); + + treePage.toggleAlertInTree(1); + treePage.toggleAlertInTree(2); + treePage.toggleAlertInTree(3); + page.clickActionDropdownOption('Open'); + expect(treePage.getAlertStatusForTreeView(1, 'NEW')).toEqual('OPEN'); + expect(treePage.getAlertStatusForTreeView(2, 'NEW')).toEqual('OPEN'); + expect(treePage.getAlertStatusForTreeView(3, 'NEW')).toEqual('OPEN'); + + treePage.toggleAlertInTree(4); + treePage.toggleAlertInTree(5); + page.clickActionDropdownOption('Dismiss'); + expect(treePage.getAlertStatusForTreeView(4, 'NEW')).toEqual('DISMISS'); + expect(treePage.getAlertStatusForTreeView(5, 'NEW')).toEqual('DISMISS'); + + treePage.toggleAlertInTree(8); + treePage.toggleAlertInTree(9); + page.clickActionDropdownOption('Escalate'); + expect(treePage.getAlertStatusForTreeView(8, 'NEW')).toEqual('ESCALATE'); + expect(treePage.getAlertStatusForTreeView(9, 'NEW')).toEqual('ESCALATE'); + + treePage.toggleAlertInTree(10); + treePage.toggleAlertInTree(11); + treePage.toggleAlertInTree(12); + page.clickActionDropdownOption('Resolve'); + expect(treePage.getAlertStatusForTreeView(10, 'NEW')).toEqual('RESOLVE'); + expect(treePage.getAlertStatusForTreeView(11, 'NEW')).toEqual('RESOLVE'); + expect(treePage.getAlertStatusForTreeView(12, 'NEW')).toEqual('RESOLVE'); + }); + }); http://git-wip-us.apache.org/repos/asf/metron/blob/942aaaf2/metron-interface/metron-alerts/e2e/alerts-list/alerts-list.e2e-spec.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/e2e/alerts-list/alerts-list.e2e-spec.ts b/metron-interface/metron-alerts/e2e/alerts-list/alerts-list.e2e-spec.ts index 43290fe..6b2ffd0 100644 --- a/metron-interface/metron-alerts/e2e/alerts-list/alerts-list.e2e-spec.ts +++ b/metron-interface/metron-alerts/e2e/alerts-list/alerts-list.e2e-spec.ts @@ -25,7 +25,7 @@ describe('metron-alerts App', function() { let page: MetronAlertsPage; let loginPage: LoginPage; let columnNames = [ 'Score', 'id', 'timestamp', 'source:type', 'ip_src_addr', 'enrichm...:country', - 'ip_dst_addr', 'host', 'alert_status', '', '', '' ]; + 'ip_dst_addr', 'host', 'alert_status', '', '']; let colNamesColumnConfig = [ 'score', 'id', 'timestamp', 'source:type', 'ip_src_addr', 'enrichments:geo:ip_dst_addr:country', 'ip_dst_addr', 'host', 'alert_status' ]; http://git-wip-us.apache.org/repos/asf/metron/blob/942aaaf2/metron-interface/metron-alerts/e2e/alerts-list/alerts-list.po.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/e2e/alerts-list/alerts-list.po.ts b/metron-interface/metron-alerts/e2e/alerts-list/alerts-list.po.ts index 39aefa7..7fee303 100644 --- a/metron-interface/metron-alerts/e2e/alerts-list/alerts-list.po.ts +++ b/metron-interface/metron-alerts/e2e/alerts-list/alerts-list.po.ts @@ -17,6 +17,7 @@ */ import {browser, element, by, protractor} from 'protractor'; +import {waitForElementVisibility, waitForElementPresence} from '../utils/e2e_util'; export class MetronAlertsPage { navigateTo() { @@ -69,12 +70,15 @@ export class MetronAlertsPage { } clickActionDropdown() { - return element(by.buttonText('ACTIONS')).click(); + let actionsDropDown = element(by.buttonText('ACTIONS')); + browser.actions().mouseMove(actionsDropDown).perform(); + return actionsDropDown.click(); } clickActionDropdownOption(option: string) { this.clickActionDropdown().then(() => { element(by.cssContainingText('.dropdown-menu span', option)).click(); + browser.sleep(2000); }); } @@ -115,7 +119,8 @@ export class MetronAlertsPage { } clickSettings() { - return element(by.css('.btn.settings')).click(); + let settingsIcon = element(by.css('.btn.settings')); + return waitForElementVisibility(settingsIcon).then(() => settingsIcon.click()); } getSettingsLabels() { @@ -165,7 +170,7 @@ export class MetronAlertsPage { } clickTableText(name: string) { - element.all(by.linkText(name)).get(0).click(); + waitForElementPresence(element.all(by.css('app-table-view tbody tr'))).then(() => element.all(by.linkText(name)).get(0).click()); } clickClearSearch() { http://git-wip-us.apache.org/repos/asf/metron/blob/942aaaf2/metron-interface/metron-alerts/e2e/alerts-list/tree-view/tree-view.e2e-spec.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/e2e/alerts-list/tree-view/tree-view.e2e-spec.ts b/metron-interface/metron-alerts/e2e/alerts-list/tree-view/tree-view.e2e-spec.ts new file mode 100644 index 0000000..caa3754 --- /dev/null +++ b/metron-interface/metron-alerts/e2e/alerts-list/tree-view/tree-view.e2e-spec.ts @@ -0,0 +1,236 @@ +/// <reference path="../../matchers/custom-matchers.d.ts"/> +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {customMatchers} from '../../matchers/custom-matchers'; +import {LoginPage} from '../../login/login.po'; +import {TreeViewPage} from './tree-view.po'; +import {loadTestData, deleteTestData} from '../../utils/e2e_util'; +import {MetronAlertsPage} from '../alerts-list.po'; + +describe('metron-alerts tree view', function () { + let page: TreeViewPage; + let listPage: MetronAlertsPage; + let loginPage: LoginPage; + + beforeAll(() => { + loadTestData(); + loginPage = new LoginPage(); + page = new TreeViewPage(); + listPage = new MetronAlertsPage(); + loginPage.login(); + page.navigateToAlertsList(); + }); + + afterAll(() => { + loginPage.logout(); + deleteTestData(); + }); + + beforeEach(() => { + jasmine.addMatchers(customMatchers); + }); + + it('should have all group by elements', () => { + let groupByItems = { + 'source:type': '1', + 'ip_dst_addr': '8', + 'host': '9', + 'enrichm...:country': '3', + 'ip_src_addr': '2' + }; + expect(page.getGroupByCount()).toEqualBcoz(Object.keys(groupByItems).length, '5 Group By Elements should be present'); + expect(page.getGroupByItemNames()).toEqualBcoz(Object.keys(groupByItems), 'Group By Elements names should be present'); + expect(page.getGroupByItemCounts()).toEqualBcoz(Object.keys(groupByItems).map(key => groupByItems[key]), + '5 Group By Elements values should be present'); + }); + + it('drag and drop should change group order', () => { + let before = { + 'firstDashRow': ['0', 'alerts_ui_e2e', 'ALERTS', '169'], + 'firstSubGroup': '0 US (22)', + 'secondSubGroup': '0 RU (44)', + 'thirdSubGroup': '0 FR (25)' + }; + + let after = { + 'firstDashRow': ['0', 'US', 'ALERTS', '22'], + 'secondDashRow': ['0', 'RU', 'ALERTS', '44'], + 'thirdDashRow': ['0', 'FR', 'ALERTS', '25'], + 'firstDashSubGroup': '0 alerts_ui_e2e (22)', + 'secondDashSubGroup': '0 alerts_ui_e2e (44)', + 'thirdDashSubGroup': '0 alerts_ui_e2e (25)' + }; + + page.selectGroup('source:type'); + page.selectGroup('enrichments:geo:ip_dst_addr:country'); + page.expandDashGroup('alerts_ui_e2e'); + expect(page.getDashGroupValues('alerts_ui_e2e')).toEqualBcoz(before.firstDashRow, 'First Dash Row should be correct'); + expect(page.getSubGroupValues('alerts_ui_e2e', 'US')).toEqualBcoz(before.firstSubGroup, + 'Dash Group Values should be correct for US'); + expect(page.getSubGroupValues('alerts_ui_e2e', 'RU')).toEqualBcoz(before.secondSubGroup, + 'Dash Group Values should be present for RU'); + expect(page.getSubGroupValues('alerts_ui_e2e', 'FR')).toEqualBcoz(before.thirdSubGroup, + 'Dash Group Values should be present for FR'); + + page.dragGroup('source:type', 'ip_src_addr'); + //page.selectGroup('source:type'); + expect(page.getDashGroupValues('US')).toEqualBcoz(after.firstDashRow, 'First Dash Row after ' + + 'reorder should be correct'); + expect(page.getDashGroupValues('RU')).toEqualBcoz(after.secondDashRow, 'Second Dash Row after ' + + 'reorder should be correct'); + expect(page.getDashGroupValues('FR')).toEqualBcoz(after.thirdDashRow, 'Third Dash Row after ' + + 'reorder should be correct'); + + page.expandDashGroup('US'); + expect(page.getSubGroupValues('US', 'alerts_ui_e2e')).toEqualBcoz(after.firstDashSubGroup, + 'First Dash Group Values should be present for alerts_ui_e2e'); + + page.expandDashGroup('RU'); + expect(page.getSubGroupValues('RU', 'alerts_ui_e2e')).toEqualBcoz(after.secondDashSubGroup, + 'Second Dash Group Values should be present for alerts_ui_e2e'); + + page.expandDashGroup('FR'); + expect(page.getSubGroupValues('FR', 'alerts_ui_e2e')).toEqualBcoz(after.thirdDashSubGroup, + 'Third Dash Group Values should be present for alerts_ui_e2e'); + + page.dragGroup('source:type', 'ip_dst_addr'); + page.unGroup(); + }); + + it('should have group details for single group by', () => { + let dashRowValues = ['0', 'alerts_ui_e2e', 'ALERTS', '169']; + let row1_page1 = ['-', 'dcda4423-7...0962fafc47', '2017-09-13 17:59:32', 'alerts_ui_e2e', + '192.168.138.158', 'US', '72.34.49.86', 'comarksecurity.com', 'NEW', '', '']; + let row1_page2 = ['-', '07b29c29-9...ff19eaa888', '2017-09-13 17:59:37', 'alerts_ui_e2e', + '192.168.138.158', 'FR', '62.75.195.236', '62.75.195.236', 'NEW', '', '']; + + page.selectGroup('source:type'); + expect(page.getActiveGroups()).toEqualBcoz(['source:type'], 'only source type group should be selected'); + expect(page.getDashGroupValues('alerts_ui_e2e')).toEqualBcoz(dashRowValues, 'Dash Group Values should be present'); + + page.expandDashGroup('alerts_ui_e2e'); + expect(page.getDashGroupTableValuesForRow('alerts_ui_e2e', 0)).toEqualBcoz(row1_page1, 'Dash Group Values should be present'); + + page.clickOnNextPage('alerts_ui_e2e'); + expect(page.getTableValuesByRowId('alerts_ui_e2e', 0, 'FR')).toEqualBcoz(row1_page2, 'Dash Group Values should be present'); + + page.unGroup(); + expect(page.getActiveGroups()).toEqualBcoz([], 'no groups should be selected'); + }); + + it('should have group details for multiple group by', () => { + + let dashRow_runLoveUs = { + 'dashRow': ['0', 'runlove.us', 'ALERTS', '13'], + 'firstSubGroup': '0 US (13)', + 'firstSubGroupIdCol': ['9a969c64-b...001cb011a3', 'a651f7c3-1...a97d4966c9', 'afc36901-3...d931231ab2', + 'd860ac35-1...f9e282d571', '04a5c3d0-9...af17c06fbc'] + }; + + let dashRow_62_75_195_236 = { + 'dashRow': ['0', '62.75.195.236', 'ALERTS', '18'], + 'firstSubGroup': '0 FR (18)', + 'firstSubGroupIdCol': ['07b29c29-9...ff19eaa888', '7cd91565-1...de5be54a6e', 'ca5bde58-a...f3a88d2df4', + '5d6faf83-8...b88a407647', 'e2883424-f...79bb8b0606'] + }; + + page.selectGroup('host'); + page.selectGroup('enrichments:geo:ip_dst_addr:country'); + expect(page.getActiveGroups()).toEqualBcoz(['host', 'enrichments:geo:ip_dst_addr:country'], 'two groups should be selected'); + + expect(page.getDashGroupValues('runlove.us')).toEqualBcoz(dashRow_runLoveUs.dashRow, + 'Dash Group Values should be present for runlove.us'); + page.expandDashGroup('runlove.us'); + expect(page.getSubGroupValues('runlove.us', 'US')).toEqualBcoz(dashRow_runLoveUs.firstSubGroup, + 'Dash Group Values should be present for runlove.us'); + page.expandSubGroup('runlove.us', 'US'); + expect(page.getCellValuesFromTable('runlove.us', 'id', '04a5c3d0-9...af17c06fbc')).toEqual(dashRow_runLoveUs.firstSubGroupIdCol, + 'id should not be sorted'); + + expect(page.getDashGroupValues('62.75.195.236')).toEqualBcoz(dashRow_62_75_195_236.dashRow, 'Dash Group Values should be present'); + page.expandDashGroup('62.75.195.236'); + expect(page.getSubGroupValues('62.75.195.236', 'FR')).toEqualBcoz(dashRow_62_75_195_236.firstSubGroup, + 'Dash Group Values should be present for 62.75.195.236'); + page.expandSubGroup('62.75.195.236', 'FR'); + expect(page.getCellValuesFromTable('62.75.195.236', 'id', 'e2883424-f...79bb8b0606')).toEqual(dashRow_62_75_195_236.firstSubGroupIdCol, + 'id should not be sorted'); + + page.unGroup(); + expect(page.getActiveGroups()).toEqualBcoz([], 'no groups should be selected'); + }); + + + it('should have sort working for group details for multiple sub groups', () => { + + let usIDCol = ['dcda4423-7...0962fafc47', '9a969c64-b...001cb011a3', 'a651f7c3-1...a97d4966c9', + 'afc36901-3...d931231ab2', 'd860ac35-1...f9e282d571']; + let ruIDCol = ['350c0e9f-a...3cbe5b29d2', '9b47e24a-e...2ca6627943', '4cac5e2c-3...3deb1ebcc6', + 'eb54c3fa-c...e02719c3b0', 'cace11d0-c...b1bd7b9499']; + let frIDCol = ['07b29c29-9...ff19eaa888', '7cd91565-1...de5be54a6e', 'ca5bde58-a...f3a88d2df4', + '5d6faf83-8...b88a407647', 'e2883424-f...79bb8b0606']; + + let usSortedIDCol = ['04a5c3d0-9...af17c06fbc', '06e70f55-4...f486927126', '105529cb-2...61b58237cc', + '4c732cb0-0...6a93129aba', '500eb5e2-6...37b0f98772']; + let ruSortedIDCol = ['001b5451-6...38ec4221ee', '00814048-d...c9e6f27800', '0454b31e-e...0a711a36e7', + '09552ace-9...e146579030', '0e99ba49-4...456c107bc9']; + let frSortedIDCol = ['07b29c29-9...ff19eaa888', '2681ed49-b...c33a80d429', '29ffaeb4-e...36822e5f81', + '2cc174d7-c...8073777309', '436b9ecf-b...5f1ece4c4d']; + + page.selectGroup('source:type'); + page.selectGroup('enrichments:geo:ip_dst_addr:country'); + + page.expandDashGroup('alerts_ui_e2e'); + page.expandSubGroup('alerts_ui_e2e', 'US'); + page.expandSubGroup('alerts_ui_e2e', 'RU'); + page.expandSubGroup('alerts_ui_e2e', 'FR'); + + let unsortedIds = [...usIDCol, ...ruIDCol, ...frIDCol]; + let sortedIds = [...usSortedIDCol, ...ruSortedIDCol, ...frSortedIDCol]; + + expect(page.getCellValuesFromTable('alerts_ui_e2e', 'id', 'e2883424-f...79bb8b0606')).toEqual(unsortedIds, 'id should not be sorted'); + + page.sortSubGroup('alerts_ui_e2e', 'id'); + + expect(page.getCellValuesFromTable('alerts_ui_e2e', 'id', '436b9ecf-b...5f1ece4c4d')).toEqual(sortedIds, 'id should be sorted'); + + page.unGroup(); + expect(page.getActiveGroups()).toEqualBcoz([], 'no groups should be selected'); + }); + + it('should have search working for group details for multiple sub groups', () => { + + page.selectGroup('source:type'); + page.selectGroup('enrichments:geo:ip_dst_addr:country'); + + page.expandDashGroup('alerts_ui_e2e'); + expect(page.getNumOfSubGroups('alerts_ui_e2e')).toEqual(3, 'three sub groups should be present'); + + listPage.setSearchText('enrichments:geo:ip_dst_addr:country:FR'); + + expect(page.getNumOfSubGroups('alerts_ui_e2e')).toEqual(1, 'one sub groups should be present'); + page.expandSubGroup('alerts_ui_e2e', 'FR'); + + let expected = ['FR', 'FR', 'FR', 'FR', 'FR']; + expect(page.getCellValuesFromTable('alerts_ui_e2e', 'enrichments:geo:ip_dst_addr:country', 'FR')).toEqual(expected, + 'id should be sorted'); + + page.unGroup(); + expect(page.getActiveGroups()).toEqualBcoz([], 'no groups should be selected'); + }); + +}); http://git-wip-us.apache.org/repos/asf/metron/blob/942aaaf2/metron-interface/metron-alerts/e2e/alerts-list/tree-view/tree-view.po.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/e2e/alerts-list/tree-view/tree-view.po.ts b/metron-interface/metron-alerts/e2e/alerts-list/tree-view/tree-view.po.ts new file mode 100644 index 0000000..b8472df --- /dev/null +++ b/metron-interface/metron-alerts/e2e/alerts-list/tree-view/tree-view.po.ts @@ -0,0 +1,162 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {browser, element, by, protractor} from 'protractor'; +import {waitForElementPresence, waitForTextChange} from '../../utils/e2e_util'; + +export class TreeViewPage { + navigateToAlertsList() { + browser.waitForAngularEnabled(false); + return browser.get('/alerts-list'); + } + + clickOnRow(id: string) { + let idElement = element(by.css('a[title="' + id +'"]')); + waitForElementPresence(idElement) + .then(() => browser.actions().mouseMove(idElement).perform()) + .then(() => idElement.element(by.xpath('../..')).all(by.css('td')).get(9).click()); + browser.sleep(2000); + } + + getActiveGroups() { + return element.all(by.css('app-group-by .group-by-items.active')).getAttribute('data-name'); + } + + getGroupByCount() { + return waitForElementPresence(element.all(by.css('app-group-by .group-by-items'))).then(() => { + return element.all(by.css('app-group-by .group-by-items')).count(); + }); + } + + getGroupByItemNames() { + return element.all(by.css('app-group-by .group-by-items .name')).getText(); + } + + getGroupByItemCounts() { + return element.all(by.css('app-group-by .group-by-items .count')).getText(); + } + + getSubGroupValues(name: string, rowName: string) { + return element(by.css('[data-name="' + name + '"] table tbody tr[data-name="' + rowName + '"]')).getText(); + } + + selectGroup(name: string) { + return element(by.css('app-group-by div[data-name="' + name + '"]')).click(); + } + + dragGroup(from: string, to: string) { + browser.actions().dragAndDrop( + element(by.css('app-group-by div[data-name="' + from + '"]')), + element(by.css('app-group-by div[data-name="' + to + '"]')) + ).perform(); + } + + getDashGroupValues(name: string) { + return waitForElementPresence(element(by.css('[data-name="' + name + '"] .card-header span'))).then(() => { + return element.all(by.css('[data-name="' + name + '"] .card-header span')).getText(); + }); + } + + expandDashGroup(name: string) { + waitForElementPresence( element(by.css('[data-name="' + name + '"] .card-header'))).then(() => { + this.scrollToDashRow(name); + element(by.css('[data-name="' + name + '"] .card-header i')).click(); + browser.sleep(2000); + }); + } + + expandSubGroup(groupName: string, rowName: string) { + browser.actions().mouseMove(element(by.css('[data-name="' + groupName + '"] tr[data-name="' + rowName + '"]'))).perform(); + return element(by.css('[data-name="' + groupName + '"] tr[data-name="' + rowName + '"]')).click(); + } + + getDashGroupTableValuesForRow(name: string, rowId: number) { + this.scrollToDashRow(name); + return waitForElementPresence(element(by.css('[data-name="' + name + '"] table tbody tr'))).then(() => { + return element.all(by.css('[data-name="' + name + '"] table tbody tr')).get(rowId).all(by.css('td')).getText(); + }); + } + + getTableValuesByRowId(name: string, rowId: number, waitForAnchor: string) { + return waitForElementPresence(element(by. cssContainingText('[data-name="' + name + '"] a', waitForAnchor))).then(() => { + return element.all(by.css('[data-name="' + name + '"] table tbody tr')).get(rowId).all(by.css('td')).getText(); + }); + } + + getTableValuesForRow(name: string, rowName: string, waitForAnchor: string) { + return waitForElementPresence(element(by. cssContainingText('[data-name="' + name + '"] a', waitForAnchor))).then(() => { + return element.all(by.css('[data-name="' + name + '"] tr[data-name="' + rowName + '"]')).all(by.css('td')).getText(); + }); + } + + scrollToDashRow(name: string) { + let scrollToEle = element(by.css('[data-name="' + name + '"] .card-header')); + waitForElementPresence(scrollToEle).then(() => { + return browser.actions().mouseMove(scrollToEle).perform(); + }); + } + + clickOnNextPage(name: string) { + return element(by.css('[data-name="' + name + '"] i.fa-chevron-right')).click(); + } + + unGroup() { + return element(by.css('app-group-by .ungroup-button')).click(); + } + + getIdOfAllExpandedRows() { + return element.all(by.css('[data-name="' + name + '"] table tbody tr')).then(row => { + browser.pause(); + }); + } + + getNumOfSubGroups(groupName: string) { + return element.all(by.css('[data-name="' + groupName + '"] table tbody tr')).count(); + } + + getCellValuesFromTable(groupName: string, cellName: string, waitForAnchor: string) { + return waitForElementPresence(element(by. cssContainingText('[data-name="' + cellName + '"] a', waitForAnchor))).then(() => { + return element.all(by.css('[data-name="' + groupName + '"] table tbody [data-name="' + cellName + '"]')).map(element => { + browser.actions().mouseMove(element).perform(); + return (element.getText()); + }); + }); + } + + sortSubGroup(groupName: string, colName: string) { + return element.all(by.css('[data-name="' + groupName + '"] metron-config-sorter[title="' + colName + '"]')).click(); + } + + toggleAlertInTree(index: number) { + let selector = by.css('app-tree-view tbody tr'); + let checkbox = element.all(selector).get(index).element(by.css('label')); + waitForElementPresence(checkbox).then(() => { + browser.actions().mouseMove(checkbox).perform().then(() => { + checkbox.click(); + }); + }); + } + + getAlertStatusForTreeView(rowIndex: number, previousText) { + let row = element.all(by.css('app-tree-view tbody tr')).get(rowIndex); + let column = row.all(by.css('td a')).get(8); + return waitForTextChange(column, previousText).then(() => { + return column.getText(); + }); + } +} http://git-wip-us.apache.org/repos/asf/metron/blob/942aaaf2/metron-interface/metron-alerts/e2e/login/login.po.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/e2e/login/login.po.ts b/metron-interface/metron-alerts/e2e/login/login.po.ts index 2f0f81d..8d37800 100644 --- a/metron-interface/metron-alerts/e2e/login/login.po.ts +++ b/metron-interface/metron-alerts/e2e/login/login.po.ts @@ -51,6 +51,7 @@ export class LoginPage { browser.waitForAngularEnabled(false); let errElement = element(by.css('.login-failed-msg')); return waitForElementVisibility(errElement).then(() => { + browser.sleep(1000); return errElement.getText().then((message) => { return message.replace(/\n/, '').replace(/LOG\ IN$/, ''); }); http://git-wip-us.apache.org/repos/asf/metron/blob/942aaaf2/metron-interface/metron-alerts/e2e/utils/e2e_util.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/e2e/utils/e2e_util.ts b/metron-interface/metron-alerts/e2e/utils/e2e_util.ts index 341e668..47f01e2 100644 --- a/metron-interface/metron-alerts/e2e/utils/e2e_util.ts +++ b/metron-interface/metron-alerts/e2e/utils/e2e_util.ts @@ -17,7 +17,12 @@ export function waitForURL(url: string) { export function waitForText(element, text) { let EC = protractor.ExpectedConditions; - return browser.wait(EC.textToBePresentInElement(element, text)); + return browser.wait(EC.textToBePresentInElementValue(element, text)); +} + +export function waitForTextChange(element, previousText) { + let EC = protractor.ExpectedConditions; + return browser.wait(EC.not(EC.textToBePresentInElement(element, previousText))); } export function waitForElementInVisibility (_element ) { http://git-wip-us.apache.org/repos/asf/metron/blob/942aaaf2/metron-interface/metron-alerts/package.json ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/package.json b/metron-interface/metron-alerts/package.json index 6ff3c3c..1322a65 100644 --- a/metron-interface/metron-alerts/package.json +++ b/metron-interface/metron-alerts/package.json @@ -25,6 +25,7 @@ "bootstrap": "4.0.0-alpha.6", "core-js": "^2.4.1", "font-awesome": "^4.7.0", + "ng2-dragula": "^1.5.0", "moment": "^2.18.1", "rxjs": "^5.1.0", "web-animations-js": "^2.2.2", http://git-wip-us.apache.org/repos/asf/metron/blob/942aaaf2/metron-interface/metron-alerts/protractor.conf.js ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/protractor.conf.js b/metron-interface/metron-alerts/protractor.conf.js index fe0fec0..4fc25be 100644 --- a/metron-interface/metron-alerts/protractor.conf.js +++ b/metron-interface/metron-alerts/protractor.conf.js @@ -29,6 +29,7 @@ exports.config = { './e2e/alerts-list/alerts-list.e2e-spec.ts', './e2e/alerts-list/configure-table/configure-table.e2e-spec.ts', './e2e/alerts-list/save-search/save-search.e2e-spec.ts', + './e2e/alerts-list/tree-view/tree-view.e2e-spec.ts', './e2e/alerts-list/alert-filters/alert-filters.e2e-spec.ts', './e2e/alerts-list/alert-status/alerts-list-status.e2e-spec.ts', './e2e/alert-details/alert-status/alert-details-status.e2e-spec.ts' http://git-wip-us.apache.org/repos/asf/metron/blob/942aaaf2/metron-interface/metron-alerts/src/_hexagon.scss ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/_hexagon.scss b/metron-interface/metron-alerts/src/_hexagon.scss new file mode 100644 index 0000000..667008a --- /dev/null +++ b/metron-interface/metron-alerts/src/_hexagon.scss @@ -0,0 +1,91 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "variables"; + +.hexagon { + position: absolute; + left: -15px; + top: 17px; + width: 30px; + height: 17.321px; + background-color: #64C7CC; + margin: 8.660px 0; + + &:before, + &:after { + content: ""; + position: absolute; + width: 0; + border-left: 15px solid transparent; + border-right: 15px solid transparent; + } + + &:before { + bottom: 100%; + border-bottom: 8.660px solid #64C7CC; + } + + &:after { + top: 100%; + width: 0; + border-top: 8.660px solid #64C7CC; + } + + &.error { + background-color: $errors-red; + &:before { + border-bottom: 8.660px solid $errors-red; + } + + &:after { + border-top: 8.660px solid $errors-red; + } + } + + &.warning { + background-color: $warning-yellow; + &:before { + border-bottom: 8.660px solid $warning-yellow; + } + + &:after { + border-top: 8.660px solid $warning-yellow; + } + } + + &.info { + background-color: $info-yellow; + &:before { + border-bottom: 8.660px solid $info-yellow; + } + + &:after { + border-top: 8.660px solid $info-yellow; + } + } + + span { + line-height: 1.7; + position: absolute; + font-size: 12px; + text-align: center; + width: 100%; + color: $white; + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/metron/blob/942aaaf2/metron-interface/metron-alerts/src/_variables.scss ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/_variables.scss b/metron-interface/metron-alerts/src/_variables.scss index e4055ce..44ed9f6 100644 --- a/metron-interface/metron-alerts/src/_variables.scss +++ b/metron-interface/metron-alerts/src/_variables.scss @@ -32,6 +32,10 @@ $list-group-border-color: #404040; $list-group-active-bg: #28A9E2; $list-group-item-padding-y: 4px; $list-group-item-padding-x: 10px; +$tooltip-bg: #494411; +$tooltip-padding-y: 2px; +$tooltip-padding-x: 5px; +$tooltip-arrow-width: 0px; //Metron variables $mine-shaft: #2E2E2E; @@ -40,26 +44,36 @@ $mine-shaft-2: #333333; $mine-shaft-3: #262626; $mine-shaft-4: #383838; $mine-shaft-5: #3D3D3D; +$mine-shaft-6: #252525; +$mine-shaft-7: #2C2C2C; +$mine-shaft-8: #353535; +$mine-shaft-9: #2B2B2B; +$mine-shaft-10: #303030; $dove-grey: #737373; $tundora: #4D4D4D; $tundora-1: #404040; +$tundora-2: #4C4C4C; $curious-blue: #27AAE1; $blue-chill: #0F6F9E; $piction-blue: #32ABDF; +$picton-blue-1: #53C3EA; $dove-grey: #6B6B6B; $dove-grey-1: #676767; +$dove-grey-2: #6D6D6D; $checkbox-checked-color: #32abe2; $edit-panel-background: #083b44; $errors-red: #D60A15; $warning-yellow: #D6711D; $info-yellow: #AC9B5A; $eden: #0C3B43; +$eden-1: #0F4450; $all-ports: #006EA0; $blue-mine: #1B596C; $gothic: #689AA9; $tiber: #0B363E; $silver: #BDBDBD; $silver-1: #B8B8B8; +$silver-2: #C8C8C8; $breaker-bay: #669AAA; $gray: #909090; $silver-chalice: #B2B2B2; @@ -72,6 +86,8 @@ $eastern-blue: #1F91BE; $mantis: #80BF4D; $sky-blue: #75D2ED; $outer-space: #2E3A3F; +$white: #FFFFFF; +$iron: #D1D3D4; $rolling-stone: #808285; $nile-blue: #18404E; $apple-blossom: #A94442; http://git-wip-us.apache.org/repos/asf/metron/blob/942aaaf2/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.component.html ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.component.html b/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.component.html index 1183223..bcecef3 100644 --- a/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.component.html +++ b/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.component.html @@ -40,6 +40,9 @@ </div> <div class="col-md-3 px-0"> <div class="pull-right" style="position: relative; display: block;"> + <div class="btn cog"> + <i class="fa fa-cog configure-table-icon" (click)="showConfigureTable()"></i> + </div> <div class="btn settings"> <i #settingsIcon class="fa fa-sliders" aria-hidden="true"></i> </div> @@ -64,25 +67,36 @@ <div class="container-fluid no-gutters"> <div class="row"> - <div class="px-0" style="width: 200px;max-width: 200px;"> - <app-alert-filters [facets]="searchResponse.facetCounts" (facetFilterChange)="onAddFacetFilter($event)"> </app-alert-filters> + <div class="px-0" style="width: 200px;max-width: 200px;"> + <app-alert-filters [facets]="searchResponse.facetCounts" (facetFilterChange)="onAddFacetFilter($event)"> </app-alert-filters> + </div> + <div class="col px-0 ml-4"> + <div class="col-sm-12 pl-0 pb-3"> + <app-group-by [facets]="searchResponse.facetCounts" (groupsChange)="onGroupsChange($event)"> </app-group-by> </div> - <div class="col"> + <div class="col-sm-12 px-0"> <app-table-view #dataViewComponent - [alerts]="alerts" + [alerts]="alerts" *ngIf="queryBuilder.groupRequest.groups.length === 0" [queryBuilder]="queryBuilder" [pagination]="pagination" [alertsColumnsToDisplay]="alertsColumnsToDisplay" - [(selectedAlerts)]="selectedAlerts" + [selectedAlerts]="selectedAlerts" (onResize)="onResize()" (onAddFilter)="onAddFilter($event)" (onRefreshData)="onRefreshData($event)" (onShowDetails)="showDetails($event)" - (onShowConfigureTable)="showConfigureTable()" - (onSelectedAlertsChange)="onSelectedAlertsChange($event)"> - - </app-table-view> + (onSelectedAlertsChange)="onSelectedAlertsChange($event)"></app-table-view> + <app-tree-view #dataViewComponent *ngIf="queryBuilder.groupRequest.groups.length !== 0" + [alerts]="alerts" + [queryBuilder]="queryBuilder" + [alertsColumnsToDisplay]="alertsColumnsToDisplay" + [selectedAlerts]="selectedAlerts" + (onResize)="onResize()" + (onAddFilter)="onAddFilter($event)" + (onShowDetails)="showDetails($event)" + (onSelectedAlertsChange)="onSelectedAlertsChange($event)"></app-tree-view> </div> </div> + </div> </div> http://git-wip-us.apache.org/repos/asf/metron/blob/942aaaf2/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.component.scss ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.component.scss b/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.component.scss index 6a26d3c..a803df0 100644 --- a/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.component.scss +++ b/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.component.scss @@ -205,7 +205,7 @@ $searchbox-height: 42px; } } -.settings { +.settings, .cog { height: 38px; padding: 0px; line-height: 40px; http://git-wip-us.apache.org/repos/asf/metron/blob/942aaaf2/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.component.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.component.ts b/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.component.ts index 46b7796..06d3fb2 100644 --- a/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.component.ts +++ b/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.component.ts @@ -38,7 +38,6 @@ import {ElasticsearchUtils} from '../../utils/elasticsearch-utils'; import {TableViewComponent} from './table-view/table-view.component'; import {Filter} from '../../model/filter'; import {Pagination} from '../../model/pagination'; -import {environment} from '../../../environments/environment'; import {PatchRequest} from '../../model/patch-request'; @Component({ @@ -60,7 +59,6 @@ export class AlertsListComponent implements OnInit, OnDestroy { pauseRefresh = false; lastPauseRefreshValue = false; threatScoreFieldName = 'threat:triage:score'; - indices: string[]; @ViewChild('table') table: ElementRef; @ViewChild('dataViewComponent') dataViewComponent: TableViewComponent; @@ -85,9 +83,6 @@ export class AlertsListComponent implements OnInit, OnDestroy { this.restoreRefreshState(); } }); - if (environment.indices) { - this.indices = environment.indices.split(','); - } } addAlertChangedListner() { @@ -107,6 +102,7 @@ export class AlertsListComponent implements OnInit, OnDestroy { addLoadSavedSearchListner() { this.saveSearchService.loadSavedSearch$.subscribe((savedSearch: SaveSearch) => { let queryBuilder = new QueryBuilder(); + queryBuilder.setGroupby(this.queryBuilder.groupRequest.groups.map(group => group.field)); queryBuilder.searchRequest = savedSearch.searchRequest; this.queryBuilder = queryBuilder; this.prepareColumnData(savedSearch.tableColumns, []); @@ -115,7 +111,7 @@ export class AlertsListComponent implements OnInit, OnDestroy { } calcColumnsToDisplay() { - let availableWidth = document.documentElement.clientWidth - (200 + (15 * 3)); /* screenwidth - (navPaneWidth + (paddings))*/ + let availableWidth = document.documentElement.clientWidth - (200 + (15 * 4)); /* screenwidth - (navPaneWidth + (paddings))*/ availableWidth = availableWidth - (55 + 25 + 25); /* availableWidth - (score + colunSelectIcon +selectCheckbox )*/ let tWidth = 0; this.alertsColumnsToDisplay = this.alertsColumns.filter(colMetaData => { @@ -181,6 +177,7 @@ export class AlertsListComponent implements OnInit, OnDestroy { } onSelectedAlertsChange(selectedAlerts) { + this.selectedAlerts = selectedAlerts; if (selectedAlerts.length > 0) { this.pause(); } else { @@ -198,6 +195,11 @@ export class AlertsListComponent implements OnInit, OnDestroy { this.search(); } + onGroupsChange(groups) { + this.queryBuilder.setGroupby(groups); + this.search(); + } + onPausePlay() { this.pauseRefresh = !this.pauseRefresh; if (this.pauseRefresh) { @@ -232,8 +234,6 @@ export class AlertsListComponent implements OnInit, OnDestroy { this.updateService.updateAlertState(this.selectedAlerts, 'ESCALATE').subscribe(results => { this.updateSelectedAlertStatus('ESCALATE'); }); - this.alertsService.escalate(this.selectedAlerts).subscribe(); - } processDismiss() { @@ -269,14 +269,9 @@ export class AlertsListComponent implements OnInit, OnDestroy { if (resetPaginationParams) { this.pagination.from = 0; } - this.queryBuilder.searchRequest.from = this.pagination.from; - if (this.tableMetaData.size) { - this.pagination.size = this.tableMetaData.size; - } - this.queryBuilder.searchRequest.size = this.pagination.size; - if (this.indices) { - this.queryBuilder.searchRequest.indices = this.indices; - } + + this.setSearchRequestSize(); + this.searchService.search(this.queryBuilder.searchRequest).subscribe(results => { this.setData(results); }, error => { @@ -287,6 +282,19 @@ export class AlertsListComponent implements OnInit, OnDestroy { this.tryStartPolling(); } + setSearchRequestSize() { + if (this.queryBuilder.groupRequest.groups.length == 0) { + this.queryBuilder.searchRequest.from = this.pagination.from; + if (this.tableMetaData.size) { + this.pagination.size = this.tableMetaData.size; + } + this.queryBuilder.searchRequest.size = this.pagination.size; + } else { + this.queryBuilder.searchRequest.from = 0; + this.queryBuilder.searchRequest.size = 0; + } + } + saveCurrentSearch(savedSearch: SaveSearch) { if (this.queryBuilder.query !== '*') { if (!savedSearch) { @@ -375,9 +383,7 @@ export class AlertsListComponent implements OnInit, OnDestroy { updateSelectedAlertStatus(status: string) { for (let selectedAlert of this.selectedAlerts) { - selectedAlert.status = status; - this.alerts.filter(alert => alert.source.guid === selectedAlert.source.guid) - .map(alert => alert.source['alert_status'] = status); + selectedAlert.source['alert_status'] = status; } this.selectedAlerts = []; this.resume(); http://git-wip-us.apache.org/repos/asf/metron/blob/942aaaf2/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.module.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.module.ts b/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.module.ts index 8ee194e..27b7e2e 100644 --- a/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.module.ts +++ b/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.module.ts @@ -16,6 +16,7 @@ * limitations under the License. */ import {NgModule} from '@angular/core'; +import {DecimalPipe} from '@angular/common'; import {AlertsListComponent} from './alerts-list.component'; import {routing} from './alerts-list.routing'; @@ -26,15 +27,17 @@ import {ListGroupModule} from '../../shared/list-group/list-grup.module'; import {CollapseModule} from '../../shared/collapse/collapse.module'; import {MetronTablePaginationModule} from '../../shared/metron-table/metron-table-pagination/metron-table-pagination.module'; import {ConfigureRowsModule} from '../configure-rows/configure-rows.module'; +import {GroupByModule} from '../../shared/group-by/group-by.module'; import {AlertFiltersComponent} from './alert-filters/alert-filters.component'; import {TableViewComponent} from './table-view/table-view.component'; +import {TreeViewComponent} from './tree-view/tree-view.component'; @NgModule({ imports: [routing, SharedModule, ConfigureRowsModule, MetronSorterModule, MetronTablePaginationModule, - ListGroupModule, CollapseModule], + ListGroupModule, CollapseModule, GroupByModule], exports: [AlertsListComponent], - declarations: [AlertsListComponent, TableViewComponent, AlertFiltersComponent], - providers: [SearchService] + declarations: [AlertsListComponent, TableViewComponent, TreeViewComponent, AlertFiltersComponent], + providers: [DecimalPipe, SearchService] }) export class AlertsListModule { } http://git-wip-us.apache.org/repos/asf/metron/blob/942aaaf2/metron-interface/metron-alerts/src/app/alerts/alerts-list/query-builder.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/alerts/alerts-list/query-builder.ts b/metron-interface/metron-alerts/src/app/alerts/alerts-list/query-builder.ts index 0b76ee1..863e127 100644 --- a/metron-interface/metron-alerts/src/app/alerts/alerts-list/query-builder.ts +++ b/metron-interface/metron-alerts/src/app/alerts/alerts-list/query-builder.ts @@ -19,9 +19,12 @@ import {Filter} from '../../model/filter'; import {ColumnNamesService} from '../../service/column-names.service'; import {SearchRequest} from '../../model/search-request'; import {SortField} from '../../model/sort-field'; +import {GroupRequest} from '../../model/group-request'; +import {Group} from '../../model/group'; export class QueryBuilder { private _searchRequest = new SearchRequest(); + private _groupRequest = new GroupRequest(); private _query = '*'; private _displayQuery = this._query; private _filters: Filter[] = []; @@ -62,6 +65,11 @@ export class QueryBuilder { this.query = this._searchRequest.query; } + get groupRequest(): GroupRequest { + this._groupRequest.query = this.generateSelect(); + return this._groupRequest; + } + addOrUpdateFilter(filter: Filter) { let existingFilter = this._filters.find(tFilter => tFilter.field === filter.field); if (existingFilter) { @@ -111,6 +119,10 @@ export class QueryBuilder { this.searchRequest.size = size; } + setGroupby(groups: string[]) { + this.groupRequest.groups = groups.map(groupName => new Group(groupName)); + } + setSort(sortBy: string, order: string) { let sortField = new SortField(); sortField.field = sortBy; http://git-wip-us.apache.org/repos/asf/metron/blob/942aaaf2/metron-interface/metron-alerts/src/app/alerts/alerts-list/table-view/table-view.component.html ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/alerts/alerts-list/table-view/table-view.component.html b/metron-interface/metron-alerts/src/app/alerts/alerts-list/table-view/table-view.component.html index 561b299..b8fd14f 100644 --- a/metron-interface/metron-alerts/src/app/alerts/alerts-list/table-view/table-view.component.html +++ b/metron-interface/metron-alerts/src/app/alerts/alerts-list/table-view/table-view.component.html @@ -18,7 +18,6 @@ <th style="width:55px"> <metron-config-sorter [type]="'number'" [sortBy]="threatScoreFieldName"> Score </metron-config-sorter> </th> <th *ngFor="let column of alertsColumnsToDisplay" [id]="column.name"> <metron-config-sorter [type]="column.type" [sortBy]="column.name" title="{{column.name}}"> {{ column.name | columnNameTranslate | centerEllipses:15 }}</metron-config-sorter> </th> <th></th> - <th style="width:25px"><i class="fa fa-cog configure-table-icon" (click)="showConfigureTable()"></i></th> <th style="width:25px"><input id="select-deselect-all" class="fontawesome-checkbox" type="checkbox" (click)="selectAllRows($event)"><label for="select-deselect-all"></label></th> </tr> </thead> @@ -31,7 +30,6 @@ <a (click)="addFilter(column.name, getValue(alert, column, false))" title="{{getValue(alert, column, true)}}" style="color:#689AA9">{{ getValue(alert, column, true) | centerEllipses:20:cell }}</a> </td> <td> <i class="fa fa-comments-o" aria-hidden="true" *ngIf="alert.source.comments && alert.source.comments.length > 0"></i> </td> - <td> </td> <td><input id="{{ alert.id }}" class="fontawesome-checkbox" type="checkbox" name="{{alert.id}}" (click)="selectRow($event, alert)" [checked]="selectedAlerts.indexOf(alert) != -1"><label attr.for="{{ alert.id }}"></label></td> </tr> </tbody> http://git-wip-us.apache.org/repos/asf/metron/blob/942aaaf2/metron-interface/metron-alerts/src/app/alerts/alerts-list/table-view/table-view.component.scss ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/alerts/alerts-list/table-view/table-view.component.scss b/metron-interface/metron-alerts/src/app/alerts/alerts-list/table-view/table-view.component.scss index fa2417e..eec7f92 100644 --- a/metron-interface/metron-alerts/src/app/alerts/alerts-list/table-view/table-view.component.scss +++ b/metron-interface/metron-alerts/src/app/alerts/alerts-list/table-view/table-view.component.scss @@ -18,7 +18,7 @@ @import "../../../../variables.scss"; .table-wrapper { - min-height: calc(100vh - 250px); + min-height: calc(100vh - 320px); } .configure-table-icon { http://git-wip-us.apache.org/repos/asf/metron/blob/942aaaf2/metron-interface/metron-alerts/src/app/alerts/alerts-list/tree-view/tree-group-data.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/alerts/alerts-list/tree-view/tree-group-data.ts b/metron-interface/metron-alerts/src/app/alerts/alerts-list/tree-view/tree-group-data.ts new file mode 100644 index 0000000..ae65a67 --- /dev/null +++ b/metron-interface/metron-alerts/src/app/alerts/alerts-list/tree-view/tree-group-data.ts @@ -0,0 +1,65 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Subscription} from 'rxjs/Rx'; +import {SearchResponse} from '../../../model/search-response'; +import {Pagination} from '../../../model/pagination'; +import {TREE_SUB_GROUP_SIZE} from '../../../utils/constants'; +import {SortField} from '../../../model/sort-field'; +import {SortEvent} from '../../../shared/metron-table/metron-table.directive'; +import {Sort} from '../../../utils/enums'; + +export class TreeGroupData { + key: string; + total: number; + level: number; + show: boolean; + expand = false; + score: number; + + // Used by only Dashrow + sortField: SortField; + sortEvent: SortEvent = { sortBy: '', type: '', sortOrder: Sort.ASC}; + treeSubGroups: TreeGroupData[] = []; + + // Used by only Leafnodes + groupQueryMap = null; + response: SearchResponse = new SearchResponse(); + pagingData: Pagination = new Pagination(); + + + constructor(key: string, total: number, score: number, level: number, expand: boolean) { + this.key = key; + this.total = total; + this.score = score; + this.level = level; + this.show = expand; + + this.pagingData.size = TREE_SUB_GROUP_SIZE; + } +} + + +export class TreeAlertsSubscription { + refreshTimer: Subscription; + group: TreeGroupData; + + constructor(refreshTimer: Subscription, group: TreeGroupData) { + this.refreshTimer = refreshTimer; + this.group = group; + } +} http://git-wip-us.apache.org/repos/asf/metron/blob/942aaaf2/metron-interface/metron-alerts/src/app/alerts/alerts-list/tree-view/tree-view.component.html ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/alerts/alerts-list/tree-view/tree-view.component.html b/metron-interface/metron-alerts/src/app/alerts/alerts-list/tree-view/tree-view.component.html new file mode 100644 index 0000000..e89cdc9 --- /dev/null +++ b/metron-interface/metron-alerts/src/app/alerts/alerts-list/tree-view/tree-view.component.html @@ -0,0 +1,114 @@ +<!-- + Licensed to the Apache Software + Foundation (ASF) under one or more contributor license agreements. See the + NOTICE file distributed with this work for additional information regarding + copyright ownership. The ASF licenses this file to You under the Apache License, + Version 2.0 (the "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed + under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + OR CONDITIONS OF ANY KIND, either express or implied. See the License for + the specific language governing permissions and limitations under the License. +--> +<div role="tablist" class="tree-wrapper" aria-multiselectable="true"> + <div class="card" *ngFor="let group of topGroups; let i = index;" [attr.data-name]="group.key"> + <div class="card-header" role="tab" [attr.id]="'title-' + i"> + <div class="hexagon" appAlertSeverityHexagon [severity]="group.score"> + <span *ngIf="group.score < 1000" class="dash-score">{{ group.score }}</span> + <span *ngIf="group.score >= 1000" class="dash-score">999<sup>+</sup></span> + </div> + <div class="mrow top-group" (click)="toggleTopLevelGroup(group)"> + <div class="col-5 text-light severity-padding"> <span class="title"> {{ group.key | centerEllipses:45 }} </span> </div> + <div class="col-6 text-light two-line"> <span class="text-dark"> ALERTS </span> <br> <span class="title"> {{ group.total | number }} </span> </div> + <div class="col-1 text-right pr-4"> + <i class="down-arrow" data-animation="false" data-toggle="tooltip" data-placement="left" title="Open Group" + aria-expanded="false" [attr.href]="'#body-' + i" [attr.aria-controls]="'body-' + i"> </i> + </div> + </div> + </div> + <div class="collapse" role="tabpanel" [ngClass]="{'show': group.expand}"> + <div class="card-block"> + <div class="table-wrapper"> + <table class="table table-sm" metron-config-table [data]="alerts" [cellSelectable]="true" (onSort)="sortTreeSubGroup($event, group)" style="white-space: nowrap;" (window:resize)="resize()" #table> + <thead> + <tr> + <th> </th> + <th class="table-score-col"> + <metron-config-sorter [type]="'number'" [sortBy]="threatScoreFieldName" [sortOnCol]="group.sortEvent.sortBy" [sortOrder]="group.sortEvent.sortOrder"> Score </metron-config-sorter> + </th> + <th *ngFor="let column of alertsColumnsToDisplay" [id]="column.name"> + <metron-config-sorter [type]="column.type" [sortBy]="column.name" title="{{column.name}}" [sortOnCol]="group.sortEvent.sortBy" [sortOrder]="group.sortEvent.sortOrder"> {{ column.name | columnNameTranslate | centerEllipses:15 }}</metron-config-sorter> + </th> + <th style="width: 15px"></th> + <th style="width:25px"><input id="select-deselect-all-{{ group.key }}" class="fontawesome-checkbox" type="checkbox" (click)="selectAllGroupRows($event, group)"> + <label for="select-deselect-all-{{ group.key }}"></label> + </th> + </tr> + </thead> + <tbody> + <ng-container> + <tr *ngFor="let alert of group.response.results" [ngClass]="{'selected' : selectedAlerts.indexOf(alert) != -1, 'd-none': !group.expand || !group.show}" (click)="showDetails($event, alert)"> + <td [attr.colspan]="2" (click)="addFilter(threatScoreFieldName, alert.source[threatScoreFieldName])"> + <div appAlertSeverity [severity]="alert.source[threatScoreFieldName]"> + <a> {{ alert.source[threatScoreFieldName] ? alert.source[threatScoreFieldName] : '-' }}</a> + </div> + </td> + <td #cell *ngFor="let column of alertsColumnsToDisplay" [attr.data-name]="column.name"> + <a title="{{getValue(alert, column, true)}}" style="color:#689AA9" (click)="addFilter(column.name, getValue(alert, column, false))">{{ getValue(alert, column, true) | centerEllipses:20:cell }}</a> + </td> + <td> <i class="fa fa-comments-o" aria-hidden="true" *ngIf="alert.source.comments && alert.source.comments.length > 0"></i> </td> + <td> + <input id="{{ alert.id }}" class="fontawesome-checkbox" type="checkbox" name="{{alert.id}}" (click)="selectRow($event, alert)" [checked]="selectedAlerts.indexOf(alert) != -1"> + <label attr.for="{{ alert.id }}"></label> + </td> + </tr> + + <tr *ngIf="group.response.results.length > 0" class="no-hover" [ngClass]="{'d-none': !group.expand || !group.show}"> + <td [attr.colspan]="alertsColumnsToDisplay.length + 3" class="text-right"> + <metron-table-pagination [(pagination)]="group.pagingData" (pageChange)="groupPageChange(group)"> </metron-table-pagination> + </td> + </tr> + </ng-container> + <ng-container *ngFor="let subGroup of group.treeSubGroups;let i = index"> + + <tr class="table-group-row" [ngClass]="{'d-none': !subGroup.show}" (click)="toggleSubGroups(group, subGroup, i)" [attr.data-name]="subGroup.key"> + <td [attr.colspan]="alertsColumnsToDisplay.length + 4" [ngStyle]="{'padding-left.px': (16 * (subGroup.level -1))}"> + <span class="table-group-icon-col" data-animation="false" data-toggle="tooltip" data-placement="bottom" title="Open Group"> + <i class="fa" aria-hidden="true" [ngClass]="{'fa-caret-down': subGroup.expand, 'fa-caret-right': !subGroup.expand}"></i> + </span> + <span class="score" appAlertSeverity [severity]="subGroup.score"> {{ subGroup.score }} </span> + <span class="group-value"> <span class="text-light"> {{ subGroup.key }} </span> ({{ subGroup.total}})</span> + </td> + </tr> + + <tr *ngFor="let alert of subGroup.response.results" [ngClass]="{'selected' : selectedAlerts.indexOf(alert) != -1, 'd-none': !subGroup.expand || !subGroup.show}" (click)="showDetails($event, alert)"> + <td [attr.colspan]="2" [ngStyle]="{'padding-left.px': (16 * (subGroup.level -1)) + 23}" (click)="addFilter(threatScoreFieldName, alert.source[threatScoreFieldName])"> + <div appAlertSeverity [severity]="alert.source[threatScoreFieldName]"> + <a> {{ alert.source[threatScoreFieldName] ? alert.source[threatScoreFieldName] : '-' }}</a> + </div> + </td> + <td #cell *ngFor="let column of alertsColumnsToDisplay" [attr.data-name]="column.name"> + <a (click)="addFilter(column.name, getValue(alert, column, false))" title="{{getValue(alert, column, true)}}" style="color:#689AA9" (click)="addFilter(column.name, getValue(alert, column, false))">{{ getValue(alert, column, true) | centerEllipses:20:cell }}</a> + </td> + <td> <i class="fa fa-comments-o" aria-hidden="true" *ngIf="alert.source.comments && alert.source.comments.length > 0"></i> </td> + <td> + <input id="{{ alert.id }}" class="fontawesome-checkbox" type="checkbox" name="{{alert.id}}" (click)="selectRow($event, alert)" [checked]="selectedAlerts.indexOf(alert) != -1"> + <label attr.for="{{ alert.id }}"></label> + </td> + </tr> + + <tr *ngIf="subGroup.response.results.length > 0" class="no-hover" [ngClass]="{'d-none': !subGroup.expand || !subGroup.show}"> + <td [attr.colspan]="alertsColumnsToDisplay.length + 3" class="text-right"> + <metron-table-pagination [(pagination)]="subGroup.pagingData" (pageChange)="groupPageChange(subGroup)"> </metron-table-pagination> + </td> + </tr> + + </ng-container> + </tbody> + </table> + </div> + </div> + </div> + </div> +</div> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/metron/blob/942aaaf2/metron-interface/metron-alerts/src/app/alerts/alerts-list/tree-view/tree-view.component.scss ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/alerts/alerts-list/tree-view/tree-view.component.scss b/metron-interface/metron-alerts/src/app/alerts/alerts-list/tree-view/tree-view.component.scss new file mode 100644 index 0000000..8668b49 --- /dev/null +++ b/metron-interface/metron-alerts/src/app/alerts/alerts-list/tree-view/tree-view.component.scss @@ -0,0 +1,153 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../../variables.scss"; + +$group-height: 70px; + +.card { + background: none; + border-radius: 0px; + margin-bottom: 12px; + + .title { + font-size: 20px; + } + + .group-value { + padding-left: 12px; + } + + .severity-padding { + padding-left: 30px; + } + + .text-light { + font-family: Roboto; + color: $iron; + } + + .text-dark { + font-size: 12px; + font-family: Roboto; + color: $dusty-grey; + } + + .card-header { + padding: 0px; + border-radius: 0px; + height: $group-height; + line-height: $group-height; + background: $mine-shaft-8; + } + + .down-arrow { + padding: 10px 20px; + + &:hover { + background: $eden-1; + border: 2px solid $blue-mine; + } + + &:after { + top: 2px; + right: 37px; + color: $gothic; + font-size: 25px; + content: '\f107'; + font-style: normal; + position: absolute; + font-family: "FontAwesome"; + } + } + + .two-line { + padding-top: 16px; + line-height: 1; + } + + .collapse { + background: $mine-shaft-9; + } + .card-block { + padding-bottom: 0px; + background: $mine-shaft-9; + } + + .fa-caret-right, .fa-caret-down { + padding-right: 4px; + font-size: 15px; + } + + .table-group-icon-col { + width:5px; + box-sizing: border-box; + padding: 6px 0px 7px 5px; + border: 1px solid transparent; + + &:hover { + background: $eden-1; + border: 1px solid $blue-mine; + } + } + + .table-score-col { + width:55px; + } + + .table-group-row { + background: $mine-shaft-10; + } + + .no-hover { + height: $group-height; + + td { + text-align:center; + vertical-align:middle; + border-bottom: none; + + &:hover { + background: none; + } + } + } +} + +.table { + tr { + line-height: 25px; + height: 35px; + } +} + +sup { + font-size: 100%; +} + +.configure-table-icon { + font-size: 16px; + cursor: pointer; +} + +.tree-wrapper { + min-height: calc(100vh - 280px); +} + +.top-group { + cursor: pointer; +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/metron/blob/942aaaf2/metron-interface/metron-alerts/src/app/alerts/alerts-list/tree-view/tree-view.component.spec.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/alerts/alerts-list/tree-view/tree-view.component.spec.ts b/metron-interface/metron-alerts/src/app/alerts/alerts-list/tree-view/tree-view.component.spec.ts new file mode 100644 index 0000000..8a50404 --- /dev/null +++ b/metron-interface/metron-alerts/src/app/alerts/alerts-list/tree-view/tree-view.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TreeViewComponent } from './tree-view.component'; + +describe('TreeViewComponent', () => { + let component: TreeViewComponent; + let fixture: ComponentFixture<TreeViewComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ TreeViewComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TreeViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); http://git-wip-us.apache.org/repos/asf/metron/blob/942aaaf2/metron-interface/metron-alerts/src/app/alerts/alerts-list/tree-view/tree-view.component.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/alerts/alerts-list/tree-view/tree-view.component.ts b/metron-interface/metron-alerts/src/app/alerts/alerts-list/tree-view/tree-view.component.ts new file mode 100644 index 0000000..75a7e1c --- /dev/null +++ b/metron-interface/metron-alerts/src/app/alerts/alerts-list/tree-view/tree-view.component.ts @@ -0,0 +1,352 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import {Router} from '@angular/router'; +import {Subscription} from 'rxjs/Rx'; + +import {TableViewComponent} from '../table-view/table-view.component'; +import {SearchResponse} from '../../../model/search-response'; +import {SearchService} from '../../../service/search.service'; +import {TreeGroupData, TreeAlertsSubscription} from './tree-group-data'; +import {GroupResponse} from '../../../model/group-response'; +import {GroupResult} from '../../../model/group-result'; +import {Group} from '../../../model/group'; +import {SortField} from '../../../model/sort-field'; +import {Sort} from '../../../utils/enums'; +import {MetronDialogBox, DialogType} from '../../../shared/metron-dialog-box'; +import {ElasticsearchUtils} from '../../../utils/elasticsearch-utils'; +import {SearchRequest} from '../../../model/search-request'; +import {UpdateService} from '../../../service/update.service'; +import {PatchRequest} from '../../../model/patch-request'; + +@Component({ + selector: 'app-tree-view', + templateUrl: './tree-view.component.html', + styleUrls: ['./tree-view.component.scss'] +}) + +export class TreeViewComponent extends TableViewComponent implements OnChanges { + + groupByFields: string[] = []; + topGroups: TreeGroupData[] = []; + groupResponse: GroupResponse = new GroupResponse(); + treeGroupSubscriptionMap: {[key: string]: TreeAlertsSubscription } = {}; + + constructor(router: Router, + searchService: SearchService, + metronDialogBox: MetronDialogBox, + private updateService: UpdateService) { + super(router, searchService, metronDialogBox); + } + + addAlertChangedListner() { + this.updateService.alertChanged$.subscribe(patchRequest => { + this.updateAlert(patchRequest); + }); + } + + collapseGroup(groupArray: TreeGroupData[], level: number, index: number) { + for (let i = index + 1; i < groupArray.length; i++) { + if (groupArray[i].level > (level)) { + groupArray[i].show = false; + groupArray[i].expand = false; + } else { + break; + } + } + } + + createQuery(selectedGroup: TreeGroupData) { + let searchQuery = this.queryBuilder.generateSelect(); + let groupQery = Object.keys(selectedGroup.groupQueryMap).map(key => { + return key.replace(/:/g, '\\:') + + ':' + + String(selectedGroup.groupQueryMap[key]) + .replace(/[\*\+\-=~><\"\?^\${}\(\)\:\!\/[\]\\\s]/g, '\\$&') // replace single special characters + .replace(/\|\|/g, '\\||') // replace || + .replace(/\&\&/g, '\\&&'); // replace && + }).join(' AND '); + + groupQery += searchQuery === '*' ? '' : (' AND ' + searchQuery); + return groupQery; + } + + expandGroup(groupArray: TreeGroupData[], level: number, index: number) { + for (let i = index + 1; i < groupArray.length; i++) { + if (groupArray[i].level === (level + 1)) { + groupArray[i].show = true; + } else { + break; + } + } + } + + getAlerts(selectedGroup: TreeGroupData): Subscription { + let searchRequest = new SearchRequest(); + searchRequest.query = this.createQuery(selectedGroup); + searchRequest.from = selectedGroup.pagingData.from; + searchRequest.size = selectedGroup.pagingData.size; + searchRequest.sort = selectedGroup.sortField ? [selectedGroup.sortField] : []; + + return this.searchGroup(selectedGroup, searchRequest); + } + + getGroups() { + let groupRequest = this.queryBuilder.groupRequest; + groupRequest.query = this.queryBuilder.generateSelect(); + + this.searchService.groups(groupRequest).subscribe(groupResponse => { + this.updateGroupData(groupResponse); + }); + } + + updateGroupData(groupResponse) { + this.selectedAlerts = []; + this.groupResponse = groupResponse; + this.parseTopLevelGroup(); + } + + groupPageChange(group: TreeGroupData) { + this.getAlerts(group); + } + + createTopGroups(groupByFields: string[]) { + this.topGroups = []; + this.treeGroupSubscriptionMap = {}; + + this.groupResponse.groupResults.forEach((groupResult: GroupResult) => { + let treeGroupData = new TreeGroupData(groupResult.key, groupResult.total, groupResult.score, 0, false); + if (groupByFields.length === 1) { + treeGroupData.groupQueryMap = this.createTopGroupQueryMap(groupByFields[0], groupResult); + } + + this.topGroups.push(treeGroupData); + }); + } + + createTopGroupQueryMap(groupByFields: string, groupResult: GroupResult) { + let groupQueryMap = {}; + groupQueryMap[groupByFields] = groupResult.key; + return groupQueryMap; + } + + initTopGroups() { + let groupByFields = this.queryBuilder.groupRequest.groups.map(group => group.field); + let currentTopGroupKeys = this.groupResponse.groupResults.map(groupResult => groupResult.key); + let previousTopGroupKeys = this.topGroups.map(group => group.key); + + if (this.topGroups.length === 0 || JSON.stringify(this.groupByFields) !== JSON.stringify(groupByFields) || + JSON.stringify(currentTopGroupKeys) !== JSON.stringify(previousTopGroupKeys)) { + this.createTopGroups(groupByFields); + } + + this.groupByFields = groupByFields; + } + + search(resetPaginationParams = true, pageSize: number = null) { + this.getGroups(); + } + + ngOnChanges(changes: SimpleChanges) { + if ((changes['alerts'] && changes['alerts'].currentValue)) { + this.search(); + } + } + + ngOnInit() { + this.addAlertChangedListner(); + } + + searchGroup(selectedGroup: TreeGroupData, searchRequest: SearchRequest): Subscription { + return this.searchService.search(searchRequest).subscribe(results => { + this.setData(selectedGroup, results); + }, error => { + this.metronDialogBox.showConfirmationMessage(ElasticsearchUtils.extractESErrorMessage(error), DialogType.Error); + }); + } + + setData(selectedGroup: TreeGroupData, results: SearchResponse) { + selectedGroup.response.results = results.results; + selectedGroup.pagingData.total = results.total; + selectedGroup.total = results.total; + + this.topGroups.map(topGroup => { + if (topGroup.treeSubGroups.length > 0) { + topGroup.total = topGroup.treeSubGroups.reduce((total, subGroup) => { return total + subGroup.total }, 0); + } + }); + } + + checkAndToSubscription(group: TreeGroupData) { + if (group.groupQueryMap) { + let key = JSON.stringify(group.groupQueryMap); + if (this.treeGroupSubscriptionMap[key]) { + this.removeFromSubscription(group); + } + + let subscription = this.getAlerts(group); + this.treeGroupSubscriptionMap[key] = new TreeAlertsSubscription(subscription, group); + } + } + + removeFromSubscription(group: TreeGroupData) { + if (group.groupQueryMap) { + let key = JSON.stringify(group.groupQueryMap); + let subscription = this.treeGroupSubscriptionMap[key].refreshTimer; + if (subscription && !subscription.closed) { + subscription.unsubscribe(); + } + delete this.treeGroupSubscriptionMap[key]; + } + } + + toggleSubGroups(topLevelGroup: TreeGroupData, selectedGroup: TreeGroupData, index: number) { + selectedGroup.expand = !selectedGroup.expand; + + if (selectedGroup.expand) { + this.expandGroup(topLevelGroup.treeSubGroups, selectedGroup.level, index); + this.checkAndToSubscription(selectedGroup); + } else { + this.collapseGroup(topLevelGroup.treeSubGroups, selectedGroup.level, index); + this.removeFromSubscription(selectedGroup); + } + } + + toggleTopLevelGroup(group: TreeGroupData) { + group.expand = !group.expand; + group.show = !group.show; + + if (group.expand) { + this.checkAndToSubscription(group); + } else { + this.removeFromSubscription(group); + } + } + + parseSubGroups(group: GroupResult, groupAsArray: TreeGroupData[], + groupQueryMap: {[key: string]: string}, groupedBy: string, level: number, index: number): number { + index++; + groupQueryMap[groupedBy] = group.key; + + let currentTreeNodeData = (groupAsArray.length > 0) ? groupAsArray[index] : null; + + if (currentTreeNodeData && (currentTreeNodeData.key === group.key) && (currentTreeNodeData.level === level)) { + currentTreeNodeData.total = group.total; + } else { + let newTreeNodeData = new TreeGroupData(group.key, group.total, group.score, level, level === 1); + if (!currentTreeNodeData) { + groupAsArray.push(newTreeNodeData); + } else { + groupAsArray.splice(index, 1, newTreeNodeData); + } + } + + if (!group.groupResults) { + groupAsArray[index].groupQueryMap = JSON.parse(JSON.stringify(groupQueryMap)); + if (groupAsArray[index].expand && groupAsArray[index].show && groupAsArray[index].groupQueryMap) { + this.checkAndToSubscription(groupAsArray[index]); + } + return index; + } + + group.groupResults.forEach(subGroup => { + index = this.parseSubGroups(subGroup, groupAsArray, groupQueryMap, group.groupedBy, level + 1, index); + }); + + return index; + } + + parseTopLevelGroup() { + let groupedBy = this.groupResponse.groupedBy; + + this.initTopGroups(); + + for (let i = 0; i < this.groupResponse.groupResults.length; i++) { + let index = -1; + let topGroup = this.topGroups[i]; + let resultGroup = this.groupResponse.groupResults[i]; + let groupQueryMap = this.createTopGroupQueryMap(groupedBy, resultGroup); + + topGroup.total = resultGroup.total; + + if (resultGroup.groupResults) { + resultGroup.groupResults.forEach(subGroup => { + index = this.parseSubGroups(subGroup, topGroup.treeSubGroups, groupQueryMap, resultGroup.groupedBy, 1, index); + }); + + topGroup.treeSubGroups.splice(index + 1); + } + } + + if (this.groupByFields.length === 1) { + this.refreshAllExpandedGroups(); + } + } + + sortTreeSubGroup($event, treeGroup: TreeGroupData) { + let sortBy = $event.sortBy === 'id' ? '_uid' : $event.sortBy; + + let sortField = new SortField(); + sortField.field = sortBy; + sortField.sortOrder = $event.sortOrder === Sort.ASC ? 'asc' : 'desc'; + + treeGroup.sortEvent = $event; + treeGroup.sortField = sortField; + treeGroup.treeSubGroups.forEach(treeSubGroup => treeSubGroup.sortField = sortField); + + this.refreshAllExpandedGroups(); + } + + selectAllGroupRows($event, group: TreeGroupData) { + this.selectedAlerts = []; + + if ($event.target.checked) { + if (group.expand && group.show && group.response) { + this.selectedAlerts = group.response.results; + } + + group.treeSubGroups.forEach(subGroup => { + if (subGroup.expand && subGroup.show && subGroup.response) { + this.selectedAlerts = this.selectedAlerts.concat(subGroup.response.results); + } + }); + } + + this.onSelectedAlertsChange.emit(this.selectedAlerts); + } + + refreshAllExpandedGroups() { + Object.keys(this.treeGroupSubscriptionMap).forEach(key => { + this.getAlerts(this.treeGroupSubscriptionMap[key].group); + }); + } + + updateAlert(patchRequest: PatchRequest) { + this.searchService.getAlert(patchRequest.sensorType, patchRequest.guid).subscribe(alertSource => { + + Object.keys(this.treeGroupSubscriptionMap).forEach(key => { + let group = this.treeGroupSubscriptionMap[key].group; + if(group.response && group.response.results && group.response.results.length > 0) { + group.response.results.filter(alert => alert.source.guid === patchRequest.guid) + .map(alert => alert.source = alertSource); + } + }); + }); + } +} http://git-wip-us.apache.org/repos/asf/metron/blob/942aaaf2/metron-interface/metron-alerts/src/app/alerts/configure-rows/configure-rows.component.scss ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/alerts/configure-rows/configure-rows.component.scss b/metron-interface/metron-alerts/src/app/alerts/configure-rows/configure-rows.component.scss index 4c29e28..7d16a4f 100644 --- a/metron-interface/metron-alerts/src/app/alerts/configure-rows/configure-rows.component.scss +++ b/metron-interface/metron-alerts/src/app/alerts/configure-rows/configure-rows.component.scss @@ -25,7 +25,7 @@ label { .card { width: 349px; position: absolute; - left: -140px; + left: -115px; z-index: 1; top: 50px; border-radius: 0; @@ -78,7 +78,7 @@ label { .fa-sort-asc { position: absolute; bottom: -50px; - left: 10px; + left: 44px; font-size: 62px; color: #333333; z-index: 2;
