[Reporting] Allow reports to be deleted in Management > Kibana > Reporting (#60077)
* [Reporting] Feature Delete Button in Job Listing * refactor listing buttons * multi-delete * confirm modal * remove unused * fix test * mock the id generator for snapshotting * simplify * add search bar above table * fix types errors
This commit is contained in:
parent
915b784cd6
commit
ce2e3fd621
|
@ -4,6 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import Boom from 'boom';
|
||||
import { errors as elasticsearchErrors } from 'elasticsearch';
|
||||
import { ElasticsearchServiceSetup } from 'kibana/server';
|
||||
import { get } from 'lodash';
|
||||
|
@ -152,5 +154,21 @@ export function jobsQueryFactory(server: ServerFacade, elasticsearch: Elasticsea
|
|||
return hits[0];
|
||||
});
|
||||
},
|
||||
|
||||
async delete(deleteIndex: string, id: string) {
|
||||
try {
|
||||
const query = { id, index: deleteIndex };
|
||||
return callAsInternalUser('delete', query);
|
||||
} catch (error) {
|
||||
const wrappedError = new Error(
|
||||
i18n.translate('xpack.reporting.jobsQuery.deleteError', {
|
||||
defaultMessage: 'Could not delete the report: {error}',
|
||||
values: { error: error.message },
|
||||
})
|
||||
);
|
||||
|
||||
throw Boom.boomify(wrappedError, { statusCode: error.status });
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -18,9 +18,13 @@ import {
|
|||
} from '../../types';
|
||||
import { jobsQueryFactory } from '../lib/jobs_query';
|
||||
import { ReportingSetupDeps, ReportingCore } from '../types';
|
||||
import { jobResponseHandlerFactory } from './lib/job_response_handler';
|
||||
import {
|
||||
deleteJobResponseHandlerFactory,
|
||||
downloadJobResponseHandlerFactory,
|
||||
} from './lib/job_response_handler';
|
||||
import { makeRequestFacade } from './lib/make_request_facade';
|
||||
import {
|
||||
getRouteConfigFactoryDeletePre,
|
||||
getRouteConfigFactoryDownloadPre,
|
||||
getRouteConfigFactoryManagementPre,
|
||||
} from './lib/route_config_factories';
|
||||
|
@ -40,7 +44,6 @@ export function registerJobInfoRoutes(
|
|||
const { elasticsearch } = plugins;
|
||||
const jobsQuery = jobsQueryFactory(server, elasticsearch);
|
||||
const getRouteConfig = getRouteConfigFactoryManagementPre(server, plugins, logger);
|
||||
const getRouteConfigDownload = getRouteConfigFactoryDownloadPre(server, plugins, logger);
|
||||
|
||||
// list jobs in the queue, paginated
|
||||
server.route({
|
||||
|
@ -138,7 +141,8 @@ export function registerJobInfoRoutes(
|
|||
|
||||
// trigger a download of the output from a job
|
||||
const exportTypesRegistry = reporting.getExportTypesRegistry();
|
||||
const jobResponseHandler = jobResponseHandlerFactory(server, elasticsearch, exportTypesRegistry);
|
||||
const getRouteConfigDownload = getRouteConfigFactoryDownloadPre(server, plugins, logger);
|
||||
const downloadResponseHandler = downloadJobResponseHandlerFactory(server, elasticsearch, exportTypesRegistry); // prettier-ignore
|
||||
server.route({
|
||||
path: `${MAIN_ENTRY}/download/{docId}`,
|
||||
method: 'GET',
|
||||
|
@ -147,7 +151,47 @@ export function registerJobInfoRoutes(
|
|||
const request = makeRequestFacade(legacyRequest);
|
||||
const { docId } = request.params;
|
||||
|
||||
let response = await jobResponseHandler(
|
||||
let response = await downloadResponseHandler(
|
||||
request.pre.management.jobTypes,
|
||||
request.pre.user,
|
||||
h,
|
||||
{ docId }
|
||||
);
|
||||
|
||||
if (isResponse(response)) {
|
||||
const { statusCode } = response;
|
||||
|
||||
if (statusCode !== 200) {
|
||||
if (statusCode === 500) {
|
||||
logger.error(`Report ${docId} has failed: ${JSON.stringify(response.source)}`);
|
||||
} else {
|
||||
logger.debug(
|
||||
`Report ${docId} has non-OK status: [${statusCode}] Reason: [${JSON.stringify(
|
||||
response.source
|
||||
)}]`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
response = response.header('accept-ranges', 'none');
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
// allow a report to be deleted
|
||||
const getRouteConfigDelete = getRouteConfigFactoryDeletePre(server, plugins, logger);
|
||||
const deleteResponseHandler = deleteJobResponseHandlerFactory(server, elasticsearch);
|
||||
server.route({
|
||||
path: `${MAIN_ENTRY}/delete/{docId}`,
|
||||
method: 'DELETE',
|
||||
options: getRouteConfigDelete(),
|
||||
handler: async (legacyRequest: Legacy.Request, h: ReportingResponseToolkit) => {
|
||||
const request = makeRequestFacade(legacyRequest);
|
||||
const { docId } = request.params;
|
||||
|
||||
let response = await deleteResponseHandler(
|
||||
request.pre.management.jobTypes,
|
||||
request.pre.user,
|
||||
h,
|
||||
|
|
|
@ -20,7 +20,7 @@ interface JobResponseHandlerOpts {
|
|||
excludeContent?: boolean;
|
||||
}
|
||||
|
||||
export function jobResponseHandlerFactory(
|
||||
export function downloadJobResponseHandlerFactory(
|
||||
server: ServerFacade,
|
||||
elasticsearch: ElasticsearchServiceSetup,
|
||||
exportTypesRegistry: ExportTypesRegistry
|
||||
|
@ -36,6 +36,7 @@ export function jobResponseHandlerFactory(
|
|||
opts: JobResponseHandlerOpts = {}
|
||||
) {
|
||||
const { docId } = params;
|
||||
// TODO: async/await
|
||||
return jobsQuery.get(user, docId, { includeContent: !opts.excludeContent }).then(doc => {
|
||||
if (!doc) return Boom.notFound();
|
||||
|
||||
|
@ -67,3 +68,34 @@ export function jobResponseHandlerFactory(
|
|||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteJobResponseHandlerFactory(
|
||||
server: ServerFacade,
|
||||
elasticsearch: ElasticsearchServiceSetup
|
||||
) {
|
||||
const jobsQuery = jobsQueryFactory(server, elasticsearch);
|
||||
|
||||
return async function deleteJobResponseHander(
|
||||
validJobTypes: string[],
|
||||
user: any,
|
||||
h: ResponseToolkit,
|
||||
params: JobResponseHandlerParams
|
||||
) {
|
||||
const { docId } = params;
|
||||
const doc = await jobsQuery.get(user, docId, { includeContent: false });
|
||||
if (!doc) return Boom.notFound();
|
||||
|
||||
const { jobtype: jobType } = doc._source;
|
||||
if (!validJobTypes.includes(jobType)) {
|
||||
return Boom.unauthorized(`Sorry, you are not authorized to delete ${jobType} reports`);
|
||||
}
|
||||
|
||||
try {
|
||||
const docIndex = doc._index;
|
||||
await jobsQuery.delete(docIndex, docId);
|
||||
return h.response({ deleted: true });
|
||||
} catch (error) {
|
||||
return Boom.boomify(error, { statusCode: error.statusCode });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -106,7 +106,22 @@ export function getRouteConfigFactoryDownloadPre(
|
|||
const getManagementRouteConfig = getRouteConfigFactoryManagementPre(server, plugins, logger);
|
||||
return (): RouteConfigFactory => ({
|
||||
...getManagementRouteConfig(),
|
||||
tags: [API_TAG],
|
||||
tags: [API_TAG, 'download'],
|
||||
response: {
|
||||
ranges: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function getRouteConfigFactoryDeletePre(
|
||||
server: ServerFacade,
|
||||
plugins: ReportingSetupDeps,
|
||||
logger: Logger
|
||||
): GetRouteConfigFactoryFn {
|
||||
const getManagementRouteConfig = getRouteConfigFactoryManagementPre(server, plugins, logger);
|
||||
return (): RouteConfigFactory => ({
|
||||
...getManagementRouteConfig(),
|
||||
tags: [API_TAG, 'delete'],
|
||||
response: {
|
||||
ranges: false,
|
||||
},
|
||||
|
|
1
x-pack/legacy/plugins/reporting/types.d.ts
vendored
1
x-pack/legacy/plugins/reporting/types.d.ts
vendored
|
@ -197,6 +197,7 @@ export interface JobDocPayload<JobParamsType> {
|
|||
|
||||
export interface JobSource<JobParamsType> {
|
||||
_id: string;
|
||||
_index: string;
|
||||
_source: {
|
||||
jobtype: string;
|
||||
output: JobDocOutput;
|
||||
|
|
|
@ -2,6 +2,526 @@
|
|||
|
||||
exports[`ReportListing Report job listing with some items 1`] = `
|
||||
Array [
|
||||
<EuiInMemoryTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"field": "object_title",
|
||||
"name": "Report",
|
||||
"render": [Function],
|
||||
},
|
||||
Object {
|
||||
"field": "created_at",
|
||||
"name": "Created at",
|
||||
"render": [Function],
|
||||
},
|
||||
Object {
|
||||
"field": "status",
|
||||
"name": "Status",
|
||||
"render": [Function],
|
||||
},
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"render": [Function],
|
||||
},
|
||||
],
|
||||
"name": "Actions",
|
||||
},
|
||||
]
|
||||
}
|
||||
data-test-subj="reportJobListing"
|
||||
isSelectable={true}
|
||||
itemId="id"
|
||||
items={Array []}
|
||||
loading={true}
|
||||
message="Loading reports"
|
||||
onChange={[Function]}
|
||||
pagination={
|
||||
Object {
|
||||
"hidePerPageOptions": true,
|
||||
"pageIndex": 0,
|
||||
"pageSize": 10,
|
||||
"totalItemCount": 0,
|
||||
}
|
||||
}
|
||||
responsive={true}
|
||||
search={
|
||||
Object {
|
||||
"toolsRight": null,
|
||||
}
|
||||
}
|
||||
selection={
|
||||
Object {
|
||||
"itemId": "id",
|
||||
"onSelectionChange": [Function],
|
||||
}
|
||||
}
|
||||
tableLayout="fixed"
|
||||
>
|
||||
<div>
|
||||
<EuiSearchBar
|
||||
onChange={[Function]}
|
||||
toolsRight={null}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="m"
|
||||
wrap={true}
|
||||
>
|
||||
<div
|
||||
className="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive euiFlexGroup--wrap"
|
||||
>
|
||||
<EuiFlexItem
|
||||
className="euiSearchBar__searchHolder"
|
||||
grow={true}
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem euiSearchBar__searchHolder"
|
||||
>
|
||||
<EuiSearchBox
|
||||
incremental={false}
|
||||
isInvalid={false}
|
||||
onSearch={[Function]}
|
||||
placeholder="Search..."
|
||||
query=""
|
||||
>
|
||||
<EuiFieldSearch
|
||||
aria-label="This is a search bar. After typing your query, hit enter to filter the results lower in the page."
|
||||
compressed={false}
|
||||
defaultValue=""
|
||||
fullWidth={true}
|
||||
incremental={false}
|
||||
inputRef={[Function]}
|
||||
isClearable={true}
|
||||
isInvalid={false}
|
||||
isLoading={false}
|
||||
onSearch={[Function]}
|
||||
placeholder="Search..."
|
||||
>
|
||||
<EuiFormControlLayout
|
||||
compressed={false}
|
||||
fullWidth={true}
|
||||
icon="search"
|
||||
isLoading={false}
|
||||
>
|
||||
<div
|
||||
className="euiFormControlLayout euiFormControlLayout--fullWidth"
|
||||
>
|
||||
<div
|
||||
className="euiFormControlLayout__childrenWrapper"
|
||||
>
|
||||
<EuiValidatableControl
|
||||
isInvalid={false}
|
||||
>
|
||||
<input
|
||||
aria-label="This is a search bar. After typing your query, hit enter to filter the results lower in the page."
|
||||
className="euiFieldSearch euiFieldSearch--fullWidth"
|
||||
defaultValue=""
|
||||
onKeyUp={[Function]}
|
||||
placeholder="Search..."
|
||||
type="search"
|
||||
/>
|
||||
</EuiValidatableControl>
|
||||
<EuiFormControlLayoutIcons
|
||||
icon="search"
|
||||
isLoading={false}
|
||||
>
|
||||
<div
|
||||
className="euiFormControlLayoutIcons"
|
||||
>
|
||||
<EuiFormControlLayoutCustomIcon
|
||||
type="search"
|
||||
>
|
||||
<span
|
||||
className="euiFormControlLayoutCustomIcon"
|
||||
>
|
||||
<EuiIcon
|
||||
aria-hidden="true"
|
||||
className="euiFormControlLayoutCustomIcon__icon"
|
||||
type="search"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="euiFormControlLayoutCustomIcon__icon"
|
||||
data-euiicon-type="search"
|
||||
/>
|
||||
</EuiIcon>
|
||||
</span>
|
||||
</EuiFormControlLayoutCustomIcon>
|
||||
</div>
|
||||
</EuiFormControlLayoutIcons>
|
||||
</div>
|
||||
</div>
|
||||
</EuiFormControlLayout>
|
||||
</EuiFieldSearch>
|
||||
</EuiSearchBox>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</div>
|
||||
</EuiFlexGroup>
|
||||
</EuiSearchBar>
|
||||
<EuiSpacer
|
||||
size="l"
|
||||
>
|
||||
<div
|
||||
className="euiSpacer euiSpacer--l"
|
||||
/>
|
||||
</EuiSpacer>
|
||||
<EuiBasicTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"field": "object_title",
|
||||
"name": "Report",
|
||||
"render": [Function],
|
||||
},
|
||||
Object {
|
||||
"field": "created_at",
|
||||
"name": "Created at",
|
||||
"render": [Function],
|
||||
},
|
||||
Object {
|
||||
"field": "status",
|
||||
"name": "Status",
|
||||
"render": [Function],
|
||||
},
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"render": [Function],
|
||||
},
|
||||
],
|
||||
"name": "Actions",
|
||||
},
|
||||
]
|
||||
}
|
||||
data-test-subj="reportJobListing"
|
||||
isSelectable={true}
|
||||
itemId="id"
|
||||
items={Array []}
|
||||
loading={true}
|
||||
noItemsMessage="Loading reports"
|
||||
onChange={[Function]}
|
||||
pagination={
|
||||
Object {
|
||||
"hidePerPageOptions": true,
|
||||
"pageIndex": 0,
|
||||
"pageSize": 10,
|
||||
"pageSizeOptions": Array [
|
||||
10,
|
||||
25,
|
||||
50,
|
||||
],
|
||||
"totalItemCount": 0,
|
||||
}
|
||||
}
|
||||
responsive={true}
|
||||
selection={
|
||||
Object {
|
||||
"itemId": "id",
|
||||
"onSelectionChange": [Function],
|
||||
}
|
||||
}
|
||||
tableLayout="fixed"
|
||||
>
|
||||
<div
|
||||
className="euiBasicTable euiBasicTable-loading"
|
||||
data-test-subj="reportJobListing"
|
||||
>
|
||||
<div>
|
||||
<EuiTableHeaderMobile>
|
||||
<div
|
||||
className="euiTableHeaderMobile"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="baseline"
|
||||
justifyContent="spaceBetween"
|
||||
responsive={false}
|
||||
>
|
||||
<div
|
||||
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<EuiI18n
|
||||
default="Select all rows"
|
||||
token="euiBasicTable.selectAllRows"
|
||||
>
|
||||
<EuiCheckbox
|
||||
aria-label="Select all rows"
|
||||
checked={false}
|
||||
compressed={false}
|
||||
disabled={true}
|
||||
id="_selection_column-checkbox_generated-id"
|
||||
indeterminate={false}
|
||||
label="Select all rows"
|
||||
onChange={[Function]}
|
||||
>
|
||||
<div
|
||||
className="euiCheckbox"
|
||||
>
|
||||
<input
|
||||
aria-label="Select all rows"
|
||||
checked={false}
|
||||
className="euiCheckbox__input"
|
||||
disabled={true}
|
||||
id="_selection_column-checkbox_generated-id"
|
||||
onChange={[Function]}
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
className="euiCheckbox__square"
|
||||
/>
|
||||
<label
|
||||
className="euiCheckbox__label"
|
||||
htmlFor="_selection_column-checkbox_generated-id"
|
||||
>
|
||||
Select all rows
|
||||
</label>
|
||||
</div>
|
||||
</EuiCheckbox>
|
||||
</EuiI18n>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</div>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</EuiTableHeaderMobile>
|
||||
<EuiTable
|
||||
responsive={true}
|
||||
tableLayout="fixed"
|
||||
>
|
||||
<table
|
||||
className="euiTable euiTable--responsive"
|
||||
>
|
||||
<EuiScreenReaderOnly>
|
||||
<caption
|
||||
className="euiScreenReaderOnly euiTableCaption"
|
||||
>
|
||||
<EuiDelayRender
|
||||
delay={500}
|
||||
/>
|
||||
</caption>
|
||||
</EuiScreenReaderOnly>
|
||||
<EuiTableHeader>
|
||||
<thead>
|
||||
<tr>
|
||||
<EuiTableHeaderCellCheckbox
|
||||
key="_selection_column_h"
|
||||
>
|
||||
<th
|
||||
className="euiTableHeaderCellCheckbox"
|
||||
scope="col"
|
||||
style={
|
||||
Object {
|
||||
"width": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="euiTableCellContent"
|
||||
>
|
||||
<EuiI18n
|
||||
default="Select all rows"
|
||||
token="euiBasicTable.selectAllRows"
|
||||
>
|
||||
<EuiCheckbox
|
||||
aria-label="Select all rows"
|
||||
checked={false}
|
||||
compressed={false}
|
||||
data-test-subj="checkboxSelectAll"
|
||||
disabled={true}
|
||||
id="_selection_column-checkbox_generated-id"
|
||||
indeterminate={false}
|
||||
label={null}
|
||||
onChange={[Function]}
|
||||
type="inList"
|
||||
>
|
||||
<div
|
||||
className="euiCheckbox euiCheckbox--inList euiCheckbox--noLabel"
|
||||
>
|
||||
<input
|
||||
aria-label="Select all rows"
|
||||
checked={false}
|
||||
className="euiCheckbox__input"
|
||||
data-test-subj="checkboxSelectAll"
|
||||
disabled={true}
|
||||
id="_selection_column-checkbox_generated-id"
|
||||
onChange={[Function]}
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
className="euiCheckbox__square"
|
||||
/>
|
||||
</div>
|
||||
</EuiCheckbox>
|
||||
</EuiI18n>
|
||||
</div>
|
||||
</th>
|
||||
</EuiTableHeaderCellCheckbox>
|
||||
<EuiTableHeaderCell
|
||||
align="left"
|
||||
data-test-subj="tableHeaderCell_object_title_0"
|
||||
key="_data_h_object_title_0"
|
||||
>
|
||||
<th
|
||||
className="euiTableHeaderCell"
|
||||
data-test-subj="tableHeaderCell_object_title_0"
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
style={
|
||||
Object {
|
||||
"width": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="euiTableCellContent"
|
||||
>
|
||||
<span
|
||||
className="euiTableCellContent__text"
|
||||
>
|
||||
Report
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
</EuiTableHeaderCell>
|
||||
<EuiTableHeaderCell
|
||||
align="left"
|
||||
data-test-subj="tableHeaderCell_created_at_1"
|
||||
key="_data_h_created_at_1"
|
||||
>
|
||||
<th
|
||||
className="euiTableHeaderCell"
|
||||
data-test-subj="tableHeaderCell_created_at_1"
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
style={
|
||||
Object {
|
||||
"width": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="euiTableCellContent"
|
||||
>
|
||||
<span
|
||||
className="euiTableCellContent__text"
|
||||
>
|
||||
Created at
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
</EuiTableHeaderCell>
|
||||
<EuiTableHeaderCell
|
||||
align="left"
|
||||
data-test-subj="tableHeaderCell_status_2"
|
||||
key="_data_h_status_2"
|
||||
>
|
||||
<th
|
||||
className="euiTableHeaderCell"
|
||||
data-test-subj="tableHeaderCell_status_2"
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
style={
|
||||
Object {
|
||||
"width": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="euiTableCellContent"
|
||||
>
|
||||
<span
|
||||
className="euiTableCellContent__text"
|
||||
>
|
||||
Status
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
</EuiTableHeaderCell>
|
||||
<EuiTableHeaderCell
|
||||
align="right"
|
||||
key="_actions_h_3"
|
||||
>
|
||||
<th
|
||||
className="euiTableHeaderCell"
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
style={
|
||||
Object {
|
||||
"width": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="euiTableCellContent euiTableCellContent--alignRight"
|
||||
>
|
||||
<span
|
||||
className="euiTableCellContent__text"
|
||||
>
|
||||
Actions
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
</EuiTableHeaderCell>
|
||||
</tr>
|
||||
</thead>
|
||||
</EuiTableHeader>
|
||||
<EuiTableBody>
|
||||
<tbody>
|
||||
<EuiTableRow>
|
||||
<tr
|
||||
className="euiTableRow"
|
||||
>
|
||||
<EuiTableRowCell
|
||||
align="center"
|
||||
colSpan={5}
|
||||
isMobileFullWidth={true}
|
||||
>
|
||||
<td
|
||||
className="euiTableRowCell euiTableRowCell--isMobileFullWidth"
|
||||
colSpan={5}
|
||||
style={
|
||||
Object {
|
||||
"width": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="euiTableCellContent euiTableCellContent--alignCenter"
|
||||
>
|
||||
<span
|
||||
className="euiTableCellContent__text"
|
||||
>
|
||||
Loading reports
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</EuiTableRowCell>
|
||||
</tr>
|
||||
</EuiTableRow>
|
||||
</tbody>
|
||||
</EuiTableBody>
|
||||
</table>
|
||||
</EuiTable>
|
||||
</div>
|
||||
</div>
|
||||
</EuiBasicTable>
|
||||
</div>
|
||||
</EuiInMemoryTable>,
|
||||
<EuiBasicTable
|
||||
columns={
|
||||
Array [
|
||||
|
@ -31,6 +551,7 @@ Array [
|
|||
]
|
||||
}
|
||||
data-test-subj="reportJobListing"
|
||||
isSelectable={true}
|
||||
itemId="id"
|
||||
items={Array []}
|
||||
loading={true}
|
||||
|
@ -41,10 +562,21 @@ Array [
|
|||
"hidePerPageOptions": true,
|
||||
"pageIndex": 0,
|
||||
"pageSize": 10,
|
||||
"pageSizeOptions": Array [
|
||||
10,
|
||||
25,
|
||||
50,
|
||||
],
|
||||
"totalItemCount": 0,
|
||||
}
|
||||
}
|
||||
responsive={true}
|
||||
selection={
|
||||
Object {
|
||||
"itemId": "id",
|
||||
"onSelectionChange": [Function],
|
||||
}
|
||||
}
|
||||
tableLayout="fixed"
|
||||
>
|
||||
<div
|
||||
|
@ -69,7 +601,46 @@ Array [
|
|||
>
|
||||
<div
|
||||
className="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
/>
|
||||
>
|
||||
<EuiI18n
|
||||
default="Select all rows"
|
||||
token="euiBasicTable.selectAllRows"
|
||||
>
|
||||
<EuiCheckbox
|
||||
aria-label="Select all rows"
|
||||
checked={false}
|
||||
compressed={false}
|
||||
disabled={true}
|
||||
id="_selection_column-checkbox_generated-id"
|
||||
indeterminate={false}
|
||||
label="Select all rows"
|
||||
onChange={[Function]}
|
||||
>
|
||||
<div
|
||||
className="euiCheckbox"
|
||||
>
|
||||
<input
|
||||
aria-label="Select all rows"
|
||||
checked={false}
|
||||
className="euiCheckbox__input"
|
||||
disabled={true}
|
||||
id="_selection_column-checkbox_generated-id"
|
||||
onChange={[Function]}
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
className="euiCheckbox__square"
|
||||
/>
|
||||
<label
|
||||
className="euiCheckbox__label"
|
||||
htmlFor="_selection_column-checkbox_generated-id"
|
||||
>
|
||||
Select all rows
|
||||
</label>
|
||||
</div>
|
||||
</EuiCheckbox>
|
||||
</EuiI18n>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
|
@ -101,6 +672,59 @@ Array [
|
|||
<EuiTableHeader>
|
||||
<thead>
|
||||
<tr>
|
||||
<EuiTableHeaderCellCheckbox
|
||||
key="_selection_column_h"
|
||||
>
|
||||
<th
|
||||
className="euiTableHeaderCellCheckbox"
|
||||
scope="col"
|
||||
style={
|
||||
Object {
|
||||
"width": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="euiTableCellContent"
|
||||
>
|
||||
<EuiI18n
|
||||
default="Select all rows"
|
||||
token="euiBasicTable.selectAllRows"
|
||||
>
|
||||
<EuiCheckbox
|
||||
aria-label="Select all rows"
|
||||
checked={false}
|
||||
compressed={false}
|
||||
data-test-subj="checkboxSelectAll"
|
||||
disabled={true}
|
||||
id="_selection_column-checkbox_generated-id"
|
||||
indeterminate={false}
|
||||
label={null}
|
||||
onChange={[Function]}
|
||||
type="inList"
|
||||
>
|
||||
<div
|
||||
className="euiCheckbox euiCheckbox--inList euiCheckbox--noLabel"
|
||||
>
|
||||
<input
|
||||
aria-label="Select all rows"
|
||||
checked={false}
|
||||
className="euiCheckbox__input"
|
||||
data-test-subj="checkboxSelectAll"
|
||||
disabled={true}
|
||||
id="_selection_column-checkbox_generated-id"
|
||||
onChange={[Function]}
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
className="euiCheckbox__square"
|
||||
/>
|
||||
</div>
|
||||
</EuiCheckbox>
|
||||
</EuiI18n>
|
||||
</div>
|
||||
</th>
|
||||
</EuiTableHeaderCellCheckbox>
|
||||
<EuiTableHeaderCell
|
||||
align="left"
|
||||
data-test-subj="tableHeaderCell_object_title_0"
|
||||
|
@ -218,12 +842,12 @@ Array [
|
|||
>
|
||||
<EuiTableRowCell
|
||||
align="center"
|
||||
colSpan={4}
|
||||
colSpan={5}
|
||||
isMobileFullWidth={true}
|
||||
>
|
||||
<td
|
||||
className="euiTableRowCell euiTableRowCell--isMobileFullWidth"
|
||||
colSpan={4}
|
||||
colSpan={5}
|
||||
style={
|
||||
Object {
|
||||
"width": undefined,
|
||||
|
@ -272,7 +896,46 @@ Array [
|
|||
>
|
||||
<div
|
||||
className="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
/>
|
||||
>
|
||||
<EuiI18n
|
||||
default="Select all rows"
|
||||
token="euiBasicTable.selectAllRows"
|
||||
>
|
||||
<EuiCheckbox
|
||||
aria-label="Select all rows"
|
||||
checked={false}
|
||||
compressed={false}
|
||||
disabled={true}
|
||||
id="_selection_column-checkbox_generated-id"
|
||||
indeterminate={false}
|
||||
label="Select all rows"
|
||||
onChange={[Function]}
|
||||
>
|
||||
<div
|
||||
className="euiCheckbox"
|
||||
>
|
||||
<input
|
||||
aria-label="Select all rows"
|
||||
checked={false}
|
||||
className="euiCheckbox__input"
|
||||
disabled={true}
|
||||
id="_selection_column-checkbox_generated-id"
|
||||
onChange={[Function]}
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
className="euiCheckbox__square"
|
||||
/>
|
||||
<label
|
||||
className="euiCheckbox__label"
|
||||
htmlFor="_selection_column-checkbox_generated-id"
|
||||
>
|
||||
Select all rows
|
||||
</label>
|
||||
</div>
|
||||
</EuiCheckbox>
|
||||
</EuiI18n>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
|
@ -304,6 +967,59 @@ Array [
|
|||
<EuiTableHeader>
|
||||
<thead>
|
||||
<tr>
|
||||
<EuiTableHeaderCellCheckbox
|
||||
key="_selection_column_h"
|
||||
>
|
||||
<th
|
||||
className="euiTableHeaderCellCheckbox"
|
||||
scope="col"
|
||||
style={
|
||||
Object {
|
||||
"width": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="euiTableCellContent"
|
||||
>
|
||||
<EuiI18n
|
||||
default="Select all rows"
|
||||
token="euiBasicTable.selectAllRows"
|
||||
>
|
||||
<EuiCheckbox
|
||||
aria-label="Select all rows"
|
||||
checked={false}
|
||||
compressed={false}
|
||||
data-test-subj="checkboxSelectAll"
|
||||
disabled={true}
|
||||
id="_selection_column-checkbox_generated-id"
|
||||
indeterminate={false}
|
||||
label={null}
|
||||
onChange={[Function]}
|
||||
type="inList"
|
||||
>
|
||||
<div
|
||||
className="euiCheckbox euiCheckbox--inList euiCheckbox--noLabel"
|
||||
>
|
||||
<input
|
||||
aria-label="Select all rows"
|
||||
checked={false}
|
||||
className="euiCheckbox__input"
|
||||
data-test-subj="checkboxSelectAll"
|
||||
disabled={true}
|
||||
id="_selection_column-checkbox_generated-id"
|
||||
onChange={[Function]}
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
className="euiCheckbox__square"
|
||||
/>
|
||||
</div>
|
||||
</EuiCheckbox>
|
||||
</EuiI18n>
|
||||
</div>
|
||||
</th>
|
||||
</EuiTableHeaderCellCheckbox>
|
||||
<EuiTableHeaderCell
|
||||
align="left"
|
||||
data-test-subj="tableHeaderCell_object_title_0"
|
||||
|
@ -421,12 +1137,12 @@ Array [
|
|||
>
|
||||
<EuiTableRowCell
|
||||
align="center"
|
||||
colSpan={4}
|
||||
colSpan={5}
|
||||
isMobileFullWidth={true}
|
||||
>
|
||||
<td
|
||||
className="euiTableRowCell euiTableRowCell--isMobileFullWidth"
|
||||
colSpan={4}
|
||||
colSpan={5}
|
||||
style={
|
||||
Object {
|
||||
"width": undefined,
|
||||
|
|
10
x-pack/plugins/reporting/public/components/buttons/index.tsx
Normal file
10
x-pack/plugins/reporting/public/components/buttons/index.tsx
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 { ReportErrorButton } from './report_error_button';
|
||||
export { ReportDeleteButton } from './report_delete_button';
|
||||
export { ReportDownloadButton } from './report_download_button';
|
||||
export { ReportInfoButton } from './report_info_button';
|
|
@ -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 { EuiConfirmModal, EuiOverlayMask, EuiButton } from '@elastic/eui';
|
||||
import React, { PureComponent, Fragment } from 'react';
|
||||
import { Job, Props as ListingProps } from '../report_listing';
|
||||
|
||||
type DeleteFn = () => Promise<void>;
|
||||
type Props = { jobsToDelete: Job[]; performDelete: DeleteFn } & ListingProps;
|
||||
interface State {
|
||||
showConfirm: boolean;
|
||||
}
|
||||
|
||||
export class ReportDeleteButton extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { showConfirm: false };
|
||||
}
|
||||
|
||||
private hideConfirm() {
|
||||
this.setState({ showConfirm: false });
|
||||
}
|
||||
|
||||
private showConfirm() {
|
||||
this.setState({ showConfirm: true });
|
||||
}
|
||||
|
||||
private renderConfirm() {
|
||||
const { intl, jobsToDelete } = this.props;
|
||||
|
||||
const title =
|
||||
jobsToDelete.length > 1
|
||||
? intl.formatMessage(
|
||||
{
|
||||
id: 'xpack.reporting.listing.table.deleteNumConfirmTitle',
|
||||
defaultMessage: `Delete {num} reports?`,
|
||||
},
|
||||
{ num: jobsToDelete.length }
|
||||
)
|
||||
: intl.formatMessage(
|
||||
{
|
||||
id: 'xpack.reporting.listing.table.deleteConfirmTitle',
|
||||
defaultMessage: `Delete the "{name}" report?`,
|
||||
},
|
||||
{ name: jobsToDelete[0].object_title }
|
||||
);
|
||||
const message = intl.formatMessage({
|
||||
id: 'xpack.reporting.listing.table.deleteConfirmMessage',
|
||||
defaultMessage: `You can't recover deleted reports.`,
|
||||
});
|
||||
const confirmButtonText = intl.formatMessage({
|
||||
id: 'xpack.reporting.listing.table.deleteConfirmButton',
|
||||
defaultMessage: `Delete`,
|
||||
});
|
||||
const cancelButtonText = intl.formatMessage({
|
||||
id: 'xpack.reporting.listing.table.deleteCancelButton',
|
||||
defaultMessage: `Cancel`,
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
title={title}
|
||||
onCancel={() => this.hideConfirm()}
|
||||
onConfirm={() => this.props.performDelete()}
|
||||
confirmButtonText={confirmButtonText}
|
||||
cancelButtonText={cancelButtonText}
|
||||
defaultFocusedButton="confirm"
|
||||
buttonColor="danger"
|
||||
>
|
||||
{message}
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { jobsToDelete, intl } = this.props;
|
||||
if (jobsToDelete.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiButton onClick={() => this.showConfirm()} iconType="trash" color={'danger'}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: 'xpack.reporting.listing.table.deleteReportButton',
|
||||
defaultMessage: `Delete ({num})`,
|
||||
},
|
||||
{ num: jobsToDelete.length }
|
||||
)}
|
||||
</EuiButton>
|
||||
{this.state.showConfirm ? this.renderConfirm() : null}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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, EuiToolTip } from '@elastic/eui';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { JobStatuses } from '../../../constants';
|
||||
import { Job as ListingJob, Props as ListingProps } from '../report_listing';
|
||||
|
||||
type Props = { record: ListingJob } & ListingProps;
|
||||
|
||||
export const ReportDownloadButton: FunctionComponent<Props> = (props: Props) => {
|
||||
const { record, apiClient, intl } = props;
|
||||
|
||||
if (record.status !== JobStatuses.COMPLETED) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const button = (
|
||||
<EuiButtonIcon
|
||||
onClick={() => apiClient.downloadReport(record.id)}
|
||||
iconType="importAction"
|
||||
aria-label={intl.formatMessage({
|
||||
id: 'xpack.reporting.listing.table.downloadReportAriaLabel',
|
||||
defaultMessage: 'Download report',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
if (record.csv_contains_formulas) {
|
||||
return (
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={intl.formatMessage({
|
||||
id: 'xpack.reporting.listing.table.csvContainsFormulas',
|
||||
defaultMessage:
|
||||
'Your CSV contains characters which spreadsheet applications can interpret as formulas.',
|
||||
})}
|
||||
>
|
||||
{button}
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
|
||||
if (record.max_size_reached) {
|
||||
return (
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={intl.formatMessage({
|
||||
id: 'xpack.reporting.listing.table.maxSizeReachedTooltip',
|
||||
defaultMessage: 'Max size reached, contains partial data.',
|
||||
})}
|
||||
>
|
||||
{button}
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={intl.formatMessage({
|
||||
id: 'xpack.reporting.listing.table.downloadReport',
|
||||
defaultMessage: 'Download report',
|
||||
})}
|
||||
>
|
||||
{button}
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
|
@ -7,12 +7,14 @@
|
|||
import { EuiButtonIcon, EuiCallOut, EuiPopover } from '@elastic/eui';
|
||||
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import React, { Component } from 'react';
|
||||
import { JobContent, ReportingAPIClient } from '../lib/reporting_api_client';
|
||||
import { JobStatuses } from '../../../constants';
|
||||
import { JobContent, ReportingAPIClient } from '../../lib/reporting_api_client';
|
||||
import { Job as ListingJob } from '../report_listing';
|
||||
|
||||
interface Props {
|
||||
jobId: string;
|
||||
intl: InjectedIntl;
|
||||
apiClient: ReportingAPIClient;
|
||||
record: ListingJob;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -39,12 +41,18 @@ class ReportErrorButtonUi extends Component<Props, State> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const { record, intl } = this.props;
|
||||
|
||||
if (record.status !== JobStatuses.FAILED) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const button = (
|
||||
<EuiButtonIcon
|
||||
onClick={this.togglePopover}
|
||||
iconType="alert"
|
||||
color={'danger'}
|
||||
aria-label={this.props.intl.formatMessage({
|
||||
aria-label={intl.formatMessage({
|
||||
id: 'xpack.reporting.errorButton.showReportErrorAriaLabel',
|
||||
defaultMessage: 'Show report error',
|
||||
})}
|
||||
|
@ -89,9 +97,11 @@ class ReportErrorButtonUi extends Component<Props, State> {
|
|||
};
|
||||
|
||||
private loadError = async () => {
|
||||
const { record, apiClient, intl } = this.props;
|
||||
|
||||
this.setState({ isLoading: true });
|
||||
try {
|
||||
const reportContent: JobContent = await this.props.apiClient.getContent(this.props.jobId);
|
||||
const reportContent: JobContent = await apiClient.getContent(record.id);
|
||||
if (this.mounted) {
|
||||
this.setState({ isLoading: false, error: reportContent.content });
|
||||
}
|
||||
|
@ -99,7 +109,7 @@ class ReportErrorButtonUi extends Component<Props, State> {
|
|||
if (this.mounted) {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
calloutTitle: this.props.intl.formatMessage({
|
||||
calloutTitle: intl.formatMessage({
|
||||
id: 'xpack.reporting.errorButton.unableToFetchReportContentTitle',
|
||||
defaultMessage: 'Unable to fetch report content',
|
||||
}),
|
|
@ -7,9 +7,10 @@
|
|||
import React from 'react';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { ReportInfoButton } from './report_info_button';
|
||||
import { ReportingAPIClient } from '../lib/reporting_api_client';
|
||||
|
||||
jest.mock('../lib/reporting_api_client');
|
||||
jest.mock('../../lib/reporting_api_client');
|
||||
|
||||
import { ReportingAPIClient } from '../../lib/reporting_api_client';
|
||||
|
||||
const httpSetup = {} as any;
|
||||
const apiClient = new ReportingAPIClient(httpSetup);
|
|
@ -17,8 +17,8 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { get } from 'lodash';
|
||||
import { USES_HEADLESS_JOB_TYPES } from '../../constants';
|
||||
import { JobInfo, ReportingAPIClient } from '../lib/reporting_api_client';
|
||||
import { USES_HEADLESS_JOB_TYPES } from '../../../constants';
|
||||
import { JobInfo, ReportingAPIClient } from '../../lib/reporting_api_client';
|
||||
|
||||
interface Props {
|
||||
jobId: string;
|
|
@ -9,8 +9,8 @@ import { FormattedMessage } from '@kbn/i18n/react';
|
|||
import { ToastInput } from 'src/core/public';
|
||||
import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
|
||||
import { JobId, JobSummary } from '../../index.d';
|
||||
import { DownloadButton } from './job_download_button';
|
||||
import { ReportLink } from './report_link';
|
||||
import { DownloadButton } from './download_button';
|
||||
|
||||
export const getSuccessToast = (
|
||||
job: JobSummary,
|
||||
|
|
|
@ -9,8 +9,8 @@ import { FormattedMessage } from '@kbn/i18n/react';
|
|||
import { ToastInput } from 'src/core/public';
|
||||
import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
|
||||
import { JobId, JobSummary } from '../../index.d';
|
||||
import { DownloadButton } from './job_download_button';
|
||||
import { ReportLink } from './report_link';
|
||||
import { DownloadButton } from './download_button';
|
||||
|
||||
export const getWarningFormulasToast = (
|
||||
job: JobSummary,
|
||||
|
|
|
@ -9,8 +9,8 @@ import { FormattedMessage } from '@kbn/i18n/react';
|
|||
import { ToastInput } from 'src/core/public';
|
||||
import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
|
||||
import { JobId, JobSummary } from '../../index.d';
|
||||
import { DownloadButton } from './job_download_button';
|
||||
import { ReportLink } from './report_link';
|
||||
import { DownloadButton } from './download_button';
|
||||
|
||||
export const getWarningMaxSizeToast = (
|
||||
job: JobSummary,
|
||||
|
|
|
@ -5,12 +5,15 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { ReportListing } from './report_listing';
|
||||
import { Observable } from 'rxjs';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { ILicense } from '../../../licensing/public';
|
||||
import { ReportingAPIClient } from '../lib/reporting_api_client';
|
||||
|
||||
jest.mock('@elastic/eui/lib/components/form/form_row/make_id', () => () => 'generated-id');
|
||||
|
||||
import { ReportListing } from './report_listing';
|
||||
|
||||
const reportingAPIClient = {
|
||||
list: () =>
|
||||
Promise.resolve([
|
||||
|
|
|
@ -4,34 +4,34 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import { get } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import React, { Component } from 'react';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiButtonIcon,
|
||||
EuiInMemoryTable,
|
||||
EuiPageContent,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
EuiTitle,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { ToastsSetup, ApplicationStart } from 'src/core/public';
|
||||
import { LicensingPluginSetup, ILicense } from '../../../licensing/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import { get } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { Component, default as React } from 'react';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { ApplicationStart, ToastsSetup } from 'src/core/public';
|
||||
import { ILicense, LicensingPluginSetup } from '../../../licensing/public';
|
||||
import { Poller } from '../../common/poller';
|
||||
import { JobStatuses, JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG } from '../../constants';
|
||||
import { ReportingAPIClient, JobQueueEntry } from '../lib/reporting_api_client';
|
||||
import { checkLicense } from '../lib/license_check';
|
||||
import { ReportErrorButton } from './report_error_button';
|
||||
import { ReportInfoButton } from './report_info_button';
|
||||
import { JobQueueEntry, ReportingAPIClient } from '../lib/reporting_api_client';
|
||||
import {
|
||||
ReportDeleteButton,
|
||||
ReportDownloadButton,
|
||||
ReportErrorButton,
|
||||
ReportInfoButton,
|
||||
} from './buttons';
|
||||
|
||||
interface Job {
|
||||
export interface Job {
|
||||
id: string;
|
||||
type: string;
|
||||
object_type: string;
|
||||
|
@ -49,7 +49,7 @@ interface Job {
|
|||
warnings: string[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
export interface Props {
|
||||
intl: InjectedIntl;
|
||||
apiClient: ReportingAPIClient;
|
||||
license$: LicensingPluginSetup['license$'];
|
||||
|
@ -61,6 +61,7 @@ interface State {
|
|||
page: number;
|
||||
total: number;
|
||||
jobs: Job[];
|
||||
selectedJobs: Job[];
|
||||
isLoading: boolean;
|
||||
showLinks: boolean;
|
||||
enableLinks: boolean;
|
||||
|
@ -113,6 +114,7 @@ class ReportListingUi extends Component<Props, State> {
|
|||
page: 0,
|
||||
total: 0,
|
||||
jobs: [],
|
||||
selectedJobs: [],
|
||||
isLoading: false,
|
||||
showLinks: false,
|
||||
enableLinks: false,
|
||||
|
@ -182,6 +184,140 @@ class ReportListingUi extends Component<Props, State> {
|
|||
});
|
||||
};
|
||||
|
||||
private onSelectionChange = (jobs: Job[]) => {
|
||||
this.setState(current => ({ ...current, selectedJobs: jobs }));
|
||||
};
|
||||
|
||||
private removeRecord = (record: Job) => {
|
||||
const { jobs } = this.state;
|
||||
const filtered = jobs.filter(j => j.id !== record.id);
|
||||
this.setState(current => ({ ...current, jobs: filtered }));
|
||||
};
|
||||
|
||||
private renderDeleteButton = () => {
|
||||
const { selectedJobs } = this.state;
|
||||
if (selectedJobs.length === 0) return null;
|
||||
|
||||
const performDelete = async () => {
|
||||
for (const record of selectedJobs) {
|
||||
try {
|
||||
await this.props.apiClient.deleteReport(record.id);
|
||||
this.removeRecord(record);
|
||||
this.props.toasts.addSuccess(
|
||||
this.props.intl.formatMessage(
|
||||
{
|
||||
id: 'xpack.reporting.listing.table.deleteConfim',
|
||||
defaultMessage: `The {reportTitle} report was deleted`,
|
||||
},
|
||||
{ reportTitle: record.object_title }
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
this.props.toasts.addDanger(
|
||||
this.props.intl.formatMessage(
|
||||
{
|
||||
id: 'xpack.reporting.listing.table.deleteFailedErrorMessage',
|
||||
defaultMessage: `The report was not deleted: {error}`,
|
||||
},
|
||||
{ error }
|
||||
)
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ReportDeleteButton
|
||||
jobsToDelete={selectedJobs}
|
||||
performDelete={performDelete}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 this.props.apiClient.list(this.state.page);
|
||||
total = await this.props.apiClient.total();
|
||||
this.isInitialJobsFetch = false;
|
||||
} catch (fetchError) {
|
||||
if (!this.licenseAllowsToShowThisPage()) {
|
||||
this.props.toasts.addDanger(this.state.badLicenseMessage);
|
||||
this.props.redirect('kibana#/management');
|
||||
return;
|
||||
}
|
||||
|
||||
if (fetchError.message === 'Failed to fetch') {
|
||||
this.props.toasts.addDanger(
|
||||
fetchError.message ||
|
||||
this.props.intl.formatMessage({
|
||||
id: 'xpack.reporting.listing.table.requestFailedErrorMessage',
|
||||
defaultMessage: '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): Job => {
|
||||
const { _source: source } = job;
|
||||
return {
|
||||
id: job._id,
|
||||
type: source.jobtype,
|
||||
object_type: source.payload.objectType,
|
||||
object_title: source.payload.title,
|
||||
created_by: source.created_by,
|
||||
created_at: source.created_at,
|
||||
started_at: source.started_at,
|
||||
completed_at: source.completed_at,
|
||||
status: source.status,
|
||||
statusLabel: jobStatusLabelsMap.get(source.status as JobStatuses) || source.status,
|
||||
max_size_reached: source.output ? source.output.max_size_reached : false,
|
||||
attempts: source.attempts,
|
||||
max_attempts: source.max_attempts,
|
||||
csv_contains_formulas: get(source, 'output.csv_contains_formulas'),
|
||||
warnings: source.output ? source.output.warnings : undefined,
|
||||
};
|
||||
}
|
||||
),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
private licenseAllowsToShowThisPage = () => {
|
||||
return this.state.showLinks && this.state.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;
|
||||
}
|
||||
}
|
||||
|
||||
private renderTable() {
|
||||
const { intl } = this.props;
|
||||
|
||||
|
@ -317,9 +453,9 @@ class ReportListingUi extends Component<Props, State> {
|
|||
render: (record: Job) => {
|
||||
return (
|
||||
<div>
|
||||
{this.renderDownloadButton(record)}
|
||||
{this.renderReportErrorButton(record)}
|
||||
{this.renderInfoButton(record)}
|
||||
<ReportDownloadButton {...this.props} record={record} />
|
||||
<ReportErrorButton {...this.props} record={record} />
|
||||
<ReportInfoButton {...this.props} jobId={record.id} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
@ -335,13 +471,22 @@ class ReportListingUi extends Component<Props, State> {
|
|||
hidePerPageOptions: true,
|
||||
};
|
||||
|
||||
const selection = {
|
||||
itemId: 'id',
|
||||
onSelectionChange: this.onSelectionChange,
|
||||
};
|
||||
|
||||
const search = {
|
||||
toolsRight: this.renderDeleteButton(),
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiBasicTable
|
||||
itemId={'id'}
|
||||
<EuiInMemoryTable
|
||||
itemId="id"
|
||||
items={this.state.jobs}
|
||||
loading={this.state.isLoading}
|
||||
columns={tableColumns}
|
||||
noItemsMessage={
|
||||
message={
|
||||
this.state.isLoading
|
||||
? intl.formatMessage({
|
||||
id: 'xpack.reporting.listing.table.loadingReportsDescription',
|
||||
|
@ -353,154 +498,14 @@ class ReportListingUi extends Component<Props, State> {
|
|||
})
|
||||
}
|
||||
pagination={pagination}
|
||||
selection={selection}
|
||||
search={search}
|
||||
isSelectable={true}
|
||||
onChange={this.onTableChange}
|
||||
data-test-subj="reportJobListing"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private renderDownloadButton = (record: Job) => {
|
||||
if (record.status !== JobStatuses.COMPLETED) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { intl } = this.props;
|
||||
const button = (
|
||||
<EuiButtonIcon
|
||||
onClick={() => this.props.apiClient.downloadReport(record.id)}
|
||||
iconType="importAction"
|
||||
aria-label={intl.formatMessage({
|
||||
id: 'xpack.reporting.listing.table.downloadReportAriaLabel',
|
||||
defaultMessage: 'Download report',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
if (record.csv_contains_formulas) {
|
||||
return (
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={intl.formatMessage({
|
||||
id: 'xpack.reporting.listing.table.csvContainsFormulas',
|
||||
defaultMessage:
|
||||
'Your CSV contains characters which spreadsheet applications can interpret as formulas.',
|
||||
})}
|
||||
>
|
||||
{button}
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
|
||||
if (record.max_size_reached) {
|
||||
return (
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={intl.formatMessage({
|
||||
id: 'xpack.reporting.listing.table.maxSizeReachedTooltip',
|
||||
defaultMessage: 'Max size reached, contains partial data.',
|
||||
})}
|
||||
>
|
||||
{button}
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
|
||||
return button;
|
||||
};
|
||||
|
||||
private renderReportErrorButton = (record: Job) => {
|
||||
if (record.status !== JobStatuses.FAILED) {
|
||||
return;
|
||||
}
|
||||
|
||||
return <ReportErrorButton apiClient={this.props.apiClient} jobId={record.id} />;
|
||||
};
|
||||
|
||||
private renderInfoButton = (record: Job) => {
|
||||
return <ReportInfoButton apiClient={this.props.apiClient} 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 this.props.apiClient.list(this.state.page);
|
||||
total = await this.props.apiClient.total();
|
||||
this.isInitialJobsFetch = false;
|
||||
} catch (fetchError) {
|
||||
if (!this.licenseAllowsToShowThisPage()) {
|
||||
this.props.toasts.addDanger(this.state.badLicenseMessage);
|
||||
this.props.redirect('kibana#/management');
|
||||
return;
|
||||
}
|
||||
|
||||
if (fetchError.message === 'Failed to fetch') {
|
||||
this.props.toasts.addDanger(
|
||||
fetchError.message ||
|
||||
this.props.intl.formatMessage({
|
||||
id: 'xpack.reporting.listing.table.requestFailedErrorMessage',
|
||||
defaultMessage: '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): Job => {
|
||||
const { _source: source } = job;
|
||||
return {
|
||||
id: job._id,
|
||||
type: source.jobtype,
|
||||
object_type: source.payload.objectType,
|
||||
object_title: source.payload.title,
|
||||
created_by: source.created_by,
|
||||
created_at: source.created_at,
|
||||
started_at: source.started_at,
|
||||
completed_at: source.completed_at,
|
||||
status: source.status,
|
||||
statusLabel: jobStatusLabelsMap.get(source.status as JobStatuses) || source.status,
|
||||
max_size_reached: source.output ? source.output.max_size_reached : false,
|
||||
attempts: source.attempts,
|
||||
max_attempts: source.max_attempts,
|
||||
csv_contains_formulas: get(source, 'output.csv_contains_formulas'),
|
||||
warnings: source.output ? source.output.warnings : undefined,
|
||||
};
|
||||
}
|
||||
),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
private licenseAllowsToShowThisPage = () => {
|
||||
return this.state.showLinks && this.state.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const ReportListing = injectI18n(ReportListingUi);
|
||||
|
|
|
@ -85,6 +85,12 @@ export class ReportingAPIClient {
|
|||
window.open(location);
|
||||
}
|
||||
|
||||
public async deleteReport(jobId: string) {
|
||||
return await this.http.delete(`${API_LIST_URL}/delete/${jobId}`, {
|
||||
asSystemRequest: true,
|
||||
});
|
||||
}
|
||||
|
||||
public list = (page = 0, jobIds: string[] = []): Promise<JobQueueEntry[]> => {
|
||||
const query = { page } as any;
|
||||
if (jobIds.length > 0) {
|
||||
|
|
Loading…
Reference in a new issue