Skip to content

Commit

Permalink
Merge branch 'main' into 152438-improve-preview-loading-state
Browse files Browse the repository at this point in the history
  • Loading branch information
maryam-saeidi authored Mar 21, 2023
2 parents 40bb66e + 16de10b commit 4292bdc
Show file tree
Hide file tree
Showing 193 changed files with 4,060 additions and 1,391 deletions.
63 changes: 58 additions & 5 deletions examples/controls_example/public/edit_example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
* Side Public License, v 1.
*/

import { pickBy } from 'lodash';
import React, { useState } from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiButtonGroup,
EuiFlexGroup,
EuiFlexItem,
EuiLoadingContent,
Expand All @@ -19,8 +21,13 @@ import {
EuiTitle,
} from '@elastic/eui';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { LazyControlGroupRenderer, ControlGroupContainer } from '@kbn/controls-plugin/public';
import {
LazyControlGroupRenderer,
ControlGroupContainer,
ControlGroupInput,
} from '@kbn/controls-plugin/public';
import { withSuspense } from '@kbn/presentation-util-plugin/public';
import { ACTION_EDIT_CONTROL, ACTION_DELETE_CONTROL } from '@kbn/controls-plugin/public';

const ControlGroupRenderer = withSuspense(LazyControlGroupRenderer);

Expand All @@ -30,6 +37,27 @@ export const EditExample = () => {
const [isSaving, setIsSaving] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [controlGroup, setControlGroup] = useState<ControlGroupContainer>();
const [toggleIconIdToSelectedMapIcon, setToggleIconIdToSelectedMapIcon] = useState<{
[id: string]: boolean;
}>({});

function onChangeIconsMultiIcons(optionId: string) {
const newToggleIconIdToSelectedMapIcon = {
...toggleIconIdToSelectedMapIcon,
...{
[optionId]: !toggleIconIdToSelectedMapIcon[optionId],
},
};

if (controlGroup) {
const disabledActions: string[] = Object.keys(
pickBy(newToggleIconIdToSelectedMapIcon, (value) => value)
);
controlGroup.updateInput({ disabledActions });
}

setToggleIconIdToSelectedMapIcon(newToggleIconIdToSelectedMapIcon);
}

async function onSave() {
setIsSaving(true);
Expand All @@ -48,16 +76,20 @@ export const EditExample = () => {
// simulated async load await
await new Promise((resolve) => setTimeout(resolve, 1000));

let input = {};
let input: Partial<ControlGroupInput> = {};
const inputAsString = localStorage.getItem(INPUT_KEY);
if (inputAsString) {
try {
input = JSON.parse(inputAsString);
const disabledActions = input.disabledActions ?? [];
setToggleIconIdToSelectedMapIcon({
[ACTION_EDIT_CONTROL]: disabledActions.includes(ACTION_EDIT_CONTROL),
[ACTION_DELETE_CONTROL]: disabledActions.includes(ACTION_DELETE_CONTROL),
});
} catch (e) {
// ignore parse errors
}
}

setIsLoading(false);
return input;
}
Expand All @@ -72,7 +104,7 @@ export const EditExample = () => {
</EuiText>
<EuiSpacer size="m" />
<EuiPanel hasBorder={true}>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexGroup gutterSize="m" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
color="primary"
Expand All @@ -85,11 +117,32 @@ export const EditExample = () => {
Add control
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={true}>
<EuiButtonGroup
legend="Text style"
buttonSize="m"
options={[
{
id: ACTION_EDIT_CONTROL,
label: 'Disable edit action',
value: ACTION_EDIT_CONTROL,
},
{
id: ACTION_DELETE_CONTROL,
label: 'Disable delete action',
value: ACTION_DELETE_CONTROL,
},
]}
idToSelectedMap={toggleIconIdToSelectedMapIcon}
type="multi"
onChange={(id: string) => onChangeIconsMultiIcons(id)}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill
color="primary"
isDisabled={controlGroup === undefined || isSaving}
fill
onClick={onSave}
isLoading={isSaving}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"osquery-pack": "edd84b2c59ef36214ece0676706da8f22175c660",
"osquery-pack-asset": "18e08979d46ee7e5538f54c080aec4d8c58516ca",
"osquery-saved-query": "f5e4e303f65c7607248ea8b2672f1ee30e4fb15e",
"query": "f94de164936da788e9215c0e9b824f8b948ea859",
"query": "ec6000b775f06f81470df42d23f7a88cb31d64ba",
"rules-settings": "9854495c3b54b16a6625fb250c35e5504da72266",
"sample-data-telemetry": "c38daf1a49ed24f2a4fb091e6e1e833fccf19935",
"search": "01bc42d635e9ea0588741c4c7a2bbd3feb3ac5dc",
Expand Down
7 changes: 3 additions & 4 deletions src/plugins/controls/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@
"embeddable",
"dataViews",
"data",
"unifiedSearch"
"unifiedSearch",
"uiActions"
],
"extraPublicDirs": [
"common"
]
"extraPublicDirs": ["common"]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* 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 {
lazyLoadReduxEmbeddablePackage,
ReduxEmbeddablePackage,
} from '@kbn/presentation-util-plugin/public';
import { ErrorEmbeddable } from '@kbn/embeddable-plugin/public';

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 '../control_group_input_builder';
import { ControlGroupContainer } from '../embeddable/control_group_container';
import { OptionsListEmbeddable } from '../../options_list/embeddable/options_list_embeddable';

let container: ControlGroupContainer;
let embeddable: OptionsListEmbeddable;
let reduxEmbeddablePackage: ReduxEmbeddablePackage;

beforeAll(async () => {
reduxEmbeddablePackage = await lazyLoadReduxEmbeddablePackage();

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(reduxEmbeddablePackage, controlGroupInput);
await container.untilInitialized();

embeddable = container.getChild(container.getChildIds()[0]);
});

test('Action is incompatible with Error Embeddables', async () => {
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(
reduxEmbeddablePackage,
{} 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,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 from 'react';

import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import { ViewMode, isErrorEmbeddable } from '@kbn/embeddable-plugin/public';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';

import { ACTION_DELETE_CONTROL } from '.';
import { pluginServices } from '../../services';
import { ControlGroupStrings } from '../control_group_strings';
import { ControlEmbeddable, DataControlInput } from '../../types';
import { isControlGroup } from '../embeddable/control_group_helpers';

export interface DeleteControlActionContext {
embeddable: ControlEmbeddable<DataControlInput>;
}

export class DeleteControlAction implements Action<DeleteControlActionContext> {
public readonly type = ACTION_DELETE_CONTROL;
public readonly id = ACTION_DELETE_CONTROL;
public order = 2;

private openConfirm;

constructor() {
({
overlays: { openConfirm: this.openConfirm },
} = pluginServices.getServices());
}

public readonly MenuItem = ({ context }: { context: DeleteControlActionContext }) => {
return (
<EuiToolTip content={this.getDisplayName(context)}>
<EuiButtonIcon
data-test-subj={`control-action-${context.embeddable.id}-delete`}
aria-label={this.getDisplayName(context)}
iconType={this.getIconType(context)}
onClick={() => this.execute(context)}
color="danger"
/>
</EuiToolTip>
);
};

public getDisplayName({ embeddable }: DeleteControlActionContext) {
if (!embeddable.parent || !isControlGroup(embeddable.parent)) {
throw new IncompatibleActionError();
}
return ControlGroupStrings.floatingActions.getRemoveButtonTitle();
}

public getIconType({ embeddable }: DeleteControlActionContext) {
if (!embeddable.parent || !isControlGroup(embeddable.parent)) {
throw new IncompatibleActionError();
}
return 'cross';
}

public async isCompatible({ embeddable }: DeleteControlActionContext) {
if (isErrorEmbeddable(embeddable)) return false;
const controlGroup = embeddable.parent;
return Boolean(
controlGroup &&
isControlGroup(controlGroup) &&
controlGroup.getInput().viewMode === ViewMode.EDIT
);
}

public async execute({ embeddable }: DeleteControlActionContext) {
if (!embeddable.parent || !isControlGroup(embeddable.parent)) {
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.parent?.removeEmbeddable(embeddable.id);
}
});
}
}
Loading

0 comments on commit 4292bdc

Please sign in to comment.