[Embeddables Rebuild] Make parent and ID static (#175137)

Makes the `parentApi` key of the embeddable framework static.
Makes the `uuid` key of the embeddable framework static.
Introduces a new `CanAccessViewMode` interface.
This commit is contained in:
Devon Thomson 2024-01-19 20:38:46 -05:00 committed by GitHub
parent be9a89d94a
commit 3a544a3b86
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 347 additions and 322 deletions

View file

@ -6,13 +6,13 @@
* Side Public License, v 1.
*/
import { apiPublishesParentApi } from '@kbn/presentation-publishing';
import { apiHasParentApi, PublishesViewMode } from '@kbn/presentation-publishing';
export interface PanelPackage {
panelType: string;
initialState: unknown;
}
export interface PresentationContainer {
export interface PresentationContainer extends Partial<PublishesViewMode> {
removePanel: (panelId: string) => void;
canRemovePanels?: () => boolean;
replacePanel: (idToRemove: string, newPanel: PanelPackage) => Promise<string>;
@ -27,7 +27,7 @@ export const apiIsPresentationContainer = (
export const getContainerParentFromAPI = (
api: null | unknown
): PresentationContainer | undefined => {
const apiParent = apiPublishesParentApi(api) ? api.parentApi.value : null;
const apiParent = apiHasParentApi(api) ? api.parentApi : null;
if (!apiParent) return undefined;
return apiIsPresentationContainer(apiParent) ? apiParent : undefined;
};

View file

@ -10,6 +10,13 @@ export interface EmbeddableApiContext {
embeddable: unknown;
}
export {
apiCanAccessViewMode,
getInheritedViewMode,
getViewModeSubject,
useInheritedViewMode,
type CanAccessViewMode,
} from './interfaces/can_access_view_mode';
export {
apiFiresPhaseEvents,
type FiresPhaseEvents,
@ -17,12 +24,18 @@ export {
type PhaseEventType,
} from './interfaces/fires_phase_events';
export { hasEditCapabilities, type HasEditCapabilities } from './interfaces/has_edit_capabilities';
export { apiHasParentApi, type HasParentApi } from './interfaces/has_parent_api';
export {
apiHasType,
apiIsOfType,
type HasType,
type HasTypeDisplayName,
} from './interfaces/has_type';
export {
apiPublishesBlockingError,
useBlockingError,
type PublishesBlockingError,
} from './interfaces/publishes_blocking_error';
export {
apiPublishesDataLoading,
useDataLoading,
@ -38,16 +51,6 @@ export {
useDisabledActionIds,
type PublishesDisabledActionIds,
} from './interfaces/publishes_disabled_action_ids';
export {
apiPublishesBlockingError,
useBlockingError,
type PublishesBlockingError,
} from './interfaces/publishes_blocking_error';
export {
apiPublishesUniqueId,
useUniqueId,
type PublishesUniqueId,
} from './interfaces/publishes_uuid';
export {
apiPublishesLocalUnifiedSearch,
apiPublishesPartialLocalUnifiedSearch,
@ -75,16 +78,12 @@ export {
type PublishesPanelTitle,
type PublishesWritablePanelTitle,
} from './interfaces/publishes_panel_title';
export {
apiPublishesParentApi,
useParentApi,
type PublishesParentApi,
} from './interfaces/publishes_parent_api';
export {
apiPublishesSavedObjectId,
useSavedObjectId,
type PublishesSavedObjectId,
} from './interfaces/publishes_saved_object_id';
export { apiHasUniqueId, type HasUniqueId } from './interfaces/has_uuid';
export {
apiPublishesViewMode,
apiPublishesWritableViewMode,
@ -99,4 +98,3 @@ export {
usePublishingSubject,
type PublishingSubject,
} from './publishing_subject';
export { useApiPublisher } from './publishing_utils';

View file

@ -0,0 +1,54 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { useStateFromPublishingSubject } from '../publishing_subject';
import { apiHasParentApi, HasParentApi } from './has_parent_api';
import { apiPublishesViewMode, PublishesViewMode, ViewMode } from './publishes_view_mode';
/**
* This API can access a view mode, either its own or from its parent API.
*/
export type CanAccessViewMode =
| Partial<PublishesViewMode>
| Partial<HasParentApi<Partial<PublishesViewMode>>>;
/**
* A type guard which can be used to determine if a given API has access to a view mode, its own or from its parent.
*/
export const apiCanAccessViewMode = (api: unknown): api is CanAccessViewMode => {
return apiPublishesViewMode(api) || (apiHasParentApi(api) && apiPublishesViewMode(api.parentApi));
};
/**
* A function which will get the view mode from the API or the parent API. if this api has a view mode AND its
* parent has a view mode, we consider the APIs version the source of truth.
*/
export const getInheritedViewMode = (api?: CanAccessViewMode) => {
if (apiPublishesViewMode(api)) return api.viewMode.getValue();
if (apiHasParentApi(api) && apiPublishesViewMode(api.parentApi)) {
return api.parentApi.viewMode.getValue();
}
};
export const getViewModeSubject = (api?: CanAccessViewMode) => {
if (apiPublishesViewMode(api)) return api.viewMode;
if (apiHasParentApi(api) && apiPublishesViewMode(api.parentApi)) {
return api.parentApi.viewMode;
}
};
/**
* A hook that gets a view mode from this API or its parent as a reactive variable which will cause re-renders on change.
* if this api has a view mode AND its parent has a view mode, we consider the APIs version the source of truth.
*/
export const useInheritedViewMode = <ApiType extends CanAccessViewMode = CanAccessViewMode>(
api: ApiType | undefined
) => {
const subject = getViewModeSubject(api);
useStateFromPublishingSubject<ViewMode, typeof subject>(subject);
};

View file

@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export interface HasParentApi<ParentApiType extends unknown = unknown> {
parentApi: ParentApiType;
}
/**
* A type guard which checks whether or not a given API has a parent API.
*/
export const apiHasParentApi = (unknownApi: null | unknown): unknownApi is HasParentApi => {
return Boolean(unknownApi && (unknownApi as HasParentApi)?.parentApi !== undefined);
};

View file

@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export interface HasUniqueId {
uuid: string;
}
export const apiHasUniqueId = (unknownApi: null | unknown): unknownApi is HasUniqueId => {
return Boolean(unknownApi && (unknownApi as HasUniqueId)?.uuid !== undefined);
};

View file

@ -1,35 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { PublishingSubject, useStateFromPublishingSubject } from '../publishing_subject';
export interface PublishesParentApi<ParentApiType extends unknown = unknown> {
parentApi: PublishingSubject<ParentApiType>;
}
type UnwrapParent<ApiType extends unknown> = ApiType extends PublishesParentApi<infer ParentType>
? ParentType
: unknown;
/**
* A type guard which checks whether or not a given API publishes its parent API.
*/
export const apiPublishesParentApi = (
unknownApi: null | unknown
): unknownApi is PublishesParentApi => {
return Boolean(unknownApi && (unknownApi as PublishesParentApi)?.parentApi !== undefined);
};
export const useParentApi = <
ApiType extends Partial<PublishesParentApi> = Partial<PublishesParentApi>
>(
api: ApiType
): UnwrapParent<ApiType> =>
useStateFromPublishingSubject<unknown, ApiType['parentApi']>(
apiPublishesParentApi(api) ? api.parentApi : undefined
) as UnwrapParent<ApiType>;

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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { PublishingSubject, useStateFromPublishingSubject } from '../publishing_subject';
export interface PublishesUniqueId {
uuid: PublishingSubject<string>;
}
export const apiPublishesUniqueId = (
unknownApi: null | unknown
): unknownApi is PublishesUniqueId => {
return Boolean(unknownApi && (unknownApi as PublishesUniqueId)?.uuid !== undefined);
};
/**
* Gets this API's UUID as a reactive variable which will cause re-renders on change.
*/
export const useUniqueId = <
ApiType extends Partial<PublishesUniqueId> = Partial<PublishesUniqueId>
>(
api: ApiType
) =>
useStateFromPublishingSubject<string, ApiType['uuid']>(
apiPublishesUniqueId(api) ? api.uuid : undefined
);

View file

@ -18,6 +18,10 @@ export interface PublishesViewMode {
viewMode: PublishingSubject<ViewMode>;
}
/**
* This API publishes a writable universal view mode which can change compatibility of actions and the
* visibility of components.
*/
export type PublishesWritableViewMode = PublishesViewMode & {
setViewMode: (viewMode: ViewMode) => void;
};

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { ViewMode } from '@kbn/presentation-publishing';
import { PublishesViewMode, ViewMode } from '@kbn/presentation-publishing';
import { BehaviorSubject } from 'rxjs';
import { pluginServices } from '../services/plugin_services';
import { AddToLibraryAction, AddPanelToLibraryActionApi } from './add_to_library_action';
@ -40,7 +40,7 @@ describe('Add to library action', () => {
});
it('is incompatible when view mode is view', async () => {
context.embeddable.viewMode = new BehaviorSubject<ViewMode>('view');
(context.embeddable as PublishesViewMode).viewMode = new BehaviorSubject<ViewMode>('view');
expect(await action.isCompatible(context)).toBe(false);
});

View file

@ -8,10 +8,11 @@
import { apiCanLinkToLibrary, CanLinkToLibrary } from '@kbn/presentation-library';
import {
apiPublishesViewMode,
apiCanAccessViewMode,
EmbeddableApiContext,
PublishesPanelTitle,
PublishesViewMode,
CanAccessViewMode,
getInheritedViewMode,
} from '@kbn/presentation-publishing';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { pluginServices } from '../services/plugin_services';
@ -19,12 +20,12 @@ import { dashboardAddToLibraryActionStrings } from './_dashboard_actions_strings
export const ACTION_ADD_TO_LIBRARY = 'saveToLibrary';
export type AddPanelToLibraryActionApi = PublishesViewMode &
export type AddPanelToLibraryActionApi = CanAccessViewMode &
CanLinkToLibrary &
Partial<PublishesPanelTitle>;
const isApiCompatible = (api: unknown | null): api is AddPanelToLibraryActionApi =>
Boolean(apiPublishesViewMode(api) && apiCanLinkToLibrary(api));
Boolean(apiCanAccessViewMode(api) && apiCanLinkToLibrary(api));
export class AddToLibraryAction implements Action<EmbeddableApiContext> {
public readonly type = ACTION_ADD_TO_LIBRARY;
@ -51,7 +52,7 @@ export class AddToLibraryAction implements Action<EmbeddableApiContext> {
public async isCompatible({ embeddable }: EmbeddableApiContext) {
if (!isApiCompatible(embeddable)) return false;
return embeddable.viewMode.value === 'edit' && (await embeddable.canLinkToLibrary());
return getInheritedViewMode(embeddable) === 'edit' && (await embeddable.canLinkToLibrary());
}
public async execute({ embeddable }: EmbeddableApiContext) {

View file

@ -6,8 +6,7 @@
* Side Public License, v 1.
*/
import { CanDuplicatePanels } from '@kbn/presentation-containers';
import { ViewMode } from '@kbn/presentation-publishing';
import { PublishesViewMode, ViewMode } from '@kbn/presentation-publishing';
import { BehaviorSubject } from 'rxjs';
import { ClonePanelAction, ClonePanelActionApi } from './clone_panel_action';
@ -19,11 +18,11 @@ describe('Clone panel action', () => {
action = new ClonePanelAction();
context = {
embeddable: {
uuid: new BehaviorSubject<string>('superId'),
uuid: 'superId',
viewMode: new BehaviorSubject<ViewMode>('edit'),
parentApi: new BehaviorSubject<CanDuplicatePanels>({
parentApi: {
duplicatePanel: jest.fn(),
}),
},
},
};
});
@ -40,12 +39,12 @@ describe('Clone panel action', () => {
});
it('is incompatible when view mode is view', async () => {
context.embeddable.viewMode = new BehaviorSubject<ViewMode>('view');
(context.embeddable as PublishesViewMode).viewMode = new BehaviorSubject<ViewMode>('view');
expect(await action.isCompatible(context)).toBe(false);
});
it('calls the parent duplicatePanel method on execute', async () => {
action.execute(context);
expect(context.embeddable.parentApi.value.duplicatePanel).toHaveBeenCalled();
expect(context.embeddable.parentApi.duplicatePanel).toHaveBeenCalled();
});
});

View file

@ -6,34 +6,34 @@
* Side Public License, v 1.
*/
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { apiCanDuplicatePanels, CanDuplicatePanels } from '@kbn/presentation-containers';
import {
apiPublishesUniqueId,
apiPublishesParentApi,
apiPublishesViewMode,
apiCanAccessViewMode,
apiHasParentApi,
apiHasUniqueId,
CanAccessViewMode,
EmbeddableApiContext,
getInheritedViewMode,
HasParentApi,
PublishesBlockingError,
PublishesUniqueId,
PublishesParentApi,
PublishesViewMode,
HasUniqueId,
} from '@kbn/presentation-publishing';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { dashboardClonePanelActionStrings } from './_dashboard_actions_strings';
export const ACTION_CLONE_PANEL = 'clonePanel';
export type ClonePanelActionApi = PublishesViewMode &
PublishesUniqueId &
PublishesParentApi<CanDuplicatePanels> &
export type ClonePanelActionApi = CanAccessViewMode &
HasUniqueId &
HasParentApi<CanDuplicatePanels> &
Partial<PublishesBlockingError>;
const isApiCompatible = (api: unknown | null): api is ClonePanelActionApi =>
Boolean(
apiPublishesUniqueId(api) &&
apiPublishesViewMode(api) &&
apiPublishesParentApi(api) &&
apiCanDuplicatePanels(api.parentApi.value)
apiHasUniqueId(api) &&
apiCanAccessViewMode(api) &&
apiHasParentApi(api) &&
apiCanDuplicatePanels(api.parentApi)
);
export class ClonePanelAction implements Action<EmbeddableApiContext> {
@ -55,11 +55,11 @@ export class ClonePanelAction implements Action<EmbeddableApiContext> {
public async isCompatible({ embeddable }: EmbeddableApiContext) {
if (!isApiCompatible(embeddable)) return false;
return Boolean(!embeddable.blockingError?.value && embeddable.viewMode.value === 'edit');
return Boolean(!embeddable.blockingError?.value && getInheritedViewMode(embeddable) === 'edit');
}
public async execute({ embeddable }: EmbeddableApiContext) {
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
embeddable.parentApi.value.duplicatePanel(embeddable.uuid.value);
embeddable.parentApi.duplicatePanel(embeddable.uuid);
}
}

View file

@ -11,13 +11,13 @@ import React from 'react';
import { CoreStart } from '@kbn/core-lifecycle-browser';
import {
apiIsOfType,
apiPublishesUniqueId,
apiPublishesParentApi,
apiHasUniqueId,
apiHasParentApi,
apiPublishesSavedObjectId,
HasType,
EmbeddableApiContext,
PublishesUniqueId,
PublishesParentApi,
HasUniqueId,
HasParentApi,
PublishesSavedObjectId,
} from '@kbn/presentation-publishing';
import { toMountPoint } from '@kbn/react-kibana-mount';
@ -37,18 +37,18 @@ export interface DashboardCopyToCapabilities {
}
export type CopyToDashboardAPI = HasType &
PublishesUniqueId &
PublishesParentApi<
HasUniqueId &
HasParentApi<
{ type: typeof DASHBOARD_CONTAINER_TYPE } & PublishesSavedObjectId &
DashboardPluginInternalFunctions
>;
const apiIsCompatible = (api: unknown): api is CopyToDashboardAPI => {
return (
apiPublishesUniqueId(api) &&
apiPublishesParentApi(api) &&
apiIsOfType(api.parentApi.value, DASHBOARD_CONTAINER_TYPE) &&
apiPublishesSavedObjectId(api.parentApi.value)
apiHasUniqueId(api) &&
apiHasParentApi(api) &&
apiIsOfType(api.parentApi, DASHBOARD_CONTAINER_TYPE) &&
apiPublishesSavedObjectId(api.parentApi)
);
};

View file

@ -46,11 +46,11 @@ export function CopyToDashboardModal({ api, closeModal }: CopyToDashboardModalPr
null
);
const dashboardId = api.parentApi.value.savedObjectId.value;
const dashboardId = api.parentApi.savedObjectId.value;
const onSubmit = useCallback(() => {
const dashboard = api.parentApi.value;
const panelToCopy = dashboard.getDashboardPanelFromId(api.uuid.value);
const dashboard = api.parentApi;
const panelToCopy = dashboard.getDashboardPanelFromId(api.uuid);
if (!panelToCopy) {
throw new PanelNotFoundError();

View file

@ -6,8 +6,6 @@
* Side Public License, v 1.
*/
import { CanExpandPanels } from '@kbn/presentation-containers';
import { ViewMode } from '@kbn/presentation-publishing';
import { BehaviorSubject } from 'rxjs';
import { ExpandPanelActionApi, ExpandPanelAction } from './expand_panel_action';
@ -19,12 +17,11 @@ describe('Expand panel action', () => {
action = new ExpandPanelAction();
context = {
embeddable: {
uuid: new BehaviorSubject<string>('superId'),
viewMode: new BehaviorSubject<ViewMode>('edit'),
parentApi: new BehaviorSubject<CanExpandPanels>({
uuid: 'superId',
parentApi: {
expandPanel: jest.fn(),
expandedPanelId: new BehaviorSubject<string | undefined>(undefined),
}),
},
},
};
});
@ -42,7 +39,7 @@ describe('Expand panel action', () => {
it('returns the correct icon based on expanded panel id', async () => {
expect(await action.getIconType(context)).toBe('expand');
context.embeddable.parentApi.value.expandedPanelId = new BehaviorSubject<string | undefined>(
context.embeddable.parentApi.expandedPanelId = new BehaviorSubject<string | undefined>(
'superPanelId'
);
expect(await action.getIconType(context)).toBe('minimize');
@ -50,7 +47,7 @@ describe('Expand panel action', () => {
it('returns the correct display name based on expanded panel id', async () => {
expect(await action.getDisplayName(context)).toBe('Maximize panel');
context.embeddable.parentApi.value.expandedPanelId = new BehaviorSubject<string | undefined>(
context.embeddable.parentApi.expandedPanelId = new BehaviorSubject<string | undefined>(
'superPanelId'
);
expect(await action.getDisplayName(context)).toBe('Minimize');
@ -58,6 +55,6 @@ describe('Expand panel action', () => {
it('calls the parent expandPanel method on execute', async () => {
action.execute(context);
expect(context.embeddable.parentApi.value.expandPanel).toHaveBeenCalled();
expect(context.embeddable.parentApi.expandPanel).toHaveBeenCalled();
});
});

View file

@ -8,13 +8,11 @@
import { apiCanExpandPanels, CanExpandPanels } from '@kbn/presentation-containers';
import {
apiPublishesUniqueId,
apiPublishesParentApi,
apiPublishesViewMode,
apiHasParentApi,
apiHasUniqueId,
EmbeddableApiContext,
PublishesUniqueId,
PublishesParentApi,
PublishesViewMode,
HasParentApi,
HasUniqueId,
} from '@kbn/presentation-publishing';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
@ -22,17 +20,10 @@ import { dashboardExpandPanelActionStrings } from './_dashboard_actions_strings'
export const ACTION_EXPAND_PANEL = 'togglePanel';
export type ExpandPanelActionApi = PublishesViewMode &
PublishesUniqueId &
PublishesParentApi<CanExpandPanels>;
export type ExpandPanelActionApi = HasUniqueId & HasParentApi<CanExpandPanels>;
const isApiCompatible = (api: unknown | null): api is ExpandPanelActionApi =>
Boolean(
apiPublishesUniqueId(api) &&
apiPublishesViewMode(api) &&
apiPublishesParentApi(api) &&
apiCanExpandPanels(api.parentApi.value)
);
Boolean(apiHasUniqueId(api) && apiHasParentApi(api) && apiCanExpandPanels(api.parentApi));
export class ExpandPanelAction implements Action<EmbeddableApiContext> {
public readonly type = ACTION_EXPAND_PANEL;
@ -43,14 +34,14 @@ export class ExpandPanelAction implements Action<EmbeddableApiContext> {
public getDisplayName({ embeddable }: EmbeddableApiContext) {
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
return embeddable.parentApi.value.expandedPanelId.value
return embeddable.parentApi.expandedPanelId.value
? dashboardExpandPanelActionStrings.getMinimizeTitle()
: dashboardExpandPanelActionStrings.getMaximizeTitle();
}
public getIconType({ embeddable }: EmbeddableApiContext) {
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
return embeddable.parentApi.value.expandedPanelId.value ? 'minimize' : 'expand';
return embeddable.parentApi.expandedPanelId.value ? 'minimize' : 'expand';
}
public async isCompatible({ embeddable }: EmbeddableApiContext) {
@ -59,8 +50,8 @@ export class ExpandPanelAction implements Action<EmbeddableApiContext> {
public async execute({ embeddable }: EmbeddableApiContext) {
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
embeddable.parentApi.value.expandPanel(
embeddable.parentApi.value.expandedPanelId.value ? undefined : embeddable.uuid.value
embeddable.parentApi.expandPanel(
embeddable.parentApi.expandedPanelId.value ? undefined : embeddable.uuid
);
}
}

View file

@ -10,7 +10,6 @@ import { Filter, FilterStateStore, type AggregateQuery, type Query } from '@kbn/
import { ViewMode } from '@kbn/presentation-publishing';
import { BehaviorSubject } from 'rxjs';
import { DashboardPluginInternalFunctions } from '../dashboard_container/external_api/dashboard_api';
import {
FiltersNotificationAction,
FiltersNotificationActionApi,
@ -56,12 +55,12 @@ describe('filters notification action', () => {
action = new FiltersNotificationAction();
context = {
embeddable: {
uuid: new BehaviorSubject<string>('testId'),
uuid: 'testId',
viewMode: viewModeSubject,
parentApi: new BehaviorSubject<DashboardPluginInternalFunctions>({
parentApi: {
getAllDataViews: jest.fn(),
getDashboardPanelFromId: jest.fn(),
}),
},
localFilters: filtersSubject,
localQuery: querySubject,
},

View file

@ -13,14 +13,16 @@ import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import {
apiCanAccessViewMode,
apiPublishesPartialLocalUnifiedSearch,
apiPublishesUniqueId,
apiPublishesViewMode,
apiHasUniqueId,
CanAccessViewMode,
EmbeddableApiContext,
getInheritedViewMode,
getViewModeSubject,
HasParentApi,
PublishesLocalUnifiedSearch,
PublishesParentApi,
PublishesUniqueId,
PublishesViewMode,
HasUniqueId,
} from '@kbn/presentation-publishing';
import { merge } from 'rxjs';
import { DashboardPluginInternalFunctions } from '../dashboard_container/external_api/dashboard_api';
@ -30,20 +32,18 @@ import { dashboardFilterNotificationActionStrings } from './_dashboard_actions_s
export const BADGE_FILTERS_NOTIFICATION = 'ACTION_FILTERS_NOTIFICATION';
export type FiltersNotificationActionApi = PublishesUniqueId &
PublishesViewMode &
export type FiltersNotificationActionApi = HasUniqueId &
CanAccessViewMode &
Partial<PublishesLocalUnifiedSearch> &
PublishesParentApi<DashboardPluginInternalFunctions>;
HasParentApi<DashboardPluginInternalFunctions>;
const isApiCompatible = (api: unknown | null): api is FiltersNotificationActionApi =>
Boolean(
apiPublishesUniqueId(api) &&
apiPublishesViewMode(api) &&
apiPublishesPartialLocalUnifiedSearch(api)
apiHasUniqueId(api) && apiCanAccessViewMode(api) && apiPublishesPartialLocalUnifiedSearch(api)
);
const compatibilityCheck = (api: EmbeddableApiContext['embeddable']) => {
if (!isApiCompatible(api) || api.viewMode.value !== 'edit') return false;
if (!isApiCompatible(api) || getInheritedViewMode(api) !== 'edit') return false;
const query = api.localQuery?.value;
return (
(api.localFilters?.value ?? []).length > 0 ||
@ -102,12 +102,10 @@ export class FiltersNotificationAction implements Action<EmbeddableApiContext> {
) {
if (!isApiCompatible(embeddable)) return;
return merge(
...[embeddable.localQuery, embeddable.localFilters, embeddable.viewMode].filter((value) =>
Boolean(value)
...[embeddable.localQuery, embeddable.localFilters, getViewModeSubject(embeddable)].filter(
(value) => Boolean(value)
)
).subscribe(() => {
onChange(compatibilityCheck(embeddable), this);
});
).subscribe(() => onChange(compatibilityCheck(embeddable), this));
}
public execute = async () => {};

View file

@ -14,7 +14,6 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { BehaviorSubject } from 'rxjs';
import { DashboardPluginInternalFunctions } from '../dashboard_container/external_api/dashboard_api';
import { FiltersNotificationActionApi } from './filters_notification_action';
import { FiltersNotificationPopover } from './filters_notification_popover';
@ -58,12 +57,12 @@ describe('filters notification popover', () => {
updateQuery = (query) => querySubject.next(query);
api = {
uuid: new BehaviorSubject<string>('testId'),
uuid: 'testId',
viewMode: new BehaviorSubject<ViewMode>('edit'),
parentApi: new BehaviorSubject<DashboardPluginInternalFunctions>({
parentApi: {
getAllDataViews: jest.fn(),
getDashboardPanelFromId: jest.fn(),
}),
},
localFilters: filtersSubject,
localQuery: querySubject,
};
@ -75,15 +74,13 @@ describe('filters notification popover', () => {
<FiltersNotificationPopover api={api} />
</I18nProvider>
);
await userEvent.click(
await screen.findByTestId(`embeddablePanelNotification-${api.uuid.value}`)
);
await userEvent.click(await screen.findByTestId(`embeddablePanelNotification-${api.uuid}`));
await waitForEuiPopoverOpen();
};
it('calls get all dataviews from the parent', async () => {
render(<FiltersNotificationPopover api={api} />);
expect(api.parentApi.value?.getAllDataViews).toHaveBeenCalled();
expect(api.parentApi?.getAllDataViews).toHaveBeenCalled();
});
it('renders the filter section when given filters', async () => {

View file

@ -56,7 +56,7 @@ export function FiltersNotificationPopover({ api }: { api: FiltersNotificationAc
}
}, [api, setDisableEditButton]);
const dataViews = useMemo(() => api.parentApi.value?.getAllDataViews(), [api]);
const dataViews = useMemo(() => api.parentApi?.getAllDataViews(), [api]);
return (
<EuiPopover
@ -65,7 +65,7 @@ export function FiltersNotificationPopover({ api }: { api: FiltersNotificationAc
color="text"
iconType={'filter'}
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
data-test-subj={`embeddablePanelNotification-${api.uuid.value}`}
data-test-subj={`embeddablePanelNotification-${api.uuid}`}
aria-label={displayName}
/>
}

View file

@ -8,7 +8,11 @@
import React from 'react';
import { EmbeddableApiContext } from '@kbn/presentation-publishing';
import {
EmbeddableApiContext,
getInheritedViewMode,
getViewModeSubject,
} from '@kbn/presentation-publishing';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { LibraryNotificationPopover } from './library_notification_popover';
import { unlinkActionIsCompatible, UnlinkFromLibraryAction } from './unlink_from_library_action';
@ -43,7 +47,7 @@ export class LibraryNotificationAction implements Action<EmbeddableApiContext> {
* TODO: Upgrade this action by subscribing to changes in the existance of a saved object id. Currently,
* this is unnecessary because a link or unlink operation will cause the panel to unmount and remount.
*/
return embeddable.viewMode.subscribe((viewMode) => {
return getViewModeSubject(embeddable)?.subscribe((viewMode) => {
embeddable.canUnlinkFromLibrary().then((canUnlink) => {
onChange(viewMode === 'edit' && canUnlink, this);
});
@ -62,7 +66,7 @@ export class LibraryNotificationAction implements Action<EmbeddableApiContext> {
public isCompatible = async ({ embeddable }: EmbeddableApiContext) => {
if (!unlinkActionIsCompatible(embeddable)) return false;
return embeddable.viewMode.value === 'edit' && embeddable.canUnlinkFromLibrary();
return getInheritedViewMode(embeddable) === 'edit' && embeddable.canUnlinkFromLibrary();
};
public execute = async () => {};

View file

@ -31,7 +31,7 @@ export const openReplacePanelFlyout = async ({
} = pluginServices.getServices();
// send the overlay ref to the parent if it is capable of tracking overlays
const overlayTracker = tracksOverlays(api.parentApi.value) ? api.parentApi.value : undefined;
const overlayTracker = tracksOverlays(api.parentApi) ? api.parentApi : undefined;
const flyoutSession = openFlyout(
toMountPoint(

View file

@ -6,8 +6,7 @@
* Side Public License, v 1.
*/
import { PresentationContainer } from '@kbn/presentation-containers';
import { ViewMode } from '@kbn/presentation-publishing';
import { PublishesViewMode, ViewMode } from '@kbn/presentation-publishing';
import { BehaviorSubject } from 'rxjs';
import { ReplacePanelSOFinder } from '.';
import { ReplacePanelAction, ReplacePanelActionApi } from './replace_panel_action';
@ -27,12 +26,12 @@ describe('replace panel action', () => {
action = new ReplacePanelAction(savedObjectFinder);
context = {
embeddable: {
uuid: new BehaviorSubject<string>('superId'),
uuid: 'superId',
viewMode: new BehaviorSubject<ViewMode>('edit'),
parentApi: new BehaviorSubject<PresentationContainer>({
parentApi: {
removePanel: jest.fn(),
replacePanel: jest.fn(),
}),
},
},
};
});
@ -49,7 +48,7 @@ describe('replace panel action', () => {
});
it('is incompatible when view mode is view', async () => {
context.embeddable.viewMode = new BehaviorSubject<ViewMode>('view');
(context.embeddable as PublishesViewMode).viewMode = new BehaviorSubject<ViewMode>('view');
expect(await action.isCompatible(context)).toBe(false);
});

View file

@ -12,14 +12,15 @@ import {
TracksOverlays,
} from '@kbn/presentation-containers';
import {
apiPublishesUniqueId,
apiPublishesParentApi,
apiPublishesViewMode,
apiHasUniqueId,
EmbeddableApiContext,
PublishesUniqueId,
HasUniqueId,
PublishesPanelTitle,
PublishesParentApi,
PublishesViewMode,
apiCanAccessViewMode,
CanAccessViewMode,
HasParentApi,
apiHasParentApi,
getInheritedViewMode,
} from '@kbn/presentation-publishing';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { ReplacePanelSOFinder } from '.';
@ -28,17 +29,17 @@ import { dashboardReplacePanelActionStrings } from './_dashboard_actions_strings
export const ACTION_REPLACE_PANEL = 'replacePanel';
export type ReplacePanelActionApi = PublishesViewMode &
PublishesUniqueId &
export type ReplacePanelActionApi = CanAccessViewMode &
HasUniqueId &
Partial<PublishesPanelTitle> &
PublishesParentApi<PresentationContainer & Partial<TracksOverlays>>;
HasParentApi<PresentationContainer & Partial<TracksOverlays>>;
const isApiCompatible = (api: unknown | null): api is ReplacePanelActionApi =>
Boolean(
apiPublishesUniqueId(api) &&
apiPublishesViewMode(api) &&
apiPublishesParentApi(api) &&
apiIsPresentationContainer(api.parentApi.value)
apiHasUniqueId(api) &&
apiCanAccessViewMode(api) &&
apiHasParentApi(api) &&
apiIsPresentationContainer(api.parentApi)
);
export class ReplacePanelAction implements Action<EmbeddableApiContext> {
@ -60,7 +61,7 @@ export class ReplacePanelAction implements Action<EmbeddableApiContext> {
public async isCompatible({ embeddable }: EmbeddableApiContext) {
if (!isApiCompatible(embeddable)) return false;
return embeddable.viewMode.value === 'edit';
return getInheritedViewMode(embeddable) === 'edit';
}
public async execute({ embeddable }: EmbeddableApiContext) {

View file

@ -49,7 +49,7 @@ export class ReplacePanelFlyout extends React.Component<Props> {
};
public onReplacePanel = async (savedObjectId: string, type: string, name: string) => {
this.props.api.parentApi.value.replacePanel(this.props.api.uuid.value, {
this.props.api.parentApi.replacePanel(this.props.api.uuid, {
panelType: type,
initialState: { savedObjectId },
});

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { ViewMode } from '@kbn/presentation-publishing';
import { PublishesViewMode, ViewMode } from '@kbn/presentation-publishing';
import { BehaviorSubject } from 'rxjs';
import { pluginServices } from '../services/plugin_services';
import {
@ -43,7 +43,7 @@ describe('Unlink from library action', () => {
});
it('is incompatible when view mode is view', async () => {
context.embeddable.viewMode = new BehaviorSubject<ViewMode>('view');
(context.embeddable as PublishesViewMode).viewMode = new BehaviorSubject<ViewMode>('view');
expect(await action.isCompatible(context)).toBe(false);
});

View file

@ -10,24 +10,25 @@ import { apiCanUnlinkFromLibrary, CanUnlinkFromLibrary } from '@kbn/presentation
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import {
apiPublishesViewMode,
apiCanAccessViewMode,
CanAccessViewMode,
EmbeddableApiContext,
getInheritedViewMode,
PublishesPanelTitle,
PublishesViewMode,
} from '@kbn/presentation-publishing';
import { pluginServices } from '../services/plugin_services';
import { dashboardUnlinkFromLibraryActionStrings } from './_dashboard_actions_strings';
export const ACTION_UNLINK_FROM_LIBRARY = 'unlinkFromLibrary';
export type UnlinkPanelFromLibraryActionApi = PublishesViewMode &
export type UnlinkPanelFromLibraryActionApi = CanAccessViewMode &
CanUnlinkFromLibrary &
Partial<PublishesPanelTitle>;
export const unlinkActionIsCompatible = (
api: unknown | null
): api is UnlinkPanelFromLibraryActionApi =>
Boolean(apiPublishesViewMode(api) && apiCanUnlinkFromLibrary(api));
Boolean(apiCanAccessViewMode(api) && apiCanUnlinkFromLibrary(api));
export class UnlinkFromLibraryAction implements Action<EmbeddableApiContext> {
public readonly type = ACTION_UNLINK_FROM_LIBRARY;
@ -54,7 +55,7 @@ export class UnlinkFromLibraryAction implements Action<EmbeddableApiContext> {
public async isCompatible({ embeddable }: EmbeddableApiContext) {
if (!unlinkActionIsCompatible(embeddable)) return false;
return embeddable.viewMode.value === 'edit' && (await embeddable.canUnlinkFromLibrary());
return getInheritedViewMode(embeddable) === 'edit' && (await embeddable.canUnlinkFromLibrary());
}
public async execute({ embeddable }: EmbeddableApiContext) {

View file

@ -8,17 +8,15 @@
import { css } from '@emotion/react';
import { PresentationPanel } from '@kbn/presentation-panel-plugin/public';
import { useApiPublisher } from '@kbn/presentation-publishing';
import { PanelCompatibleComponent } from '@kbn/presentation-panel-plugin/public/panel_component/types';
import { isPromise } from '@kbn/std';
import React, { ReactNode, useEffect, useMemo, useState } from 'react';
import React, { ReactNode, useEffect, useImperativeHandle, useMemo, useState } from 'react';
import { untilPluginStartServicesReady } from '../kibana_services';
import { LegacyEmbeddableAPI } from '../lib/embeddables/i_embeddable';
import { CreateEmbeddableComponent } from '../registry/create_embeddable_component';
import { EmbeddablePanelProps, LegacyEmbeddableCompatibilityComponent } from './types';
import { EmbeddablePanelProps } from './types';
const getComponentFromEmbeddable = async (
embeddable: EmbeddablePanelProps['embeddable']
): Promise<LegacyEmbeddableCompatibilityComponent> => {
): Promise<PanelCompatibleComponent> => {
const startServicesPromise = untilPluginStartServicesReady();
const embeddablePromise =
typeof embeddable === 'function' ? embeddable() : Promise.resolve(embeddable);
@ -27,7 +25,7 @@ const getComponentFromEmbeddable = async (
await unwrappedEmbeddable.parent.untilEmbeddableLoaded(unwrappedEmbeddable.id);
}
return CreateEmbeddableComponent((apiRef) => {
return React.forwardRef((props, apiRef) => {
const [node, setNode] = useState<ReactNode | undefined>();
const embeddableRoot: React.RefObject<HTMLDivElement> = useMemo(() => React.createRef(), []);
@ -45,7 +43,7 @@ const getComponentFromEmbeddable = async (
};
}, [embeddableRoot]);
useApiPublisher(unwrappedEmbeddable, apiRef);
useImperativeHandle(apiRef, () => unwrappedEmbeddable);
return (
<div css={css(`width: 100%; height: 100%; display:flex`)} ref={embeddableRoot}>
@ -56,12 +54,10 @@ const getComponentFromEmbeddable = async (
};
/**
* Loads and renders an embeddable.
* Loads and renders a legacy embeddable.
*/
export const EmbeddablePanel = (props: EmbeddablePanelProps) => {
const { embeddable, ...passThroughProps } = props;
const componentPromise = useMemo(() => getComponentFromEmbeddable(embeddable), [embeddable]);
return (
<PresentationPanel<LegacyEmbeddableAPI> {...passThroughProps} Component={componentPromise} />
);
return <PresentationPanel {...passThroughProps} Component={componentPromise} />;
};

View file

@ -93,9 +93,8 @@ export const legacyEmbeddableToApi = (
})
);
// legacy embeddables don't support ID changing or parent changing, so we don't need to subscribe to anything.
const uuid = new BehaviorSubject<string>(embeddable.id);
const parentApi = new BehaviorSubject<unknown>(embeddable.parent ?? undefined);
const uuid = embeddable.id;
const parentApi = embeddable.parent;
/**
* We treat all legacy embeddable types as if they can support local unified search state, because there is no programmatic way
@ -150,8 +149,8 @@ export const legacyEmbeddableToApi = (
return {
api: {
parentApi: parentApi as LegacyEmbeddableAPI['parentApi'],
uuid,
parentApi,
viewMode,
dataLoading,
blockingError,

View file

@ -8,6 +8,7 @@
import { ErrorLike } from '@kbn/expressions-plugin/common';
import { CanLinkToLibrary, CanUnlinkFromLibrary } from '@kbn/presentation-library';
import { DefaultPresentationPanelApi } from '@kbn/presentation-panel-plugin/public/panel_component/types';
import {
HasEditCapabilities,
HasType,
@ -16,8 +17,8 @@ import {
PublishesDataViews,
PublishesDisabledActionIds,
PublishesLocalUnifiedSearch,
PublishesParentApi,
PublishesUniqueId,
HasParentApi,
HasUniqueId,
PublishesViewMode,
PublishesWritablePanelDescription,
PublishesWritablePanelTitle,
@ -36,9 +37,8 @@ export type { EmbeddableInput };
* Types for compatibility between the legacy Embeddable system and the new system
*/
export type LegacyEmbeddableAPI = HasType &
PublishesUniqueId &
HasUniqueId &
PublishesViewMode &
PublishesParentApi &
PublishesDataViews &
HasEditCapabilities &
PublishesDataLoading &
@ -49,6 +49,7 @@ export type LegacyEmbeddableAPI = HasType &
PublishesWritablePanelTitle &
PublishesWritablePanelDescription &
Partial<CanLinkToLibrary & CanUnlinkFromLibrary> &
HasParentApi<DefaultPresentationPanelApi['parentApi']> &
EmbeddableHasTimeRange;
export interface EmbeddableAppContext {

View file

@ -9,7 +9,7 @@
import { DataView } from '@kbn/data-views-plugin/common';
import { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
import { TracksOverlays } from '@kbn/presentation-containers';
import { ViewMode } from '@kbn/presentation-publishing';
import { PublishesViewMode, ViewMode } from '@kbn/presentation-publishing';
import { BehaviorSubject } from 'rxjs';
import { core } from '../../kibana_services';
@ -23,7 +23,7 @@ describe('Customize panel action', () => {
action = new CustomizePanelAction();
context = {
embeddable: {
parentApi: new BehaviorSubject<unknown>({}),
parentApi: {},
viewMode: new BehaviorSubject<ViewMode>('edit'),
dataViews: new BehaviorSubject<DataView[] | undefined>(undefined),
},
@ -35,7 +35,7 @@ describe('Customize panel action', () => {
});
it('is compatible in view mode when API exposes writable unified search', async () => {
context.embeddable.viewMode = new BehaviorSubject<ViewMode>('view');
(context.embeddable as PublishesViewMode).viewMode = new BehaviorSubject<ViewMode>('view');
context.embeddable.localTimeRange = new BehaviorSubject<TimeRange | undefined>({
from: 'now-15m',
to: 'now',
@ -61,11 +61,11 @@ describe('Customize panel action', () => {
});
it('opens overlay on parent if parent is an overlay tracker', async () => {
context.embeddable.parentApi = new BehaviorSubject<unknown>({
context.embeddable.parentApi = {
openOverlay: jest.fn(),
clearOverlays: jest.fn(),
});
};
await action.execute(context);
expect((context.embeddable.parentApi.value as TracksOverlays).openOverlay).toHaveBeenCalled();
expect((context.embeddable.parentApi as TracksOverlays).openOverlay).toHaveBeenCalled();
});
});

View file

@ -8,13 +8,14 @@
import { i18n } from '@kbn/i18n';
import {
apiCanAccessViewMode,
apiPublishesDataViews,
apiPublishesLocalUnifiedSearch,
apiPublishesViewMode,
CanAccessViewMode,
EmbeddableApiContext,
getInheritedViewMode,
HasParentApi,
PublishesDataViews,
PublishesParentApi,
PublishesViewMode,
PublishesWritableLocalUnifiedSearch,
PublishesWritablePanelDescription,
PublishesWritablePanelTitle,
@ -24,19 +25,19 @@ import { openCustomizePanelFlyout } from './open_customize_panel';
export const ACTION_CUSTOMIZE_PANEL = 'ACTION_CUSTOMIZE_PANEL';
export type CustomizePanelActionApi = PublishesViewMode &
export type CustomizePanelActionApi = CanAccessViewMode &
PublishesDataViews &
Partial<
PublishesWritableLocalUnifiedSearch &
PublishesWritablePanelDescription &
PublishesWritablePanelTitle &
PublishesParentApi
HasParentApi
>;
export const isApiCompatibleWithCustomizePanelAction = (
api: unknown | null
): api is CustomizePanelActionApi =>
Boolean(apiPublishesViewMode(api) && apiPublishesDataViews(api));
Boolean(apiCanAccessViewMode(api) && apiPublishesDataViews(api));
export class CustomizePanelAction implements Action<EmbeddableApiContext> {
public type = ACTION_CUSTOMIZE_PANEL;
@ -59,7 +60,7 @@ export class CustomizePanelAction implements Action<EmbeddableApiContext> {
if (!isApiCompatibleWithCustomizePanelAction(embeddable)) return false;
// It should be possible to customize just the time range in View mode
return (
embeddable.viewMode.value === 'edit' ||
getInheritedViewMode(embeddable) === 'edit' ||
(apiPublishesLocalUnifiedSearch(embeddable) &&
(embeddable.isCompatibleWithLocalUnifiedSearch?.() ?? true))
);

View file

@ -29,7 +29,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { UI_SETTINGS } from '@kbn/data-plugin/public';
import { apiPublishesLocalUnifiedSearch } from '@kbn/presentation-publishing';
import { apiPublishesLocalUnifiedSearch, getInheritedViewMode } from '@kbn/presentation-publishing';
import { core } from '../../kibana_services';
import { CustomizePanelActionApi } from './customize_panel_action';
@ -54,7 +54,7 @@ export const CustomizePanelEditor = ({
* eventually the panel editor could be made to use state from the API instead (which will allow us to use a push flyout)
* For now, we copy the state here with `useState` initializing it to the latest value.
*/
const editMode = api.viewMode.value === 'edit';
const editMode = getInheritedViewMode(api) === 'edit';
const [hideTitle, setHideTitle] = useState(api.hidePanelTitle?.value);
const [panelDescription, setPanelDescription] = useState(
api.panelDescription?.value ?? api.defaultPanelDescription?.value

View file

@ -22,7 +22,7 @@ export const openCustomizePanelFlyout = ({
api: CustomizePanelActionApi;
}) => {
// send the overlay ref to the parent if it is capable of tracking overlays
const parent = api.parentApi?.value;
const parent = api.parentApi;
const overlayTracker = tracksOverlays(parent) ? parent : undefined;
const { Provider: KibanaReactContextProvider } = createKibanaReactContext({

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { ViewMode } from '@kbn/presentation-publishing';
import { PublishesViewMode, ViewMode } from '@kbn/presentation-publishing';
import { BehaviorSubject } from 'rxjs';
import { EditPanelAction, EditPanelActionApi } from './edit_panel_action';
@ -42,7 +42,7 @@ describe('Edit panel action', () => {
});
it('is incompatible when view mode is view', async () => {
context.embeddable.viewMode = new BehaviorSubject<ViewMode>('view');
(context.embeddable as PublishesViewMode).viewMode = new BehaviorSubject<ViewMode>('view');
expect(await action.isCompatible(context)).toBe(false);
});

View file

@ -9,11 +9,13 @@
import { i18n } from '@kbn/i18n';
import {
apiPublishesViewMode,
hasEditCapabilities,
HasEditCapabilities,
EmbeddableApiContext,
PublishesViewMode,
CanAccessViewMode,
apiCanAccessViewMode,
getInheritedViewMode,
getViewModeSubject,
} from '@kbn/presentation-publishing';
import {
Action,
@ -23,10 +25,10 @@ import {
export const ACTION_EDIT_PANEL = 'editPanel';
export type EditPanelActionApi = PublishesViewMode & HasEditCapabilities;
export type EditPanelActionApi = CanAccessViewMode & HasEditCapabilities;
const isApiCompatible = (api: unknown | null): api is EditPanelActionApi => {
return hasEditCapabilities(api) && apiPublishesViewMode(api);
return hasEditCapabilities(api) && apiCanAccessViewMode(api);
};
export class EditPanelAction
@ -53,7 +55,7 @@ export class EditPanelAction
onChange: (isCompatible: boolean, action: Action<EmbeddableApiContext>) => void
) {
if (!isApiCompatible(embeddable)) return;
return embeddable.viewMode.subscribe((viewMode) => {
return getViewModeSubject(embeddable)?.subscribe((viewMode) => {
if (viewMode === 'edit' && isApiCompatible(embeddable) && embeddable.isEditingEnabled()) {
onChange(true, this);
return;
@ -77,7 +79,7 @@ export class EditPanelAction
public async isCompatible({ embeddable }: EmbeddableApiContext) {
if (!isApiCompatible(embeddable) || !embeddable.isEditingEnabled()) return false;
return embeddable.viewMode.value === 'edit';
return getInheritedViewMode(embeddable) === 'edit';
}
public async execute({ embeddable }: EmbeddableApiContext) {

View file

@ -15,7 +15,6 @@
*/
import { TracksOverlays } from '@kbn/presentation-containers';
import { BehaviorSubject } from 'rxjs';
import { inspector } from '../../kibana_services';
import { InspectPanelActionApi, InspectPanelAction } from './inspect_panel_action';
@ -74,11 +73,11 @@ describe('Inspect panel action', () => {
it('opens overlay on parent if parent is an overlay tracker', async () => {
inspector.open = jest.fn().mockReturnValue({ onClose: Promise.resolve(undefined) });
context.embeddable.parentApi = new BehaviorSubject<unknown>({
context.embeddable.parentApi = {
openOverlay: jest.fn(),
clearOverlays: jest.fn(),
});
};
await action.execute(context);
expect((context.embeddable.parentApi.value as TracksOverlays).openOverlay).toHaveBeenCalled();
expect((context.embeddable.parentApi as TracksOverlays).openOverlay).toHaveBeenCalled();
});
});

View file

@ -12,7 +12,7 @@ import { tracksOverlays } from '@kbn/presentation-containers';
import {
EmbeddableApiContext,
PublishesPanelTitle,
PublishesParentApi,
HasParentApi,
} from '@kbn/presentation-publishing';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { inspector } from '../../kibana_services';
@ -20,7 +20,7 @@ import { inspector } from '../../kibana_services';
export const ACTION_INSPECT_PANEL = 'openInspector';
export type InspectPanelActionApi = HasInspectorAdapters &
Partial<PublishesPanelTitle & PublishesParentApi>;
Partial<PublishesPanelTitle & HasParentApi>;
const isApiCompatible = (api: unknown | null): api is InspectPanelActionApi => {
return Boolean(api) && apiHasInspectorAdapters(api);
};
@ -67,11 +67,10 @@ export class InspectPanelAction implements Action<EmbeddableApiContext> {
},
});
session.onClose.finally(() => {
if (tracksOverlays(embeddable.parentApi?.value)) embeddable.parentApi?.value.clearOverlays();
if (tracksOverlays(embeddable.parentApi)) embeddable.parentApi.clearOverlays();
});
// send the overlay ref to the parent API if it is capable of tracking overlays
if (tracksOverlays(embeddable.parentApi?.value))
embeddable.parentApi?.value.openOverlay(session);
if (tracksOverlays(embeddable.parentApi)) embeddable.parentApi?.openOverlay(session);
}
}

View file

@ -6,7 +6,6 @@
* Side Public License, v 1.
*/
import { PresentationContainer } from '@kbn/presentation-containers';
import { ViewMode } from '@kbn/presentation-publishing';
import { BehaviorSubject } from 'rxjs';
import { RemovePanelAction, RemovePanelActionApi } from './remove_panel_action';
@ -19,13 +18,13 @@ describe('Remove panel action', () => {
action = new RemovePanelAction();
context = {
embeddable: {
uuid: new BehaviorSubject<string>('superId'),
uuid: 'superId',
viewMode: new BehaviorSubject<ViewMode>('edit'),
parentApi: new BehaviorSubject<PresentationContainer>({
parentApi: {
removePanel: jest.fn(),
canRemovePanels: jest.fn().mockReturnValue(true),
replacePanel: jest.fn(),
}),
},
},
};
});
@ -48,6 +47,6 @@ describe('Remove panel action', () => {
it('calls the parent removePanel method on execute', async () => {
action.execute(context);
expect(context.embeddable.parentApi.value.removePanel).toHaveBeenCalled();
expect(context.embeddable.parentApi.removePanel).toHaveBeenCalled();
});
});

View file

@ -8,11 +8,12 @@
import { i18n } from '@kbn/i18n';
import {
apiPublishesUniqueId,
apiPublishesViewMode,
apiCanAccessViewMode,
apiHasUniqueId,
EmbeddableApiContext,
PublishesUniqueId,
PublishesParentApi,
getInheritedViewMode,
HasParentApi,
HasUniqueId,
PublishesViewMode,
} from '@kbn/presentation-publishing';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
@ -22,11 +23,11 @@ import { getContainerParentFromAPI, PresentationContainer } from '@kbn/presentat
export const ACTION_REMOVE_PANEL = 'deletePanel';
export type RemovePanelActionApi = PublishesViewMode &
PublishesUniqueId &
PublishesParentApi<PresentationContainer>;
HasUniqueId &
HasParentApi<PresentationContainer>;
const isApiCompatible = (api: unknown | null): api is RemovePanelActionApi =>
Boolean(apiPublishesUniqueId(api) && apiPublishesViewMode(api) && getContainerParentFromAPI(api));
Boolean(apiHasUniqueId(api) && apiCanAccessViewMode(api) && getContainerParentFromAPI(api));
export class RemovePanelAction implements Action<EmbeddableApiContext> {
public readonly type = ACTION_REMOVE_PANEL;
@ -50,12 +51,12 @@ export class RemovePanelAction implements Action<EmbeddableApiContext> {
// any parent can disallow panel removal by implementing canRemovePanels. If this method
// is not implemented, panel removal is always allowed.
const parentAllowsPanelRemoval = embeddable.parentApi.value.canRemovePanels?.() ?? true;
return Boolean(embeddable.viewMode.value === 'edit' && parentAllowsPanelRemoval);
const parentAllowsPanelRemoval = embeddable.parentApi.canRemovePanels?.() ?? true;
return Boolean(getInheritedViewMode(embeddable) === 'edit' && parentAllowsPanelRemoval);
}
public async execute({ embeddable }: EmbeddableApiContext) {
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
embeddable.parentApi?.value.removePanel(embeddable.uuid.value);
embeddable.parentApi?.removePanel(embeddable.uuid);
}
}

View file

@ -21,7 +21,7 @@ import {
} from '@elastic/eui';
import { Action, buildContextMenuForActions } from '@kbn/ui-actions-plugin/public';
import { PublishesViewMode, useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import { getViewModeSubject, useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import { uiActions } from '../../kibana_services';
import { contextMenuTrigger, CONTEXT_MENU_TRIGGER } from '../../panel_actions';
import { getContextMenuAriaLabel } from '../presentation_panel_strings';
@ -44,7 +44,7 @@ export const PresentationPanelContextMenu = ({
const [contextMenuPanels, setContextMenuPanels] = useState<EuiContextMenuPanelDescriptor[]>([]);
const { title, parentViewMode } = useBatchedPublishingSubjects({
title: api?.panelTitle,
title: api.panelTitle,
/**
* View mode changes often have the biggest influence over which actions will be compatible,
@ -52,7 +52,7 @@ export const PresentationPanelContextMenu = ({
* actions should eventually all be Frequent Compatibility Change Actions which can track their
* own dependencies.
*/
parentViewMode: (api?.parentApi?.value as Partial<PublishesViewMode>)?.viewMode,
parentViewMode: getViewModeSubject(api),
});
useEffect(() => {

View file

@ -38,7 +38,7 @@ export const PresentationPanelTitle = ({
embPanel__placeholderTitleText: !panelTitle,
});
if (viewMode !== 'edit' && isApiCompatibleWithCustomizePanelAction(api)) {
if (viewMode !== 'edit' || !isApiCompatibleWithCustomizePanelAction(api)) {
return <span className={titleClassNames}>{panelTitle}</span>;
}

View file

@ -31,7 +31,10 @@ export const PresentationPanelError = ({
() => (isEditable ? () => editPanelAction?.execute({ embeddable: api }) : undefined),
[api, isEditable]
);
const label = useMemo(() => editPanelAction?.getDisplayName({ embeddable: api }), [api]);
const label = useMemo(
() => (isEditable ? editPanelAction?.getDisplayName({ embeddable: api }) : ''),
[api, isEditable]
);
const panelTitle = usePanelTitle(api);
const ariaLabel = useMemo(

View file

@ -8,7 +8,7 @@
import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';
import { DataView } from '@kbn/data-views-plugin/common';
import { PublishesDataViews, ViewMode } from '@kbn/presentation-publishing';
import { PublishesDataViews, PublishesViewMode, ViewMode } from '@kbn/presentation-publishing';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
@ -153,7 +153,7 @@ describe('Presentation panel', () => {
});
it('does not render a title when in view mode when the provided title is blank', async () => {
const api: DefaultPresentationPanelApi = {
const api: DefaultPresentationPanelApi & PublishesViewMode = {
panelTitle: new BehaviorSubject<string | undefined>(''),
viewMode: new BehaviorSubject<ViewMode>('view'),
};
@ -164,9 +164,10 @@ describe('Presentation panel', () => {
});
it('renders a placeholder title when in edit mode and the provided title is blank', async () => {
const api: DefaultPresentationPanelApi = {
const api: DefaultPresentationPanelApi & PublishesDataViews & PublishesViewMode = {
panelTitle: new BehaviorSubject<string | undefined>(''),
viewMode: new BehaviorSubject<ViewMode>('edit'),
dataViews: new BehaviorSubject<DataView[] | undefined>([]),
};
await renderPresentationPanel({ api });
await waitFor(() => {
@ -177,7 +178,7 @@ describe('Presentation panel', () => {
it('opens customize panel flyout on title click when in edit mode', async () => {
const spy = jest.spyOn(openCustomizePanel, 'openCustomizePanelFlyout');
const api: DefaultPresentationPanelApi & PublishesDataViews = {
const api: DefaultPresentationPanelApi & PublishesDataViews & PublishesViewMode = {
panelTitle: new BehaviorSubject<string | undefined>('TITLE'),
viewMode: new BehaviorSubject<ViewMode>('edit'),
dataViews: new BehaviorSubject<DataView[] | undefined>([]),
@ -193,7 +194,7 @@ describe('Presentation panel', () => {
});
it('does not show title customize link in view mode', async () => {
const api: DefaultPresentationPanelApi & PublishesDataViews = {
const api: DefaultPresentationPanelApi & PublishesDataViews & PublishesViewMode = {
panelTitle: new BehaviorSubject<string | undefined>('SUPER TITLE'),
viewMode: new BehaviorSubject<ViewMode>('view'),
dataViews: new BehaviorSubject<DataView[] | undefined>([]),
@ -206,7 +207,7 @@ describe('Presentation panel', () => {
});
it('hides title when API hide title option is true', async () => {
const api: DefaultPresentationPanelApi = {
const api: DefaultPresentationPanelApi & PublishesViewMode = {
panelTitle: new BehaviorSubject<string | undefined>('SUPER TITLE'),
hidePanelTitle: new BehaviorSubject<boolean | undefined>(true),
viewMode: new BehaviorSubject<ViewMode>('view'),
@ -216,12 +217,14 @@ describe('Presentation panel', () => {
});
it('hides title when parent hide title option is true', async () => {
const api: DefaultPresentationPanelApi = {
const api: DefaultPresentationPanelApi & PublishesViewMode = {
panelTitle: new BehaviorSubject<string | undefined>('SUPER TITLE'),
viewMode: new BehaviorSubject<ViewMode>('view'),
parentApi: new BehaviorSubject<unknown>({
hidePanelTitle: new BehaviorSubject<boolean | undefined>(true),
}),
parentApi: {
removePanel: jest.fn(),
replacePanel: jest.fn(),
viewMode: new BehaviorSubject<ViewMode>('view'),
},
};
await renderPresentationPanel({ api });
expect(screen.queryByTestId('presentationPanelTitle')).not.toBeInTheDocument();

View file

@ -7,12 +7,16 @@
*/
import { EuiFlexGroup, EuiPanel, htmlIdGenerator } from '@elastic/eui';
import { PanelLoader } from '@kbn/panel-loader';
import {
apiFiresPhaseEvents,
apiHasParentApi,
apiPublishesViewMode,
useBatchedPublishingSubjects,
} from '@kbn/presentation-publishing';
import classNames from 'classnames';
import React, { useEffect, useMemo, useState } from 'react';
import { Subscription } from 'rxjs';
import { PanelLoader } from '@kbn/panel-loader';
import { apiFiresPhaseEvents, useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import { PresentationPanelHeader } from './panel_header/presentation_panel_header';
import { PresentationPanelError } from './presentation_panel_error';
import { DefaultPresentationPanelApi, PresentationPanelInternalProps } from './types';
@ -38,9 +42,13 @@ export const PresentationPanelInternal = <
const [api, setApi] = useState<ApiType | null>(null);
const headerId = useMemo(() => htmlIdGenerator()(), []);
const viewModeSubject = (() => {
if (apiPublishesViewMode(api)) return api.viewMode;
if (apiHasParentApi(api) && apiPublishesViewMode(api.parentApi)) return api.parentApi.viewMode;
})();
const {
uuid,
viewMode,
rawViewMode,
blockingError,
panelTitle,
dataLoading,
@ -51,18 +59,19 @@ export const PresentationPanelInternal = <
} = useBatchedPublishingSubjects({
dataLoading: api?.dataLoading,
blockingError: api?.blockingError,
viewMode: api?.viewMode,
uuid: api?.uuid,
panelTitle: api?.panelTitle,
hidePanelTitle: api?.hidePanelTitle,
panelDescription: api?.panelDescription,
defaultPanelTitle: api?.defaultPanelTitle,
parentHidePanelTitle: (api?.parentApi?.value as DefaultPresentationPanelApi)?.hidePanelTitle,
rawViewMode: viewModeSubject,
parentHidePanelTitle: api?.parentApi?.hidePanelTitle,
});
const viewMode = rawViewMode ?? 'view';
const [initialLoadComplete, setInitialLoadComplete] = useState(!dataLoading);
if (dataLoading === false && !initialLoadComplete) {
if (!initialLoadComplete && (dataLoading === false || (api && !api.dataLoading))) {
setInitialLoadComplete(true);
}
@ -91,11 +100,11 @@ export const PresentationPanelInternal = <
role="figure"
paddingSize="none"
className={classNames('embPanel', {
'embPanel--editing': viewMode !== 'view',
'embPanel--editing': viewMode === 'edit',
})}
hasShadow={showShadow}
aria-labelledby={headerId}
data-test-embeddable-id={uuid}
data-test-embeddable-id={api?.uuid}
data-test-subj="embeddablePanel"
>
{!hideHeader && api && (

View file

@ -6,15 +6,16 @@
* Side Public License, v 1.
*/
import { PresentationContainer } from '@kbn/presentation-containers';
import {
PhaseEvent,
PublishesDataLoading,
PublishesDisabledActionIds,
PublishesBlockingError,
PublishesUniqueId,
HasUniqueId,
PublishesPanelDescription,
PublishesPanelTitle,
PublishesParentApi,
HasParentApi,
PublishesViewMode,
} from '@kbn/presentation-publishing';
import { UiActionsService } from '@kbn/ui-actions-plugin/public';
@ -56,14 +57,16 @@ export interface PresentationPanelInternalProps<
}
export type DefaultPresentationPanelApi = Partial<
PublishesUniqueId &
PublishesParentApi &
PublishesDataLoading &
PublishesViewMode &
PublishesBlockingError &
HasUniqueId &
PublishesPanelTitle &
PublishesDataLoading &
PublishesBlockingError &
PublishesPanelDescription &
PublishesDisabledActionIds
PublishesDisabledActionIds &
HasParentApi<
PresentationContainer &
Partial<Pick<PublishesPanelTitle, 'hidePanelTitle'> & PublishesViewMode>
>
>;
export type PresentationPanelProps<