[embeddable] make presentation interface names consistent (#205279)

PR cleans up presentation interface names for consistentency
* adds `$` suffix to all observables. For example, `dataLoading` =>
`dataLoading$`
* removes `Panel` naming convention from interface names since an api
may not be a panel, an api may be a dashboard. For example,
`PublisesPanelTitle` => `PublishesTitle`

#### Note to Reviewers
Pay special attention to any place where your application creates an
untyped API. In the example below, there is no typescript violation when
the parent returns `dataLoading` instead of `dataLoading$` since the
parent is not typed as `PublishesDataLoading`. Please check for
instances like these.

```
<ReactEmbeddableRenderer
  getParentApi={() => {
    dataLoading: new BehaviorSubject()
  }}
/>
```

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2025-01-22 09:46:01 -07:00 committed by GitHub
parent 33145379e5
commit 05916056cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
229 changed files with 1263 additions and 1297 deletions

View file

@ -70,7 +70,7 @@ export const EditExample = () => {
INPUT_KEY,
JSON.stringify({
...controlGroupAPI.snapshotRuntimeState(),
disabledActions: controlGroupAPI.disabledActionIds.getValue(), // not part of runtime
disabledActions: controlGroupAPI.disabledActionIds$.getValue(), // not part of runtime
})
);

View file

@ -119,9 +119,9 @@ export const ReactControlExample = ({
const children$ = new BehaviorSubject<{ [key: string]: unknown }>({});
return {
dataLoading: dataLoading$,
dataLoading$,
unifiedSearchFilters$,
viewMode: viewMode$,
viewMode$,
filters$,
query$,
timeRange$,
@ -149,7 +149,7 @@ export const ReactControlExample = ({
useEffect(() => {
const subscription = combineCompatibleChildrenApis<PublishesDataLoading, boolean | undefined>(
dashboardApi,
'dataLoading',
'dataLoading$',
apiPublishesDataLoading,
undefined,
// flatten method
@ -249,7 +249,7 @@ export const ReactControlExample = ({
if (!controlGroupApi) {
return;
}
const subscription = controlGroupApi.unsavedChanges.subscribe((nextUnsavedChanges) => {
const subscription = controlGroupApi.unsavedChanges$.subscribe((nextUnsavedChanges) => {
if (!nextUnsavedChanges) {
clearControlGroupRuntimeState();
setUnsavedChanges(undefined);

View file

@ -37,7 +37,7 @@ export const PresentationContainerExample = ({ uiActions }: { uiActions: UiActio
}, [cleanUp]);
const [dataLoading, panels, timeRange] = useBatchedPublishingSubjects(
pageApi.dataLoading,
pageApi.dataLoading$,
componentApi.panels$,
pageApi.timeRange$
);
@ -95,7 +95,7 @@ export const PresentationContainerExample = ({ uiActions }: { uiActions: UiActio
<TopNav
onSave={componentApi.onSave}
resetUnsavedChanges={pageApi.resetUnsavedChanges}
unsavedChanges$={pageApi.unsavedChanges}
unsavedChanges$={pageApi.unsavedChanges$}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -15,7 +15,7 @@ import { PublishesUnsavedChanges } from '@kbn/presentation-publishing';
interface Props {
onSave: () => Promise<void>;
resetUnsavedChanges: () => void;
unsavedChanges$: PublishesUnsavedChanges['unsavedChanges'];
unsavedChanges$: PublishesUnsavedChanges['unsavedChanges$'];
}
export function TopNav(props: Props) {

View file

@ -81,7 +81,7 @@ export function getPageApi() {
boolean | undefined
>(
{ children$ },
'dataLoading',
'dataLoading$',
apiPublishesDataLoading,
undefined,
// flatten method
@ -193,7 +193,7 @@ export function getPageApi() {
},
canRemovePanels: () => true,
children$,
dataLoading: dataLoading$,
dataLoading$,
executionContext: {
type: 'presentationContainerEmbeddableExample',
},
@ -210,7 +210,7 @@ export function getPageApi() {
children$.next(omit(children$.value, id));
},
saveNotification$,
viewMode: new BehaviorSubject<ViewMode>('edit'),
viewMode$: new BehaviorSubject<ViewMode>('edit'),
/**
* return last saved embeddable state
*/
@ -252,7 +252,7 @@ export function getPageApi() {
return true;
},
timeRange$,
unsavedChanges: unsavedChanges$ as PublishingSubject<object | undefined>,
unsavedChanges$: unsavedChanges$ as PublishingSubject<object | undefined>,
} as PageApi,
};
}

View file

@ -47,7 +47,7 @@ export const RenderExamples = () => {
const [api, setApi] = useState<SearchApi | null>(null);
const [hidePanelChrome, setHidePanelChrome] = useState<boolean>(false);
const [dataLoading, timeRange] = useBatchedOptionalPublishingSubjects(
api?.dataLoading,
api?.dataLoading$,
parentApi.timeRange$
);

View file

@ -40,10 +40,10 @@ export const StateManagementExample = ({ uiActions }: { uiActions: UiActionsStar
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
useEffect(() => {
if (!bookApi || !bookApi.unsavedChanges) {
if (!bookApi || !bookApi.unsavedChanges$) {
return;
}
const subscription = bookApi.unsavedChanges.subscribe((unsavedChanges) => {
const subscription = bookApi.unsavedChanges$.subscribe((unsavedChanges) => {
setHasUnsavedChanges(unsavedChanges !== undefined);
unsavedChangesSessionStorage.save(unsavedChanges ?? {});
});
@ -158,7 +158,7 @@ export const StateManagementExample = ({ uiActions }: { uiActions: UiActionsStar
return unsavedChangesSessionStorage.load();
},
saveNotification$,
viewMode: new BehaviorSubject<ViewMode>('edit'),
viewMode$: new BehaviorSubject<ViewMode>('edit'),
};
}}
onApiAvailable={(api) => {

View file

@ -60,7 +60,7 @@ export const initializeDataTableQueries = async (
dataView$.next(defaultDataView);
return;
}
const dataViewSubscription = dataViewProvider.dataViews.subscribe((dataViews) => {
const dataViewSubscription = dataViewProvider.dataViews$.subscribe((dataViews) => {
dataView$.next(dataViews?.[0] ?? defaultDataView);
});
return () => dataViewSubscription.unsubscribe();

View file

@ -17,7 +17,7 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import {
initializeTimeRange,
initializeTitles,
initializeTitleManager,
useBatchedPublishingSubjects,
} from '@kbn/presentation-publishing';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
@ -40,8 +40,8 @@ export const getDataTableFactory = (
buildEmbeddable: async (state, buildApi, uuid, parentApi) => {
const storage = new Storage(localStorage);
const timeRange = initializeTimeRange(state);
const queryLoading$ = new BehaviorSubject<boolean | undefined>(true);
const { titlesApi, titleComparators, serializeTitles } = initializeTitles(state);
const dataLoading$ = new BehaviorSubject<boolean | undefined>(true);
const titleManager = initializeTitleManager(state);
const allServices: UnifiedDataTableProps['services'] = {
...services,
storage,
@ -53,18 +53,18 @@ export const getDataTableFactory = (
const api = buildApi(
{
...timeRange.api,
...titlesApi,
dataLoading: queryLoading$,
...titleManager.api,
dataLoading$,
serializeState: () => {
return {
rawState: { ...serializeTitles(), ...timeRange.serialize() },
rawState: { ...titleManager.serialize(), ...timeRange.serialize() },
};
},
},
{ ...titleComparators, ...timeRange.comparators }
{ ...titleManager.comparators, ...timeRange.comparators }
);
const queryService = await initializeDataTableQueries(services, api, queryLoading$);
const queryService = await initializeDataTableQueries(services, api, dataLoading$);
// Create the React Embeddable component
return {
@ -74,7 +74,7 @@ export const getDataTableFactory = (
const [fields, rows, loading, dataView] = useBatchedPublishingSubjects(
queryService.fields$,
queryService.rows$,
queryLoading$,
dataLoading$,
queryService.dataView$
);

View file

@ -12,7 +12,7 @@ import { css } from '@emotion/react';
import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public';
import { i18n } from '@kbn/i18n';
import {
initializeTitles,
initializeTitleManager,
useInheritedViewMode,
useStateFromPublishingSubject,
} from '@kbn/presentation-publishing';
@ -40,7 +40,7 @@ export const markdownEmbeddableFactory: ReactEmbeddableFactory<
/**
* initialize state (source of truth)
*/
const { titlesApi, titleComparators, serializeTitles } = initializeTitles(state);
const titleManager = initializeTitleManager(state);
const content$ = new BehaviorSubject(state.content);
/**
@ -50,11 +50,11 @@ export const markdownEmbeddableFactory: ReactEmbeddableFactory<
*/
const api = buildApi(
{
...titlesApi,
...titleManager.api,
serializeState: () => {
return {
rawState: {
...serializeTitles(),
...titleManager.serialize(),
content: content$.getValue(),
},
};
@ -69,7 +69,7 @@ export const markdownEmbeddableFactory: ReactEmbeddableFactory<
*/
{
content: [content$, (value) => content$.next(value)],
...titleComparators,
...titleManager.comparators,
}
);

View file

@ -15,7 +15,7 @@ import { DataView } from '@kbn/data-views-plugin/common';
import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/public';
import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public';
import { i18n } from '@kbn/i18n';
import { initializeTitles, useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import { initializeTitleManager, useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import { LazyDataViewPicker, withSuspense } from '@kbn/presentation-util-plugin/public';
import {
UnifiedFieldListSidebarContainer,
@ -69,7 +69,7 @@ export const getFieldListFactory = (
},
buildEmbeddable: async (initialState, buildApi) => {
const subscriptions = new Subscription();
const { titlesApi, titleComparators, serializeTitles } = initializeTitles(initialState);
const titleManager = initializeTitleManager(initialState);
// set up data views
const [allDataViews, defaultDataViewId] = await Promise.all([
@ -105,8 +105,8 @@ export const getFieldListFactory = (
const api = buildApi(
{
...titlesApi,
dataViews: dataViews$,
...titleManager.api,
dataViews$,
selectedFields: selectedFieldNames$,
serializeState: () => {
const dataViewId = selectedDataViewId$.getValue();
@ -121,7 +121,7 @@ export const getFieldListFactory = (
: [];
return {
rawState: {
...serializeTitles(),
...titleManager.serialize(),
// here we skip serializing the dataViewId, because the reference contains that information.
selectedFieldNames: selectedFieldNames$.getValue(),
},
@ -130,7 +130,7 @@ export const getFieldListFactory = (
},
},
{
...titleComparators,
...titleManager.comparators,
dataViewId: [selectedDataViewId$, (value) => selectedDataViewId$.next(value)],
selectedFieldNames: [
selectedFieldNames$,

View file

@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n';
import {
apiHasParentApi,
getUnchangingComparator,
initializeTitles,
initializeTitleManager,
SerializedTitles,
SerializedPanelState,
useBatchedPublishingSubjects,
@ -81,7 +81,7 @@ export const getSavedBookEmbeddableFactory = (core: CoreStart) => {
};
},
buildEmbeddable: async (state, buildApi) => {
const { titlesApi, titleComparators, serializeTitles } = initializeTitles(state);
const titleManager = initializeTitleManager(state);
const bookAttributesManager = stateManagerFromAttributes(state);
const isByReference = Boolean(state.savedBookId);
@ -90,21 +90,21 @@ export const getSavedBookEmbeddableFactory = (core: CoreStart) => {
// if this book is currently by reference, we serialize the reference only.
const bookByReferenceState: BookByReferenceSerializedState = {
savedBookId: newId ?? state.savedBookId!,
...serializeTitles(),
...titleManager.serialize(),
};
return { rawState: bookByReferenceState };
}
// if this book is currently by value, we serialize the entire state.
const bookByValueState: BookByValueSerializedState = {
attributes: serializeBookAttributes(bookAttributesManager),
...serializeTitles(),
...titleManager.serialize(),
};
return { rawState: bookByValueState };
};
const api = buildApi(
{
...titlesApi,
...titleManager.api,
onEdit: async () => {
openSavedBookEditor({
attributesManager: bookAttributesManager,
@ -152,7 +152,7 @@ export const getSavedBookEmbeddableFactory = (core: CoreStart) => {
{
savedBookId: getUnchangingComparator(), // saved book id will not change over the lifetime of the embeddable.
...bookAttributesManager.comparators,
...titleComparators,
...titleManager.comparators,
}
);

View file

@ -49,9 +49,9 @@ export const getSearchEmbeddableFactory = (services: Services) => {
const api = buildApi(
{
...timeRange.api,
blockingError: blockingError$,
dataViews: dataViews$,
dataLoading: dataLoading$,
blockingError$,
dataViews$,
dataLoading$,
serializeState: () => {
return {
rawState: {

View file

@ -72,8 +72,8 @@ export const GridExample = ({
const mockDashboardApi = useMockDashboardApi({ savedState: savedState.current });
const [viewMode, expandedPanelId] = useBatchedPublishingSubjects(
mockDashboardApi.viewMode,
mockDashboardApi.expandedPanelId
mockDashboardApi.viewMode$,
mockDashboardApi.expandedPanelId$
);
useEffect(() => {
@ -244,7 +244,7 @@ export const GridExample = ({
]}
idSelected={viewMode}
onChange={(id) => {
mockDashboardApi.viewMode.next(id);
mockDashboardApi.viewMode$.next(id);
}}
/>
</EuiFormRow>

View file

@ -48,10 +48,10 @@ export const useMockDashboardApi = ({
}),
filters$: new BehaviorSubject([]),
query$: new BehaviorSubject(''),
viewMode: new BehaviorSubject('edit'),
viewMode$: new BehaviorSubject('edit'),
panels$,
rows$: new BehaviorSubject<MockedDashboardRowMap>(savedState.rows),
expandedPanelId: expandedPanelId$,
expandedPanelId$,
expandPanel: (id: string) => {
if (expandedPanelId$.getValue()) {
expandedPanelId$.next(undefined);

View file

@ -27,7 +27,7 @@ export const DualDashboardsExample = () => {
const [secondDashboardApi, setSecondDashboardApi] = useState<DashboardApi | undefined>();
const ButtonControls = ({ dashboardApi }: { dashboardApi: DashboardApi }) => {
const viewMode = useStateFromPublishingSubject(dashboardApi.viewMode);
const viewMode = useStateFromPublishingSubject(dashboardApi.viewMode$);
return (
<EuiButtonGroup

View file

@ -104,7 +104,7 @@ describe('GetCsvReportPanelAction', () => {
}),
hasTimeRange: () => true,
parentApi: {
viewMode: new BehaviorSubject('view'),
viewMode$: new BehaviorSubject('view'),
},
},
} as EmbeddableApiContext;

View file

@ -21,7 +21,7 @@ export const apiCanDuplicatePanels = (
export interface CanExpandPanels {
expandPanel: (panelId: string) => void;
expandedPanelId: PublishingSubject<string | undefined>;
expandedPanelId$: PublishingSubject<string | undefined>;
}
export const apiCanExpandPanels = (unknownApi: unknown | null): unknownApi is CanExpandPanels => {

View file

@ -13,11 +13,11 @@ import { waitFor } from '@testing-library/react';
describe('childrenUnsavedChanges$', () => {
const child1Api = {
unsavedChanges: new BehaviorSubject<object | undefined>(undefined),
unsavedChanges$: new BehaviorSubject<object | undefined>(undefined),
resetUnsavedChanges: () => true,
};
const child2Api = {
unsavedChanges: new BehaviorSubject<object | undefined>(undefined),
unsavedChanges$: new BehaviorSubject<object | undefined>(undefined),
resetUnsavedChanges: () => true,
};
const children$ = new BehaviorSubject<{ [key: string]: unknown }>({});
@ -25,8 +25,8 @@ describe('childrenUnsavedChanges$', () => {
beforeEach(() => {
onFireMock.mockReset();
child1Api.unsavedChanges.next(undefined);
child2Api.unsavedChanges.next(undefined);
child1Api.unsavedChanges$.next(undefined);
child2Api.unsavedChanges$.next(undefined);
children$.next({
child1: child1Api,
child2: child2Api,
@ -61,7 +61,7 @@ describe('childrenUnsavedChanges$', () => {
}
);
child1Api.unsavedChanges.next({
child1Api.unsavedChanges$.next({
key1: 'modified value',
});
@ -98,7 +98,7 @@ describe('childrenUnsavedChanges$', () => {
children$.next({
...children$.value,
child3: {
unsavedChanges: new BehaviorSubject<object | undefined>({ key1: 'modified value' }),
unsavedChanges$: new BehaviorSubject<object | undefined>({ key1: 'modified value' }),
resetUnsavedChanges: () => true,
},
});

View file

@ -33,7 +33,7 @@ export function childrenUnsavedChanges$(children$: PresentationContainer['childr
? of([])
: combineLatest(
childrenThatPublishUnsavedChanges.map(([childId, child]) =>
child.unsavedChanges.pipe(map((unsavedChanges) => ({ childId, unsavedChanges })))
child.unsavedChanges$.pipe(map((unsavedChanges) => ({ childId, unsavedChanges })))
)
);
}),

View file

@ -43,14 +43,14 @@ describe('unsavedChanges api', () => {
});
test('should have no unsaved changes after initialization', () => {
expect(api?.unsavedChanges.value).toBeUndefined();
expect(api?.unsavedChanges$.value).toBeUndefined();
});
test('should have unsaved changes when state changes', async () => {
key1$.next('modified key1 value');
await waitFor(
() =>
expect(api?.unsavedChanges.value).toEqual({
expect(api?.unsavedChanges$.value).toEqual({
key1: 'modified key1 value',
}),
{
@ -61,28 +61,28 @@ describe('unsavedChanges api', () => {
test('should have no unsaved changes after save', async () => {
key1$.next('modified key1 value');
await waitFor(() => expect(api?.unsavedChanges.value).not.toBeUndefined(), {
await waitFor(() => expect(api?.unsavedChanges$.value).not.toBeUndefined(), {
interval: COMPARATOR_SUBJECTS_DEBOUNCE + 1,
});
// trigger save
parentApi.saveNotification$.next();
await waitFor(() => expect(api?.unsavedChanges.value).toBeUndefined(), {
await waitFor(() => expect(api?.unsavedChanges$.value).toBeUndefined(), {
interval: COMPARATOR_SUBJECTS_DEBOUNCE + 1,
});
});
test('should have no unsaved changes after reset', async () => {
key1$.next('modified key1 value');
await waitFor(() => expect(api?.unsavedChanges.value).not.toBeUndefined(), {
await waitFor(() => expect(api?.unsavedChanges$.value).not.toBeUndefined(), {
interval: COMPARATOR_SUBJECTS_DEBOUNCE + 1,
});
// trigger reset
api?.resetUnsavedChanges();
await waitFor(() => expect(api?.unsavedChanges.value).toBeUndefined(), {
await waitFor(() => expect(api?.unsavedChanges$.value).toBeUndefined(), {
interval: COMPARATOR_SUBJECTS_DEBOUNCE + 1,
});
});

View file

@ -62,7 +62,7 @@ export const initializeUnsavedChanges = <RuntimeState extends {} = {}>(
comparatorKeys.push(key);
}
const unsavedChanges = new BehaviorSubject<Partial<RuntimeState> | undefined>(
const unsavedChanges$ = new BehaviorSubject<Partial<RuntimeState> | undefined>(
runComparators(
comparators,
comparatorKeys,
@ -84,7 +84,7 @@ export const initializeUnsavedChanges = <RuntimeState extends {} = {}>(
combineLatestWith(lastSavedState$)
)
.subscribe(([latestState, lastSavedState]) => {
unsavedChanges.next(
unsavedChanges$.next(
runComparators(comparators, comparatorKeys, lastSavedState, latestState)
);
})
@ -92,7 +92,7 @@ export const initializeUnsavedChanges = <RuntimeState extends {} = {}>(
return {
api: {
unsavedChanges,
unsavedChanges$,
resetUnsavedChanges: () => {
const lastSaved = lastSavedState$.getValue();

View file

@ -127,24 +127,25 @@ export {
type ViewMode,
} from './interfaces/publishes_view_mode';
export {
apiPublishesPanelDescription,
apiPublishesWritablePanelDescription,
getPanelDescription,
type PublishesPanelDescription,
type PublishesWritablePanelDescription,
} from './interfaces/titles/publishes_panel_description';
apiPublishesDescription,
apiPublishesWritableDescription,
getDescription,
type PublishesDescription,
type PublishesWritableDescription,
} from './interfaces/titles/publishes_description';
export {
apiPublishesPanelTitle,
apiPublishesWritablePanelTitle,
getPanelTitle,
type PublishesPanelTitle,
type PublishesWritablePanelTitle,
} from './interfaces/titles/publishes_panel_title';
apiPublishesTitle,
apiPublishesWritableTitle,
getTitle,
type PublishesTitle,
type PublishesWritableTitle,
} from './interfaces/titles/publishes_title';
export {
initializeTitles,
initializeTitleManager,
stateHasTitles,
type TitlesApi,
type SerializedTitles,
} from './interfaces/titles/titles_api';
} from './interfaces/titles/title_manager';
export {
useBatchedOptionalPublishingSubjects,
useBatchedPublishingSubjects,

View file

@ -30,16 +30,16 @@ export const apiCanAccessViewMode = (api: unknown): api is CanAccessViewMode =>
* 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 (apiPublishesViewMode(api)) return api.viewMode$.getValue();
if (apiHasParentApi(api) && apiPublishesViewMode(api.parentApi)) {
return api.parentApi.viewMode.getValue();
return api.parentApi.viewMode$.getValue();
}
};
export const getViewModeSubject = (api?: CanAccessViewMode) => {
if (apiPublishesViewMode(api)) return api.viewMode;
if (apiPublishesViewMode(api)) return api.viewMode$;
if (apiHasParentApi(api) && apiPublishesViewMode(api.parentApi)) {
return api.parentApi.viewMode;
return api.parentApi.viewMode$;
}
};

View file

@ -10,15 +10,17 @@
import { PublishingSubject } from '../publishing_subject';
export interface PublishesBlockingError {
blockingError: PublishingSubject<Error | undefined>;
blockingError$: PublishingSubject<Error | undefined>;
}
export const apiPublishesBlockingError = (
unknownApi: null | unknown
): unknownApi is PublishesBlockingError => {
return Boolean(unknownApi && (unknownApi as PublishesBlockingError)?.blockingError !== undefined);
return Boolean(
unknownApi && (unknownApi as PublishesBlockingError)?.blockingError$ !== undefined
);
};
export function hasBlockingError(api: unknown) {
return apiPublishesBlockingError(api) && api.blockingError?.value !== undefined;
return apiPublishesBlockingError(api) && api.blockingError$?.value !== undefined;
}

View file

@ -10,11 +10,11 @@
import { PublishingSubject } from '../publishing_subject';
export interface PublishesDataLoading {
dataLoading: PublishingSubject<boolean | undefined>;
dataLoading$: PublishingSubject<boolean | undefined>;
}
export const apiPublishesDataLoading = (
unknownApi: null | unknown
): unknownApi is PublishesDataLoading => {
return Boolean(unknownApi && (unknownApi as PublishesDataLoading)?.dataLoading !== undefined);
return Boolean(unknownApi && (unknownApi as PublishesDataLoading)?.dataLoading$ !== undefined);
};

View file

@ -11,7 +11,7 @@ import { DataView } from '@kbn/data-views-plugin/common';
import { PublishingSubject } from '../publishing_subject';
export interface PublishesDataViews {
dataViews: PublishingSubject<DataView[] | undefined>;
dataViews$: PublishingSubject<DataView[] | undefined>;
}
export type PublishesWritableDataViews = PublishesDataViews & {
@ -21,5 +21,5 @@ export type PublishesWritableDataViews = PublishesDataViews & {
export const apiPublishesDataViews = (
unknownApi: null | unknown
): unknownApi is PublishesDataViews => {
return Boolean(unknownApi && (unknownApi as PublishesDataViews)?.dataViews !== undefined);
return Boolean(unknownApi && (unknownApi as PublishesDataViews)?.dataViews$ !== undefined);
};

View file

@ -10,7 +10,7 @@
import { PublishingSubject } from '../publishing_subject';
export interface PublishesDisabledActionIds {
disabledActionIds: PublishingSubject<string[] | undefined>;
disabledActionIds$: PublishingSubject<string[] | undefined>;
setDisabledActionIds: (ids: string[] | undefined) => void;
getAllTriggersDisabled?: () => boolean;
}
@ -24,7 +24,7 @@ export const apiPublishesDisabledActionIds = (
): unknownApi is PublishesDisabledActionIds => {
return Boolean(
unknownApi &&
(unknownApi as PublishesDisabledActionIds)?.disabledActionIds !== undefined &&
(unknownApi as PublishesDisabledActionIds)?.disabledActionIds$ !== undefined &&
typeof (unknownApi as PublishesDisabledActionIds)?.setDisabledActionIds === 'function'
);
};

View file

@ -13,7 +13,7 @@ import { PublishingSubject } from '../publishing_subject';
* This API publishes a saved object id which can be used to determine which saved object this API is linked to.
*/
export interface PublishesSavedObjectId {
savedObjectId: PublishingSubject<string | undefined>;
savedObjectId$: PublishingSubject<string | undefined>;
}
/**
@ -22,5 +22,7 @@ export interface PublishesSavedObjectId {
export const apiPublishesSavedObjectId = (
unknownApi: null | unknown
): unknownApi is PublishesSavedObjectId => {
return Boolean(unknownApi && (unknownApi as PublishesSavedObjectId)?.savedObjectId !== undefined);
return Boolean(
unknownApi && (unknownApi as PublishesSavedObjectId)?.savedObjectId$ !== undefined
);
};

View file

@ -10,14 +10,14 @@
import { PublishingSubject } from '../publishing_subject';
export interface PublishesUnsavedChanges<Runtime extends object = object> {
unsavedChanges: PublishingSubject<Partial<Runtime> | undefined>;
unsavedChanges$: PublishingSubject<Partial<Runtime> | undefined>;
resetUnsavedChanges: () => boolean;
}
export const apiPublishesUnsavedChanges = (api: unknown): api is PublishesUnsavedChanges => {
return Boolean(
api &&
(api as PublishesUnsavedChanges).unsavedChanges &&
(api as PublishesUnsavedChanges).unsavedChanges$ &&
(api as PublishesUnsavedChanges).resetUnsavedChanges
);
};

View file

@ -16,7 +16,7 @@ export type ViewMode = 'view' | 'edit' | 'print' | 'preview';
* visibility of components.
*/
export interface PublishesViewMode {
viewMode: PublishingSubject<ViewMode>;
viewMode$: PublishingSubject<ViewMode>;
}
/**
@ -33,7 +33,7 @@ export type PublishesWritableViewMode = PublishesViewMode & {
export const apiPublishesViewMode = (
unknownApi: null | unknown
): unknownApi is PublishesViewMode => {
return Boolean(unknownApi && (unknownApi as PublishesViewMode)?.viewMode !== undefined);
return Boolean(unknownApi && (unknownApi as PublishesViewMode)?.viewMode$ !== undefined);
};
export const apiPublishesWritableViewMode = (

View file

@ -8,30 +8,30 @@
*/
import { BehaviorSubject } from 'rxjs';
import { getPanelDescription } from './publishes_panel_description';
import { getDescription } from './publishes_description';
describe('getPanelDescription', () => {
describe('getDescription', () => {
test('should return default description when description is undefined', () => {
const api = {
panelDescription: new BehaviorSubject<string | undefined>(undefined),
defaultPanelDescription: new BehaviorSubject<string | undefined>('default description'),
description$: new BehaviorSubject<string | undefined>(undefined),
defaultDescription$: new BehaviorSubject<string | undefined>('default description'),
};
expect(getPanelDescription(api)).toBe('default description');
expect(getDescription(api)).toBe('default description');
});
test('should return empty description when description is empty string', () => {
const api = {
panelDescription: new BehaviorSubject<string | undefined>(''),
defaultPanelDescription: new BehaviorSubject<string | undefined>('default description'),
description$: new BehaviorSubject<string | undefined>(''),
defaultDescription$: new BehaviorSubject<string | undefined>('default description'),
};
expect(getPanelDescription(api)).toBe('');
expect(getDescription(api)).toBe('');
});
test('should return description when description is provided', () => {
const api = {
panelDescription: new BehaviorSubject<string | undefined>('custom description'),
defaultPanelDescription: new BehaviorSubject<string | undefined>('default description'),
description$: new BehaviorSubject<string | undefined>('custom description'),
defaultDescription$: new BehaviorSubject<string | undefined>('default description'),
};
expect(getPanelDescription(api)).toBe('custom description');
expect(getDescription(api)).toBe('custom description');
});
});

View file

@ -0,0 +1,39 @@
/*
* 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 { PublishingSubject } from '../../publishing_subject';
export interface PublishesDescription {
description$: PublishingSubject<string | undefined>;
defaultDescription$?: PublishingSubject<string | undefined>;
}
export function getDescription(api: Partial<PublishesDescription>): string | undefined {
return api.description$?.value ?? api.defaultDescription$?.value;
}
export type PublishesWritableDescription = PublishesDescription & {
setDescription: (newTitle: string | undefined) => void;
};
export const apiPublishesDescription = (
unknownApi: null | unknown
): unknownApi is PublishesDescription => {
return Boolean(unknownApi && (unknownApi as PublishesDescription)?.description$ !== undefined);
};
export const apiPublishesWritableDescription = (
unknownApi: null | unknown
): unknownApi is PublishesWritableDescription => {
return (
apiPublishesDescription(unknownApi) &&
(unknownApi as PublishesWritableDescription).setDescription !== undefined &&
typeof (unknownApi as PublishesWritableDescription).setDescription === 'function'
);
};

View file

@ -1,41 +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 { PublishingSubject } from '../../publishing_subject';
export interface PublishesPanelDescription {
panelDescription: PublishingSubject<string | undefined>;
defaultPanelDescription?: PublishingSubject<string | undefined>;
}
export function getPanelDescription(api: Partial<PublishesPanelDescription>): string | undefined {
return api.panelDescription?.value ?? api.defaultPanelDescription?.value;
}
export type PublishesWritablePanelDescription = PublishesPanelDescription & {
setPanelDescription: (newTitle: string | undefined) => void;
};
export const apiPublishesPanelDescription = (
unknownApi: null | unknown
): unknownApi is PublishesPanelDescription => {
return Boolean(
unknownApi && (unknownApi as PublishesPanelDescription)?.panelDescription !== undefined
);
};
export const apiPublishesWritablePanelDescription = (
unknownApi: null | unknown
): unknownApi is PublishesWritablePanelDescription => {
return (
apiPublishesPanelDescription(unknownApi) &&
(unknownApi as PublishesWritablePanelDescription).setPanelDescription !== undefined &&
typeof (unknownApi as PublishesWritablePanelDescription).setPanelDescription === 'function'
);
};

View file

@ -1,47 +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 { PublishingSubject } from '../../publishing_subject';
export interface PublishesPanelTitle {
panelTitle: PublishingSubject<string | undefined>;
hidePanelTitle: PublishingSubject<boolean | undefined>;
defaultPanelTitle?: PublishingSubject<string | undefined>;
}
export function getPanelTitle(api: Partial<PublishesPanelTitle>): string | undefined {
return api.panelTitle?.value ?? api.defaultPanelTitle?.value;
}
export type PublishesWritablePanelTitle = PublishesPanelTitle & {
setPanelTitle: (newTitle: string | undefined) => void;
setHidePanelTitle: (hide: boolean | undefined) => void;
};
export const apiPublishesPanelTitle = (
unknownApi: null | unknown
): unknownApi is PublishesPanelTitle => {
return Boolean(
unknownApi &&
(unknownApi as PublishesPanelTitle)?.panelTitle !== undefined &&
(unknownApi as PublishesPanelTitle)?.hidePanelTitle !== undefined
);
};
export const apiPublishesWritablePanelTitle = (
unknownApi: null | unknown
): unknownApi is PublishesWritablePanelTitle => {
return (
apiPublishesPanelTitle(unknownApi) &&
(unknownApi as PublishesWritablePanelTitle).setPanelTitle !== undefined &&
(typeof (unknownApi as PublishesWritablePanelTitle).setPanelTitle === 'function' &&
(unknownApi as PublishesWritablePanelTitle).setHidePanelTitle) !== undefined &&
typeof (unknownApi as PublishesWritablePanelTitle).setHidePanelTitle === 'function'
);
};

View file

@ -8,30 +8,30 @@
*/
import { BehaviorSubject } from 'rxjs';
import { getPanelTitle } from './publishes_panel_title';
import { getTitle } from './publishes_title';
describe('getPanelTitle', () => {
test('should return default title when title is undefined', () => {
const api = {
panelTitle: new BehaviorSubject<string | undefined>(undefined),
defaultPanelTitle: new BehaviorSubject<string | undefined>('default title'),
title$: new BehaviorSubject<string | undefined>(undefined),
defaultTitle$: new BehaviorSubject<string | undefined>('default title'),
};
expect(getPanelTitle(api)).toBe('default title');
expect(getTitle(api)).toBe('default title');
});
test('should return empty title when title is empty string', () => {
const api = {
panelTitle: new BehaviorSubject<string | undefined>(''),
defaultPanelTitle: new BehaviorSubject<string | undefined>('default title'),
title$: new BehaviorSubject<string | undefined>(''),
defaultTitle$: new BehaviorSubject<string | undefined>('default title'),
};
expect(getPanelTitle(api)).toBe('');
expect(getTitle(api)).toBe('');
});
test('should return title when title is provided', () => {
const api = {
panelTitle: new BehaviorSubject<string | undefined>('custom title'),
defaultPanelTitle: new BehaviorSubject<string | undefined>('default title'),
title$: new BehaviorSubject<string | undefined>('custom title'),
defaultTitle$: new BehaviorSubject<string | undefined>('default title'),
};
expect(getPanelTitle(api)).toBe('custom title');
expect(getTitle(api)).toBe('custom title');
});
});

View file

@ -0,0 +1,45 @@
/*
* 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 { PublishingSubject } from '../../publishing_subject';
export interface PublishesTitle {
title$: PublishingSubject<string | undefined>;
hideTitle$: PublishingSubject<boolean | undefined>;
defaultTitle$?: PublishingSubject<string | undefined>;
}
export function getTitle(api: Partial<PublishesTitle>): string | undefined {
return api.title$?.value ?? api.defaultTitle$?.value;
}
export type PublishesWritableTitle = PublishesTitle & {
setTitle: (newTitle: string | undefined) => void;
setHideTitle: (hide: boolean | undefined) => void;
};
export const apiPublishesTitle = (unknownApi: null | unknown): unknownApi is PublishesTitle => {
return Boolean(
unknownApi &&
(unknownApi as PublishesTitle)?.title$ !== undefined &&
(unknownApi as PublishesTitle)?.hideTitle$ !== undefined
);
};
export const apiPublishesWritableTitle = (
unknownApi: null | unknown
): unknownApi is PublishesWritableTitle => {
return (
apiPublishesTitle(unknownApi) &&
(unknownApi as PublishesWritableTitle).setTitle !== undefined &&
(typeof (unknownApi as PublishesWritableTitle).setTitle === 'function' &&
(unknownApi as PublishesWritableTitle).setHideTitle) !== undefined &&
typeof (unknownApi as PublishesWritableTitle).setHideTitle === 'function'
);
};

View file

@ -0,0 +1,67 @@
/*
* 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 { initializeTitleManager, SerializedTitles } from './title_manager';
describe('titles api', () => {
const rawState: SerializedTitles = {
title: 'very cool title',
description: 'less cool description',
hidePanelTitles: false,
};
it('should initialize publishing subjects with the provided rawState', () => {
const { api } = initializeTitleManager(rawState);
expect(api.title$.value).toBe(rawState.title);
expect(api.description$.value).toBe(rawState.description);
expect(api.hideTitle$.value).toBe(rawState.hidePanelTitles);
});
it('should update publishing subject values when set functions are called', () => {
const { api } = initializeTitleManager(rawState);
api.setTitle('even cooler title');
api.setDescription('super uncool description');
api.setHideTitle(true);
expect(api.title$.value).toEqual('even cooler title');
expect(api.description$.value).toEqual('super uncool description');
expect(api.hideTitle$.value).toBe(true);
});
it('should correctly serialize current state', () => {
const titleManager = initializeTitleManager(rawState);
titleManager.api.setTitle('UH OH, A TITLE');
const serializedTitles = titleManager.serialize();
expect(serializedTitles).toMatchInlineSnapshot(`
Object {
"description": "less cool description",
"hidePanelTitles": false,
"title": "UH OH, A TITLE",
}
`);
});
it('should return the correct set of comparators', () => {
const { comparators } = initializeTitleManager(rawState);
expect(comparators.title).toBeDefined();
expect(comparators.description).toBeDefined();
expect(comparators.hidePanelTitles).toBeDefined();
});
it('should correctly compare hidePanelTitles with custom comparator', () => {
const { comparators } = initializeTitleManager(rawState);
expect(comparators.hidePanelTitles![2]!(true, false)).toBe(false);
expect(comparators.hidePanelTitles![2]!(undefined, false)).toBe(true);
expect(comparators.hidePanelTitles![2]!(true, undefined)).toBe(false);
});
});

View file

@ -0,0 +1,72 @@
/*
* 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 { BehaviorSubject } from 'rxjs';
import { StateComparators } from '../../comparators';
import { PublishesWritableDescription } from './publishes_description';
import { PublishesWritableTitle } from './publishes_title';
export interface SerializedTitles {
title?: string;
description?: string;
hidePanelTitles?: boolean;
}
export const stateHasTitles = (state: unknown): state is SerializedTitles => {
return (
(state as SerializedTitles)?.title !== undefined ||
(state as SerializedTitles)?.description !== undefined ||
(state as SerializedTitles)?.hidePanelTitles !== undefined
);
};
export interface TitlesApi extends PublishesWritableTitle, PublishesWritableDescription {}
export const initializeTitleManager = (
rawState: SerializedTitles
): {
api: TitlesApi;
comparators: StateComparators<SerializedTitles>;
serialize: () => SerializedTitles;
} => {
const title$ = new BehaviorSubject<string | undefined>(rawState.title);
const description$ = new BehaviorSubject<string | undefined>(rawState.description);
const hideTitle$ = new BehaviorSubject<boolean | undefined>(rawState.hidePanelTitles);
const setTitle = (value: string | undefined) => {
if (value !== title$.value) title$.next(value);
};
const setHideTitle = (value: boolean | undefined) => {
if (value !== hideTitle$.value) hideTitle$.next(value);
};
const setDescription = (value: string | undefined) => {
if (value !== description$.value) description$.next(value);
};
return {
api: {
title$,
hideTitle$,
setTitle,
setHideTitle,
description$,
setDescription,
},
comparators: {
title: [title$, setTitle],
description: [description$, setDescription],
hidePanelTitles: [hideTitle$, setHideTitle, (a, b) => Boolean(a) === Boolean(b)],
} as StateComparators<SerializedTitles>,
serialize: () => ({
title: title$.value,
hidePanelTitles: hideTitle$.value,
description: description$.value,
}),
};
};

View file

@ -1,67 +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 { initializeTitles, SerializedTitles } from './titles_api';
describe('titles api', () => {
const rawState: SerializedTitles = {
title: 'very cool title',
description: 'less cool description',
hidePanelTitles: false,
};
it('should initialize publishing subjects with the provided rawState', () => {
const { titlesApi } = initializeTitles(rawState);
expect(titlesApi.panelTitle.value).toBe(rawState.title);
expect(titlesApi.panelDescription.value).toBe(rawState.description);
expect(titlesApi.hidePanelTitle.value).toBe(rawState.hidePanelTitles);
});
it('should update publishing subject values when set functions are called', () => {
const { titlesApi } = initializeTitles(rawState);
titlesApi.setPanelTitle('even cooler title');
titlesApi.setPanelDescription('super uncool description');
titlesApi.setHidePanelTitle(true);
expect(titlesApi.panelTitle.value).toEqual('even cooler title');
expect(titlesApi.panelDescription.value).toEqual('super uncool description');
expect(titlesApi.hidePanelTitle.value).toBe(true);
});
it('should correctly serialize current state', () => {
const { serializeTitles, titlesApi } = initializeTitles(rawState);
titlesApi.setPanelTitle('UH OH, A TITLE');
const serializedTitles = serializeTitles();
expect(serializedTitles).toMatchInlineSnapshot(`
Object {
"description": "less cool description",
"hidePanelTitles": false,
"title": "UH OH, A TITLE",
}
`);
});
it('should return the correct set of comparators', () => {
const { titleComparators } = initializeTitles(rawState);
expect(titleComparators.title).toBeDefined();
expect(titleComparators.description).toBeDefined();
expect(titleComparators.hidePanelTitles).toBeDefined();
});
it('should correctly compare hidePanelTitles with custom comparator', () => {
const { titleComparators } = initializeTitles(rawState);
expect(titleComparators.hidePanelTitles![2]!(true, false)).toBe(false);
expect(titleComparators.hidePanelTitles![2]!(undefined, false)).toBe(true);
expect(titleComparators.hidePanelTitles![2]!(true, undefined)).toBe(false);
});
});

View file

@ -1,76 +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 { BehaviorSubject } from 'rxjs';
import { StateComparators } from '../../comparators';
import { PublishesWritablePanelDescription } from './publishes_panel_description';
import { PublishesWritablePanelTitle } from './publishes_panel_title';
export interface SerializedTitles {
title?: string;
description?: string;
hidePanelTitles?: boolean;
}
export const stateHasTitles = (state: unknown): state is SerializedTitles => {
return (
(state as SerializedTitles)?.title !== undefined ||
(state as SerializedTitles)?.description !== undefined ||
(state as SerializedTitles)?.hidePanelTitles !== undefined
);
};
export interface TitlesApi extends PublishesWritablePanelTitle, PublishesWritablePanelDescription {}
export const initializeTitles = (
rawState: SerializedTitles
): {
titlesApi: TitlesApi;
titleComparators: StateComparators<SerializedTitles>;
serializeTitles: () => SerializedTitles;
} => {
const panelTitle = new BehaviorSubject<string | undefined>(rawState.title);
const panelDescription = new BehaviorSubject<string | undefined>(rawState.description);
const hidePanelTitle = new BehaviorSubject<boolean | undefined>(rawState.hidePanelTitles);
const setPanelTitle = (value: string | undefined) => {
if (value !== panelTitle.value) panelTitle.next(value);
};
const setHidePanelTitle = (value: boolean | undefined) => {
if (value !== hidePanelTitle.value) hidePanelTitle.next(value);
};
const setPanelDescription = (value: string | undefined) => {
if (value !== panelDescription.value) panelDescription.next(value);
};
const titleComparators: StateComparators<SerializedTitles> = {
title: [panelTitle, setPanelTitle],
description: [panelDescription, setPanelDescription],
hidePanelTitles: [hidePanelTitle, setHidePanelTitle, (a, b) => Boolean(a) === Boolean(b)],
};
const titlesApi = {
panelTitle,
hidePanelTitle,
setPanelTitle,
setHidePanelTitle,
panelDescription,
setPanelDescription,
};
return {
serializeTitles: () => ({
title: panelTitle.value,
hidePanelTitles: hidePanelTitle.value,
description: panelDescription.value,
}),
titleComparators,
titlesApi,
};
};

View file

@ -15,7 +15,7 @@ import { EmbeddableEnhancedPluginStart } from '@kbn/embeddable-enhanced-plugin/p
import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public';
import { i18n } from '@kbn/i18n';
import { PresentationContainer } from '@kbn/presentation-containers';
import { getUnchangingComparator, initializeTitles } from '@kbn/presentation-publishing';
import { getUnchangingComparator, initializeTitleManager } from '@kbn/presentation-publishing';
import { IMAGE_CLICK_TRIGGER } from '../actions';
import { openImageEditor } from '../components/image_editor/open_image_editor';
@ -38,11 +38,11 @@ export const getImageEmbeddableFactory = ({
type: IMAGE_EMBEDDABLE_TYPE,
deserializeState: (state) => state.rawState,
buildEmbeddable: async (initialState, buildApi, uuid) => {
const { titlesApi, titleComparators, serializeTitles } = initializeTitles(initialState);
const titleManager = initializeTitleManager(initialState);
const dynamicActionsApi = embeddableEnhanced?.initializeReactEmbeddableDynamicActions(
uuid,
() => titlesApi.panelTitle.getValue(),
() => titleManager.api.title$.getValue(),
initialState
);
// if it is provided, start the dynamic actions manager
@ -54,9 +54,9 @@ export const getImageEmbeddableFactory = ({
const embeddable = buildApi(
{
...titlesApi,
...titleManager.api,
...(dynamicActionsApi?.dynamicActionsApi ?? {}),
dataLoading: dataLoading$,
dataLoading$,
supportedTriggers: () => [IMAGE_CLICK_TRIGGER],
onEdit: async () => {
try {
@ -77,7 +77,7 @@ export const getImageEmbeddableFactory = ({
serializeState: () => {
return {
rawState: {
...serializeTitles(),
...titleManager.serialize(),
...(dynamicActionsApi?.serializeDynamicActions() ?? {}),
imageConfig: imageConfig$.getValue(),
},
@ -85,7 +85,7 @@ export const getImageEmbeddableFactory = ({
},
},
{
...titleComparators,
...titleManager.comparators,
...(dynamicActionsApi?.dynamicActionsComparator ?? {
enhancements: getUnchangingComparator(),
}),

View file

@ -9,8 +9,8 @@
import { apiIsPresentationContainer } from '@kbn/presentation-containers';
import {
apiPublishesPanelDescription,
apiPublishesPanelTitle,
apiPublishesDescription,
apiPublishesTitle,
apiPublishesSavedObjectId,
} from '@kbn/presentation-publishing';
import { LinksParentApi } from '../types';
@ -18,5 +18,5 @@ import { LinksParentApi } from '../types';
export const isParentApiCompatible = (parentApi: unknown): parentApi is LinksParentApi =>
apiIsPresentationContainer(parentApi) &&
apiPublishesSavedObjectId(parentApi) &&
apiPublishesPanelTitle(parentApi) &&
apiPublishesPanelDescription(parentApi);
apiPublishesTitle(parentApi) &&
apiPublishesDescription(parentApi);

View file

@ -263,8 +263,8 @@ describe('Dashboard link component', () => {
test('current dashboard is not a clickable href', async () => {
const parentApi = createMockLinksParent({});
parentApi.savedObjectId = new BehaviorSubject<string | undefined>('123');
parentApi.panelTitle = new BehaviorSubject<string | undefined>('current dashboard');
parentApi.savedObjectId$ = new BehaviorSubject<string | undefined>('123');
parentApi.title$ = new BehaviorSubject<string | undefined>('current dashboard');
render(
<DashboardLinkComponent
@ -310,9 +310,9 @@ describe('Dashboard link component', () => {
test('current dashboard title updates when parent changes', async () => {
const parentApi = {
...createMockLinksParent({}),
panelTitle: new BehaviorSubject<string | undefined>('old title'),
panelDescription: new BehaviorSubject<string | undefined>('old description'),
savedObjectId: new BehaviorSubject<string | undefined>('123'),
title$: new BehaviorSubject<string | undefined>('old title'),
description$: new BehaviorSubject<string | undefined>('old description'),
savedObjectId$: new BehaviorSubject<string | undefined>('123'),
};
const { rerender } = render(
@ -328,7 +328,7 @@ describe('Dashboard link component', () => {
);
expect(await screen.findByTestId('dashboardLink--bar')).toHaveTextContent('old title');
parentApi.panelTitle.next('new title');
parentApi.title$.next('new title');
rerender(
<DashboardLinkComponent
link={{
@ -367,7 +367,7 @@ describe('Dashboard link component', () => {
test('can override link label for the current dashboard', async () => {
const customLabel = 'my new label for the current dashboard';
const parentApi = createMockLinksParent({});
parentApi.savedObjectId = new BehaviorSubject<string | undefined>('123');
parentApi.savedObjectId$ = new BehaviorSubject<string | undefined>('123');
render(
<DashboardLinkComponent

View file

@ -46,9 +46,9 @@ export const DashboardLinkComponent = ({
filters,
query,
] = useBatchedPublishingSubjects(
parentApi.savedObjectId,
parentApi.panelTitle,
parentApi.panelDescription,
parentApi.savedObjectId$,
parentApi.title$,
parentApi.description$,
parentApi.timeRange$,
parentApi.filters$,
parentApi.query$

View file

@ -60,7 +60,7 @@ export async function openEditorFlyout({
const parentDashboardId =
parentDashboard && apiPublishesSavedObjectId(parentDashboard)
? parentDashboard.savedObjectId.value
? parentDashboard.savedObjectId$.value
: undefined;
return new Promise<LinksRuntimeState | undefined>((resolve) => {

View file

@ -218,8 +218,8 @@ describe('getLinksEmbeddableFactory', () => {
references: [],
});
expect(await api.canUnlinkFromLibrary()).toBe(true);
expect(api.defaultPanelTitle!.value).toBe('links 001');
expect(api.defaultPanelDescription!.value).toBe('some links');
expect(api.defaultTitle$?.value).toBe('links 001');
expect(api.defaultDescription$?.value).toBe('some links');
});
});

View file

@ -15,7 +15,7 @@ import { EuiListGroup, EuiPanel } from '@elastic/eui';
import { PanelIncompatibleError, ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public';
import {
SerializedTitles,
initializeTitles,
initializeTitleManager,
SerializedPanelState,
useBatchedOptionalPublishingSubjects,
} from '@kbn/presentation-publishing';
@ -94,25 +94,25 @@ export const getLinksEmbeddableFactory = () => {
};
},
buildEmbeddable: async (state, buildApi, uuid, parentApi) => {
const error$ = new BehaviorSubject<Error | undefined>(state.error);
if (!isParentApiCompatible(parentApi)) error$.next(new PanelIncompatibleError());
const blockingError$ = new BehaviorSubject<Error | undefined>(state.error);
if (!isParentApiCompatible(parentApi)) blockingError$.next(new PanelIncompatibleError());
const links$ = new BehaviorSubject<ResolvedLink[] | undefined>(state.links);
const layout$ = new BehaviorSubject<LinksLayoutType | undefined>(state.layout);
const defaultPanelTitle = new BehaviorSubject<string | undefined>(state.defaultPanelTitle);
const defaultPanelDescription = new BehaviorSubject<string | undefined>(
const defaultTitle$ = new BehaviorSubject<string | undefined>(state.defaultPanelTitle);
const defaultDescription$ = new BehaviorSubject<string | undefined>(
state.defaultPanelDescription
);
const savedObjectId$ = new BehaviorSubject(state.savedObjectId);
const isByReference = Boolean(state.savedObjectId);
const { titlesApi, titleComparators, serializeTitles } = initializeTitles(state);
const titleManager = initializeTitleManager(state);
const serializeLinksState = (byReference: boolean, newId?: string) => {
if (byReference) {
const linksByReferenceState: LinksByReferenceSerializedState = {
savedObjectId: newId ?? state.savedObjectId!,
...serializeTitles(),
...titleManager.serialize(),
};
return { rawState: linksByReferenceState, references: [] };
}
@ -120,22 +120,22 @@ export const getLinksEmbeddableFactory = () => {
const { attributes, references } = serializeLinksAttributes(runtimeState);
const linksByValueState: LinksByValueSerializedState = {
attributes,
...serializeTitles(),
...titleManager.serialize(),
};
return { rawState: linksByValueState, references };
};
const api = buildApi(
{
...titlesApi,
blockingError: error$,
defaultPanelTitle,
defaultPanelDescription,
isEditingEnabled: () => Boolean(error$.value === undefined),
...titleManager.api,
blockingError$,
defaultTitle$,
defaultDescription$,
isEditingEnabled: () => Boolean(blockingError$.value === undefined),
getTypeDisplayName: () => DISPLAY_NAME,
serializeState: () => serializeLinksState(isByReference),
saveToLibrary: async (newTitle: string) => {
defaultPanelTitle.next(newTitle);
defaultTitle$.next(newTitle);
const runtimeState = api.snapshotRuntimeState();
const { attributes, references } = serializeLinksAttributes(runtimeState);
const {
@ -196,26 +196,23 @@ export const getLinksEmbeddableFactory = () => {
}
links$.next(newState.links);
layout$.next(newState.layout);
defaultPanelTitle.next(newState.defaultPanelTitle);
defaultPanelDescription.next(newState.defaultPanelDescription);
defaultTitle$.next(newState.defaultPanelTitle);
defaultDescription$.next(newState.defaultPanelDescription);
},
},
{
...titleComparators,
...titleManager.comparators,
links: [links$, (nextLinks?: ResolvedLink[]) => links$.next(nextLinks ?? [])],
layout: [
layout$,
(nextLayout?: LinksLayoutType) => layout$.next(nextLayout ?? LINKS_VERTICAL_LAYOUT),
],
error: [error$, (nextError?: Error) => error$.next(nextError)],
error: [blockingError$, (nextError?: Error) => blockingError$.next(nextError)],
defaultPanelDescription: [
defaultPanelDescription,
(nextDescription?: string) => defaultPanelDescription.next(nextDescription),
],
defaultPanelTitle: [
defaultPanelTitle,
(nextTitle?: string) => defaultPanelTitle.next(nextTitle),
defaultDescription$,
(nextDescription?: string) => defaultDescription$.next(nextDescription),
],
defaultPanelTitle: [defaultTitle$, (nextTitle?: string) => defaultTitle$.next(nextTitle)],
savedObjectId: [savedObjectId$, (val) => savedObjectId$.next(val)],
}
);

View file

@ -58,9 +58,9 @@ export const getMockLinksParentApi = (
to: 'now',
}),
timeslice$: new BehaviorSubject<[number, number] | undefined>(undefined),
savedObjectId: new BehaviorSubject<string | undefined>('999'),
hidePanelTitle: new BehaviorSubject<boolean | undefined>(false),
panelTitle: new BehaviorSubject<string | undefined>('My Dashboard'),
panelDescription: new BehaviorSubject<string | undefined>(''),
savedObjectId$: new BehaviorSubject<string | undefined>('999'),
hideTitle$: new BehaviorSubject<boolean | undefined>(false),
title$: new BehaviorSubject<string | undefined>('My Dashboard'),
description$: new BehaviorSubject<string | undefined>(''),
getSerializedStateForChild: () => ({ rawState: serializedState, references }),
});

View file

@ -11,8 +11,8 @@ import {
HasEditCapabilities,
HasLibraryTransforms,
HasType,
PublishesPanelDescription,
PublishesPanelTitle,
PublishesDescription,
PublishesTitle,
PublishesSavedObjectId,
PublishesUnifiedSearch,
SerializedTitles,
@ -31,8 +31,8 @@ export type LinksParentApi = PresentationContainer &
HasType<typeof DASHBOARD_API_TYPE> &
HasSerializedChildState<LinksSerializedState> &
PublishesSavedObjectId &
PublishesPanelTitle &
PublishesPanelDescription &
PublishesTitle &
PublishesDescription &
PublishesUnifiedSearch & {
locator?: Pick<LocatorPublic<DashboardLocatorParams>, 'navigate' | 'getRedirectUrl'>;
};

View file

@ -25,8 +25,8 @@ describe('Customize panel action', () => {
context = {
embeddable: {
parentApi: {},
viewMode: new BehaviorSubject<ViewMode>('edit'),
dataViews: new BehaviorSubject<DataView[] | undefined>(undefined),
viewMode$: new BehaviorSubject<ViewMode>('edit'),
dataViews$: new BehaviorSubject<DataView[] | undefined>(undefined),
},
};
});
@ -36,7 +36,7 @@ describe('Customize panel action', () => {
});
it('is compatible in view mode when API exposes writable unified search', async () => {
(context.embeddable as PublishesViewMode).viewMode = new BehaviorSubject<ViewMode>('view');
(context.embeddable as PublishesViewMode).viewMode$ = new BehaviorSubject<ViewMode>('view');
context.embeddable.timeRange$ = new BehaviorSubject<TimeRange | undefined>({
from: 'now-15m',
to: 'now',

View file

@ -13,15 +13,15 @@ import {
apiCanAccessViewMode,
apiPublishesDataViews,
apiPublishesUnifiedSearch,
apiPublishesPanelTitle,
apiPublishesTitle,
CanAccessViewMode,
EmbeddableApiContext,
getInheritedViewMode,
HasParentApi,
PublishesDataViews,
PublishesWritableUnifiedSearch,
PublishesWritablePanelDescription,
PublishesWritablePanelTitle,
PublishesWritableDescription,
PublishesWritableTitle,
PublishesUnifiedSearch,
} from '@kbn/presentation-publishing';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
@ -32,15 +32,15 @@ export type CustomizePanelActionApi = CanAccessViewMode &
Partial<
PublishesDataViews &
PublishesWritableUnifiedSearch &
PublishesWritablePanelDescription &
PublishesWritablePanelTitle &
PublishesWritableDescription &
PublishesWritableTitle &
HasParentApi<Partial<PublishesUnifiedSearch & TracksOverlays>>
>;
export const isApiCompatibleWithCustomizePanelAction = (
api: unknown | null
): api is CustomizePanelActionApi =>
apiCanAccessViewMode(api) && (apiPublishesDataViews(api) || apiPublishesPanelTitle(api));
apiCanAccessViewMode(api) && (apiPublishesDataViews(api) || apiPublishesTitle(api));
export class CustomizePanelAction implements Action<EmbeddableApiContext> {
public type = ACTION_CUSTOMIZE_PANEL;

View file

@ -25,20 +25,20 @@ describe('customize panel editor', () => {
let setDescription: (description?: string) => void;
beforeEach(() => {
const titleSubject = new BehaviorSubject<string | undefined>(undefined);
setTitle = jest.fn((title) => titleSubject.next(title));
const descriptionSubject = new BehaviorSubject<string | undefined>(undefined);
setDescription = jest.fn((description) => descriptionSubject.next(description));
const viewMode = new BehaviorSubject<ViewMode>('edit');
setViewMode = jest.fn((nextViewMode) => viewMode.next(nextViewMode));
const title$ = new BehaviorSubject<string | undefined>(undefined);
setTitle = jest.fn((title) => title$.next(title));
const description$ = new BehaviorSubject<string | undefined>(undefined);
setDescription = jest.fn((description) => description$.next(description));
const viewMode$ = new BehaviorSubject<ViewMode>('edit');
setViewMode = jest.fn((nextViewMode) => viewMode$.next(nextViewMode));
api = {
viewMode,
dataViews: new BehaviorSubject<DataView[] | undefined>([]),
panelTitle: titleSubject,
setPanelTitle: setTitle,
panelDescription: descriptionSubject,
setPanelDescription: setDescription,
viewMode$,
dataViews$: new BehaviorSubject<DataView[] | undefined>([]),
title$,
setTitle,
description$,
setDescription,
};
});
@ -61,7 +61,7 @@ describe('customize panel editor', () => {
});
it('Initializes panel title with default title from API', () => {
api.defaultPanelTitle = new BehaviorSubject<string | undefined>('Default title');
api.defaultTitle$ = new BehaviorSubject<string | undefined>('Default title');
renderPanelEditor();
expect(screen.getByTestId('customEmbeddablePanelTitleInput')).toHaveValue('Default title');
});
@ -82,7 +82,7 @@ describe('customize panel editor', () => {
});
it('should use default title when title is undefined', () => {
api.defaultPanelTitle = new BehaviorSubject<string | undefined>('Default title');
api.defaultTitle$ = new BehaviorSubject<string | undefined>('Default title');
setTitle(undefined);
renderPanelEditor();
const titleInput = screen.getByTestId('customEmbeddablePanelTitleInput');
@ -90,7 +90,7 @@ describe('customize panel editor', () => {
});
it('should use title even when empty string', () => {
api.defaultPanelTitle = new BehaviorSubject<string | undefined>('Default title');
api.defaultTitle$ = new BehaviorSubject<string | undefined>('Default title');
setTitle('');
renderPanelEditor();
const titleInput = screen.getByTestId('customEmbeddablePanelTitleInput');
@ -98,7 +98,7 @@ describe('customize panel editor', () => {
});
it('Resets panel title to default when reset button is pressed', async () => {
api.defaultPanelTitle = new BehaviorSubject<string | undefined>('Default title');
api.defaultTitle$ = new BehaviorSubject<string | undefined>('Default title');
setTitle('Initial title');
renderPanelEditor();
await userEvent.type(screen.getByTestId('customEmbeddablePanelTitleInput'), 'New title');
@ -107,7 +107,7 @@ describe('customize panel editor', () => {
});
it('should hide title reset when no default exists', async () => {
api.defaultPanelTitle = new BehaviorSubject<string | undefined>(undefined);
api.defaultTitle$ = new BehaviorSubject<string | undefined>(undefined);
setTitle('Initial title');
renderPanelEditor();
await userEvent.type(screen.getByTestId('customEmbeddablePanelTitleInput'), 'New title');
@ -129,7 +129,7 @@ describe('customize panel editor', () => {
});
it('Initializes panel description with default description from API', () => {
api.defaultPanelDescription = new BehaviorSubject<string | undefined>('Default description');
api.defaultDescription$ = new BehaviorSubject<string | undefined>('Default description');
renderPanelEditor();
expect(screen.getByTestId('customEmbeddablePanelDescriptionInput')).toHaveValue(
'Default description'
@ -155,7 +155,7 @@ describe('customize panel editor', () => {
});
it('should use default description when description is undefined', () => {
api.defaultPanelDescription = new BehaviorSubject<string | undefined>('Default description');
api.defaultDescription$ = new BehaviorSubject<string | undefined>('Default description');
setDescription(undefined);
renderPanelEditor();
const descriptionInput = screen.getByTestId('customEmbeddablePanelDescriptionInput');
@ -163,7 +163,7 @@ describe('customize panel editor', () => {
});
it('should use description even when empty string', () => {
api.defaultPanelDescription = new BehaviorSubject<string | undefined>('Default description');
api.defaultDescription$ = new BehaviorSubject<string | undefined>('Default description');
setDescription('');
renderPanelEditor();
const descriptionInput = screen.getByTestId('customEmbeddablePanelDescriptionInput');
@ -171,7 +171,7 @@ describe('customize panel editor', () => {
});
it('Resets panel description to default when reset button is pressed', async () => {
api.defaultPanelDescription = new BehaviorSubject<string | undefined>('Default description');
api.defaultDescription$ = new BehaviorSubject<string | undefined>('Default description');
setDescription('Initial description');
renderPanelEditor();
await userEvent.type(
@ -185,7 +185,7 @@ describe('customize panel editor', () => {
});
it('should hide description reset when no default exists', async () => {
api.defaultPanelDescription = new BehaviorSubject<string | undefined>(undefined);
api.defaultDescription$ = new BehaviorSubject<string | undefined>(undefined);
setDescription('Initial description');
renderPanelEditor();
await userEvent.type(

View file

@ -34,8 +34,8 @@ import {
apiPublishesTimeRange,
apiPublishesUnifiedSearch,
getInheritedViewMode,
getPanelDescription,
getPanelTitle,
getDescription,
getTitle,
PublishesUnifiedSearch,
} from '@kbn/presentation-publishing';
@ -63,9 +63,9 @@ export const CustomizePanelEditor = ({
* For now, we copy the state here with `useState` initializing it to the latest value.
*/
const editMode = getInheritedViewMode(api) === 'edit';
const [hideTitle, setHideTitle] = useState(api.hidePanelTitle?.value);
const [panelTitle, setPanelTitle] = useState(getPanelTitle(api));
const [panelDescription, setPanelDescription] = useState(getPanelDescription(api));
const [hideTitle, setHideTitle] = useState(api.hideTitle$?.value);
const [panelTitle, setPanelTitle] = useState(getTitle(api));
const [panelDescription, setPanelDescription] = useState(getDescription(api));
const [timeRange, setTimeRange] = useState(
api.timeRange$?.value ?? api.parentApi?.timeRange$?.value
);
@ -99,10 +99,9 @@ export const CustomizePanelEditor = ({
const dateFormat = useMemo(() => core.uiSettings.get<string>(UI_SETTINGS.DATE_FORMAT), []);
const save = () => {
if (panelTitle !== api.panelTitle?.value) api.setPanelTitle?.(panelTitle);
if (hideTitle !== api.hidePanelTitle?.value) api.setHidePanelTitle?.(hideTitle);
if (panelDescription !== api.panelDescription?.value)
api.setPanelDescription?.(panelDescription);
if (panelTitle !== api.title$?.value) api.setTitle?.(panelTitle);
if (hideTitle !== api.hideTitle$?.value) api.setHideTitle?.(hideTitle);
if (panelDescription !== api.description$?.value) api.setDescription?.(panelDescription);
const newTimeRange = hasOwnTimeRange ? timeRange : undefined;
if (newTimeRange !== api.timeRange$?.value) {
@ -139,12 +138,12 @@ export const CustomizePanelEditor = ({
/>
}
labelAppend={
api?.defaultPanelTitle?.value && (
api?.defaultTitle$?.value && (
<EuiButtonEmpty
size="xs"
data-test-subj="resetCustomEmbeddablePanelTitleButton"
onClick={() => setPanelTitle(api.defaultPanelTitle?.value)}
disabled={hideTitle || panelTitle === api?.defaultPanelTitle?.value}
onClick={() => setPanelTitle(api.defaultTitle$?.value)}
disabled={hideTitle || panelTitle === api?.defaultTitle$?.value}
aria-label={i18n.translate(
'presentationPanel.action.customizePanel.flyout.optionsMenuForm.resetCustomTitleButtonAriaLabel',
{
@ -186,12 +185,12 @@ export const CustomizePanelEditor = ({
/>
}
labelAppend={
api.defaultPanelDescription?.value && (
api.defaultDescription$?.value && (
<EuiButtonEmpty
size="xs"
data-test-subj="resetCustomEmbeddablePanelDescriptionButton"
onClick={() => setPanelDescription(api.defaultPanelDescription?.value)}
disabled={api.defaultPanelDescription?.value === panelDescription}
onClick={() => setPanelDescription(api.defaultDescription$?.value)}
disabled={api.defaultDescription$?.value === panelDescription}
aria-label={i18n.translate(
'presentationPanel.action.customizePanel.flyout.optionsMenuForm.resetCustomDescriptionButtonAriaLabel',
{

View file

@ -38,7 +38,7 @@ interface FiltersDetailsProps {
export function FiltersDetails({ editMode, api }: FiltersDetailsProps) {
const [queryString, setQueryString] = useState<string>('');
const [queryLanguage, setQueryLanguage] = useState<'sql' | 'esql' | undefined>();
const dataViews = api.dataViews?.value ?? [];
const dataViews = api.dataViews$?.value ?? [];
const filters = useMemo(() => api.filters$?.value ?? [], [api]);

View file

@ -14,16 +14,16 @@ import { EditPanelAction, EditPanelActionApi } from './edit_panel_action';
describe('Edit panel action', () => {
let action: EditPanelAction;
let context: { embeddable: EditPanelActionApi };
let updateViewMode: (viewMode: ViewMode) => void;
let setViewMode: (viewMode: ViewMode) => void;
beforeEach(() => {
const viewModeSubject = new BehaviorSubject<ViewMode>('edit');
updateViewMode = (viewMode) => viewModeSubject.next(viewMode);
const viewMode$ = new BehaviorSubject<ViewMode>('edit');
setViewMode = (viewMode) => viewMode$.next(viewMode);
action = new EditPanelAction();
context = {
embeddable: {
viewMode: viewModeSubject,
viewMode$,
onEdit: jest.fn(),
isEditingEnabled: jest.fn().mockReturnValue(true),
getTypeDisplayName: jest.fn().mockReturnValue('A very fun panel type'),
@ -43,7 +43,7 @@ describe('Edit panel action', () => {
});
it('is incompatible when view mode is view', async () => {
(context.embeddable as PublishesViewMode).viewMode = new BehaviorSubject<ViewMode>('view');
(context.embeddable as PublishesViewMode).viewMode$ = new BehaviorSubject<ViewMode>('view');
expect(await action.isCompatible(context)).toBe(false);
});
@ -66,7 +66,7 @@ describe('Edit panel action', () => {
it('calls onChange when view mode changes', () => {
const onChange = jest.fn();
action.subscribeToCompatibilityChanges(context, onChange);
updateViewMode('view');
setViewMode('view');
expect(onChange).toHaveBeenCalledWith(false, action);
});
});

View file

@ -12,16 +12,15 @@ import { apiHasInspectorAdapters, HasInspectorAdapters } from '@kbn/inspector-pl
import { tracksOverlays } from '@kbn/presentation-containers';
import {
EmbeddableApiContext,
getPanelTitle,
PublishesPanelTitle,
getTitle,
PublishesTitle,
HasParentApi,
} from '@kbn/presentation-publishing';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { ACTION_INSPECT_PANEL } from './constants';
import { inspector } from '../../kibana_services';
export type InspectPanelActionApi = HasInspectorAdapters &
Partial<PublishesPanelTitle & HasParentApi>;
export type InspectPanelActionApi = HasInspectorAdapters & Partial<PublishesTitle & HasParentApi>;
const isApiCompatible = (api: unknown | null): api is InspectPanelActionApi => {
return Boolean(api) && apiHasInspectorAdapters(api);
};
@ -57,7 +56,7 @@ export class InspectPanelAction implements Action<EmbeddableApiContext> {
}
const panelTitle =
getPanelTitle(embeddable) ||
getTitle(embeddable) ||
i18n.translate('presentationPanel.action.inspectPanel.untitledEmbeddableFilename', {
defaultMessage: '[No Title]',
});

View file

@ -21,7 +21,7 @@ describe('Remove panel action', () => {
context = {
embeddable: {
uuid: 'superId',
viewMode: new BehaviorSubject<ViewMode>('edit'),
viewMode$: new BehaviorSubject<ViewMode>('edit'),
parentApi: getMockPresentationContainer(),
},
};
@ -39,7 +39,7 @@ describe('Remove panel action', () => {
});
it('is incompatible when view mode is view', async () => {
context.embeddable.viewMode = new BehaviorSubject<ViewMode>('view');
context.embeddable.viewMode$ = new BehaviorSubject<ViewMode>('view');
expect(await action.isCompatible(context)).toBe(false);
});

View file

@ -209,12 +209,12 @@ export const PresentationPanelHoverActions = ({
parentHideTitle,
parentViewMode,
] = useBatchedOptionalPublishingSubjects(
api?.defaultPanelTitle,
api?.panelTitle,
api?.panelDescription,
api?.hidePanelTitle,
api?.defaultTitle$,
api?.title$,
api?.description$,
api?.hideTitle$,
api?.hasLockedHoverActions$,
api?.parentApi?.hidePanelTitle,
api?.parentApi?.hideTitle$,
/**
* View mode changes often have the biggest influence over which actions will be compatible,
* so we build and update all actions when the view mode changes. This is temporary, as these
@ -332,7 +332,7 @@ export const PresentationPanelHoverActions = ({
})()) as AnyApiAction[];
if (canceled) return;
const disabledActions = api.disabledActionIds?.value;
const disabledActions = api.disabledActionIds$?.value;
if (disabledActions) {
compatibleActions = compatibleActions.filter(
(action) => disabledActions.indexOf(action.id) === -1

View file

@ -50,7 +50,7 @@ export const usePresentationPanelHeaderActions = <
embeddable: api,
})) as AnyApiAction[]) ?? [];
const disabledActions = (api.disabledActionIds?.value ?? []).concat(disabledNotifications);
const disabledActions = (api.disabledActionIds$?.value ?? []).concat(disabledNotifications);
nextActions = nextActions.filter((badge) => disabledActions.indexOf(badge.id) === -1);
return nextActions;
};

View file

@ -62,7 +62,7 @@ export const PresentationPanelErrorInternal = ({ api, error }: PresentationPanel
});
}, [api, isEditable]);
const panelTitle = useStateFromPublishingSubject(api?.panelTitle);
const panelTitle = useStateFromPublishingSubject(api?.title$);
const ariaLabel = useMemo(
() =>
panelTitle

View file

@ -51,7 +51,7 @@ describe('Presentation panel', () => {
it('renders a blocking error when one is present', async () => {
const api: DefaultPresentationPanelApi = {
uuid: 'test',
blockingError: new BehaviorSubject<Error | undefined>(new Error('UH OH')),
blockingError$: new BehaviorSubject<Error | undefined>(new Error('UH OH')),
};
render(<PresentationPanel Component={getMockPresentationPanelCompatibleComponent(api)} />);
await waitFor(() => expect(screen.getByTestId('embeddableStackError')).toBeInTheDocument());
@ -91,7 +91,7 @@ describe('Presentation panel', () => {
it('gets compatible actions for the given API', async () => {
const api: DefaultPresentationPanelApi = {
uuid: 'test',
panelTitle: new BehaviorSubject<string | undefined>('superTest'),
title$: new BehaviorSubject<string | undefined>('superTest'),
};
await renderPresentationPanel({ api });
expect(uiActions.getTriggerCompatibleActions).toHaveBeenCalledWith('CONTEXT_MENU_TRIGGER', {
@ -116,7 +116,7 @@ describe('Presentation panel', () => {
it('does not show actions which are disabled by the API', async () => {
const api: DefaultPresentationPanelApi = {
uuid: 'test',
disabledActionIds: new BehaviorSubject<string[] | undefined>(['actionA']),
disabledActionIds$: new BehaviorSubject<string[] | undefined>(['actionA']),
};
const getActions = jest.fn().mockReturnValue([mockAction('actionA'), mockAction('actionB')]);
await renderPresentationPanel({ api, props: { getActions } });
@ -161,8 +161,8 @@ describe('Presentation panel', () => {
it('renders the panel title from the api and not the default title', async () => {
const api: DefaultPresentationPanelApi = {
uuid: 'test',
panelTitle: new BehaviorSubject<string | undefined>('SUPER TITLE'),
defaultPanelTitle: new BehaviorSubject<string | undefined>('SO Title'),
title$: new BehaviorSubject<string | undefined>('SUPER TITLE'),
defaultTitle$: new BehaviorSubject<string | undefined>('SO Title'),
};
await renderPresentationPanel({ api });
await waitFor(() => {
@ -173,7 +173,7 @@ describe('Presentation panel', () => {
it('renders the default title from the api when a panel title is not provided', async () => {
const api: DefaultPresentationPanelApi = {
uuid: 'test',
defaultPanelTitle: new BehaviorSubject<string | undefined>('SO Title'),
defaultTitle$: new BehaviorSubject<string | undefined>('SO Title'),
};
await renderPresentationPanel({ api });
await waitFor(() => {
@ -184,7 +184,7 @@ describe('Presentation panel', () => {
it("does not render an info icon when the api doesn't provide a panel description or default description", async () => {
const api: DefaultPresentationPanelApi = {
uuid: 'test',
panelTitle: new BehaviorSubject<string | undefined>('SUPER TITLE'),
title$: new BehaviorSubject<string | undefined>('SUPER TITLE'),
};
await renderPresentationPanel({ api });
await waitFor(() => {
@ -195,8 +195,8 @@ describe('Presentation panel', () => {
it('renders an info icon when the api provides a panel description', async () => {
const api: DefaultPresentationPanelApi = {
uuid: 'test',
panelTitle: new BehaviorSubject<string | undefined>('SUPER TITLE'),
panelDescription: new BehaviorSubject<string | undefined>('SUPER DESCRIPTION'),
title$: new BehaviorSubject<string | undefined>('SUPER TITLE'),
description$: new BehaviorSubject<string | undefined>('SUPER DESCRIPTION'),
};
await renderPresentationPanel({ api });
await waitFor(() => {
@ -207,8 +207,8 @@ describe('Presentation panel', () => {
it('renders an info icon when the api provides a default description', async () => {
const api: DefaultPresentationPanelApi = {
uuid: 'test',
panelTitle: new BehaviorSubject<string | undefined>('SUPER TITLE'),
defaultPanelDescription: new BehaviorSubject<string | undefined>('SO Description'),
title$: new BehaviorSubject<string | undefined>('SUPER TITLE'),
defaultDescription$: new BehaviorSubject<string | undefined>('SO Description'),
};
await renderPresentationPanel({ api });
await waitFor(() => {
@ -219,8 +219,8 @@ describe('Presentation panel', () => {
it('does not render a title when in view mode when the provided title is blank', async () => {
const api: DefaultPresentationPanelApi & PublishesViewMode = {
uuid: 'test',
panelTitle: new BehaviorSubject<string | undefined>(''),
viewMode: new BehaviorSubject<ViewMode>('view'),
title$: new BehaviorSubject<string | undefined>(''),
viewMode$: new BehaviorSubject<ViewMode>('view'),
};
await renderPresentationPanel({ api });
expect(screen.queryByTestId('presentationPanelTitle')).not.toBeInTheDocument();
@ -229,9 +229,9 @@ describe('Presentation panel', () => {
it('does not render a title when in edit mode and the provided title is blank', async () => {
const api: DefaultPresentationPanelApi & PublishesDataViews & PublishesViewMode = {
uuid: 'test',
panelTitle: new BehaviorSubject<string | undefined>(''),
viewMode: new BehaviorSubject<ViewMode>('edit'),
dataViews: new BehaviorSubject<DataView[] | undefined>([]),
title$: new BehaviorSubject<string | undefined>(''),
viewMode$: new BehaviorSubject<ViewMode>('edit'),
dataViews$: new BehaviorSubject<DataView[] | undefined>([]),
};
await renderPresentationPanel({ api });
expect(screen.queryByTestId('presentationPanelTitle')).not.toBeInTheDocument();
@ -242,9 +242,9 @@ describe('Presentation panel', () => {
const api: DefaultPresentationPanelApi & PublishesDataViews & PublishesViewMode = {
uuid: 'test',
panelTitle: new BehaviorSubject<string | undefined>('TITLE'),
viewMode: new BehaviorSubject<ViewMode>('edit'),
dataViews: new BehaviorSubject<DataView[] | undefined>([]),
title$: new BehaviorSubject<string | undefined>('TITLE'),
viewMode$: new BehaviorSubject<ViewMode>('edit'),
dataViews$: new BehaviorSubject<DataView[] | undefined>([]),
};
await renderPresentationPanel({ api });
await waitFor(() => {
@ -259,9 +259,9 @@ describe('Presentation panel', () => {
it('does not show title customize link in view mode', async () => {
const api: DefaultPresentationPanelApi & PublishesDataViews & PublishesViewMode = {
uuid: 'test',
panelTitle: new BehaviorSubject<string | undefined>('SUPER TITLE'),
viewMode: new BehaviorSubject<ViewMode>('view'),
dataViews: new BehaviorSubject<DataView[] | undefined>([]),
title$: new BehaviorSubject<string | undefined>('SUPER TITLE'),
viewMode$: new BehaviorSubject<ViewMode>('view'),
dataViews$: new BehaviorSubject<DataView[] | undefined>([]),
};
await renderPresentationPanel({ api });
await waitFor(() => {
@ -273,9 +273,9 @@ describe('Presentation panel', () => {
it('hides title in view mode when API hide title option is true', async () => {
const api: DefaultPresentationPanelApi & PublishesViewMode = {
uuid: 'test',
panelTitle: new BehaviorSubject<string | undefined>('SUPER TITLE'),
hidePanelTitle: new BehaviorSubject<boolean | undefined>(true),
viewMode: new BehaviorSubject<ViewMode>('view'),
title$: new BehaviorSubject<string | undefined>('SUPER TITLE'),
hideTitle$: new BehaviorSubject<boolean | undefined>(true),
viewMode$: new BehaviorSubject<ViewMode>('view'),
};
await renderPresentationPanel({ api });
expect(screen.queryByTestId('presentationPanelTitle')).not.toBeInTheDocument();
@ -284,9 +284,9 @@ describe('Presentation panel', () => {
it('hides title in edit mode when API hide title option is true', async () => {
const api: DefaultPresentationPanelApi & PublishesViewMode = {
uuid: 'test',
panelTitle: new BehaviorSubject<string | undefined>('SUPER TITLE'),
hidePanelTitle: new BehaviorSubject<boolean | undefined>(true),
viewMode: new BehaviorSubject<ViewMode>('edit'),
title$: new BehaviorSubject<string | undefined>('SUPER TITLE'),
hideTitle$: new BehaviorSubject<boolean | undefined>(true),
viewMode$: new BehaviorSubject<ViewMode>('edit'),
};
await renderPresentationPanel({ api });
expect(screen.queryByTestId('presentationPanelTitle')).not.toBeInTheDocument();
@ -295,10 +295,10 @@ describe('Presentation panel', () => {
it('hides title in view mode when parent hide title option is true', async () => {
const api: DefaultPresentationPanelApi & PublishesViewMode = {
uuid: 'test',
panelTitle: new BehaviorSubject<string | undefined>('SUPER TITLE'),
viewMode: new BehaviorSubject<ViewMode>('view'),
title$: new BehaviorSubject<string | undefined>('SUPER TITLE'),
viewMode$: new BehaviorSubject<ViewMode>('view'),
parentApi: {
viewMode: new BehaviorSubject<ViewMode>('view'),
viewMode$: new BehaviorSubject<ViewMode>('view'),
...getMockPresentationContainer(),
},
};
@ -309,10 +309,10 @@ describe('Presentation panel', () => {
it('hides title in edit mode when parent hide title option is true', async () => {
const api: DefaultPresentationPanelApi & PublishesViewMode = {
uuid: 'test',
panelTitle: new BehaviorSubject<string | undefined>('SUPER TITLE'),
viewMode: new BehaviorSubject<ViewMode>('edit'),
title$: new BehaviorSubject<string | undefined>('SUPER TITLE'),
viewMode$: new BehaviorSubject<ViewMode>('edit'),
parentApi: {
viewMode: new BehaviorSubject<ViewMode>('edit'),
viewMode$: new BehaviorSubject<ViewMode>('edit'),
...getMockPresentationContainer(),
},
};

View file

@ -50,8 +50,8 @@ export const PresentationPanelInternal = <
const dragHandles = useRef<{ [dragHandleKey: string]: HTMLElement | null }>({});
const viewModeSubject = (() => {
if (apiPublishesViewMode(api)) return api.viewMode;
if (apiHasParentApi(api) && apiPublishesViewMode(api.parentApi)) return api.parentApi.viewMode;
if (apiPublishesViewMode(api)) return api.viewMode$;
if (apiHasParentApi(api) && apiPublishesViewMode(api.parentApi)) return api.parentApi.viewMode$;
})();
const [
@ -65,20 +65,20 @@ export const PresentationPanelInternal = <
rawViewMode,
parentHidePanelTitle,
] = useBatchedOptionalPublishingSubjects(
api?.dataLoading,
api?.blockingError,
api?.panelTitle,
api?.hidePanelTitle,
api?.panelDescription,
api?.defaultPanelTitle,
api?.defaultPanelDescription,
api?.dataLoading$,
api?.blockingError$,
api?.title$,
api?.hideTitle$,
api?.description$,
api?.defaultTitle$,
api?.defaultDescription$,
viewModeSubject,
api?.parentApi?.hidePanelTitle
api?.parentApi?.hideTitle$
);
const viewMode = rawViewMode ?? 'view';
const [initialLoadComplete, setInitialLoadComplete] = useState(!dataLoading);
if (!initialLoadComplete && (dataLoading === false || (api && !api.dataLoading))) {
if (!initialLoadComplete && (dataLoading === false || (api && !api.dataLoading$))) {
setInitialLoadComplete(true);
}

View file

@ -15,8 +15,8 @@ import {
PublishesBlockingError,
PublishesDataLoading,
PublishesDisabledActionIds,
PublishesPanelDescription,
PublishesPanelTitle,
PublishesDescription,
PublishesTitle,
PublishesViewMode,
} from '@kbn/presentation-publishing';
import { UiActionsService } from '@kbn/ui-actions-plugin/public';
@ -74,14 +74,13 @@ export interface PresentationPanelInternalProps<
export interface DefaultPresentationPanelApi
extends HasUniqueId,
Partial<
PublishesPanelTitle &
PublishesTitle &
PublishesDataLoading &
PublishesBlockingError &
PublishesPanelDescription &
PublishesDescription &
PublishesDisabledActionIds &
HasParentApi<
PresentationContainer &
Partial<Pick<PublishesPanelTitle, 'hidePanelTitle'> & PublishesViewMode>
PresentationContainer & Partial<Pick<PublishesTitle, 'hideTitle$'> & PublishesViewMode>
> &
CanLockHoverActions
> {}

View file

@ -14,7 +14,7 @@ import { ClearControlAction } from './clear_control_action';
import type { ViewMode } from '@kbn/presentation-publishing';
const dashboardApi = {
viewMode: new BehaviorSubject<ViewMode>('view'),
viewMode$: new BehaviorSubject<ViewMode>('view'),
};
const controlGroupApi = getMockedControlGroupApi(dashboardApi, {
removePanel: jest.fn(),

View file

@ -17,7 +17,7 @@ import { coreServices } from '../services/kibana_services';
import { DeleteControlAction } from './delete_control_action';
const dashboardApi = {
viewMode: new BehaviorSubject<ViewMode>('view'),
viewMode$: new BehaviorSubject<ViewMode>('view'),
};
const controlGroupApi = getMockedControlGroupApi(dashboardApi, {
removePanel: jest.fn(),

View file

@ -29,7 +29,7 @@ dataService.query.timefilter.timefilter.calculateBounds = (timeRange: TimeRange)
};
const dashboardApi = {
viewMode: new BehaviorSubject<ViewMode>('view'),
viewMode$: new BehaviorSubject<ViewMode>('view'),
};
const controlGroupApi = getMockedControlGroupApi(dashboardApi, {
removePanel: jest.fn(),
@ -88,7 +88,7 @@ describe('Incompatible embeddables', () => {
describe('Compatible embeddables', () => {
beforeAll(() => {
dashboardApi.viewMode.next('edit');
dashboardApi.viewMode$.next('edit');
});
test('Action is compatible with embeddables that are editable', async () => {

View file

@ -31,8 +31,8 @@ export const ControlClone = ({
}) => {
const [width, panelTitle, defaultPanelTitle] = useBatchedPublishingSubjects(
controlApi ? controlApi.width : new BehaviorSubject(DEFAULT_CONTROL_GROW),
controlApi?.panelTitle ? controlApi.panelTitle : new BehaviorSubject(undefined),
controlApi?.defaultPanelTitle ? controlApi.defaultPanelTitle : new BehaviorSubject('')
controlApi?.title$ ? controlApi.title$ : new BehaviorSubject(undefined),
controlApi?.defaultTitle$ ? controlApi.defaultTitle$ : new BehaviorSubject('')
);
return (

View file

@ -87,7 +87,7 @@ export const ControlPanel = <ApiType extends DefaultControlApi = DefaultControlA
apiHasParentApi(api.parentApi) && // api.parentApi => controlGroupApi
apiPublishesViewMode(api.parentApi.parentApi) // controlGroupApi.parentApi => dashboardApi
)
return api.parentApi.parentApi.viewMode; // get view mode from dashboard API
return api.parentApi.parentApi.viewMode$; // get view mode from dashboard API
})();
const [
@ -101,21 +101,21 @@ export const ControlPanel = <ApiType extends DefaultControlApi = DefaultControlA
disabledActionIds,
rawViewMode,
] = useBatchedOptionalPublishingSubjects(
api?.dataLoading,
api?.blockingError,
api?.panelTitle,
api?.defaultPanelTitle,
api?.dataLoading$,
api?.blockingError$,
api?.title$,
api?.defaultTitle$,
api?.grow,
api?.width,
api?.parentApi?.labelPosition,
api?.parentApi?.disabledActionIds,
api?.parentApi?.disabledActionIds$,
viewModeSubject
);
const usingTwoLineLayout = labelPosition === 'twoLine';
const controlType = api ? api.type : undefined;
const [initialLoadComplete, setInitialLoadComplete] = useState(!dataLoading);
if (!initialLoadComplete && (dataLoading === false || (api && !api.dataLoading))) {
if (!initialLoadComplete && (dataLoading === false || (api && !api.dataLoading$))) {
setInitialLoadComplete(true);
}

View file

@ -116,7 +116,7 @@ export const ControlGroupRenderer = ({
*/
useEffect(() => {
if (!controlGroup) return;
const stateChangeSubscription = controlGroup.unsavedChanges.subscribe((changes) => {
const stateChangeSubscription = controlGroup.unsavedChanges$.subscribe((changes) => {
runtimeState$.next({ ...runtimeState$.getValue(), ...changes });
});
return () => {
@ -168,8 +168,8 @@ export const ControlGroupRenderer = ({
type={CONTROL_GROUP_TYPE}
getParentApi={() => ({
reload$,
dataLoading: dataLoading$,
viewMode: viewMode$,
dataLoading$,
viewMode$,
query$: searchApi.query$,
timeRange$: searchApi.timeRange$,
unifiedSearchFilters$: searchApi.filters$,

View file

@ -55,8 +55,8 @@ export function initializeControlGroupUnsavedChanges(
return {
api: {
unsavedChanges: combineLatest([
controlGroupUnsavedChanges.api.unsavedChanges,
unsavedChanges$: combineLatest([
controlGroupUnsavedChanges.api.unsavedChanges$,
childrenUnsavedChanges$(children$),
]).pipe(
map(([unsavedControlGroupState, unsavedControlsState]) => {
@ -87,7 +87,7 @@ export function initializeControlGroupUnsavedChanges(
applySelections();
}
},
} as Pick<PublishesUnsavedChanges, 'unsavedChanges'> & {
} as Pick<PublishesUnsavedChanges, 'unsavedChanges$'> & {
asyncResetUnsavedChanges: () => Promise<void>;
},
};

View file

@ -85,7 +85,7 @@ export const getControlGroupEmbeddableFactory = () => {
...controlsManager.api,
autoApplySelections$,
});
const dataViews = new BehaviorSubject<DataView[] | undefined>(undefined);
const dataViews$ = new BehaviorSubject<DataView[] | undefined>(undefined);
const chainingSystem$ = new BehaviorSubject<ControlGroupChainingSystem>(
chainingSystem ?? DEFAULT_CONTROL_CHAINING
);
@ -130,7 +130,7 @@ export const getControlGroupEmbeddableFactory = () => {
const api = setApi({
...controlsManager.api,
disabledActionIds: disabledActionIds$,
disabledActionIds$,
...unsavedChanges.api,
...selectionsManager.api,
controlFetch$: (controlUuid: string) =>
@ -166,7 +166,7 @@ export const getControlGroupEmbeddableFactory = () => {
isEditingEnabled: () => true,
openAddDataControlFlyout: (settings) => {
const parentDataViewId = apiPublishesDataViews(parentApi)
? parentApi.dataViews.value?.[0]?.id
? parentApi.dataViews$.value?.[0]?.id
: undefined;
const newControlState = controlsManager.getNewControlState();
@ -201,7 +201,7 @@ export const getControlGroupEmbeddableFactory = () => {
references,
};
},
dataViews,
dataViews$,
labelPosition: labelPosition$,
saveNotification$: apiHasSaveNotification(parentApi)
? parentApi.saveNotification$
@ -227,8 +227,8 @@ export const getControlGroupEmbeddableFactory = () => {
const childrenDataViewsSubscription = combineCompatibleChildrenApis<
PublishesDataViews,
DataView[]
>(api, 'dataViews', apiPublishesDataViews, []).subscribe((newDataViews) =>
dataViews.next(newDataViews)
>(api, 'dataViews$', apiPublishesDataViews, []).subscribe((newDataViews) =>
dataViews$.next(newDataViews)
);
const saveNotificationSubscription = apiHasSaveNotification(parentApi)

View file

@ -53,7 +53,7 @@ export type ControlGroupApi = PresentationContainer &
PublishesDataViews &
HasSerializedChildState<ControlPanelState> &
HasEditCapabilities &
Pick<PublishesUnsavedChanges<ControlGroupRuntimeState>, 'unsavedChanges'> &
Pick<PublishesUnsavedChanges<ControlGroupRuntimeState>, 'unsavedChanges$'> &
PublishesTimeslice &
PublishesDisabledActionIds &
Partial<HasParentApi<PublishesUnifiedSearch> & HasSaveNotification & PublishesReload> & {

View file

@ -52,20 +52,20 @@ describe('initializeDataControl', () => {
controlGroupApi
);
dataControl.api.defaultPanelTitle!.pipe(skip(1), first()).subscribe(() => {
dataControl.api.defaultTitle$!.pipe(skip(1), first()).subscribe(() => {
done();
});
});
test('should set data view', () => {
const dataViews = dataControl!.api.dataViews.value;
const dataViews = dataControl!.api.dataViews$.value;
expect(dataViews).not.toBeUndefined();
expect(dataViews!.length).toBe(1);
expect(dataViews![0].id).toBe('myDataViewId');
});
test('should set default panel title', () => {
const defaultPanelTitle = dataControl!.api.defaultPanelTitle!.value;
const defaultPanelTitle = dataControl!.api.defaultTitle$!.value;
expect(defaultPanelTitle).not.toBeUndefined();
expect(defaultPanelTitle).toBe('My field name');
});
@ -86,13 +86,13 @@ describe('initializeDataControl', () => {
controlGroupApi
);
dataControl.api.dataViews.pipe(skip(1), first()).subscribe(() => {
dataControl.api.dataViews$.pipe(skip(1), first()).subscribe(() => {
done();
});
});
test('should set blocking error', () => {
const error = dataControl!.api.blockingError.value;
const error = dataControl!.api.blockingError$.value;
expect(error).not.toBeUndefined();
expect(error!.message).toBe(
'Simulated error: no data view found for id notGonnaFindMeDataViewId'
@ -100,9 +100,9 @@ describe('initializeDataControl', () => {
});
test('should clear blocking error when valid data view id provided', (done) => {
dataControl!.api.dataViews.pipe(skip(1), first()).subscribe((dataView) => {
dataControl!.api.dataViews$.pipe(skip(1), first()).subscribe((dataView) => {
expect(dataView).not.toBeUndefined();
expect(dataControl!.api.blockingError.value).toBeUndefined();
expect(dataControl!.api.blockingError$.value).toBeUndefined();
done();
});
dataControl!.stateManager.dataViewId.next('myDataViewId');
@ -124,25 +124,23 @@ describe('initializeDataControl', () => {
controlGroupApi
);
dataControl.api.defaultPanelTitle!.pipe(skip(1), first()).subscribe(() => {
dataControl.api.defaultTitle$!.pipe(skip(1), first()).subscribe(() => {
done();
});
});
test('should set blocking error', () => {
const error = dataControl!.api.blockingError.value;
const error = dataControl!.api.blockingError$.value;
expect(error).not.toBeUndefined();
expect(error!.message).toBe('Could not locate field: notGonnaFindMeFieldName');
});
test('should clear blocking error when valid field name provided', (done) => {
dataControl!.api
.defaultPanelTitle!.pipe(skip(1), first())
.subscribe((defaultPanelTitle) => {
expect(defaultPanelTitle).toBe('My field name');
expect(dataControl!.api.blockingError.value).toBeUndefined();
done();
});
dataControl!.api.defaultTitle$!.pipe(skip(1), first()).subscribe((defaultTitle) => {
expect(defaultTitle).toBe('My field name');
expect(dataControl!.api.blockingError$.value).toBeUndefined();
done();
});
dataControl!.stateManager.fieldName.next('myFieldName');
});
});

View file

@ -52,11 +52,11 @@ export const initializeDataControl = <EditorState extends object = {}>(
} => {
const defaultControl = initializeDefaultControlApi(state);
const panelTitle = new BehaviorSubject<string | undefined>(state.title);
const defaultPanelTitle = new BehaviorSubject<string | undefined>(undefined);
const title$ = new BehaviorSubject<string | undefined>(state.title);
const defaultTitle$ = new BehaviorSubject<string | undefined>(undefined);
const dataViewId = new BehaviorSubject<string>(state.dataViewId);
const fieldName = new BehaviorSubject<string>(state.fieldName);
const dataViews = new BehaviorSubject<DataView[] | undefined>(undefined);
const dataViews$ = new BehaviorSubject<DataView[] | undefined>(undefined);
const filters$ = new BehaviorSubject<Filter[] | undefined>(undefined);
const filtersReady$ = new BehaviorSubject<boolean>(false);
const field$ = new BehaviorSubject<DataViewField | undefined>(undefined);
@ -68,14 +68,14 @@ export const initializeDataControl = <EditorState extends object = {}>(
...defaultControl.stateManager,
dataViewId,
fieldName,
title: panelTitle,
title: title$,
};
const dataViewIdSubscription = dataViewId
.pipe(
tap(() => {
filtersReady$.next(false);
if (defaultControl.api.blockingError.value) {
if (defaultControl.api.blockingError$.value) {
defaultControl.api.setBlockingError(undefined);
}
}),
@ -93,10 +93,10 @@ export const initializeDataControl = <EditorState extends object = {}>(
if (error) {
defaultControl.api.setBlockingError(error);
}
dataViews.next(dataView ? [dataView] : undefined);
dataViews$.next(dataView ? [dataView] : undefined);
});
const fieldNameSubscription = combineLatest([dataViews, fieldName])
const fieldNameSubscription = combineLatest([dataViews$, fieldName])
.pipe(
tap(() => {
filtersReady$.next(false);
@ -120,12 +120,12 @@ export const initializeDataControl = <EditorState extends object = {}>(
})
)
);
} else if (defaultControl.api.blockingError.value) {
} else if (defaultControl.api.blockingError$.value) {
defaultControl.api.setBlockingError(undefined);
}
field$.next(field);
defaultPanelTitle.next(field ? field.displayName || field.name : nextFieldName);
defaultTitle$.next(field ? field.displayName || field.name : nextFieldName);
const spec = field?.toSpec();
if (spec) {
fieldFormatter.next(dataView.getFormatterForField(spec).getConverterFor('text'));
@ -172,7 +172,7 @@ export const initializeDataControl = <EditorState extends object = {}>(
},
controlType,
controlId,
initialDefaultPanelTitle: defaultPanelTitle.getValue(),
initialDefaultPanelTitle: defaultTitle$.getValue(),
controlGroupApi,
});
};
@ -186,9 +186,9 @@ export const initializeDataControl = <EditorState extends object = {}>(
const api: ControlApiInitialization<DataControlApi> = {
...defaultControl.api,
panelTitle,
defaultPanelTitle,
dataViews,
title$,
defaultTitle$,
dataViews$,
field$,
fieldFormatter,
onEdit,
@ -196,7 +196,7 @@ export const initializeDataControl = <EditorState extends object = {}>(
isEditingEnabled: () => true,
untilFiltersReady: async () => {
return new Promise((resolve) => {
combineLatest([defaultControl.api.blockingError, filtersReady$])
combineLatest([defaultControl.api.blockingError$, filtersReady$])
.pipe(
first(([blockingError, filtersReady]) => filtersReady || blockingError !== undefined)
)
@ -216,7 +216,7 @@ export const initializeDataControl = <EditorState extends object = {}>(
},
comparators: {
...defaultControl.comparators,
title: [panelTitle, (value: string | undefined) => panelTitle.next(value)],
title: [title$, (value: string | undefined) => title$.next(value)],
dataViewId: [dataViewId, (value: string) => dataViewId.next(value)],
fieldName: [fieldName, (value: string) => fieldName.next(value)],
},
@ -235,7 +235,7 @@ export const initializeDataControl = <EditorState extends object = {}>(
...defaultControl.serialize().rawState,
dataViewId: dataViewId.getValue(),
fieldName: fieldName.getValue(),
title: panelTitle.getValue(),
title: title$.getValue(),
},
references: [
{

View file

@ -31,7 +31,7 @@ export const getOptionsListMocks = () => {
availableOptions$: new BehaviorSubject<OptionsListSuggestions | undefined>(undefined),
invalidSelections$: new BehaviorSubject<Set<OptionsListSelection>>(new Set([])),
totalCardinality$: new BehaviorSubject<number | undefined>(undefined),
dataLoading: new BehaviorSubject<boolean>(false),
dataLoading$: new BehaviorSubject<boolean>(false),
parentApi: {
allowExpensiveQueries$: new BehaviorSubject<boolean>(true),
},

View file

@ -58,12 +58,12 @@ export const OptionsListControl = ({
stateManager.selectedOptions,
api.invalidSelections$,
api.field$,
api.dataLoading,
api.panelTitle,
api.dataLoading$,
api.title$,
api.fieldFormatter
);
const [defaultPanelTitle] = useBatchedOptionalPublishingSubjects(api.defaultPanelTitle);
const [defaultPanelTitle] = useBatchedOptionalPublishingSubjects(api.defaultTitle$);
const delimiter = useMemo(() => OptionsListStrings.control.getSeparator(field?.type), [field]);

View file

@ -23,7 +23,7 @@ export const OptionsListPopover = () => {
api.field$,
api.availableOptions$,
api.invalidSelections$,
api.dataLoading
api.dataLoading$
);
const [showOnlySelected, setShowOnlySelected] = useState(false);

View file

@ -43,7 +43,7 @@ export const OptionsListPopoverFooter = () => {
const [exclude, loading, allowExpensiveQueries] = useBatchedPublishingSubjects(
stateManager.exclude,
api.dataLoading,
api.dataLoading$,
api.parentApi.allowExpensiveQueries$
);

View file

@ -34,7 +34,7 @@ export const OptionsListPopoverInvalidSelections = () => {
api.invalidSelections$,
api.fieldFormatter
);
const defaultPanelTitle = useStateFromPublishingSubject(api.defaultPanelTitle);
const defaultPanelTitle = useStateFromPublishingSubject(api.defaultTitle$);
const [selectableOptions, setSelectableOptions] = useState<EuiSelectableOption[]>([]); // will be set in following useEffect
useEffect(() => {

View file

@ -59,7 +59,7 @@ export const OptionsListPopoverSuggestions = ({
api.invalidSelections$,
api.availableOptions$,
api.totalCardinality$,
api.dataLoading,
api.dataLoading$,
api.fieldFormatter,
api.parentApi.allowExpensiveQueries$
);

View file

@ -33,7 +33,7 @@ export function fetchAndValidate$({
api,
stateManager,
}: {
api: Pick<OptionsListControlApi, 'dataViews' | 'field$' | 'setBlockingError' | 'parentApi'> &
api: Pick<OptionsListControlApi, 'dataViews$' | 'field$' | 'setBlockingError' | 'parentApi'> &
Pick<OptionsListComponentApi, 'loadMoreSubject'> & {
controlFetch$: Observable<ControlFetchContext>;
loadingSuggestions$: BehaviorSubject<boolean>;
@ -49,7 +49,7 @@ export function fetchAndValidate$({
let abortController: AbortController | undefined;
return combineLatest([
api.dataViews,
api.dataViews$,
api.field$,
api.controlFetch$,
api.parentApi.allowExpensiveQueries$,

View file

@ -126,7 +126,7 @@ export const getOptionsListControlFactory = (): DataControlFactory<
const loadingSuggestions$ = new BehaviorSubject<boolean>(false);
const dataLoadingSubscription = combineLatest([
loadingSuggestions$,
dataControl.api.dataLoading,
dataControl.api.dataLoading$,
])
.pipe(
debounceTime(100), // debounce set loading so that it doesn't flash as the user types
@ -188,7 +188,7 @@ export const getOptionsListControlFactory = (): DataControlFactory<
if (Object.hasOwn(result, 'error')) {
dataControl.api.setBlockingError((result as { error: Error }).error);
return;
} else if (dataControl.api.blockingError.getValue()) {
} else if (dataControl.api.blockingError$.getValue()) {
// otherwise, if there was a previous error, clear it
dataControl.api.setBlockingError(undefined);
}
@ -231,7 +231,7 @@ export const getOptionsListControlFactory = (): DataControlFactory<
});
/** Output filters when selections change */
const outputFilterSubscription = combineLatest([
dataControl.api.dataViews,
dataControl.api.dataViews$,
dataControl.stateManager.fieldName,
selections.selectedOptions$,
selections.existsSelected$,
@ -263,7 +263,7 @@ export const getOptionsListControlFactory = (): DataControlFactory<
const api = buildApi(
{
...dataControl.api,
dataLoading: dataLoading$,
dataLoading$,
getTypeDisplayName: OptionsListStrings.control.getDisplayName,
serializeState: () => {
const { rawState: dataControlState, references } = dataControl.serialize();

View file

@ -147,7 +147,7 @@ describe('RangesliderControlApi', () => {
controlGroupApi
);
expect(api.filters$.value).toBeUndefined();
expect(api.blockingError.value?.message).toEqual(
expect(api.blockingError$.value?.message).toEqual(
'no data view found for id notGonnaFindMeDataView'
);
});

View file

@ -85,7 +85,7 @@ export const getRangesliderControlFactory = (): DataControlFactory<
const api = buildApi(
{
...dataControl.api,
dataLoading: dataLoading$,
dataLoading$,
getTypeDisplayName: RangeSliderStrings.control.getDisplayName,
serializeState: () => {
const { rawState: dataControlState, references } = dataControl.serialize();
@ -117,7 +117,7 @@ export const getRangesliderControlFactory = (): DataControlFactory<
const dataLoadingSubscription = combineLatest([
loadingMinMax$,
loadingHasNoResults$,
dataControl.api.dataLoading,
dataControl.api.dataLoading$,
])
.pipe(
debounceTime(100),
@ -142,11 +142,11 @@ export const getRangesliderControlFactory = (): DataControlFactory<
const min$ = new BehaviorSubject<number | undefined>(undefined);
const minMaxSubscription = minMax$({
controlFetch$,
dataViews$: dataControl.api.dataViews,
dataViews$: dataControl.api.dataViews$,
fieldName$: dataControl.stateManager.fieldName,
setIsLoading: (isLoading: boolean) => {
// clear previous loading error on next loading start
if (isLoading && dataControl.api.blockingError.value) {
if (isLoading && dataControl.api.blockingError$.value) {
dataControl.api.setBlockingError(undefined);
}
loadingMinMax$.next(isLoading);
@ -171,7 +171,7 @@ export const getRangesliderControlFactory = (): DataControlFactory<
);
const outputFilterSubscription = combineLatest([
dataControl.api.dataViews,
dataControl.api.dataViews$,
dataControl.stateManager.fieldName,
selections.value$,
])
@ -201,7 +201,7 @@ export const getRangesliderControlFactory = (): DataControlFactory<
const selectionHasNoResults$ = new BehaviorSubject(false);
const hasNotResultsSubscription = hasNoResults$({
controlFetch$,
dataViews$: dataControl.api.dataViews,
dataViews$: dataControl.api.dataViews$,
rangeFilters$: dataControl.api.filters$,
ignoreParentSettings$: controlGroupApi.ignoreParentSettings$,
setIsLoading: (isLoading: boolean) => {

View file

@ -25,7 +25,7 @@ export function hasNoResults$({
setIsLoading,
}: {
controlFetch$: Observable<ControlFetchContext>;
dataViews$?: PublishesDataViews['dataViews'];
dataViews$?: PublishesDataViews['dataViews$'];
rangeFilters$: DataControlApi['filters$'];
ignoreParentSettings$: ControlGroupApi['ignoreParentSettings$'];
setIsLoading: (isLoading: boolean) => void;

View file

@ -26,7 +26,7 @@ export function minMax$({
}: {
controlFetch$: Observable<ControlFetchContext>;
controlGroupApi: ControlGroupApi;
dataViews$: PublishesDataViews['dataViews'];
dataViews$: PublishesDataViews['dataViews$'];
fieldName$: PublishingSubject<string>;
setIsLoading: (isLoading: boolean) => void;
}) {

View file

@ -12,7 +12,7 @@ import { FieldFormatConvertFunction } from '@kbn/field-formats-plugin/common';
import {
HasEditCapabilities,
PublishesDataViews,
PublishesPanelTitle,
PublishesTitle,
PublishingSubject,
} from '@kbn/presentation-publishing';
@ -29,7 +29,7 @@ export interface PublishesField {
}
export type DataControlApi = DefaultControlApi &
Omit<PublishesPanelTitle, 'hidePanelTitle'> & // control titles cannot be hidden
Omit<PublishesTitle, 'hideTitle$'> & // control titles cannot be hidden
HasEditCapabilities &
PublishesDataViews &
PublishesField &

View file

@ -23,8 +23,8 @@ export const initializeDefaultControlApi = (
comparators: StateComparators<DefaultControlState>;
serialize: () => SerializedPanelState<DefaultControlState>;
} => {
const dataLoading = new BehaviorSubject<boolean | undefined>(false);
const blockingError = new BehaviorSubject<Error | undefined>(undefined);
const dataLoading$ = new BehaviorSubject<boolean | undefined>(false);
const blockingError$ = new BehaviorSubject<Error | undefined>(undefined);
const grow = new BehaviorSubject<boolean | undefined>(state.grow);
const width = new BehaviorSubject<ControlWidth | undefined>(state.width);
@ -32,10 +32,10 @@ export const initializeDefaultControlApi = (
api: {
grow,
width,
dataLoading,
blockingError,
setBlockingError: (error) => blockingError.next(error),
setDataLoading: (loading) => dataLoading.next(loading),
dataLoading$,
blockingError$,
setBlockingError: (error) => blockingError$.next(error),
setDataLoading: (loading) => dataLoading$.next(loading),
},
comparators: {
grow: [grow, (newGrow: boolean | undefined) => grow.next(newGrow)],

View file

@ -42,7 +42,7 @@ export const getMockedBuildApi =
...api,
uuid,
parentApi: controlGroupApi ?? getMockedControlGroupApi(),
unsavedChanges: new BehaviorSubject<Partial<StateType> | undefined>(undefined),
unsavedChanges$: new BehaviorSubject<Partial<StateType> | undefined>(undefined),
resetUnsavedChanges: () => {
return true;
},

View file

@ -47,7 +47,7 @@ describe('TimesliderControlApi', () => {
...api,
uuid,
parentApi: controlGroupApi,
unsavedChanges: new BehaviorSubject<Partial<TimesliderControlState> | undefined>(undefined),
unsavedChanges$: new BehaviorSubject<Partial<TimesliderControlState> | undefined>(undefined),
resetUnsavedChanges: () => {
return true;
},

View file

@ -194,7 +194,7 @@ export const getTimesliderControlFactory = (): ControlFactory<
const dashboardDataLoading$ =
apiHasParentApi(controlGroupApi) && apiPublishesDataLoading(controlGroupApi.parentApi)
? controlGroupApi.parentApi.dataLoading
? controlGroupApi.parentApi.dataLoading$
: new BehaviorSubject<boolean | undefined>(false);
const waitForDashboardPanelsToLoad$ = dashboardDataLoading$.pipe(
// debounce to give time for panels to start loading if they are going to load from time changes
@ -212,7 +212,7 @@ export const getTimesliderControlFactory = (): ControlFactory<
const api = buildApi(
{
...defaultControl.api,
defaultPanelTitle: new BehaviorSubject<string | undefined>(displayName),
defaultTitle$: new BehaviorSubject<string | undefined>(displayName),
timeslice$,
serializeState: () => {
const { rawState: defaultControlState } = defaultControl.serialize();

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { PublishesPanelTitle, PublishesTimeslice } from '@kbn/presentation-publishing';
import type { PublishesTitle, PublishesTimeslice } from '@kbn/presentation-publishing';
import type { DefaultControlState } from '../../../common';
import type { DefaultControlApi } from '../types';
@ -21,5 +21,5 @@ export interface TimesliderControlState extends DefaultControlState {
}
export type TimesliderControlApi = DefaultControlApi &
Pick<PublishesPanelTitle, 'defaultPanelTitle'> &
Pick<PublishesTitle, 'defaultTitle$'> &
PublishesTimeslice;

View file

@ -18,7 +18,7 @@ import {
PublishesBlockingError,
PublishesDataLoading,
PublishesDisabledActionIds,
PublishesPanelTitle,
PublishesTitle,
PublishesUnsavedChanges,
PublishingSubject,
StateComparators,
@ -35,7 +35,7 @@ export interface HasCustomPrepend {
export type DefaultControlApi = PublishesDataLoading &
PublishesBlockingError &
PublishesUnsavedChanges &
Partial<PublishesPanelTitle & PublishesDisabledActionIds & HasCustomPrepend> &
Partial<PublishesTitle & PublishesDisabledActionIds & HasCustomPrepend> &
CanClearSelections &
HasType &
HasUniqueId &
@ -49,7 +49,7 @@ export type DefaultControlApi = PublishesDataLoading &
export type ControlApiRegistration<ControlApi extends DefaultControlApi = DefaultControlApi> = Omit<
ControlApi,
'uuid' | 'parentApi' | 'type' | 'unsavedChanges' | 'resetUnsavedChanges'
'uuid' | 'parentApi' | 'type' | 'unsavedChanges$' | 'resetUnsavedChanges'
>;
export type ControlApiInitialization<ControlApi extends DefaultControlApi = DefaultControlApi> =

View file

@ -20,7 +20,7 @@ describe('Clone panel action', () => {
context = {
embeddable: {
uuid: 'superId',
viewMode: new BehaviorSubject<ViewMode>('edit'),
viewMode$: new BehaviorSubject<ViewMode>('edit'),
serializeState: () => {
return {
rawState: {},
@ -45,7 +45,7 @@ describe('Clone panel action', () => {
});
it('is incompatible when view mode is view', async () => {
(context.embeddable as PublishesViewMode).viewMode = new BehaviorSubject<ViewMode>('view');
(context.embeddable as PublishesViewMode).viewMode$ = new BehaviorSubject<ViewMode>('view');
expect(await action.isCompatible(context)).toBe(false);
});

View file

@ -58,7 +58,9 @@ export class ClonePanelAction implements Action<EmbeddableApiContext> {
public async isCompatible({ embeddable }: EmbeddableApiContext) {
if (!isApiCompatible(embeddable)) return false;
return Boolean(!embeddable.blockingError?.value && getInheritedViewMode(embeddable) === 'edit');
return Boolean(
!embeddable.blockingError$?.value && getInheritedViewMode(embeddable) === 'edit'
);
}
public async execute({ embeddable }: EmbeddableApiContext) {

View file

@ -48,7 +48,7 @@ export function CopyToDashboardModal({ api, closeModal }: CopyToDashboardModalPr
null
);
const dashboardId = api.parentApi.savedObjectId.value;
const dashboardId = api.parentApi.savedObjectId$.value;
const onSubmit = useCallback(() => {
const dashboard = api.parentApi;

View file

@ -13,17 +13,17 @@ import { ExpandPanelActionApi, ExpandPanelAction } from './expand_panel_action';
describe('Expand panel action', () => {
let action: ExpandPanelAction;
let context: { embeddable: ExpandPanelActionApi };
let expandPanelIdSubject: BehaviorSubject<string | undefined>;
let expandedPanelId$: BehaviorSubject<string | undefined>;
beforeEach(() => {
expandPanelIdSubject = new BehaviorSubject<string | undefined>(undefined);
expandedPanelId$ = new BehaviorSubject<string | undefined>(undefined);
action = new ExpandPanelAction();
context = {
embeddable: {
uuid: 'superId',
parentApi: {
expandPanel: jest.fn(),
expandedPanelId: expandPanelIdSubject,
expandedPanelId$,
},
},
};
@ -43,19 +43,19 @@ describe('Expand panel action', () => {
it('calls onChange when expandedPanelId changes', async () => {
const onChange = jest.fn();
action.subscribeToCompatibilityChanges(context, onChange);
expandPanelIdSubject.next('superPanelId');
expandedPanelId$.next('superPanelId');
expect(onChange).toHaveBeenCalledWith(true, action);
});
it('returns the correct icon based on expanded panel id', async () => {
expect(await action.getIconType(context)).toBe('expand');
expandPanelIdSubject.next('superPanelId');
expandedPanelId$.next('superPanelId');
expect(await action.getIconType(context)).toBe('minimize');
});
it('returns the correct display name based on expanded panel id', async () => {
expect(await action.getDisplayName(context)).toBe('Maximize');
expandPanelIdSubject.next('superPanelId');
expandedPanelId$.next('superPanelId');
expect(await action.getDisplayName(context)).toBe('Minimize');
});

View file

@ -34,14 +34,14 @@ export class ExpandPanelAction implements Action<EmbeddableApiContext> {
public getDisplayName({ embeddable }: EmbeddableApiContext) {
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
return embeddable.parentApi.expandedPanelId.value
return embeddable.parentApi.expandedPanelId$.value
? dashboardExpandPanelActionStrings.getMinimizeTitle()
: dashboardExpandPanelActionStrings.getMaximizeTitle();
}
public getIconType({ embeddable }: EmbeddableApiContext) {
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
return embeddable.parentApi.expandedPanelId.value ? 'minimize' : 'expand';
return embeddable.parentApi.expandedPanelId$.value ? 'minimize' : 'expand';
}
public async isCompatible({ embeddable }: EmbeddableApiContext) {
@ -57,7 +57,7 @@ export class ExpandPanelAction implements Action<EmbeddableApiContext> {
onChange: (isCompatible: boolean, action: ExpandPanelAction) => void
) {
if (!isApiCompatible(embeddable)) return;
return embeddable.parentApi.expandedPanelId.pipe(skip(1)).subscribe(() => {
return embeddable.parentApi.expandedPanelId$.pipe(skip(1)).subscribe(() => {
onChange(isApiCompatible(embeddable), this);
});
}

View file

@ -18,11 +18,7 @@ import {
apiHasInspectorAdapters,
type Adapters,
} from '@kbn/inspector-plugin/public';
import {
EmbeddableApiContext,
PublishesPanelTitle,
getPanelTitle,
} from '@kbn/presentation-publishing';
import { EmbeddableApiContext, PublishesTitle, getTitle } from '@kbn/presentation-publishing';
import { coreServices, fieldFormatService } from '../services/kibana_services';
import { dashboardExportCsvActionStrings } from './_dashboard_actions_strings';
import { ACTION_EXPORT_CSV } from './constants';
@ -32,7 +28,7 @@ export type ExportContext = EmbeddableApiContext & {
asString?: boolean;
};
export type ExportCsvActionApi = HasInspectorAdapters & Partial<PublishesPanelTitle>;
export type ExportCsvActionApi = HasInspectorAdapters & Partial<PublishesTitle>;
const isApiCompatible = (api: unknown | null): api is ExportCsvActionApi =>
Boolean(apiHasInspectorAdapters(api));
@ -90,7 +86,7 @@ export class ExportCSVAction implements Action<ExportContext> {
const postFix = datatables.length > 1 ? `-${i + 1}` : '';
const untitledFilename = dashboardExportCsvActionStrings.getUntitledFilename();
memo[`${getPanelTitle(embeddable) || untitledFilename}${postFix}.csv`] = {
memo[`${getTitle(embeddable) || untitledFilename}${postFix}.csv`] = {
content: exporters.datatableToCSV(datatable, {
csvSeparator: coreServices.uiSettings.get('csv:separator', ','),
quoteValues: coreServices.uiSettings.get('csv:quoteValues', true),

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