-
Notifications
You must be signed in to change notification settings - Fork 8.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Embeddable Rebuild] [Controls] Remove non-React controls from controls
plugin
#192017
Changes from 1 commit
d1fefd8
0f2ad3f
79b7a09
f318f75
2859409
7b1707c
c3d1f0e
651fbc8
8c09d88
a814f2c
ed106ab
c75aeb9
970347d
2f90dcc
049851f
27596ab
ddfc7f3
adf1eb9
0844191
9ea6f56
bc0710a
a8514e2
0f6d3a6
641bc89
12ed334
156fcf2
0fd22e7
7fc7df7
e0694ab
2238540
a337f11
bbfcdf1
3640f0c
e51ca08
eb2517a
dbc461e
69ed309
6c714c8
3917c17
62c217c
2084639
b8ed89b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import React, { SyntheticEvent } from 'react'; | ||
|
||
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; | ||
import { apiIsPresentationContainer, PresentationContainer } from '@kbn/presentation-containers'; | ||
import { | ||
apiCanAccessViewMode, | ||
apiHasParentApi, | ||
apiHasType, | ||
apiHasUniqueId, | ||
apiIsOfType, | ||
EmbeddableApiContext, | ||
HasParentApi, | ||
HasType, | ||
HasUniqueId, | ||
} from '@kbn/presentation-publishing'; | ||
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; | ||
|
||
import { ACTION_CLEAR_CONTROL } from '.'; | ||
import { CanClearSelections, isClearableControl } from '../../types'; | ||
import { ControlGroupStrings } from '../control_group_strings'; | ||
import { CONTROL_GROUP_TYPE } from '../types'; | ||
|
||
export type ClearControlActionApi = HasType & | ||
HasUniqueId & | ||
CanClearSelections & | ||
HasParentApi<PresentationContainer & HasType>; | ||
|
||
const isApiCompatible = (api: unknown | null): api is ClearControlActionApi => | ||
Boolean( | ||
apiHasType(api) && | ||
apiHasUniqueId(api) && | ||
isClearableControl(api) && | ||
apiHasParentApi(api) && | ||
apiCanAccessViewMode(api.parentApi) && | ||
apiIsOfType(api.parentApi, CONTROL_GROUP_TYPE) && | ||
apiIsPresentationContainer(api.parentApi) | ||
); | ||
|
||
export class ClearControlAction implements Action<EmbeddableApiContext> { | ||
public readonly type = ACTION_CLEAR_CONTROL; | ||
public readonly id = ACTION_CLEAR_CONTROL; | ||
public order = 1; | ||
|
||
constructor() {} | ||
|
||
public readonly MenuItem = ({ context }: { context: EmbeddableApiContext }) => { | ||
if (!isApiCompatible(context.embeddable)) throw new IncompatibleActionError(); | ||
|
||
return ( | ||
<EuiToolTip content={this.getDisplayName(context)}> | ||
<EuiButtonIcon | ||
data-test-subj={`control-action-${context.embeddable.uuid}-erase`} | ||
aria-label={this.getDisplayName(context)} | ||
iconType={this.getIconType(context)} | ||
onClick={(event: SyntheticEvent<HTMLButtonElement>) => { | ||
(event.target as HTMLButtonElement).blur(); | ||
this.execute(context); | ||
}} | ||
color="text" | ||
/> | ||
</EuiToolTip> | ||
); | ||
}; | ||
|
||
public getDisplayName({ embeddable }: EmbeddableApiContext) { | ||
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); | ||
return ControlGroupStrings.floatingActions.getClearButtonTitle(); | ||
} | ||
|
||
public getIconType({ embeddable }: EmbeddableApiContext) { | ||
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); | ||
return 'eraser'; | ||
} | ||
|
||
public async isCompatible({ embeddable }: EmbeddableApiContext) { | ||
return isApiCompatible(embeddable); | ||
} | ||
|
||
public async execute({ embeddable }: EmbeddableApiContext) { | ||
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); | ||
embeddable.clearSelections(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import { ErrorEmbeddable } from '@kbn/embeddable-plugin/public'; | ||
|
||
import { OPTIONS_LIST_CONTROL } from '../../../common'; | ||
import { ControlOutput } from '../../types'; | ||
import { ControlGroupInput } from '../types'; | ||
import { pluginServices } from '../../services'; | ||
import { DeleteControlAction } from './delete_control_action'; | ||
import { OptionsListEmbeddableInput } from '../../options_list'; | ||
import { controlGroupInputBuilder } from '../external_api/control_group_input_builder'; | ||
import { ControlGroupContainer } from '../embeddable/control_group_container'; | ||
import { OptionsListEmbeddableFactory } from '../../options_list/embeddable/options_list_embeddable_factory'; | ||
import { OptionsListEmbeddable } from '../../options_list/embeddable/options_list_embeddable'; | ||
import { mockedReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public/mocks'; | ||
|
||
let container: ControlGroupContainer; | ||
let embeddable: OptionsListEmbeddable; | ||
|
||
beforeAll(async () => { | ||
pluginServices.getServices().controls.getControlFactory = jest | ||
.fn() | ||
.mockImplementation((type: string) => { | ||
if (type === OPTIONS_LIST_CONTROL) return new OptionsListEmbeddableFactory(); | ||
}); | ||
|
||
const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput; | ||
controlGroupInputBuilder.addOptionsListControl(controlGroupInput, { | ||
dataViewId: 'test-data-view', | ||
title: 'test', | ||
fieldName: 'test-field', | ||
width: 'medium', | ||
grow: false, | ||
}); | ||
container = new ControlGroupContainer(mockedReduxEmbeddablePackage, controlGroupInput); | ||
await container.untilInitialized(); | ||
|
||
embeddable = container.getChild(container.getChildIds()[0]); | ||
expect(embeddable.type).toBe(OPTIONS_LIST_CONTROL); | ||
}); | ||
|
||
test('Action is incompatible with Error Embeddables', async () => { | ||
Heenawter marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const deleteControlAction = new DeleteControlAction(); | ||
const errorEmbeddable = new ErrorEmbeddable('Wow what an awful error', { id: ' 404' }); | ||
expect(await deleteControlAction.isCompatible({ embeddable: errorEmbeddable as any })).toBe( | ||
false | ||
); | ||
}); | ||
|
||
test('Execute throws an error when called with an embeddable not in a parent', async () => { | ||
const deleteControlAction = new DeleteControlAction(); | ||
const optionsListEmbeddable = new OptionsListEmbeddable( | ||
mockedReduxEmbeddablePackage, | ||
{} as OptionsListEmbeddableInput, | ||
{} as ControlOutput | ||
); | ||
await expect(async () => { | ||
await deleteControlAction.execute({ embeddable: optionsListEmbeddable }); | ||
}).rejects.toThrow(Error); | ||
}); | ||
|
||
describe('Execute should open a confirm modal', () => { | ||
test('Canceling modal will keep control', async () => { | ||
const spyOn = jest.fn().mockResolvedValue(false); | ||
pluginServices.getServices().overlays.openConfirm = spyOn; | ||
|
||
const deleteControlAction = new DeleteControlAction(); | ||
await deleteControlAction.execute({ embeddable }); | ||
expect(spyOn).toHaveBeenCalled(); | ||
|
||
expect(container.getPanelCount()).toBe(1); | ||
}); | ||
|
||
test('Confirming modal will delete control', async () => { | ||
const spyOn = jest.fn().mockResolvedValue(true); | ||
pluginServices.getServices().overlays.openConfirm = spyOn; | ||
|
||
const deleteControlAction = new DeleteControlAction(); | ||
await deleteControlAction.execute({ embeddable }); | ||
expect(spyOn).toHaveBeenCalled(); | ||
|
||
expect(container.getPanelCount()).toBe(0); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import React from 'react'; | ||
|
||
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; | ||
import { ViewMode } from '@kbn/embeddable-plugin/public'; | ||
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; | ||
|
||
import { apiIsPresentationContainer, PresentationContainer } from '@kbn/presentation-containers'; | ||
import { | ||
apiCanAccessViewMode, | ||
apiHasParentApi, | ||
apiHasType, | ||
apiHasUniqueId, | ||
apiIsOfType, | ||
EmbeddableApiContext, | ||
getInheritedViewMode, | ||
HasParentApi, | ||
HasType, | ||
HasUniqueId, | ||
PublishesViewMode, | ||
} from '@kbn/presentation-publishing'; | ||
import { ACTION_DELETE_CONTROL } from '.'; | ||
import { pluginServices } from '../../services'; | ||
import { ControlGroupStrings } from '../control_group_strings'; | ||
import { CONTROL_GROUP_TYPE } from '../types'; | ||
|
||
export type DeleteControlActionApi = HasType & | ||
HasUniqueId & | ||
HasParentApi<PresentationContainer & PublishesViewMode & HasType>; | ||
|
||
const isApiCompatible = (api: unknown | null): api is DeleteControlActionApi => | ||
Boolean( | ||
apiHasType(api) && | ||
apiHasUniqueId(api) && | ||
apiHasParentApi(api) && | ||
apiCanAccessViewMode(api.parentApi) && | ||
apiIsOfType(api.parentApi, CONTROL_GROUP_TYPE) && | ||
apiIsPresentationContainer(api.parentApi) | ||
); | ||
|
||
export class DeleteControlAction implements Action<EmbeddableApiContext> { | ||
public readonly type = ACTION_DELETE_CONTROL; | ||
public readonly id = ACTION_DELETE_CONTROL; | ||
public order = 100; // should always be last | ||
|
||
private openConfirm; | ||
|
||
constructor() { | ||
({ | ||
overlays: { openConfirm: this.openConfirm }, | ||
} = pluginServices.getServices()); | ||
} | ||
|
||
public readonly MenuItem = ({ context }: { context: EmbeddableApiContext }) => { | ||
if (!isApiCompatible(context.embeddable)) throw new IncompatibleActionError(); | ||
|
||
return ( | ||
<EuiToolTip content={this.getDisplayName(context)}> | ||
<EuiButtonIcon | ||
data-test-subj={`control-action-${context.embeddable.uuid}-delete`} | ||
aria-label={this.getDisplayName(context)} | ||
iconType={this.getIconType(context)} | ||
onClick={() => this.execute(context)} | ||
color="danger" | ||
/> | ||
</EuiToolTip> | ||
); | ||
}; | ||
|
||
public getDisplayName({ embeddable }: EmbeddableApiContext) { | ||
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); | ||
return ControlGroupStrings.floatingActions.getRemoveButtonTitle(); | ||
} | ||
|
||
public getIconType({ embeddable }: EmbeddableApiContext) { | ||
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); | ||
return 'trash'; | ||
} | ||
|
||
public async isCompatible({ embeddable }: EmbeddableApiContext) { | ||
return ( | ||
isApiCompatible(embeddable) && getInheritedViewMode(embeddable.parentApi) === ViewMode.EDIT | ||
); | ||
} | ||
|
||
public async execute({ embeddable }: EmbeddableApiContext) { | ||
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); | ||
|
||
this.openConfirm(ControlGroupStrings.management.deleteControls.getSubtitle(), { | ||
confirmButtonText: ControlGroupStrings.management.deleteControls.getConfirm(), | ||
cancelButtonText: ControlGroupStrings.management.deleteControls.getCancel(), | ||
title: ControlGroupStrings.management.deleteControls.getDeleteTitle(), | ||
buttonColor: 'danger', | ||
}).then((confirmed) => { | ||
if (confirmed) { | ||
embeddable.parentApi.removePanel(embeddable.uuid); | ||
} | ||
}); | ||
} | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This file was refactored to support the new types and then moved from Note that this action used to be under the |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import { ErrorEmbeddable } from '@kbn/embeddable-plugin/public'; | ||
|
||
import { EditControlAction } from './edit_control_action'; | ||
|
||
test('Action is incompatible with Error Embeddables', async () => { | ||
const editControlAction = new EditControlAction(); | ||
const errorEmbeddable = new ErrorEmbeddable('Wow what an awful error', { id: ' 404' }); | ||
expect(await editControlAction.isCompatible({ embeddable: errorEmbeddable as any })).toBe(false); | ||
}); | ||
|
||
test('Action is incompatible with embeddables that are not editable', async () => { | ||
// const mockEmbeddableFactory = new TimeSliderEmbeddableFactory(); | ||
// const mockGetFactory = jest.fn().mockReturnValue(mockEmbeddableFactory); | ||
// pluginServices.getServices().controls.getControlFactory = mockGetFactory; | ||
// pluginServices.getServices().embeddable.getEmbeddableFactory = mockGetFactory; | ||
// const editControlAction = new EditControlAction(); | ||
// const emptyContainer = new ControlGroupContainer(mockedReduxEmbeddablePackage, controlGroupInput); | ||
// await emptyContainer.untilInitialized(); | ||
// await emptyContainer.addTimeSliderControl(); | ||
// expect( | ||
// await editControlAction.isCompatible({ | ||
// embeddable: emptyContainer.getChild(emptyContainer.getChildIds()[0]) as any, | ||
// }) | ||
// ).toBe(false); | ||
}); | ||
|
||
test('Action is compatible with embeddables that are editable', async () => { | ||
// const mockEmbeddableFactory = new OptionsListEmbeddableFactory(); | ||
// const mockGetFactory = jest.fn().mockReturnValue(mockEmbeddableFactory); | ||
// pluginServices.getServices().controls.getControlFactory = mockGetFactory; | ||
// pluginServices.getServices().embeddable.getEmbeddableFactory = mockGetFactory; | ||
// const editControlAction = new EditControlAction(); | ||
// const emptyContainer = new ControlGroupContainer(mockedReduxEmbeddablePackage, controlGroupInput); | ||
// await emptyContainer.untilInitialized(); | ||
// const control = await emptyContainer.addOptionsListControl({ | ||
// dataViewId: 'test-data-view', | ||
// title: 'test', | ||
// fieldName: 'test-field', | ||
// width: 'medium', | ||
// grow: false, | ||
// }); | ||
// expect(emptyContainer.getInput().panels[control.getInput().id].type).toBe(OPTIONS_LIST_CONTROL); | ||
// expect( | ||
// await editControlAction.isCompatible({ | ||
// embeddable: emptyContainer.getChild(emptyContainer.getChildIds()[0]) as any, | ||
// }) | ||
// ).toBe(true); | ||
}); | ||
|
||
test('Execute throws an error when called with an embeddable not in a parent', async () => { | ||
// const editControlAction = new EditControlAction(); | ||
// const optionsListEmbeddable = new OptionsListEmbeddable( | ||
// mockedReduxEmbeddablePackage, | ||
// {} as OptionsListEmbeddableInput, | ||
// {} as ControlOutput | ||
// ); | ||
// await expect(async () => { | ||
// await editControlAction.execute({ embeddable: optionsListEmbeddable }); | ||
// }).rejects.toThrow(Error); | ||
}); | ||
|
||
test('Execute should open a flyout', async () => { | ||
// const spyOn = jest.fn().mockResolvedValue(undefined); | ||
// pluginServices.getServices().overlays.openFlyout = spyOn; | ||
// const emptyContainer = new ControlGroupContainer(mockedReduxEmbeddablePackage, controlGroupInput); | ||
// await emptyContainer.untilInitialized(); | ||
// const control = (await emptyContainer.addOptionsListControl({ | ||
// dataViewId: 'test-data-view', | ||
// title: 'test', | ||
// fieldName: 'test-field', | ||
// width: 'medium', | ||
// grow: false, | ||
// })) as OptionsListEmbeddable; | ||
// expect(emptyContainer.getInput().panels[control.getInput().id].type).toBe(OPTIONS_LIST_CONTROL); | ||
// const editControlAction = new EditControlAction(deleteControlAction); | ||
// await editControlAction.execute({ embeddable: control }); | ||
// expect(spyOn).toHaveBeenCalled(); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file was refactored to support the new types and then moved from
src/plugins/controls/public/control_group/actions
tosrc/plugins/controls/public/actions
- because of this, GitHub is reporting it as an entirely new file 🤷 This is not actually a new test