Migrate x-pack-kibana source to kibana

This commit is contained in:
Jenkins CI 2018-04-20 19:13:41 +00:00 committed by spalger
parent e8ac7d8d32
commit bc5b51554d
3256 changed files with 277621 additions and 2324 deletions

View 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

View 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);

View 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);

View 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);

View file

@ -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>

View file

@ -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');
}
});
});
};
}
};
});

View file

@ -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;
}
}
}

View file

@ -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';

View 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 { 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
}
});

View 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 &gt; Kibana &gt; 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);
}

View 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;
}
}

View 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
};
});

View 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;
});
};
});

View 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);
};
});

View 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 { 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();
});

View 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);
}
};
});

View file

@ -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';

View 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">&nbsp;- 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>

View 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());
}
});

View 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 { 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
});
}
}
}
});