rate.y))),
+ legendClickDisabled: true,
+ title: i18n.translate('xpack.apm.errorRateChart.avgLabel', {
+ defaultMessage: 'Avg.',
+ }),
+ type: 'linemark',
+ hideTooltipValue: true,
+ },
+ {
+ data: errorRates,
+ type: 'line',
+ color: theme.euiColorVis7,
+ hideLegend: true,
+ title: i18n.translate('xpack.apm.errorRateChart.rateLabel', {
+ defaultMessage: 'Rate',
+ }),
+ },
+ ]}
+ onHover={combinedOnHover}
+ tickFormatY={tickFormatY}
+ formatTooltipValue={({ y }: { y?: number }) =>
+ Number.isFinite(y) ? tickFormatY(y) : 'N/A'
+ }
+ height={unit * 10}
+ />
+ >
+ );
+};
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap
index 1f935af7c899..a31b9735628a 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap
+++ b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap
@@ -114,7 +114,7 @@ exports[`Histogram Initially should have default markup 1`] = `
x1={0}
x2={0}
y1={-0}
- y2={10}
+ y2={0}
/>
0 ms
@@ -149,7 +149,7 @@ exports[`Histogram Initially should have default markup 1`] = `
x1={0}
x2={0}
y1={-0}
- y2={10}
+ y2={0}
/>
500 ms
@@ -184,7 +184,7 @@ exports[`Histogram Initially should have default markup 1`] = `
x1={0}
x2={0}
y1={-0}
- y2={10}
+ y2={0}
/>
1,000 ms
@@ -219,7 +219,7 @@ exports[`Histogram Initially should have default markup 1`] = `
x1={0}
x2={0}
y1={-0}
- y2={10}
+ y2={0}
/>
1,500 ms
@@ -254,7 +254,7 @@ exports[`Histogram Initially should have default markup 1`] = `
x1={0}
x2={0}
y1={-0}
- y2={10}
+ y2={0}
/>
2,000 ms
@@ -289,7 +289,7 @@ exports[`Histogram Initially should have default markup 1`] = `
x1={0}
x2={0}
y1={-0}
- y2={10}
+ y2={0}
/>
2,500 ms
@@ -324,7 +324,7 @@ exports[`Histogram Initially should have default markup 1`] = `
x1={0}
x2={0}
y1={-0}
- y2={10}
+ y2={0}
/>
3,000 ms
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js
index 4eca1a37c51b..002ff19d0d1d 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js
+++ b/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js
@@ -26,6 +26,10 @@ import Tooltip from '../Tooltip';
import theme from '@elastic/eui/dist/eui_theme_light.json';
import { tint } from 'polished';
import { getTimeTicksTZ, getDomainTZ } from '../helper/timezone';
+import Legends from '../CustomPlot/Legends';
+import StatusText from '../CustomPlot/StatusText';
+import { i18n } from '@kbn/i18n';
+import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue';
const XY_HEIGHT = unit * 10;
const XY_MARGIN = {
@@ -99,6 +103,7 @@ export class HistogramInner extends PureComponent {
tooltipHeader,
verticalLineHover,
width: XY_WIDTH,
+ legends,
} = this.props;
const { hoveredBucket } = this.state;
if (isEmpty(buckets) || XY_WIDTH === 0) {
@@ -139,102 +144,140 @@ export class HistogramInner extends PureComponent {
const showVerticalLineHover = verticalLineHover(hoveredBucket);
const showBackgroundHover = backgroundHover(hoveredBucket);
+ const hasValidCoordinates = buckets.some((bucket) =>
+ isValidCoordinateValue(bucket.y)
+ );
+ const noHits = this.props.noHits || !hasValidCoordinates;
+
+ const xyPlotProps = {
+ dontCheckIfEmpty: true,
+ xType: this.props.xType,
+ width: XY_WIDTH,
+ height: XY_HEIGHT,
+ margin: XY_MARGIN,
+ xDomain: xDomain,
+ yDomain: yDomain,
+ };
+
+ const xAxisProps = {
+ style: { strokeWidth: '1px' },
+ marginRight: 10,
+ tickSize: 0,
+ tickTotal: X_TICK_TOTAL,
+ tickFormat: formatX,
+ tickValues: xTickValues,
+ };
+
+ const emptyStateChart = (
+
+
+
+
+ );
+
return (
-
-
-
-
-
- {showBackgroundHover && (
-
- )}
-
- {shouldShowTooltip && (
-
- )}
-
- {selectedBucket && (
-
- )}
-
-
-
- {showVerticalLineHover && (
-
- )}
-
- {
- return {
- ...bucket,
- xCenter: (bucket.x0 + bucket.x) / 2,
- };
- })}
- onClick={this.onClick}
- onHover={this.onHover}
- onBlur={this.onBlur}
- x={(d) => x(d.xCenter)}
- y={() => 1}
- />
-
+ {noHits ? (
+ <>{emptyStateChart}>
+ ) : (
+ <>
+
+
+
+
+
+ {showBackgroundHover && (
+
+ )}
+
+ {shouldShowTooltip && (
+
+ )}
+
+ {selectedBucket && (
+
+ )}
+
+
+
+ {showVerticalLineHover && hoveredBucket?.x && (
+
+ )}
+
+ {
+ return {
+ ...bucket,
+ xCenter: (bucket.x0 + bucket.x) / 2,
+ };
+ })}
+ onClick={this.onClick}
+ onHover={this.onHover}
+ onBlur={this.onBlur}
+ x={(d) => x(d.xCenter)}
+ y={() => 1}
+ />
+
+
+ {legends && (
+ {}}
+ truncateLegends={false}
+ noHits={noHits}
+ />
+ )}
+ >
+ )}
);
@@ -255,6 +298,8 @@ HistogramInner.propTypes = {
verticalLineHover: PropTypes.func,
width: PropTypes.number.isRequired,
xType: PropTypes.string,
+ legends: PropTypes.array,
+ noHits: PropTypes.bool,
};
HistogramInner.defaultProps = {
@@ -265,6 +310,7 @@ HistogramInner.defaultProps = {
tooltipHeader: () => null,
verticalLineHover: () => null,
xType: 'linear',
+ noHits: false,
};
export default makeWidthFlexible(HistogramInner);
diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts
new file mode 100644
index 000000000000..d558e3942a42
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts
@@ -0,0 +1,109 @@
+/*
+ * 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 {
+ ERROR_GROUP_ID,
+ PROCESSOR_EVENT,
+ SERVICE_NAME,
+} from '../../../common/elasticsearch_fieldnames';
+import { ProcessorEvent } from '../../../common/processor_event';
+import { getMetricsDateHistogramParams } from '../helpers/metrics';
+import { rangeFilter } from '../helpers/range_filter';
+import {
+ Setup,
+ SetupTimeRange,
+ SetupUIFilters,
+} from '../helpers/setup_request';
+
+export async function getErrorRate({
+ serviceName,
+ groupId,
+ setup,
+}: {
+ serviceName: string;
+ groupId?: string;
+ setup: Setup & SetupTimeRange & SetupUIFilters;
+}) {
+ const { start, end, uiFiltersES, client, indices } = setup;
+
+ const filter = [
+ { term: { [SERVICE_NAME]: serviceName } },
+ { range: rangeFilter(start, end) },
+ ...uiFiltersES,
+ ];
+
+ const aggs = {
+ response_times: {
+ date_histogram: getMetricsDateHistogramParams(start, end),
+ },
+ };
+
+ const getTransactionBucketAggregation = async () => {
+ const resp = await client.search({
+ index: indices['apm_oss.transactionIndices'],
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: [
+ ...filter,
+ { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } },
+ ],
+ },
+ },
+ aggs,
+ },
+ });
+ return {
+ totalHits: resp.hits.total.value,
+ responseTimeBuckets: resp.aggregations?.response_times.buckets,
+ };
+ };
+ const getErrorBucketAggregation = async () => {
+ const groupIdFilter = groupId
+ ? [{ term: { [ERROR_GROUP_ID]: groupId } }]
+ : [];
+ const resp = await client.search({
+ index: indices['apm_oss.errorIndices'],
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: [
+ ...filter,
+ ...groupIdFilter,
+ { term: { [PROCESSOR_EVENT]: ProcessorEvent.error } },
+ ],
+ },
+ },
+ aggs,
+ },
+ });
+ return resp.aggregations?.response_times.buckets;
+ };
+
+ const [transactions, errorResponseTimeBuckets] = await Promise.all([
+ getTransactionBucketAggregation(),
+ getErrorBucketAggregation(),
+ ]);
+
+ const transactionCountByTimestamp: Record = {};
+ if (transactions?.responseTimeBuckets) {
+ transactions.responseTimeBuckets.forEach((bucket) => {
+ transactionCountByTimestamp[bucket.key] = bucket.doc_count;
+ });
+ }
+
+ const errorRates = errorResponseTimeBuckets?.map((bucket) => {
+ const { key, doc_count: errorCount } = bucket;
+ const relativeRate = errorCount / transactionCountByTimestamp[key];
+ return { x: key, y: relativeRate };
+ });
+
+ return {
+ noHits: transactions?.totalHits === 0,
+ errorRates,
+ };
+}
diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts
index 774f1f27435a..bdfb49fa3082 100644
--- a/x-pack/plugins/apm/server/routes/create_apm_api.ts
+++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts
@@ -13,6 +13,7 @@ import {
errorDistributionRoute,
errorGroupsRoute,
errorsRoute,
+ errorRateRoute,
} from './errors';
import {
serviceAgentNameRoute,
@@ -81,6 +82,7 @@ const createApmApi = () => {
.add(errorDistributionRoute)
.add(errorGroupsRoute)
.add(errorsRoute)
+ .add(errorRateRoute)
// Services
.add(serviceAgentNameRoute)
diff --git a/x-pack/plugins/apm/server/routes/errors.ts b/x-pack/plugins/apm/server/routes/errors.ts
index 1615550027d3..97314a9a6166 100644
--- a/x-pack/plugins/apm/server/routes/errors.ts
+++ b/x-pack/plugins/apm/server/routes/errors.ts
@@ -11,6 +11,7 @@ import { getErrorGroup } from '../lib/errors/get_error_group';
import { getErrorGroups } from '../lib/errors/get_error_groups';
import { setupRequest } from '../lib/helpers/setup_request';
import { uiFiltersRt, rangeRt } from './default_api_types';
+import { getErrorRate } from '../lib/errors/get_error_rate';
export const errorsRoute = createRoute(() => ({
path: '/api/apm/services/{serviceName}/errors',
@@ -80,3 +81,26 @@ export const errorDistributionRoute = createRoute(() => ({
return getErrorDistribution({ serviceName, groupId, setup });
},
}));
+
+export const errorRateRoute = createRoute(() => ({
+ path: '/api/apm/services/{serviceName}/errors/rate',
+ params: {
+ path: t.type({
+ serviceName: t.string,
+ }),
+ query: t.intersection([
+ t.partial({
+ groupId: t.string,
+ }),
+ uiFiltersRt,
+ rangeRt,
+ ]),
+ },
+ handler: async ({ context, request }) => {
+ const setup = await setupRequest(context, request);
+ const { params } = context;
+ const { serviceName } = params.path;
+ const { groupId } = params.query;
+ return getErrorRate({ serviceName, groupId, setup });
+ },
+}));
diff --git a/x-pack/plugins/beats_management/server/index.ts b/x-pack/plugins/beats_management/server/index.ts
index 607fb0ab2725..ad19087f5ac9 100644
--- a/x-pack/plugins/beats_management/server/index.ts
+++ b/x-pack/plugins/beats_management/server/index.ts
@@ -4,7 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { PluginInitializer } from '../../../../src/core/server';
import { beatsManagementConfigSchema } from '../common';
+import { BeatsManagementPlugin } from './plugin';
export const config = {
schema: beatsManagementConfigSchema,
@@ -16,8 +18,4 @@ export const config = {
},
};
-export const plugin = () => ({
- setup() {},
- start() {},
- stop() {},
-});
+export const plugin: PluginInitializer<{}, {}> = (context) => new BeatsManagementPlugin(context);
diff --git a/x-pack/plugins/beats_management/server/plugin.ts b/x-pack/plugins/beats_management/server/plugin.ts
new file mode 100644
index 000000000000..a82dbcb4a3a6
--- /dev/null
+++ b/x-pack/plugins/beats_management/server/plugin.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ CoreSetup,
+ CoreStart,
+ Plugin,
+ PluginInitializerContext,
+} from '../../../../src/core/server';
+import { SecurityPluginSetup } from '../../security/server';
+import { LicensingPluginStart } from '../../licensing/server';
+import { BeatsManagementConfigType } from '../common';
+
+interface SetupDeps {
+ security?: SecurityPluginSetup;
+}
+
+interface StartDeps {
+ licensing: LicensingPluginStart;
+}
+
+export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDeps> {
+ constructor(
+ private readonly initializerContext: PluginInitializerContext
+ ) {}
+
+ public async setup(core: CoreSetup, plugins: SetupDeps) {
+ this.initializerContext.config.create();
+
+ return {};
+ }
+
+ public async start(core: CoreStart, { licensing }: StartDeps) {
+ return {};
+ }
+}
diff --git a/x-pack/legacy/plugins/beats_management/types/formsy.d.ts b/x-pack/plugins/beats_management/types/formsy.d.ts
similarity index 100%
rename from x-pack/legacy/plugins/beats_management/types/formsy.d.ts
rename to x-pack/plugins/beats_management/types/formsy.d.ts
diff --git a/x-pack/plugins/case/server/routes/api/utils.test.ts b/x-pack/plugins/case/server/routes/api/utils.test.ts
index 81156b98bab8..2da489e64343 100644
--- a/x-pack/plugins/case/server/routes/api/utils.test.ts
+++ b/x-pack/plugins/case/server/routes/api/utils.test.ts
@@ -222,7 +222,12 @@ describe('Utils', () => {
];
const res = transformCases(
- { saved_objects: mockCases, total: mockCases.length, per_page: 10, page: 1 },
+ {
+ saved_objects: mockCases.map((obj) => ({ ...obj, score: 1 })),
+ total: mockCases.length,
+ per_page: 10,
+ page: 1,
+ },
2,
2,
extraCaseData,
@@ -232,7 +237,11 @@ describe('Utils', () => {
page: 1,
per_page: 10,
total: mockCases.length,
- cases: flattenCaseSavedObjects(mockCases, extraCaseData, '123'),
+ cases: flattenCaseSavedObjects(
+ mockCases.map((obj) => ({ ...obj, score: 1 })),
+ extraCaseData,
+ '123'
+ ),
count_open_cases: 2,
count_closed_cases: 2,
});
@@ -500,7 +509,7 @@ describe('Utils', () => {
describe('transformComments', () => {
it('transforms correctly', () => {
const comments = {
- saved_objects: mockCaseComments,
+ saved_objects: mockCaseComments.map((obj) => ({ ...obj, score: 1 })),
total: mockCaseComments.length,
per_page: 10,
page: 1,
diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts
index b7f3c68d1662..ec2881807442 100644
--- a/x-pack/plugins/case/server/routes/api/utils.ts
+++ b/x-pack/plugins/case/server/routes/api/utils.ts
@@ -101,7 +101,7 @@ export const transformCases = (
});
export const flattenCaseSavedObjects = (
- savedObjects: SavedObjectsFindResponse['saved_objects'],
+ savedObjects: Array>,
totalCommentByCase: TotalCommentByCase[],
caseConfigureConnectorId: string = 'none'
): CaseResponse[] =>
@@ -146,7 +146,7 @@ export const transformComments = (
});
export const flattenCommentSavedObjects = (
- savedObjects: SavedObjectsFindResponse['saved_objects']
+ savedObjects: Array>
): CommentResponse[] =>
savedObjects.reduce((acc: CommentResponse[], savedObject: SavedObject) => {
return [...acc, flattenCommentSavedObject(savedObject)];
diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts
index 7098f611defa..ec5d81532e23 100644
--- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts
+++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts
@@ -676,12 +676,14 @@ describe('#find', () => {
id: 'some-id',
type: 'unknown-type',
attributes: { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' },
+ score: 1,
references: [],
},
{
id: 'some-id-2',
type: 'unknown-type',
attributes: { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' },
+ score: 1,
references: [],
},
],
@@ -722,6 +724,7 @@ describe('#find', () => {
attrNotSoSecret: 'not-so-secret',
attrThree: 'three',
},
+ score: 1,
references: [],
},
{
@@ -733,6 +736,7 @@ describe('#find', () => {
attrNotSoSecret: '*not-so-secret*',
attrThree: 'three',
},
+ score: 1,
references: [],
},
],
@@ -793,6 +797,7 @@ describe('#find', () => {
attrNotSoSecret: 'not-so-secret',
attrThree: 'three',
},
+ score: 1,
references: [],
},
{
@@ -804,6 +809,7 @@ describe('#find', () => {
attrNotSoSecret: '*not-so-secret*',
attrThree: 'three',
},
+ score: 1,
references: [],
},
],
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts
index fe66496f70dc..9928ce4807da 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts
@@ -549,6 +549,7 @@ export const getFindResultStatus = (): SavedObjectsFindResponse<
searchAfterTimeDurations: ['200.00'],
bulkCreateTimeDurations: ['800.43'],
},
+ score: 1,
references: [],
updated_at: '2020-02-18T15:26:51.333Z',
version: 'WzQ2LDFd',
@@ -570,6 +571,7 @@ export const getFindResultStatus = (): SavedObjectsFindResponse<
searchAfterTimeDurations: ['200.00'],
bulkCreateTimeDurations: ['800.43'],
},
+ score: 1,
references: [],
updated_at: '2020-02-18T15:15:58.860Z',
version: 'WzMyLDFd',
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts
index 6056e692854a..01ee41e3b877 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts
@@ -391,7 +391,7 @@ export const exampleFindRuleStatusResponse: (
total: 1,
per_page: 6,
page: 1,
- saved_objects: mockStatuses,
+ saved_objects: mockStatuses.map((obj) => ({ ...obj, score: 1 })),
});
export const mockLogger: Logger = loggingServiceMock.createLogger();
diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts
index 75cd501a1a9a..190429d2dacd 100644
--- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts
+++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts
@@ -138,7 +138,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
test(`passes options.type to baseClient if valid singular type specified`, async () => {
const { client, baseClient } = await createSpacesSavedObjectsClient();
const expectedReturnValue = {
- saved_objects: [createMockResponse()],
+ saved_objects: [createMockResponse()].map((obj) => ({ ...obj, score: 1 })),
total: 1,
per_page: 0,
page: 0,
@@ -158,7 +158,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
test(`supplements options with the current namespace`, async () => {
const { client, baseClient } = await createSpacesSavedObjectsClient();
const expectedReturnValue = {
- saved_objects: [createMockResponse()],
+ saved_objects: [createMockResponse()].map((obj) => ({ ...obj, score: 1 })),
total: 1,
per_page: 0,
page: 0,
diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js
index 37b22a687741..6cafa3eeef08 100644
--- a/x-pack/scripts/functional_tests.js
+++ b/x-pack/scripts/functional_tests.js
@@ -51,6 +51,7 @@ const onlyNotInCoverageTests = [
require.resolve('../test/licensing_plugin/config.legacy.ts'),
require.resolve('../test/endpoint_api_integration_no_ingest/config.ts'),
require.resolve('../test/reporting_api_integration/config.js'),
+ require.resolve('../test/functional_embedded/config.ts'),
];
require('@kbn/plugin-helpers').babelRegister();
diff --git a/x-pack/test/functional_embedded/config.firefox.ts b/x-pack/test/functional_embedded/config.firefox.ts
new file mode 100644
index 000000000000..2051d1afd4ab
--- /dev/null
+++ b/x-pack/test/functional_embedded/config.firefox.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
+
+export default async function ({ readConfigFile }: FtrConfigProviderContext) {
+ const chromeConfig = await readConfigFile(require.resolve('./config'));
+
+ return {
+ ...chromeConfig.getAll(),
+
+ browser: {
+ type: 'firefox',
+ acceptInsecureCerts: true,
+ },
+
+ suiteTags: {
+ exclude: ['skipFirefox'],
+ },
+
+ junit: {
+ reportName: 'Firefox Kibana Embedded in iframe with X-Pack Security',
+ },
+ };
+}
diff --git a/x-pack/test/functional_embedded/config.ts b/x-pack/test/functional_embedded/config.ts
new file mode 100644
index 000000000000..95b290ece7db
--- /dev/null
+++ b/x-pack/test/functional_embedded/config.ts
@@ -0,0 +1,67 @@
+/*
+ * 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 Fs from 'fs';
+import { resolve } from 'path';
+import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils';
+import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
+import { pageObjects } from '../functional/page_objects';
+
+export default async function ({ readConfigFile }: FtrConfigProviderContext) {
+ const kibanaFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js'));
+
+ const iframeEmbeddedPlugin = resolve(__dirname, './plugins/iframe_embedded');
+
+ const servers = {
+ ...kibanaFunctionalConfig.get('servers'),
+ elasticsearch: {
+ ...kibanaFunctionalConfig.get('servers.elasticsearch'),
+ },
+ kibana: {
+ ...kibanaFunctionalConfig.get('servers.kibana'),
+ protocol: 'https',
+ ssl: {
+ enabled: true,
+ key: Fs.readFileSync(KBN_KEY_PATH).toString('utf8'),
+ certificate: Fs.readFileSync(KBN_CERT_PATH).toString('utf8'),
+ certificateAuthorities: Fs.readFileSync(CA_CERT_PATH).toString('utf8'),
+ },
+ },
+ };
+
+ return {
+ testFiles: [require.resolve('./tests')],
+ servers,
+ services: kibanaFunctionalConfig.get('services'),
+ pageObjects,
+ browser: {
+ acceptInsecureCerts: true,
+ },
+ junit: {
+ reportName: 'Kibana Embedded in iframe with X-Pack Security',
+ },
+
+ esTestCluster: kibanaFunctionalConfig.get('esTestCluster'),
+ apps: {
+ ...kibanaFunctionalConfig.get('apps'),
+ },
+
+ kbnTestServer: {
+ ...kibanaFunctionalConfig.get('kbnTestServer'),
+ serverArgs: [
+ ...kibanaFunctionalConfig.get('kbnTestServer.serverArgs'),
+ `--plugin-path=${iframeEmbeddedPlugin}`,
+ '--server.ssl.enabled=true',
+ `--server.ssl.key=${KBN_KEY_PATH}`,
+ `--server.ssl.certificate=${KBN_CERT_PATH}`,
+ `--server.ssl.certificateAuthorities=${CA_CERT_PATH}`,
+
+ '--xpack.security.sameSiteCookies=None',
+ '--xpack.security.secureCookies=true',
+ ],
+ },
+ };
+}
diff --git a/x-pack/test/functional_embedded/ftr_provider_context.d.ts b/x-pack/test/functional_embedded/ftr_provider_context.d.ts
new file mode 100644
index 000000000000..5646c06a3cd3
--- /dev/null
+++ b/x-pack/test/functional_embedded/ftr_provider_context.d.ts
@@ -0,0 +1,12 @@
+/*
+ * 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 { GenericFtrProviderContext } from '@kbn/test/types/ftr';
+import { pageObjects } from '../functional/page_objects';
+import { services } from './services';
+
+export type FtrProviderContext = GenericFtrProviderContext;
+export { pageObjects };
diff --git a/x-pack/test/functional_embedded/plugins/iframe_embedded/kibana.json b/x-pack/test/functional_embedded/plugins/iframe_embedded/kibana.json
new file mode 100644
index 000000000000..ea9f55bd21c6
--- /dev/null
+++ b/x-pack/test/functional_embedded/plugins/iframe_embedded/kibana.json
@@ -0,0 +1,7 @@
+{
+ "id": "iframe_embedded",
+ "version": "1.0.0",
+ "kibanaVersion": "kibana",
+ "server": true,
+ "ui": false
+}
diff --git a/x-pack/test/functional_embedded/plugins/iframe_embedded/package.json b/x-pack/test/functional_embedded/plugins/iframe_embedded/package.json
new file mode 100644
index 000000000000..9fa1554e5312
--- /dev/null
+++ b/x-pack/test/functional_embedded/plugins/iframe_embedded/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "iframe_embedded",
+ "version": "0.0.0",
+ "kibana": {
+ "version": "kibana"
+ },
+ "scripts": {
+ "kbn": "node ../../../../../scripts/kbn.js",
+ "build": "rm -rf './target' && tsc"
+ },
+ "devDependencies": {
+ "typescript": "3.9.5"
+ }
+}
diff --git a/x-pack/test/functional_embedded/plugins/iframe_embedded/server/index.ts b/x-pack/test/functional_embedded/plugins/iframe_embedded/server/index.ts
new file mode 100644
index 000000000000..976ef19d4d8a
--- /dev/null
+++ b/x-pack/test/functional_embedded/plugins/iframe_embedded/server/index.ts
@@ -0,0 +1,11 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { PluginInitializerContext } from 'kibana/server';
+import { IframeEmbeddedPlugin } from './plugin';
+
+export const plugin = (initContext: PluginInitializerContext) =>
+ new IframeEmbeddedPlugin(initContext);
diff --git a/x-pack/test/functional_embedded/plugins/iframe_embedded/server/plugin.ts b/x-pack/test/functional_embedded/plugins/iframe_embedded/server/plugin.ts
new file mode 100644
index 000000000000..890fe14cf03c
--- /dev/null
+++ b/x-pack/test/functional_embedded/plugins/iframe_embedded/server/plugin.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 { Plugin, CoreSetup, PluginInitializerContext } from 'src/core/server';
+
+function renderBody(iframeUrl: string) {
+ return `
+
+
+
+
+ Kibana embedded in iframe
+
+
+
+
+
+`;
+}
+export class IframeEmbeddedPlugin implements Plugin {
+ constructor(initializerContext: PluginInitializerContext) {}
+
+ public setup(core: CoreSetup) {
+ core.http.resources.register(
+ {
+ path: '/iframe_embedded',
+ validate: false,
+ },
+ async (context, request, response) => {
+ const { protocol, port, host } = core.http.getServerInfo();
+
+ const kibanaUrl = Url.format({ protocol, hostname: host, port });
+
+ return response.renderHtml({
+ body: renderBody(kibanaUrl),
+ });
+ }
+ );
+ }
+ public start() {}
+ public stop() {}
+}
diff --git a/x-pack/test/functional_embedded/services.ts b/x-pack/test/functional_embedded/services.ts
new file mode 100644
index 000000000000..1bdf67abd89d
--- /dev/null
+++ b/x-pack/test/functional_embedded/services.ts
@@ -0,0 +1,9 @@
+/*
+ * 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 { services as functionalServices } from '../functional/services';
+
+export const services = functionalServices;
diff --git a/x-pack/test/functional_embedded/tests/iframe_embedded.ts b/x-pack/test/functional_embedded/tests/iframe_embedded.ts
new file mode 100644
index 000000000000..9b5c9894a940
--- /dev/null
+++ b/x-pack/test/functional_embedded/tests/iframe_embedded.ts
@@ -0,0 +1,42 @@
+/*
+ * 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 expect from '@kbn/expect';
+
+import { FtrProviderContext } from '../ftr_provider_context';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const PageObjects = getPageObjects(['security', 'common']);
+ const browser = getService('browser');
+ const config = getService('config');
+ const testSubjects = getService('testSubjects');
+
+ describe('in iframe', () => {
+ it('should open Kibana for logged-in user', async () => {
+ const isChromeHiddenBefore = await PageObjects.common.isChromeHidden();
+ expect(isChromeHiddenBefore).to.be(true);
+
+ await PageObjects.security.login();
+
+ const { protocol, hostname, port } = config.get('servers.kibana');
+
+ const url = Url.format({
+ protocol,
+ hostname,
+ port,
+ pathname: 'iframe_embedded',
+ });
+
+ await browser.navigateTo(url);
+
+ const iframe = await testSubjects.find('iframe_embedded');
+ await browser.switchToFrame(iframe);
+
+ const isChromeHidden = await PageObjects.common.isChromeHidden();
+ expect(isChromeHidden).to.be(false);
+ });
+ });
+}
diff --git a/x-pack/test/functional_embedded/tests/index.ts b/x-pack/test/functional_embedded/tests/index.ts
new file mode 100644
index 000000000000..87ac00b24243
--- /dev/null
+++ b/x-pack/test/functional_embedded/tests/index.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 { FtrProviderContext } from '../ftr_provider_context';
+
+export default function ({ loadTestFile }: FtrProviderContext) {
+ describe('Kibana embedded', function () {
+ this.tags('ciGroup2');
+ loadTestFile(require.resolve('./iframe_embedded'));
+ });
+}