mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Lens] Embeddable react refactor (#186642)
## Summary This PR contains the refactor of the Lens embeddable with the new React architecture. fix https://github.com/elastic/kibana/issues/174957 fixes https://github.com/elastic/kibana/issues/180672 **Current status**: ✅ Ready to review ### Notes for testing and reviewers Other than reworking the Lens embeddable with the new architecture this PR contains the following major changes. #### Edit flow The `Edit` flow has changed to in-line first using the new `Edit` API provided by the new system * The impact of this change can be noticed in the code on the `Canvas` case where the Custom Lens component is instructed to avoid the inline editing. In all the other cases in-line editing is enabled by default now. * Another side effect of this has been the replacement of the special `INLINE_EDIT` action id into the regular `EDIT` action. Some tests have been affected by this replacing the `clickEdit` function with the `openEditorFromFlyout` one. * The Inline editing codebase **as been reworked entirely** so make sure to stress test this side of things. #### Attribute service Another important aspect changed in this PR is the `attributeService`: this was tied to the previous Embeddable system and it is now completely skipped. The Lens wrapper around that has been reworked to be thinner and directly call the CM services. * Please make sure to test thoroughly save/load SO flows #### Transformation API (by-value <=> by-reference flow) The new system adopts the new Transformation API (who prevents the panel to fully reload on change). * Please make sure to test thoroughly Visualize library <=> by value flows * In particular moving from one type and another should change how the Panel Settings interpret "default" values to reset #### Message system Also this part of the code was partially rewritten to be more manageable ont he embeddable surface, maintaining the core functionalities. * Please make sure to test thoroughly error messages, warnings and info messages * Some scenarios to test includes * multi-layer errors (i.e. use a broken KQL query for an annotation/multi-layers). Check that the panel recovers correctly from it when resolved * Missing references * Missing dataViews * Wrong formatted SO * Configuration mistakes - check that a broken config is not saveable ### Other areas to check * Change filters in dashboard/viz and check that are correctly handled * Check drilldowns * Check that `Unsaved changes` are correctly detected * Check that the panel updates correctly on `View` mode change ## Main type changes This PR contains also some important `type` changes, here's listed: * the `query` property now explicitly supports ES|QL query type. * in `main` it used to work without type support * `LensEmbeddableInput`/`LensEmbeddableOutput` types have changed, but the type names remained the same. ## Follow ups already planned: Some enhancements have been already collected and will be addressed in a follow up [here](https://github.com/elastic/kibana/issues/195355) ### Tasks <details> <summary>Detailed list of tasks for the refactor</summary> * New embeddable factory * [x] Define visualization context * [x] Define observables to track * [x] Basic panel settings * [x] Basic edit api * [x] inspector api * [x] Library services * [x] Unified search api * [x] Basic integrations api * [x] State management api for inline editing * Publish correct observables * [x] `dataViews` * [x] `query` * [x] `filters` * [x] `dataLoading` * [x] `savedObjectId` * Actions * [x] View underlying data api * Custom renderer * [x] Basic implementation * [x] Support callbacks * [x] Support custom styling/paddings * Expose * [x] Handle searchSession * Edit * [x] Open panel in Lens editor * Inline editing * [x] rework references logic * #180726 * integrate the logic to extract filters dataViews from filters as for the first bug in #188545 * DSL flyout * [x] open flyout * [x] save * ES|QL * [x] open flyout on creation * [x] open flyout on editing * [x] save * [x] revisit mounting logic to avoid detach if possible (not possible yet) * [x] explore the integration with the new `onEdit` api method used for the inline editing~~ * [x] created panel management module and sorted it out * [x] open in Editor * [x] fix the save on return to dashboard * ~~migrate by ref to by value on inline editing~~ will do it in a follow up PR * Add from library issues * [x] Fix missing title and tags * Data loading * [x] Compute all required data params for rendering * Render the panel * [x] hook up user messaging system * [x] Merge search context * [x] Expression variables * [x] panel settings * [x] per panel time range * [x] per panel filter * test with both DSL and ES|QL mode * Reload * [x] on unified search updates * [x] on config changes * [x] on drilldown changes? * [x] on view mode change * Attributes service * [x] load from library * [x] save to library </details> ### Pending issues: <details> <summary>Detailed list of issues</summary> * [x] Unified histogram does not render in Discover * [x] Saving to library from context menu in dashboard doesn't save the title * [x] When adding a vis from the library the new panel has no title * [x] Vis disappears when opening inline editor and cancel * Create a viz, save and return to dashboard, then edit it and cancel. * Saving an edit inline doesn't apply the changes (i.e. changing the chart type) * [x] Changing the chart type on the layer panel leads to a crash * [x] Changing the chart type won't update the visualization (via both config panel or suggestions) * [x] Edit a dimension will stretch the panel to overflow the fly-out * [x] duplicating a dimension in the inline editor by drag and drop works buggy visually * When duplicating a panel, the new panel gets the same title rather than “title (copy)” * [x] by-value panels * [x] by-reference panels * [x] brushing throughout the timerange doesn’t work * [x] filtering when clicking on value doesn’t work * [x] filtering from legend doesn’t work * [x] for lens table, the sort ascending/descending actions don’t have an effect * [x] filtering doesn’t display on table either * Discover related issues * thanks to @davismcphee investigation the source of the issue seems to be related to the way the `abortController` is managed in the new embeddable implementation as Discover is relying on that. * [x] needs to investigate for a fix that restores the previous behaviour of the `abortController` management * [x] the hits total count is not in sync with the chart/table now * [x] Change chart type via suggestion panel when inline editing in Discover doesn't update the chart * [x] Dirty panel issue (see @nickofthyme 's [comment](https://github.com/elastic/kibana/pull/186642#discussion_r1792659477) ) * [x] `Unsaved changes` issue (see @mbondyra [comment](https://github.com/elastic/kibana/pull/186642#discussion_r1795384587)) * [x] Multiple errors not rendered correctly in panel when blocking (i.e. missing field - `lens-message-list-trigger` related) * [x] recover from a blocker error required 2 renders * Missing SO error should not be handled for the custom render component (legacy behaviour) but should be correctly handled for dashboard (will be handled in a follow up PR given that is broken on `main` too) * [x] Too many requests on Unified Histogram when in Discover (3 vs 2) * [x] Too many request on slow queries for Unified Histogram (2 vs 1) * [x] Annotations preview issues (chart rendering with height `0px`) * [x] `uuid` not propagated correctly * [x] another flavour of this was `id` not propagated correctly into the `data-test-embeddable-id` attribute * [x] Dispatch correctly the `render` events * [x] refresh interval does not propagate thru the Lens custom component in Discover (thanks to @jughosta to sort this out ) </details> --------- Co-authored-by: Marta Bondyra <4283304+mbondyra@users.noreply.github.com> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Marco Vettorello <vettorello.marco@gmail.com> Co-authored-by: Marta Bondyra <marta.bondyra@elastic.co> Co-authored-by: Bhavya RM <bhavya@elastic.co> Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
parent
aead7b9acd
commit
61d0320c64
208 changed files with 8939 additions and 6891 deletions
|
@ -65,6 +65,7 @@ export const ReactEmbeddableRenderer = <
|
|||
| 'hideLoader'
|
||||
| 'hideHeader'
|
||||
| 'hideInspector'
|
||||
| 'getActions'
|
||||
>;
|
||||
hidePanelChrome?: boolean;
|
||||
/**
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
EmbeddableComponent,
|
||||
FieldBasedIndexPatternColumn,
|
||||
TypedLensByValueInput,
|
||||
LensByValueInput,
|
||||
} from '@kbn/lens-plugin/public';
|
||||
import { Datatable } from '@kbn/expressions-plugin/common';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
|
@ -27,7 +28,6 @@ import '@testing-library/jest-dom';
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { GroupPreview } from './group_preview';
|
||||
import { LensByValueInput } from '@kbn/lens-plugin/public/embeddable';
|
||||
import { DATA_LAYER_ID, DATE_HISTOGRAM_COLUMN_ID, getCurrentTimeField } from './lens_attributes';
|
||||
import { EuiSuperDatePickerTestHarness } from '@kbn/test-eui-helpers';
|
||||
|
||||
|
|
|
@ -198,28 +198,25 @@ export const GroupPreview = ({
|
|||
justifyContent="center"
|
||||
>
|
||||
<EuiFlexItem grow={0}>
|
||||
<div
|
||||
<LensEmbeddableComponent
|
||||
css={css`
|
||||
& > div {
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<LensEmbeddableComponent
|
||||
data-test-subj="chart"
|
||||
id="annotation-library-preview"
|
||||
timeRange={chartTimeRange}
|
||||
attributes={lensAttributes}
|
||||
onBrushEnd={({ range }) =>
|
||||
setChartTimeRange({
|
||||
from: new Date(range[0]).toISOString(),
|
||||
to: new Date(range[1]).toISOString(),
|
||||
})
|
||||
}
|
||||
searchSessionId={searchSessionId}
|
||||
/>
|
||||
</div>
|
||||
data-test-subj="chart"
|
||||
id="annotation-library-preview"
|
||||
timeRange={chartTimeRange}
|
||||
attributes={lensAttributes}
|
||||
onBrushEnd={({ range }) =>
|
||||
setChartTimeRange({
|
||||
from: new Date(range[0]).toISOString(),
|
||||
to: new Date(range[1]).toISOString(),
|
||||
})
|
||||
}
|
||||
searchSessionId={searchSessionId}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
|
|
|
@ -26,7 +26,7 @@ export interface ExpressionRendererParams extends IExpressionLoaderParams {
|
|||
debounce?: number;
|
||||
expression: string | ExpressionAstExpression;
|
||||
hasCustomErrorRenderer?: boolean;
|
||||
onData$?<TData, TInspectorAdapters>(
|
||||
onData$?<TData, TInspectorAdapters extends unknown>(
|
||||
data: TData,
|
||||
adapters?: TInspectorAdapters,
|
||||
partial?: boolean
|
||||
|
|
|
@ -6,13 +6,24 @@
|
|||
* 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 React from 'react';
|
||||
import { of } from 'rxjs';
|
||||
import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
||||
import { Plugin } from '.';
|
||||
import { createTopNav } from './top_nav_menu';
|
||||
|
||||
export type Setup = jest.Mocked<ReturnType<Plugin['setup']>>;
|
||||
export type Start = jest.Mocked<ReturnType<Plugin['start']>>;
|
||||
|
||||
// mock mountPointPortal
|
||||
jest.mock('@kbn/react-kibana-mount', () => {
|
||||
const original = jest.requireActual('@kbn/react-kibana-mount');
|
||||
return {
|
||||
...original,
|
||||
MountPointPortal: jest.fn(({ children }) => children),
|
||||
};
|
||||
});
|
||||
|
||||
const createSetupContract = (): jest.Mocked<Setup> => {
|
||||
const setupContract = {
|
||||
registerMenuItem: jest.fn(),
|
||||
|
@ -21,12 +32,21 @@ const createSetupContract = (): jest.Mocked<Setup> => {
|
|||
return setupContract;
|
||||
};
|
||||
|
||||
export const unifiedSearchMock = {
|
||||
ui: {
|
||||
SearchBar: () => <div className="searchBar" />,
|
||||
AggregateQuerySearchBar: () => <div className="searchBar" />,
|
||||
},
|
||||
} as unknown as UnifiedSearchPublicPluginStart;
|
||||
|
||||
const createStartContract = (): jest.Mocked<Start> => {
|
||||
const startContract = {
|
||||
ui: {
|
||||
TopNavMenu: jest.fn(),
|
||||
createTopNavWithCustomContext: jest.fn().mockImplementation(() => jest.fn()),
|
||||
AggregateQueryTopNavMenu: jest.fn(),
|
||||
TopNavMenu: jest.fn().mockImplementation(createTopNav(unifiedSearchMock, [])),
|
||||
AggregateQueryTopNavMenu: jest.fn().mockImplementation(createTopNav(unifiedSearchMock, [])),
|
||||
createTopNavWithCustomContext: jest
|
||||
.fn()
|
||||
.mockImplementation(createTopNav(unifiedSearchMock, [])),
|
||||
},
|
||||
addSolutionNavigation: jest.fn(),
|
||||
isSolutionNavEnabled$: of(false),
|
|
@ -14,16 +14,9 @@ import { MountPoint } from '@kbn/core/public';
|
|||
import { TopNavMenu } from './top_nav_menu';
|
||||
import { TopNavMenuData } from './top_nav_menu_data';
|
||||
import { findTestSubject, mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
||||
import { EuiToolTipProps } from '@elastic/eui';
|
||||
import type { TopNavMenuBadgeProps } from './top_nav_menu_badges';
|
||||
|
||||
const unifiedSearch = {
|
||||
ui: {
|
||||
SearchBar: () => <div className="searchBar" />,
|
||||
AggregateQuerySearchBar: () => <div className="searchBar" />,
|
||||
},
|
||||
} as unknown as UnifiedSearchPublicPluginStart;
|
||||
import { unifiedSearchMock } from '../mocks';
|
||||
|
||||
describe('TopNavMenu', () => {
|
||||
const WRAPPER_SELECTOR = '.kbnTopNavMenu__wrapper';
|
||||
|
@ -97,7 +90,7 @@ describe('TopNavMenu', () => {
|
|||
|
||||
it('Should render search bar', () => {
|
||||
const component = mountWithIntl(
|
||||
<TopNavMenu appName={'test'} showSearchBar={true} unifiedSearch={unifiedSearch} />
|
||||
<TopNavMenu appName={'test'} showSearchBar={true} unifiedSearch={unifiedSearchMock} />
|
||||
);
|
||||
expect(component.find(WRAPPER_SELECTOR).length).toBe(1);
|
||||
expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(0);
|
||||
|
@ -110,7 +103,7 @@ describe('TopNavMenu', () => {
|
|||
appName={'test'}
|
||||
config={menuItems}
|
||||
showSearchBar={true}
|
||||
unifiedSearch={unifiedSearch}
|
||||
unifiedSearch={unifiedSearchMock}
|
||||
/>
|
||||
);
|
||||
expect(component.find(WRAPPER_SELECTOR).length).toBe(1);
|
||||
|
@ -124,7 +117,7 @@ describe('TopNavMenu', () => {
|
|||
appName={'test'}
|
||||
config={menuItems}
|
||||
showSearchBar={true}
|
||||
unifiedSearch={unifiedSearch}
|
||||
unifiedSearch={unifiedSearchMock}
|
||||
className={'myCoolClass'}
|
||||
/>
|
||||
);
|
||||
|
@ -172,7 +165,7 @@ describe('TopNavMenu', () => {
|
|||
appName={'test'}
|
||||
config={menuItems}
|
||||
showSearchBar={true}
|
||||
unifiedSearch={unifiedSearch}
|
||||
unifiedSearch={unifiedSearchMock}
|
||||
setMenuMountPoint={setMountPoint}
|
||||
/>
|
||||
);
|
||||
|
@ -195,7 +188,7 @@ describe('TopNavMenu', () => {
|
|||
appName={'test'}
|
||||
badges={badges}
|
||||
showSearchBar={true}
|
||||
unifiedSearch={unifiedSearch}
|
||||
unifiedSearch={unifiedSearchMock}
|
||||
setMenuMountPoint={setMountPoint}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
*/
|
||||
|
||||
import React, { memo, ReactElement, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { Observable } from 'rxjs';
|
||||
import { Subject } from 'rxjs';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { IconButtonGroup, type IconButtonGroupProps } from '@kbn/shared-ux-button-toolbar';
|
||||
|
@ -70,7 +69,7 @@ export interface ChartProps {
|
|||
disabledActions?: LensEmbeddableInput['disabledActions'];
|
||||
input$?: UnifiedHistogramInput$;
|
||||
lensAdapters?: UnifiedHistogramChartLoadEvent['adapters'];
|
||||
lensEmbeddableOutput$?: Observable<LensEmbeddableOutput>;
|
||||
dataLoading$?: LensEmbeddableOutput['dataLoading'];
|
||||
isChartLoading?: boolean;
|
||||
onChartHiddenChange?: (chartHidden: boolean) => void;
|
||||
onTimeIntervalChange?: (timeInterval: string) => void;
|
||||
|
@ -105,7 +104,7 @@ export function Chart({
|
|||
disabledActions,
|
||||
input$: originalInput$,
|
||||
lensAdapters,
|
||||
lensEmbeddableOutput$,
|
||||
dataLoading$,
|
||||
isChartLoading,
|
||||
onChartHiddenChange,
|
||||
onTimeIntervalChange,
|
||||
|
@ -383,9 +382,7 @@ export function Chart({
|
|||
)}
|
||||
{canSaveVisualization && isSaveModalVisible && visContext.attributes && (
|
||||
<LensSaveModalComponent
|
||||
initialInput={
|
||||
removeTablesFromLensAttributes(visContext.attributes) as unknown as LensEmbeddableInput
|
||||
}
|
||||
initialInput={removeTablesFromLensAttributes(visContext.attributes)}
|
||||
onSave={() => {}}
|
||||
onClose={() => setIsSaveModalVisible(false)}
|
||||
isSaveable={false}
|
||||
|
@ -393,18 +390,16 @@ export function Chart({
|
|||
)}
|
||||
{isFlyoutVisible && !!visContext && !!lensVisServiceCurrentSuggestionContext && (
|
||||
<ChartConfigPanel
|
||||
{...{
|
||||
services,
|
||||
visContext,
|
||||
lensAdapters,
|
||||
lensEmbeddableOutput$,
|
||||
isFlyoutVisible,
|
||||
setIsFlyoutVisible,
|
||||
isPlainRecord,
|
||||
query,
|
||||
currentSuggestionContext: lensVisServiceCurrentSuggestionContext,
|
||||
onSuggestionContextEdit,
|
||||
}}
|
||||
services={services}
|
||||
visContext={visContext}
|
||||
lensAdapters={lensAdapters}
|
||||
dataLoading$={dataLoading$}
|
||||
isFlyoutVisible={isFlyoutVisible}
|
||||
setIsFlyoutVisible={setIsFlyoutVisible}
|
||||
isPlainRecord={isPlainRecord}
|
||||
query={query}
|
||||
currentSuggestionContext={lensVisServiceCurrentSuggestionContext}
|
||||
onSuggestionContextEdit={onSuggestionContextEdit}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
*/
|
||||
|
||||
import React, { ComponentProps, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type { Observable } from 'rxjs';
|
||||
import type { AggregateQuery, Query } from '@kbn/es-query';
|
||||
import { isEqual, isObject } from 'lodash';
|
||||
import type { LensEmbeddableOutput, Suggestion } from '@kbn/lens-plugin/public';
|
||||
|
@ -29,7 +28,7 @@ export function ChartConfigPanel({
|
|||
services,
|
||||
visContext,
|
||||
lensAdapters,
|
||||
lensEmbeddableOutput$,
|
||||
dataLoading$,
|
||||
currentSuggestionContext,
|
||||
isFlyoutVisible,
|
||||
setIsFlyoutVisible,
|
||||
|
@ -42,7 +41,7 @@ export function ChartConfigPanel({
|
|||
isFlyoutVisible: boolean;
|
||||
setIsFlyoutVisible: (flag: boolean) => void;
|
||||
lensAdapters?: UnifiedHistogramChartLoadEvent['adapters'];
|
||||
lensEmbeddableOutput$?: Observable<LensEmbeddableOutput>;
|
||||
dataLoading$?: LensEmbeddableOutput['dataLoading'];
|
||||
currentSuggestionContext: UnifiedHistogramSuggestionContext;
|
||||
isPlainRecord?: boolean;
|
||||
query?: Query | AggregateQuery;
|
||||
|
@ -108,7 +107,7 @@ export function ChartConfigPanel({
|
|||
updateSuggestion={updateSuggestion}
|
||||
updatePanelState={updatePanelState}
|
||||
lensAdapters={lensAdapters}
|
||||
output$={lensEmbeddableOutput$}
|
||||
dataLoading$={dataLoading$}
|
||||
displayFlyoutHeader
|
||||
closeFlyout={() => {
|
||||
setIsFlyoutVisible(false);
|
||||
|
@ -141,7 +140,7 @@ export function ChartConfigPanel({
|
|||
isFlyoutVisible,
|
||||
setIsFlyoutVisible,
|
||||
lensAdapters,
|
||||
lensEmbeddableOutput$,
|
||||
dataLoading$,
|
||||
currentSuggestionType,
|
||||
]);
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { Histogram } from './histogram';
|
||||
import React from 'react';
|
||||
import { of, Subject } from 'rxjs';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { unifiedHistogramServicesMock } from '../__mocks__/services';
|
||||
import { getLensVisMock } from '../__mocks__/lens_vis';
|
||||
import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield';
|
||||
|
@ -101,7 +101,7 @@ describe('Histogram', () => {
|
|||
searchSessionId: props.request.searchSessionId,
|
||||
getTimeRange: props.getTimeRange,
|
||||
attributes: (await getMockLensAttributes())!.attributes,
|
||||
onLoad: lensProps.onLoad,
|
||||
onLoad: lensProps.onLoad!,
|
||||
});
|
||||
expect(lensProps).toMatchObject(expect.objectContaining(originalProps));
|
||||
component.setProps({ request: { ...props.request, searchSessionId: '321' } }).update();
|
||||
|
@ -120,7 +120,7 @@ describe('Histogram', () => {
|
|||
it('should execute onLoad correctly', async () => {
|
||||
const { component, props } = await mountComponent();
|
||||
const embeddable = unifiedHistogramServicesMock.lens.EmbeddableComponent;
|
||||
const onLoad = component.find(embeddable).props().onLoad;
|
||||
const onLoad = component.find(embeddable).props().onLoad!;
|
||||
const adapters = createDefaultInspectorAdapters();
|
||||
adapters.tables.tables.unifiedHistogram = { meta: { statistics: { totalCount: 100 } } } as any;
|
||||
const rawResponse = {
|
||||
|
@ -172,25 +172,25 @@ describe('Histogram', () => {
|
|||
jest
|
||||
.spyOn(adapters.requests, 'getRequests')
|
||||
.mockReturnValue([{ response: { json: { rawResponse } } } as any]);
|
||||
const embeddableOutput$ = jest.fn().mockReturnValue(of('output$'));
|
||||
onLoad(true, undefined, embeddableOutput$);
|
||||
const dataLoading$ = new BehaviorSubject<boolean | undefined>(false);
|
||||
onLoad(true, undefined, dataLoading$);
|
||||
expect(props.onTotalHitsChange).toHaveBeenLastCalledWith(
|
||||
UnifiedHistogramFetchStatus.loading,
|
||||
undefined
|
||||
);
|
||||
expect(props.onChartLoad).toHaveBeenLastCalledWith({ adapters: {}, embeddableOutput$ });
|
||||
expect(props.onChartLoad).toHaveBeenLastCalledWith({ adapters: {}, dataLoading$ });
|
||||
expect(buildBucketInterval.buildBucketInterval).not.toHaveBeenCalled();
|
||||
expect(useTimeRange.useTimeRange).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ bucketInterval: undefined })
|
||||
);
|
||||
act(() => {
|
||||
onLoad(false, adapters, embeddableOutput$);
|
||||
onLoad?.(false, adapters, dataLoading$);
|
||||
});
|
||||
expect(props.onTotalHitsChange).toHaveBeenLastCalledWith(
|
||||
UnifiedHistogramFetchStatus.complete,
|
||||
100
|
||||
);
|
||||
expect(props.onChartLoad).toHaveBeenLastCalledWith({ adapters, embeddableOutput$ });
|
||||
expect(props.onChartLoad).toHaveBeenLastCalledWith({ adapters, dataLoading$ });
|
||||
expect(buildBucketInterval.buildBucketInterval).toHaveBeenCalled();
|
||||
expect(useTimeRange.useTimeRange).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ bucketInterval: mockBucketInterval })
|
||||
|
@ -200,12 +200,12 @@ describe('Histogram', () => {
|
|||
it('should execute onLoad correctly when the request has a failure status', async () => {
|
||||
const { component, props } = await mountComponent();
|
||||
const embeddable = unifiedHistogramServicesMock.lens.EmbeddableComponent;
|
||||
const onLoad = component.find(embeddable).props().onLoad;
|
||||
const onLoad = component.find(embeddable).props().onLoad!;
|
||||
const adapters = createDefaultInspectorAdapters();
|
||||
jest
|
||||
.spyOn(adapters.requests, 'getRequests')
|
||||
.mockReturnValue([{ status: RequestStatus.ERROR } as any]);
|
||||
onLoad(false, adapters);
|
||||
onLoad?.(false, adapters);
|
||||
expect(props.onTotalHitsChange).toHaveBeenLastCalledWith(
|
||||
UnifiedHistogramFetchStatus.error,
|
||||
undefined
|
||||
|
@ -216,7 +216,7 @@ describe('Histogram', () => {
|
|||
it('should execute onLoad correctly when the response has shard failures', async () => {
|
||||
const { component, props } = await mountComponent();
|
||||
const embeddable = unifiedHistogramServicesMock.lens.EmbeddableComponent;
|
||||
const onLoad = component.find(embeddable).props().onLoad;
|
||||
const onLoad = component.find(embeddable).props().onLoad!;
|
||||
const adapters = createDefaultInspectorAdapters();
|
||||
adapters.tables.tables.unifiedHistogram = { meta: { statistics: { totalCount: 100 } } } as any;
|
||||
const rawResponse = {
|
||||
|
@ -237,7 +237,7 @@ describe('Histogram', () => {
|
|||
.spyOn(adapters.requests, 'getRequests')
|
||||
.mockReturnValue([{ response: { json: { rawResponse } } } as any]);
|
||||
act(() => {
|
||||
onLoad(false, adapters);
|
||||
onLoad?.(false, adapters);
|
||||
});
|
||||
expect(props.onTotalHitsChange).toHaveBeenLastCalledWith(
|
||||
UnifiedHistogramFetchStatus.error,
|
||||
|
@ -249,7 +249,7 @@ describe('Histogram', () => {
|
|||
it('should execute onLoad correctly for textbased language and no Lens suggestions', async () => {
|
||||
const { component, props } = await mountComponent(true, false);
|
||||
const embeddable = unifiedHistogramServicesMock.lens.EmbeddableComponent;
|
||||
const onLoad = component.find(embeddable).props().onLoad;
|
||||
const onLoad = component.find(embeddable).props().onLoad!;
|
||||
const adapters = createDefaultInspectorAdapters();
|
||||
adapters.tables.tables.layerId = {
|
||||
meta: { type: 'es_ql' },
|
||||
|
@ -273,7 +273,7 @@ describe('Histogram', () => {
|
|||
],
|
||||
} as any;
|
||||
act(() => {
|
||||
onLoad(false, adapters);
|
||||
onLoad?.(false, adapters);
|
||||
});
|
||||
expect(props.onTotalHitsChange).toHaveBeenLastCalledWith(
|
||||
UnifiedHistogramFetchStatus.complete,
|
||||
|
@ -285,7 +285,7 @@ describe('Histogram', () => {
|
|||
it('should execute onLoad correctly for textbased language and Lens suggestions', async () => {
|
||||
const { component, props } = await mountComponent(true, true);
|
||||
const embeddable = unifiedHistogramServicesMock.lens.EmbeddableComponent;
|
||||
const onLoad = component.find(embeddable).props().onLoad;
|
||||
const onLoad = component.find(embeddable).props().onLoad!;
|
||||
const adapters = createDefaultInspectorAdapters();
|
||||
adapters.tables.tables.layerId = {
|
||||
meta: { type: 'es_ql' },
|
||||
|
@ -309,7 +309,7 @@ describe('Histogram', () => {
|
|||
],
|
||||
} as any;
|
||||
act(() => {
|
||||
onLoad(false, adapters);
|
||||
onLoad?.(false, adapters);
|
||||
});
|
||||
expect(props.onTotalHitsChange).toHaveBeenLastCalledWith(
|
||||
UnifiedHistogramFetchStatus.complete,
|
||||
|
|
|
@ -10,18 +10,15 @@
|
|||
import { useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import React, { useState } from 'react';
|
||||
import type { DataView, DataViewSpec } from '@kbn/data-views-plugin/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { DefaultInspectorAdapters, Datatable } from '@kbn/expressions-plugin/common';
|
||||
import type { IKibanaSearchResponse } from '@kbn/search-types';
|
||||
import type { estypes } from '@elastic/elasticsearch';
|
||||
import type { TimeRange } from '@kbn/es-query';
|
||||
import {
|
||||
EmbeddableComponentProps,
|
||||
LensEmbeddableInput,
|
||||
LensEmbeddableOutput,
|
||||
} from '@kbn/lens-plugin/public';
|
||||
import type { EmbeddableComponentProps, LensEmbeddableInput } from '@kbn/lens-plugin/public';
|
||||
import { RequestStatus } from '@kbn/inspector-plugin/public';
|
||||
import type { Observable } from 'rxjs';
|
||||
import { PublishingSubject } from '@kbn/presentation-publishing';
|
||||
import {
|
||||
UnifiedHistogramBucketInterval,
|
||||
UnifiedHistogramChartContext,
|
||||
|
@ -59,32 +56,6 @@ export interface HistogramProps {
|
|||
withDefaultActions: EmbeddableComponentProps['withDefaultActions'];
|
||||
}
|
||||
|
||||
/**
|
||||
* To prevent flakiness in the chart, we need to ensure that the data view config is valid.
|
||||
* This requires that there are not multiple different data view ids in the given configuration.
|
||||
* @param dataView
|
||||
* @param visContext
|
||||
* @param adHocDataViews
|
||||
*/
|
||||
const checkValidDataViewConfig = (
|
||||
dataView: DataView,
|
||||
visContext: UnifiedHistogramVisContext,
|
||||
adHocDataViews: { [key: string]: DataViewSpec } | undefined
|
||||
) => {
|
||||
if (!dataView.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!dataView.isPersisted() && !adHocDataViews?.[dataView.id]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dataView.id !== visContext.requestData.dataViewId) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const computeTotalHits = (
|
||||
hasLensSuggestions: boolean,
|
||||
adapterTables:
|
||||
|
@ -147,7 +118,7 @@ export function Histogram({
|
|||
(
|
||||
isLoading: boolean,
|
||||
adapters: Partial<DefaultInspectorAdapters> | undefined,
|
||||
lensEmbeddableOutput$?: Observable<LensEmbeddableOutput>
|
||||
dataLoading$?: PublishingSubject<boolean | undefined>
|
||||
) => {
|
||||
const lensRequest = adapters?.requests?.getRequests()[0];
|
||||
const requestFailed = lensRequest?.status === RequestStatus.ERROR;
|
||||
|
@ -186,7 +157,7 @@ export function Histogram({
|
|||
setBucketInterval(newBucketInterval);
|
||||
}
|
||||
|
||||
onChartLoad?.({ adapters: adapters ?? {}, embeddableOutput$: lensEmbeddableOutput$ });
|
||||
onChartLoad?.({ adapters: adapters ?? {}, dataLoading$ });
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -230,10 +201,6 @@ export function Histogram({
|
|||
}
|
||||
`;
|
||||
|
||||
if (!checkValidDataViewConfig(dataView, visContext, lensProps.attributes.state.adHocDataViews)) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
|
|
|
@ -80,6 +80,7 @@ describe('useStateProps', () => {
|
|||
"hidden": false,
|
||||
"timeInterval": "auto",
|
||||
},
|
||||
"dataLoading$": undefined,
|
||||
"hits": Object {
|
||||
"status": "uninitialized",
|
||||
"total": undefined,
|
||||
|
@ -120,7 +121,6 @@ describe('useStateProps', () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
"lensEmbeddableOutput$": undefined,
|
||||
"onBreakdownFieldChange": [Function],
|
||||
"onChartHiddenChange": [Function],
|
||||
"onChartLoad": [Function],
|
||||
|
@ -164,6 +164,7 @@ describe('useStateProps', () => {
|
|||
"hidden": false,
|
||||
"timeInterval": "auto",
|
||||
},
|
||||
"dataLoading$": undefined,
|
||||
"hits": Object {
|
||||
"status": "uninitialized",
|
||||
"total": undefined,
|
||||
|
@ -204,7 +205,6 @@ describe('useStateProps', () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
"lensEmbeddableOutput$": undefined,
|
||||
"onBreakdownFieldChange": [Function],
|
||||
"onChartHiddenChange": [Function],
|
||||
"onChartLoad": [Function],
|
||||
|
@ -348,6 +348,7 @@ describe('useStateProps', () => {
|
|||
Object {
|
||||
"breakdown": undefined,
|
||||
"chart": undefined,
|
||||
"dataLoading$": undefined,
|
||||
"hits": Object {
|
||||
"status": "uninitialized",
|
||||
"total": undefined,
|
||||
|
@ -388,7 +389,6 @@ describe('useStateProps', () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
"lensEmbeddableOutput$": undefined,
|
||||
"onBreakdownFieldChange": [Function],
|
||||
"onChartHiddenChange": [Function],
|
||||
"onChartLoad": [Function],
|
||||
|
@ -427,6 +427,7 @@ describe('useStateProps', () => {
|
|||
Object {
|
||||
"breakdown": undefined,
|
||||
"chart": undefined,
|
||||
"dataLoading$": undefined,
|
||||
"hits": Object {
|
||||
"status": "uninitialized",
|
||||
"total": undefined,
|
||||
|
@ -467,7 +468,6 @@ describe('useStateProps', () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
"lensEmbeddableOutput$": undefined,
|
||||
"onBreakdownFieldChange": [Function],
|
||||
"onChartHiddenChange": [Function],
|
||||
"onChartLoad": [Function],
|
||||
|
|
|
@ -27,7 +27,7 @@ import {
|
|||
totalHitsResultSelector,
|
||||
totalHitsStatusSelector,
|
||||
lensAdaptersSelector,
|
||||
lensEmbeddableOutputSelector$,
|
||||
lensDataLoadingSelector$,
|
||||
} from '../utils/state_selectors';
|
||||
import { useStateSelector } from '../utils/use_state_selector';
|
||||
|
||||
|
@ -52,10 +52,7 @@ export const useStateProps = ({
|
|||
const totalHitsResult = useStateSelector(stateService?.state$, totalHitsResultSelector);
|
||||
const totalHitsStatus = useStateSelector(stateService?.state$, totalHitsStatusSelector);
|
||||
const lensAdapters = useStateSelector(stateService?.state$, lensAdaptersSelector);
|
||||
const lensEmbeddableOutput$ = useStateSelector(
|
||||
stateService?.state$,
|
||||
lensEmbeddableOutputSelector$
|
||||
);
|
||||
const lensDataLoading$ = useStateSelector(stateService?.state$, lensDataLoadingSelector$);
|
||||
/**
|
||||
* Contexts
|
||||
*/
|
||||
|
@ -162,7 +159,7 @@ export const useStateProps = ({
|
|||
// We need to store the Lens request adapter in order to inspect its requests
|
||||
stateService?.setLensRequestAdapter(event.adapters.requests);
|
||||
stateService?.setLensAdapters(event.adapters);
|
||||
stateService?.setLensEmbeddableOutput$(event.embeddableOutput$);
|
||||
stateService?.setLensDataLoading$(event.dataLoading$);
|
||||
},
|
||||
[stateService]
|
||||
);
|
||||
|
@ -199,7 +196,7 @@ export const useStateProps = ({
|
|||
request,
|
||||
isPlainRecord,
|
||||
lensAdapters,
|
||||
lensEmbeddableOutput$,
|
||||
dataLoading$: lensDataLoading$,
|
||||
onTopPanelHeightChange,
|
||||
onTimeIntervalChange,
|
||||
onTotalHitsChange,
|
||||
|
|
|
@ -139,8 +139,8 @@ describe('UnifiedHistogramStateService', () => {
|
|||
stateService.setLensAdapters(undefined);
|
||||
newState = { ...newState, lensAdapters: undefined };
|
||||
expect(state).toEqual(newState);
|
||||
stateService.setLensEmbeddableOutput$(undefined);
|
||||
newState = { ...newState, lensEmbeddableOutput$: undefined };
|
||||
stateService.setLensDataLoading$(undefined);
|
||||
newState = { ...newState, dataLoading$: undefined };
|
||||
expect(state).toEqual(newState);
|
||||
stateService.setTotalHits({
|
||||
totalHitsStatus: UnifiedHistogramFetchStatus.complete,
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
*/
|
||||
|
||||
import type { RequestAdapter } from '@kbn/inspector-plugin/common';
|
||||
import type { LensEmbeddableOutput } from '@kbn/lens-plugin/public';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { PublishingSubject } from '@kbn/presentation-publishing';
|
||||
import { UnifiedHistogramFetchStatus } from '../..';
|
||||
import type { UnifiedHistogramServices, UnifiedHistogramChartLoadEvent } from '../../types';
|
||||
import {
|
||||
|
@ -49,7 +49,7 @@ export interface UnifiedHistogramState {
|
|||
/**
|
||||
* Lens embeddable output observable
|
||||
*/
|
||||
lensEmbeddableOutput$?: Observable<LensEmbeddableOutput>;
|
||||
dataLoading$?: PublishingSubject<boolean | undefined>;
|
||||
/**
|
||||
* The current time interval of the chart
|
||||
*/
|
||||
|
@ -124,9 +124,7 @@ export interface UnifiedHistogramStateService {
|
|||
* Sets the current Lens adapters
|
||||
*/
|
||||
setLensAdapters: (lensAdapters: UnifiedHistogramChartLoadEvent['adapters'] | undefined) => void;
|
||||
setLensEmbeddableOutput$: (
|
||||
lensEmbeddableOutput$: Observable<LensEmbeddableOutput> | undefined
|
||||
) => void;
|
||||
setLensDataLoading$: (dataLoading$: PublishingSubject<boolean | undefined> | undefined) => void;
|
||||
/**
|
||||
* Sets the current total hits status and result
|
||||
*/
|
||||
|
@ -214,10 +212,8 @@ export const createStateService = (
|
|||
setLensAdapters: (lensAdapters: UnifiedHistogramChartLoadEvent['adapters'] | undefined) => {
|
||||
updateState({ lensAdapters });
|
||||
},
|
||||
setLensEmbeddableOutput$: (
|
||||
lensEmbeddableOutput$: Observable<LensEmbeddableOutput> | undefined
|
||||
) => {
|
||||
updateState({ lensEmbeddableOutput$ });
|
||||
setLensDataLoading$: (dataLoading$: PublishingSubject<boolean | undefined> | undefined) => {
|
||||
updateState({ dataLoading$ });
|
||||
},
|
||||
|
||||
setTotalHits: (totalHits: {
|
||||
|
|
|
@ -16,5 +16,4 @@ export const topPanelHeightSelector = (state: UnifiedHistogramState) => state.to
|
|||
export const totalHitsResultSelector = (state: UnifiedHistogramState) => state.totalHitsResult;
|
||||
export const totalHitsStatusSelector = (state: UnifiedHistogramState) => state.totalHitsStatus;
|
||||
export const lensAdaptersSelector = (state: UnifiedHistogramState) => state.lensAdapters;
|
||||
export const lensEmbeddableOutputSelector$ = (state: UnifiedHistogramState) =>
|
||||
state.lensEmbeddableOutput$;
|
||||
export const lensDataLoadingSelector$ = (state: UnifiedHistogramState) => state.dataLoading$;
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
|
||||
import { EuiSpacer, useEuiTheme, useIsWithinBreakpoints } from '@elastic/eui';
|
||||
import React, { PropsWithChildren, ReactElement, useEffect, useMemo, useState } from 'react';
|
||||
import { Observable } from 'rxjs';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal';
|
||||
import { css } from '@emotion/css';
|
||||
|
@ -99,7 +98,7 @@ export interface UnifiedHistogramLayoutProps extends PropsWithChildren<unknown>
|
|||
*/
|
||||
hits?: UnifiedHistogramHitsContext;
|
||||
lensAdapters?: UnifiedHistogramChartLoadEvent['adapters'];
|
||||
lensEmbeddableOutput$?: Observable<LensEmbeddableOutput>;
|
||||
dataLoading$?: LensEmbeddableOutput['dataLoading'];
|
||||
/**
|
||||
* Context object for the chart -- leave undefined to hide the chart
|
||||
*/
|
||||
|
@ -214,7 +213,7 @@ export const UnifiedHistogramLayout = ({
|
|||
request,
|
||||
hits,
|
||||
lensAdapters,
|
||||
lensEmbeddableOutput$,
|
||||
dataLoading$,
|
||||
chart: originalChart,
|
||||
breakdown,
|
||||
container,
|
||||
|
@ -372,7 +371,7 @@ export const UnifiedHistogramLayout = ({
|
|||
onFilter={onFilter}
|
||||
onBrushEnd={onBrushEnd}
|
||||
lensAdapters={lensAdapters}
|
||||
lensEmbeddableOutput$={lensEmbeddableOutput$}
|
||||
dataLoading$={dataLoading$}
|
||||
withDefaultActions={withDefaultActions}
|
||||
columns={columns}
|
||||
/>
|
||||
|
|
|
@ -108,6 +108,7 @@ describe('LensVisService attributes', () => {
|
|||
"sourceField": "timestamp",
|
||||
},
|
||||
},
|
||||
"indexPatternId": "index-pattern-with-timefield-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -284,6 +285,7 @@ describe('LensVisService attributes', () => {
|
|||
"sourceField": "timestamp",
|
||||
},
|
||||
},
|
||||
"indexPatternId": "index-pattern-with-timefield-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -434,6 +436,7 @@ describe('LensVisService attributes', () => {
|
|||
"sourceField": "timestamp",
|
||||
},
|
||||
},
|
||||
"indexPatternId": "index-pattern-with-timefield-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -403,7 +403,7 @@ export class LensVisService {
|
|||
|
||||
const datasourceState = {
|
||||
layers: {
|
||||
[UNIFIED_HISTOGRAM_LAYER_ID]: { columnOrder, columns },
|
||||
[UNIFIED_HISTOGRAM_LAYER_ID]: { columnOrder, columns, indexPatternId: dataView.id },
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -10,19 +10,15 @@
|
|||
import type { IUiSettingsClient, Capabilities } from '@kbn/core/public';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import type {
|
||||
LensEmbeddableOutput,
|
||||
LensPublicStart,
|
||||
TypedLensByValueInput,
|
||||
Suggestion,
|
||||
} from '@kbn/lens-plugin/public';
|
||||
import type { LensPublicStart, TypedLensByValueInput, Suggestion } from '@kbn/lens-plugin/public';
|
||||
import type { DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import type { RequestAdapter } from '@kbn/inspector-plugin/public';
|
||||
import type { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common';
|
||||
import type { Observable, Subject } from 'rxjs';
|
||||
import type { Subject } from 'rxjs';
|
||||
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import type { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import type { ExpressionsStart } from '@kbn/expressions-plugin/public';
|
||||
import { PublishingSubject } from '@kbn/presentation-publishing';
|
||||
|
||||
/**
|
||||
* The fetch status of a Unified Histogram request
|
||||
|
@ -72,9 +68,9 @@ export interface UnifiedHistogramChartLoadEvent {
|
|||
*/
|
||||
adapters: UnifiedHistogramAdapters;
|
||||
/**
|
||||
* Observable of the lens embeddable output
|
||||
* Observable for the data change subscription
|
||||
*/
|
||||
embeddableOutput$?: Observable<LensEmbeddableOutput>;
|
||||
dataLoading$?: PublishingSubject<boolean | undefined>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -43,7 +43,7 @@ export const exportVisContext = (
|
|||
? {
|
||||
suggestionType: visContext.suggestionType,
|
||||
requestData: visContext.requestData,
|
||||
attributes: removeTablesFromLensAttributes(visContext.attributes),
|
||||
attributes: removeTablesFromLensAttributes(visContext.attributes).attributes,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
import type { Datatable } from '@kbn/expressions-plugin/common';
|
||||
import type { LensAttributes } from '@kbn/lens-embeddable-utils';
|
||||
import type { TextBasedPersistedState } from '@kbn/lens-plugin/public/datasources/text_based/types';
|
||||
import type { TypedLensByValueInput } from '@kbn/lens-plugin/public';
|
||||
|
||||
export const enrichLensAttributesWithTablesData = ({
|
||||
attributes,
|
||||
|
@ -53,6 +54,8 @@ export const enrichLensAttributesWithTablesData = ({
|
|||
return updatedAttributes;
|
||||
};
|
||||
|
||||
export const removeTablesFromLensAttributes = (attributes: LensAttributes): LensAttributes => {
|
||||
return enrichLensAttributesWithTablesData({ attributes, table: undefined });
|
||||
export const removeTablesFromLensAttributes = (
|
||||
attributes: LensAttributes
|
||||
): TypedLensByValueInput => {
|
||||
return { attributes: enrichLensAttributesWithTablesData({ attributes, table: undefined }) };
|
||||
};
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
"@kbn/discover-utils",
|
||||
"@kbn/visualization-utils",
|
||||
"@kbn/search-types",
|
||||
"@kbn/presentation-publishing",
|
||||
"@kbn/data-view-utils",
|
||||
],
|
||||
"exclude": [
|
||||
|
|
|
@ -18,6 +18,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const testSubjects = getService('testSubjects');
|
||||
const monacoEditor = getService('monacoEditor');
|
||||
const dashboardAddPanel = getService('dashboardAddPanel');
|
||||
const dashboardPanelActions = getService('dashboardPanelActions');
|
||||
const log = getService('log');
|
||||
|
||||
describe('dashboard add ES|QL chart', function () {
|
||||
before(async () => {
|
||||
|
@ -30,6 +32,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await dashboard.navigateToApp();
|
||||
await testSubjects.click('discard-unsaved-New-Dashboard');
|
||||
});
|
||||
|
||||
it('should add an ES|QL datatable chart when the ES|QL panel action is clicked', async () => {
|
||||
await dashboard.navigateToApp();
|
||||
await dashboard.clickNewDashboard();
|
||||
|
@ -57,6 +64,47 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
|
||||
it('should reset to the previous state on edit inline', async () => {
|
||||
await dashboardAddPanel.clickEditorMenuButton();
|
||||
await dashboardAddPanel.clickAddNewPanelFromUIActionLink('ES|QL');
|
||||
await dashboardAddPanel.expectEditorMenuClosed();
|
||||
await dashboard.waitForRenderComplete();
|
||||
|
||||
// Save the panel and close the flyout
|
||||
log.debug('Applies the changes');
|
||||
await testSubjects.click('applyFlyoutButton');
|
||||
|
||||
// now edit the panel and click on Cancel
|
||||
await dashboardPanelActions.clickInlineEdit();
|
||||
|
||||
const metricsConfigured = await testSubjects.findAll(
|
||||
'lnsDatatable_metrics > lnsLayerPanel-dimensionLink'
|
||||
);
|
||||
// remove the first metric from the configuration
|
||||
// Lens is x-pack so not available here, make things manually
|
||||
await testSubjects.moveMouseTo(`lnsDatatable_metrics > indexPattern-dimension-remove`);
|
||||
await testSubjects.click(`lnsDatatable_metrics > indexPattern-dimension-remove`);
|
||||
const beforeCancelMetricsConfigured = await testSubjects.findAll(
|
||||
'lnsDatatable_metrics > lnsLayerPanel-dimensionLink'
|
||||
);
|
||||
expect(beforeCancelMetricsConfigured.length).to.eql(metricsConfigured.length - 1);
|
||||
|
||||
// now click cancel
|
||||
await testSubjects.click('cancelFlyoutButton');
|
||||
await dashboard.waitForRenderComplete();
|
||||
|
||||
// re open the inline editor and check that the configured metrics are still the original ones
|
||||
await dashboardPanelActions.clickInlineEdit();
|
||||
const afterCancelMetricsConfigured = await testSubjects.findAll(
|
||||
'lnsDatatable_metrics > lnsLayerPanel-dimensionLink'
|
||||
);
|
||||
expect(afterCancelMetricsConfigured.length).to.eql(metricsConfigured.length);
|
||||
// delete the panel
|
||||
await testSubjects.click('cancelFlyoutButton');
|
||||
const panels = await dashboard.getDashboardPanels();
|
||||
await dashboardPanelActions.removePanel(panels[0]);
|
||||
});
|
||||
|
||||
it('should be able to edit the query and render another chart', async () => {
|
||||
await dashboardAddPanel.clickEditorMenuButton();
|
||||
await dashboardAddPanel.clickAddNewPanelFromUIActionLink('ES|QL');
|
||||
|
@ -70,5 +118,41 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await testSubjects.click('applyFlyoutButton');
|
||||
expect(await testSubjects.exists('mtrVis')).to.be(true);
|
||||
});
|
||||
|
||||
it('should add a second panel and remove when hitting cancel', async () => {
|
||||
await dashboardAddPanel.clickEditorMenuButton();
|
||||
await dashboardAddPanel.clickAddNewPanelFromUIActionLink('ES|QL');
|
||||
await dashboardAddPanel.expectEditorMenuClosed();
|
||||
await dashboard.waitForRenderComplete();
|
||||
// Cancel
|
||||
await testSubjects.click('cancelFlyoutButton');
|
||||
// Test that there's only 1 panel left
|
||||
await dashboard.waitForRenderComplete();
|
||||
await retry.try(async () => {
|
||||
const panelCount = await dashboard.getPanelCount();
|
||||
expect(panelCount).to.eql(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not remove the first panel of two when editing and cancelling', async () => {
|
||||
// add a second panel
|
||||
await dashboardAddPanel.clickEditorMenuButton();
|
||||
await dashboardAddPanel.clickAddNewPanelFromUIActionLink('ES|QL');
|
||||
await dashboardAddPanel.expectEditorMenuClosed();
|
||||
await dashboard.waitForRenderComplete();
|
||||
// save it
|
||||
await testSubjects.click('applyFlyoutButton');
|
||||
await dashboard.waitForRenderComplete();
|
||||
|
||||
// now edit the first one
|
||||
const [firstPanel] = await dashboard.getDashboardPanels();
|
||||
await dashboardPanelActions.clickInlineEdit(firstPanel);
|
||||
await testSubjects.click('cancelFlyoutButton');
|
||||
await dashboard.waitForRenderComplete();
|
||||
await retry.try(async () => {
|
||||
const panelCount = await dashboard.getPanelCount();
|
||||
expect(panelCount).to.eql(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await PageObjects.dashboard.expectOnDashboard('New Dashboard');
|
||||
expect(await testSubjects.exists('lnsVisualizationContainer')).to.be(true);
|
||||
|
||||
await panelActions.clickInlineEdit();
|
||||
await panelActions.clickEdit();
|
||||
const editorValue = await monacoEditor.getCodeEditorValue();
|
||||
expect(editorValue).to.eql(`FROM logs* | LIMIT 10`);
|
||||
});
|
||||
|
|
|
@ -383,6 +383,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
await testSubjects.click('querySubmitButton');
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
// for some reason the chart query is taking a very long time to return (3x the delay)
|
||||
// so wait for the chart to be loaded
|
||||
await discover.waitForChartLoadingComplete(1);
|
||||
await browser.execute(() => {
|
||||
window.ELASTIC_ESQL_DELAY_SECONDS = undefined;
|
||||
});
|
||||
|
|
|
@ -97,7 +97,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
expectedRequests?: number;
|
||||
expectedRefreshRequest?: number;
|
||||
}) => {
|
||||
it(`should send ${expectedRequests} search requests (documents + chart) on page load`, async () => {
|
||||
it(`should send no more than ${expectedRequests} search requests (documents + chart) on page load`, async () => {
|
||||
await browser.refresh();
|
||||
await browser.execute(async () => {
|
||||
performance.setResourceTimingBufferSize(Number.MAX_SAFE_INTEGER);
|
||||
|
@ -107,20 +107,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
expect(searchCount).to.be(expectedRequests);
|
||||
});
|
||||
|
||||
it(`should send ${expectedRequests} requests (documents + chart) when refreshing`, async () => {
|
||||
it(`should send no more than ${expectedRequests} requests (documents + chart) when refreshing`, async () => {
|
||||
await expectSearches(type, expectedRequests, async () => {
|
||||
await queryBar.clickQuerySubmitButton();
|
||||
});
|
||||
});
|
||||
|
||||
it(`should send ${expectedRequests} requests (documents + chart) when changing the query`, async () => {
|
||||
it(`should send no more than ${expectedRequests} requests (documents + chart) when changing the query`, async () => {
|
||||
await expectSearches(type, expectedRequests, async () => {
|
||||
await setQuery(query1);
|
||||
await queryBar.clickQuerySubmitButton();
|
||||
});
|
||||
});
|
||||
|
||||
it(`should send ${expectedRequests} requests (documents + chart) when changing the time range`, async () => {
|
||||
it(`should send no more than ${expectedRequests} requests (documents + chart) when changing the time range`, async () => {
|
||||
await expectSearches(type, expectedRequests, async () => {
|
||||
await timePicker.setAbsoluteRange(
|
||||
'Sep 21, 2015 @ 06:31:44.000',
|
||||
|
@ -174,7 +174,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
setQuery: (query) => queryBar.setQuery(query),
|
||||
});
|
||||
|
||||
it(`should send 2 requests (documents + chart) when toggling the chart visibility`, async () => {
|
||||
it(`should send no more than 2 requests (documents + chart) when toggling the chart visibility`, async () => {
|
||||
await expectSearches(type, 2, async () => {
|
||||
await discover.toggleChartVisibility();
|
||||
});
|
||||
|
@ -183,7 +183,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
|
||||
it('should send 2 requests (documents + chart) when adding a filter', async () => {
|
||||
it('should send no more than 2 requests (documents + chart) when adding a filter', async () => {
|
||||
await expectSearches(type, 2, async () => {
|
||||
await filterBar.addFilter({
|
||||
field: 'extension',
|
||||
|
@ -193,31 +193,31 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
|
||||
it('should send 2 requests (documents + chart) when sorting', async () => {
|
||||
it('should send no more than 2 requests (documents + chart) when sorting', async () => {
|
||||
await expectSearches(type, 2, async () => {
|
||||
await discover.clickFieldSort('@timestamp', 'Sort Old-New');
|
||||
});
|
||||
});
|
||||
|
||||
it('should send 2 requests (documents + chart) when changing to a breakdown field without an other bucket', async () => {
|
||||
it('should send no more than 2 requests (documents + chart) when changing to a breakdown field without an other bucket', async () => {
|
||||
await expectSearches(type, 2, async () => {
|
||||
await discover.chooseBreakdownField('type');
|
||||
});
|
||||
});
|
||||
|
||||
it('should send 3 requests (documents + chart + other bucket) when changing to a breakdown field with an other bucket', async () => {
|
||||
it('should send no more than 3 requests (documents + chart + other bucket) when changing to a breakdown field with an other bucket', async () => {
|
||||
await expectSearches(type, 3, async () => {
|
||||
await discover.chooseBreakdownField('extension.raw');
|
||||
});
|
||||
});
|
||||
|
||||
it('should send 2 requests (documents + chart) when changing the chart interval', async () => {
|
||||
it('should send no more than 2 requests (documents + chart) when changing the chart interval', async () => {
|
||||
await expectSearches(type, 2, async () => {
|
||||
await discover.setChartInterval('Day');
|
||||
});
|
||||
});
|
||||
|
||||
it('should send 2 requests (documents + chart) when changing the data view', async () => {
|
||||
it('should send no more than 2 requests (documents + chart) when changing the data view', async () => {
|
||||
await expectSearches(type, 2, async () => {
|
||||
await discover.selectIndexPattern('long-window-logstash-*');
|
||||
});
|
||||
|
|
|
@ -177,7 +177,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
dataView: 'logs*',
|
||||
});
|
||||
expect(await annotationEditor.showingMissingDataViewPrompt()).to.be(false);
|
||||
expect(await find.byCssSelector('canvas')).to.be.ok();
|
||||
// @TODO: re-enable this once the error bubbling issue is fixed at Lens custom component level
|
||||
// expect(await find.byCssSelector('canvas')).to.be.ok();
|
||||
});
|
||||
|
||||
await annotationEditor.saveGroup();
|
||||
|
|
|
@ -12,7 +12,6 @@ import { FtrService } from '../../ftr_provider_context';
|
|||
|
||||
const REMOVE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-deletePanel';
|
||||
const EDIT_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-editPanel';
|
||||
const INLINE_EDIT_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-ACTION_CONFIGURE_IN_LENS';
|
||||
const EDIT_IN_LENS_EDITOR_DATA_TEST_SUBJ = 'navigateToLensEditorLink';
|
||||
const CLONE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-clonePanel';
|
||||
const TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-togglePanel';
|
||||
|
@ -128,7 +127,9 @@ export class DashboardPanelActionsService extends FtrService {
|
|||
|
||||
async navigateToEditorFromFlyout(wrapper?: WebElementWrapper) {
|
||||
this.log.debug('navigateToEditorFromFlyout');
|
||||
await this.clickPanelAction(INLINE_EDIT_PANEL_DATA_TEST_SUBJ, wrapper);
|
||||
// make sure the context menu is open before proceeding
|
||||
await this.openContextMenu();
|
||||
await this.clickPanelAction(EDIT_PANEL_DATA_TEST_SUBJ);
|
||||
await this.header.waitUntilLoadingHasFinished();
|
||||
await this.testSubjects.clickWhenNotDisabledWithoutRetry(EDIT_IN_LENS_EDITOR_DATA_TEST_SUBJ);
|
||||
const isConfirmModalVisible = await this.testSubjects.exists('confirmModalConfirmButton');
|
||||
|
@ -139,9 +140,9 @@ export class DashboardPanelActionsService extends FtrService {
|
|||
}
|
||||
}
|
||||
|
||||
async clickInlineEdit() {
|
||||
async clickInlineEdit(wrapper?: WebElementWrapper) {
|
||||
this.log.debug('clickInlineEditAction');
|
||||
await this.clickPanelAction(INLINE_EDIT_PANEL_DATA_TEST_SUBJ);
|
||||
await this.clickPanelAction(EDIT_PANEL_DATA_TEST_SUBJ, wrapper);
|
||||
await this.header.waitUntilLoadingHasFinished();
|
||||
await this.common.waitForTopNavToBeVisible();
|
||||
}
|
||||
|
@ -307,12 +308,9 @@ export class DashboardPanelActionsService extends FtrService {
|
|||
await this.expectExistsPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ, title);
|
||||
}
|
||||
|
||||
async expectExistsEditPanelAction(title = '', allowsInlineEditing?: boolean) {
|
||||
async expectExistsEditPanelAction(title = '') {
|
||||
this.log.debug('expectExistsEditPanelAction');
|
||||
let testSubj = EDIT_PANEL_DATA_TEST_SUBJ;
|
||||
if (allowsInlineEditing) {
|
||||
testSubj = INLINE_EDIT_PANEL_DATA_TEST_SUBJ;
|
||||
}
|
||||
const testSubj = EDIT_PANEL_DATA_TEST_SUBJ;
|
||||
await this.expectExistsPanelAction(testSubj, title);
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,6 @@ import type {
|
|||
TypedLensByValueInput,
|
||||
PersistedIndexPatternLayer,
|
||||
XYState,
|
||||
LensEmbeddableInput,
|
||||
FormulaPublicApi,
|
||||
DateHistogramIndexPatternColumn,
|
||||
} from '@kbn/lens-plugin/public';
|
||||
|
@ -288,7 +287,7 @@ export const App = (props: {
|
|||
/>
|
||||
{isSaveModalVisible && (
|
||||
<LensSaveModalComponent
|
||||
initialInput={attributes as unknown as LensEmbeddableInput}
|
||||
initialInput={{ attributes }}
|
||||
onSave={() => {}}
|
||||
onClose={() => setIsSaveModalVisible(false)}
|
||||
/>
|
||||
|
|
|
@ -24,7 +24,6 @@ import type { CoreStart } from '@kbn/core/public';
|
|||
import { LensConfigBuilder } from '@kbn/lens-embeddable-utils/config_builder/config_builder';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { LensPublicStart } from '@kbn/lens-plugin/public';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import type { StartDependencies } from './plugin';
|
||||
import { LensChart } from './embeddable';
|
||||
import { MultiPaneFlyout } from './flyout';
|
||||
|
@ -46,137 +45,128 @@ export const App = (props: {
|
|||
);
|
||||
|
||||
return (
|
||||
<KibanaContextProvider
|
||||
services={{
|
||||
uiSettings: props.core.uiSettings,
|
||||
settings: props.core.settings,
|
||||
theme: props.core.theme,
|
||||
}}
|
||||
>
|
||||
<EuiPage>
|
||||
<EuiPageBody>
|
||||
<EuiPageHeader
|
||||
paddingSize="s"
|
||||
bottomBorder={true}
|
||||
pageTitle="Lens embeddable inline editing"
|
||||
/>
|
||||
<EuiPageSection paddingSize="s">
|
||||
<EuiFlexGroup
|
||||
className="eui-fullHeight"
|
||||
gutterSize="none"
|
||||
direction="row"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem className="eui-fullHeight">
|
||||
<LensChart
|
||||
configBuilder={configBuilder}
|
||||
plugins={props.plugins}
|
||||
defaultDataView={props.defaultDataView}
|
||||
isESQL
|
||||
setPanelActive={setPanelActive}
|
||||
isActive={Boolean(panelActive === 1) || !panelActive}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem className="eui-fullHeight">
|
||||
<LensChart
|
||||
configBuilder={configBuilder}
|
||||
plugins={props.plugins}
|
||||
defaultDataView={props.defaultDataView}
|
||||
setPanelActive={setPanelActive}
|
||||
isActive={Boolean(panelActive === 2) || !panelActive}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiPanel
|
||||
hasShadow={false}
|
||||
hasBorder={true}
|
||||
<EuiPage>
|
||||
<EuiPageBody>
|
||||
<EuiPageHeader
|
||||
paddingSize="s"
|
||||
bottomBorder={true}
|
||||
pageTitle="Lens embeddable inline editing"
|
||||
/>
|
||||
<EuiPageSection paddingSize="s">
|
||||
<EuiFlexGroup
|
||||
className="eui-fullHeight"
|
||||
gutterSize="none"
|
||||
direction="row"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem className="eui-fullHeight">
|
||||
<LensChart
|
||||
configBuilder={configBuilder}
|
||||
plugins={props.plugins}
|
||||
defaultDataView={props.defaultDataView}
|
||||
isESQL
|
||||
setPanelActive={setPanelActive}
|
||||
isActive={Boolean(panelActive === 1) || !panelActive}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem className="eui-fullHeight">
|
||||
<LensChart
|
||||
configBuilder={configBuilder}
|
||||
plugins={props.plugins}
|
||||
defaultDataView={props.defaultDataView}
|
||||
setPanelActive={setPanelActive}
|
||||
isActive={Boolean(panelActive === 2) || !panelActive}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiPanel
|
||||
hasShadow={false}
|
||||
hasBorder={true}
|
||||
css={css`
|
||||
opacity: ${Boolean(panelActive === 3) || !panelActive ? '1' : '0.25'};
|
||||
pointer-events: ${Boolean(panelActive === 3) || !panelActive ? 'all' : 'none'};
|
||||
`}
|
||||
>
|
||||
<EuiTitle
|
||||
size="xs"
|
||||
css={css`
|
||||
opacity: ${Boolean(panelActive === 3) || !panelActive ? '1' : '0.25'};
|
||||
pointer-events: ${Boolean(panelActive === 3) || !panelActive ? 'all' : 'none'};
|
||||
text-align: center;
|
||||
`}
|
||||
>
|
||||
<EuiTitle
|
||||
size="xs"
|
||||
css={css`
|
||||
text-align: center;
|
||||
`}
|
||||
>
|
||||
<h3>#3: Embeddable inside a flyout</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
<EuiTitle
|
||||
size="xxs"
|
||||
css={css`
|
||||
text-align: center;
|
||||
`}
|
||||
>
|
||||
<p>
|
||||
In case you do not want to use a push flyout, you can check this example.{' '}
|
||||
<br />
|
||||
In this example, we have a Lens embeddable inside a flyout and we want to
|
||||
render the inline editing Component in a second slot of the same flyout.
|
||||
</p>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
onClick={() => {
|
||||
setIsFlyoutVisible(true);
|
||||
setPanelActive(3);
|
||||
<h3>#3: Embeddable inside a flyout</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
<EuiTitle
|
||||
size="xxs"
|
||||
css={css`
|
||||
text-align: center;
|
||||
`}
|
||||
>
|
||||
<p>
|
||||
In case you do not want to use a push flyout, you can check this example. <br />
|
||||
In this example, we have a Lens embeddable inside a flyout and we want to render
|
||||
the inline editing Component in a second slot of the same flyout.
|
||||
</p>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
onClick={() => {
|
||||
setIsFlyoutVisible(true);
|
||||
setPanelActive(3);
|
||||
}}
|
||||
>
|
||||
Show flyout
|
||||
</EuiButton>
|
||||
{isFlyoutVisible ? (
|
||||
<MultiPaneFlyout
|
||||
mainContent={{
|
||||
content: (
|
||||
<LensChart
|
||||
configBuilder={configBuilder}
|
||||
plugins={props.plugins}
|
||||
defaultDataView={props.defaultDataView}
|
||||
container={container}
|
||||
setIsinlineEditingVisible={setIsinlineEditingVisible}
|
||||
onApplyCb={() => {
|
||||
setIsinlineEditingVisible(false);
|
||||
if (container) {
|
||||
ReactDOM.unmountComponentAtNode(container);
|
||||
}
|
||||
}}
|
||||
onCancelCb={() => {
|
||||
setIsinlineEditingVisible(false);
|
||||
if (container) {
|
||||
ReactDOM.unmountComponentAtNode(container);
|
||||
}
|
||||
}}
|
||||
isESQL
|
||||
isActive
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
Show flyout
|
||||
</EuiButton>
|
||||
{isFlyoutVisible ? (
|
||||
<MultiPaneFlyout
|
||||
mainContent={{
|
||||
content: (
|
||||
<LensChart
|
||||
configBuilder={configBuilder}
|
||||
plugins={props.plugins}
|
||||
defaultDataView={props.defaultDataView}
|
||||
container={container}
|
||||
setIsinlineEditingVisible={setIsinlineEditingVisible}
|
||||
onApplyCb={() => {
|
||||
setIsinlineEditingVisible(false);
|
||||
if (container) {
|
||||
ReactDOM.unmountComponentAtNode(container);
|
||||
}
|
||||
}}
|
||||
onCancelCb={() => {
|
||||
setIsinlineEditingVisible(false);
|
||||
if (container) {
|
||||
ReactDOM.unmountComponentAtNode(container);
|
||||
}
|
||||
}}
|
||||
isESQL
|
||||
isActive
|
||||
/>
|
||||
),
|
||||
}}
|
||||
inlineEditingContent={{
|
||||
visible: isInlineEditingVisible,
|
||||
}}
|
||||
setContainer={setContainer}
|
||||
onClose={() => {
|
||||
setIsFlyoutVisible(false);
|
||||
setIsinlineEditingVisible(false);
|
||||
setPanelActive(null);
|
||||
if (container) {
|
||||
ReactDOM.unmountComponentAtNode(container);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageSection>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
</KibanaContextProvider>
|
||||
inlineEditingContent={{
|
||||
visible: isInlineEditingVisible,
|
||||
}}
|
||||
setContainer={setContainer}
|
||||
onClose={() => {
|
||||
setIsFlyoutVisible(false);
|
||||
setIsinlineEditingVisible(false);
|
||||
setPanelActive(null);
|
||||
if (container) {
|
||||
ReactDOM.unmountComponentAtNode(container);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageSection>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -64,13 +64,13 @@ export const LensChart = (props: {
|
|||
(
|
||||
isLoading: boolean,
|
||||
adapters: InlineEditLensEmbeddableContext['lensEvent']['adapters'] | undefined,
|
||||
lensEmbeddableOutput$?: InlineEditLensEmbeddableContext['lensEvent']['embeddableOutput$']
|
||||
dataLoading$?: InlineEditLensEmbeddableContext['lensEvent']['dataLoading$']
|
||||
) => {
|
||||
const adapterTables = adapters?.tables?.tables;
|
||||
if (adapterTables && !isLoading) {
|
||||
setLensLoadEvent({
|
||||
adapters,
|
||||
embeddableOutput$: lensEmbeddableOutput$,
|
||||
dataLoading$,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
@ -10,6 +10,7 @@ import { render, unmountComponentAtNode } from 'react-dom';
|
|||
import { EuiCallOut } from '@elastic/eui';
|
||||
|
||||
import type { CoreSetup, AppMountParameters } from '@kbn/core/public';
|
||||
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
||||
import type { StartDependencies } from './plugin';
|
||||
|
||||
export const mount =
|
||||
|
@ -21,10 +22,15 @@ export const mount =
|
|||
const dataView = await plugins.dataViews.getDefaultDataView();
|
||||
const stateHelpers = await plugins.lens.stateHelperApi();
|
||||
|
||||
const i18nCore = core.i18n;
|
||||
|
||||
const reactElement = (
|
||||
<i18nCore.Context>
|
||||
<KibanaRenderContextProvider
|
||||
{...{
|
||||
uiSettings: core.uiSettings,
|
||||
settings: core.settings,
|
||||
theme: core.theme,
|
||||
i18n: core.i18n,
|
||||
}}
|
||||
>
|
||||
{dataView ? (
|
||||
<App
|
||||
core={core}
|
||||
|
@ -41,7 +47,7 @@ export const mount =
|
|||
<p>You need at least one dataview for this demo to work</p>
|
||||
</EuiCallOut>
|
||||
)}
|
||||
</i18nCore.Context>
|
||||
</KibanaRenderContextProvider>
|
||||
);
|
||||
|
||||
render(reactElement, element);
|
||||
|
|
|
@ -19,8 +19,8 @@
|
|||
"@kbn/developer-examples-plugin",
|
||||
"@kbn/data-views-plugin",
|
||||
"@kbn/ui-actions-plugin",
|
||||
"@kbn/kibana-react-plugin",
|
||||
"@kbn/lens-embeddable-utils",
|
||||
"@kbn/ui-theme",
|
||||
"@kbn/react-kibana-context-render",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -29,7 +29,6 @@ import type {
|
|||
TypedLensByValueInput,
|
||||
PersistedIndexPatternLayer,
|
||||
XYState,
|
||||
LensEmbeddableInput,
|
||||
DateHistogramIndexPatternColumn,
|
||||
DatatableVisualizationState,
|
||||
HeatmapVisualizationState,
|
||||
|
@ -42,7 +41,6 @@ import type {
|
|||
MetricVisualizationState,
|
||||
} from '@kbn/lens-plugin/public';
|
||||
import type { ActionExecutionContext } from '@kbn/ui-actions-plugin/public';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { CodeEditor, HJsonLang } from '@kbn/code-editor';
|
||||
import type { StartDependencies } from './plugin';
|
||||
import {
|
||||
|
@ -496,269 +494,256 @@ export const App = (props: {
|
|||
const [overrides, setOverrides] = useState<AllOverrides | undefined>();
|
||||
|
||||
return (
|
||||
<KibanaContextProvider
|
||||
services={{
|
||||
uiSettings: props.core.uiSettings,
|
||||
settings: props.core.settings,
|
||||
theme: props.core.theme,
|
||||
}}
|
||||
>
|
||||
<EuiPage>
|
||||
<EuiPageBody style={{ maxWidth: 1200, margin: '0 auto' }}>
|
||||
<EuiPageHeader
|
||||
paddingSize="s"
|
||||
bottomBorder={true}
|
||||
pageTitle="Lens embeddable playground"
|
||||
/>
|
||||
<EuiPageSection paddingSize="s">
|
||||
<EuiFlexGroup
|
||||
className="eui-fullHeight"
|
||||
gutterSize="none"
|
||||
direction="column"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem className="eui-fullHeight">
|
||||
<EuiFlexGroup className="eui-fullHeight" gutterSize="l">
|
||||
<EuiFlexItem grow={3}>
|
||||
<EuiPanel hasShadow={false}>
|
||||
<p>
|
||||
This app embeds a Lens visualization by specifying the configuration. Data
|
||||
fetching and rendering is completely managed by Lens itself.
|
||||
</p>
|
||||
<p>
|
||||
The editor on the right hand side make it possible to paste a Lens
|
||||
attributes configuration, and have it rendered. Presets are available to
|
||||
have a starting configuration, and new presets can be saved as well (not
|
||||
persisted).
|
||||
</p>
|
||||
<p>
|
||||
The Open with Lens button will take the current configuration and navigate
|
||||
to a prefilled editor.
|
||||
</p>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup wrap>
|
||||
<EuiFlexItem grow={false}>
|
||||
<AttributesMenu
|
||||
currentSO={currentSO}
|
||||
currentAttributes={currentAttributes}
|
||||
saveValidSO={saveValidSO}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<OverridesMenu
|
||||
currentAttributes={currentAttributes}
|
||||
overrides={overrides}
|
||||
setOverrides={setOverrides}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<PanelMenu
|
||||
enableTriggers={enableTriggers}
|
||||
toggleTriggers={toggleTriggers}
|
||||
enableDefaultAction={enableDefaultAction}
|
||||
setEnableDefaultAction={setEnableDefaultAction}
|
||||
enableExtraAction={enableExtraAction}
|
||||
setEnableExtraAction={setEnableExtraAction}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiPage>
|
||||
<EuiPageBody style={{ maxWidth: 1200, margin: '0 auto' }}>
|
||||
<EuiPageHeader paddingSize="s" bottomBorder={true} pageTitle="Lens embeddable playground" />
|
||||
<EuiPageSection paddingSize="s">
|
||||
<EuiFlexGroup
|
||||
className="eui-fullHeight"
|
||||
gutterSize="none"
|
||||
direction="column"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem className="eui-fullHeight">
|
||||
<EuiFlexGroup className="eui-fullHeight" gutterSize="l">
|
||||
<EuiFlexItem grow={3}>
|
||||
<EuiPanel hasShadow={false}>
|
||||
<p>
|
||||
This app embeds a Lens visualization by specifying the configuration. Data
|
||||
fetching and rendering is completely managed by Lens itself.
|
||||
</p>
|
||||
<p>
|
||||
The editor on the right hand side make it possible to paste a Lens attributes
|
||||
configuration, and have it rendered. Presets are available to have a starting
|
||||
configuration, and new presets can be saved as well (not persisted).
|
||||
</p>
|
||||
<p>
|
||||
The Open with Lens button will take the current configuration and navigate to
|
||||
a prefilled editor.
|
||||
</p>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup wrap>
|
||||
<EuiFlexItem grow={false}>
|
||||
<AttributesMenu
|
||||
currentSO={currentSO}
|
||||
currentAttributes={currentAttributes}
|
||||
saveValidSO={saveValidSO}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<OverridesMenu
|
||||
currentAttributes={currentAttributes}
|
||||
overrides={overrides}
|
||||
setOverrides={setOverrides}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<PanelMenu
|
||||
enableTriggers={enableTriggers}
|
||||
toggleTriggers={toggleTriggers}
|
||||
enableDefaultAction={enableDefaultAction}
|
||||
setEnableDefaultAction={setEnableDefaultAction}
|
||||
enableExtraAction={enableExtraAction}
|
||||
setEnableExtraAction={setEnableExtraAction}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
aria-label="Save visualization into library or embed directly into any dashboard"
|
||||
data-test-subj="lns-example-save"
|
||||
isDisabled={isDisabled}
|
||||
onClick={() => {
|
||||
setIsSaveModalVisible(true);
|
||||
}}
|
||||
>
|
||||
Save Visualization
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
{props.defaultDataView?.isTimeBased() ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
aria-label="Save visualization into library or embed directly into any dashboard"
|
||||
data-test-subj="lns-example-save"
|
||||
aria-label="Change time range"
|
||||
data-test-subj="lns-example-change-time-range"
|
||||
isDisabled={isDisabled}
|
||||
onClick={() => {
|
||||
setIsSaveModalVisible(true);
|
||||
}}
|
||||
>
|
||||
Save Visualization
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
{props.defaultDataView?.isTimeBased() ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
aria-label="Change time range"
|
||||
data-test-subj="lns-example-change-time-range"
|
||||
isDisabled={isDisabled}
|
||||
onClick={() => {
|
||||
setTime(
|
||||
time.to === 'now'
|
||||
? {
|
||||
from: '2015-09-18T06:31:44.000Z',
|
||||
to: '2015-09-23T18:31:44.000Z',
|
||||
}
|
||||
: {
|
||||
from: 'now-5d',
|
||||
to: 'now',
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
{time.to === 'now' ? 'Change time range' : 'Reset time range'}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
aria-label="Open lens in new tab"
|
||||
isDisabled={!props.plugins.lens.canUseEditor()}
|
||||
onClick={() => {
|
||||
props.plugins.lens.navigateToPrefilledEditor(
|
||||
{
|
||||
id: '',
|
||||
timeRange: time,
|
||||
attributes: currentAttributes,
|
||||
},
|
||||
{
|
||||
openInNewTab: true,
|
||||
}
|
||||
setTime(
|
||||
time.to === 'now'
|
||||
? {
|
||||
from: '2015-09-18T06:31:44.000Z',
|
||||
to: '2015-09-23T18:31:44.000Z',
|
||||
}
|
||||
: {
|
||||
from: 'now-5d',
|
||||
to: 'now',
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
Open in Lens (new tab)
|
||||
{time.to === 'now' ? 'Change time range' : 'Reset time range'}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<p>State: {isLoading ? 'Loading...' : 'Rendered'}</p>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<LensComponent
|
||||
id="myLens"
|
||||
style={{ height: 500 }}
|
||||
timeRange={time}
|
||||
attributes={currentAttributes}
|
||||
overrides={overrides}
|
||||
onLoad={(val) => {
|
||||
setIsLoading(val);
|
||||
}}
|
||||
onBrushEnd={({ range }) => {
|
||||
setTime({
|
||||
from: new Date(range[0]).toISOString(),
|
||||
to: new Date(range[1]).toISOString(),
|
||||
});
|
||||
}}
|
||||
onFilter={(_data) => {
|
||||
// call back event for on filter event
|
||||
}}
|
||||
onTableRowClick={(_data) => {
|
||||
// call back event for on table row click event
|
||||
}}
|
||||
disableTriggers={!enableTriggers}
|
||||
viewMode={ViewMode.VIEW}
|
||||
withDefaultActions={enableDefaultAction}
|
||||
extraActions={
|
||||
enableExtraAction
|
||||
? [
|
||||
{
|
||||
id: 'testAction',
|
||||
type: 'link',
|
||||
getIconType: () => 'save',
|
||||
async isCompatible(
|
||||
context: ActionExecutionContext<object>
|
||||
): Promise<boolean> {
|
||||
return true;
|
||||
},
|
||||
execute: async (context: ActionExecutionContext<object>) => {
|
||||
alert('I am an extra action');
|
||||
return;
|
||||
},
|
||||
getDisplayName: () => 'Extra action',
|
||||
},
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
{isSaveModalVisible && (
|
||||
<LensSaveModalComponent
|
||||
initialInput={currentAttributes as unknown as LensEmbeddableInput}
|
||||
onSave={() => {}}
|
||||
onClose={() => setIsSaveModalVisible(false)}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={2}>
|
||||
<EuiPanel hasShadow={false}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiText>
|
||||
<p>Paste or edit here your Lens document</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSelect
|
||||
options={charts.map(({ id }, i) => ({ value: i, text: id }))}
|
||||
value={undefined}
|
||||
onChange={(e) => switchChartPreset(+e.target.value)}
|
||||
aria-label="Load from a preset"
|
||||
prepend={'Load preset'}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
aria-label="Save the preset"
|
||||
data-test-subj="lns-example-save"
|
||||
isDisabled={isDisabled || hasParsingError}
|
||||
onClick={() => {
|
||||
const attributes = checkAndParseSO(currentSO.current);
|
||||
if (attributes) {
|
||||
const label = `custom-chart-${chartCounter}`;
|
||||
addChartConfiguration([
|
||||
...loadedCharts,
|
||||
) : null}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
aria-label="Open lens in new tab"
|
||||
isDisabled={!props.plugins.lens.canUseEditor()}
|
||||
onClick={() => {
|
||||
props.plugins.lens.navigateToPrefilledEditor(
|
||||
{
|
||||
id: '',
|
||||
timeRange: time,
|
||||
attributes: currentAttributes,
|
||||
},
|
||||
{
|
||||
openInNewTab: true,
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
Open in Lens (new tab)
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<p>State: {isLoading ? 'Loading...' : 'Rendered'}</p>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<LensComponent
|
||||
id="myLens"
|
||||
style={{ height: 500 }}
|
||||
timeRange={time}
|
||||
attributes={currentAttributes}
|
||||
overrides={overrides}
|
||||
onLoad={(val) => {
|
||||
setIsLoading(val);
|
||||
}}
|
||||
onBrushEnd={({ range }) => {
|
||||
setTime({
|
||||
from: new Date(range[0]).toISOString(),
|
||||
to: new Date(range[1]).toISOString(),
|
||||
});
|
||||
}}
|
||||
onFilter={(_data) => {
|
||||
// call back event for on filter event
|
||||
}}
|
||||
onTableRowClick={(_data) => {
|
||||
// call back event for on table row click event
|
||||
}}
|
||||
disableTriggers={!enableTriggers}
|
||||
viewMode={ViewMode.VIEW}
|
||||
withDefaultActions={enableDefaultAction}
|
||||
extraActions={
|
||||
enableExtraAction
|
||||
? [
|
||||
{
|
||||
id: label,
|
||||
attributes,
|
||||
id: 'testAction',
|
||||
type: 'link',
|
||||
getIconType: () => 'save',
|
||||
async isCompatible(
|
||||
context: ActionExecutionContext<object>
|
||||
): Promise<boolean> {
|
||||
return true;
|
||||
},
|
||||
execute: async (context: ActionExecutionContext<object>) => {
|
||||
alert('I am an extra action');
|
||||
return;
|
||||
},
|
||||
getDisplayName: () => 'Extra action',
|
||||
},
|
||||
]);
|
||||
chartCounter++;
|
||||
alert(`The preset has been saved as "${label}"`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Save as preset
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
{hasParsingErrorDebounced && currentSO.current !== currentValid && (
|
||||
<EuiCallOut title="Error" color="danger" iconType="warning">
|
||||
<p>Check the spec</p>
|
||||
</EuiCallOut>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup style={{ height: '75vh' }} direction="column">
|
||||
<EuiFlexItem>
|
||||
<CodeEditor
|
||||
languageId={HJsonLang}
|
||||
options={{
|
||||
fontSize: 14,
|
||||
wordWrap: 'on',
|
||||
}}
|
||||
value={currentSO.current}
|
||||
onChange={(newSO) => {
|
||||
const isValid = Boolean(checkAndParseSO(newSO));
|
||||
setErrorFlag(!isValid);
|
||||
currentSO.current = newSO;
|
||||
if (isValid) {
|
||||
// reset the debounced error
|
||||
setErrorDebounced(isValid);
|
||||
saveValidSO(newSO);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageSection>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
</KibanaContextProvider>
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
{isSaveModalVisible && (
|
||||
<LensSaveModalComponent
|
||||
initialInput={{ attributes: currentAttributes }}
|
||||
onSave={() => {}}
|
||||
onClose={() => setIsSaveModalVisible(false)}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={2}>
|
||||
<EuiPanel hasShadow={false}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiText>
|
||||
<p>Paste or edit here your Lens document</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSelect
|
||||
options={charts.map(({ id }, i) => ({ value: i, text: id }))}
|
||||
value={undefined}
|
||||
onChange={(e) => switchChartPreset(+e.target.value)}
|
||||
aria-label="Load from a preset"
|
||||
prepend={'Load preset'}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
aria-label="Save the preset"
|
||||
data-test-subj="lns-example-save"
|
||||
isDisabled={isDisabled || hasParsingError}
|
||||
onClick={() => {
|
||||
const attributes = checkAndParseSO(currentSO.current);
|
||||
if (attributes) {
|
||||
const label = `custom-chart-${chartCounter}`;
|
||||
addChartConfiguration([
|
||||
...loadedCharts,
|
||||
{
|
||||
id: label,
|
||||
attributes,
|
||||
},
|
||||
]);
|
||||
chartCounter++;
|
||||
alert(`The preset has been saved as "${label}"`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Save as preset
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
{hasParsingErrorDebounced && currentSO.current !== currentValid && (
|
||||
<EuiCallOut title="Error" color="danger" iconType="warning">
|
||||
<p>Check the spec</p>
|
||||
</EuiCallOut>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup style={{ height: '75vh' }} direction="column">
|
||||
<EuiFlexItem>
|
||||
<CodeEditor
|
||||
languageId={HJsonLang}
|
||||
options={{
|
||||
fontSize: 14,
|
||||
wordWrap: 'on',
|
||||
}}
|
||||
value={currentSO.current}
|
||||
onChange={(newSO) => {
|
||||
const isValid = Boolean(checkAndParseSO(newSO));
|
||||
setErrorFlag(!isValid);
|
||||
currentSO.current = newSO;
|
||||
if (isValid) {
|
||||
// reset the debounced error
|
||||
setErrorDebounced(isValid);
|
||||
saveValidSO(newSO);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageSection>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@ import { EuiCallOut } from '@elastic/eui';
|
|||
|
||||
import type { CoreSetup, AppMountParameters } from '@kbn/core/public';
|
||||
import type { TypedLensByValueInput } from '@kbn/lens-plugin/public';
|
||||
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
||||
import type { StartDependencies } from './plugin';
|
||||
|
||||
export const mount =
|
||||
|
@ -24,10 +25,15 @@ export const mount =
|
|||
const dataView = await plugins.data.indexPatterns.getDefault();
|
||||
const stateHelpers = await plugins.lens.stateHelperApi();
|
||||
|
||||
const i18nCore = core.i18n;
|
||||
|
||||
const reactElement = (
|
||||
<i18nCore.Context>
|
||||
<KibanaRenderContextProvider
|
||||
{...{
|
||||
uiSettings: core.uiSettings,
|
||||
settings: core.settings,
|
||||
theme: core.theme,
|
||||
i18n: core.i18n,
|
||||
}}
|
||||
>
|
||||
{dataView ? (
|
||||
<App
|
||||
core={core}
|
||||
|
@ -45,7 +51,7 @@ export const mount =
|
|||
<p>This demo only works if your default index pattern is set and time based</p>
|
||||
</EuiCallOut>
|
||||
)}
|
||||
</i18nCore.Context>
|
||||
</KibanaRenderContextProvider>
|
||||
);
|
||||
|
||||
render(reactElement, element);
|
||||
|
|
|
@ -21,8 +21,8 @@
|
|||
"@kbn/developer-examples-plugin",
|
||||
"@kbn/data-views-plugin",
|
||||
"@kbn/ui-actions-plugin",
|
||||
"@kbn/kibana-react-plugin",
|
||||
"@kbn/core-ui-settings-browser",
|
||||
"@kbn/code-editor",
|
||||
"@kbn/react-kibana-context-render",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -49,6 +49,8 @@ export const useCanvasApi: () => CanvasContainerApi = () => {
|
|||
createNewEmbeddable(panelType, initialState);
|
||||
},
|
||||
disableTriggers: true,
|
||||
// this is required to disable inline editing now enabled by default
|
||||
canEditInline: false,
|
||||
type: 'canvas',
|
||||
/**
|
||||
* getSerializedStateForChild is left out here because we cannot access the state here. That method
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import type { CoreStart } from '@kbn/core-lifecycle-browser';
|
||||
import { isLensApi } from '@kbn/lens-plugin/public';
|
||||
import { hasBlockingError } from '@kbn/presentation-publishing';
|
||||
import { apiPublishesTimeRange, hasBlockingError } from '@kbn/presentation-publishing';
|
||||
import { canUseCases } from '../../../client/helpers/can_use_cases';
|
||||
import { getCaseOwnerByAppId } from '../../../../common/utils/owner';
|
||||
|
||||
|
@ -20,7 +20,11 @@ export function isCompatible(
|
|||
if (!embeddable.getFullAttributes()) {
|
||||
return false;
|
||||
}
|
||||
const timeRange = embeddable.timeRange$?.value ?? embeddable.parentApi?.timeRange$?.value;
|
||||
const timeRange =
|
||||
embeddable.timeRange$?.value ??
|
||||
(embeddable.parentApi && apiPublishesTimeRange(embeddable.parentApi)
|
||||
? embeddable.parentApi?.timeRange$?.value
|
||||
: undefined);
|
||||
if (!timeRange) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
|
||||
import { createBrowserHistory } from 'history';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { getLensApiMock } from '@kbn/lens-plugin/public/react_embeddable/mocks';
|
||||
import type { PublicAppInfo } from '@kbn/core/public';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import type { LensApi, LensSavedObjectAttributes } from '@kbn/lens-plugin/public';
|
||||
import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
|
||||
import type { TimeRange } from '@kbn/es-query';
|
||||
import type { Services } from './types';
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
|
@ -39,24 +39,16 @@ export const mockLensAttributes = {
|
|||
export const getMockLensApi = (
|
||||
{ from, to = 'now' }: { from: string; to: string } = { from: 'now-24h', to: 'now' }
|
||||
): LensApi =>
|
||||
({
|
||||
type: 'lens',
|
||||
getSavedVis: () => {},
|
||||
canViewUnderlyingData$: new BehaviorSubject(true),
|
||||
getViewUnderlyingDataArgs: () => {},
|
||||
getLensApiMock({
|
||||
getFullAttributes: () => {
|
||||
return mockLensAttributes;
|
||||
},
|
||||
panelTitle: new BehaviorSubject('myPanel'),
|
||||
hidePanelTitle: new BehaviorSubject(false),
|
||||
timeslice$: new BehaviorSubject<[number, number] | undefined>(undefined),
|
||||
panelTitle: new BehaviorSubject<string | undefined>('myPanel'),
|
||||
timeRange$: new BehaviorSubject<TimeRange | undefined>({
|
||||
from,
|
||||
to,
|
||||
}),
|
||||
filters$: new BehaviorSubject<Filter[] | undefined>(undefined),
|
||||
query$: new BehaviorSubject<Query | AggregateQuery | undefined>(undefined),
|
||||
} as unknown as LensApi);
|
||||
});
|
||||
|
||||
export const getMockCurrentAppId$ = () => new BehaviorSubject<string>('securitySolutionUI');
|
||||
export const getMockApplications$ = () =>
|
||||
|
|
|
@ -9,7 +9,7 @@ import React, { useEffect, useMemo } from 'react';
|
|||
import { unmountComponentAtNode } from 'react-dom';
|
||||
import type { LensApi } from '@kbn/lens-plugin/public';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
|
||||
import { apiPublishesTimeRange, useStateFromPublishingSubject } from '@kbn/presentation-publishing';
|
||||
import { ActionWrapper } from './action_wrapper';
|
||||
import type { CasesActionContextProps, Services } from './types';
|
||||
import type { CaseUI } from '../../../../common';
|
||||
|
@ -30,7 +30,9 @@ const AddExistingCaseModalWrapper: React.FC<Props> = ({ lensApi, onClose, onSucc
|
|||
});
|
||||
|
||||
const timeRange = useStateFromPublishingSubject(lensApi.timeRange$);
|
||||
const parentTimeRange = useStateFromPublishingSubject(lensApi.parentApi?.timeRange$);
|
||||
const parentTimeRange = useStateFromPublishingSubject(
|
||||
apiPublishesTimeRange(lensApi.parentApi) ? lensApi.parentApi?.timeRange$ : undefined
|
||||
);
|
||||
const absoluteTimeRange = convertToAbsoluteTimeRange(timeRange);
|
||||
const absoluteParentTimeRange = convertToAbsoluteTimeRange(parentTimeRange);
|
||||
|
||||
|
|
|
@ -10,13 +10,17 @@ import type { RefreshInterval, TimeRange } from '@kbn/data-plugin/common/query';
|
|||
import type { Filter } from '@kbn/es-query';
|
||||
|
||||
export const PLUGIN_ID = 'lens';
|
||||
export const APP_ID = 'lens';
|
||||
export const LENS_APP_NAME = 'lens';
|
||||
export const LENS_EMBEDDABLE_TYPE = 'lens';
|
||||
export const APP_ID = PLUGIN_ID;
|
||||
export const DOC_TYPE = 'lens';
|
||||
export const LENS_APP_NAME = APP_ID;
|
||||
export const LENS_EMBEDDABLE_TYPE = DOC_TYPE;
|
||||
export const NOT_INTERNATIONALIZED_PRODUCT_NAME = 'Lens Visualizations';
|
||||
export const BASE_API_URL = '/api/lens';
|
||||
export const LENS_EDIT_BY_VALUE = 'edit_by_value';
|
||||
export const LENS_ICON = 'lensApp';
|
||||
export const STAGE_ID = 'production';
|
||||
|
||||
export const INDEX_PATTERN_TYPE = 'index-pattern';
|
||||
|
||||
export const PieChartTypes = {
|
||||
PIE: 'pie',
|
||||
|
|
|
@ -6,47 +6,52 @@
|
|||
*/
|
||||
|
||||
import { cloneDeep } from 'lodash';
|
||||
import type { SerializableRecord, Serializable } from '@kbn/utility-types';
|
||||
import type { SerializableRecord } from '@kbn/utility-types';
|
||||
import type { SavedObjectReference } from '@kbn/core/types';
|
||||
import type {
|
||||
EmbeddableStateWithType,
|
||||
import {
|
||||
EmbeddableRegistryDefinition,
|
||||
EmbeddableStateWithType,
|
||||
} from '@kbn/embeddable-plugin/common';
|
||||
import type { LensRuntimeState } from '../../public';
|
||||
|
||||
export type LensEmbeddablePersistableState = EmbeddableStateWithType & {
|
||||
attributes: SerializableRecord;
|
||||
};
|
||||
|
||||
export const inject: EmbeddableRegistryDefinition['inject'] = (state, references) => {
|
||||
// We need to clone the state because we can not modify the original state object.
|
||||
const typedState = cloneDeep(state) as LensEmbeddablePersistableState;
|
||||
export const inject: NonNullable<EmbeddableRegistryDefinition['inject']> = (
|
||||
state,
|
||||
references
|
||||
): EmbeddableStateWithType => {
|
||||
const typedState = cloneDeep(state) as unknown as LensRuntimeState;
|
||||
|
||||
if ('attributes' in typedState && typedState.attributes !== undefined) {
|
||||
// match references based on name, so only references associated with this lens panel are injected.
|
||||
const matchedReferences: SavedObjectReference[] = [];
|
||||
|
||||
if (Array.isArray(typedState.attributes.references)) {
|
||||
typedState.attributes.references.forEach((serializableRef) => {
|
||||
const internalReference = serializableRef as unknown as SavedObjectReference;
|
||||
const matchedReference = references.find(
|
||||
(reference) => reference.name === internalReference.name
|
||||
);
|
||||
if (matchedReference) matchedReferences.push(matchedReference);
|
||||
});
|
||||
}
|
||||
|
||||
typedState.attributes.references = matchedReferences as unknown as Serializable[];
|
||||
if (typedState.savedObjectId) {
|
||||
return typedState as unknown as EmbeddableStateWithType;
|
||||
}
|
||||
|
||||
return typedState;
|
||||
// match references based on name, so only references associated with this lens panel are injected.
|
||||
const matchedReferences: SavedObjectReference[] = [];
|
||||
|
||||
if (Array.isArray(typedState.attributes.references)) {
|
||||
typedState.attributes.references.forEach((serializableRef) => {
|
||||
const internalReference = serializableRef;
|
||||
const matchedReference = references.find(
|
||||
(reference) => reference.name === internalReference.name
|
||||
);
|
||||
if (matchedReference) matchedReferences.push(matchedReference);
|
||||
});
|
||||
}
|
||||
|
||||
typedState.attributes.references = matchedReferences;
|
||||
|
||||
return typedState as unknown as EmbeddableStateWithType;
|
||||
};
|
||||
|
||||
export const extract: EmbeddableRegistryDefinition['extract'] = (state) => {
|
||||
export const extract: NonNullable<EmbeddableRegistryDefinition['extract']> = (state) => {
|
||||
let references: SavedObjectReference[] = [];
|
||||
const typedState = state as LensEmbeddablePersistableState;
|
||||
const typedState = state as unknown as LensRuntimeState;
|
||||
|
||||
if ('attributes' in typedState && typedState.attributes !== undefined) {
|
||||
references = typedState.attributes.references as unknown as SavedObjectReference[];
|
||||
references = typedState.attributes.references;
|
||||
}
|
||||
|
||||
return { state, references };
|
||||
|
|
|
@ -9,7 +9,7 @@ import rison from '@kbn/rison';
|
|||
import type { SerializableRecord } from '@kbn/utility-types';
|
||||
import type { GlobalQueryStateFromUrl } from '@kbn/data-plugin/public';
|
||||
import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/common';
|
||||
import type { Filter, Query } from '@kbn/es-query';
|
||||
import type { AggregateQuery, Filter, Query } from '@kbn/es-query';
|
||||
import type { DataViewSpec, SavedQuery } from '@kbn/data-plugin/common';
|
||||
import { SavedObjectReference } from '@kbn/core-saved-objects-common';
|
||||
import type { DateRange } from '../types';
|
||||
|
@ -26,7 +26,7 @@ interface LensShareableState {
|
|||
/**
|
||||
* Optionally set a query.
|
||||
*/
|
||||
query?: Query;
|
||||
query?: Query | AggregateQuery;
|
||||
|
||||
/**
|
||||
* Optionally set the date range in the date picker.
|
||||
|
@ -88,7 +88,7 @@ export interface LensAppLocatorParams extends SerializableRecord {
|
|||
/**
|
||||
* Optionally set a query.
|
||||
*/
|
||||
query?: Query;
|
||||
query?: Query | AggregateQuery;
|
||||
|
||||
/**
|
||||
* Optionally set the date range in the date picker.
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
"expressionLegacyMetricVis",
|
||||
"expressionPartitionVis",
|
||||
"usageCollection",
|
||||
"embeddableEnhanced",
|
||||
"taskManager",
|
||||
"globalSearch",
|
||||
"savedObjectsTagging",
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -9,16 +9,14 @@ import './app.scss';
|
|||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { TimeRange } from '@kbn/es-query';
|
||||
import { EuiBreadcrumb, EuiConfirmModal } from '@elastic/eui';
|
||||
import { EuiConfirmModal } from '@elastic/eui';
|
||||
import { useExecutionContext, useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { OnSaveProps } from '@kbn/saved-objects-plugin/public';
|
||||
import type { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public';
|
||||
import type { LensAppLocatorParams } from '../../common/locator/locator';
|
||||
import { LensAppProps, LensAppServices } from './types';
|
||||
import { LensTopNavMenu } from './lens_top_nav';
|
||||
import { LensByReferenceInput } from '../embeddable';
|
||||
import { AddUserMessages, EditorFrameInstance, UserMessagesGetter } from '../types';
|
||||
import { Document } from '../persistence/saved_object_store';
|
||||
import { AddUserMessages, EditorFrameInstance, Simplify, UserMessagesGetter } from '../types';
|
||||
import { LensDocument } from '../persistence/saved_object_store';
|
||||
|
||||
import {
|
||||
setState,
|
||||
|
@ -43,15 +41,24 @@ import {
|
|||
import { replaceIndexpattern } from '../state_management/lens_slice';
|
||||
import { useApplicationUserMessages } from './get_application_user_messages';
|
||||
import { trackSaveUiCounterEvents } from '../lens_ui_telemetry';
|
||||
import {
|
||||
getCurrentTitle,
|
||||
isLegacyEditorEmbeddable,
|
||||
setBreadcrumbsTitle,
|
||||
useNavigateBackToApp,
|
||||
useShortUrlService,
|
||||
} from './app_helpers';
|
||||
|
||||
export type SaveProps = Omit<OnSaveProps, 'onTitleDuplicate' | 'newDescription'> & {
|
||||
returnToOrigin: boolean;
|
||||
dashboardId?: string | null;
|
||||
onTitleDuplicate?: OnSaveProps['onTitleDuplicate'];
|
||||
newDescription?: string;
|
||||
newTags?: string[];
|
||||
panelTimeRange?: TimeRange;
|
||||
};
|
||||
export type SaveProps = Simplify<
|
||||
Omit<OnSaveProps, 'onTitleDuplicate' | 'newDescription'> & {
|
||||
returnToOrigin: boolean;
|
||||
dashboardId?: string | null;
|
||||
onTitleDuplicate?: OnSaveProps['onTitleDuplicate'];
|
||||
newDescription?: string;
|
||||
newTags?: string[];
|
||||
panelTimeRange?: TimeRange;
|
||||
}
|
||||
>;
|
||||
|
||||
export function App({
|
||||
history,
|
||||
|
@ -127,18 +134,26 @@ export function App({
|
|||
selectSavedObjectFormat(state, selectorDependencies)
|
||||
);
|
||||
|
||||
const shortUrls = useMemo(() => share?.url.shortUrls.get(null), [share]);
|
||||
|
||||
// Used to show a popover that guides the user towards changing the date range when no data is available.
|
||||
const [indicateNoData, setIndicateNoData] = useState(false);
|
||||
const [isSaveModalVisible, setIsSaveModalVisible] = useState(false);
|
||||
const [lastKnownDoc, setLastKnownDoc] = useState<Document | undefined>(undefined);
|
||||
const [initialDocFromContext, setInitialDocFromContext] = useState<Document | undefined>(
|
||||
const [lastKnownDoc, setLastKnownDoc] = useState<LensDocument | undefined>(undefined);
|
||||
const [initialDocFromContext, setInitialDocFromContext] = useState<LensDocument | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [isGoBackToVizEditorModalVisible, setIsGoBackToVizEditorModalVisible] = useState(false);
|
||||
const [shouldCloseAndSaveTextBasedQuery, setShouldCloseAndSaveTextBasedQuery] = useState(false);
|
||||
const savedObjectId = (initialInput as LensByReferenceInput)?.savedObjectId;
|
||||
const savedObjectId = initialInput?.savedObjectId;
|
||||
|
||||
const isFromLegacyEditorEmbeddable = isLegacyEditorEmbeddable(initialContext);
|
||||
const legacyEditorAppName =
|
||||
initialContext && 'originatingApp' in initialContext
|
||||
? initialContext.originatingApp
|
||||
: undefined;
|
||||
const legacyEditorAppUrl =
|
||||
initialContext && 'vizEditorOriginatingAppUrl' in initialContext
|
||||
? initialContext.vizEditorOriginatingAppUrl
|
||||
: undefined;
|
||||
const initialContextIsEmbedded = Boolean(legacyEditorAppName);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentDoc) {
|
||||
|
@ -167,18 +182,27 @@ export function App({
|
|||
[isLinkedToOriginatingApp, savedObjectId]
|
||||
);
|
||||
|
||||
// Wrap the isEqual call to avoid to carry all the static references
|
||||
// around all the time.
|
||||
const isLensEqualWrapper = useCallback(
|
||||
(refDoc: LensDocument | undefined) => {
|
||||
return isLensEqual(
|
||||
refDoc,
|
||||
lastKnownDoc,
|
||||
data.query.filterManager.inject.bind(data.query.filterManager),
|
||||
datasourceMap,
|
||||
visualizationMap,
|
||||
annotationGroups
|
||||
);
|
||||
},
|
||||
[annotationGroups, data.query.filterManager, datasourceMap, lastKnownDoc, visualizationMap]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onAppLeave((actions) => {
|
||||
if (
|
||||
application.capabilities.visualize.save &&
|
||||
!isLensEqual(
|
||||
persistedDoc,
|
||||
lastKnownDoc,
|
||||
data.query.filterManager.inject.bind(data.query.filterManager),
|
||||
datasourceMap,
|
||||
visualizationMap,
|
||||
annotationGroups
|
||||
) &&
|
||||
!isLensEqualWrapper(persistedDoc) &&
|
||||
(isSaveable || persistedDoc)
|
||||
) {
|
||||
return actions.confirm(
|
||||
|
@ -208,6 +232,7 @@ export function App({
|
|||
datasourceMap,
|
||||
visualizationMap,
|
||||
annotationGroups,
|
||||
isLensEqualWrapper,
|
||||
]);
|
||||
|
||||
const getLegacyUrlConflictCallout = useCallback(() => {
|
||||
|
@ -235,66 +260,17 @@ export function App({
|
|||
// Sync Kibana breadcrumbs any time the saved document's title changes
|
||||
useEffect(() => {
|
||||
const isByValueMode = getIsByValueMode();
|
||||
const comesFromVizEditorDashboard =
|
||||
initialContext && 'originatingApp' in initialContext && initialContext.originatingApp;
|
||||
const breadcrumbs: EuiBreadcrumb[] = [];
|
||||
if (
|
||||
(isLinkedToOriginatingApp || comesFromVizEditorDashboard) &&
|
||||
getOriginatingAppName() &&
|
||||
redirectToOrigin
|
||||
) {
|
||||
breadcrumbs.push({
|
||||
onClick: () => {
|
||||
redirectToOrigin();
|
||||
},
|
||||
text: getOriginatingAppName(),
|
||||
});
|
||||
}
|
||||
if (!isByValueMode) {
|
||||
breadcrumbs.push({
|
||||
href: application.getUrlForApp('visualize'),
|
||||
onClick: (e) => {
|
||||
application.navigateToApp('visualize', { path: '/' });
|
||||
e.preventDefault();
|
||||
},
|
||||
text: i18n.translate('xpack.lens.breadcrumbsTitle', {
|
||||
defaultMessage: 'Visualize Library',
|
||||
}),
|
||||
});
|
||||
}
|
||||
let currentDocTitle = i18n.translate('xpack.lens.breadcrumbsCreate', {
|
||||
defaultMessage: 'Create',
|
||||
});
|
||||
if (persistedDoc) {
|
||||
currentDocTitle = isByValueMode
|
||||
? i18n.translate('xpack.lens.breadcrumbsByValue', { defaultMessage: 'Edit visualization' })
|
||||
: persistedDoc.title;
|
||||
}
|
||||
if (
|
||||
!persistedDoc?.title &&
|
||||
initialContext &&
|
||||
'isEmbeddable' in initialContext &&
|
||||
initialContext.isEmbeddable
|
||||
) {
|
||||
currentDocTitle = i18n.translate('xpack.lens.breadcrumbsEditInLensFromDashboard', {
|
||||
defaultMessage: 'Converting {title} visualization',
|
||||
values: {
|
||||
title: initialContext.title ? `"${initialContext.title}"` : initialContext.visTypeTitle,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const currentDocBreadcrumb: EuiBreadcrumb = { text: currentDocTitle };
|
||||
breadcrumbs.push(currentDocBreadcrumb);
|
||||
if (serverless?.setBreadcrumbs) {
|
||||
// TODO: https://github.com/elastic/kibana/issues/163488
|
||||
// for now, serverless breadcrumbs only set the title,
|
||||
// the rest of the breadcrumbs are handled by the serverless navigation
|
||||
// the serverless navigation is not yet aware of the byValue/originatingApp context
|
||||
serverless.setBreadcrumbs(currentDocBreadcrumb);
|
||||
} else {
|
||||
chrome.setBreadcrumbs(breadcrumbs);
|
||||
}
|
||||
const currentDocTitle = getCurrentTitle(persistedDoc, isByValueMode, initialContext);
|
||||
setBreadcrumbsTitle(
|
||||
{ application, chrome, serverless },
|
||||
{
|
||||
isByValueMode,
|
||||
currentDocTitle,
|
||||
redirectToOrigin,
|
||||
isFromLegacyEditor: Boolean(isLinkedToOriginatingApp || legacyEditorAppName),
|
||||
originatingAppName: getOriginatingAppName(),
|
||||
}
|
||||
);
|
||||
}, [
|
||||
getOriginatingAppName,
|
||||
redirectToOrigin,
|
||||
|
@ -303,8 +279,10 @@ export function App({
|
|||
chrome,
|
||||
isLinkedToOriginatingApp,
|
||||
persistedDoc,
|
||||
initialContext,
|
||||
isFromLegacyEditorEmbeddable,
|
||||
legacyEditorAppName,
|
||||
serverless,
|
||||
initialContext,
|
||||
]);
|
||||
|
||||
const switchDatasource = useCallback(() => {
|
||||
|
@ -314,12 +292,13 @@ export function App({
|
|||
}, []);
|
||||
|
||||
const runSave = useCallback(
|
||||
(saveProps: SaveProps, options: { saveToLibrary: boolean }) => {
|
||||
async (saveProps: SaveProps, options: { saveToLibrary: boolean }) => {
|
||||
dispatch(applyChanges());
|
||||
const prevVisState =
|
||||
persistedDoc?.visualizationType === visualization.activeId
|
||||
? persistedDoc?.state.visualization
|
||||
: undefined;
|
||||
|
||||
const telemetryEvents = activeVisualization?.getTelemetryEventsOnSave?.(
|
||||
visualization.state,
|
||||
prevVisState
|
||||
|
@ -327,36 +306,33 @@ export function App({
|
|||
if (telemetryEvents && telemetryEvents.length) {
|
||||
trackSaveUiCounterEvents(telemetryEvents);
|
||||
}
|
||||
return runSaveLensVisualization(
|
||||
{
|
||||
lastKnownDoc,
|
||||
getIsByValueMode,
|
||||
savedObjectsTagging,
|
||||
initialInput,
|
||||
redirectToOrigin,
|
||||
persistedDoc,
|
||||
onAppLeave,
|
||||
redirectTo,
|
||||
switchDatasource,
|
||||
originatingApp: incomingState?.originatingApp,
|
||||
textBasedLanguageSave: shouldCloseAndSaveTextBasedQuery,
|
||||
...lensAppServices,
|
||||
},
|
||||
saveProps,
|
||||
options
|
||||
).then(
|
||||
(newState) => {
|
||||
if (newState) {
|
||||
dispatchSetState(newState);
|
||||
setIsSaveModalVisible(false);
|
||||
setShouldCloseAndSaveTextBasedQuery(false);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
// error is handled inside the modal
|
||||
// so ignoring it here
|
||||
try {
|
||||
const newState = await runSaveLensVisualization(
|
||||
{
|
||||
lastKnownDoc,
|
||||
savedObjectsTagging,
|
||||
initialInput,
|
||||
redirectToOrigin,
|
||||
persistedDoc,
|
||||
onAppLeave,
|
||||
redirectTo,
|
||||
switchDatasource,
|
||||
originatingApp: incomingState?.originatingApp,
|
||||
textBasedLanguageSave: shouldCloseAndSaveTextBasedQuery,
|
||||
...lensAppServices,
|
||||
},
|
||||
saveProps,
|
||||
options
|
||||
);
|
||||
if (newState) {
|
||||
dispatchSetState(newState);
|
||||
setIsSaveModalVisible(false);
|
||||
setShouldCloseAndSaveTextBasedQuery(false);
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
// error is handled inside the modal
|
||||
// so ignoring it here
|
||||
}
|
||||
},
|
||||
[
|
||||
visualization.activeId,
|
||||
|
@ -364,7 +340,6 @@ export function App({
|
|||
activeVisualization,
|
||||
dispatch,
|
||||
lastKnownDoc,
|
||||
getIsByValueMode,
|
||||
savedObjectsTagging,
|
||||
initialInput,
|
||||
redirectToOrigin,
|
||||
|
@ -386,67 +361,20 @@ export function App({
|
|||
}
|
||||
}, [lastKnownDoc, initialDocFromContext]);
|
||||
|
||||
// if users comes to Lens from the Viz editor, they should have the option to navigate back
|
||||
const goBackToOriginatingApp = useCallback(() => {
|
||||
if (
|
||||
initialContext &&
|
||||
'vizEditorOriginatingAppUrl' in initialContext &&
|
||||
initialContext.vizEditorOriginatingAppUrl
|
||||
) {
|
||||
const [initialDocFromContextUnchanged, currentDocHasBeenSavedInLens] = [
|
||||
initialDocFromContext,
|
||||
persistedDoc,
|
||||
].map((refDoc) =>
|
||||
isLensEqual(
|
||||
refDoc,
|
||||
lastKnownDoc,
|
||||
data.query.filterManager.inject,
|
||||
datasourceMap,
|
||||
visualizationMap,
|
||||
annotationGroups
|
||||
)
|
||||
);
|
||||
if (initialDocFromContextUnchanged || currentDocHasBeenSavedInLens) {
|
||||
onAppLeave((actions) => {
|
||||
return actions.default();
|
||||
});
|
||||
application.navigateToApp('visualize', { path: initialContext.vizEditorOriginatingAppUrl });
|
||||
} else {
|
||||
setIsGoBackToVizEditorModalVisible(true);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
annotationGroups,
|
||||
const {
|
||||
shouldShowGoBackToVizEditorModal,
|
||||
goBackToOriginatingApp,
|
||||
navigateToVizEditor,
|
||||
closeGoBackToVizEditorModal,
|
||||
} = useNavigateBackToApp({
|
||||
application,
|
||||
data.query.filterManager.inject,
|
||||
datasourceMap,
|
||||
initialContext,
|
||||
initialDocFromContext,
|
||||
lastKnownDoc,
|
||||
onAppLeave,
|
||||
legacyEditorAppName,
|
||||
legacyEditorAppUrl,
|
||||
initialDocFromContext,
|
||||
persistedDoc,
|
||||
visualizationMap,
|
||||
]);
|
||||
|
||||
const navigateToVizEditor = useCallback(() => {
|
||||
setIsGoBackToVizEditorModalVisible(false);
|
||||
if (
|
||||
initialContext &&
|
||||
'vizEditorOriginatingAppUrl' in initialContext &&
|
||||
initialContext.vizEditorOriginatingAppUrl
|
||||
) {
|
||||
onAppLeave((actions) => {
|
||||
return actions.default();
|
||||
});
|
||||
application.navigateToApp('visualize', { path: initialContext.vizEditorOriginatingAppUrl });
|
||||
}
|
||||
}, [application, initialContext, onAppLeave]);
|
||||
|
||||
const initialContextIsEmbedded = useMemo(() => {
|
||||
return Boolean(
|
||||
initialContext && 'originatingApp' in initialContext && initialContext.originatingApp
|
||||
);
|
||||
}, [initialContext]);
|
||||
isLensEqual: isLensEqualWrapper,
|
||||
});
|
||||
|
||||
const indexPatternService = useMemo(
|
||||
() =>
|
||||
|
@ -471,35 +399,12 @@ export function App({
|
|||
[dataViews, uiActions, http, notifications, uiSettings, initialContext, dispatch]
|
||||
);
|
||||
|
||||
// remember latest URL based on the configuration
|
||||
// url_panel_content has a similar logic
|
||||
const shareURLCache = useRef({ params: '', url: '' });
|
||||
|
||||
const shortUrlService = useCallback(
|
||||
async (params: LensAppLocatorParams) => {
|
||||
const cacheKey = JSON.stringify(params);
|
||||
if (shareURLCache.current.params === cacheKey) {
|
||||
return shareURLCache.current.url;
|
||||
}
|
||||
if (locator && shortUrls) {
|
||||
// This is a stripped down version of what the share URL plugin is doing
|
||||
const shortUrl = await shortUrls.createWithLocator({ locator, params });
|
||||
const absoluteShortUrl = await shortUrl.locator.getUrl(shortUrl.params, { absolute: true });
|
||||
shareURLCache.current = { params: cacheKey, url: absoluteShortUrl };
|
||||
return absoluteShortUrl;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
[locator, shortUrls]
|
||||
);
|
||||
const shortUrlService = useShortUrlService(locator, share);
|
||||
|
||||
const isManaged = useLensSelector(selectIsManaged);
|
||||
|
||||
const returnToOriginSwitchLabelForContext =
|
||||
initialContext &&
|
||||
'isEmbeddable' in initialContext &&
|
||||
initialContext.isEmbeddable &&
|
||||
!persistedDoc
|
||||
isFromLegacyEditorEmbeddable && !persistedDoc
|
||||
? i18n.translate('xpack.lens.app.replacePanel', {
|
||||
defaultMessage: 'Replace panel on {originatingApp}',
|
||||
values: {
|
||||
|
@ -547,16 +452,7 @@ export function App({
|
|||
title={persistedDoc?.title}
|
||||
lensInspector={lensInspector}
|
||||
currentDoc={currentDoc}
|
||||
isCurrentStateDirty={
|
||||
!isLensEqual(
|
||||
persistedDoc,
|
||||
lastKnownDoc,
|
||||
data.query.filterManager.inject.bind(data.query.filterManager),
|
||||
datasourceMap,
|
||||
visualizationMap,
|
||||
annotationGroups
|
||||
)
|
||||
}
|
||||
isCurrentStateDirty={!isLensEqualWrapper(persistedDoc)}
|
||||
goBackToOriginatingApp={goBackToOriginatingApp}
|
||||
contextOriginatingApp={contextOriginatingApp}
|
||||
initialContextIsEmbedded={initialContextIsEmbedded}
|
||||
|
@ -612,13 +508,13 @@ export function App({
|
|||
}
|
||||
/>
|
||||
)}
|
||||
{isGoBackToVizEditorModalVisible && (
|
||||
{shouldShowGoBackToVizEditorModal && (
|
||||
<EuiConfirmModal
|
||||
maxWidth={600}
|
||||
title={i18n.translate('xpack.lens.app.unsavedWorkTitle', {
|
||||
defaultMessage: 'Unsaved changes',
|
||||
})}
|
||||
onCancel={() => setIsGoBackToVizEditorModalVisible(false)}
|
||||
onCancel={closeGoBackToVizEditorModal}
|
||||
onConfirm={navigateToVizEditor}
|
||||
cancelButtonText={i18n.translate('xpack.lens.app.goBackModalCancelBtn', {
|
||||
defaultMessage: 'Cancel',
|
||||
|
|
76
x-pack/plugins/lens/public/app_plugin/app_helpers.test.ts
Normal file
76
x-pack/plugins/lens/public/app_plugin/app_helpers.test.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import faker from 'faker';
|
||||
import { UseNavigateBackToAppProps, useNavigateBackToApp } from './app_helpers';
|
||||
import { defaultDoc, makeDefaultServices } from '../mocks/services_mock';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { LensDocument } from '../persistence';
|
||||
|
||||
function getLensDocumentMock(someProps?: Partial<LensDocument>) {
|
||||
return cloneDeep({ ...defaultDoc, ...someProps });
|
||||
}
|
||||
|
||||
const getApplicationMock = () => makeDefaultServices().application;
|
||||
|
||||
describe('App helpers', () => {
|
||||
function getDefaultProps(
|
||||
someProps?: Partial<UseNavigateBackToAppProps>
|
||||
): UseNavigateBackToAppProps {
|
||||
return {
|
||||
application: getApplicationMock(),
|
||||
onAppLeave: jest.fn(),
|
||||
legacyEditorAppName: faker.lorem.word(),
|
||||
legacyEditorAppUrl: faker.internet.url(),
|
||||
isLensEqual: jest.fn(() => true),
|
||||
initialDocFromContext: undefined,
|
||||
persistedDoc: getLensDocumentMock(),
|
||||
...someProps,
|
||||
};
|
||||
}
|
||||
describe('useNavigateBackToApp', () => {
|
||||
it('navigates back to originating app if documents has not changed', () => {
|
||||
const props = getDefaultProps();
|
||||
const { result } = renderHook(() => useNavigateBackToApp(props));
|
||||
|
||||
act(() => {
|
||||
result.current.goBackToOriginatingApp();
|
||||
});
|
||||
|
||||
expect(props.application.navigateToApp).toHaveBeenCalledWith(props.legacyEditorAppName, {
|
||||
path: props.legacyEditorAppUrl,
|
||||
});
|
||||
});
|
||||
|
||||
it('shows modal if documents are not equal', () => {
|
||||
const props = getDefaultProps({ isLensEqual: jest.fn().mockReturnValue(false) });
|
||||
const { result } = renderHook(() => useNavigateBackToApp(props));
|
||||
|
||||
act(() => {
|
||||
result.current.goBackToOriginatingApp();
|
||||
});
|
||||
|
||||
expect(props.application.navigateToApp).not.toHaveBeenCalled();
|
||||
expect(result.current.shouldShowGoBackToVizEditorModal).toBe(true);
|
||||
});
|
||||
|
||||
it('navigateToVizEditor hides modal and navigates back to Viz editor', () => {
|
||||
const props = getDefaultProps();
|
||||
const { result } = renderHook(() => useNavigateBackToApp(props));
|
||||
|
||||
act(() => {
|
||||
result.current.navigateToVizEditor();
|
||||
});
|
||||
|
||||
expect(result.current.shouldShowGoBackToVizEditorModal).toBe(false);
|
||||
expect(props.application.navigateToApp).toHaveBeenCalledWith(props.legacyEditorAppName, {
|
||||
path: props.legacyEditorAppUrl,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
207
x-pack/plugins/lens/public/app_plugin/app_helpers.ts
Normal file
207
x-pack/plugins/lens/public/app_plugin/app_helpers.ts
Normal file
|
@ -0,0 +1,207 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public';
|
||||
import { EuiBreadcrumb } from '@elastic/eui';
|
||||
import { AppLeaveHandler, ApplicationStart } from '@kbn/core-application-browser';
|
||||
import { ChromeStart } from '@kbn/core-chrome-browser';
|
||||
import { ServerlessPluginStart } from '@kbn/serverless/public';
|
||||
import { useRef, useCallback, useMemo, useState } from 'react';
|
||||
import { SharePublicStart } from '@kbn/share-plugin/public/plugin';
|
||||
import { LensAppLocator, LensAppLocatorParams } from '../../common/locator/locator';
|
||||
import { VisualizeEditorContext } from '../types';
|
||||
import { LensDocument } from '../persistence';
|
||||
import { RedirectToOriginProps } from './types';
|
||||
|
||||
const VISUALIZE_APP_ID = 'visualize';
|
||||
|
||||
export function isLegacyEditorEmbeddable(
|
||||
initialContext: VisualizeEditorContext | VisualizeFieldContext | undefined
|
||||
): initialContext is VisualizeEditorContext {
|
||||
return Boolean(initialContext && 'isEmbeddable' in initialContext && initialContext.isEmbeddable);
|
||||
}
|
||||
|
||||
export function getCurrentTitle(
|
||||
persistedDoc: LensDocument | undefined,
|
||||
isByValueMode: boolean,
|
||||
initialContext: VisualizeEditorContext | VisualizeFieldContext | undefined
|
||||
) {
|
||||
if (persistedDoc) {
|
||||
if (isByValueMode) {
|
||||
return i18n.translate('xpack.lens.breadcrumbsByValue', {
|
||||
defaultMessage: 'Edit visualization',
|
||||
});
|
||||
}
|
||||
if (persistedDoc.title) {
|
||||
return persistedDoc.title;
|
||||
}
|
||||
}
|
||||
if (!persistedDoc?.title && isLegacyEditorEmbeddable(initialContext)) {
|
||||
return i18n.translate('xpack.lens.breadcrumbsEditInLensFromDashboard', {
|
||||
defaultMessage: 'Converting {title} visualization',
|
||||
values: {
|
||||
title: initialContext.title ? `"${initialContext.title}"` : initialContext.visTypeTitle,
|
||||
},
|
||||
});
|
||||
}
|
||||
return i18n.translate('xpack.lens.breadcrumbsCreate', {
|
||||
defaultMessage: 'Create',
|
||||
});
|
||||
}
|
||||
|
||||
export function setBreadcrumbsTitle(
|
||||
{
|
||||
application,
|
||||
serverless,
|
||||
chrome,
|
||||
}: {
|
||||
application: ApplicationStart;
|
||||
serverless: ServerlessPluginStart | undefined;
|
||||
chrome: ChromeStart;
|
||||
},
|
||||
{
|
||||
isByValueMode,
|
||||
originatingAppName,
|
||||
redirectToOrigin,
|
||||
isFromLegacyEditor,
|
||||
currentDocTitle,
|
||||
}: {
|
||||
isByValueMode: boolean;
|
||||
originatingAppName: string | undefined;
|
||||
redirectToOrigin: ((props?: RedirectToOriginProps | undefined) => void) | undefined;
|
||||
isFromLegacyEditor: boolean;
|
||||
currentDocTitle: string;
|
||||
}
|
||||
) {
|
||||
const breadcrumbs: EuiBreadcrumb[] = [];
|
||||
if (isFromLegacyEditor && originatingAppName && redirectToOrigin) {
|
||||
breadcrumbs.push({
|
||||
onClick: () => {
|
||||
redirectToOrigin();
|
||||
},
|
||||
text: originatingAppName,
|
||||
});
|
||||
}
|
||||
if (!isByValueMode) {
|
||||
breadcrumbs.push({
|
||||
href: application.getUrlForApp(VISUALIZE_APP_ID),
|
||||
onClick: (e) => {
|
||||
application.navigateToApp(VISUALIZE_APP_ID, { path: '/' });
|
||||
e.preventDefault();
|
||||
},
|
||||
text: i18n.translate('xpack.lens.breadcrumbsTitle', {
|
||||
defaultMessage: 'Visualize Library',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const currentDocBreadcrumb: EuiBreadcrumb = { text: currentDocTitle };
|
||||
breadcrumbs.push(currentDocBreadcrumb);
|
||||
if (serverless?.setBreadcrumbs) {
|
||||
// TODO: https://github.com/elastic/kibana/issues/163488
|
||||
// for now, serverless breadcrumbs only set the title,
|
||||
// the rest of the breadcrumbs are handled by the serverless navigation
|
||||
// the serverless navigation is not yet aware of the byValue/originatingApp context
|
||||
serverless.setBreadcrumbs(currentDocBreadcrumb);
|
||||
} else {
|
||||
chrome.setBreadcrumbs(breadcrumbs);
|
||||
}
|
||||
}
|
||||
|
||||
export function useShortUrlService(
|
||||
locator: LensAppLocator | undefined,
|
||||
share: SharePublicStart | undefined
|
||||
) {
|
||||
const shortUrls = useMemo(() => share?.url.shortUrls.get(null), [share]);
|
||||
// remember latest URL based on the configuration
|
||||
// url_panel_content has a similar logic
|
||||
const shareURLCache = useRef({ params: '', url: '' });
|
||||
|
||||
return useCallback(
|
||||
async (params: LensAppLocatorParams) => {
|
||||
const cacheKey = JSON.stringify(params);
|
||||
if (shareURLCache.current.params === cacheKey) {
|
||||
return shareURLCache.current.url;
|
||||
}
|
||||
if (locator && shortUrls) {
|
||||
// This is a stripped down version of what the share URL plugin is doing
|
||||
const shortUrl = await shortUrls.createWithLocator({ locator, params });
|
||||
const absoluteShortUrl = await shortUrl.locator.getUrl(shortUrl.params, { absolute: true });
|
||||
shareURLCache.current = { params: cacheKey, url: absoluteShortUrl };
|
||||
return absoluteShortUrl;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
[locator, shortUrls]
|
||||
);
|
||||
}
|
||||
|
||||
export interface UseNavigateBackToAppProps {
|
||||
application: ApplicationStart;
|
||||
onAppLeave: (handler: AppLeaveHandler) => void;
|
||||
legacyEditorAppName: string | undefined;
|
||||
legacyEditorAppUrl: string | undefined;
|
||||
initialDocFromContext: LensDocument | undefined;
|
||||
persistedDoc: LensDocument | undefined;
|
||||
isLensEqual: (refDoc: LensDocument | undefined) => boolean;
|
||||
}
|
||||
|
||||
export function useNavigateBackToApp({
|
||||
application,
|
||||
onAppLeave,
|
||||
legacyEditorAppName,
|
||||
legacyEditorAppUrl,
|
||||
initialDocFromContext,
|
||||
persistedDoc,
|
||||
isLensEqual,
|
||||
}: UseNavigateBackToAppProps) {
|
||||
const [shouldShowGoBackToVizEditorModal, setIsGoBackToVizEditorModalVisible] = useState(false);
|
||||
/** Shared logic to navigate back to the originating viz editor app */
|
||||
const navigateBackToVizEditor = useCallback(() => {
|
||||
if (legacyEditorAppUrl) {
|
||||
onAppLeave((actions) => {
|
||||
return actions.default();
|
||||
});
|
||||
application.navigateToApp(legacyEditorAppName || VISUALIZE_APP_ID, {
|
||||
path: legacyEditorAppUrl,
|
||||
});
|
||||
}
|
||||
}, [application, legacyEditorAppName, legacyEditorAppUrl, onAppLeave]);
|
||||
|
||||
// if users comes to Lens from the Viz editor, they should have the option to navigate back
|
||||
// used for TopNavMenu
|
||||
const goBackToOriginatingApp = useCallback(() => {
|
||||
if (legacyEditorAppUrl) {
|
||||
if ([initialDocFromContext, persistedDoc].some(isLensEqual)) {
|
||||
navigateBackToVizEditor();
|
||||
} else {
|
||||
setIsGoBackToVizEditorModalVisible(true);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
legacyEditorAppUrl,
|
||||
initialDocFromContext,
|
||||
persistedDoc,
|
||||
isLensEqual,
|
||||
navigateBackToVizEditor,
|
||||
setIsGoBackToVizEditorModalVisible,
|
||||
]);
|
||||
|
||||
// Used for Saving Modal
|
||||
const navigateToVizEditor = useCallback(() => {
|
||||
setIsGoBackToVizEditorModalVisible(false);
|
||||
navigateBackToVizEditor();
|
||||
}, [navigateBackToVizEditor, setIsGoBackToVizEditorModalVisible]);
|
||||
|
||||
return {
|
||||
shouldShowGoBackToVizEditorModal,
|
||||
goBackToOriginatingApp,
|
||||
navigateToVizEditor,
|
||||
closeGoBackToVizEditorModal: () => setIsGoBackToVizEditorModalVisible(false),
|
||||
};
|
||||
}
|
|
@ -15,9 +15,12 @@ import {
|
|||
UserMessageGetterProps,
|
||||
filterAndSortUserMessages,
|
||||
getApplicationUserMessages,
|
||||
handleMessageOverwriteFromConsumer,
|
||||
} from './get_application_user_messages';
|
||||
import { cleanup, render, screen } from '@testing-library/react';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { FIELD_NOT_FOUND, FIELD_WRONG_TYPE } from '../user_messages_ids';
|
||||
import { LensPublicCallbacks } from '../react_embeddable/types';
|
||||
import { getLongMessage } from '../user_messages_utils';
|
||||
|
||||
jest.mock('@kbn/shared-ux-link-redirect-app', () => {
|
||||
|
@ -388,4 +391,100 @@ describe('filtering user messages', () => {
|
|||
]
|
||||
`);
|
||||
});
|
||||
|
||||
describe('override messages with custom callback', () => {
|
||||
it('should override embeddableBadge message', async () => {
|
||||
const getBadgeMessage = jest.fn(
|
||||
(): ReturnType<NonNullable<LensPublicCallbacks['onBeforeBadgesRender']>> => [
|
||||
{
|
||||
uniqueId: FIELD_NOT_FOUND,
|
||||
severity: 'warning',
|
||||
fixableInEditor: true,
|
||||
displayLocations: [
|
||||
{ id: 'embeddableBadge' },
|
||||
{ id: 'dimensionButton', dimensionId: '1' },
|
||||
],
|
||||
longMessage: 'custom',
|
||||
shortMessage: '',
|
||||
hidePopoverIcon: true,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
expect(
|
||||
handleMessageOverwriteFromConsumer(
|
||||
[
|
||||
{
|
||||
uniqueId: FIELD_NOT_FOUND,
|
||||
severity: 'error',
|
||||
fixableInEditor: true,
|
||||
displayLocations: [
|
||||
{ id: 'embeddableBadge' },
|
||||
{ id: 'dimensionButton', dimensionId: '1' },
|
||||
],
|
||||
longMessage: 'original',
|
||||
shortMessage: '',
|
||||
},
|
||||
{
|
||||
uniqueId: FIELD_WRONG_TYPE,
|
||||
severity: 'error',
|
||||
fixableInEditor: true,
|
||||
displayLocations: [{ id: 'visualization' }],
|
||||
longMessage: 'original',
|
||||
shortMessage: '',
|
||||
},
|
||||
],
|
||||
getBadgeMessage
|
||||
)
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
uniqueId: FIELD_WRONG_TYPE,
|
||||
severity: 'error',
|
||||
fixableInEditor: true,
|
||||
displayLocations: [{ id: 'visualization' }],
|
||||
longMessage: 'original',
|
||||
shortMessage: '',
|
||||
},
|
||||
{
|
||||
uniqueId: FIELD_NOT_FOUND,
|
||||
severity: 'warning',
|
||||
fixableInEditor: true,
|
||||
displayLocations: [
|
||||
{ id: 'embeddableBadge' },
|
||||
{ id: 'dimensionButton', dimensionId: '1' },
|
||||
],
|
||||
longMessage: 'custom',
|
||||
shortMessage: '',
|
||||
hidePopoverIcon: true,
|
||||
},
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('should not override embeddableBadge message if callback is not provided', async () => {
|
||||
const messages: UserMessage[] = [
|
||||
{
|
||||
uniqueId: FIELD_NOT_FOUND,
|
||||
severity: 'error',
|
||||
fixableInEditor: true,
|
||||
displayLocations: [
|
||||
{ id: 'embeddableBadge' },
|
||||
{ id: 'dimensionButton', dimensionId: '1' },
|
||||
],
|
||||
longMessage: 'original',
|
||||
shortMessage: '',
|
||||
},
|
||||
{
|
||||
uniqueId: FIELD_WRONG_TYPE,
|
||||
severity: 'error',
|
||||
fixableInEditor: true,
|
||||
displayLocations: [{ id: 'visualization' }],
|
||||
longMessage: 'original',
|
||||
shortMessage: '',
|
||||
},
|
||||
];
|
||||
expect(handleMessageOverwriteFromConsumer(messages)).toEqual(messages);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,6 +11,7 @@ import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import { Dispatch } from '@reduxjs/toolkit';
|
||||
import { partition } from 'lodash';
|
||||
import {
|
||||
updateDatasourceState,
|
||||
type DataViewsState,
|
||||
|
@ -35,6 +36,8 @@ import {
|
|||
EDITOR_UNKNOWN_DATASOURCE_TYPE,
|
||||
EDITOR_UNKNOWN_VIS_TYPE,
|
||||
} from '../user_messages_ids';
|
||||
import { nonNullable } from '../utils';
|
||||
import type { LensPublicCallbacks } from '../react_embeddable/types';
|
||||
|
||||
export interface UserMessageGetterProps {
|
||||
visualizationType: string | null | undefined;
|
||||
|
@ -203,21 +206,38 @@ function getMissingIndexPatternsErrors(
|
|||
];
|
||||
}
|
||||
|
||||
export const handleMessageOverwriteFromConsumer = (
|
||||
messages: UserMessage[],
|
||||
onBeforeBadgesRender?: LensPublicCallbacks['onBeforeBadgesRender']
|
||||
) => {
|
||||
if (onBeforeBadgesRender) {
|
||||
// we need something else to better identify those errors
|
||||
const [messagesToHandle, originalMessages] = partition(messages, (message) =>
|
||||
message.displayLocations.some((location) => location.id === 'embeddableBadge')
|
||||
);
|
||||
|
||||
if (messagesToHandle.length > 0) {
|
||||
const customBadgeMessages = onBeforeBadgesRender(messagesToHandle);
|
||||
return originalMessages.concat(customBadgeMessages);
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
};
|
||||
|
||||
export const filterAndSortUserMessages = (
|
||||
userMessages: UserMessage[],
|
||||
locationId?: UserMessagesDisplayLocationId | UserMessagesDisplayLocationId[],
|
||||
{ dimensionId, severity }: UserMessageFilters = {}
|
||||
) => {
|
||||
const locationIds = Array.isArray(locationId)
|
||||
? locationId
|
||||
: typeof locationId === 'string'
|
||||
? [locationId]
|
||||
: [];
|
||||
const locationIds = new Set(
|
||||
(Array.isArray(locationId) ? locationId : [locationId]).filter(nonNullable)
|
||||
);
|
||||
|
||||
const filteredMessages = userMessages.filter((message) => {
|
||||
if (locationIds.length) {
|
||||
if (locationIds.size) {
|
||||
const hasMatch = message.displayLocations.some((location) => {
|
||||
if (!locationIds.includes(location.id)) {
|
||||
if (!locationIds.has(location.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -229,11 +249,7 @@ export const filterAndSortUserMessages = (
|
|||
}
|
||||
}
|
||||
|
||||
if (severity && message.severity !== severity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return !severity || message.severity === severity;
|
||||
});
|
||||
|
||||
return filteredMessages.sort(bySeverity);
|
||||
|
@ -329,7 +345,7 @@ export const useApplicationUserMessages = ({
|
|||
|
||||
const getUserMessages: UserMessagesGetter = (locationId, filterArgs) =>
|
||||
filterAndSortUserMessages(
|
||||
[...userMessages, ...Object.values(additionalUserMessages)],
|
||||
userMessages.concat(Object.values(additionalUserMessages)),
|
||||
locationId,
|
||||
filterArgs ?? {}
|
||||
);
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { Filter, FilterStateStore } from '@kbn/es-query';
|
||||
import { isLensEqual } from './lens_document_equality';
|
||||
import { Document } from '../persistence/saved_object_store';
|
||||
import { LensDocument } from '../persistence/saved_object_store';
|
||||
import {
|
||||
AnnotationGroups,
|
||||
Datasource,
|
||||
|
@ -18,7 +18,7 @@ import {
|
|||
|
||||
const visualizationType = 'lnsSomeVis';
|
||||
|
||||
const defaultDoc: Document = {
|
||||
const defaultDoc: LensDocument = {
|
||||
title: 'some-title',
|
||||
visualizationType,
|
||||
state: {
|
||||
|
@ -105,7 +105,7 @@ describe('lens document equality', () => {
|
|||
expect(
|
||||
isLensEqual(
|
||||
undefined,
|
||||
{} as Document,
|
||||
{} as LensDocument,
|
||||
mockInjectFilterReferences,
|
||||
{},
|
||||
mockVisualizationMap,
|
||||
|
@ -114,7 +114,7 @@ describe('lens document equality', () => {
|
|||
).toBeFalsy();
|
||||
expect(
|
||||
isLensEqual(
|
||||
{} as Document,
|
||||
{} as LensDocument,
|
||||
undefined,
|
||||
mockInjectFilterReferences,
|
||||
{},
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { isEqual, intersection, union } from 'lodash';
|
||||
import { FilterManager } from '@kbn/data-plugin/public';
|
||||
import { Document } from '../persistence/saved_object_store';
|
||||
import { LensDocument } from '../persistence/saved_object_store';
|
||||
import { AnnotationGroups, DatasourceMap, VisualizationMap } from '../types';
|
||||
import { removePinnedFilters } from './save_modal_container';
|
||||
|
||||
|
@ -15,8 +15,8 @@ const removeNonSerializable = (obj: Parameters<JSON['stringify']>[0]) =>
|
|||
JSON.parse(JSON.stringify(obj));
|
||||
|
||||
export const isLensEqual = (
|
||||
doc1In: Document | undefined,
|
||||
doc2In: Document | undefined,
|
||||
doc1In: LensDocument | undefined,
|
||||
doc2In: LensDocument | undefined,
|
||||
injectFilterReferences: FilterManager['inject'],
|
||||
datasourceMap: DatasourceMap,
|
||||
visualizationMap: VisualizationMap,
|
||||
|
@ -54,6 +54,7 @@ export const isLensEqual = (
|
|||
}
|
||||
})()
|
||||
: isEqual(doc1.state.visualization, doc2.state.visualization);
|
||||
|
||||
if (!visualizationStateIsEqual) {
|
||||
return false;
|
||||
}
|
||||
|
@ -68,16 +69,14 @@ export const isLensEqual = (
|
|||
|
||||
if (datasourcesEqual) {
|
||||
// equal so far, so actually check
|
||||
datasourcesEqual = availableDatasourceTypes1
|
||||
.map((type) =>
|
||||
datasourceMap[type].isEqual(
|
||||
doc1.state.datasourceStates[type],
|
||||
[...doc1.references, ...(doc1.state.internalReferences || [])],
|
||||
doc2.state.datasourceStates[type],
|
||||
[...doc2.references, ...(doc2.state.internalReferences || [])]
|
||||
)
|
||||
datasourcesEqual = availableDatasourceTypes1.every((type) =>
|
||||
datasourceMap[type].isEqual(
|
||||
doc1.state.datasourceStates[type],
|
||||
doc1.references.concat(doc1.state.internalReferences || []),
|
||||
doc2.state.datasourceStates[type],
|
||||
doc2.references.concat(doc2.state.internalReferences || [])
|
||||
)
|
||||
.every((res) => res);
|
||||
);
|
||||
}
|
||||
|
||||
if (!datasourcesEqual) {
|
||||
|
@ -96,7 +95,7 @@ export const isLensEqual = (
|
|||
|
||||
function injectDocFilterReferences(
|
||||
injectFilterReferences: FilterManager['inject'],
|
||||
doc?: Document
|
||||
doc?: LensDocument
|
||||
) {
|
||||
if (!doc) return undefined;
|
||||
return {
|
||||
|
|
|
@ -37,7 +37,6 @@ import {
|
|||
} from '../utils';
|
||||
import { combineQueryAndFilters, getLayerMetaInfo } from './show_underlying_data';
|
||||
import { changeIndexPattern } from '../state_management/lens_slice';
|
||||
import { LensByReferenceInput } from '../embeddable';
|
||||
import { DEFAULT_LENS_LAYOUT_DIMENSIONS, getShareURL } from './share_action';
|
||||
import { getDatasourceLayers } from '../state_management/utils';
|
||||
|
||||
|
@ -291,7 +290,6 @@ export const LensTopNavMenu = ({
|
|||
navigation,
|
||||
uiSettings,
|
||||
application,
|
||||
attributeService,
|
||||
share,
|
||||
dataViewFieldEditor,
|
||||
dataViewEditor,
|
||||
|
@ -529,11 +527,9 @@ export const LensTopNavMenu = ({
|
|||
|
||||
const topNavConfig = useMemo(() => {
|
||||
const showReplaceInDashboard =
|
||||
initialContext?.originatingApp === 'dashboards' &&
|
||||
!(initialInput as LensByReferenceInput)?.savedObjectId;
|
||||
initialContext?.originatingApp === 'dashboards' && !initialInput?.savedObjectId;
|
||||
const showReplaceInCanvas =
|
||||
initialContext?.originatingApp === 'canvas' &&
|
||||
!(initialInput as LensByReferenceInput)?.savedObjectId;
|
||||
initialContext?.originatingApp === 'canvas' && !initialInput?.savedObjectId;
|
||||
const contextFromEmbeddable =
|
||||
initialContext && 'isEmbeddable' in initialContext && initialContext.isEmbeddable;
|
||||
|
||||
|
@ -690,8 +686,7 @@ export const LensTopNavMenu = ({
|
|||
panelTimeRange: contextFromEmbeddable ? initialContext.panelTimeRange : undefined,
|
||||
},
|
||||
{
|
||||
saveToLibrary:
|
||||
(initialInput && attributeService.inputIsRefType(initialInput)) ?? false,
|
||||
saveToLibrary: Boolean(initialInput?.savedObjectId),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -801,7 +796,6 @@ export const LensTopNavMenu = ({
|
|||
defaultLensTitle,
|
||||
onAppLeave,
|
||||
runSave,
|
||||
attributeService,
|
||||
setIsSaveModalVisible,
|
||||
goBackToOriginatingApp,
|
||||
redirectToOrigin,
|
||||
|
|
|
@ -33,12 +33,7 @@ import { EditorFrameStart, LensTopNavMenuEntryGenerator, VisualizeEditorContext
|
|||
import { addHelpMenuToAppChrome } from '../help_menu_util';
|
||||
import { LensPluginStartDependencies } from '../plugin';
|
||||
import { LENS_EMBEDDABLE_TYPE, LENS_EDIT_BY_VALUE, APP_ID } from '../../common/constants';
|
||||
import {
|
||||
LensEmbeddableInput,
|
||||
LensByReferenceInput,
|
||||
LensByValueInput,
|
||||
} from '../embeddable/embeddable';
|
||||
import { LensAttributeService } from '../lens_attribute_service';
|
||||
import { LensAttributesService } from '../lens_attribute_service';
|
||||
import { LensAppServices, RedirectToOriginProps, HistoryLocationState } from './types';
|
||||
import {
|
||||
makeConfigureStore,
|
||||
|
@ -55,6 +50,7 @@ import {
|
|||
MainHistoryLocationState,
|
||||
} from '../../common/locator/locator';
|
||||
import { SavedObjectIndexStore } from '../persistence';
|
||||
import { LensSerializedState } from '../react_embeddable/types';
|
||||
|
||||
function getInitialContext(history: AppMountParameters['history']) {
|
||||
const historyLocationState = history.location.state as
|
||||
|
@ -83,7 +79,7 @@ function getInitialContext(history: AppMountParameters['history']) {
|
|||
export async function getLensServices(
|
||||
coreStart: CoreStart,
|
||||
startDependencies: LensPluginStartDependencies,
|
||||
attributeService: LensAttributeService,
|
||||
attributeService: LensAttributesService,
|
||||
initialContext?: VisualizeFieldContext | VisualizeEditorContext,
|
||||
locator?: LensAppLocator
|
||||
): Promise<LensAppServices> {
|
||||
|
@ -146,7 +142,7 @@ export async function mountApp(
|
|||
params: AppMountParameters,
|
||||
mountProps: {
|
||||
createEditorFrame: EditorFrameStart['createInstance'];
|
||||
attributeService: LensAttributeService;
|
||||
attributeService: LensAttributesService;
|
||||
topNavMenuEntryGenerators: LensTopNavMenuEntryGenerator[];
|
||||
locator?: LensAppLocator;
|
||||
}
|
||||
|
@ -188,12 +184,12 @@ export async function mountApp(
|
|||
i18n.translate('xpack.lens.pageTitle', { defaultMessage: 'Lens' })
|
||||
);
|
||||
|
||||
const getInitialInput = (id?: string, editByValue?: boolean): LensEmbeddableInput | undefined => {
|
||||
const getInitialInput = (id?: string, editByValue?: boolean): LensSerializedState | undefined => {
|
||||
if (editByValue) {
|
||||
return embeddableEditorIncomingState?.valueInput as LensByValueInput;
|
||||
return embeddableEditorIncomingState?.valueInput as LensSerializedState;
|
||||
}
|
||||
if (id) {
|
||||
return { savedObjectId: id } as LensByReferenceInput;
|
||||
return { savedObjectId: id } as LensSerializedState;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -220,14 +216,14 @@ export async function mountApp(
|
|||
if (initialContext && 'embeddableId' in initialContext) {
|
||||
embeddableId = initialContext.embeddableId;
|
||||
}
|
||||
if (stateTransfer && props?.input) {
|
||||
const { input, isCopied } = props;
|
||||
if (stateTransfer && props?.state) {
|
||||
const { state, isCopied } = props;
|
||||
stateTransfer.navigateToWithEmbeddablePackage(mergedOriginatingApp, {
|
||||
path: embeddableEditorIncomingState?.originatingPath,
|
||||
state: {
|
||||
embeddableId: isCopied ? undefined : embeddableId,
|
||||
type: LENS_EMBEDDABLE_TYPE,
|
||||
input,
|
||||
input: { ...state, savedObject: state.savedObjectId },
|
||||
searchSessionId: data.search.session.getSessionId(),
|
||||
},
|
||||
});
|
||||
|
@ -426,7 +422,7 @@ export async function mountApp(
|
|||
return () => {
|
||||
data.search.session.clear();
|
||||
unmountComponentAtNode(params.element);
|
||||
lensServices.inspector.close();
|
||||
lensServices.inspector.closeInspector();
|
||||
unlistenParentHistory();
|
||||
lensStore.dispatch(navigateAway());
|
||||
stateTransfer.clearEditorState?.(APP_ID);
|
||||
|
|
|
@ -0,0 +1,407 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SaveProps } from './app';
|
||||
import { type SaveVisualizationProps, runSaveLensVisualization } from './save_modal_container';
|
||||
import { defaultDoc, makeDefaultServices } from '../mocks';
|
||||
import faker from 'faker';
|
||||
import { makeAttributeService } from '../mocks/services_mock';
|
||||
|
||||
jest.mock('../persistence/saved_objects_utils/check_for_duplicate_title', () => ({
|
||||
checkForDuplicateTitle: jest.fn(async () => false),
|
||||
}));
|
||||
|
||||
describe('runSaveLensVisualization', () => {
|
||||
// Need to call reset here as makeDefaultServices() reuses some mocks from core
|
||||
const resetMocks = () =>
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
function getDefaultArgs(
|
||||
servicesOverrides: Partial<SaveVisualizationProps> = {},
|
||||
{ saveToLibrary, ...propsOverrides }: Partial<SaveProps & { saveToLibrary: boolean }> = {}
|
||||
) {
|
||||
const redirectToOrigin = jest.fn();
|
||||
const redirectTo = jest.fn();
|
||||
const onAppLeave = jest.fn();
|
||||
const switchDatasource = jest.fn();
|
||||
const props: SaveVisualizationProps = {
|
||||
...makeDefaultServices(),
|
||||
// start with both the initial input and lastKnownDoc synced
|
||||
lastKnownDoc: defaultDoc,
|
||||
initialInput: { attributes: defaultDoc, savedObjectId: defaultDoc.savedObjectId },
|
||||
redirectToOrigin,
|
||||
redirectTo,
|
||||
onAppLeave,
|
||||
switchDatasource,
|
||||
...servicesOverrides,
|
||||
};
|
||||
const saveProps: SaveProps = {
|
||||
newTitle: faker.lorem.word(),
|
||||
newDescription: faker.lorem.sentence(),
|
||||
newTags: [faker.lorem.word(), faker.lorem.word()],
|
||||
isTitleDuplicateConfirmed: false,
|
||||
returnToOrigin: false,
|
||||
dashboardId: undefined,
|
||||
newCopyOnSave: false,
|
||||
...propsOverrides,
|
||||
};
|
||||
const options = {
|
||||
saveToLibrary: Boolean(saveToLibrary),
|
||||
};
|
||||
|
||||
return {
|
||||
props,
|
||||
saveProps,
|
||||
options,
|
||||
// convenience shortcuts
|
||||
/**
|
||||
* This function will be called when a fresh chart is saved
|
||||
* and in the modal the user chooses to add the chart into a specific dashboard. Make sure to pass the "dashboardId" prop as well to simulate this scenario.
|
||||
* This is used to test indirectly the redirectToDashboard call
|
||||
*/
|
||||
redirectToDashboardFn: props.stateTransfer.navigateToWithEmbeddablePackage,
|
||||
/**
|
||||
* This function will be called before reloading the editor after saving a a new document/new copy of the document
|
||||
*/
|
||||
cleanupEditor: props.stateTransfer.clearEditorState,
|
||||
saveToLibraryFn: props.attributeService.saveToLibrary,
|
||||
toasts: props.notifications.toasts,
|
||||
};
|
||||
}
|
||||
|
||||
describe('from dashboard', () => {
|
||||
describe('as by value', () => {
|
||||
const defaultByValueDoc = { ...defaultDoc, savedObjectId: undefined };
|
||||
|
||||
describe('Save and return', () => {
|
||||
resetMocks();
|
||||
|
||||
// Test the "Save and return" button
|
||||
it('should get back to dashboard', async () => {
|
||||
const { props, saveProps, options, redirectToDashboardFn, saveToLibraryFn } =
|
||||
getDefaultArgs(
|
||||
{
|
||||
lastKnownDoc: defaultByValueDoc,
|
||||
initialInput: { attributes: defaultByValueDoc },
|
||||
},
|
||||
{ returnToOrigin: true }
|
||||
);
|
||||
await runSaveLensVisualization(props, saveProps, options);
|
||||
|
||||
// callback called
|
||||
expect(props.onAppLeave).toHaveBeenCalled();
|
||||
expect(props.redirectToOrigin).toHaveBeenCalled();
|
||||
|
||||
// callback not called
|
||||
expect(redirectToDashboardFn).not.toHaveBeenCalled();
|
||||
expect(saveToLibraryFn).not.toHaveBeenCalled();
|
||||
expect(props.notifications.toasts.addSuccess).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should get back to dashboard preserving the original panel settings', async () => {
|
||||
const { props, saveProps, options } = getDefaultArgs(
|
||||
{
|
||||
lastKnownDoc: defaultByValueDoc,
|
||||
initialInput: {
|
||||
attributes: defaultByValueDoc,
|
||||
title: 'blah',
|
||||
timeRange: { from: 'now-7d', to: 'now' },
|
||||
},
|
||||
},
|
||||
{ returnToOrigin: true }
|
||||
);
|
||||
await runSaveLensVisualization(props, saveProps, options);
|
||||
|
||||
// callback called
|
||||
expect(props.onAppLeave).toHaveBeenCalled();
|
||||
expect(props.redirectToOrigin).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
state: expect.objectContaining({
|
||||
title: 'blah',
|
||||
timeRange: { from: 'now-7d', to: 'now' },
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Save to library', () => {
|
||||
resetMocks();
|
||||
|
||||
// Test the "Save to library" flow
|
||||
it('should save to library without redirect', async () => {
|
||||
const { props, saveProps, options, redirectToDashboardFn, saveToLibraryFn } =
|
||||
getDefaultArgs(
|
||||
{
|
||||
lastKnownDoc: defaultByValueDoc,
|
||||
initialInput: { attributes: defaultByValueDoc },
|
||||
},
|
||||
{
|
||||
saveToLibrary: true,
|
||||
// do not get back at dashboard once saved
|
||||
returnToOrigin: false,
|
||||
}
|
||||
);
|
||||
await runSaveLensVisualization(props, saveProps, options);
|
||||
|
||||
// callback called
|
||||
expect(saveToLibraryFn).toHaveBeenCalled();
|
||||
expect(props.notifications.toasts.addSuccess).toHaveBeenCalled();
|
||||
|
||||
// not called
|
||||
expect(props.onAppLeave).not.toHaveBeenCalled();
|
||||
expect(props.redirectToOrigin).not.toHaveBeenCalled();
|
||||
expect(redirectToDashboardFn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should save to library and redirect', async () => {
|
||||
const { props, saveProps, options, redirectToDashboardFn, saveToLibraryFn } =
|
||||
getDefaultArgs(
|
||||
{
|
||||
lastKnownDoc: defaultByValueDoc,
|
||||
initialInput: { attributes: defaultByValueDoc },
|
||||
},
|
||||
{
|
||||
saveToLibrary: true,
|
||||
// return to dashboard once saved
|
||||
returnToOrigin: true,
|
||||
}
|
||||
);
|
||||
await runSaveLensVisualization(props, saveProps, options);
|
||||
|
||||
// callback called
|
||||
expect(props.onAppLeave).toHaveBeenCalled();
|
||||
expect(props.redirectToOrigin).toHaveBeenCalled();
|
||||
expect(saveToLibraryFn).toHaveBeenCalled();
|
||||
|
||||
// not called
|
||||
expect(redirectToDashboardFn).not.toHaveBeenCalled();
|
||||
expect(props.notifications.toasts.addSuccess).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('as by reference', () => {
|
||||
resetMocks();
|
||||
// There are 4 possibilities here:
|
||||
// save the current document overwriting the existing one
|
||||
it('should overwrite and show a success toast', async () => {
|
||||
const { props, saveProps, options, redirectToDashboardFn, saveToLibraryFn, toasts } =
|
||||
getDefaultArgs(
|
||||
{
|
||||
// defaultDoc is by reference
|
||||
},
|
||||
{ newCopyOnSave: false, saveToLibrary: true }
|
||||
);
|
||||
await runSaveLensVisualization(props, saveProps, options);
|
||||
|
||||
// callback called
|
||||
expect(saveToLibraryFn).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
defaultDoc.savedObjectId
|
||||
);
|
||||
expect(toasts.addSuccess).toHaveBeenCalled();
|
||||
|
||||
// not called
|
||||
expect(props.onAppLeave).not.toHaveBeenCalled();
|
||||
expect(props.redirectToOrigin).not.toHaveBeenCalled();
|
||||
expect(redirectToDashboardFn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// save the current document as a new by-ref copy in the library
|
||||
it('should save as a new copy and show a success toast', async () => {
|
||||
const { props, saveProps, options, redirectToDashboardFn, saveToLibraryFn, toasts } =
|
||||
getDefaultArgs(
|
||||
{
|
||||
// defaultDoc is by reference
|
||||
},
|
||||
{ newCopyOnSave: true, saveToLibrary: true }
|
||||
);
|
||||
await runSaveLensVisualization(props, saveProps, options);
|
||||
|
||||
// callback called
|
||||
expect(saveToLibraryFn).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
undefined
|
||||
);
|
||||
expect(toasts.addSuccess).toHaveBeenCalled();
|
||||
|
||||
// not called
|
||||
expect(props.onAppLeave).not.toHaveBeenCalled();
|
||||
expect(props.redirectToOrigin).not.toHaveBeenCalled();
|
||||
expect(redirectToDashboardFn).not.toHaveBeenCalled();
|
||||
});
|
||||
// save the current document as a new by-value copy and add it to a dashboard
|
||||
it('should save as a new by-value copy and redirect to the dashboard', async () => {
|
||||
const dashboardId = faker.random.uuid();
|
||||
const { props, saveProps, options, redirectToDashboardFn, saveToLibraryFn, toasts } =
|
||||
getDefaultArgs(
|
||||
{
|
||||
// defaultDoc is by reference
|
||||
},
|
||||
{ newCopyOnSave: true, saveToLibrary: false, dashboardId }
|
||||
);
|
||||
await runSaveLensVisualization(props, saveProps, options);
|
||||
|
||||
// callback called
|
||||
expect(props.onAppLeave).toHaveBeenCalled();
|
||||
|
||||
// not called
|
||||
expect(props.redirectToOrigin).not.toHaveBeenCalled();
|
||||
expect(redirectToDashboardFn).toHaveBeenCalledWith(
|
||||
'dashboards',
|
||||
// make sure the new savedObject id is removed from the new input
|
||||
expect.objectContaining({
|
||||
state: expect.objectContaining({
|
||||
input: expect.objectContaining({ savedObjectId: undefined }),
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect(saveToLibraryFn).not.toHaveBeenCalled();
|
||||
expect(toasts.addSuccess).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// save the current document as a new by-ref copy and add it to a dashboard
|
||||
it('should save as a new by-ref copy and redirect to the dashboard', async () => {
|
||||
const dashboardId = faker.random.uuid();
|
||||
const { props, saveProps, options, redirectToDashboardFn, saveToLibraryFn, toasts } =
|
||||
getDefaultArgs(
|
||||
{
|
||||
// defaultDoc is by reference
|
||||
},
|
||||
{ newCopyOnSave: true, saveToLibrary: true, dashboardId }
|
||||
);
|
||||
await runSaveLensVisualization(props, saveProps, options);
|
||||
|
||||
// callback called
|
||||
expect(props.onAppLeave).toHaveBeenCalled();
|
||||
expect(redirectToDashboardFn).toHaveBeenCalledWith(
|
||||
'dashboards',
|
||||
// make sure the new savedObject id is passed with the new input
|
||||
expect.objectContaining({
|
||||
state: expect.objectContaining({
|
||||
input: expect.objectContaining({ savedObjectId: '1234' }),
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect(saveToLibraryFn).toHaveBeenCalled();
|
||||
|
||||
// not called
|
||||
expect(props.redirectToOrigin).not.toHaveBeenCalled();
|
||||
expect(toasts.addSuccess).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fresh editor start', () => {
|
||||
resetMocks();
|
||||
|
||||
it('should reload the editor if it has been saved as new copy', async () => {
|
||||
const { props, saveProps, options, saveToLibraryFn, cleanupEditor, toasts } = getDefaultArgs(
|
||||
{},
|
||||
{
|
||||
saveToLibrary: true,
|
||||
newCopyOnSave: true,
|
||||
}
|
||||
);
|
||||
const result = await runSaveLensVisualization(props, saveProps, options);
|
||||
|
||||
// callback called
|
||||
expect(saveToLibraryFn).toHaveBeenCalled();
|
||||
expect(toasts.addSuccess).toHaveBeenCalled();
|
||||
expect(cleanupEditor).toHaveBeenCalled();
|
||||
expect(props.redirectTo).toHaveBeenCalledWith(defaultDoc.savedObjectId);
|
||||
expect(result?.isLinkedToOriginatingApp).toBeFalsy();
|
||||
|
||||
// not called
|
||||
expect(props.onAppLeave).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show a notification toast and reload as first save of the document', async () => {
|
||||
const { props, saveProps, options, saveToLibraryFn, toasts } = getDefaultArgs(
|
||||
{
|
||||
lastKnownDoc: { ...defaultDoc, savedObjectId: undefined },
|
||||
persistedDoc: undefined,
|
||||
initialInput: undefined,
|
||||
},
|
||||
{ saveToLibrary: true }
|
||||
);
|
||||
await runSaveLensVisualization(props, saveProps, options);
|
||||
|
||||
// callback called
|
||||
expect(saveToLibraryFn).toHaveBeenCalled();
|
||||
expect(toasts.addSuccess).toHaveBeenCalled();
|
||||
expect(props.redirectTo).toHaveBeenCalled();
|
||||
|
||||
// not called
|
||||
expect(props.application.navigateToApp).not.toHaveBeenCalledWith('lens', { path: '/' });
|
||||
expect(props.redirectToOrigin).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw if something goes wrong when saving', async () => {
|
||||
const attributeServiceMock = {
|
||||
...makeAttributeService(defaultDoc),
|
||||
saveToLibrary: jest.fn().mockImplementation(() => Promise.reject(Error('failed to save'))),
|
||||
};
|
||||
const { props, saveProps, options, toasts } = getDefaultArgs(
|
||||
{
|
||||
lastKnownDoc: { ...defaultDoc, savedObjectId: undefined },
|
||||
attributeService: attributeServiceMock,
|
||||
},
|
||||
{ saveToLibrary: true }
|
||||
);
|
||||
try {
|
||||
await runSaveLensVisualization(props, saveProps, options);
|
||||
} catch (error) {
|
||||
expect(toasts.addDanger).toHaveBeenCalled();
|
||||
expect(toasts.addSuccess).not.toHaveBeenCalled();
|
||||
expect(error.message).toEqual('failed to save');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// While this is technically a virtual option as for now, it's still worth testing to not break it in the future
|
||||
describe('Textbased version', () => {
|
||||
resetMocks();
|
||||
|
||||
it('should have a dedicated flow for textbased saving by-ref', async () => {
|
||||
// simulate a new save
|
||||
const attributeServiceMock = makeAttributeService({
|
||||
...defaultDoc,
|
||||
savedObjectId: faker.random.uuid(),
|
||||
});
|
||||
|
||||
const { props, saveProps, options, saveToLibraryFn, cleanupEditor } = getDefaultArgs(
|
||||
{
|
||||
textBasedLanguageSave: true,
|
||||
attributeService: attributeServiceMock,
|
||||
// give a document without a savedObjectId
|
||||
lastKnownDoc: { ...defaultDoc, savedObjectId: undefined },
|
||||
persistedDoc: undefined,
|
||||
// simulate a fresh start in the editor
|
||||
initialInput: undefined,
|
||||
},
|
||||
{
|
||||
saveToLibrary: true,
|
||||
}
|
||||
);
|
||||
|
||||
await runSaveLensVisualization(props, saveProps, options);
|
||||
|
||||
// callback called
|
||||
expect(saveToLibraryFn).toHaveBeenCalled();
|
||||
expect(cleanupEditor).toHaveBeenCalled();
|
||||
expect(props.switchDatasource).toHaveBeenCalled();
|
||||
expect(props.redirectTo).not.toHaveBeenCalled();
|
||||
expect(props.application.navigateToApp).toHaveBeenCalledWith('lens', { path: '/' });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -11,25 +11,29 @@ import { isFilterPinned } from '@kbn/es-query';
|
|||
import { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public';
|
||||
import type { SavedObjectReference } from '@kbn/core/public';
|
||||
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { omit } from 'lodash';
|
||||
import { SaveModal } from './save_modal';
|
||||
import type { LensAppProps, LensAppServices } from './types';
|
||||
import type { SaveProps } from './app';
|
||||
import { Document, checkForDuplicateTitle, SavedObjectIndexStore } from '../persistence';
|
||||
import type { LensByReferenceInput, LensEmbeddableInput } from '../embeddable';
|
||||
import { checkForDuplicateTitle, SavedObjectIndexStore, LensDocument } from '../persistence';
|
||||
import { APP_ID, getFullPath } from '../../common/constants';
|
||||
import type { LensAppState } from '../state_management';
|
||||
import { getPersisted } from '../state_management/init_middleware/load_initial';
|
||||
import { VisualizeEditorContext } from '../types';
|
||||
import { getFromPreloaded } from '../state_management/init_middleware/load_initial';
|
||||
import { Simplify, VisualizeEditorContext } from '../types';
|
||||
import { redirectToDashboard } from './save_modal_container_helpers';
|
||||
import { LensSerializedState } from '../react_embeddable/types';
|
||||
import { isLegacyEditorEmbeddable } from './app_helpers';
|
||||
|
||||
type ExtraProps = Pick<LensAppProps, 'initialInput'> &
|
||||
Partial<Pick<LensAppProps, 'redirectToOrigin' | 'redirectTo' | 'onAppLeave'>>;
|
||||
type ExtraProps = Simplify<
|
||||
Pick<LensAppProps, 'initialInput'> &
|
||||
Partial<Pick<LensAppProps, 'redirectToOrigin' | 'redirectTo' | 'onAppLeave'>>
|
||||
>;
|
||||
|
||||
export type SaveModalContainerProps = {
|
||||
originatingApp?: string;
|
||||
getOriginatingPath?: (dashboardId: string) => string;
|
||||
persistedDoc?: Document;
|
||||
lastKnownDoc?: Document;
|
||||
persistedDoc?: LensDocument;
|
||||
lastKnownDoc?: LensDocument;
|
||||
returnToOriginSwitchLabel?: string;
|
||||
onClose: () => void;
|
||||
onSave?: (saveProps: SaveProps) => void;
|
||||
|
@ -78,19 +82,14 @@ export function SaveModalContainer({
|
|||
let description;
|
||||
let savedObjectId;
|
||||
const [initializing, setInitializing] = useState(true);
|
||||
const [lastKnownDoc, setLastKnownDoc] = useState<Document | undefined>(initLastKnownDoc);
|
||||
const [lastKnownDoc, setLastKnownDoc] = useState<LensDocument | undefined>(initLastKnownDoc);
|
||||
if (lastKnownDoc) {
|
||||
title = lastKnownDoc.title;
|
||||
description = lastKnownDoc.description;
|
||||
savedObjectId = lastKnownDoc.savedObjectId;
|
||||
}
|
||||
|
||||
if (
|
||||
!lastKnownDoc?.title &&
|
||||
initialContext &&
|
||||
'isEmbeddable' in initialContext &&
|
||||
initialContext.isEmbeddable
|
||||
) {
|
||||
if (!lastKnownDoc?.title && isLegacyEditorEmbeddable(initialContext)) {
|
||||
title = i18n.translate('xpack.lens.app.convertedLabel', {
|
||||
defaultMessage: '{title} (converted)',
|
||||
values: {
|
||||
|
@ -109,7 +108,7 @@ export function SaveModalContainer({
|
|||
let isMounted = true;
|
||||
|
||||
if (initialInput) {
|
||||
getPersisted({
|
||||
getFromPreloaded({
|
||||
initialInput,
|
||||
lensServices,
|
||||
})
|
||||
|
@ -133,12 +132,13 @@ export function SaveModalContainer({
|
|||
? savedObjectsTagging.ui.getTagIdsFromReferences(persistedDoc.references)
|
||||
: [];
|
||||
|
||||
const runLensSave = (saveProps: SaveProps, options: { saveToLibrary: boolean }) => {
|
||||
const runLensSave = async (saveProps: SaveProps, options: { saveToLibrary: boolean }) => {
|
||||
if (runSave) {
|
||||
// inside lens, we use the function that's passed to it
|
||||
runSave(saveProps, options);
|
||||
} else if (attributeService && lastKnownDoc) {
|
||||
runSaveLensVisualization(
|
||||
return runSave(saveProps, options);
|
||||
}
|
||||
if (attributeService && lastKnownDoc) {
|
||||
await runSaveLensVisualization(
|
||||
{
|
||||
...lensServices,
|
||||
lastKnownDoc,
|
||||
|
@ -147,16 +147,14 @@ export function SaveModalContainer({
|
|||
redirectToOrigin,
|
||||
originatingApp,
|
||||
getOriginatingPath,
|
||||
getIsByValueMode: () => false,
|
||||
onAppLeave: () => {},
|
||||
...lensServices,
|
||||
},
|
||||
saveProps,
|
||||
options
|
||||
).then(() => {
|
||||
onSave?.(saveProps);
|
||||
onClose();
|
||||
});
|
||||
);
|
||||
onSave?.(saveProps);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -188,11 +186,24 @@ export function SaveModalContainer({
|
|||
);
|
||||
}
|
||||
|
||||
function fromDocumentToSerializedState(
|
||||
doc: LensDocument,
|
||||
panelSettings: Partial<LensSerializedState>,
|
||||
originalInput?: LensAppProps['initialInput']
|
||||
): LensSerializedState {
|
||||
return {
|
||||
...originalInput,
|
||||
attributes: omit(doc, 'savedObjectId'),
|
||||
savedObjectId: doc.savedObjectId,
|
||||
...panelSettings,
|
||||
};
|
||||
}
|
||||
|
||||
const getDocToSave = (
|
||||
lastKnownDoc: Document,
|
||||
lastKnownDoc: LensDocument,
|
||||
saveProps: SaveProps,
|
||||
references: SavedObjectReference[]
|
||||
) => {
|
||||
): LensDocument => {
|
||||
const docToSave = {
|
||||
...removePinnedFilters(lastKnownDoc)!,
|
||||
references,
|
||||
|
@ -209,11 +220,10 @@ const getDocToSave = (
|
|||
return docToSave;
|
||||
};
|
||||
|
||||
export const runSaveLensVisualization = async (
|
||||
props: {
|
||||
lastKnownDoc?: Document;
|
||||
getIsByValueMode: () => boolean;
|
||||
persistedDoc?: Document;
|
||||
export type SaveVisualizationProps = Simplify<
|
||||
{
|
||||
lastKnownDoc?: LensDocument;
|
||||
persistedDoc?: LensDocument;
|
||||
originatingApp?: string;
|
||||
getOriginatingPath?: (dashboardId: string) => string;
|
||||
textBasedLanguageSave?: boolean;
|
||||
|
@ -232,7 +242,11 @@ export const runSaveLensVisualization = async (
|
|||
| 'stateTransfer'
|
||||
| 'attributeService'
|
||||
| 'savedObjectsTagging'
|
||||
>,
|
||||
>
|
||||
>;
|
||||
|
||||
export const runSaveLensVisualization = async (
|
||||
props: SaveVisualizationProps,
|
||||
saveProps: SaveProps,
|
||||
options: { saveToLibrary: boolean }
|
||||
): Promise<Partial<LensAppState> | undefined> => {
|
||||
|
@ -245,7 +259,6 @@ export const runSaveLensVisualization = async (
|
|||
stateTransfer,
|
||||
attributeService,
|
||||
savedObjectsTagging,
|
||||
getIsByValueMode,
|
||||
redirectToOrigin,
|
||||
onAppLeave,
|
||||
redirectTo,
|
||||
|
@ -262,7 +275,7 @@ export const runSaveLensVisualization = async (
|
|||
return;
|
||||
}
|
||||
|
||||
let references = lastKnownDoc.references;
|
||||
let references = lastKnownDoc.references || initialInput?.attributes?.references;
|
||||
|
||||
if (savedObjectsTagging) {
|
||||
const tagsIds =
|
||||
|
@ -277,68 +290,90 @@ export const runSaveLensVisualization = async (
|
|||
|
||||
const docToSave = getDocToSave(lastKnownDoc, saveProps, references);
|
||||
|
||||
// Required to serialize filters in by value mode until
|
||||
// https://github.com/elastic/kibana/issues/77588 is fixed
|
||||
if (getIsByValueMode()) {
|
||||
docToSave.state.filters.forEach((filter) => {
|
||||
if (typeof filter.meta.value === 'function') {
|
||||
delete filter.meta.value;
|
||||
const originalInput = saveProps.newCopyOnSave ? undefined : initialInput;
|
||||
const originalSavedObjectId = originalInput?.savedObjectId;
|
||||
if (options.saveToLibrary) {
|
||||
// this is a lower level call that the Lens attribute service one
|
||||
// @TODO: check if it's worth to replace it witht he attribute service one
|
||||
await checkForDuplicateTitle(
|
||||
{
|
||||
id: originalSavedObjectId,
|
||||
title: docToSave.title,
|
||||
displayName: i18n.translate('xpack.lens.app.saveModalType', {
|
||||
defaultMessage: 'Lens visualization',
|
||||
}),
|
||||
lastSavedTitle: lastKnownDoc.title,
|
||||
copyOnSave: saveProps.newCopyOnSave,
|
||||
isTitleDuplicateConfirmed: saveProps.isTitleDuplicateConfirmed,
|
||||
},
|
||||
saveProps.onTitleDuplicate,
|
||||
{
|
||||
client: savedObjectStore,
|
||||
...startServices,
|
||||
}
|
||||
});
|
||||
);
|
||||
// ignore duplicate title failure, user notified in save modal
|
||||
}
|
||||
|
||||
const originalInput = saveProps.newCopyOnSave ? undefined : initialInput;
|
||||
const originalSavedObjectId = (originalInput as LensByReferenceInput)?.savedObjectId;
|
||||
if (options.saveToLibrary) {
|
||||
try {
|
||||
await checkForDuplicateTitle(
|
||||
{
|
||||
id: originalSavedObjectId,
|
||||
title: docToSave.title,
|
||||
displayName: i18n.translate('xpack.lens.app.saveModalType', {
|
||||
defaultMessage: 'Lens visualization',
|
||||
}),
|
||||
lastSavedTitle: lastKnownDoc.title,
|
||||
copyOnSave: saveProps.newCopyOnSave,
|
||||
isTitleDuplicateConfirmed: saveProps.isTitleDuplicateConfirmed,
|
||||
},
|
||||
saveProps.onTitleDuplicate,
|
||||
{
|
||||
client: savedObjectStore,
|
||||
...startServices,
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
// ignore duplicate title failure, user notified in save modal
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
try {
|
||||
let newInput = (await attributeService.wrapAttributes(
|
||||
// wrap the doc into a serializable state
|
||||
const newDoc = fromDocumentToSerializedState(
|
||||
docToSave,
|
||||
options.saveToLibrary,
|
||||
{
|
||||
timeRange: saveProps.panelTimeRange ?? originalInput?.timeRange,
|
||||
savedObjectId: options.saveToLibrary ? originalSavedObjectId : undefined,
|
||||
},
|
||||
originalInput
|
||||
)) as LensEmbeddableInput;
|
||||
if (saveProps.panelTimeRange) {
|
||||
newInput = {
|
||||
...newInput,
|
||||
timeRange: saveProps.panelTimeRange,
|
||||
};
|
||||
);
|
||||
|
||||
let savedObjectId: string | undefined;
|
||||
try {
|
||||
savedObjectId =
|
||||
newDoc.attributes && options.saveToLibrary
|
||||
? await attributeService.saveToLibrary(
|
||||
newDoc.attributes,
|
||||
newDoc.attributes.references || [],
|
||||
originalSavedObjectId
|
||||
)
|
||||
: undefined;
|
||||
} catch (error) {
|
||||
notifications.toasts.addDanger({
|
||||
title: i18n.translate('xpack.lens.app.saveVisualization.errorNotificationText', {
|
||||
defaultMessage: `An error occurred while saving. Error: {errorMessage}`,
|
||||
values: {
|
||||
errorMessage: error.message,
|
||||
},
|
||||
}),
|
||||
});
|
||||
// trigger a reject to jump to the final catch clause
|
||||
throw error;
|
||||
}
|
||||
if (saveProps.returnToOrigin && redirectToOrigin) {
|
||||
|
||||
const shouldNavigateBackToOrigin = saveProps.returnToOrigin && redirectToOrigin;
|
||||
const hasRedirect = shouldNavigateBackToOrigin || saveProps.dashboardId;
|
||||
|
||||
// if a redirect was set, prevent the validation on app leave
|
||||
if (hasRedirect) {
|
||||
// disabling the validation on app leave because the document has been saved.
|
||||
onAppLeave?.((actions) => {
|
||||
return actions.default();
|
||||
});
|
||||
redirectToOrigin({ input: newInput, isCopied: saveProps.newCopyOnSave });
|
||||
}
|
||||
|
||||
if (shouldNavigateBackToOrigin) {
|
||||
redirectToOrigin({
|
||||
state: { ...newDoc, savedObjectId },
|
||||
isCopied: saveProps.newCopyOnSave,
|
||||
});
|
||||
return;
|
||||
} else if (saveProps.dashboardId) {
|
||||
// disabling the validation on app leave because the document has been saved.
|
||||
onAppLeave?.((actions) => {
|
||||
return actions.default();
|
||||
});
|
||||
}
|
||||
// should we make it more robust here and better check the context of the saving
|
||||
// or keep the responsability of the consumer of the function to provide the right set
|
||||
// of args here in case the user is within a by value chart AND want's to save it in the library
|
||||
// without redirect?
|
||||
if (saveProps.dashboardId) {
|
||||
redirectToDashboard({
|
||||
embeddableInput: newInput,
|
||||
embeddableInput: { ...newDoc, savedObjectId },
|
||||
dashboardId: saveProps.dashboardId,
|
||||
stateTransfer,
|
||||
originatingApp: props.originatingApp,
|
||||
|
@ -356,15 +391,8 @@ export const runSaveLensVisualization = async (
|
|||
})
|
||||
);
|
||||
|
||||
if (
|
||||
attributeService.inputIsRefType(newInput) &&
|
||||
newInput.savedObjectId !== originalSavedObjectId
|
||||
) {
|
||||
chrome.recentlyAccessed.add(
|
||||
getFullPath(newInput.savedObjectId),
|
||||
docToSave.title,
|
||||
newInput.savedObjectId
|
||||
);
|
||||
if (savedObjectId && savedObjectId !== originalSavedObjectId) {
|
||||
chrome.recentlyAccessed.add(getFullPath(savedObjectId), docToSave.title, savedObjectId);
|
||||
|
||||
// remove editor state so the connection is still broken after reload
|
||||
stateTransfer.clearEditorState?.(APP_ID);
|
||||
|
@ -372,18 +400,13 @@ export const runSaveLensVisualization = async (
|
|||
switchDatasource?.();
|
||||
application.navigateToApp('lens', { path: '/' });
|
||||
} else {
|
||||
redirectTo?.(newInput.savedObjectId);
|
||||
redirectTo?.(savedObjectId);
|
||||
}
|
||||
return { isLinkedToOriginatingApp: false };
|
||||
}
|
||||
|
||||
const newDoc = {
|
||||
...docToSave,
|
||||
...newInput,
|
||||
};
|
||||
|
||||
return {
|
||||
persistedDoc: newDoc,
|
||||
persistedDoc: newDoc.attributes,
|
||||
isLinkedToOriginatingApp: false,
|
||||
};
|
||||
} catch (e) {
|
||||
|
@ -393,7 +416,7 @@ export const runSaveLensVisualization = async (
|
|||
}
|
||||
};
|
||||
|
||||
export function removePinnedFilters(doc?: Document) {
|
||||
export function removePinnedFilters(doc?: LensDocument) {
|
||||
if (!doc) return undefined;
|
||||
return {
|
||||
...doc,
|
||||
|
|
|
@ -5,14 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { makeDefaultServices } from '../mocks';
|
||||
import type { LensEmbeddableInput } from '../embeddable';
|
||||
import type { LensAppServices } from './types';
|
||||
import { redirectToDashboard } from './save_modal_container_helpers';
|
||||
import { LensSerializedState } from '..';
|
||||
|
||||
describe('redirectToDashboard', () => {
|
||||
const embeddableInput = {
|
||||
test: 'test',
|
||||
} as unknown as LensEmbeddableInput;
|
||||
} as unknown as LensSerializedState;
|
||||
const mockServices = makeDefaultServices();
|
||||
|
||||
it('should call the navigateToWithEmbeddablePackage with the correct args if originatingApp is given', () => {
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
*/
|
||||
|
||||
import type { LensAppServices } from './types';
|
||||
import type { LensEmbeddableInput } from '../embeddable';
|
||||
import { LENS_EMBEDDABLE_TYPE } from '../../common/constants';
|
||||
import { LensSerializedState } from '../react_embeddable/types';
|
||||
|
||||
export const redirectToDashboard = ({
|
||||
embeddableInput,
|
||||
|
@ -16,7 +16,7 @@ export const redirectToDashboard = ({
|
|||
getOriginatingPath,
|
||||
stateTransfer,
|
||||
}: {
|
||||
embeddableInput: LensEmbeddableInput;
|
||||
embeddableInput: LensSerializedState;
|
||||
dashboardId: string;
|
||||
originatingApp?: string;
|
||||
getOriginatingPath?: (dashboardId: string) => string | undefined;
|
||||
|
|
|
@ -11,7 +11,7 @@ import { DataViewSpec } from '@kbn/data-views-plugin/common';
|
|||
import type { LensAppLocatorParams } from '../../common/locator/locator';
|
||||
import type { LensAppState } from '../state_management';
|
||||
import type { LensAppServices } from './types';
|
||||
import type { Document } from '../persistence/saved_object_store';
|
||||
import type { LensDocument } from '../persistence/saved_object_store';
|
||||
import type { DatasourceMap, VisualizationMap } from '../types';
|
||||
import { extractReferencesFromState, getResolvedDateRange } from '../utils';
|
||||
import { getEditPath } from '../../common/constants';
|
||||
|
@ -23,7 +23,7 @@ interface ShareableConfiguration
|
|||
> {
|
||||
datasourceMap: DatasourceMap;
|
||||
visualizationMap: VisualizationMap;
|
||||
currentDoc: Document | undefined;
|
||||
currentDoc: LensDocument | undefined;
|
||||
adHocDataViews?: DataViewSpec[];
|
||||
}
|
||||
|
||||
|
@ -37,7 +37,7 @@ export const DEFAULT_LENS_LAYOUT_DIMENSIONS = {
|
|||
|
||||
function getShareURLForSavedObject(
|
||||
{ application, data }: Pick<LensAppServices, 'application' | 'data'>,
|
||||
currentDoc: Document | undefined
|
||||
currentDoc: LensDocument | undefined
|
||||
) {
|
||||
return new URL(
|
||||
`${application.getUrlForApp('lens', { absolute: true })}${
|
||||
|
@ -89,7 +89,7 @@ export function getLocatorParams(
|
|||
const serializableDatasourceStates = datasourceStates as LensAppState['datasourceStates'] &
|
||||
SerializableRecord;
|
||||
|
||||
const snapshotParams = {
|
||||
const snapshotParams: LensAppLocatorParams = {
|
||||
filters,
|
||||
query,
|
||||
resolvedDateRange: getResolvedDateRange(data.query.timefilter.timefilter),
|
||||
|
|
|
@ -16,6 +16,7 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
|||
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
||||
import { isEqual } from 'lodash';
|
||||
import { RootDragDropProvider } from '@kbn/dom-drag-drop';
|
||||
import { TypedLensSerializedState } from '../../../react_embeddable/types';
|
||||
import type { LensPluginStartDependencies } from '../../../plugin';
|
||||
import {
|
||||
makeConfigureStore,
|
||||
|
@ -28,8 +29,7 @@ import { generateId } from '../../../id_generator';
|
|||
import type { DatasourceMap, VisualizationMap } from '../../../types';
|
||||
import { LensEditConfigurationFlyout } from './lens_configuration_flyout';
|
||||
import type { EditConfigPanelProps } from './types';
|
||||
import { SavedObjectIndexStore, type Document } from '../../../persistence';
|
||||
import type { TypedLensByValueInput } from '../../../embeddable/embeddable_component';
|
||||
import { SavedObjectIndexStore, type LensDocument } from '../../../persistence';
|
||||
import { DOC_TYPE } from '../../../../common/constants';
|
||||
|
||||
export type EditLensConfigurationProps = Omit<
|
||||
|
@ -87,6 +87,41 @@ export const updatingMiddleware =
|
|||
}
|
||||
};
|
||||
|
||||
const MaybeWrapper = ({
|
||||
wrapInFlyout,
|
||||
closeFlyout,
|
||||
children,
|
||||
}: {
|
||||
wrapInFlyout?: boolean;
|
||||
children: JSX.Element;
|
||||
closeFlyout?: () => void;
|
||||
}) => {
|
||||
if (!wrapInFlyout) {
|
||||
return children;
|
||||
}
|
||||
return (
|
||||
<EuiFlyout
|
||||
data-test-subj="lnsEditOnFlyFlyout"
|
||||
type="push"
|
||||
ownFocus
|
||||
paddingSize="m"
|
||||
onClose={() => {
|
||||
closeFlyout?.();
|
||||
}}
|
||||
aria-labelledby={i18n.translate('xpack.lens.config.editLabel', {
|
||||
defaultMessage: 'Edit configuration',
|
||||
})}
|
||||
size="s"
|
||||
hideCloseButton
|
||||
css={css`
|
||||
clip-path: polygon(-100% 0, 100% 0, 100% 100%, -100% 100%);
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</EuiFlyout>
|
||||
);
|
||||
};
|
||||
|
||||
export async function getEditLensConfiguration(
|
||||
coreStart: CoreStart,
|
||||
startDependencies: LensPluginStartDependencies,
|
||||
|
@ -109,30 +144,29 @@ export async function getEditLensConfiguration(
|
|||
datasourceId,
|
||||
panelId,
|
||||
savedObjectId,
|
||||
output$,
|
||||
dataLoading$,
|
||||
lensAdapters,
|
||||
updateByRefInput,
|
||||
navigateToLensEditor,
|
||||
displayFlyoutHeader,
|
||||
canEditTextBasedQuery,
|
||||
isNewPanel,
|
||||
deletePanel,
|
||||
hidesSuggestions,
|
||||
onApplyCb,
|
||||
onCancelCb,
|
||||
onApply,
|
||||
onCancel,
|
||||
hideTimeFilterInfo,
|
||||
}: EditLensConfigurationProps) => {
|
||||
if (!lensServices || !datasourceMap || !visualizationMap) {
|
||||
return <LoadingSpinnerWithOverlay />;
|
||||
}
|
||||
const [currentAttributes, setCurrentAttributes] =
|
||||
useState<TypedLensByValueInput['attributes']>(attributes);
|
||||
useState<TypedLensSerializedState['attributes']>(attributes);
|
||||
/**
|
||||
* During inline editing of a by reference panel, the panel is converted to a by value one.
|
||||
* When the user applies the changes we save them to the Lens SO
|
||||
*/
|
||||
const saveByRef = useCallback(
|
||||
async (attrs: Document) => {
|
||||
async (attrs: LensDocument) => {
|
||||
const savedObjectStore = new SavedObjectIndexStore(lensServices.contentManagement);
|
||||
await savedObjectStore.save({
|
||||
...attrs,
|
||||
|
@ -167,34 +201,6 @@ export async function getEditLensConfiguration(
|
|||
})
|
||||
);
|
||||
|
||||
const getWrapper = (children: JSX.Element) => {
|
||||
if (wrapInFlyout) {
|
||||
return (
|
||||
<EuiFlyout
|
||||
data-test-subj="lnsEditOnFlyFlyout"
|
||||
type="push"
|
||||
ownFocus
|
||||
paddingSize="m"
|
||||
onClose={() => {
|
||||
closeFlyout?.();
|
||||
}}
|
||||
aria-labelledby={i18n.translate('xpack.lens.config.editLabel', {
|
||||
defaultMessage: 'Edit configuration',
|
||||
})}
|
||||
size="s"
|
||||
hideCloseButton
|
||||
css={css`
|
||||
clip-path: polygon(-100% 0, 100% 0, 100% 100%, -100% 100%);
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</EuiFlyout>
|
||||
);
|
||||
} else {
|
||||
return children;
|
||||
}
|
||||
};
|
||||
|
||||
const configPanelProps = {
|
||||
attributes: currentAttributes,
|
||||
updatePanelState,
|
||||
|
@ -204,7 +210,7 @@ export async function getEditLensConfiguration(
|
|||
coreStart,
|
||||
startDependencies,
|
||||
visualizationMap,
|
||||
output$,
|
||||
dataLoading$,
|
||||
lensAdapters,
|
||||
datasourceMap,
|
||||
saveByRef,
|
||||
|
@ -216,22 +222,23 @@ export async function getEditLensConfiguration(
|
|||
hidesSuggestions,
|
||||
setCurrentAttributes,
|
||||
isNewPanel,
|
||||
deletePanel,
|
||||
onApplyCb,
|
||||
onCancelCb,
|
||||
onApply,
|
||||
onCancel,
|
||||
hideTimeFilterInfo,
|
||||
};
|
||||
|
||||
return getWrapper(
|
||||
<Provider store={lensStore}>
|
||||
<KibanaRenderContextProvider {...coreStart}>
|
||||
<KibanaContextProvider services={lensServices}>
|
||||
<RootDragDropProvider>
|
||||
<LensEditConfigurationFlyout {...configPanelProps} />
|
||||
</RootDragDropProvider>
|
||||
</KibanaContextProvider>
|
||||
</KibanaRenderContextProvider>
|
||||
</Provider>
|
||||
return (
|
||||
<MaybeWrapper wrapInFlyout={wrapInFlyout} closeFlyout={closeFlyout}>
|
||||
<Provider store={lensStore}>
|
||||
<KibanaRenderContextProvider {...coreStart}>
|
||||
<KibanaContextProvider services={lensServices}>
|
||||
<RootDragDropProvider>
|
||||
<LensEditConfigurationFlyout {...configPanelProps} />
|
||||
</RootDragDropProvider>
|
||||
</KibanaContextProvider>
|
||||
</KibanaRenderContextProvider>
|
||||
</Provider>
|
||||
</MaybeWrapper>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ import type { DataView } from '@kbn/data-views-plugin/common';
|
|||
import type { DatatableColumn } from '@kbn/expressions-plugin/common';
|
||||
import { getTime } from '@kbn/data-plugin/common';
|
||||
import { type DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import type { TypedLensByValueInput } from '../../../embeddable/embeddable_component';
|
||||
import { TypedLensSerializedState } from '../../../react_embeddable/types';
|
||||
import type { LensPluginStartDependencies } from '../../../plugin';
|
||||
import type { DatasourceMap, VisualizationMap } from '../../../types';
|
||||
import { suggestionsApi } from '../../../lens_suggestions_api';
|
||||
|
@ -123,7 +123,7 @@ export const getSuggestions = async (
|
|||
query,
|
||||
suggestion: firstSuggestion,
|
||||
dataView,
|
||||
}) as TypedLensByValueInput['attributes'];
|
||||
}) as TypedLensSerializedState['attributes'];
|
||||
return attrs;
|
||||
} catch (e) {
|
||||
setErrors([e]);
|
||||
|
|
|
@ -13,9 +13,9 @@ import { coreMock } from '@kbn/core/public/mocks';
|
|||
import { mockVisualizationMap, mockDatasourceMap, mockDataPlugin } from '../../../mocks';
|
||||
import type { LensPluginStartDependencies } from '../../../plugin';
|
||||
import { createMockStartDependencies } from '../../../editor_frame_service/mocks';
|
||||
import type { TypedLensByValueInput } from '../../../embeddable/embeddable_component';
|
||||
import { LensEditConfigurationFlyout } from './lens_configuration_flyout';
|
||||
import type { EditConfigPanelProps } from './types';
|
||||
import { TypedLensSerializedState } from '../../../react_embeddable/types';
|
||||
|
||||
jest.mock('@kbn/esql-utils', () => {
|
||||
return {
|
||||
|
@ -93,7 +93,7 @@ const lensAttributes = {
|
|||
esql: 'from index1 | limit 10',
|
||||
},
|
||||
references: [],
|
||||
} as unknown as TypedLensByValueInput['attributes'];
|
||||
} as unknown as TypedLensSerializedState['attributes'];
|
||||
const mockStartDependencies =
|
||||
createMockStartDependencies() as unknown as LensPluginStartDependencies;
|
||||
|
||||
|
@ -139,6 +139,8 @@ describe('LensEditConfigurationFlyout', () => {
|
|||
visualizationMap={visualizationMap}
|
||||
closeFlyout={jest.fn()}
|
||||
datasourceId={'testDatasource' as EditConfigPanelProps['datasourceId']}
|
||||
onApply={jest.fn()}
|
||||
onCancel={jest.fn()}
|
||||
{...propsOverrides}
|
||||
/>,
|
||||
{},
|
||||
|
@ -234,7 +236,7 @@ describe('LensEditConfigurationFlyout', () => {
|
|||
await renderConfigFlyout(
|
||||
{
|
||||
closeFlyout: jest.fn(),
|
||||
onApplyCb: onApplyCbSpy,
|
||||
onApply: onApplyCbSpy,
|
||||
},
|
||||
{ esql: 'from index1 | limit 10' }
|
||||
);
|
||||
|
|
|
@ -30,6 +30,7 @@ import {
|
|||
import type { AggregateQuery, Query } from '@kbn/es-query';
|
||||
import { ESQLLangEditor } from '@kbn/esql/public';
|
||||
import { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common';
|
||||
import type { TypedLensSerializedState } from '../../../react_embeddable/types';
|
||||
import { buildExpression } from '../../../editor_frame_service/editor_frame/expression_helpers';
|
||||
import { MAX_NUM_OF_COLUMNS } from '../../../datasources/text_based/utils';
|
||||
import {
|
||||
|
@ -38,7 +39,6 @@ import {
|
|||
onActiveDataChange,
|
||||
useLensDispatch,
|
||||
} from '../../../state_management';
|
||||
import type { TypedLensByValueInput } from '../../../embeddable/embeddable_component';
|
||||
import {
|
||||
EXPRESSION_BUILD_ERROR_ID,
|
||||
extractReferencesFromState,
|
||||
|
@ -67,20 +67,19 @@ export function LensEditConfigurationFlyout({
|
|||
saveByRef,
|
||||
savedObjectId,
|
||||
updateByRefInput,
|
||||
output$,
|
||||
dataLoading$,
|
||||
lensAdapters,
|
||||
navigateToLensEditor,
|
||||
displayFlyoutHeader,
|
||||
canEditTextBasedQuery,
|
||||
isNewPanel,
|
||||
deletePanel,
|
||||
hidesSuggestions,
|
||||
onApplyCb,
|
||||
onCancelCb,
|
||||
onApply: onApplyCallback,
|
||||
onCancel: onCancelCallback,
|
||||
hideTimeFilterInfo,
|
||||
}: EditConfigPanelProps) {
|
||||
const euiTheme = useEuiTheme();
|
||||
const previousAttributes = useRef<TypedLensByValueInput['attributes']>(attributes);
|
||||
const previousAttributes = useRef<TypedLensSerializedState['attributes']>(attributes);
|
||||
const previousAdapters = useRef<Partial<DefaultInspectorAdapters> | undefined>(lensAdapters);
|
||||
const prevQuery = useRef<AggregateQuery | Query>(attributes.state.query);
|
||||
const [query, setQuery] = useState<AggregateQuery | Query>(attributes.state.query);
|
||||
|
@ -117,7 +116,11 @@ export function LensEditConfigurationFlyout({
|
|||
|
||||
const dispatch = useLensDispatch();
|
||||
useEffect(() => {
|
||||
const s = output$?.subscribe(() => {
|
||||
const s = dataLoading$?.subscribe((isDataLoading) => {
|
||||
// go thru only when the loading is complete
|
||||
if (isDataLoading) {
|
||||
return;
|
||||
}
|
||||
const activeData: Record<string, Datatable> = {};
|
||||
const adaptersTables = previousAdapters.current?.tables?.tables;
|
||||
const [table] = Object.values(adaptersTables || {});
|
||||
|
@ -134,7 +137,7 @@ export function LensEditConfigurationFlyout({
|
|||
}
|
||||
});
|
||||
return () => s?.unsubscribe();
|
||||
}, [dispatch, output$, layers]);
|
||||
}, [dispatch, dataLoading$, layers]);
|
||||
|
||||
useEffect(() => {
|
||||
const abortController = new AbortController();
|
||||
|
@ -217,16 +220,10 @@ export function LensEditConfigurationFlyout({
|
|||
updateByRefInput?.(savedObjectId);
|
||||
}
|
||||
}
|
||||
// for a newly created chart, I want cancelling to also remove the panel
|
||||
if (isNewPanel && deletePanel) {
|
||||
deletePanel();
|
||||
}
|
||||
onCancelCb?.();
|
||||
onCancelCallback?.();
|
||||
closeFlyout?.();
|
||||
}, [
|
||||
attributesChanged,
|
||||
isNewPanel,
|
||||
deletePanel,
|
||||
closeFlyout,
|
||||
visualization.activeId,
|
||||
savedObjectId,
|
||||
|
@ -235,7 +232,7 @@ export function LensEditConfigurationFlyout({
|
|||
updatePanelState,
|
||||
updateSuggestion,
|
||||
updateByRefInput,
|
||||
onCancelCb,
|
||||
onCancelCallback,
|
||||
]);
|
||||
|
||||
const textBasedMode = useMemo(
|
||||
|
@ -244,6 +241,9 @@ export function LensEditConfigurationFlyout({
|
|||
);
|
||||
|
||||
const onApply = useCallback(() => {
|
||||
if (visualization.activeId == null) {
|
||||
return;
|
||||
}
|
||||
const dsStates = Object.fromEntries(
|
||||
Object.entries(datasourceStates).map(([id, ds]) => {
|
||||
const dsState = ds.state;
|
||||
|
@ -265,7 +265,7 @@ export function LensEditConfigurationFlyout({
|
|||
activeVisualization,
|
||||
})
|
||||
: [];
|
||||
const attrs = {
|
||||
const attrs: TypedLensSerializedState['attributes'] = {
|
||||
...attributes,
|
||||
state: {
|
||||
...attributes.state,
|
||||
|
@ -293,18 +293,18 @@ export function LensEditConfigurationFlyout({
|
|||
trackSaveUiCounterEvents(telemetryEvents);
|
||||
}
|
||||
|
||||
onApplyCb?.(attrs as TypedLensByValueInput['attributes']);
|
||||
onApplyCallback?.(attrs);
|
||||
closeFlyout?.();
|
||||
}, [
|
||||
visualization.activeId,
|
||||
savedObjectId,
|
||||
closeFlyout,
|
||||
onApplyCallback,
|
||||
datasourceStates,
|
||||
textBasedMode,
|
||||
visualization.state,
|
||||
visualization.activeId,
|
||||
activeVisualization,
|
||||
attributes,
|
||||
savedObjectId,
|
||||
onApplyCb,
|
||||
closeFlyout,
|
||||
datasourceMap,
|
||||
saveByRef,
|
||||
updateByRefInput,
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type { Observable } from 'rxjs';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import type { TypedLensByValueInput } from '../../../embeddable/embeddable_component';
|
||||
import type { PublishingSubject } from '@kbn/presentation-publishing';
|
||||
import type { TypedLensSerializedState } from '../../../react_embeddable/types';
|
||||
import type { LensPluginStartDependencies } from '../../../plugin';
|
||||
import type {
|
||||
DatasourceMap,
|
||||
|
@ -14,9 +14,8 @@ import type {
|
|||
FramePublicAPI,
|
||||
UserMessagesGetter,
|
||||
} from '../../../types';
|
||||
import type { LensEmbeddableOutput } from '../../../embeddable';
|
||||
import type { LensInspector } from '../../../lens_inspector_service';
|
||||
import type { Document } from '../../../persistence';
|
||||
import type { LensDocument } from '../../../persistence';
|
||||
|
||||
export interface FlyoutWrapperProps {
|
||||
children: JSX.Element;
|
||||
|
@ -37,22 +36,22 @@ export interface EditConfigPanelProps {
|
|||
visualizationMap: VisualizationMap;
|
||||
datasourceMap: DatasourceMap;
|
||||
/** The attributes of the Lens embeddable */
|
||||
attributes: TypedLensByValueInput['attributes'];
|
||||
attributes: TypedLensSerializedState['attributes'];
|
||||
/** Callback for updating the visualization and datasources state.*/
|
||||
updatePanelState: (
|
||||
datasourceState: unknown,
|
||||
visualizationState: unknown,
|
||||
visualizationType?: string
|
||||
visualizationId?: string
|
||||
) => void;
|
||||
updateSuggestion?: (attrs: TypedLensByValueInput['attributes']) => void;
|
||||
updateSuggestion?: (attrs: TypedLensSerializedState['attributes']) => void;
|
||||
/** Set the attributes state */
|
||||
setCurrentAttributes?: (attrs: TypedLensByValueInput['attributes']) => void;
|
||||
setCurrentAttributes?: (attrs: TypedLensSerializedState['attributes']) => void;
|
||||
/** Lens visualizations can be either created from ESQL (textBased) or from dataviews (formBased) */
|
||||
datasourceId: 'formBased' | 'textBased';
|
||||
/** Embeddable output observable, useful for dashboard flyout */
|
||||
output$?: Observable<LensEmbeddableOutput>;
|
||||
dataLoading$?: PublishingSubject<boolean | undefined>;
|
||||
/** Contains the active data, necessary for some panel configuration such as coloring */
|
||||
lensAdapters?: LensInspector['adapters'];
|
||||
lensAdapters?: ReturnType<LensInspector['getInspectorAdapters']>;
|
||||
/** Optional callback called when updating the by reference embeddable */
|
||||
updateByRefInput?: (soId: string) => void;
|
||||
/** Callback for closing the edit flyout */
|
||||
|
@ -69,7 +68,7 @@ export interface EditConfigPanelProps {
|
|||
*/
|
||||
savedObjectId?: string;
|
||||
/** Callback for saving the embeddable as a SO */
|
||||
saveByRef?: (attrs: Document) => void;
|
||||
saveByRef?: (attrs: LensDocument) => void;
|
||||
/** Optional callback for navigation from the header of the flyout */
|
||||
navigateToLensEditor?: () => void;
|
||||
/** If set to true it displays a header on the flyout */
|
||||
|
@ -78,21 +77,19 @@ export interface EditConfigPanelProps {
|
|||
canEditTextBasedQuery?: boolean;
|
||||
/** The flyout is used for adding a new panel by scratch */
|
||||
isNewPanel?: boolean;
|
||||
/** Handler for deleting the embeddable, used in case a user cancels a newly created chart */
|
||||
deletePanel?: () => void;
|
||||
/** If set to true the layout changes to accordion and the text based query (i.e. ES|QL) can be edited */
|
||||
hidesSuggestions?: boolean;
|
||||
/** Optional callback for apply flyout button */
|
||||
onApplyCb?: (input: TypedLensByValueInput['attributes']) => void;
|
||||
/** Optional callback for cancel flyout button */
|
||||
onCancelCb?: () => void;
|
||||
/** Apply button handler */
|
||||
onApply?: (attrs: TypedLensSerializedState['attributes']) => void;
|
||||
/** Cancel button handler */
|
||||
onCancel?: () => void;
|
||||
// in cases where the embeddable is not filtered by time
|
||||
// (e.g. through unified search) set this property to true
|
||||
hideTimeFilterInfo?: boolean;
|
||||
}
|
||||
|
||||
export interface LayerConfigurationProps {
|
||||
attributes: TypedLensByValueInput['attributes'];
|
||||
attributes: TypedLensSerializedState['attributes'];
|
||||
coreStart: CoreStart;
|
||||
startDependencies: LensPluginStartDependencies;
|
||||
visualizationMap: VisualizationMap;
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
EsQueryConfig,
|
||||
isOfQueryType,
|
||||
AggregateQuery,
|
||||
isOfAggregateQueryType,
|
||||
} from '@kbn/es-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { RecursiveReadonly } from '@kbn/utility-types';
|
||||
|
@ -219,8 +220,9 @@ export function combineQueryAndFilters(
|
|||
};
|
||||
|
||||
const allQueries = Array.isArray(query) ? query : query && isOfQueryType(query) ? [query] : [];
|
||||
const nonEmptyQueries = allQueries.filter((q) =>
|
||||
Boolean(typeof q.query === 'string' ? q.query.trim() : q.query)
|
||||
const nonEmptyQueries = allQueries.filter(
|
||||
(q) =>
|
||||
!isOfAggregateQueryType(q) && Boolean(typeof q.query === 'string' ? q.query.trim() : q.query)
|
||||
);
|
||||
|
||||
[queries.lucene, queries.kuery] = partition(nonEmptyQueries, (q) => q.language === 'lucene');
|
||||
|
|
|
@ -55,15 +55,15 @@ import type {
|
|||
UserMessagesGetter,
|
||||
StartServices,
|
||||
} from '../types';
|
||||
import type { LensAttributeService } from '../lens_attribute_service';
|
||||
import type { LensEmbeddableInput } from '../embeddable/embeddable';
|
||||
import type { LensAttributesService } from '../lens_attribute_service';
|
||||
import type { LensInspector } from '../lens_inspector_service';
|
||||
import type { IndexPatternServiceAPI } from '../data_views_service/service';
|
||||
import type { Document, SavedObjectIndexStore } from '../persistence/saved_object_store';
|
||||
import type { LensDocument, SavedObjectIndexStore } from '../persistence/saved_object_store';
|
||||
import type { LensAppLocator, LensAppLocatorParams } from '../../common/locator/locator';
|
||||
import { LensSerializedState } from '../react_embeddable/types';
|
||||
|
||||
export interface RedirectToOriginProps {
|
||||
input?: LensEmbeddableInput;
|
||||
state?: LensSerializedState;
|
||||
isCopied?: boolean;
|
||||
}
|
||||
|
||||
|
@ -76,7 +76,7 @@ export interface LensAppProps {
|
|||
redirectToOrigin?: (props?: RedirectToOriginProps) => void;
|
||||
|
||||
// The initial input passed in by the container when editing. Can be either by reference or by value.
|
||||
initialInput?: LensEmbeddableInput;
|
||||
initialInput?: LensSerializedState;
|
||||
|
||||
// State passed in by the container which is used to determine the id of the Originating App.
|
||||
incomingState?: EmbeddableEditorState;
|
||||
|
@ -110,7 +110,7 @@ export interface LensTopNavMenuProps {
|
|||
|
||||
redirectToOrigin?: (props?: RedirectToOriginProps) => void;
|
||||
// The initial input passed in by the container when editing. Can be either by reference or by value.
|
||||
initialInput?: LensEmbeddableInput;
|
||||
initialInput?: LensSerializedState;
|
||||
getIsByValueMode: () => boolean;
|
||||
indicateNoData: boolean;
|
||||
setIsSaveModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
|
@ -124,7 +124,7 @@ export interface LensTopNavMenuProps {
|
|||
initialContextIsEmbedded?: boolean;
|
||||
topNavMenuEntryGenerators: LensTopNavMenuEntryGenerator[];
|
||||
initialContext?: VisualizeFieldContext | VisualizeEditorContext;
|
||||
currentDoc: Document | undefined;
|
||||
currentDoc: LensDocument | undefined;
|
||||
indexPatternService: IndexPatternServiceAPI;
|
||||
getUserMessages: UserMessagesGetter;
|
||||
shortUrlService: (params: LensAppLocatorParams) => Promise<string>;
|
||||
|
@ -156,7 +156,7 @@ export interface LensAppServices extends StartServices {
|
|||
usageCollection?: UsageCollectionStart;
|
||||
stateTransfer: EmbeddableStateTransfer;
|
||||
navigation: NavigationPublicPluginStart;
|
||||
attributeService: LensAttributeService;
|
||||
attributeService: LensAttributesService;
|
||||
contentManagement: ContentManagementPublicStart;
|
||||
savedObjectsTagging?: SavedObjectTaggingPluginStart;
|
||||
getOriginatingAppName: () => string | undefined;
|
||||
|
|
|
@ -43,13 +43,11 @@ export * from './lens_ui_telemetry';
|
|||
export * from './lens_ui_errors';
|
||||
export * from './editor_frame_service/editor_frame';
|
||||
export * from './editor_frame_service';
|
||||
export * from './embeddable';
|
||||
export * from './app_plugin/mounter';
|
||||
export * from './lens_attribute_service';
|
||||
export * from './app_plugin/save_modal_container';
|
||||
export * from './chart_info_api';
|
||||
|
||||
export * from './trigger_actions/open_in_discover_helpers';
|
||||
export * from './trigger_actions/open_lens_config/edit_action_helpers';
|
||||
export * from './trigger_actions/open_lens_config/create_action_helpers';
|
||||
export * from './trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action_helpers';
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
*/
|
||||
|
||||
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
|
||||
import type { EditorFrameService } from './editor_frame_service';
|
||||
import { createChartInfoApi } from './chart_info_api';
|
||||
import type { LensSavedObjectAttributes } from '.';
|
||||
import { LensDocument } from './persistence';
|
||||
import { DatasourceMap, VisualizationMap } from './types';
|
||||
|
||||
const mockGetVisualizationInfo = jest.fn().mockReturnValue({
|
||||
layers: [
|
||||
|
@ -37,18 +37,19 @@ const mockGetDatasourceInfo = jest.fn().mockResolvedValue([
|
|||
describe('createChartInfoApi', () => {
|
||||
const dataViews = dataViewPluginMocks.createStartContract();
|
||||
test('get correct chart info', async () => {
|
||||
const chartInfoApi = await createChartInfoApi(dataViews, {
|
||||
loadVisualizations: () => ({
|
||||
const chartInfoApi = await createChartInfoApi(
|
||||
dataViews,
|
||||
{
|
||||
lnsXY: {
|
||||
getVisualizationInfo: mockGetVisualizationInfo,
|
||||
},
|
||||
}),
|
||||
loadDatasources: () => ({
|
||||
} as unknown as VisualizationMap,
|
||||
{
|
||||
from_based: {
|
||||
getDatasourceInfo: mockGetDatasourceInfo,
|
||||
},
|
||||
}),
|
||||
} as unknown as EditorFrameService);
|
||||
} as unknown as DatasourceMap
|
||||
);
|
||||
const vis = {
|
||||
title: 'xy',
|
||||
visualizationType: 'lnsXY',
|
||||
|
@ -69,7 +70,7 @@ describe('createChartInfoApi', () => {
|
|||
query: '',
|
||||
},
|
||||
references: [],
|
||||
} as LensSavedObjectAttributes;
|
||||
} as LensDocument;
|
||||
|
||||
const chartInfo = await chartInfoApi.getChartInfo(vis);
|
||||
|
||||
|
|
|
@ -5,23 +5,22 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Filter, Query } from '@kbn/es-query';
|
||||
import type { AggregateQuery, Filter, Query } from '@kbn/es-query';
|
||||
import type { IconType } from '@elastic/eui/src/components/icon/icon';
|
||||
import type { DataView, DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import { getActiveDatasourceIdFromDoc } from './utils';
|
||||
import type { EditorFrameService as EditorFrameServiceType } from './editor_frame_service';
|
||||
import type { OperationDescriptor } from './types';
|
||||
import type { LensSavedObjectAttributes } from '.';
|
||||
import type { DatasourceMap, OperationDescriptor, VisualizationMap } from './types';
|
||||
import { LensDocument } from './persistence';
|
||||
|
||||
export type ChartInfoApi = Promise<{
|
||||
getChartInfo: (vis: LensSavedObjectAttributes) => Promise<ChartInfo | undefined>;
|
||||
getChartInfo: (vis: LensDocument) => Promise<ChartInfo | undefined>;
|
||||
}>;
|
||||
|
||||
export interface ChartInfo {
|
||||
layers: ChartLayerDescriptor[];
|
||||
visualizationType: string;
|
||||
filters: Filter[];
|
||||
query: Query;
|
||||
query: Query | AggregateQuery;
|
||||
}
|
||||
|
||||
export interface ChartLayerDescriptor {
|
||||
|
@ -42,17 +41,14 @@ export interface ChartLayerDescriptor {
|
|||
|
||||
export const createChartInfoApi = async (
|
||||
dataViews: DataViewsPublicPluginStart,
|
||||
editorFrameService?: EditorFrameServiceType
|
||||
visualizationMap: VisualizationMap,
|
||||
datasourceMap: DatasourceMap
|
||||
): ChartInfoApi => {
|
||||
const [visualizationMap, datasourceMap] = await Promise.all([
|
||||
editorFrameService!.loadVisualizations(),
|
||||
editorFrameService!.loadDatasources(),
|
||||
]);
|
||||
return {
|
||||
async getChartInfo(vis: LensSavedObjectAttributes): Promise<ChartInfo | undefined> {
|
||||
async getChartInfo(vis: LensDocument): Promise<ChartInfo | undefined> {
|
||||
const lensVis = vis;
|
||||
const activeDatasourceId = getActiveDatasourceIdFromDoc(lensVis);
|
||||
if (!activeDatasourceId || !lensVis?.visualizationType) {
|
||||
if (!activeDatasourceId || lensVis?.visualizationType == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import { EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import { Query } from '@kbn/es-query';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { type DataView, DataViewField, FieldSpec } from '@kbn/data-plugin/common';
|
||||
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
|
@ -42,7 +43,7 @@ import { IndexPatternServiceAPI } from '../../data_views_service/service';
|
|||
import { FieldItem } from '../common/field_item';
|
||||
|
||||
export type FormBasedDataPanelProps = Omit<
|
||||
DatasourceDataPanelProps<FormBasedPrivateState>,
|
||||
DatasourceDataPanelProps<FormBasedPrivateState, Query>,
|
||||
'core' | 'onChangeIndexPattern'
|
||||
> & {
|
||||
data: DataPublicPluginStart;
|
||||
|
@ -185,7 +186,7 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({
|
|||
showNoDataPopover,
|
||||
activeIndexPatterns,
|
||||
}: Omit<
|
||||
DatasourceDataPanelProps,
|
||||
DatasourceDataPanelProps<unknown, Query>,
|
||||
'state' | 'setState' | 'core' | 'onChangeIndexPattern' | 'usedIndexPatterns'
|
||||
> & {
|
||||
data: DataPublicPluginStart;
|
||||
|
|
|
@ -51,6 +51,7 @@ import { Datatable, DatatableColumn } from '@kbn/expressions-plugin/common';
|
|||
import { filterAndSortUserMessages } from '../../app_plugin/get_application_user_messages';
|
||||
import { createMockFramePublicAPI } from '../../mocks';
|
||||
import { createMockDataViewsState } from '../../data_views_service/mocks';
|
||||
import { Query } from '@kbn/es-query';
|
||||
|
||||
jest.mock('./loader');
|
||||
jest.mock('../../id_generator');
|
||||
|
@ -193,7 +194,7 @@ const dateRange = {
|
|||
|
||||
describe('IndexPattern Data Source', () => {
|
||||
let baseState: FormBasedPrivateState;
|
||||
let FormBasedDatasource: Datasource<FormBasedPrivateState, FormBasedPersistedState>;
|
||||
let FormBasedDatasource: Datasource<FormBasedPrivateState, FormBasedPersistedState, Query>;
|
||||
|
||||
beforeEach(() => {
|
||||
const data = dataPluginMock.createStartContract();
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import React from 'react';
|
||||
import type { CoreStart, SavedObjectReference } from '@kbn/core/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { TimeRange } from '@kbn/es-query';
|
||||
import { Query, TimeRange } from '@kbn/es-query';
|
||||
import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
|
||||
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import { flatten, isEqual } from 'lodash';
|
||||
|
@ -28,7 +28,6 @@ import memoizeOne from 'memoize-one';
|
|||
import type {
|
||||
DatasourceDimensionEditorProps,
|
||||
DatasourceDimensionTriggerProps,
|
||||
DatasourceDataPanelProps,
|
||||
DatasourceLayerPanelProps,
|
||||
PublicAPIProps,
|
||||
OperationDescriptor,
|
||||
|
@ -40,6 +39,7 @@ import type {
|
|||
UserMessage,
|
||||
StateSetter,
|
||||
IndexPatternMap,
|
||||
DatasourceDataPanelProps,
|
||||
} from '../../types';
|
||||
import {
|
||||
changeIndexPattern,
|
||||
|
@ -217,7 +217,7 @@ export function getFormBasedDatasource({
|
|||
const ALIAS_IDS = ['indexpattern'];
|
||||
|
||||
// Not stateful. State is persisted to the frame
|
||||
const formBasedDatasource: Datasource<FormBasedPrivateState, FormBasedPersistedState> = {
|
||||
const formBasedDatasource: Datasource<FormBasedPrivateState, FormBasedPersistedState, Query> = {
|
||||
id: DATASOURCE_ID,
|
||||
alias: ALIAS_IDS,
|
||||
|
||||
|
@ -464,7 +464,7 @@ export function getFormBasedDatasource({
|
|||
LayerSettingsComponent(props) {
|
||||
return <LayerSettingsPanel {...props} />;
|
||||
},
|
||||
DataPanelComponent(props: DatasourceDataPanelProps<FormBasedPrivateState>) {
|
||||
DataPanelComponent(props: DatasourceDataPanelProps<FormBasedPrivateState, Query>) {
|
||||
const { onChangeIndexPattern, ...otherProps } = props;
|
||||
const layerFields = formBasedDatasource?.getSelectedFields?.(props.state);
|
||||
return (
|
||||
|
@ -869,13 +869,11 @@ export function getFormBasedDatasource({
|
|||
|
||||
getDatasourceInfo: async (state, references, dataViewsService) => {
|
||||
const layers = references ? injectReferences(state, references).layers : state.layers;
|
||||
const indexPatterns: DataView[] = [];
|
||||
for (const { indexPatternId } of Object.values(layers)) {
|
||||
const dataView = await dataViewsService?.get(indexPatternId);
|
||||
if (dataView) {
|
||||
indexPatterns.push(dataView);
|
||||
}
|
||||
}
|
||||
const indexPatterns: DataView[] = await Promise.all(
|
||||
Object.values(layers)
|
||||
.map(({ indexPatternId }) => dataViewsService?.get(indexPatternId))
|
||||
.filter(nonNullable)
|
||||
);
|
||||
return Object.entries(layers).reduce<DataSourceInfo[]>((acc, [key, layer]) => {
|
||||
const dataView = indexPatterns?.find(
|
||||
(indexPattern) => indexPattern.id === layer.indexPatternId
|
||||
|
|
|
@ -8,101 +8,83 @@
|
|||
import { getFieldByNameFactory } from './pure_helpers';
|
||||
import type { IndexPattern, IndexPatternField } from '../../types';
|
||||
|
||||
export function createMockedField(
|
||||
someProps: Partial<IndexPatternField> & Pick<IndexPatternField, 'name' | 'type'>
|
||||
) {
|
||||
return {
|
||||
displayName: someProps.name,
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
...someProps,
|
||||
};
|
||||
}
|
||||
|
||||
export const createMockedIndexPattern = (
|
||||
someProps?: Partial<IndexPattern>,
|
||||
customFields: IndexPatternField[] = []
|
||||
): IndexPattern => {
|
||||
const fields = [
|
||||
{
|
||||
createMockedField({
|
||||
name: 'timestamp',
|
||||
displayName: 'timestampLabel',
|
||||
type: 'date',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
}),
|
||||
createMockedField({
|
||||
name: 'start_date',
|
||||
displayName: 'start_date',
|
||||
type: 'date',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
}),
|
||||
createMockedField({
|
||||
name: 'bytes',
|
||||
displayName: 'bytes',
|
||||
type: 'number',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
}),
|
||||
createMockedField({
|
||||
name: 'memory',
|
||||
displayName: 'memory',
|
||||
type: 'number',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
esTypes: ['float'],
|
||||
},
|
||||
{
|
||||
}),
|
||||
createMockedField({
|
||||
name: 'source',
|
||||
displayName: 'source',
|
||||
type: 'string',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
esTypes: ['keyword'],
|
||||
},
|
||||
{
|
||||
}),
|
||||
createMockedField({
|
||||
name: 'unsupported',
|
||||
displayName: 'unsupported',
|
||||
type: 'geo',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
}),
|
||||
createMockedField({
|
||||
name: 'dest',
|
||||
displayName: 'dest',
|
||||
type: 'string',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
esTypes: ['keyword'],
|
||||
},
|
||||
{
|
||||
}),
|
||||
createMockedField({
|
||||
name: 'geo.src',
|
||||
displayName: 'geo.src',
|
||||
type: 'string',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
esTypes: ['keyword'],
|
||||
},
|
||||
{
|
||||
}),
|
||||
createMockedField({
|
||||
name: 'scripted',
|
||||
displayName: 'Scripted',
|
||||
type: 'string',
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
scripted: true,
|
||||
lang: 'painless' as const,
|
||||
script: '1234',
|
||||
},
|
||||
{
|
||||
}),
|
||||
createMockedField({
|
||||
name: 'runtime-keyword',
|
||||
displayName: 'Runtime keyword field',
|
||||
type: 'string',
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
runtime: true,
|
||||
lang: 'painless' as const,
|
||||
script: 'emit("123")',
|
||||
},
|
||||
{
|
||||
}),
|
||||
createMockedField({
|
||||
name: 'runtime-number',
|
||||
displayName: 'Runtime number field',
|
||||
type: 'number',
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
runtime: true,
|
||||
lang: 'painless' as const,
|
||||
script: 'emit(123)',
|
||||
},
|
||||
}),
|
||||
...(customFields || []),
|
||||
];
|
||||
return {
|
||||
|
@ -120,31 +102,23 @@ export const createMockedIndexPattern = (
|
|||
|
||||
export const createMockedRestrictedIndexPattern = () => {
|
||||
const fields = [
|
||||
{
|
||||
createMockedField({
|
||||
name: 'timestamp',
|
||||
displayName: 'timestampLabel',
|
||||
type: 'date',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
}),
|
||||
createMockedField({
|
||||
name: 'bytes',
|
||||
displayName: 'bytes',
|
||||
type: 'number',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
}),
|
||||
createMockedField({
|
||||
name: 'source',
|
||||
displayName: 'source',
|
||||
type: 'string',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
scripted: true,
|
||||
esTypes: ['keyword'],
|
||||
lang: 'painless' as const,
|
||||
script: '1234',
|
||||
},
|
||||
}),
|
||||
];
|
||||
return {
|
||||
id: '2',
|
||||
|
|
|
@ -362,7 +362,7 @@ export function getTextBasedDatasource({
|
|||
getUsedDataViews: (state) => {
|
||||
return Object.values(state.layers)
|
||||
.map(({ index }) => index)
|
||||
.filter((index) => index !== undefined) as string[];
|
||||
.filter(nonNullable);
|
||||
},
|
||||
|
||||
getPersistableState({ layers }: TextBasedPrivateState) {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import type { Query } from '@kbn/es-query';
|
||||
import { AggregateQuery, isOfAggregateQueryType, Query } from '@kbn/es-query';
|
||||
import { EuiErrorBoundary } from '@elastic/eui';
|
||||
|
||||
const Bee = React.lazy(() => import('./bee'));
|
||||
|
@ -34,11 +34,14 @@ function Bees({ query }: { query?: Query }) {
|
|||
);
|
||||
}
|
||||
|
||||
export function Easteregg(props: { query?: Query }) {
|
||||
export function Easteregg(props: { query?: Query | AggregateQuery }) {
|
||||
if (isOfAggregateQueryType(props.query)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
// Do not break Lens for an easteregg
|
||||
<EuiErrorBoundary style={{ display: 'none' }}>
|
||||
<Bees {...props} />
|
||||
<Bees query={props.query} />
|
||||
</EuiErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ import type {
|
|||
SuggestionRequest,
|
||||
} from '../../types';
|
||||
import { buildExpression } from './expression_helpers';
|
||||
import { Document } from '../../persistence/saved_object_store';
|
||||
import { LensDocument } from '../../persistence/saved_object_store';
|
||||
import { getActiveDatasourceIdFromDoc, sortDataViewRefs } from '../../utils';
|
||||
import type { DatasourceState, DatasourceStates, VisualizationState } from '../../state_management';
|
||||
import { readFromStorage } from '../../settings_storage';
|
||||
|
@ -353,12 +353,13 @@ export interface DocumentToExpressionReturnType {
|
|||
indexPatterns: IndexPatternMap;
|
||||
indexPatternRefs: IndexPatternRef[];
|
||||
activeVisualizationState: unknown;
|
||||
activeDatasourceState: unknown;
|
||||
}
|
||||
|
||||
export async function persistedStateToExpression(
|
||||
datasourceMap: DatasourceMap,
|
||||
visualizations: VisualizationMap,
|
||||
doc: Document,
|
||||
doc: LensDocument,
|
||||
services: {
|
||||
uiSettings: IUiSettingsClient;
|
||||
storage: IStorageWrapper;
|
||||
|
@ -381,7 +382,13 @@ export async function persistedStateToExpression(
|
|||
description,
|
||||
} = doc;
|
||||
if (!visualizationType) {
|
||||
return { ast: null, indexPatterns: {}, indexPatternRefs: [], activeVisualizationState: null };
|
||||
return {
|
||||
ast: null,
|
||||
indexPatterns: {},
|
||||
indexPatternRefs: [],
|
||||
activeVisualizationState: null,
|
||||
activeDatasourceState: null,
|
||||
};
|
||||
}
|
||||
|
||||
const annotationGroups = await initializeEventAnnotationGroups(
|
||||
|
@ -435,6 +442,7 @@ export async function persistedStateToExpression(
|
|||
indexPatterns,
|
||||
indexPatternRefs,
|
||||
activeVisualizationState,
|
||||
activeDatasourceState: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -454,6 +462,7 @@ export async function persistedStateToExpression(
|
|||
nowInstant: services.nowProvider.get(),
|
||||
}),
|
||||
activeVisualizationState,
|
||||
activeDatasourceState: datasourceStates[datasourceId]?.state,
|
||||
indexPatterns,
|
||||
indexPatternRefs,
|
||||
};
|
||||
|
|
|
@ -248,7 +248,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
const removeExpressionBuildErrorsRef = useRef<() => void>();
|
||||
|
||||
const onData$ = useCallback(
|
||||
(_data: unknown, adapters?: Partial<DefaultInspectorAdapters>) => {
|
||||
(_data: unknown, adapters?: DefaultInspectorAdapters) => {
|
||||
if (renderDeps.current) {
|
||||
dataReceivedTime.current = performance.now();
|
||||
|
||||
|
@ -283,10 +283,11 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
dispatchLens(
|
||||
onActiveDataChange({
|
||||
activeData: Object.entries(adapters.tables?.tables).reduce<Record<string, Datatable>>(
|
||||
(acc, [key, value], _index, tables) => ({
|
||||
...acc,
|
||||
[tables.length === 1 ? defaultLayerId : key]: value,
|
||||
}),
|
||||
(acc, [key, value], _index, tables) => {
|
||||
const id = tables.length === 1 ? defaultLayerId : key;
|
||||
acc[id] = value as Datatable;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
),
|
||||
})
|
||||
|
@ -726,7 +727,7 @@ export const VisualizationWrapper = ({
|
|||
ExpressionRendererComponent: ReactExpressionRendererType;
|
||||
core: CoreStart;
|
||||
onRender$: () => void;
|
||||
onData$: (data: unknown, adapters?: Partial<DefaultInspectorAdapters>) => void;
|
||||
onData$: (data: unknown, adapters?: DefaultInspectorAdapters) => void;
|
||||
onComponentRendered: () => void;
|
||||
displayOptions: VisualizationDisplayOptions | undefined;
|
||||
}) => {
|
||||
|
@ -788,7 +789,7 @@ export const VisualizationWrapper = ({
|
|||
// @ts-expect-error upgrade typescript v4.9.5
|
||||
onData$={onData$}
|
||||
onRender$={onRenderHandler}
|
||||
inspectorAdapters={lensInspector.adapters}
|
||||
inspectorAdapters={lensInspector.getInspectorAdapters()}
|
||||
executionContext={executionContext}
|
||||
renderMode="edit"
|
||||
renderError={(errorMessage?: string | null, error?: ExpressionRenderError | null) => {
|
||||
|
|
|
@ -24,7 +24,7 @@ import {
|
|||
DataViewsPublicPluginStart,
|
||||
} from '@kbn/data-views-plugin/public';
|
||||
import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
|
||||
import { Document } from '../persistence/saved_object_store';
|
||||
import { LensDocument } from '../persistence/saved_object_store';
|
||||
import {
|
||||
Datasource,
|
||||
Visualization,
|
||||
|
@ -93,7 +93,7 @@ export class EditorFrameService {
|
|||
* This is an asynchronous process.
|
||||
* @param doc parsed Lens saved object
|
||||
*/
|
||||
public documentToExpression = async (doc: Document, services: EditorFramePlugins) => {
|
||||
public documentToExpression = async (doc: LensDocument, services: EditorFramePlugins) => {
|
||||
const [resolvedDatasources, resolvedVisualizations] = await Promise.all([
|
||||
this.loadDatasources(),
|
||||
this.loadVisualizations(),
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,188 +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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC, useEffect } from 'react';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import type { Action, UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import type { Start as InspectorStartContract } from '@kbn/inspector-plugin/public';
|
||||
import { PanelLoader } from '@kbn/panel-loader';
|
||||
import { EuiLoadingChart } from '@elastic/eui';
|
||||
import {
|
||||
EmbeddableFactory,
|
||||
EmbeddableInput,
|
||||
EmbeddableOutput,
|
||||
EmbeddablePanel,
|
||||
EmbeddableRoot,
|
||||
EmbeddableStart,
|
||||
IEmbeddable,
|
||||
useEmbeddableFactory,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import type { LensByReferenceInput, LensByValueInput } from './embeddable';
|
||||
import type { Document } from '../persistence';
|
||||
import type { FormBasedPersistedState } from '../datasources/form_based/types';
|
||||
import type { TextBasedPersistedState } from '../datasources/text_based/types';
|
||||
import type { XYState } from '../visualizations/xy/types';
|
||||
import type {
|
||||
PieVisualizationState,
|
||||
LegacyMetricState,
|
||||
AllowedGaugeOverrides,
|
||||
AllowedPartitionOverrides,
|
||||
AllowedSettingsOverrides,
|
||||
AllowedXYOverrides,
|
||||
} from '../../common/types';
|
||||
import type { DatatableVisualizationState } from '../visualizations/datatable/visualization';
|
||||
import type { MetricVisualizationState } from '../visualizations/metric/types';
|
||||
import type { HeatmapVisualizationState } from '../visualizations/heatmap/types';
|
||||
import type { GaugeVisualizationState } from '../visualizations/gauge/constants';
|
||||
|
||||
type LensAttributes<TVisType, TVisState> = Omit<
|
||||
Document,
|
||||
'savedObjectId' | 'type' | 'state' | 'visualizationType'
|
||||
> & {
|
||||
visualizationType: TVisType;
|
||||
state: Omit<Document['state'], 'datasourceStates' | 'visualization'> & {
|
||||
datasourceStates: {
|
||||
formBased?: FormBasedPersistedState;
|
||||
textBased?: TextBasedPersistedState;
|
||||
};
|
||||
visualization: TVisState;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Type-safe variant of by value embeddable input for Lens.
|
||||
* This can be used to hardcode certain Lens chart configurations within another app.
|
||||
*/
|
||||
export type TypedLensByValueInput = Omit<LensByValueInput, 'attributes' | 'overrides'> & {
|
||||
attributes:
|
||||
| LensAttributes<'lnsXY', XYState>
|
||||
| LensAttributes<'lnsPie', PieVisualizationState>
|
||||
| LensAttributes<'lnsHeatmap', HeatmapVisualizationState>
|
||||
| LensAttributes<'lnsGauge', GaugeVisualizationState>
|
||||
| LensAttributes<'lnsDatatable', DatatableVisualizationState>
|
||||
| LensAttributes<'lnsLegacyMetric', LegacyMetricState>
|
||||
| LensAttributes<'lnsMetric', MetricVisualizationState>
|
||||
| LensAttributes<string, unknown>;
|
||||
|
||||
/**
|
||||
* Overrides can tweak the style of the final embeddable and are executed at the end of the Lens rendering pipeline.
|
||||
* XY charts offer an override of the Settings ('settings') and Axis ('axisX', 'axisLeft', 'axisRight') components.
|
||||
* While it is not possible to pass function/callback/handlers to the renderer, it is possible to stop them by passing the
|
||||
* "ignore" string as override value (i.e. onBrushEnd: "ignore")
|
||||
*/
|
||||
overrides?:
|
||||
| AllowedSettingsOverrides
|
||||
| AllowedXYOverrides
|
||||
| AllowedPartitionOverrides
|
||||
| AllowedGaugeOverrides;
|
||||
};
|
||||
|
||||
export type EmbeddableComponentProps = (TypedLensByValueInput | LensByReferenceInput) & {
|
||||
withDefaultActions?: boolean;
|
||||
extraActions?: Action[];
|
||||
showInspector?: boolean;
|
||||
abortController?: AbortController;
|
||||
};
|
||||
|
||||
export type EmbeddableComponent = React.ComponentType<EmbeddableComponentProps>;
|
||||
|
||||
interface PluginsStartDependencies {
|
||||
uiActions: UiActionsStart;
|
||||
embeddable: EmbeddableStart;
|
||||
inspector: InspectorStartContract;
|
||||
}
|
||||
|
||||
export function getEmbeddableComponent(core: CoreStart, plugins: PluginsStartDependencies) {
|
||||
const { embeddable: embeddableStart, uiActions } = plugins;
|
||||
const factory = embeddableStart.getEmbeddableFactory('lens')!;
|
||||
return (props: EmbeddableComponentProps) => {
|
||||
const input = { ...props };
|
||||
const hasActions =
|
||||
Boolean(input.withDefaultActions) || (input.extraActions && input.extraActions?.length > 0);
|
||||
|
||||
if (hasActions) {
|
||||
return (
|
||||
<EmbeddablePanelWrapper
|
||||
factory={factory}
|
||||
uiActions={uiActions}
|
||||
actionPredicate={() => hasActions}
|
||||
input={input}
|
||||
extraActions={input.extraActions}
|
||||
showInspector={input.showInspector}
|
||||
withDefaultActions={input.withDefaultActions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <EmbeddableRootWrapper factory={factory} input={input} />;
|
||||
};
|
||||
}
|
||||
|
||||
function EmbeddableRootWrapper({
|
||||
factory,
|
||||
input,
|
||||
}: {
|
||||
factory: EmbeddableFactory<EmbeddableInput, EmbeddableOutput>;
|
||||
input: EmbeddableComponentProps;
|
||||
}) {
|
||||
const [embeddable, loading, error] = useEmbeddableFactory({ factory, input });
|
||||
if (loading) {
|
||||
return <EuiLoadingChart />;
|
||||
}
|
||||
return <EmbeddableRoot embeddable={embeddable} loading={loading} error={error} input={input} />;
|
||||
}
|
||||
|
||||
interface EmbeddablePanelWrapperProps {
|
||||
factory: EmbeddableFactory<EmbeddableInput, EmbeddableOutput>;
|
||||
uiActions: PluginsStartDependencies['uiActions'];
|
||||
actionPredicate: (id: string) => boolean;
|
||||
input: EmbeddableComponentProps;
|
||||
extraActions?: Action[];
|
||||
showInspector?: boolean;
|
||||
withDefaultActions?: boolean;
|
||||
abortController?: AbortController;
|
||||
}
|
||||
|
||||
const EmbeddablePanelWrapper: FC<EmbeddablePanelWrapperProps> = ({
|
||||
factory,
|
||||
uiActions,
|
||||
actionPredicate,
|
||||
input,
|
||||
extraActions,
|
||||
showInspector = true,
|
||||
withDefaultActions,
|
||||
abortController,
|
||||
}) => {
|
||||
const [embeddable, loading] = useEmbeddableFactory({ factory, input });
|
||||
useEffect(() => {
|
||||
if (embeddable) {
|
||||
embeddable.updateInput(input);
|
||||
}
|
||||
}, [embeddable, input]);
|
||||
|
||||
if (loading || !embeddable) {
|
||||
return <PanelLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<EmbeddablePanel
|
||||
hideHeader={false}
|
||||
embeddable={embeddable as IEmbeddable<EmbeddableInput, EmbeddableOutput>}
|
||||
getActions={async (triggerId, context) => {
|
||||
const actions = withDefaultActions
|
||||
? await uiActions.getTriggerCompatibleActions(triggerId, context)
|
||||
: [];
|
||||
|
||||
return [...(extraActions ?? []), ...actions];
|
||||
}}
|
||||
hideInspector={!showInspector}
|
||||
actionPredicate={actionPredicate}
|
||||
showNotifications={false}
|
||||
showShadow={false}
|
||||
showBadges={false}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -1,157 +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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type {
|
||||
Capabilities,
|
||||
CoreStart,
|
||||
HttpSetup,
|
||||
IUiSettingsClient,
|
||||
ThemeServiceStart,
|
||||
} from '@kbn/core/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { RecursiveReadonly } from '@kbn/utility-types';
|
||||
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
|
||||
import { DataPublicPluginStart, FilterManager, TimefilterContract } from '@kbn/data-plugin/public';
|
||||
import type { DataViewsContract } from '@kbn/data-views-plugin/public';
|
||||
import { ReactExpressionRendererType } from '@kbn/expressions-plugin/public';
|
||||
import {
|
||||
EmbeddableFactoryDefinition,
|
||||
IContainer,
|
||||
ErrorEmbeddable,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import type { Start as InspectorStart } from '@kbn/inspector-plugin/public';
|
||||
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
|
||||
import type { LensByReferenceInput, LensEmbeddableInput } from './embeddable';
|
||||
import type { Document } from '../persistence/saved_object_store';
|
||||
import type { LensAttributeService } from '../lens_attribute_service';
|
||||
import { DOC_TYPE } from '../../common/constants';
|
||||
import { extract, inject } from '../../common/embeddable_factory';
|
||||
import type { DatasourceMap, VisualizationMap } from '../types';
|
||||
import type { DocumentToExpressionReturnType } from '../editor_frame_service/editor_frame';
|
||||
|
||||
export interface LensEmbeddableStartServices {
|
||||
data: DataPublicPluginStart;
|
||||
timefilter: TimefilterContract;
|
||||
coreHttp: HttpSetup;
|
||||
coreStart: CoreStart;
|
||||
inspector: InspectorStart;
|
||||
attributeService: LensAttributeService;
|
||||
capabilities: RecursiveReadonly<Capabilities>;
|
||||
expressionRenderer: ReactExpressionRendererType;
|
||||
dataViews: DataViewsContract;
|
||||
uiActions?: UiActionsStart;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
documentToExpression: (doc: Document) => Promise<DocumentToExpressionReturnType>;
|
||||
injectFilterReferences: FilterManager['inject'];
|
||||
visualizationMap: VisualizationMap;
|
||||
datasourceMap: DatasourceMap;
|
||||
spaces?: SpacesPluginStart;
|
||||
theme: ThemeServiceStart;
|
||||
uiSettings: IUiSettingsClient;
|
||||
}
|
||||
|
||||
export class EmbeddableFactory implements EmbeddableFactoryDefinition {
|
||||
type = DOC_TYPE;
|
||||
savedObjectMetaData = {
|
||||
name: i18n.translate('xpack.lens.lensSavedObjectLabel', {
|
||||
defaultMessage: 'Lens Visualization',
|
||||
}),
|
||||
type: DOC_TYPE,
|
||||
getIconForSavedObject: () => 'lensApp',
|
||||
};
|
||||
|
||||
constructor(private getStartServices: () => Promise<LensEmbeddableStartServices>) {}
|
||||
|
||||
public isEditable = async () => {
|
||||
const { capabilities } = await this.getStartServices();
|
||||
return Boolean(capabilities.visualize.save || capabilities.dashboard?.showWriteControls);
|
||||
};
|
||||
|
||||
canCreateNew() {
|
||||
return false;
|
||||
}
|
||||
|
||||
getDisplayName() {
|
||||
return i18n.translate('xpack.lens.embeddableDisplayName', {
|
||||
defaultMessage: 'Lens',
|
||||
});
|
||||
}
|
||||
|
||||
createFromSavedObject = async (
|
||||
savedObjectId: string,
|
||||
input: LensEmbeddableInput,
|
||||
parent?: IContainer
|
||||
) => {
|
||||
if (!(input as LensByReferenceInput).savedObjectId) {
|
||||
(input as LensByReferenceInput).savedObjectId = savedObjectId;
|
||||
}
|
||||
return this.create(input, parent);
|
||||
};
|
||||
|
||||
async create(input: LensEmbeddableInput, parent?: IContainer) {
|
||||
try {
|
||||
const {
|
||||
data,
|
||||
timefilter,
|
||||
expressionRenderer,
|
||||
documentToExpression,
|
||||
injectFilterReferences,
|
||||
visualizationMap,
|
||||
datasourceMap,
|
||||
uiActions,
|
||||
coreHttp,
|
||||
coreStart,
|
||||
attributeService,
|
||||
dataViews,
|
||||
capabilities,
|
||||
usageCollection,
|
||||
inspector,
|
||||
spaces,
|
||||
uiSettings,
|
||||
} = await this.getStartServices();
|
||||
|
||||
const { Embeddable } = await import('../async_services');
|
||||
|
||||
return new Embeddable(
|
||||
{
|
||||
attributeService,
|
||||
data,
|
||||
dataViews,
|
||||
timefilter,
|
||||
inspector,
|
||||
expressionRenderer,
|
||||
basePath: coreHttp.basePath,
|
||||
getTrigger: uiActions?.getTrigger,
|
||||
getTriggerCompatibleActions: uiActions?.getTriggerCompatibleActions,
|
||||
documentToExpression,
|
||||
injectFilterReferences,
|
||||
visualizationMap,
|
||||
datasourceMap,
|
||||
capabilities: {
|
||||
canSaveDashboards: Boolean(capabilities.dashboard?.showWriteControls),
|
||||
canSaveVisualizations: Boolean(capabilities.visualize.save),
|
||||
canOpenVisualizations: Boolean(capabilities.visualize.show),
|
||||
navLinks: capabilities.navLinks,
|
||||
discover: capabilities.discover,
|
||||
},
|
||||
coreStart,
|
||||
usageCollection,
|
||||
spaces,
|
||||
uiSettings,
|
||||
},
|
||||
input,
|
||||
parent
|
||||
);
|
||||
} catch (e) {
|
||||
return new ErrorEmbeddable(e, input, parent);
|
||||
}
|
||||
}
|
||||
|
||||
extract = extract;
|
||||
inject = inject;
|
||||
}
|
|
@ -1,10 +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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './embeddable';
|
||||
|
||||
export { type LensApi, isLensApi } from './interfaces/lens_api';
|
|
@ -1,45 +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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type {
|
||||
HasParentApi,
|
||||
HasType,
|
||||
PublishesUnifiedSearch,
|
||||
PublishesPanelTitle,
|
||||
PublishingSubject,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import {
|
||||
apiIsOfType,
|
||||
apiPublishesUnifiedSearch,
|
||||
apiPublishesPanelTitle,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { LensSavedObjectAttributes, ViewUnderlyingDataArgs } from '../embeddable';
|
||||
|
||||
export type HasLensConfig = HasType<'lens'> & {
|
||||
getSavedVis: () => Readonly<LensSavedObjectAttributes | undefined>;
|
||||
canViewUnderlyingData$: PublishingSubject<boolean>;
|
||||
getViewUnderlyingDataArgs: () => ViewUnderlyingDataArgs;
|
||||
getFullAttributes: () => LensSavedObjectAttributes | undefined;
|
||||
};
|
||||
|
||||
export type LensApi = HasLensConfig &
|
||||
PublishesPanelTitle &
|
||||
PublishesUnifiedSearch &
|
||||
Partial<HasParentApi<Partial<PublishesUnifiedSearch>>>;
|
||||
|
||||
export const isLensApi = (api: unknown): api is LensApi => {
|
||||
return Boolean(
|
||||
api &&
|
||||
apiIsOfType(api, 'lens') &&
|
||||
typeof (api as HasLensConfig).getSavedVis === 'function' &&
|
||||
(api as HasLensConfig).canViewUnderlyingData$ &&
|
||||
typeof (api as HasLensConfig).getViewUnderlyingDataArgs === 'function' &&
|
||||
typeof (api as HasLensConfig).getFullAttributes === 'function' &&
|
||||
apiPublishesPanelTitle(api) &&
|
||||
apiPublishesUnifiedSearch(api)
|
||||
);
|
||||
};
|
|
@ -7,12 +7,21 @@
|
|||
|
||||
import { LensPlugin } from './plugin';
|
||||
|
||||
export { isLensApi } from './embeddable/interfaces/lens_api';
|
||||
export { isLensApi } from './react_embeddable/type_guards';
|
||||
export { type EmbeddableComponent } from './react_embeddable/renderer/lens_custom_renderer_component';
|
||||
export type {
|
||||
EmbeddableComponentProps,
|
||||
EmbeddableComponent,
|
||||
LensApi,
|
||||
LensSerializedState,
|
||||
LensRuntimeState,
|
||||
LensByValueInput,
|
||||
LensByReferenceInput,
|
||||
TypedLensByValueInput,
|
||||
} from './embeddable/embeddable_component';
|
||||
LensEmbeddableInput,
|
||||
LensEmbeddableOutput,
|
||||
LensSavedObjectAttributes,
|
||||
LensRendererProps as EmbeddableComponentProps,
|
||||
} from './react_embeddable/types';
|
||||
|
||||
export type {
|
||||
XYState,
|
||||
XYReferenceLineLayerConfig,
|
||||
|
@ -110,14 +119,6 @@ export type {
|
|||
|
||||
export type { InlineEditLensEmbeddableContext } from './trigger_actions/open_lens_config/in_app_embeddable_edit/types';
|
||||
|
||||
export type {
|
||||
LensApi,
|
||||
LensEmbeddableInput,
|
||||
LensSavedObjectAttributes,
|
||||
Embeddable,
|
||||
LensEmbeddableOutput,
|
||||
} from './embeddable';
|
||||
|
||||
export type { ChartInfo } from './chart_info_api';
|
||||
|
||||
export { layerTypes } from '../common/layer_types';
|
||||
|
|
|
@ -6,27 +6,52 @@
|
|||
*/
|
||||
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import type { AttributeService } from '@kbn/embeddable-plugin/public';
|
||||
import type { SavedObjectReference } from '@kbn/core/types';
|
||||
import { OnSaveProps } from '@kbn/saved-objects-plugin/public';
|
||||
import { SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common';
|
||||
import { noop } from 'lodash';
|
||||
import { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common';
|
||||
import type { LensPluginStartDependencies } from './plugin';
|
||||
import type { LensSavedObjectAttributes as LensSavedObjectAttributesWithoutReferences } from '../common/content_management';
|
||||
import type {
|
||||
LensSavedObjectAttributes,
|
||||
LensByValueInput,
|
||||
LensUnwrapMetaInfo,
|
||||
LensUnwrapResult,
|
||||
LensByReferenceInput,
|
||||
} from './embeddable/embeddable';
|
||||
LensSavedObject,
|
||||
LensSavedObjectAttributes as LensSavedObjectAttributesWithoutReferences,
|
||||
} from '../common/content_management';
|
||||
import { extract, inject } from '../common/embeddable_factory';
|
||||
import { SavedObjectIndexStore, checkForDuplicateTitle } from './persistence';
|
||||
import { DOC_TYPE } from '../common/constants';
|
||||
import { SharingSavedObjectProps } from './types';
|
||||
import { LensRuntimeState, LensSavedObjectAttributes } from './react_embeddable/types';
|
||||
|
||||
export type LensAttributeService = AttributeService<
|
||||
LensSavedObjectAttributes,
|
||||
LensByValueInput,
|
||||
LensByReferenceInput,
|
||||
LensUnwrapMetaInfo
|
||||
>;
|
||||
type Reference = LensSavedObject['references'][number];
|
||||
|
||||
type CheckDuplicateTitleProps = OnSaveProps & {
|
||||
id?: string;
|
||||
displayName: string;
|
||||
lastSavedTitle: string;
|
||||
copyOnSave: boolean;
|
||||
};
|
||||
|
||||
export interface LensAttributesService {
|
||||
loadFromLibrary: (savedObjectId: string) => Promise<{
|
||||
attributes: LensSavedObjectAttributes;
|
||||
sharingSavedObjectProps: SharingSavedObjectProps;
|
||||
managed: boolean;
|
||||
}>;
|
||||
saveToLibrary: (
|
||||
attributes: LensSavedObjectAttributesWithoutReferences,
|
||||
references: Reference[],
|
||||
savedObjectId?: string
|
||||
) => Promise<string>;
|
||||
checkForDuplicateTitle: (props: CheckDuplicateTitleProps) => Promise<{ isDuplicate: boolean }>;
|
||||
injectReferences: (
|
||||
runtimeState: LensRuntimeState,
|
||||
references: SavedObjectReference[] | undefined
|
||||
) => LensRuntimeState;
|
||||
extractReferences: (runtimeState: LensRuntimeState) => {
|
||||
rawState: LensRuntimeState;
|
||||
references: SavedObjectReference[];
|
||||
};
|
||||
}
|
||||
|
||||
export const savedObjectToEmbeddableAttributes = (
|
||||
savedObject: SavedObjectCommon<LensSavedObjectAttributesWithoutReferences>
|
||||
|
@ -41,60 +66,86 @@ export const savedObjectToEmbeddableAttributes = (
|
|||
export function getLensAttributeService(
|
||||
core: CoreStart,
|
||||
startDependencies: LensPluginStartDependencies
|
||||
): LensAttributeService {
|
||||
): LensAttributesService {
|
||||
const savedObjectStore = new SavedObjectIndexStore(startDependencies.contentManagement);
|
||||
|
||||
return startDependencies.embeddable.getAttributeService<
|
||||
LensSavedObjectAttributes,
|
||||
LensByValueInput,
|
||||
LensByReferenceInput,
|
||||
LensUnwrapMetaInfo
|
||||
>(DOC_TYPE, {
|
||||
saveMethod: async (attributes: LensSavedObjectAttributes, savedObjectId?: string) => {
|
||||
const savedDoc = await savedObjectStore.save({
|
||||
...attributes,
|
||||
savedObjectId,
|
||||
type: DOC_TYPE,
|
||||
});
|
||||
return { id: savedDoc.savedObjectId };
|
||||
},
|
||||
unwrapMethod: async (savedObjectId: string): Promise<LensUnwrapResult> => {
|
||||
const {
|
||||
item: savedObject,
|
||||
meta: { outcome, aliasTargetId, aliasPurpose },
|
||||
} = await savedObjectStore.load(savedObjectId);
|
||||
const { id } = savedObject;
|
||||
|
||||
const sharingSavedObjectProps = {
|
||||
aliasTargetId,
|
||||
outcome,
|
||||
aliasPurpose,
|
||||
sourceId: id,
|
||||
};
|
||||
|
||||
return {
|
||||
loadFromLibrary: async (
|
||||
savedObjectId: string
|
||||
): Promise<{
|
||||
attributes: LensSavedObjectAttributes;
|
||||
sharingSavedObjectProps: SharingSavedObjectProps;
|
||||
managed: boolean;
|
||||
}> => {
|
||||
const { meta, item } = await savedObjectStore.load(savedObjectId);
|
||||
return {
|
||||
attributes: savedObjectToEmbeddableAttributes(savedObject),
|
||||
metaInfo: {
|
||||
sharingSavedObjectProps,
|
||||
managed: savedObject.managed,
|
||||
attributes: {
|
||||
...item.attributes,
|
||||
state: item.attributes.state as LensSavedObjectAttributes['state'],
|
||||
references: item.references,
|
||||
},
|
||||
sharingSavedObjectProps: {
|
||||
aliasTargetId: meta.aliasTargetId,
|
||||
outcome: meta.outcome,
|
||||
aliasPurpose: meta.aliasPurpose,
|
||||
sourceId: item.id,
|
||||
},
|
||||
managed: Boolean(item.managed),
|
||||
};
|
||||
},
|
||||
checkForDuplicateTitle: (props: OnSaveProps) => {
|
||||
return checkForDuplicateTitle(
|
||||
{
|
||||
title: props.newTitle,
|
||||
displayName: DOC_TYPE,
|
||||
isTitleDuplicateConfirmed: props.isTitleDuplicateConfirmed,
|
||||
lastSavedTitle: '',
|
||||
copyOnSave: false,
|
||||
},
|
||||
props.onTitleDuplicate,
|
||||
{
|
||||
client: savedObjectStore,
|
||||
...core,
|
||||
}
|
||||
);
|
||||
saveToLibrary: async (
|
||||
attributes: LensSavedObjectAttributesWithoutReferences,
|
||||
references: Reference[],
|
||||
savedObjectId?: string
|
||||
) => {
|
||||
const result = await savedObjectStore.save({
|
||||
...attributes,
|
||||
state: attributes.state as LensSavedObjectAttributes['state'],
|
||||
references,
|
||||
savedObjectId,
|
||||
});
|
||||
return result.savedObjectId;
|
||||
},
|
||||
});
|
||||
checkForDuplicateTitle: async ({
|
||||
newTitle,
|
||||
isTitleDuplicateConfirmed,
|
||||
onTitleDuplicate = noop,
|
||||
displayName = DOC_TYPE,
|
||||
lastSavedTitle = '',
|
||||
copyOnSave = false,
|
||||
id,
|
||||
}: CheckDuplicateTitleProps) => {
|
||||
return {
|
||||
isDuplicate: await checkForDuplicateTitle(
|
||||
{
|
||||
id,
|
||||
title: newTitle,
|
||||
isTitleDuplicateConfirmed,
|
||||
displayName,
|
||||
lastSavedTitle,
|
||||
copyOnSave,
|
||||
},
|
||||
onTitleDuplicate,
|
||||
{
|
||||
client: savedObjectStore,
|
||||
...core,
|
||||
}
|
||||
),
|
||||
};
|
||||
},
|
||||
// Make sure to inject references from the container down to the runtime state
|
||||
// this ensure migrations/copy to spaces works correctly
|
||||
injectReferences: (runtimeState, references) => {
|
||||
return inject(
|
||||
runtimeState as unknown as EmbeddableStateWithType,
|
||||
references ?? runtimeState.attributes.references
|
||||
) as unknown as LensRuntimeState;
|
||||
},
|
||||
// Make sure to move the internal references into the parent references
|
||||
// so migrations/move to spaces can work properly
|
||||
extractReferences: (runtimeState) => {
|
||||
const { state, references } = extract(runtimeState as unknown as EmbeddableStateWithType);
|
||||
return { rawState: state as unknown as LensRuntimeState, references };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ export const getLensInspectorService = (inspector: InspectorStartContract) => {
|
|||
const adapters: Adapters = createDefaultInspectorAdapters();
|
||||
let overlayRef: InspectorSession | undefined;
|
||||
return {
|
||||
adapters,
|
||||
getInspectorAdapters: () => adapters,
|
||||
inspect: (options?: InspectorOptions) => {
|
||||
overlayRef = inspector.open(adapters, options);
|
||||
overlayRef.onClose.then(() => {
|
||||
|
@ -28,7 +28,7 @@ export const getLensInspectorService = (inspector: InspectorStartContract) => {
|
|||
});
|
||||
return overlayRef;
|
||||
},
|
||||
close: () => overlayRef?.close(),
|
||||
closeInspector: async () => overlayRef?.close(),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import type { DatatableColumn } from '@kbn/expressions-plugin/common';
|
||||
import { mergeSuggestionWithVisContext } from './helpers';
|
||||
import { mockAllSuggestions } from '../mocks';
|
||||
import type { TypedLensByValueInput } from '../embeddable/embeddable_component';
|
||||
import { TypedLensByValueInput } from '../react_embeddable/types';
|
||||
|
||||
const context = {
|
||||
dataViewSpec: {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import type { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public';
|
||||
import { getDatasourceId } from '@kbn/visualization-utils';
|
||||
import type { VisualizeEditorContext, Suggestion } from '../types';
|
||||
import type { TypedLensByValueInput } from '../embeddable/embeddable_component';
|
||||
import { TypedLensByValueInput } from '../react_embeddable/types';
|
||||
|
||||
/**
|
||||
* Returns the suggestion updated with external visualization state for ES|QL charts
|
||||
|
|
|
@ -10,7 +10,7 @@ import type { ChartType } from '@kbn/visualization-utils';
|
|||
import { getSuggestions } from '../editor_frame_service/editor_frame/suggestion_helpers';
|
||||
import type { DatasourceMap, VisualizationMap, VisualizeEditorContext } from '../types';
|
||||
import type { DataViewsState } from '../state_management';
|
||||
import type { TypedLensByValueInput } from '../embeddable/embeddable_component';
|
||||
import type { TypedLensByValueInput } from '../react_embeddable/types';
|
||||
import { mergeSuggestionWithVisContext } from './helpers';
|
||||
|
||||
interface SuggestionsApiProps {
|
||||
|
|
|
@ -10,7 +10,7 @@ import { ChartType } from '@kbn/visualization-utils';
|
|||
import { createMockVisualization, DatasourceMock, createMockDatasource } from '../mocks';
|
||||
import { DatasourceSuggestion } from '../types';
|
||||
import { suggestionsApi } from '.';
|
||||
import type { TypedLensByValueInput } from '../embeddable/embeddable_component';
|
||||
import { TypedLensByValueInput } from '../react_embeddable/types';
|
||||
|
||||
const generateSuggestion = (state = {}, layerId: string = 'first'): DatasourceSuggestion => ({
|
||||
state,
|
||||
|
|
|
@ -48,13 +48,13 @@ export function mockDataPlugin(
|
|||
function createMockSearchService() {
|
||||
let sessionIdCounter = initialSessionId ? 1 : 0;
|
||||
let currentSessionId: string | undefined = initialSessionId;
|
||||
const start = () => {
|
||||
currentSessionId = `sessionId-${++sessionIdCounter}`;
|
||||
return currentSessionId;
|
||||
};
|
||||
|
||||
return {
|
||||
session: {
|
||||
start: jest.fn(start),
|
||||
start: jest.fn(() => {
|
||||
currentSessionId = `sessionId-${++sessionIdCounter}`;
|
||||
return currentSessionId;
|
||||
}),
|
||||
clear: jest.fn(),
|
||||
getSessionId: jest.fn(() => currentSessionId),
|
||||
getSession$: jest.fn(() => sessionIdSubject.asObservable()),
|
||||
|
@ -146,5 +146,6 @@ export function mockDataPlugin(
|
|||
fieldFormats: {
|
||||
deserialize: jest.fn(),
|
||||
},
|
||||
datatableUtilities: { getDateHistogramMeta: jest.fn(() => true) },
|
||||
} as unknown as DataPublicPluginStart;
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ type Start = jest.Mocked<LensPublicStart>;
|
|||
export const lensPluginMock = {
|
||||
createStartContract: (): Start => {
|
||||
const startContract: Start = {
|
||||
EmbeddableComponent: jest.fn(() => {
|
||||
EmbeddableComponent: jest.fn((props) => {
|
||||
return <span>Lens Embeddable Component</span>;
|
||||
}),
|
||||
SaveModalComponent: jest.fn(() => {
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Subject } from 'rxjs';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { navigationPluginMock } from '@kbn/navigation-plugin/public/mocks';
|
||||
|
@ -20,46 +19,35 @@ import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
|
|||
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks';
|
||||
|
||||
import {
|
||||
mockAttributeService,
|
||||
createEmbeddableStateTransferMock,
|
||||
} from '@kbn/embeddable-plugin/public/mocks';
|
||||
import { createEmbeddableStateTransferMock } from '@kbn/embeddable-plugin/public/mocks';
|
||||
import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks';
|
||||
import type { EmbeddableStateTransfer } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
import { presentationUtilPluginMock } from '@kbn/presentation-util-plugin/public/mocks';
|
||||
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
|
||||
import type { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
|
||||
import type { LensAttributeService } from '../lens_attribute_service';
|
||||
import type {
|
||||
LensByValueInput,
|
||||
LensByReferenceInput,
|
||||
LensSavedObjectAttributes,
|
||||
LensUnwrapMetaInfo,
|
||||
} from '../embeddable/embeddable';
|
||||
import { DOC_TYPE } from '../../common/constants';
|
||||
|
||||
import { LensAppServices } from '../app_plugin/types';
|
||||
import { mockDataPlugin } from './data_plugin_mock';
|
||||
import { getLensInspectorService } from '../lens_inspector_service';
|
||||
import { SavedObjectIndexStore } from '../persistence';
|
||||
import { LensDocument, SavedObjectIndexStore } from '../persistence';
|
||||
import { LensAttributesService } from '../lens_attribute_service';
|
||||
import { mockDatasourceStates } from './store_mocks';
|
||||
|
||||
const startMock = coreMock.createStart();
|
||||
|
||||
export const defaultDoc = {
|
||||
export const defaultDoc: LensDocument = {
|
||||
savedObjectId: '1234',
|
||||
title: 'An extremely cool default document!',
|
||||
expression: 'definitely a valid expression',
|
||||
visualizationType: 'testVis',
|
||||
state: {
|
||||
query: 'kuery',
|
||||
query: { query: 'test', language: 'kuery' },
|
||||
filters: [{ query: { match_phrase: { src: 'test' } }, meta: { index: 'index-pattern-0' } }],
|
||||
datasourceStates: {
|
||||
testDatasource: 'datasource',
|
||||
},
|
||||
datasourceStates: mockDatasourceStates(),
|
||||
visualization: {},
|
||||
},
|
||||
references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }],
|
||||
} as unknown as Document;
|
||||
};
|
||||
|
||||
export const exactMatchDoc = {
|
||||
attributes: {
|
||||
|
@ -70,6 +58,27 @@ export const exactMatchDoc = {
|
|||
},
|
||||
};
|
||||
|
||||
export function makeAttributeService(doc: LensDocument): jest.Mocked<LensAttributesService> {
|
||||
const attributeServiceMock: jest.Mocked<LensAttributesService> = {
|
||||
loadFromLibrary: jest.fn().mockResolvedValue(exactMatchDoc),
|
||||
saveToLibrary: jest.fn().mockResolvedValue(doc.savedObjectId),
|
||||
checkForDuplicateTitle: jest.fn(),
|
||||
injectReferences: jest.fn((_runtimeState, references) => ({
|
||||
..._runtimeState,
|
||||
attributes: {
|
||||
..._runtimeState.attributes,
|
||||
references: references?.length ? references : _runtimeState.attributes.references,
|
||||
},
|
||||
})),
|
||||
extractReferences: jest.fn((_runtimeState) => ({
|
||||
rawState: _runtimeState,
|
||||
references: _runtimeState.attributes.references || [],
|
||||
})),
|
||||
};
|
||||
|
||||
return attributeServiceMock;
|
||||
}
|
||||
|
||||
export function makeDefaultServices(
|
||||
sessionIdSubject = new Subject<string>(),
|
||||
sessionId: string | undefined = undefined,
|
||||
|
@ -106,44 +115,16 @@ export function makeDefaultServices(
|
|||
|
||||
const navigationStartMock = navigationPluginMock.createStartContract();
|
||||
|
||||
jest
|
||||
.spyOn(navigationStartMock.ui.AggregateQueryTopNavMenu.prototype, 'constructor')
|
||||
.mockImplementation(() => {
|
||||
return <div className="topNavMenu" />;
|
||||
});
|
||||
|
||||
function makeAttributeService(): LensAttributeService {
|
||||
const attributeServiceMock = mockAttributeService<
|
||||
LensSavedObjectAttributes,
|
||||
LensByValueInput,
|
||||
LensByReferenceInput,
|
||||
LensUnwrapMetaInfo
|
||||
>(
|
||||
DOC_TYPE,
|
||||
{
|
||||
saveMethod: jest.fn(),
|
||||
unwrapMethod: jest.fn(),
|
||||
checkForDuplicateTitle: jest.fn(),
|
||||
},
|
||||
core
|
||||
);
|
||||
attributeServiceMock.unwrapAttributes = jest.fn().mockResolvedValue(exactMatchDoc);
|
||||
attributeServiceMock.wrapAttributes = jest.fn().mockResolvedValue({
|
||||
savedObjectId: (doc as unknown as LensByReferenceInput).savedObjectId,
|
||||
});
|
||||
|
||||
return attributeServiceMock;
|
||||
}
|
||||
|
||||
return {
|
||||
...startMock,
|
||||
chrome: core.chrome,
|
||||
navigation: navigationStartMock,
|
||||
attributeService: makeAttributeService(),
|
||||
attributeService: makeAttributeService(doc),
|
||||
inspector: {
|
||||
adapters: getLensInspectorService(inspectorPluginMock.createStartContract()).adapters,
|
||||
getInspectorAdapters: getLensInspectorService(inspectorPluginMock.createStartContract())
|
||||
.getInspectorAdapters,
|
||||
inspect: jest.fn(),
|
||||
close: jest.fn(),
|
||||
closeInspector: jest.fn(),
|
||||
},
|
||||
presentationUtil: presentationUtilPluginMock.createStartContract(),
|
||||
savedObjectStore: {
|
||||
|
@ -158,6 +139,9 @@ export function makeDefaultServices(
|
|||
capabilities: {
|
||||
...core.application.capabilities,
|
||||
visualize: { save: true, saveQuery: true, show: true, createShortUrl: true },
|
||||
dashboard: {
|
||||
showWriteControls: true,
|
||||
},
|
||||
},
|
||||
getUrlForApp: jest.fn((appId: string) => `/testbasepath/app/${appId}#/`),
|
||||
},
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import React, { PropsWithChildren, ReactElement } from 'react';
|
||||
import { ReactWrapper, mount } from 'enzyme';
|
||||
import { Provider } from 'react-redux';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { PreloadedState } from '@reduxjs/toolkit';
|
||||
import { RenderOptions, render } from '@testing-library/react';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
|
@ -20,17 +19,25 @@ import { mockVisualizationMap } from './visualization_mock';
|
|||
import { mockDatasourceMap } from './datasource_mock';
|
||||
import { makeDefaultServices } from './services_mock';
|
||||
|
||||
export const mockStoreDeps = (deps?: {
|
||||
lensServices?: LensAppServices;
|
||||
datasourceMap?: DatasourceMap;
|
||||
visualizationMap?: VisualizationMap;
|
||||
}) => {
|
||||
return {
|
||||
datasourceMap: deps?.datasourceMap || mockDatasourceMap(),
|
||||
visualizationMap: deps?.visualizationMap || mockVisualizationMap(),
|
||||
lensServices: deps?.lensServices || makeDefaultServices(),
|
||||
};
|
||||
};
|
||||
export const mockStoreDeps = (
|
||||
{
|
||||
lensServices = makeDefaultServices(),
|
||||
datasourceMap = mockDatasourceMap(),
|
||||
visualizationMap = mockVisualizationMap(),
|
||||
}: {
|
||||
lensServices?: LensAppServices;
|
||||
datasourceMap?: DatasourceMap;
|
||||
visualizationMap?: VisualizationMap;
|
||||
} = {
|
||||
lensServices: makeDefaultServices(),
|
||||
datasourceMap: mockDatasourceMap(),
|
||||
visualizationMap: mockVisualizationMap(),
|
||||
}
|
||||
) => ({
|
||||
datasourceMap,
|
||||
visualizationMap,
|
||||
lensServices,
|
||||
});
|
||||
|
||||
export function mockDatasourceStates() {
|
||||
return {
|
||||
|
@ -138,12 +145,7 @@ export const mountWithProvider = async (
|
|||
}
|
||||
) => {
|
||||
const { mountArgs, lensStore, deps } = getMountWithProviderParams(component, store, options);
|
||||
|
||||
let instance: ReactWrapper = {} as ReactWrapper;
|
||||
|
||||
await act(async () => {
|
||||
instance = mount(mountArgs.component, mountArgs.options);
|
||||
});
|
||||
const instance = mount(mountArgs.component, mountArgs.options);
|
||||
return { instance, lensStore, deps };
|
||||
};
|
||||
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Filter, Query } from '@kbn/es-query';
|
||||
import { SavedObjectReference } from '@kbn/core/public';
|
||||
import type { AggregateQuery, Filter, Query } from '@kbn/es-query';
|
||||
import type { SavedObjectReference } from '@kbn/core/public';
|
||||
import type { DataViewSpec } from '@kbn/data-views-plugin/public';
|
||||
import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
|
||||
import type { SearchQuery } from '@kbn/content-management-plugin/common';
|
||||
|
@ -14,7 +14,7 @@ import type { VisualizationClient } from '@kbn/visualizations-plugin/public';
|
|||
import type { LensSavedObjectAttributes, LensSearchQuery } from '../../common/content_management';
|
||||
import { getLensClient } from './lens_client';
|
||||
|
||||
export interface Document {
|
||||
export interface LensDocument {
|
||||
savedObjectId?: string;
|
||||
type?: string;
|
||||
visualizationType: string | null;
|
||||
|
@ -23,7 +23,7 @@ export interface Document {
|
|||
state: {
|
||||
datasourceStates: Record<string, unknown>;
|
||||
visualization: unknown;
|
||||
query: Query;
|
||||
query: Query | AggregateQuery;
|
||||
globalPalette?: {
|
||||
activePaletteId: string;
|
||||
state?: unknown;
|
||||
|
@ -36,7 +36,7 @@ export interface Document {
|
|||
}
|
||||
|
||||
export interface DocumentSaver {
|
||||
save: (vis: Document) => Promise<{ savedObjectId: string }>;
|
||||
save: (vis: LensDocument) => Promise<{ savedObjectId: string }>;
|
||||
}
|
||||
|
||||
export interface DocumentLoader {
|
||||
|
@ -52,9 +52,8 @@ export class SavedObjectIndexStore implements SavedObjectStore {
|
|||
this.client = getLensClient(cm);
|
||||
}
|
||||
|
||||
save = async (vis: Document) => {
|
||||
const { savedObjectId, type, references, ...rest } = vis;
|
||||
const attributes = rest;
|
||||
save = async (vis: LensDocument) => {
|
||||
const { savedObjectId, type, references, ...attributes } = vis;
|
||||
|
||||
if (savedObjectId) {
|
||||
const result = await this.client.update({
|
||||
|
@ -65,15 +64,14 @@ export class SavedObjectIndexStore implements SavedObjectStore {
|
|||
},
|
||||
});
|
||||
return { ...vis, savedObjectId: result.item.id };
|
||||
} else {
|
||||
const result = await this.client.create({
|
||||
data: attributes,
|
||||
options: {
|
||||
references,
|
||||
},
|
||||
});
|
||||
return { ...vis, savedObjectId: result.item.id };
|
||||
}
|
||||
const result = await this.client.create({
|
||||
data: attributes,
|
||||
options: {
|
||||
references,
|
||||
},
|
||||
});
|
||||
return { ...vis, savedObjectId: result.item.id };
|
||||
};
|
||||
|
||||
async load(savedObjectId: string) {
|
||||
|
|
|
@ -14,8 +14,9 @@ import type {
|
|||
} from '@kbn/usage-collection-plugin/public';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import type { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
|
||||
import { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
|
||||
import { CONTEXT_MENU_TRIGGER } from '@kbn/embeddable-plugin/public';
|
||||
import type { EmbeddableEnhancedPluginStart } from '@kbn/embeddable-enhanced-plugin/public';
|
||||
import type { DataViewsPublicPluginStart, DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
|
||||
import type {
|
||||
|
@ -24,6 +25,7 @@ import type {
|
|||
ExpressionsStart,
|
||||
} from '@kbn/expressions-plugin/public';
|
||||
import {
|
||||
ACTION_CONVERT_DASHBOARD_PANEL_TO_LENS,
|
||||
DASHBOARD_VISUALIZATION_PANEL_TRIGGER,
|
||||
VisualizationsSetup,
|
||||
VisualizationsStart,
|
||||
|
@ -94,7 +96,13 @@ import type { HeatmapVisualization as HeatmapVisualizationType } from './visuali
|
|||
import type { GaugeVisualization as GaugeVisualizationType } from './visualizations/gauge';
|
||||
import type { TagcloudVisualization as TagcloudVisualizationType } from './visualizations/tagcloud';
|
||||
|
||||
import { APP_ID, getEditPath, NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common/constants';
|
||||
import {
|
||||
APP_ID,
|
||||
getEditPath,
|
||||
LENS_EMBEDDABLE_TYPE,
|
||||
LENS_ICON,
|
||||
NOT_INTERNATIONALIZED_PRODUCT_NAME,
|
||||
} from '../common/constants';
|
||||
import type { FormatFactory } from '../common/types';
|
||||
import type {
|
||||
Visualization,
|
||||
|
@ -103,10 +111,11 @@ import type {
|
|||
LensTopNavMenuEntryGenerator,
|
||||
VisualizeEditorContext,
|
||||
Suggestion,
|
||||
DatasourceMap,
|
||||
VisualizationMap,
|
||||
} from './types';
|
||||
import { getLensAliasConfig } from './vis_type_alias';
|
||||
import { createOpenInDiscoverAction } from './trigger_actions/open_in_discover_action';
|
||||
import { ConfigureInLensPanelAction } from './trigger_actions/open_lens_config/edit_action';
|
||||
import { CreateESQLPanelAction } from './trigger_actions/open_lens_config/create_action';
|
||||
import {
|
||||
inAppEmbeddableEditTrigger,
|
||||
|
@ -115,12 +124,12 @@ import {
|
|||
import { EditLensEmbeddableAction } from './trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action';
|
||||
import { visualizeFieldAction } from './trigger_actions/visualize_field_actions';
|
||||
import { visualizeTSVBAction } from './trigger_actions/visualize_tsvb_actions';
|
||||
import { visualizeAggBasedVisAction } from './trigger_actions/visualize_agg_based_vis_actions';
|
||||
import { visualizeDashboardVisualizePanelction } from './trigger_actions/dashboard_visualize_panel_actions';
|
||||
|
||||
import type { LensByValueInput, LensEmbeddableInput } from './embeddable';
|
||||
import { EmbeddableFactory, LensEmbeddableStartServices } from './embeddable/embeddable_factory';
|
||||
import { EmbeddableComponent, getEmbeddableComponent } from './embeddable/embeddable_component';
|
||||
import type {
|
||||
LensEmbeddableStartServices,
|
||||
LensSerializedState,
|
||||
TypedLensByValueInput,
|
||||
} from './react_embeddable/types';
|
||||
import { getSaveModalComponent } from './app_plugin/shared/saved_modal_lazy';
|
||||
import type { SaveModalContainerProps } from './app_plugin/save_modal_container';
|
||||
|
||||
|
@ -130,15 +139,16 @@ import { OpenInDiscoverDrilldown } from './trigger_actions/open_in_discover_dril
|
|||
import { ChartInfoApi } from './chart_info_api';
|
||||
import { type LensAppLocator, LensAppLocatorDefinition } from '../common/locator/locator';
|
||||
import { downloadCsvShareProvider } from './app_plugin/csv_download_provider/csv_download_provider';
|
||||
|
||||
import { LensDocument } from './persistence/saved_object_store';
|
||||
import {
|
||||
CONTENT_ID,
|
||||
LATEST_VERSION,
|
||||
LensSavedObjectAttributes,
|
||||
} from '../common/content_management';
|
||||
import type { EditLensConfigurationProps } from './app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration';
|
||||
import { savedObjectToEmbeddableAttributes } from './lens_attribute_service';
|
||||
import type { TypedLensByValueInput } from './embeddable/embeddable_component';
|
||||
import { convertToLensActionFactory } from './trigger_actions/convert_to_lens_action';
|
||||
import { LensRenderer } from './react_embeddable/renderer/lens_custom_renderer_component';
|
||||
import { deserializeState } from './react_embeddable/helper';
|
||||
|
||||
export type { SaveProps } from './app_plugin';
|
||||
|
||||
|
@ -182,6 +192,7 @@ export interface LensPluginStartDependencies {
|
|||
contentManagement: ContentManagementPublicStart;
|
||||
serverless?: ServerlessPluginStart;
|
||||
licensing?: LicensingPluginStart;
|
||||
embeddableEnhanced?: EmbeddableEnhancedPluginStart;
|
||||
}
|
||||
|
||||
export interface LensPublicSetup {
|
||||
|
@ -221,7 +232,7 @@ export interface LensPublicStart {
|
|||
*
|
||||
* @experimental
|
||||
*/
|
||||
EmbeddableComponent: EmbeddableComponent;
|
||||
EmbeddableComponent: typeof LensRenderer;
|
||||
/**
|
||||
* React component which can be used to embed a Lens Visualization Save Modal Component.
|
||||
* See `x-pack/examples/embedded_lens_example` for exemplary usage.
|
||||
|
@ -248,7 +259,7 @@ export interface LensPublicStart {
|
|||
* @experimental
|
||||
*/
|
||||
navigateToPrefilledEditor: (
|
||||
input: LensEmbeddableInput | undefined,
|
||||
input: LensSerializedState | undefined,
|
||||
options?: {
|
||||
openInNewTab?: boolean;
|
||||
originatingApp?: string;
|
||||
|
@ -303,9 +314,14 @@ export class LensPlugin {
|
|||
private topNavMenuEntries: LensTopNavMenuEntryGenerator[] = [];
|
||||
private hasDiscoverAccess: boolean = false;
|
||||
private dataViewsService: DataViewsPublicPluginStart | undefined;
|
||||
private initDependenciesForApi: () => void = () => {};
|
||||
private locator?: LensAppLocator;
|
||||
|
||||
// Note: this method will be overwritten in the setup flow
|
||||
private initEditorFrameService = async (): Promise<{
|
||||
datasourceMap: DatasourceMap;
|
||||
visualizationMap: VisualizationMap;
|
||||
}> => ({ datasourceMap: {}, visualizationMap: {} });
|
||||
|
||||
setup(
|
||||
core: CoreSetup<LensPluginStartDependencies, void>,
|
||||
{
|
||||
|
@ -326,26 +342,16 @@ export class LensPlugin {
|
|||
const startServices = createStartServicesGetter(core.getStartServices);
|
||||
|
||||
const getStartServicesForEmbeddable = async (): Promise<LensEmbeddableStartServices> => {
|
||||
const { getLensAttributeService, setUsageCollectionStart, initMemoizedErrorNotification } =
|
||||
await import('./async_services');
|
||||
const { setUsageCollectionStart, initMemoizedErrorNotification } = await import(
|
||||
'./async_services'
|
||||
);
|
||||
const { core: coreStart, plugins } = startServices();
|
||||
|
||||
await this.initParts(
|
||||
core,
|
||||
data,
|
||||
charts,
|
||||
expressions,
|
||||
fieldFormats,
|
||||
plugins.fieldFormats.deserialize
|
||||
);
|
||||
const [visualizationMap, datasourceMap] = await Promise.all([
|
||||
this.editorFrameService!.loadVisualizations(),
|
||||
this.editorFrameService!.loadDatasources(),
|
||||
const { visualizationMap, datasourceMap } = await this.initEditorFrameService();
|
||||
const [{ getLensAttributeService }, eventAnnotationService] = await Promise.all([
|
||||
import('./async_services'),
|
||||
plugins.eventAnnotation.getService(),
|
||||
]);
|
||||
const { setVisualizationMap, setDatasourceMap } = await import('./async_services');
|
||||
setDatasourceMap(datasourceMap);
|
||||
setVisualizationMap(visualizationMap);
|
||||
const eventAnnotationService = await plugins.eventAnnotation.getService();
|
||||
|
||||
if (plugins.usageCollection) {
|
||||
setUsageCollectionStart(plugins.usageCollection);
|
||||
|
@ -354,14 +360,14 @@ export class LensPlugin {
|
|||
initMemoizedErrorNotification(coreStart);
|
||||
|
||||
return {
|
||||
...plugins,
|
||||
attributeService: getLensAttributeService(coreStart, plugins),
|
||||
capabilities: coreStart.application.capabilities,
|
||||
coreHttp: coreStart.http,
|
||||
coreStart,
|
||||
data: plugins.data,
|
||||
timefilter: plugins.data.query.timefilter.timefilter,
|
||||
expressionRenderer: plugins.expressions.ReactExpressionRenderer,
|
||||
documentToExpression: (doc) =>
|
||||
documentToExpression: (doc: LensDocument) =>
|
||||
this.editorFrameService!.documentToExpression(doc, {
|
||||
dataViews: plugins.dataViews,
|
||||
storage: new Storage(localStorage),
|
||||
|
@ -373,36 +379,45 @@ export class LensPlugin {
|
|||
injectFilterReferences: data.query.filterManager.inject.bind(data.query.filterManager),
|
||||
visualizationMap,
|
||||
datasourceMap,
|
||||
dataViews: plugins.dataViews,
|
||||
uiActions: plugins.uiActions,
|
||||
usageCollection,
|
||||
inspector: plugins.inspector,
|
||||
spaces: plugins.spaces,
|
||||
theme: core.theme,
|
||||
uiSettings: core.uiSettings,
|
||||
};
|
||||
};
|
||||
|
||||
if (embeddable) {
|
||||
embeddable.registerEmbeddableFactory(
|
||||
'lens',
|
||||
new EmbeddableFactory(getStartServicesForEmbeddable)
|
||||
);
|
||||
// Let Kibana know about the Lens embeddable
|
||||
embeddable.registerReactEmbeddableFactory(LENS_EMBEDDABLE_TYPE, async () => {
|
||||
const [deps, { createLensEmbeddableFactory }] = await Promise.all([
|
||||
getStartServicesForEmbeddable(),
|
||||
import('./react_embeddable/lens_embeddable'),
|
||||
]);
|
||||
return createLensEmbeddableFactory(deps);
|
||||
});
|
||||
|
||||
embeddable.registerSavedObjectToPanelMethod<LensSavedObjectAttributes, LensByValueInput>(
|
||||
CONTENT_ID,
|
||||
(savedObject) => {
|
||||
if (!savedObject.managed) {
|
||||
return { savedObjectId: savedObject.id };
|
||||
}
|
||||
|
||||
const panel = {
|
||||
attributes: savedObjectToEmbeddableAttributes(savedObject),
|
||||
};
|
||||
|
||||
return panel;
|
||||
}
|
||||
);
|
||||
// Let Dashboard know about the Lens panel type
|
||||
embeddable.registerReactEmbeddableSavedObject<LensSavedObjectAttributes>({
|
||||
onAdd: async (container, savedObject) => {
|
||||
const { attributeService } = await getStartServicesForEmbeddable();
|
||||
// deserialize the saved object from visualize library
|
||||
// this make sure to fit into the new embeddable model, where the following build()
|
||||
// function expects a fully loaded runtime state
|
||||
const state = await deserializeState(
|
||||
attributeService,
|
||||
{ savedObjectId: savedObject.id },
|
||||
savedObject.references
|
||||
);
|
||||
container.addNewPanel({
|
||||
panelType: LENS_EMBEDDABLE_TYPE,
|
||||
initialState: state,
|
||||
});
|
||||
},
|
||||
embeddableType: LENS_EMBEDDABLE_TYPE,
|
||||
savedObjectType: LENS_EMBEDDABLE_TYPE,
|
||||
savedObjectName: i18n.translate('xpack.lens.mapSavedObjectLabel', {
|
||||
defaultMessage: 'Lens',
|
||||
}),
|
||||
getIconForSavedObject: () => LENS_ICON,
|
||||
});
|
||||
}
|
||||
|
||||
if (share) {
|
||||
|
@ -509,9 +524,10 @@ export class LensPlugin {
|
|||
);
|
||||
}
|
||||
|
||||
urlForwarding.forwardApp('lens', 'lens');
|
||||
urlForwarding.forwardApp(APP_ID, APP_ID);
|
||||
|
||||
this.initDependenciesForApi = async () => {
|
||||
// Note: this overwrites a method defined above
|
||||
this.initEditorFrameService = async () => {
|
||||
const { plugins } = startServices();
|
||||
await this.initParts(
|
||||
core,
|
||||
|
@ -521,6 +537,15 @@ export class LensPlugin {
|
|||
fieldFormats,
|
||||
plugins.fieldFormats.deserialize
|
||||
);
|
||||
// This needs to be executed before the import call to avoid race conditions
|
||||
const [visualizationMap, datasourceMap] = await Promise.all([
|
||||
this.editorFrameService!.loadVisualizations(),
|
||||
this.editorFrameService!.loadDatasources(),
|
||||
]);
|
||||
const { setVisualizationMap, setDatasourceMap } = await import('./async_services');
|
||||
setDatasourceMap(datasourceMap);
|
||||
setVisualizationMap(visualizationMap);
|
||||
return { datasourceMap, visualizationMap };
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -625,21 +650,33 @@ export class LensPlugin {
|
|||
|
||||
startDependencies.uiActions.addTriggerAction(
|
||||
DASHBOARD_VISUALIZATION_PANEL_TRIGGER,
|
||||
visualizeDashboardVisualizePanelction(core.application)
|
||||
convertToLensActionFactory(
|
||||
ACTION_CONVERT_DASHBOARD_PANEL_TO_LENS,
|
||||
i18n.translate('xpack.lens.visualizeLegacyVisualizationChart', {
|
||||
defaultMessage: 'Visualize legacy visualization chart',
|
||||
}),
|
||||
i18n.translate('xpack.lens.dashboardLabel', {
|
||||
defaultMessage: 'Dashboard',
|
||||
})
|
||||
)(core.application)
|
||||
);
|
||||
|
||||
startDependencies.uiActions.addTriggerAction(
|
||||
AGG_BASED_VISUALIZATION_TRIGGER,
|
||||
visualizeAggBasedVisAction(core.application)
|
||||
convertToLensActionFactory(
|
||||
ACTION_CONVERT_DASHBOARD_PANEL_TO_LENS,
|
||||
i18n.translate('xpack.lens.visualizeAggBasedLegend', {
|
||||
defaultMessage: 'Visualize agg based chart',
|
||||
}),
|
||||
i18n.translate('xpack.lens.AggBasedLabel', {
|
||||
defaultMessage: 'aggregation based visualization',
|
||||
})
|
||||
)(core.application)
|
||||
);
|
||||
|
||||
const editInLensAction = new ConfigureInLensPanelAction(startDependencies, core);
|
||||
// dashboard edit panel action
|
||||
startDependencies.uiActions.addTriggerAction('CONTEXT_MENU_TRIGGER', editInLensAction);
|
||||
|
||||
// Allows the Lens embeddable to easily open the inapp editing flyout
|
||||
// Allows the Lens embeddable to easily open the inline editing flyout
|
||||
const editLensEmbeddableAction = new EditLensEmbeddableAction(startDependencies, core);
|
||||
// embeddable edit panel action
|
||||
// embeddable inline edit panel action
|
||||
startDependencies.uiActions.addTriggerAction(
|
||||
IN_APP_EMBEDDABLE_EDIT_TRIGGER,
|
||||
editLensEmbeddableAction
|
||||
|
@ -648,7 +685,7 @@ export class LensPlugin {
|
|||
// Displays the add ESQL panel in the dashboard add Panel menu
|
||||
const createESQLPanelAction = new CreateESQLPanelAction(startDependencies, core, async () => {
|
||||
if (!this.editorFrameService) {
|
||||
await this.initDependenciesForApi();
|
||||
await this.initEditorFrameService();
|
||||
}
|
||||
|
||||
return this.editorFrameService!;
|
||||
|
@ -668,7 +705,7 @@ export class LensPlugin {
|
|||
}
|
||||
|
||||
return {
|
||||
EmbeddableComponent: getEmbeddableComponent(core, startDependencies),
|
||||
EmbeddableComponent: LensRenderer,
|
||||
SaveModalComponent: getSaveModalComponent(core, startDependencies),
|
||||
navigateToPrefilledEditor: (
|
||||
input,
|
||||
|
@ -705,16 +742,15 @@ export class LensPlugin {
|
|||
const { createFormulaPublicApi, createChartInfoApi, suggestionsApi } = await import(
|
||||
'./async_services'
|
||||
);
|
||||
if (!this.editorFrameService) {
|
||||
await this.initDependenciesForApi();
|
||||
}
|
||||
const [visualizationMap, datasourceMap] = await Promise.all([
|
||||
this.editorFrameService!.loadVisualizations(),
|
||||
this.editorFrameService!.loadDatasources(),
|
||||
]);
|
||||
|
||||
const { visualizationMap, datasourceMap } = await this.initEditorFrameService();
|
||||
return {
|
||||
formula: createFormulaPublicApi(),
|
||||
chartInfo: createChartInfoApi(startDependencies.dataViews, this.editorFrameService),
|
||||
chartInfo: createChartInfoApi(
|
||||
startDependencies.dataViews,
|
||||
visualizationMap,
|
||||
datasourceMap
|
||||
),
|
||||
suggestions: (
|
||||
context,
|
||||
dataView,
|
||||
|
@ -734,15 +770,11 @@ export class LensPlugin {
|
|||
},
|
||||
};
|
||||
},
|
||||
// TODO: remove this in faviour of the custom action thing
|
||||
// This is currently used in Discover by the unified histogram plugin
|
||||
EditLensConfigPanelApi: async () => {
|
||||
const { visualizationMap, datasourceMap } = await this.initEditorFrameService();
|
||||
const { getEditLensConfiguration } = await import('./async_services');
|
||||
if (!this.editorFrameService) {
|
||||
this.initDependenciesForApi();
|
||||
}
|
||||
const [visualizationMap, datasourceMap] = await Promise.all([
|
||||
this.editorFrameService!.loadVisualizations(),
|
||||
this.editorFrameService!.loadDatasources(),
|
||||
]);
|
||||
const Component = await getEditLensConfiguration(
|
||||
core,
|
||||
startDependencies,
|
||||
|
|
329
x-pack/plugins/lens/public/react_embeddable/data_loader.ts
Normal file
329
x-pack/plugins/lens/public/react_embeddable/data_loader.ts
Normal file
|
@ -0,0 +1,329 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common';
|
||||
import { fetch$, type FetchContext } from '@kbn/presentation-publishing';
|
||||
import { apiPublishesSearchSession } from '@kbn/presentation-publishing/interfaces/fetch/publishes_search_session';
|
||||
import { type KibanaExecutionContext } from '@kbn/core/public';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
type Subscription,
|
||||
distinctUntilChanged,
|
||||
debounceTime,
|
||||
skip,
|
||||
pipe,
|
||||
merge,
|
||||
tap,
|
||||
map,
|
||||
} from 'rxjs';
|
||||
import fastIsEqual from 'fast-deep-equal';
|
||||
import { getEditPath } from '../../common/constants';
|
||||
import type {
|
||||
GetStateType,
|
||||
LensApi,
|
||||
LensInternalApi,
|
||||
LensPublicCallbacks,
|
||||
VisualizationContextHelper,
|
||||
} from './types';
|
||||
import { getExpressionRendererParams } from './expressions/expression_params';
|
||||
import type { LensEmbeddableStartServices } from './types';
|
||||
import { prepareCallbacks } from './expressions/callbacks';
|
||||
import { buildUserMessagesHelpers } from './user_messages/api';
|
||||
import { getLogError } from './expressions/telemetry';
|
||||
import type { SharingSavedObjectProps, UserMessagesDisplayLocationId } from '../types';
|
||||
import { apiHasLensComponentCallbacks } from './type_guards';
|
||||
import { getRenderMode, getParentContext } from './helper';
|
||||
import { addLog } from './logger';
|
||||
import { getUsedDataViews } from './expressions/update_data_views';
|
||||
import { getMergedSearchContext } from './expressions/merged_search_context';
|
||||
|
||||
const blockingMessageDisplayLocations: UserMessagesDisplayLocationId[] = [
|
||||
'visualization',
|
||||
'visualizationOnEmbeddable',
|
||||
];
|
||||
|
||||
type ReloadReason =
|
||||
| 'attributes'
|
||||
| 'savedObjectId'
|
||||
| 'overrides'
|
||||
| 'disableTriggers'
|
||||
| 'viewMode'
|
||||
| 'searchContext';
|
||||
|
||||
/**
|
||||
* The function computes the expression used to render the panel and produces the necessary props
|
||||
* for the ExpressionWrapper component, binding any outer context to them.
|
||||
* @returns
|
||||
*/
|
||||
export function loadEmbeddableData(
|
||||
uuid: string,
|
||||
getState: GetStateType,
|
||||
api: LensApi,
|
||||
parentApi: unknown,
|
||||
internalApi: LensInternalApi,
|
||||
services: LensEmbeddableStartServices,
|
||||
{ getVisualizationContext, updateVisualizationContext }: VisualizationContextHelper,
|
||||
metaInfo?: SharingSavedObjectProps
|
||||
) {
|
||||
const { onLoad, onBeforeBadgesRender, ...callbacks } = apiHasLensComponentCallbacks(parentApi)
|
||||
? parentApi
|
||||
: ({} as LensPublicCallbacks);
|
||||
|
||||
// Some convenience api for the user messaging
|
||||
const {
|
||||
getUserMessages,
|
||||
addUserMessages,
|
||||
updateBlockingErrors,
|
||||
updateValidationErrors,
|
||||
updateWarnings,
|
||||
resetMessages,
|
||||
updateMessages,
|
||||
} = buildUserMessagesHelpers(
|
||||
api,
|
||||
internalApi,
|
||||
getVisualizationContext,
|
||||
services,
|
||||
onBeforeBadgesRender,
|
||||
services.spaces,
|
||||
metaInfo
|
||||
);
|
||||
|
||||
const dispatchBlockingErrorIfAny = () => {
|
||||
const blockingErrors = getUserMessages(blockingMessageDisplayLocations, {
|
||||
severity: 'error',
|
||||
});
|
||||
updateValidationErrors(blockingErrors);
|
||||
updateBlockingErrors(blockingErrors);
|
||||
if (blockingErrors.length > 0) {
|
||||
internalApi.dispatchError();
|
||||
}
|
||||
return blockingErrors.length > 0;
|
||||
};
|
||||
|
||||
const onRenderComplete = () => {
|
||||
updateMessages(getUserMessages('embeddableBadge'));
|
||||
// No issues so far, blocking errors are handled directly by Lens from this point on
|
||||
if (!dispatchBlockingErrorIfAny()) {
|
||||
internalApi.dispatchRenderComplete();
|
||||
}
|
||||
};
|
||||
|
||||
const unifiedSearch$ = new BehaviorSubject<
|
||||
Pick<FetchContext, 'query' | 'filters' | 'timeRange' | 'timeslice' | 'searchSessionId'>
|
||||
>({
|
||||
query: undefined,
|
||||
filters: undefined,
|
||||
timeRange: undefined,
|
||||
timeslice: undefined,
|
||||
searchSessionId: undefined,
|
||||
});
|
||||
|
||||
async function reload(
|
||||
// make reload easier to debug
|
||||
sourceId: ReloadReason
|
||||
) {
|
||||
addLog(`Embeddable reload reason: ${sourceId}`);
|
||||
resetMessages();
|
||||
|
||||
// reset the render on reload
|
||||
internalApi.dispatchRenderStart();
|
||||
|
||||
// notify about data loading
|
||||
internalApi.updateDataLoading(true);
|
||||
|
||||
// the component is ready to load
|
||||
if (apiHasLensComponentCallbacks(parentApi)) {
|
||||
parentApi.onLoad?.(true);
|
||||
}
|
||||
|
||||
const currentState = getState();
|
||||
|
||||
const { searchSessionId, ...unifiedSearch } = unifiedSearch$.getValue();
|
||||
|
||||
const getExecutionContext = () => {
|
||||
const parentContext = getParentContext(parentApi);
|
||||
const lastState = getState();
|
||||
if (lastState.attributes) {
|
||||
const child: KibanaExecutionContext = {
|
||||
type: 'lens',
|
||||
name: lastState.attributes.visualizationType ?? '',
|
||||
id: uuid || 'new',
|
||||
description: lastState.attributes.title || lastState.title || '',
|
||||
url: `${services.coreStart.application.getUrlForApp('lens')}${getEditPath(
|
||||
lastState.savedObjectId
|
||||
)}`,
|
||||
};
|
||||
|
||||
return parentContext
|
||||
? {
|
||||
...parentContext,
|
||||
child,
|
||||
}
|
||||
: child;
|
||||
}
|
||||
};
|
||||
|
||||
const onDataCallback = (adapters: Partial<DefaultInspectorAdapters> | undefined) => {
|
||||
updateVisualizationContext({
|
||||
activeData: adapters?.tables?.tables,
|
||||
});
|
||||
// data has loaded
|
||||
internalApi.updateDataLoading(false);
|
||||
// The third argument here is an observable to let the
|
||||
// consumer to be notified on data change
|
||||
onLoad?.(false, adapters, api.dataLoading);
|
||||
|
||||
api.loadViewUnderlyingData();
|
||||
|
||||
updateWarnings();
|
||||
// Render can still go wrong, so perfor a new check
|
||||
dispatchBlockingErrorIfAny();
|
||||
};
|
||||
|
||||
const { onRender, onData, handleEvent, disableTriggers } = prepareCallbacks(
|
||||
api,
|
||||
internalApi,
|
||||
parentApi,
|
||||
getState,
|
||||
services,
|
||||
getExecutionContext(),
|
||||
onDataCallback,
|
||||
onRenderComplete,
|
||||
callbacks
|
||||
);
|
||||
|
||||
const searchContext = getMergedSearchContext(
|
||||
currentState,
|
||||
unifiedSearch,
|
||||
api.timeRange$,
|
||||
parentApi,
|
||||
services
|
||||
);
|
||||
|
||||
// Go concurrently: build the expression and fetch the dataViews
|
||||
const [{ params, abortController, ...rest }, dataViews] = await Promise.all([
|
||||
getExpressionRendererParams(currentState, {
|
||||
searchContext,
|
||||
api,
|
||||
settings: {
|
||||
syncColors: currentState.syncColors,
|
||||
syncCursor: currentState.syncCursor,
|
||||
syncTooltips: currentState.syncTooltips,
|
||||
},
|
||||
renderMode: getRenderMode(parentApi),
|
||||
services,
|
||||
searchSessionId,
|
||||
abortController: internalApi.expressionAbortController$.getValue(),
|
||||
getExecutionContext,
|
||||
logError: getLogError(getExecutionContext),
|
||||
addUserMessages,
|
||||
onRender,
|
||||
onData,
|
||||
handleEvent,
|
||||
disableTriggers,
|
||||
updateBlockingErrors,
|
||||
renderCount: internalApi.renderCount$.getValue(),
|
||||
}),
|
||||
getUsedDataViews(
|
||||
currentState.attributes.references,
|
||||
currentState.attributes.state?.adHocDataViews,
|
||||
services.dataViews
|
||||
),
|
||||
]);
|
||||
|
||||
// update the visualization context before anything else
|
||||
// as it will be used to compute blocking errors also in case of issues
|
||||
updateVisualizationContext({
|
||||
doc: currentState.attributes,
|
||||
mergedSearchContext: params?.searchContext || {},
|
||||
...rest,
|
||||
});
|
||||
|
||||
// Publish the used dataViews on the Lens API
|
||||
internalApi.updateDataViews(dataViews);
|
||||
|
||||
if (params?.expression != null && !dispatchBlockingErrorIfAny()) {
|
||||
internalApi.updateExpressionParams(params);
|
||||
}
|
||||
|
||||
internalApi.updateAbortController(abortController);
|
||||
}
|
||||
|
||||
// Build a custom operator to be resused for various observables
|
||||
function waitUntilChanged() {
|
||||
return pipe(distinctUntilChanged(fastIsEqual), skip(1));
|
||||
}
|
||||
|
||||
const mergedSubscriptions = merge(
|
||||
// on data change from the parentApi, reload
|
||||
fetch$(api).pipe(
|
||||
tap((data) => {
|
||||
const searchSessionId = apiPublishesSearchSession(parentApi) ? data.searchSessionId : '';
|
||||
unifiedSearch$.next({
|
||||
query: data.query,
|
||||
filters: data.filters,
|
||||
timeRange: data.timeRange,
|
||||
timeslice: data.timeslice,
|
||||
searchSessionId,
|
||||
});
|
||||
}),
|
||||
map(() => 'searchContext' as ReloadReason)
|
||||
),
|
||||
// On state change, reload
|
||||
// this is used to refresh the chart on inline editing
|
||||
// just make sure to avoid to rerender if there's no substantial change
|
||||
// make sure to debounce one tick to make the refresh work
|
||||
internalApi.attributes$.pipe(
|
||||
waitUntilChanged(),
|
||||
tap(() => {
|
||||
// the ES|QL query may have changed, so recompute the args for view underlying data
|
||||
if (api.isTextBasedLanguage()) {
|
||||
api.loadViewUnderlyingData();
|
||||
}
|
||||
}),
|
||||
map(() => 'attributes' as ReloadReason)
|
||||
),
|
||||
api.savedObjectId.pipe(
|
||||
waitUntilChanged(),
|
||||
map(() => 'savedObjectId' as ReloadReason)
|
||||
),
|
||||
internalApi.overrides$.pipe(
|
||||
waitUntilChanged(),
|
||||
map(() => 'overrides' as ReloadReason)
|
||||
),
|
||||
internalApi.disableTriggers$.pipe(
|
||||
waitUntilChanged(),
|
||||
map(() => 'disableTriggers' as ReloadReason)
|
||||
)
|
||||
);
|
||||
|
||||
const subscriptions: Subscription[] = [
|
||||
mergedSubscriptions.pipe(debounceTime(0)).subscribe(reload),
|
||||
// make sure to reload on viewMode change
|
||||
api.viewMode.subscribe(() => {
|
||||
// only reload if drilldowns are set
|
||||
if (getState().enhancements?.dynamicActions) {
|
||||
reload('viewMode');
|
||||
}
|
||||
}),
|
||||
];
|
||||
// There are few key moments when errors are checked and displayed:
|
||||
// * at setup time (here) before the first expression evaluation
|
||||
// * at runtime => when the expression is running and ES/Kibana server could emit errors)
|
||||
// * at data time => data has arrived but for something goes wrong
|
||||
// * at render time => rendering happened but somethign went wrong
|
||||
// Bubble the error up to the embeddable system if any
|
||||
dispatchBlockingErrorIfAny();
|
||||
|
||||
return {
|
||||
cleanup: () => {
|
||||
for (const subscription of subscriptions) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
|
@ -17,7 +17,7 @@ import { DefaultInspectorAdapters, RenderMode } from '@kbn/expressions-plugin/co
|
|||
import classNames from 'classnames';
|
||||
import { getOriginalRequestErrorMessages } from '../editor_frame_service/error_helper';
|
||||
import { LensInspector } from '../lens_inspector_service';
|
||||
import { AddUserMessages } from '../types';
|
||||
import { UserMessage } from '../types';
|
||||
|
||||
export interface ExpressionWrapperProps {
|
||||
ExpressionRenderer: ReactExpressionRendererType;
|
||||
|
@ -31,7 +31,7 @@ export interface ExpressionWrapperProps {
|
|||
data: unknown,
|
||||
inspectorAdapters?: Partial<DefaultInspectorAdapters> | undefined
|
||||
) => void;
|
||||
onRender$: () => void;
|
||||
onRender$: (count: number) => void;
|
||||
renderMode?: RenderMode;
|
||||
syncColors?: boolean;
|
||||
syncTooltips?: boolean;
|
||||
|
@ -40,7 +40,7 @@ export interface ExpressionWrapperProps {
|
|||
getCompatibleCellValueActions?: ReactExpressionRendererProps['getCompatibleCellValueActions'];
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
addUserMessages: AddUserMessages;
|
||||
addUserMessages: (messages: UserMessage[]) => void;
|
||||
onRuntimeError: (error: Error) => void;
|
||||
executionContext?: KibanaExecutionContext;
|
||||
lensInspector: LensInspector;
|
||||
|
@ -75,7 +75,11 @@ export function ExpressionWrapper({
|
|||
}: ExpressionWrapperProps) {
|
||||
if (!expression) return null;
|
||||
return (
|
||||
<div className={classNames('lnsExpressionRenderer', className)} style={style}>
|
||||
<div
|
||||
className={classNames('lnsExpressionRenderer', className)}
|
||||
style={style}
|
||||
data-test-subj="lens-embeddable"
|
||||
>
|
||||
<ExpressionRendererComponent
|
||||
className="lnsExpressionRenderer__component"
|
||||
padding={noPadding ? undefined : 's'}
|
||||
|
@ -88,7 +92,7 @@ export function ExpressionWrapper({
|
|||
// @ts-expect-error upgrade typescript v4.9.5
|
||||
onData$={onData$}
|
||||
onRender$={onRender$}
|
||||
inspectorAdapters={lensInspector.adapters}
|
||||
inspectorAdapters={lensInspector.getInspectorAdapters()}
|
||||
renderMode={renderMode}
|
||||
syncColors={syncColors}
|
||||
syncTooltips={syncTooltips}
|
||||
|
@ -98,12 +102,7 @@ export function ExpressionWrapper({
|
|||
renderError={(errorMessage, error) => {
|
||||
const messages = getOriginalRequestErrorMessages(error || null);
|
||||
addUserMessages(messages);
|
||||
if (error?.original) {
|
||||
onRuntimeError(error.original);
|
||||
} else {
|
||||
onRuntimeError(new Error(errorMessage ? errorMessage : ''));
|
||||
}
|
||||
|
||||
onRuntimeError(error?.original || new Error(errorMessage ? errorMessage : ''));
|
||||
return <></>; // the embeddable will take care of displaying the messages
|
||||
}}
|
||||
onEvent={handleEvent}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { KibanaExecutionContext } from '@kbn/core/public';
|
||||
import type { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common';
|
||||
import { apiHasDisableTriggers } from '@kbn/presentation-publishing';
|
||||
import {
|
||||
GetStateType,
|
||||
LensApi,
|
||||
LensEmbeddableStartServices,
|
||||
LensInternalApi,
|
||||
LensPublicCallbacks,
|
||||
} from '../types';
|
||||
import { prepareOnRender } from './on_render';
|
||||
import { prepareEventHandler } from './on_event';
|
||||
import { addLog } from '../logger';
|
||||
|
||||
export function prepareCallbacks(
|
||||
api: LensApi,
|
||||
internalApi: LensInternalApi,
|
||||
parentApi: unknown,
|
||||
getState: GetStateType,
|
||||
services: LensEmbeddableStartServices,
|
||||
executionContext: KibanaExecutionContext | undefined,
|
||||
onDataUpdate: (adapters: Partial<DefaultInspectorAdapters | undefined>) => void,
|
||||
dispatchRenderComplete: () => void,
|
||||
callbacks: LensPublicCallbacks
|
||||
) {
|
||||
const disableTriggers = apiHasDisableTriggers(parentApi) ? parentApi.disableTriggers : undefined;
|
||||
return {
|
||||
disableTriggers,
|
||||
onRender: prepareOnRender(
|
||||
api,
|
||||
internalApi,
|
||||
parentApi,
|
||||
getState,
|
||||
services,
|
||||
executionContext,
|
||||
dispatchRenderComplete
|
||||
),
|
||||
onData: (_data: unknown, adapters: Partial<DefaultInspectorAdapters> | undefined) => {
|
||||
addLog(`onData$`);
|
||||
onDataUpdate(adapters);
|
||||
},
|
||||
handleEvent: prepareEventHandler(api, getState, callbacks, services, disableTriggers),
|
||||
};
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue