Migrate report listing management to react and EUI (#22928)
* EUIify report management page * wire ReportListing component together * fetch jobs and display content in EuiPage * display jobs in table * add title and remove page size dropdown * format date and display date in status column * add poller * add download button * report error button * remove old reporting table * fix page styling * create type for job * remove job queue service * remove angular-paging dependency from x-pack * make download lib, update job notification hack to use jobQueueClient * fix some more typescript stuff * remove last angular service * make report object type subdued color and small text * update import in canvas * stricter typing * fix stuff lost in branch merge * add return types to JobQueueClient * wrap javascript code in {} in JSX
This commit is contained in:
parent
0ff498d5c4
commit
95f48c584e
1
src/ui/public/chrome/index.d.ts
vendored
1
src/ui/public/chrome/index.d.ts
vendored
|
@ -28,6 +28,7 @@ declare class Chrome {
|
|||
public getXsrfToken(): string;
|
||||
public getKibanaVersion(): string;
|
||||
public getUiSettingsClient(): any;
|
||||
public getInjected(key: string, defaultValue?: any): any;
|
||||
}
|
||||
|
||||
declare const chrome: Chrome;
|
||||
|
|
|
@ -4,10 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-var */
|
||||
require('jquery');
|
||||
require('angular');
|
||||
require('angular-paging/dist/paging');
|
||||
export declare class Poller {
|
||||
constructor(options: any);
|
||||
|
||||
var uiModules = require('ui/modules').uiModules;
|
||||
uiModules.get('kibana', ['bw.paging']);
|
||||
public start(): void;
|
||||
}
|
|
@ -98,7 +98,6 @@
|
|||
"@scant/router": "^0.1.0",
|
||||
"@slack/client": "^4.2.2",
|
||||
"@types/moment-timezone": "^0.5.8",
|
||||
"angular-paging": "2.2.1",
|
||||
"angular-resource": "1.4.9",
|
||||
"angular-sanitize": "1.4.9",
|
||||
"angular-ui-ace": "0.2.3",
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
/* eslint import/no-unresolved: 1 */
|
||||
// TODO: remove eslint rule when updating to use the linked kibana resolve package
|
||||
import { jobCompletionNotifications } from 'plugins/reporting/services/job_completion_notifications';
|
||||
import { jobCompletionNotifications } from 'plugins/reporting/lib/job_completion_notifications';
|
||||
import { connect } from 'react-redux';
|
||||
import { compose, withProps } from 'recompose';
|
||||
import { getWorkpad, getPages } from '../../state/selectors/workpad';
|
||||
|
|
|
@ -6,11 +6,12 @@
|
|||
|
||||
export const QUEUE_DOCTYPE = 'esqueue';
|
||||
|
||||
export const JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY = 'xpack.reporting.jobCompletionNotifications';
|
||||
export const JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY =
|
||||
'xpack.reporting.jobCompletionNotifications';
|
||||
|
||||
export const API_BASE_URL = '/api/reporting';
|
||||
|
||||
export const WHITELISTED_JOB_CONTENT_TYPES = [ 'application/json', 'application/pdf', 'text/csv' ];
|
||||
export const WHITELISTED_JOB_CONTENT_TYPES = ['application/json', 'application/pdf', 'text/csv'];
|
||||
|
||||
export const UI_SETTINGS_CUSTOM_PDF_LOGO = 'xpackReporting:customPdfLogo';
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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 { EuiButtonIcon, EuiCallOut, EuiPopover } from '@elastic/eui';
|
||||
import React, { Component } from 'react';
|
||||
import { JobContent, jobQueueClient } from '../lib/job_queue_client';
|
||||
|
||||
interface Props {
|
||||
jobId: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isLoading: boolean;
|
||||
isPopoverOpen: boolean;
|
||||
calloutTitle: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export class ReportErrorButton extends Component<Props, State> {
|
||||
private mounted?: boolean;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isLoading: false,
|
||||
isPopoverOpen: false,
|
||||
calloutTitle: 'Unable to generate report',
|
||||
};
|
||||
}
|
||||
|
||||
public render() {
|
||||
const button = (
|
||||
<EuiButtonIcon
|
||||
onClick={this.togglePopover}
|
||||
iconType="alert"
|
||||
color={'danger'}
|
||||
aria-label="Show report error"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id="popover"
|
||||
button={button}
|
||||
isOpen={this.state.isPopoverOpen}
|
||||
closePopover={this.closePopover}
|
||||
anchorPosition="downRight"
|
||||
>
|
||||
<EuiCallOut color="danger" title={this.state.calloutTitle}>
|
||||
<p>{this.state.error}</p>
|
||||
</EuiCallOut>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.mounted = true;
|
||||
}
|
||||
|
||||
private togglePopover = () => {
|
||||
this.setState(prevState => {
|
||||
return { isPopoverOpen: !prevState.isPopoverOpen };
|
||||
});
|
||||
|
||||
if (!this.state.error) {
|
||||
this.loadError();
|
||||
}
|
||||
};
|
||||
|
||||
private closePopover = () => {
|
||||
this.setState({ isPopoverOpen: false });
|
||||
};
|
||||
|
||||
private loadError = async () => {
|
||||
this.setState({ isLoading: true });
|
||||
try {
|
||||
const reportContent: JobContent = await jobQueueClient.getContent(this.props.jobId);
|
||||
if (this.mounted) {
|
||||
this.setState({ isLoading: false, error: reportContent.content });
|
||||
}
|
||||
} catch (kfetchError) {
|
||||
if (this.mounted) {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
calloutTitle: 'Unable to fetch report content',
|
||||
error: kfetchError.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
311
x-pack/plugins/reporting/public/components/report_listing.tsx
Normal file
311
x-pack/plugins/reporting/public/components/report_listing.tsx
Normal file
|
@ -0,0 +1,311 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// TODO: Remove once typescript definitions are in EUI
|
||||
declare module '@elastic/eui' {
|
||||
export const EuiBasicTable: React.SFC<any>;
|
||||
export const EuiTextColor: React.SFC<any>;
|
||||
}
|
||||
|
||||
import moment from 'moment';
|
||||
import React, { Component } from 'react';
|
||||
import chrome from 'ui/chrome';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { Poller } from '../../../../common/poller';
|
||||
import { downloadReport } from '../lib/download_report';
|
||||
import { jobQueueClient, JobQueueEntry } from '../lib/job_queue_client';
|
||||
import { ReportErrorButton } from './report_error_button';
|
||||
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiButtonIcon,
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
EuiTitle,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
|
||||
interface Job {
|
||||
id: string;
|
||||
type: string;
|
||||
object_type: string;
|
||||
object_title: string;
|
||||
created_by?: string;
|
||||
created_at: string;
|
||||
started_at?: string;
|
||||
completed_at?: string;
|
||||
status: string;
|
||||
max_size_reached: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
badLicenseMessage: string;
|
||||
showLinks: boolean;
|
||||
enableLinks: boolean;
|
||||
redirect: (url: string) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
page: number;
|
||||
total: number;
|
||||
jobs: Job[];
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export class ReportListing extends Component<Props, State> {
|
||||
private mounted?: boolean;
|
||||
private poller?: any;
|
||||
private isInitialJobsFetch: boolean;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
page: 0,
|
||||
total: 0,
|
||||
jobs: [],
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
this.isInitialJobsFetch = true;
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<EuiPage className="repReportListing__page">
|
||||
<EuiPageBody restrictWidth>
|
||||
<EuiPageContent horizontalPosition="center">
|
||||
<EuiTitle>
|
||||
<h1>Reports</h1>
|
||||
</EuiTitle>
|
||||
{this.renderTable()}
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
this.poller.stop();
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.mounted = true;
|
||||
const { jobsRefresh } = chrome.getInjected('reportingPollConfig');
|
||||
this.poller = new Poller({
|
||||
functionToPoll: () => {
|
||||
return this.fetchJobs();
|
||||
},
|
||||
pollFrequencyInMillis: jobsRefresh.interval,
|
||||
trailing: false,
|
||||
continuePollingOnError: true,
|
||||
pollFrequencyErrorMultiplier: jobsRefresh.intervalErrorMultiplier,
|
||||
});
|
||||
this.poller.start();
|
||||
}
|
||||
|
||||
private renderTable() {
|
||||
const tableColumns = [
|
||||
{
|
||||
field: 'object_title',
|
||||
name: 'Report',
|
||||
render: (objectTitle: string, record: Job) => {
|
||||
return (
|
||||
<div>
|
||||
<div>{objectTitle}</div>
|
||||
<EuiText size="s">
|
||||
<EuiTextColor color="subdued">{record.object_type}</EuiTextColor>
|
||||
</EuiText>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'created_at',
|
||||
name: 'Created at',
|
||||
render: (createdAt: string, record: Job) => {
|
||||
if (record.created_by) {
|
||||
return (
|
||||
<div>
|
||||
<div>{this.formatDate(createdAt)}</div>
|
||||
<span>{record.created_by}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return this.formatDate(createdAt);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
name: 'Status',
|
||||
render: (status: string, record: Job) => {
|
||||
let maxSizeReached;
|
||||
if (record.max_size_reached) {
|
||||
maxSizeReached = <span> - max size reached</span>;
|
||||
}
|
||||
let statusTimestamp;
|
||||
if (status === 'processing' && record.started_at) {
|
||||
statusTimestamp = this.formatDate(record.started_at);
|
||||
} else if (record.completed_at && (status === 'completed' || status === 'failed')) {
|
||||
statusTimestamp = this.formatDate(record.completed_at);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{status}
|
||||
{' at '}
|
||||
<span className="eui-textNoWrap">{statusTimestamp}</span>
|
||||
{maxSizeReached}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
actions: [
|
||||
{
|
||||
render: (record: Job) => {
|
||||
return (
|
||||
<div>
|
||||
{this.renderDownloadButton(record)}
|
||||
{this.renderReportErrorButton(record)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const pagination = {
|
||||
pageIndex: this.state.page,
|
||||
pageSize: 10,
|
||||
totalItemCount: this.state.total,
|
||||
hidePerPageOptions: true,
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiBasicTable
|
||||
itemId={'id'}
|
||||
items={this.state.jobs}
|
||||
loading={this.state.isLoading}
|
||||
columns={tableColumns}
|
||||
noItemsMessage={this.state.isLoading ? 'Loading reports' : 'No reports have been created'}
|
||||
pagination={pagination}
|
||||
onChange={this.onTableChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private renderDownloadButton = (record: Job) => {
|
||||
if (record.status !== 'completed') {
|
||||
return;
|
||||
}
|
||||
|
||||
const button = (
|
||||
<EuiButtonIcon
|
||||
onClick={() => downloadReport(record.id)}
|
||||
iconType="importAction"
|
||||
aria-label="Download report"
|
||||
/>
|
||||
);
|
||||
|
||||
if (record.max_size_reached) {
|
||||
return (
|
||||
<EuiToolTip position="top" content="Max size reached, contains partial data.">
|
||||
{button}
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
|
||||
return button;
|
||||
};
|
||||
|
||||
private renderReportErrorButton = (record: Job) => {
|
||||
if (record.status !== 'failed') {
|
||||
return;
|
||||
}
|
||||
|
||||
return <ReportErrorButton jobId={record.id} />;
|
||||
};
|
||||
|
||||
private onTableChange = ({ page }: { page: { index: number } }) => {
|
||||
const { index: pageIndex } = page;
|
||||
|
||||
this.setState(
|
||||
{
|
||||
page: pageIndex,
|
||||
},
|
||||
this.fetchJobs
|
||||
);
|
||||
};
|
||||
|
||||
private fetchJobs = async () => {
|
||||
// avoid page flicker when poller is updating table - only display loading screen on first load
|
||||
if (this.isInitialJobsFetch) {
|
||||
this.setState({ isLoading: true });
|
||||
}
|
||||
|
||||
let jobs: JobQueueEntry[];
|
||||
let total: number;
|
||||
try {
|
||||
jobs = await jobQueueClient.list(this.state.page);
|
||||
total = await jobQueueClient.total();
|
||||
this.isInitialJobsFetch = false;
|
||||
} catch (kfetchError) {
|
||||
if (!this.licenseAllowsToShowThisPage()) {
|
||||
toastNotifications.addDanger(this.props.badLicenseMessage);
|
||||
this.props.redirect('/management');
|
||||
return;
|
||||
}
|
||||
|
||||
if (kfetchError.res.status !== 401 && kfetchError.res.status !== 403) {
|
||||
toastNotifications.addDanger(kfetchError.res.statusText || 'Request failed');
|
||||
}
|
||||
if (this.mounted) {
|
||||
this.setState({ isLoading: false, jobs: [], total: 0 });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.mounted) {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
total,
|
||||
jobs: jobs.map((job: JobQueueEntry) => {
|
||||
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,
|
||||
max_size_reached: job._source.output ? job._source.output.max_size_reached : false,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private licenseAllowsToShowThisPage = () => {
|
||||
return this.props.showLinks && this.props.enableLinks;
|
||||
};
|
||||
|
||||
private formatDate(timestamp: string) {
|
||||
try {
|
||||
return moment(timestamp).format('YYYY-MM-DD @ hh:mm A');
|
||||
} catch (error) {
|
||||
// ignore parse error and display unformatted value
|
||||
return timestamp;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,25 +8,22 @@ 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 { jobQueueClient } from 'plugins/reporting/lib/job_queue_client';
|
||||
import { jobCompletionNotifications } from 'plugins/reporting/lib/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';
|
||||
import { downloadReport } from '../lib/download_report';
|
||||
|
||||
/**
|
||||
* Poll for changes to reports. Inform the user of changes when the license is active.
|
||||
*/
|
||||
uiModules.get('kibana')
|
||||
.run(($http, reportingJobQueue, Private, reportingPollConfig, reportingJobCompletionNotifications) => {
|
||||
.run((Private, reportingPollConfig) => {
|
||||
// Don't show users any reporting toasts until they're logged in.
|
||||
if (Private(PathProvider).isLoginOrLogout()) {
|
||||
return;
|
||||
|
@ -44,7 +41,7 @@ uiModules.get('kibana')
|
|||
const isJobSuccessful = get(job, '_source.status') === 'completed';
|
||||
|
||||
if (!isJobSuccessful) {
|
||||
const errorDoc = await reportingJobQueue.getContent(job._id);
|
||||
const errorDoc = await jobQueueClient.getContent(job._id);
|
||||
const text = errorDoc.content;
|
||||
return toastNotifications.addDanger({
|
||||
title: `Couldn't create report for ${reportObjectType} '${reportObjectTitle}'`,
|
||||
|
@ -112,22 +109,22 @@ uiModules.get('kibana')
|
|||
return;
|
||||
}
|
||||
|
||||
const jobIds = reportingJobCompletionNotifications.getAll();
|
||||
const jobIds = jobCompletionNotifications.getAll();
|
||||
if (!jobIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const jobs = await getJobs($http, jobIds);
|
||||
const jobs = await jobQueueClient.list(0, jobIds);
|
||||
jobIds.forEach(async jobId => {
|
||||
const job = jobs.find(j => j._id === jobId);
|
||||
if (!job) {
|
||||
reportingJobCompletionNotifications.remove(jobId);
|
||||
jobCompletionNotifications.remove(jobId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (job._source.status === 'completed' || job._source.status === 'failed') {
|
||||
await showCompletionNotification(job);
|
||||
reportingJobCompletionNotifications.remove(job.id);
|
||||
jobCompletionNotifications.remove(job.id);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
@ -139,21 +136,3 @@ uiModules.get('kibana')
|
|||
});
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,22 +1,3 @@
|
|||
@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;
|
||||
}
|
||||
.repReportListing__page {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
|
14
x-pack/plugins/reporting/public/lib/download_report.ts
Normal file
14
x-pack/plugins/reporting/public/lib/download_report.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 chrome from 'ui/chrome';
|
||||
import { API_BASE_URL } from '../../common/constants';
|
||||
|
||||
export function downloadReport(jobId: string) {
|
||||
const apiBaseUrl = chrome.addBasePath(API_BASE_URL);
|
||||
const downloadLink = `${apiBaseUrl}/jobs/download/${jobId}`;
|
||||
window.open(downloadLink);
|
||||
}
|
|
@ -4,7 +4,6 @@
|
|||
* 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';
|
||||
|
||||
export const jobCompletionNotifications = {
|
||||
|
@ -39,7 +38,3 @@ export const jobCompletionNotifications = {
|
|||
);
|
||||
},
|
||||
};
|
||||
|
||||
uiModules
|
||||
.get('xpack/reporting')
|
||||
.factory('reportingJobCompletionNotifications', () => jobCompletionNotifications);
|
55
x-pack/plugins/reporting/public/lib/job_queue_client.ts
Normal file
55
x-pack/plugins/reporting/public/lib/job_queue_client.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 { kfetch } from 'ui/kfetch';
|
||||
// @ts-ignore
|
||||
import { addSystemApiHeader } from 'ui/system_api';
|
||||
|
||||
const API_BASE_URL = '/api/reporting/jobs';
|
||||
|
||||
export interface JobQueueEntry {
|
||||
_id: string;
|
||||
_source: any;
|
||||
}
|
||||
|
||||
export interface JobContent {
|
||||
content: string;
|
||||
content_type: boolean;
|
||||
}
|
||||
|
||||
class JobQueueClient {
|
||||
public list = (page = 0, jobIds?: string[]): Promise<JobQueueEntry[]> => {
|
||||
const query = { page } as any;
|
||||
if (jobIds && jobIds.length > 0) {
|
||||
// Only getting the first 10, to prevent URL overflows
|
||||
query.ids = jobIds.slice(0, 10).join(',');
|
||||
}
|
||||
return kfetch({
|
||||
method: 'GET',
|
||||
pathname: `${API_BASE_URL}/list`,
|
||||
query,
|
||||
headers: addSystemApiHeader({}),
|
||||
});
|
||||
};
|
||||
|
||||
public total(): Promise<number> {
|
||||
return kfetch({
|
||||
method: 'GET',
|
||||
pathname: `${API_BASE_URL}/count`,
|
||||
headers: addSystemApiHeader({}),
|
||||
});
|
||||
}
|
||||
|
||||
public getContent(jobId: string): Promise<JobContent> {
|
||||
return kfetch({
|
||||
method: 'GET',
|
||||
pathname: `${API_BASE_URL}/output/${jobId}`,
|
||||
headers: addSystemApiHeader({}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const jobQueueClient = new JobQueueClient();
|
|
@ -10,7 +10,7 @@ import { kfetch } from 'ui/kfetch';
|
|||
import rison from 'rison-node';
|
||||
import chrome from 'ui/chrome';
|
||||
import { QueryString } from 'ui/utils/query_string';
|
||||
import { jobCompletionNotifications } from '../services/job_completion_notifications';
|
||||
import { jobCompletionNotifications } from './job_completion_notifications';
|
||||
|
||||
const API_BASE_URL = '/api/reporting/generate';
|
||||
|
||||
|
|
|
@ -1,42 +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 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);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,92 +1,3 @@
|
|||
<kbn-management-app section="kibana">
|
||||
<div class="euiPage">
|
||||
<div class="euiPageBody">
|
||||
<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 === 'processing'"
|
||||
>
|
||||
{{ job.started_at | date : 'yyyy-MM-dd @ h:mm a' }}
|
||||
</div>
|
||||
<div
|
||||
class="metadata"
|
||||
ng-if="job.status === 'completed' || job.status === 'failed'"
|
||||
>
|
||||
{{ job.completed_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>
|
||||
</div>
|
||||
<div id="reportListingAnchor"></div>
|
||||
</kbn-management-app>
|
||||
|
|
|
@ -4,140 +4,46 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import 'angular-paging';
|
||||
import 'plugins/reporting/services/job_queue';
|
||||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import 'plugins/reporting/less/main.less';
|
||||
import { toastNotifications } 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;
|
||||
import { ReportListing } from '../../components/report_listing';
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
}
|
||||
const REACT_ANCHOR_DOM_ELEMENT_ID = 'reportListingAnchor';
|
||||
|
||||
routes.when('/management/kibana/reporting', {
|
||||
template,
|
||||
controllerAs: 'jobsCtrl',
|
||||
controller($scope, $route, $window, $interval, reportingJobQueue, kbnUrl, Private, reportingPollConfig) {
|
||||
const { jobsRefresh } = reportingPollConfig;
|
||||
controller($scope, kbnUrl, Private) {
|
||||
const xpackInfo = Private(XPackInfoProvider);
|
||||
|
||||
this.loading = false;
|
||||
this.pageSize = pageSize;
|
||||
this.currentPage = 1;
|
||||
this.reportingJobs = [];
|
||||
$scope.$$postDigest(() => {
|
||||
const node = document.getElementById(REACT_ANCHOR_DOM_ELEMENT_ID);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const licenseAllowsToShowThisPage = () => {
|
||||
return xpackInfo.get('features.reporting.management.showLinks')
|
||||
&& xpackInfo.get('features.reporting.management.enableLinks');
|
||||
};
|
||||
|
||||
const notifyAndRedirectToManagementOverviewPage = () => {
|
||||
toastNotifications.addDanger(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) {
|
||||
toastNotifications.addDanger(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
|
||||
render(
|
||||
<ReportListing
|
||||
badLicenseMessage={xpackInfo.get('features.reporting.management.message')}
|
||||
showLinks={xpackInfo.get('features.reporting.management.showLinks')}
|
||||
enableLinks={xpackInfo.get('features.reporting.management.enableLinks')}
|
||||
redirect={kbnUrl.redirect}
|
||||
/>,
|
||||
node,
|
||||
);
|
||||
});
|
||||
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());
|
||||
$scope.$on('$destroy', () => {
|
||||
const node = document.getElementById(REACT_ANCHOR_DOM_ELEMENT_ID);
|
||||
if (node) {
|
||||
unmountComponentAtNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -345,10 +345,6 @@ ammo@2.x.x:
|
|||
boom "5.x.x"
|
||||
hoek "4.x.x"
|
||||
|
||||
angular-paging@2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/angular-paging/-/angular-paging-2.2.1.tgz#8090864f71bc4c9b89307b02ab02afb205983c43"
|
||||
|
||||
angular-resource@1.4.9:
|
||||
version "1.4.9"
|
||||
resolved "https://registry.yarnpkg.com/angular-resource/-/angular-resource-1.4.9.tgz#67f09382b623fd7e61540b0d127dba99fda99d45"
|
||||
|
|
|
@ -853,10 +853,6 @@ angular-mocks@1.4.7:
|
|||
version "1.4.7"
|
||||
resolved "https://registry.yarnpkg.com/angular-mocks/-/angular-mocks-1.4.7.tgz#d7343ee0a033f9216770bda573950f6814d95227"
|
||||
|
||||
angular-paging@2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/angular-paging/-/angular-paging-2.2.1.tgz#8090864f71bc4c9b89307b02ab02afb205983c43"
|
||||
|
||||
angular-recursion@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/angular-recursion/-/angular-recursion-1.0.5.tgz#cd405428a0bf55faf52eaa7988c1fe69cd930543"
|
||||
|
|
Loading…
Reference in a new issue