[Dashboard] Cleanup services (#193644)

Closes https://github.com/elastic/kibana/issues/167437

## Summary

This PR refactors the Dashboard services to no longer use the
`PluginServiceProvider` from the `PresentationUtil` plugin.

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Hannah Mudge 2024-09-26 12:10:33 -06:00 committed by GitHub
parent 2f45c90d2f
commit ce08d4e373
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
207 changed files with 1741 additions and 4765 deletions

View file

@ -7,8 +7,6 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { setStubDashboardServices } from './public/services/mocks';
/**
* CAUTION: Be very mindful of the things you import in to this `jest_setup` file - anything that is imported
* here (either directly or implicitly through dependencies) will be **unable** to be mocked elsewhere!
@ -16,4 +14,36 @@ import { setStubDashboardServices } from './public/services/mocks';
* Refer to the "Caution" section here:
* https://jestjs.io/docs/jest-object#jestmockmodulename-factory-options
*/
setStubDashboardServices();
import {
mockDashboardBackupService,
mockDashboardContentManagementCache,
mockDashboardContentManagementService,
setStubKibanaServices,
} from './public/services/mocks';
// Start the kibana services with stubs
setStubKibanaServices();
// Mock the dashboard services
jest.mock('./public/services/dashboard_content_management_service', () => {
return {
getDashboardContentManagementCache: () => mockDashboardContentManagementCache,
getDashboardContentManagementService: () => mockDashboardContentManagementService,
};
});
jest.mock('./public/services/dashboard_backup_service', () => {
return {
getDashboardBackupService: () => mockDashboardBackupService,
};
});
jest.mock('./public/services/dashboard_recently_accessed_service', () => {
return {
getDashboardRecentlyAccessedService: () => ({
add: jest.fn(),
get: jest.fn(),
get$: jest.fn(),
}),
};
});

View file

@ -8,34 +8,36 @@
*/
import React from 'react';
import { PresentationContainer } from '@kbn/presentation-containers';
import {
apiCanAccessViewMode,
apiHasLibraryTransforms,
EmbeddableApiContext,
getPanelTitle,
PublishesPanelTitle,
CanAccessViewMode,
getInheritedViewMode,
EmbeddableApiContext,
HasInPlaceLibraryTransforms,
HasLibraryTransforms,
HasParentApi,
HasType,
HasTypeDisplayName,
apiHasType,
HasUniqueId,
HasParentApi,
apiHasUniqueId,
apiHasParentApi,
HasInPlaceLibraryTransforms,
PublishesPanelTitle,
apiCanAccessViewMode,
apiHasInPlaceLibraryTransforms,
apiHasLibraryTransforms,
apiHasParentApi,
apiHasType,
apiHasUniqueId,
getInheritedViewMode,
getPanelTitle,
} from '@kbn/presentation-publishing';
import {
OnSaveProps,
SavedObjectSaveModal,
SaveResult,
SavedObjectSaveModal,
showSaveModal,
} from '@kbn/saved-objects-plugin/public';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { PresentationContainer } from '@kbn/presentation-containers';
import { pluginServices } from '../services/plugin_services';
import { coreServices } from '../services/kibana_services';
import { dashboardAddToLibraryActionStrings } from './_dashboard_actions_strings';
export const ACTION_ADD_TO_LIBRARY = 'saveToLibrary';
@ -62,14 +64,6 @@ export class AddToLibraryAction implements Action<EmbeddableApiContext> {
public readonly id = ACTION_ADD_TO_LIBRARY;
public order = 8;
private toastsService;
constructor() {
({
notifications: { toasts: this.toastsService },
} = pluginServices.getServices());
}
public getDisplayName({ embeddable }: EmbeddableApiContext) {
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
return dashboardAddToLibraryActionStrings.getDisplayName();
@ -134,12 +128,12 @@ export class AddToLibraryAction implements Action<EmbeddableApiContext> {
initialState: byRefState,
});
}
this.toastsService.addSuccess({
coreServices.notifications.toasts.addSuccess({
title: dashboardAddToLibraryActionStrings.getSuccessMessage(title ? `'${title}'` : ''),
'data-test-subj': 'addPanelToLibrarySuccess',
});
} catch (e) {
this.toastsService.addDanger({
coreServices.notifications.toasts.addDanger({
title: dashboardAddToLibraryActionStrings.getErrorMessage(title),
'data-test-subj': 'addPanelToLibraryError',
});

View file

@ -42,8 +42,6 @@ export class ClonePanelAction implements Action<EmbeddableApiContext> {
public readonly id = ACTION_CLONE_PANEL;
public order = 45;
constructor() {}
public getDisplayName({ embeddable }: EmbeddableApiContext) {
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
return dashboardClonePanelActionStrings.getDisplayName();

View file

@ -9,26 +9,26 @@
import React from 'react';
import { CoreStart } from '@kbn/core-lifecycle-browser';
import {
apiIsOfType,
apiHasUniqueId,
apiHasParentApi,
apiPublishesSavedObjectId,
HasType,
EmbeddableApiContext,
HasUniqueId,
HasParentApi,
HasType,
HasUniqueId,
PublishesSavedObjectId,
apiHasParentApi,
apiHasUniqueId,
apiIsOfType,
apiPublishesSavedObjectId,
} from '@kbn/presentation-publishing';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { DASHBOARD_CONTAINER_TYPE } from '../dashboard_container';
import { DashboardApi } from '../dashboard_api/types';
import { pluginServices } from '../services/plugin_services';
import { CopyToDashboardModal } from './copy_to_dashboard_modal';
import { DASHBOARD_CONTAINER_TYPE } from '../dashboard_container';
import { coreServices } from '../services/kibana_services';
import { getDashboardCapabilities } from '../utils/get_dashboard_capabilities';
import { dashboardCopyToDashboardActionStrings } from './_dashboard_actions_strings';
import { CopyToDashboardModal } from './copy_to_dashboard_modal';
export const ACTION_COPY_TO_DASHBOARD = 'copyToDashboard';
@ -60,16 +60,6 @@ export class CopyToDashboardAction implements Action<EmbeddableApiContext> {
public readonly id = ACTION_COPY_TO_DASHBOARD;
public order = 1;
private dashboardCapabilities;
private openModal;
constructor(private core: CoreStart) {
({
dashboardCapabilities: this.dashboardCapabilities,
overlays: { openModal: this.openModal },
} = pluginServices.getServices());
}
public getDisplayName({ embeddable }: EmbeddableApiContext) {
if (!apiIsCompatible(embeddable)) throw new IncompatibleActionError();
@ -84,15 +74,15 @@ export class CopyToDashboardAction implements Action<EmbeddableApiContext> {
public async isCompatible({ embeddable }: EmbeddableApiContext) {
if (!apiIsCompatible(embeddable)) return false;
const { createNew: canCreateNew, showWriteControls: canEditExisting } =
this.dashboardCapabilities;
getDashboardCapabilities();
return Boolean(canCreateNew || canEditExisting);
}
public async execute({ embeddable }: EmbeddableApiContext) {
if (!apiIsCompatible(embeddable)) throw new IncompatibleActionError();
const { theme, i18n } = this.core;
const session = this.openModal(
const { theme, i18n } = coreServices;
const session = coreServices.overlays.openModal(
toMountPoint(<CopyToDashboardModal closeModal={() => session.close()} api={embeddable} />, {
theme,
i18n,

View file

@ -22,11 +22,12 @@ import { EmbeddablePackageState, PanelNotFoundError } from '@kbn/embeddable-plug
import { apiHasSnapshottableState } from '@kbn/presentation-containers/interfaces/serialized_state';
import { LazyDashboardPicker, withSuspense } from '@kbn/presentation-util-plugin/public';
import { omit } from 'lodash';
import React, { useCallback, useState } from 'react';
import { createDashboardEditUrl, CREATE_NEW_DASHBOARD_URL } from '../dashboard_constants';
import { pluginServices } from '../services/plugin_services';
import { CopyToDashboardAPI } from './copy_to_dashboard_action';
import React, { useCallback, useMemo, useState } from 'react';
import { CREATE_NEW_DASHBOARD_URL, createDashboardEditUrl } from '../dashboard_constants';
import { embeddableService } from '../services/kibana_services';
import { getDashboardCapabilities } from '../utils/get_dashboard_capabilities';
import { dashboardCopyToDashboardActionStrings } from './_dashboard_actions_strings';
import { CopyToDashboardAPI } from './copy_to_dashboard_action';
interface CopyToDashboardModalProps {
api: CopyToDashboardAPI;
@ -36,11 +37,11 @@ interface CopyToDashboardModalProps {
const DashboardPicker = withSuspense(LazyDashboardPicker);
export function CopyToDashboardModal({ api, closeModal }: CopyToDashboardModalProps) {
const {
embeddable: { getStateTransfer },
dashboardCapabilities: { createNew: canCreateNew, showWriteControls: canEditExisting },
} = pluginServices.getServices();
const stateTransfer = getStateTransfer();
const stateTransfer = useMemo(() => embeddableService.getStateTransfer(), []);
const { createNew: canCreateNew, showWriteControls: canEditExisting } = useMemo(
() => getDashboardCapabilities(),
[]
);
const [dashboardOption, setDashboardOption] = useState<'new' | 'existing'>('existing');
const [selectedDashboard, setSelectedDashboard] = useState<{ id: string; name: string } | null>(

View file

@ -31,8 +31,6 @@ export class ExpandPanelAction implements Action<EmbeddableApiContext> {
public readonly id = ACTION_EXPAND_PANEL;
public order = 7;
constructor() {}
public getDisplayName({ embeddable }: EmbeddableApiContext) {
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
return embeddable.parentApi.expandedPanelId.value

View file

@ -14,16 +14,16 @@ import { downloadMultipleAs } from '@kbn/share-plugin/public';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import {
apiHasInspectorAdapters,
HasInspectorAdapters,
apiHasInspectorAdapters,
type Adapters,
} from '@kbn/inspector-plugin/public';
import {
EmbeddableApiContext,
getPanelTitle,
PublishesPanelTitle,
getPanelTitle,
} from '@kbn/presentation-publishing';
import { pluginServices } from '../services/plugin_services';
import { coreServices, fieldFormatService } from '../services/kibana_services';
import { dashboardExportCsvActionStrings } from './_dashboard_actions_strings';
export const ACTION_EXPORT_CSV = 'ACTION_EXPORT_CSV';
@ -43,16 +43,6 @@ export class ExportCSVAction implements Action<ExportContext> {
public readonly type = ACTION_EXPORT_CSV;
public readonly order = 18; // right after Export in discover which is 19
private fieldFormats;
private uiSettings;
constructor() {
({
data: { fieldFormats: this.fieldFormats },
settings: { uiSettings: this.uiSettings },
} = pluginServices.getServices());
}
public getIconType() {
return 'exportAction';
}
@ -70,9 +60,7 @@ export class ExportCSVAction implements Action<ExportContext> {
};
private getFormatter = (): FormatFactory | undefined => {
if (this.fieldFormats) {
return this.fieldFormats.deserialize;
}
return fieldFormatService.deserialize;
};
private getDataTableContent = (adapters: Adapters | undefined) => {
@ -105,8 +93,8 @@ export class ExportCSVAction implements Action<ExportContext> {
memo[`${getPanelTitle(embeddable) || untitledFilename}${postFix}.csv`] = {
content: exporters.datatableToCSV(datatable, {
csvSeparator: this.uiSettings.get('csv:separator', ','),
quoteValues: this.uiSettings.get('csv:quoteValues', true),
csvSeparator: coreServices.uiSettings.get('csv:separator', ','),
quoteValues: coreServices.uiSettings.get('csv:quoteValues', true),
formatFactory,
escapeFormulaValues: false,
}),

View file

@ -8,28 +8,28 @@
*/
import React from 'react';
import { merge } from 'rxjs';
import { isOfAggregateQueryType, isOfQueryType } from '@kbn/es-query';
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import {
apiCanAccessViewMode,
apiPublishesPartialUnifiedSearch,
apiHasUniqueId,
CanAccessViewMode,
EmbeddableApiContext,
getInheritedViewMode,
getViewModeSubject,
HasParentApi,
PublishesUnifiedSearch,
HasUniqueId,
PublishesDataViews,
PublishesUnifiedSearch,
apiCanAccessViewMode,
apiHasUniqueId,
apiPublishesPartialUnifiedSearch,
getInheritedViewMode,
getViewModeSubject,
} from '@kbn/presentation-publishing';
import { merge } from 'rxjs';
import { pluginServices } from '../services/plugin_services';
import { FiltersNotificationPopover } from './filters_notification_popover';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { coreServices } from '../services/kibana_services';
import { dashboardFilterNotificationActionStrings } from './_dashboard_actions_strings';
import { FiltersNotificationPopover } from './filters_notification_popover';
export const BADGE_FILTERS_NOTIFICATION = 'ACTION_FILTERS_NOTIFICATION';
@ -58,18 +58,12 @@ export class FiltersNotificationAction implements Action<EmbeddableApiContext> {
public readonly type = BADGE_FILTERS_NOTIFICATION;
public readonly order = 2;
private settingsService;
constructor() {
({ settings: this.settingsService } = pluginServices.getServices());
}
public readonly MenuItem = ({ context }: { context: EmbeddableApiContext }) => {
const { embeddable } = context;
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
const { Provider: KibanaReactContextProvider } = createKibanaReactContext({
uiSettings: this.settingsService.uiSettings,
uiSettings: coreServices.uiSettings,
});
return (

View file

@ -7,7 +7,6 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { CoreStart } from '@kbn/core/public';
import { CONTEXT_MENU_TRIGGER, PANEL_NOTIFICATION_TRIGGER } from '@kbn/embeddable-plugin/public';
import { DashboardStartDependencies } from '../plugin';
import { AddToLibraryAction } from './add_to_library_action';
@ -21,13 +20,11 @@ import { UnlinkFromLibraryAction } from './unlink_from_library_action';
import { LegacyUnlinkFromLibraryAction } from './legacy_unlink_from_library_action';
interface BuildAllDashboardActionsProps {
core: CoreStart;
allowByValueEmbeddables?: boolean;
plugins: DashboardStartDependencies;
}
export const buildAllDashboardActions = async ({
core,
plugins,
allowByValueEmbeddables,
}: BuildAllDashboardActionsProps) => {
@ -66,7 +63,7 @@ export const buildAllDashboardActions = async ({
uiActions.registerAction(legacyUnlinkFromLibraryAction);
uiActions.attachAction(CONTEXT_MENU_TRIGGER, legacyUnlinkFromLibraryAction.id);
const copyToDashboardAction = new CopyToDashboardAction(core);
const copyToDashboardAction = new CopyToDashboardAction();
uiActions.registerAction(copyToDashboardAction);
uiActions.attachAction(CONTEXT_MENU_TRIGGER, copyToDashboardAction.id);
}

View file

@ -9,11 +9,11 @@
import { PublishesViewMode, ViewMode } from '@kbn/presentation-publishing';
import { BehaviorSubject } from 'rxjs';
import { pluginServices } from '../services/plugin_services';
import {
LegacyAddToLibraryAction,
LegacyAddPanelToLibraryActionApi,
} from './legacy_add_to_library_action';
import { coreServices } from '../services/kibana_services';
describe('Add to library action', () => {
let action: LegacyAddToLibraryAction;
@ -62,7 +62,7 @@ describe('Add to library action', () => {
it('shows a toast with a title from the API when successful', async () => {
await action.execute(context);
expect(pluginServices.getServices().notifications.toasts.addSuccess).toHaveBeenCalledWith({
expect(coreServices.notifications.toasts.addSuccess).toHaveBeenCalledWith({
'data-test-subj': 'addPanelToLibrarySuccess',
title: "Panel 'A very compatible API' was added to the library",
});
@ -71,7 +71,7 @@ describe('Add to library action', () => {
it('shows a danger toast when the link operation is unsuccessful', async () => {
context.embeddable.linkToLibrary = jest.fn().mockRejectedValue(new Error('Oh dang'));
await action.execute(context);
expect(pluginServices.getServices().notifications.toasts.addDanger).toHaveBeenCalledWith({
expect(coreServices.notifications.toasts.addDanger).toHaveBeenCalledWith({
'data-test-subj': 'addPanelToLibraryError',
title: 'An error was encountered adding panel A very compatible API to the library',
});

View file

@ -18,8 +18,9 @@ import {
HasLegacyLibraryTransforms,
} from '@kbn/presentation-publishing';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { pluginServices } from '../services/plugin_services';
import { dashboardAddToLibraryActionStrings } from './_dashboard_actions_strings';
import { coreServices } from '../services/kibana_services';
export const ACTION_LEGACY_ADD_TO_LIBRARY = 'legacySaveToLibrary';
@ -35,14 +36,6 @@ export class LegacyAddToLibraryAction implements Action<EmbeddableApiContext> {
public readonly id = ACTION_LEGACY_ADD_TO_LIBRARY;
public order = 15;
private toastsService;
constructor() {
({
notifications: { toasts: this.toastsService },
} = pluginServices.getServices());
}
public getDisplayName({ embeddable }: EmbeddableApiContext) {
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
return dashboardAddToLibraryActionStrings.getDisplayName();
@ -63,14 +56,14 @@ export class LegacyAddToLibraryAction implements Action<EmbeddableApiContext> {
const panelTitle = getPanelTitle(embeddable);
try {
await embeddable.linkToLibrary();
this.toastsService.addSuccess({
coreServices.notifications.toasts.addSuccess({
title: dashboardAddToLibraryActionStrings.getSuccessMessage(
panelTitle ? `'${panelTitle}'` : ''
),
'data-test-subj': 'addPanelToLibrarySuccess',
});
} catch (e) {
this.toastsService.addDanger({
coreServices.notifications.toasts.addDanger({
title: dashboardAddToLibraryActionStrings.getErrorMessage(panelTitle),
'data-test-subj': 'addPanelToLibraryError',
});

View file

@ -9,7 +9,7 @@
import { PublishesViewMode, ViewMode } from '@kbn/presentation-publishing';
import { BehaviorSubject } from 'rxjs';
import { pluginServices } from '../services/plugin_services';
import { coreServices } from '../services/kibana_services';
import {
LegacyUnlinkFromLibraryAction,
LegacyUnlinkPanelFromLibraryActionApi,
@ -61,7 +61,7 @@ describe('Unlink from library action', () => {
it('shows a toast with a title from the API when successful', async () => {
await action.execute(context);
expect(pluginServices.getServices().notifications.toasts.addSuccess).toHaveBeenCalledWith({
expect(coreServices.notifications.toasts.addSuccess).toHaveBeenCalledWith({
'data-test-subj': 'unlinkPanelSuccess',
title: "Panel 'A very compatible API' is no longer connected to the library.",
});
@ -70,7 +70,7 @@ describe('Unlink from library action', () => {
it('shows a danger toast when the link operation is unsuccessful', async () => {
context.embeddable.unlinkFromLibrary = jest.fn().mockRejectedValue(new Error('Oh dang'));
await action.execute(context);
expect(pluginServices.getServices().notifications.toasts.addDanger).toHaveBeenCalledWith({
expect(coreServices.notifications.toasts.addDanger).toHaveBeenCalledWith({
'data-test-subj': 'unlinkPanelFailure',
title: "An error occured while unlinking 'A very compatible API' from the library.",
});

View file

@ -19,8 +19,8 @@ import {
PublishesPanelTitle,
HasLegacyLibraryTransforms,
} from '@kbn/presentation-publishing';
import { pluginServices } from '../services/plugin_services';
import { dashboardUnlinkFromLibraryActionStrings } from './_dashboard_actions_strings';
import { coreServices } from '../services/kibana_services';
export const ACTION_LEGACY_UNLINK_FROM_LIBRARY = 'legacyUnlinkFromLibrary';
@ -38,14 +38,6 @@ export class LegacyUnlinkFromLibraryAction implements Action<EmbeddableApiContex
public readonly id = ACTION_LEGACY_UNLINK_FROM_LIBRARY;
public order = 15;
private toastsService;
constructor() {
({
notifications: { toasts: this.toastsService },
} = pluginServices.getServices());
}
public getDisplayName({ embeddable }: EmbeddableApiContext) {
if (!legacyUnlinkActionIsCompatible(embeddable)) throw new IncompatibleActionError();
return dashboardUnlinkFromLibraryActionStrings.getDisplayName();
@ -66,12 +58,12 @@ export class LegacyUnlinkFromLibraryAction implements Action<EmbeddableApiContex
const title = getPanelTitle(embeddable);
try {
await embeddable.unlinkFromLibrary();
this.toastsService.addSuccess({
coreServices.notifications.toasts.addSuccess({
title: dashboardUnlinkFromLibraryActionStrings.getSuccessMessage(title ? `'${title}'` : ''),
'data-test-subj': 'unlinkPanelSuccess',
});
} catch (e) {
this.toastsService.addDanger({
coreServices.notifications.toasts.addDanger({
title: dashboardUnlinkFromLibraryActionStrings.getFailureMessage(title ? `'${title}'` : ''),
'data-test-subj': 'unlinkPanelFailure',
});

View file

@ -7,28 +7,28 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { PresentationContainer } from '@kbn/presentation-containers';
import {
apiCanAccessViewMode,
apiHasLibraryTransforms,
CanAccessViewMode,
EmbeddableApiContext,
getInheritedViewMode,
getPanelTitle,
PublishesPanelTitle,
HasInPlaceLibraryTransforms,
HasLibraryTransforms,
HasParentApi,
apiHasParentApi,
HasUniqueId,
apiHasUniqueId,
HasType,
apiHasType,
HasInPlaceLibraryTransforms,
HasUniqueId,
PublishesPanelTitle,
apiCanAccessViewMode,
apiHasInPlaceLibraryTransforms,
apiHasLibraryTransforms,
apiHasParentApi,
apiHasType,
apiHasUniqueId,
getInheritedViewMode,
getPanelTitle,
} from '@kbn/presentation-publishing';
import { PresentationContainer } from '@kbn/presentation-containers';
import { pluginServices } from '../services/plugin_services';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { coreServices } from '../services/kibana_services';
import { dashboardUnlinkFromLibraryActionStrings } from './_dashboard_actions_strings';
export const ACTION_UNLINK_FROM_LIBRARY = 'unlinkFromLibrary';
@ -55,14 +55,6 @@ export class UnlinkFromLibraryAction implements Action<EmbeddableApiContext> {
public readonly id = ACTION_UNLINK_FROM_LIBRARY;
public order = 15;
private toastsService;
constructor() {
({
notifications: { toasts: this.toastsService },
} = pluginServices.getServices());
}
public getDisplayName({ embeddable }: EmbeddableApiContext) {
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
return dashboardUnlinkFromLibraryActionStrings.getDisplayName();
@ -107,12 +99,12 @@ export class UnlinkFromLibraryAction implements Action<EmbeddableApiContext> {
} else {
throw new IncompatibleActionError();
}
this.toastsService.addSuccess({
coreServices.notifications.toasts.addSuccess({
title: dashboardUnlinkFromLibraryActionStrings.getSuccessMessage(title ? `'${title}'` : ''),
'data-test-subj': 'unlinkPanelSuccess',
});
} catch (e) {
this.toastsService.addDanger({
coreServices.notifications.toasts.addDanger({
title: dashboardUnlinkFromLibraryActionStrings.getFailureMessage(title ? `'${title}'` : ''),
'data-test-subj': 'unlinkPanelFailure',
});

View file

@ -31,7 +31,7 @@ import { ControlGroupApi, ControlGroupSerializedState } from '@kbn/controls-plug
import { Filter, Query, TimeRange } from '@kbn/es-query';
import { DefaultEmbeddableApi, ErrorEmbeddable, IEmbeddable } from '@kbn/embeddable-plugin/public';
import { DashboardPanelMap, DashboardPanelState } from '../../common';
import { SaveDashboardReturn } from '../services/dashboard_content_management/types';
import { SaveDashboardReturn } from '../services/dashboard_content_management_service/types';
import { DashboardStateFromSettingsFlyout, UnsavedPanelState } from '../dashboard_container/types';
export type DashboardApi = CanExpandPanels &

View file

@ -7,22 +7,25 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { render, waitFor } from '@testing-library/react';
import { MemoryHistory, createMemoryHistory } from 'history';
import React, { useEffect } from 'react';
import { render, waitFor } from '@testing-library/react';
import { DashboardApi } from '..';
import type { DashboardRendererProps } from '../dashboard_container/external_api/dashboard_renderer';
import { LazyDashboardRenderer } from '../dashboard_container/external_api/lazy_dashboard_renderer';
import { DashboardTopNav } from '../dashboard_top_nav';
import { buildMockDashboard } from '../mocks';
import { dataService } from '../services/kibana_services';
import { DashboardApp } from './dashboard_app';
import * as dashboardRendererStuff from '../dashboard_container/external_api/lazy_dashboard_renderer';
import { DashboardApi } from '..';
/* These tests circumvent the need to test the router and legacy code
/* the dashboard app will be passed the expanded panel id from the DashboardRouter through mountApp()
/* @link https://github.com/elastic/kibana/pull/190086/
*/
jest.mock('../dashboard_container/external_api/lazy_dashboard_renderer');
jest.mock('../dashboard_top_nav');
describe('Dashboard App', () => {
dataService.query.filterManager.getFilters = jest.fn().mockImplementation(() => []);
const mockDashboard = buildMockDashboard();
let mockHistory: MemoryHistory;
// this is in url_utils dashboardApi expandedPanel subscription
@ -35,19 +38,20 @@ describe('Dashboard App', () => {
historySpy = jest.spyOn(mockHistory, 'replace');
/**
* Mock the LazyDashboardRenderer component to avoid rendering the actual dashboard
* Mock the DashboardTopNav + LazyDashboardRenderer component to avoid rendering the actual dashboard
* and hitting errors that aren't relevant
*/
jest
.spyOn(dashboardRendererStuff, 'LazyDashboardRenderer')
// we need overwrite the onApiAvailable prop to get the dashboard Api in the dashboard app
.mockImplementation(({ onApiAvailable }: DashboardRendererProps) => {
(DashboardTopNav as jest.Mock).mockImplementation(() => <>Top nav</>);
(LazyDashboardRenderer as jest.Mock).mockImplementation(
({ onApiAvailable }: DashboardRendererProps) => {
// we need overwrite the onApiAvailable prop to get access to the dashboard API in this test
useEffect(() => {
onApiAvailable?.(mockDashboard as DashboardApi);
}, [onApiAvailable]);
return <div>Test renderer</div>;
});
}
);
});
beforeEach(() => {

View file

@ -7,46 +7,52 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { v4 as uuidv4 } from 'uuid';
import { History } from 'history';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import useMount from 'react-use/lib/useMount';
import useObservable from 'react-use/lib/useObservable';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { debounceTime } from 'rxjs';
import { v4 as uuidv4 } from 'uuid';
import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
import { createKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public';
import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics';
import { debounceTime } from 'rxjs';
import { DashboardApi, DashboardRenderer } from '..';
import { SharedDashboardState } from '../../common';
import {
DASHBOARD_APP_ID,
DASHBOARD_STATE_STORAGE_KEY,
createDashboardEditUrl,
} from '../dashboard_constants';
import type { DashboardCreationOptions } from '../dashboard_container/embeddable/dashboard_container_factory';
import { DashboardRedirect } from '../dashboard_container/types';
import { DashboardTopNav } from '../dashboard_top_nav';
import {
coreServices,
dataService,
embeddableService,
screenshotModeService,
shareService,
} from '../services/kibana_services';
import { useDashboardMountContext } from './hooks/dashboard_mount_context';
import { useDashboardOutcomeValidation } from './hooks/use_dashboard_outcome_validation';
import { useObservabilityAIAssistantContext } from './hooks/use_observability_ai_assistant_context';
import { loadDashboardHistoryLocationState } from './locator/load_dashboard_history_location_state';
import {
DashboardAppNoDataPage,
isDashboardAppInNoDataState,
} from './no_data/dashboard_app_no_data';
import { loadAndRemoveDashboardState, startSyncingExpandedPanelState } from './url/url_utils';
import {
getSessionURLObservable,
getSearchSessionIdFromURL,
removeSearchSessionIdFromURL,
createSessionRestorationDataProvider,
} from './url/search_sessions_integration';
import { DashboardApi, DashboardRenderer } from '..';
import { type DashboardEmbedSettings } from './types';
import { pluginServices } from '../services/plugin_services';
import { DashboardRedirect } from '../dashboard_container/types';
import { useDashboardMountContext } from './hooks/dashboard_mount_context';
import {
createDashboardEditUrl,
DASHBOARD_APP_ID,
DASHBOARD_STATE_STORAGE_KEY,
} from '../dashboard_constants';
import { useDashboardOutcomeValidation } from './hooks/use_dashboard_outcome_validation';
import { loadDashboardHistoryLocationState } from './locator/load_dashboard_history_location_state';
import type { DashboardCreationOptions } from '../dashboard_container/embeddable/dashboard_container_factory';
import { DashboardTopNav } from '../dashboard_top_nav';
import { DashboardTabTitleSetter } from './tab_title_setter/dashboard_tab_title_setter';
import { useObservabilityAIAssistantContext } from './hooks/use_observability_ai_assistant_context';
import { SharedDashboardState } from '../../common';
import { type DashboardEmbedSettings } from './types';
import {
createSessionRestorationDataProvider,
getSearchSessionIdFromURL,
getSessionURLObservable,
removeSearchSessionIdFromURL,
} from './url/search_sessions_integration';
import { loadAndRemoveDashboardState, startSyncingExpandedPanelState } from './url/url_utils';
export interface DashboardAppProps {
history: History;
@ -71,31 +77,15 @@ export function DashboardApp({
});
const [dashboardApi, setDashboardApi] = useState<DashboardApi | undefined>(undefined);
/**
* Unpack & set up dashboard services
*/
const {
screenshotMode: { isScreenshotMode, getScreenshotContext },
coreContext: { executionContext },
embeddable: { getStateTransfer },
notifications: { toasts },
settings: { uiSettings },
data: { search, dataViews },
customBranding,
share: { url },
observabilityAIAssistant,
} = pluginServices.getServices();
const showPlainSpinner = useObservable(customBranding.hasCustomBranding$, false);
const showPlainSpinner = useObservable(coreServices.customBranding.hasCustomBranding$, false);
const { scopedHistory: getScopedHistory } = useDashboardMountContext();
useObservabilityAIAssistantContext({
observabilityAIAssistant: observabilityAIAssistant.start,
dashboardApi,
search,
dataViews,
});
useExecutionContext(executionContext, {
useExecutionContext(coreServices.executionContext, {
type: 'application',
page: 'app',
id: savedDashboardId || 'new',
@ -105,10 +95,10 @@ export function DashboardApp({
() =>
createKbnUrlStateStorage({
history,
useHash: uiSettings.get('state:storeInSessionStorage'),
...withNotifyOnErrors(toasts),
useHash: coreServices.uiSettings.get('state:storeInSessionStorage'),
...withNotifyOnErrors(coreServices.notifications.toasts),
}),
[toasts, history, uiSettings]
[history]
);
/**
@ -116,9 +106,9 @@ export function DashboardApp({
*/
useEffect(() => {
return () => {
search.session.clear();
dataService.search.session.clear();
};
}, [search.session]);
}, []);
/**
* Validate saved object load outcome
@ -141,7 +131,8 @@ export function DashboardApp({
...stateFromLocator,
// if print mode is active, force viewMode.PRINT
...(isScreenshotMode() && getScreenshotContext('layout') === 'print'
...(screenshotModeService.isScreenshotMode() &&
screenshotModeService.getScreenshotContext('layout') === 'print'
? { viewMode: ViewMode.PRINT }
: {}),
};
@ -149,7 +140,7 @@ export function DashboardApp({
return Promise.resolve<DashboardCreationOptions>({
getIncomingEmbeddable: () =>
getStateTransfer().getIncomingEmbeddablePackage(DASHBOARD_APP_ID, true),
embeddableService.getStateTransfer().getIncomingEmbeddablePackage(DASHBOARD_APP_ID, true),
// integrations
useControlGroupIntegration: true,
@ -174,16 +165,7 @@ export function DashboardApp({
getCurrentPath: () => `#${createDashboardEditUrl(dashboardId)}`,
}),
});
}, [
history,
embedSettings,
validateOutcome,
getScopedHistory,
isScreenshotMode,
getStateTransfer,
kbnUrlStateStorage,
getScreenshotContext,
]);
}, [history, embedSettings, validateOutcome, getScopedHistory, kbnUrlStateStorage]);
useEffect(() => {
if (!dashboardApi) return;
@ -208,7 +190,7 @@ export function DashboardApp({
return () => appStateSubscription.unsubscribe();
}, [dashboardApi, kbnUrlStateStorage, savedDashboardId]);
const locator = useMemo(() => url?.locators.get(DASHBOARD_APP_LOCATOR), [url]);
const locator = useMemo(() => shareService?.url.locators.get(DASHBOARD_APP_LOCATOR), []);
return showNoDataPage ? (
<DashboardAppNoDataPage onDataViewCreated={() => setShowNoDataPage(false)} />

View file

@ -9,32 +9,33 @@
import './_dashboard_app.scss';
import React from 'react';
import { parse, ParsedQuery } from 'query-string';
import { render, unmountComponentAtNode } from 'react-dom';
import { HashRouter, RouteComponentProps, Redirect } from 'react-router-dom';
import { Routes, Route } from '@kbn/shared-ux-router';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { AppMountParameters, CoreStart } from '@kbn/core/public';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { createKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { Route, Routes } from '@kbn/shared-ux-router';
import { parse, ParsedQuery } from 'query-string';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { HashRouter, Redirect, RouteComponentProps } from 'react-router-dom';
import {
createDashboardListingFilterUrl,
CREATE_NEW_DASHBOARD_URL,
createDashboardEditUrl,
createDashboardListingFilterUrl,
DASHBOARD_APP_ID,
LANDING_PAGE_PATH,
VIEW_DASHBOARD_URL,
} from '../dashboard_constants';
import { DashboardApp } from './dashboard_app';
import { pluginServices } from '../services/plugin_services';
import { RedirectToProps } from '../dashboard_container/types';
import { createDashboardEditUrl } from '../dashboard_constants';
import { DashboardNoMatch } from './listing_page/dashboard_no_match';
import { DashboardMountContext } from './hooks/dashboard_mount_context';
import { DashboardEmbedSettings, DashboardMountContextProps } from './types';
import { DashboardListingPage } from './listing_page/dashboard_listing_page';
import { coreServices, dataService, embeddableService } from '../services/kibana_services';
import { getDashboardCapabilities } from '../utils/get_dashboard_capabilities';
import { dashboardReadonlyBadge, getDashboardPageTitle } from './_dashboard_app_strings';
import { DashboardApp } from './dashboard_app';
import { DashboardMountContext } from './hooks/dashboard_mount_context';
import { DashboardListingPage } from './listing_page/dashboard_listing_page';
import { DashboardNoMatch } from './listing_page/dashboard_no_match';
import { DashboardEmbedSettings, DashboardMountContextProps } from './types';
export const dashboardUrlParams = {
showTopMenu: 'show-top-menu',
@ -56,24 +57,13 @@ export async function mountApp({
appUnMounted,
mountContext,
}: DashboardMountProps) {
const {
chrome: { setBadge, docTitle, setHelpExtension },
dashboardCapabilities: { showWriteControls },
documentationLinks: { dashboardDocLink },
application: { navigateToApp },
settings: { uiSettings },
data: dataStart,
notifications,
embeddable,
} = pluginServices.getServices();
let globalEmbedSettings: DashboardEmbedSettings | undefined;
const getUrlStateStorage = (history: RouteComponentProps['history']) =>
createKbnUrlStateStorage({
history,
useHash: uiSettings.get('state:storeInSessionStorage'),
...withNotifyOnErrors(notifications.toasts),
useHash: coreServices.uiSettings.get('state:storeInSessionStorage'),
...withNotifyOnErrors(coreServices.notifications.toasts),
});
const redirect = (redirectTo: RedirectToProps) => {
@ -87,7 +77,11 @@ export async function mountApp({
} else {
path = createDashboardListingFilterUrl(redirectTo.filter);
}
navigateToApp(DASHBOARD_APP_ID, { path: `#/${path}`, state, replace: redirectTo.useReplace });
coreServices.application.navigateToApp(DASHBOARD_APP_ID, {
path: `#/${path}`,
state,
replace: redirectTo.useReplace,
});
};
const getDashboardEmbedSettings = (
@ -120,7 +114,7 @@ export async function mountApp({
};
const renderListingPage = (routeProps: RouteComponentProps) => {
docTitle.change(getDashboardPageTitle());
coreServices.chrome.docTitle.change(getDashboardPageTitle());
const routeParams = parse(routeProps.history.location.search);
const title = (routeParams.title as string) || undefined;
const filter = (routeParams.filter as string) || undefined;
@ -139,10 +133,10 @@ export async function mountApp({
};
const hasEmbeddableIncoming = Boolean(
embeddable.getStateTransfer().getIncomingEmbeddablePackage(DASHBOARD_APP_ID, false)
embeddableService.getStateTransfer().getIncomingEmbeddablePackage(DASHBOARD_APP_ID, false)
);
if (!hasEmbeddableIncoming) {
dataStart.dataViews.clearCache();
dataService.dataViews.clearCache();
}
// dispatch synthetic hash change event to update hash history objects
@ -175,18 +169,18 @@ export async function mountApp({
</KibanaRenderContextProvider>
);
setHelpExtension({
coreServices.chrome.setHelpExtension({
appName: getDashboardPageTitle(),
links: [
{
linkType: 'documentation',
href: `${dashboardDocLink}`,
href: `${coreServices.docLinks.links.dashboard.guide}`,
},
],
});
if (!showWriteControls) {
setBadge({
if (!getDashboardCapabilities().showWriteControls) {
coreServices.chrome.setBadge({
text: dashboardReadonlyBadge.getText(),
tooltip: dashboardReadonlyBadge.getTooltip(),
iconType: 'glasses',
@ -194,7 +188,7 @@ export async function mountApp({
}
render(app, element);
return () => {
dataStart.search.session.clear();
dataService.search.session.clear();
unlistenParentHistory();
unmountComponentAtNode(element);
appUnMounted();

View file

@ -9,11 +9,11 @@
import { useCallback, useMemo, useState } from 'react';
import { pluginServices } from '../../services/plugin_services';
import { createDashboardEditUrl } from '../../dashboard_constants';
import { useDashboardMountContext } from './dashboard_mount_context';
import { LoadDashboardReturn } from '../../services/dashboard_content_management/types';
import { DashboardCreationOptions } from '../..';
import { createDashboardEditUrl } from '../../dashboard_constants';
import { LoadDashboardReturn } from '../../services/dashboard_content_management_service/types';
import { screenshotModeService, spacesService } from '../../services/kibana_services';
import { useDashboardMountContext } from './dashboard_mount_context';
export const useDashboardOutcomeValidation = () => {
const [aliasId, setAliasId] = useState<string>();
@ -23,11 +23,6 @@ export const useDashboardOutcomeValidation = () => {
const { scopedHistory: getScopedHistory } = useDashboardMountContext();
const scopedHistory = getScopedHistory?.();
/**
* Unpack dashboard services
*/
const { screenshotMode, spaces } = pluginServices.getServices();
const validateOutcome: DashboardCreationOptions['validateLoadedSavedObject'] = useCallback(
({ dashboardFound, resolveMeta, dashboardId }: LoadDashboardReturn) => {
if (!dashboardFound) {
@ -41,10 +36,10 @@ export const useDashboardOutcomeValidation = () => {
*/
if (loadOutcome === 'aliasMatch' && dashboardId && alias) {
const path = scopedHistory.location.hash.replace(dashboardId, alias);
if (screenshotMode.isScreenshotMode()) {
if (screenshotModeService.isScreenshotMode()) {
scopedHistory.replace(path); // redirect without the toast when in screenshot mode.
} else {
spaces.redirectLegacyUrl?.({ path, aliasPurpose });
spacesService?.ui.redirectLegacyUrl({ path, aliasPurpose });
}
return 'redirected'; // redirected. Stop loading dashboard.
}
@ -54,20 +49,20 @@ export const useDashboardOutcomeValidation = () => {
}
return 'valid';
},
[scopedHistory, screenshotMode, spaces]
[scopedHistory]
);
const getLegacyConflictWarning = useMemo(() => {
if (savedObjectId && outcome === 'conflict' && aliasId) {
return () =>
spaces.getLegacyUrlConflict?.({
spacesService?.ui.components.getLegacyUrlConflict({
currentObjectId: savedObjectId,
otherObjectId: aliasId,
otherObjectPath: `#${createDashboardEditUrl(aliasId)}${scopedHistory.location.search}`,
});
}
return null;
}, [aliasId, outcome, savedObjectId, scopedHistory, spaces]);
}, [aliasId, outcome, savedObjectId, scopedHistory]);
return { validateOutcome, getLegacyConflictWarning };
};

View file

@ -7,28 +7,26 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public';
import { useEffect } from 'react';
import { getESQLQueryColumns } from '@kbn/esql-utils';
import type { ISearchStart } from '@kbn/data-plugin/public';
import {
LensConfigBuilder,
LensDataset,
type LensConfig,
type LensMetricConfig,
type LensPieConfig,
type LensGaugeConfig,
type LensXYConfig,
type LensHeatmapConfig,
type LensMetricConfig,
type LensMosaicConfig,
type LensPieConfig,
type LensRegionMapConfig,
type LensTableConfig,
type LensTagCloudConfig,
type LensTreeMapConfig,
LensDataset,
type LensXYConfig,
} from '@kbn/lens-embeddable-utils/config_builder';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { LensEmbeddableInput } from '@kbn/lens-plugin/public';
import { useEffect } from 'react';
import { DashboardApi } from '../../dashboard_api/types';
import { dataService, observabilityAssistantService } from '../../services/kibana_services';
const chartTypes = [
'xy',
@ -45,25 +43,19 @@ const chartTypes = [
] as const;
export function useObservabilityAIAssistantContext({
observabilityAIAssistant,
dashboardApi,
search,
dataViews,
}: {
observabilityAIAssistant: ObservabilityAIAssistantPublicStart | undefined;
dashboardApi: DashboardApi | undefined;
search: ISearchStart;
dataViews: DataViewsPublicPluginStart;
}) {
useEffect(() => {
if (!observabilityAIAssistant) {
if (!observabilityAssistantService) {
return;
}
const {
service: { setScreenContext },
createScreenContextAction,
} = observabilityAIAssistant;
} = observabilityAssistantService;
return setScreenContext({
screenDescription:
@ -205,12 +197,12 @@ export function useObservabilityAIAssistantContext({
const [columns] = await Promise.all([
getESQLQueryColumns({
esqlQuery: query,
search: search.search,
search: dataService.search.search,
signal,
}),
]);
const configBuilder = new LensConfigBuilder(dataViews);
const configBuilder = new LensConfigBuilder(dataService.dataViews);
let config: LensConfig;
@ -382,5 +374,5 @@ export function useObservabilityAIAssistantContext({
]
: [],
});
}, [observabilityAIAssistant, dashboardApi, search, dataViews]);
}, [dashboardApi]);
}

View file

@ -13,7 +13,6 @@ import { mount, ReactWrapper, ComponentType } from 'enzyme';
import { I18nProvider } from '@kbn/i18n-react';
import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
import { pluginServices } from '../../services/plugin_services';
import { DashboardListingPage, DashboardListingPageProps } from './dashboard_listing_page';
// Mock child components. The Dashboard listing page mostly passes down props to shared UX components which are tested in their own packages.
@ -26,7 +25,12 @@ jest.mock('../../dashboard_listing/dashboard_listing', () => {
});
import { DashboardAppNoDataPage } from '../no_data/dashboard_app_no_data';
import { dataService } from '../../services/kibana_services';
import { getDashboardContentManagementService } from '../../services/dashboard_content_management_service';
const dashboardContentManagementService = getDashboardContentManagementService();
const mockIsDashboardAppInNoDataState = jest.fn().mockResolvedValue(false);
jest.mock('../no_data/dashboard_app_no_data', () => {
const originalModule = jest.requireActual('../no_data/dashboard_app_no_data');
return {
@ -59,9 +63,7 @@ function mountWith({ props: incomingProps }: { props?: DashboardListingPageProps
test('renders analytics no data page when the user has no data view', async () => {
mockIsDashboardAppInNoDataState.mockResolvedValueOnce(true);
pluginServices.getServices().data.dataViews.hasData.hasUserDataView = jest
.fn()
.mockResolvedValue(false);
dataService.dataViews.hasData.hasUserDataView = jest.fn().mockResolvedValue(false);
let component: ReactWrapper;
await act(async () => {
@ -93,9 +95,9 @@ test('When given a title that matches multiple dashboards, filter on the title',
const props = makeDefaultProps();
props.title = title;
(
pluginServices.getServices().dashboardContentManagement.findDashboards.findByTitle as jest.Mock
).mockResolvedValue(undefined);
(dashboardContentManagementService.findDashboards.findByTitle as jest.Mock).mockResolvedValue(
undefined
);
let component: ReactWrapper;
@ -115,9 +117,9 @@ test('When given a title that matches one dashboard, redirect to dashboard', asy
const title = 'search by title';
const props = makeDefaultProps();
props.title = title;
(
pluginServices.getServices().dashboardContentManagement.findDashboards.findByTitle as jest.Mock
).mockResolvedValue({ id: 'you_found_me' });
(dashboardContentManagementService.findDashboards.findByTitle as jest.Mock).mockResolvedValue({
id: 'you_found_me',
});
let component: ReactWrapper;

View file

@ -9,19 +9,20 @@
import React, { useEffect, useState } from 'react';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { syncGlobalQueryStateWithUrl } from '@kbn/data-plugin/public';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import type { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
import { DashboardRedirect } from '../../dashboard_container/types';
import { DashboardListing } from '../../dashboard_listing/dashboard_listing';
import { coreServices, dataService, serverlessService } from '../../services/kibana_services';
import { getDashboardBreadcrumb } from '../_dashboard_app_strings';
import {
DashboardAppNoDataPage,
isDashboardAppInNoDataState,
} from '../no_data/dashboard_app_no_data';
import { pluginServices } from '../../services/plugin_services';
import { getDashboardBreadcrumb } from '../_dashboard_app_strings';
import { DashboardRedirect } from '../../dashboard_container/types';
import { getDashboardListItemLink } from './get_dashboard_list_item_link';
import { DashboardListing } from '../../dashboard_listing/dashboard_listing';
import { getDashboardContentManagementService } from '../../services/dashboard_content_management_service';
export interface DashboardListingPageProps {
kbnUrlStateStorage: IKbnUrlStateStorage;
@ -36,13 +37,6 @@ export const DashboardListingPage = ({
initialFilter,
kbnUrlStateStorage,
}: DashboardListingPageProps) => {
const {
data: { query },
serverless,
chrome: { setBreadcrumbs },
dashboardContentManagement: { findDashboards },
} = pluginServices.getServices();
const [showNoDataPage, setShowNoDataPage] = useState<boolean | undefined>();
useEffect(() => {
let isMounted = true;
@ -56,40 +50,42 @@ export const DashboardListingPage = ({
}, []);
useEffect(() => {
setBreadcrumbs([
coreServices.chrome.setBreadcrumbs([
{
text: getDashboardBreadcrumb(),
},
]);
if (serverless?.setBreadcrumbs) {
if (serverlessService) {
// if serverless breadcrumbs available,
// reset any deeper context breadcrumbs to only keep the main "dashboard" part that comes from the navigation config
serverless.setBreadcrumbs([]);
serverlessService.setBreadcrumbs([]);
}
}, [setBreadcrumbs, serverless]);
}, []);
useEffect(() => {
// syncs `_g` portion of url with query services
const { stop: stopSyncingQueryServiceStateWithUrl } = syncGlobalQueryStateWithUrl(
query,
dataService.query,
kbnUrlStateStorage
);
if (title) {
findDashboards.findByTitle(title).then((result) => {
if (!result) return;
redirectTo({
destination: 'dashboard',
id: result.id,
useReplace: true,
getDashboardContentManagementService()
.findDashboards.findByTitle(title)
.then((result) => {
if (!result) return;
redirectTo({
destination: 'dashboard',
id: result.id,
useReplace: true,
});
});
});
}
return () => {
stopSyncingQueryServiceStateWithUrl();
};
}, [title, redirectTo, query, kbnUrlStateStorage, findDashboards]);
}, [title, redirectTo, kbnUrlStateStorage]);
const titleFilter = title ? `${title}` : '';

View file

@ -10,29 +10,23 @@
import React, { useEffect } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { EuiCallOut } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { LANDING_PAGE_PATH } from '../../dashboard_constants';
import { pluginServices } from '../../services/plugin_services';
import { coreServices, urlForwardingService } from '../../services/kibana_services';
import { useDashboardMountContext } from '../hooks/dashboard_mount_context';
let bannerId: string | undefined;
export const DashboardNoMatch = ({ history }: { history: RouteComponentProps['history'] }) => {
const { restorePreviousUrl } = useDashboardMountContext();
const {
analytics,
settings: { i18n: i18nStart, theme },
overlays: { banners },
urlForwarding: { navigateToLegacyKibanaUrl },
} = pluginServices.getServices();
useEffect(() => {
restorePreviousUrl();
const { navigated } = navigateToLegacyKibanaUrl(
const { navigated } = urlForwardingService.navigateToLegacyKibanaUrl(
history.location.pathname + history.location.search
);
@ -41,7 +35,7 @@ export const DashboardNoMatch = ({ history }: { history: RouteComponentProps['hi
defaultMessage: 'Page not found',
});
bannerId = banners.replace(
bannerId = coreServices.overlays.banners.replace(
bannerId,
toMountPoint(
<EuiCallOut color="warning" iconType="iInCircle" title={bannerMessage}>
@ -55,28 +49,20 @@ export const DashboardNoMatch = ({ history }: { history: RouteComponentProps['hi
/>
</p>
</EuiCallOut>,
{ analytics, i18n: i18nStart, theme }
{ analytics: coreServices.analytics, i18n: coreServices.i18n, theme: coreServices.theme }
)
);
// hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around
setTimeout(() => {
if (bannerId) {
banners.remove(bannerId);
coreServices.overlays.banners.remove(bannerId);
}
}, 15000);
history.replace(LANDING_PAGE_PATH);
}
}, [
restorePreviousUrl,
navigateToLegacyKibanaUrl,
banners,
analytics,
i18nStart,
theme,
history,
]);
}, [restorePreviousUrl, history]);
return null;
};

View file

@ -8,27 +8,23 @@
*/
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 { IKbnUrlStateStorage, setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public';
import {
DASHBOARD_APP_ID,
createDashboardEditUrl,
GLOBAL_STATE_STORAGE_KEY,
createDashboardEditUrl,
} from '../../dashboard_constants';
import { pluginServices } from '../../services/plugin_services';
import { coreServices } from '../../services/kibana_services';
export const getDashboardListItemLink = (
kbnUrlStateStorage: IKbnUrlStateStorage,
id: string,
timeRestore: boolean
) => {
const {
application: { getUrlForApp },
settings: { uiSettings },
} = pluginServices.getServices();
const useHash = uiSettings.get('state:storeInSessionStorage'); // use hash
const useHash = coreServices.uiSettings.get('state:storeInSessionStorage'); // use hash
let url = getUrlForApp(DASHBOARD_APP_ID, {
let url = coreServices.application.getUrlForApp(DASHBOARD_APP_ID, {
path: `#${createDashboardEditUrl(id)}`,
});
const globalStateInUrl = kbnUrlStateStorage.get<QueryState>(GLOBAL_STATE_STORAGE_KEY) || {};

View file

@ -10,44 +10,30 @@
import React from 'react';
import { withSuspense } from '@kbn/shared-ux-utility';
import { pluginServices } from '../../services/plugin_services';
import { DASHBOARD_APP_ID } from '../../dashboard_constants';
import {
coreServices,
dataService,
dataViewEditorService,
embeddableService,
noDataPageService,
shareService,
} from '../../services/kibana_services';
import { getDashboardBackupService } from '../../services/dashboard_backup_service';
import { getDashboardContentManagementService } from '../../services/dashboard_content_management_service';
export const DashboardAppNoDataPage = ({
onDataViewCreated,
}: {
onDataViewCreated: () => void;
}) => {
const {
application,
data: { dataViews },
dataViewEditor,
http: { basePath, get },
documentationLinks: { indexPatternsDocLink, kibanaGuideDocLink, esqlDocLink },
customBranding,
noDataPage,
share,
} = pluginServices.getServices();
const analyticsServices = {
coreStart: {
docLinks: {
links: {
kibana: { guide: kibanaGuideDocLink },
indexPatterns: { introduction: indexPatternsDocLink },
query: { queryESQL: esqlDocLink },
},
},
application,
http: { basePath, get },
customBranding: {
hasCustomBranding$: customBranding.hasCustomBranding$,
},
},
dataViews,
dataViewEditor,
noDataPage,
share: share.url ? { url: share.url } : undefined,
coreStart: coreServices,
dataViews: dataService.dataViews,
dataViewEditor: dataViewEditorService,
noDataPage: noDataPageService,
share: shareService,
};
const importPromise = import('@kbn/shared-ux-page-analytics-no-data');
@ -74,29 +60,22 @@ export const DashboardAppNoDataPage = ({
};
export const isDashboardAppInNoDataState = async () => {
const {
data: { dataViews },
embeddable,
dashboardContentManagement,
dashboardBackup,
} = pluginServices.getServices();
const hasUserDataView = await dataViews.hasData.hasUserDataView().catch(() => false);
const hasUserDataView = await dataService.dataViews.hasData.hasUserDataView().catch(() => false);
if (hasUserDataView) return false;
// consider has data if there is an incoming embeddable
const hasIncomingEmbeddable = embeddable
const hasIncomingEmbeddable = embeddableService
.getStateTransfer()
.getIncomingEmbeddablePackage(DASHBOARD_APP_ID, false);
if (hasIncomingEmbeddable) return false;
// consider has data if there is unsaved dashboard with edits
if (dashboardBackup.dashboardHasUnsavedEdits()) return false;
if (getDashboardBackupService().dashboardHasUnsavedEdits()) return false;
// consider has data if there is at least one dashboard
const { total } = await dashboardContentManagement.findDashboards
.search({ search: '', size: 1 })
const { total } = await getDashboardContentManagementService()
.findDashboards.search({ search: '', size: 1 })
.catch(() => ({ total: 0 }));
if (total > 0) return false;

View file

@ -10,14 +10,11 @@
import { useEffect } from 'react';
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import { pluginServices } from '../../services/plugin_services';
import { DashboardApi } from '../..';
import { getNewDashboardTitle } from '../_dashboard_app_strings';
import { coreServices } from '../../services/kibana_services';
export const DashboardTabTitleSetter = ({ dashboardApi }: { dashboardApi: DashboardApi }) => {
const {
chrome: { docTitle: chromeDocTitle },
} = pluginServices.getServices();
const [title, lastSavedId] = useBatchedPublishingSubjects(
dashboardApi.panelTitle,
dashboardApi.savedObjectId
@ -27,8 +24,10 @@ export const DashboardTabTitleSetter = ({ dashboardApi }: { dashboardApi: Dashbo
* Set chrome tab title when dashboard's title changes
*/
useEffect(() => {
chromeDocTitle.change(!lastSavedId ? getNewDashboardTitle() : title ?? lastSavedId);
}, [title, chromeDocTitle, lastSavedId]);
coreServices.chrome.docTitle.change(
!lastSavedId ? getNewDashboardTitle() : title ?? lastSavedId
);
}, [title, lastSavedId]);
return null;
};

View file

@ -7,14 +7,20 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { renderHook } from '@testing-library/react-hooks';
import { ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public';
import type { PresentationContainer } from '@kbn/presentation-containers';
import type { Action } from '@kbn/ui-actions-plugin/public';
import { type BaseVisType, VisGroups, VisTypeAlias } from '@kbn/visualizations-plugin/public';
import { COMMON_EMBEDDABLE_GROUPING } from '@kbn/embeddable-plugin/public';
import type { PresentationContainer } from '@kbn/presentation-containers';
import type { Action, UiActionsService } from '@kbn/ui-actions-plugin/public';
import { ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public';
import {
VisGroups,
VisTypeAlias,
VisualizationsStart,
type BaseVisType,
} from '@kbn/visualizations-plugin/public';
import { renderHook } from '@testing-library/react-hooks';
import { uiActionsService, visualizationsService } from '../../../services/kibana_services';
import { useGetDashboardPanels } from './use_get_dashboard_panels';
import { pluginServices } from '../../../services/plugin_services';
const mockApi = { addNewPanel: jest.fn() } as unknown as jest.Mocked<PresentationContainer>;
@ -24,34 +30,25 @@ describe('Get dashboard panels hook', () => {
createNewVisType: jest.fn(),
};
type PluginServices = ReturnType<typeof pluginServices.getServices>;
let compatibleTriggerActionsRequestSpy: jest.SpyInstance<
ReturnType<NonNullable<PluginServices['uiActions']['getTriggerCompatibleActions']>>
ReturnType<NonNullable<UiActionsService['getTriggerCompatibleActions']>>
>;
let dashboardVisualizationGroupGetterSpy: jest.SpyInstance<
ReturnType<PluginServices['visualizations']['getByGroup']>
ReturnType<VisualizationsStart['getByGroup']>
>;
let dashboardVisualizationAliasesGetterSpy: jest.SpyInstance<
ReturnType<PluginServices['visualizations']['getAliases']>
ReturnType<VisualizationsStart['getAliases']>
>;
beforeAll(() => {
const _pluginServices = pluginServices.getServices();
compatibleTriggerActionsRequestSpy = jest.spyOn(
_pluginServices.uiActions,
uiActionsService,
'getTriggerCompatibleActions'
);
dashboardVisualizationGroupGetterSpy = jest.spyOn(_pluginServices.visualizations, 'getByGroup');
dashboardVisualizationAliasesGetterSpy = jest.spyOn(
_pluginServices.visualizations,
'getAliases'
);
dashboardVisualizationGroupGetterSpy = jest.spyOn(visualizationsService, 'getByGroup');
dashboardVisualizationAliasesGetterSpy = jest.spyOn(visualizationsService, 'getAliases');
});
beforeEach(() => {

View file

@ -7,19 +7,20 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { useMemo, useRef, useCallback } from 'react';
import type { IconType } from '@elastic/eui';
import { ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public';
import { type Subscription, AsyncSubject, from, defer, map, lastValueFrom } from 'rxjs';
import { EmbeddableFactory, COMMON_EMBEDDABLE_GROUPING } from '@kbn/embeddable-plugin/public';
import { PresentationContainer } from '@kbn/presentation-containers';
import { type BaseVisType, VisGroups, type VisTypeAlias } from '@kbn/visualizations-plugin/public';
import { useCallback, useMemo, useRef } from 'react';
import { AsyncSubject, defer, from, lastValueFrom, map, type Subscription } from 'rxjs';
import { pluginServices } from '../../../services/plugin_services';
import type { IconType } from '@elastic/eui';
import { COMMON_EMBEDDABLE_GROUPING, EmbeddableFactory } from '@kbn/embeddable-plugin/public';
import { PresentationContainer } from '@kbn/presentation-containers';
import { ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public';
import { VisGroups, type BaseVisType, type VisTypeAlias } from '@kbn/visualizations-plugin/public';
import { uiActionsService, visualizationsService } from '../../../services/kibana_services';
import {
getAddPanelActionMenuItemsGroup,
type PanelSelectionMenuItem,
type GroupedAddPanelActions,
type PanelSelectionMenuItem,
} from './add_panel_action_menu_items';
interface UseGetDashboardPanelsArgs {
@ -46,13 +47,9 @@ export const useGetDashboardPanels = ({ api, createNewVisType }: UseGetDashboard
const panelsComputeResultCache = useRef(new AsyncSubject<GroupedAddPanelActions[]>());
const panelsComputeSubscription = useRef<Subscription | null>(null);
const {
uiActions,
visualizations: { getAliases: getVisTypeAliases, getByGroup: getVisTypesByGroup },
} = pluginServices.getServices();
const getSortedVisTypesByGroup = (group: VisGroups) =>
getVisTypesByGroup(group)
visualizationsService
.getByGroup(group)
.sort((a: BaseVisType | VisTypeAlias, b: BaseVisType | VisTypeAlias) => {
const labelA = 'titleInWizard' in a ? a.titleInWizard || a.title : a.title;
const labelB = 'titleInWizard' in b ? b.titleInWizard || a.title : a.title;
@ -70,7 +67,8 @@ export const useGetDashboardPanels = ({ api, createNewVisType }: UseGetDashboard
const toolVisTypes = getSortedVisTypesByGroup(VisGroups.TOOLS);
const legacyVisTypes = getSortedVisTypesByGroup(VisGroups.LEGACY);
const visTypeAliases = getVisTypeAliases()
const visTypeAliases = visualizationsService
.getAliases()
.sort(({ promotion: a = false }: VisTypeAlias, { promotion: b = false }: VisTypeAlias) =>
a === b ? 0 : a ? -1 : 1
)
@ -133,12 +131,12 @@ export const useGetDashboardPanels = ({ api, createNewVisType }: UseGetDashboard
() =>
defer(() => {
return from(
uiActions?.getTriggerCompatibleActions?.(ADD_PANEL_TRIGGER, {
uiActionsService.getTriggerCompatibleActions?.(ADD_PANEL_TRIGGER, {
embeddable: api,
}) ?? []
);
}),
[api, uiActions]
[api]
);
const computeAvailablePanels = useCallback(

View file

@ -7,40 +7,35 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { css } from '@emotion/react';
import React, { useCallback } from 'react';
import { METRIC_TYPE } from '@kbn/analytics';
import { useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { METRIC_TYPE } from '@kbn/analytics';
import React, { useCallback, useMemo } from 'react';
import { AddFromLibraryButton, Toolbar, ToolbarButton } from '@kbn/shared-ux-button-toolbar';
import { BaseVisType, VisTypeAlias } from '@kbn/visualizations-plugin/public';
import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
import { getCreateVisualizationButtonTitle } from '../_dashboard_app_strings';
import { EditorMenu } from './editor_menu';
import { pluginServices } from '../../services/plugin_services';
import { ControlsToolbarButton } from './controls_toolbar_button';
import { DASHBOARD_UI_METRIC_ID } from '../../dashboard_constants';
import { useDashboardApi } from '../../dashboard_api/use_dashboard_api';
import { DASHBOARD_UI_METRIC_ID } from '../../dashboard_constants';
import {
dataService,
embeddableService,
usageCollectionService,
visualizationsService,
} from '../../services/kibana_services';
import { getCreateVisualizationButtonTitle } from '../_dashboard_app_strings';
import { ControlsToolbarButton } from './controls_toolbar_button';
import { EditorMenu } from './editor_menu';
export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean }) {
const {
usageCollection,
data: { search },
embeddable: { getStateTransfer },
visualizations: { getAliases: getVisTypeAliases },
} = pluginServices.getServices();
const { euiTheme } = useEuiTheme();
const dashboardApi = useDashboardApi();
const stateTransferService = getStateTransfer();
const lensAlias = getVisTypeAliases().find(({ name }) => name === 'lens');
const trackUiMetric = usageCollection.reportUiCounter?.bind(
usageCollection,
DASHBOARD_UI_METRIC_ID
const lensAlias = useMemo(
() => visualizationsService.getAliases().find(({ name }) => name === 'lens'),
[]
);
const createNewVisType = useCallback(
@ -49,6 +44,10 @@ export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean }
let appId = '';
if (visType) {
const trackUiMetric = usageCollectionService?.reportUiCounter.bind(
usageCollectionService,
DASHBOARD_UI_METRIC_ID
);
if (trackUiMetric) {
trackUiMetric(METRIC_TYPE.CLICK, `${visType.name}:create`);
}
@ -67,16 +66,17 @@ export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean }
path = '#/create?';
}
const stateTransferService = embeddableService.getStateTransfer();
stateTransferService.navigateToEditor(appId, {
path,
state: {
originatingApp: dashboardApi.getAppContext()?.currentAppId,
originatingPath: dashboardApi.getAppContext()?.getCurrentPath?.(),
searchSessionId: search.session.getSessionId(),
searchSessionId: dataService.search.session.getSessionId(),
},
});
},
[stateTransferService, dashboardApi, search.session, trackUiMetric]
[dashboardApi]
);
/**

View file

@ -7,35 +7,23 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { render } from '@testing-library/react';
import { EditorMenu } from './editor_menu';
import React from 'react';
import { buildMockDashboard } from '../../mocks';
import { EditorMenu } from './editor_menu';
import { pluginServices } from '../../services/plugin_services';
import { DashboardContext } from '../../dashboard_api/use_dashboard_api';
import { DashboardApi } from '../../dashboard_api/types';
import { DashboardContext } from '../../dashboard_api/use_dashboard_api';
import {
embeddableService,
uiActionsService,
visualizationsService,
} from '../../services/kibana_services';
jest.mock('../../services/plugin_services', () => {
const module = jest.requireActual('../../services/plugin_services');
const _pluginServices = (module.pluginServices as typeof pluginServices).getServices();
jest
.spyOn(_pluginServices.embeddable, 'getEmbeddableFactories')
.mockReturnValue(new Map().values());
jest.spyOn(_pluginServices.uiActions, 'getTriggerCompatibleActions').mockResolvedValue([]);
jest.spyOn(_pluginServices.visualizations, 'getByGroup').mockReturnValue([]);
jest.spyOn(_pluginServices.visualizations, 'getAliases').mockReturnValue([]);
return {
...module,
pluginServices: {
...module.pluginServices,
getServices: jest.fn().mockReturnValue(_pluginServices),
},
};
});
jest.spyOn(embeddableService, 'getEmbeddableFactories').mockReturnValue(new Map().values());
jest.spyOn(uiActionsService, 'getTriggerCompatibleActions').mockResolvedValue([]);
jest.spyOn(visualizationsService, 'getByGroup').mockReturnValue([]);
jest.spyOn(visualizationsService, 'getAliases').mockReturnValue([]);
describe('editor menu', () => {
it('renders without crashing', async () => {

View file

@ -16,8 +16,8 @@ import { toMountPoint } from '@kbn/react-kibana-mount';
import { ToolbarButton } from '@kbn/shared-ux-button-toolbar';
import { useGetDashboardPanels, DashboardPanelSelectionListFlyout } from './add_new_panel';
import { pluginServices } from '../../services/plugin_services';
import { useDashboardApi } from '../../dashboard_api/use_dashboard_api';
import { coreServices } from '../../services/kibana_services';
interface EditorMenuProps
extends Pick<Parameters<typeof useGetDashboardPanels>[0], 'createNewVisType'> {
@ -27,12 +27,6 @@ interface EditorMenuProps
export const EditorMenu = ({ createNewVisType, isDisabled }: EditorMenuProps) => {
const dashboardApi = useDashboardApi();
const {
overlays,
analytics,
settings: { i18n: i18nStart, theme },
} = pluginServices.getServices();
const fetchDashboardPanels = useGetDashboardPanels({
api: dashboardApi,
createNewVisType,
@ -63,11 +57,11 @@ export const EditorMenu = ({ createNewVisType, isDisabled }: EditorMenuProps) =>
/>
);
}),
{ analytics, theme, i18n: i18nStart }
{ analytics: coreServices.analytics, theme: coreServices.theme, i18n: coreServices.i18n }
);
dashboardApi.openOverlay(
overlays.openFlyout(mount, {
coreServices.overlays.openFlyout(mount, {
size: 'm',
maxWidth: 500,
paddingSize: flyoutPanelPaddingSize,
@ -80,7 +74,7 @@ export const EditorMenu = ({ createNewVisType, isDisabled }: EditorMenuProps) =>
})
);
},
[analytics, theme, i18nStart, dashboardApi, overlays, fetchDashboardPanels]
[dashboardApi, fetchDashboardPanels]
);
return (

View file

@ -8,11 +8,12 @@
*/
import { Capabilities } from '@kbn/core/public';
import { DashboardLocatorParams } from '../../../dashboard_container';
import { convertPanelMapToSavedPanels, DashboardContainerInput } from '../../../../common';
import { DashboardLocatorParams } from '../../../dashboard_container';
import { pluginServices } from '../../../services/plugin_services';
import { shareService } from '../../../services/kibana_services';
import { showPublicUrlSwitch, ShowShareModal, ShowShareModalProps } from './show_share_modal';
import { getDashboardBackupService } from '../../../services/dashboard_backup_service';
describe('showPublicUrlSwitch', () => {
test('returns false if "dashboard" app is not available', () => {
@ -56,13 +57,11 @@ describe('showPublicUrlSwitch', () => {
});
describe('ShowShareModal', () => {
const dashboardBackupService = getDashboardBackupService();
const unsavedStateKeys = ['query', 'filters', 'options', 'savedQuery', 'panels'] as Array<
keyof DashboardLocatorParams
>;
const toggleShareMenuSpy = jest.spyOn(
pluginServices.getServices().share,
'toggleShareContextMenu'
);
const toggleShareMenuSpy = jest.spyOn(shareService!, 'toggleShareContextMenu');
afterEach(() => {
jest.clearAllMocks();
@ -71,9 +70,7 @@ describe('ShowShareModal', () => {
const getPropsAndShare = (
unsavedState?: Partial<DashboardContainerInput>
): ShowShareModalProps => {
pluginServices.getServices().dashboardBackup.getState = jest
.fn()
.mockReturnValue({ dashboardState: unsavedState });
dashboardBackupService.getState = jest.fn().mockReturnValue({ dashboardState: unsavedState });
return {
isDirty: true,
anchorElement: document.createElement('div'),
@ -169,7 +166,7 @@ describe('ShowShareModal', () => {
},
};
const props = getPropsAndShare(unsavedDashboardState);
pluginServices.getServices().dashboardBackup.getState = jest.fn().mockReturnValue({
dashboardBackupService.getState = jest.fn().mockReturnValue({
dashboardState: unsavedDashboardState,
panels: {
panel_1: { changedKey1: 'changed' },

View file

@ -7,6 +7,10 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { omit } from 'lodash';
import moment from 'moment';
import React, { ReactElement, useState } from 'react';
import { EuiCheckboxGroup } from '@elastic/eui';
import type { Capabilities } from '@kbn/core/public';
import { QueryState } from '@kbn/data-plugin/common';
@ -14,15 +18,17 @@ import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { i18n } from '@kbn/i18n';
import { getStateFromKbnUrl, setStateToKbnUrl, unhashUrl } from '@kbn/kibana-utils-plugin/public';
import { omit } from 'lodash';
import moment from 'moment';
import React, { ReactElement, useState } from 'react';
import { convertPanelMapToSavedPanels, DashboardPanelMap } from '../../../../common';
import { DashboardLocatorParams } from '../../../dashboard_container';
import { pluginServices } from '../../../services/plugin_services';
import { dashboardUrlParams } from '../../dashboard_router';
import {
getDashboardBackupService,
PANELS_CONTROL_GROUP_KEY,
} from '../../../services/dashboard_backup_service';
import { coreServices, dataService, shareService } from '../../../services/kibana_services';
import { getDashboardCapabilities } from '../../../utils/get_dashboard_capabilities';
import { shareModalStrings } from '../../_dashboard_app_strings';
import { PANELS_CONTROL_GROUP_KEY } from '../../../services/dashboard_backup/dashboard_backup_service';
import { dashboardUrlParams } from '../../dashboard_router';
const showFilterBarId = 'showFilterBar';
@ -49,21 +55,7 @@ export function ShowShareModal({
dashboardTitle,
getPanelsState,
}: ShowShareModalProps) {
const {
dashboardCapabilities: { createShortUrl: allowShortUrl },
dashboardBackup,
data: {
query: {
timefilter: {
timefilter: { getTime },
},
},
},
notifications,
share: { toggleShareContextMenu },
} = pluginServices.getServices();
if (!toggleShareContextMenu) return; // TODO: Make this logic cleaner once share is an optional service
if (!shareService) return;
const EmbedUrlParamExtension = ({
setParamValue,
@ -125,7 +117,7 @@ export function ShowShareModal({
let unsavedStateForLocator: DashboardLocatorParams = {};
const { dashboardState: unsavedDashboardState, panels: panelModifications } =
dashboardBackup.getState(savedObjectId) ?? {};
getDashboardBackupService().getState(savedObjectId) ?? {};
const allUnsavedPanels = (() => {
if (
@ -186,7 +178,7 @@ export function ShowShareModal({
refreshInterval: undefined, // We don't share refresh interval externally
viewMode: ViewMode.VIEW, // For share locators we always load the dashboard in view mode
useHash: false,
timeRange: getTime(),
timeRange: dataService.query.timefilter.timefilter.getTime(),
...unsavedStateForLocator,
};
@ -203,11 +195,11 @@ export function ShowShareModal({
unhashUrl(baseUrl)
);
toggleShareContextMenu({
shareService.toggleShareContextMenu({
isDirty,
anchorElement,
allowEmbed: true,
allowShortUrl,
allowShortUrl: getDashboardCapabilities().createShortUrl,
shareableUrl,
objectId: savedObjectId,
objectType: 'dashboard',
@ -238,6 +230,6 @@ export function ShowShareModal({
snapshotShareWarning: Boolean(unsavedDashboardState?.panels)
? shareModalStrings.getSnapshotShareWarning()
: undefined,
toasts: notifications.toasts,
toasts: coreServices.notifications.toasts,
});
}

View file

@ -7,23 +7,25 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { batch } from 'react-redux';
import { Dispatch, SetStateAction, useCallback, useMemo, useState } from 'react';
import { batch } from 'react-redux';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { TopNavMenuData } from '@kbn/navigation-plugin/public';
import type { TopNavMenuData } from '@kbn/navigation-plugin/public';
import useMountedState from 'react-use/lib/useMountedState';
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import { UI_SETTINGS } from '../../../common';
import { useDashboardApi } from '../../dashboard_api/use_dashboard_api';
import { CHANGE_CHECK_DEBOUNCE } from '../../dashboard_constants';
import { openSettingsFlyout } from '../../dashboard_container/embeddable/api';
import { confirmDiscardUnsavedChanges } from '../../dashboard_listing/confirm_overlays';
import { getDashboardBackupService } from '../../services/dashboard_backup_service';
import { SaveDashboardReturn } from '../../services/dashboard_content_management_service/types';
import { coreServices, shareService } from '../../services/kibana_services';
import { getDashboardCapabilities } from '../../utils/get_dashboard_capabilities';
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 { confirmDiscardUnsavedChanges } from '../../dashboard_listing/confirm_overlays';
import { SaveDashboardReturn } from '../../services/dashboard_content_management/types';
import { useDashboardApi } from '../../dashboard_api/use_dashboard_api';
import { openSettingsFlyout } from '../../dashboard_container/embeddable/api';
export const useDashboardMenuItems = ({
isLabsShown,
@ -40,17 +42,6 @@ export const useDashboardMenuItems = ({
const [isSaveInProgress, setIsSaveInProgress] = useState(false);
/**
* Unpack dashboard services
*/
const {
share,
dashboardBackup,
settings: { uiSettings },
dashboardCapabilities: { showWriteControls },
} = pluginServices.getServices();
const isLabsEnabled = uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI);
/**
* Unpack dashboard state from redux
*/
@ -120,7 +111,7 @@ export const useDashboardMenuItems = ({
const switchModes = switchToViewMode
? () => {
dashboardApi.setViewMode(ViewMode.VIEW);
dashboardBackup.storeViewMode(ViewMode.VIEW);
getDashboardBackupService().storeViewMode(ViewMode.VIEW);
}
: undefined;
if (!hasUnsavedChanges) {
@ -138,7 +129,7 @@ export const useDashboardMenuItems = ({
});
}, viewMode as ViewMode);
},
[dashboardApi, dashboardBackup, hasUnsavedChanges, viewMode, isMounted]
[dashboardApi, hasUnsavedChanges, viewMode, isMounted]
);
/**
@ -170,7 +161,7 @@ export const useDashboardMenuItems = ({
testId: 'dashboardEditMode',
className: 'eui-hideFor--s eui-hideFor--xs', // hide for small screens - editing doesn't work in mobile mode.
run: () => {
dashboardBackup.storeViewMode(ViewMode.EDIT);
getDashboardBackupService().storeViewMode(ViewMode.EDIT);
dashboardApi.setViewMode(ViewMode.EDIT);
dashboardApi.clearOverlays();
},
@ -243,7 +234,6 @@ export const useDashboardMenuItems = ({
dashboardApi,
setIsLabsShown,
isLabsShown,
dashboardBackup,
quickSaveDashboard,
resetChanges,
isResetting,
@ -275,9 +265,13 @@ export const useDashboardMenuItems = ({
/**
* Build ordered menus for view and edit mode.
*/
const isLabsEnabled = useMemo(() => coreServices.uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI), []);
const viewModeTopNavConfig = useMemo(() => {
const { showWriteControls } = getDashboardCapabilities();
const labsMenuItem = isLabsEnabled ? [menuItems.labs] : [];
const shareMenuItem = share ? [menuItems.share] : [];
const shareMenuItem = shareService ? [menuItems.share] : [];
const duplicateMenuItem = showWriteControls ? [menuItems.interactiveSave] : [];
const editMenuItem = showWriteControls && !managed ? [menuItems.edit] : [];
const mayberesetChangesMenuItem = showResetChange ? [resetChangesMenuItem] : [];
@ -290,19 +284,11 @@ export const useDashboardMenuItems = ({
...mayberesetChangesMenuItem,
...editMenuItem,
];
}, [
isLabsEnabled,
menuItems,
share,
showWriteControls,
managed,
showResetChange,
resetChangesMenuItem,
]);
}, [isLabsEnabled, menuItems, managed, showResetChange, resetChangesMenuItem]);
const editModeTopNavConfig = useMemo(() => {
const labsMenuItem = isLabsEnabled ? [menuItems.labs] : [];
const shareMenuItem = share ? [menuItems.share] : [];
const shareMenuItem = shareService ? [menuItems.share] : [];
const editModeItems: TopNavMenuData[] = [];
if (lastSavedId) {
@ -317,7 +303,7 @@ export const useDashboardMenuItems = ({
editModeItems.push(menuItems.switchToViewMode, menuItems.interactiveSave);
}
return [...labsMenuItem, menuItems.settings, ...shareMenuItem, ...editModeItems];
}, [isLabsEnabled, menuItems, share, lastSavedId, showResetChange, resetChangesMenuItem]);
}, [isLabsEnabled, menuItems, lastSavedId, showResetChange, resetChangesMenuItem]);
return { viewModeTopNavConfig, editModeTopNavConfig };
};

View file

@ -23,7 +23,7 @@ import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics';
import { SEARCH_SESSION_ID } from '../../dashboard_constants';
import { DashboardContainer, DashboardLocatorParams } from '../../dashboard_container';
import { convertPanelMapToSavedPanels } from '../../../common';
import { pluginServices } from '../../services/plugin_services';
import { dataService } from '../../services/kibana_services';
export const removeSearchSessionIdFromURL = (kbnUrlStateStorage: IKbnUrlStateStorage) => {
kbnUrlStateStorage.kbnUrlControls.updateAsync((nextUrl) => {
@ -69,17 +69,6 @@ function getLocatorParams({
container: DashboardContainer;
shouldRestoreSearchSession: boolean;
}): DashboardLocatorParams {
const {
data: {
query: {
queryString,
filterManager,
timefilter: { timefilter },
},
search: { session },
},
} = pluginServices.getServices();
const {
componentState: { lastSavedId },
explicitInput: { panels, query, viewMode },
@ -89,11 +78,15 @@ function getLocatorParams({
viewMode,
useHash: false,
preserveSavedFilters: false,
filters: filterManager.getFilters(),
query: queryString.formatQuery(query) as Query,
filters: dataService.query.filterManager.getFilters(),
query: dataService.query.queryString.formatQuery(query) as Query,
dashboardId: container.getDashboardSavedObjectId(),
searchSessionId: shouldRestoreSearchSession ? session.getSessionId() : undefined,
timeRange: shouldRestoreSearchSession ? timefilter.getAbsoluteTime() : timefilter.getTime(),
searchSessionId: shouldRestoreSearchSession
? dataService.search.session.getSessionId()
: undefined,
timeRange: shouldRestoreSearchSession
? dataService.query.timefilter.timefilter.getAbsoluteTime()
: dataService.query.timefilter.timefilter.getTime(),
refreshInterval: shouldRestoreSearchSession
? {
pause: true, // force pause refresh interval when restoring a session

View file

@ -7,26 +7,26 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { History } from 'history';
import _ from 'lodash';
import { skip } from 'rxjs';
import semverSatisfies from 'semver/functions/satisfies';
import { History } from 'history';
import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
import { replaceUrlHashQuery } from '@kbn/kibana-utils-plugin/common';
import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
import {
DashboardContainerInput,
DashboardPanelMap,
SharedDashboardState,
convertSavedPanelsToPanelMap,
DashboardContainerInput,
} from '../../../common';
import { pluginServices } from '../../services/plugin_services';
import { getPanelTooOldErrorString } from '../_dashboard_app_strings';
import { DASHBOARD_STATE_STORAGE_KEY, createDashboardEditUrl } from '../../dashboard_constants';
import { SavedDashboardPanel } from '../../../common/content_management';
import { migrateLegacyQuery } from '../../services/dashboard_content_management/lib/load_dashboard_state';
import { DashboardApi } from '../../dashboard_api/types';
import { DASHBOARD_STATE_STORAGE_KEY, createDashboardEditUrl } from '../../dashboard_constants';
import { migrateLegacyQuery } from '../../services/dashboard_content_management_service/lib/load_dashboard_state';
import { coreServices } from '../../services/kibana_services';
import { getPanelTooOldErrorString } from '../_dashboard_app_strings';
/**
* We no longer support loading panels from a version older than 7.3 in the URL.
@ -54,7 +54,7 @@ function getPanelsMap(appStateInUrl: SharedDashboardState): DashboardPanelMap |
}
if (isPanelVersionTooOld(appStateInUrl.panels)) {
pluginServices.getServices().notifications.toasts.addWarning(getPanelTooOldErrorString());
coreServices.notifications.toasts.addWarning(getPanelTooOldErrorString());
return undefined;
}

View file

@ -7,20 +7,18 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { findTestSubject } from '@elastic/eui/lib/test';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import React from 'react';
import { buildMockDashboard } from '../../../mocks';
import { DashboardEmptyScreen } from './dashboard_empty_screen';
import { pluginServices } from '../../../services/plugin_services';
import { DashboardContext } from '../../../dashboard_api/use_dashboard_api';
import { DashboardApi } from '../../../dashboard_api/types';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { DashboardApi } from '../../../dashboard_api/types';
import { DashboardContext } from '../../../dashboard_api/use_dashboard_api';
import { buildMockDashboard } from '../../../mocks';
import { coreServices, visualizationsService } from '../../../services/kibana_services';
import { DashboardEmptyScreen } from './dashboard_empty_screen';
pluginServices.getServices().visualizations.getAliases = jest
.fn()
.mockReturnValue([{ name: 'lens' }]);
visualizationsService.getAliases = jest.fn().mockReturnValue([{ name: 'lens' }]);
describe('DashboardEmptyScreen', () => {
function mountComponent(viewMode: ViewMode) {
@ -57,7 +55,7 @@ describe('DashboardEmptyScreen', () => {
});
test('renders correctly with readonly mode', () => {
pluginServices.getServices().dashboardCapabilities.showWriteControls = false;
(coreServices.application.capabilities as any).dashboard.showWriteControls = false;
const component = mountComponent(ViewMode.VIEW);
expect(component.render()).toMatchSnapshot();
@ -72,7 +70,7 @@ describe('DashboardEmptyScreen', () => {
// even when in edit mode, readonly users should not have access to the editing buttons in the empty prompt.
test('renders correctly with readonly and edit mode', () => {
pluginServices.getServices().dashboardCapabilities.showWriteControls = false;
(coreServices.application.capabilities as any).dashboard.showWriteControls = false;
const component = mountComponent(ViewMode.EDIT);
expect(component.render()).toMatchSnapshot();

View file

@ -21,33 +21,31 @@ import {
} from '@elastic/eui';
import { METRIC_TYPE } from '@kbn/analytics';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
import { DASHBOARD_UI_METRIC_ID } from '../../../dashboard_constants';
import { pluginServices } from '../../../services/plugin_services';
import { useDashboardApi } from '../../../dashboard_api/use_dashboard_api';
import { DASHBOARD_UI_METRIC_ID } from '../../../dashboard_constants';
import {
coreServices,
dataService,
embeddableService,
usageCollectionService,
visualizationsService,
} from '../../../services/kibana_services';
import { getDashboardCapabilities } from '../../../utils/get_dashboard_capabilities';
import { emptyScreenStrings } from '../../_dashboard_container_strings';
export function DashboardEmptyScreen() {
const {
settings: {
theme: { theme$ },
},
usageCollection,
data: { search },
http: { basePath },
embeddable: { getStateTransfer },
dashboardCapabilities: { showWriteControls },
visualizations: { getAliases: getVisTypeAliases },
} = pluginServices.getServices();
const lensAlias = useMemo(
() => getVisTypeAliases().find(({ name }) => name === 'lens'),
[getVisTypeAliases]
() => visualizationsService.getAliases().find(({ name }) => name === 'lens'),
[]
);
const { showWriteControls } = useMemo(() => {
return getDashboardCapabilities();
}, []);
const dashboardApi = useDashboardApi();
const isDarkTheme = useObservable(theme$)?.darkMode;
const isDarkTheme = useObservable(coreServices.theme.theme$)?.darkMode;
const viewMode = useStateFromPublishingSubject(dashboardApi.viewMode);
const isEditMode = useMemo(() => {
return viewMode === 'edit';
@ -55,8 +53,8 @@ export function DashboardEmptyScreen() {
const goToLens = useCallback(() => {
if (!lensAlias || !lensAlias.alias) return;
const trackUiMetric = usageCollection.reportUiCounter?.bind(
usageCollection,
const trackUiMetric = usageCollectionService?.reportUiCounter.bind(
usageCollectionService,
DASHBOARD_UI_METRIC_ID
);
@ -64,18 +62,18 @@ export function DashboardEmptyScreen() {
trackUiMetric(METRIC_TYPE.CLICK, `${lensAlias.name}:create`);
}
const appContext = dashboardApi.getAppContext();
getStateTransfer().navigateToEditor(lensAlias.alias.app, {
embeddableService.getStateTransfer().navigateToEditor(lensAlias.alias.app, {
path: lensAlias.alias.path,
state: {
originatingApp: appContext?.currentAppId,
originatingPath: appContext?.getCurrentPath?.() ?? '',
searchSessionId: search.session.getSessionId(),
searchSessionId: dataService.search.session.getSessionId(),
},
});
}, [getStateTransfer, lensAlias, dashboardApi, search.session, usageCollection]);
}, [lensAlias, dashboardApi]);
// TODO replace these SVGs with versions from EuiIllustration as soon as it becomes available.
const imageUrl = basePath.prepend(
const imageUrl = coreServices.http.basePath.prepend(
`/plugins/dashboard/assets/${isDarkTheme ? 'dashboards_dark' : 'dashboards_light'}.svg`
);

View file

@ -7,15 +7,17 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import classNames from 'classnames';
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { EuiLoadingChart } from '@elastic/eui';
import { css } from '@emotion/react';
import { EmbeddablePanel, ReactEmbeddableRenderer } from '@kbn/embeddable-plugin/public';
import classNames from 'classnames';
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import { DashboardPanelState } from '../../../../common';
import { pluginServices } from '../../../services/plugin_services';
import { useDashboardApi } from '../../../dashboard_api/use_dashboard_api';
import { embeddableService, presentationUtilService } from '../../../services/kibana_services';
type DivProps = Pick<React.HTMLAttributes<HTMLDivElement>, 'className' | 'style' | 'children'>;
@ -97,10 +99,6 @@ export const Item = React.forwardRef<HTMLDivElement, Props>(
: undefined;
const renderedEmbeddable = useMemo(() => {
const {
embeddable: { reactEmbeddableRegistryHasKey },
} = pluginServices.getServices();
const panelProps = {
showBadges: true,
showBorder: useMargins,
@ -109,7 +107,7 @@ export const Item = React.forwardRef<HTMLDivElement, Props>(
};
// render React embeddable
if (reactEmbeddableRegistryHasKey(type)) {
if (embeddableService.reactEmbeddableRegistryHasKey(type)) {
return (
<ReactEmbeddableRenderer
type={type}
@ -190,18 +188,20 @@ export const ObservedItem = React.forwardRef<HTMLDivElement, Props>((props, pane
// ReactGridLayout passes ref to children. Functional component children require forwardRef to avoid react warning
// https://github.com/react-grid-layout/react-grid-layout#custom-child-components-and-draggable-handles
export const DashboardGridItem = React.forwardRef<HTMLDivElement, Props>((props, ref) => {
const {
settings: { isProjectEnabledInLabs },
} = pluginServices.getServices();
const dashboardApi = useDashboardApi();
const [focusedPanelId, viewMode] = useBatchedPublishingSubjects(
dashboardApi.focusedPanelId$,
dashboardApi.viewMode
);
const deferBelowFoldEnabled = useMemo(
() => presentationUtilService.labsService.isProjectEnabled('labs:dashboard:deferBelowFold'),
[]
);
const isEnabled =
viewMode !== 'print' &&
isProjectEnabledInLabs('labs:dashboard:deferBelowFold') &&
deferBelowFoldEnabled &&
(!focusedPanelId || focusedPanelId === props.id);
return isEnabled ? <ObservedItem ref={ref} {...props} /> : <Item ref={ref} {...props} />;

View file

@ -9,29 +9,32 @@
import React, { useCallback, useState } from 'react';
import useMountedState from 'react-use/lib/useMountedState';
import { i18n } from '@kbn/i18n';
import {
EuiFormRow,
EuiFieldText,
EuiTextArea,
EuiForm,
EuiButton,
EuiButtonEmpty,
EuiCallOut,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiTitle,
EuiCallOut,
EuiForm,
EuiFormRow,
EuiIconTip,
EuiSwitch,
EuiText,
EuiIconTip,
EuiTextArea,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { DashboardContainerInput } from '../../../../common';
import { pluginServices } from '../../../services/plugin_services';
import { useDashboardApi } from '../../../dashboard_api/use_dashboard_api';
import { getDashboardContentManagementService } from '../../../services/dashboard_content_management_service';
import { savedObjectsTaggingService } from '../../../services/kibana_services';
interface DashboardSettingsProps {
onClose: () => void;
@ -40,11 +43,6 @@ interface DashboardSettingsProps {
const DUPLICATE_TITLE_CALLOUT_ID = 'duplicateTitleCallout';
export const DashboardSettings = ({ onClose }: DashboardSettingsProps) => {
const {
savedObjectsTagging: { components },
dashboardContentManagement: { checkForDuplicateDashboardTitle },
} = pluginServices.getServices();
const dashboardApi = useDashboardApi();
const [localSettings, setLocalSettings] = useState(dashboardApi.getSettings());
@ -63,13 +61,15 @@ export const DashboardSettings = ({ onClose }: DashboardSettingsProps) => {
const onApply = async () => {
setIsApplying(true);
const validTitle = await checkForDuplicateDashboardTitle({
title: localSettings.title,
copyOnSave: false,
lastSavedTitle: dashboardApi.panelTitle.value ?? '',
onTitleDuplicate,
isTitleDuplicateConfirmed,
});
const validTitle = await getDashboardContentManagementService().checkForDuplicateDashboardTitle(
{
title: localSettings.title,
copyOnSave: false,
lastSavedTitle: dashboardApi.panelTitle.value ?? '',
onTitleDuplicate,
isTitleDuplicateConfirmed,
}
);
if (!isMounted()) return;
@ -121,7 +121,9 @@ export const DashboardSettings = ({ onClose }: DashboardSettingsProps) => {
};
const renderTagSelector = () => {
if (!components) return;
const savedObjectsTaggingApi = savedObjectsTaggingService?.getTaggingApi();
if (!savedObjectsTaggingApi) return;
return (
<EuiFormRow
label={
@ -131,7 +133,7 @@ export const DashboardSettings = ({ onClose }: DashboardSettingsProps) => {
/>
}
>
<components.TagSelector
<savedObjectsTaggingApi.ui.components.TagSelector
selected={localSettings.tags}
onTagsSelected={(selectedTags) => updateDashboardSetting({ tags: selectedTags })}
/>

View file

@ -15,11 +15,11 @@ import {
ReferenceOrValueEmbeddable,
} from '@kbn/embeddable-plugin/public';
import {
CONTACT_CARD_EMBEDDABLE,
ContactCardEmbeddable,
ContactCardEmbeddableFactory,
ContactCardEmbeddableInput,
ContactCardEmbeddableOutput,
CONTACT_CARD_EMBEDDABLE,
} from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables';
import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks';
import {
@ -34,7 +34,7 @@ import { render } from '@testing-library/react';
import React from 'react';
import { BehaviorSubject, lastValueFrom, Subject } from 'rxjs';
import { buildMockDashboard, getSampleDashboardPanel } from '../../../mocks';
import { pluginServices } from '../../../services/plugin_services';
import { embeddableService } from '../../../services/kibana_services';
import { DashboardContainer } from '../dashboard_container';
import { duplicateDashboardPanel, incrementPanelTitle } from './duplicate_dashboard_panel';
@ -54,9 +54,7 @@ describe('Legacy embeddables', () => {
const mockEmbeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
.fn()
.mockReturnValue(mockEmbeddableFactory);
embeddableService.getEmbeddableFactory = jest.fn().mockReturnValue(mockEmbeddableFactory);
container = buildMockDashboard({
overrides: {
panels: {

View file

@ -7,6 +7,9 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { filter, map, max } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import { isReferenceOrValueEmbeddable, PanelNotFoundError } from '@kbn/embeddable-plugin/public';
import { apiHasSnapshottableState } from '@kbn/presentation-containers/interfaces/serialized_state';
import {
@ -16,11 +19,10 @@ import {
getPanelTitle,
stateHasTitles,
} from '@kbn/presentation-publishing';
import { filter, map, max } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import { DashboardPanelState, prefixReferencesFromPanel } from '../../../../common';
import { dashboardClonePanelActionStrings } from '../../../dashboard_actions/_dashboard_actions_strings';
import { pluginServices } from '../../../services/plugin_services';
import { coreServices, embeddableService } from '../../../services/kibana_services';
import { placeClonePanel } from '../../panel_placement';
import { DashboardContainer } from '../dashboard_container';
@ -105,17 +107,13 @@ const duplicateReactEmbeddableInput = async (
};
export async function duplicateDashboardPanel(this: DashboardContainer, idToDuplicate: string) {
const {
notifications: { toasts },
embeddable: { reactEmbeddableRegistryHasKey },
} = pluginServices.getServices();
const panelToClone = await this.getDashboardPanelFromId(idToDuplicate);
const duplicatedPanelState = reactEmbeddableRegistryHasKey(panelToClone.type)
const duplicatedPanelState = embeddableService.reactEmbeddableRegistryHasKey(panelToClone.type)
? await duplicateReactEmbeddableInput(this, panelToClone, idToDuplicate)
: await duplicateLegacyInput(this, panelToClone, idToDuplicate);
toasts.addSuccess({
coreServices.notifications.toasts.addSuccess({
title: dashboardClonePanelActionStrings.getSuccessMessage(),
'data-test-subj': 'addObjectToContainerSuccess',
});

View file

@ -11,20 +11,14 @@ import React from 'react';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { pluginServices } from '../../../services/plugin_services';
import { DashboardSettings } from '../../component/settings/settings_flyout';
import { DashboardContext } from '../../../dashboard_api/use_dashboard_api';
import { DashboardApi } from '../../../dashboard_api/types';
import { DashboardContext } from '../../../dashboard_api/use_dashboard_api';
import { coreServices } from '../../../services/kibana_services';
import { DashboardSettings } from '../../component/settings/settings_flyout';
export function openSettingsFlyout(dashboardApi: DashboardApi) {
const {
analytics,
settings: { i18n, theme },
overlays,
} = pluginServices.getServices();
dashboardApi.openOverlay(
overlays.openFlyout(
coreServices.overlays.openFlyout(
toMountPoint(
<DashboardContext.Provider value={dashboardApi}>
<DashboardSettings
@ -33,7 +27,7 @@ export function openSettingsFlyout(dashboardApi: DashboardApi) {
}}
/>
</DashboardContext.Provider>,
{ analytics, i18n, theme }
{ analytics: coreServices.analytics, i18n: coreServices.i18n, theme: coreServices.theme }
),
{
size: 's',

View file

@ -7,14 +7,15 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { i18n } from '@kbn/i18n';
import React, { Fragment, useCallback } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiIconTip, EuiSwitch } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiFormRow, EuiSwitch, EuiIconTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { SavedObjectSaveModal } from '@kbn/saved-objects-plugin/public';
import { savedObjectsTaggingService } from '../../../../services/kibana_services';
import type { DashboardSaveOptions } from '../../../types';
import { pluginServices } from '../../../../services/plugin_services';
/**
* TODO: Portable Dashboard followup, use redux for the state.
@ -79,12 +80,9 @@ export const DashboardSaveModal: React.FC<DashboardSaveModalProps> = ({
);
const renderDashboardSaveOptions = useCallback(() => {
const {
savedObjectsTagging: { components },
} = pluginServices.getServices();
const tagSelector = components ? (
<components.SavedObjectSaveModalTagSelector
const savedObjectsTaggingApi = savedObjectsTaggingService?.getTaggingApi();
const tagSelector = savedObjectsTaggingApi ? (
<savedObjectsTaggingApi.ui.components.SavedObjectSaveModalTagSelector
initialSelection={selectedTags}
onTagsSelected={(selectedTagIds) => {
setSelectedTags(selectedTagIds);

View file

@ -7,6 +7,10 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { cloneDeep } from 'lodash';
import React from 'react';
import { batch } from 'react-redux';
import type { Reference } from '@kbn/content-management-utils';
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
import {
@ -14,12 +18,10 @@ import {
isReferenceOrValueEmbeddable,
ViewMode,
} from '@kbn/embeddable-plugin/public';
import { i18n } from '@kbn/i18n';
import { apiHasSerializableState, SerializedPanelState } from '@kbn/presentation-containers';
import { showSaveModal } from '@kbn/saved-objects-plugin/public';
import { cloneDeep } from 'lodash';
import React from 'react';
import { batch } from 'react-redux';
import { i18n } from '@kbn/i18n';
import {
DashboardContainerInput,
DashboardPanelMap,
@ -29,8 +31,14 @@ import { DASHBOARD_CONTENT_ID, SAVED_OBJECT_POST_TIME } from '../../../dashboard
import {
SaveDashboardReturn,
SavedDashboardInput,
} from '../../../services/dashboard_content_management/types';
import { pluginServices } from '../../../services/plugin_services';
} from '../../../services/dashboard_content_management_service/types';
import { getDashboardContentManagementService } from '../../../services/dashboard_content_management_service';
import {
coreServices,
dataService,
embeddableService,
savedObjectsTaggingService,
} from '../../../services/kibana_services';
import { DashboardSaveOptions, DashboardStateFromSaveModal } from '../../types';
import { DashboardContainer } from '../dashboard_container';
import { extractTitleAndCount } from './lib/extract_title_and_count';
@ -39,9 +47,6 @@ import { DashboardSaveModal } from './overlays/save_modal';
const serializeAllPanelState = async (
dashboard: DashboardContainer
): Promise<{ panels: DashboardContainerInput['panels']; references: Reference[] }> => {
const {
embeddable: { reactEmbeddableRegistryHasKey },
} = pluginServices.getServices();
const references: Reference[] = [];
const panels = cloneDeep(dashboard.getInput().panels);
@ -49,7 +54,7 @@ const serializeAllPanelState = async (
Promise<{ uuid: string; serialized: SerializedPanelState<object> }>
> = [];
for (const [uuid, panel] of Object.entries(panels)) {
if (!reactEmbeddableRegistryHasKey(panel.type)) continue;
if (!embeddableService.reactEmbeddableRegistryHasKey(panel.type)) continue;
const api = dashboard.children$.value[uuid];
if (api && apiHasSerializableState(api)) {
@ -75,10 +80,6 @@ const serializeAllPanelState = async (
* Save the current state of this dashboard to a saved object without showing any save modal.
*/
export async function runQuickSave(this: DashboardContainer) {
const {
dashboardContentManagement: { saveDashboardState },
} = pluginServices.getServices();
const {
explicitInput: currentState,
componentState: { lastSavedId, managed },
@ -98,7 +99,7 @@ export async function runQuickSave(this: DashboardContainer) {
stateToSave = { ...stateToSave, controlGroupInput: controlGroupSerializedState };
}
const saveResult = await saveDashboardState({
const saveResult = await getDashboardContentManagementService().saveDashboardState({
controlGroupReferences,
panelReferences: references,
currentState: stateToSave,
@ -118,20 +119,11 @@ export async function runQuickSave(this: DashboardContainer) {
* accounts for scenarios of cloning elastic managed dashboard into user managed dashboards
*/
export async function runInteractiveSave(this: DashboardContainer, interactionMode: ViewMode) {
const {
data: {
query: {
timefilter: { timefilter },
},
},
savedObjectsTagging: { hasApi: hasSavedObjectsTagging },
dashboardContentManagement: { checkForDuplicateDashboardTitle, saveDashboardState },
} = pluginServices.getServices();
const {
explicitInput: currentState,
componentState: { lastSavedId, managed },
} = this.getState();
const dashboardContentManagementService = getDashboardContentManagementService();
return new Promise<SaveDashboardReturn | undefined>((resolve, reject) => {
if (interactionMode === ViewMode.EDIT && managed) {
@ -156,7 +148,7 @@ export async function runInteractiveSave(this: DashboardContainer, interactionMo
try {
if (
!(await checkForDuplicateDashboardTitle({
!(await dashboardContentManagementService.checkForDuplicateDashboardTitle({
title: newTitle,
onTitleDuplicate,
lastSavedTitle: currentState.title,
@ -172,11 +164,13 @@ export async function runInteractiveSave(this: DashboardContainer, interactionMo
tags: [] as string[],
description: newDescription,
timeRestore: newTimeRestore,
timeRange: newTimeRestore ? timefilter.getTime() : undefined,
refreshInterval: newTimeRestore ? timefilter.getRefreshInterval() : undefined,
timeRange: newTimeRestore ? dataService.query.timefilter.timefilter.getTime() : undefined,
refreshInterval: newTimeRestore
? dataService.query.timefilter.timefilter.getRefreshInterval()
: undefined,
};
if (hasSavedObjectsTagging && newTags) {
if (savedObjectsTaggingService && newTags) {
// remove `hasSavedObjectsTagging` once the savedObjectsTagging service is optional
stateFromSaveModal.tags = newTags;
}
@ -226,7 +220,7 @@ export async function runInteractiveSave(this: DashboardContainer, interactionMo
const beforeAddTime = window.performance.now();
const saveResult = await saveDashboardState({
const saveResult = await dashboardContentManagementService.saveDashboardState({
controlGroupReferences,
panelReferences: references,
saveOptions,
@ -240,7 +234,7 @@ export async function runInteractiveSave(this: DashboardContainer, interactionMo
const addDuration = window.performance.now() - beforeAddTime;
reportPerformanceMetricEvent(pluginServices.getServices().analytics, {
reportPerformanceMetricEvent(coreServices.analytics, {
eventName: SAVED_OBJECT_POST_TIME,
duration: addDuration,
meta: {
@ -279,7 +273,7 @@ export async function runInteractiveSave(this: DashboardContainer, interactionMo
newTitle = `${baseTitle} (${baseCount + 1})`;
await checkForDuplicateDashboardTitle({
await dashboardContentManagementService.checkForDuplicateDashboardTitle({
title: newTitle,
lastSavedTitle: currentState.title,
copyOnSave: true,

View file

@ -7,34 +7,34 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { EmbeddablePackageState, ViewMode } from '@kbn/embeddable-plugin/public';
import {
CONTACT_CARD_EMBEDDABLE,
ContactCardEmbeddable,
ContactCardEmbeddableFactory,
ContactCardEmbeddableInput,
ContactCardEmbeddableOutput,
CONTACT_CARD_EMBEDDABLE,
} from '@kbn/embeddable-plugin/public/lib/test_samples';
import { Filter } from '@kbn/es-query';
import { EmbeddablePackageState, ViewMode } from '@kbn/embeddable-plugin/public';
import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
import { createDashboard } from './create_dashboard';
import { getSampleDashboardPanel } from '../../../mocks';
import { pluginServices } from '../../../services/plugin_services';
import { DashboardCreationOptions } from '../dashboard_container_factory';
import { DEFAULT_DASHBOARD_INPUT } from '../../../dashboard_constants';
import { mockControlGroupApi } from '../../../mocks';
import { getSampleDashboardPanel, mockControlGroupApi } from '../../../mocks';
import { dataService, embeddableService } from '../../../services/kibana_services';
import { DashboardCreationOptions } from '../dashboard_container_factory';
import { createDashboard } from './create_dashboard';
import { getDashboardContentManagementService } from '../../../services/dashboard_content_management_service';
import { getDashboardBackupService } from '../../../services/dashboard_backup_service';
const dashboardBackupService = getDashboardBackupService();
const dashboardContentManagementService = getDashboardContentManagementService();
test("doesn't throw error when no data views are available", async () => {
pluginServices.getServices().data.dataViews.defaultDataViewExists = jest
.fn()
.mockReturnValue(false);
dataService.dataViews.defaultDataViewExists = jest.fn().mockReturnValue(false);
expect(await createDashboard()).toBeDefined();
// reset get default data view
pluginServices.getServices().data.dataViews.defaultDataViewExists = jest
.fn()
.mockResolvedValue(true);
dataService.dataViews.defaultDataViewExists = jest.fn().mockResolvedValue(true);
});
test('throws error when provided validation function returns invalid', async () => {
@ -73,79 +73,63 @@ test('does not get initial input when provided validation function returns redir
});
test('pulls state from dashboard saved object when given a saved object id', async () => {
pluginServices.getServices().dashboardContentManagement.loadDashboardState = jest
.fn()
.mockResolvedValue({
dashboardInput: {
...DEFAULT_DASHBOARD_INPUT,
description: `wow would you look at that? Wow.`,
},
});
dashboardContentManagementService.loadDashboardState = jest.fn().mockResolvedValue({
dashboardInput: {
...DEFAULT_DASHBOARD_INPUT,
description: `wow would you look at that? Wow.`,
},
});
const dashboard = await createDashboard({}, 0, 'wow-such-id');
expect(
pluginServices.getServices().dashboardContentManagement.loadDashboardState
).toHaveBeenCalledWith({ id: 'wow-such-id' });
expect(dashboardContentManagementService.loadDashboardState).toHaveBeenCalledWith({
id: 'wow-such-id',
});
expect(dashboard).toBeDefined();
expect(dashboard!.getState().explicitInput.description).toBe(`wow would you look at that? Wow.`);
});
test('passes managed state from the saved object into the Dashboard component state', async () => {
pluginServices.getServices().dashboardContentManagement.loadDashboardState = jest
.fn()
.mockResolvedValue({
dashboardInput: {
...DEFAULT_DASHBOARD_INPUT,
description: 'wow this description is okay',
},
managed: true,
});
dashboardContentManagementService.loadDashboardState = jest.fn().mockResolvedValue({
dashboardInput: {
...DEFAULT_DASHBOARD_INPUT,
description: 'wow this description is okay',
},
managed: true,
});
const dashboard = await createDashboard({}, 0, 'what-an-id');
expect(dashboard).toBeDefined();
expect(dashboard!.getState().componentState.managed).toBe(true);
});
test('pulls view mode from dashboard backup', async () => {
pluginServices.getServices().dashboardContentManagement.loadDashboardState = jest
.fn()
.mockResolvedValue({
dashboardInput: DEFAULT_DASHBOARD_INPUT,
});
pluginServices.getServices().dashboardBackup.getViewMode = jest
.fn()
.mockReturnValue(ViewMode.EDIT);
dashboardContentManagementService.loadDashboardState = jest.fn().mockResolvedValue({
dashboardInput: DEFAULT_DASHBOARD_INPUT,
});
dashboardBackupService.getViewMode = jest.fn().mockReturnValue(ViewMode.EDIT);
const dashboard = await createDashboard({ useSessionStorageIntegration: true }, 0, 'what-an-id');
expect(dashboard).toBeDefined();
expect(dashboard!.getState().explicitInput.viewMode).toBe(ViewMode.EDIT);
});
test('new dashboards start in edit mode', async () => {
pluginServices.getServices().dashboardBackup.getViewMode = jest
.fn()
.mockReturnValue(ViewMode.VIEW);
pluginServices.getServices().dashboardContentManagement.loadDashboardState = jest
.fn()
.mockResolvedValue({
newDashboardCreated: true,
dashboardInput: {
...DEFAULT_DASHBOARD_INPUT,
description: 'wow this description is okay',
},
});
dashboardBackupService.getViewMode = jest.fn().mockReturnValue(ViewMode.VIEW);
dashboardContentManagementService.loadDashboardState = jest.fn().mockResolvedValue({
newDashboardCreated: true,
dashboardInput: {
...DEFAULT_DASHBOARD_INPUT,
description: 'wow this description is okay',
},
});
const dashboard = await createDashboard({ useSessionStorageIntegration: true }, 0, 'wow-such-id');
expect(dashboard).toBeDefined();
expect(dashboard!.getState().explicitInput.viewMode).toBe(ViewMode.EDIT);
});
test('managed dashboards start in view mode', async () => {
pluginServices.getServices().dashboardBackup.getViewMode = jest
.fn()
.mockReturnValue(ViewMode.EDIT);
pluginServices.getServices().dashboardContentManagement.loadDashboardState = jest
.fn()
.mockResolvedValue({
dashboardInput: DEFAULT_DASHBOARD_INPUT,
managed: true,
});
dashboardBackupService.getViewMode = jest.fn().mockReturnValue(ViewMode.EDIT);
dashboardContentManagementService.loadDashboardState = jest.fn().mockResolvedValue({
dashboardInput: DEFAULT_DASHBOARD_INPUT,
managed: true,
});
const dashboard = await createDashboard({}, 0, 'what-an-id');
expect(dashboard).toBeDefined();
expect(dashboard!.getState().componentState.managed).toBe(true);
@ -153,15 +137,13 @@ test('managed dashboards start in view mode', async () => {
});
test('pulls state from backup which overrides state from saved object', async () => {
pluginServices.getServices().dashboardContentManagement.loadDashboardState = jest
.fn()
.mockResolvedValue({
dashboardInput: {
...DEFAULT_DASHBOARD_INPUT,
description: 'wow this description is okay',
},
});
pluginServices.getServices().dashboardBackup.getState = jest
dashboardContentManagementService.loadDashboardState = jest.fn().mockResolvedValue({
dashboardInput: {
...DEFAULT_DASHBOARD_INPUT,
description: 'wow this description is okay',
},
});
dashboardBackupService.getState = jest
.fn()
.mockReturnValue({ dashboardState: { description: 'wow this description marginally better' } });
const dashboard = await createDashboard({ useSessionStorageIntegration: true }, 0, 'wow-such-id');
@ -172,15 +154,13 @@ test('pulls state from backup which overrides state from saved object', async ()
});
test('pulls state from override input which overrides all other state sources', async () => {
pluginServices.getServices().dashboardContentManagement.loadDashboardState = jest
.fn()
.mockResolvedValue({
dashboardInput: {
...DEFAULT_DASHBOARD_INPUT,
description: 'wow this description is okay',
},
});
pluginServices.getServices().dashboardBackup.getState = jest
dashboardContentManagementService.loadDashboardState = jest.fn().mockResolvedValue({
dashboardInput: {
...DEFAULT_DASHBOARD_INPUT,
description: 'wow this description is okay',
},
});
dashboardBackupService.getState = jest
.fn()
.mockReturnValue({ description: 'wow this description marginally better' });
const dashboard = await createDashboard(
@ -198,35 +178,33 @@ test('pulls state from override input which overrides all other state sources',
});
test('pulls panels from override input', async () => {
pluginServices.getServices().embeddable.reactEmbeddableRegistryHasKey = jest
embeddableService.reactEmbeddableRegistryHasKey = jest
.fn()
.mockImplementation((type: string) => type === 'reactEmbeddable');
pluginServices.getServices().dashboardContentManagement.loadDashboardState = jest
.fn()
.mockResolvedValue({
dashboardInput: {
...DEFAULT_DASHBOARD_INPUT,
panels: {
...DEFAULT_DASHBOARD_INPUT.panels,
someLegacyPanel: {
type: 'legacy',
gridData: { x: 0, y: 0, w: 0, h: 0, i: 'someLegacyPanel' },
explicitInput: {
id: 'someLegacyPanel',
title: 'stateFromSavedObject',
},
dashboardContentManagementService.loadDashboardState = jest.fn().mockResolvedValue({
dashboardInput: {
...DEFAULT_DASHBOARD_INPUT,
panels: {
...DEFAULT_DASHBOARD_INPUT.panels,
someLegacyPanel: {
type: 'legacy',
gridData: { x: 0, y: 0, w: 0, h: 0, i: 'someLegacyPanel' },
explicitInput: {
id: 'someLegacyPanel',
title: 'stateFromSavedObject',
},
someReactEmbeddablePanel: {
type: 'reactEmbeddable',
gridData: { x: 0, y: 0, w: 0, h: 0, i: 'someReactEmbeddablePanel' },
explicitInput: {
id: 'someReactEmbeddablePanel',
title: 'stateFromSavedObject',
},
},
someReactEmbeddablePanel: {
type: 'reactEmbeddable',
gridData: { x: 0, y: 0, w: 0, h: 0, i: 'someReactEmbeddablePanel' },
explicitInput: {
id: 'someReactEmbeddablePanel',
title: 'stateFromSavedObject',
},
},
},
});
},
});
const dashboard = await createDashboard(
{
useSessionStorageIntegration: true,
@ -286,10 +264,8 @@ test('applies filters and query from state to query service', async () => {
},
getInitialInput: () => ({ filters, query }),
});
expect(pluginServices.getServices().data.query.queryString.setQuery).toHaveBeenCalledWith(query);
expect(pluginServices.getServices().data.query.filterManager.setAppFilters).toHaveBeenCalledWith(
filters
);
expect(dataService.query.queryString.setQuery).toHaveBeenCalledWith(query);
expect(dataService.query.filterManager.setAppFilters).toHaveBeenCalledWith(filters);
});
test('applies time range and refresh interval from initial input to query service if time restore is on', async () => {
@ -302,20 +278,16 @@ test('applies time range and refresh interval from initial input to query servic
},
getInitialInput: () => ({ timeRange, refreshInterval, timeRestore: true }),
});
expect(
pluginServices.getServices().data.query.timefilter.timefilter.setTime
).toHaveBeenCalledWith(timeRange);
expect(
pluginServices.getServices().data.query.timefilter.timefilter.setRefreshInterval
).toHaveBeenCalledWith(refreshInterval);
expect(dataService.query.timefilter.timefilter.setTime).toHaveBeenCalledWith(timeRange);
expect(dataService.query.timefilter.timefilter.setRefreshInterval).toHaveBeenCalledWith(
refreshInterval
);
});
test('applies time range from query service to initial input if time restore is on but there is an explicit time range in the URL', async () => {
const urlTimeRange = { from: new Date().toISOString(), to: new Date().toISOString() };
const savedTimeRange = { from: 'now - 7 days', to: 'now' };
pluginServices.getServices().data.query.timefilter.timefilter.getTime = jest
.fn()
.mockReturnValue(urlTimeRange);
dataService.query.timefilter.timefilter.getTime = jest.fn().mockReturnValue(urlTimeRange);
const kbnUrlStateStorage = createKbnUrlStateStorage();
kbnUrlStateStorage.get = jest.fn().mockReturnValue({ time: urlTimeRange });
@ -335,9 +307,7 @@ test('applies time range from query service to initial input if time restore is
test('applies time range from query service to initial input if time restore is off', async () => {
const timeRange = { from: new Date().toISOString(), to: new Date().toISOString() };
pluginServices.getServices().data.query.timefilter.timefilter.getTime = jest
.fn()
.mockReturnValue(timeRange);
dataService.query.timefilter.timefilter.getTime = jest.fn().mockReturnValue(timeRange);
const dashboard = await createDashboard({
useUnifiedSearchIntegration: true,
unifiedSearchSettings: {
@ -393,9 +363,7 @@ test('creates new embeddable with incoming embeddable if id does not match exist
create: jest.fn().mockReturnValue({ destroy: jest.fn() }),
getDefaultInput: jest.fn().mockResolvedValue({}),
};
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
.fn()
.mockReturnValue(mockContactCardFactory);
embeddableService.getEmbeddableFactory = jest.fn().mockReturnValue(mockContactCardFactory);
const dashboard = await createDashboard({
getIncomingEmbeddable: () => incomingEmbeddable,
@ -454,9 +422,7 @@ test('creates new embeddable with specified size if size is provided', async ()
create: jest.fn().mockReturnValue({ destroy: jest.fn() }),
getDefaultInput: jest.fn().mockResolvedValue({}),
};
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
.fn()
.mockReturnValue(mockContactCardFactory);
embeddableService.getEmbeddableFactory = jest.fn().mockReturnValue(mockContactCardFactory);
const dashboard = await createDashboard({
getIncomingEmbeddable: () => incomingEmbeddable,
@ -513,11 +479,9 @@ test('searchSessionId is updated prior to child embeddable parent subscription e
},
}),
};
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
.fn()
.mockReturnValue(embeddableFactory);
embeddableService.getEmbeddableFactory = jest.fn().mockReturnValue(embeddableFactory);
let sessionCount = 0;
pluginServices.getServices().data.search.session.start = () => {
dataService.search.session.start = () => {
sessionCount++;
return `searchSessionId${sessionCount}`;
};

View file

@ -7,13 +7,16 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { cloneDeep, omit } from 'lodash';
import { Subject } from 'rxjs';
import { v4 } from 'uuid';
import { ContentInsightsClient } from '@kbn/content-management-content-insights-public';
import { GlobalQueryStateFromUrl, syncGlobalQueryStateWithUrl } from '@kbn/data-plugin/public';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { TimeRange } from '@kbn/es-query';
import { lazyLoadReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
import { cloneDeep, omit } from 'lodash';
import { Subject } from 'rxjs';
import { v4 } from 'uuid';
import {
DashboardContainerInput,
DashboardPanelMap,
@ -26,11 +29,17 @@ import {
GLOBAL_STATE_STORAGE_KEY,
PanelPlacementStrategy,
} from '../../../dashboard_constants';
import {
PANELS_CONTROL_GROUP_KEY,
getDashboardBackupService,
} from '../../../services/dashboard_backup_service';
import { getDashboardContentManagementService } from '../../../services/dashboard_content_management_service';
import {
LoadDashboardReturn,
SavedDashboardInput,
} from '../../../services/dashboard_content_management/types';
import { pluginServices } from '../../../services/plugin_services';
} from '../../../services/dashboard_content_management_service/types';
import { coreServices, dataService, embeddableService } from '../../../services/kibana_services';
import { getDashboardCapabilities } from '../../../utils/get_dashboard_capabilities';
import { runPanelPlacementStrategy } from '../../panel_placement/place_new_panel_strategies';
import { startDiffingDashboardState } from '../../state/diffing/dashboard_diffing_integration';
import { DashboardPublicState, UnsavedPanelState } from '../../types';
@ -40,7 +49,6 @@ import { startSyncingDashboardDataViews } from './data_views/sync_dashboard_data
import { startQueryPerformanceTracking } from './performance/query_performance_tracking';
import { startDashboardSearchSessionIntegration } from './search_sessions/start_dashboard_search_session_integration';
import { syncUnifiedSearchState } from './unified_search/sync_dashboard_unified_search_state';
import { PANELS_CONTROL_GROUP_KEY } from '../../../services/dashboard_backup/dashboard_backup_service';
/**
* Builds a new Dashboard from scratch.
@ -50,11 +58,6 @@ export const createDashboard = async (
dashboardCreationStartTime?: number,
savedObjectId?: string
): Promise<DashboardContainer | undefined> => {
const {
data: { dataViews },
dashboardContentManagement: { loadDashboardState },
} = pluginServices.getServices();
// --------------------------------------------------------------------------------------
// Create method which allows work to be done on the dashboard container when it's ready.
// --------------------------------------------------------------------------------------
@ -71,8 +74,11 @@ export const createDashboard = async (
// Lazy load required systems and Dashboard saved object.
// --------------------------------------------------------------------------------------
const reduxEmbeddablePackagePromise = lazyLoadReduxToolsPackage();
const defaultDataViewExistsPromise = dataViews.defaultDataViewExists();
const dashboardSavedObjectPromise = loadDashboardState({ id: savedObjectId });
const defaultDataViewExistsPromise = dataService.dataViews.defaultDataViewExists();
const dashboardContentManagementService = getDashboardContentManagementService();
const dashboardSavedObjectPromise = dashboardContentManagementService.loadDashboardState({
id: savedObjectId,
});
const [reduxEmbeddablePackage, savedObjectResult] = await Promise.all([
reduxEmbeddablePackagePromise,
@ -140,21 +146,12 @@ export const initializeDashboard = async ({
untilDashboardReady: () => Promise<DashboardContainer>;
creationOptions?: DashboardCreationOptions;
}) => {
const {
dashboardBackup,
dashboardCapabilities: { showWriteControls },
embeddable: { reactEmbeddableRegistryHasKey },
data: {
query: queryService,
search: { session },
},
dashboardContentInsights,
} = pluginServices.getServices();
const {
queryString,
filterManager,
timefilter: { timefilter: timefilterService },
} = queryService;
} = dataService.query;
const dashboardBackupService = getDashboardBackupService();
const {
getInitialInput,
@ -179,7 +176,7 @@ export const initializeDashboard = async ({
// --------------------------------------------------------------------------------------
// Combine input from saved object, and session storage
// --------------------------------------------------------------------------------------
const dashboardBackupState = dashboardBackup.getState(loadDashboardReturn.dashboardId);
const dashboardBackupState = dashboardBackupService.getState(loadDashboardReturn.dashboardId);
const runtimePanelsToRestore: UnsavedPanelState = useSessionStorageIntegration
? dashboardBackupState?.panels ?? {}
: {};
@ -189,15 +186,16 @@ export const initializeDashboard = async ({
return dashboardBackupState?.dashboardState;
})();
const initialViewMode = (() => {
if (loadDashboardReturn.managed || !showWriteControls) return ViewMode.VIEW;
if (loadDashboardReturn.managed || !getDashboardCapabilities().showWriteControls)
return ViewMode.VIEW;
if (
loadDashboardReturn.newDashboardCreated ||
dashboardBackup.dashboardHasUnsavedEdits(loadDashboardReturn.dashboardId)
dashboardBackupService.dashboardHasUnsavedEdits(loadDashboardReturn.dashboardId)
) {
return ViewMode.EDIT;
}
return dashboardBackup.getViewMode();
return dashboardBackupService.getViewMode();
})();
const combinedSessionInput: DashboardContainerInput = {
@ -218,7 +216,7 @@ export const initializeDashboard = async ({
const overridePanels: DashboardPanelMap = {};
for (const panel of Object.values(overrideInput?.panels)) {
if (reactEmbeddableRegistryHasKey(panel.type)) {
if (embeddableService.reactEmbeddableRegistryHasKey(panel.type)) {
overridePanels[panel.explicitInput.id] = {
...panel,
@ -263,7 +261,7 @@ export const initializeDashboard = async ({
// Back up any view mode passed in explicitly.
if (overrideInput?.viewMode) {
dashboardBackup.storeViewMode(overrideInput?.viewMode);
dashboardBackupService.storeViewMode(overrideInput?.viewMode);
}
initialDashboardInput.executionContext = {
@ -319,7 +317,7 @@ export const initializeDashboard = async ({
// start syncing global query state with the URL.
const { stop: stopSyncingQueryServiceStateWithUrl } = syncGlobalQueryStateWithUrl(
queryService,
dataService.query,
kbnUrlStateStorage
);
@ -363,7 +361,7 @@ export const initializeDashboard = async ({
// maintain hide panel titles setting.
hidePanelTitles: panelToUpdate.explicitInput.hidePanelTitles,
};
if (reactEmbeddableRegistryHasKey(incomingEmbeddable.type)) {
if (embeddableService.reactEmbeddableRegistryHasKey(incomingEmbeddable.type)) {
panelToUpdate.explicitInput = { id: panelToUpdate.explicitInput.id };
runtimePanelsToRestore[incomingEmbeddable.embeddableId] = nextRuntimeState;
} else {
@ -399,7 +397,7 @@ export const initializeDashboard = async ({
}
);
const newPanelState: DashboardPanelState = (() => {
if (reactEmbeddableRegistryHasKey(incomingEmbeddable.type)) {
if (embeddableService.reactEmbeddableRegistryHasKey(incomingEmbeddable.type)) {
runtimePanelsToRestore[embeddableId] = incomingEmbeddable.input;
return {
explicitInput: { id: embeddableId },
@ -487,16 +485,18 @@ export const initializeDashboard = async ({
// if this incoming embeddable has a session, continue it.
if (incomingEmbeddable?.searchSessionId) {
session.continue(incomingEmbeddable.searchSessionId);
dataService.search.session.continue(incomingEmbeddable.searchSessionId);
}
if (sessionIdToRestore) {
session.restore(sessionIdToRestore);
dataService.search.session.restore(sessionIdToRestore);
}
const existingSession = session.getSessionId();
const existingSession = dataService.search.session.getSessionId();
initialSearchSessionId =
sessionIdToRestore ??
(existingSession && incomingEmbeddable ? existingSession : session.start());
(existingSession && incomingEmbeddable
? existingSession
: dataService.search.session.start());
untilDashboardReady().then(async (container) => {
await container.untilContainerInitialized();
@ -511,7 +511,11 @@ export const initializeDashboard = async ({
// We don't count views when a user is editing a dashboard and is returning from an editor after saving
// however, there is an edge case that we now count a new view when a user is editing a dashboard and is returning from an editor by canceling
// TODO: this should be revisited by making embeddable transfer support canceling logic https://github.com/elastic/kibana/issues/190485
dashboardContentInsights.trackDashboardView(loadDashboardReturn.dashboardId);
const contentInsightsClient = new ContentInsightsClient(
{ http: coreServices.http },
{ domainId: 'dashboard' }
);
contentInsightsClient.track(loadDashboardReturn.dashboardId, 'viewed');
}
return { input: initialDashboardInput, searchSessionId: initialSearchSessionId };

View file

@ -7,19 +7,17 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { uniqBy } from 'lodash';
import { combineLatest, Observable, of, switchMap } from 'rxjs';
import { DataView } from '@kbn/data-views-plugin/common';
import { combineCompatibleChildrenApis } from '@kbn/presentation-containers';
import { apiPublishesDataViews, PublishesDataViews } from '@kbn/presentation-publishing';
import { uniqBy } from 'lodash';
import { combineLatest, Observable, of, switchMap } from 'rxjs';
import { pluginServices } from '../../../../services/plugin_services';
import { dataService } from '../../../../services/kibana_services';
import { DashboardContainer } from '../../dashboard_container';
export function startSyncingDashboardDataViews(this: DashboardContainer) {
const {
data: { dataViews },
} = pluginServices.getServices();
const controlGroupDataViewsPipe: Observable<DataView[] | undefined> = this.controlGroupApi$.pipe(
switchMap((controlGroupApi) => {
return controlGroupApi ? controlGroupApi.dataViews : of([]);
@ -42,8 +40,8 @@ export function startSyncingDashboardDataViews(this: DashboardContainer) {
];
if (allDataViews.length === 0) {
return (async () => {
const defaultDataViewId = await dataViews.getDefaultId();
return [await dataViews.get(defaultDataViewId!)];
const defaultDataViewId = await dataService.dataViews.getDefaultId();
return [await dataService.dataViews.get(defaultDataViewId!)];
})();
}
return of(uniqBy(allDataViews, 'id'));

View file

@ -7,18 +7,18 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { CoreStart } from '@kbn/core/public';
import { PerformanceMetricEvent } from '@kbn/ebt-tools';
import { PresentationContainer, TracksQueryPerformance } from '@kbn/presentation-containers';
import { getMockPresentationContainer } from '@kbn/presentation-containers/mocks';
import { apiPublishesPhaseEvents, PhaseEvent, PhaseEventType } from '@kbn/presentation-publishing';
import { PhaseEvent, PhaseEventType, apiPublishesPhaseEvents } from '@kbn/presentation-publishing';
import { waitFor } from '@testing-library/react';
import { BehaviorSubject } from 'rxjs';
import { DashboardAnalyticsService } from '../../../../services/analytics/types';
import { startQueryPerformanceTracking } from './query_performance_tracking';
const mockMetricEvent = jest.fn();
jest.mock('@kbn/ebt-tools', () => ({
reportPerformanceMetricEvent: (_: DashboardAnalyticsService, args: PerformanceMetricEvent) => {
reportPerformanceMetricEvent: (_: CoreStart['analytics'], args: PerformanceMetricEvent) => {
mockMetricEvent(args);
},
}));

View file

@ -7,12 +7,14 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { combineLatest, map, of, pairwise, startWith, switchMap } from 'rxjs';
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
import { PresentationContainer, TracksQueryPerformance } from '@kbn/presentation-containers';
import { apiPublishesPhaseEvents, PublishesPhaseEvents } from '@kbn/presentation-publishing';
import { combineLatest, map, of, pairwise, startWith, switchMap } from 'rxjs';
import { PublishesPhaseEvents, apiPublishesPhaseEvents } from '@kbn/presentation-publishing';
import { DASHBOARD_LOADED_EVENT } from '../../../../dashboard_constants';
import { pluginServices } from '../../../../services/plugin_services';
import { coreServices } from '../../../../services/kibana_services';
import { DashboardLoadType } from '../../../types';
let isFirstDashboardLoadOfSession = true;
@ -26,7 +28,6 @@ const loadTypesMapping: { [key in DashboardLoadType]: number } = {
export const startQueryPerformanceTracking = (
dashboard: PresentationContainer & TracksQueryPerformance
) => {
const { analytics } = pluginServices.getServices();
const reportPerformanceMetrics = ({
timeToData,
panelCount,
@ -41,7 +42,7 @@ export const startQueryPerformanceTracking = (
const duration =
loadType === 'dashboardSubsequentLoad' ? timeToData : Math.max(timeToData, totalLoadTime);
reportPerformanceMetricEvent(analytics, {
reportPerformanceMetricEvent(coreServices.analytics, {
eventName: DASHBOARD_LOADED_EVENT,
duration,
key1: 'time_to_data',

View file

@ -11,10 +11,11 @@ import { skip } from 'rxjs';
import { noSearchSessionStorageCapabilityMessage } from '@kbn/data-plugin/public';
import { dataService } from '../../../../services/kibana_services';
import { DashboardContainer } from '../../dashboard_container';
import { pluginServices } from '../../../../services/plugin_services';
import { DashboardCreationOptions } from '../../dashboard_container_factory';
import { newSession$ } from './new_session';
import { getDashboardCapabilities } from '../../../../utils/get_dashboard_capabilities';
/**
* Enables dashboard search sessions.
@ -25,13 +26,6 @@ export function startDashboardSearchSessionIntegration(
) {
if (!searchSessionSettings) return;
const {
data: {
search: { session },
},
dashboardCapabilities: { storeSearchSession: canStoreSearchSession },
} = pluginServices.getServices();
const {
sessionIdUrlChangeObservable,
getSearchSessionIdFromURL,
@ -39,9 +33,9 @@ export function startDashboardSearchSessionIntegration(
createSessionRestorationDataProvider,
} = searchSessionSettings;
session.enableStorage(createSessionRestorationDataProvider(this), {
dataService.search.session.enableStorage(createSessionRestorationDataProvider(this), {
isDisabled: () =>
canStoreSearchSession
getDashboardCapabilities().storeSearchSession
? { disabled: false }
: {
disabled: true,
@ -60,15 +54,18 @@ export function startDashboardSearchSessionIntegration(
const updatedSearchSessionId: string | undefined = (() => {
let searchSessionIdFromURL = getSearchSessionIdFromURL();
if (searchSessionIdFromURL) {
if (session.isRestore() && session.isCurrentSession(searchSessionIdFromURL)) {
if (
dataService.search.session.isRestore() &&
dataService.search.session.isCurrentSession(searchSessionIdFromURL)
) {
// we had previously been in a restored session but have now changed state so remove the session id from the URL.
removeSessionIdFromUrl();
searchSessionIdFromURL = undefined;
} else {
session.restore(searchSessionIdFromURL);
dataService.search.session.restore(searchSessionIdFromURL);
}
}
return searchSessionIdFromURL ?? session.start();
return searchSessionIdFromURL ?? dataService.search.session.start();
})();
if (updatedSearchSessionId && updatedSearchSessionId !== currentSearchSessionId) {

View file

@ -21,9 +21,9 @@ import {
} from '@kbn/data-plugin/public';
import { DashboardContainer } from '../../dashboard_container';
import { pluginServices } from '../../../../services/plugin_services';
import { GLOBAL_STATE_STORAGE_KEY } from '../../../../dashboard_constants';
import { areTimesEqual } from '../../../state/diffing/dashboard_diffing_utils';
import { dataService } from '../../../../services/kibana_services';
/**
* Sets up syncing and subscriptions between the filter state from the Data plugin
@ -33,11 +33,7 @@ export function syncUnifiedSearchState(
this: DashboardContainer,
kbnUrlStateStorage: IKbnUrlStateStorage
) {
const {
data: { query: queryService, search },
} = pluginServices.getServices();
const { queryString, timefilter } = queryService;
const { timefilter: timefilterService } = timefilter;
const timefilterService = dataService.query.timefilter.timefilter;
// get Observable for when the dashboard's saved filters or query change.
const OnFiltersChange$ = new Subject<{ filters: Filter[]; query: Query }>();
@ -47,7 +43,7 @@ export function syncUnifiedSearchState(
} = this.getState();
OnFiltersChange$.next({
filters: filters ?? [],
query: query ?? queryString.getDefaultQuery(),
query: query ?? dataService.query.queryString.getDefaultQuery(),
});
});
@ -56,12 +52,12 @@ export function syncUnifiedSearchState(
explicitInput: { filters, query },
} = this.getState();
const intermediateFilterState: { filters: Filter[]; query: Query } = {
query: query ?? queryString.getDefaultQuery(),
query: query ?? dataService.query.queryString.getDefaultQuery(),
filters: filters ?? [],
};
const stopSyncingAppFilters = connectToQueryState(
queryService,
dataService.query,
{
get: () => intermediateFilterState,
set: ({ filters: newFilters, query: newQuery }) => {
@ -144,7 +140,7 @@ export function syncUnifiedSearchState(
}),
switchMap((done) =>
// best way on a dashboard to estimate that panels are updated is to rely on search session service state
waitUntilNextSessionCompletes$(search.session).pipe(finalize(done))
waitUntilNextSessionCompletes$(dataService.search.session).pipe(finalize(done))
)
)
.subscribe();

View file

@ -9,11 +9,11 @@
import { isErrorEmbeddable, ViewMode } from '@kbn/embeddable-plugin/public';
import {
CONTACT_CARD_EMBEDDABLE,
ContactCardEmbeddable,
ContactCardEmbeddableFactory,
ContactCardEmbeddableInput,
ContactCardEmbeddableOutput,
CONTACT_CARD_EMBEDDABLE,
EMPTY_EMBEDDABLE,
} from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables';
import type { TimeRange } from '@kbn/es-query';
@ -25,13 +25,11 @@ import {
getSampleDashboardPanel,
mockControlGroupApi,
} from '../../mocks';
import { pluginServices } from '../../services/plugin_services';
import { embeddableService } from '../../services/kibana_services';
import { DashboardContainer } from './dashboard_container';
const embeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
.fn()
.mockReturnValue(embeddableFactory);
embeddableService.getEmbeddableFactory = jest.fn().mockReturnValue(embeddableFactory);
test('DashboardContainer initializes embeddables', (done) => {
const container = buildMockDashboard({

View file

@ -7,59 +7,69 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import deepEqual from 'fast-deep-equal';
import { omit } from 'lodash';
import React from 'react';
import ReactDOM from 'react-dom';
import { batch } from 'react-redux';
import {
BehaviorSubject,
Subject,
Subscription,
distinctUntilChanged,
first,
map,
skipWhile,
switchMap,
} from 'rxjs';
import { v4 } from 'uuid';
import { METRIC_TYPE } from '@kbn/analytics';
import type { Reference } from '@kbn/content-management-utils';
import type { I18nStart, KibanaExecutionContext, OverlayRef } from '@kbn/core/public';
import {
type PublishingSubject,
apiPublishesPanelTitle,
apiPublishesUnsavedChanges,
getPanelTitle,
PublishesViewMode,
PublishesDataLoading,
apiPublishesDataLoading,
} from '@kbn/presentation-publishing';
import { ControlGroupApi, ControlGroupSerializedState } from '@kbn/controls-plugin/public';
import type { KibanaExecutionContext, OverlayRef } from '@kbn/core/public';
import { RefreshInterval } from '@kbn/data-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import {
Container,
DefaultEmbeddableApi,
EmbeddableFactoryNotFoundError,
embeddableInputToSubject,
isExplicitInputWithAttributes,
PanelNotFoundError,
ViewMode,
embeddableInputToSubject,
isExplicitInputWithAttributes,
type EmbeddableFactory,
type EmbeddableInput,
type EmbeddableOutput,
type IEmbeddable,
} from '@kbn/embeddable-plugin/public';
import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import {
HasRuntimeChildState,
HasSaveNotification,
HasSerializedChildState,
PanelPackage,
TrackContentfulRender,
TracksQueryPerformance,
combineCompatibleChildrenApis,
} from '@kbn/presentation-containers';
import { PanelPackage } from '@kbn/presentation-containers';
import { ReduxEmbeddableTools, ReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
import { LocatorPublic } from '@kbn/share-plugin/common';
import { ExitFullScreenButtonKibanaProvider } from '@kbn/shared-ux-button-exit-full-screen';
import deepEqual from 'fast-deep-equal';
import { omit } from 'lodash';
import React from 'react';
import ReactDOM from 'react-dom';
import { batch } from 'react-redux';
import { BehaviorSubject, Subject, Subscription, first, skipWhile, switchMap } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs';
import { v4 } from 'uuid';
import { PublishesSettings } from '@kbn/presentation-containers/interfaces/publishes_settings';
import { apiHasSerializableState } from '@kbn/presentation-containers/interfaces/serialized_state';
import { ControlGroupApi, ControlGroupSerializedState } from '@kbn/controls-plugin/public';
import { DashboardLocatorParams, DASHBOARD_CONTAINER_TYPE, DashboardApi } from '../..';
import {
PublishesDataLoading,
PublishesViewMode,
apiPublishesDataLoading,
apiPublishesPanelTitle,
apiPublishesUnsavedChanges,
getPanelTitle,
type PublishingSubject,
} from '@kbn/presentation-publishing';
import { ReduxEmbeddableTools, ReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { LocatorPublic } from '@kbn/share-plugin/common';
import { ExitFullScreenButtonKibanaProvider } from '@kbn/shared-ux-button-exit-full-screen';
import { DASHBOARD_CONTAINER_TYPE, DashboardApi, DashboardLocatorParams } from '../..';
import {
DashboardAttributes,
DashboardContainerInput,
@ -70,6 +80,8 @@ import {
getReferencesForControls,
getReferencesForPanelId,
} from '../../../common/dashboard_container/persistable_state/dashboard_container_references';
import { DashboardContext } from '../../dashboard_api/use_dashboard_api';
import { getPanelAddedSuccessString } from '../../dashboard_app/_dashboard_app_strings';
import {
DASHBOARD_APP_ID,
DASHBOARD_UI_METRIC_ID,
@ -77,13 +89,19 @@ import {
DEFAULT_PANEL_WIDTH,
PanelPlacementStrategy,
} from '../../dashboard_constants';
import { DashboardAnalyticsService } from '../../services/analytics/types';
import { DashboardCapabilitiesService } from '../../services/dashboard_capabilities/types';
import { pluginServices } from '../../services/plugin_services';
import { placePanel } from '../panel_placement';
import { runPanelPlacementStrategy } from '../panel_placement/place_new_panel_strategies';
import { PANELS_CONTROL_GROUP_KEY } from '../../services/dashboard_backup_service';
import { getDashboardContentManagementService } from '../../services/dashboard_content_management_service';
import {
coreServices,
dataService,
embeddableService,
usageCollectionService,
} from '../../services/kibana_services';
import { getDashboardCapabilities } from '../../utils/get_dashboard_capabilities';
import { DashboardViewport } from '../component/viewport/dashboard_viewport';
import { placePanel } from '../panel_placement';
import { getDashboardPanelPlacementSetting } from '../panel_placement/panel_placement_registry';
import { runPanelPlacementStrategy } from '../panel_placement/place_new_panel_strategies';
import { dashboardContainerReducers } from '../state/dashboard_container_reducers';
import { getDiffingMiddleware } from '../state/diffing/dashboard_diffing_integration';
import {
@ -92,7 +110,7 @@ import {
DashboardStateFromSettingsFlyout,
UnsavedPanelState,
} from '../types';
import { addFromLibrary, addOrUpdateEmbeddable, runQuickSave, runInteractiveSave } from './api';
import { addFromLibrary, addOrUpdateEmbeddable, runInteractiveSave, runQuickSave } from './api';
import { duplicateDashboardPanel } from './api/duplicate_dashboard_panel';
import {
combineDashboardFiltersWithControlGroupFilters,
@ -104,9 +122,6 @@ import {
dashboardTypeDisplayLowercase,
dashboardTypeDisplayName,
} from './dashboard_container_factory';
import { getPanelAddedSuccessString } from '../../dashboard_app/_dashboard_app_strings';
import { PANELS_CONTROL_GROUP_KEY } from '../../services/dashboard_backup/dashboard_backup_service';
import { DashboardContext } from '../../dashboard_api/use_dashboard_api';
export interface InheritedChildInput {
filters: Filter[];
@ -186,16 +201,11 @@ export class DashboardContainer
// Services that are used in the Dashboard container code
private creationOptions?: DashboardCreationOptions;
private analyticsService: DashboardAnalyticsService;
private showWriteControls: DashboardCapabilitiesService['showWriteControls'];
private i18n: I18nStart;
private theme;
private chrome;
private customBranding;
private showWriteControls: boolean;
public trackContentfulRender() {
if (!this.hadContentfulRender && this.analyticsService) {
this.analyticsService.reportEvent('dashboard_loaded_with_data', {});
if (!this.hadContentfulRender) {
coreServices.analytics.reportEvent('dashboard_loaded_with_data', {});
}
this.hadContentfulRender = true;
}
@ -238,37 +248,26 @@ export class DashboardContainer
});
}
const {
usageCollection,
embeddable: { getEmbeddableFactory },
} = pluginServices.getServices();
super(
{
...initialInput,
},
{ embeddableLoaded: {} },
getEmbeddableFactory,
embeddableService.getEmbeddableFactory,
parent,
{ untilContainerInitialized }
);
({ showWriteControls: this.showWriteControls } = getDashboardCapabilities());
this.controlGroupApi$ = controlGroupApi$;
this.untilContainerInitialized = untilContainerInitialized;
this.trackPanelAddMetric = usageCollection.reportUiCounter?.bind(
usageCollection,
this.trackPanelAddMetric = usageCollectionService?.reportUiCounter.bind(
usageCollectionService,
DASHBOARD_UI_METRIC_ID
);
({
analytics: this.analyticsService,
settings: { theme: this.theme, i18n: this.i18n },
chrome: this.chrome,
customBranding: this.customBranding,
dashboardCapabilities: { showWriteControls: this.showWriteControls },
} = pluginServices.getServices());
this.creationOptions = creationOptions;
this.searchSessionId = initialSessionId;
this.searchSessionId$.next(initialSessionId);
@ -475,12 +474,12 @@ export class DashboardContainer
ReactDOM.render(
<KibanaRenderContextProvider
analytics={this.analyticsService}
i18n={this.i18n}
theme={this.theme}
analytics={coreServices.analytics}
i18n={coreServices.i18n}
theme={coreServices.theme}
>
<ExitFullScreenButtonKibanaProvider
coreStart={{ chrome: this.chrome, customBranding: this.customBranding }}
coreStart={{ chrome: coreServices.chrome, customBranding: coreServices.customBranding }}
>
<DashboardContext.Provider value={this as DashboardApi}>
<DashboardViewport />
@ -610,14 +609,9 @@ export class DashboardContainer
panelPackage: PanelPackage,
displaySuccessMessage?: boolean
) {
const {
notifications: { toasts },
embeddable: { getEmbeddableFactory, reactEmbeddableRegistryHasKey },
} = pluginServices.getServices();
const onSuccess = (id?: string, title?: string) => {
if (!displaySuccessMessage) return;
toasts.addSuccess({
coreServices.notifications.toasts.addSuccess({
title: getPanelAddedSuccessString(title),
'data-test-subj': 'addEmbeddableToDashboardSuccess',
});
@ -628,10 +622,10 @@ export class DashboardContainer
if (this.trackPanelAddMetric) {
this.trackPanelAddMetric(METRIC_TYPE.CLICK, panelPackage.panelType);
}
if (reactEmbeddableRegistryHasKey(panelPackage.panelType)) {
if (embeddableService.reactEmbeddableRegistryHasKey(panelPackage.panelType)) {
const newId = v4();
const getCustomPlacementSettingFunc = await getDashboardPanelPlacementSetting(
const getCustomPlacementSettingFunc = getDashboardPanelPlacementSetting(
panelPackage.panelType
);
@ -671,7 +665,7 @@ export class DashboardContainer
return await this.untilReactEmbeddableLoaded<ApiType>(newId);
}
const embeddableFactory = getEmbeddableFactory(panelPackage.panelType);
const embeddableFactory = embeddableService.getEmbeddableFactory(panelPackage.panelType);
if (!embeddableFactory) {
throw new EmbeddableFactoryNotFoundError(panelPackage.panelType);
}
@ -709,11 +703,8 @@ export class DashboardContainer
}
public getDashboardPanelFromId = async (panelId: string) => {
const {
embeddable: { reactEmbeddableRegistryHasKey },
} = pluginServices.getServices();
const panel = this.getInput().panels[panelId];
if (reactEmbeddableRegistryHasKey(panel.type)) {
if (embeddableService.reactEmbeddableRegistryHasKey(panel.type)) {
const child = this.children$.value[panelId];
if (!child) throw new PanelNotFoundError();
const serialized = apiHasSerializableState(child)
@ -769,13 +760,7 @@ export class DashboardContainer
// if we are using the unified search integration, we need to force reset the time picker.
if (this.creationOptions?.useUnifiedSearchIntegration && lastSavedTimeRestore) {
const {
data: {
query: {
timefilter: { timefilter: timeFilterService },
},
},
} = pluginServices.getServices();
const timeFilterService = dataService.query.timefilter.timefilter;
if (timeRange) timeFilterService.setTime(timeRange);
if (refreshInterval) timeFilterService.setRefreshInterval(refreshInterval);
}
@ -790,13 +775,12 @@ export class DashboardContainer
this.integrationSubscriptions = new Subscription();
this.stopSyncingWithUnifiedSearch?.();
const {
dashboardContentManagement: { loadDashboardState },
} = pluginServices.getServices();
if (newCreationOptions) {
this.creationOptions = { ...this.creationOptions, ...newCreationOptions };
}
const loadDashboardReturn = await loadDashboardState({ id: newSavedObjectId });
const loadDashboardReturn = await getDashboardContentManagementService().loadDashboardState({
id: newSavedObjectId,
});
const dashboardContainerReady$ = new Subject<DashboardContainer>();
const untilDashboardReady = () =>
@ -908,13 +892,10 @@ export class DashboardContainer
};
public async getPanelTitles(): Promise<string[]> {
const {
embeddable: { reactEmbeddableRegistryHasKey },
} = pluginServices.getServices();
const titles: string[] = [];
for (const [id, panel] of Object.entries(this.getInput().panels)) {
const title = await (async () => {
if (reactEmbeddableRegistryHasKey(panel.type)) {
if (embeddableService.reactEmbeddableRegistryHasKey(panel.type)) {
const child = this.children$.value[id];
return apiPublishesPanelTitle(child) ? getPanelTitle(child) : '';
}
@ -1039,12 +1020,9 @@ export class DashboardContainer
};
public removePanel(id: string) {
const {
embeddable: { reactEmbeddableRegistryHasKey },
} = pluginServices.getServices();
const type = this.getInput().panels[id]?.type;
this.removeEmbeddable(id);
if (reactEmbeddableRegistryHasKey(type)) {
if (embeddableService.reactEmbeddableRegistryHasKey(type)) {
const { [id]: childToRemove, ...otherChildren } = this.children$.value;
this.children$.next(otherChildren);
}

View file

@ -29,7 +29,7 @@ import { DEFAULT_DASHBOARD_INPUT } from '../../dashboard_constants';
import {
LoadDashboardReturn,
SavedDashboardInput,
} from '../../services/dashboard_content_management/types';
} from '../../services/dashboard_content_management_service/types';
import type { DashboardContainer } from './dashboard_container';
export type DashboardContainerFactory = EmbeddableFactory<

View file

@ -7,22 +7,22 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { ReactWrapper } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { NotFoundPrompt } from '@kbn/shared-ux-prompt-not-found';
import { setStubKibanaServices } from '@kbn/embeddable-plugin/public/mocks';
import { NotFoundPrompt } from '@kbn/shared-ux-prompt-not-found';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { ReactWrapper } from 'enzyme';
import React from 'react';
import { act } from 'react-dom/test-utils';
import { DashboardContainerFactory } from '..';
import { DASHBOARD_CONTAINER_TYPE } from '../..';
import { DashboardRenderer } from './dashboard_renderer';
import { pluginServices } from '../../services/plugin_services';
import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common';
import { DashboardContainer } from '../embeddable/dashboard_container';
import { DashboardCreationOptions } from '../embeddable/dashboard_container_factory';
import { setStubKibanaServices as setPresentationPanelMocks } from '@kbn/presentation-panel-plugin/public/mocks';
import { BehaviorSubject } from 'rxjs';
import { DashboardContainerFactory } from '..';
import { DASHBOARD_CONTAINER_TYPE } from '../..';
import { embeddableService } from '../../services/kibana_services';
import { DashboardContainer } from '../embeddable/dashboard_container';
import { DashboardCreationOptions } from '../embeddable/dashboard_container_factory';
import { DashboardRenderer } from './dashboard_renderer';
describe('dashboard renderer', () => {
let mockDashboardContainer: DashboardContainer;
@ -39,9 +39,7 @@ describe('dashboard renderer', () => {
mockDashboardFactory = {
create: jest.fn().mockReturnValue(mockDashboardContainer),
} as unknown as DashboardContainerFactory;
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
.fn()
.mockReturnValue(mockDashboardFactory);
embeddableService.getEmbeddableFactory = jest.fn().mockReturnValue(mockDashboardFactory);
setPresentationPanelMocks();
});
@ -49,9 +47,7 @@ describe('dashboard renderer', () => {
await act(async () => {
mountWithIntl(<DashboardRenderer />);
});
expect(pluginServices.getServices().embeddable.getEmbeddableFactory).toHaveBeenCalledWith(
DASHBOARD_CONTAINER_TYPE
);
expect(embeddableService.getEmbeddableFactory).toHaveBeenCalledWith(DASHBOARD_CONTAINER_TYPE);
expect(mockDashboardFactory.create).toHaveBeenCalled();
});
@ -109,9 +105,7 @@ describe('dashboard renderer', () => {
mockDashboardFactory = {
create: jest.fn().mockReturnValue(mockErrorEmbeddable),
} as unknown as DashboardContainerFactory;
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
.fn()
.mockReturnValue(mockDashboardFactory);
embeddableService.getEmbeddableFactory = jest.fn().mockReturnValue(mockDashboardFactory);
let wrapper: ReactWrapper;
await act(async () => {
@ -133,9 +127,7 @@ describe('dashboard renderer', () => {
const mockErrorFactory = {
create: jest.fn().mockReturnValue(mockErrorEmbeddable),
} as unknown as DashboardContainerFactory;
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
.fn()
.mockReturnValue(mockErrorFactory);
embeddableService.getEmbeddableFactory = jest.fn().mockReturnValue(mockErrorFactory);
// render the dashboard - it should run into an error and render the error embeddable.
let wrapper: ReactWrapper;
@ -156,9 +148,7 @@ describe('dashboard renderer', () => {
const mockSuccessFactory = {
create: jest.fn().mockReturnValue(mockSuccessEmbeddable),
} as unknown as DashboardContainerFactory;
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
.fn()
.mockReturnValue(mockSuccessFactory);
embeddableService.getEmbeddableFactory = jest.fn().mockReturnValue(mockSuccessFactory);
// update the saved object id to trigger another dashboard load.
await act(async () => {
@ -187,9 +177,7 @@ describe('dashboard renderer', () => {
const mockErrorFactory = {
create: jest.fn().mockReturnValue(mockErrorEmbeddable),
} as unknown as DashboardContainerFactory;
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
.fn()
.mockReturnValue(mockErrorFactory);
embeddableService.getEmbeddableFactory = jest.fn().mockReturnValue(mockErrorFactory);
// render the dashboard - it should run into an error and render the error embeddable.
let wrapper: ReactWrapper;
@ -252,9 +240,7 @@ describe('dashboard renderer', () => {
const mockSuccessFactory = {
create: jest.fn().mockReturnValue(mockSuccessEmbeddable),
} as unknown as DashboardContainerFactory;
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
.fn()
.mockReturnValue(mockSuccessFactory);
embeddableService.getEmbeddableFactory = jest.fn().mockReturnValue(mockSuccessFactory);
let wrapper: ReactWrapper;
await act(async () => {
@ -279,9 +265,7 @@ describe('dashboard renderer', () => {
const mockUseMarginFalseFactory = {
create: jest.fn().mockReturnValue(mockUseMarginFalseEmbeddable),
} as unknown as DashboardContainerFactory;
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
.fn()
.mockReturnValue(mockUseMarginFalseFactory);
embeddableService.getEmbeddableFactory = jest.fn().mockReturnValue(mockUseMarginFalseFactory);
let wrapper: ReactWrapper;
await act(async () => {

View file

@ -17,11 +17,13 @@ import { v4 as uuidv4 } from 'uuid';
import { EuiLoadingElastic, EuiLoadingSpinner } from '@elastic/eui';
import { ErrorEmbeddable, isErrorEmbeddable } from '@kbn/embeddable-plugin/public';
import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common';
import { LocatorPublic } from '@kbn/share-plugin/common';
import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
import { LocatorPublic } from '@kbn/share-plugin/common';
import { DASHBOARD_CONTAINER_TYPE } from '..';
import { DashboardContainerInput } from '../../../common';
import { DashboardApi } from '../../dashboard_api/types';
import { embeddableService, screenshotModeService } from '../../services/kibana_services';
import type { DashboardContainer } from '../embeddable/dashboard_container';
import {
DashboardContainerFactory,
@ -30,8 +32,6 @@ import {
} from '../embeddable/dashboard_container_factory';
import { DashboardLocatorParams, DashboardRedirect } from '../types';
import { Dashboard404Page } from './dashboard_404';
import { DashboardApi } from '../../dashboard_api/types';
import { pluginServices } from '../../services/plugin_services';
export interface DashboardRendererProps {
onApiAvailable?: (api: DashboardApi) => void;
@ -57,8 +57,6 @@ export function DashboardRenderer({
const [fatalError, setFatalError] = useState<ErrorEmbeddable | undefined>();
const [dashboardMissing, setDashboardMissing] = useState(false);
const { embeddable, screenshotMode } = pluginServices.getServices();
const id = useMemo(() => uuidv4(), []);
useEffect(() => {
@ -93,7 +91,7 @@ export function DashboardRenderer({
(async () => {
const creationOptions = await getCreationOptions?.();
const dashboardFactory = embeddable.getEmbeddableFactory(
const dashboardFactory = embeddableService.getEmbeddableFactory(
DASHBOARD_CONTAINER_TYPE
) as DashboardContainerFactory & {
create: DashboardContainerFactoryDefinition['create'];
@ -141,7 +139,7 @@ export function DashboardRenderer({
const viewportClasses = classNames(
'dashboardViewport',
{ 'dashboardViewport--screenshotMode': screenshotMode },
{ 'dashboardViewport--screenshotMode': screenshotModeService.isScreenshotMode() },
{ 'dashboardViewport--loading': loading }
);

View file

@ -10,9 +10,13 @@
import React from 'react';
import { dynamic } from '@kbn/shared-ux-utility';
import type { DashboardRendererProps } from './dashboard_renderer';
import { untilPluginStartServicesReady } from '../../services/kibana_services';
const Component = dynamic(async () => {
const { DashboardRenderer } = await import('./dashboard_renderer');
const [{ DashboardRenderer }] = await Promise.all([
import('./dashboard_renderer'),
untilPluginStartServicesReady(),
]);
return {
default: DashboardRenderer,
};

View file

@ -8,7 +8,7 @@
*/
import { LATEST_VERSION } from '../../common/content_management';
import { convertNumberToDashboardVersion } from '../services/dashboard_content_management/lib/dashboard_versioning';
import { convertNumberToDashboardVersion } from '../services/dashboard_content_management_service/lib/dashboard_versioning';
export const DASHBOARD_CONTAINER_TYPE = 'dashboard';

View file

@ -7,10 +7,12 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { compareFilters, COMPARE_ALL_OPTIONS, isFilterPinned } from '@kbn/es-query';
import fastIsEqual from 'fast-deep-equal';
import { COMPARE_ALL_OPTIONS, compareFilters, isFilterPinned } from '@kbn/es-query';
import { DashboardContainerInput } from '../../../../common';
import { pluginServices } from '../../../services/plugin_services';
import { embeddableService } from '../../../services/kibana_services';
import { DashboardContainer } from '../../embeddable/dashboard_container';
import { DashboardContainerInputWithoutId } from '../../types';
import { areTimesEqual, getPanelLayoutsAreEqual } from './dashboard_diffing_utils';
@ -75,11 +77,8 @@ export const unsavedChangesDiffingFunctions: DashboardDiffFunctions = {
const explicitInputComparePromises = Object.values(currentValue ?? {}).map(
(panel) =>
new Promise<boolean>((resolve, reject) => {
const {
embeddable: { reactEmbeddableRegistryHasKey },
} = pluginServices.getServices();
const embeddableId = panel.explicitInput.id;
if (!embeddableId || reactEmbeddableRegistryHasKey(panel.type)) {
if (!embeddableId || embeddableService.reactEmbeddableRegistryHasKey(panel.type)) {
// if this is a new style embeddable, it will handle its own diffing.
reject();
return;

View file

@ -13,11 +13,13 @@ import { combineLatest, debounceTime, skipWhile, startWith, switchMap } from 'rx
import { DashboardContainer, DashboardCreationOptions } from '../..';
import { DashboardContainerInput } from '../../../../common';
import { CHANGE_CHECK_DEBOUNCE } from '../../../dashboard_constants';
import { pluginServices } from '../../../services/plugin_services';
import {
PANELS_CONTROL_GROUP_KEY,
getDashboardBackupService,
} from '../../../services/dashboard_backup_service';
import { UnsavedPanelState } from '../../types';
import { dashboardContainerReducers } from '../dashboard_container_reducers';
import { isKeyEqualAsync, unsavedChangesDiffingFunctions } from './dashboard_diffing_functions';
import { PANELS_CONTROL_GROUP_KEY } from '../../../services/dashboard_backup/dashboard_backup_service';
/**
* An array of reducers which cannot cause unsaved changes. Unsaved changes only compares the explicit input
@ -188,10 +190,9 @@ function backupUnsavedChanges(
dashboardChanges: Partial<DashboardContainerInput>,
reactEmbeddableChanges: UnsavedPanelState
) {
const { dashboardBackup } = pluginServices.getServices();
const dashboardStateToBackup = omit(dashboardChanges, keysToOmitFromSessionStorage);
dashboardBackup.setState(
getDashboardBackupService().setState(
this.getDashboardSavedObjectId(),
{
...dashboardStateToBackup,

View file

@ -10,6 +10,7 @@
import React from 'react';
import {
EUI_MODAL_CANCEL_BUTTON,
EuiButton,
EuiButtonEmpty,
EuiFocusTrap,
@ -19,12 +20,11 @@ import {
EuiModalHeaderTitle,
EuiOutsideClickDetector,
EuiText,
EUI_MODAL_CANCEL_BUTTON,
} from '@elastic/eui';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { pluginServices } from '../services/plugin_services';
import { coreServices } from '../services/kibana_services';
import { createConfirmStrings, resetConfirmStrings } from './_dashboard_listing_strings';
export type DiscardOrKeepSelection = 'cancel' | 'discard' | 'keep';
@ -33,21 +33,19 @@ export const confirmDiscardUnsavedChanges = (
discardCallback: () => void,
viewMode: ViewMode = ViewMode.EDIT // we want to show the danger modal on the listing page
) => {
const {
overlays: { openConfirm },
} = pluginServices.getServices();
openConfirm(resetConfirmStrings.getResetSubtitle(viewMode), {
confirmButtonText: resetConfirmStrings.getResetConfirmButtonText(),
buttonColor: viewMode === ViewMode.EDIT ? 'danger' : 'primary',
maxWidth: 500,
defaultFocusedButton: EUI_MODAL_CANCEL_BUTTON,
title: resetConfirmStrings.getResetTitle(),
}).then((isConfirmed) => {
if (isConfirmed) {
discardCallback();
}
});
coreServices.overlays
.openConfirm(resetConfirmStrings.getResetSubtitle(viewMode), {
confirmButtonText: resetConfirmStrings.getResetConfirmButtonText(),
buttonColor: viewMode === ViewMode.EDIT ? 'danger' : 'primary',
maxWidth: 500,
defaultFocusedButton: EUI_MODAL_CANCEL_BUTTON,
title: resetConfirmStrings.getResetTitle(),
})
.then((isConfirmed) => {
if (isConfirmed) {
discardCallback();
}
});
};
export const confirmCreateWithUnsaved = (
@ -57,13 +55,7 @@ export const confirmCreateWithUnsaved = (
const titleId = 'confirmDiscardOrKeepTitle';
const descriptionId = 'confirmDiscardOrKeepDescription';
const {
analytics,
settings: { i18n, theme },
overlays: { openModal },
} = pluginServices.getServices();
const session = openModal(
const session = coreServices.overlays.openModal(
toMountPoint(
<EuiFocusTrap
clickOutsideDisables={true}
@ -120,7 +112,7 @@ export const confirmCreateWithUnsaved = (
</div>
</EuiOutsideClickDetector>
</EuiFocusTrap>,
{ analytics, i18n, theme }
{ analytics: coreServices.analytics, i18n: coreServices.i18n, theme: coreServices.theme }
),
{
'data-test-subj': 'dashboardCreateConfirmModal',

View file

@ -7,22 +7,22 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ComponentType, ReactWrapper, mount } from 'enzyme';
import React, { PropsWithChildren } from 'react';
import { act } from 'react-dom/test-utils';
import { mount, ReactWrapper, ComponentType } from 'enzyme';
import { I18nProvider } from '@kbn/i18n-react';
import { pluginServices } from '../services/plugin_services';
import { DashboardListing } from './dashboard_listing';
/**
* Mock Table List view. This dashboard component is a wrapper around the shared UX table List view. We
* need to ensure we're passing down the correct props, but the table list view itself doesn't need to be rendered
* in our tests because it is covered in its package.
*/
import { TableListView } from '@kbn/content-management-table-list-view';
import { DashboardListing } from './dashboard_listing';
import { DashboardListingProps } from './types';
// import { TableListViewKibanaProvider } from '@kbn/content-management-table-list-view';
import { coreServices } from '../services/kibana_services';
jest.mock('@kbn/content-management-table-list-view-table', () => {
const originalModule = jest.requireActual('@kbn/content-management-table-list-view-table');
return {
@ -65,7 +65,7 @@ function mountWith({ props: incomingProps }: { props?: Partial<DashboardListingP
}
test('initial filter is passed through', async () => {
pluginServices.getServices().dashboardCapabilities.showWriteControls = false;
(coreServices.application.capabilities as any).dashboard.showWriteControls = false;
let component: ReactWrapper;
@ -80,7 +80,7 @@ test('initial filter is passed through', async () => {
});
test('when showWriteControls is true, table list view is passed editing functions', async () => {
pluginServices.getServices().dashboardCapabilities.showWriteControls = true;
(coreServices.application.capabilities as any).dashboard.showWriteControls = true;
let component: ReactWrapper;
@ -99,7 +99,7 @@ test('when showWriteControls is true, table list view is passed editing function
});
test('when showWriteControls is false, table list view is not passed editing functions', async () => {
pluginServices.getServices().dashboardCapabilities.showWriteControls = false;
(coreServices.application.capabilities as any).dashboard.showWriteControls = false;
let component: ReactWrapper;

View file

@ -7,26 +7,23 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { FormattedRelative, I18nProvider } from '@kbn/i18n-react';
import React, { useMemo } from 'react';
import { FavoritesClient } from '@kbn/content-management-favorites-public';
import { TableListView } from '@kbn/content-management-table-list-view';
import {
type TableListViewKibanaDependencies,
TableListViewKibanaProvider,
} from '@kbn/content-management-table-list-view-table';
import { TableListViewKibanaProvider } from '@kbn/content-management-table-list-view-table';
import { FormattedRelative, I18nProvider } from '@kbn/i18n-react';
import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
import { pluginServices } from '../services/plugin_services';
import { DASHBOARD_APP_ID, DASHBOARD_CONTENT_ID } from '../dashboard_constants';
import {
coreServices,
savedObjectsTaggingService,
usageCollectionService,
} from '../services/kibana_services';
import { DashboardUnsavedListing } from './dashboard_unsaved_listing';
import { useDashboardListingTable } from './hooks/use_dashboard_listing_table';
import {
DashboardListingProps,
DashboardSavedObjectUserContent,
TableListViewApplicationService,
} from './types';
import { DashboardListingProps, DashboardSavedObjectUserContent } from './types';
export const DashboardListing = ({
children,
@ -35,59 +32,38 @@ export const DashboardListing = ({
getDashboardUrl,
useSessionStorageIntegration,
}: DashboardListingProps) => {
const {
analytics,
application,
notifications,
overlays,
http,
i18n,
chrome: { theme },
savedObjectsTagging,
coreContext: { executionContext },
userProfile,
dashboardContentInsights: { contentInsightsClient },
dashboardFavorites,
} = pluginServices.getServices();
useExecutionContext(executionContext, {
useExecutionContext(coreServices.executionContext, {
type: 'application',
page: 'list',
});
const { unsavedDashboardIds, refreshUnsavedDashboards, tableListViewTableProps } =
useDashboardListingTable({
goToDashboard,
getDashboardUrl,
useSessionStorageIntegration,
initialFilter,
});
const {
unsavedDashboardIds,
refreshUnsavedDashboards,
tableListViewTableProps,
contentInsightsClient,
} = useDashboardListingTable({
goToDashboard,
getDashboardUrl,
useSessionStorageIntegration,
initialFilter,
});
const savedObjectsTaggingFakePlugin = useMemo(() => {
return savedObjectsTagging.hasApi // TODO: clean up this logic once https://github.com/elastic/kibana/issues/140433 is resolved
? ({
ui: savedObjectsTagging,
} as TableListViewKibanaDependencies['savedObjectsTagging'])
: undefined;
}, [savedObjectsTagging]);
const dashboardFavoritesClient = useMemo(() => {
return new FavoritesClient(DASHBOARD_APP_ID, DASHBOARD_CONTENT_ID, {
http: coreServices.http,
usageCollection: usageCollectionService,
});
}, []);
return (
<I18nProvider>
<TableListViewKibanaProvider
{...{
core: {
analytics,
application: application as TableListViewApplicationService,
notifications,
overlays,
http,
i18n,
theme,
userProfile,
},
savedObjectsTagging: savedObjectsTaggingFakePlugin,
core: coreServices,
savedObjectsTagging: savedObjectsTaggingService?.getTaggingApi(),
FormattedRelative,
favorites: dashboardFavorites,
favorites: dashboardFavoritesClient,
contentInsightsClient,
}}
>

View file

@ -7,18 +7,18 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ComponentType, ReactWrapper, mount } from 'enzyme';
import React from 'react';
import { act } from 'react-dom/test-utils';
import { mount, ReactWrapper, ComponentType } from 'enzyme';
import { I18nProvider } from '@kbn/i18n-react';
import { coreServices } from '../services/kibana_services';
import { confirmDiscardUnsavedChanges } from './confirm_overlays';
import {
DashboardListingEmptyPrompt,
DashboardListingEmptyPromptProps,
} from './dashboard_listing_empty_prompt';
import { pluginServices } from '../services/plugin_services';
import { confirmDiscardUnsavedChanges } from './confirm_overlays';
jest.mock('./confirm_overlays', () => {
const originalModule = jest.requireActual('./confirm_overlays');
@ -56,7 +56,7 @@ function mountWith({
}
test('renders readonly empty prompt when showWriteControls is off', async () => {
pluginServices.getServices().dashboardCapabilities.showWriteControls = false;
(coreServices.application.capabilities as any).dashboard.showWriteControls = false;
let component: ReactWrapper;
await act(async () => {
@ -68,7 +68,7 @@ test('renders readonly empty prompt when showWriteControls is off', async () =>
});
test('renders empty prompt with link when showWriteControls is on', async () => {
pluginServices.getServices().dashboardCapabilities.showWriteControls = true;
(coreServices.application.capabilities as any).dashboard.showWriteControls = true;
let component: ReactWrapper;
await act(async () => {
@ -80,7 +80,7 @@ test('renders empty prompt with link when showWriteControls is on', async () =>
});
test('renders disabled action button when disableCreateDashboardButton is true', async () => {
pluginServices.getServices().dashboardCapabilities.showWriteControls = true;
(coreServices.application.capabilities as any).dashboard.showWriteControls = true;
let component: ReactWrapper;
await act(async () => {
@ -95,7 +95,7 @@ test('renders disabled action button when disableCreateDashboardButton is true',
});
test('renders continue button when no dashboards exist but one is in progress', async () => {
pluginServices.getServices().dashboardCapabilities.showWriteControls = true;
(coreServices.application.capabilities as any).dashboard.showWriteControls = true;
let component: ReactWrapper;
let props: DashboardListingEmptyPromptProps;
await act(async () => {
@ -114,7 +114,7 @@ test('renders continue button when no dashboards exist but one is in progress',
});
test('renders discard button when no dashboards exist but one is in progress', async () => {
pluginServices.getServices().dashboardCapabilities.showWriteControls = true;
(coreServices.application.capabilities as any).dashboard.showWriteControls = true;
let component: ReactWrapper;
await act(async () => {
({ component } = mountWith({

View file

@ -8,24 +8,28 @@
*/
import {
EuiLink,
EuiButton,
EuiFlexItem,
EuiFlexGroup,
EuiButtonEmpty,
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiLink,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { useCallback, useMemo } from 'react';
import {
noItemsStrings,
getNewDashboardTitle,
DASHBOARD_PANELS_UNSAVED_ID,
getDashboardBackupService,
} from '../services/dashboard_backup_service';
import { coreServices } from '../services/kibana_services';
import { getDashboardCapabilities } from '../utils/get_dashboard_capabilities';
import {
dashboardUnsavedListingStrings,
getNewDashboardTitle,
noItemsStrings,
} from './_dashboard_listing_strings';
import { pluginServices } from '../services/plugin_services';
import { confirmDiscardUnsavedChanges } from './confirm_overlays';
import { DASHBOARD_PANELS_UNSAVED_ID } from '../services/dashboard_backup/dashboard_backup_service';
import { DashboardListingProps } from './types';
export interface DashboardListingEmptyPromptProps {
@ -45,12 +49,6 @@ export const DashboardListingEmptyPrompt = ({
createItem,
disableCreateDashboardButton,
}: DashboardListingEmptyPromptProps) => {
const {
application,
dashboardBackup,
dashboardCapabilities: { showWriteControls },
} = pluginServices.getServices();
const isEditingFirstDashboard = useMemo(
() => useSessionStorageIntegration && unsavedDashboardIds.length === 1,
[unsavedDashboardIds.length, useSessionStorageIntegration]
@ -78,8 +76,9 @@ export const DashboardListingEmptyPrompt = ({
color="danger"
onClick={() =>
confirmDiscardUnsavedChanges(() => {
dashboardBackup.clearState(DASHBOARD_PANELS_UNSAVED_ID);
setUnsavedDashboardIds(dashboardBackup.getDashboardIdsWithUnsavedChanges());
const dashboardBackupService = getDashboardBackupService();
dashboardBackupService.clearState(DASHBOARD_PANELS_UNSAVED_ID);
setUnsavedDashboardIds(dashboardBackupService.getDashboardIdsWithUnsavedChanges());
})
}
data-test-subj="discardDashboardPromptButton"
@ -106,12 +105,11 @@ export const DashboardListingEmptyPrompt = ({
isEditingFirstDashboard,
createItem,
disableCreateDashboardButton,
dashboardBackup,
goToDashboard,
setUnsavedDashboardIds,
]);
if (!showWriteControls) {
if (!getDashboardCapabilities().showWriteControls) {
return (
<EuiEmptyPrompt
iconType="glasses"
@ -147,7 +145,7 @@ export const DashboardListingEmptyPrompt = ({
sampleDataInstallLink: (
<EuiLink
onClick={() =>
application.navigateToApp('home', {
coreServices.application.navigateToApp('home', {
path: '#/tutorial_directory/sampleData',
})
}

View file

@ -7,26 +7,19 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { FormattedRelative, I18nProvider } from '@kbn/i18n-react';
import React, { useMemo } from 'react';
import React from 'react';
import {
type TableListViewKibanaDependencies,
TableListViewKibanaProvider,
TableListViewTable,
} from '@kbn/content-management-table-list-view-table';
import { FormattedRelative, I18nProvider } from '@kbn/i18n-react';
import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
import { pluginServices } from '../services/plugin_services';
import { coreServices, savedObjectsTaggingService } from '../services/kibana_services';
import { DashboardUnsavedListing } from './dashboard_unsaved_listing';
import { useDashboardListingTable } from './hooks/use_dashboard_listing_table';
import {
DashboardListingProps,
DashboardSavedObjectUserContent,
TableListViewApplicationService,
} from './types';
import { DashboardListingProps, DashboardSavedObjectUserContent } from './types';
export const DashboardListingTable = ({
disableCreateDashboardButton,
@ -37,21 +30,7 @@ export const DashboardListingTable = ({
urlStateEnabled,
showCreateDashboardButton = true,
}: DashboardListingProps) => {
const {
analytics,
application,
notifications,
overlays,
http,
i18n,
savedObjectsTagging,
coreContext: { executionContext },
chrome: { theme },
userProfile,
dashboardContentInsights: { contentInsightsClient },
} = pluginServices.getServices();
useExecutionContext(executionContext, {
useExecutionContext(coreServices.executionContext, {
type: 'application',
page: 'list',
});
@ -60,6 +39,7 @@ export const DashboardListingTable = ({
unsavedDashboardIds,
refreshUnsavedDashboards,
tableListViewTableProps: { title: tableCaption, ...tableListViewTable },
contentInsightsClient,
} = useDashboardListingTable({
disableCreateDashboardButton,
goToDashboard,
@ -70,35 +50,11 @@ export const DashboardListingTable = ({
showCreateDashboardButton,
});
const savedObjectsTaggingFakePlugin = useMemo(
() =>
savedObjectsTagging.hasApi // TODO: clean up this logic once https://github.com/elastic/kibana/issues/140433 is resolved
? ({
ui: savedObjectsTagging,
} as TableListViewKibanaDependencies['savedObjectsTagging'])
: undefined,
[savedObjectsTagging]
);
const core = useMemo(
() => ({
analytics,
application: application as TableListViewApplicationService,
notifications,
overlays,
http,
i18n,
theme,
userProfile,
}),
[application, notifications, overlays, http, analytics, i18n, theme, userProfile]
);
return (
<I18nProvider>
<TableListViewKibanaProvider
core={core}
savedObjectsTagging={savedObjectsTaggingFakePlugin}
core={coreServices}
savedObjectsTagging={savedObjectsTaggingService?.getTaggingApi()}
FormattedRelative={FormattedRelative}
contentInsightsClient={contentInsightsClient}
>

View file

@ -7,17 +7,21 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ComponentType, mount } from 'enzyme';
import React from 'react';
import { mount, ComponentType } from 'enzyme';
import { findTestSubject } from '@elastic/eui/lib/test';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { I18nProvider } from '@kbn/i18n-react';
import { waitFor } from '@testing-library/react';
import { findTestSubject } from '@elastic/eui/lib/test';
import { pluginServices } from '../services/plugin_services';
import {
DASHBOARD_PANELS_UNSAVED_ID,
getDashboardBackupService,
} from '../services/dashboard_backup_service';
import { getDashboardContentManagementService } from '../services/dashboard_content_management_service';
import { coreServices } from '../services/kibana_services';
import { DashboardUnsavedListing, DashboardUnsavedListingProps } from './dashboard_unsaved_listing';
import { DASHBOARD_PANELS_UNSAVED_ID } from '../services/dashboard_backup/dashboard_backup_service';
import { ViewMode } from '@kbn/embeddable-plugin/public';
const makeDefaultProps = (): DashboardUnsavedListingProps => ({
goToDashboard: jest.fn(),
@ -39,12 +43,13 @@ function mountWith({ props: incomingProps }: { props?: Partial<DashboardUnsavedL
}
describe('Unsaved listing', () => {
const dashboardBackupService = getDashboardBackupService();
const dashboardContentManagementService = getDashboardContentManagementService();
it('Gets information for each unsaved dashboard', async () => {
mountWith({});
await waitFor(() => {
expect(
pluginServices.getServices().dashboardContentManagement.findDashboards.findByIds
).toHaveBeenCalledTimes(1);
expect(dashboardContentManagementService.findDashboards.findByIds).toHaveBeenCalledTimes(1);
});
});
@ -53,9 +58,9 @@ describe('Unsaved listing', () => {
props.unsavedDashboardIds = ['dashboardUnsavedOne', DASHBOARD_PANELS_UNSAVED_ID];
mountWith({ props });
await waitFor(() => {
expect(
pluginServices.getServices().dashboardContentManagement.findDashboards.findByIds
).toHaveBeenCalledWith(['dashboardUnsavedOne']);
expect(dashboardContentManagementService.findDashboards.findByIds).toHaveBeenCalledWith([
'dashboardUnsavedOne',
]);
});
});
@ -94,17 +99,13 @@ describe('Unsaved listing', () => {
getDiscardButton().simulate('click');
waitFor(() => {
component.update();
expect(pluginServices.getServices().overlays.openConfirm).toHaveBeenCalled();
expect(pluginServices.getServices().dashboardBackup.clearState).toHaveBeenCalledWith(
'dashboardUnsavedOne'
);
expect(coreServices.overlays.openConfirm).toHaveBeenCalled();
expect(dashboardBackupService.clearState).toHaveBeenCalledWith('dashboardUnsavedOne');
});
});
it('removes unsaved changes from any dashboard which errors on fetch', async () => {
(
pluginServices.getServices().dashboardContentManagement.findDashboards.findByIds as jest.Mock
).mockResolvedValue([
(dashboardContentManagementService.findDashboards.findByIds as jest.Mock).mockResolvedValue([
{
id: 'failCase1',
status: 'error',
@ -129,17 +130,11 @@ describe('Unsaved listing', () => {
const { component } = mountWith({ props });
waitFor(() => {
component.update();
expect(pluginServices.getServices().dashboardBackup.clearState).toHaveBeenCalledWith(
'failCase1'
);
expect(pluginServices.getServices().dashboardBackup.clearState).toHaveBeenCalledWith(
'failCase2'
);
expect(dashboardBackupService.clearState).toHaveBeenCalledWith('failCase1');
expect(dashboardBackupService.clearState).toHaveBeenCalledWith('failCase2');
// clearing panels from dashboard with errors should cause getDashboardIdsWithUnsavedChanges to be called again.
expect(
pluginServices.getServices().dashboardBackup.getDashboardIdsWithUnsavedChanges
).toHaveBeenCalledTimes(2);
expect(dashboardBackupService.getDashboardIdsWithUnsavedChanges).toHaveBeenCalledTimes(2);
});
});
});

View file

@ -16,15 +16,18 @@ import {
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { pluginServices } from '../services/plugin_services';
import { confirmDiscardUnsavedChanges } from './confirm_overlays';
import { DashboardAttributes } from '../../common/content_management';
import {
DASHBOARD_PANELS_UNSAVED_ID,
getDashboardBackupService,
} from '../services/dashboard_backup_service';
import { getDashboardContentManagementService } from '../services/dashboard_content_management_service';
import { dashboardUnsavedListingStrings, getNewDashboardTitle } from './_dashboard_listing_strings';
import { DASHBOARD_PANELS_UNSAVED_ID } from '../services/dashboard_backup/dashboard_backup_service';
import { confirmDiscardUnsavedChanges } from './confirm_overlays';
const DashboardUnsavedItem = ({
id,
@ -116,12 +119,8 @@ export const DashboardUnsavedListing = ({
unsavedDashboardIds,
refreshUnsavedDashboards,
}: DashboardUnsavedListingProps) => {
const {
dashboardBackup,
dashboardContentManagement: { findDashboards },
} = pluginServices.getServices();
const [items, setItems] = useState<UnsavedItemMap>({});
const dashboardBackupService = useMemo(() => getDashboardBackupService(), []);
const onOpen = useCallback(
(id?: string) => {
@ -133,11 +132,11 @@ export const DashboardUnsavedListing = ({
const onDiscard = useCallback(
(id?: string) => {
confirmDiscardUnsavedChanges(() => {
dashboardBackup.clearState(id);
dashboardBackupService.clearState(id);
refreshUnsavedDashboards();
});
},
[refreshUnsavedDashboards, dashboardBackup]
[dashboardBackupService, refreshUnsavedDashboards]
);
useEffect(() => {
@ -148,37 +147,39 @@ export const DashboardUnsavedListing = ({
const existingDashboardsWithUnsavedChanges = unsavedDashboardIds.filter(
(id) => id !== DASHBOARD_PANELS_UNSAVED_ID
);
findDashboards.findByIds(existingDashboardsWithUnsavedChanges).then((results) => {
const dashboardMap = {};
if (canceled) {
return;
}
let hasError = false;
const newItems = results.reduce((map, result) => {
if (result.status === 'error') {
hasError = true;
if (result.error.statusCode === 404) {
// Save object not found error
dashboardBackup.clearState(result.id);
}
return map;
getDashboardContentManagementService()
.findDashboards.findByIds(existingDashboardsWithUnsavedChanges)
.then((results) => {
const dashboardMap = {};
if (canceled) {
return;
}
return {
...map,
[result.id || DASHBOARD_PANELS_UNSAVED_ID]: result.attributes,
};
}, dashboardMap);
if (hasError) {
refreshUnsavedDashboards();
return;
}
setItems(newItems);
});
let hasError = false;
const newItems = results.reduce((map, result) => {
if (result.status === 'error') {
hasError = true;
if (result.error.statusCode === 404) {
// Save object not found error
dashboardBackupService.clearState(result.id);
}
return map;
}
return {
...map,
[result.id || DASHBOARD_PANELS_UNSAVED_ID]: result.attributes,
};
}, dashboardMap);
if (hasError) {
refreshUnsavedDashboards();
return;
}
setItems(newItems);
});
return () => {
canceled = true;
};
}, [refreshUnsavedDashboards, dashboardBackup, unsavedDashboardIds, findDashboards]);
}, [dashboardBackupService, refreshUnsavedDashboards, unsavedDashboardIds]);
return unsavedDashboardIds.length === 0 ? null : (
<>

View file

@ -9,10 +9,13 @@
import { act, renderHook } from '@testing-library/react-hooks';
import { useDashboardListingTable } from './use_dashboard_listing_table';
import { pluginServices } from '../../services/plugin_services';
import { getDashboardBackupService } from '../../services/dashboard_backup_service';
import { getDashboardContentManagementService } from '../../services/dashboard_content_management_service';
import { coreServices } from '../../services/kibana_services';
import { confirmCreateWithUnsaved } from '../confirm_overlays';
import { DashboardSavedObjectUserContent } from '../types';
import { useDashboardListingTable } from './use_dashboard_listing_table';
const clearStateMock = jest.fn();
const getDashboardUrl = jest.fn();
const goToDashboard = jest.fn();
@ -26,7 +29,6 @@ const getUiSettingsMock = jest.fn().mockImplementation((key) => {
}
return null;
});
const getPluginServices = pluginServices.getServices();
jest.mock('@kbn/ebt-tools', () => ({
reportPerformanceMetricEvent: jest.fn(),
@ -45,20 +47,20 @@ jest.mock('../_dashboard_listing_strings', () => ({
}));
describe('useDashboardListingTable', () => {
const dashboardBackupService = getDashboardBackupService();
const dashboardContentManagementService = getDashboardContentManagementService();
beforeEach(() => {
jest.clearAllMocks();
getPluginServices.dashboardBackup.dashboardHasUnsavedEdits = jest.fn().mockReturnValue(true);
dashboardBackupService.dashboardHasUnsavedEdits = jest.fn().mockReturnValue(true);
getPluginServices.dashboardBackup.getDashboardIdsWithUnsavedChanges = jest
.fn()
.mockReturnValue([]);
dashboardBackupService.getDashboardIdsWithUnsavedChanges = jest.fn().mockReturnValue([]);
getPluginServices.dashboardBackup.clearState = clearStateMock;
getPluginServices.dashboardCapabilities.showWriteControls = true;
getPluginServices.dashboardContentManagement.deleteDashboards = deleteDashboards;
getPluginServices.settings.uiSettings.get = getUiSettingsMock;
getPluginServices.notifications.toasts.addError = jest.fn();
dashboardBackupService.clearState = clearStateMock;
dashboardContentManagementService.deleteDashboards = deleteDashboards;
coreServices.uiSettings.get = getUiSettingsMock;
coreServices.notifications.toasts.addError = jest.fn();
});
test('should return the correct initial hasInitialFetchReturned state', () => {
@ -232,8 +234,12 @@ describe('useDashboardListingTable', () => {
});
test('createItem should be undefined when showWriteControls equals false', () => {
getPluginServices.dashboardCapabilities.showWriteControls = false;
coreServices.application.capabilities = {
...coreServices.application.capabilities,
dashboard: {
showWriteControls: false,
},
};
const { result } = renderHook(() =>
useDashboardListingTable({
getDashboardUrl,
@ -245,7 +251,8 @@ describe('useDashboardListingTable', () => {
});
test('deleteItems should be undefined when showWriteControls equals false', () => {
getPluginServices.dashboardCapabilities.showWriteControls = false;
(coreServices.application.capabilities as any).dashboard.showWriteControls = false;
const { result } = renderHook(() =>
useDashboardListingTable({
getDashboardUrl,
@ -257,7 +264,8 @@ describe('useDashboardListingTable', () => {
});
test('editItem should be undefined when showWriteControls equals false', () => {
getPluginServices.dashboardCapabilities.showWriteControls = false;
(coreServices.application.capabilities as any).dashboard.showWriteControls = false;
const { result } = renderHook(() =>
useDashboardListingTable({
getDashboardUrl,

View file

@ -7,29 +7,34 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, { useCallback, useState, useMemo } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
import type { SavedObjectsFindOptionsReference } from '@kbn/core/public';
import { OpenContentEditorParams } from '@kbn/content-management-content-editor';
import { ContentInsightsClient } from '@kbn/content-management-content-insights-public';
import { TableListViewTableProps } from '@kbn/content-management-table-list-view-table';
import type { SavedObjectsFindOptionsReference } from '@kbn/core/public';
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { DashboardContainerInput } from '../../../common';
import { DashboardItem } from '../../../common/content_management';
import {
DASHBOARD_CONTENT_ID,
SAVED_OBJECT_DELETE_TIME,
SAVED_OBJECT_LOADED_TIME,
} from '../../dashboard_constants';
import { getDashboardBackupService } from '../../services/dashboard_backup_service';
import { getDashboardContentManagementService } from '../../services/dashboard_content_management_service';
import { getDashboardRecentlyAccessedService } from '../../services/dashboard_recently_accessed_service';
import { coreServices } from '../../services/kibana_services';
import { getDashboardCapabilities } from '../../utils/get_dashboard_capabilities';
import {
dashboardListingErrorStrings,
dashboardListingTableStrings,
} from '../_dashboard_listing_strings';
import { DashboardContainerInput } from '../../../common';
import { DashboardSavedObjectUserContent } from '../types';
import { confirmCreateWithUnsaved } from '../confirm_overlays';
import { pluginServices } from '../../services/plugin_services';
import { DashboardItem } from '../../../common/content_management';
import { DashboardListingEmptyPrompt } from '../dashboard_listing_empty_prompt';
import { DashboardSavedObjectUserContent } from '../types';
type GetDetailViewLink =
TableListViewTableProps<DashboardSavedObjectUserContent>['getDetailViewLink'];
@ -67,6 +72,7 @@ interface UseDashboardListingTableReturnType {
refreshUnsavedDashboards: () => void;
tableListViewTableProps: DashboardListingViewTableProps;
unsavedDashboardIds: string[];
contentInsightsClient: ContentInsightsClient;
}
export const useDashboardListingTable = ({
@ -90,51 +96,44 @@ export const useDashboardListingTable = ({
useSessionStorageIntegration?: boolean;
showCreateDashboardButton?: boolean;
}): UseDashboardListingTableReturnType => {
const {
dashboardBackup,
dashboardCapabilities: { showWriteControls },
settings: { uiSettings },
dashboardContentManagement: {
findDashboards,
deleteDashboards,
updateDashboardMeta,
checkForDuplicateDashboardTitle,
},
notifications: { toasts },
dashboardRecentlyAccessed,
} = pluginServices.getServices();
const { getEntityName, getTableListTitle, getEntityNamePlural } = dashboardListingTableStrings;
const title = getTableListTitle();
const entityName = getEntityName();
const entityNamePlural = getEntityNamePlural();
const [pageDataTestSubject, setPageDataTestSubject] = useState<string>();
const [hasInitialFetchReturned, setHasInitialFetchReturned] = useState(false);
const [unsavedDashboardIds, setUnsavedDashboardIds] = useState<string[]>(
dashboardBackup.getDashboardIdsWithUnsavedChanges()
const dashboardBackupService = useMemo(() => getDashboardBackupService(), []);
const dashboardContentManagementService = useMemo(
() => getDashboardContentManagementService(),
[]
);
const listingLimit = uiSettings.get(SAVED_OBJECTS_LIMIT_SETTING);
const initialPageSize = uiSettings.get(SAVED_OBJECTS_PER_PAGE_SETTING);
const [unsavedDashboardIds, setUnsavedDashboardIds] = useState<string[]>(
dashboardBackupService.getDashboardIdsWithUnsavedChanges()
);
const listingLimit = coreServices.uiSettings.get(SAVED_OBJECTS_LIMIT_SETTING);
const initialPageSize = coreServices.uiSettings.get(SAVED_OBJECTS_PER_PAGE_SETTING);
const createItem = useCallback(() => {
if (useSessionStorageIntegration && dashboardBackup.dashboardHasUnsavedEdits()) {
if (useSessionStorageIntegration && dashboardBackupService.dashboardHasUnsavedEdits()) {
confirmCreateWithUnsaved(() => {
dashboardBackup.clearState();
dashboardBackupService.clearState();
goToDashboard();
}, goToDashboard);
return;
}
goToDashboard();
}, [dashboardBackup, goToDashboard, useSessionStorageIntegration]);
}, [dashboardBackupService, goToDashboard, useSessionStorageIntegration]);
const updateItemMeta = useCallback(
async (props: Pick<DashboardContainerInput, 'id' | 'title' | 'description' | 'tags'>) => {
await updateDashboardMeta(props);
await dashboardContentManagementService.updateDashboardMeta(props);
setUnsavedDashboardIds(dashboardBackup.getDashboardIdsWithUnsavedChanges());
setUnsavedDashboardIds(dashboardBackupService.getDashboardIdsWithUnsavedChanges());
},
[dashboardBackup, updateDashboardMeta]
[dashboardBackupService, dashboardContentManagementService]
);
const contentEditorValidators: OpenContentEditorParams['customValidators'] = useMemo(
@ -145,17 +144,19 @@ export const useDashboardListingTable = ({
fn: async (value: string, id: string) => {
if (id) {
try {
const [dashboard] = await findDashboards.findByIds([id]);
const [dashboard] =
await dashboardContentManagementService.findDashboards.findByIds([id]);
if (dashboard.status === 'error') {
return;
}
const validTitle = await checkForDuplicateDashboardTitle({
title: value,
copyOnSave: false,
lastSavedTitle: dashboard.attributes.title,
isTitleDuplicateConfirmed: false,
});
const validTitle =
await dashboardContentManagementService.checkForDuplicateDashboardTitle({
title: value,
copyOnSave: false,
lastSavedTitle: dashboard.attributes.title,
isTitleDuplicateConfirmed: false,
});
if (!validTitle) {
throw new Error(dashboardListingErrorStrings.getDuplicateTitleWarning(value));
@ -168,7 +169,7 @@ export const useDashboardListingTable = ({
},
],
}),
[checkForDuplicateDashboardTitle, findDashboards]
[dashboardContentManagementService]
);
const emptyPrompt = useMemo(
@ -204,7 +205,7 @@ export const useDashboardListingTable = ({
) => {
const searchStartTime = window.performance.now();
return findDashboards
return dashboardContentManagementService.findDashboards
.search({
search: searchTerm,
size: listingLimit,
@ -214,7 +215,7 @@ export const useDashboardListingTable = ({
.then(({ total, hits }) => {
const searchEndTime = window.performance.now();
const searchDuration = searchEndTime - searchStartTime;
reportPerformanceMetricEvent(pluginServices.getServices().analytics, {
reportPerformanceMetricEvent(coreServices.analytics, {
eventName: SAVED_OBJECT_LOADED_TIME,
duration: searchDuration,
meta: {
@ -227,7 +228,7 @@ export const useDashboardListingTable = ({
};
});
},
[findDashboards, listingLimit]
[listingLimit, dashboardContentManagementService]
);
const deleteItems = useCallback(
@ -235,15 +236,15 @@ export const useDashboardListingTable = ({
try {
const deleteStartTime = window.performance.now();
await deleteDashboards(
await dashboardContentManagementService.deleteDashboards(
dashboardsToDelete.map(({ id }) => {
dashboardBackup.clearState(id);
dashboardBackupService.clearState(id);
return id;
})
);
const deleteDuration = window.performance.now() - deleteStartTime;
reportPerformanceMetricEvent(pluginServices.getServices().analytics, {
reportPerformanceMetricEvent(coreServices.analytics, {
eventName: SAVED_OBJECT_DELETE_TIME,
duration: deleteDuration,
meta: {
@ -252,14 +253,14 @@ export const useDashboardListingTable = ({
},
});
} catch (error) {
toasts.addError(error, {
coreServices.notifications.toasts.addError(error, {
title: dashboardListingErrorStrings.getErrorDeletingDashboardToast(),
});
}
setUnsavedDashboardIds(dashboardBackup.getDashboardIdsWithUnsavedChanges());
setUnsavedDashboardIds(dashboardBackupService.getDashboardIdsWithUnsavedChanges());
},
[dashboardBackup, deleteDashboards, toasts]
[dashboardBackupService, dashboardContentManagementService]
);
const editItem = useCallback(
@ -278,8 +279,9 @@ export const useDashboardListingTable = ({
[getDashboardUrl]
);
const tableListViewTableProps: DashboardListingViewTableProps = useMemo(
() => ({
const tableListViewTableProps: DashboardListingViewTableProps = useMemo(() => {
const { showWriteControls } = getDashboardCapabilities();
return {
contentEditor: {
isReadonly: !showWriteControls,
onSave: updateItemMeta,
@ -303,36 +305,38 @@ export const useDashboardListingTable = ({
title,
urlStateEnabled,
createdByEnabled: true,
recentlyAccessed: dashboardRecentlyAccessed,
}),
[
contentEditorValidators,
createItem,
dashboardListingId,
deleteItems,
editItem,
emptyPrompt,
entityName,
entityNamePlural,
findItems,
getDetailViewLink,
headingId,
initialFilter,
initialPageSize,
listingLimit,
onFetchSuccess,
showCreateDashboardButton,
showWriteControls,
title,
updateItemMeta,
urlStateEnabled,
dashboardRecentlyAccessed,
]
);
recentlyAccessed: getDashboardRecentlyAccessedService(),
};
}, [
contentEditorValidators,
createItem,
dashboardListingId,
deleteItems,
editItem,
emptyPrompt,
entityName,
entityNamePlural,
findItems,
getDetailViewLink,
headingId,
initialFilter,
initialPageSize,
listingLimit,
onFetchSuccess,
showCreateDashboardButton,
title,
updateItemMeta,
urlStateEnabled,
]);
const refreshUnsavedDashboards = useCallback(
() => setUnsavedDashboardIds(dashboardBackup.getDashboardIdsWithUnsavedChanges()),
[dashboardBackup]
() => setUnsavedDashboardIds(getDashboardBackupService().getDashboardIdsWithUnsavedChanges()),
[]
);
const contentInsightsClient = useMemo(
() => new ContentInsightsClient({ http: coreServices.http }, { domainId: 'dashboard' }),
[]
);
return {
@ -341,5 +345,6 @@ export const useDashboardListingTable = ({
refreshUnsavedDashboards,
tableListViewTableProps,
unsavedDashboardIds,
contentInsightsClient,
};
};

View file

@ -10,7 +10,6 @@
import type { PropsWithChildren } from 'react';
import type { UserContentCommonSchema } from '@kbn/content-management-table-list-view-common';
import type { ViewMode } from '@kbn/embeddable-plugin/public';
import type { DashboardApplicationService } from '../services/application/types';
export type DashboardListingProps = PropsWithChildren<{
disableCreateDashboardButton?: boolean;
@ -22,12 +21,6 @@ export type DashboardListingProps = PropsWithChildren<{
showCreateDashboardButton?: boolean;
}>;
// because the type of `application.capabilities.advancedSettings` is so generic, the provider
// requiring the `save` key to be part of it is causing type issues - so, creating a custom type
export type TableListViewApplicationService = DashboardApplicationService & {
capabilities: { advancedSettings: { save: boolean } };
};
export interface DashboardSavedObjectUserContent extends UserContentCommonSchema {
managed?: boolean;
attributes: {

View file

@ -8,13 +8,13 @@
*/
import React, { Suspense } from 'react';
import { servicesReady } from '../plugin';
import { DashboardTopNavProps } from './dashboard_top_nav_with_context';
import { untilPluginStartServicesReady } from '../services/kibana_services';
const LazyDashboardTopNav = React.lazy(() =>
(async () => {
const modulePromise = import('./dashboard_top_nav_with_context');
const [module] = await Promise.all([modulePromise, servicesReady]);
const [module] = await Promise.all([modulePromise, untilPluginStartServicesReady()]);
return {
default: module.DashboardTopNavWithContext,

View file

@ -12,10 +12,10 @@ import { render } from '@testing-library/react';
import { buildMockDashboard } from '../mocks';
import { InternalDashboardTopNav } from './internal_dashboard_top_nav';
import { setMockedPresentationUtilServices } from '@kbn/presentation-util-plugin/public/mocks';
import { pluginServices } from '../services/plugin_services';
import { TopNavMenuProps } from '@kbn/navigation-plugin/public';
import { DashboardContext } from '../dashboard_api/use_dashboard_api';
import { DashboardApi } from '../dashboard_api/types';
import { dataService, navigationService } from '../services/kibana_services';
describe('Internal dashboard top nav', () => {
const mockTopNav = (badges: TopNavMenuProps['badges'] | undefined[]) => {
@ -32,13 +32,10 @@ describe('Internal dashboard top nav', () => {
beforeEach(() => {
setMockedPresentationUtilServices();
pluginServices.getServices().data.query.filterManager.getFilters = jest
.fn()
.mockReturnValue([]);
dataService.query.filterManager.getFilters = jest.fn().mockReturnValue([]);
// topNavMenu is mocked as a jest.fn() so we want to mock it with a component
// @ts-ignore type issue with the mockTopNav for this test suite
pluginServices.getServices().navigation.TopNavMenu = ({ badges }: TopNavMenuProps) =>
mockTopNav(badges);
navigationService.ui.TopNavMenu = ({ badges }: TopNavMenuProps) => mockTopNav(badges);
});
it('should not render the managed badge by default', async () => {

View file

@ -7,48 +7,57 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import UseUnmount from 'react-use/lib/useUnmount';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import UseUnmount from 'react-use/lib/useUnmount';
import {
withSuspense,
LazyLabsFlyout,
getContextProvider as getPresentationUtilContextProvider,
} from '@kbn/presentation-util-plugin/public';
import { TopNavMenuBadgeProps, TopNavMenuProps } from '@kbn/navigation-plugin/public';
import {
EuiBadge,
EuiBreadcrumb,
EuiHorizontalRule,
EuiIcon,
EuiToolTipProps,
EuiPopover,
EuiBadge,
EuiLink,
EuiPopover,
EuiToolTipProps,
} from '@elastic/eui';
import { MountPoint } from '@kbn/core/public';
import { getManagedContentBadge } from '@kbn/managed-content-badge';
import { FormattedMessage } from '@kbn/i18n-react';
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import { Query } from '@kbn/es-query';
import { FormattedMessage } from '@kbn/i18n-react';
import { getManagedContentBadge } from '@kbn/managed-content-badge';
import { TopNavMenuBadgeProps, TopNavMenuProps } from '@kbn/navigation-plugin/public';
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import {
LazyLabsFlyout,
getContextProvider as getPresentationUtilContextProvider,
withSuspense,
} from '@kbn/presentation-util-plugin/public';
import { UI_SETTINGS } from '../../common';
import { useDashboardApi } from '../dashboard_api/use_dashboard_api';
import {
dashboardManagedBadge,
getDashboardBreadcrumb,
getDashboardTitle,
leaveConfirmStrings,
getDashboardBreadcrumb,
unsavedChangesBadgeStrings,
dashboardManagedBadge,
} from '../dashboard_app/_dashboard_app_strings';
import { UI_SETTINGS } from '../../common';
import { pluginServices } from '../services/plugin_services';
import { useDashboardMountContext } from '../dashboard_app/hooks/dashboard_mount_context';
import { DashboardEditingToolbar } from '../dashboard_app/top_nav/dashboard_editing_toolbar';
import { useDashboardMenuItems } from '../dashboard_app/top_nav/use_dashboard_menu_items';
import { DashboardEmbedSettings } from '../dashboard_app/types';
import { DashboardEditingToolbar } from '../dashboard_app/top_nav/dashboard_editing_toolbar';
import { useDashboardMountContext } from '../dashboard_app/hooks/dashboard_mount_context';
import { getFullEditPath, LEGACY_DASHBOARD_APP_ID } from '../dashboard_constants';
import './_dashboard_top_nav.scss';
import { DashboardRedirect } from '../dashboard_container/types';
import { SaveDashboardReturn } from '../services/dashboard_content_management/types';
import { useDashboardApi } from '../dashboard_api/use_dashboard_api';
import { LEGACY_DASHBOARD_APP_ID, getFullEditPath } from '../dashboard_constants';
import { openSettingsFlyout } from '../dashboard_container/embeddable/api';
import { DashboardRedirect } from '../dashboard_container/types';
import { SaveDashboardReturn } from '../services/dashboard_content_management_service/types';
import { getDashboardRecentlyAccessedService } from '../services/dashboard_recently_accessed_service';
import {
coreServices,
dataService,
embeddableService,
navigationService,
serverlessService,
} from '../services/kibana_services';
import { getDashboardCapabilities } from '../utils/get_dashboard_capabilities';
import './_dashboard_top_nav.scss';
export interface InternalDashboardTopNavProps {
customLeadingBreadCrumbs?: EuiBreadcrumb[];
@ -75,28 +84,7 @@ export function InternalDashboardTopNav({
const [isLabsShown, setIsLabsShown] = useState(false);
const dashboardTitleRef = useRef<HTMLHeadingElement>(null);
/**
* Unpack dashboard services
*/
const {
data: {
query: { filterManager },
},
chrome: {
setBreadcrumbs,
setIsVisible: setChromeVisibility,
getIsVisible$: getChromeIsVisible$,
recentlyAccessed: chromeRecentlyAccessed,
},
serverless,
settings: { uiSettings },
navigation: { TopNavMenu },
embeddable: { getStateTransfer },
initializerContext: { allowByValueEmbeddables },
dashboardCapabilities: { saveQuery: allowSaveQuery, showWriteControls },
dashboardRecentlyAccessed,
} = pluginServices.getServices();
const isLabsEnabled = uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI);
const isLabsEnabled = useMemo(() => coreServices.uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI), []);
const { setHeaderActionMenu, onAppLeave } = useDashboardMountContext();
const dashboardApi = useDashboardApi();
@ -144,36 +132,24 @@ export function InternalDashboardTopNav({
* Manage chrome visibility when dashboard is embedded.
*/
useEffect(() => {
if (!embedSettings) setChromeVisibility(viewMode !== 'print');
}, [embedSettings, setChromeVisibility, viewMode]);
if (!embedSettings) coreServices.chrome.setIsVisible(viewMode !== 'print');
}, [embedSettings, viewMode]);
/**
* populate recently accessed, and set is chrome visible.
*/
useEffect(() => {
const subscription = getChromeIsVisible$().subscribe((visible) => setIsChromeVisible(visible));
const subscription = coreServices.chrome
.getIsVisible$()
.subscribe((visible) => setIsChromeVisible(visible));
if (lastSavedId && title) {
chromeRecentlyAccessed.add(
getFullEditPath(lastSavedId, viewMode === 'edit'),
title,
lastSavedId
);
dashboardRecentlyAccessed.add(
getFullEditPath(lastSavedId, viewMode === 'edit'),
title,
lastSavedId
);
const fullEditPath = getFullEditPath(lastSavedId, viewMode === 'edit');
coreServices.chrome.recentlyAccessed.add(fullEditPath, title, lastSavedId);
getDashboardRecentlyAccessedService().add(fullEditPath, title, lastSavedId); // used to sort the listing table
}
return () => subscription.unsubscribe();
}, [
allowByValueEmbeddables,
chromeRecentlyAccessed,
getChromeIsVisible$,
lastSavedId,
viewMode,
title,
dashboardRecentlyAccessed,
]);
}, [lastSavedId, viewMode, title]);
/**
* Set breadcrumbs to dashboard title when dashboard's title or view mode changes
@ -198,17 +174,17 @@ export function InternalDashboardTopNav({
},
];
if (serverless?.setBreadcrumbs) {
if (serverlessService) {
// set serverless breadcrumbs if available,
// set only the dashboardTitleBreadcrumbs because the main breadcrumbs automatically come as part of the navigation config
serverless.setBreadcrumbs(dashboardTitleBreadcrumbs);
serverlessService.setBreadcrumbs(dashboardTitleBreadcrumbs);
} else {
/**
* non-serverless regular breadcrumbs
* Dashboard embedded in other plugins (e.g. SecuritySolution)
* will have custom leading breadcrumbs for back to their app.
**/
setBreadcrumbs(
coreServices.chrome.setBreadcrumbs(
customLeadingBreadCrumbs.concat([
{
text: getDashboardBreadcrumb(),
@ -221,22 +197,18 @@ export function InternalDashboardTopNav({
])
);
}
}, [
setBreadcrumbs,
redirectTo,
dashboardTitle,
dashboardApi,
viewMode,
serverless,
customLeadingBreadCrumbs,
]);
}, [redirectTo, dashboardTitle, dashboardApi, viewMode, customLeadingBreadCrumbs]);
/**
* Build app leave handler whenever hasUnsavedChanges changes
*/
useEffect(() => {
onAppLeave((actions) => {
if (viewMode === 'edit' && hasUnsavedChanges && !getStateTransfer().isTransferInProgress) {
if (
viewMode === 'edit' &&
hasUnsavedChanges &&
!embeddableService.getStateTransfer().isTransferInProgress
) {
return actions.confirm(
leaveConfirmStrings.getLeaveSubtitle(),
leaveConfirmStrings.getLeaveTitle()
@ -248,13 +220,13 @@ export function InternalDashboardTopNav({
// reset on app leave handler so leaving from the listing page doesn't trigger a confirmation
onAppLeave((actions) => actions.default());
};
}, [onAppLeave, getStateTransfer, hasUnsavedChanges, viewMode]);
}, [onAppLeave, hasUnsavedChanges, viewMode]);
const visibilityProps = useMemo(() => {
const shouldShowNavBarComponent = (forceShow: boolean): boolean =>
(forceShow || isChromeVisible) && !fullScreenMode;
const shouldShowFilterBar = (forceHide: boolean): boolean =>
!forceHide && (filterManager.getFilters().length > 0 || !fullScreenMode);
!forceHide && (dataService.query.filterManager.getFilters().length > 0 || !fullScreenMode);
const showTopNavMenu = shouldShowNavBarComponent(Boolean(embedSettings?.forceShowTopNavMenu));
const showQueryInput = Boolean(forceHideUnifiedSearch)
@ -275,14 +247,7 @@ export function InternalDashboardTopNav({
showQueryInput,
showDatePicker,
};
}, [
embedSettings,
filterManager,
forceHideUnifiedSearch,
fullScreenMode,
isChromeVisible,
viewMode,
]);
}, [embedSettings, forceHideUnifiedSearch, fullScreenMode, isChromeVisible, viewMode]);
const maybeRedirect = useCallback(
(result?: SaveDashboardReturn) => {
@ -338,6 +303,8 @@ export function InternalDashboardTopNav({
} as EuiToolTipProps,
});
}
const { showWriteControls } = getDashboardCapabilities();
if (showWriteControls && managed) {
const badgeProps = {
...getManagedContentBadge(dashboardManagedBadge.getBadgeAriaLabel()),
@ -390,7 +357,6 @@ export function InternalDashboardTopNav({
hasUnsavedChanges,
viewMode,
hasRunMigrations,
showWriteControls,
managed,
isPopoverOpen,
dashboardApi,
@ -405,7 +371,7 @@ export function InternalDashboardTopNav({
ref={dashboardTitleRef}
tabIndex={-1}
>{`${getDashboardBreadcrumb()} - ${dashboardTitle}`}</h1>
<TopNavMenu
<navigationService.ui.TopNavMenu
{...visibilityProps}
query={query as Query | undefined}
badges={badges}
@ -413,7 +379,9 @@ export function InternalDashboardTopNav({
useDefaultBehaviors={true}
savedQueryId={savedQueryId}
indexPatterns={allDataViews ?? []}
saveQueryMenuVisibility={allowSaveQuery ? 'allowed_by_app_privilege' : 'globally_managed'}
saveQueryMenuVisibility={
getDashboardCapabilities().saveQuery ? 'allowed_by_app_privilege' : 'globally_managed'
}
appName={LEGACY_DASHBOARD_APP_ID}
visible={viewMode !== 'print'}
setMenuMountPoint={

View file

@ -32,6 +32,8 @@ export { DashboardTopNav } from './dashboard_top_nav';
export { type DashboardAppLocator, cleanEmptyKeys } from './dashboard_app/locator/locator';
export { getDashboardLocatorParamsFromEmbeddable } from './dashboard_app/locator/get_dashboard_locator_params';
export { type SearchDashboardsResponse } from './services/dashboard_content_management_service/lib/find_dashboards';
export function plugin(initializerContext: PluginInitializerContext) {
return new DashboardPlugin(initializerContext);
}

View file

@ -15,12 +15,6 @@ import { BehaviorSubject } from 'rxjs';
import { DashboardContainerInput, DashboardPanelState } from '../common';
import { DashboardContainer } from './dashboard_container/embeddable/dashboard_container';
import { DashboardStart } from './plugin';
import { pluginServices } from './services/plugin_services';
export { setStubDashboardServices } from './services/mocks';
export const getMockedDashboardServices = () => {
return pluginServices.getServices();
};
export type Start = jest.Mocked<DashboardStart>;

View file

@ -7,75 +7,78 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { i18n } from '@kbn/i18n';
import { BehaviorSubject } from 'rxjs';
import { filter, map } from 'rxjs';
import { BehaviorSubject, filter, map } from 'rxjs';
import {
App,
Plugin,
AppUpdater,
ScopedHistory,
type CoreSetup,
type CoreStart,
AppMountParameters,
DEFAULT_APP_CATEGORIES,
PluginInitializerContext,
} from '@kbn/core/public';
import type {
ScreenshotModePluginSetup,
ScreenshotModePluginStart,
} from '@kbn/screenshot-mode-plugin/public';
import type {
UsageCollectionSetup,
UsageCollectionStart,
} from '@kbn/usage-collection-plugin/public';
import { APP_WRAPPER_CLASS } from '@kbn/core/public';
import { type UiActionsSetup, type UiActionsStart } from '@kbn/ui-actions-plugin/public';
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import type { HomePublicPluginSetup } from '@kbn/home-plugin/public';
import { replaceUrlHashQuery } from '@kbn/kibana-utils-plugin/common';
import { createKbnUrlTracker } from '@kbn/kibana-utils-plugin/public';
import type { VisualizationsStart } from '@kbn/visualizations-plugin/public';
import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public';
import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public';
import type { Start as InspectorStartContract } from '@kbn/inspector-plugin/public';
import type { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import type {
ContentManagementPublicSetup,
ContentManagementPublicStart,
} from '@kbn/content-management-plugin/public';
import { CustomBrandingStart } from '@kbn/core-custom-branding-browser';
import {
APP_WRAPPER_CLASS,
App,
AppMountParameters,
AppUpdater,
DEFAULT_APP_CATEGORIES,
Plugin,
PluginInitializerContext,
ScopedHistory,
type CoreSetup,
type CoreStart,
} from '@kbn/core/public';
import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { UrlForwardingSetup, UrlForwardingStart } from '@kbn/url-forwarding-plugin/public';
import type { SavedObjectTaggingOssPluginStart } from '@kbn/saved-objects-tagging-oss-plugin/public';
import type { ServerlessPluginStart } from '@kbn/serverless/public';
import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import type { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public/plugin';
import type { HomePublicPluginSetup } from '@kbn/home-plugin/public';
import { i18n } from '@kbn/i18n';
import type { Start as InspectorStartContract } from '@kbn/inspector-plugin/public';
import { replaceUrlHashQuery } from '@kbn/kibana-utils-plugin/common';
import { createKbnUrlTracker } from '@kbn/kibana-utils-plugin/public';
import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public';
import type { NoDataPagePluginStart } from '@kbn/no-data-page-plugin/public';
import type {
ObservabilityAIAssistantPublicSetup,
ObservabilityAIAssistantPublicStart,
} from '@kbn/observability-ai-assistant-plugin/public';
import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public';
import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
import type { SavedObjectTaggingOssPluginStart } from '@kbn/saved-objects-tagging-oss-plugin/public';
import type {
ScreenshotModePluginSetup,
ScreenshotModePluginStart,
} from '@kbn/screenshot-mode-plugin/public';
import type { ServerlessPluginStart } from '@kbn/serverless/public';
import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public';
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import { type UiActionsSetup, type UiActionsStart } from '@kbn/ui-actions-plugin/public';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import type { UrlForwardingSetup, UrlForwardingStart } from '@kbn/url-forwarding-plugin/public';
import type {
UsageCollectionSetup,
UsageCollectionStart,
} from '@kbn/usage-collection-plugin/public';
import type { VisualizationsStart } from '@kbn/visualizations-plugin/public';
import { CustomBrandingStart } from '@kbn/core-custom-branding-browser';
import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
import { DashboardContainerFactoryDefinition } from './dashboard_container/embeddable/dashboard_container_factory';
import { registerDashboardPanelPlacementSetting } from './dashboard_container/panel_placement';
import { CONTENT_ID, LATEST_VERSION } from '../common/content_management';
import {
type DashboardAppLocator,
DashboardAppLocatorDefinition,
type DashboardAppLocator,
} from './dashboard_app/locator/locator';
import { DashboardMountContextProps } from './dashboard_app/types';
import {
DASHBOARD_APP_ID,
LANDING_PAGE_PATH,
LEGACY_DASHBOARD_APP_ID,
SEARCH_SESSION_ID,
} from './dashboard_constants';
import { DashboardMountContextProps } from './dashboard_app/types';
import type { FindDashboardsService } from './services/dashboard_content_management/types';
import { CONTENT_ID, LATEST_VERSION } from '../common/content_management';
import { GetPanelPlacementSettings } from './dashboard_container/panel_placement';
import { DashboardContainerFactoryDefinition } from './dashboard_container/embeddable/dashboard_container_factory';
import {
GetPanelPlacementSettings,
registerDashboardPanelPlacementSetting,
} from './dashboard_container/panel_placement';
import type { FindDashboardsService } from './services/dashboard_content_management_service/types';
import { setKibanaServices, untilPluginStartServicesReady } from './services/kibana_services';
export interface DashboardFeatureFlagConfig {
allowByValueEmbeddables: boolean;
@ -99,6 +102,7 @@ export interface DashboardStartDependencies {
data: DataPublicPluginStart;
dataViewEditor: DataViewEditorStart;
embeddable: EmbeddableStart;
fieldFormats: FieldFormatsStart;
inspector: InspectorStartContract;
navigation: NavigationPublicPluginStart;
presentationUtil: PresentationUtilPluginStart;
@ -148,27 +152,9 @@ export class DashboardPlugin
private dashboardFeatureFlagConfig?: DashboardFeatureFlagConfig;
private locator?: DashboardAppLocator;
private async startDashboardKibanaServices(
coreStart: CoreStart,
startPlugins: DashboardStartDependencies,
initContext: PluginInitializerContext
) {
const { registry, pluginServices } = await import('./services/plugin_services');
pluginServices.setRegistry(registry.start({ coreStart, startPlugins, initContext }));
resolveServicesReady();
}
public setup(
core: CoreSetup<DashboardStartDependencies, DashboardStart>,
{
share,
embeddable,
home,
urlForwarding,
data,
contentManagement,
uiActions,
}: DashboardSetupDependencies
{ share, embeddable, home, urlForwarding, data, contentManagement }: DashboardSetupDependencies
): DashboardSetup {
this.dashboardFeatureFlagConfig =
this.initializerContext.config.get<DashboardFeatureFlagConfig>();
@ -183,11 +169,14 @@ export class DashboardPlugin
new DashboardAppLocatorDefinition({
useHashedUrl: core.uiSettings.get('state:storeInSessionStorage'),
getDashboardFilterFields: async (dashboardId: string) => {
const { pluginServices } = await import('./services/plugin_services');
const {
dashboardContentManagement: { loadDashboardState },
} = pluginServices.getServices();
return (await loadDashboardState({ id: dashboardId })).dashboardInput?.filters ?? [];
const [{ getDashboardContentManagementService }] = await Promise.all([
import('./services/dashboard_content_management_service'),
untilPluginStartServicesReady(),
]);
return (
(await getDashboardContentManagementService().loadDashboardState({ id: dashboardId }))
.dashboardInput?.filters ?? []
);
},
})
);
@ -262,6 +251,7 @@ export class DashboardPlugin
mount: async (params: AppMountParameters) => {
this.currentHistory = params.history;
params.element.classList.add(APP_WRAPPER_CLASS);
await untilPluginStartServicesReady();
const { mountApp } = await import('./dashboard_app/dashboard_router');
appMounted();
@ -340,25 +330,26 @@ export class DashboardPlugin
}
public start(core: CoreStart, plugins: DashboardStartDependencies): DashboardStart {
this.startDashboardKibanaServices(core, plugins, this.initializerContext).then(async () => {
const { buildAllDashboardActions } = await import('./dashboard_actions');
buildAllDashboardActions({
core,
plugins,
allowByValueEmbeddables: this.dashboardFeatureFlagConfig?.allowByValueEmbeddables,
});
});
setKibanaServices(core, plugins);
Promise.all([import('./dashboard_actions'), untilPluginStartServicesReady()]).then(
([{ buildAllDashboardActions }]) => {
buildAllDashboardActions({
plugins,
allowByValueEmbeddables: this.dashboardFeatureFlagConfig?.allowByValueEmbeddables,
});
}
);
return {
locator: this.locator,
dashboardFeatureFlagConfig: this.dashboardFeatureFlagConfig!,
registerDashboardPanelPlacementSetting,
findDashboardsService: async () => {
const { pluginServices } = await import('./services/plugin_services');
const {
dashboardContentManagement: { findDashboards },
} = pluginServices.getServices();
return findDashboards;
const { getDashboardContentManagementService } = await import(
'./services/dashboard_content_management_service'
);
return getDashboardContentManagementService().findDashboards;
},
};
}

View file

@ -1,22 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks';
import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public';
import { DashboardAnalyticsService } from './types';
type AnalyticsServiceFactory = PluginServiceFactory<DashboardAnalyticsService>;
export const analyticsServiceFactory: AnalyticsServiceFactory = () => {
const pluginMock = analyticsServiceMock.createAnalyticsServiceStart();
return {
reportEvent: pluginMock.reportEvent,
};
};

View file

@ -1,27 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public';
import type { DashboardStartDependencies } from '../../plugin';
import type { DashboardAnalyticsService } from './types';
export type AnalyticsServiceFactory = KibanaPluginServiceFactory<
DashboardAnalyticsService,
DashboardStartDependencies
>;
export const analyticsServiceFactory: AnalyticsServiceFactory = ({ coreStart }) => {
const {
analytics: { reportEvent },
} = coreStart;
return {
reportEvent,
};
};

View file

@ -1,14 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { CoreStart } from '@kbn/core/public';
export interface DashboardAnalyticsService {
reportEvent: CoreStart['analytics']['reportEvent'];
}

View file

@ -1,31 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { applicationServiceMock } from '@kbn/core-application-browser-mocks';
import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public';
import { DashboardApplicationService } from './types';
type ApplicationServiceFactory = PluginServiceFactory<DashboardApplicationService>;
export const applicationServiceFactory: ApplicationServiceFactory = () => {
const pluginMock = applicationServiceMock.createStartContract();
return {
currentAppId$: pluginMock.currentAppId$,
navigateToApp: pluginMock.navigateToApp,
navigateToUrl: pluginMock.navigateToUrl,
getUrlForApp: pluginMock.getUrlForApp,
capabilities: {
advancedSettings: pluginMock.capabilities.advancedSettings,
maps: pluginMock.capabilities.maps,
navLinks: pluginMock.capabilities.navLinks,
visualize: pluginMock.capabilities.visualize,
},
};
};

View file

@ -1,42 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public';
import type { DashboardStartDependencies } from '../../plugin';
import type { DashboardApplicationService } from './types';
export type ApplicationServiceFactory = KibanaPluginServiceFactory<
DashboardApplicationService,
DashboardStartDependencies
>;
export const applicationServiceFactory: ApplicationServiceFactory = ({ coreStart }) => {
const {
application: {
currentAppId$,
navigateToApp,
navigateToUrl,
getUrlForApp,
capabilities: { advancedSettings, maps, navLinks, visualize },
},
} = coreStart;
return {
currentAppId$,
navigateToApp,
navigateToUrl,
getUrlForApp,
capabilities: {
advancedSettings,
maps,
navLinks,
visualize,
},
};
};

View file

@ -1,23 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { CoreStart } from '@kbn/core/public';
export interface DashboardApplicationService {
currentAppId$: CoreStart['application']['currentAppId$'];
navigateToApp: CoreStart['application']['navigateToApp'];
navigateToUrl: CoreStart['application']['navigateToUrl'];
getUrlForApp: CoreStart['application']['getUrlForApp'];
capabilities: {
advancedSettings: CoreStart['application']['capabilities']['advancedSettings'];
maps: CoreStart['application']['capabilities']['maps']; // only used in `add_to_library_action`
navLinks: CoreStart['application']['capabilities']['navLinks'];
visualize: CoreStart['application']['capabilities']['visualize']; // only used in `add_to_library_action`
};
}

View file

@ -1,29 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { coreMock, chromeServiceMock } from '@kbn/core/public/mocks';
import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public';
import { DashboardChromeService } from './types';
type ChromeServiceFactory = PluginServiceFactory<DashboardChromeService>;
export const chromeServiceFactory: ChromeServiceFactory = () => {
const pluginMock = chromeServiceMock.createStartContract();
return {
docTitle: pluginMock.docTitle,
setBadge: pluginMock.setBadge,
getIsVisible$: pluginMock.getIsVisible$,
recentlyAccessed: pluginMock.recentlyAccessed,
setBreadcrumbs: pluginMock.setBreadcrumbs,
setHelpExtension: pluginMock.setHelpExtension,
setIsVisible: pluginMock.setIsVisible,
theme: coreMock.createStart().theme,
};
};

View file

@ -1,43 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public';
import type { DashboardStartDependencies } from '../../plugin';
import type { DashboardChromeService } from './types';
export type ChromeServiceFactory = KibanaPluginServiceFactory<
DashboardChromeService,
DashboardStartDependencies
>;
export const chromeServiceFactory: ChromeServiceFactory = ({ coreStart }) => {
const {
chrome: {
docTitle,
setBadge,
getIsVisible$,
recentlyAccessed,
setBreadcrumbs,
setHelpExtension,
setIsVisible,
},
theme,
} = coreStart;
return {
docTitle,
setBadge,
getIsVisible$,
recentlyAccessed,
setBreadcrumbs,
setHelpExtension,
setIsVisible,
theme,
};
};

View file

@ -1,21 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { CoreStart } from '@kbn/core/public';
export interface DashboardChromeService {
docTitle: CoreStart['chrome']['docTitle'];
setBadge: CoreStart['chrome']['setBadge'];
getIsVisible$: CoreStart['chrome']['getIsVisible$'];
recentlyAccessed: CoreStart['chrome']['recentlyAccessed'];
setBreadcrumbs: CoreStart['chrome']['setBreadcrumbs'];
setHelpExtension: CoreStart['chrome']['setHelpExtension'];
setIsVisible: CoreStart['chrome']['setIsVisible'];
theme: CoreStart['theme'];
}

View file

@ -1,18 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public';
import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
import { contentManagementMock } from '@kbn/content-management-plugin/public/mocks';
export type ContentManagementServiceFactory = PluginServiceFactory<ContentManagementPublicStart>;
export const contentManagementServiceFactory: ContentManagementServiceFactory = () => {
return contentManagementMock.createStartContract();
};

View file

@ -1,25 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public';
import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
import { DashboardStartDependencies } from '../../plugin';
export type ContentManagementServiceFactory = KibanaPluginServiceFactory<
ContentManagementPublicStart,
DashboardStartDependencies
>;
export const contentManagementServiceFactory: ContentManagementServiceFactory = ({
startPlugins,
}) => {
const { contentManagement } = startPlugins;
return contentManagement;
};

View file

@ -1,22 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { coreMock } from '@kbn/core/public/mocks';
import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public';
import { DashboardCoreContextService } from './types';
type CoreContextServiceFactory = PluginServiceFactory<DashboardCoreContextService>;
export const coreContextServiceFactory: CoreContextServiceFactory = () => {
const pluginMock = coreMock.createStart();
return {
executionContext: pluginMock.executionContext,
i18nContext: pluginMock.i18n.Context,
};
};

View file

@ -1,29 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public';
import type { DashboardStartDependencies } from '../../plugin';
import type { DashboardCoreContextService } from './types';
export type CoreContextServiceFactory = KibanaPluginServiceFactory<
DashboardCoreContextService,
DashboardStartDependencies
>;
export const coreContextServiceFactory: CoreContextServiceFactory = ({ coreStart }) => {
const {
executionContext,
i18n: { Context },
} = coreStart;
return {
executionContext,
i18nContext: Context,
};
};

View file

@ -1,15 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { CoreStart } from '@kbn/core/public';
export interface DashboardCoreContextService {
executionContext: CoreStart['executionContext'];
i18nContext: CoreStart['i18n']['Context'];
}

View file

@ -1,22 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public';
import { coreMock } from '@kbn/core/public/mocks';
import { DashboardCustomBrandingService } from './types';
type CustomBrandingServiceFactory = PluginServiceFactory<DashboardCustomBrandingService>;
export const customBrandingServiceFactory: CustomBrandingServiceFactory = () => {
const pluginMock = coreMock.createStart();
return {
hasCustomBranding$: pluginMock.customBranding.hasCustomBranding$,
customBranding$: pluginMock.customBranding.customBranding$,
};
};

View file

@ -1,25 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public';
import { DashboardStartDependencies } from '../../plugin';
import { DashboardCustomBrandingService } from './types';
export type CustomBrandingServiceFactory = KibanaPluginServiceFactory<
DashboardCustomBrandingService,
DashboardStartDependencies
>;
export const customBrandingServiceFactory: CustomBrandingServiceFactory = ({ coreStart }) => {
const { customBranding } = coreStart;
return {
hasCustomBranding$: customBranding.hasCustomBranding$,
customBranding$: customBranding.customBranding$,
};
};

View file

@ -1,15 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { CustomBrandingStart } from '@kbn/core-custom-branding-browser';
export interface DashboardCustomBrandingService {
hasCustomBranding$: CustomBrandingStart['hasCustomBranding$'];
customBranding$: CustomBrandingStart['customBranding$'];
}

View file

@ -1,27 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public';
import { DashboardBackupServiceType } from './types';
type DashboardBackupServiceFactory = PluginServiceFactory<DashboardBackupServiceType>;
export const dashboardBackupServiceFactory: DashboardBackupServiceFactory = () => {
return {
clearState: jest.fn(),
getState: jest.fn().mockReturnValue(undefined),
setState: jest.fn(),
getViewMode: jest.fn(),
storeViewMode: jest.fn(),
getDashboardIdsWithUnsavedChanges: jest
.fn()
.mockReturnValue(['dashboardUnsavedOne', 'dashboardUnsavedTwo']),
dashboardHasUnsavedEdits: jest.fn(),
};
};

View file

@ -1,31 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { UnsavedPanelState } from '../../dashboard_container/types';
import { SavedDashboardInput } from '../dashboard_content_management/types';
export interface DashboardBackupServiceType {
clearState: (id?: string) => void;
getState: (id: string | undefined) =>
| {
dashboardState?: Partial<SavedDashboardInput>;
panels?: UnsavedPanelState;
}
| undefined;
setState: (
id: string | undefined,
dashboardState: Partial<SavedDashboardInput>,
panels: UnsavedPanelState
) => void;
getViewMode: () => ViewMode;
storeViewMode: (viewMode: ViewMode) => void;
getDashboardIdsWithUnsavedChanges: () => string[];
dashboardHasUnsavedEdits: (id?: string) => boolean;
}

View file

@ -7,21 +7,18 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { firstValueFrom } from 'rxjs';
import { isEqual } from 'lodash';
import { firstValueFrom } from 'rxjs';
import { set } from '@kbn/safer-lodash-set';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import type { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public';
import { set } from '@kbn/safer-lodash-set';
import { DashboardSpacesService } from '../spaces/types';
import type { DashboardStartDependencies } from '../../plugin';
import type { DashboardBackupServiceType } from './types';
import type { DashboardContainerInput } from '../../../common';
import { DashboardNotificationsService } from '../notifications/types';
import { backupServiceStrings } from '../../dashboard_container/_dashboard_container_strings';
import { UnsavedPanelState } from '../../dashboard_container/types';
import type { DashboardContainerInput } from '../../common';
import { backupServiceStrings } from '../dashboard_container/_dashboard_container_strings';
import { UnsavedPanelState } from '../dashboard_container/types';
import { coreServices, spacesService } from './kibana_services';
import { SavedDashboardInput } from './dashboard_content_management_service/types';
export const DASHBOARD_PANELS_UNSAVED_ID = 'unsavedDashboard';
export const PANELS_CONTROL_GROUP_KEY = 'controlGroup';
@ -31,34 +28,39 @@ const DASHBOARD_VIEWMODE_LOCAL_KEY = 'dashboardViewMode';
// this key is named `panels` for BWC reasons, but actually contains the entire dashboard state
const DASHBOARD_STATE_SESSION_KEY = 'dashboardStateManagerPanels';
interface DashboardBackupRequiredServices {
notifications: DashboardNotificationsService;
spaces: DashboardSpacesService;
interface DashboardBackupServiceType {
clearState: (id?: string) => void;
getState: (id: string | undefined) =>
| {
dashboardState?: Partial<SavedDashboardInput>;
panels?: UnsavedPanelState;
}
| undefined;
setState: (
id: string | undefined,
dashboardState: Partial<SavedDashboardInput>,
panels: UnsavedPanelState
) => void;
getViewMode: () => ViewMode;
storeViewMode: (viewMode: ViewMode) => void;
getDashboardIdsWithUnsavedChanges: () => string[];
dashboardHasUnsavedEdits: (id?: string) => boolean;
}
export type DashboardBackupServiceFactory = KibanaPluginServiceFactory<
DashboardBackupServiceType,
DashboardStartDependencies,
DashboardBackupRequiredServices
>;
class DashboardBackupService implements DashboardBackupServiceType {
private activeSpaceId: string;
private sessionStorage: Storage;
private localStorage: Storage;
private notifications: DashboardNotificationsService;
private spaces: DashboardSpacesService;
private oldDashboardsWithUnsavedChanges: string[] = [];
constructor(requiredServices: DashboardBackupRequiredServices) {
({ notifications: this.notifications, spaces: this.spaces } = requiredServices);
constructor() {
this.sessionStorage = new Storage(sessionStorage);
this.localStorage = new Storage(localStorage);
this.activeSpaceId = 'default';
if (this.spaces.getActiveSpace$) {
firstValueFrom(this.spaces.getActiveSpace$()).then((space) => {
if (spacesService) {
firstValueFrom(spacesService.getActiveSpace$()).then((space) => {
this.activeSpaceId = space.id;
});
}
@ -72,7 +74,7 @@ class DashboardBackupService implements DashboardBackupServiceType {
try {
this.localStorage.set(DASHBOARD_VIEWMODE_LOCAL_KEY, viewMode);
} catch (e) {
this.notifications.toasts.addDanger({
coreServices.notifications.toasts.addDanger({
title: backupServiceStrings.viewModeStorageError(e.message),
'data-test-subj': 'dashboardViewmodeBackupFailure',
});
@ -99,7 +101,7 @@ class DashboardBackupService implements DashboardBackupServiceType {
});
}
} catch (e) {
this.notifications.toasts.addDanger({
coreServices.notifications.toasts.addDanger({
title: backupServiceStrings.getPanelsClearError(e.message),
'data-test-subj': 'dashboardPanelsClearFailure',
});
@ -117,7 +119,7 @@ class DashboardBackupService implements DashboardBackupServiceType {
return { dashboardState, panels };
} catch (e) {
this.notifications.toasts.addDanger({
coreServices.notifications.toasts.addDanger({
title: backupServiceStrings.getPanelsGetError(e.message),
'data-test-subj': 'dashboardPanelsGetFailure',
});
@ -138,7 +140,7 @@ class DashboardBackupService implements DashboardBackupServiceType {
set(panelsStorage, [this.activeSpaceId, id], unsavedPanels);
this.sessionStorage.set(DASHBOARD_PANELS_SESSION_KEY, panelsStorage, true);
} catch (e) {
this.notifications.toasts.addDanger({
coreServices.notifications.toasts.addDanger({
title: backupServiceStrings.getPanelsSetError(e.message),
'data-test-subj': 'dashboardPanelsSetFailure',
});
@ -178,7 +180,7 @@ class DashboardBackupService implements DashboardBackupServiceType {
return this.oldDashboardsWithUnsavedChanges;
} catch (e) {
this.notifications.toasts.addDanger({
coreServices.notifications.toasts.addDanger({
title: backupServiceStrings.getPanelsGetError(e.message),
'data-test-subj': 'dashboardPanelsGetFailure',
});
@ -191,9 +193,11 @@ class DashboardBackupService implements DashboardBackupServiceType {
}
}
export const dashboardBackupServiceFactory: DashboardBackupServiceFactory = (
core,
requiredServices
) => {
return new DashboardBackupService(requiredServices);
let dashboardBackupService: DashboardBackupService;
export const getDashboardBackupService = () => {
if (!dashboardBackupService) {
dashboardBackupService = new DashboardBackupService();
}
return dashboardBackupService;
};

View file

@ -1,30 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public';
import { DashboardCapabilitiesService } from './types';
const defaultDashboardCapabilities: DashboardCapabilitiesService = {
show: true,
createNew: true,
saveQuery: true,
createShortUrl: true,
showWriteControls: true,
storeSearchSession: true,
mapsCapabilities: { save: true },
visualizeCapabilities: { save: true },
};
type DashboardCapabilitiesServiceFactory = PluginServiceFactory<DashboardCapabilitiesService>;
export const dashboardCapabilitiesServiceFactory: DashboardCapabilitiesServiceFactory = () => {
return {
...defaultDashboardCapabilities,
};
};

View file

@ -1,19 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export interface DashboardCapabilitiesService {
show: boolean;
saveQuery: boolean;
createNew: boolean;
mapsCapabilities: { save: boolean };
createShortUrl: boolean;
showWriteControls: boolean;
visualizeCapabilities: { save: boolean };
storeSearchSession: boolean;
}

Some files were not shown because too many files have changed in this diff Show more