mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
* [Reporting] Replace Job Completion Notifier as NP Plugin * beautiful toasts * unit test * showNotifications returns observable * fix kibana.json * depends on links * no prettier ignore * Update content per feedback * Remove unnecessary wrapper * remove type annos and condense * use an observable of the stop method * rename the new platform plugin to match legacy * fix i18n config * additional plugin rename * i18n strings prefix change * try something with x-pack/.i18nrc.json * remove a few more notifier phrasing from plugin definition
This commit is contained in:
parent
620ad37c71
commit
af952997c5
25 changed files with 1130 additions and 250 deletions
|
@ -27,7 +27,7 @@
|
|||
"xpack.main": "legacy/plugins/xpack_main",
|
||||
"xpack.monitoring": "legacy/plugins/monitoring",
|
||||
"xpack.remoteClusters": "legacy/plugins/remote_clusters",
|
||||
"xpack.reporting": "legacy/plugins/reporting",
|
||||
"xpack.reporting": [ "plugins/reporting", "legacy/plugins/reporting" ],
|
||||
"xpack.rollupJobs": "legacy/plugins/rollup",
|
||||
"xpack.searchProfiler": "legacy/plugins/searchprofiler",
|
||||
"xpack.siem": "legacy/plugins/siem",
|
||||
|
|
|
@ -45,7 +45,6 @@ export const reporting = (kibana) => {
|
|||
embeddableActions: [
|
||||
'plugins/reporting/panel_actions/get_csv_panel_action',
|
||||
],
|
||||
hacks: ['plugins/reporting/hacks/job_completion_notifier'],
|
||||
home: ['plugins/reporting/register_feature'],
|
||||
managementSections: ['plugins/reporting/views/management'],
|
||||
injectDefaultVars(server, options) {
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
/*
|
||||
* 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
|
||||
}
|
||||
});
|
|
@ -1,207 +0,0 @@
|
|||
/*
|
||||
* 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 { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { get } from 'lodash';
|
||||
import { jobQueueClient } from 'plugins/reporting/lib/job_queue_client';
|
||||
import { jobCompletionNotifications } from 'plugins/reporting/lib/job_completion_notifications';
|
||||
import { JobStatuses } from '../constants/job_statuses';
|
||||
import { Path } from 'plugins/xpack_main/services/path';
|
||||
import { xpackInfo } from 'plugins/xpack_main/services/xpack_info';
|
||||
import { Poller } from '../../../../common/poller';
|
||||
import {
|
||||
EuiButton,
|
||||
} from '@elastic/eui';
|
||||
import { downloadReport } from '../lib/download_report';
|
||||
import { npStart } from 'ui/new_platform';
|
||||
|
||||
/**
|
||||
* Poll for changes to reports. Inform the user of changes when the license is active.
|
||||
*/
|
||||
uiModules.get('kibana')
|
||||
.run(reportingPollConfig => {
|
||||
// Don't show users any reporting toasts until they're logged in.
|
||||
if (Path.isUnauthenticated()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We assume that all license types offer Reporting, and that we only need to check if the
|
||||
// license is active or expired.
|
||||
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') === JobStatuses.COMPLETED;
|
||||
|
||||
if (!isJobSuccessful) {
|
||||
const errorDoc = await jobQueueClient.getContent(job._id);
|
||||
const text = errorDoc.content;
|
||||
return toastNotifications.addDanger({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.jobCompletionNotifier.couldNotCreateReportTitle"
|
||||
defaultMessage="Couldn't create report for {reportObjectType} '{reportObjectTitle}'"
|
||||
values={{ reportObjectType, reportObjectTitle }}
|
||||
/>
|
||||
),
|
||||
text,
|
||||
});
|
||||
}
|
||||
|
||||
let seeReportLink;
|
||||
|
||||
const { chrome } = npStart.core;
|
||||
|
||||
// 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.
|
||||
// NOTE: this should be looking at configuration rather than the existence of a navLink
|
||||
if (chrome.navLinks.has('kibana:management')) {
|
||||
const { baseUrl } = chrome.navLinks.get('kibana:management');
|
||||
const reportingSectionUrl = `${baseUrl}/kibana/reporting`;
|
||||
|
||||
seeReportLink = (
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.jobCompletionNotifier.reportLink.pickItUpFromPathDescription"
|
||||
defaultMessage="Pick it up from {path}."
|
||||
values={{ path: (
|
||||
<a href={reportingSectionUrl}>
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.jobCompletionNotifier.reportLink.reportingSectionUrlLinkLabel"
|
||||
defaultMessage="Management > Kibana > Reporting"
|
||||
/>
|
||||
</a>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
const downloadReportButton = (
|
||||
<EuiButton
|
||||
size="s"
|
||||
data-test-subj="downloadCompletedReportButton"
|
||||
onClick={() => { downloadReport(job._id); }}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.jobCompletionNotifier.downloadReportButtonLabel"
|
||||
defaultMessage="Download report"
|
||||
/>
|
||||
</EuiButton>
|
||||
);
|
||||
|
||||
const maxSizeReached = get(job, '_source.output.max_size_reached');
|
||||
const csvContainsFormulas = get(job, '_source.output.csv_contains_formulas');
|
||||
|
||||
if (csvContainsFormulas) {
|
||||
return toastNotifications.addWarning({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.jobCompletionNotifier.csvContainsFormulas.formulaReportTitle"
|
||||
defaultMessage="Report may contain formulas {reportObjectType} '{reportObjectTitle}'"
|
||||
values={{ reportObjectType, reportObjectTitle }}
|
||||
/>
|
||||
),
|
||||
text: (
|
||||
<div>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.jobCompletionNotifier.csvContainsFormulas.formulaReportMessage"
|
||||
defaultMessage="The report contains characters which spreadsheet applications can interpret as formulas."
|
||||
/>
|
||||
</p>
|
||||
{seeReportLink}
|
||||
{downloadReportButton}
|
||||
</div>
|
||||
),
|
||||
'data-test-subj': 'completeReportSuccess',
|
||||
});
|
||||
}
|
||||
|
||||
if (maxSizeReached) {
|
||||
return toastNotifications.addWarning({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.jobCompletionNotifier.maxSizeReached.partialReportTitle"
|
||||
defaultMessage="Created partial report for {reportObjectType} '{reportObjectTitle}'"
|
||||
values={{ reportObjectType, reportObjectTitle }}
|
||||
/>
|
||||
),
|
||||
text: (
|
||||
<div>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.jobCompletionNotifier.maxSizeReached.partialReportDescription"
|
||||
defaultMessage="The report reached the max size and contains partial data."
|
||||
/>
|
||||
</p>
|
||||
{seeReportLink}
|
||||
{downloadReportButton}
|
||||
</div>
|
||||
),
|
||||
'data-test-subj': 'completeReportSuccess',
|
||||
});
|
||||
}
|
||||
|
||||
toastNotifications.addSuccess({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.jobCompletionNotifier.successfullyCreatedReportNotificationTitle"
|
||||
defaultMessage="Created report for {reportObjectType} '{reportObjectTitle}'"
|
||||
values={{ 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 = jobCompletionNotifications.getAll();
|
||||
if (!jobIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const jobs = await jobQueueClient.list(0, jobIds);
|
||||
jobIds.forEach(async jobId => {
|
||||
const job = jobs.find(j => j._id === jobId);
|
||||
if (!job) {
|
||||
jobCompletionNotifications.remove(jobId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (job._source.status === JobStatuses.COMPLETED || job._source.status === JobStatuses.FAILED) {
|
||||
await showCompletionNotification(job);
|
||||
jobCompletionNotifications.remove(job.id);
|
||||
return;
|
||||
}
|
||||
});
|
||||
},
|
||||
pollFrequencyInMillis: jobCompletionNotifier.interval,
|
||||
trailing: true,
|
||||
continuePollingOnError: true,
|
||||
pollFrequencyErrorMultiplier: jobCompletionNotifier.intervalErrorMultiplier
|
||||
});
|
||||
poller.start();
|
||||
});
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import boom from 'boom';
|
||||
import { RequestQuery } from 'hapi';
|
||||
import { Request, ResponseToolkit } from 'hapi';
|
||||
import { API_BASE_URL } from '../../common/constants';
|
||||
import { JobDoc, KbnServer } from '../../types';
|
||||
|
@ -19,6 +20,12 @@ import {
|
|||
|
||||
const MAIN_ENTRY = `${API_BASE_URL}/jobs`;
|
||||
|
||||
type ListQuery = RequestQuery & {
|
||||
page: string;
|
||||
size: string;
|
||||
ids?: string; // optional field forbids us from extending RequestQuery
|
||||
};
|
||||
|
||||
export function registerJobs(server: KbnServer) {
|
||||
const jobsQuery = jobsQueryFactory(server);
|
||||
const getRouteConfig = getRouteConfigFactoryManagementPre(server);
|
||||
|
@ -30,12 +37,10 @@ export function registerJobs(server: KbnServer) {
|
|||
method: 'GET',
|
||||
config: getRouteConfig(),
|
||||
handler: (request: Request) => {
|
||||
// @ts-ignore
|
||||
const page = parseInt(request.query.page, 10) || 0;
|
||||
// @ts-ignore
|
||||
const size = Math.min(100, parseInt(request.query.size, 10) || 10);
|
||||
// @ts-ignore
|
||||
const jobIds = request.query.ids ? request.query.ids.split(',') : null;
|
||||
const { page: queryPage, size: querySize, ids: queryIds } = request.query as ListQuery;
|
||||
const page = parseInt(queryPage, 10) || 0;
|
||||
const size = Math.min(100, parseInt(querySize, 10) || 10);
|
||||
const jobIds = queryIds ? queryIds.split(',') : null;
|
||||
|
||||
const results = jobsQuery.list(
|
||||
request.pre.management.jobTypes,
|
||||
|
|
10
x-pack/plugins/reporting/config.ts
Normal file
10
x-pack/plugins/reporting/config.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const reportingPollConfig = {
|
||||
jobCompletionNotifier: { interval: 10000, intervalErrorMultiplier: 5 },
|
||||
jobsRefresh: { interval: 5000, intervalErrorMultiplier: 5 },
|
||||
};
|
21
x-pack/plugins/reporting/constants.ts
Normal file
21
x-pack/plugins/reporting/constants.ts
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.
|
||||
*/
|
||||
|
||||
export const JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY =
|
||||
'xpack.reporting.jobCompletionNotifications';
|
||||
|
||||
export const JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG = {
|
||||
jobCompletionNotifier: {
|
||||
interval: 10000,
|
||||
intervalErrorMultiplier: 5,
|
||||
},
|
||||
};
|
||||
|
||||
export const API_BASE_URL = '/api/reporting/jobs';
|
||||
export const REPORTING_MANAGEMENT_HOME = '/app/kibana#/management/kibana/reporting';
|
||||
|
||||
export const JOB_STATUS_FAILED = 'failed';
|
||||
export const JOB_STATUS_COMPLETED = 'completed';
|
59
x-pack/plugins/reporting/index.d.ts
vendored
Normal file
59
x-pack/plugins/reporting/index.d.ts
vendored
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 {
|
||||
CoreSetup,
|
||||
CoreStart,
|
||||
HttpServiceBase,
|
||||
Plugin,
|
||||
PluginInitializerContext,
|
||||
NotificationsStart,
|
||||
} from '../../../src/core/public';
|
||||
|
||||
export type JobId = string;
|
||||
export type JobStatus = 'completed' | 'pending' | 'processing' | 'failed';
|
||||
|
||||
export type HttpService = HttpServiceBase;
|
||||
export type NotificationsService = NotificationsStart;
|
||||
|
||||
export interface SourceJob {
|
||||
_id: JobId;
|
||||
_source: {
|
||||
status: JobStatus;
|
||||
output: {
|
||||
max_size_reached: boolean;
|
||||
csv_contains_formulas: boolean;
|
||||
};
|
||||
payload: {
|
||||
type: string;
|
||||
title: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface JobContent {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface JobSummary {
|
||||
id: JobId;
|
||||
status: JobStatus;
|
||||
title: string;
|
||||
type: string;
|
||||
maxSizeReached: boolean;
|
||||
csvContainsFormulas: boolean;
|
||||
}
|
||||
|
||||
export interface JobStatusBuckets {
|
||||
completed: JobSummary[];
|
||||
failed: JobSummary[];
|
||||
}
|
||||
|
||||
type DownloadLink = string;
|
||||
export type DownloadReportFn = (jobId: JobId) => DownloadLink;
|
||||
|
||||
type ManagementLink = string;
|
||||
export type ManagementLinkFn = () => ManagementLink;
|
8
x-pack/plugins/reporting/kibana.json
Normal file
8
x-pack/plugins/reporting/kibana.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"id": "reporting",
|
||||
"version": "8.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"requiredPlugins": [],
|
||||
"server": false,
|
||||
"ui": true
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { EuiButton } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { JobId, JobSummary } from '../../index.d';
|
||||
|
||||
interface Props {
|
||||
getUrl: (jobId: JobId) => string;
|
||||
job: JobSummary;
|
||||
}
|
||||
|
||||
export const DownloadButton = ({ getUrl, job }: Props) => {
|
||||
const downloadReport = () => {
|
||||
window.open(getUrl(job.id));
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiButton
|
||||
size="s"
|
||||
data-test-subj="downloadCompletedReportButton"
|
||||
onClick={() => {
|
||||
downloadReport();
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.publicNotifier.downloadReportButtonLabel"
|
||||
defaultMessage="Download report"
|
||||
/>
|
||||
</EuiButton>
|
||||
);
|
||||
};
|
28
x-pack/plugins/reporting/public/components/general_error.tsx
Normal file
28
x-pack/plugins/reporting/public/components/general_error.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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, { Fragment } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
|
||||
import { ToastInput } from '../../../../../src/core/public';
|
||||
|
||||
export const getGeneralErrorToast = (errorText: string, err: Error): ToastInput => ({
|
||||
text: (
|
||||
<Fragment>
|
||||
<EuiCallOut title={errorText} color="danger" iconType="alert">
|
||||
{err.toString()}
|
||||
</EuiCallOut>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.publicNotifier.error.tryRefresh"
|
||||
defaultMessage="Try refreshing the page."
|
||||
></FormattedMessage>
|
||||
</Fragment>
|
||||
),
|
||||
iconType: undefined,
|
||||
});
|
11
x-pack/plugins/reporting/public/components/index.ts
Normal file
11
x-pack/plugins/reporting/public/components/index.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { getSuccessToast } from './job_success';
|
||||
export { getFailureToast } from './job_failure';
|
||||
export { getWarningFormulasToast } from './job_warning_formulas';
|
||||
export { getWarningMaxSizeToast } from './job_warning_max_size';
|
||||
export { getGeneralErrorToast } from './general_error';
|
63
x-pack/plugins/reporting/public/components/job_failure.tsx
Normal file
63
x-pack/plugins/reporting/public/components/job_failure.tsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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, { Fragment } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
|
||||
import { ToastInput } from '../../../../../src/core/public';
|
||||
import { JobSummary, ManagementLinkFn } from '../../index.d';
|
||||
|
||||
export const getFailureToast = (
|
||||
errorText: string,
|
||||
job: JobSummary,
|
||||
getManagmenetLink: ManagementLinkFn
|
||||
): ToastInput => {
|
||||
return {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.publicNotifier.error.couldNotCreateReportTitle"
|
||||
defaultMessage="Could not create report for {reportObjectType} '{reportObjectTitle}'."
|
||||
values={{ reportObjectType: job.type, reportObjectTitle: job.title }}
|
||||
/>
|
||||
),
|
||||
text: (
|
||||
<Fragment>
|
||||
<EuiCallOut
|
||||
size="m"
|
||||
title={i18n.translate('xpack.reporting.publicNotifier.error.calloutTitle', {
|
||||
defaultMessage: 'The reporting job failed',
|
||||
})}
|
||||
color="danger"
|
||||
iconType="alert"
|
||||
>
|
||||
{errorText}
|
||||
</EuiCallOut>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.publicNotifier.error.checkManagement"
|
||||
defaultMessage="More information is available at {path}."
|
||||
values={{
|
||||
path: (
|
||||
<a href={getManagmenetLink()}>
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.publicNotifier.error.reportingSectionUrlLinkLabel"
|
||||
defaultMessage="Management > Kibana > Reporting"
|
||||
/>
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</Fragment>
|
||||
),
|
||||
iconType: undefined,
|
||||
'data-test-subj': 'completeReportFailure',
|
||||
};
|
||||
};
|
36
x-pack/plugins/reporting/public/components/job_success.tsx
Normal file
36
x-pack/plugins/reporting/public/components/job_success.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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, { Fragment } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { ToastInput } from '../../../../../src/core/public';
|
||||
import { JobId, JobSummary } from '../../index.d';
|
||||
import { ReportLink } from './report_link';
|
||||
import { DownloadButton } from './download_button';
|
||||
|
||||
export const getSuccessToast = (
|
||||
job: JobSummary,
|
||||
getReportLink: () => string,
|
||||
getDownloadLink: (jobId: JobId) => string
|
||||
): ToastInput => ({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.publicNotifier.successfullyCreatedReportNotificationTitle"
|
||||
defaultMessage="Created report for {reportObjectType} '{reportObjectTitle}'"
|
||||
values={{ reportObjectType: job.type, reportObjectTitle: job.title }}
|
||||
/>
|
||||
),
|
||||
color: 'success',
|
||||
text: (
|
||||
<Fragment>
|
||||
<p>
|
||||
<ReportLink getUrl={getReportLink} />
|
||||
</p>
|
||||
<DownloadButton getUrl={getDownloadLink} job={job} />
|
||||
</Fragment>
|
||||
),
|
||||
'data-test-subj': 'completeReportSuccess',
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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, { Fragment } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { ToastInput } from '../../../../../src/core/public';
|
||||
import { JobId, JobSummary } from '../../index.d';
|
||||
import { ReportLink } from './report_link';
|
||||
import { DownloadButton } from './download_button';
|
||||
|
||||
export const getWarningFormulasToast = (
|
||||
job: JobSummary,
|
||||
getReportLink: () => string,
|
||||
getDownloadLink: (jobId: JobId) => string
|
||||
): ToastInput => ({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.publicNotifier.csvContainsFormulas.formulaReportTitle"
|
||||
defaultMessage="Report may contain formulas {reportObjectType} '{reportObjectTitle}'"
|
||||
values={{ reportObjectType: job.type, reportObjectTitle: job.title }}
|
||||
/>
|
||||
),
|
||||
text: (
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.publicNotifier.csvContainsFormulas.formulaReportMessage"
|
||||
defaultMessage="The report contains characters which spreadsheet applications can interpret as formulas."
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<ReportLink getUrl={getReportLink} />
|
||||
</p>
|
||||
<DownloadButton getUrl={getDownloadLink} job={job} />
|
||||
</Fragment>
|
||||
),
|
||||
'data-test-subj': 'completeReportCsvFormulasWarning',
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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, { Fragment } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { ToastInput } from '../../../../../src/core/public';
|
||||
import { JobId, JobSummary } from '../../index.d';
|
||||
import { ReportLink } from './report_link';
|
||||
import { DownloadButton } from './download_button';
|
||||
|
||||
export const getWarningMaxSizeToast = (
|
||||
job: JobSummary,
|
||||
getReportLink: () => string,
|
||||
getDownloadLink: (jobId: JobId) => string
|
||||
): ToastInput => ({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.publicNotifier.maxSizeReached.partialReportTitle"
|
||||
defaultMessage="Created partial report for {reportObjectType} '{reportObjectTitle}'"
|
||||
values={{ reportObjectType: job.type, reportObjectTitle: job.title }}
|
||||
/>
|
||||
),
|
||||
text: (
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.publicNotifier.maxSizeReached.partialReportDescription"
|
||||
defaultMessage="The report reached the max size and contains partial data."
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<ReportLink getUrl={getReportLink} />
|
||||
</p>
|
||||
<DownloadButton getUrl={getDownloadLink} job={job} />
|
||||
</Fragment>
|
||||
),
|
||||
'data-test-subj': 'completeReportMaxSizeWarning',
|
||||
});
|
29
x-pack/plugins/reporting/public/components/report_link.tsx
Normal file
29
x-pack/plugins/reporting/public/components/report_link.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
interface Props {
|
||||
getUrl: () => string;
|
||||
}
|
||||
|
||||
export const ReportLink = ({ getUrl }: Props) => (
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.publicNotifier.reportLink.pickItUpFromPathDescription"
|
||||
defaultMessage="Pick it up from {path}."
|
||||
values={{
|
||||
path: (
|
||||
<a href={getUrl()}>
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.publicNotifier.reportLink.reportingSectionUrlLinkLabel"
|
||||
defaultMessage="Management > Kibana > Reporting"
|
||||
/>
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
14
x-pack/plugins/reporting/public/index.ts
Normal file
14
x-pack/plugins/reporting/public/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { PluginInitializerContext } from '../../../../src/core/public';
|
||||
import { ReportingPublicPlugin } from './plugin';
|
||||
|
||||
export function plugin(initializerContext: PluginInitializerContext) {
|
||||
return new ReportingPublicPlugin(initializerContext);
|
||||
}
|
||||
|
||||
export { ReportingPublicPlugin as Plugin };
|
198
x-pack/plugins/reporting/public/lib/__snapshots__/stream_handler.test.ts.snap
generated
Normal file
198
x-pack/plugins/reporting/public/lib/__snapshots__/stream_handler.test.ts.snap
generated
Normal file
|
@ -0,0 +1,198 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`stream handler findChangedStatusJobs finds changed status jobs 1`] = `
|
||||
Object {
|
||||
"completed": Array [
|
||||
Object {
|
||||
"csvContainsFormulas": false,
|
||||
"id": "job-source-mock1",
|
||||
"maxSizeReached": false,
|
||||
"status": "completed",
|
||||
"title": "specimen",
|
||||
"type": "spectacular",
|
||||
},
|
||||
],
|
||||
"failed": Array [
|
||||
Object {
|
||||
"csvContainsFormulas": false,
|
||||
"id": "job-source-mock2",
|
||||
"maxSizeReached": false,
|
||||
"status": "failed",
|
||||
"title": "specimen",
|
||||
"type": "spectacular",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`stream handler showNotifications show csv formulas warning 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"data-test-subj": "completeReportCsvFormulasWarning",
|
||||
"text": <React.Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="The report contains characters which spreadsheet applications can interpret as formulas."
|
||||
id="xpack.reporting.publicNotifier.csvContainsFormulas.formulaReportMessage"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<ReportLink
|
||||
getUrl={[Function]}
|
||||
/>
|
||||
</p>
|
||||
<DownloadButton
|
||||
getUrl={[Function]}
|
||||
job={
|
||||
Object {
|
||||
"csvContainsFormulas": true,
|
||||
"id": "yas3",
|
||||
"status": "completed",
|
||||
"title": "Yas",
|
||||
"type": "yas",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</React.Fragment>,
|
||||
"title": <FormattedMessage
|
||||
defaultMessage="Report may contain formulas {reportObjectType} '{reportObjectTitle}'"
|
||||
id="xpack.reporting.publicNotifier.csvContainsFormulas.formulaReportTitle"
|
||||
values={
|
||||
Object {
|
||||
"reportObjectTitle": "Yas",
|
||||
"reportObjectType": "yas",
|
||||
}
|
||||
}
|
||||
/>,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`stream handler showNotifications show failed job toast 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"data-test-subj": "completeReportFailure",
|
||||
"iconType": undefined,
|
||||
"text": <React.Fragment>
|
||||
<EuiCallOut
|
||||
color="danger"
|
||||
iconType="alert"
|
||||
size="m"
|
||||
title="The reporting job failed"
|
||||
>
|
||||
this is the completed report data
|
||||
</EuiCallOut>
|
||||
<EuiSpacer />
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="More information is available at {path}."
|
||||
id="xpack.reporting.publicNotifier.error.checkManagement"
|
||||
values={
|
||||
Object {
|
||||
"path": <a>
|
||||
<FormattedMessage
|
||||
defaultMessage="Management > Kibana > Reporting"
|
||||
id="xpack.reporting.publicNotifier.error.reportingSectionUrlLinkLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
</a>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
</React.Fragment>,
|
||||
"title": <FormattedMessage
|
||||
defaultMessage="Could not create report for {reportObjectType} '{reportObjectTitle}'."
|
||||
id="xpack.reporting.publicNotifier.error.couldNotCreateReportTitle"
|
||||
values={
|
||||
Object {
|
||||
"reportObjectTitle": "Yas 7",
|
||||
"reportObjectType": "yas",
|
||||
}
|
||||
}
|
||||
/>,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`stream handler showNotifications show max length warning 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"data-test-subj": "completeReportMaxSizeWarning",
|
||||
"text": <React.Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="The report reached the max size and contains partial data."
|
||||
id="xpack.reporting.publicNotifier.maxSizeReached.partialReportDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<ReportLink
|
||||
getUrl={[Function]}
|
||||
/>
|
||||
</p>
|
||||
<DownloadButton
|
||||
getUrl={[Function]}
|
||||
job={
|
||||
Object {
|
||||
"id": "yas2",
|
||||
"maxSizeReached": true,
|
||||
"status": "completed",
|
||||
"title": "Yas",
|
||||
"type": "yas",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</React.Fragment>,
|
||||
"title": <FormattedMessage
|
||||
defaultMessage="Created partial report for {reportObjectType} '{reportObjectTitle}'"
|
||||
id="xpack.reporting.publicNotifier.maxSizeReached.partialReportTitle"
|
||||
values={
|
||||
Object {
|
||||
"reportObjectTitle": "Yas",
|
||||
"reportObjectType": "yas",
|
||||
}
|
||||
}
|
||||
/>,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`stream handler showNotifications show success 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"color": "success",
|
||||
"data-test-subj": "completeReportSuccess",
|
||||
"text": <React.Fragment>
|
||||
<p>
|
||||
<ReportLink
|
||||
getUrl={[Function]}
|
||||
/>
|
||||
</p>
|
||||
<DownloadButton
|
||||
getUrl={[Function]}
|
||||
job={
|
||||
Object {
|
||||
"id": "yas1",
|
||||
"status": "completed",
|
||||
"title": "Yas",
|
||||
"type": "yas",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</React.Fragment>,
|
||||
"title": <FormattedMessage
|
||||
defaultMessage="Created report for {reportObjectType} '{reportObjectTitle}'"
|
||||
id="xpack.reporting.publicNotifier.successfullyCreatedReportNotificationTitle"
|
||||
values={
|
||||
Object {
|
||||
"reportObjectTitle": "Yas",
|
||||
"reportObjectType": "yas",
|
||||
}
|
||||
}
|
||||
/>,
|
||||
},
|
||||
]
|
||||
`;
|
27
x-pack/plugins/reporting/public/lib/job_queue.ts
Normal file
27
x-pack/plugins/reporting/public/lib/job_queue.ts
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 { HttpService, JobId, JobContent, SourceJob } from '../../index.d';
|
||||
import { API_BASE_URL } from '../../constants';
|
||||
|
||||
export class JobQueue {
|
||||
public findForJobIds = (http: HttpService, jobIds: JobId[]): Promise<SourceJob[]> => {
|
||||
return http.fetch(`${API_BASE_URL}/list`, {
|
||||
query: { page: 0, ids: jobIds.join(',') },
|
||||
method: 'GET',
|
||||
});
|
||||
};
|
||||
|
||||
public getContent(http: HttpService, jobId: JobId): Promise<string> {
|
||||
return http
|
||||
.fetch(`${API_BASE_URL}/output/${jobId}`, {
|
||||
method: 'GET',
|
||||
})
|
||||
.then((data: JobContent) => data.content);
|
||||
}
|
||||
}
|
||||
|
||||
export const jobQueueClient = new JobQueue();
|
264
x-pack/plugins/reporting/public/lib/stream_handler.test.ts
Normal file
264
x-pack/plugins/reporting/public/lib/stream_handler.test.ts
Normal file
|
@ -0,0 +1,264 @@
|
|||
/*
|
||||
* 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 sinon, { stub } from 'sinon';
|
||||
import { HttpServiceBase, NotificationsStart } from '../../../../../src/core/public';
|
||||
import { SourceJob, JobSummary, HttpService } from '../../index.d';
|
||||
import { JobQueue } from './job_queue';
|
||||
import { ReportingNotifierStreamHandler } from './stream_handler';
|
||||
|
||||
Object.defineProperty(window, 'sessionStorage', {
|
||||
value: {
|
||||
setItem: jest.fn(() => null),
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
|
||||
const mockJobsFound = [
|
||||
{
|
||||
_id: 'job-source-mock1',
|
||||
_source: {
|
||||
status: 'completed',
|
||||
output: { max_size_reached: false, csv_contains_formulas: false },
|
||||
payload: { type: 'spectacular', title: 'specimen' },
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: 'job-source-mock2',
|
||||
_source: {
|
||||
status: 'failed',
|
||||
output: { max_size_reached: false, csv_contains_formulas: false },
|
||||
payload: { type: 'spectacular', title: 'specimen' },
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: 'job-source-mock3',
|
||||
_source: {
|
||||
status: 'pending',
|
||||
output: { max_size_reached: false, csv_contains_formulas: false },
|
||||
payload: { type: 'spectacular', title: 'specimen' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const jobQueueClientMock: JobQueue = {
|
||||
findForJobIds: async (http: HttpService, jobIds: string[]) => {
|
||||
return mockJobsFound as SourceJob[];
|
||||
},
|
||||
getContent: () => {
|
||||
return Promise.resolve('this is the completed report data');
|
||||
},
|
||||
};
|
||||
|
||||
const httpMock: HttpService = ({
|
||||
basePath: {
|
||||
prepend: stub(),
|
||||
},
|
||||
} as unknown) as HttpServiceBase;
|
||||
|
||||
const mockShowDanger = stub();
|
||||
const mockShowSuccess = stub();
|
||||
const mockShowWarning = stub();
|
||||
const notificationsMock = ({
|
||||
toasts: {
|
||||
addDanger: mockShowDanger,
|
||||
addSuccess: mockShowSuccess,
|
||||
addWarning: mockShowWarning,
|
||||
},
|
||||
} as unknown) as NotificationsStart;
|
||||
|
||||
describe('stream handler', () => {
|
||||
afterEach(() => {
|
||||
sinon.reset();
|
||||
});
|
||||
|
||||
it('constructs', () => {
|
||||
const sh = new ReportingNotifierStreamHandler(httpMock, notificationsMock, jobQueueClientMock);
|
||||
expect(sh).not.toBe(null);
|
||||
});
|
||||
|
||||
describe('findChangedStatusJobs', () => {
|
||||
it('finds no changed status jobs from empty', done => {
|
||||
const sh = new ReportingNotifierStreamHandler(
|
||||
httpMock,
|
||||
notificationsMock,
|
||||
jobQueueClientMock
|
||||
);
|
||||
const findJobs = sh.findChangedStatusJobs([]);
|
||||
findJobs.subscribe(data => {
|
||||
expect(data).toEqual({ completed: [], failed: [] });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('finds changed status jobs', done => {
|
||||
const sh = new ReportingNotifierStreamHandler(
|
||||
httpMock,
|
||||
notificationsMock,
|
||||
jobQueueClientMock
|
||||
);
|
||||
const findJobs = sh.findChangedStatusJobs([
|
||||
'job-source-mock1',
|
||||
'job-source-mock2',
|
||||
'job-source-mock3',
|
||||
]);
|
||||
|
||||
findJobs.subscribe(data => {
|
||||
expect(data).toMatchSnapshot();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('showNotifications', () => {
|
||||
it('show success', done => {
|
||||
const sh = new ReportingNotifierStreamHandler(
|
||||
httpMock,
|
||||
notificationsMock,
|
||||
jobQueueClientMock
|
||||
);
|
||||
sh.showNotifications({
|
||||
completed: [
|
||||
{
|
||||
id: 'yas1',
|
||||
title: 'Yas',
|
||||
type: 'yas',
|
||||
status: 'completed',
|
||||
} as JobSummary,
|
||||
],
|
||||
failed: [],
|
||||
}).subscribe(() => {
|
||||
expect(mockShowDanger.callCount).toBe(0);
|
||||
expect(mockShowSuccess.callCount).toBe(1);
|
||||
expect(mockShowWarning.callCount).toBe(0);
|
||||
expect(mockShowSuccess.args[0]).toMatchSnapshot();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('show max length warning', done => {
|
||||
const sh = new ReportingNotifierStreamHandler(
|
||||
httpMock,
|
||||
notificationsMock,
|
||||
jobQueueClientMock
|
||||
);
|
||||
sh.showNotifications({
|
||||
completed: [
|
||||
{
|
||||
id: 'yas2',
|
||||
title: 'Yas',
|
||||
type: 'yas',
|
||||
status: 'completed',
|
||||
maxSizeReached: true,
|
||||
} as JobSummary,
|
||||
],
|
||||
failed: [],
|
||||
}).subscribe(() => {
|
||||
expect(mockShowDanger.callCount).toBe(0);
|
||||
expect(mockShowSuccess.callCount).toBe(0);
|
||||
expect(mockShowWarning.callCount).toBe(1);
|
||||
expect(mockShowWarning.args[0]).toMatchSnapshot();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('show csv formulas warning', done => {
|
||||
const sh = new ReportingNotifierStreamHandler(
|
||||
httpMock,
|
||||
notificationsMock,
|
||||
jobQueueClientMock
|
||||
);
|
||||
sh.showNotifications({
|
||||
completed: [
|
||||
{
|
||||
id: 'yas3',
|
||||
title: 'Yas',
|
||||
type: 'yas',
|
||||
status: 'completed',
|
||||
csvContainsFormulas: true,
|
||||
} as JobSummary,
|
||||
],
|
||||
failed: [],
|
||||
}).subscribe(() => {
|
||||
expect(mockShowDanger.callCount).toBe(0);
|
||||
expect(mockShowSuccess.callCount).toBe(0);
|
||||
expect(mockShowWarning.callCount).toBe(1);
|
||||
expect(mockShowWarning.args[0]).toMatchSnapshot();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('show failed job toast', done => {
|
||||
const sh = new ReportingNotifierStreamHandler(
|
||||
httpMock,
|
||||
notificationsMock,
|
||||
jobQueueClientMock
|
||||
);
|
||||
sh.showNotifications({
|
||||
completed: [],
|
||||
failed: [
|
||||
{
|
||||
id: 'yas7',
|
||||
title: 'Yas 7',
|
||||
type: 'yas',
|
||||
status: 'failed',
|
||||
} as JobSummary,
|
||||
],
|
||||
}).subscribe(() => {
|
||||
expect(mockShowSuccess.callCount).toBe(0);
|
||||
expect(mockShowWarning.callCount).toBe(0);
|
||||
expect(mockShowDanger.callCount).toBe(1);
|
||||
expect(mockShowDanger.args[0]).toMatchSnapshot();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('show multiple toast', done => {
|
||||
const sh = new ReportingNotifierStreamHandler(
|
||||
httpMock,
|
||||
notificationsMock,
|
||||
jobQueueClientMock
|
||||
);
|
||||
sh.showNotifications({
|
||||
completed: [
|
||||
{
|
||||
id: 'yas8',
|
||||
title: 'Yas 8',
|
||||
type: 'yas',
|
||||
status: 'completed',
|
||||
} as JobSummary,
|
||||
{
|
||||
id: 'yas9',
|
||||
title: 'Yas 9',
|
||||
type: 'yas',
|
||||
status: 'completed',
|
||||
csvContainsFormulas: true,
|
||||
} as JobSummary,
|
||||
{
|
||||
id: 'yas10',
|
||||
title: 'Yas 10',
|
||||
type: 'yas',
|
||||
status: 'completed',
|
||||
maxSizeReached: true,
|
||||
} as JobSummary,
|
||||
],
|
||||
failed: [
|
||||
{
|
||||
id: 'yas13',
|
||||
title: 'Yas 13',
|
||||
type: 'yas',
|
||||
status: 'failed',
|
||||
} as JobSummary,
|
||||
],
|
||||
}).subscribe(() => {
|
||||
expect(mockShowSuccess.callCount).toBe(1);
|
||||
expect(mockShowWarning.callCount).toBe(2);
|
||||
expect(mockShowDanger.callCount).toBe(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
150
x-pack/plugins/reporting/public/lib/stream_handler.ts
Normal file
150
x-pack/plugins/reporting/public/lib/stream_handler.ts
Normal file
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* 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 * as Rx from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY,
|
||||
JOB_STATUS_COMPLETED,
|
||||
JOB_STATUS_FAILED,
|
||||
API_BASE_URL,
|
||||
REPORTING_MANAGEMENT_HOME,
|
||||
} from '../../constants';
|
||||
import {
|
||||
JobId,
|
||||
JobSummary,
|
||||
JobStatusBuckets,
|
||||
HttpService,
|
||||
NotificationsService,
|
||||
SourceJob,
|
||||
DownloadReportFn,
|
||||
ManagementLinkFn,
|
||||
} from '../../index.d';
|
||||
import {
|
||||
getSuccessToast,
|
||||
getFailureToast,
|
||||
getWarningFormulasToast,
|
||||
getWarningMaxSizeToast,
|
||||
getGeneralErrorToast,
|
||||
} from '../components';
|
||||
import { jobQueueClient as defaultJobQueueClient } from './job_queue';
|
||||
|
||||
function updateStored(jobIds: JobId[]): void {
|
||||
sessionStorage.setItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JSON.stringify(jobIds));
|
||||
}
|
||||
|
||||
function summarizeJob(src: SourceJob): JobSummary {
|
||||
return {
|
||||
id: src._id,
|
||||
status: src._source.status,
|
||||
title: src._source.payload.title,
|
||||
type: src._source.payload.type,
|
||||
maxSizeReached: src._source.output.max_size_reached,
|
||||
csvContainsFormulas: src._source.output.csv_contains_formulas,
|
||||
};
|
||||
}
|
||||
|
||||
export class ReportingNotifierStreamHandler {
|
||||
private getManagementLink: ManagementLinkFn;
|
||||
private getDownloadLink: DownloadReportFn;
|
||||
|
||||
constructor(
|
||||
private http: HttpService,
|
||||
private notifications: NotificationsService,
|
||||
private jobQueueClient = defaultJobQueueClient
|
||||
) {
|
||||
this.getManagementLink = () => {
|
||||
return http.basePath.prepend(REPORTING_MANAGEMENT_HOME);
|
||||
};
|
||||
this.getDownloadLink = (jobId: JobId) => {
|
||||
return http.basePath.prepend(`${API_BASE_URL}/download/${jobId}`);
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Use Kibana Toast API to show our messages
|
||||
*/
|
||||
public showNotifications({
|
||||
completed: completedJobs,
|
||||
failed: failedJobs,
|
||||
}: JobStatusBuckets): Rx.Observable<JobStatusBuckets> {
|
||||
const showNotificationsAsync = async () => {
|
||||
// notifications with download link
|
||||
for (const job of completedJobs) {
|
||||
if (job.csvContainsFormulas) {
|
||||
this.notifications.toasts.addWarning(
|
||||
getWarningFormulasToast(job, this.getManagementLink, this.getDownloadLink)
|
||||
);
|
||||
} else if (job.maxSizeReached) {
|
||||
this.notifications.toasts.addWarning(
|
||||
getWarningMaxSizeToast(job, this.getManagementLink, this.getDownloadLink)
|
||||
);
|
||||
} else {
|
||||
this.notifications.toasts.addSuccess(
|
||||
getSuccessToast(job, this.getManagementLink, this.getDownloadLink)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// no download link available
|
||||
for (const job of failedJobs) {
|
||||
const content = await this.jobQueueClient.getContent(this.http, job.id);
|
||||
this.notifications.toasts.addDanger(getFailureToast(content, job, this.getManagementLink));
|
||||
}
|
||||
return { completed: completedJobs, failed: failedJobs };
|
||||
};
|
||||
|
||||
return Rx.from(showNotificationsAsync()); // convert Promise to Observable, for the convenience of the main stream
|
||||
}
|
||||
|
||||
/*
|
||||
* An observable that finds jobs that are known to be "processing" (stored in
|
||||
* session storage) but have non-processing job status on the server
|
||||
*/
|
||||
public findChangedStatusJobs(storedJobs: JobId[]): Rx.Observable<JobStatusBuckets> {
|
||||
return Rx.from(this.jobQueueClient.findForJobIds(this.http, storedJobs)).pipe(
|
||||
map((jobs: SourceJob[]) => {
|
||||
const completedJobs: JobSummary[] = [];
|
||||
const failedJobs: JobSummary[] = [];
|
||||
const pending: JobId[] = [];
|
||||
|
||||
// add side effects to storage
|
||||
for (const job of jobs) {
|
||||
const {
|
||||
_id: jobId,
|
||||
_source: { status: jobStatus },
|
||||
} = job;
|
||||
if (storedJobs.includes(jobId)) {
|
||||
if (jobStatus === JOB_STATUS_COMPLETED) {
|
||||
completedJobs.push(summarizeJob(job));
|
||||
} else if (jobStatus === JOB_STATUS_FAILED) {
|
||||
failedJobs.push(summarizeJob(job));
|
||||
} else {
|
||||
pending.push(jobId);
|
||||
}
|
||||
}
|
||||
}
|
||||
updateStored(pending); // refresh the storage of pending job IDs, minus completed and failed job IDs
|
||||
|
||||
return { completed: completedJobs, failed: failedJobs };
|
||||
}),
|
||||
catchError(err => {
|
||||
// show connection refused toast
|
||||
this.notifications.toasts.addDanger(
|
||||
getGeneralErrorToast(
|
||||
i18n.translate('xpack.reporting.publicNotifier.httpErrorMessage', {
|
||||
defaultMessage: 'Could not check Reporting job status!',
|
||||
}),
|
||||
err
|
||||
)
|
||||
); // prettier-ignore
|
||||
window.console.error(err);
|
||||
return Rx.of({ completed: [], failed: [] }); // log the error and resume
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
82
x-pack/plugins/reporting/public/plugin.tsx
Normal file
82
x-pack/plugins/reporting/public/plugin.tsx
Normal file
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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 * as Rx from 'rxjs';
|
||||
import { catchError, filter, map, mergeMap, takeUntil } from 'rxjs/operators';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
CoreSetup,
|
||||
CoreStart,
|
||||
Plugin,
|
||||
PluginInitializerContext,
|
||||
} from '../../../../src/core/public';
|
||||
import {
|
||||
JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG,
|
||||
JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY,
|
||||
} from '../constants';
|
||||
import { JobId, JobStatusBuckets, NotificationsService } from '../index.d';
|
||||
import { getGeneralErrorToast } from './components';
|
||||
import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_handler';
|
||||
|
||||
const {
|
||||
jobCompletionNotifier: { interval: JOBS_REFRESH_INTERVAL },
|
||||
} = JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG;
|
||||
|
||||
function getStored(): JobId[] {
|
||||
const sessionValue = sessionStorage.getItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY);
|
||||
return sessionValue ? JSON.parse(sessionValue) : [];
|
||||
}
|
||||
|
||||
function handleError(
|
||||
notifications: NotificationsService,
|
||||
err: Error
|
||||
): Rx.Observable<JobStatusBuckets> {
|
||||
notifications.toasts.addDanger(
|
||||
getGeneralErrorToast(
|
||||
i18n.translate('xpack.reporting.publicNotifier.pollingErrorMessage', {
|
||||
defaultMessage: 'Reporting notifier error!',
|
||||
}),
|
||||
err
|
||||
)
|
||||
);
|
||||
window.console.error(err);
|
||||
return Rx.of({ completed: [], failed: [] });
|
||||
}
|
||||
|
||||
export class ReportingPublicPlugin implements Plugin<any, any> {
|
||||
private readonly stop$ = new Rx.ReplaySubject(1);
|
||||
|
||||
// FIXME: License checking: only active, non-expired licenses allowed
|
||||
// Depends on https://github.com/elastic/kibana/pull/44922
|
||||
constructor(initializerContext: PluginInitializerContext) {}
|
||||
|
||||
public setup(core: CoreSetup) {}
|
||||
|
||||
// FIXME: only perform these actions for authenticated routes
|
||||
// Depends on https://github.com/elastic/kibana/pull/39477
|
||||
public start(core: CoreStart) {
|
||||
const { http, notifications } = core;
|
||||
const streamHandler = new StreamHandler(http, notifications);
|
||||
|
||||
Rx.timer(0, JOBS_REFRESH_INTERVAL)
|
||||
.pipe(
|
||||
takeUntil(this.stop$), // stop the interval when stop method is called
|
||||
map(() => getStored()), // read all pending job IDs from session storage
|
||||
filter(storedJobs => storedJobs.length > 0), // stop the pipeline here if there are none pending
|
||||
mergeMap(storedJobs => streamHandler.findChangedStatusJobs(storedJobs)), // look up the latest status of all pending jobs on the server
|
||||
mergeMap(({ completed, failed }) => streamHandler.showNotifications({ completed, failed })),
|
||||
catchError(err => handleError(notifications, err))
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.stop$.next();
|
||||
}
|
||||
}
|
||||
|
||||
export type Setup = ReturnType<ReportingPublicPlugin['setup']>;
|
||||
export type Start = ReturnType<ReportingPublicPlugin['start']>;
|
|
@ -8251,13 +8251,6 @@
|
|||
"xpack.reporting.exportTypes.printablePdf.logoDescription": "Elastic 提供",
|
||||
"xpack.reporting.exportTypes.printablePdf.pagingDescription": "{pageCount} ページ中 {currentPage} ページ目",
|
||||
"xpack.reporting.exportTypes.printablePdf.screenshots.unexpectedErrorMessage": "ページで予期せぬメッセージが発生しました: {toastHeaderText}",
|
||||
"xpack.reporting.jobCompletionNotifier.couldNotCreateReportTitle": "{reportObjectType}「{reportObjectTitle}」のレポートを作成できませんでした",
|
||||
"xpack.reporting.jobCompletionNotifier.downloadReportButtonLabel": "レポートをダウンロード",
|
||||
"xpack.reporting.jobCompletionNotifier.maxSizeReached.partialReportDescription": "レポートが最大サイズに達し、部分データが含まれています。",
|
||||
"xpack.reporting.jobCompletionNotifier.maxSizeReached.partialReportTitle": "{reportObjectType}「{reportObjectTitle}」の部分レポートが作成されました",
|
||||
"xpack.reporting.jobCompletionNotifier.reportLink.pickItUpFromPathDescription": "{path} から開始します。",
|
||||
"xpack.reporting.jobCompletionNotifier.reportLink.reportingSectionUrlLinkLabel": "管理 > Kibana > レポート",
|
||||
"xpack.reporting.jobCompletionNotifier.successfullyCreatedReportNotificationTitle": "{reportObjectType}「{reportObjectTitle}」のレポートが作成されました",
|
||||
"xpack.reporting.jobStatuses.cancelledText": "キャンセル済み",
|
||||
"xpack.reporting.jobStatuses.completedText": "完了",
|
||||
"xpack.reporting.jobStatuses.failedText": "失敗",
|
||||
|
|
|
@ -8408,13 +8408,6 @@
|
|||
"xpack.reporting.exportTypes.printablePdf.logoDescription": "由 Elastic 提供支持",
|
||||
"xpack.reporting.exportTypes.printablePdf.pagingDescription": "第 {currentPage} 页,共 {pageCount} 页",
|
||||
"xpack.reporting.exportTypes.printablePdf.screenshots.unexpectedErrorMessage": "在页面上出现意外消息:{toastHeaderText}",
|
||||
"xpack.reporting.jobCompletionNotifier.couldNotCreateReportTitle": "无法为 {reportObjectType} “{reportObjectTitle}” 创建报告",
|
||||
"xpack.reporting.jobCompletionNotifier.downloadReportButtonLabel": "下载报告",
|
||||
"xpack.reporting.jobCompletionNotifier.maxSizeReached.partialReportDescription": "报告已达到最大大小,仅包含部分数据。",
|
||||
"xpack.reporting.jobCompletionNotifier.maxSizeReached.partialReportTitle": "已为 {reportObjectType} “{reportObjectTitle}” 创建部分报告",
|
||||
"xpack.reporting.jobCompletionNotifier.reportLink.pickItUpFromPathDescription": "从 {path} 收集。",
|
||||
"xpack.reporting.jobCompletionNotifier.reportLink.reportingSectionUrlLinkLabel": "管理 > Kibana > Reporting",
|
||||
"xpack.reporting.jobCompletionNotifier.successfullyCreatedReportNotificationTitle": "已为 {reportObjectType} “{reportObjectTitle}” 创建报告",
|
||||
"xpack.reporting.jobStatuses.cancelledText": "已取消",
|
||||
"xpack.reporting.jobStatuses.completedText": "已完成",
|
||||
"xpack.reporting.jobStatuses.failedText": "失败",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue