mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
Migrate x-pack-kibana source to kibana
This commit is contained in:
parent
e8ac7d8d32
commit
bc5b51554d
3256 changed files with 277621 additions and 2324 deletions
8
x-pack/plugins/reporting/public/assets/app_reporting.svg
Normal file
8
x-pack/plugins/reporting/public/assets/app_reporting.svg
Normal file
|
@ -0,0 +1,8 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path fill="#00BFB3" d="M25.625,6 L6.375,6 C5.615,6 5,6.24722222 5,6.93796296 L5,29.437037 C5,30.1277778 5.615,31 6.375,31 L25.625,31 C26.385,31 27,30.1277778 27,29.437037 L27,6.93796296 C27,6.24722222 26.385,6 25.625,6"/>
|
||||
<path fill="#14A7DF" d="M21.5996,3.7998 L20.2006,3.7998 C19.8136,3.7998 19.4996,3.4868 19.4996,3.0998 L19.4996,2.4008 C19.4996,1.6268 18.8726,0.9998 18.0996,0.9998 L13.9006,0.9998 C13.1266,0.9998 12.4996,1.6268 12.4996,2.4008 L12.4996,3.0998 C12.4996,3.4868 12.1866,3.7998 11.7996,3.7998 L10.4006,3.7998 C9.6266,3.7998 8.9996,4.4268 8.9996,5.1998 L8.9996,6.5998 C8.9996,7.3728 9.6266,7.9998 10.4006,7.9998 L21.5996,7.9998 C22.3726,7.9998 22.9996,7.3728 22.9996,6.5998 L22.9996,5.1998 C22.9996,4.4268 22.3726,3.7998 21.5996,3.7998" style="mix-blend-mode:multiply"/>
|
||||
<path fill="#0078A0" d="M27,29.3125 L27,18.8075 C26.925,18.8515 20.939,16.7805 20.872,16.8515 L15.544,22.4945 L14.315,23.7955 C13.951,24.1805 13.367,24.1805 13.003,23.7955 L10.256,20.8885 C9.909,20.5225 9.358,20.5015 8.99,20.8435 L5,24.5345 L5,29.3125 C5,30.0575 5.615,30.9995 6.375,30.9995 L25.625,30.9995 C26.385,30.9995 27,30.0575 27,29.3125"/>
|
||||
<path fill="#14A7DF" d="M25.4092,10.5908 L19.4972,16.8518 L15.3562,21.2368 L14.0122,22.6608 L12.9402,23.7958 L6.1672,30.9688 C6.2352,30.9848 6.3042,30.9998 6.3752,30.9998 L25.6252,30.9998 C26.3852,30.9998 27.0002,30.0578 27.0002,29.3128 L27.0002,11.3068 C27.0002,10.4148 26.0002,9.9658 25.4092,10.5908"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
31
x-pack/plugins/reporting/public/controls/dashboard.js
Normal file
31
x-pack/plugins/reporting/public/controls/dashboard.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import 'plugins/reporting/directives/export_config';
|
||||
import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info';
|
||||
import { NavBarExtensionsRegistryProvider } from 'ui/registry/navbar_extensions';
|
||||
import { DashboardConstants } from 'plugins/kibana/dashboard/dashboard_constants';
|
||||
|
||||
function dashboardReportProvider(Private, $location, dashboardConfig) {
|
||||
const xpackInfo = Private(XPackInfoProvider);
|
||||
return {
|
||||
appName: 'dashboard',
|
||||
key: 'reporting-dashboard',
|
||||
label: 'Reporting',
|
||||
template: `<export-config object-type="Dashboard" enabled-export-type="printablePdf"></export-config>`,
|
||||
description: 'Dashboard Report',
|
||||
hideButton: () => (
|
||||
dashboardConfig.getHideWriteControls()
|
||||
|| $location.path() === DashboardConstants.LANDING_PAGE_PATH
|
||||
|| !xpackInfo.get('features.reporting.printablePdf.showLinks', false)
|
||||
),
|
||||
disableButton: () => !xpackInfo.get('features.reporting.printablePdf.enableLinks', false),
|
||||
tooltip: () => xpackInfo.get('features.reporting.printablePdf.message'),
|
||||
testId: 'topNavReportingLink',
|
||||
};
|
||||
}
|
||||
|
||||
NavBarExtensionsRegistryProvider.register(dashboardReportProvider);
|
27
x-pack/plugins/reporting/public/controls/discover.js
Normal file
27
x-pack/plugins/reporting/public/controls/discover.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import 'plugins/reporting/directives/export_config';
|
||||
import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info';
|
||||
import { NavBarExtensionsRegistryProvider } from 'ui/registry/navbar_extensions';
|
||||
|
||||
function discoverReportProvider(Private) {
|
||||
const xpackInfo = Private(XPackInfoProvider);
|
||||
return {
|
||||
appName: 'discover',
|
||||
|
||||
key: 'reporting-discover',
|
||||
label: 'Reporting',
|
||||
template: '<export-config object-type="Search" enabled-export-type="csv"></export-config>',
|
||||
description: 'Search Report',
|
||||
hideButton: () => !xpackInfo.get('features.reporting.csv.showLinks', false),
|
||||
disableButton: () => !xpackInfo.get('features.reporting.csv.enableLinks', false),
|
||||
tooltip: () => xpackInfo.get('features.reporting.csv.message'),
|
||||
testId: 'topNavReportingLink',
|
||||
};
|
||||
}
|
||||
|
||||
NavBarExtensionsRegistryProvider.register(discoverReportProvider);
|
38
x-pack/plugins/reporting/public/controls/visualize.js
Normal file
38
x-pack/plugins/reporting/public/controls/visualize.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import 'plugins/reporting/directives/export_config';
|
||||
import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info';
|
||||
import { NavBarExtensionsRegistryProvider } from 'ui/registry/navbar_extensions';
|
||||
import { VisualizeConstants } from 'plugins/kibana/visualize/visualize_constants';
|
||||
|
||||
function visualizeReportProvider(Private, $location) {
|
||||
const xpackInfo = Private(XPackInfoProvider);
|
||||
return {
|
||||
appName: 'visualize',
|
||||
|
||||
key: 'reporting-visualize',
|
||||
label: 'Reporting',
|
||||
template: `
|
||||
<export-config
|
||||
object-type="Visualization"
|
||||
enabled-export-type="printablePdf"
|
||||
options="{ layoutId: 'preserve_layout' }"
|
||||
></export-config>`,
|
||||
description: 'Visualization Report',
|
||||
hideButton: () => (
|
||||
$location.path() === VisualizeConstants.LANDING_PAGE_PATH
|
||||
|| $location.path() === VisualizeConstants.WIZARD_STEP_1_PAGE_PATH
|
||||
|| $location.path() === VisualizeConstants.WIZARD_STEP_2_PAGE_PATH
|
||||
|| !xpackInfo.get('features.reporting.printablePdf.showLinks', false)
|
||||
),
|
||||
disableButton: () => !xpackInfo.get('features.reporting.printablePdf.enableLinks', false),
|
||||
tooltip: () => xpackInfo.get('features.reporting.printablePdf.message'),
|
||||
testId: 'topNavReportingLink',
|
||||
};
|
||||
}
|
||||
|
||||
NavBarExtensionsRegistryProvider.register(visualizeReportProvider);
|
|
@ -0,0 +1,55 @@
|
|||
<div ng-show="!exportConfig.isDirty()">
|
||||
<div class="kuiLocalDropdownSection">
|
||||
<h2 class="kuiLocalDropdownTitle">
|
||||
Reporting
|
||||
</h2>
|
||||
|
||||
<div class="input-group generate-controls">
|
||||
<div class="options"></div>
|
||||
<button
|
||||
class="kuiButton kuiButton--primary"
|
||||
data-test-subj="generateReportButton"
|
||||
ng-click="exportConfig.export()"
|
||||
>
|
||||
Generate {{ exportConfig.exportType.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kuiLocalDropdownSection">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="kuiLocalDropdownHeader">
|
||||
<label
|
||||
class="kuiLocalDropdownHeader__label"
|
||||
for="reportGenerationUrl"
|
||||
>
|
||||
Generation URL
|
||||
</label>
|
||||
<div class="kuiLocalDropdownHeader__actions">
|
||||
<a
|
||||
class="kuiLocalDropdownHeader__action"
|
||||
ng-click="exportConfig.copyToClipboard('#reportGenerationUrl')"
|
||||
kbn-accessible-click
|
||||
>
|
||||
Copy
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input -->
|
||||
<input
|
||||
id="reportGenerationUrl"
|
||||
class="kuiLocalDropdownInput"
|
||||
type="text"
|
||||
readonly
|
||||
data-test-subj="reportGenerationUrl"
|
||||
value="{{ exportConfig.absoluteUrl || 'Loading...' }}"
|
||||
ng-click="updateUrl()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="exportConfig.isDirty()" data-test-subj="unsavedChangesReportingWarning">
|
||||
Please save your work before generating a report.
|
||||
</div>
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import angular from 'angular';
|
||||
import { debounce } from 'lodash';
|
||||
import 'plugins/reporting/services/document_control';
|
||||
import 'plugins/reporting/services/export_types';
|
||||
import './export_config.less';
|
||||
import template from 'plugins/reporting/directives/export_config/export_config.html';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { stateMonitorFactory } from 'ui/state_management/state_monitor_factory';
|
||||
import url from 'url';
|
||||
|
||||
const module = uiModules.get('xpack/reporting');
|
||||
|
||||
module.directive('exportConfig', ($rootScope, reportingDocumentControl, reportingExportTypes, $location, $compile) => {
|
||||
const createAbsoluteUrl = relativePath => {
|
||||
return url.resolve($location.absUrl(), relativePath);
|
||||
};
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {},
|
||||
require: ['?^dashboardApp', '?^visualizeApp', '?^discoverApp'],
|
||||
controllerAs: 'exportConfig',
|
||||
template,
|
||||
transclude: true,
|
||||
async link($scope, $el, $attr, controllers) {
|
||||
const actualControllers = controllers.filter(c => c !== null);
|
||||
if (actualControllers.length !== 1) {
|
||||
throw new Error(`Expected there to be 1 controller, but there are ${actualControllers.length}`);
|
||||
}
|
||||
const controller = actualControllers[0];
|
||||
$scope.exportConfig.isDirty = () => controller.appStatus.dirty;
|
||||
if (controller.appStatus.dirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
const exportTypeId = $attr.enabledExportType;
|
||||
$scope.exportConfig.exportType = reportingExportTypes.getById(exportTypeId);
|
||||
$scope.exportConfig.objectType = $attr.objectType;
|
||||
|
||||
$scope.options = $attr.options ? $scope.$eval($attr.options) : {};
|
||||
if ($scope.exportConfig.exportType.optionsTemplate) {
|
||||
$el.find('.options').append($compile($scope.exportConfig.exportType.optionsTemplate)($scope));
|
||||
}
|
||||
|
||||
$scope.getRelativePath = (options) => {
|
||||
return reportingDocumentControl.getPath($scope.exportConfig.exportType, controller, options || $scope.options);
|
||||
};
|
||||
|
||||
$scope.updateUrl = (options) => {
|
||||
return $scope.getRelativePath(options)
|
||||
.then(relativePath => {
|
||||
$scope.exportConfig.absoluteUrl = createAbsoluteUrl(relativePath);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('options', newOptions => $scope.updateUrl(newOptions), true);
|
||||
|
||||
await $scope.updateUrl();
|
||||
},
|
||||
controller($scope, $document, $window, $timeout, globalState) {
|
||||
const stateMonitor = stateMonitorFactory.create(globalState);
|
||||
stateMonitor.onChange(() => {
|
||||
if ($scope.exportConfig.isDirty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateUrl();
|
||||
});
|
||||
|
||||
const onResize = debounce(() => {
|
||||
$scope.updateUrl();
|
||||
}, 200);
|
||||
|
||||
angular.element($window).on('resize', onResize);
|
||||
$scope.$on('$destroy', () => {
|
||||
angular.element($window).off('resize', onResize);
|
||||
stateMonitor.destroy();
|
||||
});
|
||||
|
||||
this.export = () => {
|
||||
return $scope.getRelativePath()
|
||||
.then(relativePath => {
|
||||
return reportingDocumentControl.create(relativePath);
|
||||
})
|
||||
.then(() => {
|
||||
toastNotifications.addSuccess({
|
||||
title: `Queued report for ${this.objectType}`,
|
||||
text: 'Track its progress in Management',
|
||||
'data-test-subj': 'queueReportSuccess',
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.message === 'not exportable') {
|
||||
return toastNotifications.addWarning({
|
||||
title: 'Only saved dashboards can be exported',
|
||||
text: 'Please save your work first',
|
||||
});
|
||||
}
|
||||
|
||||
toastNotifications.addDanger({
|
||||
title: 'Reporting error',
|
||||
text: err.message || `Can't reach the server. Please try again.`,
|
||||
'data-test-subj': 'queueReportError',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.copyToClipboard = selector => {
|
||||
// updating the URL in the input because it could have potentially changed and we missed the update
|
||||
$scope.updateUrl()
|
||||
.then(() => {
|
||||
|
||||
// we're using $timeout to make sure the URL has been updated in the HTML as this is where
|
||||
// we're copying the ext from
|
||||
$timeout(() => {
|
||||
const copyTextarea = $document.find(selector)[0];
|
||||
copyTextarea.select();
|
||||
|
||||
try {
|
||||
const isCopied = document.execCommand('copy');
|
||||
if (isCopied) {
|
||||
toastNotifications.add('URL copied to clipboard');
|
||||
} else {
|
||||
toastNotifications.add('Press Ctrl+C to copy URL');
|
||||
}
|
||||
} catch (err) {
|
||||
toastNotifications.add('Press Ctrl+C to copy URL');
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
export-config {
|
||||
.generate-controls {
|
||||
button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-group {
|
||||
|
||||
.clipboard-button {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.form-control.url {
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import './export_config';
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
|
||||
uiModules.get('kibana')
|
||||
// disable stat reporting while running tests,
|
||||
// MockInjector used in these tests is not impacted
|
||||
.constant('reportingPollConfig', {
|
||||
jobCompletionNotifier: {
|
||||
interval: 0,
|
||||
intervalErrorMultiplier: 0
|
||||
},
|
||||
jobsRefresh: {
|
||||
interval: 0,
|
||||
intervalErrorMultiplier: 0
|
||||
}
|
||||
});
|
159
x-pack/plugins/reporting/public/hacks/job_completion_notifier.js
Normal file
159
x-pack/plugins/reporting/public/hacks/job_completion_notifier.js
Normal file
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import chrome from 'ui/chrome';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { addSystemApiHeader } from 'ui/system_api';
|
||||
import { get } from 'lodash';
|
||||
import {
|
||||
API_BASE_URL
|
||||
} from '../../common/constants';
|
||||
import 'plugins/reporting/services/job_queue';
|
||||
import 'plugins/reporting/services/job_completion_notifications';
|
||||
import { PathProvider } from 'plugins/xpack_main/services/path';
|
||||
import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info';
|
||||
import { Poller } from '../../../../common/poller';
|
||||
import {
|
||||
EuiButton,
|
||||
} from '@elastic/eui';
|
||||
|
||||
/**
|
||||
* Poll for changes to reports. Inform the user of changes when the license is active.
|
||||
*/
|
||||
uiModules.get('kibana')
|
||||
.run(($http, reportingJobQueue, Private, reportingPollConfig, reportingJobCompletionNotifications) => {
|
||||
// Don't show users any reporting toasts until they're logged in.
|
||||
if (Private(PathProvider).isLoginOrLogout()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We assume that all license types offer Reporting, and that we only need to check if the
|
||||
// license is active or expired.
|
||||
const xpackInfo = Private(XPackInfoProvider);
|
||||
const isLicenseActive = xpackInfo.getLicense().isActive;
|
||||
|
||||
async function showCompletionNotification(job) {
|
||||
const reportObjectTitle = job._source.payload.title;
|
||||
const reportObjectType = job._source.payload.type;
|
||||
|
||||
const isJobSuccessful = get(job, '_source.status') === 'completed';
|
||||
|
||||
if (!isJobSuccessful) {
|
||||
const errorDoc = await reportingJobQueue.getContent(job._id);
|
||||
const text = errorDoc.content;
|
||||
return toastNotifications.addDanger({
|
||||
title: `Couldn't create report for ${reportObjectType} '${reportObjectTitle}'`,
|
||||
text,
|
||||
});
|
||||
}
|
||||
|
||||
let seeReportLink;
|
||||
|
||||
// In-case the license expired/changed between the time they queued the job and the time that
|
||||
// the job completes, that way we don't give the user a toast to download their report if they can't.
|
||||
if (chrome.navLinkExists('kibana:management')) {
|
||||
const managementUrl = chrome.getNavLinkById('kibana:management').url;
|
||||
const reportingSectionUrl = `${managementUrl}/kibana/reporting`;
|
||||
seeReportLink = (
|
||||
<p>
|
||||
Pick it up from <a href={reportingSectionUrl}>Management > Kibana > Reporting</a>.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
const downloadReportButton = (
|
||||
<EuiButton
|
||||
size="s"
|
||||
data-test-subj="downloadCompletedReportButton"
|
||||
onClick={() => { downloadReport(job._id); }}
|
||||
>
|
||||
Download report
|
||||
</EuiButton>
|
||||
);
|
||||
|
||||
const maxSizeReached = get(job, '_source.output.max_size_reached');
|
||||
|
||||
if (maxSizeReached) {
|
||||
return toastNotifications.addWarning({
|
||||
title: `Created partial report for ${reportObjectType} '${reportObjectTitle}'`,
|
||||
text: (
|
||||
<div>
|
||||
<p>The report reached the max size and contains partial data.</p>
|
||||
{seeReportLink}
|
||||
{downloadReportButton}
|
||||
</div>
|
||||
),
|
||||
'data-test-subj': 'completeReportSuccess',
|
||||
});
|
||||
}
|
||||
|
||||
toastNotifications.addSuccess({
|
||||
title: `Created report for ${reportObjectType} '${reportObjectTitle}'`,
|
||||
text: (
|
||||
<div>
|
||||
{seeReportLink}
|
||||
{downloadReportButton}
|
||||
</div>
|
||||
),
|
||||
'data-test-subj': 'completeReportSuccess',
|
||||
});
|
||||
}
|
||||
|
||||
const { jobCompletionNotifier } = reportingPollConfig;
|
||||
|
||||
const poller = new Poller({
|
||||
functionToPoll: async () => {
|
||||
if (!isLicenseActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
const jobIds = reportingJobCompletionNotifications.getAll();
|
||||
if (!jobIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const jobs = await getJobs($http, jobIds);
|
||||
jobIds.forEach(async jobId => {
|
||||
const job = jobs.find(j => j._id === jobId);
|
||||
if (!job) {
|
||||
reportingJobCompletionNotifications.remove(jobId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (job._source.status === 'completed' || job._source.status === 'failed') {
|
||||
await showCompletionNotification(job);
|
||||
reportingJobCompletionNotifications.remove(job.id);
|
||||
return;
|
||||
}
|
||||
});
|
||||
},
|
||||
pollFrequencyInMillis: jobCompletionNotifier.interval,
|
||||
trailing: true,
|
||||
continuePollingOnError: true,
|
||||
pollFrequencyErrorMultiplier: jobCompletionNotifier.intervalErrorMultiplier
|
||||
});
|
||||
poller.start();
|
||||
});
|
||||
|
||||
async function getJobs($http, jobs) {
|
||||
// Get all jobs in "completed" status since last check, sorted by completion time
|
||||
const apiBaseUrl = chrome.addBasePath(API_BASE_URL);
|
||||
|
||||
// Only getting the first 10, to prevent URL overflows
|
||||
const url = `${apiBaseUrl}/jobs/list?ids=${jobs.slice(0, 10).join(',')}`;
|
||||
const headers = addSystemApiHeader({});
|
||||
const response = await $http.get(url, { headers });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
function downloadReport(jobId) {
|
||||
const apiBaseUrl = chrome.addBasePath(API_BASE_URL);
|
||||
const downloadLink = `${apiBaseUrl}/jobs/download/${jobId}`;
|
||||
window.open(downloadLink);
|
||||
}
|
||||
|
22
x-pack/plugins/reporting/public/less/main.less
Normal file
22
x-pack/plugins/reporting/public/less/main.less
Normal file
|
@ -0,0 +1,22 @@
|
|||
@import "~ui/styles/variables/colors.less";
|
||||
|
||||
.kbn-management-reporting {
|
||||
.metadata {
|
||||
color: @kibanaGray3;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: @kibanaRed1;
|
||||
}
|
||||
|
||||
// job list styles
|
||||
.job-list {
|
||||
td.actions {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.job-list.loading {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
21
x-pack/plugins/reporting/public/register_feature.js
Normal file
21
x-pack/plugins/reporting/public/register_feature.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue';
|
||||
|
||||
FeatureCatalogueRegistryProvider.register(() => {
|
||||
return {
|
||||
id: 'reporting',
|
||||
title: 'Reporting',
|
||||
description: 'Manage your reports generated from Discover, Visualize, and Dashboard.',
|
||||
icon: '/plugins/reporting/assets/app_reporting.svg',
|
||||
path: '/app/kibana#/management/kibana/reporting',
|
||||
showOnHomePage: false,
|
||||
category: FeatureCatalogueCategory.ADMIN
|
||||
};
|
||||
});
|
38
x-pack/plugins/reporting/public/services/document_control.js
Normal file
38
x-pack/plugins/reporting/public/services/document_control.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import 'plugins/reporting/services/job_completion_notifications';
|
||||
import chrome from 'ui/chrome';
|
||||
import rison from 'rison-node';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { QueryString } from 'ui/utils/query_string';
|
||||
|
||||
uiModules.get('xpack/reporting')
|
||||
.service('reportingDocumentControl', function (Private, $http, reportingJobCompletionNotifications, $injector) {
|
||||
const $Promise = $injector.get('Promise');
|
||||
const mainEntry = '/api/reporting/generate';
|
||||
const reportPrefix = chrome.addBasePath(mainEntry);
|
||||
|
||||
const getJobParams = (exportType, controller, options) => {
|
||||
const jobParamsProvider = Private(exportType.JobParamsProvider);
|
||||
return $Promise.resolve(jobParamsProvider(controller, options));
|
||||
};
|
||||
|
||||
this.getPath = (exportType, controller, options) => {
|
||||
return getJobParams(exportType, controller, options)
|
||||
.then(jobParams => {
|
||||
return `${reportPrefix}/${exportType.id}?${QueryString.param('jobParams', rison.encode(jobParams))}`;
|
||||
});
|
||||
};
|
||||
|
||||
this.create = (relativePath) => {
|
||||
return $http.post(relativePath, {})
|
||||
.then(({ data }) => {
|
||||
reportingJobCompletionNotifications.add(data.job.id);
|
||||
return data;
|
||||
});
|
||||
};
|
||||
});
|
20
x-pack/plugins/reporting/public/services/export_types.js
Normal file
20
x-pack/plugins/reporting/public/services/export_types.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { ExportTypesRegistry } from '../../common/export_types_registry';
|
||||
|
||||
export const exportTypesRegistry = new ExportTypesRegistry();
|
||||
|
||||
const context = require.context('../../export_types', true, /public\/index.js/);
|
||||
context.keys().forEach(key => context(key).register(exportTypesRegistry));
|
||||
|
||||
uiModules.get('xpack/reporting')
|
||||
.service('reportingExportTypes', function () {
|
||||
this.getById = (exportTypeId) => {
|
||||
return exportTypesRegistry.getById(exportTypeId);
|
||||
};
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../../common/constants';
|
||||
|
||||
class JobCompletionNotifications {
|
||||
|
||||
add(jobId) {
|
||||
const jobs = this.getAll();
|
||||
jobs.push(jobId);
|
||||
this._set(jobs);
|
||||
}
|
||||
|
||||
getAll() {
|
||||
const sessionValue = sessionStorage.getItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY);
|
||||
return sessionValue ? JSON.parse(sessionValue) : [];
|
||||
}
|
||||
|
||||
remove(jobId) {
|
||||
const jobs = this.getAll();
|
||||
const index = jobs.indexOf(jobId);
|
||||
if (!index) {
|
||||
throw new Error('Unable to find job to remove it');
|
||||
}
|
||||
|
||||
jobs.splice(index, 1);
|
||||
this._set(jobs);
|
||||
}
|
||||
|
||||
_set(jobs) {
|
||||
sessionStorage.setItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JSON.stringify(jobs));
|
||||
}
|
||||
}
|
||||
|
||||
uiModules.get('xpack/reporting')
|
||||
.factory('reportingJobCompletionNotifications', function () {
|
||||
return new JobCompletionNotifications();
|
||||
});
|
42
x-pack/plugins/reporting/public/services/job_queue.js
Normal file
42
x-pack/plugins/reporting/public/services/job_queue.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import url from 'url';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { addSystemApiHeader } from 'ui/system_api';
|
||||
|
||||
const module = uiModules.get('xpack/reporting');
|
||||
|
||||
module.service('reportingJobQueue', ($http) => {
|
||||
const baseUrl = '../api/reporting/jobs';
|
||||
|
||||
return {
|
||||
list(page = 0) {
|
||||
const urlObj = {
|
||||
pathname: `${baseUrl}/list`,
|
||||
query: { page }
|
||||
};
|
||||
|
||||
const headers = addSystemApiHeader({});
|
||||
return $http.get(url.format(urlObj), { headers })
|
||||
.then((res) => res.data);
|
||||
},
|
||||
|
||||
total() {
|
||||
const urlObj = { pathname: `${baseUrl}/count` };
|
||||
|
||||
const headers = addSystemApiHeader({});
|
||||
return $http.get(url.format(urlObj), { headers })
|
||||
.then((res) => res.data);
|
||||
},
|
||||
|
||||
getContent(jobId) {
|
||||
const urlObj = { pathname: `${baseUrl}/output/${jobId}` };
|
||||
return $http.get(url.format(urlObj))
|
||||
.then((res) => res.data);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import './management';
|
84
x-pack/plugins/reporting/public/views/management/jobs.html
Normal file
84
x-pack/plugins/reporting/public/views/management/jobs.html
Normal file
|
@ -0,0 +1,84 @@
|
|||
<kbn-management-app section="kibana">
|
||||
<div class="euiPage">
|
||||
<h1 class="euiTitle">Generated reports</h1>
|
||||
|
||||
<table class="table table-striped job-list" ng-class="{ loading: jobsCtrl.loading }">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Document</th>
|
||||
<th scope="col">Added</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-if="!jobsCtrl.reportingJobs.jobs.length">
|
||||
<td colspan="5">No reports have been created</td>
|
||||
</tr>
|
||||
<tr ng-if="jobsCtrl.reportingJobs.jobs.length" ng-repeat="job in jobsCtrl.reportingJobs.jobs">
|
||||
<td>
|
||||
<div>{{ job.object_title }}</div>
|
||||
<div class="metadata">{{ job.object_type }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div>{{ job.created_at | date : 'yyyy-MM-dd @ h:mm a' }}</div>
|
||||
<div class="metadata" ng-if="job.created_by">{{ job.created_by }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div ng-class="{kuiStatusText: true, 'kuiStatusText--warning': job.max_size_reached}">
|
||||
{{ job.status }}<span ng-if="job.max_size_reached"> - max size reached</span>
|
||||
</div>
|
||||
<div
|
||||
class="metadata"
|
||||
ng-if="job.status !== 'pending'"
|
||||
>
|
||||
{{ job.started_at | date : 'yyyy-MM-dd @ h:mm a' }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="actions">
|
||||
<button
|
||||
class="kuiButton kuiButton--danger"
|
||||
ng-if="job.status === 'failed' && jobsCtrl.errorMessage.job_id !== job.id"
|
||||
ng-click=jobsCtrl.showError(job.id)
|
||||
aria-label="Show report-generation error"
|
||||
>
|
||||
<span class="kuiIcon fa-question-circle"></span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="error-message"
|
||||
ng-if="jobsCtrl.errorMessage.job_id === job.id"
|
||||
>
|
||||
{{ jobsCtrl.errorMessage.message }}
|
||||
</div>
|
||||
|
||||
<button
|
||||
ng-if="job.status === 'completed'"
|
||||
ng-click=jobsCtrl.download(job.id)
|
||||
ng-class="{ kuiButton: true,
|
||||
'kuiButton--basic': !job.max_size_reached,
|
||||
'kuiButton--warning': job.max_size_reached}"
|
||||
aria-label="Download report"
|
||||
ng-attr-tooltip="{{
|
||||
job.max_size_reached ? 'Max size reached, contains partial data.' : null
|
||||
}}"
|
||||
>
|
||||
<span class="kuiIcon fa-download"></span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div style="text-align: center;">
|
||||
<paging
|
||||
page="jobsCtrl.currentPage"
|
||||
page-size="10"
|
||||
total="jobsCtrl.reportingJobs.total"
|
||||
show-prev-next="true"
|
||||
show-first-last="true"
|
||||
paging-action="jobsCtrl.setPage(page)">
|
||||
</paging>
|
||||
</div>
|
||||
</div>
|
||||
</kbn-management-app>
|
144
x-pack/plugins/reporting/public/views/management/jobs.js
Normal file
144
x-pack/plugins/reporting/public/views/management/jobs.js
Normal file
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import 'angular-paging';
|
||||
import 'plugins/reporting/services/job_queue';
|
||||
import 'plugins/reporting/less/main.less';
|
||||
import { Notifier } from 'ui/notify';
|
||||
import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info';
|
||||
|
||||
import routes from 'ui/routes';
|
||||
import template from 'plugins/reporting/views/management/jobs.html';
|
||||
import { Poller } from '../../../../../common/poller';
|
||||
|
||||
const pageSize = 10;
|
||||
|
||||
function mapJobs(jobs) {
|
||||
return jobs.map((job) => {
|
||||
return {
|
||||
id: job._id,
|
||||
type: job._source.jobtype,
|
||||
object_type: job._source.payload.type,
|
||||
object_title: job._source.payload.title,
|
||||
created_by: job._source.created_by,
|
||||
created_at: job._source.created_at,
|
||||
started_at: job._source.started_at,
|
||||
completed_at: job._source.completed_at,
|
||||
status: job._source.status,
|
||||
content_type: job._source.output ? job._source.output.content_type : false,
|
||||
max_size_reached: job._source.output ? job._source.output.max_size_reached : false
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
routes.when('/management/kibana/reporting', {
|
||||
template,
|
||||
controllerAs: 'jobsCtrl',
|
||||
controller($scope, $route, $window, $interval, reportingJobQueue, kbnUrl, Private, reportingPollConfig) {
|
||||
const { jobsRefresh } = reportingPollConfig;
|
||||
const notifier = new Notifier({ location: 'Reporting' });
|
||||
const xpackInfo = Private(XPackInfoProvider);
|
||||
|
||||
this.loading = false;
|
||||
this.pageSize = pageSize;
|
||||
this.currentPage = 1;
|
||||
this.reportingJobs = [];
|
||||
|
||||
const licenseAllowsToShowThisPage = () => {
|
||||
return xpackInfo.get('features.reporting.management.showLinks')
|
||||
&& xpackInfo.get('features.reporting.management.enableLinks');
|
||||
};
|
||||
|
||||
const notifyAndRedirectToManagementOverviewPage = () => {
|
||||
notifier.error(xpackInfo.get('features.reporting.management.message'));
|
||||
kbnUrl.redirect('/management');
|
||||
return Promise.reject();
|
||||
};
|
||||
|
||||
const getJobs = (page = 0) => {
|
||||
return reportingJobQueue.list(page)
|
||||
.then((jobs) => {
|
||||
return reportingJobQueue.total()
|
||||
.then((total) => {
|
||||
const mappedJobs = mapJobs(jobs);
|
||||
return {
|
||||
jobs: mappedJobs,
|
||||
total: total,
|
||||
pages: Math.ceil(total / pageSize),
|
||||
};
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!licenseAllowsToShowThisPage()) {
|
||||
return notifyAndRedirectToManagementOverviewPage();
|
||||
}
|
||||
|
||||
if (err.status !== 401 && err.status !== 403) {
|
||||
notifier.error(err.statusText || 'Request failed');
|
||||
}
|
||||
|
||||
return {
|
||||
jobs: [],
|
||||
total: 0,
|
||||
pages: 1,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const toggleLoading = () => {
|
||||
this.loading = !this.loading;
|
||||
};
|
||||
|
||||
const updateJobs = () => {
|
||||
return getJobs(this.currentPage - 1)
|
||||
.then((jobs) => {
|
||||
this.reportingJobs = jobs;
|
||||
});
|
||||
};
|
||||
|
||||
const updateJobsLoading = () => {
|
||||
toggleLoading();
|
||||
updateJobs().then(toggleLoading);
|
||||
};
|
||||
|
||||
// pagination logic
|
||||
this.setPage = (page) => {
|
||||
this.currentPage = page;
|
||||
};
|
||||
|
||||
// job list updating
|
||||
const poller = new Poller({
|
||||
functionToPoll: () => {
|
||||
return updateJobs();
|
||||
},
|
||||
pollFrequencyInMillis: jobsRefresh.interval,
|
||||
trailing: true,
|
||||
continuePollingOnError: true,
|
||||
pollFrequencyErrorMultiplier: jobsRefresh.intervalErrorMultiplier
|
||||
});
|
||||
poller.start();
|
||||
|
||||
// control handlers
|
||||
this.download = (jobId) => {
|
||||
$window.open(`../api/reporting/jobs/download/${jobId}`);
|
||||
};
|
||||
|
||||
// fetch and show job error details
|
||||
this.showError = (jobId) => {
|
||||
reportingJobQueue.getContent(jobId)
|
||||
.then((doc) => {
|
||||
this.errorMessage = {
|
||||
job_id: jobId,
|
||||
message: doc.content,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('jobsCtrl.currentPage', updateJobsLoading);
|
||||
|
||||
$scope.$on('$destroy', () => poller.stop());
|
||||
}
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { management } from 'ui/management';
|
||||
import routes from 'ui/routes';
|
||||
import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info';
|
||||
|
||||
import 'plugins/reporting/views/management/jobs';
|
||||
|
||||
routes.defaults(/\/management/, {
|
||||
resolve: {
|
||||
reportingManagementSection: function (Private) {
|
||||
const xpackInfo = Private(XPackInfoProvider);
|
||||
const kibanaManagementSection = management.getSection('kibana');
|
||||
const showReportingLinks = xpackInfo.get('features.reporting.management.showLinks');
|
||||
|
||||
kibanaManagementSection.deregister('reporting');
|
||||
if (showReportingLinks) {
|
||||
const enableReportingLinks = xpackInfo.get('features.reporting.management.enableLinks');
|
||||
const tooltipMessage = xpackInfo.get('features.reporting.management.message');
|
||||
|
||||
let url;
|
||||
let tooltip;
|
||||
if (enableReportingLinks) {
|
||||
url = '#/management/kibana/reporting';
|
||||
} else {
|
||||
tooltip = tooltipMessage;
|
||||
}
|
||||
|
||||
return kibanaManagementSection.register('reporting', {
|
||||
order: 15,
|
||||
display: 'Reporting',
|
||||
url,
|
||||
tooltip
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue