From 95f48c584eb881e20afc95c5f5b8ee2d712cc59a Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 27 Sep 2018 14:49:41 -0600 Subject: [PATCH] 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 --- src/ui/public/chrome/index.d.ts | 1 + .../angular-paging.js => common/poller.d.ts} | 10 +- x-pack/package.json | 1 - .../public/components/workpad_export/index.js | 2 +- .../common/{constants.js => constants.ts} | 5 +- .../public/components/report_error_button.tsx | 99 ++++++ .../public/components/report_listing.tsx | 311 ++++++++++++++++++ .../public/hacks/job_completion_notifier.js | 39 +-- .../plugins/reporting/public/less/main.less | 23 +- .../reporting/public/lib/download_report.ts | 14 + .../job_completion_notifications.d.ts | 0 .../job_completion_notifications.js | 5 - .../reporting/public/lib/job_queue_client.ts | 55 ++++ .../reporting/public/lib/reporting_client.ts | 2 +- .../reporting/public/services/job_queue.js | 42 --- .../public/views/management/jobs.html | 91 +---- .../reporting/public/views/management/jobs.js | 144 ++------ x-pack/yarn.lock | 4 - yarn.lock | 4 - 19 files changed, 526 insertions(+), 326 deletions(-) rename x-pack/{plugins/reporting/webpackShims/angular-paging.js => common/poller.d.ts} (55%) rename x-pack/plugins/reporting/common/{constants.js => constants.ts} (73%) create mode 100644 x-pack/plugins/reporting/public/components/report_error_button.tsx create mode 100644 x-pack/plugins/reporting/public/components/report_listing.tsx create mode 100644 x-pack/plugins/reporting/public/lib/download_report.ts rename x-pack/plugins/reporting/public/{services => lib}/job_completion_notifications.d.ts (100%) rename x-pack/plugins/reporting/public/{services => lib}/job_completion_notifications.js (86%) create mode 100644 x-pack/plugins/reporting/public/lib/job_queue_client.ts delete mode 100644 x-pack/plugins/reporting/public/services/job_queue.js diff --git a/src/ui/public/chrome/index.d.ts b/src/ui/public/chrome/index.d.ts index 6b7835a26f90..5e4c0c2490af 100644 --- a/src/ui/public/chrome/index.d.ts +++ b/src/ui/public/chrome/index.d.ts @@ -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; diff --git a/x-pack/plugins/reporting/webpackShims/angular-paging.js b/x-pack/common/poller.d.ts similarity index 55% rename from x-pack/plugins/reporting/webpackShims/angular-paging.js rename to x-pack/common/poller.d.ts index ed66972a53a5..c23d18dd62e8 100644 --- a/x-pack/plugins/reporting/webpackShims/angular-paging.js +++ b/x-pack/common/poller.d.ts @@ -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; +} diff --git a/x-pack/package.json b/x-pack/package.json index f501d9b5d2b5..135b7542e704 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -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", diff --git a/x-pack/plugins/canvas/public/components/workpad_export/index.js b/x-pack/plugins/canvas/public/components/workpad_export/index.js index 1ef7438f520d..3e1a1dce7055 100644 --- a/x-pack/plugins/canvas/public/components/workpad_export/index.js +++ b/x-pack/plugins/canvas/public/components/workpad_export/index.js @@ -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'; diff --git a/x-pack/plugins/reporting/common/constants.js b/x-pack/plugins/reporting/common/constants.ts similarity index 73% rename from x-pack/plugins/reporting/common/constants.js rename to x-pack/plugins/reporting/common/constants.ts index 09321bd8d3c3..bdac6ca6d132 100644 --- a/x-pack/plugins/reporting/common/constants.js +++ b/x-pack/plugins/reporting/common/constants.ts @@ -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'; diff --git a/x-pack/plugins/reporting/public/components/report_error_button.tsx b/x-pack/plugins/reporting/public/components/report_error_button.tsx new file mode 100644 index 000000000000..fa82472acd27 --- /dev/null +++ b/x-pack/plugins/reporting/public/components/report_error_button.tsx @@ -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 { + private mounted?: boolean; + + constructor(props: Props) { + super(props); + + this.state = { + isLoading: false, + isPopoverOpen: false, + calloutTitle: 'Unable to generate report', + }; + } + + public render() { + const button = ( + + ); + + return ( + + +

{this.state.error}

+
+
+ ); + } + + 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, + }); + } + } + }; +} diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx new file mode 100644 index 000000000000..46a759f530fe --- /dev/null +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -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; + export const EuiTextColor: React.SFC; +} + +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 { + 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 ( + + + + +

Reports

+
+ {this.renderTable()} +
+
+
+ ); + } + + 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 ( +
+
{objectTitle}
+ + {record.object_type} + +
+ ); + }, + }, + { + field: 'created_at', + name: 'Created at', + render: (createdAt: string, record: Job) => { + if (record.created_by) { + return ( +
+
{this.formatDate(createdAt)}
+ {record.created_by} +
+ ); + } + return this.formatDate(createdAt); + }, + }, + { + field: 'status', + name: 'Status', + render: (status: string, record: Job) => { + let maxSizeReached; + if (record.max_size_reached) { + maxSizeReached = - max size reached; + } + 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 ( +
+ {status} + {' at '} + {statusTimestamp} + {maxSizeReached} +
+ ); + }, + }, + { + name: 'Actions', + actions: [ + { + render: (record: Job) => { + return ( +
+ {this.renderDownloadButton(record)} + {this.renderReportErrorButton(record)} +
+ ); + }, + }, + ], + }, + ]; + + const pagination = { + pageIndex: this.state.page, + pageSize: 10, + totalItemCount: this.state.total, + hidePerPageOptions: true, + }; + + return ( + + ); + } + + private renderDownloadButton = (record: Job) => { + if (record.status !== 'completed') { + return; + } + + const button = ( + downloadReport(record.id)} + iconType="importAction" + aria-label="Download report" + /> + ); + + if (record.max_size_reached) { + return ( + + {button} + + ); + } + + return button; + }; + + private renderReportErrorButton = (record: Job) => { + if (record.status !== 'failed') { + return; + } + + return ; + }; + + 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; + } + } +} diff --git a/x-pack/plugins/reporting/public/hacks/job_completion_notifier.js b/x-pack/plugins/reporting/public/hacks/job_completion_notifier.js index 3b15b3557f22..b4e91a94e0c3 100644 --- a/x-pack/plugins/reporting/public/hacks/job_completion_notifier.js +++ b/x-pack/plugins/reporting/public/hacks/job_completion_notifier.js @@ -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); -} - diff --git a/x-pack/plugins/reporting/public/less/main.less b/x-pack/plugins/reporting/public/less/main.less index 0e38c2ff091e..000f350b2e50 100644 --- a/x-pack/plugins/reporting/public/less/main.less +++ b/x-pack/plugins/reporting/public/less/main.less @@ -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; } diff --git a/x-pack/plugins/reporting/public/lib/download_report.ts b/x-pack/plugins/reporting/public/lib/download_report.ts new file mode 100644 index 000000000000..da22093da2ab --- /dev/null +++ b/x-pack/plugins/reporting/public/lib/download_report.ts @@ -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); +} diff --git a/x-pack/plugins/reporting/public/services/job_completion_notifications.d.ts b/x-pack/plugins/reporting/public/lib/job_completion_notifications.d.ts similarity index 100% rename from x-pack/plugins/reporting/public/services/job_completion_notifications.d.ts rename to x-pack/plugins/reporting/public/lib/job_completion_notifications.d.ts diff --git a/x-pack/plugins/reporting/public/services/job_completion_notifications.js b/x-pack/plugins/reporting/public/lib/job_completion_notifications.js similarity index 86% rename from x-pack/plugins/reporting/public/services/job_completion_notifications.js rename to x-pack/plugins/reporting/public/lib/job_completion_notifications.js index 82506d544808..131873fb4f62 100644 --- a/x-pack/plugins/reporting/public/services/job_completion_notifications.js +++ b/x-pack/plugins/reporting/public/lib/job_completion_notifications.js @@ -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); diff --git a/x-pack/plugins/reporting/public/lib/job_queue_client.ts b/x-pack/plugins/reporting/public/lib/job_queue_client.ts new file mode 100644 index 000000000000..75080dd4474f --- /dev/null +++ b/x-pack/plugins/reporting/public/lib/job_queue_client.ts @@ -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 => { + 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 { + return kfetch({ + method: 'GET', + pathname: `${API_BASE_URL}/count`, + headers: addSystemApiHeader({}), + }); + } + + public getContent(jobId: string): Promise { + return kfetch({ + method: 'GET', + pathname: `${API_BASE_URL}/output/${jobId}`, + headers: addSystemApiHeader({}), + }); + } +} + +export const jobQueueClient = new JobQueueClient(); diff --git a/x-pack/plugins/reporting/public/lib/reporting_client.ts b/x-pack/plugins/reporting/public/lib/reporting_client.ts index 80af64706cba..2e10a1333de4 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_client.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_client.ts @@ -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'; diff --git a/x-pack/plugins/reporting/public/services/job_queue.js b/x-pack/plugins/reporting/public/services/job_queue.js deleted file mode 100644 index 0732a1b51839..000000000000 --- a/x-pack/plugins/reporting/public/services/job_queue.js +++ /dev/null @@ -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); - } - }; -}); diff --git a/x-pack/plugins/reporting/public/views/management/jobs.html b/x-pack/plugins/reporting/public/views/management/jobs.html index 055c1b0be0e3..907c375adcdf 100644 --- a/x-pack/plugins/reporting/public/views/management/jobs.html +++ b/x-pack/plugins/reporting/public/views/management/jobs.html @@ -1,92 +1,3 @@ -
-
-

Generated reports

- - - - - - - - - - - - - - - - - - - - - -
DocumentAddedStatusActions
No reports have been created
-
{{ job.object_title }}
- -
-
{{ job.created_at | date : 'yyyy-MM-dd @ h:mm a' }}
- -
-
- {{ job.status }} - max size reached -
- - -
- - -
- {{ jobsCtrl.errorMessage.message }} -
- - -
- -
- - -
-
-
+
diff --git a/x-pack/plugins/reporting/public/views/management/jobs.js b/x-pack/plugins/reporting/public/views/management/jobs.js index 210a379b33e0..3ad31363c1e7 100644 --- a/x-pack/plugins/reporting/public/views/management/jobs.js +++ b/x-pack/plugins/reporting/public/views/management/jobs.js @@ -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( + , + 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); + } + }); } }); diff --git a/x-pack/yarn.lock b/x-pack/yarn.lock index 270638892e22..5d8cbafd5743 100644 --- a/x-pack/yarn.lock +++ b/x-pack/yarn.lock @@ -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" diff --git a/yarn.lock b/yarn.lock index 805930d4961d..37322d520eac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"