EmbeddableRendererWithFactory<I>
| |
+
+Returns:
+
+`readonly [ErrorEmbeddable | IEmbeddable | undefined, boolean, string | undefined]`
+
diff --git a/docs/setup/upgrade/upgrade-migrations.asciidoc b/docs/setup/upgrade/upgrade-migrations.asciidoc
index fdcd71791ad3a..947043b21ef50 100644
--- a/docs/setup/upgrade/upgrade-migrations.asciidoc
+++ b/docs/setup/upgrade/upgrade-migrations.asciidoc
@@ -55,22 +55,55 @@ This section highlights common causes of {kib} upgrade failures and how to preve
There is a known issue in v7.12.0 for users who tried the fleet beta. Upgrade migrations fail because of a large number of documents in the `.kibana` index.
This can cause Kibana to log errors like:
-> Error: Unable to complete saved object migrations for the [.kibana] index. Please check the health of your Elasticsearch cluster and try again. Error: [receive_timeout_transport_exception]: [instance-0000000002][10.32.1.112:19541][cluster:monitor/task/get] request_id [2648] timed out after [59940ms]
-> Error: Unable to complete saved object migrations for the [.kibana] index. Please check the health of your Elasticsearch cluster and try again. Error: [timeout_exception]: Timed out waiting for completion of [org.elasticsearch.index.reindex.BulkByScrollTask@6a74c54]
+
+[source,sh]
+--------------------------------------------
+Error: Unable to complete saved object migrations for the [.kibana] index. Please check the health of your Elasticsearch cluster and try again. Error: [receive_timeout_transport_exception]: [instance-0000000002][10.32.1.112:19541][cluster:monitor/task/get] request_id [2648] timed out after [59940ms]
+
+Error: Unable to complete saved object migrations for the [.kibana] index. Please check the health of your Elasticsearch cluster and try again. Error: [timeout_exception]: Timed out waiting for completion of [org.elasticsearch.index.reindex.BulkByScrollTask@6a74c54]
+--------------------------------------------
See https://github.com/elastic/kibana/issues/95321 for instructions to work around this issue.
[float]
===== Corrupt saved objects
-We highly recommend testing your {kib} upgrade in a development cluster to discover and remedy problems caused by corrupt documents, especially when there are custom integrations creating saved objects in your environment. Saved objects that were corrupted through manual editing or integrations will cause migration failures with a log message like `Failed to transform document. Transform: index-pattern:7.0.0\n Doc: {...}` or `Unable to migrate the corrupt Saved Object document ...`. Corrupt documents will have to be fixed or deleted before an upgrade migration can succeed.
+We highly recommend testing your {kib} upgrade in a development cluster to discover and remedy problems caused by corrupt documents, especially when there are custom integrations creating saved objects in your environment.
+
+Saved objects that were corrupted through manual editing or integrations will cause migration failures with a log message like `Failed to transform document. Transform: index-pattern:7.0.0\n Doc: {...}` or `Unable to migrate the corrupt Saved Object document ...`. Corrupt documents will have to be fixed or deleted before an upgrade migration can succeed.
For example, given the following error message:
-> Unable to migrate the corrupt saved object document with _id: 'marketing_space:dashboard:e3c5fc71-ac71-4805-bcab-2bcc9cc93275'. To allow migrations to proceed, please delete this document from the [.kibana_7.12.0_001] index.
-The following steps must be followed to allow the upgrade migration to succeed.
-Please be aware the Dashboard having ID `e3c5fc71-ac71-4805-bcab-2bcc9cc93275` belonging to the space `marketing_space` will no more be available:
-1. Delete the corrupt document with `DELETE .kibana_7.12.0_001/_doc/marketing_space:dashboard:e3c5fc71-ac71-4805-bcab-2bcc9cc93275`
-2. Restart {kib}
+[source,sh]
+--------------------------------------------
+Unable to migrate the corrupt saved object document with _id: 'marketing_space:dashboard:e3c5fc71-ac71-4805-bcab-2bcc9cc93275'. To allow migrations to proceed, please delete this document from the [.kibana_7.12.0_001] index.
+--------------------------------------------
+
+The following steps must be followed to delete the document that is causing the migration to fail:
+
+. Remove the write block which the migration system has placed on the previous index:
++
+[source,sh]
+--------------------------------------------
+PUT .kibana_7.12.1_001/_settings
+{
+ "index": {
+ "blocks.write": false
+ }
+}
+--------------------------------------------
+
+. Delete the corrupt document:
++
+[source,sh]
+--------------------------------------------
+DELETE .kibana_7.12.0_001/_doc/marketing_space:dashboard:e3c5fc71-ac71-4805-bcab-2bcc9cc93275
+--------------------------------------------
+
+. Restart {kib}.
+
+In this example, the Dashboard with ID `e3c5fc71-ac71-4805-bcab-2bcc9cc93275` that belongs to the space `marketing_space` **will no longer be available**.
+
+Be sure you have a snapshot before you delete the corrupt document. If restoring from a snapshot is not an option, it is recommended to also delete the `temp` and `target` indices the migration created before restarting {kib} and retrying.
[float]
===== User defined index templates that causes new `.kibana*` indices to have incompatible settings or mappings
diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts
index c584b44286e07..ff7708689c221 100644
--- a/src/plugins/dashboard/public/index.ts
+++ b/src/plugins/dashboard/public/index.ts
@@ -22,11 +22,14 @@ export {
DashboardUrlGenerator,
DashboardFeatureFlagConfig,
} from './plugin';
+
export {
DASHBOARD_APP_URL_GENERATOR,
createDashboardUrlGenerator,
DashboardUrlGeneratorState,
} from './url_generator';
+export { DashboardAppLocator, DashboardAppLocatorParams } from './locator';
+
export { DashboardSavedObject } from './saved_dashboards';
export { SavedDashboardPanel, DashboardContainerInput } from './types';
diff --git a/src/plugins/dashboard/public/locator.test.ts b/src/plugins/dashboard/public/locator.test.ts
new file mode 100644
index 0000000000000..0b647ac00ce31
--- /dev/null
+++ b/src/plugins/dashboard/public/locator.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 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 { DashboardAppLocatorDefinition } from './locator';
+import { hashedItemStore } from '../../kibana_utils/public';
+import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock';
+import { esFilters } from '../../data/public';
+
+describe('dashboard locator', () => {
+ beforeEach(() => {
+ // @ts-ignore
+ hashedItemStore.storage = mockStorage;
+ });
+
+ test('creates a link to a saved dashboard', async () => {
+ const definition = new DashboardAppLocatorDefinition({
+ useHashedUrl: false,
+ getDashboardFilterFields: async (dashboardId: string) => [],
+ });
+ const location = await definition.getLocation({});
+
+ expect(location).toMatchObject({
+ app: 'dashboards',
+ path: '#/create?_a=()&_g=()',
+ state: {},
+ });
+ });
+
+ test('creates a link with global time range set up', async () => {
+ const definition = new DashboardAppLocatorDefinition({
+ useHashedUrl: false,
+ getDashboardFilterFields: async (dashboardId: string) => [],
+ });
+ const location = await definition.getLocation({
+ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
+ });
+
+ expect(location).toMatchObject({
+ app: 'dashboards',
+ path: '#/create?_a=()&_g=(time:(from:now-15m,mode:relative,to:now))',
+ state: {},
+ });
+ });
+
+ test('creates a link with filters, time range, refresh interval and query to a saved object', async () => {
+ const definition = new DashboardAppLocatorDefinition({
+ useHashedUrl: false,
+ getDashboardFilterFields: async (dashboardId: string) => [],
+ });
+ const location = await definition.getLocation({
+ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
+ refreshInterval: { pause: false, value: 300 },
+ dashboardId: '123',
+ filters: [
+ {
+ meta: {
+ alias: null,
+ disabled: false,
+ negate: false,
+ },
+ query: { query: 'hi' },
+ },
+ {
+ meta: {
+ alias: null,
+ disabled: false,
+ negate: false,
+ },
+ query: { query: 'hi' },
+ $state: {
+ store: esFilters.FilterStateStore.GLOBAL_STATE,
+ },
+ },
+ ],
+ query: { query: 'bye', language: 'kuery' },
+ });
+
+ expect(location).toMatchObject({
+ app: 'dashboards',
+ path: `#/view/123?_a=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),query:(language:kuery,query:bye))&_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))`,
+ state: {},
+ });
+ });
+
+ test('searchSessionId', async () => {
+ const definition = new DashboardAppLocatorDefinition({
+ useHashedUrl: false,
+ getDashboardFilterFields: async (dashboardId: string) => [],
+ });
+ const location = await definition.getLocation({
+ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
+ refreshInterval: { pause: false, value: 300 },
+ dashboardId: '123',
+ filters: [],
+ query: { query: 'bye', language: 'kuery' },
+ searchSessionId: '__sessionSearchId__',
+ });
+
+ expect(location).toMatchObject({
+ app: 'dashboards',
+ path: `#/view/123?_a=(filters:!(),query:(language:kuery,query:bye))&_g=(filters:!(),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))&searchSessionId=__sessionSearchId__`,
+ state: {},
+ });
+ });
+
+ test('savedQuery', async () => {
+ const definition = new DashboardAppLocatorDefinition({
+ useHashedUrl: false,
+ getDashboardFilterFields: async (dashboardId: string) => [],
+ });
+ const location = await definition.getLocation({
+ savedQuery: '__savedQueryId__',
+ });
+
+ expect(location).toMatchObject({
+ app: 'dashboards',
+ path: `#/create?_a=(savedQuery:__savedQueryId__)&_g=()`,
+ state: {},
+ });
+ expect(location.path).toContain('__savedQueryId__');
+ });
+
+ test('panels', async () => {
+ const definition = new DashboardAppLocatorDefinition({
+ useHashedUrl: false,
+ getDashboardFilterFields: async (dashboardId: string) => [],
+ });
+ const location = await definition.getLocation({
+ panels: [{ fakePanelContent: 'fakePanelContent' }] as any,
+ });
+
+ expect(location).toMatchObject({
+ app: 'dashboards',
+ path: `#/create?_a=(panels:!((fakePanelContent:fakePanelContent)))&_g=()`,
+ state: {},
+ });
+ });
+
+ test('if no useHash setting is given, uses the one was start services', async () => {
+ const definition = new DashboardAppLocatorDefinition({
+ useHashedUrl: true,
+ getDashboardFilterFields: async (dashboardId: string) => [],
+ });
+ const location = await definition.getLocation({
+ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
+ });
+
+ expect(location.path.indexOf('relative')).toBe(-1);
+ });
+
+ test('can override a false useHash ui setting', async () => {
+ const definition = new DashboardAppLocatorDefinition({
+ useHashedUrl: false,
+ getDashboardFilterFields: async (dashboardId: string) => [],
+ });
+ const location = await definition.getLocation({
+ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
+ useHash: true,
+ });
+
+ expect(location.path.indexOf('relative')).toBe(-1);
+ });
+
+ test('can override a true useHash ui setting', async () => {
+ const definition = new DashboardAppLocatorDefinition({
+ useHashedUrl: true,
+ getDashboardFilterFields: async (dashboardId: string) => [],
+ });
+ const location = await definition.getLocation({
+ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
+ useHash: false,
+ });
+
+ expect(location.path.indexOf('relative')).toBeGreaterThan(1);
+ });
+
+ describe('preserving saved filters', () => {
+ const savedFilter1 = {
+ meta: {
+ alias: null,
+ disabled: false,
+ negate: false,
+ },
+ query: { query: 'savedfilter1' },
+ };
+
+ const savedFilter2 = {
+ meta: {
+ alias: null,
+ disabled: false,
+ negate: false,
+ },
+ query: { query: 'savedfilter2' },
+ };
+
+ const appliedFilter = {
+ meta: {
+ alias: null,
+ disabled: false,
+ negate: false,
+ },
+ query: { query: 'appliedfilter' },
+ };
+
+ test('attaches filters from destination dashboard', async () => {
+ const definition = new DashboardAppLocatorDefinition({
+ useHashedUrl: false,
+ getDashboardFilterFields: async (dashboardId: string) => {
+ return dashboardId === 'dashboard1'
+ ? [savedFilter1]
+ : dashboardId === 'dashboard2'
+ ? [savedFilter2]
+ : [];
+ },
+ });
+
+ const location1 = await definition.getLocation({
+ dashboardId: 'dashboard1',
+ filters: [appliedFilter],
+ });
+
+ expect(location1.path).toEqual(expect.stringContaining('query:savedfilter1'));
+ expect(location1.path).toEqual(expect.stringContaining('query:appliedfilter'));
+
+ const location2 = await definition.getLocation({
+ dashboardId: 'dashboard2',
+ filters: [appliedFilter],
+ });
+
+ expect(location2.path).toEqual(expect.stringContaining('query:savedfilter2'));
+ expect(location2.path).toEqual(expect.stringContaining('query:appliedfilter'));
+ });
+
+ test("doesn't fail if can't retrieve filters from destination dashboard", async () => {
+ const definition = new DashboardAppLocatorDefinition({
+ useHashedUrl: false,
+ getDashboardFilterFields: async (dashboardId: string) => {
+ if (dashboardId === 'dashboard1') {
+ throw new Error('Not found');
+ }
+ return [];
+ },
+ });
+
+ const location = await definition.getLocation({
+ dashboardId: 'dashboard1',
+ filters: [appliedFilter],
+ });
+
+ expect(location.path).not.toEqual(expect.stringContaining('query:savedfilter1'));
+ expect(location.path).toEqual(expect.stringContaining('query:appliedfilter'));
+ });
+
+ test('can enforce empty filters', async () => {
+ const definition = new DashboardAppLocatorDefinition({
+ useHashedUrl: false,
+ getDashboardFilterFields: async (dashboardId: string) => {
+ if (dashboardId === 'dashboard1') {
+ return [savedFilter1];
+ }
+ return [];
+ },
+ });
+
+ const location = await definition.getLocation({
+ dashboardId: 'dashboard1',
+ filters: [],
+ preserveSavedFilters: false,
+ });
+
+ expect(location.path).not.toEqual(expect.stringContaining('query:savedfilter1'));
+ expect(location.path).not.toEqual(expect.stringContaining('query:appliedfilter'));
+ expect(location.path).toMatchInlineSnapshot(
+ `"#/view/dashboard1?_a=(filters:!())&_g=(filters:!())"`
+ );
+ });
+
+ test('no filters in result url if no filters applied', async () => {
+ const definition = new DashboardAppLocatorDefinition({
+ useHashedUrl: false,
+ getDashboardFilterFields: async (dashboardId: string) => {
+ if (dashboardId === 'dashboard1') {
+ return [savedFilter1];
+ }
+ return [];
+ },
+ });
+
+ const location = await definition.getLocation({
+ dashboardId: 'dashboard1',
+ });
+
+ expect(location.path).not.toEqual(expect.stringContaining('filters'));
+ expect(location.path).toMatchInlineSnapshot(`"#/view/dashboard1?_a=()&_g=()"`);
+ });
+
+ test('can turn off preserving filters', async () => {
+ const definition = new DashboardAppLocatorDefinition({
+ useHashedUrl: false,
+ getDashboardFilterFields: async (dashboardId: string) => {
+ if (dashboardId === 'dashboard1') {
+ return [savedFilter1];
+ }
+ return [];
+ },
+ });
+
+ const location = await definition.getLocation({
+ dashboardId: 'dashboard1',
+ filters: [appliedFilter],
+ preserveSavedFilters: false,
+ });
+
+ expect(location.path).not.toEqual(expect.stringContaining('query:savedfilter1'));
+ expect(location.path).toEqual(expect.stringContaining('query:appliedfilter'));
+ });
+ });
+});
diff --git a/src/plugins/dashboard/public/locator.ts b/src/plugins/dashboard/public/locator.ts
new file mode 100644
index 0000000000000..e154351819ee9
--- /dev/null
+++ b/src/plugins/dashboard/public/locator.ts
@@ -0,0 +1,160 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { SerializableState } from 'src/plugins/kibana_utils/common';
+import type { TimeRange, Filter, Query, QueryState, RefreshInterval } from '../../data/public';
+import type { LocatorDefinition, LocatorPublic } from '../../share/public';
+import type { SavedDashboardPanel } from '../common/types';
+import { esFilters } from '../../data/public';
+import { setStateToKbnUrl } from '../../kibana_utils/public';
+import { ViewMode } from '../../embeddable/public';
+import { DashboardConstants } from './dashboard_constants';
+
+const cleanEmptyKeys = (stateObj: Record