@@ -141,7 +144,7 @@ export function CopyToDashboardModal({
id="new-dashboard-option"
name="dashboard-option"
disabled={!dashboardId}
- label={dashboardCopyToDashboardAction.getNewDashboardOption()}
+ label={dashboardCopyToDashboardActionStrings.getNewDashboardOption()}
onChange={() => setDashboardOption('new')}
/>
@@ -155,7 +158,7 @@ export function CopyToDashboardModal({
closeModal()}>
- {dashboardCopyToDashboardAction.getCancelButtonName()}
+ {dashboardCopyToDashboardActionStrings.getCancelButtonName()}
- {dashboardCopyToDashboardAction.getAcceptButtonName()}
+ {dashboardCopyToDashboardActionStrings.getAcceptButtonName()}
diff --git a/src/plugins/dashboard/public/application/actions/expand_panel_action.test.tsx b/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.test.tsx
similarity index 89%
rename from src/plugins/dashboard/public/application/actions/expand_panel_action.test.tsx
rename to src/plugins/dashboard/public/dashboard_actions/expand_panel_action.test.tsx
index dd28d0060c32..44a1b2d4828d 100644
--- a/src/plugins/dashboard/public/application/actions/expand_panel_action.test.tsx
+++ b/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.test.tsx
@@ -7,8 +7,8 @@
*/
import { ExpandPanelAction } from './expand_panel_action';
-import { DashboardContainer } from '../embeddable/dashboard_container';
-import { getSampleDashboardInput, getSampleDashboardPanel } from '../test_helpers';
+import { getSampleDashboardInput, getSampleDashboardPanel } from '../mocks';
+import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
import { isErrorEmbeddable } from '@kbn/embeddable-plugin/public';
import {
@@ -19,7 +19,7 @@ import {
CONTACT_CARD_EMBEDDABLE,
} from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables';
-import { pluginServices } from '../../services/plugin_services';
+import { pluginServices } from '../services/plugin_services';
let container: DashboardContainer;
let embeddable: ContactCardEmbeddable;
@@ -40,6 +40,7 @@ beforeEach(async () => {
});
container = new DashboardContainer(input);
+ await container.untilInitialized();
const contactCardEmbeddable = await container.addNewEmbeddable<
ContactCardEmbeddableInput,
@@ -59,11 +60,11 @@ beforeEach(async () => {
test('Sets the embeddable expanded panel id on the parent', async () => {
const expandPanelAction = new ExpandPanelAction();
- expect(container.getInput().expandedPanelId).toBeUndefined();
+ expect(container.getExpandedPanelId()).toBeUndefined();
expandPanelAction.execute({ embeddable });
- expect(container.getInput().expandedPanelId).toBe(embeddable.id);
+ expect(container.getExpandedPanelId()).toBe(embeddable.id);
});
test('Is not compatible when embeddable is not in a dashboard container', async () => {
diff --git a/src/plugins/dashboard/public/application/actions/expand_panel_action.tsx b/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.tsx
similarity index 75%
rename from src/plugins/dashboard/public/application/actions/expand_panel_action.tsx
rename to src/plugins/dashboard/public/dashboard_actions/expand_panel_action.tsx
index 79ab109ddce5..0d3dd592dcc3 100644
--- a/src/plugins/dashboard/public/application/actions/expand_panel_action.tsx
+++ b/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.tsx
@@ -9,9 +9,8 @@
import type { IEmbeddable } from '@kbn/embeddable-plugin/public';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
-import { DashboardContainerInput, DASHBOARD_CONTAINER_TYPE } from '../..';
-import { dashboardExpandPanelAction } from '../../dashboard_strings';
-import { type DashboardContainer } from '../embeddable';
+import { DASHBOARD_CONTAINER_TYPE, type DashboardContainer } from '../dashboard_container';
+import { dashboardExpandPanelActionStrings } from './_dashboard_actions_strings';
export const ACTION_EXPAND_PANEL = 'togglePanel';
@@ -24,9 +23,7 @@ function isExpanded(embeddable: IEmbeddable) {
throw new IncompatibleActionError();
}
- return (
- embeddable.id === (embeddable.parent.getInput() as DashboardContainerInput).expandedPanelId
- );
+ return embeddable.id === (embeddable.parent as DashboardContainer).getExpandedPanelId();
}
export interface ExpandPanelActionContext {
@@ -46,16 +43,15 @@ export class ExpandPanelAction implements Action
{
}
return isExpanded(embeddable)
- ? dashboardExpandPanelAction.getMinimizeTitle()
- : dashboardExpandPanelAction.getMaximizeTitle();
+ ? dashboardExpandPanelActionStrings.getMinimizeTitle()
+ : dashboardExpandPanelActionStrings.getMaximizeTitle();
}
public getIconType({ embeddable }: ExpandPanelActionContext) {
if (!embeddable.parent || !isDashboard(embeddable.parent)) {
throw new IncompatibleActionError();
}
- // TODO: use 'minimize' when an eui-icon of such is available.
- return isExpanded(embeddable) ? 'expand' : 'expand';
+ return isExpanded(embeddable) ? 'minimize' : 'expand';
}
public async isCompatible({ embeddable }: ExpandPanelActionContext) {
@@ -67,8 +63,6 @@ export class ExpandPanelAction implements Action {
throw new IncompatibleActionError();
}
const newValue = isExpanded(embeddable) ? undefined : embeddable.id;
- embeddable.parent.updateInput({
- expandedPanelId: newValue,
- });
+ (embeddable.parent as DashboardContainer).setExpandedPanelId(newValue);
}
}
diff --git a/src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx b/src/plugins/dashboard/public/dashboard_actions/export_csv_action.test.tsx
similarity index 94%
rename from src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx
rename to src/plugins/dashboard/public/dashboard_actions/export_csv_action.test.tsx
index ec63c3e0ec7d..265d34992689 100644
--- a/src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx
+++ b/src/plugins/dashboard/public/dashboard_actions/export_csv_action.test.tsx
@@ -6,11 +6,6 @@
* Side Public License, v 1.
*/
-import { CoreStart } from '@kbn/core/public';
-import { isErrorEmbeddable, IContainer, ErrorEmbeddable } from '@kbn/embeddable-plugin/public';
-
-import { DashboardContainer } from '../embeddable/dashboard_container';
-import { getSampleDashboardInput, getSampleDashboardPanel } from '../test_helpers';
import {
ContactCardEmbeddable,
ContactCardEmbeddableInput,
@@ -18,10 +13,15 @@ import {
ContactCardExportableEmbeddableFactory,
CONTACT_CARD_EXPORTABLE_EMBEDDABLE,
} from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables';
+import { CoreStart } from '@kbn/core/public';
import { coreMock } from '@kbn/core/public/mocks';
-import { ExportCSVAction } from './export_csv_action';
import { LINE_FEED_CHARACTER } from '@kbn/data-plugin/common/exports/export_csv';
-import { pluginServices } from '../../services/plugin_services';
+import { isErrorEmbeddable, IContainer, ErrorEmbeddable } from '@kbn/embeddable-plugin/public';
+
+import { ExportCSVAction } from './export_csv_action';
+import { pluginServices } from '../services/plugin_services';
+import { getSampleDashboardInput, getSampleDashboardPanel } from '../mocks';
+import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
describe('Export CSV action', () => {
let container: DashboardContainer;
@@ -54,6 +54,7 @@ describe('Export CSV action', () => {
},
});
container = new DashboardContainer(input);
+ await container.untilInitialized();
const contactCardEmbeddable = await container.addNewEmbeddable<
ContactCardEmbeddableInput,
diff --git a/src/plugins/dashboard/public/application/actions/export_csv_action.tsx b/src/plugins/dashboard/public/dashboard_actions/export_csv_action.tsx
similarity index 92%
rename from src/plugins/dashboard/public/application/actions/export_csv_action.tsx
rename to src/plugins/dashboard/public/dashboard_actions/export_csv_action.tsx
index fa90da055005..11ef135c7657 100644
--- a/src/plugins/dashboard/public/application/actions/export_csv_action.tsx
+++ b/src/plugins/dashboard/public/dashboard_actions/export_csv_action.tsx
@@ -13,8 +13,8 @@ import { downloadMultipleAs } from '@kbn/share-plugin/public';
import { FormatFactory } from '@kbn/field-formats-plugin/common';
import type { Adapters, IEmbeddable } from '@kbn/embeddable-plugin/public';
-import { dashboardExportCsvAction } from '../../dashboard_strings';
-import { pluginServices } from '../../services/plugin_services';
+import { dashboardExportCsvActionStrings } from './_dashboard_actions_strings';
+import { pluginServices } from '../services/plugin_services';
export const ACTION_EXPORT_CSV = 'ACTION_EXPORT_CSV';
@@ -48,7 +48,7 @@ export class ExportCSVAction implements Action {
}
public readonly getDisplayName = (context: ExportContext): string =>
- dashboardExportCsvAction.getDisplayName();
+ dashboardExportCsvActionStrings.getDisplayName();
public async isCompatible(context: ExportContext): Promise {
return !!this.hasDatatableContent(context.embeddable?.getInspectorAdapters?.());
@@ -89,7 +89,7 @@ export class ExportCSVAction implements Action {
// skip empty datatables
if (datatable) {
const postFix = datatables.length > 1 ? `-${i + 1}` : '';
- const untitledFilename = dashboardExportCsvAction.getUntitledFilename();
+ const untitledFilename = dashboardExportCsvActionStrings.getUntitledFilename();
memo[`${context!.embeddable!.getTitle() || untitledFilename}${postFix}.csv`] = {
content: exporters.datatableToCSV(datatable, {
diff --git a/src/plugins/dashboard/public/application/actions/filters_notification_action.test.tsx b/src/plugins/dashboard/public/dashboard_actions/filters_notification_action.test.tsx
similarity index 93%
rename from src/plugins/dashboard/public/application/actions/filters_notification_action.test.tsx
rename to src/plugins/dashboard/public/dashboard_actions/filters_notification_action.test.tsx
index be9dc25f69fb..e864e35d5ad3 100644
--- a/src/plugins/dashboard/public/application/actions/filters_notification_action.test.tsx
+++ b/src/plugins/dashboard/public/dashboard_actions/filters_notification_action.test.tsx
@@ -17,9 +17,9 @@ import {
import { type Query, type AggregateQuery, Filter } from '@kbn/es-query';
import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks';
-import { getSampleDashboardInput } from '../test_helpers';
-import { pluginServices } from '../../services/plugin_services';
-import { DashboardContainer } from '../embeddable/dashboard_container';
+import { getSampleDashboardInput } from '../mocks';
+import { pluginServices } from '../services/plugin_services';
+import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
import { FiltersNotificationAction } from './filters_notification_action';
const mockEmbeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
@@ -52,6 +52,7 @@ const getMockPhraseFilter = (key: string, value: string) => {
const buildEmbeddable = async (input?: Partial) => {
const container = new DashboardContainer(getSampleDashboardInput());
+ await container.untilInitialized();
const contactCardEmbeddable = await container.addNewEmbeddable<
ContactCardEmbeddableInput,
ContactCardEmbeddableOutput,
diff --git a/src/plugins/dashboard/public/application/actions/filters_notification_action.tsx b/src/plugins/dashboard/public/dashboard_actions/filters_notification_action.tsx
similarity index 94%
rename from src/plugins/dashboard/public/application/actions/filters_notification_action.tsx
rename to src/plugins/dashboard/public/dashboard_actions/filters_notification_action.tsx
index b7ee2311ebd1..d7c6746c0c6e 100644
--- a/src/plugins/dashboard/public/application/actions/filters_notification_action.tsx
+++ b/src/plugins/dashboard/public/dashboard_actions/filters_notification_action.tsx
@@ -18,8 +18,8 @@ import { type AggregateQuery } from '@kbn/es-query';
import { I18nProvider } from '@kbn/i18n-react';
import { FiltersNotificationPopover } from './filters_notification_popover';
-import { dashboardFilterNotificationAction } from '../../dashboard_strings';
-import { pluginServices } from '../../services/plugin_services';
+import { dashboardFilterNotificationActionStrings } from './_dashboard_actions_strings';
+import { pluginServices } from '../services/plugin_services';
export const BADGE_FILTERS_NOTIFICATION = 'ACTION_FILTERS_NOTIFICATION';
@@ -32,7 +32,7 @@ export class FiltersNotificationAction implements Action {
+describe('filters notification popover', () => {
const mockEmbeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
.fn()
@@ -41,6 +41,7 @@ describe('LibraryNotificationPopover', () => {
beforeEach(async () => {
container = new DashboardContainer(getSampleDashboardInput());
+ await container.untilInitialized();
const contactCardEmbeddable = await container.addNewEmbeddable<
ContactCardEmbeddableInput,
ContactCardEmbeddableOutput,
diff --git a/src/plugins/dashboard/public/application/actions/filters_notification_popover.tsx b/src/plugins/dashboard/public/dashboard_actions/filters_notification_popover.tsx
similarity index 93%
rename from src/plugins/dashboard/public/application/actions/filters_notification_popover.tsx
rename to src/plugins/dashboard/public/dashboard_actions/filters_notification_popover.tsx
index 974c7280f896..636c88d56347 100644
--- a/src/plugins/dashboard/public/application/actions/filters_notification_popover.tsx
+++ b/src/plugins/dashboard/public/dashboard_actions/filters_notification_popover.tsx
@@ -19,7 +19,7 @@ import {
} from '@elastic/eui';
import { EditPanelAction } from '@kbn/embeddable-plugin/public';
-import { dashboardFilterNotificationAction } from '../../dashboard_strings';
+import { dashboardFilterNotificationActionStrings } from './_dashboard_actions_strings';
import { FiltersNotificationActionContext } from './filters_notification_action';
import { FiltersNotificationPopoverContents } from './filters_notification_popover_contents';
@@ -73,7 +73,7 @@ export function FiltersNotificationPopover({
fill
onClick={() => editPanelAction.execute({ embeddable })}
>
- {dashboardFilterNotificationAction.getEditButtonTitle()}
+ {dashboardFilterNotificationActionStrings.getEditButtonTitle()}
diff --git a/src/plugins/dashboard/public/application/actions/filters_notification_popover_contents.tsx b/src/plugins/dashboard/public/dashboard_actions/filters_notification_popover_contents.tsx
similarity index 90%
rename from src/plugins/dashboard/public/application/actions/filters_notification_popover_contents.tsx
rename to src/plugins/dashboard/public/dashboard_actions/filters_notification_popover_contents.tsx
index b3c37f40d6c6..9b9b34543127 100644
--- a/src/plugins/dashboard/public/application/actions/filters_notification_popover_contents.tsx
+++ b/src/plugins/dashboard/public/dashboard_actions/filters_notification_popover_contents.tsx
@@ -21,8 +21,8 @@ import {
} from '@kbn/es-query';
import { FiltersNotificationActionContext } from './filters_notification_action';
-import { dashboardFilterNotificationAction } from '../../dashboard_strings';
-import { DashboardContainer } from '../embeddable';
+import { dashboardFilterNotificationActionStrings } from './_dashboard_actions_strings';
+import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
export interface FiltersNotificationProps {
context: FiltersNotificationActionContext;
@@ -72,14 +72,14 @@ export function FiltersNotificationPopoverContents({ context }: FiltersNotificat
>
{queryString !== '' && (
{queryString}
@@ -87,7 +87,7 @@ export function FiltersNotificationPopoverContents({ context }: FiltersNotificat
)}
{filters && filters.length > 0 && (
-
+
diff --git a/src/plugins/dashboard/public/application/actions/index.ts b/src/plugins/dashboard/public/dashboard_actions/index.ts
similarity index 98%
rename from src/plugins/dashboard/public/application/actions/index.ts
rename to src/plugins/dashboard/public/dashboard_actions/index.ts
index 7793c2803754..6317c10dda1a 100644
--- a/src/plugins/dashboard/public/application/actions/index.ts
+++ b/src/plugins/dashboard/public/dashboard_actions/index.ts
@@ -12,7 +12,7 @@ import { getSavedObjectFinder } from '@kbn/saved-objects-plugin/public';
import { ExportCSVAction } from './export_csv_action';
import { ClonePanelAction } from './clone_panel_action';
-import { DashboardStartDependencies } from '../../plugin';
+import { DashboardStartDependencies } from '../plugin';
import { ExpandPanelAction } from './expand_panel_action';
import { ReplacePanelAction } from './replace_panel_action';
import { AddToLibraryAction } from './add_to_library_action';
diff --git a/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx b/src/plugins/dashboard/public/dashboard_actions/library_notification_action.test.tsx
similarity index 93%
rename from src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx
rename to src/plugins/dashboard/public/dashboard_actions/library_notification_action.test.tsx
index f30cba538b8d..ed7f501f8329 100644
--- a/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx
+++ b/src/plugins/dashboard/public/dashboard_actions/library_notification_action.test.tsx
@@ -22,11 +22,11 @@ import {
} from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables';
import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks';
-import { getSampleDashboardInput } from '../test_helpers';
-import { pluginServices } from '../../services/plugin_services';
-import { DashboardContainer } from '../embeddable/dashboard_container';
+import { getSampleDashboardInput } from '../mocks';
+import { pluginServices } from '../services/plugin_services';
import { UnlinkFromLibraryAction } from './unlink_from_library_action';
import { LibraryNotificationAction } from './library_notification_action';
+import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
const mockEmbeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
@@ -44,6 +44,7 @@ beforeEach(async () => {
} as unknown as UnlinkFromLibraryAction;
container = new DashboardContainer(getSampleDashboardInput());
+ await container.untilInitialized();
const contactCardEmbeddable = await container.addNewEmbeddable<
ContactCardEmbeddableInput,
diff --git a/src/plugins/dashboard/public/application/actions/library_notification_action.tsx b/src/plugins/dashboard/public/dashboard_actions/library_notification_action.tsx
similarity index 92%
rename from src/plugins/dashboard/public/application/actions/library_notification_action.tsx
rename to src/plugins/dashboard/public/dashboard_actions/library_notification_action.tsx
index a05b78994b31..baf3ae63b33f 100644
--- a/src/plugins/dashboard/public/application/actions/library_notification_action.tsx
+++ b/src/plugins/dashboard/public/dashboard_actions/library_notification_action.tsx
@@ -14,13 +14,13 @@ import {
isErrorEmbeddable,
isReferenceOrValueEmbeddable,
} from '@kbn/embeddable-plugin/public';
-import { KibanaThemeProvider, reactToUiComponent } from '@kbn/kibana-react-plugin/public';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
+import { KibanaThemeProvider, reactToUiComponent } from '@kbn/kibana-react-plugin/public';
-import { pluginServices } from '../../services/plugin_services';
+import { pluginServices } from '../services/plugin_services';
import { UnlinkFromLibraryAction } from './unlink_from_library_action';
-import { dashboardLibraryNotification } from '../../dashboard_strings';
import { LibraryNotificationPopover } from './library_notification_popover';
+import { dashboardLibraryNotificationStrings } from './_dashboard_actions_strings';
export const ACTION_LIBRARY_NOTIFICATION = 'ACTION_LIBRARY_NOTIFICATION';
@@ -43,7 +43,7 @@ export class LibraryNotificationAction implements Action {
const mockEmbeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
@@ -38,6 +39,8 @@ describe('LibraryNotificationPopover', () => {
beforeEach(async () => {
container = new DashboardContainer(getSampleDashboardInput());
+ await container.untilInitialized();
+
const contactCardEmbeddable = await container.addNewEmbeddable<
ContactCardEmbeddableInput,
ContactCardEmbeddableOutput,
diff --git a/src/plugins/dashboard/public/application/actions/library_notification_popover.tsx b/src/plugins/dashboard/public/dashboard_actions/library_notification_popover.tsx
similarity index 90%
rename from src/plugins/dashboard/public/application/actions/library_notification_popover.tsx
rename to src/plugins/dashboard/public/dashboard_actions/library_notification_popover.tsx
index 38c8452eadde..21dd067885ed 100644
--- a/src/plugins/dashboard/public/application/actions/library_notification_popover.tsx
+++ b/src/plugins/dashboard/public/dashboard_actions/library_notification_popover.tsx
@@ -18,9 +18,9 @@ import {
EuiText,
} from '@elastic/eui';
-import { dashboardLibraryNotification } from '../../dashboard_strings';
import { UnlinkFromLibraryAction } from './unlink_from_library_action';
import { LibraryNotificationActionContext } from './library_notification_action';
+import { dashboardLibraryNotificationStrings } from './_dashboard_actions_strings';
export interface LibraryNotificationProps {
context: LibraryNotificationActionContext;
@@ -48,7 +48,7 @@ export function LibraryNotificationPopover({
iconType={icon}
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
data-test-subj={`embeddablePanelNotification-${id}`}
- aria-label={dashboardLibraryNotification.getPopoverAriaLabel()}
+ aria-label={dashboardLibraryNotificationStrings.getPopoverAriaLabel()}
/>
}
isOpen={isPopoverOpen}
@@ -58,7 +58,7 @@ export function LibraryNotificationPopover({
{displayName}
- {dashboardLibraryNotification.getTooltip()}
+ {dashboardLibraryNotificationStrings.getTooltip()}
diff --git a/src/plugins/dashboard/public/application/actions/open_replace_panel_flyout.tsx b/src/plugins/dashboard/public/dashboard_actions/open_replace_panel_flyout.tsx
similarity index 95%
rename from src/plugins/dashboard/public/application/actions/open_replace_panel_flyout.tsx
rename to src/plugins/dashboard/public/dashboard_actions/open_replace_panel_flyout.tsx
index 5322e56831ff..c4e81a17bb76 100644
--- a/src/plugins/dashboard/public/application/actions/open_replace_panel_flyout.tsx
+++ b/src/plugins/dashboard/public/dashboard_actions/open_replace_panel_flyout.tsx
@@ -17,7 +17,7 @@ import type {
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
import { ReplacePanelFlyout } from './replace_panel_flyout';
-import { pluginServices } from '../../services/plugin_services';
+import { pluginServices } from '../services/plugin_services';
export async function openReplacePanelFlyout(options: {
embeddable: IContainer;
diff --git a/src/plugins/dashboard/public/application/actions/replace_panel_action.test.tsx b/src/plugins/dashboard/public/dashboard_actions/replace_panel_action.test.tsx
similarity index 93%
rename from src/plugins/dashboard/public/application/actions/replace_panel_action.test.tsx
rename to src/plugins/dashboard/public/dashboard_actions/replace_panel_action.test.tsx
index 0a035d06d4fd..c5695b55072d 100644
--- a/src/plugins/dashboard/public/application/actions/replace_panel_action.test.tsx
+++ b/src/plugins/dashboard/public/dashboard_actions/replace_panel_action.test.tsx
@@ -6,11 +6,6 @@
* Side Public License, v 1.
*/
-import { ReplacePanelAction } from './replace_panel_action';
-import { DashboardContainer } from '../embeddable/dashboard_container';
-import { getSampleDashboardInput, getSampleDashboardPanel } from '../test_helpers';
-
-import { isErrorEmbeddable } from '@kbn/embeddable-plugin/public';
import {
ContactCardEmbeddable,
ContactCardEmbeddableFactory,
@@ -18,8 +13,12 @@ import {
ContactCardEmbeddableOutput,
CONTACT_CARD_EMBEDDABLE,
} from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables';
+import { isErrorEmbeddable } from '@kbn/embeddable-plugin/public';
-import { pluginServices } from '../../services/plugin_services';
+import { ReplacePanelAction } from './replace_panel_action';
+import { pluginServices } from '../services/plugin_services';
+import { getSampleDashboardInput, getSampleDashboardPanel } from '../mocks';
+import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
const mockEmbeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
@@ -38,6 +37,7 @@ beforeEach(async () => {
},
});
container = new DashboardContainer(input);
+ await container.untilInitialized();
const contactCardEmbeddable = await container.addNewEmbeddable<
ContactCardEmbeddableInput,
diff --git a/src/plugins/dashboard/public/application/actions/replace_panel_action.tsx b/src/plugins/dashboard/public/dashboard_actions/replace_panel_action.tsx
similarity index 89%
rename from src/plugins/dashboard/public/application/actions/replace_panel_action.tsx
rename to src/plugins/dashboard/public/dashboard_actions/replace_panel_action.tsx
index 52f6a345a181..d377be059a13 100644
--- a/src/plugins/dashboard/public/application/actions/replace_panel_action.tsx
+++ b/src/plugins/dashboard/public/dashboard_actions/replace_panel_action.tsx
@@ -8,10 +8,10 @@
import { type IEmbeddable, ViewMode } from '@kbn/embeddable-plugin/public';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
-import type { DashboardContainer } from '../embeddable';
+
import { openReplacePanelFlyout } from './open_replace_panel_flyout';
-import { dashboardReplacePanelAction } from '../../dashboard_strings';
-import { DASHBOARD_CONTAINER_TYPE } from '../../dashboard_constants';
+import { dashboardReplacePanelActionStrings } from './_dashboard_actions_strings';
+import { type DashboardContainer, DASHBOARD_CONTAINER_TYPE } from '../dashboard_container';
export const ACTION_REPLACE_PANEL = 'replacePanel';
@@ -34,7 +34,7 @@ export class ReplacePanelAction implements Action {
if (!embeddable.parent || !isDashboard(embeddable.parent)) {
throw new IncompatibleActionError();
}
- return dashboardReplacePanelAction.getDisplayName();
+ return dashboardReplacePanelActionStrings.getDisplayName();
}
public getIconType({ embeddable }: ReplacePanelActionContext) {
diff --git a/src/plugins/dashboard/public/application/actions/replace_panel_flyout.tsx b/src/plugins/dashboard/public/dashboard_actions/replace_panel_flyout.tsx
similarity index 90%
rename from src/plugins/dashboard/public/application/actions/replace_panel_flyout.tsx
rename to src/plugins/dashboard/public/dashboard_actions/replace_panel_flyout.tsx
index 6369ff82b821..d5d5c439a474 100644
--- a/src/plugins/dashboard/public/application/actions/replace_panel_flyout.tsx
+++ b/src/plugins/dashboard/public/dashboard_actions/replace_panel_flyout.tsx
@@ -8,7 +8,7 @@
import React from 'react';
import { EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui';
-import { Toast } from '@kbn/core/public';
+
import {
EmbeddableInput,
EmbeddableOutput,
@@ -16,9 +16,11 @@ import {
IEmbeddable,
SavedObjectEmbeddableInput,
} from '@kbn/embeddable-plugin/public';
-import { DashboardPanelState } from '../embeddable';
-import { dashboardReplacePanelAction } from '../../dashboard_strings';
-import { pluginServices } from '../../services/plugin_services';
+import { Toast } from '@kbn/core/public';
+
+import { DashboardPanelState } from '../../common';
+import { pluginServices } from '../services/plugin_services';
+import { dashboardReplacePanelActionStrings } from './_dashboard_actions_strings';
interface Props {
container: IContainer;
@@ -48,7 +50,7 @@ export class ReplacePanelFlyout extends React.Component {
}
this.lastToast = toasts.addSuccess({
- title: dashboardReplacePanelAction.getSuccessMessage(name),
+ title: dashboardReplacePanelActionStrings.getSuccessMessage(name),
'data-test-subj': 'addObjectToContainerSuccess',
});
};
@@ -93,7 +95,7 @@ export class ReplacePanelFlyout extends React.Component {
const SavedObjectFinder = this.props.savedObjectsFinder;
const savedObjectsFinder = (
diff --git a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx b/src/plugins/dashboard/public/dashboard_actions/unlink_from_library_action.test.tsx
similarity index 91%
rename from src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx
rename to src/plugins/dashboard/public/dashboard_actions/unlink_from_library_action.test.tsx
index 080c358a86fd..a0e557ce107a 100644
--- a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx
+++ b/src/plugins/dashboard/public/dashboard_actions/unlink_from_library_action.test.tsx
@@ -14,7 +14,6 @@ import {
ReferenceOrValueEmbeddable,
SavedObjectEmbeddableInput,
} from '@kbn/embeddable-plugin/public';
-import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks';
import {
ContactCardEmbeddable,
ContactCardEmbeddableFactory,
@@ -22,11 +21,13 @@ import {
ContactCardEmbeddableOutput,
CONTACT_CARD_EMBEDDABLE,
} from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables';
+import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks';
-import { getSampleDashboardInput } from '../test_helpers';
-import { pluginServices } from '../../services/plugin_services';
+import { getSampleDashboardInput } from '../mocks';
+import { DashboardPanelState } from '../../common';
+import { pluginServices } from '../services/plugin_services';
import { UnlinkFromLibraryAction } from './unlink_from_library_action';
-import { DashboardContainer } from '../embeddable/dashboard_container';
+import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
let container: DashboardContainer;
let embeddable: ContactCardEmbeddable & ReferenceOrValueEmbeddable;
@@ -38,6 +39,7 @@ pluginServices.getServices().embeddable.getEmbeddableFactory = jest
beforeEach(async () => {
container = new DashboardContainer(getSampleDashboardInput());
+ await container.untilInitialized();
const contactCardEmbeddable = await container.addNewEmbeddable<
ContactCardEmbeddableInput,
@@ -148,7 +150,11 @@ test('Unlink unwraps all attributes from savedObject', async () => {
(key) => !originalPanelKeySet.has(key)
);
expect(newPanelId).toBeDefined();
- const newPanel = container.getInput().panels[newPanelId!];
+ const newPanel = container.getInput().panels[newPanelId!] as DashboardPanelState & {
+ explicitInput: { attributes: unknown };
+ };
expect(newPanel.type).toEqual(embeddable.type);
- expect(newPanel.explicitInput.attributes).toEqual(complicatedAttributes);
+ expect((newPanel.explicitInput as { attributes: unknown }).attributes).toEqual(
+ complicatedAttributes
+ );
});
diff --git a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx b/src/plugins/dashboard/public/dashboard_actions/unlink_from_library_action.tsx
similarity index 86%
rename from src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx
rename to src/plugins/dashboard/public/dashboard_actions/unlink_from_library_action.tsx
index b7c53a78becc..1dafc89972a3 100644
--- a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx
+++ b/src/plugins/dashboard/public/dashboard_actions/unlink_from_library_action.tsx
@@ -16,10 +16,11 @@ import {
isReferenceOrValueEmbeddable,
} from '@kbn/embeddable-plugin/public';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
-import { dashboardUnlinkFromLibraryAction } from '../../dashboard_strings';
-import { type DashboardPanelState, type DashboardContainer } from '..';
-import { pluginServices } from '../../services/plugin_services';
-import { DASHBOARD_CONTAINER_TYPE } from '../../dashboard_constants';
+
+import { DashboardPanelState } from '../../common';
+import { pluginServices } from '../services/plugin_services';
+import { dashboardUnlinkFromLibraryActionStrings } from './_dashboard_actions_strings';
+import { type DashboardContainer, DASHBOARD_CONTAINER_TYPE } from '../dashboard_container';
export const ACTION_UNLINK_FROM_LIBRARY = 'unlinkFromLibrary';
@@ -44,7 +45,7 @@ export class UnlinkFromLibraryAction implements Action
+ i18n.translate('dashboard.dashboardPageTitle', {
+ defaultMessage: 'Dashboards',
+ });
+
+export const dashboardReadonlyBadge = {
+ getText: () =>
+ i18n.translate('dashboard.badge.readOnly.text', {
+ defaultMessage: 'Read only',
+ }),
+ getTooltip: () =>
+ i18n.translate('dashboard.badge.readOnly.tooltip', {
+ defaultMessage: 'Unable to save dashboards',
+ }),
+};
+
+/**
+ * @param title {string} the current title of the dashboard
+ * @param viewMode {DashboardViewMode} the current mode. If in editing state, prepends 'Editing ' to the title.
+ * @returns {string} A title to display to the user based on the above parameters.
+ */
+export function getDashboardTitle(title: string, viewMode: ViewMode, isNew: boolean): string {
+ const isEditMode = viewMode === ViewMode.EDIT;
+ const dashboardTitle = isNew ? getNewDashboardTitle() : title;
+ return isEditMode
+ ? i18n.translate('dashboard.strings.dashboardEditTitle', {
+ defaultMessage: 'Editing {title}',
+ values: { title: dashboardTitle },
+ })
+ : dashboardTitle;
+}
+
+export const unsavedChangesBadgeStrings = {
+ getUnsavedChangedBadgeText: () =>
+ i18n.translate('dashboard.unsavedChangesBadge', {
+ defaultMessage: 'Unsaved changes',
+ }),
+};
+
+export const leaveConfirmStrings = {
+ getLeaveTitle: () =>
+ i18n.translate('dashboard.appLeaveConfirmModal.unsavedChangesTitle', {
+ defaultMessage: 'Unsaved changes',
+ }),
+ getLeaveSubtitle: () =>
+ i18n.translate('dashboard.appLeaveConfirmModal.unsavedChangesSubtitle', {
+ defaultMessage: 'Leave Dashboard with unsaved work?',
+ }),
+ getLeaveCancelButtonText: () =>
+ i18n.translate('dashboard.appLeaveConfirmModal.cancelButtonLabel', {
+ defaultMessage: 'Cancel',
+ }),
+};
+
+export const getCreateVisualizationButtonTitle = () =>
+ i18n.translate('dashboard.solutionToolbar.addPanelButtonLabel', {
+ defaultMessage: 'Create visualization',
+ });
+
+export const getNewDashboardTitle = () =>
+ i18n.translate('dashboard.savedDashboard.newDashboardTitle', {
+ defaultMessage: 'New Dashboard',
+ });
+
+export const getPanelAddedSuccessString = (savedObjectName: string) =>
+ i18n.translate('dashboard.addPanel.newEmbeddableAddedSuccessMessageTitle', {
+ defaultMessage: '{savedObjectName} was added',
+ values: {
+ savedObjectName,
+ },
+ });
+
+export const getDashboardURL404String = () =>
+ i18n.translate('dashboard.loadingError.dashboardNotFound', {
+ defaultMessage: 'The requested dashboard could not be found.',
+ });
+
+export const getPanelTooOldErrorString = () =>
+ i18n.translate('dashboard.loadURLError.PanelTooOld', {
+ defaultMessage: 'Cannot load panels from a URL created in a version older than 7.3',
+ });
+
+/*
+ Dashboard Listing Page
+*/
+export const discardConfirmStrings = {
+ getDiscardTitle: () =>
+ i18n.translate('dashboard.discardChangesConfirmModal.discardChangesTitle', {
+ defaultMessage: 'Discard changes to dashboard?',
+ }),
+ getDiscardSubtitle: () =>
+ i18n.translate('dashboard.discardChangesConfirmModal.discardChangesDescription', {
+ defaultMessage: `Once you discard your changes, there's no getting them back.`,
+ }),
+ getDiscardConfirmButtonText: () =>
+ i18n.translate('dashboard.discardChangesConfirmModal.confirmButtonLabel', {
+ defaultMessage: 'Discard changes',
+ }),
+ getDiscardCancelButtonText: () =>
+ i18n.translate('dashboard.discardChangesConfirmModal.cancelButtonLabel', {
+ defaultMessage: 'Cancel',
+ }),
+};
+
+export const createConfirmStrings = {
+ getCreateTitle: () =>
+ i18n.translate('dashboard.createConfirmModal.unsavedChangesTitle', {
+ defaultMessage: 'New dashboard already in progress',
+ }),
+ getCreateSubtitle: () =>
+ i18n.translate('dashboard.createConfirmModal.unsavedChangesSubtitle', {
+ defaultMessage: 'Continue editing or start over with a blank dashboard.',
+ }),
+ getStartOverButtonText: () =>
+ i18n.translate('dashboard.createConfirmModal.confirmButtonLabel', {
+ defaultMessage: 'Start over',
+ }),
+ getContinueButtonText: () =>
+ i18n.translate('dashboard.createConfirmModal.continueButtonLabel', {
+ defaultMessage: 'Continue editing',
+ }),
+ getCancelButtonText: () =>
+ i18n.translate('dashboard.createConfirmModal.cancelButtonLabel', {
+ defaultMessage: 'Cancel',
+ }),
+};
+
+export const dashboardListingErrorStrings = {
+ getErrorDeletingDashboardToast: () =>
+ i18n.translate('dashboard.deleteError.toastDescription', {
+ defaultMessage: 'Error encountered while deleting dashboard',
+ }),
+};
+
+export const dashboardListingTableStrings = {
+ getEntityName: () =>
+ i18n.translate('dashboard.listing.table.entityName', {
+ defaultMessage: 'dashboard',
+ }),
+ getEntityNamePlural: () =>
+ i18n.translate('dashboard.listing.table.entityNamePlural', {
+ defaultMessage: 'dashboards',
+ }),
+ getTableListTitle: () => getDashboardPageTitle(),
+};
+
+export const noItemsStrings = {
+ getReadonlyTitle: () =>
+ i18n.translate('dashboard.listing.readonlyNoItemsTitle', {
+ defaultMessage: 'No dashboards to view',
+ }),
+ getReadonlyBody: () =>
+ i18n.translate('dashboard.listing.readonlyNoItemsBody', {
+ defaultMessage: `There are no available dashboards. To change your permissions to view the dashboards in this space, contact your administrator.`,
+ }),
+ getReadEditTitle: () =>
+ i18n.translate('dashboard.listing.createNewDashboard.title', {
+ defaultMessage: 'Create your first dashboard',
+ }),
+ getReadEditInProgressTitle: () =>
+ i18n.translate('dashboard.listing.createNewDashboard.inProgressTitle', {
+ defaultMessage: 'Dashboard in progress',
+ }),
+ getReadEditDashboardDescription: () =>
+ i18n.translate('dashboard.listing.createNewDashboard.combineDataViewFromKibanaAppDescription', {
+ defaultMessage:
+ 'Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations.',
+ }),
+ getSampleDataLinkText: () =>
+ i18n.translate('dashboard.listing.createNewDashboard.sampleDataInstallLinkText', {
+ defaultMessage: `Add some sample data`,
+ }),
+ getCreateNewDashboardText: () =>
+ i18n.translate('dashboard.listing.createNewDashboard.createButtonLabel', {
+ defaultMessage: `Create a dashboard`,
+ }),
+};
+
+export const dashboardUnsavedListingStrings = {
+ getUnsavedChangesTitle: (plural = false) =>
+ i18n.translate('dashboard.listing.unsaved.unsavedChangesTitle', {
+ defaultMessage: 'You have unsaved changes in the following {dash}:',
+ values: {
+ dash: plural
+ ? dashboardListingTableStrings.getEntityNamePlural()
+ : dashboardListingTableStrings.getEntityName(),
+ },
+ }),
+ getLoadingTitle: () =>
+ i18n.translate('dashboard.listing.unsaved.loading', {
+ defaultMessage: 'Loading',
+ }),
+ getEditAriaLabel: (title: string) =>
+ i18n.translate('dashboard.listing.unsaved.editAria', {
+ defaultMessage: 'Continue editing {title}',
+ values: { title },
+ }),
+ getEditTitle: () =>
+ i18n.translate('dashboard.listing.unsaved.editTitle', {
+ defaultMessage: 'Continue editing',
+ }),
+ getDiscardAriaLabel: (title: string) =>
+ i18n.translate('dashboard.listing.unsaved.discardAria', {
+ defaultMessage: 'Discard changes to {title}',
+ values: { title },
+ }),
+ getDiscardTitle: () =>
+ i18n.translate('dashboard.listing.unsaved.discardTitle', {
+ defaultMessage: 'Discard changes',
+ }),
+};
+
+/*
+ Share Modal
+*/
+export const shareModalStrings = {
+ getTopMenuCheckbox: () =>
+ i18n.translate('dashboard.embedUrlParamExtension.topMenu', {
+ defaultMessage: 'Top menu',
+ }),
+ getQueryCheckbox: () =>
+ i18n.translate('dashboard.embedUrlParamExtension.query', {
+ defaultMessage: 'Query',
+ }),
+ getTimeFilterCheckbox: () =>
+ i18n.translate('dashboard.embedUrlParamExtension.timeFilter', {
+ defaultMessage: 'Time filter',
+ }),
+ getFilterBarCheckbox: () =>
+ i18n.translate('dashboard.embedUrlParamExtension.filterBar', {
+ defaultMessage: 'Filter bar',
+ }),
+ getCheckboxLegend: () =>
+ i18n.translate('dashboard.embedUrlParamExtension.include', {
+ defaultMessage: 'Include',
+ }),
+ getSnapshotShareWarning: () =>
+ i18n.translate('dashboard.snapshotShare.longUrlWarning', {
+ defaultMessage:
+ 'One or more panels on this dashboard have changed. Before you generate a snapshot, save the dashboard.',
+ }),
+};
+
+/*
+ Dashboard Top Nav
+*/
+export const getDashboardBreadcrumb = () =>
+ i18n.translate('dashboard.dashboardAppBreadcrumbsTitle', {
+ defaultMessage: 'Dashboard',
+ });
+
+export const topNavStrings = {
+ fullScreen: {
+ label: i18n.translate('dashboard.topNave.fullScreenButtonAriaLabel', {
+ defaultMessage: 'full screen',
+ }),
+
+ description: i18n.translate('dashboard.topNave.fullScreenConfigDescription', {
+ defaultMessage: 'Full Screen Mode',
+ }),
+ },
+ labs: {
+ label: i18n.translate('dashboard.topNav.labsButtonAriaLabel', {
+ defaultMessage: 'labs',
+ }),
+ description: i18n.translate('dashboard.topNav.labsConfigDescription', {
+ defaultMessage: 'Labs',
+ }),
+ },
+ edit: {
+ label: i18n.translate('dashboard.topNave.editButtonAriaLabel', {
+ defaultMessage: 'edit',
+ }),
+ description: i18n.translate('dashboard.topNave.editConfigDescription', {
+ defaultMessage: 'Switch to edit mode',
+ }),
+ },
+ quickSave: {
+ label: i18n.translate('dashboard.topNave.saveButtonAriaLabel', {
+ defaultMessage: 'save',
+ }),
+ description: i18n.translate('dashboard.topNave.saveConfigDescription', {
+ defaultMessage: 'Quick save your dashboard without any prompts',
+ }),
+ },
+ saveAs: {
+ label: i18n.translate('dashboard.topNave.saveAsButtonAriaLabel', {
+ defaultMessage: 'save as',
+ }),
+ description: i18n.translate('dashboard.topNave.saveAsConfigDescription', {
+ defaultMessage: 'Save as a new dashboard',
+ }),
+ },
+ switchToViewMode: {
+ label: i18n.translate('dashboard.topNave.cancelButtonAriaLabel', {
+ defaultMessage: 'Switch to view mode',
+ }),
+ description: i18n.translate('dashboard.topNave.viewConfigDescription', {
+ defaultMessage: 'Switch to view-only mode',
+ }),
+ },
+ share: {
+ label: i18n.translate('dashboard.topNave.shareButtonAriaLabel', {
+ defaultMessage: 'share',
+ }),
+ description: i18n.translate('dashboard.topNave.shareConfigDescription', {
+ defaultMessage: 'Share Dashboard',
+ }),
+ },
+ options: {
+ label: i18n.translate('dashboard.topNave.optionsButtonAriaLabel', {
+ defaultMessage: 'options',
+ }),
+ description: i18n.translate('dashboard.topNave.optionsConfigDescription', {
+ defaultMessage: 'Options',
+ }),
+ },
+ clone: {
+ label: i18n.translate('dashboard.topNave.cloneButtonAriaLabel', {
+ defaultMessage: 'clone',
+ }),
+ description: i18n.translate('dashboard.topNave.cloneConfigDescription', {
+ defaultMessage: 'Create a copy of your dashboard',
+ }),
+ },
+};
diff --git a/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx b/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx
new file mode 100644
index 000000000000..a89c35ced6fa
--- /dev/null
+++ b/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx
@@ -0,0 +1,192 @@
+/*
+ * 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 { History } from 'history';
+import useMount from 'react-use/lib/useMount';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+
+import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
+import { createKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public';
+
+import {
+ DashboardAppNoDataPage,
+ isDashboardAppInNoDataState,
+} from './no_data/dashboard_app_no_data';
+import {
+ loadAndRemoveDashboardState,
+ startSyncingDashboardUrlState,
+} from './url/sync_dashboard_url_state';
+import {
+ getSessionURLObservable,
+ getSearchSessionIdFromURL,
+ removeSearchSessionIdFromURL,
+ createSessionRestorationDataProvider,
+} from './url/search_sessions_integration';
+import { DASHBOARD_APP_ID } from '../dashboard_constants';
+import { pluginServices } from '../services/plugin_services';
+import { DashboardTopNav } from './top_nav/dashboard_top_nav';
+import type { DashboardContainer } from '../dashboard_container';
+import { type DashboardEmbedSettings, DashboardRedirect } from './types';
+import { useDashboardMountContext } from './hooks/dashboard_mount_context';
+import { useDashboardOutcomeValidation } from './hooks/use_dashboard_outcome_validation';
+import DashboardContainerRenderer from '../dashboard_container/dashboard_container_renderer';
+import { loadDashboardHistoryLocationState } from './locator/load_dashboard_history_location_state';
+import type { DashboardCreationOptions } from '../dashboard_container/embeddable/dashboard_container_factory';
+
+export interface DashboardAppProps {
+ history: History;
+ savedDashboardId?: string;
+ redirectTo: DashboardRedirect;
+ embedSettings?: DashboardEmbedSettings;
+}
+
+export function DashboardApp({
+ savedDashboardId,
+ embedSettings,
+ redirectTo,
+ history,
+}: DashboardAppProps) {
+ const [showNoDataPage, setShowNoDataPage] = useState(false);
+ useMount(() => {
+ (async () => setShowNoDataPage(await isDashboardAppInNoDataState()))();
+ });
+
+ const [dashboardContainer, setDashboardContainer] = useState(
+ undefined
+ );
+
+ /**
+ * Unpack & set up dashboard services
+ */
+ const {
+ coreContext: { executionContext },
+ embeddable: { getStateTransfer },
+ notifications: { toasts },
+ settings: { uiSettings },
+ data: { search },
+ } = pluginServices.getServices();
+ const incomingEmbeddable = getStateTransfer().getIncomingEmbeddablePackage(
+ DASHBOARD_APP_ID,
+ true
+ );
+ const { scopedHistory: getScopedHistory } = useDashboardMountContext();
+
+ useExecutionContext(executionContext, {
+ type: 'application',
+ page: 'app',
+ id: savedDashboardId || 'new',
+ });
+
+ const kbnUrlStateStorage = useMemo(
+ () =>
+ createKbnUrlStateStorage({
+ history,
+ useHash: uiSettings.get('state:storeInSessionStorage'),
+ ...withNotifyOnErrors(toasts),
+ }),
+ [toasts, history, uiSettings]
+ );
+
+ /**
+ * Clear search session when leaving dashboard route
+ */
+ useEffect(() => {
+ return () => {
+ search.session.clear();
+ };
+ }, [search.session]);
+
+ /**
+ * Validate saved object load outcome
+ */
+ const { validateOutcome, getLegacyConflictWarning } = useDashboardOutcomeValidation({
+ redirectTo,
+ });
+
+ /**
+ * Create options to pass into the dashboard renderer
+ */
+ const stateFromLocator = loadDashboardHistoryLocationState(getScopedHistory);
+ const getCreationOptions = useCallback((): DashboardCreationOptions => {
+ const initialUrlState = loadAndRemoveDashboardState(kbnUrlStateStorage);
+ const searchSessionIdFromURL = getSearchSessionIdFromURL(history);
+ return {
+ incomingEmbeddable,
+
+ // integrations
+ useControlGroupIntegration: true,
+ useSessionStorageIntegration: true,
+ useUnifiedSearchIntegration: true,
+ unifiedSearchSettings: {
+ kbnUrlStateStorage,
+ },
+ useSearchSessionsIntegration: true,
+ searchSessionSettings: {
+ createSessionRestorationDataProvider,
+ sessionIdToRestore: searchSessionIdFromURL,
+ sessionIdUrlChangeObservable: getSessionURLObservable(history),
+ getSearchSessionIdFromURL: () => getSearchSessionIdFromURL(history),
+ removeSessionIdFromUrl: () => removeSearchSessionIdFromURL(kbnUrlStateStorage),
+ },
+
+ // Override all state with URL + Locator input
+ overrideInput: {
+ // State loaded from the dashboard app URL and from the locator overrides all other dashboard state.
+ ...initialUrlState,
+ ...stateFromLocator,
+ },
+
+ validateLoadedSavedObject: validateOutcome,
+ };
+ }, [kbnUrlStateStorage, history, stateFromLocator, incomingEmbeddable, validateOutcome]);
+
+ /**
+ * Get the redux wrapper from the dashboard container. This is used to wrap the top nav so it can interact with the
+ * dashboard's redux state.
+ */
+ const DashboardReduxWrapper = useMemo(
+ () => dashboardContainer?.getReduxEmbeddableTools().Wrapper,
+ [dashboardContainer]
+ );
+
+ /**
+ * When the dashboard container is created, or re-created, start syncing dashboard state with the URL
+ */
+ useEffect(() => {
+ if (!dashboardContainer) return;
+ const { stopWatchingAppStateInUrl } = startSyncingDashboardUrlState({
+ kbnUrlStateStorage,
+ dashboardContainer,
+ });
+ return () => stopWatchingAppStateInUrl();
+ }, [dashboardContainer, kbnUrlStateStorage]);
+
+ return (
+ <>
+ {showNoDataPage && (
+ setShowNoDataPage(false)} />
+ )}
+ {!showNoDataPage && (
+ <>
+ {DashboardReduxWrapper && (
+
+
+
+ )}
+
+ {getLegacyConflictWarning?.()}
+ setDashboardContainer(container)}
+ />
+ >
+ )}
+ >
+ );
+}
diff --git a/src/plugins/dashboard/public/application/dashboard_router.tsx b/src/plugins/dashboard/public/dashboard_app/dashboard_router.tsx
similarity index 69%
rename from src/plugins/dashboard/public/application/dashboard_router.tsx
rename to src/plugins/dashboard/public/dashboard_app/dashboard_router.tsx
index 9ff31f266468..69dffa5bd616 100644
--- a/src/plugins/dashboard/public/application/dashboard_router.tsx
+++ b/src/plugins/dashboard/public/dashboard_app/dashboard_router.tsx
@@ -6,10 +6,10 @@
* Side Public License, v 1.
*/
-import './index.scss';
+import './_dashboard_app.scss';
+
import React from 'react';
import { History } from 'history';
-import { Provider } from 'react-redux';
import { parse, ParsedQuery } from 'query-string';
import { render, unmountComponentAtNode } from 'react-dom';
import { Switch, Route, RouteComponentProps, HashRouter, Redirect } from 'react-router-dom';
@@ -24,18 +24,23 @@ import { I18nProvider, FormattedRelative } from '@kbn/i18n-react';
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { createKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public';
+import {
+ createDashboardListingFilterUrl,
+ CREATE_NEW_DASHBOARD_URL,
+ DASHBOARD_APP_ID,
+ LANDING_PAGE_PATH,
+ VIEW_DASHBOARD_URL,
+} from '../dashboard_constants';
import { DashboardListing } from './listing';
-import { dashboardStateStore } from './state';
import { DashboardApp } from './dashboard_app';
-import { addHelpMenuToAppChrome } from './lib';
import { pluginServices } from '../services/plugin_services';
import { DashboardNoMatch } from './listing/dashboard_no_match';
+import { createDashboardEditUrl } from '../dashboard_constants';
import { DashboardStart, DashboardStartDependencies } from '../plugin';
-import { createDashboardListingFilterUrl } from '../dashboard_constants';
+import { DashboardMountContext } from './hooks/dashboard_mount_context';
import { DashboardApplicationService } from '../services/application/types';
-import { createDashboardEditUrl, DashboardConstants } from '../dashboard_constants';
-import { dashboardReadonlyBadge, getDashboardPageTitle } from '../dashboard_strings';
-import { DashboardEmbedSettings, RedirectToProps, DashboardMountContextProps } from '../types';
+import { dashboardReadonlyBadge, getDashboardPageTitle } from './_dashboard_app_strings';
+import { DashboardEmbedSettings, DashboardMountContextProps, RedirectToProps } from './types';
export const dashboardUrlParams = {
showTopMenu: 'show-top-menu',
@@ -58,18 +63,17 @@ type TableListViewApplicationService = DashboardApplicationService & {
};
export async function mountApp({ core, element, appUnMounted, mountContext }: DashboardMountProps) {
- const { DashboardMountContext } = await import('./hooks/dashboard_mount_context');
-
const {
application,
- chrome: { setBadge, docTitle },
+ chrome: { setBadge, docTitle, setHelpExtension },
dashboardCapabilities: { showWriteControls },
+ documentationLinks: { dashboardDocLink },
+ settings: { uiSettings },
+ savedObjectsTagging,
data: dataStart,
- embeddable,
notifications,
+ embeddable,
overlays,
- savedObjectsTagging,
- settings: { uiSettings },
http,
} = pluginServices.getServices();
@@ -80,7 +84,7 @@ export async function mountApp({ core, element, appUnMounted, mountContext }: Da
createKbnUrlStateStorage({
history,
useHash: uiSettings.get('state:storeInSessionStorage'),
- ...withNotifyOnErrors(core.notifications.toasts),
+ ...withNotifyOnErrors(notifications.toasts),
});
const redirect = (redirectTo: RedirectToProps) => {
@@ -90,7 +94,7 @@ export async function mountApp({ core, element, appUnMounted, mountContext }: Da
if (redirectTo.destination === 'dashboard') {
destination = redirectTo.id
? createDashboardEditUrl(redirectTo.id, redirectTo.editMode)
- : DashboardConstants.CREATE_NEW_DASHBOARD_URL;
+ : CREATE_NEW_DASHBOARD_URL;
} else {
destination = createDashboardListingFilterUrl(redirectTo.filter);
}
@@ -149,9 +153,7 @@ export async function mountApp({ core, element, appUnMounted, mountContext }: Da
};
const hasEmbeddableIncoming = Boolean(
- embeddable
- .getStateTransfer()
- .getIncomingEmbeddablePackage(DashboardConstants.DASHBOARDS_ID, false)
+ embeddable.getStateTransfer().getIncomingEmbeddablePackage(DASHBOARD_APP_ID, false)
);
if (!hasEmbeddableIncoming) {
dataStart.dataViews.clearCache();
@@ -165,54 +167,53 @@ export async function mountApp({ core, element, appUnMounted, mountContext }: Da
const app = (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
- addHelpMenuToAppChrome();
+ setHelpExtension({
+ appName: getDashboardPageTitle(),
+ links: [
+ {
+ linkType: 'documentation',
+ href: `${dashboardDocLink}`,
+ },
+ ],
+ });
if (!showWriteControls) {
setBadge({
diff --git a/src/plugins/dashboard/public/application/hooks/dashboard_mount_context.ts b/src/plugins/dashboard/public/dashboard_app/hooks/dashboard_mount_context.ts
similarity index 93%
rename from src/plugins/dashboard/public/application/hooks/dashboard_mount_context.ts
rename to src/plugins/dashboard/public/dashboard_app/hooks/dashboard_mount_context.ts
index 967fbf67e456..db67405fb113 100644
--- a/src/plugins/dashboard/public/application/hooks/dashboard_mount_context.ts
+++ b/src/plugins/dashboard/public/dashboard_app/hooks/dashboard_mount_context.ts
@@ -6,9 +6,11 @@
* Side Public License, v 1.
*/
-import { ScopedHistory } from '@kbn/core-application-browser';
import { createContext, useContext } from 'react';
-import { DashboardMountContextProps } from '../../types';
+
+import { ScopedHistory } from '@kbn/core-application-browser';
+
+import { DashboardMountContextProps } from '../types';
export const DashboardMountContext = createContext({
// default values for the dashboard mount context
diff --git a/src/plugins/dashboard/public/dashboard_app/hooks/use_dashboard_outcome_validation.tsx b/src/plugins/dashboard/public/dashboard_app/hooks/use_dashboard_outcome_validation.tsx
new file mode 100644
index 000000000000..847126a9b942
--- /dev/null
+++ b/src/plugins/dashboard/public/dashboard_app/hooks/use_dashboard_outcome_validation.tsx
@@ -0,0 +1,87 @@
+/*
+ * 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 { useCallback, useMemo, useState } from 'react';
+
+import { DashboardRedirect } from '../types';
+import { pluginServices } from '../../services/plugin_services';
+import { createDashboardEditUrl } from '../../dashboard_constants';
+import { getDashboardURL404String } from '../_dashboard_app_strings';
+import { useDashboardMountContext } from './dashboard_mount_context';
+import { LoadDashboardFromSavedObjectReturn } from '../../services/dashboard_saved_object/lib/load_dashboard_state_from_saved_object';
+
+export const useDashboardOutcomeValidation = ({
+ redirectTo,
+}: {
+ redirectTo: DashboardRedirect;
+}) => {
+ const [aliasId, setAliasId] = useState();
+ const [outcome, setOutcome] = useState();
+ const [savedObjectId, setSavedObjectId] = useState();
+
+ const { scopedHistory: getScopedHistory } = useDashboardMountContext();
+ const scopedHistory = getScopedHistory?.();
+
+ /**
+ * Unpack dashboard services
+ */
+ const {
+ notifications: { toasts },
+ screenshotMode,
+ spaces,
+ } = pluginServices.getServices();
+
+ const validateOutcome = useCallback(
+ ({ dashboardFound, resolveMeta, dashboardId }: LoadDashboardFromSavedObjectReturn) => {
+ if (!dashboardFound) {
+ toasts.addDanger(getDashboardURL404String());
+ redirectTo({ destination: 'listing' });
+ return false; // redirected. Stop loading dashboard.
+ }
+
+ if (resolveMeta && dashboardId) {
+ const {
+ outcome: loadOutcome,
+ alias_target_id: alias,
+ alias_purpose: aliasPurpose,
+ } = resolveMeta;
+ /**
+ * Handle saved object resolve alias outcome by redirecting.
+ */
+ if (loadOutcome === 'aliasMatch' && dashboardId && alias) {
+ const path = scopedHistory.location.hash.replace(dashboardId, alias);
+ if (screenshotMode.isScreenshotMode()) {
+ scopedHistory.replace(path);
+ } else {
+ spaces.redirectLegacyUrl?.({ path, aliasPurpose });
+ return false; // redirected. Stop loading dashboard.
+ }
+ }
+ setAliasId(alias);
+ setOutcome(loadOutcome);
+ setSavedObjectId(dashboardId);
+ }
+ return true;
+ },
+ [scopedHistory, redirectTo, screenshotMode, spaces, toasts]
+ );
+
+ const getLegacyConflictWarning = useMemo(() => {
+ if (savedObjectId && outcome === 'conflict' && aliasId) {
+ return () =>
+ spaces.getLegacyUrlConflict?.({
+ currentObjectId: savedObjectId,
+ otherObjectId: aliasId,
+ otherObjectPath: `#${createDashboardEditUrl(aliasId)}${scopedHistory.location.search}`,
+ });
+ }
+ return null;
+ }, [aliasId, outcome, savedObjectId, scopedHistory, spaces]);
+
+ return { validateOutcome, getLegacyConflictWarning };
+};
diff --git a/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap b/src/plugins/dashboard/public/dashboard_app/listing/__snapshots__/dashboard_listing.test.tsx.snap
similarity index 100%
rename from src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap
rename to src/plugins/dashboard/public/dashboard_app/listing/__snapshots__/dashboard_listing.test.tsx.snap
diff --git a/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx b/src/plugins/dashboard/public/dashboard_app/listing/confirm_overlays.tsx
similarity index 99%
rename from src/plugins/dashboard/public/application/listing/confirm_overlays.tsx
rename to src/plugins/dashboard/public/dashboard_app/listing/confirm_overlays.tsx
index 58ad770e1f84..e1bd8c339e22 100644
--- a/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx
+++ b/src/plugins/dashboard/public/dashboard_app/listing/confirm_overlays.tsx
@@ -22,8 +22,8 @@ import {
} from '@elastic/eui';
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
-import { createConfirmStrings, discardConfirmStrings } from '../../dashboard_strings';
import { pluginServices } from '../../services/plugin_services';
+import { createConfirmStrings, discardConfirmStrings } from '../_dashboard_app_strings';
export type DiscardOrKeepSelection = 'cancel' | 'discard' | 'keep';
diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx b/src/plugins/dashboard/public/dashboard_app/listing/dashboard_listing.test.tsx
similarity index 100%
rename from src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx
rename to src/plugins/dashboard/public/dashboard_app/listing/dashboard_listing.test.tsx
diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx b/src/plugins/dashboard/public/dashboard_app/listing/dashboard_listing.tsx
similarity index 95%
rename from src/plugins/dashboard/public/application/listing/dashboard_listing.tsx
rename to src/plugins/dashboard/public/dashboard_app/listing/dashboard_listing.tsx
index 4752348246b0..f7de6f0e0845 100644
--- a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx
+++ b/src/plugins/dashboard/public/dashboard_app/listing/dashboard_listing.tsx
@@ -20,8 +20,8 @@ import {
} from '@elastic/eui';
import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
import { syncGlobalQueryStateWithUrl } from '@kbn/data-plugin/public';
-import type { SavedObjectsFindOptionsReference, SimpleSavedObject } from '@kbn/core/public';
import type { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
+import type { SavedObjectsFindOptionsReference, SimpleSavedObject } from '@kbn/core/public';
import { TableListView, type UserContentCommonSchema } from '@kbn/content-management-table-list';
import {
@@ -30,17 +30,20 @@ import {
noItemsStrings,
dashboardUnsavedListingStrings,
getNewDashboardTitle,
- dashboardSavedObjectErrorStrings,
-} from '../../dashboard_strings';
-import { DashboardConstants } from '../..';
-import { DashboardRedirect } from '../../types';
+ dashboardListingErrorStrings,
+} from '../_dashboard_app_strings';
+import {
+ DashboardAppNoDataPage,
+ isDashboardAppInNoDataState,
+} from '../no_data/dashboard_app_no_data';
+import { DashboardRedirect } from '../types';
+import { DashboardAttributes } from '../../../common';
import { pluginServices } from '../../services/plugin_services';
import { DashboardUnsavedListing } from './dashboard_unsaved_listing';
+import { DASHBOARD_SAVED_OBJECT_TYPE } from '../../dashboard_constants';
import { getDashboardListItemLink } from './get_dashboard_list_item_link';
import { confirmCreateWithUnsaved, confirmDiscardUnsavedChanges } from './confirm_overlays';
-import { DashboardAppNoDataPage, isDashboardAppInNoDataState } from '../dashboard_app_no_data';
import { DASHBOARD_PANELS_UNSAVED_ID } from '../../services/dashboard_session_storage/dashboard_session_storage_service';
-import { DashboardAttributes } from '../embeddable';
const SAVED_OBJECTS_LIMIT_SETTING = 'savedObjects:listingLimit';
const SAVED_OBJECTS_PER_PAGE_SETTING = 'savedObjects:perPage';
@@ -293,11 +296,11 @@ export const DashboardListing = ({
await Promise.all(
dashboardsToDelete.map(({ id }) => {
dashboardSessionStorage.clearState(id);
- return savedObjectsClient.delete(DashboardConstants.DASHBOARD_SAVED_OBJECT_TYPE, id);
+ return savedObjectsClient.delete(DASHBOARD_SAVED_OBJECT_TYPE, id);
})
).catch((error) => {
toasts.addError(error, {
- title: dashboardSavedObjectErrorStrings.getErrorDeletingDashboardToast(),
+ title: dashboardListingErrorStrings.getErrorDeletingDashboardToast(),
});
});
setUnsavedDashboardIds(dashboardSessionStorage.getDashboardIdsWithUnsavedChanges());
diff --git a/src/plugins/dashboard/public/application/listing/dashboard_no_match.tsx b/src/plugins/dashboard/public/dashboard_app/listing/dashboard_no_match.tsx
similarity index 95%
rename from src/plugins/dashboard/public/application/listing/dashboard_no_match.tsx
rename to src/plugins/dashboard/public/dashboard_app/listing/dashboard_no_match.tsx
index 03e87f1a344d..895b7ce791fa 100644
--- a/src/plugins/dashboard/public/application/listing/dashboard_no_match.tsx
+++ b/src/plugins/dashboard/public/dashboard_app/listing/dashboard_no_match.tsx
@@ -14,7 +14,7 @@ import { EuiCallOut } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
-import { DashboardConstants } from '../..';
+import { LANDING_PAGE_PATH } from '../../dashboard_constants';
import { pluginServices } from '../../services/plugin_services';
import { useDashboardMountContext } from '../hooks/dashboard_mount_context';
@@ -66,7 +66,7 @@ export const DashboardNoMatch = ({ history }: { history: RouteComponentProps['hi
}
}, 15000);
- history.replace(DashboardConstants.LANDING_PAGE_PATH);
+ history.replace(LANDING_PAGE_PATH);
}
}, [restorePreviousUrl, navigateToLegacyKibanaUrl, banners, theme$, history]);
diff --git a/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.test.tsx b/src/plugins/dashboard/public/dashboard_app/listing/dashboard_unsaved_listing.test.tsx
similarity index 100%
rename from src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.test.tsx
rename to src/plugins/dashboard/public/dashboard_app/listing/dashboard_unsaved_listing.test.tsx
diff --git a/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.tsx b/src/plugins/dashboard/public/dashboard_app/listing/dashboard_unsaved_listing.tsx
similarity index 97%
rename from src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.tsx
rename to src/plugins/dashboard/public/dashboard_app/listing/dashboard_unsaved_listing.tsx
index 3aa862fe3026..2cf39d8ccb0d 100644
--- a/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.tsx
+++ b/src/plugins/dashboard/public/dashboard_app/listing/dashboard_unsaved_listing.tsx
@@ -17,12 +17,12 @@ import {
} from '@elastic/eui';
import React, { useCallback, useEffect, useState } from 'react';
-import type { DashboardRedirect } from '../../types';
+import type { DashboardRedirect } from '../types';
+import { DashboardAttributes } from '../../../common';
import { pluginServices } from '../../services/plugin_services';
import { confirmDiscardUnsavedChanges } from './confirm_overlays';
-import { dashboardUnsavedListingStrings, getNewDashboardTitle } from '../../dashboard_strings';
+import { dashboardUnsavedListingStrings, getNewDashboardTitle } from '../_dashboard_app_strings';
import { DASHBOARD_PANELS_UNSAVED_ID } from '../../services/dashboard_session_storage/dashboard_session_storage_service';
-import { DashboardAttributes } from '../embeddable';
const DashboardUnsavedItem = ({
id,
diff --git a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts b/src/plugins/dashboard/public/dashboard_app/listing/get_dashboard_list_item_link.test.ts
similarity index 100%
rename from src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts
rename to src/plugins/dashboard/public/dashboard_app/listing/get_dashboard_list_item_link.test.ts
diff --git a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts b/src/plugins/dashboard/public/dashboard_app/listing/get_dashboard_list_item_link.ts
similarity index 94%
rename from src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts
rename to src/plugins/dashboard/public/dashboard_app/listing/get_dashboard_list_item_link.ts
index 0ee6f016ad6d..521f6de79859 100644
--- a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts
+++ b/src/plugins/dashboard/public/dashboard_app/listing/get_dashboard_list_item_link.ts
@@ -10,7 +10,7 @@ import type { QueryState } from '@kbn/data-plugin/public';
import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public';
import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
import {
- DashboardConstants,
+ DASHBOARD_APP_ID,
createDashboardEditUrl,
GLOBAL_STATE_STORAGE_KEY,
} from '../../dashboard_constants';
@@ -27,7 +27,7 @@ export const getDashboardListItemLink = (
} = pluginServices.getServices();
const useHash = uiSettings.get('state:storeInSessionStorage'); // use hash
- let url = getUrlForApp(DashboardConstants.DASHBOARDS_ID, {
+ let url = getUrlForApp(DASHBOARD_APP_ID, {
path: `#${createDashboardEditUrl(id)}`,
});
const globalStateInUrl = kbnUrlStateStorage.get(GLOBAL_STATE_STORAGE_KEY) || {};
diff --git a/src/plugins/dashboard/public/application/listing/index.ts b/src/plugins/dashboard/public/dashboard_app/listing/index.ts
similarity index 100%
rename from src/plugins/dashboard/public/application/listing/index.ts
rename to src/plugins/dashboard/public/dashboard_app/listing/index.ts
diff --git a/src/plugins/dashboard/public/application/lib/load_dashboard_history_location_state.ts b/src/plugins/dashboard/public/dashboard_app/locator/load_dashboard_history_location_state.ts
similarity index 61%
rename from src/plugins/dashboard/public/application/lib/load_dashboard_history_location_state.ts
rename to src/plugins/dashboard/public/dashboard_app/locator/load_dashboard_history_location_state.ts
index 9a7d1791c6c9..c187ab4cdaee 100644
--- a/src/plugins/dashboard/public/application/lib/load_dashboard_history_location_state.ts
+++ b/src/plugins/dashboard/public/dashboard_app/locator/load_dashboard_history_location_state.ts
@@ -5,14 +5,16 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
+import { ScopedHistory } from '@kbn/core-application-browser';
-import { DashboardState } from '../../types';
-import { ForwardedDashboardState } from '../../locator';
-import { convertSavedPanelsToPanelMap } from '../../../common';
+import { ForwardedDashboardState } from './locator';
+import { convertSavedPanelsToPanelMap, DashboardContainerByValueInput } from '../../../common';
export const loadDashboardHistoryLocationState = (
- state?: ForwardedDashboardState
-): Partial => {
+ getScopedHistory: () => ScopedHistory
+): Partial => {
+ const state = getScopedHistory().location.state as undefined | ForwardedDashboardState;
+
if (!state) {
return {};
}
diff --git a/src/plugins/dashboard/public/locator.test.ts b/src/plugins/dashboard/public/dashboard_app/locator/locator.test.ts
similarity index 96%
rename from src/plugins/dashboard/public/locator.test.ts
rename to src/plugins/dashboard/public/dashboard_app/locator/locator.test.ts
index 402378162fa1..2b56acc71915 100644
--- a/src/plugins/dashboard/public/locator.test.ts
+++ b/src/plugins/dashboard/public/dashboard_app/locator/locator.test.ts
@@ -170,24 +170,6 @@ describe('dashboard locator', () => {
});
});
- 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?_g=()`,
- state: {
- savedQuery: '__savedQueryId__',
- },
- });
- });
-
test('panels', async () => {
const definition = new DashboardAppLocatorDefinition({
useHashedUrl: false,
diff --git a/src/plugins/dashboard/public/locator.ts b/src/plugins/dashboard/public/dashboard_app/locator/locator.ts
similarity index 73%
rename from src/plugins/dashboard/public/locator.ts
rename to src/plugins/dashboard/public/dashboard_app/locator/locator.ts
index a66015afcb00..7d03bc1bc65c 100644
--- a/src/plugins/dashboard/public/locator.ts
+++ b/src/plugins/dashboard/public/dashboard_app/locator/locator.ts
@@ -8,16 +8,15 @@
import type { SerializableRecord } from '@kbn/utility-types';
import { flow } from 'lodash';
-import type { Filter, TimeRange, Query } from '@kbn/es-query';
-import type { GlobalQueryStateFromUrl, RefreshInterval } from '@kbn/data-plugin/public';
-import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public';
-import { SerializableControlGroupInput } from '@kbn/controls-plugin/common';
+
+import type { Filter } from '@kbn/es-query';
import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public';
-import { ViewMode } from '@kbn/embeddable-plugin/public';
+import { SerializableControlGroupInput } from '@kbn/controls-plugin/common';
+import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public';
+import type { GlobalQueryStateFromUrl } from '@kbn/data-plugin/public';
-import type { SavedDashboardPanel } from '../common/types';
-import type { RawDashboardState } from './types';
-import { DashboardConstants } from './dashboard_constants';
+import { DASHBOARD_APP_ID, SEARCH_SESSION_ID } from '../../dashboard_constants';
+import type { DashboardContainerByValueInput, SavedDashboardPanel } from '../../../common';
/**
* Useful for ensuring that we don't pass any non-serializable values to history.push (for example, functions).
@@ -35,37 +34,18 @@ export const cleanEmptyKeys = (stateObj: Record) => {
export const DASHBOARD_APP_LOCATOR = 'DASHBOARD_APP_LOCATOR';
-/**
- * We use `type` instead of `interface` to avoid having to extend this type with
- * `SerializableRecord`. See https://github.com/microsoft/TypeScript/issues/15300.
- */
-// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
-export type DashboardAppLocatorParams = {
+export type DashboardAppLocatorParams = Partial<
+ Omit<
+ DashboardContainerByValueInput,
+ 'panels' | 'controlGroupInput' | 'executionContext' | 'isEmbeddedExternally'
+ >
+> & {
/**
* If given, the dashboard saved object with this id will be loaded. If not given,
* a new, unsaved dashboard will be loaded up.
*/
dashboardId?: string;
- /**
- * Optionally set the time range in the time picker.
- */
- timeRange?: TimeRange;
-
- /**
- * Optionally set the refresh interval.
- */
- refreshInterval?: RefreshInterval;
- /**
- * Optionally apply filers. NOTE: if given and used in conjunction with `dashboardId`, and the
- * saved dashboard has filters saved with it, this will _replace_ those filters.
- */
- filters?: Filter[];
- /**
- * Optionally set a query. NOTE: if given and used in conjunction with `dashboardId`, and the
- * saved dashboard has a query saved with it, this will _replace_ that query.
- */
- query?: Query;
/**
* If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines
* whether to hash the data in the url to avoid url length issues.
@@ -80,11 +60,6 @@ export type DashboardAppLocatorParams = {
*/
preserveSavedFilters?: boolean;
- /**
- * View mode of the dashboard.
- */
- viewMode?: ViewMode;
-
/**
* Search search session ID to restore.
* (Background search)
@@ -96,18 +71,6 @@ export type DashboardAppLocatorParams = {
*/
panels?: Array; // used SerializableRecord here to force the GridData type to be read as serializable
- /**
- * Saved query ID
- */
- savedQuery?: string;
-
- /**
- * List of tags to set to the state
- */
- tags?: string[];
-
- options?: RawDashboardState['options'];
-
/**
* Control group input
*/
@@ -179,11 +142,11 @@ export class DashboardAppLocatorDefinition implements LocatorDefinition name === 'lens');
+ const quickButtonVisTypes = ['markdown', 'maps'];
+
+ const trackUiMetric = usageCollection.reportUiCounter?.bind(
+ usageCollection,
+ DASHBOARD_UI_METRIC_ID
+ );
+
+ const createNewVisType = useCallback(
+ (visType?: BaseVisType | VisTypeAlias) => () => {
+ let path = '';
+ let appId = '';
+
+ if (visType) {
+ if (trackUiMetric) {
+ trackUiMetric(METRIC_TYPE.CLICK, `${visType.name}:create`);
+ }
+
+ if ('aliasPath' in visType) {
+ appId = visType.aliasApp;
+ path = visType.aliasPath;
+ } else {
+ appId = 'visualize';
+ path = `#/create?type=${encodeURIComponent(visType.name)}`;
+ }
+ } else {
+ appId = 'visualize';
+ path = '#/create?';
+ }
+
+ stateTransferService.navigateToEditor(appId, {
+ path,
+ state: {
+ originatingApp: DASHBOARD_APP_ID,
+ searchSessionId: search.session.getSessionId(),
+ },
+ });
+ },
+ [stateTransferService, search.session, trackUiMetric]
+ );
+
+ const getVisTypeQuickButton = (visTypeName: string) => {
+ const visType =
+ getVisualization(visTypeName) || getVisTypeAliases().find(({ name }) => name === visTypeName);
+
+ if (visType) {
+ if ('aliasPath' in visType) {
+ const { name, icon, title } = visType as VisTypeAlias;
+
+ return {
+ iconType: icon,
+ createType: title,
+ onClick: createNewVisType(visType as VisTypeAlias),
+ 'data-test-subj': `dashboardQuickButton${name}`,
+ };
+ } else {
+ const { name, icon, title, titleInWizard } = visType as BaseVisType;
+
+ return {
+ iconType: icon,
+ createType: titleInWizard || title,
+ onClick: createNewVisType(visType as BaseVisType),
+ 'data-test-subj': `dashboardQuickButton${name}`,
+ };
+ }
+ }
+ return;
+ };
+
+ const quickButtons = quickButtonVisTypes
+ .map(getVisTypeQuickButton)
+ .filter((button) => button) as QuickButtonProps[];
+
+ return (
+ <>
+
+
+ {{
+ primaryActionButton: (
+
+ ),
+ quickButtonGroup: ,
+ extraButtons: [
+ ,
+ dashboardContainer.addFromLibrary()}
+ data-test-subj="dashboardAddPanelButton"
+ />,
+ dashboardContainer.controlGroup?.getToolbarButtons(),
+ ],
+ }}
+
+ >
+ );
+}
diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_top_nav.tsx
new file mode 100644
index 000000000000..f4e0fa3b44f5
--- /dev/null
+++ b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_top_nav.tsx
@@ -0,0 +1,259 @@
+/*
+ * 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 UseUnmount from 'react-use/lib/useUnmount';
+import React, { useEffect, useMemo, useRef, useState } from 'react';
+
+import { ViewMode } from '@kbn/embeddable-plugin/public';
+import type { DataView } from '@kbn/data-views-plugin/public';
+import type { TopNavMenuProps } from '@kbn/navigation-plugin/public';
+import { withSuspense, LazyLabsFlyout } from '@kbn/presentation-util-plugin/public';
+
+import {
+ getDashboardTitle,
+ leaveConfirmStrings,
+ getDashboardBreadcrumb,
+ unsavedChangesBadgeStrings,
+} from '../_dashboard_app_strings';
+import { UI_SETTINGS } from '../../../common';
+import { pluginServices } from '../../services/plugin_services';
+import { useDashboardMenuItems } from './use_dashboard_menu_items';
+import { DashboardEmbedSettings, DashboardRedirect } from '../types';
+import { DashboardEditingToolbar } from './dashboard_editing_toolbar';
+import { useDashboardMountContext } from '../hooks/dashboard_mount_context';
+import { getFullEditPath, LEGACY_DASHBOARD_APP_ID } from '../../dashboard_constants';
+import { useDashboardContainerContext } from '../../dashboard_container/dashboard_container_renderer';
+
+export interface DashboardTopNavProps {
+ embedSettings?: DashboardEmbedSettings;
+ redirectTo: DashboardRedirect;
+}
+
+const LabsFlyout = withSuspense(LazyLabsFlyout, null);
+
+export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavProps) {
+ const [isChromeVisible, setIsChromeVisible] = useState(false);
+ const [isLabsShown, setIsLabsShown] = useState(false);
+
+ const dashboardTitleRef = useRef(null);
+
+ /**
+ * Unpack dashboard services
+ */
+ const {
+ data: {
+ query: { filterManager },
+ },
+ chrome: {
+ setBreadcrumbs,
+ setIsVisible: setChromeVisibility,
+ getIsVisible$: getChromeIsVisible$,
+ recentlyAccessed: chromeRecentlyAccessed,
+ },
+ settings: { uiSettings },
+ navigation: { TopNavMenu },
+ embeddable: { getStateTransfer },
+ initializerContext: { allowByValueEmbeddables },
+ dashboardCapabilities: { saveQuery: showSaveQuery },
+ } = pluginServices.getServices();
+ const isLabsEnabled = uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI);
+ const { setHeaderActionMenu, onAppLeave } = useDashboardMountContext();
+
+ /**
+ * Unpack dashboard state from redux
+ */
+ const {
+ useEmbeddableDispatch,
+ actions: { setSavedQueryId },
+ useEmbeddableSelector: select,
+ embeddableInstance: dashboardContainer,
+ } = useDashboardContainerContext();
+ const dispatch = useEmbeddableDispatch();
+
+ const hasUnsavedChanges = select((state) => state.componentState.hasUnsavedChanges);
+ const fullScreenMode = select((state) => state.componentState.fullScreenMode);
+ const savedQueryId = select((state) => state.componentState.savedQueryId);
+ const lastSavedId = select((state) => state.componentState.lastSavedId);
+ const viewMode = select((state) => state.explicitInput.viewMode);
+ const query = select((state) => state.explicitInput.query);
+ const title = select((state) => state.explicitInput.title);
+
+ // store data views in state & subscribe to dashboard data view changes.
+ const [allDataViews, setAllDataViews] = useState(
+ dashboardContainer.getAllDataViews()
+ );
+ useEffect(() => {
+ const subscription = dashboardContainer.onDataViewsUpdate$.subscribe((dataViews) =>
+ setAllDataViews(dataViews)
+ );
+ return () => subscription.unsubscribe();
+ }, [dashboardContainer]);
+
+ const dashboardTitle = useMemo(() => {
+ return getDashboardTitle(title, viewMode, !lastSavedId);
+ }, [title, viewMode, lastSavedId]);
+
+ /**
+ * focus on the top header when title or view mode is changed
+ */
+ useEffect(() => {
+ dashboardTitleRef.current?.focus();
+ }, [title, viewMode]);
+
+ /**
+ * Manage chrome visibility when dashboard is embedded.
+ */
+ useEffect(() => {
+ if (!embedSettings) setChromeVisibility(viewMode !== ViewMode.PRINT);
+ }, [embedSettings, setChromeVisibility, viewMode]);
+
+ /**
+ * populate recently accessed, and set is chrome visible.
+ */
+ useEffect(() => {
+ const subscription = getChromeIsVisible$().subscribe((visible) => setIsChromeVisible(visible));
+ if (lastSavedId && title) {
+ chromeRecentlyAccessed.add(
+ getFullEditPath(lastSavedId, viewMode === ViewMode.EDIT),
+ title,
+ lastSavedId
+ );
+ }
+ return () => subscription.unsubscribe();
+ }, [
+ allowByValueEmbeddables,
+ chromeRecentlyAccessed,
+ getChromeIsVisible$,
+ lastSavedId,
+ viewMode,
+ title,
+ ]);
+
+ /**
+ * Set breadcrumbs to dashboard title when dashboard's title or view mode changes
+ */
+ useEffect(() => {
+ setBreadcrumbs([
+ {
+ text: getDashboardBreadcrumb(),
+ 'data-test-subj': 'dashboardListingBreadcrumb',
+ onClick: () => {
+ redirectTo({ destination: 'listing' });
+ },
+ },
+ {
+ text: dashboardTitle,
+ },
+ ]);
+ }, [setBreadcrumbs, redirectTo, dashboardTitle]);
+
+ /**
+ * Build app leave handler whenever hasUnsavedChanges changes
+ */
+ useEffect(() => {
+ onAppLeave((actions) => {
+ if (
+ viewMode === ViewMode.EDIT &&
+ hasUnsavedChanges &&
+ !getStateTransfer().isTransferInProgress
+ ) {
+ return actions.confirm(
+ leaveConfirmStrings.getLeaveSubtitle(),
+ leaveConfirmStrings.getLeaveTitle()
+ );
+ }
+ return actions.default();
+ });
+ return () => {
+ // reset on app leave handler so leaving from the listing page doesn't trigger a confirmation
+ onAppLeave((actions) => actions.default());
+ };
+ }, [onAppLeave, getStateTransfer, hasUnsavedChanges, viewMode]);
+
+ const { viewModeTopNavConfig, editModeTopNavConfig } = useDashboardMenuItems({
+ redirectTo,
+ isLabsShown,
+ setIsLabsShown,
+ });
+
+ const getNavBarProps = (): TopNavMenuProps => {
+ const shouldShowNavBarComponent = (forceShow: boolean): boolean =>
+ (forceShow || isChromeVisible) && !fullScreenMode;
+
+ const shouldShowFilterBar = (forceHide: boolean): boolean =>
+ !forceHide && (filterManager.getFilters().length > 0 || !fullScreenMode);
+
+ const showTopNavMenu = shouldShowNavBarComponent(Boolean(embedSettings?.forceShowTopNavMenu));
+ const showQueryInput = shouldShowNavBarComponent(
+ Boolean(embedSettings?.forceShowQueryInput || viewMode === ViewMode.PRINT)
+ );
+ const showDatePicker = shouldShowNavBarComponent(Boolean(embedSettings?.forceShowDatePicker));
+ const showFilterBar = shouldShowFilterBar(Boolean(embedSettings?.forceHideFilterBar));
+ const showQueryBar = showQueryInput || showDatePicker || showFilterBar;
+ const showSearchBar = showQueryBar || showFilterBar;
+ const topNavConfig = viewMode === ViewMode.EDIT ? editModeTopNavConfig : viewModeTopNavConfig;
+
+ const badges =
+ hasUnsavedChanges && viewMode === ViewMode.EDIT
+ ? [
+ {
+ 'data-test-subj': 'dashboardUnsavedChangesBadge',
+ badgeText: unsavedChangesBadgeStrings.getUnsavedChangedBadgeText(),
+ color: 'success',
+ },
+ ]
+ : undefined;
+
+ return {
+ query,
+ badges,
+ savedQueryId,
+ showSearchBar,
+ showFilterBar,
+ showSaveQuery,
+ showQueryInput,
+ showDatePicker,
+ screenTitle: title,
+ useDefaultBehaviors: true,
+ appName: LEGACY_DASHBOARD_APP_ID,
+ visible: viewMode !== ViewMode.PRINT,
+ indexPatterns: allDataViews,
+ config: showTopNavMenu ? topNavConfig : undefined,
+ setMenuMountPoint: embedSettings ? undefined : setHeaderActionMenu,
+ className: fullScreenMode ? 'kbnTopNavMenu-isFullScreen' : undefined,
+ onQuerySubmit: (_payload, isUpdate) => {
+ if (isUpdate === false) {
+ dashboardContainer.forceRefresh();
+ }
+ },
+ onSavedQueryIdChange: (newId: string | undefined) => {
+ dispatch(setSavedQueryId(newId));
+ },
+ };
+ };
+
+ UseUnmount(() => {
+ dashboardContainer.clearOverlays();
+ });
+
+ return (
+ <>
+ {`${getDashboardBreadcrumb()} - ${dashboardTitle}`}
+
+ {viewMode !== ViewMode.PRINT && isLabsEnabled && isLabsShown ? (
+ setIsLabsShown(false)} />
+ ) : null}
+ {viewMode === ViewMode.EDIT ? : null}
+ >
+ );
+}
diff --git a/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx
similarity index 94%
rename from src/plugins/dashboard/public/application/top_nav/editor_menu.tsx
rename to src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx
index 1f17e0b6ef6a..ae338145915c 100644
--- a/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx
+++ b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx
@@ -25,14 +25,12 @@ import type {
EmbeddableInput,
} from '@kbn/embeddable-plugin/public';
-import { DashboardContainer } from '..';
-import { DashboardConstants } from '../../dashboard_constants';
-import { dashboardReplacePanelAction } from '../../dashboard_strings';
import { pluginServices } from '../../services/plugin_services';
+import { getPanelAddedSuccessString } from '../_dashboard_app_strings';
+import { DASHBOARD_APP_ID, DASHBOARD_UI_METRIC_ID } from '../../dashboard_constants';
+import { useDashboardContainerContext } from '../../dashboard_container/dashboard_container_renderer';
interface Props {
- /** Dashboard container */
- dashboardContainer: DashboardContainer;
/** Handler for creating new visualization of a specified type */
createNewVisType: (visType: BaseVisType | VisTypeAlias) => () => void;
}
@@ -50,7 +48,7 @@ interface UnwrappedEmbeddableFactory {
isEditable: boolean;
}
-export const EditorMenu = ({ dashboardContainer, createNewVisType }: Props) => {
+export const EditorMenu = ({ createNewVisType }: Props) => {
const {
embeddable,
notifications: { toasts },
@@ -63,6 +61,8 @@ export const EditorMenu = ({ dashboardContainer, createNewVisType }: Props) => {
},
} = pluginServices.getServices();
+ const { embeddableInstance: dashboardContainer } = useDashboardContainerContext();
+
const embeddableFactories = useMemo(
() => Array.from(embeddable.getEmbeddableFactories()),
[embeddable]
@@ -86,13 +86,13 @@ export const EditorMenu = ({ dashboardContainer, createNewVisType }: Props) => {
const trackUiMetric = usageCollection.reportUiCounter?.bind(
usageCollection,
- DashboardConstants.DASHBOARD_ID
+ DASHBOARD_UI_METRIC_ID
);
const createNewAggsBasedVis = useCallback(
(visType?: BaseVisType) => () =>
showNewVisModal({
- originatingApp: DashboardConstants.DASHBOARDS_ID,
+ originatingApp: DASHBOARD_APP_ID,
outsideVisualizeApp: true,
showAggsSelection: true,
selectedVisType: visType,
@@ -237,9 +237,7 @@ export const EditorMenu = ({ dashboardContainer, createNewVisType }: Props) => {
if (newEmbeddable) {
toasts.addSuccess({
- title: dashboardReplacePanelAction.getSuccessMessage(
- `'${newEmbeddable.getInput().title}'` || ''
- ),
+ title: getPanelAddedSuccessString(`'${newEmbeddable.getInput().title}'` || ''),
'data-test-subj': 'addEmbeddableToDashboardSuccess',
});
}
diff --git a/src/plugins/dashboard/public/application/top_nav/show_share_modal.test.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx
similarity index 81%
rename from src/plugins/dashboard/public/application/top_nav/show_share_modal.test.tsx
rename to src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx
index 5f6dc325ce97..f2603110ebaa 100644
--- a/src/plugins/dashboard/public/application/top_nav/show_share_modal.test.tsx
+++ b/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx
@@ -7,11 +7,10 @@
*/
import { Capabilities } from '@kbn/core/public';
+import { convertPanelMapToSavedPanels, DashboardContainerByValueInput } from '../../../../common';
-import { DashboardState } from '../../types';
-import { DashboardAppLocatorParams } from '../..';
-import { pluginServices } from '../../services/plugin_services';
-import { stateToRawDashboardState } from '../lib/convert_dashboard_state';
+import { DashboardAppLocatorParams } from '../../..';
+import { pluginServices } from '../../../services/plugin_services';
import { showPublicUrlSwitch, ShowShareModal, ShowShareModalProps } from './show_share_modal';
describe('showPublicUrlSwitch', () => {
@@ -68,14 +67,15 @@ describe('ShowShareModal', () => {
jest.clearAllMocks();
});
- const getPropsAndShare = (unsavedState?: Partial): ShowShareModalProps => {
+ const getPropsAndShare = (
+ unsavedState?: Partial
+ ): ShowShareModalProps => {
pluginServices.getServices().dashboardSessionStorage.getState = jest
.fn()
.mockReturnValue(unsavedState);
return {
isDirty: true,
anchorElement: document.createElement('div'),
- currentDashboardState: { panels: {} } as DashboardState,
};
};
@@ -94,7 +94,7 @@ describe('ShowShareModal', () => {
});
it('locatorParams unsaved state is properly propagated to locator', () => {
- const unsavedDashboardState: DashboardState = {
+ const unsavedDashboardState: DashboardContainerByValueInput = {
panels: {
panel_1: {
type: 'panel_type',
@@ -105,13 +105,11 @@ describe('ShowShareModal', () => {
},
},
},
- options: {
- hidePanelTitles: true,
- useMargins: true,
- syncColors: true,
- syncCursor: true,
- syncTooltips: true,
- },
+ hidePanelTitles: true,
+ useMargins: true,
+ syncColors: true,
+ syncCursor: true,
+ syncTooltips: true,
filters: [
{
meta: {
@@ -123,8 +121,7 @@ describe('ShowShareModal', () => {
},
],
query: { query: 'bye', language: 'kuery' },
- savedQuery: 'amazingSavedQuery',
- } as unknown as DashboardState;
+ } as unknown as DashboardContainerByValueInput;
const showModalProps = getPropsAndShare(unsavedDashboardState);
ShowShareModal(showModalProps);
expect(toggleShareMenuSpy).toHaveBeenCalledTimes(1);
@@ -133,9 +130,13 @@ describe('ShowShareModal', () => {
locatorParams: { params: DashboardAppLocatorParams };
}
).locatorParams.params;
- const rawDashboardState = stateToRawDashboardState({
- state: unsavedDashboardState,
- });
+ const {
+ initializerContext: { kibanaVersion },
+ } = pluginServices.getServices();
+ const rawDashboardState = {
+ ...unsavedDashboardState,
+ panels: convertPanelMapToSavedPanels(unsavedDashboardState.panels, kibanaVersion),
+ };
unsavedStateKeys.forEach((key) => {
expect(shareLocatorParams[key]).toStrictEqual(
(rawDashboardState as unknown as Partial)[key]
diff --git a/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx
similarity index 86%
rename from src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx
rename to src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx
index 6cff8ff20a9d..cc25ac920899 100644
--- a/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx
+++ b/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx
@@ -19,20 +19,19 @@ import { getStateFromKbnUrl } from '@kbn/kibana-utils-plugin/public';
import { setStateToKbnUrl, unhashUrl } from '@kbn/kibana-utils-plugin/public';
import type { SerializableControlGroupInput } from '@kbn/controls-plugin/common';
-import type { DashboardState } from '../../types';
-import { dashboardUrlParams } from '../dashboard_router';
-import { shareModalStrings } from '../../dashboard_strings';
-import { convertPanelMapToSavedPanels } from '../../../common';
-import { pluginServices } from '../../services/plugin_services';
-import { DashboardAppLocatorParams, DASHBOARD_APP_LOCATOR } from '../../locator';
-import { stateToRawDashboardState } from '../lib/convert_dashboard_state';
+import { dashboardUrlParams } from '../../dashboard_router';
+import { shareModalStrings } from '../../_dashboard_app_strings';
+import { pluginServices } from '../../../services/plugin_services';
+import { convertPanelMapToSavedPanels } from '../../../../common';
+import { DashboardAppLocatorParams, DASHBOARD_APP_LOCATOR } from '../../locator/locator';
const showFilterBarId = 'showFilterBar';
export interface ShowShareModalProps {
isDirty: boolean;
+ savedObjectId?: string;
+ dashboardTitle?: string;
anchorElement: HTMLElement;
- currentDashboardState: DashboardState;
}
export const showPublicUrlSwitch = (anonymousUserCapabilities: Capabilities) => {
@@ -46,7 +45,8 @@ export const showPublicUrlSwitch = (anonymousUserCapabilities: Capabilities) =>
export function ShowShareModal({
isDirty,
anchorElement,
- currentDashboardState,
+ savedObjectId,
+ dashboardTitle,
}: ShowShareModalProps) {
const {
dashboardCapabilities: { createShortUrl: allowShortUrl },
@@ -121,19 +121,13 @@ export function ShowShareModal({
);
};
- let unsavedStateForLocator: Pick<
- DashboardAppLocatorParams,
- 'options' | 'query' | 'savedQuery' | 'filters' | 'panels' | 'controlGroupInput'
- > = {};
- const { savedObjectId, title } = currentDashboardState;
+ let unsavedStateForLocator: DashboardAppLocatorParams = {};
const unsavedDashboardState = dashboardSessionStorage.getState(savedObjectId);
if (unsavedDashboardState) {
unsavedStateForLocator = {
query: unsavedDashboardState.query,
filters: unsavedDashboardState.filters,
- options: unsavedDashboardState.options,
- savedQuery: unsavedDashboardState.savedQuery,
controlGroupInput: unsavedDashboardState.controlGroupInput as SerializableControlGroupInput,
panels: unsavedDashboardState.panels
? (convertPanelMapToSavedPanels(
@@ -141,6 +135,13 @@ export function ShowShareModal({
kibanaVersion
) as DashboardAppLocatorParams['panels'])
: undefined,
+
+ // options
+ useMargins: unsavedDashboardState?.useMargins,
+ syncColors: unsavedDashboardState?.syncColors,
+ syncCursor: unsavedDashboardState?.syncCursor,
+ syncTooltips: unsavedDashboardState?.syncTooltips,
+ hidePanelTitles: unsavedDashboardState?.hidePanelTitles,
};
}
@@ -162,7 +163,7 @@ export function ShowShareModal({
const shareableUrl = setStateToKbnUrl(
'_a',
- stateToRawDashboardState({ state: unsavedDashboardState ?? {} }),
+ unsavedStateForLocator,
{ useHash: false, storeInHashQuery: true },
unhashUrl(baseUrl)
);
@@ -177,7 +178,7 @@ export function ShowShareModal({
objectType: 'dashboard',
sharingData: {
title:
- title ||
+ dashboardTitle ||
i18n.translate('dashboard.share.defaultDashboardTitle', {
defaultMessage: 'Dashboard [{date}]',
values: { date: moment().toISOString(true) },
diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx
new file mode 100644
index 000000000000..b107a82bdaa3
--- /dev/null
+++ b/src/plugins/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx
@@ -0,0 +1,256 @@
+/*
+ * 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 { batch } from 'react-redux';
+import { Dispatch, SetStateAction, useCallback, useMemo, useState } from 'react';
+
+import { ViewMode } from '@kbn/embeddable-plugin/public';
+import { TopNavMenuData } from '@kbn/navigation-plugin/public';
+
+import { DashboardRedirect } from '../types';
+import { UI_SETTINGS } from '../../../common';
+import { topNavStrings } from '../_dashboard_app_strings';
+import { ShowShareModal } from './share/show_share_modal';
+import { pluginServices } from '../../services/plugin_services';
+import { CHANGE_CHECK_DEBOUNCE } from '../../dashboard_constants';
+import { SaveDashboardReturn } from '../../services/dashboard_saved_object/types';
+import { useDashboardContainerContext } from '../../dashboard_container/dashboard_container_renderer';
+import { confirmDiscardUnsavedChanges } from '../listing/confirm_overlays';
+
+export const useDashboardMenuItems = ({
+ redirectTo,
+ isLabsShown,
+ setIsLabsShown,
+}: {
+ redirectTo: DashboardRedirect;
+ isLabsShown: boolean;
+ setIsLabsShown: Dispatch>;
+}) => {
+ const [isSaveInProgress, setIsSaveInProgress] = useState(false);
+
+ /**
+ * Unpack dashboard services
+ */
+ const {
+ share,
+ settings: { uiSettings },
+ dashboardCapabilities: { showWriteControls },
+ } = pluginServices.getServices();
+ const isLabsEnabled = uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI);
+
+ /**
+ * Unpack dashboard state from redux
+ */
+ const {
+ useEmbeddableDispatch,
+ useEmbeddableSelector: select,
+ embeddableInstance: dashboardContainer,
+ actions: { setViewMode, setFullScreenMode },
+ } = useDashboardContainerContext();
+ const dispatch = useEmbeddableDispatch();
+
+ const hasUnsavedChanges = select((state) => state.componentState.hasUnsavedChanges);
+ const lastSavedId = select((state) => state.componentState.lastSavedId);
+ const dashboardTitle = select((state) => state.explicitInput.title);
+
+ /**
+ * Show the Dashboard app's share menu
+ */
+ const showShare = useCallback(
+ (anchorElement: HTMLElement) => {
+ ShowShareModal({
+ dashboardTitle,
+ anchorElement,
+ savedObjectId: lastSavedId,
+ isDirty: Boolean(hasUnsavedChanges),
+ });
+ },
+ [dashboardTitle, hasUnsavedChanges, lastSavedId]
+ );
+
+ const maybeRedirect = useCallback(
+ (result?: SaveDashboardReturn) => {
+ if (!result) return;
+ const { redirectRequired, id } = result;
+ if (redirectRequired) {
+ redirectTo({
+ id,
+ editMode: true,
+ useReplace: true,
+ destination: 'dashboard',
+ });
+ }
+ },
+ [redirectTo]
+ );
+
+ /**
+ * Save the dashboard without any UI or popups.
+ */
+ const quickSaveDashboard = useCallback(() => {
+ setIsSaveInProgress(true);
+ dashboardContainer
+ .runQuickSave()
+ .then(() => setTimeout(() => setIsSaveInProgress(false), CHANGE_CHECK_DEBOUNCE));
+ }, [dashboardContainer]);
+
+ /**
+ * Show the dashboard's save modal
+ */
+ const saveDashboardAs = useCallback(() => {
+ dashboardContainer.runSaveAs().then((result) => maybeRedirect(result));
+ }, [maybeRedirect, dashboardContainer]);
+
+ /**
+ * Clone the dashboard
+ */
+ const clone = useCallback(() => {
+ dashboardContainer.runClone().then((result) => maybeRedirect(result));
+ }, [maybeRedirect, dashboardContainer]);
+
+ /**
+ * Returns to view mode. If the dashboard has unsaved changes shows a warning and resets to last saved state.
+ */
+ const returnToViewMode = useCallback(() => {
+ dashboardContainer.clearOverlays();
+ if (hasUnsavedChanges) {
+ confirmDiscardUnsavedChanges(() => {
+ batch(() => {
+ dashboardContainer.resetToLastSavedState();
+ dispatch(setViewMode(ViewMode.VIEW));
+ });
+ });
+ return;
+ }
+ dispatch(setViewMode(ViewMode.VIEW));
+ }, [dashboardContainer, dispatch, hasUnsavedChanges, setViewMode]);
+
+ /**
+ * Register all of the top nav configs that can be used by dashboard.
+ */
+ const menuItems = useMemo(() => {
+ return {
+ fullScreen: {
+ ...topNavStrings.fullScreen,
+ id: 'full-screen',
+ testId: 'dashboardFullScreenMode',
+ run: () => dispatch(setFullScreenMode(true)),
+ } as TopNavMenuData,
+
+ labs: {
+ ...topNavStrings.labs,
+ id: 'labs',
+ testId: 'dashboardLabs',
+ run: () => setIsLabsShown(!isLabsShown),
+ } as TopNavMenuData,
+
+ edit: {
+ ...topNavStrings.edit,
+ emphasize: true,
+ id: 'edit',
+ iconType: 'pencil',
+ testId: 'dashboardEditMode',
+ className: 'eui-hideFor--s eui-hideFor--xs', // hide for small screens - editing doesn't work in mobile mode.
+ run: () => dispatch(setViewMode(ViewMode.EDIT)),
+ } as TopNavMenuData,
+
+ quickSave: {
+ ...topNavStrings.quickSave,
+ id: 'quick-save',
+ iconType: 'save',
+ emphasize: true,
+ isLoading: isSaveInProgress,
+ testId: 'dashboardQuickSaveMenuItem',
+ disableButton: !hasUnsavedChanges || isSaveInProgress,
+ run: () => quickSaveDashboard(),
+ } as TopNavMenuData,
+
+ saveAs: {
+ description: topNavStrings.saveAs.description,
+ disableButton: isSaveInProgress,
+ id: 'save',
+ emphasize: !Boolean(lastSavedId),
+ testId: 'dashboardSaveMenuItem',
+ iconType: Boolean(lastSavedId) ? undefined : 'save',
+ label: Boolean(lastSavedId) ? topNavStrings.saveAs.label : topNavStrings.quickSave.label,
+ run: () => saveDashboardAs(),
+ } as TopNavMenuData,
+
+ switchToViewMode: {
+ ...topNavStrings.switchToViewMode,
+ id: 'cancel',
+ disableButton: isSaveInProgress || !lastSavedId,
+ testId: 'dashboardViewOnlyMode',
+ run: () => returnToViewMode(),
+ } as TopNavMenuData,
+
+ share: {
+ ...topNavStrings.share,
+ id: 'share',
+ testId: 'shareTopNavButton',
+ disableButton: isSaveInProgress,
+ run: showShare,
+ } as TopNavMenuData,
+
+ options: {
+ ...topNavStrings.options,
+ id: 'options',
+ testId: 'dashboardOptionsButton',
+ disableButton: isSaveInProgress,
+ run: (anchor) => dashboardContainer.showOptions(anchor),
+ } as TopNavMenuData,
+
+ clone: {
+ ...topNavStrings.clone,
+ id: 'clone',
+ testId: 'dashboardClone',
+ disableButton: isSaveInProgress,
+ run: () => clone(),
+ } as TopNavMenuData,
+ };
+ }, [
+ quickSaveDashboard,
+ dashboardContainer,
+ hasUnsavedChanges,
+ setFullScreenMode,
+ isSaveInProgress,
+ returnToViewMode,
+ saveDashboardAs,
+ setIsLabsShown,
+ lastSavedId,
+ setViewMode,
+ isLabsShown,
+ showShare,
+ dispatch,
+ clone,
+ ]);
+
+ /**
+ * Build ordered menus for view and edit mode.
+ */
+ const viewModeTopNavConfig = useMemo(() => {
+ const labsMenuItem = isLabsEnabled ? [menuItems.labs] : [];
+ const shareMenuItem = share ? [menuItems.share] : [];
+ const writePermissionsMenuItems = showWriteControls ? [menuItems.clone, menuItems.edit] : [];
+ return [...labsMenuItem, menuItems.fullScreen, ...shareMenuItem, ...writePermissionsMenuItems];
+ }, [menuItems, share, showWriteControls, isLabsEnabled]);
+
+ const editModeTopNavConfig = useMemo(() => {
+ const labsMenuItem = isLabsEnabled ? [menuItems.labs] : [];
+ const shareMenuItem = share ? [menuItems.share] : [];
+ const editModeItems: TopNavMenuData[] = [];
+ if (lastSavedId) {
+ editModeItems.push(menuItems.saveAs, menuItems.switchToViewMode, menuItems.quickSave);
+ } else {
+ editModeItems.push(menuItems.switchToViewMode, menuItems.saveAs);
+ }
+ return [...labsMenuItem, menuItems.options, ...shareMenuItem, ...editModeItems];
+ }, [lastSavedId, menuItems, share, isLabsEnabled]);
+
+ return { viewModeTopNavConfig, editModeTopNavConfig };
+};
diff --git a/src/plugins/dashboard/public/dashboard_app/types.ts b/src/plugins/dashboard/public/dashboard_app/types.ts
new file mode 100644
index 000000000000..cc33cec973ee
--- /dev/null
+++ b/src/plugins/dashboard/public/dashboard_app/types.ts
@@ -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 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 { AppMountParameters, ScopedHistory } from '@kbn/core-application-browser';
+
+export type DashboardRedirect = (props: RedirectToProps) => void;
+export type RedirectToProps =
+ | { destination: 'dashboard'; id?: string; useReplace?: boolean; editMode?: boolean }
+ | { destination: 'listing'; filter?: string; useReplace?: boolean };
+
+export interface DashboardEmbedSettings {
+ forceHideFilterBar?: boolean;
+ forceShowTopNavMenu?: boolean;
+ forceShowQueryInput?: boolean;
+ forceShowDatePicker?: boolean;
+}
+
+export interface DashboardMountContextProps {
+ restorePreviousUrl: () => void;
+ scopedHistory: () => ScopedHistory;
+ onAppLeave: AppMountParameters['onAppLeave'];
+ setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
+}
diff --git a/src/plugins/dashboard/public/dashboard_app/url/search_sessions_integration.ts b/src/plugins/dashboard/public/dashboard_app/url/search_sessions_integration.ts
new file mode 100644
index 000000000000..752ee39724de
--- /dev/null
+++ b/src/plugins/dashboard/public/dashboard_app/url/search_sessions_integration.ts
@@ -0,0 +1,110 @@
+/*
+ * 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 { map } from 'rxjs';
+import { History } from 'history';
+
+import {
+ getQueryParams,
+ replaceUrlHashQuery,
+ IKbnUrlStateStorage,
+ createQueryParamObservable,
+} from '@kbn/kibana-utils-plugin/public';
+import type { Query } from '@kbn/es-query';
+import { SearchSessionInfoProvider } from '@kbn/data-plugin/public';
+
+import { SEARCH_SESSION_ID } from '../../dashboard_constants';
+import { DashboardContainer } from '../../dashboard_container';
+import { convertPanelMapToSavedPanels } from '../../../common';
+import { pluginServices } from '../../services/plugin_services';
+import { DashboardAppLocatorParams, DASHBOARD_APP_LOCATOR } from '../locator/locator';
+
+export const removeSearchSessionIdFromURL = (kbnUrlStateStorage: IKbnUrlStateStorage) => {
+ kbnUrlStateStorage.kbnUrlControls.updateAsync((nextUrl) => {
+ if (nextUrl.includes(SEARCH_SESSION_ID)) {
+ return replaceUrlHashQuery(nextUrl, (hashQuery) => {
+ delete hashQuery[SEARCH_SESSION_ID];
+ return hashQuery;
+ });
+ }
+ return nextUrl;
+ });
+};
+
+export const getSearchSessionIdFromURL = (history: History): string | undefined =>
+ getQueryParams(history.location)[SEARCH_SESSION_ID] as string | undefined;
+
+export const getSessionURLObservable = (history: History) =>
+ createQueryParamObservable(history, SEARCH_SESSION_ID).pipe(
+ map((sessionId) => sessionId ?? undefined)
+ );
+
+export function createSessionRestorationDataProvider(
+ container: DashboardContainer
+): SearchSessionInfoProvider {
+ return {
+ getName: async () => container.getTitle(),
+ getLocatorData: async () => ({
+ id: DASHBOARD_APP_LOCATOR,
+ initialState: getLocatorParams({ container, shouldRestoreSearchSession: false }),
+ restoreState: getLocatorParams({ container, shouldRestoreSearchSession: true }),
+ }),
+ };
+}
+
+/**
+ * Fetches the state to store when a session is saved so that this dashboard can be recreated exactly
+ * as it was.
+ */
+function getLocatorParams({
+ container,
+ shouldRestoreSearchSession,
+}: {
+ container: DashboardContainer;
+ shouldRestoreSearchSession: boolean;
+}): DashboardAppLocatorParams {
+ const {
+ data: {
+ query: {
+ queryString,
+ filterManager,
+ timefilter: { timefilter },
+ },
+ search: { session },
+ },
+ initializerContext: { kibanaVersion },
+ } = pluginServices.getServices();
+
+ const {
+ componentState: { lastSavedId },
+ explicitInput: { panels, query, viewMode },
+ } = container.getReduxEmbeddableTools().getState();
+
+ return {
+ viewMode,
+ useHash: false,
+ preserveSavedFilters: false,
+ filters: filterManager.getFilters(),
+ query: queryString.formatQuery(query) as Query,
+ dashboardId: container.getDashboardSavedObjectId(),
+ searchSessionId: shouldRestoreSearchSession ? session.getSessionId() : undefined,
+ timeRange: shouldRestoreSearchSession ? timefilter.getAbsoluteTime() : timefilter.getTime(),
+ refreshInterval: shouldRestoreSearchSession
+ ? {
+ pause: true, // force pause refresh interval when restoring a session
+ value: 0,
+ }
+ : undefined,
+ panels: lastSavedId
+ ? undefined
+ : (convertPanelMapToSavedPanels(
+ panels,
+ kibanaVersion
+ ) as DashboardAppLocatorParams['panels']),
+ };
+}
diff --git a/src/plugins/dashboard/public/dashboard_app/url/sync_dashboard_url_state.ts b/src/plugins/dashboard/public/dashboard_app/url/sync_dashboard_url_state.ts
new file mode 100644
index 000000000000..b0d37de482de
--- /dev/null
+++ b/src/plugins/dashboard/public/dashboard_app/url/sync_dashboard_url_state.ts
@@ -0,0 +1,94 @@
+/*
+ * 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 _ from 'lodash';
+import { debounceTime } from 'rxjs/operators';
+import semverSatisfies from 'semver/functions/satisfies';
+
+import { IKbnUrlStateStorage, replaceUrlHashQuery } from '@kbn/kibana-utils-plugin/public';
+
+import {
+ DashboardPanelMap,
+ SavedDashboardPanel,
+ SharedDashboardState,
+ convertSavedPanelsToPanelMap,
+ DashboardContainerByValueInput,
+} from '../../../common';
+import { DashboardContainer } from '../../dashboard_container';
+import { pluginServices } from '../../services/plugin_services';
+import { getPanelTooOldErrorString } from '../_dashboard_app_strings';
+import { DASHBOARD_STATE_STORAGE_KEY } from '../../dashboard_constants';
+import { migrateLegacyQuery } from '../../services/dashboard_saved_object/lib/load_dashboard_state_from_saved_object';
+
+/**
+ * We no longer support loading panels from a version older than 7.3 in the URL.
+ * @returns whether or not there is a panel in the URL state saved with a version before 7.3
+ */
+export const isPanelVersionTooOld = (panels: SavedDashboardPanel[]) => {
+ for (const panel of panels) {
+ if (!panel.version || semverSatisfies(panel.version, '<7.3')) return true;
+ }
+ return false;
+};
+
+/**
+ * Loads any dashboard state from the URL, and removes the state from the URL.
+ */
+export const loadAndRemoveDashboardState = (
+ kbnUrlStateStorage: IKbnUrlStateStorage
+): Partial => {
+ const {
+ notifications: { toasts },
+ } = pluginServices.getServices();
+ const rawAppStateInUrl = kbnUrlStateStorage.get(
+ DASHBOARD_STATE_STORAGE_KEY
+ );
+ if (!rawAppStateInUrl) return {};
+
+ let panelsMap: DashboardPanelMap | undefined;
+ if (rawAppStateInUrl.panels && rawAppStateInUrl.panels.length > 0) {
+ if (isPanelVersionTooOld(rawAppStateInUrl.panels)) {
+ toasts.addWarning(getPanelTooOldErrorString());
+ } else {
+ panelsMap = convertSavedPanelsToPanelMap(rawAppStateInUrl.panels);
+ }
+ }
+
+ const nextUrl = replaceUrlHashQuery(window.location.href, (hashQuery) => {
+ delete hashQuery[DASHBOARD_STATE_STORAGE_KEY];
+ return hashQuery;
+ });
+ kbnUrlStateStorage.kbnUrlControls.update(nextUrl, true);
+ const partialState: Partial = {
+ ..._.omit(rawAppStateInUrl, ['panels', 'query']),
+ ...(panelsMap ? { panels: panelsMap } : {}),
+ ...(rawAppStateInUrl.query ? { query: migrateLegacyQuery(rawAppStateInUrl.query) } : {}),
+ };
+
+ return partialState;
+};
+
+export const startSyncingDashboardUrlState = ({
+ kbnUrlStateStorage,
+ dashboardContainer,
+}: {
+ kbnUrlStateStorage: IKbnUrlStateStorage;
+ dashboardContainer: DashboardContainer;
+}) => {
+ const appStateSubscription = kbnUrlStateStorage
+ .change$(DASHBOARD_STATE_STORAGE_KEY)
+ .pipe(debounceTime(10)) // debounce URL updates so react has time to unsubscribe when changing URLs
+ .subscribe(() => {
+ const stateFromUrl = loadAndRemoveDashboardState(kbnUrlStateStorage);
+ if (Object.keys(stateFromUrl).length === 0) return;
+ dashboardContainer.updateInput(stateFromUrl);
+ });
+
+ const stopWatchingAppStateInUrl = () => appStateSubscription.unsubscribe();
+ return { stopWatchingAppStateInUrl };
+};
diff --git a/src/plugins/dashboard/public/dashboard_constants.ts b/src/plugins/dashboard/public/dashboard_constants.ts
index c4856f8e2e34..fc2d609cae6e 100644
--- a/src/plugins/dashboard/public/dashboard_constants.ts
+++ b/src/plugins/dashboard/public/dashboard_constants.ts
@@ -7,50 +7,17 @@
*/
import { ViewMode } from '@kbn/embeddable-plugin/common';
-import type { DashboardState } from './types';
+import type { DashboardContainerByValueInput } from '../common';
+// ------------------------------------------------------------------
+// URL Constants
+// ------------------------------------------------------------------
export const DASHBOARD_STATE_STORAGE_KEY = '_a';
export const GLOBAL_STATE_STORAGE_KEY = '_g';
-
-export const DASHBOARD_GRID_COLUMN_COUNT = 48;
-export const DASHBOARD_GRID_HEIGHT = 20;
-export const DEFAULT_PANEL_WIDTH = DASHBOARD_GRID_COLUMN_COUNT / 2;
-export const DEFAULT_PANEL_HEIGHT = 15;
-export const DASHBOARD_CONTAINER_TYPE = 'dashboard';
-
-export const DashboardConstants = {
- LANDING_PAGE_PATH: '/list',
- CREATE_NEW_DASHBOARD_URL: '/create',
- VIEW_DASHBOARD_URL: '/view',
- PRINT_DASHBOARD_URL: '/print',
- ADD_EMBEDDABLE_ID: 'addEmbeddableId',
- ADD_EMBEDDABLE_TYPE: 'addEmbeddableType',
- DASHBOARDS_ID: 'dashboards',
- DASHBOARD_ID: 'dashboard',
- DASHBOARD_SAVED_OBJECT_TYPE: 'dashboard',
- SEARCH_SESSION_ID: 'searchSessionId',
- CHANGE_CHECK_DEBOUNCE: 100,
- CHANGE_APPLY_DEBOUNCE: 50,
-};
-
-export const defaultDashboardState: DashboardState = {
- viewMode: ViewMode.EDIT, // new dashboards start in edit mode.
- fullScreenMode: false,
- timeRestore: false,
- query: { query: '', language: 'kuery' },
- description: '',
- filters: [],
- panels: {},
- title: '',
- tags: [],
- options: {
- useMargins: true,
- syncColors: false,
- syncCursor: true,
- syncTooltips: false,
- hidePanelTitles: false,
- },
-};
+export const LANDING_PAGE_PATH = '/list';
+export const CREATE_NEW_DASHBOARD_URL = '/create';
+export const VIEW_DASHBOARD_URL = '/view';
+export const PRINT_DASHBOARD_URL = '/print';
export const getFullPath = (aliasId?: string, id?: string) =>
`/app/dashboards#${createDashboardEditUrl(aliasId || id)}`;
@@ -61,14 +28,57 @@ export const getFullEditPath = (id?: string, editMode?: boolean) => {
export function createDashboardEditUrl(id?: string, editMode?: boolean) {
if (!id) {
- return `${DashboardConstants.CREATE_NEW_DASHBOARD_URL}`;
+ return `${CREATE_NEW_DASHBOARD_URL}`;
}
const edit = editMode ? `?${DASHBOARD_STATE_STORAGE_KEY}=(viewMode:edit)` : '';
- return `${DashboardConstants.VIEW_DASHBOARD_URL}/${id}${edit}`;
+ return `${VIEW_DASHBOARD_URL}/${id}${edit}`;
}
export function createDashboardListingFilterUrl(filter: string | undefined) {
- return filter
- ? `${DashboardConstants.LANDING_PAGE_PATH}?filter="${filter}"`
- : DashboardConstants.LANDING_PAGE_PATH;
+ return filter ? `${LANDING_PAGE_PATH}?filter="${filter}"` : LANDING_PAGE_PATH;
}
+
+// ------------------------------------------------------------------
+// Telemetry & Events
+// ------------------------------------------------------------------
+export const DASHBOARD_LOADED_EVENT = 'dashboard_loaded';
+export const DASHBOARD_UI_METRIC_ID = 'dashboard';
+
+// ------------------------------------------------------------------
+// IDs
+// ------------------------------------------------------------------
+export const DASHBOARD_APP_ID = 'dashboards';
+export const LEGACY_DASHBOARD_APP_ID = 'dashboard';
+export const SEARCH_SESSION_ID = 'searchSessionId';
+export const DASHBOARD_SAVED_OBJECT_TYPE = 'dashboard';
+
+// ------------------------------------------------------------------
+// Grid
+// ------------------------------------------------------------------
+export const DEFAULT_PANEL_HEIGHT = 15;
+export const DASHBOARD_GRID_HEIGHT = 20;
+export const DASHBOARD_GRID_COLUMN_COUNT = 48;
+export const DEFAULT_PANEL_WIDTH = DASHBOARD_GRID_COLUMN_COUNT / 2;
+
+export const CHANGE_CHECK_DEBOUNCE = 100;
+
+// ------------------------------------------------------------------
+// Default State
+// ------------------------------------------------------------------
+export const DEFAULT_DASHBOARD_INPUT: Omit = {
+ viewMode: ViewMode.EDIT, // new dashboards start in edit mode.
+ timeRestore: false,
+ query: { query: '', language: 'kuery' },
+ description: '',
+ filters: [],
+ panels: {},
+ title: '',
+ tags: [],
+
+ // options
+ useMargins: true,
+ syncColors: false,
+ syncCursor: true,
+ syncTooltips: false,
+ hidePanelTitles: false,
+};
diff --git a/src/plugins/dashboard/public/dashboard_container/_dashboard_container.scss b/src/plugins/dashboard/public/dashboard_container/_dashboard_container.scss
new file mode 100644
index 000000000000..c4107d3f235b
--- /dev/null
+++ b/src/plugins/dashboard/public/dashboard_container/_dashboard_container.scss
@@ -0,0 +1,40 @@
+@import '../../../embeddable/public/variables';
+
+@import './component/grid/index';
+@import './component/panel/index';
+@import './component/viewport/index';
+
+.dashboardViewport {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+.dashboardViewport--loading {
+ justify-content: center;
+ align-items: center;
+}
+
+.dshStartScreen {
+ text-align: center;
+}
+
+.dshStartScreen__pageContent {
+ padding: $euiSizeXXL;
+}
+
+.dshStartScreen__panelDesc {
+ max-width: 260px;
+ margin: 0 auto;
+}
+
+.dshEmptyWidget {
+ background-color: $euiColorLightestShade;
+ border: $euiBorderThin;
+ border-style: dashed;
+ border-radius: $euiBorderRadius;
+ padding: $euiSizeXXL * 2;
+ max-width: 400px;
+ margin-left: $euiSizeS;
+ text-align: center;
+}
\ No newline at end of file
diff --git a/src/plugins/dashboard/public/dashboard_container/_dashboard_container_strings.ts b/src/plugins/dashboard/public/dashboard_container/_dashboard_container_strings.ts
new file mode 100644
index 000000000000..5bd6a24d8130
--- /dev/null
+++ b/src/plugins/dashboard/public/dashboard_container/_dashboard_container_strings.ts
@@ -0,0 +1,89 @@
+/*
+ * 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';
+
+/*
+ Empty Screen
+*/
+export const emptyScreenStrings = {
+ getEmptyDashboardTitle: () =>
+ i18n.translate('dashboard.emptyDashboardTitle', {
+ defaultMessage: 'This dashboard is empty.',
+ }),
+ getEmptyDashboardAdditionalPrivilege: () =>
+ i18n.translate('dashboard.emptyDashboardAdditionalPrivilege', {
+ defaultMessage: 'You need additional privileges to edit this dashboard.',
+ }),
+ getFillDashboardTitle: () =>
+ i18n.translate('dashboard.fillDashboardTitle', {
+ defaultMessage: 'This dashboard is empty. Let\u2019s fill it up!',
+ }),
+ getHowToStartWorkingOnNewDashboardDescription: () =>
+ i18n.translate('dashboard.howToStartWorkingOnNewDashboardDescription', {
+ defaultMessage: 'Click edit in the menu bar above to start adding panels.',
+ }),
+ getHowToStartWorkingOnNewDashboardEditLinkAriaLabel: () =>
+ i18n.translate('dashboard.howToStartWorkingOnNewDashboardEditLinkAriaLabel', {
+ defaultMessage: 'Edit dashboard',
+ }),
+ getEmptyWidgetTitle: () =>
+ i18n.translate('dashboard.emptyWidget.addPanelTitle', {
+ defaultMessage: 'Add your first visualization',
+ }),
+ getEmptyWidgetDescription: () =>
+ i18n.translate('dashboard.emptyWidget.addPanelDescription', {
+ defaultMessage: 'Create content that tells a story about your data.',
+ }),
+};
+
+export const dashboardSaveToastStrings = {
+ getSuccessString: (dashTitle: string) =>
+ i18n.translate('dashboard.dashboardWasSavedSuccessMessage', {
+ defaultMessage: `Dashboard '{dashTitle}' was saved`,
+ values: { dashTitle },
+ }),
+ getFailureString: (dashTitle: string, errorMessage: string) =>
+ i18n.translate('dashboard.dashboardWasNotSavedDangerMessage', {
+ defaultMessage: `Dashboard '{dashTitle}' was not saved. Error: {errorMessage}`,
+ values: {
+ dashTitle,
+ errorMessage,
+ },
+ }),
+};
+
+export const dashboardSavedObjectErrorStrings = {
+ getDashboardGridError: (message: string) =>
+ i18n.translate('dashboard.loadingError.dashboardGridErrorMessage', {
+ defaultMessage: 'Unable to load dashboard: {message}',
+ values: { message },
+ }),
+ getErrorDeletingDashboardToast: () =>
+ i18n.translate('dashboard.deleteError.toastDescription', {
+ defaultMessage: 'Error encountered while deleting dashboard',
+ }),
+};
+
+export const panelStorageErrorStrings = {
+ getPanelsGetError: (message: string) =>
+ i18n.translate('dashboard.panelStorageError.getError', {
+ defaultMessage: 'Error encountered while fetching unsaved changes: {message}',
+ values: { message },
+ }),
+ getPanelsSetError: (message: string) =>
+ i18n.translate('dashboard.panelStorageError.setError', {
+ defaultMessage: 'Error encountered while setting unsaved changes: {message}',
+ values: { message },
+ }),
+ getPanelsClearError: (message: string) =>
+ i18n.translate('dashboard.panelStorageError.clearError', {
+ defaultMessage: 'Error encountered while clearing unsaved changes: {message}',
+ values: { message },
+ }),
+};
diff --git a/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/dashboard_container/component/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap
similarity index 98%
rename from src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap
rename to src/plugins/dashboard/public/dashboard_container/component/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap
index 6ccb34d7f52c..bfa152c38553 100644
--- a/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap
+++ b/src/plugins/dashboard/public/dashboard_container/component/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap
@@ -2,7 +2,7 @@
exports[`DashboardEmptyScreen renders correctly with edit mode 1`] = `