mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[TextBased] Allow inline editing from dashboards (#161146)
## Summary
Part of https://github.com/elastic/kibana/issues/158802
For panels created from Discover with text based languages, not navigate
to Lens but open a flyout instead.

Follow up PR:
- Remove the SQL option from Lens dataview picker and move the FTs in
Discover/Dashboard
Note:
- Changing the query on the dashboard level is going to be added in 8.11
### Checklist
- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
3495720e42
commit
fde953907c
19 changed files with 467 additions and 69 deletions
|
@ -83,7 +83,7 @@ pageLoadAssetSize:
|
|||
kibanaUsageCollection: 16463
|
||||
kibanaUtils: 79713
|
||||
kubernetesSecurity: 77234
|
||||
lens: 37000
|
||||
lens: 38000
|
||||
licenseManagement: 41817
|
||||
licensing: 29004
|
||||
lists: 22900
|
||||
|
|
|
@ -40,6 +40,7 @@ export function FiltersNotificationPopover({
|
|||
}: FiltersNotificationProps) {
|
||||
const { embeddable } = context;
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const [disableEditbutton, setDisableEditButton] = useState(false);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
|
@ -57,26 +58,31 @@ export function FiltersNotificationPopover({
|
|||
anchorPosition="upCenter"
|
||||
>
|
||||
<EuiPopoverTitle>{displayName}</EuiPopoverTitle>
|
||||
<FiltersNotificationPopoverContents context={context} />
|
||||
<FiltersNotificationPopoverContents
|
||||
context={context}
|
||||
setDisableEditButton={setDisableEditButton}
|
||||
/>
|
||||
<EuiPopoverFooter>
|
||||
<EuiFlexGroup
|
||||
gutterSize="s"
|
||||
alignItems="center"
|
||||
justifyContent="flexEnd"
|
||||
responsive={false}
|
||||
wrap={true}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj={'filtersNotificationModal__editButton'}
|
||||
size="s"
|
||||
fill
|
||||
onClick={() => editPanelAction.execute({ embeddable })}
|
||||
>
|
||||
{dashboardFilterNotificationActionStrings.getEditButtonTitle()}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{!disableEditbutton && (
|
||||
<EuiFlexGroup
|
||||
gutterSize="s"
|
||||
alignItems="center"
|
||||
justifyContent="flexEnd"
|
||||
responsive={false}
|
||||
wrap={true}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj={'filtersNotificationModal__editButton'}
|
||||
size="s"
|
||||
fill
|
||||
onClick={() => editPanelAction.execute({ embeddable })}
|
||||
>
|
||||
{dashboardFilterNotificationActionStrings.getEditButtonTitle()}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiPopoverFooter>
|
||||
</EuiPopover>
|
||||
);
|
||||
|
|
|
@ -26,9 +26,13 @@ import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_
|
|||
|
||||
export interface FiltersNotificationProps {
|
||||
context: FiltersNotificationActionContext;
|
||||
setDisableEditButton: (flag: boolean) => void;
|
||||
}
|
||||
|
||||
export function FiltersNotificationPopoverContents({ context }: FiltersNotificationProps) {
|
||||
export function FiltersNotificationPopoverContents({
|
||||
context,
|
||||
setDisableEditButton,
|
||||
}: FiltersNotificationProps) {
|
||||
const { embeddable } = context;
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [filters, setFilters] = useState<Filter[]>([]);
|
||||
|
@ -53,6 +57,7 @@ export function FiltersNotificationPopoverContents({ context }: FiltersNotificat
|
|||
const language = getAggregateQueryMode(embeddableQuery);
|
||||
setQueryLanguage(language);
|
||||
setQueryString(embeddableQuery[language as keyof AggregateQuery]);
|
||||
setDisableEditButton(true);
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
|
|
|
@ -67,7 +67,10 @@ export function useChartConfigPanel({
|
|||
dataView={dataView}
|
||||
adaptersTables={lensTablesAdapter}
|
||||
updateAll={updateSuggestion}
|
||||
setIsFlyoutVisible={setIsFlyoutVisible}
|
||||
closeFlyout={() => {
|
||||
setIsFlyoutVisible(false);
|
||||
}}
|
||||
wrapInFlyout
|
||||
datasourceId="textBased"
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -740,6 +740,43 @@ describe('getLensAttributes', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
it('should return correct attributes for text based languages with adhoc dataview', () => {
|
||||
const adHocDataview = {
|
||||
...dataView,
|
||||
isPersisted: () => false,
|
||||
} as DataView;
|
||||
const lensAttrs = getLensAttributes({
|
||||
title: 'test',
|
||||
filters,
|
||||
query,
|
||||
dataView: adHocDataview,
|
||||
timeInterval,
|
||||
breakdownField: undefined,
|
||||
suggestion: currentSuggestionMock,
|
||||
});
|
||||
expect(lensAttrs.attributes).toEqual({
|
||||
state: expect.objectContaining({
|
||||
adHocDataViews: {
|
||||
'index-pattern-with-timefield-id': {},
|
||||
},
|
||||
}),
|
||||
references: [
|
||||
{
|
||||
id: 'index-pattern-with-timefield-id',
|
||||
name: 'indexpattern-datasource-current-indexpattern',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
{
|
||||
id: 'index-pattern-with-timefield-id',
|
||||
name: 'indexpattern-datasource-layer-unifiedHistogram',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
title: 'test',
|
||||
visualizationType: 'lnsHeatmap',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return suggestion title if no title is given', () => {
|
||||
expect(
|
||||
getLensAttributes({
|
||||
|
|
|
@ -205,6 +205,13 @@ export const getLensAttributes = ({
|
|||
filters,
|
||||
query,
|
||||
visualization,
|
||||
...(dataView &&
|
||||
dataView.id &&
|
||||
!dataView.isPersisted() && {
|
||||
adHocDataViews: {
|
||||
[dataView.id]: dataView.toSpec(false),
|
||||
},
|
||||
}),
|
||||
},
|
||||
visualizationType: suggestion ? suggestion.visualizationId : 'lnsXY',
|
||||
} as TypedLensByValueInput['attributes'];
|
||||
|
|
|
@ -51,7 +51,8 @@ export function getEditLensConfiguration(
|
|||
attributes,
|
||||
dataView,
|
||||
updateAll,
|
||||
setIsFlyoutVisible,
|
||||
closeFlyout,
|
||||
wrapInFlyout,
|
||||
datasourceId,
|
||||
adaptersTables,
|
||||
}: EditLensConfigurationProps) => {
|
||||
|
@ -88,15 +89,38 @@ export function getEditLensConfiguration(
|
|||
const lensStore: LensRootStore = makeConfigureStore(storeDeps, {
|
||||
lens: getPreloadedState(storeDeps) as LensAppState,
|
||||
} as unknown as PreloadedState<LensState>);
|
||||
const closeFlyout = () => {
|
||||
setIsFlyoutVisible?.(false);
|
||||
|
||||
const getWrapper = (children: JSX.Element) => {
|
||||
if (wrapInFlyout) {
|
||||
return (
|
||||
<EuiFlyout
|
||||
type="push"
|
||||
ownFocus
|
||||
onClose={() => {
|
||||
closeFlyout?.();
|
||||
}}
|
||||
aria-labelledby={i18n.translate('xpack.lens.config.editLabel', {
|
||||
defaultMessage: 'Edit configuration',
|
||||
})}
|
||||
size="s"
|
||||
hideCloseButton
|
||||
css={css`
|
||||
background: none;
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</EuiFlyout>
|
||||
);
|
||||
} else {
|
||||
return children;
|
||||
}
|
||||
};
|
||||
|
||||
const configPanelProps = {
|
||||
attributes,
|
||||
dataView,
|
||||
updateAll,
|
||||
setIsFlyoutVisible,
|
||||
closeFlyout,
|
||||
datasourceId,
|
||||
adaptersTables,
|
||||
coreStart,
|
||||
|
@ -105,25 +129,10 @@ export function getEditLensConfiguration(
|
|||
datasourceMap,
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlyout
|
||||
type="push"
|
||||
ownFocus
|
||||
onClose={closeFlyout}
|
||||
aria-labelledby={i18n.translate('xpack.lens.config.editLabel', {
|
||||
defaultMessage: 'Edit configuration',
|
||||
})}
|
||||
size="s"
|
||||
className="lnsEditConfigurationFlyout"
|
||||
css={css`
|
||||
background: none;
|
||||
`}
|
||||
hideCloseButton
|
||||
>
|
||||
<Provider store={lensStore}>
|
||||
<LensEditConfigurationFlyout {...configPanelProps} />
|
||||
</Provider>
|
||||
</EuiFlyout>
|
||||
return getWrapper(
|
||||
<Provider store={lensStore}>
|
||||
<LensEditConfigurationFlyout {...configPanelProps} />
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -120,22 +120,22 @@ describe('LensEditConfigurationFlyout', () => {
|
|||
startDependencies,
|
||||
visualizationMap,
|
||||
datasourceMap,
|
||||
setIsFlyoutVisible: jest.fn(),
|
||||
closeFlyout: jest.fn(),
|
||||
datasourceId: 'testDatasource',
|
||||
} as unknown as EditConfigPanelProps;
|
||||
}
|
||||
|
||||
it('should call the setIsFlyout callback if collapse button is clicked', async () => {
|
||||
const setIsFlyoutVisibleSpy = jest.fn();
|
||||
it('should call the closeFlyout callback if collapse button is clicked', async () => {
|
||||
const closeFlyoutSpy = jest.fn();
|
||||
const props = getDefaultProps();
|
||||
const newProps = {
|
||||
...props,
|
||||
setIsFlyoutVisible: setIsFlyoutVisibleSpy,
|
||||
closeFlyout: closeFlyoutSpy,
|
||||
};
|
||||
const { instance } = await prepareAndMountComponent(newProps);
|
||||
expect(instance.find(EuiFlyoutBody).exists()).toBe(true);
|
||||
instance.find('[data-test-subj="collapseFlyoutButton"]').at(1).simulate('click');
|
||||
expect(setIsFlyoutVisibleSpy).toHaveBeenCalled();
|
||||
expect(closeFlyoutSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should compute the frame public api correctly', async () => {
|
||||
|
|
|
@ -43,7 +43,8 @@ export interface EditConfigPanelProps {
|
|||
startDependencies: LensPluginStartDependencies;
|
||||
visualizationMap: VisualizationMap;
|
||||
datasourceMap: DatasourceMap;
|
||||
setIsFlyoutVisible?: (flag: boolean) => void;
|
||||
closeFlyout?: () => void;
|
||||
wrapInFlyout?: boolean;
|
||||
datasourceId: 'formBased' | 'textBased';
|
||||
adaptersTables?: Record<string, Datatable>;
|
||||
}
|
||||
|
@ -57,7 +58,7 @@ export function LensEditConfigurationFlyout({
|
|||
datasourceMap,
|
||||
datasourceId,
|
||||
updateAll,
|
||||
setIsFlyoutVisible,
|
||||
closeFlyout,
|
||||
adaptersTables,
|
||||
}: EditConfigPanelProps) {
|
||||
const currentDataViewId = dataView.id ?? '';
|
||||
|
@ -112,10 +113,6 @@ export function LensEditConfigurationFlyout({
|
|||
};
|
||||
}, [activeData, dataViews, datasourceLayers, dateRange]);
|
||||
|
||||
const closeFlyout = () => {
|
||||
setIsFlyoutVisible?.(false);
|
||||
};
|
||||
|
||||
const layerPanelsProps = {
|
||||
framePublicAPI,
|
||||
datasourceMap,
|
||||
|
|
|
@ -50,3 +50,4 @@ 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/helpers';
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
AggregateQuery,
|
||||
TimeRange,
|
||||
isOfQueryType,
|
||||
getAggregateQueryMode,
|
||||
} from '@kbn/es-query';
|
||||
import type { PaletteOutput } from '@kbn/coloring';
|
||||
import {
|
||||
|
@ -127,8 +128,11 @@ import {
|
|||
} from '../app_plugin/get_application_user_messages';
|
||||
import { MessageList } from '../editor_frame_service/editor_frame/workspace_panel/message_list';
|
||||
import type { DocumentToExpressionReturnType } from '../editor_frame_service/editor_frame';
|
||||
import type { TypedLensByValueInput } from './embeddable_component';
|
||||
import type { LensPluginStartDependencies } from '../plugin';
|
||||
import { EmbeddableFeatureBadge } from './embeddable_info_badges';
|
||||
import { getDatasourceLayers } from '../state_management/utils';
|
||||
import type { EditLensConfigurationProps } from '../app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration';
|
||||
|
||||
export type LensSavedObjectAttributes = Omit<Document, 'savedObjectId' | 'type'>;
|
||||
|
||||
|
@ -718,6 +722,70 @@ export class Embeddable
|
|||
return this.fullAttributes;
|
||||
}
|
||||
|
||||
public isTextBasedLanguage() {
|
||||
if (!this.savedVis) {
|
||||
return;
|
||||
}
|
||||
const query = this.savedVis.state.query;
|
||||
return !isOfQueryType(query);
|
||||
}
|
||||
|
||||
public getTextBasedLanguage(): string | undefined {
|
||||
if (!this.isTextBasedLanguage() || !this.savedVis?.state.query) {
|
||||
return;
|
||||
}
|
||||
const query = this.savedVis?.state.query as unknown as AggregateQuery;
|
||||
const language = getAggregateQueryMode(query);
|
||||
return String(language).toUpperCase();
|
||||
}
|
||||
|
||||
async updateVisualization(datasourceState: unknown, visualizationState: unknown) {
|
||||
const viz = this.savedVis;
|
||||
const datasourceId = (this.activeDatasourceId ??
|
||||
'formBased') as EditLensConfigurationProps['datasourceId'];
|
||||
if (viz?.state) {
|
||||
const attrs = {
|
||||
...viz,
|
||||
state: {
|
||||
...viz.state,
|
||||
visualization: visualizationState,
|
||||
datasourceStates: {
|
||||
...viz.state.datasourceStates,
|
||||
[datasourceId]: datasourceState,
|
||||
},
|
||||
},
|
||||
};
|
||||
this.updateInput({ attributes: attrs });
|
||||
}
|
||||
}
|
||||
|
||||
async openConfingPanel(startDependencies: LensPluginStartDependencies) {
|
||||
const { getEditLensConfiguration } = await import('../async_services');
|
||||
const Component = getEditLensConfiguration(
|
||||
this.deps.coreStart,
|
||||
startDependencies,
|
||||
this.deps.visualizationMap,
|
||||
this.deps.datasourceMap
|
||||
);
|
||||
|
||||
const datasourceId = (this.activeDatasourceId ??
|
||||
'formBased') as EditLensConfigurationProps['datasourceId'];
|
||||
const attributes = this.savedVis as TypedLensByValueInput['attributes'];
|
||||
const dataView = this.dataViews[0];
|
||||
if (attributes) {
|
||||
return (
|
||||
<Component
|
||||
attributes={attributes}
|
||||
dataView={dataView}
|
||||
updateAll={this.updateVisualization.bind(this)}
|
||||
datasourceId={datasourceId}
|
||||
adaptersTables={this.lensInspector.adapters.tables?.tables}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async initializeSavedVis(input: LensEmbeddableInput) {
|
||||
const unwrapResult: LensUnwrapResult | false = await this.deps.attributeService
|
||||
.unwrapAttributes(input)
|
||||
|
@ -1346,7 +1414,7 @@ export class Embeddable
|
|||
this.updateOutput({
|
||||
defaultTitle: this.savedVis.title,
|
||||
defaultDescription: this.savedVis.description,
|
||||
editable: this.getIsEditable(),
|
||||
editable: this.getIsEditable() && !this.isTextBasedLanguage(),
|
||||
title,
|
||||
description,
|
||||
editPath: getEditPath(savedObjectId),
|
||||
|
|
|
@ -104,6 +104,7 @@ import type {
|
|||
} 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/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';
|
||||
|
@ -589,6 +590,13 @@ export class LensPlugin {
|
|||
visualizeAggBasedVisAction(core.application)
|
||||
);
|
||||
|
||||
const editInLensAction = new ConfigureInLensPanelAction(
|
||||
startDependencies,
|
||||
core.overlays,
|
||||
core.theme
|
||||
);
|
||||
startDependencies.uiActions.addTriggerAction('CONTEXT_MENU_TRIGGER', editInLensAction);
|
||||
|
||||
const discoverLocator = startDependencies.share?.url.locators.get('DISCOVER_APP_LOCATOR');
|
||||
if (discoverLocator) {
|
||||
startDependencies.uiActions.addTriggerAction(
|
||||
|
|
|
@ -30,6 +30,20 @@ import { getVisualizeFieldSuggestions } from '../editor_frame_service/editor_fra
|
|||
import type { FramePublicAPI, LensEditContextMapping, LensEditEvent } from '../types';
|
||||
import { selectDataViews, selectFramePublicAPI } from './selectors';
|
||||
import { onDropForVisualization } from '../editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils';
|
||||
import type { LensAppServices } from '../app_plugin/types';
|
||||
|
||||
const getQueryFromContext = (
|
||||
context: VisualizeFieldContext | VisualizeEditorContext,
|
||||
data: LensAppServices['data']
|
||||
) => {
|
||||
if ('searchQuery' in context && context.searchQuery) {
|
||||
return context.searchQuery;
|
||||
}
|
||||
if ('query' in context && context.query) {
|
||||
return context.query;
|
||||
}
|
||||
return data.query.queryString.getQuery();
|
||||
};
|
||||
|
||||
export const initialState: LensAppState = {
|
||||
persistedDoc: undefined,
|
||||
|
@ -93,16 +107,16 @@ export const getPreloadedState = ({
|
|||
};
|
||||
}
|
||||
|
||||
const query = !initialContext
|
||||
? data.query.queryString.getDefaultQuery()
|
||||
: getQueryFromContext(initialContext, data);
|
||||
|
||||
const state = {
|
||||
...initialState,
|
||||
isLoading: true,
|
||||
// Do not use app-specific filters from previous app,
|
||||
// only if Lens was opened with the intention to visualize a field (e.g. coming from Discover)
|
||||
query: !initialContext
|
||||
? data.query.queryString.getDefaultQuery()
|
||||
: 'searchQuery' in initialContext && initialContext.searchQuery
|
||||
? initialContext.searchQuery
|
||||
: (data.query.queryString.getQuery() as Query),
|
||||
query: query as Query,
|
||||
filters: !initialContext
|
||||
? data.query.filterManager.getGlobalFilters()
|
||||
: 'searchFilters' in initialContext && initialContext.searchFilters
|
||||
|
|
|
@ -10,8 +10,7 @@ import type { IEmbeddable } from '@kbn/embeddable-plugin/public';
|
|||
import type { DataViewsService } from '@kbn/data-views-plugin/public';
|
||||
import type { LocatorPublic } from '@kbn/share-plugin/public';
|
||||
import type { SerializableRecord } from '@kbn/utility-types';
|
||||
import type { Embeddable } from '../embeddable';
|
||||
import { DOC_TYPE } from '../../common/constants';
|
||||
import { isLensEmbeddable } from './utils';
|
||||
|
||||
interface DiscoverAppLocatorParams extends SerializableRecord {
|
||||
timeRange?: TimeRange;
|
||||
|
@ -33,10 +32,6 @@ interface Context {
|
|||
timeFieldName?: string;
|
||||
}
|
||||
|
||||
export function isLensEmbeddable(embeddable: IEmbeddable): embeddable is Embeddable {
|
||||
return embeddable.type === DOC_TYPE;
|
||||
}
|
||||
|
||||
export async function isCompatible({ hasDiscoverAccess, embeddable }: Context) {
|
||||
if (!hasDiscoverAccess) return false;
|
||||
try {
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import type { IEmbeddable } from '@kbn/embeddable-plugin/public';
|
||||
import { ActionExecutionContext } from '@kbn/ui-actions-plugin/public';
|
||||
import { overlayServiceMock } from '@kbn/core-overlays-browser-mocks';
|
||||
import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
|
||||
import type { LensPluginStartDependencies } from '../../plugin';
|
||||
import { createMockStartDependencies } from '../../editor_frame_service/mocks';
|
||||
import { DOC_TYPE } from '../../../common/constants';
|
||||
import { ConfigureInLensPanelAction } from './action';
|
||||
|
||||
describe('open config panel action', () => {
|
||||
const overlays = overlayServiceMock.createStartContract();
|
||||
const theme = themeServiceMock.createStartContract();
|
||||
const mockStartDependencies =
|
||||
createMockStartDependencies() as unknown as LensPluginStartDependencies;
|
||||
describe('compatibility check', () => {
|
||||
it('is incompatible with non-lens embeddables', async () => {
|
||||
const embeddable = {
|
||||
type: 'NOT_LENS',
|
||||
isTextBasedLanguage: () => true,
|
||||
} as unknown as IEmbeddable;
|
||||
const configurablePanelAction = new ConfigureInLensPanelAction(
|
||||
mockStartDependencies,
|
||||
overlays,
|
||||
theme
|
||||
);
|
||||
|
||||
const isCompatible = await configurablePanelAction.isCompatible({
|
||||
embeddable,
|
||||
} as ActionExecutionContext<{ embeddable: IEmbeddable }>);
|
||||
|
||||
expect(isCompatible).toBeFalsy();
|
||||
});
|
||||
|
||||
it('is incompatible with non text based language embeddable', async () => {
|
||||
const embeddable = {
|
||||
type: DOC_TYPE,
|
||||
isTextBasedLanguage: () => false,
|
||||
} as unknown as IEmbeddable;
|
||||
const configurablePanelAction = new ConfigureInLensPanelAction(
|
||||
mockStartDependencies,
|
||||
overlays,
|
||||
theme
|
||||
);
|
||||
|
||||
const isCompatible = await configurablePanelAction.isCompatible({
|
||||
embeddable,
|
||||
} as ActionExecutionContext<{ embeddable: IEmbeddable }>);
|
||||
|
||||
expect(isCompatible).toBeFalsy();
|
||||
});
|
||||
|
||||
it('is compatible with text based language embeddable', async () => {
|
||||
const embeddable = {
|
||||
type: DOC_TYPE,
|
||||
isTextBasedLanguage: () => true,
|
||||
} as unknown as IEmbeddable;
|
||||
const configurablePanelAction = new ConfigureInLensPanelAction(
|
||||
mockStartDependencies,
|
||||
overlays,
|
||||
theme
|
||||
);
|
||||
|
||||
const isCompatible = await configurablePanelAction.isCompatible({
|
||||
embeddable,
|
||||
} as ActionExecutionContext<{ embeddable: IEmbeddable }>);
|
||||
|
||||
expect(isCompatible).toBeTruthy();
|
||||
});
|
||||
});
|
||||
describe('execution', () => {
|
||||
it('opens flyout when executed', async () => {
|
||||
const embeddable = {
|
||||
type: DOC_TYPE,
|
||||
isTextBasedLanguage: () => true,
|
||||
openConfingPanel: jest.fn().mockResolvedValue(<span>Lens Config Panel Component</span>),
|
||||
getRoot: () => {
|
||||
return {
|
||||
openOverlay: jest.fn(),
|
||||
clearOverlays: jest.fn(),
|
||||
};
|
||||
},
|
||||
} as unknown as IEmbeddable;
|
||||
const configurablePanelAction = new ConfigureInLensPanelAction(
|
||||
mockStartDependencies,
|
||||
overlays,
|
||||
theme
|
||||
);
|
||||
const spy = jest.spyOn(overlays, 'openFlyout');
|
||||
|
||||
await configurablePanelAction.execute({
|
||||
embeddable,
|
||||
} as ActionExecutionContext<{ embeddable: IEmbeddable }>);
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 type { IEmbeddable } from '@kbn/embeddable-plugin/public';
|
||||
import { OverlayStart, ThemeServiceStart } from '@kbn/core/public';
|
||||
import { Action } from '@kbn/ui-actions-plugin/public';
|
||||
import type { LensPluginStartDependencies } from '../../plugin';
|
||||
import { isLensEmbeddable } from '../utils';
|
||||
|
||||
const ACTION_CONFIGURE_IN_LENS = 'ACTION_CONFIGURE_IN_LENS';
|
||||
|
||||
interface Context {
|
||||
embeddable: IEmbeddable;
|
||||
}
|
||||
|
||||
export const getConfigureLensHelpersAsync = async () => await import('../../async_services');
|
||||
|
||||
export class ConfigureInLensPanelAction implements Action<Context> {
|
||||
public type = ACTION_CONFIGURE_IN_LENS;
|
||||
public id = ACTION_CONFIGURE_IN_LENS;
|
||||
public order = 50;
|
||||
|
||||
constructor(
|
||||
protected readonly startDependencies: LensPluginStartDependencies,
|
||||
protected readonly overlays: OverlayStart,
|
||||
protected readonly theme: ThemeServiceStart
|
||||
) {}
|
||||
|
||||
public getDisplayName({ embeddable }: Context): string {
|
||||
const language = isLensEmbeddable(embeddable) ? embeddable.getTextBasedLanguage() : undefined;
|
||||
return i18n.translate('xpack.lens.app.editVisualizationLabel', {
|
||||
defaultMessage: 'Edit {lang} visualization',
|
||||
values: { lang: language },
|
||||
});
|
||||
}
|
||||
|
||||
public getIconType() {
|
||||
return 'pencil';
|
||||
}
|
||||
|
||||
public async isCompatible({ embeddable }: Context) {
|
||||
const { isActionCompatible } = await getConfigureLensHelpersAsync();
|
||||
return isActionCompatible(embeddable);
|
||||
}
|
||||
|
||||
public async execute({ embeddable }: Context) {
|
||||
const { executeAction } = await getConfigureLensHelpersAsync();
|
||||
return executeAction({
|
||||
embeddable,
|
||||
startDependencies: this.startDependencies,
|
||||
overlays: this.overlays,
|
||||
theme: this.theme,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import type { IEmbeddable } from '@kbn/embeddable-plugin/public';
|
||||
import type { OverlayRef, OverlayStart, ThemeServiceStart } from '@kbn/core/public';
|
||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
|
||||
import { isLensEmbeddable } from '../utils';
|
||||
import type { LensPluginStartDependencies } from '../../plugin';
|
||||
|
||||
interface Context {
|
||||
embeddable: IEmbeddable;
|
||||
startDependencies: LensPluginStartDependencies;
|
||||
overlays: OverlayStart;
|
||||
theme: ThemeServiceStart;
|
||||
}
|
||||
|
||||
interface TracksOverlays {
|
||||
openOverlay: (ref: OverlayRef) => void;
|
||||
clearOverlays: () => void;
|
||||
}
|
||||
|
||||
function tracksOverlays(root: unknown): root is TracksOverlays {
|
||||
return Boolean((root as TracksOverlays).openOverlay && (root as TracksOverlays).clearOverlays);
|
||||
}
|
||||
|
||||
export async function isActionCompatible(embeddable: IEmbeddable) {
|
||||
return Boolean(isLensEmbeddable(embeddable) && embeddable.isTextBasedLanguage());
|
||||
}
|
||||
|
||||
export async function executeAction({ embeddable, startDependencies, overlays, theme }: Context) {
|
||||
const isCompatibleAction = await isActionCompatible(embeddable);
|
||||
if (!isCompatibleAction || !isLensEmbeddable(embeddable)) {
|
||||
throw new IncompatibleActionError();
|
||||
}
|
||||
const rootEmbeddable = embeddable.getRoot();
|
||||
const overlayTracker = tracksOverlays(rootEmbeddable) ? rootEmbeddable : undefined;
|
||||
const ConfigPanel = await embeddable.openConfingPanel(startDependencies);
|
||||
if (ConfigPanel) {
|
||||
const handle = overlays.openFlyout(
|
||||
toMountPoint(
|
||||
React.cloneElement(ConfigPanel, {
|
||||
closeFlyout: () => {
|
||||
if (overlayTracker) overlayTracker.clearOverlays();
|
||||
handle.close();
|
||||
},
|
||||
}),
|
||||
{
|
||||
theme$: theme.theme$,
|
||||
}
|
||||
),
|
||||
{
|
||||
size: 's',
|
||||
'data-test-subj': 'customizeLens',
|
||||
type: 'push',
|
||||
hideCloseButton: true,
|
||||
onClose: (overlayRef) => {
|
||||
if (overlayTracker) overlayTracker.clearOverlays();
|
||||
overlayRef.close();
|
||||
},
|
||||
outsideClickCloses: true,
|
||||
}
|
||||
);
|
||||
overlayTracker?.openOverlay(handle);
|
||||
}
|
||||
}
|
13
x-pack/plugins/lens/public/trigger_actions/utils.ts
Normal file
13
x-pack/plugins/lens/public/trigger_actions/utils.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 { IEmbeddable } from '@kbn/embeddable-plugin/public';
|
||||
import type { Embeddable } from '../embeddable';
|
||||
import { DOC_TYPE } from '../../common/constants';
|
||||
|
||||
export function isLensEmbeddable(embeddable: IEmbeddable): embeddable is Embeddable {
|
||||
return embeddable.type === DOC_TYPE;
|
||||
}
|
|
@ -78,7 +78,9 @@
|
|||
"@kbn/core-notifications-browser-mocks",
|
||||
"@kbn/core-saved-objects-utils-server",
|
||||
"@kbn/core-lifecycle-browser-mocks",
|
||||
"@kbn/unified-field-list"
|
||||
"@kbn/unified-field-list",
|
||||
"@kbn/core-overlays-browser-mocks",
|
||||
"@kbn/core-theme-browser-mocks"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue