+
+ {i18n.translate('visualizations.missedDataView.dataViewReconfigure', {
+ defaultMessage: `Recreate it in the data view management page`,
+ })}
+
+
+ ) : null
+ }
+ body={
+ <>
+
+ {i18n.translate('visualizations.missedDataView.errorMessage', {
+ defaultMessage: `Could not find the data view: {id}`,
+ values: {
+ id: error.savedObjectId,
+ },
+ })}
+
+ {viewMode === 'edit' && renderMode !== 'edit' && isEditVisEnabled ? (
+
+ {i18n.translate('visualizations.missedDataView.editInVisualizeEditor', {
+ defaultMessage: `Edit in Visualize editor to fix the error`,
+ })}
+
+ ) : null}
+ >
+ }
+ />
+ );
+};
diff --git a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts
index 744537b534f95..8fb107f827cd6 100644
--- a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts
+++ b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts
@@ -50,7 +50,11 @@ export const createVisEmbeddableFromObject =
let indexPatterns: DataView[] = [];
if (vis.type.getUsedIndexPattern) {
- indexPatterns = await vis.type.getUsedIndexPattern(vis.params);
+ try {
+ indexPatterns = await vis.type.getUsedIndexPattern(vis.params);
+ } catch (e) {
+ // nothing to be here
+ }
} else if (vis.data.indexPattern) {
indexPatterns = [vis.data.indexPattern];
}
diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx
index 1ef9eb1153d9e..f3eaa57c74505 100644
--- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx
+++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx
@@ -13,34 +13,45 @@ import React from 'react';
import { render } from 'react-dom';
import { EuiLoadingChart } from '@elastic/eui';
import { Filter, onlyDisabledFiltersChanged } from '@kbn/es-query';
-import type { SavedObjectAttributes, KibanaExecutionContext } from '@kbn/core/public';
+import type { KibanaExecutionContext, SavedObjectAttributes } from '@kbn/core/public';
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
-import { TimeRange, Query, TimefilterContract } from '@kbn/data-plugin/public';
+import { Query, TimefilterContract, TimeRange } from '@kbn/data-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import {
+ Adapters,
+ AttributeService,
+ Embeddable,
EmbeddableInput,
EmbeddableOutput,
- Embeddable,
IContainer,
- Adapters,
- SavedObjectEmbeddableInput,
ReferenceOrValueEmbeddable,
- AttributeService,
+ SavedObjectEmbeddableInput,
+ ViewMode,
} from '@kbn/embeddable-plugin/public';
import {
- IExpressionLoaderParams,
+ ExpressionAstExpression,
ExpressionLoader,
ExpressionRenderError,
- ExpressionAstExpression,
+ IExpressionLoaderParams,
} from '@kbn/expressions-plugin/public';
import type { RenderMode } from '@kbn/expressions-plugin';
+import { VisualizationMissedDataViewError } from '../components/visualization_missed_data_view_error';
import VisualizationError from '../components/visualization_error';
import { VISUALIZE_EMBEDDABLE_TYPE } from './constants';
-import { Vis, SerializedVis } from '../vis';
-import { getExecutionContext, getExpressions, getTheme, getUiActions } from '../services';
+import { SerializedVis, Vis } from '../vis';
+import {
+ getApplication,
+ getExecutionContext,
+ getExpressions,
+ getTheme,
+ getUiActions,
+} from '../services';
import { VIS_EVENT_TO_TRIGGER } from './events';
import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory';
-import { getSavedVisualization } from '../utils/saved_visualize_utils';
+import {
+ getSavedVisualization,
+ shouldShowMissedDataViewError,
+} from '../utils/saved_visualize_utils';
import { VisSavedObject } from '../types';
import { toExpressionAst } from './to_ast';
@@ -383,9 +394,23 @@ export class VisualizeEmbeddable
this.subscriptions.push(this.handler.render$.subscribe(this.onContainerRender));
this.subscriptions.push(
- this.getOutput$().subscribe(
- ({ error }) => error && render(, this.domNode)
- )
+ this.getOutput$().subscribe(({ error }) => {
+ if (error) {
+ if (error.original && shouldShowMissedDataViewError(error.original)) {
+ render(
+ ,
+ this.domNode
+ );
+ } else {
+ render(, this.domNode);
+ }
+ }
+ })
);
await this.updateHandler();
@@ -443,11 +468,16 @@ export class VisualizeEmbeddable
}
this.abortController = new AbortController();
const abortController = this.abortController;
- this.expression = await toExpressionAst(this.vis, {
- timefilter: this.timefilter,
- timeRange: this.timeRange,
- abortSignal: this.abortController!.signal,
- });
+
+ try {
+ this.expression = await toExpressionAst(this.vis, {
+ timefilter: this.timefilter,
+ timeRange: this.timeRange,
+ abortSignal: this.abortController!.signal,
+ });
+ } catch (e) {
+ this.onContainerError(e);
+ }
if (this.handler && !abortController.signal.aborted) {
await this.handler.update(this.expression, expressionParams);
diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts
index 96f4d48f12837..f8975b705caf6 100644
--- a/src/plugins/visualizations/public/mocks.ts
+++ b/src/plugins/visualizations/public/mocks.ts
@@ -23,6 +23,7 @@ import { navigationPluginMock } from '@kbn/navigation-plugin/public/mocks';
import { presentationUtilPluginMock } from '@kbn/presentation-util-plugin/public/mocks';
import { savedObjectTaggingOssPluginMock } from '@kbn/saved-objects-tagging-oss-plugin/public/mocks';
import { screenshotModePluginMock } from '@kbn/screenshot-mode-plugin/public/mocks';
+import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks';
import { VisualizationsPlugin } from './plugin';
import { Schemas } from './vis_types';
import { Schema, VisualizationsSetup, VisualizationsStart } from '.';
@@ -55,6 +56,7 @@ const createInstance = async () => {
urlForwarding: urlForwardingPluginMock.createSetupContract(),
uiActions: uiActionsPluginMock.createSetupContract(),
});
+
const doStart = () =>
plugin.start(coreMock.createStart(), {
data: dataPluginMock.createStartContract(),
@@ -74,6 +76,7 @@ const createInstance = async () => {
presentationUtil: presentationUtilPluginMock.createStartContract(coreMock.createStart()),
urlForwarding: urlForwardingPluginMock.createStartContract(),
screenshotMode: screenshotModePluginMock.createStartContract(),
+ fieldFormats: fieldFormatsServiceMock.createStartContract(),
});
return {
diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts
index 40c408605b7b8..7c704a8916af8 100644
--- a/src/plugins/visualizations/public/plugin.ts
+++ b/src/plugins/visualizations/public/plugin.ts
@@ -36,6 +36,7 @@ import type {
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import type { UiActionsStart, UiActionsSetup } from '@kbn/ui-actions-plugin/public';
import type { SavedObjectsStart } from '@kbn/saved-objects-plugin/public';
+import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import type {
Setup as InspectorSetup,
Start as InspectorStart,
@@ -89,6 +90,7 @@ import {
setSpaces,
setTheme,
setExecutionContext,
+ setFieldFormats,
} from './services';
import { VisualizeConstants } from '../common/constants';
@@ -136,6 +138,7 @@ export interface VisualizationsStartDeps {
urlForwarding: UrlForwardingStart;
usageCollection?: UsageCollectionStart;
screenshotMode: ScreenshotModePluginStart;
+ fieldFormats: FieldFormatsStart;
}
/**
@@ -359,6 +362,7 @@ export class VisualizationsPlugin
spaces,
savedObjectsTaggingOss,
usageCollection,
+ fieldFormats,
}: VisualizationsStartDeps
): VisualizationsStart {
const types = this.types.start();
@@ -377,6 +381,7 @@ export class VisualizationsPlugin
setOverlays(core.overlays);
setExecutionContext(core.executionContext);
setChrome(core.chrome);
+ setFieldFormats(fieldFormats);
if (spaces) {
setSpaces(spaces);
diff --git a/src/plugins/visualizations/public/services.ts b/src/plugins/visualizations/public/services.ts
index af19c9302b4f5..df2ed019c308b 100644
--- a/src/plugins/visualizations/public/services.ts
+++ b/src/plugins/visualizations/public/services.ts
@@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
+import { createGetterSetter } from '@kbn/kibana-utils-plugin/public';
import type {
ApplicationStart,
Capabilities,
@@ -18,12 +19,12 @@ import type {
ThemeServiceStart,
ExecutionContextSetup,
} from '@kbn/core/public';
-import { createGetterSetter } from '@kbn/kibana-utils-plugin/public';
-import { DataPublicPluginStart, TimefilterContract } from '@kbn/data-plugin/public';
-import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
-import { ExpressionsStart } from '@kbn/expressions-plugin/public';
-import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
-import { EmbeddableStart } from '@kbn/embeddable-plugin/public';
+import type { DataPublicPluginStart, TimefilterContract } from '@kbn/data-plugin/public';
+import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
+import type { ExpressionsStart } from '@kbn/expressions-plugin/public';
+import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
+import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
+import type { EmbeddableStart } from '@kbn/embeddable-plugin/public';
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import type { TypesStart } from './vis_types';
@@ -35,6 +36,9 @@ export const [getCapabilities, setCapabilities] = createGetterSetter('Http');
+export const [getFieldsFormats, setFieldFormats] =
+ createGetterSetter('Field Formats');
+
export const [getApplication, setApplication] = createGetterSetter('Application');
export const [getEmbeddable, setEmbeddable] = createGetterSetter('Embeddable');
diff --git a/src/plugins/visualizations/public/utils/saved_visualize_utils.ts b/src/plugins/visualizations/public/utils/saved_visualize_utils.ts
index f5444b6269e22..df7565827dd9f 100644
--- a/src/plugins/visualizations/public/utils/saved_visualize_utils.ts
+++ b/src/plugins/visualizations/public/utils/saved_visualize_utils.ts
@@ -299,9 +299,6 @@ export async function getSavedVisualization(
}
savedObject.visState = await updateOldState(savedObject.visState);
- if (savedObject.searchSourceFields?.index) {
- await services.dataViews.get(savedObject.searchSourceFields.index as any);
- }
return savedObject;
}
@@ -404,3 +401,6 @@ export async function saveVisualization(
return Promise.reject(err);
}
}
+
+export const shouldShowMissedDataViewError = (error: Error): error is SavedObjectNotFound =>
+ error instanceof SavedObjectNotFound && error.savedObjectType === 'data view';
diff --git a/src/plugins/visualizations/public/vis.ts b/src/plugins/visualizations/public/vis.ts
index 9614e5f415fe7..228a410739b75 100644
--- a/src/plugins/visualizations/public/vis.ts
+++ b/src/plugins/visualizations/public/vis.ts
@@ -21,10 +21,17 @@ import { Assign } from '@kbn/utility-types';
import { i18n } from '@kbn/i18n';
import { IAggConfigs, ISearchSource, AggConfigSerialized } from '@kbn/data-plugin/public';
-import type { DataView } from '@kbn/data-views-plugin/public';
+import { DataView } from '@kbn/data-views-plugin/public';
import { getSavedSearch, throwErrorOnSavedSearchUrlConflict } from '@kbn/discover-plugin/public';
import { PersistedState } from './persisted_state';
-import { getTypes, getAggs, getSearch, getSavedObjects, getSpaces } from './services';
+import {
+ getTypes,
+ getAggs,
+ getSearch,
+ getSavedObjects,
+ getSpaces,
+ getFieldsFormats,
+} from './services';
import { BaseVisType } from './vis_types';
import { SerializedVis, SerializedVisData, VisParams } from '../common/types';
@@ -120,9 +127,14 @@ export class Vis {
if (state.params || typeChanged) {
this.params = this.getParams(state.params);
}
- if (state.data && state.data.searchSource) {
- this.data.searchSource = await getSearch().searchSource.create(state.data.searchSource!);
- this.data.indexPattern = this.data.searchSource.getField('index');
+
+ try {
+ if (state.data && state.data.searchSource) {
+ this.data.searchSource = await getSearch().searchSource.create(state.data.searchSource!);
+ this.data.indexPattern = this.data.searchSource.getField('index');
+ }
+ } catch (e) {
+ // nothing to be here
}
if (state.data && state.data.savedSearchId) {
this.data.savedSearchId = state.data.savedSearchId;
@@ -137,19 +149,24 @@ export class Vis {
if (state.data && (state.data.aggs || !this.data.aggs)) {
const aggs = state.data.aggs ? cloneDeep(state.data.aggs) : [];
const configStates = this.initializeDefaultsFromSchemas(aggs, this.type.schemas.all || []);
- if (!this.data.indexPattern) {
- if (aggs.length) {
- const errorMessage = i18n.translate(
- 'visualizations.initializeWithoutIndexPatternErrorMessage',
- {
- defaultMessage: 'Trying to initialize aggs without index pattern',
- }
- );
- throw new Error(errorMessage);
- }
- return;
+
+ if (!this.data.indexPattern && aggs.length) {
+ this.data.indexPattern = new DataView({
+ spec: {
+ id: state.data.searchSource?.index,
+ title: i18n.translate('visualizations.noDataView.text', {
+ defaultMessage: 'Data view not found',
+ }),
+ },
+ fieldFormats: getFieldsFormats(),
+ });
+ this.data.searchSource = await getSearch().searchSource.createEmpty();
+ this.data.searchSource?.setField('index', this.data.indexPattern);
+ }
+
+ if (this.data.indexPattern) {
+ this.data.aggs = getAggs().createAggConfigs(this.data.indexPattern, configStates);
}
- this.data.aggs = getAggs().createAggConfigs(this.data.indexPattern, configStates);
}
}
diff --git a/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx b/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx
index b634f84365627..755667a9b49d7 100644
--- a/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx
+++ b/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx
@@ -20,7 +20,7 @@ import type {
VisualizeEditorVisInstance,
} from '../types';
import { VISUALIZE_APP_NAME } from '../../../common/constants';
-import { getTopNavConfig } from '../utils';
+import { getTopNavConfig, isFallbackDataView } from '../utils';
import type { NavigateToLensContext } from '../..';
const LOCAL_STORAGE_EDIT_IN_LENS_BADGE = 'EDIT_IN_LENS_BADGE_VISIBLE';
@@ -305,12 +305,13 @@ const TopNav = ({
showQueryInput={showQueryInput}
showSaveQuery={Boolean(services.visualizeCapabilities.saveQuery)}
dataViewPickerComponentProps={
- shouldShowDataViewPicker
+ shouldShowDataViewPicker && vis.data.indexPattern
? {
- currentDataViewId: vis.data.indexPattern!.id,
+ currentDataViewId: vis.data.indexPattern.id,
trigger: {
- label: vis.data.indexPattern!.title,
+ label: vis.data.indexPattern.title,
},
+ isMissingCurrent: isFallbackDataView(vis.data.indexPattern),
onChangeDataView,
showNewMenuTour: false,
}
diff --git a/src/plugins/visualizations/public/visualize_app/utils/utils.ts b/src/plugins/visualizations/public/visualize_app/utils/utils.ts
index 27d15ddc84d90..619505ce3e27c 100644
--- a/src/plugins/visualizations/public/visualize_app/utils/utils.ts
+++ b/src/plugins/visualizations/public/visualize_app/utils/utils.ts
@@ -11,6 +11,7 @@ import type { History } from 'history';
import type { ChromeStart, DocLinksStart } from '@kbn/core/public';
import type { Filter } from '@kbn/es-query';
import { redirectWhenMissing } from '@kbn/kibana-utils-plugin/public';
+import type { DataView } from '@kbn/data-views-plugin/public';
import { VisualizeConstants } from '../../../common/constants';
import { convertFromSerializedVis } from '../../utils/saved_visualize_utils';
import type { VisualizeServices, VisualizeEditorVisInstance } from '../types';
@@ -99,3 +100,7 @@ export const redirectToSavedObjectPage = (
export function getVizEditorOriginatingAppUrl(history: History) {
return `#/${history.location.pathname}${history.location.search}`;
}
+
+export function isFallbackDataView(dataView: DataView) {
+ return !Object.keys(dataView.getOriginalSavedObjectBody() ?? {}).length;
+}
diff --git a/test/api_integration/apis/general/csp.js b/test/api_integration/apis/general/csp.js
deleted file mode 100644
index d50080a6e5ff0..0000000000000
--- a/test/api_integration/apis/general/csp.js
+++ /dev/null
@@ -1,36 +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 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 expect from '@kbn/expect';
-
-export default function ({ getService }) {
- const supertest = getService('supertest');
-
- describe('csp smoke test', () => {
- it('app response sends content security policy headers', async () => {
- const response = await supertest.get('/app/kibana');
-
- expect(response.headers).to.have.property('content-security-policy');
- const header = response.headers['content-security-policy'];
- const parsed = new Map(
- header.split(';').map((rule) => {
- const parts = rule.trim().split(' ');
- const key = parts.splice(0, 1)[0];
- return [key, parts];
- })
- );
-
- const entries = Array.from(parsed.entries());
- expect(entries).to.eql([
- ['script-src', ["'unsafe-eval'", "'self'"]],
- ['worker-src', ['blob:', "'self'"]],
- ['style-src', ["'unsafe-inline'", "'self'"]],
- ]);
- });
- });
-}
diff --git a/test/api_integration/apis/general/index.js b/test/api_integration/apis/general/index.js
index e182d49ed4722..cd36b8a3395f7 100644
--- a/test/api_integration/apis/general/index.js
+++ b/test/api_integration/apis/general/index.js
@@ -9,6 +9,5 @@
export default function ({ loadTestFile }) {
describe('general', () => {
loadTestFile(require.resolve('./cookies'));
- loadTestFile(require.resolve('./csp'));
});
}
diff --git a/test/common/config.js b/test/common/config.js
index 5959820211559..048d032d0169a 100644
--- a/test/common/config.js
+++ b/test/common/config.js
@@ -43,6 +43,7 @@ export default function () {
// Needed for async search functional tests to introduce a delay
`--data.search.aggs.shardDelay.enabled=true`,
`--security.showInsecureClusterWarning=false`,
+ '--csp.disableUnsafeEval=true',
'--telemetry.banner=false',
'--telemetry.optIn=false',
// These are *very* important to have them pointing to staging
diff --git a/test/examples/response_stream/index.ts b/test/examples/response_stream/index.ts
new file mode 100644
index 0000000000000..b918de669819b
--- /dev/null
+++ b/test/examples/response_stream/index.ts
@@ -0,0 +1,16 @@
+/*
+ * 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 { FtrProviderContext } from '../../functional/ftr_provider_context';
+
+// eslint-disable-next-line import/no-default-export
+export default function ({ getService, getPageObjects, loadTestFile }: FtrProviderContext) {
+ describe('response stream', function () {
+ loadTestFile(require.resolve('./reducer_stream'));
+ });
+}
diff --git a/test/examples/response_stream/parse_stream.ts b/test/examples/response_stream/parse_stream.ts
new file mode 100644
index 0000000000000..3c0a128a2ff5c
--- /dev/null
+++ b/test/examples/response_stream/parse_stream.ts
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+export async function* parseStream(stream: NodeJS.ReadableStream) {
+ let partial = '';
+
+ try {
+ for await (const value of stream) {
+ const full = `${partial}${value}`;
+ const parts = full.split('\n');
+ const last = parts.pop();
+
+ partial = last ?? '';
+
+ const actions = parts.map((p) => JSON.parse(p));
+
+ for (const action of actions) {
+ yield action;
+ }
+ }
+ } catch (error) {
+ yield { type: 'error', payload: error.toString() };
+ }
+}
diff --git a/x-pack/test/api_integration/apis/aiops/example_stream.ts b/test/examples/response_stream/reducer_stream.ts
similarity index 82%
rename from x-pack/test/api_integration/apis/aiops/example_stream.ts
rename to test/examples/response_stream/reducer_stream.ts
index c1e410655dbfc..001fea1a144c8 100644
--- a/x-pack/test/api_integration/apis/aiops/example_stream.ts
+++ b/test/examples/response_stream/reducer_stream.ts
@@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 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 fetch from 'node-fetch';
@@ -10,19 +11,20 @@ import { format as formatUrl } from 'url';
import expect from '@kbn/expect';
-import { FtrProviderContext } from '../../ftr_provider_context';
+import { FtrProviderContext } from '../../functional/ftr_provider_context';
import { parseStream } from './parse_stream';
+// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
const config = getService('config');
const kibanaServerUrl = formatUrl(config.get('servers.kibana'));
- describe('POST /internal/aiops/example_stream', () => {
+ describe('POST /internal/response_stream/reducer_stream', () => {
it('should return full data without streaming', async () => {
const resp = await supertest
- .post(`/internal/aiops/example_stream`)
+ .post('/internal/response_stream/reducer_stream')
.set('kbn-xsrf', 'kibana')
.send({
timeout: 1,
@@ -55,7 +57,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should return data in chunks with streaming', async () => {
- const response = await fetch(`${kibanaServerUrl}/internal/aiops/example_stream`, {
+ const response = await fetch(`${kibanaServerUrl}/internal/response_stream/reducer_stream`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
diff --git a/test/functional/apps/dashboard/group6/dashboard_error_handling.ts b/test/functional/apps/dashboard/group6/dashboard_error_handling.ts
index e950b8aef975d..7e1956b82daf2 100644
--- a/test/functional/apps/dashboard/group6/dashboard_error_handling.ts
+++ b/test/functional/apps/dashboard/group6/dashboard_error_handling.ts
@@ -14,7 +14,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects');
const filterBar = getService('filterBar');
- const browser = getService('browser');
/**
* Common test suite for testing exception scenarious within dashboard
@@ -53,23 +52,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
// wrapping into own describe to make sure new tab is cleaned up even if test failed
// see: https://github.com/elastic/kibana/pull/67280#discussion_r430528122
describe('recreate index pattern link works', () => {
- let tabsCount = 1;
it('recreate index pattern link works', async () => {
await PageObjects.dashboard.gotoDashboardLandingPage();
await PageObjects.dashboard.loadSavedDashboard('dashboard with missing index pattern');
await PageObjects.header.waitUntilLoadingHasFinished();
- const errorEmbeddable = await testSubjects.find('embeddableStackError');
- await (await errorEmbeddable.findByTagName('a')).click();
- await browser.switchTab(1);
- tabsCount++;
- await testSubjects.existOrFail('createIndexPatternButton');
- });
+ const errorEmbeddable = await testSubjects.find('visualization-missed-data-view-error');
- after(async () => {
- if (tabsCount > 1) {
- await browser.closeCurrentWindow();
- await browser.switchTab(0);
- }
+ expect(await errorEmbeddable.isDisplayed()).to.be(true);
});
});
});
diff --git a/test/functional/apps/management/_data_view_relationships.ts b/test/functional/apps/management/_data_view_relationships.ts
new file mode 100644
index 0000000000000..c8e78eab4ce64
--- /dev/null
+++ b/test/functional/apps/management/_data_view_relationships.ts
@@ -0,0 +1,35 @@
+/*
+ * 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 expect from '@kbn/expect';
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const kibanaServer = getService('kibanaServer');
+ const browser = getService('browser');
+ const PageObjects = getPageObjects(['common', 'home', 'settings', 'discover', 'header']);
+
+ describe('data view relationships', function describeIndexTests() {
+ before(async function () {
+ await browser.setWindowSize(1200, 800);
+ await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover');
+ });
+
+ after(async () => {
+ await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover');
+ });
+
+ it('Render relationships tab and verify count', async function () {
+ await PageObjects.settings.navigateTo();
+ await PageObjects.settings.clickKibanaIndexPatterns();
+ await PageObjects.settings.clickIndexPatternLogstash();
+ await PageObjects.settings.clickRelationshipsTab();
+ expect(parseInt(await PageObjects.settings.getRelationshipsTabCount(), 10)).to.be(1);
+ });
+ });
+}
diff --git a/test/functional/apps/management/index.ts b/test/functional/apps/management/index.ts
index 840d04d0d1aed..09f9001d0236a 100644
--- a/test/functional/apps/management/index.ts
+++ b/test/functional/apps/management/index.ts
@@ -40,5 +40,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./_test_huge_fields'));
loadTestFile(require.resolve('./_handle_alias'));
loadTestFile(require.resolve('./_handle_version_conflict'));
+ loadTestFile(require.resolve('./_data_view_relationships'));
});
}
diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts
index 29fbe04f59e97..2d803016afd15 100644
--- a/test/functional/page_objects/settings_page.ts
+++ b/test/functional/page_objects/settings_page.ts
@@ -244,6 +244,13 @@ export class SettingsPageObject extends FtrService {
});
}
+ async getRelationshipsTabCount() {
+ return await this.retry.try(async () => {
+ const text = await this.testSubjects.getVisibleText('tab-relationships');
+ return text.split(' ')[1].replace(/\((.*)\)/, '$1');
+ });
+ }
+
async getFieldNames() {
const fieldNameCells = await this.testSubjects.findAll('editIndexPattern > indexedFieldName');
return await Promise.all(
@@ -562,6 +569,11 @@ export class SettingsPageObject extends FtrService {
await this.testSubjects.click('tab-sourceFilters');
}
+ async clickRelationshipsTab() {
+ this.log.debug('click Relationships tab');
+ await this.testSubjects.click('tab-relationships');
+ }
+
async editScriptedField(name: string) {
await this.filterField(name);
await this.find.clickByCssSelector('.euiTableRowCell--hasActions button:first-child');
diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts
index d295be040db7a..ed92b4e22f5b7 100644
--- a/test/plugin_functional/test_suites/core_plugins/rendering.ts
+++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts
@@ -173,6 +173,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
'xpack.discoverEnhanced.actions.exploreDataInChart.enabled (boolean)',
'xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled (boolean)',
'xpack.fleet.agents.enabled (boolean)',
+ 'xpack.fleet.enableExperimental (array)',
'xpack.global_search.search_timeout (duration)',
'xpack.graph.canEditDrillDownUrls (boolean)',
'xpack.graph.savePolicy (alternatives)',
diff --git a/tsconfig.base.json b/tsconfig.base.json
index daf7bf78903c1..a593145c4093d 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -31,6 +31,8 @@
"@kbn/paertial-results-example-plugin/*": ["examples/partial_results_example/*"],
"@kbn/preboot-example-plugin": ["examples/preboot_example"],
"@kbn/preboot-example-plugin/*": ["examples/preboot_example/*"],
+ "@kbn/response-stream-plugin": ["examples/response_stream"],
+ "@kbn/response-stream-plugin/*": ["examples/response_stream/*"],
"@kbn/routing-example-plugin": ["examples/routing_example"],
"@kbn/routing-example-plugin/*": ["examples/routing_example/*"],
"@kbn/screenshot-mode-example-plugin": ["examples/screenshot_mode_example"],
diff --git a/x-pack/plugins/aiops/common/api/index.ts b/x-pack/plugins/aiops/common/api/index.ts
index 6b987fef13d1a..2397f554c128f 100644
--- a/x-pack/plugins/aiops/common/api/index.ts
+++ b/x-pack/plugins/aiops/common/api/index.ts
@@ -9,20 +9,15 @@ import type {
AiopsExplainLogRateSpikesSchema,
AiopsExplainLogRateSpikesApiAction,
} from './explain_log_rate_spikes';
-import type { AiopsExampleStreamSchema, AiopsExampleStreamApiAction } from './example_stream';
+import { streamReducer } from './stream_reducer';
export const API_ENDPOINT = {
- EXAMPLE_STREAM: '/internal/aiops/example_stream',
EXPLAIN_LOG_RATE_SPIKES: '/internal/aiops/explain_log_rate_spikes',
} as const;
-export type ApiEndpoint = typeof API_ENDPOINT[keyof typeof API_ENDPOINT];
-export interface ApiEndpointOptions {
- [API_ENDPOINT.EXAMPLE_STREAM]: AiopsExampleStreamSchema;
- [API_ENDPOINT.EXPLAIN_LOG_RATE_SPIKES]: AiopsExplainLogRateSpikesSchema;
-}
-
-export interface ApiEndpointActions {
- [API_ENDPOINT.EXAMPLE_STREAM]: AiopsExampleStreamApiAction;
- [API_ENDPOINT.EXPLAIN_LOG_RATE_SPIKES]: AiopsExplainLogRateSpikesApiAction;
+export interface ApiExplainLogRateSpikes {
+ endpoint: typeof API_ENDPOINT.EXPLAIN_LOG_RATE_SPIKES;
+ reducer: typeof streamReducer;
+ body: AiopsExplainLogRateSpikesSchema;
+ actions: AiopsExplainLogRateSpikesApiAction;
}
diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/stream_reducer.ts b/x-pack/plugins/aiops/common/api/stream_reducer.ts
similarity index 86%
rename from x-pack/plugins/aiops/public/components/explain_log_rate_spikes/stream_reducer.ts
rename to x-pack/plugins/aiops/common/api/stream_reducer.ts
index 7ec710f4ae65d..f539f26f15b53 100644
--- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/stream_reducer.ts
+++ b/x-pack/plugins/aiops/common/api/stream_reducer.ts
@@ -5,10 +5,7 @@
* 2.0.
*/
-import {
- API_ACTION_NAME,
- AiopsExplainLogRateSpikesApiAction,
-} from '../../../common/api/explain_log_rate_spikes';
+import { API_ACTION_NAME, AiopsExplainLogRateSpikesApiAction } from './explain_log_rate_spikes';
interface StreamState {
fields: string[];
diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes.tsx
index 12c4837194f80..05eae06320027 100644
--- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes.tsx
+++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes.tsx
@@ -10,10 +10,11 @@ import React, { useEffect, FC } from 'react';
import { EuiBadge, EuiSpacer, EuiText } from '@elastic/eui';
import type { DataView } from '@kbn/data-views-plugin/public';
+import { useFetchStream } from '@kbn/aiops-utils';
+import { useKibana } from '@kbn/kibana-react-plugin/public';
-import { useStreamFetchReducer } from '../../hooks/use_stream_fetch_reducer';
-
-import { initialState, streamReducer } from './stream_reducer';
+import { initialState, streamReducer } from '../../../common/api/stream_reducer';
+import type { ApiExplainLogRateSpikes } from '../../../common/api';
/**
* ExplainLogRateSpikes props require a data view.
@@ -24,11 +25,13 @@ export interface ExplainLogRateSpikesProps {
}
export const ExplainLogRateSpikes: FC = ({ dataView }) => {
- const { start, data, isRunning } = useStreamFetchReducer(
- '/internal/aiops/explain_log_rate_spikes',
- streamReducer,
- initialState,
- { index: dataView.title }
+ const kibana = useKibana();
+ const basePath = kibana.services.http?.basePath.get() ?? '';
+
+ const { start, data, isRunning } = useFetchStream(
+ `${basePath}/internal/aiops/explain_log_rate_spikes`,
+ { index: dataView.title },
+ { reducer: streamReducer, initialState }
);
useEffect(() => {
diff --git a/x-pack/plugins/aiops/public/components/single_endpoint_streaming_demo/single_endpoint_streaming_demo.tsx b/x-pack/plugins/aiops/public/components/single_endpoint_streaming_demo/single_endpoint_streaming_demo.tsx
deleted file mode 100644
index 12f33aada133c..0000000000000
--- a/x-pack/plugins/aiops/public/components/single_endpoint_streaming_demo/single_endpoint_streaming_demo.tsx
+++ /dev/null
@@ -1,135 +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, { useEffect, useState, FC } from 'react';
-
-import { Chart, Settings, Axis, BarSeries, Position, ScaleType } from '@elastic/charts';
-
-import { i18n } from '@kbn/i18n';
-import { useKibana } from '@kbn/kibana-react-plugin/public';
-
-import {
- EuiBadge,
- EuiButton,
- EuiCheckbox,
- EuiFlexGroup,
- EuiFlexItem,
- EuiProgress,
- EuiSpacer,
- EuiText,
-} from '@elastic/eui';
-
-import { useStreamFetchReducer } from '../../hooks/use_stream_fetch_reducer';
-
-import { getStatusMessage } from './get_status_message';
-import { initialState, resetStream, streamReducer } from './stream_reducer';
-
-export const SingleEndpointStreamingDemo: FC = () => {
- const { notifications } = useKibana();
-
- const [simulateErrors, setSimulateErrors] = useState(false);
-
- const { dispatch, start, cancel, data, isCancelled, isRunning } = useStreamFetchReducer(
- '/internal/aiops/example_stream',
- streamReducer,
- initialState,
- { simulateErrors }
- );
-
- const { errors, progress, entities } = data;
-
- const onClickHandler = async () => {
- if (isRunning) {
- cancel();
- } else {
- dispatch(resetStream());
- start();
- }
- };
-
- useEffect(() => {
- if (errors.length > 0) {
- notifications.toasts.danger({ body: errors[errors.length - 1] });
- }
- }, [errors, notifications.toasts]);
-
- const buttonLabel = isRunning
- ? i18n.translate('xpack.aiops.stopbuttonText', {
- defaultMessage: 'Stop development',
- })
- : i18n.translate('xpack.aiops.startbuttonText', {
- defaultMessage: 'Start development',
- });
-
- return (
-
-
-
-
- {buttonLabel}
-
-
-
-
- {progress}%
-
-
-
-
-
-
-
-
-
-
-
-
-
- {
- return {
- x,
- y,
- };
- })
- .sort((a, b) => b.y - a.y)}
- />
-
-
- {getStatusMessage(isRunning, isCancelled, data.progress)}
- setSimulateErrors(!simulateErrors)}
- compressed
- />
-
- );
-};
diff --git a/x-pack/plugins/aiops/public/hooks/stream_fetch.ts b/x-pack/plugins/aiops/public/hooks/stream_fetch.ts
deleted file mode 100644
index abfec63702012..0000000000000
--- a/x-pack/plugins/aiops/public/hooks/stream_fetch.ts
+++ /dev/null
@@ -1,101 +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 type React from 'react';
-
-import type { ApiEndpoint, ApiEndpointActions, ApiEndpointOptions } from '../../common/api';
-
-interface ErrorAction {
- type: 'error';
- payload: string;
-}
-
-export async function* streamFetch(
- endpoint: E,
- abortCtrl: React.MutableRefObject,
- options: ApiEndpointOptions[E],
- basePath = ''
-): AsyncGenerator> {
- const stream = await fetch(`${basePath}${endpoint}`, {
- signal: abortCtrl.current.signal,
- method: 'POST',
- headers: {
- // This refers to the format of the request body,
- // not the response, which will be a uint8array Buffer.
- 'Content-Type': 'application/json',
- 'kbn-xsrf': 'stream',
- },
- body: JSON.stringify(options),
- });
-
- if (stream.body !== null) {
- // Note that Firefox 99 doesn't support `TextDecoderStream` yet.
- // That's why we skip it here and use `TextDecoder` later to decode each chunk.
- // Once Firefox supports it, we can use the following alternative:
- // const reader = stream.body.pipeThrough(new TextDecoderStream()).getReader();
- const reader = stream.body.getReader();
-
- const bufferBounce = 100;
- let partial = '';
- let actionBuffer: Array = [];
- let lastCall = 0;
-
- while (true) {
- try {
- const { value: uint8array, done } = await reader.read();
- if (done) break;
-
- const value = new TextDecoder().decode(uint8array);
-
- const full = `${partial}${value}`;
- const parts = full.split('\n');
- const last = parts.pop();
-
- partial = last ?? '';
-
- const actions = parts.map((p) => JSON.parse(p)) as Array;
- actionBuffer.push(...actions);
-
- const now = Date.now();
-
- if (now - lastCall >= bufferBounce && actionBuffer.length > 0) {
- yield actionBuffer;
- actionBuffer = [];
- lastCall = now;
-
- // In cases where the next chunk takes longer to be received than the `bufferBounce` timeout,
- // we trigger this client side timeout to clear a potential intermediate buffer state.
- // Since `yield` cannot be passed on to other scopes like callbacks,
- // this pattern using a Promise is used to wait for the timeout.
- yield new Promise>((resolve) => {
- setTimeout(() => {
- if (actionBuffer.length > 0) {
- resolve(actionBuffer);
- actionBuffer = [];
- lastCall = now;
- } else {
- resolve([]);
- }
- }, bufferBounce + 10);
- });
- }
- } catch (error) {
- if (error.name !== 'AbortError') {
- yield [{ type: 'error', payload: error.toString() }];
- }
- break;
- }
- }
-
- // The reader might finish with a partially filled actionBuffer so
- // we need to clear it once more after the request is done.
- if (actionBuffer.length > 0) {
- yield actionBuffer;
- actionBuffer.length = 0;
- }
- }
-}
diff --git a/x-pack/plugins/aiops/public/hooks/use_stream_fetch_reducer.ts b/x-pack/plugins/aiops/public/hooks/use_stream_fetch_reducer.ts
deleted file mode 100644
index ba64831bec60e..0000000000000
--- a/x-pack/plugins/aiops/public/hooks/use_stream_fetch_reducer.ts
+++ /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 {
- useEffect,
- useReducer,
- useRef,
- useState,
- Reducer,
- ReducerAction,
- ReducerState,
-} from 'react';
-
-import { useKibana } from '@kbn/kibana-react-plugin/public';
-
-import type { ApiEndpoint, ApiEndpointOptions } from '../../common/api';
-
-import { streamFetch } from './stream_fetch';
-
-export const useStreamFetchReducer = , E extends ApiEndpoint>(
- endpoint: E,
- reducer: R,
- initialState: ReducerState,
- options: ApiEndpointOptions[E]
-) => {
- const kibana = useKibana();
-
- const [isCancelled, setIsCancelled] = useState(false);
- const [isRunning, setIsRunning] = useState(false);
-
- const [data, dispatch] = useReducer(reducer, initialState);
-
- const abortCtrl = useRef(new AbortController());
-
- const start = async () => {
- if (isRunning) {
- throw new Error('Restart not supported yet');
- }
-
- setIsRunning(true);
- setIsCancelled(false);
-
- abortCtrl.current = new AbortController();
-
- for await (const actions of streamFetch(
- endpoint,
- abortCtrl,
- options,
- kibana.services.http?.basePath.get()
- )) {
- if (actions.length > 0) {
- dispatch(actions as ReducerAction);
- }
- }
-
- setIsRunning(false);
- };
-
- const cancel = () => {
- abortCtrl.current.abort();
- setIsCancelled(true);
- setIsRunning(false);
- };
-
- // If components using this custom hook get unmounted, cancel any ongoing request.
- useEffect(() => {
- return () => abortCtrl.current.abort();
- }, []);
-
- return {
- cancel,
- data,
- dispatch,
- isCancelled,
- isRunning,
- start,
- };
-};
diff --git a/x-pack/plugins/aiops/public/index.ts b/x-pack/plugins/aiops/public/index.ts
index 53fc1d7a6eeca..26166e7ca104d 100755
--- a/x-pack/plugins/aiops/public/index.ts
+++ b/x-pack/plugins/aiops/public/index.ts
@@ -14,5 +14,5 @@ export function plugin() {
}
export type { ExplainLogRateSpikesProps } from './components/explain_log_rate_spikes';
-export { ExplainLogRateSpikes, SingleEndpointStreamingDemo } from './shared_lazy_components';
+export { ExplainLogRateSpikes } from './shared_lazy_components';
export type { AiopsPluginSetup, AiopsPluginStart } from './types';
diff --git a/x-pack/plugins/aiops/public/shared_lazy_components.tsx b/x-pack/plugins/aiops/public/shared_lazy_components.tsx
index f707a77cf7f90..852841a05c2cb 100644
--- a/x-pack/plugins/aiops/public/shared_lazy_components.tsx
+++ b/x-pack/plugins/aiops/public/shared_lazy_components.tsx
@@ -12,9 +12,6 @@ import { EuiErrorBoundary, EuiLoadingContent } from '@elastic/eui';
import type { ExplainLogRateSpikesProps } from './components/explain_log_rate_spikes';
const ExplainLogRateSpikesLazy = React.lazy(() => import('./components/explain_log_rate_spikes'));
-const SingleEndpointStreamingDemoLazy = React.lazy(
- () => import('./components/single_endpoint_streaming_demo')
-);
const LazyWrapper: FC = ({ children }) => (
@@ -31,12 +28,3 @@ export const ExplainLogRateSpikes: FC = (props) => (
);
-
-/**
- * Lazy-wrapped SingleEndpointStreamingDemo React component
- */
-export const SingleEndpointStreamingDemo: FC = () => (
-
-
-
-);
diff --git a/x-pack/plugins/aiops/server/lib/stream_factory.test.ts b/x-pack/plugins/aiops/server/lib/stream_factory.test.ts
deleted file mode 100644
index 7082a4e7e763c..0000000000000
--- a/x-pack/plugins/aiops/server/lib/stream_factory.test.ts
+++ /dev/null
@@ -1,106 +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 zlib from 'zlib';
-
-import { loggerMock, MockedLogger } from '@kbn/logging-mocks';
-
-import { API_ENDPOINT } from '../../common/api';
-import type { ApiEndpointActions } from '../../common/api';
-
-import { streamFactory } from './stream_factory';
-
-type Action = ApiEndpointActions['/internal/aiops/explain_log_rate_spikes'];
-
-const mockItem1: Action = {
- type: 'add_fields',
- payload: ['clientip'],
-};
-const mockItem2: Action = {
- type: 'add_fields',
- payload: ['referer'],
-};
-
-describe('streamFactory', () => {
- let mockLogger: MockedLogger;
-
- beforeEach(() => {
- mockLogger = loggerMock.create();
- });
-
- it('should encode and receive an uncompressed stream', async () => {
- const { DELIMITER, end, push, responseWithHeaders, stream } = streamFactory<
- typeof API_ENDPOINT.EXPLAIN_LOG_RATE_SPIKES
- >(mockLogger, {});
-
- push(mockItem1);
- push(mockItem2);
- end();
-
- let streamResult = '';
- for await (const chunk of stream) {
- streamResult += chunk.toString('utf8');
- }
-
- const streamItems = streamResult.split(DELIMITER);
- const lastItem = streamItems.pop();
-
- const parsedItems = streamItems.map((d) => JSON.parse(d));
-
- expect(responseWithHeaders.headers).toBe(undefined);
- expect(parsedItems).toHaveLength(2);
- expect(parsedItems[0]).toStrictEqual(mockItem1);
- expect(parsedItems[1]).toStrictEqual(mockItem2);
- expect(lastItem).toBe('');
- });
-
- // Because zlib.gunzip's API expects a callback, we need to use `done` here
- // to indicate once all assertions are run. However, it's not allowed to use both
- // `async` and `done` for the test callback. That's why we're using an "async IIFE"
- // pattern inside the tests callback to still be able to do async/await for the
- // `for await()` part. Note that the unzipping here is done just to be able to
- // decode the stream for the test and assert it. When used in actual code,
- // the browser on the client side will automatically take care of unzipping
- // without the need for additional custom code.
- it('should encode and receive a compressed stream', (done) => {
- (async () => {
- const { DELIMITER, end, push, responseWithHeaders, stream } = streamFactory<
- typeof API_ENDPOINT.EXPLAIN_LOG_RATE_SPIKES
- >(mockLogger, { 'accept-encoding': 'gzip' });
-
- push(mockItem1);
- push(mockItem2);
- end();
-
- const chunks = [];
- for await (const chunk of stream) {
- chunks.push(chunk);
- }
-
- const buffer = Buffer.concat(chunks);
-
- zlib.gunzip(buffer, function (err, decoded) {
- expect(err).toBe(null);
-
- const streamResult = decoded.toString('utf8');
-
- const streamItems = streamResult.split(DELIMITER);
- const lastItem = streamItems.pop();
-
- const parsedItems = streamItems.map((d) => JSON.parse(d));
-
- expect(responseWithHeaders.headers).toStrictEqual({ 'content-encoding': 'gzip' });
- expect(parsedItems).toHaveLength(2);
- expect(parsedItems[0]).toStrictEqual(mockItem1);
- expect(parsedItems[1]).toStrictEqual(mockItem2);
- expect(lastItem).toBe('');
-
- done();
- });
- })();
- });
-});
diff --git a/x-pack/plugins/aiops/server/lib/stream_factory.ts b/x-pack/plugins/aiops/server/lib/stream_factory.ts
deleted file mode 100644
index dc67a54902527..0000000000000
--- a/x-pack/plugins/aiops/server/lib/stream_factory.ts
+++ /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 { Stream } from 'stream';
-import zlib from 'zlib';
-
-import type { Headers, Logger } from '@kbn/core/server';
-
-import { ApiEndpoint, ApiEndpointActions } from '../../common/api';
-
-import { acceptCompression } from './accept_compression';
-
-// We need this otherwise Kibana server will crash with a 'ERR_METHOD_NOT_IMPLEMENTED' error.
-class ResponseStream extends Stream.PassThrough {
- flush() {}
- _read() {}
-}
-
-const DELIMITER = '\n';
-
-/**
- * Sets up a response stream with support for gzip compression depending on provided
- * request headers.
- *
- * @param logger - Kibana provided logger.
- * @param headers - Request headers.
- * @returns An object with stream attributes and methods.
- */
-export function streamFactory(logger: Logger, headers: Headers) {
- const isCompressed = acceptCompression(headers);
-
- const stream = isCompressed ? zlib.createGzip() : new ResponseStream();
-
- function push(d: ApiEndpointActions[T]) {
- try {
- const line = JSON.stringify(d);
- stream.write(`${line}${DELIMITER}`);
-
- // Calling .flush() on a compression stream will
- // make zlib return as much output as currently possible.
- if (isCompressed) {
- stream.flush();
- }
- } catch (error) {
- logger.error('Could not serialize or stream a message.');
- logger.error(error);
- }
- }
-
- function end() {
- stream.end();
- }
-
- const responseWithHeaders = {
- body: stream,
- ...(isCompressed
- ? {
- headers: {
- 'content-encoding': 'gzip',
- },
- }
- : {}),
- };
-
- return { DELIMITER, end, push, responseWithHeaders, stream };
-}
diff --git a/x-pack/plugins/aiops/server/plugin.ts b/x-pack/plugins/aiops/server/plugin.ts
index 3743d32e3a081..56a2a8bb58bab 100755
--- a/x-pack/plugins/aiops/server/plugin.ts
+++ b/x-pack/plugins/aiops/server/plugin.ts
@@ -16,7 +16,7 @@ import {
AiopsPluginSetupDeps,
AiopsPluginStartDeps,
} from './types';
-import { defineExampleStreamRoute, defineExplainLogRateSpikesRoute } from './routes';
+import { defineExplainLogRateSpikesRoute } from './routes';
export class AiopsPlugin
implements Plugin
@@ -34,7 +34,6 @@ export class AiopsPlugin
// Register server side APIs
if (AIOPS_ENABLED) {
core.getStartServices().then(([_, depsStart]) => {
- defineExampleStreamRoute(router, this.logger);
defineExplainLogRateSpikesRoute(router, this.logger);
});
}
diff --git a/x-pack/plugins/aiops/server/routes/explain_log_rate_spikes.ts b/x-pack/plugins/aiops/server/routes/explain_log_rate_spikes.ts
index f8aeb06435b76..8a78dffb24b8f 100644
--- a/x-pack/plugins/aiops/server/routes/explain_log_rate_spikes.ts
+++ b/x-pack/plugins/aiops/server/routes/explain_log_rate_spikes.ts
@@ -9,15 +9,15 @@ import { firstValueFrom } from 'rxjs';
import type { IRouter, Logger } from '@kbn/core/server';
import type { DataRequestHandlerContext, IEsSearchRequest } from '@kbn/data-plugin/server';
+import { streamFactory } from '@kbn/aiops-utils';
import {
aiopsExplainLogRateSpikesSchema,
addFieldsAction,
+ AiopsExplainLogRateSpikesApiAction,
} from '../../common/api/explain_log_rate_spikes';
import { API_ENDPOINT } from '../../common/api';
-import { streamFactory } from '../lib/stream_factory';
-
export const defineExplainLogRateSpikesRoute = (
router: IRouter,
logger: Logger
@@ -60,9 +60,9 @@ export const defineExplainLogRateSpikesRoute = (
const doc = res.rawResponse.hits.hits.pop();
const fields = Object.keys(doc?._source ?? {});
- const { end, push, responseWithHeaders } = streamFactory<
- typeof API_ENDPOINT.EXPLAIN_LOG_RATE_SPIKES
- >(logger, request.headers);
+ const { end, push, responseWithHeaders } = streamFactory(
+ request.headers
+ );
async function pushField() {
setTimeout(() => {
@@ -79,7 +79,9 @@ export const defineExplainLogRateSpikesRoute = (
} else {
end();
}
- }, Math.random() * 1000);
+ // This is just exemplary demo code so we're adding a random timout of 0-250ms to each
+ // stream push to simulate string chunks appearing on the client with some randomness.
+ }, Math.random() * 250);
}
pushField();
diff --git a/x-pack/plugins/aiops/server/routes/index.ts b/x-pack/plugins/aiops/server/routes/index.ts
index d69ef6cc7df09..d8e55746f70e0 100755
--- a/x-pack/plugins/aiops/server/routes/index.ts
+++ b/x-pack/plugins/aiops/server/routes/index.ts
@@ -5,5 +5,4 @@
* 2.0.
*/
-export { defineExampleStreamRoute } from './example_stream';
export { defineExplainLogRateSpikesRoute } from './explain_log_rate_spikes';
diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts
index b7e06aa602f27..68dbcb794f807 100644
--- a/x-pack/plugins/alerting/server/types.ts
+++ b/x-pack/plugins/alerting/server/types.ts
@@ -126,7 +126,7 @@ export type ExecutorType<
export interface RuleTypeParamsValidator {
validate: (object: unknown) => Params;
- validateMutatedParams?: (mutatedOject: unknown, origObject?: unknown) => Params;
+ validateMutatedParams?: (mutatedOject: Params, origObject?: Params) => Params;
}
export interface RuleType<
diff --git a/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap
index a2baee6074989..3e9f87b47bf55 100644
--- a/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap
+++ b/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap
@@ -1014,6 +1014,13 @@ exports[`APM telemetry helpers getApmTelemetry generates a JSON object with the
}
}
},
+ "service_groups": {
+ "properties": {
+ "kuery_fields": {
+ "type": "keyword"
+ }
+ }
+ },
"tasks": {
"properties": {
"aggregated_transactions": {
@@ -1158,6 +1165,17 @@ exports[`APM telemetry helpers getApmTelemetry generates a JSON object with the
}
}
}
+ },
+ "service_groups": {
+ "properties": {
+ "took": {
+ "properties": {
+ "ms": {
+ "type": "long"
+ }
+ }
+ }
+ }
}
}
}
diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/index.js b/x-pack/plugins/apm/common/data_view_constants.ts
similarity index 62%
rename from x-pack/plugins/monitoring/server/routes/api/v1/kibana/index.js
rename to x-pack/plugins/apm/common/data_view_constants.ts
index f7eccda44972a..b448918f8facf 100644
--- a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/index.js
+++ b/x-pack/plugins/apm/common/data_view_constants.ts
@@ -5,6 +5,5 @@
* 2.0.
*/
-export { kibanaInstanceRoute } from './instance';
-export { kibanaInstancesRoute } from './instances';
-export { kibanaOverviewRoute } from './overview';
+// value of const needs to be backwards compatible
+export const APM_STATIC_DATA_VIEW_ID = 'apm_static_index_pattern_id';
diff --git a/x-pack/plugins/apm/common/trace_explorer.ts b/x-pack/plugins/apm/common/trace_explorer.ts
new file mode 100644
index 0000000000000..9ce3bc8df0bd6
--- /dev/null
+++ b/x-pack/plugins/apm/common/trace_explorer.ts
@@ -0,0 +1,16 @@
+/*
+ * 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.
+ */
+
+export interface TraceSearchQuery {
+ query: string;
+ type: TraceSearchType;
+}
+
+export enum TraceSearchType {
+ kql = 'kql',
+ eql = 'eql',
+}
diff --git a/x-pack/plugins/apm/dev_docs/telemetry.md b/x-pack/plugins/apm/dev_docs/telemetry.md
index 27b9e57447467..0085f64cdec18 100644
--- a/x-pack/plugins/apm/dev_docs/telemetry.md
+++ b/x-pack/plugins/apm/dev_docs/telemetry.md
@@ -19,7 +19,7 @@ to the telemetry cluster using the
During the APM server-side plugin's setup phase a
[Saved Object](https://www.elastic.co/guide/en/kibana/master/managing-saved-objects.html)
for APM telemetry is registered and a
-[task manager](../../task_manager/server/README.md) task is registered and started.
+[task manager](../../task_manager/README.md) task is registered and started.
The task periodically queries the APM indices and saves the results in the Saved
Object, and the usage collector periodically gets the data from the saved object
and uploads it to the telemetry cluster.
@@ -27,23 +27,19 @@ and uploads it to the telemetry cluster.
Once uploaded to the telemetry cluster, the data telemetry is stored in
`stack_stats.kibana.plugins.apm` in the xpack-phone-home index.
-### Generating sample data
+### Collect a new telemetry field
-The script in `scripts/upload_telemetry_data` can generate sample telemetry data and upload it to a cluster of your choosing.
+In order to collect a new telemetry field you need to add a task which performs the query that collects the data from the cluster.
-You'll need to set the `GITHUB_TOKEN` environment variable to a token that has `repo` scope so it can read from the
-[elastic/telemetry](https://github.com/elastic/telemetry) repository. (You probably have a token that works for this in
-~/.backport/config.json.)
+All the available tasks are [here](https://github.com/elastic/kibana/blob/ba84602455671f0f6175bbc0fd2e8f302c60bbe6/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts)
-The script will run as the `elastic` user using the elasticsearch hosts and password settings from the config/kibana.yml
-and/or config/kibana.dev.yml files.
+### Debug telemetry
-Running the script with `--clear` will delete the index first.
+The following endpoint will run the `apm-telemetry-task` which is responsible for collecting the telemetry data and once it's completed it will return the telemetry attributes.
-If you're using an Elasticsearch instance without TLS verification (if you have `elasticsearch.ssl.verificationMode: none` set in your kibana.yml)
-you can run the script with `env NODE_TLS_REJECT_UNAUTHORIZED=0` to avoid TLS connection errors.
-
-After running the script you should see sample telemetry data in the "xpack-phone-home" index.
+```
+GET /internal/apm/debug-telemetry
+```
### Updating Data Telemetry Mappings
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/feature_flag/infrastructure.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/feature_flag/infrastructure.spec.ts
index a74d4ba27610d..250247464eed1 100644
--- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/feature_flag/infrastructure.spec.ts
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/feature_flag/infrastructure.spec.ts
@@ -72,8 +72,7 @@ describe('Infrastracture feature flag', () => {
it('shows infrastructure tab in service overview page', () => {
cy.visit(serviceOverviewPath);
- cy.contains('a[role="tab"]', 'Infrastructure').click();
- cy.contains('Infrastructure data coming soon');
+ cy.contains('a[role="tab"]', 'Infrastructure');
});
});
});
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/error_details.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/error_details.spec.ts
index c131cb2dd36d7..caec7a23115ff 100644
--- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/error_details.spec.ts
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/errors/error_details.spec.ts
@@ -89,7 +89,7 @@ describe('Error details', () => {
describe('when clicking on View x occurences in discover', () => {
it('should redirects the user to discover', () => {
cy.visit(errorDetailsPageHref);
- cy.contains('View 1 occurrence in Discover.').click();
+ cy.contains('View 1 occurrence in Discover').click();
cy.url().should('include', 'app/discover');
});
});
diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json
index 9bb1c52b52d7c..f354556c97b5b 100644
--- a/x-pack/plugins/apm/kibana.json
+++ b/x-pack/plugins/apm/kibana.json
@@ -17,7 +17,8 @@
"observability",
"ruleRegistry",
"triggersActionsUi",
- "unifiedSearch"
+ "unifiedSearch",
+ "dataViews"
],
"optionalPlugins": [
"actions",
@@ -33,12 +34,16 @@
],
"server": true,
"ui": true,
- "configPath": ["xpack", "apm"],
+ "configPath": [
+ "xpack",
+ "apm"
+ ],
"requiredBundles": [
"fleet",
"kibanaReact",
"kibanaUtils",
"ml",
- "observability"
+ "observability",
+ "esUiShared"
]
}
diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx
index 19e16237c1272..b471655c6b7d5 100644
--- a/x-pack/plugins/apm/public/application/index.tsx
+++ b/x-pack/plugins/apm/public/application/index.tsx
@@ -52,6 +52,8 @@ export const renderApp = ({
inspector: pluginsStart.inspector,
observability: pluginsStart.observability,
observabilityRuleTypeRegistry,
+ dataViews: pluginsStart.dataViews,
+ unifiedSearch: pluginsStart.unifiedSearch,
};
// render APM feedback link in global help menu
diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/__snapshots__/index.test.tsx.snap b/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/__snapshots__/index.test.tsx.snap
deleted file mode 100644
index 5f300b45de80a..0000000000000
--- a/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/__snapshots__/index.test.tsx.snap
+++ /dev/null
@@ -1,78 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`DetailView should render Discover button 1`] = `
-
- View 10 occurrences in Discover.
-
-`;
-
-exports[`DetailView should render TabContent 1`] = `
-
-`;
-
-exports[`DetailView should render tabs 1`] = `
-
-