diff --git a/src/plugins/vis_type_xy/public/components/xy_settings.tsx b/src/plugins/vis_type_xy/public/components/xy_settings.tsx
index 8922f512522a0..8d6a7eecdfe52 100644
--- a/src/plugins/vis_type_xy/public/components/xy_settings.tsx
+++ b/src/plugins/vis_type_xy/public/components/xy_settings.tsx
@@ -29,7 +29,6 @@ import { renderEndzoneTooltip } from '../../../charts/public';
import { getThemeService, getUISettings } from '../services';
import { VisConfig } from '../types';
-import { fillEmptyValue } from '../utils/get_series_name_fn';
declare global {
interface Window {
@@ -134,7 +133,7 @@ export const XYSettings: FC = ({
};
const headerValueFormatter: TickFormatter | undefined = xAxis.ticks?.formatter
- ? (value) => fillEmptyValue(xAxis.ticks?.formatter?.(value)) ?? ''
+ ? (value) => xAxis.ticks?.formatter?.(value) ?? ''
: undefined;
const headerFormatter =
isTimeChart && xDomain && adjustedXDomain
diff --git a/src/plugins/vis_type_xy/public/config/get_axis.ts b/src/plugins/vis_type_xy/public/config/get_axis.ts
index 08b17c882eea6..71d33cc20d057 100644
--- a/src/plugins/vis_type_xy/public/config/get_axis.ts
+++ b/src/plugins/vis_type_xy/public/config/get_axis.ts
@@ -27,7 +27,6 @@ import {
YScaleType,
SeriesParam,
} from '../types';
-import { fillEmptyValue } from '../utils/get_series_name_fn';
export function getAxis(
{ type, title: axisTitle, labels, scale: axisScale, ...axis }: CategoryAxis,
@@ -90,8 +89,7 @@ function getLabelFormatter(
}
return (value: any) => {
- const formattedStringValue = `${formatter ? formatter(value) : value}`;
- const finalValue = fillEmptyValue(formattedStringValue);
+ const finalValue = `${formatter ? formatter(value) : value}`;
if (finalValue.length > truncate) {
return `${finalValue.slice(0, truncate)}...`;
diff --git a/src/plugins/vis_type_xy/public/plugin.ts b/src/plugins/vis_type_xy/public/plugin.ts
index e8d53127765b4..b595d3172f143 100644
--- a/src/plugins/vis_type_xy/public/plugin.ts
+++ b/src/plugins/vis_type_xy/public/plugin.ts
@@ -23,7 +23,7 @@ import {
} from './services';
import { visTypesDefinitions } from './vis_types';
-import { LEGACY_CHARTS_LIBRARY } from '../../visualizations/common/constants';
+import { LEGACY_CHARTS_LIBRARY } from '../common/';
import { xyVisRenderer } from './vis_renderer';
import * as expressionFunctions from './expression_functions';
diff --git a/src/plugins/vis_type_xy/public/utils/get_series_name_fn.ts b/src/plugins/vis_type_xy/public/utils/get_series_name_fn.ts
index 0e54650e22f75..137f8a5558010 100644
--- a/src/plugins/vis_type_xy/public/utils/get_series_name_fn.ts
+++ b/src/plugins/vis_type_xy/public/utils/get_series_name_fn.ts
@@ -8,21 +8,10 @@
import { memoize } from 'lodash';
-import { i18n } from '@kbn/i18n';
import { XYChartSeriesIdentifier, SeriesName } from '@elastic/charts';
import { VisConfig } from '../types';
-const emptyTextLabel = i18n.translate('visTypeXy.emptyTextColumnValue', {
- defaultMessage: '(empty)',
-});
-
-/**
- * Returns empty values
- */
-export const fillEmptyValue = (value: T) =>
- value === '' ? emptyTextLabel : value;
-
function getSplitValues(
splitAccessors: XYChartSeriesIdentifier['splitAccessors'],
seriesAspects?: VisConfig['aspects']['series']
@@ -36,7 +25,7 @@ function getSplitValues(
const split = (seriesAspects ?? []).find(({ accessor }) => accessor === key);
splitValues.push(split?.formatter ? split?.formatter(value) : value);
});
- return splitValues.map(fillEmptyValue);
+ return splitValues;
}
export const getSeriesNameFn = (aspects: VisConfig['aspects'], multipleY = false) =>
diff --git a/src/plugins/vis_type_xy/server/index.ts b/src/plugins/vis_type_xy/server/index.ts
new file mode 100644
index 0000000000000..a27ac49c0ea49
--- /dev/null
+++ b/src/plugins/vis_type_xy/server/index.ts
@@ -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
+ * 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 { VisTypeXyServerPlugin } from './plugin';
+
+export const plugin = () => new VisTypeXyServerPlugin();
diff --git a/src/plugins/vis_type_xy/server/plugin.ts b/src/plugins/vis_type_xy/server/plugin.ts
new file mode 100644
index 0000000000000..46d6531204c24
--- /dev/null
+++ b/src/plugins/vis_type_xy/server/plugin.ts
@@ -0,0 +1,56 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import { schema } from '@kbn/config-schema';
+
+import { CoreSetup, Plugin, UiSettingsParams } from 'kibana/server';
+
+import { LEGACY_CHARTS_LIBRARY } from '../common';
+
+export const getUiSettingsConfig: () => Record> = () => ({
+ // TODO: Remove this when vis_type_vislib is removed
+ // https://github.com/elastic/kibana/issues/56143
+ [LEGACY_CHARTS_LIBRARY]: {
+ name: i18n.translate('visTypeXy.advancedSettings.visualization.legacyChartsLibrary.name', {
+ defaultMessage: 'XY axis legacy charts library',
+ }),
+ requiresPageReload: true,
+ value: false,
+ description: i18n.translate(
+ 'visTypeXy.advancedSettings.visualization.legacyChartsLibrary.description',
+ {
+ defaultMessage: 'Enables legacy charts library for area, line and bar charts in visualize.',
+ }
+ ),
+ deprecation: {
+ message: i18n.translate(
+ 'visTypeXy.advancedSettings.visualization.legacyChartsLibrary.deprecation',
+ {
+ defaultMessage:
+ 'The legacy charts library for area, line and bar charts in visualize is deprecated and will not be supported as of 7.16.',
+ }
+ ),
+ docLinksKey: 'visualizationSettings',
+ },
+ category: ['visualization'],
+ schema: schema.boolean(),
+ },
+});
+
+export class VisTypeXyServerPlugin implements Plugin {
+ public setup(core: CoreSetup) {
+ core.uiSettings.register(getUiSettingsConfig());
+
+ return {};
+ }
+
+ public start() {
+ return {};
+ }
+}
diff --git a/src/plugins/visualizations/common/constants.ts b/src/plugins/visualizations/common/constants.ts
index a33e74b498a2c..a8a0963ac8948 100644
--- a/src/plugins/visualizations/common/constants.ts
+++ b/src/plugins/visualizations/common/constants.ts
@@ -7,4 +7,3 @@
*/
export const VISUALIZE_ENABLE_LABS_SETTING = 'visualize:enableLabs';
-export const LEGACY_CHARTS_LIBRARY = 'visualization:visualize:legacyChartsLibrary';
diff --git a/src/plugins/visualizations/server/plugin.ts b/src/plugins/visualizations/server/plugin.ts
index 1fec63f2bb45a..5a5a80b2689d6 100644
--- a/src/plugins/visualizations/server/plugin.ts
+++ b/src/plugins/visualizations/server/plugin.ts
@@ -18,7 +18,7 @@ import {
Logger,
} from '../../../core/server';
-import { VISUALIZE_ENABLE_LABS_SETTING, LEGACY_CHARTS_LIBRARY } from '../common/constants';
+import { VISUALIZE_ENABLE_LABS_SETTING } from '../common/constants';
import { visualizationSavedObjectType } from './saved_objects';
@@ -58,27 +58,6 @@ export class VisualizationsPlugin
category: ['visualization'],
schema: schema.boolean(),
},
- // TODO: Remove this when vis_type_vislib is removed
- // https://github.com/elastic/kibana/issues/56143
- [LEGACY_CHARTS_LIBRARY]: {
- name: i18n.translate(
- 'visualizations.advancedSettings.visualization.legacyChartsLibrary.name',
- {
- defaultMessage: 'Legacy charts library',
- }
- ),
- requiresPageReload: true,
- value: false,
- description: i18n.translate(
- 'visualizations.advancedSettings.visualization.legacyChartsLibrary.description',
- {
- defaultMessage:
- 'Enables legacy charts library for area, line, bar, pie charts in visualize.',
- }
- ),
- category: ['visualization'],
- schema: schema.boolean(),
- },
});
if (plugins.usageCollection) {
diff --git a/test/api_integration/apis/ui_counters/ui_counters.ts b/test/api_integration/apis/ui_counters/ui_counters.ts
index 2be6ea4341fb0..019dcfd621655 100644
--- a/test/api_integration/apis/ui_counters/ui_counters.ts
+++ b/test/api_integration/apis/ui_counters/ui_counters.ts
@@ -56,7 +56,8 @@ export default function ({ getService }: FtrProviderContext) {
return savedObject;
};
- describe('UI Counters API', () => {
+ // FLAKY: https://github.com/elastic/kibana/issues/98240
+ describe.skip('UI Counters API', () => {
const dayDate = moment().format('DDMMYYYY');
before(async () => await esArchiver.emptyKibanaIndex());
diff --git a/test/functional/apps/dashboard/dashboard_state.ts b/test/functional/apps/dashboard/dashboard_state.ts
index 047681e1a8ace..6c259f5a71efa 100644
--- a/test/functional/apps/dashboard/dashboard_state.ts
+++ b/test/functional/apps/dashboard/dashboard_state.ts
@@ -53,6 +53,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
if (isNewChartsLibraryEnabled) {
await kibanaServer.uiSettings.update({
'visualization:visualize:legacyChartsLibrary': false,
+ 'visualization:visualize:legacyPieChartsLibrary': false,
});
await browser.refresh();
}
diff --git a/test/functional/apps/dashboard/index.ts b/test/functional/apps/dashboard/index.ts
index 4b83b2ac92deb..e4dc04282e4ac 100644
--- a/test/functional/apps/dashboard/index.ts
+++ b/test/functional/apps/dashboard/index.ts
@@ -123,6 +123,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
await loadLogstash();
await kibanaServer.uiSettings.update({
'visualization:visualize:legacyChartsLibrary': false,
+ 'visualization:visualize:legacyPieChartsLibrary': false,
});
await browser.refresh();
});
@@ -131,6 +132,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
await unloadLogstash();
await kibanaServer.uiSettings.update({
'visualization:visualize:legacyChartsLibrary': true,
+ 'visualization:visualize:legacyPieChartsLibrary': true,
});
await browser.refresh();
});
diff --git a/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts b/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts
index e986429a15d26..264885490cdfc 100644
--- a/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts
+++ b/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts
@@ -12,26 +12,31 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
- const log = getService('log');
+ const security = getService('security');
const retry = getService('retry');
const PageObjects = getPageObjects(['common', 'timePicker', 'discover']);
describe('index pattern with unmapped fields', () => {
before(async () => {
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/unmapped_fields');
+ await security.testUser.setRoles(['kibana_admin', 'test-index-unmapped-fields']);
+ const fromTime = 'Jan 20, 2021 @ 00:00:00.000';
+ const toTime = 'Jan 25, 2021 @ 00:00:00.000';
+
await kibanaServer.uiSettings.replace({
defaultIndex: 'test-index-unmapped-fields',
'discover:searchFieldsFromSource': false,
+ 'timepicker:timeDefaults': `{ "from": "${fromTime}", "to": "${toTime}"}`,
});
- log.debug('discover');
- const fromTime = 'Jan 20, 2021 @ 00:00:00.000';
- const toTime = 'Jan 25, 2021 @ 00:00:00.000';
+
await PageObjects.common.navigateToApp('discover');
- await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
});
after(async () => {
await esArchiver.unload('test/functional/fixtures/es_archiver/unmapped_fields');
+ await kibanaServer.uiSettings.unset('defaultIndex');
+ await kibanaServer.uiSettings.unset('discover:searchFieldsFromSource');
+ await kibanaServer.uiSettings.unset('timepicker:timeDefaults');
});
it('unmapped fields exist on a new saved search', async () => {
diff --git a/test/functional/apps/discover/_sidebar.ts b/test/functional/apps/discover/_sidebar.ts
index 8179f4e44e8b8..d8701261126c4 100644
--- a/test/functional/apps/discover/_sidebar.ts
+++ b/test/functional/apps/discover/_sidebar.ts
@@ -14,8 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['common', 'discover', 'timePicker']);
const testSubjects = getService('testSubjects');
- // Failing: See https://github.com/elastic/kibana/issues/101449
- describe.skip('discover sidebar', function describeIndexTests() {
+ describe('discover sidebar', function describeIndexTests() {
before(async function () {
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/discover');
diff --git a/test/functional/apps/getting_started/_shakespeare.ts b/test/functional/apps/getting_started/_shakespeare.ts
index 945c1fdcbdcf4..ae6841b85c98d 100644
--- a/test/functional/apps/getting_started/_shakespeare.ts
+++ b/test/functional/apps/getting_started/_shakespeare.ts
@@ -57,6 +57,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
if (isNewChartsLibraryEnabled) {
await kibanaServer.uiSettings.update({
'visualization:visualize:legacyChartsLibrary': false,
+ 'visualization:visualize:legacyPieChartsLibrary': false,
});
await browser.refresh();
}
diff --git a/test/functional/apps/getting_started/index.ts b/test/functional/apps/getting_started/index.ts
index b75a30037d065..4c1c052ef15a2 100644
--- a/test/functional/apps/getting_started/index.ts
+++ b/test/functional/apps/getting_started/index.ts
@@ -24,6 +24,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
before(async () => {
await kibanaServer.uiSettings.update({
'visualization:visualize:legacyChartsLibrary': false,
+ 'visualization:visualize:legacyPieChartsLibrary': false,
});
await browser.refresh();
});
@@ -31,6 +32,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
after(async () => {
await kibanaServer.uiSettings.update({
'visualization:visualize:legacyChartsLibrary': true,
+ 'visualization:visualize:legacyPieChartsLibrary': true,
});
await browser.refresh();
});
diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts
index cecd206abd1db..bc6160eba3846 100644
--- a/test/functional/apps/visualize/index.ts
+++ b/test/functional/apps/visualize/index.ts
@@ -31,6 +31,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
before(async () => {
await kibanaServer.uiSettings.update({
'visualization:visualize:legacyChartsLibrary': false,
+ 'visualization:visualize:legacyPieChartsLibrary': false,
});
await browser.refresh();
});
@@ -38,6 +39,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
after(async () => {
await kibanaServer.uiSettings.update({
'visualization:visualize:legacyChartsLibrary': true,
+ 'visualization:visualize:legacyPieChartsLibrary': true,
});
await browser.refresh();
});
diff --git a/test/functional/config.js b/test/functional/config.js
index bab1148cf372a..670488003e56c 100644
--- a/test/functional/config.js
+++ b/test/functional/config.js
@@ -58,6 +58,7 @@ export default async function ({ readConfigFile }) {
'accessibility:disableAnimations': true,
'dateFormat:tz': 'UTC',
'visualization:visualize:legacyChartsLibrary': true,
+ 'visualization:visualize:legacyPieChartsLibrary': true,
},
},
@@ -292,6 +293,21 @@ export default async function ({ readConfigFile }) {
kibana: [],
},
+ 'test-index-unmapped-fields': {
+ elasticsearch: {
+ cluster: [],
+ indices: [
+ {
+ names: ['test-index-unmapped-fields'],
+ privileges: ['read', 'view_index_metadata'],
+ field_security: { grant: ['*'], except: [] },
+ },
+ ],
+ run_as: [],
+ },
+ kibana: [],
+ },
+
animals: {
elasticsearch: {
cluster: [],
diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts
index 65b899d2e2fb0..dc3a04568316e 100644
--- a/test/functional/page_objects/discover_page.ts
+++ b/test/functional/page_objects/discover_page.ts
@@ -448,7 +448,10 @@ export class DiscoverPageObject extends FtrService {
public async closeSidebarFieldFilter() {
await this.testSubjects.click('toggleFieldFilterButton');
- await this.testSubjects.missingOrFail('filterSelectionPanel');
+
+ await this.retry.waitFor('sidebar filter closed', async () => {
+ return !(await this.testSubjects.exists('filterSelectionPanel'));
+ });
}
public async waitForChartLoadingComplete(renderCount: number) {
diff --git a/test/functional/page_objects/visualize_chart_page.ts b/test/functional/page_objects/visualize_chart_page.ts
index c8587f4ffd346..64b8c363fa6c2 100644
--- a/test/functional/page_objects/visualize_chart_page.ts
+++ b/test/functional/page_objects/visualize_chart_page.ts
@@ -37,7 +37,8 @@ export class VisualizeChartPageObject extends FtrService {
public async isNewChartsLibraryEnabled(): Promise {
const legacyChartsLibrary =
Boolean(
- await this.kibanaServer.uiSettings.get('visualization:visualize:legacyChartsLibrary')
+ (await this.kibanaServer.uiSettings.get('visualization:visualize:legacyChartsLibrary')) &&
+ (await this.kibanaServer.uiSettings.get('visualization:visualize:legacyPieChartsLibrary'))
) ?? true;
const enabled = !legacyChartsLibrary;
this.log.debug(`-- isNewChartsLibraryEnabled = ${enabled}`);
diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts
index a11a254509e7a..e930406cdcce8 100644
--- a/test/functional/page_objects/visualize_page.ts
+++ b/test/functional/page_objects/visualize_page.ts
@@ -57,6 +57,7 @@ export class VisualizePageObject extends FtrService {
defaultIndex: 'logstash-*',
[UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0,0.[000]b',
'visualization:visualize:legacyChartsLibrary': !isNewLibrary,
+ 'visualization:visualize:legacyPieChartsLibrary': !isNewLibrary,
});
}
diff --git a/x-pack/examples/ui_actions_enhanced_examples/kibana.json b/x-pack/examples/ui_actions_enhanced_examples/kibana.json
index 4f5ac8519fe5b..59a0926118962 100644
--- a/x-pack/examples/ui_actions_enhanced_examples/kibana.json
+++ b/x-pack/examples/ui_actions_enhanced_examples/kibana.json
@@ -19,7 +19,6 @@
"dashboardEnhanced",
"embeddable",
"kibanaUtils",
- "kibanaReact",
- "share"
+ "kibanaReact"
]
}
diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app1_to_dashboard_drilldown/app1_to_dashboard_drilldown.ts b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app1_to_dashboard_drilldown/app1_to_dashboard_drilldown.ts
index 3efe2fed9a78a..1bb911530dc50 100644
--- a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app1_to_dashboard_drilldown/app1_to_dashboard_drilldown.ts
+++ b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app1_to_dashboard_drilldown/app1_to_dashboard_drilldown.ts
@@ -10,7 +10,7 @@ import {
DashboardEnhancedAbstractDashboardDrilldownConfig as Config,
} from '../../../../../plugins/dashboard_enhanced/public';
import { SAMPLE_APP1_CLICK_TRIGGER, SampleApp1ClickContext } from '../../triggers';
-import { KibanaURL } from '../../../../../../src/plugins/share/public';
+import { KibanaLocation } from '../../../../../../src/plugins/share/public';
export const APP1_TO_DASHBOARD_DRILLDOWN = 'APP1_TO_DASHBOARD_DRILLDOWN';
@@ -21,12 +21,11 @@ export class App1ToDashboardDrilldown extends AbstractDashboardDrilldown [SAMPLE_APP1_CLICK_TRIGGER];
- protected async getURL(config: Config, context: Context): Promise {
- const path = await this.urlGenerator.createUrl({
+ protected async getLocation(config: Config, context: Context): Promise {
+ const location = await this.locator.getLocation({
dashboardId: config.dashboardId,
});
- const url = new KibanaURL(path);
- return url;
+ return location;
}
}
diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app2_to_dashboard_drilldown/app2_to_dashboard_drilldown.ts b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app2_to_dashboard_drilldown/app2_to_dashboard_drilldown.ts
index 7245e37414686..daac998872c14 100644
--- a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app2_to_dashboard_drilldown/app2_to_dashboard_drilldown.ts
+++ b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app2_to_dashboard_drilldown/app2_to_dashboard_drilldown.ts
@@ -10,7 +10,7 @@ import {
DashboardEnhancedAbstractDashboardDrilldownConfig as Config,
} from '../../../../../plugins/dashboard_enhanced/public';
import { SAMPLE_APP2_CLICK_TRIGGER, SampleApp2ClickContext } from '../../triggers';
-import { KibanaURL } from '../../../../../../src/plugins/share/public';
+import { KibanaLocation } from '../../../../../../src/plugins/share/public';
export const APP2_TO_DASHBOARD_DRILLDOWN = 'APP2_TO_DASHBOARD_DRILLDOWN';
@@ -21,12 +21,11 @@ export class App2ToDashboardDrilldown extends AbstractDashboardDrilldown [SAMPLE_APP2_CLICK_TRIGGER];
- protected async getURL(config: Config, context: Context): Promise {
- const path = await this.urlGenerator.createUrl({
+ protected async getLocation(config: Config, context: Context): Promise {
+ const location = await this.locator.getLocation({
dashboardId: config.dashboardId,
});
- const url = new KibanaURL(path);
- return url;
+ return location;
}
}
diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts
index 37d461d6b2a50..440de161490aa 100644
--- a/x-pack/plugins/actions/server/lib/action_executor.test.ts
+++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts
@@ -109,6 +109,96 @@ test('successfully executes', async () => {
});
expect(loggerMock.debug).toBeCalledWith('executing action test:1: 1');
+ expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(`
+ Array [
+ Array [
+ Object {
+ "event": Object {
+ "action": "execute-start",
+ },
+ "kibana": Object {
+ "saved_objects": Array [
+ Object {
+ "id": "1",
+ "namespace": "some-namespace",
+ "rel": "primary",
+ "type": "action",
+ "type_id": "test",
+ },
+ ],
+ },
+ "message": "action started: test:1: 1",
+ },
+ ],
+ Array [
+ Object {
+ "event": Object {
+ "action": "execute",
+ "outcome": "success",
+ },
+ "kibana": Object {
+ "saved_objects": Array [
+ Object {
+ "id": "1",
+ "namespace": "some-namespace",
+ "rel": "primary",
+ "type": "action",
+ "type_id": "test",
+ },
+ ],
+ },
+ "message": "action executed: test:1: 1",
+ },
+ ],
+ ]
+ `);
+});
+
+test('successfully executes as a task', async () => {
+ const actionType: jest.Mocked = {
+ id: 'test',
+ name: 'Test',
+ minimumLicenseRequired: 'basic',
+ executor: jest.fn(),
+ };
+ const actionSavedObject = {
+ id: '1',
+ type: 'action',
+ attributes: {
+ actionTypeId: 'test',
+ config: {
+ bar: true,
+ },
+ secrets: {
+ baz: true,
+ },
+ },
+ references: [],
+ };
+ const actionResult = {
+ id: actionSavedObject.id,
+ name: actionSavedObject.id,
+ ...pick(actionSavedObject.attributes, 'actionTypeId', 'config'),
+ isPreconfigured: false,
+ };
+ actionsClient.get.mockResolvedValueOnce(actionResult);
+ encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
+ actionTypeRegistry.get.mockReturnValueOnce(actionType);
+
+ const scheduleDelay = 10000; // milliseconds
+ const scheduled = new Date(Date.now() - scheduleDelay);
+ await actionExecutor.execute({
+ ...executeParams,
+ taskInfo: {
+ scheduled,
+ },
+ });
+
+ const eventTask = eventLogger.logEvent.mock.calls[0][0]?.kibana?.task;
+ expect(eventTask).toBeDefined();
+ expect(eventTask?.scheduled).toBe(scheduled.toISOString());
+ expect(eventTask?.schedule_delay).toBeGreaterThanOrEqual(scheduleDelay * 1000 * 1000);
+ expect(eventTask?.schedule_delay).toBeLessThanOrEqual(2 * scheduleDelay * 1000 * 1000);
});
test('provides empty config when config and / or secrets is empty', async () => {
diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts
index e9e7b17288611..9e62b123951df 100644
--- a/x-pack/plugins/actions/server/lib/action_executor.ts
+++ b/x-pack/plugins/actions/server/lib/action_executor.ts
@@ -25,6 +25,9 @@ import { ActionsClient } from '../actions_client';
import { ActionExecutionSource } from './action_execution_source';
import { RelatedSavedObjects } from './related_saved_objects';
+// 1,000,000 nanoseconds in 1 millisecond
+const Millis2Nanos = 1000 * 1000;
+
export interface ActionExecutorContext {
logger: Logger;
spaces?: SpacesServiceStart;
@@ -39,11 +42,16 @@ export interface ActionExecutorContext {
preconfiguredActions: PreConfiguredAction[];
}
+export interface TaskInfo {
+ scheduled: Date;
+}
+
export interface ExecuteOptions {
actionId: string;
request: KibanaRequest;
params: Record;
source?: ActionExecutionSource;
+ taskInfo?: TaskInfo;
relatedSavedObjects?: RelatedSavedObjects;
}
@@ -71,6 +79,7 @@ export class ActionExecutor {
params,
request,
source,
+ taskInfo,
relatedSavedObjects,
}: ExecuteOptions): Promise> {
if (!this.isInitialized) {
@@ -143,9 +152,19 @@ export class ActionExecutor {
const actionLabel = `${actionTypeId}:${actionId}: ${name}`;
logger.debug(`executing action ${actionLabel}`);
+ const task = taskInfo
+ ? {
+ task: {
+ scheduled: taskInfo.scheduled.toISOString(),
+ schedule_delay: Millis2Nanos * (Date.now() - taskInfo.scheduled.getTime()),
+ },
+ }
+ : {};
+
const event: IEvent = {
event: { action: EVENT_LOG_ACTIONS.execute },
kibana: {
+ ...task,
saved_objects: [
{
rel: SAVED_OBJECT_REL_PRIMARY,
diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts
index 2292994e3ccfd..495d638951b56 100644
--- a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts
+++ b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts
@@ -133,6 +133,9 @@ test('executes the task by calling the executor with proper parameters', async (
authorization: 'ApiKey MTIzOmFiYw==',
},
}),
+ taskInfo: {
+ scheduled: new Date(),
+ },
});
const [executeParams] = mockedActionExecutor.execute.mock.calls[0];
@@ -255,6 +258,9 @@ test('uses API key when provided', async () => {
authorization: 'ApiKey MTIzOmFiYw==',
},
}),
+ taskInfo: {
+ scheduled: new Date(),
+ },
});
const [executeParams] = mockedActionExecutor.execute.mock.calls[0];
@@ -300,6 +306,9 @@ test('uses relatedSavedObjects when provided', async () => {
authorization: 'ApiKey MTIzOmFiYw==',
},
}),
+ taskInfo: {
+ scheduled: new Date(),
+ },
});
});
@@ -323,7 +332,6 @@ test('sanitizes invalid relatedSavedObjects when provided', async () => {
});
await taskRunner.run();
-
expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
actionId: '2',
params: { baz: true },
@@ -334,6 +342,9 @@ test('sanitizes invalid relatedSavedObjects when provided', async () => {
authorization: 'ApiKey MTIzOmFiYw==',
},
}),
+ taskInfo: {
+ scheduled: new Date(),
+ },
});
});
@@ -363,6 +374,9 @@ test(`doesn't use API key when not provided`, async () => {
request: expect.objectContaining({
headers: {},
}),
+ taskInfo: {
+ scheduled: new Date(),
+ },
});
const [executeParams] = mockedActionExecutor.execute.mock.calls[0];
diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts
index 0515963ab82f4..64169de728f75 100644
--- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts
+++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts
@@ -72,6 +72,10 @@ export class TaskRunnerFactory {
getUnsecuredSavedObjectsClient,
} = this.taskRunnerContext!;
+ const taskInfo = {
+ scheduled: taskInstance.runAt,
+ };
+
return {
async run() {
const { spaceId, actionTaskParamsId } = taskInstance.params as Record;
@@ -118,6 +122,7 @@ export class TaskRunnerFactory {
actionId,
request: fakeRequest,
...getSourceFromReferences(references),
+ taskInfo,
relatedSavedObjects: validatedRelatedSavedObjects(logger, relatedSavedObjects),
});
} catch (e) {
diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts
index 033ffcceb6a0a..1dcd19119b6fd 100644
--- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts
+++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts
@@ -195,7 +195,6 @@ test('enqueues execution per selected action', async () => {
"id": "1",
"license": "basic",
"name": "name-of-alert",
- "namespace": "test1",
"ruleset": "alerts",
},
},
diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts
index 968fff540dc03..3004ed599128e 100644
--- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts
+++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts
@@ -209,7 +209,6 @@ export function createExecutionHandler<
license: alertType.minimumLicenseRequired,
category: alertType.id,
ruleset: alertType.producer,
- ...namespace,
name: alertName,
},
};
diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts
index 8ab267a5610d3..88d1b1b24a4ec 100644
--- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts
+++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts
@@ -282,13 +282,16 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "alert execution start: \\"1\\"",
"rule": Object {
"category": "test",
"id": "1",
"license": "basic",
- "namespace": undefined,
"ruleset": "alerts",
},
}
@@ -394,6 +397,10 @@ describe('Task Runner', () => {
kind: 'alert',
},
kibana: {
+ task: {
+ schedule_delay: 0,
+ scheduled: '1970-01-01T00:00:00.000Z',
+ },
saved_objects: [
{
id: '1',
@@ -409,7 +416,6 @@ describe('Task Runner', () => {
category: 'test',
id: '1',
license: 'basic',
- namespace: undefined,
ruleset: 'alerts',
},
});
@@ -518,6 +524,10 @@ describe('Task Runner', () => {
alerting: {
status: 'active',
},
+ task: {
+ schedule_delay: 0,
+ scheduled: '1970-01-01T00:00:00.000Z',
+ },
saved_objects: [
{
id: '1',
@@ -534,7 +544,6 @@ describe('Task Runner', () => {
id: '1',
license: 'basic',
name: 'alert-name',
- namespace: undefined,
ruleset: 'alerts',
},
});
@@ -603,6 +612,10 @@ describe('Task Runner', () => {
kind: 'alert',
},
kibana: {
+ task: {
+ schedule_delay: 0,
+ scheduled: '1970-01-01T00:00:00.000Z',
+ },
saved_objects: [
{
id: '1',
@@ -618,7 +631,6 @@ describe('Task Runner', () => {
category: 'test',
id: '1',
license: 'basic',
- namespace: undefined,
ruleset: 'alerts',
},
});
@@ -700,6 +712,10 @@ describe('Task Runner', () => {
alerting: {
status: 'active',
},
+ task: {
+ schedule_delay: 0,
+ scheduled: '1970-01-01T00:00:00.000Z',
+ },
saved_objects: [
{
id: '1',
@@ -716,7 +732,6 @@ describe('Task Runner', () => {
id: '1',
license: 'basic',
name: 'alert-name',
- namespace: undefined,
ruleset: 'alerts',
},
});
@@ -854,13 +869,16 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "alert execution start: \\"1\\"",
"rule": Object {
"category": "test",
"id": "1",
"license": "basic",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -897,7 +915,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -926,6 +943,10 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "alert executed: test:1: 'alert-name'",
"rule": Object {
@@ -933,7 +954,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -1151,13 +1171,16 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "alert execution start: \\"1\\"",
"rule": Object {
"category": "test",
"id": "1",
"license": "basic",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -1194,7 +1217,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -1231,7 +1253,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -1273,7 +1294,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -1302,6 +1322,10 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "alert executed: test:1: 'alert-name'",
"rule": Object {
@@ -1309,7 +1333,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -1433,13 +1456,16 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "alert execution start: \\"1\\"",
"rule": Object {
"category": "test",
"id": "1",
"license": "basic",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -1476,7 +1502,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -1513,7 +1538,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -1555,7 +1579,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -1597,7 +1620,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -1626,6 +1648,10 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "alert executed: test:1: 'alert-name'",
"rule": Object {
@@ -1633,7 +1659,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -1968,13 +1993,16 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "alert execution start: \\"1\\"",
"rule": Object {
"category": "test",
"id": "1",
"license": "basic",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -2012,7 +2040,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -2049,7 +2076,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -2078,6 +2104,10 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "alert executed: test:1: 'alert-name'",
"rule": Object {
@@ -2085,7 +2115,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -2294,13 +2323,16 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "alert execution start: \\"1\\"",
"rule": Object {
"category": "test",
"id": "1",
"license": "basic",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -2333,13 +2365,16 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "alert execution failure: test:1: 'alert-name'",
"rule": Object {
"category": "test",
"id": "1",
"license": "basic",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -2397,13 +2432,16 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "alert execution start: \\"1\\"",
"rule": Object {
"category": "test",
"id": "1",
"license": "basic",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -2436,13 +2474,16 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "test:1: execution failed",
"rule": Object {
"category": "test",
"id": "1",
"license": "basic",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -2508,13 +2549,16 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "alert execution start: \\"1\\"",
"rule": Object {
"category": "test",
"id": "1",
"license": "basic",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -2547,13 +2591,16 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "test:1: execution failed",
"rule": Object {
"category": "test",
"id": "1",
"license": "basic",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -2619,13 +2666,16 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "alert execution start: \\"1\\"",
"rule": Object {
"category": "test",
"id": "1",
"license": "basic",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -2658,13 +2708,16 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "test:1: execution failed",
"rule": Object {
"category": "test",
"id": "1",
"license": "basic",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -2729,13 +2782,16 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "alert execution start: \\"1\\"",
"rule": Object {
"category": "test",
"id": "1",
"license": "basic",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -2768,13 +2824,16 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "test:1: execution failed",
"rule": Object {
"category": "test",
"id": "1",
"license": "basic",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -3007,13 +3066,16 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "alert execution start: \\"1\\"",
"rule": Object {
"category": "test",
"id": "1",
"license": "basic",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -3050,7 +3112,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -3087,7 +3148,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -3124,7 +3184,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -3161,7 +3220,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -3190,6 +3248,10 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "alert executed: test:1: 'alert-name'",
"rule": Object {
@@ -3197,7 +3259,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -3291,13 +3352,16 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "alert execution start: \\"1\\"",
"rule": Object {
"category": "test",
"id": "1",
"license": "basic",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -3334,7 +3398,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -3371,7 +3434,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -3400,6 +3462,10 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "alert executed: test:1: 'alert-name'",
"rule": Object {
@@ -3407,7 +3473,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -3493,13 +3558,16 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "alert execution start: \\"1\\"",
"rule": Object {
"category": "test",
"id": "1",
"license": "basic",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -3534,7 +3602,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -3569,7 +3636,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -3598,6 +3664,10 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "alert executed: test:1: 'alert-name'",
"rule": Object {
@@ -3605,7 +3675,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -3686,13 +3755,16 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "alert execution start: \\"1\\"",
"rule": Object {
"category": "test",
"id": "1",
"license": "basic",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -3729,7 +3801,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -3766,7 +3837,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -3795,6 +3865,10 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "alert executed: test:1: 'alert-name'",
"rule": Object {
@@ -3802,7 +3876,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -3885,13 +3958,16 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "alert execution start: \\"1\\"",
"rule": Object {
"category": "test",
"id": "1",
"license": "basic",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -3925,7 +4001,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -3959,7 +4034,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
@@ -3988,6 +4062,10 @@ describe('Task Runner', () => {
"type_id": "test",
},
],
+ "task": Object {
+ "schedule_delay": 0,
+ "scheduled": "1970-01-01T00:00:00.000Z",
+ },
},
"message": "alert executed: test:1: 'alert-name'",
"rule": Object {
@@ -3995,7 +4073,6 @@ describe('Task Runner', () => {
"id": "1",
"license": "basic",
"name": "alert-name",
- "namespace": undefined,
"ruleset": "alerts",
},
},
diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts
index b712b6237c8a7..c66c054bc8ac3 100644
--- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts
+++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts
@@ -54,6 +54,9 @@ import { getEsErrorMessage } from '../lib/errors';
const FALLBACK_RETRY_INTERVAL = '5m';
+// 1,000,000 nanoseconds in 1 millisecond
+const Millis2Nanos = 1000 * 1000;
+
type Event = Exclude;
interface AlertTaskRunResult {
@@ -489,15 +492,17 @@ export class TaskRunner<
schedule: taskSchedule,
} = this.taskInstance;
- const runDate = new Date().toISOString();
- this.logger.debug(`executing alert ${this.alertType.id}:${alertId} at ${runDate}`);
+ const runDate = new Date();
+ const runDateString = runDate.toISOString();
+ this.logger.debug(`executing alert ${this.alertType.id}:${alertId} at ${runDateString}`);
const namespace = this.context.spaceIdToNamespace(spaceId);
const eventLogger = this.context.eventLogger;
+ const scheduleDelay = runDate.getTime() - this.taskInstance.runAt.getTime();
const event: IEvent = {
// explicitly set execute timestamp so it will be before other events
// generated here (new-instance, schedule-action, etc)
- '@timestamp': runDate,
+ '@timestamp': runDateString,
event: {
action: EVENT_LOG_ACTIONS.execute,
kind: 'alert',
@@ -513,13 +518,16 @@ export class TaskRunner<
namespace,
},
],
+ task: {
+ scheduled: this.taskInstance.runAt.toISOString(),
+ schedule_delay: Millis2Nanos * scheduleDelay,
+ },
},
rule: {
id: alertId,
license: this.alertType.minimumLicenseRequired,
category: this.alertType.id,
ruleset: this.alertType.producer,
- namespace,
},
};
@@ -814,7 +822,6 @@ function generateNewAndRecoveredInstanceEvents<
license: ruleType.minimumLicenseRequired,
category: ruleType.id,
ruleset: ruleType.producer,
- namespace,
name: rule.name,
},
};
diff --git a/x-pack/plugins/apm/public/assets/illustration_integrations_darkmode.svg b/x-pack/plugins/apm/public/assets/illustration_integrations_darkmode.svg
new file mode 100644
index 0000000000000..b1f86be19a080
--- /dev/null
+++ b/x-pack/plugins/apm/public/assets/illustration_integrations_darkmode.svg
@@ -0,0 +1 @@
+Kibana-integrations-darkmode
\ No newline at end of file
diff --git a/x-pack/plugins/apm/public/assets/illustration_integrations_lightmode.svg b/x-pack/plugins/apm/public/assets/illustration_integrations_lightmode.svg
new file mode 100644
index 0000000000000..0cddcb0af6909
--- /dev/null
+++ b/x-pack/plugins/apm/public/assets/illustration_integrations_lightmode.svg
@@ -0,0 +1 @@
+Kibana-integrations-lightmode
\ No newline at end of file
diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/select_anomaly_severity.test.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/select_anomaly_severity.test.tsx
index 7b56eaa4721de..8c8f0aa8b9b24 100644
--- a/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/select_anomaly_severity.test.tsx
+++ b/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/select_anomaly_severity.test.tsx
@@ -7,7 +7,7 @@
import { render } from '@testing-library/react';
import React, { ReactNode } from 'react';
-import { IntlProvider } from 'react-intl';
+import { __IntlProvider as IntlProvider } from '@kbn/i18n/react';
import { ANOMALY_SEVERITY } from '../../../../common/ml_constants';
import { SelectAnomalySeverity } from './select_anomaly_severity';
diff --git a/x-pack/plugins/apm/public/components/routing/app_root.tsx b/x-pack/plugins/apm/public/components/routing/app_root.tsx
index 2bb387ae315ff..8fc59a01eeca0 100644
--- a/x-pack/plugins/apm/public/components/routing/app_root.tsx
+++ b/x-pack/plugins/apm/public/components/routing/app_root.tsx
@@ -24,7 +24,7 @@ import {
} from '../../context/apm_plugin/apm_plugin_context';
import { LicenseProvider } from '../../context/license/license_context';
import { UrlParamsProvider } from '../../context/url_params_context/url_params_context';
-import { useBreadcrumbs } from '../../hooks/use_breadcrumbs';
+import { useApmBreadcrumbs } from '../../hooks/use_apm_breadcrumbs';
import { ApmPluginStartDeps } from '../../plugin';
import { HeaderMenuPortal } from '../../../../observability/public';
import { ApmHeaderActionMenu } from '../shared/apm_header_action_menu';
@@ -79,7 +79,7 @@ export function ApmAppRoot({
}
function MountApmHeaderActionMenu() {
- useBreadcrumbs(apmRouteConfig);
+ useApmBreadcrumbs(apmRouteConfig);
const { setHeaderActionMenu } = useApmPluginContext().appMountParameters;
return (
diff --git a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx
index a16f81826636b..bcc1932dde7cb 100644
--- a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx
+++ b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx
@@ -44,6 +44,7 @@ const mockCore = {
ml: {},
},
currentAppId$: new Observable(),
+ getUrlForApp: (appId: string) => '',
navigateToUrl: (url: string) => {},
},
chrome: {
diff --git a/x-pack/plugins/apm/public/hooks/use_breadcrumbs.test.tsx b/x-pack/plugins/apm/public/hooks/use_apm_breadcrumbs.test.tsx
similarity index 79%
rename from x-pack/plugins/apm/public/hooks/use_breadcrumbs.test.tsx
rename to x-pack/plugins/apm/public/hooks/use_apm_breadcrumbs.test.tsx
index 64990651b52bb..1cdb84c324750 100644
--- a/x-pack/plugins/apm/public/hooks/use_breadcrumbs.test.tsx
+++ b/x-pack/plugins/apm/public/hooks/use_apm_breadcrumbs.test.tsx
@@ -15,14 +15,15 @@ import {
mockApmPluginContextValue,
MockApmPluginContextWrapper,
} from '../context/apm_plugin/mock_apm_plugin_context';
-import { useBreadcrumbs } from './use_breadcrumbs';
+import { useApmBreadcrumbs } from './use_apm_breadcrumbs';
+import { useBreadcrumbs } from '../../../observability/public';
+
+jest.mock('../../../observability/public');
function createWrapper(path: string) {
return ({ children }: { children?: ReactNode }) => {
const value = (produce(mockApmPluginContextValue, (draft) => {
draft.core.application.navigateToUrl = (url: string) => Promise.resolve();
- draft.core.chrome.docTitle.change = changeTitle;
- draft.core.chrome.setBreadcrumbs = setBreadcrumbs;
}) as unknown) as ApmPluginContextValue;
return (
@@ -36,27 +37,18 @@ function createWrapper(path: string) {
}
function mountBreadcrumb(path: string) {
- renderHook(() => useBreadcrumbs(apmRouteConfig), {
+ renderHook(() => useApmBreadcrumbs(apmRouteConfig), {
wrapper: createWrapper(path),
});
}
-const changeTitle = jest.fn();
-const setBreadcrumbs = jest.fn();
-
-describe('useBreadcrumbs', () => {
- it('changes the page title', () => {
- mountBreadcrumb('/');
-
- expect(changeTitle).toHaveBeenCalledWith(['APM']);
- });
-
+describe('useApmBreadcrumbs', () => {
test('/services/:serviceName/errors/:groupId', () => {
mountBreadcrumb(
'/services/opbeans-node/errors/myGroupId?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0'
);
- expect(setBreadcrumbs).toHaveBeenCalledWith(
+ expect(useBreadcrumbs).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
text: 'APM',
@@ -81,20 +73,12 @@ describe('useBreadcrumbs', () => {
expect.objectContaining({ text: 'myGroupId', href: undefined }),
])
);
-
- expect(changeTitle).toHaveBeenCalledWith([
- 'myGroupId',
- 'Errors',
- 'opbeans-node',
- 'Services',
- 'APM',
- ]);
});
test('/services/:serviceName/errors', () => {
mountBreadcrumb('/services/opbeans-node/errors?kuery=myKuery');
- expect(setBreadcrumbs).toHaveBeenCalledWith(
+ expect(useBreadcrumbs).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
text: 'APM',
@@ -111,19 +95,12 @@ describe('useBreadcrumbs', () => {
expect.objectContaining({ text: 'Errors', href: undefined }),
])
);
-
- expect(changeTitle).toHaveBeenCalledWith([
- 'Errors',
- 'opbeans-node',
- 'Services',
- 'APM',
- ]);
});
test('/services/:serviceName/transactions', () => {
mountBreadcrumb('/services/opbeans-node/transactions?kuery=myKuery');
- expect(setBreadcrumbs).toHaveBeenCalledWith(
+ expect(useBreadcrumbs).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
text: 'APM',
@@ -140,13 +117,6 @@ describe('useBreadcrumbs', () => {
expect.objectContaining({ text: 'Transactions', href: undefined }),
])
);
-
- expect(changeTitle).toHaveBeenCalledWith([
- 'Transactions',
- 'opbeans-node',
- 'Services',
- 'APM',
- ]);
});
test('/services/:serviceName/transactions/view?transactionName=my-transaction-name', () => {
@@ -154,7 +124,7 @@ describe('useBreadcrumbs', () => {
'/services/opbeans-node/transactions/view?kuery=myKuery&transactionName=my-transaction-name'
);
- expect(setBreadcrumbs).toHaveBeenCalledWith(
+ expect(useBreadcrumbs).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
text: 'APM',
@@ -179,13 +149,5 @@ describe('useBreadcrumbs', () => {
}),
])
);
-
- expect(changeTitle).toHaveBeenCalledWith([
- 'my-transaction-name',
- 'Transactions',
- 'opbeans-node',
- 'Services',
- 'APM',
- ]);
});
});
diff --git a/x-pack/plugins/apm/public/hooks/use_breadcrumbs.ts b/x-pack/plugins/apm/public/hooks/use_apm_breadcrumbs.ts
similarity index 85%
rename from x-pack/plugins/apm/public/hooks/use_breadcrumbs.ts
rename to x-pack/plugins/apm/public/hooks/use_apm_breadcrumbs.ts
index d907c27319d26..d64bcadf79577 100644
--- a/x-pack/plugins/apm/public/hooks/use_breadcrumbs.ts
+++ b/x-pack/plugins/apm/public/hooks/use_apm_breadcrumbs.ts
@@ -7,14 +7,15 @@
import { History, Location } from 'history';
import { ChromeBreadcrumb } from 'kibana/public';
-import { MouseEvent, ReactNode, useEffect } from 'react';
+import { MouseEvent } from 'react';
import {
+ match as Match,
matchPath,
RouteComponentProps,
useHistory,
- match as Match,
useLocation,
} from 'react-router-dom';
+import { useBreadcrumbs } from '../../../observability/public';
import { APMRouteDefinition, BreadcrumbTitle } from '../application/routes';
import { getAPMHref } from '../components/shared/Links/apm/APMLink';
import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context';
@@ -164,33 +165,17 @@ function routeDefinitionsToBreadcrumbs({
return breadcrumbs;
}
-/**
- * Get an array for a page title from a list of breadcrumbs
- */
-function getTitleFromBreadcrumbs(breadcrumbs: ChromeBreadcrumb[]): string[] {
- function removeNonStrings(item: ReactNode): item is string {
- return typeof item === 'string';
- }
-
- return breadcrumbs
- .map(({ text }) => text)
- .reverse()
- .filter(removeNonStrings);
-}
-
/**
* Determine the breadcrumbs from the routes, set them, and update the page
* title when the route changes.
*/
-export function useBreadcrumbs(routes: APMRouteDefinition[]) {
+export function useApmBreadcrumbs(routes: APMRouteDefinition[]) {
const history = useHistory();
const location = useLocation();
const { search } = location;
const { core } = useApmPluginContext();
const { basePath } = core.http;
const { navigateToUrl } = core.application;
- const { docTitle, setBreadcrumbs } = core.chrome;
- const changeTitle = docTitle.change;
function wrappedGetAPMHref(path: string) {
return getAPMHref({ basePath, path, search });
@@ -206,10 +191,6 @@ export function useBreadcrumbs(routes: APMRouteDefinition[]) {
wrappedGetAPMHref,
navigateToUrl,
});
- const title = getTitleFromBreadcrumbs(breadcrumbs);
- useEffect(() => {
- changeTitle(title);
- setBreadcrumbs(breadcrumbs);
- }, [breadcrumbs, changeTitle, location, title, setBreadcrumbs]);
+ useBreadcrumbs(breadcrumbs);
}
diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts
index 77e7f2834b080..012856ca9213c 100644
--- a/x-pack/plugins/apm/public/plugin.ts
+++ b/x-pack/plugins/apm/public/plugin.ts
@@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
import { i18n } from '@kbn/i18n';
import { from } from 'rxjs';
import { map } from 'rxjs/operators';
@@ -140,16 +139,42 @@ export class ApmPlugin implements Plugin {
);
const getApmDataHelper = async () => {
- const {
- fetchObservabilityOverviewPageData,
- getHasData,
- createCallApmApi,
- } = await import('./services/rest/apm_observability_overview_fetchers');
+ const { fetchObservabilityOverviewPageData, getHasData } = await import(
+ './services/rest/apm_observability_overview_fetchers'
+ );
+ const { hasFleetApmIntegrations } = await import(
+ './tutorial/tutorial_apm_fleet_check'
+ );
+
+ const { createCallApmApi } = await import(
+ './services/rest/createCallApmApi'
+ );
+
// have to do this here as well in case app isn't mounted yet
createCallApmApi(core);
- return { fetchObservabilityOverviewPageData, getHasData };
+ return {
+ fetchObservabilityOverviewPageData,
+ getHasData,
+ hasFleetApmIntegrations,
+ };
};
+
+ // Registers a status check callback for the tutorial to call and verify if the APM integration is installed on fleet.
+ pluginSetupDeps.home?.tutorials.registerCustomStatusCheck(
+ 'apm_fleet_server_status_check',
+ async () => {
+ const { hasFleetApmIntegrations } = await getApmDataHelper();
+ return hasFleetApmIntegrations();
+ }
+ );
+
+ // Registers custom component that is going to be render on fleet section
+ pluginSetupDeps.home?.tutorials.registerCustomComponent(
+ 'TutorialFleetInstructions',
+ () => import('./tutorial/tutorial_fleet_instructions')
+ );
+
plugins.observability.dashboard.register({
appName: 'apm',
hasData: async () => {
@@ -163,11 +188,12 @@ export class ApmPlugin implements Plugin {
});
const getUxDataHelper = async () => {
- const {
- fetchUxOverviewDate,
- hasRumData,
- createCallApmApi,
- } = await import('./components/app/RumDashboard/ux_overview_fetchers');
+ const { fetchUxOverviewDate, hasRumData } = await import(
+ './components/app/RumDashboard/ux_overview_fetchers'
+ );
+ const { createCallApmApi } = await import(
+ './services/rest/createCallApmApi'
+ );
// have to do this here as well in case app isn't mounted yet
createCallApmApi(core);
diff --git a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts
index ef61e25af4fc2..1b95c88a5fdc5 100644
--- a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts
+++ b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts
@@ -11,8 +11,6 @@ import {
} from '../../../../observability/public';
import { callApmApi } from './createCallApmApi';
-export { createCallApmApi } from './createCallApmApi';
-
export const fetchObservabilityOverviewPageData = async ({
absoluteTime,
relativeTime,
diff --git a/x-pack/plugins/apm/public/tutorial/tutorial_apm_fleet_check.ts b/x-pack/plugins/apm/public/tutorial/tutorial_apm_fleet_check.ts
new file mode 100644
index 0000000000000..8db8614d606a9
--- /dev/null
+++ b/x-pack/plugins/apm/public/tutorial/tutorial_apm_fleet_check.ts
@@ -0,0 +1,20 @@
+/*
+ * 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 { callApmApi } from '../services/rest/createCallApmApi';
+
+export async function hasFleetApmIntegrations() {
+ try {
+ const { hasData = false } = await callApmApi({
+ endpoint: 'GET /api/apm/fleet/has_data',
+ signal: null,
+ });
+ return hasData;
+ } catch (e) {
+ console.error('Something went wrong while fetching apm fleet data', e);
+ return false;
+ }
+}
diff --git a/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/index.tsx b/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/index.tsx
new file mode 100644
index 0000000000000..8a81b7a994e76
--- /dev/null
+++ b/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/index.tsx
@@ -0,0 +1,122 @@
+/*
+ * 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 { EuiButton } from '@elastic/eui';
+import { EuiFlexItem } from '@elastic/eui';
+import { EuiFlexGroup } from '@elastic/eui';
+import { EuiPanel } from '@elastic/eui';
+import { EuiCard } from '@elastic/eui';
+import { EuiImage } from '@elastic/eui';
+import { EuiLoadingSpinner } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { HttpStart } from 'kibana/public';
+import React, { useEffect, useState } from 'react';
+import styled from 'styled-components';
+import { APIReturnType } from '../../services/rest/createCallApmApi';
+
+interface Props {
+ http: HttpStart;
+ basePath: string;
+ isDarkTheme: boolean;
+}
+
+const CentralizedContainer = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+`;
+
+type APIResponseType = APIReturnType<'GET /api/apm/fleet/has_data'>;
+
+function TutorialFleetInstructions({ http, basePath, isDarkTheme }: Props) {
+ const [data, setData] = useState();
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ async function fetchData() {
+ setIsLoading(true);
+ try {
+ const response = await http.get('/api/apm/fleet/has_data');
+ setData(response as APIResponseType);
+ } catch (e) {
+ console.error('Error while fetching fleet details.', e);
+ }
+ setIsLoading(false);
+ }
+ fetchData();
+ }, [http]);
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ // When APM integration is enable in Fleet
+ if (data?.hasData) {
+ return (
+
+ {i18n.translate(
+ 'xpack.apm.tutorial.apmServer.fleet.manageApmIntegration.button',
+ {
+ defaultMessage: 'Manage APM integration in Fleet',
+ }
+ )}
+
+ );
+ }
+ // When APM integration is not installed in Fleet or for some reason the API didn't work out
+ return (
+
+
+
+
+ {i18n.translate(
+ 'xpack.apm.tutorial.apmServer.fleet.apmIntegration.button',
+ {
+ defaultMessage: 'APM integration',
+ }
+ )}
+
+ }
+ />
+
+
+
+
+
+
+ );
+}
+// eslint-disable-next-line import/no-default-export
+export default TutorialFleetInstructions;
diff --git a/x-pack/plugins/apm/server/lib/fleet/source_maps.test.ts b/x-pack/plugins/apm/server/lib/fleet/source_maps.test.ts
index 61a4fa4436e69..d6a1770a91591 100644
--- a/x-pack/plugins/apm/server/lib/fleet/source_maps.test.ts
+++ b/x-pack/plugins/apm/server/lib/fleet/source_maps.test.ts
@@ -103,12 +103,51 @@ const artifacts = [
describe('Source maps', () => {
describe('getPackagePolicyWithSourceMap', () => {
- it('returns unchanged package policy when artifacts is empty', () => {
+ it('removes source map from package policy', () => {
+ const packagePolicyWithSourceMaps = {
+ ...packagePolicy,
+ inputs: [
+ {
+ ...packagePolicy.inputs[0],
+ compiled_input: {
+ 'apm-server': {
+ ...packagePolicy.inputs[0].compiled_input['apm-server'],
+ value: {
+ rum: {
+ source_mapping: {
+ metadata: [
+ {
+ 'service.name': 'service_name',
+ 'service.version': '1.0.0',
+ 'bundle.filepath':
+ 'http://localhost:3000/static/js/main.chunk.js',
+ 'sourcemap.url':
+ '/api/fleet/artifacts/service_name-1.0.0/my-id-1',
+ },
+ {
+ 'service.name': 'service_name',
+ 'service.version': '2.0.0',
+ 'bundle.filepath':
+ 'http://localhost:3000/static/js/main.chunk.js',
+ 'sourcemap.url':
+ '/api/fleet/artifacts/service_name-2.0.0/my-id-2',
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+ },
+ ],
+ };
const updatedPackagePolicy = getPackagePolicyWithSourceMap({
- packagePolicy,
+ packagePolicy: packagePolicyWithSourceMaps,
artifacts: [],
});
- expect(updatedPackagePolicy).toEqual(packagePolicy);
+ expect(updatedPackagePolicy.inputs[0].config).toEqual({
+ 'apm-server': { value: { rum: { source_mapping: { metadata: [] } } } },
+ });
});
it('adds source maps into the package policy', () => {
const updatedPackagePolicy = getPackagePolicyWithSourceMap({
diff --git a/x-pack/plugins/apm/server/lib/fleet/source_maps.ts b/x-pack/plugins/apm/server/lib/fleet/source_maps.ts
index b313fbad2806f..6d608f7751f3b 100644
--- a/x-pack/plugins/apm/server/lib/fleet/source_maps.ts
+++ b/x-pack/plugins/apm/server/lib/fleet/source_maps.ts
@@ -97,9 +97,6 @@ export function getPackagePolicyWithSourceMap({
packagePolicy: PackagePolicy;
artifacts: ArtifactSourceMap[];
}) {
- if (!artifacts.length) {
- return packagePolicy;
- }
const [firstInput, ...restInputs] = packagePolicy.inputs;
return {
...packagePolicy,
diff --git a/x-pack/plugins/apm/server/routes/fleet.ts b/x-pack/plugins/apm/server/routes/fleet.ts
new file mode 100644
index 0000000000000..74ca8dc368dad
--- /dev/null
+++ b/x-pack/plugins/apm/server/routes/fleet.ts
@@ -0,0 +1,36 @@
+/*
+ * 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 Boom from '@hapi/boom';
+import { i18n } from '@kbn/i18n';
+import { getApmPackgePolicies } from '../lib/fleet/get_apm_package_policies';
+import { createApmServerRoute } from './create_apm_server_route';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
+
+const hasFleetDataRoute = createApmServerRoute({
+ endpoint: 'GET /api/apm/fleet/has_data',
+ options: { tags: [] },
+ handler: async ({ core, plugins }) => {
+ const fleetPluginStart = await plugins.fleet?.start();
+ if (!fleetPluginStart) {
+ throw Boom.internal(
+ i18n.translate('xpack.apm.fleet_has_data.fleetRequired', {
+ defaultMessage: `Fleet plugin is required`,
+ })
+ );
+ }
+ const packagePolicies = await getApmPackgePolicies({
+ core,
+ fleetPluginStart,
+ });
+ return { hasData: packagePolicies.total > 0 };
+ },
+});
+
+export const ApmFleetRouteRepository = createApmServerRouteRepository().add(
+ hasFleetDataRoute
+);
diff --git a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts
index f1c08444d2e1e..fa2f80f073958 100644
--- a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts
+++ b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts
@@ -30,6 +30,7 @@ import { sourceMapsRouteRepository } from './source_maps';
import { traceRouteRepository } from './traces';
import { transactionRouteRepository } from './transactions';
import { APMRouteHandlerResources } from './typings';
+import { ApmFleetRouteRepository } from './fleet';
const getTypedGlobalApmServerRouteRepository = () => {
const repository = createApmServerRouteRepository()
@@ -50,7 +51,8 @@ const getTypedGlobalApmServerRouteRepository = () => {
.merge(anomalyDetectionRouteRepository)
.merge(apmIndicesRouteRepository)
.merge(customLinkRouteRepository)
- .merge(sourceMapsRouteRepository);
+ .merge(sourceMapsRouteRepository)
+ .merge(ApmFleetRouteRepository);
return repository;
};
diff --git a/x-pack/plugins/apm/server/routes/source_maps.ts b/x-pack/plugins/apm/server/routes/source_maps.ts
index 24ea825774b0a..f6d160e68a76a 100644
--- a/x-pack/plugins/apm/server/routes/source_maps.ts
+++ b/x-pack/plugins/apm/server/routes/source_maps.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
import Boom from '@hapi/boom';
+import { jsonRt } from '@kbn/io-ts-utils';
import * as t from 'io-ts';
import { SavedObjectsClientContract } from 'kibana/server';
import {
@@ -34,7 +35,7 @@ export const sourceMapRt = t.intersection([
const listSourceMapRoute = createApmServerRoute({
endpoint: 'GET /api/apm/sourcemaps',
options: { tags: ['access:apm'] },
- handler: async ({ plugins, logger }) => {
+ handler: async ({ plugins }) => {
try {
const fleetPluginStart = await plugins.fleet?.start();
if (fleetPluginStart) {
@@ -51,21 +52,26 @@ const listSourceMapRoute = createApmServerRoute({
});
const uploadSourceMapRoute = createApmServerRoute({
- endpoint: 'POST /api/apm/sourcemaps/{serviceName}/{serviceVersion}',
- options: { tags: ['access:apm', 'access:apm_write'] },
+ endpoint: 'POST /api/apm/sourcemaps',
+ options: {
+ tags: ['access:apm', 'access:apm_write'],
+ body: { accepts: ['multipart/form-data'] },
+ },
params: t.type({
- path: t.type({
- serviceName: t.string,
- serviceVersion: t.string,
- }),
body: t.type({
- bundleFilepath: t.string,
- sourceMap: sourceMapRt,
+ service_name: t.string,
+ service_version: t.string,
+ bundle_filepath: t.string,
+ sourcemap: jsonRt.pipe(sourceMapRt),
}),
}),
handler: async ({ params, plugins, core }) => {
- const { serviceName, serviceVersion } = params.path;
- const { bundleFilepath, sourceMap } = params.body;
+ const {
+ service_name: serviceName,
+ service_version: serviceVersion,
+ bundle_filepath: bundleFilepath,
+ sourcemap: sourceMap,
+ } = params.body;
const fleetPluginStart = await plugins.fleet?.start();
const coreStart = await core.start();
const esClient = coreStart.elasticsearch.client.asInternalUser;
@@ -107,7 +113,7 @@ const deleteSourceMapRoute = createApmServerRoute({
id: t.string,
}),
}),
- handler: async ({ context, params, plugins, core }) => {
+ handler: async ({ params, plugins, core }) => {
const fleetPluginStart = await plugins.fleet?.start();
const { id } = params.path;
const coreStart = await core.start();
diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts
index 13bd631085aac..474464dec1f99 100644
--- a/x-pack/plugins/apm/server/routes/typings.ts
+++ b/x-pack/plugins/apm/server/routes/typings.ts
@@ -39,6 +39,7 @@ export interface APMRouteCreateOptions {
| 'access:ml:canGetJobs'
| 'access:ml:canCreateJob'
>;
+ body?: { accepts: Array<'application/json' | 'multipart/form-data'> };
};
}
diff --git a/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts b/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts
index c6afd6a592fff..55adc756f31af 100644
--- a/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts
+++ b/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts
@@ -6,7 +6,11 @@
*/
import { i18n } from '@kbn/i18n';
-import { INSTRUCTION_VARIANT } from '../../../../../../src/plugins/home/server';
+import {
+ INSTRUCTION_VARIANT,
+ TutorialSchema,
+ InstructionSetSchema,
+} from '../../../../../../src/plugins/home/server';
import {
createNodeAgentInstructions,
@@ -22,7 +26,9 @@ import {
} from '../instructions/apm_agent_instructions';
import { CloudSetup } from '../../../../cloud/server';
-export function createElasticCloudInstructions(cloudSetup?: CloudSetup) {
+export function createElasticCloudInstructions(
+ cloudSetup?: CloudSetup
+): TutorialSchema['elasticCloud'] {
const apmServerUrl = cloudSetup?.apm.url;
const instructionSets = [];
@@ -37,7 +43,9 @@ export function createElasticCloudInstructions(cloudSetup?: CloudSetup) {
};
}
-function getApmServerInstructionSet(cloudSetup?: CloudSetup) {
+function getApmServerInstructionSet(
+ cloudSetup?: CloudSetup
+): InstructionSetSchema {
const cloudId = cloudSetup?.cloudId;
return {
title: i18n.translate('xpack.apm.tutorial.apmServer.title', {
@@ -61,7 +69,9 @@ function getApmServerInstructionSet(cloudSetup?: CloudSetup) {
};
}
-function getApmAgentInstructionSet(cloudSetup?: CloudSetup) {
+function getApmAgentInstructionSet(
+ cloudSetup?: CloudSetup
+): InstructionSetSchema {
const apmServerUrl = cloudSetup?.apm.url;
const secretToken = cloudSetup?.apm.secretToken;
diff --git a/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts b/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts
index a0e96f563381c..882d45c4c21db 100644
--- a/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts
+++ b/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts
@@ -6,28 +6,31 @@
*/
import { i18n } from '@kbn/i18n';
-import { INSTRUCTION_VARIANT } from '../../../../../../src/plugins/home/server';
import {
- createWindowsServerInstructions,
- createEditConfig,
- createStartServerUnixSysv,
- createStartServerUnix,
- createDownloadServerRpm,
- createDownloadServerDeb,
- createDownloadServerOsx,
-} from '../instructions/apm_server_instructions';
+ INSTRUCTION_VARIANT,
+ InstructionsSchema,
+} from '../../../../../../src/plugins/home/server';
import {
- createNodeAgentInstructions,
createDjangoAgentInstructions,
+ createDotNetAgentInstructions,
createFlaskAgentInstructions,
- createRailsAgentInstructions,
- createRackAgentInstructions,
- createJsAgentInstructions,
createGoAgentInstructions,
createJavaAgentInstructions,
- createDotNetAgentInstructions,
+ createJsAgentInstructions,
+ createNodeAgentInstructions,
createPhpAgentInstructions,
+ createRackAgentInstructions,
+ createRailsAgentInstructions,
} from '../instructions/apm_agent_instructions';
+import {
+ createDownloadServerDeb,
+ createDownloadServerOsx,
+ createDownloadServerRpm,
+ createEditConfig,
+ createStartServerUnix,
+ createStartServerUnixSysv,
+ createWindowsServerInstructions,
+} from '../instructions/apm_server_instructions';
export function onPremInstructions({
errorIndices,
@@ -41,7 +44,7 @@ export function onPremInstructions({
metricsIndices: string;
sourcemapIndices: string;
onboardingIndices: string;
-}) {
+}): InstructionsSchema {
const EDIT_CONFIG = createEditConfig();
const START_SERVER_UNIX = createStartServerUnix();
const START_SERVER_UNIX_SYSV = createStartServerUnixSysv();
@@ -66,6 +69,12 @@ export function onPremInstructions({
iconType: 'alert',
},
instructionVariants: [
+ {
+ id: INSTRUCTION_VARIANT.FLEET,
+ instructions: [
+ { customComponentName: 'TutorialFleetInstructions' },
+ ],
+ },
{
id: INSTRUCTION_VARIANT.OSX,
instructions: [
diff --git a/x-pack/plugins/apm/server/tutorial/index.ts b/x-pack/plugins/apm/server/tutorial/index.ts
index d678677a4b751..9118c30b845d0 100644
--- a/x-pack/plugins/apm/server/tutorial/index.ts
+++ b/x-pack/plugins/apm/server/tutorial/index.ts
@@ -6,15 +6,16 @@
*/
import { i18n } from '@kbn/i18n';
-import { onPremInstructions } from './envs/on_prem';
-import { createElasticCloudInstructions } from './envs/elastic_cloud';
-import apmIndexPattern from './index_pattern.json';
-import { CloudSetup } from '../../../cloud/server';
import {
ArtifactsSchema,
TutorialsCategory,
+ TutorialSchema,
} from '../../../../../src/plugins/home/server';
+import { CloudSetup } from '../../../cloud/server';
import { APM_STATIC_INDEX_PATTERN_ID } from '../../common/index_pattern_constants';
+import { createElasticCloudInstructions } from './envs/elastic_cloud';
+import { onPremInstructions } from './envs/on_prem';
+import apmIndexPattern from './index_pattern.json';
const apmIntro = i18n.translate('xpack.apm.tutorial.introduction', {
defaultMessage:
@@ -102,6 +103,7 @@ It allows you to monitor the performance of thousands of applications in real ti
),
euiIconType: 'apmApp',
artifacts,
+ customStatusCheckName: 'apm_fleet_server_status_check',
onPrem: onPremInstructions(indices),
elasticCloud: createElasticCloudInstructions(cloud),
previewImagePath: '/plugins/apm/assets/apm.png',
@@ -113,5 +115,5 @@ It allows you to monitor the performance of thousands of applications in real ti
'An APM index pattern is required for some features in the APM UI.',
}
),
- };
+ } as TutorialSchema;
};
diff --git a/x-pack/plugins/apm/server/tutorial/instructions/apm_agent_instructions.ts b/x-pack/plugins/apm/server/tutorial/instructions/apm_agent_instructions.ts
index a25021fac5d00..ba11a996f00df 100644
--- a/x-pack/plugins/apm/server/tutorial/instructions/apm_agent_instructions.ts
+++ b/x-pack/plugins/apm/server/tutorial/instructions/apm_agent_instructions.ts
@@ -913,7 +913,10 @@ export const createPhpAgentInstructions = (
'APM is automatically started when your app boots. Configure the agent either via `php.ini` file:',
}
),
- commands: `elastic_apm.server_url=http://localhost:8200
+ commands: `elastic_apm.server_url="${
+ apmServerUrl || 'http://localhost:8200'
+ }"
+elastic.apm.secret_token="${secretToken}"
elastic_apm.service_name="My service"
`.split('\n'),
textPost: i18n.translate(
diff --git a/x-pack/plugins/canvas/public/components/var_config/delete_var.tsx b/x-pack/plugins/canvas/public/components/var_config/delete_var.tsx
index f6ba2d7e28825..1aea08a96784d 100644
--- a/x-pack/plugins/canvas/public/components/var_config/delete_var.tsx
+++ b/x-pack/plugins/canvas/public/components/var_config/delete_var.tsx
@@ -39,8 +39,6 @@ const strings = {
}),
};
-import './var_panel.scss';
-
interface Props {
selectedVar: CanvasVariable;
onDelete: (v: CanvasVariable) => void;
diff --git a/x-pack/plugins/canvas/public/components/var_config/edit_var.tsx b/x-pack/plugins/canvas/public/components/var_config/edit_var.tsx
index 35f9e67745aec..5501aa9aab637 100644
--- a/x-pack/plugins/canvas/public/components/var_config/edit_var.tsx
+++ b/x-pack/plugins/canvas/public/components/var_config/edit_var.tsx
@@ -76,9 +76,6 @@ const strings = {
}),
};
-import './edit_var.scss';
-import './var_panel.scss';
-
interface Props {
selectedVar: CanvasVariable | null;
variables: CanvasVariable[];
diff --git a/x-pack/plugins/canvas/public/components/var_config/var_config.tsx b/x-pack/plugins/canvas/public/components/var_config/var_config.tsx
index dc8898e2132e7..25c77ab7704bf 100644
--- a/x-pack/plugins/canvas/public/components/var_config/var_config.tsx
+++ b/x-pack/plugins/canvas/public/components/var_config/var_config.tsx
@@ -25,8 +25,6 @@ import { CanvasVariable } from '../../../types';
import { EditVar } from './edit_var';
import { DeleteVar } from './delete_var';
-import './var_config.scss';
-
enum PanelMode {
List,
Edit,
diff --git a/x-pack/plugins/canvas/public/style/index.scss b/x-pack/plugins/canvas/public/style/index.scss
index d9592d5c0be5f..e866eada1f85f 100644
--- a/x-pack/plugins/canvas/public/style/index.scss
+++ b/x-pack/plugins/canvas/public/style/index.scss
@@ -43,6 +43,13 @@
@import '../components/workpad_page/workpad_page';
@import '../components/workpad_page/workpad_interactive_page/workpad_interactive_page';
@import '../components/workpad_page/workpad_static_page/workpad_static_page';
+@import '../components/var_config/edit_var';
+@import '../components/var_config/var_config';
+
+@import '../transitions/fade/fade';
+@import '../transitions/rotate/rotate';
+@import '../transitions/slide/slide';
+@import '../transitions/zoom/zoom';
@import '../../canvas_plugin_src/renderers/filters/advanced_filter/component/advanced_filter.scss';
@import '../../canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.scss';
diff --git a/x-pack/plugins/canvas/public/transitions/fade/index.ts b/x-pack/plugins/canvas/public/transitions/fade/index.ts
index c8fcc574b1872..7ce717a83eeb0 100644
--- a/x-pack/plugins/canvas/public/transitions/fade/index.ts
+++ b/x-pack/plugins/canvas/public/transitions/fade/index.ts
@@ -5,8 +5,6 @@
* 2.0.
*/
-import './fade.scss';
-
import { TransitionStrings } from '../../../i18n';
const { fade: strings } = TransitionStrings;
diff --git a/x-pack/plugins/canvas/public/transitions/rotate/index.ts b/x-pack/plugins/canvas/public/transitions/rotate/index.ts
index 217fd26680959..959e1ae248f2a 100644
--- a/x-pack/plugins/canvas/public/transitions/rotate/index.ts
+++ b/x-pack/plugins/canvas/public/transitions/rotate/index.ts
@@ -5,8 +5,6 @@
* 2.0.
*/
-import './rotate.scss';
-
import { TransitionStrings } from '../../../i18n';
const { rotate: strings } = TransitionStrings;
diff --git a/x-pack/plugins/canvas/public/transitions/slide/index.ts b/x-pack/plugins/canvas/public/transitions/slide/index.ts
index 0c3f82a09dd02..1cf87acca2963 100644
--- a/x-pack/plugins/canvas/public/transitions/slide/index.ts
+++ b/x-pack/plugins/canvas/public/transitions/slide/index.ts
@@ -5,8 +5,6 @@
* 2.0.
*/
-import './slide.scss';
-
import { TransitionStrings } from '../../../i18n';
const { slide: strings } = TransitionStrings;
diff --git a/x-pack/plugins/canvas/public/transitions/zoom/index.ts b/x-pack/plugins/canvas/public/transitions/zoom/index.ts
index c7c1db25bd0d7..c102935b4118b 100644
--- a/x-pack/plugins/canvas/public/transitions/zoom/index.ts
+++ b/x-pack/plugins/canvas/public/transitions/zoom/index.ts
@@ -5,8 +5,6 @@
* 2.0.
*/
-import './zoom.scss';
-
import { TransitionStrings } from '../../../i18n';
const { zoom: strings } = TransitionStrings;
diff --git a/x-pack/plugins/cases/public/containers/api.test.tsx b/x-pack/plugins/cases/public/containers/api.test.tsx
index abdee387a2c42..30a76e28e7485 100644
--- a/x-pack/plugins/cases/public/containers/api.test.tsx
+++ b/x-pack/plugins/cases/public/containers/api.test.tsx
@@ -363,13 +363,14 @@ describe('Case Configuration API', () => {
});
test('check url, method, signal', async () => {
- await patchComment(
- basicCase.id,
- basicCase.comments[0].id,
- 'updated comment',
- basicCase.comments[0].version,
- abortCtrl.signal
- );
+ await patchComment({
+ caseId: basicCase.id,
+ commentId: basicCase.comments[0].id,
+ commentUpdate: 'updated comment',
+ version: basicCase.comments[0].version,
+ signal: abortCtrl.signal,
+ owner: SECURITY_SOLUTION_OWNER,
+ });
expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}/comments`, {
method: 'PATCH',
body: JSON.stringify({
@@ -377,19 +378,21 @@ describe('Case Configuration API', () => {
type: CommentType.user,
id: basicCase.comments[0].id,
version: basicCase.comments[0].version,
+ owner: SECURITY_SOLUTION_OWNER,
}),
signal: abortCtrl.signal,
});
});
test('happy path', async () => {
- const resp = await patchComment(
- basicCase.id,
- basicCase.comments[0].id,
- 'updated comment',
- basicCase.comments[0].version,
- abortCtrl.signal
- );
+ const resp = await patchComment({
+ caseId: basicCase.id,
+ commentId: basicCase.comments[0].id,
+ commentUpdate: 'updated comment',
+ version: basicCase.comments[0].version,
+ signal: abortCtrl.signal,
+ owner: SECURITY_SOLUTION_OWNER,
+ });
expect(resp).toEqual(basicCase);
});
});
diff --git a/x-pack/plugins/cases/public/containers/api.ts b/x-pack/plugins/cases/public/containers/api.ts
index 1a2a92850a4ad..b144a874cfc53 100644
--- a/x-pack/plugins/cases/public/containers/api.ts
+++ b/x-pack/plugins/cases/public/containers/api.ts
@@ -283,14 +283,23 @@ export const postComment = async (
return convertToCamelCase(decodeCaseResponse(response));
};
-export const patchComment = async (
- caseId: string,
- commentId: string,
- commentUpdate: string,
- version: string,
- signal: AbortSignal,
- subCaseId?: string
-): Promise => {
+export const patchComment = async ({
+ caseId,
+ commentId,
+ commentUpdate,
+ version,
+ signal,
+ owner,
+ subCaseId,
+}: {
+ caseId: string;
+ commentId: string;
+ commentUpdate: string;
+ version: string;
+ signal: AbortSignal;
+ owner: string;
+ subCaseId?: string;
+}): Promise => {
const response = await KibanaServices.get().http.fetch(getCaseCommentsUrl(caseId), {
method: 'PATCH',
body: JSON.stringify({
@@ -298,6 +307,7 @@ export const patchComment = async (
type: CommentType.user,
id: commentId,
version,
+ owner,
}),
...(subCaseId ? { query: { subCaseId } } : {}),
signal,
diff --git a/x-pack/plugins/cases/public/containers/use_update_comment.test.tsx b/x-pack/plugins/cases/public/containers/use_update_comment.test.tsx
index b936eb126f0d4..14cc4dfab3599 100644
--- a/x-pack/plugins/cases/public/containers/use_update_comment.test.tsx
+++ b/x-pack/plugins/cases/public/containers/use_update_comment.test.tsx
@@ -5,10 +5,13 @@
* 2.0.
*/
+import React from 'react';
import { renderHook, act } from '@testing-library/react-hooks';
import { useUpdateComment, UseUpdateComment } from './use_update_comment';
import { basicCase, basicCaseCommentPatch, basicSubCaseId } from './mock';
import * as api from './api';
+import { TestProviders } from '../common/mock';
+import { SECURITY_SOLUTION_OWNER } from '../../common';
jest.mock('./api');
jest.mock('../common/lib/kibana');
@@ -25,6 +28,12 @@ describe('useUpdateComment', () => {
updateCase,
version: basicCase.comments[0].version,
};
+
+ const renderHookUseUpdateComment = () =>
+ renderHook(() => useUpdateComment(), {
+ wrapper: ({ children }) => {children} ,
+ });
+
beforeEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
@@ -32,9 +41,7 @@ describe('useUpdateComment', () => {
it('init', async () => {
await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useUpdateComment()
- );
+ const { result, waitForNextUpdate } = renderHookUseUpdateComment();
await waitForNextUpdate();
expect(result.current).toEqual({
isLoadingIds: [],
@@ -48,21 +55,20 @@ describe('useUpdateComment', () => {
const spyOnPatchComment = jest.spyOn(api, 'patchComment');
await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useUpdateComment()
- );
+ const { result, waitForNextUpdate } = renderHookUseUpdateComment();
await waitForNextUpdate();
result.current.patchComment(sampleUpdate);
await waitForNextUpdate();
- expect(spyOnPatchComment).toBeCalledWith(
- basicCase.id,
- basicCase.comments[0].id,
- 'updated comment',
- basicCase.comments[0].version,
- abortCtrl.signal,
- undefined
- );
+ expect(spyOnPatchComment).toBeCalledWith({
+ caseId: basicCase.id,
+ commentId: basicCase.comments[0].id,
+ commentUpdate: 'updated comment',
+ version: basicCase.comments[0].version,
+ signal: abortCtrl.signal,
+ owner: SECURITY_SOLUTION_OWNER,
+ subCaseId: undefined,
+ });
});
});
@@ -70,29 +76,26 @@ describe('useUpdateComment', () => {
const spyOnPatchComment = jest.spyOn(api, 'patchComment');
await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useUpdateComment()
- );
+ const { result, waitForNextUpdate } = renderHookUseUpdateComment();
await waitForNextUpdate();
result.current.patchComment({ ...sampleUpdate, subCaseId: basicSubCaseId });
await waitForNextUpdate();
- expect(spyOnPatchComment).toBeCalledWith(
- basicCase.id,
- basicCase.comments[0].id,
- 'updated comment',
- basicCase.comments[0].version,
- abortCtrl.signal,
- basicSubCaseId
- );
+ expect(spyOnPatchComment).toBeCalledWith({
+ caseId: basicCase.id,
+ commentId: basicCase.comments[0].id,
+ commentUpdate: 'updated comment',
+ version: basicCase.comments[0].version,
+ signal: abortCtrl.signal,
+ owner: SECURITY_SOLUTION_OWNER,
+ subCaseId: basicSubCaseId,
+ });
});
});
it('patch comment', async () => {
await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useUpdateComment()
- );
+ const { result, waitForNextUpdate } = renderHookUseUpdateComment();
await waitForNextUpdate();
result.current.patchComment(sampleUpdate);
await waitForNextUpdate();
@@ -108,9 +111,7 @@ describe('useUpdateComment', () => {
it('set isLoading to true when posting case', async () => {
await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useUpdateComment()
- );
+ const { result, waitForNextUpdate } = renderHookUseUpdateComment();
await waitForNextUpdate();
result.current.patchComment(sampleUpdate);
@@ -125,9 +126,7 @@ describe('useUpdateComment', () => {
});
await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useUpdateComment()
- );
+ const { result, waitForNextUpdate } = renderHookUseUpdateComment();
await waitForNextUpdate();
result.current.patchComment(sampleUpdate);
diff --git a/x-pack/plugins/cases/public/containers/use_update_comment.tsx b/x-pack/plugins/cases/public/containers/use_update_comment.tsx
index 478d7ebf1fc32..3c307d86ac7bc 100644
--- a/x-pack/plugins/cases/public/containers/use_update_comment.tsx
+++ b/x-pack/plugins/cases/public/containers/use_update_comment.tsx
@@ -7,6 +7,7 @@
import { useReducer, useCallback, useRef, useEffect } from 'react';
import { useToasts } from '../common/lib/kibana';
+import { useOwnerContext } from '../components/owner_context/use_owner_context';
import { patchComment } from './api';
import * as i18n from './translations';
import { Case } from './types';
@@ -72,6 +73,9 @@ export const useUpdateComment = (): UseUpdateComment => {
const toasts = useToasts();
const isCancelledRef = useRef(false);
const abortCtrlRef = useRef(new AbortController());
+ // this hook guarantees that there will be at least one value in the owner array, we'll
+ // just use the first entry just in case there are more than one entry
+ const owner = useOwnerContext()[0];
const dispatchUpdateComment = useCallback(
async ({
@@ -89,14 +93,15 @@ export const useUpdateComment = (): UseUpdateComment => {
abortCtrlRef.current = new AbortController();
dispatch({ type: 'FETCH_INIT', payload: commentId });
- const response = await patchComment(
+ const response = await patchComment({
caseId,
commentId,
commentUpdate,
version,
- abortCtrlRef.current.signal,
- subCaseId
- );
+ signal: abortCtrlRef.current.signal,
+ subCaseId,
+ owner,
+ });
if (!isCancelledRef.current) {
updateCase(response);
diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts
index 6b4f038871626..9e2066984a9da 100644
--- a/x-pack/plugins/cases/server/client/cases/push.ts
+++ b/x-pack/plugins/cases/server/client/cases/push.ts
@@ -110,20 +110,24 @@ export const push = async (
alertsInfo,
});
- const connectorMappings = await casesClientInternal.configuration.getMappings({
+ const getMappingsResponse = await casesClientInternal.configuration.getMappings({
connector: theCase.connector,
});
- if (connectorMappings.length === 0) {
- throw new Error('Connector mapping has not been created');
- }
+ const mappings =
+ getMappingsResponse.length === 0
+ ? await casesClientInternal.configuration.createMappings({
+ connector: theCase.connector,
+ owner: theCase.owner,
+ })
+ : getMappingsResponse[0].attributes.mappings;
const externalServiceIncident = await createIncident({
actionsClient,
theCase,
userActions,
connector: connector as ActionConnector,
- mappings: connectorMappings[0].attributes.mappings,
+ mappings,
alerts,
casesConnectors,
});
diff --git a/x-pack/plugins/cloud/public/fullstory.ts b/x-pack/plugins/cloud/public/fullstory.ts
new file mode 100644
index 0000000000000..31e5ec128b9a3
--- /dev/null
+++ b/x-pack/plugins/cloud/public/fullstory.ts
@@ -0,0 +1,100 @@
+/*
+ * 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 { sha256 } from 'js-sha256';
+import type { IBasePath, PackageInfo } from '../../../../src/core/public';
+
+export interface FullStoryDeps {
+ basePath: IBasePath;
+ orgId: string;
+ packageInfo: PackageInfo;
+ userIdPromise: Promise;
+}
+
+interface FullStoryApi {
+ identify(userId: string, userVars?: Record): void;
+ event(eventName: string, eventProperties: Record): void;
+}
+
+export const initializeFullStory = async ({
+ basePath,
+ orgId,
+ packageInfo,
+ userIdPromise,
+}: FullStoryDeps) => {
+ // @ts-expect-error
+ window._fs_debug = false;
+ // @ts-expect-error
+ window._fs_host = 'fullstory.com';
+ // @ts-expect-error
+ window._fs_script = basePath.prepend(`/internal/cloud/${packageInfo.buildNum}/fullstory.js`);
+ // @ts-expect-error
+ window._fs_org = orgId;
+ // @ts-expect-error
+ window._fs_namespace = 'FSKibana';
+
+ /* eslint-disable */
+ (function(m,n,e,t,l,o,g,y){
+ if (e in m) {if(m.console && m.console.log) { m.console.log('FullStory namespace conflict. Please set window["_fs_namespace"].');} return;}
+ // @ts-expect-error
+ g=m[e]=function(a,b,s){g.q?g.q.push([a,b,s]):g._api(a,b,s);};g.q=[];
+ // @ts-expect-error
+ o=n.createElement(t);o.async=1;o.crossOrigin='anonymous';o.src=_fs_script;
+ // @ts-expect-error
+ y=n.getElementsByTagName(t)[0];y.parentNode.insertBefore(o,y);
+ // @ts-expect-error
+ g.identify=function(i,v,s){g(l,{uid:i},s);if(v)g(l,v,s)};g.setUserVars=function(v,s){g(l,v,s)};g.event=function(i,v,s){g('event',{n:i,p:v},s)};
+ // @ts-expect-error
+ g.anonymize=function(){g.identify(!!0)};
+ // @ts-expect-error
+ g.shutdown=function(){g("rec",!1)};g.restart=function(){g("rec",!0)};
+ // @ts-expect-error
+ g.log = function(a,b){g("log",[a,b])};
+ // @ts-expect-error
+ g.consent=function(a){g("consent",!arguments.length||a)};
+ // @ts-expect-error
+ g.identifyAccount=function(i,v){o='account';v=v||{};v.acctId=i;g(o,v)};
+ // @ts-expect-error
+ g.clearUserCookie=function(){};
+ // @ts-expect-error
+ g.setVars=function(n, p){g('setVars',[n,p]);};
+ // @ts-expect-error
+ g._w={};y='XMLHttpRequest';g._w[y]=m[y];y='fetch';g._w[y]=m[y];
+ // @ts-expect-error
+ if(m[y])m[y]=function(){return g._w[y].apply(this,arguments)};
+ // @ts-expect-error
+ g._v="1.3.0";
+ // @ts-expect-error
+ })(window,document,window['_fs_namespace'],'script','user');
+ /* eslint-enable */
+
+ // @ts-expect-error
+ const fullstory: FullStoryApi = window.FSKibana;
+
+ // Record an event that Kibana was opened so we can easily search for sessions that use Kibana
+ // @ts-expect-error
+ window.FSKibana.event('Loaded Kibana', {
+ kibana_version_str: packageInfo.version,
+ });
+
+ // Use a promise here so we don't have to wait to retrieve the user to start recording the session
+ userIdPromise
+ .then((userId) => {
+ if (!userId) return;
+ // Do the hashing here to keep it at clear as possible in our source code that we do not send literal user IDs
+ const hashedId = sha256(userId.toString());
+ // @ts-expect-error
+ window.FSKibana.identify(hashedId);
+ })
+ .catch((e) => {
+ // eslint-disable-next-line no-console
+ console.error(
+ `[cloud.full_story] Could not call FS.identify due to error: ${e.toString()}`,
+ e
+ );
+ });
+};
diff --git a/x-pack/plugins/cloud/public/plugin.test.mocks.ts b/x-pack/plugins/cloud/public/plugin.test.mocks.ts
new file mode 100644
index 0000000000000..889b8492d5b1b
--- /dev/null
+++ b/x-pack/plugins/cloud/public/plugin.test.mocks.ts
@@ -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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { FullStoryDeps } from './fullstory';
+
+export const initializeFullStoryMock = jest.fn();
+jest.doMock('./fullstory', () => {
+ return { initializeFullStory: initializeFullStoryMock };
+});
diff --git a/x-pack/plugins/cloud/public/plugin.test.ts b/x-pack/plugins/cloud/public/plugin.test.ts
index 981c5138d98d2..af4d3c4c9005d 100644
--- a/x-pack/plugins/cloud/public/plugin.test.ts
+++ b/x-pack/plugins/cloud/public/plugin.test.ts
@@ -9,11 +9,98 @@ import { nextTick } from '@kbn/test/jest';
import { coreMock } from 'src/core/public/mocks';
import { homePluginMock } from 'src/plugins/home/public/mocks';
import { securityMock } from '../../security/public/mocks';
-import { CloudPlugin } from './plugin';
+import { initializeFullStoryMock } from './plugin.test.mocks';
+import { CloudPlugin, CloudConfigType, loadFullStoryUserId } from './plugin';
describe('Cloud Plugin', () => {
+ describe('#setup', () => {
+ describe('setupFullstory', () => {
+ beforeEach(() => {
+ initializeFullStoryMock.mockReset();
+ });
+
+ const setupPlugin = async ({
+ config = {},
+ securityEnabled = true,
+ currentUserProps = {},
+ }: {
+ config?: Partial;
+ securityEnabled?: boolean;
+ currentUserProps?: Record;
+ }) => {
+ const initContext = coreMock.createPluginInitializerContext({
+ id: 'cloudId',
+ base_url: 'https://cloud.elastic.co',
+ deployment_url: '/abc123',
+ profile_url: '/profile/alice',
+ organization_url: '/org/myOrg',
+ full_story: {
+ enabled: false,
+ },
+ ...config,
+ });
+ const plugin = new CloudPlugin(initContext);
+
+ const coreSetup = coreMock.createSetup();
+ const securitySetup = securityMock.createSetup();
+ securitySetup.authc.getCurrentUser.mockResolvedValue(
+ securityMock.createMockAuthenticatedUser(currentUserProps)
+ );
+
+ const setup = plugin.setup(coreSetup, securityEnabled ? { security: securitySetup } : {});
+ // Wait for fullstory dynamic import to resolve
+ await new Promise((r) => setImmediate(r));
+
+ return { initContext, plugin, setup };
+ };
+
+ it('calls initializeFullStory with correct args when enabled and org_id are set', async () => {
+ const { initContext } = await setupPlugin({
+ config: { full_story: { enabled: true, org_id: 'foo' } },
+ currentUserProps: {
+ username: '1234',
+ },
+ });
+
+ expect(initializeFullStoryMock).toHaveBeenCalled();
+ const {
+ basePath,
+ orgId,
+ packageInfo,
+ userIdPromise,
+ } = initializeFullStoryMock.mock.calls[0][0];
+ expect(basePath.prepend).toBeDefined();
+ expect(orgId).toEqual('foo');
+ expect(packageInfo).toEqual(initContext.env.packageInfo);
+ expect(await userIdPromise).toEqual('1234');
+ });
+
+ it('passes undefined user ID when security is not available', async () => {
+ await setupPlugin({
+ config: { full_story: { enabled: true, org_id: 'foo' } },
+ securityEnabled: false,
+ });
+
+ expect(initializeFullStoryMock).toHaveBeenCalled();
+ const { orgId, userIdPromise } = initializeFullStoryMock.mock.calls[0][0];
+ expect(orgId).toEqual('foo');
+ expect(await userIdPromise).toEqual(undefined);
+ });
+
+ it('does not call initializeFullStory when enabled=false', async () => {
+ await setupPlugin({ config: { full_story: { enabled: false, org_id: 'foo' } } });
+ expect(initializeFullStoryMock).not.toHaveBeenCalled();
+ });
+
+ it('does not call initializeFullStory when org_id is undefined', async () => {
+ await setupPlugin({ config: { full_story: { enabled: true } } });
+ expect(initializeFullStoryMock).not.toHaveBeenCalled();
+ });
+ });
+ });
+
describe('#start', () => {
- function setupPlugin() {
+ const startPlugin = () => {
const plugin = new CloudPlugin(
coreMock.createPluginInitializerContext({
id: 'cloudId',
@@ -21,6 +108,9 @@ describe('Cloud Plugin', () => {
deployment_url: '/abc123',
profile_url: '/profile/alice',
organization_url: '/org/myOrg',
+ full_story: {
+ enabled: false,
+ },
})
);
const coreSetup = coreMock.createSetup();
@@ -29,10 +119,10 @@ describe('Cloud Plugin', () => {
plugin.setup(coreSetup, { home: homeSetup });
return { coreSetup, plugin };
- }
+ };
it('registers help support URL', async () => {
- const { plugin } = setupPlugin();
+ const { plugin } = startPlugin();
const coreStart = coreMock.createStart();
const securityStart = securityMock.createStart();
@@ -47,7 +137,7 @@ describe('Cloud Plugin', () => {
});
it('does not register custom nav links on anonymous pages', async () => {
- const { plugin } = setupPlugin();
+ const { plugin } = startPlugin();
const coreStart = coreMock.createStart();
coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(true);
@@ -68,7 +158,7 @@ describe('Cloud Plugin', () => {
});
it('registers a custom nav link for superusers', async () => {
- const { plugin } = setupPlugin();
+ const { plugin } = startPlugin();
const coreStart = coreMock.createStart();
const securityStart = securityMock.createStart();
@@ -94,7 +184,7 @@ describe('Cloud Plugin', () => {
});
it('registers a custom nav link when there is an error retrieving the current user', async () => {
- const { plugin } = setupPlugin();
+ const { plugin } = startPlugin();
const coreStart = coreMock.createStart();
const securityStart = securityMock.createStart();
@@ -116,7 +206,7 @@ describe('Cloud Plugin', () => {
});
it('does not register a custom nav link for non-superusers', async () => {
- const { plugin } = setupPlugin();
+ const { plugin } = startPlugin();
const coreStart = coreMock.createStart();
const securityStart = securityMock.createStart();
@@ -133,7 +223,7 @@ describe('Cloud Plugin', () => {
});
it('registers user profile links for superusers', async () => {
- const { plugin } = setupPlugin();
+ const { plugin } = startPlugin();
const coreStart = coreMock.createStart();
const securityStart = securityMock.createStart();
@@ -169,7 +259,7 @@ describe('Cloud Plugin', () => {
});
it('registers profile links when there is an error retrieving the current user', async () => {
- const { plugin } = setupPlugin();
+ const { plugin } = startPlugin();
const coreStart = coreMock.createStart();
const securityStart = securityMock.createStart();
@@ -201,7 +291,7 @@ describe('Cloud Plugin', () => {
});
it('does not register profile links for non-superusers', async () => {
- const { plugin } = setupPlugin();
+ const { plugin } = startPlugin();
const coreStart = coreMock.createStart();
const securityStart = securityMock.createStart();
@@ -217,4 +307,56 @@ describe('Cloud Plugin', () => {
expect(securityStart.navControlService.addUserMenuLinks).not.toHaveBeenCalled();
});
});
+
+ describe('loadFullStoryUserId', () => {
+ let consoleMock: jest.SpyInstance;
+
+ beforeEach(() => {
+ consoleMock = jest.spyOn(console, 'debug').mockImplementation(() => {});
+ });
+ afterEach(() => {
+ consoleMock.mockRestore();
+ });
+
+ it('returns principal ID when username specified', async () => {
+ expect(
+ await loadFullStoryUserId({
+ getCurrentUser: jest.fn().mockResolvedValue({
+ username: '1234',
+ }),
+ })
+ ).toEqual('1234');
+ expect(consoleMock).not.toHaveBeenCalled();
+ });
+
+ it('returns undefined if getCurrentUser throws', async () => {
+ expect(
+ await loadFullStoryUserId({
+ getCurrentUser: jest.fn().mockRejectedValue(new Error(`Oh no!`)),
+ })
+ ).toBeUndefined();
+ });
+
+ it('returns undefined if getCurrentUser returns undefined', async () => {
+ expect(
+ await loadFullStoryUserId({
+ getCurrentUser: jest.fn().mockResolvedValue(undefined),
+ })
+ ).toBeUndefined();
+ });
+
+ it('returns undefined and logs if username undefined', async () => {
+ expect(
+ await loadFullStoryUserId({
+ getCurrentUser: jest.fn().mockResolvedValue({
+ username: undefined,
+ metadata: { foo: 'bar' },
+ }),
+ })
+ ).toBeUndefined();
+ expect(consoleMock).toHaveBeenLastCalledWith(
+ `[cloud.full_story] username not specified. User metadata: {"foo":"bar"}`
+ );
+ });
+ });
});
diff --git a/x-pack/plugins/cloud/public/plugin.ts b/x-pack/plugins/cloud/public/plugin.ts
index 5820452706539..68dece1bc5d3d 100644
--- a/x-pack/plugins/cloud/public/plugin.ts
+++ b/x-pack/plugins/cloud/public/plugin.ts
@@ -5,9 +5,20 @@
* 2.0.
*/
-import { CoreSetup, CoreStart, Plugin, PluginInitializerContext, HttpStart } from 'src/core/public';
+import {
+ CoreSetup,
+ CoreStart,
+ Plugin,
+ PluginInitializerContext,
+ HttpStart,
+ IBasePath,
+} from 'src/core/public';
import { i18n } from '@kbn/i18n';
-import { SecurityPluginSetup, SecurityPluginStart } from '../../security/public';
+import type {
+ AuthenticatedUser,
+ SecurityPluginSetup,
+ SecurityPluginStart,
+} from '../../security/public';
import { getIsCloudEnabled } from '../common/is_cloud_enabled';
import { ELASTIC_SUPPORT_LINK } from '../common/constants';
import { HomePublicPluginSetup } from '../../../../src/plugins/home/public';
@@ -21,6 +32,10 @@ export interface CloudConfigType {
profile_url?: string;
deployment_url?: string;
organization_url?: string;
+ full_story: {
+ enabled: boolean;
+ org_id?: string;
+ };
}
interface CloudSetupDependencies {
@@ -51,7 +66,12 @@ export class CloudPlugin implements Plugin {
this.isCloudEnabled = false;
}
- public setup(core: CoreSetup, { home }: CloudSetupDependencies) {
+ public setup(core: CoreSetup, { home, security }: CloudSetupDependencies) {
+ this.setupFullstory({ basePath: core.http.basePath, security }).catch((e) =>
+ // eslint-disable-next-line no-console
+ console.debug(`Error setting up FullStory: ${e.toString()}`)
+ );
+
const {
id,
cname,
@@ -135,4 +155,57 @@ export class CloudPlugin implements Plugin {
const user = await security.authc.getCurrentUser().catch(() => null);
return user?.roles.includes('superuser') ?? true;
}
+
+ private async setupFullstory({
+ basePath,
+ security,
+ }: CloudSetupDependencies & { basePath: IBasePath }) {
+ const { enabled, org_id: orgId } = this.config.full_story;
+ if (!enabled || !orgId) {
+ return;
+ }
+
+ // Keep this import async so that we do not load any FullStory code into the browser when it is disabled.
+ const { initializeFullStory } = await import('./fullstory');
+ const userIdPromise: Promise = security
+ ? loadFullStoryUserId({ getCurrentUser: security.authc.getCurrentUser })
+ : Promise.resolve(undefined);
+
+ initializeFullStory({
+ basePath,
+ orgId,
+ packageInfo: this.initializerContext.env.packageInfo,
+ userIdPromise,
+ });
+ }
}
+
+/** @internal exported for testing */
+export const loadFullStoryUserId = async ({
+ getCurrentUser,
+}: {
+ getCurrentUser: () => Promise;
+}) => {
+ try {
+ const currentUser = await getCurrentUser().catch(() => undefined);
+ if (!currentUser) {
+ return undefined;
+ }
+
+ // Log very defensively here so we can debug this easily if it breaks
+ if (!currentUser.username) {
+ // eslint-disable-next-line no-console
+ console.debug(
+ `[cloud.full_story] username not specified. User metadata: ${JSON.stringify(
+ currentUser.metadata
+ )}`
+ );
+ }
+
+ return currentUser.username;
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error(`[cloud.full_story] Error loading the current user: ${e.toString()}`, e);
+ return undefined;
+ }
+};
diff --git a/x-pack/plugins/cloud/server/assets/fullstory_library.js b/x-pack/plugins/cloud/server/assets/fullstory_library.js
new file mode 100644
index 0000000000000..487d6c8e8528c
--- /dev/null
+++ b/x-pack/plugins/cloud/server/assets/fullstory_library.js
@@ -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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+/* @notice
+ * Portions of this code are licensed under the following license:
+ * For license information please see https://edge.fullstory.com/s/fs.js.LICENSE.txt
+ */
+/* eslint-disable */
+!function(e){var t={};function n(r){if(t[r])return t[r].exports;var i=t[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(r,i,function(t){return e[t]}.bind(null,i));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/s",n(n.s=4)}([function(e,t,n){"use strict";var r=this&&this.__assign||function(){return(r=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0},t.assertExhaustive=function(e,t){throw void 0===t&&(t="Reached unexpected case in exhaustive switch"),new Error(t)},t.pick=function(e){for(var t=[],n=1;na&&(i=a);var u=n.split(/[#,]/);if(u.length<3&&(u=n.split("`")).length<3)return null;var c=u[0],h=u[1],d=u[2],l=u[3],p="";void 0!==l&&(p=decodeURIComponent(l),(f.indexOf(p)>=0||v.indexOf(p)>=0)&&(o("Ignoring invalid app key \""+p+"\" from cookie."),p=""));var m=d.split(":");return{expirationAbsTimeSeconds:i,host:c,orgId:h,userId:m[0],sessionId:m[1]||"",appKeyHash:p}}function y(e){for(var t={},n=e.cookie.split(";"),r=0;r=0&&(t=t.slice(0,n)),t}(e))?e:0==e.indexOf("www.")?"app."+e.slice(4):"app."+e:e}function H(e){return e?e+"/s/fs.js":void 0}!function(e){e.MUT_INSERT=2,e.MUT_REMOVE=3,e.MUT_ATTR=4,e.MUT_TEXT=6,e.MOUSEMOVE=8,e.MOUSEMOVE_CURVE=9,e.SCROLL_LAYOUT=10,e.SCROLL_LAYOUT_CURVE=11,e.MOUSEDOWN=12,e.MOUSEUP=13,e.KEYDOWN=14,e.KEYUP=15,e.CLICK=16,e.FOCUS=17,e.VALUECHANGE=18,e.RESIZE_LAYOUT=19,e.DOMLOADED=20,e.LOAD=21,e.PLACEHOLDER_SIZE=22,e.UNLOAD=23,e.BLUR=24,e.SET_FRAME_BASE=25,e.TOUCHSTART=32,e.TOUCHEND=33,e.TOUCHCANCEL=34,e.TOUCHMOVE=35,e.TOUCHMOVE_CURVE=36,e.NAVIGATE=37,e.PLAY=38,e.PAUSE=39,e.RESIZE_VISUAL=40,e.RESIZE_VISUAL_CURVE=41,e.RESIZE_DOCUMENT=42,e.LOG=48,e.ERROR=49,e.DBL_CLICK=50,e.FORM_SUBMIT=51,e.WINDOW_FOCUS=52,e.WINDOW_BLUR=53,e.HEARTBEAT=54,e.WATCHED_ELEM=56,e.PERF_ENTRY=57,e.REC_FEAT_SUPPORTED=58,e.SELECT=59,e.CSSRULE_INSERT=60,e.CSSRULE_DELETE=61,e.FAIL_THROTTLED=62,e.AJAX_REQUEST=63,e.SCROLL_VISUAL_OFFSET=64,e.SCROLL_VISUAL_OFFSET_CURVE=65,e.MEDIA_QUERY_CHANGE=66,e.RESOURCE_TIMING_BUFFER_FULL=67,e.MUT_SHADOW=68,e.DISABLE_STYLESHEET=69,e.FULLSCREEN=70,e.FULLSCREEN_ERROR=71,e.ADOPTED_STYLESHEETS=72,e.CUSTOM_ELEMENT_DEFINED=73,e.MODAL_OPEN=74,e.MODAL_CLOSE=75,e.SLOW_INTERACTION=76,e.LONG_FRAME=77,e.TIMING=78,e.STORAGE_WRITE_FAILURE=79,e.KEEP_ELEMENT=2e3,e.KEEP_URL=2001,e.KEEP_BOUNCE=2002,e.SYS_SETVAR=8193,e.SYS_RESOURCEHASH=8195,e.SYS_SETCONSENT=8196,e.SYS_CUSTOM=8197,e.SYS_REPORTCONSENT=8198}(R||(R={})),function(e){e.Unknown=0,e.Serialization=1}(A||(A={})),function(e){e.Unknown=0,e.DomSnapshot=1,e.NodeEncoding=2,e.LzEncoding=3}(x||(x={})),function(e){e.Internal=0,e.Public=1}(O||(O={}));var j,K,V,z,Y,G,Q,X,J,$,Z,ee,te,ne=["print","alert","confirm"];function re(e){switch(e){case R.MOUSEDOWN:case R.MOUSEMOVE:case R.MOUSEMOVE_CURVE:case R.MOUSEUP:case R.KEYDOWN:case R.KEYUP:case R.TOUCHSTART:case R.TOUCHEND:case R.TOUCHMOVE:case R.TOUCHMOVE_CURVE:case R.TOUCHCANCEL:case R.CLICK:case R.SCROLL_LAYOUT:case R.SCROLL_LAYOUT_CURVE:case R.SCROLL_VISUAL_OFFSET:case R.SCROLL_VISUAL_OFFSET_CURVE:case R.NAVIGATE:return!0;}return!1}!function(e){e.GrantConsent=!0,e.RevokeConsent=!1}(j||(j={})),function(e){e.Page=0,e.Document=1}(K||(K={})),function(e){e.Unknown=0,e.Api=1,e.FsShutdownFrame=2,e.Hibernation=3,e.Reidentify=4,e.SettingsBlocked=5,e.Size=6,e.Unload=7}(V||(V={})),function(e){e.Timing=0,e.Navigation=1,e.Resource=2,e.Paint=3,e.Mark=4,e.Measure=5,e.Memory=6}(z||(z={})),function(e){e.Performance=0,e.PerformanceEntries=1,e.PerformanceMemory=2,e.Console=3,e.Ajax=4,e.PerformanceObserver=5,e.AjaxFetch=6}(Y||(Y={})),function(e){e.Node=1,e.Sheet=2}(G||(G={})),function(e){e.StyleSheetHooks=0,e.SetPropertyHooks=1}(Q||(Q={})),function(e){e.User="user",e.Account="acct",e.Event="evt"}(X||(X={})),function(e){e.Elide=0,e.Record=1,e.Whitelist=2}(J||(J={})),function(e){e.ReasonNoSuchOrg=1,e.ReasonOrgDisabled=2,e.ReasonOrgOverQuota=3,e.ReasonBlockedDomain=4,e.ReasonBlockedIp=5,e.ReasonBlockedUserAgent=6,e.ReasonBlockedGeo=7,e.ReasonBlockedTrafficRamping=8,e.ReasonInvalidURL=9,e.ReasonUserOptOut=10,e.ReasonInvalidRecScript=11,e.ReasonDeletingUser=12,e.ReasonNativeHookFailure=13}($||($={})),function(e){e.Unset=0,e.Exclude=1,e.Mask=2,e.Unmask=3,e.Watch=4,e.Keep=5}(Z||(Z={})),function(e){e.Unset=0,e.Click=1}(ee||(ee={})),function(e){e.MaxLogsPerPage=1024,e.MutationProcessingInterval=250,e.CurveSamplingInterval=142,e.DefaultBundleUploadInterval=5e3,e.HeartbeatInitial=4e3,e.HeartbeatMax=256200,e.PageInactivityTimeout=18e5,e.BackoffMax=3e5,e.ScrollSampleInterval=e.MutationProcessingInterval/5,e.InactivityThreshold=4e3,e.MaxPayloadLength=16384}(te||(te={}));function ie(e,t){return function(){try{return e.apply(this,arguments)}catch(e){try{t&&t(e)}catch(e){}}}}var oe=function(){},se=navigator.userAgent,ae=se.indexOf("MSIE ")>-1||se.indexOf("Trident/")>-1,ue=(ae&&se.indexOf("Trident/5"),ae&&se.indexOf("Trident/6"),ae&&se.indexOf("rv:11")>-1),ce=se.indexOf("Edge/")>-1;se.indexOf("CriOS");var he=/^((?!chrome|android).)*safari/i.test(window.navigator.userAgent);function de(){var e=window.navigator.userAgent.match(/Version\/(\d+)/);return e?parseInt(e[1]):-1}function le(e){if(!he)return!1;var t=de();return t>=0&&t===e}function pe(e){if(!he)return!1;var t=de();return t>=0&&tt)return!1;return n==t}function pt(e,t){var n=0;for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r)&&++n>t)return!0;return!1}Ze="function"==typeof s.objectKeys?function(e){return s.objectKeys(e)}:function(e){var t=[];for(var n in e)s.objectHasOwnProp(e,n)&&t.push(n);return t},st=(ot=function(e){return e.matches||e.msMatchesSelector||e.webkitMatchesSelector})(window.Element.prototype),!(at=window.document?window.document.documentElement:void 0)||st&&at instanceof window.Element||(st=ot(at)),it=($e=[st,function(e,t){return st.call(e,t)}])[0],rt=$e[1];var ft;ut=ae?function(e){var t=e.nextSibling;return t&&e.parentNode&&t===e.parentNode.firstChild?null:t}:function(e){return e.nextSibling};var vt;ft=ae?function(e,t){if(e){var n=e.parentNode?e.parentNode.firstChild:null;do{t(e),e=e.nextSibling}while(e&&e!=n)}}:function(e,t){for(;e;e=e.nextSibling)t(e)};vt=ae?function(e){var t=e.previousSibling;return t&&e.parentNode&&t===e.parentNode.lastChild?null:t}:function(e){return e.previousSibling};function _t(e,t){if(!e)return oe;var n=function(e){try{var t=window;return t.Zone&&t.Zone.root&&"function"==typeof t.Zone.root.wrap?t.Zone.root.wrap(e):e}catch(t){return e}}(e);return t&&(n=n.bind(t)),ie(n,function(e){o("Unexpected error: "+e)})}function gt(e){var t,n=Array.prototype.toJSON,r=String.prototype.toJSON;n&&(Array.prototype.toJSON=void 0),r&&(String.prototype.toJSON=void 0);try{t=s.jsonStringify(e)}catch(e){t=mt(e)}finally{n&&(Array.prototype.toJSON=n),r&&(String.prototype.toJSON=r)}return t}function mt(e){var t="Internal error: unable to determine what JSON error was";try{t=(t=""+e).replace(/[^a-zA-Z0-9\.\:\!\, ]/g,"_")}catch(e){}return"\""+t+"\""}function yt(e){var t=e.doctype;if(!t)return"";var n=""}function wt(e){return s.jsonParse(e)}function bt(e){var t=0,n=0;return null==e.screen?[t,n]:(t=parseInt(String(e.screen.width)),n=parseInt(String(e.screen.height)),[t=isNaN(t)?0:t,n=isNaN(n)?0:n])}var St=function(){function e(e,t){this.target=e,this.propertyName=t,this._before=oe,this._afterSync=oe,this._afterAsync=oe,this.on=!1}return e.prototype.before=function(e){return this._before=_t(e),this},e.prototype.afterSync=function(e){return this._afterSync=_t(e),this},e.prototype.afterAsync=function(e){return this._afterAsync=_t(function(t){s.setWindowTimeout(window,ie(function(){e(t)}),0)}),this},e.prototype.disable=function(){if(this.on=!1,this.shim){var e=this.shim,t=e.override,n=e["native"];this.target[this.propertyName]===t&&(this.target[this.propertyName]=n,this.shim=void 0)}},e.prototype.enable=function(){if(this.on=!0,this.shim)return!0;this.shim=this.makeShim();try{this.target[this.propertyName]=this.shim.override}catch(e){return!1}return!0},e.prototype.makeShim=function(){var e=this,t=this.target[this.propertyName];return{"native":t,override:function(){var n={that:this,args:arguments,result:null};e.on&&e._before(n);var r=t.apply(this,arguments);return e.on&&(n.result=r,e._afterSync(n),e._afterAsync(n)),r}}},e}(),Et={};function Tt(e,t){if(!e||"function"!=typeof e[t])return null;var n;Et[t]=Et[t]||[];for(var r=0;r\n",t=At(n)}if(!t){e="\n";var n=[];try{for(var r=arguments.callee.caller.caller;r&&n.lengthn)return!1;var r=new Error("Assertion failed: "+t);return Mt.sendToBugsnag(r,"error"),e}function Pt(e,t,n,r){void 0!==n&&("function"==typeof e.addEventListener?e.addEventListener(t,n,r):"function"==typeof e.addListener?e.addListener(n):o("Target of "+t+" doesn't seem to support listeners"))}function qt(e,t,n,r){void 0!==n&&("function"==typeof e.removeEventListener?e.removeEventListener(t,n,r):"function"==typeof e.removeListener?e.removeListener(n):o("Target of "+t+" doesn't seem to support listeners"))}var Ut=function(){function e(){var e=this;this._listeners=[],this._children=[],this._yesCapture=!0,this._noCapture=!1;try{var t=Object.defineProperty({},"passive",{get:function(){e._yesCapture={capture:!0,passive:!0},e._noCapture={capture:!1,passive:!0}}});window.addEventListener("test",oe,t)}catch(e){}}return e.prototype.add=function(e,t,n,r,i){return void 0===i&&(i=!1),this.addCustom(e,t,n,r,i)},e.prototype.addCustom=function(e,t,n,r,i){void 0===i&&(i=!1);var o={target:e,type:t,fn:Mt.wrap(function(e){(i||!1!==e.isTrusted||"message"==t||e._fs_trust_event)&&r(e)}),options:n?this._yesCapture:this._noCapture,index:this._listeners.length};return this._listeners.push(o),Pt(e,t,o.fn,o.options),o},e.prototype.remove=function(e){e.target&&(qt(e.target,e.type,e.fn,e.options),e.target=null,e.fn=void 0)},e.prototype.clear=function(){for(var e=0;e0&&t.height>0)return this.width=t.width,void(this.height=t.height);r=this.computeLayoutViewportSizeFromMediaQueries(e),this.width=r[0],this.height=r[1]}}return e.prototype.computeLayoutViewportSizeFromMediaQueries=function(e){var t=this.findMediaValue(e,"width",this.clientWidth,this.clientWidth+128);void 0===t&&(t=this.tryToGet(e,"innerWidth")),void 0===t&&(t=this.clientWidth);var n=this.findMediaValue(e,"height",this.clientHeight,this.clientHeight+128);return void 0===n&&(n=this.tryToGet(e,"innerHeight")),void 0===n&&(n=this.clientHeight),[t,n]},e.prototype.findMediaValue=function(e,t,n,r){if(s.matchMedia){var i=s.matchMedia(e,"(min-"+t+": "+n+"px)");if(null!=i){if(i.matches&&s.matchMedia(e,"(max-"+t+": "+n+"px)").matches)return n;for(;n<=r;){var o=s.mathFloor((n+r)/2);if(s.matchMedia(e,"(min-"+t+": "+o+"px)").matches){if(s.matchMedia(e,"(max-"+t+": "+o+"px)").matches)return o;n=o+1}else r=o-1}}}},e.prototype.tryToGet=function(e,t){try{return e[t]}catch(e){return}},e}();function Yt(e,t){return new zt(e,t)}var Gt=function(e,t){this.offsetLeft=0,this.offsetTop=0,this.pageLeft=0,this.pageTop=0,this.width=0,this.height=0,this.scale=0;var n=e.document;if(n.body){var r="BackCompat"==n.compatMode;"pageXOffset"in e?(this.pageLeft=e.pageXOffset,this.pageTop=e.pageYOffset):n.scrollingElement?(this.pageLeft=n.scrollingElement.scrollLeft,this.pageTop=n.scrollingElement.scrollTop):r?(this.pageLeft=n.body.scrollLeft,this.pageTop=n.body.scrollTop):n.documentElement&&(n.documentElement.scrollLeft>0||n.documentElement.scrollTop>0)?(this.pageLeft=n.documentElement.scrollLeft,this.pageTop=n.documentElement.scrollTop):(this.pageLeft=n.body.scrollLeft||0,this.pageTop=n.body.scrollTop||0),this.offsetLeft=this.pageLeft-t.pageLeft,this.offsetTop=this.pageTop-t.pageTop;try{var i=e.innerWidth,o=e.innerHeight}catch(e){return}if(0!=i&&0!=o){this.scale=t.width/i,this.scale<1&&(this.scale=1);var s=t.width-t.clientWidth,a=t.height-t.clientHeight;this.width=i-s/this.scale,this.height=o-a/this.scale}}};var Qt,Xt=(Qt=function(e,t){return(Qt=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])})(e,t)},function(e,t){function n(){this.constructor=e}Qt(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),Jt=function(e,t,n,r){return new(n||(n=Promise))(function(i,o){function s(e){try{u(r.next(e))}catch(e){o(e)}}function a(e){try{u(r["throw"](e))}catch(e){o(e)}}function u(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n(function(e){e(t)})).then(s,a)}u((r=r.apply(e,t||[])).next())})},$t=function(e,t){var n,r,i,o,s={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:a(0),"throw":a(1),"return":a(2)},"function"==typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function a(o){return function(a){return function(o){if(n)throw new TypeError("Generator is already executing.");for(;s;)try{if(n=1,r&&(i=2&o[0]?r["return"]:o[0]?r["throw"]||((i=r["return"])&&i.call(r),0):r.next)&&!(i=i.call(r,o[1])).done)return i;switch(r=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return s.label++,{value:o[1],done:!1};case 5:s.label++,r=o[1],o=[0];continue;case 7:o=s.ops.pop(),s.trys.pop();continue;default:if(!(i=(i=s.trys).length>0&&i[i.length-1])&&(6===o[0]||2===o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]this._due)return et.resolve().then(this._wrappedTick)["catch"](function(){})},e.registry={},e.nextId=0,e.checkedAlready=!1,e.lastCheck=0,e}(),en=function(e){function t(t){var n=e.call(this)||this;return n._interval=t,n._handle=-1,n}return Xt(t,e),t.prototype.start=function(e){var t=this;-1==this._handle&&(this.setTick(function(){e(),t.register(t._interval)}),this._handle=s.setWindowInterval(window,this._wrappedTick,this._interval),this.register(this._interval))},t.prototype.cancel=function(){-1!=this._handle&&(s.clearWindowInterval(window,this._handle),this._handle=-1,this.setTick(function(){}))},t}(Zt),tn=function(e){function t(t,n,r){void 0===n&&(n=0);for(var i=[],o=3;ot&&(this._skew=e-t,this._reportTimeSkew("timekeeper set with future ts"))},e.prototype._reportTimeSkew=function(e){this._reported++<=2&&Mt.sendToBugsnag(e,"error",{skew:this._skew,startTime:this._startTime,wallTime:this.wallTime()})},e}();function on(e){var t=e;return t.tagName?"object"==typeof t.tagName?"form":t.tagName.toLowerCase():null}var sn,an,un=n(3),cn=n(0),hn=Object.defineProperty,dn=p()%1e9,ln=window.WeakMap||((sn=function(){this.name="__st"+(1e9*s.mathRandom()>>>0)+dn++ +"__"}).prototype={set:function(e,t){var n=e[this.name];n&&n[0]===e?n[1]=t:hn(e,this.name,{value:[e,t],writable:!0})},get:function(e){var t;return(t=e[this.name])&&t[0]===e?t[1]:void 0}},sn),pn=1,fn=4,vn=function(){for(var e=0,t=0,n=arguments.length;t0&&o(n[u],this._rules[u]),r[u].length>0&&o(r[u],this._consentRules[u])}},e.prototype._fallback=function(e){for(var t=0,n=e;t0&&t.length<1e4;){var n=t.pop();delete Tn[n.id],n.node._fs==n.id&&(n.node._fs=0),n.id=0,n.next&&t.push(n.next),n.child&&t.push(n.child)}Ft(t.length<1e4,"clearIds is fast")}var qn,Un=function(){function e(e,t){this._onchange=e,this._checkElem=t,this._fallback=!1,this._elems={},this.values={},this.radios={},qn=this}return e.prototype.hookEvents=function(){(function(){var e=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,"value");if(!e||!e.set)return!1;Nn||(kt(HTMLInputElement,"value",jn),kt(HTMLInputElement,"checked",jn),kt(HTMLSelectElement,"value",jn),kt(HTMLTextAreaElement,"value",jn),kt(HTMLSelectElement,"selectedIndex",jn),kt(HTMLOptionElement,"selected",jn),Nn=!0);return!0})()||(this._fallback=!0)},e.prototype.addInput=function(e){var t=Mn(e);if(this._elems[t]=e,Vn(e)){var n=Hn(e);e.checked&&(this.radios[n]=t)}else this.values[t]=Kn(e);(function(e){switch(e.type){case"checkbox":case"radio":return e.checked!=e.hasAttribute("checked");default:return(e.value||"")!=function(e){if("select"!=on(e))return e.getAttribute("value")||"";var t=e,n=t.querySelector("option[selected]")||t.querySelector("option");if(!n)return"";return n.value||""}(e);}})(e)&&this._onchange(e)},e.prototype.diffValue=function(e,t){var n=Mn(e);if(Vn(e)){var r=Hn(e);return this.radios[r]==n!=("true"==t)}return this.values[n]!=t},e.prototype.onChange=function(e,t){void 0===t&&(t=Kn(e));var n=Mn(e);if((e=this._elems[n])&&this.diffValue(e,t))if(this._onchange(e),Vn(e)){var r=Hn(e);"false"==t&&this.radios[r]==n?delete this.radios[r]:this.radios[r]=n}else this.values[n]=t},e.prototype.tick=function(){for(var e in this._elems){var t=this._elems[e];if(this._checkElem(t)){if(this._fallback){var n=Kn(t);if(t&&this.diffValue(t,n))if(this._onchange(t),Vn(t)){var r=Hn(t);this.radios[r]=+e}else this.values[e]=n}}else delete this._elems[e],delete this.values[e],Vn(t)&&delete this.radios[Hn(t)]}},e.prototype.shutdown=function(){qn=null},e.prototype._usingFallback=function(){return this._fallback},e.prototype._trackingElem=function(e){return!!this._elems[e]},e}(),Nn=!1;var Wn,Dn={};function Bn(){try{if(qn)for(var e in Dn){var t=Dn[e],n=t[0],r=t[1];qn.onChange(n,r)}}finally{Wn=null,Dn={}}}function Hn(e){if(!e)return"";for(var t=e;t&&"form"!=on(t);)t=t.parentElement;return(t&&"form"==on(t)?Mn(t):0)+":"+e.name}function jn(e,t){var n=function e(t,n){if(void 0===n&&(n=2),n<=0)return t;var r=on(t);return"option"!=r&&"optgroup"!=r||!t.parentElement?t:e(t.parentElement,n-1)}(e),r=Mn(n);r&&qn&&qn.diffValue(n,""+t)&&(Dn[r]=[n,""+t],Wn||(Wn=new tn(Bn)).start())}function Kn(e){switch(e.type){case"checkbox":case"radio":return""+e.checked;default:var t=e.value;return t||(t=""),""+t;}}function Vn(e){return e&&"radio"==e.type}var zn={};var Yn="__default";function Gn(e){void 0===e&&(e=Yn);var t=zn[e];return t||(t=function(){var e=document.implementation.createHTMLDocument("");return e.head||e.documentElement.appendChild(e.createElement("head")),e.body||e.documentElement.appendChild(e.createElement("body")),e}(),e!==Yn&&(t.open(),t.write(e),t.close()),zn[e]=t),t}var Qn=new(function(){function e(){var e=Gn(),t=e.getElementById("urlresolver-base");t||((t=e.createElement("base")).id="urlresolver-base",e.head.appendChild(t));var n=e.getElementById("urlresolver-parser");n||((n=e.createElement("a")).id="urlresolver-parser",e.head.appendChild(n)),this.base=t,this.parser=n}return e.prototype.parseUrl=function(e,t){if("undefined"!=typeof URL)try{e||(e=document.baseURI);var n=e?new URL(t,e):new URL(t);if(n.href)return n}catch(e){}return this.parseUrlUsingBaseAndAnchor(e,t)},e.prototype.parseUrlUsingBaseAndAnchor=function(e,t){this.base.setAttribute("href",e),this.parser.setAttribute("href",t);var n=document.createElement("a");return n.href=this.parser.href,n},e.prototype.resolveUrl=function(e,t){return this.parseUrl(e,t).href},e.prototype.resolveToDocument=function(e,t){var n=Jn(e);return null==n?t:this.resolveUrl(n,t)},e}());function Xn(e,t){return Qn.parseUrl(e,t)}function Jn(e){var t=e.document,n=e.location.href;if("string"==typeof t.baseURI)n=t.baseURI;else{var r=t.getElementsByTagName("base")[0];r&&r.href&&(n=r.href)}return"about:blank"==n&&e.parent!=e?Jn(e.parent):n}var $n=new RegExp("[^\\s]"),Zn=new RegExp("[\\s]*$");String.prototype;function er(e){var t=$n.exec(e);if(!t)return e;for(var n=t.index,r=(t=Zn.exec(e))?e.length-t.index:0,i="\uFFFF",o=e.slice(n,e.length-r).split(/\r\n?|\n/g),s=0;sir?(Mt.sendToBugsnag("Ignoring huge text node","warning",{length:r}),""):e.parentNode&&"style"==on(e.parentNode)?n:t.mask?er(n):n}function sr(e){return tr[e]||e.toLowerCase()}function ar(e,t,n,r){var i,o=on(t);if(null===o)return null;var s=function(e){var t,r,s;i=null!==(r=null===(t=nr[e][o])||void 0===t?void 0:t[n])&&void 0!==r?r:null===(s=nr[e]["*"])||void 0===s?void 0:s[n]};if(s("Any"),void 0===i){var a=xn(t);if(!a)return null;a.watchKind==an.Exclude?s("Exclude"):a.mask&&s("Mask")}if(void 0===i)return r;switch(i){case"erase":return null;case"scrubUrl":return ur(r,e,{source:"dom",type:o});case"maskText":return er(r);default:return Object(cn.assertExhaustive)(i);}}function ur(e,t,n){switch(n.source){case"dom":switch(r=n.type){case"frame":case"iframe":return hr(e,t);default:return cr(e,t);}case"event":switch(r=n.type){case R.AJAX_REQUEST:case R.NAVIGATE:return cr(e,t);case R.SET_FRAME_BASE:return hr(e,t);default:return Object(cn.assertExhaustive)(r);}case"log":return hr(e,t);case"page":var r;switch(r=n.type){case"base":return hr(e,t);case"referrer":case"url":return cr(e,t);default:return Object(cn.assertExhaustive)(r);}case"perfEntry":switch(n.type){case"frame":case"iframe":case"navigation":case"other":return hr(e,t);default:return cr(e,t);}default:return Object(cn.assertExhaustive)(n);}}function cr(e,t){return lr(e,t,function(e){if(!(e in pr)){var t=["password","token","^jwt$"];switch("4K3FQ"!==e&&"NQ829"!==e&&"KCF98"!==e&&t.push("^code$"),e){case"2FVM4":t.push("^e$","^eref$","^fn$");break;case"35500":t.push("share_token","password-reset-key");break;case"1HWDJ":t.push("email_id","invite","join");break;case"J82WF":t=[".*"];break;case"8MM83":t=["^creditCard"];break;case"PAN8Z":t.push("code","hash","ol","aeh");break;case"BKP05":t.push("api_key","session_id","encryption_key");break;case"QKM7G":t.push("postcode","encryptedQuoteId","registrationId","productNumber","customerName","agentId","qqQuoteId");break;case"FP60X":t.push("phrase");break;case"GDWG7":t=["^(?!productType|utmSource).*$"];break;case"RV68C":t.push("drivingLicense");break;case"S3VEC":t.push("data");break;case"Q8RZE":t.push("myLowesCardNumber");}pr[e]=new RegExp(t.join("|"),"i")}return pr[e]}(t))}function hr(e,t){return lr(e,t,fr)}function dr(e,t,n,r){var i=new RegExp("(\\/"+t+"\\/).*$","i");n==r&&e.pathname.indexOf(t)>=0&&(e.pathname=e.pathname.replace(i,"$1"+rr))}function lr(e,t,n){var r=Xn("",e);return r.hash&&r.hash.indexOf("access_token")>=0&&(r.hash="#"+rr),dr(r,"visitor",t,"QS8RG"),dr(r,"account",t,"QS8RG"),dr(r,"parentAccount",t,"QS8RG"),dr(r,"reset_password",t,"AGQFM"),dr(r,"reset-password",t,"95NJ7"),dr(r,"dl",t,"RV68C"),dr(r,"retailer",t,"FP60X"),dr(r,"ocadotech",t,"FP60X"),dr(r,"serviceAccounts",t,"FP60X"),dr(r,"signup",t,"7R98D"),r.search&&r.search.length>0&&(r.search=function(e,t){return e.split("?").map(function(e){return function(e,t){return e.replace("?","").split("&").map(function(e){return e.split("=")}).map(function(e){var n=e[0],r=e[1],i=e.slice(2);return t.test(n)&&void 0!==r?[n,rr].concat(i):[n,r].concat(i)}).map(function(e){var t=e[0],n=e[1],r=e.slice(2);return void 0===n?t:[t,n].concat(r).join("=")}).join("&")}(e,t)}).join("?")}(r.search,n)),r.href.substring(0,2048)}var pr={};var fr=new RegExp(".*","i");var vr=/([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)/gi,_r=/(?:(http)|(ftp)|(file))[s]?:\/\/(?:[a-zA-Z]|[0-9]|[$-_@.&+#]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+/gi;function gr(e){return"function"==typeof(t=e.constructor)&&Function.prototype.toString.call(t).indexOf("[native code]")>-1;var t}var mr=function(){function e(e,t,n){this._watcher=e,this._resizer=t,this._orgId=n,Tn={},kn=1}return e.prototype.tokenizeNode=function(e,t,n,r,i,o,s){var a=this,u=xn(t),c=xn(n),h=[];return function(e){var t=kn;try{return e(),!0}catch(e){return kn=t,!1}}(function(){a.tokeNode(e,u,c,r,h,i,o,s)})||(h=[]),h},e.prototype.tokeNode=function(e,t,n,r,i,o,s,a){for(var u=[{parentMirror:t,nextMirror:n,node:r}],c=function(){var t=u.pop();if(!t)return"continue";if("string"==typeof t)return i.push(t),"continue";var n=t.parentMirror,r=t.nextMirror,c=t.node,d=h._encodeTagAndAttributes(e,n,r,c,i,o,s);if(null==d||d.watchKind===an.Exclude)return"continue";var l=c.nodeType===pn?c.shadowRoot:null;return(l||c.firstChild)&&a(d)?(u.push("]"),function(e,t){if(!e)return;var n=[];ft(e,function(e){return n.push(e)});for(;n.length>0;){var r=n.pop();r&&t(r)}}(c.firstChild,function(e){u.push({parentMirror:d,nextMirror:null,node:e})}),l&&u.push({parentMirror:d,nextMirror:null,node:l}),void u.push("[")):"continue"},h=this;u.length;)c()},e.prototype._encodeTagAndAttributes=function(e,t,n,r,i,o,s){if("script"==on(r)||8==r.nodeType)return null;var a,u,c,h,d=function(e){return e.constructor===window.ShadowRoot}(r),l=function(e){var t={id:kn++,node:e};return Tn[t.id]=t,e._fs=t.id,t}(r);if((d||(null==t?void 0:t.isInShadowDOM))&&(l.isInShadowDOM=!0),t&&(d?t.shadow=l:(a=t,u=l,c=n,h=this._resizer,Fn(u,h),u.parent=a,u.next=c,c&&(u.prev=c.prev,c.prev=u),null==u.next?(u.prev=a.lastChild,a.lastChild=u):u.next.prev=u,null==u.prev?a.child=u:u.prev.next=u)),10==r.nodeType){var p=r;return i.push("1?s.push([o.idx,a]):s.push(o.idx):s.push(i)}for(n=1;n1e3)return null;if(!n||n.nodeType!=pn)return null;var r=n;if(getComputedStyle(r).display.indexOf("inline")<0)return r;n=n.parentNode}},t}(Er),kr=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return br(t,e),t.prototype.observe=function(e){var t=this;if(e&&e.nodeType==pn){var n=e;this.growWatchedIndex(xn(e)),this._ctx.measurer.requestMeasureTask(function(){t.addEntry(n)})}},t.prototype.unobserveSubtree=function(e){var t=xn(e);t&&this.clearWatchedIndex(t)},t.prototype.nodeChanged=function(e){var t=this,n=this.relatedWatched(e);this._ctx.measurer.requestMeasureTask(function(){for(var e=0,r=n;e0){var r=zr(t[n-1],e);if(r)return void(t[n-1]=r)}else!function(e){qr.push(e),Pr||(Pr=!0,Mr(Ur))}(this.observer);t[n]=e},addListeners:function(){this.addListeners_(this.target)},addListeners_:function(e){var t=this.options;t.attributes&&e.addEventListener("DOMAttrModified",this.handleEventBound,!0),t.characterData&&e.addEventListener("DOMCharacterDataModified",this.handleEventBound,!0),t.childList&&e.addEventListener("DOMNodeInserted",this.handleEventBound,!0),(t.childList||t.subtree)&&e.addEventListener("DOMNodeRemoved",this.handleEventBound,!0)},removeListeners:function(){this.removeListeners_(this.target)},removeListeners_:function(e){var t=this.options;t.attributes&&e.removeEventListener("DOMAttrModified",this.handleEventBound,!0),t.characterData&&e.removeEventListener("DOMCharacterDataModified",this.handleEventBound,!0),t.childList&&e.removeEventListener("DOMNodeInserted",this.handleEventBound,!0),(t.childList||t.subtree)&&e.removeEventListener("DOMNodeRemoved",this.handleEventBound,!0)},addTransientObserver:function(e){if(e!==this.target){this.addListeners_(e),this.transientObservedNodes.push(e);var t=Or.get(e);t||Or.set(e,t=[]),t.push(this)}},removeTransientObservers:function(){var e=this.transientObservedNodes;this.transientObservedNodes=[],e.forEach(function(e){this.removeListeners_(e);for(var t=Or.get(e),n=0;n0||this._toRefresh.length>0){var n={},r={};for(var i in this.processRecords(e,t,r,n),r){var o=i.split("\t");t.push({Kind:R.MUT_ATTR,When:e,Args:[parseInt(o[0]),o[1],r[i]]})}for(var i in n)t.push({Kind:R.MUT_TEXT,When:e,Args:[parseInt(i),n[i]]})}var s=this._newShadowContainers;this._newShadowContainers=[];for(var a=0;a0)for(var d=0;d0){s[h]=c.target;var p=Jr(c.target);p&&(s[p.id]=p.node)}break;case"characterData":Cn(c.target)||c.oldValue!=c.target.textContent&&(r[h]=or(c.target));break;case"attributes":var f=An(w=c.target);if(_n(this._watcher.isWatched(w))>_n(f)){a(w);break}var v=Xr(c.attributeNamespace)+(c.attributeName||""),_=sr(v);if(w.hasAttribute(v)){var g=c.target.getAttribute(v);c.oldValue!=g&&(g=ar(this._ctx.options.orgId,c.target,_,g||""),this._attrVisitor(c.target,_,g||""),null!==g&&(n[h+"\t"+_]=g))}else n[h+"\t"+_]=null;}}catch(e){}for(var m=0,y=this._toRefresh;m0&&n.push({When:t,Kind:R.MUT_SHADOW,Args:[o,this._compress?this._lz.encode(s):s]})},e.prototype.genInsert=function(e,t,n,r,i,o){var s=Mn(r)||-1,a=Mn(o)||-1,u=-1===s&&-1===a,c=p(),h=this.genDocStream(e,r,i,o),d=p()-c;if(h.length>0){var l=p(),f=this._compress?this._lz.encode(h):h,v=p()-l;n.push({When:t,Kind:R.MUT_INSERT,Args:[s,a,f]},{When:t,Kind:R.TIMING,Args:[[O.Internal,A.Serialization,u?x.DomSnapshot:x.NodeEncoding,t,d,[x.LzEncoding,v]]]})}},e.prototype.genDocStream=function(e,t,n,r){var i=this;if(t&&Cn(t))return[];for(var o=[],s=this._encoder.tokenizeNode(e,t,r,n,function(e){if(e.nodeType==pn){var t=e;t.shadowRoot&&i.observe(t.shadowRoot)}i._nodeVisitor(e,o)},this._attrVisitor,this._visitChildren),a=0,u=o;a0){var i=t[t.length-1];if(i.Kind==R.MUT_REMOVE)return void i.Args.push(r)}t.push({When:e,Kind:R.MUT_REMOVE,Args:[r]})},e.prototype.setUpIEWorkarounds=function(){var t=this;if(ue){var n=Object.getOwnPropertyDescriptor(Node.prototype,"textContent"),r=n&&n.set;if(!n||!r)throw new Error("Missing textContent setter -- not safe to record mutations.");Object.defineProperty(Element.prototype,"textContent",Gr(Gr({},n),{set:function(e){try{for(var t=void 0;t=this.firstChild;)this.removeChild(t);if(null===e||""==e)return;var n=(this.ownerDocument||document).createTextNode(e);this.appendChild(n)}catch(t){r&&r.call(this,e)}}}))}this._setPropertyThrottle=new nn(e.ThrottleMax,e.ThrottleInterval,function(){return new tn(function(){t._setPropertyWasThrottled=!0,t.tearDownIEWorkarounds()}).start()});var i=this._setPropertyThrottle.guard(function(e){e.cssText=e.cssText});this._setPropertyThrottle.open(),this._setPropertyHook=Tt(CSSStyleDeclaration.prototype,"setProperty"),this._setPropertyHook&&this._setPropertyHook.afterSync(function(e){i(e.that)}),this._removePropertyHook=Tt(CSSStyleDeclaration.prototype,"removeProperty"),this._removePropertyHook&&this._removePropertyHook.afterSync(function(e){i(e.that)})},e.prototype.tearDownIEWorkarounds=function(){this._setPropertyThrottle&&this._setPropertyThrottle.close(),this._setPropertyHook&&this._setPropertyHook.disable(),this._removePropertyHook&&this._removePropertyHook.disable()},e.prototype.updateConsent=function(){var e=this,t=xn(this._root);t&&function(e,t){for(var n=[e];n.length;){var r=n.pop();if(r){t(r);for(var i=r.child,o=r.shadow;i;)n.push(i),i=i.next;o&&n.push(o)}}}(t,function(t){var n=t.node;t.matchesAnyConsentRule&&e.refreshElement(n)})},e.prototype.refreshElement=function(e){Mn(e)&&this._toRefresh.push(e)},e.ThrottleMax=1024,e.ThrottleInterval=1e4,e}();function Xr(e){return void 0===e&&(e=""),null===e?"":{"http://www.w3.org/1999/xlink":"xlink:","http://www.w3.org/XML/1998/namespace":"xml:","http://www.w3.org/2000/xmlns/":"xmlns:"}[e]||""}function Jr(e){return!(null==e?void 0:e.shadowRoot)||gr(e.shadowRoot)?null:xn(e.shadowRoot)}var $r=["navigationStart","unloadEventStart","unloadEventEnd","redirectStart","redirectEnd","fetchStart","domainLookupStart","domainLookupEnd","connectStart","connectEnd","secureConnectionStart","requestStart","responseStart","responseEnd","domLoading","domInteractive","domContentLoadedEventStart","domContentLoadedEventEnd","domComplete","loadEventStart","loadEventEnd"],Zr=["name","startTime","duration","initiatorType","redirectStart","redirectEnd","fetchStart","domainLookupStart","domainLookupEnd","connectStart","connectEnd","secureConnectionStart","requestStart","responseStart","responseEnd","unloadEventStart","unloadEventEnd","domInteractive","domContentLoadedEventStart","domContentLoadedEventEnd","domComplete","loadEventStart","loadEventEnd","type","redirectCount","decodedBodySize","encodedBodySize","transferSize"],ei=["name","startTime","duration","initiatorType","redirectStart","redirectEnd","fetchStart","domainLookupStart","domainLookupEnd","connectStart","connectEnd","secureConnectionStart","requestStart","responseStart","responseEnd","decodedBodySize","encodedBodySize","transferSize"],ti=["name","startTime","duration"],ni=["jsHeapSizeLimit","totalJSHeapSize","usedJSHeapSize"],ri=function(){function e(e,t,n){this._ctx=e,this._queue=t,this._perfSupported=!1,this._timingSupported=!1,this._getEntriesSupported=!1,this._memorySupported=!1,this._lastUsedJSHeapSize=0,this._gotLoad=!1,this._observer=null,this._observedBatches=[];var r=window.performance;r&&(this._perfSupported=!0,r.timing&&(this._timingSupported=!0),r.memory&&(this._memorySupported=!0),"function"==typeof r.getEntries&&(this._getEntriesSupported=!0),this._listeners=n.createChild())}return e.prototype.start=function(e){var t=this;this._resourceUploader=e;var n=window.performance;n&&(this._ctx.recording.inFrame||this._queue.enqueue({Kind:R.REC_FEAT_SUPPORTED,Args:[Y.Performance,this._timingSupported,Y.PerformanceEntries,this._getEntriesSupported,Y.PerformanceMemory,this._memorySupported,Y.PerformanceObserver,!!window.PerformanceObserver]}),this.observe(),!this._observer&&n.addEventListener&&n.removeEventListener&&this._listeners.add(n,"resourcetimingbufferfull",!0,function(){t._queue.enqueue({Kind:R.RESOURCE_TIMING_BUFFER_FULL,Args:[]})}),this.checkMemory())},e.prototype.onLoad=function(){this._gotLoad||(this._gotLoad=!0,this._timingSupported&&(this.recordTiming(performance.timing),this.checkForNewEntries()))},e.prototype.tick=function(e){this.checkMemory(),e&&this.checkForNewEntries()},e.prototype.shutdown=function(){this._listeners&&this._listeners.clear(),this._resourceUploader=void 0;var e=[];this._observer?(this._observer.takeRecords&&(e=this._observer.takeRecords()),this._observer.disconnect()):window.performance&&window.performance.getEntries&&(e=window.performance.getEntries()),e.length>300&&(e=e.slice(0,300),this._queue.enqueue({Kind:R.RESOURCE_TIMING_BUFFER_FULL,Args:[]})),this._observedBatches.push(e),this.tick(!0)},e.prototype.observe=function(){var e=this;if(!this._observer&&this._getEntriesSupported&&window.PerformanceObserver){this._observedBatches.push(performance.getEntries()),this._observer=new window.PerformanceObserver(function(t){var n=t.getEntries();e._observedBatches.push(n)});var t=["navigation","resource","measure","mark"];window.PerformancePaintTiming&&t.push("paint"),this._observer.observe({entryTypes:t})}},e.prototype.checkMemory=function(){if(this._memorySupported&&!this._ctx.recording.inFrame){var e=performance.memory;if(e){var t=e.usedJSHeapSize-this._lastUsedJSHeapSize;(0==this._lastUsedJSHeapSize||s.mathAbs(t/this._lastUsedJSHeapSize)>.2)&&(this.addPerfEvent(z.Memory,e,ni),this._lastUsedJSHeapSize=e.usedJSHeapSize)}}},e.prototype.recordEntry=function(e){switch(e.entryType){case"navigation":this.recordNavigation(e);break;case"resource":this.recordResource(e);break;case"paint":this.recordPaint(e);break;case"measure":this.recordMeasure(e);break;case"mark":this.recordMark(e);}},e.prototype.checkForNewEntries=function(){if(this._perfSupported&&this._getEntriesSupported){var e=this._observedBatches;this._observedBatches=[];for(var t=0,n=e;t=t&&(n=void 0),o[o.length-1]--,n&&n!==si&&r&&(o.push(s.objectKeys(n).length),a.push(u));o[o.length-1]<=0;)o.pop(),a.pop();return n})}catch(e){}return"[error serializing "+e.constructor.name+"]"}}var ui=function(){function e(e){this._requestTracker=e}return e.prototype.disable=function(){this._hook&&(this._hook.disable(),this._hook=null)},e.prototype.enable=function(e){var t,n=this,r=I(e),i=null===(t=null==r?void 0:r._w)||void 0===t?void 0:t.fetch;(i||e.fetch)&&(this._hook=Tt(i?r._w:e,"fetch"),this._hook&&this._hook.afterSync(function(e){return n.recordFetch(e.that,e.result,e.args[0],e.args[1])}))},e.prototype.recordFetch=function(e,t,n,r){var i,o="GET",s="",a={};if("string"==typeof n?s=n:"url"in n?(s=n.url,o=n.method,i=n.body,n.headers&&n.headers.forEach(function(e,t){a[e]=t})):s=""+n,r){o=r.method||o;var u=r.headers;if(u)if(nt(u))for(var c=0,h=u;c-1;n&&o?t.clone().text().then(Mt.wrap(function(i){var o=mi(i,n),s=o[0],a=o[1];r.onComplete(e,t,s,a)}))["catch"](Mt.wrap(function(n){r.onComplete(e,t,-1,void 0)})):r.onComplete(e,t,-1,void 0)}))["catch"](Mt.wrap(function(t){r.onComplete(e,t,-1,void 0)}))},e.prototype.onComplete=function(e,t,n,r){var i=this,o=-1,s="";if("headers"in t){o=t.status;s=this.serializeFetchHeaders(t.headers,function(e){return i._requestTracker.isHeaderInResponseHeaderWhitelist(e[0])})}return this._requestTracker.onComplete(e,s,o,n,r)},e.prototype.serializeFetchHeaders=function(e,t){var n="";return e.forEach(function(e,r){r=r.toLowerCase();var i=t([r,e]);n+=r+(i?": "+e:"")+di}),n},e}(),ci=function(){function e(e){this._requestTracker=e}return e.prototype.disable=function(){this._xhrOpenHook&&(this._xhrOpenHook.disable(),this._xhrOpenHook=null),this._xhrSetHeaderHook&&(this._xhrSetHeaderHook.disable(),this._xhrSetHeaderHook=null)},e.prototype.enable=function(e){var t,n=this,r=I(e),i=(null===(t=null==r?void 0:r._w)||void 0===t?void 0:t.XMLHttpRequest)||e.XMLHttpRequest;if(i){var o=i.prototype;this._xhrOpenHook=Tt(o,"open"),this._xhrOpenHook&&this._xhrOpenHook.before(function(e){var t=e.args[0],r=e.args[1];n._requestTracker.addPendingReq(e.that,t,r),e.that.addEventListener("load",Mt.wrap(function(t){n.onComplete(e.that)})),e.that.addEventListener("error",Mt.wrap(function(t){n.onComplete(e.that)}))}),this._xhrSendHook=Tt(o,"send"),this._xhrSendHook&&this._xhrSendHook.before(function(e){var t=e.args[0];n._requestTracker.addRequestBody(e.that,t)}),this._xhrSetHeaderHook=Tt(o,"setRequestHeader"),this._xhrSetHeaderHook&&this._xhrSetHeaderHook.before(function(e){var t=e.args[0],r=e.args[1];n._requestTracker.addHeader(e.that,t,r)})}},e.prototype.onComplete=function(e){var t=this,n=this.responseBody(e),r=n[0],i=n[1],o=Ei(function(e){var t=[];return e.split(di).forEach(function(e){var n=e.indexOf(":");-1!=n?t.push([e.slice(0,n).trim(),e.slice(n+1,e.length).trim()]):t.push([e.trim(),null])}),t}(e.getAllResponseHeaders()),function(e){return t._requestTracker.isHeaderInResponseHeaderWhitelist(e[0])});return this._requestTracker.onComplete(e,o,e.status,r,i)},e.prototype.responseBody=function(e){var t=this._requestTracker.pendingReq(e);if(!t)return[-1,void 0];var n=this._requestTracker.getRspWhitelist(t.url);if(e.responseType){var r=e.response;switch(r||o("Maybe response type was different that expected."),e.responseType){case"text":return mi(e.responseText,n);case"json":return function(e,t){if(!e)return[-1,void 0];return[yi(e),ai(e,te.MaxPayloadLength,t)]}(r,n);case"arraybuffer":return function(e,t){return[e?e.byteLength:-1,t?"[ArrayBuffer]":void 0]}(r,n);case"blob":return function(e,t){return[e?e.size:-1,t?"[Blob]":void 0]}(r,n);case"document":return function(e,t){return[-1,t?"[Document]":void 0]}(0,n);}}return mi(e.responseText,n)},e}();var hi,di="\r\n",li=["a-im","accept","accept-charset","accept-encoding","accept-language","accept-datetime","access-control-request-method,","access-control-request-headers","cache-control","connection","content-length","content-md5","content-type","date","expect","forwarded","from","host","if-match","if-modified-since","if-none-match","if-range","if-unmodified-since","max-forwards","origin","pragma","range","referer","te","user-agent","upgrade","via","warning"],pi=["access-control-allow-origin","access-control-allow-credentials","access-control-expose-headers","access-control-max-age","access-control-allow-methods","access-control-allow-headers","accept-patch","accept-ranges","age","allow","alt-svc","cache-control","connection","content-disposition","content-encoding","content-language","content-length","content-location","content-md5","content-range","content-type","date","delta-base","etag","expires","im","last-modified","link","location","permanent","p3p","pragma","proxy-authenticate","public-key-pins","retry-after","permanent","server","status","strict-transport-security","trailer","transfer-encoding","tk","upgrade","vary","via","warning","www-authenticate","x-frame-options"],fi={BM7A6:["x-b3-traceid"],KD87S:["transactionid"],NHYJM:["x-att-conversationid"],GBNRN:["x-trace-id"],R16RC:["x-request-id"],DE9CX:["x-client","x-client-id","ot-baggage-original-client","x-req-id","x-datadog-trace-id","x-datadog-parent-id","x-datadog-sampling-priority"]},vi={"thefullstory.com":["x-cloud-trace-context"],TN1:["x-cloud-trace-context"],KD87S:["transactionid"],PPE96:["x-b3-traceid"],HWT6H:["x-b3-traceid"],PPEY7:["x-b3-traceid"],PPK3W:["x-b3-traceid"],NHYJM:["x-att-conversationid"],GBNRN:["x-trace-id"],NK5T9:["traceid","requestid"]},_i=function(){function e(e,t){this._ctx=e,this._queue=t,this._enabled=!1,this._tracker=new gi(e,t),this._xhr=new ci(this._tracker),this._fetch=new ui(this._tracker)}return e.prototype.isEnabled=function(){return this._enabled},e.prototype.enable=function(e){this._enabled||(this._enabled=!0,this._queue.enqueue({Kind:R.REC_FEAT_SUPPORTED,Args:[Y.Ajax,!0,Y.AjaxFetch,!!e]}),this._xhr.enable(this._ctx.window),e&&this._fetch.enable(this._ctx.window))},e.prototype.disable=function(){this._enabled&&(this._enabled=!1,this._xhr.disable(),this._fetch.disable())},e.prototype.tick=function(e){this._tracker.tick(e)},e.prototype.setWatches=function(e){this._tracker.setWatches(e)},e}(),gi=function(){function e(e,t){this._ctx=e,this._queue=t,this._reqHeaderWhitelist={},this._rspHeaderWhitelist={},this._pendingReqs={},this._events=[],this._curId=1,this.addHeaderWhitelist(li,pi),this.addHeaderWhitelist(fi[e.options.orgId],vi[e.options.orgId])}return e.prototype.getReqWhitelist=function(e){var t=this.findWhitelistIndexFor(e);return t>=0&&this._reqWhitelist[t]},e.prototype.getRspWhitelist=function(e){var t=this.findWhitelistIndexFor(e);return t>=0&&this._rspWhitelist[t]},e.prototype.isHeaderInRequestHeaderWhitelist=function(e){return e in this._reqHeaderWhitelist},e.prototype.isHeaderInResponseHeaderWhitelist=function(e){return e in this._rspHeaderWhitelist},e.prototype.pushEvent=function(e){this._events.push(e)},e.prototype.setWatches=function(e){var t=this,n=[];this._reqWhitelist=[],this._rspWhitelist=[],e.forEach(function(e){n.push(e.URLRegex),t._reqWhitelist.push(Si(e.RecordReq,e.ReqWhitelist)),t._rspWhitelist.push(Si(e.RecordRsp,e.RspWhitelist))}),this._reqBodyRegex=new RegExp("("+n.join(")|(")+")")},e.prototype.addHeaderWhitelist=function(e,t){var n=this;e&&e.forEach(function(e){return n._reqHeaderWhitelist[e]=!0}),t&&t.forEach(function(e){return n._rspHeaderWhitelist[e]=!0})},e.prototype.tick=function(e){if(e){for(var t=0;te.MaxRuleBytes&&(o("CSSRule too large, inserting dummy instead: "+n.length),n="dummy {}"),this.withEventQueueForSheet(t,function(e){return e.enqueue({Kind:R.CSSRULE_INSERT,Args:"number"==typeof r?[i,[n],r]:[i,[n]]})}))},e.prototype.addDelete=function(e,t){var n=Fi(e,G.Node);n&&this.withEventQueueForSheet(e,function(e){return e.enqueue({Kind:R.CSSRULE_DELETE,Args:[n,t]})})},e.prototype.onDisableSheet=function(e,t){var n=Fi(e,G.Node);n&&this.withEventQueueForSheet(e,function(e){return e.enqueue({Kind:R.DISABLE_STYLESHEET,Args:[n,!!t]})})},e.prototype.withEventQueueForSheet=function(e,t){if(e.ownerNode)return n=this.ctx,r=e.ownerNode,i=t,void((o=I(Ai(r)||n.window))&&"function"==typeof o._withEventQueue&&o._withEventQueue(n.recording.pageSignature(),function(e){i({enqueue:function(t){Ft(null!=e,Ri)&&e.enqueue(t)},enqueueFirst:function(t){Ft(null!=e,Ri)&&e.enqueueFirst(t)}}),e=null}));var n,r,i,o;t(this.queue)},e.prototype.stop=function(){this.throttle.close();for(var e=0,t=this.hooks;e0&&i[i.length-1])&&(6===o[0]||2===o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]-1)return[n];if("srcset"==t&&("img"==i||"source"==i)){return null!=n.match(/^\s*$/)?[]:n.split(",").map(function(e){return e.trim().split(/\s+/)[0]})}var o=e;if("style"==t&&o.style){var s=o.style.backgroundImage;if(!s)return[];if(s.length>300)return[];var a=[],u=void 0;for(jt.lastIndex=0;u=jt.exec(s);){var c=u[1];c&&a.push(c.trim())}return a}return[]}(e,t,n);r5e5)){var n=ki(Ti(e));if(n){if(n.length>0&&Li.test(t))return 0;var r,i=Gn();ae?(r=i.createElement("style")).textContent=e.textContent:r=i.importNode(e,!0),i.head.appendChild(r);var o=ki(Ti(r));if(i.head.removeChild(r),o)return n.length>o.length?o.length:void 0}}}(o);void 0!==s&&t.push(function(){n._styleSheetWatcher.snapshotEl(o,s)});break;default:"#"!==e.nodeName[0]&&e.nodeName.indexOf("-")>-1&&this._customElementWatcher.onCustomNodeVisited(e);}if("scrollLeft"in e&&"scrollTop"in e){var a=e;this._ctx.measurer.requestMeasureTask(function(){0==a.scrollLeft&&0==a.scrollTop||n.addScroll(a)})}},e.prototype.isSafePointerEvent=function(e){var t=Ki(e);return!!Mn(t)&&!Cn(t)},e.prototype.addMouseMove=function(e){var t=Mn(Ki(e));this._queue.enqueue({Kind:R.MOUSEMOVE,Args:t?[e.clientX,e.clientY,t]:[e.clientX,e.clientY]})},e.prototype.addMouseDown=function(e){this._queue.enqueue({Kind:R.MOUSEDOWN,Args:[e.clientX,e.clientY]})},e.prototype.addMouseUp=function(e){this._queue.enqueue({Kind:R.MOUSEUP,Args:[e.clientX,e.clientY]})},e.prototype.addTouchEvent=function(e,t){if(void 0!==e.changedTouches)for(var n=0;n0)return n[0]}}return e.target}var Vi=/^\s*at .*(\S+\:\d+|native|())/m,zi=/^(eval@)?(\[native code\])?$/;function Yi(e){if(!e||"string"!=typeof e.stack)return[];var t=e;return t.stack.match(Vi)?t.stack.split("\n").filter(function(e){return!!e.match(Vi)}).map(function(e){e.indexOf("(eval ")>-1&&(e=e.replace(/eval code/g,"eval").replace(/(\(eval at [^\()]*)|(\)\,.*$)/g,""));var t=e.replace(/^\s+/,"").replace(/\(eval code/g,"(").replace(/\(native code\)/,"").split(/\s+/).slice(1),n=Qi(t.pop());return Gi(t.join(" "),["eval",""].indexOf(n[0])>-1?"":n[0],n[1],n[2])}):function(e){return e.split("\n").filter(function(e){return!e.match(zi)}).map(function(e){if(e.indexOf(" > eval")>-1&&(e=e.replace(/ line (\d+)(?: > eval line \d+)* > eval\:\d+\:\d+/g,":$1")),-1===e.indexOf("@")&&-1===e.indexOf(":"))return[e,"",-1,-1];var t=e.split("@"),n=Qi(t.pop());return Gi(t.join("@"),n[0],n[1],n[2])})}(t.stack)}function Gi(e,t,n,r){return[e||"",t||"",parseInt(n||"-1"),parseInt(r||"-1")]}function Qi(e){if(!e||-1===e.indexOf(":"))return["","",""];var t=/(.+?)(?:\:(\d+))?(?:\:(\d+))?$/.exec(e.replace(/[\(\)]/g,""));return t?[t[1]||"",t[2]||"",t[3]||""]:["","",""]}var Xi=function(){for(var e=0,t=0,n=arguments.length;t0&&"string"==typeof e.nodeName}(t)?function(e){return e.toString()}(t):void 0===t?"undefined":"object"!=typeof t||null==t?t:t instanceof Error?t.stack||t.name+": "+t.message:void 0;if(void 0!==o)return void 0===(o=s.jsonStringify(o))?0:("\""==o[0]&&(o=to(o,n,"...\"")),o.length<=n?(i.tokens.push(o),o.length):0);if(i.cyclic){i.opath.splice(r);var a=i.opath.lastIndexOf(t);if(a>-1){var u="";return u="\""+to(u,n-2)+"\"",i.tokens.push(u),u.length}i.opath.push(t)}var c=n,h=function(e){return c>=e.length&&(c-=e.length,i.tokens.push(e),!0)},d=function(e){","==i.tokens[i.tokens.length-1]?i.tokens[i.tokens.length-1]=e:h(e)};if(c<2)return 0;if(nt(t)){h("[");for(var l=0;l0;l++){var p=e(t[l],c-1,r+1,i);if(c-=p,0==p&&!h("null"))break;h(",")}d("]")}else{h("{");var f=Ze(t);for(l=0;l0;l++){var v=f[l],_=t[v];if(!h("\""+v+"\":"))break;if(0==(p=e(_,c-1,r+1,i))){i.tokens.pop();break}c-=p,h(",")}d("}")}return n==1/0?1:n-c}(e,t,0,i);var o=i.tokens.join("");return r?function(e,t){var n=t.replace(vr,"");return n=n.replace(_r,function(t){return ur(t,e,{source:"log",type:"debug"})})}(n,o):o}catch(e){return mt(e)}}function Zi(e,t){var n=0;try{s.jsonStringify(e,function(e,r){if(n++>t)throw"break";if("object"==typeof r)return r})}catch(e){return"break"!=e}return!1}var eo=function(e){return isNaN(e)?"Invalid Date":e.toUTCString()},to=function(e,t,n){return void 0===n&&(n="..."),e.length<=t?e:e.length<=n.length||t<=n.length?e.substring(0,t):e.substring(0,t-n.length)+n};var no=function(){for(var e=0,t=0,n=arguments.length;tthis._curveEndMs&&(this._curveEndMs=e.When),this._evts.push(e)},e.prototype.finish=function(e,t){void 0===t&&(t=[]);var n=this._evts.length;if(n<=1)return!1;for(var r=no([this._curveEndMs],t),i=this._evts[0].When,o=this._evts[n-1].When,s=0;s0?this._lastWhen:this._ctx.time.now();this.enqueueAt(t,e),Zt.checkForBrokenSchedulers()},e.prototype.enqueueAt=function(e,t){if(!this._recordingDisabled){e0){var t=e;t.When=this._eventQueue[0].When,this._eventQueue.unshift(t)}else this.enqueue(e)},e.prototype.addUnload=function(e){this._gotUnload||(this._gotUnload=!0,this.enqueue({Kind:R.UNLOAD,Args:[e]}),this.singSwanSong())},e.prototype.shutdown=function(e){this._flush(),this.addUnload(e),this._flush(),this._recordingDisabled=!0,this.stopPipeline()},e.prototype._flush=function(){this.processEvents(),this._transport.flush()},e.prototype.singSwanSong=function(){this._recordingDisabled||(this.processEvents(),this._transport.singSwanSong())},e.prototype.rebaseIframe=function(e){for(var t=0,n=this._eventQueue.length;t0){var d=h[h.length-1].Args[2];if(d)h[0].Args[9]=d}}for(var l in o){o[p=parseInt(l)].finish(R.SCROLL_LAYOUT_CURVE,[p])}for(var l in s){s[p=parseInt(l)].finish(R.SCROLL_VISUAL_OFFSET_CURVE,[p])}for(var l in i){var p;i[p=parseInt(l)].finish(R.TOUCHMOVE_CURVE,[p])}return t&&t.finish(R.RESIZE_VISUAL_CURVE),n}(t);e||(n=n.concat(this._gatherExternalEvents(0!=n.length))),this.ensureFrameIds(n),0!=n.length&&this._transport.enqueueEvents(this._ctx.recording.pageSignature(),n)}},e.prototype.ensureFrameIds=function(e){if(this._frameId)for(var t=this._parentIds,n=t&&t.length>0,r=0;r>>0).toString(16)).slice(-8);return e},e}();function uo(e){var t=new ao(1);return t.writeAscii(e),t.sumAsHex()}function co(e){var t=new Uint8Array(e);return lo(String.fromCharCode.apply(null,t))}var ho="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";function lo(e){var t;return(null!==(t=window.btoa)&&void 0!==t?t:po)(e).replace(/\+/g,"-").replace(/\//g,"_")}function po(e){for(var t=String(e),n=[],r=0,i=0,o=0,s=ho;t.charAt(0|o)||(s="=",o%1);n.push(s.charAt(63&r>>8-o%1*8))){if((i=t.charCodeAt(o+=.75))>255)throw new Error("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.");r=r<<8|i}return n.join("")}var fo=function(e,t,n,r){return new(n||(n=Promise))(function(i,o){function s(e){try{u(r.next(e))}catch(e){o(e)}}function a(e){try{u(r["throw"](e))}catch(e){o(e)}}function u(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n(function(e){e(t)})).then(s,a)}u((r=r.apply(e,t||[])).next())})},vo=function(e,t){var n,r,i,o,s={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:a(0),"throw":a(1),"return":a(2)},"function"==typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function a(o){return function(a){return function(o){if(n)throw new TypeError("Generator is already executing.");for(;s;)try{if(n=1,r&&(i=2&o[0]?r["return"]:o[0]?r["throw"]||((i=r["return"])&&i.call(r),0):r.next)&&!(i=i.call(r,o[1])).done)return i;switch(r=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return s.label++,{value:o[1],done:!1};case 5:s.label++,r=o[1],o=[0];continue;case 7:o=s.ops.pop(),s.trys.pop();continue;default:if(!(i=(i=s.trys).length>0&&i[i.length-1])&&(6===o[0]||2===o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]go?[4,t(mo)]:[3,3]:[3,5];case 2:u.sent(),i=e.now(),u.label=3;case 3:a=new Uint8Array(n,s,Math.min(o-s,_o)),r.write(a),u.label=4;case 4:return s+=_o,[3,1];case 5:return[2,{hash:r.sum(),hasher:r}];}})})}var wo=function(e,t,n,r){return new(n||(n=Promise))(function(i,o){function s(e){try{u(r.next(e))}catch(e){o(e)}}function a(e){try{u(r["throw"](e))}catch(e){o(e)}}function u(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n(function(e){e(t)})).then(s,a)}u((r=r.apply(e,t||[])).next())})},bo=function(e,t){var n,r,i,o,s={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:a(0),"throw":a(1),"return":a(2)},"function"==typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function a(o){return function(a){return function(o){if(n)throw new TypeError("Generator is already executing.");for(;s;)try{if(n=1,r&&(i=2&o[0]?r["return"]:o[0]?r["throw"]||((i=r["return"])&&i.call(r),0):r.next)&&!(i=i.call(r,o[1])).done)return i;switch(r=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return s.label++,{value:o[1],done:!1};case 5:s.label++,r=o[1],o=[0];continue;case 7:o=s.ops.pop(),s.trys.pop();continue;default:if(!(i=(i=s.trys).length>0&&i[i.length-1])&&(6===o[0]||2===o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]So){var i=ur(e,t,{source:"log",type:"bugsnag"});return Mt.sendToBugsnag("Size of blob resource exceeds limit","warning",{url:i,MaxResourceSizeBytes:So}),void r(null)}(function(e){var t=oo(),n=t.resolve,r=t.promise,i=new FileReader;return i.readAsArrayBuffer(e),i.onload=function(){n(i.result)},i.onerror=function(e){Mt.sendToBugsnag(e,"error"),n(null)},r})(n).then(function(e){r(e?{buffer:e,blob:n,contentType:n.type}:null)})},s.send(),i)}function ko(e,t){var n,r;return wo(this,void 0,et,function(){var i;return bo(this,function(o){switch(o.label){case 0:return i=e.window,(null===(r=null===(n=i.crypto)||void 0===n?void 0:n.subtle)||void 0===r?void 0:r.digest)?[4,i.crypto.subtle.digest({name:"sha-1"},t)]:[3,2];case 1:return[2,{hash:co(o.sent()),algorithm:"sha1"}];case 2:return[4,yo(e.time,so,t)];case 3:return[2,{hash:o.sent().hash,algorithm:"fsnv"}];}})})}var Io=function(e,t,n,r){return new(n||(n=Promise))(function(i,o){function s(e){try{u(r.next(e))}catch(e){o(e)}}function a(e){try{u(r["throw"](e))}catch(e){o(e)}}function u(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n(function(e){e(t)})).then(s,a)}u((r=r.apply(e,t||[])).next())})},Co=function(e,t){var n,r,i,o,s={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:a(0),"throw":a(1),"return":a(2)},"function"==typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function a(o){return function(a){return function(o){if(n)throw new TypeError("Generator is already executing.");for(;s;)try{if(n=1,r&&(i=2&o[0]?r["return"]:o[0]?r["throw"]||((i=r["return"])&&i.call(r),0):r.next)&&!(i=i.call(r,o[1])).done)return i;switch(r=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return s.label++,{value:o[1],done:!1};case 5:s.label++,r=o[1],o=[0];continue;case 7:o=s.ops.pop(),s.trys.pop();continue;default:if(!(i=(i=s.trys).length>0&&i[i.length-1])&&(6===o[0]||2===o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]0&&(this._regexes=this._joinRegexes(t))},e.prototype.matches=function(e){return!!this._regexes&&this._regexes.test(e)},e.prototype._isValidRegex=function(e){try{return new RegExp(e),!0}catch(t){return Mt.sendToBugsnag("Browser rejected UrlKeep.Regex","error",{expr:e,error:t.toString()}),!1}},e.prototype._joinRegexes=function(e){try{return new RegExp("("+e.join(")|(")+")","i")}catch(t){return Mt.sendToBugsnag("Browser rejected joining UrlKeep.Regexs","error",{exprs:e,error:t.toString()}),null}},e}();function Po(e,t){var n=Mn(e)+" ";return e.id&&(n+="#"+e.id),n+="[src="+ur(e.src,t,{source:"log",type:"debug"})+"]"}var qo,Uo=function(e){var t=e.transport,n=e.frame,r=e.orgId,s=e.scheme,a=e.script,u=e.recHost,c=e.appHost,h=Po(n,r);o("Injecting into Frame "+h);try{if(function(e){return e.id==e.name&&No.test(e.id)}(n))return void o("Blacklisted iframe: "+h);if(function(e){if(!e.contentDocument||!e.contentWindow||!e.contentWindow.location)return!0;return function(e){return!!e.src&&"about:blank"!=e.src&&e.src.indexOf("javascript:")<0}(e)&&e.src!=e.contentWindow.location.href&&"loading"==e.contentDocument.readyState}(n))return void o("Frame not yet loaded: "+h);var d=n.contentWindow,l=n.contentDocument;if(!d||!l)return void o("Missing contentWindow or contentDocument: "+h);if(!l.head)return void o("Missing contentDocument.head: "+h);if(I(d))return void o("FS already defined in Frame contentWindow: "+h+". Ignoring.");d._fs_org=r,d._fs_script=a,d._fs_rec_host=u,d._fs_app_host=c,d._fs_debug=i(),d._fs_run_in_iframe=!0,t&&(d._fs_transport=t);var p=l.createElement("script");p.async=!0,p.crossOrigin="anonymous",p.src=s+"//"+a,"testdrive"==r&&(p.src+="?allowMoo=true"),l.head.appendChild(p)}catch(e){o("iFrame no injecty. Probably not same origin.")}},No=/^fb\d{18}$/;!function(e){e[e.NoInfoYet=1]="NoInfoYet",e[e.Enabled=2]="Enabled",e[e.Disabled=3]="Disabled"}(qo||(qo={}));var Wo=function(){function e(e,t,n,r){var i=this;this._ctx=e,this._transport=n,this._injector=r,this._bundleUploadInterval=te.DefaultBundleUploadInterval,this._iFrames=[],this._pendingChildFrameIdInits=[],this._listeners=new Ut,this._getCurrentSessionEnabled=qo.NoInfoYet,this._resourceUploadingEnabled=!1,this._tickerTasks=[],this._pendingIframes={},this._watcher=new yn,this._queue=new io(e,this._transport,function(e){for(var t=i._eventWatcher.bundleEvents(e),n=void 0;n=i._tickerTasks.pop();)n();return t},t),this._keep=new Lo(e,this._queue),this._eventWatcher=new Di(e,this._queue,this._keep,this._watcher,this._listeners,function(e){i.onFrameCreated(e)},function(e){i.beforeFrameRemoved(e)},new Eo(e,this._queue,new Ao(e))),this._consoleWatcher=new Ji(e,this._queue,this._listeners),this._scheme=e.options.scheme,this._script=e.options.script,this._recHost=e.options.recHost,this._appHost=e.options.appHost,this._orgId=e.options.orgId,this._wnd=e.window}return e.prototype.bundleUploadInterval=function(){return this._bundleUploadInterval},e.prototype.start=function(e,t){var n=this;this._onFullyStarted=t,"onpagehide"in this._wnd?this._listeners.add(this._wnd,"pagehide",!1,function(e){n.onUnload()}):this._listeners.add(this._wnd,"unload",!1,function(e){n.onUnload()}),this._listeners.add(this._wnd,"message",!1,function(e){if("string"==typeof e.data){var t=e.source;n.postMessageReceived(t,Bo(e.data))}});var r=this._wnd.Document?this._wnd.Document.prototype:this._wnd.document;this._docCloseHook=Tt(r,"close"),this._docCloseHook&&this._docCloseHook.afterAsync(function(){n._listeners.refresh()})},e.prototype.queue=function(){return this._queue},e.prototype.eventWatcher=function(){return this._eventWatcher},e.prototype.console=function(){return this._consoleWatcher},e.prototype.onDomLoad=function(){this._eventWatcher.onDomLoad()},e.prototype.onLoad=function(){this._eventWatcher.onLoad()},e.prototype.shutdown=function(e){this._eventWatcher.shutdown(e),this._consoleWatcher.disable(),this._listeners&&this._listeners.clear(),this._docCloseHook&&this._docCloseHook.disable(),this.tellAllFramesTo(["ShutdownFrame"])},e.prototype.tellAllFramesTo=function(e){for(var t=0;t0){for(var e=0;e0&&this._transport.enqueueEvents(r,n);break;case"RequestFrameId":if(!e)return void o("No MessageEvent.source, iframe may have unloaded.");var s=this.iFrameWndToFsId(e);s?(o("Responding to FID request for frame "+s),this._pendingIframes[s]=!1,this.sendFrameIdToInnerFrame(e,s)):o("No FrameId found. Hoping to send one later.");}},e.prototype.sendFrameIdToInnerFrame=function(e,t){var n=this,r=function(){var r=[];0!=n._frameId&&(r=n._parentIds?n._parentIds.concat(n._frameId):[n._frameId]);var i=n._ctx.time.startTime();Do(e,["SetFrameId",t,r,i,n._scheme,n._script,n._appHost,n._orgId,n._pageRsp])};null==this._frameId?this._pendingChildFrameIdInits.push(r):r()},e.prototype.iFrameWndToFsId=function(e){for(var t=0;t=400&&502!==e||202==e||206==e}var jo=function(){return(jo=Object.assign||function(e){for(var t,n=1,r=arguments.length;n2e6))try{localStorage._fs_swan_song=t}catch(e){}},e.prototype._recover=function(){try{if("_fs_swan_song"in localStorage){var e=localStorage._fs_swan_song||localStorage.singSwanSong;delete localStorage._fs_swan_song,delete localStorage.singSwanSong;var t=wt(e);if(!(t.Bundles&&t.UserId&&t.SessionId&&t.PageId))return void o("Malformed swan song found. Ignoring it.");t.OrgId||(t.OrgId=this._identity.orgId()),t.Bundles.length>0&&(o("Sending "+t.Bundles.length+" bundles as prior page swan song"),this.sendSwanSongBundles(t))}}catch(e){o("Error recovering swan-song: "+e)}},e.prototype.sendSwanSongBundles=function(e,t){var n=this;void 0===t&&(t=0);var r=null;if(nt(e.Bundles)&&0!==e.Bundles.length&&void 0!==e.Bundles[0]){1==e.Bundles.length&&(r=this._ctx.time.wallTime()-(e.LastBundleTime||0));this._protocol.bundle({bundle:e.Bundles[0],deltaT:r,orgId:e.OrgId,pageId:e.PageId,serverBundleTime:e.ServerBundleTime,serverPageStart:e.ServerPageStart,sessionId:e.SessionId,userId:e.UserId,isNewSession:e.IsNewSession,win:function(t){o("Sent "+e.Bundles[0].Evts.length+" trailing events from last session as Seq "+e.Bundles[0].Seq),e.Bundles.shift(),e.Bundles.length>0?n.sendSwanSongBundles(jo(jo({},e),{ServerBundleTime:t.BundleTime})):o("Done with prior page swan song")},lose:function(r){Ho(r)?o("Fatal error while sending events, giving up"):(o("Failed to send events from last session, will retry while on this page"),n._lastSwanSongRetryTimeout=new n._timeoutFactory(n.sendSwanSongBundles,n._protocol.exponentialBackoffMs(t,!0),n,e,t+1).start())}})}},e}(),Vo=function(){function e(e,t,n,r){var i=this;void 0===t&&(t=new Ro(e)),void 0===n&&(n=en),void 0===r&&(r=tn),this._ctx=e,this._protocol=t,this._tickerFactory=n,this._backoffRetries=0,this._backoffTime=0,this._bundleSeq=1,this._lastPostTime=0,this._serverBundleTime=0,this._isNewSession=!1,this._largePageSize=16e6,this._outgoingEventQueue=[],this._bundleQueue=[],this._hibernating=!1,this._heartbeatInterval=0,this._lastUserActivity=this._ctx.time.wallTime(),this._finished=!1,this._scheme=e.options.scheme,this._identity=e.recording.identity,this._lastBundleTime=e.time.wallTime(),this._swanSong=new Ko(e,this._protocol,this._identity,r),this._heartbeatTimeout=new r(function(){i.onHeartbeat()}),this._hibernationTimeout=new r(function(){i.onHibernate()},te.PageInactivityTimeout)}return e.prototype.onShutdown=function(e){this._onShutdown=e},e.prototype.scheme=function(){return this._scheme},e.prototype.enqueueEvents=function(e,t){if(this.maybeHibernate(),this._hibernating){if(this._finished)return;for(var n=0,r=t;n0&&this.enqueueNextBundle(!0),this._bundleQueue.length>0||this._pendingBundle)){var e=this._bundleQueue.concat();this._pendingBundle&&e.unshift(this._pendingBundle),this._swanSong.sing({pageId:this._pageId,bundles:e,lastBundleTime:this._lastBundleTime,serverPageStart:this._serverPageStart,serverBundleTime:this._serverBundleTime,isNewSession:this._isNewSession})}},e.prototype.maybeHibernate=function(){this._hibernating||this.calcLastUserActivityDuration()>=te.PageInactivityTimeout+5e3&&this.onHibernate()},e.prototype.calcLastUserActivityDuration=function(){return s.mathFloor(this._ctx.time.wallTime()-this._lastUserActivity)},e.prototype.onHeartbeat=function(){var e=this.calcLastUserActivityDuration();this._outgoingEventQueue.push({When:this._ctx.time.now(),Kind:R.HEARTBEAT,Args:[e]}),this._heartbeatInterval*=2,this._heartbeatInterval>te.HeartbeatMax&&(this._heartbeatInterval=te.HeartbeatMax),this._heartbeatTimeout.start(this._heartbeatInterval)},e.prototype.onHibernate=function(){this._hibernating||(this.calcLastUserActivityDuration()<=2*te.PageInactivityTimeout&&(this._outgoingEventQueue.push({When:this._ctx.time.now(),Kind:R.UNLOAD,Args:[V.Hibernation]}),this.singSwanSong()),this.stopPipeline(),this._hibernating=!0)},e.prototype.enqueueAndSendBundle=function(){this._pendingBundle?this._pendingBundleFailed&&this._sendPendingBundle():0!=this._outgoingEventQueue.length?this.enqueueNextBundle():this.maybeSendNextBundle()},e.prototype.enqueueNextBundle=function(e){void 0===e&&(e=!1);var t={When:this._outgoingEventQueue[0].When,Seq:this._bundleSeq++,Evts:this._outgoingEventQueue};this._outgoingEventQueue=[],this._bundleQueue.push(t),e?this._protocol.bundleBeacon({bundle:t,deltaT:null,orgId:this._identity.orgId(),pageId:this._pageId,serverBundleTime:this._serverBundleTime,serverPageStart:this._serverPageStart,isNewSession:this._isNewSession,sessionId:this._identity.sessionId(),userId:this._identity.userId(),win:function(){},lose:function(){}}):this.maybeSendNextBundle()},e.prototype.maybeSendNextBundle=function(){this._pageId&&this._serverPageStart&&!this._pendingBundle&&0!=this._bundleQueue.length&&(this._pendingBundle=this._bundleQueue.shift(),this._sendPendingBundle())},e.prototype._sendPendingBundle=function(){var e=this,t=this._ctx.time.wallTime();if(!(te._ctx.recording.bundleUploadInterval()&&e.maybeSendNextBundle()},function(t){if(o("Failed to send events."),Ho(t))return 206==t?Mt.sendToBugsnag("Failed to send bundle, probably because of its large size","error"):t>=500&&Mt.sendToBugsnag("Failed to send bundle, recording outage likely","error"),void(e._onShutdown&&e._onShutdown());e._pendingBundleFailed=!0,e._backoffTime=e._lastPostTime+e._protocol.exponentialBackoffMs(e._backoffRetries++,!1)}))}},e.prototype.sendBundle=function(e,t,n){var r=s.mathFloor(this._ctx.time.wallTime()-this._lastUserActivity),i=this._protocol.bundle({bundle:e,deltaT:null,lastUserActivity:r,orgId:this._identity.orgId(),pageId:this._pageId,serverBundleTime:this._serverBundleTime,serverPageStart:this._serverPageStart,isNewSession:this._isNewSession,sessionId:this._identity.sessionId(),userId:this._identity.userId(),win:t,lose:n});i>this._largePageSize&&this._bundleSeq>16&&(o("splitting large page: "+i),this._ctx.recording.splitPage(V.Size))},e}(),zo=function(){var e=function(t,n){return(e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])})(t,n)};return function(t,n){function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}}(),Yo=function(e){function t(t,n,r,i,o){void 0===r&&(r=new Vo(t,n)),void 0===i&&(i=en),void 0===o&&(o=Uo);var s,a,u=e.call(this,t,i,r,o)||this;return u._protocol=n,u._domLoaded=!1,u._recordingDisabled=!1,u._integrationScriptFetched=!1,r.onShutdown(function(){return u.shutdown(V.SettingsBlocked)}),u._doc=u._wnd.document,u._frameId=0,u._identity=t.recording.identity,u._getCurrentSessionEnabled=qo.NoInfoYet,s=u._wnd,a=function(e){if(u._eventWatcher.shutdown(V.Api),e){var t=u._doc.getElementById(e);t&&t.setAttribute("_fs_embed_token",u._embedToken)}},s._fs_shutdown=a,u}return zo(t,e),t.prototype.onDomLoad=function(){var t=this;e.prototype.onDomLoad.call(this),this._domLoaded=!0,this.injectIntegrationScript(function(){t.fireFsReady(t._recordingDisabled)})},t.prototype.getReplayFlags=function(){var e=U(this._wnd);if(/[?&]_fs_force_session=true(&|#|$)/.test(location.search)&&(e+=",forceSession",this._wnd.history)){var t=location.search.replace(/(^\?|&)_fs_force_session=true(&|$)/,function(e,t,n){return n?t:""});this._wnd.history.replaceState({},"",this._wnd.location.href.replace(location.search,t))}return e},t.prototype.start=function(t,n){var r,i,o,s=this;e.prototype.start.call(this,t,n);var a=this.getReplayFlags(),u=Vt(this._doc),c=u[0],h=u[1],d=bt(this._wnd),l=d[0],p=d[1],f="";t||(f=this._identity.userId());var v=null!==(o=null===(i=null===(r=this._ctx)||void 0===r?void 0:r.recording)||void 0===i?void 0:i.preroll)&&void 0!==o?o:-1,_=ur(Jn(this._wnd),this._orgId,{source:"page",type:"base"}),g=ur(this._doc.referrer,this._orgId,{source:"page",type:"referrer"}),m=ur(this._wnd.location.href,this._orgId,{source:"page",type:"url"}),y={OrgId:this._orgId,UserId:f,Url:m,Base:_,Width:c,Height:h,ScreenWidth:l,ScreenHeight:p,Referrer:g,Preroll:v,Doctype:yt(this._doc),CompiledTimestamp:1591209308,AppId:this._identity.appId()};a&&(y.ReplayFlags=a),this._protocol.page(y,function(e){s.handleResponse(e),s.handleIdentity(e.CookieDomain,e.UserIntId,e.SessionIntId,e.PageIntId,e.EmbedToken),s.handleIntegrationScript(e.IntegrationScript),e.PreviewMode&&s.maybeInjectPreviewScript();var t=s._wnd._fs_pagestart;t&&t();var n=!!e.Consented;s._queue.enqueueFirst({Kind:R.SYS_REPORTCONSENT,Args:[n,K.Document]}),s._queue.enqueueFirst({Kind:R.SET_FRAME_BASE,Args:[ur(Jn(s._wnd),s._orgId,{source:"event",type:R.SET_FRAME_BASE}),yt(s._doc)]}),s._queue.startPipeline({pageId:e.PageIntId,serverPageStart:e.PageStart,isNewSession:!!e.IsNewSession}),s.fullyStarted()},function(e,t){t&&t.user_id&&t.cookie_domain&&t.reason_code==$.ReasonBlockedTrafficRamping&&f!=t.user_id&&s.handleIdentity(t.cookie_domain,t.user_id,"","",""),s.disableBecauseRecPageSaidSo()})},t.prototype.handleIntegrationScript=function(e){var t=this;this._integrationScriptFetched=!0,this._integrationScript=e,this.injectIntegrationScript(function(){t.fireFsReady(t._recordingDisabled)})},t.prototype.handleIdentity=function(e,t,n,r,i){var s=this._identity;s.setIds(this._wnd,e,t,n),this._embedToken=i,o("/User,"+s.userId()+"/Session,"+s.sessionId()+"/Page,"+r)},t.prototype.injectIntegrationScript=function(e){if(this._domLoaded&&this._integrationScriptFetched)if(this._integrationScript){var t=this._doc.createElement("script");this._wnd._fs_csp?(t.addEventListener("load",e),t.addEventListener("error",e),t.async=!0,t.src=this._scheme+"//"+this._recHost+"/rec/integrations?OrgId="+this._orgId,this._doc.head.appendChild(t)):(t.text=this._integrationScript,this._doc.head.appendChild(t),e())}else e()},t.prototype.maybeInjectPreviewScript=function(){if(!this._doc.getElementById("FullStory-preview-script")){var e=this._doc.createElement("script");e.id="FullStory-preview-script",e.async=!0,e.src=this._scheme+"//"+this._appHost+"/s/fspreview.js",this._doc.head.appendChild(e)}},t.prototype.disableBecauseRecPageSaidSo=function(){this.shutdown(V.SettingsBlocked),o("Disabling FS."),this._recordingDisabled=!0,this.fireFsReady(this._recordingDisabled)},t}(Wo),Go=function(){function e(e,t){void 0===t&&(t=new Qo(e)),this._wnd=e,this._messagePoster=t}return e.prototype.enqueueEvents=function(e,t){this._messagePoster.postMessage(this._wnd.parent,"EvtBundle",t,e)},e.prototype.startPipeline=function(){},e.prototype.stopPipeline=function(){},e.prototype.flush=function(){},e.prototype.singSwanSong=function(){},e.prototype.onShutdown=function(e){},e}(),Qo=function(){function e(e){this.wnd=e}return e.prototype.postMessage=function(e,t,n,r){var i=N(this.wnd);if(i)try{i.send(t,gt(n),r)}catch(e){i.send(t,gt(n))}else e.postMessage(function(e,t,n){var r=[e,t];n&&r.push(n);return gt({__fs:r})}(t,n,r),"*")},e}();var Xo,Jo=function(){var e=function(t,n){return(e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])})(t,n)};return function(t,n){function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}}(),$o=function(e){function t(t,n,r,i,o){void 0===n&&(n=new Qo(t.window)),void 0===r&&(r=new Go(t.window)),void 0===i&&(i=en),void 0===o&&(o=Uo);var s=e.call(this,t,i,r,o)||this;return s._messagePoster=n,s}return Jo(t,e),t.prototype.start=function(t,n){var r=this;e.prototype.start.call(this,t,n),this.sendRequestForFrameId(),this._listeners.add(this._wnd,"load",!1,function(){r._eventWatcher.recordingIsDetached()&&(o("Recording wrong document. Restarting recording in iframe."),r._ctx.recording.splitPage(V.FsShutdownFrame))})},t.prototype.postMessageReceived=function(t,n){if(e.prototype.postMessageReceived.call(this,t,n),t==this._wnd.parent||t==this._wnd)switch(n[0]){case"GreetFrame":this.sendRequestForFrameId();break;case"SetFrameId":try{var r=n[1],i=n[2],s=n[3],a=n[4],u=n[5],c=n[6],h=n[7],d=n[8];if(!r)return void o("Outer page gave us a bogus frame Id! Iframe: "+ur(location.href,h,{source:"log",type:"debug"}));this.setFrameIdFromOutside(r,i,s,a,u,c,h,d)}catch(e){o("Failed to parse frameId from message: "+gt(n))}break;case"SetConsent":var l=n[1];this.setConsent(l);break;case"InitFrameMobile":try{var p=JSON.parse(n[1]),f=p.StartTime;if(n.length>2){var v=n[2];if(v.hasOwnProperty("ProtocolVersion"))v.ProtocolVersion>=20180723&&v.hasOwnProperty("OuterStartTime")&&(f=v.OuterStartTime)}var _=p.Host;this.setFrameIdFromOutside(0,[],f,"https:",H(_),B(_),p.OrgId,p.PageResponse)}catch(e){o("Failed to initialize mobile web recording from message: "+gt(n))}}},t.prototype.sendRequestForFrameId=function(){this._frameId||(0!=this._frameId?this._wnd.parent?(o("Asking for a frame ID."),this._messagePoster.postMessage(this._wnd.parent,"RequestFrameId",[])):o("Orphaned window."):o("For some reason the outer window attempted to request a frameId"))},t.prototype.setFrameIdFromOutside=function(e,t,n,r,i,s,a,u){if(this._frameId)this._frameId!=e?(o("Updating frame id from "+this._frameId+" to "+e),this._ctx.recording.splitPage(V.FsShutdownFrame)):o("frame Id is already set to "+this._frameId);else{o("FrameId received within frame "+ur(location.href,a,{source:"log",type:"debug"})+": "+e),this._scheme=r,this._script=i,this._appHost=s,this._orgId=a,this._frameId=e,this._parentIds=t,this.handleResponse(u),this.fireFsReady();var c=!!u.Consented;this._queue.enqueueFirst({Kind:R.SYS_REPORTCONSENT,Args:[c,K.Document]}),this._queue.enqueueFirst({Kind:R.SET_FRAME_BASE,Args:[ur(Jn(this._wnd),a,{source:"event",type:R.SET_FRAME_BASE}),yt(this._wnd.document)]}),this._queue.rebaseIframe(n),this._ctx.time.setStartTime(n),this._queue.startPipeline({pageId:this._pageId,serverPageStart:u.PageStart,isNewSession:!!u.IsNewSession,frameId:e,parentIds:t}),this.flushPendingChildFrameInits(),this.fullyStarted()}},t}(Wo),Zo="fsidentity",es="newuid",ts=function(){function e(e,t){void 0===e&&(e=document),void 0===t&&(t=function(){}),this._doc=e,this._onWriteFailure=t,this._cookies={},this._appId=void 0}return e.prototype.initFromCookies=function(e,t){this._cookies=y(this._doc);var n=this._cookies.fs_uid;if(!n)try{n=localStorage._fs_uid}catch(e){}var r=m(n);r&&r.host.replace(/^www\./,"")==e.replace(/^www\./,"")&&r.orgId==t?this._cookie=r:this._cookie={expirationAbsTimeSeconds:g(),host:e,orgId:t,userId:"",sessionId:"",appKeyHash:""}},e.prototype.initFromParsedCookie=function(e){this._cookie=e},e.prototype.clear=function(){this._cookie.userId=this._cookie.sessionId=this._cookie.appKeyHash=this._appId="",this._cookie.expirationAbsTimeSeconds=g(),this._write()},e.prototype.host=function(){return this._cookie.host},e.prototype.orgId=function(){return this._cookie.orgId},e.prototype.userId=function(){return this._cookie.userId},e.prototype.sessionId=function(){return this._cookie.sessionId},e.prototype.appKeyHash=function(){return this._cookie.appKeyHash},e.prototype.cookieData=function(){return this._cookie},e.prototype.cookies=function(){return this._cookies},e.prototype.setCookie=function(e,t,n){void 0===n&&(n=new Date(p()+6048e5).toUTCString());var r=e+"="+t;this._domain?r+="; domain=."+encodeURIComponent(this._domain):r+="; domain=",r+="; Expires="+n+"; path=/; SameSite=Strict","https:"===location.protocol&&(r+="; Secure"),this._doc.cookie=r},e.prototype.setIds=function(e,t,n,r){(C(t)||t.match(/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/g))&&(t="");var i=function(e){return e._fs_cookie_domain}(e);"string"==typeof i&&(t=i),this._domain=t,this._cookie.userId=n,this._cookie.sessionId=r,this._write()},e.prototype.clearAppId=function(){return!!this._cookie.appKeyHash&&(this._appId="",this._cookie.appKeyHash="",this._write(),!0)},e.prototype.setAppId=function(e){this._appId=e,this._cookie.appKeyHash=uo(e),this._write()},e.prototype.appId=function(){return this._appId},e.prototype.encode=function(){var e=this._cookie.host+"#"+this._cookie.orgId+"#"+this._cookie.userId+":"+this._cookie.sessionId;return this._cookie.appKeyHash&&(e+="#"+encodeURIComponent(this._cookie.appKeyHash)+"#"),e+="/"+this._cookie.expirationAbsTimeSeconds},e.prototype._write=function(){if(null!=this._domain){var e=this.encode(),t=new Date(1e3*this._cookie.expirationAbsTimeSeconds).toUTCString();this.setCookie("fs_uid",e,t);var n=[];-1===this._doc.cookie.indexOf(e)&&n.push(["fs_uid","cookie"]);try{localStorage._fs_uid=e,localStorage._fs_uid!==e&&n.push(["fs_uid","localStorage"])}catch(e){n.push(["fs_uid","localStorage",String(e)])}n.length>0&&this._onWriteFailure(n)}},e}();!function(e){e.rec="rec",e.user="user",e.account="account",e.consent="consent",e.customEvent="event",e.log="log"}(Xo||(Xo={}));var ns={acctId:"str",displayName:"str",website:"str"},rs={uid:"str",displayName:"str",email:"str"},is={str:os,bool:ss,real:as,"int":us,date:cs,strs:hs(os),bools:hs(ss),reals:hs(as),ints:hs(us),dates:hs(cs),objs:hs(ds),obj:ds};function os(e){return"string"==typeof e}function ss(e){return"boolean"==typeof e}function as(e){return"number"==typeof e}function us(e){return"number"==typeof e&&e-s.mathFloor(e)==0}function cs(e){return!!e&&(e.constructor===Date?!isNaN(e):("number"==typeof e||"string"==typeof e)&&!isNaN(new Date(e)))}function hs(e){return function(t){if(!(t instanceof Array))return!1;for(var n=0;n=0)return o("blocking FS.identify API call; uid value ("+n+") is illegal"),[void 0,Zo];var r=uo(n),i=void 0;t&&t._cookie.appKeyHash&&t._cookie.appKeyHash!==r&&t._cookie.appKeyHash!==n&&(o("user re-identified; existing uid hash ("+t._cookie.appKeyHash+") does not match provided uid ("+n+")"),i=es);return[n,i]}(a,this._identity),c=u[0],h=u[1];if(!c){switch(h){case Zo:case void 0:break;default:o("unexpected failReason returned from setAppId: "+h);}return{events:i}}t.uid=c,this._identity.setAppId(t.uid),h==es&&(r=!0)}}i.push.apply(i,this.rawEventsFromApi(X.User,rs,t,n));break;case Xo.customEvent:var d=t.n,l=t.p;i.push.apply(i,this.rawEventsFromApi(X.Event,{},l,n,d));break;default:o("invalid operation \""+e+"\"; only \"rec\", \"account\", and \"user\" are supported at present");}}catch(t){o("unexpected exception handling "+e+" API call: "+t.message)}return{events:i,reidentify:r}},e.prototype.rawEventsFromApi=function(e,t,n,r,i){var a=function e(t,n,r){var i={PayloadToSend:{},ValidationErrors:[]},a=function(r){var o=e(t,n,r);return i.ValidationErrors=i.ValidationErrors.concat(o.ValidationErrors),o.PayloadToSend};return ht(r,function(e,r){var u=function(e,t,n,r){var i=t,a=typeof n;if("undefined"===a)return o("Cannot infer type of "+a+" "+n),r.push({Type:"vartype",FieldName:t,ValueType:a+" (unsupported)"}),null;if(s.objectHasOwnProp(e,t))return{name:t,type:e[t]};var u=t.lastIndexOf("_");if(-1==u||!vs(t.substring(u+1))){var c=function(e){for(var t in is)if(is[t](e))return t;return null}(n);if(null==c)return o("Cannot infer type of "+a+" "+n),n?r.push({Type:"vartype",FieldName:t}):r.push({Type:"vartype",FieldName:t,ValueType:"null (unsupported)"}),null;u=t.length,o("Warning: Inferring user variable \""+t+"\" to be of type \""+c+"\""),t=t+"_"+c}var h=[t.substring(0,u),t.substring(u+1)],d=h[0],l=h[1];if("object"===a&&!n)return o("null is not a valid object type"),r.push({Type:"vartype",FieldName:i,ValueType:"null (unsupported)"}),null;if(!ls.test(d)){d=d.replace(/[^a-zA-Z0-9_]/g,"").replace(/^[0-9]+/,""),/[0-9]/.test(d[0])&&(d=d.substring(1)),r.push({Type:"varname",FieldName:i});var p=d+"_"+l;if(o("Warning: variable \""+i+"\" has invalid characters. It should match /"+ls.source+"/. Converted name to \""+p+"\"."),""==d)return null;t=p}if(!vs(l))return o("Variable \""+i+"\" has invalid type \""+l+"\""),r.push({Type:"varname",FieldName:i}),null;if(!function(e,t){return is[e](t)}(l,n))return o("illegal value "+gt(n)+" for type "+l),"number"===a?a=n%1==0?"integer":"real":"object"==a&&null!=n&&n.constructor==Date&&(a=isNaN(n)?"invalid date":"date"),r.push({Type:"vartype",FieldName:i,ValueType:a}),null;return{name:t,type:l}}(n,r,e,i.ValidationErrors);if(u){var c=u.name,h=u.type;if("obj"!=h){if("objs"!=h){var d,l;i.PayloadToSend[c]=fs(h,e)}else{t!=X.Event&&i.ValidationErrors.push({Type:"vartype",FieldName:c,ValueType:"Array (unsupported)"});for(var p=e,f=[],v=0;v0&&(i.PayloadToSend[c]=f)}}else{var _=a(e),g=(l="_obj").length>(d=r).length||d.substring(d.length-l.length)!=l?c.substring(0,c.length-"_obj".length):c;i.PayloadToSend[g]=_}}else i.PayloadToSend[r]=fs("",e)}),i}(e,t,n),u=[],c=e==X.Event,h=gt(a.PayloadToSend),d=!!r&&"fs"!==r;return c?u.push({When:0,Kind:R.SYS_CUSTOM,Args:d?[i,h,r]:[i,h]}):u.push({When:0,Kind:R.SYS_SETVAR,Args:d?[e,h,r]:[e,h]}),u},e}();function fs(e,t){return"str"==e&&null!=t&&(t=t.trim()),null==t||"date"!=e&&t.constructor!=Date||(t=function(e){var t,n=e.constructor===Date?e:new Date(e);try{t=n.toISOString()}catch(e){t=null}return t}(t)),t}function vs(e){return!!is[e]}var _s=function(){return(_s=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])&&(6===o[0]||2===o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]u.length?u.length:a.length,d=1,l=d;l=0}var Es=["__zone_symbol__OriginalDelegate","nr@original"];function Ts(e,t){if(t){for(var n=0,r=Es;n16)Mt.sendToBugsnag("Too much synchronous recursion in requestMeasureTask","error");else{var n=this.performingMeasurements?this.recursionDepth:0,r=Mt.wrap(function(){var r=t.recursionDepth;t.recursionDepth=n+1;try{e()}finally{t.recursionDepth=r}});this.measurementTasks?this.measurementTasks.push(r):(this.measurementTasks=[r],this.schedule())}},e.prototype.performMeasurementsNow=function(){this.performMeasurements()},e}(),As=function(e){function t(t,n){var r=e.call(this)||this;return r.wnd=t,r.ResizeObserver=n,r}return Cs(t,e),t.prototype.schedule=function(){var e=this,t=this.ResizeObserver,n=this.wnd.document,r=n.body||n.documentElement||n.head,i=new t(function(){i.unobserve(r),e.performMeasurements()});i.observe(r)},t}(Rs),xs=function(e){function t(t,n,r){var i=e.call(this)||this;return i.wnd=t,i.requestWindowAnimationFrame=n,i.onRAF=Mt.wrap(function(){i.ch.port2.postMessage(void 0)}),i.ch=new r,i}return Cs(t,e),t.prototype.schedule=function(){this.ch.port1.onmessage=this.performMeasurements,this.requestWindowAnimationFrame(this.wnd,this.onRAF)},t}(Rs),Os=function(e){function t(t){var n=e.call(this)||this;return n.wnd=t,n}return Cs(t,e),t.prototype.schedule=function(){s.setWindowTimeout(this.wnd,this.performMeasurements,0)},t}(Rs),Ms=function(e,t,n,r){return new(n||(n=Promise))(function(i,o){function s(e){try{u(r.next(e))}catch(e){o(e)}}function a(e){try{u(r["throw"](e))}catch(e){o(e)}}function u(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n(function(e){e(t)})).then(s,a)}u((r=r.apply(e,t||[])).next())})},Ls=function(e,t){var n,r,i,o,s={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:a(0),"throw":a(1),"return":a(2)},"function"==typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function a(o){return function(a){return function(o){if(n)throw new TypeError("Generator is already executing.");for(;s;)try{if(n=1,r&&(i=2&o[0]?r["return"]:o[0]?r["throw"]||((i=r["return"])&&i.call(r),0):r.next)&&!(i=i.call(r,o[1])).done)return i;switch(r=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return s.label++,{value:o[1],done:!1};case 5:s.label++,r=o[1],o=[0];continue;case 7:o=s.ops.pop(),s.trys.pop();continue;default:if(!(i=(i=s.trys).length>0&&i[i.length-1])&&(6===o[0]||2===o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]=8)return void o("reidentified too many times; giving up");this.reidentifyCount++,W(this.wnd,[e,t]),this.splitPage(V.Reidentify,!0)}else u();void 0!==a&&(a?this.restart():this.shutdown(V.Api))}else W(this.wnd,[e,t])},e.prototype._cookies=function(){return this.identity?this.identity.cookies():(o("Error in FS integration: Can't get cookies from inside an iframe"),null)},e.prototype._setCookie=function(e,t){this.identity?this.identity.setCookie(e,t):o("Error in FS integration: Can't set cookies from inside an iframe")},e.prototype._withEventQueue=function(e,t){if(this.recorder){var n=this.recorder.queue(),r=this.recorder.pageSignature();null!=n&&null!=r?e===r?t(n):Mt.sendToBugsnag("Error in _withEventQueue: Page Signature mismatch","error",{pageSignature:r,callerSignature:e}):o("Error getting event queue or page signature: Recorder not initialized")}else o("Error in FS integration: Recorder not initialized")},e.prototype.initApi=function(){var e=I(this.wnd);e?(e.getCurrentSessionURL=_t(this.getCurrentSessionURL,this),e.getCurrentSession=_t(this.getCurrentSession,this),e.enableConsole=_t(this.enableConsole,this),e.disableConsole=_t(this.disableConsole,this),e.log=_t(this.log,this),e.shutdown=_t(this.shutdownApi,this),e.restart=_t(this.restart,this),e._api=_t(this._api,this),e._cookies=_t(this._cookies,this),e._setCookie=_t(this._setCookie,this),e._withEventQueue=_t(this._withEventQueue,this)):o("Missing browser API namespace; couldn't initialize API.")},e.prototype.start=function(){var e,t=this;e=L(this.wnd),r=e,o("script version UNSET (compiled at 1591209308)");var n=P(this.wnd);if(n){this.orgId=n;var i,s=(i=this.wnd)._fs_script||H(D(i));if(s){this.script=s;var a=F(this.wnd);if(a){this.recHost=a;var u=function(e){return e._fs_app_host||B(D(e))}(this.wnd);u?(this.appHost=u,o("script: "+this.script),o("recording host: "+this.recHost),o("orgid: "+this.orgId),"localhost:8080"==this.recHost&&(this.scheme="http:"),this.inFrame()||(this.identity=new ts(this.wnd.document,function(e){for(var n,r=0,i=e;r {
describe('full_story', () => {
- it('allows orgId when enabled: false', () => {
+ it('allows org_id when enabled: false', () => {
expect(() =>
config.schema.validate({ full_story: { enabled: false, org_id: 'asdf' } })
).not.toThrow();
});
- it('rejects undefined or empty orgId when enabled: true', () => {
+ it('rejects undefined or empty org_id when enabled: true', () => {
expect(() =>
config.schema.validate({ full_story: { enabled: true } })
).toThrowErrorMatchingInlineSnapshot(
@@ -28,7 +28,7 @@ describe('xpack.cloud config', () => {
);
});
- it('accepts orgId when enabled: true', () => {
+ it('accepts org_id when enabled: true', () => {
expect(() =>
config.schema.validate({ full_story: { enabled: true, org_id: 'asdf' } })
).not.toThrow();
diff --git a/x-pack/plugins/cloud/server/plugin.ts b/x-pack/plugins/cloud/server/plugin.ts
index fea8be9f934e1..4c0b7b7f7eca6 100644
--- a/x-pack/plugins/cloud/server/plugin.ts
+++ b/x-pack/plugins/cloud/server/plugin.ts
@@ -11,6 +11,7 @@ import { CloudConfigType } from './config';
import { registerCloudUsageCollector } from './collectors';
import { getIsCloudEnabled } from '../common/is_cloud_enabled';
import { parseDeploymentIdFromDeploymentUrl } from './utils';
+import { registerFullstoryRoute } from './routes/fullstory';
interface PluginsSetup {
usageCollection?: UsageCollectionSetup;
@@ -40,6 +41,13 @@ export class CloudPlugin implements Plugin {
const isCloudEnabled = getIsCloudEnabled(this.config.id);
registerCloudUsageCollector(usageCollection, { isCloudEnabled });
+ if (this.config.full_story.enabled) {
+ registerFullstoryRoute({
+ httpResources: core.http.resources,
+ packageInfo: this.context.env.packageInfo,
+ });
+ }
+
return {
cloudId: this.config.id,
deploymentId: parseDeploymentIdFromDeploymentUrl(this.config.deployment_url),
diff --git a/x-pack/plugins/cloud/server/routes/fullstory.test.ts b/x-pack/plugins/cloud/server/routes/fullstory.test.ts
new file mode 100644
index 0000000000000..dae541a8c033c
--- /dev/null
+++ b/x-pack/plugins/cloud/server/routes/fullstory.test.ts
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+
+jest.mock('fs/promises');
+import { renderFullStoryLibraryFactory, FULLSTORY_LIBRARY_PATH } from './fullstory';
+import fs from 'fs/promises';
+
+const fsMock = fs as jest.Mocked;
+
+describe('renderFullStoryLibraryFactory', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ fsMock.readFile.mockResolvedValue(Buffer.from('fake fs src'));
+ });
+ afterAll(() => jest.restoreAllMocks());
+
+ it('successfully returns file contents', async () => {
+ const render = renderFullStoryLibraryFactory();
+
+ const { body } = await render();
+ expect(fsMock.readFile).toHaveBeenCalledWith(FULLSTORY_LIBRARY_PATH);
+ expect(body.toString()).toEqual('fake fs src');
+ });
+
+ it('only reads from file system once callback is invoked', async () => {
+ const render = renderFullStoryLibraryFactory();
+ expect(fsMock.readFile).not.toHaveBeenCalled();
+ await render();
+ expect(fsMock.readFile).toHaveBeenCalledTimes(1);
+ });
+
+ it('does not read from filesystem on subsequent calls', async () => {
+ const render = renderFullStoryLibraryFactory();
+ await render();
+ expect(fsMock.readFile).toHaveBeenCalledTimes(1);
+ await render();
+ expect(fsMock.readFile).toHaveBeenCalledTimes(1);
+ await render();
+ expect(fsMock.readFile).toHaveBeenCalledTimes(1);
+ });
+
+ it('returns max-age cache-control in dist', async () => {
+ const render = renderFullStoryLibraryFactory(true);
+ const { headers } = await render();
+ expect(headers).toEqual({
+ 'cache-control': 'max-age=31536000',
+ });
+ });
+
+ it('returns must-revalidate cache-control and sha1 etag in dev', async () => {
+ const render = renderFullStoryLibraryFactory(false);
+ const { headers } = await render();
+ expect(headers).toEqual({
+ 'cache-control': 'must-revalidate',
+ etag: '1e02f94b45750ba9284c111d31ae7e59c13b8e6e',
+ });
+ });
+});
diff --git a/x-pack/plugins/cloud/server/routes/fullstory.ts b/x-pack/plugins/cloud/server/routes/fullstory.ts
new file mode 100644
index 0000000000000..c614d803eed9f
--- /dev/null
+++ b/x-pack/plugins/cloud/server/routes/fullstory.ts
@@ -0,0 +1,75 @@
+/*
+ * 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 path from 'path';
+import fs from 'fs/promises';
+import { createHash } from 'crypto';
+import { once } from 'lodash';
+import { HttpResources, HttpResponseOptions, PackageInfo } from '../../../../../src/core/server';
+
+const MINUTE = 60;
+const HOUR = 60 * MINUTE;
+const DAY = 24 * HOUR;
+
+/** @internal exported for testing */
+export const FULLSTORY_LIBRARY_PATH = path.join(__dirname, '..', 'assets', 'fullstory_library.js');
+
+/** @internal exported for testing */
+export const renderFullStoryLibraryFactory = (dist = true) =>
+ once(
+ async (): Promise<{
+ body: Buffer;
+ headers: HttpResponseOptions['headers'];
+ }> => {
+ const srcBuffer = await fs.readFile(FULLSTORY_LIBRARY_PATH);
+ const hash = createHash('sha1');
+ hash.update(srcBuffer);
+ const hashDigest = hash.digest('hex');
+
+ return {
+ body: srcBuffer,
+ // In dist mode, return a long max-age, otherwise use etag + must-revalidate
+ headers: dist
+ ? { 'cache-control': `max-age=${DAY * 365}` }
+ : { 'cache-control': 'must-revalidate', etag: hashDigest },
+ };
+ }
+ );
+
+export const registerFullstoryRoute = ({
+ httpResources,
+ packageInfo,
+}: {
+ httpResources: HttpResources;
+ packageInfo: Readonly;
+}) => {
+ const renderFullStoryLibrary = renderFullStoryLibraryFactory(packageInfo.dist);
+
+ /**
+ * Register a custom JS endpoint in order to acheive best caching possible with `max-age` similar to plugin bundles.
+ */
+ httpResources.register(
+ {
+ // Use the build number in the URL path to leverage max-age caching on production builds
+ path: `/internal/cloud/${packageInfo.buildNum}/fullstory.js`,
+ validate: false,
+ options: {
+ authRequired: false,
+ },
+ },
+ async (context, req, res) => {
+ try {
+ return res.renderJs(await renderFullStoryLibrary());
+ } catch (e) {
+ return res.customError({
+ body: `Could not load FullStory library from disk due to error: ${e.toString()}`,
+ statusCode: 500,
+ });
+ }
+ }
+ );
+};
diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx
index 1b1fecb2bcaf7..6f02946596a87 100644
--- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx
+++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx
@@ -5,6 +5,7 @@
* 2.0.
*/
+import type { KibanaLocation } from 'src/plugins/share/public';
import React from 'react';
import { DataPublicPluginStart } from 'src/plugins/data/public';
import { DashboardStart } from 'src/plugins/dashboard/public';
@@ -20,7 +21,6 @@ import {
CollectConfigProps,
StartServicesGetter,
} from '../../../../../../../src/plugins/kibana_utils/public';
-import { KibanaURL } from '../../../../../../../src/plugins/share/public';
import { Config } from './types';
export interface Params {
@@ -39,7 +39,7 @@ export abstract class AbstractDashboardDrilldown string[];
- protected abstract getURL(config: Config, context: Context): Promise;
+ protected abstract getLocation(config: Config, context: Context): Promise;
public readonly order = 100;
@@ -65,19 +65,25 @@ export abstract class AbstractDashboardDrilldown => {
- const url = await this.getURL(config, context);
- return url.path;
+ const { app, path } = await this.getLocation(config, context);
+ const url = await this.params.start().core.application.getUrlForApp(app, {
+ path,
+ absolute: true,
+ });
+ return url;
};
public readonly execute = async (config: Config, context: Context) => {
- const url = await this.getURL(config, context);
- await this.params.start().core.application.navigateToApp(url.appName, { path: url.appPath });
+ const { app, path, state } = await this.getLocation(config, context);
+ await this.params.start().core.application.navigateToApp(app, {
+ path,
+ state,
+ });
};
- protected get urlGenerator() {
- const urlGenerator = this.params.start().plugins.dashboard.dashboardUrlGenerator;
- if (!urlGenerator)
- throw new Error('Dashboard URL generator is required for dashboard drilldown.');
- return urlGenerator;
+ protected get locator() {
+ const locator = this.params.start().plugins.dashboard.locator;
+ if (!locator) throw new Error('Dashboard locator is required for dashboard drilldown.');
+ return locator;
}
}
diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx
index cd82e23544ad0..a817d9f65c916 100644
--- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx
+++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx
@@ -7,7 +7,7 @@
import { EmbeddableToDashboardDrilldown } from './embeddable_to_dashboard_drilldown';
import { AbstractDashboardDrilldownConfig as Config } from '../abstract_dashboard_drilldown';
-import { coreMock, savedObjectsServiceMock } from '../../../../../../../src/core/public/mocks';
+import { savedObjectsServiceMock } from '../../../../../../../src/core/public/mocks';
import {
Filter,
FilterStateStore,
@@ -19,10 +19,11 @@ import {
ApplyGlobalFilterActionContext,
esFilters,
} from '../../../../../../../src/plugins/data/public';
-import { createDashboardUrlGenerator } from '../../../../../../../src/plugins/dashboard/public/url_generator';
-import { UrlGeneratorsService } from '../../../../../../../src/plugins/share/public/url_generators';
+import {
+ DashboardAppLocatorDefinition,
+ DashboardAppLocatorParams,
+} from '../../../../../../../src/plugins/dashboard/public/locator';
import { StartDependencies } from '../../../plugin';
-import { SavedObjectLoader } from '../../../../../../../src/plugins/saved_objects/public';
import { StartServicesGetter } from '../../../../../../../src/plugins/kibana_utils/public/core';
import { EnhancedEmbeddableContext } from '../../../../../embeddable_enhanced/public';
@@ -74,13 +75,6 @@ test('inject/extract are defined', () => {
});
describe('.execute() & getHref', () => {
- /**
- * A convenience test setup helper
- * Beware: `dataPluginMock.createStartContract().actions` and extracting filters from event is mocked!
- * The url generation is not mocked and uses real implementation
- * So this tests are mostly focused on making sure the filters returned from `dataPluginMock.createStartContract().actions` helpers
- * end up in resulting navigation path
- */
async function setupTestBed(
config: Partial,
embeddableInput: { filters?: Filter[]; timeRange?: TimeRange; query?: Query },
@@ -90,7 +84,10 @@ describe('.execute() & getHref', () => {
const navigateToApp = jest.fn();
const getUrlForApp = jest.fn((app, opt) => `${app}/${opt.path}`);
const savedObjectsClient = savedObjectsServiceMock.createStartContract().client;
-
+ const definition = new DashboardAppLocatorDefinition({
+ useHashedUrl: false,
+ getDashboardFilterFields: async () => [],
+ });
const drilldown = new EmbeddableToDashboardDrilldown({
start: ((() => ({
core: {
@@ -105,17 +102,11 @@ describe('.execute() & getHref', () => {
plugins: {
uiActionsEnhanced: {},
dashboard: {
- dashboardUrlGenerator: new UrlGeneratorsService()
- .setup(coreMock.createSetup())
- .registerUrlGenerator(
- createDashboardUrlGenerator(() =>
- Promise.resolve({
- appBasePath: 'xyz/app/dashboards',
- useHashedUrl: false,
- savedDashboardLoader: ({} as unknown) as SavedObjectLoader,
- })
- )
- ),
+ locator: {
+ getLocation: async (params: DashboardAppLocatorParams) => {
+ return await definition.getLocation(params);
+ },
+ },
},
},
self: {},
diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx
index 7b55244985981..97355d9eb435e 100644
--- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx
+++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx
@@ -5,7 +5,8 @@
* 2.0.
*/
-import { DashboardUrlGeneratorState } from '../../../../../../../src/plugins/dashboard/public';
+import type { KibanaLocation } from 'src/plugins/share/public';
+import { DashboardAppLocatorParams } from '../../../../../../../src/plugins/dashboard/public';
import {
ApplyGlobalFilterActionContext,
APPLY_FILTER_TRIGGER,
@@ -23,7 +24,6 @@ import {
AbstractDashboardDrilldownParams,
AbstractDashboardDrilldownConfig as Config,
} from '../abstract_dashboard_drilldown';
-import { KibanaURL } from '../../../../../../../src/plugins/share/public';
import { EMBEDDABLE_TO_DASHBOARD_DRILLDOWN } from './constants';
import { createExtract, createInject } from '../../../../common';
import { EnhancedEmbeddableContext } from '../../../../../embeddable_enhanced/public';
@@ -49,26 +49,26 @@ export class EmbeddableToDashboardDrilldown extends AbstractDashboardDrilldown [APPLY_FILTER_TRIGGER];
- protected async getURL(config: Config, context: Context): Promise {
- const state: DashboardUrlGeneratorState = {
+ protected async getLocation(config: Config, context: Context): Promise {
+ const params: DashboardAppLocatorParams = {
dashboardId: config.dashboardId,
};
if (context.embeddable) {
const embeddable = context.embeddable as IEmbeddable;
const input = embeddable.getInput();
- if (isQuery(input.query) && config.useCurrentFilters) state.query = input.query;
+ if (isQuery(input.query) && config.useCurrentFilters) params.query = input.query;
// if useCurrentDashboardDataRange is enabled, then preserve current time range
// if undefined is passed, then destination dashboard will figure out time range itself
// for brush event this time range would be overwritten
if (isTimeRange(input.timeRange) && config.useCurrentDateRange)
- state.timeRange = input.timeRange;
+ params.timeRange = input.timeRange;
// if useCurrentDashboardFilters enabled, then preserve all the filters (pinned and unpinned)
// otherwise preserve only pinned
if (isFilters(input.filters))
- state.filters = config.useCurrentFilters
+ params.filters = config.useCurrentFilters
? input.filters
: input.filters?.filter((f) => esFilters.isFilterPinned(f));
}
@@ -79,17 +79,16 @@ export class EmbeddableToDashboardDrilldown extends AbstractDashboardDrilldown
- checkRunningSessions$(deps, config).toPromise();
+const checkNonPersistedSessions = (deps: CheckSearchSessionsDeps, config: SearchSessionsConfig) =>
+ checkNonPersistedSessions$(deps, config).toPromise();
-describe('getSearchStatus', () => {
+describe('checkNonPersistedSessions', () => {
let mockClient: any;
let savedObjectsClient: jest.Mocked;
const config: SearchSessionsConfig = {
@@ -42,7 +37,9 @@ describe('getSearchStatus', () => {
maxUpdateRetries: 3,
defaultExpiration: moment.duration(7, 'd'),
trackingInterval: moment.duration(10, 's'),
+ expireInterval: moment.duration(10, 'm'),
monitoringTaskTimeout: moment.duration(5, 'm'),
+ cleanupInterval: moment.duration(10, 's'),
management: {} as any,
};
const mockLogger: any = {
@@ -51,16 +48,6 @@ describe('getSearchStatus', () => {
error: jest.fn(),
};
- const emptySO = {
- attributes: {
- persisted: false,
- status: SearchSessionStatus.IN_PROGRESS,
- created: moment().subtract(moment.duration(3, 'm')),
- touched: moment().subtract(moment.duration(10, 's')),
- idMapping: {},
- },
- };
-
beforeEach(() => {
savedObjectsClient = savedObjectsClientMock.create();
mockClient = {
@@ -81,7 +68,7 @@ describe('getSearchStatus', () => {
total: 0,
} as any);
- await checkRunningSessions(
+ await checkNonPersistedSessions(
{
savedObjectsClient,
client: mockClient,
@@ -94,240 +81,7 @@ describe('getSearchStatus', () => {
expect(savedObjectsClient.delete).not.toBeCalled();
});
- describe('pagination', () => {
- test('fetches one page if not objects exist', async () => {
- savedObjectsClient.find.mockResolvedValueOnce({
- saved_objects: [],
- total: 0,
- } as any);
-
- await checkRunningSessions(
- {
- savedObjectsClient,
- client: mockClient,
- logger: mockLogger,
- },
- config
- );
-
- expect(savedObjectsClient.find).toHaveBeenCalledTimes(1);
- });
-
- test('fetches one page if less than page size object are returned', async () => {
- savedObjectsClient.find.mockResolvedValueOnce({
- saved_objects: [emptySO, emptySO],
- total: 5,
- } as any);
-
- await checkRunningSessions(
- {
- savedObjectsClient,
- client: mockClient,
- logger: mockLogger,
- },
- config
- );
-
- expect(savedObjectsClient.find).toHaveBeenCalledTimes(1);
- });
-
- test('fetches two pages if exactly page size objects are returned', async () => {
- let i = 0;
- savedObjectsClient.find.mockImplementation(() => {
- return new Promise((resolve) => {
- resolve({
- saved_objects: i++ === 0 ? [emptySO, emptySO, emptySO, emptySO, emptySO] : [],
- total: 5,
- page: i,
- } as any);
- });
- });
-
- await checkRunningSessions(
- {
- savedObjectsClient,
- client: mockClient,
- logger: mockLogger,
- },
- config
- );
-
- expect(savedObjectsClient.find).toHaveBeenCalledTimes(2);
-
- // validate that page number increases
- const { page: page1 } = savedObjectsClient.find.mock.calls[0][0];
- const { page: page2 } = savedObjectsClient.find.mock.calls[1][0];
- expect(page1).toBe(1);
- expect(page2).toBe(2);
- });
-
- test('fetches two pages if page size +1 objects are returned', async () => {
- let i = 0;
- savedObjectsClient.find.mockImplementation(() => {
- return new Promise((resolve) => {
- resolve({
- saved_objects: i++ === 0 ? [emptySO, emptySO, emptySO, emptySO, emptySO] : [emptySO],
- total: 5,
- page: i,
- } as any);
- });
- });
-
- await checkRunningSessions(
- {
- savedObjectsClient,
- client: mockClient,
- logger: mockLogger,
- },
- config
- );
-
- expect(savedObjectsClient.find).toHaveBeenCalledTimes(2);
- });
-
- test('fetching is abortable', async () => {
- let i = 0;
- const abort$ = new Subject();
- savedObjectsClient.find.mockImplementation(() => {
- return new Promise((resolve) => {
- if (++i === 2) {
- abort$.next();
- }
- resolve({
- saved_objects: i <= 5 ? [emptySO, emptySO, emptySO, emptySO, emptySO] : [],
- total: 25,
- page: i,
- } as any);
- });
- });
-
- await checkRunningSessions$(
- {
- savedObjectsClient,
- client: mockClient,
- logger: mockLogger,
- },
- config
- )
- .pipe(takeUntil(abort$))
- .toPromise();
-
- jest.runAllTimers();
-
- // if not for `abort$` then this would be called 6 times!
- expect(savedObjectsClient.find).toHaveBeenCalledTimes(2);
- });
-
- test('sorting is by "touched"', async () => {
- savedObjectsClient.find.mockResolvedValueOnce({
- saved_objects: [],
- total: 0,
- } as any);
-
- await checkRunningSessions(
- {
- savedObjectsClient,
- client: mockClient,
- logger: mockLogger,
- },
- config
- );
-
- expect(savedObjectsClient.find).toHaveBeenCalledWith(
- expect.objectContaining({ sortField: 'touched', sortOrder: 'asc' })
- );
- });
-
- test('sessions fetched in the beginning are processed even if sessions in the end fail', async () => {
- let i = 0;
- savedObjectsClient.find.mockImplementation(() => {
- return new Promise((resolve, reject) => {
- if (++i === 2) {
- reject(new Error('Fake find error...'));
- }
- resolve({
- saved_objects:
- i <= 5
- ? [
- i === 1
- ? {
- id: '123',
- attributes: {
- persisted: false,
- status: SearchSessionStatus.IN_PROGRESS,
- created: moment().subtract(moment.duration(3, 'm')),
- touched: moment().subtract(moment.duration(2, 'm')),
- idMapping: {
- 'map-key': {
- strategy: ENHANCED_ES_SEARCH_STRATEGY,
- id: 'async-id',
- },
- },
- },
- }
- : emptySO,
- emptySO,
- emptySO,
- emptySO,
- emptySO,
- ]
- : [],
- total: 25,
- page: i,
- } as any);
- });
- });
-
- await checkRunningSessions$(
- {
- savedObjectsClient,
- client: mockClient,
- logger: mockLogger,
- },
- config
- ).toPromise();
-
- jest.runAllTimers();
-
- expect(savedObjectsClient.find).toHaveBeenCalledTimes(2);
-
- // by checking that delete was called we validate that sessions from session that were successfully fetched were processed
- expect(mockClient.asyncSearch.delete).toBeCalled();
- const { id } = mockClient.asyncSearch.delete.mock.calls[0][0];
- expect(id).toBe('async-id');
- });
- });
-
describe('delete', () => {
- test('doesnt delete a persisted session', async () => {
- savedObjectsClient.find.mockResolvedValue({
- saved_objects: [
- {
- id: '123',
- attributes: {
- persisted: true,
- status: SearchSessionStatus.IN_PROGRESS,
- created: moment().subtract(moment.duration(30, 'm')),
- touched: moment().subtract(moment.duration(10, 'm')),
- idMapping: {},
- },
- },
- ],
- total: 1,
- } as any);
- await checkRunningSessions(
- {
- savedObjectsClient,
- client: mockClient,
- logger: mockLogger,
- },
- config
- );
-
- expect(savedObjectsClient.bulkUpdate).not.toBeCalled();
- expect(savedObjectsClient.delete).not.toBeCalled();
- });
-
test('doesnt delete a non persisted, recently touched session', async () => {
savedObjectsClient.find.mockResolvedValue({
saved_objects: [
@@ -336,6 +90,7 @@ describe('getSearchStatus', () => {
attributes: {
persisted: false,
status: SearchSessionStatus.IN_PROGRESS,
+ expires: moment().add(moment.duration(3, 'm')),
created: moment().subtract(moment.duration(3, 'm')),
touched: moment().subtract(moment.duration(10, 's')),
idMapping: {},
@@ -344,7 +99,7 @@ describe('getSearchStatus', () => {
],
total: 1,
} as any);
- await checkRunningSessions(
+ await checkNonPersistedSessions(
{
savedObjectsClient,
client: mockClient,
@@ -367,6 +122,7 @@ describe('getSearchStatus', () => {
status: SearchSessionStatus.COMPLETE,
created: moment().subtract(moment.duration(3, 'm')),
touched: moment().subtract(moment.duration(1, 'm')),
+ expires: moment().add(moment.duration(3, 'm')),
idMapping: {
'search-hash': {
id: 'search-id',
@@ -379,7 +135,7 @@ describe('getSearchStatus', () => {
],
total: 1,
} as any);
- await checkRunningSessions(
+ await checkNonPersistedSessions(
{
savedObjectsClient,
client: mockClient,
@@ -401,6 +157,7 @@ describe('getSearchStatus', () => {
attributes: {
persisted: false,
status: SearchSessionStatus.IN_PROGRESS,
+ expires: moment().add(moment.duration(3, 'm')),
created: moment().subtract(moment.duration(3, 'm')),
touched: moment().subtract(moment.duration(2, 'm')),
idMapping: {
@@ -415,7 +172,7 @@ describe('getSearchStatus', () => {
total: 1,
} as any);
- await checkRunningSessions(
+ await checkNonPersistedSessions(
{
savedObjectsClient,
client: mockClient,
@@ -441,6 +198,7 @@ describe('getSearchStatus', () => {
status: SearchSessionStatus.IN_PROGRESS,
created: moment().subtract(moment.duration(3, 'm')),
touched: moment().subtract(moment.duration(2, 'm')),
+ expires: moment().add(moment.duration(3, 'm')),
idMapping: {
'map-key': {
strategy: ENHANCED_ES_SEARCH_STRATEGY,
@@ -453,7 +211,7 @@ describe('getSearchStatus', () => {
total: 1,
} as any);
- await checkRunningSessions(
+ await checkNonPersistedSessions(
{
savedObjectsClient,
client: mockClient,
@@ -481,6 +239,7 @@ describe('getSearchStatus', () => {
attributes: {
persisted: false,
status: SearchSessionStatus.COMPLETE,
+ expires: moment().add(moment.duration(3, 'm')),
created: moment().subtract(moment.duration(30, 'm')),
touched: moment().subtract(moment.duration(6, 'm')),
idMapping: {
@@ -501,7 +260,7 @@ describe('getSearchStatus', () => {
total: 1,
} as any);
- await checkRunningSessions(
+ await checkNonPersistedSessions(
{
savedObjectsClient,
client: mockClient,
@@ -530,6 +289,7 @@ describe('getSearchStatus', () => {
attributes: {
persisted: false,
status: SearchSessionStatus.COMPLETE,
+ expires: moment().add(moment.duration(3, 'm')),
created: moment().subtract(moment.duration(30, 'm')),
touched: moment().subtract(moment.duration(6, 'm')),
idMapping: {
@@ -545,7 +305,7 @@ describe('getSearchStatus', () => {
total: 1,
} as any);
- await checkRunningSessions(
+ await checkNonPersistedSessions(
{
savedObjectsClient,
client: mockClient,
@@ -573,6 +333,7 @@ describe('getSearchStatus', () => {
status: SearchSessionStatus.IN_PROGRESS,
created: moment().subtract(moment.duration(3, 'm')),
touched: moment().subtract(moment.duration(10, 's')),
+ expires: moment().add(moment.duration(3, 'm')),
idMapping: {
'search-hash': {
id: 'search-id',
@@ -594,7 +355,7 @@ describe('getSearchStatus', () => {
},
});
- await checkRunningSessions(
+ await checkNonPersistedSessions(
{
savedObjectsClient,
client: mockClient,
@@ -614,6 +375,7 @@ describe('getSearchStatus', () => {
id: '123',
attributes: {
status: SearchSessionStatus.ERROR,
+ expires: moment().add(moment.duration(3, 'm')),
idMapping: {
'search-hash': {
id: 'search-id',
@@ -633,7 +395,7 @@ describe('getSearchStatus', () => {
total: 1,
} as any);
- await checkRunningSessions(
+ await checkNonPersistedSessions(
{
savedObjectsClient,
client: mockClient,
@@ -653,6 +415,7 @@ describe('getSearchStatus', () => {
namespaces: ['awesome'],
attributes: {
status: SearchSessionStatus.IN_PROGRESS,
+ expires: moment().add(moment.duration(3, 'm')),
touched: '123',
idMapping: {
'search-hash': {
@@ -676,7 +439,7 @@ describe('getSearchStatus', () => {
},
});
- await checkRunningSessions(
+ await checkNonPersistedSessions(
{
savedObjectsClient,
client: mockClient,
@@ -696,6 +459,7 @@ describe('getSearchStatus', () => {
const so = {
attributes: {
status: SearchSessionStatus.IN_PROGRESS,
+ expires: moment().add(moment.duration(3, 'm')),
touched: '123',
idMapping: {
'search-hash': {
@@ -719,7 +483,7 @@ describe('getSearchStatus', () => {
},
});
- await checkRunningSessions(
+ await checkNonPersistedSessions(
{
savedObjectsClient,
client: mockClient,
@@ -744,6 +508,7 @@ describe('getSearchStatus', () => {
savedObjectsClient.bulkUpdate = jest.fn();
const so = {
attributes: {
+ expires: moment().add(moment.duration(3, 'm')),
idMapping: {
'search-hash': {
id: 'search-id',
@@ -766,7 +531,7 @@ describe('getSearchStatus', () => {
},
});
- await checkRunningSessions(
+ await checkNonPersistedSessions(
{
savedObjectsClient,
client: mockClient,
diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_non_persiseted_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/check_non_persiseted_sessions.ts
new file mode 100644
index 0000000000000..8c75ce91cac6a
--- /dev/null
+++ b/x-pack/plugins/data_enhanced/server/search/session/check_non_persiseted_sessions.ts
@@ -0,0 +1,129 @@
+/*
+ * 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 { SavedObjectsFindResult } from 'kibana/server';
+import moment from 'moment';
+import { EMPTY } from 'rxjs';
+import { catchError, concatMap } from 'rxjs/operators';
+import {
+ nodeBuilder,
+ ENHANCED_ES_SEARCH_STRATEGY,
+ SEARCH_SESSION_TYPE,
+ SearchSessionSavedObjectAttributes,
+ SearchSessionStatus,
+ KueryNode,
+} from '../../../../../../src/plugins/data/common';
+import { checkSearchSessionsByPage, getSearchSessionsPage$ } from './get_search_session_page';
+import { SearchSessionsConfig, CheckSearchSessionsDeps } from './types';
+import { bulkUpdateSessions, getAllSessionsStatusUpdates } from './update_session_status';
+
+export const SEARCH_SESSIONS_CLEANUP_TASK_TYPE = 'search_sessions_cleanup';
+export const SEARCH_SESSIONS_CLEANUP_TASK_ID = `data_enhanced_${SEARCH_SESSIONS_CLEANUP_TASK_TYPE}`;
+
+function isSessionStale(
+ session: SavedObjectsFindResult,
+ config: SearchSessionsConfig
+) {
+ const curTime = moment();
+ // Delete cancelled sessions immediately
+ if (session.attributes.status === SearchSessionStatus.CANCELLED) return true;
+ // Delete if a running session wasn't polled for in the last notTouchedInProgressTimeout OR
+ // if a completed \ errored \ canceled session wasn't saved for within notTouchedTimeout
+ return (
+ (session.attributes.status === SearchSessionStatus.IN_PROGRESS &&
+ curTime.diff(moment(session.attributes.touched), 'ms') >
+ config.notTouchedInProgressTimeout.asMilliseconds()) ||
+ (session.attributes.status !== SearchSessionStatus.IN_PROGRESS &&
+ curTime.diff(moment(session.attributes.touched), 'ms') >
+ config.notTouchedTimeout.asMilliseconds())
+ );
+}
+
+function checkNonPersistedSessionsPage(
+ deps: CheckSearchSessionsDeps,
+ config: SearchSessionsConfig,
+ filter: KueryNode,
+ page: number
+) {
+ const { logger, client, savedObjectsClient } = deps;
+ logger.debug(`${SEARCH_SESSIONS_CLEANUP_TASK_TYPE} Fetching sessions from page ${page}`);
+ return getSearchSessionsPage$(deps, filter, config.pageSize, page).pipe(
+ concatMap(async (nonPersistedSearchSessions) => {
+ if (!nonPersistedSearchSessions.total) return nonPersistedSearchSessions;
+
+ logger.debug(
+ `${SEARCH_SESSIONS_CLEANUP_TASK_TYPE} Found ${nonPersistedSearchSessions.total} sessions, processing ${nonPersistedSearchSessions.saved_objects.length}`
+ );
+
+ const updatedSessions = await getAllSessionsStatusUpdates(deps, nonPersistedSearchSessions);
+ const deletedSessionIds: string[] = [];
+
+ await Promise.all(
+ nonPersistedSearchSessions.saved_objects.map(async (session) => {
+ if (isSessionStale(session, config)) {
+ // delete saved object to free up memory
+ // TODO: there's a potential rare edge case of deleting an object and then receiving a new trackId for that same session!
+ // Maybe we want to change state to deleted and cleanup later?
+ logger.debug(`Deleting stale session | ${session.id}`);
+ try {
+ deletedSessionIds.push(session.id);
+ await savedObjectsClient.delete(SEARCH_SESSION_TYPE, session.id, {
+ namespace: session.namespaces?.[0],
+ });
+ } catch (e) {
+ logger.error(
+ `${SEARCH_SESSIONS_CLEANUP_TASK_TYPE} Error while deleting session ${session.id}: ${e.message}`
+ );
+ }
+
+ // Send a delete request for each async search to ES
+ Object.keys(session.attributes.idMapping).map(async (searchKey: string) => {
+ const searchInfo = session.attributes.idMapping[searchKey];
+ if (searchInfo.strategy === ENHANCED_ES_SEARCH_STRATEGY) {
+ try {
+ await client.asyncSearch.delete({ id: searchInfo.id });
+ } catch (e) {
+ if (e.message !== 'resource_not_found_exception') {
+ logger.error(
+ `${SEARCH_SESSIONS_CLEANUP_TASK_TYPE} Error while deleting async_search ${searchInfo.id}: ${e.message}`
+ );
+ }
+ }
+ }
+ });
+ }
+ })
+ );
+
+ const nonDeletedSessions = updatedSessions.filter((updateSession) => {
+ return deletedSessionIds.indexOf(updateSession.id) === -1;
+ });
+
+ await bulkUpdateSessions(deps, nonDeletedSessions);
+
+ return nonPersistedSearchSessions;
+ })
+ );
+}
+
+export function checkNonPersistedSessions(
+ deps: CheckSearchSessionsDeps,
+ config: SearchSessionsConfig
+) {
+ const { logger } = deps;
+
+ const filters = nodeBuilder.is(`${SEARCH_SESSION_TYPE}.attributes.persisted`, 'false');
+
+ return checkSearchSessionsByPage(checkNonPersistedSessionsPage, deps, config, filters).pipe(
+ catchError((e) => {
+ logger.error(
+ `${SEARCH_SESSIONS_CLEANUP_TASK_TYPE} Error while processing sessions: ${e?.message}`
+ );
+ return EMPTY;
+ })
+ );
+}
diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.test.ts b/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.test.ts
new file mode 100644
index 0000000000000..e0b1b74b57d02
--- /dev/null
+++ b/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.test.ts
@@ -0,0 +1,76 @@
+/*
+ * 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 { checkPersistedSessionsProgress } from './check_persisted_sessions';
+import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks';
+import { SearchSessionsConfig } from './types';
+import moment from 'moment';
+import { SavedObjectsClientContract } from '../../../../../../src/core/server';
+
+describe('checkPersistedSessionsProgress', () => {
+ let mockClient: any;
+ let savedObjectsClient: jest.Mocked;
+ const config: SearchSessionsConfig = {
+ enabled: true,
+ pageSize: 5,
+ notTouchedInProgressTimeout: moment.duration(1, 'm'),
+ notTouchedTimeout: moment.duration(5, 'm'),
+ maxUpdateRetries: 3,
+ defaultExpiration: moment.duration(7, 'd'),
+ trackingInterval: moment.duration(10, 's'),
+ cleanupInterval: moment.duration(10, 's'),
+ expireInterval: moment.duration(10, 'm'),
+ monitoringTaskTimeout: moment.duration(5, 'm'),
+ management: {} as any,
+ };
+ const mockLogger: any = {
+ debug: jest.fn(),
+ warn: jest.fn(),
+ error: jest.fn(),
+ };
+
+ beforeEach(() => {
+ savedObjectsClient = savedObjectsClientMock.create();
+ mockClient = {
+ asyncSearch: {
+ status: jest.fn(),
+ delete: jest.fn(),
+ },
+ eql: {
+ status: jest.fn(),
+ delete: jest.fn(),
+ },
+ };
+ });
+
+ test('fetches only running persisted sessions', async () => {
+ savedObjectsClient.find.mockResolvedValue({
+ saved_objects: [],
+ total: 0,
+ } as any);
+
+ await checkPersistedSessionsProgress(
+ {
+ savedObjectsClient,
+ client: mockClient,
+ logger: mockLogger,
+ },
+ config
+ );
+
+ const [findInput] = savedObjectsClient.find.mock.calls[0];
+
+ expect(findInput.filter.arguments[0].arguments[0].value).toBe(
+ 'search-session.attributes.persisted'
+ );
+ expect(findInput.filter.arguments[0].arguments[1].value).toBe('true');
+ expect(findInput.filter.arguments[1].arguments[0].value).toBe(
+ 'search-session.attributes.status'
+ );
+ expect(findInput.filter.arguments[1].arguments[1].value).toBe('in_progress');
+ });
+});
diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts
new file mode 100644
index 0000000000000..0d51e97952275
--- /dev/null
+++ b/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EMPTY, Observable } from 'rxjs';
+import { catchError, concatMap } from 'rxjs/operators';
+import {
+ nodeBuilder,
+ SEARCH_SESSION_TYPE,
+ SearchSessionStatus,
+ KueryNode,
+} from '../../../../../../src/plugins/data/common';
+import { checkSearchSessionsByPage, getSearchSessionsPage$ } from './get_search_session_page';
+import { SearchSessionsConfig, CheckSearchSessionsDeps, SearchSessionsResponse } from './types';
+import { bulkUpdateSessions, getAllSessionsStatusUpdates } from './update_session_status';
+
+export const SEARCH_SESSIONS_TASK_TYPE = 'search_sessions_monitor';
+export const SEARCH_SESSIONS_TASK_ID = `data_enhanced_${SEARCH_SESSIONS_TASK_TYPE}`;
+
+function checkPersistedSessionsPage(
+ deps: CheckSearchSessionsDeps,
+ config: SearchSessionsConfig,
+ filter: KueryNode,
+ page: number
+): Observable {
+ const { logger } = deps;
+ logger.debug(`${SEARCH_SESSIONS_TASK_TYPE} Fetching sessions from page ${page}`);
+ return getSearchSessionsPage$(deps, filter, config.pageSize, page).pipe(
+ concatMap(async (persistedSearchSessions) => {
+ if (!persistedSearchSessions.total) return persistedSearchSessions;
+
+ logger.debug(
+ `${SEARCH_SESSIONS_TASK_TYPE} Found ${persistedSearchSessions.total} sessions, processing ${persistedSearchSessions.saved_objects.length}`
+ );
+
+ const updatedSessions = await getAllSessionsStatusUpdates(deps, persistedSearchSessions);
+ await bulkUpdateSessions(deps, updatedSessions);
+
+ return persistedSearchSessions;
+ })
+ );
+}
+
+export function checkPersistedSessionsProgress(
+ deps: CheckSearchSessionsDeps,
+ config: SearchSessionsConfig
+) {
+ const { logger } = deps;
+
+ const persistedSessionsFilter = nodeBuilder.and([
+ nodeBuilder.is(`${SEARCH_SESSION_TYPE}.attributes.persisted`, 'true'),
+ nodeBuilder.is(
+ `${SEARCH_SESSION_TYPE}.attributes.status`,
+ SearchSessionStatus.IN_PROGRESS.toString()
+ ),
+ ]);
+
+ return checkSearchSessionsByPage(
+ checkPersistedSessionsPage,
+ deps,
+ config,
+ persistedSessionsFilter
+ ).pipe(
+ catchError((e) => {
+ logger.error(`${SEARCH_SESSIONS_TASK_TYPE} Error while processing sessions: ${e?.message}`);
+ return EMPTY;
+ })
+ );
+}
diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts
deleted file mode 100644
index 6787d31ed2b74..0000000000000
--- a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts
+++ /dev/null
@@ -1,257 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import {
- ElasticsearchClient,
- Logger,
- SavedObjectsClientContract,
- SavedObjectsFindResult,
- SavedObjectsUpdateResponse,
-} from 'kibana/server';
-import moment from 'moment';
-import { EMPTY, from, Observable } from 'rxjs';
-import { catchError, concatMap } from 'rxjs/operators';
-import {
- nodeBuilder,
- ENHANCED_ES_SEARCH_STRATEGY,
- SEARCH_SESSION_TYPE,
- SearchSessionRequestInfo,
- SearchSessionSavedObjectAttributes,
- SearchSessionStatus,
-} from '../../../../../../src/plugins/data/common';
-import { getSearchStatus } from './get_search_status';
-import { getSessionStatus } from './get_session_status';
-import { SearchSessionsConfig, SearchStatus } from './types';
-
-export interface CheckRunningSessionsDeps {
- savedObjectsClient: SavedObjectsClientContract;
- client: ElasticsearchClient;
- logger: Logger;
-}
-
-function isSessionStale(
- session: SavedObjectsFindResult,
- config: SearchSessionsConfig,
- logger: Logger
-) {
- const curTime = moment();
- // Delete if a running session wasn't polled for in the last notTouchedInProgressTimeout OR
- // if a completed \ errored \ canceled session wasn't saved for within notTouchedTimeout
- return (
- (session.attributes.status === SearchSessionStatus.IN_PROGRESS &&
- curTime.diff(moment(session.attributes.touched), 'ms') >
- config.notTouchedInProgressTimeout.asMilliseconds()) ||
- (session.attributes.status !== SearchSessionStatus.IN_PROGRESS &&
- curTime.diff(moment(session.attributes.touched), 'ms') >
- config.notTouchedTimeout.asMilliseconds())
- );
-}
-
-async function updateSessionStatus(
- session: SavedObjectsFindResult,
- client: ElasticsearchClient,
- logger: Logger
-) {
- let sessionUpdated = false;
-
- // Check statuses of all running searches
- await Promise.all(
- Object.keys(session.attributes.idMapping).map(async (searchKey: string) => {
- const updateSearchRequest = (
- currentStatus: Pick
- ) => {
- sessionUpdated = true;
- session.attributes.idMapping[searchKey] = {
- ...session.attributes.idMapping[searchKey],
- ...currentStatus,
- };
- };
-
- const searchInfo = session.attributes.idMapping[searchKey];
- if (searchInfo.status === SearchStatus.IN_PROGRESS) {
- try {
- const currentStatus = await getSearchStatus(client, searchInfo.id);
-
- if (currentStatus.status !== searchInfo.status) {
- logger.debug(`search ${searchInfo.id} | status changed to ${currentStatus.status}`);
- updateSearchRequest(currentStatus);
- }
- } catch (e) {
- logger.error(e);
- updateSearchRequest({
- status: SearchStatus.ERROR,
- error: e.message || e.meta.error?.caused_by?.reason,
- });
- }
- }
- })
- );
-
- // And only then derive the session's status
- const sessionStatus = getSessionStatus(session.attributes);
- if (sessionStatus !== session.attributes.status) {
- const now = new Date().toISOString();
- session.attributes.status = sessionStatus;
- session.attributes.touched = now;
- if (sessionStatus === SearchSessionStatus.COMPLETE) {
- session.attributes.completed = now;
- } else if (session.attributes.completed) {
- session.attributes.completed = null;
- }
- sessionUpdated = true;
- }
-
- return sessionUpdated;
-}
-
-function getSavedSearchSessionsPage$(
- { savedObjectsClient, logger }: CheckRunningSessionsDeps,
- config: SearchSessionsConfig,
- page: number
-) {
- logger.debug(`Fetching saved search sessions page ${page}`);
- return from(
- savedObjectsClient.find({
- page,
- perPage: config.pageSize,
- type: SEARCH_SESSION_TYPE,
- namespaces: ['*'],
- // process older sessions first
- sortField: 'touched',
- sortOrder: 'asc',
- filter: nodeBuilder.or([
- nodeBuilder.and([
- nodeBuilder.is(
- `${SEARCH_SESSION_TYPE}.attributes.status`,
- SearchSessionStatus.IN_PROGRESS.toString()
- ),
- nodeBuilder.is(`${SEARCH_SESSION_TYPE}.attributes.persisted`, 'true'),
- ]),
- nodeBuilder.is(`${SEARCH_SESSION_TYPE}.attributes.persisted`, 'false'),
- ]),
- })
- );
-}
-
-function checkRunningSessionsPage(
- deps: CheckRunningSessionsDeps,
- config: SearchSessionsConfig,
- page: number
-) {
- const { logger, client, savedObjectsClient } = deps;
- return getSavedSearchSessionsPage$(deps, config, page).pipe(
- concatMap(async (runningSearchSessionsResponse) => {
- if (!runningSearchSessionsResponse.total) return;
-
- logger.debug(
- `Found ${runningSearchSessionsResponse.total} running sessions, processing ${runningSearchSessionsResponse.saved_objects.length} sessions from page ${page}`
- );
-
- const updatedSessions = new Array<
- SavedObjectsFindResult
- >();
-
- await Promise.all(
- runningSearchSessionsResponse.saved_objects.map(async (session) => {
- const updated = await updateSessionStatus(session, client, logger);
- let deleted = false;
-
- if (!session.attributes.persisted) {
- if (isSessionStale(session, config, logger)) {
- // delete saved object to free up memory
- // TODO: there's a potential rare edge case of deleting an object and then receiving a new trackId for that same session!
- // Maybe we want to change state to deleted and cleanup later?
- logger.debug(`Deleting stale session | ${session.id}`);
- try {
- await savedObjectsClient.delete(SEARCH_SESSION_TYPE, session.id, {
- namespace: session.namespaces?.[0],
- });
- deleted = true;
- } catch (e) {
- logger.error(
- `Error while deleting stale search session ${session.id}: ${e.message}`
- );
- }
-
- // Send a delete request for each async search to ES
- Object.keys(session.attributes.idMapping).map(async (searchKey: string) => {
- const searchInfo = session.attributes.idMapping[searchKey];
- if (searchInfo.strategy === ENHANCED_ES_SEARCH_STRATEGY) {
- try {
- await client.asyncSearch.delete({ id: searchInfo.id });
- } catch (e) {
- logger.error(
- `Error while deleting async_search ${searchInfo.id}: ${e.message}`
- );
- }
- }
- });
- }
- }
-
- if (updated && !deleted) {
- updatedSessions.push(session);
- }
- })
- );
-
- // Do a bulk update
- if (updatedSessions.length) {
- // If there's an error, we'll try again in the next iteration, so there's no need to check the output.
- const updatedResponse = await savedObjectsClient.bulkUpdate(
- updatedSessions.map((session) => ({
- ...session,
- namespace: session.namespaces?.[0],
- }))
- );
-
- const success: Array> = [];
- const fail: Array> = [];
-
- updatedResponse.saved_objects.forEach((savedObjectResponse) => {
- if ('error' in savedObjectResponse) {
- fail.push(savedObjectResponse);
- logger.error(
- `Error while updating search session ${savedObjectResponse?.id}: ${savedObjectResponse.error?.message}`
- );
- } else {
- success.push(savedObjectResponse);
- }
- });
-
- logger.debug(`Updating search sessions: success: ${success.length}, fail: ${fail.length}`);
- }
-
- return runningSearchSessionsResponse;
- })
- );
-}
-
-export function checkRunningSessions(deps: CheckRunningSessionsDeps, config: SearchSessionsConfig) {
- const { logger } = deps;
-
- const checkRunningSessionsByPage = (nextPage = 1): Observable =>
- checkRunningSessionsPage(deps, config, nextPage).pipe(
- concatMap((result) => {
- if (!result || !result.saved_objects || result.saved_objects.length < config.pageSize) {
- return EMPTY;
- } else {
- // TODO: while processing previous page session list might have been changed and we might skip a session,
- // because it would appear now on a different "page".
- // This isn't critical, as we would pick it up on a next task iteration, but maybe we could improve this somehow
- return checkRunningSessionsByPage(result.page + 1);
- }
- })
- );
-
- return checkRunningSessionsByPage().pipe(
- catchError((e) => {
- logger.error(`Error while processing search sessions: ${e?.message}`);
- return EMPTY;
- })
- );
-}
diff --git a/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts
new file mode 100644
index 0000000000000..e261c324f440f
--- /dev/null
+++ b/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts
@@ -0,0 +1,74 @@
+/*
+ * 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 { EMPTY, Observable } from 'rxjs';
+import { catchError, concatMap } from 'rxjs/operators';
+import {
+ nodeBuilder,
+ SEARCH_SESSION_TYPE,
+ SearchSessionStatus,
+ KueryNode,
+} from '../../../../../../src/plugins/data/common';
+import { checkSearchSessionsByPage, getSearchSessionsPage$ } from './get_search_session_page';
+import { SearchSessionsConfig, CheckSearchSessionsDeps, SearchSessionsResponse } from './types';
+import { bulkUpdateSessions, getAllSessionsStatusUpdates } from './update_session_status';
+
+export const SEARCH_SESSIONS_EXPIRE_TASK_TYPE = 'search_sessions_expire';
+export const SEARCH_SESSIONS_EXPIRE_TASK_ID = `data_enhanced_${SEARCH_SESSIONS_EXPIRE_TASK_TYPE}`;
+
+function checkSessionExpirationPage(
+ deps: CheckSearchSessionsDeps,
+ config: SearchSessionsConfig,
+ filter: KueryNode,
+ page: number
+): Observable {
+ const { logger } = deps;
+ logger.debug(`${SEARCH_SESSIONS_EXPIRE_TASK_TYPE} Fetching sessions from page ${page}`);
+ return getSearchSessionsPage$(deps, filter, config.pageSize, page).pipe(
+ concatMap(async (searchSessions) => {
+ if (!searchSessions.total) return searchSessions;
+
+ logger.debug(
+ `${SEARCH_SESSIONS_EXPIRE_TASK_TYPE} Found ${searchSessions.total} sessions, processing ${searchSessions.saved_objects.length}`
+ );
+
+ const updatedSessions = await getAllSessionsStatusUpdates(deps, searchSessions);
+ await bulkUpdateSessions(deps, updatedSessions);
+
+ return searchSessions;
+ })
+ );
+}
+
+export function checkPersistedCompletedSessionExpiration(
+ deps: CheckSearchSessionsDeps,
+ config: SearchSessionsConfig
+) {
+ const { logger } = deps;
+
+ const persistedSessionsFilter = nodeBuilder.and([
+ nodeBuilder.is(`${SEARCH_SESSION_TYPE}.attributes.persisted`, 'true'),
+ nodeBuilder.is(
+ `${SEARCH_SESSION_TYPE}.attributes.status`,
+ SearchSessionStatus.COMPLETE.toString()
+ ),
+ ]);
+
+ return checkSearchSessionsByPage(
+ checkSessionExpirationPage,
+ deps,
+ config,
+ persistedSessionsFilter
+ ).pipe(
+ catchError((e) => {
+ logger.error(
+ `${SEARCH_SESSIONS_EXPIRE_TASK_TYPE} Error while processing sessions: ${e?.message}`
+ );
+ return EMPTY;
+ })
+ );
+}
diff --git a/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.test.ts b/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.test.ts
new file mode 100644
index 0000000000000..df2b7d964642d
--- /dev/null
+++ b/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.test.ts
@@ -0,0 +1,282 @@
+/*
+ * 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 { checkSearchSessionsByPage, getSearchSessionsPage$ } from './get_search_session_page';
+import {
+ SearchSessionStatus,
+ ENHANCED_ES_SEARCH_STRATEGY,
+} from '../../../../../../src/plugins/data/common';
+import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks';
+import { SearchSessionsConfig, SearchStatus } from './types';
+import moment from 'moment';
+import { SavedObjectsClientContract } from '../../../../../../src/core/server';
+import { of, Subject, throwError } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+jest.useFakeTimers();
+
+describe('checkSearchSessionsByPage', () => {
+ const mockClient = {} as any;
+ let savedObjectsClient: jest.Mocked;
+ const config: SearchSessionsConfig = {
+ enabled: true,
+ pageSize: 5,
+ management: {} as any,
+ } as any;
+ const mockLogger: any = {
+ debug: jest.fn(),
+ warn: jest.fn(),
+ error: jest.fn(),
+ };
+
+ const emptySO = {
+ attributes: {
+ persisted: false,
+ status: SearchSessionStatus.IN_PROGRESS,
+ created: moment().subtract(moment.duration(3, 'm')),
+ touched: moment().subtract(moment.duration(10, 's')),
+ idMapping: {},
+ },
+ };
+
+ beforeEach(() => {
+ savedObjectsClient = savedObjectsClientMock.create();
+ });
+
+ describe('getSearchSessionsPage$', () => {
+ test('sorting is by "touched"', async () => {
+ savedObjectsClient.find.mockResolvedValueOnce({
+ saved_objects: [],
+ total: 0,
+ } as any);
+
+ await getSearchSessionsPage$(
+ {
+ savedObjectsClient,
+ } as any,
+ {
+ type: 'literal',
+ },
+ 1,
+ 1
+ );
+
+ expect(savedObjectsClient.find).toHaveBeenCalledWith(
+ expect.objectContaining({ sortField: 'touched', sortOrder: 'asc' })
+ );
+ });
+ });
+
+ describe('pagination', () => {
+ test('fetches one page if got empty response', async () => {
+ const checkFn = jest.fn().mockReturnValue(of(undefined));
+
+ await checkSearchSessionsByPage(
+ checkFn,
+ {
+ savedObjectsClient,
+ client: mockClient,
+ logger: mockLogger,
+ },
+ config,
+ []
+ ).toPromise();
+
+ expect(checkFn).toHaveBeenCalledTimes(1);
+ });
+
+ test('fetches one page if got response with no saved objects', async () => {
+ const checkFn = jest.fn().mockReturnValue(
+ of({
+ total: 0,
+ })
+ );
+
+ await checkSearchSessionsByPage(
+ checkFn,
+ {
+ savedObjectsClient,
+ client: mockClient,
+ logger: mockLogger,
+ },
+ config,
+ []
+ ).toPromise();
+
+ expect(checkFn).toHaveBeenCalledTimes(1);
+ });
+
+ test('fetches one page if less than page size object are returned', async () => {
+ const checkFn = jest.fn().mockReturnValue(
+ of({
+ saved_objects: [emptySO, emptySO],
+ total: 5,
+ })
+ );
+
+ await checkSearchSessionsByPage(
+ checkFn,
+ {
+ savedObjectsClient,
+ client: mockClient,
+ logger: mockLogger,
+ },
+ config,
+ []
+ ).toPromise();
+
+ expect(checkFn).toHaveBeenCalledTimes(1);
+ });
+
+ test('fetches two pages if exactly page size objects are returned', async () => {
+ let i = 0;
+
+ const checkFn = jest.fn().mockImplementation(() =>
+ of({
+ saved_objects: i++ === 0 ? [emptySO, emptySO, emptySO, emptySO, emptySO] : [],
+ total: 5,
+ page: i,
+ })
+ );
+
+ await checkSearchSessionsByPage(
+ checkFn,
+ {
+ savedObjectsClient,
+ client: mockClient,
+ logger: mockLogger,
+ },
+ config,
+ []
+ ).toPromise();
+
+ expect(checkFn).toHaveBeenCalledTimes(2);
+
+ // validate that page number increases
+ const page1 = checkFn.mock.calls[0][3];
+ const page2 = checkFn.mock.calls[1][3];
+ expect(page1).toBe(1);
+ expect(page2).toBe(2);
+ });
+
+ test('fetches two pages if page size +1 objects are returned', async () => {
+ let i = 0;
+
+ const checkFn = jest.fn().mockImplementation(() =>
+ of({
+ saved_objects: i++ === 0 ? [emptySO, emptySO, emptySO, emptySO, emptySO] : [emptySO],
+ total: i === 0 ? 5 : 1,
+ page: i,
+ })
+ );
+
+ await checkSearchSessionsByPage(
+ checkFn,
+ {
+ savedObjectsClient,
+ client: mockClient,
+ logger: mockLogger,
+ },
+ config,
+ []
+ ).toPromise();
+
+ expect(checkFn).toHaveBeenCalledTimes(2);
+ });
+
+ test('sessions fetched in the beginning are processed even if sessions in the end fail', async () => {
+ let i = 0;
+
+ const checkFn = jest.fn().mockImplementation(() => {
+ if (++i === 2) {
+ return throwError('Fake find error...');
+ }
+ return of({
+ saved_objects:
+ i <= 5
+ ? [
+ i === 1
+ ? {
+ id: '123',
+ attributes: {
+ persisted: false,
+ status: SearchSessionStatus.IN_PROGRESS,
+ created: moment().subtract(moment.duration(3, 'm')),
+ touched: moment().subtract(moment.duration(2, 'm')),
+ idMapping: {
+ 'map-key': {
+ strategy: ENHANCED_ES_SEARCH_STRATEGY,
+ id: 'async-id',
+ status: SearchStatus.IN_PROGRESS,
+ },
+ },
+ },
+ }
+ : emptySO,
+ emptySO,
+ emptySO,
+ emptySO,
+ emptySO,
+ ]
+ : [],
+ total: 25,
+ page: i,
+ });
+ });
+
+ await checkSearchSessionsByPage(
+ checkFn,
+ {
+ savedObjectsClient,
+ client: mockClient,
+ logger: mockLogger,
+ },
+ config,
+ []
+ )
+ .toPromise()
+ .catch(() => {});
+
+ expect(checkFn).toHaveBeenCalledTimes(2);
+ });
+
+ test('fetching is abortable', async () => {
+ let i = 0;
+ const abort$ = new Subject();
+
+ const checkFn = jest.fn().mockImplementation(() => {
+ if (++i === 2) {
+ abort$.next();
+ }
+
+ return of({
+ saved_objects: i <= 5 ? [emptySO, emptySO, emptySO, emptySO, emptySO] : [],
+ total: 25,
+ page: i,
+ });
+ });
+
+ await checkSearchSessionsByPage(
+ checkFn,
+ {
+ savedObjectsClient,
+ client: mockClient,
+ logger: mockLogger,
+ },
+ config,
+ []
+ )
+ .pipe(takeUntil(abort$))
+ .toPromise()
+ .catch(() => {});
+
+ jest.runAllTimers();
+
+ // if not for `abort$` then this would be called 6 times!
+ expect(checkFn).toHaveBeenCalledTimes(2);
+ });
+ });
+});
diff --git a/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.ts b/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.ts
new file mode 100644
index 0000000000000..74306bac39f7d
--- /dev/null
+++ b/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.ts
@@ -0,0 +1,61 @@
+/*
+ * 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 { SavedObjectsClientContract, Logger } from 'kibana/server';
+import { from, Observable, EMPTY } from 'rxjs';
+import { concatMap } from 'rxjs/operators';
+import {
+ SearchSessionSavedObjectAttributes,
+ SEARCH_SESSION_TYPE,
+ KueryNode,
+} from '../../../../../../src/plugins/data/common';
+import { CheckSearchSessionsDeps, CheckSearchSessionsFn, SearchSessionsConfig } from './types';
+
+export interface GetSessionsDeps {
+ savedObjectsClient: SavedObjectsClientContract;
+ logger: Logger;
+}
+
+export function getSearchSessionsPage$(
+ { savedObjectsClient }: GetSessionsDeps,
+ filter: KueryNode,
+ pageSize: number,
+ page: number
+) {
+ return from(
+ savedObjectsClient.find({
+ page,
+ perPage: pageSize,
+ type: SEARCH_SESSION_TYPE,
+ namespaces: ['*'],
+ // process older sessions first
+ sortField: 'touched',
+ sortOrder: 'asc',
+ filter,
+ })
+ );
+}
+
+export const checkSearchSessionsByPage = (
+ checkFn: CheckSearchSessionsFn,
+ deps: CheckSearchSessionsDeps,
+ config: SearchSessionsConfig,
+ filters: any,
+ nextPage = 1
+): Observable =>
+ checkFn(deps, config, filters, nextPage).pipe(
+ concatMap((result) => {
+ if (!result || !result.saved_objects || result.saved_objects.length < config.pageSize) {
+ return EMPTY;
+ } else {
+ // TODO: while processing previous page session list might have been changed and we might skip a session,
+ // because it would appear now on a different "page".
+ // This isn't critical, as we would pick it up on a next task iteration, but maybe we could improve this somehow
+ return checkSearchSessionsByPage(checkFn, deps, config, filters, result.page + 1);
+ }
+ })
+ );
diff --git a/x-pack/plugins/data_enhanced/server/search/session/index.ts b/x-pack/plugins/data_enhanced/server/search/session/index.ts
index deadeb3f8f07a..1e6841211bb66 100644
--- a/x-pack/plugins/data_enhanced/server/search/session/index.ts
+++ b/x-pack/plugins/data_enhanced/server/search/session/index.ts
@@ -6,4 +6,3 @@
*/
export * from './session_service';
-export { registerSearchSessionsTask, scheduleSearchSessionsTasks } from './monitoring_task';
diff --git a/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts b/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts
deleted file mode 100644
index 7b7b1412987be..0000000000000
--- a/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { Duration } from 'moment';
-import { filter, takeUntil } from 'rxjs/operators';
-import { BehaviorSubject } from 'rxjs';
-import {
- TaskManagerSetupContract,
- TaskManagerStartContract,
- RunContext,
- TaskRunCreatorFunction,
-} from '../../../../task_manager/server';
-import { checkRunningSessions } from './check_running_sessions';
-import { CoreSetup, SavedObjectsClient, Logger } from '../../../../../../src/core/server';
-import { ConfigSchema } from '../../../config';
-import { SEARCH_SESSION_TYPE } from '../../../../../../src/plugins/data/common';
-import { DataEnhancedStartDependencies } from '../../type';
-
-export const SEARCH_SESSIONS_TASK_TYPE = 'search_sessions_monitor';
-export const SEARCH_SESSIONS_TASK_ID = `data_enhanced_${SEARCH_SESSIONS_TASK_TYPE}`;
-
-interface SearchSessionTaskDeps {
- taskManager: TaskManagerSetupContract;
- logger: Logger;
- config: ConfigSchema;
-}
-
-function searchSessionRunner(
- core: CoreSetup,
- { logger, config }: SearchSessionTaskDeps
-): TaskRunCreatorFunction {
- return ({ taskInstance }: RunContext) => {
- const aborted$ = new BehaviorSubject(false);
- return {
- async run() {
- const sessionConfig = config.search.sessions;
- const [coreStart] = await core.getStartServices();
- if (!sessionConfig.enabled) {
- logger.debug('Search sessions are disabled. Skipping task.');
- return;
- }
- if (aborted$.getValue()) return;
-
- const internalRepo = coreStart.savedObjects.createInternalRepository([SEARCH_SESSION_TYPE]);
- const internalSavedObjectsClient = new SavedObjectsClient(internalRepo);
- await checkRunningSessions(
- {
- savedObjectsClient: internalSavedObjectsClient,
- client: coreStart.elasticsearch.client.asInternalUser,
- logger,
- },
- sessionConfig
- )
- .pipe(takeUntil(aborted$.pipe(filter((aborted) => aborted))))
- .toPromise();
-
- return {
- state: {},
- };
- },
- cancel: async () => {
- aborted$.next(true);
- },
- };
- };
-}
-
-export function registerSearchSessionsTask(
- core: CoreSetup,
- deps: SearchSessionTaskDeps
-) {
- deps.taskManager.registerTaskDefinitions({
- [SEARCH_SESSIONS_TASK_TYPE]: {
- title: 'Search Sessions Monitor',
- createTaskRunner: searchSessionRunner(core, deps),
- timeout: `${deps.config.search.sessions.monitoringTaskTimeout.asSeconds()}s`,
- },
- });
-}
-
-export async function unscheduleSearchSessionsTask(
- taskManager: TaskManagerStartContract,
- logger: Logger
-) {
- try {
- await taskManager.removeIfExists(SEARCH_SESSIONS_TASK_ID);
- logger.debug(`Search sessions cleared`);
- } catch (e) {
- logger.error(`Error clearing task, received ${e.message}`);
- }
-}
-
-export async function scheduleSearchSessionsTasks(
- taskManager: TaskManagerStartContract,
- logger: Logger,
- trackingInterval: Duration
-) {
- await taskManager.removeIfExists(SEARCH_SESSIONS_TASK_ID);
-
- try {
- await taskManager.ensureScheduled({
- id: SEARCH_SESSIONS_TASK_ID,
- taskType: SEARCH_SESSIONS_TASK_TYPE,
- schedule: {
- interval: `${trackingInterval.asSeconds()}s`,
- },
- state: {},
- params: {},
- });
-
- logger.debug(`Search sessions task, scheduled to run`);
- } catch (e) {
- logger.error(`Error scheduling task, received ${e.message}`);
- }
-}
diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts
index 374dbee2384d5..dd1eafa5d60f8 100644
--- a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts
+++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts
@@ -79,7 +79,9 @@ describe('SearchSessionService', () => {
maxUpdateRetries: MAX_UPDATE_RETRIES,
defaultExpiration: moment.duration(7, 'd'),
monitoringTaskTimeout: moment.duration(5, 'm'),
+ cleanupInterval: moment.duration(10, 's'),
trackingInterval: moment.duration(10, 's'),
+ expireInterval: moment.duration(10, 'm'),
management: {} as any,
},
},
@@ -157,7 +159,9 @@ describe('SearchSessionService', () => {
maxUpdateRetries: MAX_UPDATE_RETRIES,
defaultExpiration: moment.duration(7, 'd'),
trackingInterval: moment.duration(10, 's'),
+ expireInterval: moment.duration(10, 'm'),
monitoringTaskTimeout: moment.duration(5, 'm'),
+ cleanupInterval: moment.duration(10, 's'),
management: {} as any,
},
},
diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts
index 81a12f607935d..0998c1f42e183 100644
--- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts
+++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts
@@ -43,11 +43,26 @@ import { createRequestHash } from './utils';
import { ConfigSchema } from '../../../config';
import {
registerSearchSessionsTask,
- scheduleSearchSessionsTasks,
+ scheduleSearchSessionsTask,
unscheduleSearchSessionsTask,
-} from './monitoring_task';
+} from './setup_task';
import { SearchSessionsConfig, SearchStatus } from './types';
import { DataEnhancedStartDependencies } from '../../type';
+import {
+ checkPersistedSessionsProgress,
+ SEARCH_SESSIONS_TASK_ID,
+ SEARCH_SESSIONS_TASK_TYPE,
+} from './check_persisted_sessions';
+import {
+ SEARCH_SESSIONS_CLEANUP_TASK_TYPE,
+ checkNonPersistedSessions,
+ SEARCH_SESSIONS_CLEANUP_TASK_ID,
+} from './check_non_persiseted_sessions';
+import {
+ SEARCH_SESSIONS_EXPIRE_TASK_TYPE,
+ SEARCH_SESSIONS_EXPIRE_TASK_ID,
+ checkPersistedCompletedSessionExpiration,
+} from './expire_persisted_sessions';
export interface SearchSessionDependencies {
savedObjectsClient: SavedObjectsClientContract;
@@ -89,11 +104,35 @@ export class SearchSessionService
}
public setup(core: CoreSetup, deps: SetupDependencies) {
- registerSearchSessionsTask(core, {
+ const taskDeps = {
config: this.config,
taskManager: deps.taskManager,
logger: this.logger,
- });
+ };
+
+ registerSearchSessionsTask(
+ core,
+ taskDeps,
+ SEARCH_SESSIONS_TASK_TYPE,
+ 'persisted session progress',
+ checkPersistedSessionsProgress
+ );
+
+ registerSearchSessionsTask(
+ core,
+ taskDeps,
+ SEARCH_SESSIONS_CLEANUP_TASK_TYPE,
+ 'non persisted session cleanup',
+ checkNonPersistedSessions
+ );
+
+ registerSearchSessionsTask(
+ core,
+ taskDeps,
+ SEARCH_SESSIONS_EXPIRE_TASK_TYPE,
+ 'complete session expiration',
+ checkPersistedCompletedSessionExpiration
+ );
}
public async start(core: CoreStart, deps: StartDependencies) {
@@ -103,14 +142,37 @@ export class SearchSessionService
public stop() {}
private setupMonitoring = async (core: CoreStart, deps: StartDependencies) => {
+ const taskDeps = {
+ config: this.config,
+ taskManager: deps.taskManager,
+ logger: this.logger,
+ };
+
if (this.sessionConfig.enabled) {
- scheduleSearchSessionsTasks(
- deps.taskManager,
- this.logger,
+ scheduleSearchSessionsTask(
+ taskDeps,
+ SEARCH_SESSIONS_TASK_ID,
+ SEARCH_SESSIONS_TASK_TYPE,
this.sessionConfig.trackingInterval
);
+
+ scheduleSearchSessionsTask(
+ taskDeps,
+ SEARCH_SESSIONS_CLEANUP_TASK_ID,
+ SEARCH_SESSIONS_CLEANUP_TASK_TYPE,
+ this.sessionConfig.cleanupInterval
+ );
+
+ scheduleSearchSessionsTask(
+ taskDeps,
+ SEARCH_SESSIONS_EXPIRE_TASK_ID,
+ SEARCH_SESSIONS_EXPIRE_TASK_TYPE,
+ this.sessionConfig.expireInterval
+ );
} else {
- unscheduleSearchSessionsTask(deps.taskManager, this.logger);
+ unscheduleSearchSessionsTask(taskDeps, SEARCH_SESSIONS_TASK_ID);
+ unscheduleSearchSessionsTask(taskDeps, SEARCH_SESSIONS_CLEANUP_TASK_ID);
+ unscheduleSearchSessionsTask(taskDeps, SEARCH_SESSIONS_EXPIRE_TASK_ID);
}
};
diff --git a/x-pack/plugins/data_enhanced/server/search/session/setup_task.ts b/x-pack/plugins/data_enhanced/server/search/session/setup_task.ts
new file mode 100644
index 0000000000000..a4c9b6039ff64
--- /dev/null
+++ b/x-pack/plugins/data_enhanced/server/search/session/setup_task.ts
@@ -0,0 +1,121 @@
+/*
+ * 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 { Duration } from 'moment';
+import { filter, takeUntil } from 'rxjs/operators';
+import { BehaviorSubject } from 'rxjs';
+import { RunContext, TaskRunCreatorFunction } from '../../../../task_manager/server';
+import { CoreSetup, SavedObjectsClient } from '../../../../../../src/core/server';
+import { SEARCH_SESSION_TYPE } from '../../../../../../src/plugins/data/common';
+import { DataEnhancedStartDependencies } from '../../type';
+import {
+ SearchSessionTaskSetupDeps,
+ SearchSessionTaskStartDeps,
+ SearchSessionTaskFn,
+} from './types';
+
+export function searchSessionTaskRunner(
+ core: CoreSetup,
+ deps: SearchSessionTaskSetupDeps,
+ title: string,
+ checkFn: SearchSessionTaskFn
+): TaskRunCreatorFunction {
+ const { logger, config } = deps;
+ return ({ taskInstance }: RunContext) => {
+ const aborted$ = new BehaviorSubject(false);
+ return {
+ async run() {
+ try {
+ const sessionConfig = config.search.sessions;
+ const [coreStart] = await core.getStartServices();
+ if (!sessionConfig.enabled) {
+ logger.debug(`Search sessions are disabled. Skipping task ${title}.`);
+ return;
+ }
+ if (aborted$.getValue()) return;
+
+ const internalRepo = coreStart.savedObjects.createInternalRepository([
+ SEARCH_SESSION_TYPE,
+ ]);
+ const internalSavedObjectsClient = new SavedObjectsClient(internalRepo);
+ await checkFn(
+ {
+ logger,
+ client: coreStart.elasticsearch.client.asInternalUser,
+ savedObjectsClient: internalSavedObjectsClient,
+ },
+ sessionConfig
+ )
+ .pipe(takeUntil(aborted$.pipe(filter((aborted) => aborted))))
+ .toPromise();
+
+ return {
+ state: {},
+ };
+ } catch (e) {
+ logger.error(`An error occurred. Skipping task ${title}.`);
+ }
+ },
+ cancel: async () => {
+ aborted$.next(true);
+ },
+ };
+ };
+}
+
+export function registerSearchSessionsTask(
+ core: CoreSetup,
+ deps: SearchSessionTaskSetupDeps,
+ taskType: string,
+ title: string,
+ checkFn: SearchSessionTaskFn
+) {
+ deps.taskManager.registerTaskDefinitions({
+ [taskType]: {
+ title,
+ createTaskRunner: searchSessionTaskRunner(core, deps, title, checkFn),
+ timeout: `${deps.config.search.sessions.monitoringTaskTimeout.asSeconds()}s`,
+ },
+ });
+}
+
+export async function unscheduleSearchSessionsTask(
+ { taskManager, logger }: SearchSessionTaskStartDeps,
+ taskId: string
+) {
+ try {
+ await taskManager.removeIfExists(taskId);
+ logger.debug(`${taskId} cleared`);
+ } catch (e) {
+ logger.error(`${taskId} Error clearing task ${e.message}`);
+ }
+}
+
+export async function scheduleSearchSessionsTask(
+ { taskManager, logger }: SearchSessionTaskStartDeps,
+ taskId: string,
+ taskType: string,
+ interval: Duration
+) {
+ await taskManager.removeIfExists(taskId);
+
+ try {
+ await taskManager.ensureScheduled({
+ id: taskId,
+ taskType,
+ schedule: {
+ interval: `${interval.asSeconds()}s`,
+ },
+ state: {},
+ params: {},
+ });
+
+ logger.debug(`${taskId} scheduled to run`);
+ } catch (e) {
+ logger.error(`${taskId} Error scheduling task ${e.message}`);
+ }
+}
diff --git a/x-pack/plugins/data_enhanced/server/search/session/types.ts b/x-pack/plugins/data_enhanced/server/search/session/types.ts
index 0fa384e55f7d7..eadc3821c1043 100644
--- a/x-pack/plugins/data_enhanced/server/search/session/types.ts
+++ b/x-pack/plugins/data_enhanced/server/search/session/types.ts
@@ -5,6 +5,18 @@
* 2.0.
*/
+import {
+ ElasticsearchClient,
+ Logger,
+ SavedObjectsClientContract,
+ SavedObjectsFindResponse,
+} from 'kibana/server';
+import { Observable } from 'rxjs';
+import { KueryNode, SearchSessionSavedObjectAttributes } from 'src/plugins/data/common';
+import {
+ TaskManagerSetupContract,
+ TaskManagerStartContract,
+} from '../../../../../../x-pack/plugins/task_manager/server';
import { ConfigSchema } from '../../../config';
export enum SearchStatus {
@@ -14,3 +26,38 @@ export enum SearchStatus {
}
export type SearchSessionsConfig = ConfigSchema['search']['sessions'];
+
+export interface CheckSearchSessionsDeps {
+ savedObjectsClient: SavedObjectsClientContract;
+ client: ElasticsearchClient;
+ logger: Logger;
+}
+
+export interface SearchSessionTaskSetupDeps {
+ taskManager: TaskManagerSetupContract;
+ logger: Logger;
+ config: ConfigSchema;
+}
+
+export interface SearchSessionTaskStartDeps {
+ taskManager: TaskManagerStartContract;
+ logger: Logger;
+ config: ConfigSchema;
+}
+
+export type SearchSessionTaskFn = (
+ deps: CheckSearchSessionsDeps,
+ config: SearchSessionsConfig
+) => Observable;
+
+export type SearchSessionsResponse = SavedObjectsFindResponse<
+ SearchSessionSavedObjectAttributes,
+ unknown
+>;
+
+export type CheckSearchSessionsFn = (
+ deps: CheckSearchSessionsDeps,
+ config: SearchSessionsConfig,
+ filter: KueryNode,
+ page: number
+) => Observable;
diff --git a/x-pack/plugins/data_enhanced/server/search/session/update_session_status.test.ts b/x-pack/plugins/data_enhanced/server/search/session/update_session_status.test.ts
new file mode 100644
index 0000000000000..485a30fd54951
--- /dev/null
+++ b/x-pack/plugins/data_enhanced/server/search/session/update_session_status.test.ts
@@ -0,0 +1,323 @@
+/*
+ * 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 { bulkUpdateSessions, updateSessionStatus } from './update_session_status';
+import {
+ SearchSessionStatus,
+ SearchSessionSavedObjectAttributes,
+} from '../../../../../../src/plugins/data/common';
+import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks';
+import { SearchStatus } from './types';
+import moment from 'moment';
+import {
+ SavedObjectsBulkUpdateObject,
+ SavedObjectsClientContract,
+ SavedObjectsFindResult,
+} from '../../../../../../src/core/server';
+
+describe('bulkUpdateSessions', () => {
+ let mockClient: any;
+ let savedObjectsClient: jest.Mocked;
+ const mockLogger: any = {
+ debug: jest.fn(),
+ warn: jest.fn(),
+ error: jest.fn(),
+ };
+
+ beforeEach(() => {
+ savedObjectsClient = savedObjectsClientMock.create();
+ mockClient = {
+ asyncSearch: {
+ status: jest.fn(),
+ delete: jest.fn(),
+ },
+ eql: {
+ status: jest.fn(),
+ delete: jest.fn(),
+ },
+ };
+ });
+
+ describe('updateSessionStatus', () => {
+ test('updates expired session', async () => {
+ const so: SavedObjectsFindResult = {
+ id: '123',
+ attributes: {
+ persisted: false,
+ status: SearchSessionStatus.IN_PROGRESS,
+ expires: moment().subtract(moment.duration(5, 'd')),
+ idMapping: {
+ 'search-hash': {
+ id: 'search-id',
+ strategy: 'cool',
+ status: SearchStatus.IN_PROGRESS,
+ },
+ },
+ },
+ } as any;
+
+ const updated = await updateSessionStatus(
+ {
+ savedObjectsClient,
+ client: mockClient,
+ logger: mockLogger,
+ },
+ so
+ );
+
+ expect(updated).toBeTruthy();
+ expect(so.attributes.status).toBe(SearchSessionStatus.EXPIRED);
+ });
+
+ test('does nothing if the search is still running', async () => {
+ const so = {
+ id: '123',
+ attributes: {
+ persisted: false,
+ status: SearchSessionStatus.IN_PROGRESS,
+ created: moment().subtract(moment.duration(3, 'm')),
+ touched: moment().subtract(moment.duration(10, 's')),
+ expires: moment().add(moment.duration(5, 'd')),
+ idMapping: {
+ 'search-hash': {
+ id: 'search-id',
+ strategy: 'cool',
+ status: SearchStatus.IN_PROGRESS,
+ },
+ },
+ },
+ } as any;
+
+ mockClient.asyncSearch.status.mockResolvedValue({
+ body: {
+ is_partial: true,
+ is_running: true,
+ },
+ });
+
+ const updated = await updateSessionStatus(
+ {
+ savedObjectsClient,
+ client: mockClient,
+ logger: mockLogger,
+ },
+ so
+ );
+
+ expect(updated).toBeFalsy();
+ expect(so.attributes.status).toBe(SearchSessionStatus.IN_PROGRESS);
+ });
+
+ test("doesn't re-check completed or errored searches", async () => {
+ const so = {
+ id: '123',
+ attributes: {
+ expires: moment().add(moment.duration(5, 'd')),
+ status: SearchSessionStatus.ERROR,
+ idMapping: {
+ 'search-hash': {
+ id: 'search-id',
+ strategy: 'cool',
+ status: SearchStatus.COMPLETE,
+ },
+ 'another-search-hash': {
+ id: 'search-id',
+ strategy: 'cool',
+ status: SearchStatus.ERROR,
+ },
+ },
+ },
+ } as any;
+
+ const updated = await updateSessionStatus(
+ {
+ savedObjectsClient,
+ client: mockClient,
+ logger: mockLogger,
+ },
+ so
+ );
+
+ expect(updated).toBeFalsy();
+ expect(mockClient.asyncSearch.status).not.toBeCalled();
+ });
+
+ test('updates to complete if the search is done', async () => {
+ savedObjectsClient.bulkUpdate = jest.fn();
+ const so = {
+ attributes: {
+ status: SearchSessionStatus.IN_PROGRESS,
+ touched: '123',
+ expires: moment().add(moment.duration(5, 'd')),
+ idMapping: {
+ 'search-hash': {
+ id: 'search-id',
+ strategy: 'cool',
+ status: SearchStatus.IN_PROGRESS,
+ },
+ },
+ },
+ } as any;
+ mockClient.asyncSearch.status.mockResolvedValue({
+ body: {
+ is_partial: false,
+ is_running: false,
+ completion_status: 200,
+ },
+ });
+
+ const updated = await updateSessionStatus(
+ {
+ savedObjectsClient,
+ client: mockClient,
+ logger: mockLogger,
+ },
+ so
+ );
+
+ expect(updated).toBeTruthy();
+
+ expect(mockClient.asyncSearch.status).toBeCalledWith({ id: 'search-id' });
+ expect(so.attributes.status).toBe(SearchSessionStatus.COMPLETE);
+ expect(so.attributes.status).toBe(SearchSessionStatus.COMPLETE);
+ expect(so.attributes.touched).not.toBe('123');
+ expect(so.attributes.completed).not.toBeUndefined();
+ expect(so.attributes.idMapping['search-hash'].status).toBe(SearchStatus.COMPLETE);
+ expect(so.attributes.idMapping['search-hash'].error).toBeUndefined();
+ });
+
+ test('updates to error if the search is errored', async () => {
+ savedObjectsClient.bulkUpdate = jest.fn();
+ const so = {
+ attributes: {
+ expires: moment().add(moment.duration(5, 'd')),
+ idMapping: {
+ 'search-hash': {
+ id: 'search-id',
+ strategy: 'cool',
+ status: SearchStatus.IN_PROGRESS,
+ },
+ },
+ },
+ } as any;
+
+ mockClient.asyncSearch.status.mockResolvedValue({
+ body: {
+ is_partial: false,
+ is_running: false,
+ completion_status: 500,
+ },
+ });
+
+ const updated = await updateSessionStatus(
+ {
+ savedObjectsClient,
+ client: mockClient,
+ logger: mockLogger,
+ },
+ so
+ );
+
+ expect(updated).toBeTruthy();
+ expect(so.attributes.status).toBe(SearchSessionStatus.ERROR);
+ expect(so.attributes.touched).not.toBe('123');
+ expect(so.attributes.idMapping['search-hash'].status).toBe(SearchStatus.ERROR);
+ expect(so.attributes.idMapping['search-hash'].error).toBe(
+ 'Search completed with a 500 status'
+ );
+ });
+ });
+
+ describe('bulkUpdateSessions', () => {
+ test('does nothing if there are no open sessions', async () => {
+ await bulkUpdateSessions(
+ {
+ savedObjectsClient,
+ client: mockClient,
+ logger: mockLogger,
+ },
+ []
+ );
+
+ expect(savedObjectsClient.bulkUpdate).not.toBeCalled();
+ expect(savedObjectsClient.delete).not.toBeCalled();
+ });
+
+ test('updates in space', async () => {
+ const so = {
+ namespaces: ['awesome'],
+ attributes: {
+ expires: moment().add(moment.duration(5, 'd')),
+ status: SearchSessionStatus.IN_PROGRESS,
+ touched: '123',
+ idMapping: {
+ 'search-hash': {
+ id: 'search-id',
+ strategy: 'cool',
+ status: SearchStatus.IN_PROGRESS,
+ },
+ },
+ },
+ } as any;
+
+ savedObjectsClient.bulkUpdate = jest.fn().mockResolvedValue({
+ saved_objects: [so],
+ });
+
+ await bulkUpdateSessions(
+ {
+ savedObjectsClient,
+ client: mockClient,
+ logger: mockLogger,
+ },
+ [so]
+ );
+
+ const [updateInput] = savedObjectsClient.bulkUpdate.mock.calls[0];
+ const updatedAttributes = updateInput[0] as SavedObjectsBulkUpdateObject;
+ expect(updatedAttributes.namespace).toBe('awesome');
+ });
+
+ test('logs failures', async () => {
+ const so = {
+ namespaces: ['awesome'],
+ attributes: {
+ expires: moment().add(moment.duration(5, 'd')),
+ status: SearchSessionStatus.IN_PROGRESS,
+ touched: '123',
+ idMapping: {
+ 'search-hash': {
+ id: 'search-id',
+ strategy: 'cool',
+ status: SearchStatus.IN_PROGRESS,
+ },
+ },
+ },
+ } as any;
+
+ savedObjectsClient.bulkUpdate = jest.fn().mockResolvedValue({
+ saved_objects: [
+ {
+ error: 'nope',
+ },
+ ],
+ });
+
+ await bulkUpdateSessions(
+ {
+ savedObjectsClient,
+ client: mockClient,
+ logger: mockLogger,
+ },
+ [so]
+ );
+
+ expect(savedObjectsClient.bulkUpdate).toBeCalledTimes(1);
+ expect(mockLogger.error).toBeCalledTimes(1);
+ });
+ });
+});
diff --git a/x-pack/plugins/data_enhanced/server/search/session/update_session_status.ts b/x-pack/plugins/data_enhanced/server/search/session/update_session_status.ts
new file mode 100644
index 0000000000000..1c484467bef63
--- /dev/null
+++ b/x-pack/plugins/data_enhanced/server/search/session/update_session_status.ts
@@ -0,0 +1,128 @@
+/*
+ * 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 { SavedObjectsFindResult, SavedObjectsUpdateResponse } from 'kibana/server';
+import {
+ SearchSessionRequestInfo,
+ SearchSessionSavedObjectAttributes,
+ SearchSessionStatus,
+} from '../../../../../../src/plugins/data/common';
+import { getSearchStatus } from './get_search_status';
+import { getSessionStatus } from './get_session_status';
+import { CheckSearchSessionsDeps, SearchSessionsResponse, SearchStatus } from './types';
+import { isSearchSessionExpired } from './utils';
+
+export async function updateSessionStatus(
+ { logger, client }: CheckSearchSessionsDeps,
+ session: SavedObjectsFindResult
+) {
+ let sessionUpdated = false;
+ const isExpired = isSearchSessionExpired(session);
+
+ if (!isExpired) {
+ // Check statuses of all running searches
+ await Promise.all(
+ Object.keys(session.attributes.idMapping).map(async (searchKey: string) => {
+ const updateSearchRequest = (
+ currentStatus: Pick
+ ) => {
+ sessionUpdated = true;
+ session.attributes.idMapping[searchKey] = {
+ ...session.attributes.idMapping[searchKey],
+ ...currentStatus,
+ };
+ };
+
+ const searchInfo = session.attributes.idMapping[searchKey];
+ if (searchInfo.status === SearchStatus.IN_PROGRESS) {
+ try {
+ const currentStatus = await getSearchStatus(client, searchInfo.id);
+
+ if (currentStatus.status !== searchInfo.status) {
+ logger.debug(`search ${searchInfo.id} | status changed to ${currentStatus.status}`);
+ updateSearchRequest(currentStatus);
+ }
+ } catch (e) {
+ logger.error(e);
+ updateSearchRequest({
+ status: SearchStatus.ERROR,
+ error: e.message || e.meta.error?.caused_by?.reason,
+ });
+ }
+ }
+ })
+ );
+ }
+
+ // And only then derive the session's status
+ const sessionStatus = isExpired
+ ? SearchSessionStatus.EXPIRED
+ : getSessionStatus(session.attributes);
+ if (sessionStatus !== session.attributes.status) {
+ const now = new Date().toISOString();
+ session.attributes.status = sessionStatus;
+ session.attributes.touched = now;
+ if (sessionStatus === SearchSessionStatus.COMPLETE) {
+ session.attributes.completed = now;
+ } else if (session.attributes.completed) {
+ session.attributes.completed = null;
+ }
+ sessionUpdated = true;
+ }
+
+ return sessionUpdated;
+}
+
+export async function getAllSessionsStatusUpdates(
+ deps: CheckSearchSessionsDeps,
+ searchSessions: SearchSessionsResponse
+) {
+ const updatedSessions = new Array>();
+
+ await Promise.all(
+ searchSessions.saved_objects.map(async (session) => {
+ const updated = await updateSessionStatus(deps, session);
+
+ if (updated) {
+ updatedSessions.push(session);
+ }
+ })
+ );
+
+ return updatedSessions;
+}
+
+export async function bulkUpdateSessions(
+ { logger, savedObjectsClient }: CheckSearchSessionsDeps,
+ updatedSessions: Array>
+) {
+ if (updatedSessions.length) {
+ // If there's an error, we'll try again in the next iteration, so there's no need to check the output.
+ const updatedResponse = await savedObjectsClient.bulkUpdate(
+ updatedSessions.map((session) => ({
+ ...session,
+ namespace: session.namespaces?.[0],
+ }))
+ );
+
+ const success: Array> = [];
+ const fail: Array> = [];
+
+ updatedResponse.saved_objects.forEach((savedObjectResponse) => {
+ if ('error' in savedObjectResponse) {
+ fail.push(savedObjectResponse);
+ logger.error(
+ `Error while updating search session ${savedObjectResponse?.id}: ${savedObjectResponse.error?.message}`
+ );
+ } else {
+ success.push(savedObjectResponse);
+ }
+ });
+
+ logger.debug(`Updating search sessions: success: ${success.length}, fail: ${fail.length}`);
+ }
+}
diff --git a/x-pack/plugins/data_enhanced/server/search/session/utils.ts b/x-pack/plugins/data_enhanced/server/search/session/utils.ts
index 7b1f1a7564626..55c875602694f 100644
--- a/x-pack/plugins/data_enhanced/server/search/session/utils.ts
+++ b/x-pack/plugins/data_enhanced/server/search/session/utils.ts
@@ -7,6 +7,9 @@
import { createHash } from 'crypto';
import stringify from 'json-stable-stringify';
+import { SavedObjectsFindResult } from 'kibana/server';
+import moment from 'moment';
+import { SearchSessionSavedObjectAttributes } from 'src/plugins/data/common';
/**
* Generate the hash for this request so that, in the future, this hash can be used to look up
@@ -17,3 +20,9 @@ export function createRequestHash(keys: Record) {
const { preference, ...params } = keys;
return createHash(`sha256`).update(stringify(params)).digest('hex');
}
+
+export function isSearchSessionExpired(
+ session: SavedObjectsFindResult
+) {
+ return moment(session.attributes.expires).isBefore(moment());
+}
diff --git a/x-pack/plugins/data_visualizer/kibana.json b/x-pack/plugins/data_visualizer/kibana.json
index 00eb3d7bf142c..01aca7c2bbaee 100644
--- a/x-pack/plugins/data_visualizer/kibana.json
+++ b/x-pack/plugins/data_visualizer/kibana.json
@@ -27,5 +27,10 @@
],
"extraPublicDirs": [
"common"
- ]
+ ],
+ "owner": {
+ "name": "Machine Learning UI",
+ "githubTeam": "ml-ui"
+ },
+ "description": "The Data Visualizer tools help you understand your data, by analyzing the metrics and fields in a log file or an existing Elasticsearch index."
}
diff --git a/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts b/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts
index 7ea4289b21967..a4e3ada1c06cb 100644
--- a/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts
+++ b/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts
@@ -8,7 +8,6 @@
export const DEFAULT_INITIAL_APP_DATA = {
readOnlyMode: false,
ilmEnabled: true,
- isFederatedAuth: false,
configuredLimits: {
appSearch: {
engine: {
diff --git a/x-pack/plugins/enterprise_search/common/types/index.ts b/x-pack/plugins/enterprise_search/common/types/index.ts
index 68904483720f2..f405c86de18f0 100644
--- a/x-pack/plugins/enterprise_search/common/types/index.ts
+++ b/x-pack/plugins/enterprise_search/common/types/index.ts
@@ -17,7 +17,6 @@ import {
export interface InitialAppData {
readOnlyMode?: boolean;
ilmEnabled?: boolean;
- isFederatedAuth?: boolean;
configuredLimits?: ConfiguredLimits;
access?: ProductAccess;
appSearch?: AppSearchAccount;
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.tsx
index d56fe949431c3..2ed06d68301c9 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.tsx
@@ -8,8 +8,6 @@
import React from 'react';
import { Route, Switch, Redirect } from 'react-router-dom';
-import { APP_SEARCH_PLUGIN } from '../../../../../common/constants';
-import { NotFound } from '../../../shared/not_found';
import {
ENGINE_ANALYTICS_PATH,
ENGINE_ANALYTICS_TOP_QUERIES_PATH,
@@ -21,6 +19,7 @@ import {
ENGINE_ANALYTICS_QUERY_DETAIL_PATH,
} from '../../routes';
import { generateEnginePath, getEngineBreadcrumbs } from '../engine';
+import { NotFound } from '../not_found';
import { ANALYTICS_TITLE } from './constants';
import {
@@ -61,10 +60,7 @@ export const AnalyticsRouter: React.FC = () => {
-
+
);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx
index 015fb997c29ed..e088678a13562 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx
@@ -10,7 +10,6 @@ import { mockUseRouteMatch } from '../../../__mocks__/react_router';
import { mockEngineValues } from '../../__mocks__';
jest.mock('../../../shared/layout', () => ({
- ...jest.requireActual('../../../shared/layout'), // TODO: Remove once side nav components are gone
generateNavLink: jest.fn(({ to }) => ({ href: to })),
}));
@@ -20,9 +19,7 @@ import { shallow } from 'enzyme';
import { EuiBadge, EuiIcon } from '@elastic/eui';
-import { rerender } from '../../../test_helpers';
-
-import { useEngineNav, EngineNav } from './engine_nav';
+import { useEngineNav } from './engine_nav';
describe('useEngineNav', () => {
const values = { ...mockEngineValues, myRole: {}, dataLoading: false };
@@ -321,182 +318,3 @@ describe('useEngineNav', () => {
});
});
});
-
-describe('EngineNav', () => {
- const values = { ...mockEngineValues, myRole: {}, dataLoading: false };
-
- beforeEach(() => {
- setMockValues(values);
- });
-
- it('does not render if async data is still loading', () => {
- setMockValues({ ...values, dataLoading: true });
- const wrapper = shallow( );
- expect(wrapper.isEmptyRender()).toBe(true);
- });
-
- it('does not render without an engine name', () => {
- setMockValues({ ...values, engineName: '' });
- const wrapper = shallow( );
- expect(wrapper.isEmptyRender()).toBe(true);
- });
-
- it('renders an engine label and badges', () => {
- setMockValues({ ...values, isSampleEngine: false, isMetaEngine: false });
- const wrapper = shallow( );
- const label = wrapper.find('[data-test-subj="EngineLabel"]').find('.eui-textTruncate');
-
- expect(label.text()).toEqual('SOME-ENGINE');
- expect(wrapper.find(EuiBadge)).toHaveLength(0);
-
- setMockValues({ ...values, isSampleEngine: true });
- rerender(wrapper);
- expect(wrapper.find(EuiBadge).prop('children')).toEqual('SAMPLE ENGINE');
-
- setMockValues({ ...values, isMetaEngine: true });
- rerender(wrapper);
- expect(wrapper.find(EuiBadge).prop('children')).toEqual('META ENGINE');
- });
-
- it('renders a default engine overview link', () => {
- const wrapper = shallow( );
- expect(wrapper.find('[data-test-subj="EngineOverviewLink"]')).toHaveLength(1);
- });
-
- it('renders an analytics link', () => {
- setMockValues({ ...values, myRole: { canViewEngineAnalytics: true } });
- const wrapper = shallow( );
- expect(wrapper.find('[data-test-subj="EngineAnalyticsLink"]')).toHaveLength(1);
- });
-
- it('renders a documents link', () => {
- setMockValues({ ...values, myRole: { canViewEngineDocuments: true } });
- const wrapper = shallow( );
- expect(wrapper.find('[data-test-subj="EngineDocumentsLink"]')).toHaveLength(1);
- });
-
- it('renders a schema link', () => {
- setMockValues({ ...values, myRole: { canViewEngineSchema: true } });
- const wrapper = shallow( );
- expect(wrapper.find('[data-test-subj="EngineSchemaLink"]')).toHaveLength(1);
- });
-
- describe('schema nav icons', () => {
- const myRole = { canViewEngineSchema: true };
-
- it('renders schema errors alert icon', () => {
- setMockValues({ ...values, myRole, hasSchemaErrors: true });
- const wrapper = shallow( );
- expect(wrapper.find('[data-test-subj="EngineNavSchemaErrors"]')).toHaveLength(1);
- });
-
- it('renders unconfirmed schema fields info icon', () => {
- setMockValues({ ...values, myRole, hasUnconfirmedSchemaFields: true });
- const wrapper = shallow( );
- expect(wrapper.find('[data-test-subj="EngineNavSchemaUnconfirmedFields"]')).toHaveLength(1);
- });
-
- it('renders schema conflicts alert icon', () => {
- setMockValues({ ...values, myRole, hasSchemaConflicts: true });
- const wrapper = shallow( );
- expect(wrapper.find('[data-test-subj="EngineNavSchemaConflicts"]')).toHaveLength(1);
- });
- });
-
- describe('crawler link', () => {
- const myRole = { canViewEngineCrawler: true };
-
- it('renders', () => {
- setMockValues({ ...values, myRole });
- const wrapper = shallow( );
- expect(wrapper.find('[data-test-subj="EngineCrawlerLink"]')).toHaveLength(1);
- });
-
- it('does not render for meta engines', () => {
- setMockValues({ ...values, myRole, isMetaEngine: true });
- const wrapper = shallow( );
- expect(wrapper.find('[data-test-subj="EngineCrawlerLink"]')).toHaveLength(0);
- });
- });
-
- describe('meta engine source engines link', () => {
- const myRole = { canViewMetaEngineSourceEngines: true };
-
- it('renders', () => {
- setMockValues({ ...values, myRole, isMetaEngine: true });
- const wrapper = shallow( );
- expect(wrapper.find('[data-test-subj="MetaEngineEnginesLink"]')).toHaveLength(1);
- });
-
- it('does not render for non meta engines', () => {
- setMockValues({ ...values, myRole, isMetaEngine: false });
- const wrapper = shallow( );
- expect(wrapper.find('[data-test-subj="MetaEngineEnginesLink"]')).toHaveLength(0);
- });
- });
-
- it('renders a relevance tuning link', () => {
- setMockValues({ ...values, myRole: { canManageEngineRelevanceTuning: true } });
- const wrapper = shallow( );
- expect(wrapper.find('[data-test-subj="EngineRelevanceTuningLink"]')).toHaveLength(1);
- });
-
- describe('relevance tuning nav icons', () => {
- const myRole = { canManageEngineRelevanceTuning: true };
-
- it('renders unconfirmed schema fields info icon', () => {
- const engine = { unsearchedUnconfirmedFields: true };
- setMockValues({ ...values, myRole, engine });
- const wrapper = shallow( );
- expect(
- wrapper.find('[data-test-subj="EngineNavRelevanceTuningUnsearchedFields"]')
- ).toHaveLength(1);
- });
-
- it('renders schema conflicts alert icon', () => {
- const engine = { invalidBoosts: true };
- setMockValues({ ...values, myRole, engine });
- const wrapper = shallow( );
- expect(wrapper.find('[data-test-subj="EngineNavRelevanceTuningInvalidBoosts"]')).toHaveLength(
- 1
- );
- });
-
- it('can render multiple icons', () => {
- const engine = { invalidBoosts: true, unsearchedUnconfirmedFields: true };
- setMockValues({ ...values, myRole, engine });
- const wrapper = shallow( );
- expect(wrapper.find(EuiIcon)).toHaveLength(2);
- });
- });
-
- it('renders a synonyms link', () => {
- setMockValues({ ...values, myRole: { canManageEngineSynonyms: true } });
- const wrapper = shallow( );
- expect(wrapper.find('[data-test-subj="EngineSynonymsLink"]')).toHaveLength(1);
- });
-
- it('renders a curations link', () => {
- setMockValues({ ...values, myRole: { canManageEngineCurations: true } });
- const wrapper = shallow( );
- expect(wrapper.find('[data-test-subj="EngineCurationsLink"]')).toHaveLength(1);
- });
-
- it('renders a results settings link', () => {
- setMockValues({ ...values, myRole: { canManageEngineResultSettings: true } });
- const wrapper = shallow( );
- expect(wrapper.find('[data-test-subj="EngineResultSettingsLink"]')).toHaveLength(1);
- });
-
- it('renders a Search UI link', () => {
- setMockValues({ ...values, myRole: { canManageEngineSearchUi: true } });
- const wrapper = shallow( );
- expect(wrapper.find('[data-test-subj="EngineSearchUILink"]')).toHaveLength(1);
- });
-
- it('renders an API logs link', () => {
- setMockValues({ ...values, myRole: { canViewEngineApiLogs: true } });
- const wrapper = shallow( );
- expect(wrapper.find('[data-test-subj="EngineAPILogsLink"]')).toHaveLength(1);
- });
-});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx
index 76e751cf4da5f..70f2d04a5123d 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx
@@ -10,17 +10,10 @@ import { useRouteMatch } from 'react-router-dom';
import { useValues } from 'kea';
-import {
- EuiSideNavItemType,
- EuiText,
- EuiBadge,
- EuiIcon,
- EuiFlexGroup,
- EuiFlexItem,
-} from '@elastic/eui';
+import { EuiSideNavItemType, EuiText, EuiBadge, EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { generateNavLink, SideNavLink, SideNavItem } from '../../../shared/layout';
+import { generateNavLink } from '../../../shared/layout';
import { AppLogic } from '../../app_logic';
import {
ENGINE_PATH,
@@ -49,8 +42,6 @@ import { SCHEMA_TITLE } from '../schema';
import { SEARCH_UI_TITLE } from '../search_ui';
import { SYNONYMS_TITLE } from '../synonyms';
-import { EngineDetails } from './types';
-
import { EngineLogic, generateEnginePath } from './';
import './engine_nav.scss';
@@ -198,7 +189,10 @@ export const useEngineNav = () => {
navItems.push({
id: 'crawler',
name: CRAWLER_TITLE,
- ...generateNavLink({ to: generateEnginePath(ENGINE_CRAWLER_PATH) }),
+ ...generateNavLink({
+ to: generateEnginePath(ENGINE_CRAWLER_PATH),
+ shouldShowActiveForSubroutes: true,
+ }),
'data-test-subj': 'EngineCrawlerLink',
});
}
@@ -301,220 +295,3 @@ export const useEngineNav = () => {
return navItems;
};
-
-// TODO: Delete the below once page template migration is complete
-
-export const EngineNav: React.FC = () => {
- const {
- myRole: {
- canViewEngineAnalytics,
- canViewEngineDocuments,
- canViewEngineSchema,
- canViewEngineCrawler,
- canViewMetaEngineSourceEngines,
- canManageEngineSynonyms,
- canManageEngineCurations,
- canManageEngineRelevanceTuning,
- canManageEngineResultSettings,
- canManageEngineSearchUi,
- canViewEngineApiLogs,
- },
- } = useValues(AppLogic);
-
- const {
- engineName,
- dataLoading,
- isSampleEngine,
- isMetaEngine,
- hasSchemaErrors,
- hasSchemaConflicts,
- hasUnconfirmedSchemaFields,
- engine,
- } = useValues(EngineLogic);
-
- if (dataLoading) return null;
- if (!engineName) return null;
-
- const { invalidBoosts, unsearchedUnconfirmedFields } = engine as Required;
-
- return (
- <>
-
-
- {engineName.toUpperCase()}
- {isSampleEngine && (
-
- {i18n.translate('xpack.enterpriseSearch.appSearch.engine.sampleEngineBadge', {
- defaultMessage: 'SAMPLE ENGINE',
- })}
-
- )}
- {isMetaEngine && (
-
- {i18n.translate('xpack.enterpriseSearch.appSearch.engine.metaEngineBadge', {
- defaultMessage: 'META ENGINE',
- })}
-
- )}
-
-
-
- {OVERVIEW_TITLE}
-
- {canViewEngineAnalytics && (
-
- {ANALYTICS_TITLE}
-
- )}
- {canViewEngineDocuments && (
-
- {DOCUMENTS_TITLE}
-
- )}
- {canViewEngineSchema && (
-
-
- {SCHEMA_TITLE}
-
- {hasSchemaErrors && (
-
- )}
- {hasUnconfirmedSchemaFields && (
-
- )}
- {hasSchemaConflicts && (
-
- )}
-
-
-
- )}
- {canViewEngineCrawler && !isMetaEngine && (
-
- {CRAWLER_TITLE}
-
- )}
- {canViewMetaEngineSourceEngines && isMetaEngine && (
-
- {ENGINES_TITLE}
-
- )}
- {canManageEngineRelevanceTuning && (
-
-
- {RELEVANCE_TUNING_TITLE}
-
- {invalidBoosts && (
-
- )}
- {unsearchedUnconfirmedFields && (
-
- )}
-
-
-
- )}
- {canManageEngineSynonyms && (
-
- {SYNONYMS_TITLE}
-
- )}
- {canManageEngineCurations && (
-
- {CURATIONS_TITLE}
-
- )}
- {canManageEngineResultSettings && (
-
- {RESULT_SETTINGS_TITLE}
-
- )}
- {canManageEngineSearchUi && (
-
- {SEARCH_UI_TITLE}
-
- )}
- {canViewEngineApiLogs && (
-
- {API_LOGS_TITLE}
-
- )}
- >
- );
-};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
index da8dd8467bb61..2d1bd32a0fff5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
@@ -38,6 +38,7 @@ import { CurationsRouter } from '../curations';
import { DocumentDetail, Documents } from '../documents';
import { EngineOverview } from '../engine_overview';
import { AppSearchPageTemplate } from '../layout';
+import { NotFound } from '../not_found';
import { RelevanceTuning } from '../relevance_tuning';
import { ResultSettings } from '../result_settings';
import { SchemaRouter } from '../schema';
@@ -45,7 +46,7 @@ import { SearchUI } from '../search_ui';
import { SourceEngines } from '../source_engines';
import { Synonyms } from '../synonyms';
-import { EngineLogic } from './';
+import { EngineLogic, getEngineBreadcrumbs } from './';
export const EngineRouter: React.FC = () => {
const {
@@ -159,6 +160,9 @@ export const EngineRouter: React.FC = () => {
)}
+
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/index.ts
index 2a5b3351f41f7..86b4ff21e62d3 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/index.ts
@@ -6,6 +6,5 @@
*/
export { EngineRouter } from './engine_router';
-export { EngineNav } from './engine_nav';
export { EngineLogic } from './engine_logic';
export { generateEnginePath, getEngineBreadcrumbs } from './utils';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx
index a2e0ba4fcd44d..01472987e4d48 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx
@@ -29,11 +29,6 @@ describe('EngineOverview', () => {
setMockValues(values);
});
- it('renders', () => {
- const wrapper = shallow( );
- expect(wrapper.find('[data-test-subj="EngineOverview"]')).toHaveLength(1);
- });
-
describe('EmptyEngineOverview', () => {
it('renders when the engine has no documents & the user can add documents', () => {
const myRole = { canManageEngineDocuments: true, canViewEngineCredentials: true };
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx
index a3f98d8c13e8e..e966709dc1084 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx
@@ -25,9 +25,5 @@ export const EngineOverview: React.FC = () => {
const canAddDocuments = canManageEngineDocuments && canViewEngineCredentials;
const showEngineOverview = !isEngineEmpty || !canAddDocuments || isMetaEngine;
- return (
-
- {showEngineOverview ? : }
-
- );
+ return showEngineOverview ? : ;
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx
index 27d9c3723f126..6f8332e1e332e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx
@@ -34,6 +34,7 @@ export const EmptyEngineOverview: React.FC = () => {
,
],
}}
+ data-test-subj="EngineOverview"
>
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx
index 3cc7138623735..9c3a900dfe115 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx
@@ -36,6 +36,7 @@ export const EngineOverviewMetrics: React.FC = () => {
}),
}}
isLoading={dataLoading}
+ data-test-subj="EngineOverview"
>
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx
index c9f5452e254e1..ce4a118bef095 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx
@@ -100,8 +100,8 @@ describe('useAppSearchNav', () => {
},
{
id: 'usersRoles',
- name: 'Users & roles',
- href: '/role_mappings',
+ name: 'Users and roles',
+ href: '/users_and_roles',
},
],
},
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx
index c3b8ec642233b..793a36f48fe82 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx
@@ -13,7 +13,7 @@ import { generateNavLink } from '../../../shared/layout';
import { ROLE_MAPPINGS_TITLE } from '../../../shared/role_mapping/constants';
import { AppLogic } from '../../app_logic';
-import { ENGINES_PATH, SETTINGS_PATH, CREDENTIALS_PATH, ROLE_MAPPINGS_PATH } from '../../routes';
+import { ENGINES_PATH, SETTINGS_PATH, CREDENTIALS_PATH, USERS_AND_ROLES_PATH } from '../../routes';
import { CREDENTIALS_TITLE } from '../credentials';
import { useEngineNav } from '../engine/engine_nav';
import { ENGINES_TITLE } from '../engines';
@@ -57,7 +57,7 @@ export const useAppSearchNav = () => {
navItems.push({
id: 'usersRoles',
name: ROLE_MAPPINGS_TITLE,
- ...generateNavLink({ to: ROLE_MAPPINGS_PATH }),
+ ...generateNavLink({ to: USERS_AND_ROLES_PATH }),
});
}
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/logo.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/index.ts
similarity index 53%
rename from x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/logo.scss
rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/index.ts
index b157f55cbba68..482c1a58faa9c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/logo.scss
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/index.ts
@@ -5,18 +5,4 @@
* 2.0.
*/
-.logo404 {
- width: $euiSize * 8;
- height: $euiSize * 8;
-
- fill: $euiColorEmptyShade;
- stroke: $euiColorLightShade;
-
- &__light {
- fill: $euiColorLightShade;
- }
-
- &__dark {
- fill: $euiColorMediumShade;
- }
-}
+export { NotFound } from './not_found';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/not_found.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/not_found.test.tsx
new file mode 100644
index 0000000000000..6fed726eb5e0b
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/not_found.test.tsx
@@ -0,0 +1,38 @@
+/*
+ * 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 React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { NotFoundPrompt } from '../../../shared/not_found';
+import { SendAppSearchTelemetry } from '../../../shared/telemetry';
+import { AppSearchPageTemplate } from '../layout';
+
+import { NotFound } from './';
+
+describe('NotFound', () => {
+ const wrapper = shallow( );
+
+ it('renders the shared not found prompt', () => {
+ expect(wrapper.find(NotFoundPrompt)).toHaveLength(1);
+ });
+
+ it('renders a telemetry error event', () => {
+ expect(wrapper.find(SendAppSearchTelemetry).prop('action')).toEqual('error');
+ });
+
+ it('passes optional preceding page chrome', () => {
+ wrapper.setProps({ pageChrome: ['Engines', 'some-engine'] });
+
+ expect(wrapper.find(AppSearchPageTemplate).prop('pageChrome')).toEqual([
+ 'Engines',
+ 'some-engine',
+ '404',
+ ]);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/not_found.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/not_found.tsx
new file mode 100644
index 0000000000000..f6165fa192d57
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/not_found.tsx
@@ -0,0 +1,23 @@
+/*
+ * 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 React from 'react';
+
+import { APP_SEARCH_PLUGIN } from '../../../../../common/constants';
+import { PageTemplateProps } from '../../../shared/layout';
+import { NotFoundPrompt } from '../../../shared/not_found';
+import { SendAppSearchTelemetry } from '../../../shared/telemetry';
+import { AppSearchPageTemplate } from '../layout';
+
+export const NotFound: React.FC = ({ pageChrome = [] }) => {
+ return (
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_callouts.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_callouts.tsx
index c981d35ff20cb..bdbc414a22eaa 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_callouts.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_callouts.tsx
@@ -7,12 +7,11 @@
import React from 'react';
-import { FormattedMessage } from 'react-intl';
-
import { useValues } from 'kea';
import { EuiCallOut, EuiLink } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
import { EuiLinkTo } from '../../../shared/react_router_helpers';
import { DOCS_PREFIX, ENGINE_SCHEMA_PATH } from '../../routes';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx
index b6a9dd72cfd05..02ff4c0ba1c3d 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx
@@ -28,7 +28,7 @@ export const RoleMapping: React.FC = () => {
handleAuthProviderChange,
handleRoleChange,
handleSaveMapping,
- closeRoleMappingFlyout,
+ closeUsersAndRolesFlyout,
} = useActions(RoleMappingsLogic);
const {
@@ -45,6 +45,7 @@ export const RoleMapping: React.FC = () => {
selectedEngines,
selectedAuthProviders,
roleMappingErrors,
+ formLoading,
} = useValues(RoleMappingsLogic);
const isNew = !roleMapping;
@@ -67,8 +68,9 @@ export const RoleMapping: React.FC = () => {
return (
0} error={roleMappingErrors}>
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.test.tsx
index 308022ccb2e5a..64bf41a50a2f0 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.test.tsx
@@ -12,26 +12,39 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { RoleMappingsTable, RoleMappingsHeading } from '../../../shared/role_mapping';
-import { wsRoleMapping } from '../../../shared/role_mapping/__mocks__/roles';
+import {
+ RoleMappingsTable,
+ RoleMappingsHeading,
+ UsersHeading,
+ UsersEmptyPrompt,
+} from '../../../shared/role_mapping';
+import {
+ asRoleMapping,
+ asSingleUserRoleMapping,
+} from '../../../shared/role_mapping/__mocks__/roles';
import { RoleMapping } from './role_mapping';
import { RoleMappings } from './role_mappings';
+import { User } from './user';
describe('RoleMappings', () => {
const initializeRoleMappings = jest.fn();
const initializeRoleMapping = jest.fn();
+ const initializeSingleUserRoleMapping = jest.fn();
const handleDeleteMapping = jest.fn();
const mockValues = {
- roleMappings: [wsRoleMapping],
+ roleMappings: [asRoleMapping],
dataLoading: false,
multipleAuthProvidersConfig: false,
+ singleUserRoleMappings: [asSingleUserRoleMapping],
+ singleUserRoleMappingFlyoutOpen: false,
};
beforeEach(() => {
setMockActions({
initializeRoleMappings,
initializeRoleMapping,
+ initializeSingleUserRoleMapping,
handleDeleteMapping,
});
setMockValues(mockValues);
@@ -50,10 +63,31 @@ describe('RoleMappings', () => {
expect(wrapper.find(RoleMapping)).toHaveLength(1);
});
- it('handles onClick', () => {
+ it('renders User flyout', () => {
+ setMockValues({ ...mockValues, singleUserRoleMappingFlyoutOpen: true });
+ const wrapper = shallow( );
+
+ expect(wrapper.find(User)).toHaveLength(1);
+ });
+
+ it('handles RoleMappingsHeading onClick', () => {
const wrapper = shallow( );
wrapper.find(RoleMappingsHeading).prop('onClick')();
expect(initializeRoleMapping).toHaveBeenCalled();
});
+
+ it('handles UsersHeading onClick', () => {
+ const wrapper = shallow( );
+ wrapper.find(UsersHeading).prop('onClick')();
+
+ expect(initializeSingleUserRoleMapping).toHaveBeenCalled();
+ });
+
+ it('handles empty users state', () => {
+ setMockValues({ ...mockValues, singleUserRoleMappings: [] });
+ const wrapper = shallow( );
+
+ expect(wrapper.find(UsersEmptyPrompt)).toHaveLength(1);
+ });
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx
index 03e2ae67eca9e..3e692aa48623e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx
@@ -9,11 +9,16 @@ import React, { useEffect } from 'react';
import { useActions, useValues } from 'kea';
+import { EuiSpacer } from '@elastic/eui';
+
import { APP_SEARCH_PLUGIN } from '../../../../../common/constants';
import {
RoleMappingsTable,
RoleMappingsHeading,
RolesEmptyPrompt,
+ UsersTable,
+ UsersHeading,
+ UsersEmptyPrompt,
} from '../../../shared/role_mapping';
import { ROLE_MAPPINGS_TITLE } from '../../../shared/role_mapping/constants';
@@ -23,6 +28,7 @@ import { AppSearchPageTemplate } from '../layout';
import { ROLE_MAPPINGS_ENGINE_ACCESS_HEADING } from './constants';
import { RoleMapping } from './role_mapping';
import { RoleMappingsLogic } from './role_mappings_logic';
+import { User } from './user';
const ROLES_DOCS_LINK = `${DOCS_PREFIX}/security-and-users.html`;
@@ -31,14 +37,17 @@ export const RoleMappings: React.FC = () => {
enableRoleBasedAccess,
initializeRoleMappings,
initializeRoleMapping,
+ initializeSingleUserRoleMapping,
handleDeleteMapping,
resetState,
} = useActions(RoleMappingsLogic);
const {
roleMappings,
+ singleUserRoleMappings,
multipleAuthProvidersConfig,
dataLoading,
roleMappingFlyoutOpen,
+ singleUserRoleMappingFlyoutOpen,
} = useValues(RoleMappingsLogic);
useEffect(() => {
@@ -46,6 +55,8 @@ export const RoleMappings: React.FC = () => {
return resetState;
}, []);
+ const hasUsers = singleUserRoleMappings.length > 0;
+
const rolesEmptyState = (
{
);
+ const usersTable = (
+
+ );
+
+ const usersSection = (
+ <>
+ initializeSingleUserRoleMapping()} />
+
+ {hasUsers ? usersTable : }
+ >
+ );
+
return (
{
emptyState={rolesEmptyState}
>
{roleMappingFlyoutOpen && }
+ {singleUserRoleMappingFlyoutOpen && }
{roleMappingsSection}
+
+ {usersSection}
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts
index 6985f213d1dd5..9f3ef90c2feac 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts
@@ -15,11 +15,18 @@ import { engines } from '../../__mocks__/engines.mock';
import { nextTick } from '@kbn/test/jest';
-import { asRoleMapping } from '../../../shared/role_mapping/__mocks__/roles';
+import { elasticsearchUsers } from '../../../shared/role_mapping/__mocks__/elasticsearch_users';
+
+import {
+ asRoleMapping,
+ asSingleUserRoleMapping,
+} from '../../../shared/role_mapping/__mocks__/roles';
import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants';
import { RoleMappingsLogic } from './role_mappings_logic';
+const emptyUser = { username: '', email: '' };
+
describe('RoleMappingsLogic', () => {
const { http } = mockHttpValues;
const { clearFlashMessages, flashAPIErrors, setSuccessMessage } = mockFlashMessageHelpers;
@@ -28,6 +35,8 @@ describe('RoleMappingsLogic', () => {
attributes: [],
availableAuthProviders: [],
elasticsearchRoles: [],
+ elasticsearchUser: emptyUser,
+ elasticsearchUsers: [],
roleMapping: null,
roleMappingFlyoutOpen: false,
roleMappings: [],
@@ -43,6 +52,14 @@ describe('RoleMappingsLogic', () => {
selectedAuthProviders: [ANY_AUTH_PROVIDER],
selectedOptions: [],
roleMappingErrors: [],
+ singleUserRoleMapping: null,
+ singleUserRoleMappings: [],
+ singleUserRoleMappingFlyoutOpen: false,
+ userCreated: false,
+ userFormIsNewUser: true,
+ userFormUserIsExisting: true,
+ smtpSettingsPresent: false,
+ formLoading: false,
};
const mappingsServerProps = {
@@ -53,6 +70,9 @@ describe('RoleMappingsLogic', () => {
availableEngines: engines,
elasticsearchRoles: [],
hasAdvancedRoles: false,
+ singleUserRoleMappings: [asSingleUserRoleMapping],
+ elasticsearchUsers,
+ smtpSettingsPresent: false,
};
beforeEach(() => {
@@ -83,7 +103,19 @@ describe('RoleMappingsLogic', () => {
elasticsearchRoles: mappingsServerProps.elasticsearchRoles,
selectedEngines: new Set(),
selectedOptions: [],
+ elasticsearchUsers,
+ elasticsearchUser: elasticsearchUsers[0],
+ singleUserRoleMappings: [asSingleUserRoleMapping],
+ });
+ });
+
+ it('handles fallback if no elasticsearch users present', () => {
+ RoleMappingsLogic.actions.setRoleMappingsData({
+ ...mappingsServerProps,
+ elasticsearchUsers: [],
});
+
+ expect(RoleMappingsLogic.values.elasticsearchUser).toEqual(emptyUser);
});
});
@@ -94,6 +126,26 @@ describe('RoleMappingsLogic', () => {
expect(RoleMappingsLogic.values.dataLoading).toEqual(false);
});
+ describe('setElasticsearchUser', () => {
+ it('sets user', () => {
+ RoleMappingsLogic.actions.setElasticsearchUser(elasticsearchUsers[0]);
+
+ expect(RoleMappingsLogic.values.elasticsearchUser).toEqual(elasticsearchUsers[0]);
+ });
+
+ it('handles fallback if no user present', () => {
+ RoleMappingsLogic.actions.setElasticsearchUser(undefined);
+
+ expect(RoleMappingsLogic.values.elasticsearchUser).toEqual(emptyUser);
+ });
+ });
+
+ it('setSingleUserRoleMapping', () => {
+ RoleMappingsLogic.actions.setSingleUserRoleMapping(asSingleUserRoleMapping);
+
+ expect(RoleMappingsLogic.values.singleUserRoleMapping).toEqual(asSingleUserRoleMapping);
+ });
+
it('handleRoleChange', () => {
RoleMappingsLogic.actions.handleRoleChange('dev');
@@ -152,6 +204,12 @@ describe('RoleMappingsLogic', () => {
});
});
+ it('setUserExistingRadioValue', () => {
+ RoleMappingsLogic.actions.setUserExistingRadioValue(false);
+
+ expect(RoleMappingsLogic.values.userFormUserIsExisting).toEqual(false);
+ });
+
describe('handleAttributeSelectorChange', () => {
const elasticsearchRoles = ['foo', 'bar'];
@@ -174,6 +232,8 @@ describe('RoleMappingsLogic', () => {
attributeName: 'role',
elasticsearchRoles,
selectedEngines: new Set(),
+ elasticsearchUsers,
+ singleUserRoleMappings: [asSingleUserRoleMapping],
});
});
@@ -260,16 +320,59 @@ describe('RoleMappingsLogic', () => {
expect(clearFlashMessages).toHaveBeenCalled();
});
- it('closeRoleMappingFlyout', () => {
+ it('openSingleUserRoleMappingFlyout', () => {
+ mount(mappingsServerProps);
+ RoleMappingsLogic.actions.openSingleUserRoleMappingFlyout();
+
+ expect(RoleMappingsLogic.values.singleUserRoleMappingFlyoutOpen).toEqual(true);
+ expect(clearFlashMessages).toHaveBeenCalled();
+ });
+
+ it('closeUsersAndRolesFlyout', () => {
mount({
...mappingsServerProps,
roleMappingFlyoutOpen: true,
});
- RoleMappingsLogic.actions.closeRoleMappingFlyout();
+ RoleMappingsLogic.actions.closeUsersAndRolesFlyout();
expect(RoleMappingsLogic.values.roleMappingFlyoutOpen).toEqual(false);
expect(clearFlashMessages).toHaveBeenCalled();
});
+
+ it('setElasticsearchUsernameValue', () => {
+ const username = 'newName';
+ RoleMappingsLogic.actions.setElasticsearchUsernameValue(username);
+
+ expect(RoleMappingsLogic.values).toEqual({
+ ...DEFAULT_VALUES,
+ elasticsearchUser: {
+ ...RoleMappingsLogic.values.elasticsearchUser,
+ username,
+ },
+ });
+ });
+
+ it('setElasticsearchEmailValue', () => {
+ const email = 'newEmail@foo.cats';
+ RoleMappingsLogic.actions.setElasticsearchEmailValue(email);
+
+ expect(RoleMappingsLogic.values).toEqual({
+ ...DEFAULT_VALUES,
+ elasticsearchUser: {
+ ...RoleMappingsLogic.values.elasticsearchUser,
+ email,
+ },
+ });
+ });
+
+ it('setUserCreated', () => {
+ RoleMappingsLogic.actions.setUserCreated();
+
+ expect(RoleMappingsLogic.values).toEqual({
+ ...DEFAULT_VALUES,
+ userCreated: true,
+ });
+ });
});
describe('listeners', () => {
@@ -335,6 +438,39 @@ describe('RoleMappingsLogic', () => {
});
});
+ describe('initializeSingleUserRoleMapping', () => {
+ let setElasticsearchUserSpy: jest.MockedFunction;
+ let setRoleMappingSpy: jest.MockedFunction;
+ let setSingleUserRoleMappingSpy: jest.MockedFunction;
+ beforeEach(() => {
+ setElasticsearchUserSpy = jest.spyOn(RoleMappingsLogic.actions, 'setElasticsearchUser');
+ setRoleMappingSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMapping');
+ setSingleUserRoleMappingSpy = jest.spyOn(
+ RoleMappingsLogic.actions,
+ 'setSingleUserRoleMapping'
+ );
+ });
+
+ it('should handle the new user state and only set an empty mapping', () => {
+ RoleMappingsLogic.actions.initializeSingleUserRoleMapping();
+
+ expect(setElasticsearchUserSpy).not.toHaveBeenCalled();
+ expect(setRoleMappingSpy).not.toHaveBeenCalled();
+ expect(setSingleUserRoleMappingSpy).toHaveBeenCalledWith(undefined);
+ });
+
+ it('should handle an existing user state and set mapping', () => {
+ RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps);
+ RoleMappingsLogic.actions.initializeSingleUserRoleMapping(
+ asSingleUserRoleMapping.roleMapping.id
+ );
+
+ expect(setElasticsearchUserSpy).toHaveBeenCalled();
+ expect(setRoleMappingSpy).toHaveBeenCalled();
+ expect(setSingleUserRoleMappingSpy).toHaveBeenCalledWith(asSingleUserRoleMapping);
+ });
+ });
+
describe('handleSaveMapping', () => {
const body = {
roleType: 'owner',
@@ -430,6 +566,94 @@ describe('RoleMappingsLogic', () => {
});
});
+ describe('handleSaveUser', () => {
+ it('calls API and refreshes list when new mapping', async () => {
+ const initializeRoleMappingsSpy = jest.spyOn(
+ RoleMappingsLogic.actions,
+ 'initializeRoleMappings'
+ );
+ const setUserCreatedSpy = jest.spyOn(RoleMappingsLogic.actions, 'setUserCreated');
+ const setSingleUserRoleMappingSpy = jest.spyOn(
+ RoleMappingsLogic.actions,
+ 'setSingleUserRoleMapping'
+ );
+ RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps);
+
+ http.post.mockReturnValue(Promise.resolve(mappingsServerProps));
+ RoleMappingsLogic.actions.handleSaveUser();
+
+ expect(http.post).toHaveBeenCalledWith('/api/app_search/single_user_role_mapping', {
+ body: JSON.stringify({
+ roleMapping: {
+ engines: [],
+ roleType: 'owner',
+ accessAllEngines: true,
+ },
+ elasticsearchUser: {
+ username: elasticsearchUsers[0].username,
+ email: elasticsearchUsers[0].email,
+ },
+ }),
+ });
+ await nextTick();
+
+ expect(initializeRoleMappingsSpy).toHaveBeenCalled();
+ expect(setUserCreatedSpy).toHaveBeenCalled();
+ expect(setSingleUserRoleMappingSpy).toHaveBeenCalled();
+ });
+
+ it('calls API and refreshes list when existing mapping', async () => {
+ const initializeRoleMappingsSpy = jest.spyOn(
+ RoleMappingsLogic.actions,
+ 'initializeRoleMappings'
+ );
+ RoleMappingsLogic.actions.setSingleUserRoleMapping(asSingleUserRoleMapping);
+ RoleMappingsLogic.actions.handleAccessAllEnginesChange(false);
+
+ http.put.mockReturnValue(Promise.resolve(mappingsServerProps));
+ RoleMappingsLogic.actions.handleSaveUser();
+
+ expect(http.post).toHaveBeenCalledWith('/api/app_search/single_user_role_mapping', {
+ body: JSON.stringify({
+ roleMapping: {
+ engines: [],
+ roleType: 'owner',
+ accessAllEngines: false,
+ id: asSingleUserRoleMapping.roleMapping.id,
+ },
+ elasticsearchUser: {
+ username: '',
+ email: '',
+ },
+ }),
+ });
+ await nextTick();
+
+ expect(initializeRoleMappingsSpy).toHaveBeenCalled();
+ });
+
+ it('handles error', async () => {
+ const setRoleMappingErrorsSpy = jest.spyOn(
+ RoleMappingsLogic.actions,
+ 'setRoleMappingErrors'
+ );
+
+ http.post.mockReturnValue(
+ Promise.reject({
+ body: {
+ attributes: {
+ errors: ['this is an error'],
+ },
+ },
+ })
+ );
+ RoleMappingsLogic.actions.handleSaveUser();
+ await nextTick();
+
+ expect(setRoleMappingErrorsSpy).toHaveBeenCalledWith(['this is an error']);
+ });
+ });
+
describe('handleDeleteMapping', () => {
const roleMappingId = 'r1';
@@ -458,5 +682,52 @@ describe('RoleMappingsLogic', () => {
expect(flashAPIErrors).toHaveBeenCalledWith('this is an error');
});
});
+
+ describe('handleUsernameSelectChange', () => {
+ it('sets elasticsearchUser when match found', () => {
+ RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps);
+ const setElasticsearchUserSpy = jest.spyOn(
+ RoleMappingsLogic.actions,
+ 'setElasticsearchUser'
+ );
+ RoleMappingsLogic.actions.handleUsernameSelectChange(elasticsearchUsers[0].username);
+
+ expect(setElasticsearchUserSpy).toHaveBeenCalledWith(elasticsearchUsers[0]);
+ });
+
+ it('does not set elasticsearchUser when no match found', () => {
+ RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps);
+ const setElasticsearchUserSpy = jest.spyOn(
+ RoleMappingsLogic.actions,
+ 'setElasticsearchUser'
+ );
+ RoleMappingsLogic.actions.handleUsernameSelectChange('bogus');
+
+ expect(setElasticsearchUserSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('setUserExistingRadioValue', () => {
+ it('handles existing user', () => {
+ RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps);
+ const setElasticsearchUserSpy = jest.spyOn(
+ RoleMappingsLogic.actions,
+ 'setElasticsearchUser'
+ );
+ RoleMappingsLogic.actions.setUserExistingRadioValue(true);
+
+ expect(setElasticsearchUserSpy).toHaveBeenCalledWith(elasticsearchUsers[0]);
+ });
+
+ it('handles new user', () => {
+ const setElasticsearchUserSpy = jest.spyOn(
+ RoleMappingsLogic.actions,
+ 'setElasticsearchUser'
+ );
+ RoleMappingsLogic.actions.setUserExistingRadioValue(false);
+
+ expect(setElasticsearchUserSpy).toHaveBeenCalledWith(emptyUser);
+ });
+ });
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts
index e2ef75897528c..fb574a3588989 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts
@@ -7,16 +7,19 @@
import { kea, MakeLogicType } from 'kea';
-import { EuiComboBoxOptionOption } from '@elastic/eui';
-
import {
clearFlashMessages,
flashAPIErrors,
setSuccessMessage,
} from '../../../shared/flash_messages';
import { HttpLogic } from '../../../shared/http';
+import {
+ RoleMappingsBaseServerDetails,
+ RoleMappingsBaseActions,
+ RoleMappingsBaseValues,
+} from '../../../shared/role_mapping';
import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants';
-import { AttributeName } from '../../../shared/types';
+import { AttributeName, SingleUserRoleMapping, ElasticsearchUser } from '../../../shared/types';
import { ASRoleMapping, RoleTypes } from '../../types';
import { roleHasScopedEngines } from '../../utils/role/has_scoped_engines';
import { Engine } from '../engine/types';
@@ -27,79 +30,60 @@ import {
ROLE_MAPPING_UPDATED_MESSAGE,
} from './constants';
-interface RoleMappingsServerDetails {
+type UserMapping = SingleUserRoleMapping;
+
+interface RoleMappingsServerDetails extends RoleMappingsBaseServerDetails {
roleMappings: ASRoleMapping[];
- attributes: string[];
- authProviders: string[];
availableEngines: Engine[];
- elasticsearchRoles: string[];
+ singleUserRoleMappings: UserMapping[];
hasAdvancedRoles: boolean;
- multipleAuthProvidersConfig: boolean;
}
const getFirstAttributeName = (roleMapping: ASRoleMapping) =>
Object.entries(roleMapping.rules)[0][0] as AttributeName;
const getFirstAttributeValue = (roleMapping: ASRoleMapping) =>
Object.entries(roleMapping.rules)[0][1] as AttributeName;
+const emptyUser = { username: '', email: '' } as ElasticsearchUser;
-interface RoleMappingsActions {
- handleAccessAllEnginesChange(selected: boolean): { selected: boolean };
- handleAuthProviderChange(value: string[]): { value: string[] };
- handleAttributeSelectorChange(
- value: AttributeName,
- firstElasticsearchRole: string
- ): { value: AttributeName; firstElasticsearchRole: string };
- handleAttributeValueChange(value: string): { value: string };
- handleDeleteMapping(roleMappingId: string): { roleMappingId: string };
- handleEngineSelectionChange(engineNames: string[]): { engineNames: string[] };
- handleRoleChange(roleType: RoleTypes): { roleType: RoleTypes };
- handleSaveMapping(): void;
- initializeRoleMapping(roleMappingId?: string): { roleMappingId?: string };
- initializeRoleMappings(): void;
- resetState(): void;
+interface RoleMappingsActions extends RoleMappingsBaseActions {
setRoleMapping(roleMapping: ASRoleMapping): { roleMapping: ASRoleMapping };
+ setSingleUserRoleMapping(data?: UserMapping): { singleUserRoleMapping: UserMapping };
setRoleMappings({
roleMappings,
}: {
roleMappings: ASRoleMapping[];
}): { roleMappings: ASRoleMapping[] };
setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails;
- openRoleMappingFlyout(): void;
- closeRoleMappingFlyout(): void;
- setRoleMappingErrors(errors: string[]): { errors: string[] };
- enableRoleBasedAccess(): void;
+ handleAccessAllEnginesChange(selected: boolean): { selected: boolean };
+ handleEngineSelectionChange(engineNames: string[]): { engineNames: string[] };
+ handleRoleChange(roleType: RoleTypes): { roleType: RoleTypes };
}
-interface RoleMappingsValues {
+interface RoleMappingsValues extends RoleMappingsBaseValues {
accessAllEngines: boolean;
- attributeName: AttributeName;
- attributeValue: string;
- attributes: string[];
- availableAuthProviders: string[];
availableEngines: Engine[];
- dataLoading: boolean;
- elasticsearchRoles: string[];
- hasAdvancedRoles: boolean;
- multipleAuthProvidersConfig: boolean;
roleMapping: ASRoleMapping | null;
roleMappings: ASRoleMapping[];
+ singleUserRoleMapping: UserMapping | null;
+ singleUserRoleMappings: UserMapping[];
roleType: RoleTypes;
selectedAuthProviders: string[];
selectedEngines: Set;
- roleMappingFlyoutOpen: boolean;
- selectedOptions: EuiComboBoxOptionOption[];
- roleMappingErrors: string[];
+ hasAdvancedRoles: boolean;
}
export const RoleMappingsLogic = kea>({
- path: ['enterprise_search', 'app_search', 'role_mappings'],
+ path: ['enterprise_search', 'app_search', 'users_and_roles'],
actions: {
setRoleMappingsData: (data: RoleMappingsServerDetails) => data,
setRoleMapping: (roleMapping: ASRoleMapping) => ({ roleMapping }),
+ setElasticsearchUser: (elasticsearchUser: ElasticsearchUser) => ({ elasticsearchUser }),
+ setSingleUserRoleMapping: (singleUserRoleMapping: UserMapping) => ({ singleUserRoleMapping }),
setRoleMappings: ({ roleMappings }: { roleMappings: ASRoleMapping[] }) => ({ roleMappings }),
setRoleMappingErrors: (errors: string[]) => ({ errors }),
handleAuthProviderChange: (value: string) => ({ value }),
handleRoleChange: (roleType: RoleTypes) => ({ roleType }),
+ handleUsernameSelectChange: (username: string) => ({ username }),
handleEngineSelectionChange: (engineNames: string[]) => ({ engineNames }),
handleAttributeSelectorChange: (value: string, firstElasticsearchRole: string) => ({
value,
@@ -108,13 +92,21 @@ export const RoleMappingsLogic = kea ({ value }),
handleAccessAllEnginesChange: (selected: boolean) => ({ selected }),
enableRoleBasedAccess: true,
+ openSingleUserRoleMappingFlyout: true,
+ setUserExistingRadioValue: (userFormUserIsExisting: boolean) => ({ userFormUserIsExisting }),
resetState: true,
initializeRoleMappings: true,
+ initializeSingleUserRoleMapping: (roleMappingId?: string) => ({ roleMappingId }),
initializeRoleMapping: (roleMappingId) => ({ roleMappingId }),
handleDeleteMapping: (roleMappingId: string) => ({ roleMappingId }),
handleSaveMapping: true,
+ handleSaveUser: true,
openRoleMappingFlyout: true,
- closeRoleMappingFlyout: false,
+ closeUsersAndRolesFlyout: false,
+ setElasticsearchUsernameValue: (username: string) => ({ username }),
+ setElasticsearchEmailValue: (email: string) => ({ email }),
+ setUserCreated: true,
+ setUserFormIsNewUser: (userFormIsNewUser: boolean) => ({ userFormIsNewUser }),
},
reducers: {
dataLoading: [
@@ -134,6 +126,13 @@ export const RoleMappingsLogic = kea [],
},
],
+ singleUserRoleMappings: [
+ [],
+ {
+ setRoleMappingsData: (_, { singleUserRoleMappings }) => singleUserRoleMappings,
+ resetState: () => [],
+ },
+ ],
multipleAuthProvidersConfig: [
false,
{
@@ -165,6 +164,14 @@ export const RoleMappingsLogic = kea elasticsearchRoles,
+ closeUsersAndRolesFlyout: () => [ANY_AUTH_PROVIDER],
+ },
+ ],
+ elasticsearchUsers: [
+ [],
+ {
+ setRoleMappingsData: (_, { elasticsearchUsers }) => elasticsearchUsers,
+ resetState: () => [],
},
],
roleMapping: [
@@ -172,7 +179,7 @@ export const RoleMappingsLogic = kea roleMapping,
resetState: () => null,
- closeRoleMappingFlyout: () => null,
+ closeUsersAndRolesFlyout: () => null,
},
],
roleType: [
@@ -188,6 +195,7 @@ export const RoleMappingsLogic = kea roleMapping.accessAllEngines,
handleRoleChange: (_, { roleType }) => !roleHasScopedEngines(roleType),
handleAccessAllEnginesChange: (_, { selected }) => selected,
+ closeUsersAndRolesFlyout: () => true,
},
],
attributeValue: [
@@ -198,7 +206,7 @@ export const RoleMappingsLogic = kea value,
resetState: () => '',
- closeRoleMappingFlyout: () => '',
+ closeUsersAndRolesFlyout: () => '',
},
],
attributeName: [
@@ -207,7 +215,7 @@ export const RoleMappingsLogic = kea getFirstAttributeName(roleMapping),
handleAttributeSelectorChange: (_, { value }) => value,
resetState: () => 'username',
- closeRoleMappingFlyout: () => 'username',
+ closeUsersAndRolesFlyout: () => 'username',
},
],
selectedEngines: [
@@ -222,6 +230,7 @@ export const RoleMappingsLogic = kea new Set(),
},
],
availableAuthProviders: [
@@ -251,17 +260,83 @@ export const RoleMappingsLogic = kea true,
- closeRoleMappingFlyout: () => false,
+ closeUsersAndRolesFlyout: () => false,
initializeRoleMappings: () => false,
initializeRoleMapping: () => true,
},
],
+ singleUserRoleMappingFlyoutOpen: [
+ false,
+ {
+ openSingleUserRoleMappingFlyout: () => true,
+ closeUsersAndRolesFlyout: () => false,
+ initializeSingleUserRoleMapping: () => true,
+ },
+ ],
+ singleUserRoleMapping: [
+ null,
+ {
+ setSingleUserRoleMapping: (_, { singleUserRoleMapping }) => singleUserRoleMapping || null,
+ closeUsersAndRolesFlyout: () => null,
+ },
+ ],
roleMappingErrors: [
[],
{
setRoleMappingErrors: (_, { errors }) => errors,
handleSaveMapping: () => [],
- closeRoleMappingFlyout: () => [],
+ closeUsersAndRolesFlyout: () => [],
+ },
+ ],
+ userFormUserIsExisting: [
+ true,
+ {
+ setUserExistingRadioValue: (_, { userFormUserIsExisting }) => userFormUserIsExisting,
+ closeUsersAndRolesFlyout: () => true,
+ },
+ ],
+ elasticsearchUser: [
+ emptyUser,
+ {
+ setRoleMappingsData: (_, { elasticsearchUsers }) => elasticsearchUsers[0] || emptyUser,
+ setElasticsearchUser: (_, { elasticsearchUser }) => elasticsearchUser || emptyUser,
+ setElasticsearchUsernameValue: (state, { username }) => ({
+ ...state,
+ username,
+ }),
+ setElasticsearchEmailValue: (state, { email }) => ({
+ ...state,
+ email,
+ }),
+ closeUsersAndRolesFlyout: () => emptyUser,
+ },
+ ],
+ userCreated: [
+ false,
+ {
+ setUserCreated: () => true,
+ closeUsersAndRolesFlyout: () => false,
+ },
+ ],
+ userFormIsNewUser: [
+ true,
+ {
+ setUserFormIsNewUser: (_, { userFormIsNewUser }) => userFormIsNewUser,
+ },
+ ],
+ smtpSettingsPresent: [
+ false,
+ {
+ setRoleMappingsData: (_, { smtpSettingsPresent }) => smtpSettingsPresent,
+ },
+ ],
+ formLoading: [
+ false,
+ {
+ handleSaveMapping: () => true,
+ handleSaveUser: () => true,
+ initializeRoleMappings: () => false,
+ setRoleMappingErrors: () => false,
},
],
},
@@ -303,6 +378,17 @@ export const RoleMappingsLogic = kea id === roleMappingId);
if (roleMapping) actions.setRoleMapping(roleMapping);
},
+ initializeSingleUserRoleMapping: ({ roleMappingId }) => {
+ const singleUserRoleMapping = values.singleUserRoleMappings.find(
+ ({ roleMapping }) => roleMapping.id === roleMappingId
+ );
+ if (singleUserRoleMapping) {
+ actions.setElasticsearchUser(singleUserRoleMapping.elasticsearchUser);
+ actions.setRoleMapping(singleUserRoleMapping.roleMapping);
+ }
+ actions.setSingleUserRoleMapping(singleUserRoleMapping);
+ actions.setUserFormIsNewUser(!singleUserRoleMapping);
+ },
handleDeleteMapping: async ({ roleMappingId }) => {
const { http } = HttpLogic.values;
const route = `/api/app_search/role_mappings/${roleMappingId}`;
@@ -357,11 +443,56 @@ export const RoleMappingsLogic = kea {
clearFlashMessages();
},
- closeRoleMappingFlyout: () => {
+ handleSaveUser: async () => {
+ const { http } = HttpLogic.values;
+ const {
+ roleType,
+ singleUserRoleMapping,
+ accessAllEngines,
+ selectedEngines,
+ elasticsearchUser: { email, username },
+ } = values;
+
+ const body = JSON.stringify({
+ roleMapping: {
+ engines: accessAllEngines ? [] : Array.from(selectedEngines),
+ roleType,
+ accessAllEngines,
+ id: singleUserRoleMapping?.roleMapping?.id,
+ },
+ elasticsearchUser: {
+ username,
+ email,
+ },
+ });
+
+ try {
+ const response = await http.post('/api/app_search/single_user_role_mapping', { body });
+ actions.setSingleUserRoleMapping(response);
+ actions.setUserCreated();
+ actions.initializeRoleMappings();
+ } catch (e) {
+ actions.setRoleMappingErrors(e?.body?.attributes?.errors);
+ }
+ },
+ closeUsersAndRolesFlyout: () => {
clearFlashMessages();
+ const firstUser = values.elasticsearchUsers[0];
+ actions.setElasticsearchUser(firstUser);
},
openRoleMappingFlyout: () => {
clearFlashMessages();
},
+ openSingleUserRoleMappingFlyout: () => {
+ clearFlashMessages();
+ },
+ setUserExistingRadioValue: ({ userFormUserIsExisting }) => {
+ const firstUser = values.elasticsearchUsers[0];
+ actions.setElasticsearchUser(userFormUserIsExisting ? firstUser : emptyUser);
+ },
+ handleUsernameSelectChange: ({ username }) => {
+ const user = values.elasticsearchUsers.find((u) => u.username === username);
+ if (user) actions.setElasticsearchUser(user);
+ },
}),
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.test.tsx
new file mode 100644
index 0000000000000..cec7f1541a31a
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.test.tsx
@@ -0,0 +1,146 @@
+/*
+ * 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 '../../../__mocks__/react_router';
+import '../../../__mocks__/shallow_useeffect.mock';
+import { setMockActions, setMockValues } from '../../../__mocks__/kea_logic';
+import { engines } from '../../__mocks__/engines.mock';
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import {
+ UserFlyout,
+ UserAddedInfo,
+ UserInvitationCallout,
+ DeactivatedUserCallout,
+} from '../../../shared/role_mapping';
+import { elasticsearchUsers } from '../../../shared/role_mapping/__mocks__/elasticsearch_users';
+import { wsSingleUserRoleMapping } from '../../../shared/role_mapping/__mocks__/roles';
+
+import { EngineAssignmentSelector } from './engine_assignment_selector';
+import { User } from './user';
+
+describe('User', () => {
+ const handleSaveUser = jest.fn();
+ const closeUsersAndRolesFlyout = jest.fn();
+ const setUserExistingRadioValue = jest.fn();
+ const setElasticsearchUsernameValue = jest.fn();
+ const setElasticsearchEmailValue = jest.fn();
+ const handleRoleChange = jest.fn();
+ const handleUsernameSelectChange = jest.fn();
+
+ const mockValues = {
+ availableEngines: [],
+ singleUserRoleMapping: null,
+ userFormUserIsExisting: false,
+ elasticsearchUsers: [],
+ elasticsearchUser: {},
+ roleType: 'admin',
+ roleMappingErrors: [],
+ userCreated: false,
+ userFormIsNewUser: false,
+ hasAdvancedRoles: false,
+ };
+
+ beforeEach(() => {
+ setMockActions({
+ handleSaveUser,
+ closeUsersAndRolesFlyout,
+ setUserExistingRadioValue,
+ setElasticsearchUsernameValue,
+ setElasticsearchEmailValue,
+ handleRoleChange,
+ handleUsernameSelectChange,
+ });
+
+ setMockValues(mockValues);
+ });
+
+ it('renders', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find(UserFlyout)).toHaveLength(1);
+ });
+
+ it('renders engine assignment selector when groups present', () => {
+ setMockValues({ ...mockValues, availableEngines: engines, hasAdvancedRoles: true });
+ const wrapper = shallow( );
+
+ expect(wrapper.find(EngineAssignmentSelector)).toHaveLength(1);
+ });
+
+ it('renders userInvitationCallout', () => {
+ setMockValues({
+ ...mockValues,
+ singleUserRoleMapping: wsSingleUserRoleMapping,
+ });
+ const wrapper = shallow( );
+
+ expect(wrapper.find(UserInvitationCallout)).toHaveLength(1);
+ });
+
+ it('renders user added info when user created', () => {
+ setMockValues({
+ ...mockValues,
+ singleUserRoleMapping: wsSingleUserRoleMapping,
+ userCreated: true,
+ });
+ const wrapper = shallow( );
+
+ expect(wrapper.find(UserAddedInfo)).toHaveLength(1);
+ });
+
+ it('renders DeactivatedUserCallout', () => {
+ setMockValues({
+ ...mockValues,
+ singleUserRoleMapping: {
+ ...wsSingleUserRoleMapping,
+ invitation: null,
+ elasticsearchUser: {
+ ...wsSingleUserRoleMapping.elasticsearchUser,
+ enabled: false,
+ },
+ },
+ });
+ const wrapper = shallow( );
+
+ expect(wrapper.find(DeactivatedUserCallout)).toHaveLength(1);
+ });
+
+ it('disables form when username value not present', () => {
+ setMockValues({
+ ...mockValues,
+ singleUserRoleMapping: wsSingleUserRoleMapping,
+ elasticsearchUsers,
+ elasticsearchUser: {
+ username: null,
+ email: 'email@user.com',
+ },
+ });
+ const wrapper = shallow( );
+
+ expect(wrapper.find(UserFlyout).prop('disabled')).toEqual(true);
+ });
+
+ it('enables form when userFormUserIsExisting', () => {
+ setMockValues({
+ ...mockValues,
+ userFormUserIsExisting: true.valueOf,
+ singleUserRoleMapping: wsSingleUserRoleMapping,
+ elasticsearchUsers,
+ elasticsearchUser: {
+ username: null,
+ email: 'email@user.com',
+ },
+ });
+ const wrapper = shallow( );
+
+ expect(wrapper.find(UserFlyout).prop('disabled')).toEqual(false);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.tsx
new file mode 100644
index 0000000000000..018d29706b05f
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.tsx
@@ -0,0 +1,117 @@
+/*
+ * 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 React from 'react';
+
+import { useActions, useValues } from 'kea';
+
+import { EuiForm } from '@elastic/eui';
+
+import { getAppSearchUrl } from '../../../shared/enterprise_search_url';
+import {
+ UserFlyout,
+ UserSelector,
+ UserAddedInfo,
+ UserInvitationCallout,
+ DeactivatedUserCallout,
+} from '../../../shared/role_mapping';
+import { RoleTypes } from '../../types';
+
+import { EngineAssignmentSelector } from './engine_assignment_selector';
+import { RoleMappingsLogic } from './role_mappings_logic';
+
+const standardRoles = (['owner', 'admin'] as unknown) as RoleTypes[];
+const advancedRoles = (['dev', 'editor', 'analyst'] as unknown) as RoleTypes[];
+
+export const User: React.FC = () => {
+ const {
+ handleSaveUser,
+ closeUsersAndRolesFlyout,
+ setUserExistingRadioValue,
+ setElasticsearchUsernameValue,
+ setElasticsearchEmailValue,
+ handleRoleChange,
+ handleUsernameSelectChange,
+ } = useActions(RoleMappingsLogic);
+
+ const {
+ availableEngines,
+ singleUserRoleMapping,
+ hasAdvancedRoles,
+ userFormUserIsExisting,
+ elasticsearchUsers,
+ elasticsearchUser,
+ roleType,
+ roleMappingErrors,
+ userCreated,
+ userFormIsNewUser,
+ smtpSettingsPresent,
+ formLoading,
+ } = useValues(RoleMappingsLogic);
+
+ const roleTypes = hasAdvancedRoles ? [...standardRoles, ...advancedRoles] : standardRoles;
+ const hasEngines = availableEngines.length > 0;
+ const showEngineAssignmentSelector = hasEngines && hasAdvancedRoles;
+ const flyoutDisabled =
+ !userFormUserIsExisting && (!elasticsearchUser.email || !elasticsearchUser.username);
+ const userIsDeactivated = !!(
+ singleUserRoleMapping &&
+ !singleUserRoleMapping.invitation &&
+ !singleUserRoleMapping.elasticsearchUser.enabled
+ );
+
+ const userAddedInfo = singleUserRoleMapping && (
+
+ );
+
+ const userInvitationCallout = singleUserRoleMapping?.invitation && (
+
+ );
+
+ const createUserForm = (
+ 0} error={roleMappingErrors}>
+
+ {showEngineAssignmentSelector && }
+
+ );
+
+ return (
+
+ {userCreated ? userAddedInfo : createUserForm}
+ {userInvitationCallout}
+ {userIsDeactivated && }
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx
index 2402a6ecc6401..6647b4032e4bc 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx
@@ -7,7 +7,6 @@
import { DEFAULT_INITIAL_APP_DATA } from '../../../common/__mocks__';
import { setMockValues } from '../__mocks__/kea_logic';
-import { mockUseRouteMatch } from '../__mocks__/react_router';
import '../__mocks__/shallow_useeffect.mock';
import '../__mocks__/enterprise_search_url.mock';
@@ -17,15 +16,13 @@ import { Redirect } from 'react-router-dom';
import { shallow, ShallowWrapper } from 'enzyme';
-import { Layout, SideNav, SideNavLink } from '../shared/layout';
-
import { rerender } from '../test_helpers';
jest.mock('./app_logic', () => ({ AppLogic: jest.fn() }));
import { AppLogic } from './app_logic';
import { Credentials } from './components/credentials';
-import { EngineRouter, EngineNav } from './components/engine';
+import { EngineRouter } from './components/engine';
import { EngineCreation } from './components/engine_creation';
import { EnginesOverview } from './components/engines';
import { ErrorConnecting } from './components/error_connecting';
@@ -35,7 +32,7 @@ import { RoleMappings } from './components/role_mappings';
import { Settings } from './components/settings';
import { SetupGuide } from './components/setup_guide';
-import { AppSearch, AppSearchUnconfigured, AppSearchConfigured, AppSearchNav } from './';
+import { AppSearch, AppSearchUnconfigured, AppSearchConfigured } from './';
describe('AppSearch', () => {
it('always renders the Setup Guide', () => {
@@ -83,13 +80,6 @@ describe('AppSearchConfigured', () => {
wrapper = shallow( );
});
- it('renders with layout', () => {
- expect(wrapper.find(Layout)).toHaveLength(1);
- expect(wrapper.find(Layout).prop('readOnlyMode')).toBeFalsy();
- expect(wrapper.find(EnginesOverview)).toHaveLength(1);
- expect(wrapper.find(EngineRouter)).toHaveLength(1);
- });
-
it('renders header actions', () => {
expect(renderHeaderActions).toHaveBeenCalled();
});
@@ -98,11 +88,9 @@ describe('AppSearchConfigured', () => {
expect(AppLogic).toHaveBeenCalledWith(DEFAULT_INITIAL_APP_DATA);
});
- it('passes readOnlyMode state', () => {
- setMockValues({ myRole: {}, readOnlyMode: true });
- rerender(wrapper);
-
- expect(wrapper.find(Layout).first().prop('readOnlyMode')).toEqual(true);
+ it('renders engine routes', () => {
+ expect(wrapper.find(EnginesOverview)).toHaveLength(1);
+ expect(wrapper.find(EngineRouter)).toHaveLength(1);
});
describe('routes with ability checks', () => {
@@ -151,51 +139,3 @@ describe('AppSearchConfigured', () => {
});
});
});
-
-describe('AppSearchNav', () => {
- it('renders with the Engines link', () => {
- const wrapper = shallow( );
-
- expect(wrapper.find(SideNav)).toHaveLength(1);
- expect(wrapper.find(SideNavLink).prop('to')).toEqual('/engines');
- });
-
- describe('engine subnavigation', () => {
- const getEnginesLink = (wrapper: ShallowWrapper) => wrapper.find(SideNavLink).dive();
-
- it('does not render the engine subnav on top-level routes', () => {
- mockUseRouteMatch.mockReturnValueOnce(false);
- const wrapper = shallow( );
-
- expect(getEnginesLink(wrapper).find(EngineNav)).toHaveLength(0);
- });
-
- it('renders the engine subnav if currently on an engine route', () => {
- mockUseRouteMatch.mockReturnValueOnce(true);
- const wrapper = shallow( );
-
- expect(getEnginesLink(wrapper).find(EngineNav)).toHaveLength(1);
- });
- });
-
- it('renders the Settings link', () => {
- setMockValues({ myRole: { canViewSettings: true } });
- const wrapper = shallow( );
-
- expect(wrapper.find(SideNavLink).last().prop('to')).toEqual('/settings');
- });
-
- it('renders the Credentials link', () => {
- setMockValues({ myRole: { canViewAccountCredentials: true } });
- const wrapper = shallow( );
-
- expect(wrapper.find(SideNavLink).last().prop('to')).toEqual('/credentials');
- });
-
- it('renders the Role Mappings link', () => {
- setMockValues({ myRole: { canViewRoleMappings: true } });
- const wrapper = shallow( );
-
- expect(wrapper.find(SideNavLink).last().prop('to')).toEqual('/role_mappings');
- });
-});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx
index 191758af26758..11f706bff028f 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx
@@ -6,30 +6,26 @@
*/
import React, { useEffect } from 'react';
-import { Route, Redirect, Switch, useRouteMatch } from 'react-router-dom';
+import { Route, Redirect, Switch } from 'react-router-dom';
import { useValues } from 'kea';
-import { APP_SEARCH_PLUGIN } from '../../../common/constants';
import { InitialAppData } from '../../../common/types';
import { HttpLogic } from '../shared/http';
import { KibanaLogic } from '../shared/kibana';
-import { Layout, SideNav, SideNavLink } from '../shared/layout';
-import { NotFound } from '../shared/not_found';
-
-import { ROLE_MAPPINGS_TITLE } from '../shared/role_mapping/constants';
import { AppLogic } from './app_logic';
-import { Credentials, CREDENTIALS_TITLE } from './components/credentials';
-import { EngineNav, EngineRouter } from './components/engine';
+import { Credentials } from './components/credentials';
+import { EngineRouter } from './components/engine';
import { EngineCreation } from './components/engine_creation';
-import { EnginesOverview, ENGINES_TITLE } from './components/engines';
+import { EnginesOverview } from './components/engines';
import { ErrorConnecting } from './components/error_connecting';
import { KibanaHeaderActions } from './components/layout';
import { Library } from './components/library';
import { MetaEngineCreation } from './components/meta_engine_creation';
+import { NotFound } from './components/not_found';
import { RoleMappings } from './components/role_mappings';
-import { Settings, SETTINGS_TITLE } from './components/settings';
+import { Settings } from './components/settings';
import { SetupGuide } from './components/setup_guide';
import {
ENGINE_CREATION_PATH,
@@ -37,7 +33,7 @@ import {
SETUP_GUIDE_PATH,
SETTINGS_PATH,
CREDENTIALS_PATH,
- ROLE_MAPPINGS_PATH,
+ USERS_AND_ROLES_PATH,
ENGINES_PATH,
ENGINE_PATH,
LIBRARY_PATH,
@@ -85,7 +81,6 @@ export const AppSearchConfigured: React.FC> = (props) =
},
} = useValues(AppLogic(props));
const { renderHeaderActions } = useValues(KibanaLogic);
- const { readOnlyMode } = useValues(HttpLogic);
useEffect(() => {
renderHeaderActions(KibanaHeaderActions);
@@ -128,44 +123,13 @@ export const AppSearchConfigured: React.FC> = (props) =
)}
{canViewRoleMappings && (
-
+
)}
- } readOnlyMode={readOnlyMode}>
-
-
-
-
-
-
+
);
};
-
-export const AppSearchNav: React.FC = () => {
- const {
- myRole: { canViewSettings, canViewAccountCredentials, canViewRoleMappings },
- } = useValues(AppLogic);
-
- const isEngineRoute = !!useRouteMatch(ENGINE_PATH);
-
- return (
-
- : null} isRoot>
- {ENGINES_TITLE}
-
- {canViewSettings && {SETTINGS_TITLE} }
- {canViewAccountCredentials && (
- {CREDENTIALS_TITLE}
- )}
- {canViewRoleMappings && (
-
- {ROLE_MAPPINGS_TITLE}
-
- )}
-
- );
-};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts
index d9d1935c648f7..f086a32bbf590 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts
@@ -15,7 +15,7 @@ export const LIBRARY_PATH = '/library';
export const SETTINGS_PATH = '/settings';
export const CREDENTIALS_PATH = '/credentials';
-export const ROLE_MAPPINGS_PATH = '/role_mappings';
+export const USERS_AND_ROLES_PATH = '/users_and_roles';
export const ENGINES_PATH = '/engines';
export const ENGINE_CREATION_PATH = `${ENGINES_PATH}/new`; // This is safe from conflicting with an :engineName path because new is a reserved name
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/layout/index.ts
index 856d483e174a6..41f8869ad5f61 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/index.ts
@@ -7,7 +7,3 @@
export { EnterpriseSearchPageTemplate, PageTemplateProps } from './page_template';
export { generateNavLink } from './nav_link_helpers';
-
-// TODO: Delete these once KibanaPageTemplate migration is done
-export { Layout } from './layout';
-export { SideNav, SideNavLink, SideNavItem } from './side_nav';
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.scss b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.scss
deleted file mode 100644
index 88799406070cd..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.scss
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-.enterpriseSearchLayout {
- $sideBarWidth: $euiSize * 15;
- $sideBarMobileHeight: $euiSize * 4.75;
- $consoleHeaderHeight: 98px; // NOTE: Keep an eye on this for changes
- $pageHeight: calc(100vh - #{$consoleHeaderHeight});
-
- display: block;
- background-color: $euiColorEmptyShade;
- min-height: $pageHeight;
- position: relative;
- left: $sideBarWidth;
- width: calc(100% - #{$sideBarWidth});
- padding: 0;
-
- @include euiBreakpoint('xs', 's', 'm') {
- left: auto;
- width: 100%;
- }
-
- &__sideBarToggle {
- display: none;
-
- @include euiBreakpoint('xs', 's', 'm') {
- display: block;
-
- position: absolute;
- right: $euiSize;
- top: $sideBarMobileHeight / 2;
- transform: translateY(-50%) scale(.9);
-
- .euiButton {
- min-width: 0;
- }
- }
- }
-
- &__sideBar {
- z-index: $euiZNavigation;
- position: fixed;
- margin-left: -1 * $sideBarWidth;
- margin-right: 0;
- overflow-y: auto;
- overflow-x: hidden;
-
- height: $pageHeight;
- width: $sideBarWidth;
-
- background-color: $euiColorLightestShade;
- box-shadow: inset (-1 * $euiSizeXS) 0 $euiSizeS (-1 * $euiSizeXS) rgba($euiShadowColor, .25);
-
- @include euiBreakpoint('xs', 's', 'm') {
- position: relative;
- width: 100%;
- height: $sideBarMobileHeight;
- margin-left: 0;
- overflow-y: hidden;
-
- border-bottom: $euiBorderThin;
- box-shadow: none;
-
- &--isOpen {
- height: auto;
- overflow-y: auto;
- }
- }
- }
-
- &__body {
- padding: $euiSizeXXL;
-
- @include euiBreakpoint('m') {
- padding: $euiSizeL;
- }
- @include euiBreakpoint('xs', 's') {
- padding: $euiSize;
- }
- }
-
- &__readOnlyMode {
- margin: -$euiSizeM 0 $euiSizeL;
-
- @include euiBreakpoint('m') {
- margin: 0 0 $euiSizeL;
- }
- @include euiBreakpoint('xs', 's') {
- margin: 0;
- }
- }
-}
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx
deleted file mode 100644
index 28092f75cdede..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-
-import { shallow } from 'enzyme';
-
-import { EuiPageSideBar, EuiButton, EuiPageBody, EuiCallOut } from '@elastic/eui';
-
-import { Layout, INavContext } from './layout';
-
-describe('Layout', () => {
- it('renders', () => {
- const wrapper = shallow( );
-
- expect(wrapper.find('.enterpriseSearchLayout')).toHaveLength(1);
- expect(wrapper.find(EuiPageBody).prop('restrictWidth')).toBeFalsy();
- });
-
- it('passes the restrictWidth prop', () => {
- const wrapper = shallow( );
-
- expect(wrapper.find(EuiPageBody).prop('restrictWidth')).toEqual(true);
- });
-
- it('renders navigation', () => {
- const wrapper = shallow(Hello World} />);
-
- expect(wrapper.find('.enterpriseSearchLayout__sideBar')).toHaveLength(1);
- expect(wrapper.find('.nav-test')).toHaveLength(1);
- });
-
- it('renders navigation toggle state', () => {
- const wrapper = shallow(Hello World} />);
- expect(wrapper.find(EuiPageSideBar).prop('className')).not.toContain('--isOpen');
- expect(wrapper.find(EuiButton).prop('iconType')).toEqual('arrowRight');
-
- const toggle = wrapper.find('[data-test-subj="enterpriseSearchNavToggle"]');
- toggle.simulate('click');
-
- expect(wrapper.find(EuiPageSideBar).prop('className')).toContain('--isOpen');
- expect(wrapper.find(EuiButton).prop('iconType')).toEqual('arrowDown');
- });
-
- it('passes down NavContext to navigation links', () => {
- const wrapper = shallow( } />);
-
- const toggle = wrapper.find('[data-test-subj="enterpriseSearchNavToggle"]');
- toggle.simulate('click');
- expect(wrapper.find(EuiPageSideBar).prop('className')).toContain('--isOpen');
-
- const context = (wrapper.find('ContextProvider').prop('value') as unknown) as INavContext;
- context.closeNavigation();
- expect(wrapper.find(EuiPageSideBar).prop('className')).not.toContain('--isOpen');
- });
-
- it('renders a read-only mode callout', () => {
- const wrapper = shallow( );
-
- expect(wrapper.find(EuiCallOut)).toHaveLength(1);
- });
-
- it('renders children', () => {
- const wrapper = shallow(
-
- Test
-
- );
-
- expect(wrapper.find('.enterpriseSearchLayout__body')).toHaveLength(1);
- expect(wrapper.find('.testing')).toHaveLength(1);
- });
-});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx
deleted file mode 100644
index 9cf5fccddbd5b..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { useState } from 'react';
-
-import classNames from 'classnames';
-
-import { EuiPage, EuiPageSideBar, EuiPageBody, EuiButton, EuiCallOut } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-
-import './layout.scss';
-
-interface LayoutProps {
- navigation: React.ReactNode;
- restrictWidth?: boolean;
- readOnlyMode?: boolean;
-}
-
-export interface INavContext {
- closeNavigation(): void;
-}
-export const NavContext = React.createContext({});
-
-export const Layout: React.FC = ({
- children,
- navigation,
- restrictWidth,
- readOnlyMode,
-}) => {
- const [isNavOpen, setIsNavOpen] = useState(false);
- const toggleNavigation = () => setIsNavOpen(!isNavOpen);
- const closeNavigation = () => setIsNavOpen(false);
-
- const navClasses = classNames('enterpriseSearchLayout__sideBar', {
- 'enterpriseSearchLayout__sideBar--isOpen': isNavOpen, // eslint-disable-line @typescript-eslint/naming-convention
- });
-
- return (
-
-
-
-
- {i18n.translate('xpack.enterpriseSearch.nav.menu', {
- defaultMessage: 'Menu',
- })}
-
-
- {navigation}
-
-
- {readOnlyMode && (
-
- )}
- {children}
-
-
- );
-};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.scss b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.scss
deleted file mode 100644
index f6a1ebed7ba93..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.scss
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-$euiSizeML: $euiSize * 1.25; // 20px - between medium and large ¯\_(ツ)_/¯
-
-.enterpriseSearchProduct {
- display: flex;
- align-items: center;
- padding: $euiSizeML;
-
- background-image: url('./side_nav_bg.svg');
- background-repeat: no-repeat;
-
- @include euiBreakpoint('xs', 's', 'm') {
- padding: $euiSize $euiSizeML;
- }
-
- &__icon {
- display: flex;
- align-items: center;
- justify-content: center;
-
- width: $euiSizeXXL;
- height: $euiSizeXXL;
- margin-right: $euiSizeS;
-
- background-color: $euiColorEmptyShade;
- border-radius: 50%;
- @include euiSlightShadow();
-
- .euiIcon {
- width: $euiSizeML;
- height: $euiSizeML;
- }
- }
-
- &__title {
- .euiText {
- font-weight: $euiFontWeightMedium;
- }
- }
-}
-
-.enterpriseSearchNavLinks {
- &__item {
- display: block;
- padding: $euiSizeM $euiSizeML;
- font-size: $euiFontSizeS;
- font-weight: $euiFontWeightMedium;
- line-height: $euiFontSizeM;
-
- $activeBgColor: rgba($euiColorFullShade, .05);
-
- &--isActive {
- background-color: $activeBgColor;
- }
-
- &.euiLink {
- color: $euiTextColor;
- font-weight: $euiFontWeightMedium;
-
- &:hover {
- color: $euiTextColor;
- }
-
- &:focus {
- outline: solid 0 $activeBgColor;
- background-color: $activeBgColor;
- }
- }
- }
-
- &__subNav {
- padding-left: $euiSizeML;
-
- // Extends the click area of links more to the left, so that second tiers
- // of subnavigation links still have the same hitbox as first tier links
- .enterpriseSearchNavLinks__item {
- margin-left: -$euiSizeML;
- padding-left: $euiSizeXXL;
- }
- }
-}
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx
deleted file mode 100644
index 244037d6e1382..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { mockLocation } from '../../__mocks__/react_router';
-
-import React from 'react';
-
-import { shallow } from 'enzyme';
-
-import { EuiLink } from '@elastic/eui';
-
-import { ENTERPRISE_SEARCH_PLUGIN, APP_SEARCH_PLUGIN } from '../../../../common/constants';
-import { EuiLinkTo } from '../react_router_helpers';
-
-import { SideNav, SideNavLink, SideNavItem } from './';
-
-describe('SideNav', () => {
- it('renders link children', () => {
- const wrapper = shallow(
-
- Hello World
-
- );
-
- expect(wrapper.type()).toEqual('nav');
- expect(wrapper.find('.enterpriseSearchNavLinks')).toHaveLength(1);
- expect(wrapper.find('.testing')).toHaveLength(1);
- });
-
- it('renders a custom product', () => {
- const wrapper = shallow( );
-
- expect(wrapper.find('h3').text()).toEqual('App Search');
- expect(wrapper.find('.enterpriseSearchProduct--appSearch')).toHaveLength(1);
- });
-});
-
-describe('SideNavLink', () => {
- it('renders', () => {
- const wrapper = shallow(Link );
-
- expect(wrapper.type()).toEqual('li');
- expect(wrapper.find(EuiLinkTo)).toHaveLength(1);
- expect(wrapper.find('.enterpriseSearchNavLinks__item')).toHaveLength(1);
- });
-
- it('renders an external link if isExternal is true', () => {
- const wrapper = shallow(
-
- Link
-
- );
- const externalLink = wrapper.find(EuiLink);
-
- expect(externalLink).toHaveLength(1);
- expect(externalLink.prop('href')).toEqual('http://website.com');
- expect(externalLink.prop('target')).toEqual('_blank');
- });
-
- it('sets an active class if the current path matches the nav link', () => {
- mockLocation.pathname = '/test/';
-
- const wrapper = shallow(Link );
-
- expect(wrapper.find('.enterpriseSearchNavLinks__item--isActive')).toHaveLength(1);
- });
-
- it('sets an active class if the current path is / and the link isRoot', () => {
- mockLocation.pathname = '/';
-
- const wrapper = shallow(
-
- Link
-
- );
-
- expect(wrapper.find('.enterpriseSearchNavLinks__item--isActive')).toHaveLength(1);
- });
-
- it('passes down custom classes and props', () => {
- const wrapper = shallow(
-
- Link
-
- );
-
- expect(wrapper.find('.testing')).toHaveLength(1);
- expect(wrapper.find('[data-test-subj="testing"]')).toHaveLength(1);
- });
-
- it('renders nested subnavigation', () => {
- const subNav = (
-
- Another link!
-
- );
- const wrapper = shallow(
-
- Link
-
- );
-
- expect(wrapper.find('.enterpriseSearchNavLinks__subNav')).toHaveLength(1);
- expect(wrapper.find('[data-test-subj="subNav"]')).toHaveLength(1);
- });
-
- describe('shouldShowActiveForSubroutes', () => {
- it("won't set an active class when route is a subroute of 'to'", () => {
- mockLocation.pathname = '/documents/1234';
-
- const wrapper = shallow(
-
- Link
-
- );
-
- expect(wrapper.find('.enterpriseSearchNavLinks__item--isActive')).toHaveLength(0);
- });
-
- it('sets an active class if the current path is a subRoute of "to", and shouldShowActiveForSubroutes is true', () => {
- mockLocation.pathname = '/documents/1234';
-
- const wrapper = shallow(
-
- Link
-
- );
-
- expect(wrapper.find('.enterpriseSearchNavLinks__item--isActive')).toHaveLength(1);
- });
- });
-});
-
-describe('SideNavItem', () => {
- it('renders', () => {
- const wrapper = shallow(Test );
-
- expect(wrapper.type()).toEqual('li');
- expect(wrapper.find('.enterpriseSearchNavLinks__item')).toHaveLength(1);
- });
-
- it('renders children', () => {
- const wrapper = shallow(
-
- World
-
- );
-
- expect(wrapper.find('[data-test-subj="hello"]').text()).toEqual('World');
- });
-
- it('passes down custom classes and props', () => {
- const wrapper = shallow(
-
- Test
-
- );
-
- expect(wrapper.find('.testing')).toHaveLength(1);
- expect(wrapper.find('[data-test-subj="testing"]')).toHaveLength(1);
- });
-});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx
deleted file mode 100644
index 58a5c7bbb229f..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { useContext } from 'react';
-import { useLocation } from 'react-router-dom';
-
-import classNames from 'classnames';
-
-import { EuiIcon, EuiTitle, EuiText, EuiLink } from '@elastic/eui'; // TODO: Remove EuiLink after full Kibana transition
-import { i18n } from '@kbn/i18n';
-
-import { ENTERPRISE_SEARCH_PLUGIN } from '../../../../common/constants';
-import { stripTrailingSlash } from '../../../../common/strip_slashes';
-import { EuiLinkTo } from '../react_router_helpers';
-
-import { NavContext, INavContext } from './layout';
-
-import './side_nav.scss';
-
-/**
- * Side navigation - product & icon + links wrapper
- */
-
-interface SideNavProps {
- // Expects product plugin constants (@see common/constants.ts)
- product: {
- NAME: string;
- ID: string;
- };
-}
-
-export const SideNav: React.FC = ({ product, children }) => {
- return (
-
-
-
-
-
-
-
- {ENTERPRISE_SEARCH_PLUGIN.NAME}
-
-
- {product.NAME}
-
-
-
-
-
- );
-};
-
-/**
- * Side navigation link item
- */
-
-interface SideNavLinkProps {
- to: string;
- shouldShowActiveForSubroutes?: boolean;
- isExternal?: boolean;
- className?: string;
- isRoot?: boolean;
- subNav?: React.ReactNode;
-}
-
-export const SideNavLink: React.FC = ({
- to,
- shouldShowActiveForSubroutes = false,
- isExternal,
- children,
- className,
- isRoot,
- subNav,
- ...rest
-}) => {
- const { closeNavigation } = useContext(NavContext) as INavContext;
-
- const { pathname } = useLocation();
- const currentPath = stripTrailingSlash(pathname);
- const isActive =
- currentPath === to ||
- (shouldShowActiveForSubroutes && currentPath.startsWith(to)) ||
- (isRoot && currentPath === '');
-
- const classes = classNames('enterpriseSearchNavLinks__item', className, {
- 'enterpriseSearchNavLinks__item--isActive': !isExternal && isActive, // eslint-disable-line @typescript-eslint/naming-convention
- });
-
- return (
-
- {isExternal ? (
- // eslint-disable-next-line @elastic/eui/href-or-on-click
-
- {children}
-
- ) : (
-
- {children}
-
- )}
- {subNav && }
-
- );
-};
-
-/**
- * Side navigation non-link item
- */
-
-interface SideNavItemProps {
- className?: string;
-}
-
-export const SideNavItem: React.FC = ({ children, className, ...rest }) => {
- const classes = classNames('enterpriseSearchNavLinks__item', className);
- return (
-
- {children}
-
- );
-};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav_bg.svg b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav_bg.svg
deleted file mode 100644
index a19227ab7b7eb..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav_bg.svg
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/app_search_logo.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/app_search_logo.tsx
deleted file mode 100644
index 8eb2059afd3ed..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/app_search_logo.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-
-export const AppSearchLogo: React.FC = () => (
-
-
-
-
-
-);
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/workplace_search_logo.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/workplace_search_logo.tsx
deleted file mode 100644
index df5b1a1118c41..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/workplace_search_logo.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-
-export const WorkplaceSearchLogo: React.FC = () => (
-
-
-
-
-
-);
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/index.ts
index 482c1a58faa9c..8be374d549952 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/index.ts
@@ -5,4 +5,4 @@
* 2.0.
*/
-export { NotFound } from './not_found';
+export { NotFoundPrompt } from './not_found_prompt';
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx
deleted file mode 100644
index 1561224a26e42..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { setMockValues } from '../../__mocks__/kea_logic';
-
-import React from 'react';
-
-import { shallow } from 'enzyme';
-
-import { EuiButton as EuiButtonExternal, EuiEmptyPrompt } from '@elastic/eui';
-
-import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../../common/constants';
-import { SetAppSearchChrome } from '../kibana_chrome';
-
-import { AppSearchLogo } from './assets/app_search_logo';
-import { WorkplaceSearchLogo } from './assets/workplace_search_logo';
-
-import { NotFound } from './';
-
-describe('NotFound', () => {
- it('renders an App Search 404 view', () => {
- const wrapper = shallow( );
- const prompt = wrapper.find(EuiEmptyPrompt).dive().shallow();
-
- expect(prompt.find('h2').text()).toEqual('404 error');
- expect(prompt.find(EuiButtonExternal).prop('href')).toEqual(APP_SEARCH_PLUGIN.SUPPORT_URL);
-
- const logo = prompt.find(AppSearchLogo).dive().shallow();
- expect(logo.type()).toEqual('svg');
- });
-
- it('renders a Workplace Search 404 view', () => {
- const wrapper = shallow( );
- const prompt = wrapper.find(EuiEmptyPrompt).dive().shallow();
-
- expect(prompt.find('h2').text()).toEqual('404 error');
- expect(prompt.find(EuiButtonExternal).prop('href')).toEqual(
- WORKPLACE_SEARCH_PLUGIN.SUPPORT_URL
- );
-
- const logo = prompt.find(WorkplaceSearchLogo).dive().shallow();
- expect(logo.type()).toEqual('svg');
- });
-
- it('changes the support URL if the user has a gold+ license', () => {
- setMockValues({ hasGoldLicense: true });
- const wrapper = shallow( );
- const prompt = wrapper.find(EuiEmptyPrompt).dive().shallow();
-
- expect(prompt.find(EuiButtonExternal).prop('href')).toEqual('https://support.elastic.co');
- });
-
- it('passes down optional custom breadcrumbs', () => {
- const wrapper = shallow(
-
- );
-
- expect(wrapper.find(SetAppSearchChrome).prop('trail')).toEqual(['Hello', 'World']);
- });
-
- it('does not render anything without a valid product', () => {
- const wrapper = shallow( );
-
- expect(wrapper.isEmptyRender()).toBe(true);
- });
-});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx
deleted file mode 100644
index f288961b72de4..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-
-import { useValues } from 'kea';
-
-import {
- EuiPageContent,
- EuiEmptyPrompt,
- EuiTitle,
- EuiFlexGroup,
- EuiFlexItem,
- EuiButton,
-} from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-
-import {
- APP_SEARCH_PLUGIN,
- WORKPLACE_SEARCH_PLUGIN,
- LICENSED_SUPPORT_URL,
-} from '../../../../common/constants';
-
-import { SetAppSearchChrome, SetWorkplaceSearchChrome } from '../kibana_chrome';
-import { BreadcrumbTrail } from '../kibana_chrome/generate_breadcrumbs';
-import { LicensingLogic } from '../licensing';
-import { EuiButtonTo } from '../react_router_helpers';
-import { SendAppSearchTelemetry, SendWorkplaceSearchTelemetry } from '../telemetry';
-
-import { AppSearchLogo } from './assets/app_search_logo';
-import { WorkplaceSearchLogo } from './assets/workplace_search_logo';
-import './assets/logo.scss';
-
-interface NotFoundProps {
- // Expects product plugin constants (@see common/constants.ts)
- product: {
- ID: string;
- SUPPORT_URL: string;
- };
- // Optional breadcrumbs
- breadcrumbs?: BreadcrumbTrail;
-}
-
-export const NotFound: React.FC = ({ product = {}, breadcrumbs }) => {
- const { hasGoldLicense } = useValues(LicensingLogic);
- const supportUrl = hasGoldLicense ? LICENSED_SUPPORT_URL : product.SUPPORT_URL;
-
- let Logo;
- let SetPageChrome;
- let SendTelemetry;
-
- switch (product.ID) {
- case APP_SEARCH_PLUGIN.ID:
- Logo = AppSearchLogo;
- SetPageChrome = SetAppSearchChrome;
- SendTelemetry = SendAppSearchTelemetry;
- break;
- case WORKPLACE_SEARCH_PLUGIN.ID:
- Logo = WorkplaceSearchLogo;
- SetPageChrome = SetWorkplaceSearchChrome;
- SendTelemetry = SendWorkplaceSearchTelemetry;
- break;
- default:
- return null;
- }
-
- return (
- <>
-
-
-
-
- }
- body={
- <>
-
-
- {i18n.translate('xpack.enterpriseSearch.notFound.title', {
- defaultMessage: '404 error',
- })}
-
-
-
- {i18n.translate('xpack.enterpriseSearch.notFound.description', {
- defaultMessage: 'The page you’re looking for was not found.',
- })}
-
- >
- }
- actions={
-
-
-
- {i18n.translate('xpack.enterpriseSearch.notFound.action1', {
- defaultMessage: 'Back to your dashboard',
- })}
-
-
-
-
- {i18n.translate('xpack.enterpriseSearch.notFound.action2', {
- defaultMessage: 'Contact support',
- })}
-
-
-
- }
- />
-
- >
- );
-};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found_prompt.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found_prompt.test.tsx
new file mode 100644
index 0000000000000..c21aeff2780b6
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found_prompt.test.tsx
@@ -0,0 +1,51 @@
+/*
+ * 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 { setMockValues } from '../../__mocks__/kea_logic';
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { EuiEmptyPrompt, EuiButton } from '@elastic/eui';
+
+import { EuiButtonTo } from '../react_router_helpers';
+
+import { NotFoundPrompt } from './';
+
+describe('NotFoundPrompt', () => {
+ const subject = (props?: object) =>
+ shallow( )
+ .find(EuiEmptyPrompt)
+ .dive();
+
+ it('renders', () => {
+ const wrapper = subject({
+ productSupportUrl: 'https://discuss.elastic.co/c/enterprise-search/app-search/',
+ });
+
+ expect(wrapper.find('h1').text()).toEqual('404 error');
+ expect(wrapper.find(EuiButtonTo).prop('to')).toEqual('/');
+ expect(wrapper.find(EuiButton).prop('href')).toContain('https://discuss.elastic.co');
+ });
+
+ it('renders with a custom "Back to dashboard" link if passed', () => {
+ const wrapper = subject({
+ productSupportUrl: 'https://discuss.elastic.co/c/enterprise-search/workplace-search/',
+ backToLink: '/workplace_search/p/sources',
+ });
+
+ expect(wrapper.find(EuiButtonTo).prop('to')).toEqual('/workplace_search/p/sources');
+ });
+
+ it('renders with a link to our licensed support URL for gold+ licenses', () => {
+ setMockValues({ hasGoldLicense: true });
+ const wrapper = subject();
+
+ expect(wrapper.find(EuiButton).prop('href')).toEqual('https://support.elastic.co');
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found_prompt.tsx
new file mode 100644
index 0000000000000..97debd21ec16c
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found_prompt.tsx
@@ -0,0 +1,65 @@
+/*
+ * 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 React from 'react';
+
+import { useValues } from 'kea';
+
+import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { LICENSED_SUPPORT_URL } from '../../../../common/constants';
+import { LicensingLogic } from '../licensing';
+import { EuiButtonTo } from '../react_router_helpers';
+
+interface Props {
+ productSupportUrl: string;
+ backToLink?: string;
+}
+
+export const NotFoundPrompt: React.FC = ({ productSupportUrl, backToLink = '/' }) => {
+ const { hasGoldLicense } = useValues(LicensingLogic);
+ const supportUrl = hasGoldLicense ? LICENSED_SUPPORT_URL : productSupportUrl;
+
+ return (
+
+ {i18n.translate('xpack.enterpriseSearch.notFound.title', {
+ defaultMessage: '404 error',
+ })}
+
+ }
+ body={
+
+ {i18n.translate('xpack.enterpriseSearch.notFound.description', {
+ defaultMessage: 'The page you’re looking for was not found.',
+ })}
+
+ }
+ actions={
+
+
+
+ {i18n.translate('xpack.enterpriseSearch.notFound.action1', {
+ defaultMessage: 'Back to your dashboard',
+ })}
+
+
+
+
+ {i18n.translate('xpack.enterpriseSearch.notFound.action2', {
+ defaultMessage: 'Contact support',
+ })}
+
+
+
+ }
+ />
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/elasticsearch_users.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/elasticsearch_users.ts
index 500f560675679..6d9365d63c320 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/elasticsearch_users.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/elasticsearch_users.ts
@@ -9,5 +9,6 @@ export const elasticsearchUsers = [
{
email: 'user1@user.com',
username: 'user1',
+ enabled: true,
},
];
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx
index d19d090b6e706..8fed459b0a8dc 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx
@@ -9,12 +9,12 @@ import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
-import { EuiComboBox, EuiFieldText } from '@elastic/eui';
+import { EuiComboBox, EuiFieldText, EuiFormRow } from '@elastic/eui';
import { AttributeName } from '../types';
import { AttributeSelector } from './attribute_selector';
-import { ANY_AUTH_PROVIDER, ANY_AUTH_PROVIDER_OPTION_LABEL } from './constants';
+import { ANY_AUTH_PROVIDER, ANY_AUTH_PROVIDER_OPTION_LABEL, REQUIRED_LABEL } from './constants';
const handleAttributeSelectorChange = jest.fn();
const handleAttributeValueChange = jest.fn();
@@ -166,5 +166,12 @@ describe('AttributeSelector', () => {
baseProps.elasticsearchRoles[0]
);
});
+
+ it('shows helpText when attributeValueInvalid', () => {
+ const wrapper = shallow( );
+ const formRow = wrapper.find(EuiFormRow).at(2);
+
+ expect(formRow.prop('helpText')).toEqual(REQUIRED_LABEL);
+ });
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx
index bedb6b3df90f2..e24bc03bea452 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx
@@ -21,7 +21,7 @@ import {
ANY_AUTH_PROVIDER,
ANY_AUTH_PROVIDER_OPTION_LABEL,
ATTRIBUTE_VALUE_LABEL,
- ATTRIBUTE_VALUE_ERROR,
+ REQUIRED_LABEL,
AUTH_ANY_PROVIDER_LABEL,
AUTH_INDIVIDUAL_PROVIDER_LABEL,
AUTH_PROVIDER_LABEL,
@@ -129,8 +129,7 @@ export const AttributeSelector: React.FC = ({
{attributeName === 'role' ? (
{
+ it('renders with new', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper).toMatchInlineSnapshot(`
+
+
+
+
+
+ User deactivated
+
+
+
+
+ This user is not currently active, and access has been temporarily revoked. Users can be re-activated via the User Management area of the Kibana console.
+
+
+
+ `);
+ });
+
+ it('renders with existing', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper).toMatchInlineSnapshot(`
+
+
+
+
+
+
+ User deactivated
+
+
+
+
+ This user is not currently active, and access has been temporarily revoked. Users can be re-activated via the User Management area of the Kibana console.
+
+
+
+ `);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/deactivated_user_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/deactivated_user_callout.tsx
new file mode 100644
index 0000000000000..5b69420d169ce
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/deactivated_user_callout.tsx
@@ -0,0 +1,28 @@
+/*
+ * 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 React from 'react';
+
+import { EuiSpacer, EuiText, EuiIcon } from '@elastic/eui';
+
+interface Props {
+ isNew: boolean;
+}
+
+import { DEACTIVATED_USER_CALLOUT_LABEL, DEACTIVATED_USER_CALLOUT_DESCRIPTION } from './constants';
+
+export const DeactivatedUserCallout: React.FC = ({ isNew }) => (
+ <>
+ {!isNew && }
+
+ {DEACTIVATED_USER_CALLOUT_LABEL}
+
+
+ {DEACTIVATED_USER_CALLOUT_DESCRIPTION}
+
+ >
+);
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts
index 8096b86939ff3..c10fc5c9d8242 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts
@@ -5,7 +5,9 @@
* 2.0.
*/
+export * from './types';
export { AttributeSelector } from './attribute_selector';
+export { DeactivatedUserCallout } from './deactivated_user_callout';
export { RolesEmptyPrompt } from './roles_empty_prompt';
export { RoleMappingsTable } from './role_mappings_table';
export { RoleOptionLabel } from './role_option_label';
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx
index c0973bb2c9504..651a46f5df85e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx
@@ -20,13 +20,14 @@ import {
import { RoleMappingFlyout } from './role_mapping_flyout';
describe('RoleMappingFlyout', () => {
- const closeRoleMappingFlyout = jest.fn();
+ const closeUsersAndRolesFlyout = jest.fn();
const handleSaveMapping = jest.fn();
const props = {
isNew: true,
disabled: false,
- closeRoleMappingFlyout,
+ formLoading: false,
+ closeUsersAndRolesFlyout,
handleSaveMapping,
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx
index bae991fef3655..bcaf26ccf2cfc 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx
@@ -36,7 +36,8 @@ interface Props {
children: React.ReactNode;
isNew: boolean;
disabled: boolean;
- closeRoleMappingFlyout(): void;
+ formLoading: boolean;
+ closeUsersAndRolesFlyout(): void;
handleSaveMapping(): void;
}
@@ -44,13 +45,14 @@ export const RoleMappingFlyout: React.FC = ({
children,
isNew,
disabled,
- closeRoleMappingFlyout,
+ formLoading,
+ closeUsersAndRolesFlyout,
handleSaveMapping,
}) => (
@@ -71,11 +73,14 @@ export const RoleMappingFlyout: React.FC = ({
- {CANCEL_BUTTON_LABEL}
+
+ {CANCEL_BUTTON_LABEL}
+
{
});
it('renders auth provider display names', () => {
- const wrapper = mount( );
+ const roleMappingWithAuths = {
+ ...wsRoleMapping,
+ authProvider: ['saml', 'native'],
+ };
+ const wrapper = mount( );
- expect(wrapper.find('[data-test-subj="AuthProviderDisplayValue"]').prop('children')).toEqual(
- `${ANY_AUTH_PROVIDER_OPTION_LABEL}, other_auth`
- );
+ expect(wrapper.find('[data-test-subj="ProviderSpecificList"]')).toHaveLength(1);
});
it('handles manage click', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx
index eb9621c7a242c..4136d114d3420 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx
@@ -7,14 +7,17 @@
import React from 'react';
-import { EuiIconTip, EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui';
+import { EuiIconTip, EuiInMemoryTable, EuiBasicTableColumn, EuiLink } from '@elastic/eui';
import { ASRoleMapping } from '../../app_search/types';
import { WSRoleMapping } from '../../workplace_search/types';
+import { docLinks } from '../doc_links';
import { RoleRules } from '../types';
import './role_mappings_table.scss';
+const AUTH_PROVIDER_DOCUMENTATION_URL = `${docLinks.enterpriseSearchBase}/users-access.html`;
+
import {
ANY_AUTH_PROVIDER,
ANY_AUTH_PROVIDER_OPTION_LABEL,
@@ -25,6 +28,8 @@ import {
ATTRIBUTE_VALUE_LABEL,
FILTER_ROLE_MAPPINGS_PLACEHOLDER,
ROLE_MAPPINGS_NO_RESULTS_MESSAGE,
+ EXTERNAL_ATTRIBUTE_TOOLTIP,
+ AUTH_PROVIDER_TOOLTIP,
} from './constants';
import { UsersAndRolesRowActions } from './users_and_roles_row_actions';
@@ -46,9 +51,6 @@ interface Props {
handleDeleteMapping(roleMappingId: string): void;
}
-const getAuthProviderDisplayValue = (authProvider: string) =>
- authProvider === ANY_AUTH_PROVIDER ? ANY_AUTH_PROVIDER_OPTION_LABEL : authProvider;
-
export const RoleMappingsTable: React.FC = ({
accessItemKey,
accessHeader,
@@ -69,7 +71,19 @@ export const RoleMappingsTable: React.FC = ({
const attributeNameCol: EuiBasicTableColumn = {
field: 'attribute',
- name: EXTERNAL_ATTRIBUTE_LABEL,
+ name: (
+
+ {EXTERNAL_ATTRIBUTE_LABEL}{' '}
+
+
+ ),
render: (_, { rules }: SharedRoleMapping) => getFirstAttributeName(rules),
};
@@ -105,11 +119,19 @@ export const RoleMappingsTable: React.FC = ({
const authProviderCol: EuiBasicTableColumn = {
field: 'authProvider',
name: AUTH_PROVIDER_LABEL,
- render: (_, { authProvider }: SharedRoleMapping) => (
-
- {authProvider.map(getAuthProviderDisplayValue).join(', ')}
-
- ),
+ render: (_, { authProvider }: SharedRoleMapping) => {
+ if (authProvider[0] === ANY_AUTH_PROVIDER) {
+ return ANY_AUTH_PROVIDER_OPTION_LABEL;
+ }
+ return (
+
+ {authProvider.join(', ')}{' '}
+
+
+
+
+ );
+ },
};
const actionsCol: EuiBasicTableColumn = {
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/types.ts
new file mode 100644
index 0000000000000..2eba171c270f6
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/types.ts
@@ -0,0 +1,66 @@
+/*
+ * 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 { EuiComboBoxOptionOption } from '@elastic/eui';
+
+import { AttributeName, ElasticsearchUser } from '../../shared/types';
+
+export interface RoleMappingsBaseServerDetails {
+ attributes: string[];
+ authProviders: string[];
+ elasticsearchRoles: string[];
+ elasticsearchUsers: ElasticsearchUser[];
+ multipleAuthProvidersConfig: boolean;
+ smtpSettingsPresent: boolean;
+}
+
+export interface RoleMappingsBaseActions {
+ handleAuthProviderChange(value: string[]): { value: string[] };
+ handleAttributeSelectorChange(
+ value: AttributeName,
+ firstElasticsearchRole: string
+ ): { value: AttributeName; firstElasticsearchRole: string };
+ handleAttributeValueChange(value: string): { value: string };
+ handleDeleteMapping(roleMappingId: string): { roleMappingId: string };
+ handleUsernameSelectChange(username: string): { username: string };
+ handleSaveMapping(): void;
+ handleSaveUser(): void;
+ initializeRoleMapping(roleMappingId?: string): { roleMappingId?: string };
+ initializeSingleUserRoleMapping(roleMappingId?: string): { roleMappingId?: string };
+ initializeRoleMappings(): void;
+ resetState(): void;
+ setElasticsearchUser(
+ elasticsearchUser?: ElasticsearchUser
+ ): { elasticsearchUser: ElasticsearchUser };
+ openRoleMappingFlyout(): void;
+ openSingleUserRoleMappingFlyout(): void;
+ closeUsersAndRolesFlyout(): void;
+ setRoleMappingErrors(errors: string[]): { errors: string[] };
+ enableRoleBasedAccess(): void;
+ setUserExistingRadioValue(userFormUserIsExisting: boolean): { userFormUserIsExisting: boolean };
+ setElasticsearchUsernameValue(username: string): { username: string };
+ setElasticsearchEmailValue(email: string): { email: string };
+ setUserCreated(): void;
+ setUserFormIsNewUser(userFormIsNewUser: boolean): { userFormIsNewUser: boolean };
+}
+
+export interface RoleMappingsBaseValues extends RoleMappingsBaseServerDetails {
+ attributeName: AttributeName;
+ attributeValue: string;
+ availableAuthProviders: string[];
+ dataLoading: boolean;
+ elasticsearchUser: ElasticsearchUser;
+ roleMappingFlyoutOpen: boolean;
+ singleUserRoleMappingFlyoutOpen: boolean;
+ selectedOptions: EuiComboBoxOptionOption[];
+ roleMappingErrors: string[];
+ userFormUserIsExisting: boolean;
+ userCreated: boolean;
+ userFormIsNewUser: boolean;
+ accessAllEngines: boolean;
+ formLoading: boolean;
+}
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.test.tsx
index 30bdaa0010b58..57200b389591d 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.test.tsx
@@ -9,8 +9,6 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { EuiText } from '@elastic/eui';
-
import { UserAddedInfo } from './';
describe('UserAddedInfo', () => {
@@ -20,9 +18,103 @@ describe('UserAddedInfo', () => {
roleType: 'user',
};
- it('renders', () => {
+ it('renders with email', () => {
const wrapper = shallow( );
- expect(wrapper.find(EuiText)).toHaveLength(6);
+ expect(wrapper).toMatchInlineSnapshot(`
+
+
+
+ Username
+
+
+
+ user1
+
+
+
+
+ Email
+
+
+
+ test@test.com
+
+
+
+
+ Role
+
+
+
+ user
+
+
+
+ `);
+ });
+
+ it('renders without email', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper).toMatchInlineSnapshot(`
+
+
+
+ Username
+
+
+
+ user1
+
+
+
+
+ Email
+
+
+
+
+ —
+
+
+
+
+
+ Role
+
+
+
+ user
+
+
+
+ `);
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.tsx
index a12eae66262a0..37804414a94a9 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.tsx
@@ -7,7 +7,7 @@
import React from 'react';
-import { EuiSpacer, EuiText } from '@elastic/eui';
+import { EuiSpacer, EuiText, EuiTextColor } from '@elastic/eui';
import { USERNAME_LABEL, EMAIL_LABEL } from '../constants';
@@ -19,6 +19,8 @@ interface Props {
roleType: string;
}
+const noItemsPlaceholder = — ;
+
export const UserAddedInfo: React.FC = ({ username, email, roleType }) => (
<>
@@ -29,7 +31,7 @@ export const UserAddedInfo: React.FC = ({ username, email, roleType }) =>