Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ import {
EuiRadio,
EuiSpacer,
} from '@elastic/eui';
import { EmbeddablePackageState, PanelNotFoundError } from '@kbn/embeddable-plugin/public';
import { apiHasSnapshottableState } from '@kbn/presentation-publishing';
import { EmbeddablePackageState } from '@kbn/embeddable-plugin/public';
import { LazyDashboardPicker, withSuspense } from '@kbn/presentation-util-plugin/public';
import { omit } from 'lodash';
import React, { useCallback, useMemo, useState } from 'react';
Expand All @@ -28,6 +27,7 @@ import { embeddableService } from '../services/kibana_services';
import { getDashboardCapabilities } from '../utils/get_dashboard_capabilities';
import { dashboardCopyToDashboardActionStrings } from './_dashboard_actions_strings';
import { CopyToDashboardAPI } from './copy_to_dashboard_action';
import { DashboardApi } from '../dashboard_api/types';

interface CopyToDashboardModalProps {
api: CopyToDashboardAPI;
Expand All @@ -51,18 +51,14 @@ export function CopyToDashboardModal({ api, closeModal }: CopyToDashboardModalPr
const dashboardId = api.parentApi.savedObjectId$.value;

const onSubmit = useCallback(() => {
const dashboard = api.parentApi;
const dashboard = api.parentApi as DashboardApi;
const panelToCopy = dashboard.getDashboardPanelFromId(api.uuid);
const runtimeSnapshot = apiHasSnapshottableState(api) ? api.snapshotRuntimeState() : undefined;

if (!panelToCopy && !runtimeSnapshot) {
throw new PanelNotFoundError();
}

const state: EmbeddablePackageState = {
type: panelToCopy.type,
input: runtimeSnapshot ?? {
...omit(panelToCopy.explicitInput, 'id'),
serializedState: {
rawState: { ...omit(panelToCopy.explicitInput, 'id') },
references: panelToCopy.references ?? [],
},
size: {
width: panelToCopy.gridData.w,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export function getDashboardApi({
const panelsManager = initializePanelsManager(
incomingEmbeddable,
initialState.panels,
initialPanelsRuntimeState ?? {},
incomingEmbeddable || !initialPanelsRuntimeState ? {} : initialPanelsRuntimeState,
trackPanel,
getPanelReferences,
pushPanelReferences
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,52 +67,44 @@ export function initializePanelsManager(
// Place the incoming embeddable if there is one
// --------------------------------------------------------------------------------------
if (incomingEmbeddable) {
const incomingPanelId = incomingEmbeddable.embeddableId ?? v4();
let incomingPanelState: DashboardPanelState;
if (incomingEmbeddable.embeddableId && Boolean(panels$.value[incomingPanelId])) {
// this embeddable already exists, just update the explicit input.
incomingPanelState = panels$.value[incomingPanelId];
const sameType = incomingPanelState.type === incomingEmbeddable.type;

incomingPanelState.type = incomingEmbeddable.type;
setRuntimeStateForChild(incomingPanelId, {
// if the incoming panel is the same type as what was there before we can safely spread the old panel's explicit input
...(sameType ? incomingPanelState.explicitInput : {}),

...incomingEmbeddable.input,

// maintain hide panel titles setting.
hidePanelTitles: (incomingPanelState.explicitInput as { hidePanelTitles?: boolean })
.hidePanelTitles,
});
incomingPanelState.explicitInput = {};
} else {
// otherwise this incoming embeddable is brand new.
setRuntimeStateForChild(incomingPanelId, incomingEmbeddable.input);
const { serializedState, size, type } = incomingEmbeddable;
const newId = incomingEmbeddable.embeddableId ?? v4();
const existingPanel: DashboardPanelState | undefined = panels$.value[newId];
const sameType = existingPanel?.type === type;

const placeIncomingPanel = () => {
const { newPanelPlacement } = runPanelPlacementStrategy(
PanelPlacementStrategy.findTopLeftMostOpenSpace,
{
width: incomingEmbeddable.size?.width ?? DEFAULT_PANEL_WIDTH,
height: incomingEmbeddable.size?.height ?? DEFAULT_PANEL_HEIGHT,
width: size?.width ?? DEFAULT_PANEL_WIDTH,
height: size?.height ?? DEFAULT_PANEL_HEIGHT,
currentPanels: panels$.value,
}
);
incomingPanelState = {
explicitInput: {},
type: incomingEmbeddable.type,
gridData: {
...newPanelPlacement,
i: incomingPanelId,
},
};
return { ...newPanelPlacement, i: newId };
};
if (serializedState?.references && serializedState.references.length > 0) {
pushReferences(prefixReferencesFromPanel(newId, serializedState.references ?? []));
}

const gridData = existingPanel ? existingPanel.gridData : placeIncomingPanel();
const explicitInput = {
...(sameType ? existingPanel?.explicitInput : {}),
...serializedState.rawState,
};

const incomingPanelState: DashboardPanelState = {
type,
explicitInput,
gridData,
};

setPanels({
...panels$.value,
[incomingPanelId]: incomingPanelState,
[newId]: incomingPanelState,
});
trackPanel.setScrollToPanelId(incomingPanelId);
trackPanel.setHighlightPanelId(incomingPanelId);
trackPanel.setScrollToPanelId(newId);
trackPanel.setHighlightPanelId(newId);
}

async function untilEmbeddableLoaded<ApiType>(id: string): Promise<ApiType | undefined> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,15 @@
import React, { useCallback, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import useAsync from 'react-use/lib/useAsync';
import { v4 as uuidv4 } from 'uuid';
import {
getESQLAdHocDataview,
getESQLQueryColumns,
getIndexForESQLQuery,
getInitialESQLQuery,
} from '@kbn/esql-utils';
import { withSuspense } from '@kbn/shared-ux-utility';
import type { TypedLensByValueInput } from '@kbn/lens-plugin/public';
import type { LensSerializedState } from '@kbn/lens-plugin/public';
import { getLensAttributesFromSuggestion } from '@kbn/visualization-utils';

import {
coreServices,
dataService,
Expand All @@ -33,10 +31,6 @@ import {
import { getDashboardBackupService } from '../../services/dashboard_backup_service';
import { getDashboardContentManagementService } from '../../services/dashboard_content_management_service';

function generateId() {
return uuidv4();
}

export const DashboardAppNoDataPage = ({
onDataViewCreated,
}: {
Expand Down Expand Up @@ -100,27 +94,26 @@ export const DashboardAppNoDataPage = ({
if (chartSuggestions?.length) {
const [suggestion] = chartSuggestions;

const attrs = getLensAttributesFromSuggestion({
filters: [],
query: {
esql: esqlQuery,
},
suggestion,
dataView,
}) as TypedLensByValueInput['attributes'];

const lensEmbeddableInput = {
attributes: attrs,
id: generateId(),
};

await embeddableService.getStateTransfer().navigateToWithEmbeddablePackage('dashboards', {
state: {
type: 'lens',
input: lensEmbeddableInput,
},
path: '#/create',
});
await embeddableService
.getStateTransfer()
.navigateToWithEmbeddablePackage<LensSerializedState>('dashboards', {
state: {
type: 'lens',
serializedState: {
rawState: {
attributes: getLensAttributesFromSuggestion({
filters: [],
query: {
esql: esqlQuery,
},
suggestion,
dataView,
}),
},
},
},
path: '#/create',
});
}
} catch (error) {
if (error.name !== 'AbortError') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,13 @@ describe('embeddable state transfer', () => {

it('can send an outgoing embeddable package state', async () => {
await stateTransfer.navigateToWithEmbeddablePackage(destinationApp, {
state: { type: 'coolestType', input: { savedObjectId: '150' } },
state: { type: 'coolestType', serializedState: { rawState: { savedObjectId: '150' } } },
});
expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, {
[EMBEDDABLE_PACKAGE_STATE_KEY]: {
[destinationApp]: {
type: 'coolestType',
input: { savedObjectId: '150' },
serializedState: { rawState: { savedObjectId: '150' } },
},
},
});
Expand All @@ -145,14 +145,14 @@ describe('embeddable state transfer', () => {
kibanaIsNowForSports: 'extremeSportsKibana',
});
await stateTransfer.navigateToWithEmbeddablePackage(destinationApp, {
state: { type: 'coolestType', input: { savedObjectId: '150' } },
state: { type: 'coolestType', serializedState: { rawState: { savedObjectId: '150' } } },
});
expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, {
kibanaIsNowForSports: 'extremeSportsKibana',
[EMBEDDABLE_PACKAGE_STATE_KEY]: {
[destinationApp]: {
type: 'coolestType',
input: { savedObjectId: '150' },
serializedState: { rawState: { savedObjectId: '150' } },
},
},
});
Expand All @@ -163,7 +163,7 @@ describe('embeddable state transfer', () => {

it('sets isTransferInProgress to true when sending an outgoing embeddable package state', async () => {
await stateTransfer.navigateToWithEmbeddablePackage(destinationApp, {
state: { type: 'coolestType', input: { savedObjectId: '150' } },
state: { type: 'coolestType', serializedState: { rawState: { savedObjectId: '150' } } },
});
expect(stateTransfer.isTransferInProgress).toEqual(true);
currentAppId$.next(destinationApp);
Expand Down Expand Up @@ -220,34 +220,40 @@ describe('embeddable state transfer', () => {
[EMBEDDABLE_PACKAGE_STATE_KEY]: {
[testAppId]: {
type: 'skisEmbeddable',
input: { savedObjectId: '123' },
serializedState: { rawState: { savedObjectId: '123' } },
},
},
});
const fetchedState = stateTransfer.getIncomingEmbeddablePackage(testAppId);
expect(fetchedState).toEqual({ type: 'skisEmbeddable', input: { savedObjectId: '123' } });
expect(fetchedState).toEqual({
type: 'skisEmbeddable',
serializedState: { rawState: { savedObjectId: '123' } },
});
});

it('can fetch an incoming embeddable package state and ignore state for other apps', async () => {
store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, {
[EMBEDDABLE_PACKAGE_STATE_KEY]: {
[testAppId]: {
type: 'skisEmbeddable',
input: { savedObjectId: '123' },
serializedState: { rawState: { savedObjectId: '123' } },
},
testApp2: {
type: 'crossCountryEmbeddable',
input: { savedObjectId: '456' },
serializedState: { rawState: { savedObjectId: '456' } },
},
},
});
const fetchedState = stateTransfer.getIncomingEmbeddablePackage(testAppId);
expect(fetchedState).toEqual({ type: 'skisEmbeddable', input: { savedObjectId: '123' } });
expect(fetchedState).toEqual({
type: 'skisEmbeddable',
serializedState: { rawState: { savedObjectId: '123' } },
});

const fetchedState2 = stateTransfer.getIncomingEmbeddablePackage('testApp2');
expect(fetchedState2).toEqual({
type: 'crossCountryEmbeddable',
input: { savedObjectId: '456' },
serializedState: { rawState: { savedObjectId: '456' } },
});
});

Expand All @@ -268,7 +274,7 @@ describe('embeddable state transfer', () => {
[EMBEDDABLE_PACKAGE_STATE_KEY]: {
[testAppId]: {
type: 'coolestType',
input: { savedObjectId: '150' },
serializedState: { rawState: { savedObjectId: '150' } },
},
},
iSHouldStillbeHere: 'doing the sports thing',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,18 @@ export class EmbeddableStateTransfer {
* A wrapper around the {@link ApplicationStart.navigateToApp} method which navigates to the specified appId
* with {@link EmbeddablePackageState | embeddable package state}
*/
public async navigateToWithEmbeddablePackage(
public async navigateToWithEmbeddablePackage<SerializedStateType extends object = object>(
appId: string,
options?: { path?: string; state: EmbeddablePackageState }
options?: { path?: string; state: EmbeddablePackageState<SerializedStateType> }
): Promise<void> {
this.isTransferInProgress = true;
await this.navigateToWithState<EmbeddablePackageState>(appId, EMBEDDABLE_PACKAGE_STATE_KEY, {
...options,
});
await this.navigateToWithState<EmbeddablePackageState<SerializedStateType>>(
appId,
EMBEDDABLE_PACKAGE_STATE_KEY,
{
...options,
}
);
}

private getIncomingState<IncomingStateType>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { SerializedPanelState } from '@kbn/presentation-publishing';

export const EMBEDDABLE_EDITOR_STATE_KEY = 'embeddable_editor_state';

/**
Expand Down Expand Up @@ -36,12 +38,9 @@ export const EMBEDDABLE_PACKAGE_STATE_KEY = 'embeddable_package_state';
* A state package that contains all fields necessary to create or update an embeddable by reference or by value in a container.
* @public
*/
export interface EmbeddablePackageState {
export interface EmbeddablePackageState<SerializedStateType extends object = object> {
type: string;
/**
* For react embeddables, this input must be runtime state.
*/
input: object;
serializedState: SerializedPanelState<SerializedStateType>;
embeddableId?: string;
size?: {
width?: number;
Expand All @@ -57,7 +56,7 @@ export interface EmbeddablePackageState {
export function isEmbeddablePackageState(state: unknown): state is EmbeddablePackageState {
return (
ensureFieldOfTypeExists('type', state, 'string') &&
ensureFieldOfTypeExists('input', state, 'object')
ensureFieldOfTypeExists('serializedState', state, 'object')
);
}

Expand Down
Loading