mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Reporting/CSV] Make searches used for export inspectable (#171248)
## Summary Resolves https://github.com/elastic/kibana/issues/162366 ## Release note Added a troubleshooting enhancement for Kibana CSV export to allow users to inspect the queries used for collecting all of the data. ## Other changes: * Expose the reporting `csv.scroll` settings to the browser. * Lazy-load the report job info panel component. * Fix a few mixups of "setup" and "start" contracts. ## Screenshots <details> <summary>Option in Stack Management for CSV report jobs</summary>  </details> <details> <summary>Screencast</summary>a2fba0f4
-0ede-4d97-aad3-4b13351e24a3 </details> ## Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Sébastien Loix <sabee77@gmail.com> Co-authored-by: Sébastien Loix <sebastien.loix@elastic.co>
This commit is contained in:
parent
b1c7372083
commit
7795901fe7
21 changed files with 565 additions and 78 deletions
|
@ -51,3 +51,5 @@ export const CSV_SEARCHSOURCE_IMMEDIATE_TYPE = 'csv_searchsource_immediate';
|
|||
// but the extension points are still needed for pre-existing scripted automation, until 8.0
|
||||
export const CSV_REPORT_TYPE_DEPRECATED = 'CSV';
|
||||
export const CSV_JOB_TYPE_DEPRECATED = 'csv';
|
||||
|
||||
export { getQueryFromCsvJob, type QueryInspection } from './lib/get_query_from_job';
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../../..',
|
||||
roots: ['<rootDir>/packages/kbn-reporting/export_types/csv_common'],
|
||||
};
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { EsQuerySortValue, SearchSourceFields } from '@kbn/data-plugin/common';
|
||||
import { searchSourceCommonMock } from '@kbn/data-plugin/common/search/search_source/mocks';
|
||||
import { createStubDataView } from '@kbn/data-views-plugin/common/stubs';
|
||||
import { getQueryFromCsvJob } from './get_query_from_job';
|
||||
|
||||
describe('getQueryFromCsvJob', () => {
|
||||
it('returns QueryInspection data', async () => {
|
||||
const searchSourceStart = { ...searchSourceCommonMock };
|
||||
const originalCreate = searchSourceStart.create;
|
||||
searchSourceStart.create = jest.fn().mockImplementation(async () => {
|
||||
const original = await originalCreate();
|
||||
const originalGetField = original.getField;
|
||||
const getField = (fieldName: keyof SearchSourceFields) => {
|
||||
if (fieldName === 'index') {
|
||||
return createStubDataView({
|
||||
spec: {
|
||||
id: 'test-*',
|
||||
title: 'test-*',
|
||||
},
|
||||
});
|
||||
}
|
||||
return originalGetField(fieldName);
|
||||
};
|
||||
return {
|
||||
...original,
|
||||
getField,
|
||||
};
|
||||
});
|
||||
|
||||
const config = { scroll: { duration: '2m', size: 500 } };
|
||||
|
||||
const fromTime = '2019-06-20T00:00:00.000Z';
|
||||
const toTime = '2019-06-24T00:00:00.000Z';
|
||||
const serializedSearchSource = {
|
||||
version: true,
|
||||
query: { query: '', language: 'kuery' },
|
||||
index: '5193f870-d861-11e9-a311-0fa548c5f953',
|
||||
sort: [{ order_date: 'desc' }] as EsQuerySortValue[],
|
||||
fields: ['*'],
|
||||
filter: [],
|
||||
parent: {
|
||||
query: { language: 'kuery', query: '' },
|
||||
filter: [],
|
||||
parent: {
|
||||
filter: [
|
||||
{
|
||||
meta: { index: '5193f870-d861-11e9-a311-0fa548c5f953', params: {} },
|
||||
range: {
|
||||
order_date: {
|
||||
gte: fromTime,
|
||||
lte: toTime,
|
||||
format: 'strict_date_optional_time',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const searchSource = await searchSourceStart.create(serializedSearchSource);
|
||||
const query = getQueryFromCsvJob(searchSource, config);
|
||||
|
||||
// NOTE the mocked search source service is not returning assertable info
|
||||
expect(query).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"requestBody": undefined,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { estypes } from '@elastic/elasticsearch';
|
||||
import type { ISearchSource } from '@kbn/data-plugin/common';
|
||||
import { durationToNumber } from '@kbn/reporting-common';
|
||||
|
||||
/**
|
||||
* Type to wrap the untyped object returned when
|
||||
* getting the query from SearchSource service
|
||||
*/
|
||||
export interface QueryInspection {
|
||||
requestBody: estypes.SearchRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
interface CsvConfigType {
|
||||
scroll: {
|
||||
size: number;
|
||||
duration: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A utility to get the query from a CSV reporting job to inspect or analyze
|
||||
* @public
|
||||
*/
|
||||
export const getQueryFromCsvJob = (
|
||||
searchSource: ISearchSource,
|
||||
{ scroll: config }: CsvConfigType,
|
||||
pitId?: string
|
||||
): QueryInspection => {
|
||||
// Max number of documents in each returned page
|
||||
searchSource.setField('size', durationToNumber(config.size));
|
||||
|
||||
// Max time to wait for result
|
||||
searchSource.setField('timeout', config.duration);
|
||||
|
||||
// Request high accuracy for calculating total hits
|
||||
searchSource.setField('trackTotalHits', true);
|
||||
|
||||
if (pitId) {
|
||||
// Always use most recently provided PIT
|
||||
searchSource.setField('pit', {
|
||||
id: pitId,
|
||||
keep_alive: config.duration,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
requestBody: searchSource.getSearchRequestBody(),
|
||||
};
|
||||
};
|
|
@ -16,5 +16,6 @@
|
|||
"kbn_references": [
|
||||
"@kbn/data-plugin",
|
||||
"@kbn/reporting-common",
|
||||
"@kbn/data-views-plugin",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
export interface ClientConfigType {
|
||||
csv: { scroll: { duration: string; size: number } };
|
||||
poll: { jobsRefresh: { interval: number; intervalErrorMultiplier: number } };
|
||||
roles: { enabled: boolean };
|
||||
export_types: { pdf: { enabled: boolean }; png: { enabled: boolean }; csv: { enabled: boolean } };
|
||||
|
|
|
@ -32,6 +32,8 @@ type ReportPayload = ReportSource['payload'];
|
|||
* It can be instantiated with ReportApiJSON: the response data format for the report job APIs
|
||||
*/
|
||||
export class Job {
|
||||
public readonly payload: Omit<ReportPayload, 'headers'>;
|
||||
|
||||
public readonly id: JobId;
|
||||
public readonly index: string;
|
||||
|
||||
|
@ -73,6 +75,8 @@ export class Job {
|
|||
this.id = report.id;
|
||||
this.index = report.index;
|
||||
|
||||
this.payload = report.payload;
|
||||
|
||||
this.jobtype = report.jobtype;
|
||||
this.objectType = report.payload.objectType;
|
||||
this.title = report.payload.title;
|
||||
|
@ -105,6 +109,10 @@ export class Job {
|
|||
this.execution_time_ms = report.execution_time_ms;
|
||||
}
|
||||
|
||||
public isSearch() {
|
||||
return this.objectType === 'search';
|
||||
}
|
||||
|
||||
getStatusMessage() {
|
||||
const status = this.status;
|
||||
let smallMessage;
|
||||
|
|
|
@ -18,9 +18,12 @@ import {
|
|||
notificationServiceMock,
|
||||
coreMock,
|
||||
} from '@kbn/core/public/mocks';
|
||||
import type { LocatorPublic, SharePluginSetup } from '@kbn/share-plugin/public';
|
||||
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import type { LocatorPublic, SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public';
|
||||
import type { ILicense } from '@kbn/licensing-plugin/public';
|
||||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||
import { sharePluginMock } from '@kbn/share-plugin/public/mocks';
|
||||
|
||||
import { mockJobs } from '../../../common/test';
|
||||
|
||||
|
@ -43,9 +46,17 @@ export interface TestDependencies {
|
|||
ilmLocator: LocatorPublic<SerializableRecord>;
|
||||
uiSettings: ReturnType<typeof coreMock.createSetup>['uiSettings'];
|
||||
reportDiagnostic: typeof ReportDiagnostic;
|
||||
data: DataPublicPluginStart;
|
||||
share: SharePluginStart;
|
||||
}
|
||||
|
||||
export const mockConfig = {
|
||||
csv: {
|
||||
scroll: {
|
||||
duration: '10m',
|
||||
size: 500,
|
||||
},
|
||||
},
|
||||
poll: {
|
||||
jobCompletionNotifier: {
|
||||
interval: 5000,
|
||||
|
@ -95,9 +106,11 @@ export const createTestBed = registerTestBed(
|
|||
urlService,
|
||||
toasts,
|
||||
uiSettings,
|
||||
data,
|
||||
share,
|
||||
...rest
|
||||
}: Partial<Props> & TestDependencies) => (
|
||||
<KibanaContextProvider services={{ http, application, uiSettings }}>
|
||||
<KibanaContextProvider services={{ http, application, uiSettings, data, share }}>
|
||||
<InternalApiClientProvider apiClient={reportingAPIClient}>
|
||||
<IlmPolicyStatusContextProvider>
|
||||
<ReportListing
|
||||
|
@ -152,6 +165,8 @@ export const setup = async (props?: Partial<Props>) => {
|
|||
},
|
||||
} as unknown as SharePluginSetup['url'],
|
||||
reportDiagnostic,
|
||||
data: dataPluginMock.createStartContract(),
|
||||
share: sharePluginMock.createStartContract(),
|
||||
};
|
||||
|
||||
const testBed = createTestBed({ ...testDependencies, ...props });
|
||||
|
|
|
@ -0,0 +1,203 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { compressToEncodedURIComponent } from 'lz-string';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { EuiContextMenuItem } from '@elastic/eui';
|
||||
import type { ISearchStartSearchSource } from '@kbn/data-plugin/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import {
|
||||
getQueryFromCsvJob,
|
||||
QueryInspection,
|
||||
TaskPayloadCSV,
|
||||
} from '@kbn/reporting-export-types-csv-common';
|
||||
import type { ClientConfigType } from '@kbn/reporting-public';
|
||||
import type { LocatorClient } from '@kbn/share-plugin/common/url_service';
|
||||
|
||||
import type { Job } from '../../lib/job';
|
||||
import type { KibanaContext } from '../../types';
|
||||
|
||||
interface PropsUI {
|
||||
job: Job;
|
||||
csvConfig: ClientConfigType['csv'];
|
||||
searchSourceStart: ISearchStartSearchSource;
|
||||
locators: LocatorClient;
|
||||
}
|
||||
|
||||
const InspectInConsoleButtonUi: React.FC<PropsUI> = (props) => {
|
||||
const { csvConfig, job, searchSourceStart, locators } = props;
|
||||
|
||||
const { title: jobTitle } = job;
|
||||
const serializedSearchSource = (job.payload as TaskPayloadCSV).searchSource;
|
||||
|
||||
const handleDevToolsLinkClick = useCallback(async () => {
|
||||
const searchSource = await searchSourceStart.create(serializedSearchSource);
|
||||
const index = searchSource.getField('index');
|
||||
if (!index) {
|
||||
throw new Error(`The search must have a reference to an index pattern!`);
|
||||
}
|
||||
const indexPatternTitle = index.getIndexPattern();
|
||||
const examplePitId = i18n.translate(
|
||||
'xpack.reporting.reportInfoFlyout.devToolsContent.examplePitId',
|
||||
{
|
||||
defaultMessage: `[ID returned from first request]`,
|
||||
description: `This gets used in place of an ID string that is sent in a request body.`,
|
||||
}
|
||||
);
|
||||
const queryInfo = getQueryFromCsvJob(searchSource, csvConfig, examplePitId);
|
||||
const queryUri = compressToEncodedURIComponent(
|
||||
getTextForConsole(jobTitle, indexPatternTitle, queryInfo, csvConfig)
|
||||
);
|
||||
const consoleLocator = locators.get('CONSOLE_APP_LOCATOR');
|
||||
consoleLocator?.navigate({
|
||||
loadFrom: `data:text/plain,${queryUri}`,
|
||||
});
|
||||
}, [searchSourceStart, serializedSearchSource, jobTitle, csvConfig, locators]);
|
||||
|
||||
return (
|
||||
<EuiContextMenuItem
|
||||
data-test-subj="reportInfoFlyoutOpenInConsoleButton"
|
||||
key="download"
|
||||
icon="wrench"
|
||||
onClick={handleDevToolsLinkClick}
|
||||
>
|
||||
{i18n.translate('xpack.reporting.reportInfoFlyout.openInConsole', {
|
||||
defaultMessage: 'Inspect query in Console',
|
||||
description: 'An option in a menu of actions.',
|
||||
})}
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
job: Job;
|
||||
config: ClientConfigType;
|
||||
}
|
||||
|
||||
export const InspectInConsoleButton: React.FC<Props> = (props) => {
|
||||
const { config, job } = props;
|
||||
const { services } = useKibana<KibanaContext>();
|
||||
const { application, data, share } = services;
|
||||
const { capabilities } = application;
|
||||
const { locators } = share.url;
|
||||
|
||||
// To show the Console button,
|
||||
// check if job object type is search,
|
||||
// and if both the Dev Tools UI and the Console UI are enabled.
|
||||
const canShowDevTools = job.objectType === 'search' && capabilities.dev_tools.show;
|
||||
if (!canShowDevTools) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<InspectInConsoleButtonUi
|
||||
searchSourceStart={data.search.searchSource}
|
||||
locators={locators}
|
||||
job={job}
|
||||
csvConfig={config.csv}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const getTextForConsole = (
|
||||
jobTitle: string,
|
||||
indexPattern: string,
|
||||
queryInfo: QueryInspection,
|
||||
csvConfig: ClientConfigType['csv']
|
||||
) => {
|
||||
const { requestBody } = queryInfo;
|
||||
const queryAsString = JSON.stringify(requestBody, null, ' ');
|
||||
|
||||
const pitRequest =
|
||||
`POST /${indexPattern}/_pit?keep_alive=${csvConfig.scroll.duration}` +
|
||||
`&ignore_unavailable=true`;
|
||||
const queryRequest = `POST /_search`;
|
||||
const closePitRequest = `DELETE /_pit\n${JSON.stringify({
|
||||
id: `[ID returned from latest request]`,
|
||||
})}`;
|
||||
|
||||
const introText = i18n.translate(
|
||||
// intro to the content
|
||||
'xpack.reporting.reportInfoFlyout.devToolsContent.introText',
|
||||
{
|
||||
description: `Script used in the Console app`,
|
||||
defaultMessage: `
|
||||
# Report title: {jobTitle}
|
||||
# These are the queries used when exporting data for
|
||||
# the CSV report.
|
||||
#
|
||||
# For reference about the Elasticsearch Point-In-Time
|
||||
# API, see
|
||||
# https://www.elastic.co/guide/en/elasticsearch/reference/current/point-in-time-api.html
|
||||
|
||||
# The first query opens a Point-In-Time (PIT) context
|
||||
# and receive back the ID reference. The "keep_alive"
|
||||
# value is taken from the
|
||||
# "xpack.reporting.csv.scroll.duration" setting.
|
||||
#
|
||||
# The response will include an "id" value, which is
|
||||
# needed for the second query.
|
||||
{pitRequest}`,
|
||||
values: {
|
||||
jobTitle,
|
||||
pitRequest,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const queryText = i18n.translate(
|
||||
// query with the request path and body
|
||||
'xpack.reporting.reportInfoFlyout.devToolsContent.queryText',
|
||||
{
|
||||
description: `Script used in the Console app`,
|
||||
defaultMessage: `
|
||||
# The second query executes a search using the PIT ID.
|
||||
# The "keep_alive" and "size" values come from the
|
||||
# "xpack.reporting.csv.scroll.duration" and
|
||||
# "xpack.reporting.csv.scroll.size" settings in
|
||||
# kibana.yml.
|
||||
#
|
||||
# The reponse will include new a PIT ID, which might
|
||||
# not be the same as the ID returned from the first
|
||||
# query.
|
||||
{queryRequest}
|
||||
{queryAsString}`,
|
||||
values: { queryRequest, queryAsString },
|
||||
}
|
||||
);
|
||||
|
||||
const pagingText = i18n.translate(
|
||||
// info about querying further pages, and link to documentation
|
||||
'xpack.reporting.reportInfoFlyout.devToolsContent.pagingText',
|
||||
{
|
||||
description: `Script used in the Console app`,
|
||||
defaultMessage: `# The first request retrieves the first page of search
|
||||
# results. If you want to retrieve more hits, use PIT
|
||||
# with search_after. Always use the PIT ID from the
|
||||
# latest search response. See
|
||||
# https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html`,
|
||||
}
|
||||
);
|
||||
|
||||
const closingText = i18n.translate(
|
||||
// reminder to close the point-in-time context
|
||||
'xpack.reporting.reportInfoFlyout.devToolsContent.closingText',
|
||||
{
|
||||
description: `Script used in the Console app`,
|
||||
defaultMessage: `
|
||||
# Finally, release the resources held in Elasticsearch
|
||||
# memory by clearing the PIT.
|
||||
{closePitRequest}
|
||||
`,
|
||||
values: { closePitRequest },
|
||||
}
|
||||
);
|
||||
|
||||
return (introText + queryText + pagingText + closingText).trim();
|
||||
};
|
|
@ -5,48 +5,50 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FunctionComponent, useState, useEffect } from 'react';
|
||||
import React, { FunctionComponent, useEffect, useState } from 'react';
|
||||
import useMountedState from 'react-use/lib/useMountedState';
|
||||
|
||||
import {
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutHeader,
|
||||
EuiFlyoutFooter,
|
||||
EuiPortal,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
EuiTitle,
|
||||
EuiLoadingSpinner,
|
||||
EuiPopover,
|
||||
EuiButtonEmpty,
|
||||
EuiContextMenuItem,
|
||||
EuiContextMenuPanel,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlyoutHeader,
|
||||
EuiLoadingSpinner,
|
||||
EuiPopover,
|
||||
EuiPortal,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { ClientConfigType } from '@kbn/reporting-public';
|
||||
|
||||
import { Job } from '../../lib/job';
|
||||
import type { Job } from '../../lib/job';
|
||||
import { useInternalApiClient } from '../../lib/reporting_api_client';
|
||||
|
||||
import { InspectInConsoleButton } from './inspect_in_console_button';
|
||||
import { ReportInfoFlyoutContent } from './report_info_flyout_content';
|
||||
|
||||
interface Props {
|
||||
config: ClientConfigType;
|
||||
onClose: () => void;
|
||||
job: Job;
|
||||
}
|
||||
|
||||
export const ReportInfoFlyout: FunctionComponent<Props> = ({ onClose, job }) => {
|
||||
export const ReportInfoFlyout: FunctionComponent<Props> = ({ config, onClose, job }) => {
|
||||
const isMounted = useMountedState();
|
||||
const { apiClient } = useInternalApiClient();
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [loadingError, setLoadingError] = useState<undefined | Error>();
|
||||
const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState<boolean>(false);
|
||||
const [info, setInfo] = useState<undefined | Job>();
|
||||
const isMounted = useMountedState();
|
||||
const { apiClient } = useInternalApiClient();
|
||||
|
||||
const closePopover = () => setIsActionsPopoverOpen(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async function loadInfo() {
|
||||
async function loadInfo() {
|
||||
if (isLoading) {
|
||||
try {
|
||||
const infoResponse = await apiClient.getInfo(job.id);
|
||||
|
@ -63,7 +65,8 @@ export const ReportInfoFlyout: FunctionComponent<Props> = ({ onClose, job }) =>
|
|||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
loadInfo();
|
||||
}, [isLoading, apiClient, job.id, isMounted]);
|
||||
|
||||
const actionsButton = (
|
||||
|
@ -92,6 +95,7 @@ export const ReportInfoFlyout: FunctionComponent<Props> = ({ onClose, job }) =>
|
|||
defaultMessage: 'Download',
|
||||
})}
|
||||
</EuiContextMenuItem>,
|
||||
<InspectInConsoleButton job={job} config={config} />,
|
||||
<EuiContextMenuItem
|
||||
data-test-subj="reportInfoFlyoutOpenInKibanaButton"
|
||||
disabled={!job.canLinkToKibanaApp}
|
||||
|
@ -106,7 +110,9 @@ export const ReportInfoFlyout: FunctionComponent<Props> = ({ onClose, job }) =>
|
|||
defaultMessage: 'Open in Kibana',
|
||||
})}
|
||||
</EuiContextMenuItem>,
|
||||
];
|
||||
].filter(Boolean);
|
||||
|
||||
const closePopover = () => setIsActionsPopoverOpen(false);
|
||||
|
||||
return (
|
||||
<EuiPortal>
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ApplicationStart, ToastsSetup } from '@kbn/core/public';
|
||||
import type { ApplicationStart, ToastsStart } from '@kbn/core/public';
|
||||
import type { LicensingPluginStart } from '@kbn/licensing-plugin/public';
|
||||
import type { ClientConfigType } from '@kbn/reporting-public';
|
||||
import type { SharePluginStart } from '@kbn/share-plugin/public';
|
||||
import type { ReportingAPIClient } from '../lib/reporting_api_client';
|
||||
import type { SharePluginSetup } from '../shared_imports';
|
||||
|
||||
export interface ListingProps {
|
||||
apiClient: ReportingAPIClient;
|
||||
|
@ -17,8 +17,8 @@ export interface ListingProps {
|
|||
config: ClientConfigType;
|
||||
redirect: ApplicationStart['navigateToApp'];
|
||||
navigateToUrl: ApplicationStart['navigateToUrl'];
|
||||
toasts: ToastsSetup;
|
||||
urlService: SharePluginSetup['url'];
|
||||
toasts: ToastsStart;
|
||||
urlService: SharePluginStart['url'];
|
||||
}
|
||||
|
||||
export type ListingPropsInternal = ListingProps & {
|
||||
|
|
|
@ -7,49 +7,53 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { CoreSetup, CoreStart } from '@kbn/core/public';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { ILicense } from '@kbn/licensing-plugin/public';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import type { LicensingPluginStart } from '@kbn/licensing-plugin/public';
|
||||
import type { ManagementAppMountParams } from '@kbn/management-plugin/public';
|
||||
import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme';
|
||||
import type { ClientConfigType } from '@kbn/reporting-public';
|
||||
import type { SharePluginStart } from '@kbn/share-plugin/public';
|
||||
import { ReportListing } from '.';
|
||||
import { InternalApiClientProvider, ReportingAPIClient } from '../lib/reporting_api_client';
|
||||
import type { ManagementAppMountParams, SharePluginSetup } from '../shared_imports';
|
||||
import { KibanaContextProvider } from '../shared_imports';
|
||||
import { PolicyStatusContextProvider } from '../lib/default_status_context';
|
||||
import { InternalApiClientProvider, type ReportingAPIClient } from '../lib/reporting_api_client';
|
||||
import type { KibanaContext } from '../types';
|
||||
|
||||
export async function mountManagementSection(
|
||||
coreSetup: CoreSetup,
|
||||
coreStart: CoreStart,
|
||||
license$: Observable<ILicense>,
|
||||
license$: LicensingPluginStart['license$'],
|
||||
dataService: DataPublicPluginStart,
|
||||
shareService: SharePluginStart,
|
||||
config: ClientConfigType,
|
||||
apiClient: ReportingAPIClient,
|
||||
urlService: SharePluginSetup['url'],
|
||||
params: ManagementAppMountParams
|
||||
) {
|
||||
const services: KibanaContext = {
|
||||
http: coreStart.http,
|
||||
application: coreStart.application,
|
||||
uiSettings: coreStart.uiSettings,
|
||||
docLinks: coreStart.docLinks,
|
||||
data: dataService,
|
||||
share: shareService,
|
||||
};
|
||||
|
||||
render(
|
||||
<KibanaThemeProvider theme={{ theme$: params.theme$ }}>
|
||||
<I18nProvider>
|
||||
<KibanaContextProvider
|
||||
services={{
|
||||
http: coreSetup.http,
|
||||
application: coreStart.application,
|
||||
uiSettings: coreStart.uiSettings,
|
||||
docLinks: coreStart.docLinks,
|
||||
}}
|
||||
>
|
||||
<KibanaContextProvider services={services}>
|
||||
<InternalApiClientProvider apiClient={apiClient}>
|
||||
<PolicyStatusContextProvider config={config}>
|
||||
<ReportListing
|
||||
apiClient={apiClient}
|
||||
toasts={coreSetup.notifications.toasts}
|
||||
toasts={coreStart.notifications.toasts}
|
||||
license$={license$}
|
||||
config={config}
|
||||
redirect={coreStart.application.navigateToApp}
|
||||
navigateToUrl={coreStart.application.navigateToUrl}
|
||||
urlService={urlService}
|
||||
urlService={shareService.url}
|
||||
/>
|
||||
</PolicyStatusContextProvider>
|
||||
</InternalApiClientProvider>
|
||||
|
|
|
@ -430,6 +430,7 @@ export class ReportListingTable extends Component<ListingPropsInternal, State> {
|
|||
/>
|
||||
{!!this.state.selectedJob && (
|
||||
<ReportInfoFlyout
|
||||
config={this.props.config}
|
||||
onClose={() => this.setState({ selectedJob: undefined })}
|
||||
job={this.state.selectedJob}
|
||||
/>
|
||||
|
|
|
@ -9,6 +9,7 @@ import * as Rx from 'rxjs';
|
|||
import { catchError, filter, map, mergeMap, takeUntil } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
AppNavLinkStatus,
|
||||
CoreSetup,
|
||||
CoreStart,
|
||||
HttpSetup,
|
||||
|
@ -22,13 +23,15 @@ import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
|||
import { CONTEXT_MENU_TRIGGER } from '@kbn/embeddable-plugin/public';
|
||||
import type { HomePublicPluginSetup, HomePublicPluginStart } from '@kbn/home-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { LicensingPluginStart } from '@kbn/licensing-plugin/public';
|
||||
import { ManagementSetup, ManagementStart } from '@kbn/management-plugin/public';
|
||||
import { ClientConfigType } from '@kbn/reporting-public';
|
||||
import type { LicensingPluginStart } from '@kbn/licensing-plugin/public';
|
||||
import type { ManagementSetup, ManagementStart } from '@kbn/management-plugin/public';
|
||||
import type { ScreenshotModePluginSetup } from '@kbn/screenshot-mode-plugin/public';
|
||||
|
||||
import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, durationToNumber } from '@kbn/reporting-common';
|
||||
import type { JobId } from '@kbn/reporting-common/types';
|
||||
import type { ClientConfigType } from '@kbn/reporting-public';
|
||||
import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public';
|
||||
import type { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
|
||||
import type { ReportingSetup, ReportingStart } from '.';
|
||||
import { ReportingAPIClient } from './lib/reporting_api_client';
|
||||
|
@ -38,14 +41,8 @@ import { ReportingCsvPanelAction } from './panel_actions/get_csv_panel_action';
|
|||
import { reportingCsvShareProvider } from './share_context_menu/register_csv_reporting';
|
||||
import { reportingScreenshotShareProvider } from './share_context_menu/register_pdf_png_reporting';
|
||||
import { getSharedComponents } from './shared';
|
||||
import type {
|
||||
SharePluginSetup,
|
||||
SharePluginStart,
|
||||
UiActionsSetup,
|
||||
UiActionsStart,
|
||||
} from './shared_imports';
|
||||
import { AppNavLinkStatus } from './shared_imports';
|
||||
import type { JobSummarySet } from './types';
|
||||
|
||||
function getStored(): JobId[] {
|
||||
const sessionValue = sessionStorage.getItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY);
|
||||
return sessionValue ? JSON.parse(sessionValue) : [];
|
||||
|
@ -146,14 +143,20 @@ export class ReportingPublicPlugin
|
|||
setupDeps: ReportingPublicPluginSetupDependencies
|
||||
) {
|
||||
const { getStartServices, uiSettings } = core;
|
||||
const { home, management, screenshotMode, share, uiActions } = setupDeps;
|
||||
const {
|
||||
home: homeSetup,
|
||||
management: managementSetup,
|
||||
screenshotMode: screenshotModeSetup,
|
||||
share: shareSetup,
|
||||
uiActions: uiActionsSetup,
|
||||
} = setupDeps;
|
||||
|
||||
const startServices$ = Rx.from(getStartServices());
|
||||
const usesUiCapabilities = !this.config.roles.enabled;
|
||||
|
||||
const apiClient = this.getApiClient(core.http, core.uiSettings);
|
||||
|
||||
home.featureCatalogue.register({
|
||||
homeSetup.featureCatalogue.register({
|
||||
id: 'reporting',
|
||||
title: i18n.translate('xpack.reporting.registerFeature.reportingTitle', {
|
||||
defaultMessage: 'Reporting',
|
||||
|
@ -167,28 +170,28 @@ export class ReportingPublicPlugin
|
|||
category: 'admin',
|
||||
});
|
||||
|
||||
management.sections.section.insightsAndAlerting.registerApp({
|
||||
managementSetup.sections.section.insightsAndAlerting.registerApp({
|
||||
id: 'reporting',
|
||||
title: this.title,
|
||||
order: 3,
|
||||
mount: async (params) => {
|
||||
params.setBreadcrumbs([{ text: this.breadcrumbText }]);
|
||||
const [[start, startDeps], { mountManagementSection }] = await Promise.all([
|
||||
const [[coreStart, startDeps], { mountManagementSection }] = await Promise.all([
|
||||
getStartServices(),
|
||||
import('./management/mount_management_section'),
|
||||
]);
|
||||
|
||||
const { docTitle } = start.chrome;
|
||||
const { licensing, data, share } = startDeps;
|
||||
const { docTitle } = coreStart.chrome;
|
||||
docTitle.change(this.title);
|
||||
|
||||
const { license$ } = startDeps.licensing;
|
||||
const umountAppCallback = await mountManagementSection(
|
||||
core,
|
||||
start,
|
||||
license$,
|
||||
coreStart,
|
||||
licensing.license$,
|
||||
data,
|
||||
share,
|
||||
this.config,
|
||||
apiClient,
|
||||
share.url,
|
||||
params
|
||||
);
|
||||
|
||||
|
@ -203,7 +206,12 @@ export class ReportingPublicPlugin
|
|||
id: 'reportingRedirect',
|
||||
mount: async (params) => {
|
||||
const { mountRedirectApp } = await import('./redirect');
|
||||
return mountRedirectApp({ ...params, apiClient, screenshotMode, share });
|
||||
return mountRedirectApp({
|
||||
...params,
|
||||
apiClient,
|
||||
screenshotMode: screenshotModeSetup,
|
||||
share: shareSetup,
|
||||
});
|
||||
},
|
||||
title: 'Reporting redirect app',
|
||||
searchable: false,
|
||||
|
@ -212,7 +220,7 @@ export class ReportingPublicPlugin
|
|||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
});
|
||||
|
||||
uiActions.addTriggerAction(
|
||||
uiActionsSetup.addTriggerAction(
|
||||
CONTEXT_MENU_TRIGGER,
|
||||
new ReportingCsvPanelAction({ core, apiClient, startServices$, usesUiCapabilities })
|
||||
);
|
||||
|
@ -222,7 +230,7 @@ export class ReportingPublicPlugin
|
|||
|
||||
startServices$.subscribe(([{ application }, { licensing }]) => {
|
||||
licensing.license$.subscribe((license) => {
|
||||
share.register(
|
||||
shareSetup.register(
|
||||
reportingCsvShareProvider({
|
||||
apiClient,
|
||||
toasts,
|
||||
|
@ -235,7 +243,7 @@ export class ReportingPublicPlugin
|
|||
);
|
||||
|
||||
if (this.config.export_types.pdf.enabled || this.config.export_types.png.enabled) {
|
||||
share.register(
|
||||
shareSetup.register(
|
||||
reportingScreenshotShareProvider({
|
||||
apiClient,
|
||||
toasts,
|
||||
|
|
|
@ -5,15 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import React from 'react';
|
||||
import { EuiErrorBoundary } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
|
||||
import type { AppMountParameters } from '@kbn/core/public';
|
||||
import type { ScreenshotModePluginSetup } from '@kbn/screenshot-mode-plugin/public';
|
||||
import type { SharePluginSetup } from '../shared_imports';
|
||||
import type { ReportingAPIClient } from '../lib/reporting_api_client';
|
||||
import type { SharePluginSetup } from '@kbn/share-plugin/public';
|
||||
|
||||
import type { ReportingAPIClient } from '../lib/reporting_api_client';
|
||||
import { RedirectApp } from './redirect_app';
|
||||
|
||||
interface MountParams extends AppMountParameters {
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
// FIXME: Reporting code does not consistently import from here. It would be
|
||||
// better to remove this file and do without indirect imports.
|
||||
export type { SharePluginSetup, SharePluginStart, LocatorPublic } from '@kbn/share-plugin/public';
|
||||
|
||||
export { AppNavLinkStatus } from '@kbn/core/public';
|
||||
|
|
|
@ -6,10 +6,15 @@
|
|||
*/
|
||||
|
||||
import type { CoreSetup, CoreStart } from '@kbn/core/public';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { JOB_STATUS } from '@kbn/reporting-common';
|
||||
import type { JobId, ReportOutput, ReportSource, TaskRunResult } from '@kbn/reporting-common/types';
|
||||
import type { SharePluginStart } from '@kbn/share-plugin/public';
|
||||
|
||||
/* Notifier Toasts */
|
||||
/*
|
||||
* Notifier Toasts
|
||||
* @internal
|
||||
*/
|
||||
export interface JobSummary {
|
||||
id: JobId;
|
||||
status: JOB_STATUS;
|
||||
|
@ -20,14 +25,23 @@ export interface JobSummary {
|
|||
csvContainsFormulas: TaskRunResult['csv_contains_formulas'];
|
||||
}
|
||||
|
||||
/*
|
||||
* Notifier Toasts
|
||||
* @internal
|
||||
*/
|
||||
export interface JobSummarySet {
|
||||
completed: JobSummary[];
|
||||
failed: JobSummary[];
|
||||
}
|
||||
|
||||
/* Services received through useKibana context
|
||||
* @internal
|
||||
*/
|
||||
export interface KibanaContext {
|
||||
http: CoreSetup['http'];
|
||||
application: CoreStart['application'];
|
||||
uiSettings: CoreStart['uiSettings'];
|
||||
docLinks: CoreStart['docLinks'];
|
||||
data: DataPublicPluginStart;
|
||||
share: SharePluginStart;
|
||||
}
|
||||
|
|
|
@ -12,7 +12,13 @@ import { i18n } from '@kbn/i18n';
|
|||
import { ConfigSchema, ReportingConfigType } from '@kbn/reporting-server';
|
||||
|
||||
export const config: PluginConfigDescriptor<ReportingConfigType> = {
|
||||
exposeToBrowser: { poll: true, roles: true, export_types: true, statefulSettings: true },
|
||||
exposeToBrowser: {
|
||||
csv: { scroll: true },
|
||||
poll: true,
|
||||
roles: true,
|
||||
export_types: true,
|
||||
statefulSettings: true,
|
||||
},
|
||||
schema: ConfigSchema,
|
||||
deprecations: ({ unused }) => [
|
||||
unused('capture.browser.chromium.maxScreenshotDimension', { level: 'warning' }), // unused since 7.8
|
||||
|
|
|
@ -10,21 +10,30 @@ import { REPORT_TABLE_ID } from '@kbn/reporting-plugin/common/constants';
|
|||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||
const pageObjects = getPageObjects(['common', 'reporting']);
|
||||
const pageObjects = getPageObjects(['common', 'reporting', 'settings', 'console']);
|
||||
const log = getService('log');
|
||||
const retry = getService('retry');
|
||||
const security = getService('security');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const find = getService('find');
|
||||
|
||||
describe('Listing of Reports', function () {
|
||||
const kbnArchive =
|
||||
'x-pack/test/functional/fixtures/kbn_archiver/reporting/view_in_console_index_pattern.json';
|
||||
|
||||
before(async () => {
|
||||
await security.testUser.setRoles([
|
||||
'kibana_admin', // to access stack management
|
||||
'reporting_user', // NOTE: the built-in role granting full reporting access is deprecated. See xpack.reporting.roles.enabled
|
||||
]);
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
await kibanaServer.importExport.load(kbnArchive);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await kibanaServer.importExport.unload(kbnArchive);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
|
@ -142,5 +151,31 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('Exposes an action to see the ES query in console', async () => {
|
||||
const csvReportTitle = 'Discover search [2021-07-19T11:44:48.670-07:00]';
|
||||
await pageObjects.reporting.openReportFlyout(csvReportTitle);
|
||||
|
||||
// Open "Actions" menu inside the flyout
|
||||
await testSubjects.click('reportInfoFlyoutActionsButton');
|
||||
|
||||
expect(await find.existsByCssSelector('.euiContextMenuPanel')).to.eql(true);
|
||||
const contextMenu = await find.byClassName('euiContextMenuPanel');
|
||||
const contextMenuText = await contextMenu.getVisibleText();
|
||||
|
||||
expect(contextMenuText).to.contain('Inspect query in Console');
|
||||
|
||||
await testSubjects.click('reportInfoFlyoutOpenInConsoleButton');
|
||||
|
||||
await pageObjects.common.waitUntilUrlIncludes('dev_tools#/console');
|
||||
|
||||
await retry.try(async () => {
|
||||
const actualRequest = await pageObjects.console.getRequest();
|
||||
expect(actualRequest.trim()).to.contain(
|
||||
'# These are the queries used when exporting data for\n# the CSV report'
|
||||
);
|
||||
expect(actualRequest).to.contain('POST /_search');
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"attributes": {
|
||||
"fieldAttrs": "{\"referer\":{\"customLabel\":\"Referer custom\"}}",
|
||||
"fields": "[]",
|
||||
"timeFieldName": "@timestamp",
|
||||
"title": "ff959d40-b880-11e8-a6d9-e546fe2bba5f"
|
||||
},
|
||||
"coreMigrationVersion": "8.4.0",
|
||||
"id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f",
|
||||
"migrationVersion": {
|
||||
"index-pattern": "8.0.0"
|
||||
},
|
||||
"references": [],
|
||||
"type": "index-pattern",
|
||||
"version": "WzQsMV0="
|
||||
}
|
|
@ -192,6 +192,19 @@ export class ReportingPageObject extends FtrService {
|
|||
);
|
||||
}
|
||||
|
||||
async openReportFlyout(reportTitle: string) {
|
||||
const table = await this.testSubjects.find(REPORT_TABLE_ID);
|
||||
const allRows = await table.findAllByTestSubject(REPORT_TABLE_ROW_ID);
|
||||
for (const row of allRows) {
|
||||
const titleColumn = await row.findByTestSubject('reportingListItemObjectTitle');
|
||||
const title = await titleColumn.getVisibleText();
|
||||
if (title === reportTitle) {
|
||||
titleColumn.click();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async writeSessionReport(name: string, reportExt: string, rawPdf: Buffer, folder: string) {
|
||||
const sessionDirectory = path.resolve(folder, 'session');
|
||||
await mkdirAsync(sessionDirectory, { recursive: true });
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue