mirror of
https://github.com/valitydev/wazuh-kibana-app.git
synced 2024-11-06 09:55:18 +00:00
parent
8f9c9aa95a
commit
d750302f09
@ -27,13 +27,6 @@ All notable changes to the Wazuh app project will be documented in this file.
|
||||
- Fixed error exporting as CSV the files into a group [#1833](https://github.com/wazuh/wazuh-kibana-app/issues/1833).
|
||||
|
||||
|
||||
## Wazuh v3.10.2 - Kibana v7.5.0 - Revision 550
|
||||
|
||||
### Added
|
||||
|
||||
- Support for Kibana v7.5.0
|
||||
|
||||
|
||||
## Wazuh v3.10.2 - Kibana v7.3.2 - Revision 546
|
||||
|
||||
### Added
|
||||
|
15
README.md
15
README.md
@ -32,8 +32,8 @@ Visualize and analyze Wazuh alerts stored in Elasticsearch using our Kibana app
|
||||
|
||||
- Wazuh HIDS 3.11.0
|
||||
- Wazuh RESTful API 3.11.0
|
||||
- Kibana 7.5.0
|
||||
- Elasticsearch 7.5.0
|
||||
- Kibana 7.4.2
|
||||
- Elasticsearch 7.4.2
|
||||
|
||||
## Installation
|
||||
|
||||
@ -42,13 +42,13 @@ Install the app
|
||||
- With sudo:
|
||||
|
||||
```
|
||||
sudo -u kibana /usr/share/kibana/bin/kibana-plugin install https://packages.wazuh.com/wazuhapp/wazuhapp-3.11.0_7.5.0.zip
|
||||
sudo -u kibana /usr/share/kibana/bin/kibana-plugin install https://packages.wazuh.com/wazuhapp/wazuhapp-3.11.0_7.4.2.zip
|
||||
```
|
||||
|
||||
- Without sudo:
|
||||
|
||||
```
|
||||
su -c '/usr/share/kibana/bin/kibana-plugin install https://packages.wazuh.com/wazuhapp/wazuhapp-3.11.0_7.5.0.zip' kibana
|
||||
su -c '/usr/share/kibana/bin/kibana-plugin install https://packages.wazuh.com/wazuhapp/wazuhapp-3.11.0_7.4.2.zip' kibana
|
||||
```
|
||||
|
||||
Restart Kibana
|
||||
@ -105,13 +105,13 @@ Install the app
|
||||
- With sudo:
|
||||
|
||||
```
|
||||
sudo -u kibana /usr/share/kibana/bin/kibana-plugin install https://packages.wazuh.com/wazuhapp/wazuhapp-3.11.0_7.5.0.zip
|
||||
sudo -u kibana /usr/share/kibana/bin/kibana-plugin install https://packages.wazuh.com/wazuhapp/wazuhapp-3.11.0_7.4.2.zip
|
||||
```
|
||||
|
||||
- Without sudo:
|
||||
|
||||
```
|
||||
su -c '/usr/share/kibana/bin/kibana-plugin install https://packages.wazuh.com/wazuhapp/wazuhapp-3.11.0_7.5.0.zip' kibana
|
||||
su -c '/usr/share/kibana/bin/kibana-plugin install https://packages.wazuh.com/wazuhapp/wazuhapp-3.11.0_7.4.2.zip' kibana
|
||||
```
|
||||
|
||||
Restart Kibana
|
||||
@ -210,13 +210,10 @@ service kibana restart
|
||||
| 7.3.1 | 3.10.1 | /usr/share/kibana/bin/kibana-plugin install <https://packages.wazuh.com/wazuhapp/wazuhapp-3.10.1_7.3.1.zip> |
|
||||
| 7.3.2 | 3.10.1 | /usr/share/kibana/bin/kibana-plugin install <https://packages.wazuh.com/wazuhapp/wazuhapp-3.10.1_7.3.2.zip> |
|
||||
| 6.8.3 | 3.10.2 | /usr/share/kibana/bin/kibana-plugin install <https://packages.wazuh.com/wazuhapp/wazuhapp-3.10.2_6.8.3.zip> |
|
||||
| 6.8.4 | 3.10.2 | /usr/share/kibana/bin/kibana-plugin install <https://packages.wazuh.com/wazuhapp/wazuhapp-3.10.2_6.8.4.zip> |
|
||||
| 6.8.5 | 3.10.2 | /usr/share/kibana/bin/kibana-plugin install <https://packages.wazuh.com/wazuhapp/wazuhapp-3.10.2_6.8.5.zip> |
|
||||
| 7.3.2 | 3.10.2 | /usr/share/kibana/bin/kibana-plugin install <https://packages.wazuh.com/wazuhapp/wazuhapp-3.10.2_7.3.2.zip> |
|
||||
| 7.4.0 | 3.10.2 | /usr/share/kibana/bin/kibana-plugin install <https://packages.wazuh.com/wazuhapp/wazuhapp-3.10.2_7.4.0.zip> |
|
||||
| 7.4.1 | 3.10.2 | /usr/share/kibana/bin/kibana-plugin install <https://packages.wazuh.com/wazuhapp/wazuhapp-3.10.2_7.4.1.zip> |
|
||||
| 7.4.2 | 3.10.2 | /usr/share/kibana/bin/kibana-plugin install <https://packages.wazuh.com/wazuhapp/wazuhapp-3.10.2_7.4.2.zip> |
|
||||
| 7.5.0 | 3.10.2 | /usr/share/kibana/bin/kibana-plugin install <https://packages.wazuh.com/wazuhapp/wazuhapp-3.10.2_7.5.0.zip> |
|
||||
| 6.8.3 | 3.11.0 | /usr/share/kibana/bin/kibana-plugin install <https://packages.wazuh.com/wazuhapp/wazuhapp-3.11.0_6.8.3.zip> |
|
||||
| 7.2.0 | 3.11.0 | /usr/share/kibana/bin/kibana-plugin install <https://packages.wazuh.com/wazuhapp/wazuhapp-3.11.0_7.2.0.zip> |
|
||||
| 7.4.2 | 3.11.0 | /usr/share/kibana/bin/kibana-plugin install <https://packages.wazuh.com/wazuhapp/wazuhapp-3.11.0_7.4.2.zip> |
|
||||
|
50
index.js
50
index.js
@ -14,11 +14,11 @@
|
||||
import { initApp } from './init';
|
||||
import { resolve } from 'path';
|
||||
|
||||
export default function (kibana) {
|
||||
return new kibana.Plugin({
|
||||
require: ['kibana', 'elasticsearch'],
|
||||
export default kibana =>
|
||||
new kibana.Plugin({
|
||||
id: 'wazuh',
|
||||
name: 'wazuh',
|
||||
require: ['kibana', 'elasticsearch'],
|
||||
uiExports: {
|
||||
app: {
|
||||
id: 'wazuh',
|
||||
@ -37,48 +37,20 @@ export default function (kibana) {
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
config(Joi) {
|
||||
return Joi.object({
|
||||
enabled: Joi.boolean().default(true),
|
||||
}).default();
|
||||
},
|
||||
|
||||
init(server, options) { // eslint-disable-line no-unused-vars
|
||||
init(server, options) {
|
||||
// Kibana spaces locker
|
||||
const xpackMainPlugin = server.plugins.xpack_main;
|
||||
if (xpackMainPlugin) {
|
||||
const featureId = 'wazuh';
|
||||
|
||||
if (xpackMainPlugin) {
|
||||
xpackMainPlugin.registerFeature({
|
||||
id: featureId,
|
||||
id: 'wazuh',
|
||||
name: 'Wazuh',
|
||||
navLinkId: featureId,
|
||||
icon: 'questionInCircle',
|
||||
app: [featureId, 'kibana', 'elasticsearch'],
|
||||
catalogue: [],
|
||||
privileges: {
|
||||
all: {
|
||||
api: [],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['show'],
|
||||
},
|
||||
read: {
|
||||
api: [],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['show'],
|
||||
},
|
||||
},
|
||||
app: ['wazuh', 'kibana', 'elasticsearch'],
|
||||
navLinkId: 'wazuh',
|
||||
privileges: {}
|
||||
});
|
||||
}
|
||||
|
||||
// Add server routes and initialize the plugin here
|
||||
initApp(server);
|
||||
return initApp(server, options);
|
||||
}
|
||||
});
|
||||
}
|
2
init.js
2
init.js
@ -28,7 +28,7 @@ export function initApp(server) {
|
||||
|
||||
log('init:initApp', `Waiting for Kibana migration jobs`, 'debug');
|
||||
server.kibanaMigrator
|
||||
.runMigrations()
|
||||
.awaitMigration()
|
||||
.then(() => {
|
||||
log(
|
||||
'init:initApp',
|
||||
|
@ -4,7 +4,7 @@
|
||||
"revision": "0561",
|
||||
"code": "0561-0",
|
||||
"kibana": {
|
||||
"version": "7.5.0"
|
||||
"version": "7.4.2"
|
||||
},
|
||||
"description": "Wazuh app",
|
||||
"main": "index.js",
|
||||
|
@ -120,7 +120,7 @@ export class WelcomeScreen extends Component {
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFlexGrid columns={2}>
|
||||
{this.buildTabCard('general', 'dashboardApp')}
|
||||
{this.buildTabCard('fim', 'filebeatApp')}
|
||||
{this.buildTabCard('fim', 'loggingApp')}
|
||||
{this.buildTabCard('configuration', 'gear')}
|
||||
{this.buildTabCard('syscollector', 'notebookApp')}
|
||||
</EuiFlexGrid>
|
||||
|
@ -126,7 +126,7 @@ export class WelcomeScreen extends Component {
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGrid columns={2}>
|
||||
{this.buildTabCard('general', 'dashboardApp')}
|
||||
{this.buildTabCard('fim', 'filebeatApp')}
|
||||
{this.buildTabCard('fim', 'loggingApp')}
|
||||
{this.props.extensions.aws &&
|
||||
this.buildTabCard('aws', 'logoAWSMono')}
|
||||
</EuiFlexGrid>
|
||||
|
@ -11,7 +11,7 @@
|
||||
<md-button class="wz-menu-button" ng-href="#/manager" ng-click="setMenuNavItem('manager')"
|
||||
ng-class="{'wz-menu-active': menuNavItem === 'manager' }" aria-label="Cluster tab"
|
||||
data-test-subj="wzMenuManagement">
|
||||
<react-component name="EuiIcon" class="kbnGlobalNavLink__euiIcon" props="{type:'managementApp'}" /> Management
|
||||
<react-component name="EuiIcon" class="kbnGlobalNavLink__euiIcon" props="{type:'infraApp'}" /> Management
|
||||
</md-button>
|
||||
|
||||
<md-button class="wz-menu-button" ng-href="#/agents-preview" ng-click="setMenuNavItem('agents-preview')"
|
||||
|
@ -39,7 +39,7 @@ export class VisHandlers {
|
||||
* Get all applied filters
|
||||
* @param {*} syscollector
|
||||
*/
|
||||
async getAppliedFilters(syscollector) {
|
||||
getAppliedFilters(syscollector) {
|
||||
const appliedFilters = {};
|
||||
|
||||
if (syscollector) {
|
||||
@ -56,32 +56,34 @@ export class VisHandlers {
|
||||
}
|
||||
|
||||
// Check raw response from all rendered tables
|
||||
let tables = this.list.filter(item => (((item || {}).vis || {})._state || {}).type === 'table');
|
||||
for (let i = 0; i < tables.length; i++) {
|
||||
const columns = [];
|
||||
const title = tables[i].vis._state.title || tables[i].dataLoader.previousVisState.title || 'Table';
|
||||
const item = await tables[i].fetch();
|
||||
for (const table of item.value.visData.tables) {
|
||||
columns.push(...table.columns.map(t => t.name));
|
||||
}
|
||||
|
||||
tables[i] = !!(((((item || {}).value || {}).visData || {}).tables || [])[0] || {}).rows
|
||||
? {
|
||||
rows: item.value.visData.tables[0].rows.map(x => { return Object.values(x) }),
|
||||
title,
|
||||
columns
|
||||
const tables = this.list
|
||||
.filter(item => (((item || {}).vis || {})._state || {}).type === 'table')
|
||||
.map(item => {
|
||||
const columns = [];
|
||||
for (const table of item.dataLoader.visData.tables) {
|
||||
columns.push(...table.columns.map(t => t.name));
|
||||
}
|
||||
: false;
|
||||
}
|
||||
|
||||
return !!(((item || {}).vis || {}).searchSource || {}).rawResponse
|
||||
? {
|
||||
rawResponse: item.vis.searchSource.rawResponse,
|
||||
title:
|
||||
item.vis.title ||
|
||||
item.dataLoader.previousVisState.title ||
|
||||
'Table',
|
||||
columns
|
||||
}
|
||||
: false;
|
||||
});
|
||||
|
||||
if (this.list && this.list.length) {
|
||||
const visualization = this.list[0].vis;
|
||||
// Parse applied filters for the first visualization
|
||||
const filters = visualization.API.queryFilter.getFilters();
|
||||
|
||||
// Parse current time range
|
||||
const { from, to } = visualization.filters.timeRange;
|
||||
const { from, to } = visualization.API.timeFilter.getTime();
|
||||
const { query } = visualization.searchSource._fields.query;
|
||||
// Parse applied filters for the first visualization
|
||||
const filters = visualization.searchSource._fields.filter;
|
||||
|
||||
Object.assign(appliedFilters, {
|
||||
filters,
|
||||
@ -107,7 +109,7 @@ export class VisHandlers {
|
||||
item.vis &&
|
||||
item.vis.title !== 'Agents status' &&
|
||||
((item.dataLoader || {}).previousVisState || {}).title !==
|
||||
'Agents status' &&
|
||||
'Agents status' &&
|
||||
item.vis.searchSource &&
|
||||
item.vis.searchSource.rawResponse &&
|
||||
item.vis.searchSource.rawResponse.hits &&
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -11,14 +11,14 @@
|
||||
*/
|
||||
import $ from 'jquery';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { getVisualizeLoader } from 'ui/visualize/loader';
|
||||
import { getVisualizeLoader } from './loader';
|
||||
import { timefilter } from 'ui/timefilter';
|
||||
import dateMath from '@elastic/datemath';
|
||||
|
||||
const app = uiModules.get('app/wazuh', []);
|
||||
let lockFields = false;
|
||||
|
||||
app.directive('kbnVis', function () {
|
||||
app.directive('kbnVis', function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
@ -160,18 +160,9 @@ app.directive('kbnVis', function () {
|
||||
timeRange:
|
||||
isAgentStatus && timeFilterSeconds < 900
|
||||
? { from: 'now-15m', to: 'now', mode: 'quick' }
|
||||
: timefilter.getTime(),
|
||||
filters: isAgentStatus ? [] : discoverList[1] || []
|
||||
: timefilter.getTime()
|
||||
});
|
||||
|
||||
if (!isAgentStatus) {
|
||||
visHandler.update({
|
||||
query: discoverList[0]
|
||||
|
||||
});
|
||||
}
|
||||
setSearchSource(discoverList);
|
||||
$rootScope.rendered = true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@ -228,10 +219,10 @@ app.directive('kbnVis', function () {
|
||||
const destroyAll = () => {
|
||||
try {
|
||||
visualization.destroy();
|
||||
} catch (error) { } // eslint-disable-line
|
||||
} catch (error) {} // eslint-disable-line
|
||||
try {
|
||||
visHandler.destroy();
|
||||
} catch (error) { } // eslint-disable-line
|
||||
} catch (error) {} // eslint-disable-line
|
||||
};
|
||||
|
||||
$scope.$on('$destroy', () => {
|
||||
@ -264,7 +255,7 @@ app.directive('kbnVis', function () {
|
||||
|
||||
$rootScope.loadingStatus = `Rendering visualizations... ${
|
||||
currentCompleted > 100 ? 100 : currentCompleted
|
||||
} %`;
|
||||
} %`;
|
||||
|
||||
if (currentCompleted >= 100) {
|
||||
$rootScope.rendered = true;
|
||||
|
548
public/kibana-integrations/loader/embedded_visualize_handler.ts
Normal file
548
public/kibana-integrations/loader/embedded_visualize_handler.ts
Normal file
@ -0,0 +1,548 @@
|
||||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. 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.
|
||||
*/
|
||||
|
||||
|
||||
// @ts-ignore
|
||||
import { EventEmitter } from 'events';
|
||||
import { debounce, forEach, get, isEqual } from 'lodash';
|
||||
import * as Rx from 'rxjs';
|
||||
import { share } from 'rxjs/operators';
|
||||
// @ts-ignore
|
||||
import { i18n } from '@kbn/i18n';
|
||||
// @ts-ignore
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
// @ts-ignore
|
||||
import { registries } from 'plugins/interpreter/registries';
|
||||
// @ts-ignore
|
||||
import { Inspector } from 'ui/inspector';
|
||||
// @ts-ignore
|
||||
import { Adapters } from 'ui/inspector/types';
|
||||
// @ts-ignore
|
||||
import { PersistedState } from 'ui/persisted_state';
|
||||
// @ts-ignore
|
||||
import { IPrivate } from 'ui/private';
|
||||
// @ts-ignore
|
||||
import { RenderCompleteHelper } from 'ui/render_complete';
|
||||
// @ts-ignore
|
||||
import { AppState } from 'ui/state_management/app_state';
|
||||
// @ts-ignore
|
||||
import { timefilter } from 'ui/timefilter';
|
||||
// @ts-ignore
|
||||
import { RequestHandlerParams, Vis } from 'ui/vis';
|
||||
// @ts-ignore untyped dependency
|
||||
import { VisFiltersProvider } from 'ui/vis/vis_filters';
|
||||
// @ts-ignore
|
||||
import { PipelineDataLoader } from 'ui/visualize/loader/pipeline_data_loader';
|
||||
import { visualizationLoader } from 'ui/visualize/loader';
|
||||
import { VisualizeDataLoader } from './visualize_data_loader';
|
||||
import { onlyDisabledFiltersChanged } from 'plugins/data';
|
||||
|
||||
import { DataAdapter, RequestAdapter } from 'ui/inspector/adapters';
|
||||
|
||||
import { getTableAggs } from 'ui/visualize/loader/pipeline_helpers/utilities';
|
||||
import {
|
||||
VisResponseData,
|
||||
VisSavedObject,
|
||||
VisualizeLoaderParams,
|
||||
VisualizeUpdateParams,
|
||||
} from 'ui/visualize/loader';
|
||||
// @ts-ignore
|
||||
import { queryGeohashBounds } from 'ui/visualize/loader/utils';
|
||||
|
||||
interface EmbeddedVisualizeHandlerParams extends VisualizeLoaderParams {
|
||||
Private: IPrivate;
|
||||
queryFilter: any;
|
||||
autoFetch?: boolean;
|
||||
pipelineDataLoader?: boolean;
|
||||
}
|
||||
|
||||
const RENDER_COMPLETE_EVENT = 'render_complete';
|
||||
const DATA_SHARED_ITEM = 'data-shared-item';
|
||||
const LOADING_ATTRIBUTE = 'data-loading';
|
||||
const RENDERING_COUNT_ATTRIBUTE = 'data-rendering-count';
|
||||
|
||||
/**
|
||||
* A handler to the embedded visualization. It offers several methods to interact
|
||||
* with the visualization.
|
||||
*/
|
||||
export class EmbeddedVisualizeHandler {
|
||||
/**
|
||||
* This observable will emit every time new data is loaded for the
|
||||
* visualization. The emitted value is the loaded data after it has
|
||||
* been transformed by the visualization's response handler.
|
||||
* This should not be used by any plugin.
|
||||
* @ignore
|
||||
*/
|
||||
public readonly data$: Rx.Observable<any>;
|
||||
public readonly inspectorAdapters: Adapters = {};
|
||||
private vis: Vis;
|
||||
private handlers: any;
|
||||
private loaded: boolean = false;
|
||||
private destroyed: boolean = false;
|
||||
private pipelineDataLoader: boolean = false;
|
||||
|
||||
private listeners = new EventEmitter();
|
||||
private firstRenderComplete: Promise<void>;
|
||||
private renderCompleteHelper: RenderCompleteHelper;
|
||||
private shouldForceNextFetch: boolean = false;
|
||||
private debouncedFetchAndRender = debounce(() => {
|
||||
if (this.destroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const forceFetch = this.shouldForceNextFetch;
|
||||
this.shouldForceNextFetch = false;
|
||||
this.fetch(forceFetch).then(this.render);
|
||||
}, 100);
|
||||
|
||||
private dataLoaderParams: RequestHandlerParams;
|
||||
private readonly appState?: AppState;
|
||||
private uiState: PersistedState;
|
||||
private dataLoader: VisualizeDataLoader | PipelineDataLoader;
|
||||
private dataSubject: Rx.Subject<any>;
|
||||
private actions: any = {};
|
||||
private events$: Rx.Observable<any>;
|
||||
private autoFetch: boolean;
|
||||
private abortController?: AbortController;
|
||||
private autoRefreshFetchSubscription: Rx.Subscription | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly element: HTMLElement,
|
||||
savedObject: VisSavedObject,
|
||||
params: EmbeddedVisualizeHandlerParams
|
||||
) {
|
||||
const { searchSource, vis } = savedObject;
|
||||
|
||||
const {
|
||||
appState,
|
||||
uiState,
|
||||
queryFilter,
|
||||
timeRange,
|
||||
filters,
|
||||
query,
|
||||
autoFetch = true,
|
||||
pipelineDataLoader = false,
|
||||
Private,
|
||||
} = params;
|
||||
|
||||
this.dataLoaderParams = {
|
||||
searchSource,
|
||||
timeRange,
|
||||
query,
|
||||
queryFilter,
|
||||
filters,
|
||||
uiState,
|
||||
aggs: vis.getAggConfig(),
|
||||
forceFetch: false,
|
||||
};
|
||||
|
||||
this.pipelineDataLoader = pipelineDataLoader;
|
||||
|
||||
// Listen to the first RENDER_COMPLETE_EVENT to resolve this promise
|
||||
this.firstRenderComplete = new Promise(resolve => {
|
||||
this.listeners.once(RENDER_COMPLETE_EVENT, resolve);
|
||||
});
|
||||
|
||||
element.setAttribute(LOADING_ATTRIBUTE, '');
|
||||
element.setAttribute(DATA_SHARED_ITEM, '');
|
||||
element.setAttribute(RENDERING_COUNT_ATTRIBUTE, '0');
|
||||
|
||||
element.addEventListener('renderComplete', this.onRenderCompleteListener);
|
||||
|
||||
this.autoFetch = autoFetch;
|
||||
this.appState = appState;
|
||||
this.vis = vis;
|
||||
if (uiState) {
|
||||
vis._setUiState(uiState);
|
||||
}
|
||||
this.uiState = this.vis.getUiState();
|
||||
|
||||
this.handlers = {
|
||||
vis: this.vis,
|
||||
uiState: this.uiState,
|
||||
onDestroy: (fn: () => never) => (this.handlers.destroyFn = fn),
|
||||
};
|
||||
|
||||
this.vis.on('update', this.handleVisUpdate);
|
||||
this.vis.on('reload', this.reload);
|
||||
this.uiState.on('change', this.onUiStateChange);
|
||||
if (autoFetch) {
|
||||
this.autoRefreshFetchSubscription = timefilter.getAutoRefreshFetch$().subscribe(this.reload);
|
||||
}
|
||||
|
||||
// This is a hack to give maps visualizations access to data in the
|
||||
// globalState, since they can no longer access it via searchSource.
|
||||
// TODO: Remove this as a part of elastic/kibana#30593
|
||||
this.vis.API.getGeohashBounds = () => {
|
||||
return queryGeohashBounds(this.vis, {
|
||||
filters: this.dataLoaderParams.filters,
|
||||
query: this.dataLoaderParams.query,
|
||||
});
|
||||
};
|
||||
|
||||
//WAZUH disable pipelineLoader
|
||||
this.dataLoader = new VisualizeDataLoader(vis, Private);
|
||||
const visFilters: any = Private(VisFiltersProvider);
|
||||
this.renderCompleteHelper = new RenderCompleteHelper(element);
|
||||
this.inspectorAdapters = this.getActiveInspectorAdapters();
|
||||
this.vis.openInspector = this.openInspector;
|
||||
this.vis.hasInspector = this.hasInspector;
|
||||
|
||||
// init default actions
|
||||
forEach(this.vis.type.events, (event, eventName) => {
|
||||
if (event.disabled || !eventName) {
|
||||
return;
|
||||
} else {
|
||||
this.actions[eventName] = event.defaultAction;
|
||||
}
|
||||
});
|
||||
|
||||
this.handlers.eventsSubject = new Rx.Subject();
|
||||
this.vis.eventsSubject = this.handlers.eventsSubject;
|
||||
this.events$ = this.handlers.eventsSubject.asObservable().pipe(share());
|
||||
this.events$.subscribe(event => {
|
||||
if (this.actions[event.name]) {
|
||||
event.data.aggConfigs = getTableAggs(this.vis);
|
||||
const newFilters = this.actions[event.name](event.data) || [];
|
||||
visFilters.pushFilters(newFilters);
|
||||
}
|
||||
});
|
||||
|
||||
this.dataSubject = new Rx.Subject();
|
||||
this.data$ = this.dataSubject.asObservable().pipe(share());
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update properties of the embedded visualization. This method does not allow
|
||||
* updating all initial parameters, but only a subset of the ones allowed
|
||||
* in {@link VisualizeUpdateParams}.
|
||||
*
|
||||
* @param params The parameters that should be updated.
|
||||
*/
|
||||
public update(params: VisualizeUpdateParams = {}) {
|
||||
// Apply data- attributes to the element if specified
|
||||
const dataAttrs = params.dataAttrs;
|
||||
if (dataAttrs) {
|
||||
Object.keys(dataAttrs).forEach(key => {
|
||||
if (dataAttrs[key] === null) {
|
||||
this.element.removeAttribute(`data-${key}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.element.setAttribute(`data-${key}`, dataAttrs[key]);
|
||||
});
|
||||
}
|
||||
|
||||
let fetchRequired = false;
|
||||
if (params.hasOwnProperty('timeRange')) {
|
||||
fetchRequired = true;
|
||||
this.dataLoaderParams.timeRange = params.timeRange;
|
||||
}
|
||||
if (params.hasOwnProperty('filters')) {
|
||||
fetchRequired = true;
|
||||
this.dataLoaderParams.filters = params.filters;
|
||||
}
|
||||
if (params.hasOwnProperty('query')) {
|
||||
fetchRequired = true;
|
||||
this.dataLoaderParams.query = params.query;
|
||||
}
|
||||
|
||||
if (fetchRequired) {
|
||||
this.fetchAndRender();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the underlying Angular scope of the visualization. This should be
|
||||
* called whenever you remove the visualization.
|
||||
*/
|
||||
public destroy(): void {
|
||||
this.destroyed = true;
|
||||
this.cancel();
|
||||
this.debouncedFetchAndRender.cancel();
|
||||
if (this.autoFetch) {
|
||||
if (this.autoRefreshFetchSubscription) this.autoRefreshFetchSubscription.unsubscribe();
|
||||
}
|
||||
this.vis.removeListener('reload', this.reload);
|
||||
this.vis.removeListener('update', this.handleVisUpdate);
|
||||
this.element.removeEventListener('renderComplete', this.onRenderCompleteListener);
|
||||
this.uiState.off('change', this.onUiStateChange);
|
||||
visualizationLoader.destroy(this.element);
|
||||
this.renderCompleteHelper.destroy();
|
||||
if (this.handlers.destroyFn) {
|
||||
this.handlers.destroyFn();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the actual DOM element (wrapped in jQuery) of the rendered visualization.
|
||||
* This is especially useful if you used `append: true` in the parameters where
|
||||
* the visualization will be appended to the specified container.
|
||||
*/
|
||||
public getElement(): HTMLElement {
|
||||
return this.element;
|
||||
}
|
||||
|
||||
/**
|
||||
* renders visualization with provided data
|
||||
* @param response: visualization data
|
||||
*/
|
||||
public render = (response: VisResponseData | null = null): void => {
|
||||
const executeRenderer = this.rendererProvider(response);
|
||||
if (!executeRenderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: we have this weird situation when we need to render first,
|
||||
// and then we call fetch and render... we need to get rid of that.
|
||||
executeRenderer().then(() => {
|
||||
if (!this.loaded) {
|
||||
this.loaded = true;
|
||||
if (this.autoFetch) {
|
||||
this.fetchAndRender();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens the inspector for the embedded visualization. This will return an
|
||||
* handler to the inspector to close and interact with it.
|
||||
* @return An inspector session to interact with the opened inspector.
|
||||
*/
|
||||
public openInspector = () => {
|
||||
return Inspector.open(this.inspectorAdapters, {
|
||||
title: this.vis.title,
|
||||
});
|
||||
};
|
||||
|
||||
public hasInspector = () => {
|
||||
return Inspector.isAvailable(this.inspectorAdapters);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a promise, that will resolve (without a value) once the first rendering of
|
||||
* the visualization has finished. If you want to listen to consecutive rendering
|
||||
* events, look into the `addRenderCompleteListener` method.
|
||||
*
|
||||
* @returns Promise, that resolves as soon as the visualization is done rendering
|
||||
* for the first time.
|
||||
*/
|
||||
public whenFirstRenderComplete(): Promise<void> {
|
||||
return this.firstRenderComplete;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener to be called whenever the visualization finished rendering.
|
||||
* This can be called multiple times, when the visualization rerenders, e.g. due
|
||||
* to new data.
|
||||
*
|
||||
* @param {function} listener The listener to be notified about complete renders.
|
||||
*/
|
||||
public addRenderCompleteListener(listener: () => void) {
|
||||
this.listeners.addListener(RENDER_COMPLETE_EVENT, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a previously registered render complete listener from this handler.
|
||||
* This listener will no longer be called when the visualization finished rendering.
|
||||
*
|
||||
* @param {function} listener The listener to remove from this handler.
|
||||
*/
|
||||
public removeRenderCompleteListener(listener: () => void) {
|
||||
this.listeners.removeListener(RENDER_COMPLETE_EVENT, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force the fetch of new data and renders the chart again.
|
||||
*/
|
||||
public reload = () => {
|
||||
this.fetchAndRender(true);
|
||||
};
|
||||
|
||||
private incrementRenderingCount = () => {
|
||||
const renderingCount = Number(this.element.getAttribute(RENDERING_COUNT_ATTRIBUTE) || 0);
|
||||
this.element.setAttribute(RENDERING_COUNT_ATTRIBUTE, `${renderingCount + 1}`);
|
||||
};
|
||||
|
||||
private onRenderCompleteListener = () => {
|
||||
this.listeners.emit(RENDER_COMPLETE_EVENT);
|
||||
this.element.removeAttribute(LOADING_ATTRIBUTE);
|
||||
this.incrementRenderingCount();
|
||||
};
|
||||
|
||||
private onUiStateChange = () => {
|
||||
this.fetchAndRender();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an object of all inspectors for this vis object.
|
||||
* This must only be called after this.type has properly be initialized,
|
||||
* since we need to read out data from the the vis type to check which
|
||||
* inspectors are available.
|
||||
*/
|
||||
private getActiveInspectorAdapters = (): Adapters => {
|
||||
const adapters: Adapters = {};
|
||||
const { inspectorAdapters: typeAdapters } = this.vis.type;
|
||||
|
||||
// Add the requests inspector adapters if the vis type explicitly requested it via
|
||||
// inspectorAdapters.requests: true in its definition or if it's using the courier
|
||||
// request handler, since that will automatically log its requests.
|
||||
if ((typeAdapters && typeAdapters.requests) || this.vis.type.requestHandler === 'courier') {
|
||||
adapters.requests = new RequestAdapter();
|
||||
}
|
||||
|
||||
// Add the data inspector adapter if the vis type requested it or if the
|
||||
// vis is using courier, since we know that courier supports logging
|
||||
// its data.
|
||||
if ((typeAdapters && typeAdapters.data) || this.vis.type.requestHandler === 'courier') {
|
||||
adapters.data = new DataAdapter();
|
||||
}
|
||||
|
||||
// Add all inspectors, that are explicitly registered with this vis type
|
||||
if (typeAdapters && typeAdapters.custom) {
|
||||
Object.entries(typeAdapters.custom).forEach(([key, Adapter]) => {
|
||||
adapters[key] = new (Adapter as any)();
|
||||
});
|
||||
}
|
||||
|
||||
return adapters;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches new data and renders the chart. This will happen debounced for a couple
|
||||
* of milliseconds, to bundle fast successive calls into one fetch and render,
|
||||
* e.g. while resizing the window, this will be triggered constantly on the resize
|
||||
* event.
|
||||
*
|
||||
* @param forceFetch=false Whether the request handler should be signaled to forceFetch
|
||||
* (i.e. ignore caching in case it supports it). If at least one call to this
|
||||
* passed `true` the debounced fetch and render will be a force fetch.
|
||||
*/
|
||||
private fetchAndRender = (forceFetch = false): void => {
|
||||
this.shouldForceNextFetch = forceFetch || this.shouldForceNextFetch;
|
||||
this.element.setAttribute(LOADING_ATTRIBUTE, '');
|
||||
this.debouncedFetchAndRender();
|
||||
};
|
||||
|
||||
private handleVisUpdate = () => {
|
||||
if (this.appState) {
|
||||
this.appState.vis = this.vis.getState();
|
||||
this.appState.save();
|
||||
}
|
||||
|
||||
this.fetchAndRender();
|
||||
};
|
||||
|
||||
private cancel = () => {
|
||||
if (this.abortController) this.abortController.abort();
|
||||
};
|
||||
|
||||
private fetch = (forceFetch: boolean = false) => {
|
||||
this.cancel();
|
||||
this.abortController = new AbortController();
|
||||
this.dataLoaderParams.abortSignal = this.abortController.signal;
|
||||
this.dataLoaderParams.aggs = this.vis.getAggConfig();
|
||||
this.dataLoaderParams.forceFetch = forceFetch;
|
||||
this.dataLoaderParams.inspectorAdapters = this.inspectorAdapters;
|
||||
|
||||
this.vis.filters = { timeRange: this.dataLoaderParams.timeRange };
|
||||
this.vis.requestError = undefined;
|
||||
this.vis.showRequestError = false;
|
||||
|
||||
return (
|
||||
this.dataLoader
|
||||
// Don't pass in this.dataLoaderParams directly because it may be modified async in another
|
||||
// call to fetch before the previous one has completed
|
||||
.fetch({ ...this.dataLoaderParams })
|
||||
.then(data => {
|
||||
// Pipeline responses never throw errors, so we need to check for
|
||||
// `type: 'error'`, and then throw so it can be caught below.
|
||||
// TODO: We should revisit this after we have fully migrated
|
||||
// to the new expression pipeline infrastructure.
|
||||
if (data && data.type === 'error') {
|
||||
throw data.error;
|
||||
}
|
||||
|
||||
if (data && data.value) {
|
||||
this.dataSubject.next(data.value);
|
||||
}
|
||||
return data;
|
||||
})
|
||||
.catch(this.handleDataLoaderError)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* When dataLoader returns an error, we need to make sure it surfaces in the UI.
|
||||
*
|
||||
* TODO: Eventually we should add some custom error messages for issues that are
|
||||
* frequently encountered by users.
|
||||
*/
|
||||
private handleDataLoaderError = (error: any): void => {
|
||||
// If the data loader was aborted then no need to surface this error in the UI
|
||||
if (error && error.name === 'AbortError') return;
|
||||
|
||||
// TODO: come up with a general way to cancel execution of pipeline expressions.
|
||||
if (this.dataLoaderParams.searchSource && this.dataLoaderParams.searchSource.cancelQueued) {
|
||||
this.dataLoaderParams.searchSource.cancelQueued();
|
||||
}
|
||||
|
||||
this.vis.requestError = error;
|
||||
this.vis.showRequestError =
|
||||
error.type && ['NO_OP_SEARCH_STRATEGY', 'UNSUPPORTED_QUERY'].includes(error.type);
|
||||
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('common.ui.visualize.dataLoaderError', {
|
||||
defaultMessage: 'Error in visualization',
|
||||
}),
|
||||
text: error.message,
|
||||
});
|
||||
};
|
||||
|
||||
private rendererProvider = (response: VisResponseData | null) => {
|
||||
let renderer: any = null;
|
||||
let args: any[] = [];
|
||||
|
||||
if (this.pipelineDataLoader) {
|
||||
renderer = registries.renderers.get(get(response || {}, 'as', 'visualization'));
|
||||
args = [this.element, get(response, 'value', { visType: this.vis.type.name }), this.handlers];
|
||||
} else {
|
||||
renderer = visualizationLoader;
|
||||
args = [
|
||||
this.element,
|
||||
this.vis,
|
||||
get(response, 'value.visData', null),
|
||||
get(response, 'value.visConfig', this.vis.params),
|
||||
this.uiState,
|
||||
{
|
||||
listenOnChange: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (!renderer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return () => renderer.render(...args);
|
||||
};
|
||||
}
|
20
public/kibana-integrations/loader/index.ts
Normal file
20
public/kibana-integrations/loader/index.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. 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.
|
||||
*/
|
||||
|
||||
export * from './visualize_loader';
|
135
public/kibana-integrations/loader/visualize_data_loader.ts
Normal file
135
public/kibana-integrations/loader/visualize_data_loader.ts
Normal file
@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. 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.
|
||||
*/
|
||||
// @ts-ignore
|
||||
import { isEqual } from 'lodash';
|
||||
// @ts-ignore
|
||||
import { getVisParams } from 'ui/visualize/loader/pipeline_helpers/build_pipeline';
|
||||
// @ts-ignore
|
||||
import { VisRequestHandlersRegistryProvider as RequestHandlersProvider } from 'ui/registry/vis_request_handlers';
|
||||
// @ts-ignore
|
||||
import { VisResponseHandlersRegistryProvider as ResponseHandlerProvider } from 'ui/registry/vis_response_handlers';
|
||||
// @ts-ignore
|
||||
import { IPrivate } from 'ui/private';
|
||||
// @ts-ignore
|
||||
import {
|
||||
RequestHandler,
|
||||
RequestHandlerDescription,
|
||||
RequestHandlerParams,
|
||||
ResponseHandler,
|
||||
ResponseHandlerDescription,
|
||||
Vis,
|
||||
// @ts-ignore
|
||||
} from 'ui/vis';
|
||||
|
||||
import { VisResponseData } from 'ui/visualize/loader';
|
||||
|
||||
function getHandler<T extends RequestHandler | ResponseHandler>(
|
||||
from: Array<{ name: string; handler: T }>,
|
||||
type: string | T
|
||||
): T {
|
||||
if (typeof type === 'function') {
|
||||
return type;
|
||||
}
|
||||
const handlerDesc = from.find(handler => handler.name === type);
|
||||
if (!handlerDesc) {
|
||||
throw new Error(`Could not find handler "${type}".`);
|
||||
}
|
||||
return handlerDesc.handler;
|
||||
}
|
||||
|
||||
export class VisualizeDataLoader {
|
||||
private requestHandler: RequestHandler;
|
||||
private responseHandler: ResponseHandler;
|
||||
|
||||
private visData: any;
|
||||
private previousVisState: any;
|
||||
private previousRequestHandlerResponse: any;
|
||||
|
||||
constructor(private readonly vis: Vis, Private: IPrivate) {
|
||||
const { requestHandler, responseHandler } = vis.type;
|
||||
|
||||
const requestHandlers: RequestHandlerDescription[] = Private(RequestHandlersProvider);
|
||||
const responseHandlers: ResponseHandlerDescription[] = Private(ResponseHandlerProvider);
|
||||
this.requestHandler = getHandler(requestHandlers, requestHandler);
|
||||
this.responseHandler = getHandler(responseHandlers, responseHandler);
|
||||
}
|
||||
|
||||
public async fetch(params: RequestHandlerParams): Promise<VisResponseData | void> {
|
||||
// add necessary params to vis object (dimensions, bucket, metric, etc)
|
||||
const visParams = await getVisParams(this.vis, {
|
||||
searchSource: params.searchSource,
|
||||
timeRange: params.timeRange,
|
||||
});
|
||||
|
||||
const filters = params.filters || [];
|
||||
const savedFilters = params.searchSource.getField('filter') || [];
|
||||
|
||||
const query = params.query || params.searchSource.getField('query');
|
||||
|
||||
// searchSource is only there for courier request handler
|
||||
const requestHandlerResponse = await this.requestHandler({
|
||||
partialRows: this.vis.params.partialRows || this.vis.type.requiresPartialRows,
|
||||
metricsAtAllLevels: this.vis.isHierarchical(),
|
||||
visParams,
|
||||
...params,
|
||||
query,
|
||||
filters: filters.concat(savedFilters).filter(f => !f.meta.disabled),
|
||||
});
|
||||
|
||||
// No need to call the response handler when there have been no data nor has there been changes
|
||||
// in the vis-state (response handler does not depend on uiState)
|
||||
const canSkipResponseHandler =
|
||||
this.previousRequestHandlerResponse &&
|
||||
this.previousRequestHandlerResponse === requestHandlerResponse &&
|
||||
this.previousVisState &&
|
||||
isEqual(this.previousVisState, this.vis.getState());
|
||||
|
||||
this.previousVisState = this.vis.getState();
|
||||
this.previousRequestHandlerResponse = requestHandlerResponse;
|
||||
|
||||
if (!canSkipResponseHandler) {
|
||||
this.visData = await Promise.resolve(
|
||||
this.responseHandler(requestHandlerResponse, visParams.dimensions)
|
||||
);
|
||||
}
|
||||
|
||||
const valueAxes = (visParams || {}).valueAxes || false;
|
||||
const hasSeries = ((this.visData || {}).series || []).length;
|
||||
if (valueAxes && hasSeries) {
|
||||
if (visParams.type !== 'area') {
|
||||
visParams.valueAxes.forEach((axis: { scale: { max: number; }; }, idx: string | number) => {
|
||||
const maxValue = Math.max.apply(Math, this.visData.series[idx].values.map((x: { y: any; }) => { return x.y; }));
|
||||
const lengthMaxValue = maxValue.toString().length;
|
||||
const addTo = parseInt('1' + '0'.repeat(lengthMaxValue - 1));
|
||||
axis.scale.max = maxValue + (addTo / 2);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
as: 'visualization',
|
||||
value: {
|
||||
visType: this.vis.type.name,
|
||||
visData: this.visData,
|
||||
visConfig: visParams,
|
||||
params: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
171
public/kibana-integrations/loader/visualize_loader.ts
Normal file
171
public/kibana-integrations/loader/visualize_loader.ts
Normal file
@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* IMPORTANT: If you make changes to this API, please make sure to check that
|
||||
* the docs (docs/development/visualize/development-create-visualization.asciidoc)
|
||||
* are up to date.
|
||||
*/
|
||||
|
||||
// @ts-ignore
|
||||
import chrome from 'ui/chrome';
|
||||
// @ts-ignore
|
||||
import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter';
|
||||
// @ts-ignore
|
||||
import { IPrivate } from 'ui/private';
|
||||
import { EmbeddedVisualizeHandler } from './embedded_visualize_handler';
|
||||
import { VisSavedObject, VisualizeLoaderParams } from 'ui/visualize/loader';
|
||||
|
||||
export class VisualizeLoader {
|
||||
constructor(
|
||||
private readonly savedVisualizations: any,
|
||||
private readonly pipelineDataLoader: boolean,
|
||||
private readonly Private: IPrivate
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Renders a saved visualization specified by its id into a DOM element.
|
||||
*
|
||||
* @param element The DOM element to render the visualization into.
|
||||
* You can alternatively pass a jQuery element instead.
|
||||
* @param id The id of the saved visualization. This is the id of the
|
||||
* saved object that is stored in the .kibana index.
|
||||
* @param params A list of parameters that will influence rendering.
|
||||
*
|
||||
* @return A promise that resolves to the
|
||||
* handler for this visualization as soon as the saved object could be found.
|
||||
*/
|
||||
public async embedVisualizationWithId(
|
||||
element: HTMLElement,
|
||||
savedVisualizationId: string,
|
||||
params: VisualizeLoaderParams
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.savedVisualizations.get(savedVisualizationId).then((savedObj: VisSavedObject) => {
|
||||
const handler = this.renderVis(element, savedObj, params);
|
||||
resolve(handler);
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a saved visualization specified by its savedObject into a DOM element.
|
||||
* In most of the cases you will need this method, since it allows you to specify
|
||||
* filters, handlers, queries, etc. on the savedObject before rendering.
|
||||
*
|
||||
* We do not encourage you to use this method, since it will most likely be changed
|
||||
* or removed in a future version of Kibana. Rather embed a visualization by its id
|
||||
* via the {@link #embedVisualizationWithId} method.
|
||||
*
|
||||
* @deprecated You should rather embed by id, since this method will be removed in the future.
|
||||
* @param element The DOM element to render the visualization into.
|
||||
* You can alternatively pass a jQuery element instead.
|
||||
* @param savedObj The savedObject as it could be retrieved by the
|
||||
* `savedVisualizations` service.
|
||||
* @param params A list of parameters that will influence rendering.
|
||||
*
|
||||
* @return The handler to the visualization.
|
||||
*/
|
||||
public embedVisualizationWithSavedObject(
|
||||
el: HTMLElement,
|
||||
savedObj: VisSavedObject,
|
||||
params: VisualizeLoaderParams
|
||||
) {
|
||||
return this.renderVis(el, savedObj, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise, that resolves to a list of all saved visualizations.
|
||||
*
|
||||
* @return Resolves with a list of all saved visualizations as
|
||||
* returned by the `savedVisualizations` service in Kibana.
|
||||
*/
|
||||
public getVisualizationList(): Promise<any[]> {
|
||||
return this.savedVisualizations.find().then((result: any) => result.hits);
|
||||
}
|
||||
|
||||
private renderVis(
|
||||
container: HTMLElement,
|
||||
savedObj: VisSavedObject,
|
||||
params: VisualizeLoaderParams
|
||||
) {
|
||||
const { vis, description, searchSource } = savedObj;
|
||||
|
||||
vis.description = description;
|
||||
vis.searchSource = searchSource;
|
||||
|
||||
if (!params.append) {
|
||||
container.innerHTML = '';
|
||||
}
|
||||
|
||||
const element = document.createElement('div');
|
||||
element.className = 'visualize';
|
||||
element.setAttribute('data-test-subj', 'visualizationLoader');
|
||||
container.appendChild(element);
|
||||
// We need the container to have display: flex so visualization will render correctly
|
||||
container.style.display = 'flex';
|
||||
|
||||
// If params specified cssClass, we will set this to the element.
|
||||
if (params.cssClass) {
|
||||
params.cssClass.split(' ').forEach(cssClass => {
|
||||
element.classList.add(cssClass);
|
||||
});
|
||||
}
|
||||
|
||||
// Apply data- attributes to the element if specified
|
||||
const dataAttrs = params.dataAttrs;
|
||||
if (dataAttrs) {
|
||||
Object.keys(dataAttrs).forEach(key => {
|
||||
element.setAttribute(`data-${key}`, dataAttrs[key]);
|
||||
});
|
||||
}
|
||||
|
||||
const handlerParams = {
|
||||
...params,
|
||||
// lets add query filter angular service to the params
|
||||
queryFilter: this.Private(FilterBarQueryFilterProvider),
|
||||
// lets add Private to the params, we'll need to pass it to visualize later
|
||||
Private: this.Private,
|
||||
pipelineDataLoader: this.pipelineDataLoader,
|
||||
};
|
||||
|
||||
return new EmbeddedVisualizeHandler(element, savedObj, handlerParams);
|
||||
}
|
||||
}
|
||||
|
||||
function VisualizeLoaderProvider(
|
||||
savedVisualizations: any,
|
||||
interpreterConfig: any,
|
||||
Private: IPrivate
|
||||
) {
|
||||
return new VisualizeLoader(savedVisualizations, interpreterConfig.enableInVisualize, Private);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise, that resolves with the visualize loader, once it's ready.
|
||||
* @return A promise, that resolves to the visualize loader.
|
||||
*/
|
||||
function getVisualizeLoader(): Promise<VisualizeLoader> {
|
||||
return chrome.dangerouslyGetActiveInjector().then($injector => {
|
||||
const Private: IPrivate = $injector.get('Private');
|
||||
return Private(VisualizeLoaderProvider);
|
||||
});
|
||||
}
|
||||
|
||||
export { getVisualizeLoader, VisualizeLoaderProvider };
|
@ -38,16 +38,16 @@ export class SavedObjectLoader {
|
||||
this.loaderProperties = {
|
||||
name: `${this.lowercaseType}s`,
|
||||
noun: StringUtils.upperFirst(this.type),
|
||||
nouns: `${this.lowercaseType}s`,
|
||||
nouns: `${this.lowercaseType}s`
|
||||
};
|
||||
|
||||
this.savedObjectsClient = savedObjectClient;
|
||||
}
|
||||
|
||||
// Fake async function, only to resolve a promise
|
||||
async processFunc() {
|
||||
return;
|
||||
}
|
||||
// Fake async function, only to resolve a promise
|
||||
async processFunc() {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a saved object by id. Returns a promise that completes when the object finishes
|
||||
@ -148,13 +148,14 @@ export class SavedObjectLoader {
|
||||
page: 1,
|
||||
searchFields: ['title^3', 'description'],
|
||||
defaultSearchOperator: 'AND',
|
||||
fields,
|
||||
fields
|
||||
})
|
||||
.then((resp) => {
|
||||
.then(resp => {
|
||||
return {
|
||||
total: resp.total,
|
||||
hits: resp.savedObjects
|
||||
.map((savedObject) => this.mapSavedObjectApiHits(savedObject))
|
||||
hits: resp.savedObjects.map(savedObject =>
|
||||
this.mapSavedObjectApiHits(savedObject)
|
||||
)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -1,30 +1,23 @@
|
||||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. 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
|
||||
* Author: Elasticsearch B.V.
|
||||
* Updated by Wazuh, Inc.
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Copyright (C) 2015-2019 Wazuh, Inc.
|
||||
*
|
||||
* 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.
|
||||
* 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 'plugins/kibana/visualize/saved_visualizations/_saved_vis';
|
||||
import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { SavedObjectLoader } from './saved-object-loader';
|
||||
import { SavedObjectsClientProvider } from 'ui/saved_objects';
|
||||
import { savedObjectManagementRegistry } from 'plugins/kibana/management/saved_object_registry';
|
||||
import { start as visualizations } from 'plugins/kibana/visualize/../../../visualizations/public/np_ready/public/legacy';
|
||||
import { createVisualizeEditUrl } from 'plugins/kibana/visualize/visualize_constants';
|
||||
import { findListItems } from 'plugins/kibana/visualize/saved_visualizations/find_list_items';
|
||||
|
||||
const app = uiModules.get('app/visualize');
|
||||
|
||||
@ -32,20 +25,30 @@ const app = uiModules.get('app/visualize');
|
||||
// edited by the object editor.
|
||||
savedObjectManagementRegistry.register({
|
||||
service: 'wzsavedVisualizations',
|
||||
title: 'visualizations',
|
||||
title: 'visualizations'
|
||||
});
|
||||
|
||||
app.service('wzsavedVisualizations', function (SavedVis, Private, kbnUrl, chrome) {
|
||||
const visTypes = visualizations.types;
|
||||
app.service('wzsavedVisualizations', function(
|
||||
kbnIndex,
|
||||
SavedVis,
|
||||
Private,
|
||||
kbnUrl,
|
||||
$http,
|
||||
chrome
|
||||
) {
|
||||
const visTypes = Private(VisTypesRegistryProvider);
|
||||
|
||||
const savedObjectClient = Private(SavedObjectsClientProvider);
|
||||
const saveVisualizationLoader = new SavedObjectLoader(
|
||||
SavedVis,
|
||||
kbnIndex,
|
||||
kbnUrl,
|
||||
$http,
|
||||
chrome,
|
||||
savedObjectClient
|
||||
);
|
||||
|
||||
saveVisualizationLoader.mapHitSource = function (source, id) {
|
||||
saveVisualizationLoader.mapHitSource = function(source, id) {
|
||||
source.id = id;
|
||||
source.url = this.urlFor(id);
|
||||
|
||||
@ -58,36 +61,18 @@ app.service('wzsavedVisualizations', function (SavedVis, Private, kbnUrl, chrome
|
||||
} // eslint-disable-line no-empty
|
||||
}
|
||||
|
||||
if (!typeName || !visTypes.get(typeName)) {
|
||||
if (!typeName || !visTypes.byName[typeName]) {
|
||||
source.error = 'Unknown visualization type';
|
||||
return source;
|
||||
}
|
||||
|
||||
source.type = visTypes.get(typeName);
|
||||
source.savedObjectType = 'visualization';
|
||||
source.type = visTypes.byName[typeName];
|
||||
source.icon = source.type.icon;
|
||||
source.image = source.type.image;
|
||||
source.typeTitle = source.type.title;
|
||||
source.editUrl = `#${createVisualizeEditUrl(id)}`;
|
||||
|
||||
return source;
|
||||
};
|
||||
|
||||
saveVisualizationLoader.urlFor = function (id) {
|
||||
saveVisualizationLoader.urlFor = function(id) {
|
||||
return kbnUrl.eval('#/visualize/edit/{{id}}', { id: id });
|
||||
};
|
||||
|
||||
// This behaves similarly to find, except it returns visualizations that are
|
||||
// defined as appExtensions and which may not conform to type: visualization
|
||||
saveVisualizationLoader.findListItems = function (search = '', size = 100) {
|
||||
return findListItems({
|
||||
search,
|
||||
size,
|
||||
mapSavedObjectApiHits: this.mapSavedObjectApiHits.bind(this),
|
||||
savedObjectsClient: this.savedObjectsClient,
|
||||
visTypes: visualizations.types.getAliases(),
|
||||
});
|
||||
};
|
||||
|
||||
return saveVisualizationLoader;
|
||||
});
|
242
public/kibana-integrations/search-bar/filter_bar.tsx
Normal file
242
public/kibana-integrations/search-bar/filter_bar.tsx
Normal file
@ -0,0 +1,242 @@
|
||||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. 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 { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPopover } from '@elastic/eui';
|
||||
import {
|
||||
buildEmptyFilter,
|
||||
disableFilter,
|
||||
enableFilter,
|
||||
Filter,
|
||||
//pinFilter,
|
||||
toggleFilterDisabled,
|
||||
toggleFilterPinned,
|
||||
isFilterPinned,
|
||||
toggleFilterNegated,
|
||||
unpinFilter,
|
||||
} from '@kbn/es-query';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import classNames from 'classnames';
|
||||
import React, { Component } from 'react';
|
||||
import { UiSettingsClientContract } from 'src/core/public';
|
||||
import { IndexPattern } from 'ui/index_patterns';
|
||||
import { FilterEditor } from 'plugins/data/filter/filter_bar/filter_editor';
|
||||
import { FilterItem } from './filter_item';
|
||||
import { FilterOptions } from 'plugins/data/filter/filter_bar/filter_options';
|
||||
|
||||
interface Props {
|
||||
filters: Filter[];
|
||||
onFiltersUpdated: (filters: Filter[]) => void;
|
||||
className: string;
|
||||
indexPatterns: IndexPattern[];
|
||||
intl: InjectedIntl;
|
||||
uiSettings: UiSettingsClientContract;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isAddFilterPopoverOpen: boolean;
|
||||
}
|
||||
|
||||
class FilterBarUI extends Component<Props, State> {
|
||||
public state = {
|
||||
isAddFilterPopoverOpen: false,
|
||||
};
|
||||
|
||||
public render() {
|
||||
if (!this.props.uiSettings) {
|
||||
return null;
|
||||
}
|
||||
const classes = classNames('globalFilterBar', this.props.className);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
className="globalFilterGroup"
|
||||
gutterSize="none"
|
||||
alignItems="flexStart"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem className="globalFilterGroup__branch" grow={false}>
|
||||
<FilterOptions
|
||||
onEnableAll={this.onEnableAll}
|
||||
onDisableAll={this.onDisableAll}
|
||||
onPinAll={this.onPinAll}
|
||||
onUnpinAll={this.onUnpinAll}
|
||||
onToggleAllNegated={this.onToggleAllNegated}
|
||||
onToggleAllDisabled={this.onToggleAllDisabled}
|
||||
onRemoveAll={this.onRemoveAll}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem className="globalFilterGroup__filterFlexItem">
|
||||
<EuiFlexGroup
|
||||
className={classes}
|
||||
wrap={true}
|
||||
responsive={false}
|
||||
gutterSize="xs"
|
||||
alignItems="center"
|
||||
>
|
||||
{this.renderItems()}
|
||||
{this.renderAddFilter()}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
private renderItems() {
|
||||
return this.props.filters.map((filter, i) => (
|
||||
<EuiFlexItem key={i} grow={false} className="globalFilterBar__flexItem">
|
||||
<FilterItem
|
||||
id={`${i}`}
|
||||
filter={filter}
|
||||
onUpdate={newFilter => this.onUpdate(i, newFilter)}
|
||||
onRemove={() => this.onRemove(i)}
|
||||
indexPatterns={this.props.indexPatterns}
|
||||
uiSettings={this.props.uiSettings}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
));
|
||||
}
|
||||
|
||||
private renderAddFilter() {
|
||||
const isPinned = this.props.uiSettings.get('filters:pinnedByDefault');
|
||||
const [indexPattern] = this.props.indexPatterns;
|
||||
const index = indexPattern && indexPattern.id;
|
||||
const newFilter = buildEmptyFilter(isPinned, index);
|
||||
|
||||
const button = (
|
||||
<EuiButtonEmpty size="xs" onClick={this.onOpenAddFilterPopover} data-test-subj="addFilter">
|
||||
+{' '}
|
||||
<FormattedMessage
|
||||
id="data.filter.filterBar.addFilterButtonLabel"
|
||||
defaultMessage="Add filter"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
id="addFilterPopover"
|
||||
button={button}
|
||||
isOpen={this.state.isAddFilterPopoverOpen}
|
||||
closePopover={this.onCloseAddFilterPopover}
|
||||
anchorPosition="downLeft"
|
||||
withTitle
|
||||
panelPaddingSize="none"
|
||||
ownFocus={true}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<div style={{ width: 400 }}>
|
||||
<FilterEditor
|
||||
filter={newFilter}
|
||||
indexPatterns={this.props.indexPatterns}
|
||||
onSubmit={this.onAdd}
|
||||
onCancel={this.onCloseAddFilterPopover}
|
||||
key={JSON.stringify(newFilter)}
|
||||
uiSettings={this.props.uiSettings}
|
||||
/>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
private onAdd = (filter: Filter) => {
|
||||
this.onCloseAddFilterPopover();
|
||||
const filters = [...this.props.filters, filter];
|
||||
this.props.onFiltersUpdated(filters);
|
||||
};
|
||||
|
||||
private onRemove = (i: number) => {
|
||||
const filters = [...this.props.filters];
|
||||
filters.splice(i, 1);
|
||||
this.props.onFiltersUpdated(filters);
|
||||
};
|
||||
|
||||
private onUpdate = (i: number, filter: Filter) => {
|
||||
const filters = [...this.props.filters];
|
||||
filters[i] = filter;
|
||||
this.props.onFiltersUpdated(filters);
|
||||
};
|
||||
|
||||
private onEnableAll = () => {
|
||||
const filters = this.props.filters.map(enableFilter);
|
||||
this.props.onFiltersUpdated(filters);
|
||||
};
|
||||
|
||||
private onDisableAll = () => {
|
||||
const filters = this.props.filters.map(disableFilter);
|
||||
this.props.onFiltersUpdated(filters);
|
||||
};
|
||||
|
||||
private onPinAll = () => {
|
||||
const filters = this.props.filters.map(filter => {
|
||||
const shouldExclude =
|
||||
filter &&
|
||||
filter.meta &&
|
||||
typeof filter.meta.removable !== 'undefined' &&
|
||||
!filter.meta.removable;
|
||||
return isFilterPinned(filter) || shouldExclude
|
||||
? filter
|
||||
: toggleFilterPinned(filter);
|
||||
});
|
||||
|
||||
this.props.onFiltersUpdated(filters);
|
||||
};
|
||||
|
||||
private onUnpinAll = () => {
|
||||
const filters = this.props.filters.map(unpinFilter);
|
||||
this.props.onFiltersUpdated(filters);
|
||||
};
|
||||
|
||||
private onToggleAllNegated = () => {
|
||||
const filters = this.props.filters.map(toggleFilterNegated);
|
||||
this.props.onFiltersUpdated(filters);
|
||||
};
|
||||
|
||||
private onToggleAllDisabled = () => {
|
||||
const filters = this.props.filters.map(toggleFilterDisabled);
|
||||
this.props.onFiltersUpdated(filters);
|
||||
};
|
||||
|
||||
private isRemovable = item => {
|
||||
const property = ((item || {}).meta || {}).removable;
|
||||
return typeof property !== 'undefined' && !property;
|
||||
};
|
||||
|
||||
private onRemoveAll = () => {
|
||||
const filters = this.props.filters.filter(item => this.isRemovable(item));
|
||||
this.props.onFiltersUpdated(filters);
|
||||
};
|
||||
|
||||
private onOpenAddFilterPopover = () => {
|
||||
this.setState({
|
||||
isAddFilterPopoverOpen: true,
|
||||
});
|
||||
};
|
||||
|
||||
private onCloseAddFilterPopover = () => {
|
||||
this.setState({
|
||||
isAddFilterPopoverOpen: false,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const FilterBar = injectI18n(FilterBarUI);
|
231
public/kibana-integrations/search-bar/filter_item.tsx
Normal file
231
public/kibana-integrations/search-bar/filter_item.tsx
Normal file
@ -0,0 +1,231 @@
|
||||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. 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 { EuiContextMenu, EuiPopover } from '@elastic/eui';
|
||||
import {
|
||||
Filter,
|
||||
isFilterPinned,
|
||||
toggleFilterDisabled,
|
||||
toggleFilterNegated,
|
||||
toggleFilterPinned,
|
||||
} from '@kbn/es-query';
|
||||
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import classNames from 'classnames';
|
||||
import React, { Component } from 'react';
|
||||
import { UiSettingsClientContract } from 'src/core/public';
|
||||
import { IndexPattern } from 'ui/index_patterns';
|
||||
import { FilterEditor } from 'plugins/data/filter/filter_bar/filter_editor';
|
||||
import { FilterView } from './filter_view';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
filter: Filter;
|
||||
indexPatterns: IndexPattern[];
|
||||
className?: string;
|
||||
onUpdate: (filter: Filter) => void;
|
||||
onRemove: () => void;
|
||||
intl: InjectedIntl;
|
||||
uiSettings: UiSettingsClientContract;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isPopoverOpen: boolean;
|
||||
}
|
||||
|
||||
class FilterItemUI extends Component<Props, State> {
|
||||
public state = {
|
||||
isPopoverOpen: false,
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { filter, id } = this.props;
|
||||
const { negate, disabled } = filter.meta;
|
||||
|
||||
const classes = classNames(
|
||||
'globalFilterItem',
|
||||
{
|
||||
'globalFilterItem-isDisabled': disabled,
|
||||
'globalFilterItem-isPinned': isFilterPinned(filter),
|
||||
'globalFilterItem-isExcluded': negate,
|
||||
},
|
||||
this.props.className
|
||||
);
|
||||
|
||||
const dataTestSubjKey = filter.meta.key ? `filter-key-${filter.meta.key}` : '';
|
||||
const dataTestSubjValue = filter.meta.value ? `filter-value-${filter.meta.value}` : '';
|
||||
const dataTestSubjDisabled = `filter-${
|
||||
this.props.filter.meta.disabled ? 'disabled' : 'enabled'
|
||||
}`;
|
||||
|
||||
const badge = (
|
||||
<FilterView
|
||||
filter={filter}
|
||||
className={classes}
|
||||
iconOnClick={() => this.props.onRemove()}
|
||||
onClick={this.togglePopover}
|
||||
data-test-subj={`filter ${dataTestSubjDisabled} ${dataTestSubjKey} ${dataTestSubjValue}`}
|
||||
/>
|
||||
);
|
||||
|
||||
const panelTree = [
|
||||
{
|
||||
id: 0,
|
||||
items: [
|
||||
{
|
||||
name: isFilterPinned(filter)
|
||||
? this.props.intl.formatMessage({
|
||||
id: 'data.filter.filterBar.unpinFilterButtonLabel',
|
||||
defaultMessage: 'Unpin',
|
||||
})
|
||||
: this.props.intl.formatMessage({
|
||||
id: 'data.filter.filterBar.pinFilterButtonLabel',
|
||||
defaultMessage: 'Pin across all apps',
|
||||
}),
|
||||
icon: 'pin',
|
||||
onClick: () => {
|
||||
this.closePopover();
|
||||
this.onTogglePinned();
|
||||
},
|
||||
'data-test-subj': 'pinFilter',
|
||||
},
|
||||
{
|
||||
name: this.props.intl.formatMessage({
|
||||
id: 'data.filter.filterBar.editFilterButtonLabel',
|
||||
defaultMessage: 'Edit filter',
|
||||
}),
|
||||
icon: 'pencil',
|
||||
panel: 1,
|
||||
'data-test-subj': 'editFilter',
|
||||
},
|
||||
{
|
||||
name: negate
|
||||
? this.props.intl.formatMessage({
|
||||
id: 'data.filter.filterBar.includeFilterButtonLabel',
|
||||
defaultMessage: 'Include results',
|
||||
})
|
||||
: this.props.intl.formatMessage({
|
||||
id: 'data.filter.filterBar.excludeFilterButtonLabel',
|
||||
defaultMessage: 'Exclude results',
|
||||
}),
|
||||
icon: negate ? 'plusInCircle' : 'minusInCircle',
|
||||
onClick: () => {
|
||||
this.closePopover();
|
||||
this.onToggleNegated();
|
||||
},
|
||||
'data-test-subj': 'negateFilter',
|
||||
},
|
||||
{
|
||||
name: disabled
|
||||
? this.props.intl.formatMessage({
|
||||
id: 'data.filter.filterBar.enableFilterButtonLabel',
|
||||
defaultMessage: 'Re-enable',
|
||||
})
|
||||
: this.props.intl.formatMessage({
|
||||
id: 'data.filter.filterBar.disableFilterButtonLabel',
|
||||
defaultMessage: 'Temporarily disable',
|
||||
}),
|
||||
icon: `${disabled ? 'eye' : 'eyeClosed'}`,
|
||||
onClick: () => {
|
||||
this.closePopover();
|
||||
this.onToggleDisabled();
|
||||
},
|
||||
'data-test-subj': 'disableFilter',
|
||||
},
|
||||
{
|
||||
name: this.props.intl.formatMessage({
|
||||
id: 'data.filter.filterBar.deleteFilterButtonLabel',
|
||||
defaultMessage: 'Delete',
|
||||
}),
|
||||
icon: 'trash',
|
||||
onClick: () => {
|
||||
this.closePopover();
|
||||
this.props.onRemove();
|
||||
},
|
||||
'data-test-subj': 'deleteFilter',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
width: 400,
|
||||
content: (
|
||||
<div>
|
||||
<FilterEditor
|
||||
filter={filter}
|
||||
indexPatterns={this.props.indexPatterns}
|
||||
onSubmit={this.onSubmit}
|
||||
onCancel={this.closePopover}
|
||||
uiSettings={this.props.uiSettings}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id={`popoverFor_filter${id}`}
|
||||
className={`globalFilterItem__popover`}
|
||||
anchorClassName={`globalFilterItem__popoverAnchor`}
|
||||
isOpen={this.state.isPopoverOpen}
|
||||
closePopover={this.closePopover}
|
||||
button={badge}
|
||||
anchorPosition="downLeft"
|
||||
withTitle={true}
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<EuiContextMenu initialPanelId={0} panels={panelTree} />
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
|
||||
private closePopover = () => {
|
||||
this.setState({
|
||||
isPopoverOpen: false,
|
||||
});
|
||||
};
|
||||
|
||||
private togglePopover = () => {
|
||||
this.setState({
|
||||
isPopoverOpen: !this.state.isPopoverOpen,
|
||||
});
|
||||
};
|
||||
|
||||
private onSubmit = (filter: Filter) => {
|
||||
this.closePopover();
|
||||
this.props.onUpdate(filter);
|
||||
};
|
||||
|
||||
private onTogglePinned = () => {
|
||||
const filter = toggleFilterPinned(this.props.filter);
|
||||
this.props.onUpdate(filter);
|
||||
};
|
||||
|
||||
private onToggleNegated = () => {
|
||||
const filter = toggleFilterNegated(this.props.filter);
|
||||
this.props.onUpdate(filter);
|
||||
};
|
||||
|
||||
private onToggleDisabled = () => {
|
||||
const filter = toggleFilterDisabled(this.props.filter);
|
||||
this.props.onUpdate(filter);
|
||||
};
|
||||
}
|
||||
|
||||
export const FilterItem = injectI18n(FilterItemUI);
|
116
public/kibana-integrations/search-bar/filter_view.tsx
Normal file
116
public/kibana-integrations/search-bar/filter_view.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. 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 { EuiBadge } from '@elastic/eui';
|
||||
import { Filter, isFilterPinned } from '@kbn/es-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { SFC } from 'react';
|
||||
import {
|
||||
existsOperator,
|
||||
isOneOfOperator
|
||||
} from 'plugins/data/filter/filter_bar/filter_editor/lib/filter_operators';
|
||||
|
||||
interface Props {
|
||||
filter: Filter;
|
||||
[propName: string]: any;
|
||||
}
|
||||
|
||||
export const FilterView: SFC<Props> = ({ filter, iconOnClick, onClick, ...rest }: Props) => {
|
||||
let title = `Filter: ${getFilterDisplayText(filter)}. ${i18n.translate(
|
||||
'data.filter.filterBar.moreFilterActionsMessage',
|
||||
{
|
||||
defaultMessage: 'Select for more filter actions.',
|
||||
}
|
||||
)}`;
|
||||
|
||||
if (isFilterPinned(filter)) {
|
||||
title = `${i18n.translate('data.filter.filterBar.pinnedFilterPrefix', {
|
||||
defaultMessage: 'Pinned',
|
||||
})} ${title}`;
|
||||
}
|
||||
if (filter.meta.disabled) {
|
||||
title = `${i18n.translate('data.filter.filterBar.disabledFilterPrefix', {
|
||||
defaultMessage: 'Disabled',
|
||||
})} ${title}`;
|
||||
}
|
||||
|
||||
const isImplicit =
|
||||
typeof filter.meta.removable !== 'undefined' && !!!filter.meta.removable;
|
||||
|
||||
return !isImplicit ? (
|
||||
<EuiBadge
|
||||
title={title}
|
||||
iconType="cross"
|
||||
iconSide="right"
|
||||
closeButtonProps={{
|
||||
// Removing tab focus on close button because the same option can be optained through the context menu
|
||||
// Also, we may want to add a `DEL` keyboard press functionality
|
||||
tabIndex: -1,
|
||||
}}
|
||||
iconOnClick={iconOnClick}
|
||||
iconOnClickAriaLabel={i18n.translate('data.filter.filterBar.filterItemBadgeIconAriaLabel', {
|
||||
defaultMessage: 'Delete',
|
||||
})}
|
||||
onClick={onClick}
|
||||
onClickAriaLabel={i18n.translate('data.filter.filterBar.filterItemBadgeAriaLabel', {
|
||||
defaultMessage: 'Filter actions',
|
||||
})}
|
||||
{...rest}
|
||||
>
|
||||
<span>{getFilterDisplayText(filter)}</span>
|
||||
</EuiBadge>
|
||||
) : (
|
||||
<EuiBadge
|
||||
className={rest.className}
|
||||
>
|
||||
<span>{getFilterDisplayText(filter)}</span>
|
||||
</EuiBadge>
|
||||
);
|
||||
};
|
||||
|
||||
export function getFilterDisplayText(filter: Filter) {
|
||||
const prefix = filter.meta.negate
|
||||
? ` ${i18n.translate('data.filter.filterBar.negatedFilterPrefix', {
|
||||
defaultMessage: 'NOT ',
|
||||
})}`
|
||||
: '';
|
||||
|
||||
if (filter.meta.alias !== null) {
|
||||
return `${prefix}${filter.meta.alias}`;
|
||||
}
|
||||
|
||||
switch (filter.meta.type) {
|
||||
case 'exists':
|
||||
return `${prefix}${filter.meta.key} ${existsOperator.message}`;
|
||||
case 'geo_bounding_box':
|
||||
return `${prefix}${filter.meta.key}: ${filter.meta.value}`;
|
||||
case 'geo_polygon':
|
||||
return `${prefix}${filter.meta.key}: ${filter.meta.value}`;
|
||||
case 'phrase':
|
||||
return `${prefix}${filter.meta.key}: ${filter.meta.value}`;
|
||||
case 'phrases':
|
||||
return `${prefix}${filter.meta.key} ${isOneOfOperator.message} ${filter.meta.value}`;
|
||||
case 'query_string':
|
||||
return `${prefix}${filter.meta.value}`;
|
||||
case 'range':
|
||||
return `${prefix}${filter.meta.key}: ${filter.meta.value}`;
|
||||
default:
|
||||
return `${prefix}${JSON.stringify(filter.query)}`;
|
||||
}
|
||||
}
|
468
public/kibana-integrations/search-bar/search_bar.tsx
Normal file
468
public/kibana-integrations/search-bar/search_bar.tsx
Normal file
@ -0,0 +1,468 @@
|
||||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. 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 { Filter } from '@kbn/es-query';
|
||||
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import classNames from 'classnames';
|
||||
import React, { Component } from 'react';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
import { Storage } from 'ui/storage';
|
||||
import { get, isEqual } from 'lodash';
|
||||
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { CoreStart, UiSettingsClientContract, SavedObjectsClientContract } from 'src/core/public';
|
||||
import { IndexPattern, Query, QueryBar } from 'plugins/data';
|
||||
import { SavedQuery, SavedQueryAttributes } from 'plugins/data/search/search_bar/';
|
||||
import { SavedQueryMeta, SaveQueryForm } from 'plugins/data/search/search_bar/components/saved_query_management/save_query_form';
|
||||
import { SavedQueryManagementComponent } from 'plugins/data/search/search_bar/components/saved_query_management/saved_query_management_component';
|
||||
import { SavedQueryService } from 'plugins/data/search/search_bar/lib/saved_query_service';
|
||||
import { createSavedQueryService } from 'plugins/data/search/search_bar/lib/saved_query_service';
|
||||
import { FilterBar } from './filter_bar';
|
||||
|
||||
interface DateRange {
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* NgReact lib requires that changes to the props need to be made in the directive config as well
|
||||
* See [search_bar\directive\index.js] file
|
||||
*/
|
||||
export interface SearchBarProps {
|
||||
appName: string;
|
||||
intl: InjectedIntl;
|
||||
toasts: CoreStart['notifications']['toasts'];
|
||||
uiSettings: UiSettingsClientContract;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
indexPatterns?: IndexPattern[];
|
||||
// Query bar
|
||||
showQueryBar?: boolean;
|
||||
showQueryInput?: boolean;
|
||||
screenTitle?: string;
|
||||
store?: Storage;
|
||||
query?: Query;
|
||||
savedQuery?: SavedQuery;
|
||||
onQuerySubmit?: (payload: { dateRange: DateRange; query?: Query }) => void;
|
||||
// Filter bar
|
||||
showFilterBar?: boolean;
|
||||
filters?: Filter[];
|
||||
onFiltersUpdated?: (filters: Filter[]) => void;
|
||||
// Date picker
|
||||
showDatePicker?: boolean;
|
||||
dateRangeFrom?: string;
|
||||
dateRangeTo?: string;
|
||||
// Autorefresh
|
||||
isRefreshPaused?: boolean;
|
||||
refreshInterval?: number;
|
||||
showAutoRefreshOnly?: boolean;
|
||||
showSaveQuery?: boolean;
|
||||
onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void;
|
||||
onSaved?: (savedQuery: SavedQuery) => void;
|
||||
onSavedQueryUpdated?: (savedQuery: SavedQuery) => void;
|
||||
onClearSavedQuery?: () => void;
|
||||
customSubmitButton?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
savedQueryService: SavedQueryService;
|
||||
isFiltersVisible: boolean;
|
||||
showSaveQueryModal: boolean;
|
||||
showSaveNewQueryModal: boolean;
|
||||
showSavedQueryPopover: boolean;
|
||||
currentProps?: SearchBarProps;
|
||||
query?: Query;
|
||||
dateRangeFrom: string;
|
||||
dateRangeTo: string;
|
||||
}
|
||||
|
||||
class SearchBarUI extends Component<SearchBarProps, State> {
|
||||
public static defaultProps = {
|
||||
showQueryBar: true,
|
||||
showFilterBar: true,
|
||||
showDatePicker: true,
|
||||
showAutoRefreshOnly: false,
|
||||
};
|
||||
|
||||
public filterBarRef: Element | null = null;
|
||||
public filterBarWrapperRef: Element | null = null;
|
||||
|
||||
public static getDerivedStateFromProps(nextProps: SearchBarProps, prevState: State) {
|
||||
if (isEqual(prevState.currentProps, nextProps)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let nextQuery = null;
|
||||
if (nextProps.query && nextProps.query.query !== get(prevState, 'currentProps.query.query')) {
|
||||
nextQuery = {
|
||||
query: nextProps.query.query,
|
||||
language: nextProps.query.language,
|
||||
};
|
||||
} else if (
|
||||
nextProps.query &&
|
||||
prevState.query &&
|
||||
nextProps.query.language !== prevState.query.language
|
||||
) {
|
||||
nextQuery = {
|
||||
query: '',
|
||||
language: nextProps.query.language,
|
||||
};
|
||||
}
|
||||
|
||||
let nextDateRange = null;
|
||||
if (
|
||||
nextProps.dateRangeFrom !== get(prevState, 'currentProps.dateRangeFrom') ||
|
||||
nextProps.dateRangeTo !== get(prevState, 'currentProps.dateRangeTo')
|
||||
) {
|
||||
nextDateRange = {
|
||||
dateRangeFrom: nextProps.dateRangeFrom,
|
||||
dateRangeTo: nextProps.dateRangeTo,
|
||||
};
|
||||
}
|
||||
|
||||
const nextState: any = {
|
||||
currentProps: nextProps,
|
||||
};
|
||||
if (nextQuery) {
|
||||
nextState.query = nextQuery;
|
||||
}
|
||||
if (nextDateRange) {
|
||||
nextState.dateRangeFrom = nextDateRange.dateRangeFrom;
|
||||
nextState.dateRangeTo = nextDateRange.dateRangeTo;
|
||||
}
|
||||
return nextState;
|
||||
}
|
||||
|
||||
/*
|
||||
Keep the "draft" value in local state until the user actually submits the query. There are a couple advantages:
|
||||
|
||||
1. Each app doesn't have to maintain its own "draft" value if it wants to put off updating the query in app state
|
||||
until the user manually submits their changes. Most apps have watches on the query value in app state so we don't
|
||||
want to trigger those on every keypress. Also, some apps (e.g. dashboard) already juggle multiple query values,
|
||||
each with slightly different semantics and I'd rather not add yet another variable to the mix.
|
||||
|
||||
2. Changes to the local component state won't trigger an Angular digest cycle. Triggering digest cycles on every
|
||||
keypress has been a major source of performance issues for us in previous implementations of the query bar.
|
||||
See https://github.com/elastic/kibana/issues/14086
|
||||
*/
|
||||
public state = {
|
||||
isFiltersVisible: true,
|
||||
showSaveQueryModal: false,
|
||||
showSaveNewQueryModal: false,
|
||||
showSavedQueryPopover: false,
|
||||
currentProps: this.props,
|
||||
query: this.props.query ? { ...this.props.query } : undefined,
|
||||
dateRangeFrom: get(this.props, 'dateRangeFrom', 'now-15m'),
|
||||
dateRangeTo: get(this.props, 'dateRangeTo', 'now'),
|
||||
savedQueryService: createSavedQueryService(this.props.savedObjectsClient),
|
||||
};
|
||||
|
||||
public isDirty = () => {
|
||||
if (!this.props.showDatePicker && this.state.query && this.props.query) {
|
||||
return this.state.query.query !== this.props.query.query;
|
||||
}
|
||||
|
||||
return (
|
||||
(this.state.query && this.props.query && this.state.query.query !== this.props.query.query) ||
|
||||
this.state.dateRangeFrom !== this.props.dateRangeFrom ||
|
||||
this.state.dateRangeTo !== this.props.dateRangeTo
|
||||
);
|
||||
};
|
||||
|
||||
private getFilterUpdateFunction() {
|
||||
if (this.props.showFilterBar && this.props.onFiltersUpdated) {
|
||||
return this.props.onFiltersUpdated;
|
||||
}
|
||||
return (filters: Filter[]) => {};
|
||||
}
|
||||
|
||||
private shouldRenderQueryBar() {
|
||||
const showDatePicker = this.props.showDatePicker || this.props.showAutoRefreshOnly;
|
||||
const showQueryInput =
|
||||
this.props.showQueryInput && this.props.indexPatterns && this.state.query;
|
||||
return this.props.showQueryBar && (showDatePicker || showQueryInput);
|
||||
}
|
||||
|
||||
private shouldRenderFilterBar() {
|
||||
return this.props.showFilterBar && this.props.filters && this.props.indexPatterns;
|
||||
}
|
||||
|
||||
public setFilterBarHeight = () => {
|
||||
requestAnimationFrame(() => {
|
||||
const height =
|
||||
this.filterBarRef && this.state.isFiltersVisible ? this.filterBarRef.clientHeight : 0;
|
||||
if (this.filterBarWrapperRef) {
|
||||
this.filterBarWrapperRef.setAttribute('style', `height: ${height}px`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// member-ordering rules conflict with use-before-declaration rules
|
||||
/* eslint-disable */
|
||||
public ro = new ResizeObserver(this.setFilterBarHeight);
|
||||
/* eslint-enable */
|
||||
|
||||
public onSave = async (savedQueryMeta: SavedQueryMeta, saveAsNew = false) => {
|
||||
if (!this.state.query) return;
|
||||
|
||||
const savedQueryAttributes: SavedQueryAttributes = {
|
||||
title: savedQueryMeta.title,
|
||||
description: savedQueryMeta.description,
|
||||
query: this.state.query,
|
||||
};
|
||||
|
||||
if (savedQueryMeta.shouldIncludeFilters) {
|
||||
savedQueryAttributes.filters = this.props.filters;
|
||||
}
|
||||
|
||||
if (
|
||||
savedQueryMeta.shouldIncludeTimefilter &&
|
||||
this.state.dateRangeTo !== undefined &&
|
||||
this.state.dateRangeFrom !== undefined &&
|
||||
this.props.refreshInterval !== undefined &&
|
||||
this.props.isRefreshPaused !== undefined
|
||||
) {
|
||||
savedQueryAttributes.timefilter = {
|
||||
from: this.state.dateRangeFrom,
|
||||
to: this.state.dateRangeTo,
|
||||
refreshInterval: {
|
||||
value: this.props.refreshInterval,
|
||||
pause: this.props.isRefreshPaused,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
let response;
|
||||
if (this.props.savedQuery && !saveAsNew) {
|
||||
response = await this.state.savedQueryService.saveQuery(savedQueryAttributes, {
|
||||
overwrite: true,
|
||||
});
|
||||
} else {
|
||||
response = await this.state.savedQueryService.saveQuery(savedQueryAttributes);
|
||||
}
|
||||
|
||||
toastNotifications.addSuccess(`Your query "${response.attributes.title}" was saved`);
|
||||
|
||||
this.setState({
|
||||
showSaveQueryModal: false,
|
||||
showSaveNewQueryModal: false,
|
||||
});
|
||||
|
||||
if (this.props.onSaved) {
|
||||
this.props.onSaved(response);
|
||||
}
|
||||
|
||||
if (this.props.onQuerySubmit) {
|
||||
this.props.onQuerySubmit({
|
||||
query: this.state.query,
|
||||
dateRange: {
|
||||
from: this.state.dateRangeFrom,
|
||||
to: this.state.dateRangeTo,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toastNotifications.addDanger(`An error occured while saving your query: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
public onInitiateSave = () => {
|
||||
this.setState({
|
||||
showSaveQueryModal: true,
|
||||
});
|
||||
};
|
||||
|
||||
public onInitiateSaveNew = () => {
|
||||
this.setState({
|
||||
showSaveNewQueryModal: true,
|
||||
});
|
||||
};
|
||||
|
||||
public onQueryBarChange = (queryAndDateRange: { dateRange: DateRange; query?: Query }) => {
|
||||
this.setState({
|
||||
query: queryAndDateRange.query,
|
||||
dateRangeFrom: queryAndDateRange.dateRange.from,
|
||||
dateRangeTo: queryAndDateRange.dateRange.to,
|
||||
});
|
||||
};
|
||||
|
||||
public onQueryBarSubmit = (queryAndDateRange: { dateRange?: DateRange; query?: Query }) => {
|
||||
this.setState(
|
||||
{
|
||||
query: queryAndDateRange.query,
|
||||
dateRangeFrom:
|
||||
(queryAndDateRange.dateRange && queryAndDateRange.dateRange.from) ||
|
||||
this.state.dateRangeFrom,
|
||||
dateRangeTo:
|
||||
(queryAndDateRange.dateRange && queryAndDateRange.dateRange.to) || this.state.dateRangeTo,
|
||||
},
|
||||
() => {
|
||||
if (this.props.onQuerySubmit) {
|
||||
this.props.onQuerySubmit({
|
||||
query: this.state.query,
|
||||
dateRange: {
|
||||
from: this.state.dateRangeFrom,
|
||||
to: this.state.dateRangeTo,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
public onLoadSavedQuery = (savedQuery: SavedQuery) => {
|
||||
const dateRangeFrom = get(savedQuery, 'attributes.timefilter.from', this.state.dateRangeFrom);
|
||||
const dateRangeTo = get(savedQuery, 'attributes.timefilter.to', this.state.dateRangeTo);
|
||||
|
||||
this.setState({
|
||||
query: savedQuery.attributes.query,
|
||||
dateRangeFrom,
|
||||
dateRangeTo,
|
||||
});
|
||||
|
||||
if (this.props.onSavedQueryUpdated) {
|
||||
this.props.onSavedQueryUpdated(savedQuery);
|
||||
}
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
if (this.filterBarRef) {
|
||||
this.setFilterBarHeight();
|
||||
this.ro.observe(this.filterBarRef);
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
if (this.filterBarRef) {
|
||||
this.setFilterBarHeight();
|
||||
this.ro.unobserve(this.filterBarRef);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
// This is needed, as kbn-top-nav might render before npSetup.core.uiSettings is set.
|
||||
// This won't be needed when it's loaded exclusively with React.
|
||||
if (!this.props.uiSettings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const savedQueryManagement = this.state.query && this.props.onClearSavedQuery && (
|
||||
<SavedQueryManagementComponent
|
||||
showSaveQuery={this.props.showSaveQuery}
|
||||
loadedSavedQuery={this.props.savedQuery}
|
||||
onSave={this.onInitiateSave}
|
||||
onSaveAsNew={this.onInitiateSaveNew}
|
||||
onLoad={this.onLoadSavedQuery}
|
||||
savedQueryService={this.state.savedQueryService}
|
||||
onClearSavedQuery={this.props.onClearSavedQuery}
|
||||
></SavedQueryManagementComponent>
|
||||
);
|
||||
|
||||
let queryBar;
|
||||
if (this.shouldRenderQueryBar()) {
|
||||
queryBar = (
|
||||
<QueryBar
|
||||
toasts={this.props.toasts}
|
||||
uiSettings={this.props.uiSettings}
|
||||
savedObjectsClient={this.props.savedObjectsClient}
|
||||
query={this.state.query}
|
||||
screenTitle={this.props.screenTitle}
|
||||
onSubmit={this.onQueryBarSubmit}
|
||||
appName={this.props.appName}
|
||||
indexPatterns={this.props.indexPatterns}
|
||||
store={this.props.store}
|
||||
prepend={this.props.showFilterBar ? savedQueryManagement : undefined}
|
||||
showDatePicker={this.props.showDatePicker}
|
||||
dateRangeFrom={this.state.dateRangeFrom}
|
||||
dateRangeTo={this.state.dateRangeTo}
|
||||
isRefreshPaused={this.props.isRefreshPaused}
|
||||
refreshInterval={this.props.refreshInterval}
|
||||
showAutoRefreshOnly={this.props.showAutoRefreshOnly}
|
||||
showQueryInput={this.props.showQueryInput}
|
||||
onRefreshChange={this.props.onRefreshChange}
|
||||
onChange={this.onQueryBarChange}
|
||||
isDirty={this.isDirty()}
|
||||
customSubmitButton={
|
||||
this.props.customSubmitButton ? this.props.customSubmitButton : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let filterBar;
|
||||
if (this.shouldRenderFilterBar()) {
|
||||
const filterGroupClasses = classNames('globalFilterGroup__wrapper', {
|
||||
'globalFilterGroup__wrapper-isVisible': this.state.isFiltersVisible,
|
||||
});
|
||||
filterBar = (
|
||||
<div
|
||||
id="GlobalFilterGroup"
|
||||
ref={node => {
|
||||
this.filterBarWrapperRef = node;
|
||||
}}
|
||||
className={filterGroupClasses}
|
||||
>
|
||||
<div
|
||||
ref={node => {
|
||||
this.filterBarRef = node;
|
||||
}}
|
||||
>
|
||||
<FilterBar
|
||||
className="globalFilterGroup__filterBar"
|
||||
uiSettings={this.props.uiSettings}
|
||||
filters={this.props.filters!}
|
||||
onFiltersUpdated={this.getFilterUpdateFunction()}
|
||||
indexPatterns={this.props.indexPatterns!}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="globalQueryBar">
|
||||
{queryBar}
|
||||
{filterBar}
|
||||
|
||||
{this.state.showSaveQueryModal ? (
|
||||
<SaveQueryForm
|
||||
savedQuery={this.props.savedQuery ? this.props.savedQuery.attributes : undefined}
|
||||
savedQueryService={this.state.savedQueryService}
|
||||
onSave={this.onSave}
|
||||
onClose={() => this.setState({ showSaveQueryModal: false })}
|
||||
showFilterOption={this.props.showFilterBar}
|
||||
showTimeFilterOption={this.props.showDatePicker}
|
||||
/>
|
||||
) : null}
|
||||
{this.state.showSaveNewQueryModal ? (
|
||||
<SaveQueryForm
|
||||
savedQueryService={this.state.savedQueryService}
|
||||
onSave={savedQueryMeta => this.onSave(savedQueryMeta, true)}
|
||||
onClose={() => this.setState({ showSaveNewQueryModal: false })}
|
||||
showFilterOption={this.props.showFilterBar}
|
||||
showTimeFilterOption={this.props.showDatePicker}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const SearchBar = injectI18n(SearchBarUI);
|
121
public/kibana-integrations/search-bar/top_nav_menu.tsx
Normal file
121
public/kibana-integrations/search-bar/top_nav_menu.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. 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 React from 'react';
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { UiSettingsClientContract, SavedObjectsClientContract, CoreStart } from 'src/core/public';
|
||||
import { TopNavMenuData } from 'plugins/kibana_react/top_nav_menu/';
|
||||
import { TopNavMenuItem } from 'plugins/kibana_react/top_nav_menu/';
|
||||
import { SearchBar, SearchBarProps } from './search_bar';
|
||||
|
||||
type Props = Partial<SearchBarProps> & {
|
||||
name: string;
|
||||
uiSettings: UiSettingsClientContract;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
toasts: CoreStart['notifications']['toasts'];
|
||||
config?: TopNavMenuData[];
|
||||
showSearchBar?: boolean;
|
||||
};
|
||||
|
||||
/*
|
||||
* Top Nav Menu is a convenience wrapper component for:
|
||||
* - Top navigation menu - configured by an array of `TopNavMenuData` objects
|
||||
* - Search Bar - which includes Filter Bar \ Query Input \ Timepicker.
|
||||
*
|
||||
* See SearchBar documentation to learn more about its properties.
|
||||
*
|
||||
**/
|
||||
|
||||
export function TopNavMenu(props: Props) {
|
||||
function renderItems() {
|
||||
if (!props.config) return;
|
||||
return props.config.map((menuItem: TopNavMenuData, i: number) => {
|
||||
return (
|
||||
<EuiFlexItem grow={false} key={`nav-menu-${i}`}>
|
||||
<TopNavMenuItem {...menuItem} />
|
||||
</EuiFlexItem>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function renderSearchBar() {
|
||||
// Validate presense of all required fields
|
||||
if (!props.showSearchBar || !props.savedObjectsClient) return;
|
||||
return (
|
||||
<SearchBar
|
||||
savedObjectsClient={props.savedObjectsClient}
|
||||
query={props.query}
|
||||
filters={props.filters}
|
||||
toasts={props.toasts}
|
||||
uiSettings={props.uiSettings}
|
||||
showQueryBar={props.showQueryBar}
|
||||
showQueryInput={props.showQueryInput}
|
||||
showFilterBar={props.showFilterBar}
|
||||
showDatePicker={props.showDatePicker}
|
||||
appName={props.appName!}
|
||||
screenTitle={props.screenTitle!}
|
||||
onQuerySubmit={props.onQuerySubmit}
|
||||
onFiltersUpdated={props.onFiltersUpdated}
|
||||
dateRangeFrom={props.dateRangeFrom}
|
||||
dateRangeTo={props.dateRangeTo}
|
||||
isRefreshPaused={props.isRefreshPaused}
|
||||
showAutoRefreshOnly={props.showAutoRefreshOnly}
|
||||
onRefreshChange={props.onRefreshChange}
|
||||
refreshInterval={props.refreshInterval}
|
||||
indexPatterns={props.indexPatterns}
|
||||
store={props.store}
|
||||
savedQuery={props.savedQuery}
|
||||
showSaveQuery={props.showSaveQuery}
|
||||
onClearSavedQuery={props.onClearSavedQuery}
|
||||
onSaved={props.onSaved}
|
||||
onSavedQueryUpdated={props.onSavedQueryUpdated}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderLayout() {
|
||||
return (
|
||||
<span className="kbnTopNavMenu__wrapper">
|
||||
<EuiFlexGroup
|
||||
data-test-subj="top-nav"
|
||||
justifyContent="flexStart"
|
||||
gutterSize="none"
|
||||
className="kbnTopNavMenu"
|
||||
responsive={false}
|
||||
>
|
||||
{renderItems()}
|
||||
</EuiFlexGroup>
|
||||
{renderSearchBar()}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return <I18nProvider>{renderLayout()}</I18nProvider>;
|
||||
}
|
||||
|
||||
TopNavMenu.defaultProps = {
|
||||
showSearchBar: false,
|
||||
showQueryBar: true,
|
||||
showQueryInput: true,
|
||||
showDatePicker: true,
|
||||
showFilterBar: true,
|
||||
screenTitle: '',
|
||||
};
|
@ -20,7 +20,9 @@
|
||||
import 'ngreact';
|
||||
import { wrapInI18nContext } from 'ui/i18n';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { start as navigation } from 'plugins/navigation/legacy';
|
||||
import { TopNavMenu } from './search-bar/top_nav_menu';
|
||||
import { Storage } from 'ui/storage';
|
||||
import chrome from 'ui/chrome';
|
||||
|
||||
const module = uiModules.get('kibana');
|
||||
|
||||
@ -28,7 +30,7 @@ module.directive('wzTopNav', () => {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: '',
|
||||
compile: (elem) => {
|
||||
compile: elem => {
|
||||
const child = document.createElement('wz-top-nav-helper');
|
||||
|
||||
// Copy attributes to the child directive
|
||||
@ -40,32 +42,44 @@ module.directive('wzTopNav', () => {
|
||||
// of the config array's disableButton function return value changes.
|
||||
child.setAttribute('disabled-buttons', 'disabledButtons');
|
||||
|
||||
// Pass in storage
|
||||
const localStorage = new Storage(window.localStorage);
|
||||
child.setAttribute('store', 'store');
|
||||
child.setAttribute('ui-settings', 'uiSettings');
|
||||
child.setAttribute('saved-objects-client', 'savedObjectsClient');
|
||||
|
||||
// Append helper directive
|
||||
elem.append(child);
|
||||
|
||||
const linkFn = ($scope, _, $attr) => {
|
||||
$scope.store = localStorage;
|
||||
$scope.uiSettings = chrome.getUiSettingsClient();
|
||||
$scope.savedObjectsClient = chrome.getSavedObjectsClient();
|
||||
|
||||
// Watch config changes
|
||||
$scope.$watch(() => {
|
||||
const config = $scope.$eval($attr.config) || [];
|
||||
return config.map((item) => {
|
||||
// Copy key into id, as it's a reserved react propery.
|
||||
// This is done for Angular directive backward compatibility.
|
||||
// In React only id is recognized.
|
||||
if (item.key && !item.id) {
|
||||
item.id = item.key;
|
||||
}
|
||||
$scope.$watch(
|
||||
() => {
|
||||
const config = $scope.$eval($attr.config) || [];
|
||||
return config.map(item => {
|
||||
// Copy key into id, as it's a reserved react propery.
|
||||
// This is done for Angular directive backward compatibility.
|
||||
// In React only id is recognized.
|
||||
if (item.key && !item.id) {
|
||||
item.id = item.key;
|
||||
}
|
||||
|
||||
// Watch the disableButton functions
|
||||
if (typeof item.disableButton === 'function') {
|
||||
return item.disableButton();
|
||||
}
|
||||
return item.disableButton;
|
||||
});
|
||||
}, (newVal) => {
|
||||
$scope.disabledButtons = newVal;
|
||||
},
|
||||
true);
|
||||
// Watch the disableButton functions
|
||||
if (typeof item.disableButton === 'function') {
|
||||
return item.disableButton();
|
||||
}
|
||||
return item.disableButton;
|
||||
});
|
||||
},
|
||||
newVal => {
|
||||
$scope.disabledButtons = newVal;
|
||||
},
|
||||
true
|
||||
);
|
||||
};
|
||||
|
||||
return linkFn;
|
||||
@ -73,45 +87,46 @@ module.directive('wzTopNav', () => {
|
||||
};
|
||||
});
|
||||
|
||||
module.directive('wzTopNavHelper', (reactDirective) => {
|
||||
const { TopNavMenu } = navigation.ui;
|
||||
return reactDirective(
|
||||
wrapInI18nContext(TopNavMenu),
|
||||
[
|
||||
['config', { watchDepth: 'value' }],
|
||||
['disabledButtons', { watchDepth: 'reference' }],
|
||||
module.directive('wzTopNavHelper', reactDirective => {
|
||||
return reactDirective(wrapInI18nContext(TopNavMenu), [
|
||||
['name', { watchDepth: 'reference' }],
|
||||
['config', { watchDepth: 'value' }],
|
||||
['disabledButtons', { watchDepth: 'reference' }],
|
||||
|
||||
['query', { watchDepth: 'reference' }],
|
||||
['savedQuery', { watchDepth: 'reference' }],
|
||||
['intl', { watchDepth: 'reference' }],
|
||||
['query', { watchDepth: 'reference' }],
|
||||
['savedQuery', { watchDepth: 'reference' }],
|
||||
['store', { watchDepth: 'reference' }],
|
||||
['uiSettings', { watchDepth: 'reference' }],
|
||||
['savedObjectsClient', { watchDepth: 'reference' }],
|
||||
['intl', { watchDepth: 'reference' }],
|
||||
['store', { watchDepth: 'reference' }],
|
||||
|
||||
['onQuerySubmit', { watchDepth: 'reference' }],
|
||||
['onFiltersUpdated', { watchDepth: 'reference' }],
|
||||
['onRefreshChange', { watchDepth: 'reference' }],
|
||||
['onClearSavedQuery', { watchDepth: 'reference' }],
|
||||
['onSaved', { watchDepth: 'reference' }],
|
||||
['onSavedQueryUpdated', { watchDepth: 'reference' }],
|
||||
['onQuerySubmit', { watchDepth: 'reference' }],
|
||||
['onFiltersUpdated', { watchDepth: 'reference' }],
|
||||
['onRefreshChange', { watchDepth: 'reference' }],
|
||||
['onClearSavedQuery', { watchDepth: 'reference' }],
|
||||
['onSaved', { watchDepth: 'reference' }],
|
||||
['onSavedQueryUpdated', { watchDepth: 'reference' }],
|
||||
|
||||
['indexPatterns', { watchDepth: 'collection' }],
|
||||
['filters', { watchDepth: 'collection' }],
|
||||
['indexPatterns', { watchDepth: 'collection' }],
|
||||
['filters', { watchDepth: 'collection' }],
|
||||
|
||||
// All modifiers default to true.
|
||||
// Set to false to hide subcomponents.
|
||||
'showSearchBar',
|
||||
'showFilterBar',
|
||||
'showQueryBar',
|
||||
'showQueryInput',
|
||||
'showDatePicker',
|
||||
'showSaveQuery',
|
||||
// All modifiers default to true.
|
||||
// Set to false to hide subcomponents.
|
||||
'showSearchBar',
|
||||
'showFilterBar',
|
||||
'showQueryBar',
|
||||
'showQueryInput',
|
||||
'showDatePicker',
|
||||
'showSaveQuery',
|
||||
|
||||
'appName',
|
||||
'screenTitle',
|
||||
'dateRangeFrom',
|
||||
'dateRangeTo',
|
||||
'isRefreshPaused',
|
||||
'refreshInterval',
|
||||
'disableAutoFocus',
|
||||
'showAutoRefreshOnly',
|
||||
],
|
||||
);
|
||||
'appName',
|
||||
'screenTitle',
|
||||
'dateRangeFrom',
|
||||
'dateRangeTo',
|
||||
'isRefreshPaused',
|
||||
'refreshInterval',
|
||||
'disableAutoFocus',
|
||||
'showAutoRefreshOnly'
|
||||
]);
|
||||
});
|
||||
|
@ -1255,7 +1255,3 @@ md-chips.md-default-theme .md-chips, md-chips .md-chips{
|
||||
.table-vis-container{
|
||||
overflow: auto !important;
|
||||
}
|
||||
|
||||
.hide-close-button{
|
||||
display: none;
|
||||
}
|
@ -181,7 +181,7 @@ export class CommonData {
|
||||
}
|
||||
}
|
||||
if (agent) filters.push(filterHandler.agentQuery(agent));
|
||||
this.$rootScope.$emit('wzEventFilters', { filters, localChange, tab });
|
||||
this.$rootScope.$emit('wzEventFilters', { filters, localChange });
|
||||
if (!this.$rootScope.$$listenerCount['wzEventFilters']) {
|
||||
this.$timeout(100).then(() =>
|
||||
this.af(filterHandler, tab, localChange, (agent = false))
|
||||
|
@ -31,10 +31,6 @@ export class ReportingService {
|
||||
this.errorHandler = errorHandler;
|
||||
this.wazuhConfig = wazuhConfig;
|
||||
}
|
||||
removeTableVis(visList) {
|
||||
const attributes = JSON.parse(visList.attributes.visState);
|
||||
return attributes.type !== 'table';
|
||||
}
|
||||
|
||||
removeAgentStatusVis(idArray) {
|
||||
const monitoringEnabled = this.wazuhConfig.getConfig()[
|
||||
@ -61,16 +57,13 @@ export class ReportingService {
|
||||
|
||||
this.vis2png.clear();
|
||||
|
||||
const rawVisualizations = this.rawVisualizations.getList()
|
||||
.filter(this.removeTableVis);
|
||||
|
||||
let idArray = [];
|
||||
if (tab === 'general') {
|
||||
idArray = this.removeAgentStatusVis(
|
||||
rawVisualizations.map(item => item.id)
|
||||
this.rawVisualizations.getList().map(item => item.id)
|
||||
);
|
||||
} else {
|
||||
idArray = rawVisualizations.map(item => item.id);
|
||||
idArray = this.rawVisualizations.getList().map(item => item.id);
|
||||
}
|
||||
|
||||
for (const item of idArray) {
|
||||
@ -78,7 +71,7 @@ export class ReportingService {
|
||||
this.vis2png.assignHTMLItem(item, tmpHTMLElement);
|
||||
}
|
||||
|
||||
const appliedFilters = await this.visHandlers.getAppliedFilters(
|
||||
const appliedFilters = this.visHandlers.getAppliedFilters(
|
||||
syscollectorFilters
|
||||
);
|
||||
|
||||
@ -129,8 +122,8 @@ export class ReportingService {
|
||||
|
||||
const docType =
|
||||
type === 'agentConfig'
|
||||
? `wazuh-agent-${obj.id}`
|
||||
: `wazuh-group-${obj.name}`;
|
||||
? `wazuh-agent-${obj.id}`
|
||||
: `wazuh-group-${obj.name}`;
|
||||
|
||||
const name = `${docType}-configuration-${(Date.now() / 1000) | 0}.pdf`;
|
||||
const browserTimezone = moment.tz.guess(true);
|
||||
@ -139,9 +132,7 @@ export class ReportingService {
|
||||
array: [],
|
||||
name,
|
||||
filters: [
|
||||
type === 'agentConfig'
|
||||
? { agent: obj.id }
|
||||
: { group: obj.name }
|
||||
type === 'agentConfig' ? { agent: obj.id } : { group: obj.name }
|
||||
],
|
||||
time: '',
|
||||
searchBar: '',
|
||||
|
@ -10,7 +10,7 @@
|
||||
* Find more information about this on the LICENSE file.
|
||||
*/
|
||||
import { healthCheck } from './health-check';
|
||||
import { recentlyAccessed } from 'plugins/kibana/home/components/recently_accessed';
|
||||
import { recentlyAccessed } from 'ui/persisted_log';
|
||||
export function getSavedSearch(
|
||||
redirectWhenMissing,
|
||||
$location,
|
||||
|
@ -1,234 +1,222 @@
|
||||
<discover-app-w class="app-container">
|
||||
<h1 class="euiScreenReaderOnly">{{screenTitle}}</h1>
|
||||
<wz-top-nav
|
||||
app-name="'discover'"
|
||||
config="topNavMenu"
|
||||
show-search-bar="tabView !== 'cluster-monitoring'"
|
||||
show-date-picker="enableTimeRangeSelector"
|
||||
show-save-query="showSaveQuery"
|
||||
query="state.query"
|
||||
saved-query="savedQuery"
|
||||
screen-title="screenTitle"
|
||||
on-query-submit="updateQueryAndFetch"
|
||||
index-patterns="[indexPattern]"
|
||||
filters="filters"
|
||||
on-filters-updated="onFiltersUpdated"
|
||||
date-range-from="time.from"
|
||||
date-range-to="time.to"
|
||||
is-refresh-paused="refreshInterval.pause"
|
||||
refresh-interval="refreshInterval.value"
|
||||
on-refresh-change="onRefreshChange"
|
||||
on-saved="onQuerySaved"
|
||||
on-saved-query-updated="onSavedQueryUpdated"
|
||||
on-clear-saved-query="onClearSavedQuery"
|
||||
>
|
||||
</wz-top-nav>
|
||||
|
||||
<!-- Local nav. -->
|
||||
<wz-top-nav
|
||||
app-name="'discover'"
|
||||
config="topNavMenu"
|
||||
show-search-bar="tabView !== 'cluster-monitoring'"
|
||||
show-date-picker="enableTimeRangeSelector"
|
||||
show-save-query="showSaveQuery"
|
||||
query="state.query"
|
||||
saved-query="savedQuery"
|
||||
screen-title="screenTitle"
|
||||
on-query-submit="updateQueryAndFetch"
|
||||
index-patterns="[indexPattern]"
|
||||
filters="filters"
|
||||
on-filters-updated="onFiltersUpdated"
|
||||
date-range-from="time.from"
|
||||
date-range-to="time.to"
|
||||
is-refresh-paused="refreshInterval.pause"
|
||||
refresh-interval="refreshInterval.value"
|
||||
on-refresh-change="onRefreshChange"
|
||||
on-saved="onQuerySaved"
|
||||
on-saved-query-updated="onSavedQueryUpdated"
|
||||
on-clear-saved-query="onClearSavedQuery"
|
||||
>
|
||||
</wz-top-nav>
|
||||
|
||||
<main ng-show="tabView !== 'cluster-monitoring'" class="container-fluid">
|
||||
<div ng-show="tabView === 'discover'" class="row">
|
||||
<div ng-if="tabView === 'discover'" class="col-md-2 sidebar-container collapsible-sidebar" id="discover-sidebar">
|
||||
<div class="dscFieldChooser">
|
||||
<disc-field-chooser
|
||||
class="dscFieldChooser"
|
||||
columns="state.columns"
|
||||
hits="rows"
|
||||
field-counts="fieldCounts"
|
||||
index-pattern="searchSource.getField('index')"
|
||||
index-pattern-list="opts.indexPatternList"
|
||||
state="state"
|
||||
on-add-field="addColumn"
|
||||
on-add-filter="filterQuery"
|
||||
on-remove-field="removeColumn"
|
||||
>
|
||||
</disc-field-chooser>
|
||||
<main ng-show="tabView !== 'cluster-monitoring'" class="container-fluid">
|
||||
<div ng-show="tabView === 'discover'" class="row">
|
||||
<div ng-if="tabView === 'discover'" class="col-md-2 sidebar-container collapsible-sidebar" id="discover-sidebar">
|
||||
<disc-field-chooser
|
||||
class="dscFieldChooser"
|
||||
columns="state.columns"
|
||||
hits="rows"
|
||||
field-counts="fieldCounts"
|
||||
index-pattern="searchSource.getField('index')"
|
||||
index-pattern-list="opts.indexPatternList"
|
||||
state="state"
|
||||
on-add-field="addColumn"
|
||||
on-add-filter="filterQuery"
|
||||
on-remove-field="removeColumn"
|
||||
>
|
||||
</disc-field-chooser>
|
||||
</div>
|
||||
|
||||
<div class="dscWrapper col-md-10">
|
||||
<discover-unsupported-index-pattern
|
||||
ng-if="isUnsupportedIndexPattern"
|
||||
unsupported-type="unsupportedIndexPatternType"
|
||||
></discover-unsupported-index-pattern>
|
||||
|
||||
<discover-no-results
|
||||
ng-show="resultState === 'none'"
|
||||
shard-failures="failures"
|
||||
time-field-name="opts.timefield"
|
||||
query-language="state.query.language"
|
||||
get-doc-link="getDocLink"
|
||||
></discover-no-results>
|
||||
|
||||
<discover-uninitialized
|
||||
ng-show="resultState === 'uninitialized'"
|
||||
on-refresh="fetch"
|
||||
></discover-uninitialized>
|
||||
|
||||
<!-- loading -->
|
||||
<div ng-show="resultState === 'loading'">
|
||||
<discover-fetch-error
|
||||
ng-show="fetchError"
|
||||
fetch-error="fetchError"
|
||||
></discover-fetch-error>
|
||||
|
||||
<div ng-hide="fetchError" class="dscOverlay">
|
||||
<h2
|
||||
i18n-id="kbn.discover.searchingTitle"
|
||||
i18n-default-message="Searching"
|
||||
class="euiTitle euiTitle--small"
|
||||
></h2>
|
||||
<div class="euiSpacer euiSpacer--m"></div>
|
||||
<div
|
||||
class="euiLoadingSpinner euiLoadingSpinner--large"
|
||||
data-test-subj="loadingSpinner"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dscWrapper col-md-10">
|
||||
<discover-unsupported-index-pattern
|
||||
ng-if="isUnsupportedIndexPattern"
|
||||
unsupported-type="unsupportedIndexPatternType"
|
||||
></discover-unsupported-index-pattern>
|
||||
|
||||
<discover-no-results
|
||||
ng-show="resultState === 'none'"
|
||||
shard-failures="failures"
|
||||
time-field-name="opts.timefield"
|
||||
query-language="state.query.language"
|
||||
get-doc-link="getDocLink"
|
||||
></discover-no-results>
|
||||
|
||||
<discover-uninitialized
|
||||
ng-show="resultState === 'uninitialized'"
|
||||
on-refresh="fetch"
|
||||
></discover-uninitialized>
|
||||
|
||||
<!-- loading -->
|
||||
<div ng-show="resultState === 'loading'">
|
||||
<discover-fetch-error
|
||||
ng-show="fetchError"
|
||||
fetch-error="fetchError"
|
||||
></discover-fetch-error>
|
||||
|
||||
<div ng-hide="fetchError" class="dscOverlay">
|
||||
<h2
|
||||
i18n-id="kbn.discover.searchingTitle"
|
||||
i18n-default-message="Searching"
|
||||
class="euiTitle euiTitle--small"
|
||||
></h2>
|
||||
<div class="euiSpacer euiSpacer--m"></div>
|
||||
<div
|
||||
class="euiLoadingSpinner euiLoadingSpinner--large"
|
||||
data-test-subj="loadingSpinner"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dscWrapper__content" ng-show="resultState === 'ready'">
|
||||
<!-- result -->
|
||||
<div class="dscResults">
|
||||
<button
|
||||
class="kuiButton kuiButton--basic kuiButton--iconText dscSkipButton"
|
||||
ng-click="showAllRows(); scrollToBottom()"
|
||||
>
|
||||
<span class="kuiButton__inner">
|
||||
<span aria-hidden="true" class="kuiButton__icon kuiIcon fa-chevron-down"></span>
|
||||
<span
|
||||
i18n-id="kbn.discover.skipToBottomButtonLabel"
|
||||
i18n-default-message="Skip to bottom"
|
||||
></span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div class="dscResultCount">
|
||||
<strong data-test-subj="discoverQueryHits">{{(hits || 0) | number:0}}</strong>
|
||||
<div class="dscWrapper__content" ng-show="resultState === 'ready'">
|
||||
<!-- result -->
|
||||
<div class="dscResults">
|
||||
<button
|
||||
class="kuiButton kuiButton--basic kuiButton--iconText dscSkipButton"
|
||||
ng-click="showAllRows(); scrollToBottom()"
|
||||
>
|
||||
<span class="kuiButton__inner">
|
||||
<span aria-hidden="true" class="kuiButton__icon kuiIcon fa-chevron-down"></span>
|
||||
<span
|
||||
class="dscResultHits"
|
||||
i18n-id="kbn.discover.hitsPluralTitle"
|
||||
i18n-default-message="{hits, plural, one {hit} other {hits}}"
|
||||
i18n-values="{ hits }"
|
||||
i18n-id="kbn.discover.skipToBottomButtonLabel"
|
||||
i18n-default-message="Skip to bottom"
|
||||
></span>
|
||||
<button
|
||||
ng-if="opts.savedSearch.id"
|
||||
class="kuiLink"
|
||||
type="button"
|
||||
id="reload_saved_search"
|
||||
ng-click="resetQuery()"
|
||||
>
|
||||
{{::'kbn.discover.reloadSavedSearchButton' | i18n: {defaultMessage: 'Reset search'} }}
|
||||
</button>
|
||||
</div>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<section
|
||||
aria-label="{{::'kbn.discover.histogramOfFoundDocumentsAriaLabel' | i18n: {defaultMessage: 'Histogram of found documents'} }}"
|
||||
class="dscTimechart"
|
||||
ng-if="opts.timefield"
|
||||
<div class="dscResultCount">
|
||||
<strong data-test-subj="discoverQueryHits">{{(hits || 0) | number:0}}</strong>
|
||||
<span
|
||||
class="dscResultHits"
|
||||
i18n-id="kbn.discover.hitsPluralTitle"
|
||||
i18n-default-message="{hits, plural, one {hit} other {hits}}"
|
||||
i18n-values="{ hits }"
|
||||
></span>
|
||||
<button
|
||||
ng-if="opts.savedSearch.id"
|
||||
class="kuiLink"
|
||||
type="button"
|
||||
id="reload_saved_search"
|
||||
ng-click="resetQuery()"
|
||||
>
|
||||
<header class="dscTimechart__header">
|
||||
<div class="small">
|
||||
<label
|
||||
for="dscResultsIntervalSelector"
|
||||
tooltip="{{::'kbn.discover.howToChangeTheTimeTooltip' | i18n: {defaultMessage: 'To change the time, click the calendar icon in the navigation bar'} }}"
|
||||
>
|
||||
{{toMoment(timeRange.from)}} - {{toMoment(timeRange.to)}}
|
||||
</label>
|
||||
|
||||
—
|
||||
|
||||
<span class="form-inline">
|
||||
<select
|
||||
id="dscResultsIntervalSelector"
|
||||
class="dscResults__interval form-control"
|
||||
ng-model="state.interval"
|
||||
ng-options="interval.val as interval.display for interval in intervalOptions | filter: intervalEnabled"
|
||||
ng-blur="toggleInterval()"
|
||||
data-test-subj="discoverIntervalSelect"
|
||||
>
|
||||
</select>
|
||||
<span ng-if="bucketInterval.scaled">
|
||||
<icon-tip
|
||||
content="getBucketIntervalToolTipText()"
|
||||
position="'top'"
|
||||
></icon-tip>
|
||||
<span
|
||||
i18n-id="kbn.discover.scaledToDescription"
|
||||
i18n-default-message="Scaled to {bucketIntervalDescription}"
|
||||
i18n-values="{
|
||||
bucketIntervalDescription: bucketInterval.description
|
||||
}"
|
||||
></span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
|
||||
<discover-histogram
|
||||
class="dscHistogram"
|
||||
ng-show="vis && rows.length !== 0"
|
||||
chart-data="histogramData"
|
||||
timefilter-update-handler="timefilterUpdateHandler"
|
||||
watch-depth="reference"
|
||||
data-test-subj="discoverChart"
|
||||
></discover-histogram>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="dscTable"
|
||||
fixed-scroll
|
||||
aria-labelledby="documentsAriaLabel"
|
||||
>
|
||||
<h2 class="euiScreenReaderOnly"
|
||||
id="documentsAriaLabel"
|
||||
i18n-id="kbn.discover.documentsAriaLabel"
|
||||
i18n-default-message="Documents"
|
||||
></h2>
|
||||
<doc-table
|
||||
hits="rows"
|
||||
index-pattern="indexPattern"
|
||||
sorting="state.sort"
|
||||
columns="state.columns"
|
||||
infinite-scroll="true"
|
||||
filter="filterQuery"
|
||||
filters="state.filters"
|
||||
data-shared-item
|
||||
data-title="{{opts.savedSearch.lastSavedTitle}}"
|
||||
data-description="{{opts.savedSearch.description}}"
|
||||
minimum-visible-rows="minimumVisibleRows"
|
||||
render-complete
|
||||
on-add-column="addColumn"
|
||||
on-change-sort-order="setSortOrder"
|
||||
on-move-column="moveColumn"
|
||||
on-remove-column="removeColumn"
|
||||
></doc-table>
|
||||
|
||||
<a tabindex="0" id="discoverBottomMarker"></a>
|
||||
|
||||
<div
|
||||
ng-if="rows.length == opts.sampleSize"
|
||||
class="dscTable__footer"
|
||||
>
|
||||
<span
|
||||
i18n-id="kbn.discover.howToSeeOtherMatchingDocumentsDescription"
|
||||
i18n-default-message="These are the first {sampleSize} documents matching
|
||||
your search, refine your search to see others. "
|
||||
i18n-values="{
|
||||
sampleSize: opts.sampleSize,
|
||||
}"
|
||||
></span>
|
||||
<a
|
||||
kbn-accessible-click
|
||||
ng-click="scrollToTop()"
|
||||
i18n-id="kbn.discover.backToTopLinkText"
|
||||
i18n-default-message="Back to top."
|
||||
></a>
|
||||
</div>
|
||||
</section>
|
||||
{{::'kbn.discover.reloadSavedSearchButton' | i18n: {defaultMessage: 'Reset search'} }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section
|
||||
aria-label="{{::'kbn.discover.histogramOfFoundDocumentsAriaLabel' | i18n: {defaultMessage: 'Histogram of found documents'} }}"
|
||||
class="dscTimechart"
|
||||
ng-if="opts.timefield"
|
||||
>
|
||||
<header class="dscTimechart__header">
|
||||
<div class="small">
|
||||
<label
|
||||
for="dscResultsIntervalSelector"
|
||||
tooltip="{{::'kbn.discover.howToChangeTheTimeTooltip' | i18n: {defaultMessage: 'To change the time, click the calendar icon in the navigation bar'} }}"
|
||||
>
|
||||
{{toMoment(timeRange.from)}} - {{toMoment(timeRange.to)}}
|
||||
</label>
|
||||
|
||||
—
|
||||
|
||||
<span class="form-inline">
|
||||
<select
|
||||
id="dscResultsIntervalSelector"
|
||||
class="dscResults__interval form-control"
|
||||
ng-model="state.interval"
|
||||
ng-options="interval.val as interval.display for interval in intervalOptions | filter: intervalEnabled"
|
||||
ng-blur="toggleInterval()"
|
||||
data-test-subj="discoverIntervalSelect"
|
||||
>
|
||||
</select>
|
||||
<span ng-if="bucketInterval.scaled">
|
||||
<icon-tip
|
||||
content="getBucketIntervalToolTipText()"
|
||||
position="'top'"
|
||||
></icon-tip>
|
||||
<span
|
||||
i18n-id="kbn.discover.scaledToDescription"
|
||||
i18n-default-message="Scaled to {bucketIntervalDescription}"
|
||||
i18n-values="{
|
||||
bucketIntervalDescription: bucketInterval.description
|
||||
}"
|
||||
></span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
|
||||
<div id="discoverHistogram"
|
||||
ng-show="vis && rows.length !== 0"
|
||||
style="display: flex; height: 200px"
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="dscTable"
|
||||
fixed-scroll
|
||||
aria-label="{{::'kbn.discover.documentsAriaLabel' | i18n: {defaultMessage: 'Documents'} }}"
|
||||
>
|
||||
<doc-table
|
||||
hits="rows"
|
||||
index-pattern="indexPattern"
|
||||
sorting="state.sort"
|
||||
columns="state.columns"
|
||||
infinite-scroll="true"
|
||||
filter="filterQuery"
|
||||
filters="state.filters"
|
||||
data-shared-item
|
||||
data-title="{{opts.savedSearch.lastSavedTitle}}"
|
||||
data-description="{{opts.savedSearch.description}}"
|
||||
minimum-visible-rows="minimumVisibleRows"
|
||||
render-complete
|
||||
on-add-column="addColumn"
|
||||
on-change-sort-order="setSortOrder"
|
||||
on-move-column="moveColumn"
|
||||
on-remove-column="removeColumn"
|
||||
></doc-table>
|
||||
|
||||
<a tabindex="0" id="discoverBottomMarker"></a>
|
||||
|
||||
<div
|
||||
ng-if="rows.length == opts.sampleSize"
|
||||
class="dscTable__footer"
|
||||
>
|
||||
<span
|
||||
i18n-id="kbn.discover.howToSeeOtherMatchingDocumentsDescription"
|
||||
i18n-default-message="These are the first {sampleSize} documents matching
|
||||
your search, refine your search to see others. "
|
||||
i18n-values="{
|
||||
sampleSize: opts.sampleSize,
|
||||
}"
|
||||
></span>
|
||||
<a
|
||||
kbn-accessible-click
|
||||
ng-click="scrollToTop()"
|
||||
i18n-id="kbn.discover.backToTopLinkText"
|
||||
i18n-default-message="Back to top."
|
||||
></a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</discover-app-w>
|
||||
</div>
|
||||
</main>
|
||||
</discover-app-w>
|
@ -20,7 +20,7 @@ export const changeWazuhNavLogo = () => {
|
||||
clearInterval(interval);
|
||||
}
|
||||
const url = chrome.addBasePath('/plugins/wazuh/img/logo.svg');
|
||||
$('.euiBreadcrumbs').html(
|
||||
$('.euiBreadcrumb').html(
|
||||
`<img src="${url}" class="navBarLogo" alt="">`
|
||||
);
|
||||
}, 100);
|
||||
|
@ -185,7 +185,11 @@ export class WazuhReportingCtrl {
|
||||
log('reporting:renderTables', `isVis: ${isVis}`, 'debug');
|
||||
for (const table of tables) {
|
||||
let rowsparsed = [];
|
||||
rowsparsed = table.rows;
|
||||
if (isVis) {
|
||||
rowsparsed = rawParser(table.rawResponse, table.columns);
|
||||
} else {
|
||||
rowsparsed = table.rows;
|
||||
}
|
||||
if (Array.isArray(rowsparsed) && rowsparsed.length) {
|
||||
const rows =
|
||||
rowsparsed.length > 100 ? rowsparsed.slice(0, 99) : rowsparsed;
|
||||
@ -200,8 +204,8 @@ export class WazuhReportingCtrl {
|
||||
parseInt(a[a.length - 1]) < parseInt(b[b.length - 1])
|
||||
? 1
|
||||
: parseInt(a[a.length - 1]) > parseInt(b[b.length - 1])
|
||||
? -1
|
||||
: 0;
|
||||
? -1
|
||||
: 0;
|
||||
|
||||
TimSort.sort(rows, sortFunction);
|
||||
|
||||
@ -355,9 +359,9 @@ export class WazuhReportingCtrl {
|
||||
const seconds = date.getSeconds();
|
||||
const str = `${year}-${month < 10 ? '0' + month : month}-${
|
||||
day < 10 ? '0' + day : day
|
||||
}T${hours < 10 ? '0' + hours : hours}:${
|
||||
}T${hours < 10 ? '0' + hours : hours}:${
|
||||
minutes < 10 ? '0' + minutes : minutes
|
||||
}:${seconds < 10 ? '0' + seconds : seconds}`;
|
||||
}:${seconds < 10 ? '0' + seconds : seconds}`;
|
||||
log('reporting:formatDate', `str: ${str}`, 'debug');
|
||||
return str;
|
||||
}
|
||||
@ -458,15 +462,19 @@ export class WazuhReportingCtrl {
|
||||
|
||||
const len = filters.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
const { negate, key, value, params, type } = filters[i].meta;
|
||||
str += `${negate ? 'NOT ' : ''}`;
|
||||
str += `${key}: `;
|
||||
str += `${type === 'range'
|
||||
? `${params.gte}-${params.lt}`
|
||||
: !!value
|
||||
? value
|
||||
: (params || {}).query}`;
|
||||
str += `${i === len - 1 ? '' : ' AND ' }`;
|
||||
const filter = filters[i];
|
||||
|
||||
str +=
|
||||
i === len - 1
|
||||
? (filter.meta.negate ? 'NOT ' : '') +
|
||||
filter.meta.key +
|
||||
': ' +
|
||||
filter.meta.value
|
||||
: (filter.meta.negate ? 'NOT ' : '') +
|
||||
filter.meta.key +
|
||||
': ' +
|
||||
filter.meta.value +
|
||||
' AND ';
|
||||
}
|
||||
|
||||
if (searchBar) {
|
||||
@ -587,7 +595,7 @@ export class WazuhReportingCtrl {
|
||||
log(
|
||||
'reporting:checkTitle',
|
||||
`Item ID ${item.id}, from ${
|
||||
isAgents ? 'agents' : 'overview'
|
||||
isAgents ? 'agents' : 'overview'
|
||||
} and tab ${tab}`,
|
||||
'info'
|
||||
);
|
||||
@ -1549,7 +1557,7 @@ export class WazuhReportingCtrl {
|
||||
if (hardware.data.ram && hardware.data.ram.total)
|
||||
ulcustom.push(
|
||||
Number(hardware.data.ram.total / 1024 / 1024).toFixed(2) +
|
||||
'GB RAM'
|
||||
'GB RAM'
|
||||
);
|
||||
ulcustom &&
|
||||
ulcustom.length &&
|
||||
@ -1734,8 +1742,8 @@ export class WazuhReportingCtrl {
|
||||
plainData[key] =
|
||||
Array.isArray(data[key]) && typeof data[key][0] !== 'object'
|
||||
? data[key].map(x => {
|
||||
return typeof x === 'object' ? JSON.stringify(x) : x + '\n';
|
||||
})
|
||||
return typeof x === 'object' ? JSON.stringify(x) : x + '\n';
|
||||
})
|
||||
: data[key];
|
||||
} else if (
|
||||
Array.isArray(data[key]) &&
|
||||
@ -1755,7 +1763,7 @@ export class WazuhReportingCtrl {
|
||||
title: (section.options || {}).hideHeader
|
||||
? ''
|
||||
: (section.tabs || [])[tab] ||
|
||||
(section.isGroupConfig ? ((section.labels || [])[0] || [])[tab] : ''),
|
||||
(section.isGroupConfig ? ((section.labels || [])[0] || [])[tab] : ''),
|
||||
columns: ['', ''],
|
||||
type: 'config',
|
||||
rows: this.getConfigRows(plainData, (section.labels || [])[0])
|
||||
@ -1773,10 +1781,10 @@ export class WazuhReportingCtrl {
|
||||
typeof x[key] !== 'object'
|
||||
? x[key]
|
||||
: Array.isArray(x[key])
|
||||
? x[key].map(x => {
|
||||
? x[key].map(x => {
|
||||
return x + '\n';
|
||||
})
|
||||
: JSON.stringify(x[key])
|
||||
: JSON.stringify(x[key])
|
||||
);
|
||||
}
|
||||
while (row.length < columns.length) {
|
||||
@ -1968,10 +1976,10 @@ export class WazuhReportingCtrl {
|
||||
typeof x[key] !== 'object'
|
||||
? x[key]
|
||||
: Array.isArray(x[key])
|
||||
? x[key].map(x => {
|
||||
? x[key].map(x => {
|
||||
return x + '\n';
|
||||
})
|
||||
: JSON.stringify(x[key])
|
||||
: JSON.stringify(x[key])
|
||||
);
|
||||
});
|
||||
return row;
|
||||
@ -2157,7 +2165,7 @@ export class WazuhReportingCtrl {
|
||||
data &&
|
||||
data.data &&
|
||||
Object.keys(data.data[Object.keys(data.data)[0]]).length >
|
||||
0
|
||||
0
|
||||
) {
|
||||
if (!titleOfSection) {
|
||||
this.dd.content.push({
|
||||
@ -2209,10 +2217,10 @@ export class WazuhReportingCtrl {
|
||||
typeof x[key] !== 'object'
|
||||
? x[key]
|
||||
: Array.isArray(x[key])
|
||||
? x[key].map(x => {
|
||||
? x[key].map(x => {
|
||||
return x + '\n';
|
||||
})
|
||||
: JSON.stringify(x[key])
|
||||
: JSON.stringify(x[key])
|
||||
);
|
||||
});
|
||||
return row;
|
||||
@ -2394,22 +2402,22 @@ export class WazuhReportingCtrl {
|
||||
agentOs === 'windows'
|
||||
? ['Name', 'Architecture', 'Version', 'Vendor']
|
||||
: [
|
||||
'Name',
|
||||
'Architecture',
|
||||
'Version',
|
||||
'Vendor',
|
||||
'Description'
|
||||
],
|
||||
'Name',
|
||||
'Architecture',
|
||||
'Version',
|
||||
'Vendor',
|
||||
'Description'
|
||||
],
|
||||
rows: packages.data.items.map(x => {
|
||||
return agentOs === 'windows'
|
||||
? [x['name'], x['architecture'], x['version'], x['vendor']]
|
||||
: [
|
||||
x['name'],
|
||||
x['architecture'],
|
||||
x['version'],
|
||||
x['vendor'],
|
||||
x['description']
|
||||
];
|
||||
x['name'],
|
||||
x['architecture'],
|
||||
x['version'],
|
||||
x['vendor'],
|
||||
x['description']
|
||||
];
|
||||
})
|
||||
});
|
||||
}
|
||||
@ -2439,11 +2447,11 @@ export class WazuhReportingCtrl {
|
||||
return agentOs === 'windows'
|
||||
? [x['name'], x['cmd'], x['priority'], x['nlwp']]
|
||||
: [
|
||||
x['name'],
|
||||
x['euser'],
|
||||
x['nice'],
|
||||
ProcessEquivalence[x.state]
|
||||
];
|
||||
x['name'],
|
||||
x['euser'],
|
||||
x['nice'],
|
||||
ProcessEquivalence[x.state]
|
||||
];
|
||||
})
|
||||
});
|
||||
}
|
||||
@ -2473,18 +2481,18 @@ export class WazuhReportingCtrl {
|
||||
rows: ports.data.items.map(x => {
|
||||
return agentOs === 'windows'
|
||||
? [
|
||||
x['local']['ip'],
|
||||
x['local']['port'],
|
||||
x['process'],
|
||||
x['state'],
|
||||
x['protocol']
|
||||
]
|
||||
x['local']['ip'],
|
||||
x['local']['port'],
|
||||
x['process'],
|
||||
x['state'],
|
||||
x['protocol']
|
||||
]
|
||||
: [
|
||||
x['local']['ip'],
|
||||
x['local']['port'],
|
||||
x['state'],
|
||||
x['protocol']
|
||||
];
|
||||
x['local']['ip'],
|
||||
x['local']['port'],
|
||||
x['state'],
|
||||
x['protocol']
|
||||
];
|
||||
})
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user